main
dart 503 lines 16.2 KB
Raw
1 import 'package:flutter/material.dart';
2 import 'package:hugeicons/hugeicons.dart';
3 import 'package:localmind/core/theme/colors.dart';
4 import 'package:localmind/l10n/app_localizations.dart';
5
6 /// A modern, high-end RAM Warning Dialog component.
7 class RamWarningDialog extends StatelessWidget {
8 const RamWarningDialog({
9 super.key,
10 this.availableRam,
11 this.requiredRam,
12 this.message,
13 this.proceedLabel,
14 this.cancelLabel,
15 required this.onProceed,
16 required this.onCancel,
17 });
18
19 final String? availableRam;
20 final String? requiredRam;
21 final String? message;
22 final String? proceedLabel;
23 final String? cancelLabel;
24 final VoidCallback onProceed;
25 final VoidCallback onCancel;
26
27 static Future<bool> show({
28 required BuildContext context,
29 String? availableRam,
30 String? requiredRam,
31 String? message,
32 String? proceedLabel,
33 String? cancelLabel,
34 }) async {
35 final result = await showDialog<bool>(
36 context: context,
37 barrierDismissible: true,
38 builder: (ctx) => RamWarningDialog(
39 availableRam: availableRam,
40 requiredRam: requiredRam,
41 message: message,
42 proceedLabel: proceedLabel,
43 cancelLabel: cancelLabel,
44 onProceed: () => Navigator.of(ctx).pop(true),
45 onCancel: () => Navigator.of(ctx).pop(false),
46 ),
47 );
48 return result ?? false;
49 }
50
51 @override
52 Widget build(BuildContext context) {
53 final l10n = AppLocalizations.of(context)!;
54 final theme = Theme.of(context);
55 final isDark = theme.brightness == Brightness.dark;
56
57 const warningColor = AppColors.warning;
58
59 // Infer RAM specs from message if available parameters were omitted
60 String? availRam = availableRam;
61 String? reqRam = requiredRam;
62
63 if ((availRam == null || reqRam == null) && message != null) {
64 final matches =
65 RegExp(r'(\d+(?:\.\d+)?\s*(?:GB|MB))', caseSensitive: false)
66 .allMatches(message!)
67 .map((m) => m.group(0))
68 .whereType<String>()
69 .toList();
70 if (matches.length >= 2) {
71 availRam ??= matches[0];
72 reqRam ??= matches[1];
73 } else if (matches.length == 1) {
74 reqRam ??= matches[0];
75 }
76 }
77
78 final bodyText =
79 message ??
80 (availRam != null && reqRam != null
81 ? l10n.ram_warning_body_load(availRam, reqRam)
82 : l10n.ram_warning);
83
84 return Dialog(
85 shape: RoundedRectangleBorder(
86 borderRadius: BorderRadius.circular(24),
87 side: BorderSide(
88 color: isDark
89 ? AppColors.darkBorder.withValues(alpha: 0.6)
90 : AppColors.lightBorder,
91 width: 1,
92 ),
93 ),
94 backgroundColor: isDark ? AppColors.darkSurface : AppColors.lightSurface,
95 elevation: 16,
96 shadowColor: Colors.black.withValues(alpha: 0.3),
97 child: Container(
98 constraints: const BoxConstraints(maxWidth: 380),
99 padding: const EdgeInsets.all(24),
100 child: Column(
101 mainAxisSize: MainAxisSize.min,
102 crossAxisAlignment: CrossAxisAlignment.center,
103 children: [
104 // Glowing Warning Icon Badge
105 Container(
106 width: 60,
107 height: 60,
108 decoration: BoxDecoration(
109 color: warningColor.withValues(alpha: isDark ? 0.16 : 0.10),
110 shape: BoxShape.circle,
111 border: Border.all(
112 color: warningColor.withValues(alpha: 0.3),
113 width: 1.5,
114 ),
115 boxShadow: [
116 BoxShadow(
117 color: warningColor.withValues(alpha: 0.15),
118 blurRadius: 16,
119 spreadRadius: 2,
120 ),
121 ],
122 ),
123 child: Center(
124 child: HugeIcon(
125 icon: HugeIcons.strokeRoundedAlertCircle,
126 color: warningColor,
127 size: 30,
128 ),
129 ),
130 ),
131 const SizedBox(height: 16),
132
133 // Dialog Title
134 Text(
135 l10n.ram_warning,
136 style: TextStyle(
137 fontSize: 20,
138 fontWeight: FontWeight.w700,
139 color: isDark
140 ? AppColors.darkPrimaryText
141 : AppColors.lightPrimaryText,
142 letterSpacing: -0.3,
143 ),
144 textAlign: TextAlign.center,
145 ),
146 const SizedBox(height: 16),
147
148 // Visual RAM Stats Box (if available)
149 if (availRam != null && reqRam != null) ...[
150 Container(
151 padding: const EdgeInsets.symmetric(
152 horizontal: 16,
153 vertical: 14,
154 ),
155 decoration: BoxDecoration(
156 color: isDark
157 ? AppColors.darkBackground
158 : warningColor.withValues(alpha: 0.06),
159 borderRadius: BorderRadius.circular(16),
160 border: Border.all(
161 color: warningColor.withValues(alpha: 0.2),
162 ),
163 ),
164 child: Row(
165 children: [
166 Expanded(
167 child: Column(
168 children: [
169 Text(
170 "AVAILABLE RAM",
171 style: TextStyle(
172 fontSize: 10,
173 fontWeight: FontWeight.w700,
174 letterSpacing: 0.5,
175 color: isDark
176 ? AppColors.darkMutedText
177 : AppColors.lightMutedText,
178 ),
179 ),
180 const SizedBox(height: 4),
181 Text(
182 availRam,
183 style: const TextStyle(
184 fontSize: 17,
185 fontWeight: FontWeight.w800,
186 color: AppColors.error,
187 ),
188 ),
189 ],
190 ),
191 ),
192 Container(
193 height: 32,
194 width: 1,
195 color: isDark
196 ? AppColors.darkBorder
197 : warningColor.withValues(alpha: 0.25),
198 ),
199 Expanded(
200 child: Column(
201 children: [
202 Text(
203 "RECOMMENDED",
204 style: TextStyle(
205 fontSize: 10,
206 fontWeight: FontWeight.w700,
207 letterSpacing: 0.5,
208 color: isDark
209 ? AppColors.darkMutedText
210 : AppColors.lightMutedText,
211 ),
212 ),
213 const SizedBox(height: 4),
214 Text(
215 reqRam,
216 style: const TextStyle(
217 fontSize: 17,
218 fontWeight: FontWeight.w800,
219 color: warningColor,
220 ),
221 ),
222 ],
223 ),
224 ),
225 ],
226 ),
227 ),
228 const SizedBox(height: 16),
229 ],
230
231 // Body Warning Description
232 Text(
233 bodyText,
234 style: TextStyle(
235 fontSize: 13.5,
236 height: 1.45,
237 color: isDark
238 ? AppColors.darkMutedText
239 : AppColors.lightMutedText,
240 ),
241 textAlign: TextAlign.center,
242 ),
243 const SizedBox(height: 24),
244
245 // Actions Buttons
246 Row(
247 children: [
248 Expanded(
249 child: SizedBox(
250 height: 46,
251 child: OutlinedButton(
252 onPressed: onCancel,
253 style: OutlinedButton.styleFrom(
254 side: BorderSide(
255 color: isDark
256 ? AppColors.darkBorder
257 : AppColors.lightBorder,
258 ),
259 shape: RoundedRectangleBorder(
260 borderRadius: BorderRadius.circular(12),
261 ),
262 foregroundColor: isDark
263 ? AppColors.darkPrimaryText
264 : AppColors.lightPrimaryText,
265 ),
266 child: Text(
267 cancelLabel ?? l10n.cancel,
268 style: const TextStyle(
269 fontWeight: FontWeight.w600,
270 fontSize: 14,
271 ),
272 ),
273 ),
274 ),
275 ),
276 const SizedBox(width: 12),
277 Expanded(
278 child: SizedBox(
279 height: 46,
280 child: FilledButton(
281 onPressed: onProceed,
282 style: FilledButton.styleFrom(
283 shape: RoundedRectangleBorder(
284 borderRadius: BorderRadius.circular(12),
285 ),
286 elevation: 0,
287 ),
288 child: FittedBox(
289 child: Text(
290 proceedLabel ?? l10n.proceed_anyway,
291 style: const TextStyle(
292 fontWeight: FontWeight.w700,
293 fontSize: 14,
294 ),
295 ),
296 ),
297 ),
298 ),
299 ),
300 ],
301 ),
302 ],
303 ),
304 ),
305 );
306 }
307 }
308
309 /// A modern, styled confirmation dialog for model operations (e.g. Delete Model).
310 class ModelConfirmDialog extends StatelessWidget {
311 const ModelConfirmDialog({
312 super.key,
313 required this.title,
314 required this.message,
315 required this.confirmLabel,
316 required this.cancelLabel,
317 this.isDestructive = true,
318 this.icon,
319 required this.onConfirm,
320 required this.onCancel,
321 });
322
323 final String title;
324 final String message;
325 final String confirmLabel;
326 final String cancelLabel;
327 final bool isDestructive;
328 final dynamic icon;
329 final VoidCallback onConfirm;
330 final VoidCallback onCancel;
331
332 static Future<bool> show({
333 required BuildContext context,
334 required String title,
335 required String message,
336 required String confirmLabel,
337 required String cancelLabel,
338 bool isDestructive = true,
339 dynamic icon,
340 }) async {
341 final result = await showDialog<bool>(
342 context: context,
343 barrierDismissible: true,
344 builder: (ctx) => ModelConfirmDialog(
345 title: title,
346 message: message,
347 confirmLabel: confirmLabel,
348 cancelLabel: cancelLabel,
349 isDestructive: isDestructive,
350 icon: icon,
351 onConfirm: () => Navigator.of(ctx).pop(true),
352 onCancel: () => Navigator.of(ctx).pop(false),
353 ),
354 );
355 return result ?? false;
356 }
357
358 @override
359 Widget build(BuildContext context) {
360 final theme = Theme.of(context);
361 final isDark = theme.brightness == Brightness.dark;
362
363 final accentColor = isDestructive
364 ? AppColors.error
365 : theme.colorScheme.primary;
366 final dialogIcon =
367 icon ??
368 (isDestructive
369 ? HugeIcons.strokeRoundedDelete02
370 : HugeIcons.strokeRoundedAlertCircle);
371
372 return Dialog(
373 shape: RoundedRectangleBorder(
374 borderRadius: BorderRadius.circular(24),
375 side: BorderSide(
376 color: isDark
377 ? AppColors.darkBorder.withValues(alpha: 0.6)
378 : AppColors.lightBorder,
379 width: 1,
380 ),
381 ),
382 backgroundColor: isDark ? AppColors.darkSurface : AppColors.lightSurface,
383 elevation: 16,
384 shadowColor: Colors.black.withValues(alpha: 0.3),
385 child: Container(
386 constraints: const BoxConstraints(maxWidth: 380),
387 padding: const EdgeInsets.all(24),
388 child: Column(
389 mainAxisSize: MainAxisSize.min,
390 crossAxisAlignment: CrossAxisAlignment.center,
391 children: [
392 // Icon Badge Header
393 Container(
394 width: 56,
395 height: 56,
396 decoration: BoxDecoration(
397 color: accentColor.withValues(alpha: isDark ? 0.16 : 0.10),
398 shape: BoxShape.circle,
399 border: Border.all(
400 color: accentColor.withValues(alpha: 0.3),
401 width: 1.5,
402 ),
403 ),
404 child: Center(
405 child: HugeIcon(icon: dialogIcon, color: accentColor, size: 28),
406 ),
407 ),
408 const SizedBox(height: 16),
409
410 // Title
411 Text(
412 title,
413 style: TextStyle(
414 fontSize: 19,
415 fontWeight: FontWeight.w700,
416 color: isDark
417 ? AppColors.darkPrimaryText
418 : AppColors.lightPrimaryText,
419 letterSpacing: -0.3,
420 ),
421 textAlign: TextAlign.center,
422 ),
423 const SizedBox(height: 12),
424
425 // Message
426 Text(
427 message,
428 style: TextStyle(
429 fontSize: 13.5,
430 height: 1.45,
431 color: isDark
432 ? AppColors.darkMutedText
433 : AppColors.lightMutedText,
434 ),
435 textAlign: TextAlign.center,
436 ),
437 const SizedBox(height: 24),
438
439 // Buttons
440 Row(
441 children: [
442 Expanded(
443 child: SizedBox(
444 height: 46,
445 child: OutlinedButton(
446 onPressed: onCancel,
447 style: OutlinedButton.styleFrom(
448 side: BorderSide(
449 color: isDark
450 ? AppColors.darkBorder
451 : AppColors.lightBorder,
452 ),
453 shape: RoundedRectangleBorder(
454 borderRadius: BorderRadius.circular(12),
455 ),
456 foregroundColor: isDark
457 ? AppColors.darkPrimaryText
458 : AppColors.lightPrimaryText,
459 ),
460 child: Text(
461 cancelLabel,
462 style: const TextStyle(
463 fontWeight: FontWeight.w600,
464 fontSize: 14,
465 ),
466 ),
467 ),
468 ),
469 ),
470 const SizedBox(width: 12),
471 Expanded(
472 child: SizedBox(
473 height: 46,
474 child: FilledButton(
475 onPressed: onConfirm,
476 style: FilledButton.styleFrom(
477 backgroundColor: accentColor,
478 foregroundColor: isDestructive
479 ? Colors.white
480 : Colors.black,
481 shape: RoundedRectangleBorder(
482 borderRadius: BorderRadius.circular(12),
483 ),
484 elevation: 0,
485 ),
486 child: Text(
487 confirmLabel,
488 style: const TextStyle(
489 fontWeight: FontWeight.w700,
490 fontSize: 14,
491 ),
492 ),
493 ),
494 ),
495 ),
496 ],
497 ),
498 ],
499 ),
500 ),
501 );
502 }
503 }