Saba-dart/lib/screens/settings_screen.dart
2026-04-13 23:41:27 +03:30

804 lines
31 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../main.dart';
import '../utils/app_lock_service.dart';
import '../utils/contact_helper.dart';
import '../utils/secure_messaging_service.dart';
import '../utils/app_theme.dart';
import 'splash_screen.dart';
enum _AppLockDialogMode { enable, change, disable }
String? _validateAppLockPasscode(String value) {
final normalized = value.trim();
if (normalized.isEmpty) {
return 'رمز را وارد کنید.';
}
if (!RegExp(r'^\d{4,8}$').hasMatch(normalized)) {
return 'رمز باید ۴ تا ۸ رقم باشد.';
}
return null;
}
class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key});
@override
State<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
bool _isResetting = false;
static const _platform =
MethodChannel('com.example.saba_secure_sms/sms_role');
bool _isCurrentlyDefault = false;
bool _isAppLockEnabled = false;
bool _isAppLockLoading = true;
@override
void initState() {
super.initState();
_checkDefaultStatus();
_loadAppLockStatus();
}
Future<void> _checkDefaultStatus() async {
try {
final bool isDefault = await _platform.invokeMethod('isDefaultSmsApp');
if (mounted) setState(() => _isCurrentlyDefault = isDefault);
} catch (_) {}
}
Future<void> _changeDefaultApp() async {
try {
if (_isCurrentlyDefault) {
// If already default, open settings to allow unsetting
await _platform.invokeMethod('openDefaultAppsSettings');
} else {
// If not default, request it
await _platform.invokeMethod('requestDefaultSmsApp');
}
// Re-check after returning
_checkDefaultStatus();
} catch (_) {}
}
Future<void> _confirmReset() async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: const Text('ریست کامل برنامه'),
content: const Text(
'با این کار تمام کلیدها، گروه‌ها، پیام‌های ذخیره‌شده، کش بازگشایی و تنظیمات امنیتی حذف می‌شوند و برنامه مثل نصب تازه می‌شود. ادامه می‌دهید؟',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('لغو'),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
onPressed: () => Navigator.pop(ctx, true),
child: const Text('ریست کامل'),
),
],
),
);
if (confirmed != true || !mounted) return;
setState(() => _isResetting = true);
await SecureMessagingService.instance.resetForFreshInstall();
await AppLockService.instance.clear();
ContactHelper.clearCache();
if (!mounted) return;
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (_) => const SplashScreen()),
(route) => false,
);
}
Future<void> _loadAppLockStatus() async {
await AppLockService.instance.init();
if (!mounted) return;
setState(() {
_isAppLockEnabled = AppLockService.instance.isEnabled;
_isAppLockLoading = false;
});
}
Future<void> _showEnableAppLockDialog() async {
final enabled = await showDialog<bool>(
context: context,
builder: (_) => const _AppLockDialog(mode: _AppLockDialogMode.enable),
);
if (enabled == true) {
await _loadAppLockStatus();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('رمز برنامه با موفقیت فعال شد.'),
),
);
}
}
Future<void> _showChangeAppLockDialog() async {
final changed = await showDialog<bool>(
context: context,
builder: (_) => const _AppLockDialog(mode: _AppLockDialogMode.change),
);
if (changed == true && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('رمز برنامه با موفقیت تغییر کرد.'),
),
);
}
}
Future<void> _showDisableAppLockDialog() async {
final disabled = await showDialog<bool>(
context: context,
builder: (_) => const _AppLockDialog(mode: _AppLockDialogMode.disable),
);
if (disabled == true) {
await _loadAppLockStatus();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('رمز برنامه غیرفعال شد.'),
),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.darkBg,
appBar: AppBar(
title: const Text('تنظیمات'),
backgroundColor: Colors.transparent,
elevation: 0,
),
body: Stack(
children: [
Positioned.fill(child: CustomPaint(painter: MeshBackgroundPainter())),
SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
_buildSettingSection(
icon: Icons.sms_rounded,
iconColor: Colors.blueAccent,
title: 'برنامه پیش‌فرض پیامک',
content: _isCurrentlyDefault
? 'صبا در حال حاضر برنامه پیش‌فرض پیامک گوشی شماست.'
: 'صبا برنامه پیش‌فرض نیست. برای استفاده از تمام امکانات امنیتی، صبا را به عنوان گزینه پیش‌فرض انتخاب کنید.',
action: SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: _changeDefaultApp,
style: OutlinedButton.styleFrom(
foregroundColor: _isCurrentlyDefault
? Colors.white70
: Colors.blueAccent,
side: BorderSide(
color: _isCurrentlyDefault
? Colors.white24
: Colors.blueAccent),
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14)),
),
icon: Icon(_isCurrentlyDefault
? Icons.settings_applications
: Icons.check_circle_outline),
label: Text(_isCurrentlyDefault
? 'تغییر در تنظیمات سیستمی'
: 'انتخاب به عنوان پیش‌فرض'),
),
),
),
const SizedBox(height: 16),
_buildSettingSection(
icon: Icons.palette_rounded,
iconColor: Colors.purpleAccent,
title: 'رنگ‌بندی برنامه',
content:
'رنگ مورد علاقه‌ی خود را برای محیط برنامه انتخاب کنید:',
action: SizedBox(
height: 55,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
_buildColorItem(const Color(0xFF7000FF), 'بنفش'),
_buildColorItem(const Color(0xFF00D2FF), 'آبی'),
_buildColorItem(const Color(0xFF00FFD1), 'فیروزه‌ای'),
_buildColorItem(const Color(0xFF4CAF50), 'سبز'),
_buildColorItem(const Color(0xFFFF9800), 'نارنجی'),
_buildColorItem(const Color(0xFFE91E63), 'صورتی'),
_buildColorItem(const Color(0xFFF44336), 'قرمز'),
],
),
),
),
const SizedBox(height: 16),
_buildSettingSection(
icon: Icons.lock_outline_rounded,
iconColor: Colors.amberAccent,
title: 'قفل برنامه',
content: _isAppLockEnabled
? 'رمز برنامه فعال است. از این به بعد، هر بار ورود یا بازگشت به اپ نیاز به وارد کردن رمز خواهد داشت.'
: 'اگر این گزینه را فعال کنید، بدون وارد کردن رمز هیچ‌کس نمی‌تواند وارد برنامه شود.',
action: _isAppLockLoading
? const Center(child: CircularProgressIndicator())
: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: _isAppLockEnabled
? Colors.greenAccent
.withValues(alpha: 0.25)
: Colors.white12,
),
),
child: Row(
children: [
Icon(
_isAppLockEnabled
? Icons.verified_user_rounded
: Icons.lock_open_rounded,
color: _isAppLockEnabled
? Colors.greenAccent
: Colors.white60,
),
const SizedBox(width: 12),
Expanded(
child: Text(
_isAppLockEnabled
? 'رمز برنامه فعال است'
: 'رمز برنامه غیرفعال است',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
const SizedBox(height: 14),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: _isAppLockEnabled
? _showChangeAppLockDialog
: _showEnableAppLockDialog,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
vertical: 14,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
icon: Icon(
_isAppLockEnabled
? Icons.password_rounded
: Icons.lock_rounded,
),
label: Text(
_isAppLockEnabled
? 'تغییر رمز برنامه'
: 'فعال‌سازی رمز برنامه',
),
),
),
if (_isAppLockEnabled) ...[
const SizedBox(height: 10),
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: _showDisableAppLockDialog,
style: OutlinedButton.styleFrom(
foregroundColor: Colors.redAccent,
side: const BorderSide(
color: Colors.redAccent,
),
padding: const EdgeInsets.symmetric(
vertical: 14,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
icon: const Icon(Icons.lock_reset_rounded),
label:
const Text('غیرفعال‌سازی رمز برنامه'),
),
),
],
],
),
),
const SizedBox(height: 16),
_buildSettingSection(
icon: Icons.security_rounded,
iconColor: Colors.cyanAccent,
title: 'حریم خصوصی و استتار',
content:
'با فعال‌سازی این گزینه، آیکون و نام برنامه در لیست برنامه‌های گوشی به "ماشین حساب" تغییر می‌کند تا امنیت شما حفظ شود.',
action: FutureBuilder<bool>(
future: _platform
.invokeMethod<bool>('getStealthMode')
.then((v) => v ?? false),
builder: (context, snapshot) {
final isStealth = snapshot.data ?? false;
return Container(
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(12),
),
child: SwitchListTile(
title: const Text('حالت استتار (ماشین حساب)',
style: TextStyle(
fontSize: 14, color: Colors.white)),
subtitle: Text(
isStealth
? 'فعال (برنامه مخفی است)'
: 'غیرفعال',
style: const TextStyle(
fontSize: 12, color: Colors.white60)),
value: isStealth,
contentPadding:
const EdgeInsets.symmetric(horizontal: 12),
secondary: const Icon(Icons.calculate_outlined,
color: Colors.white70),
activeThumbColor: Colors.cyanAccent,
onChanged: (bool value) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: AppTheme.darkCard,
title: Text(
value
? 'فعال‌سازی حالت مخفی'
: 'خروج از حالت مخفی',
style:
const TextStyle(color: Colors.white)),
content: Text(
value
? 'با تایید این گزینه، برنامه بسته شده و آیکون آن به ماشین حساب تغییر می‌کند.'
: 'با تایید این گزینه، آیکون اصلی صبا باز می‌گردد.',
style: const TextStyle(
color: Colors.white70)),
actions: [
TextButton(
onPressed: () =>
Navigator.pop(ctx, false),
child: const Text('لغو')),
ElevatedButton(
onPressed: () =>
Navigator.pop(ctx, true),
child: const Text('تایید')),
],
),
);
if (confirmed == true) {
await _platform.invokeMethod(
'setStealthMode', {'enabled': value});
}
},
),
);
},
),
),
const SizedBox(height: 32),
Center(
child: TextButton.icon(
onPressed: _isResetting ? null : _confirmReset,
style: TextButton.styleFrom(
foregroundColor:
Colors.redAccent.withValues(alpha: 0.8)),
icon: const Icon(Icons.dangerous_outlined),
label: const Text('ریست کامل و حذف تمام داده‌ها',
style: TextStyle(fontWeight: FontWeight.bold)),
),
),
const SizedBox(height: 40),
],
),
),
),
],
),
);
}
Widget _buildSettingSection({
required IconData icon,
required Color iconColor,
required String title,
required String content,
required Widget action,
}) {
return AppTheme.glassWrapper(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, color: iconColor),
const SizedBox(width: 12),
Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white),
),
],
),
const SizedBox(height: 12),
Text(
content,
style: const TextStyle(
height: 1.5, color: Colors.white70, fontSize: 13),
),
const SizedBox(height: 20),
action,
],
),
),
);
}
Widget _buildColorItem(Color color, String label) {
final bool isSelected = themeNotifier.value.toARGB32() == color.toARGB32();
return GestureDetector(
onTap: () async {
themeNotifier.value = color;
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('primary_color_value', color.toARGB32());
setState(() {}); // Refresh local UI for selection check
},
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8),
padding: const EdgeInsets.all(3),
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: isSelected ? color : Colors.transparent,
width: 2,
),
),
child: Container(
width: 38,
height: 38,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: color.withValues(alpha: 0.4),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: isSelected
? const Icon(Icons.check, color: Colors.white, size: 20)
: null,
),
),
);
}
}
class _AppLockDialog extends StatefulWidget {
const _AppLockDialog({required this.mode});
final _AppLockDialogMode mode;
@override
State<_AppLockDialog> createState() => _AppLockDialogState();
}
class _AppLockDialogState extends State<_AppLockDialog> {
final TextEditingController _currentController = TextEditingController();
final TextEditingController _nextController = TextEditingController();
final TextEditingController _confirmController = TextEditingController();
bool _isSubmitting = false;
String? _errorText;
bool get _isEnableMode => widget.mode == _AppLockDialogMode.enable;
bool get _isChangeMode => widget.mode == _AppLockDialogMode.change;
bool get _isDisableMode => widget.mode == _AppLockDialogMode.disable;
@override
void dispose() {
_currentController.dispose();
_nextController.dispose();
_confirmController.dispose();
super.dispose();
}
Future<void> _close([bool result = false]) async {
FocusManager.instance.primaryFocus?.unfocus();
await Future<void>.delayed(const Duration(milliseconds: 40));
if (!mounted) return;
Navigator.of(context).pop(result);
}
Future<void> _submit() async {
if (_isSubmitting) return;
if (_isEnableMode) {
final nextError = _validateAppLockPasscode(_nextController.text);
if (nextError != null) {
setState(() => _errorText = nextError);
return;
}
if (_nextController.text.trim() != _confirmController.text.trim()) {
setState(() => _errorText = 'تکرار رمز با رمز اصلی یکسان نیست.');
return;
}
}
if (_isChangeMode) {
final currentError = _validateAppLockPasscode(_currentController.text);
final nextError = _validateAppLockPasscode(_nextController.text);
if (currentError != null) {
setState(() => _errorText = 'رمز فعلی معتبر نیست.');
return;
}
if (nextError != null) {
setState(() => _errorText = nextError);
return;
}
if (_nextController.text.trim() != _confirmController.text.trim()) {
setState(() => _errorText = 'تکرار رمز جدید با رمز جدید یکسان نیست.');
return;
}
}
if (_isDisableMode) {
final currentError = _validateAppLockPasscode(_currentController.text);
if (currentError != null) {
setState(() => _errorText = 'رمز فعلی را درست وارد کنید.');
return;
}
}
setState(() {
_isSubmitting = true;
_errorText = null;
});
if (_isEnableMode) {
await AppLockService.instance.setPasscode(_nextController.text.trim());
await _close(true);
return;
}
if (_isChangeMode) {
final updated = await AppLockService.instance.changePasscode(
currentPasscode: _currentController.text.trim(),
newPasscode: _nextController.text.trim(),
);
if (!mounted) return;
if (!updated) {
setState(() {
_isSubmitting = false;
_errorText = 'رمز فعلی درست نیست.';
});
return;
}
await _close(true);
return;
}
final removed = await AppLockService.instance
.disablePasscode(_currentController.text.trim());
if (!mounted) return;
if (!removed) {
setState(() {
_isSubmitting = false;
_errorText = 'رمز فعلی درست نیست.';
});
return;
}
await _close(true);
}
String get _title {
switch (widget.mode) {
case _AppLockDialogMode.enable:
return 'فعال‌سازی رمز برنامه';
case _AppLockDialogMode.change:
return 'تغییر رمز برنامه';
case _AppLockDialogMode.disable:
return 'غیرفعال‌سازی رمز برنامه';
}
}
String get _primaryActionLabel {
switch (widget.mode) {
case _AppLockDialogMode.enable:
return _isSubmitting ? 'در حال ذخیره...' : 'فعال‌سازی';
case _AppLockDialogMode.change:
return _isSubmitting ? 'در حال ذخیره...' : 'ذخیره تغییرات';
case _AppLockDialogMode.disable:
return _isSubmitting ? 'در حال بررسی...' : 'حذف رمز';
}
}
Widget _buildPasscodeField({
required TextEditingController controller,
required String label,
ValueChanged<String>? onSubmitted,
}) {
return TextField(
controller: controller,
keyboardType: TextInputType.number,
obscureText: true,
obscuringCharacter: '',
textInputAction: TextInputAction.done,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(8),
],
onSubmitted: onSubmitted,
decoration: InputDecoration(
labelText: label,
hintText: '۴ تا ۸ رقم',
filled: true,
fillColor: Colors.white.withValues(alpha: 0.06),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
),
),
);
}
@override
Widget build(BuildContext context) {
return AlertDialog(
backgroundColor: AppTheme.darkCard,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
title: Text(
_title,
style: const TextStyle(color: Colors.white),
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (_isEnableMode)
const Text(
'یک رمز ۴ تا ۸ رقمی انتخاب کنید. از این به بعد، برای ورود یا بازگشت به برنامه باید همین رمز وارد شود.',
style: TextStyle(color: Colors.white70, height: 1.6),
),
if (_isDisableMode)
const Text(
'برای حذف قفل برنامه، رمز فعلی را وارد کنید.',
style: TextStyle(color: Colors.white70, height: 1.6),
),
if (_isEnableMode || _isDisableMode) const SizedBox(height: 16),
if (_isChangeMode) ...[
_buildPasscodeField(
controller: _currentController,
label: 'رمز فعلی',
),
const SizedBox(height: 12),
_buildPasscodeField(
controller: _nextController,
label: 'رمز جدید',
),
const SizedBox(height: 12),
_buildPasscodeField(
controller: _confirmController,
label: 'تکرار رمز جدید',
onSubmitted: (_) => _submit(),
),
],
if (_isEnableMode) ...[
_buildPasscodeField(
controller: _nextController,
label: 'رمز جدید',
),
const SizedBox(height: 12),
_buildPasscodeField(
controller: _confirmController,
label: 'تکرار رمز',
onSubmitted: (_) => _submit(),
),
],
if (_isDisableMode)
_buildPasscodeField(
controller: _currentController,
label: 'رمز فعلی',
onSubmitted: (_) => _submit(),
),
if (_errorText != null) ...[
const SizedBox(height: 12),
Text(
_errorText!,
style: const TextStyle(color: Colors.redAccent),
),
],
],
),
actions: [
TextButton(
onPressed: _isSubmitting ? null : _close,
child: const Text('لغو'),
),
ElevatedButton(
style: _isDisableMode
? ElevatedButton.styleFrom(
backgroundColor: Colors.redAccent,
foregroundColor: Colors.white,
)
: null,
onPressed: _isSubmitting ? null : _submit,
child: Text(_primaryActionLabel),
),
],
);
}
}
class MeshBackgroundPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final Paint paint1 = Paint()
..shader = RadialGradient(
colors: [
const Color(0xFF7000FF).withValues(alpha: 0.12),
const Color(0xFF7000FF).withValues(alpha: 0.0),
],
).createShader(Rect.fromCircle(
center: Offset(size.width * 0.2, size.height * 0.15), radius: 300));
canvas.drawCircle(
Offset(size.width * 0.2, size.height * 0.15), 300, paint1);
final Paint paint2 = Paint()
..shader = RadialGradient(
colors: [
const Color(0xFF00D2FF).withValues(alpha: 0.1),
const Color(0xFF00D2FF).withValues(alpha: 0.0),
],
).createShader(Rect.fromCircle(
center: Offset(size.width * 0.8, size.height * 0.8), radius: 400));
canvas.drawCircle(Offset(size.width * 0.8, size.height * 0.8), 400, paint2);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}