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>()!;
|
||||
}
|
||||
}
|
||||
+123
-69
@@ -1,15 +1,14 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
import '../api/mapflow_api.dart';
|
||||
import '../location/current_location.dart';
|
||||
import '../models/place_models.dart';
|
||||
|
||||
final placeControllerProvider =
|
||||
AsyncNotifierProvider<PlaceController, PlaceState>(PlaceController.new);
|
||||
import '../../../shared/location/current_location.dart';
|
||||
import '../data/mapflow_api.dart';
|
||||
import '../domain/place_models.dart';
|
||||
|
||||
const _unset = Object();
|
||||
|
||||
enum PlaceLoadStatus { loading, ready, failure }
|
||||
|
||||
class PlaceState {
|
||||
const PlaceState({
|
||||
required this.selectedTrait,
|
||||
@@ -65,9 +64,9 @@ class PlaceState {
|
||||
Object? selectedTrait = _unset,
|
||||
List<PlaceRecommendation>? places,
|
||||
Object? selectedPlaceId = _unset,
|
||||
AppUser? currentUser,
|
||||
Object? currentUser = _unset,
|
||||
bool? hasTelegramAuth,
|
||||
LatLng? userCoordinate,
|
||||
Object? userCoordinate = _unset,
|
||||
VoiceReviewDraft? reviewDraft,
|
||||
}) {
|
||||
return PlaceState(
|
||||
@@ -78,58 +77,82 @@ class PlaceState {
|
||||
selectedPlaceId: identical(selectedPlaceId, _unset)
|
||||
? this.selectedPlaceId
|
||||
: selectedPlaceId as String?,
|
||||
currentUser: currentUser ?? this.currentUser,
|
||||
currentUser: identical(currentUser, _unset)
|
||||
? this.currentUser
|
||||
: currentUser as AppUser?,
|
||||
hasTelegramAuth: hasTelegramAuth ?? this.hasTelegramAuth,
|
||||
userCoordinate: userCoordinate ?? this.userCoordinate,
|
||||
userCoordinate: identical(userCoordinate, _unset)
|
||||
? this.userCoordinate
|
||||
: userCoordinate as LatLng?,
|
||||
reviewDraft: reviewDraft ?? this.reviewDraft,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PlaceController extends AsyncNotifier<PlaceState> {
|
||||
final _api = MapflowApi();
|
||||
final _location = CurrentLocation();
|
||||
class PlaceViewState {
|
||||
const PlaceViewState({
|
||||
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) {
|
||||
return const PlaceState(
|
||||
selectedTrait: null,
|
||||
places: [],
|
||||
selectedPlaceId: null,
|
||||
currentUser: null,
|
||||
hasTelegramAuth: false,
|
||||
userCoordinate: null,
|
||||
reviewDraft: VoiceReviewDraft(
|
||||
placeName: '',
|
||||
duration: Duration.zero,
|
||||
extractedTraits: {},
|
||||
evidence: [],
|
||||
),
|
||||
);
|
||||
emit(PlaceViewState.ready(_emptyState(hasTelegramAuth: false)));
|
||||
return;
|
||||
}
|
||||
|
||||
final currentUser = await _api.authenticateTelegram();
|
||||
final userCoordinate = await _location.resolve();
|
||||
final places = await _api.fetchPlaces();
|
||||
return PlaceState(
|
||||
selectedTrait: null,
|
||||
places: places,
|
||||
selectedPlaceId: places.isEmpty ? null : places.first.id,
|
||||
currentUser: currentUser,
|
||||
hasTelegramAuth: _api.hasTelegramAuth,
|
||||
userCoordinate: userCoordinate,
|
||||
reviewDraft: const VoiceReviewDraft(
|
||||
placeName: '',
|
||||
duration: Duration.zero,
|
||||
extractedTraits: {},
|
||||
evidence: [],
|
||||
emit(
|
||||
PlaceViewState.ready(
|
||||
PlaceState(
|
||||
selectedTrait: null,
|
||||
places: places,
|
||||
selectedPlaceId: places.isEmpty ? null : places.first.id,
|
||||
currentUser: currentUser,
|
||||
hasTelegramAuth: _api.hasTelegramAuth,
|
||||
userCoordinate: userCoordinate,
|
||||
reviewDraft: _emptyDraft,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void selectTrait(PlaceTrait trait) {
|
||||
final value = state.requireValue;
|
||||
final value = _requireReady();
|
||||
PlaceRecommendation? next;
|
||||
for (final place in value.places) {
|
||||
if (place.traits.contains(trait)) {
|
||||
@@ -137,38 +160,46 @@ class PlaceController extends AsyncNotifier<PlaceState> {
|
||||
break;
|
||||
}
|
||||
}
|
||||
state = AsyncData(
|
||||
value.copyWith(selectedTrait: trait, selectedPlaceId: next?.id),
|
||||
emit(
|
||||
PlaceViewState.ready(
|
||||
value.copyWith(selectedTrait: trait, selectedPlaceId: next?.id),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void clearTrait() {
|
||||
final value = state.requireValue;
|
||||
final value = _requireReady();
|
||||
final selectedPlaceId = value.places.isEmpty ? null : value.places.first.id;
|
||||
state = AsyncData(
|
||||
value.copyWith(selectedTrait: null, selectedPlaceId: selectedPlaceId),
|
||||
emit(
|
||||
PlaceViewState.ready(
|
||||
value.copyWith(selectedTrait: null, selectedPlaceId: selectedPlaceId),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void selectPlace(String placeId) {
|
||||
final value = state.requireValue;
|
||||
state = AsyncData(value.copyWith(selectedPlaceId: placeId));
|
||||
final value = _requireReady();
|
||||
emit(PlaceViewState.ready(value.copyWith(selectedPlaceId: placeId)));
|
||||
}
|
||||
|
||||
void setReviewPlace(String placeName) {
|
||||
final value = state.requireValue;
|
||||
state = AsyncData(
|
||||
value.copyWith(
|
||||
reviewDraft: value.reviewDraft.copyWith(placeName: placeName),
|
||||
final value = _requireReady();
|
||||
emit(
|
||||
PlaceViewState.ready(
|
||||
value.copyWith(
|
||||
reviewDraft: value.reviewDraft.copyWith(placeName: placeName),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void setReviewDuration(Duration duration) {
|
||||
final value = state.requireValue;
|
||||
state = AsyncData(
|
||||
value.copyWith(
|
||||
reviewDraft: value.reviewDraft.copyWith(duration: duration),
|
||||
final value = _requireReady();
|
||||
emit(
|
||||
PlaceViewState.ready(
|
||||
value.copyWith(
|
||||
reviewDraft: value.reviewDraft.copyWith(duration: duration),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -179,13 +210,12 @@ class PlaceController extends AsyncNotifier<PlaceState> {
|
||||
required String audioContentBase64,
|
||||
required String audioMimeType,
|
||||
}) async {
|
||||
final value = state.requireValue;
|
||||
final value = _requireReady();
|
||||
if (!value.hasTelegramAuth) {
|
||||
throw StateError('Открой через Telegram, чтобы оставить голос.');
|
||||
}
|
||||
|
||||
final draft = value.reviewDraft;
|
||||
|
||||
await _api.createVoiceExperience(
|
||||
googlePlaceId: place.googlePlaceId,
|
||||
googleName: place.name,
|
||||
@@ -198,17 +228,41 @@ class PlaceController extends AsyncNotifier<PlaceState> {
|
||||
|
||||
final places = await _api.fetchPlaces();
|
||||
final selectedPlace = places.isEmpty ? null : places.first.id;
|
||||
state = AsyncData(
|
||||
value.copyWith(
|
||||
places: places,
|
||||
selectedPlaceId: selectedPlace,
|
||||
reviewDraft: const VoiceReviewDraft(
|
||||
placeName: '',
|
||||
duration: Duration.zero,
|
||||
extractedTraits: {},
|
||||
evidence: [],
|
||||
emit(
|
||||
PlaceViewState.ready(
|
||||
value.copyWith(
|
||||
places: places,
|
||||
selectedPlaceId: selectedPlace,
|
||||
reviewDraft: _emptyDraft,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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:latlong2/latlong.dart';
|
||||
|
||||
import '../auth/telegram_session.dart' as telegram_auth;
|
||||
import '../models/place_models.dart';
|
||||
import '../../../shared/auth/telegram_session.dart' as telegram_auth;
|
||||
import '../domain/place_models.dart';
|
||||
|
||||
class 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();
|
||||
}
|
||||
+47
-374
@@ -3,18 +3,20 @@ import 'dart:convert';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:latlong2/latlong.dart' hide Path;
|
||||
import 'package:waveform_flutter/waveform_flutter.dart' show Amplitude;
|
||||
import 'package:waveform_recorder/waveform_recorder.dart';
|
||||
|
||||
import '../api/mapflow_api.dart';
|
||||
import '../auth/telegram_login_button.dart';
|
||||
import '../auth/telegram_session.dart' as telegram_session;
|
||||
import '../models/place_models.dart';
|
||||
import '../state/place_controller.dart';
|
||||
import '../../../app/router/app_router.dart';
|
||||
import '../../../shared/auth/telegram_login_button.dart';
|
||||
import '../../../shared/auth/telegram_session.dart' as telegram_session;
|
||||
import '../application/place_cubit.dart';
|
||||
import '../data/mapflow_api.dart';
|
||||
import '../domain/place_models.dart';
|
||||
|
||||
const _mapboxAccessToken = String.fromEnvironment('MAPBOX_ACCESS_TOKEN');
|
||||
const _mapboxStyle = String.fromEnvironment(
|
||||
@@ -22,26 +24,31 @@ const _mapboxStyle = String.fromEnvironment(
|
||||
defaultValue: 'mapbox/streets-v12',
|
||||
);
|
||||
|
||||
class MapflowShell extends ConsumerWidget {
|
||||
class MapflowShell extends StatelessWidget {
|
||||
const MapflowShell({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final asyncState = ref.watch(placeControllerProvider);
|
||||
|
||||
return asyncState.when(
|
||||
data: (state) => state.hasTelegramAuth
|
||||
? _MapContent(state: state)
|
||||
: _TelegramLoginScreen(
|
||||
onAuthenticated: () => ref.invalidate(placeControllerProvider),
|
||||
),
|
||||
loading: () => const _MapLoading(),
|
||||
error: (error, _) => _MapError(message: error.toString()),
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<PlaceCubit, PlaceViewState>(
|
||||
builder: (context, viewState) {
|
||||
return switch (viewState.status) {
|
||||
PlaceLoadStatus.loading => const _MapLoading(),
|
||||
PlaceLoadStatus.failure => _MapError(
|
||||
message: viewState.errorMessage ?? '',
|
||||
),
|
||||
PlaceLoadStatus.ready =>
|
||||
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});
|
||||
|
||||
static const _fallbackCenter = LatLng(10.7718, 106.6982);
|
||||
@@ -49,10 +56,11 @@ class _MapContent extends ConsumerWidget {
|
||||
final PlaceState state;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Widget build(BuildContext context) {
|
||||
final selected = state.selectedPlace;
|
||||
final userCoordinate = state.userCoordinate;
|
||||
final mapCenter = userCoordinate ?? selected?.coordinate ?? _fallbackCenter;
|
||||
final placeCubit = context.read<PlaceCubit>();
|
||||
final traitCounts = _countPlaceTraits(state.places);
|
||||
final availableTraits = [
|
||||
for (final trait in PlaceTrait.values)
|
||||
@@ -80,9 +88,7 @@ class _MapContent extends ConsumerWidget {
|
||||
point: place.coordinate,
|
||||
child: _PlaceMarker(
|
||||
selected: selected?.id == place.id,
|
||||
onTap: () => ref
|
||||
.read(placeControllerProvider.notifier)
|
||||
.selectPlace(place.id),
|
||||
onTap: () => placeCubit.selectPlace(place.id),
|
||||
),
|
||||
),
|
||||
if (userCoordinate != null)
|
||||
@@ -104,7 +110,7 @@ class _MapContent extends ConsumerWidget {
|
||||
user: state.currentUser,
|
||||
onLogout: () {
|
||||
telegram_session.clearMapflowSession();
|
||||
ref.invalidate(placeControllerProvider);
|
||||
placeCubit.load();
|
||||
telegram_session.reloadApp();
|
||||
},
|
||||
),
|
||||
@@ -115,11 +121,7 @@ class _MapContent extends ConsumerWidget {
|
||||
child: Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: _AdminReviewsButton(
|
||||
onPressed: () => Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => const AdminVoiceExperiencesScreen(),
|
||||
),
|
||||
),
|
||||
onPressed: () => context.push('/admin/reviews'),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -138,9 +140,7 @@ class _MapContent extends ConsumerWidget {
|
||||
),
|
||||
_PlaceCarousel(
|
||||
places: state.recommendations,
|
||||
onSelect: (place) => ref
|
||||
.read(placeControllerProvider.notifier)
|
||||
.selectPlace(place.id),
|
||||
onSelect: (place) => placeCubit.selectPlace(place.id),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -167,13 +167,11 @@ class _MapContent extends ConsumerWidget {
|
||||
}
|
||||
|
||||
void _openAddFlow(BuildContext context, LatLng? coordinate) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
fullscreenDialog: true,
|
||||
builder: (_) => AddExperienceFlow(
|
||||
coordinate: coordinate,
|
||||
hasTelegramAuth: state.hasTelegramAuth,
|
||||
),
|
||||
context.push(
|
||||
'/experience/new',
|
||||
extra: AddExperienceArgs(
|
||||
coordinate: coordinate,
|
||||
hasTelegramAuth: state.hasTelegramAuth,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -535,7 +533,7 @@ class _MapAttribution extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _TraitBar extends ConsumerWidget {
|
||||
class _TraitBar extends StatelessWidget {
|
||||
const _TraitBar({
|
||||
required this.selectedTrait,
|
||||
required this.traits,
|
||||
@@ -547,8 +545,8 @@ class _TraitBar extends ConsumerWidget {
|
||||
final Map<PlaceTrait, int> traitCounts;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final controller = ref.read(placeControllerProvider.notifier);
|
||||
Widget build(BuildContext context) {
|
||||
final controller = context.read<PlaceCubit>();
|
||||
|
||||
return SizedBox(
|
||||
height: 54,
|
||||
@@ -715,7 +713,7 @@ class _PlacePhotoCard extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class AddExperienceFlow extends ConsumerStatefulWidget {
|
||||
class AddExperienceFlow extends StatefulWidget {
|
||||
const AddExperienceFlow({
|
||||
super.key,
|
||||
required this.coordinate,
|
||||
@@ -726,10 +724,10 @@ class AddExperienceFlow extends ConsumerStatefulWidget {
|
||||
final bool hasTelegramAuth;
|
||||
|
||||
@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 _nearbyPlaceRadiusMeters = 50;
|
||||
|
||||
@@ -831,9 +829,7 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
|
||||
_informationUnits + informationDelta,
|
||||
);
|
||||
});
|
||||
ref
|
||||
.read(placeControllerProvider.notifier)
|
||||
.setReviewDuration(_waveController.timeElapsed);
|
||||
context.read<PlaceCubit>().setReviewDuration(_waveController.timeElapsed);
|
||||
}
|
||||
|
||||
double _normalizeDbLevel(double currentDb) {
|
||||
@@ -869,7 +865,7 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = ref.read(placeControllerProvider.notifier);
|
||||
final controller = context.read<PlaceCubit>();
|
||||
final informationProgress = (_informationUnits / _minimumInformationUnits)
|
||||
.clamp(0.0, 1.0);
|
||||
final content = switch (_step) {
|
||||
@@ -916,7 +912,7 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
context.pop();
|
||||
},
|
||||
),
|
||||
};
|
||||
@@ -937,7 +933,7 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
|
||||
step: _step,
|
||||
total: 3,
|
||||
dark: true,
|
||||
onClose: () => Navigator.of(context).pop(),
|
||||
onClose: () => context.pop(),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
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 {
|
||||
const _IntroStep({required this.onNext});
|
||||
|
||||
+3
-94
@@ -1,101 +1,10 @@
|
||||
import 'package:flutter/gestures.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 'screens/mapflow_shell.dart';
|
||||
import 'app/app.dart';
|
||||
import 'shared/auth/telegram_session.dart' as telegram_session;
|
||||
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
telegram_session.configureTelegramWebApp();
|
||||
runApp(const ProviderScope(child: 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(),
|
||||
);
|
||||
}
|
||||
runApp(const MapflowApp());
|
||||
}
|
||||
|
||||
+56
-208
@@ -1,22 +1,6 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -41,6 +25,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.13.1"
|
||||
bloc:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: bloc
|
||||
sha256: e03b235924e4f509c27b5d6b2f949200e0a91149a9818b4f65eeb56662b75413
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.2.1"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -57,14 +49,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -89,22 +73,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -185,11 +153,35 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
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:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@@ -206,14 +198,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -232,14 +216,6 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -312,6 +288,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -336,14 +320,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -360,14 +336,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.20.2"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: io
|
||||
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
jni:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -452,10 +420,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.0"
|
||||
version: "1.18.0"
|
||||
mgrs_dart:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -464,14 +432,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -480,14 +440,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.17.6"
|
||||
node_preamble:
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: node_preamble
|
||||
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
|
||||
name: nested
|
||||
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
version: "1.0.0"
|
||||
objective_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -608,14 +568,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pool
|
||||
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.2"
|
||||
posix:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -632,6 +584,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -712,46 +672,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -765,22 +685,6 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -797,14 +701,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -829,30 +725,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
test:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test
|
||||
sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.30.0"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.10"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.16"
|
||||
version: "0.7.11"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -917,14 +797,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.2.0"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: watcher
|
||||
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
waveform_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -949,30 +821,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
+3
-1
@@ -34,7 +34,6 @@ dependencies:
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.8
|
||||
flutter_riverpod: ^3.3.1
|
||||
flutter_map: ^8.3.0
|
||||
latlong2: ^0.9.1
|
||||
http: ^1.6.0
|
||||
@@ -43,6 +42,9 @@ dependencies:
|
||||
flutter_svg: ^2.3.0
|
||||
waveform_recorder: ^1.8.0
|
||||
waveform_flutter: ^1.2.0
|
||||
go_router: ^17.3.0
|
||||
flutter_bloc: ^9.1.1
|
||||
flex_color_scheme: ^8.4.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
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() {
|
||||
testWidgets('renders Telegram login before authorization', (tester) async {
|
||||
await tester.pumpWidget(const ProviderScope(child: MapflowApp()));
|
||||
await tester.pumpWidget(const MapflowApp());
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('MapFlow'), findsOneWidget);
|
||||
|
||||
Reference in New Issue
Block a user