Initial Flutter app
This commit is contained in:
209
lib/state/experience_controller.dart
Normal file
209
lib/state/experience_controller.dart
Normal file
@@ -0,0 +1,209 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
import '../models/experience_models.dart';
|
||||
|
||||
final experienceControllerProvider =
|
||||
NotifierProvider<ExperienceController, ExperienceState>(
|
||||
ExperienceController.new,
|
||||
);
|
||||
|
||||
class ExperienceState {
|
||||
const ExperienceState({
|
||||
required this.experiences,
|
||||
required this.selectedExperienceId,
|
||||
required this.filterEmotion,
|
||||
required this.profile,
|
||||
});
|
||||
|
||||
final List<PlaceExperience> experiences;
|
||||
final String? selectedExperienceId;
|
||||
final ExperienceEmotion? filterEmotion;
|
||||
final ExperienceAuthor profile;
|
||||
|
||||
List<PlaceExperience> get visibleExperiences {
|
||||
final emotion = filterEmotion;
|
||||
if (emotion == null) {
|
||||
return experiences;
|
||||
}
|
||||
return experiences.where((item) => item.emotion == emotion).toList();
|
||||
}
|
||||
|
||||
PlaceExperience? get selectedExperience {
|
||||
for (final experience in experiences) {
|
||||
if (experience.id == selectedExperienceId) {
|
||||
return experience;
|
||||
}
|
||||
}
|
||||
return visibleExperiences.isEmpty ? null : visibleExperiences.first;
|
||||
}
|
||||
|
||||
ExperienceState copyWith({
|
||||
List<PlaceExperience>? experiences,
|
||||
String? selectedExperienceId,
|
||||
bool clearSelection = false,
|
||||
ExperienceEmotion? filterEmotion,
|
||||
bool clearFilter = false,
|
||||
ExperienceAuthor? profile,
|
||||
}) {
|
||||
return ExperienceState(
|
||||
experiences: experiences ?? this.experiences,
|
||||
selectedExperienceId: clearSelection
|
||||
? null
|
||||
: selectedExperienceId ?? this.selectedExperienceId,
|
||||
filterEmotion: clearFilter ? null : filterEmotion ?? this.filterEmotion,
|
||||
profile: profile ?? this.profile,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ExperienceController extends Notifier<ExperienceState> {
|
||||
@override
|
||||
ExperienceState build() {
|
||||
final experiences = _seedExperiences();
|
||||
return ExperienceState(
|
||||
experiences: experiences,
|
||||
selectedExperienceId: experiences.first.id,
|
||||
filterEmotion: null,
|
||||
profile: const ExperienceAuthor(
|
||||
name: 'Руслан',
|
||||
facets: [
|
||||
ProfileFacet(name: 'темп', value: 'спокойно, без очередей'),
|
||||
ProfileFacet(name: 'еда', value: 'яркое блюдо важнее кухни'),
|
||||
ProfileFacet(
|
||||
name: 'контекст',
|
||||
value: 'работа днем, прогулки вечером',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void selectExperience(String id) {
|
||||
state = state.copyWith(selectedExperienceId: id);
|
||||
}
|
||||
|
||||
void setEmotionFilter(ExperienceEmotion? emotion) {
|
||||
if (emotion == null) {
|
||||
state = state.copyWith(clearFilter: true);
|
||||
return;
|
||||
}
|
||||
final nextVisible = state.experiences.firstWhere(
|
||||
(item) => item.emotion == emotion,
|
||||
orElse: () => state.experiences.first,
|
||||
);
|
||||
state = state.copyWith(
|
||||
filterEmotion: emotion,
|
||||
selectedExperienceId: nextVisible.id,
|
||||
);
|
||||
}
|
||||
|
||||
void addExperience({
|
||||
required String placeName,
|
||||
required String dishName,
|
||||
required ExperienceEmotion emotion,
|
||||
required LatLng coordinate,
|
||||
required String context,
|
||||
}) {
|
||||
final experience = PlaceExperience(
|
||||
id: 'local-${DateTime.now().microsecondsSinceEpoch}',
|
||||
placeName: placeName.trim().isEmpty ? 'Новое место' : placeName.trim(),
|
||||
neighborhood: 'рядом',
|
||||
coordinate: coordinate,
|
||||
emotion: emotion,
|
||||
intensity: 3,
|
||||
context: context.trim().isEmpty ? 'личная заметка' : context.trim(),
|
||||
dish: DishSignal(
|
||||
name: dishName.trim().isEmpty ? 'блюдо' : dishName.trim(),
|
||||
reason: 'стоит проверить лично',
|
||||
texture: 'новый сигнал',
|
||||
),
|
||||
author: state.profile,
|
||||
createdLabel: 'сейчас',
|
||||
);
|
||||
|
||||
state = state.copyWith(
|
||||
experiences: [experience, ...state.experiences],
|
||||
selectedExperienceId: experience.id,
|
||||
clearFilter: true,
|
||||
);
|
||||
}
|
||||
|
||||
List<PlaceExperience> _seedExperiences() {
|
||||
const author = ExperienceAuthor(
|
||||
name: 'Mira',
|
||||
facets: [
|
||||
ProfileFacet(name: 'темп', value: 'медленно'),
|
||||
ProfileFacet(name: 'еда', value: 'текстура'),
|
||||
ProfileFacet(name: 'настроение', value: 'тихое внимание'),
|
||||
],
|
||||
);
|
||||
|
||||
return const [
|
||||
PlaceExperience(
|
||||
id: 'secret-garden',
|
||||
placeName: 'Secret Garden',
|
||||
neighborhood: 'District 1',
|
||||
coordinate: LatLng(10.7752, 106.7009),
|
||||
emotion: ExperienceEmotion.comfort,
|
||||
intensity: 4,
|
||||
context: 'крыша, зелень, хороший разговор',
|
||||
dish: DishSignal(
|
||||
name: 'caramelized pork clay pot',
|
||||
reason: 'мягко собирает вечер',
|
||||
texture: 'густой соус, рис, тепло',
|
||||
),
|
||||
author: author,
|
||||
createdLabel: 'вчера',
|
||||
),
|
||||
PlaceExperience(
|
||||
id: 'banh-mi-huynh-hoa',
|
||||
placeName: 'Banh Mi Huynh Hoa',
|
||||
neighborhood: 'District 1',
|
||||
coordinate: LatLng(10.7716, 106.6920),
|
||||
emotion: ExperienceEmotion.energy,
|
||||
intensity: 5,
|
||||
context: 'быстро, плотно, без церемоний',
|
||||
dish: DishSignal(
|
||||
name: 'banh mi dac biet',
|
||||
reason: 'если хочется прямого удара вкуса',
|
||||
texture: 'хруст, паштет, травы',
|
||||
),
|
||||
author: author,
|
||||
createdLabel: '3 дня назад',
|
||||
),
|
||||
PlaceExperience(
|
||||
id: 'the-workshop',
|
||||
placeName: 'The Workshop',
|
||||
neighborhood: 'District 1',
|
||||
coordinate: LatLng(10.7740, 106.7042),
|
||||
emotion: ExperienceEmotion.focus,
|
||||
intensity: 4,
|
||||
context: 'ноутбук, кофе, два часа ясности',
|
||||
dish: DishSignal(
|
||||
name: 'egg coffee',
|
||||
reason: 'сладкая пауза между задачами',
|
||||
texture: 'крем, горечь, плотность',
|
||||
),
|
||||
author: author,
|
||||
createdLabel: 'на неделе',
|
||||
),
|
||||
PlaceExperience(
|
||||
id: 'oc-dao',
|
||||
placeName: 'Oc Dao',
|
||||
neighborhood: 'District 1',
|
||||
coordinate: LatLng(10.7607, 106.6898),
|
||||
emotion: ExperienceEmotion.curiosity,
|
||||
intensity: 5,
|
||||
context: 'пробовать руками, спорить, заказывать еще',
|
||||
dish: DishSignal(
|
||||
name: 'grilled scallops',
|
||||
reason: 'блюдо ведет сильнее, чем место',
|
||||
texture: 'дым, масло, арахис',
|
||||
),
|
||||
author: author,
|
||||
createdLabel: 'месяц назад',
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
233
lib/state/place_controller.dart
Normal file
233
lib/state/place_controller.dart
Normal file
@@ -0,0 +1,233 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
import '../models/place_models.dart';
|
||||
|
||||
final placeControllerProvider = NotifierProvider<PlaceController, PlaceState>(
|
||||
PlaceController.new,
|
||||
);
|
||||
|
||||
class PlaceState {
|
||||
const PlaceState({
|
||||
required this.intent,
|
||||
required this.places,
|
||||
required this.selectedPlaceId,
|
||||
required this.reviewDraft,
|
||||
});
|
||||
|
||||
final UserIntent intent;
|
||||
final List<PlaceRecommendation> places;
|
||||
final String? selectedPlaceId;
|
||||
final VoiceReviewDraft reviewDraft;
|
||||
|
||||
List<PlaceRecommendation> get recommendations {
|
||||
final wanted = intent.traits;
|
||||
final ranked = [...places]
|
||||
..sort((a, b) {
|
||||
final aScore = a.traits.intersection(wanted).length;
|
||||
final bScore = b.traits.intersection(wanted).length;
|
||||
return bScore.compareTo(aScore);
|
||||
});
|
||||
return ranked.take(4).toList();
|
||||
}
|
||||
|
||||
PlaceRecommendation? get selectedPlace {
|
||||
for (final place in places) {
|
||||
if (place.id == selectedPlaceId) {
|
||||
return place;
|
||||
}
|
||||
}
|
||||
return recommendations.isEmpty ? null : recommendations.first;
|
||||
}
|
||||
|
||||
PlaceState copyWith({
|
||||
UserIntent? intent,
|
||||
List<PlaceRecommendation>? places,
|
||||
String? selectedPlaceId,
|
||||
VoiceReviewDraft? reviewDraft,
|
||||
}) {
|
||||
return PlaceState(
|
||||
intent: intent ?? this.intent,
|
||||
places: places ?? this.places,
|
||||
selectedPlaceId: selectedPlaceId ?? this.selectedPlaceId,
|
||||
reviewDraft: reviewDraft ?? this.reviewDraft,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PlaceController extends Notifier<PlaceState> {
|
||||
@override
|
||||
PlaceState build() {
|
||||
final places = _seedPlaces();
|
||||
return PlaceState(
|
||||
intent: UserIntent.exhale,
|
||||
places: places,
|
||||
selectedPlaceId: places.first.id,
|
||||
reviewDraft: const VoiceReviewDraft(
|
||||
placeName: '',
|
||||
duration: Duration.zero,
|
||||
extractedTraits: {},
|
||||
suggestedIntents: {},
|
||||
evidence: [],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void selectIntent(UserIntent intent) {
|
||||
PlaceRecommendation? next;
|
||||
for (final place in state.places) {
|
||||
if (place.traits.intersection(intent.traits).isNotEmpty) {
|
||||
next = place;
|
||||
break;
|
||||
}
|
||||
}
|
||||
state = state.copyWith(intent: intent, selectedPlaceId: next?.id);
|
||||
}
|
||||
|
||||
void selectPlace(String placeId) {
|
||||
state = state.copyWith(selectedPlaceId: placeId);
|
||||
}
|
||||
|
||||
void setReviewPlace(String placeName) {
|
||||
state = state.copyWith(
|
||||
reviewDraft: state.reviewDraft.copyWith(placeName: placeName),
|
||||
);
|
||||
}
|
||||
|
||||
void setReviewDuration(Duration duration) {
|
||||
state = state.copyWith(
|
||||
reviewDraft: state.reviewDraft.copyWith(duration: duration),
|
||||
);
|
||||
}
|
||||
|
||||
void analyzeVoiceReview() {
|
||||
final placeName = state.reviewDraft.placeName.trim().isEmpty
|
||||
? 'Новое место'
|
||||
: state.reviewDraft.placeName.trim();
|
||||
|
||||
state = state.copyWith(
|
||||
reviewDraft: state.reviewDraft.copyWith(
|
||||
placeName: placeName,
|
||||
duration: state.reviewDraft.duration.inSeconds < 30
|
||||
? const Duration(seconds: 36)
|
||||
: state.reviewDraft.duration,
|
||||
extractedTraits: {
|
||||
PlaceTrait.cozy,
|
||||
PlaceTrait.private,
|
||||
PlaceTrait.beautiful,
|
||||
PlaceTrait.calm,
|
||||
},
|
||||
suggestedIntents: {UserIntent.exhale, UserIntent.date},
|
||||
evidence: [
|
||||
'можно нормально поговорить',
|
||||
'место мягкое, не давит',
|
||||
'туда хочется привести человека вечером',
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void publishReview({LatLng? coordinate}) {
|
||||
final draft = state.reviewDraft;
|
||||
final place = PlaceRecommendation(
|
||||
id: 'local-${DateTime.now().microsecondsSinceEpoch}',
|
||||
name: draft.placeName.trim().isEmpty ? 'Новое место' : draft.placeName,
|
||||
area: 'добавлено голосом',
|
||||
photoUrls: const [
|
||||
'https://images.unsplash.com/photo-1554118811-1e0d58224f24?auto=format&fit=crop&w=600&q=80',
|
||||
'https://images.unsplash.com/photo-1514933651103-005eec06c04b?auto=format&fit=crop&w=600&q=80',
|
||||
'https://images.unsplash.com/photo-1555396273-367ea4eb4db5?auto=format&fit=crop&w=600&q=80',
|
||||
],
|
||||
coordinate: coordinate ?? const LatLng(10.7729, 106.7004),
|
||||
traits: draft.extractedTraits.isEmpty
|
||||
? {PlaceTrait.cozy, PlaceTrait.calm}
|
||||
: draft.extractedTraits,
|
||||
);
|
||||
|
||||
state = state.copyWith(
|
||||
places: [place, ...state.places],
|
||||
selectedPlaceId: place.id,
|
||||
reviewDraft: const VoiceReviewDraft(
|
||||
placeName: '',
|
||||
duration: Duration.zero,
|
||||
extractedTraits: {},
|
||||
suggestedIntents: {},
|
||||
evidence: [],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<PlaceRecommendation> _seedPlaces() {
|
||||
return const [
|
||||
PlaceRecommendation(
|
||||
id: 'secret-garden',
|
||||
name: 'Secret Garden',
|
||||
area: 'District 1',
|
||||
photoUrls: [
|
||||
'https://images.unsplash.com/photo-1552566626-52f8b828add9?auto=format&fit=crop&w=600&q=80',
|
||||
'https://images.unsplash.com/photo-1559339352-11d035aa65de?auto=format&fit=crop&w=600&q=80',
|
||||
'https://images.unsplash.com/photo-1521017432531-fbd92d768814?auto=format&fit=crop&w=600&q=80',
|
||||
],
|
||||
coordinate: LatLng(10.7752, 106.7009),
|
||||
traits: {
|
||||
PlaceTrait.calm,
|
||||
PlaceTrait.cozy,
|
||||
PlaceTrait.private,
|
||||
PlaceTrait.beautiful,
|
||||
PlaceTrait.social,
|
||||
},
|
||||
),
|
||||
PlaceRecommendation(
|
||||
id: 'workshop',
|
||||
name: 'The Workshop',
|
||||
area: 'District 1',
|
||||
photoUrls: [
|
||||
'https://images.unsplash.com/photo-1495474472287-4d71bcdd2085?auto=format&fit=crop&w=600&q=80',
|
||||
'https://images.unsplash.com/photo-1501339847302-ac426a4a7cbb?auto=format&fit=crop&w=600&q=80',
|
||||
'https://images.unsplash.com/photo-1511920170033-f8396924c348?auto=format&fit=crop&w=600&q=80',
|
||||
],
|
||||
coordinate: LatLng(10.7740, 106.7042),
|
||||
traits: {
|
||||
PlaceTrait.focused,
|
||||
PlaceTrait.calm,
|
||||
PlaceTrait.neutral,
|
||||
PlaceTrait.solo,
|
||||
},
|
||||
),
|
||||
PlaceRecommendation(
|
||||
id: 'oc-dao',
|
||||
name: 'Oc Dao',
|
||||
area: 'District 1',
|
||||
photoUrls: [
|
||||
'https://images.unsplash.com/photo-1555396273-367ea4eb4db5?auto=format&fit=crop&w=600&q=80',
|
||||
'https://images.unsplash.com/photo-1544025162-d76694265947?auto=format&fit=crop&w=600&q=80',
|
||||
'https://images.unsplash.com/photo-1551218808-94e220e084d2?auto=format&fit=crop&w=600&q=80',
|
||||
],
|
||||
coordinate: LatLng(10.7607, 106.6898),
|
||||
traits: {
|
||||
PlaceTrait.alive,
|
||||
PlaceTrait.open,
|
||||
PlaceTrait.social,
|
||||
PlaceTrait.unusual,
|
||||
},
|
||||
),
|
||||
PlaceRecommendation(
|
||||
id: 'l-usine',
|
||||
name: 'L\'Usine',
|
||||
area: 'Dong Khoi',
|
||||
photoUrls: [
|
||||
'https://images.unsplash.com/photo-1514933651103-005eec06c04b?auto=format&fit=crop&w=600&q=80',
|
||||
'https://images.unsplash.com/photo-1550966871-3ed3cdb5ed0c?auto=format&fit=crop&w=600&q=80',
|
||||
'https://images.unsplash.com/photo-1551632436-cbf8dd35adfa?auto=format&fit=crop&w=600&q=80',
|
||||
],
|
||||
coordinate: LatLng(10.7755, 106.7038),
|
||||
traits: {
|
||||
PlaceTrait.status,
|
||||
PlaceTrait.beautiful,
|
||||
PlaceTrait.private,
|
||||
PlaceTrait.clear,
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user