Refactor Flutter app architecture
Build and deploy Flutter Web / build (push) Successful in 3m7s

This commit is contained in:
Ruslan Bakiev
2026-06-12 14:25:24 +07:00
parent be41f74b33
commit 5e78a134b0
17 changed files with 821 additions and 751 deletions
+76
View File
@@ -0,0 +1,76 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import '../features/mapflow/application/place_cubit.dart';
import '../features/mapflow/data/mapflow_api.dart';
import '../shared/location/current_location.dart';
import 'router/app_router.dart';
import 'theme/mapflow_theme.dart';
class MapflowApp extends StatefulWidget {
const MapflowApp({super.key});
@override
State<MapflowApp> createState() => _MapflowAppState();
}
class _MapflowAppState extends State<MapflowApp> {
late final PlaceCubit _placeCubit;
late final GoRouter _router;
@override
void initState() {
super.initState();
_placeCubit = PlaceCubit(api: MapflowApi(), location: CurrentLocation())
..load();
_router = createAppRouter();
}
@override
void dispose() {
_router.dispose();
_placeCubit.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
final shortcuts =
Map<ShortcutActivator, Intent>.of(WidgetsApp.defaultShortcuts)
..[const SingleActivator(LogicalKeyboardKey.tab)] =
const NextFocusIntent()
..[const SingleActivator(LogicalKeyboardKey.tab, shift: true)] =
const PreviousFocusIntent();
return BlocProvider.value(
value: _placeCubit,
child: MaterialApp.router(
title: 'MapFlow',
debugShowCheckedModeBanner: false,
routerConfig: _router,
scrollBehavior: const MaterialScrollBehavior().copyWith(
scrollbars: true,
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
PointerDeviceKind.trackpad,
PointerDeviceKind.stylus,
},
),
theme: MapflowTheme.light(),
builder: (context, child) {
return Shortcuts(
shortcuts: shortcuts,
child: FocusTraversalGroup(
policy: ReadingOrderTraversalPolicy(),
child: child ?? const SizedBox.shrink(),
),
);
},
),
);
}
}
+56
View File
@@ -0,0 +1,56 @@
import 'package:flutter/widgets.dart';
import 'package:go_router/go_router.dart';
import 'package:latlong2/latlong.dart';
import '../../features/mapflow/presentation/admin_voice_experiences_screen.dart';
import '../../features/mapflow/presentation/mapflow_shell.dart';
class AddExperienceArgs {
const AddExperienceArgs({
required this.coordinate,
required this.hasTelegramAuth,
});
final LatLng? coordinate;
final bool hasTelegramAuth;
}
GoRouter createAppRouter() {
return GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => const MapflowShell(),
routes: [
GoRoute(
path: 'experience/new',
pageBuilder: (context, state) {
final args = state.extra as AddExperienceArgs;
return CustomTransitionPage<void>(
fullscreenDialog: true,
key: state.pageKey,
child: AddExperienceFlow(
coordinate: args.coordinate,
hasTelegramAuth: args.hasTelegramAuth,
),
transitionsBuilder: (context, animation, _, child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 1),
end: Offset.zero,
).animate(animation),
child: child,
);
},
);
},
),
GoRoute(
path: 'admin/reviews',
builder: (context, state) => const AdminVoiceExperiencesScreen(),
),
],
),
],
);
}
+124
View File
@@ -0,0 +1,124 @@
import 'package:flex_color_scheme/flex_color_scheme.dart';
import 'package:flutter/material.dart';
@immutable
class MapflowThemeTokens extends ThemeExtension<MapflowThemeTokens> {
const MapflowThemeTokens({
required this.mapPanel,
required this.mapPanelBorder,
required this.mapShadow,
required this.danger,
required this.onDark,
required this.panelRadius,
});
final Color mapPanel;
final Color mapPanelBorder;
final Color mapShadow;
final Color danger;
final Color onDark;
final double panelRadius;
@override
MapflowThemeTokens copyWith({
Color? mapPanel,
Color? mapPanelBorder,
Color? mapShadow,
Color? danger,
Color? onDark,
double? panelRadius,
}) {
return MapflowThemeTokens(
mapPanel: mapPanel ?? this.mapPanel,
mapPanelBorder: mapPanelBorder ?? this.mapPanelBorder,
mapShadow: mapShadow ?? this.mapShadow,
danger: danger ?? this.danger,
onDark: onDark ?? this.onDark,
panelRadius: panelRadius ?? this.panelRadius,
);
}
@override
MapflowThemeTokens lerp(
covariant ThemeExtension<MapflowThemeTokens>? other,
double t,
) {
if (other is! MapflowThemeTokens) {
return this;
}
return MapflowThemeTokens(
mapPanel: Color.lerp(mapPanel, other.mapPanel, t)!,
mapPanelBorder: Color.lerp(mapPanelBorder, other.mapPanelBorder, t)!,
mapShadow: Color.lerp(mapShadow, other.mapShadow, t)!,
danger: Color.lerp(danger, other.danger, t)!,
onDark: Color.lerp(onDark, other.onDark, t)!,
panelRadius: lerpDouble(panelRadius, other.panelRadius, t),
);
}
}
class MapflowTheme {
const MapflowTheme._();
static const tokens = MapflowThemeTokens(
mapPanel: Color(0xFFFFFBF5),
mapPanelBorder: Color(0xFFE0D8CA),
mapShadow: Color(0x33000000),
danger: Color(0xFFE11D48),
onDark: Colors.white,
panelRadius: 8,
);
static ThemeData light() {
return FlexThemeData.light(
useMaterial3: true,
colors: const FlexSchemeColor(
primary: Color(0xFF0F766E),
primaryContainer: Color(0xFFBFE7DF),
secondary: Color(0xFFE11D48),
secondaryContainer: Color(0xFFFFD9E1),
tertiary: Color(0xFF7C3AED),
tertiaryContainer: Color(0xFFE9DDFF),
appBarColor: Color(0xFFF7F3EA),
error: Color(0xFFB3261E),
),
scaffoldBackground: const Color(0xFFF7F3EA),
surface: tokens.mapPanel,
subThemesData: const FlexSubThemesData(
defaultRadius: 8,
inputDecoratorRadius: 8,
cardRadius: 8,
chipRadius: 8,
filledButtonRadius: 8,
),
fontFamily: 'SF Pro Display',
extensions: const [tokens],
).copyWith(
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFFF7F3EA),
foregroundColor: Color(0xFF17211D),
surfaceTintColor: Colors.transparent,
),
cardTheme: CardThemeData(
elevation: 0,
color: tokens.mapPanel,
surfaceTintColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(tokens.panelRadius),
side: BorderSide(color: tokens.mapPanelBorder),
),
),
chipTheme: ChipThemeData(
shape: StadiumBorder(side: BorderSide(color: tokens.mapPanelBorder)),
),
);
}
}
double lerpDouble(double a, double b, double t) => a + (b - a) * t;
extension MapflowThemeContext on BuildContext {
MapflowThemeTokens get mapflowTokens {
return Theme.of(this).extension<MapflowThemeTokens>()!;
}
}
@@ -1,15 +1,14 @@
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import '../api/mapflow_api.dart'; import '../../../shared/location/current_location.dart';
import '../location/current_location.dart'; import '../data/mapflow_api.dart';
import '../models/place_models.dart'; import '../domain/place_models.dart';
final placeControllerProvider =
AsyncNotifierProvider<PlaceController, PlaceState>(PlaceController.new);
const _unset = Object(); const _unset = Object();
enum PlaceLoadStatus { loading, ready, failure }
class PlaceState { class PlaceState {
const PlaceState({ const PlaceState({
required this.selectedTrait, required this.selectedTrait,
@@ -65,9 +64,9 @@ class PlaceState {
Object? selectedTrait = _unset, Object? selectedTrait = _unset,
List<PlaceRecommendation>? places, List<PlaceRecommendation>? places,
Object? selectedPlaceId = _unset, Object? selectedPlaceId = _unset,
AppUser? currentUser, Object? currentUser = _unset,
bool? hasTelegramAuth, bool? hasTelegramAuth,
LatLng? userCoordinate, Object? userCoordinate = _unset,
VoiceReviewDraft? reviewDraft, VoiceReviewDraft? reviewDraft,
}) { }) {
return PlaceState( return PlaceState(
@@ -78,58 +77,82 @@ class PlaceState {
selectedPlaceId: identical(selectedPlaceId, _unset) selectedPlaceId: identical(selectedPlaceId, _unset)
? this.selectedPlaceId ? this.selectedPlaceId
: selectedPlaceId as String?, : selectedPlaceId as String?,
currentUser: currentUser ?? this.currentUser, currentUser: identical(currentUser, _unset)
? this.currentUser
: currentUser as AppUser?,
hasTelegramAuth: hasTelegramAuth ?? this.hasTelegramAuth, hasTelegramAuth: hasTelegramAuth ?? this.hasTelegramAuth,
userCoordinate: userCoordinate ?? this.userCoordinate, userCoordinate: identical(userCoordinate, _unset)
? this.userCoordinate
: userCoordinate as LatLng?,
reviewDraft: reviewDraft ?? this.reviewDraft, reviewDraft: reviewDraft ?? this.reviewDraft,
); );
} }
} }
class PlaceController extends AsyncNotifier<PlaceState> { class PlaceViewState {
final _api = MapflowApi(); const PlaceViewState({
final _location = CurrentLocation(); required this.status,
required this.placeState,
required this.errorMessage,
});
const PlaceViewState.loading()
: status = PlaceLoadStatus.loading,
placeState = null,
errorMessage = null;
const PlaceViewState.ready(PlaceState state)
: status = PlaceLoadStatus.ready,
placeState = state,
errorMessage = null;
const PlaceViewState.failure(String message)
: status = PlaceLoadStatus.failure,
placeState = null,
errorMessage = message;
final PlaceLoadStatus status;
final PlaceState? placeState;
final String? errorMessage;
}
class PlaceCubit extends Cubit<PlaceViewState> {
PlaceCubit({required MapflowApi api, required CurrentLocation location})
: _api = api,
_location = location,
super(const PlaceViewState.loading());
final MapflowApi _api;
final CurrentLocation _location;
Future<void> load() async {
emit(const PlaceViewState.loading());
@override
Future<PlaceState> build() async {
if (!_api.hasTelegramAuth) { if (!_api.hasTelegramAuth) {
return const PlaceState( emit(PlaceViewState.ready(_emptyState(hasTelegramAuth: false)));
selectedTrait: null, return;
places: [],
selectedPlaceId: null,
currentUser: null,
hasTelegramAuth: false,
userCoordinate: null,
reviewDraft: VoiceReviewDraft(
placeName: '',
duration: Duration.zero,
extractedTraits: {},
evidence: [],
),
);
} }
final currentUser = await _api.authenticateTelegram(); final currentUser = await _api.authenticateTelegram();
final userCoordinate = await _location.resolve(); final userCoordinate = await _location.resolve();
final places = await _api.fetchPlaces(); final places = await _api.fetchPlaces();
return PlaceState( emit(
selectedTrait: null, PlaceViewState.ready(
places: places, PlaceState(
selectedPlaceId: places.isEmpty ? null : places.first.id, selectedTrait: null,
currentUser: currentUser, places: places,
hasTelegramAuth: _api.hasTelegramAuth, selectedPlaceId: places.isEmpty ? null : places.first.id,
userCoordinate: userCoordinate, currentUser: currentUser,
reviewDraft: const VoiceReviewDraft( hasTelegramAuth: _api.hasTelegramAuth,
placeName: '', userCoordinate: userCoordinate,
duration: Duration.zero, reviewDraft: _emptyDraft,
extractedTraits: {}, ),
evidence: [],
), ),
); );
} }
void selectTrait(PlaceTrait trait) { void selectTrait(PlaceTrait trait) {
final value = state.requireValue; final value = _requireReady();
PlaceRecommendation? next; PlaceRecommendation? next;
for (final place in value.places) { for (final place in value.places) {
if (place.traits.contains(trait)) { if (place.traits.contains(trait)) {
@@ -137,38 +160,46 @@ class PlaceController extends AsyncNotifier<PlaceState> {
break; break;
} }
} }
state = AsyncData( emit(
value.copyWith(selectedTrait: trait, selectedPlaceId: next?.id), PlaceViewState.ready(
value.copyWith(selectedTrait: trait, selectedPlaceId: next?.id),
),
); );
} }
void clearTrait() { void clearTrait() {
final value = state.requireValue; final value = _requireReady();
final selectedPlaceId = value.places.isEmpty ? null : value.places.first.id; final selectedPlaceId = value.places.isEmpty ? null : value.places.first.id;
state = AsyncData( emit(
value.copyWith(selectedTrait: null, selectedPlaceId: selectedPlaceId), PlaceViewState.ready(
value.copyWith(selectedTrait: null, selectedPlaceId: selectedPlaceId),
),
); );
} }
void selectPlace(String placeId) { void selectPlace(String placeId) {
final value = state.requireValue; final value = _requireReady();
state = AsyncData(value.copyWith(selectedPlaceId: placeId)); emit(PlaceViewState.ready(value.copyWith(selectedPlaceId: placeId)));
} }
void setReviewPlace(String placeName) { void setReviewPlace(String placeName) {
final value = state.requireValue; final value = _requireReady();
state = AsyncData( emit(
value.copyWith( PlaceViewState.ready(
reviewDraft: value.reviewDraft.copyWith(placeName: placeName), value.copyWith(
reviewDraft: value.reviewDraft.copyWith(placeName: placeName),
),
), ),
); );
} }
void setReviewDuration(Duration duration) { void setReviewDuration(Duration duration) {
final value = state.requireValue; final value = _requireReady();
state = AsyncData( emit(
value.copyWith( PlaceViewState.ready(
reviewDraft: value.reviewDraft.copyWith(duration: duration), value.copyWith(
reviewDraft: value.reviewDraft.copyWith(duration: duration),
),
), ),
); );
} }
@@ -179,13 +210,12 @@ class PlaceController extends AsyncNotifier<PlaceState> {
required String audioContentBase64, required String audioContentBase64,
required String audioMimeType, required String audioMimeType,
}) async { }) async {
final value = state.requireValue; final value = _requireReady();
if (!value.hasTelegramAuth) { if (!value.hasTelegramAuth) {
throw StateError('Открой через Telegram, чтобы оставить голос.'); throw StateError('Открой через Telegram, чтобы оставить голос.');
} }
final draft = value.reviewDraft; final draft = value.reviewDraft;
await _api.createVoiceExperience( await _api.createVoiceExperience(
googlePlaceId: place.googlePlaceId, googlePlaceId: place.googlePlaceId,
googleName: place.name, googleName: place.name,
@@ -198,17 +228,41 @@ class PlaceController extends AsyncNotifier<PlaceState> {
final places = await _api.fetchPlaces(); final places = await _api.fetchPlaces();
final selectedPlace = places.isEmpty ? null : places.first.id; final selectedPlace = places.isEmpty ? null : places.first.id;
state = AsyncData( emit(
value.copyWith( PlaceViewState.ready(
places: places, value.copyWith(
selectedPlaceId: selectedPlace, places: places,
reviewDraft: const VoiceReviewDraft( selectedPlaceId: selectedPlace,
placeName: '', reviewDraft: _emptyDraft,
duration: Duration.zero,
extractedTraits: {},
evidence: [],
), ),
), ),
); );
} }
PlaceState _requireReady() {
final value = state.placeState;
if (value == null) {
throw StateError('Place state is not ready.');
}
return value;
}
static PlaceState _emptyState({required bool hasTelegramAuth}) {
return PlaceState(
selectedTrait: null,
places: const [],
selectedPlaceId: null,
currentUser: null,
hasTelegramAuth: hasTelegramAuth,
userCoordinate: null,
reviewDraft: _emptyDraft,
);
}
} }
const _emptyDraft = VoiceReviewDraft(
placeName: '',
duration: Duration.zero,
extractedTraits: {},
evidence: [],
);
@@ -3,8 +3,8 @@ import 'dart:convert';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import '../auth/telegram_session.dart' as telegram_auth; import '../../../shared/auth/telegram_session.dart' as telegram_auth;
import '../models/place_models.dart'; import '../domain/place_models.dart';
class MapflowApi { class MapflowApi {
MapflowApi({ MapflowApi({
@@ -0,0 +1,329 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import '../data/mapflow_api.dart';
import '../domain/place_models.dart';
class AdminVoiceExperiencesScreen extends StatefulWidget {
const AdminVoiceExperiencesScreen({super.key});
@override
State<AdminVoiceExperiencesScreen> createState() =>
_AdminVoiceExperiencesScreenState();
}
class _AdminVoiceExperiencesScreenState
extends State<AdminVoiceExperiencesScreen> {
late final Future<List<VoiceExperienceDebug>> _future = MapflowApi()
.fetchVoiceExperiences();
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFFFFBF5),
appBar: AppBar(title: const Text('Отзывы')),
body: FutureBuilder<List<VoiceExperienceDebug>>(
future: _future,
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text(snapshot.error.toString()));
}
final reviews = snapshot.data ?? const <VoiceExperienceDebug>[];
return ListView.separated(
padding: const EdgeInsets.all(12),
itemCount: reviews.length,
separatorBuilder: (_, _) => const SizedBox(height: 8),
itemBuilder: (context, index) {
return _AdminVoiceExperienceRow(review: reviews[index]);
},
);
},
),
);
}
}
class _AdminVoiceExperienceRow extends StatelessWidget {
const _AdminVoiceExperienceRow({required this.review});
final VoiceExperienceDebug review;
@override
Widget build(BuildContext context) {
final selectedTags = _selectedAdminTags(review.analysis);
return Material(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
review.placeName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w900),
),
),
Text(
review.status,
style: const TextStyle(fontWeight: FontWeight.w700),
),
],
),
const SizedBox(height: 6),
Text(
'${review.userName} · ${review.durationSeconds}s',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Color(0xFF6B6258)),
),
if (review.transcript?.trim().isNotEmpty == true) ...[
const SizedBox(height: 8),
Text(
review.transcript!,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
],
const SizedBox(height: 10),
_AdminOntologySnowflake(selectedTags: selectedTags),
],
),
),
);
}
}
class _AdminOntologySnowflake extends StatelessWidget {
const _AdminOntologySnowflake({required this.selectedTags});
final Set<String> selectedTags;
static const _axes = [
_AdminOntologyAxis(
id: 'energy',
label: 'энергия',
angle: -math.pi / 2,
leaves: [
_AdminOntologyLeaf('calm', 'спокойное', -0.22),
_AdminOntologyLeaf('dynamic', 'живое', 0.22),
],
),
_AdminOntologyAxis(
id: 'privacy',
label: 'приватность',
angle: -math.pi / 2 + math.pi * 2 / 5,
leaves: [
_AdminOntologyLeaf('intimate', 'камерное', -0.2),
_AdminOntologyLeaf('open', 'открытое', 0.2),
],
),
_AdminOntologyAxis(
id: 'function',
label: 'сценарий',
angle: -math.pi / 2 + math.pi * 4 / 5,
leaves: [
_AdminOntologyLeaf('reset', 'выдохнуть', -0.25),
_AdminOntologyLeaf('impress', 'впечатлить', 0),
_AdminOntologyLeaf('transit', 'транзитное', 0.25),
],
),
_AdminOntologyAxis(
id: 'aesthetic',
label: 'образ',
angle: -math.pi / 2 + math.pi * 6 / 5,
leaves: [
_AdminOntologyLeaf('clean', 'чистое', -0.2),
_AdminOntologyLeaf('expressive', 'выразительное', 0.2),
],
),
_AdminOntologyAxis(
id: 'sociality',
label: 'социальность',
angle: -math.pi / 2 + math.pi * 8 / 5,
leaves: [
_AdminOntologyLeaf('solo', 'для себя', -0.2),
_AdminOntologyLeaf('group', 'для компании', 0.2),
],
),
];
@override
Widget build(BuildContext context) {
return SizedBox(
height: 300,
width: double.infinity,
child: CustomPaint(
painter: _AdminOntologySnowflakePainter(
axes: _axes,
selectedTags: selectedTags,
),
),
);
}
}
class _AdminOntologySnowflakePainter extends CustomPainter {
const _AdminOntologySnowflakePainter({
required this.axes,
required this.selectedTags,
});
final List<_AdminOntologyAxis> axes;
final Set<String> selectedTags;
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = math.min(size.width, size.height);
final axisRadius = radius * 0.25;
final leafRadius = radius * 0.43;
final baseLine = Paint()
..color = const Color(0xFFE6DDD2)
..strokeWidth = 1.3
..style = PaintingStyle.stroke;
final selectedLine = Paint()
..color = const Color(0xFFE11D48)
..strokeWidth = 2.2
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke;
final node = Paint()..color = const Color(0xFFDED3C7);
final selectedNode = Paint()..color = const Color(0xFFE11D48);
final centerNode = Paint()..color = const Color(0xFF241B18);
canvas.drawCircle(center, 5, centerNode);
_drawLabel(canvas, size, center + const Offset(0, 14), 'место', true);
for (final axis in axes) {
final axisOffset = Offset(math.cos(axis.angle), math.sin(axis.angle));
final axisPoint = center + axisOffset * axisRadius;
final hasSelectedLeaf = axis.leaves.any(
(leaf) => selectedTags.contains('${axis.id}:${leaf.id}'),
);
canvas.drawLine(
center,
axisPoint,
hasSelectedLeaf ? selectedLine : baseLine,
);
canvas.drawCircle(
axisPoint,
hasSelectedLeaf ? 5.5 : 4.5,
hasSelectedLeaf ? selectedNode : node,
);
_drawLabel(
canvas,
size,
axisPoint + axisOffset * 18,
axis.label,
hasSelectedLeaf,
fontSize: 11,
);
for (final leaf in axis.leaves) {
final leafAngle = axis.angle + leaf.angleOffset;
final leafOffset = Offset(math.cos(leafAngle), math.sin(leafAngle));
final leafPoint = center + leafOffset * leafRadius;
final tag = '${axis.id}:${leaf.id}';
final selected = selectedTags.contains(tag);
canvas.drawLine(
axisPoint,
leafPoint,
selected ? selectedLine : baseLine,
);
canvas.drawCircle(
leafPoint,
selected ? 8 : 5.5,
selected ? selectedNode : node,
);
_drawLabel(
canvas,
size,
leafPoint + leafOffset * 20,
leaf.label,
selected,
);
}
}
}
void _drawLabel(
Canvas canvas,
Size size,
Offset anchor,
String label,
bool selected, {
double fontSize = 12,
}) {
final painter = TextPainter(
text: TextSpan(
text: label,
style: TextStyle(
color: selected ? const Color(0xFFE11D48) : const Color(0xFF746A60),
fontSize: fontSize,
fontWeight: selected ? FontWeight.w900 : FontWeight.w700,
height: 1,
),
),
textDirection: TextDirection.ltr,
maxLines: 1,
)..layout(maxWidth: 86);
final dx = (anchor.dx - painter.width / 2).clamp(
0.0,
size.width - painter.width,
);
final dy = (anchor.dy - painter.height / 2).clamp(
0.0,
size.height - painter.height,
);
painter.paint(canvas, Offset(dx, dy));
}
@override
bool shouldRepaint(covariant _AdminOntologySnowflakePainter oldDelegate) {
return oldDelegate.selectedTags != selectedTags;
}
}
class _AdminOntologyAxis {
const _AdminOntologyAxis({
required this.id,
required this.label,
required this.angle,
required this.leaves,
});
final String id;
final String label;
final double angle;
final List<_AdminOntologyLeaf> leaves;
}
class _AdminOntologyLeaf {
const _AdminOntologyLeaf(this.id, this.label, this.angleOffset);
final String id;
final String label;
final double angleOffset;
}
Set<String> _selectedAdminTags(Map<String, dynamic>? analysis) {
final tags = analysis?['tags'];
if (tags is! List) {
return const {};
}
return tags.whereType<String>().toSet();
}
@@ -3,18 +3,20 @@ import 'dart:convert';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:latlong2/latlong.dart' hide Path; import 'package:latlong2/latlong.dart' hide Path;
import 'package:waveform_flutter/waveform_flutter.dart' show Amplitude; import 'package:waveform_flutter/waveform_flutter.dart' show Amplitude;
import 'package:waveform_recorder/waveform_recorder.dart'; import 'package:waveform_recorder/waveform_recorder.dart';
import '../api/mapflow_api.dart'; import '../../../app/router/app_router.dart';
import '../auth/telegram_login_button.dart'; import '../../../shared/auth/telegram_login_button.dart';
import '../auth/telegram_session.dart' as telegram_session; import '../../../shared/auth/telegram_session.dart' as telegram_session;
import '../models/place_models.dart'; import '../application/place_cubit.dart';
import '../state/place_controller.dart'; import '../data/mapflow_api.dart';
import '../domain/place_models.dart';
const _mapboxAccessToken = String.fromEnvironment('MAPBOX_ACCESS_TOKEN'); const _mapboxAccessToken = String.fromEnvironment('MAPBOX_ACCESS_TOKEN');
const _mapboxStyle = String.fromEnvironment( const _mapboxStyle = String.fromEnvironment(
@@ -22,26 +24,31 @@ const _mapboxStyle = String.fromEnvironment(
defaultValue: 'mapbox/streets-v12', defaultValue: 'mapbox/streets-v12',
); );
class MapflowShell extends ConsumerWidget { class MapflowShell extends StatelessWidget {
const MapflowShell({super.key}); const MapflowShell({super.key});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context) {
final asyncState = ref.watch(placeControllerProvider); return BlocBuilder<PlaceCubit, PlaceViewState>(
builder: (context, viewState) {
return asyncState.when( return switch (viewState.status) {
data: (state) => state.hasTelegramAuth PlaceLoadStatus.loading => const _MapLoading(),
? _MapContent(state: state) PlaceLoadStatus.failure => _MapError(
: _TelegramLoginScreen( message: viewState.errorMessage ?? '',
onAuthenticated: () => ref.invalidate(placeControllerProvider), ),
), PlaceLoadStatus.ready =>
loading: () => const _MapLoading(), viewState.placeState!.hasTelegramAuth
error: (error, _) => _MapError(message: error.toString()), ? _MapContent(state: viewState.placeState!)
: _TelegramLoginScreen(
onAuthenticated: () => context.read<PlaceCubit>().load(),
),
};
},
); );
} }
} }
class _MapContent extends ConsumerWidget { class _MapContent extends StatelessWidget {
const _MapContent({required this.state}); const _MapContent({required this.state});
static const _fallbackCenter = LatLng(10.7718, 106.6982); static const _fallbackCenter = LatLng(10.7718, 106.6982);
@@ -49,10 +56,11 @@ class _MapContent extends ConsumerWidget {
final PlaceState state; final PlaceState state;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context) {
final selected = state.selectedPlace; final selected = state.selectedPlace;
final userCoordinate = state.userCoordinate; final userCoordinate = state.userCoordinate;
final mapCenter = userCoordinate ?? selected?.coordinate ?? _fallbackCenter; final mapCenter = userCoordinate ?? selected?.coordinate ?? _fallbackCenter;
final placeCubit = context.read<PlaceCubit>();
final traitCounts = _countPlaceTraits(state.places); final traitCounts = _countPlaceTraits(state.places);
final availableTraits = [ final availableTraits = [
for (final trait in PlaceTrait.values) for (final trait in PlaceTrait.values)
@@ -80,9 +88,7 @@ class _MapContent extends ConsumerWidget {
point: place.coordinate, point: place.coordinate,
child: _PlaceMarker( child: _PlaceMarker(
selected: selected?.id == place.id, selected: selected?.id == place.id,
onTap: () => ref onTap: () => placeCubit.selectPlace(place.id),
.read(placeControllerProvider.notifier)
.selectPlace(place.id),
), ),
), ),
if (userCoordinate != null) if (userCoordinate != null)
@@ -104,7 +110,7 @@ class _MapContent extends ConsumerWidget {
user: state.currentUser, user: state.currentUser,
onLogout: () { onLogout: () {
telegram_session.clearMapflowSession(); telegram_session.clearMapflowSession();
ref.invalidate(placeControllerProvider); placeCubit.load();
telegram_session.reloadApp(); telegram_session.reloadApp();
}, },
), ),
@@ -115,11 +121,7 @@ class _MapContent extends ConsumerWidget {
child: Align( child: Align(
alignment: Alignment.topRight, alignment: Alignment.topRight,
child: _AdminReviewsButton( child: _AdminReviewsButton(
onPressed: () => Navigator.of(context).push( onPressed: () => context.push('/admin/reviews'),
MaterialPageRoute<void>(
builder: (_) => const AdminVoiceExperiencesScreen(),
),
),
), ),
), ),
), ),
@@ -138,9 +140,7 @@ class _MapContent extends ConsumerWidget {
), ),
_PlaceCarousel( _PlaceCarousel(
places: state.recommendations, places: state.recommendations,
onSelect: (place) => ref onSelect: (place) => placeCubit.selectPlace(place.id),
.read(placeControllerProvider.notifier)
.selectPlace(place.id),
), ),
], ],
), ),
@@ -167,13 +167,11 @@ class _MapContent extends ConsumerWidget {
} }
void _openAddFlow(BuildContext context, LatLng? coordinate) { void _openAddFlow(BuildContext context, LatLng? coordinate) {
Navigator.of(context).push( context.push(
MaterialPageRoute<void>( '/experience/new',
fullscreenDialog: true, extra: AddExperienceArgs(
builder: (_) => AddExperienceFlow( coordinate: coordinate,
coordinate: coordinate, hasTelegramAuth: state.hasTelegramAuth,
hasTelegramAuth: state.hasTelegramAuth,
),
), ),
); );
} }
@@ -535,7 +533,7 @@ class _MapAttribution extends StatelessWidget {
} }
} }
class _TraitBar extends ConsumerWidget { class _TraitBar extends StatelessWidget {
const _TraitBar({ const _TraitBar({
required this.selectedTrait, required this.selectedTrait,
required this.traits, required this.traits,
@@ -547,8 +545,8 @@ class _TraitBar extends ConsumerWidget {
final Map<PlaceTrait, int> traitCounts; final Map<PlaceTrait, int> traitCounts;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context) {
final controller = ref.read(placeControllerProvider.notifier); final controller = context.read<PlaceCubit>();
return SizedBox( return SizedBox(
height: 54, height: 54,
@@ -715,7 +713,7 @@ class _PlacePhotoCard extends StatelessWidget {
} }
} }
class AddExperienceFlow extends ConsumerStatefulWidget { class AddExperienceFlow extends StatefulWidget {
const AddExperienceFlow({ const AddExperienceFlow({
super.key, super.key,
required this.coordinate, required this.coordinate,
@@ -726,10 +724,10 @@ class AddExperienceFlow extends ConsumerStatefulWidget {
final bool hasTelegramAuth; final bool hasTelegramAuth;
@override @override
ConsumerState<AddExperienceFlow> createState() => _AddExperienceFlowState(); State<AddExperienceFlow> createState() => _AddExperienceFlowState();
} }
class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> { class _AddExperienceFlowState extends State<AddExperienceFlow> {
static const _minimumInformationUnits = 16.0; static const _minimumInformationUnits = 16.0;
static const _nearbyPlaceRadiusMeters = 50; static const _nearbyPlaceRadiusMeters = 50;
@@ -831,9 +829,7 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
_informationUnits + informationDelta, _informationUnits + informationDelta,
); );
}); });
ref context.read<PlaceCubit>().setReviewDuration(_waveController.timeElapsed);
.read(placeControllerProvider.notifier)
.setReviewDuration(_waveController.timeElapsed);
} }
double _normalizeDbLevel(double currentDb) { double _normalizeDbLevel(double currentDb) {
@@ -869,7 +865,7 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final controller = ref.read(placeControllerProvider.notifier); final controller = context.read<PlaceCubit>();
final informationProgress = (_informationUnits / _minimumInformationUnits) final informationProgress = (_informationUnits / _minimumInformationUnits)
.clamp(0.0, 1.0); .clamp(0.0, 1.0);
final content = switch (_step) { final content = switch (_step) {
@@ -916,7 +912,7 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
if (!context.mounted) { if (!context.mounted) {
return; return;
} }
Navigator.of(context).pop(); context.pop();
}, },
), ),
}; };
@@ -937,7 +933,7 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
step: _step, step: _step,
total: 3, total: 3,
dark: true, dark: true,
onClose: () => Navigator.of(context).pop(), onClose: () => context.pop(),
), ),
const SizedBox(height: 18), const SizedBox(height: 18),
Expanded( Expanded(
@@ -954,329 +950,6 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
} }
} }
class AdminVoiceExperiencesScreen extends StatefulWidget {
const AdminVoiceExperiencesScreen({super.key});
@override
State<AdminVoiceExperiencesScreen> createState() =>
_AdminVoiceExperiencesScreenState();
}
class _AdminVoiceExperiencesScreenState
extends State<AdminVoiceExperiencesScreen> {
late final Future<List<VoiceExperienceDebug>> _future = MapflowApi()
.fetchVoiceExperiences();
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFFFFBF5),
appBar: AppBar(title: const Text('Отзывы')),
body: FutureBuilder<List<VoiceExperienceDebug>>(
future: _future,
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text(snapshot.error.toString()));
}
final reviews = snapshot.data ?? const <VoiceExperienceDebug>[];
return ListView.separated(
padding: const EdgeInsets.all(12),
itemCount: reviews.length,
separatorBuilder: (_, _) => const SizedBox(height: 8),
itemBuilder: (context, index) {
return _AdminVoiceExperienceRow(review: reviews[index]);
},
);
},
),
);
}
}
class _AdminVoiceExperienceRow extends StatelessWidget {
const _AdminVoiceExperienceRow({required this.review});
final VoiceExperienceDebug review;
@override
Widget build(BuildContext context) {
final selectedTags = _selectedAdminTags(review.analysis);
return Material(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
review.placeName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w900),
),
),
Text(
review.status,
style: const TextStyle(fontWeight: FontWeight.w700),
),
],
),
const SizedBox(height: 6),
Text(
'${review.userName} · ${review.durationSeconds}s',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Color(0xFF6B6258)),
),
if (review.transcript?.trim().isNotEmpty == true) ...[
const SizedBox(height: 8),
Text(
review.transcript!,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
],
const SizedBox(height: 10),
_AdminOntologySnowflake(selectedTags: selectedTags),
],
),
),
);
}
}
class _AdminOntologySnowflake extends StatelessWidget {
const _AdminOntologySnowflake({required this.selectedTags});
final Set<String> selectedTags;
static const _axes = [
_AdminOntologyAxis(
id: 'energy',
label: 'энергия',
angle: -math.pi / 2,
leaves: [
_AdminOntologyLeaf('calm', 'спокойное', -0.22),
_AdminOntologyLeaf('dynamic', 'живое', 0.22),
],
),
_AdminOntologyAxis(
id: 'privacy',
label: 'приватность',
angle: -math.pi / 2 + math.pi * 2 / 5,
leaves: [
_AdminOntologyLeaf('intimate', 'камерное', -0.2),
_AdminOntologyLeaf('open', 'открытое', 0.2),
],
),
_AdminOntologyAxis(
id: 'function',
label: 'сценарий',
angle: -math.pi / 2 + math.pi * 4 / 5,
leaves: [
_AdminOntologyLeaf('reset', 'выдохнуть', -0.25),
_AdminOntologyLeaf('impress', 'впечатлить', 0),
_AdminOntologyLeaf('transit', 'транзитное', 0.25),
],
),
_AdminOntologyAxis(
id: 'aesthetic',
label: 'образ',
angle: -math.pi / 2 + math.pi * 6 / 5,
leaves: [
_AdminOntologyLeaf('clean', 'чистое', -0.2),
_AdminOntologyLeaf('expressive', 'выразительное', 0.2),
],
),
_AdminOntologyAxis(
id: 'sociality',
label: 'социальность',
angle: -math.pi / 2 + math.pi * 8 / 5,
leaves: [
_AdminOntologyLeaf('solo', 'для себя', -0.2),
_AdminOntologyLeaf('group', 'для компании', 0.2),
],
),
];
@override
Widget build(BuildContext context) {
return SizedBox(
height: 300,
width: double.infinity,
child: CustomPaint(
painter: _AdminOntologySnowflakePainter(
axes: _axes,
selectedTags: selectedTags,
),
),
);
}
}
class _AdminOntologySnowflakePainter extends CustomPainter {
const _AdminOntologySnowflakePainter({
required this.axes,
required this.selectedTags,
});
final List<_AdminOntologyAxis> axes;
final Set<String> selectedTags;
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = math.min(size.width, size.height);
final axisRadius = radius * 0.25;
final leafRadius = radius * 0.43;
final baseLine = Paint()
..color = const Color(0xFFE6DDD2)
..strokeWidth = 1.3
..style = PaintingStyle.stroke;
final selectedLine = Paint()
..color = const Color(0xFFE11D48)
..strokeWidth = 2.2
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke;
final node = Paint()..color = const Color(0xFFDED3C7);
final selectedNode = Paint()..color = const Color(0xFFE11D48);
final centerNode = Paint()..color = const Color(0xFF241B18);
canvas.drawCircle(center, 5, centerNode);
_drawLabel(canvas, size, center + const Offset(0, 14), 'место', true);
for (final axis in axes) {
final axisOffset = Offset(math.cos(axis.angle), math.sin(axis.angle));
final axisPoint = center + axisOffset * axisRadius;
final hasSelectedLeaf = axis.leaves.any(
(leaf) => selectedTags.contains('${axis.id}:${leaf.id}'),
);
canvas.drawLine(
center,
axisPoint,
hasSelectedLeaf ? selectedLine : baseLine,
);
canvas.drawCircle(
axisPoint,
hasSelectedLeaf ? 5.5 : 4.5,
hasSelectedLeaf ? selectedNode : node,
);
_drawLabel(
canvas,
size,
axisPoint + axisOffset * 18,
axis.label,
hasSelectedLeaf,
fontSize: 11,
);
for (final leaf in axis.leaves) {
final leafAngle = axis.angle + leaf.angleOffset;
final leafOffset = Offset(math.cos(leafAngle), math.sin(leafAngle));
final leafPoint = center + leafOffset * leafRadius;
final tag = '${axis.id}:${leaf.id}';
final selected = selectedTags.contains(tag);
canvas.drawLine(
axisPoint,
leafPoint,
selected ? selectedLine : baseLine,
);
canvas.drawCircle(
leafPoint,
selected ? 8 : 5.5,
selected ? selectedNode : node,
);
_drawLabel(
canvas,
size,
leafPoint + leafOffset * 20,
leaf.label,
selected,
);
}
}
}
void _drawLabel(
Canvas canvas,
Size size,
Offset anchor,
String label,
bool selected, {
double fontSize = 12,
}) {
final painter = TextPainter(
text: TextSpan(
text: label,
style: TextStyle(
color: selected ? const Color(0xFFE11D48) : const Color(0xFF746A60),
fontSize: fontSize,
fontWeight: selected ? FontWeight.w900 : FontWeight.w700,
height: 1,
),
),
textDirection: TextDirection.ltr,
maxLines: 1,
)..layout(maxWidth: 86);
final dx = (anchor.dx - painter.width / 2).clamp(
0.0,
size.width - painter.width,
);
final dy = (anchor.dy - painter.height / 2).clamp(
0.0,
size.height - painter.height,
);
painter.paint(canvas, Offset(dx, dy));
}
@override
bool shouldRepaint(covariant _AdminOntologySnowflakePainter oldDelegate) {
return oldDelegate.selectedTags != selectedTags;
}
}
class _AdminOntologyAxis {
const _AdminOntologyAxis({
required this.id,
required this.label,
required this.angle,
required this.leaves,
});
final String id;
final String label;
final double angle;
final List<_AdminOntologyLeaf> leaves;
}
class _AdminOntologyLeaf {
const _AdminOntologyLeaf(this.id, this.label, this.angleOffset);
final String id;
final String label;
final double angleOffset;
}
Set<String> _selectedAdminTags(Map<String, dynamic>? analysis) {
final tags = analysis?['tags'];
if (tags is! List) {
return const {};
}
return tags.whereType<String>().toSet();
}
class _IntroStep extends StatelessWidget { class _IntroStep extends StatelessWidget {
const _IntroStep({required this.onNext}); const _IntroStep({required this.onNext});
+3 -94
View File
@@ -1,101 +1,10 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'auth/telegram_session.dart' as telegram_session; import 'app/app.dart';
import 'screens/mapflow_shell.dart'; import 'shared/auth/telegram_session.dart' as telegram_session;
void main() { void main() {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
telegram_session.configureTelegramWebApp(); telegram_session.configureTelegramWebApp();
runApp(const ProviderScope(child: MapflowApp())); runApp(const MapflowApp());
}
class MapflowApp extends StatelessWidget {
const MapflowApp({super.key});
ThemeData _buildTheme() {
final colorScheme = ColorScheme.fromSeed(
seedColor: const Color(0xFF0F766E),
primary: const Color(0xFF0F766E),
secondary: const Color(0xFFE11D48),
surface: const Color(0xFFFFFBF5),
);
return ThemeData(
useMaterial3: true,
colorScheme: colorScheme,
scaffoldBackgroundColor: const Color(0xFFF7F3EA),
fontFamily: 'SF Pro Display',
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFFF7F3EA),
foregroundColor: Color(0xFF17211D),
surfaceTintColor: Colors.transparent,
),
cardTheme: CardThemeData(
elevation: 0,
color: const Color(0xFFFFFBF5),
surfaceTintColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: const BorderSide(color: Color(0xFFE0D8CA)),
),
),
chipTheme: const ChipThemeData(
shape: StadiumBorder(side: BorderSide(color: Color(0xFFE0D8CA))),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: const Color(0xFFFFFFFF),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Color(0xFFD8D0C3)),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Color(0xFFD8D0C3)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: colorScheme.primary, width: 1.5),
),
),
);
}
@override
Widget build(BuildContext context) {
final shortcuts =
Map<ShortcutActivator, Intent>.of(WidgetsApp.defaultShortcuts)
..[const SingleActivator(LogicalKeyboardKey.tab)] =
const NextFocusIntent()
..[const SingleActivator(LogicalKeyboardKey.tab, shift: true)] =
const PreviousFocusIntent();
return MaterialApp(
title: 'MapFlow',
debugShowCheckedModeBanner: false,
scrollBehavior: const MaterialScrollBehavior().copyWith(
scrollbars: true,
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
PointerDeviceKind.trackpad,
PointerDeviceKind.stylus,
},
),
theme: _buildTheme(),
builder: (context, child) {
return Shortcuts(
shortcuts: shortcuts,
child: FocusTraversalGroup(
policy: ReadingOrderTraversalPolicy(),
child: child ?? const SizedBox.shrink(),
),
);
},
home: const MapflowShell(),
);
}
} }
+56 -208
View File
@@ -1,22 +1,6 @@
# Generated by pub # Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile # See https://dart.dev/tools/pub/glossary#lockfile
packages: packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: c209688d9f5a5f26b2fb47a188131a6fb9e876ae9e47af3737c0b4f58a93470d
url: "https://pub.dev"
source: hosted
version: "91.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: f51c8499b35f9b26820cfe914828a6a98a94efd5cc78b37bb7d03debae3a1d08
url: "https://pub.dev"
source: hosted
version: "8.4.1"
archive: archive:
dependency: transitive dependency: transitive
description: description:
@@ -41,6 +25,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.13.1" version: "2.13.1"
bloc:
dependency: transitive
description:
name: bloc
sha256: e03b235924e4f509c27b5d6b2f949200e0a91149a9818b4f65eeb56662b75413
url: "https://pub.dev"
source: hosted
version: "9.2.1"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@@ -57,14 +49,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.1" version: "1.4.1"
cli_config:
dependency: transitive
description:
name: cli_config
sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec
url: "https://pub.dev"
source: hosted
version: "0.2.0"
clock: clock:
dependency: transitive dependency: transitive
description: description:
@@ -89,22 +73,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.19.1" version: "1.19.1"
convert:
dependency: transitive
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev"
source: hosted
version: "3.1.2"
coverage:
dependency: transitive
description:
name: coverage
sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d"
url: "https://pub.dev"
source: hosted
version: "1.15.0"
cross_file: cross_file:
dependency: transitive dependency: transitive
description: description:
@@ -185,11 +153,35 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.1" version: "1.1.1"
flex_color_scheme:
dependency: "direct main"
description:
name: flex_color_scheme
sha256: ab854146f201d2d62cc251fd525ef023b84182c4a0bfe4ae4c18ffc505b412d3
url: "https://pub.dev"
source: hosted
version: "8.4.0"
flex_seed_scheme:
dependency: transitive
description:
name: flex_seed_scheme
sha256: a3183753bbcfc3af106224bff3ab3e1844b73f58062136b7499919f49f3667e7
url: "https://pub.dev"
source: hosted
version: "4.0.1"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_bloc:
dependency: "direct main"
description:
name: flutter_bloc
sha256: cf51747952201a455a1c840f8171d273be009b932c75093020f9af64f2123e38
url: "https://pub.dev"
source: hosted
version: "9.1.1"
flutter_lints: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
@@ -206,14 +198,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.3.0" version: "8.3.0"
flutter_riverpod:
dependency: "direct main"
description:
name: flutter_riverpod
sha256: "4e166be88e1dbbaa34a280bdb744aeae73b7ef25fdf8db7a3bb776760a3648e2"
url: "https://pub.dev"
source: hosted
version: "3.3.1"
flutter_svg: flutter_svg:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -232,14 +216,6 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.dev"
source: hosted
version: "4.0.0"
geoclue: geoclue:
dependency: transitive dependency: transitive
description: description:
@@ -312,6 +288,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.3" version: "2.1.3"
go_router:
dependency: "direct main"
description:
name: go_router
sha256: "5922b2861e2235a3504896f0d6fa07d84141b480cf52eecd2f42cd25585a9e8a"
url: "https://pub.dev"
source: hosted
version: "17.3.0"
gsettings: gsettings:
dependency: transitive dependency: transitive
description: description:
@@ -336,14 +320,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.6.0" version: "1.6.0"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
url: "https://pub.dev"
source: hosted
version: "3.2.2"
http_parser: http_parser:
dependency: transitive dependency: transitive
description: description:
@@ -360,14 +336,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.20.2" version: "0.20.2"
io:
dependency: transitive
description:
name: io
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
url: "https://pub.dev"
source: hosted
version: "1.0.5"
jni: jni:
dependency: transitive dependency: transitive
description: description:
@@ -452,10 +420,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.17.0" version: "1.18.0"
mgrs_dart: mgrs_dart:
dependency: transitive dependency: transitive
description: description:
@@ -464,14 +432,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.0" version: "3.0.0"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
native_toolchain_c: native_toolchain_c:
dependency: transitive dependency: transitive
description: description:
@@ -480,14 +440,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.17.6" version: "0.17.6"
node_preamble: nested:
dependency: transitive dependency: transitive
description: description:
name: node_preamble name: nested
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.2" version: "1.0.0"
objective_c: objective_c:
dependency: transitive dependency: transitive
description: description:
@@ -608,14 +568,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.8" version: "2.1.8"
pool:
dependency: transitive
description:
name: pool
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
url: "https://pub.dev"
source: hosted
version: "1.5.2"
posix: posix:
dependency: transitive dependency: transitive
description: description:
@@ -632,6 +584,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.0" version: "3.0.0"
provider:
dependency: transitive
description:
name: provider
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
url: "https://pub.dev"
source: hosted
version: "6.1.5+1"
pub_semver: pub_semver:
dependency: transitive dependency: transitive
description: description:
@@ -712,46 +672,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.7" version: "1.0.7"
riverpod:
dependency: transitive
description:
name: riverpod
sha256: "8c22216be8ad3ef2b44af3a329693558c98eca7b8bd4ef495c92db0bba279f83"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
shelf:
dependency: transitive
description:
name: shelf
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
url: "https://pub.dev"
source: hosted
version: "1.4.2"
shelf_packages_handler:
dependency: transitive
description:
name: shelf_packages_handler
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
shelf_static:
dependency: transitive
description:
name: shelf_static
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
url: "https://pub.dev"
source: hosted
version: "1.1.3"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
simple_sparse_list: simple_sparse_list:
dependency: transitive dependency: transitive
description: description:
@@ -765,22 +685,6 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
source_map_stack_trace:
dependency: transitive
description:
name: source_map_stack_trace
sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
url: "https://pub.dev"
source: hosted
version: "2.1.2"
source_maps:
dependency: transitive
description:
name: source_maps
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
url: "https://pub.dev"
source: hosted
version: "0.10.13"
source_span: source_span:
dependency: transitive dependency: transitive
description: description:
@@ -797,14 +701,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.12.1" version: "1.12.1"
state_notifier:
dependency: transitive
description:
name: state_notifier
sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb
url: "https://pub.dev"
source: hosted
version: "1.0.0"
stream_channel: stream_channel:
dependency: transitive dependency: transitive
description: description:
@@ -829,30 +725,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.2" version: "1.2.2"
test:
dependency: transitive
description:
name: test
sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7"
url: "https://pub.dev"
source: hosted
version: "1.30.0"
test_api: test_api:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.10" version: "0.7.11"
test_core:
dependency: transitive
description:
name: test_core
sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51"
url: "https://pub.dev"
source: hosted
version: "0.6.16"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@@ -917,14 +797,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "15.2.0" version: "15.2.0"
watcher:
dependency: transitive
description:
name: watcher
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
waveform_flutter: waveform_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -949,30 +821,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.1" version: "1.1.1"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
url: "https://pub.dev"
source: hosted
version: "3.0.3"
webkit_inspection_protocol:
dependency: transitive
description:
name: webkit_inspection_protocol
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
win32: win32:
dependency: transitive dependency: transitive
description: description:
+3 -1
View File
@@ -34,7 +34,6 @@ dependencies:
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8
flutter_riverpod: ^3.3.1
flutter_map: ^8.3.0 flutter_map: ^8.3.0
latlong2: ^0.9.1 latlong2: ^0.9.1
http: ^1.6.0 http: ^1.6.0
@@ -43,6 +42,9 @@ dependencies:
flutter_svg: ^2.3.0 flutter_svg: ^2.3.0
waveform_recorder: ^1.8.0 waveform_recorder: ^1.8.0
waveform_flutter: ^1.2.0 waveform_flutter: ^1.2.0
go_router: ^17.3.0
flutter_bloc: ^9.1.1
flex_color_scheme: ^8.4.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
+2 -3
View File
@@ -1,11 +1,10 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mapflow/main.dart'; import 'package:mapflow/app/app.dart';
void main() { void main() {
testWidgets('renders Telegram login before authorization', (tester) async { testWidgets('renders Telegram login before authorization', (tester) async {
await tester.pumpWidget(const ProviderScope(child: MapflowApp())); await tester.pumpWidget(const MapflowApp());
await tester.pump(); await tester.pump();
expect(find.text('MapFlow'), findsOneWidget); expect(find.text('MapFlow'), findsOneWidget);