fix functional bugs
This commit is contained in:
@@ -19,11 +19,14 @@ class AddCardScreen extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: BlocListener<AddCardBloc, AddCardState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.status != current.status &&
|
||||
current.status == AddCardStatus.success,
|
||||
listenWhen: (previous, current) {
|
||||
print(
|
||||
'Смена статуса: ${previous.status} -> ${current.status} ${current.errorMessage}');
|
||||
return previous.status != current.status &&
|
||||
current.status == AddCardStatus.success;
|
||||
},
|
||||
listener: (context, state) {
|
||||
context.pop();
|
||||
context.pop(true);
|
||||
},
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg),
|
||||
@@ -149,10 +152,7 @@ class AddCardScreen extends StatelessWidget {
|
||||
child: InkWell(
|
||||
onTap: state.isFormValid
|
||||
? () => {
|
||||
context.read<AddCardBloc>().add(
|
||||
AddCardSubmitted()),
|
||||
context.read<PaymentMethodsBloc>()..add(PaymentMethodsStarted()),
|
||||
context.pop()
|
||||
context.read<AddCardBloc>().add(AddCardSubmitted()),
|
||||
}
|
||||
: null,
|
||||
borderRadius: BorderRadius.circular(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../../core/app_colors.dart';
|
||||
import '../components/custom_app_bar.dart';
|
||||
@@ -17,12 +18,10 @@ class DocumentsScreen extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
children: [
|
||||
// ✅ Используем общий AppBar
|
||||
const SizedBox(height: 16),
|
||||
CustomAppBar(title: 'Документы'),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Список ссылок
|
||||
LinkRow(
|
||||
icon: 'assets/icons/doc.png',
|
||||
title: 'Договор аренды',
|
||||
@@ -33,14 +32,14 @@ class DocumentsScreen extends StatelessWidget {
|
||||
LinkRow(
|
||||
icon: 'assets/icons/doc.png',
|
||||
title: 'Политика конфиденциальности',
|
||||
onTap: () => openLink('https://...'),
|
||||
onTap: () => context.push('/privacy-policy')
|
||||
),
|
||||
const Divider(height: 1, color: Colors.white24),
|
||||
const SizedBox(height: 12),
|
||||
LinkRow(
|
||||
icon: 'assets/icons/doc.png',
|
||||
title: 'Правила вождения',
|
||||
onTap: () => openLink('https://...'),
|
||||
onTap: () => openLink('https://behappybel.by/#rule'),
|
||||
),
|
||||
const Divider(height: 1, color: Colors.white24),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
@@ -93,7 +93,9 @@ class _MapScreenState extends State<MapScreen> {
|
||||
listenWhen: (previous, current) {
|
||||
return current.lastNotification !=
|
||||
previous.lastNotification ||
|
||||
current.flags != previous.flags;
|
||||
current.flags != previous.flags ||
|
||||
previous.selectedScooterForFocus?.id
|
||||
!= current.selectedScooterForFocus?.id;
|
||||
},
|
||||
|
||||
listener: (context, state) {
|
||||
@@ -164,14 +166,39 @@ class _MapScreenState extends State<MapScreen> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (state.selectedScooterForFocus != null) {
|
||||
final targetScooter = state.selectedScooterForFocus!;
|
||||
|
||||
print("RESERVED SCOOTER: $targetScooter");
|
||||
|
||||
_moveCameraToPoint(
|
||||
targetScooter.longitude,
|
||||
targetScooter.latitude,
|
||||
zoom: 17,
|
||||
);
|
||||
|
||||
context.read<MapBloc>().add(ClearMapFocus());
|
||||
}
|
||||
},
|
||||
buildWhen: (previous, current) =>
|
||||
previous.scooters != current.scooters ||
|
||||
previous.zones != current.zones,
|
||||
buildWhen: (previous, current) {
|
||||
return previous.scooters != current.scooters ||
|
||||
previous.reservedScooters != current.reservedScooters ||
|
||||
previous.zones != current.zones ||
|
||||
previous.status != current.status;
|
||||
},
|
||||
|
||||
builder: (context, state) {
|
||||
final scooters = _buildScooterPlacemarks(
|
||||
state.scooters,
|
||||
state.address ?? "Unknown address",
|
||||
final freeScooters = _buildScooterPlacemarks(
|
||||
scooters: state.scooters,
|
||||
iconAsset: 'assets/icons/scooter_placemark_fill.png',
|
||||
isClickable: true,
|
||||
);
|
||||
|
||||
final reservedScooters = _buildScooterPlacemarks(
|
||||
scooters: state.reservedScooters ?? [],
|
||||
iconAsset: 'assets/icons/scooter_reserved_placemark_fill.png',
|
||||
isClickable: false,
|
||||
);
|
||||
|
||||
final zonePolygons = _buildZonePolygons(state.zones);
|
||||
@@ -193,9 +220,10 @@ class _MapScreenState extends State<MapScreen> {
|
||||
},
|
||||
mapObjects: [
|
||||
...zonePolygons,
|
||||
...reservedScooters,
|
||||
ClusterizedPlacemarkCollection(
|
||||
mapId: const MapObjectId('scooters_cluster'),
|
||||
placemarks: scooters,
|
||||
placemarks: freeScooters,
|
||||
radius: 30,
|
||||
minZoom: 15,
|
||||
consumeTapEvents: true,
|
||||
@@ -232,7 +260,6 @@ class _MapScreenState extends State<MapScreen> {
|
||||
},
|
||||
),
|
||||
|
||||
// Индикатор загрузки (отдельный строитель для статуса)
|
||||
BlocBuilder<MapBloc, ScooterState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.status != current.status,
|
||||
@@ -378,6 +405,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
}
|
||||
|
||||
void _onMarkerTap(List<Scooter> scooters) async {
|
||||
context.read<MapBloc>().add(CheckUser());
|
||||
final flags = context.read<MapBloc>().state.flags;
|
||||
|
||||
if (!flags.hasCard) {
|
||||
@@ -553,26 +581,26 @@ class _MapScreenState extends State<MapScreen> {
|
||||
await ClusterIconPainter.initImage('assets/icons/scooter_placemark.png');
|
||||
}
|
||||
|
||||
List<PlacemarkMapObject> _buildScooterPlacemarks(
|
||||
List<Scooter> scooters,
|
||||
String address,
|
||||
) {
|
||||
List<PlacemarkMapObject> _buildScooterPlacemarks({
|
||||
required List<Scooter> scooters,
|
||||
required String iconAsset,
|
||||
required bool isClickable,
|
||||
}) {
|
||||
return scooters.map((scooter) {
|
||||
return PlacemarkMapObject(
|
||||
mapId: MapObjectId('${scooter.id}'),
|
||||
mapId: MapObjectId('${isClickable ? "" : "reserved_"}${scooter.id}'), // уникальный ID для карты
|
||||
point: Point(latitude: scooter.longitude, longitude: scooter.latitude),
|
||||
icon: PlacemarkIcon.single(
|
||||
PlacemarkIconStyle(
|
||||
image: BitmapDescriptor.fromAssetImage(
|
||||
'assets/icons/scooter_placemark_fill.png',
|
||||
),
|
||||
image: BitmapDescriptor.fromAssetImage(iconAsset),
|
||||
scale: 0.2,
|
||||
),
|
||||
),
|
||||
opacity: 1.0,
|
||||
onTap: (object, point) async => {
|
||||
_onMarkerTap([scooter]),
|
||||
},
|
||||
consumeTapEvents: isClickable,
|
||||
onTap: isClickable
|
||||
? (object, point) async => _onMarkerTap([scooter])
|
||||
: null,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@@ -9,9 +9,11 @@ import '../components/custom_app_bar.dart';
|
||||
import '../components/gradient_button.dart';
|
||||
import '../components/payment_option.dart';
|
||||
import '../components/sheet/payment_method_sheet.dart';
|
||||
import '../event/map_event.dart';
|
||||
import '../event/payment_confirm_event.dart';
|
||||
import '../event/payment_method_sheet_event.dart';
|
||||
import '../state/payment_confirm_state.dart';
|
||||
import '../viewmodel/map_bloc.dart';
|
||||
import '../viewmodel/payment_confirm_bloc.dart';
|
||||
import '../viewmodel/payment_method_sheet_bloc.dart';
|
||||
|
||||
@@ -78,6 +80,7 @@ class _PaymentConfirmScreenContent extends StatelessWidget {
|
||||
|
||||
listener: (context, state) {
|
||||
if (state.status == PaymentConfirmStatus.success && state.paymentCompleted) {
|
||||
context.read<MapBloc>().add(ClearMapPlacemarks());
|
||||
context.go('/home');
|
||||
} else if (state.status == PaymentConfirmStatus.failure) {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
|
||||
@@ -17,43 +17,63 @@ class PaymentMethodsScreen extends StatelessWidget {
|
||||
body: Container(
|
||||
decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20),
|
||||
child: CustomAppBar(title: 'Способы оплаты'),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Expanded(
|
||||
child: BlocConsumer<PaymentMethodsBloc, PaymentMethodsState>(
|
||||
listener: (context, state) {
|
||||
if (state.status == PaymentMethodsStatus.failure) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(state.errorMessage ?? 'Ошибка')),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state.status == PaymentMethodsStatus.loading && state.cards.isEmpty) {
|
||||
return const Center(child: CircularProgressIndicator(color: Color(0xFF00D4AA)));
|
||||
}
|
||||
child: BlocConsumer<PaymentMethodsBloc, PaymentMethodsState>(
|
||||
listener: (context, state) {
|
||||
if (state.status == PaymentMethodsStatus.failure) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(state.errorMessage ?? 'Ошибка')),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
final isNetworkProcessing = state.status == PaymentMethodsStatus.loading ||
|
||||
(state.isDeleting ?? false) ||
|
||||
(state.isSettingMain ?? false);
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildBalanceCard(context, state.balance),
|
||||
const SizedBox(height: 20),
|
||||
_buildCardsList(context, state),
|
||||
],
|
||||
return Stack(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20),
|
||||
child: CustomAppBar(title: 'Способы оплаты'),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
Expanded(
|
||||
child: state.cards.isEmpty && state.status == PaymentMethodsStatus.loading
|
||||
? const Center(
|
||||
child: CircularProgressIndicator(color: Color(0xFF00D4AA)),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildBalanceCard(context, state.balance),
|
||||
const SizedBox(height: 20),
|
||||
_buildCardsList(context, state),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
if (isNetworkProcessing && state.cards.isNotEmpty)
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(0.4),
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Color(0xFF00D4AA),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -163,7 +183,13 @@ class PaymentMethodsScreen extends StatelessWidget {
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => context.go('/home/payment-methods/add-card'),
|
||||
onTap: () async {
|
||||
final isCardAdded = await context.push<bool>('/home/payment-methods/add-card');
|
||||
|
||||
if (isCardAdded == true && context.mounted) {
|
||||
context.read<PaymentMethodsBloc>().add(PaymentMethodsStarted());
|
||||
}
|
||||
},
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
|
||||
@@ -50,7 +50,9 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg),
|
||||
decoration: const BoxDecoration(
|
||||
gradient: AppColors.phoneScreenBg,
|
||||
),
|
||||
child: SafeArea(
|
||||
child: BlocBuilder<ProfileBloc, ProfileState>(
|
||||
builder: (context, state) {
|
||||
@@ -70,56 +72,82 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
}
|
||||
|
||||
final profile = state.profile!;
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
CustomAppBar(title: 'Профиль'),
|
||||
const SizedBox(height: 32),
|
||||
Stack(
|
||||
alignment: Alignment.topRight,
|
||||
children: [
|
||||
|
||||
CircleAvatar(
|
||||
radius: 60,
|
||||
backgroundColor: AppColors.checkboxFill,
|
||||
backgroundImage: (profile.avatarUrl != null && profile.avatarUrl!.isNotEmpty)
|
||||
? NetworkImage("${profile.avatarUrl!}?v=${DateTime.now().minute}")
|
||||
: null,
|
||||
child: (profile.avatarUrl == null || profile.avatarUrl!.isEmpty)
|
||||
? Text(
|
||||
profile.name.isNotEmpty ? profile.name[0].toUpperCase() : '',
|
||||
style: const TextStyle(fontSize: 50, color: AppColors.darkBlue),
|
||||
)
|
||||
: null,
|
||||
), GestureDetector(
|
||||
onTap: _pickImage,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(top: 0, right: 0),
|
||||
child: Image.asset(
|
||||
'assets/icons/edit.png',
|
||||
width: 24,
|
||||
height: 24,
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SingleChildScrollView(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: constraints.maxHeight,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
CustomAppBar(title: 'Профиль'),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
Stack(
|
||||
alignment: Alignment.topRight,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 60,
|
||||
backgroundColor: AppColors.checkboxFill,
|
||||
backgroundImage:
|
||||
(profile.avatarUrl != null &&
|
||||
profile.avatarUrl!.isNotEmpty)
|
||||
? NetworkImage(
|
||||
"${profile.avatarUrl!}?v=${DateTime.now().minute}",
|
||||
)
|
||||
: null,
|
||||
child:
|
||||
(profile.avatarUrl == null ||
|
||||
profile.avatarUrl!.isEmpty)
|
||||
? Text(
|
||||
profile.name.isNotEmpty
|
||||
? profile.name[0].toUpperCase()
|
||||
: '',
|
||||
style: const TextStyle(
|
||||
fontSize: 50,
|
||||
color: AppColors.darkBlue,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
|
||||
GestureDetector(
|
||||
onTap: _pickImage,
|
||||
child: Container(
|
||||
child: Image.asset(
|
||||
'assets/icons/edit.png',
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
_ProfileInfoBlock(
|
||||
profile: profile,
|
||||
onEditTap: () => context.go("/home/profile/edit"),
|
||||
),
|
||||
|
||||
// const SizedBox(height: 24),
|
||||
// _SettingsBlock(
|
||||
// notificationsEnabled: notificationsEnabled,
|
||||
// onNotificationsChanged: (v) =>
|
||||
// setState(() => notificationsEnabled = v),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
_ProfileInfoBlock(
|
||||
profile: profile,
|
||||
onEditTap: () => context.go("/home/profile/edit"),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_SettingsBlock(
|
||||
notificationsEnabled: notificationsEnabled,
|
||||
onNotificationsChanged: (v) =>
|
||||
setState(() => notificationsEnabled = v),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
|
||||
import 'dart:math' as math;
|
||||
import 'package:be_happy/presentation/event/spalsh_event.dart';
|
||||
import 'package:be_happy/presentation/viewmodel/splash_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
// Подключи сюда свои реальные экраны:
|
||||
import 'phone_screen.dart';
|
||||
import 'pin_login_screen.dart';
|
||||
|
||||
class SplashScreen extends StatefulWidget {
|
||||
const SplashScreen({super.key});
|
||||
@@ -19,35 +14,61 @@ class SplashScreen extends StatefulWidget {
|
||||
class _SplashScreenState extends State<SplashScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
late final Animation<double> _revealAnimation;
|
||||
|
||||
static const double logoSize = 300;
|
||||
// Фаза 1: Заполнение цветом слева направо (0.0 -> 0.5)
|
||||
late final Animation<double> _fillProgress;
|
||||
|
||||
// Фаза 2: Укатывание вправо (0.6 -> 1.0)
|
||||
late final Animation<double> _rollTranslation;
|
||||
late final Animation<double> _rollRotation;
|
||||
|
||||
// Уменьшенный размер логотипа по вашему запросу
|
||||
static const double logoSize = 130;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// контроллер анимации
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 2500),
|
||||
duration: const Duration(milliseconds: 3000),
|
||||
);
|
||||
|
||||
// анимация движения "затемняющего" прямоугольника
|
||||
_revealAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
|
||||
_fillProgress = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(0.0, 0.5, curve: Curves.easeInOut),
|
||||
),
|
||||
);
|
||||
|
||||
// запускаем анимацию
|
||||
_controller.forward().then((_) async {
|
||||
// небольшая пауза после анимации
|
||||
await Future.delayed(const Duration(milliseconds: 800));
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
context.read<SplashBloc>().add(AuthCheckRequested());
|
||||
_rollTranslation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(0.6, 1.0, curve: Curves.easeInCubic),
|
||||
),
|
||||
);
|
||||
|
||||
_rollRotation = Tween<double>(begin: 0.0, end: 2 * math.pi).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(0.6, 1.0, curve: Curves.easeIn),
|
||||
),
|
||||
);
|
||||
|
||||
// Добавляем задержку перед стартом, чтобы пользователь успел увидеть экран
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
if (!mounted) return;
|
||||
|
||||
// Ждем 500мс после того, как первый кадр отрисовался
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
if (!mounted) return;
|
||||
// Запускаем анимацию
|
||||
_controller.forward().then((_) {
|
||||
if (!mounted) return;
|
||||
context.read<SplashBloc>().add(AuthCheckRequested());
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -56,69 +77,102 @@ class _SplashScreenState extends State<SplashScreen>
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final double screenWidth = MediaQuery.of(context).size.width;
|
||||
final double endTranslation = screenWidth / 2 + logoSize;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFF3A3A3A),
|
||||
body: Center(
|
||||
child: AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, _) {
|
||||
final double offset = _revealAnimation.value * (logoSize * 1.2);
|
||||
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Цветной логотип (на заднем плане)
|
||||
Image.asset(
|
||||
'assets/logo_color.png',
|
||||
width: logoSize,
|
||||
height: logoSize,
|
||||
fit: BoxFit.contain,
|
||||
body: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0xFF293A69),
|
||||
Color(0xFF202741),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Прямоугольник, который "уезжает" вправо, открывая логотип
|
||||
ClipRect(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: FractionallySizedBox(
|
||||
widthFactor: 1,
|
||||
child: Container(
|
||||
width: logoSize,
|
||||
height: logoSize,
|
||||
color: const Color(0xFF3A3A3A),
|
||||
transform: Matrix4.translationValues(offset, 0, 0),
|
||||
),
|
||||
// 2. Волна
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Image.asset(
|
||||
'assets/wave.png',
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Image.asset(
|
||||
'assets/splash_map.png',
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
|
||||
Center(
|
||||
child: AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
final double translationX = _rollTranslation.value * endTranslation;
|
||||
|
||||
return Transform(
|
||||
transform: Matrix4.translationValues(translationX, 0, 0)
|
||||
..rotateZ(_rollRotation.value),
|
||||
alignment: Alignment.center,
|
||||
// Используем ShaderMask для эффекта заполнения/проявления
|
||||
child: ShaderMask(
|
||||
shaderCallback: (bounds) {
|
||||
return LinearGradient(
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
// Используем _fillProgress для сдвига жесткой границы градиента
|
||||
colors: const [Colors.white, Colors.transparent],
|
||||
stops: [_fillProgress.value, _fillProgress.value],
|
||||
).createShader(bounds);
|
||||
},
|
||||
blendMode: BlendMode.dstIn, // Оставляет только ту часть логотипа, где градиент белый
|
||||
child: Image.asset(
|
||||
'assets/splash_logo.png',
|
||||
width: logoSize,
|
||||
height: logoSize,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Обводка логотипа (поверх)
|
||||
Image.asset(
|
||||
'assets/logo_outline.png',
|
||||
width: logoSize * 1.01,
|
||||
height: logoSize * 1.01,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
bottomNavigationBar: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 24),
|
||||
child: Text(
|
||||
'Версия приложения 1.0',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
fontSize: 12,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 5. Версия приложения
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 24,
|
||||
child: Text(
|
||||
'Версия приложения 1.0',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.6),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w300,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -27,15 +27,26 @@ class SubscriptionDetailsScreen extends StatelessWidget {
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: BlocBuilder<SubscriptionDetailsBloc, SubscriptionDetailsState>(
|
||||
builder: (context, state) {
|
||||
String title = "Загрузка...";
|
||||
if (state is DetailsContentState) {
|
||||
title = state.subscription.title;
|
||||
}
|
||||
return CustomAppBar(title: title);
|
||||
},
|
||||
),
|
||||
child:
|
||||
BlocConsumer<
|
||||
SubscriptionDetailsBloc,
|
||||
SubscriptionDetailsState
|
||||
>(
|
||||
listenWhen: (previous, current) =>
|
||||
current is DetailsContentState && current.isSuccess,
|
||||
listener: (context, state) {
|
||||
if (state is DetailsContentState && state.isSuccess) {
|
||||
context.pop(true);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
String title = "Загрузка...";
|
||||
if (state is DetailsContentState) {
|
||||
title = state.subscription.title;
|
||||
}
|
||||
return CustomAppBar(title: title);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
@@ -53,11 +64,16 @@ class SubscriptionDetailsScreen extends StatelessWidget {
|
||||
child: Image.asset('assets/wave.png'),
|
||||
),
|
||||
),
|
||||
BlocBuilder<SubscriptionDetailsBloc, SubscriptionDetailsState>(
|
||||
BlocBuilder<
|
||||
SubscriptionDetailsBloc,
|
||||
SubscriptionDetailsState
|
||||
>(
|
||||
builder: (context, state) {
|
||||
if (state is DetailsLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(color: Color(0xFF80FFD1)),
|
||||
child: CircularProgressIndicator(
|
||||
color: Color(0xFF80FFD1),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (state is DetailsError) {
|
||||
@@ -85,6 +101,7 @@ class SubscriptionDetailsScreen extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildContent(BuildContext context, DetailsContentState state) {
|
||||
final bool isAvailableForPurchase = state.subscription.isActive;
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
|
||||
child: Column(
|
||||
@@ -98,22 +115,28 @@ class SubscriptionDetailsScreen extends StatelessWidget {
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
|
||||
_ActionCard(state: state),
|
||||
if (isAvailableForPurchase) ...[
|
||||
const SizedBox(height: 30),
|
||||
|
||||
const SizedBox(height: 30),
|
||||
_ActionCard(state: state),
|
||||
|
||||
GradientButton(
|
||||
text: 'Активировать',
|
||||
onTap: () => context.read<SubscriptionDetailsBloc>().add(
|
||||
ActivateSubscriptionPressed(),
|
||||
const SizedBox(height: 30),
|
||||
|
||||
GradientButton(
|
||||
text: state.isAlreadyPurchased ? 'Продлить' : 'Активировать',
|
||||
onTap: () {
|
||||
context.read<SubscriptionDetailsBloc>().add(
|
||||
ActivateSubscriptionPressed(),
|
||||
);
|
||||
},
|
||||
enabled: state.isAgreed,
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
fontSize: 16,
|
||||
showArrows: true,
|
||||
),
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
fontSize: 16,
|
||||
showArrows: true,
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
@@ -232,4 +255,4 @@ class _PriceRow extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:be_happy/presentation/event/subscription_list_event.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -44,17 +45,26 @@ class SubscriptionsListScreen extends StatelessWidget {
|
||||
if (state is SubscriptionsLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else if (state is SubscriptionsLoaded) {
|
||||
final activeIds = state.activeSubscriptions.map((e) => e.id).toSet();
|
||||
final clientSubsMap = {
|
||||
for (var sub in state.activeSubscriptions) sub.subscriptionId: sub
|
||||
};
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.only(top: 20),
|
||||
itemCount: state.subscriptions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final subscription = state.subscriptions[index];
|
||||
final bool isActive = activeIds.contains(subscription.id);
|
||||
|
||||
final bool isPurchased = subscription.isCurrent;
|
||||
|
||||
final clientSub = clientSubsMap[subscription.id];
|
||||
final DateTime? expirationDate = isPurchased ? clientSub?.expiredAt : null;
|
||||
|
||||
return SubscriptionCard(
|
||||
subscription: subscription,
|
||||
isActive: isActive,
|
||||
isActive: isPurchased,
|
||||
expiredAt: expirationDate,
|
||||
onRefresh: () { context.read<SubscriptionListBloc>().add(LoadSubscriptionsEvent());},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -170,7 +170,7 @@ class TopUpScreen extends StatelessWidget {
|
||||
create: (context) =>
|
||||
PaymentMethodSheetBloc(getIt<GetPaymentCardsUsecase>())
|
||||
..add(PaymentMethodSheetStarted()),
|
||||
child: const PaymentMethodSheet(),
|
||||
child: const PaymentMethodSheet(showBalance: false,),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user