Compare commits

...

45 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
Ruslan Bakiev
f9d6e4fa5b Polish voice recording screen
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 2m27s
2026-05-09 17:28:58 +07:00
Ruslan Bakiev
35ccfe2368 Record voice before selecting place
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 1m46s
2026-05-09 16:56:15 +07:00
Ruslan Bakiev
b93f7ec4ec Use nearby Google places for reviews
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 1m48s
2026-05-09 15:19:30 +07:00
Ruslan Bakiev
02b3521320 Improve voice recording screen
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 1m46s
2026-05-09 15:10:18 +07:00
Ruslan Bakiev
c02c050607 Use latest tag for Flutter deploys
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 1m53s
2026-05-09 15:02:23 +07:00
Ruslan Bakiev
f929790bd6 Fail Flutter deploy on webhook errors
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 1m49s
2026-05-09 14:48:56 +07:00
Ruslan Bakiev
8cf2b29143 Deploy Flutter through Dokploy webhook
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 2m12s
2026-05-09 14:44:03 +07:00
Ruslan Bakiev
f5f59d3020 Require Google place for voice reviews
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 1m49s
2026-05-09 14:18:07 +07:00
Ruslan Bakiev
b819b51c1f Add live microphone waveform
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 2m3s
2026-05-09 14:08:27 +07:00
Ruslan Bakiev
56703c887f Require place before voice review
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 2m8s
2026-05-08 20:31:36 +07:00
Ruslan Bakiev
929d3a46d3 Center map on user location
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 2m28s
2026-05-08 20:23:15 +07:00
17 changed files with 1691 additions and 392 deletions

View File

@@ -7,11 +7,11 @@ on:
jobs: jobs:
build: build:
runs-on: build-host runs-on: builder
env: env:
SERVICE_NAME: flutter SERVICE_NAME: flutter
IMAGE: gitea.dsrptlab.com/mapflow/flutter:${{ github.sha }} IMAGE_SHA: gitea.dsrptlab.com/mapflow/flutter:${{ github.sha }}
DOKPLOY_APPLICATION_ID: 9UR7Tpp9v_I6Ueu04sUiu IMAGE_LATEST: gitea.dsrptlab.com/mapflow/flutter:latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -22,27 +22,24 @@ jobs:
auth="$(printf '%s:%s' "${{ secrets.REGISTRY_USERNAME }}" "${{ secrets.REGISTRY_TOKEN }}" | base64 | tr -d '\n')" 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 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 - name: Build and push image
run: | run: |
set -euo pipefail set -euo pipefail
builder="builder" for attempt in 1 2 3; do
if ! docker buildx inspect "$builder" >/dev/null 2>&1; then if docker buildx build \
docker buildx create --name "$builder" --driver docker-container --buildkitd-config /etc/buildkit/buildkitd.toml --push \
fi --provenance=false \
docker buildx use "$builder" --tag "$IMAGE_SHA" \
docker buildx inspect --bootstrap --tag "$IMAGE_LATEST" \
docker buildx build \ --build-arg MAPBOX_ACCESS_TOKEN="${{ secrets.MAPBOX_ACCESS_TOKEN }}" \
--push \ --build-arg MAPBOX_STYLE="mapbox/streets-v12" \
--tag "$IMAGE" \ --build-arg TELEGRAM_BOT_USERNAME="carfteebot" \
--build-arg MAPBOX_ACCESS_TOKEN="${{ secrets.MAPBOX_ACCESS_TOKEN }}" \ .; then
--build-arg MAPBOX_STYLE="mapbox/streets-v12" \ exit 0
--build-arg TELEGRAM_BOT_USERNAME="carfteebot" \ fi
. sleep "$((attempt * 10))"
done
exit 1
- name: Skip stale deployment - name: Skip stale deployment
run: | run: |
@@ -54,34 +51,18 @@ jobs:
echo "A newer main commit exists: $latest_sha. Skipping deploy for ${GITHUB_SHA}." echo "A newer main commit exists: $latest_sha. Skipping deploy for ${GITHUB_SHA}."
fi fi
- name: Update Dokploy image - name: Trigger Dokploy deploy webhook
run: | run: |
set -euo pipefail set -euo pipefail
[ -f .deploy-current ] || exit 0 [ -f .deploy-current ] || exit 0
payload=$(cat <<JSON payload=$(cat <<JSON
{"applicationId":"$DOKPLOY_APPLICATION_ID","dockerImage":"$IMAGE","username":"${{ secrets.REGISTRY_USERNAME }}","password":"${{ secrets.REGISTRY_TOKEN }}","registryUrl":"gitea.dsrptlab.com"} {"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 JSON
) )
curl -fsS -X POST "${{ secrets.DOKPLOY_URL }}/api/application.saveDockerProvider" \ response_file="$(mktemp)"
-H "x-api-key: ${{ secrets.DOKPLOY_TOKEN }}" \ 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" \ -H "Content-Type: application/json" \
-d "$payload" -d "$payload")"
cat "$response_file"
- name: Deploy in Dokploy [ "$status_code" = "200" ]
run: |
set -euo pipefail
[ -f .deploy-current ] || exit 0
short_sha="${GITHUB_SHA:0:7}"
payload=$(cat <<JSON
{"applicationId":"$DOKPLOY_APPLICATION_ID","title":"$SERVICE_NAME #${GITHUB_RUN_NUMBER:-0} $short_sha","description":"Deploy registry image $IMAGE"}
JSON
)
curl -fsS -X POST "${{ secrets.DOKPLOY_URL }}/api/application.deploy" \
-H "x-api-key: ${{ secrets.DOKPLOY_TOKEN }}" \
-H "Content-Type: application/json" \
-d "$payload"
- name: Prune shared BuildKit cache
run: |
set -euo pipefail
docker buildx prune --builder builder --all --max-used-space 40gb -f

View File

@@ -1,4 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<application <application
android:label="mapflow" android:label="mapflow"
android:name="${applicationName}" android:name="${applicationName}"

View File

@@ -24,6 +24,10 @@
<string>$(FLUTTER_BUILD_NUMBER)</string> <string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>NSLocationWhenInUseUsageDescription</key>
<string>MapFlow uses your location to show places around you.</string>
<key>NSMicrophoneUsageDescription</key>
<string>MapFlow uses your microphone to record your voice review.</string>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>

View File

@@ -51,6 +51,7 @@ class MapflowApi {
lastName lastName
photoUrl photoUrl
languageCode languageCode
isAdmin
} }
} }
'''); ''');
@@ -73,6 +74,7 @@ class MapflowApi {
lastName lastName
photoUrl photoUrl
languageCode languageCode
isAdmin
} }
} }
} }
@@ -97,6 +99,7 @@ class MapflowApi {
lastName lastName
photoUrl photoUrl
languageCode languageCode
isAdmin
} }
} }
} }
@@ -126,31 +129,29 @@ class MapflowApi {
); );
} }
Future<TelegramBotLoginStatus> fetchTelegramBotLoginStatus( Future<TelegramBotLoginSession> completeTelegramBotLogin(String token) async {
String token,
) async {
final data = await _graphql( final data = await _graphql(
''' '''
query TelegramBotLoginStatus(\$token: String!) { mutation CompleteTelegramBotLogin(\$token: String!) {
telegramBotLoginStatus(token: \$token) { completeTelegramBotLogin(token: \$token) {
status
sessionToken sessionToken
user { user {
id id
telegramId telegramId
username username
firstName firstName
lastName lastName
photoUrl photoUrl
languageCode languageCode
} isAdmin
} }
} }
}
''', ''',
variables: {'token': token}, variables: {'token': token},
); );
return TelegramBotLoginStatus.fromJson( return TelegramBotLoginSession.fromJson(
data['telegramBotLoginStatus'] as Map<String, dynamic>, data['completeTelegramBotLogin'] as Map<String, dynamic>,
); );
} }
@@ -163,6 +164,8 @@ class MapflowApi {
name name
latitude latitude
longitude longitude
googlePrimaryType
googleTypes
experiences { experiences {
id id
status status
@@ -178,6 +181,7 @@ class MapflowApi {
final place = item as Map<String, dynamic>; final place = item as Map<String, dynamic>;
return PlaceRecommendation( return PlaceRecommendation(
id: place['id'] as String, id: place['id'] as String,
googlePlaceId: place['googlePlaceId'] as String,
name: place['name'] as String, name: place['name'] as String,
area: '', area: '',
photoUrls: const [], photoUrls: const [],
@@ -186,6 +190,65 @@ class MapflowApi {
(place['longitude'] as num).toDouble(), (place['longitude'] as num).toDouble(),
), ),
traits: _traitsFromExperiences(place['experiences'] as List<dynamic>), 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();
}
Future<List<PlaceRecommendation>> fetchNearbyPlaces({
required LatLng coordinate,
required int radiusMeters,
}) async {
final data = await _graphql(
'''
query NearbyPlaces(\$input: NearbyPlacesInput!) {
nearbyPlaces(input: \$input) {
id
googlePlaceId
name
latitude
longitude
googlePrimaryType
googleTypes
experiences {
id
status
analysis
createdAt
}
}
}
''',
variables: {
'input': {
'latitude': coordinate.latitude,
'longitude': coordinate.longitude,
'radiusMeters': radiusMeters,
},
},
);
final places = data['nearbyPlaces'] as List<dynamic>;
return places.map((item) {
final place = item as Map<String, dynamic>;
return PlaceRecommendation(
id: place['id'] as String,
googlePlaceId: place['googlePlaceId'] as String,
name: place['name'] as String,
area: '',
photoUrls: const [],
coordinate: LatLng(
(place['latitude'] as num).toDouble(),
(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(); }).toList();
} }
@@ -196,6 +259,8 @@ class MapflowApi {
required LatLng coordinate, required LatLng coordinate,
required int durationSeconds, required int durationSeconds,
required String audioObjectKey, required String audioObjectKey,
required String audioContentBase64,
required String audioMimeType,
}) async { }) async {
if (!hasTelegramAuth) { if (!hasTelegramAuth) {
throw StateError('Telegram authorization is required.'); throw StateError('Telegram authorization is required.');
@@ -217,11 +282,43 @@ class MapflowApi {
'longitude': coordinate.longitude, 'longitude': coordinate.longitude,
'durationSeconds': durationSeconds, 'durationSeconds': durationSeconds,
'audioObjectKey': audioObjectKey, '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( Future<Map<String, dynamic>> _graphql(
String query, { String query, {
Map<String, dynamic>? variables, Map<String, dynamic>? variables,

View File

@@ -4,17 +4,13 @@ String telegramLoginData() => '';
String mapflowSessionToken() => ''; String mapflowSessionToken() => '';
String pendingTelegramLoginToken() => '';
String telegramLoginTokenFromUrl() => ''; String telegramLoginTokenFromUrl() => '';
void saveMapflowSessionToken(String token) {} void saveMapflowSessionToken(String token) {}
void clearMapflowSession() {} void clearMapflowSession() {}
void savePendingTelegramLoginToken(String token) {} void configureTelegramWebApp() {}
void clearPendingTelegramLoginToken() {}
void openExternalUrl(String url) {} void openExternalUrl(String url) {}

View File

@@ -4,15 +4,29 @@ import 'package:web/web.dart' as web;
const telegramLoginStorageKey = 'mapflow.telegramLoginData'; const telegramLoginStorageKey = 'mapflow.telegramLoginData';
const mapflowSessionStorageKey = 'mapflow.sessionToken'; const mapflowSessionStorageKey = 'mapflow.sessionToken';
const pendingTelegramLoginStorageKey = 'mapflow.pendingTelegramLoginToken';
@JS('window.Telegram.WebApp.initData') @JS('Telegram')
external JSString? get _telegramInitData; 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') @JS('JSON.stringify')
external JSString _jsonStringify(JSAny? value); external JSString _jsonStringify(JSAny? value);
String telegramInitData() => _telegramInitData?.toDart ?? ''; _TelegramWebApp? get _webApp => _telegram?.webApp;
String telegramInitData() => _webApp?.initData?.toDart ?? '';
String telegramLoginData() => String telegramLoginData() =>
web.window.localStorage.getItem(telegramLoginStorageKey) ?? ''; web.window.localStorage.getItem(telegramLoginStorageKey) ?? '';
@@ -20,9 +34,6 @@ String telegramLoginData() =>
String mapflowSessionToken() => String mapflowSessionToken() =>
web.window.localStorage.getItem(mapflowSessionStorageKey) ?? ''; web.window.localStorage.getItem(mapflowSessionStorageKey) ?? '';
String pendingTelegramLoginToken() =>
web.window.localStorage.getItem(pendingTelegramLoginStorageKey) ?? '';
String telegramLoginTokenFromUrl() { String telegramLoginTokenFromUrl() {
final uri = Uri.parse(web.window.location.href); final uri = Uri.parse(web.window.location.href);
return uri.queryParameters['telegram_login'] ?? ''; return uri.queryParameters['telegram_login'] ?? '';
@@ -40,19 +51,23 @@ void saveMapflowSessionToken(String token) {
void clearMapflowSession() { void clearMapflowSession() {
web.window.localStorage.removeItem(mapflowSessionStorageKey); web.window.localStorage.removeItem(mapflowSessionStorageKey);
web.window.localStorage.removeItem(telegramLoginStorageKey); web.window.localStorage.removeItem(telegramLoginStorageKey);
clearPendingTelegramLoginToken();
} }
void savePendingTelegramLoginToken(String token) { void configureTelegramWebApp() {
web.window.localStorage.setItem(pendingTelegramLoginStorageKey, token); final webApp = _webApp;
} if (webApp == null) {
return;
}
void clearPendingTelegramLoginToken() { webApp.ready();
web.window.localStorage.removeItem(pendingTelegramLoginStorageKey); webApp.expand();
if (webApp.isVersionAtLeast('7.7')) {
webApp.disableVerticalSwipes();
}
} }
void openExternalUrl(String url) { void openExternalUrl(String url) {
web.window.open(url, '_blank', 'noopener,noreferrer'); web.window.location.assign(url);
} }
void reloadApp() { void reloadApp() {

View File

@@ -0,0 +1,32 @@
import 'package:geolocator/geolocator.dart';
import 'package:latlong2/latlong.dart';
class CurrentLocation {
Future<LatLng?> resolve() async {
final serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
return null;
}
final permission = await _resolvePermission();
if (permission != LocationPermission.whileInUse &&
permission != LocationPermission.always) {
return null;
}
final position = await Geolocator.getCurrentPosition(
locationSettings: const LocationSettings(accuracy: LocationAccuracy.high),
);
return LatLng(position.latitude, position.longitude);
}
Future<LocationPermission> _resolvePermission() async {
final permission = await Geolocator.checkPermission();
if (permission != LocationPermission.denied) {
return permission;
}
return Geolocator.requestPermission();
}
}

View File

@@ -3,10 +3,12 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'auth/telegram_session.dart' as telegram_session;
import 'screens/mapflow_shell.dart'; import 'screens/mapflow_shell.dart';
void main() { void main() {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
telegram_session.configureTelegramWebApp();
runApp(const ProviderScope(child: MapflowApp())); runApp(const ProviderScope(child: MapflowApp()));
} }

View File

@@ -10,6 +10,7 @@ class AppUser {
required this.lastName, required this.lastName,
required this.photoUrl, required this.photoUrl,
required this.languageCode, required this.languageCode,
required this.isAdmin,
}); });
factory AppUser.fromJson(Map<String, dynamic> json) { factory AppUser.fromJson(Map<String, dynamic> json) {
@@ -21,6 +22,7 @@ class AppUser {
lastName: json['lastName'] as String?, lastName: json['lastName'] as String?,
photoUrl: json['photoUrl'] as String?, photoUrl: json['photoUrl'] as String?,
languageCode: json['languageCode'] as String?, languageCode: json['languageCode'] as String?,
isAdmin: json['isAdmin'] as bool? ?? false,
); );
} }
@@ -31,6 +33,51 @@ class AppUser {
final String? lastName; final String? lastName;
final String? photoUrl; final String? photoUrl;
final String? languageCode; 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 { enum PlaceTrait {
@@ -84,19 +131,25 @@ extension PlaceTraitText on PlaceTrait {
class PlaceRecommendation { class PlaceRecommendation {
const PlaceRecommendation({ const PlaceRecommendation({
required this.id, required this.id,
required this.googlePlaceId,
required this.name, required this.name,
required this.area, required this.area,
required this.photoUrls, required this.photoUrls,
required this.coordinate, required this.coordinate,
required this.traits, required this.traits,
required this.googlePrimaryType,
required this.googleTypes,
}); });
final String id; final String id;
final String googlePlaceId;
final String name; final String name;
final String area; final String area;
final List<String> photoUrls; final List<String> photoUrls;
final LatLng coordinate; final LatLng coordinate;
final Set<PlaceTrait> traits; final Set<PlaceTrait> traits;
final String? googlePrimaryType;
final List<String> googleTypes;
String get coverPhotoUrl => photoUrls.first; String get coverPhotoUrl => photoUrls.first;
} }
@@ -151,26 +204,19 @@ class TelegramBotLogin {
final DateTime expiresAt; final DateTime expiresAt;
} }
class TelegramBotLoginStatus { class TelegramBotLoginSession {
const TelegramBotLoginStatus({ const TelegramBotLoginSession({
required this.status,
required this.sessionToken, required this.sessionToken,
required this.user, required this.user,
}); });
factory TelegramBotLoginStatus.fromJson(Map<String, dynamic> json) { factory TelegramBotLoginSession.fromJson(Map<String, dynamic> json) {
final user = json['user']; return TelegramBotLoginSession(
return TelegramBotLoginStatus( sessionToken: json['sessionToken'] as String,
status: json['status'] as String, user: AppUser.fromJson(json['user'] as Map<String, dynamic>),
sessionToken: json['sessionToken'] as String?,
user: user is Map<String, dynamic> ? AppUser.fromJson(user) : null,
); );
} }
final String status; final String sessionToken;
final String? sessionToken; final AppUser user;
final AppUser? user;
bool get isConfirmed => status == 'CONFIRMED' && sessionToken != null;
bool get isExpired => status == 'EXPIRED';
} }

File diff suppressed because it is too large Load Diff

View File

@@ -2,11 +2,14 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import '../api/mapflow_api.dart'; import '../api/mapflow_api.dart';
import '../location/current_location.dart';
import '../models/place_models.dart'; import '../models/place_models.dart';
final placeControllerProvider = final placeControllerProvider =
AsyncNotifierProvider<PlaceController, PlaceState>(PlaceController.new); AsyncNotifierProvider<PlaceController, PlaceState>(PlaceController.new);
const _unset = Object();
class PlaceState { class PlaceState {
const PlaceState({ const PlaceState({
required this.selectedTrait, required this.selectedTrait,
@@ -14,49 +17,70 @@ class PlaceState {
required this.selectedPlaceId, required this.selectedPlaceId,
required this.currentUser, required this.currentUser,
required this.hasTelegramAuth, required this.hasTelegramAuth,
required this.userCoordinate,
required this.reviewDraft, required this.reviewDraft,
}); });
final PlaceTrait selectedTrait; static final _distance = Distance();
final PlaceTrait? selectedTrait;
final List<PlaceRecommendation> places; final List<PlaceRecommendation> places;
final String? selectedPlaceId; final String? selectedPlaceId;
final AppUser? currentUser; final AppUser? currentUser;
final bool hasTelegramAuth; final bool hasTelegramAuth;
final LatLng? userCoordinate;
final VoiceReviewDraft reviewDraft; final VoiceReviewDraft reviewDraft;
List<PlaceRecommendation> get recommendations { List<PlaceRecommendation> get recommendations {
final ranked = [...places] final selected = selectedTrait;
final matchingPlaces = selected == null
? places
: places.where((place) => place.traits.contains(selected));
final coordinate = userCoordinate;
if (coordinate == null) {
return matchingPlaces.toList();
}
final ranked = [...matchingPlaces]
..sort((a, b) { ..sort((a, b) {
final aScore = a.traits.contains(selectedTrait) ? 1 : 0; return _distance(
final bScore = b.traits.contains(selectedTrait) ? 1 : 0; coordinate,
return bScore.compareTo(aScore); a.coordinate,
).compareTo(_distance(coordinate, b.coordinate));
}); });
return ranked.take(4).toList(); return ranked;
} }
PlaceRecommendation? get selectedPlace { PlaceRecommendation? get selectedPlace {
for (final place in places) { final visiblePlaces = recommendations;
for (final place in visiblePlaces) {
if (place.id == selectedPlaceId) { if (place.id == selectedPlaceId) {
return place; return place;
} }
} }
return recommendations.isEmpty ? null : recommendations.first; return visiblePlaces.isEmpty ? null : visiblePlaces.first;
} }
PlaceState copyWith({ PlaceState copyWith({
PlaceTrait? selectedTrait, Object? selectedTrait = _unset,
List<PlaceRecommendation>? places, List<PlaceRecommendation>? places,
String? selectedPlaceId, Object? selectedPlaceId = _unset,
AppUser? currentUser, AppUser? currentUser,
bool? hasTelegramAuth, bool? hasTelegramAuth,
LatLng? userCoordinate,
VoiceReviewDraft? reviewDraft, VoiceReviewDraft? reviewDraft,
}) { }) {
return PlaceState( return PlaceState(
selectedTrait: selectedTrait ?? this.selectedTrait, selectedTrait: identical(selectedTrait, _unset)
? this.selectedTrait
: selectedTrait as PlaceTrait?,
places: places ?? this.places, places: places ?? this.places,
selectedPlaceId: selectedPlaceId ?? this.selectedPlaceId, selectedPlaceId: identical(selectedPlaceId, _unset)
? this.selectedPlaceId
: selectedPlaceId as String?,
currentUser: currentUser ?? this.currentUser, currentUser: currentUser ?? this.currentUser,
hasTelegramAuth: hasTelegramAuth ?? this.hasTelegramAuth, hasTelegramAuth: hasTelegramAuth ?? this.hasTelegramAuth,
userCoordinate: userCoordinate ?? this.userCoordinate,
reviewDraft: reviewDraft ?? this.reviewDraft, reviewDraft: reviewDraft ?? this.reviewDraft,
); );
} }
@@ -64,16 +88,18 @@ class PlaceState {
class PlaceController extends AsyncNotifier<PlaceState> { class PlaceController extends AsyncNotifier<PlaceState> {
final _api = MapflowApi(); final _api = MapflowApi();
final _location = CurrentLocation();
@override @override
Future<PlaceState> build() async { Future<PlaceState> build() async {
if (!_api.hasTelegramAuth) { if (!_api.hasTelegramAuth) {
return const PlaceState( return const PlaceState(
selectedTrait: PlaceTrait.calm, selectedTrait: null,
places: [], places: [],
selectedPlaceId: null, selectedPlaceId: null,
currentUser: null, currentUser: null,
hasTelegramAuth: false, hasTelegramAuth: false,
userCoordinate: null,
reviewDraft: VoiceReviewDraft( reviewDraft: VoiceReviewDraft(
placeName: '', placeName: '',
duration: Duration.zero, duration: Duration.zero,
@@ -84,13 +110,15 @@ class PlaceController extends AsyncNotifier<PlaceState> {
} }
final currentUser = await _api.authenticateTelegram(); final currentUser = await _api.authenticateTelegram();
final userCoordinate = await _location.resolve();
final places = await _api.fetchPlaces(); final places = await _api.fetchPlaces();
return PlaceState( return PlaceState(
selectedTrait: PlaceTrait.calm, selectedTrait: null,
places: places, places: places,
selectedPlaceId: places.isEmpty ? null : places.first.id, selectedPlaceId: places.isEmpty ? null : places.first.id,
currentUser: currentUser, currentUser: currentUser,
hasTelegramAuth: _api.hasTelegramAuth, hasTelegramAuth: _api.hasTelegramAuth,
userCoordinate: userCoordinate,
reviewDraft: const VoiceReviewDraft( reviewDraft: const VoiceReviewDraft(
placeName: '', placeName: '',
duration: Duration.zero, duration: Duration.zero,
@@ -114,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) { void selectPlace(String placeId) {
final value = state.requireValue; final value = state.requireValue;
state = AsyncData(value.copyWith(selectedPlaceId: placeId)); state = AsyncData(value.copyWith(selectedPlaceId: placeId));
@@ -137,24 +173,27 @@ class PlaceController extends AsyncNotifier<PlaceState> {
); );
} }
Future<void> publishReview({LatLng? coordinate}) async { Future<void> publishReview({
required PlaceRecommendation place,
required String audioObjectKey,
required String audioContentBase64,
required String audioMimeType,
}) async {
final value = state.requireValue; final value = state.requireValue;
if (!value.hasTelegramAuth) { if (!value.hasTelegramAuth) {
throw StateError('Открой через Telegram, чтобы оставить голос.'); throw StateError('Открой через Telegram, чтобы оставить голос.');
} }
final draft = value.reviewDraft; final draft = value.reviewDraft;
final placeName = draft.placeName.trim().isEmpty
? 'Место на карте'
: draft.placeName.trim();
final point = coordinate ?? const LatLng(10.7729, 106.7004);
await _api.createVoiceExperience( await _api.createVoiceExperience(
googlePlaceId: 'manual-${point.latitude}-${point.longitude}-$placeName', googlePlaceId: place.googlePlaceId,
googleName: placeName, googleName: place.name,
coordinate: point, coordinate: place.coordinate,
durationSeconds: draft.duration.inSeconds, durationSeconds: draft.duration.inSeconds,
audioObjectKey: 'web-recording-${DateTime.now().microsecondsSinceEpoch}', audioObjectKey: audioObjectKey,
audioContentBase64: audioContentBase64,
audioMimeType: audioMimeType,
); );
final places = await _api.fetchPlaces(); final places = await _api.fetchPlaces();

View File

@@ -5,6 +5,12 @@
import FlutterMacOS import FlutterMacOS
import Foundation import Foundation
import geolocator_apple
import package_info_plus
import record_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin"))
} }

View File

@@ -8,5 +8,9 @@
<true/> <true/>
<key>com.apple.security.network.server</key> <key>com.apple.security.network.server</key>
<true/> <true/>
<key>com.apple.security.personal-information.location</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
</dict> </dict>
</plist> </plist>

View File

@@ -24,6 +24,10 @@
<string>$(MACOSX_DEPLOYMENT_TARGET)</string> <string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key> <key>NSHumanReadableCopyright</key>
<string>$(PRODUCT_COPYRIGHT)</string> <string>$(PRODUCT_COPYRIGHT)</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>MapFlow uses your location to show places around you.</string>
<key>NSMicrophoneUsageDescription</key>
<string>MapFlow uses your microphone to record your voice review.</string>
<key>NSMainNibFile</key> <key>NSMainNibFile</key>
<string>MainMenu</string> <string>MainMenu</string>
<key>NSPrincipalClass</key> <key>NSPrincipalClass</key>

View File

@@ -4,5 +4,9 @@
<dict> <dict>
<key>com.apple.security.app-sandbox</key> <key>com.apple.security.app-sandbox</key>
<true/> <true/>
<key>com.apple.security.personal-information.location</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
</dict> </dict>
</plist> </plist>

View File

@@ -53,10 +53,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: characters name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.1"
cli_config: cli_config:
dependency: transitive dependency: transitive
description: description:
@@ -105,6 +105,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.15.0" 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: crypto:
dependency: transitive dependency: transitive
description: description:
@@ -137,6 +145,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0" version: "1.0.0"
dbus:
dependency: transitive
description:
name: dbus
sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270
url: "https://pub.dev"
source: hosted
version: "0.7.12"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@@ -198,11 +214,24 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.3.1" 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: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
frontend_server_client: frontend_server_client:
dependency: transitive dependency: transitive
description: description:
@@ -211,6 +240,70 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.0" version: "4.0.0"
geoclue:
dependency: transitive
description:
name: geoclue
sha256: c2a998c77474fc57aa00c6baa2928e58f4b267649057a1c76738656e9dbd2a7f
url: "https://pub.dev"
source: hosted
version: "0.1.1"
geolocator:
dependency: "direct main"
description:
name: geolocator
sha256: "79939537046c9025be47ec645f35c8090ecadb6fe98eba146a0d25e8c1357516"
url: "https://pub.dev"
source: hosted
version: "14.0.2"
geolocator_android:
dependency: transitive
description:
name: geolocator_android
sha256: "179c3cb66dfa674fc9ccbf2be872a02658724d1c067634e2c427cf6df7df901a"
url: "https://pub.dev"
source: hosted
version: "5.0.2"
geolocator_apple:
dependency: transitive
description:
name: geolocator_apple
sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22
url: "https://pub.dev"
source: hosted
version: "2.3.13"
geolocator_linux:
dependency: transitive
description:
name: geolocator_linux
sha256: d64112a205931926f4363bb6bd48f14cb38e7326833041d170615586cd143797
url: "https://pub.dev"
source: hosted
version: "0.2.4"
geolocator_platform_interface:
dependency: transitive
description:
name: geolocator_platform_interface
sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67"
url: "https://pub.dev"
source: hosted
version: "4.2.6"
geolocator_web:
dependency: transitive
description:
name: geolocator_web
sha256: b1ae9bdfd90f861fde8fd4f209c37b953d65e92823cb73c7dee1fa021b06f172
url: "https://pub.dev"
source: hosted
version: "4.1.3"
geolocator_windows:
dependency: transitive
description:
name: geolocator_windows
sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6"
url: "https://pub.dev"
source: hosted
version: "0.2.5"
glob: glob:
dependency: transitive dependency: transitive
description: description:
@@ -219,6 +312,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.3" version: "2.1.3"
gsettings:
dependency: transitive
description:
name: gsettings
sha256: "1b0ce661f5436d2db1e51f3c4295a49849f03d304003a7ba177d01e3a858249c"
url: "https://pub.dev"
source: hosted
version: "0.2.8"
hooks: hooks:
dependency: transitive dependency: transitive
description: description:
@@ -283,14 +384,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.1" version: "1.0.1"
js:
dependency: transitive
description:
name: js
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
url: "https://pub.dev"
source: hosted
version: "0.7.2"
latlong2: latlong2:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -343,18 +436,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.17" version: "0.12.19"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
name: material_color_utilities name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.11.1" version: "0.13.0"
meta: meta:
dependency: transitive dependency: transitive
description: description:
@@ -411,6 +504,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "2.2.0"
package_info_plus:
dependency: transitive
description:
name: package_info_plus
sha256: "468c26b4254ab01979fa5e4a98cb343ea3631b9acee6f21028997419a80e1a20"
url: "https://pub.dev"
source: hosted
version: "9.0.1"
package_info_plus_platform_interface:
dependency: transitive
description:
name: package_info_plus_platform_interface
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
path: path:
dependency: transitive dependency: transitive
description: description:
@@ -419,6 +528,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.1" 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: path_provider:
dependency: transitive dependency: transitive
description: description:
@@ -467,6 +584,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" version: "2.3.0"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
url: "https://pub.dev"
source: hosted
version: "7.0.2"
platform: platform:
dependency: transitive dependency: transitive
description: description:
@@ -515,6 +640,54 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "2.2.0"
record:
dependency: transitive
description:
name: record
sha256: d5b6b334f3ab02460db6544e08583c942dbf23e3504bf1e14fd4cbe3d9409277
url: "https://pub.dev"
source: hosted
version: "6.2.0"
record_android:
dependency: transitive
description:
name: record_android
sha256: "94783f08403aed33ffb68797bf0715b0812eb852f3c7985644c945faea462ba1"
url: "https://pub.dev"
source: hosted
version: "1.5.1"
record_ios:
dependency: transitive
description:
name: record_ios
sha256: "8df7c136131bd05efc19256af29b2ba6ccc000ccc2c80d4b6b6d7a8d21a3b5a9"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
record_linux:
dependency: transitive
description:
name: record_linux
sha256: c31a35cc158cd666fc6395f7f56fc054f31685571684be6b97670a27649ce5c7
url: "https://pub.dev"
source: hosted
version: "1.3.0"
record_macos:
dependency: transitive
description:
name: record_macos
sha256: "084902e63fc9c0c224c29203d6c75f0bdf9b6a40536c9d916393c8f4c4256488"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
record_platform_interface:
dependency: transitive
description:
name: record_platform_interface
sha256: "8a81dbc4e14e1272a285bbfef6c9136d070a47d9b0d1f40aa6193516253ee2f6"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
record_use: record_use:
dependency: transitive dependency: transitive
description: description:
@@ -523,6 +696,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.0" version: "0.6.0"
record_web:
dependency: transitive
description:
name: record_web
sha256: "7e9846981c1f2d111d86f0ae3309071f5bba8b624d1c977316706f08fc31d16d"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
record_windows:
dependency: transitive
description:
name: record_windows
sha256: "223258060a1d25c62bae18282c16783f28581ec19401d17e56b5205b9f039d78"
url: "https://pub.dev"
source: hosted
version: "1.0.7"
riverpod: riverpod:
dependency: transitive dependency: transitive
description: description:
@@ -644,26 +833,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test name: test
sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.26.3" version: "1.30.0"
test_api: test_api:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.7" version: "0.7.10"
test_core: test_core:
dependency: transitive dependency: transitive
description: description:
name: test_core name: test_core
sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.12" version: "0.6.16"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@@ -688,6 +877,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.5.3" 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: vector_math:
dependency: transitive dependency: transitive
description: description:
@@ -712,6 +925,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.1" 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: web:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -744,6 +973,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.1" version: "1.2.1"
win32:
dependency: transitive
description:
name: win32
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
url: "https://pub.dev"
source: hosted
version: "5.15.0"
wkt_parser: wkt_parser:
dependency: transitive dependency: transitive
description: description:
@@ -760,6 +997,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
xml:
dependency: transitive
description:
name: xml
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
url: "https://pub.dev"
source: hosted
version: "6.6.1"
yaml: yaml:
dependency: transitive dependency: transitive
description: description:
@@ -769,5 +1014,5 @@ packages:
source: hosted source: hosted
version: "3.1.3" version: "3.1.3"
sdks: sdks:
dart: ">=3.10.7 <4.0.0" dart: ">=3.11.0 <4.0.0"
flutter: ">=3.38.4" flutter: ">=3.38.4"

View File

@@ -19,7 +19,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1 version: 1.0.0+1
environment: environment:
sdk: ^3.10.7 sdk: ^3.11.0
# Dependencies specify other packages that your package needs in order to work. # Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions # To automatically upgrade your package dependencies to the latest versions
@@ -39,6 +39,10 @@ dependencies:
latlong2: ^0.9.1 latlong2: ^0.9.1
http: ^1.6.0 http: ^1.6.0
web: ^1.1.1 web: ^1.1.1
geolocator: ^14.0.2
flutter_svg: ^2.3.0
waveform_recorder: ^1.8.0
waveform_flutter: ^1.2.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: