From 0f5061ce9692c04ce21af8e9c600d4af1a034b85 Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev <572431+veikab@users.noreply.github.com> Date: Fri, 12 Jun 2026 23:06:17 +0700 Subject: [PATCH] Fix add review route and photo previews --- lib/app/router/app_router.dart | 64 +++++++------- .../mapflow/presentation/mapflow_shell.dart | 78 ++++------------ .../widgets/place_photo_card.dart | 88 ++++++++++++++++--- 3 files changed, 124 insertions(+), 106 deletions(-) diff --git a/lib/app/router/app_router.dart b/lib/app/router/app_router.dart index ce45c95..8805248 100644 --- a/lib/app/router/app_router.dart +++ b/lib/app/router/app_router.dart @@ -22,41 +22,39 @@ GoRouter createAppRouter() { GoRoute( path: '/', builder: (context, state) => const MapflowShell(), - routes: [ - GoRoute( - path: 'experience/new', - pageBuilder: (context, state) { - final extra = state.extra; - final args = extra is AddExperienceArgs - ? extra - : const AddExperienceArgs( - coordinate: null, - hasTelegramAuth: true, - ); - return CustomTransitionPage( - fullscreenDialog: true, - key: state.pageKey, - child: AddExperienceFlow( - coordinate: args.coordinate, - hasTelegramAuth: args.hasTelegramAuth, - ), - transitionsBuilder: (context, animation, _, child) { - return SlideTransition( - position: Tween( - begin: const Offset(0, 1), - end: Offset.zero, - ).animate(animation), - child: child, - ); - }, + ), + GoRoute( + path: '/experience/new', + pageBuilder: (context, state) { + final extra = state.extra; + final args = extra is AddExperienceArgs + ? extra + : const AddExperienceArgs( + coordinate: null, + hasTelegramAuth: true, + ); + return CustomTransitionPage( + fullscreenDialog: true, + key: state.pageKey, + child: AddExperienceFlow( + coordinate: args.coordinate, + hasTelegramAuth: args.hasTelegramAuth, + ), + transitionsBuilder: (context, animation, _, child) { + return SlideTransition( + position: Tween( + begin: const Offset(0, 1), + end: Offset.zero, + ).animate(animation), + child: child, ); }, - ), - GoRoute( - path: 'admin/reviews', - builder: (context, state) => const AdminVoiceExperiencesScreen(), - ), - ], + ); + }, + ), + GoRoute( + path: '/admin/reviews', + builder: (context, state) => const AdminVoiceExperiencesScreen(), ), ], ); diff --git a/lib/features/mapflow/presentation/mapflow_shell.dart b/lib/features/mapflow/presentation/mapflow_shell.dart index cfba0b7..af6f75f 100644 --- a/lib/features/mapflow/presentation/mapflow_shell.dart +++ b/lib/features/mapflow/presentation/mapflow_shell.dart @@ -320,15 +320,18 @@ class _AddReviewButton extends StatelessWidget { final colorScheme = Theme.of(context).colorScheme; return Padding( - padding: const EdgeInsets.only(top: 8, right: 12), - child: IconButton.filled( - onPressed: onPressed, - icon: const Icon(Icons.add_location_alt_outlined), - style: FilledButton.styleFrom( - backgroundColor: tokens.mapPanel, - foregroundColor: colorScheme.onSurface, + padding: const EdgeInsets.only(top: 62, right: 12), + child: SizedBox.square( + dimension: 52, + child: IconButton.filled( + onPressed: onPressed, + icon: const Icon(Icons.add_location_alt_outlined), + style: FilledButton.styleFrom( + backgroundColor: tokens.mapPanel, + foregroundColor: colorScheme.onSurface, + ), + tooltip: 'Добавить отзыв', ), - tooltip: 'Добавить отзыв', ), ); } @@ -1043,8 +1046,7 @@ class _AddExperienceFlowState extends State { final informationProgress = (_informationUnits / _minimumInformationUnits) .clamp(0.0, 1.0); final content = switch (_step) { - 0 => _IntroStep(onNext: () => setState(() => _step = 1)), - 1 => _VoiceStep( + 0 => _VoiceStep( placeName: '', promptHints: _voicePromptHints, hasTelegramAuth: widget.hasTelegramAuth, @@ -1061,11 +1063,11 @@ class _AddExperienceFlowState extends State { } setState(() { _nearbyPlacesFuture = _loadNearbyPlaces(); - _step = 2; + _step = 1; }); }, ), - _ => _PlaceStep( + 1 => _PlaceStep( placesFuture: _nearbyPlacesFuture, radiusMeters: _nearbyPlaceRadiusMeters, isSubmitting: _submitting, @@ -1073,12 +1075,13 @@ class _AddExperienceFlowState extends State { controller.setReviewPlace(place.name); setState(() { _selectedPlaceForSubmit = place; - _step = 3; + _step = 2; }); }, ), + _ => const SizedBox.shrink(), }; - final contentWithFavoriteStep = _step == 3 + final contentWithFavoriteStep = _step == 2 ? _FavoriteStep( place: _selectedPlaceForSubmit, isSubmitting: _submitting, @@ -1100,7 +1103,7 @@ class _AddExperienceFlowState extends State { children: [ _StoryProgress( step: _step, - total: 4, + total: 3, dark: true, onClose: () => context.pop(), ), @@ -1163,47 +1166,6 @@ class _AddExperienceFlowState extends State { } } -class _IntroStep extends StatelessWidget { - const _IntroStep({required this.onNext}); - - final VoidCallback onNext; - - @override - Widget build(BuildContext context) { - final tokens = context.mapflowTokens; - - return _StepLayout( - body: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 70, - height: 70, - decoration: BoxDecoration( - color: tokens.danger, - borderRadius: BorderRadius.circular(tokens.panelRadius), - ), - child: Icon(Icons.graphic_eq, color: tokens.onDark, size: 38), - ), - const SizedBox(height: 22), - Text( - 'Поделись ощущением от места голосом. Нужно быть в заведении или не дальше 50 м. Аудио обработаем и удалим.', - textAlign: TextAlign.left, - style: Theme.of(context).textTheme.titleLarge?.copyWith( - color: tokens.onDark, - fontWeight: FontWeight.w800, - height: 1.18, - letterSpacing: 0, - ), - ), - ], - ), - action: FilledButton(onPressed: onNext, child: const Text('Далее')), - ); - } -} - class _PlaceStep extends StatelessWidget { const _PlaceStep({ required this.placesFuture, @@ -1792,17 +1754,15 @@ class _VoiceRecordButtonState extends State<_VoiceRecordButton> } class _StepLayout extends StatelessWidget { - const _StepLayout({required this.body, this.action}); + const _StepLayout({required this.body}); final Widget body; - final Widget? action; @override Widget build(BuildContext context) { return Column( children: [ Expanded(child: body), - if (action != null) SizedBox(width: double.infinity, child: action), ], ); } diff --git a/lib/features/mapflow/presentation/widgets/place_photo_card.dart b/lib/features/mapflow/presentation/widgets/place_photo_card.dart index 460e2eb..8f1a259 100644 --- a/lib/features/mapflow/presentation/widgets/place_photo_card.dart +++ b/lib/features/mapflow/presentation/widgets/place_photo_card.dart @@ -19,11 +19,12 @@ class PlacePhotoCard extends StatelessWidget { Widget build(BuildContext context) { final tokens = context.mapflowTokens; final colorScheme = Theme.of(context).colorScheme; + final photoUrls = place.photoUrls.take(3).toList(); return GestureDetector( onTap: onTap, child: Container( - width: 150, + width: 226, decoration: BoxDecoration( borderRadius: BorderRadius.circular(tokens.panelRadius), border: Border.all(color: colorScheme.surface), @@ -39,24 +40,15 @@ class PlacePhotoCard extends StatelessWidget { child: Stack( fit: StackFit.expand, children: [ - if (place.photoUrls.isEmpty) + if (photoUrls.isEmpty) ColoredBox( color: colorScheme.primary, child: Icon(Icons.place_outlined, color: colorScheme.onPrimary), ) else - PageView.builder( - itemCount: place.photoUrls.length, - itemBuilder: (context, index) { - return Image.network( - place.photoUrls[index], - fit: BoxFit.cover, - errorBuilder: (_, _, _) => ColoredBox( - color: tokens.mapPanelBorder, - child: const Icon(Icons.place_outlined), - ), - ); - }, + _PhotoTriptych( + photoUrls: photoUrls, + fallbackColor: tokens.mapPanelBorder, ), DecoratedBox( decoration: BoxDecoration( @@ -105,3 +97,71 @@ class PlacePhotoCard extends StatelessWidget { ); } } + +class _PhotoTriptych extends StatelessWidget { + const _PhotoTriptych({ + required this.photoUrls, + required this.fallbackColor, + }); + + final List photoUrls; + final Color fallbackColor; + + @override + Widget build(BuildContext context) { + if (photoUrls.length == 1) { + return _PhotoTile(url: photoUrls.first, fallbackColor: fallbackColor); + } + + return Row( + children: [ + Expanded( + flex: 3, + child: _PhotoTile(url: photoUrls[0], fallbackColor: fallbackColor), + ), + const SizedBox(width: 2), + Expanded( + flex: 2, + child: Column( + children: [ + Expanded( + child: _PhotoTile( + url: photoUrls[1], + fallbackColor: fallbackColor, + ), + ), + if (photoUrls.length > 2) ...[ + const SizedBox(height: 2), + Expanded( + child: _PhotoTile( + url: photoUrls[2], + fallbackColor: fallbackColor, + ), + ), + ], + ], + ), + ), + ], + ); + } +} + +class _PhotoTile extends StatelessWidget { + const _PhotoTile({required this.url, required this.fallbackColor}); + + final String url; + final Color fallbackColor; + + @override + Widget build(BuildContext context) { + return Image.network( + url, + fit: BoxFit.cover, + errorBuilder: (_, _, _) => ColoredBox( + color: fallbackColor, + child: const Icon(Icons.place_outlined), + ), + ); + } +}