From 84792dcefe7284d79b2ed4a2ab6b0c7bded3eb4f Mon Sep 17 00:00:00 2001 From: Titan Date: Fri, 30 May 2025 03:34:17 +0900 Subject: [PATCH 1/2] Refactor localization implementation and remove unused ARB files --- assets/lang/en.json | 26 +++++++++++ lib/l10n/app_ru.arb => assets/lang/ru.json | 2 +- lib/l10n/app_th.arb => assets/lang/th.json | 2 +- lib/l10n/app_zh.arb => assets/lang/zh.json | 2 +- l10n.yaml | 4 -- lib/l10n/app_en.arb | 26 ----------- lib/localization_service.dart | 32 ++++++++++++++ lib/main.dart | 50 ++++++++-------------- lib/pages/apps/apps_page.dart | 8 ++-- lib/pages/main/location_widget.dart | 4 +- lib/pages/main/main_btn.dart | 27 +++++------- lib/pages/main/main_page.dart | 4 +- lib/pages/servers/servers_list.dart | 16 +++---- lib/pages/servers/servers_page.dart | 8 ++-- lib/search_dialog.dart | 12 +++--- pubspec.lock | 20 ++++----- pubspec.yaml | 18 +++++--- 17 files changed, 138 insertions(+), 123 deletions(-) create mode 100644 assets/lang/en.json rename lib/l10n/app_ru.arb => assets/lang/ru.json (97%) rename lib/l10n/app_th.arb => assets/lang/th.json (98%) rename lib/l10n/app_zh.arb => assets/lang/zh.json (97%) delete mode 100644 l10n.yaml delete mode 100644 lib/l10n/app_en.arb create mode 100644 lib/localization_service.dart diff --git a/assets/lang/en.json b/assets/lang/en.json new file mode 100644 index 0000000..a5c0797 --- /dev/null +++ b/assets/lang/en.json @@ -0,0 +1,26 @@ +{ + + "app_name": "VPN Client", + "apps_selection": "App Selection", + "search": "Search", + "your_location": "Your Location", + "auto_select": "Auto Select", + "kazakhstan": "Kazakhstan", + "turkey": "Turkey", + "poland": "Poland", + "fastest": "Fastest", + "selected_server": "Selected server", + "server_selection": "Server selection", + "all_servers": "All servers", + "country_name": "Country name", + "all_apps": "All Applications", + "done": "Done", + "cancel": "Cancel", + "recently_searched": "Recently searched", + "nothing_found": "Nothing found", + "connected": "CONNECTED", + "disconnected": "DISCONNECTED", + "reconnecting": "RECONNECTING", + "connecting": "CONNECTING", + "disconnecting": "DISCONNECTING" +} diff --git a/lib/l10n/app_ru.arb b/assets/lang/ru.json similarity index 97% rename from lib/l10n/app_ru.arb rename to assets/lang/ru.json index 067094e..b495f72 100644 --- a/lib/l10n/app_ru.arb +++ b/assets/lang/ru.json @@ -1,5 +1,5 @@ { - "@@locale": "ru", + "app_name": "VPN Клиент", "apps_selection": "Выбор приложений", "search": "Поиск", diff --git a/lib/l10n/app_th.arb b/assets/lang/th.json similarity index 98% rename from lib/l10n/app_th.arb rename to assets/lang/th.json index c258b47..420c726 100644 --- a/lib/l10n/app_th.arb +++ b/assets/lang/th.json @@ -1,5 +1,5 @@ { - "@@locale": "th", + "app_name": "VPN Client", "apps_selection": "เลือกแอป", "search": "ค้นหา", diff --git a/lib/l10n/app_zh.arb b/assets/lang/zh.json similarity index 97% rename from lib/l10n/app_zh.arb rename to assets/lang/zh.json index fae8b5e..3985ada 100644 --- a/lib/l10n/app_zh.arb +++ b/assets/lang/zh.json @@ -1,5 +1,5 @@ { - "@@locale": "zh", + "app_name": "VPN客户端", "apps_selection": "应用选择", "search": "搜索", diff --git a/l10n.yaml b/l10n.yaml deleted file mode 100644 index d5830f6..0000000 --- a/l10n.yaml +++ /dev/null @@ -1,4 +0,0 @@ -synthetic-package: true -arb-dir: lib/l10n -template-arb-file: app_en.arb -output-localization-file: app_localizations.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb deleted file mode 100644 index 385ce8c..0000000 --- a/lib/l10n/app_en.arb +++ /dev/null @@ -1,26 +0,0 @@ -{ - "@@locale": "en", - "app_name": "VPN Client", - "apps_selection": "App Selection", - "search": "Search", - "your_location": "Your Location", - "auto_select": "Auto Select", - "kazakhstan": "Kazakhstan", - "turkey": "Turkey", - "poland": "Poland", - "fastest": "Fastest", - "selected_server": "Selected server", - "server_selection": "Server selection", - "all_servers": "All servers", - "country_name": "Country name", - "all_apps": "All Applications", - "done": "Done", - "cancel": "Cancel", - "recently_searched": "Recently searched", - "nothing_found": "Nothing found", - "connected": "CONNECTED", - "disconnected": "DISCONNECTED", - "reconnecting": "RECONNECTING", - "connecting": "CONNECTING", - "disconnecting": "DISCONNECTING" -} diff --git a/lib/localization_service.dart b/lib/localization_service.dart new file mode 100644 index 0000000..259fbbd --- /dev/null +++ b/lib/localization_service.dart @@ -0,0 +1,32 @@ +import 'dart:convert'; +import 'package:flutter/services.dart'; +import 'package:flutter/material.dart'; + +class LocalizationService { + static Map _localizedStrings = {}; + static late Locale _currentLocale; + + static Future load(Locale locale) async { + _currentLocale = locale; + String langCode = locale.languageCode; + + // Try loading the file, fallback to English + try { + final String jsonString = await rootBundle.loadString( + 'assets/lang/$langCode.json', + ); + _localizedStrings = json.decode(jsonString); + } catch (_) { + final String fallback = await rootBundle.loadString( + 'assets/lang/en.json', + ); + _localizedStrings = json.decode(fallback); + } + } + + static String to(String key) { + return _localizedStrings[key] ?? '[$key]'; + } + + static Locale get currentLocale => _currentLocale; +} diff --git a/lib/main.dart b/lib/main.dart index 334992a..7415286 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,18 +1,24 @@ import 'package:flutter/material.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; - import 'package:vpn_client/pages/apps/apps_page.dart'; +import 'dart:ui' as ui; import 'package:vpn_client/pages/main/main_page.dart'; import 'package:vpn_client/pages/servers/servers_page.dart'; import 'package:vpn_client/theme_provider.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:vpn_client/vpn_state.dart'; +import 'package:vpn_client/localization_service.dart'; import 'design/colors.dart'; import 'nav_bar.dart'; -void main() { +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + Locale userLocale = + ui.PlatformDispatcher.instance.locale; // <-- Get the system locale + await LocalizationService.load(userLocale); + runApp( MultiProvider( providers: [ @@ -36,28 +42,23 @@ class App extends StatelessWidget { final Locale? manualLocale = null; // ← use system by default return MaterialApp( + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], debugShowCheckedModeBanner: false, title: 'VPN Client', theme: lightTheme, darkTheme: darkTheme, locale: manualLocale, - localeResolutionCallback: (locale, supportedLocales) { + localeResolutionCallback: (locale, _) { if (locale == null) return const Locale('en'); // Check for exact match - for (var supportedLocale in supportedLocales) { - if (supportedLocale.languageCode == locale.languageCode && - (supportedLocale.countryCode == null || - supportedLocale.countryCode == locale.countryCode)) { - return supportedLocale; - } - } - - // If Chinese variants are not supported, fallback to zh - if (locale.languageCode == 'zh') { - return supportedLocales.contains(const Locale('zh')) - ? const Locale('zh') - : const Locale('en'); + final supported = ['en', 'ru', 'th', 'zh']; + if (supported.contains(locale.languageCode)) { + return Locale(locale.languageCode); } // Fallback to 'en' if not found @@ -66,19 +67,6 @@ class App extends StatelessWidget { themeMode: themeProvider.themeMode, home: const MainScreen(), - - localizationsDelegates: const [ - AppLocalizations.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: const [ - Locale('en'), - Locale('ru'), - Locale('th'), - Locale('zh'), - ], ); } } diff --git a/lib/pages/apps/apps_page.dart b/lib/pages/apps/apps_page.dart index 9e45e79..fdd99ba 100644 --- a/lib/pages/apps/apps_page.dart +++ b/lib/pages/apps/apps_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:vpn_client/pages/apps/apps_list.dart'; import 'package:vpn_client/search_dialog.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:vpn_client/localization_service.dart'; class AppsPage extends StatefulWidget { const AppsPage({super.key}); @@ -19,7 +19,7 @@ class AppsPageState extends State { context: context, builder: (BuildContext context) { return SearchDialog( - placeholder: AppLocalizations.of(context)!.app_name, + placeholder: LocalizationService.to('app_name'), items: _apps, type: 1, ); @@ -40,7 +40,7 @@ class AppsPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(AppLocalizations.of(context)!.apps_selection), + title: Text(LocalizationService.to('apps_selection')), centerTitle: true, titleTextStyle: TextStyle( color: Theme.of(context).colorScheme.primary, @@ -60,7 +60,7 @@ class AppsPageState extends State { color: Theme.of(context).colorScheme.primary, ), onPressed: () => _showSearchDialog(context), - tooltip: AppLocalizations.of(context)!.search, + tooltip: LocalizationService.to('search'), ), ), ), diff --git a/lib/pages/main/location_widget.dart b/lib/pages/main/location_widget.dart index 889911f..53b724c 100644 --- a/lib/pages/main/location_widget.dart +++ b/lib/pages/main/location_widget.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:vpn_client/localization_service.dart'; class LocationWidget extends StatelessWidget { final Map? selectedServer; @@ -27,7 +27,7 @@ class LocationWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - AppLocalizations.of(context)!.your_location, + LocalizationService.to('your_location'), style: TextStyle( fontSize: 14, fontWeight: FontWeight.w400, diff --git a/lib/pages/main/main_btn.dart b/lib/pages/main/main_btn.dart index 1a1b40f..7e5d82a 100644 --- a/lib/pages/main/main_btn.dart +++ b/lib/pages/main/main_btn.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:vpn_client/design/colors.dart'; import 'package:flutter_v2ray/flutter_v2ray.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:vpn_client/localization_service.dart'; import 'package:vpn_client/vpn_state.dart'; final FlutterV2ray flutterV2ray = FlutterV2ray( @@ -47,21 +47,16 @@ class MainBtnState extends State with SingleTickerProviderStateMixin { super.dispose(); } - String get connectionStatusText { - final localizations = AppLocalizations.of(context)!; + String connectionStatusText(BuildContext context) { final vpnState = Provider.of(context, listen: false); - switch (vpnState.connectionStatus) { - case ConnectionStatus.connected: - return localizations.connected; - case ConnectionStatus.disconnected: - return localizations.disconnected; - case ConnectionStatus.reconnecting: - return localizations.reconnecting; - case ConnectionStatus.disconnecting: - return localizations.disconnecting; - case ConnectionStatus.connecting: - return localizations.connecting; - } + + return { + ConnectionStatus.connected: LocalizationService.to('connected'), + ConnectionStatus.disconnected: LocalizationService.to('disconnected'), + ConnectionStatus.reconnecting: LocalizationService.to('reconnecting'), + ConnectionStatus.disconnecting: LocalizationService.to('disconnecting'), + ConnectionStatus.connecting: LocalizationService.to('connecting'), + }[vpnState.connectionStatus]!; } Future _toggleConnection(BuildContext context) async { @@ -160,7 +155,7 @@ class MainBtnState extends State with SingleTickerProviderStateMixin { ), const SizedBox(height: 20), Text( - connectionStatusText, + connectionStatusText(context), style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, diff --git a/lib/pages/main/main_page.dart b/lib/pages/main/main_page.dart index f08a8e4..de76bd8 100644 --- a/lib/pages/main/main_page.dart +++ b/lib/pages/main/main_page.dart @@ -4,7 +4,7 @@ import 'dart:convert'; import 'package:vpn_client/pages/main/main_btn.dart'; import 'package:vpn_client/pages/main/location_widget.dart'; import 'package:vpn_client/pages/main/stat_bar.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:vpn_client/localization_service.dart'; class MainPage extends StatefulWidget { const MainPage({super.key}); @@ -55,7 +55,7 @@ class MainPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(AppLocalizations.of(context)!.app_name), + title: Text(LocalizationService.to('app_name')), centerTitle: true, titleTextStyle: TextStyle( color: Theme.of(context).colorScheme.primary, diff --git a/lib/pages/servers/servers_list.dart b/lib/pages/servers/servers_list.dart index 7a89d8d..c4343ce 100644 --- a/lib/pages/servers/servers_list.dart +++ b/lib/pages/servers/servers_list.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:vpn_client/pages/servers/servers_list_item.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:vpn_client/localization_service.dart'; import 'dart:convert'; class ServersList extends StatefulWidget { @@ -62,25 +62,25 @@ class ServersListState extends State { List> serversList = [ { 'icon': 'assets/images/flags/auto.svg', - 'text': AppLocalizations.of(context)!.auto_select, - 'ping': AppLocalizations.of(context)!.fastest, + 'text': LocalizationService.to('auto_select'), + 'ping': LocalizationService.to('fastest'), 'isActive': true, }, { 'icon': 'assets/images/flags/Kazahstan.svg', - 'text': AppLocalizations.of(context)!.kazakhstan, + 'text': LocalizationService.to('kazakhstan'), 'ping': '48', 'isActive': false, }, { 'icon': 'assets/images/flags/Turkey.svg', - 'text': AppLocalizations.of(context)!.turkey, + 'text': LocalizationService.to('turkey'), 'ping': '142', 'isActive': false, }, { 'icon': 'assets/images/flags/Poland.svg', - 'text': AppLocalizations.of(context)!.poland, + 'text': LocalizationService.to('poland'), 'ping': '298', 'isActive': false, }, @@ -178,7 +178,7 @@ class ServersListState extends State { Container( margin: const EdgeInsets.only(left: 10), child: Text( - AppLocalizations.of(context)!.selected_server, + LocalizationService.to('selected_server'), style: TextStyle(color: Colors.grey), ), ), @@ -197,7 +197,7 @@ class ServersListState extends State { Container( margin: const EdgeInsets.only(left: 10), child: Text( - AppLocalizations.of(context)!.all_servers, + LocalizationService.to('all_servers'), style: TextStyle(color: Colors.grey), ), ), diff --git a/lib/pages/servers/servers_page.dart b/lib/pages/servers/servers_page.dart index 41b0ad2..dda5741 100644 --- a/lib/pages/servers/servers_page.dart +++ b/lib/pages/servers/servers_page.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:vpn_client/pages/servers/servers_list.dart'; import 'package:vpn_client/search_dialog.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:vpn_client/localization_service.dart'; class ServersPage extends StatefulWidget { final Function(int) onNavBarTap; @@ -23,7 +23,7 @@ class ServersPageState extends State { context: context, builder: (BuildContext context) { return SearchDialog( - placeholder: AppLocalizations.of(context)!.country_name, + placeholder: LocalizationService.to('country_name'), items: _servers, type: 2, ); @@ -47,7 +47,7 @@ class ServersPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(AppLocalizations.of(context)!.selected_server), + title: Text(LocalizationService.to('selected_server')), centerTitle: true, titleTextStyle: TextStyle( color: Theme.of(context).colorScheme.primary, @@ -67,7 +67,7 @@ class ServersPageState extends State { color: Theme.of(context).colorScheme.primary, ), onPressed: () => _showSearchDialog(context), - tooltip: AppLocalizations.of(context)!.search, + tooltip: LocalizationService.to('search'), ), ), ), diff --git a/lib/search_dialog.dart b/lib/search_dialog.dart index 67d3a06..9aaa304 100644 --- a/lib/search_dialog.dart +++ b/lib/search_dialog.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:vpn_client/pages/apps/apps_list_item.dart'; import 'package:vpn_client/pages/servers/servers_list_item.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:vpn_client/localization_service.dart'; import 'dart:convert'; class SearchDialog extends StatefulWidget { @@ -137,7 +137,7 @@ class _SearchDialogState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - AppLocalizations.of(context)!.search, + LocalizationService.to('search'), style: TextStyle( fontSize: 24, fontWeight: FontWeight.w600, @@ -155,7 +155,7 @@ class _SearchDialogState extends State { Navigator.of(context).pop(widget.items); }, child: Text( - AppLocalizations.of(context)!.done, + LocalizationService.to('done'), textAlign: TextAlign.center, style: TextStyle( color: Colors.blue, @@ -174,7 +174,7 @@ class _SearchDialogState extends State { Navigator.of(context).pop(); }, child: Text( - AppLocalizations.of(context)!.cancel, + LocalizationService.to('cancel'), textAlign: TextAlign.center, style: TextStyle( color: Colors.blue, @@ -248,7 +248,7 @@ class _SearchDialogState extends State { Container( margin: const EdgeInsets.only(left: 20), child: Text( - AppLocalizations.of(context)!.recently_searched, + LocalizationService.to('recently_searched'), style: TextStyle(color: Colors.grey), ), ), @@ -311,7 +311,7 @@ class _SearchDialogState extends State { ? _filteredItems.isEmpty ? Center( child: Text( - AppLocalizations.of(context)!.nothing_found, + LocalizationService.to('nothing_found'), style: TextStyle( color: Theme.of(context).colorScheme.primary, ), diff --git a/pubspec.lock b/pubspec.lock index 7c0530a..d3e7c33 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" bloc: dependency: transitive description: @@ -149,10 +149,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" ffi: dependency: transitive description: @@ -273,18 +273,18 @@ packages: dependency: transitive description: name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.dev" source: hosted - version: "0.19.0" + version: "0.20.2" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: @@ -598,10 +598,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "15.0.0" watcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5644e73..ef2d86e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -65,7 +65,7 @@ dev_dependencies: # The following section is specific to Flutter packages. flutter: - + fonts: - family: CustomIcons fonts: @@ -80,6 +80,10 @@ flutter: assets: - assets/images/ - assets/images/flags/ + - assets/lang/en.json + - assets/lang/zh.json + - assets/lang/ru.json + - assets/lang/th.json # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images @@ -107,12 +111,12 @@ flutter: # For details regarding fonts from package dependencies, # see https://flutter.dev/to/font-from-package - generate: true + generate: false -l10n: - arb-dir: l10n - template-arb-file: app_en.arb - output-localization-file: app_localizations.dart - untranslated-messages-file: lib/l10n/untranslated_messages.txt +# l10n: +# arb-dir: l10n +# template-arb-file: app_en.arb +# output-localization-file: app_localizations.dart +# untranslated-messages-file: lib/l10n/untranslated_messages.txt From 999a2f4b7c169e6a88377bf0d0ef57b0b306b88e Mon Sep 17 00:00:00 2001 From: Titan Date: Fri, 30 May 2025 05:10:07 +0900 Subject: [PATCH 2/2] Add support for settings page and localization updates - Introduced new SVG assets for active home, server, and settings icons. - Added new action button and reset settings dialog components. - Implemented support service card and setting info card for displaying connection and support status. - Integrated URL launcher functionality to connect to Telegram bot. - Updated localization files (en.json, ru.json, th.json, zh.json) with new keys for settings and actions. - Modified main.dart and nav_bar.dart to include the new settings page. - Updated colors.dart to change the main gradient colors. - Registered URL launcher plugin for Linux and Windows platforms. - Updated pubspec.yaml to include url_launcher dependency. --- assets/images/active_home_o.svg | 14 +++ assets/images/active_server_o.svg | 14 +++ assets/images/active_settings_o.svg | 14 +++ assets/images/support_icons.png | Bin 0 -> 1108 bytes assets/lang/en.json | 17 ++- assets/lang/ru.json | 17 ++- assets/lang/th.json | 17 ++- assets/lang/zh.json | 17 ++- lib/design/colors.dart | 2 +- lib/design/images.dart | 7 +- lib/main.dart | 3 +- lib/nav_bar.dart | 2 +- lib/pages/settings/action_button.dart | 75 ++++++++++++ lib/pages/settings/reset_settings_dialog.dart | 95 +++++++++++++++ lib/pages/settings/setting_info_card.dart | 98 +++++++++++++++ lib/pages/settings/setting_page.dart | 112 ++++++++++++++++++ lib/pages/settings/snackbar_utils.dart | 49 ++++++++ lib/pages/settings/support_service_card.dart | 55 +++++++++ lib/pages/settings/url_launcher_utils.dart | 13 ++ linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 64 ++++++++++ pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 26 files changed, 688 insertions(+), 9 deletions(-) create mode 100644 assets/images/active_home_o.svg create mode 100644 assets/images/active_server_o.svg create mode 100644 assets/images/active_settings_o.svg create mode 100644 assets/images/support_icons.png create mode 100644 lib/pages/settings/action_button.dart create mode 100644 lib/pages/settings/reset_settings_dialog.dart create mode 100644 lib/pages/settings/setting_info_card.dart create mode 100644 lib/pages/settings/setting_page.dart create mode 100644 lib/pages/settings/snackbar_utils.dart create mode 100644 lib/pages/settings/support_service_card.dart create mode 100644 lib/pages/settings/url_launcher_utils.dart diff --git a/assets/images/active_home_o.svg b/assets/images/active_home_o.svg new file mode 100644 index 0000000..f0b32ed --- /dev/null +++ b/assets/images/active_home_o.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/images/active_server_o.svg b/assets/images/active_server_o.svg new file mode 100644 index 0000000..4830726 --- /dev/null +++ b/assets/images/active_server_o.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/images/active_settings_o.svg b/assets/images/active_settings_o.svg new file mode 100644 index 0000000..b56dea8 --- /dev/null +++ b/assets/images/active_settings_o.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/images/support_icons.png b/assets/images/support_icons.png new file mode 100644 index 0000000000000000000000000000000000000000..83a72d9cd5bd47556d0c9c49aa8f8d44b07e4b8c GIT binary patch literal 1108 zcmV-a1grarP)eB9LGOzl{CR!Fqb&&Aze%CKhR9kLj=(Yw6J;zd+?-(+C$SGgajeQ9#U)Q!9$T8 zdT>j@Ibv{DQmD-@)gWYfbkdg%sNf`{bwkX@_EYUVv>ckSo0F!O%z z_x;X$^XAR|MsNV8Wi|5n#1J8I7Gw;F6;H+X4LTgzonMKIlq=NXxK28|Uw|lg(6r() zq?iV*z$I!SY{ZlCaz^T&Jz#eCT>@?buUA2tUrZ1~#_5)A@9)gg?h|_qnB9G22sr^9 zuc|zM97hm0UVatt!Cya>4(=E*yZbH`#UE`1mT|h}VQC};?9BP!fySDO^G_Zj-*TOF zHUxkSjq(2x571a#WDv!xf4|m>v=EO@))U}T*>1LRzUv$vW-C#{AQEl>V6(8rTIMcm znR_fOE^){GBbbDbPYemOyZ1VROS*kT!VL`cCm8Hc>qZwr<)Y(iUb< z?*f{m&~CPIZFGdFaY`$F&RggG%cJDHb**^738A?y80b&%)%6K&@c>byiSKVtGti&V z?IKpM6ZOH+ARi75>Lz`TE5jE7n3d6)iD3sH{du(m*5}_T5(LCaxA0xrvV|z76EOjY{g! z#6n{NY~g)J5qf7KGC zildN6X%$M^sfv%nvb(DEhNfg7=dF{pZzzjW;k)!LrS{6tfz<4`B<+-Hm@gIx@{-r>cpT-Bko?n=U^Le{@xOifS2vrzQ%802$G&&%gMRUw*rz9H(sn zVD-;6e)#ccBH{3%HhW1s#q`WKTCu|H>6O}Ag<{N=8xR6j7szNI1;oXIIO%IjndftM$K-A9CIXId6lg(Ny+)(oQiIz-m^{)52Qp z&^`Gi=dCj~F&Wg}DqD|~4-jQ*eStGGJI7RzPjksiJyN&`-VLV5@+`ZnB<&l)?`T8P z6{eMFL~j2RY|m9f$mWpi%;f>6)>_n&_J1`pA>RY?wH2g99>F*i%dB2DlQ)`Mwt;v} zW%)($vFoJMu&Yk&Wiv}g^BDocs+ll92s!RL>79D^o=Ux!%{WGL%R}HCN3U9+FJAdQ z#rWsGl_-wH*GNtxgjqd(2jU=PZ)w`>=D;VIX~koJ#S>v7I7>A@w?SkPas?jQuH)>x a!2An_lT3|F$&McY0000 { ServersPage(onNavBarTap: _handleNavBarTap), const MainPage(), const PlaceholderPage(text: 'Speed Page'), - const PlaceholderPage(text: 'Settings Page'), + SettingPage(onNavBarTap: _handleNavBarTap), ]; } diff --git a/lib/nav_bar.dart b/lib/nav_bar.dart index 4438d2e..0b3113b 100644 --- a/lib/nav_bar.dart +++ b/lib/nav_bar.dart @@ -27,7 +27,7 @@ class NavBarState extends State { activeServerIcon, activeHomeIcon, speedIcon, - settingsIcon, + activeSettingsIcon, ]; @override diff --git a/lib/pages/settings/action_button.dart b/lib/pages/settings/action_button.dart new file mode 100644 index 0000000..b712585 --- /dev/null +++ b/lib/pages/settings/action_button.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:vpn_client/localization_service.dart'; + +class ActionButton extends StatelessWidget { + final bool isConnected; + final VoidCallback onResetPressed; + final VoidCallback onConnectPressed; + + const ActionButton({ + super.key, + required this.isConnected, + required this.onResetPressed, + required this.onConnectPressed, + }); + + @override + Widget build(BuildContext context) { + return isConnected + ? Material( + elevation: 0, + borderRadius: BorderRadius.circular(8), + color: Colors.white, + child: SizedBox( + width: 500, + child: TextButton( + onPressed: onResetPressed, + style: TextButton.styleFrom( + backgroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text( + LocalizationService.to('reset_settings'), + style: const TextStyle(color: Colors.red, fontSize: 16), + ), + ), + ), + ) + : Container( + width: 500, + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Color(0xFFFBB800), Color(0xFFEA7500)], + ), + borderRadius: BorderRadius.circular(8), + ), + child: ElevatedButton( + onPressed: onConnectPressed, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.transparent, + shadowColor: Colors.transparent, + padding: const EdgeInsets.symmetric( + horizontal: 130, + vertical: 12, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text( + LocalizationService.to('connect'), + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + ); + } +} diff --git a/lib/pages/settings/reset_settings_dialog.dart b/lib/pages/settings/reset_settings_dialog.dart new file mode 100644 index 0000000..6e519d3 --- /dev/null +++ b/lib/pages/settings/reset_settings_dialog.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:vpn_client/localization_service.dart'; + +class ResetSettingsDialog extends StatelessWidget { + const ResetSettingsDialog({super.key}); + + @override + Widget build(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Container( + width: 500, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + LocalizationService.to('reset_settings'), + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.black, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + + Text( + LocalizationService.to('are_you_sure_reset'), + style: const TextStyle( + fontSize: 14, + color: Colors.grey, + height: 1.4, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + + Row( + children: [ + Expanded( + child: TextButton( + onPressed: () { + Navigator.of(context).pop(false); + }, + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text( + LocalizationService.to('cancel'), + style: const TextStyle( + color: Colors.orange, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: TextButton( + onPressed: () { + Navigator.of(context).pop(true); + }, + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text( + LocalizationService.to('reset'), + style: const TextStyle( + color: Colors.red, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/settings/setting_info_card.dart b/lib/pages/settings/setting_info_card.dart new file mode 100644 index 0000000..a13fa78 --- /dev/null +++ b/lib/pages/settings/setting_info_card.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:vpn_client/localization_service.dart'; + +class SettingInfoCard extends StatelessWidget { + final bool isConnected; + final String connectionStatus; + final String supportStatus; + final String userId; + + const SettingInfoCard({ + super.key, + required this.isConnected, + required this.connectionStatus, + required this.supportStatus, + required this.userId, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(13), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + LocalizationService.to('about_app'), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.grey, + ), + ), + ), + ), + _buildSettingRow( + LocalizationService.to('version'), + 'v 1.0', + Colors.orange, + ), + _buildSettingRow( + LocalizationService.to('connection'), + isConnected + ? connectionStatus + : LocalizationService.to('not_connected'), + isConnected ? Colors.orange : Colors.red, + ), + _buildSettingRow( + LocalizationService.to('support'), + isConnected ? supportStatus : LocalizationService.to('unavailable'), + isConnected ? Colors.orange : Colors.grey, + ), + _buildSettingRow( + LocalizationService.to('your_id'), + isConnected ? userId : '—', + isConnected ? Colors.grey[600]! : Colors.grey, + ), + ], + ), + ); + } + + Widget _buildSettingRow(String label, String value, Color valueColor) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: const TextStyle(fontSize: 16, color: Colors.black), + ), + Text( + value, + style: TextStyle( + fontSize: 16, + color: valueColor, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/settings/setting_page.dart b/lib/pages/settings/setting_page.dart new file mode 100644 index 0000000..3874257 --- /dev/null +++ b/lib/pages/settings/setting_page.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; +import 'package:vpn_client/localization_service.dart'; +import 'setting_info_card.dart'; +import 'support_service_card.dart'; +import 'action_button.dart'; +import 'reset_settings_dialog.dart'; +import 'snackbar_utils.dart'; +import 'url_launcher_utils.dart'; + +class SettingPage extends StatefulWidget { + final Function(int) onNavBarTap; + + const SettingPage({super.key, required this.onNavBarTap}); + + @override + State createState() => _SettingPageState(); +} + +class _SettingPageState extends State { + bool _isConnected = true; + String _connectionStatus = '1 me/vnp_client_bot'; + String _supportStatus = '1 me/vnp_client_support'; + String _userId = '2485926342'; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey[50], + appBar: AppBar( + backgroundColor: Colors.grey[50], + elevation: 0, + title: Text( + LocalizationService.to('settings'), + style: const TextStyle( + color: Colors.black, + fontSize: 18, + fontWeight: FontWeight.w500, + ), + ), + centerTitle: true, + leading: const SizedBox(), + ), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 20), + + SettingInfoCard( + isConnected: _isConnected, + connectionStatus: _connectionStatus, + supportStatus: _supportStatus, + userId: _userId, + ), + + const SizedBox(height: 20), + + SupportServiceCard( + onTap: () { + // Handle support service tap + }, + ), + + const SizedBox(height: 30), + + Center( + child: ActionButton( + isConnected: _isConnected, + onResetPressed: _showResetDialog, + onConnectPressed: _connectToBot, + ), + ), + ], + ), + ), + ); + } + + void _showResetDialog() async { + final result = await showDialog( + context: context, + barrierDismissible: true, + builder: (context) => const ResetSettingsDialog(), + ); + + if (result == true) { + _resetSettings(); + } + } + + void _resetSettings() { + setState(() { + _isConnected = false; + _connectionStatus = ''; + _supportStatus = ''; + _userId = ''; + }); + + SnackbarUtils.showResetSuccessSnackbar(context); + } + + void _connectToBot() async { + final success = await UrlLauncherUtils.launchTelegramBot(); + + if (!mounted) return; + + if (!success) { + SnackbarUtils.showTelegramErrorSnackbar(context); + } + } +} diff --git a/lib/pages/settings/snackbar_utils.dart b/lib/pages/settings/snackbar_utils.dart new file mode 100644 index 0000000..7037c9d --- /dev/null +++ b/lib/pages/settings/snackbar_utils.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:vpn_client/localization_service.dart'; + +class SnackbarUtils { + static void showResetSuccessSnackbar(BuildContext context) { + final snackBar = SnackBar( + backgroundColor: Colors.transparent, + elevation: 0, + behavior: SnackBarBehavior.floating, + content: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color.fromARGB(255, 122, 122, 122), + Color.fromARGB(255, 122, 122, 122), + ], + ), + borderRadius: BorderRadius.circular(50), + boxShadow: [ + BoxShadow( + color: const Color(0x1A9CA9C2), + blurRadius: 16, + offset: const Offset(0, 1), + ), + ], + ), + child: Text( + LocalizationService.to('connection_reset'), + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.white), + ), + ), + ); + + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } + + static void showTelegramErrorSnackbar(BuildContext context) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(LocalizationService.to('failed_open_telegram')), + backgroundColor: Colors.red, + ), + ); + } +} diff --git a/lib/pages/settings/support_service_card.dart b/lib/pages/settings/support_service_card.dart new file mode 100644 index 0000000..d417629 --- /dev/null +++ b/lib/pages/settings/support_service_card.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:vpn_client/localization_service.dart'; + +class SupportServiceCard extends StatelessWidget { + final VoidCallback? onTap; + + const SupportServiceCard({super.key, this.onTap}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(13), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: InkWell( + onTap: onTap, + child: Row( + children: [ + Container( + width: 24, + height: 24, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(4), + ), + child: Image.asset( + 'assets/images/support_icons.png', + width: 16, + height: 16, + fit: BoxFit.cover, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + LocalizationService.to('support_service'), + style: const TextStyle(fontSize: 16, color: Colors.black), + ), + ), + Icon(Icons.chevron_right, color: Colors.grey[400], size: 20), + ], + ), + ), + ); + } +} diff --git a/lib/pages/settings/url_launcher_utils.dart b/lib/pages/settings/url_launcher_utils.dart new file mode 100644 index 0000000..89be3fe --- /dev/null +++ b/lib/pages/settings/url_launcher_utils.dart @@ -0,0 +1,13 @@ +import 'package:url_launcher/url_launcher.dart'; + +class UrlLauncherUtils { + static Future launchTelegramBot() async { + const botUrl = 'https://t.me/vnp_client_bot'; + + if (await canLaunchUrl(Uri.parse(botUrl))) { + await launchUrl(Uri.parse(botUrl), mode: LaunchMode.externalApplication); + return true; + } + return false; + } +} diff --git a/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 d3e7c33..d3143e9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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: diff --git a/pubspec.yaml b/pubspec.yaml index ef2d86e..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: 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