chore: Add App Store screenshots

Refs:
This commit is contained in:
Janez T
2026-05-27 10:45:51 +02:00
parent c176ed0775
commit 0406b59d3b
34 changed files with 430 additions and 92 deletions

2
.gitignore vendored
View File

@@ -73,6 +73,8 @@ GoogleService-Info.plist
**/fastlane/.env
**/fastlane/.env.*
fastlane/README.md
ios/.bundle/
ios/vendor/
# Play Store release assets & scripts
play-store/

View 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);
}

View File

@@ -1,49 +1,47 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.7)
base64
nkf
rexml
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
CFPropertyList (3.0.9)
abbrev (0.1.2)
addressable (2.9.0)
public_suffix (>= 2.0.2, < 8.0)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.4.0)
aws-partitions (1.1172.0)
aws-sdk-core (3.233.0)
aws-eventstream (1.3.2)
aws-partitions (1.1109.0)
aws-sdk-core (3.224.1)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
bigdecimal
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.113.0)
aws-sdk-core (~> 3, >= 3.231.0)
aws-sdk-kms (1.101.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.199.1)
aws-sdk-core (~> 3, >= 3.231.0)
aws-sdk-s3 (1.188.0)
aws-sdk-core (~> 3, >= 3.224.1)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.12.1)
aws-sigv4 (1.11.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.3.0)
bigdecimal (3.3.1)
claide (1.1.0)
colored (1.2)
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
csv (3.3.5)
declarative (0.0.20)
digest-crc (0.7.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)
emoji_regex (3.2.3)
excon (0.112.0)
faraday (1.10.4)
excon (0.109.0)
faraday (1.10.5)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
@@ -55,25 +53,26 @@ GEM
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.7)
faraday-cookie_jar (0.0.8)
faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
http-cookie (>= 1.0.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.1)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.1.1)
faraday-multipart (1.2.0)
multipart-post (~> 2.0)
faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday-retry (1.0.4)
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.4.0)
fastlane (2.228.0)
fastimage (2.4.1)
fastlane (2.229.0)
CFPropertyList (>= 2.3, < 4.0.0)
abbrev (~> 0.1.2)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
@@ -81,6 +80,7 @@ GEM
bundler (>= 1.12.0, < 3.0.0)
colored (~> 1.2)
commander (~> 4.6)
csv (~> 3.3)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
@@ -100,6 +100,7 @@ GEM
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (>= 2.0.0, < 3.0.0)
mutex_m (~> 0.3.0)
naturally (~> 2.2)
optparse (>= 0.1.1, < 1.0.0)
plist (>= 3.1.0, < 4.0.0)
@@ -114,8 +115,7 @@ GEM
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.4.1)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
fastlane-sirp (1.0.0)
sysrandom (~> 1.0)
fastlane-sirp (1.1.0)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.54.0)
google-apis-core (>= 0.11.0, < 2.a)
@@ -131,19 +131,19 @@ GEM
google-apis-core (>= 0.11.0, < 2.a)
google-apis-playcustomapp_v1 (0.13.0)
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-cloud-core (1.8.0)
google-cloud-core (1.6.1)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.5.0)
google-cloud-storage (1.47.0)
google-cloud-errors (1.3.1)
google-cloud-storage (1.37.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.31.0)
google-apis-storage_v1 (~> 0.1)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
@@ -159,42 +159,40 @@ GEM
httpclient (2.9.0)
mutex_m
jmespath (1.6.2)
json (2.15.1)
jwt (2.10.2)
json (2.7.6)
jwt (2.10.3)
base64
logger (1.7.0)
mini_magick (4.13.2)
mini_mime (1.1.5)
multi_json (1.17.0)
multi_json (1.15.0)
multipart-post (2.4.1)
mutex_m (0.3.0)
nanaimo (0.4.0)
naturally (2.3.0)
nkf (0.2.0)
optparse (0.6.0)
optparse (0.8.1)
os (1.1.4)
plist (3.7.2)
public_suffix (6.0.2)
rake (13.3.0)
public_suffix (5.1.1)
rake (13.4.2)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
retriable (3.8.0)
rexml (3.4.4)
rouge (3.28.0)
ruby2_keywords (0.0.5)
rubyzip (2.4.1)
security (0.1.5)
signet (0.21.0)
signet (0.18.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 4.0)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.10)
CFPropertyList
naturally
sysrandom (1.0.5)
terminal-notifier (2.0.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
@@ -204,6 +202,7 @@ GEM
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
uber (0.1.0)
unf (0.2.0)
unicode-display_width (2.6.0)
word_wrap (1.0.0)
xcodeproj (1.27.0)
@@ -226,4 +225,4 @@ DEPENDENCIES
fastlane
BUNDLED WITH
2.7.2
2.4.22

View File

@@ -57,6 +57,8 @@ PODS:
- FlutterMacOS
- image_picker_ios (0.0.1):
- Flutter
- integration_test (0.0.1):
- Flutter
- nsd_ios (0.0.1):
- Flutter
- package_info_plus (0.4.5):
@@ -100,6 +102,7 @@ DEPENDENCIES:
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
- 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`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
@@ -144,6 +147,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/geolocator_apple/darwin"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
integration_test:
:path: ".symlinks/plugins/integration_test/ios"
nsd_ios:
:path: ".symlinks/plugins/nsd_ios/ios"
package_info_plus:
@@ -182,6 +187,7 @@ SPEC CHECKSUMS:
flutter_local_notifications: 643a3eda1ce1c0599413ca31672536d423dee214
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
nsd_ios: 596ad79109ddd3e52d665f650f36428049e3653e
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880

View File

@@ -16,17 +16,66 @@
default_platform(:ios)
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"
lane :release do
increment_build_number(xcodeproj: "Runner.xcodeproj")
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
desc "Push a new beta build to TestFlight"
lane :beta do
increment_build_number(xcodeproj: "Runner.xcodeproj")
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

View File

@@ -31,6 +31,22 @@ Push a new release build to the App Store
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.

View File

@@ -0,0 +1 @@
Copyright 2026 Janez Troha

View 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.

View File

@@ -0,0 +1 @@
meshcore,sar,search,rescue,mesh,lora,offline,maps,radio,gps

View File

@@ -0,0 +1 @@
https://github.com/dz0ny/meshcore-sar

View File

@@ -0,0 +1 @@
MeshCore SAR

View File

@@ -0,0 +1 @@
https://github.com/dz0ny/meshcore-sar

View File

@@ -0,0 +1 @@
Field coordination for search and rescue teams using MeshCore, BLE, offline maps, messaging, voice, and SAR markers.

View File

@@ -0,0 +1 @@
Adds App Store screenshots and TestFlight metadata for MeshCore SAR publishing.

View File

@@ -0,0 +1 @@
SAR mesh field coordination

View File

@@ -0,0 +1 @@
https://github.com/dz0ny/meshcore-sar/issues

View File

@@ -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 classname="fastlane.lanes" name="1: increment_build_number" time="0.48394">
</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 classname="fastlane.lanes" name="1: upload_to_app_store" time="37.324492">
</testcase>

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 538 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 KiB

View File

@@ -50,6 +50,9 @@ class MeshCoreSarApp extends StatefulWidget {
}
class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
static const bool _isScreenshotRun = bool.fromEnvironment(
'MESHCORE_SCREENSHOTS',
);
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
AppThemeMode _themeMode = AppThemeMode.system;
Locale? _locale;
@@ -87,7 +90,9 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
_pendingNotificationPayload = NotificationService().consumeLaunchPayload();
// Check if we need to request location permissions
await _checkLocationPermissions();
if (!_isScreenshotRun) {
await _checkLocationPermissions();
}
// Check for app updates (Android only) - runs in background
// Shows notification if update is available

View File

@@ -56,6 +56,7 @@ class OfflineTilesProvider extends ChangeNotifier {
TileDownloadService? _downloadService;
StreamSubscription<TileDownloadEvent>? _downloadSubscription;
StreamSubscription<Set<TilePeer>>? _peersSubscription;
bool _isDisposed = false;
// Drawing state
DrawingMode _drawingMode = DrawingMode.none;
@@ -436,7 +437,9 @@ class OfflineTilesProvider extends ChangeNotifier {
_peersSubscription = null;
await _sharing.stopPeerDiscovery();
_discoveredPeers = {};
notifyListeners();
if (!_isDisposed) {
notifyListeners();
}
}
void addManualPeer(String ipAddress) {
@@ -569,6 +572,7 @@ class OfflineTilesProvider extends ChangeNotifier {
@override
void dispose() {
_isDisposed = true;
_downloadSubscription?.cancel();
_downloadService?.dispose();
_peersSubscription?.cancel();

View File

@@ -38,6 +38,9 @@ class ContactsTab extends StatefulWidget {
}
class _ContactsTabState extends State<ContactsTab> {
static const bool _isScreenshotRun = bool.fromEnvironment(
'MESHCORE_SCREENSHOTS',
);
Position? _currentPosition;
final Map<ContactSection, String> _sectionFilters = {
ContactSection.teamMembers: '',
@@ -62,7 +65,9 @@ class _ContactsTabState extends State<ContactsTab> {
for (final section in ContactSection.values)
section: TextEditingController(text: _sectionFilters[section] ?? ''),
};
_getCurrentLocation();
if (!_isScreenshotRun) {
_getCurrentLocation();
}
// Mark all contacts as viewed when tab is opened
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<ContactsProvider>().markAllAsViewed();

View File

@@ -12,6 +12,9 @@ class NotificationService {
factory NotificationService() => _instance;
NotificationService._internal();
static const bool _skipPermissionRequests = bool.fromEnvironment(
'MESHCORE_SCREENSHOTS',
);
final FlutterLocalNotificationsPlugin _notificationsPlugin =
FlutterLocalNotificationsPlugin();
static const String _prefMessagesEnabled = 'notifications_messages_enabled';
@@ -90,9 +93,9 @@ class NotificationService {
// iOS initialization settings
final darwinSettings = DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
requestAlertPermission: !_skipPermissionRequests,
requestBadgePermission: !_skipPermissionRequests,
requestSoundPermission: !_skipPermissionRequests,
);
// Combined initialization settings
@@ -114,7 +117,11 @@ class NotificationService {
}
// Request permissions
await _requestPermissions();
if (_skipPermissionRequests) {
_permissionGranted = true;
} else {
await _requestPermissions();
}
await _loadPreferences();
// Create notification channels (Android)
@@ -327,6 +334,8 @@ class NotificationService {
String? notes,
AppLocalizations? localizations,
}) async {
if (_skipPermissionRequests) return;
if (!_isInitialized) {
debugPrint(
'⚠️ [NotificationService] Not initialized, skipping notification',
@@ -509,6 +518,8 @@ class NotificationService {
String? channelName,
AppLocalizations? localizations,
}) async {
if (_skipPermissionRequests) return;
if (!_isInitialized) {
debugPrint(
'⚠️ [NotificationService] Not initialized, skipping notification',
@@ -672,6 +683,8 @@ class NotificationService {
required String downloadUrl,
AppLocalizations? localizations,
}) async {
if (_skipPermissionRequests) return;
if (!_isInitialized) {
debugPrint(
'⚠️ [NotificationService] Not initialized, skipping notification',
@@ -772,6 +785,8 @@ class NotificationService {
required double batteryPercent,
required bool isCurrentDevice,
}) async {
if (_skipPermissionRequests) return false;
if (!_isInitialized) {
debugPrint(
'⚠️ [NotificationService] Not initialized, skipping notification',
@@ -863,6 +878,8 @@ class NotificationService {
required String contactKey,
String? contactName,
}) async {
if (_skipPermissionRequests) return false;
if (!_isInitialized) return false;
if (!_permissionGranted) return false;
if (!_discoveryNotificationsEnabled) return false;

View File

@@ -11,6 +11,7 @@ class VoicePlayerService {
final AudioPlayer _player = AudioPlayer();
final StreamController<void> _events = StreamController<void>.broadcast();
bool _isPlaying = false;
bool _isDisposed = false;
Duration _position = Duration.zero;
Duration _duration = Duration.zero;
Timer? _fallbackTicker;
@@ -30,22 +31,22 @@ class VoicePlayerService {
} else {
_stopFallbackTicker();
}
_events.add(null);
_emit();
});
_player.onLog.listen((msg) => debugPrint('🔊 [VoicePlayer] log: $msg'));
_player.onPositionChanged.listen((position) {
_position = position;
_events.add(null);
_emit();
});
_player.onDurationChanged.listen((duration) {
_duration = duration;
_events.add(null);
_emit();
});
_player.onPlayerComplete.listen((_) {
_isPlaying = false;
_position = _duration;
_stopFallbackTicker();
_events.add(null);
_emit();
});
}
@@ -57,7 +58,7 @@ class VoicePlayerService {
milliseconds: (pcmSamples.length * 1000) ~/ sampleRateHz,
);
_playbackStartedAt = DateTime.now();
_events.add(null);
_emit();
final wavBytes = _buildWav(pcmSamples, sampleRate: sampleRateHz);
final tmpDir = await getTemporaryDirectory();
@@ -70,14 +71,14 @@ class VoicePlayerService {
try {
_isPlaying = true;
_startFallbackTicker();
_events.add(null);
_emit();
await _player.play(DeviceFileSource(file.path));
debugPrint('🔊 [VoicePlayer] play() returned (audio playing)');
} catch (e, st) {
debugPrint('❌ [VoicePlayer] play() error: $e\n$st');
_isPlaying = false;
_stopFallbackTicker();
_events.add(null);
_emit();
}
}
@@ -88,15 +89,22 @@ class VoicePlayerService {
_position = Duration.zero;
_playbackStartedAt = null;
_stopFallbackTicker();
_events.add(null);
_emit();
}
void dispose() {
_isDisposed = true;
_stopFallbackTicker();
_events.close();
_player.dispose();
}
void _emit() {
if (!_isDisposed) {
_events.add(null);
}
}
void _startFallbackTicker() {
if (_fallbackTicker != null) return;
_fallbackTicker = Timer.periodic(const Duration(milliseconds: 100), (_) {
@@ -107,7 +115,7 @@ class VoicePlayerService {
final clamped = elapsed > _duration ? _duration : elapsed;
if (clamped > _position) {
_position = clamped;
_events.add(null);
_emit();
}
});
}

View File

@@ -503,6 +503,11 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.8.1"
flutter_driver:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_launcher_icons:
dependency: "direct dev"
description:
@@ -591,6 +596,11 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
fuchsia_remote_debug_protocol:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
geoclue:
dependency: transitive
description:
@@ -767,6 +777,11 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.2.2"
integration_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
intl:
dependency: "direct main"
description:
@@ -1112,6 +1127,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.5.0"
process:
dependency: transitive
description:
name: process
sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744
url: "https://pub.dev"
source: hosted
version: "5.0.5"
proj4dart:
dependency: "direct main"
description:
@@ -1405,6 +1428,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
@@ -1589,6 +1620,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.1"
webdriver:
dependency: transitive
description:
name: webdriver
sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
webserial:
dependency: "direct main"
description:

View File

@@ -129,9 +129,15 @@ dependencies:
webserial: ^1.2.0
dev_dependencies:
flutter_driver:
sdk: flutter
flutter_test:
sdk: flutter
integration_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# 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

View File

@@ -14,12 +14,18 @@ NC='\033[0m' # No Color
# Configuration
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"
# Device configurations for App Store screenshots
# iOS devices (required sizes: 6.7", 6.5", 5.5")
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)
@@ -45,7 +51,7 @@ mkdir -p "$OUTPUT_DIR"
# Function to list available devices
list_devices() {
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 -e "${YELLOW}🤖 Available Android Emulators:${NC}"
emulator -list-avds
@@ -74,23 +80,46 @@ take_ios_screenshots() {
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
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
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"
rm -f "$device_dir"/$remove_pattern
# Run the integration test
flutter drive \
SCREENSHOT_OUTPUT_DIR="$device_dir" flutter drive \
--driver=test_driver/integration_test.dart \
--target="$INTEGRATION_TEST" \
-d "$device_id" \
--dart-define=MESHCORE_SCREENSHOTS=true \
--dart-define=SCREENSHOT_PREFIX="$screenshot_prefix" \
--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 ""
}
@@ -129,12 +158,14 @@ take_android_screenshots() {
mkdir -p "$device_dir"
# Run the integration test
flutter drive \
SCREENSHOT_OUTPUT_DIR="$device_dir" flutter drive \
--driver=test_driver/integration_test.dart \
--target="$INTEGRATION_TEST" \
-d emulator-5554 \
--dart-define=MESHCORE_SCREENSHOTS=true \
--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
@@ -192,16 +223,11 @@ while [[ $# -gt 0 ]]; do
esac
done
# Create test driver if it doesn't exist
# Ensure test driver exists
DRIVER_FILE="test_driver/integration_test.dart"
mkdir -p test_driver
if [ ! -f "$DRIVER_FILE" ]; then
echo -e "${YELLOW}📝 Creating integration test driver...${NC}"
cat > "$DRIVER_FILE" << 'EOF'
import 'package:integration_test/integration_test_driver.dart';
Future<void> main() => integrationDriver();
EOF
echo -e "${RED}❌ Error: Integration test driver not found at $DRIVER_FILE${NC}"
exit 1
fi
# Take screenshots
@@ -239,10 +265,18 @@ echo -e "${GREEN}╔════════════════════
echo -e "${GREEN}║ ✅ Screenshot Capture Complete! ║${NC}"
echo -e "${GREEN}╔═══════════════════════════════════════════╗${NC}"
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 -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 " 3. Add captions and localization if needed"
echo ""

View 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;
},
);
}