mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
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:
@@ -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
100
.github/workflows/README.md
vendored
Normal 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
|
||||||
349
.github/workflows/build-multiplatform.yml
vendored
Normal file
349
.github/workflows/build-multiplatform.yml
vendored
Normal 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 }}
|
||||||
Binary file not shown.
BIN
ios/Runner.ipa
BIN
ios/Runner.ipa
Binary file not shown.
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
196
lib/models/sar_template.dart
Normal file
196
lib/models/sar_template.dart
Normal 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,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
450
lib/screens/sar_template_management_screen.dart
Normal file
450
lib/screens/sar_template_management_screen.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
));
|
));
|
||||||
|
|||||||
71
lib/services/message_destination_preferences.dart
Normal file
71
lib/services/message_destination_preferences.dart
Normal 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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
244
lib/services/sar_template_service.dart
Normal file
244
lib/services/sar_template_service.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
344
lib/widgets/messages/recipient_selector_sheet.dart
Normal file
344
lib/widgets/messages/recipient_selector_sheet.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
321
lib/widgets/sar/sar_template_edit_dialog.dart
Normal file
321
lib/widgets/sar/sar_template_edit_dialog.dart
Normal 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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user