Compare commits

...

25 Commits

Author SHA1 Message Date
Janez T
f7b6143a57 chore: bump app version and update Android/iOS build tooling
- Bump version to 2026.0613.2+57
- Gradle wrapper 8.12 -> 8.14; Flutter migrator flags (builtInKotlin/newDsl)
  and relax kotlin.jvm.target.validation to warning for third-party plugins
- Refresh iOS Xcode project, scheme, Info.plist, Podfile.lock and SwiftPM
  Package.resolved

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 13:19:20 +02:00
Janez T
3c08e150ef feat: align fast-GPS beacons with MeshUI firmware (16-byte format + cadence parity)
Switch the fast-GPS beacon to the firmware's canonical 16-byte wire format
(magic, key6, lat/lon microdegrees, speed km/h) — no on-wire timestamp; the
receiver stamps its own RX time. Drops compatibility with the older 19-byte app
format.

Mirror the firmware's adaptive send cadence in the app-fallback path
(LocationTrackingService): anchor + dwell motion hysteresis, EMA ground speed,
9-min stationary keepalive, fix-quality (accuracy) gate, and a post-RX hold-off
to avoid channel collisions. Parked nodes now beacon the stable anchor instead
of GPS wander.

Add the fast_gps_region config: region-scope picker in settings backed by the
firmware's fast_gps_region / fast_gps_regions custom vars.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 13:17:02 +02:00
Janez T
3283d4ca20 docs: Add Windows BLE pairing instructions to README
Document pairing the MeshCore radio via the Windows 11 system Bluetooth
menu (with PIN) before connecting in-app, per user report.

Fixes #41

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 10:41:35 +02:00
Janez T
bdbceece75 fix: Normalize raw transport path for firmware v1.15 photo/audio transfer
CMD_SEND_RAW_DATA (0x19) was passed the encoded MeshCore route descriptor
(e.g. 0x41 for a 1-hop 2-byte-hash route), but the v1.15 companion firmware
raw-data handler treats the path byte as a legacy literal hop count, not an
encoded descriptor. It misread 0x41 as "65 path bytes" and rejected the
command (ERROR: Unsupported command), so media fetch always failed while
normal messaging worked.

- Add ContactRouteCodec.toLegacyRawPath() converting encoded descriptor +
  path into legacy format (literal hop count + first byte of each hop hash).
- Apply it in ConnectionProvider.sendRawVoicePacket, the single chokepoint
  all raw media (voice/image fragments, swarm, route probes) flows through.
- Pace served fragments 350ms apart to avoid firmware "ERROR: Table full"
  when bursting many raw packets.
- Tests for the descriptor conversion.

Fixes #43

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 10:38:05 +02:00
Janez T
365bd76cbc feat: Add Mapy.cz Outdoor (OSM + LIDAR) base map layer
- New MapLayerType.mapyOutdoor with Referer header support
  (tile server rejects requests without mapy.com Referer)
- Headered layers get a dedicated NetworkTileProvider sharing
  the offline tile cache
- Offline region downloads pass layer headers through
  TileDownloadService

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 17:28:56 +02:00
Janez T
ba27843166 feat: UX polish, localization, advert relocation, and map filter fixes
- i18n: translate connection screen + chat tab keys into 13 locales;
  add "Send my contact" (advert) strings in all locales
- feat: move self-advert from home header into composer "+" action menu
  (new lib/utils/advert_helper.dart, always shows Flood/Direct sheet)
- fix: hide-repeaters map toggle was bypassed in simple mode
- fix: simple mode map now shows only favourite chat contacts
- fix: wrap ListTiles in Material (contact_tile secondary actions,
  inferred contact group card) to satisfy Flutter ink assertions
- device-authoritative contact/channel sync and UX review fixes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 10:01:10 +02:00
Janez T
3e1113035d Merge branch 'chore/app-store-screenshots' 2026-05-27 10:53:43 +02:00
Janez T
89359c5e34 Merge remote-tracking branch 'origin/main' 2026-05-27 10:53:31 +02:00
Janez T
0406b59d3b chore: Add App Store screenshots
Refs:
2026-05-27 10:45:51 +02:00
Janez Troha
c176ed0775 Update iOS TestFlight link in README 2026-05-22 12:46:29 +02:00
Janez T
6062f91c2d Throttle message notifications during sync 2026-05-10 17:48:13 +02:00
Janez T
e1400cf1ec feat: Modernize map selection and tile cache 2026-04-26 08:41:50 +02:00
Janez Troha
b040a8bd3a Merge pull request #39 from dz0ny/feat/reported-issues-ci
Feat/reported issues ci
2026-04-26 08:40:12 +02:00
Janez T
c847bd6465 feat: Add UTM coordinates #38
ref:
2026-04-26 08:30:28 +02:00
Janez T
132a576785 fix: Split trail recording #32
ref:
2026-04-26 08:30:28 +02:00
Janez T
9b8b1bf473 fix: Add background service #37
ref: #31
2026-04-26 08:30:28 +02:00
Janez T
0008eff1b8 fix: Stabilize reported tests #31
ref: #32 #37
2026-04-26 08:30:28 +02:00
Janez T
2fe125d6ba chore: Add Play release cycle #31
ref:
2026-04-26 08:30:28 +02:00
Janez T
5834454087 feat: Add UTM coordinates #38
ref:
2026-04-26 08:28:41 +02:00
Janez T
73e78691ed fix: Split trail recording #32
ref:
2026-04-26 08:24:11 +02:00
Janez T
d067095345 fix: Add background service #37
ref: #31
2026-04-26 08:24:09 +02:00
Janez T
0b463f6454 fix: Stabilize reported tests #31
ref: #32 #37
2026-04-26 08:24:07 +02:00
Janez T
a472b0c05c chore: Add Play release cycle #31
ref:
2026-04-26 08:24:04 +02:00
Janez T
4181b33da1 Fix echo metadata and bump build number #123 2026-04-26 08:02:53 +02:00
Janez T
f9dfdc2340 fix: Add manual TCP connect flow 2026-04-18 08:59:36 +02:00
143 changed files with 17997 additions and 3936 deletions

View File

@@ -26,7 +26,7 @@ jobs:
uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: "17"
@@ -44,6 +44,14 @@ jobs:
cache-key: flutter-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}
pub-cache-key: flutter-pub-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}-${{ hashFiles('pubspec.lock') }}
- name: Setup Ruby
if: github.event_name == 'release'
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.3"
bundler-cache: true
working-directory: android
- name: Cache Android NDK
uses: actions/cache@v4
with:
@@ -55,22 +63,76 @@ jobs:
- name: Install dependencies
run: flutter pub get
- name: Validate Android release secrets
if: github.event_name == 'release'
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
GOOGLE_PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
run: |
test -n "$ANDROID_KEYSTORE_BASE64"
test -n "$ANDROID_KEYSTORE_PASSWORD"
test -n "$ANDROID_KEY_ALIAS"
test -n "$ANDROID_KEY_PASSWORD"
test -n "$GOOGLE_PLAY_SERVICE_ACCOUNT_JSON"
- name: Create Android signing files
if: github.event_name == 'release'
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: |
echo "$ANDROID_KEYSTORE_BASE64" | base64 --decode > android/release-keystore.jks
cat <<EOF > android/key.properties
storePassword=$ANDROID_KEYSTORE_PASSWORD
keyPassword=$ANDROID_KEY_PASSWORD
keyAlias=$ANDROID_KEY_ALIAS
storeFile=../release-keystore.jks
EOF
- name: Build APK
run: flutter build apk --release
- name: Prepare APK artifact
- name: Build Play release AAB
if: github.event_name == 'release'
run: flutter build appbundle --release
- name: Prepare Android artifacts
run: |
RAW_TAG="${{ github.event.release.tag_name || github.ref_name }}"
TAG="${RAW_TAG//\//-}"
cp build/app/outputs/flutter-apk/app-release.apk "${APP_NAME}-${TAG}-android.apk"
if [ -f build/app/outputs/bundle/release/app-release.aab ]; then
cp build/app/outputs/bundle/release/app-release.aab "${APP_NAME}-${TAG}-play.aab"
fi
- name: Upload APK artifact
- name: Upload Android artifacts
uses: actions/upload-artifact@v4
with:
name: release-android
path: "${{ env.APP_NAME }}-*-android.apk"
path: |
${{ env.APP_NAME }}-*-android.apk
${{ env.APP_NAME }}-*-play.aab
if-no-files-found: error
- name: Upload Play release to internal testing
if: github.event_name == 'release'
env:
GOOGLE_PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
SKIP_ANDROID_BUILD: "1"
run: cd android && bundle exec fastlane android internal
- name: Upload Play release to production
if: github.event_name == 'release' && !github.event.release.prerelease
env:
GOOGLE_PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
SKIP_ANDROID_BUILD: "1"
run: cd android && bundle exec fastlane android production
build-linux:
name: Build Linux App
runs-on: ubuntu-latest
@@ -348,6 +410,7 @@ jobs:
body: |
Release assets are published for all supported build targets:
- Android (`.apk`)
- Google Play (`.aab`)
- Linux (`.tar.gz`)
- macOS (`.dmg`)
- Windows (`.zip`)
@@ -362,6 +425,7 @@ jobs:
4. Install the signed `.ipa` on your iPhone using Apple Configurator 2 or Xcode (Devices and Simulators).
files: |
dist/*.apk
dist/*.aab
dist/*.tar.gz
dist/*.zip
dist/*.dmg

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

@@ -16,7 +16,7 @@
MeshCore SAR helps teams coordinate in low-connectivity or no-connectivity environments with messaging, voice, images, maps, and live location context in one app.
It uses the MeshCore protocol over LoRa for long-range, infrastructure-free communication.
`iOS TestFlight:` https://testflight.apple.com/join/nCZYMPPz
`iOS TestFlight:` https://testflight.apple.com/join/tngpPF12
## Highlights
@@ -75,6 +75,24 @@ It uses the MeshCore protocol over LoRa for long-range, infrastructure-free comm
- Incident command and coordination roles
- Operators working in weak/no cellular coverage
## Connecting Your Radio (BLE)
The app pairs with your MeshCore radio over Bluetooth Low Energy.
### Windows
On Windows 11 (tested on 25H2), pair the radio through the system Bluetooth menu **before** connecting in the app:
1. Open **Settings → Bluetooth & devices → Add device → Bluetooth**.
2. Select your MeshCore radio and complete pairing, entering the **PIN** when prompted.
3. Launch MeshCore SAR and connect — the radio now appears as a paired device.
If the radio does not show up or fails to connect in-app, remove it from the Windows Bluetooth menu and repeat the pairing step.
### iOS / Android
Connect directly from within the app — no separate system pairing step is required.
## Permissions (App Use)
- Bluetooth: mesh device communication

4
android/Gemfile Normal file
View File

@@ -0,0 +1,4 @@
source "https://rubygems.org"
gem "fastlane"
gem "ostruct"

View File

@@ -1,4 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Bluetooth permissions -->
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
@@ -62,6 +63,12 @@
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<service
android:name="id.flutter.flutter_background_service.BackgroundService"
android:exported="false"
android:foregroundServiceType="location"
android:stopWithTask="false"
tools:replace="android:exported" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and

View File

@@ -16,6 +16,24 @@ subprojects {
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
// flutter_avif_android 3.1.0 (latest) ships duplicate Java and Kotlin
// copies of FlutterAvifPlugin. Kotlin 2.2+ rejects the redeclaration. The
// plugin's Java source dir contains only this redundant copy, so empty it
// and let the identical Kotlin implementation compile. kotlinc reads the
// module's Java source roots for joint compilation, so clearing srcDirs
// (rather than an exclude filter, which AGP doesn't forward to kotlinc) is
// what actually removes the duplicate from its view. Registered before
// evaluationDependsOn so it also covers :app, which that call evaluates
// eagerly.
afterEvaluate {
if (project.name == "flutter_avif_android") {
extensions.findByName("android")?.let { ext ->
(ext as com.android.build.gradle.BaseExtension)
.sourceSets.getByName("main").java
.setSrcDirs(emptyList<String>())
}
}
}
project.evaluationDependsOn(":app")
}

View File

@@ -16,44 +16,72 @@
default_platform(:android)
platform :android do
def project_root
File.expand_path("../..", __dir__)
end
def play_service_account_json
ENV.fetch("GOOGLE_PLAY_SERVICE_ACCOUNT_JSON")
end
def play_build_command
"cd #{project_root} && flutter build appbundle --release"
end
def play_aab_path
File.join(project_root, "build/app/outputs/bundle/release/app-release.aab")
end
def play_package_name
"com.meshcore.sar.meshcore_sar_app"
end
def direct_apk_build_command
"cd #{project_root} && flutter build apk --release"
end
def direct_apk_path
File.join(project_root, "build/app/outputs/flutter-apk/app-release.apk")
end
def skip_android_build?
ENV["SKIP_ANDROID_BUILD"] == "1"
end
desc "Runs all the tests"
lane :test do
gradle(task: "test")
end
desc "Build release APK"
lane :beta do
# Get the project root directory (two levels up from android/fastlane/)
project_root = File.expand_path("../..", __dir__)
lane :direct_apk do
sh(direct_apk_build_command)
# Build APK with Flutter
sh("cd #{project_root} && flutter build apk --release")
apk_path = File.join(project_root, "build/app/outputs/flutter-apk/app-release.apk")
# Uncomment to distribute via Firebase App Distribution:
# firebase_app_distribution(
# app: "YOUR_FIREBASE_APP_ID",
# apk_path: apk_path,
# groups: "testers"
# )
UI.success("APK built at: #{apk_path}")
UI.success("APK built successfully at: #{direct_apk_path}")
end
desc "Deploy a new version to the Google Play"
lane :deploy do
# Get the project root directory (two levels up from android/fastlane/)
project_root = File.expand_path("../..", __dir__)
# Build AAB with Flutter (required for Play Store since 2021)
sh("cd #{project_root} && flutter build appbundle --release")
aab_path = File.join(project_root, "build/app/outputs/bundle/release/app-release.aab")
desc "Build Play release AAB and upload to internal testing"
lane :internal do
sh(play_build_command) unless skip_android_build?
upload_to_play_store(
aab: aab_path,
track: "internal"
aab: play_aab_path,
package_name: play_package_name,
track: "internal",
release_status: "draft",
json_key_data: play_service_account_json
)
end
desc "Build Play release AAB and upload to production"
lane :production do
sh(play_build_command) unless skip_android_build?
upload_to_play_store(
aab: play_aab_path,
package_name: play_package_name,
track: "production",
json_key_data: play_service_account_json
)
end
end

View File

@@ -1,3 +1,13 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true
# This builtInKotlin flag was added automatically by Flutter migrator
android.builtInKotlin=false
# This newDsl flag was added automatically by Flutter migrator
android.newDsl=false
# Kotlin 2.2+ fails the build when a module's Java and Kotlin compile tasks
# target different JVM versions. Several Flutter plugins still pin mismatched
# targets (e.g. Java 1.8 vs Kotlin 1.8, or Java 11 vs Kotlin 17). On Android
# every class is dexed by D8/R8, so the class-file version mismatch is benign;
# downgrade the strict check to a warning so third-party plugins build.
kotlin.jvm.target.validation.mode=warning

View File

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip

View File

@@ -19,8 +19,8 @@ pluginManagement {
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.9.1" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")

128
docs/google-play-release.md Normal file
View File

@@ -0,0 +1,128 @@
# Google Play Release Runbook
## Purpose
This runbook prepares and ships the Android release build for `com.meshcore.sar.meshcore_sar_app`.
## Distribution Modes
- GitHub release: signed APK for direct install or manual distribution.
- Google Play: signed AAB uploaded through Fastlane to Play internal testing and, for non-prerelease GitHub releases, production.
## Required Secrets and Local Files
GitHub Actions secrets for `dz0ny/meshcore-sar`:
- `ANDROID_KEYSTORE_BASE64`
- `ANDROID_KEYSTORE_PASSWORD`
- `ANDROID_KEY_ALIAS`
- `ANDROID_KEY_PASSWORD`
- `GOOGLE_PLAY_SERVICE_ACCOUNT_JSON`
Local files used to seed the secrets:
- `android/key.properties`
- release keystore referenced by `android/key.properties`
- `/Users/dz0ny/android-keystores/fastlane-480919-0cb30c62db50.json`
The Play Console service account must have release access for `com.meshcore.sar.meshcore_sar_app`.
## CI Release Flow
The release flow runs from `.github/workflows/build-artifacts.yml` when a GitHub release is published.
Android release steps:
1. Validate all Android signing and Google Play secrets.
2. Recreate `android/release-keystore.jks` and `android/key.properties` from GitHub secrets.
3. Build the signed APK with `flutter build apk --release`.
4. Build the signed Play AAB with `flutter build appbundle --release`.
5. Upload the APK and AAB as GitHub release assets.
6. Upload the AAB to Play internal testing with `bundle exec fastlane android internal`.
7. Upload the AAB to Play production when the GitHub release is not marked as prerelease.
The same workflow also builds Linux, macOS, Windows, iOS unsigned, and web artifacts.
## Versioning Rules
- Version source of truth: `pubspec.yaml`.
- Android `versionCode` is the number after `+`.
- `versionCode` must always increase for Play uploads.
- `make bump`, `make build`, and `make bundle` increment the version.
- Use `make build-no-bump` or `make bundle-no-bump` only when intentionally rebuilding the same version locally.
## Manual Build Commands
Run commands from the repo root.
```bash
flutter build apk --release
flutter build appbundle --release
```
Repo shortcuts:
```bash
make build
make bundle
make build-no-bump
make bundle-no-bump
```
## Fastlane Lanes
Run commands from `android/`.
```bash
bundle exec fastlane android direct_apk
bundle exec fastlane android internal
bundle exec fastlane android production
```
Set `SKIP_ANDROID_BUILD=1` when an AAB has already been built and Fastlane should only upload it.
```bash
SKIP_ANDROID_BUILD=1 GOOGLE_PLAY_SERVICE_ACCOUNT_JSON="$(cat /Users/dz0ny/android-keystores/fastlane-480919-0cb30c62db50.json)" bundle exec fastlane android internal
```
## Expected Artifacts
- APK: `build/app/outputs/flutter-apk/app-release.apk`
- AAB: `build/app/outputs/bundle/release/app-release.aab`
- Release APK asset: `meshcore-sar-<tag>-android.apk`
- Release AAB asset: `meshcore-sar-<tag>-play.aab`
## Internal Testing Release Checklist
1. Confirm `pubspec.yaml` version is correct and `versionCode` increased.
2. Confirm `android/key.properties` points to the release keystore for local builds.
3. Confirm all five GitHub Actions secrets exist.
4. Publish a prerelease in GitHub to build assets and upload Play internal testing without production promotion.
5. Install from the internal testing track on a real Android device.
6. Smoke test startup, permissions, map, messaging, telemetry, and offline map behavior.
## Production Release Checklist
1. Complete internal testing validation first.
2. Confirm Play Console forms are current:
- App content
- Data safety
- App access
- Ads declaration
- Content rating
3. Confirm store assets are current:
- app icon
- feature graphic
- phone screenshots
- tablet screenshots if used
- support URL
- privacy policy URL
4. Publish a non-prerelease GitHub release.
5. Confirm the GitHub release has the APK and AAB assets.
6. Confirm Play Console has the new internal and production release entries.
## Rollback Rules
- Never reuse or lower an Android `versionCode`.
- If a Play release is bad, halt rollout in Play Console and ship a new higher-version fix.
- Keep GitHub direct APK and Play AAB artifacts separate; do not upload APKs to Play.

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

@@ -1,200 +1,51 @@
PODS:
- audioplayers_darwin (0.0.1):
- Flutter
- FlutterMacOS
- codec2_flutter (0.1.0):
- Flutter
- device_info_plus (0.0.1):
- Flutter
- DKImagePickerController/Core (4.3.9):
- DKImagePickerController/ImageDataManager
- DKImagePickerController/Resource
- DKImagePickerController/ImageDataManager (4.3.9)
- DKImagePickerController/PhotoGallery (4.3.9):
- DKImagePickerController/Core
- DKPhotoGallery
- DKImagePickerController/Resource (4.3.9)
- DKPhotoGallery (0.0.19):
- DKPhotoGallery/Core (= 0.0.19)
- DKPhotoGallery/Model (= 0.0.19)
- DKPhotoGallery/Preview (= 0.0.19)
- DKPhotoGallery/Resource (= 0.0.19)
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Core (0.0.19):
- DKPhotoGallery/Model
- DKPhotoGallery/Preview
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Model (0.0.19):
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Preview (0.0.19):
- DKPhotoGallery/Model
- DKPhotoGallery/Resource
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Resource (0.0.19):
- SDWebImage
- SwiftyGif
- file_picker (0.0.1):
- DKImagePickerController/PhotoGallery
- Flutter
- Flutter (1.0.0)
- flutter_avif_ios (0.0.1):
- Flutter
- flutter_background_service_ios (0.0.3):
- Flutter
- flutter_blue_plus_darwin (0.0.2):
- Flutter
- FlutterMacOS
- flutter_compass (0.0.1):
- Flutter
- flutter_local_notifications (0.0.1):
- Flutter
- geolocator_apple (1.2.0):
- Flutter
- FlutterMacOS
- image_picker_ios (0.0.1):
- Flutter
- nsd_ios (0.0.1):
- Flutter
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- record_ios (1.2.0):
- Flutter
- SDWebImage (5.21.7):
- SDWebImage/Core (= 5.21.7)
- SDWebImage/Core (5.21.7)
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- SwiftyGif (5.4.5)
- url_launcher_ios (0.0.1):
- Flutter
- vibration (3.0.0):
- Flutter
- wakelock_plus (0.0.1):
- Flutter
DEPENDENCIES:
- audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/darwin`)
- codec2_flutter (from `.symlinks/plugins/codec2_flutter/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- Flutter (from `Flutter`)
- flutter_avif_ios (from `.symlinks/plugins/flutter_avif_ios/ios`)
- flutter_background_service_ios (from `.symlinks/plugins/flutter_background_service_ios/ios`)
- flutter_blue_plus_darwin (from `.symlinks/plugins/flutter_blue_plus_darwin/darwin`)
- flutter_compass (from `.symlinks/plugins/flutter_compass/ios`)
- 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`)
- 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`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- record_ios (from `.symlinks/plugins/record_ios/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- vibration (from `.symlinks/plugins/vibration/ios`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
SPEC REPOS:
trunk:
- DKImagePickerController
- DKPhotoGallery
- SDWebImage
- SwiftyGif
EXTERNAL SOURCES:
audioplayers_darwin:
:path: ".symlinks/plugins/audioplayers_darwin/darwin"
codec2_flutter:
:path: ".symlinks/plugins/codec2_flutter/ios"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
file_picker:
:path: ".symlinks/plugins/file_picker/ios"
Flutter:
:path: Flutter
flutter_avif_ios:
:path: ".symlinks/plugins/flutter_avif_ios/ios"
flutter_background_service_ios:
:path: ".symlinks/plugins/flutter_background_service_ios/ios"
flutter_blue_plus_darwin:
:path: ".symlinks/plugins/flutter_blue_plus_darwin/darwin"
flutter_compass:
:path: ".symlinks/plugins/flutter_compass/ios"
flutter_local_notifications:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
geolocator_apple:
:path: ".symlinks/plugins/geolocator_apple/darwin"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
nsd_ios:
:path: ".symlinks/plugins/nsd_ios/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
record_ios:
:path: ".symlinks/plugins/record_ios/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
vibration:
:path: ".symlinks/plugins/vibration/ios"
wakelock_plus:
:path: ".symlinks/plugins/wakelock_plus/ios"
SPEC CHECKSUMS:
audioplayers_darwin: 835ced6edd4c9fc8ebb0a7cc9e294a91d99917d5
codec2_flutter: 15e24fa897d9d903a2afb1cc5a17ae3ac88b6d6f
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_avif_ios: 2553d2aafda56339cf924e943da89ad3f40af2dd
flutter_background_service_ios: 00d31bdff7b4bfe06d32375df358abe0329cf87e
flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3
flutter_compass: b236ab69b61545cce89fd58527f401a7587d5cc1
flutter_local_notifications: 643a3eda1ce1c0599413ca31672536d423dee214
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
nsd_ios: 596ad79109ddd3e52d665f650f36428049e3653e
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
record_ios: 412daca2350b228e698fffcd08f1f94ceb1e3844
SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
vibration: ca8104a8875b9c493e15b21b04e456befd0ff6eb
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
PODFILE CHECKSUM: 2d56c9747241a29800bf539d978b210bda037878

View File

@@ -16,6 +16,7 @@
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
B117D6FC7E214B5BDF078335 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5E125ECD1538D6FD469DAD40 /* Pods_RunnerTests.framework */; };
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -65,6 +66,7 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -80,6 +82,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
6E07C83B534F125C2CBE6788 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -111,6 +114,7 @@
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
@@ -187,6 +191,9 @@
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
packageProductDependencies = (
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
);
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
@@ -213,6 +220,9 @@
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
packageReferences = (
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
);
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
@@ -489,7 +499,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 137;
CURRENT_PROJECT_VERSION = 142;
DEVELOPMENT_TEAM = JND55328G8;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -511,7 +521,7 @@
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 137;
CURRENT_PROJECT_VERSION = 142;
DEVELOPMENT_TEAM = JND55328G8;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
@@ -530,7 +540,7 @@
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 137;
CURRENT_PROJECT_VERSION = 142;
DEVELOPMENT_TEAM = JND55328G8;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
@@ -547,7 +557,7 @@
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 137;
CURRENT_PROJECT_VERSION = 142;
DEVELOPMENT_TEAM = JND55328G8;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
@@ -679,7 +689,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 137;
CURRENT_PROJECT_VERSION = 142;
DEVELOPMENT_TEAM = JND55328G8;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -702,7 +712,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 137;
CURRENT_PROJECT_VERSION = 142;
DEVELOPMENT_TEAM = JND55328G8;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -752,6 +762,18 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
isa = XCSwiftPackageProductDependency;
productName = FlutterGeneratedPluginSwiftPackage;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View File

@@ -0,0 +1,59 @@
{
"pins" : [
{
"identity" : "dkcamera",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKCamera",
"state" : {
"branch" : "master",
"revision" : "5c691d11014b910aff69f960475d70e65d9dcc96"
}
},
{
"identity" : "dkimagepickercontroller",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKImagePickerController",
"state" : {
"branch" : "4.3.9",
"revision" : "0bdfeacefa308545adde07bef86e349186335915"
}
},
{
"identity" : "dkphotogallery",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKPhotoGallery",
"state" : {
"branch" : "master",
"revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d"
}
},
{
"identity" : "sdwebimage",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SDWebImage/SDWebImage",
"state" : {
"revision" : "2de3a496eaf6df9a1312862adcfd54acd73c39c0",
"version" : "5.21.7"
}
},
{
"identity" : "swiftygif",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kirualex/SwiftyGif.git",
"state" : {
"revision" : "4430cbc148baa3907651d40562d96325426f409a",
"version" : "5.4.5"
}
},
{
"identity" : "tocropviewcontroller",
"kind" : "remoteSourceControl",
"location" : "https://github.com/TimOliver/TOCropViewController",
"state" : {
"revision" : "d4a6d8100f4b886fdbc8ae399bf144ff3e9afb7e",
"version" : "2.8.0"
}
}
],
"version" : 2
}

View File

@@ -5,6 +5,24 @@
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Prepare Flutter Framework Script"
scriptText = "/bin/sh &quot;$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh&quot; prepare&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"

View File

@@ -0,0 +1,59 @@
{
"pins" : [
{
"identity" : "dkcamera",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKCamera",
"state" : {
"branch" : "master",
"revision" : "5c691d11014b910aff69f960475d70e65d9dcc96"
}
},
{
"identity" : "dkimagepickercontroller",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKImagePickerController",
"state" : {
"branch" : "4.3.9",
"revision" : "0bdfeacefa308545adde07bef86e349186335915"
}
},
{
"identity" : "dkphotogallery",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKPhotoGallery",
"state" : {
"branch" : "master",
"revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d"
}
},
{
"identity" : "sdwebimage",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SDWebImage/SDWebImage",
"state" : {
"revision" : "2de3a496eaf6df9a1312862adcfd54acd73c39c0",
"version" : "5.21.7"
}
},
{
"identity" : "swiftygif",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kirualex/SwiftyGif.git",
"state" : {
"revision" : "4430cbc148baa3907651d40562d96325426f409a",
"version" : "5.4.5"
}
},
{
"identity" : "tocropviewcontroller",
"kind" : "remoteSourceControl",
"location" : "https://github.com/TimOliver/TOCropViewController",
"state" : {
"revision" : "d4a6d8100f4b886fdbc8ae399bf144ff3e9afb7e",
"version" : "2.8.0"
}
}
],
"version" : 2
}

View File

@@ -43,7 +43,7 @@
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>137</string>
<string>142</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>ITSAppUsesNonExemptEncryption</key>

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,22 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000194">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000247">
</testcase>
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.331157">
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.741448">
</testcase>
<testcase classname="fastlane.lanes" name="2: build_app" time="116.573845">
<testcase classname="fastlane.lanes" name="2: build_app" time="110.585223">
</testcase>
<testcase classname="fastlane.lanes" name="3: upload_to_app_store" time="420.572527">
<testcase classname="fastlane.lanes" name="3: upload_to_testflight" time="353.311697">
</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

@@ -1875,5 +1875,125 @@
"enterRegionName": "Regionsname (z.B. auckland)",
"noRegionsFound": "Keine Regionen auf diesem Repeater gefunden.",
"regionScopeSet": "Regionsbereich auf {name} gesetzt",
"regionScopeCleared": "Regionsbereich gelöscht"
"regionScopeCleared": "Regionsbereich gelöscht",
"simpleMode": "Einfacher Modus",
"simpleModeDescription": "Nur Chat und Karte mit favorisierten Kontakten",
"simpleModeRequiresLockedDestination": "Aktivieren Sie zuerst „Ziel sperren“ mit einem Kanal, um den einfachen Modus zu nutzen",
"noFavouriteContacts": "Keine favorisierten Kontakte",
"selectDestinationFirst": "Wählen Sie zuerst einen Kanal, Kontakt oder Raum",
"recipientNoLongerAvailable": "Empfänger ist nicht mehr verfügbar",
"failedToSendMessage": "Senden fehlgeschlagen: {error}",
"tictactoeDirectOnly": "Tic-Tac-Toe funktioniert nur in Direktnachrichten. Wählen Sie zuerst einen Kontakt.",
"deviceKeyUnavailable": "Geräteschlüssel ist nicht verfügbar",
"imageCompressionFailed": "Bildkomprimierung fehlgeschlagen",
"imageFragmentationFailed": "Bildfragmentierung fehlgeschlagen",
"failedToAnnounceImage": "Bild konnte nicht angekündigt werden",
"noRecipientSelected": "Kein Empfänger ausgewählt",
"imageSendFailed": "Bildversand fehlgeschlagen",
"microphonePermissionRequiredForVoice": "Für Sprachnachrichten ist die Mikrofonberechtigung erforderlich",
"recordingDiscarded": "Aufnahme verworfen",
"publicChannelMediaWarning": "Sie sind dabei, {mediaType} an den öffentlichen Kanal zu senden. Das ist nicht ratsam, da alle im Mesh-Netz es empfangen können. Wählen Sie einen privaten oder getaggten Kanal, sofern dies nicht beabsichtigt ist.",
"selectPrivateChannelToShareLocation": "Wählen Sie einen privaten Kanal, um Ihren Standort zu teilen.",
"stopSharingMyLocation": "Meinen Standort nicht mehr teilen",
"shareMyLocation": "Meinen Standort teilen",
"stopRecording": "Aufnahme stoppen",
"recordVoice": "Sprachnachricht aufnehmen",
"moreActions": "Weitere Aktionen",
"noMessagesInFilter": "Keine Nachrichten in diesem Filter",
"noMatchesInFilter": "Keine Treffer in diesem Filter",
"syncFailed": "Synchronisierung fehlgeschlagen: {error}",
"invalidLink": "Ungültiger Link",
"cannotOpenLink": "Link kann nicht geöffnet werden",
"failedToOpenLink": "Link konnte nicht geöffnet werden",
"failedToResendMessage": "Nachricht konnte nicht erneut gesendet werden",
"retryFailed": "Wiederholung fehlgeschlagen: {error}",
"deleteDrawingAndMessage": "Zeichnung & Nachricht löschen",
"lastEchoRelay": "Letztes Echo Relais",
"lastEchoPath": "Letztes Echo Pfad",
"lastEchoBytesReport": "Letztes Echo Byte-Bericht",
"noGpsCoordinatesAvailable": "Keine GPS-Koordinaten verfügbar",
"notASarMarker": "Keine SAR-Markierung",
"failedToSaveTemplate": "Vorlage konnte nicht gespeichert werden: {error}",
"drawingNotFound": "Zeichnung nicht gefunden",
"unknownDrawingType": "Unbekannter Zeichnungstyp",
"customMapMarker": "Eigene Kartenmarkierung",
"customMapPointLabel": "Punkt: {point}",
"mapIdLabel": "Karten-ID: {id}",
"cannotFetchVoice": "Sprachnachricht kann nicht abgerufen werden",
"senderContactUnknown": "Absenderkontakt ist unbekannt. Synchronisieren Sie zuerst die Kontakte.",
"senderRouteUnknown": "Absenderroute ist unbekannt. Synchronisieren Sie zuerst Kontakte/Pfad.",
"messageTooFar": "Nachricht ist zu weit entfernt ({hops} Hops, max. {maxHops}).",
"senderRouteNoPathResponse": "Absenderroute hat auf die Pfadprüfung nicht reagiert. Synchronisieren Sie Kontakte/Pfad und versuchen Sie es erneut.",
"senderRouteNoRawResponse": "Absenderroute hat auf dem Raw-Transportpfad nicht reagiert.",
"voiceFetchOverHops": "Sprachabruf über {hops} Hops kann eine Weile dauern.",
"voiceReceiveCanceled": "Sprachempfang abgebrochen",
"fetchingMissingVoiceFragments": "Fehlende Sprachfragmente werden abgerufen",
"receivingVoice": "Sprachnachricht wird empfangen",
"cannotFetchImage": "Bild kann nicht abgerufen werden",
"imageFetchOverHops": "Bildabruf über {hops} Hops kann eine Weile dauern.",
"imageAlreadyBeingReceived": "Bild wird bereits empfangen",
"loadImage": "Bild laden",
"imageFetchFailedToSendRequest": "Bildabruf-Anfrage konnte nicht gesendet werden",
"imageUnavailable": "Bild derzeit nicht verfügbar",
"imageFetchTimedOut": "Bildabruf hat das Zeitlimit überschritten",
"imageReceiveCanceled": "Bildempfang abgebrochen",
"closeImagePreview": "Bildvorschau schließen",
"recordingReleaseToSend": "Aufnahme läuft ... loslassen zum Senden",
"sendingVoice": "Sprachnachricht wird gesendet ...",
"sendLongPressToRecordVoice": "Senden (lange drücken für Sprachaufnahme)",
"discardRecording": "Aufnahme verwerfen",
"hopCount": "{count, plural, =1{1 Hop} other{{count} Hops}}",
"channelsSectionDescription": "Broadcast-Kanäle für Mesh-Verkehr in der Nähe",
"roomsSectionDescription": "Gemeinsame Räume für laufende Teamkoordination",
"contactsSectionDescription": "Personen und Geräte, die Sie direkt erreichen können",
"noRecentChatters": "Keine kürzlichen Teilnehmer",
"public": "Öffentlich",
"failedToSendTicTacToeMove": "Tic-Tac-Toe-Zug konnte nicht gesendet werden",
"youWon": "Sie haben gewonnen",
"opponentWon": "Gegner hat gewonnen",
"gameDraw": "Unentschieden",
"yourTurn": "Sie sind am Zug",
"opponentTurn": "Gegner ist am Zug",
"pleaseEnterValidCoordinates": "Bitte geben Sie gültige Koordinaten ein",
"locationNotAvailable": "Standort nicht verfügbar",
"customCaveMapPoint": "Eigener Höhlenkarten-Punkt",
"mapPoint": "Kartenpunkt",
"fetchingMissingImageFragments": "Fehlende Fragmente werden abgerufen …",
"loadingImage": "Wird geladen …",
"receivingImage": "Wird empfangen …",
"tapToLoad": "Zum Laden tippen",
"connectDevice": "Gerät verbinden",
"chooseTransportSubtitle": "Wählen Sie Bluetooth, WiFi oder Seriell als Verbindung",
"ble": "BLE",
"network": "Netzwerk",
"serial": "Seriell",
"scan": "Scannen",
"pressScanToSearchForDevices": "Drücken Sie auf Scannen, um nach Geräten in der Nähe zu suchen",
"failedToConnectToDevice": "Verbindung mit {name} fehlgeschlagen",
"unknownDevice": "Unbekanntes Gerät",
"showingCachedResultsTapRefresh": "Zwischengespeicherte Ergebnisse werden angezeigt. Tippen Sie auf Aktualisieren, um erneut zu scannen.",
"scanningLocalNetworkOnPort": "Lokales Netzwerk wird nach MeshCore-WiFi-Geräten auf Port {port} durchsucht",
"cancelScanAndAddServer": "Scan abbrechen und Server hinzufügen",
"addServer": "Server hinzufügen",
"scanningProgressIps": "Scannen... {scanned}/{total} IPs",
"cancelAndEnterManually": "Abbrechen und manuell eingeben",
"noRecentOrDiscoveredServers": "Noch keine kürzlich verwendeten oder gefundenen Server",
"recentlyUsed": "Kürzlich verwendet",
"connecting": "Verbindung wird hergestellt...",
"discoveredOnThisNetwork": "In diesem Netzwerk gefunden",
"serverNoLongerAvailable": "Server unter {host}:{port} ist nicht mehr verfügbar. Bitte scannen Sie erneut, um aktive Server zu finden.",
"failedToConnectToHost": "Verbindung mit {host}:{port} fehlgeschlagen",
"enterValidIpAddress": "Geben Sie eine gültige IP-Adresse ein",
"enterValidTcpPort": "Geben Sie einen gültigen TCP-Port ein",
"ipAddress": "IP-Adresse",
"tcpPort": "TCP-Port",
"customServerPort": "Benutzerdefinierter Server-Port",
"chat": "Chat",
"restoringPreviousLink": "Vorherige Verbindung wird wiederhergestellt",
"noDeviceConnected": "Kein Gerät verbunden",
"sendAdvert": "Meinen Kontakt senden",
"advertMode": "Meinen Kontakt senden",
"advertModeSubtitle": "Wählen Sie, wie weit Ihre Kontaktdaten übertragen werden sollen.",
"floodAdvertSent": "Kontakt über das Mesh-Netzwerk gesendet",
"directAdvertSent": "Kontakt an Geräte in der Nähe gesendet"
}

View File

@@ -2400,7 +2400,7 @@
"offZeroSeconds": "Ανενεργό (0 δ)",
"offlineMaps": "Χάρτες εκτός σύνδεσης",
"older": "Παλαιότερο",
"oneByteMode0": "1 byte (mode 0)",
"oneByteMode0": "1 byte (λειτουργία 0)",
"oneHour": "1 ώρα",
"oneMinute": "1 λεπτό",
"openPacketLogs": "Άνοιγμα αρχείων πακέτων",
@@ -2447,9 +2447,9 @@
"themeLanguageAndDisplayPreferences": "Θέμα, γλώσσα και προτιμήσεις εμφάνισης",
"thirtyMinutes": "30 λεπτά",
"thirtySeconds": "30 δευτερόλεπτα",
"threeBytesMode2": "3 bytes (mode 2)",
"threeBytesMode2": "3 bytes (λειτουργία 2)",
"trafficStatsMessageHistoryAndDeveloperTools": "Στατιστικά κίνησης, ιστορικό μηνυμάτων και εργαλεία προγραμματιστή",
"twoBytesMode1": "2 bytes (mode 1)",
"twoBytesMode1": "2 bytes (λειτουργία 1)",
"twoMinutes": "2 λεπτά",
"undo": "Αναίρεση",
"updated": "Ενημερώθηκε",
@@ -2468,5 +2468,125 @@
"enterRegionName": "Όνομα περιοχής (π.χ. auckland)",
"noRegionsFound": "Δεν βρέθηκαν περιοχές σε αυτόν τον αναμεταδότη.",
"regionScopeSet": "Εύρος περιοχής ορίστηκε σε {name}",
"regionScopeCleared": "Εύρος περιοχής εκκαθαρίστηκε"
"regionScopeCleared": "Εύρος περιοχής εκκαθαρίστηκε",
"simpleMode": "Απλή λειτουργία",
"simpleModeDescription": "Μόνο συνομιλία και χάρτης με αγαπημένες επαφές",
"simpleModeRequiresLockedDestination": "Ενεργοποιήστε πρώτα το «Κλείδωμα προορισμού» με ένα κανάλι για να χρησιμοποιήσετε την Απλή λειτουργία",
"noFavouriteContacts": "Καμία αγαπημένη επαφή",
"selectDestinationFirst": "Επιλέξτε πρώτα κανάλι, επαφή ή δωμάτιο",
"recipientNoLongerAvailable": "Ο παραλήπτης δεν είναι πλέον διαθέσιμος",
"failedToSendMessage": "Αποτυχία αποστολής: {error}",
"tictactoeDirectOnly": "Η τρίλιζα λειτουργεί μόνο σε άμεσα μηνύματα. Επιλέξτε πρώτα μια επαφή.",
"deviceKeyUnavailable": "Το κλειδί της συσκευής δεν είναι διαθέσιμο",
"imageCompressionFailed": "Αποτυχία συμπίεσης εικόνας",
"imageFragmentationFailed": "Αποτυχία κατακερματισμού εικόνας",
"failedToAnnounceImage": "Αποτυχία ανακοίνωσης εικόνας",
"noRecipientSelected": "Δεν επιλέχθηκε παραλήπτης",
"imageSendFailed": "Αποτυχία αποστολής εικόνας",
"microphonePermissionRequiredForVoice": "Απαιτείται άδεια μικροφώνου για φωνητικά μηνύματα",
"recordingDiscarded": "Η ηχογράφηση απορρίφθηκε",
"publicChannelMediaWarning": "Πρόκειται να στείλετε {mediaType} στο δημόσιο κανάλι. Δεν συνιστάται, καθώς μπορεί να το λάβουν όλοι στο δίκτυο mesh. Επιλέξτε ιδιωτικό ή επισημασμένο κανάλι, εκτός αν αυτό θέλετε.",
"selectPrivateChannelToShareLocation": "Επιλέξτε ιδιωτικό κανάλι για να μοιραστείτε την τοποθεσία σας.",
"stopSharingMyLocation": "Διακοπή κοινοποίησης της τοποθεσίας μου",
"shareMyLocation": "Κοινοποίηση της τοποθεσίας μου",
"stopRecording": "Διακοπή ηχογράφησης",
"recordVoice": "Ηχογράφηση φωνητικού μηνύματος",
"moreActions": "Περισσότερες ενέργειες",
"noMessagesInFilter": "Δεν υπάρχουν μηνύματα σε αυτό το φίλτρο",
"noMatchesInFilter": "Δεν υπάρχουν αποτελέσματα σε αυτό το φίλτρο",
"syncFailed": "Αποτυχία συγχρονισμού: {error}",
"invalidLink": "Μη έγκυρος σύνδεσμος",
"cannotOpenLink": "Δεν είναι δυνατό το άνοιγμα του συνδέσμου",
"failedToOpenLink": "Αποτυχία ανοίγματος του συνδέσμου",
"failedToResendMessage": "Αποτυχία επαναποστολής του μηνύματος",
"retryFailed": "Η επανάληψη απέτυχε: {error}",
"deleteDrawingAndMessage": "Διαγραφή σχεδίου και μηνύματος",
"lastEchoRelay": "Τελευταία ηχώ: αναμεταδότης",
"lastEchoPath": "Τελευταία ηχώ: διαδρομή",
"lastEchoBytesReport": "Τελευταία ηχώ: αναφορά byte",
"noGpsCoordinatesAvailable": "Δεν υπάρχουν διαθέσιμες συντεταγμένες GPS",
"notASarMarker": "Δεν είναι δείκτης SAR",
"failedToSaveTemplate": "Αποτυχία αποθήκευσης προτύπου: {error}",
"drawingNotFound": "Το σχέδιο δεν βρέθηκε",
"unknownDrawingType": "Άγνωστος τύπος σχεδίου",
"customMapMarker": "Προσαρμοσμένος δείκτης χάρτη",
"customMapPointLabel": "Σημείο: {point}",
"mapIdLabel": "ID χάρτη: {id}",
"cannotFetchVoice": "Δεν είναι δυνατή η λήψη της φωνής",
"senderContactUnknown": "Η επαφή του αποστολέα είναι άγνωστη. Συγχρονίστε πρώτα τις επαφές.",
"senderRouteUnknown": "Η διαδρομή του αποστολέα είναι άγνωστη. Συγχρονίστε πρώτα επαφές/διαδρομή.",
"messageTooFar": "Το μήνυμα είναι πολύ μακριά ({hops} αναπηδήσεις, μέγ. {maxHops}).",
"senderRouteNoPathResponse": "Η διαδρομή του αποστολέα δεν απάντησε στον έλεγχο διαδρομής. Συγχρονίστε επαφές/διαδρομή και δοκιμάστε ξανά.",
"senderRouteNoRawResponse": "Η διαδρομή του αποστολέα δεν απάντησε στη raw διαδρομή μεταφοράς.",
"voiceFetchOverHops": "Η λήψη φωνής μέσω {hops} αναπηδήσεων μπορεί να διαρκέσει.",
"voiceReceiveCanceled": "Η λήψη φωνής ακυρώθηκε",
"fetchingMissingVoiceFragments": "Λήψη φωνητικών τμημάτων που λείπουν",
"receivingVoice": "Λήψη φωνής",
"cannotFetchImage": "Δεν είναι δυνατή η λήψη της εικόνας",
"imageFetchOverHops": "Η λήψη εικόνας μέσω {hops} αναπηδήσεων μπορεί να διαρκέσει.",
"imageAlreadyBeingReceived": "Η εικόνα λαμβάνεται ήδη",
"loadImage": "Φόρτωση εικόνας",
"imageFetchFailedToSendRequest": "Αποτυχία αποστολής αιτήματος εικόνας",
"imageUnavailable": "Η εικόνα δεν είναι διαθέσιμη αυτήν τη στιγμή",
"imageFetchTimedOut": "Η λήψη της εικόνας έληξε",
"imageReceiveCanceled": "Η λήψη της εικόνας ακυρώθηκε",
"closeImagePreview": "Κλείσιμο προεπισκόπησης εικόνας",
"recordingReleaseToSend": "Ηχογράφηση... αφήστε για αποστολή",
"sendingVoice": "Αποστολή φωνής...",
"sendLongPressToRecordVoice": "Αποστολή (παρατεταμένο πάτημα για ηχογράφηση)",
"discardRecording": "Απόρριψη ηχογράφησης",
"hopCount": "{count, plural, one{{count} αναπήδηση} other{{count} αναπηδήσεις}}",
"channelsSectionDescription": "Κανάλια εκπομπής για κοντινή κίνηση mesh",
"roomsSectionDescription": "Κοινόχρηστοι χώροι για συνεχή συντονισμό ομάδας",
"contactsSectionDescription": "Άτομα και συσκευές που μπορείτε να προσεγγίσετε άμεσα",
"noRecentChatters": "Κανένας πρόσφατος συνομιλητής",
"public": "Δημόσιο",
"failedToSendTicTacToeMove": "Αποτυχία αποστολής κίνησης τρίλιζας",
"youWon": "Κερδίσατε",
"opponentWon": "Κέρδισε ο αντίπαλος",
"gameDraw": "Ισοπαλία",
"yourTurn": "Η σειρά σας",
"opponentTurn": "Σειρά του αντιπάλου",
"pleaseEnterValidCoordinates": "Εισαγάγετε έγκυρες συντεταγμένες",
"locationNotAvailable": "Η τοποθεσία δεν είναι διαθέσιμη",
"customCaveMapPoint": "Προσαρμοσμένο σημείο χάρτη σπηλαίου",
"mapPoint": "Σημείο χάρτη",
"fetchingMissingImageFragments": "Λήψη τμημάτων που λείπουν…",
"loadingImage": "Φόρτωση…",
"receivingImage": "Λήψη…",
"tapToLoad": "Πατήστε για φόρτωση",
"connectDevice": "Σύνδεση συσκευής",
"chooseTransportSubtitle": "Επιλέξτε μεταφορά Bluetooth, WiFi ή σειριακή",
"ble": "BLE",
"network": "Δίκτυο",
"serial": "Σειριακή θύρα",
"scan": "Σάρωση",
"pressScanToSearchForDevices": "Πατήστε «Σάρωση» για αναζήτηση κοντινών συσκευών",
"failedToConnectToDevice": "Αποτυχία σύνδεσης με {name}",
"unknownDevice": "Άγνωστη συσκευή",
"showingCachedResultsTapRefresh": "Εμφανίζονται αποτελέσματα από την προσωρινή μνήμη. Πατήστε ανανέωση για νέα σάρωση.",
"scanningLocalNetworkOnPort": "Σάρωση τοπικού δικτύου για συσκευές MeshCore WiFi στη θύρα {port}",
"cancelScanAndAddServer": "Ακύρωση σάρωσης και προσθήκη διακομιστή",
"addServer": "Προσθήκη διακομιστή",
"scanningProgressIps": "Σάρωση... {scanned}/{total} διευθύνσεις IP",
"cancelAndEnterManually": "Ακύρωση και χειροκίνητη εισαγωγή",
"noRecentOrDiscoveredServers": "Δεν υπάρχουν ακόμη πρόσφατοι ή εντοπισμένοι διακομιστές",
"recentlyUsed": "Πρόσφατα χρησιμοποιημένοι",
"connecting": "Γίνεται σύνδεση...",
"discoveredOnThisNetwork": "Εντοπίστηκαν σε αυτό το δίκτυο",
"serverNoLongerAvailable": "Ο διακομιστής στο {host}:{port} δεν είναι πλέον διαθέσιμος. Σαρώστε ξανά για να βρείτε ενεργούς διακομιστές.",
"failedToConnectToHost": "Αποτυχία σύνδεσης με {host}:{port}",
"enterValidIpAddress": "Εισαγάγετε έγκυρη διεύθυνση IP",
"enterValidTcpPort": "Εισαγάγετε έγκυρη θύρα TCP",
"ipAddress": "Διεύθυνση IP",
"tcpPort": "Θύρα TCP",
"customServerPort": "Προσαρμοσμένη θύρα διακομιστή",
"chat": "Συνομιλία",
"restoringPreviousLink": "Επαναφορά προηγούμενης σύνδεσης",
"noDeviceConnected": "Δεν υπάρχει συνδεδεμένη συσκευή",
"sendAdvert": "Αποστολή της επαφής μου",
"advertMode": "Αποστολή της επαφής μου",
"advertModeSubtitle": "Επιλέξτε πόσο μακριά θα φτάσουν τα στοιχεία επαφής σας.",
"floodAdvertSent": "Η επαφή στάλθηκε σε όλο το δίκτυο mesh",
"directAdvertSent": "Η επαφή στάλθηκε σε κοντινές συσκευές"
}

View File

@@ -2018,9 +2018,9 @@
},
"reply": "Reply",
"@reply": {},
"technicalDetails": "Technical details",
"technicalDetails": "Details",
"@technicalDetails": {},
"messageTechnicalDetails": "Message technical details",
"messageTechnicalDetails": "Message details",
"@messageTechnicalDetails": {},
"linkQuality": "Link quality",
"@linkQuality": {},
@@ -2842,6 +2842,10 @@
}
},
"sizeCompressionAndPreview": "Size, compression, and preview",
"simpleMode": "Simple mode",
"simpleModeDescription": "Only chat and map with favourite contacts",
"simpleModeRequiresLockedDestination": "Enable \"Lock destination\" with a channel first to use Simple mode",
"noFavouriteContacts": "No favourite contacts",
"skip": "Skip",
"smallerFileSize": "Smaller file size",
"sort": "Sort",
@@ -2883,5 +2887,561 @@
"type": "String"
}
}
},
"selectDestinationFirst": "Select a channel, contact, or room first",
"@selectDestinationFirst": {
"description": ""
},
"recipientNoLongerAvailable": "Recipient is no longer available",
"@recipientNoLongerAvailable": {
"description": ""
},
"failedToSendMessage": "Failed to send: {error}",
"@failedToSendMessage": {
"description": "",
"placeholders": {
"error": {
"type": "String"
}
}
},
"tictactoeDirectOnly": "Tic-Tac-Toe works only in direct messages. Choose a contact first.",
"@tictactoeDirectOnly": {
"description": ""
},
"deviceKeyUnavailable": "Device key is unavailable",
"@deviceKeyUnavailable": {
"description": ""
},
"imageCompressionFailed": "Image compression failed",
"@imageCompressionFailed": {
"description": ""
},
"imageFragmentationFailed": "Image fragmentation failed",
"@imageFragmentationFailed": {
"description": ""
},
"failedToAnnounceImage": "Failed to announce image",
"@failedToAnnounceImage": {
"description": ""
},
"noRecipientSelected": "No recipient selected",
"@noRecipientSelected": {
"description": ""
},
"imageSendFailed": "Image send failed",
"@imageSendFailed": {
"description": ""
},
"microphonePermissionRequiredForVoice": "Microphone permission required for voice",
"@microphonePermissionRequiredForVoice": {
"description": ""
},
"recordingDiscarded": "Recording discarded",
"@recordingDiscarded": {
"description": ""
},
"publicChannelMediaWarning": "You are about to send {mediaType} to the Public Channel. This is not advised because everyone on the mesh may receive it. Choose a private or tagged channel unless this is what you want.",
"@publicChannelMediaWarning": {
"description": "",
"placeholders": {
"mediaType": {
"type": "String"
}
}
},
"selectPrivateChannelToShareLocation": "Select a private channel to share your location.",
"@selectPrivateChannelToShareLocation": {
"description": ""
},
"stopSharingMyLocation": "Stop sharing my location",
"@stopSharingMyLocation": {
"description": ""
},
"shareMyLocation": "Share my location",
"@shareMyLocation": {
"description": ""
},
"stopRecording": "Stop recording",
"@stopRecording": {
"description": ""
},
"recordVoice": "Record voice",
"@recordVoice": {
"description": ""
},
"moreActions": "More actions",
"@moreActions": {
"description": ""
},
"noMessagesInFilter": "No messages in this filter",
"@noMessagesInFilter": {
"description": ""
},
"noMatchesInFilter": "No matches in this filter",
"@noMatchesInFilter": {
"description": ""
},
"syncFailed": "Sync failed: {error}",
"@syncFailed": {
"description": "",
"placeholders": {
"error": {
"type": "String"
}
}
},
"invalidLink": "Invalid link",
"@invalidLink": {
"description": ""
},
"cannotOpenLink": "Cannot open link",
"@cannotOpenLink": {
"description": ""
},
"failedToOpenLink": "Failed to open link",
"@failedToOpenLink": {
"description": ""
},
"failedToResendMessage": "Failed to resend message",
"@failedToResendMessage": {
"description": ""
},
"retryFailed": "Retry failed: {error}",
"@retryFailed": {
"description": "",
"placeholders": {
"error": {
"type": "String"
}
}
},
"deleteDrawingAndMessage": "Delete drawing & message",
"@deleteDrawingAndMessage": {
"description": ""
},
"lastEchoRelay": "Last echo relay",
"@lastEchoRelay": {
"description": ""
},
"lastEchoPath": "Last echo path",
"@lastEchoPath": {
"description": ""
},
"lastEchoBytesReport": "Last echo bytes report",
"@lastEchoBytesReport": {
"description": ""
},
"noGpsCoordinatesAvailable": "No GPS coordinates available",
"@noGpsCoordinatesAvailable": {
"description": ""
},
"notASarMarker": "Not a SAR marker",
"@notASarMarker": {
"description": ""
},
"failedToSaveTemplate": "Failed to save template: {error}",
"@failedToSaveTemplate": {
"description": "",
"placeholders": {
"error": {
"type": "String"
}
}
},
"drawingNotFound": "Drawing not found",
"@drawingNotFound": {
"description": ""
},
"unknownDrawingType": "Unknown drawing type",
"@unknownDrawingType": {
"description": ""
},
"customMapMarker": "Custom map marker",
"@customMapMarker": {
"description": ""
},
"customMapPointLabel": "Point: {point}",
"@customMapPointLabel": {
"description": "",
"placeholders": {
"point": {
"type": "String"
}
}
},
"mapIdLabel": "Map ID: {id}",
"@mapIdLabel": {
"description": "",
"placeholders": {
"id": {
"type": "String"
}
}
},
"cannotFetchVoice": "Cannot fetch voice",
"@cannotFetchVoice": {
"description": ""
},
"senderContactUnknown": "Sender contact is unknown. Sync contacts first.",
"@senderContactUnknown": {
"description": ""
},
"senderRouteUnknown": "Sender route is unknown. Sync contacts/path first.",
"@senderRouteUnknown": {
"description": ""
},
"messageTooFar": "Message is too far ({hops} hops, max {maxHops}).",
"@messageTooFar": {
"description": "",
"placeholders": {
"hops": {
"type": "String"
},
"maxHops": {
"type": "String"
}
}
},
"senderRouteNoPathResponse": "Sender route did not respond to a path check. Sync contacts/path and try again.",
"@senderRouteNoPathResponse": {
"description": ""
},
"senderRouteNoRawResponse": "Sender route did not respond on the raw transport path.",
"@senderRouteNoRawResponse": {
"description": ""
},
"voiceFetchOverHops": "Voice fetch over {hops} hops may take a while.",
"@voiceFetchOverHops": {
"description": "",
"placeholders": {
"hops": {
"type": "String"
}
}
},
"voiceReceiveCanceled": "Voice receive canceled",
"@voiceReceiveCanceled": {
"description": ""
},
"fetchingMissingVoiceFragments": "Fetching missing voice fragments",
"@fetchingMissingVoiceFragments": {
"description": ""
},
"receivingVoice": "Receiving voice",
"@receivingVoice": {
"description": ""
},
"cannotFetchImage": "Cannot fetch image",
"@cannotFetchImage": {
"description": ""
},
"imageFetchOverHops": "Image fetch over {hops} hops may take a while.",
"@imageFetchOverHops": {
"description": "",
"placeholders": {
"hops": {
"type": "String"
}
}
},
"imageAlreadyBeingReceived": "Image is already being received",
"@imageAlreadyBeingReceived": {
"description": ""
},
"loadImage": "Load image",
"@loadImage": {
"description": ""
},
"imageFetchFailedToSendRequest": "Image fetch failed to send request",
"@imageFetchFailedToSendRequest": {
"description": ""
},
"imageUnavailable": "Image unavailable right now",
"@imageUnavailable": {
"description": ""
},
"imageFetchTimedOut": "Image fetch timed out",
"@imageFetchTimedOut": {
"description": ""
},
"imageReceiveCanceled": "Image receive canceled",
"@imageReceiveCanceled": {
"description": ""
},
"closeImagePreview": "Close image preview",
"@closeImagePreview": {
"description": ""
},
"recordingReleaseToSend": "Recording... release to send voice",
"@recordingReleaseToSend": {
"description": ""
},
"sendingVoice": "Sending voice...",
"@sendingVoice": {
"description": ""
},
"sendLongPressToRecordVoice": "Send (long press to record voice)",
"@sendLongPressToRecordVoice": {
"description": ""
},
"discardRecording": "Discard recording",
"@discardRecording": {
"description": ""
},
"hopCount": "{count, plural, =1{1 hop} other{{count} hops}}",
"@hopCount": {
"description": "",
"placeholders": {
"count": {
"type": "num"
}
}
},
"channelsSectionDescription": "Broadcast lanes for nearby mesh traffic",
"@channelsSectionDescription": {
"description": ""
},
"roomsSectionDescription": "Shared spaces for ongoing team coordination",
"@roomsSectionDescription": {
"description": ""
},
"contactsSectionDescription": "Direct people and devices you can reach",
"@contactsSectionDescription": {
"description": ""
},
"noRecentChatters": "No recent chatters",
"@noRecentChatters": {
"description": ""
},
"public": "Public",
"@public": {
"description": ""
},
"failedToSendTicTacToeMove": "Failed to send Tic-Tac-Toe move",
"@failedToSendTicTacToeMove": {
"description": ""
},
"youWon": "You won",
"@youWon": {
"description": ""
},
"opponentWon": "Opponent won",
"@opponentWon": {
"description": ""
},
"gameDraw": "Draw",
"@gameDraw": {
"description": ""
},
"yourTurn": "Your turn",
"@yourTurn": {
"description": ""
},
"opponentTurn": "Opponent turn",
"@opponentTurn": {
"description": ""
},
"pleaseEnterValidCoordinates": "Please enter valid coordinates",
"@pleaseEnterValidCoordinates": {
"description": ""
},
"locationNotAvailable": "Location not available",
"@locationNotAvailable": {
"description": ""
},
"customCaveMapPoint": "Custom cave map point",
"@customCaveMapPoint": {
"description": ""
},
"mapPoint": "Map point",
"@mapPoint": {
"description": ""
},
"fetchingMissingImageFragments": "Fetching missing fragments…",
"@fetchingMissingImageFragments": {
"description": ""
},
"loadingImage": "Loading…",
"@loadingImage": {
"description": ""
},
"receivingImage": "Receiving…",
"@receivingImage": {
"description": ""
},
"tapToLoad": "Tap to load",
"@tapToLoad": {
"description": ""
},
"connectDevice": "Connect Device",
"@connectDevice": {
"description": "Connection dialog title"
},
"chooseTransportSubtitle": "Choose Bluetooth, WiFi, or Serial transport",
"@chooseTransportSubtitle": {
"description": "Connection dialog subtitle describing available transports"
},
"ble": "BLE",
"@ble": {
"description": "Bluetooth Low Energy tab label in connection dialog"
},
"network": "Network",
"@network": {
"description": "Network tab label in connection dialog"
},
"serial": "Serial",
"@serial": {
"description": "Serial tab label in connection dialog"
},
"scan": "Scan",
"@scan": {
"description": "Button label to start scanning for devices"
},
"pressScanToSearchForDevices": "Press scan to search for nearby devices",
"@pressScanToSearchForDevices": {
"description": "Empty state shown before the first BLE scan"
},
"failedToConnectToDevice": "Failed to connect to {name}",
"@failedToConnectToDevice": {
"description": "Error when connecting to a BLE device fails",
"placeholders": {
"name": {
"type": "String"
}
}
},
"unknownDevice": "Unknown Device",
"@unknownDevice": {
"description": "Fallback name for a BLE device without a name"
},
"showingCachedResultsTapRefresh": "Showing cached results. Tap refresh to rescan.",
"@showingCachedResultsTapRefresh": {
"description": "Banner shown when cached network scan results are displayed"
},
"scanningLocalNetworkOnPort": "Scanning local network for MeshCore WiFi devices on port {port}",
"@scanningLocalNetworkOnPort": {
"description": "Banner shown while scanning the local network",
"placeholders": {
"port": {
"type": "int"
}
}
},
"cancelScanAndAddServer": "Cancel scan and add server",
"@cancelScanAndAddServer": {
"description": "Tooltip for the add-server button while a network scan is running"
},
"addServer": "Add server",
"@addServer": {
"description": "Tooltip for the add-server button"
},
"scanningProgressIps": "Scanning... {scanned}/{total} IPs",
"@scanningProgressIps": {
"description": "Network scan progress label",
"placeholders": {
"scanned": {
"type": "int"
},
"total": {
"type": "String"
}
}
},
"cancelAndEnterManually": "Cancel and enter manually",
"@cancelAndEnterManually": {
"description": "Button to cancel the network scan and enter a server address manually"
},
"noRecentOrDiscoveredServers": "No recent or discovered servers yet",
"@noRecentOrDiscoveredServers": {
"description": "Empty state for the network servers tab"
},
"recentlyUsed": "Recently used",
"@recentlyUsed": {
"description": "Section header for recently used servers"
},
"connecting": "Connecting...",
"@connecting": {
"description": "Status label while a connection is being established"
},
"discoveredOnThisNetwork": "Discovered on this network",
"@discoveredOnThisNetwork": {
"description": "Section header for servers discovered on the local network"
},
"serverNoLongerAvailable": "Server at {host}:{port} is no longer available. Please scan again to find active servers.",
"@serverNoLongerAvailable": {
"description": "Error when a previously discovered server does not respond anymore",
"placeholders": {
"host": {
"type": "String"
},
"port": {
"type": "int"
}
}
},
"failedToConnectToHost": "Failed to connect to {host}:{port}",
"@failedToConnectToHost": {
"description": "Error when a TCP connection attempt fails",
"placeholders": {
"host": {
"type": "String"
},
"port": {
"type": "int"
}
}
},
"enterValidIpAddress": "Enter a valid IP address",
"@enterValidIpAddress": {
"description": "Validation error for the IP address field"
},
"enterValidTcpPort": "Enter a valid TCP port",
"@enterValidTcpPort": {
"description": "Validation error for the TCP port field"
},
"ipAddress": "IP address",
"@ipAddress": {
"description": "Label for the IP address text field"
},
"tcpPort": "TCP port",
"@tcpPort": {
"description": "Label for the TCP port text field"
},
"customServerPort": "Custom server port",
"@customServerPort": {
"description": "Helper text for the TCP port text field"
},
"chat": "Chat",
"@chat": {
"description": "Chat tab label on the home screen"
},
"restoringPreviousLink": "Restoring previous link",
"@restoringPreviousLink": {
"description": "Status text while automatically reconnecting to the last device"
},
"noDeviceConnected": "No device connected",
"@noDeviceConnected": {
"description": "Status text when no device is connected"
},
"sendAdvert": "Send my contact",
"@sendAdvert": {
"description": "Composer action that broadcasts this device's contact info (advert) to the mesh"
},
"advertMode": "Send my contact",
"@advertMode": {
"description": "Title of the sheet choosing how far the contact broadcast travels"
},
"advertModeSubtitle": "Choose how far your contact info should travel.",
"@advertModeSubtitle": {
"description": "Subtitle of the contact broadcast mode sheet"
},
"floodAdvertSent": "Contact sent across the mesh",
"@floodAdvertSent": {
"description": "Toast after broadcasting contact info via flood (through repeaters)"
},
"directAdvertSent": "Contact sent to nearby devices",
"@directAdvertSent": {
"description": "Toast after broadcasting contact info direct (zero-hop, nearby only)"
}
}

View File

@@ -1875,5 +1875,125 @@
"enterRegionName": "Nombre de región (ej. auckland)",
"noRegionsFound": "No se encontraron regiones en este repetidor.",
"regionScopeSet": "Ámbito de región establecido a {name}",
"regionScopeCleared": "Ámbito de región limpiado"
"regionScopeCleared": "Ámbito de región limpiado",
"simpleMode": "Modo simple",
"simpleModeDescription": "Solo chat y mapa con contactos favoritos",
"simpleModeRequiresLockedDestination": "Activa primero «Bloquear destino» con un canal para usar el modo simple",
"noFavouriteContacts": "Sin contactos favoritos",
"selectDestinationFirst": "Selecciona primero un canal, contacto o sala",
"recipientNoLongerAvailable": "El destinatario ya no está disponible",
"failedToSendMessage": "Error al enviar: {error}",
"tictactoeDirectOnly": "El tres en raya solo funciona en mensajes directos. Elige primero un contacto.",
"deviceKeyUnavailable": "La clave del dispositivo no está disponible",
"imageCompressionFailed": "Error al comprimir la imagen",
"imageFragmentationFailed": "Error al fragmentar la imagen",
"failedToAnnounceImage": "Error al anunciar la imagen",
"noRecipientSelected": "Ningún destinatario seleccionado",
"imageSendFailed": "Error al enviar la imagen",
"microphonePermissionRequiredForVoice": "Se necesita permiso de micrófono para los mensajes de voz",
"recordingDiscarded": "Grabación descartada",
"publicChannelMediaWarning": "Estás a punto de enviar {mediaType} al canal público. No es recomendable, porque cualquiera en la red mesh podría recibirlo. Elige un canal privado o etiquetado, salvo que sea lo que quieres.",
"selectPrivateChannelToShareLocation": "Selecciona un canal privado para compartir tu ubicación.",
"stopSharingMyLocation": "Dejar de compartir mi ubicación",
"shareMyLocation": "Compartir mi ubicación",
"stopRecording": "Detener grabación",
"recordVoice": "Grabar voz",
"moreActions": "Más acciones",
"noMessagesInFilter": "No hay mensajes en este filtro",
"noMatchesInFilter": "No hay coincidencias en este filtro",
"syncFailed": "Error de sincronización: {error}",
"invalidLink": "Enlace no válido",
"cannotOpenLink": "No se puede abrir el enlace",
"failedToOpenLink": "Error al abrir el enlace",
"failedToResendMessage": "Error al reenviar el mensaje",
"retryFailed": "Error al reintentar: {error}",
"deleteDrawingAndMessage": "Eliminar dibujo y mensaje",
"lastEchoRelay": "Último eco: repetidor",
"lastEchoPath": "Último eco: ruta",
"lastEchoBytesReport": "Último eco: informe de bytes",
"noGpsCoordinatesAvailable": "No hay coordenadas GPS disponibles",
"notASarMarker": "No es un marcador SAR",
"failedToSaveTemplate": "Error al guardar la plantilla: {error}",
"drawingNotFound": "Dibujo no encontrado",
"unknownDrawingType": "Tipo de dibujo desconocido",
"customMapMarker": "Marcador de mapa personalizado",
"customMapPointLabel": "Punto: {point}",
"mapIdLabel": "ID del mapa: {id}",
"cannotFetchVoice": "No se puede obtener la voz",
"senderContactUnknown": "El contacto del remitente es desconocido. Sincroniza primero los contactos.",
"senderRouteUnknown": "La ruta del remitente es desconocida. Sincroniza primero los contactos/la ruta.",
"messageTooFar": "El mensaje está demasiado lejos ({hops} saltos, máx. {maxHops}).",
"senderRouteNoPathResponse": "La ruta del remitente no respondió a la comprobación de ruta. Sincroniza los contactos/la ruta e inténtalo de nuevo.",
"senderRouteNoRawResponse": "La ruta del remitente no respondió en la ruta de transporte raw.",
"voiceFetchOverHops": "Obtener la voz a través de {hops} saltos puede tardar un poco.",
"voiceReceiveCanceled": "Recepción de voz cancelada",
"fetchingMissingVoiceFragments": "Obteniendo fragmentos de voz que faltan",
"receivingVoice": "Recibiendo voz",
"cannotFetchImage": "No se puede obtener la imagen",
"imageFetchOverHops": "Obtener la imagen a través de {hops} saltos puede tardar un poco.",
"imageAlreadyBeingReceived": "La imagen ya se está recibiendo",
"loadImage": "Cargar imagen",
"imageFetchFailedToSendRequest": "No se pudo enviar la solicitud de imagen",
"imageUnavailable": "Imagen no disponible por ahora",
"imageFetchTimedOut": "La obtención de la imagen agotó el tiempo de espera",
"imageReceiveCanceled": "Recepción de imagen cancelada",
"closeImagePreview": "Cerrar vista previa de imagen",
"recordingReleaseToSend": "Grabando... suelta para enviar la voz",
"sendingVoice": "Enviando voz...",
"sendLongPressToRecordVoice": "Enviar (mantén pulsado para grabar voz)",
"discardRecording": "Descartar grabación",
"hopCount": "{count, plural, =1{1 salto} other{{count} saltos}}",
"channelsSectionDescription": "Canales de difusión para el tráfico mesh cercano",
"roomsSectionDescription": "Espacios compartidos para la coordinación del equipo",
"contactsSectionDescription": "Personas y dispositivos a tu alcance directo",
"noRecentChatters": "Sin participantes recientes",
"public": "Público",
"failedToSendTicTacToeMove": "No se pudo enviar el movimiento de tres en raya",
"youWon": "Has ganado",
"opponentWon": "Ha ganado el rival",
"gameDraw": "Empate",
"yourTurn": "Te toca",
"opponentTurn": "Le toca al rival",
"pleaseEnterValidCoordinates": "Introduce coordenadas válidas",
"locationNotAvailable": "Ubicación no disponible",
"customCaveMapPoint": "Punto de mapa de cueva personalizado",
"mapPoint": "Punto del mapa",
"fetchingMissingImageFragments": "Obteniendo fragmentos que faltan…",
"loadingImage": "Cargando…",
"receivingImage": "Recibiendo…",
"tapToLoad": "Toca para cargar",
"connectDevice": "Conectar dispositivo",
"chooseTransportSubtitle": "Elige el transporte Bluetooth, WiFi o Serie",
"ble": "BLE",
"network": "Red",
"serial": "Serie",
"scan": "Buscar",
"pressScanToSearchForDevices": "Pulsa Buscar para encontrar dispositivos cercanos",
"failedToConnectToDevice": "Error al conectar con {name}",
"unknownDevice": "Dispositivo desconocido",
"showingCachedResultsTapRefresh": "Mostrando resultados en caché. Toca actualizar para volver a buscar.",
"scanningLocalNetworkOnPort": "Buscando dispositivos MeshCore WiFi en la red local en el puerto {port}",
"cancelScanAndAddServer": "Cancelar búsqueda y añadir servidor",
"addServer": "Añadir servidor",
"scanningProgressIps": "Buscando... {scanned}/{total} IP",
"cancelAndEnterManually": "Cancelar e introducir manualmente",
"noRecentOrDiscoveredServers": "Aún no hay servidores recientes ni descubiertos",
"recentlyUsed": "Usados recientemente",
"connecting": "Conectando...",
"discoveredOnThisNetwork": "Descubiertos en esta red",
"serverNoLongerAvailable": "El servidor en {host}:{port} ya no está disponible. Vuelve a buscar para encontrar servidores activos.",
"failedToConnectToHost": "Error al conectar con {host}:{port}",
"enterValidIpAddress": "Introduce una dirección IP válida",
"enterValidTcpPort": "Introduce un puerto TCP válido",
"ipAddress": "Dirección IP",
"tcpPort": "Puerto TCP",
"customServerPort": "Puerto de servidor personalizado",
"chat": "Chat",
"restoringPreviousLink": "Restaurando el enlace anterior",
"noDeviceConnected": "Ningún dispositivo conectado",
"sendAdvert": "Enviar mi contacto",
"advertMode": "Enviar mi contacto",
"advertModeSubtitle": "Elige hasta dónde debe llegar tu información de contacto.",
"floodAdvertSent": "Contacto enviado a través de la red mesh",
"directAdvertSent": "Contacto enviado a dispositivos cercanos"
}

View File

@@ -1878,5 +1878,125 @@
"enterRegionName": "Nom de région (ex. auckland)",
"noRegionsFound": "Aucune région trouvée sur ce relais.",
"regionScopeSet": "Portée régionale définie sur {name}",
"regionScopeCleared": "Portée régionale effacée"
"regionScopeCleared": "Portée régionale effacée",
"simpleMode": "Mode simple",
"simpleModeDescription": "Uniquement le chat et la carte avec les contacts favoris",
"simpleModeRequiresLockedDestination": "Activez d'abord « Verrouiller la destination » avec un canal pour utiliser le mode simple",
"noFavouriteContacts": "Aucun contact favori",
"selectDestinationFirst": "Sélectionnez d'abord un canal, un contact ou un salon",
"recipientNoLongerAvailable": "Le destinataire n'est plus disponible",
"failedToSendMessage": "Échec de l'envoi : {error}",
"tictactoeDirectOnly": "Le morpion ne fonctionne qu'en messages directs. Choisissez d'abord un contact.",
"deviceKeyUnavailable": "La clé de l'appareil n'est pas disponible",
"imageCompressionFailed": "Échec de la compression de l'image",
"imageFragmentationFailed": "Échec de la fragmentation de l'image",
"failedToAnnounceImage": "Échec de l'annonce de l'image",
"noRecipientSelected": "Aucun destinataire sélectionné",
"imageSendFailed": "Échec de l'envoi de l'image",
"microphonePermissionRequiredForVoice": "L'autorisation du microphone est requise pour la voix",
"recordingDiscarded": "Enregistrement abandonné",
"publicChannelMediaWarning": "Vous êtes sur le point d'envoyer {mediaType} sur le canal public. Ce n'est pas conseillé, car tout le monde sur le réseau mesh peut le recevoir. Choisissez un canal privé ou étiqueté, sauf si c'est bien votre intention.",
"selectPrivateChannelToShareLocation": "Sélectionnez un canal privé pour partager votre position.",
"stopSharingMyLocation": "Arrêter de partager ma position",
"shareMyLocation": "Partager ma position",
"stopRecording": "Arrêter l'enregistrement",
"recordVoice": "Enregistrer un message vocal",
"moreActions": "Plus d'actions",
"noMessagesInFilter": "Aucun message dans ce filtre",
"noMatchesInFilter": "Aucun résultat dans ce filtre",
"syncFailed": "Échec de la synchronisation : {error}",
"invalidLink": "Lien invalide",
"cannotOpenLink": "Impossible d'ouvrir le lien",
"failedToOpenLink": "Échec de l'ouverture du lien",
"failedToResendMessage": "Échec du renvoi du message",
"retryFailed": "Échec de la nouvelle tentative : {error}",
"deleteDrawingAndMessage": "Supprimer le dessin et le message",
"lastEchoRelay": "Dernier écho : relais",
"lastEchoPath": "Dernier écho : chemin",
"lastEchoBytesReport": "Dernier écho : rapport d'octets",
"noGpsCoordinatesAvailable": "Aucune coordonnée GPS disponible",
"notASarMarker": "Ce n'est pas un marqueur SAR",
"failedToSaveTemplate": "Échec de l'enregistrement du modèle : {error}",
"drawingNotFound": "Dessin introuvable",
"unknownDrawingType": "Type de dessin inconnu",
"customMapMarker": "Marqueur de carte personnalisé",
"customMapPointLabel": "Point : {point}",
"mapIdLabel": "ID de carte : {id}",
"cannotFetchVoice": "Impossible de récupérer la voix",
"senderContactUnknown": "Le contact de l'expéditeur est inconnu. Synchronisez d'abord les contacts.",
"senderRouteUnknown": "La route de l'expéditeur est inconnue. Synchronisez d'abord les contacts/le chemin.",
"messageTooFar": "Le message est trop loin ({hops} sauts, max. {maxHops}).",
"senderRouteNoPathResponse": "La route de l'expéditeur n'a pas répondu à la vérification du chemin. Synchronisez les contacts/le chemin et réessayez.",
"senderRouteNoRawResponse": "La route de l'expéditeur n'a pas répondu sur le chemin de transport brut.",
"voiceFetchOverHops": "La récupération de la voix sur {hops} sauts peut prendre un moment.",
"voiceReceiveCanceled": "Réception vocale annulée",
"fetchingMissingVoiceFragments": "Récupération des fragments vocaux manquants",
"receivingVoice": "Réception de la voix",
"cannotFetchImage": "Impossible de récupérer l'image",
"imageFetchOverHops": "La récupération de l'image sur {hops} sauts peut prendre un moment.",
"imageAlreadyBeingReceived": "L'image est déjà en cours de réception",
"loadImage": "Charger l'image",
"imageFetchFailedToSendRequest": "Échec de l'envoi de la demande d'image",
"imageUnavailable": "Image indisponible pour le moment",
"imageFetchTimedOut": "Délai de récupération de l'image dépassé",
"imageReceiveCanceled": "Réception de l'image annulée",
"closeImagePreview": "Fermer l'aperçu de l'image",
"recordingReleaseToSend": "Enregistrement... relâchez pour envoyer",
"sendingVoice": "Envoi du message vocal...",
"sendLongPressToRecordVoice": "Envoyer (appui long pour enregistrer la voix)",
"discardRecording": "Abandonner l'enregistrement",
"hopCount": "{count, plural, one{{count} saut} other{{count} sauts}}",
"channelsSectionDescription": "Canaux de diffusion pour le trafic mesh à proximité",
"roomsSectionDescription": "Espaces partagés pour la coordination d'équipe",
"contactsSectionDescription": "Personnes et appareils joignables directement",
"noRecentChatters": "Aucun participant récent",
"public": "Public",
"failedToSendTicTacToeMove": "Échec de l'envoi du coup de morpion",
"youWon": "Vous avez gagné",
"opponentWon": "L'adversaire a gagné",
"gameDraw": "Match nul",
"yourTurn": "À vous de jouer",
"opponentTurn": "Au tour de l'adversaire",
"pleaseEnterValidCoordinates": "Veuillez saisir des coordonnées valides",
"locationNotAvailable": "Position non disponible",
"customCaveMapPoint": "Point de carte de grotte personnalisé",
"mapPoint": "Point de carte",
"fetchingMissingImageFragments": "Récupération des fragments manquants…",
"loadingImage": "Chargement…",
"receivingImage": "Réception…",
"tapToLoad": "Touchez pour charger",
"connectDevice": "Connecter l'appareil",
"chooseTransportSubtitle": "Choisissez le transport Bluetooth, WiFi ou Série",
"ble": "BLE",
"network": "Réseau",
"serial": "Série",
"scan": "Rechercher",
"pressScanToSearchForDevices": "Appuyez sur Rechercher pour trouver les appareils à proximité",
"failedToConnectToDevice": "Échec de la connexion à {name}",
"unknownDevice": "Appareil inconnu",
"showingCachedResultsTapRefresh": "Affichage des résultats en cache. Touchez Actualiser pour relancer la recherche.",
"scanningLocalNetworkOnPort": "Recherche d'appareils MeshCore WiFi sur le réseau local, port {port}",
"cancelScanAndAddServer": "Annuler la recherche et ajouter un serveur",
"addServer": "Ajouter un serveur",
"scanningProgressIps": "Recherche... {scanned}/{total} IP",
"cancelAndEnterManually": "Annuler et saisir manuellement",
"noRecentOrDiscoveredServers": "Aucun serveur récent ou découvert pour le moment",
"recentlyUsed": "Utilisés récemment",
"connecting": "Connexion...",
"discoveredOnThisNetwork": "Découverts sur ce réseau",
"serverNoLongerAvailable": "Le serveur {host}:{port} n'est plus disponible. Veuillez relancer la recherche pour trouver des serveurs actifs.",
"failedToConnectToHost": "Échec de la connexion à {host}:{port}",
"enterValidIpAddress": "Saisissez une adresse IP valide",
"enterValidTcpPort": "Saisissez un port TCP valide",
"ipAddress": "Adresse IP",
"tcpPort": "Port TCP",
"customServerPort": "Port de serveur personnalisé",
"chat": "Chat",
"restoringPreviousLink": "Restauration de la liaison précédente",
"noDeviceConnected": "Aucun appareil connecté",
"sendAdvert": "Envoyer mon contact",
"advertMode": "Envoyer mon contact",
"advertModeSubtitle": "Choisissez jusqu'où vos informations de contact doivent être diffusées.",
"floodAdvertSent": "Contact envoyé à travers le réseau mesh",
"directAdvertSent": "Contact envoyé aux appareils à proximité"
}

View File

@@ -908,5 +908,125 @@
"enterRegionName": "Naziv regije (npr. auckland)",
"noRegionsFound": "Nema pronađenih regija na ovom repetitoru.",
"regionScopeSet": "Opseg regije postavljen na {name}",
"regionScopeCleared": "Opseg regije obrisan"
"regionScopeCleared": "Opseg regije obrisan",
"simpleMode": "Jednostavni način",
"simpleModeDescription": "Samo razgovor i karta s omiljenim kontaktima",
"simpleModeRequiresLockedDestination": "Za korištenje jednostavnog načina najprije omogućite „Zaključaj odredište” s kanalom",
"noFavouriteContacts": "Nema omiljenih kontakata",
"selectDestinationFirst": "Najprije odaberite kanal, kontakt ili sobu",
"recipientNoLongerAvailable": "Primatelj više nije dostupan",
"failedToSendMessage": "Slanje nije uspjelo: {error}",
"tictactoeDirectOnly": "Križić-kružić radi samo u izravnim porukama. Najprije odaberite kontakt.",
"deviceKeyUnavailable": "Ključ uređaja nije dostupan",
"imageCompressionFailed": "Komprimiranje slike nije uspjelo",
"imageFragmentationFailed": "Fragmentiranje slike nije uspjelo",
"failedToAnnounceImage": "Najava slike nije uspjela",
"noRecipientSelected": "Primatelj nije odabran",
"imageSendFailed": "Slanje slike nije uspjelo",
"microphonePermissionRequiredForVoice": "Za glasovne poruke potrebna je dozvola za mikrofon",
"recordingDiscarded": "Snimka je odbačena",
"publicChannelMediaWarning": "Upravo ćete poslati {mediaType} na javni kanal. To se ne preporučuje jer sadržaj mogu primiti svi na mesh mreži. Odaberite privatni ili označeni kanal, osim ako je to baš ono što želite.",
"selectPrivateChannelToShareLocation": "Odaberite privatni kanal za dijeljenje lokacije.",
"stopSharingMyLocation": "Prestani dijeliti moju lokaciju",
"shareMyLocation": "Dijeli moju lokaciju",
"stopRecording": "Zaustavi snimanje",
"recordVoice": "Snimi glasovnu poruku",
"moreActions": "Više radnji",
"noMessagesInFilter": "Nema poruka u ovom filtru",
"noMatchesInFilter": "Nema rezultata u ovom filtru",
"syncFailed": "Sinkronizacija nije uspjela: {error}",
"invalidLink": "Nevažeća poveznica",
"cannotOpenLink": "Poveznicu nije moguće otvoriti",
"failedToOpenLink": "Otvaranje poveznice nije uspjelo",
"failedToResendMessage": "Ponovno slanje poruke nije uspjelo",
"retryFailed": "Ponovni pokušaj nije uspio: {error}",
"deleteDrawingAndMessage": "Izbriši crtež i poruku",
"lastEchoRelay": "Zadnji odjek relej",
"lastEchoPath": "Zadnji odjek putanja",
"lastEchoBytesReport": "Zadnji odjek izvještaj o bajtovima",
"noGpsCoordinatesAvailable": "GPS koordinate nisu dostupne",
"notASarMarker": "Nije SAR marker",
"failedToSaveTemplate": "Spremanje predloška nije uspjelo: {error}",
"drawingNotFound": "Crtež nije pronađen",
"unknownDrawingType": "Nepoznata vrsta crteža",
"customMapMarker": "Prilagođeni marker karte",
"customMapPointLabel": "Točka: {point}",
"mapIdLabel": "ID karte: {id}",
"cannotFetchVoice": "Glasovnu poruku nije moguće dohvatiti",
"senderContactUnknown": "Kontakt pošiljatelja nije poznat. Najprije sinkronizirajte kontakte.",
"senderRouteUnknown": "Ruta pošiljatelja nije poznata. Najprije sinkronizirajte kontakte/putanju.",
"messageTooFar": "Poruka je predaleko ({hops} skokova, najviše {maxHops}).",
"senderRouteNoPathResponse": "Ruta pošiljatelja nije odgovorila na provjeru putanje. Sinkronizirajte kontakte/putanju i pokušajte ponovno.",
"senderRouteNoRawResponse": "Ruta pošiljatelja nije odgovorila na izravnoj prijenosnoj putanji.",
"voiceFetchOverHops": "Dohvaćanje glasa preko {hops} skokova može potrajati.",
"voiceReceiveCanceled": "Primanje glasa je otkazano",
"fetchingMissingVoiceFragments": "Dohvaćanje nedostajućih glasovnih fragmenata",
"receivingVoice": "Primanje glasa",
"cannotFetchImage": "Sliku nije moguće dohvatiti",
"imageFetchOverHops": "Dohvaćanje slike preko {hops} skokova može potrajati.",
"imageAlreadyBeingReceived": "Slika se već prima",
"loadImage": "Učitaj sliku",
"imageFetchFailedToSendRequest": "Zahtjev za dohvaćanje slike nije poslan",
"imageUnavailable": "Slika trenutačno nije dostupna",
"imageFetchTimedOut": "Dohvaćanje slike je isteklo",
"imageReceiveCanceled": "Primanje slike je otkazano",
"closeImagePreview": "Zatvori pregled slike",
"recordingReleaseToSend": "Snimanje... otpustite za slanje glasa",
"sendingVoice": "Slanje glasa...",
"sendLongPressToRecordVoice": "Pošalji (dugi pritisak za snimanje glasa)",
"discardRecording": "Odbaci snimku",
"hopCount": "{count, plural, one{{count} skok} few{{count} skoka} other{{count} skokova}}",
"channelsSectionDescription": "Kanali za emitiranje mesh prometa u blizini",
"roomsSectionDescription": "Zajednički prostori za stalnu koordinaciju tima",
"contactsSectionDescription": "Osobe i uređaji koje možete izravno dosegnuti",
"noRecentChatters": "Nema nedavnih sudionika",
"public": "Javno",
"failedToSendTicTacToeMove": "Potez križić-kružić nije poslan",
"youWon": "Pobijedili ste",
"opponentWon": "Protivnik je pobijedio",
"gameDraw": "Neriješeno",
"yourTurn": "Vi ste na potezu",
"opponentTurn": "Protivnik je na potezu",
"pleaseEnterValidCoordinates": "Unesite valjane koordinate",
"locationNotAvailable": "Lokacija nije dostupna",
"customCaveMapPoint": "Prilagođena točka karte špilje",
"mapPoint": "Točka karte",
"fetchingMissingImageFragments": "Dohvaćanje nedostajućih fragmenata…",
"loadingImage": "Učitavanje…",
"receivingImage": "Primanje…",
"tapToLoad": "Dodirnite za učitavanje",
"connectDevice": "Poveži uređaj",
"chooseTransportSubtitle": "Odaberite način povezivanja: Bluetooth, WiFi ili serijski",
"ble": "BLE",
"network": "Mreža",
"serial": "Serijski",
"scan": "Skeniraj",
"pressScanToSearchForDevices": "Pritisnite Skeniraj za traženje uređaja u blizini",
"failedToConnectToDevice": "Povezivanje s uređajem {name} nije uspjelo",
"unknownDevice": "Nepoznat uređaj",
"showingCachedResultsTapRefresh": "Prikazani su predmemorirani rezultati. Dodirnite osvježi za ponovno skeniranje.",
"scanningLocalNetworkOnPort": "Skeniranje lokalne mreže za MeshCore WiFi uređaje na portu {port}",
"cancelScanAndAddServer": "Otkaži skeniranje i dodaj poslužitelj",
"addServer": "Dodaj poslužitelj",
"scanningProgressIps": "Skeniranje... {scanned}/{total} IP adresa",
"cancelAndEnterManually": "Otkaži i unesi ručno",
"noRecentOrDiscoveredServers": "Još nema nedavnih ili otkrivenih poslužitelja",
"recentlyUsed": "Nedavno korišteno",
"connecting": "Povezivanje...",
"discoveredOnThisNetwork": "Otkriveno na ovoj mreži",
"serverNoLongerAvailable": "Poslužitelj na {host}:{port} više nije dostupan. Skenirajte ponovno da pronađete aktivne poslužitelje.",
"failedToConnectToHost": "Povezivanje s {host}:{port} nije uspjelo",
"enterValidIpAddress": "Unesite valjanu IP adresu",
"enterValidTcpPort": "Unesite valjani TCP port",
"ipAddress": "IP adresa",
"tcpPort": "TCP port",
"customServerPort": "Prilagođeni port poslužitelja",
"chat": "Razgovor",
"restoringPreviousLink": "Obnavljanje prethodne veze",
"noDeviceConnected": "Nijedan uređaj nije povezan",
"sendAdvert": "Pošalji moj kontakt",
"advertMode": "Pošalji moj kontakt",
"advertModeSubtitle": "Odaberite koliko daleko trebaju putovati vaši kontaktni podaci.",
"floodAdvertSent": "Kontakt poslan preplavljanjem kroz mrežu",
"directAdvertSent": "Kontakt poslan izravno obližnjim uređajima"
}

View File

@@ -1878,5 +1878,125 @@
"enterRegionName": "Nome regione (es. auckland)",
"noRegionsFound": "Nessuna regione trovata su questo ripetitore.",
"regionScopeSet": "Ambito regionale impostato su {name}",
"regionScopeCleared": "Ambito regionale cancellato"
"regionScopeCleared": "Ambito regionale cancellato",
"simpleMode": "Modalità semplice",
"simpleModeDescription": "Solo chat e mappa con i contatti preferiti",
"simpleModeRequiresLockedDestination": "Attiva prima «Blocca destinazione» con un canale per usare la modalità semplice",
"noFavouriteContacts": "Nessun contatto preferito",
"selectDestinationFirst": "Seleziona prima un canale, un contatto o una stanza",
"recipientNoLongerAvailable": "Il destinatario non è più disponibile",
"failedToSendMessage": "Invio non riuscito: {error}",
"tictactoeDirectOnly": "Il tris funziona solo nei messaggi diretti. Scegli prima un contatto.",
"deviceKeyUnavailable": "La chiave del dispositivo non è disponibile",
"imageCompressionFailed": "Compressione dell'immagine non riuscita",
"imageFragmentationFailed": "Frammentazione dell'immagine non riuscita",
"failedToAnnounceImage": "Annuncio dell'immagine non riuscito",
"noRecipientSelected": "Nessun destinatario selezionato",
"imageSendFailed": "Invio dell'immagine non riuscito",
"microphonePermissionRequiredForVoice": "Per i messaggi vocali è necessario il permesso del microfono",
"recordingDiscarded": "Registrazione scartata",
"publicChannelMediaWarning": "Stai per inviare {mediaType} al Canale Pubblico. Non è consigliato, perché chiunque sulla rete mesh potrebbe riceverlo. Scegli un canale privato o con tag, a meno che non sia ciò che vuoi.",
"selectPrivateChannelToShareLocation": "Seleziona un canale privato per condividere la tua posizione.",
"stopSharingMyLocation": "Smetti di condividere la mia posizione",
"shareMyLocation": "Condividi la mia posizione",
"stopRecording": "Interrompi registrazione",
"recordVoice": "Registra messaggio vocale",
"moreActions": "Altre azioni",
"noMessagesInFilter": "Nessun messaggio in questo filtro",
"noMatchesInFilter": "Nessuna corrispondenza in questo filtro",
"syncFailed": "Sincronizzazione non riuscita: {error}",
"invalidLink": "Link non valido",
"cannotOpenLink": "Impossibile aprire il link",
"failedToOpenLink": "Apertura del link non riuscita",
"failedToResendMessage": "Reinvio del messaggio non riuscito",
"retryFailed": "Nuovo tentativo non riuscito: {error}",
"deleteDrawingAndMessage": "Elimina disegno e messaggio",
"lastEchoRelay": "Ultimo eco: relay",
"lastEchoPath": "Ultimo eco: percorso",
"lastEchoBytesReport": "Ultimo eco: report byte",
"noGpsCoordinatesAvailable": "Nessuna coordinata GPS disponibile",
"notASarMarker": "Non è un marcatore SAR",
"failedToSaveTemplate": "Salvataggio del modello non riuscito: {error}",
"drawingNotFound": "Disegno non trovato",
"unknownDrawingType": "Tipo di disegno sconosciuto",
"customMapMarker": "Marcatore mappa personalizzato",
"customMapPointLabel": "Punto: {point}",
"mapIdLabel": "ID mappa: {id}",
"cannotFetchVoice": "Impossibile recuperare il vocale",
"senderContactUnknown": "Il contatto del mittente è sconosciuto. Sincronizza prima i contatti.",
"senderRouteUnknown": "Il percorso del mittente è sconosciuto. Sincronizza prima contatti/percorso.",
"messageTooFar": "Il messaggio è troppo lontano ({hops} hop, max {maxHops}).",
"senderRouteNoPathResponse": "Il percorso del mittente non ha risposto alla verifica. Sincronizza contatti/percorso e riprova.",
"senderRouteNoRawResponse": "Il percorso del mittente non ha risposto sul percorso di trasporto raw.",
"voiceFetchOverHops": "Il recupero del vocale su {hops} hop può richiedere un po' di tempo.",
"voiceReceiveCanceled": "Ricezione vocale annullata",
"fetchingMissingVoiceFragments": "Recupero dei frammenti vocali mancanti",
"receivingVoice": "Ricezione vocale",
"cannotFetchImage": "Impossibile recuperare l'immagine",
"imageFetchOverHops": "Il recupero dell'immagine su {hops} hop può richiedere un po' di tempo.",
"imageAlreadyBeingReceived": "L'immagine è già in ricezione",
"loadImage": "Carica immagine",
"imageFetchFailedToSendRequest": "Invio della richiesta immagine non riuscito",
"imageUnavailable": "Immagine al momento non disponibile",
"imageFetchTimedOut": "Recupero dell'immagine scaduto",
"imageReceiveCanceled": "Ricezione dell'immagine annullata",
"closeImagePreview": "Chiudi anteprima immagine",
"recordingReleaseToSend": "Registrazione... rilascia per inviare",
"sendingVoice": "Invio vocale...",
"sendLongPressToRecordVoice": "Invia (pressione lunga per registrare un vocale)",
"discardRecording": "Scarta registrazione",
"hopCount": "{count, plural, =1{1 hop} other{{count} hop}}",
"channelsSectionDescription": "Canali broadcast per il traffico mesh nelle vicinanze",
"roomsSectionDescription": "Spazi condivisi per il coordinamento continuo del team",
"contactsSectionDescription": "Persone e dispositivi raggiungibili direttamente",
"noRecentChatters": "Nessun partecipante recente",
"public": "Pubblico",
"failedToSendTicTacToeMove": "Invio della mossa di tris non riuscito",
"youWon": "Hai vinto",
"opponentWon": "Ha vinto l'avversario",
"gameDraw": "Pareggio",
"yourTurn": "Tocca a te",
"opponentTurn": "Tocca all'avversario",
"pleaseEnterValidCoordinates": "Inserisci coordinate valide",
"locationNotAvailable": "Posizione non disponibile",
"customCaveMapPoint": "Punto mappa grotta personalizzato",
"mapPoint": "Punto mappa",
"fetchingMissingImageFragments": "Recupero frammenti mancanti…",
"loadingImage": "Caricamento…",
"receivingImage": "Ricezione…",
"tapToLoad": "Tocca per caricare",
"connectDevice": "Connetti dispositivo",
"chooseTransportSubtitle": "Scegli il trasporto Bluetooth, WiFi o Seriale",
"ble": "BLE",
"network": "Rete",
"serial": "Seriale",
"scan": "Cerca",
"pressScanToSearchForDevices": "Premi Cerca per trovare i dispositivi nelle vicinanze",
"failedToConnectToDevice": "Impossibile connettersi a {name}",
"unknownDevice": "Dispositivo sconosciuto",
"showingCachedResultsTapRefresh": "Risultati in cache. Tocca Aggiorna per ripetere la scansione.",
"scanningLocalNetworkOnPort": "Scansione della rete locale per dispositivi MeshCore WiFi sulla porta {port}",
"cancelScanAndAddServer": "Annulla la scansione e aggiungi un server",
"addServer": "Aggiungi server",
"scanningProgressIps": "Scansione... {scanned}/{total} IP",
"cancelAndEnterManually": "Annulla e inserisci manualmente",
"noRecentOrDiscoveredServers": "Nessun server recente o rilevato per ora",
"recentlyUsed": "Usati di recente",
"connecting": "Connessione...",
"discoveredOnThisNetwork": "Rilevati su questa rete",
"serverNoLongerAvailable": "Il server {host}:{port} non è più disponibile. Esegui di nuovo la scansione per trovare server attivi.",
"failedToConnectToHost": "Impossibile connettersi a {host}:{port}",
"enterValidIpAddress": "Inserisci un indirizzo IP valido",
"enterValidTcpPort": "Inserisci una porta TCP valida",
"ipAddress": "Indirizzo IP",
"tcpPort": "Porta TCP",
"customServerPort": "Porta server personalizzata",
"chat": "Chat",
"restoringPreviousLink": "Ripristino del collegamento precedente",
"noDeviceConnected": "Nessun dispositivo connesso",
"sendAdvert": "Invia il mio contatto",
"advertMode": "Invia il mio contatto",
"advertModeSubtitle": "Scegli quanto lontano devono viaggiare le tue informazioni di contatto.",
"floodAdvertSent": "Contatto inviato attraverso la rete mesh",
"directAdvertSent": "Contatto inviato ai dispositivi vicini"
}

View File

@@ -2769,13 +2769,13 @@ abstract class AppLocalizations {
/// No description provided for @technicalDetails.
///
/// In en, this message translates to:
/// **'Technical details'**
/// **'Details'**
String get technicalDetails;
/// No description provided for @messageTechnicalDetails.
///
/// In en, this message translates to:
/// **'Message technical details'**
/// **'Message details'**
String get messageTechnicalDetails;
/// No description provided for @linkQuality.
@@ -5376,6 +5376,30 @@ abstract class AppLocalizations {
/// **'Size, compression, and preview'**
String get sizeCompressionAndPreview;
/// No description provided for @simpleMode.
///
/// In en, this message translates to:
/// **'Simple mode'**
String get simpleMode;
/// No description provided for @simpleModeDescription.
///
/// In en, this message translates to:
/// **'Only chat and map with favourite contacts'**
String get simpleModeDescription;
/// No description provided for @simpleModeRequiresLockedDestination.
///
/// In en, this message translates to:
/// **'Enable \"Lock destination\" with a channel first to use Simple mode'**
String get simpleModeRequiresLockedDestination;
/// No description provided for @noFavouriteContacts.
///
/// In en, this message translates to:
/// **'No favourite contacts'**
String get noFavouriteContacts;
/// No description provided for @skip.
///
/// In en, this message translates to:
@@ -5585,6 +5609,702 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Region scope cleared'**
String get regionScopeCleared;
///
///
/// In en, this message translates to:
/// **'Select a channel, contact, or room first'**
String get selectDestinationFirst;
///
///
/// In en, this message translates to:
/// **'Recipient is no longer available'**
String get recipientNoLongerAvailable;
///
///
/// In en, this message translates to:
/// **'Failed to send: {error}'**
String failedToSendMessage(String error);
///
///
/// In en, this message translates to:
/// **'Tic-Tac-Toe works only in direct messages. Choose a contact first.'**
String get tictactoeDirectOnly;
///
///
/// In en, this message translates to:
/// **'Device key is unavailable'**
String get deviceKeyUnavailable;
///
///
/// In en, this message translates to:
/// **'Image compression failed'**
String get imageCompressionFailed;
///
///
/// In en, this message translates to:
/// **'Image fragmentation failed'**
String get imageFragmentationFailed;
///
///
/// In en, this message translates to:
/// **'Failed to announce image'**
String get failedToAnnounceImage;
///
///
/// In en, this message translates to:
/// **'No recipient selected'**
String get noRecipientSelected;
///
///
/// In en, this message translates to:
/// **'Image send failed'**
String get imageSendFailed;
///
///
/// In en, this message translates to:
/// **'Microphone permission required for voice'**
String get microphonePermissionRequiredForVoice;
///
///
/// In en, this message translates to:
/// **'Recording discarded'**
String get recordingDiscarded;
///
///
/// In en, this message translates to:
/// **'You are about to send {mediaType} to the Public Channel. This is not advised because everyone on the mesh may receive it. Choose a private or tagged channel unless this is what you want.'**
String publicChannelMediaWarning(String mediaType);
///
///
/// In en, this message translates to:
/// **'Select a private channel to share your location.'**
String get selectPrivateChannelToShareLocation;
///
///
/// In en, this message translates to:
/// **'Stop sharing my location'**
String get stopSharingMyLocation;
///
///
/// In en, this message translates to:
/// **'Share my location'**
String get shareMyLocation;
///
///
/// In en, this message translates to:
/// **'Stop recording'**
String get stopRecording;
///
///
/// In en, this message translates to:
/// **'Record voice'**
String get recordVoice;
///
///
/// In en, this message translates to:
/// **'More actions'**
String get moreActions;
///
///
/// In en, this message translates to:
/// **'No messages in this filter'**
String get noMessagesInFilter;
///
///
/// In en, this message translates to:
/// **'No matches in this filter'**
String get noMatchesInFilter;
///
///
/// In en, this message translates to:
/// **'Sync failed: {error}'**
String syncFailed(String error);
///
///
/// In en, this message translates to:
/// **'Invalid link'**
String get invalidLink;
///
///
/// In en, this message translates to:
/// **'Cannot open link'**
String get cannotOpenLink;
///
///
/// In en, this message translates to:
/// **'Failed to open link'**
String get failedToOpenLink;
///
///
/// In en, this message translates to:
/// **'Failed to resend message'**
String get failedToResendMessage;
///
///
/// In en, this message translates to:
/// **'Retry failed: {error}'**
String retryFailed(String error);
///
///
/// In en, this message translates to:
/// **'Delete drawing & message'**
String get deleteDrawingAndMessage;
///
///
/// In en, this message translates to:
/// **'Last echo relay'**
String get lastEchoRelay;
///
///
/// In en, this message translates to:
/// **'Last echo path'**
String get lastEchoPath;
///
///
/// In en, this message translates to:
/// **'Last echo bytes report'**
String get lastEchoBytesReport;
///
///
/// In en, this message translates to:
/// **'No GPS coordinates available'**
String get noGpsCoordinatesAvailable;
///
///
/// In en, this message translates to:
/// **'Not a SAR marker'**
String get notASarMarker;
///
///
/// In en, this message translates to:
/// **'Failed to save template: {error}'**
String failedToSaveTemplate(String error);
///
///
/// In en, this message translates to:
/// **'Drawing not found'**
String get drawingNotFound;
///
///
/// In en, this message translates to:
/// **'Unknown drawing type'**
String get unknownDrawingType;
///
///
/// In en, this message translates to:
/// **'Custom map marker'**
String get customMapMarker;
///
///
/// In en, this message translates to:
/// **'Point: {point}'**
String customMapPointLabel(String point);
///
///
/// In en, this message translates to:
/// **'Map ID: {id}'**
String mapIdLabel(String id);
///
///
/// In en, this message translates to:
/// **'Cannot fetch voice'**
String get cannotFetchVoice;
///
///
/// In en, this message translates to:
/// **'Sender contact is unknown. Sync contacts first.'**
String get senderContactUnknown;
///
///
/// In en, this message translates to:
/// **'Sender route is unknown. Sync contacts/path first.'**
String get senderRouteUnknown;
///
///
/// In en, this message translates to:
/// **'Message is too far ({hops} hops, max {maxHops}).'**
String messageTooFar(String hops, String maxHops);
///
///
/// In en, this message translates to:
/// **'Sender route did not respond to a path check. Sync contacts/path and try again.'**
String get senderRouteNoPathResponse;
///
///
/// In en, this message translates to:
/// **'Sender route did not respond on the raw transport path.'**
String get senderRouteNoRawResponse;
///
///
/// In en, this message translates to:
/// **'Voice fetch over {hops} hops may take a while.'**
String voiceFetchOverHops(String hops);
///
///
/// In en, this message translates to:
/// **'Voice receive canceled'**
String get voiceReceiveCanceled;
///
///
/// In en, this message translates to:
/// **'Fetching missing voice fragments'**
String get fetchingMissingVoiceFragments;
///
///
/// In en, this message translates to:
/// **'Receiving voice'**
String get receivingVoice;
///
///
/// In en, this message translates to:
/// **'Cannot fetch image'**
String get cannotFetchImage;
///
///
/// In en, this message translates to:
/// **'Image fetch over {hops} hops may take a while.'**
String imageFetchOverHops(String hops);
///
///
/// In en, this message translates to:
/// **'Image is already being received'**
String get imageAlreadyBeingReceived;
///
///
/// In en, this message translates to:
/// **'Load image'**
String get loadImage;
///
///
/// In en, this message translates to:
/// **'Image fetch failed to send request'**
String get imageFetchFailedToSendRequest;
///
///
/// In en, this message translates to:
/// **'Image unavailable right now'**
String get imageUnavailable;
///
///
/// In en, this message translates to:
/// **'Image fetch timed out'**
String get imageFetchTimedOut;
///
///
/// In en, this message translates to:
/// **'Image receive canceled'**
String get imageReceiveCanceled;
///
///
/// In en, this message translates to:
/// **'Close image preview'**
String get closeImagePreview;
///
///
/// In en, this message translates to:
/// **'Recording... release to send voice'**
String get recordingReleaseToSend;
///
///
/// In en, this message translates to:
/// **'Sending voice...'**
String get sendingVoice;
///
///
/// In en, this message translates to:
/// **'Send (long press to record voice)'**
String get sendLongPressToRecordVoice;
///
///
/// In en, this message translates to:
/// **'Discard recording'**
String get discardRecording;
///
///
/// In en, this message translates to:
/// **'{count, plural, =1{1 hop} other{{count} hops}}'**
String hopCount(num count);
///
///
/// In en, this message translates to:
/// **'Broadcast lanes for nearby mesh traffic'**
String get channelsSectionDescription;
///
///
/// In en, this message translates to:
/// **'Shared spaces for ongoing team coordination'**
String get roomsSectionDescription;
///
///
/// In en, this message translates to:
/// **'Direct people and devices you can reach'**
String get contactsSectionDescription;
///
///
/// In en, this message translates to:
/// **'No recent chatters'**
String get noRecentChatters;
///
///
/// In en, this message translates to:
/// **'Public'**
String get public;
///
///
/// In en, this message translates to:
/// **'Failed to send Tic-Tac-Toe move'**
String get failedToSendTicTacToeMove;
///
///
/// In en, this message translates to:
/// **'You won'**
String get youWon;
///
///
/// In en, this message translates to:
/// **'Opponent won'**
String get opponentWon;
///
///
/// In en, this message translates to:
/// **'Draw'**
String get gameDraw;
///
///
/// In en, this message translates to:
/// **'Your turn'**
String get yourTurn;
///
///
/// In en, this message translates to:
/// **'Opponent turn'**
String get opponentTurn;
///
///
/// In en, this message translates to:
/// **'Please enter valid coordinates'**
String get pleaseEnterValidCoordinates;
///
///
/// In en, this message translates to:
/// **'Location not available'**
String get locationNotAvailable;
///
///
/// In en, this message translates to:
/// **'Custom cave map point'**
String get customCaveMapPoint;
///
///
/// In en, this message translates to:
/// **'Map point'**
String get mapPoint;
///
///
/// In en, this message translates to:
/// **'Fetching missing fragments…'**
String get fetchingMissingImageFragments;
///
///
/// In en, this message translates to:
/// **'Loading…'**
String get loadingImage;
///
///
/// In en, this message translates to:
/// **'Receiving…'**
String get receivingImage;
///
///
/// In en, this message translates to:
/// **'Tap to load'**
String get tapToLoad;
/// Connection dialog title
///
/// In en, this message translates to:
/// **'Connect Device'**
String get connectDevice;
/// Connection dialog subtitle describing available transports
///
/// In en, this message translates to:
/// **'Choose Bluetooth, WiFi, or Serial transport'**
String get chooseTransportSubtitle;
/// Bluetooth Low Energy tab label in connection dialog
///
/// In en, this message translates to:
/// **'BLE'**
String get ble;
/// Network tab label in connection dialog
///
/// In en, this message translates to:
/// **'Network'**
String get network;
/// Serial tab label in connection dialog
///
/// In en, this message translates to:
/// **'Serial'**
String get serial;
/// Button label to start scanning for devices
///
/// In en, this message translates to:
/// **'Scan'**
String get scan;
/// Empty state shown before the first BLE scan
///
/// In en, this message translates to:
/// **'Press scan to search for nearby devices'**
String get pressScanToSearchForDevices;
/// Error when connecting to a BLE device fails
///
/// In en, this message translates to:
/// **'Failed to connect to {name}'**
String failedToConnectToDevice(String name);
/// Fallback name for a BLE device without a name
///
/// In en, this message translates to:
/// **'Unknown Device'**
String get unknownDevice;
/// Banner shown when cached network scan results are displayed
///
/// In en, this message translates to:
/// **'Showing cached results. Tap refresh to rescan.'**
String get showingCachedResultsTapRefresh;
/// Banner shown while scanning the local network
///
/// In en, this message translates to:
/// **'Scanning local network for MeshCore WiFi devices on port {port}'**
String scanningLocalNetworkOnPort(int port);
/// Tooltip for the add-server button while a network scan is running
///
/// In en, this message translates to:
/// **'Cancel scan and add server'**
String get cancelScanAndAddServer;
/// Tooltip for the add-server button
///
/// In en, this message translates to:
/// **'Add server'**
String get addServer;
/// Network scan progress label
///
/// In en, this message translates to:
/// **'Scanning... {scanned}/{total} IPs'**
String scanningProgressIps(int scanned, String total);
/// Button to cancel the network scan and enter a server address manually
///
/// In en, this message translates to:
/// **'Cancel and enter manually'**
String get cancelAndEnterManually;
/// Empty state for the network servers tab
///
/// In en, this message translates to:
/// **'No recent or discovered servers yet'**
String get noRecentOrDiscoveredServers;
/// Section header for recently used servers
///
/// In en, this message translates to:
/// **'Recently used'**
String get recentlyUsed;
/// Status label while a connection is being established
///
/// In en, this message translates to:
/// **'Connecting...'**
String get connecting;
/// Section header for servers discovered on the local network
///
/// In en, this message translates to:
/// **'Discovered on this network'**
String get discoveredOnThisNetwork;
/// Error when a previously discovered server does not respond anymore
///
/// In en, this message translates to:
/// **'Server at {host}:{port} is no longer available. Please scan again to find active servers.'**
String serverNoLongerAvailable(String host, int port);
/// Error when a TCP connection attempt fails
///
/// In en, this message translates to:
/// **'Failed to connect to {host}:{port}'**
String failedToConnectToHost(String host, int port);
/// Validation error for the IP address field
///
/// In en, this message translates to:
/// **'Enter a valid IP address'**
String get enterValidIpAddress;
/// Validation error for the TCP port field
///
/// In en, this message translates to:
/// **'Enter a valid TCP port'**
String get enterValidTcpPort;
/// Label for the IP address text field
///
/// In en, this message translates to:
/// **'IP address'**
String get ipAddress;
/// Label for the TCP port text field
///
/// In en, this message translates to:
/// **'TCP port'**
String get tcpPort;
/// Helper text for the TCP port text field
///
/// In en, this message translates to:
/// **'Custom server port'**
String get customServerPort;
/// Chat tab label on the home screen
///
/// In en, this message translates to:
/// **'Chat'**
String get chat;
/// Status text while automatically reconnecting to the last device
///
/// In en, this message translates to:
/// **'Restoring previous link'**
String get restoringPreviousLink;
/// Status text when no device is connected
///
/// In en, this message translates to:
/// **'No device connected'**
String get noDeviceConnected;
/// Composer action that broadcasts this device's contact info (advert) to the mesh
///
/// In en, this message translates to:
/// **'Send my contact'**
String get sendAdvert;
/// Title of the sheet choosing how far the contact broadcast travels
///
/// In en, this message translates to:
/// **'Send my contact'**
String get advertMode;
/// Subtitle of the contact broadcast mode sheet
///
/// In en, this message translates to:
/// **'Choose how far your contact info should travel.'**
String get advertModeSubtitle;
/// Toast after broadcasting contact info via flood (through repeaters)
///
/// In en, this message translates to:
/// **'Contact sent across the mesh'**
String get floodAdvertSent;
/// Toast after broadcasting contact info direct (zero-hop, nearby only)
///
/// In en, this message translates to:
/// **'Contact sent to nearby devices'**
String get directAdvertSent;
}
class _AppLocalizationsDelegate

View File

@@ -2921,6 +2921,20 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get sizeCompressionAndPreview => 'Größe, Komprimierung und Vorschau';
@override
String get simpleMode => 'Einfacher Modus';
@override
String get simpleModeDescription =>
'Nur Chat und Karte mit favorisierten Kontakten';
@override
String get simpleModeRequiresLockedDestination =>
'Aktivieren Sie zuerst „Ziel sperren“ mit einem Kanal, um den einfachen Modus zu nutzen';
@override
String get noFavouriteContacts => 'Keine favorisierten Kontakte';
@override
String get skip => 'Überspringen';
@@ -3032,4 +3046,415 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get regionScopeCleared => 'Regionsbereich gelöscht';
@override
String get selectDestinationFirst =>
'Wählen Sie zuerst einen Kanal, Kontakt oder Raum';
@override
String get recipientNoLongerAvailable => 'Empfänger ist nicht mehr verfügbar';
@override
String failedToSendMessage(String error) {
return 'Senden fehlgeschlagen: $error';
}
@override
String get tictactoeDirectOnly =>
'Tic-Tac-Toe funktioniert nur in Direktnachrichten. Wählen Sie zuerst einen Kontakt.';
@override
String get deviceKeyUnavailable => 'Geräteschlüssel ist nicht verfügbar';
@override
String get imageCompressionFailed => 'Bildkomprimierung fehlgeschlagen';
@override
String get imageFragmentationFailed => 'Bildfragmentierung fehlgeschlagen';
@override
String get failedToAnnounceImage => 'Bild konnte nicht angekündigt werden';
@override
String get noRecipientSelected => 'Kein Empfänger ausgewählt';
@override
String get imageSendFailed => 'Bildversand fehlgeschlagen';
@override
String get microphonePermissionRequiredForVoice =>
'Für Sprachnachrichten ist die Mikrofonberechtigung erforderlich';
@override
String get recordingDiscarded => 'Aufnahme verworfen';
@override
String publicChannelMediaWarning(String mediaType) {
return 'Sie sind dabei, $mediaType an den öffentlichen Kanal zu senden. Das ist nicht ratsam, da alle im Mesh-Netz es empfangen können. Wählen Sie einen privaten oder getaggten Kanal, sofern dies nicht beabsichtigt ist.';
}
@override
String get selectPrivateChannelToShareLocation =>
'Wählen Sie einen privaten Kanal, um Ihren Standort zu teilen.';
@override
String get stopSharingMyLocation => 'Meinen Standort nicht mehr teilen';
@override
String get shareMyLocation => 'Meinen Standort teilen';
@override
String get stopRecording => 'Aufnahme stoppen';
@override
String get recordVoice => 'Sprachnachricht aufnehmen';
@override
String get moreActions => 'Weitere Aktionen';
@override
String get noMessagesInFilter => 'Keine Nachrichten in diesem Filter';
@override
String get noMatchesInFilter => 'Keine Treffer in diesem Filter';
@override
String syncFailed(String error) {
return 'Synchronisierung fehlgeschlagen: $error';
}
@override
String get invalidLink => 'Ungültiger Link';
@override
String get cannotOpenLink => 'Link kann nicht geöffnet werden';
@override
String get failedToOpenLink => 'Link konnte nicht geöffnet werden';
@override
String get failedToResendMessage =>
'Nachricht konnte nicht erneut gesendet werden';
@override
String retryFailed(String error) {
return 'Wiederholung fehlgeschlagen: $error';
}
@override
String get deleteDrawingAndMessage => 'Zeichnung & Nachricht löschen';
@override
String get lastEchoRelay => 'Letztes Echo Relais';
@override
String get lastEchoPath => 'Letztes Echo Pfad';
@override
String get lastEchoBytesReport => 'Letztes Echo Byte-Bericht';
@override
String get noGpsCoordinatesAvailable => 'Keine GPS-Koordinaten verfügbar';
@override
String get notASarMarker => 'Keine SAR-Markierung';
@override
String failedToSaveTemplate(String error) {
return 'Vorlage konnte nicht gespeichert werden: $error';
}
@override
String get drawingNotFound => 'Zeichnung nicht gefunden';
@override
String get unknownDrawingType => 'Unbekannter Zeichnungstyp';
@override
String get customMapMarker => 'Eigene Kartenmarkierung';
@override
String customMapPointLabel(String point) {
return 'Punkt: $point';
}
@override
String mapIdLabel(String id) {
return 'Karten-ID: $id';
}
@override
String get cannotFetchVoice => 'Sprachnachricht kann nicht abgerufen werden';
@override
String get senderContactUnknown =>
'Absenderkontakt ist unbekannt. Synchronisieren Sie zuerst die Kontakte.';
@override
String get senderRouteUnknown =>
'Absenderroute ist unbekannt. Synchronisieren Sie zuerst Kontakte/Pfad.';
@override
String messageTooFar(String hops, String maxHops) {
return 'Nachricht ist zu weit entfernt ($hops Hops, max. $maxHops).';
}
@override
String get senderRouteNoPathResponse =>
'Absenderroute hat auf die Pfadprüfung nicht reagiert. Synchronisieren Sie Kontakte/Pfad und versuchen Sie es erneut.';
@override
String get senderRouteNoRawResponse =>
'Absenderroute hat auf dem Raw-Transportpfad nicht reagiert.';
@override
String voiceFetchOverHops(String hops) {
return 'Sprachabruf über $hops Hops kann eine Weile dauern.';
}
@override
String get voiceReceiveCanceled => 'Sprachempfang abgebrochen';
@override
String get fetchingMissingVoiceFragments =>
'Fehlende Sprachfragmente werden abgerufen';
@override
String get receivingVoice => 'Sprachnachricht wird empfangen';
@override
String get cannotFetchImage => 'Bild kann nicht abgerufen werden';
@override
String imageFetchOverHops(String hops) {
return 'Bildabruf über $hops Hops kann eine Weile dauern.';
}
@override
String get imageAlreadyBeingReceived => 'Bild wird bereits empfangen';
@override
String get loadImage => 'Bild laden';
@override
String get imageFetchFailedToSendRequest =>
'Bildabruf-Anfrage konnte nicht gesendet werden';
@override
String get imageUnavailable => 'Bild derzeit nicht verfügbar';
@override
String get imageFetchTimedOut => 'Bildabruf hat das Zeitlimit überschritten';
@override
String get imageReceiveCanceled => 'Bildempfang abgebrochen';
@override
String get closeImagePreview => 'Bildvorschau schließen';
@override
String get recordingReleaseToSend =>
'Aufnahme läuft ... loslassen zum Senden';
@override
String get sendingVoice => 'Sprachnachricht wird gesendet ...';
@override
String get sendLongPressToRecordVoice =>
'Senden (lange drücken für Sprachaufnahme)';
@override
String get discardRecording => 'Aufnahme verwerfen';
@override
String hopCount(num count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count Hops',
one: '1 Hop',
);
return '$_temp0';
}
@override
String get channelsSectionDescription =>
'Broadcast-Kanäle für Mesh-Verkehr in der Nähe';
@override
String get roomsSectionDescription =>
'Gemeinsame Räume für laufende Teamkoordination';
@override
String get contactsSectionDescription =>
'Personen und Geräte, die Sie direkt erreichen können';
@override
String get noRecentChatters => 'Keine kürzlichen Teilnehmer';
@override
String get public => 'Öffentlich';
@override
String get failedToSendTicTacToeMove =>
'Tic-Tac-Toe-Zug konnte nicht gesendet werden';
@override
String get youWon => 'Sie haben gewonnen';
@override
String get opponentWon => 'Gegner hat gewonnen';
@override
String get gameDraw => 'Unentschieden';
@override
String get yourTurn => 'Sie sind am Zug';
@override
String get opponentTurn => 'Gegner ist am Zug';
@override
String get pleaseEnterValidCoordinates =>
'Bitte geben Sie gültige Koordinaten ein';
@override
String get locationNotAvailable => 'Standort nicht verfügbar';
@override
String get customCaveMapPoint => 'Eigener Höhlenkarten-Punkt';
@override
String get mapPoint => 'Kartenpunkt';
@override
String get fetchingMissingImageFragments =>
'Fehlende Fragmente werden abgerufen …';
@override
String get loadingImage => 'Wird geladen …';
@override
String get receivingImage => 'Wird empfangen …';
@override
String get tapToLoad => 'Zum Laden tippen';
@override
String get connectDevice => 'Gerät verbinden';
@override
String get chooseTransportSubtitle =>
'Wählen Sie Bluetooth, WiFi oder Seriell als Verbindung';
@override
String get ble => 'BLE';
@override
String get network => 'Netzwerk';
@override
String get serial => 'Seriell';
@override
String get scan => 'Scannen';
@override
String get pressScanToSearchForDevices =>
'Drücken Sie auf Scannen, um nach Geräten in der Nähe zu suchen';
@override
String failedToConnectToDevice(String name) {
return 'Verbindung mit $name fehlgeschlagen';
}
@override
String get unknownDevice => 'Unbekanntes Gerät';
@override
String get showingCachedResultsTapRefresh =>
'Zwischengespeicherte Ergebnisse werden angezeigt. Tippen Sie auf Aktualisieren, um erneut zu scannen.';
@override
String scanningLocalNetworkOnPort(int port) {
return 'Lokales Netzwerk wird nach MeshCore-WiFi-Geräten auf Port $port durchsucht';
}
@override
String get cancelScanAndAddServer => 'Scan abbrechen und Server hinzufügen';
@override
String get addServer => 'Server hinzufügen';
@override
String scanningProgressIps(int scanned, String total) {
return 'Scannen... $scanned/$total IPs';
}
@override
String get cancelAndEnterManually => 'Abbrechen und manuell eingeben';
@override
String get noRecentOrDiscoveredServers =>
'Noch keine kürzlich verwendeten oder gefundenen Server';
@override
String get recentlyUsed => 'Kürzlich verwendet';
@override
String get connecting => 'Verbindung wird hergestellt...';
@override
String get discoveredOnThisNetwork => 'In diesem Netzwerk gefunden';
@override
String serverNoLongerAvailable(String host, int port) {
return 'Server unter $host:$port ist nicht mehr verfügbar. Bitte scannen Sie erneut, um aktive Server zu finden.';
}
@override
String failedToConnectToHost(String host, int port) {
return 'Verbindung mit $host:$port fehlgeschlagen';
}
@override
String get enterValidIpAddress => 'Geben Sie eine gültige IP-Adresse ein';
@override
String get enterValidTcpPort => 'Geben Sie einen gültigen TCP-Port ein';
@override
String get ipAddress => 'IP-Adresse';
@override
String get tcpPort => 'TCP-Port';
@override
String get customServerPort => 'Benutzerdefinierter Server-Port';
@override
String get chat => 'Chat';
@override
String get restoringPreviousLink =>
'Vorherige Verbindung wird wiederhergestellt';
@override
String get noDeviceConnected => 'Kein Gerät verbunden';
@override
String get sendAdvert => 'Meinen Kontakt senden';
@override
String get advertMode => 'Meinen Kontakt senden';
@override
String get advertModeSubtitle =>
'Wählen Sie, wie weit Ihre Kontaktdaten übertragen werden sollen.';
@override
String get floodAdvertSent => 'Kontakt über das Mesh-Netzwerk gesendet';
@override
String get directAdvertSent => 'Kontakt an Geräte in der Nähe gesendet';
}

View File

@@ -2811,7 +2811,7 @@ class AppLocalizationsEl extends AppLocalizations {
String get older => 'Παλαιότερο';
@override
String get oneByteMode0 => '1 byte (mode 0)';
String get oneByteMode0 => '1 byte (λειτουργία 0)';
@override
String get oneHour => '1 ώρα';
@@ -2923,6 +2923,20 @@ class AppLocalizationsEl extends AppLocalizations {
@override
String get sizeCompressionAndPreview => 'Μέγεθος, συμπίεση και προεπισκόπηση';
@override
String get simpleMode => 'Απλή λειτουργία';
@override
String get simpleModeDescription =>
'Μόνο συνομιλία και χάρτης με αγαπημένες επαφές';
@override
String get simpleModeRequiresLockedDestination =>
'Ενεργοποιήστε πρώτα το «Κλείδωμα προορισμού» με ένα κανάλι για να χρησιμοποιήσετε την Απλή λειτουργία';
@override
String get noFavouriteContacts => 'Καμία αγαπημένη επαφή';
@override
String get skip => 'Παράλειψη';
@@ -2966,14 +2980,14 @@ class AppLocalizationsEl extends AppLocalizations {
String get thirtySeconds => '30 δευτερόλεπτα';
@override
String get threeBytesMode2 => '3 bytes (mode 2)';
String get threeBytesMode2 => '3 bytes (λειτουργία 2)';
@override
String get trafficStatsMessageHistoryAndDeveloperTools =>
'Στατιστικά κίνησης, ιστορικό μηνυμάτων και εργαλεία προγραμματιστή';
@override
String get twoBytesMode1 => '2 bytes (mode 1)';
String get twoBytesMode1 => '2 bytes (λειτουργία 1)';
@override
String get twoMinutes => '2 λεπτά';
@@ -3035,4 +3049,412 @@ class AppLocalizationsEl extends AppLocalizations {
@override
String get regionScopeCleared => 'Εύρος περιοχής εκκαθαρίστηκε';
@override
String get selectDestinationFirst => 'Επιλέξτε πρώτα κανάλι, επαφή ή δωμάτιο';
@override
String get recipientNoLongerAvailable =>
'Ο παραλήπτης δεν είναι πλέον διαθέσιμος';
@override
String failedToSendMessage(String error) {
return 'Αποτυχία αποστολής: $error';
}
@override
String get tictactoeDirectOnly =>
'Η τρίλιζα λειτουργεί μόνο σε άμεσα μηνύματα. Επιλέξτε πρώτα μια επαφή.';
@override
String get deviceKeyUnavailable =>
'Το κλειδί της συσκευής δεν είναι διαθέσιμο';
@override
String get imageCompressionFailed => 'Αποτυχία συμπίεσης εικόνας';
@override
String get imageFragmentationFailed => 'Αποτυχία κατακερματισμού εικόνας';
@override
String get failedToAnnounceImage => 'Αποτυχία ανακοίνωσης εικόνας';
@override
String get noRecipientSelected => 'Δεν επιλέχθηκε παραλήπτης';
@override
String get imageSendFailed => 'Αποτυχία αποστολής εικόνας';
@override
String get microphonePermissionRequiredForVoice =>
'Απαιτείται άδεια μικροφώνου για φωνητικά μηνύματα';
@override
String get recordingDiscarded => 'Η ηχογράφηση απορρίφθηκε';
@override
String publicChannelMediaWarning(String mediaType) {
return 'Πρόκειται να στείλετε $mediaType στο δημόσιο κανάλι. Δεν συνιστάται, καθώς μπορεί να το λάβουν όλοι στο δίκτυο mesh. Επιλέξτε ιδιωτικό ή επισημασμένο κανάλι, εκτός αν αυτό θέλετε.';
}
@override
String get selectPrivateChannelToShareLocation =>
'Επιλέξτε ιδιωτικό κανάλι για να μοιραστείτε την τοποθεσία σας.';
@override
String get stopSharingMyLocation => 'Διακοπή κοινοποίησης της τοποθεσίας μου';
@override
String get shareMyLocation => 'Κοινοποίηση της τοποθεσίας μου';
@override
String get stopRecording => 'Διακοπή ηχογράφησης';
@override
String get recordVoice => 'Ηχογράφηση φωνητικού μηνύματος';
@override
String get moreActions => 'Περισσότερες ενέργειες';
@override
String get noMessagesInFilter => 'Δεν υπάρχουν μηνύματα σε αυτό το φίλτρο';
@override
String get noMatchesInFilter => 'Δεν υπάρχουν αποτελέσματα σε αυτό το φίλτρο';
@override
String syncFailed(String error) {
return 'Αποτυχία συγχρονισμού: $error';
}
@override
String get invalidLink => 'Μη έγκυρος σύνδεσμος';
@override
String get cannotOpenLink => 'Δεν είναι δυνατό το άνοιγμα του συνδέσμου';
@override
String get failedToOpenLink => 'Αποτυχία ανοίγματος του συνδέσμου';
@override
String get failedToResendMessage => 'Αποτυχία επαναποστολής του μηνύματος';
@override
String retryFailed(String error) {
return 'Η επανάληψη απέτυχε: $error';
}
@override
String get deleteDrawingAndMessage => 'Διαγραφή σχεδίου και μηνύματος';
@override
String get lastEchoRelay => 'Τελευταία ηχώ: αναμεταδότης';
@override
String get lastEchoPath => 'Τελευταία ηχώ: διαδρομή';
@override
String get lastEchoBytesReport => 'Τελευταία ηχώ: αναφορά byte';
@override
String get noGpsCoordinatesAvailable =>
'Δεν υπάρχουν διαθέσιμες συντεταγμένες GPS';
@override
String get notASarMarker => 'Δεν είναι δείκτης SAR';
@override
String failedToSaveTemplate(String error) {
return 'Αποτυχία αποθήκευσης προτύπου: $error';
}
@override
String get drawingNotFound => 'Το σχέδιο δεν βρέθηκε';
@override
String get unknownDrawingType => 'Άγνωστος τύπος σχεδίου';
@override
String get customMapMarker => 'Προσαρμοσμένος δείκτης χάρτη';
@override
String customMapPointLabel(String point) {
return 'Σημείο: $point';
}
@override
String mapIdLabel(String id) {
return 'ID χάρτη: $id';
}
@override
String get cannotFetchVoice => 'Δεν είναι δυνατή η λήψη της φωνής';
@override
String get senderContactUnknown =>
'Η επαφή του αποστολέα είναι άγνωστη. Συγχρονίστε πρώτα τις επαφές.';
@override
String get senderRouteUnknown =>
'Η διαδρομή του αποστολέα είναι άγνωστη. Συγχρονίστε πρώτα επαφές/διαδρομή.';
@override
String messageTooFar(String hops, String maxHops) {
return 'Το μήνυμα είναι πολύ μακριά ($hops αναπηδήσεις, μέγ. $maxHops).';
}
@override
String get senderRouteNoPathResponse =>
'Η διαδρομή του αποστολέα δεν απάντησε στον έλεγχο διαδρομής. Συγχρονίστε επαφές/διαδρομή και δοκιμάστε ξανά.';
@override
String get senderRouteNoRawResponse =>
'Η διαδρομή του αποστολέα δεν απάντησε στη raw διαδρομή μεταφοράς.';
@override
String voiceFetchOverHops(String hops) {
return 'Η λήψη φωνής μέσω $hops αναπηδήσεων μπορεί να διαρκέσει.';
}
@override
String get voiceReceiveCanceled => 'Η λήψη φωνής ακυρώθηκε';
@override
String get fetchingMissingVoiceFragments =>
'Λήψη φωνητικών τμημάτων που λείπουν';
@override
String get receivingVoice => 'Λήψη φωνής';
@override
String get cannotFetchImage => 'Δεν είναι δυνατή η λήψη της εικόνας';
@override
String imageFetchOverHops(String hops) {
return 'Η λήψη εικόνας μέσω $hops αναπηδήσεων μπορεί να διαρκέσει.';
}
@override
String get imageAlreadyBeingReceived => 'Η εικόνα λαμβάνεται ήδη';
@override
String get loadImage => 'Φόρτωση εικόνας';
@override
String get imageFetchFailedToSendRequest =>
'Αποτυχία αποστολής αιτήματος εικόνας';
@override
String get imageUnavailable => 'Η εικόνα δεν είναι διαθέσιμη αυτήν τη στιγμή';
@override
String get imageFetchTimedOut => 'Η λήψη της εικόνας έληξε';
@override
String get imageReceiveCanceled => 'Η λήψη της εικόνας ακυρώθηκε';
@override
String get closeImagePreview => 'Κλείσιμο προεπισκόπησης εικόνας';
@override
String get recordingReleaseToSend => 'Ηχογράφηση... αφήστε για αποστολή';
@override
String get sendingVoice => 'Αποστολή φωνής...';
@override
String get sendLongPressToRecordVoice =>
'Αποστολή (παρατεταμένο πάτημα για ηχογράφηση)';
@override
String get discardRecording => 'Απόρριψη ηχογράφησης';
@override
String hopCount(num count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count αναπηδήσεις',
one: '$count αναπήδηση',
);
return '$_temp0';
}
@override
String get channelsSectionDescription =>
'Κανάλια εκπομπής για κοντινή κίνηση mesh';
@override
String get roomsSectionDescription =>
'Κοινόχρηστοι χώροι για συνεχή συντονισμό ομάδας';
@override
String get contactsSectionDescription =>
'Άτομα και συσκευές που μπορείτε να προσεγγίσετε άμεσα';
@override
String get noRecentChatters => 'Κανένας πρόσφατος συνομιλητής';
@override
String get public => 'Δημόσιο';
@override
String get failedToSendTicTacToeMove => 'Αποτυχία αποστολής κίνησης τρίλιζας';
@override
String get youWon => 'Κερδίσατε';
@override
String get opponentWon => 'Κέρδισε ο αντίπαλος';
@override
String get gameDraw => 'Ισοπαλία';
@override
String get yourTurn => 'Η σειρά σας';
@override
String get opponentTurn => 'Σειρά του αντιπάλου';
@override
String get pleaseEnterValidCoordinates => 'Εισαγάγετε έγκυρες συντεταγμένες';
@override
String get locationNotAvailable => 'Η τοποθεσία δεν είναι διαθέσιμη';
@override
String get customCaveMapPoint => 'Προσαρμοσμένο σημείο χάρτη σπηλαίου';
@override
String get mapPoint => 'Σημείο χάρτη';
@override
String get fetchingMissingImageFragments => 'Λήψη τμημάτων που λείπουν…';
@override
String get loadingImage => 'Φόρτωση…';
@override
String get receivingImage => 'Λήψη…';
@override
String get tapToLoad => 'Πατήστε για φόρτωση';
@override
String get connectDevice => 'Σύνδεση συσκευής';
@override
String get chooseTransportSubtitle =>
'Επιλέξτε μεταφορά Bluetooth, WiFi ή σειριακή';
@override
String get ble => 'BLE';
@override
String get network => 'Δίκτυο';
@override
String get serial => 'Σειριακή θύρα';
@override
String get scan => 'Σάρωση';
@override
String get pressScanToSearchForDevices =>
'Πατήστε «Σάρωση» για αναζήτηση κοντινών συσκευών';
@override
String failedToConnectToDevice(String name) {
return 'Αποτυχία σύνδεσης με $name';
}
@override
String get unknownDevice => 'Άγνωστη συσκευή';
@override
String get showingCachedResultsTapRefresh =>
'Εμφανίζονται αποτελέσματα από την προσωρινή μνήμη. Πατήστε ανανέωση για νέα σάρωση.';
@override
String scanningLocalNetworkOnPort(int port) {
return 'Σάρωση τοπικού δικτύου για συσκευές MeshCore WiFi στη θύρα $port';
}
@override
String get cancelScanAndAddServer =>
'Ακύρωση σάρωσης και προσθήκη διακομιστή';
@override
String get addServer => 'Προσθήκη διακομιστή';
@override
String scanningProgressIps(int scanned, String total) {
return 'Σάρωση... $scanned/$total διευθύνσεις IP';
}
@override
String get cancelAndEnterManually => 'Ακύρωση και χειροκίνητη εισαγωγή';
@override
String get noRecentOrDiscoveredServers =>
'Δεν υπάρχουν ακόμη πρόσφατοι ή εντοπισμένοι διακομιστές';
@override
String get recentlyUsed => 'Πρόσφατα χρησιμοποιημένοι';
@override
String get connecting => 'Γίνεται σύνδεση...';
@override
String get discoveredOnThisNetwork => 'Εντοπίστηκαν σε αυτό το δίκτυο';
@override
String serverNoLongerAvailable(String host, int port) {
return 'Ο διακομιστής στο $host:$port δεν είναι πλέον διαθέσιμος. Σαρώστε ξανά για να βρείτε ενεργούς διακομιστές.';
}
@override
String failedToConnectToHost(String host, int port) {
return 'Αποτυχία σύνδεσης με $host:$port';
}
@override
String get enterValidIpAddress => 'Εισαγάγετε έγκυρη διεύθυνση IP';
@override
String get enterValidTcpPort => 'Εισαγάγετε έγκυρη θύρα TCP';
@override
String get ipAddress => 'Διεύθυνση IP';
@override
String get tcpPort => 'Θύρα TCP';
@override
String get customServerPort => 'Προσαρμοσμένη θύρα διακομιστή';
@override
String get chat => 'Συνομιλία';
@override
String get restoringPreviousLink => 'Επαναφορά προηγούμενης σύνδεσης';
@override
String get noDeviceConnected => 'Δεν υπάρχει συνδεδεμένη συσκευή';
@override
String get sendAdvert => 'Αποστολή της επαφής μου';
@override
String get advertMode => 'Αποστολή της επαφής μου';
@override
String get advertModeSubtitle =>
'Επιλέξτε πόσο μακριά θα φτάσουν τα στοιχεία επαφής σας.';
@override
String get floodAdvertSent => 'Η επαφή στάλθηκε σε όλο το δίκτυο mesh';
@override
String get directAdvertSent => 'Η επαφή στάλθηκε σε κοντινές συσκευές';
}

View File

@@ -1471,10 +1471,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get reply => 'Reply';
@override
String get technicalDetails => 'Technical details';
String get technicalDetails => 'Details';
@override
String get messageTechnicalDetails => 'Message technical details';
String get messageTechnicalDetails => 'Message details';
@override
String get linkQuality => 'Link quality';
@@ -2888,6 +2888,20 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get sizeCompressionAndPreview => 'Size, compression, and preview';
@override
String get simpleMode => 'Simple mode';
@override
String get simpleModeDescription =>
'Only chat and map with favourite contacts';
@override
String get simpleModeRequiresLockedDestination =>
'Enable \"Lock destination\" with a channel first to use Simple mode';
@override
String get noFavouriteContacts => 'No favourite contacts';
@override
String get skip => 'Skip';
@@ -2999,4 +3013,408 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get regionScopeCleared => 'Region scope cleared';
@override
String get selectDestinationFirst =>
'Select a channel, contact, or room first';
@override
String get recipientNoLongerAvailable => 'Recipient is no longer available';
@override
String failedToSendMessage(String error) {
return 'Failed to send: $error';
}
@override
String get tictactoeDirectOnly =>
'Tic-Tac-Toe works only in direct messages. Choose a contact first.';
@override
String get deviceKeyUnavailable => 'Device key is unavailable';
@override
String get imageCompressionFailed => 'Image compression failed';
@override
String get imageFragmentationFailed => 'Image fragmentation failed';
@override
String get failedToAnnounceImage => 'Failed to announce image';
@override
String get noRecipientSelected => 'No recipient selected';
@override
String get imageSendFailed => 'Image send failed';
@override
String get microphonePermissionRequiredForVoice =>
'Microphone permission required for voice';
@override
String get recordingDiscarded => 'Recording discarded';
@override
String publicChannelMediaWarning(String mediaType) {
return 'You are about to send $mediaType to the Public Channel. This is not advised because everyone on the mesh may receive it. Choose a private or tagged channel unless this is what you want.';
}
@override
String get selectPrivateChannelToShareLocation =>
'Select a private channel to share your location.';
@override
String get stopSharingMyLocation => 'Stop sharing my location';
@override
String get shareMyLocation => 'Share my location';
@override
String get stopRecording => 'Stop recording';
@override
String get recordVoice => 'Record voice';
@override
String get moreActions => 'More actions';
@override
String get noMessagesInFilter => 'No messages in this filter';
@override
String get noMatchesInFilter => 'No matches in this filter';
@override
String syncFailed(String error) {
return 'Sync failed: $error';
}
@override
String get invalidLink => 'Invalid link';
@override
String get cannotOpenLink => 'Cannot open link';
@override
String get failedToOpenLink => 'Failed to open link';
@override
String get failedToResendMessage => 'Failed to resend message';
@override
String retryFailed(String error) {
return 'Retry failed: $error';
}
@override
String get deleteDrawingAndMessage => 'Delete drawing & message';
@override
String get lastEchoRelay => 'Last echo relay';
@override
String get lastEchoPath => 'Last echo path';
@override
String get lastEchoBytesReport => 'Last echo bytes report';
@override
String get noGpsCoordinatesAvailable => 'No GPS coordinates available';
@override
String get notASarMarker => 'Not a SAR marker';
@override
String failedToSaveTemplate(String error) {
return 'Failed to save template: $error';
}
@override
String get drawingNotFound => 'Drawing not found';
@override
String get unknownDrawingType => 'Unknown drawing type';
@override
String get customMapMarker => 'Custom map marker';
@override
String customMapPointLabel(String point) {
return 'Point: $point';
}
@override
String mapIdLabel(String id) {
return 'Map ID: $id';
}
@override
String get cannotFetchVoice => 'Cannot fetch voice';
@override
String get senderContactUnknown =>
'Sender contact is unknown. Sync contacts first.';
@override
String get senderRouteUnknown =>
'Sender route is unknown. Sync contacts/path first.';
@override
String messageTooFar(String hops, String maxHops) {
return 'Message is too far ($hops hops, max $maxHops).';
}
@override
String get senderRouteNoPathResponse =>
'Sender route did not respond to a path check. Sync contacts/path and try again.';
@override
String get senderRouteNoRawResponse =>
'Sender route did not respond on the raw transport path.';
@override
String voiceFetchOverHops(String hops) {
return 'Voice fetch over $hops hops may take a while.';
}
@override
String get voiceReceiveCanceled => 'Voice receive canceled';
@override
String get fetchingMissingVoiceFragments =>
'Fetching missing voice fragments';
@override
String get receivingVoice => 'Receiving voice';
@override
String get cannotFetchImage => 'Cannot fetch image';
@override
String imageFetchOverHops(String hops) {
return 'Image fetch over $hops hops may take a while.';
}
@override
String get imageAlreadyBeingReceived => 'Image is already being received';
@override
String get loadImage => 'Load image';
@override
String get imageFetchFailedToSendRequest =>
'Image fetch failed to send request';
@override
String get imageUnavailable => 'Image unavailable right now';
@override
String get imageFetchTimedOut => 'Image fetch timed out';
@override
String get imageReceiveCanceled => 'Image receive canceled';
@override
String get closeImagePreview => 'Close image preview';
@override
String get recordingReleaseToSend => 'Recording... release to send voice';
@override
String get sendingVoice => 'Sending voice...';
@override
String get sendLongPressToRecordVoice => 'Send (long press to record voice)';
@override
String get discardRecording => 'Discard recording';
@override
String hopCount(num count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count hops',
one: '1 hop',
);
return '$_temp0';
}
@override
String get channelsSectionDescription =>
'Broadcast lanes for nearby mesh traffic';
@override
String get roomsSectionDescription =>
'Shared spaces for ongoing team coordination';
@override
String get contactsSectionDescription =>
'Direct people and devices you can reach';
@override
String get noRecentChatters => 'No recent chatters';
@override
String get public => 'Public';
@override
String get failedToSendTicTacToeMove => 'Failed to send Tic-Tac-Toe move';
@override
String get youWon => 'You won';
@override
String get opponentWon => 'Opponent won';
@override
String get gameDraw => 'Draw';
@override
String get yourTurn => 'Your turn';
@override
String get opponentTurn => 'Opponent turn';
@override
String get pleaseEnterValidCoordinates => 'Please enter valid coordinates';
@override
String get locationNotAvailable => 'Location not available';
@override
String get customCaveMapPoint => 'Custom cave map point';
@override
String get mapPoint => 'Map point';
@override
String get fetchingMissingImageFragments => 'Fetching missing fragments…';
@override
String get loadingImage => 'Loading…';
@override
String get receivingImage => 'Receiving…';
@override
String get tapToLoad => 'Tap to load';
@override
String get connectDevice => 'Connect Device';
@override
String get chooseTransportSubtitle =>
'Choose Bluetooth, WiFi, or Serial transport';
@override
String get ble => 'BLE';
@override
String get network => 'Network';
@override
String get serial => 'Serial';
@override
String get scan => 'Scan';
@override
String get pressScanToSearchForDevices =>
'Press scan to search for nearby devices';
@override
String failedToConnectToDevice(String name) {
return 'Failed to connect to $name';
}
@override
String get unknownDevice => 'Unknown Device';
@override
String get showingCachedResultsTapRefresh =>
'Showing cached results. Tap refresh to rescan.';
@override
String scanningLocalNetworkOnPort(int port) {
return 'Scanning local network for MeshCore WiFi devices on port $port';
}
@override
String get cancelScanAndAddServer => 'Cancel scan and add server';
@override
String get addServer => 'Add server';
@override
String scanningProgressIps(int scanned, String total) {
return 'Scanning... $scanned/$total IPs';
}
@override
String get cancelAndEnterManually => 'Cancel and enter manually';
@override
String get noRecentOrDiscoveredServers =>
'No recent or discovered servers yet';
@override
String get recentlyUsed => 'Recently used';
@override
String get connecting => 'Connecting...';
@override
String get discoveredOnThisNetwork => 'Discovered on this network';
@override
String serverNoLongerAvailable(String host, int port) {
return 'Server at $host:$port is no longer available. Please scan again to find active servers.';
}
@override
String failedToConnectToHost(String host, int port) {
return 'Failed to connect to $host:$port';
}
@override
String get enterValidIpAddress => 'Enter a valid IP address';
@override
String get enterValidTcpPort => 'Enter a valid TCP port';
@override
String get ipAddress => 'IP address';
@override
String get tcpPort => 'TCP port';
@override
String get customServerPort => 'Custom server port';
@override
String get chat => 'Chat';
@override
String get restoringPreviousLink => 'Restoring previous link';
@override
String get noDeviceConnected => 'No device connected';
@override
String get sendAdvert => 'Send my contact';
@override
String get advertMode => 'Send my contact';
@override
String get advertModeSubtitle =>
'Choose how far your contact info should travel.';
@override
String get floodAdvertSent => 'Contact sent across the mesh';
@override
String get directAdvertSent => 'Contact sent to nearby devices';
}

View File

@@ -2923,6 +2923,20 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get sizeCompressionAndPreview => 'Tamaño, compresión y vista previa';
@override
String get simpleMode => 'Modo simple';
@override
String get simpleModeDescription =>
'Solo chat y mapa con contactos favoritos';
@override
String get simpleModeRequiresLockedDestination =>
'Activa primero «Bloquear destino» con un canal para usar el modo simple';
@override
String get noFavouriteContacts => 'Sin contactos favoritos';
@override
String get skip => 'Omitir';
@@ -3034,4 +3048,414 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get regionScopeCleared => 'Ámbito de región limpiado';
@override
String get selectDestinationFirst =>
'Selecciona primero un canal, contacto o sala';
@override
String get recipientNoLongerAvailable =>
'El destinatario ya no está disponible';
@override
String failedToSendMessage(String error) {
return 'Error al enviar: $error';
}
@override
String get tictactoeDirectOnly =>
'El tres en raya solo funciona en mensajes directos. Elige primero un contacto.';
@override
String get deviceKeyUnavailable =>
'La clave del dispositivo no está disponible';
@override
String get imageCompressionFailed => 'Error al comprimir la imagen';
@override
String get imageFragmentationFailed => 'Error al fragmentar la imagen';
@override
String get failedToAnnounceImage => 'Error al anunciar la imagen';
@override
String get noRecipientSelected => 'Ningún destinatario seleccionado';
@override
String get imageSendFailed => 'Error al enviar la imagen';
@override
String get microphonePermissionRequiredForVoice =>
'Se necesita permiso de micrófono para los mensajes de voz';
@override
String get recordingDiscarded => 'Grabación descartada';
@override
String publicChannelMediaWarning(String mediaType) {
return 'Estás a punto de enviar $mediaType al canal público. No es recomendable, porque cualquiera en la red mesh podría recibirlo. Elige un canal privado o etiquetado, salvo que sea lo que quieres.';
}
@override
String get selectPrivateChannelToShareLocation =>
'Selecciona un canal privado para compartir tu ubicación.';
@override
String get stopSharingMyLocation => 'Dejar de compartir mi ubicación';
@override
String get shareMyLocation => 'Compartir mi ubicación';
@override
String get stopRecording => 'Detener grabación';
@override
String get recordVoice => 'Grabar voz';
@override
String get moreActions => 'Más acciones';
@override
String get noMessagesInFilter => 'No hay mensajes en este filtro';
@override
String get noMatchesInFilter => 'No hay coincidencias en este filtro';
@override
String syncFailed(String error) {
return 'Error de sincronización: $error';
}
@override
String get invalidLink => 'Enlace no válido';
@override
String get cannotOpenLink => 'No se puede abrir el enlace';
@override
String get failedToOpenLink => 'Error al abrir el enlace';
@override
String get failedToResendMessage => 'Error al reenviar el mensaje';
@override
String retryFailed(String error) {
return 'Error al reintentar: $error';
}
@override
String get deleteDrawingAndMessage => 'Eliminar dibujo y mensaje';
@override
String get lastEchoRelay => 'Último eco: repetidor';
@override
String get lastEchoPath => 'Último eco: ruta';
@override
String get lastEchoBytesReport => 'Último eco: informe de bytes';
@override
String get noGpsCoordinatesAvailable => 'No hay coordenadas GPS disponibles';
@override
String get notASarMarker => 'No es un marcador SAR';
@override
String failedToSaveTemplate(String error) {
return 'Error al guardar la plantilla: $error';
}
@override
String get drawingNotFound => 'Dibujo no encontrado';
@override
String get unknownDrawingType => 'Tipo de dibujo desconocido';
@override
String get customMapMarker => 'Marcador de mapa personalizado';
@override
String customMapPointLabel(String point) {
return 'Punto: $point';
}
@override
String mapIdLabel(String id) {
return 'ID del mapa: $id';
}
@override
String get cannotFetchVoice => 'No se puede obtener la voz';
@override
String get senderContactUnknown =>
'El contacto del remitente es desconocido. Sincroniza primero los contactos.';
@override
String get senderRouteUnknown =>
'La ruta del remitente es desconocida. Sincroniza primero los contactos/la ruta.';
@override
String messageTooFar(String hops, String maxHops) {
return 'El mensaje está demasiado lejos ($hops saltos, máx. $maxHops).';
}
@override
String get senderRouteNoPathResponse =>
'La ruta del remitente no respondió a la comprobación de ruta. Sincroniza los contactos/la ruta e inténtalo de nuevo.';
@override
String get senderRouteNoRawResponse =>
'La ruta del remitente no respondió en la ruta de transporte raw.';
@override
String voiceFetchOverHops(String hops) {
return 'Obtener la voz a través de $hops saltos puede tardar un poco.';
}
@override
String get voiceReceiveCanceled => 'Recepción de voz cancelada';
@override
String get fetchingMissingVoiceFragments =>
'Obteniendo fragmentos de voz que faltan';
@override
String get receivingVoice => 'Recibiendo voz';
@override
String get cannotFetchImage => 'No se puede obtener la imagen';
@override
String imageFetchOverHops(String hops) {
return 'Obtener la imagen a través de $hops saltos puede tardar un poco.';
}
@override
String get imageAlreadyBeingReceived => 'La imagen ya se está recibiendo';
@override
String get loadImage => 'Cargar imagen';
@override
String get imageFetchFailedToSendRequest =>
'No se pudo enviar la solicitud de imagen';
@override
String get imageUnavailable => 'Imagen no disponible por ahora';
@override
String get imageFetchTimedOut =>
'La obtención de la imagen agotó el tiempo de espera';
@override
String get imageReceiveCanceled => 'Recepción de imagen cancelada';
@override
String get closeImagePreview => 'Cerrar vista previa de imagen';
@override
String get recordingReleaseToSend => 'Grabando... suelta para enviar la voz';
@override
String get sendingVoice => 'Enviando voz...';
@override
String get sendLongPressToRecordVoice =>
'Enviar (mantén pulsado para grabar voz)';
@override
String get discardRecording => 'Descartar grabación';
@override
String hopCount(num count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count saltos',
one: '1 salto',
);
return '$_temp0';
}
@override
String get channelsSectionDescription =>
'Canales de difusión para el tráfico mesh cercano';
@override
String get roomsSectionDescription =>
'Espacios compartidos para la coordinación del equipo';
@override
String get contactsSectionDescription =>
'Personas y dispositivos a tu alcance directo';
@override
String get noRecentChatters => 'Sin participantes recientes';
@override
String get public => 'Público';
@override
String get failedToSendTicTacToeMove =>
'No se pudo enviar el movimiento de tres en raya';
@override
String get youWon => 'Has ganado';
@override
String get opponentWon => 'Ha ganado el rival';
@override
String get gameDraw => 'Empate';
@override
String get yourTurn => 'Te toca';
@override
String get opponentTurn => 'Le toca al rival';
@override
String get pleaseEnterValidCoordinates => 'Introduce coordenadas válidas';
@override
String get locationNotAvailable => 'Ubicación no disponible';
@override
String get customCaveMapPoint => 'Punto de mapa de cueva personalizado';
@override
String get mapPoint => 'Punto del mapa';
@override
String get fetchingMissingImageFragments =>
'Obteniendo fragmentos que faltan…';
@override
String get loadingImage => 'Cargando…';
@override
String get receivingImage => 'Recibiendo…';
@override
String get tapToLoad => 'Toca para cargar';
@override
String get connectDevice => 'Conectar dispositivo';
@override
String get chooseTransportSubtitle =>
'Elige el transporte Bluetooth, WiFi o Serie';
@override
String get ble => 'BLE';
@override
String get network => 'Red';
@override
String get serial => 'Serie';
@override
String get scan => 'Buscar';
@override
String get pressScanToSearchForDevices =>
'Pulsa Buscar para encontrar dispositivos cercanos';
@override
String failedToConnectToDevice(String name) {
return 'Error al conectar con $name';
}
@override
String get unknownDevice => 'Dispositivo desconocido';
@override
String get showingCachedResultsTapRefresh =>
'Mostrando resultados en caché. Toca actualizar para volver a buscar.';
@override
String scanningLocalNetworkOnPort(int port) {
return 'Buscando dispositivos MeshCore WiFi en la red local en el puerto $port';
}
@override
String get cancelScanAndAddServer => 'Cancelar búsqueda y añadir servidor';
@override
String get addServer => 'Añadir servidor';
@override
String scanningProgressIps(int scanned, String total) {
return 'Buscando... $scanned/$total IP';
}
@override
String get cancelAndEnterManually => 'Cancelar e introducir manualmente';
@override
String get noRecentOrDiscoveredServers =>
'Aún no hay servidores recientes ni descubiertos';
@override
String get recentlyUsed => 'Usados recientemente';
@override
String get connecting => 'Conectando...';
@override
String get discoveredOnThisNetwork => 'Descubiertos en esta red';
@override
String serverNoLongerAvailable(String host, int port) {
return 'El servidor en $host:$port ya no está disponible. Vuelve a buscar para encontrar servidores activos.';
}
@override
String failedToConnectToHost(String host, int port) {
return 'Error al conectar con $host:$port';
}
@override
String get enterValidIpAddress => 'Introduce una dirección IP válida';
@override
String get enterValidTcpPort => 'Introduce un puerto TCP válido';
@override
String get ipAddress => 'Dirección IP';
@override
String get tcpPort => 'Puerto TCP';
@override
String get customServerPort => 'Puerto de servidor personalizado';
@override
String get chat => 'Chat';
@override
String get restoringPreviousLink => 'Restaurando el enlace anterior';
@override
String get noDeviceConnected => 'Ningún dispositivo conectado';
@override
String get sendAdvert => 'Enviar mi contacto';
@override
String get advertMode => 'Enviar mi contacto';
@override
String get advertModeSubtitle =>
'Elige hasta dónde debe llegar tu información de contacto.';
@override
String get floodAdvertSent => 'Contacto enviado a través de la red mesh';
@override
String get directAdvertSent => 'Contacto enviado a dispositivos cercanos';
}

View File

@@ -2928,6 +2928,20 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get sizeCompressionAndPreview => 'Taille, compression et aperçu';
@override
String get simpleMode => 'Mode simple';
@override
String get simpleModeDescription =>
'Uniquement le chat et la carte avec les contacts favoris';
@override
String get simpleModeRequiresLockedDestination =>
'Activez d\'abord « Verrouiller la destination » avec un canal pour utiliser le mode simple';
@override
String get noFavouriteContacts => 'Aucun contact favori';
@override
String get skip => 'Passer';
@@ -3039,4 +3053,418 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get regionScopeCleared => 'Portée régionale effacée';
@override
String get selectDestinationFirst =>
'Sélectionnez d\'abord un canal, un contact ou un salon';
@override
String get recipientNoLongerAvailable =>
'Le destinataire n\'est plus disponible';
@override
String failedToSendMessage(String error) {
return 'Échec de l\'envoi : $error';
}
@override
String get tictactoeDirectOnly =>
'Le morpion ne fonctionne qu\'en messages directs. Choisissez d\'abord un contact.';
@override
String get deviceKeyUnavailable =>
'La clé de l\'appareil n\'est pas disponible';
@override
String get imageCompressionFailed => 'Échec de la compression de l\'image';
@override
String get imageFragmentationFailed =>
'Échec de la fragmentation de l\'image';
@override
String get failedToAnnounceImage => 'Échec de l\'annonce de l\'image';
@override
String get noRecipientSelected => 'Aucun destinataire sélectionné';
@override
String get imageSendFailed => 'Échec de l\'envoi de l\'image';
@override
String get microphonePermissionRequiredForVoice =>
'L\'autorisation du microphone est requise pour la voix';
@override
String get recordingDiscarded => 'Enregistrement abandonné';
@override
String publicChannelMediaWarning(String mediaType) {
return 'Vous êtes sur le point d\'envoyer $mediaType sur le canal public. Ce n\'est pas conseillé, car tout le monde sur le réseau mesh peut le recevoir. Choisissez un canal privé ou étiqueté, sauf si c\'est bien votre intention.';
}
@override
String get selectPrivateChannelToShareLocation =>
'Sélectionnez un canal privé pour partager votre position.';
@override
String get stopSharingMyLocation => 'Arrêter de partager ma position';
@override
String get shareMyLocation => 'Partager ma position';
@override
String get stopRecording => 'Arrêter l\'enregistrement';
@override
String get recordVoice => 'Enregistrer un message vocal';
@override
String get moreActions => 'Plus d\'actions';
@override
String get noMessagesInFilter => 'Aucun message dans ce filtre';
@override
String get noMatchesInFilter => 'Aucun résultat dans ce filtre';
@override
String syncFailed(String error) {
return 'Échec de la synchronisation : $error';
}
@override
String get invalidLink => 'Lien invalide';
@override
String get cannotOpenLink => 'Impossible d\'ouvrir le lien';
@override
String get failedToOpenLink => 'Échec de l\'ouverture du lien';
@override
String get failedToResendMessage => 'Échec du renvoi du message';
@override
String retryFailed(String error) {
return 'Échec de la nouvelle tentative : $error';
}
@override
String get deleteDrawingAndMessage => 'Supprimer le dessin et le message';
@override
String get lastEchoRelay => 'Dernier écho : relais';
@override
String get lastEchoPath => 'Dernier écho : chemin';
@override
String get lastEchoBytesReport => 'Dernier écho : rapport d\'octets';
@override
String get noGpsCoordinatesAvailable => 'Aucune coordonnée GPS disponible';
@override
String get notASarMarker => 'Ce n\'est pas un marqueur SAR';
@override
String failedToSaveTemplate(String error) {
return 'Échec de l\'enregistrement du modèle : $error';
}
@override
String get drawingNotFound => 'Dessin introuvable';
@override
String get unknownDrawingType => 'Type de dessin inconnu';
@override
String get customMapMarker => 'Marqueur de carte personnalisé';
@override
String customMapPointLabel(String point) {
return 'Point : $point';
}
@override
String mapIdLabel(String id) {
return 'ID de carte : $id';
}
@override
String get cannotFetchVoice => 'Impossible de récupérer la voix';
@override
String get senderContactUnknown =>
'Le contact de l\'expéditeur est inconnu. Synchronisez d\'abord les contacts.';
@override
String get senderRouteUnknown =>
'La route de l\'expéditeur est inconnue. Synchronisez d\'abord les contacts/le chemin.';
@override
String messageTooFar(String hops, String maxHops) {
return 'Le message est trop loin ($hops sauts, max. $maxHops).';
}
@override
String get senderRouteNoPathResponse =>
'La route de l\'expéditeur n\'a pas répondu à la vérification du chemin. Synchronisez les contacts/le chemin et réessayez.';
@override
String get senderRouteNoRawResponse =>
'La route de l\'expéditeur n\'a pas répondu sur le chemin de transport brut.';
@override
String voiceFetchOverHops(String hops) {
return 'La récupération de la voix sur $hops sauts peut prendre un moment.';
}
@override
String get voiceReceiveCanceled => 'Réception vocale annulée';
@override
String get fetchingMissingVoiceFragments =>
'Récupération des fragments vocaux manquants';
@override
String get receivingVoice => 'Réception de la voix';
@override
String get cannotFetchImage => 'Impossible de récupérer l\'image';
@override
String imageFetchOverHops(String hops) {
return 'La récupération de l\'image sur $hops sauts peut prendre un moment.';
}
@override
String get imageAlreadyBeingReceived =>
'L\'image est déjà en cours de réception';
@override
String get loadImage => 'Charger l\'image';
@override
String get imageFetchFailedToSendRequest =>
'Échec de l\'envoi de la demande d\'image';
@override
String get imageUnavailable => 'Image indisponible pour le moment';
@override
String get imageFetchTimedOut => 'Délai de récupération de l\'image dépassé';
@override
String get imageReceiveCanceled => 'Réception de l\'image annulée';
@override
String get closeImagePreview => 'Fermer l\'aperçu de l\'image';
@override
String get recordingReleaseToSend =>
'Enregistrement... relâchez pour envoyer';
@override
String get sendingVoice => 'Envoi du message vocal...';
@override
String get sendLongPressToRecordVoice =>
'Envoyer (appui long pour enregistrer la voix)';
@override
String get discardRecording => 'Abandonner l\'enregistrement';
@override
String hopCount(num count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count sauts',
one: '$count saut',
);
return '$_temp0';
}
@override
String get channelsSectionDescription =>
'Canaux de diffusion pour le trafic mesh à proximité';
@override
String get roomsSectionDescription =>
'Espaces partagés pour la coordination d\'équipe';
@override
String get contactsSectionDescription =>
'Personnes et appareils joignables directement';
@override
String get noRecentChatters => 'Aucun participant récent';
@override
String get public => 'Public';
@override
String get failedToSendTicTacToeMove =>
'Échec de l\'envoi du coup de morpion';
@override
String get youWon => 'Vous avez gagné';
@override
String get opponentWon => 'L\'adversaire a gagné';
@override
String get gameDraw => 'Match nul';
@override
String get yourTurn => 'À vous de jouer';
@override
String get opponentTurn => 'Au tour de l\'adversaire';
@override
String get pleaseEnterValidCoordinates =>
'Veuillez saisir des coordonnées valides';
@override
String get locationNotAvailable => 'Position non disponible';
@override
String get customCaveMapPoint => 'Point de carte de grotte personnalisé';
@override
String get mapPoint => 'Point de carte';
@override
String get fetchingMissingImageFragments =>
'Récupération des fragments manquants…';
@override
String get loadingImage => 'Chargement…';
@override
String get receivingImage => 'Réception…';
@override
String get tapToLoad => 'Touchez pour charger';
@override
String get connectDevice => 'Connecter l\'appareil';
@override
String get chooseTransportSubtitle =>
'Choisissez le transport Bluetooth, WiFi ou Série';
@override
String get ble => 'BLE';
@override
String get network => 'Réseau';
@override
String get serial => 'Série';
@override
String get scan => 'Rechercher';
@override
String get pressScanToSearchForDevices =>
'Appuyez sur Rechercher pour trouver les appareils à proximité';
@override
String failedToConnectToDevice(String name) {
return 'Échec de la connexion à $name';
}
@override
String get unknownDevice => 'Appareil inconnu';
@override
String get showingCachedResultsTapRefresh =>
'Affichage des résultats en cache. Touchez Actualiser pour relancer la recherche.';
@override
String scanningLocalNetworkOnPort(int port) {
return 'Recherche d\'appareils MeshCore WiFi sur le réseau local, port $port';
}
@override
String get cancelScanAndAddServer =>
'Annuler la recherche et ajouter un serveur';
@override
String get addServer => 'Ajouter un serveur';
@override
String scanningProgressIps(int scanned, String total) {
return 'Recherche... $scanned/$total IP';
}
@override
String get cancelAndEnterManually => 'Annuler et saisir manuellement';
@override
String get noRecentOrDiscoveredServers =>
'Aucun serveur récent ou découvert pour le moment';
@override
String get recentlyUsed => 'Utilisés récemment';
@override
String get connecting => 'Connexion...';
@override
String get discoveredOnThisNetwork => 'Découverts sur ce réseau';
@override
String serverNoLongerAvailable(String host, int port) {
return 'Le serveur $host:$port n\'est plus disponible. Veuillez relancer la recherche pour trouver des serveurs actifs.';
}
@override
String failedToConnectToHost(String host, int port) {
return 'Échec de la connexion à $host:$port';
}
@override
String get enterValidIpAddress => 'Saisissez une adresse IP valide';
@override
String get enterValidTcpPort => 'Saisissez un port TCP valide';
@override
String get ipAddress => 'Adresse IP';
@override
String get tcpPort => 'Port TCP';
@override
String get customServerPort => 'Port de serveur personnalisé';
@override
String get chat => 'Chat';
@override
String get restoringPreviousLink => 'Restauration de la liaison précédente';
@override
String get noDeviceConnected => 'Aucun appareil connecté';
@override
String get sendAdvert => 'Envoyer mon contact';
@override
String get advertMode => 'Envoyer mon contact';
@override
String get advertModeSubtitle =>
'Choisissez jusqu\'où vos informations de contact doivent être diffusées.';
@override
String get floodAdvertSent => 'Contact envoyé à travers le réseau mesh';
@override
String get directAdvertSent => 'Contact envoyé aux appareils à proximité';
}

View File

@@ -2901,6 +2901,20 @@ class AppLocalizationsHr extends AppLocalizations {
@override
String get sizeCompressionAndPreview => 'Veličina, kompresija i pregled';
@override
String get simpleMode => 'Jednostavni način';
@override
String get simpleModeDescription =>
'Samo razgovor i karta s omiljenim kontaktima';
@override
String get simpleModeRequiresLockedDestination =>
'Za korištenje jednostavnog načina najprije omogućite „Zaključaj odredište” s kanalom';
@override
String get noFavouriteContacts => 'Nema omiljenih kontakata';
@override
String get skip => 'Preskoči';
@@ -3012,4 +3026,411 @@ class AppLocalizationsHr extends AppLocalizations {
@override
String get regionScopeCleared => 'Opseg regije obrisan';
@override
String get selectDestinationFirst =>
'Najprije odaberite kanal, kontakt ili sobu';
@override
String get recipientNoLongerAvailable => 'Primatelj više nije dostupan';
@override
String failedToSendMessage(String error) {
return 'Slanje nije uspjelo: $error';
}
@override
String get tictactoeDirectOnly =>
'Križić-kružić radi samo u izravnim porukama. Najprije odaberite kontakt.';
@override
String get deviceKeyUnavailable => 'Ključ uređaja nije dostupan';
@override
String get imageCompressionFailed => 'Komprimiranje slike nije uspjelo';
@override
String get imageFragmentationFailed => 'Fragmentiranje slike nije uspjelo';
@override
String get failedToAnnounceImage => 'Najava slike nije uspjela';
@override
String get noRecipientSelected => 'Primatelj nije odabran';
@override
String get imageSendFailed => 'Slanje slike nije uspjelo';
@override
String get microphonePermissionRequiredForVoice =>
'Za glasovne poruke potrebna je dozvola za mikrofon';
@override
String get recordingDiscarded => 'Snimka je odbačena';
@override
String publicChannelMediaWarning(String mediaType) {
return 'Upravo ćete poslati $mediaType na javni kanal. To se ne preporučuje jer sadržaj mogu primiti svi na mesh mreži. Odaberite privatni ili označeni kanal, osim ako je to baš ono što želite.';
}
@override
String get selectPrivateChannelToShareLocation =>
'Odaberite privatni kanal za dijeljenje lokacije.';
@override
String get stopSharingMyLocation => 'Prestani dijeliti moju lokaciju';
@override
String get shareMyLocation => 'Dijeli moju lokaciju';
@override
String get stopRecording => 'Zaustavi snimanje';
@override
String get recordVoice => 'Snimi glasovnu poruku';
@override
String get moreActions => 'Više radnji';
@override
String get noMessagesInFilter => 'Nema poruka u ovom filtru';
@override
String get noMatchesInFilter => 'Nema rezultata u ovom filtru';
@override
String syncFailed(String error) {
return 'Sinkronizacija nije uspjela: $error';
}
@override
String get invalidLink => 'Nevažeća poveznica';
@override
String get cannotOpenLink => 'Poveznicu nije moguće otvoriti';
@override
String get failedToOpenLink => 'Otvaranje poveznice nije uspjelo';
@override
String get failedToResendMessage => 'Ponovno slanje poruke nije uspjelo';
@override
String retryFailed(String error) {
return 'Ponovni pokušaj nije uspio: $error';
}
@override
String get deleteDrawingAndMessage => 'Izbriši crtež i poruku';
@override
String get lastEchoRelay => 'Zadnji odjek relej';
@override
String get lastEchoPath => 'Zadnji odjek putanja';
@override
String get lastEchoBytesReport => 'Zadnji odjek izvještaj o bajtovima';
@override
String get noGpsCoordinatesAvailable => 'GPS koordinate nisu dostupne';
@override
String get notASarMarker => 'Nije SAR marker';
@override
String failedToSaveTemplate(String error) {
return 'Spremanje predloška nije uspjelo: $error';
}
@override
String get drawingNotFound => 'Crtež nije pronađen';
@override
String get unknownDrawingType => 'Nepoznata vrsta crteža';
@override
String get customMapMarker => 'Prilagođeni marker karte';
@override
String customMapPointLabel(String point) {
return 'Točka: $point';
}
@override
String mapIdLabel(String id) {
return 'ID karte: $id';
}
@override
String get cannotFetchVoice => 'Glasovnu poruku nije moguće dohvatiti';
@override
String get senderContactUnknown =>
'Kontakt pošiljatelja nije poznat. Najprije sinkronizirajte kontakte.';
@override
String get senderRouteUnknown =>
'Ruta pošiljatelja nije poznata. Najprije sinkronizirajte kontakte/putanju.';
@override
String messageTooFar(String hops, String maxHops) {
return 'Poruka je predaleko ($hops skokova, najviše $maxHops).';
}
@override
String get senderRouteNoPathResponse =>
'Ruta pošiljatelja nije odgovorila na provjeru putanje. Sinkronizirajte kontakte/putanju i pokušajte ponovno.';
@override
String get senderRouteNoRawResponse =>
'Ruta pošiljatelja nije odgovorila na izravnoj prijenosnoj putanji.';
@override
String voiceFetchOverHops(String hops) {
return 'Dohvaćanje glasa preko $hops skokova može potrajati.';
}
@override
String get voiceReceiveCanceled => 'Primanje glasa je otkazano';
@override
String get fetchingMissingVoiceFragments =>
'Dohvaćanje nedostajućih glasovnih fragmenata';
@override
String get receivingVoice => 'Primanje glasa';
@override
String get cannotFetchImage => 'Sliku nije moguće dohvatiti';
@override
String imageFetchOverHops(String hops) {
return 'Dohvaćanje slike preko $hops skokova može potrajati.';
}
@override
String get imageAlreadyBeingReceived => 'Slika se već prima';
@override
String get loadImage => 'Učitaj sliku';
@override
String get imageFetchFailedToSendRequest =>
'Zahtjev za dohvaćanje slike nije poslan';
@override
String get imageUnavailable => 'Slika trenutačno nije dostupna';
@override
String get imageFetchTimedOut => 'Dohvaćanje slike je isteklo';
@override
String get imageReceiveCanceled => 'Primanje slike je otkazano';
@override
String get closeImagePreview => 'Zatvori pregled slike';
@override
String get recordingReleaseToSend => 'Snimanje... otpustite za slanje glasa';
@override
String get sendingVoice => 'Slanje glasa...';
@override
String get sendLongPressToRecordVoice =>
'Pošalji (dugi pritisak za snimanje glasa)';
@override
String get discardRecording => 'Odbaci snimku';
@override
String hopCount(num count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count skokova',
few: '$count skoka',
one: '$count skok',
);
return '$_temp0';
}
@override
String get channelsSectionDescription =>
'Kanali za emitiranje mesh prometa u blizini';
@override
String get roomsSectionDescription =>
'Zajednički prostori za stalnu koordinaciju tima';
@override
String get contactsSectionDescription =>
'Osobe i uređaji koje možete izravno dosegnuti';
@override
String get noRecentChatters => 'Nema nedavnih sudionika';
@override
String get public => 'Javno';
@override
String get failedToSendTicTacToeMove => 'Potez križić-kružić nije poslan';
@override
String get youWon => 'Pobijedili ste';
@override
String get opponentWon => 'Protivnik je pobijedio';
@override
String get gameDraw => 'Neriješeno';
@override
String get yourTurn => 'Vi ste na potezu';
@override
String get opponentTurn => 'Protivnik je na potezu';
@override
String get pleaseEnterValidCoordinates => 'Unesite valjane koordinate';
@override
String get locationNotAvailable => 'Lokacija nije dostupna';
@override
String get customCaveMapPoint => 'Prilagođena točka karte špilje';
@override
String get mapPoint => 'Točka karte';
@override
String get fetchingMissingImageFragments =>
'Dohvaćanje nedostajućih fragmenata…';
@override
String get loadingImage => 'Učitavanje…';
@override
String get receivingImage => 'Primanje…';
@override
String get tapToLoad => 'Dodirnite za učitavanje';
@override
String get connectDevice => 'Poveži uređaj';
@override
String get chooseTransportSubtitle =>
'Odaberite način povezivanja: Bluetooth, WiFi ili serijski';
@override
String get ble => 'BLE';
@override
String get network => 'Mreža';
@override
String get serial => 'Serijski';
@override
String get scan => 'Skeniraj';
@override
String get pressScanToSearchForDevices =>
'Pritisnite Skeniraj za traženje uređaja u blizini';
@override
String failedToConnectToDevice(String name) {
return 'Povezivanje s uređajem $name nije uspjelo';
}
@override
String get unknownDevice => 'Nepoznat uređaj';
@override
String get showingCachedResultsTapRefresh =>
'Prikazani su predmemorirani rezultati. Dodirnite osvježi za ponovno skeniranje.';
@override
String scanningLocalNetworkOnPort(int port) {
return 'Skeniranje lokalne mreže za MeshCore WiFi uređaje na portu $port';
}
@override
String get cancelScanAndAddServer => 'Otkaži skeniranje i dodaj poslužitelj';
@override
String get addServer => 'Dodaj poslužitelj';
@override
String scanningProgressIps(int scanned, String total) {
return 'Skeniranje... $scanned/$total IP adresa';
}
@override
String get cancelAndEnterManually => 'Otkaži i unesi ručno';
@override
String get noRecentOrDiscoveredServers =>
'Još nema nedavnih ili otkrivenih poslužitelja';
@override
String get recentlyUsed => 'Nedavno korišteno';
@override
String get connecting => 'Povezivanje...';
@override
String get discoveredOnThisNetwork => 'Otkriveno na ovoj mreži';
@override
String serverNoLongerAvailable(String host, int port) {
return 'Poslužitelj na $host:$port više nije dostupan. Skenirajte ponovno da pronađete aktivne poslužitelje.';
}
@override
String failedToConnectToHost(String host, int port) {
return 'Povezivanje s $host:$port nije uspjelo';
}
@override
String get enterValidIpAddress => 'Unesite valjanu IP adresu';
@override
String get enterValidTcpPort => 'Unesite valjani TCP port';
@override
String get ipAddress => 'IP adresa';
@override
String get tcpPort => 'TCP port';
@override
String get customServerPort => 'Prilagođeni port poslužitelja';
@override
String get chat => 'Razgovor';
@override
String get restoringPreviousLink => 'Obnavljanje prethodne veze';
@override
String get noDeviceConnected => 'Nijedan uređaj nije povezan';
@override
String get sendAdvert => 'Pošalji moj kontakt';
@override
String get advertMode => 'Pošalji moj kontakt';
@override
String get advertModeSubtitle =>
'Odaberite koliko daleko trebaju putovati vaši kontaktni podaci.';
@override
String get floodAdvertSent => 'Kontakt poslan preplavljanjem kroz mrežu';
@override
String get directAdvertSent => 'Kontakt poslan izravno obližnjim uređajima';
}

View File

@@ -2915,6 +2915,20 @@ class AppLocalizationsIt extends AppLocalizations {
String get sizeCompressionAndPreview =>
'Dimensione, compressione e anteprima';
@override
String get simpleMode => 'Modalità semplice';
@override
String get simpleModeDescription =>
'Solo chat e mappa con i contatti preferiti';
@override
String get simpleModeRequiresLockedDestination =>
'Attiva prima «Blocca destinazione» con un canale per usare la modalità semplice';
@override
String get noFavouriteContacts => 'Nessun contatto preferito';
@override
String get skip => 'Salta';
@@ -3026,4 +3040,415 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get regionScopeCleared => 'Ambito regionale cancellato';
@override
String get selectDestinationFirst =>
'Seleziona prima un canale, un contatto o una stanza';
@override
String get recipientNoLongerAvailable =>
'Il destinatario non è più disponibile';
@override
String failedToSendMessage(String error) {
return 'Invio non riuscito: $error';
}
@override
String get tictactoeDirectOnly =>
'Il tris funziona solo nei messaggi diretti. Scegli prima un contatto.';
@override
String get deviceKeyUnavailable =>
'La chiave del dispositivo non è disponibile';
@override
String get imageCompressionFailed =>
'Compressione dell\'immagine non riuscita';
@override
String get imageFragmentationFailed =>
'Frammentazione dell\'immagine non riuscita';
@override
String get failedToAnnounceImage => 'Annuncio dell\'immagine non riuscito';
@override
String get noRecipientSelected => 'Nessun destinatario selezionato';
@override
String get imageSendFailed => 'Invio dell\'immagine non riuscito';
@override
String get microphonePermissionRequiredForVoice =>
'Per i messaggi vocali è necessario il permesso del microfono';
@override
String get recordingDiscarded => 'Registrazione scartata';
@override
String publicChannelMediaWarning(String mediaType) {
return 'Stai per inviare $mediaType al Canale Pubblico. Non è consigliato, perché chiunque sulla rete mesh potrebbe riceverlo. Scegli un canale privato o con tag, a meno che non sia ciò che vuoi.';
}
@override
String get selectPrivateChannelToShareLocation =>
'Seleziona un canale privato per condividere la tua posizione.';
@override
String get stopSharingMyLocation => 'Smetti di condividere la mia posizione';
@override
String get shareMyLocation => 'Condividi la mia posizione';
@override
String get stopRecording => 'Interrompi registrazione';
@override
String get recordVoice => 'Registra messaggio vocale';
@override
String get moreActions => 'Altre azioni';
@override
String get noMessagesInFilter => 'Nessun messaggio in questo filtro';
@override
String get noMatchesInFilter => 'Nessuna corrispondenza in questo filtro';
@override
String syncFailed(String error) {
return 'Sincronizzazione non riuscita: $error';
}
@override
String get invalidLink => 'Link non valido';
@override
String get cannotOpenLink => 'Impossibile aprire il link';
@override
String get failedToOpenLink => 'Apertura del link non riuscita';
@override
String get failedToResendMessage => 'Reinvio del messaggio non riuscito';
@override
String retryFailed(String error) {
return 'Nuovo tentativo non riuscito: $error';
}
@override
String get deleteDrawingAndMessage => 'Elimina disegno e messaggio';
@override
String get lastEchoRelay => 'Ultimo eco: relay';
@override
String get lastEchoPath => 'Ultimo eco: percorso';
@override
String get lastEchoBytesReport => 'Ultimo eco: report byte';
@override
String get noGpsCoordinatesAvailable => 'Nessuna coordinata GPS disponibile';
@override
String get notASarMarker => 'Non è un marcatore SAR';
@override
String failedToSaveTemplate(String error) {
return 'Salvataggio del modello non riuscito: $error';
}
@override
String get drawingNotFound => 'Disegno non trovato';
@override
String get unknownDrawingType => 'Tipo di disegno sconosciuto';
@override
String get customMapMarker => 'Marcatore mappa personalizzato';
@override
String customMapPointLabel(String point) {
return 'Punto: $point';
}
@override
String mapIdLabel(String id) {
return 'ID mappa: $id';
}
@override
String get cannotFetchVoice => 'Impossibile recuperare il vocale';
@override
String get senderContactUnknown =>
'Il contatto del mittente è sconosciuto. Sincronizza prima i contatti.';
@override
String get senderRouteUnknown =>
'Il percorso del mittente è sconosciuto. Sincronizza prima contatti/percorso.';
@override
String messageTooFar(String hops, String maxHops) {
return 'Il messaggio è troppo lontano ($hops hop, max $maxHops).';
}
@override
String get senderRouteNoPathResponse =>
'Il percorso del mittente non ha risposto alla verifica. Sincronizza contatti/percorso e riprova.';
@override
String get senderRouteNoRawResponse =>
'Il percorso del mittente non ha risposto sul percorso di trasporto raw.';
@override
String voiceFetchOverHops(String hops) {
return 'Il recupero del vocale su $hops hop può richiedere un po\' di tempo.';
}
@override
String get voiceReceiveCanceled => 'Ricezione vocale annullata';
@override
String get fetchingMissingVoiceFragments =>
'Recupero dei frammenti vocali mancanti';
@override
String get receivingVoice => 'Ricezione vocale';
@override
String get cannotFetchImage => 'Impossibile recuperare l\'immagine';
@override
String imageFetchOverHops(String hops) {
return 'Il recupero dell\'immagine su $hops hop può richiedere un po\' di tempo.';
}
@override
String get imageAlreadyBeingReceived => 'L\'immagine è già in ricezione';
@override
String get loadImage => 'Carica immagine';
@override
String get imageFetchFailedToSendRequest =>
'Invio della richiesta immagine non riuscito';
@override
String get imageUnavailable => 'Immagine al momento non disponibile';
@override
String get imageFetchTimedOut => 'Recupero dell\'immagine scaduto';
@override
String get imageReceiveCanceled => 'Ricezione dell\'immagine annullata';
@override
String get closeImagePreview => 'Chiudi anteprima immagine';
@override
String get recordingReleaseToSend => 'Registrazione... rilascia per inviare';
@override
String get sendingVoice => 'Invio vocale...';
@override
String get sendLongPressToRecordVoice =>
'Invia (pressione lunga per registrare un vocale)';
@override
String get discardRecording => 'Scarta registrazione';
@override
String hopCount(num count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count hop',
one: '1 hop',
);
return '$_temp0';
}
@override
String get channelsSectionDescription =>
'Canali broadcast per il traffico mesh nelle vicinanze';
@override
String get roomsSectionDescription =>
'Spazi condivisi per il coordinamento continuo del team';
@override
String get contactsSectionDescription =>
'Persone e dispositivi raggiungibili direttamente';
@override
String get noRecentChatters => 'Nessun partecipante recente';
@override
String get public => 'Pubblico';
@override
String get failedToSendTicTacToeMove =>
'Invio della mossa di tris non riuscito';
@override
String get youWon => 'Hai vinto';
@override
String get opponentWon => 'Ha vinto l\'avversario';
@override
String get gameDraw => 'Pareggio';
@override
String get yourTurn => 'Tocca a te';
@override
String get opponentTurn => 'Tocca all\'avversario';
@override
String get pleaseEnterValidCoordinates => 'Inserisci coordinate valide';
@override
String get locationNotAvailable => 'Posizione non disponibile';
@override
String get customCaveMapPoint => 'Punto mappa grotta personalizzato';
@override
String get mapPoint => 'Punto mappa';
@override
String get fetchingMissingImageFragments => 'Recupero frammenti mancanti…';
@override
String get loadingImage => 'Caricamento…';
@override
String get receivingImage => 'Ricezione…';
@override
String get tapToLoad => 'Tocca per caricare';
@override
String get connectDevice => 'Connetti dispositivo';
@override
String get chooseTransportSubtitle =>
'Scegli il trasporto Bluetooth, WiFi o Seriale';
@override
String get ble => 'BLE';
@override
String get network => 'Rete';
@override
String get serial => 'Seriale';
@override
String get scan => 'Cerca';
@override
String get pressScanToSearchForDevices =>
'Premi Cerca per trovare i dispositivi nelle vicinanze';
@override
String failedToConnectToDevice(String name) {
return 'Impossibile connettersi a $name';
}
@override
String get unknownDevice => 'Dispositivo sconosciuto';
@override
String get showingCachedResultsTapRefresh =>
'Risultati in cache. Tocca Aggiorna per ripetere la scansione.';
@override
String scanningLocalNetworkOnPort(int port) {
return 'Scansione della rete locale per dispositivi MeshCore WiFi sulla porta $port';
}
@override
String get cancelScanAndAddServer =>
'Annulla la scansione e aggiungi un server';
@override
String get addServer => 'Aggiungi server';
@override
String scanningProgressIps(int scanned, String total) {
return 'Scansione... $scanned/$total IP';
}
@override
String get cancelAndEnterManually => 'Annulla e inserisci manualmente';
@override
String get noRecentOrDiscoveredServers =>
'Nessun server recente o rilevato per ora';
@override
String get recentlyUsed => 'Usati di recente';
@override
String get connecting => 'Connessione...';
@override
String get discoveredOnThisNetwork => 'Rilevati su questa rete';
@override
String serverNoLongerAvailable(String host, int port) {
return 'Il server $host:$port non è più disponibile. Esegui di nuovo la scansione per trovare server attivi.';
}
@override
String failedToConnectToHost(String host, int port) {
return 'Impossibile connettersi a $host:$port';
}
@override
String get enterValidIpAddress => 'Inserisci un indirizzo IP valido';
@override
String get enterValidTcpPort => 'Inserisci una porta TCP valida';
@override
String get ipAddress => 'Indirizzo IP';
@override
String get tcpPort => 'Porta TCP';
@override
String get customServerPort => 'Porta server personalizzata';
@override
String get chat => 'Chat';
@override
String get restoringPreviousLink => 'Ripristino del collegamento precedente';
@override
String get noDeviceConnected => 'Nessun dispositivo connesso';
@override
String get sendAdvert => 'Invia il mio contatto';
@override
String get advertMode => 'Invia il mio contatto';
@override
String get advertModeSubtitle =>
'Scegli quanto lontano devono viaggiare le tue informazioni di contatto.';
@override
String get floodAdvertSent => 'Contatto inviato attraverso la rete mesh';
@override
String get directAdvertSent => 'Contatto inviato ai dispositivi vicini';
}

View File

@@ -2912,6 +2912,20 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get sizeCompressionAndPreview => 'Rozmiar, kompresja i podgląd';
@override
String get simpleMode => 'Tryb prosty';
@override
String get simpleModeDescription =>
'Tylko czat i mapa z ulubionymi kontaktami';
@override
String get simpleModeRequiresLockedDestination =>
'Aby użyć trybu prostego, najpierw włącz „Zablokuj cel” z kanałem';
@override
String get noFavouriteContacts => 'Brak ulubionych kontaktów';
@override
String get skip => 'Pomiń';
@@ -3023,4 +3037,414 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get regionScopeCleared => 'Zakres regionu wyczyszczony';
@override
String get selectDestinationFirst =>
'Najpierw wybierz kanał, kontakt lub pokój';
@override
String get recipientNoLongerAvailable => 'Odbiorca nie jest już dostępny';
@override
String failedToSendMessage(String error) {
return 'Nie udało się wysłać: $error';
}
@override
String get tictactoeDirectOnly =>
'Kółko i krzyżyk działa tylko w wiadomościach bezpośrednich. Najpierw wybierz kontakt.';
@override
String get deviceKeyUnavailable => 'Klucz urządzenia jest niedostępny';
@override
String get imageCompressionFailed => 'Kompresja obrazu nie powiodła się';
@override
String get imageFragmentationFailed => 'Fragmentacja obrazu nie powiodła się';
@override
String get failedToAnnounceImage => 'Nie udało się ogłosić obrazu';
@override
String get noRecipientSelected => 'Nie wybrano odbiorcy';
@override
String get imageSendFailed => 'Wysyłanie obrazu nie powiodło się';
@override
String get microphonePermissionRequiredForVoice =>
'Wiadomości głosowe wymagają uprawnienia do mikrofonu';
@override
String get recordingDiscarded => 'Nagranie odrzucone';
@override
String publicChannelMediaWarning(String mediaType) {
return 'Za chwilę wyślesz $mediaType na kanał publiczny. Nie jest to zalecane, ponieważ może to odebrać każdy w sieci mesh. Wybierz kanał prywatny lub oznaczony, chyba że właśnie tego chcesz.';
}
@override
String get selectPrivateChannelToShareLocation =>
'Wybierz prywatny kanał, aby udostępnić swoją lokalizację.';
@override
String get stopSharingMyLocation => 'Przestań udostępniać moją lokalizację';
@override
String get shareMyLocation => 'Udostępnij moją lokalizację';
@override
String get stopRecording => 'Zatrzymaj nagrywanie';
@override
String get recordVoice => 'Nagraj wiadomość głosową';
@override
String get moreActions => 'Więcej działań';
@override
String get noMessagesInFilter => 'Brak wiadomości w tym filtrze';
@override
String get noMatchesInFilter => 'Brak dopasowań w tym filtrze';
@override
String syncFailed(String error) {
return 'Synchronizacja nie powiodła się: $error';
}
@override
String get invalidLink => 'Nieprawidłowy link';
@override
String get cannotOpenLink => 'Nie można otworzyć linku';
@override
String get failedToOpenLink => 'Nie udało się otworzyć linku';
@override
String get failedToResendMessage =>
'Nie udało się ponownie wysłać wiadomości';
@override
String retryFailed(String error) {
return 'Ponowna próba nie powiodła się: $error';
}
@override
String get deleteDrawingAndMessage => 'Usuń rysunek i wiadomość';
@override
String get lastEchoRelay => 'Ostatnie echo przekaźnik';
@override
String get lastEchoPath => 'Ostatnie echo ścieżka';
@override
String get lastEchoBytesReport => 'Ostatnie echo raport bajtów';
@override
String get noGpsCoordinatesAvailable => 'Brak dostępnych współrzędnych GPS';
@override
String get notASarMarker => 'To nie jest znacznik SAR';
@override
String failedToSaveTemplate(String error) {
return 'Nie udało się zapisać szablonu: $error';
}
@override
String get drawingNotFound => 'Nie znaleziono rysunku';
@override
String get unknownDrawingType => 'Nieznany typ rysunku';
@override
String get customMapMarker => 'Niestandardowy znacznik mapy';
@override
String customMapPointLabel(String point) {
return 'Punkt: $point';
}
@override
String mapIdLabel(String id) {
return 'ID mapy: $id';
}
@override
String get cannotFetchVoice => 'Nie można pobrać wiadomości głosowej';
@override
String get senderContactUnknown =>
'Kontakt nadawcy jest nieznany. Najpierw zsynchronizuj kontakty.';
@override
String get senderRouteUnknown =>
'Trasa nadawcy jest nieznana. Najpierw zsynchronizuj kontakty/ścieżkę.';
@override
String messageTooFar(String hops, String maxHops) {
return 'Wiadomość jest zbyt daleko ($hops skoków, maks. $maxHops).';
}
@override
String get senderRouteNoPathResponse =>
'Trasa nadawcy nie odpowiedziała na sprawdzenie ścieżki. Zsynchronizuj kontakty/ścieżkę i spróbuj ponownie.';
@override
String get senderRouteNoRawResponse =>
'Trasa nadawcy nie odpowiedziała na surowej ścieżce transportowej.';
@override
String voiceFetchOverHops(String hops) {
return 'Pobieranie głosu przez $hops skoków może chwilę potrwać.';
}
@override
String get voiceReceiveCanceled => 'Odbieranie głosu anulowane';
@override
String get fetchingMissingVoiceFragments =>
'Pobieranie brakujących fragmentów głosu';
@override
String get receivingVoice => 'Odbieranie głosu';
@override
String get cannotFetchImage => 'Nie można pobrać obrazu';
@override
String imageFetchOverHops(String hops) {
return 'Pobieranie obrazu przez $hops skoków może chwilę potrwać.';
}
@override
String get imageAlreadyBeingReceived => 'Obraz jest już odbierany';
@override
String get loadImage => 'Wczytaj obraz';
@override
String get imageFetchFailedToSendRequest =>
'Nie udało się wysłać żądania pobrania obrazu';
@override
String get imageUnavailable => 'Obraz chwilowo niedostępny';
@override
String get imageFetchTimedOut => 'Upłynął limit czasu pobierania obrazu';
@override
String get imageReceiveCanceled => 'Odbieranie obrazu anulowane';
@override
String get closeImagePreview => 'Zamknij podgląd obrazu';
@override
String get recordingReleaseToSend => 'Nagrywanie... puść, aby wysłać';
@override
String get sendingVoice => 'Wysyłanie głosu...';
@override
String get sendLongPressToRecordVoice =>
'Wyślij (przytrzymaj, aby nagrać głos)';
@override
String get discardRecording => 'Odrzuć nagranie';
@override
String hopCount(num count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count skoku',
many: '$count skoków',
few: '$count skoki',
one: '$count skok',
);
return '$_temp0';
}
@override
String get channelsSectionDescription =>
'Kanały rozgłoszeniowe dla pobliskiego ruchu mesh';
@override
String get roomsSectionDescription =>
'Wspólne przestrzenie do bieżącej koordynacji zespołu';
@override
String get contactsSectionDescription =>
'Osoby i urządzenia w bezpośrednim zasięgu';
@override
String get noRecentChatters => 'Brak ostatnich rozmówców';
@override
String get public => 'Publiczny';
@override
String get failedToSendTicTacToeMove =>
'Nie udało się wysłać ruchu w kółko i krzyżyk';
@override
String get youWon => 'Wygrałeś';
@override
String get opponentWon => 'Wygrał przeciwnik';
@override
String get gameDraw => 'Remis';
@override
String get yourTurn => 'Twoja kolej';
@override
String get opponentTurn => 'Kolej przeciwnika';
@override
String get pleaseEnterValidCoordinates => 'Wprowadź prawidłowe współrzędne';
@override
String get locationNotAvailable => 'Lokalizacja niedostępna';
@override
String get customCaveMapPoint => 'Niestandardowy punkt mapy jaskini';
@override
String get mapPoint => 'Punkt mapy';
@override
String get fetchingMissingImageFragments =>
'Pobieranie brakujących fragmentów…';
@override
String get loadingImage => 'Wczytywanie…';
@override
String get receivingImage => 'Odbieranie…';
@override
String get tapToLoad => 'Dotknij, aby wczytać';
@override
String get connectDevice => 'Połącz urządzenie';
@override
String get chooseTransportSubtitle =>
'Wybierz transport: Bluetooth, WiFi lub port szeregowy';
@override
String get ble => 'BLE';
@override
String get network => 'Sieć';
@override
String get serial => 'Port szeregowy';
@override
String get scan => 'Skanuj';
@override
String get pressScanToSearchForDevices =>
'Naciśnij Skanuj, aby wyszukać pobliskie urządzenia';
@override
String failedToConnectToDevice(String name) {
return 'Nie udało się połączyć z $name';
}
@override
String get unknownDevice => 'Nieznane urządzenie';
@override
String get showingCachedResultsTapRefresh =>
'Wyświetlane są wyniki z pamięci podręcznej. Dotknij odświeżania, aby zeskanować ponownie.';
@override
String scanningLocalNetworkOnPort(int port) {
return 'Skanowanie sieci lokalnej w poszukiwaniu urządzeń MeshCore WiFi na porcie $port';
}
@override
String get cancelScanAndAddServer => 'Anuluj skanowanie i dodaj serwer';
@override
String get addServer => 'Dodaj serwer';
@override
String scanningProgressIps(int scanned, String total) {
return 'Skanowanie... $scanned/$total adresów IP';
}
@override
String get cancelAndEnterManually => 'Anuluj i wprowadź ręcznie';
@override
String get noRecentOrDiscoveredServers =>
'Brak ostatnich ani wykrytych serwerów';
@override
String get recentlyUsed => 'Ostatnio używane';
@override
String get connecting => 'Łączenie...';
@override
String get discoveredOnThisNetwork => 'Wykryte w tej sieci';
@override
String serverNoLongerAvailable(String host, int port) {
return 'Serwer $host:$port nie jest już dostępny. Zeskanuj ponownie, aby znaleźć aktywne serwery.';
}
@override
String failedToConnectToHost(String host, int port) {
return 'Nie udało się połączyć z $host:$port';
}
@override
String get enterValidIpAddress => 'Wprowadź prawidłowy adres IP';
@override
String get enterValidTcpPort => 'Wprowadź prawidłowy port TCP';
@override
String get ipAddress => 'Adres IP';
@override
String get tcpPort => 'Port TCP';
@override
String get customServerPort => 'Niestandardowy port serwera';
@override
String get chat => 'Czat';
@override
String get restoringPreviousLink => 'Przywracanie poprzedniego połączenia';
@override
String get noDeviceConnected => 'Brak połączonego urządzenia';
@override
String get sendAdvert => 'Wyślij mój kontakt';
@override
String get advertMode => 'Wyślij mój kontakt';
@override
String get advertModeSubtitle =>
'Wybierz, jak daleko mają dotrzeć Twoje dane kontaktowe.';
@override
String get floodAdvertSent => 'Kontakt wysłano do całej sieci mesh';
@override
String get directAdvertSent => 'Kontakt wysłano do urządzeń w pobliżu';
}

View File

@@ -2921,6 +2921,20 @@ class AppLocalizationsPt extends AppLocalizations {
String get sizeCompressionAndPreview =>
'Tamanho, compressão e pré-visualização';
@override
String get simpleMode => 'Modo simples';
@override
String get simpleModeDescription =>
'Apenas chat e mapa com contatos favoritos';
@override
String get simpleModeRequiresLockedDestination =>
'Ative primeiro \"Bloquear destino\" com um canal para usar o modo simples';
@override
String get noFavouriteContacts => 'Nenhum contato favorito';
@override
String get skip => 'Saltar';
@@ -3032,4 +3046,414 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get regionScopeCleared => 'Âmbito de região limpo';
@override
String get selectDestinationFirst =>
'Selecione primeiro um canal, contato ou sala';
@override
String get recipientNoLongerAvailable =>
'O destinatário não está mais disponível';
@override
String failedToSendMessage(String error) {
return 'Falha ao enviar: $error';
}
@override
String get tictactoeDirectOnly =>
'O jogo da velha funciona apenas em mensagens diretas. Escolha primeiro um contato.';
@override
String get deviceKeyUnavailable =>
'A chave do dispositivo não está disponível';
@override
String get imageCompressionFailed => 'Falha na compressão da imagem';
@override
String get imageFragmentationFailed => 'Falha na fragmentação da imagem';
@override
String get failedToAnnounceImage => 'Falha ao anunciar a imagem';
@override
String get noRecipientSelected => 'Nenhum destinatário selecionado';
@override
String get imageSendFailed => 'Falha no envio da imagem';
@override
String get microphonePermissionRequiredForVoice =>
'É necessária permissão de microfone para mensagens de voz';
@override
String get recordingDiscarded => 'Gravação descartada';
@override
String publicChannelMediaWarning(String mediaType) {
return 'Você está prestes a enviar $mediaType para o canal público. Isso não é recomendado, pois todos na rede mesh podem receber. Escolha um canal privado ou com tag, a menos que seja isso que você quer.';
}
@override
String get selectPrivateChannelToShareLocation =>
'Selecione um canal privado para compartilhar sua localização.';
@override
String get stopSharingMyLocation => 'Parar de compartilhar minha localização';
@override
String get shareMyLocation => 'Compartilhar minha localização';
@override
String get stopRecording => 'Parar gravação';
@override
String get recordVoice => 'Gravar mensagem de voz';
@override
String get moreActions => 'Mais ações';
@override
String get noMessagesInFilter => 'Nenhuma mensagem neste filtro';
@override
String get noMatchesInFilter => 'Nenhum resultado neste filtro';
@override
String syncFailed(String error) {
return 'Falha na sincronização: $error';
}
@override
String get invalidLink => 'Link inválido';
@override
String get cannotOpenLink => 'Não é possível abrir o link';
@override
String get failedToOpenLink => 'Falha ao abrir o link';
@override
String get failedToResendMessage => 'Falha ao reenviar a mensagem';
@override
String retryFailed(String error) {
return 'Falha na nova tentativa: $error';
}
@override
String get deleteDrawingAndMessage => 'Excluir desenho e mensagem';
@override
String get lastEchoRelay => 'Último eco: repetidor';
@override
String get lastEchoPath => 'Último eco: caminho';
@override
String get lastEchoBytesReport => 'Último eco: relatório de bytes';
@override
String get noGpsCoordinatesAvailable => 'Nenhuma coordenada GPS disponível';
@override
String get notASarMarker => 'Não é um marcador SAR';
@override
String failedToSaveTemplate(String error) {
return 'Falha ao salvar o modelo: $error';
}
@override
String get drawingNotFound => 'Desenho não encontrado';
@override
String get unknownDrawingType => 'Tipo de desenho desconhecido';
@override
String get customMapMarker => 'Marcador de mapa personalizado';
@override
String customMapPointLabel(String point) {
return 'Ponto: $point';
}
@override
String mapIdLabel(String id) {
return 'ID do mapa: $id';
}
@override
String get cannotFetchVoice => 'Não é possível obter a voz';
@override
String get senderContactUnknown =>
'O contato do remetente é desconhecido. Sincronize os contatos primeiro.';
@override
String get senderRouteUnknown =>
'A rota do remetente é desconhecida. Sincronize os contatos/o caminho primeiro.';
@override
String messageTooFar(String hops, String maxHops) {
return 'A mensagem está longe demais ($hops saltos, máx. $maxHops).';
}
@override
String get senderRouteNoPathResponse =>
'A rota do remetente não respondeu à verificação de caminho. Sincronize os contatos/o caminho e tente novamente.';
@override
String get senderRouteNoRawResponse =>
'A rota do remetente não respondeu no caminho de transporte raw.';
@override
String voiceFetchOverHops(String hops) {
return 'Obter a voz por $hops saltos pode demorar um pouco.';
}
@override
String get voiceReceiveCanceled => 'Recebimento de voz cancelado';
@override
String get fetchingMissingVoiceFragments =>
'Obtendo fragmentos de voz ausentes';
@override
String get receivingVoice => 'Recebendo voz';
@override
String get cannotFetchImage => 'Não é possível obter a imagem';
@override
String imageFetchOverHops(String hops) {
return 'Obter a imagem por $hops saltos pode demorar um pouco.';
}
@override
String get imageAlreadyBeingReceived => 'A imagem já está sendo recebida';
@override
String get loadImage => 'Carregar imagem';
@override
String get imageFetchFailedToSendRequest =>
'Falha ao enviar a solicitação da imagem';
@override
String get imageUnavailable => 'Imagem indisponível no momento';
@override
String get imageFetchTimedOut => 'Tempo esgotado ao obter a imagem';
@override
String get imageReceiveCanceled => 'Recebimento da imagem cancelado';
@override
String get closeImagePreview => 'Fechar visualização da imagem';
@override
String get recordingReleaseToSend => 'Gravando... solte para enviar';
@override
String get sendingVoice => 'Enviando voz...';
@override
String get sendLongPressToRecordVoice =>
'Enviar (toque longo para gravar voz)';
@override
String get discardRecording => 'Descartar gravação';
@override
String hopCount(num count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count saltos',
one: '1 salto',
);
return '$_temp0';
}
@override
String get channelsSectionDescription =>
'Canais de difusão para o tráfego mesh próximo';
@override
String get roomsSectionDescription =>
'Espaços compartilhados para coordenação contínua da equipe';
@override
String get contactsSectionDescription =>
'Pessoas e dispositivos ao seu alcance direto';
@override
String get noRecentChatters => 'Sem participantes recentes';
@override
String get public => 'Público';
@override
String get failedToSendTicTacToeMove =>
'Falha ao enviar a jogada do jogo da velha';
@override
String get youWon => 'Você venceu';
@override
String get opponentWon => 'O adversário venceu';
@override
String get gameDraw => 'Empate';
@override
String get yourTurn => 'Sua vez';
@override
String get opponentTurn => 'Vez do adversário';
@override
String get pleaseEnterValidCoordinates => 'Insira coordenadas válidas';
@override
String get locationNotAvailable => 'Localização não disponível';
@override
String get customCaveMapPoint => 'Ponto de mapa de caverna personalizado';
@override
String get mapPoint => 'Ponto do mapa';
@override
String get fetchingMissingImageFragments => 'Obtendo fragmentos ausentes…';
@override
String get loadingImage => 'Carregando…';
@override
String get receivingImage => 'Recebendo…';
@override
String get tapToLoad => 'Toque para carregar';
@override
String get connectDevice => 'Conectar dispositivo';
@override
String get chooseTransportSubtitle =>
'Escolha o transporte Bluetooth, WiFi ou Serial';
@override
String get ble => 'BLE';
@override
String get network => 'Rede';
@override
String get serial => 'Serial';
@override
String get scan => 'Escanear';
@override
String get pressScanToSearchForDevices =>
'Pressione escanear para procurar dispositivos próximos';
@override
String failedToConnectToDevice(String name) {
return 'Falha ao conectar a $name';
}
@override
String get unknownDevice => 'Dispositivo desconhecido';
@override
String get showingCachedResultsTapRefresh =>
'Exibindo resultados em cache. Toque em atualizar para escanear novamente.';
@override
String scanningLocalNetworkOnPort(int port) {
return 'Escaneando a rede local em busca de dispositivos MeshCore WiFi na porta $port';
}
@override
String get cancelScanAndAddServer =>
'Cancelar escaneamento e adicionar servidor';
@override
String get addServer => 'Adicionar servidor';
@override
String scanningProgressIps(int scanned, String total) {
return 'Escaneando... $scanned/$total IPs';
}
@override
String get cancelAndEnterManually => 'Cancelar e inserir manualmente';
@override
String get noRecentOrDiscoveredServers =>
'Ainda não há servidores recentes ou descobertos';
@override
String get recentlyUsed => 'Usados recentemente';
@override
String get connecting => 'Conectando...';
@override
String get discoveredOnThisNetwork => 'Descobertos nesta rede';
@override
String serverNoLongerAvailable(String host, int port) {
return 'O servidor em $host:$port não está mais disponível. Escaneie novamente para encontrar servidores ativos.';
}
@override
String failedToConnectToHost(String host, int port) {
return 'Falha ao conectar a $host:$port';
}
@override
String get enterValidIpAddress => 'Insira um endereço IP válido';
@override
String get enterValidTcpPort => 'Insira uma porta TCP válida';
@override
String get ipAddress => 'Endereço IP';
@override
String get tcpPort => 'Porta TCP';
@override
String get customServerPort => 'Porta de servidor personalizada';
@override
String get chat => 'Chat';
@override
String get restoringPreviousLink => 'Restaurando a conexão anterior';
@override
String get noDeviceConnected => 'Nenhum dispositivo conectado';
@override
String get sendAdvert => 'Enviar meu contato';
@override
String get advertMode => 'Enviar meu contato';
@override
String get advertModeSubtitle =>
'Escolha até onde suas informações de contato devem chegar.';
@override
String get floodAdvertSent => 'Contato enviado por toda a rede mesh';
@override
String get directAdvertSent =>
'Contato enviado para os dispositivos próximos';
}

View File

@@ -2908,6 +2908,20 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get sizeCompressionAndPreview => 'Размер, сжатие и предпросмотр';
@override
String get simpleMode => 'Простой режим';
@override
String get simpleModeDescription =>
'Только чат и карта с избранными контактами';
@override
String get simpleModeRequiresLockedDestination =>
'Сначала включите «Зафиксировать назначение» с каналом, чтобы использовать простой режим';
@override
String get noFavouriteContacts => 'Нет избранных контактов';
@override
String get skip => 'Пропустить';
@@ -3019,4 +3033,415 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get regionScopeCleared => 'Область региона очищена';
@override
String get selectDestinationFirst =>
'Сначала выберите канал, контакт или комнату';
@override
String get recipientNoLongerAvailable => 'Получатель больше не доступен';
@override
String failedToSendMessage(String error) {
return 'Не удалось отправить: $error';
}
@override
String get tictactoeDirectOnly =>
'Крестики-нолики работают только в личных сообщениях. Сначала выберите контакт.';
@override
String get deviceKeyUnavailable => 'Ключ устройства недоступен';
@override
String get imageCompressionFailed => 'Не удалось сжать изображение';
@override
String get imageFragmentationFailed =>
'Не удалось разделить изображение на фрагменты';
@override
String get failedToAnnounceImage => 'Не удалось анонсировать изображение';
@override
String get noRecipientSelected => 'Получатель не выбран';
@override
String get imageSendFailed => 'Не удалось отправить изображение';
@override
String get microphonePermissionRequiredForVoice =>
'Для голосовых сообщений требуется разрешение на микрофон';
@override
String get recordingDiscarded => 'Запись отменена';
@override
String publicChannelMediaWarning(String mediaType) {
return 'Вы собираетесь отправить $mediaType в публичный канал. Это не рекомендуется: получить это может любой в mesh-сети. Выберите приватный или тегированный канал, если только это не то, что вы хотите.';
}
@override
String get selectPrivateChannelToShareLocation =>
'Выберите приватный канал, чтобы поделиться местоположением.';
@override
String get stopSharingMyLocation => 'Прекратить делиться местоположением';
@override
String get shareMyLocation => 'Поделиться местоположением';
@override
String get stopRecording => 'Остановить запись';
@override
String get recordVoice => 'Записать голосовое сообщение';
@override
String get moreActions => 'Другие действия';
@override
String get noMessagesInFilter => 'В этом фильтре нет сообщений';
@override
String get noMatchesInFilter => 'В этом фильтре нет совпадений';
@override
String syncFailed(String error) {
return 'Ошибка синхронизации: $error';
}
@override
String get invalidLink => 'Недействительная ссылка';
@override
String get cannotOpenLink => 'Не удаётся открыть ссылку';
@override
String get failedToOpenLink => 'Не удалось открыть ссылку';
@override
String get failedToResendMessage => 'Не удалось повторно отправить сообщение';
@override
String retryFailed(String error) {
return 'Повторная попытка не удалась: $error';
}
@override
String get deleteDrawingAndMessage => 'Удалить рисунок и сообщение';
@override
String get lastEchoRelay => 'Последнее эхо: ретранслятор';
@override
String get lastEchoPath => 'Последнее эхо: путь';
@override
String get lastEchoBytesReport => 'Последнее эхо: отчёт о байтах';
@override
String get noGpsCoordinatesAvailable => 'Координаты GPS недоступны';
@override
String get notASarMarker => 'Это не маркер SAR';
@override
String failedToSaveTemplate(String error) {
return 'Не удалось сохранить шаблон: $error';
}
@override
String get drawingNotFound => 'Рисунок не найден';
@override
String get unknownDrawingType => 'Неизвестный тип рисунка';
@override
String get customMapMarker => 'Пользовательский маркер карты';
@override
String customMapPointLabel(String point) {
return 'Точка: $point';
}
@override
String mapIdLabel(String id) {
return 'ID карты: $id';
}
@override
String get cannotFetchVoice => 'Не удаётся получить голосовое сообщение';
@override
String get senderContactUnknown =>
'Контакт отправителя неизвестен. Сначала синхронизируйте контакты.';
@override
String get senderRouteUnknown =>
'Маршрут отправителя неизвестен. Сначала синхронизируйте контакты/путь.';
@override
String messageTooFar(String hops, String maxHops) {
return 'Сообщение слишком далеко ($hops хопов, макс. $maxHops).';
}
@override
String get senderRouteNoPathResponse =>
'Маршрут отправителя не ответил на проверку пути. Синхронизируйте контакты/путь и попробуйте снова.';
@override
String get senderRouteNoRawResponse =>
'Маршрут отправителя не ответил на raw-транспортном пути.';
@override
String voiceFetchOverHops(String hops) {
return 'Получение голоса через $hops хопов может занять время.';
}
@override
String get voiceReceiveCanceled => 'Приём голосового сообщения отменён';
@override
String get fetchingMissingVoiceFragments =>
'Получение недостающих фрагментов голоса';
@override
String get receivingVoice => 'Приём голоса';
@override
String get cannotFetchImage => 'Не удаётся получить изображение';
@override
String imageFetchOverHops(String hops) {
return 'Получение изображения через $hops хопов может занять время.';
}
@override
String get imageAlreadyBeingReceived => 'Изображение уже принимается';
@override
String get loadImage => 'Загрузить изображение';
@override
String get imageFetchFailedToSendRequest =>
'Не удалось отправить запрос на изображение';
@override
String get imageUnavailable => 'Изображение сейчас недоступно';
@override
String get imageFetchTimedOut => 'Время ожидания изображения истекло';
@override
String get imageReceiveCanceled => 'Приём изображения отменён';
@override
String get closeImagePreview => 'Закрыть просмотр изображения';
@override
String get recordingReleaseToSend => 'Запись... отпустите, чтобы отправить';
@override
String get sendingVoice => 'Отправка голоса...';
@override
String get sendLongPressToRecordVoice =>
'Отправить (удерживайте для записи голоса)';
@override
String get discardRecording => 'Удалить запись';
@override
String hopCount(num count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count хопа',
many: '$count хопов',
few: '$count хопа',
one: '$count хоп',
);
return '$_temp0';
}
@override
String get channelsSectionDescription =>
'Широковещательные каналы для mesh-трафика поблизости';
@override
String get roomsSectionDescription =>
'Общие пространства для постоянной координации команды';
@override
String get contactsSectionDescription =>
'Люди и устройства в прямой досягаемости';
@override
String get noRecentChatters => 'Нет недавних собеседников';
@override
String get public => 'Публичный';
@override
String get failedToSendTicTacToeMove =>
'Не удалось отправить ход в крестики-нолики';
@override
String get youWon => 'Вы победили';
@override
String get opponentWon => 'Победил соперник';
@override
String get gameDraw => 'Ничья';
@override
String get yourTurn => 'Ваш ход';
@override
String get opponentTurn => 'Ход соперника';
@override
String get pleaseEnterValidCoordinates => 'Введите корректные координаты';
@override
String get locationNotAvailable => 'Местоположение недоступно';
@override
String get customCaveMapPoint => 'Пользовательская точка карты пещеры';
@override
String get mapPoint => 'Точка карты';
@override
String get fetchingMissingImageFragments =>
'Получение недостающих фрагментов…';
@override
String get loadingImage => 'Загрузка…';
@override
String get receivingImage => 'Приём…';
@override
String get tapToLoad => 'Нажмите, чтобы загрузить';
@override
String get connectDevice => 'Подключить устройство';
@override
String get chooseTransportSubtitle =>
'Выберите транспорт: Bluetooth, WiFi или последовательный порт';
@override
String get ble => 'BLE';
@override
String get network => 'Сеть';
@override
String get serial => 'Последовательный порт';
@override
String get scan => 'Сканировать';
@override
String get pressScanToSearchForDevices =>
'Нажмите «Сканировать», чтобы найти устройства поблизости';
@override
String failedToConnectToDevice(String name) {
return 'Не удалось подключиться к $name';
}
@override
String get unknownDevice => 'Неизвестное устройство';
@override
String get showingCachedResultsTapRefresh =>
'Показаны кэшированные результаты. Нажмите «Обновить», чтобы пересканировать.';
@override
String scanningLocalNetworkOnPort(int port) {
return 'Сканирование локальной сети на наличие WiFi-устройств MeshCore на порту $port';
}
@override
String get cancelScanAndAddServer =>
'Отменить сканирование и добавить сервер';
@override
String get addServer => 'Добавить сервер';
@override
String scanningProgressIps(int scanned, String total) {
return 'Сканирование... $scanned/$total IP-адресов';
}
@override
String get cancelAndEnterManually => 'Отменить и ввести вручную';
@override
String get noRecentOrDiscoveredServers =>
'Пока нет недавних или обнаруженных серверов';
@override
String get recentlyUsed => 'Недавно использованные';
@override
String get connecting => 'Подключение...';
@override
String get discoveredOnThisNetwork => 'Обнаружены в этой сети';
@override
String serverNoLongerAvailable(String host, int port) {
return 'Сервер $host:$port больше недоступен. Выполните сканирование снова, чтобы найти активные серверы.';
}
@override
String failedToConnectToHost(String host, int port) {
return 'Не удалось подключиться к $host:$port';
}
@override
String get enterValidIpAddress => 'Введите корректный IP-адрес';
@override
String get enterValidTcpPort => 'Введите корректный TCP-порт';
@override
String get ipAddress => 'IP-адрес';
@override
String get tcpPort => 'TCP-порт';
@override
String get customServerPort => 'Пользовательский порт сервера';
@override
String get chat => 'Чат';
@override
String get restoringPreviousLink => 'Восстановление предыдущего подключения';
@override
String get noDeviceConnected => 'Нет подключённого устройства';
@override
String get sendAdvert => 'Отправить мой контакт';
@override
String get advertMode => 'Отправить мой контакт';
@override
String get advertModeSubtitle =>
'Выберите, как далеко должны распространиться ваши контактные данные.';
@override
String get floodAdvertSent => 'Контакт отправлен по всей mesh-сети';
@override
String get directAdvertSent => 'Контакт отправлен ближайшим устройствам';
}

View File

@@ -2899,6 +2899,20 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get sizeCompressionAndPreview => 'Velikost, stiskanje in predogled';
@override
String get simpleMode => 'Enostavni način';
@override
String get simpleModeDescription =>
'Samo klepet in zemljevid s priljubljenimi stiki';
@override
String get simpleModeRequiresLockedDestination =>
'Za uporabo enostavnega načina najprej omogočite »Zakleni cilj« s kanalom';
@override
String get noFavouriteContacts => 'Ni priljubljenih stikov';
@override
String get skip => 'Preskoči';
@@ -3010,4 +3024,414 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get regionScopeCleared => 'Obseg regije počiščen';
@override
String get selectDestinationFirst => 'Najprej izberite kanal, stik ali sobo';
@override
String get recipientNoLongerAvailable => 'Prejemnik ni več na voljo';
@override
String failedToSendMessage(String error) {
return 'Pošiljanje ni uspelo: $error';
}
@override
String get tictactoeDirectOnly =>
'Križci in krožci delujejo samo v neposrednih sporočilih. Najprej izberite stik.';
@override
String get deviceKeyUnavailable => 'Ključ naprave ni na voljo';
@override
String get imageCompressionFailed => 'Stiskanje slike ni uspelo';
@override
String get imageFragmentationFailed => 'Razdelitev slike ni uspela';
@override
String get failedToAnnounceImage => 'Najava slike ni uspela';
@override
String get noRecipientSelected => 'Prejemnik ni izbran';
@override
String get imageSendFailed => 'Pošiljanje slike ni uspelo';
@override
String get microphonePermissionRequiredForVoice =>
'Za glasovna sporočila je potrebno dovoljenje za mikrofon';
@override
String get recordingDiscarded => 'Posnetek je zavržen';
@override
String publicChannelMediaWarning(String mediaType) {
return 'Pravkar boste poslali $mediaType v javni kanal. To ni priporočljivo, ker lahko vsebino prejmejo vsi v omrežju mesh. Izberite zaseben ali označen kanal, razen če to res želite.';
}
@override
String get selectPrivateChannelToShareLocation =>
'Za deljenje lokacije izberite zaseben kanal.';
@override
String get stopSharingMyLocation => 'Prenehaj deliti mojo lokacijo';
@override
String get shareMyLocation => 'Deli mojo lokacijo';
@override
String get stopRecording => 'Ustavi snemanje';
@override
String get recordVoice => 'Posnemi glasovno sporočilo';
@override
String get moreActions => 'Več dejanj';
@override
String get noMessagesInFilter => 'V tem filtru ni sporočil';
@override
String get noMatchesInFilter => 'V tem filtru ni zadetkov';
@override
String syncFailed(String error) {
return 'Sinhronizacija ni uspela: $error';
}
@override
String get invalidLink => 'Neveljavna povezava';
@override
String get cannotOpenLink => 'Povezave ni mogoče odpreti';
@override
String get failedToOpenLink => 'Odpiranje povezave ni uspelo';
@override
String get failedToResendMessage => 'Ponovno pošiljanje sporočila ni uspelo';
@override
String retryFailed(String error) {
return 'Ponovni poskus ni uspel: $error';
}
@override
String get deleteDrawingAndMessage => 'Izbriši risbo in sporočilo';
@override
String get lastEchoRelay => 'Posrednik zadnjega odmeva';
@override
String get lastEchoPath => 'Pot zadnjega odmeva';
@override
String get lastEchoBytesReport => 'Poročilo o bajtih zadnjega odmeva';
@override
String get noGpsCoordinatesAvailable => 'GPS-koordinate niso na voljo';
@override
String get notASarMarker => 'To ni SAR označevalec';
@override
String failedToSaveTemplate(String error) {
return 'Shranjevanje predloge ni uspelo: $error';
}
@override
String get drawingNotFound => 'Risbe ni mogoče najti';
@override
String get unknownDrawingType => 'Neznana vrsta risbe';
@override
String get customMapMarker => 'Označevalec karte po meri';
@override
String customMapPointLabel(String point) {
return 'Točka: $point';
}
@override
String mapIdLabel(String id) {
return 'ID karte: $id';
}
@override
String get cannotFetchVoice => 'Glasovnega sporočila ni mogoče prenesti';
@override
String get senderContactUnknown =>
'Stik pošiljatelja ni znan. Najprej sinhronizirajte stike.';
@override
String get senderRouteUnknown =>
'Pot pošiljatelja ni znana. Najprej sinhronizirajte stike/poti.';
@override
String messageTooFar(String hops, String maxHops) {
return 'Sporočilo je predaleč ($hops skokov, največ $maxHops).';
}
@override
String get senderRouteNoPathResponse =>
'Pot pošiljatelja se ni odzvala na preverjanje poti. Sinhronizirajte stike/poti in poskusite znova.';
@override
String get senderRouteNoRawResponse =>
'Pot pošiljatelja se ni odzvala na neposredni prenosni poti.';
@override
String voiceFetchOverHops(String hops) {
return 'Prenos glasu prek $hops skokov lahko traja dlje.';
}
@override
String get voiceReceiveCanceled =>
'Sprejem glasovnega sporočila je preklican';
@override
String get fetchingMissingVoiceFragments =>
'Pridobivanje manjkajočih glasovnih delov';
@override
String get receivingVoice => 'Sprejemanje glasu';
@override
String get cannotFetchImage => 'Slike ni mogoče prenesti';
@override
String imageFetchOverHops(String hops) {
return 'Prenos slike prek $hops skokov lahko traja dlje.';
}
@override
String get imageAlreadyBeingReceived => 'Slika se že sprejema';
@override
String get loadImage => 'Naloži sliko';
@override
String get imageFetchFailedToSendRequest =>
'Zahteve za prenos slike ni bilo mogoče poslati';
@override
String get imageUnavailable => 'Slika trenutno ni na voljo';
@override
String get imageFetchTimedOut => 'Prenos slike je potekel';
@override
String get imageReceiveCanceled => 'Sprejem slike je preklican';
@override
String get closeImagePreview => 'Zapri predogled slike';
@override
String get recordingReleaseToSend =>
'Snemanje ... spustite za pošiljanje glasu';
@override
String get sendingVoice => 'Pošiljanje glasu ...';
@override
String get sendLongPressToRecordVoice =>
'Pošlji (dolg pritisk za snemanje glasu)';
@override
String get discardRecording => 'Zavrzi posnetek';
@override
String hopCount(num count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count skokov',
few: '$count skoki',
two: '$count skoka',
one: '$count skok',
);
return '$_temp0';
}
@override
String get channelsSectionDescription =>
'Oddajni kanali za bližnji promet v omrežju mesh';
@override
String get roomsSectionDescription =>
'Skupni prostori za stalno usklajevanje ekipe';
@override
String get contactsSectionDescription =>
'Osebe in naprave, ki jih lahko dosežete neposredno';
@override
String get noRecentChatters => 'Ni nedavnih klepetalcev';
@override
String get public => 'Javno';
@override
String get failedToSendTicTacToeMove =>
'Poteze pri križcih in krožcih ni bilo mogoče poslati';
@override
String get youWon => 'Zmagali ste';
@override
String get opponentWon => 'Zmagal je nasprotnik';
@override
String get gameDraw => 'Neodločeno';
@override
String get yourTurn => 'Vi ste na vrsti';
@override
String get opponentTurn => 'Na vrsti je nasprotnik';
@override
String get pleaseEnterValidCoordinates => 'Vnesite veljavne koordinate';
@override
String get locationNotAvailable => 'Lokacija ni na voljo';
@override
String get customCaveMapPoint => 'Točka jamske karte po meri';
@override
String get mapPoint => 'Točka na karti';
@override
String get fetchingMissingImageFragments =>
'Pridobivanje manjkajočih delov …';
@override
String get loadingImage => 'Nalaganje …';
@override
String get receivingImage => 'Sprejemanje …';
@override
String get tapToLoad => 'Tapnite za nalaganje';
@override
String get connectDevice => 'Poveži napravo';
@override
String get chooseTransportSubtitle =>
'Izberite način povezave: Bluetooth, WiFi ali serijska';
@override
String get ble => 'BLE';
@override
String get network => 'Omrežje';
@override
String get serial => 'Serijsko';
@override
String get scan => 'Poišči';
@override
String get pressScanToSearchForDevices =>
'Pritisnite Poišči za iskanje naprav v bližini';
@override
String failedToConnectToDevice(String name) {
return 'Povezava z napravo $name ni uspela';
}
@override
String get unknownDevice => 'Neznana naprava';
@override
String get showingCachedResultsTapRefresh =>
'Prikazani so predpomnjeni rezultati. Za ponovno iskanje tapnite osveži.';
@override
String scanningLocalNetworkOnPort(int port) {
return 'Iskanje naprav MeshCore WiFi v lokalnem omrežju na vratih $port';
}
@override
String get cancelScanAndAddServer => 'Prekliči iskanje in dodaj strežnik';
@override
String get addServer => 'Dodaj strežnik';
@override
String scanningProgressIps(int scanned, String total) {
return 'Iskanje … $scanned/$total IP naslovov';
}
@override
String get cancelAndEnterManually => 'Prekliči in vnesi ročno';
@override
String get noRecentOrDiscoveredServers =>
'Še ni nedavnih ali odkritih strežnikov';
@override
String get recentlyUsed => 'Nedavno uporabljeno';
@override
String get connecting => 'Povezovanje …';
@override
String get discoveredOnThisNetwork => 'Odkrito v tem omrežju';
@override
String serverNoLongerAvailable(String host, int port) {
return 'Strežnik na $host:$port ni več dosegljiv. Ponovite iskanje, da najdete aktivne strežnike.';
}
@override
String failedToConnectToHost(String host, int port) {
return 'Povezava s strežnikom $host:$port ni uspela';
}
@override
String get enterValidIpAddress => 'Vnesite veljaven IP naslov';
@override
String get enterValidTcpPort => 'Vnesite veljavna vrata TCP';
@override
String get ipAddress => 'IP naslov';
@override
String get tcpPort => 'Vrata TCP';
@override
String get customServerPort => 'Vrata strežnika po meri';
@override
String get chat => 'Klepet';
@override
String get restoringPreviousLink => 'Obnavljanje prejšnje povezave';
@override
String get noDeviceConnected => 'Ni povezane naprave';
@override
String get sendAdvert => 'Pošlji moj kontakt';
@override
String get advertMode => 'Pošlji moj kontakt';
@override
String get advertModeSubtitle =>
'Izberite, kako daleč naj potujejo vaši kontaktni podatki.';
@override
String get floodAdvertSent => 'Kontakt poslan z razprševanjem po omrežju';
@override
String get directAdvertSent => 'Kontakt poslan neposredno bližnjim napravam';
}

View File

@@ -2896,6 +2896,20 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get sizeCompressionAndPreview => 'Boyut, sıkıştırma ve önizleme';
@override
String get simpleMode => 'Basit mod';
@override
String get simpleModeDescription =>
'Yalnızca favori kişilerle sohbet ve harita';
@override
String get simpleModeRequiresLockedDestination =>
'Basit modu kullanmak için önce bir kanalla \"Hedefi kilitle\" seçeneğini etkinleştirin';
@override
String get noFavouriteContacts => 'Favori kişi yok';
@override
String get skip => 'Atla';
@@ -3006,4 +3020,405 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get regionScopeCleared => 'Bölge kapsamı temizlendi';
@override
String get selectDestinationFirst => 'Önce bir kanal, kişi veya oda seçin';
@override
String get recipientNoLongerAvailable => 'Alıcı artık mevcut değil';
@override
String failedToSendMessage(String error) {
return 'Gönderilemedi: $error';
}
@override
String get tictactoeDirectOnly =>
'XOX yalnızca doğrudan mesajlarda çalışır. Önce bir kişi seçin.';
@override
String get deviceKeyUnavailable => 'Cihaz anahtarı mevcut değil';
@override
String get imageCompressionFailed => 'Resim sıkıştırma başarısız';
@override
String get imageFragmentationFailed => 'Resim parçalama başarısız';
@override
String get failedToAnnounceImage => 'Resim duyurulamadı';
@override
String get noRecipientSelected => 'Alıcı seçilmedi';
@override
String get imageSendFailed => 'Resim gönderilemedi';
@override
String get microphonePermissionRequiredForVoice =>
'Sesli mesajlar için mikrofon izni gerekli';
@override
String get recordingDiscarded => 'Kayıt silindi';
@override
String publicChannelMediaWarning(String mediaType) {
return 'Genel kanala $mediaType göndermek üzeresiniz. Mesh ağındaki herkes bunu alabileceği için bu önerilmez. Bunu özellikle istemiyorsanız özel veya etiketli bir kanal seçin.';
}
@override
String get selectPrivateChannelToShareLocation =>
'Konumunuzu paylaşmak için özel bir kanal seçin.';
@override
String get stopSharingMyLocation => 'Konumumu paylaşmayı durdur';
@override
String get shareMyLocation => 'Konumumu paylaş';
@override
String get stopRecording => 'Kaydı durdur';
@override
String get recordVoice => 'Sesli mesaj kaydet';
@override
String get moreActions => 'Diğer işlemler';
@override
String get noMessagesInFilter => 'Bu filtrede mesaj yok';
@override
String get noMatchesInFilter => 'Bu filtrede eşleşme yok';
@override
String syncFailed(String error) {
return 'Senkronizasyon başarısız: $error';
}
@override
String get invalidLink => 'Geçersiz bağlantı';
@override
String get cannotOpenLink => 'Bağlantıılamıyor';
@override
String get failedToOpenLink => 'Bağlantıılamadı';
@override
String get failedToResendMessage => 'Mesaj yeniden gönderilemedi';
@override
String retryFailed(String error) {
return 'Yeniden deneme başarısız: $error';
}
@override
String get deleteDrawingAndMessage => 'Çizimi ve mesajı sil';
@override
String get lastEchoRelay => 'Son yankı: röle';
@override
String get lastEchoPath => 'Son yankı: yol';
@override
String get lastEchoBytesReport => 'Son yankı: bayt raporu';
@override
String get noGpsCoordinatesAvailable => 'Kullanılabilir GPS koordinatı yok';
@override
String get notASarMarker => 'Bu bir SAR işareti değil';
@override
String failedToSaveTemplate(String error) {
return 'Şablon kaydedilemedi: $error';
}
@override
String get drawingNotFound => 'Çizim bulunamadı';
@override
String get unknownDrawingType => 'Bilinmeyen çizim türü';
@override
String get customMapMarker => 'Özel harita işareti';
@override
String customMapPointLabel(String point) {
return 'Nokta: $point';
}
@override
String mapIdLabel(String id) {
return 'Harita ID: $id';
}
@override
String get cannotFetchVoice => 'Ses alınamıyor';
@override
String get senderContactUnknown =>
'Gönderenin kişisi bilinmiyor. Önce kişileri senkronize edin.';
@override
String get senderRouteUnknown =>
'Gönderenin rotası bilinmiyor. Önce kişileri/yolu senkronize edin.';
@override
String messageTooFar(String hops, String maxHops) {
return 'Mesaj çok uzakta ($hops atlama, en fazla $maxHops).';
}
@override
String get senderRouteNoPathResponse =>
'Gönderenin rotası yol denetimine yanıt vermedi. Kişileri/yolu senkronize edip yeniden deneyin.';
@override
String get senderRouteNoRawResponse =>
'Gönderenin rotası ham aktarım yolunda yanıt vermedi.';
@override
String voiceFetchOverHops(String hops) {
return '$hops atlama üzerinden ses alma biraz sürebilir.';
}
@override
String get voiceReceiveCanceled => 'Ses alımı iptal edildi';
@override
String get fetchingMissingVoiceFragments => 'Eksik ses parçaları alınıyor';
@override
String get receivingVoice => 'Ses alınıyor';
@override
String get cannotFetchImage => 'Resim alınamıyor';
@override
String imageFetchOverHops(String hops) {
return '$hops atlama üzerinden resim alma biraz sürebilir.';
}
@override
String get imageAlreadyBeingReceived => 'Resim zaten alınıyor';
@override
String get loadImage => 'Resmi yükle';
@override
String get imageFetchFailedToSendRequest => 'Resim isteği gönderilemedi';
@override
String get imageUnavailable => 'Resim şu anda kullanılamıyor';
@override
String get imageFetchTimedOut => 'Resim alma zaman aşımına uğradı';
@override
String get imageReceiveCanceled => 'Resim alımı iptal edildi';
@override
String get closeImagePreview => 'Resim önizlemesini kapat';
@override
String get recordingReleaseToSend => 'Kaydediliyor... göndermek için bırakın';
@override
String get sendingVoice => 'Ses gönderiliyor...';
@override
String get sendLongPressToRecordVoice =>
'Gönder (ses kaydı için basılı tutun)';
@override
String get discardRecording => 'Kaydı sil';
@override
String hopCount(num count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count atlama',
);
return '$_temp0';
}
@override
String get channelsSectionDescription =>
'Yakındaki mesh trafiği için yayın kanalları';
@override
String get roomsSectionDescription =>
'Sürekli ekip koordinasyonu için ortak alanlar';
@override
String get contactsSectionDescription =>
'Doğrudan ulaşabileceğiniz kişiler ve cihazlar';
@override
String get noRecentChatters => 'Yakın zamanda konuşan yok';
@override
String get public => 'Genel';
@override
String get failedToSendTicTacToeMove => 'XOX hamlesi gönderilemedi';
@override
String get youWon => 'Kazandınız';
@override
String get opponentWon => 'Rakip kazandı';
@override
String get gameDraw => 'Berabere';
@override
String get yourTurn => 'Sıra sizde';
@override
String get opponentTurn => 'Sıra rakipte';
@override
String get pleaseEnterValidCoordinates => 'Geçerli koordinatlar girin';
@override
String get locationNotAvailable => 'Konum mevcut değil';
@override
String get customCaveMapPoint => 'Özel mağara haritası noktası';
@override
String get mapPoint => 'Harita noktası';
@override
String get fetchingMissingImageFragments => 'Eksik parçalar alınıyor…';
@override
String get loadingImage => 'Yükleniyor…';
@override
String get receivingImage => 'Alınıyor…';
@override
String get tapToLoad => 'Yüklemek için dokunun';
@override
String get connectDevice => 'Cihaza bağlan';
@override
String get chooseTransportSubtitle =>
'Bluetooth, WiFi veya Seri aktarım seçin';
@override
String get ble => 'BLE';
@override
String get network => '';
@override
String get serial => 'Seri';
@override
String get scan => 'Tara';
@override
String get pressScanToSearchForDevices =>
'Yakındaki cihazları aramak için tara düğmesine basın';
@override
String failedToConnectToDevice(String name) {
return '$name cihazına bağlanılamadı';
}
@override
String get unknownDevice => 'Bilinmeyen cihaz';
@override
String get showingCachedResultsTapRefresh =>
'Önbelleğe alınmış sonuçlar gösteriliyor. Yeniden taramak için yenile düğmesine dokunun.';
@override
String scanningLocalNetworkOnPort(int port) {
return 'Yerel ağda $port bağlantı noktasındaki MeshCore WiFi cihazları taranıyor';
}
@override
String get cancelScanAndAddServer => 'Taramayı iptal et ve sunucu ekle';
@override
String get addServer => 'Sunucu ekle';
@override
String scanningProgressIps(int scanned, String total) {
return 'Taranıyor... $scanned/$total IP';
}
@override
String get cancelAndEnterManually => 'İptal et ve elle gir';
@override
String get noRecentOrDiscoveredServers =>
'Henüz son kullanılan veya keşfedilen sunucu yok';
@override
String get recentlyUsed => 'Son kullanılanlar';
@override
String get connecting => 'Bağlanıyor...';
@override
String get discoveredOnThisNetwork => 'Bu ağda keşfedildi';
@override
String serverNoLongerAvailable(String host, int port) {
return '$host:$port adresindeki sunucu artık kullanılamıyor. Etkin sunucuları bulmak için lütfen yeniden tarayın.';
}
@override
String failedToConnectToHost(String host, int port) {
return '$host:$port adresine bağlanılamadı';
}
@override
String get enterValidIpAddress => 'Geçerli bir IP adresi girin';
@override
String get enterValidTcpPort => 'Geçerli bir TCP bağlantı noktası girin';
@override
String get ipAddress => 'IP adresi';
@override
String get tcpPort => 'TCP bağlantı noktası';
@override
String get customServerPort => 'Özel sunucu bağlantı noktası';
@override
String get chat => 'Sohbet';
@override
String get restoringPreviousLink => 'Önceki bağlantı geri yükleniyor';
@override
String get noDeviceConnected => 'Bağlı cihaz yok';
@override
String get sendAdvert => 'Kişi bilgimi gönder';
@override
String get advertMode => 'Kişi bilgimi gönder';
@override
String get advertModeSubtitle =>
'Kişi bilgilerinizin ne kadar uzağa gideceğini seçin.';
@override
String get floodAdvertSent => 'Kişi bilgisi tüm mesh ağına gönderildi';
@override
String get directAdvertSent => 'Kişi bilgisi yakındaki cihazlara gönderildi';
}

View File

@@ -2918,6 +2918,19 @@ class AppLocalizationsUk extends AppLocalizations {
String get sizeCompressionAndPreview =>
'Розмір, стиснення та попередній перегляд';
@override
String get simpleMode => 'Простий режим';
@override
String get simpleModeDescription => 'Лише чат і карта з обраними контактами';
@override
String get simpleModeRequiresLockedDestination =>
'Спочатку увімкніть «Зафіксувати призначення» з каналом, щоб використовувати простий режим';
@override
String get noFavouriteContacts => 'Немає обраних контактів';
@override
String get skip => 'Пропустити';
@@ -3029,4 +3042,415 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get regionScopeCleared => 'Область регіону очищена';
@override
String get selectDestinationFirst =>
'Спершу виберіть канал, контакт або кімнату';
@override
String get recipientNoLongerAvailable => 'Одержувач більше недоступний';
@override
String failedToSendMessage(String error) {
return 'Не вдалося надіслати: $error';
}
@override
String get tictactoeDirectOnly =>
'Хрестики-нулики працюють лише в особистих повідомленнях. Спершу виберіть контакт.';
@override
String get deviceKeyUnavailable => 'Ключ пристрою недоступний';
@override
String get imageCompressionFailed => 'Не вдалося стиснути зображення';
@override
String get imageFragmentationFailed =>
'Не вдалося розділити зображення на фрагменти';
@override
String get failedToAnnounceImage => 'Не вдалося анонсувати зображення';
@override
String get noRecipientSelected => 'Одержувача не вибрано';
@override
String get imageSendFailed => 'Не вдалося надіслати зображення';
@override
String get microphonePermissionRequiredForVoice =>
'Для голосових повідомлень потрібен дозвіл на мікрофон';
@override
String get recordingDiscarded => 'Запис відхилено';
@override
String publicChannelMediaWarning(String mediaType) {
return 'Ви збираєтеся надіслати $mediaType в публічний канал. Це не рекомендується: отримати це може будь-хто в mesh-мережі. Виберіть приватний або тегований канал, якщо тільки це не те, що ви хочете.';
}
@override
String get selectPrivateChannelToShareLocation =>
'Виберіть приватний канал, щоб поділитися місцезнаходженням.';
@override
String get stopSharingMyLocation => 'Припинити ділитися місцезнаходженням';
@override
String get shareMyLocation => 'Поділитися місцезнаходженням';
@override
String get stopRecording => 'Зупинити запис';
@override
String get recordVoice => 'Записати голосове повідомлення';
@override
String get moreActions => 'Інші дії';
@override
String get noMessagesInFilter => 'У цьому фільтрі немає повідомлень';
@override
String get noMatchesInFilter => 'У цьому фільтрі немає збігів';
@override
String syncFailed(String error) {
return 'Помилка синхронізації: $error';
}
@override
String get invalidLink => 'Недійсне посилання';
@override
String get cannotOpenLink => 'Не вдається відкрити посилання';
@override
String get failedToOpenLink => 'Не вдалося відкрити посилання';
@override
String get failedToResendMessage =>
'Не вдалося повторно надіслати повідомлення';
@override
String retryFailed(String error) {
return 'Повторна спроба не вдалася: $error';
}
@override
String get deleteDrawingAndMessage => 'Видалити рисунок і повідомлення';
@override
String get lastEchoRelay => 'Останнє відлуння: ретранслятор';
@override
String get lastEchoPath => 'Останнє відлуння: шлях';
@override
String get lastEchoBytesReport => 'Останнє відлуння: звіт про байти';
@override
String get noGpsCoordinatesAvailable => 'Координати GPS недоступні';
@override
String get notASarMarker => 'Це не маркер SAR';
@override
String failedToSaveTemplate(String error) {
return 'Не вдалося зберегти шаблон: $error';
}
@override
String get drawingNotFound => 'Рисунок не знайдено';
@override
String get unknownDrawingType => 'Невідомий тип рисунка';
@override
String get customMapMarker => 'Власний маркер карти';
@override
String customMapPointLabel(String point) {
return 'Точка: $point';
}
@override
String mapIdLabel(String id) {
return 'ID карти: $id';
}
@override
String get cannotFetchVoice => 'Не вдається отримати голосове повідомлення';
@override
String get senderContactUnknown =>
'Контакт відправника невідомий. Спершу синхронізуйте контакти.';
@override
String get senderRouteUnknown =>
'Маршрут відправника невідомий. Спершу синхронізуйте контакти/шлях.';
@override
String messageTooFar(String hops, String maxHops) {
return 'Повідомлення задалеко ($hops хопів, макс. $maxHops).';
}
@override
String get senderRouteNoPathResponse =>
'Маршрут відправника не відповів на перевірку шляху. Синхронізуйте контакти/шлях і спробуйте ще раз.';
@override
String get senderRouteNoRawResponse =>
'Маршрут відправника не відповів на raw-транспортному шляху.';
@override
String voiceFetchOverHops(String hops) {
return 'Отримання голосу через $hops хопів може зайняти час.';
}
@override
String get voiceReceiveCanceled =>
'Приймання голосового повідомлення скасовано';
@override
String get fetchingMissingVoiceFragments =>
'Отримання відсутніх фрагментів голосу';
@override
String get receivingVoice => 'Приймання голосу';
@override
String get cannotFetchImage => 'Не вдається отримати зображення';
@override
String imageFetchOverHops(String hops) {
return 'Отримання зображення через $hops хопів може зайняти час.';
}
@override
String get imageAlreadyBeingReceived => 'Зображення вже приймається';
@override
String get loadImage => 'Завантажити зображення';
@override
String get imageFetchFailedToSendRequest =>
'Не вдалося надіслати запит на зображення';
@override
String get imageUnavailable => 'Зображення зараз недоступне';
@override
String get imageFetchTimedOut => 'Час очікування зображення минув';
@override
String get imageReceiveCanceled => 'Приймання зображення скасовано';
@override
String get closeImagePreview => 'Закрити перегляд зображення';
@override
String get recordingReleaseToSend => 'Запис... відпустіть, щоб надіслати';
@override
String get sendingVoice => 'Надсилання голосу...';
@override
String get sendLongPressToRecordVoice =>
'Надіслати (утримуйте для запису голосу)';
@override
String get discardRecording => 'Відхилити запис';
@override
String hopCount(num count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count хопа',
many: '$count хопів',
few: '$count хопи',
one: '$count хоп',
);
return '$_temp0';
}
@override
String get channelsSectionDescription =>
'Широкомовні канали для mesh-трафіку поблизу';
@override
String get roomsSectionDescription =>
'Спільні простори для постійної координації команди';
@override
String get contactsSectionDescription =>
'Люди та пристрої в прямій досяжності';
@override
String get noRecentChatters => 'Немає нещодавніх співрозмовників';
@override
String get public => 'Публічний';
@override
String get failedToSendTicTacToeMove =>
'Не вдалося надіслати хід у хрестики-нулики';
@override
String get youWon => 'Ви перемогли';
@override
String get opponentWon => 'Переміг суперник';
@override
String get gameDraw => 'Нічия';
@override
String get yourTurn => 'Ваш хід';
@override
String get opponentTurn => 'Хід суперника';
@override
String get pleaseEnterValidCoordinates => 'Введіть коректні координати';
@override
String get locationNotAvailable => 'Місцезнаходження недоступне';
@override
String get customCaveMapPoint => 'Власна точка карти печери';
@override
String get mapPoint => 'Точка карти';
@override
String get fetchingMissingImageFragments => 'Отримання відсутніх фрагментів…';
@override
String get loadingImage => 'Завантаження…';
@override
String get receivingImage => 'Приймання…';
@override
String get tapToLoad => 'Торкніться, щоб завантажити';
@override
String get connectDevice => 'Підключити пристрій';
@override
String get chooseTransportSubtitle =>
'Виберіть транспорт: Bluetooth, WiFi або послідовний порт';
@override
String get ble => 'BLE';
@override
String get network => 'Мережа';
@override
String get serial => 'Послідовний порт';
@override
String get scan => 'Сканувати';
@override
String get pressScanToSearchForDevices =>
'Натисніть «Сканувати», щоб знайти пристрої поблизу';
@override
String failedToConnectToDevice(String name) {
return 'Не вдалося підключитися до $name';
}
@override
String get unknownDevice => 'Невідомий пристрій';
@override
String get showingCachedResultsTapRefresh =>
'Показано кешовані результати. Натисніть «Оновити», щоб пересканувати.';
@override
String scanningLocalNetworkOnPort(int port) {
return 'Сканування локальної мережі на наявність WiFi-пристроїв MeshCore на порту $port';
}
@override
String get cancelScanAndAddServer => 'Скасувати сканування та додати сервер';
@override
String get addServer => 'Додати сервер';
@override
String scanningProgressIps(int scanned, String total) {
return 'Сканування... $scanned/$total IP-адрес';
}
@override
String get cancelAndEnterManually => 'Скасувати та ввести вручну';
@override
String get noRecentOrDiscoveredServers =>
'Поки немає нещодавніх або виявлених серверів';
@override
String get recentlyUsed => 'Нещодавно використані';
@override
String get connecting => 'Підключення...';
@override
String get discoveredOnThisNetwork => 'Виявлені в цій мережі';
@override
String serverNoLongerAvailable(String host, int port) {
return 'Сервер $host:$port більше недоступний. Виконайте сканування ще раз, щоб знайти активні сервери.';
}
@override
String failedToConnectToHost(String host, int port) {
return 'Не вдалося підключитися до $host:$port';
}
@override
String get enterValidIpAddress => 'Введіть коректну IP-адресу';
@override
String get enterValidTcpPort => 'Введіть коректний TCP-порт';
@override
String get ipAddress => 'IP-адреса';
@override
String get tcpPort => 'TCP-порт';
@override
String get customServerPort => 'Власний порт сервера';
@override
String get chat => 'Чат';
@override
String get restoringPreviousLink => 'Відновлення попереднього підключення';
@override
String get noDeviceConnected => 'Немає підключеного пристрою';
@override
String get sendAdvert => 'Надіслати мій контакт';
@override
String get advertMode => 'Надіслати мій контакт';
@override
String get advertModeSubtitle =>
'Виберіть, як далеко мають поширитися ваші контактні дані.';
@override
String get floodAdvertSent => 'Контакт надіслано по всій mesh-мережі';
@override
String get directAdvertSent => 'Контакт надіслано пристроям поблизу';
}

View File

@@ -2796,6 +2796,19 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get sizeCompressionAndPreview => '大小、压缩和预览';
@override
String get simpleMode => '简单模式';
@override
String get simpleModeDescription => '仅与收藏联系人聊天和查看地图';
@override
String get simpleModeRequiresLockedDestination =>
'请先启用“锁定目的地”并选择一个频道,再使用简单模式';
@override
String get noFavouriteContacts => '暂无收藏联系人';
@override
String get skip => '跳过';
@@ -2902,4 +2915,389 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get regionScopeCleared => '区域范围已清除';
@override
String get selectDestinationFirst => '请先选择频道、联系人或房间';
@override
String get recipientNoLongerAvailable => '收件人已不可用';
@override
String failedToSendMessage(String error) {
return '发送失败:$error';
}
@override
String get tictactoeDirectOnly => '井字棋仅适用于私聊消息。请先选择联系人。';
@override
String get deviceKeyUnavailable => '设备密钥不可用';
@override
String get imageCompressionFailed => '图片压缩失败';
@override
String get imageFragmentationFailed => '图片分片失败';
@override
String get failedToAnnounceImage => '图片通告失败';
@override
String get noRecipientSelected => '未选择收件人';
@override
String get imageSendFailed => '图片发送失败';
@override
String get microphonePermissionRequiredForVoice => '语音消息需要麦克风权限';
@override
String get recordingDiscarded => '已丢弃录音';
@override
String publicChannelMediaWarning(String mediaType) {
return '您即将向公共频道发送$mediaType。不建议这样做,因为 mesh 网络中的所有人都可能收到。除非确有需要,请选择私有或带标签的频道。';
}
@override
String get selectPrivateChannelToShareLocation => '请选择私有频道来分享您的位置。';
@override
String get stopSharingMyLocation => '停止分享我的位置';
@override
String get shareMyLocation => '分享我的位置';
@override
String get stopRecording => '停止录音';
@override
String get recordVoice => '录制语音';
@override
String get moreActions => '更多操作';
@override
String get noMessagesInFilter => '此筛选条件下没有消息';
@override
String get noMatchesInFilter => '此筛选条件下没有匹配项';
@override
String syncFailed(String error) {
return '同步失败:$error';
}
@override
String get invalidLink => '无效链接';
@override
String get cannotOpenLink => '无法打开链接';
@override
String get failedToOpenLink => '打开链接失败';
@override
String get failedToResendMessage => '重新发送消息失败';
@override
String retryFailed(String error) {
return '重试失败:$error';
}
@override
String get deleteDrawingAndMessage => '删除绘图和消息';
@override
String get lastEchoRelay => '最后回声:中继';
@override
String get lastEchoPath => '最后回声:路径';
@override
String get lastEchoBytesReport => '最后回声:字节报告';
@override
String get noGpsCoordinatesAvailable => '没有可用的 GPS 坐标';
@override
String get notASarMarker => '不是搜救标记';
@override
String failedToSaveTemplate(String error) {
return '保存模板失败:$error';
}
@override
String get drawingNotFound => '未找到绘图';
@override
String get unknownDrawingType => '未知绘图类型';
@override
String get customMapMarker => '自定义地图标记';
@override
String customMapPointLabel(String point) {
return '坐标点:$point';
}
@override
String mapIdLabel(String id) {
return '地图 ID$id';
}
@override
String get cannotFetchVoice => '无法获取语音';
@override
String get senderContactUnknown => '发送者联系人未知。请先同步联系人。';
@override
String get senderRouteUnknown => '发送者路由未知。请先同步联系人/路径。';
@override
String messageTooFar(String hops, String maxHops) {
return '消息距离过远($hops 跳,最多 $maxHops 跳)。';
}
@override
String get senderRouteNoPathResponse => '发送者路由未响应路径检测。请同步联系人/路径后重试。';
@override
String get senderRouteNoRawResponse => '发送者路由在原始传输路径上未响应。';
@override
String voiceFetchOverHops(String hops) {
return '通过 $hops 跳获取语音可能需要一些时间。';
}
@override
String get voiceReceiveCanceled => '已取消接收语音';
@override
String get fetchingMissingVoiceFragments => '正在获取缺失的语音分片';
@override
String get receivingVoice => '正在接收语音';
@override
String get cannotFetchImage => '无法获取图片';
@override
String imageFetchOverHops(String hops) {
return '通过 $hops 跳获取图片可能需要一些时间。';
}
@override
String get imageAlreadyBeingReceived => '图片正在接收中';
@override
String get loadImage => '加载图片';
@override
String get imageFetchFailedToSendRequest => '图片获取请求发送失败';
@override
String get imageUnavailable => '图片暂时不可用';
@override
String get imageFetchTimedOut => '图片获取超时';
@override
String get imageReceiveCanceled => '已取消接收图片';
@override
String get closeImagePreview => '关闭图片预览';
@override
String get recordingReleaseToSend => '正在录音……松开发送';
@override
String get sendingVoice => '正在发送语音……';
@override
String get sendLongPressToRecordVoice => '发送(长按录制语音)';
@override
String get discardRecording => '丢弃录音';
@override
String hopCount(num count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count',
);
return '$_temp0';
}
@override
String get channelsSectionDescription => '用于附近 mesh 通信的广播频道';
@override
String get roomsSectionDescription => '用于团队持续协调的共享空间';
@override
String get contactsSectionDescription => '可直接联系的人员和设备';
@override
String get noRecentChatters => '最近没有人发言';
@override
String get public => '公共';
@override
String get failedToSendTicTacToeMove => '井字棋走子发送失败';
@override
String get youWon => '您赢了';
@override
String get opponentWon => '对手赢了';
@override
String get gameDraw => '平局';
@override
String get yourTurn => '轮到您';
@override
String get opponentTurn => '轮到对手';
@override
String get pleaseEnterValidCoordinates => '请输入有效坐标';
@override
String get locationNotAvailable => '位置不可用';
@override
String get customCaveMapPoint => '自定义洞穴地图点';
@override
String get mapPoint => '地图点';
@override
String get fetchingMissingImageFragments => '正在获取缺失分片……';
@override
String get loadingImage => '加载中……';
@override
String get receivingImage => '接收中……';
@override
String get tapToLoad => '点按加载';
@override
String get connectDevice => '连接设备';
@override
String get chooseTransportSubtitle => '选择 Bluetooth、WiFi 或串口传输';
@override
String get ble => 'BLE';
@override
String get network => '网络';
@override
String get serial => '串口';
@override
String get scan => '扫描';
@override
String get pressScanToSearchForDevices => '点按扫描以搜索附近设备';
@override
String failedToConnectToDevice(String name) {
return '连接 $name 失败';
}
@override
String get unknownDevice => '未知设备';
@override
String get showingCachedResultsTapRefresh => '正在显示缓存结果。点按刷新以重新扫描。';
@override
String scanningLocalNetworkOnPort(int port) {
return '正在扫描本地网络中端口 $port 上的 MeshCore WiFi 设备';
}
@override
String get cancelScanAndAddServer => '取消扫描并添加服务器';
@override
String get addServer => '添加服务器';
@override
String scanningProgressIps(int scanned, String total) {
return '正在扫描……$scanned/$total 个 IP';
}
@override
String get cancelAndEnterManually => '取消并手动输入';
@override
String get noRecentOrDiscoveredServers => '暂无最近使用或发现的服务器';
@override
String get recentlyUsed => '最近使用';
@override
String get connecting => '正在连接……';
@override
String get discoveredOnThisNetwork => '在此网络中发现';
@override
String serverNoLongerAvailable(String host, int port) {
return '$host:$port 上的服务器已不可用。请重新扫描以查找活跃的服务器。';
}
@override
String failedToConnectToHost(String host, int port) {
return '连接 $host:$port 失败';
}
@override
String get enterValidIpAddress => '请输入有效的 IP 地址';
@override
String get enterValidTcpPort => '请输入有效的 TCP 端口';
@override
String get ipAddress => 'IP 地址';
@override
String get tcpPort => 'TCP 端口';
@override
String get customServerPort => '自定义服务器端口';
@override
String get chat => '聊天';
@override
String get restoringPreviousLink => '正在恢复先前的连接';
@override
String get noDeviceConnected => '未连接设备';
@override
String get sendAdvert => '发送我的联系人信息';
@override
String get advertMode => '发送我的联系人信息';
@override
String get advertModeSubtitle => '选择您的联系人信息应传播多远。';
@override
String get floodAdvertSent => '联系人信息已发送至整个 mesh 网络';
@override
String get directAdvertSent => '联系人信息已发送至附近设备';
}

View File

@@ -2493,5 +2493,125 @@
"enterRegionName": "Nazwa regionu (np. auckland)",
"noRegionsFound": "Nie znaleziono regionów na tym przekaźniku.",
"regionScopeSet": "Zakres regionu ustawiony na {name}",
"regionScopeCleared": "Zakres regionu wyczyszczony"
"regionScopeCleared": "Zakres regionu wyczyszczony",
"simpleMode": "Tryb prosty",
"simpleModeDescription": "Tylko czat i mapa z ulubionymi kontaktami",
"simpleModeRequiresLockedDestination": "Aby użyć trybu prostego, najpierw włącz „Zablokuj cel” z kanałem",
"noFavouriteContacts": "Brak ulubionych kontaktów",
"selectDestinationFirst": "Najpierw wybierz kanał, kontakt lub pokój",
"recipientNoLongerAvailable": "Odbiorca nie jest już dostępny",
"failedToSendMessage": "Nie udało się wysłać: {error}",
"tictactoeDirectOnly": "Kółko i krzyżyk działa tylko w wiadomościach bezpośrednich. Najpierw wybierz kontakt.",
"deviceKeyUnavailable": "Klucz urządzenia jest niedostępny",
"imageCompressionFailed": "Kompresja obrazu nie powiodła się",
"imageFragmentationFailed": "Fragmentacja obrazu nie powiodła się",
"failedToAnnounceImage": "Nie udało się ogłosić obrazu",
"noRecipientSelected": "Nie wybrano odbiorcy",
"imageSendFailed": "Wysyłanie obrazu nie powiodło się",
"microphonePermissionRequiredForVoice": "Wiadomości głosowe wymagają uprawnienia do mikrofonu",
"recordingDiscarded": "Nagranie odrzucone",
"publicChannelMediaWarning": "Za chwilę wyślesz {mediaType} na kanał publiczny. Nie jest to zalecane, ponieważ może to odebrać każdy w sieci mesh. Wybierz kanał prywatny lub oznaczony, chyba że właśnie tego chcesz.",
"selectPrivateChannelToShareLocation": "Wybierz prywatny kanał, aby udostępnić swoją lokalizację.",
"stopSharingMyLocation": "Przestań udostępniać moją lokalizację",
"shareMyLocation": "Udostępnij moją lokalizację",
"stopRecording": "Zatrzymaj nagrywanie",
"recordVoice": "Nagraj wiadomość głosową",
"moreActions": "Więcej działań",
"noMessagesInFilter": "Brak wiadomości w tym filtrze",
"noMatchesInFilter": "Brak dopasowań w tym filtrze",
"syncFailed": "Synchronizacja nie powiodła się: {error}",
"invalidLink": "Nieprawidłowy link",
"cannotOpenLink": "Nie można otworzyć linku",
"failedToOpenLink": "Nie udało się otworzyć linku",
"failedToResendMessage": "Nie udało się ponownie wysłać wiadomości",
"retryFailed": "Ponowna próba nie powiodła się: {error}",
"deleteDrawingAndMessage": "Usuń rysunek i wiadomość",
"lastEchoRelay": "Ostatnie echo przekaźnik",
"lastEchoPath": "Ostatnie echo ścieżka",
"lastEchoBytesReport": "Ostatnie echo raport bajtów",
"noGpsCoordinatesAvailable": "Brak dostępnych współrzędnych GPS",
"notASarMarker": "To nie jest znacznik SAR",
"failedToSaveTemplate": "Nie udało się zapisać szablonu: {error}",
"drawingNotFound": "Nie znaleziono rysunku",
"unknownDrawingType": "Nieznany typ rysunku",
"customMapMarker": "Niestandardowy znacznik mapy",
"customMapPointLabel": "Punkt: {point}",
"mapIdLabel": "ID mapy: {id}",
"cannotFetchVoice": "Nie można pobrać wiadomości głosowej",
"senderContactUnknown": "Kontakt nadawcy jest nieznany. Najpierw zsynchronizuj kontakty.",
"senderRouteUnknown": "Trasa nadawcy jest nieznana. Najpierw zsynchronizuj kontakty/ścieżkę.",
"messageTooFar": "Wiadomość jest zbyt daleko ({hops} skoków, maks. {maxHops}).",
"senderRouteNoPathResponse": "Trasa nadawcy nie odpowiedziała na sprawdzenie ścieżki. Zsynchronizuj kontakty/ścieżkę i spróbuj ponownie.",
"senderRouteNoRawResponse": "Trasa nadawcy nie odpowiedziała na surowej ścieżce transportowej.",
"voiceFetchOverHops": "Pobieranie głosu przez {hops} skoków może chwilę potrwać.",
"voiceReceiveCanceled": "Odbieranie głosu anulowane",
"fetchingMissingVoiceFragments": "Pobieranie brakujących fragmentów głosu",
"receivingVoice": "Odbieranie głosu",
"cannotFetchImage": "Nie można pobrać obrazu",
"imageFetchOverHops": "Pobieranie obrazu przez {hops} skoków może chwilę potrwać.",
"imageAlreadyBeingReceived": "Obraz jest już odbierany",
"loadImage": "Wczytaj obraz",
"imageFetchFailedToSendRequest": "Nie udało się wysłać żądania pobrania obrazu",
"imageUnavailable": "Obraz chwilowo niedostępny",
"imageFetchTimedOut": "Upłynął limit czasu pobierania obrazu",
"imageReceiveCanceled": "Odbieranie obrazu anulowane",
"closeImagePreview": "Zamknij podgląd obrazu",
"recordingReleaseToSend": "Nagrywanie... puść, aby wysłać",
"sendingVoice": "Wysyłanie głosu...",
"sendLongPressToRecordVoice": "Wyślij (przytrzymaj, aby nagrać głos)",
"discardRecording": "Odrzuć nagranie",
"hopCount": "{count, plural, one{{count} skok} few{{count} skoki} many{{count} skoków} other{{count} skoku}}",
"channelsSectionDescription": "Kanały rozgłoszeniowe dla pobliskiego ruchu mesh",
"roomsSectionDescription": "Wspólne przestrzenie do bieżącej koordynacji zespołu",
"contactsSectionDescription": "Osoby i urządzenia w bezpośrednim zasięgu",
"noRecentChatters": "Brak ostatnich rozmówców",
"public": "Publiczny",
"failedToSendTicTacToeMove": "Nie udało się wysłać ruchu w kółko i krzyżyk",
"youWon": "Wygrałeś",
"opponentWon": "Wygrał przeciwnik",
"gameDraw": "Remis",
"yourTurn": "Twoja kolej",
"opponentTurn": "Kolej przeciwnika",
"pleaseEnterValidCoordinates": "Wprowadź prawidłowe współrzędne",
"locationNotAvailable": "Lokalizacja niedostępna",
"customCaveMapPoint": "Niestandardowy punkt mapy jaskini",
"mapPoint": "Punkt mapy",
"fetchingMissingImageFragments": "Pobieranie brakujących fragmentów…",
"loadingImage": "Wczytywanie…",
"receivingImage": "Odbieranie…",
"tapToLoad": "Dotknij, aby wczytać",
"connectDevice": "Połącz urządzenie",
"chooseTransportSubtitle": "Wybierz transport: Bluetooth, WiFi lub port szeregowy",
"ble": "BLE",
"network": "Sieć",
"serial": "Port szeregowy",
"scan": "Skanuj",
"pressScanToSearchForDevices": "Naciśnij Skanuj, aby wyszukać pobliskie urządzenia",
"failedToConnectToDevice": "Nie udało się połączyć z {name}",
"unknownDevice": "Nieznane urządzenie",
"showingCachedResultsTapRefresh": "Wyświetlane są wyniki z pamięci podręcznej. Dotknij odświeżania, aby zeskanować ponownie.",
"scanningLocalNetworkOnPort": "Skanowanie sieci lokalnej w poszukiwaniu urządzeń MeshCore WiFi na porcie {port}",
"cancelScanAndAddServer": "Anuluj skanowanie i dodaj serwer",
"addServer": "Dodaj serwer",
"scanningProgressIps": "Skanowanie... {scanned}/{total} adresów IP",
"cancelAndEnterManually": "Anuluj i wprowadź ręcznie",
"noRecentOrDiscoveredServers": "Brak ostatnich ani wykrytych serwerów",
"recentlyUsed": "Ostatnio używane",
"connecting": "Łączenie...",
"discoveredOnThisNetwork": "Wykryte w tej sieci",
"serverNoLongerAvailable": "Serwer {host}:{port} nie jest już dostępny. Zeskanuj ponownie, aby znaleźć aktywne serwery.",
"failedToConnectToHost": "Nie udało się połączyć z {host}:{port}",
"enterValidIpAddress": "Wprowadź prawidłowy adres IP",
"enterValidTcpPort": "Wprowadź prawidłowy port TCP",
"ipAddress": "Adres IP",
"tcpPort": "Port TCP",
"customServerPort": "Niestandardowy port serwera",
"chat": "Czat",
"restoringPreviousLink": "Przywracanie poprzedniego połączenia",
"noDeviceConnected": "Brak połączonego urządzenia",
"sendAdvert": "Wyślij mój kontakt",
"advertMode": "Wyślij mój kontakt",
"advertModeSubtitle": "Wybierz, jak daleko mają dotrzeć Twoje dane kontaktowe.",
"floodAdvertSent": "Kontakt wysłano do całej sieci mesh",
"directAdvertSent": "Kontakt wysłano do urządzeń w pobliżu"
}

View File

@@ -2493,5 +2493,125 @@
"enterRegionName": "Nome da região (ex. auckland)",
"noRegionsFound": "Nenhuma região encontrada neste repetidor.",
"regionScopeSet": "Âmbito de região definido para {name}",
"regionScopeCleared": "Âmbito de região limpo"
"regionScopeCleared": "Âmbito de região limpo",
"simpleMode": "Modo simples",
"simpleModeDescription": "Apenas chat e mapa com contatos favoritos",
"simpleModeRequiresLockedDestination": "Ative primeiro \"Bloquear destino\" com um canal para usar o modo simples",
"noFavouriteContacts": "Nenhum contato favorito",
"selectDestinationFirst": "Selecione primeiro um canal, contato ou sala",
"recipientNoLongerAvailable": "O destinatário não está mais disponível",
"failedToSendMessage": "Falha ao enviar: {error}",
"tictactoeDirectOnly": "O jogo da velha funciona apenas em mensagens diretas. Escolha primeiro um contato.",
"deviceKeyUnavailable": "A chave do dispositivo não está disponível",
"imageCompressionFailed": "Falha na compressão da imagem",
"imageFragmentationFailed": "Falha na fragmentação da imagem",
"failedToAnnounceImage": "Falha ao anunciar a imagem",
"noRecipientSelected": "Nenhum destinatário selecionado",
"imageSendFailed": "Falha no envio da imagem",
"microphonePermissionRequiredForVoice": "É necessária permissão de microfone para mensagens de voz",
"recordingDiscarded": "Gravação descartada",
"publicChannelMediaWarning": "Você está prestes a enviar {mediaType} para o canal público. Isso não é recomendado, pois todos na rede mesh podem receber. Escolha um canal privado ou com tag, a menos que seja isso que você quer.",
"selectPrivateChannelToShareLocation": "Selecione um canal privado para compartilhar sua localização.",
"stopSharingMyLocation": "Parar de compartilhar minha localização",
"shareMyLocation": "Compartilhar minha localização",
"stopRecording": "Parar gravação",
"recordVoice": "Gravar mensagem de voz",
"moreActions": "Mais ações",
"noMessagesInFilter": "Nenhuma mensagem neste filtro",
"noMatchesInFilter": "Nenhum resultado neste filtro",
"syncFailed": "Falha na sincronização: {error}",
"invalidLink": "Link inválido",
"cannotOpenLink": "Não é possível abrir o link",
"failedToOpenLink": "Falha ao abrir o link",
"failedToResendMessage": "Falha ao reenviar a mensagem",
"retryFailed": "Falha na nova tentativa: {error}",
"deleteDrawingAndMessage": "Excluir desenho e mensagem",
"lastEchoRelay": "Último eco: repetidor",
"lastEchoPath": "Último eco: caminho",
"lastEchoBytesReport": "Último eco: relatório de bytes",
"noGpsCoordinatesAvailable": "Nenhuma coordenada GPS disponível",
"notASarMarker": "Não é um marcador SAR",
"failedToSaveTemplate": "Falha ao salvar o modelo: {error}",
"drawingNotFound": "Desenho não encontrado",
"unknownDrawingType": "Tipo de desenho desconhecido",
"customMapMarker": "Marcador de mapa personalizado",
"customMapPointLabel": "Ponto: {point}",
"mapIdLabel": "ID do mapa: {id}",
"cannotFetchVoice": "Não é possível obter a voz",
"senderContactUnknown": "O contato do remetente é desconhecido. Sincronize os contatos primeiro.",
"senderRouteUnknown": "A rota do remetente é desconhecida. Sincronize os contatos/o caminho primeiro.",
"messageTooFar": "A mensagem está longe demais ({hops} saltos, máx. {maxHops}).",
"senderRouteNoPathResponse": "A rota do remetente não respondeu à verificação de caminho. Sincronize os contatos/o caminho e tente novamente.",
"senderRouteNoRawResponse": "A rota do remetente não respondeu no caminho de transporte raw.",
"voiceFetchOverHops": "Obter a voz por {hops} saltos pode demorar um pouco.",
"voiceReceiveCanceled": "Recebimento de voz cancelado",
"fetchingMissingVoiceFragments": "Obtendo fragmentos de voz ausentes",
"receivingVoice": "Recebendo voz",
"cannotFetchImage": "Não é possível obter a imagem",
"imageFetchOverHops": "Obter a imagem por {hops} saltos pode demorar um pouco.",
"imageAlreadyBeingReceived": "A imagem já está sendo recebida",
"loadImage": "Carregar imagem",
"imageFetchFailedToSendRequest": "Falha ao enviar a solicitação da imagem",
"imageUnavailable": "Imagem indisponível no momento",
"imageFetchTimedOut": "Tempo esgotado ao obter a imagem",
"imageReceiveCanceled": "Recebimento da imagem cancelado",
"closeImagePreview": "Fechar visualização da imagem",
"recordingReleaseToSend": "Gravando... solte para enviar",
"sendingVoice": "Enviando voz...",
"sendLongPressToRecordVoice": "Enviar (toque longo para gravar voz)",
"discardRecording": "Descartar gravação",
"hopCount": "{count, plural, =1{1 salto} other{{count} saltos}}",
"channelsSectionDescription": "Canais de difusão para o tráfego mesh próximo",
"roomsSectionDescription": "Espaços compartilhados para coordenação contínua da equipe",
"contactsSectionDescription": "Pessoas e dispositivos ao seu alcance direto",
"noRecentChatters": "Sem participantes recentes",
"public": "Público",
"failedToSendTicTacToeMove": "Falha ao enviar a jogada do jogo da velha",
"youWon": "Você venceu",
"opponentWon": "O adversário venceu",
"gameDraw": "Empate",
"yourTurn": "Sua vez",
"opponentTurn": "Vez do adversário",
"pleaseEnterValidCoordinates": "Insira coordenadas válidas",
"locationNotAvailable": "Localização não disponível",
"customCaveMapPoint": "Ponto de mapa de caverna personalizado",
"mapPoint": "Ponto do mapa",
"fetchingMissingImageFragments": "Obtendo fragmentos ausentes…",
"loadingImage": "Carregando…",
"receivingImage": "Recebendo…",
"tapToLoad": "Toque para carregar",
"connectDevice": "Conectar dispositivo",
"chooseTransportSubtitle": "Escolha o transporte Bluetooth, WiFi ou Serial",
"ble": "BLE",
"network": "Rede",
"serial": "Serial",
"scan": "Escanear",
"pressScanToSearchForDevices": "Pressione escanear para procurar dispositivos próximos",
"failedToConnectToDevice": "Falha ao conectar a {name}",
"unknownDevice": "Dispositivo desconhecido",
"showingCachedResultsTapRefresh": "Exibindo resultados em cache. Toque em atualizar para escanear novamente.",
"scanningLocalNetworkOnPort": "Escaneando a rede local em busca de dispositivos MeshCore WiFi na porta {port}",
"cancelScanAndAddServer": "Cancelar escaneamento e adicionar servidor",
"addServer": "Adicionar servidor",
"scanningProgressIps": "Escaneando... {scanned}/{total} IPs",
"cancelAndEnterManually": "Cancelar e inserir manualmente",
"noRecentOrDiscoveredServers": "Ainda não há servidores recentes ou descobertos",
"recentlyUsed": "Usados recentemente",
"connecting": "Conectando...",
"discoveredOnThisNetwork": "Descobertos nesta rede",
"serverNoLongerAvailable": "O servidor em {host}:{port} não está mais disponível. Escaneie novamente para encontrar servidores ativos.",
"failedToConnectToHost": "Falha ao conectar a {host}:{port}",
"enterValidIpAddress": "Insira um endereço IP válido",
"enterValidTcpPort": "Insira uma porta TCP válida",
"ipAddress": "Endereço IP",
"tcpPort": "Porta TCP",
"customServerPort": "Porta de servidor personalizada",
"chat": "Chat",
"restoringPreviousLink": "Restaurando a conexão anterior",
"noDeviceConnected": "Nenhum dispositivo conectado",
"sendAdvert": "Enviar meu contato",
"advertMode": "Enviar meu contato",
"advertModeSubtitle": "Escolha até onde suas informações de contato devem chegar.",
"floodAdvertSent": "Contato enviado por toda a rede mesh",
"directAdvertSent": "Contato enviado para os dispositivos próximos"
}

View File

@@ -908,5 +908,125 @@
"enterRegionName": "Название региона (напр. auckland)",
"noRegionsFound": "Регионы не найдены на этом ретрансляторе.",
"regionScopeSet": "Область региона установлена на {name}",
"regionScopeCleared": "Область региона очищена"
"regionScopeCleared": "Область региона очищена",
"simpleMode": "Простой режим",
"simpleModeDescription": "Только чат и карта с избранными контактами",
"simpleModeRequiresLockedDestination": "Сначала включите «Зафиксировать назначение» с каналом, чтобы использовать простой режим",
"noFavouriteContacts": "Нет избранных контактов",
"selectDestinationFirst": "Сначала выберите канал, контакт или комнату",
"recipientNoLongerAvailable": "Получатель больше не доступен",
"failedToSendMessage": "Не удалось отправить: {error}",
"tictactoeDirectOnly": "Крестики-нолики работают только в личных сообщениях. Сначала выберите контакт.",
"deviceKeyUnavailable": "Ключ устройства недоступен",
"imageCompressionFailed": "Не удалось сжать изображение",
"imageFragmentationFailed": "Не удалось разделить изображение на фрагменты",
"failedToAnnounceImage": "Не удалось анонсировать изображение",
"noRecipientSelected": "Получатель не выбран",
"imageSendFailed": "Не удалось отправить изображение",
"microphonePermissionRequiredForVoice": "Для голосовых сообщений требуется разрешение на микрофон",
"recordingDiscarded": "Запись отменена",
"publicChannelMediaWarning": "Вы собираетесь отправить {mediaType} в публичный канал. Это не рекомендуется: получить это может любой в mesh-сети. Выберите приватный или тегированный канал, если только это не то, что вы хотите.",
"selectPrivateChannelToShareLocation": "Выберите приватный канал, чтобы поделиться местоположением.",
"stopSharingMyLocation": "Прекратить делиться местоположением",
"shareMyLocation": "Поделиться местоположением",
"stopRecording": "Остановить запись",
"recordVoice": "Записать голосовое сообщение",
"moreActions": "Другие действия",
"noMessagesInFilter": "В этом фильтре нет сообщений",
"noMatchesInFilter": "В этом фильтре нет совпадений",
"syncFailed": "Ошибка синхронизации: {error}",
"invalidLink": "Недействительная ссылка",
"cannotOpenLink": "Не удаётся открыть ссылку",
"failedToOpenLink": "Не удалось открыть ссылку",
"failedToResendMessage": "Не удалось повторно отправить сообщение",
"retryFailed": "Повторная попытка не удалась: {error}",
"deleteDrawingAndMessage": "Удалить рисунок и сообщение",
"lastEchoRelay": "Последнее эхо: ретранслятор",
"lastEchoPath": "Последнее эхо: путь",
"lastEchoBytesReport": "Последнее эхо: отчёт о байтах",
"noGpsCoordinatesAvailable": "Координаты GPS недоступны",
"notASarMarker": "Это не маркер SAR",
"failedToSaveTemplate": "Не удалось сохранить шаблон: {error}",
"drawingNotFound": "Рисунок не найден",
"unknownDrawingType": "Неизвестный тип рисунка",
"customMapMarker": "Пользовательский маркер карты",
"customMapPointLabel": "Точка: {point}",
"mapIdLabel": "ID карты: {id}",
"cannotFetchVoice": "Не удаётся получить голосовое сообщение",
"senderContactUnknown": "Контакт отправителя неизвестен. Сначала синхронизируйте контакты.",
"senderRouteUnknown": "Маршрут отправителя неизвестен. Сначала синхронизируйте контакты/путь.",
"messageTooFar": "Сообщение слишком далеко ({hops} хопов, макс. {maxHops}).",
"senderRouteNoPathResponse": "Маршрут отправителя не ответил на проверку пути. Синхронизируйте контакты/путь и попробуйте снова.",
"senderRouteNoRawResponse": "Маршрут отправителя не ответил на raw-транспортном пути.",
"voiceFetchOverHops": "Получение голоса через {hops} хопов может занять время.",
"voiceReceiveCanceled": "Приём голосового сообщения отменён",
"fetchingMissingVoiceFragments": "Получение недостающих фрагментов голоса",
"receivingVoice": "Приём голоса",
"cannotFetchImage": "Не удаётся получить изображение",
"imageFetchOverHops": "Получение изображения через {hops} хопов может занять время.",
"imageAlreadyBeingReceived": "Изображение уже принимается",
"loadImage": "Загрузить изображение",
"imageFetchFailedToSendRequest": "Не удалось отправить запрос на изображение",
"imageUnavailable": "Изображение сейчас недоступно",
"imageFetchTimedOut": "Время ожидания изображения истекло",
"imageReceiveCanceled": "Приём изображения отменён",
"closeImagePreview": "Закрыть просмотр изображения",
"recordingReleaseToSend": "Запись... отпустите, чтобы отправить",
"sendingVoice": "Отправка голоса...",
"sendLongPressToRecordVoice": "Отправить (удерживайте для записи голоса)",
"discardRecording": "Удалить запись",
"hopCount": "{count, plural, one{{count} хоп} few{{count} хопа} many{{count} хопов} other{{count} хопа}}",
"channelsSectionDescription": "Широковещательные каналы для mesh-трафика поблизости",
"roomsSectionDescription": "Общие пространства для постоянной координации команды",
"contactsSectionDescription": "Люди и устройства в прямой досягаемости",
"noRecentChatters": "Нет недавних собеседников",
"public": "Публичный",
"failedToSendTicTacToeMove": "Не удалось отправить ход в крестики-нолики",
"youWon": "Вы победили",
"opponentWon": "Победил соперник",
"gameDraw": "Ничья",
"yourTurn": "Ваш ход",
"opponentTurn": "Ход соперника",
"pleaseEnterValidCoordinates": "Введите корректные координаты",
"locationNotAvailable": "Местоположение недоступно",
"customCaveMapPoint": "Пользовательская точка карты пещеры",
"mapPoint": "Точка карты",
"fetchingMissingImageFragments": "Получение недостающих фрагментов…",
"loadingImage": "Загрузка…",
"receivingImage": "Приём…",
"tapToLoad": "Нажмите, чтобы загрузить",
"connectDevice": "Подключить устройство",
"chooseTransportSubtitle": "Выберите транспорт: Bluetooth, WiFi или последовательный порт",
"ble": "BLE",
"network": "Сеть",
"serial": "Последовательный порт",
"scan": "Сканировать",
"pressScanToSearchForDevices": "Нажмите «Сканировать», чтобы найти устройства поблизости",
"failedToConnectToDevice": "Не удалось подключиться к {name}",
"unknownDevice": "Неизвестное устройство",
"showingCachedResultsTapRefresh": "Показаны кэшированные результаты. Нажмите «Обновить», чтобы пересканировать.",
"scanningLocalNetworkOnPort": "Сканирование локальной сети на наличие WiFi-устройств MeshCore на порту {port}",
"cancelScanAndAddServer": "Отменить сканирование и добавить сервер",
"addServer": "Добавить сервер",
"scanningProgressIps": "Сканирование... {scanned}/{total} IP-адресов",
"cancelAndEnterManually": "Отменить и ввести вручную",
"noRecentOrDiscoveredServers": "Пока нет недавних или обнаруженных серверов",
"recentlyUsed": "Недавно использованные",
"connecting": "Подключение...",
"discoveredOnThisNetwork": "Обнаружены в этой сети",
"serverNoLongerAvailable": "Сервер {host}:{port} больше недоступен. Выполните сканирование снова, чтобы найти активные серверы.",
"failedToConnectToHost": "Не удалось подключиться к {host}:{port}",
"enterValidIpAddress": "Введите корректный IP-адрес",
"enterValidTcpPort": "Введите корректный TCP-порт",
"ipAddress": "IP-адрес",
"tcpPort": "TCP-порт",
"customServerPort": "Пользовательский порт сервера",
"chat": "Чат",
"restoringPreviousLink": "Восстановление предыдущего подключения",
"noDeviceConnected": "Нет подключённого устройства",
"sendAdvert": "Отправить мой контакт",
"advertMode": "Отправить мой контакт",
"advertModeSubtitle": "Выберите, как далеко должны распространиться ваши контактные данные.",
"floodAdvertSent": "Контакт отправлен по всей mesh-сети",
"directAdvertSent": "Контакт отправлен ближайшим устройствам"
}

View File

@@ -908,5 +908,125 @@
"enterRegionName": "Ime regije (npr. auckland)",
"noRegionsFound": "Na tem repetitorju ni najdenih regij.",
"regionScopeSet": "Obseg regije nastavljen na {name}",
"regionScopeCleared": "Obseg regije počiščen"
"regionScopeCleared": "Obseg regije počiščen",
"simpleMode": "Enostavni način",
"simpleModeDescription": "Samo klepet in zemljevid s priljubljenimi stiki",
"simpleModeRequiresLockedDestination": "Za uporabo enostavnega načina najprej omogočite »Zakleni cilj« s kanalom",
"noFavouriteContacts": "Ni priljubljenih stikov",
"selectDestinationFirst": "Najprej izberite kanal, stik ali sobo",
"recipientNoLongerAvailable": "Prejemnik ni več na voljo",
"failedToSendMessage": "Pošiljanje ni uspelo: {error}",
"tictactoeDirectOnly": "Križci in krožci delujejo samo v neposrednih sporočilih. Najprej izberite stik.",
"deviceKeyUnavailable": "Ključ naprave ni na voljo",
"imageCompressionFailed": "Stiskanje slike ni uspelo",
"imageFragmentationFailed": "Razdelitev slike ni uspela",
"failedToAnnounceImage": "Najava slike ni uspela",
"noRecipientSelected": "Prejemnik ni izbran",
"imageSendFailed": "Pošiljanje slike ni uspelo",
"microphonePermissionRequiredForVoice": "Za glasovna sporočila je potrebno dovoljenje za mikrofon",
"recordingDiscarded": "Posnetek je zavržen",
"publicChannelMediaWarning": "Pravkar boste poslali {mediaType} v javni kanal. To ni priporočljivo, ker lahko vsebino prejmejo vsi v omrežju mesh. Izberite zaseben ali označen kanal, razen če to res želite.",
"selectPrivateChannelToShareLocation": "Za deljenje lokacije izberite zaseben kanal.",
"stopSharingMyLocation": "Prenehaj deliti mojo lokacijo",
"shareMyLocation": "Deli mojo lokacijo",
"stopRecording": "Ustavi snemanje",
"recordVoice": "Posnemi glasovno sporočilo",
"moreActions": "Več dejanj",
"noMessagesInFilter": "V tem filtru ni sporočil",
"noMatchesInFilter": "V tem filtru ni zadetkov",
"syncFailed": "Sinhronizacija ni uspela: {error}",
"invalidLink": "Neveljavna povezava",
"cannotOpenLink": "Povezave ni mogoče odpreti",
"failedToOpenLink": "Odpiranje povezave ni uspelo",
"failedToResendMessage": "Ponovno pošiljanje sporočila ni uspelo",
"retryFailed": "Ponovni poskus ni uspel: {error}",
"deleteDrawingAndMessage": "Izbriši risbo in sporočilo",
"lastEchoRelay": "Posrednik zadnjega odmeva",
"lastEchoPath": "Pot zadnjega odmeva",
"lastEchoBytesReport": "Poročilo o bajtih zadnjega odmeva",
"noGpsCoordinatesAvailable": "GPS-koordinate niso na voljo",
"notASarMarker": "To ni SAR označevalec",
"failedToSaveTemplate": "Shranjevanje predloge ni uspelo: {error}",
"drawingNotFound": "Risbe ni mogoče najti",
"unknownDrawingType": "Neznana vrsta risbe",
"customMapMarker": "Označevalec karte po meri",
"customMapPointLabel": "Točka: {point}",
"mapIdLabel": "ID karte: {id}",
"cannotFetchVoice": "Glasovnega sporočila ni mogoče prenesti",
"senderContactUnknown": "Stik pošiljatelja ni znan. Najprej sinhronizirajte stike.",
"senderRouteUnknown": "Pot pošiljatelja ni znana. Najprej sinhronizirajte stike/poti.",
"messageTooFar": "Sporočilo je predaleč ({hops} skokov, največ {maxHops}).",
"senderRouteNoPathResponse": "Pot pošiljatelja se ni odzvala na preverjanje poti. Sinhronizirajte stike/poti in poskusite znova.",
"senderRouteNoRawResponse": "Pot pošiljatelja se ni odzvala na neposredni prenosni poti.",
"voiceFetchOverHops": "Prenos glasu prek {hops} skokov lahko traja dlje.",
"voiceReceiveCanceled": "Sprejem glasovnega sporočila je preklican",
"fetchingMissingVoiceFragments": "Pridobivanje manjkajočih glasovnih delov",
"receivingVoice": "Sprejemanje glasu",
"cannotFetchImage": "Slike ni mogoče prenesti",
"imageFetchOverHops": "Prenos slike prek {hops} skokov lahko traja dlje.",
"imageAlreadyBeingReceived": "Slika se že sprejema",
"loadImage": "Naloži sliko",
"imageFetchFailedToSendRequest": "Zahteve za prenos slike ni bilo mogoče poslati",
"imageUnavailable": "Slika trenutno ni na voljo",
"imageFetchTimedOut": "Prenos slike je potekel",
"imageReceiveCanceled": "Sprejem slike je preklican",
"closeImagePreview": "Zapri predogled slike",
"recordingReleaseToSend": "Snemanje ... spustite za pošiljanje glasu",
"sendingVoice": "Pošiljanje glasu ...",
"sendLongPressToRecordVoice": "Pošlji (dolg pritisk za snemanje glasu)",
"discardRecording": "Zavrzi posnetek",
"hopCount": "{count, plural, one{{count} skok} two{{count} skoka} few{{count} skoki} other{{count} skokov}}",
"channelsSectionDescription": "Oddajni kanali za bližnji promet v omrežju mesh",
"roomsSectionDescription": "Skupni prostori za stalno usklajevanje ekipe",
"contactsSectionDescription": "Osebe in naprave, ki jih lahko dosežete neposredno",
"noRecentChatters": "Ni nedavnih klepetalcev",
"public": "Javno",
"failedToSendTicTacToeMove": "Poteze pri križcih in krožcih ni bilo mogoče poslati",
"youWon": "Zmagali ste",
"opponentWon": "Zmagal je nasprotnik",
"gameDraw": "Neodločeno",
"yourTurn": "Vi ste na vrsti",
"opponentTurn": "Na vrsti je nasprotnik",
"pleaseEnterValidCoordinates": "Vnesite veljavne koordinate",
"locationNotAvailable": "Lokacija ni na voljo",
"customCaveMapPoint": "Točka jamske karte po meri",
"mapPoint": "Točka na karti",
"fetchingMissingImageFragments": "Pridobivanje manjkajočih delov …",
"loadingImage": "Nalaganje …",
"receivingImage": "Sprejemanje …",
"tapToLoad": "Tapnite za nalaganje",
"connectDevice": "Poveži napravo",
"chooseTransportSubtitle": "Izberite način povezave: Bluetooth, WiFi ali serijska",
"ble": "BLE",
"network": "Omrežje",
"serial": "Serijsko",
"scan": "Poišči",
"pressScanToSearchForDevices": "Pritisnite Poišči za iskanje naprav v bližini",
"failedToConnectToDevice": "Povezava z napravo {name} ni uspela",
"unknownDevice": "Neznana naprava",
"showingCachedResultsTapRefresh": "Prikazani so predpomnjeni rezultati. Za ponovno iskanje tapnite osveži.",
"scanningLocalNetworkOnPort": "Iskanje naprav MeshCore WiFi v lokalnem omrežju na vratih {port}",
"cancelScanAndAddServer": "Prekliči iskanje in dodaj strežnik",
"addServer": "Dodaj strežnik",
"scanningProgressIps": "Iskanje … {scanned}/{total} IP naslovov",
"cancelAndEnterManually": "Prekliči in vnesi ročno",
"noRecentOrDiscoveredServers": "Še ni nedavnih ali odkritih strežnikov",
"recentlyUsed": "Nedavno uporabljeno",
"connecting": "Povezovanje …",
"discoveredOnThisNetwork": "Odkrito v tem omrežju",
"serverNoLongerAvailable": "Strežnik na {host}:{port} ni več dosegljiv. Ponovite iskanje, da najdete aktivne strežnike.",
"failedToConnectToHost": "Povezava s strežnikom {host}:{port} ni uspela",
"enterValidIpAddress": "Vnesite veljaven IP naslov",
"enterValidTcpPort": "Vnesite veljavna vrata TCP",
"ipAddress": "IP naslov",
"tcpPort": "Vrata TCP",
"customServerPort": "Vrata strežnika po meri",
"chat": "Klepet",
"restoringPreviousLink": "Obnavljanje prejšnje povezave",
"noDeviceConnected": "Ni povezane naprave",
"sendAdvert": "Pošlji moj kontakt",
"advertMode": "Pošlji moj kontakt",
"advertModeSubtitle": "Izberite, kako daleč naj potujejo vaši kontaktni podatki.",
"floodAdvertSent": "Kontakt poslan z razprševanjem po omrežju",
"directAdvertSent": "Kontakt poslan neposredno bližnjim napravam"
}

View File

@@ -2493,5 +2493,125 @@
"enterRegionName": "Bölge adı (ör. auckland)",
"noRegionsFound": "Bu aktarıcıda bölge bulunamadı.",
"regionScopeSet": "Bölge kapsamı {name} olarak ayarlandı",
"regionScopeCleared": "Bölge kapsamı temizlendi"
"regionScopeCleared": "Bölge kapsamı temizlendi",
"simpleMode": "Basit mod",
"simpleModeDescription": "Yalnızca favori kişilerle sohbet ve harita",
"simpleModeRequiresLockedDestination": "Basit modu kullanmak için önce bir kanalla \"Hedefi kilitle\" seçeneğini etkinleştirin",
"noFavouriteContacts": "Favori kişi yok",
"selectDestinationFirst": "Önce bir kanal, kişi veya oda seçin",
"recipientNoLongerAvailable": "Alıcı artık mevcut değil",
"failedToSendMessage": "Gönderilemedi: {error}",
"tictactoeDirectOnly": "XOX yalnızca doğrudan mesajlarda çalışır. Önce bir kişi seçin.",
"deviceKeyUnavailable": "Cihaz anahtarı mevcut değil",
"imageCompressionFailed": "Resim sıkıştırma başarısız",
"imageFragmentationFailed": "Resim parçalama başarısız",
"failedToAnnounceImage": "Resim duyurulamadı",
"noRecipientSelected": "Alıcı seçilmedi",
"imageSendFailed": "Resim gönderilemedi",
"microphonePermissionRequiredForVoice": "Sesli mesajlar için mikrofon izni gerekli",
"recordingDiscarded": "Kayıt silindi",
"publicChannelMediaWarning": "Genel kanala {mediaType} göndermek üzeresiniz. Mesh ağındaki herkes bunu alabileceği için bu önerilmez. Bunu özellikle istemiyorsanız özel veya etiketli bir kanal seçin.",
"selectPrivateChannelToShareLocation": "Konumunuzu paylaşmak için özel bir kanal seçin.",
"stopSharingMyLocation": "Konumumu paylaşmayı durdur",
"shareMyLocation": "Konumumu paylaş",
"stopRecording": "Kaydı durdur",
"recordVoice": "Sesli mesaj kaydet",
"moreActions": "Diğer işlemler",
"noMessagesInFilter": "Bu filtrede mesaj yok",
"noMatchesInFilter": "Bu filtrede eşleşme yok",
"syncFailed": "Senkronizasyon başarısız: {error}",
"invalidLink": "Geçersiz bağlantı",
"cannotOpenLink": "Bağlantıılamıyor",
"failedToOpenLink": "Bağlantıılamadı",
"failedToResendMessage": "Mesaj yeniden gönderilemedi",
"retryFailed": "Yeniden deneme başarısız: {error}",
"deleteDrawingAndMessage": "Çizimi ve mesajı sil",
"lastEchoRelay": "Son yankı: röle",
"lastEchoPath": "Son yankı: yol",
"lastEchoBytesReport": "Son yankı: bayt raporu",
"noGpsCoordinatesAvailable": "Kullanılabilir GPS koordinatı yok",
"notASarMarker": "Bu bir SAR işareti değil",
"failedToSaveTemplate": "Şablon kaydedilemedi: {error}",
"drawingNotFound": "Çizim bulunamadı",
"unknownDrawingType": "Bilinmeyen çizim türü",
"customMapMarker": "Özel harita işareti",
"customMapPointLabel": "Nokta: {point}",
"mapIdLabel": "Harita ID: {id}",
"cannotFetchVoice": "Ses alınamıyor",
"senderContactUnknown": "Gönderenin kişisi bilinmiyor. Önce kişileri senkronize edin.",
"senderRouteUnknown": "Gönderenin rotası bilinmiyor. Önce kişileri/yolu senkronize edin.",
"messageTooFar": "Mesaj çok uzakta ({hops} atlama, en fazla {maxHops}).",
"senderRouteNoPathResponse": "Gönderenin rotası yol denetimine yanıt vermedi. Kişileri/yolu senkronize edip yeniden deneyin.",
"senderRouteNoRawResponse": "Gönderenin rotası ham aktarım yolunda yanıt vermedi.",
"voiceFetchOverHops": "{hops} atlama üzerinden ses alma biraz sürebilir.",
"voiceReceiveCanceled": "Ses alımı iptal edildi",
"fetchingMissingVoiceFragments": "Eksik ses parçaları alınıyor",
"receivingVoice": "Ses alınıyor",
"cannotFetchImage": "Resim alınamıyor",
"imageFetchOverHops": "{hops} atlama üzerinden resim alma biraz sürebilir.",
"imageAlreadyBeingReceived": "Resim zaten alınıyor",
"loadImage": "Resmi yükle",
"imageFetchFailedToSendRequest": "Resim isteği gönderilemedi",
"imageUnavailable": "Resim şu anda kullanılamıyor",
"imageFetchTimedOut": "Resim alma zaman aşımına uğradı",
"imageReceiveCanceled": "Resim alımı iptal edildi",
"closeImagePreview": "Resim önizlemesini kapat",
"recordingReleaseToSend": "Kaydediliyor... göndermek için bırakın",
"sendingVoice": "Ses gönderiliyor...",
"sendLongPressToRecordVoice": "Gönder (ses kaydı için basılı tutun)",
"discardRecording": "Kaydı sil",
"hopCount": "{count, plural, other{{count} atlama}}",
"channelsSectionDescription": "Yakındaki mesh trafiği için yayın kanalları",
"roomsSectionDescription": "Sürekli ekip koordinasyonu için ortak alanlar",
"contactsSectionDescription": "Doğrudan ulaşabileceğiniz kişiler ve cihazlar",
"noRecentChatters": "Yakın zamanda konuşan yok",
"public": "Genel",
"failedToSendTicTacToeMove": "XOX hamlesi gönderilemedi",
"youWon": "Kazandınız",
"opponentWon": "Rakip kazandı",
"gameDraw": "Berabere",
"yourTurn": "Sıra sizde",
"opponentTurn": "Sıra rakipte",
"pleaseEnterValidCoordinates": "Geçerli koordinatlar girin",
"locationNotAvailable": "Konum mevcut değil",
"customCaveMapPoint": "Özel mağara haritası noktası",
"mapPoint": "Harita noktası",
"fetchingMissingImageFragments": "Eksik parçalar alınıyor…",
"loadingImage": "Yükleniyor…",
"receivingImage": "Alınıyor…",
"tapToLoad": "Yüklemek için dokunun",
"connectDevice": "Cihaza bağlan",
"chooseTransportSubtitle": "Bluetooth, WiFi veya Seri aktarım seçin",
"ble": "BLE",
"network": "Ağ",
"serial": "Seri",
"scan": "Tara",
"pressScanToSearchForDevices": "Yakındaki cihazları aramak için tara düğmesine basın",
"failedToConnectToDevice": "{name} cihazına bağlanılamadı",
"unknownDevice": "Bilinmeyen cihaz",
"showingCachedResultsTapRefresh": "Önbelleğe alınmış sonuçlar gösteriliyor. Yeniden taramak için yenile düğmesine dokunun.",
"scanningLocalNetworkOnPort": "Yerel ağda {port} bağlantı noktasındaki MeshCore WiFi cihazları taranıyor",
"cancelScanAndAddServer": "Taramayı iptal et ve sunucu ekle",
"addServer": "Sunucu ekle",
"scanningProgressIps": "Taranıyor... {scanned}/{total} IP",
"cancelAndEnterManually": "İptal et ve elle gir",
"noRecentOrDiscoveredServers": "Henüz son kullanılan veya keşfedilen sunucu yok",
"recentlyUsed": "Son kullanılanlar",
"connecting": "Bağlanıyor...",
"discoveredOnThisNetwork": "Bu ağda keşfedildi",
"serverNoLongerAvailable": "{host}:{port} adresindeki sunucu artık kullanılamıyor. Etkin sunucuları bulmak için lütfen yeniden tarayın.",
"failedToConnectToHost": "{host}:{port} adresine bağlanılamadı",
"enterValidIpAddress": "Geçerli bir IP adresi girin",
"enterValidTcpPort": "Geçerli bir TCP bağlantı noktası girin",
"ipAddress": "IP adresi",
"tcpPort": "TCP bağlantı noktası",
"customServerPort": "Özel sunucu bağlantı noktası",
"chat": "Sohbet",
"restoringPreviousLink": "Önceki bağlantı geri yükleniyor",
"noDeviceConnected": "Bağlı cihaz yok",
"sendAdvert": "Kişi bilgimi gönder",
"advertMode": "Kişi bilgimi gönder",
"advertModeSubtitle": "Kişi bilgilerinizin ne kadar uzağa gideceğini seçin.",
"floodAdvertSent": "Kişi bilgisi tüm mesh ağına gönderildi",
"directAdvertSent": "Kişi bilgisi yakındaki cihazlara gönderildi"
}

View File

@@ -2493,5 +2493,125 @@
"enterRegionName": "Назва регіону (напр. auckland)",
"noRegionsFound": "Регіони не знайдені на цьому ретрансляторі.",
"regionScopeSet": "Область регіону встановлена на {name}",
"regionScopeCleared": "Область регіону очищена"
"regionScopeCleared": "Область регіону очищена",
"simpleMode": "Простий режим",
"simpleModeDescription": "Лише чат і карта з обраними контактами",
"simpleModeRequiresLockedDestination": "Спочатку увімкніть «Зафіксувати призначення» з каналом, щоб використовувати простий режим",
"noFavouriteContacts": "Немає обраних контактів",
"selectDestinationFirst": "Спершу виберіть канал, контакт або кімнату",
"recipientNoLongerAvailable": "Одержувач більше недоступний",
"failedToSendMessage": "Не вдалося надіслати: {error}",
"tictactoeDirectOnly": "Хрестики-нулики працюють лише в особистих повідомленнях. Спершу виберіть контакт.",
"deviceKeyUnavailable": "Ключ пристрою недоступний",
"imageCompressionFailed": "Не вдалося стиснути зображення",
"imageFragmentationFailed": "Не вдалося розділити зображення на фрагменти",
"failedToAnnounceImage": "Не вдалося анонсувати зображення",
"noRecipientSelected": "Одержувача не вибрано",
"imageSendFailed": "Не вдалося надіслати зображення",
"microphonePermissionRequiredForVoice": "Для голосових повідомлень потрібен дозвіл на мікрофон",
"recordingDiscarded": "Запис відхилено",
"publicChannelMediaWarning": "Ви збираєтеся надіслати {mediaType} в публічний канал. Це не рекомендується: отримати це може будь-хто в mesh-мережі. Виберіть приватний або тегований канал, якщо тільки це не те, що ви хочете.",
"selectPrivateChannelToShareLocation": "Виберіть приватний канал, щоб поділитися місцезнаходженням.",
"stopSharingMyLocation": "Припинити ділитися місцезнаходженням",
"shareMyLocation": "Поділитися місцезнаходженням",
"stopRecording": "Зупинити запис",
"recordVoice": "Записати голосове повідомлення",
"moreActions": "Інші дії",
"noMessagesInFilter": "У цьому фільтрі немає повідомлень",
"noMatchesInFilter": "У цьому фільтрі немає збігів",
"syncFailed": "Помилка синхронізації: {error}",
"invalidLink": "Недійсне посилання",
"cannotOpenLink": "Не вдається відкрити посилання",
"failedToOpenLink": "Не вдалося відкрити посилання",
"failedToResendMessage": "Не вдалося повторно надіслати повідомлення",
"retryFailed": "Повторна спроба не вдалася: {error}",
"deleteDrawingAndMessage": "Видалити рисунок і повідомлення",
"lastEchoRelay": "Останнє відлуння: ретранслятор",
"lastEchoPath": "Останнє відлуння: шлях",
"lastEchoBytesReport": "Останнє відлуння: звіт про байти",
"noGpsCoordinatesAvailable": "Координати GPS недоступні",
"notASarMarker": "Це не маркер SAR",
"failedToSaveTemplate": "Не вдалося зберегти шаблон: {error}",
"drawingNotFound": "Рисунок не знайдено",
"unknownDrawingType": "Невідомий тип рисунка",
"customMapMarker": "Власний маркер карти",
"customMapPointLabel": "Точка: {point}",
"mapIdLabel": "ID карти: {id}",
"cannotFetchVoice": "Не вдається отримати голосове повідомлення",
"senderContactUnknown": "Контакт відправника невідомий. Спершу синхронізуйте контакти.",
"senderRouteUnknown": "Маршрут відправника невідомий. Спершу синхронізуйте контакти/шлях.",
"messageTooFar": "Повідомлення задалеко ({hops} хопів, макс. {maxHops}).",
"senderRouteNoPathResponse": "Маршрут відправника не відповів на перевірку шляху. Синхронізуйте контакти/шлях і спробуйте ще раз.",
"senderRouteNoRawResponse": "Маршрут відправника не відповів на raw-транспортному шляху.",
"voiceFetchOverHops": "Отримання голосу через {hops} хопів може зайняти час.",
"voiceReceiveCanceled": "Приймання голосового повідомлення скасовано",
"fetchingMissingVoiceFragments": "Отримання відсутніх фрагментів голосу",
"receivingVoice": "Приймання голосу",
"cannotFetchImage": "Не вдається отримати зображення",
"imageFetchOverHops": "Отримання зображення через {hops} хопів може зайняти час.",
"imageAlreadyBeingReceived": "Зображення вже приймається",
"loadImage": "Завантажити зображення",
"imageFetchFailedToSendRequest": "Не вдалося надіслати запит на зображення",
"imageUnavailable": "Зображення зараз недоступне",
"imageFetchTimedOut": "Час очікування зображення минув",
"imageReceiveCanceled": "Приймання зображення скасовано",
"closeImagePreview": "Закрити перегляд зображення",
"recordingReleaseToSend": "Запис... відпустіть, щоб надіслати",
"sendingVoice": "Надсилання голосу...",
"sendLongPressToRecordVoice": "Надіслати (утримуйте для запису голосу)",
"discardRecording": "Відхилити запис",
"hopCount": "{count, plural, one{{count} хоп} few{{count} хопи} many{{count} хопів} other{{count} хопа}}",
"channelsSectionDescription": "Широкомовні канали для mesh-трафіку поблизу",
"roomsSectionDescription": "Спільні простори для постійної координації команди",
"contactsSectionDescription": "Люди та пристрої в прямій досяжності",
"noRecentChatters": "Немає нещодавніх співрозмовників",
"public": "Публічний",
"failedToSendTicTacToeMove": "Не вдалося надіслати хід у хрестики-нулики",
"youWon": "Ви перемогли",
"opponentWon": "Переміг суперник",
"gameDraw": "Нічия",
"yourTurn": "Ваш хід",
"opponentTurn": "Хід суперника",
"pleaseEnterValidCoordinates": "Введіть коректні координати",
"locationNotAvailable": "Місцезнаходження недоступне",
"customCaveMapPoint": "Власна точка карти печери",
"mapPoint": "Точка карти",
"fetchingMissingImageFragments": "Отримання відсутніх фрагментів…",
"loadingImage": "Завантаження…",
"receivingImage": "Приймання…",
"tapToLoad": "Торкніться, щоб завантажити",
"connectDevice": "Підключити пристрій",
"chooseTransportSubtitle": "Виберіть транспорт: Bluetooth, WiFi або послідовний порт",
"ble": "BLE",
"network": "Мережа",
"serial": "Послідовний порт",
"scan": "Сканувати",
"pressScanToSearchForDevices": "Натисніть «Сканувати», щоб знайти пристрої поблизу",
"failedToConnectToDevice": "Не вдалося підключитися до {name}",
"unknownDevice": "Невідомий пристрій",
"showingCachedResultsTapRefresh": "Показано кешовані результати. Натисніть «Оновити», щоб пересканувати.",
"scanningLocalNetworkOnPort": "Сканування локальної мережі на наявність WiFi-пристроїв MeshCore на порту {port}",
"cancelScanAndAddServer": "Скасувати сканування та додати сервер",
"addServer": "Додати сервер",
"scanningProgressIps": "Сканування... {scanned}/{total} IP-адрес",
"cancelAndEnterManually": "Скасувати та ввести вручну",
"noRecentOrDiscoveredServers": "Поки немає нещодавніх або виявлених серверів",
"recentlyUsed": "Нещодавно використані",
"connecting": "Підключення...",
"discoveredOnThisNetwork": "Виявлені в цій мережі",
"serverNoLongerAvailable": "Сервер {host}:{port} більше недоступний. Виконайте сканування ще раз, щоб знайти активні сервери.",
"failedToConnectToHost": "Не вдалося підключитися до {host}:{port}",
"enterValidIpAddress": "Введіть коректну IP-адресу",
"enterValidTcpPort": "Введіть коректний TCP-порт",
"ipAddress": "IP-адреса",
"tcpPort": "TCP-порт",
"customServerPort": "Власний порт сервера",
"chat": "Чат",
"restoringPreviousLink": "Відновлення попереднього підключення",
"noDeviceConnected": "Немає підключеного пристрою",
"sendAdvert": "Надіслати мій контакт",
"advertMode": "Надіслати мій контакт",
"advertModeSubtitle": "Виберіть, як далеко мають поширитися ваші контактні дані.",
"floodAdvertSent": "Контакт надіслано по всій mesh-мережі",
"directAdvertSent": "Контакт надіслано пристроям поблизу"
}

View File

@@ -2493,5 +2493,125 @@
"enterRegionName": "区域名称(例如 auckland",
"noRegionsFound": "此中继器上未找到区域。",
"regionScopeSet": "区域范围已设置为 {name}",
"regionScopeCleared": "区域范围已清除"
"regionScopeCleared": "区域范围已清除",
"simpleMode": "简单模式",
"simpleModeDescription": "仅与收藏联系人聊天和查看地图",
"simpleModeRequiresLockedDestination": "请先启用“锁定目的地”并选择一个频道,再使用简单模式",
"noFavouriteContacts": "暂无收藏联系人",
"selectDestinationFirst": "请先选择频道、联系人或房间",
"recipientNoLongerAvailable": "收件人已不可用",
"failedToSendMessage": "发送失败:{error}",
"tictactoeDirectOnly": "井字棋仅适用于私聊消息。请先选择联系人。",
"deviceKeyUnavailable": "设备密钥不可用",
"imageCompressionFailed": "图片压缩失败",
"imageFragmentationFailed": "图片分片失败",
"failedToAnnounceImage": "图片通告失败",
"noRecipientSelected": "未选择收件人",
"imageSendFailed": "图片发送失败",
"microphonePermissionRequiredForVoice": "语音消息需要麦克风权限",
"recordingDiscarded": "已丢弃录音",
"publicChannelMediaWarning": "您即将向公共频道发送{mediaType}。不建议这样做,因为 mesh 网络中的所有人都可能收到。除非确有需要,请选择私有或带标签的频道。",
"selectPrivateChannelToShareLocation": "请选择私有频道来分享您的位置。",
"stopSharingMyLocation": "停止分享我的位置",
"shareMyLocation": "分享我的位置",
"stopRecording": "停止录音",
"recordVoice": "录制语音",
"moreActions": "更多操作",
"noMessagesInFilter": "此筛选条件下没有消息",
"noMatchesInFilter": "此筛选条件下没有匹配项",
"syncFailed": "同步失败:{error}",
"invalidLink": "无效链接",
"cannotOpenLink": "无法打开链接",
"failedToOpenLink": "打开链接失败",
"failedToResendMessage": "重新发送消息失败",
"retryFailed": "重试失败:{error}",
"deleteDrawingAndMessage": "删除绘图和消息",
"lastEchoRelay": "最后回声:中继",
"lastEchoPath": "最后回声:路径",
"lastEchoBytesReport": "最后回声:字节报告",
"noGpsCoordinatesAvailable": "没有可用的 GPS 坐标",
"notASarMarker": "不是搜救标记",
"failedToSaveTemplate": "保存模板失败:{error}",
"drawingNotFound": "未找到绘图",
"unknownDrawingType": "未知绘图类型",
"customMapMarker": "自定义地图标记",
"customMapPointLabel": "坐标点:{point}",
"mapIdLabel": "地图 ID{id}",
"cannotFetchVoice": "无法获取语音",
"senderContactUnknown": "发送者联系人未知。请先同步联系人。",
"senderRouteUnknown": "发送者路由未知。请先同步联系人/路径。",
"messageTooFar": "消息距离过远({hops} 跳,最多 {maxHops} 跳)。",
"senderRouteNoPathResponse": "发送者路由未响应路径检测。请同步联系人/路径后重试。",
"senderRouteNoRawResponse": "发送者路由在原始传输路径上未响应。",
"voiceFetchOverHops": "通过 {hops} 跳获取语音可能需要一些时间。",
"voiceReceiveCanceled": "已取消接收语音",
"fetchingMissingVoiceFragments": "正在获取缺失的语音分片",
"receivingVoice": "正在接收语音",
"cannotFetchImage": "无法获取图片",
"imageFetchOverHops": "通过 {hops} 跳获取图片可能需要一些时间。",
"imageAlreadyBeingReceived": "图片正在接收中",
"loadImage": "加载图片",
"imageFetchFailedToSendRequest": "图片获取请求发送失败",
"imageUnavailable": "图片暂时不可用",
"imageFetchTimedOut": "图片获取超时",
"imageReceiveCanceled": "已取消接收图片",
"closeImagePreview": "关闭图片预览",
"recordingReleaseToSend": "正在录音……松开发送",
"sendingVoice": "正在发送语音……",
"sendLongPressToRecordVoice": "发送(长按录制语音)",
"discardRecording": "丢弃录音",
"hopCount": "{count, plural, other{{count} 跳}}",
"channelsSectionDescription": "用于附近 mesh 通信的广播频道",
"roomsSectionDescription": "用于团队持续协调的共享空间",
"contactsSectionDescription": "可直接联系的人员和设备",
"noRecentChatters": "最近没有人发言",
"public": "公共",
"failedToSendTicTacToeMove": "井字棋走子发送失败",
"youWon": "您赢了",
"opponentWon": "对手赢了",
"gameDraw": "平局",
"yourTurn": "轮到您",
"opponentTurn": "轮到对手",
"pleaseEnterValidCoordinates": "请输入有效坐标",
"locationNotAvailable": "位置不可用",
"customCaveMapPoint": "自定义洞穴地图点",
"mapPoint": "地图点",
"fetchingMissingImageFragments": "正在获取缺失分片……",
"loadingImage": "加载中……",
"receivingImage": "接收中……",
"tapToLoad": "点按加载",
"connectDevice": "连接设备",
"chooseTransportSubtitle": "选择 Bluetooth、WiFi 或串口传输",
"ble": "BLE",
"network": "网络",
"serial": "串口",
"scan": "扫描",
"pressScanToSearchForDevices": "点按扫描以搜索附近设备",
"failedToConnectToDevice": "连接 {name} 失败",
"unknownDevice": "未知设备",
"showingCachedResultsTapRefresh": "正在显示缓存结果。点按刷新以重新扫描。",
"scanningLocalNetworkOnPort": "正在扫描本地网络中端口 {port} 上的 MeshCore WiFi 设备",
"cancelScanAndAddServer": "取消扫描并添加服务器",
"addServer": "添加服务器",
"scanningProgressIps": "正在扫描……{scanned}/{total} 个 IP",
"cancelAndEnterManually": "取消并手动输入",
"noRecentOrDiscoveredServers": "暂无最近使用或发现的服务器",
"recentlyUsed": "最近使用",
"connecting": "正在连接……",
"discoveredOnThisNetwork": "在此网络中发现",
"serverNoLongerAvailable": "{host}:{port} 上的服务器已不可用。请重新扫描以查找活跃的服务器。",
"failedToConnectToHost": "连接 {host}:{port} 失败",
"enterValidIpAddress": "请输入有效的 IP 地址",
"enterValidTcpPort": "请输入有效的 TCP 端口",
"ipAddress": "IP 地址",
"tcpPort": "TCP 端口",
"customServerPort": "自定义服务器端口",
"chat": "聊天",
"restoringPreviousLink": "正在恢复先前的连接",
"noDeviceConnected": "未连接设备",
"sendAdvert": "发送我的联系人信息",
"advertMode": "发送我的联系人信息",
"advertModeSubtitle": "选择您的联系人信息应传播多远。",
"floodAdvertSent": "联系人信息已发送至整个 mesh 网络",
"directAdvertSent": "联系人信息已发送至附近设备"
}

View File

@@ -34,6 +34,7 @@ import 'screens/discovery_screen.dart';
import 'screens/home_screen.dart';
import 'screens/welcome_wizard_screen.dart';
import 'theme/app_theme.dart';
import 'utils/toast_logger.dart';
import 'l10n/app_localizations.dart';
Future<void> main() async {
@@ -50,6 +51,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 +91,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
@@ -390,6 +396,7 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
final systemBrightness = MediaQuery.platformBrightnessOf(context);
final materialApp = MaterialApp(
navigatorKey: _navigatorKey,
scaffoldMessengerKey: rootScaffoldMessengerKey,
key: ValueKey<String?>(
'${_locale?.languageCode ?? 'system'}_${_themeMode.name}',
),

View File

@@ -196,6 +196,54 @@ class ContactRouteCodec {
final hopCount = raw & 0x3F;
return hopCount * hashSize <= maxPathBytes;
}
/// Converts an encoded MeshCore route descriptor + path into the legacy raw
/// path format expected by the `CMD_SEND_RAW_DATA` (0x19) handler in
/// companion firmware v1.15.
///
/// The raw-data handler treats the path-length byte as a *literal* hop count
/// (one byte per hop) rather than as an encoded descriptor, so a 1-hop route
/// using 2-byte hashes (descriptor `0x41`) would otherwise be misread as
/// "65 path bytes" and rejected with `ERROR: Unsupported command`. We
/// down-sample every hop to its first hash byte and emit a plain hop count.
///
/// Routes that are already legacy (hash size 1, descriptor `< 0x40`) pass
/// through unchanged. Unknown/flood descriptors collapse to a zero-hop
/// direct path (raw transport does not support flood routing).
static LegacyRawPath toLegacyRawPath(int descriptor, Uint8List path) {
final raw = toUnsignedDescriptor(descriptor);
if (raw == _unknownDescriptor) {
return const LegacyRawPath(length: 0, path: <int>[]);
}
if (raw < 0x40) {
// Hash size 1: the encoded path is already one byte per hop.
final hopCount = raw;
final available = math.min(hopCount, path.length);
return LegacyRawPath(
length: hopCount,
path: Uint8List.fromList(path.sublist(0, available)),
);
}
final hopCount = raw & 0x3F;
final hashSize = ((raw >> 6) & 0x03) + 1;
final legacy = Uint8List(hopCount);
for (var hop = 0; hop < hopCount; hop++) {
final srcIndex = hop * hashSize;
if (srcIndex < path.length) {
legacy[hop] = path[srcIndex];
}
}
return LegacyRawPath(length: hopCount, path: legacy);
}
}
/// Legacy raw-transport path: a literal hop count plus one byte per hop, as
/// expected by the firmware `CMD_SEND_RAW_DATA` handler.
class LegacyRawPath {
const LegacyRawPath({required this.length, required this.path});
final int length;
final List<int> path;
}
extension ContactLocalization on Contact {

View File

@@ -12,6 +12,8 @@ enum MapLayerType {
// Kept for stored preference compatibility after MBTiles removal.
vectorMbtiles,
wmsBase,
// Appended last: stored layer preference is the enum index.
mapyOutdoor,
}
class MapLayer {
@@ -21,6 +23,9 @@ class MapLayer {
final String attribution;
final double maxZoom;
/// Extra HTTP headers required by the tile server (e.g. Referer).
final Map<String, String>? headers;
// WMS specific properties
final bool isWms;
final String? wmsBaseUrl;
@@ -36,6 +41,7 @@ class MapLayer {
required this.urlTemplate,
required this.attribution,
required this.maxZoom,
this.headers,
this.isWms = false,
this.wmsBaseUrl,
this.wmsLayers,
@@ -64,6 +70,9 @@ class MapLayer {
case MapLayerType.vectorMbtiles:
// Legacy value kept only for preference migration compatibility.
return name;
case MapLayerType.mapyOutdoor:
// Brand name, not translated.
return name;
case MapLayerType.wmsBase:
// For WMS layers, use the name (will be localized separately)
return name;
@@ -86,6 +95,17 @@ class MapLayer {
maxZoom: 17.49, // OpenTopoMap maximum (just below level 18)
);
/// Mapy.cz outdoor map: OSM data with LIDAR-based shaded relief.
/// The tile server rejects requests without a mapy.com Referer.
static const mapyOutdoor = MapLayer(
type: MapLayerType.mapyOutdoor,
name: 'Mapy.cz Outdoor (OSM + LIDAR)',
urlTemplate: 'https://mapserver.mapy.cz/turist-en/retina/{z}-{x}-{y}',
attribution: '© Seznam.cz, a.s., © OpenStreetMap contributors',
maxZoom: 19,
headers: {'Referer': 'https://mapy.com/'},
);
static const esriWorldImagery = MapLayer(
type: MapLayerType.esriWorldImagery,
name: 'ESRI Satellite',
@@ -160,6 +180,7 @@ class MapLayer {
static const List<MapLayer> allLayers = [
openStreetMap,
openTopoMap,
mapyOutdoor,
esriWorldImagery,
googleHybrid,
googleRoadmap,

View File

@@ -147,6 +147,22 @@ class ChannelLocationSharingResult {
});
}
/// Radio fast-GPS region scope, mirroring the firmware's `fast_gps_region`
/// custom var. Index 0 is unscoped (flood everywhere); higher indices restrict
/// beacons to a named region. `labels` comes from the firmware so the picker
/// always matches the regions that build supports.
class FastGpsRegionState {
final List<String> labels;
final int selectedIndex;
const FastGpsRegionState({required this.labels, required this.selectedIndex});
String get selectedLabel =>
(selectedIndex >= 0 && selectedIndex < labels.length)
? labels[selectedIndex]
: (labels.isNotEmpty ? labels.first : 'Unscoped');
}
/// Main App Provider - coordinates all other providers
class AppProvider with ChangeNotifier {
static const int _maxDirectPayloadHops = 3;
@@ -196,7 +212,8 @@ class AppProvider with ChangeNotifier {
bool _isInitialized = false;
bool get isInitialized => _isInitialized;
bool get isSimpleMode => true;
bool _isSimpleMode = false;
bool get isSimpleMode => _isSimpleMode;
bool _isMapEnabled = true;
bool get isMapEnabled => _isMapEnabled;
@@ -283,6 +300,7 @@ class AppProvider with ChangeNotifier {
_canStartAutomaticMessageSync;
_wasDeviceConnected = connectionProvider.deviceInfo.isConnected;
_initializeLocationTracking();
_loadSimpleMode();
_loadMapEnabled();
_loadContactsEnabled();
_loadContactsSectionVisibility();
@@ -593,6 +611,29 @@ class AppProvider with ChangeNotifier {
return true;
}
/// Load simple mode setting from shared preferences
Future<void> _loadSimpleMode() async {
try {
final prefs = await SharedPreferences.getInstance();
_isSimpleMode = prefs.getBool(_scopedKey('simple_mode_enabled')) ?? false;
notifyListeners();
} catch (e) {
debugPrint('Error loading simple mode setting: $e');
}
}
/// Toggle simple mode on/off
Future<void> toggleSimpleMode(bool enabled) async {
try {
_isSimpleMode = enabled;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_scopedKey('simple_mode_enabled'), enabled);
notifyListeners();
} catch (e) {
debugPrint('Error saving simple mode setting: $e');
}
}
/// Load map enabled setting from shared preferences
Future<void> _loadMapEnabled() async {
try {
@@ -956,9 +997,17 @@ class AppProvider with ChangeNotifier {
);
};
locationTrackingService.onFastLocationUpdate = (position, reason) {
unawaited(_sendFastLocationUpdate(position, reason: reason));
};
locationTrackingService.onFastLocationUpdate =
(latitude, longitude, speedKmh, reason) {
unawaited(
_sendFastLocationUpdate(
latitude: latitude,
longitude: longitude,
speedKmh: speedKmh,
reason: reason,
),
);
};
debugPrint('✅ [AppProvider] Location tracking service initialized');
} catch (e) {
@@ -1141,9 +1190,22 @@ class AppProvider with ChangeNotifier {
}
if (isEmptyChannelInfo) {
// The device channel list is authoritative: an empty slot means
// any locally retained channel in that slot was deleted on the
// device, so drop it from the channel list and the contacts UI.
// Message history is kept (only app-initiated deletes clear it).
debugPrint(
' ⏭️ Ignoring empty channel slot $channelIdx during sync',
' 🧹 Empty channel slot $channelIdx - dropping stale local channel if present',
);
channelsProvider.removeChannel(channelIdx);
final publicKeyBytes = Uint8List(32);
publicKeyBytes[0] = 0xFF; // Special marker for channels
publicKeyBytes[1] = channelIdx; // Channel index
final publicKeyHex = publicKeyBytes
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join('');
contactsProvider.removeContact(publicKeyHex);
return;
}
@@ -1773,7 +1835,13 @@ class AppProvider with ChangeNotifier {
debugPrint(
'🔊 [AppProvider] Echo detected - Message: $messageId, Count: $echoCount',
);
messagesProvider.handleMessageEcho(messageId, echoCount, snrRaw, rssiDbm);
messagesProvider.handleMessageEcho(
messageId,
echoCount,
snrRaw,
rssiDbm,
pathBytes: _latestChannelEchoPathBytes(),
);
};
connectionProvider.prepareDirectMessageSendCallback =
@@ -2012,9 +2080,8 @@ class AppProvider with ChangeNotifier {
final latestContact =
contactsProvider.findContactByKey(contact.publicKey) ?? contact;
final manualSelection = await _pathHistoryService.getManualSelectionForContact(
latestContact,
);
final manualSelection = await _pathHistoryService
.getManualSelectionForContact(latestContact);
final session =
_directMessageRouteSessions[messageId] ??
_DirectMessageRouteSession(
@@ -2542,8 +2609,13 @@ class AppProvider with ChangeNotifier {
// Get battery and storage information
await connectionProvider.getBatteryAndStorage();
// Load contacts (waits for device to finish sending all contacts)
await connectionProvider.getContacts();
// Load contacts (waits for device to finish sending all contacts).
// The device list is authoritative: contacts it no longer has are
// deleted locally once a complete list was received.
final contactListComplete = await connectionProvider.getContacts();
contactsProvider.finalizeDeviceContactSync(
deviceListComplete: contactListComplete,
);
// Sync all channels so slot assignment and channel state mirror the device.
final channelsToSync = connectionProvider.deviceInfo.maxChannels;
@@ -2561,9 +2633,10 @@ class AppProvider with ChangeNotifier {
debugPrint(
'🔄 [AppProvider] Performing initial message sync (fallback for missed pushes)',
);
final initialMessageCount = await connectionProvider.syncAllMessages(
force: true,
);
final initialMessageCount = await messagesProvider
.withReceivedNotificationsSuppressed(
() => connectionProvider.syncAllMessages(force: true),
);
debugPrint(
'📥 [AppProvider] Initial sync retrieved $initialMessageCount message(s)',
);
@@ -2615,15 +2688,19 @@ class AppProvider with ChangeNotifier {
devicePublicKey: connectionProvider.deviceInfo.publicKey,
);
channelsProvider.prepareForDeviceSync();
await connectionProvider.getContacts();
final contactListComplete = await connectionProvider.getContacts();
contactsProvider.finalizeDeviceContactSync(
deviceListComplete: contactListComplete,
);
await connectionProvider.syncChannels(
maxChannels: connectionProvider.deviceInfo.maxChannels,
);
await refreshChannelLocationSharingState();
final messageCount = await connectionProvider.syncAllMessages(
force: true,
);
final messageCount = await messagesProvider
.withReceivedNotificationsSuppressed(
() => connectionProvider.syncAllMessages(force: true),
);
debugPrint(
'📥 [AppProvider] Reconnect sync retrieved $messageCount message(s)',
);
@@ -2850,6 +2927,34 @@ class AppProvider with ChangeNotifier {
}
}
/// Reads the radio's fast-GPS region scope, or `null` when the firmware does
/// not expose region scoping (older builds only know `fast_gps_channel`).
Future<FastGpsRegionState?> getFastGpsRegionState() async {
if (!connectionProvider.deviceInfo.isConnected) {
return null;
}
final vars = await connectionProvider.getCustomVars();
final labelsRaw = vars['fast_gps_regions'];
if (labelsRaw == null || labelsRaw.isEmpty) {
return null;
}
final labels = labelsRaw.split(',');
final index = int.tryParse(vars['fast_gps_region'] ?? '0') ?? 0;
return FastGpsRegionState(
labels: labels,
selectedIndex: (index >= 0 && index < labels.length) ? index : 0,
);
}
/// Sets the radio's fast-GPS region scope by index (0 = unscoped).
Future<void> setFastGpsRegion(int index) async {
if (!connectionProvider.deviceInfo.isConnected) {
throw StateError('Connect to a device first');
}
await _setDeviceCustomVarOrThrow('fast_gps_region', index.toString());
notifyListeners();
}
Future<ChannelLocationSharingState> getChannelLocationSharingState(
int channelIdx,
) async {
@@ -2869,7 +2974,8 @@ class AppProvider with ChangeNotifier {
final sharingMode = channelLocationSharingModeForChannel(channelIdx);
return ChannelLocationSharingState(
mode: sharingMode ??
mode:
sharingMode ??
(_hardwareChannelLocationSharingSupported
? ChannelLocationSharingMode.hardware
: ChannelLocationSharingMode.appFallback),
@@ -2911,7 +3017,9 @@ class AppProvider with ChangeNotifier {
await locationTrackingService.setFastLocationUpdatesEnabled(false);
await refreshChannelLocationSharingState();
if (_hardwareChannelLocationSharingChannelIdx != channelIdx) {
throw StateError('Radio location sharing did not enable for this channel');
throw StateError(
'Radio location sharing did not enable for this channel',
);
}
notifyListeners();
return ChannelLocationSharingResult(
@@ -2921,13 +3029,16 @@ class AppProvider with ChangeNotifier {
hardwareSupported: true,
isConnected: true,
),
message: l10n?.sharingLocationFromRadio ?? 'Sharing location on this channel from the radio.',
message:
l10n?.sharingLocationFromRadio ??
'Sharing location on this channel from the radio.',
);
}
_hardwareChannelLocationSharingSupported = false;
_hardwareChannelLocationSharingChannelIdx = null;
final previousEnabled = locationTrackingService.fastLocationUpdatesEnabled;
final previousEnabled =
locationTrackingService.fastLocationUpdatesEnabled;
final previousChannelIdx = locationTrackingService.fastLocationChannelIdx;
try {
await locationTrackingService.updateFastLocationChannelIdx(channelIdx);
@@ -2952,12 +3063,14 @@ class AppProvider with ChangeNotifier {
state: const ChannelLocationSharingState(
mode: ChannelLocationSharingMode.appFallback,
isSharing: true,
hardwareSupported: false,
isConnected: true,
),
message: l10n?.sharingLocationFromPhone ?? 'Sharing location on this channel from the phone.',
);
}
hardwareSupported: false,
isConnected: true,
),
message:
l10n?.sharingLocationFromPhone ??
'Sharing location on this channel from the phone.',
);
}
final activeHardwareChannelIdx = _hardwareChannelLocationSharingIdxFromVars(
vars,
@@ -2979,7 +3092,9 @@ class AppProvider with ChangeNotifier {
_hardwareChannelLocationSharingChannelIdx = null;
await refreshChannelLocationSharingState();
if (_hardwareChannelLocationSharingChannelIdx == channelIdx) {
throw StateError('Radio location sharing is still enabled for this channel');
throw StateError(
'Radio location sharing is still enabled for this channel',
);
}
notifyListeners();
return ChannelLocationSharingResult(
@@ -2989,7 +3104,9 @@ class AppProvider with ChangeNotifier {
hardwareSupported: true,
isConnected: true,
),
message: l10n?.stoppedSharingLocation ?? 'Stopped sharing location on this channel.',
message:
l10n?.stoppedSharingLocation ??
'Stopped sharing location on this channel.',
);
}
@@ -3003,12 +3120,16 @@ class AppProvider with ChangeNotifier {
hardwareSupported: false,
isConnected: true,
),
message: l10n?.stoppedSharingLocation ?? 'Stopped sharing location on this channel.',
message:
l10n?.stoppedSharingLocation ??
'Stopped sharing location on this channel.',
);
}
Future<void> _sendFastLocationUpdate(
dynamic position, {
Future<void> _sendFastLocationUpdate({
required double latitude,
required double longitude,
required int speedKmh,
required String reason,
}) async {
if (!connectionProvider.deviceInfo.isConnected) {
@@ -3041,20 +3162,21 @@ class AppProvider with ChangeNotifier {
.sublist(0, 6)
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join();
final clampedSpeedKmh = speedKmh.clamp(0, 255);
final packet = FastGpsPacket(
senderKey6: senderKey6,
latitude: position.latitude as double,
longitude: position.longitude as double,
timestampSeconds: DateTime.now().millisecondsSinceEpoch ~/ 1000,
latitude: latitude,
longitude: longitude,
speedKmh: clampedSpeedKmh,
);
debugPrint(
'📤 [AppProvider] Fast GPS send '
'reason=$reason '
'sender=$senderKey6 '
'channel=$channelIdx '
'lat=${position.latitude} '
'lon=${position.longitude} '
'ts=${packet.timestampSeconds}',
'lat=$latitude '
'lon=$longitude '
'speed=${clampedSpeedKmh}km/h',
);
try {
await connectionProvider.sendChannelData(
@@ -3064,7 +3186,7 @@ class AppProvider with ChangeNotifier {
);
debugPrint(
'✅ [AppProvider] Fast GPS sent '
'sender=$senderKey6 channel=$channelIdx ts=${packet.timestampSeconds}',
'sender=$senderKey6 channel=$channelIdx speed=${speedKmh}km/h',
);
} catch (e) {
debugPrint('⚠️ [AppProvider] Fast GPS send failed: $e');
@@ -3084,7 +3206,17 @@ class AppProvider with ChangeNotifier {
return false;
}
await _sendFastLocationUpdate(position, reason: 'test');
// Geolocator reports ground speed in m/s; the beacon carries km/h (0..255).
final speedMs = position.speed;
final speedKmh = (speedMs.isFinite && speedMs > 0)
? (speedMs * 3.6).round().clamp(0, 255)
: 0;
await _sendFastLocationUpdate(
latitude: position.latitude,
longitude: position.longitude,
speedKmh: speedKmh,
reason: 'test',
);
return true;
}
@@ -3626,6 +3758,10 @@ class AppProvider with ChangeNotifier {
return false;
}
// Heard a peer beacon — yield the channel briefly before our own send, to
// avoid collisions (mirrors the firmware's post-RX hold-off).
locationTrackingService.noteFastGpsBeaconHeard();
final sender = _resolveContactByPrefixHex(fastGpsPacket.senderKey6);
if (sender != null) {
contactsProvider.updateFastGps(
@@ -4056,6 +4192,23 @@ class AppProvider with ChangeNotifier {
return decoded.pathBytes;
}
Uint8List? _latestChannelEchoPathBytes() {
for (final log in connectionProvider.bleService.packetLogs.reversed) {
if (log.responseCode != 0x88) continue;
final decoded = LogRxRouteDecoder.decode(log.rawData);
if (decoded == null ||
decoded.payloadType != 0x05 ||
decoded.pathBytes.isEmpty) {
continue;
}
return Uint8List.fromList(decoded.pathBytes);
}
return null;
}
// Removed _syncMessages() - messages are automatically synced via PUSH_CODE_MSG_WAITING events
// The ConnectionProvider's onMessageWaiting callback handles automatic message fetching
@@ -4064,8 +4217,15 @@ class AppProvider with ChangeNotifier {
if (!connectionProvider.deviceInfo.isConnected) return;
try {
// Sync contacts
await connectionProvider.getContacts();
// Sync contacts. The device list is authoritative: contacts it no
// longer has are deleted locally once a complete list was received.
await contactsProvider.prepareForDeviceContactSync(
devicePublicKey: connectionProvider.deviceInfo.publicKey,
);
final contactListComplete = await connectionProvider.getContacts();
contactsProvider.finalizeDeviceContactSync(
deviceListComplete: contactListComplete,
);
// Sync all channels so refresh reflects the full device state.
channelsProvider.prepareForDeviceSync();

View File

@@ -163,6 +163,10 @@ class ConnectionProvider with ChangeNotifier {
// Completer to wait for contacts sync to finish
Completer<void>? _contactsSyncCompleter;
// True only when the device signaled end-of-contacts for the last sync
// (false on timeout or disconnect, i.e. a possibly partial list).
bool _contactsSyncEndedNormally = false;
// Lightweight guards for other commands that can be double-tapped
bool _isLoginInProgress = false;
DateTime? _lastLoginRequestedAt;
@@ -444,6 +448,7 @@ class ConnectionProvider with ChangeNotifier {
service.onContactsComplete = (contacts) {
debugPrint('📥 [Provider] Contacts sync complete: ${contacts.length}');
_isContactsSyncInProgress = false;
_contactsSyncEndedNormally = true;
if (_contactsSyncCompleter != null &&
!_contactsSyncCompleter!.isCompleted) {
_contactsSyncCompleter!.complete();
@@ -992,15 +997,20 @@ class ConnectionProvider with ChangeNotifier {
///
/// Waits for the device to finish sending all contacts (up to 5 s timeout)
/// so callers don't need an arbitrary delay.
Future<void> getContacts() async {
///
/// Returns true only when the device signaled end-of-contacts, i.e. the
/// received list is complete and can be treated as authoritative. Returns
/// false on timeout, disconnect, or error (possibly partial list).
Future<bool> getContacts() async {
if (!_activeService.isConnected) {
_error = 'Not connected to device';
notifyListeners();
return;
return false;
}
try {
_isContactsSyncInProgress = true;
_contactsSyncEndedNormally = false;
_contactsSyncCompleter = Completer<void>();
await _activeService.getContacts();
await _contactsSyncCompleter!.future.timeout(
@@ -1011,10 +1021,12 @@ class ConnectionProvider with ChangeNotifier {
);
},
);
return _contactsSyncEndedNormally;
} catch (e) {
_isContactsSyncInProgress = false;
_error = 'Failed to get contacts: $e';
notifyListeners();
return false;
} finally {
_isContactsSyncInProgress = false;
_contactsSyncCompleter = null;
@@ -1821,15 +1833,23 @@ class ConnectionProvider with ChangeNotifier {
/// Send a raw binary voice packet directly to a contact (cmdSendRawData, code 25).
/// Only works for contacts with a known direct route (outPathLen >= 0).
///
/// Callers pass the contact's *encoded* route descriptor ([routeEncodedPathLen])
/// and encoded [outPath]. The `CMD_SEND_RAW_DATA` handler in companion firmware
/// v1.15 misinterprets encoded descriptors as a literal path-byte count, so we
/// normalise to the legacy raw path format (literal hop count + one byte per
/// hop) before handing the frame to the transport. See
/// [ContactRouteCodec.toLegacyRawPath].
Future<void> sendRawVoicePacket({
required Uint8List contactPath,
required int contactPathLen,
required Uint8List payload,
}) async {
if (!_activeService.isConnected) return;
final legacy = ContactRouteCodec.toLegacyRawPath(contactPathLen, contactPath);
await _activeService.sendRawVoicePacket(
contactPathLen: contactPathLen,
contactPath: contactPath,
contactPathLen: legacy.length,
contactPath: Uint8List.fromList(legacy.path),
payload: payload,
);
}

View File

@@ -303,6 +303,45 @@ class ContactsProvider with ChangeNotifier {
);
}
/// Finish a device contact sync started with [prepareForDeviceContactSync].
///
/// The device contact list is authoritative: when [deviceListComplete] is
/// true, any contact still left in the retained snapshot (i.e. not sent by
/// the device during this sync) is deleted locally. Channel pseudo-contacts
/// are excluded — they mirror device channel slots and are reconciled by the
/// channel sync instead. When the sync was partial (timeout/disconnect),
/// nothing is deleted so a flaky link can't wipe valid contacts.
void finalizeDeviceContactSync({required bool deviceListComplete}) {
if (!deviceListComplete) {
if (_retainedContactsForSync.isNotEmpty) {
debugPrint(
' [ContactsProvider] Partial contact sync - keeping '
'${_retainedContactsForSync.length} unconfirmed contact(s)',
);
}
_retainedContactsForSync.clear();
return;
}
final staleKeys = _retainedContactsForSync.keys
.where((key) => _contacts[key]?.type != ContactType.channel)
.where(_contacts.containsKey)
.toList();
for (final key in staleKeys) {
final removed = _contacts.remove(key);
debugPrint(
'🗑️ [ContactsProvider] Removing contact no longer on device: '
'${removed?.advName} (${key.substring(0, 8)}...)',
);
}
_retainedContactsForSync.clear();
if (staleKeys.isNotEmpty) {
_persistContacts();
notifyListeners();
}
}
/// Remove self-contact from loaded contacts (called after BLE connection established)
void _removeSelfContact(Uint8List devicePublicKey) {
final selfKeyHex = devicePublicKey
@@ -386,6 +425,10 @@ class ContactsProvider with ChangeNotifier {
ContactTelemetry? get selfTelemetry => _selfTelemetry;
List<Contact> get favouriteContacts =>
_contacts.values.where((c) => c.isFavourite).toList();
List<Contact> get favouriteChatContacts =>
chatContacts.where((c) => c.isFavourite).toList();
List<Contact> get favouriteContactsWithLocation =>
favouriteContacts.where((c) => effectiveLocationFor(c) != null).toList();
List<SavedContactGroup> get savedContactGroups =>
List<SavedContactGroup>.from(_savedContactGroups)
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
@@ -1121,6 +1164,10 @@ class ContactsProvider with ChangeNotifier {
return;
}
// The fast-GPS beacon carries no timestamp — stamp our own receive time.
final receivedAt = DateTime.now();
final receivedAtSeconds = receivedAt.millisecondsSinceEpoch ~/ 1000;
final updatedTelemetry = _mergeTelemetryForContact(
existingTelemetry: contact.telemetry,
incomingTelemetry: ContactTelemetry(
@@ -1128,9 +1175,7 @@ class ContactsProvider with ChangeNotifier {
batteryPercentage: null,
batteryMilliVolts: null,
temperature: null,
timestamp: DateTime.fromMillisecondsSinceEpoch(
packet.timestampSeconds * 1000,
),
timestamp: receivedAt,
humidity: null,
pressure: null,
extraSensorData: null,
@@ -1144,13 +1189,13 @@ class ContactsProvider with ChangeNotifier {
'contactKey=${contact.publicKeyHex} '
'lat=${packet.latitude} '
'lon=${packet.longitude} '
'ts=${packet.timestampSeconds}',
'speed=${packet.speedKmh}km/h',
);
final updatedContact = contact.copyWith(
telemetry: updatedTelemetry,
lastAdvert: packet.timestampSeconds,
lastMod: packet.timestampSeconds,
lastAdvert: receivedAtSeconds,
lastMod: receivedAtSeconds,
advLat: _coordinateToAdvertMicrodegrees(packet.latitude),
advLon: _coordinateToAdvertMicrodegrees(packet.longitude),
);
@@ -1263,11 +1308,17 @@ class ContactsProvider with ChangeNotifier {
Map<String, dynamic>? existingExtraSensorData,
Map<String, dynamic>? incomingExtraSensorData,
) {
final merged = <String, dynamic>{...?existingExtraSensorData};
if (incomingExtraSensorData == null || incomingExtraSensorData.isEmpty) {
return merged;
return <String, dynamic>{...?existingExtraSensorData};
}
final isFullTelemetryRefresh = incomingExtraSensorData.containsKey(
_rawTelemetryHexKey,
);
final merged = <String, dynamic>{
if (!isFullTelemetryRefresh) ...?existingExtraSensorData,
};
final incomingMetricFamilies = incomingExtraSensorData.keys
.map(_telemetryMetricFamilyForKey)
.whereType<String>()

View File

@@ -9,6 +9,11 @@ typedef RawPacketSender =
required Uint8List payload,
});
/// Pacing between consecutive raw fragments. Firmware v1.15 rejects bursts of
/// raw packets with `ERROR: Table full` when fragments are blasted too quickly,
/// so we space them out to keep the radio's raw transmit queue from overflowing.
const Duration _interFragmentDelay = Duration(milliseconds: 350);
Future<bool> serveCachedSessionFragments<T>({
required String providerLabel,
required String sessionId,
@@ -19,6 +24,7 @@ Future<bool> serveCachedSessionFragments<T>({
required Uint8List Function(T fragment) encodeBinary,
required RawPacketSender? sendRawPacket,
Set<int>? requestedIndices,
Duration interFragmentDelay = _interFragmentDelay,
}) async {
if (fragments.isEmpty) {
debugPrint('⚠️ [$providerLabel] No cached fragments for $sessionId');
@@ -56,6 +62,9 @@ Future<bool> serveCachedSessionFragments<T>({
continue;
}
try {
if (servedCount > 0 && interFragmentDelay > Duration.zero) {
await Future<void>.delayed(interFragmentDelay);
}
await sendRawPacket(
contactPath: requester.outPath,
contactPathLen: requester.routeEncodedPathLen,

View File

@@ -39,6 +39,7 @@ class MapProvider with ChangeNotifier {
LocationTrail? _currentTrail;
bool _isTrailVisible = true;
bool _isTrailRecordingEnabled = true;
final List<LocationTrail> _trailHistory = [];
bool _showCadastralOverlay = false;
@@ -71,6 +72,7 @@ class MapProvider with ChangeNotifier {
String? get targetMapId => _targetMapId;
LocationTrail? get currentTrail => _currentTrail;
bool get isTrailVisible => _isTrailVisible;
bool get isTrailRecordingEnabled => _isTrailRecordingEnabled;
List<LocationTrail> get trailHistory => List.unmodifiable(_trailHistory);
bool get isTrailActive => _currentTrail?.isActive ?? false;
@@ -373,6 +375,10 @@ class MapProvider with ChangeNotifier {
}
void addTrailPoint(LatLng position, {double? accuracy, double? speed}) {
if (!_isTrailRecordingEnabled) {
return;
}
if (_currentTrail == null || !_currentTrail!.isActive) {
startTrail();
}
@@ -405,6 +411,24 @@ class MapProvider with ChangeNotifier {
notifyListeners();
}
void setTrailRecordingEnabled(bool enabled) {
if (_isTrailRecordingEnabled == enabled) {
return;
}
_isTrailRecordingEnabled = enabled;
if (!enabled) {
if (_currentTrail != null) {
endTrail();
} else {
notifyListeners();
}
return;
}
notifyListeners();
}
void clearCurrentTrail() {
if (_currentTrail != null) {
_currentTrail = null;
@@ -698,6 +722,7 @@ class MapProvider with ChangeNotifier {
'trailHistory': _trailHistory.map((trail) => trail.toJson()).toList(),
'importedTrail': _importedTrail?.toJson(),
'isTrailVisible': _isTrailVisible,
'isTrailRecordingEnabled': _isTrailRecordingEnabled,
'showCadastralOverlay': _showCadastralOverlay,
'showForestRoadsOverlay': _showForestRoadsOverlay,
'showHikingTrailsOverlay': _showHikingTrailsOverlay,
@@ -728,6 +753,8 @@ class MapProvider with ChangeNotifier {
? LocationTrail.fromJson(json['importedTrail'] as Map<String, dynamic>)
: null;
_isTrailVisible = json['isTrailVisible'] as bool? ?? true;
_isTrailRecordingEnabled =
json['isTrailRecordingEnabled'] as bool? ?? true;
_showCadastralOverlay = json['showCadastralOverlay'] as bool? ?? false;
_showForestRoadsOverlay = json['showForestRoadsOverlay'] as bool? ?? false;
_showHikingTrailsOverlay =

View File

@@ -36,6 +36,7 @@ class MessagesProvider with ChangeNotifier {
bool _isInitialized = false;
bool _isPersisting = false;
bool _persistRequested = false;
bool _suppressReceivedNotifications = false;
AppLocalizations? _localizations;
final Map<String, MessageContactLocation> _messageContactLocations = {};
final Map<String, MessageReceptionDetails> _messageReceptionDetails = {};
@@ -198,6 +199,9 @@ class MessagesProvider with ChangeNotifier {
_messages[index] = _messages[index].copyWith(
usedFloodFallback: selection.usesFlood,
pathLen: nextPathLen,
pathBytes: selection.hasDirectPath
? Uint8List.fromList(selection.pathBytes)
: Uint8List(0),
);
}
@@ -803,15 +807,30 @@ class MessagesProvider with ChangeNotifier {
if (contactLocationSnapshot != null) {
_messageContactLocations[existingId] = contactLocationSnapshot;
}
_messageReceptionDetails[existingId] =
MessageReceptionDetails.mergeDuplicate(
existing: _messageReceptionDetails[existingId],
incoming: receptionDetailsSnapshot,
);
final mergedReceptionDetails = MessageReceptionDetails.mergeDuplicate(
existing: _messageReceptionDetails[existingId],
incoming: receptionDetailsSnapshot,
);
_messageReceptionDetails[existingId] = mergedReceptionDetails;
final existingMessage = _messages[matchingSentReplayIndex];
final routeMetadata = _messageRouteMetadata[existingId];
_messages[matchingSentReplayIndex] = existingMessage.copyWith(
pathLen: finalMessage.pathLen > 0 ? finalMessage.pathLen : existingMessage.pathLen,
pathBytes: finalMessage.pathBytes ?? existingMessage.pathBytes,
echoCount: _mergeSentReplayEchoCount(
existingMessage,
mergedReceptionDetails,
),
pathLen: _mergeSentReplayPathLen(
existingMessage,
finalMessage,
routeMetadata,
),
pathBytes: _mergeSentReplayPathBytes(
existingMessage,
finalMessage,
routeMetadata,
),
firstEchoAt: existingMessage.firstEchoAt ?? DateTime.now(),
lastEchoAt: DateTime.now(),
);
_persistMessages();
return;
@@ -832,11 +851,13 @@ class MessagesProvider with ChangeNotifier {
_sarMarkers[marker.id] = marker;
// Trigger urgent notification for received SAR messages (not sent by user)
if (!finalMessage.isSentMessage) {
if (!finalMessage.isSentMessage && !_suppressReceivedNotifications) {
_triggerSarNotification(finalMessage, marker);
}
}
} else if (!finalMessage.isSentMessage && !finalMessage.isSystemMessage) {
} else if (!finalMessage.isSentMessage &&
!finalMessage.isSystemMessage &&
!_suppressReceivedNotifications) {
// Trigger notification for regular messages (not SAR, not sent by user, not system)
_triggerMessageNotification(finalMessage);
}
@@ -847,6 +868,18 @@ class MessagesProvider with ChangeNotifier {
notifyListeners();
}
Future<T> withReceivedNotificationsSuppressed<T>(
Future<T> Function() action,
) async {
final previous = _suppressReceivedNotifications;
_suppressReceivedNotifications = true;
try {
return await action();
} finally {
_suppressReceivedNotifications = previous;
}
}
/// Check if a message is a duplicate
///
/// Messages are considered duplicates if they have:
@@ -1032,6 +1065,51 @@ class MessagesProvider with ChangeNotifier {
return _matchesDuplicateSenderIdentity(existing, message);
}
int _mergeSentReplayEchoCount(
Message existing,
MessageReceptionDetails mergedReceptionDetails,
) {
final replayCount = mergedReceptionDetails.receivedCopies > 0
? mergedReceptionDetails.receivedCopies - 1
: 0;
return replayCount > existing.echoCount ? replayCount : existing.echoCount;
}
int _mergeSentReplayPathLen(
Message existing,
Message incoming,
MessageRouteMetadata? routeMetadata,
) {
if (routeMetadata?.mode == PathSelectionMode.flood ||
existing.usedFloodFallback) {
return existing.pathLen;
}
final routeHopCount = routeMetadata?.hopCount;
if (routeHopCount != null && routeHopCount > 0) {
return routeHopCount;
}
if (existing.pathLen > 0) {
return existing.pathLen;
}
return incoming.pathLen > 0 ? incoming.pathLen : existing.pathLen;
}
Uint8List? _mergeSentReplayPathBytes(
Message existing,
Message incoming,
MessageRouteMetadata? routeMetadata,
) {
if (routeMetadata?.mode == PathSelectionMode.flood ||
existing.usedFloodFallback) {
return existing.pathBytes;
}
return existing.pathBytes ?? incoming.pathBytes;
}
/// Add multiple messages
void addMessages(List<Message> messages) {
int addedCount = 0;
@@ -2166,7 +2244,9 @@ class MessagesProvider with ChangeNotifier {
int echoCount,
int snrRaw,
int rssiDbm,
) {
{
Uint8List? pathBytes,
}) {
debugPrint('🔊 [MessagesProvider] handleMessageEcho called');
debugPrint(' Message ID: $messageId');
debugPrint(' Echo count: $echoCount');
@@ -2181,18 +2261,35 @@ class MessagesProvider with ChangeNotifier {
' ✅ Found message: ${message.text.substring(0, message.text.length > 30 ? 30 : message.text.length)}...',
);
final nextEchoCount = echoCount > message.echoCount
? echoCount
: message.echoCount + 1;
// Update echo count
final updatedMessage = message.copyWith(
echoCount: echoCount,
echoCount: nextEchoCount,
firstEchoAt: message.firstEchoAt ?? DateTime.now(),
lastEchoSnrRaw: snrRaw.toSigned(8),
lastEchoRssiDbm: rssiDbm.toSigned(8),
lastEchoAt: DateTime.now(),
);
_messages[index] = updatedMessage;
_messageReceptionDetails[messageId] = _messageReceptionDetails[messageId]
?.copyWith(
capturedAt: DateTime.now(),
rssiDbm: rssiDbm.toSigned(8),
snrDb: snrRaw.toSigned(8) / 4.0,
pathBytes: pathBytes?.toList(),
) ??
MessageReceptionDetails(
capturedAt: DateTime.now(),
rssiDbm: rssiDbm.toSigned(8),
snrDb: snrRaw.toSigned(8) / 4.0,
pathBytes: pathBytes?.toList(),
);
_clearChannelSendWarning(messageId);
debugPrint(' Updated echo count to: $echoCount');
debugPrint(' Updated echo count to: $nextEchoCount');
_persistMessages();
notifyListeners();
debugPrint(' ✅ Echo update complete, UI notified');

View File

@@ -56,9 +56,11 @@ class OfflineTilesProvider extends ChangeNotifier {
TileDownloadService? _downloadService;
StreamSubscription<TileDownloadEvent>? _downloadSubscription;
StreamSubscription<Set<TilePeer>>? _peersSubscription;
bool _isDisposed = false;
// Drawing state
DrawingMode _drawingMode = DrawingMode.none;
DrawingMode _downloadSelectionMode = DrawingMode.none;
final List<List<LatLng>> _polygons = [];
List<LatLng> _currentVertices = [];
LatLng? _rectangleFirstCorner;
@@ -94,6 +96,7 @@ class OfflineTilesProvider extends ChangeNotifier {
// Getters
DrawingMode get drawingMode => _drawingMode;
DrawingMode get downloadSelectionMode => _downloadSelectionMode;
List<List<LatLng>> get polygons => List.unmodifiable(_polygons);
List<LatLng> get currentVertices => List.unmodifiable(_currentVertices);
LatLng? get rectangleFirstCorner => _rectangleFirstCorner;
@@ -131,6 +134,15 @@ class OfflineTilesProvider extends ChangeNotifier {
notifyListeners();
}
void startDownloadSelectionMode(DrawingMode mode) {
_polygons.clear();
_currentVertices = [];
_rectangleFirstCorner = null;
_downloadSelectionMode = mode;
_drawingMode = mode;
notifyListeners();
}
void addVertex(LatLng point) {
if (_drawingMode == DrawingMode.polygon) {
_currentVertices = [..._currentVertices, point];
@@ -181,6 +193,27 @@ class OfflineTilesProvider extends ChangeNotifier {
notifyListeners();
}
void setCurrentViewBounds({
required double north,
required double south,
required double east,
required double west,
}) {
_polygons
..clear()
..add([
LatLng(north, west),
LatLng(north, east),
LatLng(south, east),
LatLng(south, west),
]);
_currentVertices = [];
_rectangleFirstCorner = null;
_downloadSelectionMode = DrawingMode.none;
_drawingMode = DrawingMode.none;
notifyListeners();
}
void undoLastVertex() {
if (_currentVertices.isNotEmpty) {
_currentVertices = _currentVertices.sublist(0, _currentVertices.length - 1);
@@ -207,6 +240,15 @@ class OfflineTilesProvider extends ChangeNotifier {
notifyListeners();
}
void setSelectedLayerIfDifferent(MapLayer layer) {
if (_selectedLayer.type == layer.type &&
_selectedLayer.urlTemplate == layer.urlTemplate) {
return;
}
_selectedLayer = layer;
notifyListeners();
}
// Download control
Future<void> startDownload() async {
@@ -224,6 +266,7 @@ class OfflineTilesProvider extends ChangeNotifier {
maxZoom: _maxZoom,
urlTemplate: _selectedLayer.urlTemplate,
displayName: _selectedLayer.name,
headers: _selectedLayer.headers,
);
await for (final event in stream) {
@@ -273,10 +316,12 @@ class OfflineTilesProvider extends ChangeNotifier {
case TileDownloadComplete():
_isDownloading = false;
_tileOverlays.clear();
notifyListeners();
case TileDownloadCancelled():
_isDownloading = false;
_tileOverlays.clear();
notifyListeners();
}
}
@@ -341,19 +386,12 @@ class OfflineTilesProvider extends ChangeNotifier {
_coverageOverlays = [];
notifyListeners();
final manifest = await _cache.loadManifest(style.hash);
final tiles = await _cache.listTilesForStyle(style.hash);
// Convert manifest keys to tile bound overlays
// Convert cached tile coordinates to bound overlays
final overlays = <TileOverlay>[];
for (final key in manifest) {
final parts = key.split('/');
if (parts.length != 3) continue;
final z = int.tryParse(parts[0]);
final x = int.tryParse(parts[1]);
final y = int.tryParse(parts[2]);
if (z == null || x == null || y == null) continue;
final bounds = TileMathService.tileBounds(x, y, z);
for (final tile in tiles) {
final bounds = TileMathService.tileBounds(tile.x, tile.y, tile.z);
overlays.add(TileOverlay(
north: bounds.north,
south: bounds.south,
@@ -400,7 +438,9 @@ class OfflineTilesProvider extends ChangeNotifier {
_peersSubscription = null;
await _sharing.stopPeerDiscovery();
_discoveredPeers = {};
notifyListeners();
if (!_isDisposed) {
notifyListeners();
}
}
void addManualPeer(String ipAddress) {
@@ -533,6 +573,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();
@@ -531,11 +536,13 @@ class _ContactsTabState extends State<ContactsTab> {
) async {
final channelIdx = channel.publicKey.length > 1 ? channel.publicKey[1] : 0;
try {
final result = await context.read<AppProvider>().setChannelLocationSharingEnabled(
channelIdx,
enabled,
l10n: AppLocalizations.of(context),
);
final result = await context
.read<AppProvider>()
.setChannelLocationSharingEnabled(
channelIdx,
enabled,
l10n: AppLocalizations.of(context),
);
if (!context.mounted) return;
ToastLogger.success(context, result.message);
} catch (error) {
@@ -572,7 +579,11 @@ class _ContactsTabState extends State<ContactsTab> {
.read<AppProvider>()
.getChannelLocationSharingState(channelIdx);
if (!context.mounted) return;
await _setChannelLocationSharing(context, channel, !sharingState.isSharing);
await _setChannelLocationSharing(
context,
channel,
!sharingState.isSharing,
);
} catch (error) {
if (!context.mounted) return;
ToastLogger.error(context, _locationSharingErrorMessage(error));
@@ -586,9 +597,9 @@ class _ContactsTabState extends State<ContactsTab> {
);
final channelLocationSharingFuture =
!channel.isPublicChannel && channel.publicKey.length > 1
? context
.read<AppProvider>()
.getChannelLocationSharingState(channel.publicKey[1])
? context.read<AppProvider>().getChannelLocationSharingState(
channel.publicKey[1],
)
: null;
showModalBottomSheet<void>(
@@ -1443,7 +1454,9 @@ class _ContactsTabState extends State<ContactsTab> {
height: 1.1,
),
decoration: InputDecoration(
hintText: AppLocalizations.of(context)!.searchThisSection,
hintText: AppLocalizations.of(
context,
)!.searchThisSection,
hintStyle: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant.withValues(
alpha: 0.85,
@@ -1818,81 +1831,90 @@ class _InferredContactGroupCard extends StatelessWidget {
return Container(
margin: const EdgeInsets.only(bottom: 10),
decoration: BoxDecoration(
child: Material(
color: colorScheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: colorScheme.outlineVariant.withValues(alpha: 0.35),
),
),
child: Theme(
data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
tilePadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 0),
childrenPadding: const EdgeInsets.fromLTRB(10, 0, 10, 6),
minTileHeight: 44,
initiallyExpanded: false,
leading: Icon(
Icons.folder_copy_outlined,
size: 16,
color: colorScheme.primary,
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(
color: colorScheme.outlineVariant.withValues(alpha: 0.35),
),
title: Row(
children: [
Expanded(
child: Text(
label,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w800,
fontSize: 13,
),
child: Theme(
data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
tilePadding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 0,
),
childrenPadding: const EdgeInsets.fromLTRB(10, 0, 10, 6),
minTileHeight: 44,
initiallyExpanded: false,
leading: Icon(
Icons.folder_copy_outlined,
size: 16,
color: colorScheme.primary,
),
title: Row(
children: [
Expanded(
child: Text(
label,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w800,
fontSize: 13,
),
),
),
),
const SizedBox(width: 6),
Container(
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 1),
decoration: BoxDecoration(
color: colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(999),
),
child: Text(
contacts.length.toString(),
style: Theme.of(context).textTheme.labelSmall,
),
),
if (onDelete != null) ...[
const SizedBox(width: 2),
IconButton(
tooltip: AppLocalizations.of(context)!.deleteGroup,
onPressed: onDelete,
visualDensity: VisualDensity.compact,
constraints: const BoxConstraints.tightFor(
width: 32,
height: 32,
const SizedBox(width: 6),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 7,
vertical: 1,
),
padding: EdgeInsets.zero,
icon: Icon(
Icons.delete_outline_rounded,
size: 16,
color: colorScheme.error,
decoration: BoxDecoration(
color: colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(999),
),
child: Text(
contacts.length.toString(),
style: Theme.of(context).textTheme.labelSmall,
),
),
if (onDelete != null) ...[
const SizedBox(width: 2),
IconButton(
tooltip: AppLocalizations.of(context)!.deleteGroup,
onPressed: onDelete,
visualDensity: VisualDensity.compact,
constraints: const BoxConstraints.tightFor(
width: 32,
height: 32,
),
padding: EdgeInsets.zero,
icon: Icon(
Icons.delete_outline_rounded,
size: 16,
color: colorScheme.error,
),
),
],
],
),
children: [
...contacts.map(
(contact) => ContactTile(
contact: contact,
compact: compactContacts,
currentPosition: currentPosition,
calculateDistance: calculateDistance,
formatDistance: formatDistance,
onNavigateToMap: onNavigateToMap,
onNavigateToMessages: onNavigateToMessages,
),
),
],
),
children: [
...contacts.map(
(contact) => ContactTile(
contact: contact,
compact: compactContacts,
currentPosition: currentPosition,
calculateDistance: calculateDistance,
formatDistance: formatDistance,
onNavigateToMap: onNavigateToMap,
onNavigateToMessages: onNavigateToMessages,
),
),
],
),
),
);
@@ -1935,11 +1957,7 @@ class _ChannelActivityCard extends StatelessWidget {
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
),
child: Icon(
Icons.location_pin,
size: 11,
color: Colors.white,
),
child: Icon(Icons.location_pin, size: 11, color: Colors.white),
);
}
@@ -2594,8 +2612,7 @@ class _ContactsRegionScopeSheet extends StatefulWidget {
_ContactsRegionScopeSheetState();
}
class _ContactsRegionScopeSheetState
extends State<_ContactsRegionScopeSheet> {
class _ContactsRegionScopeSheetState extends State<_ContactsRegionScopeSheet> {
final TextEditingController _nameController = TextEditingController();
@override

View File

@@ -550,6 +550,37 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
}
}
bool get _hasUnsavedChanges =>
_radioSettingsDirty || _autoDiscoverySettingsDirty;
Future<void> _handlePopInvoked(bool didPop, Object? result) async {
if (didPop) return;
final discard = await showDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
title: const Text('Discard unsaved changes?'),
content: const Text(
'You have changes that have not been saved to the device.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(false),
child: const Text('Keep editing'),
),
FilledButton(
onPressed: () => Navigator.of(dialogContext).pop(true),
child: const Text('Discard'),
),
],
),
);
if (discard == true && mounted) {
Navigator.of(context).pop();
}
}
String _autoDiscoverySignature(DeviceInfo deviceInfo) {
return [
deviceInfo.manualAddContacts,
@@ -766,6 +797,78 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
}
}
Future<bool> _confirmApplyRadioSettings({
required int freqKhz,
required int bandwidth,
required int spreadingFactor,
required int codingRate,
required int txPower,
}) async {
final deviceInfo = context.read<ConnectionProvider>().deviceInfo;
final oldFreqKhz = deviceInfo.radioFreq;
final oldBandwidth = _normalizeBandwidthValue(deviceInfo.radioBw);
final oldSpreadingFactor = _normalizeSpreadingFactor(deviceInfo.radioSf);
final oldCodingRate = _normalizeCodingRate(deviceInfo.radioCr);
final oldTxPower = deviceInfo.txPower;
String formatFreq(int? khz) =>
khz != null ? (khz / 1000).toStringAsFixed(3) : '';
final changes = <String>[
if (oldFreqKhz != freqKhz)
'${AppLocalizations.of(context)!.frequencyMHz}: ${formatFreq(oldFreqKhz)}${formatFreq(freqKhz)}',
if (oldBandwidth != bandwidth)
'${AppLocalizations.of(context)!.bandwidth}: ${oldBandwidth != null ? _bandwidthFromValue(oldBandwidth) : ''}${_bandwidthFromValue(bandwidth)}',
if (oldSpreadingFactor != spreadingFactor)
'${AppLocalizations.of(context)!.spreadingFactor}: ${oldSpreadingFactor ?? ''}$spreadingFactor',
if (oldCodingRate != codingRate)
'${AppLocalizations.of(context)!.codingRate}: ${oldCodingRate ?? ''}$codingRate',
if (oldTxPower != txPower)
'${AppLocalizations.of(context)!.txPowerDbm}: ${oldTxPower ?? ''}$txPower',
];
if (changes.isEmpty) {
return true;
}
final confirmed = await showDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
title: const Text('Apply radio settings?'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (final change in changes)
Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Text(change),
),
const SizedBox(height: 8),
Text(
'Changing these settings will disconnect you from any mesh nodes not using the same settings.',
style: TextStyle(
color: Theme.of(dialogContext).colorScheme.error,
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(false),
child: Text(AppLocalizations.of(context)!.cancel),
),
FilledButton(
onPressed: () => Navigator.of(dialogContext).pop(true),
child: const Text('Apply'),
),
],
),
);
return confirmed == true;
}
Future<void> _saveRadioSettings() async {
final connectionProvider = context.read<ConnectionProvider>();
final validator = ValidationService();
@@ -808,6 +911,24 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
// Convert from MHz to kHz for protocol
final freqKhz = (freqResult.value! * 1000).round();
// Confirm before applying — wrong radio params knock this node off
// the mesh shared with the rest of the team.
final confirmed = await _confirmApplyRadioSettings(
freqKhz: freqKhz,
bandwidth: _bandwidthToValue(_selectedBandwidth),
spreadingFactor: _selectedSpreadingFactor,
codingRate: _selectedCodingRate,
txPower: txPowerResult.value!,
);
if (!confirmed || !mounted) {
if (mounted) {
setState(() {
_isSavingRadioSettings = false;
});
}
return;
}
await connectionProvider.setRadioParams(
frequency: freqKhz,
bandwidth: _bandwidthToValue(_selectedBandwidth),
@@ -1096,29 +1217,57 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
}
Future<void> _confirmFactoryReset() async {
final deviceInfo = context.read<ConnectionProvider>().deviceInfo;
final deviceName = (deviceInfo.selfName ?? deviceInfo.deviceName)?.trim();
final confirmationWord = (deviceName == null || deviceName.isEmpty)
? 'WIPE'
: deviceName;
final confirmationController = TextEditingController();
final confirmed = await showDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(AppLocalizations.of(context)!.wipeDeviceData),
content: const Text(
'This will erase all data on the connected device, including contacts, keys, and saved settings. This cannot be undone.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(false),
child: Text(AppLocalizations.of(context)!.cancel),
builder: (dialogContext) => StatefulBuilder(
builder: (dialogContext, setDialogState) => AlertDialog(
title: Text(AppLocalizations.of(context)!.wipeDeviceData),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'This will erase all data on the connected device, including contacts, keys, and saved settings. This cannot be undone.',
),
const SizedBox(height: 12),
Text('Type "$confirmationWord" to confirm:'),
const SizedBox(height: 8),
TextField(
controller: confirmationController,
autofocus: true,
decoration: InputDecoration(hintText: confirmationWord),
onChanged: (_) => setDialogState(() {}),
),
],
),
FilledButton(
style: FilledButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.error,
foregroundColor: Theme.of(context).colorScheme.onError,
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(false),
child: Text(AppLocalizations.of(context)!.cancel),
),
onPressed: () => Navigator.of(dialogContext).pop(true),
child: Text(AppLocalizations.of(context)!.wipeDevice),
),
],
FilledButton(
style: FilledButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.error,
foregroundColor: Theme.of(context).colorScheme.onError,
),
onPressed:
confirmationController.text.trim() == confirmationWord
? () => Navigator.of(dialogContext).pop(true)
: null,
child: Text(AppLocalizations.of(context)!.wipeDevice),
),
],
),
),
);
confirmationController.dispose();
if (confirmed != true || !mounted) return;
@@ -1367,7 +1516,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
final colorScheme = theme.colorScheme;
final locationSet = _advertLocationPolicy != 0;
return Scaffold(
final scaffold = Scaffold(
appBar: AppBar(title: Text(AppLocalizations.of(context)!.deviceSettings)),
body: ColoredBox(
color: colorScheme.surface,
@@ -2123,9 +2272,8 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
onChanged: (value) {
setState(() {
_repeatEnabled = value;
_radioSettingsSaved = false;
_radioSettingsError = null;
});
_markRadioSettingsDirty();
},
),
),
@@ -2375,6 +2523,12 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
),
),
);
return PopScope(
canPop: !_hasUnsavedChanges,
onPopInvokedWithResult: _handlePopInvoked,
child: scaffold,
);
}
String _getDeviceTypeString(BuildContext context, int? deviceType) {

View File

@@ -100,10 +100,21 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
});
try {
await context.read<ConnectionProvider>().discoverNodeType(
advertType: advertType,
);
final connectionProvider = context.read<ConnectionProvider>();
connectionProvider.clearError();
await connectionProvider.discoverNodeType(advertType: advertType);
if (!mounted) return;
final discoveryError = connectionProvider.error;
if (discoveryError != null) {
connectionProvider.clearError();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(discoveryError),
backgroundColor: Colors.red,
),
);
return;
}
final l10n = AppLocalizations.of(context)!;
final label = switch (advertType) {
_repeaterAdvertType => l10n.repeaterDiscoverySent,
@@ -153,8 +164,10 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
return;
}
} else {
connectionProvider.clearError();
await connectionProvider.getContact(advert.publicKey);
if (connectionProvider.error == 'Not found') {
final resolveError = connectionProvider.error;
if (resolveError == 'Not found') {
connectionProvider.clearError();
final added = await _addPendingAdvertToRadio(
advert,
@@ -164,6 +177,23 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
if (!added) {
return;
}
} else if (resolveError != null) {
// Other failures (timeout, disconnect, ...): surface the error and
// leave the advert pending so the user can retry.
connectionProvider.clearError();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(
context,
)!.failedToAddContact(resolveError),
),
backgroundColor: Colors.red,
),
);
}
return;
}
}
if ((advert.typeValue ?? 0) == _sensorAdvertType) {
@@ -248,10 +278,12 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
required ContactsProvider contactsProvider,
}) async {
final contact = _contactFromPendingAdvert(advert);
connectionProvider.clearError();
await connectionProvider.addOrUpdateContact(contact);
final addError = connectionProvider.error;
if (addError != null) {
connectionProvider.clearError();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context)!.failedToAddContact(addError.toString()))),

View File

@@ -1,11 +1,8 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:geolocator/geolocator.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:vibration/vibration.dart';
import '../providers/connection_provider.dart';
import '../providers/app_provider.dart';
import '../models/device_info.dart' show ConnectionMode, DeviceInfo;
@@ -24,7 +21,6 @@ import 'device_config_screen.dart';
import 'packet_log_screen.dart';
import 'live_traffic_screen.dart';
import 'profiles_screen.dart';
import '../utils/toast_logger.dart';
import '../l10n/app_localizations.dart';
import '../widgets/permission_request_dialog.dart';
import '../widgets/connection_dialog.dart';
@@ -37,8 +33,6 @@ import '../services/profiles_feature_service.dart';
enum _HomeTab { messages, contacts, sensors, map }
enum _AdvertMode { flood, direct }
class HomeScreen extends StatefulWidget {
final Function(AppThemeMode) onThemeChanged;
final Function(Locale?) onLocaleChanged;
@@ -70,11 +64,15 @@ class _HomeScreenState extends State<HomeScreen>
bool _isMapEnabled = true;
bool _isContactsEnabled = true;
bool _isSensorsEnabled = false;
bool _isSimpleMode = false;
AppLifecycleState _lifecycleState = AppLifecycleState.resumed;
String? _lastProfileDeviceKey;
Timer? _sensorAutoRefreshTicker;
List<_HomeTab> get _enabledTabs {
if (_isSimpleMode) {
return const [_HomeTab.messages, _HomeTab.map];
}
return [
_HomeTab.messages,
if (_isContactsEnabled) _HomeTab.contacts,
@@ -101,6 +99,7 @@ class _HomeScreenState extends State<HomeScreen>
_isMapEnabled = _appProvider.isMapEnabled;
_isContactsEnabled = _appProvider.isContactsEnabled;
_isSensorsEnabled = _appProvider.isSensorsEnabled;
_isSimpleMode = _appProvider.isSimpleMode;
_appProvider.addListener(_handleAppProviderChanged);
// Initialize synchronously so first build always has a valid controller.
@@ -137,6 +136,7 @@ class _HomeScreenState extends State<HomeScreen>
mapEnabled: _appProvider.isMapEnabled,
contactsEnabled: _appProvider.isContactsEnabled,
sensorsEnabled: _appProvider.isSensorsEnabled,
simpleMode: _appProvider.isSimpleMode,
);
}
@@ -161,10 +161,12 @@ class _HomeScreenState extends State<HomeScreen>
required bool mapEnabled,
required bool contactsEnabled,
required bool sensorsEnabled,
required bool simpleMode,
}) {
if (_isMapEnabled == mapEnabled &&
_isContactsEnabled == contactsEnabled &&
_isSensorsEnabled == sensorsEnabled) {
_isSensorsEnabled == sensorsEnabled &&
_isSimpleMode == simpleMode) {
return;
}
@@ -182,6 +184,7 @@ class _HomeScreenState extends State<HomeScreen>
_isMapEnabled = mapEnabled;
_isContactsEnabled = contactsEnabled;
_isSensorsEnabled = sensorsEnabled;
_isSimpleMode = simpleMode;
if (!_isMapEnabled) {
_isMapFullscreen = false;
}
@@ -359,113 +362,6 @@ class _HomeScreenState extends State<HomeScreen>
);
}
Future<void> _triggerAdvertFeedback() async {
final platform = Theme.of(context).platform;
try {
if (platform == TargetPlatform.iOS) {
await HapticFeedback.lightImpact();
await Future.delayed(const Duration(milliseconds: 50));
await HapticFeedback.lightImpact();
} else {
if (await Vibration.hasVibrator()) {
await Vibration.vibrate(duration: 50);
} else {
await HapticFeedback.mediumImpact();
}
}
} catch (e) {
debugPrint('Haptic feedback error: $e');
await HapticFeedback.vibrate();
}
}
Future<_AdvertMode?> _showAdvertModeSheet(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final theme = Theme.of(context);
return showModalBottomSheet<_AdvertMode>(
context: context,
showDragHandle: true,
builder: (context) => SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 8, 20, 20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Advert mode',
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 6),
Text(
'Choose how far this announcement should travel.',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 16),
ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 4,
vertical: 4,
),
leading: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: theme.colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(14),
),
child: Icon(
Icons.hub_rounded,
color: theme.colorScheme.onPrimaryContainer,
),
),
title: Text(l10n.flood),
subtitle: Text(l10n.relayThroughRepeatersAcrossTheMesh),
trailing: const Icon(Icons.chevron_right_rounded),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
onTap: () => Navigator.of(context).pop(_AdvertMode.flood),
),
const SizedBox(height: 8),
ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 4,
vertical: 4,
),
leading: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: theme.colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(14),
),
child: Icon(
Icons.near_me_rounded,
color: theme.colorScheme.onSecondaryContainer,
),
),
title: Text(l10n.direct),
subtitle: Text(l10n.nearbyOnlyWithoutRepeaterFlooding),
trailing: const Icon(Icons.chevron_right_rounded),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
onTap: () => Navigator.of(context).pop(_AdvertMode.direct),
),
],
),
),
),
);
}
void _showDeviceInfoSheet(BuildContext context, DeviceInfo deviceInfo) {
// Request self telemetry so it's fresh
context.read<ConnectionProvider>().requestSelfTelemetry();
@@ -679,108 +575,6 @@ class _HomeScreenState extends State<HomeScreen>
);
}
Future<void> _advertiseDevice(
BuildContext context, {
bool floodMode = true,
}) async {
final connectionProvider = context.read<ConnectionProvider>();
if (!connectionProvider.deviceInfo.isConnected) {
if (context.mounted) {
ToastLogger.error(
context,
AppLocalizations.of(context)!.deviceNotConnected,
);
}
return;
}
try {
// Check if location services are enabled
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
if (context.mounted) {
ToastLogger.error(
context,
AppLocalizations.of(context)!.locationServicesDisabled,
);
}
return;
}
// Check location permissions
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
if (context.mounted) {
ToastLogger.error(
context,
AppLocalizations.of(context)!.locationPermissionDenied,
);
}
return;
}
}
if (permission == LocationPermission.deniedForever) {
if (context.mounted) {
ToastLogger.error(
context,
AppLocalizations.of(context)!.locationPermissionPermanentlyDenied,
);
}
return;
}
// Get current GPS position
Position? position;
try {
position = await Geolocator.getCurrentPosition(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.best,
distanceFilter: 0,
),
).timeout(const Duration(seconds: 5));
} catch (e) {
debugPrint('❌ Failed to get GPS position: $e');
if (context.mounted) {
ToastLogger.error(
context,
AppLocalizations.of(context)!.failedToGetGpsLocation,
);
}
return;
}
// Update lat/lon on device
await connectionProvider.setAdvertLatLon(
latitude: position.latitude,
longitude: position.longitude,
);
// Small delay to ensure the lat/lon is set
await Future.delayed(const Duration(milliseconds: 100));
await connectionProvider.sendSelfAdvert(floodMode: floodMode);
if (context.mounted) {
ToastLogger.success(
context,
floodMode ? 'Flood advert sent' : 'Direct advert sent',
);
}
} catch (e) {
debugPrint('❌ Failed to advertise device: $e');
if (context.mounted) {
ToastLogger.error(
context,
AppLocalizations.of(context)!.failedToAdvertise(e.toString()),
);
}
}
}
Future<void> _showConnectionDialog(BuildContext context) async {
await showConnectionDialogFlow(
context,
@@ -837,102 +631,105 @@ class _HomeScreenState extends State<HomeScreen>
icon: const Icon(Icons.more_vert),
itemBuilder: (context) {
final items = <PopupMenuEntry<void>>[];
final profilesEnabled = context
.read<ProfileManager>()
.profilesEnabled;
final profilesEnabled =
!_isSimpleMode &&
context.read<ProfileManager>().profilesEnabled;
items.add(
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.radar_outlined),
const SizedBox(width: 8),
Text(AppLocalizations.of(context)!.liveTraffic),
],
),
onTap: () {
final navigator = Navigator.of(context);
final provider = context.read<ConnectionProvider>();
Future.delayed(Duration.zero, () {
if (!mounted) return;
navigator.push(
MaterialPageRoute(
builder: (_) => LiveTrafficScreen.fromProvider(
provider,
openPacketLogs: () {
navigator.push(
MaterialPageRoute(
builder: (_) => PacketLogScreen(
bleService: provider.bleService,
),
if (!_isSimpleMode) {
items.add(
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.radar_outlined),
const SizedBox(width: 8),
Text(AppLocalizations.of(context)!.liveTraffic),
],
),
onTap: () {
final navigator = Navigator.of(context);
final provider = context.read<ConnectionProvider>();
Future.delayed(Duration.zero, () {
if (!mounted) return;
navigator.push(
MaterialPageRoute(
builder: (_) =>
LiveTrafficScreen.fromProvider(
provider,
openPacketLogs: () {
navigator.push(
MaterialPageRoute(
builder: (_) => PacketLogScreen(
bleService: provider.bleService,
),
),
);
},
),
);
},
),
),
);
});
},
),
);
items.add(
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.router_outlined),
const SizedBox(width: 8),
Text(AppLocalizations.of(context)!.repeatersMap),
],
);
});
},
),
onTap: () {
final navigator = Navigator.of(context);
Future.delayed(Duration.zero, () {
if (!mounted) return;
navigator.push(
MaterialPageRoute(
builder: (context) =>
const RepeatersMapScreen(),
fullscreenDialog: true,
),
);
});
},
),
);
);
items.add(
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.person_search),
const SizedBox(width: 8),
Consumer<ContactsProvider>(
builder: (context, contactsProvider, child) {
final pendingCount =
contactsProvider.pendingAdverts.length;
return Text(
pendingCount > 0
? 'Discovery ($pendingCount)'
: 'Discovery',
);
},
),
],
items.add(
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.router_outlined),
const SizedBox(width: 8),
Text(AppLocalizations.of(context)!.repeatersMap),
],
),
onTap: () {
final navigator = Navigator.of(context);
Future.delayed(Duration.zero, () {
if (!mounted) return;
navigator.push(
MaterialPageRoute(
builder: (context) =>
const RepeatersMapScreen(),
fullscreenDialog: true,
),
);
});
},
),
onTap: () {
final navigator = Navigator.of(context);
Future.delayed(Duration.zero, () {
if (!mounted) return;
navigator.push(
MaterialPageRoute(
builder: (context) => const DiscoveryScreen(),
);
items.add(
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.person_search),
const SizedBox(width: 8),
Consumer<ContactsProvider>(
builder: (context, contactsProvider, child) {
final pendingCount =
contactsProvider.pendingAdverts.length;
return Text(
pendingCount > 0
? 'Discovery ($pendingCount)'
: 'Discovery',
);
},
),
);
});
},
),
);
],
),
onTap: () {
final navigator = Navigator.of(context);
Future.delayed(Duration.zero, () {
if (!mounted) return;
navigator.push(
MaterialPageRoute(
builder: (context) => const DiscoveryScreen(),
),
);
});
},
),
);
}
items.add(
PopupMenuItem(
@@ -1066,7 +863,7 @@ class _HomeScreenState extends State<HomeScreen>
Icons.message,
unreadCount,
),
text: 'Chat',
text: AppLocalizations.of(context)!.chat,
);
case _HomeTab.contacts:
return Tab(
@@ -1082,9 +879,9 @@ class _HomeScreenState extends State<HomeScreen>
text: AppLocalizations.of(context)!.map,
);
case _HomeTab.sensors:
return const Tab(
icon: Icon(Icons.sensors),
text: 'Sensors',
return Tab(
icon: const Icon(Icons.sensors),
text: AppLocalizations.of(context)!.sensors,
);
}
}).toList(),
@@ -1122,8 +919,8 @@ class _HomeScreenState extends State<HomeScreen>
),
Text(
provider.isReconnecting
? 'Restoring previous link'
: 'No device connected',
? AppLocalizations.of(context)!.restoringPreviousLink
: AppLocalizations.of(context)!.noDeviceConnected,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
@@ -1307,50 +1104,6 @@ class _HomeScreenState extends State<HomeScreen>
padding: EdgeInsets.zero,
),
),
const SizedBox(width: 4),
GestureDetector(
onTap: () async {
await _triggerAdvertFeedback();
if (!mounted || !context.mounted) return;
await _advertiseDevice(context);
},
onLongPress: () async {
await _triggerAdvertFeedback();
if (!mounted || !context.mounted) return;
final mode = await _showAdvertModeSheet(context);
if (!mounted || !context.mounted || mode == null) {
return;
}
await _advertiseDevice(
context,
floodMode: mode == _AdvertMode.flood,
);
},
child: Container(
width: isTight ? 38 : 40,
height: isTight ? 38 : 40,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
theme.colorScheme.primary,
theme.colorScheme.primary.withValues(
alpha: 0.78,
),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
Icons.campaign_rounded,
color: Colors.white,
size: isTight ? 18 : 20,
),
),
),
],
),
),

File diff suppressed because it is too large Load Diff

View File

@@ -27,6 +27,7 @@ import '../services/message_destination_preferences.dart';
import '../services/voice_bitrate_preferences.dart';
import '../services/voice_recorder_service.dart';
import '../services/voice_codec_service.dart';
import '../utils/advert_helper.dart';
import '../utils/toast_logger.dart';
import '../utils/key_comparison.dart';
import '../utils/contact_sorting.dart';
@@ -149,6 +150,8 @@ class _MessagesTabState extends State<MessagesTab> {
String _mentionQuery = '';
List<Contact> _mentionSuggestions = const [];
ContactsProvider? _contactsProvider;
AppProvider? _appProvider;
bool? _lastKnownSimpleMode;
// Message destination state
String _destinationType =
@@ -207,6 +210,13 @@ class _MessagesTabState extends State<MessagesTab> {
_contactsProvider = contactsProvider;
_contactsProvider?.addListener(_handleContactsChanged);
}
final appProvider = context.read<AppProvider>();
if (!identical(_appProvider, appProvider)) {
_appProvider?.removeListener(_handleSimpleModeChanged);
_appProvider = appProvider;
_lastKnownSimpleMode = appProvider.isSimpleMode;
_appProvider?.addListener(_handleSimpleModeChanged);
}
_scheduleDestinationSync();
}
@@ -217,6 +227,7 @@ class _MessagesTabState extends State<MessagesTab> {
_voiceStreamSub?.cancel();
_voiceRecorder.dispose();
_contactsProvider?.removeListener(_handleContactsChanged);
_appProvider?.removeListener(_handleSimpleModeChanged);
_focusNode.removeListener(_handleFocusChanged);
_textController.dispose();
_focusNode.dispose();
@@ -246,6 +257,14 @@ class _MessagesTabState extends State<MessagesTab> {
_scheduleDestinationSync();
}
void _handleSimpleModeChanged() {
if (!mounted) return;
final simpleMode = _appProvider?.isSimpleMode;
if (simpleMode == _lastKnownSimpleMode) return;
_lastKnownSimpleMode = simpleMode;
_scheduleDestinationSync();
}
Future<void> _synchronizeDestinationState() async {
if (!mounted) return;
final messagesProvider = context.read<MessagesProvider>();
@@ -409,8 +428,11 @@ class _MessagesTabState extends State<MessagesTab> {
List<Contact> _buildMentionSuggestions(String query) {
final contactsProvider = context.read<ContactsProvider>();
final normalizedQuery = query.trim().toLowerCase();
final candidates = context.read<AppProvider>().isSimpleMode
? contactsProvider.favouriteChatContacts
: contactsProvider.chatContacts;
final contacts =
contactsProvider.chatContacts.where((contact) {
candidates.where((contact) {
if (normalizedQuery.isEmpty) return true;
return contact.displayName.toLowerCase().contains(normalizedQuery);
}).toList()..sort((a, b) {
@@ -488,6 +510,57 @@ class _MessagesTabState extends State<MessagesTab> {
savedDestination?['publicKey'];
if (!mounted) return;
final contactsProvider = context.read<ContactsProvider>();
if (context.read<AppProvider>().isSimpleMode) {
// Simple mode allows the locked channel/room plus direct messages to
// favourite contacts. A favourite chosen by the user (override or
// saved selection) wins over the locked anchor; anything else falls
// back to the locked destination, then to the first favourite. The
// fallback is never persisted over the normal-mode preference.
final favourites = contactsProvider.favouriteChatContacts;
final candidateType = overrideType ?? savedDestination?['type'];
final candidateKey =
overrideRecipientPublicKeyHex ?? savedDestination?['publicKey'];
Contact? favourite;
if (candidateType ==
MessageDestinationPreferences.destinationTypeContact) {
favourite = favourites
.where((c) => c.publicKeyHex == candidateKey)
.firstOrNull;
}
String destinationType;
Contact? recipient;
if (favourite != null) {
destinationType = MessageDestinationPreferences.destinationTypeContact;
recipient = favourite;
} else if (lockedDestination != null) {
destinationType =
lockedDestination['type'] ??
MessageDestinationPreferences.destinationTypeChannel;
recipient = _resolveDestinationRecipient(
contactsProvider,
destinationType,
lockedDestination['publicKey'],
);
} else {
destinationType = MessageDestinationPreferences.destinationTypeContact;
recipient = favourites.firstOrNull;
}
setState(() {
// Keep the selector usable so favourites stay reachable.
_isDestinationLocked = false;
_destinationType = destinationType;
_selectedRecipient = recipient;
});
_enforceMessageByteLimit();
await _loadRegionScope();
return;
}
final recipient = _resolveDestinationRecipient(
contactsProvider,
effectiveType,
@@ -495,7 +568,8 @@ class _MessagesTabState extends State<MessagesTab> {
);
final allowsEmptyRecipient =
effectiveType == MessageDestinationPreferences.destinationTypeAll ||
(effectiveType == MessageDestinationPreferences.destinationTypeChannel &&
(effectiveType ==
MessageDestinationPreferences.destinationTypeChannel &&
effectivePublicKeyHex == null);
final shouldFallbackToPublicChannel =
recipient == null && !allowsEmptyRecipient;
@@ -537,7 +611,8 @@ class _MessagesTabState extends State<MessagesTab> {
final candidates = switch (type) {
MessageDestinationPreferences.destinationTypeChannel =>
contactsProvider.channels,
MessageDestinationPreferences.destinationTypeRoom => contactsProvider.rooms,
MessageDestinationPreferences.destinationTypeRoom =>
contactsProvider.rooms,
MessageDestinationPreferences.destinationTypeContact =>
contactsProvider.chatContacts,
_ => contactsProvider.contacts,
@@ -549,41 +624,82 @@ class _MessagesTabState extends State<MessagesTab> {
}
/// Show recipient selector bottom sheet
void _showRecipientSelector() {
Future<void> _showRecipientSelector() async {
if (_isDestinationLocked) {
return;
}
final contactsProvider = context.read<ContactsProvider>();
final messagesProvider = context.read<MessagesProvider>();
final isSimpleMode = context.read<AppProvider>().isSimpleMode;
final contacts = List<Contact>.from(contactsProvider.chatContacts)
..sort((a, b) {
final primary = compareContactsByFavouriteThenLastSeen(a, b);
if (primary != 0) {
return primary;
}
// In simple mode the only channel/room offered is the locked anchor, so
// the user can return to the team channel after messaging a favourite.
Contact? lockedRecipient;
String? lockedType;
if (isSimpleMode) {
final lockedDestination =
await MessageDestinationPreferences.getLockedDestination();
if (lockedDestination != null) {
lockedType =
lockedDestination['type'] ??
MessageDestinationPreferences.destinationTypeChannel;
lockedRecipient = _resolveDestinationRecipient(
contactsProvider,
lockedType,
lockedDestination['publicKey'],
);
}
}
if (!mounted) return;
return compareContactsByDisplayName(a, b);
});
final rooms = List<Contact>.from(contactsProvider.rooms)
..sort((a, b) {
final primary = compareContactsByLastSeen(a, b);
if (primary != 0) {
return primary;
}
final contacts =
List<Contact>.from(
isSimpleMode
? contactsProvider.favouriteChatContacts
: contactsProvider.chatContacts,
)..sort((a, b) {
final primary = compareContactsByFavouriteThenLastSeen(a, b);
if (primary != 0) {
return primary;
}
return compareContactsByDisplayName(a, b);
});
final channels = List<Contact>.from(contactsProvider.channels)
..sort((a, b) {
final primary = compareContactsByLastSeen(a, b);
if (primary != 0) {
return primary;
}
return compareContactsByDisplayName(a, b);
});
final rooms =
isSimpleMode
? <Contact>[
if (lockedRecipient != null &&
lockedType ==
MessageDestinationPreferences.destinationTypeRoom)
lockedRecipient,
]
: List<Contact>.from(contactsProvider.rooms)
..sort((a, b) {
final primary = compareContactsByLastSeen(a, b);
if (primary != 0) {
return primary;
}
return compareContactsByDisplayName(a, b);
});
return compareContactsByDisplayName(a, b);
});
final channels =
isSimpleMode
? <Contact>[
if (lockedRecipient != null &&
lockedType ==
MessageDestinationPreferences.destinationTypeChannel)
lockedRecipient,
]
: List<Contact>.from(contactsProvider.channels)
..sort((a, b) {
final primary = compareContactsByLastSeen(a, b);
if (primary != 0) {
return primary;
}
return compareContactsByDisplayName(a, b);
});
showModalBottomSheet(
context: context,
@@ -832,7 +948,10 @@ class _MessagesTabState extends State<MessagesTab> {
if (_selectedRecipient != null) {
return _selectedRecipient!.displayName;
}
return 'Public Channel';
if (context.read<AppProvider>().isSimpleMode) {
return AppLocalizations.of(context)!.noFavouriteContacts;
}
return AppLocalizations.of(context)!.publicChannel;
}
Future<void> _sendMessage() async {
@@ -845,13 +964,32 @@ class _MessagesTabState extends State<MessagesTab> {
if (!connectionProvider.deviceInfo.isConnected) {
if (!mounted) return;
ToastLogger.error(context, 'Not connected to device');
ToastLogger.error(
context,
AppLocalizations.of(context)!.notConnectedToDevice,
);
return;
}
if (_destinationType == MessageDestinationPreferences.destinationTypeAll) {
if (!mounted) return;
ToastLogger.error(context, 'Select a channel, contact, or room first');
ToastLogger.error(
context,
AppLocalizations.of(context)!.selectDestinationFirst,
);
return;
}
// Refuse to send if the contact/room destination no longer resolves to a
// recipient - never silently redirect private traffic to the public channel.
if (_destinationType !=
MessageDestinationPreferences.destinationTypeChannel &&
_selectedRecipient == null) {
if (!mounted) return;
ToastLogger.error(
context,
AppLocalizations.of(context)!.recipientNoLongerAvailable,
);
return;
}
@@ -871,35 +1009,27 @@ class _MessagesTabState extends State<MessagesTab> {
messagesProvider,
channelIdx,
);
} else if (_selectedRecipient != null) {
// Send to contact or room
} else {
// Send to contact or room (recipient presence already validated above)
await _sendToRecipient(
text,
connectionProvider,
messagesProvider,
contactsProvider,
);
} else {
// Fallback to public channel if no recipient selected
debugPrint(
'⚠️ [MessagesTab] No recipient selected, falling back to public channel',
);
await _sendToChannel(text, connectionProvider, messagesProvider, 0);
}
_markCurrentDestinationAsRead();
if (!mounted) return;
} catch (e) {
// The placeholder bubble is marked failed by the send helpers, so the
// inline retry chip is the recovery path - don't restore the composer.
if (!mounted) return;
if (_textController.text.isEmpty) {
_textController.text = text;
_textController.selection = TextSelection.collapsed(
offset: _textController.text.length,
);
_focusNode.requestFocus();
}
ToastLogger.error(context, 'Failed to send: $e');
ToastLogger.error(
context,
AppLocalizations.of(context)!.failedToSendMessage('$e'),
);
}
}
@@ -936,12 +1066,18 @@ class _MessagesTabState extends State<MessagesTab> {
messagesProvider.addSentMessage(sentMessage, contact: _selectedRecipient);
// Send to selected channel (with region scope if set)
await connectionProvider.sendChannelMessage(
channelIdx: channelIdx,
text: text,
messageId: messageId,
floodScopeKey: _channelRegionScopeKey,
);
try {
await connectionProvider.sendChannelMessage(
channelIdx: channelIdx,
text: text,
messageId: messageId,
floodScopeKey: _channelRegionScopeKey,
);
} catch (e) {
// Mark the placeholder as failed so the retry chip appears
messagesProvider.markMessageFailed(messageId);
rethrow;
}
}
/// Send message to contact or room
@@ -979,12 +1115,19 @@ class _MessagesTabState extends State<MessagesTab> {
messagesProvider.addSentMessage(sentMessage, contact: _selectedRecipient);
// Send message to selected recipient
final sentSuccessfully = await connectionProvider.sendTextMessage(
contactPublicKey: _selectedRecipient!.publicKey,
text: text,
messageId: messageId,
contact: _selectedRecipient,
);
final bool sentSuccessfully;
try {
sentSuccessfully = await connectionProvider.sendTextMessage(
contactPublicKey: _selectedRecipient!.publicKey,
text: text,
messageId: messageId,
contact: _selectedRecipient,
);
} catch (e) {
// Mark the placeholder as failed so the retry chip appears
messagesProvider.markMessageFailed(messageId);
rethrow;
}
if (!sentSuccessfully) {
// Mark message as failed if sending failed
@@ -1027,8 +1170,9 @@ class _MessagesTabState extends State<MessagesTab> {
final connectionProvider = context.read<ConnectionProvider>();
if (_selectedRecipient != null &&
connectionProvider.deviceInfo.isConnected) {
await connectionProvider
.setContactDirect(_selectedRecipient!);
await connectionProvider.setContactDirect(
_selectedRecipient!,
);
}
await _sendMessage();
},
@@ -1042,8 +1186,9 @@ class _MessagesTabState extends State<MessagesTab> {
final connectionProvider = context.read<ConnectionProvider>();
if (_selectedRecipient != null &&
connectionProvider.deviceInfo.isConnected) {
await connectionProvider
.resetPath(_selectedRecipient!.publicKey);
await connectionProvider.resetPath(
_selectedRecipient!.publicKey,
);
}
await _sendMessage();
},
@@ -1062,7 +1207,7 @@ class _MessagesTabState extends State<MessagesTab> {
_selectedRecipient == null) {
ToastLogger.warning(
context,
'Tic-Tac-Toe works only in direct messages. Choose a contact first.',
AppLocalizations.of(context)!.tictactoeDirectOnly,
);
return;
}
@@ -1071,13 +1216,19 @@ class _MessagesTabState extends State<MessagesTab> {
final messagesProvider = context.read<MessagesProvider>();
final contactsProvider = context.read<ContactsProvider>();
if (!connectionProvider.deviceInfo.isConnected) {
ToastLogger.error(context, 'Not connected to device');
ToastLogger.error(
context,
AppLocalizations.of(context)!.notConnectedToDevice,
);
return;
}
final devicePublicKey = connectionProvider.deviceInfo.publicKey;
if (devicePublicKey == null || devicePublicKey.length < 6) {
ToastLogger.error(context, 'Device key unavailable');
ToastLogger.error(
context,
AppLocalizations.of(context)!.deviceKeyUnavailable,
);
return;
}
@@ -1115,7 +1266,10 @@ class _MessagesTabState extends State<MessagesTab> {
final connectionProvider = context.read<ConnectionProvider>();
final messagesProvider = context.read<MessagesProvider>();
if (!connectionProvider.deviceInfo.isConnected) {
ToastLogger.error(context, 'Not connected to device');
ToastLogger.error(
context,
AppLocalizations.of(context)!.notConnectedToDevice,
);
return;
}
@@ -1145,7 +1299,10 @@ class _MessagesTabState extends State<MessagesTab> {
);
if (result == null) {
if (!mounted) return;
ToastLogger.error(context, 'Image compression failed');
ToastLogger.error(
context,
AppLocalizations.of(context)!.imageCompressionFailed,
);
return;
}
final compressed = result.bytes;
@@ -1176,7 +1333,10 @@ class _MessagesTabState extends State<MessagesTab> {
if (fragments.isEmpty) {
if (!mounted) return;
ToastLogger.error(context, 'Image fragmentation failed');
ToastLogger.error(
context,
AppLocalizations.of(context)!.imageFragmentationFailed,
);
return;
}
@@ -1184,7 +1344,10 @@ class _MessagesTabState extends State<MessagesTab> {
final deviceKey = connectionProvider.deviceInfo.publicKey;
if (deviceKey == null || deviceKey.length < 6) {
if (!mounted) return;
ToastLogger.error(context, 'Device key unavailable');
ToastLogger.error(
context,
AppLocalizations.of(context)!.deviceKeyUnavailable,
);
return;
}
final envelope = ImageEnvelope(
@@ -1247,13 +1410,19 @@ class _MessagesTabState extends State<MessagesTab> {
if (!sent) {
messagesProvider.markMessageFailed(msgId);
if (!mounted) return;
ToastLogger.error(context, 'Failed to announce image');
ToastLogger.error(
context,
AppLocalizations.of(context)!.failedToAnnounceImage,
);
return;
}
} else {
messagesProvider.markMessageFailed(msgId);
if (!mounted) return;
ToastLogger.error(context, 'No recipient selected');
ToastLogger.error(
context,
AppLocalizations.of(context)!.noRecipientSelected,
);
return;
}
@@ -1282,7 +1451,7 @@ class _MessagesTabState extends State<MessagesTab> {
messagesProvider.markMessageFailed(failedMessageId);
}
if (!mounted) return;
ToastLogger.error(context, 'Image send failed');
ToastLogger.error(context, AppLocalizations.of(context)!.imageSendFailed);
} finally {
if (mounted) setState(() => _isSendingImage = false);
}
@@ -1306,7 +1475,10 @@ class _MessagesTabState extends State<MessagesTab> {
debugPrint('🎙️ [Voice] hasPermission=$hasPermission');
if (!hasPermission) {
if (!mounted) return;
ToastLogger.error(context, 'Microphone permission required for voice');
ToastLogger.error(
context,
AppLocalizations.of(context)!.microphonePermissionRequiredForVoice,
);
return;
}
@@ -1317,7 +1489,10 @@ class _MessagesTabState extends State<MessagesTab> {
'🎙️ [Voice] isConnected=${connectionProvider.deviceInfo.isConnected}',
);
if (!connectionProvider.deviceInfo.isConnected) {
ToastLogger.error(context, 'Not connected to device');
ToastLogger.error(
context,
AppLocalizations.of(context)!.notConnectedToDevice,
);
return;
}
@@ -1463,6 +1638,27 @@ class _MessagesTabState extends State<MessagesTab> {
}
}
/// Stop the recorder and discard the captured audio without sending.
Future<void> _cancelVoiceRecording() async {
if (!_isRecording) return;
debugPrint(
'🎙️ [Voice] _cancelVoiceRecording: discarding ${_recordedChunks.length} chunks',
);
await _voiceStreamSub?.cancel();
_voiceStreamSub = null;
await _voiceRecorder.stopCapture();
_recordedChunks.clear();
_currentVoiceSessionId = null;
if (!mounted) return;
setState(() {
_isRecording = false;
_isSendingVoice = false;
});
ToastLogger.info(context, AppLocalizations.of(context)!.recordingDiscarded);
}
Future<void> _encodeAndSendAllPackets({
required List<Int16List> chunks,
required String sessionId,
@@ -1580,7 +1776,10 @@ class _MessagesTabState extends State<MessagesTab> {
} else {
messagesProvider.markMessageFailed(msgId);
if (!mounted) return;
ToastLogger.error(context, 'No recipient selected');
ToastLogger.error(
context,
AppLocalizations.of(context)!.noRecipientSelected,
);
return;
}
} catch (e, st) {
@@ -1684,9 +1883,11 @@ class _MessagesTabState extends State<MessagesTab> {
builder: (dialogContext) => AlertDialog(
title: Text(AppLocalizations.of(context)!.sendToPublicChannel),
content: Text(
'You are about to send $mediaType to the Public Channel. '
'This is not advised because everyone on the mesh may receive it. '
'Choose a private or tagged channel unless this is what you want.',
AppLocalizations.of(context)!.publicChannelMediaWarning(
mediaType == 'image'
? AppLocalizations.of(context)!.image
: AppLocalizations.of(context)!.voice,
),
),
actions: [
TextButton(
@@ -1773,11 +1974,13 @@ class _MessagesTabState extends State<MessagesTab> {
bool enabled,
) async {
try {
final result = await context.read<AppProvider>().setChannelLocationSharingEnabled(
channelIdx,
enabled,
l10n: AppLocalizations.of(context),
);
final result = await context
.read<AppProvider>()
.setChannelLocationSharingEnabled(
channelIdx,
enabled,
l10n: AppLocalizations.of(context),
);
if (!mounted) return;
ToastLogger.success(context, result.message);
} catch (error) {
@@ -1792,17 +1995,19 @@ class _MessagesTabState extends State<MessagesTab> {
if (!mounted) return;
ToastLogger.error(
context,
'Select a private channel to share your location.',
AppLocalizations.of(context)!.selectPrivateChannelToShareLocation,
);
return;
}
final channelIdx = recipient.publicKey.length > 1 ? recipient.publicKey[1] : 0;
final channelIdx = recipient.publicKey.length > 1
? recipient.publicKey[1]
: 0;
if (channelIdx <= 0) {
if (!mounted) return;
ToastLogger.error(
context,
'Select a private channel to share your location.',
AppLocalizations.of(context)!.selectPrivateChannelToShareLocation,
);
return;
}
@@ -1812,7 +2017,10 @@ class _MessagesTabState extends State<MessagesTab> {
.read<AppProvider>()
.getChannelLocationSharingState(channelIdx);
if (!mounted) return;
await _setSelectedChannelLocationSharing(channelIdx, !sharingState.isSharing);
await _setSelectedChannelLocationSharing(
channelIdx,
!sharingState.isSharing,
);
} catch (error) {
if (!mounted) return;
ToastLogger.error(context, _locationSharingErrorMessage(error));
@@ -1823,9 +2031,9 @@ class _MessagesTabState extends State<MessagesTab> {
final privateChannelIdx = _selectedPrivateChannelIdx();
final locationSharingStateFuture = privateChannelIdx == null
? null
: context
.read<AppProvider>()
.getChannelLocationSharingState(privateChannelIdx);
: context.read<AppProvider>().getChannelLocationSharingState(
privateChannelIdx,
);
showModalBottomSheet(
context: context,
showDragHandle: true,
@@ -1848,6 +2056,21 @@ class _MessagesTabState extends State<MessagesTab> {
_showFilteredMessageSearch();
}),
),
_ComposerActionTile(
icon: Icons.campaign_rounded,
title: l10n.sendAdvert,
color: theme.colorScheme.primary,
onTap: () => runAction(() async {
await triggerAdvertFeedback(context);
if (!mounted) return;
final mode = await showAdvertModeSheet(context);
if (!mounted || mode == null) return;
await advertiseDevice(
context,
floodMode: mode == AdvertMode.flood,
);
}),
),
_ComposerActionTile(
icon: Icons.add_location_alt_rounded,
title: l10n.sendSarMarker,
@@ -1863,8 +2086,8 @@ class _MessagesTabState extends State<MessagesTab> {
? Icons.location_off_rounded
: Icons.share_location_rounded,
title: sharingState?.isSharing == true
? 'Stop sharing my location'
: 'Share my location',
? l10n.stopSharingMyLocation
: l10n.shareMyLocation,
color: const Color(0xFF0F766E),
onTap: () => runAction(() async {
await _handleChannelLocationSharingAction();
@@ -1873,7 +2096,7 @@ class _MessagesTabState extends State<MessagesTab> {
if (_voiceSupported)
_ComposerActionTile(
icon: _isRecording ? Icons.stop_rounded : Icons.mic_rounded,
title: _isRecording ? 'Stop recording' : 'Record voice',
title: _isRecording ? l10n.stopRecording : l10n.recordVoice,
color: const Color(0xFF7C3AED),
enabled: !_isSendingVoice,
busy: _isSendingVoice,
@@ -1947,7 +2170,7 @@ class _MessagesTabState extends State<MessagesTab> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'More actions',
l10n.moreActions,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w700,
),
@@ -2060,7 +2283,9 @@ class _MessagesTabState extends State<MessagesTab> {
controller: searchController,
autofocus: true,
decoration: InputDecoration(
hintText: AppLocalizations.of(context)!.searchInCurrentFilter,
hintText: AppLocalizations.of(
context,
)!.searchInCurrentFilter,
prefixIcon: const Icon(Icons.search),
suffixIcon: searchController.text.isEmpty
? null
@@ -2083,8 +2308,12 @@ class _MessagesTabState extends State<MessagesTab> {
? Center(
child: Text(
query.isEmpty
? 'No messages in this filter'
: 'No matches in this filter',
? AppLocalizations.of(
context,
)!.noMessagesInFilter
: AppLocalizations.of(
context,
)!.noMatchesInFilter,
),
)
: ListView.separated(
@@ -2098,7 +2327,7 @@ class _MessagesTabState extends State<MessagesTab> {
true
? message.senderName!
: message.isSentMessage
? 'You'
? AppLocalizations.of(context)!.you
: _getDestinationLabel();
return ListTile(
@@ -2163,11 +2392,7 @@ class _MessagesTabState extends State<MessagesTab> {
border: Border.all(color: Colors.white, width: 1.5),
),
alignment: Alignment.center,
child: Icon(
Icons.location_pin,
size: 9,
color: Colors.white,
),
child: Icon(Icons.location_pin, size: 9, color: Colors.white),
),
),
],
@@ -2195,7 +2420,10 @@ class _MessagesTabState extends State<MessagesTab> {
if (!connectionProvider.deviceInfo.isConnected) {
if (!mounted) return;
ToastLogger.error(context, 'Not connected to device');
ToastLogger.error(
context,
AppLocalizations.of(context)!.notConnectedToDevice,
);
return;
}
@@ -2203,7 +2431,7 @@ class _MessagesTabState extends State<MessagesTab> {
if (!mounted) return;
ToastLogger.error(
context,
'Please select a destination to send SAR marker',
AppLocalizations.of(context)!.pleaseSelectADestinationToSendSarMarker,
);
return;
}
@@ -2215,9 +2443,12 @@ class _MessagesTabState extends State<MessagesTab> {
'S:$emoji:${colorIndex.toString()}:${position.latitude.toStringAsFixed(5)},${position.longitude.toStringAsFixed(5)}:$name';
if (sendToAllContacts) {
// Send to all chat contacts (ContactType.chat)
// Send to all chat contacts (ContactType.chat); in simple mode only
// favourite contacts are reachable.
final contactsProvider = context.read<ContactsProvider>();
final chatContacts = contactsProvider.chatContacts;
final chatContacts = context.read<AppProvider>().isSimpleMode
? contactsProvider.favouriteChatContacts
: contactsProvider.chatContacts;
if (chatContacts.isEmpty) {
if (!mounted) return;
@@ -2392,11 +2623,17 @@ class _MessagesTabState extends State<MessagesTab> {
}
if (!mounted) return;
ToastLogger.success(context, 'SAR marker sent to room');
ToastLogger.success(
context,
AppLocalizations.of(context)!.sarMarkerSentToRoom,
);
}
} catch (e) {
if (!mounted) return;
ToastLogger.error(context, 'Failed to send SAR marker: $e');
ToastLogger.error(
context,
AppLocalizations.of(context)!.failedToSendSarMarker('$e'),
);
}
}
@@ -2407,7 +2644,10 @@ class _MessagesTabState extends State<MessagesTab> {
if (!connectionProvider.deviceInfo.isConnected) {
if (!mounted) return;
ToastLogger.warning(context, 'Not connected - cannot sync messages');
ToastLogger.warning(
context,
AppLocalizations.of(context)!.notConnectedToDevice,
);
return;
}
@@ -2415,11 +2655,15 @@ class _MessagesTabState extends State<MessagesTab> {
debugPrint(
'🔄 [MessagesTab] Manual refresh triggered - syncing messages',
);
await connectionProvider.syncAllMessages(force: true);
if (!mounted) return;
} catch (e) {
debugPrint('❌ [MessagesTab] Sync error: $e');
if (!mounted) return;
ToastLogger.error(context, 'Sync failed: $e');
ToastLogger.error(
context,
AppLocalizations.of(context)!.syncFailed('$e'),
);
}
}
@@ -2639,6 +2883,7 @@ class _MessagesTabState extends State<MessagesTab> {
onShowRecipientSelector: _showRecipientSelector,
onStartVoiceRecording: _startVoiceRecording,
onStopAndSendVoice: _stopAndSendVoice,
onCancelVoiceRecording: _cancelVoiceRecording,
onSendMessage: _sendMessage,
onLongPressSend: _isContactDestination()
? _showSendModeSheet

View File

@@ -114,6 +114,9 @@ class _OfflineMapScreenState extends State<OfflineMapScreen> {
children: [
TileLayer(
urlTemplate: provider.selectedLayer.urlTemplate,
tileProvider: NetworkTileProvider(
headers: provider.selectedLayer.headers ?? const {},
),
userAgentPackageName: 'com.meshcore.sar',
maxZoom: provider.selectedLayer.maxZoom,
),

View File

@@ -19,11 +19,7 @@ class ProfilesScreen extends StatelessWidget {
title: Text(AppLocalizations.of(context)!.profiles),
actions: [
IconButton(
onPressed: () async {
await context
.read<ProfileWorkspaceCoordinator>()
.importProfileFromFile();
},
onPressed: () => _importProfile(context),
icon: const Icon(Icons.file_open),
tooltip: AppLocalizations.of(context)!.importProfile,
),
@@ -126,9 +122,11 @@ class ProfilesScreen extends StatelessWidget {
await _renameProfile(context, profile);
break;
case 'delete':
await context
.read<ProfileWorkspaceCoordinator>()
.deleteProfile(profile);
await _deleteProfile(
context,
profile,
isActive: isActive,
);
break;
}
},
@@ -183,6 +181,67 @@ class ProfilesScreen extends StatelessWidget {
return sections.isEmpty ? 'Empty profile' : sections.join(' | ');
}
Future<void> _importProfile(BuildContext context) async {
final messenger = ScaffoldMessenger.of(context);
final errorColor = Theme.of(context).colorScheme.error;
final coordinator = context.read<ProfileWorkspaceCoordinator>();
try {
final imported = await coordinator.importProfileFromFile();
if (imported == null) {
messenger.showSnackBar(
SnackBar(
content: const Text('Could not import profile — invalid file'),
backgroundColor: errorColor,
),
);
return;
}
messenger.showSnackBar(
SnackBar(content: Text('Imported profile "${imported.name}"')),
);
} catch (_) {
messenger.showSnackBar(
SnackBar(
content: const Text('Could not import profile — invalid file'),
backgroundColor: errorColor,
),
);
}
}
Future<void> _deleteProfile(
BuildContext context,
ConfigProfile profile, {
required bool isActive,
}) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Delete Profile'),
content: Text(
'Delete "${profile.name}"? Its messages and contacts will be '
'permanently erased.'
'${isActive ? '\n\nThis profile is currently active. The app will switch to the Default profile.' : ''}',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(AppLocalizations.of(context)!.cancel),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: Text(AppLocalizations.of(context)!.delete),
),
],
),
);
if (confirmed != true || !context.mounted) {
return;
}
await context.read<ProfileWorkspaceCoordinator>().deleteProfile(profile);
}
Future<void> _createProfile(BuildContext context) async {
final controller = TextEditingController();
final name = await showDialog<String>(

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../l10n/app_localizations.dart';
import '../models/device_info.dart';
import '../providers/connection_provider.dart';
import '../services/wizard_preferences.dart';
import '../widgets/connection_dialog.dart';
@@ -168,7 +169,9 @@ class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
codingRate: 5,
),
];
_RadioPreset _selectedPreset = _radioPresets[4];
/// Null means "keep the device's current radio settings" (no preset write).
_RadioPreset? _selectedPreset;
bool _userSelectedPreset = false;
@override
void initState() {
@@ -176,6 +179,77 @@ class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
final deviceInfo = context.read<ConnectionProvider>().deviceInfo;
_deviceNameController.text =
deviceInfo.selfName ?? deviceInfo.deviceName ?? '';
_selectedPreset = _matchRadioPreset(deviceInfo);
}
static const Map<int, int> _bandwidthHzToValue = {
7800: 0,
10400: 1,
15600: 2,
20800: 3,
31250: 4,
41700: 5,
62500: 6,
125000: 7,
250000: 8,
500000: 9,
};
static int? _normalizeBandwidthValue(int? bw) {
if (bw == null) {
return null;
}
if (bw >= 0 && bw <= 9) {
return bw;
}
return _bandwidthHzToValue[bw];
}
static int? _normalizeSpreadingFactor(int? spreadingFactor) {
if (spreadingFactor == null ||
spreadingFactor < 7 ||
spreadingFactor > 12) {
return null;
}
return spreadingFactor;
}
static int? _normalizeCodingRate(int? codingRate) {
if (codingRate == null) {
return null;
}
if (codingRate >= 5 && codingRate <= 8) {
return codingRate;
}
if (codingRate >= 1 && codingRate <= 4) {
return codingRate + 4;
}
return null;
}
/// Find the preset matching the device's current radio settings, if any.
static _RadioPreset? _matchRadioPreset(DeviceInfo deviceInfo) {
final frequencyKhz = deviceInfo.radioFreq;
final bandwidth = _normalizeBandwidthValue(deviceInfo.radioBw);
final spreadingFactor = _normalizeSpreadingFactor(deviceInfo.radioSf);
final codingRate = _normalizeCodingRate(deviceInfo.radioCr);
if (frequencyKhz == null ||
bandwidth == null ||
spreadingFactor == null ||
codingRate == null) {
return null;
}
for (final preset in _radioPresets) {
if (preset.frequencyKhz == frequencyKhz &&
preset.bandwidth == bandwidth &&
preset.spreadingFactor == spreadingFactor &&
preset.codingRate == codingRate) {
return preset;
}
}
return null;
}
@override
@@ -233,13 +307,14 @@ class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
if (!mounted) return;
final deviceInfo = connectionProvider.deviceInfo;
final fetchedName = deviceInfo.selfName ?? deviceInfo.deviceName ?? '';
if (fetchedName.isNotEmpty) {
setState(() {
setState(() {
if (fetchedName.isNotEmpty) {
_deviceNameController.text = fetchedName;
});
} else {
setState(() {});
}
}
if (!_userSelectedPreset) {
_selectedPreset = _matchRadioPreset(deviceInfo);
}
});
}
Future<void> _handleDeviceSetupAction() async {
@@ -263,21 +338,49 @@ class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
_isApplyingDeviceSetup = true;
});
final selectedPreset = _selectedPreset;
try {
connectionProvider.clearError();
await connectionProvider.setAdvertName(trimmedName);
await connectionProvider.setRadioParams(
frequency: _selectedPreset.frequencyKhz,
bandwidth: _selectedPreset.bandwidth,
spreadingFactor: _selectedPreset.spreadingFactor,
codingRate: _selectedPreset.codingRate,
);
var saveError = connectionProvider.error;
// Only write radio params when the user picked a preset that differs
// from the device's current settings.
if (saveError == null &&
selectedPreset != null &&
selectedPreset != _matchRadioPreset(deviceInfo)) {
await connectionProvider.setRadioParams(
frequency: selectedPreset.frequencyKhz,
bandwidth: selectedPreset.bandwidth,
spreadingFactor: selectedPreset.spreadingFactor,
codingRate: selectedPreset.codingRate,
);
saveError = connectionProvider.error;
}
if (saveError != null) {
connectionProvider.clearError();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(l10n.failedToSave(saveError)),
backgroundColor: Colors.red,
),
);
return;
}
await connectionProvider.refreshDeviceInfo();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
l10n.wizardDeviceSetupSaved(trimmedName, _selectedPreset.label),
l10n.wizardDeviceSetupSaved(
trimmedName,
selectedPreset?.label ?? 'current radio settings',
),
),
backgroundColor: Colors.green,
),
@@ -555,7 +658,8 @@ class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
),
),
const SizedBox(height: 12),
DropdownButtonFormField<_RadioPreset>(
DropdownButtonFormField<_RadioPreset?>(
key: ValueKey(_selectedPreset?.id ?? 'keep_current'),
initialValue: _selectedPreset,
isExpanded: true,
decoration: InputDecoration(
@@ -563,21 +667,28 @@ class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
border: OutlineInputBorder(),
helperText: l10n.wizardConfigRegionHelp,
),
items: _radioPresets
.map(
(preset) => DropdownMenuItem<_RadioPreset>(
value: preset,
child: Text(
preset.label,
overflow: TextOverflow.ellipsis,
),
items: [
const DropdownMenuItem<_RadioPreset?>(
value: null,
child: Text(
'Keep current settings',
overflow: TextOverflow.ellipsis,
),
),
..._radioPresets.map(
(preset) => DropdownMenuItem<_RadioPreset?>(
value: preset,
child: Text(
preset.label,
overflow: TextOverflow.ellipsis,
),
)
.toList(),
),
),
],
onChanged: (preset) {
if (preset == null) return;
setState(() {
_selectedPreset = preset;
_userSelectedPreset = true;
});
},
),
@@ -591,13 +702,14 @@ class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_selectedPreset.label,
_selectedPreset?.label ?? 'Keep current settings',
style: Theme.of(context).textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 4),
Text(
_selectedPreset.summary,
_selectedPreset?.summary ??
'Saving will not change the radio settings on your device.',
style: Theme.of(context).textTheme.bodyMedium,
),
SizedBox(height: 14),

View File

@@ -1,5 +1,8 @@
import 'dart:async';
import 'dart:io';
import 'dart:ui';
import 'package:flutter_background_service/flutter_background_service.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter/widgets.dart';
import 'package:geolocator/geolocator.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -14,8 +17,13 @@ class BackgroundLocationService {
static const String _prefKeyDistance = 'background_tracking_distance';
static const String _prefKeyLastLat = 'background_last_lat';
static const String _prefKeyLastLon = 'background_last_lon';
static const String _notificationChannelId =
'meshcore_sar_background_tracking';
static const int _notificationId = 9101;
MeshCoreBleService? _bleService;
final FlutterBackgroundService _service = FlutterBackgroundService();
bool _serviceConfigured = false;
String _scopedKey(String baseKey) {
return ProfileStorageScope.scopedKey(baseKey);
@@ -33,8 +41,6 @@ class BackgroundLocationService {
/// Start location tracking and automatic advertisement
/// Returns true if successful, false otherwise
///
/// Note: This is foreground tracking. For true background operation,
/// additional platform-specific configuration is required.
Future<bool> startTracking({double distanceThreshold = 10.0}) async {
if (!_isInitialized || _bleService == null) {
debugPrint(
@@ -80,6 +86,8 @@ class BackgroundLocationService {
await prefs.setBool(_scopedKey(_prefKeyEnabled), true);
await prefs.setDouble(_scopedKey(_prefKeyDistance), distanceThreshold);
await _startForegroundService(distanceThreshold);
// Start listening to position updates
Position? lastPosition;
try {
@@ -171,12 +179,87 @@ class BackgroundLocationService {
debugPrint('🛑 [BackgroundLocation] Stopping tracking');
await _positionSubscription?.cancel();
_positionSubscription = null;
await _stopForegroundService();
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_scopedKey(_prefKeyEnabled), false);
debugPrint('✅ [BackgroundLocation] Tracking stopped');
}
Future<void> _startForegroundService(double distanceThreshold) async {
if (!Platform.isAndroid && !Platform.isIOS) {
return;
}
try {
if (!_serviceConfigured) {
await _configureForegroundService();
}
final running = await _service.isRunning();
if (!running) {
await _service.startService();
}
_service.invoke('trackingUpdate', {
'distanceThreshold': distanceThreshold,
});
} catch (e) {
debugPrint('⚠️ [BackgroundLocation] Foreground service start failed: $e');
}
}
Future<void> _stopForegroundService() async {
if (!Platform.isAndroid && !Platform.isIOS) {
return;
}
try {
if (await _service.isRunning()) {
_service.invoke('stopService');
}
} catch (e) {
debugPrint('⚠️ [BackgroundLocation] Foreground service stop failed: $e');
}
}
Future<void> _configureForegroundService() async {
const channel = AndroidNotificationChannel(
_notificationChannelId,
'Background tracking',
description:
'Keeps MeshCore SAR location sharing active while the app is in the background.',
importance: Importance.low,
);
final notifications = FlutterLocalNotificationsPlugin();
await notifications
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>()
?.createNotificationChannel(channel);
await _service.configure(
androidConfiguration: AndroidConfiguration(
onStart: meshCoreSarBackgroundServiceStart,
autoStart: false,
autoStartOnBoot: false,
isForegroundMode: true,
notificationChannelId: _notificationChannelId,
initialNotificationTitle: 'MeshCore SAR',
initialNotificationContent: 'Maintaining background tracking',
foregroundServiceNotificationId: _notificationId,
foregroundServiceTypes: [AndroidForegroundType.location],
),
iosConfiguration: IosConfiguration(
autoStart: false,
onForeground: meshCoreSarBackgroundServiceStart,
onBackground: meshCoreSarBackgroundServiceIos,
),
);
_serviceConfigured = true;
}
/// Update the distance threshold for location updates
/// Note: This will restart tracking with the new threshold
Future<void> updateDistanceThreshold(double distance) async {
@@ -212,3 +295,40 @@ class BackgroundLocationService {
);
}
}
@pragma('vm:entry-point')
Future<bool> meshCoreSarBackgroundServiceIos(ServiceInstance service) async {
WidgetsFlutterBinding.ensureInitialized();
DartPluginRegistrant.ensureInitialized();
return true;
}
@pragma('vm:entry-point')
void meshCoreSarBackgroundServiceStart(ServiceInstance service) {
DartPluginRegistrant.ensureInitialized();
if (service is AndroidServiceInstance) {
service.setAsForegroundService();
service.setForegroundNotificationInfo(
title: 'MeshCore SAR',
content: 'Maintaining background tracking',
);
}
service.on('trackingUpdate').listen((event) {
if (service is AndroidServiceInstance) {
final threshold = event?['distanceThreshold'];
final suffix = threshold is num
? ' (${threshold.toStringAsFixed(0)} m updates)'
: '';
service.setForegroundNotificationInfo(
title: 'MeshCore SAR',
content: 'Maintaining background tracking$suffix',
);
}
});
service.on('stopService').listen((event) {
service.stopSelf();
});
}

View File

@@ -25,15 +25,43 @@ class LocationTrackingService {
static const int _defaultFastLocationActiveCadenceSeconds = 60;
static const int _minFastLocationActiveCadenceSeconds = 60;
static const int _maxFastLocationActiveCadenceSeconds = 60;
static const Duration _fastLocationSlowInterval = Duration(
seconds: 60,
);
// Moving cadence tiers — identical to the MeshUI firmware
// (FAST_GPS_*_INTERVAL_MS / FAST_GPS_SPEED_*_MAX_MPS in MyMesh.cpp).
static const Duration _fastLocationWalkingInterval = Duration(seconds: 30);
static const Duration _fastLocationFastInterval = Duration(seconds: 15);
static const Duration _fastLocationVeryFastInterval = Duration(seconds: 5);
static const double _fastLocationIdleSpeedMaxMetersPerSecond = 0.75;
static const double _fastLocationWalkingSpeedMaxMetersPerSecond = 1.8;
static const double _fastLocationFastSpeedMaxMetersPerSecond = 4.0;
// Stationary keepalive — a parked node still beacons, but only every 9 min
// (FAST_GPS_STATIONARY_*_INTERVAL_MS), not on GPS jitter.
static const Duration _fastLocationStationaryInterval = Duration(minutes: 9);
// Moving/stationary detection: anchor + dwell hysteresis (position-only, no
// Doppler), mirroring FAST_GPS_MOVE_* in MyMesh.cpp. A parked node holds an
// anchor; jitter inside the radius is absorbed and the stable anchor is
// reported, so multipath wander can't paint a fake track.
static const double _fastLocationMoveRadiusMeters = 25.0;
static const Duration _fastLocationMoveDwell = Duration(seconds: 15);
static const Duration _fastLocationStopDwell = Duration(seconds: 60);
static const Duration _fastLocationMoveEval = Duration(seconds: 1);
static const double _fastLocationSpeedGateMeters = 5.0;
static const Duration _fastLocationSpeedRebaseline = Duration(seconds: 8);
static const double _fastLocationSpeedGlitchMaxKmh = 300.0;
// Fix-quality gate. The firmware requires >= 5 satellites (FAST_GPS_MIN_SATS);
// Geolocator does not expose a satellite count on all platforms, so we gate on
// horizontal accuracy as the closest available proxy.
static const double _fastLocationMaxAccuracyMeters = 50.0;
// Yield ~5s after hearing a peer beacon, to avoid channel collisions
// (FAST_GPS_CHANNEL_RX_HOLDOFF_MS).
static const Duration _fastLocationRxHoldoff = Duration(seconds: 5);
// Background keepalive tick when not in active use — cheap, just enough to fire
// the 9-min stationary beacon while parked.
static const Duration _fastLocationActiveTick = Duration(seconds: 15);
// ============================================================================
// Singleton Pattern
// ============================================================================
@@ -128,7 +156,22 @@ class LocationTrackingService {
Timer? _fastLocationTimer;
bool _isFastLocationActiveUse = false;
DateTime? _lastFastLocationSentAt;
Position? _lastFastLocationSentPosition;
int? _lastFastLocationSentLatE6;
int? _lastFastLocationSentLonE6;
// Firmware-mirrored fast-GPS motion state (app-fallback path). See
// MyMesh::updateGpsStatusCache / maybeSendFastGpsUpdate.
int? _fastGpsAnchorLatE6;
int? _fastGpsAnchorLonE6;
bool _fastGpsRefValid = false;
bool _fastGpsIsMoving = false;
DateTime? _fastGpsMoveStateSince;
DateTime? _fastGpsMoveEvalAt;
double _fastGpsSpeedKmh = 0.0;
int? _fastGpsSpeedPrevLatE6;
int? _fastGpsSpeedPrevLonE6;
DateTime? _fastGpsSpeedPrevAt;
DateTime? _fastGpsRxHoldoffUntil;
// ============================================================================
// Callback Properties
@@ -146,8 +189,11 @@ class LocationTrackingService {
/// Called when tracking state changes
void Function(bool isTracking)? onTrackingStateChanged;
/// Called when a fast private GPS update should be sent
void Function(Position position, String reason)? onFastLocationUpdate;
/// Called when a fast private GPS update should be sent. Carries the resolved
/// beacon coordinates (de-jittered anchor when parked) and EMA ground speed in
/// km/h, so the caller just encodes and transmits.
void Function(double latitude, double longitude, int speedKmh, String reason)?
onFastLocationUpdate;
// ============================================================================
// Initialization
@@ -418,6 +464,7 @@ class LocationTrackingService {
isTracking = false;
onTrackingStateChanged?.call(false);
_refreshFastLocationTimer();
_resetFastGpsMotionState();
// Reset first position flag so next connection starts fresh
_firstPositionSet = false;
@@ -533,6 +580,9 @@ class LocationTrackingService {
Future<void> setFastLocationUpdatesEnabled(bool enabled) async {
fastLocationUpdatesEnabled = enabled;
if (!enabled) {
_resetFastGpsMotionState();
}
await saveSettings();
_refreshFastLocationTimer();
}
@@ -560,23 +610,35 @@ class LocationTrackingService {
_refreshFastLocationTimer();
}
/// Arms the post-RX send hold-off after hearing a peer's fast-GPS beacon, so
/// the phone yields the channel briefly to avoid collisions
/// (FAST_GPS_CHANNEL_RX_HOLDOFF_MS). Called by the app's incoming-beacon path.
void noteFastGpsBeaconHeard() {
_fastGpsRxHoldoffUntil = DateTime.now().add(_fastLocationRxHoldoff);
}
/// Clears all fast-GPS motion/anchor state. Mirrors resetFastGpsShareState.
void _resetFastGpsMotionState() {
_lastFastLocationSentAt = null;
_lastFastLocationSentLatE6 = null;
_lastFastLocationSentLonE6 = null;
_fastGpsAnchorLatE6 = null;
_fastGpsAnchorLonE6 = null;
_fastGpsRefValid = false;
_fastGpsIsMoving = false;
_fastGpsMoveStateSince = null;
_fastGpsMoveEvalAt = null;
_fastGpsSpeedKmh = 0.0;
_fastGpsSpeedPrevLatE6 = null;
_fastGpsSpeedPrevLonE6 = null;
_fastGpsSpeedPrevAt = null;
_fastGpsRxHoldoffUntil = null;
}
void _evaluateFastLocationMovement(Position position) {
if (!fastLocationUpdatesEnabled || fastLocationChannelIdx == null) return;
final previous = _lastFastLocationSentPosition;
if (previous == null) {
_emitFastLocationUpdate(position, reason: 'initial');
return;
}
final distance = Geolocator.distanceBetween(
previous.latitude,
previous.longitude,
position.latitude,
position.longitude,
);
if (distance >= fastLocationMovementThresholdMeters) {
_emitFastLocationUpdate(position, reason: 'movement');
}
_updateFastGpsMotionState(position);
_maybeSendFastLocation();
}
void _refreshFastLocationTimer() {
@@ -584,75 +646,213 @@ class LocationTrackingService {
_fastLocationTimer = null;
if (!isTracking ||
!fastLocationUpdatesEnabled ||
fastLocationChannelIdx == null ||
!_isFastLocationActiveUse) {
fastLocationChannelIdx == null) {
return;
}
_fastLocationTimer = Timer.periodic(
Duration(seconds: fastLocationActiveCadenceSeconds),
(_) {
final position = currentPosition;
if (position == null) return;
_emitFastLocationUpdate(position, reason: 'active_use');
},
// Always tick (even when parked) so the stationary keepalive fires without a
// position-stream event. Tick fast while in active use; otherwise just often
// enough to drive the 9-min keepalive cheaply.
final period = _isFastLocationActiveUse
? _fastLocationActiveTick
: _fastLocationStationaryInterval;
_fastLocationTimer = Timer.periodic(period, (_) => _maybeSendFastLocation());
}
/// Updates the EMA ground speed and the moving/stationary anchor state machine
/// from a fresh fix. Faithful port of MyMesh::updateGpsStatusCache.
void _updateFastGpsMotionState(Position position) {
// Fix-quality gate (firmware: satellitesCount >= FAST_GPS_MIN_SATS).
final accuracy = position.accuracy;
if (accuracy.isFinite &&
accuracy > 0 &&
accuracy > _fastLocationMaxAccuracyMeters) {
return;
}
final now = DateTime.now();
final latE6 = (position.latitude * 1e6).round();
final lonE6 = (position.longitude * 1e6).round();
// Ground speed from successive fixes, EMA-smoothed (0.4 prev / 0.6 inst).
if (_fastGpsSpeedPrevLatE6 == null || _fastGpsSpeedPrevAt == null) {
_fastGpsSpeedPrevLatE6 = latE6;
_fastGpsSpeedPrevLonE6 = lonE6;
_fastGpsSpeedPrevAt = now;
} else {
final distM = _distanceE6(
_fastGpsSpeedPrevLatE6!,
_fastGpsSpeedPrevLonE6!,
latE6,
lonE6,
);
final dtMs = now.difference(_fastGpsSpeedPrevAt!).inMilliseconds;
if (distM >= _fastLocationSpeedGateMeters && dtMs >= 1000) {
final instKmh = (distM / 1000.0) / (dtMs / 3600000.0);
if (instKmh <= _fastLocationSpeedGlitchMaxKmh) {
_fastGpsSpeedKmh = _fastGpsSpeedKmh * 0.4 + instKmh * 0.6;
}
_fastGpsSpeedPrevLatE6 = latE6;
_fastGpsSpeedPrevLonE6 = lonE6;
_fastGpsSpeedPrevAt = now;
} else if (dtMs >= _fastLocationSpeedRebaseline.inMilliseconds) {
_fastGpsSpeedKmh = 0.0;
_fastGpsSpeedPrevLatE6 = latE6;
_fastGpsSpeedPrevLonE6 = lonE6;
_fastGpsSpeedPrevAt = now;
}
}
// Moving/stationary state machine (anchor + dwell hysteresis), ~1 Hz.
final evalAt = _fastGpsMoveEvalAt;
if (evalAt != null && now.difference(evalAt) < _fastLocationMoveEval) {
return;
}
_fastGpsMoveEvalAt = now;
if (!_fastGpsRefValid) {
_fastGpsAnchorLatE6 = latE6;
_fastGpsAnchorLonE6 = lonE6;
_fastGpsRefValid = true;
_fastGpsMoveStateSince = null;
_fastGpsIsMoving = false;
return;
}
final refDist = _distanceE6(
_fastGpsAnchorLatE6!,
_fastGpsAnchorLonE6!,
latE6,
lonE6,
);
if (!_fastGpsIsMoving) {
if (refDist > _fastLocationMoveRadiusMeters) {
if (_fastGpsMoveStateSince == null) {
_fastGpsMoveStateSince = now;
} else if (now.difference(_fastGpsMoveStateSince!) >=
_fastLocationMoveDwell) {
_fastGpsIsMoving = true;
_fastGpsAnchorLatE6 = latE6;
_fastGpsAnchorLonE6 = lonE6;
_fastGpsMoveStateSince = null;
}
} else {
_fastGpsMoveStateSince = null;
// Track slow GPS bias so cumulative drift never reaches the radius.
_fastGpsAnchorLatE6 =
_fastGpsAnchorLatE6! + ((latE6 - _fastGpsAnchorLatE6!) ~/ 8);
_fastGpsAnchorLonE6 =
_fastGpsAnchorLonE6! + ((lonE6 - _fastGpsAnchorLonE6!) ~/ 8);
}
} else {
if (refDist > _fastLocationMoveRadiusMeters) {
_fastGpsAnchorLatE6 = latE6;
_fastGpsAnchorLonE6 = lonE6;
_fastGpsMoveStateSince = null;
} else if (_fastGpsMoveStateSince == null) {
_fastGpsMoveStateSince = now;
} else if (now.difference(_fastGpsMoveStateSince!) >=
_fastLocationStopDwell) {
_fastGpsIsMoving = false;
_fastGpsAnchorLatE6 = latE6;
_fastGpsAnchorLonE6 = lonE6;
_fastGpsMoveStateSince = null;
}
}
}
/// Decides whether to emit a beacon now, mirroring maybeSendFastGpsUpdate:
/// moving → speed-tier cadence once past the movement threshold; parked →
/// flat 9-min keepalive of the stable anchor. Honours the RX hold-off.
void _maybeSendFastLocation() {
if (!fastLocationUpdatesEnabled || fastLocationChannelIdx == null) return;
final position = currentPosition;
if (position == null) return;
// No valid anchor yet means no good fix has landed — don't beacon (the
// firmware likewise bails until it has a usable fix).
if (!_fastGpsRefValid) return;
final int reportLatE6;
final int reportLonE6;
if (_fastGpsIsMoving) {
reportLatE6 = (position.latitude * 1e6).round();
reportLonE6 = (position.longitude * 1e6).round();
} else {
// Parked: report the stable anchor, not the wander.
reportLatE6 = _fastGpsAnchorLatE6!;
reportLonE6 = _fastGpsAnchorLonE6!;
}
final now = DateTime.now();
final lastAt = _lastFastLocationSentAt;
bool shouldSend = lastAt == null || _lastFastLocationSentLatE6 == null;
String reason = 'initial';
if (!shouldSend) {
if (_fastGpsIsMoving) {
final distM = _distanceE6(
_lastFastLocationSentLatE6!,
_lastFastLocationSentLonE6!,
reportLatE6,
reportLonE6,
);
if (distM > fastLocationMovementThresholdMeters) {
final elapsed = now.difference(lastAt!);
final interval = _movingIntervalForSpeedKmh(_fastGpsSpeedKmh);
shouldSend = elapsed >= interval;
reason = 'movement';
}
} else if (now.difference(lastAt!) >= _fastLocationStationaryInterval) {
shouldSend = true;
reason = 'stationary';
}
}
if (!shouldSend) return;
// Yield the channel briefly after hearing a peer beacon.
final holdoff = _fastGpsRxHoldoffUntil;
if (holdoff != null) {
if (now.isBefore(holdoff)) return;
_fastGpsRxHoldoffUntil = null;
}
_lastFastLocationSentLatE6 = reportLatE6;
_lastFastLocationSentLonE6 = reportLonE6;
_lastFastLocationSentAt = now;
final speedKmh = _fastGpsSpeedKmh < 0
? 0
: (_fastGpsSpeedKmh > 255.0 ? 255 : (_fastGpsSpeedKmh + 0.5).floor());
onFastLocationUpdate?.call(
reportLatE6 / 1e6,
reportLonE6 / 1e6,
speedKmh,
reason,
);
}
void _emitFastLocationUpdate(Position position, {required String reason}) {
if (!fastLocationUpdatesEnabled || fastLocationChannelIdx == null) return;
final now = DateTime.now();
final previous = _lastFastLocationSentPosition;
final previousTime = _lastFastLocationSentAt;
if (previous != null && previousTime != null) {
final distance = Geolocator.distanceBetween(
previous.latitude,
previous.longitude,
position.latitude,
position.longitude,
);
final elapsedMs = now.difference(previousTime).inMilliseconds;
final minimumInterval = _fastLocationMinimumIntervalFor(
distanceMeters: distance,
elapsedMs: elapsedMs,
);
if (elapsedMs < minimumInterval.inMilliseconds) {
return;
}
if (distance < 1.0 && elapsedMs < 3000) {
return;
}
} else if (previousTime != null &&
now.difference(previousTime) < _fastLocationSlowInterval) {
return;
Duration _movingIntervalForSpeedKmh(double speedKmh) {
final speedMps = speedKmh / 3.6;
if (speedMps < _fastLocationIdleSpeedMaxMetersPerSecond) {
return _fastLocationStationaryInterval;
}
_lastFastLocationSentPosition = position;
_lastFastLocationSentAt = now;
onFastLocationUpdate?.call(position, reason);
}
Duration _fastLocationMinimumIntervalFor({
required double distanceMeters,
required int elapsedMs,
}) {
if (elapsedMs <= 0) {
return _fastLocationSlowInterval;
}
final speedMetersPerSecond = distanceMeters / (elapsedMs / 1000.0);
if (speedMetersPerSecond < _fastLocationIdleSpeedMaxMetersPerSecond) {
return _fastLocationSlowInterval;
}
if (speedMetersPerSecond < _fastLocationWalkingSpeedMaxMetersPerSecond) {
if (speedMps < _fastLocationWalkingSpeedMaxMetersPerSecond) {
return _fastLocationWalkingInterval;
}
if (speedMetersPerSecond < _fastLocationFastSpeedMaxMetersPerSecond) {
if (speedMps < _fastLocationFastSpeedMaxMetersPerSecond) {
return _fastLocationFastInterval;
}
return _fastLocationVeryFastInterval;
}
double _distanceE6(int latA, int lonA, int latB, int lonB) {
return Geolocator.distanceBetween(
latA / 1e6,
lonA / 1e6,
latB / 1e6,
lonB / 1e6,
);
}
// ============================================================================
// Mesh Network Broadcasting
// ============================================================================

View File

@@ -5,18 +5,23 @@ import 'package:nsd/nsd.dart';
/// Discovered MeshCore device on the network (TCP/WiFi)
class DiscoveredServer {
final String name;
final String ipAddress;
final int port;
final int responseTime; // milliseconds
const DiscoveredServer({
required this.name,
required this.ipAddress,
required this.port,
required this.responseTime,
});
String get displayName => name.trim().isNotEmpty ? name.trim() : ipAddress;
@override
String toString() => 'DiscoveredServer($ipAddress:$port, ${responseTime}ms)';
String toString() =>
'DiscoveredServer($displayName @ $ipAddress:$port, ${responseTime}ms)';
@override
bool operator ==(Object other) =>
@@ -46,6 +51,7 @@ class NetworkScannerService {
bool _isScanning = false;
bool get isScanning => _isScanning;
int _scanSession = 0;
List<DiscoveredServer> _cachedServers = [];
List<DiscoveredServer> get cachedServers => List.unmodifiable(_cachedServers);
@@ -88,7 +94,11 @@ class NetworkScannerService {
}
/// Try a raw TCP connect to check if the MeshCore TCP server is listening.
Future<DiscoveredServer?> _checkDevice(String ip, int port) async {
Future<DiscoveredServer?> _checkDevice(
String ip,
int port, {
String? name,
}) async {
final sw = Stopwatch()..start();
Socket? socket;
try {
@@ -101,6 +111,7 @@ class NetworkScannerService {
debugPrint(
'✅ [NetworkScanner] Found device at $ip:$port (${sw.elapsedMilliseconds}ms)');
return DiscoveredServer(
name: (name ?? ip).trim(),
ipAddress: ip,
port: port,
responseTime: sw.elapsedMilliseconds,
@@ -117,7 +128,10 @@ class NetworkScannerService {
// ── mDNS discovery ─────────────────────────────────────────────────────────
Future<List<DiscoveredServer>> _discoverViaMdns({int? port}) async {
Future<List<DiscoveredServer>> _discoverViaMdns({
int? port,
required int scanSession,
}) async {
final scanPort = port ?? defaultPort;
final found = <DiscoveredServer>[];
@@ -131,12 +145,25 @@ class NetworkScannerService {
);
await Future.delayed(bonjourTimeout);
if (!_isScanSessionActive(scanSession)) {
return found;
}
for (final service in _activeDiscovery?.services ?? []) {
if (!_isScanSessionActive(scanSession)) {
break;
}
for (final addr in service.addresses ?? []) {
if (!_isScanSessionActive(scanSession)) {
break;
}
if (localIps.contains(addr.address)) continue;
final result =
await _checkDevice(addr.address, service.port ?? scanPort);
await _checkDevice(
addr.address,
service.port ?? scanPort,
name: service.name,
);
if (result != null) {
found.add(result);
onServerDiscovered?.call(result);
@@ -163,7 +190,10 @@ class NetworkScannerService {
// ── Port scan fallback ─────────────────────────────────────────────────────
Future<List<DiscoveredServer>> _scanByPort({int? port}) async {
Future<List<DiscoveredServer>> _scanByPort({
int? port,
required int scanSession,
}) async {
final scanPort = port ?? defaultPort;
final found = <DiscoveredServer>[];
@@ -175,10 +205,12 @@ class NetworkScannerService {
'🔍 [NetworkScanner] Port scan: ${ips.length} IPs, port $scanPort');
int scanned = 0;
for (int i = 0; i < ips.length; i += parallelScans) {
for (int i = 0; i < ips.length && _isScanSessionActive(scanSession); i += parallelScans) {
final batch = ips.skip(i).take(parallelScans).toList();
final results =
await Future.wait(batch.map((ip) => _checkDevice(ip, scanPort)));
await Future.wait(
batch.map((ip) => _checkDevice(ip, scanPort, name: ip)),
);
for (final result in results) {
if (result != null && !localIps.contains(result.ipAddress)) {
@@ -200,28 +232,51 @@ class NetworkScannerService {
Future<List<DiscoveredServer>> scan({int? port}) async {
if (_isScanning) return [];
_isScanning = true;
final scanSession = ++_scanSession;
try {
var found = await _discoverViaMdns(port: port);
if (found.isEmpty) {
var found = await _discoverViaMdns(port: port, scanSession: scanSession);
if (_isScanSessionActive(scanSession) && found.isEmpty) {
debugPrint(
'🔍 [NetworkScanner] mDNS found nothing, falling back to port scan');
found = await _scanByPort(port: port);
found = await _scanByPort(port: port, scanSession: scanSession);
}
if (_isScanSessionActive(scanSession)) {
_cachedServers = found;
}
_cachedServers = found;
return found;
} finally {
_isScanning = false;
if (_scanSession == scanSession) {
_isScanning = false;
}
}
}
/// Verify a previously discovered device is still reachable.
Future<bool> verifyServer(DiscoveredServer server) async {
final result = await _checkDevice(server.ipAddress, server.port);
final result = await _checkDevice(
server.ipAddress,
server.port,
name: server.name,
);
return result != null;
}
void clearCache() => _cachedServers = [];
void stopScan() => _isScanning = false;
bool _isScanSessionActive(int session) => _isScanning && _scanSession == session;
void stopScan() {
_scanSession += 1;
_isScanning = false;
final activeDiscovery = _activeDiscovery;
_activeDiscovery = null;
if (activeDiscovery != null) {
unawaited(
stopDiscovery(activeDiscovery).catchError((Object error) {
debugPrint('⚠️ [NetworkScanner] Failed to stop mDNS discovery: $error');
}),
);
}
}
}

View File

@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -12,6 +14,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';
@@ -29,6 +34,13 @@ class NotificationService {
bool _muteForegroundNotifications = true;
AppLifecycleState _lifecycleState = AppLifecycleState.resumed;
String? _launchPayload;
Timer? _messageNotificationTimer;
int _pendingMessageNotificationCount = 0;
String? _pendingMessageSenderName;
String? _pendingMessageText;
bool _pendingMessageIsChannel = false;
String? _pendingMessageChannelName;
AppLocalizations? _pendingMessageLocalizations;
// Notification IDs
static const int _sarNotificationId = 1000;
@@ -36,6 +48,8 @@ class NotificationService {
static const int _updateNotificationId = 3000;
static const int _batteryNotificationId = 4000;
static const int _discoveryNotificationId = 5000;
static const Duration _messageNotificationCoalesceWindow =
Duration(seconds: 2);
// Notification channels
static const String _urgentChannelId = 'sar_urgent';
@@ -90,9 +104,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 +128,11 @@ class NotificationService {
}
// Request permissions
await _requestPermissions();
if (_skipPermissionRequests) {
_permissionGranted = true;
} else {
await _requestPermissions();
}
await _loadPreferences();
// Create notification channels (Android)
@@ -327,6 +345,8 @@ class NotificationService {
String? notes,
AppLocalizations? localizations,
}) async {
if (_skipPermissionRequests) return;
if (!_isInitialized) {
debugPrint(
'⚠️ [NotificationService] Not initialized, skipping notification',
@@ -509,6 +529,8 @@ class NotificationService {
String? channelName,
AppLocalizations? localizations,
}) async {
if (_skipPermissionRequests) return;
if (!_isInitialized) {
debugPrint(
'⚠️ [NotificationService] Not initialized, skipping notification',
@@ -533,17 +555,53 @@ class NotificationService {
return;
}
try {
// Generate unique notification ID based on timestamp
final notificationId =
_messageNotificationId +
(DateTime.now().millisecondsSinceEpoch % 1000);
_pendingMessageNotificationCount++;
_pendingMessageSenderName = senderName;
_pendingMessageText = messageText;
_pendingMessageIsChannel = isChannelMessage;
_pendingMessageChannelName = channelName;
_pendingMessageLocalizations = localizations;
_messageNotificationTimer?.cancel();
_messageNotificationTimer = Timer(_messageNotificationCoalesceWindow, () {
unawaited(_flushMessageNotification());
});
}
Future<void> _flushMessageNotification() async {
final count = _pendingMessageNotificationCount;
final senderName = _pendingMessageSenderName;
final messageText = _pendingMessageText;
final isChannelMessage = _pendingMessageIsChannel;
final channelName = _pendingMessageChannelName;
final localizations = _pendingMessageLocalizations;
_pendingMessageNotificationCount = 0;
_pendingMessageSenderName = null;
_pendingMessageText = null;
_pendingMessageIsChannel = false;
_pendingMessageChannelName = null;
_pendingMessageLocalizations = null;
if (count == 0 || senderName == null || messageText == null) {
return;
}
if (!_isInitialized ||
!_permissionGranted ||
!_messageNotificationsEnabled ||
_shouldSuppressForegroundNotifications()) {
return;
}
try {
// Build notification title and body
final resolvedChannelName = channelName?.trim().isNotEmpty == true
? channelName!.trim()
: (localizations?.publicChannel ?? 'Public');
final title = isChannelMessage
final title = count > 1
? (localizations?.messages ?? 'Messages')
: isChannelMessage
? (localizations != null
? '${localizations.channel}: $resolvedChannelName'
: 'Channel: $resolvedChannelName')
@@ -551,7 +609,9 @@ class NotificationService {
? '${localizations.newMessage} ${localizations.from} $senderName'
: 'New message from $senderName');
final body = messageText.length > 200
final body = count > 1
? 'You have $count new messages'
: messageText.length > 200
? '${messageText.substring(0, 200)}...'
: messageText;
@@ -570,7 +630,7 @@ class NotificationService {
styleInformation: BigTextStyleInformation(
body,
contentTitle: title,
summaryText: senderName,
summaryText: count > 1 ? body : senderName,
),
);
@@ -594,7 +654,7 @@ class NotificationService {
// Show notification
await _notificationsPlugin.show(
id: notificationId,
id: _messageNotificationId,
title: title,
body: body,
notificationDetails: notificationDetails,
@@ -602,6 +662,7 @@ class NotificationService {
);
debugPrint('✅ [NotificationService] Showed message notification');
debugPrint(' Count: $count');
debugPrint(' Sender: $senderName');
debugPrint(' Type: ${isChannelMessage ? "Channel" : "Direct"}');
} catch (e) {
@@ -672,6 +733,8 @@ class NotificationService {
required String downloadUrl,
AppLocalizations? localizations,
}) async {
if (_skipPermissionRequests) return;
if (!_isInitialized) {
debugPrint(
'⚠️ [NotificationService] Not initialized, skipping notification',
@@ -772,6 +835,8 @@ class NotificationService {
required double batteryPercent,
required bool isCurrentDevice,
}) async {
if (_skipPermissionRequests) return false;
if (!_isInitialized) {
debugPrint(
'⚠️ [NotificationService] Not initialized, skipping notification',
@@ -863,6 +928,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

@@ -3,7 +3,7 @@ import 'package:flutter_map/flutter_map.dart';
import 'offline_tile_cache_service.dart';
/// A [MapCachingProvider] that checks the offline AVIF tile cache before
/// A [MapCachingProvider] that checks the offline tile cache before
/// falling through to the built-in cache/network path.
///
/// This allows preloaded tiles to be served during normal map browsing.
@@ -23,12 +23,16 @@ class OfflineMapCachingProvider implements MapCachingProvider {
if (coords != null) {
final styleHash = _cache.styleHashFromUrl(_extractUrlTemplate(url));
// Check local AVIF cache first
final pngBytes = await _cache.getTileAsPng(
styleHash, coords.z, coords.x, coords.y);
if (pngBytes != null) {
// Check local offline cache first
final cachedTile = await _cache.getTileData(
styleHash,
coords.z,
coords.x,
coords.y,
);
if (cachedTile != null) {
return (
bytes: pngBytes,
bytes: cachedTile.bytes,
metadata: CachedMapTileMetadata(
staleAt: DateTime.now().add(const Duration(days: 365)),
lastModified: null,

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,102 @@
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
class RecentTcpConnection {
final String name;
final String host;
final int port;
final DateTime lastUsedAt;
const RecentTcpConnection({
required this.name,
required this.host,
required this.port,
required this.lastUsedAt,
});
Map<String, Object> toJson() => <String, Object>{
'name': name,
'host': host,
'port': port,
'lastUsedAt': lastUsedAt.toIso8601String(),
};
factory RecentTcpConnection.fromJson(Map<String, dynamic> json) {
return RecentTcpConnection(
name: (json['name'] as String?)?.trim().isNotEmpty == true
? (json['name'] as String).trim()
: ((json['host'] as String?) ?? '').trim(),
host: ((json['host'] as String?) ?? '').trim(),
port: (json['port'] as num?)?.toInt() ?? 0,
lastUsedAt:
DateTime.tryParse((json['lastUsedAt'] as String?) ?? '') ??
DateTime.fromMillisecondsSinceEpoch(0),
);
}
}
class RecentTcpConnectionsService {
static const String _prefsKey = 'recent_tcp_connections_v1';
static const int _maxEntries = 5;
static Future<List<RecentTcpConnection>> load() async {
final prefs = await SharedPreferences.getInstance();
final storedEntries = prefs.getStringList(_prefsKey) ?? const <String>[];
final connections = <RecentTcpConnection>[];
for (final entry in storedEntries) {
try {
final decoded = jsonDecode(entry);
if (decoded is! Map<String, dynamic>) {
continue;
}
final connection = RecentTcpConnection.fromJson(decoded);
if (connection.host.isEmpty || connection.port <= 0) {
continue;
}
connections.add(connection);
} catch (_) {
continue;
}
}
connections.sort((a, b) => b.lastUsedAt.compareTo(a.lastUsedAt));
return connections.take(_maxEntries).toList(growable: false);
}
static Future<List<RecentTcpConnection>> remember({
required String name,
required String host,
required int port,
}) async {
final trimmedHost = host.trim();
if (trimmedHost.isEmpty || port <= 0) {
return load();
}
final normalizedName = name.trim().isNotEmpty ? name.trim() : trimmedHost;
final existing = await load();
final updated = <RecentTcpConnection>[
RecentTcpConnection(
name: normalizedName,
host: trimmedHost,
port: port,
lastUsedAt: DateTime.now(),
),
...existing.where(
(connection) =>
connection.host != trimmedHost || connection.port != port,
),
].take(_maxEntries).toList(growable: false);
final prefs = await SharedPreferences.getInstance();
await prefs.setStringList(
_prefsKey,
updated
.map((connection) => jsonEncode(connection.toJson()))
.toList(growable: false),
);
return updated;
}
}

View File

@@ -84,6 +84,7 @@ class TileDownloadService {
required int maxZoom,
required String urlTemplate,
String? displayName,
Map<String, String>? headers,
int maxConcurrency = 6,
int rateLimit = 30,
}) {
@@ -96,6 +97,7 @@ class TileDownloadService {
maxZoom: maxZoom,
urlTemplate: urlTemplate,
displayName: displayName,
headers: headers,
maxConcurrency: maxConcurrency,
rateLimit: rateLimit,
);
@@ -110,6 +112,7 @@ class TileDownloadService {
required int maxZoom,
required String urlTemplate,
String? displayName,
Map<String, String>? headers,
required int maxConcurrency,
required int rateLimit,
}) async {
@@ -184,7 +187,7 @@ class TileDownloadService {
break;
}
final future = _downloadSingleTile(tile, urlTemplate, styleHash)
final future = _downloadSingleTile(tile, urlTemplate, styleHash, headers)
.then((event) {
if (!controller.isClosed) {
controller.add(event);
@@ -220,6 +223,7 @@ class TileDownloadService {
TileCoord tile,
String urlTemplate,
String styleHash,
Map<String, String>? extraHeaders,
) async {
final bounds = TileMathService.tileBounds(tile.x, tile.y, tile.z);
@@ -241,7 +245,7 @@ class TileDownloadService {
try {
final response = await _httpClient.get(
Uri.parse(url),
headers: {'User-Agent': 'MeshCoreSAR/1.0'},
headers: {'User-Agent': 'MeshCoreSAR/1.0', ...?extraHeaders},
);
if (response.statusCode != 200) {
@@ -252,9 +256,15 @@ class TileDownloadService {
return TileFailed(tile, 'HTTP ${response.statusCode}');
}
// Store tile (PNG → AVIF conversion happens inside cache service)
await _cache.putTile(
styleHash, tile.z, tile.x, tile.y, response.bodyBytes);
styleHash,
tile.z,
tile.x,
tile.y,
response.bodyBytes,
contentType: response.headers['content-type'],
sourceUrl: url,
);
return TileDownloaded(
north: bounds.north,

View File

@@ -36,6 +36,16 @@ class PeerCatalog {
const PeerCatalog({required this.peer, required this.styles});
}
class PeerTileResponse {
final Uint8List bytes;
final String? contentType;
const PeerTileResponse({
required this.bytes,
required this.contentType,
});
}
/// Progress events during a P2P sync.
sealed class PeerSyncEvent {}
@@ -69,13 +79,13 @@ class PeerSyncComplete extends PeerSyncEvent {
class PeerSyncCancelled extends PeerSyncEvent {}
/// HTTP server that serves cached AVIF tiles to other devices on the
/// HTTP server that serves cached tiles to other devices on the
/// local network, with mDNS advertisement, peer discovery, and P2P sync.
///
/// Protocol:
/// GET /styles → JSON array of StyleInfo
/// GET /tiles/{hash}/list → JSON array of {z, x, y}
/// GET /tiles/{hash}/{z}/{x}/{y}.avif → AVIF bytes | 404
/// GET /tiles/{hash}/{z}/{x}/{y} → tile bytes | 404
class TileSharingService {
TileSharingService._();
static final instance = TileSharingService._();
@@ -222,8 +232,8 @@ class TileSharingService {
}
}
/// Fetch a single tile from a peer. Returns raw AVIF bytes or null.
Future<Uint8List?> fetchTileFromPeer(
/// Fetch a single tile from a peer. Returns raw tile bytes or null.
Future<PeerTileResponse?> fetchTileFromPeer(
TilePeer peer,
String styleHash,
int z,
@@ -231,11 +241,15 @@ class TileSharingService {
int y,
) async {
try {
final uri =
Uri.parse('${peer.baseUrl}/tiles/$styleHash/$z/$x/$y.avif');
final uri = Uri.parse('${peer.baseUrl}/tiles/$styleHash/$z/$x/$y');
final response =
await _httpClient.get(uri).timeout(const Duration(seconds: 5));
if (response.statusCode == 200) return response.bodyBytes;
if (response.statusCode == 200) {
return PeerTileResponse(
bytes: response.bodyBytes,
contentType: response.headers['content-type'],
);
}
} catch (e) {
// Silently fail — caller will try next peer
}
@@ -243,15 +257,15 @@ class TileSharingService {
}
/// Try fetching a tile from any available peer (for the caching provider).
Future<Uint8List?> fetchFromAnyPeer(
Future<PeerTileResponse?> fetchFromAnyPeer(
String styleHash,
int z,
int x,
int y,
) async {
for (final peer in _discoveredPeers) {
final bytes = await fetchTileFromPeer(peer, styleHash, z, x, y);
if (bytes != null) return bytes;
final tile = await fetchTileFromPeer(peer, styleHash, z, x, y);
if (tile != null) return tile;
}
return null;
}
@@ -299,6 +313,7 @@ class TileSharingService {
styleHash,
displayName: styleMeta.displayName,
urlTemplate: styleMeta.urlTemplate,
region: styleMeta.region,
);
// Collect tile lists from all peers and merge (union)
@@ -356,20 +371,26 @@ class TileSharingService {
}
// Try this peer, then fallback to others
Uint8List? bytes =
PeerTileResponse? tileResponse =
await fetchTileFromPeer(peer, styleHash, tile.z, tile.x, tile.y);
if (bytes == null) {
if (tileResponse == null) {
for (final fallback in peers) {
if (fallback == peer) continue;
bytes = await fetchTileFromPeer(
tileResponse = await fetchTileFromPeer(
fallback, styleHash, tile.z, tile.x, tile.y);
if (bytes != null) break;
if (tileResponse != null) break;
}
}
if (bytes != null) {
if (tileResponse != null) {
await _cache.putRawTile(
styleHash, tile.z, tile.x, tile.y, bytes);
styleHash,
tile.z,
tile.x,
tile.y,
tileResponse.bytes,
contentType: tileResponse.contentType,
);
downloaded++;
controller.add(PeerSyncTileDownloaded(
downloaded: downloaded, total: total));
@@ -457,9 +478,9 @@ class TileSharingService {
return;
}
// GET /tiles/{hash}/{z}/{x}/{y}.avif → tile bytes
// GET /tiles/{hash}/{z}/{x}/{y} → tile bytes
final tilePattern =
RegExp(r'^/tiles/([a-f0-9]+)/(\d+)/(\d+)/(\d+)\.avif$');
RegExp(r'^/tiles/([a-f0-9]+)/(\d+)/(\d+)/(\d+)(?:\.avif)?$');
final tileMatch = tilePattern.firstMatch(path);
if (tileMatch != null) {
final styleHash = tileMatch.group(1)!;
@@ -467,12 +488,14 @@ class TileSharingService {
final x = int.parse(tileMatch.group(3)!);
final y = int.parse(tileMatch.group(4)!);
final bytes = await _cache.getRawTile(styleHash, z, x, y);
if (bytes != null) {
final tile = await _cache.getTileData(styleHash, z, x, y);
if (tile != null) {
request.response
..statusCode = HttpStatus.ok
..headers.contentType = ContentType('image', 'avif')
..add(bytes);
..headers.contentType = tile.contentType == null
? ContentType.binary
: ContentType.parse(tile.contentType!)
..add(tile.bytes);
await request.response.close();
return;
}

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

@@ -0,0 +1,220 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:geolocator/geolocator.dart';
import 'package:provider/provider.dart';
import 'package:vibration/vibration.dart';
import '../l10n/app_localizations.dart';
import '../providers/connection_provider.dart';
import 'toast_logger.dart';
enum AdvertMode { flood, direct }
Future<void> triggerAdvertFeedback(BuildContext context) async {
final platform = Theme.of(context).platform;
try {
if (platform == TargetPlatform.iOS) {
await HapticFeedback.lightImpact();
await Future.delayed(const Duration(milliseconds: 50));
await HapticFeedback.lightImpact();
} else {
if (await Vibration.hasVibrator()) {
await Vibration.vibrate(duration: 50);
} else {
await HapticFeedback.mediumImpact();
}
}
} catch (e) {
debugPrint('Haptic feedback error: $e');
await HapticFeedback.vibrate();
}
}
Future<AdvertMode?> showAdvertModeSheet(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final theme = Theme.of(context);
return showModalBottomSheet<AdvertMode>(
context: context,
showDragHandle: true,
builder: (context) => SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 8, 20, 20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.advertMode,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 6),
Text(
l10n.advertModeSubtitle,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 16),
ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 4,
vertical: 4,
),
leading: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: theme.colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(14),
),
child: Icon(
Icons.hub_rounded,
color: theme.colorScheme.onPrimaryContainer,
),
),
title: Text(l10n.flood),
subtitle: Text(l10n.relayThroughRepeatersAcrossTheMesh),
trailing: const Icon(Icons.chevron_right_rounded),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
onTap: () => Navigator.of(context).pop(AdvertMode.flood),
),
const SizedBox(height: 8),
ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 4,
vertical: 4,
),
leading: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: theme.colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(14),
),
child: Icon(
Icons.near_me_rounded,
color: theme.colorScheme.onSecondaryContainer,
),
),
title: Text(l10n.direct),
subtitle: Text(l10n.nearbyOnlyWithoutRepeaterFlooding),
trailing: const Icon(Icons.chevron_right_rounded),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
onTap: () => Navigator.of(context).pop(AdvertMode.direct),
),
],
),
),
),
);
}
Future<void> advertiseDevice(
BuildContext context, {
bool floodMode = true,
}) async {
final connectionProvider = context.read<ConnectionProvider>();
if (!connectionProvider.deviceInfo.isConnected) {
if (context.mounted) {
ToastLogger.error(
context,
AppLocalizations.of(context)!.deviceNotConnected,
);
}
return;
}
try {
// Check if location services are enabled
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
if (context.mounted) {
ToastLogger.error(
context,
AppLocalizations.of(context)!.locationServicesDisabled,
);
}
return;
}
// Check location permissions
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
if (context.mounted) {
ToastLogger.error(
context,
AppLocalizations.of(context)!.locationPermissionDenied,
);
}
return;
}
}
if (permission == LocationPermission.deniedForever) {
if (context.mounted) {
ToastLogger.error(
context,
AppLocalizations.of(context)!.locationPermissionPermanentlyDenied,
);
}
return;
}
// Get current GPS position
Position? position;
try {
position = await Geolocator.getCurrentPosition(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.best,
distanceFilter: 0,
),
).timeout(const Duration(seconds: 5));
} catch (e) {
debugPrint('❌ Failed to get GPS position: $e');
if (context.mounted) {
ToastLogger.error(
context,
AppLocalizations.of(context)!.failedToGetGpsLocation,
);
}
return;
}
// Update lat/lon on device
await connectionProvider.setAdvertLatLon(
latitude: position.latitude,
longitude: position.longitude,
);
// Small delay to ensure the lat/lon is set
await Future.delayed(const Duration(milliseconds: 100));
await connectionProvider.sendSelfAdvert(floodMode: floodMode);
if (context.mounted) {
final l10n = AppLocalizations.of(context)!;
ToastLogger.success(
context,
floodMode ? l10n.floodAdvertSent : l10n.directAdvertSent,
);
}
} catch (e) {
debugPrint('❌ Failed to advertise device: $e');
if (context.mounted) {
ToastLogger.error(
context,
AppLocalizations.of(context)!.failedToAdvertise(e.toString()),
);
}
}
}

Some files were not shown because too many files have changed in this diff Show More