This commit is contained in:
@@ -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(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>()!;
|
||||||
|
}
|
||||||
|
}
|
||||||
+108
-54
@@ -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(
|
||||||
|
PlaceViewState.ready(
|
||||||
|
PlaceState(
|
||||||
selectedTrait: null,
|
selectedTrait: null,
|
||||||
places: places,
|
places: places,
|
||||||
selectedPlaceId: places.isEmpty ? null : places.first.id,
|
selectedPlaceId: places.isEmpty ? null : places.first.id,
|
||||||
currentUser: currentUser,
|
currentUser: currentUser,
|
||||||
hasTelegramAuth: _api.hasTelegramAuth,
|
hasTelegramAuth: _api.hasTelegramAuth,
|
||||||
userCoordinate: userCoordinate,
|
userCoordinate: userCoordinate,
|
||||||
reviewDraft: const VoiceReviewDraft(
|
reviewDraft: _emptyDraft,
|
||||||
placeName: '',
|
),
|
||||||
duration: Duration.zero,
|
|
||||||
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,39 +160,47 @@ class PlaceController extends AsyncNotifier<PlaceState> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
state = AsyncData(
|
emit(
|
||||||
|
PlaceViewState.ready(
|
||||||
value.copyWith(selectedTrait: trait, selectedPlaceId: next?.id),
|
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(
|
||||||
|
PlaceViewState.ready(
|
||||||
value.copyWith(selectedTrait: null, selectedPlaceId: selectedPlaceId),
|
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(
|
||||||
|
PlaceViewState.ready(
|
||||||
value.copyWith(
|
value.copyWith(
|
||||||
reviewDraft: value.reviewDraft.copyWith(placeName: placeName),
|
reviewDraft: value.reviewDraft.copyWith(placeName: placeName),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setReviewDuration(Duration duration) {
|
void setReviewDuration(Duration duration) {
|
||||||
final value = state.requireValue;
|
final value = _requireReady();
|
||||||
state = AsyncData(
|
emit(
|
||||||
|
PlaceViewState.ready(
|
||||||
value.copyWith(
|
value.copyWith(
|
||||||
reviewDraft: value.reviewDraft.copyWith(duration: duration),
|
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(
|
||||||
|
PlaceViewState.ready(
|
||||||
value.copyWith(
|
value.copyWith(
|
||||||
places: places,
|
places: places,
|
||||||
selectedPlaceId: selectedPlace,
|
selectedPlaceId: selectedPlace,
|
||||||
reviewDraft: const VoiceReviewDraft(
|
reviewDraft: _emptyDraft,
|
||||||
placeName: '',
|
|
||||||
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();
|
||||||
|
}
|
||||||
+44
-371
@@ -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),
|
|
||||||
),
|
),
|
||||||
loading: () => const _MapLoading(),
|
PlaceLoadStatus.ready =>
|
||||||
error: (error, _) => _MapError(message: error.toString()),
|
viewState.placeState!.hasTelegramAuth
|
||||||
|
? _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,14 +167,12 @@ 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
@@ -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
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user