main
dart 551 lines 21 KB
Raw
1 import 'package:flutter/material.dart';
2 import 'package:flutter_riverpod/flutter_riverpod.dart';
3 import 'package:go_router/go_router.dart';
4 import 'package:hugeicons/hugeicons.dart';
5 import 'package:shadcn_ui/shadcn_ui.dart';
6 import 'package:cue/cue.dart';
7
8 import '../../../core/providers/app_providers.dart';
9 import '../../../core/routes/app_routes.dart';
10 import '../../../l10n/app_localizations.dart';
11
12 class _LanguageOption {
13 final String code;
14 final String nativeName;
15 final String englishName;
16 final String flag;
17 final String flagAsset;
18 final String countryCode;
19 final String shortText;
20 final List<Color> gradient;
21
22 const _LanguageOption({
23 required this.code,
24 required this.nativeName,
25 required this.englishName,
26 required this.flag,
27 required this.flagAsset,
28 required this.countryCode,
29 required this.shortText,
30 required this.gradient,
31 });
32 }
33
34 const _languages = [
35 _LanguageOption(
36 code: 'en',
37 nativeName: 'English',
38 englishName: 'English',
39 flag: '🇺🇸',
40 flagAsset: 'assets/images/flag_us.png',
41 countryCode: 'US',
42 shortText: 'En',
43 gradient: [Color(0xFF6366F1), Color(0xFF4F46E5)],
44 ),
45 _LanguageOption(
46 code: 'it',
47 nativeName: 'Italiano',
48 englishName: 'Italian',
49 flag: '🇮🇹',
50 flagAsset: 'assets/images/flag_it.png',
51 countryCode: 'IT',
52 shortText: 'It',
53 gradient: [Color(0xFF71717A), Color(0xFF52525B)],
54 ),
55 _LanguageOption(
56 code: 'es',
57 nativeName: 'Español',
58 englishName: 'Spanish',
59 flag: '🇪🇸',
60 flagAsset: 'assets/images/flag_es.png',
61 countryCode: 'ES',
62 shortText: 'Es',
63 gradient: [Color(0xFFF59E0B), Color(0xFFD97706)],
64 ),
65 _LanguageOption(
66 code: 'zh',
67 nativeName: '中文',
68 englishName: 'Chinese',
69 flag: '🇨🇳',
70 flagAsset: 'assets/images/flag_cn.png',
71 countryCode: 'CN',
72 shortText: 'Zh',
73 gradient: [Color(0xFFEF4444), Color(0xFFDC2626)],
74 ),
75 _LanguageOption(
76 code: 'zh_TW',
77 nativeName: '繁體中文',
78 englishName: 'Traditional Chinese',
79 flag: '🇹🇼',
80 flagAsset: 'assets/images/flag_tw.png',
81 countryCode: 'TW',
82 shortText: 'Zh-TW',
83 gradient: [Color(0xFF2563EB), Color(0xFF1D4ED8)],
84 ),
85 _LanguageOption(
86 code: 'ar',
87 nativeName: 'العربية',
88 englishName: 'Arabic',
89 flag: '🇸🇦',
90 flagAsset: 'assets/images/flag_sa.png',
91 countryCode: 'SA',
92 shortText: 'Ar',
93 gradient: [Color(0xFF10B981), Color(0xFF059669)],
94 ),
95 _LanguageOption(
96 code: 'bn',
97 nativeName: 'বাংলা',
98 englishName: 'Bengali',
99 flag: '🇧🇩',
100 flagAsset: 'assets/images/flag_bd.png',
101 countryCode: 'BD',
102 shortText: 'Bn',
103 gradient: [Color(0xFF06B6D4), Color(0xFF0891B2)],
104 ),
105 _LanguageOption(
106 code: 'hi',
107 nativeName: 'हिन्दी',
108 englishName: 'Hindi',
109 flag: '🇮🇳',
110 flagAsset: 'assets/images/flag_in.png',
111 countryCode: 'IN',
112 shortText: 'Hi',
113 gradient: [Color(0xFFEC4899), Color(0xFFDB2777)],
114 ),
115 _LanguageOption(
116 code: 'ja',
117 nativeName: '日本語',
118 englishName: 'Japanese',
119 flag: '🇯🇵',
120 flagAsset: 'assets/images/flag_jp.png',
121 countryCode: 'JP',
122 shortText: 'Ja',
123 gradient: [Color(0xFFF43F5E), Color(0xFFBE123C)],
124 ),
125 _LanguageOption(
126 code: 'ru',
127 nativeName: 'Русский',
128 englishName: 'Russian',
129 flag: '🇷🇺',
130 flagAsset: 'assets/images/flag_ru.png',
131 countryCode: 'RU',
132 shortText: 'Ru',
133 gradient: [Color(0xFF3B82F6), Color(0xFF1D4ED8)],
134 ),
135 _LanguageOption(
136 code: 'tr',
137 nativeName: 'Türkçe',
138 englishName: 'Turkish',
139 flag: '🇹🇷',
140 flagAsset: 'assets/images/flag_tr.png',
141 countryCode: 'TR',
142 shortText: 'Tr',
143 gradient: [Color(0xFFE11D48), Color(0xFFBE123C)],
144 ),
145 _LanguageOption(
146 code: 'fr',
147 nativeName: 'Français',
148 englishName: 'French',
149 flag: '🇫🇷',
150 flagAsset: 'assets/images/flag_fr.png',
151 countryCode: 'FR',
152 shortText: 'Fr',
153 gradient: [Color(0xFF2563EB), Color(0xFF1D4ED8)],
154 ),
155 _LanguageOption(
156 code: 'de',
157 nativeName: 'Deutsch',
158 englishName: 'German',
159 flag: '🇩🇪',
160 flagAsset: 'assets/images/flag_de.png',
161 countryCode: 'DE',
162 shortText: 'De',
163 gradient: [Color(0xFFF59E0B), Color(0xFFEF4444)],
164 ),
165 _LanguageOption(
166 code: 'pt',
167 nativeName: 'Português',
168 englishName: 'Portuguese',
169 flag: '🇧🇷',
170 flagAsset: 'assets/images/flag_br.png',
171 countryCode: 'BR',
172 shortText: 'Pt',
173 gradient: [Color(0xFF10B981), Color(0xFF059669)],
174 ),
175 _LanguageOption(
176 code: 'ko',
177 nativeName: '한국어',
178 englishName: 'Korean',
179 flag: '🇰🇷',
180 flagAsset: 'assets/images/flag_kr.png',
181 countryCode: 'KR',
182 shortText: 'Ko',
183 gradient: [Color(0xFF3B82F6), Color(0xFF1D4ED8)],
184 ),
185 ];
186
187 class OnboardingLanguageScreen extends ConsumerStatefulWidget {
188 const OnboardingLanguageScreen({super.key});
189
190 @override
191 ConsumerState<OnboardingLanguageScreen> createState() =>
192 _OnboardingLanguageScreenState();
193 }
194
195 class _OnboardingLanguageScreenState
196 extends ConsumerState<OnboardingLanguageScreen> {
197 String? _selectedCode;
198
199 @override
200 void initState() {
201 super.initState();
202 // Read the current settings locale code
203 WidgetsBinding.instance.addPostFrameCallback((_) {
204 final settings = ref.read(settingsProvider);
205 setState(() {
206 _selectedCode = settings.localeCode ?? 'en';
207 });
208 });
209 }
210
211 @override
212 Widget build(BuildContext context) {
213 final l10n = AppLocalizations.of(context)!;
214 final theme = Theme.of(context);
215
216 return Scaffold(
217 appBar: AppBar(
218 title: Row(
219 children: [
220 Padding(
221 padding: const EdgeInsets.all(8),
222 child: Image.asset(
223 'assets/images/logo.webp',
224 width: 24,
225 height: 24,
226 ),
227 ),
228 Text(
229 l10n.onboarding_localmind,
230 style: const TextStyle(letterSpacing: 2, fontSize: 14),
231 ),
232 ],
233 ),
234 centerTitle: false,
235 actions: [
236 TextButton.icon(
237 onPressed: () => _skipOnboarding(),
238 label: Text(l10n.skip),
239 icon: HugeIcon(
240 icon: HugeIcons.strokeRoundedNext,
241 color: theme.colorScheme.primary,
242 size: 20,
243 ),
244 ),
245 ],
246 ),
247 body: Cue.onMount(
248 motion: .smooth(),
249 child: Column(
250 crossAxisAlignment: CrossAxisAlignment.stretch,
251 children: [
252 Expanded(
253 child: Scrollbar(
254 thickness: 2,
255 radius: const Radius.circular(4),
256 child: SingleChildScrollView(
257 padding: const EdgeInsets.symmetric(horizontal: 24.0),
258 child: Column(
259 crossAxisAlignment: CrossAxisAlignment.stretch,
260 children: [
261 const SizedBox(height: 16),
262 Actor(
263 acts: [.fadeIn(), .slideY(from: 0.08)],
264 child: Column(
265 crossAxisAlignment: CrossAxisAlignment.stretch,
266 children: [
267 Text(
268 l10n.onboarding_welcome,
269 textAlign: TextAlign.center,
270 style: theme.textTheme.headlineLarge?.copyWith(
271 fontWeight: FontWeight.w800,
272 height: 1.1,
273 ),
274 ),
275 const SizedBox(height: 10),
276 Text(
277 l10n.onboarding_choose_language,
278 textAlign: TextAlign.center,
279 style: theme.textTheme.titleMedium?.copyWith(
280 fontWeight: FontWeight.w600,
281 color: theme.colorScheme.primary,
282 ),
283 ),
284 const SizedBox(height: 6),
285 Text(
286 l10n.onboarding_choose_language_desc,
287 textAlign: TextAlign.center,
288 style: theme.textTheme.bodySmall?.copyWith(
289 color: theme.colorScheme.onSurface.withValues(
290 alpha: 0.7,
291 ),
292 height: 1.4,
293 ),
294 ),
295 ],
296 ),
297 ),
298 const SizedBox(height: 20),
299 Actor(
300 delay: 60.ms,
301 acts: [.fadeIn(), .slideY(from: 0.08)],
302 child: GridView.builder(
303 shrinkWrap: true,
304 physics: const NeverScrollableScrollPhysics(),
305 gridDelegate:
306 const SliverGridDelegateWithFixedCrossAxisCount(
307 crossAxisCount: 3,
308 mainAxisSpacing: 8,
309 crossAxisSpacing: 8,
310 mainAxisExtent: 108,
311 ),
312 itemCount: _languages.length,
313 itemBuilder: (context, index) {
314 final lang = _languages[index];
315 final isSelected = _selectedCode == lang.code;
316
317 return GestureDetector(
318 onTap: () {
319 setState(() {
320 _selectedCode = lang.code;
321 });
322 // Dynamically apply settings localization update
323 ref
324 .read(settingsProvider.notifier)
325 .setLocaleCode(lang.code);
326 },
327 child: AnimatedContainer(
328 duration: const Duration(milliseconds: 200),
329 padding: const EdgeInsets.all(10),
330 decoration: BoxDecoration(
331 color: theme.colorScheme.surface,
332 borderRadius: BorderRadius.circular(16),
333 border: Border.all(
334 color: isSelected
335 ? theme.colorScheme.primary
336 : theme.colorScheme.outline.withValues(
337 alpha: 0.15,
338 ),
339 width: isSelected ? 2 : 1,
340 ),
341 boxShadow: isSelected
342 ? [
343 BoxShadow(
344 color: theme.colorScheme.primary
345 .withValues(alpha: 0.1),
346 blurRadius: 8,
347 offset: const Offset(0, 2),
348 ),
349 ]
350 : null,
351 ),
352 child: Column(
353 crossAxisAlignment: CrossAxisAlignment.start,
354 mainAxisAlignment:
355 MainAxisAlignment.spaceBetween,
356 children: [
357 Row(
358 mainAxisAlignment:
359 MainAxisAlignment.spaceBetween,
360 children: [
361 Container(
362 width: 34,
363 height: 23,
364 decoration: BoxDecoration(
365 color: Colors.white,
366 borderRadius: BorderRadius.circular(
367 5,
368 ),
369 border: Border.all(
370 color: theme.colorScheme.outline
371 .withValues(alpha: 0.15),
372 width: 1,
373 ),
374 boxShadow: [
375 BoxShadow(
376 color: Colors.black.withValues(
377 alpha: 0.05,
378 ),
379 blurRadius: 4,
380 offset: const Offset(0, 2),
381 ),
382 ],
383 ),
384 alignment: Alignment.center,
385 child: ClipRRect(
386 borderRadius: BorderRadius.circular(
387 4,
388 ),
389 child: Image.asset(
390 lang.flagAsset,
391 width: 34,
392 height: 23,
393 fit: BoxFit.cover,
394 errorBuilder:
395 (context, error, stackTrace) {
396 return Text(
397 lang.flag,
398 style: const TextStyle(
399 fontSize: 14,
400 ),
401 );
402 },
403 ),
404 ),
405 ),
406 AnimatedContainer(
407 duration: const Duration(
408 milliseconds: 200,
409 ),
410 width: 18,
411 height: 18,
412 decoration: BoxDecoration(
413 shape: BoxShape.circle,
414 color: isSelected
415 ? theme.colorScheme.primary
416 : Colors.transparent,
417 border: Border.all(
418 color: isSelected
419 ? theme.colorScheme.primary
420 : theme.colorScheme.outline
421 .withValues(alpha: 0.3),
422 width: 2,
423 ),
424 ),
425 child: isSelected
426 ? const HugeIcon(
427 icon: HugeIcons
428 .strokeRoundedTick01,
429 color: Colors.white,
430 size: 11,
431 )
432 : null,
433 ),
434 ],
435 ),
436 Column(
437 crossAxisAlignment:
438 CrossAxisAlignment.start,
439 mainAxisSize: MainAxisSize.min,
440 children: [
441 Text(
442 lang.nativeName,
443 maxLines: 1,
444 overflow: TextOverflow.ellipsis,
445 style: theme.textTheme.titleSmall
446 ?.copyWith(
447 fontWeight: FontWeight.bold,
448 fontSize: 13,
449 ),
450 ),
451 const SizedBox(height: 2),
452 Text(
453 '${lang.englishName} • ${lang.shortText.toUpperCase()}',
454 maxLines: 2,
455 overflow: TextOverflow.ellipsis,
456 style: theme.textTheme.labelSmall
457 ?.copyWith(
458 color: theme
459 .colorScheme
460 .onSurface
461 .withValues(alpha: 0.5),
462 fontSize: 10,
463 height: 1.15,
464 ),
465 ),
466 ],
467 ),
468 ],
469 ),
470 ),
471 );
472 },
473 ),
474 ),
475 const SizedBox(height: 32),
476 ],
477 ),
478 ),
479 ),
480 ),
481 SizedBox(height: 8),
482 Actor(
483 delay: 120.ms,
484 acts: [.fadeIn(), .slideY(from: 0.08)],
485 child: Padding(
486 padding: const EdgeInsets.symmetric(horizontal: 24.0),
487 child: Column(
488 children: [
489 Row(
490 mainAxisAlignment: MainAxisAlignment.center,
491 children: [
492 Container(
493 width: 8,
494 height: 8,
495 decoration: BoxDecoration(
496 color: theme.colorScheme.primary,
497 shape: BoxShape.circle,
498 ),
499 ),
500 const SizedBox(width: 8),
501 Text(
502 l10n.ready_continue,
503 style: theme.textTheme.labelSmall?.copyWith(
504 color: theme.colorScheme.onSurface.withValues(
505 alpha: 0.5,
506 ),
507 letterSpacing: 2,
508 fontWeight: FontWeight.w600,
509 ),
510 ),
511 ],
512 ),
513 const SizedBox(height: 16),
514 ShadButton(
515 width: double.infinity,
516 onPressed: () {
517 context.push(AppRoutes.onboardingServerType);
518 },
519 child: Text(
520 l10n.continue_action,
521 style: const TextStyle(
522 fontSize: 16,
523 fontWeight: FontWeight.w600,
524 ),
525 ),
526 ),
527 SizedBox(height: MediaQuery.of(context).padding.bottom),
528 ],
529 ),
530 ),
531 ),
532 ],
533 ),
534 ),
535 );
536 }
537
538 Future<void> _completeOnboarding() async {
539 final settings = ref.read(settingsProvider);
540 await ref
541 .read(settingsProvider.notifier)
542 .updateSettings(settings.copyWith(hasCompletedOnboarding: true));
543 if (mounted) {
544 context.go(AppRoutes.home);
545 }
546 }
547
548 void _skipOnboarding() {
549 _completeOnboarding();
550 }
551 }