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
|
|
@ -7,6 +7,7 @@
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="traccar_client"
|
android:label="traccar_client"
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,9 @@ class BridgeModule : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||||
val limit = (call.arguments as? Number)?.toInt() ?: 100
|
val limit = (call.arguments as? Number)?.toInt() ?: 100
|
||||||
fetchLogs(limit, result)
|
fetchLogs(limit, result)
|
||||||
}
|
}
|
||||||
|
"openBatteryOptimizationSettings" -> {
|
||||||
|
openBatteryOptimizationSettings(result)
|
||||||
|
}
|
||||||
else -> result.notImplemented()
|
else -> result.notImplemented()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -189,4 +192,27 @@ class BridgeModule : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||||
}
|
}
|
||||||
}.start()
|
}.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun openBatteryOptimizationSettings(result: MethodChannel.Result) {
|
||||||
|
try {
|
||||||
|
val packageName = context?.packageName ?: ""
|
||||||
|
val intent = android.content.Intent("android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS").apply {
|
||||||
|
data = android.net.Uri.parse("package:$packageName")
|
||||||
|
}
|
||||||
|
intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
context?.startActivity(intent)
|
||||||
|
result.success(true)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Fallback to general battery settings
|
||||||
|
try {
|
||||||
|
val intent = android.content.Intent("android.settings.IGNORE_BATTERY_OPTIMIZATION_SETTINGS").apply {
|
||||||
|
}
|
||||||
|
intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
context?.startActivity(intent)
|
||||||
|
result.success(true)
|
||||||
|
} catch (e2: Exception) {
|
||||||
|
result.success(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -92,4 +92,16 @@ class LocationBridge {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<bool> openBatteryOptimizationSettings() async {
|
||||||
|
try {
|
||||||
|
final result = await _methodChannel.invokeMethod<bool>(
|
||||||
|
'openBatteryOptimizationSettings',
|
||||||
|
);
|
||||||
|
return result ?? false;
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
debugPrint('Failed to open battery settings: ${e.message}');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:traccar_client/main_screen.dart';
|
import 'package:traccar_client/main_screen.dart';
|
||||||
|
import 'package:traccar_client/permission_screen.dart';
|
||||||
import 'package:traccar_client/preferences.dart';
|
import 'package:traccar_client/preferences.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
|
|
@ -21,7 +23,9 @@ class TraccarClientApp extends StatelessWidget {
|
||||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
|
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
|
||||||
appBarTheme: const AppBarTheme(centerTitle: true),
|
appBarTheme: const AppBarTheme(centerTitle: true),
|
||||||
),
|
),
|
||||||
home: const MainScreen(),
|
home: Preferences.permissionsGranted
|
||||||
|
? const MainScreen()
|
||||||
|
: const PermissionScreen(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,7 @@ class Preferences {
|
||||||
static const String keyHeartbeat = 'heartbeat';
|
static const String keyHeartbeat = 'heartbeat';
|
||||||
static const String keyOfflineBuffer = 'offline_buffer';
|
static const String keyOfflineBuffer = 'offline_buffer';
|
||||||
static const String keyStopDetection = 'stop_detection';
|
static const String keyStopDetection = 'stop_detection';
|
||||||
|
static const String keyPermissionsGranted = 'permissions_granted';
|
||||||
|
|
||||||
static SharedPreferences? _instance;
|
static SharedPreferences? _instance;
|
||||||
|
|
||||||
|
|
@ -52,6 +53,13 @@ class Preferences {
|
||||||
|
|
||||||
static bool get stopDetection => instance.getBool(keyStopDetection) ?? true;
|
static bool get stopDetection => instance.getBool(keyStopDetection) ?? true;
|
||||||
|
|
||||||
|
static bool get permissionsGranted =>
|
||||||
|
instance.getBool(keyPermissionsGranted) ?? false;
|
||||||
|
|
||||||
|
static Future<void> setPermissionsGranted(bool value) async {
|
||||||
|
await instance.setBool(keyPermissionsGranted, value);
|
||||||
|
}
|
||||||
|
|
||||||
static Future<void> setServerUrl(String value) async {
|
static Future<void> setServerUrl(String value) async {
|
||||||
await instance.setString(keyServerUrl, value);
|
await instance.setString(keyServerUrl, value);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue