Switch web map to Mapbox SDK
Build and deploy Flutter Web / build (push) Failing after 5m12s

This commit is contained in:
Ruslan Bakiev
2026-06-24 01:27:28 +07:00
parent b64d202c4b
commit 817da526e9
7 changed files with 322 additions and 93 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
--tag "$IMAGE_SHA" \
--tag "$IMAGE_LATEST" \
--build-arg MAPBOX_ACCESS_TOKEN="${{ secrets.MAPBOX_ACCESS_TOKEN }}" \
--build-arg MAPBOX_STYLE="mapbox/streets-v12" \
--build-arg MAPBOX_STYLE="mapbox/standard" \
--build-arg TELEGRAM_BOT_USERNAME="carfteebot" \
--build-arg BUILD_VERSION="${GITHUB_SHA}" \
. &&
+1 -1
View File
@@ -1,7 +1,7 @@
FROM ghcr.io/cirruslabs/flutter:stable AS build
WORKDIR /app
ARG MAPBOX_ACCESS_TOKEN=""
ARG MAPBOX_STYLE="mapbox/streets-v12"
ARG MAPBOX_STYLE="mapbox/standard"
ARG TELEGRAM_BOT_USERNAME="carfteebot"
ARG BUILD_VERSION="local"
COPY pubspec.* ./
@@ -65,7 +65,7 @@ class PlaceState {
return place;
}
}
return visiblePlaces.isEmpty ? null : visiblePlaces.first;
return null;
}
PlaceState copyWith({
@@ -165,7 +165,7 @@ class PlaceCubit extends Cubit<PlaceViewState> {
PlaceState(
selectedTrait: null,
places: places,
selectedPlaceId: places.isEmpty ? null : places.first.id,
selectedPlaceId: null,
currentUser: currentUser,
hasTelegramAuth: _authRepository.hasTelegramAuth,
userCoordinate: null,
@@ -201,10 +201,9 @@ class PlaceCubit extends Cubit<PlaceViewState> {
void clearTrait() {
final value = _requireReady();
final selectedPlaceId = value.places.isEmpty ? null : value.places.first.id;
emit(
PlaceViewState.ready(
value.copyWith(selectedTrait: null, selectedPlaceId: selectedPlaceId),
value.copyWith(selectedTrait: null, selectedPlaceId: null),
),
);
}
@@ -337,12 +336,11 @@ class PlaceCubit extends Cubit<PlaceViewState> {
);
final places = await _placesRepository.fetchPlaces();
final selectedPlace = places.isEmpty ? null : places.first.id;
emit(
PlaceViewState.ready(
value.copyWith(
places: places,
selectedPlaceId: selectedPlace,
selectedPlaceId: null,
reviewDraft: _emptyDraft,
voiceFilterTranscript: '',
voiceFilterTags: const [],
@@ -4,10 +4,10 @@ import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:latlong2/latlong.dart' hide Path;
import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart' as mbx;
import 'package:waveform_flutter/waveform_flutter.dart' show Amplitude;
import 'package:waveform_recorder/waveform_recorder.dart';
@@ -21,10 +21,9 @@ import '../data/places_repository.dart';
import '../domain/place_models.dart';
import 'widgets/place_photo_card.dart';
const _mapboxAccessToken = String.fromEnvironment('MAPBOX_ACCESS_TOKEN');
const _mapboxStyle = String.fromEnvironment(
'MAPBOX_STYLE',
defaultValue: 'mapbox/streets-v12',
defaultValue: 'mapbox/standard',
);
class MapflowShell extends StatelessWidget {
@@ -51,18 +50,24 @@ class MapflowShell extends StatelessWidget {
}
}
class _MapContent extends StatelessWidget {
class _MapContent extends StatefulWidget {
const _MapContent({required this.state});
static const _fallbackCenter = LatLng(10.7718, 106.6982);
final PlaceState state;
@override
State<_MapContent> createState() => _MapContentState();
}
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 = userCoordinate ?? selected?.coordinate ?? _fallbackCenter;
final mapCenter = _focusCenter(state) ?? _fallbackCenter;
final placeCubit = context.read<PlaceCubit>();
final traitCounts = _countPlaceTraits(state.places);
final availableTraits = [
@@ -81,38 +86,13 @@ class _MapContent extends StatelessWidget {
return Scaffold(
body: Stack(
children: [
FlutterMap(
options: MapOptions(
_MapboxMapLayer(
initialCenter: mapCenter,
initialZoom: 14.2,
minZoom: 3,
maxZoom: 18,
),
children: [
const _BaseMapTileLayer(),
MarkerLayer(
markers: [
for (final place in state.recommendations)
Marker(
width: 52,
height: 52,
point: place.coordinate,
child: _PlaceMarker(
selected: selected?.id == place.id,
onTap: () => _selectAndShowPlace(context, place),
),
),
if (userCoordinate != null)
Marker(
width: 30,
height: 30,
point: userCoordinate,
child: const _UserLocationMarker(),
),
],
),
const _MapAttribution(),
],
focusCenter: _focusCenter(state),
places: state.recommendations,
selectedPlaceId: selected?.id,
userCoordinate: userCoordinate,
onPlaceTap: (place) => _selectAndShowPlace(context, place),
),
SafeArea(
child: Align(
@@ -183,6 +163,10 @@ class _MapContent extends StatelessWidget {
);
}
LatLng? _focusCenter(PlaceState state) {
return state.userCoordinate ?? state.selectedPlace?.coordinate;
}
Future<void> _openSearchSheet(
BuildContext context, {
required List<PlaceTrait> traits,
@@ -314,6 +298,169 @@ class _UserLocationMarker extends StatelessWidget {
}
}
class _ProjectedMarker {
const _ProjectedMarker({required this.place, required this.offset});
final PlaceRecommendation place;
final Offset offset;
}
class _MapboxMapLayer extends StatefulWidget {
const _MapboxMapLayer({
required this.initialCenter,
required this.focusCenter,
required this.places,
required this.selectedPlaceId,
required this.userCoordinate,
required this.onPlaceTap,
});
final LatLng initialCenter;
final LatLng? focusCenter;
final List<PlaceRecommendation> places;
final String? selectedPlaceId;
final LatLng? userCoordinate;
final ValueChanged<PlaceRecommendation> onPlaceTap;
@override
State<_MapboxMapLayer> createState() => _MapboxMapLayerState();
}
class _MapboxMapLayerState extends State<_MapboxMapLayer> {
mbx.MapboxMap? _mapboxMap;
var _mapLoaded = false;
var _didInitialFlyIn = false;
var _projectionVersion = 0;
LatLng? _lastFocusCenter;
List<_ProjectedMarker> _projectedPlaces = const [];
Offset? _projectedUserCoordinate;
@override
void didUpdateWidget(covariant _MapboxMapLayer oldWidget) {
super.didUpdateWidget(oldWidget);
_updateCameraForFocus();
unawaited(_projectMarkers());
}
@override
Widget build(BuildContext context) {
return Stack(
fit: StackFit.expand,
children: [
mbx.MapWidget(
styleUri: _mapboxStyleUri,
viewport: mbx.CameraViewportState(
center: _mapboxPoint(widget.initialCenter),
zoom: _didInitialFlyIn ? 14.8 : 1.25,
bearing: _didInitialFlyIn ? -16 : 0,
pitch: _didInitialFlyIn ? 62 : 0,
),
onMapCreated: _onMapCreated,
onMapLoadedListener: (_) {
_mapLoaded = true;
_updateCameraForFocus();
unawaited(_projectMarkers());
},
onCameraChangeListener: (_) => unawaited(_projectMarkers()),
onMapIdleListener: (_) => unawaited(_projectMarkers()),
),
for (final projected in _projectedPlaces)
Positioned(
left: projected.offset.dx - 26,
top: projected.offset.dy - 26,
width: 52,
height: 52,
child: _PlaceMarker(
selected: projected.place.id == widget.selectedPlaceId,
onTap: () => widget.onPlaceTap(projected.place),
),
),
if (_projectedUserCoordinate != null)
Positioned(
left: _projectedUserCoordinate!.dx - 15,
top: _projectedUserCoordinate!.dy - 15,
width: 30,
height: 30,
child: const _UserLocationMarker(),
),
],
);
}
void _onMapCreated(mbx.MapboxMap mapboxMap) {
_mapboxMap = mapboxMap;
}
void _updateCameraForFocus() {
final mapboxMap = _mapboxMap;
final focusCenter = widget.focusCenter;
if (!_mapLoaded || mapboxMap == null || focusCenter == null) {
return;
}
if (focusCenter == _lastFocusCenter) {
return;
}
_lastFocusCenter = focusCenter;
final camera = mbx.CameraOptions(
center: _mapboxPoint(focusCenter),
zoom: 14.8,
bearing: -16,
pitch: 62,
);
if (_didInitialFlyIn) {
unawaited(
mapboxMap.easeTo(camera, mbx.MapAnimationOptions(duration: 900)),
);
return;
}
_didInitialFlyIn = true;
unawaited(mapboxMap.flyTo(camera, mbx.MapAnimationOptions(duration: 2600)));
}
Future<void> _projectMarkers() async {
final mapboxMap = _mapboxMap;
if (!_mapLoaded || mapboxMap == null) {
return;
}
final version = ++_projectionVersion;
final coordinates = [
for (final place in widget.places) _mapboxPoint(place.coordinate),
if (widget.userCoordinate != null) _mapboxPoint(widget.userCoordinate!),
];
if (coordinates.isEmpty) {
if (!mounted) {
return;
}
setState(() {
_projectedPlaces = const [];
_projectedUserCoordinate = null;
});
return;
}
final pixels = await mapboxMap.pixelsForCoordinates(coordinates);
if (!mounted || version != _projectionVersion) {
return;
}
setState(() {
_projectedPlaces = [
for (var index = 0; index < widget.places.length; index++)
_ProjectedMarker(
place: widget.places[index],
offset: Offset(pixels[index].x, pixels[index].y),
),
];
_projectedUserCoordinate = widget.userCoordinate == null
? null
: Offset(pixels.last.x, pixels.last.y);
});
}
}
class _UserAvatar extends StatelessWidget {
const _UserAvatar({
required this.user,
@@ -916,12 +1063,13 @@ class _MapError extends StatelessWidget {
return Scaffold(
body: Stack(
children: [
FlutterMap(
options: const MapOptions(
initialCenter: LatLng(10.7718, 106.6982),
initialZoom: 14.2,
),
children: [const _BaseMapTileLayer(), const _MapAttribution()],
const _MapboxMapLayer(
initialCenter: LatLng(16.0544, 108.2022),
focusCenter: LatLng(16.0544, 108.2022),
places: [],
selectedPlaceId: null,
userCoordinate: null,
onPlaceTap: _ignorePlaceTap,
),
SafeArea(
child: Align(
@@ -944,50 +1092,22 @@ class _MapError extends StatelessWidget {
}
}
class _BaseMapTileLayer extends StatelessWidget {
const _BaseMapTileLayer();
void _ignorePlaceTap(PlaceRecommendation _) {}
static const _osmUrl = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png';
@override
Widget build(BuildContext context) {
if (_mapboxAccessToken.isEmpty) {
return TileLayer(
urlTemplate: _osmUrl,
userAgentPackageName: 'com.mapflow.app',
mbx.Point _mapboxPoint(LatLng coordinate) {
return mbx.Point(
coordinates: mbx.Position(coordinate.longitude, coordinate.latitude),
);
}
return TileLayer(
urlTemplate:
'https://api.mapbox.com/styles/v1/$_mapboxStyle/tiles/512/{z}/{x}/{y}@2x'
'?access_token=$_mapboxAccessToken',
tileDimension: 512,
zoomOffset: -1,
maxNativeZoom: 22,
userAgentPackageName: 'com.mapflow.app',
);
String get _mapboxStyleUri {
if (_mapboxStyle.startsWith('mapbox://')) {
return _mapboxStyle;
}
if (_mapboxStyle.startsWith('mapbox/')) {
return 'mapbox://styles/$_mapboxStyle';
}
class _MapAttribution extends StatelessWidget {
const _MapAttribution();
@override
Widget build(BuildContext context) {
if (_mapboxAccessToken.isEmpty) {
return const RichAttributionWidget(
attributions: [TextSourceAttribution('OpenStreetMap contributors')],
);
}
return const RichAttributionWidget(
attributions: [
TextSourceAttribution('Mapbox', prependCopyright: false),
TextSourceAttribution('OpenStreetMap contributors'),
],
);
}
return _mapboxStyle;
}
class _TraitPickerSheet extends StatelessWidget {
+6
View File
@@ -1,11 +1,17 @@
import 'package:flutter/material.dart';
import 'package:flutter_web_plugins/url_strategy.dart';
import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart' as mbx;
import 'app/app.dart';
import 'shared/auth/telegram_session.dart' as telegram_session;
const _mapboxAccessToken = String.fromEnvironment('MAPBOX_ACCESS_TOKEN');
void main() {
WidgetsFlutterBinding.ensureInitialized();
if (_mapboxAccessToken.isNotEmpty) {
mbx.MapboxOptions.setAccessToken(_mapboxAccessToken);
}
usePathUrlStrategy();
telegram_session.configureTelegramWebApp();
runApp(const MapflowApp());
+104
View File
@@ -41,6 +41,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.13.1"
benchmark:
dependency: transitive
description:
name: benchmark
sha256: cb3eeea01e3f054df76ee9775ca680f3afa5f19f39b2bb426ba78ba27654493b
url: "https://pub.dev"
source: hosted
version: "0.3.0"
bloc:
dependency: transitive
description:
@@ -209,6 +217,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.0"
dart_sort_queue:
dependency: transitive
description:
name: dart_sort_queue
sha256: f3353ba8b4850e072d3368757f62edb79af34a9703c3e3df9c59342721f5f5b1
url: "https://pub.dev"
source: hosted
version: "0.0.2+3"
dart_style:
dependency: transitive
description:
@@ -342,6 +358,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "8.3.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785"
url: "https://pub.dev"
source: hosted
version: "2.0.35"
flutter_svg:
dependency: "direct main"
description:
@@ -424,6 +448,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.2.5"
geotypes:
dependency: transitive
description:
name: geotypes
sha256: "5bedf57de92283133dd221e363812ef50eaaba414f0823b1974ef7d84b86991f"
url: "https://pub.dev"
source: hosted
version: "0.0.2"
glob:
dependency: transitive
description:
@@ -640,6 +672,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.0"
mapbox_maps_flutter:
dependency: "direct main"
description:
name: mapbox_maps_flutter
sha256: f98e3808113088c6e0bc60f592d23eb5a23215b10a9389754642c8e5f9bdc117
url: "https://pub.dev"
source: hosted
version: "3.0.0-alpha.1"
mapbox_maps_flutter_mobile:
dependency: transitive
description:
name: mapbox_maps_flutter_mobile
sha256: "4ef1a6cb23dfe4a6f5bb896ea7b1195dd474aaffd056bab77434fda03bc9446b"
url: "https://pub.dev"
source: hosted
version: "3.0.0-alpha.1"
mapbox_maps_flutter_platform_interface:
dependency: transitive
description:
name: mapbox_maps_flutter_platform_interface
sha256: e64dc1df85cb2fe5c7d704771a01e53b6d6ccdafb8be70c2879c0d7e8bf6cbd8
url: "https://pub.dev"
source: hosted
version: "3.0.0-alpha.1"
mapbox_maps_flutter_web:
dependency: transitive
description:
name: mapbox_maps_flutter_web
sha256: "01ef5f2b4578e33cbdae27d05b7b9aa37246b205746a552f507d66b0deaf20cc"
url: "https://pub.dev"
source: hosted
version: "3.0.0-alpha.1"
matcher:
dependency: transitive
description:
@@ -880,6 +944,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.5.0"
rbush:
dependency: transitive
description:
name: rbush
sha256: "48b683421b4afb43a642f82c6aa31911e54f3069143d31c7d33cbe329df13403"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
record:
dependency: transitive
description:
@@ -1045,6 +1117,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.1.5"
sweepline_intersections:
dependency: transitive
description:
name: sweepline_intersections
sha256: a665c707200a4f07140a4029b41a7c4883beb3f04322cd8e08ebf650f69e1176
url: "https://pub.dev"
source: hosted
version: "0.0.4"
term_glyph:
dependency: transitive
description:
@@ -1061,6 +1141,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.10"
turf:
dependency: transitive
description:
name: turf
sha256: "7684654c561e751332bc2fa6e9bb7bcf0c71bad5315acf126206e7ecfef67256"
url: "https://pub.dev"
source: hosted
version: "0.0.12"
turf_equality:
dependency: transitive
description:
name: turf_equality
sha256: f0f44ffe389547941358e0d3d4a747db2bd56115b32ff1cede5e5bdf3126a3e2
url: "https://pub.dev"
source: hosted
version: "0.1.0"
turf_pip:
dependency: transitive
description:
name: turf_pip
sha256: ba4fd414baffd5d7b30880658ad6db82461c49ec023f8ffd0c23d398ad8b14be
url: "https://pub.dev"
source: hosted
version: "0.0.2"
typed_data:
dependency: transitive
description:
+1
View File
@@ -51,6 +51,7 @@ dependencies:
gql_code_builder_serializers: ^0.1.0+1
built_collection: ^5.1.1
built_value: ^8.12.6
mapbox_maps_flutter: ^3.0.0-alpha.1
dev_dependencies:
flutter_test: