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 new file mode 100644 index 0000000..612e72d --- /dev/null +++ b/assets/lang/en.json @@ -0,0 +1,41 @@ +{ + + "app_name": "VPN Client", + "apps_selection": "App Selection", + "search": "Search", + "your_location": "Your Location", + "auto_select": "Auto Select", + "kazakhstan": "Kazakhstan", + "turkey": "Turkey", + "poland": "Poland", + "fastest": "Fastest", + "selected_server": "Selected server", + "server_selection": "Server selection", + "all_servers": "All servers", + "country_name": "Country name", + "all_apps": "All Applications", + "done": "Done", + "cancel": "Cancel", + "recently_searched": "Recently searched", + "nothing_found": "Nothing found", + "connected": "CONNECTED", + "disconnected": "DISCONNECTED", + "reconnecting": "RECONNECTING", + "connecting": "CONNECTING", + "disconnecting": "DISCONNECTING", + "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/lib/l10n/app_ru.arb b/assets/lang/ru.json similarity index 53% rename from lib/l10n/app_ru.arb rename to assets/lang/ru.json index 067094e..1e302b1 100644 --- a/lib/l10n/app_ru.arb +++ b/assets/lang/ru.json @@ -1,5 +1,5 @@ { - "@@locale": "ru", + "app_name": "VPN Клиент", "apps_selection": "Выбор приложений", "search": "Поиск", @@ -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/l10n/app_th.arb b/assets/lang/th.json similarity index 53% rename from lib/l10n/app_th.arb rename to assets/lang/th.json index c258b47..de766dc 100644 --- a/lib/l10n/app_th.arb +++ b/assets/lang/th.json @@ -1,5 +1,5 @@ { - "@@locale": "th", + "app_name": "VPN Client", "apps_selection": "เลือกแอป", "search": "ค้นหา", @@ -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/lib/l10n/app_zh.arb b/assets/lang/zh.json similarity index 56% rename from lib/l10n/app_zh.arb rename to assets/lang/zh.json index fae8b5e..43128d3 100644 --- a/lib/l10n/app_zh.arb +++ b/assets/lang/zh.json @@ -1,5 +1,5 @@ { - "@@locale": "zh", + "app_name": "VPN客户端", "apps_selection": "应用选择", "search": "搜索", @@ -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/l10n.yaml b/l10n.yaml deleted file mode 100644 index d5830f6..0000000 --- a/l10n.yaml +++ /dev/null @@ -1,4 +0,0 @@ -synthetic-package: true -arb-dir: lib/l10n -template-arb-file: app_en.arb -output-localization-file: app_localizations.dart diff --git a/lib/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_en.arb b/lib/l10n/app_en.arb deleted file mode 100644 index 385ce8c..0000000 --- a/lib/l10n/app_en.arb +++ /dev/null @@ -1,26 +0,0 @@ -{ - "@@locale": "en", - "app_name": "VPN Client", - "apps_selection": "App Selection", - "search": "Search", - "your_location": "Your Location", - "auto_select": "Auto Select", - "kazakhstan": "Kazakhstan", - "turkey": "Turkey", - "poland": "Poland", - "fastest": "Fastest", - "selected_server": "Selected server", - "server_selection": "Server selection", - "all_servers": "All servers", - "country_name": "Country name", - "all_apps": "All Applications", - "done": "Done", - "cancel": "Cancel", - "recently_searched": "Recently searched", - "nothing_found": "Nothing found", - "connected": "CONNECTED", - "disconnected": "DISCONNECTED", - "reconnecting": "RECONNECTING", - "connecting": "CONNECTING", - "disconnecting": "DISCONNECTING" -} diff --git a/lib/localization_service.dart b/lib/localization_service.dart new file mode 100644 index 0000000..259fbbd --- /dev/null +++ b/lib/localization_service.dart @@ -0,0 +1,32 @@ +import 'dart:convert'; +import 'package:flutter/services.dart'; +import 'package:flutter/material.dart'; + +class LocalizationService { + static Map _localizedStrings = {}; + static late Locale _currentLocale; + + static Future load(Locale locale) async { + _currentLocale = locale; + String langCode = locale.languageCode; + + // Try loading the file, fallback to English + try { + final String jsonString = await rootBundle.loadString( + 'assets/lang/$langCode.json', + ); + _localizedStrings = json.decode(jsonString); + } catch (_) { + final String fallback = await rootBundle.loadString( + 'assets/lang/en.json', + ); + _localizedStrings = json.decode(fallback); + } + } + + static String to(String key) { + return _localizedStrings[key] ?? '[$key]'; + } + + static Locale get currentLocale => _currentLocale; +} diff --git a/lib/main.dart b/lib/main.dart index 334992a..469fc36 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,18 +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/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: [ @@ -36,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 @@ -66,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'), - ], ); } } @@ -102,7 +91,7 @@ class _MainScreenState extends State { ServersPage(onNavBarTap: _handleNavBarTap), const MainPage(), const PlaceholderPage(text: 'Speed Page'), - const PlaceholderPage(text: 'Settings Page'), + SettingPage(onNavBarTap: _handleNavBarTap), ]; } diff --git a/lib/nav_bar.dart b/lib/nav_bar.dart index 4438d2e..0b3113b 100644 --- a/lib/nav_bar.dart +++ b/lib/nav_bar.dart @@ -27,7 +27,7 @@ class NavBarState extends State { activeServerIcon, activeHomeIcon, speedIcon, - settingsIcon, + activeSettingsIcon, ]; @override diff --git a/lib/pages/apps/apps_page.dart b/lib/pages/apps/apps_page.dart index 9e45e79..fdd99ba 100644 --- a/lib/pages/apps/apps_page.dart +++ b/lib/pages/apps/apps_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:vpn_client/pages/apps/apps_list.dart'; import 'package:vpn_client/search_dialog.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:vpn_client/localization_service.dart'; class AppsPage extends StatefulWidget { const AppsPage({super.key}); @@ -19,7 +19,7 @@ class AppsPageState extends State { context: context, builder: (BuildContext context) { return SearchDialog( - placeholder: AppLocalizations.of(context)!.app_name, + placeholder: LocalizationService.to('app_name'), items: _apps, type: 1, ); @@ -40,7 +40,7 @@ class AppsPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(AppLocalizations.of(context)!.apps_selection), + title: Text(LocalizationService.to('apps_selection')), centerTitle: true, titleTextStyle: TextStyle( color: Theme.of(context).colorScheme.primary, @@ -60,7 +60,7 @@ class AppsPageState extends State { color: Theme.of(context).colorScheme.primary, ), onPressed: () => _showSearchDialog(context), - tooltip: AppLocalizations.of(context)!.search, + tooltip: LocalizationService.to('search'), ), ), ), diff --git a/lib/pages/main/location_widget.dart b/lib/pages/main/location_widget.dart index 889911f..53b724c 100644 --- a/lib/pages/main/location_widget.dart +++ b/lib/pages/main/location_widget.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:vpn_client/localization_service.dart'; class LocationWidget extends StatelessWidget { final Map? selectedServer; @@ -27,7 +27,7 @@ class LocationWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - AppLocalizations.of(context)!.your_location, + LocalizationService.to('your_location'), style: TextStyle( fontSize: 14, fontWeight: FontWeight.w400, diff --git a/lib/pages/main/main_btn.dart b/lib/pages/main/main_btn.dart index 1a1b40f..7e5d82a 100644 --- a/lib/pages/main/main_btn.dart +++ b/lib/pages/main/main_btn.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:vpn_client/design/colors.dart'; import 'package:flutter_v2ray/flutter_v2ray.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:vpn_client/localization_service.dart'; import 'package:vpn_client/vpn_state.dart'; final FlutterV2ray flutterV2ray = FlutterV2ray( @@ -47,21 +47,16 @@ class MainBtnState extends State with SingleTickerProviderStateMixin { super.dispose(); } - String get connectionStatusText { - final localizations = AppLocalizations.of(context)!; + String connectionStatusText(BuildContext context) { final vpnState = Provider.of(context, listen: false); - switch (vpnState.connectionStatus) { - case ConnectionStatus.connected: - return localizations.connected; - case ConnectionStatus.disconnected: - return localizations.disconnected; - case ConnectionStatus.reconnecting: - return localizations.reconnecting; - case ConnectionStatus.disconnecting: - return localizations.disconnecting; - case ConnectionStatus.connecting: - return localizations.connecting; - } + + return { + ConnectionStatus.connected: LocalizationService.to('connected'), + ConnectionStatus.disconnected: LocalizationService.to('disconnected'), + ConnectionStatus.reconnecting: LocalizationService.to('reconnecting'), + ConnectionStatus.disconnecting: LocalizationService.to('disconnecting'), + ConnectionStatus.connecting: LocalizationService.to('connecting'), + }[vpnState.connectionStatus]!; } Future _toggleConnection(BuildContext context) async { @@ -160,7 +155,7 @@ class MainBtnState extends State with SingleTickerProviderStateMixin { ), const SizedBox(height: 20), Text( - connectionStatusText, + connectionStatusText(context), style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, diff --git a/lib/pages/main/main_page.dart b/lib/pages/main/main_page.dart index f08a8e4..de76bd8 100644 --- a/lib/pages/main/main_page.dart +++ b/lib/pages/main/main_page.dart @@ -4,7 +4,7 @@ import 'dart:convert'; import 'package:vpn_client/pages/main/main_btn.dart'; import 'package:vpn_client/pages/main/location_widget.dart'; import 'package:vpn_client/pages/main/stat_bar.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:vpn_client/localization_service.dart'; class MainPage extends StatefulWidget { const MainPage({super.key}); @@ -55,7 +55,7 @@ class MainPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(AppLocalizations.of(context)!.app_name), + title: Text(LocalizationService.to('app_name')), centerTitle: true, titleTextStyle: TextStyle( color: Theme.of(context).colorScheme.primary, diff --git a/lib/pages/servers/servers_list.dart b/lib/pages/servers/servers_list.dart index 7a89d8d..c4343ce 100644 --- a/lib/pages/servers/servers_list.dart +++ b/lib/pages/servers/servers_list.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:vpn_client/pages/servers/servers_list_item.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:vpn_client/localization_service.dart'; import 'dart:convert'; class ServersList extends StatefulWidget { @@ -62,25 +62,25 @@ class ServersListState extends State { List> serversList = [ { 'icon': 'assets/images/flags/auto.svg', - 'text': AppLocalizations.of(context)!.auto_select, - 'ping': AppLocalizations.of(context)!.fastest, + 'text': LocalizationService.to('auto_select'), + 'ping': LocalizationService.to('fastest'), 'isActive': true, }, { 'icon': 'assets/images/flags/Kazahstan.svg', - 'text': AppLocalizations.of(context)!.kazakhstan, + 'text': LocalizationService.to('kazakhstan'), 'ping': '48', 'isActive': false, }, { 'icon': 'assets/images/flags/Turkey.svg', - 'text': AppLocalizations.of(context)!.turkey, + 'text': LocalizationService.to('turkey'), 'ping': '142', 'isActive': false, }, { 'icon': 'assets/images/flags/Poland.svg', - 'text': AppLocalizations.of(context)!.poland, + 'text': LocalizationService.to('poland'), 'ping': '298', 'isActive': false, }, @@ -178,7 +178,7 @@ class ServersListState extends State { Container( margin: const EdgeInsets.only(left: 10), child: Text( - AppLocalizations.of(context)!.selected_server, + LocalizationService.to('selected_server'), style: TextStyle(color: Colors.grey), ), ), @@ -197,7 +197,7 @@ class ServersListState extends State { Container( margin: const EdgeInsets.only(left: 10), child: Text( - AppLocalizations.of(context)!.all_servers, + LocalizationService.to('all_servers'), style: TextStyle(color: Colors.grey), ), ), diff --git a/lib/pages/servers/servers_page.dart b/lib/pages/servers/servers_page.dart index 41b0ad2..dda5741 100644 --- a/lib/pages/servers/servers_page.dart +++ b/lib/pages/servers/servers_page.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:vpn_client/pages/servers/servers_list.dart'; import 'package:vpn_client/search_dialog.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:vpn_client/localization_service.dart'; class ServersPage extends StatefulWidget { final Function(int) onNavBarTap; @@ -23,7 +23,7 @@ class ServersPageState extends State { context: context, builder: (BuildContext context) { return SearchDialog( - placeholder: AppLocalizations.of(context)!.country_name, + placeholder: LocalizationService.to('country_name'), items: _servers, type: 2, ); @@ -47,7 +47,7 @@ class ServersPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(AppLocalizations.of(context)!.selected_server), + title: Text(LocalizationService.to('selected_server')), centerTitle: true, titleTextStyle: TextStyle( color: Theme.of(context).colorScheme.primary, @@ -67,7 +67,7 @@ class ServersPageState extends State { color: Theme.of(context).colorScheme.primary, ), onPressed: () => _showSearchDialog(context), - tooltip: AppLocalizations.of(context)!.search, + tooltip: LocalizationService.to('search'), ), ), ), diff --git a/lib/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 67d3a06..9aaa304 100644 --- a/lib/search_dialog.dart +++ b/lib/search_dialog.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:vpn_client/pages/apps/apps_list_item.dart'; import 'package:vpn_client/pages/servers/servers_list_item.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:vpn_client/localization_service.dart'; import 'dart:convert'; class SearchDialog extends StatefulWidget { @@ -137,7 +137,7 @@ class _SearchDialogState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - AppLocalizations.of(context)!.search, + LocalizationService.to('search'), style: TextStyle( fontSize: 24, fontWeight: FontWeight.w600, @@ -155,7 +155,7 @@ class _SearchDialogState extends State { Navigator.of(context).pop(widget.items); }, child: Text( - AppLocalizations.of(context)!.done, + LocalizationService.to('done'), textAlign: TextAlign.center, style: TextStyle( color: Colors.blue, @@ -174,7 +174,7 @@ class _SearchDialogState extends State { Navigator.of(context).pop(); }, child: Text( - AppLocalizations.of(context)!.cancel, + LocalizationService.to('cancel'), textAlign: TextAlign.center, style: TextStyle( color: Colors.blue, @@ -248,7 +248,7 @@ class _SearchDialogState extends State { Container( margin: const EdgeInsets.only(left: 20), child: Text( - AppLocalizations.of(context)!.recently_searched, + LocalizationService.to('recently_searched'), style: TextStyle(color: Colors.grey), ), ), @@ -311,7 +311,7 @@ class _SearchDialogState extends State { ? _filteredItems.isEmpty ? Center( child: Text( - AppLocalizations.of(context)!.nothing_found, + LocalizationService.to('nothing_found'), style: TextStyle( color: Theme.of(context).colorScheme.primary, ), diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e71a16d..f6f23bf 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) 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 2e1de87..f16b4c3 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 724bb2a..997e35d 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,7 +6,9 @@ import FlutterMacOS import Foundation import shared_preferences_foundation +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 7c0530a..d3143e9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" bloc: dependency: transitive description: @@ -149,10 +149,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" ffi: dependency: transitive description: @@ -273,18 +273,18 @@ packages: dependency: transitive description: name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.dev" source: hosted - version: "0.19.0" + version: "0.20.2" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: @@ -562,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: @@ -598,10 +662,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "15.0.0" watcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5644e73..320fca9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -47,6 +47,7 @@ dependencies: # 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: @@ -65,7 +66,7 @@ dev_dependencies: # The following section is specific to Flutter packages. flutter: - + fonts: - family: CustomIcons fonts: @@ -80,6 +81,10 @@ flutter: assets: - assets/images/ - assets/images/flags/ + - assets/lang/en.json + - assets/lang/zh.json + - assets/lang/ru.json + - assets/lang/th.json # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images @@ -107,12 +112,12 @@ flutter: # For details regarding fonts from package dependencies, # see https://flutter.dev/to/font-from-package - generate: true + generate: false -l10n: - arb-dir: l10n - template-arb-file: app_en.arb - output-localization-file: app_localizations.dart - untranslated-messages-file: lib/l10n/untranslated_messages.txt +# l10n: +# arb-dir: l10n +# template-arb-file: app_en.arb +# output-localization-file: app_localizations.dart +# untranslated-messages-file: lib/l10n/untranslated_messages.txt diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 8b6d468..4f78848 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,9 @@ #include "generated_plugin_registrant.h" +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b93c4c3..88b22e5 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST