test-flutter/lib/main.dart
2026-08-25 17:23:48 +03:30

307 lines
10 KiB
Dart

import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
void main() => runApp(const LocationApp());
class LocationApp extends StatelessWidget {
const LocationApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'دقت موقعیت مکانی',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF006C67),
brightness: Brightness.light,
),
useMaterial3: true,
),
home: const Directionality(
textDirection: TextDirection.rtl,
child: LocationPage(),
),
);
}
}
class LocationPage extends StatefulWidget {
const LocationPage({super.key});
@override
State<LocationPage> createState() => _LocationPageState();
}
class _LocationPageState extends State<LocationPage> {
final Stopwatch _stopwatch = Stopwatch();
Timer? _timer;
Position? _position;
Duration _elapsed = Duration.zero;
String _status = 'دریافت خودکار موقعیت مکانی در حال شروع است…';
bool _loading = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => _getLocation());
}
@override
void dispose() {
_timer?.cancel();
_stopwatch.stop();
super.dispose();
}
void _startTimer() {
_timer?.cancel();
_elapsed = Duration.zero;
_stopwatch
..reset()
..start();
_timer = Timer.periodic(const Duration(milliseconds: 100), (_) {
if (mounted) setState(() => _elapsed = _stopwatch.elapsed);
});
}
void _stopTimer() {
_stopwatch.stop();
_timer?.cancel();
_timer = null;
_elapsed = _stopwatch.elapsed;
}
Future<void> _getLocation() async {
if (_loading) return;
_startTimer();
setState(() {
_loading = true;
_status = 'در حال بررسی مجوز و دریافت موقعیت مکانی…';
});
try {
final serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
_showError(
kIsWeb
? 'دسترسی موقعیت مکانی مرورگر یا دستگاه خاموش است.'
: 'سرویس Location دستگاه خاموش است؛ آن را روشن کنید و صفحه را دوباره باز کنید.',
);
return;
}
var permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
}
if (permission == LocationPermission.denied) {
_showError(
'مجوز موقعیت مکانی داده نشد. مجوز را از تنظیمات مرورگر فعال و صفحه را دوباره باز کنید.',
);
return;
}
if (permission == LocationPermission.deniedForever) {
_showError(
kIsWeb
? 'دسترسی Location برای این سایت مسدود شده است؛ آن را از تنظیمات سایت مرورگر فعال کنید.'
: 'مجوز Location مسدود شده است؛ آن را از تنظیمات برنامه فعال کنید.',
);
return;
}
final position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.best,
timeLimit: const Duration(seconds: 30),
);
_stopTimer();
debugPrint('---------- LOCATION RESULT ----------');
debugPrint('Latitude : ${position.latitude}');
debugPrint('Longitude: ${position.longitude}');
debugPrint('Accuracy : ${position.accuracy} meters');
debugPrint('Elapsed : ${_elapsed.inMilliseconds} ms');
debugPrint('Timestamp: ${position.timestamp.toIso8601String()}');
debugPrint('-------------------------------------');
if (!mounted) return;
setState(() {
_position = position;
_status = 'موقعیت مکانی با موفقیت دریافت شد.';
});
} on TimeoutException {
_showError(
'دریافت موقعیت بیش از ۳۰ ثانیه طول کشید؛ در فضای باز صفحه را دوباره بارگذاری کنید.',
);
} catch (error, stackTrace) {
debugPrint('Location error: $error');
debugPrintStack(stackTrace: stackTrace);
_showError(
kIsWeb
? 'دریافت موقعیت ممکن نشد. دسترسی Location مرورگر را بررسی کنید. خطا: $error'
: 'دریافت موقعیت ممکن نشد: $error',
);
} finally {
_stopTimer();
if (mounted) setState(() => _loading = false);
}
}
void _showError(String message) {
if (!mounted) return;
setState(() => _status = message);
}
String get _formattedElapsed {
final seconds = _elapsed.inMilliseconds / 1000;
return '${seconds.toStringAsFixed(1)} ثانیه';
}
@override
Widget build(BuildContext context) {
final position = _position;
return Scaffold(
appBar: AppBar(title: const Text('موقعیت مکانی من'), centerTitle: true),
body: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 560),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Icon(
position == null
? Icons.location_searching_rounded
: Icons.my_location_rounded,
size: 76,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 18),
Text(
_status,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 18),
Card(
margin: EdgeInsets.zero,
color: Theme.of(context).colorScheme.primaryContainer,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_loading) ...[
const SizedBox.square(
dimension: 18,
child: CircularProgressIndicator(strokeWidth: 2),
),
const SizedBox(width: 10),
] else
const Icon(Icons.timer_outlined),
const SizedBox(width: 8),
Text(
'${_loading ? 'زمان سپری‌شده' : 'زمان نهایی'}: $_formattedElapsed',
style: Theme.of(context).textTheme.titleMedium,
),
],
),
),
),
const SizedBox(height: 20),
if (position != null) ...[
_LocationValue(
icon: Icons.north_rounded,
label: 'عرض جغرافیایی (Latitude)',
value: position.latitude.toStringAsFixed(7),
),
const SizedBox(height: 12),
_LocationValue(
icon: Icons.east_rounded,
label: 'طول جغرافیایی (Longitude)',
value: position.longitude.toStringAsFixed(7),
),
const SizedBox(height: 12),
_LocationValue(
icon: Icons.gps_fixed_rounded,
label: 'دقت تخمینی موقعیت (Accuracy)',
value: '${position.accuracy.toStringAsFixed(2)} متر',
),
const SizedBox(height: 12),
_LocationValue(
icon: Icons.schedule_rounded,
label: 'زمان ثبت',
value: position.timestamp.toLocal().toString(),
),
const SizedBox(height: 12),
Text(
'عدد Accuracy شعاع خطای تخمینی است؛ هرچه کمتر باشد، موقعیت دقیق‌تر است.',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodySmall,
),
],
],
),
),
),
),
),
);
}
}
class _LocationValue extends StatelessWidget {
const _LocationValue({
required this.icon,
required this.label,
required this.value,
});
final IconData icon;
final String label;
final String value;
@override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.zero,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
children: [
Icon(icon, color: Theme.of(context).colorScheme.primary),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: Theme.of(context).textTheme.bodySmall),
const SizedBox(height: 3),
Directionality(
textDirection: TextDirection.ltr,
child: SelectableText(
value,
style: Theme.of(context).textTheme.titleLarge,
),
),
],
),
),
],
),
),
);
}
}