beerds-flutter/lib/main_screen.dart

325 lines
11 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Основные импорты пакетов
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart'; // Для выбора изображений
import 'package:http/http.dart' as http; // Для сетевых запросов
import 'package:permission_handler/permission_handler.dart'; // Управление разрешениями
import 'package:flutter_image_compress/flutter_image_compress.dart'; // Сжатие картинок
import 'package:path_provider/path_provider.dart'; // Получение пути
import 'dart:io'; // Для работы с файлами
import 'dart:convert'; // Для работы с JSON
import 'result_screen.dart'; // Экран с результатами распознавания
/// Главный экран приложения с выбором типа животного
class MainScreen extends StatelessWidget {
final ImagePicker _picker = ImagePicker(); // Экземпляр ImagePicker
MainScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Распознать породу')
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Кнопка для распознавания кошки
ElevatedButton(
onPressed: () async {
final status = await Permission.camera.request();
if (status.isGranted) {
_navigateToChooseSource(context, 'cats');
}
},
child: const Text('Распознать кошку'),
),
const SizedBox(height: 20),
// Кнопка для распознавания собаки
ElevatedButton(
onPressed: () async {
final status = await Permission.camera.request();
if (status.isGranted) {
_navigateToChooseSource(context, 'dogs');
}
},
child: const Text('Распознать собаку'),
),
],
),
),
);
}
/// Навигация к экрану выбора источника изображения
void _navigateToChooseSource(BuildContext context, String type) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder:
(context) => ChooseSourceScreen(
onSourceSelected:
(source) => _checkAndRequestPermission(context, source, type),
),
),
);
}
/// Проверяет и запрашивает разрешения для доступа к камере или галерее
Future<void> _checkAndRequestPermission(
BuildContext context,
ImageSource source,
String type,
) async {
Permission permission;
if (source == ImageSource.camera) {
permission = Permission.camera;
} else {
permission = Permission.photos;
}
final status = await permission.request();
if (status.isGranted) {
_pickImage(context, source, type);
} else {
_showSettingsDialog(context); // Показать диалог настроек при отказе
}
}
/// Показывает диалоговое окно с предложением открыть настройки
void _showSettingsDialog(BuildContext context) {
showDialog(
context: context,
builder:
(BuildContext context) => AlertDialog(
title: const Text('Требуется разрешение'),
content: const Text(
'Пожалуйста, предоставьте разрешение в настройках',
),
actions: [
TextButton(
child: const Text('Отмена'),
onPressed: () => Navigator.pop(context),
),
TextButton(
child: const Text('Настройки'),
onPressed: () {
openAppSettings(); // Открыть системные настройки
Navigator.pop(context);
},
),
],
),
);
}
/// Выбирает изображение из указанного источника
Future<void> _pickImage(
BuildContext context,
ImageSource source,
String type,
) async {
try {
final XFile? image = await _picker.pickImage(source: source);
if (image != null) {
// Переход на экран загрузки с выбранным изображением
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder:
(context) => LoadingScreen(
imageFile: File(image.path),
animalType: type,
),
),
);
}
} catch (e) {
if (context.mounted) {
// Показать ошибку в случае исключения
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Ошибка при выборе изображения: $e')),
);
}
}
}
}
/// Экран выбора источника изображения (камера/галерея)
class ChooseSourceScreen extends StatelessWidget {
final Function(ImageSource) onSourceSelected;
const ChooseSourceScreen({super.key, required this.onSourceSelected});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Выберите источник'),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed:
() => Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => MainScreen()),
(route) => false,
),
),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Кнопка для съемки фото
ElevatedButton(
child: const Text('Сделать фото'),
onPressed: () => onSourceSelected(ImageSource.camera),
),
const SizedBox(height: 20),
// Кнопка для выбора из галереи
ElevatedButton(
child: const Text('Выбрать из галереи'),
onPressed: () => onSourceSelected(ImageSource.gallery),
),
],
),
),
);
}
}
/// Экран загрузки и обработки изображения (МОДИФИЦИРОВАННЫЙ)
class LoadingScreen extends StatelessWidget {
final File imageFile;
final String animalType;
const LoadingScreen({
super.key,
required this.imageFile,
required this.animalType,
});
/// Отправка изображения на сервер и обработка ответа
Future<void> _uploadImage(BuildContext context) async {
// Сжимаем изображение перед отправкой
final compressedFile = await _compressImage(context, imageFile);
if (compressedFile == null) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Не удалось подготовить изображение')),
);
Navigator.pop(context);
}
return;
}
// Формирование multipart-запроса со сжатым изображением
var request = http.MultipartRequest(
'POST',
Uri.parse('https://xn-----6kcp3cadbabfh8a0a.xn--p1ai/beerds/$animalType'),
);
request.fields['type'] = animalType;
request.files.add(
await http.MultipartFile.fromPath('image', compressedFile.path),
);
try {
var response = await request.send();
if (response.statusCode == 201) {
// Успешный ответ сервера
var jsonResponse = await response.stream.bytesToString();
Map<String, dynamic> parsedJson = jsonDecode(jsonResponse);
ApiResponse apiResponse = ApiResponse.fromJson(parsedJson);
// Сортировка результатов по убыванию вероятности
var sortedEntries =
apiResponse.results.entries.toList()..sort(
(a, b) => double.parse(b.key).compareTo(double.parse(a.key)),
);
// Подготовка данных для отображения
List<double> probabilities = [];
List<String> breeds = [];
List<String> images = [];
for (var entry in sortedEntries) {
final breedName = entry.value;
final breedImages = apiResponse.images.firstWhere(
(img) => img.name == breedName,
orElse: () => BreedImages(name: breedName, url: []),
);
probabilities.add(double.parse(entry.key));
breeds.add(breedName);
images.add(breedImages.url.isNotEmpty ? breedImages.url.first : '');
}
// Переход на экран результатов
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder:
(context) => ResultScreen(
probabilities: probabilities,
breeds: breeds,
images: images,
),
),
);
} else {
if (context.mounted) {
// Обработка ошибки сервера
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Ошибка сервера: ${response.statusCode}')),
);
}
}
} catch (e) {
if (context.mounted) {
// Обработка сетевых ошибок
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Ошибка подключения: $e')));
}
}
}
/// Метод для сжатия изображения
Future<File?> _compressImage(BuildContext context, File file) async {
try {
final tempDir = await getTemporaryDirectory();
final targetPath =
'${tempDir.path}/compressed_${DateTime.now().millisecondsSinceEpoch}.jpg';
final compressedFile = await FlutterImageCompress.compressAndGetFile(
file.absolute.path,
targetPath,
quality: 70, // Качество сжатия (0-100)
minWidth: 600, // Максимальная ширина
minHeight: 600, // Максимальная высота
autoCorrectionAngle: true, // Автоповорот
keepExif: false, // Удаление EXIF данных
);
return compressedFile != null ? File(compressedFile.path) : null;
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Ошибка сжатия: $e')));
}
return null;
}
}
@override
Widget build(BuildContext context) {
_uploadImage(context);
return const Scaffold(body: Center(child: CircularProgressIndicator()));
}
}