tracpulse/lib/permission_screen.dart

550 lines
17 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:traccar_client/bridge/location_bridge.dart';
import 'package:traccar_client/preferences.dart';
import 'package:traccar_client/main_screen.dart';
class PermissionScreen extends StatefulWidget {
const PermissionScreen({super.key});
@override
State<PermissionScreen> createState() => _PermissionScreenState();
}
class _PermissionScreenState extends State<PermissionScreen> {
bool _locationGranted = false;
bool _notificationGranted = false;
bool _batteryOptOut = false;
@override
void initState() {
super.initState();
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarColor: Color(0xFF0d0d0d),
statusBarIconBrightness: Brightness.light,
),
);
_checkPermissions();
}
Future<void> _checkPermissions() async {
final location = await Permission.locationAlways.status;
final notification = await Permission.notification.status;
final batteryOptOut = await _isBatteryOptimizationDisabled();
if (mounted) {
setState(() {
_locationGranted = location.isGranted;
_notificationGranted = notification.isGranted;
_batteryOptOut = batteryOptOut;
});
}
}
Future<bool> _isBatteryOptimizationDisabled() async {
try {
final result = await LocationBridge.isBatteryOptimizationDisabled();
return result == true;
} catch (_) {
return false;
}
}
bool get _allGranted =>
_locationGranted && _notificationGranted && _batteryOptOut;
Future<void> _requestLocation() async {
// Check current status first
var status = await Permission.locationAlways.status;
if (status.isGranted) {
if (mounted) setState(() => _locationGranted = true);
return;
}
// On Android, request locationWhenInUse first before locationAlways
var whenInUse = await Permission.locationWhenInUse.status;
if (!whenInUse.isGranted) {
whenInUse = await Permission.locationWhenInUse.request();
}
// Now request locationAlways (background permission)
status = await Permission.locationAlways.request();
if (!status.isGranted && status.isPermanentlyDenied) {
await openAppSettings();
}
final finalStatus = await Permission.locationAlways.status;
if (mounted) {
setState(() => _locationGranted = finalStatus.isGranted);
}
}
Future<void> _requestNotification() async {
var status = await Permission.notification.status;
if (status.isGranted) {
if (mounted) setState(() => _notificationGranted = true);
return;
}
status = await Permission.notification.request();
if (!status.isGranted && status.isPermanentlyDenied) {
await openAppSettings();
}
final finalStatus = await Permission.notification.status;
if (mounted) {
setState(() => _notificationGranted = finalStatus.isGranted);
}
}
Future<void> _requestBatteryOptOut() async {
final opened = await LocationBridge.openBatteryOptimizationSettings();
if (mounted) {
_showBatteryOptDialog(opened);
}
}
void _showBatteryOptDialog(bool settingsOpened) {
showDialog(
context: context,
barrierDismissible: false,
builder: (ctx) => AlertDialog(
backgroundColor: const Color(0xFF1a1a1a),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
side: const BorderSide(color: Color(0xFF2a2a2a)),
),
title: const Text(
'BATTERY OPTIMIZATION',
style: TextStyle(
fontFamily: 'monospace',
fontSize: 14,
fontWeight: FontWeight.w700,
letterSpacing: 2,
color: Color(0xFFe0e0e0),
),
),
content: Text(
settingsOpened
? 'A settings screen has opened.\n\n'
'Find "Traccar Client" and set it to "Don\'t optimize" or "Unrestricted", then come back and tap CHECK & DONE.'
: 'Open Settings → Apps → Traccar Client → Battery and select "Don\'t optimize" or "Unrestricted", then come back and tap CHECK & DONE.',
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 12,
color: Color(0xFF9e9e9e),
),
),
actions: [
TextButton(
onPressed: () {
Navigator.pop(ctx);
openAppSettings();
},
child: const Text(
'OPEN APP SETTINGS',
style: TextStyle(
fontFamily: 'monospace',
fontSize: 11,
fontWeight: FontWeight.w700,
letterSpacing: 1,
color: Color(0xFF00bcd4),
),
),
),
TextButton(
onPressed: () async {
Navigator.pop(ctx);
final disabled = await _isBatteryOptimizationDisabled();
if (mounted) {
setState(() => _batteryOptOut = disabled);
}
},
child: const Text(
'CHECK & DONE',
style: TextStyle(
fontFamily: 'monospace',
fontSize: 11,
fontWeight: FontWeight.w700,
letterSpacing: 1,
color: Color(0xFF00e676),
),
),
),
],
),
);
}
Future<void> _complete() async {
await Preferences.setPermissionsGranted(true);
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const MainScreen()),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0d0d0d),
body: SafeArea(
child: Column(
children: [
_buildHeader(),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildIntroCard(),
const SizedBox(height: 20),
_buildPermissionItem(
icon: Icons.location_on,
title: 'LOCATION',
subtitle: 'All the time access',
granted: _locationGranted,
onRequest: _requestLocation,
),
const SizedBox(height: 12),
_buildPermissionItem(
icon: Icons.notifications,
title: 'NOTIFICATION',
subtitle: 'Show sync & tracking alerts',
granted: _notificationGranted,
onRequest: _requestNotification,
),
const SizedBox(height: 12),
_buildPermissionItem(
icon: Icons.battery_full,
title: 'BATTERY',
subtitle: 'Disable optimization for reliability',
granted: _batteryOptOut,
onRequest: _requestBatteryOptOut,
),
const SizedBox(height: 32),
_buildStatusSummary(),
const SizedBox(height: 24),
_buildContinueButton(),
const SizedBox(height: 20),
],
),
),
),
],
),
),
);
}
Widget _buildHeader() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
decoration: const BoxDecoration(
color: Color(0xFF161616),
border: Border(bottom: BorderSide(color: Color(0xFF2a2a2a), width: 1)),
),
child: const Row(
children: [
Icon(Icons.shield, color: Color(0xFF00bcd4), size: 20),
SizedBox(width: 12),
Text(
'PERMISSIONS',
style: TextStyle(
fontFamily: 'monospace',
fontSize: 16,
fontWeight: FontWeight.w700,
letterSpacing: 3,
color: Color(0xFFe0e0e0),
),
),
],
),
);
}
Widget _buildIntroCard() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF161616),
borderRadius: BorderRadius.circular(4),
border: Border.all(color: const Color(0xFF2a2a2a), width: 1),
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: const BoxDecoration(
color: Color(0xFF00bcd4),
borderRadius: BorderRadius.all(Radius.circular(4)),
),
child: const Icon(
Icons.info_outline,
color: Colors.white,
size: 20,
),
),
const SizedBox(width: 14),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Required Permissions',
style: TextStyle(
fontFamily: 'monospace',
fontSize: 13,
fontWeight: FontWeight.w700,
letterSpacing: 1,
color: Color(0xFFe0e0e0),
),
),
SizedBox(height: 4),
Text(
'Traccar Client needs these permissions to track your location reliably in the background.',
style: TextStyle(
fontFamily: 'monospace',
fontSize: 11,
color: Color(0xFF757575),
),
),
],
),
),
],
),
);
}
Widget _buildPermissionItem({
required IconData icon,
required String title,
required String subtitle,
required bool granted,
required VoidCallback onRequest,
}) {
return Material(
color: const Color(0xFF161616),
borderRadius: BorderRadius.circular(4),
child: InkWell(
onTap: granted ? null : onRequest,
borderRadius: BorderRadius.circular(4),
child: Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: granted
? const Color(0xFF00e676).withValues(alpha: 0.3)
: const Color(0xFF2a2a2a),
width: 1,
),
),
child: Row(
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: granted
? const Color(0xFF00e676).withValues(alpha: 0.15)
: const Color(0xFF2a2a2a),
borderRadius: BorderRadius.circular(4),
),
child: Icon(
icon,
color: granted
? const Color(0xFF00e676)
: const Color(0xFF616161),
size: 18,
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 12,
fontWeight: FontWeight.w700,
letterSpacing: 1,
color: Color(0xFFe0e0e0),
),
),
const SizedBox(height: 2),
Text(
subtitle,
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 10,
color: Color(0xFF616161),
),
),
],
),
),
if (granted)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: const Color(0xFF00e676).withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(3),
border: Border.all(
color: const Color(0xFF00e676).withValues(alpha: 0.4),
width: 1,
),
),
child: const Text(
'GRANTED',
style: TextStyle(
fontFamily: 'monospace',
fontSize: 9,
fontWeight: FontWeight.w700,
letterSpacing: 1,
color: Color(0xFF00e676),
),
),
)
else
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: const Color(0xFF00bcd4).withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(3),
border: Border.all(
color: const Color(0xFF00bcd4).withValues(alpha: 0.4),
width: 1,
),
),
child: const Text(
'TAP TO GRANT',
style: TextStyle(
fontFamily: 'monospace',
fontSize: 9,
fontWeight: FontWeight.w700,
letterSpacing: 1,
color: Color(0xFF00bcd4),
),
),
),
],
),
),
),
);
}
Widget _buildStatusSummary() {
final granted = [
_locationGranted,
_notificationGranted,
_batteryOptOut,
].where((x) => x).length;
const total = 3;
final progress = granted / total;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
granted == total
? Icons.check_circle
: Icons.check_circle_outline,
color: const Color(0xFF00e676),
size: 14,
),
const SizedBox(width: 8),
Text(
'$granted / $total GRANTED',
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 11,
fontWeight: FontWeight.w700,
letterSpacing: 1,
color: Color(0xFF00e676),
),
),
],
),
const SizedBox(height: 10),
Container(
height: 4,
decoration: BoxDecoration(
color: const Color(0xFF2a2a2a),
borderRadius: BorderRadius.circular(2),
),
child: FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: progress,
child: Container(
decoration: BoxDecoration(
color: const Color(0xFF00e676),
borderRadius: BorderRadius.circular(2),
),
),
),
),
],
);
}
Widget _buildContinueButton() {
return Material(
color: _allGranted
? const Color(0xFF00e676).withValues(alpha: 0.15)
: const Color(0xFF2a2a2a),
borderRadius: BorderRadius.circular(4),
child: InkWell(
onTap: _allGranted ? _complete : null,
borderRadius: BorderRadius.circular(4),
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: _allGranted
? const Color(0xFF00e676).withValues(alpha: 0.4)
: const Color(0xFF2a2a2a),
width: 1,
),
),
child: Center(
child: Text(
_allGranted ? 'CONTINUE' : 'GRANT ALL PERMISSIONS',
style: TextStyle(
fontFamily: 'monospace',
fontSize: 13,
fontWeight: FontWeight.w700,
letterSpacing: 2,
color: _allGranted
? const Color(0xFF00e676)
: const Color(0xFF616161),
),
),
),
),
),
);
}
}