325 lines
11 KiB
Dart
325 lines
11 KiB
Dart
// Основные импорты пакетов
|
||
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()));
|
||
}
|
||
}
|