some fix
# Conflicts: # lib/presentation/navigation/app_router.dart # pubspec.yaml
This commit is contained in:
@@ -1,44 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../core/app_colors.dart';
|
||||
|
||||
|
||||
class AppCheckbox extends StatelessWidget {
|
||||
final bool value;
|
||||
final Function(bool?) onChanged;
|
||||
final bool isError;
|
||||
|
||||
|
||||
const AppCheckbox({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
this.isError = false,
|
||||
});
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () => onChanged(!value),
|
||||
child: Container(
|
||||
width: 22,
|
||||
height: 22,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color: isError ? AppColors.checkboxErrorBorder : AppColors.checkboxBorder,
|
||||
width: 2,
|
||||
),
|
||||
color: value ? AppColors.checkboxFill : Colors.transparent,
|
||||
),
|
||||
child: value
|
||||
? const Icon(
|
||||
Icons.check,
|
||||
size: 16,
|
||||
color: Color(0xFF000032),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../core/app_colors.dart';
|
||||
|
||||
|
||||
class AppCheckbox extends StatelessWidget {
|
||||
final bool value;
|
||||
final Function(bool?) onChanged;
|
||||
final bool isError;
|
||||
|
||||
|
||||
const AppCheckbox({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
this.isError = false,
|
||||
});
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () => onChanged(!value),
|
||||
child: Container(
|
||||
width: 22,
|
||||
height: 22,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color: isError ? AppColors.checkboxErrorBorder : AppColors.checkboxBorder,
|
||||
width: 2,
|
||||
),
|
||||
color: value ? AppColors.checkboxFill : Colors.transparent,
|
||||
),
|
||||
child: value
|
||||
? const Icon(
|
||||
Icons.check,
|
||||
size: 16,
|
||||
color: Color(0xFF000032),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../core/app_colors.dart';
|
||||
|
||||
|
||||
class GradientButton extends StatelessWidget {
|
||||
final String text;
|
||||
final VoidCallback? onTap;
|
||||
final bool enabled;
|
||||
|
||||
|
||||
const GradientButton({
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.onTap,
|
||||
this.enabled = true,
|
||||
});
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: enabled ? onTap : null,
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 350, // Максимальная ширина кнопки
|
||||
),
|
||||
height: 66,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
gradient: enabled ? AppColors.activeButtonGradient : null,
|
||||
color: enabled ? null : AppColors.disabledButtonColor,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
text,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: enabled ? AppColors.activeButtonText : AppColors.disabledButtonText,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../core/app_colors.dart';
|
||||
|
||||
|
||||
class GradientButton extends StatelessWidget {
|
||||
final String text;
|
||||
final VoidCallback? onTap;
|
||||
final bool enabled;
|
||||
|
||||
|
||||
const GradientButton({
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.onTap,
|
||||
this.enabled = true,
|
||||
});
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: enabled ? onTap : null,
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 350, // Максимальная ширина кнопки
|
||||
),
|
||||
height: 66,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
gradient: enabled ? AppColors.activeButtonGradient : null,
|
||||
color: enabled ? null : AppColors.disabledButtonColor,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
text,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: enabled ? AppColors.activeButtonText : AppColors.disabledButtonText,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,80 +1,80 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class CardInputField extends StatelessWidget {
|
||||
final String hintText;
|
||||
final IconData? icon;
|
||||
final TextEditingController? controller;
|
||||
final ValueChanged<String>? onChanged;
|
||||
final TextInputType keyboardType;
|
||||
final List<TextInputFormatter>? inputFormatters;
|
||||
final bool obscureText;
|
||||
final int? maxLength;
|
||||
final double letterSpacing;
|
||||
final TextCapitalization textCapitalization; // ← новый параметр
|
||||
|
||||
const CardInputField({
|
||||
super.key,
|
||||
required this.hintText,
|
||||
this.icon,
|
||||
this.controller,
|
||||
this.onChanged,
|
||||
this.keyboardType = TextInputType.text,
|
||||
this.inputFormatters,
|
||||
this.obscureText = false,
|
||||
this.maxLength,
|
||||
this.letterSpacing = 0,
|
||||
this.textCapitalization = TextCapitalization.none, // ← по умолчанию none
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 46,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
onChanged: onChanged,
|
||||
keyboardType: keyboardType,
|
||||
inputFormatters: inputFormatters,
|
||||
obscureText: obscureText,
|
||||
maxLength: maxLength,
|
||||
textCapitalization: textCapitalization, // ← передаём в TextField
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
letterSpacing: letterSpacing,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: hintText,
|
||||
hintStyle: TextStyle(
|
||||
color: Colors.white.withOpacity(0.4),
|
||||
fontSize: 16,
|
||||
letterSpacing: letterSpacing,
|
||||
),
|
||||
counterText: '',
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (icon != null) ...[
|
||||
const SizedBox(width: 12),
|
||||
Icon(
|
||||
icon,
|
||||
color: Colors.white.withOpacity(0.6),
|
||||
size: 22,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class CardInputField extends StatelessWidget {
|
||||
final String hintText;
|
||||
final IconData? icon;
|
||||
final TextEditingController? controller;
|
||||
final ValueChanged<String>? onChanged;
|
||||
final TextInputType keyboardType;
|
||||
final List<TextInputFormatter>? inputFormatters;
|
||||
final bool obscureText;
|
||||
final int? maxLength;
|
||||
final double letterSpacing;
|
||||
final TextCapitalization textCapitalization; // ← новый параметр
|
||||
|
||||
const CardInputField({
|
||||
super.key,
|
||||
required this.hintText,
|
||||
this.icon,
|
||||
this.controller,
|
||||
this.onChanged,
|
||||
this.keyboardType = TextInputType.text,
|
||||
this.inputFormatters,
|
||||
this.obscureText = false,
|
||||
this.maxLength,
|
||||
this.letterSpacing = 0,
|
||||
this.textCapitalization = TextCapitalization.none, // ← по умолчанию none
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 46,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
onChanged: onChanged,
|
||||
keyboardType: keyboardType,
|
||||
inputFormatters: inputFormatters,
|
||||
obscureText: obscureText,
|
||||
maxLength: maxLength,
|
||||
textCapitalization: textCapitalization, // ← передаём в TextField
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
letterSpacing: letterSpacing,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: hintText,
|
||||
hintStyle: TextStyle(
|
||||
color: Colors.white.withOpacity(0.4),
|
||||
fontSize: 16,
|
||||
letterSpacing: letterSpacing,
|
||||
),
|
||||
counterText: '',
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (icon != null) ...[
|
||||
const SizedBox(width: 12),
|
||||
Icon(
|
||||
icon,
|
||||
color: Colors.white.withOpacity(0.6),
|
||||
size: 22,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,43 +1,43 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../core/app_colors.dart';
|
||||
|
||||
|
||||
class CodeDots extends StatelessWidget {
|
||||
final String code;
|
||||
final int length;
|
||||
final bool isError;
|
||||
|
||||
|
||||
const CodeDots({
|
||||
super.key,
|
||||
required this.code,
|
||||
required this.length,
|
||||
this.isError = false,
|
||||
});
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(length, (i) {
|
||||
bool filled = i < code.length;
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text(
|
||||
filled ? code[i] : '●',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isError
|
||||
? AppColors.pinError
|
||||
: filled
|
||||
? AppColors.smsDigit
|
||||
: AppColors.digitPlaceholder,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../core/app_colors.dart';
|
||||
|
||||
|
||||
class CodeDots extends StatelessWidget {
|
||||
final String code;
|
||||
final int length;
|
||||
final bool isError;
|
||||
|
||||
|
||||
const CodeDots({
|
||||
super.key,
|
||||
required this.code,
|
||||
required this.length,
|
||||
this.isError = false,
|
||||
});
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(length, (i) {
|
||||
bool filled = i < code.length;
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text(
|
||||
filled ? code[i] : '●',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isError
|
||||
? AppColors.pinError
|
||||
: filled
|
||||
? AppColors.smsDigit
|
||||
: AppColors.digitPlaceholder,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,244 +1,244 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:html/parser.dart' as html_parser;
|
||||
import 'package:html/dom.dart' as dom;
|
||||
import 'dart:convert';
|
||||
|
||||
/// Парсер контента новостей (поддерживает HTML и JSON-формат)
|
||||
class ContentParser {
|
||||
|
||||
/// Парсит текст новости: сначала проверяет textJson, потом text
|
||||
static List<Widget> parseContent({String? textJson, String? text}) {
|
||||
if (textJson != null && textJson.isNotEmpty) {
|
||||
return _parseJsonText(textJson);
|
||||
} else if (text != null && text.isNotEmpty) {
|
||||
return _parseHtmlText(text);
|
||||
}
|
||||
return const [];
|
||||
}
|
||||
|
||||
// 🔹 HTML-парсинг
|
||||
static List<Widget> _parseHtmlText(String htmlText) {
|
||||
final parsedHtml = html_parser.parse(htmlText);
|
||||
final List<dom.Node> elements = parsedHtml.body?.nodes ?? [];
|
||||
return _parseHtmlElements(elements);
|
||||
}
|
||||
|
||||
static List<Widget> _parseHtmlElements(List<dom.Node> nodes) {
|
||||
List<Widget> widgets = [];
|
||||
|
||||
for (final node in nodes) {
|
||||
if (node is dom.Element) {
|
||||
final text = node.text.trim();
|
||||
if (text.isEmpty) continue;
|
||||
|
||||
switch (node.localName) {
|
||||
case 'h1':
|
||||
case 'h2':
|
||||
case 'h3':
|
||||
widgets.add(_buildHeader(text));
|
||||
break;
|
||||
case 'p':
|
||||
widgets.add(_buildParagraph(text));
|
||||
break;
|
||||
case 'ul':
|
||||
widgets.add(_buildUnorderedList(node.nodes));
|
||||
break;
|
||||
default:
|
||||
widgets.add(_buildDefaultText(text));
|
||||
}
|
||||
} else if (node is dom.Text) {
|
||||
final text = node.text.trim();
|
||||
if (text.isNotEmpty) {
|
||||
widgets.add(_buildDefaultText(text));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return widgets;
|
||||
}
|
||||
|
||||
// 🔹 JSON-парсинг
|
||||
static List<Widget> _parseJsonText(String jsonString) {
|
||||
try {
|
||||
final dynamic data = jsonDecode(jsonString);
|
||||
return _parseJsonNode(data);
|
||||
} catch (e) {
|
||||
return [
|
||||
const Text(
|
||||
'Ошибка отображения текста новости',
|
||||
style: TextStyle(color: Colors.red, fontSize: 14),
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
static List<Widget> _parseJsonNode(dynamic node) {
|
||||
List<Widget> widgets = [];
|
||||
|
||||
if (node is List) {
|
||||
for (final item in node) {
|
||||
widgets.addAll(_parseJsonNode(item));
|
||||
}
|
||||
} else if (node is Map<String, dynamic>) {
|
||||
final type = node['type'] as String?;
|
||||
final content = node['content'];
|
||||
|
||||
switch (type) {
|
||||
case 'div':
|
||||
if (content is List) {
|
||||
widgets.addAll(_parseJsonNode(content));
|
||||
}
|
||||
break;
|
||||
case 'h1':
|
||||
case 'h2':
|
||||
case 'h3':
|
||||
final text = _extractJsonText(content);
|
||||
if (text != null && text.isNotEmpty) {
|
||||
widgets.add(_buildHeader(text));
|
||||
}
|
||||
break;
|
||||
case 'p':
|
||||
final text = _extractJsonText(content);
|
||||
if (text != null && text.isNotEmpty) {
|
||||
widgets.add(_buildParagraph(text));
|
||||
}
|
||||
break;
|
||||
case 'ul':
|
||||
if (content is List) {
|
||||
widgets.add(_buildJsonUnorderedList(content));
|
||||
}
|
||||
break;
|
||||
case 'li':
|
||||
final text = _extractJsonText(content);
|
||||
if (text != null && text.isNotEmpty) {
|
||||
widgets.add(_buildListItem(text));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
final text = _extractJsonText(content);
|
||||
if (text != null && text.isNotEmpty) {
|
||||
widgets.add(_buildDefaultText(text));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return widgets;
|
||||
}
|
||||
|
||||
// 🔹 Вспомогательные методы извлечения текста из JSON
|
||||
static String? _extractJsonText(dynamic content) {
|
||||
if (content == null) return null;
|
||||
if (content is List) {
|
||||
return content.map((e) => e?.toString() ?? '').where((s) => s.isNotEmpty).join(' ');
|
||||
}
|
||||
return content.toString();
|
||||
}
|
||||
|
||||
// 🔹 Виджеты для отображения (принимают НЕ nullable String)
|
||||
static Widget _buildHeader(String text) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text(
|
||||
text, // ✅ text — не nullable
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Widget _buildParagraph(String text) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 14,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Widget _buildDefaultText(String text) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Widget _buildUnorderedList(List<dom.Node> children) {
|
||||
final items = <Widget>[];
|
||||
for (final child in children) {
|
||||
if (child is dom.Element && child.localName == 'li') {
|
||||
final text = child.text.trim();
|
||||
if (text.isNotEmpty) {
|
||||
items.add(_buildListItem(text));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: items,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Widget _buildJsonUnorderedList(List<dynamic> content) {
|
||||
final items = <Widget>[];
|
||||
for (final item in content) {
|
||||
if (item is Map<String, dynamic> && item['type'] == 'li') {
|
||||
final text = _extractJsonText(item['content']);
|
||||
if (text != null && text.isNotEmpty) {
|
||||
items.add(_buildListItem(text));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: items,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Widget _buildListItem(String text) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 16, top: 4, bottom: 4),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('• ',
|
||||
style: TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 14,
|
||||
)),
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 14,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:html/parser.dart' as html_parser;
|
||||
import 'package:html/dom.dart' as dom;
|
||||
import 'dart:convert';
|
||||
|
||||
/// Парсер контента новостей (поддерживает HTML и JSON-формат)
|
||||
class ContentParser {
|
||||
|
||||
/// Парсит текст новости: сначала проверяет textJson, потом text
|
||||
static List<Widget> parseContent({String? textJson, String? text}) {
|
||||
if (textJson != null && textJson.isNotEmpty) {
|
||||
return _parseJsonText(textJson);
|
||||
} else if (text != null && text.isNotEmpty) {
|
||||
return _parseHtmlText(text);
|
||||
}
|
||||
return const [];
|
||||
}
|
||||
|
||||
// 🔹 HTML-парсинг
|
||||
static List<Widget> _parseHtmlText(String htmlText) {
|
||||
final parsedHtml = html_parser.parse(htmlText);
|
||||
final List<dom.Node> elements = parsedHtml.body?.nodes ?? [];
|
||||
return _parseHtmlElements(elements);
|
||||
}
|
||||
|
||||
static List<Widget> _parseHtmlElements(List<dom.Node> nodes) {
|
||||
List<Widget> widgets = [];
|
||||
|
||||
for (final node in nodes) {
|
||||
if (node is dom.Element) {
|
||||
final text = node.text.trim();
|
||||
if (text.isEmpty) continue;
|
||||
|
||||
switch (node.localName) {
|
||||
case 'h1':
|
||||
case 'h2':
|
||||
case 'h3':
|
||||
widgets.add(_buildHeader(text));
|
||||
break;
|
||||
case 'p':
|
||||
widgets.add(_buildParagraph(text));
|
||||
break;
|
||||
case 'ul':
|
||||
widgets.add(_buildUnorderedList(node.nodes));
|
||||
break;
|
||||
default:
|
||||
widgets.add(_buildDefaultText(text));
|
||||
}
|
||||
} else if (node is dom.Text) {
|
||||
final text = node.text.trim();
|
||||
if (text.isNotEmpty) {
|
||||
widgets.add(_buildDefaultText(text));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return widgets;
|
||||
}
|
||||
|
||||
// 🔹 JSON-парсинг
|
||||
static List<Widget> _parseJsonText(String jsonString) {
|
||||
try {
|
||||
final dynamic data = jsonDecode(jsonString);
|
||||
return _parseJsonNode(data);
|
||||
} catch (e) {
|
||||
return [
|
||||
const Text(
|
||||
'Ошибка отображения текста новости',
|
||||
style: TextStyle(color: Colors.red, fontSize: 14),
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
static List<Widget> _parseJsonNode(dynamic node) {
|
||||
List<Widget> widgets = [];
|
||||
|
||||
if (node is List) {
|
||||
for (final item in node) {
|
||||
widgets.addAll(_parseJsonNode(item));
|
||||
}
|
||||
} else if (node is Map<String, dynamic>) {
|
||||
final type = node['type'] as String?;
|
||||
final content = node['content'];
|
||||
|
||||
switch (type) {
|
||||
case 'div':
|
||||
if (content is List) {
|
||||
widgets.addAll(_parseJsonNode(content));
|
||||
}
|
||||
break;
|
||||
case 'h1':
|
||||
case 'h2':
|
||||
case 'h3':
|
||||
final text = _extractJsonText(content);
|
||||
if (text != null && text.isNotEmpty) {
|
||||
widgets.add(_buildHeader(text));
|
||||
}
|
||||
break;
|
||||
case 'p':
|
||||
final text = _extractJsonText(content);
|
||||
if (text != null && text.isNotEmpty) {
|
||||
widgets.add(_buildParagraph(text));
|
||||
}
|
||||
break;
|
||||
case 'ul':
|
||||
if (content is List) {
|
||||
widgets.add(_buildJsonUnorderedList(content));
|
||||
}
|
||||
break;
|
||||
case 'li':
|
||||
final text = _extractJsonText(content);
|
||||
if (text != null && text.isNotEmpty) {
|
||||
widgets.add(_buildListItem(text));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
final text = _extractJsonText(content);
|
||||
if (text != null && text.isNotEmpty) {
|
||||
widgets.add(_buildDefaultText(text));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return widgets;
|
||||
}
|
||||
|
||||
// 🔹 Вспомогательные методы извлечения текста из JSON
|
||||
static String? _extractJsonText(dynamic content) {
|
||||
if (content == null) return null;
|
||||
if (content is List) {
|
||||
return content.map((e) => e?.toString() ?? '').where((s) => s.isNotEmpty).join(' ');
|
||||
}
|
||||
return content.toString();
|
||||
}
|
||||
|
||||
// 🔹 Виджеты для отображения (принимают НЕ nullable String)
|
||||
static Widget _buildHeader(String text) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text(
|
||||
text, // ✅ text — не nullable
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Widget _buildParagraph(String text) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 14,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Widget _buildDefaultText(String text) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Widget _buildUnorderedList(List<dom.Node> children) {
|
||||
final items = <Widget>[];
|
||||
for (final child in children) {
|
||||
if (child is dom.Element && child.localName == 'li') {
|
||||
final text = child.text.trim();
|
||||
if (text.isNotEmpty) {
|
||||
items.add(_buildListItem(text));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: items,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Widget _buildJsonUnorderedList(List<dynamic> content) {
|
||||
final items = <Widget>[];
|
||||
for (final item in content) {
|
||||
if (item is Map<String, dynamic> && item['type'] == 'li') {
|
||||
final text = _extractJsonText(item['content']);
|
||||
if (text != null && text.isNotEmpty) {
|
||||
items.add(_buildListItem(text));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: items,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Widget _buildListItem(String text) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 16, top: 4, bottom: 4),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('• ',
|
||||
style: TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 14,
|
||||
)),
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 14,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,34 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CustomAppBar extends StatelessWidget {
|
||||
final String title;
|
||||
|
||||
const CustomAppBar({super.key, required this.title});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
InkWell(
|
||||
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: 20),
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CustomAppBar extends StatelessWidget {
|
||||
final String title;
|
||||
|
||||
const CustomAppBar({super.key, required this.title});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
InkWell(
|
||||
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: 20),
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,96 +1,96 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CancelBookingDialog extends StatelessWidget {
|
||||
const CancelBookingDialog({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
insetPadding: const EdgeInsets.symmetric(horizontal: 40),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF2E3253).withOpacity(0.95),
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
"Отменить\nбронирование?",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
"Вы действительно хотите отменить\nбронирование",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 14,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 🔹 Кнопка "Отменить" — ПЕРВАЯ, зелёная (градиент), возвращает true
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(26),
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF8EFEB5), Color(0xFF86FEF1)],
|
||||
),
|
||||
),
|
||||
child: ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context, true), // ✅ true = отменить
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.transparent,
|
||||
shadowColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(26),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
"Отменить",
|
||||
style: TextStyle(
|
||||
color: Color(0xFF1D273A), // тёмный текст на светлом градиенте
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 🔹 Кнопка "Оставить" — ВТОРАЯ, тёмная, возвращает false
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 52,
|
||||
child: ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context, false), // ❌ false = оставить
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF0D1024),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(26),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
"Оставить",
|
||||
style: TextStyle(color: Colors.white, fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CancelBookingDialog extends StatelessWidget {
|
||||
const CancelBookingDialog({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
insetPadding: const EdgeInsets.symmetric(horizontal: 40),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF2E3253).withOpacity(0.95),
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
"Отменить\nбронирование?",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
"Вы действительно хотите отменить\nбронирование",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 14,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 🔹 Кнопка "Отменить" — ПЕРВАЯ, зелёная (градиент), возвращает true
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(26),
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF8EFEB5), Color(0xFF86FEF1)],
|
||||
),
|
||||
),
|
||||
child: ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context, true), // ✅ true = отменить
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.transparent,
|
||||
shadowColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(26),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
"Отменить",
|
||||
style: TextStyle(
|
||||
color: Color(0xFF1D273A), // тёмный текст на светлом градиенте
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 🔹 Кнопка "Оставить" — ВТОРАЯ, тёмная, возвращает false
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 52,
|
||||
child: ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context, false), // ❌ false = оставить
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF0D1024),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(26),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
"Оставить",
|
||||
style: TextStyle(color: Colors.white, fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,79 +1,79 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CannotFinishRideDialog extends StatelessWidget {
|
||||
const CannotFinishRideDialog({super.key});
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
return Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
insetPadding: const EdgeInsets.symmetric(horizontal: 40),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF2E3253).withOpacity(0.95),
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
"Завершить поездку невозможно",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
"Вы находитесь далеко от парковки.",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 14,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(26),
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF8EFEB5), Color(0xFF86FEF1)],
|
||||
),
|
||||
),
|
||||
child: ElevatedButton(
|
||||
onPressed: () => {
|
||||
Navigator.pop(context)
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.transparent,
|
||||
shadowColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(26),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
"Понятно",
|
||||
style: TextStyle(
|
||||
color: Color(0xFF1D273A),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CannotFinishRideDialog extends StatelessWidget {
|
||||
const CannotFinishRideDialog({super.key});
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
return Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
insetPadding: const EdgeInsets.symmetric(horizontal: 40),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF2E3253).withOpacity(0.95),
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
"Завершить поездку невозможно",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
"Вы находитесь далеко от парковки.",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 14,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(26),
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF8EFEB5), Color(0xFF86FEF1)],
|
||||
),
|
||||
),
|
||||
child: ElevatedButton(
|
||||
onPressed: () => {
|
||||
Navigator.pop(context)
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.transparent,
|
||||
shadowColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(26),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
"Понятно",
|
||||
style: TextStyle(
|
||||
color: Color(0xFF1D273A),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,96 +1,96 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FinishRideConfirmationDialog extends StatelessWidget {
|
||||
const FinishRideConfirmationDialog({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
insetPadding: const EdgeInsets.symmetric(horizontal: 40),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF2E3253).withOpacity(0.95),
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
"Завершить поездку?",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
"Вы действительно хотите завершить поездку?",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 14,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 🔹 Кнопка "Завершить" — ПЕРВАЯ, зелёная (градиент), возвращает true
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(26),
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF8EFEB5), Color(0xFF86FEF1)],
|
||||
),
|
||||
),
|
||||
child: ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context, true), // ✅ true = завершить
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.transparent,
|
||||
shadowColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(26),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
"Завершить",
|
||||
style: TextStyle(
|
||||
color: Color(0xFF1D273A), // тёмный текст на светлом градиенте
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 🔹 Кнопка "Отмена" — ВТОРАЯ, тёмная, возвращает false
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 52,
|
||||
child: ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context, false), // ❌ false = отмена
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF0D1024),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(26),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
"Отмена",
|
||||
style: TextStyle(color: Colors.white, fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FinishRideConfirmationDialog extends StatelessWidget {
|
||||
const FinishRideConfirmationDialog({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
insetPadding: const EdgeInsets.symmetric(horizontal: 40),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF2E3253).withOpacity(0.95),
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
"Завершить поездку?",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
"Вы действительно хотите завершить поездку?",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 14,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 🔹 Кнопка "Завершить" — ПЕРВАЯ, зелёная (градиент), возвращает true
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(26),
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF8EFEB5), Color(0xFF86FEF1)],
|
||||
),
|
||||
),
|
||||
child: ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context, true), // ✅ true = завершить
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.transparent,
|
||||
shadowColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(26),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
"Завершить",
|
||||
style: TextStyle(
|
||||
color: Color(0xFF1D273A), // тёмный текст на светлом градиенте
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 🔹 Кнопка "Отмена" — ВТОРАЯ, тёмная, возвращает false
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 52,
|
||||
child: ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context, false), // ❌ false = отмена
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF0D1024),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(26),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
"Отмена",
|
||||
style: TextStyle(color: Colors.white, fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,90 +1,90 @@
|
||||
import 'package:bot_toast/bot_toast.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class FineNotificationCard extends StatelessWidget {
|
||||
final VoidCallback onClose;
|
||||
|
||||
const FineNotificationCard({
|
||||
super.key,
|
||||
required this.onClose,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.fromLTRB(24, 10, 20, 24),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF2D2B4D),
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: 64),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'У вас имеются неоплаченные штрафы',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
Text(
|
||||
'Оплатите штраф, чтобы начать поездку',
|
||||
style: TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 14,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Positioned(
|
||||
top: -20,
|
||||
left: -30,
|
||||
child: Image.asset(
|
||||
'assets/icons/card-screen.png',
|
||||
width: 120,
|
||||
height: 120,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
|
||||
Positioned(
|
||||
top: 15,
|
||||
right: 15,
|
||||
child: GestureDetector(
|
||||
onTap: onClose,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.close, color: Colors.white70, size: 20),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
import 'package:bot_toast/bot_toast.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class FineNotificationCard extends StatelessWidget {
|
||||
final VoidCallback onClose;
|
||||
|
||||
const FineNotificationCard({
|
||||
super.key,
|
||||
required this.onClose,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.fromLTRB(24, 10, 20, 24),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF2D2B4D),
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: 64),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'У вас имеются неоплаченные штрафы',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
Text(
|
||||
'Оплатите штраф, чтобы начать поездку',
|
||||
style: TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 14,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Positioned(
|
||||
top: -20,
|
||||
left: -30,
|
||||
child: Image.asset(
|
||||
'assets/icons/card-screen.png',
|
||||
width: 120,
|
||||
height: 120,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
|
||||
Positioned(
|
||||
top: 15,
|
||||
right: 15,
|
||||
child: GestureDetector(
|
||||
onTap: onClose,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.close, color: Colors.white70, size: 20),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
50
lib/presentation/components/full_screen_image_viewer.dart
Normal file
50
lib/presentation/components/full_screen_image_viewer.dart
Normal file
@@ -0,0 +1,50 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FullScreenImageViewer extends StatefulWidget {
|
||||
final List<String> photoUrls;
|
||||
final int initialIndex;
|
||||
|
||||
const FullScreenImageViewer({
|
||||
super.key,
|
||||
required this.photoUrls,
|
||||
required this.initialIndex,
|
||||
});
|
||||
|
||||
@override
|
||||
State<FullScreenImageViewer> createState() => _FullScreenImageViewerState();
|
||||
}
|
||||
|
||||
class _FullScreenImageViewerState extends State<FullScreenImageViewer> {
|
||||
late PageController _pageController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pageController = PageController(initialPage: widget.initialIndex);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
iconTheme: const IconThemeData(color: Colors.white),
|
||||
),
|
||||
body: PageView.builder(
|
||||
controller: _pageController,
|
||||
itemCount: widget.photoUrls.length,
|
||||
itemBuilder: (context, index) {
|
||||
final isNetwork = widget.photoUrls[index].startsWith('http');
|
||||
return InteractiveViewer(
|
||||
child: Center(
|
||||
child: isNetwork
|
||||
? Image.network(widget.photoUrls[index])
|
||||
: Image.asset(widget.photoUrls[index]),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,119 +1,119 @@
|
||||
// import 'package:flutter/material.dart';
|
||||
// import '../core/app_colors.dart';
|
||||
//
|
||||
//
|
||||
// class GradientButton extends StatelessWidget {
|
||||
// final String text;
|
||||
// final VoidCallback? onTap;
|
||||
// final bool enabled;
|
||||
//
|
||||
//
|
||||
// const GradientButton({
|
||||
// super.key,
|
||||
// required this.text,
|
||||
// required this.onTap,
|
||||
// this.enabled = true,
|
||||
// });
|
||||
//
|
||||
//
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return GestureDetector(
|
||||
// onTap: enabled ? onTap : null,
|
||||
// child: Container(
|
||||
// constraints: const BoxConstraints(
|
||||
// maxWidth: 350, // Максимальная ширина кнопки
|
||||
// ),
|
||||
// height: 66,
|
||||
// decoration: BoxDecoration(
|
||||
// borderRadius: BorderRadius.circular(24),
|
||||
// gradient: enabled ? AppColors.activeButtonGradient : null,
|
||||
// color: enabled ? null : AppColors.disabledButtonColor,
|
||||
// ),
|
||||
// alignment: Alignment.center,
|
||||
// child: Text(
|
||||
// text,
|
||||
// textAlign: TextAlign.center,
|
||||
// style: TextStyle(
|
||||
// color: enabled ? AppColors.activeButtonText : AppColors.disabledButtonText,
|
||||
// fontSize: 20,
|
||||
// fontWeight: FontWeight.w600,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../core/app_colors.dart';
|
||||
|
||||
class GradientButton extends StatelessWidget {
|
||||
final String text;
|
||||
final VoidCallback? onTap;
|
||||
final bool enabled;
|
||||
final bool showArrows; // Новый параметр для стрелок
|
||||
final double height; // Параметр высоты
|
||||
final double width;
|
||||
final double fontSize; // Параметр размера шрифта
|
||||
|
||||
const GradientButton({
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.onTap,
|
||||
this.enabled = true,
|
||||
this.showArrows = false,
|
||||
this.width = 220,
|
||||
this.height = 50,
|
||||
this.fontSize = 12,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color arrow1 = enabled ? const Color(0x33000032) : const Color(0x33FFFFFF);
|
||||
final Color arrow2 = enabled ? const Color(0x66000032) : const Color(0x66FFFFFF);
|
||||
final Color arrow3 = enabled ? const Color(0x99000032) : const Color(0x99FFFFFF);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: enabled ? onTap : null,
|
||||
child: Container(
|
||||
width: width,
|
||||
height: height,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
gradient: enabled ? AppColors.activeButtonGradient : null,
|
||||
color: enabled ? null : AppColors.disabledButtonColor,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: showArrows
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
text,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: enabled ? AppColors.activeButtonText : AppColors.disabledButtonText,
|
||||
fontSize: fontSize,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Icon(Icons.arrow_forward_ios_sharp, color: arrow1, size: 12),
|
||||
Icon(Icons.arrow_forward_ios_sharp, color: arrow2, size: 12),
|
||||
Icon(Icons.arrow_forward_ios_sharp, color: arrow3, size: 12),
|
||||
],
|
||||
)
|
||||
: Text(
|
||||
text,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: enabled ? AppColors.activeButtonText : AppColors.disabledButtonText,
|
||||
fontSize: fontSize,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// import 'package:flutter/material.dart';
|
||||
// import '../core/app_colors.dart';
|
||||
//
|
||||
//
|
||||
// class GradientButton extends StatelessWidget {
|
||||
// final String text;
|
||||
// final VoidCallback? onTap;
|
||||
// final bool enabled;
|
||||
//
|
||||
//
|
||||
// const GradientButton({
|
||||
// super.key,
|
||||
// required this.text,
|
||||
// required this.onTap,
|
||||
// this.enabled = true,
|
||||
// });
|
||||
//
|
||||
//
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return GestureDetector(
|
||||
// onTap: enabled ? onTap : null,
|
||||
// child: Container(
|
||||
// constraints: const BoxConstraints(
|
||||
// maxWidth: 350, // Максимальная ширина кнопки
|
||||
// ),
|
||||
// height: 66,
|
||||
// decoration: BoxDecoration(
|
||||
// borderRadius: BorderRadius.circular(24),
|
||||
// gradient: enabled ? AppColors.activeButtonGradient : null,
|
||||
// color: enabled ? null : AppColors.disabledButtonColor,
|
||||
// ),
|
||||
// alignment: Alignment.center,
|
||||
// child: Text(
|
||||
// text,
|
||||
// textAlign: TextAlign.center,
|
||||
// style: TextStyle(
|
||||
// color: enabled ? AppColors.activeButtonText : AppColors.disabledButtonText,
|
||||
// fontSize: 20,
|
||||
// fontWeight: FontWeight.w600,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../core/app_colors.dart';
|
||||
|
||||
class GradientButton extends StatelessWidget {
|
||||
final String text;
|
||||
final VoidCallback? onTap;
|
||||
final bool enabled;
|
||||
final bool showArrows; // Новый параметр для стрелок
|
||||
final double height; // Параметр высоты
|
||||
final double width;
|
||||
final double fontSize; // Параметр размера шрифта
|
||||
|
||||
const GradientButton({
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.onTap,
|
||||
this.enabled = true,
|
||||
this.showArrows = false,
|
||||
this.width = 220,
|
||||
this.height = 50,
|
||||
this.fontSize = 12,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color arrow1 = enabled ? const Color(0x33000032) : const Color(0x33FFFFFF);
|
||||
final Color arrow2 = enabled ? const Color(0x66000032) : const Color(0x66FFFFFF);
|
||||
final Color arrow3 = enabled ? const Color(0x99000032) : const Color(0x99FFFFFF);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: enabled ? onTap : null,
|
||||
child: Container(
|
||||
width: width,
|
||||
height: height,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
gradient: enabled ? AppColors.activeButtonGradient : null,
|
||||
color: enabled ? null : AppColors.disabledButtonColor,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: showArrows
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
text,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: enabled ? AppColors.activeButtonText : AppColors.disabledButtonText,
|
||||
fontSize: fontSize,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Icon(Icons.arrow_forward_ios_sharp, color: arrow1, size: 12),
|
||||
Icon(Icons.arrow_forward_ios_sharp, color: arrow2, size: 12),
|
||||
Icon(Icons.arrow_forward_ios_sharp, color: arrow3, size: 12),
|
||||
],
|
||||
)
|
||||
: Text(
|
||||
text,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: enabled ? AppColors.activeButtonText : AppColors.disabledButtonText,
|
||||
fontSize: fontSize,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,67 +1,67 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart'; // ✅ Добавлен
|
||||
|
||||
class LinkRow extends StatelessWidget {
|
||||
final String icon;
|
||||
final String title;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const LinkRow({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
child: Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
icon,
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.arrow_forward_ios_sharp, color: const Color(0x99FFFFFF), size: 12),
|
||||
Icon(Icons.arrow_forward_ios_sharp, color: const Color(0x66FFFFFF), size: 12),
|
||||
Icon(Icons.arrow_forward_ios_sharp, color: const Color(0x33FFFFFF), size: 12),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Выносим метод открытия ссылки
|
||||
void openLink(String url) async {
|
||||
final Uri uri = Uri.parse(url);
|
||||
|
||||
try {
|
||||
await launchUrl(
|
||||
uri,
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
} catch (e) {
|
||||
// TODO: Показать ошибку пользователю
|
||||
print('Не удалось открыть ссылку: $e');
|
||||
}
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart'; // ✅ Добавлен
|
||||
|
||||
class LinkRow extends StatelessWidget {
|
||||
final String icon;
|
||||
final String title;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const LinkRow({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
child: Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
icon,
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.arrow_forward_ios_sharp, color: const Color(0x99FFFFFF), size: 12),
|
||||
Icon(Icons.arrow_forward_ios_sharp, color: const Color(0x66FFFFFF), size: 12),
|
||||
Icon(Icons.arrow_forward_ios_sharp, color: const Color(0x33FFFFFF), size: 12),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Выносим метод открытия ссылки
|
||||
void openLink(String url) async {
|
||||
final Uri uri = Uri.parse(url);
|
||||
|
||||
try {
|
||||
await launchUrl(
|
||||
uri,
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
} catch (e) {
|
||||
// TODO: Показать ошибку пользователю
|
||||
print('Не удалось открыть ссылку: $e');
|
||||
}
|
||||
}
|
||||
@@ -1,97 +1,97 @@
|
||||
import 'dart:ui' as ui;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class ClusterIconPainter {
|
||||
final int clusterSize;
|
||||
|
||||
static ui.Image? _scooterImageCache;
|
||||
|
||||
const ClusterIconPainter(this.clusterSize);
|
||||
|
||||
static Future<void> initImage(String assetPath) async {
|
||||
if (_scooterImageCache != null) return;
|
||||
final data = await rootBundle.load(assetPath);
|
||||
final codec = await ui.instantiateImageCodec(data.buffer.asUint8List());
|
||||
final frame = await codec.getNextFrame();
|
||||
_scooterImageCache = frame.image;
|
||||
}
|
||||
|
||||
Future<Uint8List> getClusterIconBytes() async {
|
||||
const size = Size(180, 180);
|
||||
final recorder = ui.PictureRecorder();
|
||||
final canvas = Canvas(recorder);
|
||||
|
||||
final mainCenter = Offset(size.width * 0.45, size.height * 0.55);
|
||||
final mainRadius = size.width * 0.35;
|
||||
|
||||
final greenPaint = Paint()
|
||||
..color = const Color(0xFF8BFFAA)
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
canvas.drawCircle(mainCenter, mainRadius, greenPaint);
|
||||
|
||||
if (_scooterImageCache != null) {
|
||||
final imageSize = mainRadius * 1.3;
|
||||
final rect = Rect.fromCenter(
|
||||
center: mainCenter,
|
||||
width: imageSize,
|
||||
height: imageSize,
|
||||
);
|
||||
paintImage(
|
||||
canvas: canvas,
|
||||
image: _scooterImageCache!,
|
||||
rect: rect,
|
||||
fit: BoxFit.contain,
|
||||
);
|
||||
}
|
||||
|
||||
if (clusterSize > 1) {
|
||||
_drawBadge(canvas, mainCenter, mainRadius);
|
||||
}
|
||||
|
||||
final image = await recorder.endRecording().toImage(
|
||||
size.width.toInt(),
|
||||
size.height.toInt(),
|
||||
);
|
||||
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
||||
return byteData!.buffer.asUint8List();
|
||||
}
|
||||
|
||||
void _drawBadge(Canvas canvas, Offset mainCenter, double mainRadius) {
|
||||
final badgeCenter = Offset(
|
||||
mainCenter.dx + mainRadius * 0.7,
|
||||
mainCenter.dy - mainRadius * 0.7,
|
||||
);
|
||||
final badgeRadius = mainRadius * 0.45;
|
||||
|
||||
final badgePaint = Paint()..color = Color(0xFF000032);
|
||||
|
||||
|
||||
canvas.drawCircle(badgeCenter, badgeRadius, Paint()
|
||||
..color = Colors.black26
|
||||
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 3));
|
||||
|
||||
canvas.drawCircle(badgeCenter, badgeRadius, badgePaint);
|
||||
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: clusterSize > 99 ? '99+' : clusterSize.toString(),
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: badgeRadius * 1.1,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout();
|
||||
|
||||
textPainter.paint(
|
||||
canvas,
|
||||
Offset(
|
||||
badgeCenter.dx - textPainter.width / 2,
|
||||
badgeCenter.dy - textPainter.height / 2,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
import 'dart:ui' as ui;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class ClusterIconPainter {
|
||||
final int clusterSize;
|
||||
|
||||
static ui.Image? _scooterImageCache;
|
||||
|
||||
const ClusterIconPainter(this.clusterSize);
|
||||
|
||||
static Future<void> initImage(String assetPath) async {
|
||||
if (_scooterImageCache != null) return;
|
||||
final data = await rootBundle.load(assetPath);
|
||||
final codec = await ui.instantiateImageCodec(data.buffer.asUint8List());
|
||||
final frame = await codec.getNextFrame();
|
||||
_scooterImageCache = frame.image;
|
||||
}
|
||||
|
||||
Future<Uint8List> getClusterIconBytes() async {
|
||||
const size = Size(180, 180);
|
||||
final recorder = ui.PictureRecorder();
|
||||
final canvas = Canvas(recorder);
|
||||
|
||||
final mainCenter = Offset(size.width * 0.45, size.height * 0.55);
|
||||
final mainRadius = size.width * 0.35;
|
||||
|
||||
final greenPaint = Paint()
|
||||
..color = const Color(0xFF8BFFAA)
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
canvas.drawCircle(mainCenter, mainRadius, greenPaint);
|
||||
|
||||
if (_scooterImageCache != null) {
|
||||
final imageSize = mainRadius * 1.3;
|
||||
final rect = Rect.fromCenter(
|
||||
center: mainCenter,
|
||||
width: imageSize,
|
||||
height: imageSize,
|
||||
);
|
||||
paintImage(
|
||||
canvas: canvas,
|
||||
image: _scooterImageCache!,
|
||||
rect: rect,
|
||||
fit: BoxFit.contain,
|
||||
);
|
||||
}
|
||||
|
||||
if (clusterSize > 1) {
|
||||
_drawBadge(canvas, mainCenter, mainRadius);
|
||||
}
|
||||
|
||||
final image = await recorder.endRecording().toImage(
|
||||
size.width.toInt(),
|
||||
size.height.toInt(),
|
||||
);
|
||||
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
||||
return byteData!.buffer.asUint8List();
|
||||
}
|
||||
|
||||
void _drawBadge(Canvas canvas, Offset mainCenter, double mainRadius) {
|
||||
final badgeCenter = Offset(
|
||||
mainCenter.dx + mainRadius * 0.7,
|
||||
mainCenter.dy - mainRadius * 0.7,
|
||||
);
|
||||
final badgeRadius = mainRadius * 0.45;
|
||||
|
||||
final badgePaint = Paint()..color = Color(0xFF000032);
|
||||
|
||||
|
||||
canvas.drawCircle(badgeCenter, badgeRadius, Paint()
|
||||
..color = Colors.black26
|
||||
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 3));
|
||||
|
||||
canvas.drawCircle(badgeCenter, badgeRadius, badgePaint);
|
||||
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: clusterSize > 99 ? '99+' : clusterSize.toString(),
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: badgeRadius * 1.1,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout();
|
||||
|
||||
textPainter.paint(
|
||||
canvas,
|
||||
Offset(
|
||||
badgeCenter.dx - textPainter.width / 2,
|
||||
badgeCenter.dy - textPainter.height / 2,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,73 +1,73 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class NotificationToast extends StatelessWidget {
|
||||
final String title;
|
||||
final VoidCallback onClose;
|
||||
|
||||
const NotificationToast({required this.title, required this.onClose});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 50),
|
||||
child: Material(
|
||||
elevation: 8,
|
||||
shadowColor: Colors.black26,
|
||||
color: Colors.red,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: IntrinsicHeight(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Container(
|
||||
width: 60,
|
||||
child: Image.asset(
|
||||
'assets/icons/clichnik.png',
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Данная территория не предназначена для катания!",
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
// maxLines: 1,
|
||||
// overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
"Самокат 123-456! Вернитесь на разрешенный участок дороги",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
),
|
||||
// maxLines: 2,
|
||||
// overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.close, size: 20),
|
||||
onPressed: onClose,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class NotificationToast extends StatelessWidget {
|
||||
final String title;
|
||||
final VoidCallback onClose;
|
||||
|
||||
const NotificationToast({required this.title, required this.onClose});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 50),
|
||||
child: Material(
|
||||
elevation: 8,
|
||||
shadowColor: Colors.black26,
|
||||
color: Colors.red,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: IntrinsicHeight(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Container(
|
||||
width: 60,
|
||||
child: Image.asset(
|
||||
'assets/icons/clichnik.png',
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Данная территория не предназначена для катания!",
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
// maxLines: 1,
|
||||
// overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
"Самокат 123-456! Вернитесь на разрешенный участок дороги",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
),
|
||||
// maxLines: 2,
|
||||
// overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.close, size: 20),
|
||||
onPressed: onClose,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,131 +1,131 @@
|
||||
import 'package:bot_toast/bot_toast.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class PaymentNotificationCard extends StatelessWidget {
|
||||
final VoidCallback onBindCard;
|
||||
final VoidCallback onClose;
|
||||
|
||||
const PaymentNotificationCard({
|
||||
super.key,
|
||||
required this.onBindCard,
|
||||
required this.onClose,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.fromLTRB(24, 10, 20, 24),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF2D2B4D),
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 64),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Не привязана карта',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'Привяжите карту, чтобы начать поездку',
|
||||
style: TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 14,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
FilledButton(
|
||||
onPressed: onBindCard,
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF0D0B26),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 10,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
),
|
||||
child: const Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.credit_card,
|
||||
color: Color(0xFF80FFD1),
|
||||
size: 28,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'Привязать карту',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.add,
|
||||
color: Colors.cyanAccent,
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Positioned(
|
||||
top: -20,
|
||||
left: -30,
|
||||
child: Image.asset(
|
||||
'assets/icons/card-screen.png',
|
||||
width: 120,
|
||||
height: 120,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
|
||||
Positioned(
|
||||
top: 15,
|
||||
right: 15,
|
||||
child: GestureDetector(
|
||||
onTap: onClose,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.close, color: Colors.white70, size: 20),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
import 'package:bot_toast/bot_toast.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class PaymentNotificationCard extends StatelessWidget {
|
||||
final VoidCallback onBindCard;
|
||||
final VoidCallback onClose;
|
||||
|
||||
const PaymentNotificationCard({
|
||||
super.key,
|
||||
required this.onBindCard,
|
||||
required this.onClose,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.fromLTRB(24, 10, 20, 24),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF2D2B4D),
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 64),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Не привязана карта',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'Привяжите карту, чтобы начать поездку',
|
||||
style: TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 14,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
FilledButton(
|
||||
onPressed: onBindCard,
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF0D0B26),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 10,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
),
|
||||
child: const Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.credit_card,
|
||||
color: Color(0xFF80FFD1),
|
||||
size: 28,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'Привязать карту',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.add,
|
||||
color: Colors.cyanAccent,
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Positioned(
|
||||
top: -20,
|
||||
left: -30,
|
||||
child: Image.asset(
|
||||
'assets/icons/card-screen.png',
|
||||
width: 120,
|
||||
height: 120,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
|
||||
Positioned(
|
||||
top: 15,
|
||||
right: 15,
|
||||
child: GestureDetector(
|
||||
onTap: onClose,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.close, color: Colors.white70, size: 20),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,86 +1,86 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PaymentOption extends StatelessWidget {
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final bool isSelected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const PaymentOption({
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.isSelected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
height: 56,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x00000032).withOpacity(1),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.6),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 🔹 РАДИО-КНОПКА
|
||||
Container(
|
||||
width: 20,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? const Color(0xFF66E3C4)
|
||||
: Colors.white.withOpacity(0.4),
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: isSelected
|
||||
? const Center(
|
||||
child: SizedBox(
|
||||
width: 10,
|
||||
height: 10,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xFF66E3C4),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PaymentOption extends StatelessWidget {
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final bool isSelected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const PaymentOption({
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.isSelected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
height: 56,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x00000032).withOpacity(1),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.6),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 🔹 РАДИО-КНОПКА
|
||||
Container(
|
||||
width: 20,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? const Color(0xFF66E3C4)
|
||||
: Colors.white.withOpacity(0.4),
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: isSelected
|
||||
? const Center(
|
||||
child: SizedBox(
|
||||
width: 10,
|
||||
height: 10,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xFF66E3C4),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,37 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PeriodSelector extends StatelessWidget {
|
||||
final List<String> periods;
|
||||
final int currentIndex;
|
||||
final Function(int) onSelect;
|
||||
|
||||
PeriodSelector({required this.currentIndex, required this.onSelect, required this.periods});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: List.generate(periods.length, (index) {
|
||||
bool isSelected = currentIndex == index;
|
||||
return GestureDetector(
|
||||
onTap: () => onSelect(index),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? const Color(0xFF80FFD1) : const Color(0xFF1E2652),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
periods[index],
|
||||
style: TextStyle(
|
||||
color: isSelected ? Colors.black : Colors.white.withOpacity(0.5),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PeriodSelector extends StatelessWidget {
|
||||
final List<String> periods;
|
||||
final int currentIndex;
|
||||
final Function(int) onSelect;
|
||||
|
||||
PeriodSelector({required this.currentIndex, required this.onSelect, required this.periods});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: List.generate(periods.length, (index) {
|
||||
bool isSelected = currentIndex == index;
|
||||
return GestureDetector(
|
||||
onTap: () => onSelect(index),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? const Color(0xFF80FFD1) : const Color(0xFF1E2652),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
periods[index],
|
||||
style: TextStyle(
|
||||
color: isSelected ? Colors.black : Colors.white.withOpacity(0.5),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,170 +1,170 @@
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BatteryIndicator extends StatelessWidget {
|
||||
final double percent;
|
||||
|
||||
const BatteryIndicator({super.key, required this.percent});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 400,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
CustomPaint(
|
||||
size: const Size(320, 320),
|
||||
painter: _BatteryRingPainter(percent: percent),
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Заряд батареи',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'${(percent * 100).toInt()}%',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 48,
|
||||
fontWeight: FontWeight.w300,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BatteryRingPainter extends CustomPainter {
|
||||
final double percent;
|
||||
|
||||
_BatteryRingPainter({required this.percent});
|
||||
|
||||
// 🔹 Возвращает цвета и stops для текущего диапазона
|
||||
(List<Color>, List<double>?) _getGradientForPercent() {
|
||||
final p = percent * 100;
|
||||
|
||||
if (p >= 51) {
|
||||
return (
|
||||
const [
|
||||
Color(0xFF86EFAC),
|
||||
Color(0xFF67E8F9),
|
||||
Color(0xFF86EFAC),
|
||||
],
|
||||
const [0.0, 0.5, 1.0],
|
||||
);
|
||||
} else if (p >= 16) {
|
||||
return (
|
||||
const [
|
||||
Color(0xFFF1FF8B),
|
||||
Color(0xFF8BFFAA),
|
||||
Color(0xFFF1FF8B),
|
||||
],
|
||||
const [0.0, 0.5, 1.0],
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
const [
|
||||
Color(0xFFFF5757),
|
||||
Color(0xFFF1FF8B),
|
||||
Color(0xFFFF5757),
|
||||
],
|
||||
const [0.0, 0.5, 1.0],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final center = size.center(Offset.zero);
|
||||
final radius = size.width / 2 - 20;
|
||||
|
||||
// Фоновое кольцо
|
||||
final backgroundPaint = Paint()
|
||||
..color = Colors.white.withOpacity(0.08)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 40;
|
||||
canvas.drawCircle(center, radius, backgroundPaint);
|
||||
|
||||
// Деления
|
||||
final tickPaint = Paint()
|
||||
..color = Colors.white.withOpacity(0.15)
|
||||
..strokeWidth = 2;
|
||||
for (int i = 0; i < 100; i++) {
|
||||
final angle = 2 * pi * i / 100 - pi / 2;
|
||||
final isMajor = i % 10 == 0;
|
||||
|
||||
final innerRadius = radius - 40;
|
||||
final outerRadius = isMajor ? radius - 28 : radius - 32;
|
||||
|
||||
final p1 = Offset(
|
||||
center.dx + cos(angle) * innerRadius,
|
||||
center.dy + sin(angle) * innerRadius,
|
||||
);
|
||||
final p2 = Offset(
|
||||
center.dx + cos(angle) * outerRadius,
|
||||
center.dy + sin(angle) * outerRadius,
|
||||
);
|
||||
|
||||
canvas.drawLine(p1, p2, tickPaint);
|
||||
}
|
||||
|
||||
// Получаем градиент по проценту
|
||||
final (colors, stops) = _getGradientForPercent();
|
||||
|
||||
// Glow дуга
|
||||
final glowPaint = Paint()
|
||||
..shader = SweepGradient(
|
||||
colors: colors,
|
||||
stops: stops,
|
||||
).createShader(Rect.fromCircle(center: center, radius: radius))
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 25
|
||||
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 85)
|
||||
..strokeCap = StrokeCap.round;
|
||||
|
||||
canvas.drawArc(
|
||||
Rect.fromCircle(center: center, radius: radius),
|
||||
-pi / 2,
|
||||
2 * pi * percent,
|
||||
false,
|
||||
glowPaint,
|
||||
);
|
||||
|
||||
// Основная дуга
|
||||
final progressPaint = Paint()
|
||||
..shader = SweepGradient(
|
||||
colors: colors,
|
||||
stops: stops,
|
||||
).createShader(Rect.fromCircle(center: center, radius: radius))
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 20
|
||||
..strokeCap = StrokeCap.round;
|
||||
|
||||
canvas.drawArc(
|
||||
Rect.fromCircle(center: center, radius: radius),
|
||||
-pi / 2,
|
||||
2 * pi * percent,
|
||||
false,
|
||||
progressPaint,
|
||||
);
|
||||
|
||||
// Внутренний круг
|
||||
final innerCircle = Paint()
|
||||
..color = const Color(0xFF16233F)
|
||||
..style = PaintingStyle.fill;
|
||||
canvas.drawCircle(center, radius - 60, innerCircle);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BatteryIndicator extends StatelessWidget {
|
||||
final double percent;
|
||||
|
||||
const BatteryIndicator({super.key, required this.percent});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 400,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
CustomPaint(
|
||||
size: const Size(320, 320),
|
||||
painter: _BatteryRingPainter(percent: percent),
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Заряд батареи',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'${(percent * 100).toInt()}%',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 48,
|
||||
fontWeight: FontWeight.w300,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BatteryRingPainter extends CustomPainter {
|
||||
final double percent;
|
||||
|
||||
_BatteryRingPainter({required this.percent});
|
||||
|
||||
// 🔹 Возвращает цвета и stops для текущего диапазона
|
||||
(List<Color>, List<double>?) _getGradientForPercent() {
|
||||
final p = percent * 100;
|
||||
|
||||
if (p >= 51) {
|
||||
return (
|
||||
const [
|
||||
Color(0xFF86EFAC),
|
||||
Color(0xFF67E8F9),
|
||||
Color(0xFF86EFAC),
|
||||
],
|
||||
const [0.0, 0.5, 1.0],
|
||||
);
|
||||
} else if (p >= 16) {
|
||||
return (
|
||||
const [
|
||||
Color(0xFFF1FF8B),
|
||||
Color(0xFF8BFFAA),
|
||||
Color(0xFFF1FF8B),
|
||||
],
|
||||
const [0.0, 0.5, 1.0],
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
const [
|
||||
Color(0xFFFF5757),
|
||||
Color(0xFFF1FF8B),
|
||||
Color(0xFFFF5757),
|
||||
],
|
||||
const [0.0, 0.5, 1.0],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final center = size.center(Offset.zero);
|
||||
final radius = size.width / 2 - 20;
|
||||
|
||||
// Фоновое кольцо
|
||||
final backgroundPaint = Paint()
|
||||
..color = Colors.white.withOpacity(0.08)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 40;
|
||||
canvas.drawCircle(center, radius, backgroundPaint);
|
||||
|
||||
// Деления
|
||||
final tickPaint = Paint()
|
||||
..color = Colors.white.withOpacity(0.15)
|
||||
..strokeWidth = 2;
|
||||
for (int i = 0; i < 100; i++) {
|
||||
final angle = 2 * pi * i / 100 - pi / 2;
|
||||
final isMajor = i % 10 == 0;
|
||||
|
||||
final innerRadius = radius - 40;
|
||||
final outerRadius = isMajor ? radius - 28 : radius - 32;
|
||||
|
||||
final p1 = Offset(
|
||||
center.dx + cos(angle) * innerRadius,
|
||||
center.dy + sin(angle) * innerRadius,
|
||||
);
|
||||
final p2 = Offset(
|
||||
center.dx + cos(angle) * outerRadius,
|
||||
center.dy + sin(angle) * outerRadius,
|
||||
);
|
||||
|
||||
canvas.drawLine(p1, p2, tickPaint);
|
||||
}
|
||||
|
||||
// Получаем градиент по проценту
|
||||
final (colors, stops) = _getGradientForPercent();
|
||||
|
||||
// Glow дуга
|
||||
final glowPaint = Paint()
|
||||
..shader = SweepGradient(
|
||||
colors: colors,
|
||||
stops: stops,
|
||||
).createShader(Rect.fromCircle(center: center, radius: radius))
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 25
|
||||
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 85)
|
||||
..strokeCap = StrokeCap.round;
|
||||
|
||||
canvas.drawArc(
|
||||
Rect.fromCircle(center: center, radius: radius),
|
||||
-pi / 2,
|
||||
2 * pi * percent,
|
||||
false,
|
||||
glowPaint,
|
||||
);
|
||||
|
||||
// Основная дуга
|
||||
final progressPaint = Paint()
|
||||
..shader = SweepGradient(
|
||||
colors: colors,
|
||||
stops: stops,
|
||||
).createShader(Rect.fromCircle(center: center, radius: radius))
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 20
|
||||
..strokeCap = StrokeCap.round;
|
||||
|
||||
canvas.drawArc(
|
||||
Rect.fromCircle(center: center, radius: radius),
|
||||
-pi / 2,
|
||||
2 * pi * percent,
|
||||
false,
|
||||
progressPaint,
|
||||
);
|
||||
|
||||
// Внутренний круг
|
||||
final innerCircle = Paint()
|
||||
..color = const Color(0xFF16233F)
|
||||
..style = PaintingStyle.fill;
|
||||
canvas.drawCircle(center, radius - 60, innerCircle);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
||||
}
|
||||
@@ -1,95 +1,95 @@
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MiniBatteryIndicator extends StatelessWidget {
|
||||
final int percent;
|
||||
|
||||
const MiniBatteryIndicator({super.key, required this.percent});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: CustomPaint(
|
||||
painter: _MiniBatteryRingPainter(percent: percent),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MiniBatteryRingPainter extends CustomPainter {
|
||||
final int percent;
|
||||
|
||||
_MiniBatteryRingPainter({required this.percent});
|
||||
|
||||
(List<Color>, List<double>?) _getGradientForPercent() {
|
||||
final p = percent;
|
||||
|
||||
if (p >= 51) {
|
||||
return (
|
||||
const [
|
||||
Color(0xFF86EFAC),
|
||||
Color(0xFF67E8F9),
|
||||
Color(0xFF86EFAC),
|
||||
],
|
||||
const [0.0, 0.5, 1.0],
|
||||
);
|
||||
} else if (p >= 16) {
|
||||
return (
|
||||
const [
|
||||
Color(0xFFF1FF8B),
|
||||
Color(0xFF8BFFAA),
|
||||
Color(0xFFF1FF8B),
|
||||
],
|
||||
const [0.0, 0.5, 1.0],
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
const [
|
||||
Color(0xFFFF5757),
|
||||
Color(0xFFF1FF8B),
|
||||
Color(0xFFFF5757),
|
||||
],
|
||||
const [0.0, 0.5, 1.0],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final center = size.center(Offset.zero);
|
||||
final radius = size.width / 2 - 4; // поменьше
|
||||
|
||||
// Фоновое кольцо
|
||||
final backgroundPaint = Paint()
|
||||
..color = Colors.white.withOpacity(0.08)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 4;
|
||||
canvas.drawCircle(center, radius, backgroundPaint);
|
||||
|
||||
// Получаем цвета
|
||||
final (colors, stops) = _getGradientForPercent();
|
||||
|
||||
// Основная дуга
|
||||
final progressPaint = Paint()
|
||||
..shader = SweepGradient(
|
||||
colors: colors,
|
||||
stops: stops,
|
||||
).createShader(Rect.fromCircle(center: center, radius: radius))
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 4
|
||||
..strokeCap = StrokeCap.round;
|
||||
|
||||
canvas.drawArc(
|
||||
Rect.fromCircle(center: center, radius: radius),
|
||||
-pi / 2,
|
||||
2 * pi * (percent / 100),
|
||||
false,
|
||||
progressPaint,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
||||
}
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MiniBatteryIndicator extends StatelessWidget {
|
||||
final int percent;
|
||||
|
||||
const MiniBatteryIndicator({super.key, required this.percent});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: CustomPaint(
|
||||
painter: _MiniBatteryRingPainter(percent: percent),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MiniBatteryRingPainter extends CustomPainter {
|
||||
final int percent;
|
||||
|
||||
_MiniBatteryRingPainter({required this.percent});
|
||||
|
||||
(List<Color>, List<double>?) _getGradientForPercent() {
|
||||
final p = percent;
|
||||
|
||||
if (p >= 51) {
|
||||
return (
|
||||
const [
|
||||
Color(0xFF86EFAC),
|
||||
Color(0xFF67E8F9),
|
||||
Color(0xFF86EFAC),
|
||||
],
|
||||
const [0.0, 0.5, 1.0],
|
||||
);
|
||||
} else if (p >= 16) {
|
||||
return (
|
||||
const [
|
||||
Color(0xFFF1FF8B),
|
||||
Color(0xFF8BFFAA),
|
||||
Color(0xFFF1FF8B),
|
||||
],
|
||||
const [0.0, 0.5, 1.0],
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
const [
|
||||
Color(0xFFFF5757),
|
||||
Color(0xFFF1FF8B),
|
||||
Color(0xFFFF5757),
|
||||
],
|
||||
const [0.0, 0.5, 1.0],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final center = size.center(Offset.zero);
|
||||
final radius = size.width / 2 - 4; // поменьше
|
||||
|
||||
// Фоновое кольцо
|
||||
final backgroundPaint = Paint()
|
||||
..color = Colors.white.withOpacity(0.08)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 4;
|
||||
canvas.drawCircle(center, radius, backgroundPaint);
|
||||
|
||||
// Получаем цвета
|
||||
final (colors, stops) = _getGradientForPercent();
|
||||
|
||||
// Основная дуга
|
||||
final progressPaint = Paint()
|
||||
..shader = SweepGradient(
|
||||
colors: colors,
|
||||
stops: stops,
|
||||
).createShader(Rect.fromCircle(center: center, radius: radius))
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 4
|
||||
..strokeCap = StrokeCap.round;
|
||||
|
||||
canvas.drawArc(
|
||||
Rect.fromCircle(center: center, radius: radius),
|
||||
-pi / 2,
|
||||
2 * pi * (percent / 100),
|
||||
false,
|
||||
progressPaint,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
||||
}
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../core/app_colors.dart';
|
||||
|
||||
class ScooterInfoItem extends StatelessWidget {
|
||||
final String icon;
|
||||
final String text;
|
||||
|
||||
const ScooterInfoItem({super.key, required this.icon, required this.text});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
icon,
|
||||
height: 20,
|
||||
width: 20,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../core/app_colors.dart';
|
||||
|
||||
class ScooterInfoItem extends StatelessWidget {
|
||||
final String icon;
|
||||
final String text;
|
||||
|
||||
const ScooterInfoItem({super.key, required this.icon, required this.text});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
icon,
|
||||
height: 20,
|
||||
width: 20,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,87 +1,87 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'scooter_info_item.dart';
|
||||
|
||||
class ScooterInfoSection extends StatelessWidget {
|
||||
final double distance;
|
||||
final String location;
|
||||
final int batteryLevel;
|
||||
|
||||
const ScooterInfoSection({
|
||||
super.key,
|
||||
required this.distance,
|
||||
required this.location,
|
||||
required this.batteryLevel,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 1. Расчет оставшегося расстояния (макс. 62 км)
|
||||
final double remainingKm = (62 * batteryLevel) / 100;
|
||||
|
||||
// Красиво форматируем: если число целое — убираем точку (например, 31 км вместо 31.0 км)
|
||||
final String kmText = remainingKm % 1 == 0
|
||||
? '${remainingKm.toInt()} км'
|
||||
: '${remainingKm.toStringAsFixed(1)} км';
|
||||
|
||||
// 2. Расчет оставшегося времени (макс. 2.5 часа = 150 минут)
|
||||
final int totalMinutes = ((150 * batteryLevel) / 100).round();
|
||||
final int hours = totalMinutes ~/ 60;
|
||||
final int minutes = totalMinutes % 60;
|
||||
|
||||
// Формируем строку времени в зависимости от получившихся значений
|
||||
String timeText = '';
|
||||
if (hours > 0 && minutes > 0) {
|
||||
timeText = '$hours ч $minutes мин';
|
||||
} else if (hours > 0) {
|
||||
timeText = '$hours ч';
|
||||
} else {
|
||||
timeText = '$minutes мин';
|
||||
}
|
||||
|
||||
final int walkingMinutes = (distance / 80).round().clamp(1, 999);
|
||||
final int walkHours = walkingMinutes ~/ 60;
|
||||
final int walkMins = walkingMinutes % 60;
|
||||
|
||||
String walkingText = '';
|
||||
if (walkHours > 0 && walkMins > 0) {
|
||||
walkingText = '$walkHours ч $walkMins мин';
|
||||
} else if (walkHours > 0) {
|
||||
walkingText = '$walkHours ч';
|
||||
} else {
|
||||
walkingText = '$walkingMinutes мин';
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// Динамическая строка с зарядом
|
||||
ScooterInfoItem(
|
||||
icon: 'assets/icons/bolt.png',
|
||||
text: '$kmText или $timeText',
|
||||
),
|
||||
ScooterInfoItem(
|
||||
icon: 'assets/icons/speed.png',
|
||||
text: 'max = 25 км/ч',
|
||||
),
|
||||
ScooterInfoItem(
|
||||
icon: 'assets/icons/location.png',
|
||||
text: location,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ScooterInfoItem(
|
||||
icon: 'assets/icons/person.png',
|
||||
text: distance.toInt() >= 1000
|
||||
? '${((distance.toInt()) / 1000).toStringAsFixed(1)} km'
|
||||
: '${distance.toInt()} m',
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
ScooterInfoItem(
|
||||
icon: 'assets/icons/time.png',
|
||||
text: walkingText,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
import 'package:flutter/material.dart';
|
||||
import 'scooter_info_item.dart';
|
||||
|
||||
class ScooterInfoSection extends StatelessWidget {
|
||||
final double distance;
|
||||
final String location;
|
||||
final int batteryLevel;
|
||||
|
||||
const ScooterInfoSection({
|
||||
super.key,
|
||||
required this.distance,
|
||||
required this.location,
|
||||
required this.batteryLevel,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 1. Расчет оставшегося расстояния (макс. 62 км)
|
||||
final double remainingKm = (62 * batteryLevel) / 100;
|
||||
|
||||
// Красиво форматируем: если число целое — убираем точку (например, 31 км вместо 31.0 км)
|
||||
final String kmText = remainingKm % 1 == 0
|
||||
? '${remainingKm.toInt()} км'
|
||||
: '${remainingKm.toStringAsFixed(1)} км';
|
||||
|
||||
// 2. Расчет оставшегося времени (макс. 2.5 часа = 150 минут)
|
||||
final int totalMinutes = ((150 * batteryLevel) / 100).round();
|
||||
final int hours = totalMinutes ~/ 60;
|
||||
final int minutes = totalMinutes % 60;
|
||||
|
||||
// Формируем строку времени в зависимости от получившихся значений
|
||||
String timeText = '';
|
||||
if (hours > 0 && minutes > 0) {
|
||||
timeText = '$hours ч $minutes мин';
|
||||
} else if (hours > 0) {
|
||||
timeText = '$hours ч';
|
||||
} else {
|
||||
timeText = '$minutes мин';
|
||||
}
|
||||
|
||||
final int walkingMinutes = (distance / 80).round().clamp(1, 999);
|
||||
final int walkHours = walkingMinutes ~/ 60;
|
||||
final int walkMins = walkingMinutes % 60;
|
||||
|
||||
String walkingText = '';
|
||||
if (walkHours > 0 && walkMins > 0) {
|
||||
walkingText = '$walkHours ч $walkMins мин';
|
||||
} else if (walkHours > 0) {
|
||||
walkingText = '$walkHours ч';
|
||||
} else {
|
||||
walkingText = '$walkingMinutes мин';
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// Динамическая строка с зарядом
|
||||
ScooterInfoItem(
|
||||
icon: 'assets/icons/bolt.png',
|
||||
text: '$kmText или $timeText',
|
||||
),
|
||||
ScooterInfoItem(
|
||||
icon: 'assets/icons/speed.png',
|
||||
text: 'max = 25 км/ч',
|
||||
),
|
||||
ScooterInfoItem(
|
||||
icon: 'assets/icons/location.png',
|
||||
text: location,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ScooterInfoItem(
|
||||
icon: 'assets/icons/person.png',
|
||||
text: distance.toInt() >= 1000
|
||||
? '${((distance.toInt()) / 1000).toStringAsFixed(1)} km'
|
||||
: '${distance.toInt()} m',
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
ScooterInfoItem(
|
||||
icon: 'assets/icons/time.png',
|
||||
text: walkingText,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,172 +1,172 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SlideToReserveButton extends StatefulWidget {
|
||||
final VoidCallback onSlideComplete;
|
||||
|
||||
const SlideToReserveButton({
|
||||
super.key,
|
||||
required this.onSlideComplete,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SlideToReserveButton> createState() => _SlideToReserveButtonState();
|
||||
}
|
||||
|
||||
class _SlideToReserveButtonState extends State<SlideToReserveButton>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _dragAnimation;
|
||||
double _dragOffset = 0;
|
||||
final double _maxDrag = 240; // ширина кнопки - ширина круга
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
);
|
||||
_dragAnimation = Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onDragUpdate(DragUpdateDetails details) {
|
||||
setState(() {
|
||||
_dragOffset += details.delta.dx;
|
||||
if (_dragOffset < 0) _dragOffset = 0;
|
||||
if (_dragOffset > _maxDrag) _dragOffset = _maxDrag;
|
||||
});
|
||||
}
|
||||
|
||||
void _onDragEnd(DragEndDetails details) {
|
||||
if (_dragOffset >= _maxDrag * 0.8) {
|
||||
_controller.forward();
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
widget.onSlideComplete();
|
||||
});
|
||||
} else {
|
||||
_controller.reverse();
|
||||
setState(() {
|
||||
_dragOffset = 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isSlided = _dragOffset >= _maxDrag * 0.8;
|
||||
final progress = _dragOffset / _maxDrag;
|
||||
|
||||
return SizedBox(
|
||||
height: 64,
|
||||
child: Stack(
|
||||
children: [
|
||||
// Фон кнопки: темный → градиент
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(40),
|
||||
color: isSlided
|
||||
? Colors.transparent
|
||||
: const Color(0xFF141530),
|
||||
),
|
||||
),
|
||||
|
||||
// Градиентный фон (появляется при свайпе)
|
||||
if (isSlided)
|
||||
ShaderMask(
|
||||
shaderCallback: (Rect bounds) {
|
||||
return const LinearGradient(
|
||||
colors: [Color(0xFF67E8F9), Color(0xFF86EFAC)],
|
||||
).createShader(bounds);
|
||||
},
|
||||
blendMode: BlendMode.srcIn,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(40),
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Текст «Забронировать» — виден только в начальном состоянии
|
||||
if (!isSlided)
|
||||
Center(
|
||||
child: Text(
|
||||
'Забронировать',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Три стрелки — справа от текста (только когда текст виден)
|
||||
if (!isSlided)
|
||||
Positioned(
|
||||
right: 40,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.arrow_forward_ios, size: 12, color: Colors.white),
|
||||
Icon(Icons.arrow_forward_ios, size: 12, color: Colors.white.withOpacity(0.6)),
|
||||
Icon(Icons.arrow_forward_ios, size: 12, color: Colors.white.withOpacity(0.3)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Круг с иконкой самоката — двигается слева направо
|
||||
Transform.translate(
|
||||
offset: Offset(_dragOffset, 0),
|
||||
child: SizedBox(
|
||||
width: 56,
|
||||
height: 56,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
Icons.electric_scooter,
|
||||
color: Color(0xFF141530),
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Иконка самоката справа — появляется при полном свайпе
|
||||
if (isSlided)
|
||||
Positioned(
|
||||
right: 16,
|
||||
child: SizedBox(
|
||||
width: 32,
|
||||
height: 32,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
Icons.electric_scooter,
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SlideToReserveButton extends StatefulWidget {
|
||||
final VoidCallback onSlideComplete;
|
||||
|
||||
const SlideToReserveButton({
|
||||
super.key,
|
||||
required this.onSlideComplete,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SlideToReserveButton> createState() => _SlideToReserveButtonState();
|
||||
}
|
||||
|
||||
class _SlideToReserveButtonState extends State<SlideToReserveButton>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _dragAnimation;
|
||||
double _dragOffset = 0;
|
||||
final double _maxDrag = 240; // ширина кнопки - ширина круга
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
);
|
||||
_dragAnimation = Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onDragUpdate(DragUpdateDetails details) {
|
||||
setState(() {
|
||||
_dragOffset += details.delta.dx;
|
||||
if (_dragOffset < 0) _dragOffset = 0;
|
||||
if (_dragOffset > _maxDrag) _dragOffset = _maxDrag;
|
||||
});
|
||||
}
|
||||
|
||||
void _onDragEnd(DragEndDetails details) {
|
||||
if (_dragOffset >= _maxDrag * 0.8) {
|
||||
_controller.forward();
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
widget.onSlideComplete();
|
||||
});
|
||||
} else {
|
||||
_controller.reverse();
|
||||
setState(() {
|
||||
_dragOffset = 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isSlided = _dragOffset >= _maxDrag * 0.8;
|
||||
final progress = _dragOffset / _maxDrag;
|
||||
|
||||
return SizedBox(
|
||||
height: 64,
|
||||
child: Stack(
|
||||
children: [
|
||||
// Фон кнопки: темный → градиент
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(40),
|
||||
color: isSlided
|
||||
? Colors.transparent
|
||||
: const Color(0xFF141530),
|
||||
),
|
||||
),
|
||||
|
||||
// Градиентный фон (появляется при свайпе)
|
||||
if (isSlided)
|
||||
ShaderMask(
|
||||
shaderCallback: (Rect bounds) {
|
||||
return const LinearGradient(
|
||||
colors: [Color(0xFF67E8F9), Color(0xFF86EFAC)],
|
||||
).createShader(bounds);
|
||||
},
|
||||
blendMode: BlendMode.srcIn,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(40),
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Текст «Забронировать» — виден только в начальном состоянии
|
||||
if (!isSlided)
|
||||
Center(
|
||||
child: Text(
|
||||
'Забронировать',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Три стрелки — справа от текста (только когда текст виден)
|
||||
if (!isSlided)
|
||||
Positioned(
|
||||
right: 40,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.arrow_forward_ios, size: 12, color: Colors.white),
|
||||
Icon(Icons.arrow_forward_ios, size: 12, color: Colors.white.withOpacity(0.6)),
|
||||
Icon(Icons.arrow_forward_ios, size: 12, color: Colors.white.withOpacity(0.3)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Круг с иконкой самоката — двигается слева направо
|
||||
Transform.translate(
|
||||
offset: Offset(_dragOffset, 0),
|
||||
child: SizedBox(
|
||||
width: 56,
|
||||
height: 56,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
Icons.electric_scooter,
|
||||
color: Color(0xFF141530),
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Иконка самоката справа — появляется при полном свайпе
|
||||
if (isSlided)
|
||||
Positioned(
|
||||
right: 16,
|
||||
child: SizedBox(
|
||||
width: 32,
|
||||
height: 32,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
Icons.electric_scooter,
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,271 +1,268 @@
|
||||
import 'dart:ui';
|
||||
import 'package:be_happy/presentation/components/scooter/mini_battery_indicator.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../domain/entities/scooter.dart';
|
||||
import '../state/scooter_detail_modal_state.dart';
|
||||
import '../viewmodel/scooter_detail_modal_bloc.dart';
|
||||
import 'gradient_button.dart';
|
||||
|
||||
class ScooterData {
|
||||
final String distance;
|
||||
final String number;
|
||||
final double batteryPercent;
|
||||
|
||||
ScooterData({
|
||||
required this.distance,
|
||||
required this.number,
|
||||
required this.batteryPercent,
|
||||
});
|
||||
}
|
||||
|
||||
class ScooterBottomSheet extends StatefulWidget {
|
||||
|
||||
const ScooterBottomSheet({super.key});
|
||||
|
||||
@override
|
||||
State<ScooterBottomSheet> createState() => _ScooterBottomSheetState();
|
||||
}
|
||||
|
||||
class _ScooterBottomSheetState extends State<ScooterBottomSheet> {
|
||||
|
||||
final PageController _pageController = PageController(viewportFraction: 0.5);
|
||||
double _currentPage = 0;
|
||||
|
||||
_ScooterBottomSheetState();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pageController.addListener(() {
|
||||
setState(() {
|
||||
_currentPage = _pageController.page ?? 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<ScooterDetailModalBloc, ScooterDetailModalState>(
|
||||
builder: (context, state) {
|
||||
if (state.status == ScooterDetailModalStatus.loading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(color: Colors.white),
|
||||
);
|
||||
}
|
||||
|
||||
if (state.status == ScooterDetailModalStatus.success) {
|
||||
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(
|
||||
height: 320,
|
||||
padding: const EdgeInsets.only(top: 20, bottom: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF000032).withOpacity(0.88),
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header с адресом (без изменений)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: const Icon(Icons.arrow_back_ios, color: Colors.white),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
state.address ?? "Unknown address",
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// PageView с динамическим списком
|
||||
SizedBox(
|
||||
height: 220,
|
||||
child: PageView.builder(
|
||||
controller: _pageController,
|
||||
padEnds: false, // Оставляем false, чтобы первый элемент прилипал к левому краю
|
||||
itemCount: state.scooters!.length,
|
||||
itemBuilder: (context, index) {
|
||||
final scooter = state.scooters![index];
|
||||
final diff = (_currentPage - index).abs();
|
||||
final isActive = diff < 0.5;
|
||||
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
// 2. Добавляем левый отступ ТОЛЬКО для первого элемента,
|
||||
// чтобы он совпадал с заголовком
|
||||
margin: EdgeInsets.only(
|
||||
left: index == 0 ? 20 : 0,
|
||||
right: 16,
|
||||
),
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Transform.scale(
|
||||
scale: isActive ? 1.0 : 0.9,
|
||||
child: _ScooterCard(
|
||||
scooter: scooter,
|
||||
isActive: isActive,
|
||||
address: state.address ?? "Unknown address",
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Center(child: Text("Error"));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ScooterCard extends StatelessWidget {
|
||||
final Scooter scooter;
|
||||
final bool isActive;
|
||||
final String address;
|
||||
|
||||
const _ScooterCard({required this.scooter, required this.isActive, required this.address});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Container(
|
||||
width: 220,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Colors.white.withOpacity(isActive ? 0.35 : 0.25),
|
||||
Colors.white.withOpacity(isActive ? 0.25 : 0.18),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
border: Border.all(color: Colors.white.withOpacity(0.4), width: 1),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.location_on_outlined,
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
(scooter.distance?.toInt() ?? 0) >= 1000
|
||||
? '${((scooter.distance?.toInt() ?? 0) / 1000).toStringAsFixed(1)} km'
|
||||
: '${scooter.distance?.toInt() ?? 0} m',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.qr_code_2, color: Colors.white, size: 18),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
scooter.number,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
MiniBatteryIndicator(percent: scooter.batteryLevel),
|
||||
const SizedBox(width: 8),
|
||||
Transform.translate(
|
||||
offset: const Offset(-40, 0),
|
||||
child: Text(
|
||||
'${(scooter.batteryLevel)}%',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
context.go('/home/scooter/${scooter.id}', extra: [address, scooter.distance]);
|
||||
},
|
||||
child: Container(
|
||||
height: 42,
|
||||
alignment: Alignment.center,
|
||||
child: GradientButton(
|
||||
text: "Подробнеe",
|
||||
showArrows: true,
|
||||
height: 32,
|
||||
width: double.infinity,
|
||||
fontSize: 12,
|
||||
onTap: () {
|
||||
context.go('/home/scooter/${scooter.id}', extra: [address, scooter.distance]);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Positioned(
|
||||
right: isActive ? -30 : -5,
|
||||
top: isActive ? -10 : 15,
|
||||
child: SizedBox(
|
||||
height: isActive ? 170 : 120,
|
||||
child: Image.asset(
|
||||
"assets/icons/e6a5dcb6a3e2ec2362c25ea49509ab10d2312b19-reverse.png",
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
import 'dart:ui';
|
||||
import 'package:be_happy/presentation/components/scooter/mini_battery_indicator.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../domain/entities/scooter.dart';
|
||||
import '../state/scooter_detail_modal_state.dart';
|
||||
import '../viewmodel/scooter_detail_modal_bloc.dart';
|
||||
import 'gradient_button.dart';
|
||||
|
||||
class ScooterData {
|
||||
final String distance;
|
||||
final String number;
|
||||
final double batteryPercent;
|
||||
|
||||
ScooterData({
|
||||
required this.distance,
|
||||
required this.number,
|
||||
required this.batteryPercent,
|
||||
});
|
||||
}
|
||||
|
||||
class ScooterBottomSheet extends StatefulWidget {
|
||||
|
||||
const ScooterBottomSheet({super.key});
|
||||
|
||||
@override
|
||||
State<ScooterBottomSheet> createState() => _ScooterBottomSheetState();
|
||||
}
|
||||
|
||||
class _ScooterBottomSheetState extends State<ScooterBottomSheet> {
|
||||
|
||||
final PageController _pageController = PageController(viewportFraction: 0.5);
|
||||
double _currentPage = 0;
|
||||
|
||||
_ScooterBottomSheetState();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pageController.addListener(() {
|
||||
setState(() {
|
||||
_currentPage = _pageController.page ?? 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<ScooterDetailModalBloc, ScooterDetailModalState>(
|
||||
builder: (context, state) {
|
||||
if (state.status == ScooterDetailModalStatus.loading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(color: Colors.white),
|
||||
);
|
||||
}
|
||||
|
||||
if (state.status == ScooterDetailModalStatus.success) {
|
||||
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(
|
||||
height: 320,
|
||||
padding: const EdgeInsets.only(top: 20, bottom: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF000032).withOpacity(0.88),
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header с адресом (без изменений)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: const Icon(Icons.arrow_back_ios, color: Colors.white),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
state.address ?? "Unknown address",
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
SizedBox(
|
||||
height: 220,
|
||||
child: PageView.builder(
|
||||
controller: _pageController,
|
||||
padEnds: false, // Оставляем false, чтобы первый элемент прилипал к левому краю
|
||||
itemCount: state.scooters!.length,
|
||||
itemBuilder: (context, index) {
|
||||
final scooter = state.scooters![index];
|
||||
final diff = (_currentPage - index).abs();
|
||||
final isActive = diff < 0.5;
|
||||
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
margin: EdgeInsets.only(
|
||||
left: index == 0 ? 20 : 0,
|
||||
right: 16,
|
||||
),
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Transform.scale(
|
||||
scale: isActive ? 1.0 : 0.9,
|
||||
child: _ScooterCard(
|
||||
scooter: scooter,
|
||||
isActive: isActive,
|
||||
address: state.address ?? "Unknown address",
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Center(child: Text("Error"));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ScooterCard extends StatelessWidget {
|
||||
final Scooter scooter;
|
||||
final bool isActive;
|
||||
final String address;
|
||||
|
||||
const _ScooterCard({required this.scooter, required this.isActive, required this.address});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Container(
|
||||
width: 220,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Colors.white.withOpacity(isActive ? 0.35 : 0.25),
|
||||
Colors.white.withOpacity(isActive ? 0.25 : 0.18),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
border: Border.all(color: Colors.white.withOpacity(0.4), width: 1),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.location_on_outlined,
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
(scooter.distance?.toInt() ?? 0) >= 1000
|
||||
? '${((scooter.distance?.toInt() ?? 0) / 1000).toStringAsFixed(1)} km'
|
||||
: '${scooter.distance?.toInt() ?? 0} m',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.qr_code_2, color: Colors.white, size: 18),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
scooter.number,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
MiniBatteryIndicator(percent: scooter.batteryLevel),
|
||||
const SizedBox(width: 8),
|
||||
Transform.translate(
|
||||
offset: const Offset(-40, 0),
|
||||
child: Text(
|
||||
'${(scooter.batteryLevel)}%',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
context.go('/home/scooter/${scooter.id}', extra: [address, scooter.distance]);
|
||||
},
|
||||
child: Container(
|
||||
height: 42,
|
||||
alignment: Alignment.center,
|
||||
child: GradientButton(
|
||||
text: "Подробнеe",
|
||||
showArrows: true,
|
||||
height: 32,
|
||||
width: double.infinity,
|
||||
fontSize: 12,
|
||||
onTap: () {
|
||||
context.go('/home/scooter/${scooter.id}', extra: [address, scooter.distance]);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Positioned(
|
||||
right: isActive ? -30 : -5,
|
||||
top: isActive ? -10 : 15,
|
||||
child: SizedBox(
|
||||
height: isActive ? 170 : 120,
|
||||
child: Image.asset(
|
||||
"assets/icons/e6a5dcb6a3e2ec2362c25ea49509ab10d2312b19-reverse.png",
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,433 +1,433 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../di/service_locator.dart';
|
||||
import '../../../domain/entities/scooter_order.dart';
|
||||
import '../../event/current_rides_event.dart';
|
||||
import '../../state/current_rides_state.dart';
|
||||
import '../../viewmodel/current_rides_bloc.dart';
|
||||
import '../gradient_button.dart';
|
||||
import 'reserved_ride_sheet.dart';
|
||||
import 'active_ride_sheet.dart';
|
||||
|
||||
class CurrentRidesSheet extends StatefulWidget {
|
||||
|
||||
const CurrentRidesSheet({super.key});
|
||||
|
||||
@override
|
||||
State<CurrentRidesSheet> createState() => _CurrentRidesSheetState();
|
||||
}
|
||||
|
||||
class _CurrentRidesSheetState extends State<CurrentRidesSheet> {
|
||||
late final CurrentRidesBloc _bloc;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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(
|
||||
height: 450,
|
||||
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(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
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(
|
||||
'Текущие поездки',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Expanded(
|
||||
child: BlocBuilder<CurrentRidesBloc, CurrentRidesState>(
|
||||
builder: (context, state) {
|
||||
if (state.status == CurrentRidesStatus.loading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(color: Colors.white),
|
||||
);
|
||||
}
|
||||
|
||||
if (state.status == CurrentRidesStatus.failure) {
|
||||
return Center(
|
||||
child: Text(
|
||||
state.errorMessage ?? 'Ошибка загрузки',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state.orders.isEmpty) {
|
||||
return const Center(
|
||||
child: Text(
|
||||
'Нет активных поездок',
|
||||
style: TextStyle(color: Colors.white70),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
children: state.orders.map((order) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: _RideCard(order: order),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Container(
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(
|
||||
color: Colors.white.withOpacity(0.4),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'Взять ещё самокат',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RideCard extends StatefulWidget {
|
||||
final ScooterOrder order;
|
||||
|
||||
const _RideCard({required this.order});
|
||||
|
||||
@override
|
||||
State<_RideCard> createState() => _RideCardState();
|
||||
}
|
||||
|
||||
class _RideCardState extends State<_RideCard> {
|
||||
late Timer _timer;
|
||||
late Duration _elapsedTime;
|
||||
late Duration _reservationTime;
|
||||
late DateTime _startTime;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_startTime = widget.order.startAt ?? widget.order.createdAt;
|
||||
_elapsedTime = DateTime.now().difference(_startTime);
|
||||
_reservationTime = const Duration(minutes: 5);
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_elapsedTime = DateTime.now().difference(_startTime);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isReserved =
|
||||
widget.order.status == 'Booking' || //Drive, Finish
|
||||
widget.order.status == 'holding';
|
||||
Duration displayTime;
|
||||
if (isReserved) {
|
||||
displayTime = _reservationTime - _elapsedTime;
|
||||
if (displayTime.isNegative) {
|
||||
displayTime = Duration.zero;
|
||||
}
|
||||
} else {
|
||||
displayTime = _elapsedTime;
|
||||
}
|
||||
|
||||
final isExpired = isReserved && (_reservationTime - _elapsedTime).isNegative;
|
||||
|
||||
final timeString = _formatDuration(displayTime);
|
||||
final statusText = widget.order.status == 'Booking' ? 'Забронирован' : "Активный";
|
||||
final statusColor = widget.order.status == 'Booking' ? Color(0xFFFFCC00) : Color(0xFF8bffaa);
|
||||
|
||||
final scooterNumber =
|
||||
widget.order.scooter.number ?? widget.order.scooterId.toString();
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 70,
|
||||
child: Image.asset(
|
||||
'assets/icons/e6a5dcb6a3e2ec2362c25ea49509ab10d2312b19-reverse.png',
|
||||
height: 70,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
statusText,
|
||||
style: TextStyle(
|
||||
color: statusColor,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Image.asset("assets/icons/qr_icon_order.png"),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
scooterNumber,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
if (isReserved)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
"Тариф",
|
||||
style: TextStyle(
|
||||
color: Color(0xFF8bffaa),
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset("assets/icons/timer.png", width: 14,),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
widget.order.plan?.title ?? "Название тарифа",
|
||||
style: TextStyle(
|
||||
color: Color(0xFF8bffaa),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
timeString,
|
||||
style: TextStyle(
|
||||
color: statusColor,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
fontFamily: 'DigitalNumbers',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
height: 32,
|
||||
child: GradientButton(
|
||||
text: 'Подробнее',
|
||||
showArrows: false,
|
||||
height: 32,
|
||||
width: 100,
|
||||
fontSize: 11,
|
||||
enabled: !isExpired,
|
||||
onTap: !isExpired ? () {
|
||||
if (isReserved) {
|
||||
Navigator.pop(context);
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => ReservedRideSheet(
|
||||
orderId: widget.order.id,
|
||||
scooterNumber: scooterNumber,
|
||||
initialReservationTime:
|
||||
_reservationTime - _elapsedTime,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
Navigator.pop(context);
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => ActiveRideSheet(
|
||||
orderId: widget.order.id,
|
||||
scooterNumber: scooterNumber,
|
||||
initialElapsedTime: _elapsedTime,
|
||||
),
|
||||
);
|
||||
}
|
||||
} : null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getStatusText(String status) {
|
||||
switch (status.toLowerCase()) {
|
||||
case 'reserved':
|
||||
case 'holding':
|
||||
return 'Забронирован';
|
||||
case 'active':
|
||||
case 'in_progress':
|
||||
return 'Активно';
|
||||
case 'completed':
|
||||
return 'Завершено';
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
Color _getStatusColor(String status) {
|
||||
switch (status.toLowerCase()) {
|
||||
case 'reserved':
|
||||
case 'holding':
|
||||
return const Color(0xFFFFB800);
|
||||
case 'active':
|
||||
case 'in_progress':
|
||||
return const Color(0xFF66E3C4);
|
||||
default:
|
||||
return Colors.white;
|
||||
}
|
||||
}
|
||||
|
||||
String _getLocationText() {
|
||||
/*if (widget.order.scooter != null && widget.order.scooter!.address != null) {
|
||||
return widget.order.scooter!.address!;
|
||||
}*/
|
||||
return 'Московский 33';
|
||||
}
|
||||
|
||||
String _formatDuration(Duration duration) {
|
||||
String twoDigits(int n) => n.toString().padLeft(2, '0');
|
||||
final minutes = twoDigits(duration.inMinutes.remainder(60));
|
||||
final seconds = twoDigits(duration.inSeconds.remainder(60));
|
||||
return '$minutes:$seconds';
|
||||
}
|
||||
}
|
||||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../di/service_locator.dart';
|
||||
import '../../../domain/entities/scooter_order.dart';
|
||||
import '../../event/current_rides_event.dart';
|
||||
import '../../state/current_rides_state.dart';
|
||||
import '../../viewmodel/current_rides_bloc.dart';
|
||||
import '../gradient_button.dart';
|
||||
import 'reserved_ride_sheet.dart';
|
||||
import 'active_ride_sheet.dart';
|
||||
|
||||
class CurrentRidesSheet extends StatefulWidget {
|
||||
|
||||
const CurrentRidesSheet({super.key});
|
||||
|
||||
@override
|
||||
State<CurrentRidesSheet> createState() => _CurrentRidesSheetState();
|
||||
}
|
||||
|
||||
class _CurrentRidesSheetState extends State<CurrentRidesSheet> {
|
||||
late final CurrentRidesBloc _bloc;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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(
|
||||
height: 450,
|
||||
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(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
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(
|
||||
'Текущие поездки',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Expanded(
|
||||
child: BlocBuilder<CurrentRidesBloc, CurrentRidesState>(
|
||||
builder: (context, state) {
|
||||
if (state.status == CurrentRidesStatus.loading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(color: Colors.white),
|
||||
);
|
||||
}
|
||||
|
||||
if (state.status == CurrentRidesStatus.failure) {
|
||||
return Center(
|
||||
child: Text(
|
||||
state.errorMessage ?? 'Ошибка загрузки',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state.orders.isEmpty) {
|
||||
return const Center(
|
||||
child: Text(
|
||||
'Нет активных поездок',
|
||||
style: TextStyle(color: Colors.white70),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
children: state.orders.map((order) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: _RideCard(order: order),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Container(
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(
|
||||
color: Colors.white.withOpacity(0.4),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'Взять ещё самокат',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RideCard extends StatefulWidget {
|
||||
final ScooterOrder order;
|
||||
|
||||
const _RideCard({required this.order});
|
||||
|
||||
@override
|
||||
State<_RideCard> createState() => _RideCardState();
|
||||
}
|
||||
|
||||
class _RideCardState extends State<_RideCard> {
|
||||
late Timer _timer;
|
||||
late Duration _elapsedTime;
|
||||
late Duration _reservationTime;
|
||||
late DateTime _startTime;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_startTime = widget.order.startAt ?? widget.order.createdAt;
|
||||
_elapsedTime = DateTime.now().difference(_startTime);
|
||||
_reservationTime = const Duration(minutes: 5);
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_elapsedTime = DateTime.now().difference(_startTime);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isReserved =
|
||||
widget.order.status == 'Booking' || //Drive, Finish
|
||||
widget.order.status == 'holding';
|
||||
Duration displayTime;
|
||||
if (isReserved) {
|
||||
displayTime = _reservationTime - _elapsedTime;
|
||||
if (displayTime.isNegative) {
|
||||
displayTime = Duration.zero;
|
||||
}
|
||||
} else {
|
||||
displayTime = _elapsedTime;
|
||||
}
|
||||
|
||||
final isExpired = isReserved && (_reservationTime - _elapsedTime).isNegative;
|
||||
|
||||
final timeString = _formatDuration(displayTime);
|
||||
final statusText = widget.order.status == 'Booking' ? 'Забронирован' : "Активный";
|
||||
final statusColor = widget.order.status == 'Booking' ? Color(0xFFFFCC00) : Color(0xFF8bffaa);
|
||||
|
||||
final scooterNumber =
|
||||
widget.order.scooter.number ?? widget.order.scooterId.toString();
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 70,
|
||||
child: Image.asset(
|
||||
'assets/icons/e6a5dcb6a3e2ec2362c25ea49509ab10d2312b19-reverse.png',
|
||||
height: 70,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
statusText,
|
||||
style: TextStyle(
|
||||
color: statusColor,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Image.asset("assets/icons/qr_icon_order.png"),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
scooterNumber,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
if (isReserved)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
"Тариф",
|
||||
style: TextStyle(
|
||||
color: Color(0xFF8bffaa),
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset("assets/icons/timer.png", width: 14,),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
widget.order.plan?.title ?? "Название тарифа",
|
||||
style: TextStyle(
|
||||
color: Color(0xFF8bffaa),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
timeString,
|
||||
style: TextStyle(
|
||||
color: statusColor,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
fontFamily: 'DigitalNumbers',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
height: 32,
|
||||
child: GradientButton(
|
||||
text: 'Подробнее',
|
||||
showArrows: false,
|
||||
height: 32,
|
||||
width: 100,
|
||||
fontSize: 11,
|
||||
enabled: !isExpired,
|
||||
onTap: !isExpired ? () {
|
||||
if (isReserved) {
|
||||
Navigator.pop(context);
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => ReservedRideSheet(
|
||||
orderId: widget.order.id,
|
||||
scooterNumber: scooterNumber,
|
||||
initialReservationTime:
|
||||
_reservationTime - _elapsedTime,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
Navigator.pop(context);
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => ActiveRideSheet(
|
||||
orderId: widget.order.id,
|
||||
scooterNumber: scooterNumber,
|
||||
initialElapsedTime: _elapsedTime,
|
||||
),
|
||||
);
|
||||
}
|
||||
} : null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getStatusText(String status) {
|
||||
switch (status.toLowerCase()) {
|
||||
case 'reserved':
|
||||
case 'holding':
|
||||
return 'Забронирован';
|
||||
case 'active':
|
||||
case 'in_progress':
|
||||
return 'Активно';
|
||||
case 'completed':
|
||||
return 'Завершено';
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
Color _getStatusColor(String status) {
|
||||
switch (status.toLowerCase()) {
|
||||
case 'reserved':
|
||||
case 'holding':
|
||||
return const Color(0xFFFFB800);
|
||||
case 'active':
|
||||
case 'in_progress':
|
||||
return const Color(0xFF66E3C4);
|
||||
default:
|
||||
return Colors.white;
|
||||
}
|
||||
}
|
||||
|
||||
String _getLocationText() {
|
||||
/*if (widget.order.scooter != null && widget.order.scooter!.address != null) {
|
||||
return widget.order.scooter!.address!;
|
||||
}*/
|
||||
return 'Московский 33';
|
||||
}
|
||||
|
||||
String _formatDuration(Duration duration) {
|
||||
String twoDigits(int n) => n.toString().padLeft(2, '0');
|
||||
final minutes = twoDigits(duration.inMinutes.remainder(60));
|
||||
final seconds = twoDigits(duration.inSeconds.remainder(60));
|
||||
return '$minutes:$seconds';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,132 +1,132 @@
|
||||
import 'dart:ui';
|
||||
import 'package:be_happy/presentation/event/map_event.dart';
|
||||
import 'package:be_happy/presentation/event/map_settings_modal_event.dart';
|
||||
import 'package:be_happy/presentation/state/map_settings_modal_state.dart';
|
||||
import 'package:be_happy/presentation/viewmodel/map_settings_modal_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../viewmodel/map_bloc.dart';
|
||||
|
||||
class MapSettingsSheet extends StatelessWidget {
|
||||
final VoidCallback? onClose;
|
||||
|
||||
const MapSettingsSheet({super.key, this.onClose});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<MapSettingsModalBloc, MapSettingsModalState>(
|
||||
builder: (context, state) {
|
||||
final List<_SettingItemData> items = [
|
||||
_SettingItemData(
|
||||
label: 'Геозоны',
|
||||
icon: Icons.gps_fixed_outlined,
|
||||
color: const Color(0xFF86EFAC),
|
||||
isActive: state.isAllGeozonesActive,
|
||||
onChanged: (val) => context.read<MapSettingsModalBloc>().add(AllGeozonesToggled(val)),
|
||||
),
|
||||
_SettingItemData(
|
||||
label: 'Парковка',
|
||||
icon: Icons.home_outlined,
|
||||
color: const Color(0xFFA78BFA),
|
||||
isActive: state.isParkingZoneActive,
|
||||
onChanged: (val) => context.read<MapSettingsModalBloc>().add(ParkingZonesToggled(val)),
|
||||
),
|
||||
_SettingItemData(
|
||||
label: 'Парковка запрещена',
|
||||
icon: Icons.block_outlined,
|
||||
color: const Color(0xFFF59E0B),
|
||||
isActive: state.isRestrictedParkingZoneActive,
|
||||
onChanged: (val) => context.read<MapSettingsModalBloc>().add(RestrictedParkingZonesToggled(val)),
|
||||
),
|
||||
_SettingItemData(
|
||||
label: 'Запрещено кататься',
|
||||
icon: Icons.warning_amber_outlined,
|
||||
color: const Color(0xFFEF4444),
|
||||
isActive: state.isRestrictedDrivingZoneActive,
|
||||
onChanged: (val) => context.read<MapSettingsModalBloc>().add(RestrictedDrivingZonesToggled(val)),
|
||||
),
|
||||
];
|
||||
|
||||
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(
|
||||
height: 365,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF000032).withOpacity(0.88),
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Параметры карты',
|
||||
style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.read<MapSettingsModalBloc>().add(ApllyButtonClick());
|
||||
context.read<MapBloc>().add(UpdateMap());
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text(
|
||||
'Готово',
|
||||
style: TextStyle(color: Color(0xFF66E3C4), fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
return ListTile(
|
||||
leading: Icon(item.icon, color: item.color),
|
||||
title: Text(item.label, style: const TextStyle(color: Colors.white)),
|
||||
trailing: Switch.adaptive(
|
||||
value: item.isActive,
|
||||
onChanged: item.onChanged,
|
||||
activeTrackColor: const Color(0xFF66E3C4),
|
||||
inactiveThumbColor: Colors.white,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Вспомогательный класс для описания строк
|
||||
class _SettingItemData {
|
||||
final String label;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final bool isActive;
|
||||
final ValueChanged<bool> onChanged;
|
||||
|
||||
_SettingItemData({
|
||||
required this.label,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
required this.isActive,
|
||||
required this.onChanged,
|
||||
});
|
||||
}
|
||||
import 'dart:ui';
|
||||
import 'package:be_happy/presentation/event/map_event.dart';
|
||||
import 'package:be_happy/presentation/event/map_settings_modal_event.dart';
|
||||
import 'package:be_happy/presentation/state/map_settings_modal_state.dart';
|
||||
import 'package:be_happy/presentation/viewmodel/map_settings_modal_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../viewmodel/map_bloc.dart';
|
||||
|
||||
class MapSettingsSheet extends StatelessWidget {
|
||||
final VoidCallback? onClose;
|
||||
|
||||
const MapSettingsSheet({super.key, this.onClose});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<MapSettingsModalBloc, MapSettingsModalState>(
|
||||
builder: (context, state) {
|
||||
final List<_SettingItemData> items = [
|
||||
_SettingItemData(
|
||||
label: 'Геозоны',
|
||||
icon: Icons.gps_fixed_outlined,
|
||||
color: const Color(0xFF86EFAC),
|
||||
isActive: state.isAllGeozonesActive,
|
||||
onChanged: (val) => context.read<MapSettingsModalBloc>().add(AllGeozonesToggled(val)),
|
||||
),
|
||||
_SettingItemData(
|
||||
label: 'Парковка',
|
||||
icon: Icons.home_outlined,
|
||||
color: const Color(0xFFA78BFA),
|
||||
isActive: state.isParkingZoneActive,
|
||||
onChanged: (val) => context.read<MapSettingsModalBloc>().add(ParkingZonesToggled(val)),
|
||||
),
|
||||
_SettingItemData(
|
||||
label: 'Парковка запрещена',
|
||||
icon: Icons.block_outlined,
|
||||
color: const Color(0xFFF59E0B),
|
||||
isActive: state.isRestrictedParkingZoneActive,
|
||||
onChanged: (val) => context.read<MapSettingsModalBloc>().add(RestrictedParkingZonesToggled(val)),
|
||||
),
|
||||
_SettingItemData(
|
||||
label: 'Запрещено кататься',
|
||||
icon: Icons.warning_amber_outlined,
|
||||
color: const Color(0xFFEF4444),
|
||||
isActive: state.isRestrictedDrivingZoneActive,
|
||||
onChanged: (val) => context.read<MapSettingsModalBloc>().add(RestrictedDrivingZonesToggled(val)),
|
||||
),
|
||||
];
|
||||
|
||||
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(
|
||||
height: 365,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF000032).withOpacity(0.88),
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Параметры карты',
|
||||
style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.read<MapSettingsModalBloc>().add(ApllyButtonClick());
|
||||
context.read<MapBloc>().add(UpdateMap());
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text(
|
||||
'Готово',
|
||||
style: TextStyle(color: Color(0xFF66E3C4), fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
return ListTile(
|
||||
leading: Icon(item.icon, color: item.color),
|
||||
title: Text(item.label, style: const TextStyle(color: Colors.white)),
|
||||
trailing: Switch.adaptive(
|
||||
value: item.isActive,
|
||||
onChanged: item.onChanged,
|
||||
activeTrackColor: const Color(0xFF66E3C4),
|
||||
inactiveThumbColor: Colors.white,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Вспомогательный класс для описания строк
|
||||
class _SettingItemData {
|
||||
final String label;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final bool isActive;
|
||||
final ValueChanged<bool> onChanged;
|
||||
|
||||
_SettingItemData({
|
||||
required this.label,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
required this.isActive,
|
||||
required this.onChanged,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,202 +1,202 @@
|
||||
import 'dart:ui';
|
||||
import 'package:be_happy/presentation/event/tariff_sheet_event.dart';
|
||||
import 'package:be_happy/presentation/viewmodel/tariff_sheet_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:be_happy/presentation/components/payment_option.dart';
|
||||
|
||||
import '../../../domain/entities/payment_card.dart';
|
||||
import '../../event/payment_method_sheet_event.dart';
|
||||
import '../../state/payment_method_sheet_state.dart';
|
||||
import '../../viewmodel/payment_method_sheet_bloc.dart';
|
||||
|
||||
class PaymentMethodSheet extends StatefulWidget {
|
||||
final PaymentCard? initialSelectedCard;
|
||||
final bool showBalance;
|
||||
|
||||
const PaymentMethodSheet({
|
||||
super.key,
|
||||
this.initialSelectedCard,
|
||||
this.showBalance = true,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PaymentMethodSheet> createState() => _PaymentMethodSheetState();
|
||||
}
|
||||
|
||||
class _PaymentMethodSheetState extends State<PaymentMethodSheet> {
|
||||
int? _selectedPaymentMethod = -2;
|
||||
int? _processingCardId; // <-- Переменная для отслеживания отправленного запроса
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
context.read<PaymentMethodSheetBloc>().add(PaymentMethodSheetStarted());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<PaymentMethodSheetBloc, PaymentMethodSheetState>(
|
||||
listener: (context, state) {
|
||||
if (state.status == PaymentMethodSheetStatus.success && _processingCardId != null) {
|
||||
final updatedCard = state.cards.cast<PaymentCard?>().firstWhere(
|
||||
(c) => c?.id == _processingCardId,
|
||||
orElse: () => state.cards.cast<PaymentCard?>().firstWhere((c) => c?.isMain == true, orElse: () => null),
|
||||
);
|
||||
|
||||
_processingCardId = null;
|
||||
Navigator.pop(context, updatedCard);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
|
||||
if (state.status == PaymentMethodSheetStatus.success && _selectedPaymentMethod == -2) {
|
||||
if (widget.initialSelectedCard != null) {
|
||||
final initialIndex = state.cards.indexWhere(
|
||||
(card) => card.cardLastNumber == widget.initialSelectedCard!.cardLastNumber
|
||||
);
|
||||
_selectedPaymentMethod = initialIndex != -1 ? initialIndex : -1;
|
||||
} else {
|
||||
final mainCardIndex = state.cards.indexWhere((card) => card.isMain);
|
||||
_selectedPaymentMethod = mainCardIndex != -1 ? mainCardIndex : (widget.showBalance ? -1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
height: 450,
|
||||
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(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
children: [
|
||||
if (widget.showBalance) ...[
|
||||
PaymentOption(
|
||||
title: 'Баланс',
|
||||
subtitle: '${state.balance} баллов',
|
||||
isSelected: _selectedPaymentMethod == -1,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_selectedPaymentMethod = -1;
|
||||
});
|
||||
Navigator.pop(context, 'balance');
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
|
||||
...state.cards.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final card = entry.value;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: PaymentOption(
|
||||
title: card.isMain ? "Основная" : "Карта",
|
||||
subtitle: '${card.cardLastNumber}',
|
||||
isSelected: _selectedPaymentMethod == index,
|
||||
onTap: () async {
|
||||
setState(() {
|
||||
_selectedPaymentMethod = index;
|
||||
_processingCardId = card.id;
|
||||
});
|
||||
|
||||
// Отправляем событие в Блок
|
||||
context.read<PaymentMethodSheetBloc>().add(
|
||||
SelectPaymentCardRequested(card),
|
||||
);
|
||||
|
||||
},
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
|
||||
Container(
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(
|
||||
color: Colors.white.withOpacity(0.4),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
context.read<PaymentMethodSheetBloc>().add(AddPaymentCardRequested());
|
||||
},
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
child: Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.add,
|
||||
color: const Color(0xFF66E3C4),
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'Добавить платежную карту',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _getCardType(String lastNumber) {
|
||||
if (lastNumber.isEmpty) return 'Card';
|
||||
final firstDigit = lastNumber[0];
|
||||
switch (firstDigit) {
|
||||
case '4':
|
||||
return 'Visa';
|
||||
case '5':
|
||||
return 'Mastercard';
|
||||
case '9':
|
||||
return 'BelCard';
|
||||
default:
|
||||
return 'Card';
|
||||
}
|
||||
}
|
||||
}
|
||||
import 'dart:ui';
|
||||
import 'package:be_happy/presentation/event/tariff_sheet_event.dart';
|
||||
import 'package:be_happy/presentation/viewmodel/tariff_sheet_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:be_happy/presentation/components/payment_option.dart';
|
||||
|
||||
import '../../../domain/entities/payment_card.dart';
|
||||
import '../../event/payment_method_sheet_event.dart';
|
||||
import '../../state/payment_method_sheet_state.dart';
|
||||
import '../../viewmodel/payment_method_sheet_bloc.dart';
|
||||
|
||||
class PaymentMethodSheet extends StatefulWidget {
|
||||
final PaymentCard? initialSelectedCard;
|
||||
final bool showBalance;
|
||||
|
||||
const PaymentMethodSheet({
|
||||
super.key,
|
||||
this.initialSelectedCard,
|
||||
this.showBalance = true,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PaymentMethodSheet> createState() => _PaymentMethodSheetState();
|
||||
}
|
||||
|
||||
class _PaymentMethodSheetState extends State<PaymentMethodSheet> {
|
||||
int? _selectedPaymentMethod = -2;
|
||||
int? _processingCardId; // <-- Переменная для отслеживания отправленного запроса
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
context.read<PaymentMethodSheetBloc>().add(PaymentMethodSheetStarted());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<PaymentMethodSheetBloc, PaymentMethodSheetState>(
|
||||
listener: (context, state) {
|
||||
if (state.status == PaymentMethodSheetStatus.success && _processingCardId != null) {
|
||||
final updatedCard = state.cards.cast<PaymentCard?>().firstWhere(
|
||||
(c) => c?.id == _processingCardId,
|
||||
orElse: () => state.cards.cast<PaymentCard?>().firstWhere((c) => c?.isMain == true, orElse: () => null),
|
||||
);
|
||||
|
||||
_processingCardId = null;
|
||||
Navigator.pop(context, updatedCard);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
|
||||
if (state.status == PaymentMethodSheetStatus.success && _selectedPaymentMethod == -2) {
|
||||
if (widget.initialSelectedCard != null) {
|
||||
final initialIndex = state.cards.indexWhere(
|
||||
(card) => card.cardLastNumber == widget.initialSelectedCard!.cardLastNumber
|
||||
);
|
||||
_selectedPaymentMethod = initialIndex != -1 ? initialIndex : -1;
|
||||
} else {
|
||||
final mainCardIndex = state.cards.indexWhere((card) => card.isMain);
|
||||
_selectedPaymentMethod = mainCardIndex != -1 ? mainCardIndex : (widget.showBalance ? -1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
height: 450,
|
||||
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(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
children: [
|
||||
if (widget.showBalance) ...[
|
||||
PaymentOption(
|
||||
title: 'Баланс',
|
||||
subtitle: '${state.balance} баллов',
|
||||
isSelected: _selectedPaymentMethod == -1,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_selectedPaymentMethod = -1;
|
||||
});
|
||||
Navigator.pop(context, 'balance');
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
|
||||
...state.cards.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final card = entry.value;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: PaymentOption(
|
||||
title: card.isMain ? "Основная" : "Карта",
|
||||
subtitle: '${card.cardLastNumber}',
|
||||
isSelected: _selectedPaymentMethod == index,
|
||||
onTap: () async {
|
||||
setState(() {
|
||||
_selectedPaymentMethod = index;
|
||||
_processingCardId = card.id;
|
||||
});
|
||||
|
||||
// Отправляем событие в Блок
|
||||
context.read<PaymentMethodSheetBloc>().add(
|
||||
SelectPaymentCardRequested(card),
|
||||
);
|
||||
|
||||
},
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
|
||||
Container(
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(
|
||||
color: Colors.white.withOpacity(0.4),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
context.read<PaymentMethodSheetBloc>().add(AddPaymentCardRequested());
|
||||
},
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
child: Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.add,
|
||||
color: const Color(0xFF66E3C4),
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'Добавить платежную карту',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _getCardType(String lastNumber) {
|
||||
if (lastNumber.isEmpty) return 'Card';
|
||||
final firstDigit = lastNumber[0];
|
||||
switch (firstDigit) {
|
||||
case '4':
|
||||
return 'Visa';
|
||||
case '5':
|
||||
return 'Mastercard';
|
||||
case '9':
|
||||
return 'BelCard';
|
||||
default:
|
||||
return 'Card';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,357 +1,357 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../di/service_locator.dart';
|
||||
import '../../event/reserved_ride_event.dart';
|
||||
import '../../state/reserved_ride_state.dart';
|
||||
import '../../viewmodel/reserved_ride_bloc.dart';
|
||||
import '../dialog/cancel_booking_dialog.dart';
|
||||
import '../gradient_button.dart';
|
||||
import 'active_ride_sheet.dart';
|
||||
|
||||
class ReservedRideSheet extends StatefulWidget {
|
||||
final String scooterNumber;
|
||||
final int orderId;
|
||||
final Duration initialReservationTime;
|
||||
|
||||
const ReservedRideSheet({
|
||||
super.key,
|
||||
required this.scooterNumber,
|
||||
required this.orderId,
|
||||
this.initialReservationTime = const Duration(minutes: 3, seconds: 17),
|
||||
});
|
||||
|
||||
@override
|
||||
State<ReservedRideSheet> createState() => _ReservedRideSheetState();
|
||||
}
|
||||
|
||||
class _ReservedRideSheetState extends State<ReservedRideSheet> {
|
||||
late final ReservedRideBloc _bloc;
|
||||
late Duration _reservationTime;
|
||||
late Timer _timer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_bloc = getIt<ReservedRideBloc>();
|
||||
_reservationTime = widget.initialReservationTime;
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_reservationTime = _reservationTime - const Duration(seconds: 1);
|
||||
if (_reservationTime.isNegative) {
|
||||
_reservationTime = Duration.zero;
|
||||
timer.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer.cancel();
|
||||
_bloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _bloc,
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: 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(0xFF000032).withOpacity(0.5),
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(30),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// HEADER
|
||||
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(
|
||||
'Бесплатное бронирование',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// ТАЙМЕР + ИНФО О САМОКАТЕ (КОМПАКТНЫЙ)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
// Таймер
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
_formatDuration(_reservationTime),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
fontFamily: 'Digital Numbers',
|
||||
),
|
||||
),
|
||||
),
|
||||
// Иконка и информация (ВЫСОКИЙ БЛОК)
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 16,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Иконка самоката (ВЫШЕ)
|
||||
SizedBox(
|
||||
width: 44,
|
||||
height: 56,
|
||||
child: Image.asset(
|
||||
'assets/icons/e6a5dcb6a3e2ec2362c25ea49509ab10d2312b19-reverse.png',
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// Инфо
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFFFB800),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const Text(
|
||||
'Забронирован',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'№${widget.scooterNumber}',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// КНОПКА "НАЧАТЬ ПОЕЗДКУ"
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: BlocListener<ReservedRideBloc, ReservedRideState>(
|
||||
listener: (context, state) {
|
||||
if (state.rideStarted) {
|
||||
Navigator.pop(context);
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => ActiveRideSheet(
|
||||
scooterNumber: widget.scooterNumber,
|
||||
initialElapsedTime: Duration.zero,
|
||||
orderId: widget.orderId,
|
||||
),
|
||||
);
|
||||
} else if (state.status ==
|
||||
ReservedRideStatus.failure) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.errorMessage ?? 'Ошибка'),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: GradientButton(
|
||||
text: 'Начать поездку',
|
||||
showArrows: true,
|
||||
height: 48,
|
||||
width: double.infinity,
|
||||
fontSize: 15,
|
||||
onTap: () {
|
||||
_bloc.add(StartRide(widget.orderId));
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// КНОПКА "ОТМЕНИТЬ БРОНИРОВАНИЕ"
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: BlocListener<ReservedRideBloc, ReservedRideState>(
|
||||
listener: (context, state) {
|
||||
if (state.rideCancelled) {
|
||||
Navigator.pop(context);
|
||||
} else if (state.status ==
|
||||
ReservedRideStatus.failure) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.errorMessage ?? 'Ошибка'),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(
|
||||
color: Colors.white.withOpacity(0.4),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
final result = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
const CancelBookingDialog(),
|
||||
);
|
||||
if (result != null && result) {
|
||||
_bloc.add(CancelRide(widget.orderId));
|
||||
}
|
||||
},
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
child:
|
||||
BlocBuilder<
|
||||
ReservedRideBloc,
|
||||
ReservedRideState
|
||||
>(
|
||||
builder: (context, state) {
|
||||
if (state.status ==
|
||||
ReservedRideStatus.loading) {
|
||||
return const Center(
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return const Center(
|
||||
child: Text(
|
||||
'Отменить бронирование',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatDuration(Duration duration) {
|
||||
String twoDigits(int n) => n.toString().padLeft(2, '0');
|
||||
final minutes = twoDigits(duration.inMinutes.remainder(60));
|
||||
final seconds = twoDigits(duration.inSeconds.remainder(60));
|
||||
return '$minutes:$seconds';
|
||||
}
|
||||
}
|
||||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../di/service_locator.dart';
|
||||
import '../../event/reserved_ride_event.dart';
|
||||
import '../../state/reserved_ride_state.dart';
|
||||
import '../../viewmodel/reserved_ride_bloc.dart';
|
||||
import '../dialog/cancel_booking_dialog.dart';
|
||||
import '../gradient_button.dart';
|
||||
import 'active_ride_sheet.dart';
|
||||
|
||||
class ReservedRideSheet extends StatefulWidget {
|
||||
final String scooterNumber;
|
||||
final int orderId;
|
||||
final Duration initialReservationTime;
|
||||
|
||||
const ReservedRideSheet({
|
||||
super.key,
|
||||
required this.scooterNumber,
|
||||
required this.orderId,
|
||||
this.initialReservationTime = const Duration(minutes: 3, seconds: 17),
|
||||
});
|
||||
|
||||
@override
|
||||
State<ReservedRideSheet> createState() => _ReservedRideSheetState();
|
||||
}
|
||||
|
||||
class _ReservedRideSheetState extends State<ReservedRideSheet> {
|
||||
late final ReservedRideBloc _bloc;
|
||||
late Duration _reservationTime;
|
||||
late Timer _timer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_bloc = getIt<ReservedRideBloc>();
|
||||
_reservationTime = widget.initialReservationTime;
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_reservationTime = _reservationTime - const Duration(seconds: 1);
|
||||
if (_reservationTime.isNegative) {
|
||||
_reservationTime = Duration.zero;
|
||||
timer.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer.cancel();
|
||||
_bloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _bloc,
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: 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(0xFF000032).withOpacity(0.5),
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(30),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// HEADER
|
||||
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(
|
||||
'Бесплатное бронирование',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// ТАЙМЕР + ИНФО О САМОКАТЕ (КОМПАКТНЫЙ)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
// Таймер
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
_formatDuration(_reservationTime),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
fontFamily: 'Digital Numbers',
|
||||
),
|
||||
),
|
||||
),
|
||||
// Иконка и информация (ВЫСОКИЙ БЛОК)
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 16,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Иконка самоката (ВЫШЕ)
|
||||
SizedBox(
|
||||
width: 44,
|
||||
height: 56,
|
||||
child: Image.asset(
|
||||
'assets/icons/e6a5dcb6a3e2ec2362c25ea49509ab10d2312b19-reverse.png',
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// Инфо
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFFFB800),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const Text(
|
||||
'Забронирован',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'№${widget.scooterNumber}',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// КНОПКА "НАЧАТЬ ПОЕЗДКУ"
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: BlocListener<ReservedRideBloc, ReservedRideState>(
|
||||
listener: (context, state) {
|
||||
if (state.rideStarted) {
|
||||
Navigator.pop(context);
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => ActiveRideSheet(
|
||||
scooterNumber: widget.scooterNumber,
|
||||
initialElapsedTime: Duration.zero,
|
||||
orderId: widget.orderId,
|
||||
),
|
||||
);
|
||||
} else if (state.status ==
|
||||
ReservedRideStatus.failure) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.errorMessage ?? 'Ошибка'),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: GradientButton(
|
||||
text: 'Начать поездку',
|
||||
showArrows: true,
|
||||
height: 48,
|
||||
width: double.infinity,
|
||||
fontSize: 15,
|
||||
onTap: () {
|
||||
_bloc.add(StartRide(widget.orderId));
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// КНОПКА "ОТМЕНИТЬ БРОНИРОВАНИЕ"
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: BlocListener<ReservedRideBloc, ReservedRideState>(
|
||||
listener: (context, state) {
|
||||
if (state.rideCancelled) {
|
||||
Navigator.pop(context);
|
||||
} else if (state.status ==
|
||||
ReservedRideStatus.failure) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.errorMessage ?? 'Ошибка'),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(
|
||||
color: Colors.white.withOpacity(0.4),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
final result = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
const CancelBookingDialog(),
|
||||
);
|
||||
if (result != null && result) {
|
||||
_bloc.add(CancelRide(widget.orderId));
|
||||
}
|
||||
},
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
child:
|
||||
BlocBuilder<
|
||||
ReservedRideBloc,
|
||||
ReservedRideState
|
||||
>(
|
||||
builder: (context, state) {
|
||||
if (state.status ==
|
||||
ReservedRideStatus.loading) {
|
||||
return const Center(
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return const Center(
|
||||
child: Text(
|
||||
'Отменить бронирование',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatDuration(Duration duration) {
|
||||
String twoDigits(int n) => n.toString().padLeft(2, '0');
|
||||
final minutes = twoDigits(duration.inMinutes.remainder(60));
|
||||
final seconds = twoDigits(duration.inSeconds.remainder(60));
|
||||
return '$minutes:$seconds';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,290 +1,287 @@
|
||||
import 'dart:ui';
|
||||
import 'package:be_happy/presentation/components/scooter/mini_battery_indicator.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../domain/entities/scooter.dart';
|
||||
import '../../state/scooter_detail_modal_state.dart';
|
||||
import '../../viewmodel/scooter_detail_modal_bloc.dart';
|
||||
import '../gradient_button.dart';
|
||||
|
||||
class ScooterData {
|
||||
final String distance;
|
||||
final String number;
|
||||
final double batteryPercent;
|
||||
|
||||
ScooterData({
|
||||
required this.distance,
|
||||
required this.number,
|
||||
required this.batteryPercent,
|
||||
});
|
||||
}
|
||||
|
||||
class ScooterBottomSheet extends StatefulWidget {
|
||||
const ScooterBottomSheet({super.key});
|
||||
|
||||
@override
|
||||
State<ScooterBottomSheet> createState() => _ScooterBottomSheetState();
|
||||
}
|
||||
|
||||
class _ScooterBottomSheetState extends State<ScooterBottomSheet> {
|
||||
final PageController _pageController = PageController(viewportFraction: 0.5);
|
||||
double _currentPage = 0;
|
||||
|
||||
_ScooterBottomSheetState();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pageController.addListener(() {
|
||||
setState(() {
|
||||
_currentPage = _pageController.page ?? 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<ScooterDetailModalBloc, ScooterDetailModalState>(
|
||||
builder: (context, state) {
|
||||
if (state.status == ScooterDetailModalStatus.loading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(color: Colors.white),
|
||||
);
|
||||
}
|
||||
|
||||
if (state.status == ScooterDetailModalStatus.success) {
|
||||
return Dismissible(
|
||||
key: const Key('scooter-modal'),
|
||||
direction: DismissDirection.down, // Закрытие только вниз
|
||||
onDismissed: (_) => context.pop(),
|
||||
child: 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(
|
||||
height: 320,
|
||||
padding: const EdgeInsets.only(top: 20, bottom: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF000032).withOpacity(0.88),
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(30),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header с адресом (без изменений)
|
||||
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(
|
||||
state.address ?? "Unknown address",
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// PageView с динамическим списком
|
||||
SizedBox(
|
||||
height: 220,
|
||||
child: PageView.builder(
|
||||
controller: _pageController,
|
||||
padEnds: false,
|
||||
// Оставляем false, чтобы первый элемент прилипал к левому краю
|
||||
itemCount: state.scooters!.length,
|
||||
itemBuilder: (context, index) {
|
||||
final scooter = state.scooters![index];
|
||||
final diff = (_currentPage - index).abs();
|
||||
final isActive = diff < 0.5;
|
||||
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
// 2. Добавляем левый отступ ТОЛЬКО для первого элемента,
|
||||
// чтобы он совпадал с заголовком
|
||||
margin: EdgeInsets.only(
|
||||
left: index == 0 ? 10 : 0,
|
||||
right: 10,
|
||||
),
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Transform.scale(
|
||||
scale: isActive ? 1.0 : 0.9,
|
||||
child: _ScooterCard(
|
||||
scooter: scooter,
|
||||
isActive: isActive,
|
||||
address: state.address ?? "Unknown address",
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Center(child: Text("Error"));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ScooterCard extends StatelessWidget {
|
||||
final Scooter scooter;
|
||||
final String address;
|
||||
final bool isActive;
|
||||
|
||||
const _ScooterCard({required this.scooter, required this.isActive, required this.address});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Container(
|
||||
width: 220,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Colors.white.withOpacity(isActive ? 0.35 : 0.25),
|
||||
Colors.white.withOpacity(isActive ? 0.25 : 0.18),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
border: Border.all(color: Colors.white.withOpacity(0.4), width: 1),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.location_on_outlined,
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
(scooter.distance?.toInt() ?? 0) >= 1000
|
||||
? '${((scooter.distance?.toInt() ?? 0) / 1000).toStringAsFixed(1)} km'
|
||||
: '${scooter.distance?.toInt() ?? 0} m',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.qr_code_scanner_outlined,
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
scooter.number,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
MiniBatteryIndicator(percent: scooter.batteryLevel),
|
||||
const SizedBox(width: 8),
|
||||
Transform.translate(
|
||||
offset: const Offset(-40, 0),
|
||||
child: Text(
|
||||
'${(scooter.batteryLevel)}%',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
|
||||
GradientButton(
|
||||
text: "Подробнеe",
|
||||
showArrows: true,
|
||||
height: 32,
|
||||
width: double.infinity,
|
||||
fontSize: 12,
|
||||
onTap: () {
|
||||
Navigator.pop(context, [scooter, address]);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Positioned(
|
||||
right: isActive ? -30 : -5,
|
||||
top: isActive ? -10 : 15,
|
||||
child: SizedBox(
|
||||
height: isActive ? 190 : 160,
|
||||
child: Image.asset(
|
||||
"assets/icons/e6a5dcb6a3e2ec2362c25ea49509ab10d2312b19-reverse.png",
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
import 'dart:ui';
|
||||
import 'package:be_happy/presentation/components/scooter/mini_battery_indicator.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../domain/entities/scooter.dart';
|
||||
import '../../state/scooter_detail_modal_state.dart';
|
||||
import '../../viewmodel/scooter_detail_modal_bloc.dart';
|
||||
import '../gradient_button.dart';
|
||||
|
||||
class ScooterData {
|
||||
final String distance;
|
||||
final String number;
|
||||
final double batteryPercent;
|
||||
|
||||
ScooterData({
|
||||
required this.distance,
|
||||
required this.number,
|
||||
required this.batteryPercent,
|
||||
});
|
||||
}
|
||||
|
||||
class ScooterBottomSheet extends StatefulWidget {
|
||||
const ScooterBottomSheet({super.key});
|
||||
|
||||
@override
|
||||
State<ScooterBottomSheet> createState() => _ScooterBottomSheetState();
|
||||
}
|
||||
|
||||
class _ScooterBottomSheetState extends State<ScooterBottomSheet> {
|
||||
final PageController _pageController = PageController(viewportFraction: 0.5);
|
||||
double _currentPage = 0;
|
||||
|
||||
_ScooterBottomSheetState();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pageController.addListener(() {
|
||||
setState(() {
|
||||
_currentPage = _pageController.page ?? 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<ScooterDetailModalBloc, ScooterDetailModalState>(
|
||||
builder: (context, state) {
|
||||
if (state.status == ScooterDetailModalStatus.loading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(color: Colors.white),
|
||||
);
|
||||
}
|
||||
|
||||
if (state.status == ScooterDetailModalStatus.success) {
|
||||
return Dismissible(
|
||||
key: const Key('scooter-modal'),
|
||||
direction: DismissDirection.down,
|
||||
onDismissed: (_) => context.pop(),
|
||||
child: 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(
|
||||
height: 320,
|
||||
padding: const EdgeInsets.only(top: 20, bottom: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF000032).withOpacity(0.88),
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(30),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
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(
|
||||
state.address ?? "Unknown address",
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
SizedBox(
|
||||
height: 220,
|
||||
child: PageView.builder(
|
||||
controller: _pageController,
|
||||
padEnds: false,
|
||||
itemCount: state.scooters!.length,
|
||||
itemBuilder: (context, index) {
|
||||
final scooter = state.scooters![index];
|
||||
final diff = (_currentPage - index).abs();
|
||||
final isActive = diff < 0.5;
|
||||
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
// 2. Добавляем левый отступ ТОЛЬКО для первого элемента,
|
||||
// чтобы он совпадал с заголовком
|
||||
margin: EdgeInsets.only(
|
||||
left: index == 0 ? 10 : 0,
|
||||
right: 10,
|
||||
),
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Transform.scale(
|
||||
scale: isActive ? 1.0 : 0.9,
|
||||
child: _ScooterCard(
|
||||
scooter: scooter,
|
||||
isActive: isActive,
|
||||
address: state.address ?? "Unknown address",
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Center(child: Text("Error"));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ScooterCard extends StatelessWidget {
|
||||
final Scooter scooter;
|
||||
final String address;
|
||||
final bool isActive;
|
||||
|
||||
const _ScooterCard({required this.scooter, required this.isActive, required this.address});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Container(
|
||||
width: 220,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Colors.white.withOpacity(isActive ? 0.35 : 0.25),
|
||||
Colors.white.withOpacity(isActive ? 0.25 : 0.18),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
border: Border.all(color: Colors.white.withOpacity(0.4), width: 1),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.location_on_outlined,
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
(scooter.distance?.toInt() ?? 0) >= 1000
|
||||
? '${((scooter.distance?.toInt() ?? 0) / 1000).toStringAsFixed(1)} km'
|
||||
: '${scooter.distance?.toInt() ?? 0} m',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.qr_code_scanner_outlined,
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
scooter.number,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
MiniBatteryIndicator(percent: scooter.batteryLevel),
|
||||
const SizedBox(width: 8),
|
||||
Transform.translate(
|
||||
offset: const Offset(-40, 0),
|
||||
child: Text(
|
||||
'${(scooter.batteryLevel)}%',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
|
||||
GradientButton(
|
||||
text: "Подробнеe",
|
||||
showArrows: true,
|
||||
height: 32,
|
||||
width: double.infinity,
|
||||
fontSize: 12,
|
||||
onTap: () {
|
||||
Navigator.pop(context, [scooter, address]);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Positioned(
|
||||
right: isActive ? -30 : -5,
|
||||
top: isActive ? -10 : 15,
|
||||
child: SizedBox(
|
||||
height: isActive ? 190 : 160,
|
||||
child: Image.asset(
|
||||
"assets/icons/e6a5dcb6a3e2ec2362c25ea49509ab10d2312b19-reverse.png",
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,186 +1,186 @@
|
||||
import 'dart:ui';
|
||||
import 'package:be_happy/domain/entities/tariff.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
class TariffInfoSheet extends StatefulWidget {
|
||||
final Tariff tariff;
|
||||
final bool isInsurance;
|
||||
final Function(bool) onChanged;
|
||||
|
||||
const TariffInfoSheet({super.key, required this.tariff, required this.isInsurance, required this.onChanged});
|
||||
|
||||
@override
|
||||
State<TariffInfoSheet> createState() => _TariffInfoSheetState();
|
||||
}
|
||||
|
||||
class _TariffInfoSheetState extends State<TariffInfoSheet> {
|
||||
late bool _isInsuranceEnabled;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_isInsuranceEnabled = widget.isInsurance;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DraggableScrollableSheet(
|
||||
initialChildSize: 0.6,
|
||||
minChildSize: 0.4,
|
||||
maxChildSize: 0.9,
|
||||
expand: false,
|
||||
builder: (context, scrollController) {
|
||||
return ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF000032).withOpacity(0.9),
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Полоска сверху (Handle)
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Контент со скроллом
|
||||
Expanded(
|
||||
child: ListView(
|
||||
controller: scrollController,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
children: [
|
||||
// Заголовок
|
||||
Center(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.timer_outlined, color: Color(0xFF66E3C4), size: 18),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
widget.tariff.title,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF66E3C4),
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
|
||||
// Таблица цен
|
||||
_buildPriceRow('Старт поездки', '${widget.tariff.startPrice} BYN'),
|
||||
_buildPriceRow('Последующая минута', '${widget.tariff.drivePrice} BYN'),
|
||||
_buildPriceRow('Пауза', '${widget.tariff.pausePrice} BYN/мин'),
|
||||
_buildPriceRow('КЕШБЭК', '${widget.tariff.cashback * 100}%', isAccent: true),
|
||||
|
||||
const Divider(color: Colors.white24, height: 40),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
const Text(
|
||||
'Страховка',
|
||||
style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Image.asset('assets/icons/info_icon.png', width: 18, height: 18),
|
||||
const Spacer(),
|
||||
Text(
|
||||
'${widget.tariff.insurance} BYN',
|
||||
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Transform.scale(
|
||||
scale: 0.8,
|
||||
child: CupertinoSwitch(
|
||||
value: _isInsuranceEnabled,
|
||||
activeColor: const Color(0xFF66E3C4),
|
||||
onChanged: (val) {
|
||||
setState(() => _isInsuranceEnabled = val);
|
||||
widget.onChanged(val);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const Divider(color: Colors.white24, height: 40),
|
||||
|
||||
_buildInfoBullet('Оплата страховки осуществляется только по банковской карте отдельным платежом'),
|
||||
_buildInfoBullet('В режиме паузы время тарифа приостанавливается'),
|
||||
_buildInfoBullet('При старте заказа будет заблокирована сумма в размере 7 рублей для проверки платежеспособности. Сумма разблокируется по факту списания средств за поездку.'),
|
||||
|
||||
const SizedBox(height: 30),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPriceRow(String label, String value, {bool isAccent = false}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(color: Colors.white.withOpacity(0.8), fontSize: 15),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: isAccent ? FontWeight.bold : FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoBullet(String text) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 6),
|
||||
child: Icon(Icons.circle, size: 4, color: Colors.white),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
fontSize: 13,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
import 'dart:ui';
|
||||
import 'package:be_happy/domain/entities/tariff.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
class TariffInfoSheet extends StatefulWidget {
|
||||
final Tariff tariff;
|
||||
final bool isInsurance;
|
||||
final Function(bool) onChanged;
|
||||
|
||||
const TariffInfoSheet({super.key, required this.tariff, required this.isInsurance, required this.onChanged});
|
||||
|
||||
@override
|
||||
State<TariffInfoSheet> createState() => _TariffInfoSheetState();
|
||||
}
|
||||
|
||||
class _TariffInfoSheetState extends State<TariffInfoSheet> {
|
||||
late bool _isInsuranceEnabled;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_isInsuranceEnabled = widget.isInsurance;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DraggableScrollableSheet(
|
||||
initialChildSize: 0.6,
|
||||
minChildSize: 0.4,
|
||||
maxChildSize: 0.9,
|
||||
expand: false,
|
||||
builder: (context, scrollController) {
|
||||
return ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF000032).withOpacity(0.9),
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Полоска сверху (Handle)
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Контент со скроллом
|
||||
Expanded(
|
||||
child: ListView(
|
||||
controller: scrollController,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
children: [
|
||||
// Заголовок
|
||||
Center(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.timer_outlined, color: Color(0xFF66E3C4), size: 18),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
widget.tariff.title,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF66E3C4),
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
|
||||
// Таблица цен
|
||||
_buildPriceRow('Старт поездки', '${widget.tariff.startPrice} BYN'),
|
||||
_buildPriceRow('Последующая минута', '${widget.tariff.drivePrice} BYN'),
|
||||
_buildPriceRow('Пауза', '${widget.tariff.pausePrice} BYN/мин'),
|
||||
_buildPriceRow('КЕШБЭК', '${widget.tariff.cashback * 100}%', isAccent: true),
|
||||
|
||||
const Divider(color: Colors.white24, height: 40),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
const Text(
|
||||
'Страховка',
|
||||
style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Image.asset('assets/icons/info_icon.png', width: 18, height: 18),
|
||||
const Spacer(),
|
||||
Text(
|
||||
'${widget.tariff.insurance} BYN',
|
||||
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Transform.scale(
|
||||
scale: 0.8,
|
||||
child: CupertinoSwitch(
|
||||
value: _isInsuranceEnabled,
|
||||
activeColor: const Color(0xFF66E3C4),
|
||||
onChanged: (val) {
|
||||
setState(() => _isInsuranceEnabled = val);
|
||||
widget.onChanged(val);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const Divider(color: Colors.white24, height: 40),
|
||||
|
||||
_buildInfoBullet('Оплата страховки осуществляется только по банковской карте отдельным платежом'),
|
||||
_buildInfoBullet('В режиме паузы время тарифа приостанавливается'),
|
||||
_buildInfoBullet('При старте заказа будет заблокирована сумма в размере 7 рублей для проверки платежеспособности. Сумма разблокируется по факту списания средств за поездку.'),
|
||||
|
||||
const SizedBox(height: 30),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPriceRow(String label, String value, {bool isAccent = false}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(color: Colors.white.withOpacity(0.8), fontSize: 15),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: isAccent ? FontWeight.bold : FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoBullet(String text) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 6),
|
||||
child: Icon(Icons.circle, size: 4, color: Colors.white),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
fontSize: 13,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,209 +1,209 @@
|
||||
import 'package:be_happy/presentation/viewmodel/map_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../core/app_colors.dart';
|
||||
import '../event/map_event.dart';
|
||||
|
||||
class SideMenu extends StatelessWidget {
|
||||
|
||||
const SideMenu({super.key,});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
final state = context.watch<MapBloc>().state;
|
||||
|
||||
return Drawer(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppColors.phoneScreenBg,
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Заголовок
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 30),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Добро пожаловать!',
|
||||
style: TextStyle(
|
||||
color: AppColors.smsDigit,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
state.phoneNumber ?? "Загрузка...",
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Баланс ${state.balance} баллов',
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const Divider(color: Colors.white30, height: 1),
|
||||
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
_MenuItem(
|
||||
icon: 'assets/icons/person_icon.png',
|
||||
title: 'Профиль',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
context.go('/home/profile');
|
||||
},
|
||||
),
|
||||
_MenuItem(
|
||||
icon: 'assets/icons/list_star_icon.png',
|
||||
title: 'История поездок',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
context.go("/home/order-history");
|
||||
},
|
||||
),
|
||||
const Divider(color: Colors.white30, height: 1),
|
||||
_MenuItem(
|
||||
icon: 'assets/icons/creditcard_icon.png',
|
||||
title: 'Способ оплаты',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
context.go("/home/payment-methods");
|
||||
},
|
||||
),
|
||||
_MenuItem(
|
||||
icon: 'assets/icons/promo_icon.png',
|
||||
title: 'Промокоды',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
context.go("/home/promo");
|
||||
},
|
||||
),
|
||||
_MenuItem(
|
||||
icon: 'assets/icons/plans_icon.png',
|
||||
title: 'Абонементы',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
context.push("/home/subscriptions");
|
||||
},
|
||||
),
|
||||
const Divider(color: Colors.white30, height: 1),
|
||||
_MenuItem(
|
||||
icon: 'assets/icons/magazine_icon.png',
|
||||
title: 'Правила пользования самокатом',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
context.go("/home/rules");
|
||||
}
|
||||
),
|
||||
_MenuItem(
|
||||
icon: 'assets/icons/headphones_icon.png',
|
||||
title: 'Техподдержка',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
context.go('/home/support');
|
||||
},
|
||||
),
|
||||
_MenuItem(
|
||||
icon: 'assets/icons/doc_icon.png',
|
||||
title: 'Документы',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
context.go("/home/documents");
|
||||
},
|
||||
),
|
||||
_MenuItem(
|
||||
icon: 'assets/icons/news_icon.png',
|
||||
title: 'Новости',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
context.go("/home/news");
|
||||
},
|
||||
),
|
||||
const Divider(color: Colors.white30, height: 1),
|
||||
_MenuItem(
|
||||
icon: 'assets/icons/logout_icon.png',
|
||||
title: 'Выход',
|
||||
onTap: () {
|
||||
context.read<MapBloc>().stopNotificationStream();
|
||||
context.read<MapBloc>().add(LogoutPressed());
|
||||
context.go('/login');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Картинка внизу (внутри скролла)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2),
|
||||
child: Image.asset(
|
||||
'assets/wave.png',
|
||||
width: double.infinity,
|
||||
fit: BoxFit.fitWidth,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MenuItem extends StatelessWidget {
|
||||
final String icon;
|
||||
final String title;
|
||||
final Color? color;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _MenuItem({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.onTap,
|
||||
this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
leading: Image.asset(
|
||||
icon,
|
||||
width: 24,
|
||||
height: 24
|
||||
),
|
||||
title: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: color ?? Colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
onTap: onTap,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 5),
|
||||
);
|
||||
}
|
||||
import 'package:be_happy/presentation/viewmodel/map_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../core/app_colors.dart';
|
||||
import '../event/map_event.dart';
|
||||
|
||||
class SideMenu extends StatelessWidget {
|
||||
|
||||
const SideMenu({super.key,});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
final state = context.watch<MapBloc>().state;
|
||||
|
||||
return Drawer(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppColors.phoneScreenBg,
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Заголовок
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 30),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Добро пожаловать!',
|
||||
style: TextStyle(
|
||||
color: AppColors.smsDigit,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
state.phoneNumber ?? "Загрузка...",
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Баланс ${state.balance} баллов',
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const Divider(color: Colors.white30, height: 1),
|
||||
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
_MenuItem(
|
||||
icon: 'assets/icons/person_icon.png',
|
||||
title: 'Профиль',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
context.go('/home/profile');
|
||||
},
|
||||
),
|
||||
_MenuItem(
|
||||
icon: 'assets/icons/list_star_icon.png',
|
||||
title: 'История поездок',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
context.go("/home/order-history");
|
||||
},
|
||||
),
|
||||
const Divider(color: Colors.white30, height: 1),
|
||||
_MenuItem(
|
||||
icon: 'assets/icons/creditcard_icon.png',
|
||||
title: 'Способ оплаты',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
context.go("/home/payment-methods");
|
||||
},
|
||||
),
|
||||
_MenuItem(
|
||||
icon: 'assets/icons/promo_icon.png',
|
||||
title: 'Промокоды',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
context.go("/home/promo");
|
||||
},
|
||||
),
|
||||
_MenuItem(
|
||||
icon: 'assets/icons/plans_icon.png',
|
||||
title: 'Абонементы',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
context.push("/home/subscriptions");
|
||||
},
|
||||
),
|
||||
const Divider(color: Colors.white30, height: 1),
|
||||
_MenuItem(
|
||||
icon: 'assets/icons/magazine_icon.png',
|
||||
title: 'Правила пользования самокатом',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
context.go("/home/rules");
|
||||
}
|
||||
),
|
||||
_MenuItem(
|
||||
icon: 'assets/icons/headphones_icon.png',
|
||||
title: 'Техподдержка',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
context.go('/home/support');
|
||||
},
|
||||
),
|
||||
_MenuItem(
|
||||
icon: 'assets/icons/doc_icon.png',
|
||||
title: 'Документы',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
context.go("/home/documents");
|
||||
},
|
||||
),
|
||||
_MenuItem(
|
||||
icon: 'assets/icons/news_icon.png',
|
||||
title: 'Новости',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
context.go("/home/news");
|
||||
},
|
||||
),
|
||||
const Divider(color: Colors.white30, height: 1),
|
||||
_MenuItem(
|
||||
icon: 'assets/icons/logout_icon.png',
|
||||
title: 'Выход',
|
||||
onTap: () {
|
||||
context.read<MapBloc>().stopNotificationStream();
|
||||
context.read<MapBloc>().add(LogoutPressed());
|
||||
context.go('/login');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Картинка внизу (внутри скролла)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2),
|
||||
child: Image.asset(
|
||||
'assets/wave.png',
|
||||
width: double.infinity,
|
||||
fit: BoxFit.fitWidth,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MenuItem extends StatelessWidget {
|
||||
final String icon;
|
||||
final String title;
|
||||
final Color? color;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _MenuItem({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.onTap,
|
||||
this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
leading: Image.asset(
|
||||
icon,
|
||||
width: 24,
|
||||
height: 24
|
||||
),
|
||||
title: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: color ?? Colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
onTap: onTap,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 5),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,117 +1,117 @@
|
||||
import 'package:be_happy/presentation/components/gradient_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../domain/entities/subscription.dart';
|
||||
import '../event/subscription_list_event.dart';
|
||||
|
||||
class SubscriptionCard extends StatelessWidget {
|
||||
final Subscription subscription;
|
||||
final bool isActive;
|
||||
final DateTime? expiredAt;
|
||||
final VoidCallback? onRefresh;
|
||||
|
||||
const SubscriptionCard({super.key, required this.subscription, required this.isActive, this.expiredAt, this.onRefresh});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final minPriceOption = subscription.options.isNotEmpty
|
||||
? subscription.options.reduce((a, b) => a.price < b.price ? a : b)
|
||||
: null;
|
||||
|
||||
/*final maxDaysOption = subscription.options.isNotEmpty
|
||||
? subscription.options.reduce((a, b) => a.days > b.days ? a : b)
|
||||
: null;*/
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF0D143C),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: isActive ? MainAxisAlignment.spaceBetween : MainAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
subscription.title,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
if (isActive)
|
||||
Container(
|
||||
width: 100,
|
||||
alignment: Alignment.center,
|
||||
padding: EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
"АКТИВНА",
|
||||
style: TextStyle(
|
||||
color: Colors.greenAccent,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
]
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
subscription.shortDescription,
|
||||
style: TextStyle(color: Colors.white.withOpacity(0.7), fontSize: 14),
|
||||
),
|
||||
Spacer(),
|
||||
if (isActive && expiredAt != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final day = expiredAt!.day.toString().padLeft(2, '0');
|
||||
final month = expiredAt!.month.toString().padLeft(2, '0');
|
||||
final year = expiredAt!.year;
|
||||
|
||||
return Text(
|
||||
"Период действия: до $day.$month.$year",
|
||||
style: const TextStyle(color: Colors.white, fontSize: 14),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (minPriceOption != null)
|
||||
Text(
|
||||
"от ${minPriceOption.pricePrint}",
|
||||
style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w500),
|
||||
)
|
||||
else
|
||||
const SizedBox.shrink(),
|
||||
GradientButton(
|
||||
onTap: () async {
|
||||
final isSubscribed = await context.push<bool>("/home/subscriptions/${subscription.id}");
|
||||
if (isSubscribed == true && onRefresh != null) {
|
||||
onRefresh!();
|
||||
}
|
||||
},
|
||||
text: "Подробнее",
|
||||
enabled: true,
|
||||
width: 120,
|
||||
height: 40,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
import 'package:be_happy/presentation/components/gradient_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../domain/entities/subscription.dart';
|
||||
import '../event/subscription_list_event.dart';
|
||||
|
||||
class SubscriptionCard extends StatelessWidget {
|
||||
final Subscription subscription;
|
||||
final bool isActive;
|
||||
final DateTime? expiredAt;
|
||||
final VoidCallback? onRefresh;
|
||||
|
||||
const SubscriptionCard({super.key, required this.subscription, required this.isActive, this.expiredAt, this.onRefresh});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final minPriceOption = subscription.options.isNotEmpty
|
||||
? subscription.options.reduce((a, b) => a.price < b.price ? a : b)
|
||||
: null;
|
||||
|
||||
/*final maxDaysOption = subscription.options.isNotEmpty
|
||||
? subscription.options.reduce((a, b) => a.days > b.days ? a : b)
|
||||
: null;*/
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF0D143C),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: isActive ? MainAxisAlignment.spaceBetween : MainAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
subscription.title,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
if (isActive)
|
||||
Container(
|
||||
width: 100,
|
||||
alignment: Alignment.center,
|
||||
padding: EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
"АКТИВНА",
|
||||
style: TextStyle(
|
||||
color: Colors.greenAccent,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
]
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
subscription.shortDescription,
|
||||
style: TextStyle(color: Colors.white.withOpacity(0.7), fontSize: 14),
|
||||
),
|
||||
Spacer(),
|
||||
if (isActive && expiredAt != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final day = expiredAt!.day.toString().padLeft(2, '0');
|
||||
final month = expiredAt!.month.toString().padLeft(2, '0');
|
||||
final year = expiredAt!.year;
|
||||
|
||||
return Text(
|
||||
"Период действия: до $day.$month.$year",
|
||||
style: const TextStyle(color: Colors.white, fontSize: 14),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (minPriceOption != null)
|
||||
Text(
|
||||
"от ${minPriceOption.pricePrint}",
|
||||
style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w500),
|
||||
)
|
||||
else
|
||||
const SizedBox.shrink(),
|
||||
GradientButton(
|
||||
onTap: () async {
|
||||
final isSubscribed = await context.push<bool>("/home/subscriptions/${subscription.id}");
|
||||
if (isSubscribed == true && onRefresh != null) {
|
||||
onRefresh!();
|
||||
}
|
||||
},
|
||||
text: "Подробнее",
|
||||
enabled: true,
|
||||
width: 120,
|
||||
height: 40,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,90 +1,90 @@
|
||||
import 'package:bot_toast/bot_toast.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class UnpaidOrderNotificationCard extends StatelessWidget {
|
||||
final VoidCallback onClose;
|
||||
|
||||
const UnpaidOrderNotificationCard({
|
||||
super.key,
|
||||
required this.onClose,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.fromLTRB(24, 10, 20, 24),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF2D2B4D),
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: 64),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'У вас имеются неоплаченные поездки!',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
Text(
|
||||
'Оплатите поездки, чтобы начать новую поездку',
|
||||
style: TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 14,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Positioned(
|
||||
top: -20,
|
||||
left: -30,
|
||||
child: Image.asset(
|
||||
'assets/icons/card-screen.png',
|
||||
width: 120,
|
||||
height: 120,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
|
||||
Positioned(
|
||||
top: 15,
|
||||
right: 15,
|
||||
child: GestureDetector(
|
||||
onTap: onClose,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.close, color: Colors.white70, size: 20),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
import 'package:bot_toast/bot_toast.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class UnpaidOrderNotificationCard extends StatelessWidget {
|
||||
final VoidCallback onClose;
|
||||
|
||||
const UnpaidOrderNotificationCard({
|
||||
super.key,
|
||||
required this.onClose,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.fromLTRB(24, 10, 20, 24),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF2D2B4D),
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: 64),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'У вас имеются неоплаченные поездки!',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
Text(
|
||||
'Оплатите поездки, чтобы начать новую поездку',
|
||||
style: TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 14,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Positioned(
|
||||
top: -20,
|
||||
left: -30,
|
||||
child: Image.asset(
|
||||
'assets/icons/card-screen.png',
|
||||
width: 120,
|
||||
height: 120,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
|
||||
Positioned(
|
||||
top: 15,
|
||||
right: 15,
|
||||
child: GestureDetector(
|
||||
onTap: onClose,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.close, color: Colors.white70, size: 20),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +1,49 @@
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class CardNumberFormatter extends TextInputFormatter {
|
||||
@override
|
||||
TextEditingValue formatEditUpdate(
|
||||
TextEditingValue oldValue,
|
||||
TextEditingValue newValue,
|
||||
) {
|
||||
var text = newValue.text.replaceAll(' ', '');
|
||||
var buffer = StringBuffer();
|
||||
for (int i = 0; i < text.length; i++) {
|
||||
buffer.write(text[i]);
|
||||
var nonZeroIndex = i + 1;
|
||||
if (nonZeroIndex % 4 == 0 && nonZeroIndex != text.length) {
|
||||
buffer.write(' '); // Добавляем пробел каждые 4 цифры
|
||||
}
|
||||
}
|
||||
var string = buffer.toString();
|
||||
return newValue.copyWith(
|
||||
text: string,
|
||||
selection: TextSelection.collapsed(offset: string.length),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CardMonthInputFormatter extends TextInputFormatter {
|
||||
@override
|
||||
TextEditingValue formatEditUpdate(
|
||||
TextEditingValue oldValue,
|
||||
TextEditingValue newValue,
|
||||
) {
|
||||
var newText = newValue.text;
|
||||
if (newValue.selection.baseOffset == 0) return newValue;
|
||||
|
||||
var buffer = StringBuffer();
|
||||
for (int i = 0; i < newText.length; i++) {
|
||||
buffer.write(newText[i]);
|
||||
var nonZeroIndex = i + 1;
|
||||
if (nonZeroIndex % 2 == 0 && nonZeroIndex != newText.length) {
|
||||
buffer.write('/'); // Добавляем слэш после 2-й цифры
|
||||
}
|
||||
}
|
||||
var string = buffer.toString();
|
||||
return newValue.copyWith(
|
||||
text: string,
|
||||
selection: TextSelection.collapsed(offset: string.length),
|
||||
);
|
||||
}
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class CardNumberFormatter extends TextInputFormatter {
|
||||
@override
|
||||
TextEditingValue formatEditUpdate(
|
||||
TextEditingValue oldValue,
|
||||
TextEditingValue newValue,
|
||||
) {
|
||||
var text = newValue.text.replaceAll(' ', '');
|
||||
var buffer = StringBuffer();
|
||||
for (int i = 0; i < text.length; i++) {
|
||||
buffer.write(text[i]);
|
||||
var nonZeroIndex = i + 1;
|
||||
if (nonZeroIndex % 4 == 0 && nonZeroIndex != text.length) {
|
||||
buffer.write(' '); // Добавляем пробел каждые 4 цифры
|
||||
}
|
||||
}
|
||||
var string = buffer.toString();
|
||||
return newValue.copyWith(
|
||||
text: string,
|
||||
selection: TextSelection.collapsed(offset: string.length),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CardMonthInputFormatter extends TextInputFormatter {
|
||||
@override
|
||||
TextEditingValue formatEditUpdate(
|
||||
TextEditingValue oldValue,
|
||||
TextEditingValue newValue,
|
||||
) {
|
||||
var newText = newValue.text;
|
||||
if (newValue.selection.baseOffset == 0) return newValue;
|
||||
|
||||
var buffer = StringBuffer();
|
||||
for (int i = 0; i < newText.length; i++) {
|
||||
buffer.write(newText[i]);
|
||||
var nonZeroIndex = i + 1;
|
||||
if (nonZeroIndex % 2 == 0 && nonZeroIndex != newText.length) {
|
||||
buffer.write('/'); // Добавляем слэш после 2-й цифры
|
||||
}
|
||||
}
|
||||
var string = buffer.toString();
|
||||
return newValue.copyWith(
|
||||
text: string,
|
||||
selection: TextSelection.collapsed(offset: string.length),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user