Skip to content

Commit 56f0bab

Browse files
committed
Apply PR #27: refactor search dialog, improve localization, update dependencies
1 parent 130df0e commit 56f0bab

16 files changed

+592
-457
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// lib/core/constants/storage_keys.dart
2+
class StorageKeys {
3+
static const String selectedApps = 'selected_apps';
4+
static const String selectedServers = 'selected_servers';
5+
static const String recentlySearchedApps = 'recently_searched_apps';
6+
static const String recentlySearchedServers = 'recently_searched_servers';
7+
static const String isDarkTheme = 'isDarkTheme';
8+
}

lib/design/dimensions.dart

Lines changed: 0 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,3 @@
1-
import 'package:flutter/material.dart';
2-
3-
class CustomString {
4-
final BuildContext context;
5-
late Locale locale;
6-
7-
CustomString(this.context) {
8-
locale = Localizations.localeOf(context);
9-
}
10-
11-
String get connected {
12-
return _localized('connected');
13-
}
14-
15-
String get disconnected {
16-
return _localized('disconnected');
17-
}
18-
19-
String get connecting {
20-
return _localized('connecting');
21-
}
22-
23-
String get disconnecting {
24-
return _localized('disconnecting');
25-
}
26-
27-
String get allapp {
28-
return _localized('all_apps');
29-
}
30-
31-
String _localized(String key) {
32-
switch (locale.languageCode) {
33-
case 'ru':
34-
return {
35-
'connected': 'ПОДКЛЮЧЕН',
36-
'disconnected': 'ОТКЛЮЧЕН',
37-
'connecting': 'ПОДКЛЮЧЕНИЕ',
38-
'disconnecting': 'ОТКЛЮЧЕНИЕ',
39-
"all_apps": "Все приложения",
40-
}[key]!;
41-
case 'th':
42-
return {
43-
"connected": "เชื่อมต่อแล้ว",
44-
"disconnected": "ไม่ได้เชื่อมต่อ",
45-
"connecting": "กำลังเชื่อมต่อ",
46-
"disconnecting": "กำลังตัดการเชื่อมต่อ",
47-
"all_apps": "แอปทั้งหมด",
48-
}[key]!;
49-
case 'zh':
50-
return {
51-
"connected": "已连接",
52-
"disconnected": "已断开",
53-
"connecting": "正在连接",
54-
"disconnecting": "正在断开",
55-
"all_apps": "所有应用",
56-
}[key]!;
57-
case 'en':
58-
default:
59-
return {
60-
'connected': 'CONNECTED',
61-
'disconnected': 'DISCONNECTED',
62-
'connecting': 'CONNECTING',
63-
'disconnecting': 'DISCONNECTING',
64-
"all_apps": "All Applications",
65-
}[key]!;
66-
}
67-
}
68-
}
69-
701
// style
712
const double elevation0 = 0;
723

lib/main.dart

Lines changed: 29 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,18 @@
11
import 'package:flutter/material.dart';
2+
import 'package:flutter_localizations/flutter_localizations.dart';
3+
import 'package:vpn_client/l10n/app_localizations.dart';
24
import 'package:provider/provider.dart';
35
import 'package:vpn_client/pages/apps/apps_page.dart';
4-
import 'dart:ui' as ui;
56
import 'package:vpn_client/pages/main/main_page.dart';
67
import 'package:vpn_client/pages/servers/servers_page.dart';
78
import 'package:vpn_client/theme_provider.dart';
8-
import 'package:flutter_localizations/flutter_localizations.dart';
9-
import 'package:vpn_client/vpn_state.dart';
10-
import 'package:vpn_client/localization_service.dart';
119

1210
import 'design/colors.dart';
1311
import 'nav_bar.dart';
1412

