From c468dadbca168ce768882399ca06b87b47c0adbe Mon Sep 17 00:00:00 2001 From: "anton.v.dodonov" Date: Sun, 27 Apr 2025 15:56:11 +0000 Subject: [PATCH] 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: