Compare commits
45 Commits
f388b7a3d2
...
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 | ||
|
|
f9d6e4fa5b | ||
|
|
35ccfe2368 | ||
|
|
b93f7ec4ec | ||
|
|
02b3521320 | ||
|
|
c02c050607 | ||
|
|
f929790bd6 | ||
|
|
8cf2b29143 | ||
|
|
f5f59d3020 | ||
|
|
b819b51c1f | ||
|
|
56703c887f | ||
|
|
929d3a46d3 |
@@ -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
|
|
||||||
fi
|
|
||||||
docker buildx use "$builder"
|
|
||||||
docker buildx inspect --bootstrap
|
|
||||||
docker buildx build \
|
|
||||||
--push \
|
--push \
|
||||||
--tag "$IMAGE" \
|
--provenance=false \
|
||||||
|
--tag "$IMAGE_SHA" \
|
||||||
|
--tag "$IMAGE_LATEST" \
|
||||||
--build-arg MAPBOX_ACCESS_TOKEN="${{ secrets.MAPBOX_ACCESS_TOKEN }}" \
|
--build-arg MAPBOX_ACCESS_TOKEN="${{ secrets.MAPBOX_ACCESS_TOKEN }}" \
|
||||||
--build-arg MAPBOX_STYLE="mapbox/streets-v12" \
|
--build-arg MAPBOX_STYLE="mapbox/streets-v12" \
|
||||||
--build-arg TELEGRAM_BOT_USERNAME="carfteebot" \
|
--build-arg TELEGRAM_BOT_USERNAME="carfteebot" \
|
||||||
.
|
.; then
|
||||||
|
exit 0
|
||||||
|
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
|
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,14 +129,11 @@ 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
|
||||||
@@ -143,14 +143,15 @@ class MapflowApi {
|
|||||||
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,
|
||||||
|
|||||||
@@ -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) {}
|
||||||
|
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
32
lib/location/current_location.dart
Normal file
32
lib/location/current_location.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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();
|
||||||
|
|||||||
@@ -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"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
287
pubspec.lock
287
pubspec.lock
@@ -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"
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user