mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-06-14 22:55:12 +10:00
issue #112 fixes and more
This commit is contained in:
@@ -0,0 +1,244 @@
|
|||||||
|
# TestFlight and App Store Deployment Guide
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- [x] Apple Developer Account ($99/year) - [developer.apple.com](https://developer.apple.com)
|
||||||
|
- [x] Xcode installed
|
||||||
|
- [x] Apple Transporter app installed
|
||||||
|
- [x] App icons ready (1024x1024px)
|
||||||
|
- [x] Bundle ID configured: `com.monitormx.meshcoreopen`
|
||||||
|
|
||||||
|
## Step 1: Register Bundle Identifier
|
||||||
|
|
||||||
|
1. Go to [Apple Developer - Identifiers](https://developer.apple.com/account/resources/identifiers/list)
|
||||||
|
2. Click the **"+"** button
|
||||||
|
3. Select **"App IDs"** → Continue
|
||||||
|
4. Select **"App"** → Continue
|
||||||
|
5. Fill in:
|
||||||
|
- **Description**: Meshcore Open
|
||||||
|
- **Bundle ID**: Explicit - `com.monitormx.meshcoreopen`
|
||||||
|
- **Capabilities**: Leave defaults (or add as needed)
|
||||||
|
6. Click **Continue** → **Register**
|
||||||
|
|
||||||
|
## Step 2: Create App in App Store Connect
|
||||||
|
|
||||||
|
1. Go to [App Store Connect](https://appstoreconnect.apple.com)
|
||||||
|
2. Sign in with your Apple ID
|
||||||
|
3. Click **"My Apps"**
|
||||||
|
4. Click the **"+"** button → **"New App"**
|
||||||
|
5. Fill in the form:
|
||||||
|
- **Platforms**: iOS
|
||||||
|
- **Name**: Meshcore Open
|
||||||
|
- **Primary Language**: English (U.S.)
|
||||||
|
- **Bundle ID**: Select `com.monitormx.meshcoreopen` from dropdown
|
||||||
|
- **SKU**: `meshcore-open-001` (or any unique identifier)
|
||||||
|
- **User Access**: Full Access
|
||||||
|
6. Click **"Create"**
|
||||||
|
|
||||||
|
## Step 3: Build the IPA
|
||||||
|
|
||||||
|
Run these commands from the project directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add CocoaPods to PATH
|
||||||
|
export PATH="/opt/homebrew/lib/ruby/gems/4.0.0/bin:$PATH"
|
||||||
|
|
||||||
|
# Clean previous builds
|
||||||
|
../flutter/bin/flutter clean
|
||||||
|
|
||||||
|
# Build IPA for App Store
|
||||||
|
../flutter/bin/flutter build ipa
|
||||||
|
```
|
||||||
|
|
||||||
|
The IPA will be created at: `build/ios/ipa/meshcore_open.ipa`
|
||||||
|
|
||||||
|
## Step 4: Upload to App Store Connect via Transporter
|
||||||
|
|
||||||
|
1. **Open Apple Transporter**
|
||||||
|
- Launch from Applications folder
|
||||||
|
- Sign in with your Apple ID
|
||||||
|
|
||||||
|
2. **Upload the IPA**
|
||||||
|
- Drag and drop `build/ios/ipa/meshcore_open.ipa` into Transporter
|
||||||
|
- Click **"Deliver"**
|
||||||
|
- Wait for upload to complete (usually 1-5 minutes)
|
||||||
|
|
||||||
|
3. **Processing**
|
||||||
|
- Apple will process your build (10-30 minutes)
|
||||||
|
- You'll receive an email when processing is complete
|
||||||
|
|
||||||
|
## Step 5: Configure App Store Connect Metadata
|
||||||
|
|
||||||
|
### App Information
|
||||||
|
1. In App Store Connect, go to your app
|
||||||
|
2. Fill in required information:
|
||||||
|
- **Subtitle**: Short description (30 chars max)
|
||||||
|
- **Privacy Policy URL**: Required for Bluetooth apps
|
||||||
|
- **Category**: Utilities or Productivity
|
||||||
|
- **Age Rating**: Complete questionnaire
|
||||||
|
|
||||||
|
### App Store Listing
|
||||||
|
1. Go to **App Store** tab
|
||||||
|
2. Upload **Screenshots** (required):
|
||||||
|
- iPhone 6.7" display (1290 x 2796 pixels) - At least 1 screenshot
|
||||||
|
- iPhone 6.5" display (1242 x 2688 pixels) - At least 1 screenshot
|
||||||
|
- Optional: iPad screenshots
|
||||||
|
|
||||||
|
3. Fill in **Description**:
|
||||||
|
```
|
||||||
|
Meshcore Open is a Flutter client for MeshCore LoRa mesh networking devices.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- BLE connectivity to MeshCore devices
|
||||||
|
- Real-time mesh network communication
|
||||||
|
- Map visualization with OpenStreetMap
|
||||||
|
- Community management with QR code scanning
|
||||||
|
- Message tracking and retry system
|
||||||
|
|
||||||
|
Connect to your MeshCore LoRa device and start communicating over the mesh network.
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Keywords**: `lora,mesh,networking,bluetooth,communication`
|
||||||
|
5. **Support URL**: Your GitHub or website URL
|
||||||
|
6. **Marketing URL**: (Optional)
|
||||||
|
|
||||||
|
### Version Information
|
||||||
|
1. **What's New in This Version**:
|
||||||
|
```
|
||||||
|
Initial release of Meshcore Open
|
||||||
|
|
||||||
|
- BLE device connectivity
|
||||||
|
- Mesh network messaging
|
||||||
|
- Map integration
|
||||||
|
- Community features
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Build**: Select the uploaded build once processing completes
|
||||||
|
|
||||||
|
## Step 6: TestFlight Setup
|
||||||
|
|
||||||
|
### Internal Testing (No Review Required)
|
||||||
|
1. Go to **TestFlight** tab in App Store Connect
|
||||||
|
2. Click **Internal Testing** → **"+"** to create a group
|
||||||
|
3. Name your group (e.g., "Internal Testers")
|
||||||
|
4. Add yourself as a tester using your email
|
||||||
|
5. Select the build you uploaded
|
||||||
|
6. Testers will receive an email with TestFlight invitation
|
||||||
|
|
||||||
|
### External Testing (Requires Beta Review)
|
||||||
|
1. Click **External Testing** → **"+"** to create a group
|
||||||
|
2. Add build and testers
|
||||||
|
3. Fill in **Test Information**:
|
||||||
|
- **What to Test**: Brief description of features
|
||||||
|
- **Feedback Email**: Your email address
|
||||||
|
4. Click **Submit for Review**
|
||||||
|
5. Beta review typically takes 24-48 hours
|
||||||
|
|
||||||
|
## Step 7: App Store Submission
|
||||||
|
|
||||||
|
Once you're ready for public release:
|
||||||
|
|
||||||
|
1. Go to **App Store** tab
|
||||||
|
2. Complete all required metadata (if not done)
|
||||||
|
3. Select your build
|
||||||
|
4. Fill in **App Review Information**:
|
||||||
|
- **Contact Information**: Your name, phone, email
|
||||||
|
- **Demo Account**: If app requires login
|
||||||
|
- **Notes**: Any special instructions for reviewers
|
||||||
|
5. Answer **Export Compliance** questions:
|
||||||
|
- Does your app use encryption? **Yes** (uses TLS/HTTPS)
|
||||||
|
- Is encryption registration required? **No** (standard encryption)
|
||||||
|
6. Click **Add for Review**
|
||||||
|
7. Review summary and click **Submit to App Review**
|
||||||
|
|
||||||
|
## Step 8: After Submission
|
||||||
|
|
||||||
|
- **App Review**: Typically 24-48 hours
|
||||||
|
- **Common Rejection Reasons**:
|
||||||
|
- Missing privacy policy
|
||||||
|
- Incomplete app information
|
||||||
|
- Crashes or bugs
|
||||||
|
- Misleading app description
|
||||||
|
|
||||||
|
- **If Approved**: You can release immediately or schedule a release date
|
||||||
|
- **If Rejected**: Address issues and resubmit
|
||||||
|
|
||||||
|
## Updating the App
|
||||||
|
|
||||||
|
When you need to release an update:
|
||||||
|
|
||||||
|
1. **Update version** in `pubspec.yaml`:
|
||||||
|
```yaml
|
||||||
|
version: 0.5.0+6 # Increment version (0.5.0) and build number (+6)
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Build new IPA**:
|
||||||
|
```bash
|
||||||
|
export PATH="/opt/homebrew/lib/ruby/gems/4.0.0/bin:$PATH"
|
||||||
|
../flutter/bin/flutter clean
|
||||||
|
../flutter/bin/flutter build ipa
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Upload via Transporter** (same process as above)
|
||||||
|
|
||||||
|
4. **Create new version** in App Store Connect:
|
||||||
|
- Click **"+"** next to versions
|
||||||
|
- Select version number
|
||||||
|
- Update "What's New" text
|
||||||
|
- Select new build
|
||||||
|
- Submit for review
|
||||||
|
|
||||||
|
## macOS Build (Bonus)
|
||||||
|
|
||||||
|
To build for macOS:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export PATH="/opt/homebrew/lib/ruby/gems/4.0.0/bin:$PATH"
|
||||||
|
../flutter/bin/flutter build macos --release
|
||||||
|
cd build/macos/Build/Products/Release
|
||||||
|
zip -r meshcore_open-macos.zip meshcore_open.app
|
||||||
|
```
|
||||||
|
|
||||||
|
Distribution:
|
||||||
|
- Share the zip file directly
|
||||||
|
- Users unzip and drag to Applications
|
||||||
|
- First run: Right-click → Open (to bypass Gatekeeper)
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Build Errors
|
||||||
|
- **CocoaPods not found**: Ensure PATH includes `/opt/homebrew/lib/ruby/gems/4.0.0/bin`
|
||||||
|
- **No signing certificate**: Configure Team in Xcode (Signing & Capabilities)
|
||||||
|
- **Bundle ID mismatch**: Check `ios/Runner.xcodeproj/project.pbxproj`
|
||||||
|
|
||||||
|
### Upload Errors
|
||||||
|
- **No profiles found**: Create app in App Store Connect first
|
||||||
|
- **Bundle ID not registered**: Register in Apple Developer portal
|
||||||
|
- **Authentication failed**: Use Transporter app instead of CLI
|
||||||
|
|
||||||
|
### TestFlight Issues
|
||||||
|
- **Build not appearing**: Wait 10-30 minutes for processing
|
||||||
|
- **Can't add testers**: Check you have available slots (100 internal, 10,000 external)
|
||||||
|
- **TestFlight crashes**: Check device logs in Xcode → Devices & Simulators
|
||||||
|
|
||||||
|
## Important Files
|
||||||
|
|
||||||
|
- **iOS IPA**: `build/ios/ipa/meshcore_open.ipa`
|
||||||
|
- **macOS App**: `build/macos/Build/Products/Release/meshcore_open.app`
|
||||||
|
- **Bundle ID Config**: `ios/Runner.xcodeproj/project.pbxproj`
|
||||||
|
- **Version Info**: `pubspec.yaml`
|
||||||
|
|
||||||
|
## Useful Links
|
||||||
|
|
||||||
|
- [App Store Connect](https://appstoreconnect.apple.com)
|
||||||
|
- [Apple Developer Portal](https://developer.apple.com/account)
|
||||||
|
- [TestFlight Documentation](https://developer.apple.com/testflight/)
|
||||||
|
- [App Store Review Guidelines](https://developer.apple.com/app-store/review/guidelines/)
|
||||||
|
- [Flutter iOS Deployment](https://docs.flutter.dev/deployment/ios)
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues with:
|
||||||
|
- **App Store Process**: [Apple Developer Support](https://developer.apple.com/contact/)
|
||||||
|
- **Flutter Build Issues**: [Flutter GitHub](https://github.com/flutter/flutter/issues)
|
||||||
|
- **Meshcore Open App**: [GitHub Issues](https://github.com/wel97459/meshcore-open/issues)
|
||||||
@@ -95,6 +95,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
double? _selfLongitude;
|
double? _selfLongitude;
|
||||||
bool _isLoadingContacts = false;
|
bool _isLoadingContacts = false;
|
||||||
bool _isLoadingChannels = false;
|
bool _isLoadingChannels = false;
|
||||||
|
bool _hasLoadedChannels = false;
|
||||||
bool _batteryRequested = false;
|
bool _batteryRequested = false;
|
||||||
bool _awaitingSelfInfo = false;
|
bool _awaitingSelfInfo = false;
|
||||||
bool _preserveContactsOnRefresh = false;
|
bool _preserveContactsOnRefresh = false;
|
||||||
@@ -122,7 +123,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
List<Channel> _previousChannelsCache = [];
|
List<Channel> _previousChannelsCache = [];
|
||||||
static const int _maxChannelSyncRetries = 3;
|
static const int _maxChannelSyncRetries = 3;
|
||||||
static const int _channelSyncTimeoutMs = 2000; // 2 second timeout per channel
|
static const int _channelSyncTimeoutMs = 2000; // 2 second timeout per channel
|
||||||
static const Duration _batteryPollInterval = Duration(seconds: 30);
|
static const Duration _batteryPollInterval = Duration(seconds: 120);
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
MessageRetryService? _retryService;
|
MessageRetryService? _retryService;
|
||||||
@@ -927,6 +928,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
_pendingQueueSync = false;
|
_pendingQueueSync = false;
|
||||||
_isSyncingChannels = false;
|
_isSyncingChannels = false;
|
||||||
_channelSyncInFlight = false;
|
_channelSyncInFlight = false;
|
||||||
|
_hasLoadedChannels = false;
|
||||||
|
|
||||||
_setState(MeshCoreConnectionState.disconnected);
|
_setState(MeshCoreConnectionState.disconnected);
|
||||||
if (!manual) {
|
if (!manual) {
|
||||||
@@ -1493,13 +1495,19 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
await sendCliCommand('set privacy ${enabled ? 'on' : 'off'}');
|
await sendCliCommand('set privacy ${enabled ? 'on' : 'off'}');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> getChannels({int? maxChannels}) async {
|
Future<void> getChannels({int? maxChannels, bool force = false}) async {
|
||||||
if (!isConnected) return;
|
if (!isConnected) return;
|
||||||
if (_isSyncingChannels) {
|
if (_isSyncingChannels) {
|
||||||
debugPrint('[ChannelSync] Already syncing channels, ignoring request');
|
debugPrint('[ChannelSync] Already syncing channels, ignoring request');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip fetching if already loaded and not forced
|
||||||
|
if (_hasLoadedChannels && !force) {
|
||||||
|
debugPrint('[ChannelSync] Channels already loaded, skipping fetch (use force=true to reload)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_isLoadingChannels = true;
|
_isLoadingChannels = true;
|
||||||
_isSyncingChannels = true;
|
_isSyncingChannels = true;
|
||||||
_previousChannelsCache = List<Channel>.from(_channels);
|
_previousChannelsCache = List<Channel>.from(_channels);
|
||||||
@@ -1619,6 +1627,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
_totalChannelsToRequest = 0;
|
_totalChannelsToRequest = 0;
|
||||||
|
|
||||||
if (completed) {
|
if (completed) {
|
||||||
|
_hasLoadedChannels = true;
|
||||||
_previousChannelsCache.clear();
|
_previousChannelsCache.clear();
|
||||||
}
|
}
|
||||||
// Keep cache on failure/disconnection for future attempts
|
// Keep cache on failure/disconnection for future attempts
|
||||||
@@ -1629,7 +1638,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
|
|
||||||
await sendFrame(buildSetChannelFrame(index, name, psk));
|
await sendFrame(buildSetChannelFrame(index, name, psk));
|
||||||
// Refresh channels after setting
|
// Refresh channels after setting
|
||||||
await getChannels();
|
await getChannels(force: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteChannel(int index) async {
|
Future<void> deleteChannel(int index) async {
|
||||||
@@ -1644,7 +1653,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
// Clear in-memory messages for this channel
|
// Clear in-memory messages for this channel
|
||||||
_channelMessages.remove(index);
|
_channelMessages.remove(index);
|
||||||
// Refresh channels after deleting
|
// Refresh channels after deleting
|
||||||
await getChannels();
|
await getChannels(force: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleFrame(List<int> data) {
|
void _handleFrame(List<int> data) {
|
||||||
@@ -2105,6 +2114,15 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (message != null) {
|
if (message != null) {
|
||||||
|
// Ignore messages from self (device hearing its own broadcast)
|
||||||
|
// BUT allow repeated messages (pathLength indicates it went through repeater)
|
||||||
|
if (_selfPublicKey != null &&
|
||||||
|
message.senderKeyHex == pubKeyToHex(_selfPublicKey!) &&
|
||||||
|
(message.pathLength == null || message.pathLength == 0)) {
|
||||||
|
debugPrint('Ignoring direct message from self');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final contact = _contacts.cast<Contact?>().firstWhere(
|
final contact = _contacts.cast<Contact?>().firstWhere(
|
||||||
(c) => c?.publicKeyHex == message!.senderKeyHex,
|
(c) => c?.publicKeyHex == message!.senderKeyHex,
|
||||||
orElse: () => null,
|
orElse: () => null,
|
||||||
@@ -3066,28 +3084,19 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool _shouldDropSelfChannelMessage(String senderName, Uint8List pathBytes) {
|
bool _shouldDropSelfChannelMessage(String senderName, Uint8List pathBytes) {
|
||||||
final selfKey = _selfPublicKey;
|
|
||||||
if (selfKey == null) return false;
|
|
||||||
if (pathBytes.length < pathHashSize) return false;
|
|
||||||
final trimmed = senderName.trim();
|
final trimmed = senderName.trim();
|
||||||
if (trimmed.isEmpty) return false;
|
if (trimmed.isEmpty) return false;
|
||||||
|
|
||||||
final selfName = _selfName?.trim();
|
final selfName = _selfName?.trim();
|
||||||
if (selfName == null || selfName.isEmpty) return false;
|
if (selfName == null || selfName.isEmpty) return false;
|
||||||
|
|
||||||
|
// If sender name doesn't match, keep the message
|
||||||
if (trimmed != selfName) return false;
|
if (trimmed != selfName) return false;
|
||||||
final prefix = selfKey.sublist(0, pathHashSize);
|
|
||||||
for (int i = 0; i + pathHashSize <= pathBytes.length; i += pathHashSize) {
|
// Name matches - this is from self
|
||||||
var match = true;
|
// Drop only if pathBytes is empty (direct broadcast)
|
||||||
for (int j = 0; j < pathHashSize; j++) {
|
// Keep if pathBytes has data (repeated through another node)
|
||||||
if (pathBytes[i + j] != prefix[j]) {
|
return pathBytes.isEmpty;
|
||||||
match = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (match) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Uint8List _selectPreferredPathBytes(Uint8List existing, Uint8List incoming) {
|
Uint8List _selectPreferredPathBytes(Uint8List existing, Uint8List incoming) {
|
||||||
|
|||||||
@@ -128,6 +128,9 @@ class MeshCoreApp extends StatelessWidget {
|
|||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
|
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
|
snackBarTheme: const SnackBarThemeData(
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
darkTheme: ThemeData(
|
darkTheme: ThemeData(
|
||||||
colorScheme: ColorScheme.fromSeed(
|
colorScheme: ColorScheme.fromSeed(
|
||||||
@@ -135,6 +138,9 @@ class MeshCoreApp extends StatelessWidget {
|
|||||||
brightness: Brightness.dark,
|
brightness: Brightness.dark,
|
||||||
),
|
),
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
|
snackBarTheme: const SnackBarThemeData(
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
themeMode: _themeModeFromSetting(settingsService.settings.themeMode),
|
themeMode: _themeModeFromSetting(settingsService.settings.themeMode),
|
||||||
home: const ScannerScreen(),
|
home: const ScannerScreen(),
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
),
|
),
|
||||||
body: RefreshIndicator(
|
body: RefreshIndicator(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
await context.read<MeshCoreConnector>().getChannels();
|
await context.read<MeshCoreConnector>().getChannels(force: true);
|
||||||
},
|
},
|
||||||
child: () {
|
child: () {
|
||||||
if (connector.isLoadingChannels) {
|
if (connector.isLoadingChannels) {
|
||||||
|
|||||||
@@ -225,13 +225,15 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Re center map after removed markers have loaded
|
// Re center map after removed markers have loaded
|
||||||
if (!_hasInitializedMap && _removedMarkersLoaded && hasMapContent) {
|
if (!_hasInitializedMap && _removedMarkersLoaded) {
|
||||||
_hasInitializedMap = true;
|
_hasInitializedMap = true;
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
if (hasMapContent) {
|
||||||
if (mounted) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
_mapController.move(center, initialZoom);
|
if (mounted) {
|
||||||
}
|
_mapController.move(center, initialZoom);
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final allowBack = !connector.isConnected;
|
final allowBack = !connector.isConnected;
|
||||||
@@ -275,9 +277,7 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: !hasMapContent
|
body: Stack(
|
||||||
? _buildEmptyState()
|
|
||||||
: Stack(
|
|
||||||
children: [
|
children: [
|
||||||
FlutterMap(
|
FlutterMap(
|
||||||
mapController: _mapController,
|
mapController: _mapController,
|
||||||
@@ -376,27 +376,6 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEmptyState() {
|
|
||||||
return Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(Icons.location_off, size: 64, color: Colors.grey[400]),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(
|
|
||||||
context.l10n.map_noNodesWithLocation,
|
|
||||||
style: TextStyle(fontSize: 18, color: Colors.grey[600]),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
context.l10n.map_nodesNeedGps,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Marker> _buildMarkers(List<Contact> contacts, settings) {
|
List<Marker> _buildMarkers(List<Contact> contacts, settings) {
|
||||||
final markers = <Marker>[];
|
final markers = <Marker>[];
|
||||||
|
|||||||
@@ -780,10 +780,15 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (txPower == null || txPower < 0 || txPower > 22) {
|
final maxTxPower = widget.connector.maxTxPower ?? 22;
|
||||||
|
if (txPower == null || txPower < 0 || txPower > maxTxPower) {
|
||||||
ScaffoldMessenger.of(
|
ScaffoldMessenger.of(
|
||||||
context,
|
context,
|
||||||
).showSnackBar(SnackBar(content: Text(l10n.settings_txPowerInvalid)));
|
).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('${l10n.settings_txPowerInvalid} (0-$maxTxPower dBm)'),
|
||||||
|
),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -932,7 +937,9 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: l10n.settings_txPower,
|
labelText: l10n.settings_txPower,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
helperText: l10n.settings_txPowerHelper,
|
helperText: widget.connector.maxTxPower != null
|
||||||
|
? '${l10n.settings_txPowerHelper} (max: ${widget.connector.maxTxPower} dBm)'
|
||||||
|
: l10n.settings_txPowerHelper,
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -9,9 +9,6 @@ PODS:
|
|||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- package_info_plus (0.0.1):
|
- package_info_plus (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- path_provider_foundation (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- FlutterMacOS
|
|
||||||
- shared_preferences_foundation (0.0.1):
|
- shared_preferences_foundation (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
@@ -29,7 +26,6 @@ DEPENDENCIES:
|
|||||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||||
- mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos`)
|
- mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos`)
|
||||||
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
|
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
|
||||||
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
|
|
||||||
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||||
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
|
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
|
||||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||||
@@ -46,8 +42,6 @@ EXTERNAL SOURCES:
|
|||||||
:path: Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos
|
||||||
package_info_plus:
|
package_info_plus:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
|
||||||
path_provider_foundation:
|
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
|
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
|
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
|
||||||
sqflite_darwin:
|
sqflite_darwin:
|
||||||
@@ -63,7 +57,6 @@ SPEC CHECKSUMS:
|
|||||||
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
||||||
mobile_scanner: 0e365ed56cad24f28c0fd858ca04edefb40dfac3
|
mobile_scanner: 0e365ed56cad24f28c0fd858ca04edefb40dfac3
|
||||||
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
|
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
|
||||||
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
|
||||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||||
url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd
|
url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd
|
||||||
|
|||||||
Reference in New Issue
Block a user