feat: add permission screen shown before main screen
This commit is contained in:
parent
a7a6a6a82d
commit
650a6efeca
6 changed files with 562 additions and 1 deletions
510
lib/permission_screen.dart
Normal file
510
lib/permission_screen.dart
Normal file
|
|
@ -0,0 +1,510 @@
|
|||
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;
|
||||
// Battery optimization check is Android-only
|
||||
final batteryOptOut = await _isBatteryOptimizationDisabled();
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_locationGranted = location.isGranted;
|
||||
_notificationGranted = notification.isGranted;
|
||||
_batteryOptOut = batteryOptOut;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _isBatteryOptimizationDisabled() async {
|
||||
// On Android, check if battery optimization is disabled for the app
|
||||
// We approximate this - in production you'd use a native method
|
||||
// For now, assume not granted until user says so
|
||||
return false;
|
||||
}
|
||||
|
||||
bool get _allGranted =>
|
||||
_locationGranted && _notificationGranted && _batteryOptOut;
|
||||
|
||||
Future<void> _requestLocation() async {
|
||||
final status = await Permission.locationAlways.request();
|
||||
if (mounted) {
|
||||
setState(() => _locationGranted = status.isGranted);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _requestNotification() async {
|
||||
final status = await Permission.notification.request();
|
||||
if (mounted) {
|
||||
setState(() => _notificationGranted = status.isGranted);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _requestBatteryOptOut() async {
|
||||
final success = await LocationBridge.openBatteryOptimizationSettings();
|
||||
if (success) {
|
||||
// Show dialog asking user to confirm after disabling battery opt
|
||||
if (mounted) _showBatteryOptDialog();
|
||||
} else {
|
||||
// Fallback to manual instructions dialog
|
||||
if (mounted) _showBatteryOptDialog();
|
||||
}
|
||||
}
|
||||
|
||||
void _showBatteryOptDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
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: const Text(
|
||||
'Please disable battery optimization for this app to ensure reliable background tracking.\n\n'
|
||||
'In the next screen, find "Traccar Client" and set it to "Don\'t optimize" or "Unrestricted".',
|
||||
style: TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12,
|
||||
color: Color(0xFF9e9e9e),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: const Text(
|
||||
'OPEN SETTINGS',
|
||||
style: TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 1,
|
||||
color: Color(0xFF00bcd4),
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(ctx);
|
||||
setState(() => _batteryOptOut = true);
|
||||
},
|
||||
child: const Text(
|
||||
'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: Row(
|
||||
children: [
|
||||
const Icon(Icons.shield, color: Color(0xFF00bcd4), size: 20),
|
||||
const SizedBox(width: 12),
|
||||
const 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: BoxDecoration(
|
||||
color: const Color(0xFF00bcd4).withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.info_outline,
|
||||
color: Color(0xFF00bcd4),
|
||||
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).withOpacity(0.3)
|
||||
: const Color(0xFF2a2a2a),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: granted
|
||||
? const Color(0xFF00e676).withOpacity(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).withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
border: Border.all(
|
||||
color: const Color(0xFF00e676).withOpacity(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).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
border: Border.all(
|
||||
color: const Color(0xFF00bcd4).withOpacity(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;
|
||||
final total = 3;
|
||||
final progress = granted / total;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.check_circle_outline,
|
||||
color: 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).withOpacity(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).withOpacity(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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue