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 ae4846f..5a685f4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,19 +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 '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 { @@ -22,49 +21,21 @@ class App extends StatelessWidget { @override Widget build(BuildContext context) { final themeProvider = Provider.of(context); - final Locale? manualLocale = null; + return MaterialApp( debugShowCheckedModeBanner: false, title: 'VPN Client', theme: lightTheme, darkTheme: darkTheme, - locale: manualLocale, - localeResolutionCallback: (locale, supportedLocales) { - if (locale == null) return const Locale('en'); - 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'); - } - 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(); } @@ -72,6 +43,7 @@ class MainScreen extends StatefulWidget { class _MainScreenState extends State { int _currentIndex = 2; late List _pages; + @override void initState() { super.initState(); @@ -79,15 +51,17 @@ 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(), ]; } + void _handleNavBarTap(int index) { setState(() { _currentIndex = index; }); } + @override Widget build(BuildContext context) { return Scaffold( @@ -95,16 +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 4438d2e..b9073e1 100644 --- a/lib/nav_bar.dart +++ b/lib/nav_bar.dart @@ -1,70 +1,54 @@ 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( - duration: const Duration(milliseconds: 200), - curve: Curves.easeInOut, - padding: const EdgeInsets.all(8), - child: isActive ? _activeIcons[index] : _inactiveIcons[index], - ), + onTap: () => onItemTapped(index), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + padding: const EdgeInsets.all(8), + 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 7080164..b7766d7 100644 --- a/lib/pages/main/location_widget.dart +++ b/lib/pages/main/location_widget.dart @@ -2,20 +2,67 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:vpn_client/l10n/app_localizations.dart'; + class LocationWidget extends StatelessWidget { + final String title; final Map? selectedServer; + final VoidCallback? onTap; - const LocationWidget({super.key, this.selectedServer}); + const LocationWidget({ + super.key, + required this.title, + this.selectedServer, + this.onTap, + }); @override Widget build(BuildContext context) { - final String locationName = selectedServer?['text'] ?? '...'; - final String iconPath = - selectedServer?['icon'] ?? 'assets/images/flags/auto.svg'; + final String locationName = selectedServer?['text'] ?? '...'; final String iconPath = selectedServer?['icon'] ?? 'assets/images/flags/auto.svg'; - return Container( - margin: const EdgeInsets.all(30), - padding: const EdgeInsets.only(left: 14), + return GestureDetector( onTap: onTap, + child: Container( + padding: const EdgeInsets.only(left: 14), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.onSurface, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.secondary, + ), + ), + Text( + locationName, + style: TextStyle( + fontSize: 17, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.primary, + ), + ), + ], + ), + const Spacer(), + Column( + children: [ + const SizedBox(height: 20), + SvgPicture.asset(iconPath, width: 48, height: 48), + ], + ), + ], + ), + ), + ); + } +} decoration: BoxDecoration( color: Theme.of(context).colorScheme.onSurface, borderRadius: BorderRadius.circular(12), diff --git a/lib/pages/main/main_btn.dart b/lib/pages/main/main_btn.dart index 6df7051..42f6e54 100644 --- a/lib/pages/main/main_btn.dart +++ b/lib/pages/main/main_btn.dart @@ -1,59 +1,26 @@ 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: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) { - // Handle status changes if needed - }, -); -enum VpnConnectionState { connected, disconnected, connecting, disconnecting } 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'); - /// - 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; @override void initState() { super.initState(); + _animationController = AnimationController( vsync: this, duration: const Duration(seconds: 1), @@ -61,13 +28,7 @@ class MainBtnState extends State with SingleTickerProviderStateMixin { _sizeAnimation = Tween(begin: 0, end: 150).animate( CurvedAnimation(parent: _animationController, curve: Curves.ease), ); - - WidgetsBinding.instance.addPostFrameCallback((_) { - final vpnState = Provider.of(context, listen: false); - if (vpnState.connectionStatus == ConnectionStatus.connected) { - _animationController.forward(); - } - }); + _animationController.repeat(reverse: true); } @override @@ -76,117 +37,39 @@ class MainBtnState extends State with SingleTickerProviderStateMixin { super.dispose(); } - String connectionStatusText(BuildContext context) { - final vpnState = Provider.of(context, listen: false); - - 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]!; - } - - 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); - - switch (vpnState.connectionStatus) { - case ConnectionStatus.disconnected: - vpnState.setConnectionStatus(ConnectionStatus.connecting); - _animationController.repeat(reverse: true); - 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); - - if (await flutterV2ray.requestPermission()) { - await flutterV2ray.startV2Ray( - remark: parser.remark, - config: parser.getFullConfiguration(), - blockedApps: null, - bypassSubnets: null, - proxyOnly: false, - ); - } - - 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: - } - } - @override Widget build(BuildContext context) { - final vpnState = Provider.of(context); - return Column( children: [ Text( - vpnState.connectionTimeText, + widget.connectionTime, style: TextStyle( fontSize: 40, fontWeight: FontWeight.w600, color: - _vpnState == VpnConnectionState.connected + widget.connectionStatus == 'Connected' ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.secondary, ), ), const SizedBox(height: 70), GestureDetector( - onTap: () => _toggleConnection(context), + 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: - Theme.of(context) - .colorScheme - .surfaceContainerHighest, // Usar cor do tema conforme sugestão do linter + decoration: const BoxDecoration( + color: Colors.grey, shape: BoxShape.circle, ), ), @@ -218,14 +101,43 @@ class MainBtnState extends State with SingleTickerProviderStateMixin { ), const SizedBox(height: 20), Text( - currentStatusText, - 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: Theme.of(context).textTheme.bodyLarge?.color, + color: Colors.black, ), ), ], ); } } +// 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'; + + +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 2383932..676e98a 100644 --- a/lib/pages/main/main_page.dart +++ b/lib/pages/main/main_page.dart @@ -1,79 +1,60 @@ 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/l10n/app_localizations.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; - bool _isInitialized = false; - - @override - void initState() { - super.initState(); - _loadSelectedServer(); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - if (!_isInitialized) { - // Schedule VpnState connection status update after build - WidgetsBinding.instance.addPostFrameCallback((_) {}); - _isInitialized = true; - } - } - - 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: Text(AppLocalizations.of(context).appName), + 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: SafeArea( + body: Padding( + padding: const EdgeInsets.all(16.0), child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const StatBar(), - const MainBtn(), - LocationWidget(selectedServer: _selectedServer), + 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(), + const MainBtn(), + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + LocationWidget(selectedServer: _selectedServer) + // GestureDetector( + // onTap: _navigateToServersList, + // child: LocationWidget(selectedServer: _selectedServer), + // ), + ], + ), + ], + ),*/ ); } } diff --git a/lib/pages/main/stat_bar.dart b/lib/pages/main/stat_bar.dart index ab669e3..63e6971 100644 --- a/lib/pages/main/stat_bar.dart +++ b/lib/pages/main/stat_bar.dart @@ -1,85 +1,87 @@ 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 Container( - margin: EdgeInsets.only(top: 37), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _buildStatItem(CustomIcons.download, '0 Mb/s', context), - _buildStatItem(CustomIcons.upload, '0 Mb/s', context), - _buildStatItem(CustomIcons.ping, '0 ms', context), - ], - ), + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 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: - (MediaQuery.of(context).size.width / 3) - 20, // Para dar algum espaço - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), + width: 100, + height: 75, decoration: BoxDecoration( - color: Theme.of(context).colorScheme.onSurface, - borderRadius: BorderRadius.circular(12), - boxShadow: [ + boxShadow: const [ BoxShadow( - color: Theme.of( - context, - ).shadowColor.withAlpha((255 * 0.1).round()), // Usar cor do tema - offset: const Offset(0.0, 2.0), - blurRadius: 8.0, + color: Color(0x1A9CB2C2), + offset: Offset(0.0, 1.0), + blurRadius: 32.0, ), ], + color: Theme.of(context).colorScheme.onSurface, + borderRadius: BorderRadius.circular(12), ), - // 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, + 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(stat['icon'], + size: 20, + color: Theme.of(context).colorScheme.onSurface, + ), ), - ), - const SizedBox(height: 8), - Text( - text, - style: TextStyle( - fontSize: fontSize14, // Usando constante de dimensions.dart - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.primary, + const SizedBox(height: 6), + Text( + stat['text'], + style: const TextStyle( + fontSize: 14, + 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 1868347..de07e79 100644 --- a/lib/pages/servers/servers_list.dart +++ b/lib/pages/servers/servers_list.dart @@ -1,203 +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 '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 +class ServersList extends StatelessWidget { + final List> servers; + final Function(Map) onTap; - const ServersList({ - super.key, - this.onServersLoaded, - this.servers, - this.onItemTapNavigate, - }); - - @override - State createState() => ServersListState(); -} - -class ServersListState extends State { - List> _servers = []; - bool _isLoading = true; - bool _dataLoaded = false; // Flag para controlar o carregamento inicial - - @override - void initState() { - super.initState(); - if (widget.servers != null && widget.servers!.isNotEmpty) { - _servers = widget.servers!; - _isLoading = false; - _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 (!_dataLoaded) { - // Carregar apenas se os dados não foram carregados via widget.servers ou anteriormente - _loadServers(); - } - } - - @override - void didUpdateWidget(covariant ServersList oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.servers != null && widget.servers != oldWidget.servers) { - setState(() { - _servers = widget.servers!; - _isLoading = false; - _dataLoaded = true; - }); - _saveSelectedServers(); - } - } - - Future _loadServers() async { - setState(() { - // 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': localizations.auto_select, - 'ping': localizations.fastest, - 'isActive': true, - }, - { - 'icon': 'assets/images/flags/Kazahstan.svg', - 'text': localizations.kazakhstan, - 'ping': '48', - 'isActive': false, - }, - { - 'icon': 'assets/images/flags/Turkey.svg', - 'text': localizations.turkey, - 'ping': '142', - 'isActive': false, - }, - { - 'icon': 'assets/images/flags/Poland.svg', - 'text': localizations.poland, - 'ping': '298', - 'isActive': false, - }, - ]; - - final prefs = await SharedPreferences.getInstance(); - final String? savedServers = prefs.getString(StorageKeys.selectedServers); - if (savedServers != null) { - final List savedServersList = jsonDecode(savedServers); - for (var savedServerItem in savedServersList) { - final index = serversList.indexWhere( - (server) => server['text'] == savedServerItem['text'], - ); - if (index != -1) { - serversList[index]['isActive'] = savedServerItem['isActive']; - } - } - } - - setState(() { - _servers = serversList; - _isLoading = false; - _dataLoaded = true; // Marcar que os dados foram carregados - }); - - if (widget.onServersLoaded != null) { - widget.onServersLoaded!(_servers); - } - } catch (e) { - setState(() { - _isLoading = false; - _dataLoaded = true; // Marcar como tentado carregar para evitar loop - }); - debugPrint('Error loading servers: $e'); - } - } - - Future _saveSelectedServers() async { - final prefs = await SharedPreferences.getInstance(); - final selectedServersData = - _servers - .map( - (server) => { - 'text': server['text'], - 'isActive': server['isActive'], - 'icon': server['icon'], - 'ping': server['ping'], - }, - ) - .toList(); - await prefs.setString( - StorageKeys.selectedServers, - jsonEncode(selectedServersData), - ); - } - - List> get servers => _servers; - - void _onItemTapped(int indexInFullList) { - setState(() { - for (int i = 0; i < _servers.length; i++) { - _servers[i]['isActive'] = (i == indexInFullList); - } - }); - - _saveSelectedServers(); - if (widget.onServersLoaded != null) { - widget.onServersLoaded!(_servers); - } - - if (widget.onItemTapNavigate != null) { - widget.onItemTapNavigate!(indexInFullList); - } - } + const ServersList({super.key, required this.servers, required this.onTap}); @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(); + 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, @@ -206,66 +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, - top: 10, - bottom: 5, - ), // Adicionado espaçamento - child: Text( - AppLocalizations.of(context)!.selected_server, - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - fontSize: 14, // Consistência de tamanho - ), - ), - ), - ...activeServers.map((server) { - 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, - top: 15, - bottom: 5, - ), // Adicionado espaçamento - child: Text( - AppLocalizations.of(context)!.all_servers, - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - fontSize: 14, // Consistência de tamanho - ), - ), - ), - ...inactiveServers.map((server) { - 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 884371d..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,13 +30,11 @@ class ServerListItem extends StatelessWidget { height: 52, margin: const EdgeInsets.symmetric(vertical: 8), decoration: BoxDecoration( - color: Theme.of(context).colorScheme.onSurface, // Usar cor do tema + color: isActive ? selectedColor : Colors.white, borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( - color: Theme.of( - context, - ).shadowColor.withAlpha((255 * 0.1).round()), // Usar cor do tema + color: Colors.grey.withValues(alpha: 0.2), blurRadius: 10, offset: const Offset(0, 1), ), @@ -57,20 +48,15 @@ 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: TextStyle( - fontSize: 16, - color: - Theme.of( - context, - ).colorScheme.primary, // Usar cor do tema - ), + style: const TextStyle(fontSize: 16, color: Colors.black), ), ), ], @@ -78,20 +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: TextStyle( - fontSize: 14, - color: - Theme.of( - context, - ).colorScheme.secondary, // Usar cor do tema - ), - ), - if (ping.isNotEmpty) - Image.asset(pingImage, width: 52, height: 52), + style: const TextStyle(fontSize: 14, color: Colors.grey), + + ], ), ), diff --git a/lib/pages/servers/servers_page.dart b/lib/pages/servers/servers_page.dart index 95c6ef4..f484def 100644 --- a/lib/pages/servers/servers_page.dart +++ b/lib/pages/servers/servers_page.dart @@ -1,42 +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'; -import 'package:vpn_client/l10n/app_localizations.dart'; - -class ServersPage extends StatefulWidget { - final Function(int) onNavBarTap; - const ServersPage({super.key, required this.onNavBarTap}); - @override - State createState() => ServersPageState(); -} +class ServersPage extends StatelessWidget { + const ServersPage({super.key}); -class ServersPageState extends State { - List> _servers = []; - - 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: LocalizationService.to('country_name'), - items: _servers, + placeholder: 'Название страны', + 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'); @@ -47,7 +34,7 @@ class ServersPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(LocalizationService.to('selected_server')), + title: const Text('Выбор сервера'), centerTitle: true, titleTextStyle: TextStyle( color: Theme.of(context).colorScheme.primary, @@ -67,7 +54,7 @@ class ServersPageState extends State { color: Theme.of(context).colorScheme.primary, ), onPressed: () => _showSearchDialog(context), - tooltip: LocalizationService.to('search'), + tooltip: 'Поиск', ), ), ), @@ -80,10 +67,6 @@ class ServersPageState extends State { }); }, servers: _servers, - onItemTapNavigate: (selectedIndex) { - // Passando a callback - widget.onNavBarTap(2); // Navega para a página principal (índice 2) - }, ), ); } diff --git a/lib/pages/settings/settings_page.dart b/lib/pages/settings/settings_page.dart new file mode 100644 index 0000000..788eb52 --- /dev/null +++ b/lib/pages/settings/settings_page.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class SettingsPage extends StatelessWidget { + const SettingsPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const Center(child: Text('Settings Page')); + } +} \ No newline at end of file diff --git a/lib/pages/speed/speed_page.dart b/lib/pages/speed/speed_page.dart new file mode 100644 index 0000000..b3be8a8 --- /dev/null +++ b/lib/pages/speed/speed_page.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class SpeedPage extends StatelessWidget { + const SpeedPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const Center(child: Text('Speed Page')); + } +} \ No newline at end of file diff --git a/lib/providers/vpn_provider.dart b/lib/providers/vpn_provider.dart new file mode 100644 index 0000000..22ba54a --- /dev/null +++ b/lib/providers/vpn_provider.dart @@ -0,0 +1,190 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'dart:convert'; +import 'dart:async'; +import 'dart:developer'; + +import 'package:flutter_v2ray/flutter_v2ray.dart'; +import 'package:vpnclient_engine_flutter/vpnclient_engine_flutter.dart'; + +// This is a change to test diff +class VPNProvider extends ChangeNotifier { + bool _isConnected = false; + bool get isConnected => _isConnected; + String _connectionStatus = 'Disconnected'; + String get connectionStatus => _connectionStatus; + String _connectionTime = "00:00:00"; + String get connectionTime => _connectionTime; + + Map? _selectedServer; + + Map? get selectedServer => _selectedServer; + List> _servers = []; + List> get servers => _servers; + Timer? _timer; + + final FlutterV2ray flutterV2ray = FlutterV2ray( + onStatusChanged: (status) { + // do something + }, + ); + + VPNProvider() { + _loadSelectedServer(); + } + + void connect() async{ + _connectionStatus = 'Connecting'; + notifyListeners(); + //_animationController.repeat(reverse: true); + + VPNclientEngine.ClearSubscriptions(); + VPNclientEngine.addSubscription(subscriptionURL: "https://pastebin.com/raw/ZCYiJ98W"); + await VPNclientEngine.updateSubscription(subscriptionIndex: 0); + + await flutterV2ray.initializeV2Ray(); + + + + // v2ray share link like vmess://, vless://, ... + String link = "vless://c61daf3e-83ff-424f-a4ff-5bfcb46f0b30@5.35.98.91:8443?encryption=none&flow=&security=reality&sni=yandex.ru&fp=chrome&pbk=rLCmXWNVoRBiknloDUsbNS5ONjiI70v-BWQpWq0HCQ0&sid=108108108108#%F0%9F%87%B7%F0%9F%87%BA+%F0%9F%99%8F+Russia+%231"; + V2RayURL parser = FlutterV2ray.parseFromURL(link); + + + // Get Server Delay + log('${flutterV2ray.getServerDelay(config: parser.getFullConfiguration())}ms'); + + // Permission is not required if you using proxy only + if (await flutterV2ray.requestPermission()){ + flutterV2ray.startV2Ray( + remark: parser.remark, + // The use of parser.getFullConfiguration() is not mandatory, + // and you can enter the desired V2Ray configuration in JSON format + config: parser.getFullConfiguration(), + blockedApps: null, + bypassSubnets: null, + proxyOnly: false, + ); + } + +// Disconnect +///flutterV2ray.stopV2Ray(); + +VPNclientEngine.pingServer(subscriptionIndex: 0, index: 1); + VPNclientEngine.onPingResult.listen((result) { + log("Ping result: ${result.latencyInMs} ms"); + }); + + + ///final result = await platform.invokeMethod('startVPN'); + + await VPNclientEngine.connect(subscriptionIndex: 0, serverIndex: 1); + _isConnected = true; + _connectionStatus = 'Connected'; + startTimer(); + notifyListeners(); + // _animationController.stop(); + } + + void disconnect() async{ + _connectionStatus = 'Disconnecting'; + notifyListeners(); + stopTimer(); + await VPNclientEngine.disconnect(); + _isConnected = false; + _connectionStatus = 'Disconnected'; + notifyListeners(); + // _animationController.reverse(); + //_animationController.stop(); + } + + Future _loadSelectedServer() async { + final prefs = await SharedPreferences.getInstance(); + final String? savedServer = prefs.getString('selectedServer'); + if (savedServer != null) { + _selectedServer = Map.from(jsonDecode(savedServer)); + } else { + _selectedServer = null; + } + notifyListeners(); + } + + Future selectServer(Map server) async { + final prefs = await SharedPreferences.getInstance(); + _selectedServer = server; + await prefs.setString('selectedServer', jsonEncode(server)); + notifyListeners(); + } + +void startTimer() { + int seconds = 1; + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + int hours = seconds ~/ 3600; + int minutes = (seconds % 3600) ~/ 60; + int remainingSeconds = seconds % 60; + _connectionTime = + '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${remainingSeconds.toString().padLeft(2, '0')}'; + notifyListeners(); + + seconds++; + }); + } + + void stopTimer() { + _timer?.cancel(); + _connectionTime = "00:00:00"; + notifyListeners(); + } + +Future _loadServers() async { + + try { + List> serversList = [ + { + 'icon': 'assets/images/flags/auto.svg', + 'text': 'Автовыбор', + 'ping': 'Самый быстрый', + 'isActive': true, + }, + { + 'icon': 'assets/images/flags/Kazahstan.svg', + 'text': 'Казахстан', + 'ping': '48', + 'isActive': false, + }, + { + 'icon': 'assets/images/flags/Turkey.svg', + 'text': 'Турция', + 'ping': '142', + 'isActive': false, + }, + { + 'icon': 'assets/images/flags/Poland.svg', + 'text': 'Польша', + 'ping': '298', + 'isActive': false, + }, + ]; + + + _servers = serversList; + notifyListeners(); + } catch (e) { + debugPrint('Error loading servers: $e'); + } + } + void _updateServers(Map server) { + for (int i = 0; i < _servers.length; i++) { + _servers[i]['isActive'] = false; + } + + final index = _servers.indexWhere( + (element) => element['text'] == server['text'], + ); + if (index != -1) { + _servers[index]['isActive'] = true; + } + notifyListeners(); + } + +} \ No newline at end of file diff --git a/lib/search_dialog.dart b/lib/search_dialog.dart index 89b77bb..deaef0a 100644 --- a/lib/search_dialog.dart +++ b/lib/search_dialog.dart @@ -1,21 +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 'package:vpn_client/l10n/app_localizations.dart'; -import 'package:vpn_client/core/constants/storage_keys.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 @@ -24,101 +24,29 @@ class SearchDialog extends StatefulWidget { class _SearchDialogState extends State { final TextEditingController _searchController = TextEditingController(); - List> _filteredItems = []; - List> _recentlySearchedItems = []; + late List> _filteredItems; late int _searchDialogType; - String? _allAppsString; - bool _dependenciesInitialized = false; - @override void initState() { super.initState(); _searchDialogType = widget.type; + _filteredItems = widget.items; _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 && _allAppsString != null) { - return item['text'] != _allAppsString; - } - return true; - }).toList(); - if (_searchController.text.isNotEmpty) { - _filterItems(); - } else { - setState(() {}); - } - } - - Future _loadRecentlySearched() async { - final prefs = await SharedPreferences.getInstance(); - final String key = - _searchDialogType == 1 - ? StorageKeys.recentlySearchedApps - : StorageKeys.recentlySearchedServers; - final String? recentlySearched = prefs.getString(key); - if (recentlySearched != null) { - setState(() { - _recentlySearchedItems = List>.from( - jsonDecode(recentlySearched), - ); - }); - } - } - - Future _addOrUpdateRecentlySearched(Map item) async { - final prefs = await SharedPreferences.getInstance(); - final String key = - _searchDialogType == 1 - ? StorageKeys.recentlySearchedApps - : StorageKeys.recentlySearchedServers; - 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(() { - _filteredItems = - widget.items.where((item) { - if (_searchDialogType == 1 && _allAppsString != null) { - return item['text'].toLowerCase().contains(query) && - item['text'] != _allAppsString; - } - return item['text'].toLowerCase().contains(query); - }).toList(); + _filteredItems = widget.items.where((item) { + if (_searchDialogType == 1) { + return item['text'].toLowerCase().contains(query) && item['text'] != 'Все приложения'; + } + return item['text'].toLowerCase().contains(query); + }).toList(); }); } - 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 void dispose() { _searchController.dispose(); super.dispose(); @@ -126,7 +54,9 @@ class _SearchDialogState extends State { @override Widget build(BuildContext context) { - final isQueryEmpty = _searchController.text.isEmpty; + final isQueryEmpty = _searchController.text.isEmpty; + + final showFilteredItems = !isQueryEmpty; return Dialog( insetPadding: EdgeInsets.zero, @@ -151,60 +81,61 @@ class _SearchDialogState extends State { child: Stack( alignment: Alignment.center, children: [ - Center( - child: Text( - AppLocalizations.of(context)!.search, - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.w600, - color: Theme.of(context).colorScheme.primary, + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Поиск', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.primary, + ), ), - ), + ], ), if (_searchDialogType == 1) - 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, + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(widget.items); + }, + child: const Text( + 'Готово', + textAlign: TextAlign.center, + style: TextStyle(color: Colors.blue, fontSize: 16), ), ), - ), + ], ), if (_searchDialogType == 2) - 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, + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text( + 'Отмена', + textAlign: TextAlign.center, + style: TextStyle(color: Colors.blue, fontSize: 16), ), ), - ), + ], ), ], ), ), Container( decoration: BoxDecoration( - color: Theme.of(context).cardColor, + color: Colors.white, borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( - color: Theme.of( - context, - ).shadowColor.withAlpha((255 * 0.1).round()), + color: Colors.grey.withValues(alpha: 0.2), blurRadius: 10, offset: const Offset(0, 1), ), @@ -218,182 +149,102 @@ class _SearchDialogState extends State { controller: _searchController, decoration: InputDecoration( hintText: widget.placeholder, - hintStyle: TextStyle(color: Theme.of(context).hintColor), + hintStyle: const TextStyle(color: Colors.grey), suffixIcon: Icon( Icons.search, color: Theme.of(context).colorScheme.primary, ), - fillColor: Theme.of(context).cardColor, + fillColor: Colors.white, filled: true, border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide( - color: Theme.of( - context, - ).dividerColor.withAlpha((255 * 0.5).round()), - width: 0.5, + color: Colors.grey.shade300, + width: 0, ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide( - color: Theme.of( - context, - ).dividerColor.withAlpha((255 * 0.5).round()), - width: 0.5, + color: Colors.grey.shade300, + width: 0, ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), - borderSide: BorderSide( - color: Theme.of(context).colorScheme.primary, - width: 1, - ), + borderSide: BorderSide(color: Colors.grey.shade300, width: 0), ), contentPadding: const EdgeInsets.all(14), ), ), ), - const SizedBox(height: 7), - 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], - ); - }, - ); - } else { - return ServerListItem( - icon: item['icon'], - text: item['text'], - ping: item['ping'], - isActive: item['isActive'] ?? false, - onTap: () { - _handleServerSelection(item); - }, - ); - } - }), - ), - ), - ], + // Отображаем отфильтрованный список + Expanded( + child: showFilteredItems + ? _filteredItems.isEmpty + ? Center( + child: Text( + 'Ничего не найдено', + style: TextStyle( + color: Theme.of(context).colorScheme.primary, ), ), + ) + : ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 14), + itemCount: _filteredItems.length, + itemBuilder: (context, index) { + final item = _filteredItems[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(() { + _filteredItems[index]['isActive'] = + !_filteredItems[index]['isActive']; + }); + final originalIndex = widget.items.indexWhere( + (i) => i['text'] == item['text'], + ); + if (originalIndex != -1) { + widget.items[originalIndex]['isActive'] = + _filteredItems[index]['isActive']; + } + }, + ); + } else { + return ServerListItem( + icon: item['icon'], + text: item['text'], + ping: item['ping'], + isActive: item['isActive'] ?? false, + selectedColor: widget.selectedColor, + onTap: () { + widget.onSelect(item); + Navigator.of(context).pop(); + }, + ); + } + }, + ) + : 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, + ), ), - Expanded( - child: - (!isQueryEmpty || - (isQueryEmpty && _recentlySearchedItems.isEmpty)) - ? _filteredItems.isEmpty - ? Center( - child: Text( - AppLocalizations.of(context)!.nothing_found, - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - ), - ), - ) - : ListView.builder( - padding: const EdgeInsets.symmetric( - horizontal: 14, - ), - itemCount: _filteredItems.length, - itemBuilder: (context, index) { - final item = _filteredItems[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(() { - _filteredItems[index]['isActive'] = - !_filteredItems[index]['isActive']; - if (_searchController.text.isNotEmpty) { - _addOrUpdateRecentlySearched( - _filteredItems[index], - ); - } - }); - final originalIndex = widget.items - .indexWhere( - (i) => i['text'] == item['text'], - ); - if (originalIndex != -1) { - widget.items[originalIndex]['isActive'] = - _filteredItems[index]['isActive']; - } - }, - ); - } else { - return ServerListItem( - icon: item['icon'], - text: item['text'], - ping: item['ping'], - isActive: item['isActive'] ?? false, - onTap: () { - _handleServerSelection(item); - }, - ); - } - }, - ) - : const SizedBox.shrink(), ), - SizedBox(height: MediaQuery.of(context).padding.bottom + 10), ], ), ),