Refactor SAR marker handling and add template management

- Updated CompassSarList and DetailedCompassDialog to use marker.displayName instead of marker.type.displayName.
- Enhanced DrawingLayer and DrawingMarkersLayer to support simple mode for drawing visibility and interaction.
- Added toggle switches in DrawingToolbar for showing/hiding received drawings and SAR markers.
- Modified MapMarkers to utilize custom emojis and display names for markers.
- Introduced RecipientSelectorSheet for selecting message recipients with search functionality.
- Refactored SarUpdateSheet to use SAR templates instead of marker types, allowing for emoji and name customization.
- Created SarTemplateEditDialog for adding and editing SAR templates with color selection and preview.
This commit is contained in:
Janez T
2025-10-21 23:44:49 +02:00
parent e9d516b749
commit 021ce21cbe
60 changed files with 8736 additions and 1413 deletions

View File

@@ -4,7 +4,24 @@
"Bash(flutter analyze lib)", "Bash(flutter analyze lib)",
"Read(//Users/dz0ny/meshcore-sar/MeshCore/**)", "Read(//Users/dz0ny/meshcore-sar/MeshCore/**)",
"Bash(flutter analyze lib/providers/connection_provider.dart)", "Bash(flutter analyze lib/providers/connection_provider.dart)",
"Bash(flutter analyze lib/providers/connection_provider.dart --fatal-infos)" "Bash(flutter analyze lib/providers/connection_provider.dart --fatal-infos)",
"Bash(flutter analyze:*)",
"Bash(flutter gen-l10n:*)",
"WebSearch",
"WebFetch(domain:fmtc.jaffaketchup.dev)",
"WebFetch(domain:pub.dev)",
"Bash(for lang in de es fr it)",
"Bash(do echo \"=== app_$lang.arb ===\")",
"Bash(done)",
"Read(//Users/dz0ny/.pub-cache/hosted/pub.dev/flutter_map_tile_caching-10.1.1/lib/src/store/**)",
"Read(//Users/dz0ny/.pub-cache/hosted/pub.dev/flutter_map_tile_caching-10.1.1/lib/**)",
"Bash(cat:*)",
"Read(//Users/dz0ny/.pub-cache/hosted/pub.dev/**)",
"Bash(for lang in hr sl de es fr it)",
"Bash(do)",
"Bash(wc:*)",
"Bash(for lang in de es fr it hr sl)",
"Bash(do echo \"=== $lang ===\")"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []

100
.github/workflows/README.md vendored Normal file
View File

@@ -0,0 +1,100 @@
# CI/CD Workflows
## Build Multi-Platform Workflow
Automatically builds the MeshCore SAR app for Android, iOS, macOS, and Windows.
### Triggers
- **Push to main/develop**: Builds with version `1.0.0+1` (from pubspec.yaml)
- **Pull requests to main**: Test builds only
- **Version tags** (e.g., `v1.2.3`): Release builds with versioned artifacts
- **Manual**: Via GitHub Actions UI
### Creating a Release
1. **Ensure your local version is 1.0.0**:
```bash
# pubspec.yaml should show:
version: 1.0.0+1
```
2. **Commit all changes**:
```bash
git add .
git commit -m "Prepare for release"
git push origin main
```
3. **Create and push a version tag**:
```bash
git tag v1.2.3
git push origin v1.2.3
```
4. **GitHub Actions will**:
- Temporarily patch version to `1.2.3+<timestamp>` in builds only
- Build Android APK & App Bundle
- Build iOS app (unsigned - configure secrets for signed IPA)
- Build macOS DMG
- Build Windows executable (ZIP)
- Create a draft GitHub release with all artifacts
5. **Publish the release**:
- Go to GitHub Releases
- Edit the draft release
- Add release notes
- Publish
### Build Artifacts
| Platform | Artifact | Location |
|----------|----------|----------|
| Android APK | `app-release.apk` | `artifacts/android-apk/` |
| Android Bundle | `app-release.aab` | `artifacts/android-appbundle/` |
| macOS | `MeshCore-SAR.dmg` | `artifacts/macos-dmg/` |
| iOS | `Runner.app` | `artifacts/ios-build/` (unsigned) |
| Windows | `MeshCore-SAR-Windows.zip` | `artifacts/windows-executable/` |
### iOS Code Signing (Optional)
To build signed IPA files, add these repository secrets:
1. **IOS_P12_BASE64**: Base64-encoded .p12 certificate
```bash
base64 -i Certificate.p12 | pbcopy
```
2. **IOS_P12_PASSWORD**: Password for the .p12 certificate
3. **IOS_PROVISION_PROFILE_BASE64**: Base64-encoded provisioning profile
```bash
base64 -i Profile.mobileprovision | pbcopy
```
Then uncomment the signing steps in the workflow.
### Version Numbering
- **Local**: Always keep `pubspec.yaml` at `version: 1.0.0+1`
- **CI builds**: Uses `1.0.0+1` for regular commits
- **Release builds**: Uses `<tag>+<timestamp>` (e.g., `1.2.3+1729512345`)
- **No commits**: Version changes are temporary and never committed back
### Troubleshooting
**Build fails on macOS DMG creation**:
- The workflow tries `create-dmg` first, then falls back to `hdiutil`
- Check if app icon path exists at `macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png`
**Windows build fails**:
- Ensure Windows desktop is properly configured in project
- Run locally: `flutter config --enable-windows-desktop && flutter build windows`
**iOS build fails**:
- Check CocoaPods version and dependencies
- Review code signing configuration (currently set to `--no-codesign`)
**Android build fails**:
- Verify Java 17 is compatible with your Gradle version
- Check `android/build.gradle` for minimum SDK requirements

View File

@@ -0,0 +1,349 @@
name: Build Multi-Platform
on:
push:
branches: [main, develop]
tags:
- 'v*'
pull_request:
branches: [main]
workflow_dispatch:
env:
FLUTTER_VERSION: '3.24.0'
jobs:
# Update version from tag
update-version:
name: Update Version from Tag
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
outputs:
version: ${{ steps.get_version.outputs.version }}
build_number: ${{ steps.get_version.outputs.build_number }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Get version from tag
id: get_version
run: |
# Extract version from tag (e.g., v1.2.3 -> 1.2.3)
VERSION=${GITHUB_REF#refs/tags/v}
echo "version=$VERSION" >> $GITHUB_OUTPUT
# Generate build number from timestamp
BUILD_NUMBER=$(date +%s)
echo "build_number=$BUILD_NUMBER" >> $GITHUB_OUTPUT
echo "Version: $VERSION"
echo "Build Number: $BUILD_NUMBER"
- name: Update pubspec.yaml
run: |
VERSION="${{ steps.get_version.outputs.version }}"
BUILD_NUMBER="${{ steps.get_version.outputs.build_number }}"
# Update version in pubspec.yaml
sed -i.bak "s/^version: .*/version: $VERSION+$BUILD_NUMBER/" pubspec.yaml
echo "Updated pubspec.yaml:"
grep "^version:" pubspec.yaml
- name: Upload updated pubspec
uses: actions/upload-artifact@v4
with:
name: versioned-pubspec
path: pubspec.yaml
retention-days: 1
# Android APK Build
build-android:
name: Build Android APK
runs-on: ubuntu-latest
needs: [update-version]
if: always() && (needs.update-version.result == 'success' || needs.update-version.result == 'skipped')
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download versioned pubspec
if: startsWith(github.ref, 'refs/tags/v')
uses: actions/download-artifact@v4
with:
name: versioned-pubspec
path: .
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
cache: 'gradle'
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: 'stable'
cache: true
- name: Install dependencies
run: flutter pub get
- name: Generate localizations
run: flutter gen-l10n
- name: Run analyzer
run: flutter analyze
- name: Run tests
run: flutter test
- name: Build APK
run: flutter build apk --release
- name: Build App Bundle
run: flutter build appbundle --release
- name: Upload APK
uses: actions/upload-artifact@v4
with:
name: android-apk
path: build/app/outputs/flutter-apk/app-release.apk
retention-days: 30
- name: Upload App Bundle
uses: actions/upload-artifact@v4
with:
name: android-appbundle
path: build/app/outputs/bundle/release/app-release.aab
retention-days: 30
# iOS IPA Build
build-ios:
name: Build iOS IPA
runs-on: macos-latest
needs: [update-version]
if: always() && (needs.update-version.result == 'success' || needs.update-version.result == 'skipped')
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download versioned pubspec
if: startsWith(github.ref, 'refs/tags/v')
uses: actions/download-artifact@v4
with:
name: versioned-pubspec
path: .
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: 'stable'
cache: true
- name: Install dependencies
run: flutter pub get
- name: Generate localizations
run: flutter gen-l10n
- name: Install CocoaPods dependencies
run: |
cd ios
pod install
cd ..
- name: Build iOS (No Codesign)
run: flutter build ios --release --no-codesign
# For signed builds, uncomment and configure secrets:
# - name: Import Code Signing Certificates
# uses: apple-actions/import-codesign-certs@v2
# with:
# p12-file-base64: ${{ secrets.IOS_P12_BASE64 }}
# p12-password: ${{ secrets.IOS_P12_PASSWORD }}
# - name: Import Provisioning Profile
# run: |
# mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
# echo "${{ secrets.IOS_PROVISION_PROFILE_BASE64 }}" | base64 --decode > ~/Library/MobileDevice/Provisioning\ Profiles/profile.mobileprovision
# - name: Build IPA (Signed)
# run: flutter build ipa --release --export-options-plist=ios/ExportOptions.plist
- name: Upload iOS Build
uses: actions/upload-artifact@v4
with:
name: ios-build
path: build/ios/iphoneos/Runner.app
retention-days: 30
# Uncomment when signing is configured:
# - name: Upload IPA
# uses: actions/upload-artifact@v4
# with:
# name: ios-ipa
# path: build/ios/ipa/*.ipa
# retention-days: 30
# macOS DMG Build
build-macos:
name: Build macOS DMG
runs-on: macos-latest
needs: [update-version]
if: always() && (needs.update-version.result == 'success' || needs.update-version.result == 'skipped')
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download versioned pubspec
if: startsWith(github.ref, 'refs/tags/v')
uses: actions/download-artifact@v4
with:
name: versioned-pubspec
path: .
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: 'stable'
cache: true
- name: Enable macOS desktop
run: flutter config --enable-macos-desktop
- name: Install dependencies
run: flutter pub get
- name: Generate localizations
run: flutter gen-l10n
- name: Build macOS app
run: flutter build macos --release
- name: Create DMG
run: |
# Install create-dmg if not available
brew install create-dmg || true
# Create DMG from the built app
create-dmg \
--volname "MeshCore SAR" \
--volicon "macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png" \
--window-pos 200 120 \
--window-size 800 400 \
--icon-size 100 \
--icon "MeshCore SAR.app" 200 190 \
--hide-extension "MeshCore SAR.app" \
--app-drop-link 600 185 \
"MeshCore-SAR.dmg" \
"build/macos/Build/Products/Release/meshcore_sar_app.app" || \
# Fallback: simple DMG creation
hdiutil create -volname "MeshCore SAR" \
-srcfolder build/macos/Build/Products/Release/meshcore_sar_app.app \
-ov -format UDZO MeshCore-SAR.dmg
- name: Upload macOS DMG
uses: actions/upload-artifact@v4
with:
name: macos-dmg
path: MeshCore-SAR.dmg
retention-days: 30
- name: Upload macOS App
uses: actions/upload-artifact@v4
with:
name: macos-app
path: build/macos/Build/Products/Release/meshcore_sar_app.app
retention-days: 30
# Windows Executable Build
build-windows:
name: Build Windows Executable
runs-on: windows-latest
needs: [update-version]
if: always() && (needs.update-version.result == 'success' || needs.update-version.result == 'skipped')
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download versioned pubspec
if: startsWith(github.ref, 'refs/tags/v')
uses: actions/download-artifact@v4
with:
name: versioned-pubspec
path: .
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: 'stable'
cache: true
- name: Enable Windows desktop
run: flutter config --enable-windows-desktop
- name: Install dependencies
run: flutter pub get
- name: Generate localizations
run: flutter gen-l10n
- name: Build Windows app
run: flutter build windows --release
- name: Create installer (Optional - using Inno Setup)
shell: pwsh
run: |
# Download and install Inno Setup if needed
# This is optional - you can skip installer creation
Write-Host "Windows executable built successfully"
Write-Host "To create installer, configure Inno Setup script"
- name: Archive Windows Build
shell: pwsh
run: |
Compress-Archive -Path build/windows/x64/runner/Release/* -DestinationPath MeshCore-SAR-Windows.zip
- name: Upload Windows Executable
uses: actions/upload-artifact@v4
with:
name: windows-executable
path: MeshCore-SAR-Windows.zip
retention-days: 30
# Create Release on Tag
create-release:
name: Create Release
needs: [build-android, build-ios, build-macos, build-windows]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Create Release
uses: softprops/action-gh-release@v1
with:
files: |
artifacts/android-apk/*.apk
artifacts/android-appbundle/*.aab
artifacts/macos-dmg/*.dmg
artifacts/windows-executable/*.zip
draft: true
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

1338
CLAUDE.md

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

View File

@@ -489,7 +489,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 34; CURRENT_PROJECT_VERSION = 38;
DEVELOPMENT_TEAM = JND55328G8; DEVELOPMENT_TEAM = JND55328G8;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -511,7 +511,7 @@
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 34; CURRENT_PROJECT_VERSION = 38;
DEVELOPMENT_TEAM = JND55328G8; DEVELOPMENT_TEAM = JND55328G8;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
@@ -530,7 +530,7 @@
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 34; CURRENT_PROJECT_VERSION = 38;
DEVELOPMENT_TEAM = JND55328G8; DEVELOPMENT_TEAM = JND55328G8;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
@@ -547,7 +547,7 @@
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 34; CURRENT_PROJECT_VERSION = 38;
DEVELOPMENT_TEAM = JND55328G8; DEVELOPMENT_TEAM = JND55328G8;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
@@ -679,7 +679,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 34; CURRENT_PROJECT_VERSION = 38;
DEVELOPMENT_TEAM = JND55328G8; DEVELOPMENT_TEAM = JND55328G8;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -702,7 +702,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 34; CURRENT_PROJECT_VERSION = 38;
DEVELOPMENT_TEAM = JND55328G8; DEVELOPMENT_TEAM = JND55328G8;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;

View File

@@ -43,7 +43,7 @@
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>34</string> <string>38</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>NSBluetoothAlwaysUsageDescription</key> <key>NSBluetoothAlwaysUsageDescription</key>

View File

@@ -5,22 +5,22 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000217"> <testcase classname="fastlane.lanes" name="0: default_platform" time="0.00019">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.348684"> <testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.500434">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="2: build_app" time="128.917542"> <testcase classname="fastlane.lanes" name="2: build_app" time="116.977772">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="3: upload_to_testflight" time="303.805661"> <testcase classname="fastlane.lanes" name="3: upload_to_testflight" time="318.137705">
</testcase> </testcase>

View File

@@ -207,6 +207,16 @@
"description": "Beschreibung für die Einstellung der RX/TX-Indikatoren" "description": "Beschreibung für die Einstellung der RX/TX-Indikatoren"
}, },
"simpleMode": "Einfacher Modus",
"@simpleMode": {
"description": "Einstellung zum Aktivieren des einfachen Modus"
},
"simpleModeDescription": "Nicht wesentliche Informationen in Nachrichten und Kontakten ausblenden",
"@simpleModeDescription": {
"description": "Beschreibung für die Einstellung des einfachen Modus"
},
"language": "Sprache", "language": "Sprache",
"@language": { "@language": {
"description": "Beschriftung der Spracheinstellung" "description": "Beschriftung der Spracheinstellung"
@@ -514,7 +524,7 @@
"description": "Beschriftung der Aktualisieren-Schaltfläche" "description": "Beschriftung der Aktualisieren-Schaltfläche"
}, },
"sendDirectMessage": "Direktnachricht senden", "sendDirectMessage": "Senden",
"@sendDirectMessage": { "@sendDirectMessage": {
"description": "Aktion zum Senden einer Direktnachricht an Kontakt" "description": "Aktion zum Senden einer Direktnachricht an Kontakt"
}, },
@@ -634,6 +644,36 @@
"description": "Aktion zum Löschen aller lokalen Zeichnungen" "description": "Aktion zum Löschen aller lokalen Zeichnungen"
}, },
"showReceivedDrawings": "Empfangene Zeichnungen anzeigen",
"@showReceivedDrawings": {
"description": "Umschalter zum Ein-/Ausblenden empfangener Zeichnungen von anderen Teammitgliedern"
},
"showingAllDrawings": "Alle Zeichnungen werden angezeigt",
"@showingAllDrawings": {
"description": "Untertitel, wenn empfangene Zeichnungen sichtbar sind"
},
"showingOnlyYourDrawings": "Nur Ihre Zeichnungen werden angezeigt",
"@showingOnlyYourDrawings": {
"description": "Untertitel, wenn empfangene Zeichnungen ausgeblendet sind"
},
"showSarMarkers": "SAR-Markierungen anzeigen",
"@showSarMarkers": {
"description": "Umschalter zum Ein-/Ausblenden von SAR-Markierungen auf der Karte"
},
"showingSarMarkers": "SAR-Markierungen werden angezeigt",
"@showingSarMarkers": {
"description": "Untertitel, wenn SAR-Markierungen sichtbar sind"
},
"hidingSarMarkers": "SAR-Markierungen ausgeblendet",
"@hidingSarMarkers": {
"description": "Untertitel, wenn SAR-Markierungen ausgeblendet sind"
},
"clearAll": "Alle löschen", "clearAll": "Alle löschen",
"@clearAll": { "@clearAll": {
"description": "Beschriftung der Alle-löschen-Schaltfläche" "description": "Beschriftung der Alle-löschen-Schaltfläche"
@@ -762,6 +802,11 @@
"description": "Beschriftung für Standortbereich" "description": "Beschriftung für Standortbereich"
}, },
"myLocation": "Mein Standort",
"@myLocation": {
"description": "Schaltflächenbeschriftung zum Einfügen der aktuellen GPS-Position"
},
"fromMap": "Von Karte", "fromMap": "Von Karte",
"@fromMap": { "@fromMap": {
"description": "Badge, das anzeigt, dass der Standort vom Kartentippen stammt" "description": "Badge, das anzeigt, dass der Standort vom Kartentippen stammt"
@@ -1125,6 +1170,21 @@
"description": "Beschriftung der GPS-Genauigkeit" "description": "Beschriftung der GPS-Genauigkeit"
}, },
"distance": "Entfernung",
"@distance": {
"description": "Entfernungsbeschriftung im Kompass"
},
"bearing": "Peilung",
"@bearing": {
"description": "Peilungsbeschriftung im Kompass"
},
"direction": "Richtung",
"@direction": {
"description": "Richtungsbeschriftung im Kompass"
},
"filterMarkers": "Markierungen filtern", "filterMarkers": "Markierungen filtern",
"@filterMarkers": { "@filterMarkers": {
"description": "Titel für Markierungen-filtern-Dialog" "description": "Titel für Markierungen-filtern-Dialog"
@@ -1538,6 +1598,21 @@
"description": "Name der ESRI-Satellitenbildebene" "description": "Name der ESRI-Satellitenbildebene"
}, },
"googleHybrid": "Google Hybrid",
"@googleHybrid": {
"description": "Name der Google Hybrid-Ebene (Satellit + Beschriftungen)"
},
"googleRoadmap": "Google Straßenkarte",
"@googleRoadmap": {
"description": "Name der Google Straßenkarten-Ebene"
},
"googleTerrain": "Google Gelände",
"@googleTerrain": {
"description": "Name der Google Gelände-Ebene (topografisch)"
},
"downloadVisibleArea": "Sichtbaren Bereich herunterladen", "downloadVisibleArea": "Sichtbaren Bereich herunterladen",
"@downloadVisibleArea": { "@downloadVisibleArea": {
"description": "Tooltip für Schaltfläche zum Herunterladen des sichtbaren Bereichs" "description": "Tooltip für Schaltfläche zum Herunterladen des sichtbaren Bereichs"
@@ -1673,6 +1748,11 @@
"description": "Infonachricht, wenn Nachricht gelöscht wird" "description": "Infonachricht, wenn Nachricht gelöscht wird"
}, },
"copyText": "Text kopieren",
"textCopiedToClipboard": "Text in Zwischenablage kopiert",
"deleteMessage": "Nachricht löschen",
"deleteMessageConfirmation": "Möchten Sie diese Nachricht wirklich löschen?",
"refreshedContacts": "Kontakte aktualisiert", "refreshedContacts": "Kontakte aktualisiert",
"@refreshedContacts": { "@refreshedContacts": {
"description": "Erfolgsmeldung, wenn Kontakte aktualisiert werden" "description": "Erfolgsmeldung, wenn Kontakte aktualisiert werden"
@@ -1901,6 +1981,111 @@
"description": "Fehlermeldung, wenn MBTiles-Löschung fehlschlägt" "description": "Fehlermeldung, wenn MBTiles-Löschung fehlschlägt"
}, },
"importExportCachedTiles": "Import/Export gecachter Kacheln",
"@importExportCachedTiles": {
"description": "Titel für Import/Export-Bereich"
},
"importExportDescription": "Sichern, teilen und wiederherstellen Sie heruntergeladene Kartenkacheln zwischen Geräten",
"@importExportDescription": {
"description": "Beschreibung der Import/Export-Funktionalität"
},
"exportTilesToFile": "Kacheln in Datei exportieren",
"@exportTilesToFile": {
"description": "Schaltfläche zum Exportieren von Kacheln"
},
"importTilesFromFile": "Kacheln aus Datei importieren",
"@importTilesFromFile": {
"description": "Schaltfläche zum Importieren von Kacheln"
},
"selectExportLocation": "Exportspeicherort wählen",
"@selectExportLocation": {
"description": "Titel für Export-Dateiauswahl"
},
"selectImportFile": "Kachel-Archiv auswählen",
"@selectImportFile": {
"description": "Titel für Import-Dateiauswahl"
},
"exportingTiles": "Exportiere Kacheln...",
"@exportingTiles": {
"description": "Statusmeldung während des Exports"
},
"importingTiles": "Importiere Kacheln...",
"@importingTiles": {
"description": "Statusmeldung während des Imports"
},
"exportSuccess": "{count} Kacheln erfolgreich exportiert",
"@exportSuccess": {
"description": "Erfolgsmeldung nach Export",
"placeholders": {
"count": {
"type": "int"
}
}
},
"importSuccess": "{count} Speicher erfolgreich importiert",
"@importSuccess": {
"description": "Erfolgsmeldung nach Import",
"placeholders": {
"count": {
"type": "int"
}
}
},
"exportFailed": "Export fehlgeschlagen: {error}",
"@exportFailed": {
"description": "Fehlermeldung bei Export-Fehler",
"placeholders": {
"error": {
"type": "String"
}
}
},
"importFailed": "Import fehlgeschlagen: {error}",
"@importFailed": {
"description": "Fehlermeldung bei Import-Fehler",
"placeholders": {
"error": {
"type": "String"
}
}
},
"exportNote": "Erstellt eine komprimierte Archivdatei (.fmtc), die auf anderen Geräten geteilt und importiert werden kann.",
"@exportNote": {
"description": "Hinweis zur Export-Funktionalität"
},
"importNote": "Importiert Kartenkacheln aus einer zuvor exportierten Archivdatei. Kacheln werden mit dem vorhandenen Cache zusammengeführt.",
"@importNote": {
"description": "Hinweis zur Import-Funktionalität"
},
"noTilesToExport": "Keine Kacheln zum Exportieren verfügbar",
"@noTilesToExport": {
"description": "Meldung wenn Cache leer ist"
},
"archiveContainsStores": "Archiv enthält {count} Speicher",
"@archiveContainsStores": {
"description": "Information über Archivinhalte",
"placeholders": {
"count": {
"type": "int"
}
}
},
"vectorTiles": "Vektor-Tiles", "vectorTiles": "Vektor-Tiles",
"@vectorTiles": { "@vectorTiles": {
"description": "Beschriftung für Vektor-Tile-Typ" "description": "Beschriftung für Vektor-Tile-Typ"
@@ -2139,5 +2324,63 @@
"type": "String" "type": "String"
} }
} }
} },
"sarTemplates": "SAR Templates",
"manageSarTemplates": "Manage cursor on target templates",
"addTemplate": "Add Template",
"editTemplate": "Edit Template",
"deleteTemplate": "Delete Template",
"templateName": "Template Name",
"templateNameHint": "e.g. Found Person",
"templateEmoji": "Emoji",
"emojiRequired": "Emoji ist erforderlich",
"nameRequired": "Name ist erforderlich",
"templateDescription": "Description (Optional)",
"templateDescriptionHint": "Add additional context...",
"templateColor": "Color",
"previewFormat": "Preview (SAR Message Format)",
"importFromClipboard": "Import",
"exportToClipboard": "Export",
"deleteTemplateConfirmation": "Delete template '{name}'?",
"templateAdded": "Template added",
"templateUpdated": "Template updated",
"templateDeleted": "Template deleted",
"templatesImported": "{count, plural, =0{No templates imported} =1{Imported 1 template} other{Imported {count} templates}}",
"templatesExported": "{count, plural, =1{Exported 1 template to clipboard} other{Exported {count} templates to clipboard}}",
"importFailed": "Import failed: {error}",
"exportFailed": "Export failed: {error}",
"resetToDefaults": "Reset to Defaults",
"resetToDefaultsConfirmation": "This will delete all custom templates and restore the 4 default templates. Continue?",
"reset": "Reset",
"resetComplete": "Templates reset to defaults",
"noTemplates": "No templates available",
"tapAddToCreate": "Tap + to create your first template",
"ok": "OK",
"delete": "Löschen",
"permissionsSection": "Berechtigungen",
"locationPermission": "Standortberechtigung",
"checking": "Überprüfen...",
"locationPermissionGrantedAlways": "Erteilt (Immer)",
"locationPermissionGrantedWhileInUse": "Erteilt (Während der Nutzung)",
"locationPermissionDeniedTapToRequest": "Verweigert - Tippen zum Anfragen",
"locationPermissionPermanentlyDeniedOpenSettings": "Dauerhaft verweigert - Einstellungen öffnen",
"locationPermissionDialogContent": "Die Standortberechtigung wurde dauerhaft verweigert. Bitte aktivieren Sie sie in Ihren Geräteeinstellungen, um GPS-Tracking und Standortfreigabe zu nutzen.",
"openSettings": "Einstellungen öffnen",
"locationPermissionGranted": "Standortberechtigung erteilt!",
"locationPermissionRequiredForGps": "Die Standortberechtigung ist erforderlich für GPS-Tracking und Standortfreigabe.",
"locationPermissionAlreadyGranted": "Die Standortberechtigung wurde bereits erteilt.",
"sarNavyBlue": "SAR Navy Blau",
"sarNavyBlueDescription": "Professionell/Einsatzmodus",
"selectRecipient": "Empfänger auswählen",
"broadcastToAllNearby": "An alle in der Nähe senden",
"searchRecipients": "Empfänger suchen...",
"noContactsFound": "Keine Kontakte gefunden",
"noRoomsFound": "Keine Räume gefunden",
"noContactsOrRoomsAvailable": "Keine Kontakte oder Räume verfügbar",
"messagesWillBeSentToPublicChannel": "Nachrichten werden an öffentlichen Kanal gesendet",
"newMessage": "Neue Nachricht",
"channel": "Kanal"
} }

View File

@@ -207,6 +207,16 @@
"description": "Description for RX/TX indicators setting" "description": "Description for RX/TX indicators setting"
}, },
"simpleMode": "Simple Mode",
"@simpleMode": {
"description": "Setting to enable simple mode"
},
"simpleModeDescription": "Hide non-essential information in messages and contacts",
"@simpleModeDescription": {
"description": "Description for simple mode setting"
},
"language": "Language", "language": "Language",
"@language": { "@language": {
"description": "Language setting label" "description": "Language setting label"
@@ -514,7 +524,7 @@
"description": "Refresh button label" "description": "Refresh button label"
}, },
"sendDirectMessage": "Send Direct Message", "sendDirectMessage": "Send",
"@sendDirectMessage": { "@sendDirectMessage": {
"description": "Action to send direct message to contact" "description": "Action to send direct message to contact"
}, },
@@ -634,6 +644,36 @@
"description": "Action to clear all local drawings" "description": "Action to clear all local drawings"
}, },
"showReceivedDrawings": "Show Received Drawings",
"@showReceivedDrawings": {
"description": "Toggle to show/hide received drawings from other team members"
},
"showingAllDrawings": "Showing all drawings",
"@showingAllDrawings": {
"description": "Subtitle when received drawings are visible"
},
"showingOnlyYourDrawings": "Showing only your drawings",
"@showingOnlyYourDrawings": {
"description": "Subtitle when received drawings are hidden"
},
"showSarMarkers": "Show SAR Markers",
"@showSarMarkers": {
"description": "Toggle to show/hide SAR markers on map"
},
"showingSarMarkers": "Showing SAR markers",
"@showingSarMarkers": {
"description": "Subtitle when SAR markers are visible"
},
"hidingSarMarkers": "Hiding SAR markers",
"@hidingSarMarkers": {
"description": "Subtitle when SAR markers are hidden"
},
"clearAll": "Clear All", "clearAll": "Clear All",
"@clearAll": { "@clearAll": {
"description": "Clear all button label" "description": "Clear all button label"
@@ -762,6 +802,11 @@
"description": "Label for location section" "description": "Label for location section"
}, },
"myLocation": "My Location",
"@myLocation": {
"description": "Button label to insert current GPS location"
},
"fromMap": "From Map", "fromMap": "From Map",
"@fromMap": { "@fromMap": {
"description": "Badge showing location is from map tap" "description": "Badge showing location is from map tap"
@@ -1125,6 +1170,21 @@
"description": "GPS accuracy label" "description": "GPS accuracy label"
}, },
"distance": "Distance",
"@distance": {
"description": "Distance label in compass"
},
"bearing": "Bearing",
"@bearing": {
"description": "Bearing label in compass"
},
"direction": "Direction",
"@direction": {
"description": "Direction label in compass"
},
"filterMarkers": "Filter Markers", "filterMarkers": "Filter Markers",
"@filterMarkers": { "@filterMarkers": {
"description": "Title for filter markers dialog" "description": "Title for filter markers dialog"
@@ -1538,6 +1598,21 @@
"description": "ESRI Satellite imagery layer name" "description": "ESRI Satellite imagery layer name"
}, },
"googleHybrid": "Google Hybrid",
"@googleHybrid": {
"description": "Google Hybrid layer name (satellite + labels)"
},
"googleRoadmap": "Google Roadmap",
"@googleRoadmap": {
"description": "Google Roadmap layer name (street map)"
},
"googleTerrain": "Google Terrain",
"@googleTerrain": {
"description": "Google Terrain layer name (topographic)"
},
"downloadVisibleArea": "Download visible area", "downloadVisibleArea": "Download visible area",
"@downloadVisibleArea": { "@downloadVisibleArea": {
"description": "Tooltip for download visible area button" "description": "Tooltip for download visible area button"
@@ -1673,6 +1748,26 @@
"description": "Info message when message is deleted" "description": "Info message when message is deleted"
}, },
"copyText": "Copy text",
"@copyText": {
"description": "Option to copy message text to clipboard"
},
"textCopiedToClipboard": "Text copied to clipboard",
"@textCopiedToClipboard": {
"description": "Success message when text is copied"
},
"deleteMessage": "Delete message",
"@deleteMessage": {
"description": "Dialog title for deleting a message"
},
"deleteMessageConfirmation": "Are you sure you want to delete this message?",
"@deleteMessageConfirmation": {
"description": "Confirmation text for message deletion"
},
"refreshedContacts": "Refreshed contacts", "refreshedContacts": "Refreshed contacts",
"@refreshedContacts": { "@refreshedContacts": {
"description": "Success message when contacts are refreshed" "description": "Success message when contacts are refreshed"
@@ -1901,6 +1996,111 @@
"description": "Error message when MBTiles deletion fails" "description": "Error message when MBTiles deletion fails"
}, },
"importExportCachedTiles": "Import/Export Cached Tiles",
"@importExportCachedTiles": {
"description": "Title for import/export section"
},
"importExportDescription": "Backup, share, and restore downloaded map tiles between devices",
"@importExportDescription": {
"description": "Description for import/export functionality"
},
"exportTilesToFile": "Export Tiles to File",
"@exportTilesToFile": {
"description": "Button to export tiles to archive file"
},
"importTilesFromFile": "Import Tiles from File",
"@importTilesFromFile": {
"description": "Button to import tiles from archive file"
},
"selectExportLocation": "Select Export Location",
"@selectExportLocation": {
"description": "Title for export file picker"
},
"selectImportFile": "Select Tile Archive",
"@selectImportFile": {
"description": "Title for import file picker"
},
"exportingTiles": "Exporting tiles...",
"@exportingTiles": {
"description": "Status message during export"
},
"importingTiles": "Importing tiles...",
"@importingTiles": {
"description": "Status message during import"
},
"exportSuccess": "Exported {count} tiles successfully",
"@exportSuccess": {
"description": "Success message after export",
"placeholders": {
"count": {
"type": "int"
}
}
},
"importSuccess": "Imported {count} stores successfully",
"@importSuccess": {
"description": "Success message after import",
"placeholders": {
"count": {
"type": "int"
}
}
},
"exportFailed": "Export failed: {error}",
"@exportFailed": {
"description": "Error message when export fails",
"placeholders": {
"error": {
"type": "String"
}
}
},
"importFailed": "Import failed: {error}",
"@importFailed": {
"description": "Error message when import fails",
"placeholders": {
"error": {
"type": "String"
}
}
},
"exportNote": "Creates a compressed archive (.fmtc) file that can be shared and imported on other devices.",
"@exportNote": {
"description": "Note about export functionality"
},
"importNote": "Imports map tiles from a previously exported archive file. Tiles will be merged with existing cache.",
"@importNote": {
"description": "Note about import functionality"
},
"noTilesToExport": "No tiles available to export",
"@noTilesToExport": {
"description": "Message when cache is empty"
},
"archiveContainsStores": "Archive contains {count} stores",
"@archiveContainsStores": {
"description": "Information about archive contents",
"placeholders": {
"count": {
"type": "int"
}
}
},
"vectorTiles": "Vector Tiles", "vectorTiles": "Vector Tiles",
"@vectorTiles": { "@vectorTiles": {
"description": "Label for vector tile type" "description": "Label for vector tile type"
@@ -2139,5 +2339,305 @@
"type": "String" "type": "String"
} }
} }
},
"sarTemplates": "SAR Templates",
"@sarTemplates": {
"description": "SAR templates menu title"
},
"manageSarTemplates": "Manage cursor on target templates",
"@manageSarTemplates": {
"description": "Subtitle for SAR templates settings"
},
"addTemplate": "Add Template",
"@addTemplate": {
"description": "Button to add new SAR template"
},
"editTemplate": "Edit Template",
"@editTemplate": {
"description": "Dialog title for editing template"
},
"deleteTemplate": "Delete Template",
"@deleteTemplate": {
"description": "Action to delete template"
},
"templateName": "Template Name",
"@templateName": {
"description": "Label for template name field"
},
"templateNameHint": "e.g. Found Person",
"@templateNameHint": {
"description": "Hint text for template name"
},
"templateEmoji": "Emoji",
"@templateEmoji": {
"description": "Label for template emoji field"
},
"emojiRequired": "Emoji is required",
"@emojiRequired": {
"description": "Validation error when emoji field is empty"
},
"nameRequired": "Name is required",
"@nameRequired": {
"description": "Validation error when name field is empty"
},
"templateDescription": "Description (Optional)",
"@templateDescription": {
"description": "Label for template description field"
},
"templateDescriptionHint": "Add additional context...",
"@templateDescriptionHint": {
"description": "Hint text for template description"
},
"templateColor": "Color",
"@templateColor": {
"description": "Label for template color picker"
},
"previewFormat": "Preview (SAR Message Format)",
"@previewFormat": {
"description": "Label for format preview"
},
"importFromClipboard": "Import",
"@importFromClipboard": {
"description": "Button to import templates from clipboard"
},
"exportToClipboard": "Export",
"@exportToClipboard": {
"description": "Button to export templates to clipboard"
},
"deleteTemplateConfirmation": "Delete template '{name}'?",
"@deleteTemplateConfirmation": {
"description": "Confirmation message for template deletion",
"placeholders": {
"name": {
"type": "String"
}
}
},
"templateAdded": "Template added",
"@templateAdded": {
"description": "Success message when template is added"
},
"templateUpdated": "Template updated",
"@templateUpdated": {
"description": "Success message when template is updated"
},
"templateDeleted": "Template deleted",
"@templateDeleted": {
"description": "Success message when template is deleted"
},
"templatesImported": "{count, plural, =0{No templates imported} =1{Imported 1 template} other{Imported {count} templates}}",
"@templatesImported": {
"description": "Success message after importing templates",
"placeholders": {
"count": {
"type": "int"
}
}
},
"templatesExported": "{count, plural, =1{Exported 1 template to clipboard} other{Exported {count} templates to clipboard}}",
"@templatesExported": {
"description": "Success message after exporting templates",
"placeholders": {
"count": {
"type": "int"
}
}
},
"importFailed": "Import failed: {error}",
"@importFailed": {
"description": "Error message when import fails",
"placeholders": {
"error": {
"type": "String"
}
}
},
"exportFailed": "Export failed: {error}",
"@exportFailed": {
"description": "Error message when export fails",
"placeholders": {
"error": {
"type": "String"
}
}
},
"resetToDefaults": "Reset to Defaults",
"@resetToDefaults": {
"description": "Action to reset templates to defaults"
},
"resetToDefaultsConfirmation": "This will delete all custom templates and restore the 4 default templates. Continue?",
"@resetToDefaultsConfirmation": {
"description": "Confirmation message for reset to defaults"
},
"reset": "Reset",
"@reset": {
"description": "Reset button label"
},
"resetComplete": "Templates reset to defaults",
"@resetComplete": {
"description": "Success message after reset"
},
"noTemplates": "No templates available",
"@noTemplates": {
"description": "Message when no templates exist"
},
"tapAddToCreate": "Tap + to create your first template",
"@tapAddToCreate": {
"description": "Helper text when no templates exist"
},
"ok": "OK",
"@ok": {
"description": "OK button label"
},
"delete": "Delete",
"@delete": {
"description": "Delete button label"
},
"permissionsSection": "Permissions",
"@permissionsSection": {
"description": "Permissions section header"
},
"locationPermission": "Location Permission",
"@locationPermission": {
"description": "Location permission label"
},
"checking": "Checking...",
"@checking": {
"description": "Loading state indicator"
},
"locationPermissionGrantedAlways": "Granted (Always)",
"@locationPermissionGrantedAlways": {
"description": "Location permission status: granted always"
},
"locationPermissionGrantedWhileInUse": "Granted (While In Use)",
"@locationPermissionGrantedWhileInUse": {
"description": "Location permission status: granted while in use"
},
"locationPermissionDeniedTapToRequest": "Denied - Tap to request",
"@locationPermissionDeniedTapToRequest": {
"description": "Location permission status: denied, user can request"
},
"locationPermissionPermanentlyDeniedOpenSettings": "Permanently Denied - Open Settings",
"@locationPermissionPermanentlyDeniedOpenSettings": {
"description": "Location permission status: permanently denied"
},
"locationPermissionDialogContent": "Location permission is permanently denied. Please enable it in your device settings to use GPS tracking and location sharing features.",
"@locationPermissionDialogContent": {
"description": "Content for location permission dialog when permanently denied"
},
"openSettings": "Open Settings",
"@openSettings": {
"description": "Button to open device settings"
},
"locationPermissionGranted": "Location permission granted!",
"@locationPermissionGranted": {
"description": "Success message when location permission is granted"
},
"locationPermissionRequiredForGps": "Location permission is required for GPS tracking and location sharing.",
"@locationPermissionRequiredForGps": {
"description": "Info message about location permission requirement"
},
"locationPermissionAlreadyGranted": "Location permission is already granted.",
"@locationPermissionAlreadyGranted": {
"description": "Info message when permission is already granted"
},
"sarNavyBlue": "SAR Navy Blue",
"@sarNavyBlue": {
"description": "SAR Navy Blue theme name"
},
"sarNavyBlueDescription": "Professional/Operations Mode",
"@sarNavyBlueDescription": {
"description": "Description for SAR Navy Blue theme"
},
"selectRecipient": "Select Recipient",
"@selectRecipient": {
"description": "Title for recipient selector sheet"
},
"broadcastToAllNearby": "Broadcast to all nearby",
"@broadcastToAllNearby": {
"description": "Subtitle for public channel option"
},
"searchRecipients": "Search recipients...",
"@searchRecipients": {
"description": "Placeholder text for recipient search field"
},
"noContactsFound": "No contacts found",
"@noContactsFound": {
"description": "Message when no contacts match search"
},
"noRoomsFound": "No rooms found",
"@noRoomsFound": {
"description": "Message when no rooms match search"
},
"noContactsOrRoomsAvailable": "No contacts or rooms available",
"@noContactsOrRoomsAvailable": {
"description": "Message when no contacts or rooms exist"
},
"messagesWillBeSentToPublicChannel": "Messages will be sent to public channel",
"@messagesWillBeSentToPublicChannel": {
"description": "Info message when only public channel is available"
},
"newMessage": "New message",
"@newMessage": {
"description": "Notification title for new message"
},
"channel": "Channel",
"@channel": {
"description": "Channel label in notifications"
} }
} }

View File

@@ -207,6 +207,16 @@
"description": "Descripción de la configuración de indicadores RX/TX" "description": "Descripción de la configuración de indicadores RX/TX"
}, },
"simpleMode": "Modo Simple",
"@simpleMode": {
"description": "Configuración para habilitar el modo simple"
},
"simpleModeDescription": "Ocultar información no esencial en mensajes y contactos",
"@simpleModeDescription": {
"description": "Descripción de la configuración del modo simple"
},
"language": "Idioma", "language": "Idioma",
"@language": { "@language": {
"description": "Etiqueta de la configuración de idioma" "description": "Etiqueta de la configuración de idioma"
@@ -514,7 +524,7 @@
"description": "Etiqueta del botón de actualizar" "description": "Etiqueta del botón de actualizar"
}, },
"sendDirectMessage": "Enviar mensaje directo", "sendDirectMessage": "Enviar",
"@sendDirectMessage": { "@sendDirectMessage": {
"description": "Acción para enviar mensaje directo al contacto" "description": "Acción para enviar mensaje directo al contacto"
}, },
@@ -634,6 +644,36 @@
"description": "Acción para borrar todos los dibujos locales" "description": "Acción para borrar todos los dibujos locales"
}, },
"showReceivedDrawings": "Mostrar dibujos recibidos",
"@showReceivedDrawings": {
"description": "Alternar para mostrar/ocultar dibujos recibidos de otros miembros del equipo"
},
"showingAllDrawings": "Mostrando todos los dibujos",
"@showingAllDrawings": {
"description": "Subtítulo cuando los dibujos recibidos son visibles"
},
"showingOnlyYourDrawings": "Mostrando solo tus dibujos",
"@showingOnlyYourDrawings": {
"description": "Subtítulo cuando los dibujos recibidos están ocultos"
},
"showSarMarkers": "Mostrar marcadores SAR",
"@showSarMarkers": {
"description": "Alternar para mostrar/ocultar marcadores SAR en el mapa"
},
"showingSarMarkers": "Mostrando marcadores SAR",
"@showingSarMarkers": {
"description": "Subtítulo cuando los marcadores SAR son visibles"
},
"hidingSarMarkers": "Ocultando marcadores SAR",
"@hidingSarMarkers": {
"description": "Subtítulo cuando los marcadores SAR están ocultos"
},
"clearAll": "Borrar todo", "clearAll": "Borrar todo",
"@clearAll": { "@clearAll": {
"description": "Etiqueta del botón de borrar todo" "description": "Etiqueta del botón de borrar todo"
@@ -762,6 +802,11 @@
"description": "Etiqueta para la sección de ubicación" "description": "Etiqueta para la sección de ubicación"
}, },
"myLocation": "Mi ubicación",
"@myLocation": {
"description": "Etiqueta del botón para insertar la ubicación GPS actual"
},
"fromMap": "Desde el mapa", "fromMap": "Desde el mapa",
"@fromMap": { "@fromMap": {
"description": "Insignia que muestra que la ubicación es desde un toque en el mapa" "description": "Insignia que muestra que la ubicación es desde un toque en el mapa"
@@ -1125,6 +1170,21 @@
"description": "Etiqueta de precisión GPS" "description": "Etiqueta de precisión GPS"
}, },
"distance": "Distancia",
"@distance": {
"description": "Etiqueta de distancia en la brújula"
},
"bearing": "Rumbo",
"@bearing": {
"description": "Etiqueta de rumbo en la brújula"
},
"direction": "Dirección",
"@direction": {
"description": "Etiqueta de dirección en la brújula"
},
"filterMarkers": "Filtrar marcadores", "filterMarkers": "Filtrar marcadores",
"@filterMarkers": { "@filterMarkers": {
"description": "Título del diálogo de filtrar marcadores" "description": "Título del diálogo de filtrar marcadores"
@@ -1538,6 +1598,21 @@
"description": "Nombre de capa de imágenes de satélite ESRI" "description": "Nombre de capa de imágenes de satélite ESRI"
}, },
"googleHybrid": "Google Híbrido",
"@googleHybrid": {
"description": "Nombre de capa Google Híbrido (satélite + etiquetas)"
},
"googleRoadmap": "Google Mapa de Carreteras",
"@googleRoadmap": {
"description": "Nombre de capa Google Mapa de Carreteras"
},
"googleTerrain": "Google Terreno",
"@googleTerrain": {
"description": "Nombre de capa Google Terreno (topográfico)"
},
"downloadVisibleArea": "Descargar área visible", "downloadVisibleArea": "Descargar área visible",
"@downloadVisibleArea": { "@downloadVisibleArea": {
"description": "Tooltip para el botón de descargar área visible" "description": "Tooltip para el botón de descargar área visible"
@@ -1673,6 +1748,11 @@
"description": "Mensaje de información cuando se elimina mensaje" "description": "Mensaje de información cuando se elimina mensaje"
}, },
"copyText": "Copiar texto",
"textCopiedToClipboard": "Texto copiado al portapapeles",
"deleteMessage": "Eliminar mensaje",
"deleteMessageConfirmation": "¿Está seguro de que desea eliminar este mensaje?",
"refreshedContacts": "Contactos actualizados", "refreshedContacts": "Contactos actualizados",
"@refreshedContacts": { "@refreshedContacts": {
"description": "Mensaje de éxito cuando se actualizan contactos" "description": "Mensaje de éxito cuando se actualizan contactos"
@@ -1901,6 +1981,111 @@
"description": "Mensaje de error cuando falla la eliminación de MBTiles" "description": "Mensaje de error cuando falla la eliminación de MBTiles"
}, },
"importExportCachedTiles": "Importar/Exportar teselas en caché",
"@importExportCachedTiles": {
"description": "Título para sección de importar/exportar"
},
"importExportDescription": "Realice copias de seguridad, comparta y restaure teselas de mapas descargadas entre dispositivos",
"@importExportDescription": {
"description": "Descripción de funcionalidad de importar/exportar"
},
"exportTilesToFile": "Exportar teselas a archivo",
"@exportTilesToFile": {
"description": "Botón para exportar teselas"
},
"importTilesFromFile": "Importar teselas desde archivo",
"@importTilesFromFile": {
"description": "Botón para importar teselas"
},
"selectExportLocation": "Seleccionar ubicación de exportación",
"@selectExportLocation": {
"description": "Título para selector de archivo de exportación"
},
"selectImportFile": "Seleccionar archivo de teselas",
"@selectImportFile": {
"description": "Título para selector de archivo de importación"
},
"exportingTiles": "Exportando teselas...",
"@exportingTiles": {
"description": "Mensaje de estado durante exportación"
},
"importingTiles": "Importando teselas...",
"@importingTiles": {
"description": "Mensaje de estado durante importación"
},
"exportSuccess": "{count} teselas exportadas exitosamente",
"@exportSuccess": {
"description": "Mensaje de éxito después de exportar",
"placeholders": {
"count": {
"type": "int"
}
}
},
"importSuccess": "{count} almacenes importados exitosamente",
"@importSuccess": {
"description": "Mensaje de éxito después de importar",
"placeholders": {
"count": {
"type": "int"
}
}
},
"exportFailed": "Error en exportación: {error}",
"@exportFailed": {
"description": "Mensaje de error cuando falla exportación",
"placeholders": {
"error": {
"type": "String"
}
}
},
"importFailed": "Error en importación: {error}",
"@importFailed": {
"description": "Mensaje de error cuando falla importación",
"placeholders": {
"error": {
"type": "String"
}
}
},
"exportNote": "Crea un archivo comprimido (.fmtc) que se puede compartir e importar en otros dispositivos.",
"@exportNote": {
"description": "Nota sobre funcionalidad de exportación"
},
"importNote": "Importa teselas de mapa desde un archivo previamente exportado. Las teselas se fusionarán con la caché existente.",
"@importNote": {
"description": "Nota sobre funcionalidad de importación"
},
"noTilesToExport": "No hay teselas para exportar",
"@noTilesToExport": {
"description": "Mensaje cuando caché está vacío"
},
"archiveContainsStores": "El archivo contiene {count} almacenes",
"@archiveContainsStores": {
"description": "Información sobre contenido del archivo",
"placeholders": {
"count": {
"type": "int"
}
}
},
"vectorTiles": "Teselas vectoriales", "vectorTiles": "Teselas vectoriales",
"@vectorTiles": { "@vectorTiles": {
"description": "Etiqueta del tipo de tesela vectorial" "description": "Etiqueta del tipo de tesela vectorial"
@@ -2134,5 +2319,63 @@
"type": "String" "type": "String"
} }
} }
} },
"sarTemplates": "SAR Templates",
"manageSarTemplates": "Manage cursor on target templates",
"addTemplate": "Add Template",
"editTemplate": "Edit Template",
"deleteTemplate": "Delete Template",
"templateName": "Template Name",
"templateNameHint": "e.g. Found Person",
"templateEmoji": "Emoji",
"emojiRequired": "Se requiere emoji",
"nameRequired": "Se requiere nombre",
"templateDescription": "Description (Optional)",
"templateDescriptionHint": "Add additional context...",
"templateColor": "Color",
"previewFormat": "Preview (SAR Message Format)",
"importFromClipboard": "Import",
"exportToClipboard": "Export",
"deleteTemplateConfirmation": "Delete template '{name}'?",
"templateAdded": "Template added",
"templateUpdated": "Template updated",
"templateDeleted": "Template deleted",
"templatesImported": "{count, plural, =0{No templates imported} =1{Imported 1 template} other{Imported {count} templates}}",
"templatesExported": "{count, plural, =1{Exported 1 template to clipboard} other{Exported {count} templates to clipboard}}",
"importFailed": "Import failed: {error}",
"exportFailed": "Export failed: {error}",
"resetToDefaults": "Reset to Defaults",
"resetToDefaultsConfirmation": "This will delete all custom templates and restore the 4 default templates. Continue?",
"reset": "Reset",
"resetComplete": "Templates reset to defaults",
"noTemplates": "No templates available",
"tapAddToCreate": "Tap + to create your first template",
"ok": "OK",
"delete": "Eliminar",
"permissionsSection": "Permisos",
"locationPermission": "Permiso de ubicación",
"checking": "Comprobando...",
"locationPermissionGrantedAlways": "Concedido (Siempre)",
"locationPermissionGrantedWhileInUse": "Concedido (Durante el uso)",
"locationPermissionDeniedTapToRequest": "Denegado - Toca para solicitar",
"locationPermissionPermanentlyDeniedOpenSettings": "Denegado permanentemente - Abrir ajustes",
"locationPermissionDialogContent": "El permiso de ubicación está permanentemente denegado. Por favor, actívalo en la configuración de tu dispositivo para usar el rastreo GPS y compartir ubicación.",
"openSettings": "Abrir ajustes",
"locationPermissionGranted": "¡Permiso de ubicación concedido!",
"locationPermissionRequiredForGps": "El permiso de ubicación es necesario para el rastreo GPS y compartir ubicación.",
"locationPermissionAlreadyGranted": "El permiso de ubicación ya está concedido.",
"sarNavyBlue": "SAR Azul Marino",
"sarNavyBlueDescription": "Modo Profesional/Operaciones",
"selectRecipient": "Seleccionar destinatario",
"broadcastToAllNearby": "Transmitir a todos cercanos",
"searchRecipients": "Buscar destinatarios...",
"noContactsFound": "No se encontraron contactos",
"noRoomsFound": "No se encontraron salas",
"noContactsOrRoomsAvailable": "No hay contactos o salas disponibles",
"messagesWillBeSentToPublicChannel": "Los mensajes se enviarán al canal público",
"newMessage": "Nuevo mensaje",
"channel": "Canal"
} }

View File

@@ -207,6 +207,16 @@
"description": "Description du paramètre des indicateurs RX/TX" "description": "Description du paramètre des indicateurs RX/TX"
}, },
"simpleMode": "Mode Simple",
"@simpleMode": {
"description": "Paramètre pour activer le mode simple"
},
"simpleModeDescription": "Masquer les informations non essentielles dans les messages et les contacts",
"@simpleModeDescription": {
"description": "Description du paramètre du mode simple"
},
"language": "Langue", "language": "Langue",
"@language": { "@language": {
"description": "Libellé du paramètre de langue" "description": "Libellé du paramètre de langue"
@@ -514,7 +524,7 @@
"description": "Libellé du bouton Actualiser" "description": "Libellé du bouton Actualiser"
}, },
"sendDirectMessage": "Envoyer un message direct", "sendDirectMessage": "Envoyer",
"@sendDirectMessage": { "@sendDirectMessage": {
"description": "Action pour envoyer un message direct au contact" "description": "Action pour envoyer un message direct au contact"
}, },
@@ -634,6 +644,36 @@
"description": "Action pour effacer tous les dessins locaux" "description": "Action pour effacer tous les dessins locaux"
}, },
"showReceivedDrawings": "Afficher les dessins reçus",
"@showReceivedDrawings": {
"description": "Basculer pour afficher/masquer les dessins reçus des autres membres de l'équipe"
},
"showingAllDrawings": "Affichage de tous les dessins",
"@showingAllDrawings": {
"description": "Sous-titre lorsque les dessins reçus sont visibles"
},
"showingOnlyYourDrawings": "Affichage uniquement de vos dessins",
"@showingOnlyYourDrawings": {
"description": "Sous-titre lorsque les dessins reçus sont masqués"
},
"showSarMarkers": "Afficher les marqueurs SAR",
"@showSarMarkers": {
"description": "Basculer pour afficher/masquer les marqueurs SAR sur la carte"
},
"showingSarMarkers": "Affichage des marqueurs SAR",
"@showingSarMarkers": {
"description": "Sous-titre lorsque les marqueurs SAR sont visibles"
},
"hidingSarMarkers": "Masquage des marqueurs SAR",
"@hidingSarMarkers": {
"description": "Sous-titre lorsque les marqueurs SAR sont masqués"
},
"clearAll": "Tout effacer", "clearAll": "Tout effacer",
"@clearAll": { "@clearAll": {
"description": "Libellé du bouton Tout effacer" "description": "Libellé du bouton Tout effacer"
@@ -762,6 +802,11 @@
"description": "Libellé de la section de position" "description": "Libellé de la section de position"
}, },
"myLocation": "Ma position",
"@myLocation": {
"description": "Libellé du bouton pour insérer la position GPS actuelle"
},
"fromMap": "Depuis la carte", "fromMap": "Depuis la carte",
"@fromMap": { "@fromMap": {
"description": "Badge indiquant que la position provient d'un clic sur la carte" "description": "Badge indiquant que la position provient d'un clic sur la carte"
@@ -1125,6 +1170,21 @@
"description": "Libellé de la précision GPS" "description": "Libellé de la précision GPS"
}, },
"distance": "Distance",
"@distance": {
"description": "Libellé de la distance dans la boussole"
},
"bearing": "Relèvement",
"@bearing": {
"description": "Libellé du relèvement dans la boussole"
},
"direction": "Direction",
"@direction": {
"description": "Libellé de la direction dans la boussole"
},
"filterMarkers": "Filtrer les marqueurs", "filterMarkers": "Filtrer les marqueurs",
"@filterMarkers": { "@filterMarkers": {
"description": "Titre de la boîte de dialogue de filtrage des marqueurs" "description": "Titre de la boîte de dialogue de filtrage des marqueurs"
@@ -1538,6 +1598,21 @@
"description": "Nom de la couche d'imagerie satellite ESRI" "description": "Nom de la couche d'imagerie satellite ESRI"
}, },
"googleHybrid": "Google Hybride",
"@googleHybrid": {
"description": "Nom de la couche Google Hybride (satellite + étiquettes)"
},
"googleRoadmap": "Google Carte Routière",
"@googleRoadmap": {
"description": "Nom de la couche Google Carte Routière"
},
"googleTerrain": "Google Terrain",
"@googleTerrain": {
"description": "Nom de la couche Google Terrain (topographique)"
},
"downloadVisibleArea": "Télécharger la zone visible", "downloadVisibleArea": "Télécharger la zone visible",
"@downloadVisibleArea": { "@downloadVisibleArea": {
"description": "Info-bulle du bouton de téléchargement de la zone visible" "description": "Info-bulle du bouton de téléchargement de la zone visible"
@@ -1673,6 +1748,11 @@
"description": "Message d'information lorsque le message est supprimé" "description": "Message d'information lorsque le message est supprimé"
}, },
"copyText": "Copier le texte",
"textCopiedToClipboard": "Texte copié dans le presse-papiers",
"deleteMessage": "Supprimer le message",
"deleteMessageConfirmation": "Êtes-vous sûr de vouloir supprimer ce message?",
"refreshedContacts": "Contacts actualisés", "refreshedContacts": "Contacts actualisés",
"@refreshedContacts": { "@refreshedContacts": {
"description": "Message de succès lorsque les contacts sont actualisés" "description": "Message de succès lorsque les contacts sont actualisés"
@@ -1901,6 +1981,111 @@
"description": "Message d'erreur lorsque la suppression de MBTiles échoue" "description": "Message d'erreur lorsque la suppression de MBTiles échoue"
}, },
"importExportCachedTiles": "Importer/Exporter les tuiles en cache",
"@importExportCachedTiles": {
"description": "Titre pour section d'importation/exportation"
},
"importExportDescription": "Sauvegarder, partager et restaurer les tuiles de carte téléchargées entre appareils",
"@importExportDescription": {
"description": "Description de la fonctionnalité d'importation/exportation"
},
"exportTilesToFile": "Exporter les tuiles vers fichier",
"@exportTilesToFile": {
"description": "Bouton pour exporter les tuiles"
},
"importTilesFromFile": "Importer les tuiles depuis fichier",
"@importTilesFromFile": {
"description": "Bouton pour importer les tuiles"
},
"selectExportLocation": "Sélectionner l'emplacement d'exportation",
"@selectExportLocation": {
"description": "Titre pour sélecteur de fichier d'exportation"
},
"selectImportFile": "Sélectionner l'archive de tuiles",
"@selectImportFile": {
"description": "Titre pour sélecteur de fichier d'importation"
},
"exportingTiles": "Exportation des tuiles...",
"@exportingTiles": {
"description": "Message de statut pendant l'exportation"
},
"importingTiles": "Importation des tuiles...",
"@importingTiles": {
"description": "Message de statut pendant l'importation"
},
"exportSuccess": "{count} tuiles exportées avec succès",
"@exportSuccess": {
"description": "Message de succès après exportation",
"placeholders": {
"count": {
"type": "int"
}
}
},
"importSuccess": "{count} magasins importés avec succès",
"@importSuccess": {
"description": "Message de succès après importation",
"placeholders": {
"count": {
"type": "int"
}
}
},
"exportFailed": "Échec de l'exportation: {error}",
"@exportFailed": {
"description": "Message d'erreur lors de l'échec de l'exportation",
"placeholders": {
"error": {
"type": "String"
}
}
},
"importFailed": "Échec de l'importation: {error}",
"@importFailed": {
"description": "Message d'erreur lors de l'échec de l'importation",
"placeholders": {
"error": {
"type": "String"
}
}
},
"exportNote": "Crée un fichier d'archive compressé (.fmtc) qui peut être partagé et importé sur d'autres appareils.",
"@exportNote": {
"description": "Note sur la fonctionnalité d'exportation"
},
"importNote": "Importe les tuiles de carte depuis un fichier d'archive précédemment exporté. Les tuiles seront fusionnées avec le cache existant.",
"@importNote": {
"description": "Note sur la fonctionnalité d'importation"
},
"noTilesToExport": "Aucune tuile à exporter",
"@noTilesToExport": {
"description": "Message quand le cache est vide"
},
"archiveContainsStores": "L'archive contient {count} magasins",
"@archiveContainsStores": {
"description": "Information sur le contenu de l'archive",
"placeholders": {
"count": {
"type": "int"
}
}
},
"vectorTiles": "Tuiles vectorielles", "vectorTiles": "Tuiles vectorielles",
"@vectorTiles": { "@vectorTiles": {
"description": "Libellé du type de tuile vectorielle" "description": "Libellé du type de tuile vectorielle"
@@ -2139,5 +2324,63 @@
"type": "String" "type": "String"
} }
} }
} },
"sarTemplates": "SAR Templates",
"manageSarTemplates": "Manage cursor on target templates",
"addTemplate": "Add Template",
"editTemplate": "Edit Template",
"deleteTemplate": "Delete Template",
"templateName": "Template Name",
"templateNameHint": "e.g. Found Person",
"templateEmoji": "Emoji",
"emojiRequired": "Emoji est requis",
"nameRequired": "Nom est requis",
"templateDescription": "Description (Optional)",
"templateDescriptionHint": "Add additional context...",
"templateColor": "Color",
"previewFormat": "Preview (SAR Message Format)",
"importFromClipboard": "Import",
"exportToClipboard": "Export",
"deleteTemplateConfirmation": "Delete template '{name}'?",
"templateAdded": "Template added",
"templateUpdated": "Template updated",
"templateDeleted": "Template deleted",
"templatesImported": "{count, plural, =0{No templates imported} =1{Imported 1 template} other{Imported {count} templates}}",
"templatesExported": "{count, plural, =1{Exported 1 template to clipboard} other{Exported {count} templates to clipboard}}",
"importFailed": "Import failed: {error}",
"exportFailed": "Export failed: {error}",
"resetToDefaults": "Reset to Defaults",
"resetToDefaultsConfirmation": "This will delete all custom templates and restore the 4 default templates. Continue?",
"reset": "Reset",
"resetComplete": "Templates reset to defaults",
"noTemplates": "No templates available",
"tapAddToCreate": "Tap + to create your first template",
"ok": "OK",
"delete": "Supprimer",
"permissionsSection": "Autorisations",
"locationPermission": "Autorisation de localisation",
"checking": "Vérification...",
"locationPermissionGrantedAlways": "Accordée (Toujours)",
"locationPermissionGrantedWhileInUse": "Accordée (En cours d'utilisation)",
"locationPermissionDeniedTapToRequest": "Refusée - Appuyez pour demander",
"locationPermissionPermanentlyDeniedOpenSettings": "Refusée définitivement - Ouvrir les paramètres",
"locationPermissionDialogContent": "L'autorisation de localisation est définitivement refusée. Veuillez l'activer dans les paramètres de votre appareil pour utiliser le suivi GPS et le partage de localisation.",
"openSettings": "Ouvrir les paramètres",
"locationPermissionGranted": "Autorisation de localisation accordée !",
"locationPermissionRequiredForGps": "L'autorisation de localisation est nécessaire pour le suivi GPS et le partage de localisation.",
"locationPermissionAlreadyGranted": "L'autorisation de localisation est déjà accordée.",
"sarNavyBlue": "SAR Bleu Marine",
"sarNavyBlueDescription": "Mode Professionnel/Opérations",
"selectRecipient": "Sélectionner le destinataire",
"broadcastToAllNearby": "Diffuser à tous à proximité",
"searchRecipients": "Rechercher des destinataires...",
"noContactsFound": "Aucun contact trouvé",
"noRoomsFound": "Aucune salle trouvée",
"noContactsOrRoomsAvailable": "Aucun contact ou salle disponible",
"messagesWillBeSentToPublicChannel": "Les messages seront envoyés au canal public",
"newMessage": "Nouveau message",
"channel": "Canal"
} }

View File

@@ -75,6 +75,10 @@
"displayPacketActivity": "Prikaži indikatore aktivnosti paketa u gornjoj traci", "displayPacketActivity": "Prikaži indikatore aktivnosti paketa u gornjoj traci",
"simpleMode": "Jednostavni način",
"simpleModeDescription": "Sakrij nevažne informacije u porukama i kontaktima",
"language": "Jezik", "language": "Jezik",
"chooseLanguage": "Odaberite jezik", "chooseLanguage": "Odaberite jezik",
@@ -181,7 +185,7 @@
"refresh": "Osvježi", "refresh": "Osvježi",
"sendDirectMessage": "Pošalji izravnu poruku", "sendDirectMessage": "Pošalji",
"resetPath": "Resetiraj put (preusmjeri)", "resetPath": "Resetiraj put (preusmjeri)",
@@ -221,6 +225,18 @@
"clearAllDrawings": "Očisti sve crteže", "clearAllDrawings": "Očisti sve crteže",
"showReceivedDrawings": "Prikaži primljene crteže",
"showingAllDrawings": "Prikazujem sve crteže",
"showingOnlyYourDrawings": "Prikazujem samo vaše crteže",
"showSarMarkers": "Prikaži SAR oznake",
"showingSarMarkers": "Prikazujem SAR oznake",
"hidingSarMarkers": "Skrivam SAR oznake",
"clearAll": "Očisti sve", "clearAll": "Očisti sve",
"noLocalDrawings": "Nema lokalnih crteža za dijeljenje", "noLocalDrawings": "Nema lokalnih crteža za dijeljenje",
@@ -263,6 +279,8 @@
"location": "Lokacija", "location": "Lokacija",
"myLocation": "Moja lokacija",
"fromMap": "S karte", "fromMap": "S karte",
"gettingLocation": "Dohvaćanje lokacije...", "gettingLocation": "Dohvaćanje lokacije...",
@@ -379,6 +397,12 @@
"accuracy": "Točnost", "accuracy": "Točnost",
"distance": "Udaljenost",
"bearing": "Azimut",
"direction": "Smjer",
"filterMarkers": "Filtriraj markere", "filterMarkers": "Filtriraj markere",
"filterMarkersTooltip": "Filtriraj markere", "filterMarkersTooltip": "Filtriraj markere",
@@ -521,6 +545,12 @@
"esriSatellite": "ESRI satelit", "esriSatellite": "ESRI satelit",
"googleHybrid": "Google hibridno",
"googleRoadmap": "Google cestovna karta",
"googleTerrain": "Google teren",
"downloadVisibleArea": "Preuzmi vidljivo područje", "downloadVisibleArea": "Preuzmi vidljivo područje",
"initializingMap": "Inicijalizacija karte...", "initializingMap": "Inicijalizacija karte...",
@@ -586,6 +616,10 @@
"cannotReplyContactNotFound": "Ne mogu odgovoriti: kontakt nije pronađen", "cannotReplyContactNotFound": "Ne mogu odgovoriti: kontakt nije pronađen",
"messageDeleted": "Poruka izbrisana", "messageDeleted": "Poruka izbrisana",
"copyText": "Kopiraj tekst",
"textCopiedToClipboard": "Tekst kopiran u međuspremnik",
"deleteMessage": "Izbriši poruku",
"deleteMessageConfirmation": "Jeste li sigurni da želite izbrisati ovu poruku?",
"refreshedContacts": "Kontakti osvježeni", "refreshedContacts": "Kontakti osvježeni",
@@ -636,6 +670,38 @@
"failedToDeleteMbtiles": "Neuspjelo brisanje offline karte", "failedToDeleteMbtiles": "Neuspjelo brisanje offline karte",
"importExportCachedTiles": "Uvoz/Izvoz predmemoriranih pločica",
"importExportDescription": "Sigurnosno kopirajte, dijelite i vraćajte preuzete pločice karte između uređaja",
"exportTilesToFile": "Izvezi pločice u datoteku",
"importTilesFromFile": "Uvezi pločice iz datoteke",
"selectExportLocation": "Odaberite lokaciju izvoza",
"selectImportFile": "Odaberite arhivu pločica",
"exportingTiles": "Izvažanje pločica...",
"importingTiles": "Uvažanje pločica...",
"exportSuccess": "Uspješno izvezeno {count} pločica",
"importSuccess": "Uspješno uvezeno {count} skladišta",
"exportFailed": "Izvoz nije uspio: {error}",
"importFailed": "Uvoz nije uspio: {error}",
"exportNote": "Stvara komprimiranu arhivsku datoteku (.fmtc) koju možete dijeliti i uvesti na drugim uređajima.",
"importNote": "Uvozi pločice karte iz prethodno izvezene arhivske datoteke. Pločice će biti spojene s postojećom predmemorijom.",
"noTilesToExport": "Nema pločica za izvoz",
"archiveContainsStores": "Arhiva sadrži {count} skladišta",
"vectorTiles": "Vektorske pločice", "vectorTiles": "Vektorske pločice",
"schema": "Shema", "schema": "Shema",
@@ -722,5 +788,63 @@
"failedToSave": "Neuspjelo spremanje: {error}", "failedToSave": "Neuspjelo spremanje: {error}",
"failedToGetLocation": "Neuspjelo dohvaćanje lokacije: {error}" "failedToGetLocation": "Neuspjelo dohvaćanje lokacije: {error}",
"sarTemplates": "SAR Templates",
"manageSarTemplates": "Manage cursor on target templates",
"addTemplate": "Add Template",
"editTemplate": "Edit Template",
"deleteTemplate": "Delete Template",
"templateName": "Template Name",
"templateNameHint": "e.g. Found Person",
"templateEmoji": "Emoji",
"emojiRequired": "Emoji je obavezan",
"nameRequired": "Ime je obavezno",
"templateDescription": "Description (Optional)",
"templateDescriptionHint": "Add additional context...",
"templateColor": "Color",
"previewFormat": "Preview (SAR Message Format)",
"importFromClipboard": "Import",
"exportToClipboard": "Export",
"deleteTemplateConfirmation": "Delete template '{name}'?",
"templateAdded": "Template added",
"templateUpdated": "Template updated",
"templateDeleted": "Template deleted",
"templatesImported": "{count, plural, =0{No templates imported} =1{Imported 1 template} other{Imported {count} templates}}",
"templatesExported": "{count, plural, =1{Exported 1 template to clipboard} other{Exported {count} templates to clipboard}}",
"importFailed": "Import failed: {error}",
"exportFailed": "Export failed: {error}",
"resetToDefaults": "Reset to Defaults",
"resetToDefaultsConfirmation": "This will delete all custom templates and restore the 4 default templates. Continue?",
"reset": "Reset",
"resetComplete": "Templates reset to defaults",
"noTemplates": "No templates available",
"tapAddToCreate": "Tap + to create your first template",
"ok": "OK",
"delete": "Obriši",
"permissionsSection": "Dozvole",
"locationPermission": "Dozvola za lokaciju",
"checking": "Provjera...",
"locationPermissionGrantedAlways": "Odobreno (Uvijek)",
"locationPermissionGrantedWhileInUse": "Odobreno (Tijekom uporabe)",
"locationPermissionDeniedTapToRequest": "Odbijeno - Dodirnite za zahtjev",
"locationPermissionPermanentlyDeniedOpenSettings": "Trajno odbijeno - Otvori postavke",
"locationPermissionDialogContent": "Dozvola za lokaciju je trajno odbijena. Omogućite je u postavkama uređaja kako biste koristili GPS praćenje i dijeljenje lokacije.",
"openSettings": "Otvori postavke",
"locationPermissionGranted": "Dozvola za lokaciju odobrena!",
"locationPermissionRequiredForGps": "Dozvola za lokaciju je potrebna za GPS praćenje i dijeljenje lokacije.",
"locationPermissionAlreadyGranted": "Dozvola za lokaciju je već odobrena.",
"sarNavyBlue": "SAR Mornarsko Plava",
"sarNavyBlueDescription": "Profesionalni/Operativni Način",
"selectRecipient": "Odaberi primatelja",
"broadcastToAllNearby": "Emituj svima u blizini",
"searchRecipients": "Pretraži primatelje...",
"noContactsFound": "Nema kontakata",
"noRoomsFound": "Nema soba",
"noContactsOrRoomsAvailable": "Nema dostupnih kontakata ili soba",
"messagesWillBeSentToPublicChannel": "Poruke će biti poslane na javni kanal",
"newMessage": "Nova poruka",
"channel": "Kanal"
} }

View File

@@ -207,6 +207,16 @@
"description": "Descrizione per l'impostazione degli indicatori RX/TX" "description": "Descrizione per l'impostazione degli indicatori RX/TX"
}, },
"simpleMode": "Modalità Semplice",
"@simpleMode": {
"description": "Impostazione per abilitare la modalità semplice"
},
"simpleModeDescription": "Nascondi informazioni non essenziali nei messaggi e contatti",
"@simpleModeDescription": {
"description": "Descrizione per l'impostazione della modalità semplice"
},
"language": "Lingua", "language": "Lingua",
"@language": { "@language": {
"description": "Etichetta impostazione lingua" "description": "Etichetta impostazione lingua"
@@ -514,7 +524,7 @@
"description": "Etichetta pulsante Aggiorna" "description": "Etichetta pulsante Aggiorna"
}, },
"sendDirectMessage": "Invia Messaggio Diretto", "sendDirectMessage": "Invia",
"@sendDirectMessage": { "@sendDirectMessage": {
"description": "Azione per inviare messaggio diretto al contatto" "description": "Azione per inviare messaggio diretto al contatto"
}, },
@@ -634,6 +644,36 @@
"description": "Azione per cancellare tutti i disegni locali" "description": "Azione per cancellare tutti i disegni locali"
}, },
"showReceivedDrawings": "Mostra Disegni Ricevuti",
"@showReceivedDrawings": {
"description": "Interruttore per mostrare/nascondere i disegni ricevuti da altri membri del team"
},
"showingAllDrawings": "Visualizzazione di tutti i disegni",
"@showingAllDrawings": {
"description": "Sottotitolo quando i disegni ricevuti sono visibili"
},
"showingOnlyYourDrawings": "Visualizzazione solo dei tuoi disegni",
"@showingOnlyYourDrawings": {
"description": "Sottotitolo quando i disegni ricevuti sono nascosti"
},
"showSarMarkers": "Mostra marcatori SAR",
"@showSarMarkers": {
"description": "Interruttore per mostrare/nascondere i marcatori SAR sulla mappa"
},
"showingSarMarkers": "Visualizzazione marcatori SAR",
"@showingSarMarkers": {
"description": "Sottotitolo quando i marcatori SAR sono visibili"
},
"hidingSarMarkers": "Nascondere marcatori SAR",
"@hidingSarMarkers": {
"description": "Sottotitolo quando i marcatori SAR sono nascosti"
},
"clearAll": "Cancella Tutto", "clearAll": "Cancella Tutto",
"@clearAll": { "@clearAll": {
"description": "Etichetta pulsante Cancella Tutto" "description": "Etichetta pulsante Cancella Tutto"
@@ -762,6 +802,11 @@
"description": "Etichetta per la sezione posizione" "description": "Etichetta per la sezione posizione"
}, },
"myLocation": "La mia posizione",
"@myLocation": {
"description": "Etichetta del pulsante per inserire la posizione GPS attuale"
},
"fromMap": "Dalla Mappa", "fromMap": "Dalla Mappa",
"@fromMap": { "@fromMap": {
"description": "Badge che mostra che la posizione proviene dal tocco sulla mappa" "description": "Badge che mostra che la posizione proviene dal tocco sulla mappa"
@@ -1125,6 +1170,21 @@
"description": "Etichetta precisione GPS" "description": "Etichetta precisione GPS"
}, },
"distance": "Distanza",
"@distance": {
"description": "Etichetta distanza nella bussola"
},
"bearing": "Rilevamento",
"@bearing": {
"description": "Etichetta rilevamento nella bussola"
},
"direction": "Direzione",
"@direction": {
"description": "Etichetta direzione nella bussola"
},
"filterMarkers": "Filtra Marcatori", "filterMarkers": "Filtra Marcatori",
"@filterMarkers": { "@filterMarkers": {
"description": "Titolo per la finestra filtra marcatori" "description": "Titolo per la finestra filtra marcatori"
@@ -1538,6 +1598,21 @@
"description": "Nome livello immagini satellitari ESRI" "description": "Nome livello immagini satellitari ESRI"
}, },
"googleHybrid": "Google Ibrido",
"@googleHybrid": {
"description": "Nome livello Google Ibrido (satellite + etichette)"
},
"googleRoadmap": "Google Mappa Stradale",
"@googleRoadmap": {
"description": "Nome livello Google Mappa Stradale"
},
"googleTerrain": "Google Terreno",
"@googleTerrain": {
"description": "Nome livello Google Terreno (topografico)"
},
"downloadVisibleArea": "Scarica area visibile", "downloadVisibleArea": "Scarica area visibile",
"@downloadVisibleArea": { "@downloadVisibleArea": {
"description": "Tooltip per il pulsante scarica area visibile" "description": "Tooltip per il pulsante scarica area visibile"
@@ -1673,6 +1748,11 @@
"description": "Messaggio informativo quando un messaggio viene eliminato" "description": "Messaggio informativo quando un messaggio viene eliminato"
}, },
"copyText": "Copia testo",
"textCopiedToClipboard": "Testo copiato negli appunti",
"deleteMessage": "Elimina messaggio",
"deleteMessageConfirmation": "Sei sicuro di voler eliminare questo messaggio?",
"refreshedContacts": "Contatti aggiornati", "refreshedContacts": "Contatti aggiornati",
"@refreshedContacts": { "@refreshedContacts": {
"description": "Messaggio di successo quando i contatti vengono aggiornati" "description": "Messaggio di successo quando i contatti vengono aggiornati"
@@ -1901,6 +1981,111 @@
"description": "Messaggio di errore quando l'eliminazione MBTiles fallisce" "description": "Messaggio di errore quando l'eliminazione MBTiles fallisce"
}, },
"importExportCachedTiles": "Importa/Esporta tile in cache",
"@importExportCachedTiles": {
"description": "Titolo per sezione di importazione/esportazione"
},
"importExportDescription": "Esegui backup, condividi e ripristina tile mappa scaricati tra dispositivi",
"@importExportDescription": {
"description": "Descrizione della funzionalità di importazione/esportazione"
},
"exportTilesToFile": "Esporta tile su file",
"@exportTilesToFile": {
"description": "Pulsante per esportare tile"
},
"importTilesFromFile": "Importa tile da file",
"@importTilesFromFile": {
"description": "Pulsante per importare tile"
},
"selectExportLocation": "Seleziona posizione esportazione",
"@selectExportLocation": {
"description": "Titolo per selettore file di esportazione"
},
"selectImportFile": "Seleziona archivio tile",
"@selectImportFile": {
"description": "Titolo per selettore file di importazione"
},
"exportingTiles": "Esportazione tile...",
"@exportingTiles": {
"description": "Messaggio di stato durante esportazione"
},
"importingTiles": "Importazione tile...",
"@importingTiles": {
"description": "Messaggio di stato durante importazione"
},
"exportSuccess": "{count} tile esportati con successo",
"@exportSuccess": {
"description": "Messaggio di successo dopo esportazione",
"placeholders": {
"count": {
"type": "int"
}
}
},
"importSuccess": "{count} archivi importati con successo",
"@importSuccess": {
"description": "Messaggio di successo dopo importazione",
"placeholders": {
"count": {
"type": "int"
}
}
},
"exportFailed": "Esportazione fallita: {error}",
"@exportFailed": {
"description": "Messaggio di errore quando esportazione fallisce",
"placeholders": {
"error": {
"type": "String"
}
}
},
"importFailed": "Importazione fallita: {error}",
"@importFailed": {
"description": "Messaggio di errore quando importazione fallisce",
"placeholders": {
"error": {
"type": "String"
}
}
},
"exportNote": "Crea un file archivio compresso (.fmtc) che può essere condiviso e importato su altri dispositivi.",
"@exportNote": {
"description": "Nota sulla funzionalità di esportazione"
},
"importNote": "Importa tile mappa da un file archivio precedentemente esportato. I tile verranno uniti con la cache esistente.",
"@importNote": {
"description": "Nota sulla funzionalità di importazione"
},
"noTilesToExport": "Nessun tile da esportare",
"@noTilesToExport": {
"description": "Messaggio quando cache è vuota"
},
"archiveContainsStores": "L'archivio contiene {count} archivi",
"@archiveContainsStores": {
"description": "Informazioni sul contenuto dell'archivio",
"placeholders": {
"count": {
"type": "int"
}
}
},
"vectorTiles": "Tile Vettoriali", "vectorTiles": "Tile Vettoriali",
"@vectorTiles": { "@vectorTiles": {
"description": "Etichetta per il tipo tile vettoriale" "description": "Etichetta per il tipo tile vettoriale"
@@ -2139,5 +2324,63 @@
"type": "String" "type": "String"
} }
} }
} },
"sarTemplates": "SAR Templates",
"manageSarTemplates": "Manage cursor on target templates",
"addTemplate": "Add Template",
"editTemplate": "Edit Template",
"deleteTemplate": "Delete Template",
"templateName": "Template Name",
"templateNameHint": "e.g. Found Person",
"templateEmoji": "Emoji",
"emojiRequired": "Emoji è obbligatorio",
"nameRequired": "Nome è obbligatorio",
"templateDescription": "Description (Optional)",
"templateDescriptionHint": "Add additional context...",
"templateColor": "Color",
"previewFormat": "Preview (SAR Message Format)",
"importFromClipboard": "Import",
"exportToClipboard": "Export",
"deleteTemplateConfirmation": "Delete template '{name}'?",
"templateAdded": "Template added",
"templateUpdated": "Template updated",
"templateDeleted": "Template deleted",
"templatesImported": "{count, plural, =0{No templates imported} =1{Imported 1 template} other{Imported {count} templates}}",
"templatesExported": "{count, plural, =1{Exported 1 template to clipboard} other{Exported {count} templates to clipboard}}",
"importFailed": "Import failed: {error}",
"exportFailed": "Export failed: {error}",
"resetToDefaults": "Reset to Defaults",
"resetToDefaultsConfirmation": "This will delete all custom templates and restore the 4 default templates. Continue?",
"reset": "Reset",
"resetComplete": "Templates reset to defaults",
"noTemplates": "No templates available",
"tapAddToCreate": "Tap + to create your first template",
"ok": "OK",
"delete": "Elimina",
"permissionsSection": "Permessi",
"locationPermission": "Permesso di posizione",
"checking": "Verifica in corso...",
"locationPermissionGrantedAlways": "Concesso (Sempre)",
"locationPermissionGrantedWhileInUse": "Concesso (Durante l'uso)",
"locationPermissionDeniedTapToRequest": "Negato - Tocca per richiedere",
"locationPermissionPermanentlyDeniedOpenSettings": "Negato permanentemente - Apri impostazioni",
"locationPermissionDialogContent": "Il permesso di posizione è permanentemente negato. Si prega di abilitarlo nelle impostazioni del dispositivo per utilizzare il tracciamento GPS e la condivisione della posizione.",
"openSettings": "Apri impostazioni",
"locationPermissionGranted": "Permesso di posizione concesso!",
"locationPermissionRequiredForGps": "Il permesso di posizione è necessario per il tracciamento GPS e la condivisione della posizione.",
"locationPermissionAlreadyGranted": "Il permesso di posizione è già concesso.",
"sarNavyBlue": "SAR Blu Navy",
"sarNavyBlueDescription": "Modalità Professionale/Operativa",
"selectRecipient": "Seleziona destinatario",
"broadcastToAllNearby": "Trasmetti a tutti nelle vicinanze",
"searchRecipients": "Cerca destinatari...",
"noContactsFound": "Nessun contatto trovato",
"noRoomsFound": "Nessuna stanza trovata",
"noContactsOrRoomsAvailable": "Nessun contatto o stanza disponibile",
"messagesWillBeSentToPublicChannel": "I messaggi saranno inviati al canale pubblico",
"newMessage": "Nuovo messaggio",
"channel": "Canale"
} }

View File

@@ -330,6 +330,18 @@ abstract class AppLocalizations {
/// **'Display packet activity indicators in top bar'** /// **'Display packet activity indicators in top bar'**
String get displayPacketActivity; String get displayPacketActivity;
/// Setting to enable simple mode
///
/// In en, this message translates to:
/// **'Simple Mode'**
String get simpleMode;
/// Description for simple mode setting
///
/// In en, this message translates to:
/// **'Hide non-essential information in messages and contacts'**
String get simpleModeDescription;
/// Language setting label /// Language setting label
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -656,7 +668,7 @@ abstract class AppLocalizations {
/// Action to send direct message to contact /// Action to send direct message to contact
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Send Direct Message'** /// **'Send'**
String get sendDirectMessage; String get sendDirectMessage;
/// Action to reset contact path for re-routing /// Action to reset contact path for re-routing
@@ -773,6 +785,42 @@ abstract class AppLocalizations {
/// **'Clear All Drawings'** /// **'Clear All Drawings'**
String get clearAllDrawings; String get clearAllDrawings;
/// Toggle to show/hide received drawings from other team members
///
/// In en, this message translates to:
/// **'Show Received Drawings'**
String get showReceivedDrawings;
/// Subtitle when received drawings are visible
///
/// In en, this message translates to:
/// **'Showing all drawings'**
String get showingAllDrawings;
/// Subtitle when received drawings are hidden
///
/// In en, this message translates to:
/// **'Showing only your drawings'**
String get showingOnlyYourDrawings;
/// Toggle to show/hide SAR markers on map
///
/// In en, this message translates to:
/// **'Show SAR Markers'**
String get showSarMarkers;
/// Subtitle when SAR markers are visible
///
/// In en, this message translates to:
/// **'Showing SAR markers'**
String get showingSarMarkers;
/// Subtitle when SAR markers are hidden
///
/// In en, this message translates to:
/// **'Hiding SAR markers'**
String get hidingSarMarkers;
/// Clear all button label /// Clear all button label
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -899,6 +947,12 @@ abstract class AppLocalizations {
/// **'Location'** /// **'Location'**
String get location; String get location;
/// Button label to insert current GPS location
///
/// In en, this message translates to:
/// **'My Location'**
String get myLocation;
/// Badge showing location is from map tap /// Badge showing location is from map tap
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -1247,6 +1301,18 @@ abstract class AppLocalizations {
/// **'Accuracy'** /// **'Accuracy'**
String get accuracy; String get accuracy;
/// Bearing label in compass
///
/// In en, this message translates to:
/// **'Bearing'**
String get bearing;
/// Direction label in compass
///
/// In en, this message translates to:
/// **'Direction'**
String get direction;
/// Title for filter markers dialog /// Title for filter markers dialog
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -1673,6 +1739,24 @@ abstract class AppLocalizations {
/// **'ESRI Satellite'** /// **'ESRI Satellite'**
String get esriSatellite; String get esriSatellite;
/// Google Hybrid layer name (satellite + labels)
///
/// In en, this message translates to:
/// **'Google Hybrid'**
String get googleHybrid;
/// Google Roadmap layer name (street map)
///
/// In en, this message translates to:
/// **'Google Roadmap'**
String get googleRoadmap;
/// Google Terrain layer name (topographic)
///
/// In en, this message translates to:
/// **'Google Terrain'**
String get googleTerrain;
/// Tooltip for download visible area button /// Tooltip for download visible area button
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -1805,6 +1889,24 @@ abstract class AppLocalizations {
/// **'Message deleted'** /// **'Message deleted'**
String get messageDeleted; String get messageDeleted;
/// Option to copy message text to clipboard
///
/// In en, this message translates to:
/// **'Copy text'**
String get copyText;
/// Dialog title for deleting a message
///
/// In en, this message translates to:
/// **'Delete message'**
String get deleteMessage;
/// Confirmation text for message deletion
///
/// In en, this message translates to:
/// **'Are you sure you want to delete this message?'**
String get deleteMessageConfirmation;
/// Success message when contacts are refreshed /// Success message when contacts are refreshed
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -2039,6 +2141,102 @@ abstract class AppLocalizations {
/// **'Failed to delete offline map'** /// **'Failed to delete offline map'**
String get failedToDeleteMbtiles; String get failedToDeleteMbtiles;
/// Title for import/export section
///
/// In en, this message translates to:
/// **'Import/Export Cached Tiles'**
String get importExportCachedTiles;
/// Description for import/export functionality
///
/// In en, this message translates to:
/// **'Backup, share, and restore downloaded map tiles between devices'**
String get importExportDescription;
/// Button to export tiles to archive file
///
/// In en, this message translates to:
/// **'Export Tiles to File'**
String get exportTilesToFile;
/// Button to import tiles from archive file
///
/// In en, this message translates to:
/// **'Import Tiles from File'**
String get importTilesFromFile;
/// Title for export file picker
///
/// In en, this message translates to:
/// **'Select Export Location'**
String get selectExportLocation;
/// Title for import file picker
///
/// In en, this message translates to:
/// **'Select Tile Archive'**
String get selectImportFile;
/// Status message during export
///
/// In en, this message translates to:
/// **'Exporting tiles...'**
String get exportingTiles;
/// Status message during import
///
/// In en, this message translates to:
/// **'Importing tiles...'**
String get importingTiles;
/// Success message after export
///
/// In en, this message translates to:
/// **'Exported {count} tiles successfully'**
String exportSuccess(int count);
/// Success message after import
///
/// In en, this message translates to:
/// **'Imported {count} stores successfully'**
String importSuccess(int count);
/// Error message when export fails
///
/// In en, this message translates to:
/// **'Export failed: {error}'**
String exportFailed(String error);
/// Error message when import fails
///
/// In en, this message translates to:
/// **'Import failed: {error}'**
String importFailed(String error);
/// Note about export functionality
///
/// In en, this message translates to:
/// **'Creates a compressed archive (.fmtc) file that can be shared and imported on other devices.'**
String get exportNote;
/// Note about import functionality
///
/// In en, this message translates to:
/// **'Imports map tiles from a previously exported archive file. Tiles will be merged with existing cache.'**
String get importNote;
/// Message when cache is empty
///
/// In en, this message translates to:
/// **'No tiles available to export'**
String get noTilesToExport;
/// Information about archive contents
///
/// In en, this message translates to:
/// **'Archive contains {count} stores'**
String archiveContainsStores(int count);
/// Label for vector tile type /// Label for vector tile type
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -2296,6 +2494,318 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'Failed to get location: {error}'** /// **'Failed to get location: {error}'**
String failedToGetLocation(String error); String failedToGetLocation(String error);
/// SAR templates menu title
///
/// In en, this message translates to:
/// **'SAR Templates'**
String get sarTemplates;
/// Subtitle for SAR templates settings
///
/// In en, this message translates to:
/// **'Manage cursor on target templates'**
String get manageSarTemplates;
/// Button to add new SAR template
///
/// In en, this message translates to:
/// **'Add Template'**
String get addTemplate;
/// Dialog title for editing template
///
/// In en, this message translates to:
/// **'Edit Template'**
String get editTemplate;
/// Action to delete template
///
/// In en, this message translates to:
/// **'Delete Template'**
String get deleteTemplate;
/// Label for template name field
///
/// In en, this message translates to:
/// **'Template Name'**
String get templateName;
/// Hint text for template name
///
/// In en, this message translates to:
/// **'e.g. Found Person'**
String get templateNameHint;
/// Label for template emoji field
///
/// In en, this message translates to:
/// **'Emoji'**
String get templateEmoji;
/// Validation error when emoji field is empty
///
/// In en, this message translates to:
/// **'Emoji is required'**
String get emojiRequired;
/// Validation error when name field is empty
///
/// In en, this message translates to:
/// **'Name is required'**
String get nameRequired;
/// Label for template description field
///
/// In en, this message translates to:
/// **'Description (Optional)'**
String get templateDescription;
/// Hint text for template description
///
/// In en, this message translates to:
/// **'Add additional context...'**
String get templateDescriptionHint;
/// Label for template color picker
///
/// In en, this message translates to:
/// **'Color'**
String get templateColor;
/// Label for format preview
///
/// In en, this message translates to:
/// **'Preview (SAR Message Format)'**
String get previewFormat;
/// Button to import templates from clipboard
///
/// In en, this message translates to:
/// **'Import'**
String get importFromClipboard;
/// Button to export templates to clipboard
///
/// In en, this message translates to:
/// **'Export'**
String get exportToClipboard;
/// Confirmation message for template deletion
///
/// In en, this message translates to:
/// **'Delete template \'{name}\'?'**
String deleteTemplateConfirmation(String name);
/// Success message when template is added
///
/// In en, this message translates to:
/// **'Template added'**
String get templateAdded;
/// Success message when template is updated
///
/// In en, this message translates to:
/// **'Template updated'**
String get templateUpdated;
/// Success message when template is deleted
///
/// In en, this message translates to:
/// **'Template deleted'**
String get templateDeleted;
/// Success message after importing templates
///
/// In en, this message translates to:
/// **'{count, plural, =0{No templates imported} =1{Imported 1 template} other{Imported {count} templates}}'**
String templatesImported(int count);
/// Success message after exporting templates
///
/// In en, this message translates to:
/// **'{count, plural, =1{Exported 1 template to clipboard} other{Exported {count} templates to clipboard}}'**
String templatesExported(int count);
/// Action to reset templates to defaults
///
/// In en, this message translates to:
/// **'Reset to Defaults'**
String get resetToDefaults;
/// Confirmation message for reset to defaults
///
/// In en, this message translates to:
/// **'This will delete all custom templates and restore the 4 default templates. Continue?'**
String get resetToDefaultsConfirmation;
/// Reset button label
///
/// In en, this message translates to:
/// **'Reset'**
String get reset;
/// Success message after reset
///
/// In en, this message translates to:
/// **'Templates reset to defaults'**
String get resetComplete;
/// Message when no templates exist
///
/// In en, this message translates to:
/// **'No templates available'**
String get noTemplates;
/// Helper text when no templates exist
///
/// In en, this message translates to:
/// **'Tap + to create your first template'**
String get tapAddToCreate;
/// OK button label
///
/// In en, this message translates to:
/// **'OK'**
String get ok;
/// Permissions section header
///
/// In en, this message translates to:
/// **'Permissions'**
String get permissionsSection;
/// Location permission label
///
/// In en, this message translates to:
/// **'Location Permission'**
String get locationPermission;
/// Loading state indicator
///
/// In en, this message translates to:
/// **'Checking...'**
String get checking;
/// Location permission status: granted always
///
/// In en, this message translates to:
/// **'Granted (Always)'**
String get locationPermissionGrantedAlways;
/// Location permission status: granted while in use
///
/// In en, this message translates to:
/// **'Granted (While In Use)'**
String get locationPermissionGrantedWhileInUse;
/// Location permission status: denied, user can request
///
/// In en, this message translates to:
/// **'Denied - Tap to request'**
String get locationPermissionDeniedTapToRequest;
/// Location permission status: permanently denied
///
/// In en, this message translates to:
/// **'Permanently Denied - Open Settings'**
String get locationPermissionPermanentlyDeniedOpenSettings;
/// Content for location permission dialog when permanently denied
///
/// In en, this message translates to:
/// **'Location permission is permanently denied. Please enable it in your device settings to use GPS tracking and location sharing features.'**
String get locationPermissionDialogContent;
/// Button to open device settings
///
/// In en, this message translates to:
/// **'Open Settings'**
String get openSettings;
/// Success message when location permission is granted
///
/// In en, this message translates to:
/// **'Location permission granted!'**
String get locationPermissionGranted;
/// Info message about location permission requirement
///
/// In en, this message translates to:
/// **'Location permission is required for GPS tracking and location sharing.'**
String get locationPermissionRequiredForGps;
/// Info message when permission is already granted
///
/// In en, this message translates to:
/// **'Location permission is already granted.'**
String get locationPermissionAlreadyGranted;
/// SAR Navy Blue theme name
///
/// In en, this message translates to:
/// **'SAR Navy Blue'**
String get sarNavyBlue;
/// Description for SAR Navy Blue theme
///
/// In en, this message translates to:
/// **'Professional/Operations Mode'**
String get sarNavyBlueDescription;
/// Title for recipient selector sheet
///
/// In en, this message translates to:
/// **'Select Recipient'**
String get selectRecipient;
/// Subtitle for public channel option
///
/// In en, this message translates to:
/// **'Broadcast to all nearby'**
String get broadcastToAllNearby;
/// Placeholder text for recipient search field
///
/// In en, this message translates to:
/// **'Search recipients...'**
String get searchRecipients;
/// Message when no contacts match search
///
/// In en, this message translates to:
/// **'No contacts found'**
String get noContactsFound;
/// Message when no rooms match search
///
/// In en, this message translates to:
/// **'No rooms found'**
String get noRoomsFound;
/// Message when no contacts or rooms exist
///
/// In en, this message translates to:
/// **'No contacts or rooms available'**
String get noContactsOrRoomsAvailable;
/// Info message when only public channel is available
///
/// In en, this message translates to:
/// **'Messages will be sent to public channel'**
String get messagesWillBeSentToPublicChannel;
/// Notification title for new message
///
/// In en, this message translates to:
/// **'New message'**
String get newMessage;
/// Channel label in notifications
///
/// In en, this message translates to:
/// **'Channel'**
String get channel;
} }
class _AppLocalizationsDelegate class _AppLocalizationsDelegate

View File

@@ -130,6 +130,13 @@ class AppLocalizationsDe extends AppLocalizations {
String get displayPacketActivity => String get displayPacketActivity =>
'Paketaktivitätsindikatoren in der oberen Leiste anzeigen'; 'Paketaktivitätsindikatoren in der oberen Leiste anzeigen';
@override
String get simpleMode => 'Einfacher Modus';
@override
String get simpleModeDescription =>
'Nicht wesentliche Informationen in Nachrichten und Kontakten ausblenden';
@override @override
String get language => 'Sprache'; String get language => 'Sprache';
@@ -316,7 +323,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get refresh => 'Aktualisieren'; String get refresh => 'Aktualisieren';
@override @override
String get sendDirectMessage => 'Direktnachricht senden'; String get sendDirectMessage => 'Senden';
@override @override
String get resetPath => 'Pfad zurücksetzen (Umleitung)'; String get resetPath => 'Pfad zurücksetzen (Umleitung)';
@@ -385,6 +392,24 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get clearAllDrawings => 'Alle Zeichnungen löschen'; String get clearAllDrawings => 'Alle Zeichnungen löschen';
@override
String get showReceivedDrawings => 'Empfangene Zeichnungen anzeigen';
@override
String get showingAllDrawings => 'Alle Zeichnungen werden angezeigt';
@override
String get showingOnlyYourDrawings => 'Nur Ihre Zeichnungen werden angezeigt';
@override
String get showSarMarkers => 'SAR-Markierungen anzeigen';
@override
String get showingSarMarkers => 'SAR-Markierungen werden angezeigt';
@override
String get hidingSarMarkers => 'SAR-Markierungen ausgeblendet';
@override @override
String get clearAll => 'Alle löschen'; String get clearAll => 'Alle löschen';
@@ -460,6 +485,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get location => 'Standort'; String get location => 'Standort';
@override
String get myLocation => 'Mein Standort';
@override @override
String get fromMap => 'Von Karte'; String get fromMap => 'Von Karte';
@@ -664,6 +692,12 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get accuracy => 'Genauigkeit'; String get accuracy => 'Genauigkeit';
@override
String get bearing => 'Peilung';
@override
String get direction => 'Richtung';
@override @override
String get filterMarkers => 'Markierungen filtern'; String get filterMarkers => 'Markierungen filtern';
@@ -906,6 +940,15 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get esriSatellite => 'ESRI-Satellit'; String get esriSatellite => 'ESRI-Satellit';
@override
String get googleHybrid => 'Google Hybrid';
@override
String get googleRoadmap => 'Google Straßenkarte';
@override
String get googleTerrain => 'Google Gelände';
@override @override
String get downloadVisibleArea => 'Sichtbaren Bereich herunterladen'; String get downloadVisibleArea => 'Sichtbaren Bereich herunterladen';
@@ -988,6 +1031,16 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get messageDeleted => 'Nachricht gelöscht'; String get messageDeleted => 'Nachricht gelöscht';
@override
String get copyText => 'Text kopieren';
@override
String get deleteMessage => 'Nachricht löschen';
@override
String get deleteMessageConfirmation =>
'Möchten Sie diese Nachricht wirklich löschen?';
@override @override
String get refreshedContacts => 'Kontakte aktualisiert'; String get refreshedContacts => 'Kontakte aktualisiert';
@@ -1123,6 +1176,67 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get failedToDeleteMbtiles => 'Fehler beim Löschen der Offline-Karte'; String get failedToDeleteMbtiles => 'Fehler beim Löschen der Offline-Karte';
@override
String get importExportCachedTiles => 'Import/Export gecachter Kacheln';
@override
String get importExportDescription =>
'Sichern, teilen und wiederherstellen Sie heruntergeladene Kartenkacheln zwischen Geräten';
@override
String get exportTilesToFile => 'Kacheln in Datei exportieren';
@override
String get importTilesFromFile => 'Kacheln aus Datei importieren';
@override
String get selectExportLocation => 'Exportspeicherort wählen';
@override
String get selectImportFile => 'Kachel-Archiv auswählen';
@override
String get exportingTiles => 'Exportiere Kacheln...';
@override
String get importingTiles => 'Importiere Kacheln...';
@override
String exportSuccess(int count) {
return '$count Kacheln erfolgreich exportiert';
}
@override
String importSuccess(int count) {
return '$count Speicher erfolgreich importiert';
}
@override
String exportFailed(String error) {
return 'Export failed: $error';
}
@override
String importFailed(String error) {
return 'Import failed: $error';
}
@override
String get exportNote =>
'Erstellt eine komprimierte Archivdatei (.fmtc), die auf anderen Geräten geteilt und importiert werden kann.';
@override
String get importNote =>
'Importiert Kartenkacheln aus einer zuvor exportierten Archivdatei. Kacheln werden mit dem vorhandenen Cache zusammengeführt.';
@override
String get noTilesToExport => 'Keine Kacheln zum Exportieren verfügbar';
@override
String archiveContainsStores(int count) {
return 'Archiv enthält $count Speicher';
}
@override @override
String get vectorTiles => 'Vektor-Tiles'; String get vectorTiles => 'Vektor-Tiles';
@@ -1263,4 +1377,188 @@ class AppLocalizationsDe extends AppLocalizations {
String failedToGetLocation(String error) { String failedToGetLocation(String error) {
return 'Fehler beim Abrufen des Standorts: $error'; return 'Fehler beim Abrufen des Standorts: $error';
} }
@override
String get sarTemplates => 'SAR Templates';
@override
String get manageSarTemplates => 'Manage cursor on target templates';
@override
String get addTemplate => 'Add Template';
@override
String get editTemplate => 'Edit Template';
@override
String get deleteTemplate => 'Delete Template';
@override
String get templateName => 'Template Name';
@override
String get templateNameHint => 'e.g. Found Person';
@override
String get templateEmoji => 'Emoji';
@override
String get emojiRequired => 'Emoji ist erforderlich';
@override
String get nameRequired => 'Name ist erforderlich';
@override
String get templateDescription => 'Description (Optional)';
@override
String get templateDescriptionHint => 'Add additional context...';
@override
String get templateColor => 'Color';
@override
String get previewFormat => 'Preview (SAR Message Format)';
@override
String get importFromClipboard => 'Import';
@override
String get exportToClipboard => 'Export';
@override
String deleteTemplateConfirmation(String name) {
return 'Delete template \'$name\'?';
}
@override
String get templateAdded => 'Template added';
@override
String get templateUpdated => 'Template updated';
@override
String get templateDeleted => 'Template deleted';
@override
String templatesImported(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'Imported $count templates',
one: 'Imported 1 template',
zero: 'No templates imported',
);
return '$_temp0';
}
@override
String templatesExported(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'Exported $count templates to clipboard',
one: 'Exported 1 template to clipboard',
);
return '$_temp0';
}
@override
String get resetToDefaults => 'Reset to Defaults';
@override
String get resetToDefaultsConfirmation =>
'This will delete all custom templates and restore the 4 default templates. Continue?';
@override
String get reset => 'Reset';
@override
String get resetComplete => 'Templates reset to defaults';
@override
String get noTemplates => 'No templates available';
@override
String get tapAddToCreate => 'Tap + to create your first template';
@override
String get ok => 'OK';
@override
String get permissionsSection => 'Berechtigungen';
@override
String get locationPermission => 'Standortberechtigung';
@override
String get checking => 'Überprüfen...';
@override
String get locationPermissionGrantedAlways => 'Erteilt (Immer)';
@override
String get locationPermissionGrantedWhileInUse =>
'Erteilt (Während der Nutzung)';
@override
String get locationPermissionDeniedTapToRequest =>
'Verweigert - Tippen zum Anfragen';
@override
String get locationPermissionPermanentlyDeniedOpenSettings =>
'Dauerhaft verweigert - Einstellungen öffnen';
@override
String get locationPermissionDialogContent =>
'Die Standortberechtigung wurde dauerhaft verweigert. Bitte aktivieren Sie sie in Ihren Geräteeinstellungen, um GPS-Tracking und Standortfreigabe zu nutzen.';
@override
String get openSettings => 'Einstellungen öffnen';
@override
String get locationPermissionGranted => 'Standortberechtigung erteilt!';
@override
String get locationPermissionRequiredForGps =>
'Die Standortberechtigung ist erforderlich für GPS-Tracking und Standortfreigabe.';
@override
String get locationPermissionAlreadyGranted =>
'Die Standortberechtigung wurde bereits erteilt.';
@override
String get sarNavyBlue => 'SAR Navy Blau';
@override
String get sarNavyBlueDescription => 'Professionell/Einsatzmodus';
@override
String get selectRecipient => 'Empfänger auswählen';
@override
String get broadcastToAllNearby => 'An alle in der Nähe senden';
@override
String get searchRecipients => 'Empfänger suchen...';
@override
String get noContactsFound => 'Keine Kontakte gefunden';
@override
String get noRoomsFound => 'Keine Räume gefunden';
@override
String get noContactsOrRoomsAvailable =>
'Keine Kontakte oder Räume verfügbar';
@override
String get messagesWillBeSentToPublicChannel =>
'Nachrichten werden an öffentlichen Kanal gesendet';
@override
String get newMessage => 'Neue Nachricht';
@override
String get channel => 'Kanal';
} }

View File

@@ -129,6 +129,13 @@ class AppLocalizationsEn extends AppLocalizations {
String get displayPacketActivity => String get displayPacketActivity =>
'Display packet activity indicators in top bar'; 'Display packet activity indicators in top bar';
@override
String get simpleMode => 'Simple Mode';
@override
String get simpleModeDescription =>
'Hide non-essential information in messages and contacts';
@override @override
String get language => 'Language'; String get language => 'Language';
@@ -314,7 +321,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get refresh => 'Refresh'; String get refresh => 'Refresh';
@override @override
String get sendDirectMessage => 'Send Direct Message'; String get sendDirectMessage => 'Send';
@override @override
String get resetPath => 'Reset Path (Re-route)'; String get resetPath => 'Reset Path (Re-route)';
@@ -382,6 +389,24 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get clearAllDrawings => 'Clear All Drawings'; String get clearAllDrawings => 'Clear All Drawings';
@override
String get showReceivedDrawings => 'Show Received Drawings';
@override
String get showingAllDrawings => 'Showing all drawings';
@override
String get showingOnlyYourDrawings => 'Showing only your drawings';
@override
String get showSarMarkers => 'Show SAR Markers';
@override
String get showingSarMarkers => 'Showing SAR markers';
@override
String get hidingSarMarkers => 'Hiding SAR markers';
@override @override
String get clearAll => 'Clear All'; String get clearAll => 'Clear All';
@@ -457,6 +482,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get location => 'Location'; String get location => 'Location';
@override
String get myLocation => 'My Location';
@override @override
String get fromMap => 'From Map'; String get fromMap => 'From Map';
@@ -660,6 +688,12 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get accuracy => 'Accuracy'; String get accuracy => 'Accuracy';
@override
String get bearing => 'Bearing';
@override
String get direction => 'Direction';
@override @override
String get filterMarkers => 'Filter Markers'; String get filterMarkers => 'Filter Markers';
@@ -900,6 +934,15 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get esriSatellite => 'ESRI Satellite'; String get esriSatellite => 'ESRI Satellite';
@override
String get googleHybrid => 'Google Hybrid';
@override
String get googleRoadmap => 'Google Roadmap';
@override
String get googleTerrain => 'Google Terrain';
@override @override
String get downloadVisibleArea => 'Download visible area'; String get downloadVisibleArea => 'Download visible area';
@@ -978,6 +1021,16 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get messageDeleted => 'Message deleted'; String get messageDeleted => 'Message deleted';
@override
String get copyText => 'Copy text';
@override
String get deleteMessage => 'Delete message';
@override
String get deleteMessageConfirmation =>
'Are you sure you want to delete this message?';
@override @override
String get refreshedContacts => 'Refreshed contacts'; String get refreshedContacts => 'Refreshed contacts';
@@ -1112,6 +1165,67 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get failedToDeleteMbtiles => 'Failed to delete offline map'; String get failedToDeleteMbtiles => 'Failed to delete offline map';
@override
String get importExportCachedTiles => 'Import/Export Cached Tiles';
@override
String get importExportDescription =>
'Backup, share, and restore downloaded map tiles between devices';
@override
String get exportTilesToFile => 'Export Tiles to File';
@override
String get importTilesFromFile => 'Import Tiles from File';
@override
String get selectExportLocation => 'Select Export Location';
@override
String get selectImportFile => 'Select Tile Archive';
@override
String get exportingTiles => 'Exporting tiles...';
@override
String get importingTiles => 'Importing tiles...';
@override
String exportSuccess(int count) {
return 'Exported $count tiles successfully';
}
@override
String importSuccess(int count) {
return 'Imported $count stores successfully';
}
@override
String exportFailed(String error) {
return 'Export failed: $error';
}
@override
String importFailed(String error) {
return 'Import failed: $error';
}
@override
String get exportNote =>
'Creates a compressed archive (.fmtc) file that can be shared and imported on other devices.';
@override
String get importNote =>
'Imports map tiles from a previously exported archive file. Tiles will be merged with existing cache.';
@override
String get noTilesToExport => 'No tiles available to export';
@override
String archiveContainsStores(int count) {
return 'Archive contains $count stores';
}
@override @override
String get vectorTiles => 'Vector Tiles'; String get vectorTiles => 'Vector Tiles';
@@ -1250,4 +1364,185 @@ class AppLocalizationsEn extends AppLocalizations {
String failedToGetLocation(String error) { String failedToGetLocation(String error) {
return 'Failed to get location: $error'; return 'Failed to get location: $error';
} }
@override
String get sarTemplates => 'SAR Templates';
@override
String get manageSarTemplates => 'Manage cursor on target templates';
@override
String get addTemplate => 'Add Template';
@override
String get editTemplate => 'Edit Template';
@override
String get deleteTemplate => 'Delete Template';
@override
String get templateName => 'Template Name';
@override
String get templateNameHint => 'e.g. Found Person';
@override
String get templateEmoji => 'Emoji';
@override
String get emojiRequired => 'Emoji is required';
@override
String get nameRequired => 'Name is required';
@override
String get templateDescription => 'Description (Optional)';
@override
String get templateDescriptionHint => 'Add additional context...';
@override
String get templateColor => 'Color';
@override
String get previewFormat => 'Preview (SAR Message Format)';
@override
String get importFromClipboard => 'Import';
@override
String get exportToClipboard => 'Export';
@override
String deleteTemplateConfirmation(String name) {
return 'Delete template \'$name\'?';
}
@override
String get templateAdded => 'Template added';
@override
String get templateUpdated => 'Template updated';
@override
String get templateDeleted => 'Template deleted';
@override
String templatesImported(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'Imported $count templates',
one: 'Imported 1 template',
zero: 'No templates imported',
);
return '$_temp0';
}
@override
String templatesExported(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'Exported $count templates to clipboard',
one: 'Exported 1 template to clipboard',
);
return '$_temp0';
}
@override
String get resetToDefaults => 'Reset to Defaults';
@override
String get resetToDefaultsConfirmation =>
'This will delete all custom templates and restore the 4 default templates. Continue?';
@override
String get reset => 'Reset';
@override
String get resetComplete => 'Templates reset to defaults';
@override
String get noTemplates => 'No templates available';
@override
String get tapAddToCreate => 'Tap + to create your first template';
@override
String get ok => 'OK';
@override
String get permissionsSection => 'Permissions';
@override
String get locationPermission => 'Location Permission';
@override
String get checking => 'Checking...';
@override
String get locationPermissionGrantedAlways => 'Granted (Always)';
@override
String get locationPermissionGrantedWhileInUse => 'Granted (While In Use)';
@override
String get locationPermissionDeniedTapToRequest => 'Denied - Tap to request';
@override
String get locationPermissionPermanentlyDeniedOpenSettings =>
'Permanently Denied - Open Settings';
@override
String get locationPermissionDialogContent =>
'Location permission is permanently denied. Please enable it in your device settings to use GPS tracking and location sharing features.';
@override
String get openSettings => 'Open Settings';
@override
String get locationPermissionGranted => 'Location permission granted!';
@override
String get locationPermissionRequiredForGps =>
'Location permission is required for GPS tracking and location sharing.';
@override
String get locationPermissionAlreadyGranted =>
'Location permission is already granted.';
@override
String get sarNavyBlue => 'SAR Navy Blue';
@override
String get sarNavyBlueDescription => 'Professional/Operations Mode';
@override
String get selectRecipient => 'Select Recipient';
@override
String get broadcastToAllNearby => 'Broadcast to all nearby';
@override
String get searchRecipients => 'Search recipients...';
@override
String get noContactsFound => 'No contacts found';
@override
String get noRoomsFound => 'No rooms found';
@override
String get noContactsOrRoomsAvailable => 'No contacts or rooms available';
@override
String get messagesWillBeSentToPublicChannel =>
'Messages will be sent to public channel';
@override
String get newMessage => 'New message';
@override
String get channel => 'Channel';
} }

View File

@@ -129,6 +129,13 @@ class AppLocalizationsEs extends AppLocalizations {
String get displayPacketActivity => String get displayPacketActivity =>
'Mostrar indicadores de actividad de paquetes en la barra superior'; 'Mostrar indicadores de actividad de paquetes en la barra superior';
@override
String get simpleMode => 'Modo Simple';
@override
String get simpleModeDescription =>
'Ocultar información no esencial en mensajes y contactos';
@override @override
String get language => 'Idioma'; String get language => 'Idioma';
@@ -315,7 +322,7 @@ class AppLocalizationsEs extends AppLocalizations {
String get refresh => 'Actualizar'; String get refresh => 'Actualizar';
@override @override
String get sendDirectMessage => 'Enviar mensaje directo'; String get sendDirectMessage => 'Enviar';
@override @override
String get resetPath => 'Restablecer ruta (Re-enrutar)'; String get resetPath => 'Restablecer ruta (Re-enrutar)';
@@ -383,6 +390,24 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get clearAllDrawings => 'Borrar todos los dibujos'; String get clearAllDrawings => 'Borrar todos los dibujos';
@override
String get showReceivedDrawings => 'Mostrar dibujos recibidos';
@override
String get showingAllDrawings => 'Mostrando todos los dibujos';
@override
String get showingOnlyYourDrawings => 'Mostrando solo tus dibujos';
@override
String get showSarMarkers => 'Mostrar marcadores SAR';
@override
String get showingSarMarkers => 'Mostrando marcadores SAR';
@override
String get hidingSarMarkers => 'Ocultando marcadores SAR';
@override @override
String get clearAll => 'Borrar todo'; String get clearAll => 'Borrar todo';
@@ -458,6 +483,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get location => 'Ubicación'; String get location => 'Ubicación';
@override
String get myLocation => 'Mi ubicación';
@override @override
String get fromMap => 'Desde el mapa'; String get fromMap => 'Desde el mapa';
@@ -661,6 +689,12 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get accuracy => 'Precisión'; String get accuracy => 'Precisión';
@override
String get bearing => 'Rumbo';
@override
String get direction => 'Dirección';
@override @override
String get filterMarkers => 'Filtrar marcadores'; String get filterMarkers => 'Filtrar marcadores';
@@ -904,6 +938,15 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get esriSatellite => 'ESRI Satélite'; String get esriSatellite => 'ESRI Satélite';
@override
String get googleHybrid => 'Google Híbrido';
@override
String get googleRoadmap => 'Google Mapa de Carreteras';
@override
String get googleTerrain => 'Google Terreno';
@override @override
String get downloadVisibleArea => 'Descargar área visible'; String get downloadVisibleArea => 'Descargar área visible';
@@ -984,6 +1027,16 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get messageDeleted => 'Mensaje eliminado'; String get messageDeleted => 'Mensaje eliminado';
@override
String get copyText => 'Copiar texto';
@override
String get deleteMessage => 'Eliminar mensaje';
@override
String get deleteMessageConfirmation =>
'¿Está seguro de que desea eliminar este mensaje?';
@override @override
String get refreshedContacts => 'Contactos actualizados'; String get refreshedContacts => 'Contactos actualizados';
@@ -1120,6 +1173,67 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get failedToDeleteMbtiles => 'Error al eliminar mapa sin conexión'; String get failedToDeleteMbtiles => 'Error al eliminar mapa sin conexión';
@override
String get importExportCachedTiles => 'Importar/Exportar teselas en caché';
@override
String get importExportDescription =>
'Realice copias de seguridad, comparta y restaure teselas de mapas descargadas entre dispositivos';
@override
String get exportTilesToFile => 'Exportar teselas a archivo';
@override
String get importTilesFromFile => 'Importar teselas desde archivo';
@override
String get selectExportLocation => 'Seleccionar ubicación de exportación';
@override
String get selectImportFile => 'Seleccionar archivo de teselas';
@override
String get exportingTiles => 'Exportando teselas...';
@override
String get importingTiles => 'Importando teselas...';
@override
String exportSuccess(int count) {
return '$count teselas exportadas exitosamente';
}
@override
String importSuccess(int count) {
return '$count almacenes importados exitosamente';
}
@override
String exportFailed(String error) {
return 'Export failed: $error';
}
@override
String importFailed(String error) {
return 'Import failed: $error';
}
@override
String get exportNote =>
'Crea un archivo comprimido (.fmtc) que se puede compartir e importar en otros dispositivos.';
@override
String get importNote =>
'Importa teselas de mapa desde un archivo previamente exportado. Las teselas se fusionarán con la caché existente.';
@override
String get noTilesToExport => 'No hay teselas para exportar';
@override
String archiveContainsStores(int count) {
return 'El archivo contiene $count almacenes';
}
@override @override
String get vectorTiles => 'Teselas vectoriales'; String get vectorTiles => 'Teselas vectoriales';
@@ -1258,4 +1372,188 @@ class AppLocalizationsEs extends AppLocalizations {
String failedToGetLocation(String error) { String failedToGetLocation(String error) {
return 'Error al obtener ubicación: $error'; return 'Error al obtener ubicación: $error';
} }
@override
String get sarTemplates => 'SAR Templates';
@override
String get manageSarTemplates => 'Manage cursor on target templates';
@override
String get addTemplate => 'Add Template';
@override
String get editTemplate => 'Edit Template';
@override
String get deleteTemplate => 'Delete Template';
@override
String get templateName => 'Template Name';
@override
String get templateNameHint => 'e.g. Found Person';
@override
String get templateEmoji => 'Emoji';
@override
String get emojiRequired => 'Se requiere emoji';
@override
String get nameRequired => 'Se requiere nombre';
@override
String get templateDescription => 'Description (Optional)';
@override
String get templateDescriptionHint => 'Add additional context...';
@override
String get templateColor => 'Color';
@override
String get previewFormat => 'Preview (SAR Message Format)';
@override
String get importFromClipboard => 'Import';
@override
String get exportToClipboard => 'Export';
@override
String deleteTemplateConfirmation(String name) {
return 'Delete template \'$name\'?';
}
@override
String get templateAdded => 'Template added';
@override
String get templateUpdated => 'Template updated';
@override
String get templateDeleted => 'Template deleted';
@override
String templatesImported(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'Imported $count templates',
one: 'Imported 1 template',
zero: 'No templates imported',
);
return '$_temp0';
}
@override
String templatesExported(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'Exported $count templates to clipboard',
one: 'Exported 1 template to clipboard',
);
return '$_temp0';
}
@override
String get resetToDefaults => 'Reset to Defaults';
@override
String get resetToDefaultsConfirmation =>
'This will delete all custom templates and restore the 4 default templates. Continue?';
@override
String get reset => 'Reset';
@override
String get resetComplete => 'Templates reset to defaults';
@override
String get noTemplates => 'No templates available';
@override
String get tapAddToCreate => 'Tap + to create your first template';
@override
String get ok => 'OK';
@override
String get permissionsSection => 'Permisos';
@override
String get locationPermission => 'Permiso de ubicación';
@override
String get checking => 'Comprobando...';
@override
String get locationPermissionGrantedAlways => 'Concedido (Siempre)';
@override
String get locationPermissionGrantedWhileInUse =>
'Concedido (Durante el uso)';
@override
String get locationPermissionDeniedTapToRequest =>
'Denegado - Toca para solicitar';
@override
String get locationPermissionPermanentlyDeniedOpenSettings =>
'Denegado permanentemente - Abrir ajustes';
@override
String get locationPermissionDialogContent =>
'El permiso de ubicación está permanentemente denegado. Por favor, actívalo en la configuración de tu dispositivo para usar el rastreo GPS y compartir ubicación.';
@override
String get openSettings => 'Abrir ajustes';
@override
String get locationPermissionGranted => '¡Permiso de ubicación concedido!';
@override
String get locationPermissionRequiredForGps =>
'El permiso de ubicación es necesario para el rastreo GPS y compartir ubicación.';
@override
String get locationPermissionAlreadyGranted =>
'El permiso de ubicación ya está concedido.';
@override
String get sarNavyBlue => 'SAR Azul Marino';
@override
String get sarNavyBlueDescription => 'Modo Profesional/Operaciones';
@override
String get selectRecipient => 'Seleccionar destinatario';
@override
String get broadcastToAllNearby => 'Transmitir a todos cercanos';
@override
String get searchRecipients => 'Buscar destinatarios...';
@override
String get noContactsFound => 'No se encontraron contactos';
@override
String get noRoomsFound => 'No se encontraron salas';
@override
String get noContactsOrRoomsAvailable =>
'No hay contactos o salas disponibles';
@override
String get messagesWillBeSentToPublicChannel =>
'Los mensajes se enviarán al canal público';
@override
String get newMessage => 'Nuevo mensaje';
@override
String get channel => 'Canal';
} }

View File

@@ -130,6 +130,13 @@ class AppLocalizationsFr extends AppLocalizations {
String get displayPacketActivity => String get displayPacketActivity =>
'Afficher les indicateurs d\'activité des paquets dans la barre supérieure'; 'Afficher les indicateurs d\'activité des paquets dans la barre supérieure';
@override
String get simpleMode => 'Mode Simple';
@override
String get simpleModeDescription =>
'Masquer les informations non essentielles dans les messages et les contacts';
@override @override
String get language => 'Langue'; String get language => 'Langue';
@@ -317,7 +324,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get refresh => 'Actualiser'; String get refresh => 'Actualiser';
@override @override
String get sendDirectMessage => 'Envoyer un message direct'; String get sendDirectMessage => 'Envoyer';
@override @override
String get resetPath => 'Réinitialiser le chemin (Re-router)'; String get resetPath => 'Réinitialiser le chemin (Re-router)';
@@ -385,6 +392,24 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get clearAllDrawings => 'Effacer tous les dessins'; String get clearAllDrawings => 'Effacer tous les dessins';
@override
String get showReceivedDrawings => 'Afficher les dessins reçus';
@override
String get showingAllDrawings => 'Affichage de tous les dessins';
@override
String get showingOnlyYourDrawings => 'Affichage uniquement de vos dessins';
@override
String get showSarMarkers => 'Afficher les marqueurs SAR';
@override
String get showingSarMarkers => 'Affichage des marqueurs SAR';
@override
String get hidingSarMarkers => 'Masquage des marqueurs SAR';
@override @override
String get clearAll => 'Tout effacer'; String get clearAll => 'Tout effacer';
@@ -461,6 +486,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get location => 'Position'; String get location => 'Position';
@override
String get myLocation => 'Ma position';
@override @override
String get fromMap => 'Depuis la carte'; String get fromMap => 'Depuis la carte';
@@ -665,6 +693,12 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get accuracy => 'Précision'; String get accuracy => 'Précision';
@override
String get bearing => 'Relèvement';
@override
String get direction => 'Direction';
@override @override
String get filterMarkers => 'Filtrer les marqueurs'; String get filterMarkers => 'Filtrer les marqueurs';
@@ -909,6 +943,15 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get esriSatellite => 'Satellite ESRI'; String get esriSatellite => 'Satellite ESRI';
@override
String get googleHybrid => 'Google Hybride';
@override
String get googleRoadmap => 'Google Carte Routière';
@override
String get googleTerrain => 'Google Terrain';
@override @override
String get downloadVisibleArea => 'Télécharger la zone visible'; String get downloadVisibleArea => 'Télécharger la zone visible';
@@ -989,6 +1032,16 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get messageDeleted => 'Message supprimé'; String get messageDeleted => 'Message supprimé';
@override
String get copyText => 'Copier le texte';
@override
String get deleteMessage => 'Supprimer le message';
@override
String get deleteMessageConfirmation =>
'Êtes-vous sûr de vouloir supprimer ce message?';
@override @override
String get refreshedContacts => 'Contacts actualisés'; String get refreshedContacts => 'Contacts actualisés';
@@ -1126,6 +1179,68 @@ class AppLocalizationsFr extends AppLocalizations {
String get failedToDeleteMbtiles => String get failedToDeleteMbtiles =>
'Échec de la suppression de la carte hors ligne'; 'Échec de la suppression de la carte hors ligne';
@override
String get importExportCachedTiles => 'Importer/Exporter les tuiles en cache';
@override
String get importExportDescription =>
'Sauvegarder, partager et restaurer les tuiles de carte téléchargées entre appareils';
@override
String get exportTilesToFile => 'Exporter les tuiles vers fichier';
@override
String get importTilesFromFile => 'Importer les tuiles depuis fichier';
@override
String get selectExportLocation =>
'Sélectionner l\'emplacement d\'exportation';
@override
String get selectImportFile => 'Sélectionner l\'archive de tuiles';
@override
String get exportingTiles => 'Exportation des tuiles...';
@override
String get importingTiles => 'Importation des tuiles...';
@override
String exportSuccess(int count) {
return '$count tuiles exportées avec succès';
}
@override
String importSuccess(int count) {
return '$count magasins importés avec succès';
}
@override
String exportFailed(String error) {
return 'Export failed: $error';
}
@override
String importFailed(String error) {
return 'Import failed: $error';
}
@override
String get exportNote =>
'Crée un fichier d\'archive compressé (.fmtc) qui peut être partagé et importé sur d\'autres appareils.';
@override
String get importNote =>
'Importe les tuiles de carte depuis un fichier d\'archive précédemment exporté. Les tuiles seront fusionnées avec le cache existant.';
@override
String get noTilesToExport => 'Aucune tuile à exporter';
@override
String archiveContainsStores(int count) {
return 'L\'archive contient $count magasins';
}
@override @override
String get vectorTiles => 'Tuiles vectorielles'; String get vectorTiles => 'Tuiles vectorielles';
@@ -1265,4 +1380,188 @@ class AppLocalizationsFr extends AppLocalizations {
String failedToGetLocation(String error) { String failedToGetLocation(String error) {
return 'Échec de l\'obtention de la position : $error'; return 'Échec de l\'obtention de la position : $error';
} }
@override
String get sarTemplates => 'SAR Templates';
@override
String get manageSarTemplates => 'Manage cursor on target templates';
@override
String get addTemplate => 'Add Template';
@override
String get editTemplate => 'Edit Template';
@override
String get deleteTemplate => 'Delete Template';
@override
String get templateName => 'Template Name';
@override
String get templateNameHint => 'e.g. Found Person';
@override
String get templateEmoji => 'Emoji';
@override
String get emojiRequired => 'Emoji est requis';
@override
String get nameRequired => 'Nom est requis';
@override
String get templateDescription => 'Description (Optional)';
@override
String get templateDescriptionHint => 'Add additional context...';
@override
String get templateColor => 'Color';
@override
String get previewFormat => 'Preview (SAR Message Format)';
@override
String get importFromClipboard => 'Import';
@override
String get exportToClipboard => 'Export';
@override
String deleteTemplateConfirmation(String name) {
return 'Delete template \'$name\'?';
}
@override
String get templateAdded => 'Template added';
@override
String get templateUpdated => 'Template updated';
@override
String get templateDeleted => 'Template deleted';
@override
String templatesImported(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'Imported $count templates',
one: 'Imported 1 template',
zero: 'No templates imported',
);
return '$_temp0';
}
@override
String templatesExported(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'Exported $count templates to clipboard',
one: 'Exported 1 template to clipboard',
);
return '$_temp0';
}
@override
String get resetToDefaults => 'Reset to Defaults';
@override
String get resetToDefaultsConfirmation =>
'This will delete all custom templates and restore the 4 default templates. Continue?';
@override
String get reset => 'Reset';
@override
String get resetComplete => 'Templates reset to defaults';
@override
String get noTemplates => 'No templates available';
@override
String get tapAddToCreate => 'Tap + to create your first template';
@override
String get ok => 'OK';
@override
String get permissionsSection => 'Autorisations';
@override
String get locationPermission => 'Autorisation de localisation';
@override
String get checking => 'Vérification...';
@override
String get locationPermissionGrantedAlways => 'Accordée (Toujours)';
@override
String get locationPermissionGrantedWhileInUse =>
'Accordée (En cours d\'utilisation)';
@override
String get locationPermissionDeniedTapToRequest =>
'Refusée - Appuyez pour demander';
@override
String get locationPermissionPermanentlyDeniedOpenSettings =>
'Refusée définitivement - Ouvrir les paramètres';
@override
String get locationPermissionDialogContent =>
'L\'autorisation de localisation est définitivement refusée. Veuillez l\'activer dans les paramètres de votre appareil pour utiliser le suivi GPS et le partage de localisation.';
@override
String get openSettings => 'Ouvrir les paramètres';
@override
String get locationPermissionGranted =>
'Autorisation de localisation accordée !';
@override
String get locationPermissionRequiredForGps =>
'L\'autorisation de localisation est nécessaire pour le suivi GPS et le partage de localisation.';
@override
String get locationPermissionAlreadyGranted =>
'L\'autorisation de localisation est déjà accordée.';
@override
String get sarNavyBlue => 'SAR Bleu Marine';
@override
String get sarNavyBlueDescription => 'Mode Professionnel/Opérations';
@override
String get selectRecipient => 'Sélectionner le destinataire';
@override
String get broadcastToAllNearby => 'Diffuser à tous à proximité';
@override
String get searchRecipients => 'Rechercher des destinataires...';
@override
String get noContactsFound => 'Aucun contact trouvé';
@override
String get noRoomsFound => 'Aucune salle trouvée';
@override
String get noContactsOrRoomsAvailable => 'Aucun contact ou salle disponible';
@override
String get messagesWillBeSentToPublicChannel =>
'Les messages seront envoyés au canal public';
@override
String get newMessage => 'Nouveau message';
@override
String get channel => 'Canal';
} }

View File

@@ -129,6 +129,13 @@ class AppLocalizationsHr extends AppLocalizations {
String get displayPacketActivity => String get displayPacketActivity =>
'Prikaži indikatore aktivnosti paketa u gornjoj traci'; 'Prikaži indikatore aktivnosti paketa u gornjoj traci';
@override
String get simpleMode => 'Jednostavni način';
@override
String get simpleModeDescription =>
'Sakrij nevažne informacije u porukama i kontaktima';
@override @override
String get language => 'Jezik'; String get language => 'Jezik';
@@ -305,7 +312,7 @@ class AppLocalizationsHr extends AppLocalizations {
String get deleteContact => 'Izbriši kontakt'; String get deleteContact => 'Izbriši kontakt';
@override @override
String get delete => 'Izbriši'; String get delete => 'Obriši';
@override @override
String get viewOnMap => 'Prikaži na karti'; String get viewOnMap => 'Prikaži na karti';
@@ -314,7 +321,7 @@ class AppLocalizationsHr extends AppLocalizations {
String get refresh => 'Osvježi'; String get refresh => 'Osvježi';
@override @override
String get sendDirectMessage => 'Pošalji izravnu poruku'; String get sendDirectMessage => 'Pošalji';
@override @override
String get resetPath => 'Resetiraj put (preusmjeri)'; String get resetPath => 'Resetiraj put (preusmjeri)';
@@ -382,6 +389,24 @@ class AppLocalizationsHr extends AppLocalizations {
@override @override
String get clearAllDrawings => 'Očisti sve crteže'; String get clearAllDrawings => 'Očisti sve crteže';
@override
String get showReceivedDrawings => 'Prikaži primljene crteže';
@override
String get showingAllDrawings => 'Prikazujem sve crteže';
@override
String get showingOnlyYourDrawings => 'Prikazujem samo vaše crteže';
@override
String get showSarMarkers => 'Prikaži SAR oznake';
@override
String get showingSarMarkers => 'Prikazujem SAR oznake';
@override
String get hidingSarMarkers => 'Skrivam SAR oznake';
@override @override
String get clearAll => 'Očisti sve'; String get clearAll => 'Očisti sve';
@@ -457,6 +482,9 @@ class AppLocalizationsHr extends AppLocalizations {
@override @override
String get location => 'Lokacija'; String get location => 'Lokacija';
@override
String get myLocation => 'Moja lokacija';
@override @override
String get fromMap => 'S karte'; String get fromMap => 'S karte';
@@ -660,6 +688,12 @@ class AppLocalizationsHr extends AppLocalizations {
@override @override
String get accuracy => 'Točnost'; String get accuracy => 'Točnost';
@override
String get bearing => 'Azimut';
@override
String get direction => 'Smjer';
@override @override
String get filterMarkers => 'Filtriraj markere'; String get filterMarkers => 'Filtriraj markere';
@@ -900,6 +934,15 @@ class AppLocalizationsHr extends AppLocalizations {
@override @override
String get esriSatellite => 'ESRI satelit'; String get esriSatellite => 'ESRI satelit';
@override
String get googleHybrid => 'Google hibridno';
@override
String get googleRoadmap => 'Google cestovna karta';
@override
String get googleTerrain => 'Google teren';
@override @override
String get downloadVisibleArea => 'Preuzmi vidljivo područje'; String get downloadVisibleArea => 'Preuzmi vidljivo područje';
@@ -980,6 +1023,16 @@ class AppLocalizationsHr extends AppLocalizations {
@override @override
String get messageDeleted => 'Poruka izbrisana'; String get messageDeleted => 'Poruka izbrisana';
@override
String get copyText => 'Kopiraj tekst';
@override
String get deleteMessage => 'Izbriši poruku';
@override
String get deleteMessageConfirmation =>
'Jeste li sigurni da želite izbrisati ovu poruku?';
@override @override
String get refreshedContacts => 'Kontakti osvježeni'; String get refreshedContacts => 'Kontakti osvježeni';
@@ -1113,6 +1166,67 @@ class AppLocalizationsHr extends AppLocalizations {
@override @override
String get failedToDeleteMbtiles => 'Neuspjelo brisanje offline karte'; String get failedToDeleteMbtiles => 'Neuspjelo brisanje offline karte';
@override
String get importExportCachedTiles => 'Uvoz/Izvoz predmemoriranih pločica';
@override
String get importExportDescription =>
'Sigurnosno kopirajte, dijelite i vraćajte preuzete pločice karte između uređaja';
@override
String get exportTilesToFile => 'Izvezi pločice u datoteku';
@override
String get importTilesFromFile => 'Uvezi pločice iz datoteke';
@override
String get selectExportLocation => 'Odaberite lokaciju izvoza';
@override
String get selectImportFile => 'Odaberite arhivu pločica';
@override
String get exportingTiles => 'Izvažanje pločica...';
@override
String get importingTiles => 'Uvažanje pločica...';
@override
String exportSuccess(int count) {
return 'Uspješno izvezeno $count pločica';
}
@override
String importSuccess(int count) {
return 'Uspješno uvezeno $count skladišta';
}
@override
String exportFailed(String error) {
return 'Export failed: $error';
}
@override
String importFailed(String error) {
return 'Import failed: $error';
}
@override
String get exportNote =>
'Stvara komprimiranu arhivsku datoteku (.fmtc) koju možete dijeliti i uvesti na drugim uređajima.';
@override
String get importNote =>
'Uvozi pločice karte iz prethodno izvezene arhivske datoteke. Pločice će biti spojene s postojećom predmemorijom.';
@override
String get noTilesToExport => 'Nema pločica za izvoz';
@override
String archiveContainsStores(int count) {
return 'Arhiva sadrži $count skladišta';
}
@override @override
String get vectorTiles => 'Vektorske pločice'; String get vectorTiles => 'Vektorske pločice';
@@ -1251,4 +1365,187 @@ class AppLocalizationsHr extends AppLocalizations {
String failedToGetLocation(String error) { String failedToGetLocation(String error) {
return 'Neuspjelo dohvaćanje lokacije: $error'; return 'Neuspjelo dohvaćanje lokacije: $error';
} }
@override
String get sarTemplates => 'SAR Templates';
@override
String get manageSarTemplates => 'Manage cursor on target templates';
@override
String get addTemplate => 'Add Template';
@override
String get editTemplate => 'Edit Template';
@override
String get deleteTemplate => 'Delete Template';
@override
String get templateName => 'Template Name';
@override
String get templateNameHint => 'e.g. Found Person';
@override
String get templateEmoji => 'Emoji';
@override
String get emojiRequired => 'Emoji je obavezan';
@override
String get nameRequired => 'Ime je obavezno';
@override
String get templateDescription => 'Description (Optional)';
@override
String get templateDescriptionHint => 'Add additional context...';
@override
String get templateColor => 'Color';
@override
String get previewFormat => 'Preview (SAR Message Format)';
@override
String get importFromClipboard => 'Import';
@override
String get exportToClipboard => 'Export';
@override
String deleteTemplateConfirmation(String name) {
return 'Delete template \'$name\'?';
}
@override
String get templateAdded => 'Template added';
@override
String get templateUpdated => 'Template updated';
@override
String get templateDeleted => 'Template deleted';
@override
String templatesImported(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'Imported $count templates',
one: 'Imported 1 template',
zero: 'No templates imported',
);
return '$_temp0';
}
@override
String templatesExported(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'Exported $count templates to clipboard',
one: 'Exported 1 template to clipboard',
);
return '$_temp0';
}
@override
String get resetToDefaults => 'Reset to Defaults';
@override
String get resetToDefaultsConfirmation =>
'This will delete all custom templates and restore the 4 default templates. Continue?';
@override
String get reset => 'Reset';
@override
String get resetComplete => 'Templates reset to defaults';
@override
String get noTemplates => 'No templates available';
@override
String get tapAddToCreate => 'Tap + to create your first template';
@override
String get ok => 'OK';
@override
String get permissionsSection => 'Dozvole';
@override
String get locationPermission => 'Dozvola za lokaciju';
@override
String get checking => 'Provjera...';
@override
String get locationPermissionGrantedAlways => 'Odobreno (Uvijek)';
@override
String get locationPermissionGrantedWhileInUse =>
'Odobreno (Tijekom uporabe)';
@override
String get locationPermissionDeniedTapToRequest =>
'Odbijeno - Dodirnite za zahtjev';
@override
String get locationPermissionPermanentlyDeniedOpenSettings =>
'Trajno odbijeno - Otvori postavke';
@override
String get locationPermissionDialogContent =>
'Dozvola za lokaciju je trajno odbijena. Omogućite je u postavkama uređaja kako biste koristili GPS praćenje i dijeljenje lokacije.';
@override
String get openSettings => 'Otvori postavke';
@override
String get locationPermissionGranted => 'Dozvola za lokaciju odobrena!';
@override
String get locationPermissionRequiredForGps =>
'Dozvola za lokaciju je potrebna za GPS praćenje i dijeljenje lokacije.';
@override
String get locationPermissionAlreadyGranted =>
'Dozvola za lokaciju je već odobrena.';
@override
String get sarNavyBlue => 'SAR Mornarsko Plava';
@override
String get sarNavyBlueDescription => 'Profesionalni/Operativni Način';
@override
String get selectRecipient => 'Odaberi primatelja';
@override
String get broadcastToAllNearby => 'Emituj svima u blizini';
@override
String get searchRecipients => 'Pretraži primatelje...';
@override
String get noContactsFound => 'Nema kontakata';
@override
String get noRoomsFound => 'Nema soba';
@override
String get noContactsOrRoomsAvailable => 'Nema dostupnih kontakata ili soba';
@override
String get messagesWillBeSentToPublicChannel =>
'Poruke će biti poslane na javni kanal';
@override
String get newMessage => 'Nova poruka';
@override
String get channel => 'Kanal';
} }

View File

@@ -129,6 +129,13 @@ class AppLocalizationsIt extends AppLocalizations {
String get displayPacketActivity => String get displayPacketActivity =>
'Mostra indicatori di attività pacchetti nella barra superiore'; 'Mostra indicatori di attività pacchetti nella barra superiore';
@override
String get simpleMode => 'Modalità Semplice';
@override
String get simpleModeDescription =>
'Nascondi informazioni non essenziali nei messaggi e contatti';
@override @override
String get language => 'Lingua'; String get language => 'Lingua';
@@ -316,7 +323,7 @@ class AppLocalizationsIt extends AppLocalizations {
String get refresh => 'Aggiorna'; String get refresh => 'Aggiorna';
@override @override
String get sendDirectMessage => 'Invia Messaggio Diretto'; String get sendDirectMessage => 'Invia';
@override @override
String get resetPath => 'Resetta Percorso (Ri-instrada)'; String get resetPath => 'Resetta Percorso (Ri-instrada)';
@@ -384,6 +391,24 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get clearAllDrawings => 'Cancella Tutti i Disegni'; String get clearAllDrawings => 'Cancella Tutti i Disegni';
@override
String get showReceivedDrawings => 'Mostra Disegni Ricevuti';
@override
String get showingAllDrawings => 'Visualizzazione di tutti i disegni';
@override
String get showingOnlyYourDrawings => 'Visualizzazione solo dei tuoi disegni';
@override
String get showSarMarkers => 'Mostra marcatori SAR';
@override
String get showingSarMarkers => 'Visualizzazione marcatori SAR';
@override
String get hidingSarMarkers => 'Nascondere marcatori SAR';
@override @override
String get clearAll => 'Cancella Tutto'; String get clearAll => 'Cancella Tutto';
@@ -459,6 +484,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get location => 'Posizione'; String get location => 'Posizione';
@override
String get myLocation => 'La mia posizione';
@override @override
String get fromMap => 'Dalla Mappa'; String get fromMap => 'Dalla Mappa';
@@ -662,6 +690,12 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get accuracy => 'Precisione'; String get accuracy => 'Precisione';
@override
String get bearing => 'Rilevamento';
@override
String get direction => 'Direzione';
@override @override
String get filterMarkers => 'Filtra Marcatori'; String get filterMarkers => 'Filtra Marcatori';
@@ -905,6 +939,15 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get esriSatellite => 'ESRI Satellite'; String get esriSatellite => 'ESRI Satellite';
@override
String get googleHybrid => 'Google Ibrido';
@override
String get googleRoadmap => 'Google Mappa Stradale';
@override
String get googleTerrain => 'Google Terreno';
@override @override
String get downloadVisibleArea => 'Scarica area visibile'; String get downloadVisibleArea => 'Scarica area visibile';
@@ -986,6 +1029,16 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get messageDeleted => 'Messaggio eliminato'; String get messageDeleted => 'Messaggio eliminato';
@override
String get copyText => 'Copia testo';
@override
String get deleteMessage => 'Elimina messaggio';
@override
String get deleteMessageConfirmation =>
'Sei sicuro di voler eliminare questo messaggio?';
@override @override
String get refreshedContacts => 'Contatti aggiornati'; String get refreshedContacts => 'Contatti aggiornati';
@@ -1121,6 +1174,67 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get failedToDeleteMbtiles => 'Impossibile eliminare la mappa offline'; String get failedToDeleteMbtiles => 'Impossibile eliminare la mappa offline';
@override
String get importExportCachedTiles => 'Importa/Esporta tile in cache';
@override
String get importExportDescription =>
'Esegui backup, condividi e ripristina tile mappa scaricati tra dispositivi';
@override
String get exportTilesToFile => 'Esporta tile su file';
@override
String get importTilesFromFile => 'Importa tile da file';
@override
String get selectExportLocation => 'Seleziona posizione esportazione';
@override
String get selectImportFile => 'Seleziona archivio tile';
@override
String get exportingTiles => 'Esportazione tile...';
@override
String get importingTiles => 'Importazione tile...';
@override
String exportSuccess(int count) {
return '$count tile esportati con successo';
}
@override
String importSuccess(int count) {
return '$count archivi importati con successo';
}
@override
String exportFailed(String error) {
return 'Export failed: $error';
}
@override
String importFailed(String error) {
return 'Import failed: $error';
}
@override
String get exportNote =>
'Crea un file archivio compresso (.fmtc) che può essere condiviso e importato su altri dispositivi.';
@override
String get importNote =>
'Importa tile mappa da un file archivio precedentemente esportato. I tile verranno uniti con la cache esistente.';
@override
String get noTilesToExport => 'Nessun tile da esportare';
@override
String archiveContainsStores(int count) {
return 'L\'archivio contiene $count archivi';
}
@override @override
String get vectorTiles => 'Tile Vettoriali'; String get vectorTiles => 'Tile Vettoriali';
@@ -1261,4 +1375,187 @@ class AppLocalizationsIt extends AppLocalizations {
String failedToGetLocation(String error) { String failedToGetLocation(String error) {
return 'Impossibile ottenere la posizione: $error'; return 'Impossibile ottenere la posizione: $error';
} }
@override
String get sarTemplates => 'SAR Templates';
@override
String get manageSarTemplates => 'Manage cursor on target templates';
@override
String get addTemplate => 'Add Template';
@override
String get editTemplate => 'Edit Template';
@override
String get deleteTemplate => 'Delete Template';
@override
String get templateName => 'Template Name';
@override
String get templateNameHint => 'e.g. Found Person';
@override
String get templateEmoji => 'Emoji';
@override
String get emojiRequired => 'Emoji è obbligatorio';
@override
String get nameRequired => 'Nome è obbligatorio';
@override
String get templateDescription => 'Description (Optional)';
@override
String get templateDescriptionHint => 'Add additional context...';
@override
String get templateColor => 'Color';
@override
String get previewFormat => 'Preview (SAR Message Format)';
@override
String get importFromClipboard => 'Import';
@override
String get exportToClipboard => 'Export';
@override
String deleteTemplateConfirmation(String name) {
return 'Delete template \'$name\'?';
}
@override
String get templateAdded => 'Template added';
@override
String get templateUpdated => 'Template updated';
@override
String get templateDeleted => 'Template deleted';
@override
String templatesImported(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'Imported $count templates',
one: 'Imported 1 template',
zero: 'No templates imported',
);
return '$_temp0';
}
@override
String templatesExported(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'Exported $count templates to clipboard',
one: 'Exported 1 template to clipboard',
);
return '$_temp0';
}
@override
String get resetToDefaults => 'Reset to Defaults';
@override
String get resetToDefaultsConfirmation =>
'This will delete all custom templates and restore the 4 default templates. Continue?';
@override
String get reset => 'Reset';
@override
String get resetComplete => 'Templates reset to defaults';
@override
String get noTemplates => 'No templates available';
@override
String get tapAddToCreate => 'Tap + to create your first template';
@override
String get ok => 'OK';
@override
String get permissionsSection => 'Permessi';
@override
String get locationPermission => 'Permesso di posizione';
@override
String get checking => 'Verifica in corso...';
@override
String get locationPermissionGrantedAlways => 'Concesso (Sempre)';
@override
String get locationPermissionGrantedWhileInUse => 'Concesso (Durante l\'uso)';
@override
String get locationPermissionDeniedTapToRequest =>
'Negato - Tocca per richiedere';
@override
String get locationPermissionPermanentlyDeniedOpenSettings =>
'Negato permanentemente - Apri impostazioni';
@override
String get locationPermissionDialogContent =>
'Il permesso di posizione è permanentemente negato. Si prega di abilitarlo nelle impostazioni del dispositivo per utilizzare il tracciamento GPS e la condivisione della posizione.';
@override
String get openSettings => 'Apri impostazioni';
@override
String get locationPermissionGranted => 'Permesso di posizione concesso!';
@override
String get locationPermissionRequiredForGps =>
'Il permesso di posizione è necessario per il tracciamento GPS e la condivisione della posizione.';
@override
String get locationPermissionAlreadyGranted =>
'Il permesso di posizione è già concesso.';
@override
String get sarNavyBlue => 'SAR Blu Navy';
@override
String get sarNavyBlueDescription => 'Modalità Professionale/Operativa';
@override
String get selectRecipient => 'Seleziona destinatario';
@override
String get broadcastToAllNearby => 'Trasmetti a tutti nelle vicinanze';
@override
String get searchRecipients => 'Cerca destinatari...';
@override
String get noContactsFound => 'Nessun contatto trovato';
@override
String get noRoomsFound => 'Nessuna stanza trovata';
@override
String get noContactsOrRoomsAvailable =>
'Nessun contatto o stanza disponibile';
@override
String get messagesWillBeSentToPublicChannel =>
'I messaggi saranno inviati al canale pubblico';
@override
String get newMessage => 'Nuovo messaggio';
@override
String get channel => 'Canale';
} }

View File

@@ -129,6 +129,13 @@ class AppLocalizationsSl extends AppLocalizations {
String get displayPacketActivity => String get displayPacketActivity =>
'Prikaži kazalnike aktivnosti paketov v zgornji vrstici'; 'Prikaži kazalnike aktivnosti paketov v zgornji vrstici';
@override
String get simpleMode => 'Preprost način';
@override
String get simpleModeDescription =>
'Skrij nepomembne informacije v sporočilih in kontaktih';
@override @override
String get language => 'Jezik'; String get language => 'Jezik';
@@ -314,7 +321,7 @@ class AppLocalizationsSl extends AppLocalizations {
String get refresh => 'Osveži'; String get refresh => 'Osveži';
@override @override
String get sendDirectMessage => 'Pošlji neposredno sporočilo'; String get sendDirectMessage => 'Pošlji';
@override @override
String get resetPath => 'Ponastavi pot (preusmeri)'; String get resetPath => 'Ponastavi pot (preusmeri)';
@@ -382,6 +389,24 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get clearAllDrawings => 'Počisti vse risbe'; String get clearAllDrawings => 'Počisti vse risbe';
@override
String get showReceivedDrawings => 'Prikaži prejete risbe';
@override
String get showingAllDrawings => 'Prikazujem vse risbe';
@override
String get showingOnlyYourDrawings => 'Prikazujem samo vaše risbe';
@override
String get showSarMarkers => 'Prikaži SAR označevalce';
@override
String get showingSarMarkers => 'Prikazujem SAR označevalce';
@override
String get hidingSarMarkers => 'Skrivam SAR označevalce';
@override @override
String get clearAll => 'Počisti vse'; String get clearAll => 'Počisti vse';
@@ -457,6 +482,9 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get location => 'Lokacija'; String get location => 'Lokacija';
@override
String get myLocation => 'Moja lokacija';
@override @override
String get fromMap => 'Z zemljevida'; String get fromMap => 'Z zemljevida';
@@ -660,6 +688,12 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get accuracy => 'Natančnost'; String get accuracy => 'Natančnost';
@override
String get bearing => 'Azimut';
@override
String get direction => 'Smer';
@override @override
String get filterMarkers => 'Filtriraj označevalce'; String get filterMarkers => 'Filtriraj označevalce';
@@ -900,6 +934,15 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get esriSatellite => 'ESRI satelit'; String get esriSatellite => 'ESRI satelit';
@override
String get googleHybrid => 'Google hibridno';
@override
String get googleRoadmap => 'Google cestni zemljevid';
@override
String get googleTerrain => 'Google teren';
@override @override
String get downloadVisibleArea => 'Prenesi vidno območje'; String get downloadVisibleArea => 'Prenesi vidno območje';
@@ -980,6 +1023,16 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get messageDeleted => 'Sporočilo izbrisano'; String get messageDeleted => 'Sporočilo izbrisano';
@override
String get copyText => 'Kopiraj besedilo';
@override
String get deleteMessage => 'Izbriši sporočilo';
@override
String get deleteMessageConfirmation =>
'Ali ste prepričani, da želite izbrisati to sporočilo?';
@override @override
String get refreshedContacts => 'Stiki osveženi'; String get refreshedContacts => 'Stiki osveženi';
@@ -1116,6 +1169,67 @@ class AppLocalizationsSl extends AppLocalizations {
String get failedToDeleteMbtiles => String get failedToDeleteMbtiles =>
'Brisanje brezpoveznega zemljevida ni uspelo'; 'Brisanje brezpoveznega zemljevida ni uspelo';
@override
String get importExportCachedTiles => 'Uvoz/Izvoz predpomnjenih ploščic';
@override
String get importExportDescription =>
'Varnostno kopirajte, delite in obnovite prenesene ploščice zemljevida med napravami';
@override
String get exportTilesToFile => 'Izvozi ploščice v datoteko';
@override
String get importTilesFromFile => 'Uvozi ploščice iz datoteke';
@override
String get selectExportLocation => 'Izberi lokacijo izvoza';
@override
String get selectImportFile => 'Izberi arhiv ploščic';
@override
String get exportingTiles => 'Izvažanje ploščic...';
@override
String get importingTiles => 'Uvažanje ploščic...';
@override
String exportSuccess(int count) {
return 'Uspešno izvoženih $count ploščic';
}
@override
String importSuccess(int count) {
return 'Uspešno uvoženih $count skladišč';
}
@override
String exportFailed(String error) {
return 'Export failed: $error';
}
@override
String importFailed(String error) {
return 'Import failed: $error';
}
@override
String get exportNote =>
'Ustvari stisnjeno arhivsko datoteko (.fmtc), ki jo lahko delite in uvozite na drugih napravah.';
@override
String get importNote =>
'Uvozi ploščice zemljevida iz predhodno izvožene arhivske datoteke. Ploščice bodo združene z obstoječim predpomnilnikom.';
@override
String get noTilesToExport => 'Ni ploščic za izvoz';
@override
String archiveContainsStores(int count) {
return 'Arhiv vsebuje $count skladišč';
}
@override @override
String get vectorTiles => 'Vektorske ploščice'; String get vectorTiles => 'Vektorske ploščice';
@@ -1254,4 +1368,186 @@ class AppLocalizationsSl extends AppLocalizations {
String failedToGetLocation(String error) { String failedToGetLocation(String error) {
return 'Pridobivanje lokacije ni uspelo: $error'; return 'Pridobivanje lokacije ni uspelo: $error';
} }
@override
String get sarTemplates => 'SAR Templates';
@override
String get manageSarTemplates => 'Manage cursor on target templates';
@override
String get addTemplate => 'Add Template';
@override
String get editTemplate => 'Edit Template';
@override
String get deleteTemplate => 'Delete Template';
@override
String get templateName => 'Template Name';
@override
String get templateNameHint => 'e.g. Found Person';
@override
String get templateEmoji => 'Emoji';
@override
String get emojiRequired => 'Emoji je obvezen';
@override
String get nameRequired => 'Ime je obvezno';
@override
String get templateDescription => 'Description (Optional)';
@override
String get templateDescriptionHint => 'Add additional context...';
@override
String get templateColor => 'Color';
@override
String get previewFormat => 'Preview (SAR Message Format)';
@override
String get importFromClipboard => 'Import';
@override
String get exportToClipboard => 'Export';
@override
String deleteTemplateConfirmation(String name) {
return 'Delete template \'$name\'?';
}
@override
String get templateAdded => 'Template added';
@override
String get templateUpdated => 'Template updated';
@override
String get templateDeleted => 'Template deleted';
@override
String templatesImported(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'Imported $count templates',
one: 'Imported 1 template',
zero: 'No templates imported',
);
return '$_temp0';
}
@override
String templatesExported(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'Exported $count templates to clipboard',
one: 'Exported 1 template to clipboard',
);
return '$_temp0';
}
@override
String get resetToDefaults => 'Reset to Defaults';
@override
String get resetToDefaultsConfirmation =>
'This will delete all custom templates and restore the 4 default templates. Continue?';
@override
String get reset => 'Reset';
@override
String get resetComplete => 'Templates reset to defaults';
@override
String get noTemplates => 'No templates available';
@override
String get tapAddToCreate => 'Tap + to create your first template';
@override
String get ok => 'OK';
@override
String get permissionsSection => 'Dovoljenja';
@override
String get locationPermission => 'Dovoljenje za lokacijo';
@override
String get checking => 'Preverjanje...';
@override
String get locationPermissionGrantedAlways => 'Odobreno (Vedno)';
@override
String get locationPermissionGrantedWhileInUse => 'Odobreno (Med uporabo)';
@override
String get locationPermissionDeniedTapToRequest =>
'Zavrnjeno - Tapnite za zahtevo';
@override
String get locationPermissionPermanentlyDeniedOpenSettings =>
'Trajno zavrnjeno - Odpri nastavitve';
@override
String get locationPermissionDialogContent =>
'Dovoljenje za lokacijo je trajno zavrnjeno. Omogočite ga v nastavitvah naprave za uporabo sledenja GPS in deljenja lokacije.';
@override
String get openSettings => 'Odpri nastavitve';
@override
String get locationPermissionGranted => 'Dovoljenje za lokacijo odobreno!';
@override
String get locationPermissionRequiredForGps =>
'Dovoljenje za lokacijo je potrebno za sledenje GPS in deljenje lokacije.';
@override
String get locationPermissionAlreadyGranted =>
'Dovoljenje za lokacijo je že odobreno.';
@override
String get sarNavyBlue => 'SAR Mornarska Modra';
@override
String get sarNavyBlueDescription => 'Profesionalni/Operativni Način';
@override
String get selectRecipient => 'Izberi prejemnika';
@override
String get broadcastToAllNearby => 'Oddajaj vsem v bližini';
@override
String get searchRecipients => 'Išči prejemnike...';
@override
String get noContactsFound => 'Ni kontaktov';
@override
String get noRoomsFound => 'Ni sob';
@override
String get noContactsOrRoomsAvailable => 'Ni na voljo kontaktov ali sob';
@override
String get messagesWillBeSentToPublicChannel =>
'Sporočila bodo poslana na javni kanal';
@override
String get newMessage => 'Novo sporočilo';
@override
String get channel => 'Kanal';
} }

View File

@@ -75,6 +75,10 @@
"displayPacketActivity": "Prikaži kazalnike aktivnosti paketov v zgornji vrstici", "displayPacketActivity": "Prikaži kazalnike aktivnosti paketov v zgornji vrstici",
"simpleMode": "Preprost način",
"simpleModeDescription": "Skrij nepomembne informacije v sporočilih in kontaktih",
"language": "Jezik", "language": "Jezik",
"chooseLanguage": "Izberite jezik", "chooseLanguage": "Izberite jezik",
@@ -181,7 +185,7 @@
"refresh": "Osveži", "refresh": "Osveži",
"sendDirectMessage": "Pošlji neposredno sporočilo", "sendDirectMessage": "Pošlji",
"resetPath": "Ponastavi pot (preusmeri)", "resetPath": "Ponastavi pot (preusmeri)",
@@ -221,6 +225,18 @@
"clearAllDrawings": "Počisti vse risbe", "clearAllDrawings": "Počisti vse risbe",
"showReceivedDrawings": "Prikaži prejete risbe",
"showingAllDrawings": "Prikazujem vse risbe",
"showingOnlyYourDrawings": "Prikazujem samo vaše risbe",
"showSarMarkers": "Prikaži SAR označevalce",
"showingSarMarkers": "Prikazujem SAR označevalce",
"hidingSarMarkers": "Skrivam SAR označevalce",
"clearAll": "Počisti vse", "clearAll": "Počisti vse",
"noLocalDrawings": "Ni lokalnih risb za deljenje", "noLocalDrawings": "Ni lokalnih risb za deljenje",
@@ -263,6 +279,8 @@
"location": "Lokacija", "location": "Lokacija",
"myLocation": "Moja lokacija",
"fromMap": "Z zemljevida", "fromMap": "Z zemljevida",
"gettingLocation": "Pridobivanje lokacije...", "gettingLocation": "Pridobivanje lokacije...",
@@ -379,6 +397,12 @@
"accuracy": "Natančnost", "accuracy": "Natančnost",
"distance": "Razdalja",
"bearing": "Azimut",
"direction": "Smer",
"filterMarkers": "Filtriraj označevalce", "filterMarkers": "Filtriraj označevalce",
"filterMarkersTooltip": "Filtriraj označevalce", "filterMarkersTooltip": "Filtriraj označevalce",
@@ -521,6 +545,12 @@
"esriSatellite": "ESRI satelit", "esriSatellite": "ESRI satelit",
"googleHybrid": "Google hibridno",
"googleRoadmap": "Google cestni zemljevid",
"googleTerrain": "Google teren",
"downloadVisibleArea": "Prenesi vidno območje", "downloadVisibleArea": "Prenesi vidno območje",
"initializingMap": "Inicializacija zemljevida...", "initializingMap": "Inicializacija zemljevida...",
@@ -586,6 +616,10 @@
"cannotReplyContactNotFound": "Ni mogoče odgovoriti: stik ni najden", "cannotReplyContactNotFound": "Ni mogoče odgovoriti: stik ni najden",
"messageDeleted": "Sporočilo izbrisano", "messageDeleted": "Sporočilo izbrisano",
"copyText": "Kopiraj besedilo",
"textCopiedToClipboard": "Besedilo kopirano v odložišče",
"deleteMessage": "Izbriši sporočilo",
"deleteMessageConfirmation": "Ali ste prepričani, da želite izbrisati to sporočilo?",
"refreshedContacts": "Stiki osveženi", "refreshedContacts": "Stiki osveženi",
@@ -636,6 +670,38 @@
"failedToDeleteMbtiles": "Brisanje brezpoveznega zemljevida ni uspelo", "failedToDeleteMbtiles": "Brisanje brezpoveznega zemljevida ni uspelo",
"importExportCachedTiles": "Uvoz/Izvoz predpomnjenih ploščic",
"importExportDescription": "Varnostno kopirajte, delite in obnovite prenesene ploščice zemljevida med napravami",
"exportTilesToFile": "Izvozi ploščice v datoteko",
"importTilesFromFile": "Uvozi ploščice iz datoteke",
"selectExportLocation": "Izberi lokacijo izvoza",
"selectImportFile": "Izberi arhiv ploščic",
"exportingTiles": "Izvažanje ploščic...",
"importingTiles": "Uvažanje ploščic...",
"exportSuccess": "Uspešno izvoženih {count} ploščic",
"importSuccess": "Uspešno uvoženih {count} skladišč",
"exportFailed": "Izvoz ni uspel: {error}",
"importFailed": "Uvoz ni uspel: {error}",
"exportNote": "Ustvari stisnjeno arhivsko datoteko (.fmtc), ki jo lahko delite in uvozite na drugih napravah.",
"importNote": "Uvozi ploščice zemljevida iz predhodno izvožene arhivske datoteke. Ploščice bodo združene z obstoječim predpomnilnikom.",
"noTilesToExport": "Ni ploščic za izvoz",
"archiveContainsStores": "Arhiv vsebuje {count} skladišč",
"vectorTiles": "Vektorske ploščice", "vectorTiles": "Vektorske ploščice",
"schema": "Shema", "schema": "Shema",
@@ -722,5 +788,63 @@
"failedToSave": "Shranjevanje ni uspelo: {error}", "failedToSave": "Shranjevanje ni uspelo: {error}",
"failedToGetLocation": "Pridobivanje lokacije ni uspelo: {error}" "failedToGetLocation": "Pridobivanje lokacije ni uspelo: {error}",
"sarTemplates": "SAR Templates",
"manageSarTemplates": "Manage cursor on target templates",
"addTemplate": "Add Template",
"editTemplate": "Edit Template",
"deleteTemplate": "Delete Template",
"templateName": "Template Name",
"templateNameHint": "e.g. Found Person",
"templateEmoji": "Emoji",
"emojiRequired": "Emoji je obvezen",
"nameRequired": "Ime je obvezno",
"templateDescription": "Description (Optional)",
"templateDescriptionHint": "Add additional context...",
"templateColor": "Color",
"previewFormat": "Preview (SAR Message Format)",
"importFromClipboard": "Import",
"exportToClipboard": "Export",
"deleteTemplateConfirmation": "Delete template '{name}'?",
"templateAdded": "Template added",
"templateUpdated": "Template updated",
"templateDeleted": "Template deleted",
"templatesImported": "{count, plural, =0{No templates imported} =1{Imported 1 template} other{Imported {count} templates}}",
"templatesExported": "{count, plural, =1{Exported 1 template to clipboard} other{Exported {count} templates to clipboard}}",
"importFailed": "Import failed: {error}",
"exportFailed": "Export failed: {error}",
"resetToDefaults": "Reset to Defaults",
"resetToDefaultsConfirmation": "This will delete all custom templates and restore the 4 default templates. Continue?",
"reset": "Reset",
"resetComplete": "Templates reset to defaults",
"noTemplates": "No templates available",
"tapAddToCreate": "Tap + to create your first template",
"ok": "OK",
"delete": "Izbriši",
"permissionsSection": "Dovoljenja",
"locationPermission": "Dovoljenje za lokacijo",
"checking": "Preverjanje...",
"locationPermissionGrantedAlways": "Odobreno (Vedno)",
"locationPermissionGrantedWhileInUse": "Odobreno (Med uporabo)",
"locationPermissionDeniedTapToRequest": "Zavrnjeno - Tapnite za zahtevo",
"locationPermissionPermanentlyDeniedOpenSettings": "Trajno zavrnjeno - Odpri nastavitve",
"locationPermissionDialogContent": "Dovoljenje za lokacijo je trajno zavrnjeno. Omogočite ga v nastavitvah naprave za uporabo sledenja GPS in deljenja lokacije.",
"openSettings": "Odpri nastavitve",
"locationPermissionGranted": "Dovoljenje za lokacijo odobreno!",
"locationPermissionRequiredForGps": "Dovoljenje za lokacijo je potrebno za sledenje GPS in deljenje lokacije.",
"locationPermissionAlreadyGranted": "Dovoljenje za lokacijo je že odobreno.",
"sarNavyBlue": "SAR Mornarska Modra",
"sarNavyBlueDescription": "Profesionalni/Operativni Način",
"selectRecipient": "Izberi prejemnika",
"broadcastToAllNearby": "Oddajaj vsem v bližini",
"searchRecipients": "Išči prejemnike...",
"noContactsFound": "Ni kontaktov",
"noRoomsFound": "Ni sob",
"noContactsOrRoomsAvailable": "Ni na voljo kontaktov ali sob",
"messagesWillBeSentToPublicChannel": "Sporočila bodo poslana na javni kanal",
"newMessage": "Novo sporočilo",
"channel": "Kanal"
} }

View File

@@ -113,9 +113,11 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
ChangeNotifierProvider(create: (_) => ConnectionProvider()), ChangeNotifierProvider(create: (_) => ConnectionProvider()),
ChangeNotifierProvider( ChangeNotifierProvider(
create: (_) { create: (_) {
// Don't initialize here - it will be initialized in AppProvider.initialize() // Initialize early to load persisted contacts for offline viewing
// after connection is established and device info is available // Self-contact filtering will happen later when BLE connects
return ContactsProvider(); final provider = ContactsProvider();
provider.initializeEarly();
return provider;
}, },
), ),
ChangeNotifierProvider( ChangeNotifierProvider(

View File

@@ -71,6 +71,7 @@ abstract class MapDrawing {
final DateTime createdAt; final DateTime createdAt;
final String? senderName; // Name of sender (null if local drawing) final String? senderName; // Name of sender (null if local drawing)
final bool isReceived; // True if drawing was received from another node final bool isReceived; // True if drawing was received from another node
final String? messageId; // ID of the source message (for navigation)
MapDrawing({ MapDrawing({
required this.id, required this.id,
@@ -79,6 +80,7 @@ abstract class MapDrawing {
required this.createdAt, required this.createdAt,
this.senderName, this.senderName,
this.isReceived = false, this.isReceived = false,
this.messageId,
}); });
/// Convert to JSON for persistence /// Convert to JSON for persistence
@@ -90,8 +92,12 @@ abstract class MapDrawing {
Map<String, dynamic> toNetworkJson(); Map<String, dynamic> toNetworkJson();
/// Parse network JSON (compact format) /// Parse network JSON (compact format)
/// senderName will be populated from packet metadata /// senderName and messageId will be populated from packet metadata
static MapDrawing? fromNetworkJson(Map<String, dynamic> json, {String? senderName}) { static MapDrawing? fromNetworkJson(
Map<String, dynamic> json, {
String? senderName,
String? messageId,
}) {
final typeNum = json['t'] as int?; final typeNum = json['t'] as int?;
if (typeNum == null || typeNum < 0 || typeNum >= DrawingShapeType.values.length) { if (typeNum == null || typeNum < 0 || typeNum >= DrawingShapeType.values.length) {
return null; return null;
@@ -102,9 +108,17 @@ abstract class MapDrawing {
switch (type) { switch (type) {
case DrawingShapeType.line: case DrawingShapeType.line:
return LineDrawing.fromNetworkJson(json, senderName: senderName); return LineDrawing.fromNetworkJson(
json,
senderName: senderName,
messageId: messageId,
);
case DrawingShapeType.rectangle: case DrawingShapeType.rectangle:
return RectangleDrawing.fromNetworkJson(json, senderName: senderName); return RectangleDrawing.fromNetworkJson(
json,
senderName: senderName,
messageId: messageId,
);
} }
} catch (e) { } catch (e) {
return null; return null;
@@ -144,6 +158,7 @@ class LineDrawing extends MapDrawing {
required this.points, required this.points,
super.senderName, super.senderName,
super.isReceived, super.isReceived,
super.messageId,
}) : super(type: DrawingShapeType.line); }) : super(type: DrawingShapeType.line);
@override @override
@@ -184,7 +199,11 @@ class LineDrawing extends MapDrawing {
); );
} }
static LineDrawing fromNetworkJson(Map<String, dynamic> json, {String? senderName}) { static LineDrawing fromNetworkJson(
Map<String, dynamic> json, {
String? senderName,
String? messageId,
}) {
// Parse ultra-compact format // Parse ultra-compact format
final pointsFlat = (json['p'] as List<dynamic>).cast<double>(); final pointsFlat = (json['p'] as List<dynamic>).cast<double>();
final points = <LatLng>[]; final points = <LatLng>[];
@@ -199,6 +218,7 @@ class LineDrawing extends MapDrawing {
points: points, points: points,
senderName: senderName, senderName: senderName,
isReceived: true, isReceived: true,
messageId: messageId, // Link to source message
); );
} }
@@ -226,6 +246,7 @@ class RectangleDrawing extends MapDrawing {
required this.bottomRight, required this.bottomRight,
super.senderName, super.senderName,
super.isReceived, super.isReceived,
super.messageId,
}) : super(type: DrawingShapeType.rectangle); }) : super(type: DrawingShapeType.rectangle);
/// Get all corner points for rendering /// Get all corner points for rendering
@@ -276,7 +297,11 @@ class RectangleDrawing extends MapDrawing {
); );
} }
static RectangleDrawing fromNetworkJson(Map<String, dynamic> json, {String? senderName}) { static RectangleDrawing fromNetworkJson(
Map<String, dynamic> json, {
String? senderName,
String? messageId,
}) {
// Parse ultra-compact format // Parse ultra-compact format
final bounds = (json['b'] as List<dynamic>).cast<double>(); final bounds = (json['b'] as List<dynamic>).cast<double>();
@@ -288,6 +313,7 @@ class RectangleDrawing extends MapDrawing {
bottomRight: LatLng(bounds[2], bounds[3]), bottomRight: LatLng(bounds[2], bounds[3]),
senderName: senderName, senderName: senderName,
isReceived: true, isReceived: true,
messageId: messageId, // Link to source message
); );
} }

View File

@@ -6,6 +6,9 @@ enum MapLayerType {
openStreetMap, openStreetMap,
openTopoMap, openTopoMap,
esriWorldImagery, esriWorldImagery,
googleHybrid,
googleRoadmap,
googleTerrain,
vectorMbtiles, vectorMbtiles,
} }
@@ -46,6 +49,12 @@ class MapLayer {
return localizations.openTopoMap; return localizations.openTopoMap;
case MapLayerType.esriWorldImagery: case MapLayerType.esriWorldImagery:
return localizations.esriSatellite; return localizations.esriSatellite;
case MapLayerType.googleHybrid:
return localizations.googleHybrid;
case MapLayerType.googleRoadmap:
return localizations.googleRoadmap;
case MapLayerType.googleTerrain:
return localizations.googleTerrain;
case MapLayerType.vectorMbtiles: case MapLayerType.vectorMbtiles:
// For vector tiles, use the name from metadata // For vector tiles, use the name from metadata
return name; return name;
@@ -77,10 +86,37 @@ class MapLayer {
maxZoom: 19, // ESRI World Imagery maximum maxZoom: 19, // ESRI World Imagery maximum
); );
static const googleHybrid = MapLayer(
type: MapLayerType.googleHybrid,
name: 'Google Hybrid',
urlTemplate: 'http://mt0.google.com/vt/lyrs=y&hl=en&x={x}&y={y}&z={z}',
attribution: '© Google',
maxZoom: 20, // Google Maps maximum
);
static const googleRoadmap = MapLayer(
type: MapLayerType.googleRoadmap,
name: 'Google Roadmap',
urlTemplate: 'http://mt0.google.com/vt/lyrs=m&hl=en&x={x}&y={y}&z={z}',
attribution: '© Google',
maxZoom: 20, // Google Maps maximum
);
static const googleTerrain = MapLayer(
type: MapLayerType.googleTerrain,
name: 'Google Terrain',
urlTemplate: 'http://mt0.google.com/vt/lyrs=p&hl=en&x={x}&y={y}&z={z}',
attribution: '© Google',
maxZoom: 20, // Google Maps maximum
);
static const List<MapLayer> allLayers = [ static const List<MapLayer> allLayers = [
openStreetMap, openStreetMap,
openTopoMap, openTopoMap,
esriWorldImagery, esriWorldImagery,
googleHybrid,
googleRoadmap,
googleTerrain,
]; ];
static MapLayer fromType(MapLayerType type) { static MapLayer fromType(MapLayerType type) {

View File

@@ -1,4 +1,5 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import 'sar_marker.dart'; import 'sar_marker.dart';
@@ -51,6 +52,7 @@ class Message {
final SarMarkerType? sarMarkerType; final SarMarkerType? sarMarkerType;
final LatLng? sarGpsCoordinates; final LatLng? sarGpsCoordinates;
final String? sarNotes; // Optional message/notes for SAR marker final String? sarNotes; // Optional message/notes for SAR marker
final String? sarCustomEmoji; // Custom emoji for unknown SAR marker types
// Display metadata // Display metadata
final DateTime receivedAt; final DateTime receivedAt;
@@ -89,6 +91,7 @@ class Message {
this.sarMarkerType, this.sarMarkerType,
this.sarGpsCoordinates, this.sarGpsCoordinates,
this.sarNotes, this.sarNotes,
this.sarCustomEmoji,
required this.receivedAt, required this.receivedAt,
this.senderName, this.senderName,
this.deliveryStatus = MessageDeliveryStatus.received, this.deliveryStatus = MessageDeliveryStatus.received,
@@ -171,6 +174,13 @@ class Message {
return null; return null;
} }
// Debug: Check what's in sarNotes
debugPrint('📍 [Message.toSarMarker] Converting to marker:');
debugPrint(' message.text: "${text}"');
debugPrint(' message.sarNotes: "${sarNotes}"');
debugPrint(' message.sarMarkerType: ${sarMarkerType}');
debugPrint(' message.sarCustomEmoji: "${sarCustomEmoji}"');
return SarMarker( return SarMarker(
id: id, id: id,
type: sarMarkerType!, type: sarMarkerType!,
@@ -179,6 +189,7 @@ class Message {
senderPublicKey: senderPublicKeyPrefix, senderPublicKey: senderPublicKeyPrefix,
senderName: senderName, senderName: senderName,
notes: sarNotes, // Use dedicated notes field instead of full text notes: sarNotes, // Use dedicated notes field instead of full text
customEmoji: sarCustomEmoji, // Preserve custom emoji for unknown types
); );
} }
@@ -275,6 +286,7 @@ class Message {
SarMarkerType? sarMarkerType, SarMarkerType? sarMarkerType,
LatLng? sarGpsCoordinates, LatLng? sarGpsCoordinates,
String? sarNotes, String? sarNotes,
String? sarCustomEmoji,
DateTime? receivedAt, DateTime? receivedAt,
String? senderName, String? senderName,
MessageDeliveryStatus? deliveryStatus, MessageDeliveryStatus? deliveryStatus,
@@ -303,6 +315,7 @@ class Message {
sarMarkerType: sarMarkerType ?? this.sarMarkerType, sarMarkerType: sarMarkerType ?? this.sarMarkerType,
sarGpsCoordinates: sarGpsCoordinates ?? this.sarGpsCoordinates, sarGpsCoordinates: sarGpsCoordinates ?? this.sarGpsCoordinates,
sarNotes: sarNotes ?? this.sarNotes, sarNotes: sarNotes ?? this.sarNotes,
sarCustomEmoji: sarCustomEmoji ?? this.sarCustomEmoji,
receivedAt: receivedAt ?? this.receivedAt, receivedAt: receivedAt ?? this.receivedAt,
senderName: senderName ?? this.senderName, senderName: senderName ?? this.senderName,
deliveryStatus: deliveryStatus ?? this.deliveryStatus, deliveryStatus: deliveryStatus ?? this.deliveryStatus,

View File

@@ -2,6 +2,7 @@ import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import '../l10n/app_localizations.dart'; import '../l10n/app_localizations.dart';
import '../services/sar_template_service.dart';
/// SAR (Search & Rescue) marker types /// SAR (Search & Rescue) marker types
enum SarMarkerType { enum SarMarkerType {
@@ -75,6 +76,7 @@ class SarMarker {
final Uint8List? senderPublicKey; final Uint8List? senderPublicKey;
final String? senderName; final String? senderName;
final String? notes; final String? notes;
final String? customEmoji; // For custom SAR markers not in predefined types
SarMarker({ SarMarker({
required this.id, required this.id,
@@ -84,6 +86,7 @@ class SarMarker {
this.senderPublicKey, this.senderPublicKey,
this.senderName, this.senderName,
this.notes, this.notes,
this.customEmoji,
}); });
/// Get sender public key as hex string (short) /// Get sender public key as hex string (short)
@@ -109,9 +112,47 @@ class SarMarker {
return DateTime.now().difference(timestamp).inHours < 1; return DateTime.now().difference(timestamp).inHours < 1;
} }
/// Get display name /// Get the emoji to display (custom emoji if available, otherwise type emoji)
String get emoji {
return customEmoji ?? type.emoji;
}
/// Get display name - uses notes if available, otherwise looks up template by emoji, otherwise type name
String get displayName { String get displayName {
return '${type.emoji} ${type.displayName}'; if (notes != null && notes!.isNotEmpty) {
return notes!;
}
// If no notes and we have a custom emoji, try to look up the template
if (customEmoji != null) {
// Import the service here to avoid circular dependencies
// We'll use a static lookup method
return _lookupTemplateNameByEmoji(customEmoji!) ?? type.displayName;
}
return type.displayName;
}
/// Look up template name by emoji from SarTemplateService
static String? _lookupTemplateNameByEmoji(String emoji) {
try {
// Use the singleton instance
final service = SarTemplateService();
if (!service.isInitialized) {
return null;
}
// Find template with matching emoji
final template = service.templates.firstWhere(
(t) => t.emoji == emoji,
orElse: () => throw StateError('No template found'),
);
return template.name;
} catch (e) {
// Template not found or service not initialized
return null;
}
} }
SarMarker copyWith({ SarMarker copyWith({
@@ -122,6 +163,7 @@ class SarMarker {
Uint8List? senderPublicKey, Uint8List? senderPublicKey,
String? senderName, String? senderName,
String? notes, String? notes,
String? customEmoji,
}) { }) {
return SarMarker( return SarMarker(
id: id ?? this.id, id: id ?? this.id,
@@ -131,6 +173,7 @@ class SarMarker {
senderPublicKey: senderPublicKey ?? this.senderPublicKey, senderPublicKey: senderPublicKey ?? this.senderPublicKey,
senderName: senderName ?? this.senderName, senderName: senderName ?? this.senderName,
notes: notes ?? this.notes, notes: notes ?? this.notes,
customEmoji: customEmoji ?? this.customEmoji,
); );
} }

View File

@@ -0,0 +1,196 @@
import 'package:flutter/material.dart';
/// SAR Template - Customizable template for SAR (Cursor on Target) messages
class SarTemplate {
final String id;
final String emoji;
final String name;
final String description;
final String colorHex;
final bool isDefault;
SarTemplate({
required this.id,
required this.emoji,
required this.name,
required this.description,
required this.colorHex,
this.isDefault = false,
});
/// Get color from hex string
Color get color {
final hexCode = colorHex.replaceAll('#', '');
return Color(int.parse('FF$hexCode', radix: 16));
}
/// Create from JSON
factory SarTemplate.fromJson(Map<String, dynamic> json) {
return SarTemplate(
id: json['id'] as String,
emoji: json['emoji'] as String,
name: json['name'] as String,
description: json['description'] as String? ?? '',
colorHex: json['colorHex'] as String,
isDefault: json['isDefault'] as bool? ?? false,
);
}
/// Convert to JSON
Map<String, dynamic> toJson() {
return {
'id': id,
'emoji': emoji,
'name': name,
'description': description,
'colorHex': colorHex,
'isDefault': isDefault,
};
}
/// Create from SAR message format (S:emoji:0,0:description)
/// Example: S:🧑:0,0:Person found
factory SarTemplate.fromSarMessage(String message) {
final trimmed = message.trim();
if (!trimmed.startsWith('S:')) {
throw FormatException('SAR message must start with "S:"');
}
// Parse format: S:emoji:lat,lon:description
final parts = trimmed.split(':');
if (parts.length < 3) {
throw FormatException('Invalid SAR message format');
}
final emoji = parts[1].trim();
if (emoji.isEmpty) {
throw FormatException('Emoji cannot be empty');
}
// Extract description (everything after the third colon)
String description = '';
if (parts.length > 3) {
description = parts.sublist(3).join(':').trim();
}
// Generate ID from emoji + description
final id = '${emoji}_${DateTime.now().millisecondsSinceEpoch}';
// Auto-assign color based on emoji
String colorHex = _getColorForEmoji(emoji);
return SarTemplate(
id: id,
emoji: emoji,
name: description.isNotEmpty ? description : emoji,
description: description,
colorHex: colorHex,
isDefault: false,
);
}
/// Convert to SAR message format with placeholder coordinates
/// Example: S:🧑:0,0:Person found
String toSarMessage() {
if (description.isNotEmpty) {
return 'S:$emoji:0,0:$description';
}
return 'S:$emoji:0,0';
}
/// Auto-assign color based on emoji
static String _getColorForEmoji(String emoji) {
// Default emoji to color mapping
final colorMap = {
'🧑': '#4CAF50', // Green - Person
'👤': '#4CAF50', // Green - Person
'🔥': '#F44336', // Red - Fire
'🏕️': '#FF9800', // Orange - Staging
'': '#FF9800', // Orange - Staging
'📦': '#9C27B0', // Purple - Object
'🚁': '#2196F3', // Blue - Helicopter
'🚒': '#F44336', // Red - Fire truck
'🚑': '#F44336', // Red - Ambulance
'⚠️': '#FFC107', // Yellow - Warning
'': '#F44336', // Red - Hazard
'': '#4CAF50', // Green - Safe
'🏥': '#F44336', // Red - Medical
'💧': '#2196F3', // Blue - Water
'🌲': '#4CAF50', // Green - Forest
'⛰️': '#795548', // Brown - Mountain
};
return colorMap[emoji] ?? '#9E9E9E'; // Default gray
}
/// Copy with modifications
SarTemplate copyWith({
String? id,
String? emoji,
String? name,
String? description,
String? colorHex,
bool? isDefault,
}) {
return SarTemplate(
id: id ?? this.id,
emoji: emoji ?? this.emoji,
name: name ?? this.name,
description: description ?? this.description,
colorHex: colorHex ?? this.colorHex,
isDefault: isDefault ?? this.isDefault,
);
}
@override
String toString() {
return 'SarTemplate(id: $id, emoji: $emoji, name: $name, description: $description)';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is SarTemplate && id == other.id;
}
@override
int get hashCode => id.hashCode;
/// Default templates (matches existing SarMarkerType)
static List<SarTemplate> get defaults {
return [
SarTemplate(
id: 'default_found_person',
emoji: '🧑',
name: 'Found Person',
description: '',
colorHex: '#4CAF50',
isDefault: true,
),
SarTemplate(
id: 'default_fire',
emoji: '🔥',
name: 'Fire',
description: '',
colorHex: '#F44336',
isDefault: true,
),
SarTemplate(
id: 'default_staging_area',
emoji: '🏕️',
name: 'Staging Area',
description: '',
colorHex: '#FF9800',
isDefault: true,
),
SarTemplate(
id: 'default_object',
emoji: '📦',
name: 'Object',
description: '',
colorHex: '#9C27B0',
isDefault: true,
),
];
}
}

View File

@@ -24,6 +24,9 @@ class AppProvider with ChangeNotifier {
bool _isInitialized = false; bool _isInitialized = false;
bool get isInitialized => _isInitialized; bool get isInitialized => _isInitialized;
bool _isSimpleMode = false;
bool get isSimpleMode => _isSimpleMode;
AppProvider({ AppProvider({
required this.connectionProvider, required this.connectionProvider,
required this.contactsProvider, required this.contactsProvider,
@@ -35,9 +38,33 @@ class AppProvider with ChangeNotifier {
_setupCallbacks(); _setupCallbacks();
_initializeTileCache(); _initializeTileCache();
_initializeLocationTracking(); _initializeLocationTracking();
_loadSimpleMode();
_isInitialized = true; _isInitialized = true;
} }
/// Load simple mode setting from shared preferences
Future<void> _loadSimpleMode() async {
try {
final prefs = await SharedPreferences.getInstance();
_isSimpleMode = prefs.getBool('simple_mode') ?? 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('simple_mode', enabled);
notifyListeners();
} catch (e) {
debugPrint('Error saving simple mode setting: $e');
}
}
/// Initialize tile cache service /// Initialize tile cache service
Future<void> _initializeTileCache() async { Future<void> _initializeTileCache() async {
try { try {
@@ -116,19 +143,17 @@ class AppProvider with ChangeNotifier {
final drawing = DrawingMessageParser.parseDrawingMessage( final drawing = DrawingMessageParser.parseDrawingMessage(
message.text, message.text,
senderName: senderName, senderName: senderName,
messageId: message.id, // Pass message ID for navigation linking
); );
if (drawing != null) { if (drawing != null) {
debugPrint('🎨 [AppProvider] Drawing parsed successfully: ${drawing.type.name} from ${drawing.senderName ?? "unknown"}'); debugPrint('🎨 [AppProvider] Drawing parsed successfully: ${drawing.type.name} from ${drawing.senderName ?? "unknown"}');
debugPrint(' Drawing linked to message ID: ${message.id}');
drawingProvider.addReceivedDrawing(drawing); drawingProvider.addReceivedDrawing(drawing);
// Add informational message to chat // Add the original drawing message to chat (not a modified info message)
final drawingTypeStr = drawing.type.name.substring(0, 1).toUpperCase() + // This allows users to click on the drawing message to navigate to it
drawing.type.name.substring(1);
final infoMessage = message.copyWith(
text: '📍 Received map drawing ($drawingTypeStr) from ${drawing.senderName ?? "unknown"}',
);
messagesProvider.addMessage( messagesProvider.addMessage(
infoMessage, message,
contactLookup: (name) => '', contactLookup: (name) => '',
); );
} else { } else {
@@ -238,12 +263,11 @@ class AppProvider with ChangeNotifier {
try { try {
// Initialize contacts provider with device public key to exclude self // Initialize contacts provider with device public key to exclude self
// If already initialized (from early load), this will just filter out self-contact
// This must happen before getContacts to ensure proper filtering // This must happen before getContacts to ensure proper filtering
if (!contactsProvider.isInitialized) { await contactsProvider.initialize(
await contactsProvider.initialize( devicePublicKey: connectionProvider.deviceInfo.publicKey,
devicePublicKey: connectionProvider.deviceInfo.publicKey, );
);
}
// Note: Device clock is automatically synced during connection in MeshCoreBleService // Note: Device clock is automatically synced during connection in MeshCoreBleService
// No need to sync it again here // No need to sync it again here
@@ -257,9 +281,12 @@ class AppProvider with ChangeNotifier {
// Small delay to ensure contacts are fully loaded // Small delay to ensure contacts are fully loaded
await Future.delayed(const Duration(milliseconds: 500)); await Future.delayed(const Duration(milliseconds: 500));
// Sync all channels to get channel names // Sync channels to get channel names
debugPrint('📻 [AppProvider] Syncing channels...'); // In simple mode: only sync first 5 channels for faster startup
await connectionProvider.syncChannels(); // In normal mode: sync all channels (up to device max)
final channelsToSync = _isSimpleMode ? 5 : null;
debugPrint('📻 [AppProvider] Syncing channels${_isSimpleMode ? ' (simple mode: max 5)' : ''}...');
await connectionProvider.syncChannels(maxChannels: channelsToSync);
debugPrint('✅ [AppProvider] Channel sync complete'); debugPrint('✅ [AppProvider] Channel sync complete');
// Configure the default public channel (channel 0) // Configure the default public channel (channel 0)

View File

@@ -17,10 +17,49 @@ class ContactsProvider with ChangeNotifier {
bool get isInitialized => _isInitialized; bool get isInitialized => _isInitialized;
/// Initialize and load persisted contacts at app startup
/// This loads contacts without filtering, allowing offline viewing
Future<void> initializeEarly() async {
if (_isInitialized) return;
try {
debugPrint('📦 [ContactsProvider] Early loading persisted contacts (no filtering)...');
final storedContacts = await _storageService.loadContacts();
// Add stored contacts (excluding any with all-zeros public key)
const publicChannelKey = '0000000000000000000000000000000000000000000000000000000000000000';
for (final contact in storedContacts) {
// Skip any contacts with all-zeros public key (shouldn't happen, but safety check)
if (contact.publicKeyHex == publicChannelKey) {
continue;
}
_contacts[contact.publicKeyHex] = contact;
}
_isInitialized = true;
debugPrint('✅ [ContactsProvider] Early loaded ${storedContacts.length} persisted contacts');
// Ensure public channel exists after loading
_ensurePublicChannelExists();
notifyListeners();
} catch (e) {
debugPrint('❌ [ContactsProvider] Error in early initialization: $e');
_isInitialized = true; // Mark as initialized even on error
_ensurePublicChannelExists();
}
}
/// Initialize and load persisted contacts /// Initialize and load persisted contacts
/// [devicePublicKey] - device's own public key to exclude from loaded contacts /// [devicePublicKey] - device's own public key to exclude from loaded contacts
Future<void> initialize({Uint8List? devicePublicKey}) async { Future<void> initialize({Uint8List? devicePublicKey}) async {
if (_isInitialized) return; if (_isInitialized) {
// If already initialized (from early load), just filter out self-contact
if (devicePublicKey != null) {
_removeSelfContact(devicePublicKey);
}
return;
}
try { try {
debugPrint('📦 [ContactsProvider] Loading persisted contacts...'); debugPrint('📦 [ContactsProvider] Loading persisted contacts...');
@@ -52,6 +91,18 @@ class ContactsProvider with ChangeNotifier {
} }
} }
/// Remove self-contact from loaded contacts (called after BLE connection established)
void _removeSelfContact(Uint8List devicePublicKey) {
final selfKeyHex = devicePublicKey.map((b) => b.toRadixString(16).padLeft(2, '0')).join('');
if (_contacts.containsKey(selfKeyHex)) {
final selfContact = _contacts[selfKeyHex]!;
debugPrint('🗑️ [ContactsProvider] Removing self-contact: ${selfContact.advName}');
_contacts.remove(selfKeyHex);
_persistContacts();
notifyListeners();
}
}
/// Ensure public channel always exists in the list /// Ensure public channel always exists in the list
void _ensurePublicChannelExists() { void _ensurePublicChannelExists() {
// Public channel has all-zeros public key (32 bytes = 64 hex chars) // Public channel has all-zeros public key (32 bytes = 64 hex chars)

View File

@@ -20,6 +20,8 @@ class DrawingProvider with ChangeNotifier {
// Drawing state // Drawing state
DrawingMode _drawingMode = DrawingMode.none; DrawingMode _drawingMode = DrawingMode.none;
Color _selectedColor = DrawingColors.palette[0]; Color _selectedColor = DrawingColors.palette[0];
bool _showReceivedDrawings = true;
bool _showSarMarkers = true;
// Completed drawings // Completed drawings
final List<MapDrawing> _drawings = []; final List<MapDrawing> _drawings = [];
@@ -32,7 +34,11 @@ class DrawingProvider with ChangeNotifier {
// Getters // Getters
DrawingMode get drawingMode => _drawingMode; DrawingMode get drawingMode => _drawingMode;
Color get selectedColor => _selectedColor; Color get selectedColor => _selectedColor;
List<MapDrawing> get drawings => List.unmodifiable(_drawings); bool get showReceivedDrawings => _showReceivedDrawings;
bool get showSarMarkers => _showSarMarkers;
List<MapDrawing> get drawings => _showReceivedDrawings
? List.unmodifiable(_drawings)
: List.unmodifiable(_drawings.where((d) => !d.isReceived).toList());
MapDrawing? get currentDrawing => _currentDrawing; MapDrawing? get currentDrawing => _currentDrawing;
List<LatLng> get currentLinePoints => List.unmodifiable(_currentLinePoints); List<LatLng> get currentLinePoints => List.unmodifiable(_currentLinePoints);
LatLng? get rectangleStartPoint => _rectangleStartPoint; LatLng? get rectangleStartPoint => _rectangleStartPoint;
@@ -59,6 +65,18 @@ class DrawingProvider with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
/// Toggle visibility of received drawings
void toggleReceivedDrawings() {
_showReceivedDrawings = !_showReceivedDrawings;
notifyListeners();
}
/// Toggle visibility of SAR markers
void toggleSarMarkers() {
_showSarMarkers = !_showSarMarkers;
notifyListeners();
}
/// Start drawing a line /// Start drawing a line
void startLine(LatLng point) { void startLine(LatLng point) {
if (_drawingMode != DrawingMode.line) return; if (_drawingMode != DrawingMode.line) return;

View File

@@ -31,6 +31,9 @@ class MessagesProvider with ChangeNotifier {
// Track which contact each sent message was sent to (for retry logic) // Track which contact each sent message was sent to (for retry logic)
final Map<String, Contact> _messageContactMap = {}; final Map<String, Contact> _messageContactMap = {};
// Navigation state for message highlighting/scrolling
String? _targetMessageId;
// Callback to connection provider for sending messages (set by AppProvider) // Callback to connection provider for sending messages (set by AppProvider)
Future<bool> Function({ Future<bool> Function({
required Uint8List contactPublicKey, required Uint8List contactPublicKey,
@@ -70,11 +73,24 @@ class MessagesProvider with ChangeNotifier {
bool get isInitialized => _isInitialized; bool get isInitialized => _isInitialized;
String? get targetMessageId => _targetMessageId;
/// Set localizations for notifications /// Set localizations for notifications
void setLocalizations(AppLocalizations localizations) { void setLocalizations(AppLocalizations localizations) {
_localizations = localizations; _localizations = localizations;
} }
/// Navigate to a specific message (scroll and highlight)
void navigateToMessage(String messageId) {
_targetMessageId = messageId;
notifyListeners();
}
/// Clear message navigation state
void clearMessageNavigation() {
_targetMessageId = null;
}
/// Get count of unread messages (excluding sent messages and system messages) /// Get count of unread messages (excluding sent messages and system messages)
int get unreadCount => _messages int get unreadCount => _messages
.where((m) => .where((m) =>
@@ -178,6 +194,9 @@ class MessagesProvider with ChangeNotifier {
_triggerSarNotification(finalMessage, marker); _triggerSarNotification(finalMessage, marker);
} }
} }
} else if (!finalMessage.isSentMessage && !finalMessage.isSystemMessage) {
// Trigger notification for regular messages (not SAR, not sent by user, not system)
_triggerMessageNotification(finalMessage);
} }
// Persist to storage asynchronously // Persist to storage asynchronously
@@ -296,6 +315,40 @@ class MessagesProvider with ChangeNotifier {
} }
} }
/// Trigger notification for regular message
Future<void> _triggerMessageNotification(Message message) async {
try {
// Get sender name from message
final senderName = message.senderName ?? message.senderKeyShort ?? 'Unknown';
// Determine if it's a channel message
final isChannelMessage = message.isChannelMessage;
// Get channel name if available
String? channelName;
if (isChannelMessage) {
// You could map channelIdx to channel name here if needed
// For now, use "Public" for channel 0
channelName = message.channelIdx == 0 ? 'Public' : 'Channel ${message.channelIdx}';
}
debugPrint('🔔 [MessagesProvider] Triggering message notification');
debugPrint(' Sender: $senderName');
debugPrint(' Type: ${isChannelMessage ? "Channel" : "Direct"}');
debugPrint(' Message: ${message.text.substring(0, message.text.length > 50 ? 50 : message.text.length)}...');
await _notificationService.showMessageNotification(
senderName: senderName,
messageText: message.text,
isChannelMessage: isChannelMessage,
channelName: channelName,
localizations: _localizations,
);
} catch (e) {
debugPrint('❌ [MessagesProvider] Error triggering message notification: $e');
}
}
/// Persist messages to storage (async, non-blocking) /// Persist messages to storage (async, non-blocking)
Future<void> _persistMessages() async { Future<void> _persistMessages() async {
try { try {
@@ -529,6 +582,11 @@ class MessagesProvider with ChangeNotifier {
if (sendingMessage.isSarMarker) { if (sendingMessage.isSarMarker) {
final marker = sendingMessage.toSarMarker(); final marker = sendingMessage.toSarMarker();
if (marker != null) { if (marker != null) {
debugPrint(' ✅ SAR Marker created:');
debugPrint(' marker.id: ${marker.id}');
debugPrint(' marker.notes: "${marker.notes}"');
debugPrint(' marker.type: ${marker.type}');
debugPrint(' marker.displayName: ${marker.displayName}');
_sarMarkers[marker.id] = marker; _sarMarkers[marker.id] = marker;
} }
} }

View File

@@ -8,7 +8,9 @@ import '../providers/app_provider.dart';
import '../widgets/contacts/contact_tile.dart'; import '../widgets/contacts/contact_tile.dart';
class ContactsTab extends StatefulWidget { class ContactsTab extends StatefulWidget {
const ContactsTab({super.key}); final VoidCallback? onNavigateToMap;
const ContactsTab({super.key, this.onNavigateToMap});
@override @override
State<ContactsTab> createState() => _ContactsTabState(); State<ContactsTab> createState() => _ContactsTabState();
@@ -87,6 +89,9 @@ class _ContactsTabState extends State<ContactsTab> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
final appProvider = context.watch<AppProvider>();
final isSimpleMode = appProvider.isSimpleMode;
return Consumer<ContactsProvider>( return Consumer<ContactsProvider>(
builder: (context, contactsProvider, child) { builder: (context, contactsProvider, child) {
final chatContacts = contactsProvider.chatContacts; final chatContacts = contactsProvider.chatContacts;
@@ -143,6 +148,7 @@ class _ContactsTabState extends State<ContactsTab> {
currentPosition: _currentPosition, currentPosition: _currentPosition,
calculateDistance: _calculateDistanceInMeters, calculateDistance: _calculateDistanceInMeters,
formatDistance: _formatDistance, formatDistance: _formatDistance,
onNavigateToMap: widget.onNavigateToMap,
), ),
), ),
const Divider(height: 32), const Divider(height: 32),
@@ -161,6 +167,7 @@ class _ContactsTabState extends State<ContactsTab> {
currentPosition: _currentPosition, currentPosition: _currentPosition,
calculateDistance: _calculateDistanceInMeters, calculateDistance: _calculateDistanceInMeters,
formatDistance: _formatDistance, formatDistance: _formatDistance,
onNavigateToMap: widget.onNavigateToMap,
), ),
), ),
const Divider(height: 32), const Divider(height: 32),
@@ -179,13 +186,14 @@ class _ContactsTabState extends State<ContactsTab> {
currentPosition: _currentPosition, currentPosition: _currentPosition,
calculateDistance: _calculateDistanceInMeters, calculateDistance: _calculateDistanceInMeters,
formatDistance: _formatDistance, formatDistance: _formatDistance,
onNavigateToMap: widget.onNavigateToMap,
), ),
), ),
const Divider(height: 32), const Divider(height: 32),
], ],
// Channels // Channels (hidden in simple mode)
if (channels.isNotEmpty) ...[ if (!isSimpleMode && channels.isNotEmpty) ...[
_SectionHeader( _SectionHeader(
title: l10n.channels, title: l10n.channels,
count: channels.length, count: channels.length,
@@ -197,6 +205,7 @@ class _ContactsTabState extends State<ContactsTab> {
currentPosition: _currentPosition, currentPosition: _currentPosition,
calculateDistance: _calculateDistanceInMeters, calculateDistance: _calculateDistanceInMeters,
formatDistance: _formatDistance, formatDistance: _formatDistance,
onNavigateToMap: widget.onNavigateToMap,
), ),
), ),
], ],

View File

@@ -581,13 +581,14 @@ class _HomeScreenState extends State<HomeScreen>
controller: _tabController, controller: _tabController,
children: [ children: [
MessagesTab(onNavigateToMap: () => _tabController.animateTo(2)), MessagesTab(onNavigateToMap: () => _tabController.animateTo(2)),
const ContactsTab(), ContactsTab(onNavigateToMap: () => _tabController.animateTo(2)),
MapTab( MapTab(
onFullscreenChanged: (isFullscreen) { onFullscreenChanged: (isFullscreen) {
setState(() { setState(() {
_isMapFullscreen = isFullscreen; _isMapFullscreen = isFullscreen;
}); });
}, },
onNavigateToMessages: () => _tabController.animateTo(0),
), ),
], ],
), ),
@@ -772,32 +773,35 @@ class _HomeScreenState extends State<HomeScreen>
], ],
), ),
), ),
const SizedBox(width: 8), // Settings cog - hidden in simple mode
GestureDetector( if (!context.watch<AppProvider>().isSimpleMode) ...[
onTap: () { const SizedBox(width: 8),
Navigator.push( GestureDetector(
context, onTap: () {
MaterialPageRoute( Navigator.push(
builder: (context) => const DeviceConfigScreen(), context,
), MaterialPageRoute(
); builder: (context) => const DeviceConfigScreen(),
}, ),
onLongPress: () { );
Navigator.push( },
context, onLongPress: () {
MaterialPageRoute( Navigator.push(
builder: (context) => context,
PacketLogScreen(bleService: provider.bleService), MaterialPageRoute(
), builder: (context) =>
); PacketLogScreen(bleService: provider.bleService),
}, ),
child: Container( );
width: 32, },
height: 32, child: Container(
alignment: Alignment.center, width: 32,
child: const Icon(Icons.settings, size: 18), height: 32,
alignment: Alignment.center,
child: const Icon(Icons.settings, size: 18),
),
), ),
), ],
], ],
), ),
), ),

View File

@@ -3,6 +3,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
import '../services/tile_cache_service.dart'; import '../services/tile_cache_service.dart';
import '../services/validation_service.dart'; import '../services/validation_service.dart';
import '../services/mbtiles_service.dart'; import '../services/mbtiles_service.dart';
@@ -391,6 +393,133 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
} }
} }
Future<void> _exportTiles() async {
try {
// Check if there are tiles to export
final tileCount = await widget.tileCacheService.getCachedTileCount();
if (tileCount == 0) {
_showError(AppLocalizations.of(context)!.noTilesToExport);
return;
}
if (!mounted) return;
setState(() {
_isLoading = true;
_statusMessage = AppLocalizations.of(context)!.exportingTiles;
});
// Export to temporary directory first (works on all platforms)
final tempDir = await getTemporaryDirectory();
final fileName = 'meshcore_tiles_${DateTime.now().millisecondsSinceEpoch}.fmtc';
final tempFilePath = '${tempDir.path}/$fileName';
final exportedCount = await widget.tileCacheService.exportStore(tempFilePath);
if (!mounted) return;
setState(() {
_isLoading = false;
_statusMessage = null;
});
// Share the file using share_plus (works on all platforms)
final file = File(tempFilePath);
if (await file.exists()) {
// Get the button position for iPad popover
final box = context.findRenderObject() as RenderBox?;
final sharePositionOrigin = box != null
? box.localToGlobal(Offset.zero) & box.size
: null;
final result = await SharePlus.instance.share(
ShareParams(
files: [XFile(tempFilePath)],
subject: 'MeshCore Map Tiles Export',
text: 'Exported $exportedCount map tiles',
sharePositionOrigin: sharePositionOrigin,
),
);
if (mounted) {
if (result.status == ShareResultStatus.success) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context)!.exportSuccess(exportedCount)),
backgroundColor: Colors.green,
),
);
}
}
} else {
_showError('Export file not found');
}
} catch (e) {
if (!mounted) return;
setState(() {
_isLoading = false;
_statusMessage = null;
});
_showError(AppLocalizations.of(context)!.exportFailed(e.toString()));
}
}
Future<void> _importTiles() async {
try {
// Use file picker to select import file
final result = await FilePicker.platform.pickFiles(
dialogTitle: AppLocalizations.of(context)!.selectImportFile,
type: FileType.custom,
allowedExtensions: ['fmtc'],
);
if (result == null || result.files.isEmpty) return;
final filePath = result.files.first.path;
if (filePath == null) return;
if (!mounted) return;
setState(() {
_isLoading = true;
_statusMessage = AppLocalizations.of(context)!.importingTiles;
});
// Optional: Preview stores in archive before importing
try {
final stores = await widget.tileCacheService.listArchiveStores(filePath);
debugPrint('Archive contains stores: $stores');
} catch (e) {
debugPrint('Could not list stores: $e');
}
final importResult = await widget.tileCacheService.importStore(filePath);
if (!mounted) return;
setState(() {
_isLoading = false;
_statusMessage = null;
});
await _loadCacheStats(); // Refresh stats after import
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(context)!.importSuccess(importResult['successfulStores'] as int),
),
backgroundColor: Colors.green,
),
);
}
} catch (e) {
if (!mounted) return;
setState(() {
_isLoading = false;
_statusMessage = null;
});
_showError(AppLocalizations.of(context)!.importFailed(e.toString()));
}
}
void _showError(String message) { void _showError(String message) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
@@ -424,6 +553,10 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
_buildMbtilesCard(), _buildMbtilesCard(),
const SizedBox(height: 16), const SizedBox(height: 16),
// Import/Export Cached Tiles
_buildImportExportCard(),
const SizedBox(height: 16),
// Download Region // Download Region
_buildDownloadCard(), _buildDownloadCard(),
const SizedBox(height: 16), const SizedBox(height: 16),
@@ -636,6 +769,60 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
); );
} }
Widget _buildImportExportCard() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppLocalizations.of(context)!.importExportCachedTiles,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
AppLocalizations.of(context)!.importExportDescription,
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
const SizedBox(height: 16),
// Export Section
ElevatedButton.icon(
onPressed: _isDownloading || _isLoading ? null : _exportTiles,
icon: const Icon(Icons.file_upload),
label: Text(AppLocalizations.of(context)!.exportTilesToFile),
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(48),
),
),
const SizedBox(height: 8),
Text(
AppLocalizations.of(context)!.exportNote,
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
const SizedBox(height: 16),
// Import Section
ElevatedButton.icon(
onPressed: _isDownloading || _isLoading ? null : _importTiles,
icon: const Icon(Icons.file_download),
label: Text(AppLocalizations.of(context)!.importTilesFromFile),
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(48),
),
),
const SizedBox(height: 8),
Text(
AppLocalizations.of(context)!.importNote,
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
],
),
),
);
}
Widget _buildInfoRow(String label, String value) { Widget _buildInfoRow(String label, String value) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 4), padding: const EdgeInsets.symmetric(vertical: 4),

View File

@@ -41,10 +41,12 @@ import 'map_management_screen.dart';
class MapTab extends StatefulWidget { class MapTab extends StatefulWidget {
final Function(bool)? onFullscreenChanged; final Function(bool)? onFullscreenChanged;
final VoidCallback? onNavigateToMessages;
const MapTab({ const MapTab({
super.key, super.key,
this.onFullscreenChanged, this.onFullscreenChanged,
this.onNavigateToMessages,
}); });
@override @override
@@ -96,6 +98,15 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
// Access the singleton LocationTrackingService from AppProvider // Access the singleton LocationTrackingService from AppProvider
LocationTrackingService get _locationService => LocationTrackingService(); LocationTrackingService get _locationService => LocationTrackingService();
/// Helper to compare public keys byte-by-byte
bool _publicKeysMatch(Uint8List key1, Uint8List key2) {
if (key1.length != key2.length) return false;
for (int i = 0; i < key1.length; i++) {
if (key1[i] != key2[i]) return false;
}
return true;
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -867,17 +878,17 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
builder: (context) => SarUpdateSheet( builder: (context) => SarUpdateSheet(
prePopulatedPosition: position, prePopulatedPosition: position,
allowLocationUpdate: false, // Don't allow changing to current location allowLocationUpdate: false, // Don't allow changing to current location
onSend: (sarType, position, notes, roomPublicKey, sendToChannel) async { onSend: (emoji, name, position, roomPublicKey, sendToChannel) async {
await _sendSarMessage(sarType, position, notes, roomPublicKey, sendToChannel); await _sendSarMessage(emoji, name, position, roomPublicKey, sendToChannel);
}, },
), ),
); );
} }
Future<void> _sendSarMessage( Future<void> _sendSarMessage(
SarMarkerType sarType, String emoji,
String name,
Position position, Position position,
String? notes,
Uint8List? roomPublicKey, Uint8List? roomPublicKey,
bool sendToChannel, bool sendToChannel,
) async { ) async {
@@ -907,27 +918,50 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
} }
try { try {
// Format: S:<emoji>:<latitude>,<longitude> // Format: S:<emoji>:<latitude>,<longitude>:<name>
final sarMessage = 'S:${sarType.emoji}:${position.latitude},${position.longitude}'; // Round coordinates to 5 decimal places (~1m accuracy) since most GPS is only that accurate
final sarMessage = 'S:$emoji:${position.latitude.toStringAsFixed(5)},${position.longitude.toStringAsFixed(5)}:$name';
// Add notes if provided
final fullMessage = notes != null && notes.isNotEmpty
? '$sarMessage $notes'
: sarMessage;
if (sendToChannel) { if (sendToChannel) {
// Create message ID
final messageId = '${DateTime.now().millisecondsSinceEpoch}_channel_sent';
final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
// Get current device's public key (first 6 bytes)
final devicePublicKey = connectionProvider.deviceInfo.publicKey;
final senderPublicKeyPrefix = devicePublicKey?.sublist(0, 6);
// Create sent message object
final sentMessage = Message(
id: messageId,
messageType: MessageType.channel,
senderPublicKeyPrefix: senderPublicKeyPrefix,
pathLen: 0,
textType: MessageTextType.plain,
senderTimestamp: timestamp,
text: sarMessage,
receivedAt: DateTime.now(),
deliveryStatus: MessageDeliveryStatus.sending,
channelIdx: 0,
// SAR marker data is automatically added by SarMessageParser.enhanceMessage in MessagesProvider
);
// Add to messages list with "sending" status
messagesProvider.addSentMessage(sentMessage);
// Send to public channel (ephemeral, over-the-air only) // Send to public channel (ephemeral, over-the-air only)
await connectionProvider.sendChannelMessage( await connectionProvider.sendChannelMessage(
channelIdx: 0, channelIdx: 0,
text: fullMessage, text: sarMessage,
messageId: messageId,
); );
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( const SnackBar(
content: Text('${sarType.displayName} marker broadcast to public channel'), content: Text('SAR marker broadcast to public channel'),
backgroundColor: Colors.orange, backgroundColor: Colors.orange,
duration: const Duration(seconds: 2), duration: Duration(seconds: 2),
), ),
); );
} else { } else {
@@ -939,7 +973,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
final devicePublicKey = connectionProvider.deviceInfo.publicKey; final devicePublicKey = connectionProvider.deviceInfo.publicKey;
final senderPublicKeyPrefix = devicePublicKey?.sublist(0, 6); final senderPublicKeyPrefix = devicePublicKey?.sublist(0, 6);
// Create sent message object // Create sent message object with recipient public key for retry support
final sentMessage = Message( final sentMessage = Message(
id: messageId, id: messageId,
messageType: MessageType.contact, messageType: MessageType.contact,
@@ -947,20 +981,29 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
pathLen: 0, pathLen: 0,
textType: MessageTextType.plain, textType: MessageTextType.plain,
senderTimestamp: timestamp, senderTimestamp: timestamp,
text: fullMessage, text: sarMessage,
receivedAt: DateTime.now(), receivedAt: DateTime.now(),
deliveryStatus: MessageDeliveryStatus.sending, deliveryStatus: MessageDeliveryStatus.sending,
recipientPublicKey: roomPublicKey, // Store recipient for retry
// SAR marker data is automatically added by SarMessageParser.enhanceMessage in MessagesProvider // SAR marker data is automatically added by SarMessageParser.enhanceMessage in MessagesProvider
); );
// Add to messages list with "sending" status // Add to messages list with "sending" status
messagesProvider.addSentMessage(sentMessage); messagesProvider.addSentMessage(sentMessage);
// Look up the room contact for path logging
final contactsProvider = context.read<ContactsProvider>();
final roomContact = contactsProvider.contacts.where((c) {
return c.publicKey.length >= roomPublicKey!.length &&
_publicKeysMatch(c.publicKey, roomPublicKey);
}).firstOrNull;
// Send SAR message to selected room (persisted and immutable) // Send SAR message to selected room (persisted and immutable)
final sentSuccessfully = await connectionProvider.sendTextMessage( final sentSuccessfully = await connectionProvider.sendTextMessage(
contactPublicKey: roomPublicKey!, contactPublicKey: roomPublicKey!,
text: fullMessage, text: sarMessage,
messageId: messageId, // Pass message ID so it can be tracked messageId: messageId, // Pass message ID so it can be tracked
contact: roomContact, // Include contact for path status logging
); );
if (!sentSuccessfully) { if (!sentSuccessfully) {
@@ -970,10 +1013,10 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( const SnackBar(
content: Text('${sarType.displayName} marker sent to room'), content: Text('SAR marker sent to room'),
backgroundColor: Colors.green, backgroundColor: Colors.green,
duration: const Duration(seconds: 2), duration: Duration(seconds: 2),
), ),
); );
} }
@@ -991,10 +1034,17 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); // Required for AutomaticKeepAliveClientMixin super.build(context); // Required for AutomaticKeepAliveClientMixin
final appProvider = context.watch<AppProvider>();
final isSimpleMode = appProvider.isSimpleMode;
return Consumer3<ContactsProvider, MessagesProvider, DrawingProvider>( return Consumer3<ContactsProvider, MessagesProvider, DrawingProvider>(
builder: (context, contactsProvider, messagesProvider, drawingProvider, child) { builder: (context, contactsProvider, messagesProvider, drawingProvider, child) {
final contactsWithLocation = contactsProvider.contactsWithLocation; final contactsWithLocation = contactsProvider.contactsWithLocation;
final sarMarkers = messagesProvider.sarMarkers; // Filter SAR markers based on visibility toggle
final allSarMarkers = messagesProvider.sarMarkers;
final sarMarkers = drawingProvider.showSarMarkers
? allSarMarkers
: <SarMarker>[];
final center = _calculateCenter(contactsWithLocation, sarMarkers); final center = _calculateCenter(contactsWithLocation, sarMarkers);
return Stack( return Stack(
@@ -1167,6 +1217,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
DrawingLayer( DrawingLayer(
drawings: drawingProvider.drawings, drawings: drawingProvider.drawings,
previewDrawing: drawingProvider.getPreviewDrawing(), previewDrawing: drawingProvider.getPreviewDrawing(),
isSimpleMode: isSimpleMode,
), ),
MarkerLayer( MarkerLayer(
markers: [ markers: [
@@ -1191,12 +1242,9 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
context: context, context: context,
mapRotation: _getMapRotation(), mapRotation: _getMapRotation(),
onTap: (marker) { onTap: (marker) {
_showDetailedCompassWithSarMarker( // Navigate to the corresponding message in Messages tab
context, messagesProvider.navigateToMessage(marker.id);
contactsProvider.contactsWithLocation, widget.onNavigateToMessages?.call();
messagesProvider.sarMarkers,
marker,
);
}, },
), ),
// User location marker // User location marker
@@ -1284,9 +1332,17 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
DrawingMarkersLayer( DrawingMarkersLayer(
drawings: drawingProvider.drawings, drawings: drawingProvider.drawings,
showDeleteButtons: drawingProvider.isDrawing, showDeleteButtons: drawingProvider.isDrawing,
isSimpleMode: isSimpleMode,
onDeleteDrawing: (drawingId) { onDeleteDrawing: (drawingId) {
drawingProvider.removeDrawing(drawingId); drawingProvider.removeDrawing(drawingId);
}, },
onTapDrawing: (drawing) {
// Navigate to the corresponding message in Messages tab
if (drawing.messageId != null) {
messagesProvider.navigateToMessage(drawing.messageId!);
widget.onNavigateToMessages?.call();
}
},
), ),
], ],
), ),
@@ -1406,6 +1462,21 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
child: const Icon(Icons.layers), child: const Icon(Icons.layers),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
// In simple mode: show fullscreen button directly
// In normal mode: show options menu (which includes fullscreen)
if (context.watch<AppProvider>().isSimpleMode)
FloatingActionButton.small(
heroTag: 'fullscreen_toggle',
onPressed: () {
setState(() {
_isFullscreen = !_isFullscreen;
});
_saveSettings();
widget.onFullscreenChanged?.call(_isFullscreen);
},
child: Icon(_isFullscreen ? Icons.fullscreen_exit : Icons.fullscreen),
)
else
FloatingActionButton.small( FloatingActionButton.small(
heroTag: 'options_menu', heroTag: 'options_menu',
onPressed: () => _showOptionsMenu(context), onPressed: () => _showOptionsMenu(context),

View File

@@ -1,4 +1,3 @@
import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -8,12 +7,14 @@ import '../providers/contacts_provider.dart';
import '../providers/map_provider.dart'; import '../providers/map_provider.dart';
import '../providers/connection_provider.dart'; import '../providers/connection_provider.dart';
import '../models/message.dart'; import '../models/message.dart';
import '../models/contact.dart';
import '../models/sar_marker.dart'; import '../models/sar_marker.dart';
import '../widgets/messages/sar_update_sheet.dart'; import '../widgets/messages/sar_update_sheet.dart';
import '../widgets/messages/recipient_selector_sheet.dart';
import '../widgets/contacts/direct_message_sheet.dart'; import '../widgets/contacts/direct_message_sheet.dart';
import '../services/message_destination_preferences.dart';
import '../utils/toast_logger.dart'; import '../utils/toast_logger.dart';
import '../l10n/app_localizations.dart'; import '../l10n/app_localizations.dart';
import '../utils/sar_marker_extensions.dart';
import '../utils/message_extensions.dart'; import '../utils/message_extensions.dart';
class MessagesTab extends StatefulWidget { class MessagesTab extends StatefulWidget {
@@ -28,8 +29,14 @@ class MessagesTab extends StatefulWidget {
class _MessagesTabState extends State<MessagesTab> { class _MessagesTabState extends State<MessagesTab> {
final TextEditingController _textController = TextEditingController(); final TextEditingController _textController = TextEditingController();
final FocusNode _focusNode = FocusNode(); final FocusNode _focusNode = FocusNode();
final ScrollController _scrollController = ScrollController();
int _characterCount = 0; int _characterCount = 0;
static const int _maxCharacters = 160; static const int _maxCharacters = 160;
String? _highlightedMessageId;
// Message destination state
String _destinationType = MessageDestinationPreferences.destinationTypeChannel;
Contact? _selectedRecipient;
/// Helper method to compare two public keys for equality /// Helper method to compare two public keys for equality
bool _publicKeysMatch(Uint8List key1, Uint8List key2) { bool _publicKeysMatch(Uint8List key1, Uint8List key2) {
@@ -44,9 +51,21 @@ class _MessagesTabState extends State<MessagesTab> {
void initState() { void initState() {
super.initState(); super.initState();
_textController.addListener(_updateCharacterCount); _textController.addListener(_updateCharacterCount);
// Load saved message destination
_loadSavedDestination();
// Mark all messages as read when tab is opened // Mark all messages as read when tab is opened
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<MessagesProvider>().markAllAsRead(); context.read<MessagesProvider>().markAllAsRead();
_checkForNavigationRequest();
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Check for navigation request whenever dependencies change
WidgetsBinding.instance.addPostFrameCallback((_) {
_checkForNavigationRequest();
}); });
} }
@@ -54,21 +73,183 @@ class _MessagesTabState extends State<MessagesTab> {
void dispose() { void dispose() {
_textController.dispose(); _textController.dispose();
_focusNode.dispose(); _focusNode.dispose();
_scrollController.dispose();
super.dispose(); super.dispose();
} }
void _checkForNavigationRequest() {
final messagesProvider = context.read<MessagesProvider>();
final targetMessageId = messagesProvider.targetMessageId;
if (targetMessageId != null) {
_scrollToMessage(targetMessageId);
messagesProvider.clearMessageNavigation();
}
}
void _scrollToMessage(String messageId) {
final messagesProvider = context.read<MessagesProvider>();
final messages = _getFilteredMessages(messagesProvider);
final messageIndex = messages.indexWhere((m) => m.id == messageId);
if (messageIndex != -1 && _scrollController.hasClients) {
// Calculate position - accounting for reverse list
final itemHeight = 80.0; // Approximate height of a message bubble
final targetOffset = messageIndex * itemHeight;
// Scroll to the message
_scrollController.animateTo(
targetOffset,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
// Highlight the message briefly
setState(() {
_highlightedMessageId = messageId;
});
// Clear highlight after 2 seconds
Future.delayed(const Duration(seconds: 2), () {
if (mounted) {
setState(() {
_highlightedMessageId = null;
});
}
});
}
}
void _updateCharacterCount() { void _updateCharacterCount() {
setState(() { setState(() {
_characterCount = _textController.text.length; _characterCount = _textController.text.length;
}); });
} }
/// Load saved message destination from preferences
Future<void> _loadSavedDestination() async {
final savedDestination = await MessageDestinationPreferences.getDestination();
if (savedDestination == null || !mounted) {
// Default to public channel
return;
}
final type = savedDestination['type']!;
final publicKey = savedDestination['publicKey'];
setState(() {
_destinationType = type;
});
// If it's a contact or room, try to find it in the contacts list
if (publicKey != null && mounted) {
final contactsProvider = context.read<ContactsProvider>();
final contact = contactsProvider.contacts.where((c) {
return c.publicKeyHex == publicKey;
}).firstOrNull;
if (contact != null) {
setState(() {
_selectedRecipient = contact;
});
} else {
// Contact/room not found, fallback to public channel
debugPrint(
'⚠️ [MessagesTab] Saved recipient not found, falling back to public channel',
);
setState(() {
_destinationType = MessageDestinationPreferences.destinationTypeChannel;
_selectedRecipient = null;
});
await MessageDestinationPreferences.clearDestination();
}
}
}
/// Show recipient selector bottom sheet
void _showRecipientSelector() {
final contactsProvider = context.read<ContactsProvider>();
// Filter contacts by type
final contacts = contactsProvider.contacts
.where((c) => c.type == ContactType.chat)
.toList();
final rooms = contactsProvider.contacts
.where((c) => c.type == ContactType.room)
.toList();
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => RecipientSelectorSheet(
contacts: contacts,
rooms: rooms,
currentDestinationType: _destinationType,
currentRecipientPublicKey: _selectedRecipient?.publicKeyHex,
onSelect: _onRecipientSelected,
),
);
}
/// Handle recipient selection
Future<void> _onRecipientSelected(String type, Contact? recipient) async {
// Get display name before async gap
final recipientName = type == MessageDestinationPreferences.destinationTypeChannel
? AppLocalizations.of(context)!.publicChannel
: (recipient?.displayName ?? recipient?.advName ?? 'Unknown');
setState(() {
_destinationType = type;
_selectedRecipient = recipient;
});
// Save to preferences
await MessageDestinationPreferences.setDestination(
type,
recipientPublicKey: recipient?.publicKeyHex,
);
// Show confirmation toast
if (!mounted) return;
ToastLogger.success(
context,
'Messages will be sent to: $recipientName',
);
}
/// Get icon for current destination type
IconData _getDestinationIcon() {
if (_destinationType == MessageDestinationPreferences.destinationTypeChannel) {
return Icons.public;
} else if (_destinationType == MessageDestinationPreferences.destinationTypeRoom) {
return Icons.meeting_room;
} else {
return Icons.person;
}
}
/// Get tooltip for destination button
String _getDestinationTooltip() {
final l10n = AppLocalizations.of(context)!;
if (_destinationType == MessageDestinationPreferences.destinationTypeChannel) {
return '${l10n.publicChannel} (tap to change)';
} else if (_selectedRecipient != null) {
final recipientName = _selectedRecipient!.displayName ?? _selectedRecipient!.advName;
return '$recipientName (tap to change)';
}
return 'Select recipient';
}
Future<void> _sendMessage() async { Future<void> _sendMessage() async {
final text = _textController.text.trim(); final text = _textController.text.trim();
if (text.isEmpty) return; if (text.isEmpty) return;
final connectionProvider = context.read<ConnectionProvider>(); final connectionProvider = context.read<ConnectionProvider>();
final messagesProvider = context.read<MessagesProvider>(); final messagesProvider = context.read<MessagesProvider>();
final contactsProvider = context.read<ContactsProvider>();
if (!connectionProvider.deviceInfo.isConnected) { if (!connectionProvider.deviceInfo.isConnected) {
if (!mounted) return; if (!mounted) return;
@@ -77,37 +258,23 @@ class _MessagesTabState extends State<MessagesTab> {
} }
try { try {
// Create message ID // Check destination type and send accordingly
final messageId = '${DateTime.now().millisecondsSinceEpoch}_channel_sent'; if (_destinationType == MessageDestinationPreferences.destinationTypeChannel) {
final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000; // Send to public channel
await _sendToChannel(text, connectionProvider, messagesProvider);
// Get current device's public key (first 6 bytes) } else if (_selectedRecipient != null) {
final devicePublicKey = connectionProvider.deviceInfo.publicKey; // Send to contact or room
final senderPublicKeyPrefix = devicePublicKey?.sublist(0, 6); await _sendToRecipient(
text,
// Create sent message object connectionProvider,
final sentMessage = Message( messagesProvider,
id: messageId, contactsProvider,
messageType: MessageType.channel, );
senderPublicKeyPrefix: senderPublicKeyPrefix, } else {
pathLen: 0, // Fallback to public channel if no recipient selected
textType: MessageTextType.plain, debugPrint('⚠️ [MessagesTab] No recipient selected, falling back to channel');
senderTimestamp: timestamp, await _sendToChannel(text, connectionProvider, messagesProvider);
text: text, }
receivedAt: DateTime.now(),
deliveryStatus: MessageDeliveryStatus.sending,
channelIdx: 0,
);
// Add to messages list with "sending" status
messagesProvider.addSentMessage(sentMessage);
// Send to public channel (channel 0)
await connectionProvider.sendChannelMessage(
channelIdx: 0,
text: text,
messageId: messageId,
);
_textController.clear(); _textController.clear();
_focusNode.unfocus(); _focusNode.unfocus();
@@ -119,17 +286,104 @@ class _MessagesTabState extends State<MessagesTab> {
} }
} }
/// Send message to public channel
Future<void> _sendToChannel(
String text,
ConnectionProvider connectionProvider,
MessagesProvider messagesProvider,
) async {
// Create message ID
final messageId = '${DateTime.now().millisecondsSinceEpoch}_channel_sent';
final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
// Get current device's public key (first 6 bytes)
final devicePublicKey = connectionProvider.deviceInfo.publicKey;
final senderPublicKeyPrefix = devicePublicKey?.sublist(0, 6);
// Create sent message object
final sentMessage = Message(
id: messageId,
messageType: MessageType.channel,
senderPublicKeyPrefix: senderPublicKeyPrefix,
pathLen: 0,
textType: MessageTextType.plain,
senderTimestamp: timestamp,
text: text,
receivedAt: DateTime.now(),
deliveryStatus: MessageDeliveryStatus.sending,
channelIdx: 0,
);
// Add to messages list with "sending" status
messagesProvider.addSentMessage(sentMessage);
// Send to public channel (channel 0)
await connectionProvider.sendChannelMessage(
channelIdx: 0,
text: text,
messageId: messageId,
);
}
/// Send message to contact or room
Future<void> _sendToRecipient(
String text,
ConnectionProvider connectionProvider,
MessagesProvider messagesProvider,
ContactsProvider contactsProvider,
) async {
if (_selectedRecipient == null) return;
// Create message ID
final messageId = '${DateTime.now().millisecondsSinceEpoch}_sent';
final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
// Get current device's public key (first 6 bytes)
final devicePublicKey = connectionProvider.deviceInfo.publicKey;
final senderPublicKeyPrefix = devicePublicKey?.sublist(0, 6);
// Create sent message object with recipient public key for retry support
final sentMessage = Message(
id: messageId,
messageType: MessageType.contact,
senderPublicKeyPrefix: senderPublicKeyPrefix,
pathLen: 0,
textType: MessageTextType.plain,
senderTimestamp: timestamp,
text: text,
receivedAt: DateTime.now(),
deliveryStatus: MessageDeliveryStatus.sending,
recipientPublicKey: _selectedRecipient!.publicKey,
);
// Add to messages list with "sending" status
messagesProvider.addSentMessage(sentMessage);
// Send message to selected recipient
final sentSuccessfully = await connectionProvider.sendTextMessage(
contactPublicKey: _selectedRecipient!.publicKey,
text: text,
messageId: messageId,
contact: _selectedRecipient,
);
if (!sentSuccessfully) {
// Mark message as failed if sending failed
messagesProvider.markMessageFailed(messageId);
}
}
void _showSarDialog() { void _showSarDialog() {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
builder: (context) => SarUpdateSheet( builder: (context) => SarUpdateSheet(
onSend: (sarType, position, notes, roomPublicKey, sendToChannel) async { onSend: (emoji, name, position, roomPublicKey, sendToChannel) async {
await _sendSarMessage( await _sendSarMessage(
sarType, emoji,
name,
position, position,
notes,
roomPublicKey, roomPublicKey,
sendToChannel, sendToChannel,
); );
@@ -139,9 +393,9 @@ class _MessagesTabState extends State<MessagesTab> {
} }
Future<void> _sendSarMessage( Future<void> _sendSarMessage(
SarMarkerType sarType, String emoji,
String name,
Position position, Position position,
String? notes,
Uint8List? roomPublicKey, Uint8List? roomPublicKey,
bool sendToChannel, bool sendToChannel,
) async { ) async {
@@ -161,14 +415,10 @@ class _MessagesTabState extends State<MessagesTab> {
} }
try { try {
// Format: S:<emoji>:<latitude>,<longitude> // Format: S:<emoji>:<latitude>,<longitude>:<name>
// Round coordinates to 5 decimal places (~1m accuracy) since most GPS is only that accurate
final sarMessage = final sarMessage =
'S:${sarType.emoji}:${position.latitude},${position.longitude}'; 'S:$emoji:${position.latitude.toStringAsFixed(5)},${position.longitude.toStringAsFixed(5)}:$name';
// Add notes if provided
final fullMessage = notes != null && notes.isNotEmpty
? '$sarMessage $notes'
: sarMessage;
if (sendToChannel) { if (sendToChannel) {
// Create message ID // Create message ID
@@ -187,7 +437,7 @@ class _MessagesTabState extends State<MessagesTab> {
pathLen: 0, pathLen: 0,
textType: MessageTextType.plain, textType: MessageTextType.plain,
senderTimestamp: timestamp, senderTimestamp: timestamp,
text: fullMessage, text: sarMessage,
receivedAt: DateTime.now(), receivedAt: DateTime.now(),
deliveryStatus: MessageDeliveryStatus.sending, deliveryStatus: MessageDeliveryStatus.sending,
channelIdx: 0, channelIdx: 0,
@@ -200,14 +450,14 @@ class _MessagesTabState extends State<MessagesTab> {
// Send to public channel (ephemeral, over-the-air only) // Send to public channel (ephemeral, over-the-air only)
await connectionProvider.sendChannelMessage( await connectionProvider.sendChannelMessage(
channelIdx: 0, channelIdx: 0,
text: fullMessage, text: sarMessage,
messageId: messageId, messageId: messageId,
); );
if (!mounted) return; if (!mounted) return;
ToastLogger.success( ToastLogger.success(
context, context,
'${sarType.getLocalizedName(context)} marker broadcast to public channel', 'SAR marker broadcast to public channel',
); );
} else { } else {
// Create message ID // Create message ID
@@ -226,7 +476,7 @@ class _MessagesTabState extends State<MessagesTab> {
pathLen: 0, pathLen: 0,
textType: MessageTextType.plain, textType: MessageTextType.plain,
senderTimestamp: timestamp, senderTimestamp: timestamp,
text: fullMessage, text: sarMessage,
receivedAt: DateTime.now(), receivedAt: DateTime.now(),
deliveryStatus: MessageDeliveryStatus.sending, deliveryStatus: MessageDeliveryStatus.sending,
recipientPublicKey: roomPublicKey, // Store recipient for retry recipientPublicKey: roomPublicKey, // Store recipient for retry
@@ -246,7 +496,7 @@ class _MessagesTabState extends State<MessagesTab> {
// Send SAR message to selected room (persisted and immutable) // Send SAR message to selected room (persisted and immutable)
final sentSuccessfully = await connectionProvider.sendTextMessage( final sentSuccessfully = await connectionProvider.sendTextMessage(
contactPublicKey: roomPublicKey!, contactPublicKey: roomPublicKey!,
text: fullMessage, text: sarMessage,
messageId: messageId, // Pass message ID so it can be tracked messageId: messageId, // Pass message ID so it can be tracked
contact: roomContact, // Include contact for path status logging contact: roomContact, // Include contact for path status logging
); );
@@ -259,7 +509,7 @@ class _MessagesTabState extends State<MessagesTab> {
if (!mounted) return; if (!mounted) return;
ToastLogger.success( ToastLogger.success(
context, context,
'${sarType.getLocalizedName(context)} marker sent to room', 'SAR marker sent to room',
); );
} }
} catch (e) { } catch (e) {
@@ -359,11 +609,13 @@ class _MessagesTabState extends State<MessagesTab> {
), ),
) )
: ListView.builder( : ListView.builder(
controller: _scrollController,
reverse: true, reverse: true,
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
itemCount: messages.length, itemCount: messages.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final message = messages[index]; final message = messages[index];
final isHighlighted = message.id == _highlightedMessageId;
// Display system messages with minimal styling // Display system messages with minimal styling
if (message.isSystemMessage) { if (message.isSystemMessage) {
@@ -372,6 +624,7 @@ class _MessagesTabState extends State<MessagesTab> {
return _MessageBubble( return _MessageBubble(
message: message, message: message,
isHighlighted: isHighlighted,
onTap: onTap:
message.isSarMarker && message.isSarMarker &&
message.sarGpsCoordinates != null message.sarGpsCoordinates != null
@@ -420,7 +673,22 @@ class _MessagesTabState extends State<MessagesTab> {
).colorScheme.onPrimaryContainer, ).colorScheme.onPrimaryContainer,
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 4),
// Destination switcher button
IconButton(
icon: Icon(_getDestinationIcon()),
tooltip: _getDestinationTooltip(),
onPressed: _showRecipientSelector,
style: IconButton.styleFrom(
backgroundColor: _destinationType == MessageDestinationPreferences.destinationTypeChannel
? Theme.of(context).colorScheme.surfaceContainerHighest
: Theme.of(context).colorScheme.secondaryContainer,
foregroundColor: _destinationType == MessageDestinationPreferences.destinationTypeChannel
? Theme.of(context).colorScheme.onSurface
: Theme.of(context).colorScheme.onSecondaryContainer,
),
),
const SizedBox(width: 4),
// Text field with embedded send button // Text field with embedded send button
Expanded( Expanded(
child: TextField( child: TextField(
@@ -431,7 +699,9 @@ class _MessagesTabState extends State<MessagesTab> {
maxLengthEnforcement: MaxLengthEnforcement.enforced, maxLengthEnforcement: MaxLengthEnforcement.enforced,
style: const TextStyle(fontSize: 14), style: const TextStyle(fontSize: 14),
decoration: InputDecoration( decoration: InputDecoration(
hintText: AppLocalizations.of(context)!.typeYourMessage, hintText: AppLocalizations.of(
context,
)!.typeYourMessage,
hintStyle: const TextStyle(fontSize: 14), hintStyle: const TextStyle(fontSize: 14),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(24), borderRadius: BorderRadius.circular(24),
@@ -448,7 +718,9 @@ class _MessagesTabState extends State<MessagesTab> {
fontSize: 10, fontSize: 10,
color: _characterCount > _maxCharacters * 0.9 color: _characterCount > _maxCharacters * 0.9
? Colors.orange ? Colors.orange
: Theme.of(context).textTheme.bodySmall?.color, : Theme.of(
context,
).textTheme.bodySmall?.color,
), ),
suffixIcon: IconButton( suffixIcon: IconButton(
icon: Icon( icon: Icon(
@@ -481,8 +753,13 @@ class _MessagesTabState extends State<MessagesTab> {
class _MessageBubble extends StatelessWidget { class _MessageBubble extends StatelessWidget {
final Message message; final Message message;
final VoidCallback? onTap; final VoidCallback? onTap;
final bool isHighlighted;
const _MessageBubble({required this.message, this.onTap}); const _MessageBubble({
required this.message,
this.onTap,
this.isHighlighted = false,
});
/// Helper method to compare two public keys for equality /// Helper method to compare two public keys for equality
bool _publicKeysMatch(Uint8List key1, Uint8List key2) { bool _publicKeysMatch(Uint8List key1, Uint8List key2) {
@@ -604,11 +881,11 @@ class _MessageBubble extends StatelessWidget {
// Copy text option // Copy text option
ListTile( ListTile(
leading: const Icon(Icons.copy), leading: const Icon(Icons.copy),
title: const Text('Copy text'), title: Text(AppLocalizations.of(context)!.copyText),
onTap: () { onTap: () {
Clipboard.setData(ClipboardData(text: message.text)); Clipboard.setData(ClipboardData(text: message.text));
Navigator.pop(context); Navigator.pop(context);
ToastLogger.success(context, 'Text copied to clipboard'); ToastLogger.success(context, AppLocalizations.of(context)!.textCopiedToClipboard);
}, },
), ),
// Delete message option // Delete message option
@@ -672,8 +949,8 @@ class _MessageBubble extends StatelessWidget {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text('Delete message'), title: Text(l10n.deleteMessage),
content: const Text('Are you sure you want to delete this message?'), content: Text(l10n.deleteMessageConfirmation),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
@@ -684,10 +961,10 @@ class _MessageBubble extends StatelessWidget {
final messagesProvider = context.read<MessagesProvider>(); final messagesProvider = context.read<MessagesProvider>();
messagesProvider.deleteMessage(message.id); messagesProvider.deleteMessage(message.id);
Navigator.pop(context); Navigator.pop(context);
ToastLogger.info(context, 'Message deleted'); ToastLogger.info(context, l10n.messageDeleted);
}, },
style: TextButton.styleFrom(foregroundColor: Colors.red), style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('Delete'), child: Text(l10n.delete),
), ),
], ],
), ),
@@ -820,39 +1097,55 @@ class _MessageBubble extends StatelessWidget {
margin: const EdgeInsets.only(bottom: 8), margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isSarMarker color: isHighlighted
? _getSarMarkerColor(context, isDarkMode) ? Theme.of(context).colorScheme.primaryContainer
: _getMessageBubbleColor(context, isOwnMessage, isDarkMode), : isSarMarker
? _getSarMarkerColor(context, isDarkMode)
: _getMessageBubbleColor(context, isOwnMessage, isDarkMode),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: isSarMarker border: isHighlighted
? Border.all( ? Border.all(
color: _getSarMarkerBorderColor(context, isDarkMode), color: Theme.of(context).colorScheme.primary,
width: 2, width: 3,
) )
: isOwnMessage : isSarMarker
? Border.all( ? Border.all(
color: Theme.of( color: _getSarMarkerBorderColor(context, isDarkMode),
context, width: 2,
).colorScheme.primary.withValues(alpha: 0.3), )
width: 1.5, : isOwnMessage
) ? Border.all(
: !message.isRead && color: Theme.of(
!message.isSentMessage && context,
!message.isSystemMessage ).colorScheme.primary.withValues(alpha: 0.3),
? Border.all(color: Colors.blue, width: 1.5) width: 1.5,
: null, )
boxShadow: isSarMarker : !message.isRead &&
!message.isSentMessage &&
!message.isSystemMessage
? Border.all(color: Colors.blue, width: 1.5)
: null,
boxShadow: isHighlighted
? [ ? [
BoxShadow( BoxShadow(
color: _getSarMarkerBorderColor( color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.5),
context, blurRadius: 12,
isDarkMode, spreadRadius: 2,
).withValues(alpha: 0.3),
blurRadius: 8,
offset: const Offset(0, 2), offset: const Offset(0, 2),
), ),
] ]
: null, : isSarMarker
? [
BoxShadow(
color: _getSarMarkerBorderColor(
context,
isDarkMode,
).withValues(alpha: 0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
]
: null,
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -968,7 +1261,8 @@ class _MessageBubble extends StatelessWidget {
Row( Row(
children: [ children: [
Text( Text(
message.sarMarkerType!.emoji, // Use custom emoji if available (for unknown types), otherwise use type emoji
message.sarCustomEmoji ?? message.sarMarkerType!.emoji,
style: const TextStyle(fontSize: 28), style: const TextStyle(fontSize: 28),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
@@ -977,7 +1271,10 @@ class _MessageBubble extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
message.sarMarkerType!.getLocalizedName(context), // Show template name (sarNotes) if available, otherwise show localized type name
message.sarNotes != null && message.sarNotes!.isNotEmpty
? message.sarNotes!
: message.sarMarkerType!.getLocalizedName(context),
style: Theme.of(context).textTheme.titleSmall style: Theme.of(context).textTheme.titleSmall
?.copyWith(fontWeight: FontWeight.bold), ?.copyWith(fontWeight: FontWeight.bold),
), ),
@@ -997,23 +1294,6 @@ class _MessageBubble extends StatelessWidget {
), ),
], ],
), ),
// Display SAR notes/message if present
if (message.sarNotes != null && message.sarNotes!.isNotEmpty) ...[
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Theme.of(
context,
).colorScheme.surfaceVariant.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(8),
),
child: Text(
message.sarNotes!,
style: Theme.of(context).textTheme.bodySmall,
),
),
],
] ]
// Regular message content // Regular message content
else else

View File

@@ -0,0 +1,450 @@
import 'package:flutter/material.dart';
import '../models/sar_template.dart';
import '../services/sar_template_service.dart';
import '../widgets/sar/sar_template_edit_dialog.dart';
import '../l10n/app_localizations.dart';
/// Screen for managing SAR templates
class SarTemplateManagementScreen extends StatefulWidget {
const SarTemplateManagementScreen({super.key});
@override
State<SarTemplateManagementScreen> createState() => _SarTemplateManagementScreenState();
}
class _SarTemplateManagementScreenState extends State<SarTemplateManagementScreen> {
final SarTemplateService _templateService = SarTemplateService();
bool _isLoading = false;
@override
void initState() {
super.initState();
_initializeService();
}
Future<void> _initializeService() async {
if (!_templateService.isInitialized) {
setState(() => _isLoading = true);
await _templateService.initialize();
if (mounted) {
setState(() => _isLoading = false);
}
}
}
Future<void> _addTemplate() async {
await showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => SarTemplateEditDialog(
onSave: (template) async {
await _templateService.addTemplate(template);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context)!.templateAdded),
backgroundColor: Colors.green,
),
);
}
},
),
);
}
Future<void> _editTemplate(SarTemplate template) async {
await showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => SarTemplateEditDialog(
template: template,
onSave: (updatedTemplate) async {
await _templateService.updateTemplate(template.id, updatedTemplate);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context)!.templateUpdated),
backgroundColor: Colors.green,
),
);
}
},
),
);
}
Future<void> _deleteTemplate(SarTemplate template) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(AppLocalizations.of(context)!.deleteTemplate),
content: Text(
AppLocalizations.of(context)!.deleteTemplateConfirmation(template.name),
),
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) {
await _templateService.deleteTemplate(template.id);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context)!.templateDeleted),
backgroundColor: Colors.orange,
),
);
}
}
}
Future<void> _importFromClipboard() async {
setState(() => _isLoading = true);
try {
final importedCount = await _templateService.importFromClipboard();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context)!.templatesImported(importedCount)),
backgroundColor: importedCount > 0 ? Colors.green : Colors.orange,
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context)!.importFailed(e.toString())),
backgroundColor: Colors.red,
),
);
}
} finally {
if (mounted) {
setState(() => _isLoading = false);
}
}
}
Future<void> _exportToClipboard() async {
try {
await _templateService.exportToClipboard();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(context)!.templatesExported(_templateService.templateCount),
),
backgroundColor: Colors.green,
action: SnackBarAction(
label: AppLocalizations.of(context)!.ok,
textColor: Colors.white,
onPressed: () {},
),
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context)!.exportFailed(e.toString())),
backgroundColor: Colors.red,
),
);
}
}
}
Future<void> _resetToDefaults() async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(AppLocalizations.of(context)!.resetToDefaults),
content: Text(AppLocalizations.of(context)!.resetToDefaultsConfirmation),
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)!.reset),
),
],
),
);
if (confirmed == true) {
await _templateService.resetToDefaults();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context)!.resetComplete),
backgroundColor: Colors.green,
),
);
}
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: Text(l10n.sarTemplates),
actions: [
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert),
tooltip: 'More options',
onSelected: (value) {
switch (value) {
case 'import':
_importFromClipboard();
break;
case 'export':
_exportToClipboard();
break;
case 'reset':
_resetToDefaults();
break;
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'import',
child: ListTile(
leading: const Icon(Icons.download),
title: Text(l10n.importFromClipboard),
contentPadding: EdgeInsets.zero,
),
),
PopupMenuItem(
value: 'export',
child: ListTile(
leading: const Icon(Icons.upload),
title: Text(l10n.exportToClipboard),
contentPadding: EdgeInsets.zero,
),
),
const PopupMenuDivider(),
PopupMenuItem(
value: 'reset',
child: ListTile(
leading: const Icon(Icons.restart_alt),
title: Text(l10n.resetToDefaults),
contentPadding: EdgeInsets.zero,
),
),
],
),
],
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: ListenableBuilder(
listenable: _templateService,
builder: (context, child) {
final templates = _templateService.templates;
if (templates.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.location_searching,
size: 64,
color: colorScheme.onSurface.withValues(alpha: 0.3),
),
const SizedBox(height: 16),
Text(
l10n.noTemplates,
style: theme.textTheme.titleMedium?.copyWith(
color: colorScheme.onSurface.withValues(alpha: 0.6),
),
),
const SizedBox(height: 8),
Text(
l10n.tapAddToCreate,
style: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurface.withValues(alpha: 0.5),
),
),
],
),
);
}
return ListView.builder(
itemCount: templates.length,
itemBuilder: (context, index) {
final template = templates[index];
return _TemplateListItem(
template: template,
onTap: () => _editTemplate(template),
onDelete: () => _deleteTemplate(template),
);
},
);
},
),
floatingActionButton: FloatingActionButton.extended(
onPressed: _addTemplate,
icon: const Icon(Icons.add),
label: Text(l10n.addTemplate),
),
);
}
}
/// Template list item widget
class _TemplateListItem extends StatelessWidget {
final SarTemplate template;
final VoidCallback onTap;
final VoidCallback onDelete;
const _TemplateListItem({
required this.template,
required this.onTap,
required this.onDelete,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Dismissible(
key: Key(template.id),
direction: DismissDirection.endToStart,
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
child: const Icon(Icons.delete, color: Colors.white),
),
confirmDismiss: (direction) async {
// Show confirmation dialog
return await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(AppLocalizations.of(context)!.deleteTemplate),
content: Text(
AppLocalizations.of(context)!.deleteTemplateConfirmation(template.name),
),
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),
),
],
),
);
},
onDismissed: (direction) => onDelete(),
child: ListTile(
onTap: onTap,
leading: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: template.color,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: template.color.withValues(alpha: 0.3),
blurRadius: 4,
spreadRadius: 1,
),
],
),
child: Center(
child: Text(
template.emoji,
style: const TextStyle(fontSize: 24),
),
),
),
title: Row(
children: [
Text(
template.name,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
if (template.isDefault) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.blue.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: Colors.blue.withValues(alpha: 0.5),
),
),
child: Text(
'DEFAULT',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: Colors.blue.shade700,
),
),
),
],
],
),
subtitle: template.description.isNotEmpty
? Text(
template.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurface.withValues(alpha: 0.6),
),
)
: Text(
template.toSarMessage(),
style: TextStyle(
fontFamily: 'monospace',
fontSize: 12,
color: colorScheme.onSurface.withValues(alpha: 0.5),
),
),
trailing: IconButton(
icon: const Icon(Icons.delete_outline),
color: Colors.red,
onPressed: onDelete,
),
),
);
}
}

View File

@@ -12,6 +12,7 @@ import '../services/locale_preferences.dart';
import '../utils/sample_data_generator.dart'; import '../utils/sample_data_generator.dart';
import '../theme/app_theme.dart'; import '../theme/app_theme.dart';
import '../l10n/app_localizations.dart'; import '../l10n/app_localizations.dart';
import 'sar_template_management_screen.dart';
class SettingsScreen extends StatefulWidget { class SettingsScreen extends StatefulWidget {
final Function(AppThemeMode) onThemeChanged; final Function(AppThemeMode) onThemeChanged;
@@ -259,27 +260,27 @@ class _SettingsScreenState extends State<SettingsScreen> {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Row( title: Row(
children: [ children: [
Icon(Icons.settings, size: 24), const Icon(Icons.settings, size: 24),
SizedBox(width: 12), const SizedBox(width: 12),
Text('Location Permission'), Text(AppLocalizations.of(context)!.locationPermission),
], ],
), ),
content: const Text( content: Text(
'Location permission is permanently denied. Please enable it in your device settings to use GPS tracking and location sharing features.', AppLocalizations.of(context)!.locationPermissionDialogContent,
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: const Text('Cancel'), child: Text(AppLocalizations.of(context)!.cancel),
), ),
ElevatedButton( ElevatedButton(
onPressed: () async { onPressed: () async {
Navigator.pop(context); Navigator.pop(context);
await Geolocator.openAppSettings(); await Geolocator.openAppSettings();
}, },
child: const Text('Open Settings'), child: Text(AppLocalizations.of(context)!.openSettings),
), ),
], ],
), ),
@@ -294,8 +295,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
newPermission == LocationPermission.always) { newPermission == LocationPermission.always) {
// Permission granted // Permission granted
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( SnackBar(
content: Text('Location permission granted!'), content: Text(AppLocalizations.of(context)!.locationPermissionGranted),
backgroundColor: Colors.green, backgroundColor: Colors.green,
), ),
); );
@@ -303,10 +304,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
} else { } else {
// Permission denied // Permission denied
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( SnackBar(
content: Text('Location permission is required for GPS tracking and location sharing.'), content: Text(AppLocalizations.of(context)!.locationPermissionRequiredForGps),
backgroundColor: Colors.orange, backgroundColor: Colors.orange,
duration: Duration(seconds: 4), duration: const Duration(seconds: 4),
), ),
); );
} }
@@ -314,8 +315,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
// Already granted - show info // Already granted - show info
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( SnackBar(
content: Text('Location permission is already granted.'), content: Text(AppLocalizations.of(context)!.locationPermissionAlreadyGranted),
backgroundColor: Colors.blue, backgroundColor: Colors.blue,
), ),
); );
@@ -433,6 +434,17 @@ class _SettingsScreenState extends State<SettingsScreen> {
await _saveRxTxPreference(value); await _saveRxTxPreference(value);
}, },
), ),
Consumer<AppProvider>(
builder: (context, appProvider, child) => SwitchListTile(
secondary: const Icon(Icons.visibility_off),
title: Text(AppLocalizations.of(context)!.simpleMode),
subtitle: Text(AppLocalizations.of(context)!.simpleModeDescription),
value: appProvider.isSimpleMode,
onChanged: (value) async {
await appProvider.toggleSimpleMode(value);
},
),
),
ListTile( ListTile(
leading: const Icon(Icons.language), leading: const Icon(Icons.language),
title: Text(AppLocalizations.of(context)!.language), title: Text(AppLocalizations.of(context)!.language),
@@ -440,18 +452,32 @@ class _SettingsScreenState extends State<SettingsScreen> {
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
onTap: () => _showLanguageDialog(), onTap: () => _showLanguageDialog(),
), ),
ListTile(
leading: const Icon(Icons.location_searching),
title: Text(AppLocalizations.of(context)!.sarTemplates),
subtitle: Text(AppLocalizations.of(context)!.manageSarTemplates),
trailing: const Icon(Icons.chevron_right),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SarTemplateManagementScreen(),
),
);
},
),
const Divider(), const Divider(),
// Permissions Section // Permissions Section
_buildSectionHeader('Permissions'), _buildSectionHeader(AppLocalizations.of(context)!.permissionsSection),
ListTile( ListTile(
leading: const Icon(Icons.location_on), leading: const Icon(Icons.location_on),
title: const Text('Location Permission'), title: Text(AppLocalizations.of(context)!.locationPermission),
subtitle: FutureBuilder<LocationPermission>( subtitle: FutureBuilder<LocationPermission>(
future: Geolocator.checkPermission(), future: Geolocator.checkPermission(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData) { if (!snapshot.hasData) {
return const Text('Checking...'); return Text(AppLocalizations.of(context)!.checking);
} }
final permission = snapshot.data!; final permission = snapshot.data!;
String statusText; String statusText;
@@ -459,23 +485,23 @@ class _SettingsScreenState extends State<SettingsScreen> {
switch (permission) { switch (permission) {
case LocationPermission.always: case LocationPermission.always:
statusText = 'Granted (Always)'; statusText = AppLocalizations.of(context)!.locationPermissionGrantedAlways;
statusColor = Colors.green; statusColor = Colors.green;
break; break;
case LocationPermission.whileInUse: case LocationPermission.whileInUse:
statusText = 'Granted (While In Use)'; statusText = AppLocalizations.of(context)!.locationPermissionGrantedWhileInUse;
statusColor = Colors.green; statusColor = Colors.green;
break; break;
case LocationPermission.denied: case LocationPermission.denied:
statusText = 'Denied - Tap to request'; statusText = AppLocalizations.of(context)!.locationPermissionDeniedTapToRequest;
statusColor = Colors.orange; statusColor = Colors.orange;
break; break;
case LocationPermission.deniedForever: case LocationPermission.deniedForever:
statusText = 'Permanently Denied - Open Settings'; statusText = AppLocalizations.of(context)!.locationPermissionPermanentlyDeniedOpenSettings;
statusColor = Colors.red; statusColor = Colors.red;
break; break;
default: default:
statusText = 'Unknown'; statusText = AppLocalizations.of(context)!.unknown;
statusColor = Colors.grey; statusColor = Colors.grey;
} }
@@ -906,7 +932,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
RadioListTile<AppThemeMode>( RadioListTile<AppThemeMode>(
title: Row( title: Row(
children: [ children: [
const Text('SAR Navy Blue'), Text(AppLocalizations.of(context)!.sarNavyBlue),
const SizedBox(width: 8), const SizedBox(width: 8),
Container( Container(
width: 16, width: 16,
@@ -919,7 +945,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
), ),
], ],
), ),
subtitle: const Text('Professional/Operations Mode'), subtitle: Text(AppLocalizations.of(context)!.sarNavyBlueDescription),
value: AppThemeMode.sarNavyBlue, value: AppThemeMode.sarNavyBlue,
groupValue: _selectedTheme, groupValue: _selectedTheme,
onChanged: (value) { onChanged: (value) {

View File

@@ -184,7 +184,7 @@ class MapMarkerService {
), ),
padding: const EdgeInsets.all(6), padding: const EdgeInsets.all(6),
child: Text( child: Text(
marker.type.emoji, marker.emoji, // Use custom emoji if available
style: const TextStyle(fontSize: 18), style: const TextStyle(fontSize: 18),
), ),
), ),
@@ -198,7 +198,7 @@ class MapMarkerService {
borderRadius: BorderRadius.circular(3), borderRadius: BorderRadius.circular(3),
), ),
child: Text( child: Text(
marker.type.displayName, marker.displayName, // Uses notes if available, otherwise type.displayName
style: const TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 9, fontSize: 9,

View File

@@ -401,8 +401,8 @@ class MeshCoreBleService {
required double latitude, required double latitude,
required double longitude, required double longitude,
}) async { }) async {
// Note: This command does not return an ACK, so we use writeData (fire-and-forget) // This command returns OK (0x00) response, so wait for acknowledgment
await _commandSender.writeData(FrameBuilder.buildSetAdvertLatLon( await _commandSender.writeDataAndWaitForAck(FrameBuilder.buildSetAdvertLatLon(
latitude: latitude, latitude: latitude,
longitude: longitude, longitude: longitude,
)); ));

View File

@@ -0,0 +1,71 @@
import 'package:shared_preferences/shared_preferences.dart';
/// Service for managing message destination preferences
/// Stores the last selected recipient (channel, contact, or room) for sending messages
class MessageDestinationPreferences {
static const String _destinationTypeKey = 'message_destination_type';
static const String _recipientPublicKeyKey = 'message_recipient_public_key';
/// Destination types
static const String destinationTypeChannel = 'channel';
static const String destinationTypeContact = 'contact';
static const String destinationTypeRoom = 'room';
/// Get the saved destination configuration
/// Returns a map with 'type' and optional 'publicKey'
/// Returns null if no preference is saved (defaults to public channel)
static Future<Map<String, String>?> getDestination() async {
final prefs = await SharedPreferences.getInstance();
final type = prefs.getString(_destinationTypeKey);
if (type == null) {
return null; // Use default (public channel)
}
final publicKey = prefs.getString(_recipientPublicKeyKey);
return {
'type': type,
if (publicKey != null) 'publicKey': publicKey,
};
}
/// Save the selected destination
/// [type] - one of: destinationTypeChannel, destinationTypeContact, destinationTypeRoom
/// [recipientPublicKey] - hex string of recipient's public key (required for contact/room)
static Future<void> setDestination(
String type, {
String? recipientPublicKey,
}) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_destinationTypeKey, type);
if (recipientPublicKey != null) {
await prefs.setString(_recipientPublicKeyKey, recipientPublicKey);
} else {
await prefs.remove(_recipientPublicKeyKey);
}
}
/// Clear the saved destination (resets to default public channel)
static Future<void> clearDestination() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_destinationTypeKey);
await prefs.remove(_recipientPublicKeyKey);
}
/// Get display name for destination type
static String getDestinationTypeName(String type) {
switch (type) {
case destinationTypeChannel:
return 'Channel';
case destinationTypeContact:
return 'Contact';
case destinationTypeRoom:
return 'Room';
default:
return 'Unknown';
}
}
}

View File

@@ -20,6 +20,7 @@ class NotificationService {
// Notification IDs // Notification IDs
static const int _sarNotificationId = 1000; static const int _sarNotificationId = 1000;
static const int _messageNotificationId = 2000;
// Notification channels // Notification channels
static const String _urgentChannelId = 'sar_urgent'; static const String _urgentChannelId = 'sar_urgent';
@@ -27,6 +28,11 @@ class NotificationService {
static const String _urgentChannelDescription = static const String _urgentChannelDescription =
'Critical alerts for SAR markers (found persons, fires, staging areas)'; 'Critical alerts for SAR markers (found persons, fires, staging areas)';
static const String _messagesChannelId = 'messages';
static const String _messagesChannelName = 'Messages';
static const String _messagesChannelDescription =
'Notifications for incoming messages from contacts and channels';
/// Initialize notification service /// Initialize notification service
Future<void> initialize() async { Future<void> initialize() async {
if (_isInitialized) return; if (_isInitialized) return;
@@ -125,8 +131,20 @@ class NotificationService {
sound: RawResourceAndroidNotificationSound('notification'), sound: RawResourceAndroidNotificationSound('notification'),
); );
// Messages channel with high priority
const messagesChannel = AndroidNotificationChannel(
_messagesChannelId,
_messagesChannelName,
description: _messagesChannelDescription,
importance: Importance.high,
playSound: true,
enableVibration: true,
showBadge: true,
);
await androidPlugin.createNotificationChannel(urgentChannel); await androidPlugin.createNotificationChannel(urgentChannel);
debugPrint('✅ [NotificationService] Created urgent notification channel'); await androidPlugin.createNotificationChannel(messagesChannel);
debugPrint('✅ [NotificationService] Created notification channels');
} catch (e) { } catch (e) {
debugPrint('⚠️ [NotificationService] Error creating channels: $e'); debugPrint('⚠️ [NotificationService] Error creating channels: $e');
} }
@@ -303,6 +321,93 @@ class NotificationService {
} }
} }
/// Show notification for regular message (contact or channel)
Future<void> showMessageNotification({
required String senderName,
required String messageText,
required bool isChannelMessage,
String? channelName,
AppLocalizations? localizations,
}) async {
if (!_isInitialized) {
debugPrint('⚠️ [NotificationService] Not initialized, skipping notification');
return;
}
if (!_permissionGranted) {
debugPrint('⚠️ [NotificationService] Permission not granted, skipping notification');
return;
}
try {
// Generate unique notification ID based on timestamp
final notificationId = _messageNotificationId + (DateTime.now().millisecondsSinceEpoch % 1000);
// Build notification title and body
final title = isChannelMessage
? (localizations != null
? '${localizations.channel}: ${channelName ?? "Public"}'
: 'Channel: ${channelName ?? "Public"}')
: (localizations != null
? '${localizations.newMessage} ${localizations.from} $senderName'
: 'New message from $senderName');
final body = messageText.length > 200
? '${messageText.substring(0, 200)}...'
: messageText;
// Android notification details
final androidDetails = AndroidNotificationDetails(
_messagesChannelId,
_messagesChannelName,
channelDescription: _messagesChannelDescription,
importance: Importance.high,
priority: Priority.high,
ticker: title,
playSound: true,
enableVibration: true,
showWhen: true,
when: DateTime.now().millisecondsSinceEpoch,
styleInformation: BigTextStyleInformation(
body,
contentTitle: title,
summaryText: senderName,
),
);
// iOS notification details
final darwinDetails = DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
sound: 'default',
threadIdentifier: isChannelMessage ? 'channel_messages' : 'direct_messages',
subtitle: senderName,
);
// Combined notification details
final notificationDetails = NotificationDetails(
android: androidDetails,
iOS: darwinDetails,
);
// Show notification
await _notificationsPlugin.show(
notificationId,
title,
body,
notificationDetails,
payload: 'message:${isChannelMessage ? "channel" : "contact"}',
);
debugPrint('✅ [NotificationService] Showed message notification');
debugPrint(' Sender: $senderName');
debugPrint(' Type: ${isChannelMessage ? "Channel" : "Direct"}');
} catch (e) {
debugPrint('❌ [NotificationService] Error showing message notification: $e');
}
}
/// Cancel all notifications /// Cancel all notifications
Future<void> cancelAll() async { Future<void> cancelAll() async {
try { try {

View File

@@ -0,0 +1,244 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/sar_template.dart';
import '../utils/sar_message_parser.dart';
/// SAR Template Service - Manages SAR templates with persistence
class SarTemplateService extends ChangeNotifier {
static final SarTemplateService _instance = SarTemplateService._internal();
factory SarTemplateService() => _instance;
SarTemplateService._internal();
static const String _storageKey = 'sar_templates';
List<SarTemplate> _templates = [];
bool _initialized = false;
/// Get all templates
List<SarTemplate> get templates => List.unmodifiable(_templates);
/// Get default templates
List<SarTemplate> get defaultTemplates =>
_templates.where((t) => t.isDefault).toList();
/// Get custom templates
List<SarTemplate> get customTemplates =>
_templates.where((t) => !t.isDefault).toList();
/// Check if initialized
bool get isInitialized => _initialized;
/// Initialize service and load templates
Future<void> initialize() async {
if (_initialized) return;
try {
final prefs = await SharedPreferences.getInstance();
final jsonString = prefs.getString(_storageKey);
if (jsonString != null && jsonString.isNotEmpty) {
// Load saved templates
final List<dynamic> jsonList = json.decode(jsonString);
_templates = jsonList.map((json) => SarTemplate.fromJson(json)).toList();
// Ensure defaults exist (in case user deleted them or version upgrade)
_ensureDefaultTemplates();
} else {
// First time - initialize with defaults
_templates = SarTemplate.defaults;
await _saveToStorage();
}
_initialized = true;
notifyListeners();
debugPrint('SarTemplateService initialized with ${_templates.length} templates');
} catch (e) {
debugPrint('Error initializing SAR templates: $e');
// Fallback to defaults on error
_templates = SarTemplate.defaults;
_initialized = true;
notifyListeners();
}
}
/// Ensure default templates exist
void _ensureDefaultTemplates() {
final defaults = SarTemplate.defaults;
final existingDefaultIds = _templates.where((t) => t.isDefault).map((t) => t.id).toSet();
// Add missing defaults
for (final defaultTemplate in defaults) {
if (!existingDefaultIds.contains(defaultTemplate.id)) {
_templates.insert(0, defaultTemplate);
}
}
}
/// Save templates to storage
Future<void> _saveToStorage() async {
try {
final prefs = await SharedPreferences.getInstance();
final jsonList = _templates.map((t) => t.toJson()).toList();
final jsonString = json.encode(jsonList);
await prefs.setString(_storageKey, jsonString);
debugPrint('Saved ${_templates.length} SAR templates to storage');
} catch (e) {
debugPrint('Error saving SAR templates: $e');
rethrow;
}
}
/// Add new template
Future<void> addTemplate(SarTemplate template) async {
_templates.add(template);
await _saveToStorage();
notifyListeners();
debugPrint('Added SAR template: ${template.name}');
}
/// Update existing template
Future<void> updateTemplate(String id, SarTemplate updatedTemplate) async {
final index = _templates.indexWhere((t) => t.id == id);
if (index != -1) {
_templates[index] = updatedTemplate;
await _saveToStorage();
notifyListeners();
debugPrint('Updated SAR template: ${updatedTemplate.name}');
} else {
throw Exception('Template with id $id not found');
}
}
/// Delete template
Future<void> deleteTemplate(String id) async {
final template = _templates.firstWhere((t) => t.id == id);
_templates.removeWhere((t) => t.id == id);
await _saveToStorage();
notifyListeners();
debugPrint('Deleted SAR template: ${template.name}');
}
/// Get template by ID
SarTemplate? getTemplateById(String id) {
try {
return _templates.firstWhere((t) => t.id == id);
} catch (e) {
return null;
}
}
/// Import templates from clipboard
/// Expects SAR message format (one per line):
/// S:🧑:0,0:Person found
/// S:🔥:0,0:Active fire
Future<int> importFromClipboard() async {
try {
final clipboardData = await Clipboard.getData(Clipboard.kTextPlain);
if (clipboardData == null || clipboardData.text == null || clipboardData.text!.trim().isEmpty) {
throw Exception('Clipboard is empty');
}
return importFromText(clipboardData.text!);
} catch (e) {
debugPrint('Error importing from clipboard: $e');
rethrow;
}
}
/// Import templates from text (SAR message format)
Future<int> importFromText(String text) async {
try {
final lines = text.split('\n').where((line) => line.trim().isNotEmpty).toList();
int importedCount = 0;
final List<String> errors = [];
for (final line in lines) {
final trimmed = line.trim();
if (!trimmed.startsWith('S:')) {
errors.add('Invalid format: $trimmed');
continue;
}
// Validate with parser
if (!SarMessageParser.isValidFormat(trimmed)) {
final error = SarMessageParser.getFormatError(trimmed);
errors.add(error ?? 'Invalid SAR message format');
continue;
}
try {
final template = SarTemplate.fromSarMessage(trimmed);
// Check for duplicates (same emoji + name)
final isDuplicate = _templates.any((t) =>
t.emoji == template.emoji && t.name == template.name
);
if (!isDuplicate) {
_templates.add(template);
importedCount++;
}
} catch (e) {
errors.add('Error parsing line: $trimmed - $e');
}
}
if (importedCount > 0) {
await _saveToStorage();
notifyListeners();
}
if (errors.isNotEmpty) {
debugPrint('Import errors: ${errors.join(', ')}');
}
debugPrint('Imported $importedCount SAR templates');
return importedCount;
} catch (e) {
debugPrint('Error importing templates: $e');
rethrow;
}
}
/// Export all templates to clipboard (SAR message format)
Future<void> exportToClipboard() async {
try {
final sarMessages = _templates.map((t) => t.toSarMessage()).join('\n');
await Clipboard.setData(ClipboardData(text: sarMessages));
debugPrint('Exported ${_templates.length} templates to clipboard');
} catch (e) {
debugPrint('Error exporting to clipboard: $e');
rethrow;
}
}
/// Export templates to text (SAR message format)
String exportToText() {
return _templates.map((t) => t.toSarMessage()).join('\n');
}
/// Reset to default templates
Future<void> resetToDefaults() async {
_templates = SarTemplate.defaults;
await _saveToStorage();
notifyListeners();
debugPrint('Reset to default SAR templates');
}
/// Clear all templates (including defaults)
Future<void> clearAll() async {
_templates.clear();
await _saveToStorage();
notifyListeners();
debugPrint('Cleared all SAR templates');
}
/// Get count of templates
int get templateCount => _templates.length;
/// Check if template exists
bool hasTemplate(String id) {
return _templates.any((t) => t.id == id);
}
}

View File

@@ -1,7 +1,6 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart';
import 'package:flutter_map_tile_caching/custom_backend_api.dart';
import 'package:vector_map_tiles_mbtiles/vector_map_tiles_mbtiles.dart'; import 'package:vector_map_tiles_mbtiles/vector_map_tiles_mbtiles.dart';
import 'package:mbtiles/mbtiles.dart'; import 'package:mbtiles/mbtiles.dart';
import '../models/map_layer.dart'; import '../models/map_layer.dart';
@@ -173,6 +172,91 @@ class TileCacheService {
} }
} }
/// Export the current tile cache store to an archive file
///
/// [outputPath] - Full path where the archive should be saved (e.g., '/path/to/export.fmtc')
///
/// Returns the number of tiles exported
Future<int> exportStore(String outputPath) async {
if (!_isInitialized) {
throw StateError(
'TileCacheService not initialized. Call initialize() first.',
);
}
try {
final external = FMTCRoot.external(pathToArchive: outputPath);
final result = await external.export(storeNames: [_storeName]);
debugPrint('Export completed: $result tiles exported to $outputPath');
return result;
} catch (e) {
debugPrint('Error exporting store: $e');
rethrow;
}
}
/// Import a tile cache store from an archive file
///
/// [filePath] - Path to the .fmtc archive file to import
/// [storeNames] - Optional list of store names to import (null = import all)
/// [strategy] - Conflict resolution strategy (default: merge)
///
/// Returns a map with import statistics (e.g., tile count, stores imported)
Future<Map<String, dynamic>> importStore(
String filePath, {
List<String>? storeNames,
ImportConflictStrategy strategy = ImportConflictStrategy.merge,
}) async {
if (!_isInitialized) {
throw StateError(
'TileCacheService not initialized. Call initialize() first.',
);
}
try {
final external = FMTCRoot.external(pathToArchive: filePath);
final result = external.import(storeNames: storeNames, strategy: strategy);
// Wait for the import to complete and get tile count
final tileCount = await result.complete;
// Wait for store states
final storesToStates = await result.storesToStates;
debugPrint('Import completed: $tileCount tiles imported, ${storesToStates.length} stores');
// Count successful stores (those that weren't skipped)
final successfulCount = storesToStates.values.where((state) => state.name != null).length;
return {
'successfulStores': successfulCount,
'tileCount': tileCount,
'storesToStates': storesToStates,
};
} catch (e) {
debugPrint('Error importing store: $e');
rethrow;
}
}
/// List all stores available in an archive file without importing
///
/// [filePath] - Path to the .fmtc archive file to inspect
///
/// Returns a list of store names contained in the archive
Future<List<String>> listArchiveStores(String filePath) async {
try {
final external = FMTCRoot.external(pathToArchive: filePath);
final stores = await external.listStores;
debugPrint('Archive contains ${stores.length} stores: $stores');
return stores;
} catch (e) {
debugPrint('Error listing archive stores: $e');
rethrow;
}
}
void dispose() { void dispose() {
_isInitialized = false; _isInitialized = false;
} }

View File

@@ -12,9 +12,13 @@ class DrawingMessageParser {
} }
/// Parse drawing message text into MapDrawing object /// Parse drawing message text into MapDrawing object
/// senderName should be extracted from packet metadata /// senderName and messageId should be extracted from packet metadata
/// Returns null if parsing fails /// Returns null if parsing fails
static MapDrawing? parseDrawingMessage(String text, {String? senderName}) { static MapDrawing? parseDrawingMessage(
String text, {
String? senderName,
String? messageId,
}) {
if (!isDrawingMessage(text)) { if (!isDrawingMessage(text)) {
return null; return null;
} }
@@ -27,8 +31,12 @@ class DrawingMessageParser {
final json = jsonDecode(jsonStr) as Map<String, dynamic>; final json = jsonDecode(jsonStr) as Map<String, dynamic>;
// Use ultra-compact network format parser // Use ultra-compact network format parser
// Sender name comes from packet metadata, not JSON // Sender name and message ID come from packet metadata, not JSON
return MapDrawing.fromNetworkJson(json, senderName: senderName); return MapDrawing.fromNetworkJson(
json,
senderName: senderName,
messageId: messageId,
);
} catch (e) { } catch (e) {
return null; return null;
} }

View File

@@ -142,7 +142,7 @@ class SampleDataGenerator {
pathLen: 1, pathLen: 1,
textType: MessageTextType.plain, textType: MessageTextType.plain,
senderTimestamp: timestamp.millisecondsSinceEpoch ~/ 1000, senderTimestamp: timestamp.millisecondsSinceEpoch ~/ 1000,
text: 'S:🧑:${lat.toStringAsFixed(4)},${lon.toStringAsFixed(4)}', text: 'S:🧑:${lat.toStringAsFixed(5)},${lon.toStringAsFixed(5)}',
receivedAt: timestamp, receivedAt: timestamp,
isSarMarker: true, isSarMarker: true,
sarMarkerType: SarMarkerType.foundPerson, sarMarkerType: SarMarkerType.foundPerson,
@@ -171,7 +171,7 @@ class SampleDataGenerator {
pathLen: 1, pathLen: 1,
textType: MessageTextType.plain, textType: MessageTextType.plain,
senderTimestamp: timestamp.millisecondsSinceEpoch ~/ 1000, senderTimestamp: timestamp.millisecondsSinceEpoch ~/ 1000,
text: 'S:🔥:${lat.toStringAsFixed(4)},${lon.toStringAsFixed(4)}', text: 'S:🔥:${lat.toStringAsFixed(5)},${lon.toStringAsFixed(5)}',
receivedAt: timestamp, receivedAt: timestamp,
isSarMarker: true, isSarMarker: true,
sarMarkerType: SarMarkerType.fire, sarMarkerType: SarMarkerType.fire,
@@ -200,7 +200,7 @@ class SampleDataGenerator {
pathLen: 1, pathLen: 1,
textType: MessageTextType.plain, textType: MessageTextType.plain,
senderTimestamp: timestamp.millisecondsSinceEpoch ~/ 1000, senderTimestamp: timestamp.millisecondsSinceEpoch ~/ 1000,
text: 'S:🏕️:${lat.toStringAsFixed(4)},${lon.toStringAsFixed(4)}', text: 'S:🏕️:${lat.toStringAsFixed(5)},${lon.toStringAsFixed(5)}',
receivedAt: timestamp, receivedAt: timestamp,
isSarMarker: true, isSarMarker: true,
sarMarkerType: SarMarkerType.stagingArea, sarMarkerType: SarMarkerType.stagingArea,
@@ -236,7 +236,7 @@ class SampleDataGenerator {
pathLen: 1, pathLen: 1,
textType: MessageTextType.plain, textType: MessageTextType.plain,
senderTimestamp: timestamp.millisecondsSinceEpoch ~/ 1000, senderTimestamp: timestamp.millisecondsSinceEpoch ~/ 1000,
text: 'S:📦:${lat.toStringAsFixed(4)},${lon.toStringAsFixed(4)}${notes[i % notes.length]}', text: 'S:📦:${lat.toStringAsFixed(5)},${lon.toStringAsFixed(5)}${notes[i % notes.length]}',
receivedAt: timestamp, receivedAt: timestamp,
isSarMarker: true, isSarMarker: true,
sarMarkerType: SarMarkerType.object, sarMarkerType: SarMarkerType.object,
@@ -281,14 +281,14 @@ class SampleDataGenerator {
// Mix regular messages and SAR markers // Mix regular messages and SAR markers
final emergencyMessages = [ final emergencyMessages = [
'URGENT: Medical assistance needed at sector 4', 'URGENT: Medical assistance needed at sector 4',
'S:🧑:${center.latitude.toStringAsFixed(4)},${(center.longitude + 0.005).toStringAsFixed(4)} Adult male, conscious', 'S:🧑:${center.latitude.toStringAsFixed(5)},${(center.longitude + 0.005).toStringAsFixed(5)} Adult male, conscious',
'Fire spotted - coordinates incoming', 'Fire spotted - coordinates incoming',
'S:🔥:${(center.latitude + 0.008).toStringAsFixed(4)},${(center.longitude + 0.003).toStringAsFixed(4)} Spreading rapidly!', 'S:🔥:${(center.latitude + 0.008).toStringAsFixed(5)},${(center.longitude + 0.003).toStringAsFixed(5)} Spreading rapidly!',
'PRIORITY: Need helicopter support', 'PRIORITY: Need helicopter support',
'Medical team en route to your location', 'Medical team en route to your location',
'Evac helicopter ETA 10 minutes', 'Evac helicopter ETA 10 minutes',
'Emergency resolved - all clear', 'Emergency resolved - all clear',
'S:🏕️:${(center.latitude - 0.002).toStringAsFixed(4)},${(center.longitude - 0.004).toStringAsFixed(4)} Emergency staging area', 'S:🏕️:${(center.latitude - 0.002).toStringAsFixed(5)},${(center.longitude - 0.004).toStringAsFixed(5)} Emergency staging area',
'Emergency services notified and responding', 'Emergency services notified and responding',
]; ];

View File

@@ -81,6 +81,9 @@ class SarMessageParser {
sarMarkerType: sarInfo.type, sarMarkerType: sarInfo.type,
sarGpsCoordinates: sarInfo.location, sarGpsCoordinates: sarInfo.location,
sarNotes: sarInfo.notes, // Extract and store notes sarNotes: sarInfo.notes, // Extract and store notes
sarCustomEmoji: sarInfo.type == SarMarkerType.unknown
? sarInfo.emoji // Preserve custom emoji for unknown types
: null,
); );
} }

View File

@@ -8,6 +8,7 @@ import '../../models/room_login_state.dart';
import '../../providers/connection_provider.dart'; import '../../providers/connection_provider.dart';
import '../../providers/contacts_provider.dart'; import '../../providers/contacts_provider.dart';
import '../../providers/map_provider.dart'; import '../../providers/map_provider.dart';
import '../../providers/app_provider.dart';
import 'direct_message_sheet.dart'; import 'direct_message_sheet.dart';
import 'room_login_sheet.dart'; import 'room_login_sheet.dart';
import '../../utils/toast_logger.dart'; import '../../utils/toast_logger.dart';
@@ -18,6 +19,7 @@ class ContactTile extends StatelessWidget {
final Position? currentPosition; final Position? currentPosition;
final double Function(double, double, double, double)? calculateDistance; final double Function(double, double, double, double)? calculateDistance;
final String Function(double)? formatDistance; final String Function(double)? formatDistance;
final VoidCallback? onNavigateToMap;
const ContactTile({ const ContactTile({
super.key, super.key,
@@ -25,10 +27,14 @@ class ContactTile extends StatelessWidget {
this.currentPosition, this.currentPosition,
this.calculateDistance, this.calculateDistance,
this.formatDistance, this.formatDistance,
this.onNavigateToMap,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final appProvider = context.watch<AppProvider>();
final isSimpleMode = appProvider.isSimpleMode;
final hasTelemetry = contact.telemetry != null && contact.telemetry!.isRecent; final hasTelemetry = contact.telemetry != null && contact.telemetry!.isRecent;
final battery = contact.displayBattery; final battery = contact.displayBattery;
final location = contact.displayLocation; final location = contact.displayLocation;
@@ -113,8 +119,8 @@ class ContactTile extends StatelessWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
// Battery indicator // Battery indicator - hidden in simple mode
if (battery != null) ...[ if (!isSimpleMode && battery != null) ...[
const SizedBox(width: 4), const SizedBox(width: 4),
Icon( Icon(
_getBatteryIcon(battery), _getBatteryIcon(battery),
@@ -130,8 +136,8 @@ class ContactTile extends StatelessWidget {
), ),
), ),
], ],
// Connection type indicator (direct/flood) - shown for all contact types // Connection type indicator (direct/flood) - hidden in simple mode
if (contact.type != ContactType.channel) ...[ if (!isSimpleMode && contact.type != ContactType.channel) ...[
const SizedBox(width: 4), const SizedBox(width: 4),
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
@@ -168,129 +174,186 @@ class ContactTile extends StatelessWidget {
], ],
], ],
), ),
subtitle: Column( subtitle: isSimpleMode
crossAxisAlignment: CrossAxisAlignment.start, ? Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
const SizedBox(height: 4),
// Room login status badges
if (roomLoginState != null && roomLoginState.isLoggedIn) ...[
Row(
children: [ children: [
if (roomLoginState.isAdmin) const SizedBox(height: 4),
Container( // Simple mode: Only show location and distance
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), if (location != null) ...[
decoration: BoxDecoration( Row(
color: Colors.red.withOpacity(0.2),
borderRadius: BorderRadius.circular(4),
border: Border.all(color: Colors.red, width: 1),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.admin_panel_settings, size: 10, color: Colors.red),
const SizedBox(width: 2),
Text(
AppLocalizations.of(context)!.admin,
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: Colors.red,
fontWeight: FontWeight.bold,
fontSize: 10,
),
),
],
),
),
if (roomLoginState.isAdmin) const SizedBox(width: 4),
Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.2),
borderRadius: BorderRadius.circular(4),
border: Border.all(color: Colors.green, width: 1),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [ children: [
const Icon(Icons.check_circle, size: 10, color: Colors.green), const Icon(Icons.location_on, size: 12, color: Colors.blue),
const SizedBox(width: 2), const SizedBox(width: 4),
Text( Expanded(
AppLocalizations.of(context)!.loggedIn, child: Text(
style: Theme.of(context).textTheme.labelSmall?.copyWith( 'GPS: ${location.latitude.toStringAsFixed(5)}, ${location.longitude.toStringAsFixed(5)}',
color: Colors.green, style: Theme.of(context).textTheme.labelSmall,
fontWeight: FontWeight.bold, overflow: TextOverflow.ellipsis,
fontSize: 10,
), ),
), ),
], ],
), ),
), if (distanceText != null) ...[
], const SizedBox(height: 4),
), Row(
const SizedBox(height: 4), children: [
], const Icon(Icons.straighten, size: 12, color: Colors.blue),
// Last seen + GPS info combined const SizedBox(width: 4),
Row( Text(
children: [ '${AppLocalizations.of(context)!.distance}: $distanceText',
Icon( style: Theme.of(context).textTheme.labelSmall?.copyWith(
Icons.access_time, color: Colors.blue,
size: 12, fontWeight: FontWeight.w500,
color: contact.isRecentlySeen ? Colors.green : Colors.grey, ),
), ),
const SizedBox(width: 4), ],
Text( ),
contact.timeSinceLastSeen, ],
style: Theme.of(context).textTheme.labelSmall, ] else
), Text(
if (location != null) ...[ AppLocalizations.of(context)!.noGpsData,
const SizedBox(width: 8), style: Theme.of(context).textTheme.labelSmall?.copyWith(
const Text('', style: TextStyle(color: Colors.grey)), color: Colors.grey,
const SizedBox(width: 8), ),
if (hasTelemetry)
const Icon(Icons.sensors, size: 12, color: Colors.green)
else
const Icon(Icons.sensors_off, size: 12, color: Colors.grey),
const SizedBox(width: 4),
Expanded(
child: Text(
'GPS: ${location.latitude.toStringAsFixed(4)}, ${location.longitude.toStringAsFixed(4)}',
style: Theme.of(context).textTheme.labelSmall,
overflow: TextOverflow.ellipsis,
), ),
),
] else ...[
const SizedBox(width: 8),
const Text('', style: TextStyle(color: Colors.grey)),
const SizedBox(width: 8),
const Icon(Icons.sensors_off, size: 12, color: Colors.grey),
const SizedBox(width: 4),
Text(
AppLocalizations.of(context)!.noGpsData,
style: Theme.of(context).textTheme.labelSmall,
),
], ],
], )
), : Column(
// Distance info (new row) crossAxisAlignment: CrossAxisAlignment.start,
if (distanceText != null) ...[
const SizedBox(height: 4),
Row(
children: [ children: [
const Icon(Icons.straighten, size: 12, color: Colors.blue), const SizedBox(height: 4),
const SizedBox(width: 4), // Room login status badges
Text( if (roomLoginState != null && roomLoginState.isLoggedIn) ...[
'${AppLocalizations.of(context)!.distance}: $distanceText', Row(
style: Theme.of(context).textTheme.labelSmall?.copyWith( children: [
color: Colors.blue, if (roomLoginState.isAdmin)
fontWeight: FontWeight.w500, Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.2),
borderRadius: BorderRadius.circular(4),
border: Border.all(color: Colors.red, width: 1),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.admin_panel_settings, size: 10, color: Colors.red),
const SizedBox(width: 2),
Text(
AppLocalizations.of(context)!.admin,
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: Colors.red,
fontWeight: FontWeight.bold,
fontSize: 10,
),
),
],
),
),
if (roomLoginState.isAdmin) const SizedBox(width: 4),
Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.2),
borderRadius: BorderRadius.circular(4),
border: Border.all(color: Colors.green, width: 1),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.check_circle, size: 10, color: Colors.green),
const SizedBox(width: 2),
Text(
AppLocalizations.of(context)!.loggedIn,
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: Colors.green,
fontWeight: FontWeight.bold,
fontSize: 10,
),
),
],
),
),
],
), ),
const SizedBox(height: 4),
],
// Last seen + GPS info combined
Row(
children: [
Icon(
Icons.access_time,
size: 12,
color: contact.isRecentlySeen ? Colors.green : Colors.grey,
),
const SizedBox(width: 4),
Text(
contact.timeSinceLastSeen,
style: Theme.of(context).textTheme.labelSmall,
),
if (location != null) ...[
const SizedBox(width: 8),
const Text('', style: TextStyle(color: Colors.grey)),
const SizedBox(width: 8),
if (hasTelemetry)
const Icon(Icons.sensors, size: 12, color: Colors.green)
else
const Icon(Icons.sensors_off, size: 12, color: Colors.grey),
const SizedBox(width: 4),
Expanded(
child: Text(
'GPS: ${location.latitude.toStringAsFixed(5)}, ${location.longitude.toStringAsFixed(5)}',
style: Theme.of(context).textTheme.labelSmall,
overflow: TextOverflow.ellipsis,
),
),
] else ...[
const SizedBox(width: 8),
const Text('', style: TextStyle(color: Colors.grey)),
const SizedBox(width: 8),
const Icon(Icons.sensors_off, size: 12, color: Colors.grey),
const SizedBox(width: 4),
Text(
AppLocalizations.of(context)!.noGpsData,
style: Theme.of(context).textTheme.labelSmall,
),
],
],
), ),
// Distance info (new row)
if (distanceText != null) ...[
const SizedBox(height: 4),
Row(
children: [
const Icon(Icons.straighten, size: 12, color: Colors.blue),
const SizedBox(width: 4),
Text(
'${AppLocalizations.of(context)!.distance}: $distanceText',
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: Colors.blue,
fontWeight: FontWeight.w500,
),
),
],
),
],
], ],
), ),
],
],
),
trailing: null, trailing: null,
onTap: () => _showContactDetails(context, contact), onTap: () {
// In simple mode, tap directly opens message sheet for chat contacts
if (isSimpleMode && contact.type == ContactType.chat) {
_showDirectMessageDialog(context, contact);
} else if (isSimpleMode && contact.type == ContactType.repeater) {
// In simple mode, tapping a repeater jumps to the map
_jumpToMapForRepeater(context, contact);
} else if (isSimpleMode && contact.type == ContactType.room && !contact.isPublicChannel) {
_showRoomLoginDialog(context, contact);
} else {
_showContactDetails(context, contact);
}
},
onLongPress: () async { onLongPress: () async {
final connectionProvider = context.read<ConnectionProvider>(); final connectionProvider = context.read<ConnectionProvider>();
@@ -360,6 +423,29 @@ class ContactTile extends StatelessWidget {
); );
} }
void _jumpToMapForRepeater(BuildContext context, Contact contact) {
final location = contact.displayLocation;
if (location != null) {
final mapProvider = context.read<MapProvider>();
// Navigate to map location
mapProvider.navigateToLocation(
location: LatLng(location.latitude, location.longitude),
zoom: 15.0,
animate: true,
);
// Switch to map tab using callback
onNavigateToMap?.call();
} else {
// No location available, just show toast
ToastLogger.info(
context,
'Repeater ${contact.displayName} has no location data',
);
}
}
void _showDeleteConfirmation(BuildContext context, Contact contact) { void _showDeleteConfirmation(BuildContext context, Contact contact) {
showDialog( showDialog(
context: context, context: context,
@@ -587,8 +673,8 @@ class ContactTile extends StatelessWidget {
); );
Navigator.pop(context); Navigator.pop(context);
// Switch to map tab (assuming it's index 2) // Switch to map tab using callback
DefaultTabController.of(context).animateTo(2); onNavigateToMap?.call();
}, },
icon: const Icon(Icons.map, size: 18), icon: const Icon(Icons.map, size: 18),
label: Text(AppLocalizations.of(context)!.viewOnMap), label: Text(AppLocalizations.of(context)!.viewOnMap),

View File

@@ -1,12 +1,14 @@
import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:geolocator/geolocator.dart'; import 'package:geolocator/geolocator.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import '../../models/contact.dart'; import '../../models/contact.dart';
import '../../models/message.dart'; import '../../models/message.dart';
import '../../providers/connection_provider.dart'; import '../../providers/connection_provider.dart';
import '../../providers/messages_provider.dart'; import '../../providers/messages_provider.dart';
import '../../providers/app_provider.dart';
import '../../utils/toast_logger.dart'; import '../../utils/toast_logger.dart';
import '../../l10n/app_localizations.dart'; import '../../l10n/app_localizations.dart';
@@ -174,6 +176,9 @@ class _DirectMessageSheetState extends State<DirectMessageSheet> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final colorScheme = theme.colorScheme; final colorScheme = theme.colorScheme;
final appProvider = context.watch<AppProvider>();
final isSimpleMode = appProvider.isSimpleMode;
final contactLocation = widget.contact.displayLocation;
return Container( return Container(
height: MediaQuery.of(context).size.height * 0.9, height: MediaQuery.of(context).size.height * 0.9,
@@ -222,35 +227,90 @@ class _DirectMessageSheetState extends State<DirectMessageSheet> {
), ),
), ),
// Info banner // Mini map in simple mode (scrollable content)
Container( Expanded(
margin: const EdgeInsets.symmetric(horizontal: 16), child: SingleChildScrollView(
padding: const EdgeInsets.all(16), child: Column(
decoration: BoxDecoration( children: [
color: Theme.of(context).colorScheme.primaryContainer, const SizedBox(height: 16),
borderRadius: BorderRadius.circular(8), if (isSimpleMode && contactLocation != null) ...[
), GestureDetector(
child: Row( onTap: () {
children: [ // Hide keyboard when tapping on map
Icon(Icons.info_outline, color: Theme.of(context).colorScheme.onPrimaryContainer), _focusNode.unfocus();
const SizedBox(width: 12), },
Expanded( child: Container(
child: Text( margin: const EdgeInsets.symmetric(horizontal: 16),
AppLocalizations.of(context)!.directMessageInfo(widget.contact.displayName), height: 200,
style: TextStyle( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.onPrimaryContainer, borderRadius: BorderRadius.circular(12),
fontSize: 13, border: Border.all(color: colorScheme.outline),
),
clipBehavior: Clip.antiAlias,
child: FlutterMap(
options: MapOptions(
initialCenter: LatLng(
contactLocation.latitude,
contactLocation.longitude,
),
initialZoom: 13.0,
interactionOptions: const InteractionOptions(
flags: InteractiveFlag.pinchZoom | InteractiveFlag.drag,
),
),
children: [
TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'com.meshcore.sar',
),
MarkerLayer(
markers: [
Marker(
point: LatLng(
contactLocation.latitude,
contactLocation.longitude,
),
width: 40,
height: 40,
child: Icon(
Icons.location_on,
color: colorScheme.primary,
size: 40,
),
),
],
),
],
),
),
), ),
), const SizedBox(height: 8),
), // Location coordinates
], Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.gps_fixed, size: 14, color: colorScheme.onSurfaceVariant),
const SizedBox(width: 4),
Text(
'${contactLocation.latitude.toStringAsFixed(5)}, ${contactLocation.longitude.toStringAsFixed(5)}',
style: TextStyle(
color: colorScheme.onSurfaceVariant,
fontSize: 12,
fontFamily: 'monospace',
),
),
],
),
),
const SizedBox(height: 16),
],
],
),
), ),
), ),
const SizedBox(height: 16),
const Spacer(),
// Message input // Message input
Container( Container(
padding: EdgeInsets.only( padding: EdgeInsets.only(
@@ -321,7 +381,7 @@ class _DirectMessageSheetState extends State<DirectMessageSheet> {
OutlinedButton.icon( OutlinedButton.icon(
onPressed: _insertCurrentLocation, onPressed: _insertCurrentLocation,
icon: const Icon(Icons.my_location, size: 18), icon: const Icon(Icons.my_location, size: 18),
label: const Text('Location'), label: Text(AppLocalizations.of(context)!.myLocation),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
side: BorderSide(color: colorScheme.outline), side: BorderSide(color: colorScheme.outline),

View File

@@ -124,7 +124,7 @@ class CompassSarList extends StatelessWidget {
color: markerColor, color: markerColor,
size: 24, size: 24,
), ),
title: Text(marker.type.displayName), title: Text(marker.displayName),
subtitle: Text( subtitle: Text(
'${_bearingToCardinal(bearing)}${_formatDistance(distance)}${marker.timeAgo}', '${_bearingToCardinal(bearing)}${_formatDistance(distance)}${marker.timeAgo}',
style: Theme.of(context).textTheme.bodySmall, style: Theme.of(context).textTheme.bodySmall,

View File

@@ -534,7 +534,7 @@ class _DetailedCompassDialogState extends State<DetailedCompassDialog> {
additionalInfo = 'Battery: ${_selectedContact!.telemetry!.batteryPercentage!.round()}%'; additionalInfo = 'Battery: ${_selectedContact!.telemetry!.batteryPercentage!.round()}%';
} }
} else if (_selectedSarMarker != null) { } else if (_selectedSarMarker != null) {
title = _selectedSarMarker!.type.displayName; title = _selectedSarMarker!.displayName;
targetLocation = _selectedSarMarker!.location; targetLocation = _selectedSarMarker!.location;
additionalInfo = _selectedSarMarker!.timeAgo; additionalInfo = _selectedSarMarker!.timeAgo;
@@ -664,21 +664,21 @@ class _DetailedCompassDialogState extends State<DetailedCompassDialog> {
children: [ children: [
_buildLargeInfoCard( _buildLargeInfoCard(
context, context,
'Distance', AppLocalizations.of(context)!.distance,
_formatDistance(distance), _formatDistance(distance),
Icons.straighten, Icons.straighten,
color, color,
), ),
_buildLargeInfoCard( _buildLargeInfoCard(
context, context,
'Bearing', AppLocalizations.of(context)!.bearing,
'${bearing.round()}°', '${bearing.round()}°',
Icons.navigation, Icons.navigation,
color, color,
), ),
_buildLargeInfoCard( _buildLargeInfoCard(
context, context,
'Direction', AppLocalizations.of(context)!.direction,
_bearingToCardinal(bearing), _bearingToCardinal(bearing),
Icons.explore, Icons.explore,
color, color,

View File

@@ -8,11 +8,13 @@ import '../../l10n/app_localizations.dart';
class DrawingLayer extends StatelessWidget { class DrawingLayer extends StatelessWidget {
final List<MapDrawing> drawings; final List<MapDrawing> drawings;
final MapDrawing? previewDrawing; final MapDrawing? previewDrawing;
final bool isSimpleMode;
const DrawingLayer({ const DrawingLayer({
super.key, super.key,
required this.drawings, required this.drawings,
this.previewDrawing, this.previewDrawing,
this.isSimpleMode = false,
}); });
@override @override
@@ -45,8 +47,9 @@ class DrawingLayer extends StatelessWidget {
opacity = 0.6; opacity = 0.6;
strokeWidth = 4.0; strokeWidth = 4.0;
} else if (drawing.isReceived) { } else if (drawing.isReceived) {
// Received drawing from another node (thinner, more transparent) // Received drawing from another node
opacity = 0.7; // In simple mode: solid (opacity 1.0), in normal mode: translucent (0.7)
opacity = isSimpleMode ? 1.0 : 0.7;
strokeWidth = 3.0; strokeWidth = 3.0;
} else { } else {
// Local drawing (solid line, normal thickness) // Local drawing (solid line, normal thickness)
@@ -82,13 +85,17 @@ class DrawingLayer extends StatelessWidget {
class DrawingMarkersLayer extends StatelessWidget { class DrawingMarkersLayer extends StatelessWidget {
final List<MapDrawing> drawings; final List<MapDrawing> drawings;
final Function(String drawingId)? onDeleteDrawing; final Function(String drawingId)? onDeleteDrawing;
final Function(MapDrawing drawing)? onTapDrawing;
final bool showDeleteButtons; final bool showDeleteButtons;
final bool isSimpleMode;
const DrawingMarkersLayer({ const DrawingMarkersLayer({
super.key, super.key,
required this.drawings, required this.drawings,
this.onDeleteDrawing, this.onDeleteDrawing,
this.onTapDrawing,
this.showDeleteButtons = false, this.showDeleteButtons = false,
this.isSimpleMode = false,
}); });
@override @override
@@ -134,49 +141,64 @@ class DrawingMarkersLayer extends StatelessWidget {
), ),
), ),
); );
} else if (drawing.isReceived && drawing.senderName != null) { } else if (drawing.isReceived && drawing.senderName != null && !isSimpleMode) {
// Show sender badge for received drawings (when not in drawing mode) // Show sender badge for received drawings (when not in drawing mode and not in simple mode)
// Make it tappable if message ID is available
markers.add( markers.add(
Marker( Marker(
point: centerPoint, point: centerPoint,
width: 120, width: 120,
height: 30, height: 30,
child: Container( child: GestureDetector(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), onTap: drawing.messageId != null && onTapDrawing != null
decoration: BoxDecoration( ? () => onTapDrawing!(drawing)
color: drawing.color.withValues(alpha: 0.9), : null,
borderRadius: BorderRadius.circular(12), child: Container(
border: Border.all(color: Colors.white, width: 1.5), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
boxShadow: [ decoration: BoxDecoration(
BoxShadow( color: drawing.color.withValues(alpha: 0.9),
color: Colors.black.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(12),
blurRadius: 4, border: Border.all(color: Colors.white, width: 1.5),
offset: const Offset(0, 2), boxShadow: [
), BoxShadow(
], color: Colors.black.withValues(alpha: 0.3),
), blurRadius: 4,
child: Row( offset: const Offset(0, 2),
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.person,
color: Colors.white,
size: 14,
),
const SizedBox(width: 4),
Flexible(
child: Text(
drawing.senderName!,
style: const TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
), ),
), ],
], ),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.person,
color: Colors.white,
size: 14,
),
const SizedBox(width: 4),
Flexible(
child: Text(
drawing.senderName!,
style: const TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
// Add indicator that this is tappable
if (drawing.messageId != null && onTapDrawing != null) ...[
const SizedBox(width: 4),
const Icon(
Icons.arrow_forward_ios,
color: Colors.white,
size: 10,
),
],
],
),
), ),
), ),
), ),

View File

@@ -201,6 +201,43 @@ class DrawingToolbar extends StatelessWidget {
drawingProvider.setDrawingMode(DrawingMode.rectangle); drawingProvider.setDrawingMode(DrawingMode.rectangle);
}, },
), ),
const Divider(),
// Toggle received drawings visibility
SwitchListTile(
secondary: Icon(
drawingProvider.showReceivedDrawings
? Icons.visibility
: Icons.visibility_off,
),
title: Text(AppLocalizations.of(context)!.showReceivedDrawings),
subtitle: Text(
drawingProvider.showReceivedDrawings
? AppLocalizations.of(context)!.showingAllDrawings
: AppLocalizations.of(context)!.showingOnlyYourDrawings,
),
value: drawingProvider.showReceivedDrawings,
onChanged: (value) {
drawingProvider.toggleReceivedDrawings();
},
),
// Toggle SAR markers visibility
SwitchListTile(
secondary: Icon(
drawingProvider.showSarMarkers
? Icons.pin_drop
: Icons.pin_drop_outlined,
),
title: Text(AppLocalizations.of(context)!.showSarMarkers),
subtitle: Text(
drawingProvider.showSarMarkers
? AppLocalizations.of(context)!.showingSarMarkers
: AppLocalizations.of(context)!.hidingSarMarkers,
),
value: drawingProvider.showSarMarkers,
onChanged: (value) {
drawingProvider.toggleSarMarkers();
},
),
if (drawingProvider.drawings.isNotEmpty) ...[ if (drawingProvider.drawings.isNotEmpty) ...[
const Divider(), const Divider(),
ListTile( ListTile(

View File

@@ -158,7 +158,7 @@ class MapMarkers {
), ),
padding: const EdgeInsets.all(6), padding: const EdgeInsets.all(6),
child: Text( child: Text(
marker.type.emoji, marker.emoji, // Use custom emoji if available
style: const TextStyle(fontSize: 18), style: const TextStyle(fontSize: 18),
), ),
), ),
@@ -171,16 +171,27 @@ class MapMarkers {
color: Colors.black.withOpacity(0.7), color: Colors.black.withOpacity(0.7),
borderRadius: BorderRadius.circular(3), borderRadius: BorderRadius.circular(3),
), ),
child: Text( child: Builder(
marker.type.getLocalizedName(context), builder: (context) {
style: const TextStyle( // Debug: Print what we're actually displaying
color: Colors.white, debugPrint('🗺️ [MapMarker] Displaying SAR marker:');
fontSize: 9, debugPrint(' marker.notes: "${marker.notes}"');
fontWeight: FontWeight.bold, debugPrint(' marker.type: ${marker.type}');
), debugPrint(' marker.type.displayName: ${marker.type.displayName}');
overflow: TextOverflow.ellipsis, debugPrint(' marker.displayName: ${marker.displayName}');
maxLines: 1,
textAlign: TextAlign.center, return Text(
marker.displayName,
style: const TextStyle(
color: Colors.white,
fontSize: 9,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
textAlign: TextAlign.center,
);
},
), ),
), ),
], ],
@@ -252,9 +263,9 @@ class MapMarkers {
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: Row( title: Row(
children: [ children: [
Text(marker.type.emoji, style: const TextStyle(fontSize: 24)), Text(marker.emoji, style: const TextStyle(fontSize: 24)), // Use custom emoji if available
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded(child: Text(marker.type.getLocalizedName(context))), Expanded(child: Text(marker.displayName)),
], ],
), ),
content: Column( content: Column(

View File

@@ -0,0 +1,344 @@
import 'package:flutter/material.dart';
import '../../models/contact.dart';
import '../../l10n/app_localizations.dart';
/// Bottom sheet for selecting message recipient (channel, contact, or room)
class RecipientSelectorSheet extends StatefulWidget {
final List<Contact> contacts;
final List<Contact> rooms;
final String? currentDestinationType;
final String? currentRecipientPublicKey;
final Function(String type, Contact? recipient) onSelect;
const RecipientSelectorSheet({
super.key,
required this.contacts,
required this.rooms,
this.currentDestinationType,
this.currentRecipientPublicKey,
required this.onSelect,
});
@override
State<RecipientSelectorSheet> createState() => _RecipientSelectorSheetState();
}
class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
final TextEditingController _searchController = TextEditingController();
String _searchQuery = '';
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
List<Contact> _filterContacts(List<Contact> contacts) {
if (_searchQuery.isEmpty) return contacts;
final query = _searchQuery.toLowerCase();
return contacts.where((contact) {
final name = contact.displayName?.toLowerCase() ?? contact.advName.toLowerCase();
return name.contains(query);
}).toList();
}
bool _isSelected(String type, Contact? contact) {
if (widget.currentDestinationType != type) return false;
if (type == 'channel') return true;
if (contact == null) return false;
return contact.publicKeyHex == widget.currentRecipientPublicKey;
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final filteredContacts = _filterContacts(widget.contacts);
final filteredRooms = _filterContacts(widget.rooms);
return Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.8,
),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
width: 1,
),
),
),
child: Row(
children: [
Text(
l10n.selectRecipient,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const Spacer(),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
tooltip: l10n.close,
),
],
),
),
// Search field
Padding(
padding: const EdgeInsets.all(16),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: l10n.searchRecipients,
prefixIcon: const Icon(Icons.search),
suffixIcon: _searchQuery.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
setState(() {
_searchQuery = '';
});
},
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
onChanged: (value) {
setState(() {
_searchQuery = value;
});
},
),
),
// Recipients list
Flexible(
child: ListView(
shrinkWrap: true,
children: [
// Public Channel option
_buildRecipientTile(
context: context,
icon: Icons.public,
title: l10n.publicChannel,
subtitle: l10n.broadcastToAllNearby,
isSelected: _isSelected('channel', null),
onTap: () {
widget.onSelect('channel', null);
Navigator.pop(context);
},
),
const Divider(),
// Contacts section
if (widget.contacts.isNotEmpty) ...[
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: Text(
l10n.contacts,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
),
if (filteredContacts.isEmpty)
Padding(
padding: const EdgeInsets.all(16),
child: Text(
l10n.noContactsFound,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).disabledColor,
fontStyle: FontStyle.italic,
),
textAlign: TextAlign.center,
),
)
else
...filteredContacts.map((contact) {
return _buildRecipientTile(
context: context,
icon: Icons.person,
title: contact.displayName ?? contact.advName,
subtitle: contact.publicKeyShort,
emoji: contact.roleEmoji,
isSelected: _isSelected('contact', contact),
onTap: () {
widget.onSelect('contact', contact);
Navigator.pop(context);
},
);
}),
],
const Divider(),
// Rooms section
if (widget.rooms.isNotEmpty) ...[
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: Text(
l10n.rooms,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
),
if (filteredRooms.isEmpty)
Padding(
padding: const EdgeInsets.all(16),
child: Text(
l10n.noRoomsFound,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).disabledColor,
fontStyle: FontStyle.italic,
),
textAlign: TextAlign.center,
),
)
else
...filteredRooms.map((room) {
return _buildRecipientTile(
context: context,
icon: Icons.meeting_room,
title: room.displayName ?? room.advName,
subtitle: room.publicKeyShort,
emoji: room.roleEmoji,
isSelected: _isSelected('room', room),
onTap: () {
widget.onSelect('room', room);
Navigator.pop(context);
},
);
}),
],
// Empty state
if (widget.contacts.isEmpty && widget.rooms.isEmpty) ...[
Padding(
padding: const EdgeInsets.all(32),
child: Column(
children: [
Icon(
Icons.people_outline,
size: 64,
color: Theme.of(context).disabledColor,
),
const SizedBox(height: 16),
Text(
l10n.noContactsOrRoomsAvailable,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).disabledColor,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
l10n.messagesWillBeSentToPublicChannel,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).disabledColor,
),
textAlign: TextAlign.center,
),
],
),
),
],
const SizedBox(height: 16),
],
),
),
],
),
);
}
Widget _buildRecipientTile({
required BuildContext context,
required IconData icon,
required String title,
required String subtitle,
String? emoji,
required bool isSelected,
required VoidCallback onTap,
}) {
return ListTile(
leading: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: isSelected
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: BorderRadius.circular(20),
),
child: Icon(
icon,
color: isSelected
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
title: Row(
children: [
if (emoji != null && emoji.isNotEmpty) ...[
Text(emoji, style: const TextStyle(fontSize: 16)),
const SizedBox(width: 8),
],
Expanded(
child: Text(
title,
style: TextStyle(
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
),
),
],
),
subtitle: Text(
subtitle,
style: const TextStyle(
fontSize: 12,
fontFamily: 'monospace',
).copyWith(
color: Theme.of(context).textTheme.bodySmall?.color?.withValues(alpha: 0.6),
),
),
trailing: isSelected
? Icon(
Icons.check_circle,
color: Theme.of(context).colorScheme.primary,
)
: null,
onTap: onTap,
);
}
}

View File

@@ -4,14 +4,15 @@ import 'package:provider/provider.dart';
import 'package:geolocator/geolocator.dart'; import 'package:geolocator/geolocator.dart';
import '../../providers/contacts_provider.dart'; import '../../providers/contacts_provider.dart';
import '../../models/contact.dart'; import '../../models/contact.dart';
import '../../models/sar_marker.dart'; import '../../models/sar_template.dart';
import '../../services/validation_service.dart'; import '../../services/validation_service.dart';
import '../../services/sar_template_service.dart';
import '../../l10n/app_localizations.dart'; import '../../l10n/app_localizations.dart';
/// SAR Update Sheet - Modal bottom sheet for creating and sending SAR markers /// SAR Update Sheet - Modal bottom sheet for creating and sending SAR markers
/// This widget is public so it can be used from both messages_tab.dart and map_tab.dart /// This widget is public so it can be used from both messages_tab.dart and map_tab.dart
class SarUpdateSheet extends StatefulWidget { class SarUpdateSheet extends StatefulWidget {
final Future<void> Function(SarMarkerType, Position, String?, Uint8List?, bool) onSend; final Future<void> Function(String emoji, String name, Position, Uint8List?, bool) onSend;
final Position? prePopulatedPosition; final Position? prePopulatedPosition;
final bool allowLocationUpdate; final bool allowLocationUpdate;
@@ -27,7 +28,9 @@ class SarUpdateSheet extends StatefulWidget {
} }
class _SarUpdateSheetState extends State<SarUpdateSheet> { class _SarUpdateSheetState extends State<SarUpdateSheet> {
SarMarkerType _selectedType = SarMarkerType.foundPerson; SarTemplate? _selectedTemplate;
List<SarTemplate> _templates = [];
final SarTemplateService _templateService = SarTemplateService();
Position? _currentPosition; Position? _currentPosition;
bool _loadingLocation = false; bool _loadingLocation = false;
String? _locationError; String? _locationError;
@@ -37,6 +40,7 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_initializeTemplates();
// Use pre-populated position if provided, otherwise get current location // Use pre-populated position if provided, otherwise get current location
if (widget.prePopulatedPosition != null) { if (widget.prePopulatedPosition != null) {
_currentPosition = widget.prePopulatedPosition; _currentPosition = widget.prePopulatedPosition;
@@ -46,6 +50,21 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
_setDefaultDestination(); _setDefaultDestination();
} }
Future<void> _initializeTemplates() async {
if (!_templateService.isInitialized) {
await _templateService.initialize();
}
if (mounted) {
setState(() {
_templates = _templateService.templates;
// Select first template by default
if (_templates.isNotEmpty) {
_selectedTemplate = _templates.first;
}
});
}
}
void _setDefaultDestination() { void _setDefaultDestination() {
// Set default to first room, or first channel if no rooms exist // Set default to first room, or first channel if no rooms exist
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -140,19 +159,23 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Get keyboard height to adjust padding // Get keyboard height to adjust padding
final keyboardHeight = MediaQuery.of(context).viewInsets.bottom; final keyboardHeight = MediaQuery.of(context).viewInsets.bottom;
final bottomSafeArea = MediaQuery.of(context).padding.bottom;
final theme = Theme.of(context); final theme = Theme.of(context);
final colorScheme = theme.colorScheme; final colorScheme = theme.colorScheme;
return Container( return AnimatedPadding(
height: MediaQuery.of(context).size.height * 0.9, padding: EdgeInsets.only(bottom: keyboardHeight),
decoration: BoxDecoration( duration: const Duration(milliseconds: 100),
color: colorScheme.surface, child: Container(
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), height: MediaQuery.of(context).size.height * 0.9,
), decoration: BoxDecoration(
child: Column( color: colorScheme.surface,
children: [ borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
// Header ),
Container( child: Column(
children: [
// Header
Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest, color: colorScheme.surfaceContainerHighest,
@@ -196,7 +219,9 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
left: 16, left: 16,
right: 16, right: 16,
top: 16, top: 16,
bottom: keyboardHeight > 0 ? keyboardHeight + 16 : 16, // Add bottom padding for button area (button + padding + safe area)
// Button height ~48px + container padding 32px + safe area
bottom: 80 + bottomSafeArea,
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -211,30 +236,17 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
MarkerTypeChip( ..._templates.map((template) {
type: SarMarkerType.foundPerson, return Padding(
isSelected: _selectedType == SarMarkerType.foundPerson, padding: const EdgeInsets.only(bottom: 8),
onTap: () => setState(() => _selectedType = SarMarkerType.foundPerson), child: TemplateChip(
), template: template,
const SizedBox(height: 8), isSelected: _selectedTemplate?.id == template.id,
MarkerTypeChip( onTap: () => setState(() => _selectedTemplate = template),
type: SarMarkerType.fire, ),
isSelected: _selectedType == SarMarkerType.fire, );
onTap: () => setState(() => _selectedType = SarMarkerType.fire), }).toList(),
), const SizedBox(height: 16),
const SizedBox(height: 8),
MarkerTypeChip(
type: SarMarkerType.stagingArea,
isSelected: _selectedType == SarMarkerType.stagingArea,
onTap: () => setState(() => _selectedType = SarMarkerType.stagingArea),
),
const SizedBox(height: 8),
MarkerTypeChip(
type: SarMarkerType.object,
isSelected: _selectedType == SarMarkerType.object,
onTap: () => setState(() => _selectedType = SarMarkerType.object),
),
const SizedBox(height: 24),
// Destination selection (compact dropdown with rooms and channel) // Destination selection (compact dropdown with rooms and channel)
Text( Text(
@@ -595,35 +607,28 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
// Bottom action button // Bottom action button
Container( Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: colorScheme.surface,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 4,
offset: const Offset(0, -2),
),
],
),
child: SafeArea( child: SafeArea(
top: false, top: false,
child: SizedBox( child: SizedBox(
width: double.infinity, width: double.infinity,
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: _currentPosition == null || _selectedContact == null onPressed: _currentPosition == null || _selectedContact == null || _selectedTemplate == null
? null ? null
: () async { : () async {
final validator = ValidationService(); final validator = ValidationService();
final notes = _notesController.text.trim();
// Validate coordinates
final coordResult = validator.validateCoordinates(
_currentPosition!.latitude,
_currentPosition!.longitude,
);
if (!coordResult.isValid) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(coordResult.errorMessage!),
backgroundColor: Colors.red,
),
);
}
return;
}
// Validate notes length if provided // Validate notes length if provided
final notes = _notesController.text.trim();
if (notes.isNotEmpty) { if (notes.isNotEmpty) {
final notesResult = validator.validateName( final notesResult = validator.validateName(
notes, notes,
@@ -642,6 +647,23 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
} }
} }
// Validate coordinates
final coordResult = validator.validateCoordinates(
_currentPosition!.latitude,
_currentPosition!.longitude,
);
if (!coordResult.isValid) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(coordResult.errorMessage!),
backgroundColor: Colors.red,
),
);
}
return;
}
// Validate location accuracy (warn if >50m) // Validate location accuracy (warn if >50m)
if (_currentPosition!.accuracy != null && if (_currentPosition!.accuracy != null &&
_currentPosition!.accuracy! > 50.0) { _currentPosition!.accuracy! > 50.0) {
@@ -669,10 +691,21 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
if (shouldContinue != true) return; if (shouldContinue != true) return;
} }
// Combine template name with optional notes
String displayText;
if (notes.isNotEmpty) {
// Include both template name and custom notes
displayText = '${_selectedTemplate!.name} - $notes';
} else {
// Just the template name
displayText = _selectedTemplate!.name;
}
// Send SAR marker with emoji and display text
await widget.onSend( await widget.onSend(
_selectedType, _selectedTemplate!.emoji,
displayText,
_currentPosition!, _currentPosition!,
notes.isEmpty ? null : notes,
_selectedContact!.isChannel _selectedContact!.isChannel
? null ? null
: _selectedContact!.publicKey, : _selectedContact!.publicKey,
@@ -701,43 +734,29 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
), ),
], ],
), ),
),
); );
} }
} }
/// Marker Type Chip widget - Displays a selectable SAR marker type /// Template Chip widget - Displays a selectable SAR template
class MarkerTypeChip extends StatelessWidget { class TemplateChip extends StatelessWidget {
final SarMarkerType type; final SarTemplate template;
final bool isSelected; final bool isSelected;
final VoidCallback onTap; final VoidCallback onTap;
const MarkerTypeChip({ const TemplateChip({
super.key, super.key,
required this.type, required this.template,
required this.isSelected, required this.isSelected,
required this.onTap, required this.onTap,
}); });
Color _getMarkerColor() {
switch (type) {
case SarMarkerType.foundPerson:
return Colors.green;
case SarMarkerType.fire:
return Colors.red;
case SarMarkerType.stagingArea:
return Colors.orange;
case SarMarkerType.object:
return Colors.purple;
case SarMarkerType.unknown:
return Colors.grey;
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final color = _getMarkerColor();
final theme = Theme.of(context); final theme = Theme.of(context);
final colorScheme = theme.colorScheme; final colorScheme = theme.colorScheme;
final color = template.color;
return InkWell( return InkWell(
onTap: onTap, onTap: onTap,
@@ -758,18 +777,35 @@ class MarkerTypeChip extends StatelessWidget {
child: Row( child: Row(
children: [ children: [
Text( Text(
type.emoji, template.emoji,
style: const TextStyle(fontSize: 32), style: const TextStyle(fontSize: 32),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded( Expanded(
child: Text( child: Column(
type.getLocalizedName(context), crossAxisAlignment: CrossAxisAlignment.start,
style: TextStyle( children: [
fontSize: 16, Text(
fontWeight: FontWeight.w500, template.name,
color: isSelected ? color : colorScheme.onSurface, style: TextStyle(
), fontSize: 16,
fontWeight: FontWeight.w600,
color: isSelected ? color : colorScheme.onSurface,
),
),
if (template.description.isNotEmpty) ...[
const SizedBox(height: 2),
Text(
template.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12,
color: colorScheme.onSurface.withValues(alpha: 0.6),
),
),
],
],
), ),
), ),
if (isSelected) if (isSelected)

View File

@@ -0,0 +1,321 @@
import 'package:flutter/material.dart';
import '../../models/sar_template.dart';
import '../../l10n/app_localizations.dart';
/// Dialog for adding or editing SAR templates
class SarTemplateEditDialog extends StatefulWidget {
final SarTemplate? template; // Null for new template
final Function(SarTemplate) onSave;
const SarTemplateEditDialog({
super.key,
this.template,
required this.onSave,
});
@override
State<SarTemplateEditDialog> createState() => _SarTemplateEditDialogState();
}
class _SarTemplateEditDialogState extends State<SarTemplateEditDialog> {
late TextEditingController _emojiController;
late TextEditingController _nameController;
late TextEditingController _descriptionController;
late String _selectedColor;
final List<Map<String, dynamic>> _colorOptions = [
{'name': 'Green', 'hex': '#4CAF50'},
{'name': 'Red', 'hex': '#F44336'},
{'name': 'Orange', 'hex': '#FF9800'},
{'name': 'Purple', 'hex': '#9C27B0'},
{'name': 'Blue', 'hex': '#2196F3'},
{'name': 'Yellow', 'hex': '#FFC107'},
{'name': 'Brown', 'hex': '#795548'},
{'name': 'Gray', 'hex': '#9E9E9E'},
];
String? _emojiError;
String? _nameError;
@override
void initState() {
super.initState();
_emojiController = TextEditingController(text: widget.template?.emoji ?? '');
_nameController = TextEditingController(text: widget.template?.name ?? '');
_descriptionController = TextEditingController(text: widget.template?.description ?? '');
_selectedColor = widget.template?.colorHex ?? '#4CAF50';
}
@override
void dispose() {
_emojiController.dispose();
_nameController.dispose();
_descriptionController.dispose();
super.dispose();
}
bool _validate() {
final l10n = AppLocalizations.of(context)!;
setState(() {
_emojiError = null;
_nameError = null;
});
bool isValid = true;
if (_emojiController.text.trim().isEmpty) {
setState(() {
_emojiError = l10n.emojiRequired;
});
isValid = false;
}
if (_nameController.text.trim().isEmpty) {
setState(() {
_nameError = l10n.nameRequired;
});
isValid = false;
}
return isValid;
}
void _save() {
if (!_validate()) return;
final template = SarTemplate(
id: widget.template?.id ?? 'custom_${DateTime.now().millisecondsSinceEpoch}',
emoji: _emojiController.text.trim(),
name: _nameController.text.trim(),
description: _descriptionController.text.trim(),
colorHex: _selectedColor,
isDefault: widget.template?.isDefault ?? false,
);
widget.onSave(template);
Navigator.of(context).pop();
}
String _getPreview() {
final emoji = _emojiController.text.trim();
final description = _descriptionController.text.trim();
if (emoji.isEmpty) return 'S::0,0';
if (description.isEmpty) return 'S:$emoji:0,0';
return 'S:$emoji:0,0:$description';
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final l10n = AppLocalizations.of(context)!;
final bottomPadding = MediaQuery.of(context).viewInsets.bottom;
return Container(
decoration: BoxDecoration(
color: colorScheme.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
),
child: DraggableScrollableSheet(
initialChildSize: 0.9,
minChildSize: 0.5,
maxChildSize: 0.95,
expand: false,
builder: (context, scrollController) {
return SingleChildScrollView(
controller: scrollController,
child: Padding(
padding: EdgeInsets.fromLTRB(24, 24, 24, 24 + bottomPadding),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Drag handle
Center(
child: Container(
width: 40,
height: 4,
margin: const EdgeInsets.only(bottom: 20),
decoration: BoxDecoration(
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.4),
borderRadius: BorderRadius.circular(2),
),
),
),
// Header
Text(
widget.template == null ? l10n.addTemplate : l10n.editTemplate,
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 24),
// Emoji field
TextField(
controller: _emojiController,
decoration: InputDecoration(
labelText: l10n.templateEmoji,
hintText: '🧑',
errorText: _emojiError,
filled: true,
fillColor: colorScheme.surfaceContainerHighest,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
prefixIcon: const Icon(Icons.emoji_emotions),
),
maxLength: 4,
style: const TextStyle(fontSize: 24),
textAlign: TextAlign.center,
onChanged: (_) => setState(() {}),
),
const SizedBox(height: 16),
// Name field
TextField(
controller: _nameController,
decoration: InputDecoration(
labelText: l10n.templateName,
hintText: l10n.templateNameHint,
errorText: _nameError,
filled: true,
fillColor: colorScheme.surfaceContainerHighest,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
prefixIcon: const Icon(Icons.label),
),
maxLength: 30,
onChanged: (_) => setState(() {}),
),
const SizedBox(height: 16),
// Description field
TextField(
controller: _descriptionController,
decoration: InputDecoration(
labelText: l10n.templateDescription,
hintText: l10n.templateDescriptionHint,
filled: true,
fillColor: colorScheme.surfaceContainerHighest,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
prefixIcon: const Icon(Icons.description),
),
maxLength: 100,
maxLines: 2,
onChanged: (_) => setState(() {}),
),
const SizedBox(height: 16),
// Color picker
Text(
l10n.templateColor,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 12),
Wrap(
spacing: 12,
runSpacing: 12,
children: _colorOptions.map((colorOption) {
final hex = colorOption['hex'] as String;
final color = Color(int.parse('FF${hex.replaceAll('#', '')}', radix: 16));
final isSelected = _selectedColor == hex;
return GestureDetector(
onTap: () => setState(() => _selectedColor = hex),
child: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: Border.all(
color: isSelected ? colorScheme.primary : Colors.transparent,
width: 3,
),
boxShadow: [
if (isSelected)
BoxShadow(
color: colorScheme.primary.withValues(alpha: 0.3),
blurRadius: 8,
spreadRadius: 2,
),
],
),
child: isSelected
? const Icon(Icons.check, color: Colors.white)
: null,
),
);
}).toList(),
),
const SizedBox(height: 24),
// Preview
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: colorScheme.outline.withValues(alpha: 0.3),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.previewFormat,
style: theme.textTheme.labelSmall?.copyWith(
color: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
_getPreview(),
style: TextStyle(
fontFamily: 'monospace',
fontSize: 14,
color: colorScheme.onSurface,
),
),
],
),
),
const SizedBox(height: 24),
// Actions
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(l10n.cancel),
),
const SizedBox(width: 12),
ElevatedButton.icon(
onPressed: _save,
icon: const Icon(Icons.save),
label: Text(l10n.save),
),
],
),
],
),
),
);
},
),
);
}
}