From 6823b6d72583be34f35a2e76b6858c38d0618b0a Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev <572431+veikab@users.noreply.github.com> Date: Wed, 24 Jun 2026 12:17:38 +0700 Subject: [PATCH] Polish startup loader and map fly-in --- instructions | 2 +- .../mapflow/presentation/mapflow_shell.dart | 135 ++++++++---------- web/icons/mapflow-signal-spoon.svg | 41 ++++++ web/index.prod.html | 20 ++- 4 files changed, 121 insertions(+), 77 deletions(-) create mode 100644 web/icons/mapflow-signal-spoon.svg diff --git a/instructions b/instructions index 9e16b68..bf94f17 160000 --- a/instructions +++ b/instructions @@ -1 +1 @@ -Subproject commit 9e16b68277faca494de422380e280a9da9400530 +Subproject commit bf94f17aa29e6d2dde97af2d59aac84d683e98f0 diff --git a/lib/features/mapflow/presentation/mapflow_shell.dart b/lib/features/mapflow/presentation/mapflow_shell.dart index 47f2967..f9bf8ae 100644 --- a/lib/features/mapflow/presentation/mapflow_shell.dart +++ b/lib/features/mapflow/presentation/mapflow_shell.dart @@ -62,14 +62,11 @@ class _MapContent extends StatefulWidget { } class _MapContentState extends State<_MapContent> { - static const _fallbackCenter = LatLng(16.0544, 108.2022); - @override Widget build(BuildContext context) { final state = widget.state; final selected = state.selectedPlace; final userCoordinate = state.userCoordinate; - final mapCenter = _focusCenter(state) ?? _fallbackCenter; final placeCubit = context.read(); final traitCounts = _countPlaceTraits(state.places); final availableTraits = [ @@ -89,7 +86,6 @@ class _MapContentState extends State<_MapContent> { body: Stack( children: [ _MapboxMapLayer( - initialCenter: mapCenter, focusCenter: _focusCenter(state), places: state.recommendations, selectedPlaceId: selected?.id, @@ -309,7 +305,6 @@ class _ProjectedMarker { class _MapboxMapLayer extends StatefulWidget { const _MapboxMapLayer({ - required this.initialCenter, required this.focusCenter, required this.places, required this.selectedPlaceId, @@ -317,7 +312,6 @@ class _MapboxMapLayer extends StatefulWidget { required this.onPlaceTap, }); - final LatLng initialCenter; final LatLng? focusCenter; final List places; final String? selectedPlaceId; @@ -334,7 +328,9 @@ class _MapboxMapLayerState extends State<_MapboxMapLayer> { mbx.MapboxMap? _mapboxMap; var _mapLoaded = false; var _didInitialFlyIn = false; + var _cameraAnimationSequence = 0; var _projectionVersion = 0; + Timer? _initialFlyInTimer; LatLng? _lastFocusCenter; List<_ProjectedMarker> _projectedPlaces = const []; Offset? _projectedUserCoordinate; @@ -354,12 +350,10 @@ class _MapboxMapLayerState extends State<_MapboxMapLayer> { mbx.MapWidget( styleUri: _mapboxStyleUri, viewport: mbx.CameraViewportState( - center: _mapboxPoint( - _didInitialFlyIn ? widget.initialCenter : _worldCenter, - ), - zoom: _didInitialFlyIn ? 14.8 : 0.58, - bearing: _didInitialFlyIn ? -16 : -24, - pitch: _didInitialFlyIn ? 62 : 0, + center: _mapboxPoint(_worldCenter), + zoom: 0.58, + bearing: -42, + pitch: 0, ), onMapCreated: _onMapCreated, onStyleLoadedListener: (_) { @@ -402,6 +396,12 @@ class _MapboxMapLayerState extends State<_MapboxMapLayer> { _mapboxMap = mapboxMap; } + @override + void dispose() { + _initialFlyInTimer?.cancel(); + super.dispose(); + } + Future _enableGlobeProjection() async { final mapboxMap = _mapboxMap; if (mapboxMap == null) { @@ -430,6 +430,7 @@ class _MapboxMapLayerState extends State<_MapboxMapLayer> { pitch: 62, ); if (_didInitialFlyIn) { + _cameraAnimationSequence++; unawaited( mapboxMap.easeTo(camera, mbx.MapAnimationOptions(duration: 900)), ); @@ -437,12 +438,29 @@ class _MapboxMapLayerState extends State<_MapboxMapLayer> { } _didInitialFlyIn = true; + final animationSequence = ++_cameraAnimationSequence; unawaited( - mapboxMap.flyTo( - camera, - mbx.MapAnimationOptions(duration: 4200, startDelay: 350), + mapboxMap.easeTo( + mbx.CameraOptions( + center: _mapboxPoint(_worldCenter), + zoom: 0.62, + bearing: 26, + pitch: 0, + ), + mbx.MapAnimationOptions(duration: 1100), ), ); + _initialFlyInTimer?.cancel(); + _initialFlyInTimer = Timer(const Duration(milliseconds: 760), () { + if (!mounted || + animationSequence != _cameraAnimationSequence || + _mapboxMap != mapboxMap) { + return; + } + unawaited( + mapboxMap.flyTo(camera, mbx.MapAnimationOptions(duration: 4600)), + ); + }); } Future _projectMarkers() async { @@ -1043,9 +1061,7 @@ class _TelegramLoginScreenState extends State<_TelegramLoginScreen> { } class _AnimatedEarthBackdrop extends StatefulWidget { - const _AnimatedEarthBackdrop({this.compact = false}); - - final bool compact; + const _AnimatedEarthBackdrop(); @override State<_AnimatedEarthBackdrop> createState() => _AnimatedEarthBackdropState(); @@ -1080,7 +1096,6 @@ class _AnimatedEarthBackdropState extends State<_AnimatedEarthBackdrop> painter: _EarthBackdropPainter( progress: _controller.value, tokens: context.mapflowTokens, - compact: widget.compact, ), child: const SizedBox.expand(), ); @@ -1091,15 +1106,10 @@ class _AnimatedEarthBackdropState extends State<_AnimatedEarthBackdrop> } class _EarthBackdropPainter extends CustomPainter { - const _EarthBackdropPainter({ - required this.progress, - required this.tokens, - required this.compact, - }); + const _EarthBackdropPainter({required this.progress, required this.tokens}); final double progress; final MapflowThemeTokens tokens; - final bool compact; static const _landMasses = <({double lon, double lat, double sx, double sy})>[ (lon: -106, lat: 42, sx: 0.30, sy: 0.18), @@ -1432,78 +1442,58 @@ class _EarthBackdropPainter extends CustomPainter { } Offset _earthCenter(Size size) { - if (compact) { - return Offset(size.width * 0.50, size.height * 0.39); - } return Offset(size.width * 0.50, size.height * 0.40); } double _earthRadius(Size size) { final base = math.min(size.width, size.height); - final radius = base * (compact ? 0.18 : 0.31); - return radius.clamp(116.0, compact ? 180.0 : 310.0); + final radius = base * 0.31; + return radius.clamp(116.0, 310.0); } double _radians(double degrees) => degrees * math.pi / 180; @override bool shouldRepaint(covariant _EarthBackdropPainter oldDelegate) { - return oldDelegate.progress != progress || - oldDelegate.tokens != tokens || - oldDelegate.compact != compact; + return oldDelegate.progress != progress || oldDelegate.tokens != tokens; } } class _MapLoading extends StatelessWidget { const _MapLoading(); + static const _iconPath = 'icons/mapflow-signal-spoon.svg'; + @override Widget build(BuildContext context) { final tokens = context.mapflowTokens; return Scaffold( backgroundColor: tokens.darkSurface, - body: Stack( - children: [ - const Positioned.fill(child: _AnimatedEarthBackdrop(compact: true)), - Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(tokens.panelRadius), - child: Image.network( - 'icons/Icon-192.png', - width: 64, - height: 64, - errorBuilder: (_, _, _) => Icon( - Icons.map_outlined, - color: tokens.voiceAccent, - size: 56, - ), - ), + body: Center( + child: SizedBox.square( + dimension: 112, + child: Stack( + alignment: Alignment.center, + children: [ + SizedBox.square( + dimension: 96, + child: CircularProgressIndicator( + strokeWidth: 3, + color: tokens.voiceAccent, + backgroundColor: tokens.onDark.withValues(alpha: 0.1), ), - const SizedBox(height: 18), - Text( - 'MapFlow', - style: Theme.of(context).textTheme.titleLarge?.copyWith( - color: tokens.onDark, - fontWeight: FontWeight.w900, - letterSpacing: 0, - ), + ), + ClipOval( + child: SvgPicture.network( + _iconPath, + width: 72, + height: 72, + fit: BoxFit.cover, ), - const SizedBox(height: 18), - SizedBox( - width: 96, - child: LinearProgressIndicator( - minHeight: 3, - color: tokens.voiceAccent, - backgroundColor: tokens.onDark.withValues(alpha: 0.12), - ), - ), - ], - ), + ), + ], ), - ], + ), ), ); } @@ -1522,7 +1512,6 @@ class _MapError extends StatelessWidget { body: Stack( children: [ const _MapboxMapLayer( - initialCenter: LatLng(16.0544, 108.2022), focusCenter: LatLng(16.0544, 108.2022), places: [], selectedPlaceId: null, diff --git a/web/icons/mapflow-signal-spoon.svg b/web/icons/mapflow-signal-spoon.svg new file mode 100644 index 0000000..50b6276 --- /dev/null +++ b/web/icons/mapflow-signal-spoon.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/index.prod.html b/web/index.prod.html index 4b4b804..782d82e 100644 --- a/web/index.prod.html +++ b/web/index.prod.html @@ -27,6 +27,21 @@ color: #F8EEFF !important; } + #sw-loading .sw-logo-container { + margin-bottom: 0 !important; + } + + #sw-loading .sw-logo-image { + border-radius: 50% !important; + box-shadow: 0 0 32px rgba(255, 45, 117, 0.34) !important; + } + + #sw-loading .sw-title, + #sw-loading .sw-status, + #sw-loading .sw-percentage { + display: none !important; + } + html, body { background: #05030B; @@ -95,11 +110,10 @@ data-sw-bootstrap src="bootstrap.js" data-config='{ - "logo": "icons/Icon-192.png?v={{sw_version}}", - "title": "MapFlow", + "logo": "icons/mapflow-signal-spoon.svg?v={{sw_version}}", "theme": "dark", "color": "#FF2D75", - "showPercentage": true + "showPercentage": false }' >