Polish startup loader and map fly-in
Build and deploy Flutter Web / build (push) Successful in 4m35s

This commit is contained in:
Ruslan Bakiev
2026-06-24 12:17:38 +07:00
parent 66ab3c2e0b
commit 6823b6d725
4 changed files with 121 additions and 77 deletions
@@ -62,14 +62,11 @@ class _MapContent extends StatefulWidget {
} }
class _MapContentState extends State<_MapContent> { class _MapContentState extends State<_MapContent> {
static const _fallbackCenter = LatLng(16.0544, 108.2022);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final state = widget.state; final state = widget.state;
final selected = state.selectedPlace; final selected = state.selectedPlace;
final userCoordinate = state.userCoordinate; final userCoordinate = state.userCoordinate;
final mapCenter = _focusCenter(state) ?? _fallbackCenter;
final placeCubit = context.read<PlaceCubit>(); final placeCubit = context.read<PlaceCubit>();
final traitCounts = _countPlaceTraits(state.places); final traitCounts = _countPlaceTraits(state.places);
final availableTraits = [ final availableTraits = [
@@ -89,7 +86,6 @@ class _MapContentState extends State<_MapContent> {
body: Stack( body: Stack(
children: [ children: [
_MapboxMapLayer( _MapboxMapLayer(
initialCenter: mapCenter,
focusCenter: _focusCenter(state), focusCenter: _focusCenter(state),
places: state.recommendations, places: state.recommendations,
selectedPlaceId: selected?.id, selectedPlaceId: selected?.id,
@@ -309,7 +305,6 @@ class _ProjectedMarker {
class _MapboxMapLayer extends StatefulWidget { class _MapboxMapLayer extends StatefulWidget {
const _MapboxMapLayer({ const _MapboxMapLayer({
required this.initialCenter,
required this.focusCenter, required this.focusCenter,
required this.places, required this.places,
required this.selectedPlaceId, required this.selectedPlaceId,
@@ -317,7 +312,6 @@ class _MapboxMapLayer extends StatefulWidget {
required this.onPlaceTap, required this.onPlaceTap,
}); });
final LatLng initialCenter;
final LatLng? focusCenter; final LatLng? focusCenter;
final List<PlaceRecommendation> places; final List<PlaceRecommendation> places;
final String? selectedPlaceId; final String? selectedPlaceId;
@@ -334,7 +328,9 @@ class _MapboxMapLayerState extends State<_MapboxMapLayer> {
mbx.MapboxMap? _mapboxMap; mbx.MapboxMap? _mapboxMap;
var _mapLoaded = false; var _mapLoaded = false;
var _didInitialFlyIn = false; var _didInitialFlyIn = false;
var _cameraAnimationSequence = 0;
var _projectionVersion = 0; var _projectionVersion = 0;
Timer? _initialFlyInTimer;
LatLng? _lastFocusCenter; LatLng? _lastFocusCenter;
List<_ProjectedMarker> _projectedPlaces = const []; List<_ProjectedMarker> _projectedPlaces = const [];
Offset? _projectedUserCoordinate; Offset? _projectedUserCoordinate;
@@ -354,12 +350,10 @@ class _MapboxMapLayerState extends State<_MapboxMapLayer> {
mbx.MapWidget( mbx.MapWidget(
styleUri: _mapboxStyleUri, styleUri: _mapboxStyleUri,
viewport: mbx.CameraViewportState( viewport: mbx.CameraViewportState(
center: _mapboxPoint( center: _mapboxPoint(_worldCenter),
_didInitialFlyIn ? widget.initialCenter : _worldCenter, zoom: 0.58,
), bearing: -42,
zoom: _didInitialFlyIn ? 14.8 : 0.58, pitch: 0,
bearing: _didInitialFlyIn ? -16 : -24,
pitch: _didInitialFlyIn ? 62 : 0,
), ),
onMapCreated: _onMapCreated, onMapCreated: _onMapCreated,
onStyleLoadedListener: (_) { onStyleLoadedListener: (_) {
@@ -402,6 +396,12 @@ class _MapboxMapLayerState extends State<_MapboxMapLayer> {
_mapboxMap = mapboxMap; _mapboxMap = mapboxMap;
} }
@override
void dispose() {
_initialFlyInTimer?.cancel();
super.dispose();
}
Future<void> _enableGlobeProjection() async { Future<void> _enableGlobeProjection() async {
final mapboxMap = _mapboxMap; final mapboxMap = _mapboxMap;
if (mapboxMap == null) { if (mapboxMap == null) {
@@ -430,6 +430,7 @@ class _MapboxMapLayerState extends State<_MapboxMapLayer> {
pitch: 62, pitch: 62,
); );
if (_didInitialFlyIn) { if (_didInitialFlyIn) {
_cameraAnimationSequence++;
unawaited( unawaited(
mapboxMap.easeTo(camera, mbx.MapAnimationOptions(duration: 900)), mapboxMap.easeTo(camera, mbx.MapAnimationOptions(duration: 900)),
); );
@@ -437,12 +438,29 @@ class _MapboxMapLayerState extends State<_MapboxMapLayer> {
} }
_didInitialFlyIn = true; _didInitialFlyIn = true;
final animationSequence = ++_cameraAnimationSequence;
unawaited( unawaited(
mapboxMap.flyTo( mapboxMap.easeTo(
camera, mbx.CameraOptions(
mbx.MapAnimationOptions(duration: 4200, startDelay: 350), 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<void> _projectMarkers() async { Future<void> _projectMarkers() async {
@@ -1043,9 +1061,7 @@ class _TelegramLoginScreenState extends State<_TelegramLoginScreen> {
} }
class _AnimatedEarthBackdrop extends StatefulWidget { class _AnimatedEarthBackdrop extends StatefulWidget {
const _AnimatedEarthBackdrop({this.compact = false}); const _AnimatedEarthBackdrop();
final bool compact;
@override @override
State<_AnimatedEarthBackdrop> createState() => _AnimatedEarthBackdropState(); State<_AnimatedEarthBackdrop> createState() => _AnimatedEarthBackdropState();
@@ -1080,7 +1096,6 @@ class _AnimatedEarthBackdropState extends State<_AnimatedEarthBackdrop>
painter: _EarthBackdropPainter( painter: _EarthBackdropPainter(
progress: _controller.value, progress: _controller.value,
tokens: context.mapflowTokens, tokens: context.mapflowTokens,
compact: widget.compact,
), ),
child: const SizedBox.expand(), child: const SizedBox.expand(),
); );
@@ -1091,15 +1106,10 @@ class _AnimatedEarthBackdropState extends State<_AnimatedEarthBackdrop>
} }
class _EarthBackdropPainter extends CustomPainter { class _EarthBackdropPainter extends CustomPainter {
const _EarthBackdropPainter({ const _EarthBackdropPainter({required this.progress, required this.tokens});
required this.progress,
required this.tokens,
required this.compact,
});
final double progress; final double progress;
final MapflowThemeTokens tokens; final MapflowThemeTokens tokens;
final bool compact;
static const _landMasses = <({double lon, double lat, double sx, double sy})>[ static const _landMasses = <({double lon, double lat, double sx, double sy})>[
(lon: -106, lat: 42, sx: 0.30, sy: 0.18), (lon: -106, lat: 42, sx: 0.30, sy: 0.18),
@@ -1432,78 +1442,58 @@ class _EarthBackdropPainter extends CustomPainter {
} }
Offset _earthCenter(Size size) { 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); return Offset(size.width * 0.50, size.height * 0.40);
} }
double _earthRadius(Size size) { double _earthRadius(Size size) {
final base = math.min(size.width, size.height); final base = math.min(size.width, size.height);
final radius = base * (compact ? 0.18 : 0.31); final radius = base * 0.31;
return radius.clamp(116.0, compact ? 180.0 : 310.0); return radius.clamp(116.0, 310.0);
} }
double _radians(double degrees) => degrees * math.pi / 180; double _radians(double degrees) => degrees * math.pi / 180;
@override @override
bool shouldRepaint(covariant _EarthBackdropPainter oldDelegate) { bool shouldRepaint(covariant _EarthBackdropPainter oldDelegate) {
return oldDelegate.progress != progress || return oldDelegate.progress != progress || oldDelegate.tokens != tokens;
oldDelegate.tokens != tokens ||
oldDelegate.compact != compact;
} }
} }
class _MapLoading extends StatelessWidget { class _MapLoading extends StatelessWidget {
const _MapLoading(); const _MapLoading();
static const _iconPath = 'icons/mapflow-signal-spoon.svg';
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final tokens = context.mapflowTokens; final tokens = context.mapflowTokens;
return Scaffold( return Scaffold(
backgroundColor: tokens.darkSurface, backgroundColor: tokens.darkSurface,
body: Stack( body: Center(
child: SizedBox.square(
dimension: 112,
child: Stack(
alignment: Alignment.center,
children: [ children: [
const Positioned.fill(child: _AnimatedEarthBackdrop(compact: true)), SizedBox.square(
Center( dimension: 96,
child: Column( child: CircularProgressIndicator(
mainAxisSize: MainAxisSize.min, strokeWidth: 3,
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, color: tokens.voiceAccent,
size: 56, backgroundColor: tokens.onDark.withValues(alpha: 0.1),
), ),
), ),
), ClipOval(
const SizedBox(height: 18), child: SvgPicture.network(
Text( _iconPath,
'MapFlow', width: 72,
style: Theme.of(context).textTheme.titleLarge?.copyWith( height: 72,
color: tokens.onDark, fit: BoxFit.cover,
fontWeight: FontWeight.w900,
letterSpacing: 0,
),
),
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( body: Stack(
children: [ children: [
const _MapboxMapLayer( const _MapboxMapLayer(
initialCenter: LatLng(16.0544, 108.2022),
focusCenter: LatLng(16.0544, 108.2022), focusCenter: LatLng(16.0544, 108.2022),
places: [], places: [],
selectedPlaceId: null, selectedPlaceId: null,
+41
View File
@@ -0,0 +1,41 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 192 192" role="img" aria-label="MapFlow">
<defs>
<radialGradient id="bg" cx="35%" cy="25%" r="85%">
<stop offset="0%" stop-color="#4DF2FF"/>
<stop offset="42%" stop-color="#8E4DFF"/>
<stop offset="100%" stop-color="#090411"/>
</radialGradient>
<linearGradient id="spoon" x1="62" y1="42" x2="126" y2="154">
<stop offset="0%" stop-color="#FFFFFF"/>
<stop offset="48%" stop-color="#D9FBFF"/>
<stop offset="100%" stop-color="#FF4FA3"/>
</linearGradient>
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="5" result="blur"/>
<feColorMatrix in="blur" values="0 0 0 0 1 0 0 0 0 0.18 0 0 0 0 0.52 0 0 0 0.9 0"/>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<rect width="192" height="192" rx="48" fill="url(#bg)"/>
<path d="M45 137c26 18 76 18 102 0" fill="none" stroke="#4DF2FF" stroke-width="7" stroke-linecap="round" opacity=".32"/>
<path d="M36 113c36 27 84 27 120 0" fill="none" stroke="#4DF2FF" stroke-width="6" stroke-linecap="round" opacity=".42"/>
<path d="M57 82c24-23 54-23 78 0" fill="none" stroke="#FFFFFF" stroke-width="5" stroke-linecap="round" opacity=".44"/>
<path d="M74 68c13-11 31-11 44 0" fill="none" stroke="#FFFFFF" stroke-width="4" stroke-linecap="round" opacity=".64"/>
<g filter="url(#glow)">
<ellipse cx="96" cy="50" rx="18" ry="24" fill="url(#spoon)" transform="rotate(-18 96 50)"/>
<path d="M95 70c13 19 19 39 18 64-.1 8-5.3 13.7-12.2 13.7S89 142 88.7 134c-.7-25 3.9-45.2 16.6-65.5" fill="url(#spoon)"/>
<circle cx="96" cy="92" r="8" fill="#090411"/>
<circle cx="96" cy="92" r="3.5" fill="#4DF2FF"/>
</g>
<g fill="none" stroke="#FFFFFF" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" opacity=".84">
<path d="M77 104l-18 12 19 15"/>
<path d="M115 104l18 12-19 15"/>
<path d="M78 104l18-12 19 12"/>
<circle cx="59" cy="116" r="5" fill="#FFFFFF"/>
<circle cx="133" cy="116" r="5" fill="#FFFFFF"/>
<circle cx="96" cy="92" r="5" fill="#FFFFFF"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

+17 -3
View File
@@ -27,6 +27,21 @@
color: #F8EEFF !important; 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, html,
body { body {
background: #05030B; background: #05030B;
@@ -95,11 +110,10 @@
data-sw-bootstrap data-sw-bootstrap
src="bootstrap.js" src="bootstrap.js"
data-config='{ data-config='{
"logo": "icons/Icon-192.png?v={{sw_version}}", "logo": "icons/mapflow-signal-spoon.svg?v={{sw_version}}",
"title": "MapFlow",
"theme": "dark", "theme": "dark",
"color": "#FF2D75", "color": "#FF2D75",
"showPercentage": true "showPercentage": false
}' }'
></script> ></script>
</body> </body>