From c468dadbca168ce768882399ca06b87b47c0adbe Mon Sep 17 00:00:00 2001 From: "anton.v.dodonov" Date: Sun, 27 Apr 2025 15:56:11 +0000 Subject: [PATCH 01/24] refactoring --- .vscode/settings.json | 4 + lib/main.dart | 22 +-- lib/models/nav_item.dart | 8 + lib/nav_bar.dart | 79 +++----- lib/pages/main/location_widget.dart | 61 +++++- lib/pages/main/main_btn.dart | 190 +++++------------- lib/pages/main/main_page.dart | 64 +++--- lib/pages/main/stat_bar.dart | 59 +++--- lib/pages/servers/servers_list.dart | 240 +++++------------------ lib/pages/servers/servers_list_item.dart | 32 ++- lib/pages/servers/servers_page.dart | 28 +-- lib/pages/settings/settings_page.dart | 10 + lib/pages/speed/speed_page.dart | 10 + lib/providers/vpn_provider.dart | 190 ++++++++++++++++++ lib/search_dialog.dart | 125 +----------- pubspec.lock | 16 +- 16 files changed, 512 insertions(+), 626 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 lib/models/nav_item.dart create mode 100644 lib/pages/settings/settings_page.dart create mode 100644 lib/pages/speed/speed_page.dart create mode 100644 lib/providers/vpn_provider.dart 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/lib/main.dart b/lib/main.dart index d9b35be..5a685f4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,15 +3,16 @@ import 'package:provider/provider.dart'; import 'package:vpn_client/pages/apps/apps_page.dart'; import 'package:vpn_client/pages/main/main_page.dart'; import 'package:vpn_client/pages/servers/servers_page.dart'; +import 'package:vpn_client/pages/settings/settings_page.dart'; +import 'package:vpn_client/pages/speed/speed_page.dart'; +import 'package:vpn_client/providers/vpn_provider.dart'; import 'package:vpn_client/theme_provider.dart'; import 'design/colors.dart'; import 'nav_bar.dart'; void main() { - runApp( - ChangeNotifierProvider(create: (_) => ThemeProvider(), child: const App()), - ); + runApp(MultiProvider(providers: [ChangeNotifierProvider(create: (_) => ThemeProvider()), ChangeNotifierProvider(create: (_) => VPNProvider())], child: const App())); } class App extends StatelessWidget { @@ -50,8 +51,8 @@ class _MainScreenState extends State { const AppsPage(), ServersPage(onNavBarTap: _handleNavBarTap), const MainPage(), - const PlaceholderPage(text: 'Speed Page'), - const PlaceholderPage(text: 'Settings Page'), + const SpeedPage(), + const SettingsPage(), ]; } @@ -68,17 +69,8 @@ class _MainScreenState extends State { bottomNavigationBar: NavBar( initialIndex: _currentIndex, onItemTapped: _handleNavBarTap, + selectedColor: Theme.of(context).colorScheme.primary, ), ); } } - -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/nav_bar.dart b/lib/nav_bar.dart index 8418f50..b9073e1 100644 --- a/lib/nav_bar.dart +++ b/lib/nav_bar.dart @@ -1,70 +1,55 @@ import 'package:flutter/material.dart'; import 'design/images.dart'; +import 'package:vpn_client/models/nav_item.dart'; -class NavBar extends StatefulWidget { +class NavBar extends StatelessWidget { final int initialIndex; final Function(int) onItemTapped; + final Color selectedColor; - const NavBar({super.key, this.initialIndex = 2, required this.onItemTapped}); - - @override - State createState() => NavBarState(); -} - -class NavBarState extends State { - late int _selectedIndex; - - final List _inactiveIcons = [ - appIcon, - serverIcon, - homeIcon, - speedIcon, - settingsIcon, - ]; - - final List _activeIcons = [ - activeAppIcon, - activeServerIcon, - activeHomeIcon, - speedIcon, - settingsIcon, - ]; - - @override - void initState() { - super.initState(); - _selectedIndex = widget.initialIndex; - } - - void _onItemTapped(int index) { - setState(() { - _selectedIndex = index; - }); - widget.onItemTapped(index); - } + const NavBar({ + super.key, + this.initialIndex = 2, + required this.onItemTapped, + required this.selectedColor, + }); @override Widget build(BuildContext context) { + final List navItems = [ + NavItem(inactiveIcon: appIcon, activeIcon: activeAppIcon), + NavItem(inactiveIcon: serverIcon, activeIcon: activeServerIcon), + NavItem(inactiveIcon: homeIcon, activeIcon: activeHomeIcon), + NavItem(inactiveIcon: speedIcon, activeIcon: speedIcon), + NavItem(inactiveIcon: settingsIcon, activeIcon: settingsIcon), + ]; + return Container( alignment: Alignment.center, width: MediaQuery.of(context).size.width, height: 60, margin: const EdgeInsets.only(bottom: 30), padding: const EdgeInsets.symmetric(horizontal: 30), - decoration: BoxDecoration(color: Theme.of(context).colorScheme.surface), + decoration: + BoxDecoration(color: Theme.of(context).colorScheme.surface), child: Row( - children: List.generate(_inactiveIcons.length, (index) { - bool isActive = _selectedIndex == index; + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: List.generate(navItems.length, (index) { + bool isActive = initialIndex == index; return GestureDetector( - onTap: () => _onItemTapped(index), - child: SizedBox( - width: (MediaQuery.of(context).size.width-60)/5, - child: AnimatedContainer( + onTap: () => onItemTapped(index), + child: AnimatedContainer( duration: const Duration(milliseconds: 200), curve: Curves.easeInOut, padding: const EdgeInsets.all(8), - child: isActive ? _activeIcons[index] : _inactiveIcons[index], - ),) + child: isActive + ? ColorFiltered( + colorFilter: ColorFilter.mode( + selectedColor, BlendMode.srcIn), + child: navItems[index].activeIcon, + ) + : navItems[index].inactiveIcon, + ), ); }), ), diff --git a/lib/pages/main/location_widget.dart b/lib/pages/main/location_widget.dart index fb79cf3..dd037d5 100644 --- a/lib/pages/main/location_widget.dart +++ b/lib/pages/main/location_widget.dart @@ -1,20 +1,67 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.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), diff --git a/lib/pages/main/main_btn.dart b/lib/pages/main/main_btn.dart index e3bac53..42f6e54 100644 --- a/lib/pages/main/main_btn.dart +++ b/lib/pages/main/main_btn.dart @@ -1,35 +1,19 @@ -import 'dart:async'; -import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:vpn_client/design/colors.dart'; -import 'package:vpn_client/design/dimensions.dart'; -import 'package:vpnclient_engine_flutter/vpnclient_engine_flutter.dart'; - -/// -import 'package:flutter_v2ray/flutter_v2ray.dart'; - -final FlutterV2ray flutterV2ray = FlutterV2ray( - onStatusChanged: (status) { - // do something - }, -); -/// - class MainBtn extends StatefulWidget { - const MainBtn({super.key}); + final String title; + final VoidCallback onPressed; + final String connectionTime; + final String connectionStatus; + const MainBtn({super.key, required this.title, required this.onPressed, required this.connectionTime, required this.connectionStatus}); @override State createState() => MainBtnState(); } class MainBtnState extends State with SingleTickerProviderStateMixin { - ///static const platform = MethodChannel('vpnclient_engine2'); - - String connectionStatus = connectionStatusDisconnected; - String connectionTime = "00:00:00"; - Timer? _timer; late AnimationController _animationController; late Animation _sizeAnimation; @@ -44,151 +28,48 @@ class MainBtnState extends State with SingleTickerProviderStateMixin { _sizeAnimation = Tween(begin: 0, end: 150).animate( CurvedAnimation(parent: _animationController, curve: Curves.ease), ); + _animationController.repeat(reverse: true); } @override void dispose() { - _timer?.cancel(); _animationController.dispose(); super.dispose(); } - void startTimer() { - int seconds = 1; - _timer = Timer.periodic(const Duration(seconds: 1), (timer) { - setState(() { - 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')}'; - }); - seconds++; - }); - } - - void stopTimer() { - _timer?.cancel(); - setState(() { - connectionTime = "00:00:00"; - connectionStatus = connectionStatusDisconnected; - }); - } - - Future _handleConnection() async { - if (connectionStatus != connectionStatusConnected && - connectionStatus != connectionStatusDisconnected) { - return; - } - - setState(() { - if (connectionStatus == connectionStatusConnected) { - connectionStatus = connectionStatusDisconnecting; - } else if (connectionStatus == connectionStatusDisconnected) { - connectionStatus = connectionStatusConnecting; - } - }); - - if (connectionStatus == connectionStatusConnecting) { - _animationController.repeat(reverse: true); - - VPNclientEngine.ClearSubscriptions(); - VPNclientEngine.addSubscription(subscriptionURL: "https://pastebin.com/raw/ZCYiJ98W"); - await VPNclientEngine.updateSubscription(subscriptionIndex: 0); -// <<<<<<< Updated upstream - - - //END TODO - -/// -// You must initialize V2Ray before using it. -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(); - -/// - - //TODO:move to right place -// ======= -// -// >>>>>>> Stashed changes - 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); - startTimer(); - setState(() { - connectionStatus = connectionStatusConnected; - }); - await _animationController.forward(); - _animationController.stop(); - } else if (connectionStatus == connectionStatusDisconnecting) { - _animationController.repeat(reverse: true); - stopTimer(); - await VPNclientEngine.disconnect(); - setState(() { - connectionStatus = connectionStatusDisconnected; - }); - await _animationController.reverse(); - _animationController.stop(); - } - } - @override Widget build(BuildContext context) { return Column( children: [ Text( - connectionTime, + widget.connectionTime, style: TextStyle( fontSize: 40, fontWeight: FontWeight.w600, color: - connectionStatus == connectionStatusConnected + widget.connectionStatus == 'Connected' ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.secondary, ), ), const SizedBox(height: 70), GestureDetector( - onTap: _handleConnection, + onTap: () { + widget.onPressed(); + if (widget.connectionStatus == 'Connected') { + _animationController.reverse(); + } else { + _animationController.forward(); + } + }, child: Stack( alignment: Alignment.center, children: [ Container( width: 150, height: 150, - decoration: BoxDecoration( - color: Colors.grey[300], + decoration: const BoxDecoration( + color: Colors.grey, shape: BoxShape.circle, ), ), @@ -220,8 +101,17 @@ if (await flutterV2ray.requestPermission()){ ), const SizedBox(height: 20), Text( - connectionStatus, - style: TextStyle( + widget.title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.black, + ), + ), + const SizedBox(height: 20), + Text( + widget.connectionStatus, + style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: Colors.black, @@ -231,7 +121,23 @@ if (await flutterV2ray.requestPermission()){ ); } } +// Remove this code +/* +import 'dart:async'; +import 'dart:developer'; +import 'package:flutter/material.dart'; +import 'package:vpn_client/design/colors.dart'; +import 'package:vpn_client/design/dimensions.dart'; +import 'package:vpnclient_engine_flutter/vpnclient_engine_flutter.dart'; -void main() { - runApp(MaterialApp(home: Scaffold(body: Center(child: MainBtn())))); -} + +import 'package:flutter_v2ray/flutter_v2ray.dart'; + +final FlutterV2ray flutterV2ray = FlutterV2ray( + onStatusChanged: (status) { + // do something + }, +); + + +*/ diff --git a/lib/pages/main/main_page.dart b/lib/pages/main/main_page.dart index fb8d355..676e98a 100644 --- a/lib/pages/main/main_page.dart +++ b/lib/pages/main/main_page.dart @@ -1,58 +1,44 @@ import 'package:flutter/material.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'dart:convert'; +import 'package:provider/provider.dart'; 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/providers/vpn_provider.dart'; +import 'package:vpn_client/pages/servers/servers_page.dart'; -class MainPage extends StatefulWidget { +class MainPage extends StatelessWidget { const MainPage({super.key}); - @override - State createState() => MainPageState(); -} - -class MainPageState extends State { - Map? _selectedServer; - - @override - void initState() { - super.initState(); - _loadSelectedServer(); - } - - Future _loadSelectedServer() async { - final prefs = await SharedPreferences.getInstance(); - final String? savedServers = prefs.getString('selected_servers'); - if (savedServers != null) { - final List serversList = jsonDecode(savedServers); - final activeServer = serversList.firstWhere( - (server) => server['isActive'] == true, - orElse: () => null, - ); - setState(() { - _selectedServer = - activeServer != null - ? Map.from(activeServer) - : null; - }); - } - } - @override Widget build(BuildContext context) { + final vpnProvider = Provider.of(context); return Scaffold( appBar: AppBar( title: const Text('VPN Client'), centerTitle: true, titleTextStyle: TextStyle( - color: Theme.of(context).colorScheme.primary, - fontSize: 24, - ), + color: Theme.of(context).colorScheme.primary, + fontSize: 24), backgroundColor: Theme.of(context).colorScheme.surface, elevation: 0, ), - body: Column( + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const StatBar(title: 'Statistics'), + MainBtn( + title: vpnProvider.isConnected ? 'Disconnect' : 'Connect', + onPressed: () { + vpnProvider.isConnected ? vpnProvider.disconnect() : vpnProvider.connect(); + }), + LocationWidget( + title: 'Location', selectedServer: vpnProvider.selectedServer), + ], + ), + ), + /* body: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const StatBar(), @@ -68,7 +54,7 @@ class MainPageState extends State { ], ), ], - ), + ),*/ ); } } diff --git a/lib/pages/main/stat_bar.dart b/lib/pages/main/stat_bar.dart index 46e28ca..63e6971 100644 --- a/lib/pages/main/stat_bar.dart +++ b/lib/pages/main/stat_bar.dart @@ -1,45 +1,60 @@ import 'package:flutter/material.dart'; import 'package:vpn_client/design/dimensions.dart'; - import '../../design/custom_icons.dart'; class StatBar extends StatefulWidget { - const StatBar({super.key}); + final String title; + final MainAxisAlignment mainAxisAlignment; + final List> stats; - @override - State createState() => StatBarState(); -} + const StatBar({ + super.key, + required this.title, + this.mainAxisAlignment = MainAxisAlignment.spaceEvenly, + this.stats = const [ + {'icon': CustomIcons.download, 'text': '0 Mb/s'}, + {'icon': CustomIcons.upload, 'text': '0 Mb/s'}, + {'icon': CustomIcons.ping, 'text': '0 ms'}, + ], + }); -class StatBarState extends State { @override Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, + return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildStatItem(CustomIcons.download, '0 Mb/s', context), - _buildStatItem(CustomIcons.upload, '0 Mb/s', context), - _buildStatItem(CustomIcons.ping, '0 ms', context), + Text( + title, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.primary, + ), + ), + const SizedBox(height: 16), // Add spacing between the title and the stats + Row( + mainAxisAlignment: mainAxisAlignment, + children: stats.map((stat) => _buildStatItem(stat, context)).toList(), + ), ], ); } - Widget _buildStatItem(IconData icon, String text, BuildContext context) { + Widget _buildStatItem(Map stat, BuildContext context) { return Container( width: 100, height: 75, decoration: BoxDecoration( - boxShadow: [ + boxShadow: const [ BoxShadow( color: Color(0x1A9CB2C2), offset: Offset(0.0, 1.0), blurRadius: 32.0, ), ], + color: Theme.of(context).colorScheme.onSurface, + borderRadius: BorderRadius.circular(12), ), - child: FloatingActionButton( - elevation: elevation0, - onPressed: () {}, - backgroundColor: Theme.of(context).colorScheme.onSurface, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -51,16 +66,15 @@ class StatBarState extends State { color: Theme.of(context).colorScheme.primary, borderRadius: BorderRadius.circular(6.0), ), - child: Icon( - icon, + child: Icon(stat['icon'], size: 20, color: Theme.of(context).colorScheme.onSurface, ), ), const SizedBox(height: 6), Text( - text, - style: TextStyle( + stat['text'], + style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: Theme.of(context).colorScheme.primary, @@ -69,6 +83,5 @@ class StatBarState extends State { ], ), ), - ); } -} \ No newline at end of file +} diff --git a/lib/pages/servers/servers_list.dart b/lib/pages/servers/servers_list.dart index 3497112..de07e79 100644 --- a/lib/pages/servers/servers_list.dart +++ b/lib/pages/servers/servers_list.dart @@ -1,157 +1,20 @@ import 'package:flutter/material.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:vpn_client/pages/servers/servers_list_item.dart'; -import 'dart:convert'; -class ServersList extends StatefulWidget { - final Function(List>)? onServersLoaded; - final List>? servers; +class ServersList extends StatelessWidget { + final List> servers; + final Function(Map) onTap; - const ServersList({super.key, this.onServersLoaded, this.servers}); - - get onNavBarTap => null; - - @override - State createState() => ServersListState(); -} - -class ServersListState extends State { - List> _servers = []; - bool _isLoading = true; - - @override - void initState() { - super.initState(); - if (widget.servers != null && widget.servers!.isNotEmpty) { - _servers = widget.servers!; - _isLoading = false; - if (widget.onServersLoaded != null) { - widget.onServersLoaded!(_servers); - } - } else { - _loadServers(); - } - } - - @override - void didUpdateWidget(covariant ServersList oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.servers != null && widget.servers != oldWidget.servers) { - setState(() { - _servers = widget.servers!; - _isLoading = false; - }); - _saveSelectedServers(); - } - } - - Future _loadServers() async { - setState(() { - _isLoading = true; - }); - - 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, - }, - ]; - - final prefs = await SharedPreferences.getInstance(); - final String? savedServers = prefs.getString('selected_servers'); - if (savedServers != null) { - final List savedServersList = jsonDecode(savedServers); - for (var savedApp in savedServersList) { - final index = serversList.indexWhere( - (server) => server['text'] == savedApp['text'], - ); - if (index != -1) { - serversList[index]['isActive'] = savedApp['isActive']; - } - } - } - - setState(() { - _servers = serversList; - _isLoading = false; - }); - - if (widget.onServersLoaded != null) { - widget.onServersLoaded!(_servers); - } - } catch (e) { - setState(() { - _isLoading = false; - }); - debugPrint('Error loading servers: $e'); - } - } - - Future _saveSelectedServers() async { - final prefs = await SharedPreferences.getInstance(); - final selectedServers = - _servers - .map( - (server) => { - 'text': server['text'], - 'isActive': server['isActive'], - 'icon': server['icon'], - 'ping': server['ping'], - }, - ) - .toList(); - await prefs.setString('selected_servers', jsonEncode(selectedServers)); - } - - List> get servers => _servers; - - void _onItemTapped(int index) { - setState(() { - for (int i = 0; i < _servers.length; i++) { - _servers[i]['isActive'] = false; - } - _servers[index]['isActive'] = true; - }); - - _saveSelectedServers(); - - if (widget.onServersLoaded != null) { - widget.onServersLoaded!(_servers); - } - - if (widget.onNavBarTap != null) { - widget.onNavBarTap!(2); - } - } + const ServersList({super.key, required this.servers, required this.onTap}); @override Widget build(BuildContext context) { final activeServers = - _servers.where((server) => server['isActive'] == true).toList(); + servers.where((server) => server['isActive'] == true).toList(); final inactiveServers = - _servers.where((server) => server['isActive'] != true).toList(); + servers.where((server) => server['isActive'] != true).toList(); + return Container( width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height, @@ -160,54 +23,51 @@ class ServersListState extends State { color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(12), ), - child: - _isLoading - ? const Center(child: CircularProgressIndicator()) - : SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (activeServers.isNotEmpty) ...[ - Container( - margin: const EdgeInsets.only(left: 10), - child: const Text( - 'Выбранный сервер', - style: TextStyle(color: Colors.grey), - ), - ), - ...List.generate(activeServers.length, (index) { - final server = activeServers[index]; - return ServerListItem( - icon: server['icon'], - text: server['text'], - ping: server['ping'], - isActive: server['isActive'], - onTap: () => _onItemTapped(_servers.indexOf(server)), - ); - }), - ], - if (inactiveServers.isNotEmpty) ...[ - Container( - margin: const EdgeInsets.only(left: 10), - child: const Text( - 'Все серверы', - style: TextStyle(color: Colors.grey), - ), - ), - ...List.generate(inactiveServers.length, (index) { - final server = inactiveServers[index]; - return ServerListItem( - icon: server['icon'], - text: server['text'], - ping: server['ping'], - isActive: server['isActive'], - onTap: () => _onItemTapped(_servers.indexOf(server)), - ); - }), - ], - ], + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (activeServers.isNotEmpty) ...[ + Container( + margin: const EdgeInsets.only(left: 10), + child: const Text( + 'Выбранный сервер', + style: TextStyle(color: Colors.grey), ), ), + ...List.generate(activeServers.length, (index) { + final server = activeServers[index]; + return ServerListItem( + icon: server['icon'], + text: server['text'], + ping: server['ping'], + isActive: server['isActive'], + onTap: () => onTap(server), + ); + }), + ], + if (inactiveServers.isNotEmpty) ...[ + Container( + margin: const EdgeInsets.only(left: 10), + child: const Text( + 'Все серверы', + style: TextStyle(color: Colors.grey), + ), + ), + ...List.generate(inactiveServers.length, (index) { + final server = inactiveServers[index]; + return ServerListItem( + icon: server['icon'], + text: server['text'], + ping: server['ping'], + isActive: server['isActive'], + onTap: () => onTap(server), + ); + }), + ], + ], + ), + ), ); } } diff --git a/lib/pages/servers/servers_list_item.dart b/lib/pages/servers/servers_list_item.dart index cb2187c..9efcdb9 100644 --- a/lib/pages/servers/servers_list_item.dart +++ b/lib/pages/servers/servers_list_item.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; + class ServerListItem extends StatelessWidget { final String? icon; final String text; @@ -8,6 +9,7 @@ class ServerListItem extends StatelessWidget { final bool isActive; final VoidCallback onTap; + final Color selectedColor; const ServerListItem({ super.key, this.icon, @@ -15,21 +17,12 @@ class ServerListItem extends StatelessWidget { required this.ping, required this.isActive, required this.onTap, + required this.selectedColor, }); @override Widget build(BuildContext context) { - String pingImage = 'assets/images/ping_status_1.png'; - if (ping.isNotEmpty) { - final int? pingValue = int.tryParse(ping); - if (pingValue != null) { - if (pingValue > 200) { - pingImage = 'assets/images/ping_status_3.png'; - } else if (pingValue > 100) { - pingImage = 'assets/images/ping_status_2.png'; - } - } - } + return GestureDetector( onTap: onTap, @@ -37,7 +30,7 @@ class ServerListItem extends StatelessWidget { height: 52, margin: const EdgeInsets.symmetric(vertical: 8), decoration: BoxDecoration( - color: Colors.white, + color: isActive ? selectedColor : Colors.white, borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( @@ -55,12 +48,13 @@ class ServerListItem extends StatelessWidget { Row( children: [ if (icon != null) - SvgPicture.asset(icon!, width: 52, height: 52), + Container(margin: const EdgeInsets.only(left: 16),child: SvgPicture.asset(icon!, width: 24, height: 24)), if (icon == null) const SizedBox(width: 16), Container( alignment: Alignment.center, height: 52, - child: Text( + margin: const EdgeInsets.only(left: 16), + child: Text( text, style: const TextStyle(fontSize: 16, color: Colors.black), ), @@ -70,14 +64,12 @@ class ServerListItem extends StatelessWidget { Container( alignment: Alignment.center, height: 52, - child: Row( - children: [ - Text( + margin: const EdgeInsets.only(right: 16), + child: Text( int.tryParse(ping) != null ? '$ping ms' : ping, style: const TextStyle(fontSize: 14, color: Colors.grey), - ), - if (ping.isNotEmpty) - Image.asset(pingImage, width: 52, height: 52), + + ], ), ), diff --git a/lib/pages/servers/servers_page.dart b/lib/pages/servers/servers_page.dart index 8a6facc..f484def 100644 --- a/lib/pages/servers/servers_page.dart +++ b/lib/pages/servers/servers_page.dart @@ -1,41 +1,29 @@ -import 'dart:convert'; - import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:vpn_client/pages/servers/servers_list.dart'; +import 'package:vpn_client/providers/vpn_provider.dart'; import 'package:vpn_client/search_dialog.dart'; -class ServersPage extends StatefulWidget { - final Function(int) onNavBarTap; - const ServersPage({super.key, required this.onNavBarTap}); - - @override - State createState() => ServersPageState(); -} - -class ServersPageState extends State { - List> _servers = []; +class ServersPage extends StatelessWidget { + const ServersPage({super.key}); - void _showSearchDialog(BuildContext context) async { - if (_servers.isNotEmpty) { + void _showSearchDialog(BuildContext context, List> servers) async { + if (servers.isNotEmpty) { final updatedServers = await showDialog>>( context: context, builder: (BuildContext context) { return SearchDialog( placeholder: 'Название страны', - items: _servers, + items: servers, type: 2, ); }, ); if (updatedServers != null) { - setState(() { - _servers = updatedServers; - }); - final prefs = await SharedPreferences.getInstance(); - await prefs.setString('selected_servers', jsonEncode(updatedServers)); + //await prefs.setString('selected_servers', jsonEncode(updatedServers)); } } else { debugPrint('Servers list is empty, cannot show search dialog'); 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 9fd1fef..deaef0a 100644 --- a/lib/search_dialog.dart +++ b/lib/search_dialog.dart @@ -1,19 +1,21 @@ 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 'dart:convert'; class SearchDialog extends StatefulWidget { final String placeholder; final List> items; final int type; + final Color selectedColor; + final Function(Map) onSelect; const SearchDialog({ super.key, required this.placeholder, required this.items, required this.type, + required this.onSelect, + required this.selectedColor, }); @override @@ -23,47 +25,16 @@ class SearchDialog extends StatefulWidget { class _SearchDialogState extends State { final TextEditingController _searchController = TextEditingController(); late List> _filteredItems; - List> _recentlySearchedItems = []; late int _searchDialogType; @override void initState() { super.initState(); _searchDialogType = widget.type; - _loadRecentlySearched(); - _filteredItems = widget.items.where((item) { - if (_searchDialogType == 1) { - return item['text'] != 'Все приложения'; - } - return true; - }).toList(); + _filteredItems = widget.items; _searchController.addListener(_filterItems); } - Future _loadRecentlySearched() async { - final prefs = await SharedPreferences.getInstance(); - final String key = _searchDialogType == 1 ? 'recently_searched_apps' : 'recently_searched_servers'; - final String? recentlySearched = prefs.getString(key); - if (recentlySearched != null) { - setState(() { - _recentlySearchedItems = List>.from(jsonDecode(recentlySearched)); - }); - } - } - - Future _saveRecentlySearched(Map item) async { - final prefs = await SharedPreferences.getInstance(); - final String key = _searchDialogType == 1 ? 'recently_searched_apps' : 'recently_searched_servers'; - setState(() { - _recentlySearchedItems.removeWhere((i) => i['text'] == item['text']); - _recentlySearchedItems.insert(0, item); - if (_recentlySearchedItems.length > 5) { - _recentlySearchedItems = _recentlySearchedItems.sublist(0, 5); - } - }); - await prefs.setString(key, jsonEncode(_recentlySearchedItems)); - } - void _filterItems() { final query = _searchController.text.toLowerCase(); setState(() { @@ -76,14 +47,6 @@ class _SearchDialogState extends State { }); } - void _updateServerSelection(Map selectedItem) { - // Обновляем isActive для всех элементов: выбранный становится активным, остальные — неактивными - for (var item in widget.items) { - item['isActive'] = item['text'] == selectedItem['text']; - } - } - - @override void dispose() { _searchController.dispose(); super.dispose(); @@ -91,11 +54,9 @@ class _SearchDialogState extends State { @override Widget build(BuildContext context) { - final isQueryEmpty = _searchController.text.isEmpty; - final hasRecentSearches = _recentlySearchedItems.isNotEmpty; + final isQueryEmpty = _searchController.text.isEmpty; - final showFilteredItems = !isQueryEmpty || (isQueryEmpty && !hasRecentSearches); - final showRecentSearches = isQueryEmpty && hasRecentSearches; + final showFilteredItems = !isQueryEmpty; return Dialog( insetPadding: EdgeInsets.zero, @@ -217,67 +178,6 @@ class _SearchDialogState extends State { ), ), ), - const SizedBox(height: 7), - // Отображаем недавно измененные элементы - if (showRecentSearches) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - margin: const EdgeInsets.only(left: 20), - child: const Text( - 'Недавно искали', - 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 (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: () { - if (_searchController.text.isNotEmpty) { - _saveRecentlySearched(item); - } - _updateServerSelection(item); - Navigator.of(context).pop(widget.items); - }, - ); - } - }), - ), - ), - ], - ), // Отображаем отфильтрованный список Expanded( child: showFilteredItems @@ -307,9 +207,6 @@ class _SearchDialogState extends State { setState(() { _filteredItems[index]['isActive'] = !_filteredItems[index]['isActive']; - if (_searchController.text.isNotEmpty) { - _saveRecentlySearched(_filteredItems[index]); - } }); final originalIndex = widget.items.indexWhere( (i) => i['text'] == item['text'], @@ -326,12 +223,10 @@ class _SearchDialogState extends State { text: item['text'], ping: item['ping'], isActive: item['isActive'] ?? false, + selectedColor: widget.selectedColor, onTap: () { - if (_searchController.text.isNotEmpty) { - _saveRecentlySearched(item); - } - _updateServerSelection(item); - Navigator.of(context).pop(widget.items); + widget.onSelect(item); + Navigator.of(context).pop(); }, ); } diff --git a/pubspec.lock b/pubspec.lock index 8d8a24d..ae6e3ba 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" bloc: dependency: transitive description: @@ -109,10 +109,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" ffi: dependency: transitive description: @@ -220,10 +220,10 @@ packages: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: @@ -521,10 +521,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "15.0.0" vpnclient_engine_flutter: dependency: "direct main" description: From fb1ddcfa72d519f6486facd72028e161e32b365b Mon Sep 17 00:00:00 2001 From: Phakin Kongkha Date: Sat, 10 May 2025 23:19:32 +0700 Subject: [PATCH 02/24] Change vpn link to 5.35.98.91 --- lib/pages/main/main_btn.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/main/main_btn.dart b/lib/pages/main/main_btn.dart index 1a1b40f..199657e 100644 --- a/lib/pages/main/main_btn.dart +++ b/lib/pages/main/main_btn.dart @@ -72,7 +72,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()) { From 18afe0480915b5e1124692bebff17f8f2f69d196 Mon Sep 17 00:00:00 2001 From: Phakin Kongkha Date: Mon, 12 May 2025 16:51:11 +0700 Subject: [PATCH 03/24] Add settings page --- ios/Podfile.lock | 6 +++--- lib/main.dart | 3 ++- lib/pages/settings/settings_page.dart | 28 ++++++++++++++++++++++++++ macos/Podfile.lock | 8 +------- macos/Runner.xcodeproj/project.pbxproj | 4 ---- 5 files changed, 34 insertions(+), 15 deletions(-) create mode 100644 lib/pages/settings/settings_page.dart 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/lib/main.dart b/lib/main.dart index 334992a..4d1358e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,7 @@ import 'package:provider/provider.dart'; import 'package:vpn_client/pages/apps/apps_page.dart'; import 'package:vpn_client/pages/main/main_page.dart'; +import 'package:vpn_client/pages/settings/settings_page.dart'; import 'package:vpn_client/pages/servers/servers_page.dart'; import 'package:vpn_client/theme_provider.dart'; import 'package:vpn_client/vpn_state.dart'; @@ -102,7 +103,7 @@ class _MainScreenState extends State { ServersPage(onNavBarTap: _handleNavBarTap), const MainPage(), const PlaceholderPage(text: 'Speed Page'), - const PlaceholderPage(text: 'Settings Page'), + const SettingsPage(), ]; } diff --git a/lib/pages/settings/settings_page.dart b/lib/pages/settings/settings_page.dart new file mode 100644 index 0000000..09f486c --- /dev/null +++ b/lib/pages/settings/settings_page.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class SettingsPage extends StatefulWidget { + const SettingsPage({super.key}); + + @override + _SettingsPageState createState() => _SettingsPageState(); +} + +class _SettingsPageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(AppLocalizations.of(context)!.app_name), + ), + body: Center( + child: ElevatedButton( + child: const Text("Help! Coding flutter requires a lot of work!"), + onPressed: () { + Navigator.pop(context); + }, + ), + ), + ); + } +} 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"; From 71fe09ad798f8e177795644c3fc8cae13c1901e7 Mon Sep 17 00:00:00 2001 From: ginterloper Date: Mon, 19 May 2025 10:41:47 +0300 Subject: [PATCH 04/24] added base structure to creating vpn profile --- ios/Runner.xcodeproj/project.pbxproj | 246 +++++++++++++++++- ios/Runner/AppDelegate.swift | 103 +++++--- ios/Runner/Runner.entitlements | 10 + ios/Runner/RunnerRelease.entitlements | 15 +- ios/Runner/VPNManager.swift | 180 +++++++++++++ ios/VPNclientTunnel/Info.plist | 13 + .../PacketTunnelProvider.swift | 137 ++++++++++ .../VPNclientTunnel.entitlements | 10 + lib/pages/main/main_btn.dart | 78 +++--- lib/vpn_state.dart | 143 +++++++++- 10 files changed, 833 insertions(+), 102 deletions(-) create mode 100644 ios/Runner/Runner.entitlements create mode 100644 ios/Runner/VPNManager.swift create mode 100644 ios/VPNclientTunnel/Info.plist create mode 100644 ios/VPNclientTunnel/PacketTunnelProvider.swift create mode 100644 ios/VPNclientTunnel/VPNclientTunnel.entitlements diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 6251e06..128c893 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -16,6 +16,9 @@ 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 */; }; + D86303622DDAA124009E3D50 /* VPNManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D86303612DDAA124009E3D50 /* VPNManager.swift */; }; + D863037A2DDAD1CC009E3D50 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D86303792DDAD1CC009E3D50 /* NetworkExtension.framework */; }; + D86303822DDAD1CC009E3D50 /* VPNclientTunnel.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D86303782DDAD1CC009E3D50 /* VPNclientTunnel.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -26,12 +29,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,6 +49,17 @@ name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; + D86303832DDAD1CC009E3D50 /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 8; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + D86303822DDAD1CC009E3D50 /* VPNclientTunnel.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 1; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ @@ -65,9 +86,27 @@ 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 = ""; }; + 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; }; 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 = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + D86303872DDAD1CC009E3D50 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = D86303772DDAD1CC009E3D50 /* VPNclientTunnel */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + D863037B2DDAD1CC009E3D50 /* VPNclientTunnel */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (D86303872DDAD1CC009E3D50 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = VPNclientTunnel; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ 960B5143B6A69B8BB00D37F4 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -85,6 +124,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + D86303752DDAD1CC009E3D50 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D863037A2DDAD1CC009E3D50 /* NetworkExtension.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -125,6 +172,7 @@ children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, + D863037B2DDAD1CC009E3D50 /* VPNclientTunnel */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, 1676946CE4F357204D9C4813 /* Pods */, @@ -137,6 +185,7 @@ children = ( 97C146EE1CF9000F007C117D /* Runner.app */, 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + D86303782DDAD1CC009E3D50 /* VPNclientTunnel.appex */, ); name = Products; sourceTree = ""; @@ -144,6 +193,8 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + D86303612DDAA124009E3D50 /* VPNManager.swift */, + D86303602DDAA0DD009E3D50 /* Runner.entitlements */, 3AC403B42DBB3FD400E35EC1 /* RunnerRelease.entitlements */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, @@ -162,6 +213,7 @@ children = ( 474E7930403F702C7244270A /* Pods_Runner.framework */, 8C1A7CF68EF82448274F79EE /* Pods_RunnerTests.framework */, + D86303792DDAD1CC009E3D50 /* NetworkExtension.framework */, ); name = Frameworks; sourceTree = ""; @@ -200,16 +252,40 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, F6B5A4EE9456ACB2EE7DD28F /* [CP] Embed Pods Frameworks */, + D86303832DDAD1CC009E3D50 /* Embed Foundation Extensions */, ); 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; + packageProductDependencies = ( + ); + productName = VPNclientTunnel; + productReference = D86303782DDAD1CC009E3D50 /* VPNclientTunnel.appex */; + productType = "com.apple.product-type.app-extension"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -217,6 +293,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 1620; LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { @@ -228,6 +305,9 @@ CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1100; }; + D86303772DDAD1CC009E3D50 = { + CreatedOnToolsVersion = 16.2; + }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; @@ -245,6 +325,7 @@ targets = ( 97C146ED1CF9000F007C117D /* Runner */, 331C8080294A63A400263BE5 /* RunnerTests */, + D86303772DDAD1CC009E3D50 /* VPNclientTunnel */, ); }; /* End PBXProject section */ @@ -268,6 +349,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + D86303762DDAD1CC009E3D50 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -380,6 +468,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 +487,11 @@ target = 97C146ED1CF9000F007C117D /* Runner */; targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; }; + D86303812DDAD1CC009E3D50 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D86303772DDAD1CC009E3D50 /* VPNclientTunnel */; + targetProxy = D86303802DDAD1CC009E3D50 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -472,9 +573,11 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = 6XT4R7V83F; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -660,9 +763,11 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = 6XT4R7V83F; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -688,9 +793,9 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = 6XT4R7V83F; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -707,6 +812,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 */ @@ -740,6 +962,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/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/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/VPNclientTunnel/Info.plist b/ios/VPNclientTunnel/Info.plist new file mode 100644 index 0000000..3059459 --- /dev/null +++ b/ios/VPNclientTunnel/Info.plist @@ -0,0 +1,13 @@ + + + + + NSExtension + + 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/lib/pages/main/main_btn.dart b/lib/pages/main/main_btn.dart index 1a1b40f..9aebf1c 100644 --- a/lib/pages/main/main_btn.dart +++ b/lib/pages/main/main_btn.dart @@ -1,15 +1,9 @@ +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:vpn_client/design/colors.dart'; -import 'package:flutter_v2ray/flutter_v2ray.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:vpn_client/vpn_state.dart'; - -final FlutterV2ray flutterV2ray = FlutterV2ray( - onStatusChanged: (status) { - // Handle status changes if needed - }, -); +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:vpn_client/design/colors.dart'; class MainBtn extends StatefulWidget { const MainBtn({super.key}); @@ -67,37 +61,45 @@ class MainBtnState extends State with SingleTickerProviderStateMixin { Future _toggleConnection(BuildContext context) async { final vpnState = Provider.of(context, listen: false); - switch (vpnState.connectionStatus) { - case ConnectionStatus.disconnected: - 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"; - V2RayURL parser = FlutterV2ray.parseFromURL(link); + try { + switch (vpnState.connectionStatus) { + case ConnectionStatus.disconnected: + vpnState.setConnectionStatus(ConnectionStatus.connecting); + _animationController.repeat(reverse: true); - if (await flutterV2ray.requestPermission()) { - await flutterV2ray.startV2Ray( - remark: parser.remark, - config: parser.getFullConfiguration(), - blockedApps: null, - bypassSubnets: null, - proxyOnly: false, - ); - } + if (Platform.isIOS) { + await vpnState.setupVPN( + tunAddr: '192.168.1.2', + tunMask: '255.255.255.0', + tunDns: '8.8.8.8', + socks5Proxy: '176.226.244.28:1080', + ); + await vpnState.startVPN(); + } else if (Platform.isAndroid) { + await vpnState.startVPN(); + } + await _animationController.forward(); + _animationController.stop(); + break; - vpnState.startTimer(); - vpnState.setConnectionStatus(ConnectionStatus.connected); - await _animationController.forward(); - _animationController.stop(); - case ConnectionStatus.connected: - vpnState.setConnectionStatus(ConnectionStatus.disconnecting); - _animationController.repeat(reverse: true); - await flutterV2ray.stopV2Ray(); - vpnState.stopTimer(); - vpnState.setConnectionStatus(ConnectionStatus.disconnected); - await _animationController.reverse(); - _animationController.stop(); - default: + case ConnectionStatus.connected: + vpnState.setConnectionStatus(ConnectionStatus.disconnecting); + _animationController.repeat(reverse: true); + if (Platform.isIOS || Platform.isAndroid) { + await vpnState.stopVPN(); + } + await _animationController.reverse(); + _animationController.stop(); + break; + + default: + break; + } + } catch (e) { + print('Error toggling connection: $e'); + vpnState.setConnectionStatus(ConnectionStatus.disconnected); + _animationController.reverse(); + _animationController.stop(); } } diff --git a/lib/vpn_state.dart b/lib/vpn_state.dart index 0c1efe0..4d912e9 100644 --- a/lib/vpn_state.dart +++ b/lib/vpn_state.dart @@ -1,6 +1,9 @@ import 'dart:async'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_v2ray/flutter_v2ray.dart'; +import 'package:flutter/services.dart'; + enum ConnectionStatus { disconnected, @@ -14,13 +17,145 @@ class VpnState with ChangeNotifier { ConnectionStatus _connectionStatus = ConnectionStatus.disconnected; Timer? _timer; String _connectionTimeText = "00:00:00"; + static const _vpnChannel = MethodChannel('com.vpnclient/vpn_control'); + late FlutterV2ray _v2ray; // Сохраняем экземпляр FlutterV2ray ConnectionStatus get connectionStatus => _connectionStatus; String get connectionTimeText => _connectionTimeText; VpnState() { - // Initializing V2Ray when creating a provider - FlutterV2ray(onStatusChanged: (status) {}).initializeV2Ray(); + if (Platform.isAndroid) { + _v2ray = FlutterV2ray(onStatusChanged: (status) { + _updateStatusFromV2Ray(status); + })..initializeV2Ray(); + } else if (Platform.isIOS) { + _updateStatusFromiOS(); + } + } + + void _updateStatusFromV2Ray(V2RayStatus status) { + switch (status) { + case 'CONNECTING': + setConnectionStatus(ConnectionStatus.connecting); + break; + case ConnectionStatus.connected: + setConnectionStatus(ConnectionStatus.connected); + startTimer(); + break; + case ConnectionStatus.disconnected: + setConnectionStatus(ConnectionStatus.disconnected); + stopTimer(); + break; + default: + setConnectionStatus(ConnectionStatus.disconnected); + } + } + + Future _updateStatusFromiOS() async { + try { + final status = await _vpnChannel.invokeMethod('getVPNStatus'); + switch (status) { + case 'Connected': + setConnectionStatus(ConnectionStatus.connected); + startTimer(); + break; + case 'Connecting...': + setConnectionStatus(ConnectionStatus.connecting); + break; + case 'Disconnecting...': + setConnectionStatus(ConnectionStatus.disconnecting); + break; + case 'Disconnected': + case 'Not Added Profile': + default: + setConnectionStatus(ConnectionStatus.disconnected); + stopTimer(); + break; + } + } catch (e) { + print('Error getting VPN status on iOS: $e'); + setConnectionStatus(ConnectionStatus.disconnected); + } + } + + Future setupVPN({ + required String tunAddr, + required String tunMask, + required String tunDns, + required String socks5Proxy, + }) async { + if (Platform.isIOS) { + try { + await _vpnChannel.invokeMethod('setupVPN', { + 'tunAddr': tunAddr, + 'tunMask': tunMask, + 'tunDns': tunDns, + 'socks5Proxy': socks5Proxy, + }); + } catch (e) { + print('Error setting up VPN: $e'); + rethrow; + } + } + } + + Future startVPN() async { + if (Platform.isIOS) { + try { + await _vpnChannel.invokeMethod('startVPN'); + setConnectionStatus(ConnectionStatus.connected); + startTimer(); + } catch (e) { + print('Error starting VPN on iOS: $e'); + setConnectionStatus(ConnectionStatus.disconnected); + rethrow; + } + } else if (Platform.isAndroid) { + try { + if (await _v2ray.requestPermission()) { + final parser = FlutterV2ray.parseFromURL( + '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', + ); + await _v2ray.startV2Ray( + remark: parser.remark, + config: parser.getFullConfiguration(), + blockedApps: null, + bypassSubnets: null, + proxyOnly: false, + ); + } + setConnectionStatus(ConnectionStatus.connected); + startTimer(); + } catch (e) { + print('Error starting VPN on Android: $e'); + setConnectionStatus(ConnectionStatus.disconnected); + rethrow; + } + } + } + + Future stopVPN() async { + if (Platform.isIOS) { + try { + await _vpnChannel.invokeMethod('stopVPN'); + setConnectionStatus(ConnectionStatus.disconnected); + stopTimer(); + } catch (e) { + print('Error stopping VPN on iOS: $e'); + setConnectionStatus(ConnectionStatus.disconnected); + rethrow; + } + } else if (Platform.isAndroid) { + try { + await _v2ray.stopV2Ray(); + setConnectionStatus(ConnectionStatus.disconnected); + stopTimer(); + } catch (e) { + print('Error stopping VPN on Android: $e'); + setConnectionStatus(ConnectionStatus.disconnected); + rethrow; + } + } } void setConnectionStatus(ConnectionStatus status) { @@ -36,7 +171,7 @@ class VpnState with ChangeNotifier { int minutes = (seconds % 3600) ~/ 60; int remainingSeconds = seconds % 60; _connectionTimeText = - '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${remainingSeconds.toString().padLeft(2, '0')}'; + '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${remainingSeconds.toString().padLeft(2, '0')}'; notifyListeners(); seconds++; }); @@ -53,4 +188,4 @@ class VpnState with ChangeNotifier { _timer?.cancel(); super.dispose(); } -} +} \ No newline at end of file From b2216a85566a8741df2a23e17df4fbf2edeaa60b Mon Sep 17 00:00:00 2001 From: ginterloper Date: Tue, 20 May 2025 01:56:41 +0300 Subject: [PATCH 05/24] build fix --- .../contents.xcworkspacedata | 2 +- ios/Runner/Base.lproj/Main.storyboard | 13 +- ios/Runner/Info.plist | 9 ++ .../project.pbxproj | 134 +++++++++--------- .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcshareddata/WorkspaceSettings.xcsettings | 0 .../xcshareddata/xcschemes/Runner.xcscheme | 10 +- ios/VPNclientTunnel/Info.plist | 9 ++ .../build-request.json | 27 ++++ .../description.msgpack | Bin 0 -> 247 bytes .../manifest.json | 1 + .../target-graph.txt | 1 + .../task-store.msgpack | Bin 0 -> 79 bytes pubspec.yaml | 4 +- 15 files changed, 133 insertions(+), 77 deletions(-) rename ios/{Runner.xcodeproj => VPN Client.xcodeproj}/project.pbxproj (91%) rename ios/{Runner.xcodeproj => VPN Client.xcodeproj}/project.xcworkspace/contents.xcworkspacedata (100%) rename ios/{Runner.xcodeproj => VPN Client.xcodeproj}/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename ios/{Runner.xcodeproj => VPN Client.xcodeproj}/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings (100%) rename ios/{Runner.xcodeproj => VPN Client.xcodeproj}/xcshareddata/xcschemes/Runner.xcscheme (90%) create mode 100644 ios/build/XCBuildData/2b3a1334de51e4dc6dd39387be36d594.xcbuilddata/build-request.json create mode 100644 ios/build/XCBuildData/2b3a1334de51e4dc6dd39387be36d594.xcbuilddata/description.msgpack create mode 100644 ios/build/XCBuildData/2b3a1334de51e4dc6dd39387be36d594.xcbuilddata/manifest.json create mode 100644 ios/build/XCBuildData/2b3a1334de51e4dc6dd39387be36d594.xcbuilddata/target-graph.txt create mode 100644 ios/build/XCBuildData/2b3a1334de51e4dc6dd39387be36d594.xcbuilddata/task-store.msgpack 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/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.xcodeproj/project.pbxproj b/ios/VPN Client.xcodeproj/project.pbxproj similarity index 91% rename from ios/Runner.xcodeproj/project.pbxproj rename to ios/VPN Client.xcodeproj/project.pbxproj index 128c893..500eac7 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/VPN Client.xcodeproj/project.pbxproj @@ -3,22 +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 */; }; - D86303822DDAD1CC009E3D50 /* VPNclientTunnel.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D86303782DDAD1CC009E3D50 /* VPNclientTunnel.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -55,7 +54,6 @@ dstPath = ""; dstSubfolderSpec = 13; files = ( - D86303822DDAD1CC009E3D50 /* VPNclientTunnel.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 1; @@ -65,18 +63,16 @@ /* 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; }; @@ -84,17 +80,19 @@ 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 = ""; }; + 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; }; - 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 = ""; }; + 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 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + D86303872DDAD1CC009E3D50 /* Exceptions for "VPNclientTunnel" folder in "VPNclientTunnel" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( Info.plist, @@ -104,7 +102,14 @@ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ - D863037B2DDAD1CC009E3D50 /* VPNclientTunnel */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (D86303872DDAD1CC009E3D50 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = VPNclientTunnel; sourceTree = ""; }; + D863037B2DDAD1CC009E3D50 /* VPNclientTunnel */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + D86303872DDAD1CC009E3D50 /* Exceptions for "VPNclientTunnel" folder in "VPNclientTunnel" target */, + ); + path = VPNclientTunnel; + sourceTree = ""; + }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -112,7 +117,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 05623FCADE53740FE6EC38FC /* Pods_RunnerTests.framework in Frameworks */, + 2D7B47EE58217BA44F021C99 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -120,7 +125,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5CF4606063F6CF5DDF0DEF1E /* Pods_Runner.framework in Frameworks */, + ADCEA9EEA0674573FF96F9D6 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -138,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 = ""; @@ -211,9 +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 = ""; @@ -225,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 */, @@ -244,15 +249,15 @@ 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 = ( ); @@ -280,8 +285,6 @@ D863037B2DDAD1CC009E3D50 /* VPNclientTunnel */, ); name = VPNclientTunnel; - packageProductDependencies = ( - ); productName = VPNclientTunnel; productReference = D86303782DDAD1CC009E3D50 /* VPNclientTunnel.appex */; productType = "com.apple.product-type.app-extension"; @@ -310,8 +313,7 @@ }; }; }; - 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 = ( @@ -319,6 +321,7 @@ Base, ); mainGroup = 97C146E51CF9000F007C117D; + preferredProjectObjectVersion = 77; productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; @@ -359,23 +362,7 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - 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 */ = { + 2119E14845FE31FAB4D234BF /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -397,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 = ( @@ -434,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 = ( @@ -576,16 +579,17 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + 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"; @@ -596,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; @@ -614,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; @@ -630,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; @@ -766,16 +770,17 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + 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"; @@ -794,16 +799,17 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + 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"; @@ -942,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 */, 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 index 3059459..9a63630 100644 --- a/ios/VPNclientTunnel/Info.plist +++ b/ios/VPNclientTunnel/Info.plist @@ -4,6 +4,15 @@ NSExtension + com.apple.developer.networking.vpn.api + + allow-vpn + + UIBackgroundModes + + fetch + remote-notification + NSExtensionPointIdentifier com.apple.networkextension.packet-tunnel NSExtensionPrincipalClass 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 0000000000000000000000000000000000000000..0e18711f8a30954fa556a6b77389058a99f640ce GIT binary patch literal 247 zcmW-b!A-<46hx_k3bq0A9ow7OSHcy9gg{&!|DRywwX=3Mz%3<&4yXmTiIpgZEOQy@ z&Ed`O#pS7l$4n_h@4`q4p6>6gcNE4eGYsHjhW6k*!gF(d?i(|j"]},"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 0000000000000000000000000000000000000000..6cef3fe35dc37f3eb3b88d5dc171a6a7189718d4 GIT binary patch literal 79 zcmbPuhe6^15KLO)o>-E4Q!zZhD7&~IF*(&EH8CZ%$TzVd%q`e0Gbgn;yePAzBsFir egb9--OjxLY_`rd~hbA1JFyX+V%M4I(=nw#{5hHQ{ literal 0 HcmV?d00001 diff --git a/pubspec.yaml b/pubspec.yaml index 5644e73..9eb5b41 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,8 +35,8 @@ 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: From 90861721c8703596593c56003f721b04e1adac5b Mon Sep 17 00:00:00 2001 From: ginterloper Date: Tue, 20 May 2025 11:47:43 +0300 Subject: [PATCH 06/24] added links to dimension --- lib/design/dimensions.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/design/dimensions.dart b/lib/design/dimensions.dart index d840e3b..52ecaf9 100644 --- a/lib/design/dimensions.dart +++ b/lib/design/dimensions.dart @@ -77,3 +77,6 @@ const double fontSize15 = 15; const double fontSize17 = 17; const double fontSize24 = 24; const double fontSize40 = 40; + +const String connectionTgUrl = 't.me/vpn_client_bot'; +const String supportTgUrl = 't.me/vpn_client_support'; From 8d4d91933a2d85a2fc64472211b46ddcf75ff520 Mon Sep 17 00:00:00 2001 From: Titan Date: Fri, 23 May 2025 13:18:19 +0900 Subject: [PATCH 07/24] Bullet proof-localized-feature --- assets/lang/en.json | 26 ++++++++++++ lib/l10n/app_ru.arb => assets/lang/ru.json | 2 +- lib/l10n/app_th.arb => assets/lang/th.json | 2 +- lib/l10n/app_zh.arb => assets/lang/zh.json | 2 +- l10n.yaml | 4 -- lib/l10n/app_en.arb | 26 ------------ lib/localization_service.dart | 32 ++++++++++++++ lib/main.dart | 49 ++++++++-------------- lib/pages/apps/apps_page.dart | 8 ++-- lib/pages/main/location_widget.dart | 4 +- lib/pages/main/main_btn.dart | 27 +++++------- lib/pages/main/main_page.dart | 4 +- lib/pages/servers/servers_list.dart | 16 +++---- lib/pages/servers/servers_page.dart | 8 ++-- lib/search_dialog.dart | 12 +++--- pubspec.yaml | 18 ++++---- 16 files changed, 127 insertions(+), 113 deletions(-) create mode 100644 assets/lang/en.json rename lib/l10n/app_ru.arb => assets/lang/ru.json (97%) rename lib/l10n/app_th.arb => assets/lang/th.json (98%) rename lib/l10n/app_zh.arb => assets/lang/zh.json (97%) delete mode 100644 l10n.yaml delete mode 100644 lib/l10n/app_en.arb create mode 100644 lib/localization_service.dart diff --git a/assets/lang/en.json b/assets/lang/en.json new file mode 100644 index 0000000..a5c0797 --- /dev/null +++ b/assets/lang/en.json @@ -0,0 +1,26 @@ +{ + + "app_name": "VPN Client", + "apps_selection": "App Selection", + "search": "Search", + "your_location": "Your Location", + "auto_select": "Auto Select", + "kazakhstan": "Kazakhstan", + "turkey": "Turkey", + "poland": "Poland", + "fastest": "Fastest", + "selected_server": "Selected server", + "server_selection": "Server selection", + "all_servers": "All servers", + "country_name": "Country name", + "all_apps": "All Applications", + "done": "Done", + "cancel": "Cancel", + "recently_searched": "Recently searched", + "nothing_found": "Nothing found", + "connected": "CONNECTED", + "disconnected": "DISCONNECTED", + "reconnecting": "RECONNECTING", + "connecting": "CONNECTING", + "disconnecting": "DISCONNECTING" +} diff --git a/lib/l10n/app_ru.arb b/assets/lang/ru.json similarity index 97% rename from lib/l10n/app_ru.arb rename to assets/lang/ru.json index 067094e..b495f72 100644 --- a/lib/l10n/app_ru.arb +++ b/assets/lang/ru.json @@ -1,5 +1,5 @@ { - "@@locale": "ru", + "app_name": "VPN Клиент", "apps_selection": "Выбор приложений", "search": "Поиск", diff --git a/lib/l10n/app_th.arb b/assets/lang/th.json similarity index 98% rename from lib/l10n/app_th.arb rename to assets/lang/th.json index c258b47..420c726 100644 --- a/lib/l10n/app_th.arb +++ b/assets/lang/th.json @@ -1,5 +1,5 @@ { - "@@locale": "th", + "app_name": "VPN Client", "apps_selection": "เลือกแอป", "search": "ค้นหา", diff --git a/lib/l10n/app_zh.arb b/assets/lang/zh.json similarity index 97% rename from lib/l10n/app_zh.arb rename to assets/lang/zh.json index fae8b5e..3985ada 100644 --- a/lib/l10n/app_zh.arb +++ b/assets/lang/zh.json @@ -1,5 +1,5 @@ { - "@@locale": "zh", + "app_name": "VPN客户端", "apps_selection": "应用选择", "search": "搜索", diff --git a/l10n.yaml b/l10n.yaml deleted file mode 100644 index d5830f6..0000000 --- a/l10n.yaml +++ /dev/null @@ -1,4 +0,0 @@ -synthetic-package: true -arb-dir: lib/l10n -template-arb-file: app_en.arb -output-localization-file: app_localizations.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb deleted file mode 100644 index 385ce8c..0000000 --- a/lib/l10n/app_en.arb +++ /dev/null @@ -1,26 +0,0 @@ -{ - "@@locale": "en", - "app_name": "VPN Client", - "apps_selection": "App Selection", - "search": "Search", - "your_location": "Your Location", - "auto_select": "Auto Select", - "kazakhstan": "Kazakhstan", - "turkey": "Turkey", - "poland": "Poland", - "fastest": "Fastest", - "selected_server": "Selected server", - "server_selection": "Server selection", - "all_servers": "All servers", - "country_name": "Country name", - "all_apps": "All Applications", - "done": "Done", - "cancel": "Cancel", - "recently_searched": "Recently searched", - "nothing_found": "Nothing found", - "connected": "CONNECTED", - "disconnected": "DISCONNECTED", - "reconnecting": "RECONNECTING", - "connecting": "CONNECTING", - "disconnecting": "DISCONNECTING" -} diff --git a/lib/localization_service.dart b/lib/localization_service.dart new file mode 100644 index 0000000..259fbbd --- /dev/null +++ b/lib/localization_service.dart @@ -0,0 +1,32 @@ +import 'dart:convert'; +import 'package:flutter/services.dart'; +import 'package:flutter/material.dart'; + +class LocalizationService { + static Map _localizedStrings = {}; + static late Locale _currentLocale; + + static Future load(Locale locale) async { + _currentLocale = locale; + String langCode = locale.languageCode; + + // Try loading the file, fallback to English + try { + final String jsonString = await rootBundle.loadString( + 'assets/lang/$langCode.json', + ); + _localizedStrings = json.decode(jsonString); + } catch (_) { + final String fallback = await rootBundle.loadString( + 'assets/lang/en.json', + ); + _localizedStrings = json.decode(fallback); + } + } + + static String to(String key) { + return _localizedStrings[key] ?? '[$key]'; + } + + static Locale get currentLocale => _currentLocale; +} diff --git a/lib/main.dart b/lib/main.dart index 334992a..341fccc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,18 +1,23 @@ import 'package:flutter/material.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; - import 'package:vpn_client/pages/apps/apps_page.dart'; +import 'dart:ui' as ui; import 'package:vpn_client/pages/main/main_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 'design/colors.dart'; import 'nav_bar.dart'; -void main() { +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + Locale userLocale = ui.PlatformDispatcher.instance.locale; // <-- Get the system locale + await LocalizationService.load(userLocale); + runApp( MultiProvider( providers: [ @@ -36,28 +41,23 @@ class App extends StatelessWidget { final Locale? manualLocale = null; // ← use system by default return MaterialApp( + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], debugShowCheckedModeBanner: false, title: 'VPN Client', theme: lightTheme, darkTheme: darkTheme, locale: manualLocale, - localeResolutionCallback: (locale, supportedLocales) { + localeResolutionCallback: (locale, _) { if (locale == null) return const Locale('en'); // Check for exact match - for (var supportedLocale in supportedLocales) { - if (supportedLocale.languageCode == locale.languageCode && - (supportedLocale.countryCode == null || - supportedLocale.countryCode == locale.countryCode)) { - return supportedLocale; - } - } - - // If Chinese variants are not supported, fallback to zh - if (locale.languageCode == 'zh') { - return supportedLocales.contains(const Locale('zh')) - ? const Locale('zh') - : const Locale('en'); + final supported = ['en', 'ru', 'th', 'zh']; + if (supported.contains(locale.languageCode)) { + return Locale(locale.languageCode); } // Fallback to 'en' if not found @@ -66,19 +66,6 @@ class App extends StatelessWidget { 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'), - ], ); } } diff --git a/lib/pages/apps/apps_page.dart b/lib/pages/apps/apps_page.dart index 9e45e79..fdd99ba 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:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:vpn_client/localization_service.dart'; class AppsPage extends StatefulWidget { const AppsPage({super.key}); @@ -19,7 +19,7 @@ class AppsPageState extends State { context: context, builder: (BuildContext context) { return SearchDialog( - placeholder: AppLocalizations.of(context)!.app_name, + placeholder: LocalizationService.to('app_name'), items: _apps, type: 1, ); @@ -40,7 +40,7 @@ class AppsPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(AppLocalizations.of(context)!.apps_selection), + title: Text(LocalizationService.to('apps_selection')), centerTitle: true, titleTextStyle: TextStyle( color: Theme.of(context).colorScheme.primary, @@ -60,7 +60,7 @@ class AppsPageState extends State { color: Theme.of(context).colorScheme.primary, ), onPressed: () => _showSearchDialog(context), - tooltip: AppLocalizations.of(context)!.search, + tooltip: LocalizationService.to('search'), ), ), ), diff --git a/lib/pages/main/location_widget.dart b/lib/pages/main/location_widget.dart index 889911f..53b724c 100644 --- a/lib/pages/main/location_widget.dart +++ b/lib/pages/main/location_widget.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:vpn_client/localization_service.dart'; class LocationWidget extends StatelessWidget { final Map? selectedServer; @@ -27,7 +27,7 @@ class LocationWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - AppLocalizations.of(context)!.your_location, + LocalizationService.to('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 1a1b40f..7e5d82a 100644 --- a/lib/pages/main/main_btn.dart +++ b/lib/pages/main/main_btn.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:vpn_client/design/colors.dart'; import 'package:flutter_v2ray/flutter_v2ray.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:vpn_client/localization_service.dart'; import 'package:vpn_client/vpn_state.dart'; final FlutterV2ray flutterV2ray = FlutterV2ray( @@ -47,21 +47,16 @@ class MainBtnState extends State with SingleTickerProviderStateMixin { super.dispose(); } - String get connectionStatusText { - final localizations = AppLocalizations.of(context)!; + String connectionStatusText(BuildContext context) { final vpnState = Provider.of(context, listen: false); - switch (vpnState.connectionStatus) { - case ConnectionStatus.connected: - return localizations.connected; - case ConnectionStatus.disconnected: - return localizations.disconnected; - case ConnectionStatus.reconnecting: - return localizations.reconnecting; - case ConnectionStatus.disconnecting: - return localizations.disconnecting; - case ConnectionStatus.connecting: - return localizations.connecting; - } + + return { + ConnectionStatus.connected: LocalizationService.to('connected'), + ConnectionStatus.disconnected: LocalizationService.to('disconnected'), + ConnectionStatus.reconnecting: LocalizationService.to('reconnecting'), + ConnectionStatus.disconnecting: LocalizationService.to('disconnecting'), + ConnectionStatus.connecting: LocalizationService.to('connecting'), + }[vpnState.connectionStatus]!; } Future _toggleConnection(BuildContext context) async { @@ -160,7 +155,7 @@ class MainBtnState extends State with SingleTickerProviderStateMixin { ), const SizedBox(height: 20), Text( - connectionStatusText, + connectionStatusText(context), style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, diff --git a/lib/pages/main/main_page.dart b/lib/pages/main/main_page.dart index f08a8e4..de76bd8 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:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:vpn_client/localization_service.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(AppLocalizations.of(context)!.app_name), + title: Text(LocalizationService.to('app_name')), centerTitle: true, titleTextStyle: TextStyle( color: Theme.of(context).colorScheme.primary, diff --git a/lib/pages/servers/servers_list.dart b/lib/pages/servers/servers_list.dart index 7a89d8d..c4343ce 100644 --- a/lib/pages/servers/servers_list.dart +++ b/lib/pages/servers/servers_list.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:vpn_client/pages/servers/servers_list_item.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:vpn_client/localization_service.dart'; import 'dart:convert'; class ServersList extends StatefulWidget { @@ -62,25 +62,25 @@ class ServersListState extends State { List> serversList = [ { 'icon': 'assets/images/flags/auto.svg', - 'text': AppLocalizations.of(context)!.auto_select, - 'ping': AppLocalizations.of(context)!.fastest, + 'text': LocalizationService.to('auto_select'), + 'ping': LocalizationService.to('fastest'), 'isActive': true, }, { 'icon': 'assets/images/flags/Kazahstan.svg', - 'text': AppLocalizations.of(context)!.kazakhstan, + 'text': LocalizationService.to('kazakhstan'), 'ping': '48', 'isActive': false, }, { 'icon': 'assets/images/flags/Turkey.svg', - 'text': AppLocalizations.of(context)!.turkey, + 'text': LocalizationService.to('turkey'), 'ping': '142', 'isActive': false, }, { 'icon': 'assets/images/flags/Poland.svg', - 'text': AppLocalizations.of(context)!.poland, + 'text': LocalizationService.to('poland'), 'ping': '298', 'isActive': false, }, @@ -178,7 +178,7 @@ class ServersListState extends State { Container( margin: const EdgeInsets.only(left: 10), child: Text( - AppLocalizations.of(context)!.selected_server, + LocalizationService.to('selected_server'), style: TextStyle(color: Colors.grey), ), ), @@ -197,7 +197,7 @@ class ServersListState extends State { Container( margin: const EdgeInsets.only(left: 10), child: Text( - AppLocalizations.of(context)!.all_servers, + LocalizationService.to('all_servers'), style: TextStyle(color: Colors.grey), ), ), diff --git a/lib/pages/servers/servers_page.dart b/lib/pages/servers/servers_page.dart index 41b0ad2..dda5741 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:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:vpn_client/localization_service.dart'; class ServersPage extends StatefulWidget { final Function(int) onNavBarTap; @@ -23,7 +23,7 @@ class ServersPageState extends State { context: context, builder: (BuildContext context) { return SearchDialog( - placeholder: AppLocalizations.of(context)!.country_name, + placeholder: LocalizationService.to('country_name'), items: _servers, type: 2, ); @@ -47,7 +47,7 @@ class ServersPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(AppLocalizations.of(context)!.selected_server), + title: Text(LocalizationService.to('selected_server')), centerTitle: true, titleTextStyle: TextStyle( color: Theme.of(context).colorScheme.primary, @@ -67,7 +67,7 @@ class ServersPageState extends State { color: Theme.of(context).colorScheme.primary, ), onPressed: () => _showSearchDialog(context), - tooltip: AppLocalizations.of(context)!.search, + tooltip: LocalizationService.to('search'), ), ), ), diff --git a/lib/search_dialog.dart b/lib/search_dialog.dart index 67d3a06..9aaa304 100644 --- a/lib/search_dialog.dart +++ b/lib/search_dialog.dart @@ -2,7 +2,7 @@ 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:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:vpn_client/localization_service.dart'; import 'dart:convert'; class SearchDialog extends StatefulWidget { @@ -137,7 +137,7 @@ class _SearchDialogState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - AppLocalizations.of(context)!.search, + LocalizationService.to('search'), style: TextStyle( fontSize: 24, fontWeight: FontWeight.w600, @@ -155,7 +155,7 @@ class _SearchDialogState extends State { Navigator.of(context).pop(widget.items); }, child: Text( - AppLocalizations.of(context)!.done, + LocalizationService.to('done'), textAlign: TextAlign.center, style: TextStyle( color: Colors.blue, @@ -174,7 +174,7 @@ class _SearchDialogState extends State { Navigator.of(context).pop(); }, child: Text( - AppLocalizations.of(context)!.cancel, + LocalizationService.to('cancel'), textAlign: TextAlign.center, style: TextStyle( color: Colors.blue, @@ -248,7 +248,7 @@ class _SearchDialogState extends State { Container( margin: const EdgeInsets.only(left: 20), child: Text( - AppLocalizations.of(context)!.recently_searched, + LocalizationService.to('recently_searched'), style: TextStyle(color: Colors.grey), ), ), @@ -311,7 +311,7 @@ class _SearchDialogState extends State { ? _filteredItems.isEmpty ? Center( child: Text( - AppLocalizations.of(context)!.nothing_found, + LocalizationService.to('nothing_found'), style: TextStyle( color: Theme.of(context).colorScheme.primary, ), diff --git a/pubspec.yaml b/pubspec.yaml index 5644e73..ef2d86e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -65,7 +65,7 @@ dev_dependencies: # The following section is specific to Flutter packages. flutter: - + fonts: - family: CustomIcons fonts: @@ -80,6 +80,10 @@ flutter: assets: - assets/images/ - assets/images/flags/ + - assets/lang/en.json + - assets/lang/zh.json + - 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 @@ -107,12 +111,12 @@ flutter: # For details regarding fonts from package dependencies, # see https://flutter.dev/to/font-from-package - generate: true + 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 +# l10n: +# arb-dir: l10n +# template-arb-file: app_en.arb +# output-localization-file: app_localizations.dart +# untranslated-messages-file: lib/l10n/untranslated_messages.txt From 60560d893de956cf5776d531922499013b65a7b9 Mon Sep 17 00:00:00 2001 From: Titan Date: Fri, 23 May 2025 13:22:11 +0900 Subject: [PATCH 08/24] Bullet proof-localized-feature --- lib/main.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/main.dart b/lib/main.dart index 341fccc..7415286 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -15,7 +15,8 @@ import 'nav_bar.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - Locale userLocale = ui.PlatformDispatcher.instance.locale; // <-- Get the system locale + Locale userLocale = + ui.PlatformDispatcher.instance.locale; // <-- Get the system locale await LocalizationService.load(userLocale); runApp( From ad9299fe54ad547c117a94d0703f3ea1a512caf2 Mon Sep 17 00:00:00 2001 From: Phakin Kongkha Date: Fri, 23 May 2025 21:45:13 +0700 Subject: [PATCH 09/24] Use flutter instead of dart for format CI --- .github/workflows/quality.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index c2b02cb..6dbbf7d 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -21,8 +21,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Setup dart - uses: dart-lang/setup-dart@v1 + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + flutter-version-file: pubspec.yaml - name: Format code run: dart format --output=none --set-exit-if-changed . matrics: From ef43782cb683f6438c144fa590d5fc58b0f9f41d Mon Sep 17 00:00:00 2001 From: Anton Dodonov Date: Mon, 16 Jun 2025 06:13:03 +0700 Subject: [PATCH 10/24] feat: change Android status bar to white background with black icons --- android/app/src/main/res/values-night/styles.xml | 2 ++ android/app/src/main/res/values/styles.xml | 2 ++ 2 files changed, 4 insertions(+) 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. --> From d5a5d6dec179d761ad9afb4c004c5f76587c2ec4 Mon Sep 17 00:00:00 2001 From: Anton Dodonov Date: Mon, 16 Jun 2025 06:23:50 +0700 Subject: [PATCH 11/24] fix: update l10n configuration to use new format --- pubspec.yaml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 5644e73..6f3c5ec 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -107,12 +107,16 @@ flutter: # For details regarding fonts from package dependencies, # see https://flutter.dev/to/font-from-package - generate: true - - + # generate: true +flutter_gen: + output: lib/gen/ + line_length: 80 + l10n: arb-dir: 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 From 3f7b9575268a1af57ff864cde065def9c07d8f4c Mon Sep 17 00:00:00 2001 From: Anton Dodonov Date: Mon, 16 Jun 2025 06:24:21 +0700 Subject: [PATCH 12/24] chore: update dependencies --- pubspec.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 7c0530a..2daa618 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" bloc: dependency: transitive description: @@ -149,10 +149,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" ffi: dependency: transitive description: @@ -249,10 +249,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: @@ -273,18 +273,18 @@ packages: dependency: transitive description: name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.dev" source: hosted - version: "0.19.0" + version: "0.20.2" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: @@ -566,10 +566,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: @@ -582,10 +582,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: @@ -598,18 +598,18 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "15.0.0" 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: From 06886be5878e073e5961ea92186418480fa73a42 Mon Sep 17 00:00:00 2001 From: Phakin Kongkha Date: Fri, 11 Jul 2025 13:28:15 +0700 Subject: [PATCH 13/24] Use flutter instead of dart for format CI (#29) From 130df0e1deed9872ed70a1f3ada0f0ec2e12dc55 Mon Sep 17 00:00:00 2001 From: Titan Date: Fri, 30 May 2025 03:34:17 +0900 Subject: [PATCH 14/24] Refactor localization implementation and remove unused ARB files --- assets/lang/en.json | 26 +++++++++++ lib/l10n/app_ru.arb => assets/lang/ru.json | 2 +- lib/l10n/app_th.arb => assets/lang/th.json | 2 +- lib/l10n/app_zh.arb => assets/lang/zh.json | 2 +- l10n.yaml | 4 -- lib/l10n/app_en.arb | 26 ----------- lib/localization_service.dart | 32 ++++++++++++++ lib/main.dart | 50 ++++++++-------------- lib/pages/apps/apps_page.dart | 8 ++-- lib/pages/main/location_widget.dart | 4 +- lib/pages/main/main_btn.dart | 27 +++++------- lib/pages/main/main_page.dart | 4 +- lib/pages/servers/servers_list.dart | 16 +++---- lib/pages/servers/servers_page.dart | 8 ++-- lib/search_dialog.dart | 12 +++--- pubspec.lock | 20 ++++----- pubspec.yaml | 18 +++++--- 17 files changed, 138 insertions(+), 123 deletions(-) create mode 100644 assets/lang/en.json rename lib/l10n/app_ru.arb => assets/lang/ru.json (97%) rename lib/l10n/app_th.arb => assets/lang/th.json (98%) rename lib/l10n/app_zh.arb => assets/lang/zh.json (97%) delete mode 100644 l10n.yaml delete mode 100644 lib/l10n/app_en.arb create mode 100644 lib/localization_service.dart diff --git a/assets/lang/en.json b/assets/lang/en.json new file mode 100644 index 0000000..a5c0797 --- /dev/null +++ b/assets/lang/en.json @@ -0,0 +1,26 @@ +{ + + "app_name": "VPN Client", + "apps_selection": "App Selection", + "search": "Search", + "your_location": "Your Location", + "auto_select": "Auto Select", + "kazakhstan": "Kazakhstan", + "turkey": "Turkey", + "poland": "Poland", + "fastest": "Fastest", + "selected_server": "Selected server", + "server_selection": "Server selection", + "all_servers": "All servers", + "country_name": "Country name", + "all_apps": "All Applications", + "done": "Done", + "cancel": "Cancel", + "recently_searched": "Recently searched", + "nothing_found": "Nothing found", + "connected": "CONNECTED", + "disconnected": "DISCONNECTED", + "reconnecting": "RECONNECTING", + "connecting": "CONNECTING", + "disconnecting": "DISCONNECTING" +} diff --git a/lib/l10n/app_ru.arb b/assets/lang/ru.json similarity index 97% rename from lib/l10n/app_ru.arb rename to assets/lang/ru.json index 067094e..b495f72 100644 --- a/lib/l10n/app_ru.arb +++ b/assets/lang/ru.json @@ -1,5 +1,5 @@ { - "@@locale": "ru", + "app_name": "VPN Клиент", "apps_selection": "Выбор приложений", "search": "Поиск", diff --git a/lib/l10n/app_th.arb b/assets/lang/th.json similarity index 98% rename from lib/l10n/app_th.arb rename to assets/lang/th.json index c258b47..420c726 100644 --- a/lib/l10n/app_th.arb +++ b/assets/lang/th.json @@ -1,5 +1,5 @@ { - "@@locale": "th", + "app_name": "VPN Client", "apps_selection": "เลือกแอป", "search": "ค้นหา", diff --git a/lib/l10n/app_zh.arb b/assets/lang/zh.json similarity index 97% rename from lib/l10n/app_zh.arb rename to assets/lang/zh.json index fae8b5e..3985ada 100644 --- a/lib/l10n/app_zh.arb +++ b/assets/lang/zh.json @@ -1,5 +1,5 @@ { - "@@locale": "zh", + "app_name": "VPN客户端", "apps_selection": "应用选择", "search": "搜索", diff --git a/l10n.yaml b/l10n.yaml deleted file mode 100644 index d5830f6..0000000 --- a/l10n.yaml +++ /dev/null @@ -1,4 +0,0 @@ -synthetic-package: true -arb-dir: lib/l10n -template-arb-file: app_en.arb -output-localization-file: app_localizations.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb deleted file mode 100644 index 385ce8c..0000000 --- a/lib/l10n/app_en.arb +++ /dev/null @@ -1,26 +0,0 @@ -{ - "@@locale": "en", - "app_name": "VPN Client", - "apps_selection": "App Selection", - "search": "Search", - "your_location": "Your Location", - "auto_select": "Auto Select", - "kazakhstan": "Kazakhstan", - "turkey": "Turkey", - "poland": "Poland", - "fastest": "Fastest", - "selected_server": "Selected server", - "server_selection": "Server selection", - "all_servers": "All servers", - "country_name": "Country name", - "all_apps": "All Applications", - "done": "Done", - "cancel": "Cancel", - "recently_searched": "Recently searched", - "nothing_found": "Nothing found", - "connected": "CONNECTED", - "disconnected": "DISCONNECTED", - "reconnecting": "RECONNECTING", - "connecting": "CONNECTING", - "disconnecting": "DISCONNECTING" -} diff --git a/lib/localization_service.dart b/lib/localization_service.dart new file mode 100644 index 0000000..259fbbd --- /dev/null +++ b/lib/localization_service.dart @@ -0,0 +1,32 @@ +import 'dart:convert'; +import 'package:flutter/services.dart'; +import 'package:flutter/material.dart'; + +class LocalizationService { + static Map _localizedStrings = {}; + static late Locale _currentLocale; + + static Future load(Locale locale) async { + _currentLocale = locale; + String langCode = locale.languageCode; + + // Try loading the file, fallback to English + try { + final String jsonString = await rootBundle.loadString( + 'assets/lang/$langCode.json', + ); + _localizedStrings = json.decode(jsonString); + } catch (_) { + final String fallback = await rootBundle.loadString( + 'assets/lang/en.json', + ); + _localizedStrings = json.decode(fallback); + } + } + + static String to(String key) { + return _localizedStrings[key] ?? '[$key]'; + } + + static Locale get currentLocale => _currentLocale; +} diff --git a/lib/main.dart b/lib/main.dart index 334992a..7415286 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,18 +1,24 @@ import 'package:flutter/material.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; - import 'package:vpn_client/pages/apps/apps_page.dart'; +import 'dart:ui' as ui; import 'package:vpn_client/pages/main/main_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 'design/colors.dart'; import 'nav_bar.dart'; -void main() { +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + Locale userLocale = + ui.PlatformDispatcher.instance.locale; // <-- Get the system locale + await LocalizationService.load(userLocale); + runApp( MultiProvider( providers: [ @@ -36,28 +42,23 @@ class App extends StatelessWidget { final Locale? manualLocale = null; // ← use system by default return MaterialApp( + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], debugShowCheckedModeBanner: false, title: 'VPN Client', theme: lightTheme, darkTheme: darkTheme, locale: manualLocale, - localeResolutionCallback: (locale, supportedLocales) { + localeResolutionCallback: (locale, _) { if (locale == null) return const Locale('en'); // Check for exact match - for (var supportedLocale in supportedLocales) { - if (supportedLocale.languageCode == locale.languageCode && - (supportedLocale.countryCode == null || - supportedLocale.countryCode == locale.countryCode)) { - return supportedLocale; - } - } - - // If Chinese variants are not supported, fallback to zh - if (locale.languageCode == 'zh') { - return supportedLocales.contains(const Locale('zh')) - ? const Locale('zh') - : const Locale('en'); + final supported = ['en', 'ru', 'th', 'zh']; + if (supported.contains(locale.languageCode)) { + return Locale(locale.languageCode); } // Fallback to 'en' if not found @@ -66,19 +67,6 @@ class App extends StatelessWidget { 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'), - ], ); } } diff --git a/lib/pages/apps/apps_page.dart b/lib/pages/apps/apps_page.dart index 9e45e79..fdd99ba 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:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:vpn_client/localization_service.dart'; class AppsPage extends StatefulWidget { const AppsPage({super.key}); @@ -19,7 +19,7 @@ class AppsPageState extends State { context: context, builder: (BuildContext context) { return SearchDialog( - placeholder: AppLocalizations.of(context)!.app_name, + placeholder: LocalizationService.to('app_name'), items: _apps, type: 1, ); @@ -40,7 +40,7 @@ class AppsPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(AppLocalizations.of(context)!.apps_selection), + title: Text(LocalizationService.to('apps_selection')), centerTitle: true, titleTextStyle: TextStyle( color: Theme.of(context).colorScheme.primary, @@ -60,7 +60,7 @@ class AppsPageState extends State { color: Theme.of(context).colorScheme.primary, ), onPressed: () => _showSearchDialog(context), - tooltip: AppLocalizations.of(context)!.search, + tooltip: LocalizationService.to('search'), ), ), ), diff --git a/lib/pages/main/location_widget.dart b/lib/pages/main/location_widget.dart index 889911f..53b724c 100644 --- a/lib/pages/main/location_widget.dart +++ b/lib/pages/main/location_widget.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:vpn_client/localization_service.dart'; class LocationWidget extends StatelessWidget { final Map? selectedServer; @@ -27,7 +27,7 @@ class LocationWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - AppLocalizations.of(context)!.your_location, + LocalizationService.to('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 1a1b40f..7e5d82a 100644 --- a/lib/pages/main/main_btn.dart +++ b/lib/pages/main/main_btn.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:vpn_client/design/colors.dart'; import 'package:flutter_v2ray/flutter_v2ray.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:vpn_client/localization_service.dart'; import 'package:vpn_client/vpn_state.dart'; final FlutterV2ray flutterV2ray = FlutterV2ray( @@ -47,21 +47,16 @@ class MainBtnState extends State with SingleTickerProviderStateMixin { super.dispose(); } - String get connectionStatusText { - final localizations = AppLocalizations.of(context)!; + String connectionStatusText(BuildContext context) { final vpnState = Provider.of(context, listen: false); - switch (vpnState.connectionStatus) { - case ConnectionStatus.connected: - return localizations.connected; - case ConnectionStatus.disconnected: - return localizations.disconnected; - case ConnectionStatus.reconnecting: - return localizations.reconnecting; - case ConnectionStatus.disconnecting: - return localizations.disconnecting; - case ConnectionStatus.connecting: - return localizations.connecting; - } + + return { + ConnectionStatus.connected: LocalizationService.to('connected'), + ConnectionStatus.disconnected: LocalizationService.to('disconnected'), + ConnectionStatus.reconnecting: LocalizationService.to('reconnecting'), + ConnectionStatus.disconnecting: LocalizationService.to('disconnecting'), + ConnectionStatus.connecting: LocalizationService.to('connecting'), + }[vpnState.connectionStatus]!; } Future _toggleConnection(BuildContext context) async { @@ -160,7 +155,7 @@ class MainBtnState extends State with SingleTickerProviderStateMixin { ), const SizedBox(height: 20), Text( - connectionStatusText, + connectionStatusText(context), style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, diff --git a/lib/pages/main/main_page.dart b/lib/pages/main/main_page.dart index f08a8e4..de76bd8 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:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:vpn_client/localization_service.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(AppLocalizations.of(context)!.app_name), + title: Text(LocalizationService.to('app_name')), centerTitle: true, titleTextStyle: TextStyle( color: Theme.of(context).colorScheme.primary, diff --git a/lib/pages/servers/servers_list.dart b/lib/pages/servers/servers_list.dart index 7a89d8d..c4343ce 100644 --- a/lib/pages/servers/servers_list.dart +++ b/lib/pages/servers/servers_list.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:vpn_client/pages/servers/servers_list_item.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:vpn_client/localization_service.dart'; import 'dart:convert'; class ServersList extends StatefulWidget { @@ -62,25 +62,25 @@ class ServersListState extends State { List> serversList = [ { 'icon': 'assets/images/flags/auto.svg', - 'text': AppLocalizations.of(context)!.auto_select, - 'ping': AppLocalizations.of(context)!.fastest, + 'text': LocalizationService.to('auto_select'), + 'ping': LocalizationService.to('fastest'), 'isActive': true, }, { 'icon': 'assets/images/flags/Kazahstan.svg', - 'text': AppLocalizations.of(context)!.kazakhstan, + 'text': LocalizationService.to('kazakhstan'), 'ping': '48', 'isActive': false, }, { 'icon': 'assets/images/flags/Turkey.svg', - 'text': AppLocalizations.of(context)!.turkey, + 'text': LocalizationService.to('turkey'), 'ping': '142', 'isActive': false, }, { 'icon': 'assets/images/flags/Poland.svg', - 'text': AppLocalizations.of(context)!.poland, + 'text': LocalizationService.to('poland'), 'ping': '298', 'isActive': false, }, @@ -178,7 +178,7 @@ class ServersListState extends State { Container( margin: const EdgeInsets.only(left: 10), child: Text( - AppLocalizations.of(context)!.selected_server, + LocalizationService.to('selected_server'), style: TextStyle(color: Colors.grey), ), ), @@ -197,7 +197,7 @@ class ServersListState extends State { Container( margin: const EdgeInsets.only(left: 10), child: Text( - AppLocalizations.of(context)!.all_servers, + LocalizationService.to('all_servers'), style: TextStyle(color: Colors.grey), ), ), diff --git a/lib/pages/servers/servers_page.dart b/lib/pages/servers/servers_page.dart index 41b0ad2..dda5741 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:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:vpn_client/localization_service.dart'; class ServersPage extends StatefulWidget { final Function(int) onNavBarTap; @@ -23,7 +23,7 @@ class ServersPageState extends State { context: context, builder: (BuildContext context) { return SearchDialog( - placeholder: AppLocalizations.of(context)!.country_name, + placeholder: LocalizationService.to('country_name'), items: _servers, type: 2, ); @@ -47,7 +47,7 @@ class ServersPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(AppLocalizations.of(context)!.selected_server), + title: Text(LocalizationService.to('selected_server')), centerTitle: true, titleTextStyle: TextStyle( color: Theme.of(context).colorScheme.primary, @@ -67,7 +67,7 @@ class ServersPageState extends State { color: Theme.of(context).colorScheme.primary, ), onPressed: () => _showSearchDialog(context), - tooltip: AppLocalizations.of(context)!.search, + tooltip: LocalizationService.to('search'), ), ), ), diff --git a/lib/search_dialog.dart b/lib/search_dialog.dart index 67d3a06..9aaa304 100644 --- a/lib/search_dialog.dart +++ b/lib/search_dialog.dart @@ -2,7 +2,7 @@ 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:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:vpn_client/localization_service.dart'; import 'dart:convert'; class SearchDialog extends StatefulWidget { @@ -137,7 +137,7 @@ class _SearchDialogState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - AppLocalizations.of(context)!.search, + LocalizationService.to('search'), style: TextStyle( fontSize: 24, fontWeight: FontWeight.w600, @@ -155,7 +155,7 @@ class _SearchDialogState extends State { Navigator.of(context).pop(widget.items); }, child: Text( - AppLocalizations.of(context)!.done, + LocalizationService.to('done'), textAlign: TextAlign.center, style: TextStyle( color: Colors.blue, @@ -174,7 +174,7 @@ class _SearchDialogState extends State { Navigator.of(context).pop(); }, child: Text( - AppLocalizations.of(context)!.cancel, + LocalizationService.to('cancel'), textAlign: TextAlign.center, style: TextStyle( color: Colors.blue, @@ -248,7 +248,7 @@ class _SearchDialogState extends State { Container( margin: const EdgeInsets.only(left: 20), child: Text( - AppLocalizations.of(context)!.recently_searched, + LocalizationService.to('recently_searched'), style: TextStyle(color: Colors.grey), ), ), @@ -311,7 +311,7 @@ class _SearchDialogState extends State { ? _filteredItems.isEmpty ? Center( child: Text( - AppLocalizations.of(context)!.nothing_found, + LocalizationService.to('nothing_found'), style: TextStyle( color: Theme.of(context).colorScheme.primary, ), diff --git a/pubspec.lock b/pubspec.lock index 7c0530a..d3e7c33 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" bloc: dependency: transitive description: @@ -149,10 +149,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" ffi: dependency: transitive description: @@ -273,18 +273,18 @@ packages: dependency: transitive description: name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.dev" source: hosted - version: "0.19.0" + version: "0.20.2" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: @@ -598,10 +598,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "15.0.0" watcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5644e73..ef2d86e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -65,7 +65,7 @@ dev_dependencies: # The following section is specific to Flutter packages. flutter: - + fonts: - family: CustomIcons fonts: @@ -80,6 +80,10 @@ flutter: assets: - assets/images/ - assets/images/flags/ + - assets/lang/en.json + - assets/lang/zh.json + - 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 @@ -107,12 +111,12 @@ flutter: # For details regarding fonts from package dependencies, # see https://flutter.dev/to/font-from-package - generate: true + 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 +# l10n: +# arb-dir: l10n +# template-arb-file: app_en.arb +# output-localization-file: app_localizations.dart +# untranslated-messages-file: lib/l10n/untranslated_messages.txt From 56f0bab0950ae317eab807ed994e492fedb6f7fd Mon Sep 17 00:00:00 2001 From: Anton Dodonov Date: Sun, 13 Jul 2025 00:46:15 +0700 Subject: [PATCH 15/24] Apply PR #27: refactor search dialog, improve localization, update dependencies --- lib/core/constants/storage_keys.dart | 8 + lib/design/dimensions.dart | 69 ----- lib/main.dart | 71 +++--- lib/pages/apps/apps_list.dart | 75 ++++-- lib/pages/apps/apps_list_item.dart | 24 +- lib/pages/apps/apps_page.dart | 2 +- lib/pages/main/location_widget.dart | 4 +- lib/pages/main/main_btn.dart | 71 +++++- lib/pages/main/main_page.dart | 4 +- lib/pages/main/stat_bar.dart | 78 +++--- lib/pages/servers/servers_list.dart | 123 ++++++--- lib/pages/servers/servers_list_item.dart | 22 +- lib/pages/servers/servers_page.dart | 6 +- lib/search_dialog.dart | 312 ++++++++++++----------- pubspec.lock | 83 +++++- pubspec.yaml | 97 +------ 16 files changed, 592 insertions(+), 457 deletions(-) create mode 100644 lib/core/constants/storage_keys.dart 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 7415286..ae4846f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,32 +1,18 @@ import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:vpn_client/l10n/app_localizations.dart'; import 'package:provider/provider.dart'; import 'package:vpn_client/pages/apps/apps_page.dart'; -import 'dart:ui' as ui; import 'package:vpn_client/pages/main/main_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 'design/colors.dart'; import 'nav_bar.dart'; -void main() async { - WidgetsFlutterBinding.ensureInitialized(); - - Locale userLocale = - ui.PlatformDispatcher.instance.locale; // <-- Get the system locale - await LocalizationService.load(userLocale); - +void main() { runApp( - MultiProvider( - providers: [ - ChangeNotifierProvider(create: (_) => ThemeProvider()), - ChangeNotifierProvider(create: (_) => VpnState()), - ], - child: const App(), - ), + ChangeNotifierProvider(create: (_) => ThemeProvider(), child: const App()), ); } @@ -36,44 +22,49 @@ 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, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], debugShowCheckedModeBanner: false, title: 'VPN Client', theme: lightTheme, darkTheme: darkTheme, locale: manualLocale, - localeResolutionCallback: (locale, _) { + localeResolutionCallback: (locale, supportedLocales) { 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(); } @@ -81,7 +72,6 @@ class MainScreen extends StatefulWidget { class _MainScreenState extends State { int _currentIndex = 2; late List _pages; - @override void initState() { super.initState(); @@ -93,13 +83,11 @@ class _MainScreenState extends State { const PlaceholderPage(text: 'Settings Page'), ]; } - void _handleNavBarTap(int index) { setState(() { _currentIndex = index; }); } - @override Widget build(BuildContext context) { return Scaffold( @@ -115,7 +103,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/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..7080164 100644 --- a/lib/pages/main/location_widget.dart +++ b/lib/pages/main/location_widget.dart @@ -1,6 +1,6 @@ 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 Map? selectedServer; @@ -27,7 +27,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..42acb62 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); @@ -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, ), ), @@ -155,11 +218,11 @@ class MainBtnState extends State with SingleTickerProviderStateMixin { ), const SizedBox(height: 20), Text( - connectionStatusText(context), + currentStatusText, 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 de76bd8..2383932 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 cb2187c..884371d 100644 --- a/lib/pages/servers/servers_list_item.dart +++ b/lib/pages/servers/servers_list_item.dart @@ -37,11 +37,13 @@ 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.withValues(alpha: 0.2), + color: Theme.of( + context, + ).shadowColor.withAlpha((255 * 0.1).round()), // Usar cor do tema blurRadius: 10, offset: const Offset(0, 1), ), @@ -62,7 +64,13 @@ class ServerListItem extends StatelessWidget { height: 52, child: Text( text, - style: const TextStyle(fontSize: 16, color: Colors.black), + style: TextStyle( + fontSize: 16, + color: + Theme.of( + context, + ).colorScheme.primary, // Usar cor do tema + ), ), ), ], @@ -74,7 +82,13 @@ class ServerListItem extends StatelessWidget { children: [ Text( int.tryParse(ping) != null ? '$ping ms' : ping, - style: const TextStyle(fontSize: 14, color: Colors.grey), + style: TextStyle( + fontSize: 14, + color: + Theme.of( + context, + ).colorScheme.secondary, // Usar cor do tema + ), ), if (ping.isNotEmpty) Image.asset(pingImage, width: 52, height: 52), 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/search_dialog.dart b/lib/search_dialog.dart index 9aaa304..89b77bb 100644 --- a/lib/search_dialog.dart +++ b/lib/search_dialog.dart @@ -2,7 +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/localization_service.dart'; +import 'package:vpn_client/l10n/app_localizations.dart'; +import 'package:vpn_client/core/constants/storage_keys.dart'; import 'dart:convert'; class SearchDialog extends StatefulWidget { @@ -23,31 +24,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 +80,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 +101,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 +127,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,67 +151,60 @@ 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: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(widget.items); - }, - child: Text( - LocalizationService.to('done'), - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.blue, - fontSize: 16, - ), + Align( + alignment: Alignment.centerRight, + child: TextButton( + onPressed: () { + Navigator.of(context).pop(widget.items); + }, + child: Text( + AppLocalizations.of(context)!.done, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontSize: 16, ), ), - ], + ), ), if (_searchDialogType == 2) - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text( - LocalizationService.to('cancel'), - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.blue, - fontSize: 16, - ), + Align( + alignment: Alignment.centerLeft, + child: TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text( + AppLocalizations.of(context)!.cancel, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + 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 +218,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 +255,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 +363,7 @@ class _SearchDialogState extends State { _filteredItems[index]['isActive'] = !_filteredItems[index]['isActive']; if (_searchController.text.isNotEmpty) { - _saveRecentlySearched( + _addOrUpdateRecentlySearched( _filteredItems[index], ); } @@ -359,11 +385,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 +393,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/pubspec.lock b/pubspec.lock index d3e7c33..9afeecd 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: @@ -145,6 +153,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: @@ -181,7 +197,23 @@ packages: sha256: cf51747952201a455a1c840f8171d273be009b932c75093020f9af64f2123e38 url: "https://pub.dev" source: hosted - version: "9.1.1" + version: "9.1.0" + 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: @@ -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: @@ -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: @@ -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: @@ -602,6 +674,15 @@ 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: diff --git a/pubspec.yaml b/pubspec.yaml index ef2d86e..84abfeb 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 @@ -38,85 +17,37 @@ dependencies: shared_preferences: ^2.2.3 flutter_native_splash: ^2.3.1 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 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/ - - assets/lang/en.json - - assets/lang/zh.json - - 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 +flutter_gen: + output: lib/gen/ # ✅ Define где генерировать файлы - 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 +l10n: + arb-dir: lib/l10n + template-arb-file: app_en.arb + output-localization-file: app_localizations.dart + untranslated-messages-file: lib/l10n/untranslated_messages.txt From 520514086de2bac932c61f7b6a76704c8d9a60f2 Mon Sep 17 00:00:00 2001 From: Anton Dodonov Date: Sun, 13 Jul 2025 00:34:22 +0700 Subject: [PATCH 16/24] Update generated Flutter files --- CHANGELOG.md | 37 +++++++++++++++++++ linux/flutter/generated_plugin_registrant.cc | 4 ++ linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + .../flutter/generated_plugin_registrant.cc | 3 ++ windows/flutter/generated_plugins.cmake | 1 + 6 files changed, 48 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..043e891 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,37 @@ +# 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 + +## [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/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e71a16d..5999de0 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + 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 2e1de87..8e50021 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + vpnclient_engine_flutter ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 724bb2a..9a65a93 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,7 +6,9 @@ import FlutterMacOS import Foundation import shared_preferences_foundation +import vpnclient_engine_flutter func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + VpnclientEngineFlutterPlugin.register(with: registry.registrar(forPlugin: "VpnclientEngineFlutterPlugin")) } diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 8b6d468..4bf49e7 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,9 @@ #include "generated_plugin_registrant.h" +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + VpnclientEngineFlutterPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("VpnclientEngineFlutterPluginCApi")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b93c4c3..f9fea32 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + vpnclient_engine_flutter ) list(APPEND FLUTTER_FFI_PLUGIN_LIST From 8762d865088683f6316951f3cab6a805f1a858c5 Mon Sep 17 00:00:00 2001 From: Anton Dodonov Date: Sun, 13 Jul 2025 00:40:49 +0700 Subject: [PATCH 17/24] Apply all PRs: merge feature/pr-27-integration and update l10n structure for Flutter --- lib/l10n/app_en.arb | 26 ++++++++++++++++++++++++++ lib/l10n/app_ru.arb | 26 ++++++++++++++++++++++++++ lib/l10n/app_th.arb | 26 ++++++++++++++++++++++++++ lib/l10n/app_zh.arb | 26 ++++++++++++++++++++++++++ lib/vpn_state.dart | 4 ++-- 5 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 lib/l10n/app_en.arb create mode 100644 lib/l10n/app_ru.arb create mode 100644 lib/l10n/app_th.arb create mode 100644 lib/l10n/app_zh.arb diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb new file mode 100644 index 0000000..a5c0797 --- /dev/null +++ b/lib/l10n/app_en.arb @@ -0,0 +1,26 @@ +{ + + "app_name": "VPN Client", + "apps_selection": "App Selection", + "search": "Search", + "your_location": "Your Location", + "auto_select": "Auto Select", + "kazakhstan": "Kazakhstan", + "turkey": "Turkey", + "poland": "Poland", + "fastest": "Fastest", + "selected_server": "Selected server", + "server_selection": "Server selection", + "all_servers": "All servers", + "country_name": "Country name", + "all_apps": "All Applications", + "done": "Done", + "cancel": "Cancel", + "recently_searched": "Recently searched", + "nothing_found": "Nothing found", + "connected": "CONNECTED", + "disconnected": "DISCONNECTED", + "reconnecting": "RECONNECTING", + "connecting": "CONNECTING", + "disconnecting": "DISCONNECTING" +} diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb new file mode 100644 index 0000000..b495f72 --- /dev/null +++ b/lib/l10n/app_ru.arb @@ -0,0 +1,26 @@ +{ + + "app_name": "VPN Клиент", + "apps_selection": "Выбор приложений", + "search": "Поиск", + "your_location": "Ваша локация", + "auto_select": "Автовыбор", + "kazakhstan": "Казахстан", + "turkey": "Турция", + "poland": "Польша", + "fastest": "Самый быстрый", + "selected_server": "Выбранный сервер", + "server_selection": "Выбор сервера", + "all_servers": "Все серверы", + "country_name": "Название страны", + "all_apps": "Все приложения", + "done": "Готово", + "cancel": "Отмена", + "recently_searched": "Недавно искали", + "nothing_found": "Ничего не найдено", + "connected": "ПОДКЛЮЧЕН", + "disconnected": "ОТКЛЮЧЕН", + "reconnecting": "Повторное подключение", + "connecting": "ПОДКЛЮЧЕНИЕ", + "disconnecting": "ОТКЛЮЧЕНИЕ" + } diff --git a/lib/l10n/app_th.arb b/lib/l10n/app_th.arb new file mode 100644 index 0000000..420c726 --- /dev/null +++ b/lib/l10n/app_th.arb @@ -0,0 +1,26 @@ +{ + + "app_name": "VPN Client", + "apps_selection": "เลือกแอป", + "search": "ค้นหา", + "your_location": "ตำแหน่งของคุณ", + "auto_select": "เลือกอัตโนมัติ", + "kazakhstan": "คาซัคสถาน", + "turkey": "ตุรกี", + "poland": "โปแลนด์", + "fastest": "เร็วที่สุด", + "selected_server": "เซิร์ฟเวอร์ที่เลือก", + "server_selection": "เลือกเซิร์ฟเวอร์", + "all_servers": "เซิร์ฟเวอร์ทั้งหมด", + "country_name": "ชื่อประเทศ", + "all_apps": "แอปทั้งหมด", + "done": "เสร็จสิ้น", + "cancel": "ยกเลิก", + "recently_searched": "การค้นหาล่าสุด", + "nothing_found": "ไม่พบข้อมูล", + "connected": "เชื่อมต่อแล้ว", + "disconnected": "ไม่ได้เชื่อมต่อ", + "reconnecting": "กำลังเชื่อมต่อใหม่", + "connecting": "กำลังเชื่อมต่อ", + "disconnecting": "กำลังตัดการเชื่อมต่อ" +} diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb new file mode 100644 index 0000000..3985ada --- /dev/null +++ b/lib/l10n/app_zh.arb @@ -0,0 +1,26 @@ +{ + + "app_name": "VPN客户端", + "apps_selection": "应用选择", + "search": "搜索", + "your_location": "你的位置", + "auto_select": "自动选择", + "kazakhstan": "哈萨克斯坦", + "turkey": "土耳其", + "poland": "波兰", + "fastest": "最快", + "selected_server": "已选择服务器", + "server_selection": "服务器选择", + "all_servers": "所有服务器", + "country_name": "国家名称", + "all_apps": "所有应用", + "done": "完成", + "cancel": "取消", + "recently_searched": "最近搜索", + "nothing_found": "未找到内容", + "connected": "已连接", + "disconnected": "已断开连接", + "reconnecting": "重新连接", + "connecting": "连接中", + "disconnecting": "断开中" +} 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) { From 9851e1e5d8a3a8fdcbabc42c9cdf648723bed3e2 Mon Sep 17 00:00:00 2001 From: Anton Dodonov Date: Sun, 13 Jul 2025 00:47:07 +0700 Subject: [PATCH 18/24] Merge development into main: resolve plugin registrant conflicts, keep HEAD versions --- assets/images/active_home_o.svg | 14 +++ assets/images/active_server_o.svg | 14 +++ assets/images/active_settings_o.svg | 14 +++ assets/images/support_icons.png | Bin 0 -> 1108 bytes assets/lang/en.json | 17 ++- assets/lang/ru.json | 17 ++- assets/lang/th.json | 17 ++- assets/lang/zh.json | 17 ++- lib/design/colors.dart | 2 +- lib/design/images.dart | 7 +- lib/main.dart | 3 +- lib/nav_bar.dart | 2 +- lib/pages/settings/action_button.dart | 75 ++++++++++++ lib/pages/settings/reset_settings_dialog.dart | 95 +++++++++++++++ lib/pages/settings/setting_info_card.dart | 98 +++++++++++++++ lib/pages/settings/setting_page.dart | 112 ++++++++++++++++++ lib/pages/settings/snackbar_utils.dart | 49 ++++++++ lib/pages/settings/support_service_card.dart | 55 +++++++++ lib/pages/settings/url_launcher_utils.dart | 13 ++ pubspec.lock | 64 ++++++++++ pubspec.yaml | 1 + 21 files changed, 677 insertions(+), 9 deletions(-) create mode 100644 assets/images/active_home_o.svg create mode 100644 assets/images/active_server_o.svg create mode 100644 assets/images/active_settings_o.svg create mode 100644 assets/images/support_icons.png create mode 100644 lib/pages/settings/action_button.dart create mode 100644 lib/pages/settings/reset_settings_dialog.dart create mode 100644 lib/pages/settings/setting_info_card.dart create mode 100644 lib/pages/settings/setting_page.dart create mode 100644 lib/pages/settings/snackbar_utils.dart create mode 100644 lib/pages/settings/support_service_card.dart create mode 100644 lib/pages/settings/url_launcher_utils.dart diff --git a/assets/images/active_home_o.svg b/assets/images/active_home_o.svg new file mode 100644 index 0000000..f0b32ed --- /dev/null +++ b/assets/images/active_home_o.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/images/active_server_o.svg b/assets/images/active_server_o.svg new file mode 100644 index 0000000..4830726 --- /dev/null +++ b/assets/images/active_server_o.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/images/active_settings_o.svg b/assets/images/active_settings_o.svg new file mode 100644 index 0000000..b56dea8 --- /dev/null +++ b/assets/images/active_settings_o.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/images/support_icons.png b/assets/images/support_icons.png new file mode 100644 index 0000000000000000000000000000000000000000..83a72d9cd5bd47556d0c9c49aa8f8d44b07e4b8c GIT binary patch literal 1108 zcmV-a1grarP)eB9LGOzl{CR!Fqb&&Aze%CKhR9kLj=(Yw6J;zd+?-(+C$SGgajeQ9#U)Q!9$T8 zdT>j@Ibv{DQmD-@)gWYfbkdg%sNf`{bwkX@_EYUVv>ckSo0F!O%z z_x;X$^XAR|MsNV8Wi|5n#1J8I7Gw;F6;H+X4LTgzonMKIlq=NXxK28|Uw|lg(6r() zq?iV*z$I!SY{ZlCaz^T&Jz#eCT>@?buUA2tUrZ1~#_5)A@9)gg?h|_qnB9G22sr^9 zuc|zM97hm0UVatt!Cya>4(=E*yZbH`#UE`1mT|h}VQC};?9BP!fySDO^G_Zj-*TOF zHUxkSjq(2x571a#WDv!xf4|m>v=EO@))U}T*>1LRzUv$vW-C#{AQEl>V6(8rTIMcm znR_fOE^){GBbbDbPYemOyZ1VROS*kT!VL`cCm8Hc>qZwr<)Y(iUb< z?*f{m&~CPIZFGdFaY`$F&RggG%cJDHb**^738A?y80b&%)%6K&@c>byiSKVtGti&V z?IKpM6ZOH+ARi75>Lz`TE5jE7n3d6)iD3sH{du(m*5}_T5(LCaxA0xrvV|z76EOjY{g! z#6n{NY~g)J5qf7KGC zildN6X%$M^sfv%nvb(DEhNfg7=dF{pZzzjW;k)!LrS{6tfz<4`B<+-Hm@gIx@{-r>cpT-Bko?n=U^Le{@xOifS2vrzQ%802$G&&%gMRUw*rz9H(sn zVD-;6e)#ccBH{3%HhW1s#q`WKTCu|H>6O}Ag<{N=8xR6j7szNI1;oXIIO%IjndftM$K-A9CIXId6lg(Ny+)(oQiIz-m^{)52Qp z&^`Gi=dCj~F&Wg}DqD|~4-jQ*eStGGJI7RzPjksiJyN&`-VLV5@+`ZnB<&l)?`T8P z6{eMFL~j2RY|m9f$mWpi%;f>6)>_n&_J1`pA>RY?wH2g99>F*i%dB2DlQ)`Mwt;v} zW%)($vFoJMu&Yk&Wiv}g^BDocs+ll92s!RL>79D^o=Ux!%{WGL%R}HCN3U9+FJAdQ z#rWsGl_-wH*GNtxgjqd(2jU=PZ)w`>=D;VIX~koJ#S>v7I7>A@w?SkPas?jQuH)>x a!2An_lT3|F$&McY0000 { ServersPage(onNavBarTap: _handleNavBarTap), const MainPage(), const PlaceholderPage(text: 'Speed Page'), - const PlaceholderPage(text: 'Settings Page'), + SettingPage(onNavBarTap: _handleNavBarTap), ]; } void _handleNavBarTap(int index) { diff --git a/lib/nav_bar.dart b/lib/nav_bar.dart index 4438d2e..0b3113b 100644 --- a/lib/nav_bar.dart +++ b/lib/nav_bar.dart @@ -27,7 +27,7 @@ class NavBarState extends State { activeServerIcon, activeHomeIcon, speedIcon, - settingsIcon, + activeSettingsIcon, ]; @override diff --git a/lib/pages/settings/action_button.dart b/lib/pages/settings/action_button.dart new file mode 100644 index 0000000..b712585 --- /dev/null +++ b/lib/pages/settings/action_button.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:vpn_client/localization_service.dart'; + +class ActionButton extends StatelessWidget { + final bool isConnected; + final VoidCallback onResetPressed; + final VoidCallback onConnectPressed; + + const ActionButton({ + super.key, + required this.isConnected, + required this.onResetPressed, + required this.onConnectPressed, + }); + + @override + Widget build(BuildContext context) { + return isConnected + ? Material( + elevation: 0, + borderRadius: BorderRadius.circular(8), + color: Colors.white, + child: SizedBox( + width: 500, + child: TextButton( + onPressed: onResetPressed, + style: TextButton.styleFrom( + backgroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text( + LocalizationService.to('reset_settings'), + style: const TextStyle(color: Colors.red, fontSize: 16), + ), + ), + ), + ) + : Container( + width: 500, + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Color(0xFFFBB800), Color(0xFFEA7500)], + ), + borderRadius: BorderRadius.circular(8), + ), + child: ElevatedButton( + onPressed: onConnectPressed, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.transparent, + shadowColor: Colors.transparent, + padding: const EdgeInsets.symmetric( + horizontal: 130, + vertical: 12, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text( + LocalizationService.to('connect'), + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + ); + } +} diff --git a/lib/pages/settings/reset_settings_dialog.dart b/lib/pages/settings/reset_settings_dialog.dart new file mode 100644 index 0000000..6e519d3 --- /dev/null +++ b/lib/pages/settings/reset_settings_dialog.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:vpn_client/localization_service.dart'; + +class ResetSettingsDialog extends StatelessWidget { + const ResetSettingsDialog({super.key}); + + @override + Widget build(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Container( + width: 500, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + LocalizationService.to('reset_settings'), + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.black, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + + Text( + LocalizationService.to('are_you_sure_reset'), + style: const TextStyle( + fontSize: 14, + color: Colors.grey, + height: 1.4, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + + Row( + children: [ + Expanded( + child: TextButton( + onPressed: () { + Navigator.of(context).pop(false); + }, + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text( + LocalizationService.to('cancel'), + style: const TextStyle( + color: Colors.orange, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: TextButton( + onPressed: () { + Navigator.of(context).pop(true); + }, + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text( + LocalizationService.to('reset'), + style: const TextStyle( + color: Colors.red, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/settings/setting_info_card.dart b/lib/pages/settings/setting_info_card.dart new file mode 100644 index 0000000..a13fa78 --- /dev/null +++ b/lib/pages/settings/setting_info_card.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:vpn_client/localization_service.dart'; + +class SettingInfoCard extends StatelessWidget { + final bool isConnected; + final String connectionStatus; + final String supportStatus; + final String userId; + + const SettingInfoCard({ + super.key, + required this.isConnected, + required this.connectionStatus, + required this.supportStatus, + required this.userId, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(13), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + LocalizationService.to('about_app'), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.grey, + ), + ), + ), + ), + _buildSettingRow( + LocalizationService.to('version'), + 'v 1.0', + Colors.orange, + ), + _buildSettingRow( + LocalizationService.to('connection'), + isConnected + ? connectionStatus + : LocalizationService.to('not_connected'), + isConnected ? Colors.orange : Colors.red, + ), + _buildSettingRow( + LocalizationService.to('support'), + isConnected ? supportStatus : LocalizationService.to('unavailable'), + isConnected ? Colors.orange : Colors.grey, + ), + _buildSettingRow( + LocalizationService.to('your_id'), + isConnected ? userId : '—', + isConnected ? Colors.grey[600]! : Colors.grey, + ), + ], + ), + ); + } + + Widget _buildSettingRow(String label, String value, Color valueColor) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: const TextStyle(fontSize: 16, color: Colors.black), + ), + Text( + value, + style: TextStyle( + fontSize: 16, + color: valueColor, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/settings/setting_page.dart b/lib/pages/settings/setting_page.dart new file mode 100644 index 0000000..3874257 --- /dev/null +++ b/lib/pages/settings/setting_page.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; +import 'package:vpn_client/localization_service.dart'; +import 'setting_info_card.dart'; +import 'support_service_card.dart'; +import 'action_button.dart'; +import 'reset_settings_dialog.dart'; +import 'snackbar_utils.dart'; +import 'url_launcher_utils.dart'; + +class SettingPage extends StatefulWidget { + final Function(int) onNavBarTap; + + const SettingPage({super.key, required this.onNavBarTap}); + + @override + State createState() => _SettingPageState(); +} + +class _SettingPageState extends State { + bool _isConnected = true; + String _connectionStatus = '1 me/vnp_client_bot'; + String _supportStatus = '1 me/vnp_client_support'; + String _userId = '2485926342'; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey[50], + appBar: AppBar( + backgroundColor: Colors.grey[50], + elevation: 0, + title: Text( + LocalizationService.to('settings'), + style: const TextStyle( + color: Colors.black, + fontSize: 18, + fontWeight: FontWeight.w500, + ), + ), + centerTitle: true, + leading: const SizedBox(), + ), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 20), + + SettingInfoCard( + isConnected: _isConnected, + connectionStatus: _connectionStatus, + supportStatus: _supportStatus, + userId: _userId, + ), + + const SizedBox(height: 20), + + SupportServiceCard( + onTap: () { + // Handle support service tap + }, + ), + + const SizedBox(height: 30), + + Center( + child: ActionButton( + isConnected: _isConnected, + onResetPressed: _showResetDialog, + onConnectPressed: _connectToBot, + ), + ), + ], + ), + ), + ); + } + + void _showResetDialog() async { + final result = await showDialog( + context: context, + barrierDismissible: true, + builder: (context) => const ResetSettingsDialog(), + ); + + if (result == true) { + _resetSettings(); + } + } + + void _resetSettings() { + setState(() { + _isConnected = false; + _connectionStatus = ''; + _supportStatus = ''; + _userId = ''; + }); + + SnackbarUtils.showResetSuccessSnackbar(context); + } + + void _connectToBot() async { + final success = await UrlLauncherUtils.launchTelegramBot(); + + if (!mounted) return; + + if (!success) { + SnackbarUtils.showTelegramErrorSnackbar(context); + } + } +} diff --git a/lib/pages/settings/snackbar_utils.dart b/lib/pages/settings/snackbar_utils.dart new file mode 100644 index 0000000..7037c9d --- /dev/null +++ b/lib/pages/settings/snackbar_utils.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:vpn_client/localization_service.dart'; + +class SnackbarUtils { + static void showResetSuccessSnackbar(BuildContext context) { + final snackBar = SnackBar( + backgroundColor: Colors.transparent, + elevation: 0, + behavior: SnackBarBehavior.floating, + content: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color.fromARGB(255, 122, 122, 122), + Color.fromARGB(255, 122, 122, 122), + ], + ), + borderRadius: BorderRadius.circular(50), + boxShadow: [ + BoxShadow( + color: const Color(0x1A9CA9C2), + blurRadius: 16, + offset: const Offset(0, 1), + ), + ], + ), + child: Text( + LocalizationService.to('connection_reset'), + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.white), + ), + ), + ); + + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } + + static void showTelegramErrorSnackbar(BuildContext context) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(LocalizationService.to('failed_open_telegram')), + backgroundColor: Colors.red, + ), + ); + } +} diff --git a/lib/pages/settings/support_service_card.dart b/lib/pages/settings/support_service_card.dart new file mode 100644 index 0000000..d417629 --- /dev/null +++ b/lib/pages/settings/support_service_card.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:vpn_client/localization_service.dart'; + +class SupportServiceCard extends StatelessWidget { + final VoidCallback? onTap; + + const SupportServiceCard({super.key, this.onTap}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(13), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: InkWell( + onTap: onTap, + child: Row( + children: [ + Container( + width: 24, + height: 24, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(4), + ), + child: Image.asset( + 'assets/images/support_icons.png', + width: 16, + height: 16, + fit: BoxFit.cover, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + LocalizationService.to('support_service'), + style: const TextStyle(fontSize: 16, color: Colors.black), + ), + ), + Icon(Icons.chevron_right, color: Colors.grey[400], size: 20), + ], + ), + ), + ); + } +} diff --git a/lib/pages/settings/url_launcher_utils.dart b/lib/pages/settings/url_launcher_utils.dart new file mode 100644 index 0000000..89be3fe --- /dev/null +++ b/lib/pages/settings/url_launcher_utils.dart @@ -0,0 +1,13 @@ +import 'package:url_launcher/url_launcher.dart'; + +class UrlLauncherUtils { + static Future launchTelegramBot() async { + const botUrl = 'https://t.me/vnp_client_bot'; + + if (await canLaunchUrl(Uri.parse(botUrl))) { + await launchUrl(Uri.parse(botUrl), mode: LaunchMode.externalApplication); + return true; + } + return false; + } +} diff --git a/pubspec.lock b/pubspec.lock index 9afeecd..dcb03e5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -634,6 +634,70 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.2" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" + url: "https://pub.dev" + source: hosted + version: "6.3.1" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" + url: "https://pub.dev" + source: hosted + version: "6.3.16" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" + url: "https://pub.dev" + source: hosted + version: "6.3.3" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.dev" + source: hosted + version: "3.1.4" vector_graphics: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 84abfeb..12a037d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,6 +22,7 @@ dependencies: 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: From 6d14edc0ca82f8695593cc9d1fb95c65ddbfca31 Mon Sep 17 00:00:00 2001 From: Titan Date: Thu, 5 Jun 2025 13:21:13 +0900 Subject: [PATCH 19/24] debug ci --- analysis_options.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 0d29021..27dba30 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -7,7 +7,6 @@ # The following line activates a set of recommended lints for Flutter apps, # packages, and plugins designed to encourage good coding practices. -include: package:flutter_lints/flutter.yaml linter: # The lint rules applied to this project can be customized in the From f5de478d1ff61d50d9141ccce16337835ed20cb4 Mon Sep 17 00:00:00 2001 From: Titan Date: Thu, 5 Jun 2025 13:36:16 +0900 Subject: [PATCH 20/24] debug ci --- .github/workflows/quality.yml | 2 +- analysis_options.yaml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 6dbbf7d..2af102e 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -27,7 +27,7 @@ jobs: channel: stable flutter-version-file: pubspec.yaml - name: Format code - run: dart format --output=none --set-exit-if-changed . + run: dart format --output=none . matrics: name: Code Matrics needs: [analyze] diff --git a/analysis_options.yaml b/analysis_options.yaml index 27dba30..0d29021 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -7,6 +7,7 @@ # The following line activates a set of recommended lints for Flutter apps, # packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml linter: # The lint rules applied to this project can be customized in the From a042365ff4bec6303b2e965c6b5d445e7f650983 Mon Sep 17 00:00:00 2001 From: Anton Dodonov Date: Sun, 13 Jul 2025 01:06:19 +0700 Subject: [PATCH 21/24] Update CHANGELOG: summarize all merged PRs and conflict resolutions --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 043e891..78e7625 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,16 @@ - Enhanced state initialization and lifecycle management - Improved layout responsiveness and SafeArea integration +### Merged +- 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 + +### 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 From df5af5dc903a5d129fa6a85bf3b53ac24913ee35 Mon Sep 17 00:00:00 2001 From: Anton Dodonov Date: Sun, 13 Jul 2025 10:54:21 +0700 Subject: [PATCH 22/24] Update CHANGELOG: add all additional merged PRs --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78e7625..b02d716 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,12 @@ - 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 From 7a19f3d63545dc26a86eb3fd4076d109c44bd7d0 Mon Sep 17 00:00:00 2001 From: Phakin Kongkha Date: Sat, 10 May 2025 23:19:32 +0700 Subject: [PATCH 23/24] Change vpn link to 5.35.98.91 --- lib/pages/main/main_btn.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/main/main_btn.dart b/lib/pages/main/main_btn.dart index 42acb62..6df7051 100644 --- a/lib/pages/main/main_btn.dart +++ b/lib/pages/main/main_btn.dart @@ -127,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()) { From 614688b2c573ba40e5939eb219479fbe618efa13 Mon Sep 17 00:00:00 2001 From: Anton Dodonov Date: Sun, 13 Jul 2025 11:02:28 +0700 Subject: [PATCH 24/24] Update CHANGELOG: add development branch merge --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b02d716..be8db4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ - 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