Compare commits

...

34 Commits

Author SHA1 Message Date
Ruslan Bakiev
7e3d03d225 Show only populated place filters
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 3m30s
2026-05-14 22:48:47 +07:00
Ruslan Bakiev
697f029ad2 Separate browsing filters from review radius
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 4m2s
2026-05-14 22:34:59 +07:00
Ruslan Bakiev
0b1493a02e Show ontology snowflake in admin reviews
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 2m55s
2026-05-14 22:06:10 +07:00
Ruslan Bakiev
dfe2c52f8f Retry Flutter admin tags deploy
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 3m43s
2026-05-14 20:31:52 +07:00
Ruslan Bakiev
abae8b905c Show ontology tag highlights in admin
Some checks failed
Build and deploy Flutter Web / build (push) Failing after 25m24s
2026-05-14 20:02:37 +07:00
Ruslan Bakiev
0532f6aa88 Restore waveform recording controller
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 2m52s
2026-05-14 16:35:28 +07:00
Ruslan Bakiev
34a197f786 Decouple voice progress from amplitude stream
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 2m30s
2026-05-14 14:10:21 +07:00
Ruslan Bakiev
1b6b40849e Throttle voice progress updates
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 2m38s
2026-05-14 13:55:00 +07:00
Ruslan Bakiev
584e30624d Use single microphone stream for recording
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 2m50s
2026-05-14 09:16:31 +07:00
Ruslan Bakiev
21945b2335 Retry CI image push
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 6m57s
2026-05-14 08:54:12 +07:00
Ruslan Bakiev
fbf9104d2d Add admin review debug screen
Some checks failed
Build and deploy Flutter Web / build (push) Failing after 14s
2026-05-14 08:44:20 +07:00
Ruslan Bakiev
cdf6a43d49 Fix Telegram viewport and SVG avatars
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 2m48s
2026-05-13 22:50:51 +07:00
Ruslan Bakiev
d3721e44e7 Darken recording flow and increase voice requirement
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 2m41s
2026-05-13 22:34:10 +07:00
Ruslan Bakiev
28e8cee6e6 Show Google place types in selection cards
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 2m11s
2026-05-13 22:24:20 +07:00
Ruslan Bakiev
29e856bbd8 Remove website Telegram login countdown
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 2m12s
2026-05-13 20:10:00 +07:00
Ruslan Bakiev
a8b6aa6e02 Complete Telegram bot login from callback URL
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 2m18s
2026-05-13 19:36:09 +07:00
Ruslan Bakiev
5b2cd4158c Hide voice waveform visualization
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 2m39s
2026-05-13 17:59:30 +07:00
Ruslan Bakiev
04fa49737d Expand voice recording layout
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 2m7s
2026-05-13 17:50:17 +07:00
Ruslan Bakiev
729dd21b78 Add compact voice progress grid
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 2m6s
2026-05-13 17:41:28 +07:00
Ruslan Bakiev
fcc2c26752 Restore wave voice recorder UI
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 10s
2026-05-13 17:22:44 +07:00
Ruslan Bakiev
069dcab479 Revert "Layer voice wave under grid"
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 26s
This reverts commit 8fda6f554d.
2026-05-13 17:15:51 +07:00
Ruslan Bakiev
8fda6f554d Layer voice wave under grid
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 2m15s
2026-05-13 17:08:24 +07:00
Ruslan Bakiev
73ed4c2614 Make voice grid visibly animate
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 2m6s
2026-05-13 16:58:42 +07:00
Ruslan Bakiev
2366587693 Restore voice information grid
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 2m14s
2026-05-13 16:22:18 +07:00
Ruslan Bakiev
d7b419fea6 Use waveform recorder for voice capture
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 2m26s
2026-05-13 16:01:18 +07:00
Ruslan Bakiev
4a2e458a01 Use Web Audio for browser voice meter
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 2m22s
2026-05-13 15:31:56 +07:00
Ruslan Bakiev
c9be8b5e75 Trigger Dokploy from workflow secret
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 2m10s
2026-05-13 14:59:28 +07:00
Ruslan Bakiev
f2277626f1 Verify deploy hook
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 8s
2026-05-13 14:52:44 +07:00
Ruslan Bakiev
8c7e62d9e1 Remove Dokploy webhook from workflow
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 3m3s
2026-05-13 14:33:48 +07:00
Ruslan Bakiev
765219cc20 Rework voice meter signal visualization
Some checks failed
Build and deploy Flutter Web / build (push) Failing after 2m20s
2026-05-13 14:16:18 +07:00
Ruslan Bakiev
906c23366f Use recorder amplitude for web voice meter
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 1m53s
2026-05-09 23:08:30 +07:00
Ruslan Bakiev
2c9bcad0cc Fix adaptive voice information meter
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 1m41s
2026-05-09 18:12:00 +07:00
Ruslan Bakiev
adc935b6cf Gate voice review by information fill
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 1m45s
2026-05-09 17:51:42 +07:00
Ruslan Bakiev
6055a101e8 Use real PCM voice waveform
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 1m57s
2026-05-09 17:41:34 +07:00
10 changed files with 1072 additions and 479 deletions

View File

@@ -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

View File

@@ -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,

View File

@@ -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) {}

View File

@@ -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() {

View File

@@ -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()));
}

View File

@@ -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

View File

@@ -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();

View File

@@ -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:

View File

@@ -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: