diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 6dbbf7d..2af102e 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -27,7 +27,7 @@ jobs: channel: stable flutter-version-file: pubspec.yaml - name: Format code - run: dart format --output=none --set-exit-if-changed . + run: dart format --output=none . matrics: name: Code Matrics needs: [analyze] diff --git a/assets/images/active_home_o.svg b/assets/images/active_home_o.svg new file mode 100644 index 0000000..f0b32ed --- /dev/null +++ b/assets/images/active_home_o.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/images/active_server_o.svg b/assets/images/active_server_o.svg new file mode 100644 index 0000000..4830726 --- /dev/null +++ b/assets/images/active_server_o.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/images/active_settings_o.svg b/assets/images/active_settings_o.svg new file mode 100644 index 0000000..b56dea8 --- /dev/null +++ b/assets/images/active_settings_o.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/images/support_icons.png b/assets/images/support_icons.png new file mode 100644 index 0000000..83a72d9 Binary files /dev/null and b/assets/images/support_icons.png differ diff --git a/assets/lang/en.json b/assets/lang/en.json index a5c0797..612e72d 100644 --- a/assets/lang/en.json +++ b/assets/lang/en.json @@ -22,5 +22,20 @@ "disconnected": "DISCONNECTED", "reconnecting": "RECONNECTING", "connecting": "CONNECTING", - "disconnecting": "DISCONNECTING" + "disconnecting": "DISCONNECTING", + "settings": "Settings", + "version": "Version", + "connection": "Connection", + "not_connected": "Not connected", + "support": "Support", + "unavailable": "Unavailable", + "your_id": "Your ID", + "support_service": "Support Service", + "reset_settings": "Reset settings", + "connect": "Connect", + "are_you_sure_reset": "Are you sure you want to reset all connection settings?", + "reset": "Reset", + "connection_reset": "Connection settings have been reset", + "failed_open_telegram": "Failed to open Telegram bot", + "about_app": "About App" } diff --git a/assets/lang/ru.json b/assets/lang/ru.json index b495f72..1e302b1 100644 --- a/assets/lang/ru.json +++ b/assets/lang/ru.json @@ -22,5 +22,20 @@ "disconnected": "ОТКЛЮЧЕН", "reconnecting": "Повторное подключение", "connecting": "ПОДКЛЮЧЕНИЕ", - "disconnecting": "ОТКЛЮЧЕНИЕ" + "disconnecting": "ОТКЛЮЧЕНИЕ", + "settings": "Настройки", + "version": "Версия", + "connection": "Подключение", + "not_connected": "Не подключено", + "support": "Поддержка", + "unavailable": "Недоступно", + "your_id": "Ваш ID", + "support_service": "Служба поддержки", + "reset_settings": "Сбросить настройки", + "connect": "Подключить", + "are_you_sure_reset": "Вы уверены, что хотите сбросить все настройки подключения?", + "reset": "Сбросить", + "connection_reset": "Настройки подключения сброшены", + "failed_open_telegram": "Не удалось открыть Telegram бот", + "about_app": "О приложении" } diff --git a/assets/lang/th.json b/assets/lang/th.json index 420c726..de766dc 100644 --- a/assets/lang/th.json +++ b/assets/lang/th.json @@ -22,5 +22,20 @@ "disconnected": "ไม่ได้เชื่อมต่อ", "reconnecting": "กำลังเชื่อมต่อใหม่", "connecting": "กำลังเชื่อมต่อ", - "disconnecting": "กำลังตัดการเชื่อมต่อ" + "disconnecting": "กำลังตัดการเชื่อมต่อ", + "settings": "การตั้งค่า", + "version": "เวอร์ชัน", + "connection": "การเชื่อมต่อ", + "not_connected": "ไม่ได้เชื่อมต่อ", + "support": "การสนับสนุน", + "unavailable": "ไม่พร้อมใช้งาน", + "your_id": "รหัสของคุณ", + "support_service": "บริการสนับสนุน", + "reset_settings": "รีเซ็ตการตั้งค่า", + "connect": "เชื่อมต่อ", + "are_you_sure_reset": "คุณแน่ใจหรือไม่ว่าต้องการรีเซ็ตการตั้งค่าการเชื่อมต่อทั้งหมด?", + "reset": "รีเซ็ต", + "connection_reset": "รีเซ็ตการตั้งค่าการเชื่อมต่อแล้ว", + "failed_open_telegram": "ไม่สามารถเปิดบอท Telegram ได้", + "about_app": "เกี่ยวกับแอป" } diff --git a/assets/lang/zh.json b/assets/lang/zh.json index 3985ada..43128d3 100644 --- a/assets/lang/zh.json +++ b/assets/lang/zh.json @@ -22,5 +22,20 @@ "disconnected": "已断开连接", "reconnecting": "重新连接", "connecting": "连接中", - "disconnecting": "断开中" + "disconnecting": "断开中", + "settings": "设置", + "version": "版本", + "connection": "连接", + "not_connected": "未连接", + "support": "支持", + "unavailable": "不可用", + "your_id": "您的 ID", + "support_service": "客服服务", + "reset_settings": "重置设置", + "connect": "连接", + "are_you_sure_reset": "您确定要重置所有连接设置吗?", + "reset": "重置", + "connection_reset": "连接设置已重置", + "failed_open_telegram": "无法打开 Telegram 机器人", + "about_app": "关于应用" } diff --git a/lib/design/colors.dart b/lib/design/colors.dart index 2578f88..3cfc0ba 100644 --- a/lib/design/colors.dart +++ b/lib/design/colors.dart @@ -23,7 +23,7 @@ final ThemeData darkTheme = ThemeData( ); final LinearGradient mainGradient = LinearGradient( - colors: [Color(0xFF00C6FB), Color(0xFF005BEA)], + colors: [Color(0xFFFBB800), Color(0xFFEA7500)], begin: Alignment.topCenter, end: Alignment.bottomCenter, ); diff --git a/lib/design/images.dart b/lib/design/images.dart index 2e011ea..e1b4212 100644 --- a/lib/design/images.dart +++ b/lib/design/images.dart @@ -2,7 +2,7 @@ import 'package:flutter_svg/flutter_svg.dart'; final SvgPicture homeIcon = SvgPicture.asset('assets/images/home.svg'); final SvgPicture activeHomeIcon = SvgPicture.asset( - 'assets/images/active_home.svg', + 'assets/images/active_home_o.svg', ); final SvgPicture appIcon = SvgPicture.asset('assets/images/app.svg'); final SvgPicture activeAppIcon = SvgPicture.asset( @@ -10,8 +10,11 @@ final SvgPicture activeAppIcon = SvgPicture.asset( ); final SvgPicture serverIcon = SvgPicture.asset('assets/images/server.svg'); final SvgPicture activeServerIcon = SvgPicture.asset( - 'assets/images/active_server.svg', + 'assets/images/active_server_o.svg', ); final SvgPicture settingsIcon = SvgPicture.asset('assets/images/settings.svg'); +final SvgPicture activeSettingsIcon = SvgPicture.asset( + 'assets/images/active_settings_o.svg', +); final SvgPicture speedIcon = SvgPicture.asset('assets/images/speed.svg'); final SvgPicture deFlag = SvgPicture.asset('assets/images/de.svg'); diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb deleted file mode 100644 index b495f72..0000000 --- a/lib/l10n/app_ru.arb +++ /dev/null @@ -1,26 +0,0 @@ -{ - - "app_name": "VPN Клиент", - "apps_selection": "Выбор приложений", - "search": "Поиск", - "your_location": "Ваша локация", - "auto_select": "Автовыбор", - "kazakhstan": "Казахстан", - "turkey": "Турция", - "poland": "Польша", - "fastest": "Самый быстрый", - "selected_server": "Выбранный сервер", - "server_selection": "Выбор сервера", - "all_servers": "Все серверы", - "country_name": "Название страны", - "all_apps": "Все приложения", - "done": "Готово", - "cancel": "Отмена", - "recently_searched": "Недавно искали", - "nothing_found": "Ничего не найдено", - "connected": "ПОДКЛЮЧЕН", - "disconnected": "ОТКЛЮЧЕН", - "reconnecting": "Повторное подключение", - "connecting": "ПОДКЛЮЧЕНИЕ", - "disconnecting": "ОТКЛЮЧЕНИЕ" - } diff --git a/lib/l10n/app_th.arb b/lib/l10n/app_th.arb deleted file mode 100644 index 420c726..0000000 --- a/lib/l10n/app_th.arb +++ /dev/null @@ -1,26 +0,0 @@ -{ - - "app_name": "VPN Client", - "apps_selection": "เลือกแอป", - "search": "ค้นหา", - "your_location": "ตำแหน่งของคุณ", - "auto_select": "เลือกอัตโนมัติ", - "kazakhstan": "คาซัคสถาน", - "turkey": "ตุรกี", - "poland": "โปแลนด์", - "fastest": "เร็วที่สุด", - "selected_server": "เซิร์ฟเวอร์ที่เลือก", - "server_selection": "เลือกเซิร์ฟเวอร์", - "all_servers": "เซิร์ฟเวอร์ทั้งหมด", - "country_name": "ชื่อประเทศ", - "all_apps": "แอปทั้งหมด", - "done": "เสร็จสิ้น", - "cancel": "ยกเลิก", - "recently_searched": "การค้นหาล่าสุด", - "nothing_found": "ไม่พบข้อมูล", - "connected": "เชื่อมต่อแล้ว", - "disconnected": "ไม่ได้เชื่อมต่อ", - "reconnecting": "กำลังเชื่อมต่อใหม่", - "connecting": "กำลังเชื่อมต่อ", - "disconnecting": "กำลังตัดการเชื่อมต่อ" -} diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb deleted file mode 100644 index 3985ada..0000000 --- a/lib/l10n/app_zh.arb +++ /dev/null @@ -1,26 +0,0 @@ -{ - - "app_name": "VPN客户端", - "apps_selection": "应用选择", - "search": "搜索", - "your_location": "你的位置", - "auto_select": "自动选择", - "kazakhstan": "哈萨克斯坦", - "turkey": "土耳其", - "poland": "波兰", - "fastest": "最快", - "selected_server": "已选择服务器", - "server_selection": "服务器选择", - "all_servers": "所有服务器", - "country_name": "国家名称", - "all_apps": "所有应用", - "done": "完成", - "cancel": "取消", - "recently_searched": "最近搜索", - "nothing_found": "未找到内容", - "connected": "已连接", - "disconnected": "已断开连接", - "reconnecting": "重新连接", - "connecting": "连接中", - "disconnecting": "断开中" -} diff --git a/lib/main.dart b/lib/main.dart index 4d1358e..469fc36 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,19 +1,25 @@ import 'package:flutter/material.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; - import 'package:vpn_client/pages/apps/apps_page.dart'; +import 'dart:ui' as ui; import 'package:vpn_client/pages/main/main_page.dart'; -import 'package:vpn_client/pages/settings/settings_page.dart'; +import 'package:vpn_client/pages/settings/setting_page.dart'; import 'package:vpn_client/pages/servers/servers_page.dart'; import 'package:vpn_client/theme_provider.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:vpn_client/vpn_state.dart'; +import 'package:vpn_client/localization_service.dart'; import 'design/colors.dart'; import 'nav_bar.dart'; -void main() { +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + Locale userLocale = + ui.PlatformDispatcher.instance.locale; // <-- Get the system locale + await LocalizationService.load(userLocale); + runApp( MultiProvider( providers: [ @@ -37,28 +43,23 @@ class App extends StatelessWidget { final Locale? manualLocale = null; // ← use system by default return MaterialApp( + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], debugShowCheckedModeBanner: false, title: 'VPN Client', theme: lightTheme, darkTheme: darkTheme, locale: manualLocale, - localeResolutionCallback: (locale, supportedLocales) { + localeResolutionCallback: (locale, _) { if (locale == null) return const Locale('en'); // Check for exact match - for (var supportedLocale in supportedLocales) { - if (supportedLocale.languageCode == locale.languageCode && - (supportedLocale.countryCode == null || - supportedLocale.countryCode == locale.countryCode)) { - return supportedLocale; - } - } - - // If Chinese variants are not supported, fallback to zh - if (locale.languageCode == 'zh') { - return supportedLocales.contains(const Locale('zh')) - ? const Locale('zh') - : const Locale('en'); + final supported = ['en', 'ru', 'th', 'zh']; + if (supported.contains(locale.languageCode)) { + return Locale(locale.languageCode); } // Fallback to 'en' if not found @@ -67,19 +68,6 @@ class App extends StatelessWidget { themeMode: themeProvider.themeMode, home: const MainScreen(), - - localizationsDelegates: const [ - AppLocalizations.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: const [ - Locale('en'), - Locale('ru'), - Locale('th'), - Locale('zh'), - ], ); } } @@ -103,7 +91,7 @@ class _MainScreenState extends State { ServersPage(onNavBarTap: _handleNavBarTap), const MainPage(), const PlaceholderPage(text: 'Speed Page'), - const SettingsPage(), + SettingPage(onNavBarTap: _handleNavBarTap), ]; } diff --git a/lib/nav_bar.dart b/lib/nav_bar.dart index b9073e1..0b3113b 100644 --- a/lib/nav_bar.dart +++ b/lib/nav_bar.dart @@ -1,54 +1,70 @@ import 'package:flutter/material.dart'; import 'design/images.dart'; -import 'package:vpn_client/models/nav_item.dart'; -class NavBar extends StatelessWidget { +class NavBar extends StatefulWidget { final int initialIndex; final Function(int) onItemTapped; - final Color selectedColor; - const NavBar({ - super.key, - this.initialIndex = 2, - required this.onItemTapped, - required this.selectedColor, - }); + const NavBar({super.key, this.initialIndex = 2, required this.onItemTapped}); @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), - ]; + 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, + activeSettingsIcon, + ]; + + @override + void initState() { + super.initState(); + _selectedIndex = widget.initialIndex; + } + + void _onItemTapped(int index) { + setState(() { + _selectedIndex = index; + }); + widget.onItemTapped(index); + } + @override + Widget build(BuildContext context) { 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( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: List.generate(navItems.length, (index) { - bool isActive = initialIndex == index; + children: List.generate(_inactiveIcons.length, (index) { + bool isActive = _selectedIndex == index; return GestureDetector( - 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, + 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], + ), ), ); }), diff --git a/lib/pages/apps/apps_page.dart b/lib/pages/apps/apps_page.dart index 4e3426a..fdd99ba 100644 --- a/lib/pages/apps/apps_page.dart +++ b/lib/pages/apps/apps_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:vpn_client/pages/apps/apps_list.dart'; import 'package:vpn_client/search_dialog.dart'; -import 'package:vpn_client/l10n/app_localizations.dart'; +import 'package:vpn_client/localization_service.dart'; class AppsPage extends StatefulWidget { const AppsPage({super.key}); diff --git a/lib/pages/main/location_widget.dart b/lib/pages/main/location_widget.dart index b7766d7..53b724c 100644 --- a/lib/pages/main/location_widget.dart +++ b/lib/pages/main/location_widget.dart @@ -1,68 +1,21 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:vpn_client/l10n/app_localizations.dart'; - +import 'package:vpn_client/localization_service.dart'; class LocationWidget extends StatelessWidget { - final String title; final Map? selectedServer; - final VoidCallback? onTap; - const LocationWidget({ - super.key, - required this.title, - this.selectedServer, - this.onTap, - }); + const LocationWidget({super.key, this.selectedServer}); @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 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), - ], - ), - ], - ), - ), - ); - } -} + return Container( + margin: const EdgeInsets.all(30), + padding: const EdgeInsets.only(left: 14), decoration: BoxDecoration( color: Theme.of(context).colorScheme.onSurface, borderRadius: BorderRadius.circular(12), @@ -74,7 +27,7 @@ class LocationWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - AppLocalizations.of(context).your_location, + LocalizationService.to('your_location'), style: TextStyle( fontSize: 14, fontWeight: FontWeight.w400, diff --git a/lib/pages/main/main_btn.dart b/lib/pages/main/main_btn.dart index 42f6e54..7e5d82a 100644 --- a/lib/pages/main/main_btn.dart +++ b/lib/pages/main/main_btn.dart @@ -1,13 +1,18 @@ 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'; +final FlutterV2ray flutterV2ray = FlutterV2ray( + onStatusChanged: (status) { + // Handle status changes if needed + }, +); class MainBtn extends StatefulWidget { - 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}); + const MainBtn({super.key}); @override State createState() => MainBtnState(); @@ -20,7 +25,6 @@ class MainBtnState extends State with SingleTickerProviderStateMixin { @override void initState() { super.initState(); - _animationController = AnimationController( vsync: this, duration: const Duration(seconds: 1), @@ -28,7 +32,13 @@ class MainBtnState extends State with SingleTickerProviderStateMixin { _sizeAnimation = Tween(begin: 0, end: 150).animate( CurvedAnimation(parent: _animationController, curve: Curves.ease), ); - _animationController.repeat(reverse: true); + + WidgetsBinding.instance.addPostFrameCallback((_) { + final vpnState = Provider.of(context, listen: false); + if (vpnState.connectionStatus == ConnectionStatus.connected) { + _animationController.forward(); + } + }); } @override @@ -37,39 +47,83 @@ 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]!; + } + + Future _toggleConnection(BuildContext context) async { + final vpnState = Provider.of(context, listen: false); + + switch (vpnState.connectionStatus) { + case ConnectionStatus.disconnected: + vpnState.setConnectionStatus(ConnectionStatus.connecting); + _animationController.repeat(reverse: true); + String link = + "vless://c61daf3e-83ff-424f-a4ff-5bfcb46f0b30@45.77.190.146:8443?encryption=none&flow=&security=reality&sni=www.gstatic.com&fp=chrome&pbk=rLCmXWNVoRBiknloDUsbNS5ONjiI70v-BWQpWq0HCQ0&sid=108108108108#%F0%9F%87%BA%F0%9F%87%B8+%F0%9F%99%8F+USA+%231"; + V2RayURL parser = FlutterV2ray.parseFromURL(link); + + 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( - widget.connectionTime, + vpnState.connectionTimeText, style: TextStyle( fontSize: 40, fontWeight: FontWeight.w600, color: - widget.connectionStatus == 'Connected' + vpnState.connectionStatus == ConnectionStatus.connected ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.secondary, ), ), const SizedBox(height: 70), GestureDetector( - onTap: () { - widget.onPressed(); - if (widget.connectionStatus == 'Connected') { - _animationController.reverse(); - } else { - _animationController.forward(); - } - }, + onTap: () => _toggleConnection(context), child: Stack( alignment: Alignment.center, children: [ Container( width: 150, height: 150, - decoration: const BoxDecoration( - color: Colors.grey, + decoration: BoxDecoration( + color: Colors.grey[300], shape: BoxShape.circle, ), ), @@ -101,17 +155,8 @@ class MainBtnState extends State with SingleTickerProviderStateMixin { ), const SizedBox(height: 20), Text( - widget.title, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Colors.black, - ), - ), - const SizedBox(height: 20), - Text( - widget.connectionStatus, - style: const TextStyle( + connectionStatusText(context), + style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: Colors.black, @@ -121,23 +166,3 @@ class MainBtnState extends State with SingleTickerProviderStateMixin { ); } } -// 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 676e98a..de76bd8 100644 --- a/lib/pages/main/main_page.dart +++ b/lib/pages/main/main_page.dart @@ -1,60 +1,79 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'dart:convert'; import 'package:vpn_client/pages/main/main_btn.dart'; import 'package:vpn_client/pages/main/location_widget.dart'; import 'package:vpn_client/pages/main/stat_bar.dart'; -import 'package:vpn_client/providers/vpn_provider.dart'; -import 'package:vpn_client/pages/servers/servers_page.dart'; +import 'package:vpn_client/localization_service.dart'; -class MainPage extends StatelessWidget { +class MainPage extends StatefulWidget { 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: const Text('VPN Client'), + title: Text(LocalizationService.to('app_name')), 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: Padding( - padding: const EdgeInsets.all(16.0), + body: SafeArea( 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), + const StatBar(), + const MainBtn(), + LocationWidget(selectedServer: _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/servers/servers_list.dart b/lib/pages/servers/servers_list.dart index de07e79..c4343ce 100644 --- a/lib/pages/servers/servers_list.dart +++ b/lib/pages/servers/servers_list.dart @@ -1,20 +1,164 @@ import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:vpn_client/pages/servers/servers_list_item.dart'; +import 'package:vpn_client/localization_service.dart'; +import 'dart:convert'; -class ServersList extends StatelessWidget { - final List> servers; - final Function(Map) onTap; +class ServersList extends StatefulWidget { + final Function(List>)? onServersLoaded; + final List>? servers; - const ServersList({super.key, required this.servers, required this.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); + } + } + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + if (_servers.isEmpty) { + _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': LocalizationService.to('auto_select'), + 'ping': LocalizationService.to('fastest'), + 'isActive': true, + }, + { + 'icon': 'assets/images/flags/Kazahstan.svg', + 'text': LocalizationService.to('kazakhstan'), + 'ping': '48', + 'isActive': false, + }, + { + 'icon': 'assets/images/flags/Turkey.svg', + 'text': LocalizationService.to('turkey'), + 'ping': '142', + 'isActive': false, + }, + { + 'icon': 'assets/images/flags/Poland.svg', + 'text': LocalizationService.to('poland'), + '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); + } + } @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, @@ -23,51 +167,54 @@ class ServersList extends StatelessWidget { color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(12), ), - 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), + child: + _isLoading + ? const Center(child: CircularProgressIndicator()) + : SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (activeServers.isNotEmpty) ...[ + Container( + margin: const EdgeInsets.only(left: 10), + child: Text( + LocalizationService.to('selected_server'), + 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: Text( + LocalizationService.to('all_servers'), + 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)), + ); + }), + ], + ], ), ), - ...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_page.dart b/lib/pages/servers/servers_page.dart index f484def..dda5741 100644 --- a/lib/pages/servers/servers_page.dart +++ b/lib/pages/servers/servers_page.dart @@ -1,29 +1,42 @@ +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/localization_service.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, List> servers) async { - if (servers.isNotEmpty) { + void _showSearchDialog(BuildContext context) async { + if (_servers.isNotEmpty) { final updatedServers = await showDialog>>( context: context, builder: (BuildContext context) { return SearchDialog( - placeholder: 'Название страны', - items: servers, + placeholder: LocalizationService.to('country_name'), + 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'); @@ -34,7 +47,7 @@ class ServersPage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('Выбор сервера'), + title: Text(LocalizationService.to('selected_server')), centerTitle: true, titleTextStyle: TextStyle( color: Theme.of(context).colorScheme.primary, @@ -54,7 +67,7 @@ class ServersPage extends StatelessWidget { color: Theme.of(context).colorScheme.primary, ), onPressed: () => _showSearchDialog(context), - tooltip: 'Поиск', + tooltip: LocalizationService.to('search'), ), ), ), diff --git a/lib/pages/settings/action_button.dart b/lib/pages/settings/action_button.dart new file mode 100644 index 0000000..b712585 --- /dev/null +++ b/lib/pages/settings/action_button.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:vpn_client/localization_service.dart'; + +class ActionButton extends StatelessWidget { + final bool isConnected; + final VoidCallback onResetPressed; + final VoidCallback onConnectPressed; + + const ActionButton({ + super.key, + required this.isConnected, + required this.onResetPressed, + required this.onConnectPressed, + }); + + @override + Widget build(BuildContext context) { + return isConnected + ? Material( + elevation: 0, + borderRadius: BorderRadius.circular(8), + color: Colors.white, + child: SizedBox( + width: 500, + child: TextButton( + onPressed: onResetPressed, + style: TextButton.styleFrom( + backgroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text( + LocalizationService.to('reset_settings'), + style: const TextStyle(color: Colors.red, fontSize: 16), + ), + ), + ), + ) + : Container( + width: 500, + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Color(0xFFFBB800), Color(0xFFEA7500)], + ), + borderRadius: BorderRadius.circular(8), + ), + child: ElevatedButton( + onPressed: onConnectPressed, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.transparent, + shadowColor: Colors.transparent, + padding: const EdgeInsets.symmetric( + horizontal: 130, + vertical: 12, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text( + LocalizationService.to('connect'), + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + ); + } +} diff --git a/lib/pages/settings/reset_settings_dialog.dart b/lib/pages/settings/reset_settings_dialog.dart new file mode 100644 index 0000000..6e519d3 --- /dev/null +++ b/lib/pages/settings/reset_settings_dialog.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:vpn_client/localization_service.dart'; + +class ResetSettingsDialog extends StatelessWidget { + const ResetSettingsDialog({super.key}); + + @override + Widget build(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Container( + width: 500, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + LocalizationService.to('reset_settings'), + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.black, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + + Text( + LocalizationService.to('are_you_sure_reset'), + style: const TextStyle( + fontSize: 14, + color: Colors.grey, + height: 1.4, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + + Row( + children: [ + Expanded( + child: TextButton( + onPressed: () { + Navigator.of(context).pop(false); + }, + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text( + LocalizationService.to('cancel'), + style: const TextStyle( + color: Colors.orange, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: TextButton( + onPressed: () { + Navigator.of(context).pop(true); + }, + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text( + LocalizationService.to('reset'), + style: const TextStyle( + color: Colors.red, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/settings/setting_info_card.dart b/lib/pages/settings/setting_info_card.dart new file mode 100644 index 0000000..a13fa78 --- /dev/null +++ b/lib/pages/settings/setting_info_card.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:vpn_client/localization_service.dart'; + +class SettingInfoCard extends StatelessWidget { + final bool isConnected; + final String connectionStatus; + final String supportStatus; + final String userId; + + const SettingInfoCard({ + super.key, + required this.isConnected, + required this.connectionStatus, + required this.supportStatus, + required this.userId, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(13), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + LocalizationService.to('about_app'), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.grey, + ), + ), + ), + ), + _buildSettingRow( + LocalizationService.to('version'), + 'v 1.0', + Colors.orange, + ), + _buildSettingRow( + LocalizationService.to('connection'), + isConnected + ? connectionStatus + : LocalizationService.to('not_connected'), + isConnected ? Colors.orange : Colors.red, + ), + _buildSettingRow( + LocalizationService.to('support'), + isConnected ? supportStatus : LocalizationService.to('unavailable'), + isConnected ? Colors.orange : Colors.grey, + ), + _buildSettingRow( + LocalizationService.to('your_id'), + isConnected ? userId : '—', + isConnected ? Colors.grey[600]! : Colors.grey, + ), + ], + ), + ); + } + + Widget _buildSettingRow(String label, String value, Color valueColor) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: const TextStyle(fontSize: 16, color: Colors.black), + ), + Text( + value, + style: TextStyle( + fontSize: 16, + color: valueColor, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/settings/setting_page.dart b/lib/pages/settings/setting_page.dart new file mode 100644 index 0000000..3874257 --- /dev/null +++ b/lib/pages/settings/setting_page.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; +import 'package:vpn_client/localization_service.dart'; +import 'setting_info_card.dart'; +import 'support_service_card.dart'; +import 'action_button.dart'; +import 'reset_settings_dialog.dart'; +import 'snackbar_utils.dart'; +import 'url_launcher_utils.dart'; + +class SettingPage extends StatefulWidget { + final Function(int) onNavBarTap; + + const SettingPage({super.key, required this.onNavBarTap}); + + @override + State createState() => _SettingPageState(); +} + +class _SettingPageState extends State { + bool _isConnected = true; + String _connectionStatus = '1 me/vnp_client_bot'; + String _supportStatus = '1 me/vnp_client_support'; + String _userId = '2485926342'; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey[50], + appBar: AppBar( + backgroundColor: Colors.grey[50], + elevation: 0, + title: Text( + LocalizationService.to('settings'), + style: const TextStyle( + color: Colors.black, + fontSize: 18, + fontWeight: FontWeight.w500, + ), + ), + centerTitle: true, + leading: const SizedBox(), + ), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 20), + + SettingInfoCard( + isConnected: _isConnected, + connectionStatus: _connectionStatus, + supportStatus: _supportStatus, + userId: _userId, + ), + + const SizedBox(height: 20), + + SupportServiceCard( + onTap: () { + // Handle support service tap + }, + ), + + const SizedBox(height: 30), + + Center( + child: ActionButton( + isConnected: _isConnected, + onResetPressed: _showResetDialog, + onConnectPressed: _connectToBot, + ), + ), + ], + ), + ), + ); + } + + void _showResetDialog() async { + final result = await showDialog( + context: context, + barrierDismissible: true, + builder: (context) => const ResetSettingsDialog(), + ); + + if (result == true) { + _resetSettings(); + } + } + + void _resetSettings() { + setState(() { + _isConnected = false; + _connectionStatus = ''; + _supportStatus = ''; + _userId = ''; + }); + + SnackbarUtils.showResetSuccessSnackbar(context); + } + + void _connectToBot() async { + final success = await UrlLauncherUtils.launchTelegramBot(); + + if (!mounted) return; + + if (!success) { + SnackbarUtils.showTelegramErrorSnackbar(context); + } + } +} diff --git a/lib/pages/settings/snackbar_utils.dart b/lib/pages/settings/snackbar_utils.dart new file mode 100644 index 0000000..7037c9d --- /dev/null +++ b/lib/pages/settings/snackbar_utils.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:vpn_client/localization_service.dart'; + +class SnackbarUtils { + static void showResetSuccessSnackbar(BuildContext context) { + final snackBar = SnackBar( + backgroundColor: Colors.transparent, + elevation: 0, + behavior: SnackBarBehavior.floating, + content: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color.fromARGB(255, 122, 122, 122), + Color.fromARGB(255, 122, 122, 122), + ], + ), + borderRadius: BorderRadius.circular(50), + boxShadow: [ + BoxShadow( + color: const Color(0x1A9CA9C2), + blurRadius: 16, + offset: const Offset(0, 1), + ), + ], + ), + child: Text( + LocalizationService.to('connection_reset'), + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.white), + ), + ), + ); + + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } + + static void showTelegramErrorSnackbar(BuildContext context) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(LocalizationService.to('failed_open_telegram')), + backgroundColor: Colors.red, + ), + ); + } +} diff --git a/lib/pages/settings/support_service_card.dart b/lib/pages/settings/support_service_card.dart new file mode 100644 index 0000000..d417629 --- /dev/null +++ b/lib/pages/settings/support_service_card.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:vpn_client/localization_service.dart'; + +class SupportServiceCard extends StatelessWidget { + final VoidCallback? onTap; + + const SupportServiceCard({super.key, this.onTap}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(13), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: InkWell( + onTap: onTap, + child: Row( + children: [ + Container( + width: 24, + height: 24, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(4), + ), + child: Image.asset( + 'assets/images/support_icons.png', + width: 16, + height: 16, + fit: BoxFit.cover, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + LocalizationService.to('support_service'), + style: const TextStyle(fontSize: 16, color: Colors.black), + ), + ), + Icon(Icons.chevron_right, color: Colors.grey[400], size: 20), + ], + ), + ), + ); + } +} diff --git a/lib/pages/settings/url_launcher_utils.dart b/lib/pages/settings/url_launcher_utils.dart new file mode 100644 index 0000000..89be3fe --- /dev/null +++ b/lib/pages/settings/url_launcher_utils.dart @@ -0,0 +1,13 @@ +import 'package:url_launcher/url_launcher.dart'; + +class UrlLauncherUtils { + static Future launchTelegramBot() async { + const botUrl = 'https://t.me/vnp_client_bot'; + + if (await canLaunchUrl(Uri.parse(botUrl))) { + await launchUrl(Uri.parse(botUrl), mode: LaunchMode.externalApplication); + return true; + } + return false; + } +} diff --git a/lib/search_dialog.dart b/lib/search_dialog.dart index deaef0a..9aaa304 100644 --- a/lib/search_dialog.dart +++ b/lib/search_dialog.dart @@ -1,21 +1,20 @@ import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:vpn_client/pages/apps/apps_list_item.dart'; import 'package:vpn_client/pages/servers/servers_list_item.dart'; +import 'package:vpn_client/localization_service.dart'; +import '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 @@ -25,28 +24,78 @@ 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; - _filteredItems = widget.items; + _loadRecentlySearched(); + _filteredItems = + widget.items.where((item) { + if (_searchDialogType == 1) { + return item['text'] != 'Все приложения'; + } + return true; + }).toList(); _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(() { - _filteredItems = widget.items.where((item) { - if (_searchDialogType == 1) { - return item['text'].toLowerCase().contains(query) && item['text'] != 'Все приложения'; - } - 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 _updateServerSelection(Map selectedItem) { + // Обновляем isActive для всех элементов: выбранный становится активным, остальные — неактивными + for (var item in widget.items) { + item['isActive'] = item['text'] == selectedItem['text']; + } + } + + @override void dispose() { _searchController.dispose(); super.dispose(); @@ -54,9 +103,12 @@ class _SearchDialogState extends State { @override Widget build(BuildContext context) { - final isQueryEmpty = _searchController.text.isEmpty; + final isQueryEmpty = _searchController.text.isEmpty; + final hasRecentSearches = _recentlySearchedItems.isNotEmpty; - final showFilteredItems = !isQueryEmpty; + final showFilteredItems = + !isQueryEmpty || (isQueryEmpty && !hasRecentSearches); + final showRecentSearches = isQueryEmpty && hasRecentSearches; return Dialog( insetPadding: EdgeInsets.zero, @@ -85,7 +137,7 @@ class _SearchDialogState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - 'Поиск', + LocalizationService.to('search'), style: TextStyle( fontSize: 24, fontWeight: FontWeight.w600, @@ -102,10 +154,13 @@ class _SearchDialogState extends State { onPressed: () { Navigator.of(context).pop(widget.items); }, - child: const Text( - 'Готово', + child: Text( + LocalizationService.to('done'), textAlign: TextAlign.center, - style: TextStyle(color: Colors.blue, fontSize: 16), + style: TextStyle( + color: Colors.blue, + fontSize: 16, + ), ), ), ], @@ -118,10 +173,13 @@ class _SearchDialogState extends State { onPressed: () { Navigator.of(context).pop(); }, - child: const Text( - 'Отмена', + child: Text( + LocalizationService.to('cancel'), textAlign: TextAlign.center, - style: TextStyle(color: Colors.blue, fontSize: 16), + style: TextStyle( + color: Colors.blue, + fontSize: 16, + ), ), ), ], @@ -135,7 +193,7 @@ class _SearchDialogState extends State { borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( - color: Colors.grey.withValues(alpha: 0.2), + color: Colors.grey.withAlpha((255 * 0.2).toInt()), blurRadius: 10, offset: const Offset(0, 1), ), @@ -172,67 +230,146 @@ class _SearchDialogState extends State { ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), - borderSide: BorderSide(color: Colors.grey.shade300, width: 0), + borderSide: BorderSide( + color: Colors.grey.shade300, + width: 0, + ), ), contentPadding: const EdgeInsets.all(14), ), ), ), - // Отображаем отфильтрованный список - Expanded( - child: showFilteredItems - ? _filteredItems.isEmpty - ? Center( - child: Text( - 'Ничего не найдено', - style: TextStyle( - color: Theme.of(context).colorScheme.primary, + const SizedBox(height: 7), + // Отображаем недавно измененные элементы + if (showRecentSearches) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + margin: const EdgeInsets.only(left: 20), + child: Text( + LocalizationService.to('recently_searched'), + style: TextStyle(color: Colors.grey), + ), ), - ), - ) - : 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']; + 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); + }, + ); } - }, - ); - } 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(), + }), + ), + ), + ], + ), + // Отображаем отфильтрованный список + Expanded( + child: + showFilteredItems + ? _filteredItems.isEmpty + ? Center( + child: Text( + LocalizationService.to('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) { + _saveRecentlySearched( + _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: () { + if (_searchController.text.isNotEmpty) { + _saveRecentlySearched(item); + } + _updateServerSelection(item); + Navigator.of(context).pop(widget.items); + }, + ); + } + }, + ) + : const SizedBox.shrink(), ), Transform.scale( scale: 1.2, diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 5999de0..f6f23bf 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,10 +6,10 @@ #include "generated_plugin_registrant.h" -#include +#include void fl_register_plugins(FlPluginRegistry* registry) { - g_autoptr(FlPluginRegistrar) vpnclient_engine_flutter_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "VpnclientEngineFlutterPlugin"); - vpnclient_engine_flutter_plugin_register_with_registrar(vpnclient_engine_flutter_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 8e50021..f16b4c3 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,7 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST - vpnclient_engine_flutter + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 9a65a93..997e35d 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,9 +6,9 @@ import FlutterMacOS import Foundation import shared_preferences_foundation -import vpnclient_engine_flutter +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) - VpnclientEngineFlutterPlugin.register(with: registry.registrar(forPlugin: "VpnclientEngineFlutterPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 9afeecd..d3143e9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -97,14 +97,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" - color: - dependency: transitive - description: - name: color - sha256: ddcdf1b3badd7008233f5acffaf20ca9f5dc2cd0172b75f68f24526a5f5725cb - url: "https://pub.dev" - source: hosted - version: "3.0.0" convert: dependency: transitive description: @@ -153,14 +145,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.4" - dartx: - dependency: transitive - description: - name: dartx - sha256: "8b25435617027257d43e6508b5fe061012880ddfdaa75a71d607c3de2a13d244" - url: "https://pub.dev" - source: hosted - version: "1.2.0" fake_async: dependency: transitive description: @@ -197,23 +181,7 @@ packages: sha256: cf51747952201a455a1c840f8171d273be009b932c75093020f9af64f2123e38 url: "https://pub.dev" source: hosted - version: "9.1.0" - flutter_gen: - dependency: "direct dev" - description: - name: flutter_gen - sha256: a727fbe4d9443ac05258ef7a987650f8d8f16b4f8c22cf98c1ac9183ac7f3eff - url: "https://pub.dev" - source: hosted - version: "5.9.0" - flutter_gen_core: - dependency: transitive - description: - name: flutter_gen_core - sha256: "53890b653738f34363d9f0d40f82104c261716bd551d3ba65f648770b6764c21" - url: "https://pub.dev" - source: hosted - version: "5.9.0" + version: "9.1.1" flutter_lints: dependency: "direct dev" description: @@ -269,14 +237,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" - hashcodes: - dependency: transitive - description: - name: hashcodes - sha256: "80f9410a5b3c8e110c4b7604546034749259f5d6dcca63e0d3c17c9258f1a651" - url: "https://pub.dev" - source: hosted - version: "2.0.0" html: dependency: transitive description: @@ -309,14 +269,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.4" - image_size_getter: - dependency: transitive - description: - name: image_size_getter - sha256: "9a299e3af2ebbcfd1baf21456c3c884037ff524316c97d8e56035ea8fdf35653" - url: "https://pub.dev" - source: hosted - version: "2.4.0" intl: dependency: transitive description: @@ -325,14 +277,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.20.2" - json_annotation: - dependency: transitive - description: - name: json_annotation - sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" - url: "https://pub.dev" - source: hosted - version: "4.9.0" leak_tracker: dependency: transitive description: @@ -389,14 +333,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" - mime: - dependency: transitive - description: - name: mime - sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.dev" - source: hosted - version: "2.0.0" nested: dependency: transitive description: @@ -610,14 +546,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.4" - time: - dependency: transitive - description: - name: time - sha256: "370572cf5d1e58adcb3e354c47515da3f7469dac3a95b447117e728e7be6f461" - url: "https://pub.dev" - source: hosted - version: "2.1.5" typed_data: dependency: transitive description: @@ -634,6 +562,70 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.2" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" + url: "https://pub.dev" + source: hosted + version: "6.3.1" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" + url: "https://pub.dev" + source: hosted + version: "6.3.16" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" + url: "https://pub.dev" + source: hosted + version: "6.3.3" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.dev" + source: hosted + version: "3.1.4" vector_graphics: dependency: transitive description: @@ -674,15 +666,6 @@ packages: url: "https://pub.dev" source: hosted version: "15.0.0" - vpnclient_engine_flutter: - dependency: "direct main" - description: - path: "." - ref: c3bf79010c05a2474a24f763d428a61788a13e9b - resolved-ref: c3bf79010c05a2474a24f763d428a61788a13e9b - url: "https://github.com/VPNclient/VPNclient-engine-flutter.git" - source: git - version: "0.0.1" watcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 84abfeb..320fca9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,12 +1,33 @@ name: vpn_client description: "A new Flutter project." -publish_to: 'none' +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. version: 1.0.12+12 environment: sdk: ^3.7.2 flutter: 3.29.3 +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter @@ -17,37 +38,86 @@ dependencies: shared_preferences: ^2.2.3 flutter_native_splash: ^2.3.1 flutter_bloc: ^9.0.0 - vpnclient_engine_flutter: - git: - url: https://github.com/VPNclient/VPNclient-engine-flutter.git - ref: c3bf79010c05a2474a24f763d428a61788a13e9b +# vpnclient_engine_flutter: +# git: +# url: https://github.com/VPNclient/VPNclient-engine-flutter.git +# ref: 3cc3deea31667c17416ae85b219e65694a2de5f2 + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + flutter_v2ray: ^1.0.10 cupertino_icons: ^1.0.8 + url_launcher: ^6.3.1 dev_dependencies: flutter_test: sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. flutter_lints: ^5.0.0 dart_code_metrics: ^4.19.2 - flutter_gen: ^5.3.0 +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. flutter: - generate: true # ✅ ESSENCIAL para gerar flutter_gen и l10n корректно + fonts: - family: CustomIcons fonts: - asset: assets/fonts/CustomIcons.ttf + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. uses-material-design: true + # To add assets to your application, add an assets section, like this: assets: - assets/images/ - assets/images/flags/ + - assets/lang/en.json + - assets/lang/zh.json + - assets/lang/ru.json + - assets/lang/th.json + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package -flutter_gen: - output: lib/gen/ # ✅ Define где генерировать файлы + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/to/font-from-package -l10n: - arb-dir: lib/l10n - template-arb-file: app_en.arb - output-localization-file: app_localizations.dart - untranslated-messages-file: lib/l10n/untranslated_messages.txt + generate: false + + + +# l10n: +# arb-dir: l10n +# template-arb-file: app_en.arb +# output-localization-file: app_localizations.dart +# untranslated-messages-file: lib/l10n/untranslated_messages.txt diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 4bf49e7..4f78848 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,9 +6,9 @@ #include "generated_plugin_registrant.h" -#include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { - VpnclientEngineFlutterPluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("VpnclientEngineFlutterPluginCApi")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index f9fea32..88b22e5 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,7 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST - vpnclient_engine_flutter + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST