307 lines
8.5 KiB
Dart
307 lines
8.5 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
|
|
import '../utils/app_lock_service.dart';
|
|
import '../utils/app_theme.dart';
|
|
|
|
class AppLockOverlay extends StatefulWidget {
|
|
const AppLockOverlay({
|
|
super.key,
|
|
required this.child,
|
|
});
|
|
|
|
final Widget child;
|
|
|
|
@override
|
|
State<AppLockOverlay> createState() => _AppLockOverlayState();
|
|
}
|
|
|
|
class _AppLockOverlayState extends State<AppLockOverlay>
|
|
with WidgetsBindingObserver {
|
|
bool _shouldRelockOnResume = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
WidgetsBinding.instance.addObserver(this);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
WidgetsBinding.instance.removeObserver(this);
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
final service = AppLockService.instance;
|
|
if (!service.isEnabled) return;
|
|
|
|
if (state == AppLifecycleState.inactive ||
|
|
state == AppLifecycleState.paused ||
|
|
state == AppLifecycleState.hidden) {
|
|
_shouldRelockOnResume = true;
|
|
return;
|
|
}
|
|
|
|
if (state == AppLifecycleState.resumed && _shouldRelockOnResume) {
|
|
_shouldRelockOnResume = false;
|
|
service.lock();
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return AnimatedBuilder(
|
|
animation: AppLockService.instance,
|
|
builder: (context, _) {
|
|
final service = AppLockService.instance;
|
|
return Stack(
|
|
fit: StackFit.expand,
|
|
children: [
|
|
widget.child,
|
|
if (service.initialized && service.isLocked)
|
|
const Positioned.fill(child: _AppLockBarrier()),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class _AppLockBarrier extends StatelessWidget {
|
|
const _AppLockBarrier();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return PopScope(
|
|
canPop: false,
|
|
child: Material(
|
|
color: AppTheme.darkBg.withValues(alpha: 0.96),
|
|
child: Stack(
|
|
fit: StackFit.expand,
|
|
children: [
|
|
Positioned.fill(
|
|
child: CustomPaint(
|
|
painter: _LockBackgroundPainter(),
|
|
),
|
|
),
|
|
const SafeArea(
|
|
child: Center(
|
|
child: Padding(
|
|
padding: EdgeInsets.all(20),
|
|
child: _AppLockCard(),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _AppLockCard extends StatefulWidget {
|
|
const _AppLockCard();
|
|
|
|
@override
|
|
State<_AppLockCard> createState() => _AppLockCardState();
|
|
}
|
|
|
|
class _AppLockCardState extends State<_AppLockCard> {
|
|
final TextEditingController _controller = TextEditingController();
|
|
bool _isVerifying = false;
|
|
String? _errorText;
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _unlock() async {
|
|
if (_isVerifying) return;
|
|
|
|
FocusScope.of(context).unfocus();
|
|
final passcode = _controller.text.trim();
|
|
if (passcode.isEmpty) {
|
|
setState(() => _errorText = 'رمز برنامه را وارد کنید.');
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
_isVerifying = true;
|
|
_errorText = null;
|
|
});
|
|
|
|
final isValid = await AppLockService.instance.verifyPasscode(passcode);
|
|
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_isVerifying = false;
|
|
_errorText = isValid ? null : 'رمز واردشده درست نیست.';
|
|
});
|
|
|
|
if (isValid) {
|
|
_controller.clear();
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
|
|
return ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 420),
|
|
child: AppTheme.glassWrapper(
|
|
radius: 28,
|
|
sigma: 14,
|
|
child: Padding(
|
|
padding: const EdgeInsets.fromLTRB(24, 28, 24, 24),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: 76,
|
|
height: 76,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
theme.primaryColor.withValues(alpha: 0.95),
|
|
AppTheme.accentCyan.withValues(alpha: 0.95),
|
|
],
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: theme.primaryColor.withValues(alpha: 0.28),
|
|
blurRadius: 20,
|
|
spreadRadius: 1,
|
|
),
|
|
],
|
|
),
|
|
child: const Icon(
|
|
Icons.lock_rounded,
|
|
size: 38,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
const SizedBox(height: 18),
|
|
const Text(
|
|
'قفل برنامه فعال است',
|
|
style: TextStyle(
|
|
fontSize: 22,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 10),
|
|
const Text(
|
|
'برای ورود به صبا، رمز برنامه را وارد کنید.',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
height: 1.6,
|
|
color: Colors.white70,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 24),
|
|
TextField(
|
|
controller: _controller,
|
|
autofocus: true,
|
|
keyboardType: TextInputType.number,
|
|
textInputAction: TextInputAction.done,
|
|
obscureText: true,
|
|
obscuringCharacter: '•',
|
|
inputFormatters: [
|
|
FilteringTextInputFormatter.digitsOnly,
|
|
LengthLimitingTextInputFormatter(8),
|
|
],
|
|
onSubmitted: (_) => _unlock(),
|
|
decoration: InputDecoration(
|
|
labelText: 'رمز برنامه',
|
|
hintText: '۴ تا ۸ رقم',
|
|
errorText: _errorText,
|
|
filled: true,
|
|
fillColor: Colors.white.withValues(alpha: 0.06),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 18),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton.icon(
|
|
onPressed: _isVerifying ? null : _unlock,
|
|
style: ElevatedButton.styleFrom(
|
|
padding: const EdgeInsets.symmetric(vertical: 15),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
),
|
|
icon: _isVerifying
|
|
? const SizedBox(
|
|
width: 18,
|
|
height: 18,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
)
|
|
: const Icon(Icons.lock_open_rounded),
|
|
label:
|
|
Text(_isVerifying ? 'در حال بررسی...' : 'ورود به برنامه'),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _LockBackgroundPainter extends CustomPainter {
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
final purplePaint = Paint()
|
|
..shader = RadialGradient(
|
|
colors: [
|
|
AppTheme.accentPurple.withValues(alpha: 0.22),
|
|
AppTheme.accentPurple.withValues(alpha: 0),
|
|
],
|
|
).createShader(
|
|
Rect.fromCircle(
|
|
center: Offset(size.width * 0.18, size.height * 0.2),
|
|
radius: size.width * 0.55,
|
|
),
|
|
);
|
|
|
|
final cyanPaint = Paint()
|
|
..shader = RadialGradient(
|
|
colors: [
|
|
AppTheme.accentCyan.withValues(alpha: 0.14),
|
|
AppTheme.accentCyan.withValues(alpha: 0),
|
|
],
|
|
).createShader(
|
|
Rect.fromCircle(
|
|
center: Offset(size.width * 0.82, size.height * 0.78),
|
|
radius: size.width * 0.6,
|
|
),
|
|
);
|
|
|
|
canvas.drawCircle(
|
|
Offset(size.width * 0.18, size.height * 0.2),
|
|
size.width * 0.55,
|
|
purplePaint,
|
|
);
|
|
canvas.drawCircle(
|
|
Offset(size.width * 0.82, size.height * 0.78),
|
|
size.width * 0.6,
|
|
cyanPaint,
|
|
);
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
|
}
|