Mztj67_#M}W?l>kYSliK<%xAp;0j{!}J0!o7b
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
literal 0
HcmV?d00001
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..0467bf12aa4d28f374bb26596605a46dcbb3e7c8
GIT binary patch
literal 1418
zcmV;51$Fv~P)q
zKfU)WzW*n(@|xWGCA9ScMt*e9`2kdxPQ&&>|-UCa7_51w+
zLUsW@ZzZSW0y$)Hp~e9%PvP|a03ks1`~K?q{u;6NC8*{AOqIUq{CL&;p56Lf$oQGq
z^={4hPQv)y=I|4n+?>7Fim=dxt1
z2H+Dm+1+fh+IF>G0SjJMkQQre1x4|G*Z==(Ot&kCnUrL4I(rf(ucITwmuHf^hXiJT
zkdTm&kdTm&kdTm&kdP`esgWG0BcWCVkVZ&2dUwN`cgM8QJb`Z7Z~e<&Yj2(}>Tmf`
zm1{eLgw!b{bXkjWbF%dTkTZEJWyWOb##Lfw4EK2}<0d6%>AGS{po>WCOy&f$Tay_>
z?NBlkpo@s-O;0V%Y_Xa-G#_O08q5LR*~F%&)}{}r&L%Sbs8AS4t7Y0NEx*{soY=0MZExqA5XHQkqi#4gW3
zqODM^iyZl;dvf)-bOXtOru(s)Uc7~BFx{w-FK;2{`VA?(g&@3z&bfLFyctOH!cVsF
z7IL=fo-qBndRUm;kAdXR4e6>k-z|21AaN%ubeVrHl*<|s&Ax@W-t?LR(P-24A5=>a
z*R9#QvjzF8n%@1Nw@?CG@6(%>+-0ASK~jEmCV|&a*7-GKT72W<(TbSjf)&Eme6nGE
z>Gkj4Sq&2e+-G%|+NM8OOm5zVl9{Z8Dd8A5z3y8mZ=4Bv4%>as_{9cN#bm~;h>62(
zdqY93Zy}v&c4n($Vv!UybR8ocs7#zbfX1IY-*w~)p}XyZ-SFC~4w>BvMVr`dFbelV{lLL0bx7@*ZZdebr3`sP;?
zVImji)kG)(6Juv0lz@q`F!k1FE;CQ(D0iG$wchPbKZQELlsZ#~rt8#90Y_Xh&3U-<
z{s<&cCV_1`^TD^ia9!*mQDq&
zn2{r`j};V|uV%_wsP!zB?m%;FeaRe+X47K0e+KE!8C{gAWF8)lCd1u1%~|M!XNRvw
zvtqy3iz0WSpWdhn6$hP8PaRBmp)q`#PCA`Vd#Tc$@f1tAcM>f_I@bC)hkI9|o(Iqv
zo}Piadq!j76}004RBio<`)70k^`K1NK)q>w?p^C6J2ZC!+UppiK6&y3Kmbv&O!oYF
z34$0Z;QO!JOY#!`qyGH<3Pd}Pt@q*A0V=3SVtWKRR8d8Z&@)3qLPA19LPA19LPEUC
YUoZo%k(ykuW&i*H07*qoM6N<$f+CH{y8r+H
literal 0
HcmV?d00001
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
new file mode 100644
index 0000000..0bedcf2
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
new file mode 100644
index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838
GIT binary patch
literal 68
zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J
Q1PU{Fy85}Sb4q9e0B4a5jsO4v
literal 0
HcmV?d00001
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838
GIT binary patch
literal 68
zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J
Q1PU{Fy85}Sb4q9e0B4a5jsO4v
literal 0
HcmV?d00001
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
new file mode 100644
index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838
GIT binary patch
literal 68
zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J
Q1PU{Fy85}Sb4q9e0B4a5jsO4v
literal 0
HcmV?d00001
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
new file mode 100644
index 0000000..89c2725
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
@@ -0,0 +1,5 @@
+# Launch Screen Assets
+
+You can customize the launch screen with your own desired assets by replacing the image files in this directory.
+
+You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
\ No newline at end of file
diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..f2e259c
--- /dev/null
+++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..f3c2851
--- /dev/null
+++ b/ios/Runner/Base.lproj/Main.storyboard
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
new file mode 100644
index 0000000..8fde69f
--- /dev/null
+++ b/ios/Runner/Info.plist
@@ -0,0 +1,49 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ Mapflow
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ mapflow
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ LSRequiresIPhoneOS
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ CADisableMinimumFrameDurationOnPhone
+
+ UIApplicationSupportsIndirectInputEvents
+
+
+
diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h
new file mode 100644
index 0000000..308a2a5
--- /dev/null
+++ b/ios/Runner/Runner-Bridging-Header.h
@@ -0,0 +1 @@
+#import "GeneratedPluginRegistrant.h"
diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift
new file mode 100644
index 0000000..86a7c3b
--- /dev/null
+++ b/ios/RunnerTests/RunnerTests.swift
@@ -0,0 +1,12 @@
+import Flutter
+import UIKit
+import XCTest
+
+class RunnerTests: XCTestCase {
+
+ func testExample() {
+ // If you add code to the Runner application, consider adding tests here.
+ // See https://developer.apple.com/documentation/xctest for more information about using XCTest.
+ }
+
+}
diff --git a/lib/main.dart b/lib/main.dart
new file mode 100644
index 0000000..d0dd374
--- /dev/null
+++ b/lib/main.dart
@@ -0,0 +1,99 @@
+import 'package:flutter/gestures.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+
+import 'screens/mapflow_shell.dart';
+
+void main() {
+ WidgetsFlutterBinding.ensureInitialized();
+ 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.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(),
+ );
+ }
+}
diff --git a/lib/models/experience_models.dart b/lib/models/experience_models.dart
new file mode 100644
index 0000000..fce758f
--- /dev/null
+++ b/lib/models/experience_models.dart
@@ -0,0 +1,78 @@
+import 'package:flutter/material.dart';
+import 'package:latlong2/latlong.dart';
+
+enum ExperienceEmotion { comfort, energy, curiosity, tenderness, focus }
+
+extension ExperienceEmotionText on ExperienceEmotion {
+ String get label {
+ return switch (this) {
+ ExperienceEmotion.comfort => 'уют',
+ ExperienceEmotion.energy => 'энергия',
+ ExperienceEmotion.curiosity => 'любопытство',
+ ExperienceEmotion.tenderness => 'нежность',
+ ExperienceEmotion.focus => 'собранность',
+ };
+ }
+
+ IconData get icon {
+ return switch (this) {
+ ExperienceEmotion.comfort => Icons.weekend_outlined,
+ ExperienceEmotion.energy => Icons.bolt_outlined,
+ ExperienceEmotion.curiosity => Icons.explore_outlined,
+ ExperienceEmotion.tenderness => Icons.spa_outlined,
+ ExperienceEmotion.focus => Icons.center_focus_strong_outlined,
+ };
+ }
+}
+
+class DishSignal {
+ const DishSignal({
+ required this.name,
+ required this.reason,
+ required this.texture,
+ });
+
+ final String name;
+ final String reason;
+ final String texture;
+}
+
+class ProfileFacet {
+ const ProfileFacet({required this.name, required this.value});
+
+ final String name;
+ final String value;
+}
+
+class ExperienceAuthor {
+ const ExperienceAuthor({required this.name, required this.facets});
+
+ final String name;
+ final List facets;
+}
+
+class PlaceExperience {
+ const PlaceExperience({
+ required this.id,
+ required this.placeName,
+ required this.neighborhood,
+ required this.coordinate,
+ required this.emotion,
+ required this.intensity,
+ required this.context,
+ required this.dish,
+ required this.author,
+ required this.createdLabel,
+ });
+
+ final String id;
+ final String placeName;
+ final String neighborhood;
+ final LatLng coordinate;
+ final ExperienceEmotion emotion;
+ final int intensity;
+ final String context;
+ final DishSignal dish;
+ final ExperienceAuthor author;
+ final String createdLabel;
+}
diff --git a/lib/models/place_models.dart b/lib/models/place_models.dart
new file mode 100644
index 0000000..acc52d7
--- /dev/null
+++ b/lib/models/place_models.dart
@@ -0,0 +1,174 @@
+import 'package:flutter/material.dart';
+import 'package:latlong2/latlong.dart';
+
+enum PlaceTrait {
+ calm,
+ alive,
+ cozy,
+ cold,
+ status,
+ simple,
+ free,
+ formal,
+ private,
+ open,
+ social,
+ solo,
+ beautiful,
+ neutral,
+ unusual,
+ clear,
+ focused,
+ transit,
+}
+
+extension PlaceTraitText on PlaceTrait {
+ String get label {
+ return switch (this) {
+ PlaceTrait.calm => 'спокойное',
+ PlaceTrait.alive => 'живое',
+ PlaceTrait.cozy => 'уютное',
+ PlaceTrait.cold => 'холодное',
+ PlaceTrait.status => 'статусное',
+ PlaceTrait.simple => 'простое',
+ PlaceTrait.free => 'свободное',
+ PlaceTrait.formal => 'формальное',
+ PlaceTrait.private => 'приватное',
+ PlaceTrait.open => 'открытое',
+ PlaceTrait.social => 'для общения',
+ PlaceTrait.solo => 'для себя',
+ PlaceTrait.beautiful => 'красивое',
+ PlaceTrait.neutral => 'нейтральное',
+ PlaceTrait.unusual => 'необычное',
+ PlaceTrait.clear => 'понятное',
+ PlaceTrait.focused => 'помогает собраться',
+ PlaceTrait.transit => 'транзитное',
+ };
+ }
+}
+
+enum UserIntent { exhale, date, meet, focus, move, surprise, alone, impress }
+
+extension UserIntentText on UserIntent {
+ String get title {
+ return switch (this) {
+ UserIntent.exhale => 'выдохнуть',
+ UserIntent.date => 'свидание',
+ UserIntent.meet => 'встретиться',
+ UserIntent.focus => 'поработать',
+ UserIntent.move => 'движ',
+ UserIntent.surprise => 'удивиться',
+ UserIntent.alone => 'побыть одному',
+ UserIntent.impress => 'привести кого-то',
+ };
+ }
+
+ String get subtitle {
+ return switch (this) {
+ UserIntent.exhale => 'тихо, мягко, без давления',
+ UserIntent.date => 'красиво и лично',
+ UserIntent.meet => 'общение без лишней формальности',
+ UserIntent.focus => 'собраться и не выпадать',
+ UserIntent.move => 'живо, шумно, с энергией',
+ UserIntent.surprise => 'не как обычно',
+ UserIntent.alone => 'быть в своем ритме',
+ UserIntent.impress => 'место должно держать момент',
+ };
+ }
+
+ IconData get icon {
+ return switch (this) {
+ UserIntent.exhale => Icons.air_outlined,
+ UserIntent.date => Icons.favorite_border,
+ UserIntent.meet => Icons.forum_outlined,
+ UserIntent.focus => Icons.center_focus_strong_outlined,
+ UserIntent.move => Icons.bolt_outlined,
+ UserIntent.surprise => Icons.auto_awesome_outlined,
+ UserIntent.alone => Icons.person_outline,
+ UserIntent.impress => Icons.diamond_outlined,
+ };
+ }
+
+ Set get traits {
+ return switch (this) {
+ UserIntent.exhale => {PlaceTrait.calm, PlaceTrait.cozy, PlaceTrait.solo},
+ UserIntent.date => {
+ PlaceTrait.private,
+ PlaceTrait.beautiful,
+ PlaceTrait.social,
+ },
+ UserIntent.meet => {PlaceTrait.social, PlaceTrait.free, PlaceTrait.alive},
+ UserIntent.focus => {
+ PlaceTrait.focused,
+ PlaceTrait.calm,
+ PlaceTrait.neutral,
+ },
+ UserIntent.move => {PlaceTrait.alive, PlaceTrait.open, PlaceTrait.social},
+ UserIntent.surprise => {
+ PlaceTrait.unusual,
+ PlaceTrait.alive,
+ PlaceTrait.open,
+ },
+ UserIntent.alone => {PlaceTrait.solo, PlaceTrait.free, PlaceTrait.calm},
+ UserIntent.impress => {
+ PlaceTrait.status,
+ PlaceTrait.beautiful,
+ PlaceTrait.private,
+ },
+ };
+ }
+}
+
+class PlaceRecommendation {
+ const PlaceRecommendation({
+ required this.id,
+ required this.name,
+ required this.area,
+ required this.photoUrls,
+ required this.coordinate,
+ required this.traits,
+ });
+
+ final String id;
+ final String name;
+ final String area;
+ final List photoUrls;
+ final LatLng coordinate;
+ final Set traits;
+
+ String get coverPhotoUrl => photoUrls.first;
+}
+
+class VoiceReviewDraft {
+ const VoiceReviewDraft({
+ required this.placeName,
+ required this.duration,
+ required this.extractedTraits,
+ required this.suggestedIntents,
+ required this.evidence,
+ });
+
+ final String placeName;
+ final Duration duration;
+ final Set extractedTraits;
+ final Set suggestedIntents;
+ final List evidence;
+
+ bool get isLongEnough => duration.inSeconds >= 30;
+
+ VoiceReviewDraft copyWith({
+ String? placeName,
+ Duration? duration,
+ Set? extractedTraits,
+ Set? suggestedIntents,
+ List? evidence,
+ }) {
+ return VoiceReviewDraft(
+ placeName: placeName ?? this.placeName,
+ duration: duration ?? this.duration,
+ extractedTraits: extractedTraits ?? this.extractedTraits,
+ suggestedIntents: suggestedIntents ?? this.suggestedIntents,
+ evidence: evidence ?? this.evidence,
+ );
+ }
+}
diff --git a/lib/screens/experience_map_screen.dart b/lib/screens/experience_map_screen.dart
new file mode 100644
index 0000000..91f2599
--- /dev/null
+++ b/lib/screens/experience_map_screen.dart
@@ -0,0 +1,495 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_map/flutter_map.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:latlong2/latlong.dart';
+
+import '../models/experience_models.dart';
+import '../state/experience_controller.dart';
+
+class ExperienceMapScreen extends ConsumerWidget {
+ const ExperienceMapScreen({super.key});
+
+ static const _initialCenter = LatLng(10.7718, 106.6982);
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final state = ref.watch(experienceControllerProvider);
+ final selected = state.selectedExperience;
+
+ return Scaffold(
+ body: LayoutBuilder(
+ builder: (context, constraints) {
+ final wide = constraints.maxWidth >= 780;
+ return Stack(
+ children: [
+ FlutterMap(
+ options: MapOptions(
+ initialCenter: selected?.coordinate ?? _initialCenter,
+ initialZoom: 14.2,
+ minZoom: 3,
+ maxZoom: 18,
+ onLongPress: (_, point) =>
+ _showShareSheet(context, ref, point),
+ ),
+ children: [
+ TileLayer(
+ urlTemplate:
+ 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
+ userAgentPackageName: 'com.mapflow.app',
+ ),
+ MarkerLayer(
+ markers: [
+ for (final experience in state.visibleExperiences)
+ Marker(
+ width: 54,
+ height: 54,
+ point: experience.coordinate,
+ child: _ExperienceMarker(
+ experience: experience,
+ selected: selected?.id == experience.id,
+ onTap: () => ref
+ .read(experienceControllerProvider.notifier)
+ .selectExperience(experience.id),
+ ),
+ ),
+ ],
+ ),
+ const RichAttributionWidget(
+ attributions: [
+ TextSourceAttribution('OpenStreetMap contributors'),
+ ],
+ ),
+ ],
+ ),
+ SafeArea(
+ child: Padding(
+ padding: const EdgeInsets.all(14),
+ child: Align(
+ alignment: Alignment.topLeft,
+ child: _TopPanel(state: state),
+ ),
+ ),
+ ),
+ if (wide)
+ SafeArea(
+ child: Align(
+ alignment: Alignment.centerRight,
+ child: Padding(
+ padding: const EdgeInsets.all(18),
+ child: SizedBox(
+ width: 330,
+ child: _ExperiencePanel(
+ experience: selected,
+ onShare: () => _showShareSheet(
+ context,
+ ref,
+ selected?.coordinate ?? _initialCenter,
+ ),
+ ),
+ ),
+ ),
+ ),
+ )
+ else
+ Align(
+ alignment: Alignment.bottomCenter,
+ child: _BottomExperiencePanel(
+ experience: selected,
+ onShare: () => _showShareSheet(
+ context,
+ ref,
+ selected?.coordinate ?? _initialCenter,
+ ),
+ ),
+ ),
+ ],
+ );
+ },
+ ),
+ );
+ }
+
+ void _showShareSheet(BuildContext context, WidgetRef ref, LatLng coordinate) {
+ final placeController = TextEditingController();
+ final dishController = TextEditingController();
+ final contextController = TextEditingController();
+ var emotion = ExperienceEmotion.comfort;
+
+ showModalBottomSheet(
+ context: context,
+ showDragHandle: true,
+ isScrollControlled: true,
+ builder: (sheetContext) {
+ return StatefulBuilder(
+ builder: (context, setState) {
+ return Padding(
+ padding: EdgeInsets.fromLTRB(
+ 18,
+ 8,
+ 18,
+ MediaQuery.viewInsetsOf(context).bottom + 18,
+ ),
+ child: ConstrainedBox(
+ constraints: const BoxConstraints(maxWidth: 560),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'Поделиться опытом',
+ style: Theme.of(context).textTheme.titleLarge?.copyWith(
+ fontWeight: FontWeight.w800,
+ ),
+ ),
+ const SizedBox(height: 14),
+ TextField(
+ controller: placeController,
+ decoration: const InputDecoration(labelText: 'Место'),
+ ),
+ const SizedBox(height: 10),
+ TextField(
+ controller: dishController,
+ decoration: const InputDecoration(labelText: 'Блюдо'),
+ ),
+ const SizedBox(height: 10),
+ TextField(
+ controller: contextController,
+ minLines: 2,
+ maxLines: 3,
+ decoration: const InputDecoration(labelText: 'Контекст'),
+ ),
+ const SizedBox(height: 12),
+ Wrap(
+ spacing: 8,
+ runSpacing: 8,
+ children: [
+ for (final item in ExperienceEmotion.values)
+ ChoiceChip(
+ label: Text(item.label),
+ selected: emotion == item,
+ onSelected: (_) => setState(() => emotion = item),
+ ),
+ ],
+ ),
+ const SizedBox(height: 16),
+ SizedBox(
+ width: double.infinity,
+ child: FilledButton.icon(
+ onPressed: () {
+ ref
+ .read(experienceControllerProvider.notifier)
+ .addExperience(
+ placeName: placeController.text,
+ dishName: dishController.text,
+ emotion: emotion,
+ coordinate: coordinate,
+ context: contextController.text,
+ );
+ Navigator.of(sheetContext).pop();
+ },
+ icon: const Icon(Icons.add_location_alt_outlined),
+ label: const Text('Сохранить'),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ },
+ );
+ },
+ ).whenComplete(() {
+ placeController.dispose();
+ dishController.dispose();
+ contextController.dispose();
+ });
+ }
+}
+
+class _TopPanel extends ConsumerWidget {
+ const _TopPanel({required this.state});
+
+ final ExperienceState state;
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final controller = ref.read(experienceControllerProvider.notifier);
+
+ return Material(
+ color: const Color(0xFFFFFBF5),
+ borderRadius: BorderRadius.circular(8),
+ child: Padding(
+ padding: const EdgeInsets.all(12),
+ child: ConstrainedBox(
+ constraints: const BoxConstraints(maxWidth: 560),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const Icon(Icons.map_outlined, size: 22),
+ const SizedBox(width: 8),
+ Text(
+ 'MapFlow',
+ style: Theme.of(context).textTheme.titleLarge?.copyWith(
+ fontWeight: FontWeight.w900,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 10),
+ Wrap(
+ spacing: 8,
+ runSpacing: 8,
+ children: [
+ FilterChip(
+ label: const Text('все'),
+ selected: state.filterEmotion == null,
+ onSelected: (_) => controller.setEmotionFilter(null),
+ ),
+ for (final emotion in ExperienceEmotion.values)
+ FilterChip(
+ avatar: Icon(emotion.icon, size: 17),
+ label: Text(emotion.label),
+ selected: state.filterEmotion == emotion,
+ onSelected: (_) => controller.setEmotionFilter(emotion),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class _BottomExperiencePanel extends StatelessWidget {
+ const _BottomExperiencePanel({
+ required this.experience,
+ required this.onShare,
+ });
+
+ final PlaceExperience? experience;
+ final VoidCallback onShare;
+
+ @override
+ Widget build(BuildContext context) {
+ return SafeArea(
+ top: false,
+ child: Padding(
+ padding: const EdgeInsets.fromLTRB(12, 0, 12, 12),
+ child: _ExperiencePanel(experience: experience, onShare: onShare),
+ ),
+ );
+ }
+}
+
+class _ExperiencePanel extends StatelessWidget {
+ const _ExperiencePanel({required this.experience, required this.onShare});
+
+ final PlaceExperience? experience;
+ final VoidCallback onShare;
+
+ @override
+ Widget build(BuildContext context) {
+ final item = experience;
+
+ return Material(
+ color: const Color(0xFFFFFBF5),
+ borderRadius: BorderRadius.circular(8),
+ child: Padding(
+ padding: const EdgeInsets.all(14),
+ child: item == null
+ ? FilledButton.icon(
+ onPressed: onShare,
+ icon: const Icon(Icons.add_location_alt_outlined),
+ label: const Text('Поделиться'),
+ )
+ : Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ _EmotionBadge(emotion: item.emotion),
+ const SizedBox(width: 10),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ item.placeName,
+ style: Theme.of(context).textTheme.titleLarge
+ ?.copyWith(fontWeight: FontWeight.w900),
+ ),
+ Text(item.neighborhood),
+ ],
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ Text(
+ item.dish.name,
+ style: Theme.of(context).textTheme.titleMedium?.copyWith(
+ fontWeight: FontWeight.w800,
+ ),
+ ),
+ const SizedBox(height: 4),
+ Text(item.dish.reason),
+ const SizedBox(height: 8),
+ Wrap(
+ spacing: 8,
+ runSpacing: 8,
+ children: [
+ _TinyPill(text: item.dish.texture),
+ _TinyPill(text: item.context),
+ _TinyPill(text: item.createdLabel),
+ ],
+ ),
+ const SizedBox(height: 14),
+ _AuthorBlock(author: item.author),
+ const SizedBox(height: 14),
+ SizedBox(
+ width: double.infinity,
+ child: FilledButton.icon(
+ onPressed: onShare,
+ icon: const Icon(Icons.add_location_alt_outlined),
+ label: const Text('Поделиться рядом'),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class _ExperienceMarker extends StatelessWidget {
+ const _ExperienceMarker({
+ required this.experience,
+ required this.selected,
+ required this.onTap,
+ });
+
+ final PlaceExperience experience;
+ final bool selected;
+ final VoidCallback onTap;
+
+ @override
+ Widget build(BuildContext context) {
+ final color = _emotionColor(experience.emotion);
+
+ return GestureDetector(
+ onTap: onTap,
+ child: AnimatedContainer(
+ duration: const Duration(milliseconds: 180),
+ decoration: BoxDecoration(
+ color: selected ? color : Colors.white,
+ shape: BoxShape.circle,
+ border: Border.all(color: color, width: selected ? 3 : 2),
+ boxShadow: const [
+ BoxShadow(
+ color: Color(0x33000000),
+ blurRadius: 14,
+ offset: Offset(0, 8),
+ ),
+ ],
+ ),
+ child: Icon(
+ experience.emotion.icon,
+ color: selected ? Colors.white : color,
+ size: 24,
+ ),
+ ),
+ );
+ }
+}
+
+class _EmotionBadge extends StatelessWidget {
+ const _EmotionBadge({required this.emotion});
+
+ final ExperienceEmotion emotion;
+
+ @override
+ Widget build(BuildContext context) {
+ final color = _emotionColor(emotion);
+
+ return Container(
+ width: 46,
+ height: 46,
+ decoration: BoxDecoration(
+ color: color.withValues(alpha: 0.12),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Icon(emotion.icon, color: color),
+ );
+ }
+}
+
+class _AuthorBlock extends StatelessWidget {
+ const _AuthorBlock({required this.author});
+
+ final ExperienceAuthor author;
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ author.name,
+ style: Theme.of(
+ context,
+ ).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w800),
+ ),
+ const SizedBox(height: 8),
+ Wrap(
+ spacing: 8,
+ runSpacing: 8,
+ children: [
+ for (final facet in author.facets)
+ _TinyPill(text: '${facet.name}: ${facet.value}'),
+ ],
+ ),
+ ],
+ );
+ }
+}
+
+class _TinyPill extends StatelessWidget {
+ const _TinyPill({required this.text});
+
+ final String text;
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
+ decoration: BoxDecoration(
+ color: const Color(0xFFF0E8DA),
+ borderRadius: BorderRadius.circular(999),
+ ),
+ child: Text(
+ text,
+ style: Theme.of(
+ context,
+ ).textTheme.labelMedium?.copyWith(fontWeight: FontWeight.w700),
+ ),
+ );
+ }
+}
+
+Color _emotionColor(ExperienceEmotion emotion) {
+ return switch (emotion) {
+ ExperienceEmotion.comfort => const Color(0xFF0F766E),
+ ExperienceEmotion.energy => const Color(0xFFE11D48),
+ ExperienceEmotion.curiosity => const Color(0xFF7C3AED),
+ ExperienceEmotion.tenderness => const Color(0xFFDB2777),
+ ExperienceEmotion.focus => const Color(0xFF2563EB),
+ };
+}
diff --git a/lib/screens/mapflow_shell.dart b/lib/screens/mapflow_shell.dart
new file mode 100644
index 0000000..aeb37b2
--- /dev/null
+++ b/lib/screens/mapflow_shell.dart
@@ -0,0 +1,623 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_map/flutter_map.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:latlong2/latlong.dart';
+
+import '../models/place_models.dart';
+import '../state/place_controller.dart';
+
+class MapflowShell extends ConsumerWidget {
+ const MapflowShell({super.key});
+
+ static const _center = LatLng(10.7718, 106.6982);
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final state = ref.watch(placeControllerProvider);
+ final selected = state.selectedPlace;
+
+ return Scaffold(
+ body: Stack(
+ children: [
+ FlutterMap(
+ options: MapOptions(
+ initialCenter: selected?.coordinate ?? _center,
+ initialZoom: 14.2,
+ minZoom: 3,
+ maxZoom: 18,
+ ),
+ children: [
+ TileLayer(
+ urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
+ userAgentPackageName: 'com.mapflow.app',
+ ),
+ MarkerLayer(
+ markers: [
+ for (final place in state.recommendations)
+ Marker(
+ width: 52,
+ height: 52,
+ point: place.coordinate,
+ child: _PlaceMarker(
+ selected: selected?.id == place.id,
+ onTap: () => ref
+ .read(placeControllerProvider.notifier)
+ .selectPlace(place.id),
+ ),
+ ),
+ ],
+ ),
+ const RichAttributionWidget(
+ attributions: [
+ TextSourceAttribution('OpenStreetMap contributors'),
+ ],
+ ),
+ ],
+ ),
+ SafeArea(
+ child: Align(
+ alignment: Alignment.topCenter,
+ child: _IntentBar(intent: state.intent),
+ ),
+ ),
+ Align(
+ alignment: Alignment.bottomCenter,
+ child: SafeArea(
+ top: false,
+ child: _PlaceCarousel(
+ places: state.recommendations,
+ onSelect: (place) => ref
+ .read(placeControllerProvider.notifier)
+ .selectPlace(place.id),
+ ),
+ ),
+ ),
+ SafeArea(
+ child: Align(
+ alignment: Alignment.centerRight,
+ child: Padding(
+ padding: const EdgeInsets.only(right: 12),
+ child: FloatingActionButton(
+ onPressed: () => _openAddFlow(context, selected?.coordinate),
+ child: const Icon(Icons.add_location_alt_outlined),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ void _openAddFlow(BuildContext context, LatLng? coordinate) {
+ Navigator.of(context).push(
+ MaterialPageRoute(
+ fullscreenDialog: true,
+ builder: (_) => AddExperienceFlow(coordinate: coordinate ?? _center),
+ ),
+ );
+ }
+}
+
+class _IntentBar extends ConsumerWidget {
+ const _IntentBar({required this.intent});
+
+ final UserIntent intent;
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final controller = ref.read(placeControllerProvider.notifier);
+
+ return Container(
+ height: 58,
+ margin: const EdgeInsets.fromLTRB(10, 8, 10, 0),
+ child: ListView.separated(
+ scrollDirection: Axis.horizontal,
+ itemCount: UserIntent.values.length,
+ separatorBuilder: (_, _) => const SizedBox(width: 8),
+ itemBuilder: (context, index) {
+ final item = UserIntent.values[index];
+ return ChoiceChip(
+ avatar: Icon(item.icon, size: 17),
+ label: Text(item.title),
+ selected: item == intent,
+ onSelected: (_) => controller.selectIntent(item),
+ backgroundColor: const Color(0xFFFFFBF5),
+ selectedColor: Theme.of(context).colorScheme.primaryContainer,
+ padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
+ );
+ },
+ ),
+ );
+ }
+}
+
+class _PlaceMarker extends StatelessWidget {
+ const _PlaceMarker({required this.selected, required this.onTap});
+
+ final bool selected;
+ final VoidCallback onTap;
+
+ @override
+ Widget build(BuildContext context) {
+ final color = Theme.of(context).colorScheme.primary;
+ return GestureDetector(
+ onTap: onTap,
+ child: AnimatedContainer(
+ duration: const Duration(milliseconds: 160),
+ decoration: BoxDecoration(
+ color: selected ? color : Colors.white,
+ shape: BoxShape.circle,
+ border: Border.all(color: color, width: selected ? 3 : 2),
+ boxShadow: const [
+ BoxShadow(
+ color: Color(0x33000000),
+ blurRadius: 14,
+ offset: Offset(0, 8),
+ ),
+ ],
+ ),
+ child: Icon(Icons.place, color: selected ? Colors.white : color),
+ ),
+ );
+ }
+}
+
+class _PlaceCarousel extends StatelessWidget {
+ const _PlaceCarousel({required this.places, required this.onSelect});
+
+ final List places;
+ final ValueChanged onSelect;
+
+ @override
+ Widget build(BuildContext context) {
+ return SizedBox(
+ height: 172,
+ child: ListView.separated(
+ scrollDirection: Axis.horizontal,
+ padding: const EdgeInsets.fromLTRB(12, 0, 12, 12),
+ itemCount: places.length,
+ separatorBuilder: (_, _) => const SizedBox(width: 10),
+ itemBuilder: (context, index) {
+ final place = places[index];
+ return _PlacePhotoCard(place: place, onTap: () => onSelect(place));
+ },
+ ),
+ );
+ }
+}
+
+class _PlacePhotoCard extends StatelessWidget {
+ const _PlacePhotoCard({required this.place, required this.onTap});
+
+ final PlaceRecommendation place;
+ final VoidCallback onTap;
+
+ @override
+ Widget build(BuildContext context) {
+ return GestureDetector(
+ onTap: onTap,
+ child: Container(
+ width: 150,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(color: Colors.white),
+ boxShadow: const [
+ BoxShadow(
+ color: Color(0x33000000),
+ blurRadius: 16,
+ offset: Offset(0, 8),
+ ),
+ ],
+ ),
+ clipBehavior: Clip.antiAlias,
+ child: Stack(
+ fit: StackFit.expand,
+ children: [
+ PageView.builder(
+ itemCount: place.photoUrls.length,
+ itemBuilder: (context, index) {
+ return Image.network(
+ place.photoUrls[index],
+ fit: BoxFit.cover,
+ errorBuilder: (_, _, _) => Container(
+ color: const Color(0xFFE0D8CA),
+ child: const Icon(Icons.place_outlined),
+ ),
+ );
+ },
+ ),
+ const DecoratedBox(
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ begin: Alignment.topCenter,
+ end: Alignment.bottomCenter,
+ colors: [Colors.transparent, Color(0xCC000000)],
+ ),
+ ),
+ ),
+ Positioned(
+ left: 10,
+ right: 10,
+ bottom: 12,
+ child: Text(
+ place.name,
+ maxLines: 2,
+ overflow: TextOverflow.ellipsis,
+ style: const TextStyle(
+ color: Colors.white,
+ fontWeight: FontWeight.w900,
+ height: 1.05,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class AddExperienceFlow extends ConsumerStatefulWidget {
+ const AddExperienceFlow({super.key, required this.coordinate});
+
+ final LatLng coordinate;
+
+ @override
+ ConsumerState createState() => _AddExperienceFlowState();
+}
+
+class _AddExperienceFlowState extends ConsumerState {
+ Timer? _timer;
+ var _step = 0;
+ var _seconds = 0;
+ var _recording = false;
+ _GooglePlaceStub? _selectedGooglePlace;
+
+ static const _nearbyPlaces = [
+ _GooglePlaceStub(
+ name: 'Secret Garden',
+ area: '120 m',
+ coordinate: LatLng(10.7752, 106.7009),
+ ),
+ _GooglePlaceStub(
+ name: 'The Workshop',
+ area: '210 m',
+ coordinate: LatLng(10.7740, 106.7042),
+ ),
+ _GooglePlaceStub(
+ name: 'L\'Usine',
+ area: '360 m',
+ coordinate: LatLng(10.7755, 106.7038),
+ ),
+ _GooglePlaceStub(
+ name: 'Oc Dao',
+ area: '780 m',
+ coordinate: LatLng(10.7607, 106.6898),
+ ),
+ ];
+
+ @override
+ void dispose() {
+ _timer?.cancel();
+ super.dispose();
+ }
+
+ void _toggleRecording() {
+ setState(() => _recording = !_recording);
+ if (_recording) {
+ _timer = Timer.periodic(const Duration(seconds: 1), (_) {
+ setState(() => _seconds += 1);
+ ref
+ .read(placeControllerProvider.notifier)
+ .setReviewDuration(Duration(seconds: _seconds));
+ });
+ } else {
+ _timer?.cancel();
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final controller = ref.read(placeControllerProvider.notifier);
+ final time =
+ '${(_seconds ~/ 60).toString().padLeft(2, '0')}:'
+ '${(_seconds % 60).toString().padLeft(2, '0')}';
+
+ final content = switch (_step) {
+ 0 => _IntroStep(onNext: () => setState(() => _step = 1)),
+ 1 => _PlaceStep(
+ places: _nearbyPlaces,
+ onSelect: (place) {
+ setState(() {
+ _selectedGooglePlace = place;
+ _step = 2;
+ });
+ controller.setReviewPlace(place.name);
+ },
+ ),
+ _ => _VoiceStep(
+ place: _selectedGooglePlace,
+ seconds: _seconds,
+ time: time,
+ isRecording: _recording,
+ canContinue: _seconds >= 60,
+ onToggleRecording: _toggleRecording,
+ onNext: () {
+ final coordinate =
+ _selectedGooglePlace?.coordinate ?? widget.coordinate;
+ controller.setReviewPlace(_selectedGooglePlace?.name ?? '');
+ controller.analyzeVoiceReview();
+ controller.publishReview(coordinate: coordinate);
+ Navigator.of(context).pop();
+ },
+ ),
+ };
+
+ return Scaffold(
+ backgroundColor: const Color(0xFFF7F3EA),
+ body: SafeArea(
+ child: Padding(
+ padding: EdgeInsets.fromLTRB(
+ 16,
+ 10,
+ 16,
+ MediaQuery.viewInsetsOf(context).bottom + 18,
+ ),
+ child: Column(
+ children: [
+ _StoryProgress(
+ step: _step,
+ total: 3,
+ onClose: () => Navigator.of(context).pop(),
+ ),
+ const SizedBox(height: 18),
+ Expanded(
+ child: AnimatedSwitcher(
+ duration: const Duration(milliseconds: 220),
+ child: KeyedSubtree(key: ValueKey(_step), child: content),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class _IntroStep extends StatelessWidget {
+ const _IntroStep({required this.onNext});
+
+ final VoidCallback onNext;
+
+ @override
+ Widget build(BuildContext context) {
+ return _StepLayout(
+ body: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Container(
+ width: 70,
+ height: 70,
+ decoration: BoxDecoration(
+ color: const Color(0xFFE11D48),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Icon(Icons.graphic_eq, color: Colors.white, size: 38),
+ ),
+ const SizedBox(height: 22),
+ Text(
+ 'Расскажи про место голосом',
+ style: Theme.of(context).textTheme.headlineSmall?.copyWith(
+ fontWeight: FontWeight.w900,
+ letterSpacing: 0,
+ ),
+ ),
+ const SizedBox(height: 10),
+ const Text(
+ 'Поделись ощущением, а не оценкой. Мы разберем голос через AI и удалим аудио после обработки.',
+ ),
+ ],
+ ),
+ action: FilledButton(onPressed: onNext, child: const Text('Далее')),
+ );
+ }
+}
+
+class _PlaceStep extends StatelessWidget {
+ const _PlaceStep({required this.places, required this.onSelect});
+
+ final List<_GooglePlaceStub> places;
+ final ValueChanged<_GooglePlaceStub> onSelect;
+
+ @override
+ Widget build(BuildContext context) {
+ return _StepLayout(
+ body: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'Выбери место рядом',
+ style: Theme.of(context).textTheme.headlineSmall?.copyWith(
+ fontWeight: FontWeight.w900,
+ letterSpacing: 0,
+ ),
+ ),
+ const SizedBox(height: 6),
+ const Text('Покажем Google Places по твоей геолокации.'),
+ const SizedBox(height: 16),
+ Expanded(
+ child: ListView.separated(
+ itemCount: places.length,
+ separatorBuilder: (_, _) => const SizedBox(height: 10),
+ itemBuilder: (context, index) {
+ final place = places[index];
+ return ListTile(
+ onTap: () => onSelect(place),
+ contentPadding: const EdgeInsets.symmetric(
+ horizontal: 14,
+ vertical: 8,
+ ),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(8),
+ ),
+ tileColor: const Color(0xFFFFFBF5),
+ leading: const Icon(Icons.place_outlined),
+ title: Text(
+ place.name,
+ style: const TextStyle(fontWeight: FontWeight.w800),
+ ),
+ trailing: Text(place.area),
+ );
+ },
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class _VoiceStep extends StatelessWidget {
+ const _VoiceStep({
+ required this.place,
+ required this.seconds,
+ required this.time,
+ required this.isRecording,
+ required this.canContinue,
+ required this.onToggleRecording,
+ required this.onNext,
+ });
+
+ final _GooglePlaceStub? place;
+ final int seconds;
+ final String time;
+ final bool isRecording;
+ final bool canContinue;
+ final VoidCallback onToggleRecording;
+ final VoidCallback onNext;
+
+ @override
+ Widget build(BuildContext context) {
+ return _StepLayout(
+ body: Column(
+ children: [
+ const Spacer(),
+ Text(
+ place?.name ?? 'Место',
+ textAlign: TextAlign.center,
+ style: Theme.of(
+ context,
+ ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w900),
+ ),
+ const SizedBox(height: 8),
+ const Text('Минимум 60 секунд', textAlign: TextAlign.center),
+ const SizedBox(height: 26),
+ SizedBox(
+ width: 132,
+ height: 132,
+ child: FilledButton(
+ onPressed: onToggleRecording,
+ style: FilledButton.styleFrom(shape: const CircleBorder()),
+ child: Icon(isRecording ? Icons.stop : Icons.mic, size: 54),
+ ),
+ ),
+ const SizedBox(height: 22),
+ Text(
+ time,
+ style: Theme.of(
+ context,
+ ).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.w900),
+ ),
+ const SizedBox(height: 12),
+ LinearProgressIndicator(value: (seconds / 60).clamp(0.0, 1.0)),
+ const Spacer(),
+ ],
+ ),
+ action: FilledButton(
+ onPressed: canContinue ? onNext : null,
+ child: const Text('Далее'),
+ ),
+ );
+ }
+}
+
+class _StepLayout extends StatelessWidget {
+ const _StepLayout({required this.body, this.action});
+
+ final Widget body;
+ final Widget? action;
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ children: [
+ Expanded(child: body),
+ if (action != null) SizedBox(width: double.infinity, child: action),
+ ],
+ );
+ }
+}
+
+class _StoryProgress extends StatelessWidget {
+ const _StoryProgress({
+ required this.step,
+ required this.total,
+ required this.onClose,
+ });
+
+ final int step;
+ final int total;
+ final VoidCallback onClose;
+
+ @override
+ Widget build(BuildContext context) {
+ return Row(
+ children: [
+ Expanded(
+ child: Row(
+ children: [
+ for (var index = 0; index < total; index++) ...[
+ Expanded(
+ child: AnimatedContainer(
+ duration: const Duration(milliseconds: 180),
+ height: 5,
+ decoration: BoxDecoration(
+ color: index <= step
+ ? Theme.of(context).colorScheme.primary
+ : const Color(0xFFE0D8CA),
+ borderRadius: BorderRadius.circular(99),
+ ),
+ ),
+ ),
+ if (index != total - 1) const SizedBox(width: 6),
+ ],
+ ],
+ ),
+ ),
+ const SizedBox(width: 10),
+ IconButton(
+ onPressed: onClose,
+ icon: const Icon(Icons.close),
+ tooltip: 'Закрыть',
+ ),
+ ],
+ );
+ }
+}
+
+class _GooglePlaceStub {
+ const _GooglePlaceStub({
+ required this.name,
+ required this.area,
+ required this.coordinate,
+ });
+
+ final String name;
+ final String area;
+ final LatLng coordinate;
+}
diff --git a/lib/state/experience_controller.dart b/lib/state/experience_controller.dart
new file mode 100644
index 0000000..f809751
--- /dev/null
+++ b/lib/state/experience_controller.dart
@@ -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.new,
+ );
+
+class ExperienceState {
+ const ExperienceState({
+ required this.experiences,
+ required this.selectedExperienceId,
+ required this.filterEmotion,
+ required this.profile,
+ });
+
+ final List experiences;
+ final String? selectedExperienceId;
+ final ExperienceEmotion? filterEmotion;
+ final ExperienceAuthor profile;
+
+ List 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? 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 {
+ @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 _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: 'месяц назад',
+ ),
+ ];
+ }
+}
diff --git a/lib/state/place_controller.dart b/lib/state/place_controller.dart
new file mode 100644
index 0000000..b85fd62
--- /dev/null
+++ b/lib/state/place_controller.dart
@@ -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.new,
+);
+
+class PlaceState {
+ const PlaceState({
+ required this.intent,
+ required this.places,
+ required this.selectedPlaceId,
+ required this.reviewDraft,
+ });
+
+ final UserIntent intent;
+ final List places;
+ final String? selectedPlaceId;
+ final VoiceReviewDraft reviewDraft;
+
+ List 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? 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 {
+ @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 _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,
+ },
+ ),
+ ];
+ }
+}
diff --git a/macos/.gitignore b/macos/.gitignore
new file mode 100644
index 0000000..746adbb
--- /dev/null
+++ b/macos/.gitignore
@@ -0,0 +1,7 @@
+# Flutter-related
+**/Flutter/ephemeral/
+**/Pods/
+
+# Xcode-related
+**/dgph
+**/xcuserdata/
diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig
new file mode 100644
index 0000000..c2efd0b
--- /dev/null
+++ b/macos/Flutter/Flutter-Debug.xcconfig
@@ -0,0 +1 @@
+#include "ephemeral/Flutter-Generated.xcconfig"
diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig
new file mode 100644
index 0000000..c2efd0b
--- /dev/null
+++ b/macos/Flutter/Flutter-Release.xcconfig
@@ -0,0 +1 @@
+#include "ephemeral/Flutter-Generated.xcconfig"
diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift
new file mode 100644
index 0000000..cccf817
--- /dev/null
+++ b/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -0,0 +1,10 @@
+//
+// Generated file. Do not edit.
+//
+
+import FlutterMacOS
+import Foundation
+
+
+func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
+}
diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..913e187
--- /dev/null
+++ b/macos/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,705 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 54;
+ objects = {
+
+/* Begin PBXAggregateTarget section */
+ 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {
+ isa = PBXAggregateTarget;
+ buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */;
+ buildPhases = (
+ 33CC111E2044C6BF0003C045 /* ShellScript */,
+ );
+ dependencies = (
+ );
+ name = "Flutter Assemble";
+ productName = FLX;
+ };
+/* End PBXAggregateTarget section */
+
+/* Begin PBXBuildFile section */
+ 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
+ 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
+ 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
+ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
+ 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
+ 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 33CC10EC2044A3C60003C045;
+ remoteInfo = Runner;
+ };
+ 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 33CC111A2044C6BA0003C045;
+ remoteInfo = FLX;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 33CC110E2044A8840003C045 /* Bundle Framework */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ );
+ name = "Bundle Framework";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; };
+ 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; };
+ 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; };
+ 33CC10ED2044A3C60003C045 /* mapflow.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "mapflow.app"; sourceTree = BUILT_PRODUCTS_DIR; };
+ 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; };
+ 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; };
+ 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; };
+ 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; };
+ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; };
+ 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; };
+ 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; };
+ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; };
+ 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; };
+ 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; };
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; };
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 331C80D2294CF70F00263BE5 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 33CC10EA2044A3C60003C045 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 331C80D6294CF71000263BE5 /* RunnerTests */ = {
+ isa = PBXGroup;
+ children = (
+ 331C80D7294CF71000263BE5 /* RunnerTests.swift */,
+ );
+ path = RunnerTests;
+ sourceTree = "";
+ };
+ 33BA886A226E78AF003329D5 /* Configs */ = {
+ isa = PBXGroup;
+ children = (
+ 33E5194F232828860026EE4D /* AppInfo.xcconfig */,
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */,
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+ 333000ED22D3DE5D00554162 /* Warnings.xcconfig */,
+ );
+ path = Configs;
+ sourceTree = "";
+ };
+ 33CC10E42044A3C60003C045 = {
+ isa = PBXGroup;
+ children = (
+ 33FAB671232836740065AC1E /* Runner */,
+ 33CEB47122A05771004F2AC0 /* Flutter */,
+ 331C80D6294CF71000263BE5 /* RunnerTests */,
+ 33CC10EE2044A3C60003C045 /* Products */,
+ D73912EC22F37F3D000D13A0 /* Frameworks */,
+ );
+ sourceTree = "";
+ };
+ 33CC10EE2044A3C60003C045 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 33CC10ED2044A3C60003C045 /* mapflow.app */,
+ 331C80D5294CF71000263BE5 /* RunnerTests.xctest */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 33CC11242044D66E0003C045 /* Resources */ = {
+ isa = PBXGroup;
+ children = (
+ 33CC10F22044A3C60003C045 /* Assets.xcassets */,
+ 33CC10F42044A3C60003C045 /* MainMenu.xib */,
+ 33CC10F72044A3C60003C045 /* Info.plist */,
+ );
+ name = Resources;
+ path = ..;
+ sourceTree = "";
+ };
+ 33CEB47122A05771004F2AC0 /* Flutter */ = {
+ isa = PBXGroup;
+ children = (
+ 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
+ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
+ 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
+ 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,
+ );
+ path = Flutter;
+ sourceTree = "";
+ };
+ 33FAB671232836740065AC1E /* Runner */ = {
+ isa = PBXGroup;
+ children = (
+ 33CC10F02044A3C60003C045 /* AppDelegate.swift */,
+ 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
+ 33E51913231747F40026EE4D /* DebugProfile.entitlements */,
+ 33E51914231749380026EE4D /* Release.entitlements */,
+ 33CC11242044D66E0003C045 /* Resources */,
+ 33BA886A226E78AF003329D5 /* Configs */,
+ );
+ path = Runner;
+ sourceTree = "";
+ };
+ D73912EC22F37F3D000D13A0 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 331C80D4294CF70F00263BE5 /* RunnerTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
+ buildPhases = (
+ 331C80D1294CF70F00263BE5 /* Sources */,
+ 331C80D2294CF70F00263BE5 /* Frameworks */,
+ 331C80D3294CF70F00263BE5 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 331C80DA294CF71000263BE5 /* PBXTargetDependency */,
+ );
+ name = RunnerTests;
+ productName = RunnerTests;
+ productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+ 33CC10EC2044A3C60003C045 /* Runner */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
+ buildPhases = (
+ 33CC10E92044A3C60003C045 /* Sources */,
+ 33CC10EA2044A3C60003C045 /* Frameworks */,
+ 33CC10EB2044A3C60003C045 /* Resources */,
+ 33CC110E2044A8840003C045 /* Bundle Framework */,
+ 3399D490228B24CF009A79C7 /* ShellScript */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 33CC11202044C79F0003C045 /* PBXTargetDependency */,
+ );
+ name = Runner;
+ productName = Runner;
+ productReference = 33CC10ED2044A3C60003C045 /* mapflow.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 33CC10E52044A3C60003C045 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = YES;
+ LastSwiftUpdateCheck = 0920;
+ LastUpgradeCheck = 1510;
+ ORGANIZATIONNAME = "";
+ TargetAttributes = {
+ 331C80D4294CF70F00263BE5 = {
+ CreatedOnToolsVersion = 14.0;
+ TestTargetID = 33CC10EC2044A3C60003C045;
+ };
+ 33CC10EC2044A3C60003C045 = {
+ CreatedOnToolsVersion = 9.2;
+ LastSwiftMigration = 1100;
+ ProvisioningStyle = Automatic;
+ SystemCapabilities = {
+ com.apple.Sandbox = {
+ enabled = 1;
+ };
+ };
+ };
+ 33CC111A2044C6BA0003C045 = {
+ CreatedOnToolsVersion = 9.2;
+ ProvisioningStyle = Manual;
+ };
+ };
+ };
+ buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */;
+ compatibilityVersion = "Xcode 9.3";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 33CC10E42044A3C60003C045;
+ productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 33CC10EC2044A3C60003C045 /* Runner */,
+ 331C80D4294CF70F00263BE5 /* RunnerTests */,
+ 33CC111A2044C6BA0003C045 /* Flutter Assemble */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 331C80D3294CF70F00263BE5 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 33CC10EB2044A3C60003C045 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
+ 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 3399D490228B24CF009A79C7 /* ShellScript */ = {
+ isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ );
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n";
+ };
+ 33CC111E2044C6BF0003C045 /* ShellScript */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ Flutter/ephemeral/FlutterInputs.xcfilelist,
+ );
+ inputPaths = (
+ Flutter/ephemeral/tripwire,
+ );
+ outputFileListPaths = (
+ Flutter/ephemeral/FlutterOutputs.xcfilelist,
+ );
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 331C80D1294CF70F00263BE5 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 33CC10E92044A3C60003C045 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,
+ 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,
+ 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 33CC10EC2044A3C60003C045 /* Runner */;
+ targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */;
+ };
+ 33CC11202044C79F0003C045 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;
+ targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+ 33CC10F42044A3C60003C045 /* MainMenu.xib */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 33CC10F52044A3C60003C045 /* Base */,
+ );
+ name = MainMenu.xib;
+ path = Runner;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 331C80DB294CF71000263BE5 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.mapflow.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/mapflow.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/mapflow";
+ };
+ name = Debug;
+ };
+ 331C80DC294CF71000263BE5 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.mapflow.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/mapflow.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/mapflow";
+ };
+ name = Release;
+ };
+ 331C80DD294CF71000263BE5 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.mapflow.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/mapflow.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/mapflow";
+ };
+ name = Profile;
+ };
+ 338D0CE9231458BD00FA5F75 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CODE_SIGN_IDENTITY = "-";
+ COPY_PHASE_STRIP = NO;
+ DEAD_CODE_STRIPPING = YES;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.15;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = macosx;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ };
+ name = Profile;
+ };
+ 338D0CEA231458BD00FA5F75 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_VERSION = 5.0;
+ };
+ name = Profile;
+ };
+ 338D0CEB231458BD00FA5F75 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Manual;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Profile;
+ };
+ 33CC10F92044A3C60003C045 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CODE_SIGN_IDENTITY = "-";
+ COPY_PHASE_STRIP = NO;
+ DEAD_CODE_STRIPPING = YES;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.15;
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = macosx;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ 33CC10FA2044A3C60003C045 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CODE_SIGN_IDENTITY = "-";
+ COPY_PHASE_STRIP = NO;
+ DEAD_CODE_STRIPPING = YES;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.15;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = macosx;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ };
+ name = Release;
+ };
+ 33CC10FC2044A3C60003C045 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ };
+ name = Debug;
+ };
+ 33CC10FD2044A3C60003C045 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_VERSION = 5.0;
+ };
+ name = Release;
+ };
+ 33CC111C2044C6BA0003C045 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Manual;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Debug;
+ };
+ 33CC111D2044C6BA0003C045 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Automatic;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 331C80DB294CF71000263BE5 /* Debug */,
+ 331C80DC294CF71000263BE5 /* Release */,
+ 331C80DD294CF71000263BE5 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 33CC10F92044A3C60003C045 /* Debug */,
+ 33CC10FA2044A3C60003C045 /* Release */,
+ 338D0CE9231458BD00FA5F75 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 33CC10FC2044A3C60003C045 /* Debug */,
+ 33CC10FD2044A3C60003C045 /* Release */,
+ 338D0CEA231458BD00FA5F75 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 33CC111C2044C6BA0003C045 /* Debug */,
+ 33CC111D2044C6BA0003C045 /* Release */,
+ 338D0CEB231458BD00FA5F75 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 33CC10E52044A3C60003C045 /* Project object */;
+}
diff --git a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 0000000..c7c3a0c
--- /dev/null
+++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..1d526a1
--- /dev/null
+++ b/macos/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift
new file mode 100644
index 0000000..b3c1761
--- /dev/null
+++ b/macos/Runner/AppDelegate.swift
@@ -0,0 +1,13 @@
+import Cocoa
+import FlutterMacOS
+
+@main
+class AppDelegate: FlutterAppDelegate {
+ override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
+ return true
+ }
+
+ override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
+ return true
+ }
+}
diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..a2ec33f
--- /dev/null
+++ b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,68 @@
+{
+ "images" : [
+ {
+ "size" : "16x16",
+ "idiom" : "mac",
+ "filename" : "app_icon_16.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "16x16",
+ "idiom" : "mac",
+ "filename" : "app_icon_32.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "32x32",
+ "idiom" : "mac",
+ "filename" : "app_icon_32.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "32x32",
+ "idiom" : "mac",
+ "filename" : "app_icon_64.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "128x128",
+ "idiom" : "mac",
+ "filename" : "app_icon_128.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "128x128",
+ "idiom" : "mac",
+ "filename" : "app_icon_256.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "256x256",
+ "idiom" : "mac",
+ "filename" : "app_icon_256.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "256x256",
+ "idiom" : "mac",
+ "filename" : "app_icon_512.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "512x512",
+ "idiom" : "mac",
+ "filename" : "app_icon_512.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "512x512",
+ "idiom" : "mac",
+ "filename" : "app_icon_1024.png",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
new file mode 100644
index 0000000000000000000000000000000000000000..82b6f9d9a33e198f5747104729e1fcef999772a5
GIT binary patch
literal 102994
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