Files
be_happy_public/lib/presentation/components/sheet/active_ride_sheet.dart
2026-05-29 11:40:55 +03:00

536 lines
23 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:async';
import 'dart:ui';
import 'package:be_happy/domain/entities/scooter.dart';
import 'package:bot_toast/bot_toast.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import '../../../di/service_locator.dart';
import '../../event/active_ride_event.dart';
import '../../event/map_event.dart';
import '../../state/active_ride_state.dart';
import '../../viewmodel/active_ride_bloc.dart';
import '../../viewmodel/map_bloc.dart';
import '../dialog/finish_ride_confirmation_dialog.dart';
import '../notification_toast.dart';
class ActiveRideSheet extends StatefulWidget {
final int orderId;
final String scooterNumber;
final Duration initialElapsedTime;
const ActiveRideSheet({
super.key,
required this.orderId,
required this.scooterNumber,
this.initialElapsedTime = Duration.zero,
});
@override
State<ActiveRideSheet> createState() => _ActiveRideSheetState();
}
class _ActiveRideSheetState extends State<ActiveRideSheet> {
late final ActiveRideBloc _bloc;
Timer? _localTimer;
@override
void initState() {
super.initState();
_bloc = getIt<ActiveRideBloc>();
_bloc.add(LoadScooterOrder(widget.orderId));
_localTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (mounted && !_bloc.state.isPaused) {
setState(() {});
}
});
}
@override
void dispose() {
_bloc.close();
_localTimer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _bloc,
child: BlocConsumer<ActiveRideBloc, ActiveRideState>(
listenWhen: (previous, current) => previous.inZone != current.inZone || previous.status != current.status,
listener: (context, state) {
if (!state.inZone) {
BotToast.showCustomNotification(
toastBuilder: (_) {
return NotificationToast(
title: "Вы покинули зону разрешенную для езды",
onClose: () {
BotToast.cleanAll();
},
);
},
);
}
if (state.status == ActiveRideStatus.success && state.order != null) {
final scooter = state.order!.scooter;
context.read<MapBloc>().add(FocusOnScooter(Scooter(id: scooter.id,
title: scooter.title, status: scooter.status,
latitude: state.longitude, longitude: state.latitude,
batteryLevel: scooter.batteryLevel, isOnline: scooter.isOnline,
maxSpeed: scooter.maxSpeed, number: scooter.number)));
}
},
builder: (context, state) {
// Логика отображения загрузки и ошибок остается прежней
if (state.status == ActiveRideStatus.loading && state.order == null) {
return _buildLoading();
}
if (state.status == ActiveRideStatus.failure && state.order == null) {
return _buildError(state.errorMessage);
}
return _buildContent(state);
},
),
);
}
Widget _buildLoading() {
return Align(
alignment: Alignment.bottomCenter,
child: Container(
padding: const EdgeInsets.all(40),
decoration: BoxDecoration(
color: const Color(0x00000032).withOpacity(0.6),
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
),
child: const CircularProgressIndicator(color: Colors.white),
),
);
}
Widget _buildError(String? message) {
return Align(
alignment: Alignment.bottomCenter,
child: Container(
padding: const EdgeInsets.all(40),
decoration: BoxDecoration(
color: const Color(0x00000032).withOpacity(0.6),
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
),
child: Text(
message ?? 'Ошибка загрузки',
style: const TextStyle(color: Colors.white),
),
),
);
}
Widget _buildContent(ActiveRideState state) {
final displayTime = state.isPaused
? state.elapsedTime
: state.elapsedTime + (DateTime.now().difference(DateTime.now().subtract(const Duration(seconds: 1))));
final effectiveElapsedTime = state.isPaused
? state.elapsedTime
: DateTime.now().difference(state.order?.startAt ?? state.order?.createdAt ?? DateTime.now());
return Align(
alignment: Alignment.bottomCenter,
child: ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: Container(
padding: const EdgeInsets.only(top: 20, bottom: 10),
decoration: BoxDecoration(
color: const Color(0x00000032).withOpacity(0.6),
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
children: [
GestureDetector(
onTap: () => Navigator.pop(context),
child: Row(
children: [
Icon(
Icons.arrow_back_ios_sharp,
color: const Color(0x99FFFFFF),
size: 20,
),
Icon(
Icons.arrow_back_ios_sharp,
color: const Color(0x66FFFFFF),
size: 20,
),
Icon(
Icons.arrow_back_ios_sharp,
color: const Color(0x22FFFFFF),
size: 20,
),
],
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
'Самокат ${widget.scooterNumber}',
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
const SizedBox(height: 20),
Container(
width: double.infinity,
margin: const EdgeInsets.symmetric(horizontal: 20),
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(16),
),
child: Column(
children: [
Text(
_formatDuration(effectiveElapsedTime),
style: const TextStyle(
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.bold,
fontFeatures: const [FontFeature.tabularFigures()],
fontFamily: 'DigitalNumbers',
),
),
const SizedBox(height: 4),
Text(
state.isPaused ? 'на паузе' : 'время в пути',
style: TextStyle(
color: Colors.white.withOpacity(0.6),
fontSize: 13,
),
),
],
),
),
const SizedBox(height: 20),
// 🔹 КНОПКИ УПРАВЛЕНИЯ
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
children: [
Expanded(
child: Container(
height: 56,
decoration: BoxDecoration(
gradient: state.isPaused
? null
: const LinearGradient(
colors: [Color(0xFF66E3C4), Color(0xFF4CD1B5)],
),
color: state.isPaused ? Colors.white.withOpacity(0.15) : null,
borderRadius: BorderRadius.circular(16),
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: state.status == ActiveRideStatus.loading
? null
: () {
if (state.isPaused) {
_bloc.add(ResumeRide(widget.orderId));
} else {
_bloc.add(PauseRide(widget.orderId));
}
},
borderRadius: BorderRadius.circular(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
state.isPaused ? Icons.play_arrow : Icons.pause,
color: state.isPaused
? Colors.white.withOpacity(0.8)
: const Color(0xFF0A0F2E),
size: 24,
),
const SizedBox(height: 4),
Text(
state.isPaused ? 'ПРОДОЛЖИТЬ' : 'ПАУЗА',
style: TextStyle(
color: state.isPaused
? Colors.white.withOpacity(0.8)
: const Color(0xFF0A0F2E),
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: Container(
height: 56,
decoration: BoxDecoration(
color: const Color(0xFFB84949),
borderRadius: BorderRadius.circular(16),
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: state.status == ActiveRideStatus.loading
? null
: () async {
// 🔹 Показываем диалог подтверждения
final result = await showDialog<bool>(
context: context,
builder: (context) => const FinishRideConfirmationDialog(),
);
// 🔹 Если пользователь подтвердил — завершаем и переходим
if (result == true) {
_bloc.add(FinishRide(widget.orderId));
Navigator.pop(context); // закрываем ActiveRideSheet
context.go("/home/order-photos/${widget.orderId}");
}
// 🔹 Если отменил — ничего не делаем, диалог уже закрылся
},
borderRadius: BorderRadius.circular(16),
child: Column( // ✅ Вернули Column с иконкой и текстом
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.stop,
color: Colors.white,
size: 24,
),
const SizedBox(height: 4),
const Text(
'ЗАВЕРШИТЬ',
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: Container(
height: 56,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Colors.white.withOpacity(0.2),
width: 1,
),
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
// TODO: Поддержка
},
borderRadius: BorderRadius.circular(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.phone_in_talk,
color: Colors.white.withOpacity(0.8),
size: 24,
),
const SizedBox(height: 4),
Text(
'Поддержка',
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
),
),
],
),
),
const SizedBox(height: 16),
// 🔹 СТАТИСТИКА (2 равных столбца, правый на всю высоту)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: IntrinsicHeight(
child: Row(
children: [
// 🔹 ЛЕВЫЙ СТОЛБЕЦ: Скорость + Расстояние
Expanded(
child: Column(
children: [
// Скорость
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(16),
),
child: Column(
children: [
Text(
state.speed.toStringAsFixed(1),
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'скорость',
style: TextStyle(
color: Colors.white.withOpacity(0.6),
fontSize: 12,
),
),
],
),
),
const SizedBox(height: 12),
// Расстояние
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(16),
),
child: Column(
children: [
Text(
state.distance.toStringAsFixed(1),
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'расстояние',
style: TextStyle(
color: Colors.white.withOpacity(0.6),
fontSize: 12,
),
),
],
),
),
],
),
),
const SizedBox(width: 12),
// 🔹 ПРАВЫЙ СТОЛБЕЦ: Стоимость (на всю высоту)
Expanded(
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(16),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Стоимость',
style: TextStyle(
color: Colors.white.withOpacity(0.6),
fontSize: 12,
),
),
const SizedBox(height: 8),
RichText(
text: TextSpan(
children: [
TextSpan(
text: state.cost.toStringAsFixed(1),
style: const TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
fontFamily: 'Digital Numbers',
fontFeatures: [FontFeature.tabularFigures()],
),
),
const TextSpan(
text: ' BYN',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
],
),
),
],
),
),
),
],
),
),
),
const SizedBox(height: 24),
],
),
),
),
),
);
}
String _formatDuration(Duration duration) {
String twoDigits(int n) => n.toString().padLeft(2, '0');
final hours = twoDigits(duration.inHours);
final minutes = twoDigits(duration.inMinutes.remainder(60));
final seconds = twoDigits(duration.inSeconds.remainder(60));
return '$hours:$minutes:$seconds';
}
}