Compare commits
34 Commits
f9d6e4fa5b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e3d03d225 | ||
|
|
697f029ad2 | ||
|
|
0b1493a02e | ||
|
|
dfe2c52f8f | ||
|
|
abae8b905c | ||
|
|
0532f6aa88 | ||
|
|
34a197f786 | ||
|
|
1b6b40849e | ||
|
|
584e30624d | ||
|
|
21945b2335 | ||
|
|
fbf9104d2d | ||
|
|
cdf6a43d49 | ||
|
|
d3721e44e7 | ||
|
|
28e8cee6e6 | ||
|
|
29e856bbd8 | ||
|
|
a8b6aa6e02 | ||
|
|
5b2cd4158c | ||
|
|
04fa49737d | ||
|
|
729dd21b78 | ||
|
|
fcc2c26752 | ||
|
|
069dcab479 | ||
|
|
8fda6f554d | ||
|
|
73ed4c2614 | ||
|
|
2366587693 | ||
|
|
d7b419fea6 | ||
|
|
4a2e458a01 | ||
|
|
c9be8b5e75 | ||
|
|
f2277626f1 | ||
|
|
8c7e62d9e1 | ||
|
|
765219cc20 | ||
|
|
906c23366f | ||
|
|
2c9bcad0cc | ||
|
|
adc935b6cf | ||
|
|
6055a101e8 |
@@ -7,12 +7,11 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: build-host
|
||||
runs-on: builder
|
||||
env:
|
||||
SERVICE_NAME: flutter
|
||||
IMAGE_SHA: gitea.dsrptlab.com/mapflow/flutter:${{ github.sha }}
|
||||
IMAGE_LATEST: gitea.dsrptlab.com/mapflow/flutter:latest
|
||||
DOKPLOY_DEPLOY_WEBHOOK: http://sin.dsrptlab.com:3000/api/deploy/b7iCKmvlGMU-2HmI79fcC
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -23,28 +22,24 @@ jobs:
|
||||
auth="$(printf '%s:%s' "${{ secrets.REGISTRY_USERNAME }}" "${{ secrets.REGISTRY_TOKEN }}" | base64 | tr -d '\n')"
|
||||
printf '{"auths":{"gitea.dsrptlab.com":{"auth":"%s"}}}\n' "$auth" > ~/.docker/config.json
|
||||
|
||||
- name: Free Docker build space
|
||||
run: |
|
||||
set -euo pipefail
|
||||
docker buildx prune --builder builder --all --max-used-space 40gb -f || true
|
||||
|
||||
- name: Build and push image
|
||||
run: |
|
||||
set -euo pipefail
|
||||
builder="builder"
|
||||
if ! docker buildx inspect "$builder" >/dev/null 2>&1; then
|
||||
docker buildx create --name "$builder" --driver docker-container --buildkitd-config /etc/buildkit/buildkitd.toml
|
||||
fi
|
||||
docker buildx use "$builder"
|
||||
docker buildx inspect --bootstrap
|
||||
docker buildx build \
|
||||
--push \
|
||||
--tag "$IMAGE_SHA" \
|
||||
--tag "$IMAGE_LATEST" \
|
||||
--build-arg MAPBOX_ACCESS_TOKEN="${{ secrets.MAPBOX_ACCESS_TOKEN }}" \
|
||||
--build-arg MAPBOX_STYLE="mapbox/streets-v12" \
|
||||
--build-arg TELEGRAM_BOT_USERNAME="carfteebot" \
|
||||
.
|
||||
for attempt in 1 2 3; do
|
||||
if docker buildx build \
|
||||
--push \
|
||||
--provenance=false \
|
||||
--tag "$IMAGE_SHA" \
|
||||
--tag "$IMAGE_LATEST" \
|
||||
--build-arg MAPBOX_ACCESS_TOKEN="${{ secrets.MAPBOX_ACCESS_TOKEN }}" \
|
||||
--build-arg MAPBOX_STYLE="mapbox/streets-v12" \
|
||||
--build-arg TELEGRAM_BOT_USERNAME="carfteebot" \
|
||||
.; then
|
||||
exit 0
|
||||
fi
|
||||
sleep "$((attempt * 10))"
|
||||
done
|
||||
exit 1
|
||||
|
||||
- name: Skip stale deployment
|
||||
run: |
|
||||
@@ -56,23 +51,18 @@ jobs:
|
||||
echo "A newer main commit exists: $latest_sha. Skipping deploy for ${GITHUB_SHA}."
|
||||
fi
|
||||
|
||||
- name: Trigger Dokploy webhook
|
||||
- name: Trigger Dokploy deploy webhook
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[ -f .deploy-current ] || exit 0
|
||||
payload=$(cat <<JSON
|
||||
{"ref":"refs/heads/main","after":"$GITHUB_SHA","commits":[{"id":"$GITHUB_SHA","message":"$SERVICE_NAME #${GITHUB_RUN_NUMBER:-0} ${GITHUB_SHA:0:7}"}]}
|
||||
{"ref":"refs/heads/main","after":"$GITHUB_SHA","commits":[{"id":"$GITHUB_SHA","message":"$SERVICE_NAME #${GITHUB_RUN_NUMBER:-0} ${GITHUB_SHA:0:7}","modified":["Dockerfile"]}]}
|
||||
JSON
|
||||
)
|
||||
response_file="$(mktemp)"
|
||||
status_code="$(curl -sS -o "$response_file" -w "%{http_code}" -X POST "$DOKPLOY_DEPLOY_WEBHOOK" \
|
||||
status_code="$(curl -sS -o "$response_file" -w "%{http_code}" -X POST "${{ secrets.DOKPLOY_DEPLOY_WEBHOOK }}" \
|
||||
-H "x-gitea-event: push" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload")"
|
||||
cat "$response_file"
|
||||
[ "$status_code" = "200" ]
|
||||
|
||||
- name: Prune shared BuildKit cache
|
||||
run: |
|
||||
set -euo pipefail
|
||||
docker buildx prune --builder builder --all --max-used-space 40gb -f
|
||||
|
||||
@@ -51,6 +51,7 @@ class MapflowApi {
|
||||
lastName
|
||||
photoUrl
|
||||
languageCode
|
||||
isAdmin
|
||||
}
|
||||
}
|
||||
''');
|
||||
@@ -73,6 +74,7 @@ class MapflowApi {
|
||||
lastName
|
||||
photoUrl
|
||||
languageCode
|
||||
isAdmin
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,6 +99,7 @@ class MapflowApi {
|
||||
lastName
|
||||
photoUrl
|
||||
languageCode
|
||||
isAdmin
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -126,31 +129,29 @@ class MapflowApi {
|
||||
);
|
||||
}
|
||||
|
||||
Future<TelegramBotLoginStatus> fetchTelegramBotLoginStatus(
|
||||
String token,
|
||||
) async {
|
||||
Future<TelegramBotLoginSession> completeTelegramBotLogin(String token) async {
|
||||
final data = await _graphql(
|
||||
'''
|
||||
query TelegramBotLoginStatus(\$token: String!) {
|
||||
telegramBotLoginStatus(token: \$token) {
|
||||
status
|
||||
mutation CompleteTelegramBotLogin(\$token: String!) {
|
||||
completeTelegramBotLogin(token: \$token) {
|
||||
sessionToken
|
||||
user {
|
||||
id
|
||||
telegramId
|
||||
username
|
||||
firstName
|
||||
lastName
|
||||
photoUrl
|
||||
languageCode
|
||||
}
|
||||
lastName
|
||||
photoUrl
|
||||
languageCode
|
||||
isAdmin
|
||||
}
|
||||
}
|
||||
}
|
||||
''',
|
||||
variables: {'token': token},
|
||||
);
|
||||
return TelegramBotLoginStatus.fromJson(
|
||||
data['telegramBotLoginStatus'] as Map<String, dynamic>,
|
||||
return TelegramBotLoginSession.fromJson(
|
||||
data['completeTelegramBotLogin'] as Map<String, dynamic>,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -163,6 +164,8 @@ class MapflowApi {
|
||||
name
|
||||
latitude
|
||||
longitude
|
||||
googlePrimaryType
|
||||
googleTypes
|
||||
experiences {
|
||||
id
|
||||
status
|
||||
@@ -187,6 +190,10 @@ class MapflowApi {
|
||||
(place['longitude'] as num).toDouble(),
|
||||
),
|
||||
traits: _traitsFromExperiences(place['experiences'] as List<dynamic>),
|
||||
googlePrimaryType: place['googlePrimaryType'] as String?,
|
||||
googleTypes: (place['googleTypes'] as List<dynamic>)
|
||||
.map((type) => type as String)
|
||||
.toList(),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
@@ -204,6 +211,8 @@ class MapflowApi {
|
||||
name
|
||||
latitude
|
||||
longitude
|
||||
googlePrimaryType
|
||||
googleTypes
|
||||
experiences {
|
||||
id
|
||||
status
|
||||
@@ -236,6 +245,10 @@ class MapflowApi {
|
||||
(place['longitude'] as num).toDouble(),
|
||||
),
|
||||
traits: _traitsFromExperiences(place['experiences'] as List<dynamic>),
|
||||
googlePrimaryType: place['googlePrimaryType'] as String?,
|
||||
googleTypes: (place['googleTypes'] as List<dynamic>)
|
||||
.map((type) => type as String)
|
||||
.toList(),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
@@ -246,6 +259,8 @@ class MapflowApi {
|
||||
required LatLng coordinate,
|
||||
required int durationSeconds,
|
||||
required String audioObjectKey,
|
||||
required String audioContentBase64,
|
||||
required String audioMimeType,
|
||||
}) async {
|
||||
if (!hasTelegramAuth) {
|
||||
throw StateError('Telegram authorization is required.');
|
||||
@@ -267,11 +282,43 @@ class MapflowApi {
|
||||
'longitude': coordinate.longitude,
|
||||
'durationSeconds': durationSeconds,
|
||||
'audioObjectKey': audioObjectKey,
|
||||
'audioContentBase64': audioContentBase64,
|
||||
'audioMimeType': audioMimeType,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<VoiceExperienceDebug>> fetchVoiceExperiences() async {
|
||||
final data = await _graphql('''
|
||||
query VoiceExperiences {
|
||||
voiceExperiences {
|
||||
id
|
||||
status
|
||||
durationSeconds
|
||||
transcript
|
||||
analysis
|
||||
createdAt
|
||||
place {
|
||||
name
|
||||
}
|
||||
user {
|
||||
telegramId
|
||||
username
|
||||
firstName
|
||||
}
|
||||
}
|
||||
}
|
||||
''');
|
||||
|
||||
final experiences = data['voiceExperiences'] as List<dynamic>;
|
||||
return experiences
|
||||
.map(
|
||||
(item) => VoiceExperienceDebug.fromJson(item as Map<String, dynamic>),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _graphql(
|
||||
String query, {
|
||||
Map<String, dynamic>? variables,
|
||||
|
||||
@@ -4,17 +4,13 @@ String telegramLoginData() => '';
|
||||
|
||||
String mapflowSessionToken() => '';
|
||||
|
||||
String pendingTelegramLoginToken() => '';
|
||||
|
||||
String telegramLoginTokenFromUrl() => '';
|
||||
|
||||
void saveMapflowSessionToken(String token) {}
|
||||
|
||||
void clearMapflowSession() {}
|
||||
|
||||
void savePendingTelegramLoginToken(String token) {}
|
||||
|
||||
void clearPendingTelegramLoginToken() {}
|
||||
void configureTelegramWebApp() {}
|
||||
|
||||
void openExternalUrl(String url) {}
|
||||
|
||||
|
||||
@@ -4,15 +4,29 @@ import 'package:web/web.dart' as web;
|
||||
|
||||
const telegramLoginStorageKey = 'mapflow.telegramLoginData';
|
||||
const mapflowSessionStorageKey = 'mapflow.sessionToken';
|
||||
const pendingTelegramLoginStorageKey = 'mapflow.pendingTelegramLoginToken';
|
||||
|
||||
@JS('window.Telegram.WebApp.initData')
|
||||
external JSString? get _telegramInitData;
|
||||
@JS('Telegram')
|
||||
external _Telegram? get _telegram;
|
||||
|
||||
extension type _Telegram(JSObject _) implements JSObject {
|
||||
@JS('WebApp')
|
||||
external _TelegramWebApp? get webApp;
|
||||
}
|
||||
|
||||
extension type _TelegramWebApp(JSObject _) implements JSObject {
|
||||
external JSString? get initData;
|
||||
external void ready();
|
||||
external void expand();
|
||||
external bool isVersionAtLeast(String version);
|
||||
external void disableVerticalSwipes();
|
||||
}
|
||||
|
||||
@JS('JSON.stringify')
|
||||
external JSString _jsonStringify(JSAny? value);
|
||||
|
||||
String telegramInitData() => _telegramInitData?.toDart ?? '';
|
||||
_TelegramWebApp? get _webApp => _telegram?.webApp;
|
||||
|
||||
String telegramInitData() => _webApp?.initData?.toDart ?? '';
|
||||
|
||||
String telegramLoginData() =>
|
||||
web.window.localStorage.getItem(telegramLoginStorageKey) ?? '';
|
||||
@@ -20,9 +34,6 @@ String telegramLoginData() =>
|
||||
String mapflowSessionToken() =>
|
||||
web.window.localStorage.getItem(mapflowSessionStorageKey) ?? '';
|
||||
|
||||
String pendingTelegramLoginToken() =>
|
||||
web.window.localStorage.getItem(pendingTelegramLoginStorageKey) ?? '';
|
||||
|
||||
String telegramLoginTokenFromUrl() {
|
||||
final uri = Uri.parse(web.window.location.href);
|
||||
return uri.queryParameters['telegram_login'] ?? '';
|
||||
@@ -40,19 +51,23 @@ void saveMapflowSessionToken(String token) {
|
||||
void clearMapflowSession() {
|
||||
web.window.localStorage.removeItem(mapflowSessionStorageKey);
|
||||
web.window.localStorage.removeItem(telegramLoginStorageKey);
|
||||
clearPendingTelegramLoginToken();
|
||||
}
|
||||
|
||||
void savePendingTelegramLoginToken(String token) {
|
||||
web.window.localStorage.setItem(pendingTelegramLoginStorageKey, token);
|
||||
}
|
||||
void configureTelegramWebApp() {
|
||||
final webApp = _webApp;
|
||||
if (webApp == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
void clearPendingTelegramLoginToken() {
|
||||
web.window.localStorage.removeItem(pendingTelegramLoginStorageKey);
|
||||
webApp.ready();
|
||||
webApp.expand();
|
||||
if (webApp.isVersionAtLeast('7.7')) {
|
||||
webApp.disableVerticalSwipes();
|
||||
}
|
||||
}
|
||||
|
||||
void openExternalUrl(String url) {
|
||||
web.window.open(url, '_blank', 'noopener,noreferrer');
|
||||
web.window.location.assign(url);
|
||||
}
|
||||
|
||||
void reloadApp() {
|
||||
|
||||
@@ -3,10 +3,12 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'auth/telegram_session.dart' as telegram_session;
|
||||
import 'screens/mapflow_shell.dart';
|
||||
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
telegram_session.configureTelegramWebApp();
|
||||
runApp(const ProviderScope(child: MapflowApp()));
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ class AppUser {
|
||||
required this.lastName,
|
||||
required this.photoUrl,
|
||||
required this.languageCode,
|
||||
required this.isAdmin,
|
||||
});
|
||||
|
||||
factory AppUser.fromJson(Map<String, dynamic> json) {
|
||||
@@ -21,6 +22,7 @@ class AppUser {
|
||||
lastName: json['lastName'] as String?,
|
||||
photoUrl: json['photoUrl'] as String?,
|
||||
languageCode: json['languageCode'] as String?,
|
||||
isAdmin: json['isAdmin'] as bool? ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,6 +33,51 @@ class AppUser {
|
||||
final String? lastName;
|
||||
final String? photoUrl;
|
||||
final String? languageCode;
|
||||
final bool isAdmin;
|
||||
}
|
||||
|
||||
class VoiceExperienceDebug {
|
||||
const VoiceExperienceDebug({
|
||||
required this.id,
|
||||
required this.placeName,
|
||||
required this.userName,
|
||||
required this.status,
|
||||
required this.durationSeconds,
|
||||
required this.transcript,
|
||||
required this.analysis,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory VoiceExperienceDebug.fromJson(Map<String, dynamic> json) {
|
||||
final place = json['place'] as Map<String, dynamic>;
|
||||
final user = json['user'] as Map<String, dynamic>?;
|
||||
final firstName = user?['firstName'] as String?;
|
||||
final username = user?['username'] as String?;
|
||||
final telegramId = user?['telegramId'] as String?;
|
||||
return VoiceExperienceDebug(
|
||||
id: json['id'] as String,
|
||||
placeName: place['name'] as String,
|
||||
userName: firstName?.trim().isNotEmpty == true
|
||||
? firstName!
|
||||
: username?.trim().isNotEmpty == true
|
||||
? '@$username'
|
||||
: telegramId ?? '',
|
||||
status: json['status'] as String,
|
||||
durationSeconds: json['durationSeconds'] as int,
|
||||
transcript: json['transcript'] as String?,
|
||||
analysis: json['analysis'] as Map<String, dynamic>?,
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
final String id;
|
||||
final String placeName;
|
||||
final String userName;
|
||||
final String status;
|
||||
final int durationSeconds;
|
||||
final String? transcript;
|
||||
final Map<String, dynamic>? analysis;
|
||||
final DateTime createdAt;
|
||||
}
|
||||
|
||||
enum PlaceTrait {
|
||||
@@ -90,6 +137,8 @@ class PlaceRecommendation {
|
||||
required this.photoUrls,
|
||||
required this.coordinate,
|
||||
required this.traits,
|
||||
required this.googlePrimaryType,
|
||||
required this.googleTypes,
|
||||
});
|
||||
|
||||
final String id;
|
||||
@@ -99,6 +148,8 @@ class PlaceRecommendation {
|
||||
final List<String> photoUrls;
|
||||
final LatLng coordinate;
|
||||
final Set<PlaceTrait> traits;
|
||||
final String? googlePrimaryType;
|
||||
final List<String> googleTypes;
|
||||
|
||||
String get coverPhotoUrl => photoUrls.first;
|
||||
}
|
||||
@@ -153,26 +204,19 @@ class TelegramBotLogin {
|
||||
final DateTime expiresAt;
|
||||
}
|
||||
|
||||
class TelegramBotLoginStatus {
|
||||
const TelegramBotLoginStatus({
|
||||
required this.status,
|
||||
class TelegramBotLoginSession {
|
||||
const TelegramBotLoginSession({
|
||||
required this.sessionToken,
|
||||
required this.user,
|
||||
});
|
||||
|
||||
factory TelegramBotLoginStatus.fromJson(Map<String, dynamic> json) {
|
||||
final user = json['user'];
|
||||
return TelegramBotLoginStatus(
|
||||
status: json['status'] as String,
|
||||
sessionToken: json['sessionToken'] as String?,
|
||||
user: user is Map<String, dynamic> ? AppUser.fromJson(user) : null,
|
||||
factory TelegramBotLoginSession.fromJson(Map<String, dynamic> json) {
|
||||
return TelegramBotLoginSession(
|
||||
sessionToken: json['sessionToken'] as String,
|
||||
user: AppUser.fromJson(json['user'] as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
|
||||
final String status;
|
||||
final String? sessionToken;
|
||||
final AppUser? user;
|
||||
|
||||
bool get isConfirmed => status == 'CONFIRMED' && sessionToken != null;
|
||||
bool get isExpired => status == 'EXPIRED';
|
||||
final String sessionToken;
|
||||
final AppUser user;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,8 @@ import '../models/place_models.dart';
|
||||
final placeControllerProvider =
|
||||
AsyncNotifierProvider<PlaceController, PlaceState>(PlaceController.new);
|
||||
|
||||
const _unset = Object();
|
||||
|
||||
class PlaceState {
|
||||
const PlaceState({
|
||||
required this.selectedTrait,
|
||||
@@ -20,9 +22,8 @@ class PlaceState {
|
||||
});
|
||||
|
||||
static final _distance = Distance();
|
||||
static const nearbyRecommendationRadiusMeters = 500.0;
|
||||
|
||||
final PlaceTrait selectedTrait;
|
||||
final PlaceTrait? selectedTrait;
|
||||
final List<PlaceRecommendation> places;
|
||||
final String? selectedPlaceId;
|
||||
final AppUser? currentUser;
|
||||
@@ -31,55 +32,52 @@ class PlaceState {
|
||||
final VoiceReviewDraft reviewDraft;
|
||||
|
||||
List<PlaceRecommendation> get recommendations {
|
||||
final selected = selectedTrait;
|
||||
final matchingPlaces = selected == null
|
||||
? places
|
||||
: places.where((place) => place.traits.contains(selected));
|
||||
final coordinate = userCoordinate;
|
||||
if (coordinate == null) {
|
||||
return const [];
|
||||
return matchingPlaces.toList();
|
||||
}
|
||||
|
||||
final nearbyPlaces = places.where((place) {
|
||||
return _distance(coordinate, place.coordinate) <=
|
||||
nearbyRecommendationRadiusMeters;
|
||||
});
|
||||
|
||||
final ranked = [...nearbyPlaces]
|
||||
final ranked = [...matchingPlaces]
|
||||
..sort((a, b) {
|
||||
final aScore = a.traits.contains(selectedTrait) ? 1 : 0;
|
||||
final bScore = b.traits.contains(selectedTrait) ? 1 : 0;
|
||||
final scoreOrder = bScore.compareTo(aScore);
|
||||
if (scoreOrder != 0) {
|
||||
return scoreOrder;
|
||||
}
|
||||
|
||||
return _distance(
|
||||
coordinate,
|
||||
a.coordinate,
|
||||
).compareTo(_distance(coordinate, b.coordinate));
|
||||
});
|
||||
return ranked.take(4).toList();
|
||||
return ranked;
|
||||
}
|
||||
|
||||
PlaceRecommendation? get selectedPlace {
|
||||
for (final place in places) {
|
||||
final visiblePlaces = recommendations;
|
||||
for (final place in visiblePlaces) {
|
||||
if (place.id == selectedPlaceId) {
|
||||
return place;
|
||||
}
|
||||
}
|
||||
return recommendations.isEmpty ? null : recommendations.first;
|
||||
return visiblePlaces.isEmpty ? null : visiblePlaces.first;
|
||||
}
|
||||
|
||||
PlaceState copyWith({
|
||||
PlaceTrait? selectedTrait,
|
||||
Object? selectedTrait = _unset,
|
||||
List<PlaceRecommendation>? places,
|
||||
String? selectedPlaceId,
|
||||
Object? selectedPlaceId = _unset,
|
||||
AppUser? currentUser,
|
||||
bool? hasTelegramAuth,
|
||||
LatLng? userCoordinate,
|
||||
VoiceReviewDraft? reviewDraft,
|
||||
}) {
|
||||
return PlaceState(
|
||||
selectedTrait: selectedTrait ?? this.selectedTrait,
|
||||
selectedTrait: identical(selectedTrait, _unset)
|
||||
? this.selectedTrait
|
||||
: selectedTrait as PlaceTrait?,
|
||||
places: places ?? this.places,
|
||||
selectedPlaceId: selectedPlaceId ?? this.selectedPlaceId,
|
||||
selectedPlaceId: identical(selectedPlaceId, _unset)
|
||||
? this.selectedPlaceId
|
||||
: selectedPlaceId as String?,
|
||||
currentUser: currentUser ?? this.currentUser,
|
||||
hasTelegramAuth: hasTelegramAuth ?? this.hasTelegramAuth,
|
||||
userCoordinate: userCoordinate ?? this.userCoordinate,
|
||||
@@ -96,7 +94,7 @@ class PlaceController extends AsyncNotifier<PlaceState> {
|
||||
Future<PlaceState> build() async {
|
||||
if (!_api.hasTelegramAuth) {
|
||||
return const PlaceState(
|
||||
selectedTrait: PlaceTrait.calm,
|
||||
selectedTrait: null,
|
||||
places: [],
|
||||
selectedPlaceId: null,
|
||||
currentUser: null,
|
||||
@@ -115,7 +113,7 @@ class PlaceController extends AsyncNotifier<PlaceState> {
|
||||
final userCoordinate = await _location.resolve();
|
||||
final places = await _api.fetchPlaces();
|
||||
return PlaceState(
|
||||
selectedTrait: PlaceTrait.calm,
|
||||
selectedTrait: null,
|
||||
places: places,
|
||||
selectedPlaceId: places.isEmpty ? null : places.first.id,
|
||||
currentUser: currentUser,
|
||||
@@ -144,6 +142,14 @@ class PlaceController extends AsyncNotifier<PlaceState> {
|
||||
);
|
||||
}
|
||||
|
||||
void clearTrait() {
|
||||
final value = state.requireValue;
|
||||
final selectedPlaceId = value.places.isEmpty ? null : value.places.first.id;
|
||||
state = AsyncData(
|
||||
value.copyWith(selectedTrait: null, selectedPlaceId: selectedPlaceId),
|
||||
);
|
||||
}
|
||||
|
||||
void selectPlace(String placeId) {
|
||||
final value = state.requireValue;
|
||||
state = AsyncData(value.copyWith(selectedPlaceId: placeId));
|
||||
@@ -167,7 +173,12 @@ class PlaceController extends AsyncNotifier<PlaceState> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> publishReview({required PlaceRecommendation place}) async {
|
||||
Future<void> publishReview({
|
||||
required PlaceRecommendation place,
|
||||
required String audioObjectKey,
|
||||
required String audioContentBase64,
|
||||
required String audioMimeType,
|
||||
}) async {
|
||||
final value = state.requireValue;
|
||||
if (!value.hasTelegramAuth) {
|
||||
throw StateError('Открой через Telegram, чтобы оставить голос.');
|
||||
@@ -180,7 +191,9 @@ class PlaceController extends AsyncNotifier<PlaceState> {
|
||||
googleName: place.name,
|
||||
coordinate: place.coordinate,
|
||||
durationSeconds: draft.duration.inSeconds,
|
||||
audioObjectKey: 'web-recording-${DateTime.now().microsecondsSinceEpoch}',
|
||||
audioObjectKey: audioObjectKey,
|
||||
audioContentBase64: audioContentBase64,
|
||||
audioMimeType: audioMimeType,
|
||||
);
|
||||
|
||||
final places = await _api.fetchPlaces();
|
||||
|
||||
74
pubspec.lock
74
pubspec.lock
@@ -105,6 +105,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.15.0"
|
||||
cross_file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cross_file
|
||||
sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.5+2"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -206,6 +214,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.1"
|
||||
flutter_svg:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_svg
|
||||
sha256: "35882981abcbfb8c15b286f0cd690ff25bac12d95eff3e25ee207f37d4c42e7f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@@ -512,6 +528,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_parsing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_parsing
|
||||
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
path_provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -617,7 +641,7 @@ packages:
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
record:
|
||||
dependency: "direct main"
|
||||
dependency: transitive
|
||||
description:
|
||||
name: record
|
||||
sha256: d5b6b334f3ab02460db6544e08583c942dbf23e3504bf1e14fd4cbe3d9409277
|
||||
@@ -736,14 +760,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.4"
|
||||
siri_wave:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: siri_wave
|
||||
sha256: ea815d6627dc297f6be883bb0dd7a579a5f5f9729242d47c10e95850cccf169a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
@@ -861,6 +877,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.3"
|
||||
vector_graphics:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics
|
||||
sha256: "4d35a36400983c3457c289d4d553b5308f506ea84f7e51c7a564651b5525209a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
vector_graphics_codec:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics_codec
|
||||
sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.13"
|
||||
vector_graphics_compiler:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics_compiler
|
||||
sha256: "98e7e94de127b46a86ef46197fff84ff99f3d3b80a708390d717ad731efef598"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -885,6 +925,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
waveform_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: waveform_flutter
|
||||
sha256: "08c9e98d4cf119428d8b3c083ed42c11c468623eaffdf30420ae38e36662922a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
waveform_recorder:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: waveform_recorder
|
||||
sha256: "1ca0a19b143d1bdef2adfb3d28f0627c18aee5285235c8cf81a89bf29a0420e1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.8.0"
|
||||
web:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
@@ -40,8 +40,9 @@ dependencies:
|
||||
http: ^1.6.0
|
||||
web: ^1.1.1
|
||||
geolocator: ^14.0.2
|
||||
record: ^6.2.0
|
||||
siri_wave: ^2.3.1
|
||||
flutter_svg: ^2.3.0
|
||||
waveform_recorder: ^1.8.0
|
||||
waveform_flutter: ^1.2.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user