15-
void main() async {
16-
WidgetsFlutterBinding.ensureInitialized();
17-
18-
Locale userLocale =
19-
ui.PlatformDispatcher.instance.locale; // <-- Get the system locale
20-
await LocalizationService.load(userLocale);
21-
13+
void main() {
2214
runApp(
23-
MultiProvider(
24-
providers: [
25-
ChangeNotifierProvider(create: (_) => ThemeProvider()),
26-
ChangeNotifierProvider(create: (_) => VpnState()),
27-
],
28-
child: const App(),
29-
),
15+
ChangeNotifierProvider(create: (_) => ThemeProvider(), child: const App()),
3016
);
3117
}
3218

@@ -36,52 +22,56 @@ class App extends StatelessWidget {
3622
@override
3723
Widget build(BuildContext context) {
3824
final themeProvider = Provider.of<ThemeProvider>(context);
39-
40-
// If you want to override it manually, do it here (or leave as null to use system):
41-
// final Locale? manualLocale = const Locale('ru'); // ← override example
42-
final Locale? manualLocale = null; // ← use system by default
43-
25+
final Locale? manualLocale = null;
4426
return MaterialApp(
45-
localizationsDelegates: const [
46-
GlobalMaterialLocalizations.delegate,
47-
GlobalWidgetsLocalizations.delegate,
48-
GlobalCupertinoLocalizations.delegate,
49-
],
5027
debugShowCheckedModeBanner: false,
5128
title: 'VPN Client',
5229
theme: lightTheme,
5330
darkTheme: darkTheme,
5431
locale: manualLocale,
55-
localeResolutionCallback: (locale, _) {
32+
localeResolutionCallback: (locale, supportedLocales) {
5633
if (locale == null) return const Locale('en');
57-
58-
// Check for exact match
59-
final supported = ['en', 'ru', 'th', 'zh'];
60-
if (supported.contains(locale.languageCode)) {
61-
return Locale(locale.languageCode);
34+
for (var supportedLocale in supportedLocales) {
35+
if (supportedLocale.languageCode == locale.languageCode &&
36+
(supportedLocale.countryCode == null ||
37+
supportedLocale.countryCode == locale.countryCode)) {
38+
return supportedLocale;
39+
}
40+
}
41+
if (locale.languageCode == 'zh') {
42+
return supportedLocales.contains(const Locale('zh'))
43+
? const Locale('zh')
44+
: const Locale('en');
6245
}
63-
64-
// Fallback to 'en' if not found
6546
return const Locale('en');
6647
},
67-
6848
themeMode: themeProvider.themeMode,
6949
home: const MainScreen(),
50+
localizationsDelegates: const [
51+
AppLocalizations.delegate,
52+
GlobalMaterialLocalizations.delegate,
53+
GlobalWidgetsLocalizations.delegate,
54+
GlobalCupertinoLocalizations.delegate,
55+
],
56+
supportedLocales: const [
57+
Locale('en'),
58+
Locale('ru'),
59+
Locale('th'),
60+
Locale('zh'),
61+
],
7062
);
7163
}
7264
}
7365

7466
class MainScreen extends StatefulWidget {
7567
const MainScreen({super.key});
76-
7768
@override
7869
State<MainScreen> createState() => _MainScreenState();
7970
}
8071

8172
class _MainScreenState extends State<MainScreen> {
8273
int _currentIndex = 2;
8374
late List<Widget> _pages;
84-
8575
@override
8676
void initState() {
8777
super.initState();
@@ -93,13 +83,11 @@ class _MainScreenState extends State<MainScreen> {
9383
const PlaceholderPage(text: 'Settings Page'),
9484
];
9585
}
96-
9786
void _handleNavBarTap(int index) {
9887
setState(() {
9988
_currentIndex = index;
10089
});
10190
}
102-
10391
@override
10492
Widget build(BuildContext context) {
10593
return Scaffold(
@@ -115,7 +103,6 @@ class _MainScreenState extends State<MainScreen> {
115103
class PlaceholderPage extends StatelessWidget {
116104
final String text;
117105
const PlaceholderPage({super.key, required this.text});
118-
119106
@override
120107
Widget build(BuildContext context) {
121108
return Center(child: Text(text));

lib/pages/apps/apps_list.dart

Lines changed: 56 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import 'package:flutter/material.dart';
22
import 'package:shared_preferences/shared_preferences.dart';
3-
import 'package:vpn_client/design/dimensions.dart';
43
import 'apps_list_item.dart';
4+
import 'package:vpn_client/l10n/app_localizations.dart';
55
import 'dart:convert';
6+
import 'package:vpn_client/core/constants/storage_keys.dart'; // Importar as constantes
67

78
class AppsList extends StatefulWidget {
89
final Function(List<Map<String, dynamic>>)? onAppsLoaded;
@@ -17,32 +18,28 @@ class AppsList extends StatefulWidget {
1718
class AppsListState extends State<AppsList> {
1819
List<Map<String, dynamic>> _apps = [];
1920
bool _isLoading = true;
21+
bool _dataLoaded = false; // Flag para controlar o carregamento inicial
2022

2123
@override
2224
void initState() {
2325
super.initState();
2426
if (widget.apps != null && widget.apps!.isNotEmpty) {
2527
_apps = widget.apps!;
2628
_isLoading = false;
27-
if (widget.onAppsLoaded != null) {
28-
widget.onAppsLoaded!(_apps);
29-
}
30-
} else {
31-
_loadApps();
29+
_dataLoaded =
30+
true; // Marcar como carregado se dados iniciais foram fornecidos
31+
// widget.onAppsLoaded é chamado em didUpdateWidget ou após _loadApps
3232
}
3333
}
3434

3535
late String textallapps;
36-
bool _initialized = false;
37-
3836
@override
3937
void didChangeDependencies() {
4038
super.didChangeDependencies();
41-
if (!_initialized) {
42-
final statusText = CustomString(context);
43-
textallapps = statusText.allapp;
39+
if (!_dataLoaded) {
40+
// Carregar apenas se os dados não foram carregados via widget.apps ou anteriormente
41+
textallapps = AppLocalizations.of(context)!.all_apps;
4442
_loadApps();
45-
_initialized = true;
4643
}
4744
}
4845

@@ -53,22 +50,41 @@ class AppsListState extends State<AppsList> {
5350
setState(() {
5451
_apps = widget.apps!;
5552
_isLoading = false;
53+
_dataLoaded = true;
5654
});
5755
_saveSelectedApps();
5856
}
5957
}
6058

6159
Future<void> _loadApps() async {
6260
setState(() {
63-
_isLoading = true;
61+
// Evitar mostrar loading se já estiver carregando ou já carregou
62+
if (!_dataLoaded) _isLoading = true;
6463
});
6564

65+
// Simulação de carregamento
66+
// Em um app real, aqui viria a lógica de buscar dados de uma API ou DB
67+
await Future.delayed(const Duration(milliseconds: 100)); // Simular delay
68+
69+
// Definir textallapps aqui se ainda não foi definido em didChangeDependencies
70+
// Isso é um fallback, idealmente textallapps já está disponível.
71+
// Adicionando verificação de 'mounted' para o BuildContext
72+
if (!mounted) return;
73+
final localizations = AppLocalizations.of(context);
74+
textallapps = localizations?.all_apps ?? "All Applications";
75+
6676
try {
77+
// Se os dados já foram carregados (ex: por uma busca anterior que atualizou widget.apps), não recarregar do zero
78+
if (_dataLoaded && _apps.isNotEmpty) {
79+
setState(() => _isLoading = false);
80+
return;
81+
}
82+
6783
List<Map<String, dynamic>> appsList = [
6884
{
6985
'icon': null,
7086
'image': null,
71-
'text': textallapps,
87+
'text': textallapps, // Usar a string localizada
7288
'isSwitch': true,
7389
'isActive': false,
7490
},
@@ -106,7 +122,7 @@ class AppsListState extends State<AppsList> {
106122
]);
107123

108124
final prefs = await SharedPreferences.getInstance();
109-
final String? savedApps = prefs.getString('selected_apps');
125+
final String? savedApps = prefs.getString(StorageKeys.selectedApps);
110126
if (savedApps != null) {
111127
final List<dynamic> savedAppsList = jsonDecode(savedApps);
112128
for (var savedApp in savedAppsList) {
@@ -122,6 +138,7 @@ class AppsListState extends State<AppsList> {
122138
setState(() {
123139
_apps = appsList;
124140
_isLoading = false;
141+
_dataLoaded = true; // Marcar que os dados foram carregados
125142
});
126143

127144
if (widget.onAppsLoaded != null) {
@@ -130,6 +147,7 @@ class AppsListState extends State<AppsList> {
130147
} catch (e) {
131148
setState(() {
132149
_isLoading = false;
150+
_dataLoaded = true; // Marcar como tentado carregar para evitar loop
133151
});
134152
debugPrint('Error loading apps: $e');
135153
}
@@ -141,7 +159,7 @@ class AppsListState extends State<AppsList> {
141159
_apps
142160
.map((app) => {'text': app['text'], 'isActive': app['isActive']})
143161
.toList();
144-
await prefs.setString('selected_apps', jsonEncode(selectedApps));
162+
await prefs.setString(StorageKeys.selectedApps, jsonEncode(selectedApps));
145163
}
146164

147165
List<Map<String, dynamic>> get apps => _apps;
@@ -150,14 +168,19 @@ class AppsListState extends State<AppsList> {
150168
setState(() {
151169
if (index == 0 && _apps[index]['isSwitch']) {
152170
_apps[0]['isActive'] = !_apps[0]['isActive'];
171+
// Se "Todos os aplicativos" for ativado, desabilitar os outros itens da lista (visual)
172+
// A lógica de 'isEnabled' no AppListItem já cuida disso visualmente.
173+
// Aqui, garantimos que os outros não estejam 'isActive' se "Todos" estiver ativo.
153174
if (_apps[0]['isActive']) {
154175
for (int i = 1; i < _apps.length; i++) {
155-
_apps[i]['isEnabled'] = false;
176+
_apps[i]['isActive'] =
177+
false; // Desmarcar outros se "Todos" for selecionado
156178
}
157179
}
158180
} else {
159181
_apps[index]['isActive'] = !_apps[index]['isActive'];
160-
if (_apps[index]['isActive']) {
182+
// Se um app individual for ativado, "Todos os aplicativos" deve ser desativado
183+
if (_apps[index]['isActive'] && index != 0) {
161184
_apps[0]['isActive'] = false;
162185
}
163186
}
@@ -170,6 +193,18 @@ class AppsListState extends State<AppsList> {
170193

171194
@override
172195
Widget build(BuildContext context) {
196+
// Garante que textallapps seja inicializado se didChangeDependencies não for chamado a tempo
197+
// ou se o widget for reconstruído antes.
198+
textallapps = AppLocalizations.of(context)?.all_apps ?? "All Applications";
199+
200+
// Atualiza o texto do primeiro item se ele ainda não estiver com o texto localizado
201+
// Isso pode acontecer se _loadApps for chamado antes de textallapps ser definido por didChangeDependencies
202+
if (_apps.isNotEmpty &&
203+
_apps[0]['text'] != textallapps &&
204+
_apps[0]['isSwitch'] == true) {
205+
_apps[0]['text'] = textallapps;
206+
}
207+
173208
return Container(
174209
width: MediaQuery.of(context).size.width,
175210
height: MediaQuery.of(context).size.height,
@@ -191,7 +226,9 @@ class AppsListState extends State<AppsList> {
191226
text: _apps[index]['text'],
192227
isSwitch: _apps[index]['isSwitch'],
193228
isActive: _apps[index]['isActive'],
194-
isEnabled: index == 0 || !_apps[0]['isActive'],
229+
isEnabled:
230+
index == 0 ||
231+
!_apps[0]['isActive'], // Item é habilitado se for o switch "Todos" ou se "Todos" não estiver ativo
195232
onTap: () => _onItemTapped(index),
196233
);
197234
}),

0 commit comments

Comments
 (0)