- Add url_launcher dependency - Add Android queries element for https scheme visibility - Add triple-tap detection on Settings AppBar title - Simplify to direct URL launch (no animation)
430 lines
13 KiB
Dart
430 lines
13 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
import 'bridge/location_bridge.dart';
|
|
import 'preferences.dart';
|
|
|
|
class SettingsScreen extends StatefulWidget {
|
|
const SettingsScreen({super.key});
|
|
|
|
@override
|
|
State<SettingsScreen> createState() => _SettingsScreenState();
|
|
}
|
|
|
|
class _SettingsScreenState extends State<SettingsScreen> {
|
|
late TextEditingController _serverUrlController;
|
|
late TextEditingController _deviceIdController;
|
|
late TextEditingController _distanceFilterController;
|
|
late TextEditingController _intervalController;
|
|
late TextEditingController _heartbeatController;
|
|
late int _accuracy;
|
|
late bool _offlineBuffer;
|
|
late bool _stopDetection;
|
|
int _tapCount = 0;
|
|
DateTime? _lastTapTime;
|
|
|
|
Future<void> _handleAppBarTap() async {
|
|
const tripleTapInterval = Duration(milliseconds: 500);
|
|
final now = DateTime.now();
|
|
|
|
if (_lastTapTime != null &&
|
|
now.difference(_lastTapTime!) > tripleTapInterval) {
|
|
_tapCount = 0;
|
|
}
|
|
|
|
_tapCount++;
|
|
_lastTapTime = now;
|
|
|
|
if (_tapCount == 3) {
|
|
_tapCount = 0;
|
|
final uri = Uri.parse('https://fiatcode.dev');
|
|
if (await canLaunchUrl(uri)) {
|
|
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
SystemChrome.setSystemUIOverlayStyle(
|
|
const SystemUiOverlayStyle(
|
|
statusBarColor: Color(0xFF0d0d0d),
|
|
statusBarIconBrightness: Brightness.light,
|
|
),
|
|
);
|
|
_serverUrlController = TextEditingController(text: Preferences.serverUrl);
|
|
_deviceIdController = TextEditingController(text: Preferences.deviceId);
|
|
_distanceFilterController = TextEditingController(
|
|
text: Preferences.distanceFilter.toString(),
|
|
);
|
|
_intervalController = TextEditingController(
|
|
text: Preferences.interval.toString(),
|
|
);
|
|
_heartbeatController = TextEditingController(
|
|
text: Preferences.heartbeat.toString(),
|
|
);
|
|
_accuracy = Preferences.accuracy;
|
|
_offlineBuffer = Preferences.offlineBuffer;
|
|
_stopDetection = Preferences.stopDetection;
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_serverUrlController.dispose();
|
|
_deviceIdController.dispose();
|
|
_distanceFilterController.dispose();
|
|
_intervalController.dispose();
|
|
_heartbeatController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _saveSettings() async {
|
|
final distanceFilter = int.tryParse(_distanceFilterController.text) ?? 75;
|
|
final interval = int.tryParse(_intervalController.text) ?? 300;
|
|
final heartbeat = int.tryParse(_heartbeatController.text) ?? 60;
|
|
|
|
await Preferences.setServerUrl(_serverUrlController.text);
|
|
await Preferences.setDeviceId(_deviceIdController.text);
|
|
await Preferences.setAccuracy(_accuracy);
|
|
await Preferences.setDistanceFilter(distanceFilter);
|
|
await Preferences.setInterval(interval);
|
|
await Preferences.setHeartbeat(heartbeat);
|
|
await Preferences.setOfflineBuffer(_offlineBuffer);
|
|
await Preferences.setStopDetection(_stopDetection);
|
|
|
|
await LocationBridge.updateConfig({
|
|
'serverUrl': _serverUrlController.text,
|
|
'deviceId': _deviceIdController.text,
|
|
'accuracy': _accuracy,
|
|
'distanceFilter': distanceFilter,
|
|
'interval': interval,
|
|
'heartbeat': heartbeat,
|
|
'offlineBuffer': _offlineBuffer,
|
|
'stopDetection': _stopDetection,
|
|
});
|
|
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Row(
|
|
children: const [
|
|
Icon(Icons.check_circle, color: Color(0xFF00e676), size: 18),
|
|
SizedBox(width: 10),
|
|
Text(
|
|
'Settings saved',
|
|
style: TextStyle(fontFamily: 'monospace', letterSpacing: 1),
|
|
),
|
|
],
|
|
),
|
|
backgroundColor: const Color(0xFF1a1a1a),
|
|
behavior: SnackBarBehavior.floating,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: const Color(0xFF0d0d0d),
|
|
appBar: AppBar(
|
|
backgroundColor: const Color(0xFF161616),
|
|
elevation: 0,
|
|
leading: IconButton(
|
|
icon: const Icon(
|
|
Icons.arrow_back,
|
|
color: Color(0xFF9e9e9e),
|
|
size: 20,
|
|
),
|
|
onPressed: () => Navigator.pop(context),
|
|
),
|
|
title: GestureDetector(
|
|
onTap: _handleAppBarTap,
|
|
child: const Text(
|
|
'SETTINGS',
|
|
style: TextStyle(
|
|
fontFamily: 'monospace',
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 2,
|
|
color: Color(0xFFe0e0e0),
|
|
),
|
|
),
|
|
),
|
|
bottom: PreferredSize(
|
|
preferredSize: const Size.fromHeight(1),
|
|
child: Container(height: 1, color: const Color(0xFF2a2a2a)),
|
|
),
|
|
),
|
|
body: ListView(
|
|
padding: const EdgeInsets.all(20),
|
|
children: [
|
|
_buildSectionHeader('SERVER'),
|
|
const SizedBox(height: 12),
|
|
_buildTextField('Server URL', _serverUrlController),
|
|
const SizedBox(height: 12),
|
|
_buildTextField('Device ID', _deviceIdController),
|
|
const SizedBox(height: 24),
|
|
_buildSectionHeader('LOCATION'),
|
|
const SizedBox(height: 12),
|
|
_buildAccuracyDropdown(),
|
|
const SizedBox(height: 12),
|
|
_buildNumberField('Distance Filter (m)', _distanceFilterController),
|
|
const SizedBox(height: 12),
|
|
_buildNumberField('Update Interval (s)', _intervalController),
|
|
const SizedBox(height: 12),
|
|
_buildNumberField('Heartbeat (s)', _heartbeatController),
|
|
const SizedBox(height: 24),
|
|
_buildSectionHeader('ADVANCED'),
|
|
const SizedBox(height: 8),
|
|
_buildSwitch(
|
|
'Offline Buffering',
|
|
'Queue locations when network unavailable',
|
|
_offlineBuffer,
|
|
(v) => setState(() => _offlineBuffer = v),
|
|
),
|
|
const SizedBox(height: 4),
|
|
_buildSwitch(
|
|
'Stop Detection',
|
|
'Auto-stop tracking when stationary',
|
|
_stopDetection,
|
|
(v) => setState(() => _stopDetection = v),
|
|
),
|
|
const SizedBox(height: 32),
|
|
_buildSaveButton(),
|
|
const SizedBox(height: 20),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSectionHeader(String title) {
|
|
return Row(
|
|
children: [
|
|
Container(
|
|
width: 3,
|
|
height: 14,
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF00bcd4),
|
|
borderRadius: BorderRadius.circular(1),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
title,
|
|
style: const TextStyle(
|
|
fontFamily: 'monospace',
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 2,
|
|
color: Color(0xFF00bcd4),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildTextField(String label, TextEditingController controller) {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF161616),
|
|
borderRadius: BorderRadius.circular(4),
|
|
border: Border.all(color: const Color(0xFF2a2a2a), width: 1),
|
|
),
|
|
child: TextField(
|
|
controller: controller,
|
|
style: const TextStyle(
|
|
fontFamily: 'monospace',
|
|
fontSize: 13,
|
|
color: Color(0xFFe0e0e0),
|
|
),
|
|
decoration: InputDecoration(
|
|
labelText: label,
|
|
labelStyle: const TextStyle(
|
|
fontFamily: 'monospace',
|
|
fontSize: 11,
|
|
letterSpacing: 1,
|
|
color: Color(0xFF616161),
|
|
),
|
|
border: InputBorder.none,
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: 14,
|
|
vertical: 14,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildAccuracyDropdown() {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 14),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF161616),
|
|
borderRadius: BorderRadius.circular(4),
|
|
border: Border.all(color: const Color(0xFF2a2a2a), width: 1),
|
|
),
|
|
child: DropdownButtonFormField<int>(
|
|
initialValue: _accuracy,
|
|
dropdownColor: const Color(0xFF1a1a1a),
|
|
style: const TextStyle(
|
|
fontFamily: 'monospace',
|
|
fontSize: 13,
|
|
color: Color(0xFFe0e0e0),
|
|
),
|
|
decoration: const InputDecoration(
|
|
labelText: 'Accuracy',
|
|
labelStyle: TextStyle(
|
|
fontFamily: 'monospace',
|
|
fontSize: 11,
|
|
letterSpacing: 1,
|
|
color: Color(0xFF616161),
|
|
),
|
|
border: InputBorder.none,
|
|
),
|
|
items: const [
|
|
DropdownMenuItem(
|
|
value: 0,
|
|
child: Text('High', style: TextStyle(fontFamily: 'monospace')),
|
|
),
|
|
DropdownMenuItem(
|
|
value: 1,
|
|
child: Text(
|
|
'High Accuracy',
|
|
style: TextStyle(fontFamily: 'monospace'),
|
|
),
|
|
),
|
|
DropdownMenuItem(
|
|
value: 2,
|
|
child: Text('Balanced', style: TextStyle(fontFamily: 'monospace')),
|
|
),
|
|
DropdownMenuItem(
|
|
value: 3,
|
|
child: Text('Low', style: TextStyle(fontFamily: 'monospace')),
|
|
),
|
|
],
|
|
onChanged: (v) => setState(() => _accuracy = v!),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildNumberField(String label, TextEditingController controller) {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF161616),
|
|
borderRadius: BorderRadius.circular(4),
|
|
border: Border.all(color: const Color(0xFF2a2a2a), width: 1),
|
|
),
|
|
child: TextField(
|
|
controller: controller,
|
|
keyboardType: TextInputType.number,
|
|
style: const TextStyle(
|
|
fontFamily: 'monospace',
|
|
fontSize: 13,
|
|
color: Color(0xFFe0e0e0),
|
|
),
|
|
decoration: InputDecoration(
|
|
labelText: label,
|
|
labelStyle: const TextStyle(
|
|
fontFamily: 'monospace',
|
|
fontSize: 11,
|
|
letterSpacing: 1,
|
|
color: Color(0xFF616161),
|
|
),
|
|
border: InputBorder.none,
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: 14,
|
|
vertical: 14,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSwitch(
|
|
String title,
|
|
String subtitle,
|
|
bool value,
|
|
ValueChanged<bool> onChanged,
|
|
) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF161616),
|
|
borderRadius: BorderRadius.circular(4),
|
|
border: Border.all(color: const Color(0xFF2a2a2a), width: 1),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: const TextStyle(
|
|
fontFamily: 'monospace',
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w600,
|
|
color: Color(0xFFe0e0e0),
|
|
),
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
subtitle,
|
|
style: const TextStyle(
|
|
fontFamily: 'monospace',
|
|
fontSize: 10,
|
|
color: Color(0xFF616161),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Switch(
|
|
value: value,
|
|
onChanged: onChanged,
|
|
activeThumbColor: const Color(0xFF00e676),
|
|
activeTrackColor: const Color(0xFF00e676).withValues(alpha: 0.3),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSaveButton() {
|
|
return Material(
|
|
color: const Color(0xFF00bcd4).withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(4),
|
|
child: InkWell(
|
|
onTap: _saveSettings,
|
|
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: const Color(0xFF00bcd4).withValues(alpha: 0.4),
|
|
width: 1,
|
|
),
|
|
),
|
|
child: const Center(
|
|
child: Text(
|
|
'SAVE SETTINGS',
|
|
style: TextStyle(
|
|
fontFamily: 'monospace',
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 2,
|
|
color: Color(0xFF00bcd4),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|