Merge branch 'chore/app-store-screenshots'
2
.gitignore
vendored
@@ -73,6 +73,8 @@ GoogleService-Info.plist
|
|||||||
**/fastlane/.env
|
**/fastlane/.env
|
||||||
**/fastlane/.env.*
|
**/fastlane/.env.*
|
||||||
fastlane/README.md
|
fastlane/README.md
|
||||||
|
ios/.bundle/
|
||||||
|
ios/vendor/
|
||||||
|
|
||||||
# Play Store release assets & scripts
|
# Play Store release assets & scripts
|
||||||
play-store/
|
play-store/
|
||||||
|
|||||||
116
integration_test/app_screenshots_test.dart
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
|
import 'package:meshcore_sar_app/l10n/app_localizations.dart';
|
||||||
|
import 'package:meshcore_sar_app/main.dart';
|
||||||
|
import 'package:meshcore_sar_app/providers/contacts_provider.dart';
|
||||||
|
import 'package:meshcore_sar_app/providers/messages_provider.dart';
|
||||||
|
import 'package:meshcore_sar_app/screens/home_screen.dart';
|
||||||
|
import 'package:meshcore_sar_app/services/wizard_preferences.dart';
|
||||||
|
import 'package:meshcore_sar_app/utils/sample_data_generator.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
const screenshotPrefix = String.fromEnvironment('SCREENSHOT_PREFIX');
|
||||||
|
|
||||||
|
testWidgets('captures App Store screenshots', (tester) async {
|
||||||
|
SharedPreferences.setMockInitialValues({
|
||||||
|
'wizard_completed': true,
|
||||||
|
'wizard_version': 1,
|
||||||
|
});
|
||||||
|
await WizardPreferences.setWizardCompleted(true);
|
||||||
|
|
||||||
|
await tester.pumpWidget(const MeshCoreSarApp());
|
||||||
|
await _pumpUntilFound(tester, find.byType(HomeScreen));
|
||||||
|
await binding.convertFlutterSurfaceToImage();
|
||||||
|
|
||||||
|
final homeContext = tester.element(find.byType(HomeScreen));
|
||||||
|
_loadSampleData(homeContext);
|
||||||
|
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||||
|
|
||||||
|
await _takeScreenshot(binding, tester, '${screenshotPrefix}01-messages');
|
||||||
|
|
||||||
|
await _tapTab(tester, 'Contacts');
|
||||||
|
await _takeScreenshot(binding, tester, '${screenshotPrefix}02-contacts');
|
||||||
|
|
||||||
|
await _tapTab(tester, 'Map');
|
||||||
|
await tester.pumpAndSettle(const Duration(seconds: 2));
|
||||||
|
await _takeScreenshot(binding, tester, '${screenshotPrefix}03-map');
|
||||||
|
|
||||||
|
await _openOverflowItem(tester, Icons.more_vert, 'Settings');
|
||||||
|
await _takeScreenshot(binding, tester, '${screenshotPrefix}04-settings');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _loadSampleData(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
const centerLocation = LatLng(46.0569, 14.5058);
|
||||||
|
final contacts = SampleDataGenerator.generateContacts(
|
||||||
|
centerLocation: centerLocation,
|
||||||
|
l10n: l10n,
|
||||||
|
teamMemberCount: 5,
|
||||||
|
channelCount: 2,
|
||||||
|
);
|
||||||
|
final sampleMessages = SampleDataGenerator.generateAllMessages(
|
||||||
|
centerLocation: centerLocation,
|
||||||
|
l10n: l10n,
|
||||||
|
foundPersonCount: 2,
|
||||||
|
fireCount: 1,
|
||||||
|
stagingCount: 1,
|
||||||
|
objectCount: 1,
|
||||||
|
generalChannelMessages: 8,
|
||||||
|
emergencyChannelMessages: 5,
|
||||||
|
);
|
||||||
|
context.read<ContactsProvider>().addContacts(contacts);
|
||||||
|
for (final message in sampleMessages.messages) {
|
||||||
|
context.read<MessagesProvider>().addMessage(
|
||||||
|
message,
|
||||||
|
contactLocationSnapshot: sampleMessages.contactLocations[message.id],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pumpUntilFound(
|
||||||
|
WidgetTester tester,
|
||||||
|
Finder finder, {
|
||||||
|
Duration timeout = const Duration(seconds: 10),
|
||||||
|
}) async {
|
||||||
|
final end = DateTime.now().add(timeout);
|
||||||
|
while (DateTime.now().isBefore(end)) {
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
if (finder.evaluate().isNotEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(finder, findsOneWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _tapTab(WidgetTester tester, String label) async {
|
||||||
|
final tab = find.text(label).last;
|
||||||
|
await tester.tap(tab);
|
||||||
|
await tester.pumpAndSettle(const Duration(milliseconds: 500));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _openOverflowItem(
|
||||||
|
WidgetTester tester,
|
||||||
|
IconData buttonIcon,
|
||||||
|
String label,
|
||||||
|
) async {
|
||||||
|
await tester.tap(find.byIcon(buttonIcon));
|
||||||
|
await tester.pumpAndSettle(const Duration(milliseconds: 500));
|
||||||
|
await tester.tap(find.text(label));
|
||||||
|
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _takeScreenshot(
|
||||||
|
IntegrationTestWidgetsFlutterBinding binding,
|
||||||
|
WidgetTester tester,
|
||||||
|
String name,
|
||||||
|
) async {
|
||||||
|
await tester.pumpAndSettle(const Duration(milliseconds: 500));
|
||||||
|
final bytes = await binding.takeScreenshot(name);
|
||||||
|
expect(bytes.isNotEmpty, isTrue);
|
||||||
|
}
|
||||||
@@ -1,49 +1,47 @@
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
CFPropertyList (3.0.7)
|
CFPropertyList (3.0.9)
|
||||||
base64
|
abbrev (0.1.2)
|
||||||
nkf
|
addressable (2.9.0)
|
||||||
rexml
|
public_suffix (>= 2.0.2, < 8.0)
|
||||||
addressable (2.8.7)
|
|
||||||
public_suffix (>= 2.0.2, < 7.0)
|
|
||||||
artifactory (3.0.17)
|
artifactory (3.0.17)
|
||||||
atomos (0.1.3)
|
atomos (0.1.3)
|
||||||
aws-eventstream (1.4.0)
|
aws-eventstream (1.3.2)
|
||||||
aws-partitions (1.1172.0)
|
aws-partitions (1.1109.0)
|
||||||
aws-sdk-core (3.233.0)
|
aws-sdk-core (3.224.1)
|
||||||
aws-eventstream (~> 1, >= 1.3.0)
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
aws-partitions (~> 1, >= 1.992.0)
|
aws-partitions (~> 1, >= 1.992.0)
|
||||||
aws-sigv4 (~> 1.9)
|
aws-sigv4 (~> 1.9)
|
||||||
base64
|
base64
|
||||||
bigdecimal
|
|
||||||
jmespath (~> 1, >= 1.6.1)
|
jmespath (~> 1, >= 1.6.1)
|
||||||
logger
|
logger
|
||||||
aws-sdk-kms (1.113.0)
|
aws-sdk-kms (1.101.0)
|
||||||
aws-sdk-core (~> 3, >= 3.231.0)
|
aws-sdk-core (~> 3, >= 3.216.0)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sdk-s3 (1.199.1)
|
aws-sdk-s3 (1.188.0)
|
||||||
aws-sdk-core (~> 3, >= 3.231.0)
|
aws-sdk-core (~> 3, >= 3.224.1)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sigv4 (1.12.1)
|
aws-sigv4 (1.11.0)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
babosa (1.0.4)
|
babosa (1.0.4)
|
||||||
base64 (0.3.0)
|
base64 (0.3.0)
|
||||||
bigdecimal (3.3.1)
|
|
||||||
claide (1.1.0)
|
claide (1.1.0)
|
||||||
colored (1.2)
|
colored (1.2)
|
||||||
colored2 (3.1.2)
|
colored2 (3.1.2)
|
||||||
commander (4.6.0)
|
commander (4.6.0)
|
||||||
highline (~> 2.0.0)
|
highline (~> 2.0.0)
|
||||||
|
csv (3.3.5)
|
||||||
declarative (0.0.20)
|
declarative (0.0.20)
|
||||||
digest-crc (0.7.0)
|
digest-crc (0.7.0)
|
||||||
rake (>= 12.0.0, < 14.0.0)
|
rake (>= 12.0.0, < 14.0.0)
|
||||||
domain_name (0.6.20240107)
|
domain_name (0.5.20190701)
|
||||||
|
unf (>= 0.0.5, < 1.0.0)
|
||||||
dotenv (2.8.1)
|
dotenv (2.8.1)
|
||||||
emoji_regex (3.2.3)
|
emoji_regex (3.2.3)
|
||||||
excon (0.112.0)
|
excon (0.109.0)
|
||||||
faraday (1.10.4)
|
faraday (1.10.5)
|
||||||
faraday-em_http (~> 1.0)
|
faraday-em_http (~> 1.0)
|
||||||
faraday-em_synchrony (~> 1.0)
|
faraday-em_synchrony (~> 1.0)
|
||||||
faraday-excon (~> 1.1)
|
faraday-excon (~> 1.1)
|
||||||
@@ -55,25 +53,26 @@ GEM
|
|||||||
faraday-rack (~> 1.0)
|
faraday-rack (~> 1.0)
|
||||||
faraday-retry (~> 1.0)
|
faraday-retry (~> 1.0)
|
||||||
ruby2_keywords (>= 0.0.4)
|
ruby2_keywords (>= 0.0.4)
|
||||||
faraday-cookie_jar (0.0.7)
|
faraday-cookie_jar (0.0.8)
|
||||||
faraday (>= 0.8.0)
|
faraday (>= 0.8.0)
|
||||||
http-cookie (~> 1.0.0)
|
http-cookie (>= 1.0.0)
|
||||||
faraday-em_http (1.0.0)
|
faraday-em_http (1.0.0)
|
||||||
faraday-em_synchrony (1.0.1)
|
faraday-em_synchrony (1.0.1)
|
||||||
faraday-excon (1.1.0)
|
faraday-excon (1.1.0)
|
||||||
faraday-httpclient (1.0.1)
|
faraday-httpclient (1.0.1)
|
||||||
faraday-multipart (1.1.1)
|
faraday-multipart (1.2.0)
|
||||||
multipart-post (~> 2.0)
|
multipart-post (~> 2.0)
|
||||||
faraday-net_http (1.0.2)
|
faraday-net_http (1.0.2)
|
||||||
faraday-net_http_persistent (1.2.0)
|
faraday-net_http_persistent (1.2.0)
|
||||||
faraday-patron (1.0.0)
|
faraday-patron (1.0.0)
|
||||||
faraday-rack (1.0.0)
|
faraday-rack (1.0.0)
|
||||||
faraday-retry (1.0.3)
|
faraday-retry (1.0.4)
|
||||||
faraday_middleware (1.2.1)
|
faraday_middleware (1.2.1)
|
||||||
faraday (~> 1.0)
|
faraday (~> 1.0)
|
||||||
fastimage (2.4.0)
|
fastimage (2.4.1)
|
||||||
fastlane (2.228.0)
|
fastlane (2.229.0)
|
||||||
CFPropertyList (>= 2.3, < 4.0.0)
|
CFPropertyList (>= 2.3, < 4.0.0)
|
||||||
|
abbrev (~> 0.1.2)
|
||||||
addressable (>= 2.8, < 3.0.0)
|
addressable (>= 2.8, < 3.0.0)
|
||||||
artifactory (~> 3.0)
|
artifactory (~> 3.0)
|
||||||
aws-sdk-s3 (~> 1.0)
|
aws-sdk-s3 (~> 1.0)
|
||||||
@@ -81,6 +80,7 @@ GEM
|
|||||||
bundler (>= 1.12.0, < 3.0.0)
|
bundler (>= 1.12.0, < 3.0.0)
|
||||||
colored (~> 1.2)
|
colored (~> 1.2)
|
||||||
commander (~> 4.6)
|
commander (~> 4.6)
|
||||||
|
csv (~> 3.3)
|
||||||
dotenv (>= 2.1.1, < 3.0.0)
|
dotenv (>= 2.1.1, < 3.0.0)
|
||||||
emoji_regex (>= 0.1, < 4.0)
|
emoji_regex (>= 0.1, < 4.0)
|
||||||
excon (>= 0.71.0, < 1.0.0)
|
excon (>= 0.71.0, < 1.0.0)
|
||||||
@@ -100,6 +100,7 @@ GEM
|
|||||||
jwt (>= 2.1.0, < 3)
|
jwt (>= 2.1.0, < 3)
|
||||||
mini_magick (>= 4.9.4, < 5.0.0)
|
mini_magick (>= 4.9.4, < 5.0.0)
|
||||||
multipart-post (>= 2.0.0, < 3.0.0)
|
multipart-post (>= 2.0.0, < 3.0.0)
|
||||||
|
mutex_m (~> 0.3.0)
|
||||||
naturally (~> 2.2)
|
naturally (~> 2.2)
|
||||||
optparse (>= 0.1.1, < 1.0.0)
|
optparse (>= 0.1.1, < 1.0.0)
|
||||||
plist (>= 3.1.0, < 4.0.0)
|
plist (>= 3.1.0, < 4.0.0)
|
||||||
@@ -114,8 +115,7 @@ GEM
|
|||||||
xcodeproj (>= 1.13.0, < 2.0.0)
|
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||||
xcpretty (~> 0.4.1)
|
xcpretty (~> 0.4.1)
|
||||||
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
||||||
fastlane-sirp (1.0.0)
|
fastlane-sirp (1.1.0)
|
||||||
sysrandom (~> 1.0)
|
|
||||||
gh_inspector (1.1.3)
|
gh_inspector (1.1.3)
|
||||||
google-apis-androidpublisher_v3 (0.54.0)
|
google-apis-androidpublisher_v3 (0.54.0)
|
||||||
google-apis-core (>= 0.11.0, < 2.a)
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
@@ -131,19 +131,19 @@ GEM
|
|||||||
google-apis-core (>= 0.11.0, < 2.a)
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
google-apis-playcustomapp_v1 (0.13.0)
|
google-apis-playcustomapp_v1 (0.13.0)
|
||||||
google-apis-core (>= 0.11.0, < 2.a)
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
google-apis-storage_v1 (0.31.0)
|
google-apis-storage_v1 (0.32.0)
|
||||||
google-apis-core (>= 0.11.0, < 2.a)
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
google-cloud-core (1.8.0)
|
google-cloud-core (1.6.1)
|
||||||
google-cloud-env (>= 1.0, < 3.a)
|
google-cloud-env (>= 1.0, < 3.a)
|
||||||
google-cloud-errors (~> 1.0)
|
google-cloud-errors (~> 1.0)
|
||||||
google-cloud-env (1.6.0)
|
google-cloud-env (1.6.0)
|
||||||
faraday (>= 0.17.3, < 3.0)
|
faraday (>= 0.17.3, < 3.0)
|
||||||
google-cloud-errors (1.5.0)
|
google-cloud-errors (1.3.1)
|
||||||
google-cloud-storage (1.47.0)
|
google-cloud-storage (1.37.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
digest-crc (~> 0.4)
|
digest-crc (~> 0.4)
|
||||||
google-apis-iamcredentials_v1 (~> 0.1)
|
google-apis-iamcredentials_v1 (~> 0.1)
|
||||||
google-apis-storage_v1 (~> 0.31.0)
|
google-apis-storage_v1 (~> 0.1)
|
||||||
google-cloud-core (~> 1.6)
|
google-cloud-core (~> 1.6)
|
||||||
googleauth (>= 0.16.2, < 2.a)
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
mini_mime (~> 1.0)
|
mini_mime (~> 1.0)
|
||||||
@@ -159,42 +159,40 @@ GEM
|
|||||||
httpclient (2.9.0)
|
httpclient (2.9.0)
|
||||||
mutex_m
|
mutex_m
|
||||||
jmespath (1.6.2)
|
jmespath (1.6.2)
|
||||||
json (2.15.1)
|
json (2.7.6)
|
||||||
jwt (2.10.2)
|
jwt (2.10.3)
|
||||||
base64
|
base64
|
||||||
logger (1.7.0)
|
logger (1.7.0)
|
||||||
mini_magick (4.13.2)
|
mini_magick (4.13.2)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
multi_json (1.17.0)
|
multi_json (1.15.0)
|
||||||
multipart-post (2.4.1)
|
multipart-post (2.4.1)
|
||||||
mutex_m (0.3.0)
|
mutex_m (0.3.0)
|
||||||
nanaimo (0.4.0)
|
nanaimo (0.4.0)
|
||||||
naturally (2.3.0)
|
naturally (2.3.0)
|
||||||
nkf (0.2.0)
|
optparse (0.8.1)
|
||||||
optparse (0.6.0)
|
|
||||||
os (1.1.4)
|
os (1.1.4)
|
||||||
plist (3.7.2)
|
plist (3.7.2)
|
||||||
public_suffix (6.0.2)
|
public_suffix (5.1.1)
|
||||||
rake (13.3.0)
|
rake (13.4.2)
|
||||||
representable (3.2.0)
|
representable (3.2.0)
|
||||||
declarative (< 0.1.0)
|
declarative (< 0.1.0)
|
||||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||||
uber (< 0.2.0)
|
uber (< 0.2.0)
|
||||||
retriable (3.1.2)
|
retriable (3.8.0)
|
||||||
rexml (3.4.4)
|
rexml (3.4.4)
|
||||||
rouge (3.28.0)
|
rouge (3.28.0)
|
||||||
ruby2_keywords (0.0.5)
|
ruby2_keywords (0.0.5)
|
||||||
rubyzip (2.4.1)
|
rubyzip (2.4.1)
|
||||||
security (0.1.5)
|
security (0.1.5)
|
||||||
signet (0.21.0)
|
signet (0.18.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
faraday (>= 0.17.5, < 3.a)
|
faraday (>= 0.17.5, < 3.a)
|
||||||
jwt (>= 1.5, < 4.0)
|
jwt (>= 1.5, < 3.0)
|
||||||
multi_json (~> 1.10)
|
multi_json (~> 1.10)
|
||||||
simctl (1.6.10)
|
simctl (1.6.10)
|
||||||
CFPropertyList
|
CFPropertyList
|
||||||
naturally
|
naturally
|
||||||
sysrandom (1.0.5)
|
|
||||||
terminal-notifier (2.0.0)
|
terminal-notifier (2.0.0)
|
||||||
terminal-table (3.0.2)
|
terminal-table (3.0.2)
|
||||||
unicode-display_width (>= 1.1.1, < 3)
|
unicode-display_width (>= 1.1.1, < 3)
|
||||||
@@ -204,6 +202,7 @@ GEM
|
|||||||
tty-spinner (0.9.3)
|
tty-spinner (0.9.3)
|
||||||
tty-cursor (~> 0.7)
|
tty-cursor (~> 0.7)
|
||||||
uber (0.1.0)
|
uber (0.1.0)
|
||||||
|
unf (0.2.0)
|
||||||
unicode-display_width (2.6.0)
|
unicode-display_width (2.6.0)
|
||||||
word_wrap (1.0.0)
|
word_wrap (1.0.0)
|
||||||
xcodeproj (1.27.0)
|
xcodeproj (1.27.0)
|
||||||
@@ -226,4 +225,4 @@ DEPENDENCIES
|
|||||||
fastlane
|
fastlane
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.7.2
|
2.4.22
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ PODS:
|
|||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- image_picker_ios (0.0.1):
|
- image_picker_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- integration_test (0.0.1):
|
||||||
|
- Flutter
|
||||||
- nsd_ios (0.0.1):
|
- nsd_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- package_info_plus (0.4.5):
|
- package_info_plus (0.4.5):
|
||||||
@@ -100,6 +102,7 @@ DEPENDENCIES:
|
|||||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||||
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
|
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
|
||||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||||
|
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
||||||
- nsd_ios (from `.symlinks/plugins/nsd_ios/ios`)
|
- nsd_ios (from `.symlinks/plugins/nsd_ios/ios`)
|
||||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||||
@@ -144,6 +147,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/geolocator_apple/darwin"
|
:path: ".symlinks/plugins/geolocator_apple/darwin"
|
||||||
image_picker_ios:
|
image_picker_ios:
|
||||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||||
|
integration_test:
|
||||||
|
:path: ".symlinks/plugins/integration_test/ios"
|
||||||
nsd_ios:
|
nsd_ios:
|
||||||
:path: ".symlinks/plugins/nsd_ios/ios"
|
:path: ".symlinks/plugins/nsd_ios/ios"
|
||||||
package_info_plus:
|
package_info_plus:
|
||||||
@@ -182,6 +187,7 @@ SPEC CHECKSUMS:
|
|||||||
flutter_local_notifications: 643a3eda1ce1c0599413ca31672536d423dee214
|
flutter_local_notifications: 643a3eda1ce1c0599413ca31672536d423dee214
|
||||||
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
|
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
|
||||||
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
||||||
|
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
|
||||||
nsd_ios: 596ad79109ddd3e52d665f650f36428049e3653e
|
nsd_ios: 596ad79109ddd3e52d665f650f36428049e3653e
|
||||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||||
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
||||||
|
|||||||
@@ -16,17 +16,66 @@
|
|||||||
default_platform(:ios)
|
default_platform(:ios)
|
||||||
|
|
||||||
platform :ios do
|
platform :ios do
|
||||||
|
def project_root
|
||||||
|
File.expand_path("../..", __dir__)
|
||||||
|
end
|
||||||
|
|
||||||
|
def beta_app_info
|
||||||
|
{
|
||||||
|
"en-US" => {
|
||||||
|
description: "Field-ready SAR coordination over MeshCore with chat, voice, images, offline maps, and live team context.",
|
||||||
|
feedback_email: "hey@dz0ny.dev",
|
||||||
|
marketing_url: "https://github.com/dz0ny/meshcore-sar",
|
||||||
|
privacy_policy_url: "https://github.com/dz0ny/meshcore-sar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def beta_build_info
|
||||||
|
{
|
||||||
|
"en-US" => {
|
||||||
|
whats_new: "Test MeshCore SAR field coordination flows: messaging, maps, contacts, voice, images, and SAR markers."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
desc "Push a new release build to the App Store"
|
desc "Push a new release build to the App Store"
|
||||||
lane :release do
|
lane :release do
|
||||||
increment_build_number(xcodeproj: "Runner.xcodeproj")
|
increment_build_number(xcodeproj: "Runner.xcodeproj")
|
||||||
build_app(workspace: "Runner.xcworkspace", scheme: "Runner")
|
build_app(workspace: "Runner.xcworkspace", scheme: "Runner")
|
||||||
upload_to_app_store(skip_metadata: true, skip_screenshots: true)
|
upload_to_testflight(
|
||||||
|
skip_waiting_for_build_processing: true,
|
||||||
|
localized_app_info: beta_app_info,
|
||||||
|
localized_build_info: beta_build_info,
|
||||||
|
changelog: "Test MeshCore SAR field coordination flows: messaging, maps, contacts, voice, images, and SAR markers."
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "Push a new beta build to TestFlight"
|
desc "Push a new beta build to TestFlight"
|
||||||
lane :beta do
|
lane :beta do
|
||||||
increment_build_number(xcodeproj: "Runner.xcodeproj")
|
increment_build_number(xcodeproj: "Runner.xcodeproj")
|
||||||
build_app(workspace: "Runner.xcworkspace", scheme: "Runner")
|
build_app(workspace: "Runner.xcworkspace", scheme: "Runner")
|
||||||
upload_to_testflight(skip_waiting_for_build_processing: true)
|
upload_to_testflight(
|
||||||
|
skip_waiting_for_build_processing: true,
|
||||||
|
localized_app_info: beta_app_info,
|
||||||
|
localized_build_info: beta_build_info,
|
||||||
|
changelog: "Test MeshCore SAR field coordination flows: messaging, maps, contacts, voice, images, and SAR markers."
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "Capture iOS screenshots for App Store Connect"
|
||||||
|
lane :screenshots do
|
||||||
|
sh("cd #{project_root} && scripts/take_screenshots.sh --ios")
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "Upload App Store screenshots"
|
||||||
|
lane :store_assets do
|
||||||
|
upload_to_app_store(
|
||||||
|
skip_binary_upload: true,
|
||||||
|
skip_metadata: true,
|
||||||
|
skip_screenshots: false,
|
||||||
|
submit_for_review: false,
|
||||||
|
force: true
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -31,6 +31,22 @@ Push a new release build to the App Store
|
|||||||
|
|
||||||
Push a new beta build to TestFlight
|
Push a new beta build to TestFlight
|
||||||
|
|
||||||
|
### ios screenshots
|
||||||
|
|
||||||
|
```sh
|
||||||
|
[bundle exec] fastlane ios screenshots
|
||||||
|
```
|
||||||
|
|
||||||
|
Capture iOS screenshots for App Store Connect
|
||||||
|
|
||||||
|
### ios store_assets
|
||||||
|
|
||||||
|
```sh
|
||||||
|
[bundle exec] fastlane ios store_assets
|
||||||
|
```
|
||||||
|
|
||||||
|
Upload App Store screenshots
|
||||||
|
|
||||||
----
|
----
|
||||||
|
|
||||||
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
|
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
|
||||||
|
|||||||
1
ios/fastlane/metadata/copyright.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Copyright 2026 Janez Troha
|
||||||
14
ios/fastlane/metadata/en-US/description.txt
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
MeshCore SAR gives search and rescue teams a field-ready coordination workspace for low-connectivity operations.
|
||||||
|
|
||||||
|
Use mesh chat, live location context, SAR markers, offline-first maps, tactical drawings, voice clips, and compressed image transfer in one app. The app is built around MeshCore and BLE so teams can coordinate over LoRa links when normal infrastructure is weak or unavailable.
|
||||||
|
|
||||||
|
Key capabilities:
|
||||||
|
- Direct and group mesh messaging
|
||||||
|
- Live team contacts with signal, battery, and location context
|
||||||
|
- Offline maps with SAR markers and tactical overlays
|
||||||
|
- Voice clips optimized for low-bandwidth links
|
||||||
|
- Image transfer with compact encoding
|
||||||
|
- GPX trail import/export
|
||||||
|
- Incident, staging, and discovery workflows for field teams
|
||||||
|
|
||||||
|
MeshCore SAR is intended for search and rescue field teams, incident command, and operators who need practical coordination tools outside normal coverage.
|
||||||
1
ios/fastlane/metadata/en-US/keywords.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
meshcore,sar,search,rescue,mesh,lora,offline,maps,radio,gps
|
||||||
1
ios/fastlane/metadata/en-US/marketing_url.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
https://github.com/dz0ny/meshcore-sar
|
||||||
1
ios/fastlane/metadata/en-US/name.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
MeshCore SAR
|
||||||
1
ios/fastlane/metadata/en-US/privacy_url.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
https://github.com/dz0ny/meshcore-sar
|
||||||
1
ios/fastlane/metadata/en-US/promotional_text.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Field coordination for search and rescue teams using MeshCore, BLE, offline maps, messaging, voice, and SAR markers.
|
||||||
1
ios/fastlane/metadata/en-US/release_notes.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Adds App Store screenshots and TestFlight metadata for MeshCore SAR publishing.
|
||||||
1
ios/fastlane/metadata/en-US/subtitle.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
SAR mesh field coordination
|
||||||
1
ios/fastlane/metadata/en-US/support_url.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
https://github.com/dz0ny/meshcore-sar/issues
|
||||||
@@ -5,22 +5,12 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000252">
|
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000199">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.48394">
|
<testcase classname="fastlane.lanes" name="1: upload_to_app_store" time="37.324492">
|
||||||
|
|
||||||
</testcase>
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="2: build_app" time="153.436107">
|
|
||||||
|
|
||||||
</testcase>
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="3: upload_to_app_store" time="6642.682605">
|
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|||||||
BIN
ios/fastlane/screenshots/en-US/01-messages.png
Normal file
|
After Width: | Height: | Size: 584 KiB |
BIN
ios/fastlane/screenshots/en-US/02-contacts.png
Normal file
|
After Width: | Height: | Size: 595 KiB |
BIN
ios/fastlane/screenshots/en-US/03-map.png
Normal file
|
After Width: | Height: | Size: 3.9 MiB |
BIN
ios/fastlane/screenshots/en-US/04-settings.png
Normal file
|
After Width: | Height: | Size: 538 KiB |
BIN
ios/fastlane/screenshots/en-US/ipad-01-messages.png
Normal file
|
After Width: | Height: | Size: 500 KiB |
BIN
ios/fastlane/screenshots/en-US/ipad-02-contacts.png
Normal file
|
After Width: | Height: | Size: 526 KiB |
BIN
ios/fastlane/screenshots/en-US/ipad-03-map.png
Normal file
|
After Width: | Height: | Size: 7.0 MiB |
BIN
ios/fastlane/screenshots/en-US/ipad-04-settings.png
Normal file
|
After Width: | Height: | Size: 486 KiB |
@@ -50,6 +50,9 @@ class MeshCoreSarApp extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
|
class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
|
||||||
|
static const bool _isScreenshotRun = bool.fromEnvironment(
|
||||||
|
'MESHCORE_SCREENSHOTS',
|
||||||
|
);
|
||||||
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
|
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
|
||||||
AppThemeMode _themeMode = AppThemeMode.system;
|
AppThemeMode _themeMode = AppThemeMode.system;
|
||||||
Locale? _locale;
|
Locale? _locale;
|
||||||
@@ -87,7 +90,9 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
|
|||||||
_pendingNotificationPayload = NotificationService().consumeLaunchPayload();
|
_pendingNotificationPayload = NotificationService().consumeLaunchPayload();
|
||||||
|
|
||||||
// Check if we need to request location permissions
|
// Check if we need to request location permissions
|
||||||
|
if (!_isScreenshotRun) {
|
||||||
await _checkLocationPermissions();
|
await _checkLocationPermissions();
|
||||||
|
}
|
||||||
|
|
||||||
// Check for app updates (Android only) - runs in background
|
// Check for app updates (Android only) - runs in background
|
||||||
// Shows notification if update is available
|
// Shows notification if update is available
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ class OfflineTilesProvider extends ChangeNotifier {
|
|||||||
TileDownloadService? _downloadService;
|
TileDownloadService? _downloadService;
|
||||||
StreamSubscription<TileDownloadEvent>? _downloadSubscription;
|
StreamSubscription<TileDownloadEvent>? _downloadSubscription;
|
||||||
StreamSubscription<Set<TilePeer>>? _peersSubscription;
|
StreamSubscription<Set<TilePeer>>? _peersSubscription;
|
||||||
|
bool _isDisposed = false;
|
||||||
|
|
||||||
// Drawing state
|
// Drawing state
|
||||||
DrawingMode _drawingMode = DrawingMode.none;
|
DrawingMode _drawingMode = DrawingMode.none;
|
||||||
@@ -436,8 +437,10 @@ class OfflineTilesProvider extends ChangeNotifier {
|
|||||||
_peersSubscription = null;
|
_peersSubscription = null;
|
||||||
await _sharing.stopPeerDiscovery();
|
await _sharing.stopPeerDiscovery();
|
||||||
_discoveredPeers = {};
|
_discoveredPeers = {};
|
||||||
|
if (!_isDisposed) {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void addManualPeer(String ipAddress) {
|
void addManualPeer(String ipAddress) {
|
||||||
_sharing.addManualPeer(ipAddress);
|
_sharing.addManualPeer(ipAddress);
|
||||||
@@ -569,6 +572,7 @@ class OfflineTilesProvider extends ChangeNotifier {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_isDisposed = true;
|
||||||
_downloadSubscription?.cancel();
|
_downloadSubscription?.cancel();
|
||||||
_downloadService?.dispose();
|
_downloadService?.dispose();
|
||||||
_peersSubscription?.cancel();
|
_peersSubscription?.cancel();
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ class ContactsTab extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ContactsTabState extends State<ContactsTab> {
|
class _ContactsTabState extends State<ContactsTab> {
|
||||||
|
static const bool _isScreenshotRun = bool.fromEnvironment(
|
||||||
|
'MESHCORE_SCREENSHOTS',
|
||||||
|
);
|
||||||
Position? _currentPosition;
|
Position? _currentPosition;
|
||||||
final Map<ContactSection, String> _sectionFilters = {
|
final Map<ContactSection, String> _sectionFilters = {
|
||||||
ContactSection.teamMembers: '',
|
ContactSection.teamMembers: '',
|
||||||
@@ -62,7 +65,9 @@ class _ContactsTabState extends State<ContactsTab> {
|
|||||||
for (final section in ContactSection.values)
|
for (final section in ContactSection.values)
|
||||||
section: TextEditingController(text: _sectionFilters[section] ?? ''),
|
section: TextEditingController(text: _sectionFilters[section] ?? ''),
|
||||||
};
|
};
|
||||||
|
if (!_isScreenshotRun) {
|
||||||
_getCurrentLocation();
|
_getCurrentLocation();
|
||||||
|
}
|
||||||
// Mark all contacts as viewed when tab is opened
|
// Mark all contacts as viewed when tab is opened
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
context.read<ContactsProvider>().markAllAsViewed();
|
context.read<ContactsProvider>().markAllAsViewed();
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ class NotificationService {
|
|||||||
factory NotificationService() => _instance;
|
factory NotificationService() => _instance;
|
||||||
NotificationService._internal();
|
NotificationService._internal();
|
||||||
|
|
||||||
|
static const bool _skipPermissionRequests = bool.fromEnvironment(
|
||||||
|
'MESHCORE_SCREENSHOTS',
|
||||||
|
);
|
||||||
final FlutterLocalNotificationsPlugin _notificationsPlugin =
|
final FlutterLocalNotificationsPlugin _notificationsPlugin =
|
||||||
FlutterLocalNotificationsPlugin();
|
FlutterLocalNotificationsPlugin();
|
||||||
static const String _prefMessagesEnabled = 'notifications_messages_enabled';
|
static const String _prefMessagesEnabled = 'notifications_messages_enabled';
|
||||||
@@ -101,9 +104,9 @@ class NotificationService {
|
|||||||
|
|
||||||
// iOS initialization settings
|
// iOS initialization settings
|
||||||
final darwinSettings = DarwinInitializationSettings(
|
final darwinSettings = DarwinInitializationSettings(
|
||||||
requestAlertPermission: true,
|
requestAlertPermission: !_skipPermissionRequests,
|
||||||
requestBadgePermission: true,
|
requestBadgePermission: !_skipPermissionRequests,
|
||||||
requestSoundPermission: true,
|
requestSoundPermission: !_skipPermissionRequests,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Combined initialization settings
|
// Combined initialization settings
|
||||||
@@ -125,7 +128,11 @@ class NotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Request permissions
|
// Request permissions
|
||||||
|
if (_skipPermissionRequests) {
|
||||||
|
_permissionGranted = true;
|
||||||
|
} else {
|
||||||
await _requestPermissions();
|
await _requestPermissions();
|
||||||
|
}
|
||||||
await _loadPreferences();
|
await _loadPreferences();
|
||||||
|
|
||||||
// Create notification channels (Android)
|
// Create notification channels (Android)
|
||||||
@@ -338,6 +345,8 @@ class NotificationService {
|
|||||||
String? notes,
|
String? notes,
|
||||||
AppLocalizations? localizations,
|
AppLocalizations? localizations,
|
||||||
}) async {
|
}) async {
|
||||||
|
if (_skipPermissionRequests) return;
|
||||||
|
|
||||||
if (!_isInitialized) {
|
if (!_isInitialized) {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'⚠️ [NotificationService] Not initialized, skipping notification',
|
'⚠️ [NotificationService] Not initialized, skipping notification',
|
||||||
@@ -520,6 +529,8 @@ class NotificationService {
|
|||||||
String? channelName,
|
String? channelName,
|
||||||
AppLocalizations? localizations,
|
AppLocalizations? localizations,
|
||||||
}) async {
|
}) async {
|
||||||
|
if (_skipPermissionRequests) return;
|
||||||
|
|
||||||
if (!_isInitialized) {
|
if (!_isInitialized) {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'⚠️ [NotificationService] Not initialized, skipping notification',
|
'⚠️ [NotificationService] Not initialized, skipping notification',
|
||||||
@@ -722,6 +733,8 @@ class NotificationService {
|
|||||||
required String downloadUrl,
|
required String downloadUrl,
|
||||||
AppLocalizations? localizations,
|
AppLocalizations? localizations,
|
||||||
}) async {
|
}) async {
|
||||||
|
if (_skipPermissionRequests) return;
|
||||||
|
|
||||||
if (!_isInitialized) {
|
if (!_isInitialized) {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'⚠️ [NotificationService] Not initialized, skipping notification',
|
'⚠️ [NotificationService] Not initialized, skipping notification',
|
||||||
@@ -822,6 +835,8 @@ class NotificationService {
|
|||||||
required double batteryPercent,
|
required double batteryPercent,
|
||||||
required bool isCurrentDevice,
|
required bool isCurrentDevice,
|
||||||
}) async {
|
}) async {
|
||||||
|
if (_skipPermissionRequests) return false;
|
||||||
|
|
||||||
if (!_isInitialized) {
|
if (!_isInitialized) {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'⚠️ [NotificationService] Not initialized, skipping notification',
|
'⚠️ [NotificationService] Not initialized, skipping notification',
|
||||||
@@ -913,6 +928,8 @@ class NotificationService {
|
|||||||
required String contactKey,
|
required String contactKey,
|
||||||
String? contactName,
|
String? contactName,
|
||||||
}) async {
|
}) async {
|
||||||
|
if (_skipPermissionRequests) return false;
|
||||||
|
|
||||||
if (!_isInitialized) return false;
|
if (!_isInitialized) return false;
|
||||||
if (!_permissionGranted) return false;
|
if (!_permissionGranted) return false;
|
||||||
if (!_discoveryNotificationsEnabled) return false;
|
if (!_discoveryNotificationsEnabled) return false;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ class VoicePlayerService {
|
|||||||
final AudioPlayer _player = AudioPlayer();
|
final AudioPlayer _player = AudioPlayer();
|
||||||
final StreamController<void> _events = StreamController<void>.broadcast();
|
final StreamController<void> _events = StreamController<void>.broadcast();
|
||||||
bool _isPlaying = false;
|
bool _isPlaying = false;
|
||||||
|
bool _isDisposed = false;
|
||||||
Duration _position = Duration.zero;
|
Duration _position = Duration.zero;
|
||||||
Duration _duration = Duration.zero;
|
Duration _duration = Duration.zero;
|
||||||
Timer? _fallbackTicker;
|
Timer? _fallbackTicker;
|
||||||
@@ -30,22 +31,22 @@ class VoicePlayerService {
|
|||||||
} else {
|
} else {
|
||||||
_stopFallbackTicker();
|
_stopFallbackTicker();
|
||||||
}
|
}
|
||||||
_events.add(null);
|
_emit();
|
||||||
});
|
});
|
||||||
_player.onLog.listen((msg) => debugPrint('🔊 [VoicePlayer] log: $msg'));
|
_player.onLog.listen((msg) => debugPrint('🔊 [VoicePlayer] log: $msg'));
|
||||||
_player.onPositionChanged.listen((position) {
|
_player.onPositionChanged.listen((position) {
|
||||||
_position = position;
|
_position = position;
|
||||||
_events.add(null);
|
_emit();
|
||||||
});
|
});
|
||||||
_player.onDurationChanged.listen((duration) {
|
_player.onDurationChanged.listen((duration) {
|
||||||
_duration = duration;
|
_duration = duration;
|
||||||
_events.add(null);
|
_emit();
|
||||||
});
|
});
|
||||||
_player.onPlayerComplete.listen((_) {
|
_player.onPlayerComplete.listen((_) {
|
||||||
_isPlaying = false;
|
_isPlaying = false;
|
||||||
_position = _duration;
|
_position = _duration;
|
||||||
_stopFallbackTicker();
|
_stopFallbackTicker();
|
||||||
_events.add(null);
|
_emit();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +58,7 @@ class VoicePlayerService {
|
|||||||
milliseconds: (pcmSamples.length * 1000) ~/ sampleRateHz,
|
milliseconds: (pcmSamples.length * 1000) ~/ sampleRateHz,
|
||||||
);
|
);
|
||||||
_playbackStartedAt = DateTime.now();
|
_playbackStartedAt = DateTime.now();
|
||||||
_events.add(null);
|
_emit();
|
||||||
|
|
||||||
final wavBytes = _buildWav(pcmSamples, sampleRate: sampleRateHz);
|
final wavBytes = _buildWav(pcmSamples, sampleRate: sampleRateHz);
|
||||||
final tmpDir = await getTemporaryDirectory();
|
final tmpDir = await getTemporaryDirectory();
|
||||||
@@ -70,14 +71,14 @@ class VoicePlayerService {
|
|||||||
try {
|
try {
|
||||||
_isPlaying = true;
|
_isPlaying = true;
|
||||||
_startFallbackTicker();
|
_startFallbackTicker();
|
||||||
_events.add(null);
|
_emit();
|
||||||
await _player.play(DeviceFileSource(file.path));
|
await _player.play(DeviceFileSource(file.path));
|
||||||
debugPrint('🔊 [VoicePlayer] play() returned (audio playing)');
|
debugPrint('🔊 [VoicePlayer] play() returned (audio playing)');
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint('❌ [VoicePlayer] play() error: $e\n$st');
|
debugPrint('❌ [VoicePlayer] play() error: $e\n$st');
|
||||||
_isPlaying = false;
|
_isPlaying = false;
|
||||||
_stopFallbackTicker();
|
_stopFallbackTicker();
|
||||||
_events.add(null);
|
_emit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,15 +89,22 @@ class VoicePlayerService {
|
|||||||
_position = Duration.zero;
|
_position = Duration.zero;
|
||||||
_playbackStartedAt = null;
|
_playbackStartedAt = null;
|
||||||
_stopFallbackTicker();
|
_stopFallbackTicker();
|
||||||
_events.add(null);
|
_emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_isDisposed = true;
|
||||||
_stopFallbackTicker();
|
_stopFallbackTicker();
|
||||||
_events.close();
|
_events.close();
|
||||||
_player.dispose();
|
_player.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _emit() {
|
||||||
|
if (!_isDisposed) {
|
||||||
|
_events.add(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _startFallbackTicker() {
|
void _startFallbackTicker() {
|
||||||
if (_fallbackTicker != null) return;
|
if (_fallbackTicker != null) return;
|
||||||
_fallbackTicker = Timer.periodic(const Duration(milliseconds: 100), (_) {
|
_fallbackTicker = Timer.periodic(const Duration(milliseconds: 100), (_) {
|
||||||
@@ -107,7 +115,7 @@ class VoicePlayerService {
|
|||||||
final clamped = elapsed > _duration ? _duration : elapsed;
|
final clamped = elapsed > _duration ? _duration : elapsed;
|
||||||
if (clamped > _position) {
|
if (clamped > _position) {
|
||||||
_position = clamped;
|
_position = clamped;
|
||||||
_events.add(null);
|
_emit();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
39
pubspec.lock
@@ -503,6 +503,11 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.1"
|
version: "0.8.1"
|
||||||
|
flutter_driver:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
flutter_launcher_icons:
|
flutter_launcher_icons:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@@ -591,6 +596,11 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
fuchsia_remote_debug_protocol:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
geoclue:
|
geoclue:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -767,6 +777,11 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.2"
|
version: "0.2.2"
|
||||||
|
integration_test:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
intl:
|
intl:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1112,6 +1127,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.5.0"
|
version: "6.5.0"
|
||||||
|
process:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: process
|
||||||
|
sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.0.5"
|
||||||
proj4dart:
|
proj4dart:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1405,6 +1428,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.1"
|
version: "1.4.1"
|
||||||
|
sync_http:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sync_http
|
||||||
|
sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.1"
|
||||||
synchronized:
|
synchronized:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1589,6 +1620,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.1.1"
|
||||||
|
webdriver:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: webdriver
|
||||||
|
sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.0"
|
||||||
webserial:
|
webserial:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -129,9 +129,15 @@ dependencies:
|
|||||||
webserial: ^1.2.0
|
webserial: ^1.2.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
flutter_driver:
|
||||||
|
sdk: flutter
|
||||||
|
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
|
integration_test:
|
||||||
|
sdk: flutter
|
||||||
|
|
||||||
# The "flutter_lints" package below contains a set of recommended lints to
|
# The "flutter_lints" package below contains a set of recommended lints to
|
||||||
# encourage good coding practices. The lint set provided by the package is
|
# encourage good coding practices. The lint set provided by the package is
|
||||||
# activated in the `analysis_options.yaml` file located at the root of your
|
# activated in the `analysis_options.yaml` file located at the root of your
|
||||||
|
|||||||
@@ -14,12 +14,18 @@ NC='\033[0m' # No Color
|
|||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
OUTPUT_DIR="screenshots"
|
OUTPUT_DIR="screenshots"
|
||||||
|
IOS_OUTPUT_DIR="ios/fastlane/screenshots/en-US"
|
||||||
|
IOS_SCREENSHOT_WIDTH=1284
|
||||||
|
IOS_SCREENSHOT_HEIGHT=2778
|
||||||
|
IPAD_SCREENSHOT_WIDTH=2048
|
||||||
|
IPAD_SCREENSHOT_HEIGHT=2732
|
||||||
INTEGRATION_TEST="integration_test/app_screenshots_test.dart"
|
INTEGRATION_TEST="integration_test/app_screenshots_test.dart"
|
||||||
|
|
||||||
# Device configurations for App Store screenshots
|
# Device configurations for App Store screenshots
|
||||||
# iOS devices (required sizes: 6.7", 6.5", 5.5")
|
# iOS devices (required sizes: 6.7", 6.5", 5.5")
|
||||||
IOS_DEVICES=(
|
IOS_DEVICES=(
|
||||||
"iPhone Air" # 6.3" - 1206x2622 (newer large format)
|
"iPhone 17 Pro Max" # 6.9" - App Store large phone format
|
||||||
|
"iPad Pro 13-inch (M5)" # 13" - required iPad format
|
||||||
)
|
)
|
||||||
|
|
||||||
# Android devices (phone + tablet recommended)
|
# Android devices (phone + tablet recommended)
|
||||||
@@ -45,7 +51,7 @@ mkdir -p "$OUTPUT_DIR"
|
|||||||
# Function to list available devices
|
# Function to list available devices
|
||||||
list_devices() {
|
list_devices() {
|
||||||
echo -e "${YELLOW}📱 Available iOS Simulators:${NC}"
|
echo -e "${YELLOW}📱 Available iOS Simulators:${NC}"
|
||||||
xcrun simctl list devices available | grep "iPhone" | grep -v "unavailable"
|
xcrun simctl list devices available | grep -E "iPhone|iPad" | grep -v "unavailable"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${YELLOW}🤖 Available Android Emulators:${NC}"
|
echo -e "${YELLOW}🤖 Available Android Emulators:${NC}"
|
||||||
emulator -list-avds
|
emulator -list-avds
|
||||||
@@ -74,23 +80,46 @@ take_ios_screenshots() {
|
|||||||
|
|
||||||
echo -e "${BLUE} Device ID: $device_id${NC}"
|
echo -e "${BLUE} Device ID: $device_id${NC}"
|
||||||
|
|
||||||
# Boot the simulator if not already booted
|
xcrun simctl shutdown "$device_id" 2>/dev/null || true
|
||||||
xcrun simctl boot "$device_id" 2>/dev/null || true
|
xcrun simctl boot "$device_id" 2>/dev/null || true
|
||||||
sleep 3
|
sleep 3
|
||||||
|
xcrun simctl uninstall "$device_id" com.meshcore.sar.meshcoreSarApp 2>/dev/null || true
|
||||||
|
xcrun simctl privacy "$device_id" grant notifications com.meshcore.sar.meshcoreSarApp 2>/dev/null || true
|
||||||
|
xcrun simctl privacy "$device_id" grant location com.meshcore.sar.meshcoreSarApp 2>/dev/null || true
|
||||||
|
|
||||||
# Create device-specific output directory
|
# Create device-specific output directory
|
||||||
local device_dir="$OUTPUT_DIR/ios/${device_name// /_}"
|
local device_dir="$IOS_OUTPUT_DIR"
|
||||||
|
local screenshot_prefix=""
|
||||||
|
local screenshot_width="$IOS_SCREENSHOT_WIDTH"
|
||||||
|
local screenshot_height="$IOS_SCREENSHOT_HEIGHT"
|
||||||
|
local remove_pattern="[0-9][0-9]-*.png"
|
||||||
|
if [[ "$device_name" == *"iPad"* ]]; then
|
||||||
|
screenshot_prefix="ipad-"
|
||||||
|
screenshot_width="$IPAD_SCREENSHOT_WIDTH"
|
||||||
|
screenshot_height="$IPAD_SCREENSHOT_HEIGHT"
|
||||||
|
remove_pattern="ipad-*.png"
|
||||||
|
fi
|
||||||
mkdir -p "$device_dir"
|
mkdir -p "$device_dir"
|
||||||
|
rm -f "$device_dir"/$remove_pattern
|
||||||
|
|
||||||
# Run the integration test
|
# Run the integration test
|
||||||
flutter drive \
|
SCREENSHOT_OUTPUT_DIR="$device_dir" flutter drive \
|
||||||
--driver=test_driver/integration_test.dart \
|
--driver=test_driver/integration_test.dart \
|
||||||
--target="$INTEGRATION_TEST" \
|
--target="$INTEGRATION_TEST" \
|
||||||
-d "$device_id" \
|
-d "$device_id" \
|
||||||
|
--dart-define=MESHCORE_SCREENSHOTS=true \
|
||||||
|
--dart-define=SCREENSHOT_PREFIX="$screenshot_prefix" \
|
||||||
--screenshot="$device_dir" || {
|
--screenshot="$device_dir" || {
|
||||||
echo -e "${YELLOW}⚠️ Warning: Screenshot capture had issues on $device_name${NC}"
|
echo -e "${RED}❌ Screenshot capture failed on $device_name${NC}"
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if command -v sips >/dev/null 2>&1; then
|
||||||
|
for screenshot in "$device_dir"/$remove_pattern; do
|
||||||
|
sips -z "$screenshot_height" "$screenshot_width" "$screenshot" >/dev/null
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
echo -e "${GREEN}✅ Completed: $device_name${NC}"
|
echo -e "${GREEN}✅ Completed: $device_name${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
@@ -129,12 +158,14 @@ take_android_screenshots() {
|
|||||||
mkdir -p "$device_dir"
|
mkdir -p "$device_dir"
|
||||||
|
|
||||||
# Run the integration test
|
# Run the integration test
|
||||||
flutter drive \
|
SCREENSHOT_OUTPUT_DIR="$device_dir" flutter drive \
|
||||||
--driver=test_driver/integration_test.dart \
|
--driver=test_driver/integration_test.dart \
|
||||||
--target="$INTEGRATION_TEST" \
|
--target="$INTEGRATION_TEST" \
|
||||||
-d emulator-5554 \
|
-d emulator-5554 \
|
||||||
|
--dart-define=MESHCORE_SCREENSHOTS=true \
|
||||||
--screenshot="$device_dir" || {
|
--screenshot="$device_dir" || {
|
||||||
echo -e "${YELLOW}⚠️ Warning: Screenshot capture had issues on $device_name${NC}"
|
echo -e "${RED}❌ Screenshot capture failed on $device_name${NC}"
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Kill emulator
|
# Kill emulator
|
||||||
@@ -192,16 +223,11 @@ while [[ $# -gt 0 ]]; do
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
# Create test driver if it doesn't exist
|
# Ensure test driver exists
|
||||||
DRIVER_FILE="test_driver/integration_test.dart"
|
DRIVER_FILE="test_driver/integration_test.dart"
|
||||||
mkdir -p test_driver
|
|
||||||
if [ ! -f "$DRIVER_FILE" ]; then
|
if [ ! -f "$DRIVER_FILE" ]; then
|
||||||
echo -e "${YELLOW}📝 Creating integration test driver...${NC}"
|
echo -e "${RED}❌ Error: Integration test driver not found at $DRIVER_FILE${NC}"
|
||||||
cat > "$DRIVER_FILE" << 'EOF'
|
exit 1
|
||||||
import 'package:integration_test/integration_test_driver.dart';
|
|
||||||
|
|
||||||
Future<void> main() => integrationDriver();
|
|
||||||
EOF
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Take screenshots
|
# Take screenshots
|
||||||
@@ -239,10 +265,18 @@ echo -e "${GREEN}╔════════════════════
|
|||||||
echo -e "${GREEN}║ ✅ Screenshot Capture Complete! ║${NC}"
|
echo -e "${GREEN}║ ✅ Screenshot Capture Complete! ║${NC}"
|
||||||
echo -e "${GREEN}╔═══════════════════════════════════════════╗${NC}"
|
echo -e "${GREEN}╔═══════════════════════════════════════════╗${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${BLUE}📁 Screenshots saved to: $OUTPUT_DIR${NC}"
|
if [ "$PLATFORM" = "ios" ]; then
|
||||||
|
echo -e "${BLUE}📁 Screenshots saved to: $IOS_OUTPUT_DIR${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${BLUE}📁 Screenshots saved to: $OUTPUT_DIR${NC}"
|
||||||
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${YELLOW}Next steps:${NC}"
|
echo -e "${YELLOW}Next steps:${NC}"
|
||||||
echo -e " 1. Review screenshots in $OUTPUT_DIR"
|
if [ "$PLATFORM" = "ios" ]; then
|
||||||
|
echo -e " 1. Review screenshots in $IOS_OUTPUT_DIR"
|
||||||
|
else
|
||||||
|
echo -e " 1. Review screenshots in $OUTPUT_DIR"
|
||||||
|
fi
|
||||||
echo -e " 2. Organize by device size for App Store"
|
echo -e " 2. Organize by device size for App Store"
|
||||||
echo -e " 3. Add captions and localization if needed"
|
echo -e " 3. Add captions and localization if needed"
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
19
test_driver/integration_test.dart
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter_driver/flutter_driver.dart';
|
||||||
|
import 'package:integration_test/integration_test_driver_extended.dart';
|
||||||
|
|
||||||
|
Future<void> main() async {
|
||||||
|
final driver = await FlutterDriver.connect();
|
||||||
|
await integrationDriver(
|
||||||
|
driver: driver,
|
||||||
|
onScreenshot: (name, image, [args]) async {
|
||||||
|
final outputDir = Platform.environment['SCREENSHOT_OUTPUT_DIR'] ??
|
||||||
|
'ios/fastlane/screenshots/en-US';
|
||||||
|
final file = File('$outputDir/$name.png');
|
||||||
|
await file.parent.create(recursive: true);
|
||||||
|
await file.writeAsBytes(image);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||