Files
be_happy_public/lib/presentation/screens/send_photo_screen.dart

393 lines
15 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 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import '../../core/app_colors.dart';
import '../../domain/usecase/finish_ride_usecase.dart';
import '../components/custom_app_bar.dart';
import '../components/gradient_button.dart';
import '../viewmodel/send_photo_bloc.dart';
import '../event/send_photo_event.dart';
import '../state/send_photo_state.dart';
import '../../domain/usecase/upload_scooter_photos_usecase.dart';
import '../../di/service_locator.dart' as di;
class SendPhotoScreen extends StatelessWidget {
final int orderId;
const SendPhotoScreen({super.key, required this.orderId});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => SendPhotoBloc(
di.getIt<UploadScooterPhotosUsecase>(),
di.getIt<FinishRideUsecase>(),
),
child: SendPhotoView(orderId: orderId),
);
}
}
class SendPhotoView extends StatefulWidget {
final int orderId;
const SendPhotoView({super.key, required this.orderId});
@override
State<SendPhotoView> createState() => _SendPhotoViewState();
}
class _SendPhotoViewState extends State<SendPhotoView> {
final ImagePicker _imagePicker = ImagePicker();
// Метод для выбора фото (добавляет к существующим)
Future<void> _pickImage() async {
await showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (BuildContext context) {
return SafeArea(
child: Wrap(
children: [
ListTile(
leading: const Icon(Icons.camera_alt),
title: const Text('Сделать фото'),
onTap: () {
Navigator.pop(context);
_getImage(ImageSource.camera);
},
),
// ListTile(
// leading: const Icon(Icons.photo_library),
// title: const Text('Выбрать из галереи'),
// onTap: () {
// Navigator.pop(context);
// _getImage(ImageSource.gallery);
// },
// ),
ListTile(
leading: const Icon(Icons.cancel),
title: const Text('Отмена'),
onTap: () {
Navigator.pop(context);
},
),
],
),
);
},
);
}
Future<void> _getImage(ImageSource source) async {
final XFile? pickedFile = await _imagePicker.pickImage(
source: source,
imageQuality: 80,
);
if (pickedFile != null && mounted) {
final currentImages = context.read<SendPhotoBloc>().state.selectedImages;
context.read<SendPhotoBloc>().add(
PhotoSelected([...currentImages, pickedFile.path]),
);
}
}
void _removeImage(String path) {
final currentImages = context.read<SendPhotoBloc>().state.selectedImages;
final updatedList = currentImages.where((p) => p != path).toList();
context.read<SendPhotoBloc>().add(PhotoSelected(updatedList));
}
String _getTopText(int photoCount) {
if (photoCount == 0) {
return 'Для завершения аренды\nсфотографируйте самокат';
} else if (photoCount == 1) {
return 'Сделайте фото руля самоката';
} else {
return 'Отправьте фото на проверку';
}
}
@override
Widget build(BuildContext context) {
final isUploading = context.select(
(SendPhotoBloc bloc) => bloc.state.status == SendPhotoStatus.loading,
);
return Scaffold(
body: Stack(
children: [
Container(
width: double.infinity,
decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg),
child: SafeArea(
child: Column(
children: [
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: CustomAppBar(title: "Отправить фото"),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 60,
),
child: Text(
_getTopText(
context.select(
(SendPhotoBloc bloc) =>
bloc.state.selectedImages.length,
),
),
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
height: 1.4,
),
),
),
// Основной контент центрируем
Expanded(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 20,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// ... inside SingleChildScrollView -> Column children:
BlocBuilder<SendPhotoBloc, SendPhotoState>(
builder: (context, state) {
final photoCount = state.selectedImages.length;
return Wrap(
spacing: 16,
runSpacing: 16,
alignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
// Список выбранных фото
...state.selectedImages.map(
(path) => _buildPhotoThumbnail(path),
),
const SizedBox(height: 35),
// Кнопка "Добавить", если фото меньше лимита
if (photoCount < 2)
GestureDetector(
onTap: _pickImage,
child: _buildAddButton(),
),
],
);
},
),
const SizedBox(height: 100),
// Информационный блок под кнопкой добавления фото
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.25),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.white.withOpacity(0.08),
width: 1,
),
),
child: const Text(
'Перед завершением поездки убедитесь, что Вы находитесь в зоне завершения поездки фиолетового цвета.',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: 14,
height: 1.4,
),
),
),
const SizedBox(
height: 40,
), // Уменьшено со 200 для гармоничного отступа
// Кнопка отправки
BlocConsumer<SendPhotoBloc, SendPhotoState>(
listener: (context, state) {
if (state.status == SendPhotoStatus.success) {
context.go(
'/home/checkout/${widget.orderId}',
extra: state.recievedPhotoIds,
);
} else if (state.status ==
SendPhotoStatus.failure) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
state.errorMessage ?? 'Ошибка',
),
),
);
}
},
builder: (context, state) {
final photoCount = state.selectedImages.length;
final isEnabled =
photoCount >= 2 &&
state.status != SendPhotoStatus.loading;
return GradientButton(
text: 'Отправить',
onTap: isEnabled
? () => context.read<SendPhotoBloc>().add(
PhotoUploadSubmitted(widget.orderId),
)
: null,
enabled: isEnabled,
showArrows: true,
height: 56,
width: double.infinity,
fontSize: 16,
);
},
),
],
),
),
),
),
],
),
),
),
// Оверлей загрузки: показывается, пока фото отправляются на сервер
if (isUploading)
Positioned.fill(
child: AbsorbPointer(
absorbing: true,
child: Container(
color: Colors.black.withOpacity(0.45),
child: const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(color: Color(0xFF8BFFAA)),
SizedBox(height: 16),
Text(
'Загружаем фото...',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
),
),
],
),
);
}
// миниатюра с крестиком
Widget _buildPhotoThumbnail(String path) {
return Stack(
clipBehavior: Clip.none,
children: [
Container(
width: 96,
height: 96,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Image.file(File(path), fit: BoxFit.cover),
),
),
// Кнопка удаления (крестик)
Positioned(
top: -8,
right: -8,
child: GestureDetector(
onTap: () => _removeImage(path),
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Color(0xFF75FBF0),
shape: BoxShape.circle,
),
child: const Icon(
Icons.close,
size: 16,
color: Color(0xFF242F51),
),
),
),
),
],
);
}
// Виджет кнопки добавления
Widget _buildAddButton() {
return Container(
width: 96,
height: 96,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: const LinearGradient(
colors: [
Color(0xFF242F51), // полупрозрачный белый
Color(0xFF242F51), // ещё более прозрачный
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
boxShadow: [
// Глубокое свечение
BoxShadow(
color: Color(0xFF8BFFAA).withOpacity(0.5),
blurRadius: 50,
spreadRadius: 6,
offset: Offset.zero,
),
// Внутреннее свечение (для объёма)
BoxShadow(
color: Color(0xFF8BFFAA).withOpacity(0.2),
blurRadius: 10,
spreadRadius: -2,
offset: Offset.zero,
),
],
),
child: const Center(
child: Icon(Icons.add, size: 32, color: Colors.white),
),
);
}
}