diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..a54dadad --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +github: [zjs81] +custom: + - "https://paypal.me/zjs82" diff --git a/README.md b/README.md index acf1215a..8ad65f99 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh - **Live Map View**: Real-time visualization of mesh network nodes on an interactive map - **Node Filtering**: Filter by node type (chat, repeater, sensor) and time range - **Location Sharing**: Share GPS coordinates and custom markers with contacts -- **Offline Maps**: Download map tiles for offline use in remote areas +- **Offline Maps**: Download map tiles for offline use in remote areas (with [StadiaMaps](https://stadiamaps.com/pricing/) Free Subscription API-Key) - **MGRS Coordinates**: Support for Military Grid Reference System coordinate format ### Device Management diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 4ff626f2..5a533139 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -22,7 +22,7 @@ + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index f9ecda0f..dd58900e 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 08a0eeda..1c752ac2 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml new file mode 100644 index 00000000..1f4b7d7b --- /dev/null +++ b/android/app/src/main/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #9F9D95 + \ No newline at end of file diff --git a/assets/fonts/Inter-OFL.txt b/assets/fonts/Inter-OFL.txt new file mode 100644 index 00000000..21f6aff9 --- /dev/null +++ b/assets/fonts/Inter-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/assets/fonts/Inter.ttf b/assets/fonts/Inter.ttf new file mode 100644 index 00000000..047c92f6 Binary files /dev/null and b/assets/fonts/Inter.ttf differ diff --git a/assets/fonts/InterItalic.ttf b/assets/fonts/InterItalic.ttf new file mode 100644 index 00000000..c1775784 Binary files /dev/null and b/assets/fonts/InterItalic.ttf differ diff --git a/assets/fonts/JetBrainsMono-OFL.txt b/assets/fonts/JetBrainsMono-OFL.txt new file mode 100644 index 00000000..821a3dac --- /dev/null +++ b/assets/fonts/JetBrainsMono-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. + +This license is copied below, and is also available with a FAQ at: https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/assets/fonts/JetBrainsMono.ttf b/assets/fonts/JetBrainsMono.ttf new file mode 100644 index 00000000..aa310be8 Binary files /dev/null and b/assets/fonts/JetBrainsMono.ttf differ diff --git a/assets/icons/ic_foreground.png b/assets/icons/ic_foreground.png new file mode 100644 index 00000000..3e3e115b Binary files /dev/null and b/assets/icons/ic_foreground.png differ diff --git a/documentation/routing-paths.md b/documentation/routing-paths.md index 88cac9f2..56b4867f 100644 --- a/documentation/routing-paths.md +++ b/documentation/routing-paths.md @@ -36,11 +36,11 @@ The connector reads a single-mode byte and clamps to `0..3`, so the supported ho Paths in messages and storage consist of: -- **`pathLength`**: Encoded path length in bytes as carried by the frame/header. This is not the hop count when `pathHashByteWidth > 1`. - - Note: on-air headers carry `pathLength` as a byte count. However, in the app's data model and storage the `pathLength` field is stored as the decoded hop count (number of hops). When reading or writing model objects (contacts, messages, storage), treat `pathLength` as the hop count; when constructing frames use the encoded byte length. -- **`pathBytes`**: Raw bytes of the path, grouped by `pathHashByteWidth` -- **`hopCount`**: Derived display value computed from bytes and width: `(byteCount + hashByteWidth - 1) ~/ hashByteWidth` -- **Example**: With `pathHashByteWidth=2`, a 3-hop path needs 6 bytes, so `pathLength = 6` and `hopCount = 3`: + - **`pathLength` (model/storage)**: Hop count (number of hops). Negative values (e.g. `-1`) are used as a flood sentinel. + - **On-air `path_len` byte**: A packed byte that encodes hop count + hash width and is decoded into `pathLength` + `pathBytes` when parsing frames. + - **`pathBytes`**: Raw bytes of the path (concatenated hop prefixes), grouped by `pathHashByteWidth`. + - **`hopCount`**: Derived display value computed from bytes and width: `(byteCount + hashByteWidth - 1) ~/ hashByteWidth`. + - **Example**: With `pathHashByteWidth=2`, a 3-hop path has 6 bytes (`pathBytes.length = 6`) and `pathLength = 3`: - `pathBytes = [0xA1, 0xA2, 0xB1, 0xB2, 0xC1, 0xC2]` - Hops: `[0xA1A2]`, `[0xB1B2]`, `[0xC1C2]` diff --git a/documentation/settings.md b/documentation/settings.md index a2f4192a..8567cf4c 100644 --- a/documentation/settings.md +++ b/documentation/settings.md @@ -52,7 +52,7 @@ These settings are sent directly to the connected device firmware. ### Radio Settings Opens a dialog pre-populated with the device's current radio settings. Contains: -- **Preset dropdown**: Regional presets — selecting a preset immediately fills all fields below. Includes presets for Australia, Australia (Narrow), Australia SA WA QLD, Czech Republic, EU 433MHz, EU/UK (Long Range), EU/UK (Medium Range), EU/UK (Narrow), New Zealand, New Zealand (Narrow), Portugal 433, Portugal 869, numerous Russia city presets, Switzerland, USA Arizona, USA/Canada, and Vietnam +- **Preset dropdown**: Regional presets — selecting a preset immediately fills all fields below. Includes presets for Australia, Australia (Narrow), Australia (Mid), Australia SA WA QLD, Czech Republic, EU 433MHz, EU/UK (Long Range), EU/UK (Medium Range), EU/UK (Narrow), New Zealand, New Zealand (Narrow), Portugal 433, Portugal 869, numerous Russia city presets, Switzerland, USA Arizona, USA/Canada, and Vietnam - **Frequency** (MHz): Free text, validated 300–2500 MHz - **Bandwidth**: Dropdown (7.8 / 10.4 / 15.6 / 20.8 / 31.25 / 41.7 / 62.5 / 125 / 250 / 500 kHz) - **Spreading Factor**: SF5–SF12 @@ -129,6 +129,10 @@ A dedicated sub-screen for app-level preferences (nothing here is sent to the de - **Show Other Nodes**: Toggle room/sensor markers - **Time Filter**: All time / Last 1h / Last 6h / Last 24h / Last week - **Units**: Metric / Imperial +- **Raster Tile Source**: Sets the MAP theme and with that from where to get the map tile data: + - OpenStreetMap (Auto/Standard/Dark) is provided by the free OpenStreetMap tile server. (Can only be used for live view or already cached.) + - Stamen Terrain / AlidadeSmooth Dark / Outdoors / OSM Bright are [StadiaMaps.com raster tile maps](https://stadiamaps.com/products/maps/interactive-basemaps/) for which you can choose an Hosted Endpoint (Worldwide / Europe hosted) and have to provide the API key to your subscription. (You can cache these maps for offline Map usage.) + There will be no account provided by meshcore-open. You will have to get you own subscription. StadiaMaps offers a [free](https://stadiamaps.com/pricing/) subscription to download up to 200'000 Standart Raster Basemap tiles. - **Offline Map Cache**: Navigate to tile download screen ### Translation diff --git a/ios/ExportOptions.plist b/ios/ExportOptions.plist new file mode 100644 index 00000000..aadd0dac --- /dev/null +++ b/ios/ExportOptions.plist @@ -0,0 +1,16 @@ + + + + + method + app-store-connect + teamID + X74VJ4UAST + signingStyle + automatic + uploadSymbols + + destination + export + + diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b0e98ca4..bc43ea0c 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,73 +1,21 @@ PODS: - Flutter (1.0.0) - - flutter_blue_plus_darwin (0.0.2): - - Flutter - - FlutterMacOS - flutter_foreground_task (0.0.1): - Flutter - - flutter_local_notifications (0.0.1): - - Flutter - - mobile_scanner (7.0.0): - - Flutter - - FlutterMacOS - - package_info_plus (0.4.5): - - Flutter - - share_plus (0.0.1): - - Flutter - - shared_preferences_foundation (0.0.1): - - Flutter - - FlutterMacOS - - sqflite_darwin (0.0.4): - - Flutter - - FlutterMacOS - - url_launcher_ios (0.0.1): - - Flutter DEPENDENCIES: - Flutter (from `Flutter`) - - flutter_blue_plus_darwin (from `.symlinks/plugins/flutter_blue_plus_darwin/darwin`) - flutter_foreground_task (from `.symlinks/plugins/flutter_foreground_task/ios`) - - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - - mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`) - - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - - share_plus (from `.symlinks/plugins/share_plus/ios`) - - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) - - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) EXTERNAL SOURCES: Flutter: :path: Flutter - flutter_blue_plus_darwin: - :path: ".symlinks/plugins/flutter_blue_plus_darwin/darwin" flutter_foreground_task: :path: ".symlinks/plugins/flutter_foreground_task/ios" - flutter_local_notifications: - :path: ".symlinks/plugins/flutter_local_notifications/ios" - mobile_scanner: - :path: ".symlinks/plugins/mobile_scanner/darwin" - package_info_plus: - :path: ".symlinks/plugins/package_info_plus/ios" - share_plus: - :path: ".symlinks/plugins/share_plus/ios" - shared_preferences_foundation: - :path: ".symlinks/plugins/shared_preferences_foundation/darwin" - sqflite_darwin: - :path: ".symlinks/plugins/sqflite_darwin/darwin" - url_launcher_ios: - :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 - flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3 flutter_foreground_task: a159d2c2173b33699ddb3e6c2a067045d7cebb89 - flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb - mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93 - package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 - share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a - shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb - sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 - url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b PODFILE CHECKSUM: e42b502c78c33aa1ed9d42eaea8960ce2139504b diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 7bf9b4cd..52009fe5 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; @@ -52,6 +53,7 @@ 718BC7DCCFC5C370705C12E5 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; @@ -67,6 +69,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, 9A698254711B63C3940A64CB /* libPods-Runner.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -85,6 +88,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, @@ -180,13 +184,15 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, F0D7F2413C6E4B7A9B1C2D3E /* Fix Native Asset Minimum OS */, - B788CEDB957A87EE8AC593BB /* [CP] Copy Pods Resources */, ); buildRules = ( ); dependencies = ( ); name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; @@ -220,6 +226,9 @@ Base, ); mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; @@ -283,39 +292,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - B788CEDB957A87EE8AC593BB /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; - F0D7F2413C6E4B7A9B1C2D3E /* Fix Native Asset Minimum OS */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}", - ); - name = "Fix Native Asset Minimum OS"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "set -e\nFRAMEWORKS_DIR=\"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}\"\nMIN_OS=\"${IPHONEOS_DEPLOYMENT_TARGET}\"\nif [ ! -d \"$FRAMEWORKS_DIR\" ] || [ -z \"$MIN_OS\" ]; then\n exit 0\nfi\nfind \"$FRAMEWORKS_DIR\" -maxdepth 2 -name Info.plist | while read -r plist; do\n bundle_id=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' \"$plist\" 2>/dev/null || true)\n case \"$bundle_id\" in\n io.flutter.flutter.native-assets.*)\n /usr/libexec/PlistBuddy -c \"Set :MinimumOSVersion $MIN_OS\" \"$plist\" 2>/dev/null || \\\n /usr/libexec/PlistBuddy -c \"Add :MinimumOSVersion string $MIN_OS\" \"$plist\"\n ;;\n esac\ndone\n"; - }; DE3B2E091393835C0B38492E /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -338,6 +314,22 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + F0D7F2413C6E4B7A9B1C2D3E /* Fix Native Asset Minimum OS */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}", + ); + name = "Fix Native Asset Minimum OS"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "set -e\nFRAMEWORKS_DIR=\"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}\"\nMIN_OS=\"${IPHONEOS_DEPLOYMENT_TARGET}\"\nif [ ! -d \"$FRAMEWORKS_DIR\" ] || [ -z \"$MIN_OS\" ]; then\n exit 0\nfi\nfind \"$FRAMEWORKS_DIR\" -maxdepth 2 -name Info.plist | while read -r plist; do\n bundle_id=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' \"$plist\" 2>/dev/null || true)\n case \"$bundle_id\" in\n io.flutter.flutter.native-assets.*)\n /usr/libexec/PlistBuddy -c \"Set :MinimumOSVersion $MIN_OS\" \"$plist\" 2>/dev/null || \\\n /usr/libexec/PlistBuddy -c \"Add :MinimumOSVersion string $MIN_OS\" \"$plist\"\n ;;\n esac\ndone\n"; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -446,7 +438,9 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = X74VJ4UAST; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -625,7 +619,9 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = X74VJ4UAST; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -647,7 +643,9 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = X74VJ4UAST; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -696,6 +694,20 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index e3773d42..c3fedb29 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -5,6 +5,24 @@ + + + + + + + + + + _loadDiscoveredContactCache() async { final cached = await _discoveryContactStore.loadContacts(); + // Trim a previously-saved oversized list down to the freshest entries so a + // device that grew unbounded before the cap existed recovers on load. + if (cached.length > _maxDiscoveredContacts) { + cached.sort((a, b) => b.lastSeen.compareTo(a.lastSeen)); + cached.removeRange(_maxDiscoveredContacts, cached.length); + unawaited(_discoveryContactStore.saveContacts(cached)); + } _discoveredContacts ..clear() ..addAll(cached); @@ -2558,6 +2574,8 @@ class MeshCoreConnector extends ChangeNotifier { _selfName = null; _selfLatitude = null; _selfLongitude = null; + _lastZeroHopAdvertLatitude = null; + _lastZeroHopAdvertLongitude = null; _awaitingSelfInfo = false; _webInitialHandshakeRequestSent = false; _selfInfoRetryTimer?.cancel(); @@ -2724,6 +2742,8 @@ class MeshCoreConnector extends ChangeNotifier { _selfName = null; _selfLatitude = null; _selfLongitude = null; + _lastZeroHopAdvertLatitude = null; + _lastZeroHopAdvertLongitude = null; _clientRepeat = null; _rememberedNonRepeatRadioState = null; _firmwareVerCode = null; @@ -3883,6 +3903,11 @@ class MeshCoreConnector extends ChangeNotifier { Future sendSelfAdvert({bool flood = true}) async { if (!isConnected) return; await sendFrame(buildSendSelfAdvertFrame(flood: flood)); + if (!flood) { + _lastZeroHopAdvertAt = DateTime.now(); + _lastZeroHopAdvertLatitude = _selfLatitude; + _lastZeroHopAdvertLongitude = _selfLongitude; + } } Future rebootDevice() async { @@ -4346,6 +4371,42 @@ class MeshCoreConnector extends ChangeNotifier { tag: 'Connector', ); } + + const locationChangeEpsilon = 2.25e-4; // ~25 meters in degrees. + final lastAdvertLatitude = _lastZeroHopAdvertLatitude; + final lastAdvertLongitude = _lastZeroHopAdvertLongitude; + final currentLatitude = _selfLatitude; + final currentLongitude = _selfLongitude; + final latChanged = + lastAdvertLatitude != null && + currentLatitude != null && + (currentLatitude - lastAdvertLatitude).abs() >= locationChangeEpsilon; + final lonChanged = + lastAdvertLongitude != null && + currentLongitude != null && + (currentLongitude - lastAdvertLongitude).abs() >= locationChangeEpsilon; + final gpsSampleChanged = + hasValidLocation(currentLatitude, currentLongitude) && + (!hasValidLocation(lastAdvertLatitude, lastAdvertLongitude) || + latChanged || + lonChanged); + final effectiveGpsIntervalSeconds = + _appSettingsService?.resolvedGpsIntervalSeconds(_currentCustomVars) ?? + 0; + final timeSinceLastZeroHopAdvert = DateTime.now().difference( + _lastZeroHopAdvertAt, + ); + final shouldAutoSendZeroHopAdvert = + (gpsSampleChanged || (_clientRepeat ?? false)) && + _advertLocPolicy == 1 && + (_appSettingsService?.settings.autoSendZeroHopAdvertOnGpsUpdate ?? + false) && + effectiveGpsIntervalSeconds > 0 && + timeSinceLastZeroHopAdvert.inSeconds >= effectiveGpsIntervalSeconds; + if (shouldAutoSendZeroHopAdvert) { + unawaited(sendSelfAdvert(flood: false)); + } + final selfName = _selfName?.trim(); if (_activeTransport == MeshCoreTransportType.usb && selfName != null && @@ -7139,6 +7200,10 @@ class MeshCoreConnector extends ChangeNotifier { isActive: addActive, flags: 0, ); + + if (_discoveredContacts.length >= _maxDiscoveredContacts) { + _evictStalestDiscoveredContact(); + } _discoveredContacts.add(disContact); unawaited(_persistDiscoveredContacts()); @@ -7156,6 +7221,19 @@ class MeshCoreConnector extends ChangeNotifier { } } + void _evictStalestDiscoveredContact() { + if (_discoveredContacts.isEmpty) return; + var stalestIndex = 0; + for (int i = 1; i < _discoveredContacts.length; i++) { + if (_discoveredContacts[i].lastSeen.isBefore( + _discoveredContacts[stalestIndex].lastSeen, + )) { + stalestIndex = i; + } + } + _discoveredContacts.removeAt(stalestIndex); + } + void removeAllDiscoveredContacts() { _discoveredContacts.clear(); unawaited(_persistDiscoveredContacts()); diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index c9923e75..b98e3c5d 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -100,6 +100,8 @@ "settings_locationInvalid": "Невалидна ширина или дължина.", "settings_latitude": "Широчина", "settings_longitude": "Дължина", + "settings_autoZeroHopAdvertOnGpsUpdate": "Автоматична zero-hop обява при GPS обновяване", + "settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Когато GPS местоположението се промени, изпрати zero-hop обява (изисква местоположение в обявата).", "settings_privacyMode": "Режим на поверителност", "settings_privacyModeSubtitle": "Скриване на име/местоположение в рекламите", "settings_privacyModeToggle": "Активирайте режим на поверителност, за да скриете името и местоположението си в рекламите.", @@ -240,6 +242,19 @@ "appSettings_last6Hours": "Последни 6 часа", "appSettings_last24Hours": "Последно 24 часа", "appSettings_lastWeek": "Миналата седмица", + "appSettings_rasterTileSource": "Източник на растерни плочки", + "appSettings_stadiaEndpoint": "Крайна точка на Stadia", + "appSettings_stadiaApiKey": "API ключ за Stadia", + "appSettings_stadiaApiKeyRequired": "Задължително за използване на Stadia Maps", + "appSettings_stadiaApiKeyConfigured": "Конфигурирано: {maskedKey}", + "@appSettings_stadiaApiKeyConfigured": { + "placeholders": { + "maskedKey": { + "type": "String" + } + } + }, + "appSettings_stadiaApiKeyDialogDescription": "Въведете своя API ключ за Stadia Maps. Приложението го използва за заявки за растерни плочки.", "appSettings_offlineMapCache": "Кеш на офлайн карти", "appSettings_noAreaSelected": "Няма избрана област", "appSettings_areaSelectedZoom": "Избрана е област (мащаб {minZoom}-{maxZoom})", @@ -769,6 +784,56 @@ } } }, + "mapCache_cachedTilesLabel": "Cached tiles", + "mapCache_cachedTileSummaryLabel": "Cached tile summary", + "mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.", + "@mapCache_bulkDownloadDisabledForSource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.", + "@mapCache_bulkDownloadDisabledInConfig": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summarySource": "Source: {source}", + "@mapCache_summarySource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}", + "@mapCache_summaryCachedTilesForSource": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}", + "@mapCache_summaryCachedInSelection": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryApproxCacheSize": "Approx cache size: {size}", + "@mapCache_summaryApproxCacheSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, "mapCache_boundsLabel": "Север {north}, Юг {south}, Изток {east}, Запад {west}", "@mapCache_boundsLabel": { "placeholders": { diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index dc4c9dab..f5cc3685 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -100,6 +100,8 @@ "settings_locationInvalid": "Ungültiger Breiten- oder Längengrad.", "settings_latitude": "Breitengrad", "settings_longitude": "Längengrad", + "settings_autoZeroHopAdvertOnGpsUpdate": "Automatische Zero-Hop-Ankündigung bei GPS-Update", + "settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Wenn sich der GPS-Standort ändert, eine Zero-Hop-Ankündigung senden (erfordert Standort in der Ankündigung).", "settings_privacyMode": "Privatsphärenmodus", "settings_privacyModeSubtitle": "Name und Standort in Ankündigungen verbergen", "settings_privacyModeToggle": "Privatsphärenmodus aktivieren, um Namen und Standort in Ankündigungen zu verbergen.", @@ -240,6 +242,19 @@ "appSettings_last6Hours": "Letzte 6 Stunden", "appSettings_last24Hours": "Letzte 24 Stunden", "appSettings_lastWeek": "Letzte Woche", + "appSettings_rasterTileSource": "Quelle für Rasterkacheln", + "appSettings_stadiaEndpoint": "Stadia-Endpunkt", + "appSettings_stadiaApiKey": "Stadia-API-Schlüssel", + "appSettings_stadiaApiKeyRequired": "Für die Nutzung von Stadia Maps erforderlich", + "appSettings_stadiaApiKeyConfigured": "Konfiguriert: {maskedKey}", + "@appSettings_stadiaApiKeyConfigured": { + "placeholders": { + "maskedKey": { + "type": "String" + } + } + }, + "appSettings_stadiaApiKeyDialogDescription": "Geben Sie Ihren Stadia-Maps-API-Schlüssel ein. Die App verwendet ihn für Rasterkachel-Anfragen.", "appSettings_offlineMapCache": "Offline-Karten-Cache", "appSettings_noAreaSelected": "Kein Bereich ausgewählt", "appSettings_areaSelectedZoom": "Bereich ausgewählt (Zoom {minZoom}-{maxZoom})", @@ -791,6 +806,56 @@ } } }, + "mapCache_cachedTilesLabel": "Cached tiles", + "mapCache_cachedTileSummaryLabel": "Cached tile summary", + "mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.", + "@mapCache_bulkDownloadDisabledForSource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.", + "@mapCache_bulkDownloadDisabledInConfig": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summarySource": "Source: {source}", + "@mapCache_summarySource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}", + "@mapCache_summaryCachedTilesForSource": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}", + "@mapCache_summaryCachedInSelection": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryApproxCacheSize": "Approx cache size: {size}", + "@mapCache_summaryApproxCacheSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "@mapCache_boundsLabel": { "placeholders": { diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 85f3f49d..9c9c8286 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -206,6 +206,8 @@ "settings_telemetryEnvironmentMode": "Telemetry Environment Mode", "settings_advertLocation": "Advert Location", "settings_advertLocationSubtitle": "Include location in advert.", + "settings_autoZeroHopAdvertOnGpsUpdate": "Auto Zero-Hop Advert On GPS Update", + "settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "When GPS location changes, send a zero-hop advert (requires Advert Location).", "settings_multiAck": "Multi-ACKs", "settings_telemetryModeUpdated": "Telemetry mode updated", "settings_actions": "Actions", @@ -365,6 +367,19 @@ "appSettings_last6Hours": "Last 6 hours", "appSettings_last24Hours": "Last 24 hours", "appSettings_lastWeek": "Last week", + "appSettings_rasterTileSource": "Raster Tile Source", + "appSettings_stadiaEndpoint": "Stadia Endpoint", + "appSettings_stadiaApiKey": "Stadia API Key", + "appSettings_stadiaApiKeyRequired": "Required for Stadia Maps usage", + "appSettings_stadiaApiKeyConfigured": "Configured: {maskedKey}", + "@appSettings_stadiaApiKeyConfigured": { + "placeholders": { + "maskedKey": { + "type": "String" + } + } + }, + "appSettings_stadiaApiKeyDialogDescription": "Enter your Stadia Maps API key. This app uses it for raster tile requests.", "appSettings_offlineMapCache": "Offline Map Cache", "appSettings_unitsTitle": "Units", "appSettings_unitsMetric": "Metric (m / km)", @@ -723,6 +738,7 @@ } }, "chat_sendGif": "Send GIF", + "chat_receivedGif": "Received a GIF", "chat_reply": "Reply", "chat_addReaction": "Add Reaction", "chat_me": "Me", @@ -1095,6 +1111,56 @@ } } }, + "mapCache_cachedTilesLabel": "Cached tiles", + "mapCache_cachedTileSummaryLabel": "Cached tile summary", + "mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.", + "@mapCache_bulkDownloadDisabledForSource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.", + "@mapCache_bulkDownloadDisabledInConfig": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summarySource": "Source: {source}", + "@mapCache_summarySource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}", + "@mapCache_summaryCachedTilesForSource": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}", + "@mapCache_summaryCachedInSelection": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryApproxCacheSize": "Approx cache size: {size}", + "@mapCache_summaryApproxCacheSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "@mapCache_boundsLabel": { "placeholders": { @@ -1413,6 +1479,23 @@ "repeater_advancedSettingsSubtitle": "Tuning knobs for experienced operators", "repeater_pathHashMode": "Path hash mode", "repeater_pathHashModeHelper": "Bytes used to encode this repeater's ID in flood path/loop-detect tags. 0=1 byte (256 IDs, up to 64 hops), 1=2 bytes (65K IDs, up to 32 hops), 2=3 bytes (16M IDs, up to 21 hops). Firmware before v1.14 always used 1-byte paths; v1.14 and newer can be configured for 2- or 3-byte paths.", + "repeater_keySettings": "Change Identity Keys", + "repeater_keySettingsSubtitle": "Change the public/private keypair", + "repeater_prvKey": "Private key", + "repeater_prvKeyHelper": "A new private key for the repeater, a 128-character hex string.", + "repeater_generatePrvKey": "Generate a random keypair", + "repeater_stopGeneratingPrvKey": "Interrupt search for keypair", + "repeater_pubKey": "Public key", + "repeater_pubKeyHelper": "This is the public key that goes with the generated private key. You can't set this directly.", + "repeater_pubKeyPrefix": "Desired prefix", + "repeater_pubKeyPrefixHelper": "Find a public key that starts with these hex digits. Expected tries needed: {tries}.", + "@repeater_pubKeyPrefixHelper": { + "placeholders": { + "tries": { + "type": "int" + } + } + }, "repeater_txDelay": "Flood TX delay", "repeater_txDelayHelper": "Retransmit spacing for flood traffic, as a multiplier of the packet's airtime (0-2, default 0.5). Higher = fewer collisions but slower delivery.", "repeater_directTxDelay": "Direct TX delay", @@ -1593,7 +1676,7 @@ "repeater_cliHelpPowersavingOnOff": "Enables or disables powersaving mode (where supported).", "repeater_cliHelpErase": "(Serial only) Formats the device file system. Wipes all settings and contacts.", "repeater_cliHelpSetDutyCycle": "Sets the maximum allowed transmit duty cycle as a percentage (1-100). Internally adjusts the airtime factor.", - "repeater_cliHelpSetPrvKey": "(Serial only) Replaces the device identity private key. Reboot required to apply. Generates a new public key.", + "repeater_cliHelpSetPrvKey": "Replaces the device identity private key. Reboot required to apply. Generates a new public key.", "repeater_cliHelpSetRadioRxGain": "(SX126x only) Toggles boosted RX gain for improved sensitivity at higher current draw.", "repeater_cliHelpSetOwnerInfo": "Sets the owner contact info string included in adverts. Use '|' for newlines.", "repeater_cliHelpSetPathHashMode": "Sets the path-hash mode. 0 = legacy, 1 = standard, 2 = strict. Affects how routing paths are matched.", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 303f8cb5..f181ad1b 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -100,6 +100,8 @@ "settings_locationInvalid": "Latitud o longitud inválidos.", "settings_latitude": "Latitud", "settings_longitude": "Longitud", + "settings_autoZeroHopAdvertOnGpsUpdate": "Anuncio de cero saltos automático al actualizar GPS", + "settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Cuando cambie la ubicación GPS, enviar un anuncio de cero saltos (requiere ubicación en anuncio).", "settings_privacyMode": "Modo de privacidad", "settings_privacyModeSubtitle": "Ocultar nombre/ubicación en anuncios", "settings_privacyModeToggle": "Activar el modo de privacidad para ocultar tu nombre y ubicación en los anuncios.", @@ -240,6 +242,19 @@ "appSettings_last6Hours": "Últimas 6 horas", "appSettings_last24Hours": "Últimas 24 horas", "appSettings_lastWeek": "La semana pasada", + "appSettings_rasterTileSource": "Fuente de teselas ráster", + "appSettings_stadiaEndpoint": "Punto de acceso de Stadia", + "appSettings_stadiaApiKey": "Clave API de Stadia", + "appSettings_stadiaApiKeyRequired": "Obligatorio para usar Stadia Maps", + "appSettings_stadiaApiKeyConfigured": "Configurado: {maskedKey}", + "@appSettings_stadiaApiKeyConfigured": { + "placeholders": { + "maskedKey": { + "type": "String" + } + } + }, + "appSettings_stadiaApiKeyDialogDescription": "Introduce tu clave API de Stadia Maps. La aplicación la usa para solicitar teselas ráster.", "appSettings_offlineMapCache": "Caché de mapa sin conexión", "appSettings_noAreaSelected": "No se ha seleccionado ningún área", "appSettings_areaSelectedZoom": "Área seleccionada (zoom {minZoom}-{maxZoom})", @@ -791,6 +806,56 @@ } } }, + "mapCache_cachedTilesLabel": "Cached tiles", + "mapCache_cachedTileSummaryLabel": "Cached tile summary", + "mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.", + "@mapCache_bulkDownloadDisabledForSource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.", + "@mapCache_bulkDownloadDisabledInConfig": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summarySource": "Source: {source}", + "@mapCache_summarySource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}", + "@mapCache_summaryCachedTilesForSource": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}", + "@mapCache_summaryCachedInSelection": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryApproxCacheSize": "Approx cache size: {size}", + "@mapCache_summaryApproxCacheSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "@mapCache_boundsLabel": { "placeholders": { diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 2c90f639..24dbf401 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -100,6 +100,8 @@ "settings_locationInvalid": "Latitude ou longitude invalide.", "settings_latitude": "Latitude", "settings_longitude": "Longitude", + "settings_autoZeroHopAdvertOnGpsUpdate": "Annonce zéro saut automatique lors de la mise à jour GPS", + "settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Lorsque la position GPS change, envoyer une annonce zéro saut (nécessite la position dans l'annonce).", "settings_privacyMode": "Mode de confidentialité", "settings_privacyModeSubtitle": "Masquer le nom et la localisation dans les annonces", "settings_privacyModeToggle": "Activez le mode confidentialité pour masquer votre nom et votre localisation dans les annonces.", @@ -240,6 +242,19 @@ "appSettings_last6Hours": "Dernières 6 heures", "appSettings_last24Hours": "Dernières 24 heures", "appSettings_lastWeek": "La semaine dernière", + "appSettings_rasterTileSource": "Source de tuiles raster", + "appSettings_stadiaEndpoint": "Point de terminaison Stadia", + "appSettings_stadiaApiKey": "Clé API Stadia", + "appSettings_stadiaApiKeyRequired": "Requis pour l’utilisation de Stadia Maps", + "appSettings_stadiaApiKeyConfigured": "Configuré : {maskedKey}", + "@appSettings_stadiaApiKeyConfigured": { + "placeholders": { + "maskedKey": { + "type": "String" + } + } + }, + "appSettings_stadiaApiKeyDialogDescription": "Entrez votre clé API Stadia Maps. Cette application l’utilise pour les requêtes de tuiles raster.", "appSettings_offlineMapCache": "Cache de carte hors ligne", "appSettings_noAreaSelected": "Aucune zone sélectionnée", "appSettings_areaSelectedZoom": "Zone sélectionnée (zoom {minZoom}-{maxZoom})", @@ -791,6 +806,56 @@ } } }, + "mapCache_cachedTilesLabel": "Cached tiles", + "mapCache_cachedTileSummaryLabel": "Cached tile summary", + "mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.", + "@mapCache_bulkDownloadDisabledForSource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.", + "@mapCache_bulkDownloadDisabledInConfig": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summarySource": "Source: {source}", + "@mapCache_summarySource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}", + "@mapCache_summaryCachedTilesForSource": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}", + "@mapCache_summaryCachedInSelection": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryApproxCacheSize": "Approx cache size: {size}", + "@mapCache_summaryApproxCacheSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, "mapCache_boundsLabel": "N {north}, S {south}, E {east}, O {west}", "@mapCache_boundsLabel": { "placeholders": { diff --git a/lib/l10n/app_hu.arb b/lib/l10n/app_hu.arb index 044f1490..c174c44c 100644 --- a/lib/l10n/app_hu.arb +++ b/lib/l10n/app_hu.arb @@ -161,6 +161,8 @@ "settings_locationIntervalInvalid": "Az intervallumnak legalább 60 másodpercnek kell lennie, és kevesebbnek kell lennie 86 400 másodpercnél.", "settings_latitude": "Szélesség", "settings_longitude": "Hosszúság", + "settings_autoZeroHopAdvertOnGpsUpdate": "Automatikus zero-hop hirdetés GPS-frissítéskor", + "settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Ha a GPS-helyzet megváltozik, küldjön zero-hop hirdetést (a hirdetésben helymegadás szükséges).", "settings_contactSettings": "Kapcsolati beállítások", "settings_contactSettingsSubtitle": "A névjegyek hozzáadásának beállításai.", "settings_privacyMode": "Adatvédelmi mód", @@ -332,6 +334,19 @@ "appSettings_last6Hours": "Utolsó 6 óra", "appSettings_last24Hours": "Az elmúlt 24 óra", "appSettings_lastWeek": "Múlt héten", + "appSettings_rasterTileSource": "Raszteres csempeforrás", + "appSettings_stadiaEndpoint": "Stadia végpont", + "appSettings_stadiaApiKey": "Stadia API-kulcs", + "appSettings_stadiaApiKeyRequired": "A Stadia Maps használatához kötelező", + "appSettings_stadiaApiKeyConfigured": "Konfigurálva: {maskedKey}", + "@appSettings_stadiaApiKeyConfigured": { + "placeholders": { + "maskedKey": { + "type": "String" + } + } + }, + "appSettings_stadiaApiKeyDialogDescription": "Adja meg a Stadia Maps API-kulcsát. Az alkalmazás ezt használja raszteres csempekérésekhez.", "appSettings_offlineMapCache": "Offline térképgyorsítótár", "appSettings_unitsTitle": "Egységek", "appSettings_unitsMetric": "Metrikus (m/km)", @@ -952,6 +967,56 @@ } } }, + "mapCache_cachedTilesLabel": "Cached tiles", + "mapCache_cachedTileSummaryLabel": "Cached tile summary", + "mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.", + "@mapCache_bulkDownloadDisabledForSource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.", + "@mapCache_bulkDownloadDisabledInConfig": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summarySource": "Source: {source}", + "@mapCache_summarySource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}", + "@mapCache_summaryCachedTilesForSource": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}", + "@mapCache_summaryCachedInSelection": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryApproxCacheSize": "Approx cache size: {size}", + "@mapCache_summaryApproxCacheSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, "mapCache_boundsLabel": "É {north}, D {south}, K {east}, Ny {west}", "@mapCache_boundsLabel": { "placeholders": { diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index c61bae5a..00de2511 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -240,6 +240,19 @@ "appSettings_last6Hours": "Ultimi 6 ore", "appSettings_last24Hours": "Ultime 24 ore", "appSettings_lastWeek": "La settimana scorsa", + "appSettings_rasterTileSource": "Sorgente tile raster", + "appSettings_stadiaEndpoint": "Endpoint Stadia", + "appSettings_stadiaApiKey": "Chiave API Stadia", + "appSettings_stadiaApiKeyRequired": "Obbligatoria per usare Stadia Maps", + "appSettings_stadiaApiKeyConfigured": "Configurata: {maskedKey}", + "@appSettings_stadiaApiKeyConfigured": { + "placeholders": { + "maskedKey": { + "type": "String" + } + } + }, + "appSettings_stadiaApiKeyDialogDescription": "Inserisci la chiave API di Stadia Maps. Questa app la usa per le richieste di tile raster.", "appSettings_offlineMapCache": "Cache Mappa Offline", "appSettings_noAreaSelected": "Nessun'area selezionata", "appSettings_areaSelectedZoom": "Area selezionata (zoom {minZoom}-{maxZoom})", @@ -837,6 +850,56 @@ } } }, + "mapCache_cachedTilesLabel": "Cached tiles", + "mapCache_cachedTileSummaryLabel": "Cached tile summary", + "mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.", + "@mapCache_bulkDownloadDisabledForSource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.", + "@mapCache_bulkDownloadDisabledInConfig": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summarySource": "Source: {source}", + "@mapCache_summarySource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}", + "@mapCache_summaryCachedTilesForSource": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}", + "@mapCache_summaryCachedInSelection": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryApproxCacheSize": "Approx cache size: {size}", + "@mapCache_summaryApproxCacheSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "@mapCache_boundsLabel": { "placeholders": { @@ -2207,6 +2270,8 @@ "settings_telemetryEnvironmentMode": "Modalità di ambiente di telemetria", "settings_advertLocation": "Posizione dell'annuncio", "settings_advertLocationSubtitle": "Includi la posizione nell'annuncio", + "settings_autoZeroHopAdvertOnGpsUpdate": "Annuncio zero-hop automatico all'aggiornamento GPS", + "settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Quando la posizione GPS cambia, invia un annuncio zero-hop (richiede la posizione nell'annuncio).", "settings_privacy": "Impostazioni sulla privacy", "settings_denyAll": "Negare tutto", "settings_privacySubtitle": "Controlla le informazioni che vengono condivise.", diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 1c8b728f..51021664 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -167,6 +167,8 @@ "settings_locationIntervalInvalid": "間隔は少なくとも60秒で、86400秒未満でなければなりません。", "settings_latitude": "緯度", "settings_longitude": "経度", + "settings_autoZeroHopAdvertOnGpsUpdate": "GPS更新時にゼロホップ広告を自動送信", + "settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "GPS位置が変化したときにゼロホップ広告を送信します(広告への位置情報の含有が必要)。", "settings_contactSettings": "連絡先設定", "settings_contactSettingsSubtitle": "連絡先の追加方法に関する設定", "settings_privacyMode": "プライバシーモード", @@ -338,6 +340,19 @@ "appSettings_last6Hours": "過去6時間", "appSettings_last24Hours": "過去24時間", "appSettings_lastWeek": "過去1週間", + "appSettings_rasterTileSource": "ラスタタイルのソース", + "appSettings_stadiaEndpoint": "Stadia エンドポイント", + "appSettings_stadiaApiKey": "Stadia API キー", + "appSettings_stadiaApiKeyRequired": "Stadia Maps の利用に必要です", + "appSettings_stadiaApiKeyConfigured": "設定済み: {maskedKey}", + "@appSettings_stadiaApiKeyConfigured": { + "placeholders": { + "maskedKey": { + "type": "String" + } + } + }, + "appSettings_stadiaApiKeyDialogDescription": "Stadia Maps の API キーを入力してください。このアプリはラスタタイルの取得に使用します。", "appSettings_offlineMapCache": "オフライン地図キャッシュ", "appSettings_unitsTitle": "単位", "appSettings_unitsMetric": "メートル法 (m / km)", @@ -970,6 +985,56 @@ } } }, + "mapCache_cachedTilesLabel": "Cached tiles", + "mapCache_cachedTileSummaryLabel": "Cached tile summary", + "mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.", + "@mapCache_bulkDownloadDisabledForSource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.", + "@mapCache_bulkDownloadDisabledInConfig": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summarySource": "Source: {source}", + "@mapCache_summarySource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}", + "@mapCache_summaryCachedTilesForSource": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}", + "@mapCache_summaryCachedInSelection": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryApproxCacheSize": "Approx cache size: {size}", + "@mapCache_summaryApproxCacheSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "@mapCache_boundsLabel": { "placeholders": { diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index d22e8072..d619973e 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -161,6 +161,8 @@ "settings_locationIntervalInvalid": "간격은 최소 60초 이상, 86400초 미만이어야 합니다.", "settings_latitude": "위도", "settings_longitude": "경도", + "settings_autoZeroHopAdvertOnGpsUpdate": "GPS 업데이트 시 제로 홉 광고 자동 전송", + "settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "GPS 위치가 변경되면 제로 홉 광고를 전송합니다(광고에 위치 포함 필요).", "settings_contactSettings": "연락처 설정", "settings_contactSettingsSubtitle": "연락처 추가 방식 설정", "settings_privacyMode": "개인 정보 보호 모드", @@ -332,6 +334,19 @@ "appSettings_last6Hours": "지난 6시간", "appSettings_last24Hours": "지난 24시간", "appSettings_lastWeek": "지난 주", + "appSettings_rasterTileSource": "래스터 타일 소스", + "appSettings_stadiaEndpoint": "Stadia 엔드포인트", + "appSettings_stadiaApiKey": "Stadia API 키", + "appSettings_stadiaApiKeyRequired": "Stadia Maps를 사용하려면 필요합니다", + "appSettings_stadiaApiKeyConfigured": "설정됨: {maskedKey}", + "@appSettings_stadiaApiKeyConfigured": { + "placeholders": { + "maskedKey": { + "type": "String" + } + } + }, + "appSettings_stadiaApiKeyDialogDescription": "Stadia Maps API 키를 입력하세요. 이 앱은 래스터 타일 요청에 이 키를 사용합니다.", "appSettings_offlineMapCache": "오프라인 지도 캐시", "appSettings_unitsTitle": "단위", "appSettings_unitsMetric": "미터법 (m / km)", @@ -952,6 +967,56 @@ } } }, + "mapCache_cachedTilesLabel": "Cached tiles", + "mapCache_cachedTileSummaryLabel": "Cached tile summary", + "mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.", + "@mapCache_bulkDownloadDisabledForSource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.", + "@mapCache_bulkDownloadDisabledInConfig": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summarySource": "Source: {source}", + "@mapCache_summarySource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}", + "@mapCache_summaryCachedTilesForSource": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}", + "@mapCache_summaryCachedInSelection": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryApproxCacheSize": "Approx cache size: {size}", + "@mapCache_summaryApproxCacheSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "@mapCache_boundsLabel": { "placeholders": { diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 87ad67a4..2ad4638e 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1030,6 +1030,18 @@ abstract class AppLocalizations { /// **'Include location in advert.'** String get settings_advertLocationSubtitle; + /// No description provided for @settings_autoZeroHopAdvertOnGpsUpdate. + /// + /// In en, this message translates to: + /// **'Auto Zero-Hop Advert On GPS Update'** + String get settings_autoZeroHopAdvertOnGpsUpdate; + + /// No description provided for @settings_autoZeroHopAdvertOnGpsUpdateSubtitle. + /// + /// In en, this message translates to: + /// **'When GPS location changes, send a zero-hop advert (requires Advert Location).'** + String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle; + /// No description provided for @settings_multiAck. /// /// In en, this message translates to: @@ -1816,6 +1828,42 @@ abstract class AppLocalizations { /// **'Last week'** String get appSettings_lastWeek; + /// No description provided for @appSettings_rasterTileSource. + /// + /// In en, this message translates to: + /// **'Raster Tile Source'** + String get appSettings_rasterTileSource; + + /// No description provided for @appSettings_stadiaEndpoint. + /// + /// In en, this message translates to: + /// **'Stadia Endpoint'** + String get appSettings_stadiaEndpoint; + + /// No description provided for @appSettings_stadiaApiKey. + /// + /// In en, this message translates to: + /// **'Stadia API Key'** + String get appSettings_stadiaApiKey; + + /// No description provided for @appSettings_stadiaApiKeyRequired. + /// + /// In en, this message translates to: + /// **'Required for Stadia Maps usage'** + String get appSettings_stadiaApiKeyRequired; + + /// No description provided for @appSettings_stadiaApiKeyConfigured. + /// + /// In en, this message translates to: + /// **'Configured: {maskedKey}'** + String appSettings_stadiaApiKeyConfigured(String maskedKey); + + /// No description provided for @appSettings_stadiaApiKeyDialogDescription. + /// + /// In en, this message translates to: + /// **'Enter your Stadia Maps API key. This app uses it for raster tile requests.'** + String get appSettings_stadiaApiKeyDialogDescription; + /// No description provided for @appSettings_offlineMapCache. /// /// In en, this message translates to: @@ -2668,6 +2716,12 @@ abstract class AppLocalizations { /// **'Send GIF'** String get chat_sendGif; + /// No description provided for @chat_receivedGif. + /// + /// In en, this message translates to: + /// **'Received a GIF'** + String get chat_receivedGif; + /// No description provided for @chat_reply. /// /// In en, this message translates to: @@ -3820,6 +3874,54 @@ abstract class AppLocalizations { /// **'Failed downloads: {count}'** String mapCache_failedDownloads(int count); + /// No description provided for @mapCache_cachedTilesLabel. + /// + /// In en, this message translates to: + /// **'Cached tiles'** + String get mapCache_cachedTilesLabel; + + /// No description provided for @mapCache_cachedTileSummaryLabel. + /// + /// In en, this message translates to: + /// **'Cached tile summary'** + String get mapCache_cachedTileSummaryLabel; + + /// No description provided for @mapCache_bulkDownloadDisabledForSource. + /// + /// In en, this message translates to: + /// **'Offline bulk downloads are disabled for {source}.'** + String mapCache_bulkDownloadDisabledForSource(String source); + + /// No description provided for @mapCache_bulkDownloadDisabledInConfig. + /// + /// In en, this message translates to: + /// **'Offline bulk downloads are disabled for {source} in this app configuration.'** + String mapCache_bulkDownloadDisabledInConfig(String source); + + /// No description provided for @mapCache_summarySource. + /// + /// In en, this message translates to: + /// **'Source: {source}'** + String mapCache_summarySource(String source); + + /// No description provided for @mapCache_summaryCachedTilesForSource. + /// + /// In en, this message translates to: + /// **'Cached tiles for source: {count}'** + String mapCache_summaryCachedTilesForSource(int count); + + /// No description provided for @mapCache_summaryCachedInSelection. + /// + /// In en, this message translates to: + /// **'Cached in selected area/zoom: {count}'** + String mapCache_summaryCachedInSelection(int count); + + /// No description provided for @mapCache_summaryApproxCacheSize. + /// + /// In en, this message translates to: + /// **'Approx cache size: {size}'** + String mapCache_summaryApproxCacheSize(String size); + /// No description provided for @mapCache_boundsLabel. /// /// In en, this message translates to: @@ -4772,6 +4874,66 @@ abstract class AppLocalizations { /// **'Bytes used to encode this repeater\'s ID in flood path/loop-detect tags. 0=1 byte (256 IDs, up to 64 hops), 1=2 bytes (65K IDs, up to 32 hops), 2=3 bytes (16M IDs, up to 21 hops). Firmware before v1.14 always used 1-byte paths; v1.14 and newer can be configured for 2- or 3-byte paths.'** String get repeater_pathHashModeHelper; + /// No description provided for @repeater_keySettings. + /// + /// In en, this message translates to: + /// **'Change Identity Keys'** + String get repeater_keySettings; + + /// No description provided for @repeater_keySettingsSubtitle. + /// + /// In en, this message translates to: + /// **'Change the public/private keypair'** + String get repeater_keySettingsSubtitle; + + /// No description provided for @repeater_prvKey. + /// + /// In en, this message translates to: + /// **'Private key'** + String get repeater_prvKey; + + /// No description provided for @repeater_prvKeyHelper. + /// + /// In en, this message translates to: + /// **'A new private key for the repeater, a 128-character hex string.'** + String get repeater_prvKeyHelper; + + /// No description provided for @repeater_generatePrvKey. + /// + /// In en, this message translates to: + /// **'Generate a random keypair'** + String get repeater_generatePrvKey; + + /// No description provided for @repeater_stopGeneratingPrvKey. + /// + /// In en, this message translates to: + /// **'Interrupt search for keypair'** + String get repeater_stopGeneratingPrvKey; + + /// No description provided for @repeater_pubKey. + /// + /// In en, this message translates to: + /// **'Public key'** + String get repeater_pubKey; + + /// No description provided for @repeater_pubKeyHelper. + /// + /// In en, this message translates to: + /// **'This is the public key that goes with the generated private key. You can\'t set this directly.'** + String get repeater_pubKeyHelper; + + /// No description provided for @repeater_pubKeyPrefix. + /// + /// In en, this message translates to: + /// **'Desired prefix'** + String get repeater_pubKeyPrefix; + + /// No description provided for @repeater_pubKeyPrefixHelper. + /// + /// In en, this message translates to: + /// **'Find a public key that starts with these hex digits. Expected tries needed: {tries}.'** + String repeater_pubKeyPrefixHelper(int tries); + /// No description provided for @repeater_txDelay. /// /// In en, this message translates to: @@ -5543,7 +5705,7 @@ abstract class AppLocalizations { /// No description provided for @repeater_cliHelpSetPrvKey. /// /// In en, this message translates to: - /// **'(Serial only) Replaces the device identity private key. Reboot required to apply. Generates a new public key.'** + /// **'Replaces the device identity private key. Reboot required to apply. Generates a new public key.'** String get repeater_cliHelpSetPrvKey; /// No description provided for @repeater_cliHelpSetRadioRxGain. diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index f06e5055..41c11438 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -506,6 +506,14 @@ class AppLocalizationsBg extends AppLocalizations { String get settings_advertLocationSubtitle => 'Включи местоположение в обявата'; + @override + String get settings_autoZeroHopAdvertOnGpsUpdate => + 'Автоматична zero-hop обява при GPS обновяване'; + + @override + String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle => + 'Когато GPS местоположението се промени, изпрати zero-hop обява (изисква местоположение в обявата).'; + @override String get settings_multiAck => 'Множество ACK'; @@ -951,6 +959,28 @@ class AppLocalizationsBg extends AppLocalizations { @override String get appSettings_lastWeek => 'Миналата седмица'; + @override + String get appSettings_rasterTileSource => 'Източник на растерни плочки'; + + @override + String get appSettings_stadiaEndpoint => 'Крайна точка на Stadia'; + + @override + String get appSettings_stadiaApiKey => 'API ключ за Stadia'; + + @override + String get appSettings_stadiaApiKeyRequired => + 'Задължително за използване на Stadia Maps'; + + @override + String appSettings_stadiaApiKeyConfigured(String maskedKey) { + return 'Конфигурирано: $maskedKey'; + } + + @override + String get appSettings_stadiaApiKeyDialogDescription => + 'Въведете своя API ключ за Stadia Maps. Приложението го използва за заявки за растерни плочки.'; + @override String get appSettings_offlineMapCache => 'Кеш на офлайн карти'; @@ -1450,6 +1480,9 @@ class AppLocalizationsBg extends AppLocalizations { @override String get chat_sendGif => 'Изпрати GIF'; + @override + String get chat_receivedGif => 'Received a GIF'; + @override String get chat_reply => 'Отговори'; @@ -2102,6 +2135,42 @@ class AppLocalizationsBg extends AppLocalizations { return 'Неуспешни изтегляния: $count'; } + @override + String get mapCache_cachedTilesLabel => 'Cached tiles'; + + @override + String get mapCache_cachedTileSummaryLabel => 'Cached tile summary'; + + @override + String mapCache_bulkDownloadDisabledForSource(String source) { + return 'Offline bulk downloads are disabled for $source.'; + } + + @override + String mapCache_bulkDownloadDisabledInConfig(String source) { + return 'Offline bulk downloads are disabled for $source in this app configuration.'; + } + + @override + String mapCache_summarySource(String source) { + return 'Source: $source'; + } + + @override + String mapCache_summaryCachedTilesForSource(int count) { + return 'Cached tiles for source: $count'; + } + + @override + String mapCache_summaryCachedInSelection(int count) { + return 'Cached in selected area/zoom: $count'; + } + + @override + String mapCache_summaryApproxCacheSize(String size) { + return 'Approx cache size: $size'; + } + @override String mapCache_boundsLabel( String north, @@ -2651,6 +2720,41 @@ class AppLocalizationsBg extends AppLocalizations { String get repeater_pathHashModeHelper => 'Байтовете, използвани за кодиране на идентификатора на този повторител в таговете за откриване на потоци/цикли, са: 0=1 байт (256 идентификатора, до 64 скока), 1=2 байта (65 000 идентификатора, до 32 скока), 2=3 байта (16 милиона идентификатора, до 21 скока). Версиите 1.13 и по-старите фърмуери използват многобайтови пътища - само след като мрежата е актуализирана до версия 1.14 или по-нова.'; + @override + String get repeater_keySettings => 'Change Identity Keys'; + + @override + String get repeater_keySettingsSubtitle => + 'Change the public/private keypair'; + + @override + String get repeater_prvKey => 'Private key'; + + @override + String get repeater_prvKeyHelper => + 'A new private key for the repeater, a 128-character hex string.'; + + @override + String get repeater_generatePrvKey => 'Generate a random keypair'; + + @override + String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair'; + + @override + String get repeater_pubKey => 'Public key'; + + @override + String get repeater_pubKeyHelper => + 'This is the public key that goes with the generated private key. You can\'t set this directly.'; + + @override + String get repeater_pubKeyPrefix => 'Desired prefix'; + + @override + String repeater_pubKeyPrefixHelper(int tries) { + return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.'; + } + @override String get repeater_txDelay => 'Забавяне на Flood TX'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 0a59487e..5da89506 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -506,6 +506,14 @@ class AppLocalizationsDe extends AppLocalizations { String get settings_advertLocationSubtitle => 'Standort in die Ankündigung einschließen.'; + @override + String get settings_autoZeroHopAdvertOnGpsUpdate => + 'Automatische Zero-Hop-Ankündigung bei GPS-Update'; + + @override + String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle => + 'Wenn sich der GPS-Standort ändert, eine Zero-Hop-Ankündigung senden (erfordert Standort in der Ankündigung).'; + @override String get settings_multiAck => 'Mehrfach-ACKs'; @@ -948,6 +956,28 @@ class AppLocalizationsDe extends AppLocalizations { @override String get appSettings_lastWeek => 'Letzte Woche'; + @override + String get appSettings_rasterTileSource => 'Quelle für Rasterkacheln'; + + @override + String get appSettings_stadiaEndpoint => 'Stadia-Endpunkt'; + + @override + String get appSettings_stadiaApiKey => 'Stadia-API-Schlüssel'; + + @override + String get appSettings_stadiaApiKeyRequired => + 'Für die Nutzung von Stadia Maps erforderlich'; + + @override + String appSettings_stadiaApiKeyConfigured(String maskedKey) { + return 'Konfiguriert: $maskedKey'; + } + + @override + String get appSettings_stadiaApiKeyDialogDescription => + 'Geben Sie Ihren Stadia-Maps-API-Schlüssel ein. Die App verwendet ihn für Rasterkachel-Anfragen.'; + @override String get appSettings_offlineMapCache => 'Offline-Karten-Cache'; @@ -1451,6 +1481,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get chat_sendGif => 'GIF senden'; + @override + String get chat_receivedGif => 'Received a GIF'; + @override String get chat_reply => 'Antworten'; @@ -2102,6 +2135,42 @@ class AppLocalizationsDe extends AppLocalizations { return 'Fehlgeschlagene Downloads: $count'; } + @override + String get mapCache_cachedTilesLabel => 'Cached tiles'; + + @override + String get mapCache_cachedTileSummaryLabel => 'Cached tile summary'; + + @override + String mapCache_bulkDownloadDisabledForSource(String source) { + return 'Offline bulk downloads are disabled for $source.'; + } + + @override + String mapCache_bulkDownloadDisabledInConfig(String source) { + return 'Offline bulk downloads are disabled for $source in this app configuration.'; + } + + @override + String mapCache_summarySource(String source) { + return 'Source: $source'; + } + + @override + String mapCache_summaryCachedTilesForSource(int count) { + return 'Cached tiles for source: $count'; + } + + @override + String mapCache_summaryCachedInSelection(int count) { + return 'Cached in selected area/zoom: $count'; + } + + @override + String mapCache_summaryApproxCacheSize(String size) { + return 'Approx cache size: $size'; + } + @override String mapCache_boundsLabel( String north, @@ -2647,6 +2716,41 @@ class AppLocalizationsDe extends AppLocalizations { String get repeater_pathHashModeHelper => 'Bytes, die zur Kodierung der ID dieses Repeaters in Flood-Pfad-/Schleifen-Erkennung-Tags verwendet werden. 0 = 1 Byte (256 IDs, bis zu 64 Hops), 1 = 2 Bytes (65.000 IDs, bis zu 32 Hops), 2 = 3 Bytes (16 Millionen IDs, bis zu 21 Hops). Firmware-Versionen 1.13 und älter verwenden mehrstellige Pfade – ab Version 1.14+ wird nur ein Pfad erstellt, sobald das Netzwerk aktiv ist.'; + @override + String get repeater_keySettings => 'Change Identity Keys'; + + @override + String get repeater_keySettingsSubtitle => + 'Change the public/private keypair'; + + @override + String get repeater_prvKey => 'Private key'; + + @override + String get repeater_prvKeyHelper => + 'A new private key for the repeater, a 128-character hex string.'; + + @override + String get repeater_generatePrvKey => 'Generate a random keypair'; + + @override + String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair'; + + @override + String get repeater_pubKey => 'Public key'; + + @override + String get repeater_pubKeyHelper => + 'This is the public key that goes with the generated private key. You can\'t set this directly.'; + + @override + String get repeater_pubKeyPrefix => 'Desired prefix'; + + @override + String repeater_pubKeyPrefixHelper(int tries) { + return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.'; + } + @override String get repeater_txDelay => 'Flood-TX-Verzögerung'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 4fa895e7..ccdd9ed0 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -496,6 +496,14 @@ class AppLocalizationsEn extends AppLocalizations { @override String get settings_advertLocationSubtitle => 'Include location in advert.'; + @override + String get settings_autoZeroHopAdvertOnGpsUpdate => + 'Auto Zero-Hop Advert On GPS Update'; + + @override + String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle => + 'When GPS location changes, send a zero-hop advert (requires Advert Location).'; + @override String get settings_multiAck => 'Multi-ACKs'; @@ -931,6 +939,28 @@ class AppLocalizationsEn extends AppLocalizations { @override String get appSettings_lastWeek => 'Last week'; + @override + String get appSettings_rasterTileSource => 'Raster Tile Source'; + + @override + String get appSettings_stadiaEndpoint => 'Stadia Endpoint'; + + @override + String get appSettings_stadiaApiKey => 'Stadia API Key'; + + @override + String get appSettings_stadiaApiKeyRequired => + 'Required for Stadia Maps usage'; + + @override + String appSettings_stadiaApiKeyConfigured(String maskedKey) { + return 'Configured: $maskedKey'; + } + + @override + String get appSettings_stadiaApiKeyDialogDescription => + 'Enter your Stadia Maps API key. This app uses it for raster tile requests.'; + @override String get appSettings_offlineMapCache => 'Offline Map Cache'; @@ -1420,6 +1450,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get chat_sendGif => 'Send GIF'; + @override + String get chat_receivedGif => 'Received a GIF'; + @override String get chat_reply => 'Reply'; @@ -2063,6 +2096,42 @@ class AppLocalizationsEn extends AppLocalizations { return 'Failed downloads: $count'; } + @override + String get mapCache_cachedTilesLabel => 'Cached tiles'; + + @override + String get mapCache_cachedTileSummaryLabel => 'Cached tile summary'; + + @override + String mapCache_bulkDownloadDisabledForSource(String source) { + return 'Offline bulk downloads are disabled for $source.'; + } + + @override + String mapCache_bulkDownloadDisabledInConfig(String source) { + return 'Offline bulk downloads are disabled for $source in this app configuration.'; + } + + @override + String mapCache_summarySource(String source) { + return 'Source: $source'; + } + + @override + String mapCache_summaryCachedTilesForSource(int count) { + return 'Cached tiles for source: $count'; + } + + @override + String mapCache_summaryCachedInSelection(int count) { + return 'Cached in selected area/zoom: $count'; + } + + @override + String mapCache_summaryApproxCacheSize(String size) { + return 'Approx cache size: $size'; + } + @override String mapCache_boundsLabel( String north, @@ -2601,6 +2670,41 @@ class AppLocalizationsEn extends AppLocalizations { String get repeater_pathHashModeHelper => 'Bytes used to encode this repeater\'s ID in flood path/loop-detect tags. 0=1 byte (256 IDs, up to 64 hops), 1=2 bytes (65K IDs, up to 32 hops), 2=3 bytes (16M IDs, up to 21 hops). Firmware before v1.14 always used 1-byte paths; v1.14 and newer can be configured for 2- or 3-byte paths.'; + @override + String get repeater_keySettings => 'Change Identity Keys'; + + @override + String get repeater_keySettingsSubtitle => + 'Change the public/private keypair'; + + @override + String get repeater_prvKey => 'Private key'; + + @override + String get repeater_prvKeyHelper => + 'A new private key for the repeater, a 128-character hex string.'; + + @override + String get repeater_generatePrvKey => 'Generate a random keypair'; + + @override + String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair'; + + @override + String get repeater_pubKey => 'Public key'; + + @override + String get repeater_pubKeyHelper => + 'This is the public key that goes with the generated private key. You can\'t set this directly.'; + + @override + String get repeater_pubKeyPrefix => 'Desired prefix'; + + @override + String repeater_pubKeyPrefixHelper(int tries) { + return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.'; + } + @override String get repeater_txDelay => 'Flood TX delay'; @@ -3071,7 +3175,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get repeater_cliHelpSetPrvKey => - '(Serial only) Replaces the device identity private key. Reboot required to apply. Generates a new public key.'; + 'Replaces the device identity private key. Reboot required to apply. Generates a new public key.'; @override String get repeater_cliHelpSetRadioRxGain => diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 0897a684..da679126 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -503,6 +503,14 @@ class AppLocalizationsEs extends AppLocalizations { @override String get settings_advertLocationSubtitle => 'Incluir ubicación en anuncio'; + @override + String get settings_autoZeroHopAdvertOnGpsUpdate => + 'Anuncio de cero saltos automático al actualizar GPS'; + + @override + String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle => + 'Cuando cambie la ubicación GPS, enviar un anuncio de cero saltos (requiere ubicación en anuncio).'; + @override String get settings_multiAck => 'Múltiples respuestas de confirmación'; @@ -945,6 +953,28 @@ class AppLocalizationsEs extends AppLocalizations { @override String get appSettings_lastWeek => 'La semana pasada'; + @override + String get appSettings_rasterTileSource => 'Fuente de teselas ráster'; + + @override + String get appSettings_stadiaEndpoint => 'Punto de acceso de Stadia'; + + @override + String get appSettings_stadiaApiKey => 'Clave API de Stadia'; + + @override + String get appSettings_stadiaApiKeyRequired => + 'Obligatorio para usar Stadia Maps'; + + @override + String appSettings_stadiaApiKeyConfigured(String maskedKey) { + return 'Configurado: $maskedKey'; + } + + @override + String get appSettings_stadiaApiKeyDialogDescription => + 'Introduce tu clave API de Stadia Maps. La aplicación la usa para solicitar teselas ráster.'; + @override String get appSettings_offlineMapCache => 'Caché de mapa sin conexión'; @@ -1447,6 +1477,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get chat_sendGif => 'Enviar GIF'; + @override + String get chat_receivedGif => 'Received a GIF'; + @override String get chat_reply => 'Responder'; @@ -2096,6 +2129,42 @@ class AppLocalizationsEs extends AppLocalizations { return 'Descargas fallidas: $count'; } + @override + String get mapCache_cachedTilesLabel => 'Cached tiles'; + + @override + String get mapCache_cachedTileSummaryLabel => 'Cached tile summary'; + + @override + String mapCache_bulkDownloadDisabledForSource(String source) { + return 'Offline bulk downloads are disabled for $source.'; + } + + @override + String mapCache_bulkDownloadDisabledInConfig(String source) { + return 'Offline bulk downloads are disabled for $source in this app configuration.'; + } + + @override + String mapCache_summarySource(String source) { + return 'Source: $source'; + } + + @override + String mapCache_summaryCachedTilesForSource(int count) { + return 'Cached tiles for source: $count'; + } + + @override + String mapCache_summaryCachedInSelection(int count) { + return 'Cached in selected area/zoom: $count'; + } + + @override + String mapCache_summaryApproxCacheSize(String size) { + return 'Approx cache size: $size'; + } + @override String mapCache_boundsLabel( String north, @@ -2646,6 +2715,41 @@ class AppLocalizationsEs extends AppLocalizations { String get repeater_pathHashModeHelper => 'Bytes utilizados para codificar el ID de este repetidor en las etiquetas de ruta/detección de bucles. 0=1 byte (256 IDs, hasta 64 saltos), 1=2 bytes (65.000 IDs, hasta 32 saltos), 2=3 bytes (16 millones de IDs, hasta 21 saltos). Las versiones 1.13 y anteriores de firmware eliminan rutas de múltiples bytes; solo se detectan una vez que la red está activa en la versión 1.14 o posterior.'; + @override + String get repeater_keySettings => 'Change Identity Keys'; + + @override + String get repeater_keySettingsSubtitle => + 'Change the public/private keypair'; + + @override + String get repeater_prvKey => 'Private key'; + + @override + String get repeater_prvKeyHelper => + 'A new private key for the repeater, a 128-character hex string.'; + + @override + String get repeater_generatePrvKey => 'Generate a random keypair'; + + @override + String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair'; + + @override + String get repeater_pubKey => 'Public key'; + + @override + String get repeater_pubKeyHelper => + 'This is the public key that goes with the generated private key. You can\'t set this directly.'; + + @override + String get repeater_pubKeyPrefix => 'Desired prefix'; + + @override + String repeater_pubKeyPrefixHelper(int tries) { + return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.'; + } + @override String get repeater_txDelay => 'Retraso TX por inundación'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index eb960ea4..29022552 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -507,6 +507,14 @@ class AppLocalizationsFr extends AppLocalizations { String get settings_advertLocationSubtitle => 'Inclure la localisation dans l\'annonce'; + @override + String get settings_autoZeroHopAdvertOnGpsUpdate => + 'Annonce zéro saut automatique lors de la mise à jour GPS'; + + @override + String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle => + 'Lorsque la position GPS change, envoyer une annonce zéro saut (nécessite la position dans l\'annonce).'; + @override String get settings_multiAck => 'Plusieurs accusés de réception'; @@ -949,6 +957,28 @@ class AppLocalizationsFr extends AppLocalizations { @override String get appSettings_lastWeek => 'La semaine dernière'; + @override + String get appSettings_rasterTileSource => 'Source de tuiles raster'; + + @override + String get appSettings_stadiaEndpoint => 'Point de terminaison Stadia'; + + @override + String get appSettings_stadiaApiKey => 'Clé API Stadia'; + + @override + String get appSettings_stadiaApiKeyRequired => + 'Requis pour l’utilisation de Stadia Maps'; + + @override + String appSettings_stadiaApiKeyConfigured(String maskedKey) { + return 'Configuré : $maskedKey'; + } + + @override + String get appSettings_stadiaApiKeyDialogDescription => + 'Entrez votre clé API Stadia Maps. Cette application l’utilise pour les requêtes de tuiles raster.'; + @override String get appSettings_offlineMapCache => 'Cache de carte hors ligne'; @@ -1449,6 +1479,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get chat_sendGif => 'Envoyer un GIF'; + @override + String get chat_receivedGif => 'Received a GIF'; + @override String get chat_reply => 'Répondre'; @@ -2103,6 +2136,42 @@ class AppLocalizationsFr extends AppLocalizations { return 'Téléchargements échoués : $count'; } + @override + String get mapCache_cachedTilesLabel => 'Cached tiles'; + + @override + String get mapCache_cachedTileSummaryLabel => 'Cached tile summary'; + + @override + String mapCache_bulkDownloadDisabledForSource(String source) { + return 'Offline bulk downloads are disabled for $source.'; + } + + @override + String mapCache_bulkDownloadDisabledInConfig(String source) { + return 'Offline bulk downloads are disabled for $source in this app configuration.'; + } + + @override + String mapCache_summarySource(String source) { + return 'Source: $source'; + } + + @override + String mapCache_summaryCachedTilesForSource(int count) { + return 'Cached tiles for source: $count'; + } + + @override + String mapCache_summaryCachedInSelection(int count) { + return 'Cached in selected area/zoom: $count'; + } + + @override + String mapCache_summaryApproxCacheSize(String size) { + return 'Approx cache size: $size'; + } + @override String mapCache_boundsLabel( String north, @@ -2657,6 +2726,41 @@ class AppLocalizationsFr extends AppLocalizations { String get repeater_pathHashModeHelper => 'Octets utilisés pour encoder l\'ID de ce routeur dans les balises de détection de flux/boucles. 0 = 1 octet (256 ID, jusqu\'à 64 sauts), 1 = 2 octets (65 000 ID, jusqu\'à 32 sauts), 2 = 3 octets (16 millions d\'ID, jusqu\'à 21 sauts). Les versions 1.13 et antérieures utilisent des chemins multi-octets ; à partir de la version 1.14, cela n\'est plus nécessaire.'; + @override + String get repeater_keySettings => 'Change Identity Keys'; + + @override + String get repeater_keySettingsSubtitle => + 'Change the public/private keypair'; + + @override + String get repeater_prvKey => 'Private key'; + + @override + String get repeater_prvKeyHelper => + 'A new private key for the repeater, a 128-character hex string.'; + + @override + String get repeater_generatePrvKey => 'Generate a random keypair'; + + @override + String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair'; + + @override + String get repeater_pubKey => 'Public key'; + + @override + String get repeater_pubKeyHelper => + 'This is the public key that goes with the generated private key. You can\'t set this directly.'; + + @override + String get repeater_pubKeyPrefix => 'Desired prefix'; + + @override + String repeater_pubKeyPrefixHelper(int tries) { + return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.'; + } + @override String get repeater_txDelay => 'Délai de transmission en flood'; diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index c1188965..7fa5c541 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -502,6 +502,14 @@ class AppLocalizationsHu extends AppLocalizations { String get settings_advertLocationSubtitle => 'A hirdetésben szerepeljen a helyszín.'; + @override + String get settings_autoZeroHopAdvertOnGpsUpdate => + 'Automatikus zero-hop hirdetés GPS-frissítéskor'; + + @override + String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle => + 'Ha a GPS-helyzet megváltozik, küldjön zero-hop hirdetést (a hirdetésben helymegadás szükséges).'; + @override String get settings_multiAck => 'Multi-ACK'; @@ -941,6 +949,28 @@ class AppLocalizationsHu extends AppLocalizations { @override String get appSettings_lastWeek => 'Múlt héten'; + @override + String get appSettings_rasterTileSource => 'Raszteres csempeforrás'; + + @override + String get appSettings_stadiaEndpoint => 'Stadia végpont'; + + @override + String get appSettings_stadiaApiKey => 'Stadia API-kulcs'; + + @override + String get appSettings_stadiaApiKeyRequired => + 'A Stadia Maps használatához kötelező'; + + @override + String appSettings_stadiaApiKeyConfigured(String maskedKey) { + return 'Konfigurálva: $maskedKey'; + } + + @override + String get appSettings_stadiaApiKeyDialogDescription => + 'Adja meg a Stadia Maps API-kulcsát. Az alkalmazás ezt használja raszteres csempekérésekhez.'; + @override String get appSettings_offlineMapCache => 'Offline térképgyorsítótár'; @@ -1442,6 +1472,9 @@ class AppLocalizationsHu extends AppLocalizations { @override String get chat_sendGif => 'GIF küldése'; + @override + String get chat_receivedGif => 'Received a GIF'; + @override String get chat_reply => 'Válasz'; @@ -2094,6 +2127,42 @@ class AppLocalizationsHu extends AppLocalizations { return 'Sikertelen letöltések: $count'; } + @override + String get mapCache_cachedTilesLabel => 'Cached tiles'; + + @override + String get mapCache_cachedTileSummaryLabel => 'Cached tile summary'; + + @override + String mapCache_bulkDownloadDisabledForSource(String source) { + return 'Offline bulk downloads are disabled for $source.'; + } + + @override + String mapCache_bulkDownloadDisabledInConfig(String source) { + return 'Offline bulk downloads are disabled for $source in this app configuration.'; + } + + @override + String mapCache_summarySource(String source) { + return 'Source: $source'; + } + + @override + String mapCache_summaryCachedTilesForSource(int count) { + return 'Cached tiles for source: $count'; + } + + @override + String mapCache_summaryCachedInSelection(int count) { + return 'Cached in selected area/zoom: $count'; + } + + @override + String mapCache_summaryApproxCacheSize(String size) { + return 'Approx cache size: $size'; + } + @override String mapCache_boundsLabel( String north, @@ -2640,6 +2709,41 @@ class AppLocalizationsHu extends AppLocalizations { String get repeater_pathHashModeHelper => 'Az átjátszó azonosítójának elárasztási útvonal/hurokészlelési címkékbe való kódolására használt bájtok. 0 = 1 bájt (256 azonosító, legfeljebb 64 ugrás), 1 = 2 bájt (65 000 azonosító, legfeljebb 32 ugrás), 2 = 3 bájt (16 millió azonosító, legfeljebb 21 ugrás). A v1.13 és régebbi firmware eldobja a többbájtos elérési utat – csak akkor emelje meg, ha a hálózat a v1.14+ verziót használja.'; + @override + String get repeater_keySettings => 'Change Identity Keys'; + + @override + String get repeater_keySettingsSubtitle => + 'Change the public/private keypair'; + + @override + String get repeater_prvKey => 'Private key'; + + @override + String get repeater_prvKeyHelper => + 'A new private key for the repeater, a 128-character hex string.'; + + @override + String get repeater_generatePrvKey => 'Generate a random keypair'; + + @override + String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair'; + + @override + String get repeater_pubKey => 'Public key'; + + @override + String get repeater_pubKeyHelper => + 'This is the public key that goes with the generated private key. You can\'t set this directly.'; + + @override + String get repeater_pubKeyPrefix => 'Desired prefix'; + + @override + String repeater_pubKeyPrefixHelper(int tries) { + return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.'; + } + @override String get repeater_txDelay => 'Áradási TX késleltetés'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 30d47357..0e01ff91 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -507,6 +507,14 @@ class AppLocalizationsIt extends AppLocalizations { String get settings_advertLocationSubtitle => 'Includi la posizione nell\'annuncio'; + @override + String get settings_autoZeroHopAdvertOnGpsUpdate => + 'Annuncio zero-hop automatico all\'aggiornamento GPS'; + + @override + String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle => + 'Quando la posizione GPS cambia, invia un annuncio zero-hop (richiede la posizione nell\'annuncio).'; + @override String get settings_multiAck => 'ACK multipli'; @@ -948,6 +956,28 @@ class AppLocalizationsIt extends AppLocalizations { @override String get appSettings_lastWeek => 'La settimana scorsa'; + @override + String get appSettings_rasterTileSource => 'Sorgente tile raster'; + + @override + String get appSettings_stadiaEndpoint => 'Endpoint Stadia'; + + @override + String get appSettings_stadiaApiKey => 'Chiave API Stadia'; + + @override + String get appSettings_stadiaApiKeyRequired => + 'Obbligatoria per usare Stadia Maps'; + + @override + String appSettings_stadiaApiKeyConfigured(String maskedKey) { + return 'Configurata: $maskedKey'; + } + + @override + String get appSettings_stadiaApiKeyDialogDescription => + 'Inserisci la chiave API di Stadia Maps. Questa app la usa per le richieste di tile raster.'; + @override String get appSettings_offlineMapCache => 'Cache Mappa Offline'; @@ -1448,6 +1478,9 @@ class AppLocalizationsIt extends AppLocalizations { @override String get chat_sendGif => 'Invia GIF'; + @override + String get chat_receivedGif => 'Received a GIF'; + @override String get chat_reply => 'Rispondi'; @@ -2098,6 +2131,42 @@ class AppLocalizationsIt extends AppLocalizations { return 'Download falliti: $count'; } + @override + String get mapCache_cachedTilesLabel => 'Cached tiles'; + + @override + String get mapCache_cachedTileSummaryLabel => 'Cached tile summary'; + + @override + String mapCache_bulkDownloadDisabledForSource(String source) { + return 'Offline bulk downloads are disabled for $source.'; + } + + @override + String mapCache_bulkDownloadDisabledInConfig(String source) { + return 'Offline bulk downloads are disabled for $source in this app configuration.'; + } + + @override + String mapCache_summarySource(String source) { + return 'Source: $source'; + } + + @override + String mapCache_summaryCachedTilesForSource(int count) { + return 'Cached tiles for source: $count'; + } + + @override + String mapCache_summaryCachedInSelection(int count) { + return 'Cached in selected area/zoom: $count'; + } + + @override + String mapCache_summaryApproxCacheSize(String size) { + return 'Approx cache size: $size'; + } + @override String mapCache_boundsLabel( String north, @@ -2648,6 +2717,41 @@ class AppLocalizationsIt extends AppLocalizations { String get repeater_pathHashModeHelper => 'Byte utilizzati per codificare l\'ID di questo ripetitore nei tag di percorso flood/rilevamento loop. 0=1 byte (256 ID, fino a 64 salti), 1=2 byte (65.000 ID, fino a 32 salti), 2=3 byte (16 milioni di ID, fino a 21 salti). Il firmware precedente alla v1.14 usava sempre percorsi a 1 byte; v1.14 e versioni successive possono essere configurate per percorsi a 2 o 3 byte.'; + @override + String get repeater_keySettings => 'Change Identity Keys'; + + @override + String get repeater_keySettingsSubtitle => + 'Change the public/private keypair'; + + @override + String get repeater_prvKey => 'Private key'; + + @override + String get repeater_prvKeyHelper => + 'A new private key for the repeater, a 128-character hex string.'; + + @override + String get repeater_generatePrvKey => 'Generate a random keypair'; + + @override + String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair'; + + @override + String get repeater_pubKey => 'Public key'; + + @override + String get repeater_pubKeyHelper => + 'This is the public key that goes with the generated private key. You can\'t set this directly.'; + + @override + String get repeater_pubKeyPrefix => 'Desired prefix'; + + @override + String repeater_pubKeyPrefixHelper(int tries) { + return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.'; + } + @override String get repeater_txDelay => 'Ritardo a Flood, TX'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 4cc75df7..e16d6419 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -482,6 +482,13 @@ class AppLocalizationsJa extends AppLocalizations { @override String get settings_advertLocationSubtitle => '広告に場所を記載してください。'; + @override + String get settings_autoZeroHopAdvertOnGpsUpdate => 'GPS更新時にゼロホップ広告を自動送信'; + + @override + String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle => + 'GPS位置が変化したときにゼロホップ広告を送信します(広告への位置情報の含有が必要)。'; + @override String get settings_multiAck => 'マルチ ACK'; @@ -896,6 +903,27 @@ class AppLocalizationsJa extends AppLocalizations { @override String get appSettings_lastWeek => '過去1週間'; + @override + String get appSettings_rasterTileSource => 'ラスタタイルのソース'; + + @override + String get appSettings_stadiaEndpoint => 'Stadia エンドポイント'; + + @override + String get appSettings_stadiaApiKey => 'Stadia API キー'; + + @override + String get appSettings_stadiaApiKeyRequired => 'Stadia Maps の利用に必要です'; + + @override + String appSettings_stadiaApiKeyConfigured(String maskedKey) { + return '設定済み: $maskedKey'; + } + + @override + String get appSettings_stadiaApiKeyDialogDescription => + 'Stadia Maps の API キーを入力してください。このアプリはラスタタイルの取得に使用します。'; + @override String get appSettings_offlineMapCache => 'オフライン地図キャッシュ'; @@ -1379,6 +1407,9 @@ class AppLocalizationsJa extends AppLocalizations { @override String get chat_sendGif => 'GIF を送信'; + @override + String get chat_receivedGif => 'Received a GIF'; + @override String get chat_reply => '返信'; @@ -2011,6 +2042,42 @@ class AppLocalizationsJa extends AppLocalizations { return '失敗したダウンロード: $count'; } + @override + String get mapCache_cachedTilesLabel => 'Cached tiles'; + + @override + String get mapCache_cachedTileSummaryLabel => 'Cached tile summary'; + + @override + String mapCache_bulkDownloadDisabledForSource(String source) { + return 'Offline bulk downloads are disabled for $source.'; + } + + @override + String mapCache_bulkDownloadDisabledInConfig(String source) { + return 'Offline bulk downloads are disabled for $source in this app configuration.'; + } + + @override + String mapCache_summarySource(String source) { + return 'Source: $source'; + } + + @override + String mapCache_summaryCachedTilesForSource(int count) { + return 'Cached tiles for source: $count'; + } + + @override + String mapCache_summaryCachedInSelection(int count) { + return 'Cached in selected area/zoom: $count'; + } + + @override + String mapCache_summaryApproxCacheSize(String size) { + return 'Approx cache size: $size'; + } + @override String mapCache_boundsLabel( String north, @@ -2531,6 +2598,41 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_pathHashModeHelper => 'このリピータのIDをフローパス/ループ検出タグにエンコードするために使用されるバイト数。 0=1バイト (256個のID、最大64ホップ)、1=2バイト (65,000個のID、最大32ホップ)、2=3バイト (160万個のID、最大21ホップ)。 v1.13およびそれ以前のファームウェアでは、マルチバイトパスがサポートされていません。 v1.14以降のバージョンでは、一度ネットワークが起動されると、パスが一度だけ検出されます。'; + @override + String get repeater_keySettings => 'Change Identity Keys'; + + @override + String get repeater_keySettingsSubtitle => + 'Change the public/private keypair'; + + @override + String get repeater_prvKey => 'Private key'; + + @override + String get repeater_prvKeyHelper => + 'A new private key for the repeater, a 128-character hex string.'; + + @override + String get repeater_generatePrvKey => 'Generate a random keypair'; + + @override + String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair'; + + @override + String get repeater_pubKey => 'Public key'; + + @override + String get repeater_pubKeyHelper => + 'This is the public key that goes with the generated private key. You can\'t set this directly.'; + + @override + String get repeater_pubKeyPrefix => 'Desired prefix'; + + @override + String repeater_pubKeyPrefixHelper(int tries) { + return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.'; + } + @override String get repeater_txDelay => 'フロイド・TXでの遅延'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 434b3968..209867fe 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -483,6 +483,14 @@ class AppLocalizationsKo extends AppLocalizations { @override String get settings_advertLocationSubtitle => '광고에 위치 정보를 포함하세요.'; + @override + String get settings_autoZeroHopAdvertOnGpsUpdate => + 'GPS 업데이트 시 제로 홉 광고 자동 전송'; + + @override + String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle => + 'GPS 위치가 변경되면 제로 홉 광고를 전송합니다(광고에 위치 포함 필요).'; + @override String get settings_multiAck => '다중 ACK'; @@ -898,6 +906,27 @@ class AppLocalizationsKo extends AppLocalizations { @override String get appSettings_lastWeek => '지난 주'; + @override + String get appSettings_rasterTileSource => '래스터 타일 소스'; + + @override + String get appSettings_stadiaEndpoint => 'Stadia 엔드포인트'; + + @override + String get appSettings_stadiaApiKey => 'Stadia API 키'; + + @override + String get appSettings_stadiaApiKeyRequired => 'Stadia Maps를 사용하려면 필요합니다'; + + @override + String appSettings_stadiaApiKeyConfigured(String maskedKey) { + return '설정됨: $maskedKey'; + } + + @override + String get appSettings_stadiaApiKeyDialogDescription => + 'Stadia Maps API 키를 입력하세요. 이 앱은 래스터 타일 요청에 이 키를 사용합니다.'; + @override String get appSettings_offlineMapCache => '오프라인 지도 캐시'; @@ -1381,6 +1410,9 @@ class AppLocalizationsKo extends AppLocalizations { @override String get chat_sendGif => 'GIF 보내기'; + @override + String get chat_receivedGif => 'Received a GIF'; + @override String get chat_reply => '답변'; @@ -2014,6 +2046,42 @@ class AppLocalizationsKo extends AppLocalizations { return '실패한 다운로드: $count'; } + @override + String get mapCache_cachedTilesLabel => 'Cached tiles'; + + @override + String get mapCache_cachedTileSummaryLabel => 'Cached tile summary'; + + @override + String mapCache_bulkDownloadDisabledForSource(String source) { + return 'Offline bulk downloads are disabled for $source.'; + } + + @override + String mapCache_bulkDownloadDisabledInConfig(String source) { + return 'Offline bulk downloads are disabled for $source in this app configuration.'; + } + + @override + String mapCache_summarySource(String source) { + return 'Source: $source'; + } + + @override + String mapCache_summaryCachedTilesForSource(int count) { + return 'Cached tiles for source: $count'; + } + + @override + String mapCache_summaryCachedInSelection(int count) { + return 'Cached in selected area/zoom: $count'; + } + + @override + String mapCache_summaryApproxCacheSize(String size) { + return 'Approx cache size: $size'; + } + @override String mapCache_boundsLabel( String north, @@ -2536,6 +2604,41 @@ class AppLocalizationsKo extends AppLocalizations { String get repeater_pathHashModeHelper => '이 리피터의 ID를 플러드 경로/루프 감지 태그에 인코딩하는 데 사용되는 바이트 수입니다. 0=1바이트(256개 ID, 최대 64홉), 1=2바이트(65,000개 ID, 최대 32홉), 2=3바이트(1,600만 개 ID, 최대 21홉). v1.14 이전 펌웨어는 항상 1바이트 경로를 사용했으며, v1.14 이상은 2바이트 또는 3바이트 경로로 설정할 수 있습니다.'; + @override + String get repeater_keySettings => 'Change Identity Keys'; + + @override + String get repeater_keySettingsSubtitle => + 'Change the public/private keypair'; + + @override + String get repeater_prvKey => 'Private key'; + + @override + String get repeater_prvKeyHelper => + 'A new private key for the repeater, a 128-character hex string.'; + + @override + String get repeater_generatePrvKey => 'Generate a random keypair'; + + @override + String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair'; + + @override + String get repeater_pubKey => 'Public key'; + + @override + String get repeater_pubKeyHelper => + 'This is the public key that goes with the generated private key. You can\'t set this directly.'; + + @override + String get repeater_pubKeyPrefix => 'Desired prefix'; + + @override + String repeater_pubKeyPrefixHelper(int tries) { + return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.'; + } + @override String get repeater_txDelay => '플러드 TX 지연'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index e84ed4ed..bf33733a 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -501,6 +501,14 @@ class AppLocalizationsNl extends AppLocalizations { String get settings_advertLocationSubtitle => 'Locatie opnemen in advertentie'; + @override + String get settings_autoZeroHopAdvertOnGpsUpdate => + 'Automatische zero-hop-advertentie bij GPS-update'; + + @override + String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle => + 'Wanneer de GPS-locatie verandert, een zero-hop-advertentie verzenden (vereist locatie in advertentie).'; + @override String get settings_multiAck => 'Meerdere bevestigingen'; @@ -940,6 +948,28 @@ class AppLocalizationsNl extends AppLocalizations { @override String get appSettings_lastWeek => 'Afgelopen week'; + @override + String get appSettings_rasterTileSource => 'Rastertegelbron'; + + @override + String get appSettings_stadiaEndpoint => 'Stadia-eindpunt'; + + @override + String get appSettings_stadiaApiKey => 'Stadia API-sleutel'; + + @override + String get appSettings_stadiaApiKeyRequired => + 'Vereist voor gebruik van Stadia Maps'; + + @override + String appSettings_stadiaApiKeyConfigured(String maskedKey) { + return 'Geconfigureerd: $maskedKey'; + } + + @override + String get appSettings_stadiaApiKeyDialogDescription => + 'Voer je Stadia Maps API-sleutel in. De app gebruikt die voor rastertegelverzoeken.'; + @override String get appSettings_offlineMapCache => 'Offline Kaartcache'; @@ -1436,6 +1466,9 @@ class AppLocalizationsNl extends AppLocalizations { @override String get chat_sendGif => 'GIF verzenden'; + @override + String get chat_receivedGif => 'Received a GIF'; + @override String get chat_reply => 'Reageren'; @@ -2086,6 +2119,42 @@ class AppLocalizationsNl extends AppLocalizations { return 'Mislukte downloads: $count'; } + @override + String get mapCache_cachedTilesLabel => 'Cached tiles'; + + @override + String get mapCache_cachedTileSummaryLabel => 'Cached tile summary'; + + @override + String mapCache_bulkDownloadDisabledForSource(String source) { + return 'Offline bulk downloads are disabled for $source.'; + } + + @override + String mapCache_bulkDownloadDisabledInConfig(String source) { + return 'Offline bulk downloads are disabled for $source in this app configuration.'; + } + + @override + String mapCache_summarySource(String source) { + return 'Source: $source'; + } + + @override + String mapCache_summaryCachedTilesForSource(int count) { + return 'Cached tiles for source: $count'; + } + + @override + String mapCache_summaryCachedInSelection(int count) { + return 'Cached in selected area/zoom: $count'; + } + + @override + String mapCache_summaryApproxCacheSize(String size) { + return 'Approx cache size: $size'; + } + @override String mapCache_boundsLabel( String north, @@ -2629,6 +2698,41 @@ class AppLocalizationsNl extends AppLocalizations { String get repeater_pathHashModeHelper => 'Bytes die gebruikt worden om de ID van deze repeater te coderen in flood-pad/lusdetectietags. 0=1 byte (256 ID\'s, tot 64 hops), 1=2 bytes (65.000 ID\'s, tot 32 hops), 2=3 bytes (16 miljoen ID\'s, tot 21 hops). Firmware vóór v1.14 gebruikte altijd 1-byte paden; v1.14 en nieuwer kunnen worden ingesteld op 2- of 3-byte paden.'; + @override + String get repeater_keySettings => 'Change Identity Keys'; + + @override + String get repeater_keySettingsSubtitle => + 'Change the public/private keypair'; + + @override + String get repeater_prvKey => 'Private key'; + + @override + String get repeater_prvKeyHelper => + 'A new private key for the repeater, a 128-character hex string.'; + + @override + String get repeater_generatePrvKey => 'Generate a random keypair'; + + @override + String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair'; + + @override + String get repeater_pubKey => 'Public key'; + + @override + String get repeater_pubKeyHelper => + 'This is the public key that goes with the generated private key. You can\'t set this directly.'; + + @override + String get repeater_pubKeyPrefix => 'Desired prefix'; + + @override + String repeater_pubKeyPrefixHelper(int tries) { + return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.'; + } + @override String get repeater_txDelay => 'Vertraging bij Flood TX'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index d2cf3986..f0f57506 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -508,6 +508,14 @@ class AppLocalizationsPl extends AppLocalizations { String get settings_advertLocationSubtitle => 'Uwzględnij lokalizację w ogłoszeniu'; + @override + String get settings_autoZeroHopAdvertOnGpsUpdate => + 'Automatyczne ogłoszenie zero-hop przy aktualizacji GPS'; + + @override + String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle => + 'Gdy lokalizacja GPS się zmieni, wyślij ogłoszenie zero-hop (wymaga lokalizacji w ogłoszeniu).'; + @override String get settings_multiAck => 'Wielokrotne potwierdzenia odbioru'; @@ -949,6 +957,28 @@ class AppLocalizationsPl extends AppLocalizations { @override String get appSettings_lastWeek => 'Ostatni tydzień'; + @override + String get appSettings_rasterTileSource => 'Źródło kafelków rastrowych'; + + @override + String get appSettings_stadiaEndpoint => 'Punkt końcowy Stadia'; + + @override + String get appSettings_stadiaApiKey => 'Klucz API Stadia'; + + @override + String get appSettings_stadiaApiKeyRequired => + 'Wymagane do korzystania ze Stadia Maps'; + + @override + String appSettings_stadiaApiKeyConfigured(String maskedKey) { + return 'Skonfigurowano: $maskedKey'; + } + + @override + String get appSettings_stadiaApiKeyDialogDescription => + 'Wprowadź swój klucz API Stadia Maps. Aplikacja używa go do żądań kafelków rastrowych.'; + @override String get appSettings_offlineMapCache => 'Pamięć podręczna map offline'; @@ -1459,6 +1489,9 @@ class AppLocalizationsPl extends AppLocalizations { @override String get chat_sendGif => 'Wyślij GIF'; + @override + String get chat_receivedGif => 'Received a GIF'; + @override String get chat_reply => 'Odpowiedz'; @@ -2116,6 +2149,42 @@ class AppLocalizationsPl extends AppLocalizations { return 'Nieudane pobrania: $count'; } + @override + String get mapCache_cachedTilesLabel => 'Cached tiles'; + + @override + String get mapCache_cachedTileSummaryLabel => 'Cached tile summary'; + + @override + String mapCache_bulkDownloadDisabledForSource(String source) { + return 'Offline bulk downloads are disabled for $source.'; + } + + @override + String mapCache_bulkDownloadDisabledInConfig(String source) { + return 'Offline bulk downloads are disabled for $source in this app configuration.'; + } + + @override + String mapCache_summarySource(String source) { + return 'Source: $source'; + } + + @override + String mapCache_summaryCachedTilesForSource(int count) { + return 'Cached tiles for source: $count'; + } + + @override + String mapCache_summaryCachedInSelection(int count) { + return 'Cached in selected area/zoom: $count'; + } + + @override + String mapCache_summaryApproxCacheSize(String size) { + return 'Approx cache size: $size'; + } + @override String mapCache_boundsLabel( String north, @@ -2662,6 +2731,41 @@ class AppLocalizationsPl extends AppLocalizations { String get repeater_pathHashModeHelper => 'Bajty używane do kodowania identyfikatora tego repeatera w tagach ścieżki flood/wykrywania pętli. 0=1 bajt (256 identyfikatorów, do 64 skoków), 1=2 bajty (65 000 identyfikatorów, do 32 skoków), 2=3 bajty (16 milionów identyfikatorów, do 21 skoków). Firmware sprzed v1.14 zawsze używał ścieżek 1-bajtowych; v1.14 i nowsze można skonfigurować na ścieżki 2- lub 3-bajtowe.'; + @override + String get repeater_keySettings => 'Change Identity Keys'; + + @override + String get repeater_keySettingsSubtitle => + 'Change the public/private keypair'; + + @override + String get repeater_prvKey => 'Private key'; + + @override + String get repeater_prvKeyHelper => + 'A new private key for the repeater, a 128-character hex string.'; + + @override + String get repeater_generatePrvKey => 'Generate a random keypair'; + + @override + String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair'; + + @override + String get repeater_pubKey => 'Public key'; + + @override + String get repeater_pubKeyHelper => + 'This is the public key that goes with the generated private key. You can\'t set this directly.'; + + @override + String get repeater_pubKeyPrefix => 'Desired prefix'; + + @override + String repeater_pubKeyPrefixHelper(int tries) { + return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.'; + } + @override String get repeater_txDelay => 'Opóźnienie w Flood, TX'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index de14aa42..55c2fa48 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -505,6 +505,14 @@ class AppLocalizationsPt extends AppLocalizations { String get settings_advertLocationSubtitle => 'Incluir localização no anúncio'; + @override + String get settings_autoZeroHopAdvertOnGpsUpdate => + 'Anúncio zero-hop automático na atualização do GPS'; + + @override + String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle => + 'Quando a localização GPS mudar, enviar um anúncio zero-hop (requer localização no anúncio).'; + @override String get settings_multiAck => 'Multi-ACKs'; @@ -946,6 +954,28 @@ class AppLocalizationsPt extends AppLocalizations { @override String get appSettings_lastWeek => 'Da última semana'; + @override + String get appSettings_rasterTileSource => 'Fonte de blocos raster'; + + @override + String get appSettings_stadiaEndpoint => 'Endpoint da Stadia'; + + @override + String get appSettings_stadiaApiKey => 'Chave da API Stadia'; + + @override + String get appSettings_stadiaApiKeyRequired => + 'Obrigatório para usar o Stadia Maps'; + + @override + String appSettings_stadiaApiKeyConfigured(String maskedKey) { + return 'Configurado: $maskedKey'; + } + + @override + String get appSettings_stadiaApiKeyDialogDescription => + 'Insira sua chave da API Stadia Maps. O aplicativo a usa para solicitações de blocos raster.'; + @override String get appSettings_offlineMapCache => 'Cache de Mapa Offline'; @@ -1446,6 +1476,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get chat_sendGif => 'Enviar GIF'; + @override + String get chat_receivedGif => 'Received a GIF'; + @override String get chat_reply => 'Responder'; @@ -2099,6 +2132,42 @@ class AppLocalizationsPt extends AppLocalizations { return 'Downloads falhas: $count'; } + @override + String get mapCache_cachedTilesLabel => 'Cached tiles'; + + @override + String get mapCache_cachedTileSummaryLabel => 'Cached tile summary'; + + @override + String mapCache_bulkDownloadDisabledForSource(String source) { + return 'Offline bulk downloads are disabled for $source.'; + } + + @override + String mapCache_bulkDownloadDisabledInConfig(String source) { + return 'Offline bulk downloads are disabled for $source in this app configuration.'; + } + + @override + String mapCache_summarySource(String source) { + return 'Source: $source'; + } + + @override + String mapCache_summaryCachedTilesForSource(int count) { + return 'Cached tiles for source: $count'; + } + + @override + String mapCache_summaryCachedInSelection(int count) { + return 'Cached in selected area/zoom: $count'; + } + + @override + String mapCache_summaryApproxCacheSize(String size) { + return 'Approx cache size: $size'; + } + @override String mapCache_boundsLabel( String north, @@ -2647,6 +2716,41 @@ class AppLocalizationsPt extends AppLocalizations { String get repeater_pathHashModeHelper => 'Bytes usados para codificar o ID deste repetidor nas tags de caminho flood/detecção de loop. 0=1 byte (256 IDs, até 64 saltos), 1=2 bytes (65.000 IDs, até 32 saltos), 2=3 bytes (16 milhões de IDs, até 21 saltos). O firmware anterior à v1.14 sempre usava caminhos de 1 byte; v1.14 e versões mais recentes podem ser configuradas para caminhos de 2 ou 3 bytes.'; + @override + String get repeater_keySettings => 'Change Identity Keys'; + + @override + String get repeater_keySettingsSubtitle => + 'Change the public/private keypair'; + + @override + String get repeater_prvKey => 'Private key'; + + @override + String get repeater_prvKeyHelper => + 'A new private key for the repeater, a 128-character hex string.'; + + @override + String get repeater_generatePrvKey => 'Generate a random keypair'; + + @override + String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair'; + + @override + String get repeater_pubKey => 'Public key'; + + @override + String get repeater_pubKeyHelper => + 'This is the public key that goes with the generated private key. You can\'t set this directly.'; + + @override + String get repeater_pubKeyPrefix => 'Desired prefix'; + + @override + String repeater_pubKeyPrefixHelper(int tries) { + return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.'; + } + @override String get repeater_txDelay => 'Atraso na entrega em Flood, TX'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 95f99a91..d32b69a4 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -505,6 +505,14 @@ class AppLocalizationsRu extends AppLocalizations { String get settings_advertLocationSubtitle => 'Включить местоположение в объявление'; + @override + String get settings_autoZeroHopAdvertOnGpsUpdate => + 'Авто-объявление без хопов при обновлении GPS'; + + @override + String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle => + 'Когда GPS-местоположение меняется, отправлять объявление без хопов (требуется геопозиция в объявлении).'; + @override String get settings_multiAck => 'Несколько подтверждений'; @@ -949,6 +957,28 @@ class AppLocalizationsRu extends AppLocalizations { @override String get appSettings_lastWeek => 'Последнюю неделю'; + @override + String get appSettings_rasterTileSource => 'Источник растровых тайлов'; + + @override + String get appSettings_stadiaEndpoint => 'Конечная точка Stadia'; + + @override + String get appSettings_stadiaApiKey => 'Ключ API Stadia'; + + @override + String get appSettings_stadiaApiKeyRequired => + 'Требуется для использования Stadia Maps'; + + @override + String appSettings_stadiaApiKeyConfigured(String maskedKey) { + return 'Настроено: $maskedKey'; + } + + @override + String get appSettings_stadiaApiKeyDialogDescription => + 'Введите свой ключ API Stadia Maps. Приложение использует его для запросов растровых тайлов.'; + @override String get appSettings_offlineMapCache => 'Кэш офлайн-карты'; @@ -1447,6 +1477,9 @@ class AppLocalizationsRu extends AppLocalizations { @override String get chat_sendGif => 'Отправить GIF'; + @override + String get chat_receivedGif => 'Received a GIF'; + @override String get chat_reply => 'Ответить'; @@ -2103,6 +2136,42 @@ class AppLocalizationsRu extends AppLocalizations { return 'Неудачных загрузок: $count'; } + @override + String get mapCache_cachedTilesLabel => 'Cached tiles'; + + @override + String get mapCache_cachedTileSummaryLabel => 'Cached tile summary'; + + @override + String mapCache_bulkDownloadDisabledForSource(String source) { + return 'Offline bulk downloads are disabled for $source.'; + } + + @override + String mapCache_bulkDownloadDisabledInConfig(String source) { + return 'Offline bulk downloads are disabled for $source in this app configuration.'; + } + + @override + String mapCache_summarySource(String source) { + return 'Source: $source'; + } + + @override + String mapCache_summaryCachedTilesForSource(int count) { + return 'Cached tiles for source: $count'; + } + + @override + String mapCache_summaryCachedInSelection(int count) { + return 'Cached in selected area/zoom: $count'; + } + + @override + String mapCache_summaryApproxCacheSize(String size) { + return 'Approx cache size: $size'; + } + @override String mapCache_boundsLabel( String north, @@ -2652,6 +2721,41 @@ class AppLocalizationsRu extends AppLocalizations { String get repeater_pathHashModeHelper => 'Байты, используемые для кодирования идентификатора этого ретранслятора в тегах flood-маршрута/обнаружения циклов. 0 = 1 байт (256 идентификаторов, до 64 переходов), 1 = 2 байта (65 000 идентификаторов, до 32 переходов), 2 = 3 байта (16 миллионов идентификаторов, до 21 перехода). Прошивки до v1.14 всегда использовали 1-байтовые маршруты; v1.14 и новее можно настроить на 2- или 3-байтовые маршруты.'; + @override + String get repeater_keySettings => 'Change Identity Keys'; + + @override + String get repeater_keySettingsSubtitle => + 'Change the public/private keypair'; + + @override + String get repeater_prvKey => 'Private key'; + + @override + String get repeater_prvKeyHelper => + 'A new private key for the repeater, a 128-character hex string.'; + + @override + String get repeater_generatePrvKey => 'Generate a random keypair'; + + @override + String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair'; + + @override + String get repeater_pubKey => 'Public key'; + + @override + String get repeater_pubKeyHelper => + 'This is the public key that goes with the generated private key. You can\'t set this directly.'; + + @override + String get repeater_pubKeyPrefix => 'Desired prefix'; + + @override + String repeater_pubKeyPrefixHelper(int tries) { + return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.'; + } + @override String get repeater_txDelay => 'Задержка в работе системы Flood TX'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 941d6078..5ee8ec33 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -499,6 +499,14 @@ class AppLocalizationsSk extends AppLocalizations { @override String get settings_advertLocationSubtitle => 'Zahrnúť polohu do inzerátu'; + @override + String get settings_autoZeroHopAdvertOnGpsUpdate => + 'Automatický zero-hop inzerát pri aktualizácii GPS'; + + @override + String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle => + 'Keď sa GPS poloha zmení, odoslať zero-hop inzerát (vyžaduje polohu v inzeráte).'; + @override String get settings_multiAck => 'Viaceré ACK'; @@ -936,6 +944,28 @@ class AppLocalizationsSk extends AppLocalizations { @override String get appSettings_lastWeek => 'Minul týždeň'; + @override + String get appSettings_rasterTileSource => 'Zdroj rastrových dlaždíc'; + + @override + String get appSettings_stadiaEndpoint => 'Koncový bod Stadia'; + + @override + String get appSettings_stadiaApiKey => 'Kľúč API Stadia'; + + @override + String get appSettings_stadiaApiKeyRequired => + 'Vyžaduje sa na používanie Stadia Maps'; + + @override + String appSettings_stadiaApiKeyConfigured(String maskedKey) { + return 'Nakonfigurované: $maskedKey'; + } + + @override + String get appSettings_stadiaApiKeyDialogDescription => + 'Zadajte svoj kľúč API pre Stadia Maps. Aplikácia ho používa na požiadavky na rastrové dlaždice.'; + @override String get appSettings_offlineMapCache => 'Offline Mapa Pamäť'; @@ -1436,6 +1466,9 @@ class AppLocalizationsSk extends AppLocalizations { @override String get chat_sendGif => 'Odoslať GIF'; + @override + String get chat_receivedGif => 'Received a GIF'; + @override String get chat_reply => 'Odpovedať'; @@ -2089,6 +2122,42 @@ class AppLocalizationsSk extends AppLocalizations { return 'Neúspešné stiahnutia: $count'; } + @override + String get mapCache_cachedTilesLabel => 'Cached tiles'; + + @override + String get mapCache_cachedTileSummaryLabel => 'Cached tile summary'; + + @override + String mapCache_bulkDownloadDisabledForSource(String source) { + return 'Offline bulk downloads are disabled for $source.'; + } + + @override + String mapCache_bulkDownloadDisabledInConfig(String source) { + return 'Offline bulk downloads are disabled for $source in this app configuration.'; + } + + @override + String mapCache_summarySource(String source) { + return 'Source: $source'; + } + + @override + String mapCache_summaryCachedTilesForSource(int count) { + return 'Cached tiles for source: $count'; + } + + @override + String mapCache_summaryCachedInSelection(int count) { + return 'Cached in selected area/zoom: $count'; + } + + @override + String mapCache_summaryApproxCacheSize(String size) { + return 'Approx cache size: $size'; + } + @override String mapCache_boundsLabel( String north, @@ -2633,6 +2702,41 @@ class AppLocalizationsSk extends AppLocalizations { String get repeater_pathHashModeHelper => 'Bajty použité na zakódovanie ID tohto opakovača v tagoch flood trasy/detekcie slučky. 0=1 bajt (256 ID, až 64 skokov), 1=2 bajty (65 000 ID, až 32 skokov), 2=3 bajty (16 miliónov ID, až 21 skokov). Firmvér pred v1.14 vždy používal 1-bajtové trasy; v1.14 a novšie možno nakonfigurovať na 2- alebo 3-bajtové trasy.'; + @override + String get repeater_keySettings => 'Change Identity Keys'; + + @override + String get repeater_keySettingsSubtitle => + 'Change the public/private keypair'; + + @override + String get repeater_prvKey => 'Private key'; + + @override + String get repeater_prvKeyHelper => + 'A new private key for the repeater, a 128-character hex string.'; + + @override + String get repeater_generatePrvKey => 'Generate a random keypair'; + + @override + String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair'; + + @override + String get repeater_pubKey => 'Public key'; + + @override + String get repeater_pubKeyHelper => + 'This is the public key that goes with the generated private key. You can\'t set this directly.'; + + @override + String get repeater_pubKeyPrefix => 'Desired prefix'; + + @override + String repeater_pubKeyPrefixHelper(int tries) { + return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.'; + } + @override String get repeater_txDelay => 'Zpoždanie v Flood, TX'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index f55a98e9..07d1bd4f 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -500,6 +500,14 @@ class AppLocalizationsSl extends AppLocalizations { @override String get settings_advertLocationSubtitle => 'Vključi lokacijo v oglas.'; + @override + String get settings_autoZeroHopAdvertOnGpsUpdate => + 'Samodejni zero-hop oglas ob posodobitvi GPS'; + + @override + String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle => + 'Ko se GPS lokacija spremeni, pošlji zero-hop oglas (zahteva lokacijo v oglasu).'; + @override String get settings_multiAck => 'Več potrdil'; @@ -937,6 +945,28 @@ class AppLocalizationsSl extends AppLocalizations { @override String get appSettings_lastWeek => 'Prejšnji teden'; + @override + String get appSettings_rasterTileSource => 'Vir rastrskih ploščic'; + + @override + String get appSettings_stadiaEndpoint => 'Končna točka Stadia'; + + @override + String get appSettings_stadiaApiKey => 'Ključ API Stadia'; + + @override + String get appSettings_stadiaApiKeyRequired => + 'Obvezno za uporabo Stadia Maps'; + + @override + String appSettings_stadiaApiKeyConfigured(String maskedKey) { + return 'Nastavljeno: $maskedKey'; + } + + @override + String get appSettings_stadiaApiKeyDialogDescription => + 'Vnesite svoj ključ API za Stadia Maps. Aplikacija ga uporablja za zahteve rastrskih ploščic.'; + @override String get appSettings_offlineMapCache => 'Shramba zemljevidov brez povezave'; @@ -1435,6 +1465,9 @@ class AppLocalizationsSl extends AppLocalizations { @override String get chat_sendGif => 'Pošlji GIF'; + @override + String get chat_receivedGif => 'Received a GIF'; + @override String get chat_reply => 'Odgovori'; @@ -2086,6 +2119,42 @@ class AppLocalizationsSl extends AppLocalizations { return 'Poslovniški izniki: $count'; } + @override + String get mapCache_cachedTilesLabel => 'Cached tiles'; + + @override + String get mapCache_cachedTileSummaryLabel => 'Cached tile summary'; + + @override + String mapCache_bulkDownloadDisabledForSource(String source) { + return 'Offline bulk downloads are disabled for $source.'; + } + + @override + String mapCache_bulkDownloadDisabledInConfig(String source) { + return 'Offline bulk downloads are disabled for $source in this app configuration.'; + } + + @override + String mapCache_summarySource(String source) { + return 'Source: $source'; + } + + @override + String mapCache_summaryCachedTilesForSource(int count) { + return 'Cached tiles for source: $count'; + } + + @override + String mapCache_summaryCachedInSelection(int count) { + return 'Cached in selected area/zoom: $count'; + } + + @override + String mapCache_summaryApproxCacheSize(String size) { + return 'Approx cache size: $size'; + } + @override String mapCache_boundsLabel( String north, @@ -2629,6 +2698,41 @@ class AppLocalizationsSl extends AppLocalizations { String get repeater_pathHashModeHelper => 'Bajti, uporabljeni za kodiranje ID-ja tega repetitorja v oznakah flood poti/zaznavanja zank. 0=1 bajt (256 ID-jev, do 64 skokov), 1=2 bajta (65.000 ID-jev, do 32 skokov), 2=3 bajti (16 milijonov ID-jev, do 21 skokov). Vdelana programska oprema pred v1.14 je vedno uporabljala 1-bajtne poti; v1.14 in novejše je mogoče nastaviti na 2- ali 3-bajtne poti.'; + @override + String get repeater_keySettings => 'Change Identity Keys'; + + @override + String get repeater_keySettingsSubtitle => + 'Change the public/private keypair'; + + @override + String get repeater_prvKey => 'Private key'; + + @override + String get repeater_prvKeyHelper => + 'A new private key for the repeater, a 128-character hex string.'; + + @override + String get repeater_generatePrvKey => 'Generate a random keypair'; + + @override + String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair'; + + @override + String get repeater_pubKey => 'Public key'; + + @override + String get repeater_pubKeyHelper => + 'This is the public key that goes with the generated private key. You can\'t set this directly.'; + + @override + String get repeater_pubKeyPrefix => 'Desired prefix'; + + @override + String repeater_pubKeyPrefixHelper(int tries) { + return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.'; + } + @override String get repeater_txDelay => 'Zatemnitevanje zaradi poplav v Texasu'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 62806e0b..8bdd20f9 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -497,6 +497,14 @@ class AppLocalizationsSv extends AppLocalizations { @override String get settings_advertLocationSubtitle => 'Inkludera plats i annonsen'; + @override + String get settings_autoZeroHopAdvertOnGpsUpdate => + 'Automatisk zero-hop-annons vid GPS-uppdatering'; + + @override + String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle => + 'När GPS-positionen ändras, skicka en zero-hop-annons (kräver plats i annonsen).'; + @override String get settings_multiAck => 'Flera bekräftelser'; @@ -930,6 +938,28 @@ class AppLocalizationsSv extends AppLocalizations { @override String get appSettings_lastWeek => 'Förra veckan'; + @override + String get appSettings_rasterTileSource => 'Källa för rasterplattor'; + + @override + String get appSettings_stadiaEndpoint => 'Stadia-slutpunkt'; + + @override + String get appSettings_stadiaApiKey => 'Stadia API-nyckel'; + + @override + String get appSettings_stadiaApiKeyRequired => + 'Krävs för att använda Stadia Maps'; + + @override + String appSettings_stadiaApiKeyConfigured(String maskedKey) { + return 'Konfigurerad: $maskedKey'; + } + + @override + String get appSettings_stadiaApiKeyDialogDescription => + 'Ange din Stadia Maps API-nyckel. Appen använder den för förfrågningar om rasterplattor.'; + @override String get appSettings_offlineMapCache => 'Offline Kartcache'; @@ -1428,6 +1458,9 @@ class AppLocalizationsSv extends AppLocalizations { @override String get chat_sendGif => 'Skicka GIF'; + @override + String get chat_receivedGif => 'Received a GIF'; + @override String get chat_reply => 'Svara'; @@ -2074,6 +2107,42 @@ class AppLocalizationsSv extends AppLocalizations { return 'Misslyckade nedladdningar: $count'; } + @override + String get mapCache_cachedTilesLabel => 'Cached tiles'; + + @override + String get mapCache_cachedTileSummaryLabel => 'Cached tile summary'; + + @override + String mapCache_bulkDownloadDisabledForSource(String source) { + return 'Offline bulk downloads are disabled for $source.'; + } + + @override + String mapCache_bulkDownloadDisabledInConfig(String source) { + return 'Offline bulk downloads are disabled for $source in this app configuration.'; + } + + @override + String mapCache_summarySource(String source) { + return 'Source: $source'; + } + + @override + String mapCache_summaryCachedTilesForSource(int count) { + return 'Cached tiles for source: $count'; + } + + @override + String mapCache_summaryCachedInSelection(int count) { + return 'Cached in selected area/zoom: $count'; + } + + @override + String mapCache_summaryApproxCacheSize(String size) { + return 'Approx cache size: $size'; + } + @override String mapCache_boundsLabel( String north, @@ -2617,6 +2686,41 @@ class AppLocalizationsSv extends AppLocalizations { String get repeater_pathHashModeHelper => 'Byte som används för att koda denna repeaters ID i taggar för flood-väg/loopdetektering. 0=1 byte (256 ID:n, upp till 64 hopp), 1=2 byte (65 000 ID:n, upp till 32 hopp), 2=3 byte (16 miljoner ID:n, upp till 21 hopp). Firmware före v1.14 använde alltid 1-byte-vägar; v1.14 och nyare kan konfigureras för 2- eller 3-byte-vägar.'; + @override + String get repeater_keySettings => 'Change Identity Keys'; + + @override + String get repeater_keySettingsSubtitle => + 'Change the public/private keypair'; + + @override + String get repeater_prvKey => 'Private key'; + + @override + String get repeater_prvKeyHelper => + 'A new private key for the repeater, a 128-character hex string.'; + + @override + String get repeater_generatePrvKey => 'Generate a random keypair'; + + @override + String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair'; + + @override + String get repeater_pubKey => 'Public key'; + + @override + String get repeater_pubKeyHelper => + 'This is the public key that goes with the generated private key. You can\'t set this directly.'; + + @override + String get repeater_pubKeyPrefix => 'Desired prefix'; + + @override + String repeater_pubKeyPrefixHelper(int tries) { + return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.'; + } + @override String get repeater_txDelay => 'Försening i Flood TX'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index d1bae5b7..2e87ba8d 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -501,6 +501,14 @@ class AppLocalizationsUk extends AppLocalizations { String get settings_advertLocationSubtitle => 'Включити геопозицію в оголошення'; + @override + String get settings_autoZeroHopAdvertOnGpsUpdate => + 'Автооголошення без хопів при оновленні GPS'; + + @override + String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle => + 'Коли GPS-локація змінюється, надсилати оголошення без хопів (потрібна геопозиція в оголошенні).'; + @override String get settings_multiAck => 'Багато підтверджень'; @@ -943,6 +951,28 @@ class AppLocalizationsUk extends AppLocalizations { @override String get appSettings_lastWeek => 'Минулий тиждень'; + @override + String get appSettings_rasterTileSource => 'Джерело растрових тайлів'; + + @override + String get appSettings_stadiaEndpoint => 'Кінцева точка Stadia'; + + @override + String get appSettings_stadiaApiKey => 'Ключ API Stadia'; + + @override + String get appSettings_stadiaApiKeyRequired => + 'Потрібно для використання Stadia Maps'; + + @override + String appSettings_stadiaApiKeyConfigured(String maskedKey) { + return 'Налаштовано: $maskedKey'; + } + + @override + String get appSettings_stadiaApiKeyDialogDescription => + 'Введіть свій ключ API Stadia Maps. Програма використовує його для запитів растрових тайлів.'; + @override String get appSettings_offlineMapCache => 'Офлайн-кеш карти'; @@ -1441,6 +1471,9 @@ class AppLocalizationsUk extends AppLocalizations { @override String get chat_sendGif => 'Надіслати GIF'; + @override + String get chat_receivedGif => 'Received a GIF'; + @override String get chat_reply => 'Відповісти'; @@ -2096,6 +2129,42 @@ class AppLocalizationsUk extends AppLocalizations { return 'Невдалі завантаження: $count'; } + @override + String get mapCache_cachedTilesLabel => 'Cached tiles'; + + @override + String get mapCache_cachedTileSummaryLabel => 'Cached tile summary'; + + @override + String mapCache_bulkDownloadDisabledForSource(String source) { + return 'Offline bulk downloads are disabled for $source.'; + } + + @override + String mapCache_bulkDownloadDisabledInConfig(String source) { + return 'Offline bulk downloads are disabled for $source in this app configuration.'; + } + + @override + String mapCache_summarySource(String source) { + return 'Source: $source'; + } + + @override + String mapCache_summaryCachedTilesForSource(int count) { + return 'Cached tiles for source: $count'; + } + + @override + String mapCache_summaryCachedInSelection(int count) { + return 'Cached in selected area/zoom: $count'; + } + + @override + String mapCache_summaryApproxCacheSize(String size) { + return 'Approx cache size: $size'; + } + @override String mapCache_boundsLabel( String north, @@ -2649,6 +2718,41 @@ class AppLocalizationsUk extends AppLocalizations { String get repeater_pathHashModeHelper => 'Байти, що використовуються для кодування ідентифікатора цього ретранслятора в тегах flood-шляху/виявлення петель. 0=1 байт (256 ідентифікаторів, до 64 переходів), 1=2 байти (65 000 ідентифікаторів, до 32 переходів), 2=3 байти (16 мільйонів ідентифікаторів, до 21 переходу). Прошивки до v1.14 завжди використовували 1-байтові шляхи; v1.14 і новіші можна налаштувати на 2- або 3-байтові шляхи.'; + @override + String get repeater_keySettings => 'Change Identity Keys'; + + @override + String get repeater_keySettingsSubtitle => + 'Change the public/private keypair'; + + @override + String get repeater_prvKey => 'Private key'; + + @override + String get repeater_prvKeyHelper => + 'A new private key for the repeater, a 128-character hex string.'; + + @override + String get repeater_generatePrvKey => 'Generate a random keypair'; + + @override + String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair'; + + @override + String get repeater_pubKey => 'Public key'; + + @override + String get repeater_pubKeyHelper => + 'This is the public key that goes with the generated private key. You can\'t set this directly.'; + + @override + String get repeater_pubKeyPrefix => 'Desired prefix'; + + @override + String repeater_pubKeyPrefixHelper(int tries) { + return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.'; + } + @override String get repeater_txDelay => 'Затримка у Flood, штат Техас'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index aff4d5e8..fa2813c3 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -476,6 +476,13 @@ class AppLocalizationsZh extends AppLocalizations { @override String get settings_advertLocationSubtitle => '在广告中包含位置'; + @override + String get settings_autoZeroHopAdvertOnGpsUpdate => 'GPS 更新时自动发送零跳广告'; + + @override + String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle => + '当 GPS 位置变化时,发送零跳广告(需要在广告中包含位置)。'; + @override String get settings_multiAck => '多重ACK'; @@ -883,6 +890,27 @@ class AppLocalizationsZh extends AppLocalizations { @override String get appSettings_lastWeek => '上周'; + @override + String get appSettings_rasterTileSource => '栅格瓦片源'; + + @override + String get appSettings_stadiaEndpoint => 'Stadia 端点'; + + @override + String get appSettings_stadiaApiKey => 'Stadia API 密钥'; + + @override + String get appSettings_stadiaApiKeyRequired => '使用 Stadia Maps 时必需'; + + @override + String appSettings_stadiaApiKeyConfigured(String maskedKey) { + return '已配置:$maskedKey'; + } + + @override + String get appSettings_stadiaApiKeyDialogDescription => + '请输入你的 Stadia Maps API 密钥。该应用会使用它来请求栅格瓦片。'; + @override String get appSettings_offlineMapCache => '离线地图缓存'; @@ -1365,6 +1393,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get chat_sendGif => '发送 GIF'; + @override + String get chat_receivedGif => 'Received a GIF'; + @override String get chat_reply => '回复'; @@ -1990,6 +2021,42 @@ class AppLocalizationsZh extends AppLocalizations { return '下载失败:$count'; } + @override + String get mapCache_cachedTilesLabel => 'Cached tiles'; + + @override + String get mapCache_cachedTileSummaryLabel => 'Cached tile summary'; + + @override + String mapCache_bulkDownloadDisabledForSource(String source) { + return 'Offline bulk downloads are disabled for $source.'; + } + + @override + String mapCache_bulkDownloadDisabledInConfig(String source) { + return 'Offline bulk downloads are disabled for $source in this app configuration.'; + } + + @override + String mapCache_summarySource(String source) { + return 'Source: $source'; + } + + @override + String mapCache_summaryCachedTilesForSource(int count) { + return 'Cached tiles for source: $count'; + } + + @override + String mapCache_summaryCachedInSelection(int count) { + return 'Cached in selected area/zoom: $count'; + } + + @override + String mapCache_summaryApproxCacheSize(String size) { + return 'Approx cache size: $size'; + } + @override String mapCache_boundsLabel( String north, @@ -2506,6 +2573,41 @@ class AppLocalizationsZh extends AppLocalizations { String get repeater_pathHashModeHelper => '用于在洪泛路径/环路检测标签中编码此中继器 ID 的字节数。0=1 字节(256 个 ID,最多 64 跳),1=2 字节(65K 个 ID,最多 32 跳),2=3 字节(16M 个 ID,最多 21 跳)。v1.14 之前的固件始终使用 1 字节路径;v1.14 及更新版本可配置为 2 或 3 字节路径。'; + @override + String get repeater_keySettings => 'Change Identity Keys'; + + @override + String get repeater_keySettingsSubtitle => + 'Change the public/private keypair'; + + @override + String get repeater_prvKey => 'Private key'; + + @override + String get repeater_prvKeyHelper => + 'A new private key for the repeater, a 128-character hex string.'; + + @override + String get repeater_generatePrvKey => 'Generate a random keypair'; + + @override + String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair'; + + @override + String get repeater_pubKey => 'Public key'; + + @override + String get repeater_pubKeyHelper => + 'This is the public key that goes with the generated private key. You can\'t set this directly.'; + + @override + String get repeater_pubKeyPrefix => 'Desired prefix'; + + @override + String repeater_pubKeyPrefixHelper(int tries) { + return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.'; + } + @override String get repeater_txDelay => '洪水(德克萨斯州)延误'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 1f5e0762..9671cf8e 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -100,6 +100,8 @@ "settings_locationInvalid": "Ongeldige breedtegraad of lengtegraad.", "settings_latitude": "Breedtegraad", "settings_longitude": "Lengtegraad", + "settings_autoZeroHopAdvertOnGpsUpdate": "Automatische zero-hop-advertentie bij GPS-update", + "settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Wanneer de GPS-locatie verandert, een zero-hop-advertentie verzenden (vereist locatie in advertentie).", "settings_privacyMode": "Privacy-modus", "settings_privacyModeSubtitle": "Naam/locatie verbergen in advertenties", "settings_privacyModeToggle": "Schakel privacy modus in om je naam en locatie in advertenties te verbergen.", @@ -240,6 +242,19 @@ "appSettings_last6Hours": "Afgelopen 6 uur", "appSettings_last24Hours": "Afgelopen 24 uur", "appSettings_lastWeek": "Afgelopen week", + "appSettings_rasterTileSource": "Rastertegelbron", + "appSettings_stadiaEndpoint": "Stadia-eindpunt", + "appSettings_stadiaApiKey": "Stadia API-sleutel", + "appSettings_stadiaApiKeyRequired": "Vereist voor gebruik van Stadia Maps", + "appSettings_stadiaApiKeyConfigured": "Geconfigureerd: {maskedKey}", + "@appSettings_stadiaApiKeyConfigured": { + "placeholders": { + "maskedKey": { + "type": "String" + } + } + }, + "appSettings_stadiaApiKeyDialogDescription": "Voer je Stadia Maps API-sleutel in. De app gebruikt die voor rastertegelverzoeken.", "appSettings_offlineMapCache": "Offline Kaartcache", "appSettings_noAreaSelected": "Geen gebied geselecteerd", "appSettings_areaSelectedZoom": "Geselecteerd gebied (zoom {minZoom}-{maxZoom})", @@ -769,6 +784,56 @@ } } }, + "mapCache_cachedTilesLabel": "Cached tiles", + "mapCache_cachedTileSummaryLabel": "Cached tile summary", + "mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.", + "@mapCache_bulkDownloadDisabledForSource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.", + "@mapCache_bulkDownloadDisabledInConfig": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summarySource": "Source: {source}", + "@mapCache_summarySource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}", + "@mapCache_summaryCachedTilesForSource": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}", + "@mapCache_summaryCachedInSelection": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryApproxCacheSize": "Approx cache size: {size}", + "@mapCache_summaryApproxCacheSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "@mapCache_boundsLabel": { "placeholders": { diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 2cc10066..17285d0f 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -100,6 +100,8 @@ "settings_locationInvalid": "Nieprawidłowa szerokość geograficzna lub długość geograficzna.", "settings_latitude": "Szerokość", "settings_longitude": "Długość", + "settings_autoZeroHopAdvertOnGpsUpdate": "Automatyczne ogłoszenie zero-hop przy aktualizacji GPS", + "settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Gdy lokalizacja GPS się zmieni, wyślij ogłoszenie zero-hop (wymaga lokalizacji w ogłoszeniu).", "settings_privacyMode": "Tryb prywatności", "settings_privacyModeSubtitle": "Ukryj imię/lokalizację w rozgłoszeniach", "settings_privacyModeToggle": "Włącz tryb prywatności, aby ukryć swoje imię i lokalizację w rozgłoszeniach.", @@ -240,6 +242,19 @@ "appSettings_last6Hours": "Ostatnie 6 godzin", "appSettings_last24Hours": "Ostatnie 24 godziny", "appSettings_lastWeek": "Ostatni tydzień", + "appSettings_rasterTileSource": "Źródło kafelków rastrowych", + "appSettings_stadiaEndpoint": "Punkt końcowy Stadia", + "appSettings_stadiaApiKey": "Klucz API Stadia", + "appSettings_stadiaApiKeyRequired": "Wymagane do korzystania ze Stadia Maps", + "appSettings_stadiaApiKeyConfigured": "Skonfigurowano: {maskedKey}", + "@appSettings_stadiaApiKeyConfigured": { + "placeholders": { + "maskedKey": { + "type": "String" + } + } + }, + "appSettings_stadiaApiKeyDialogDescription": "Wprowadź swój klucz API Stadia Maps. Aplikacja używa go do żądań kafelków rastrowych.", "appSettings_offlineMapCache": "Pamięć podręczna map offline", "appSettings_noAreaSelected": "Nie wybrano żadnego obszaru.", "appSettings_areaSelectedZoom": "Wybrany obszar (skala {minZoom}-{maxZoom})", @@ -779,6 +794,56 @@ } } }, + "mapCache_cachedTilesLabel": "Cached tiles", + "mapCache_cachedTileSummaryLabel": "Cached tile summary", + "mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.", + "@mapCache_bulkDownloadDisabledForSource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.", + "@mapCache_bulkDownloadDisabledInConfig": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summarySource": "Source: {source}", + "@mapCache_summarySource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}", + "@mapCache_summaryCachedTilesForSource": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}", + "@mapCache_summaryCachedInSelection": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryApproxCacheSize": "Approx cache size: {size}", + "@mapCache_summaryApproxCacheSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "@mapCache_boundsLabel": { "placeholders": { diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index b6b30e00..90b8071a 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -100,6 +100,8 @@ "settings_locationInvalid": "Latitude ou longitude inválidos.", "settings_latitude": "Latitude", "settings_longitude": "Longitude", + "settings_autoZeroHopAdvertOnGpsUpdate": "Anúncio zero-hop automático na atualização do GPS", + "settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Quando a localização GPS mudar, enviar um anúncio zero-hop (requer localização no anúncio).", "settings_privacyMode": "Modo de Privacidade", "settings_privacyModeSubtitle": "Esconder nome/localização em anúncios", "settings_privacyModeToggle": "Ative o modo de privacidade para ocultar seu nome e localização em anúncios.", @@ -240,6 +242,19 @@ "appSettings_last6Hours": "Últimos 6 horas", "appSettings_last24Hours": "Últimas 24 horas", "appSettings_lastWeek": "Da última semana", + "appSettings_rasterTileSource": "Fonte de blocos raster", + "appSettings_stadiaEndpoint": "Endpoint da Stadia", + "appSettings_stadiaApiKey": "Chave da API Stadia", + "appSettings_stadiaApiKeyRequired": "Obrigatório para usar o Stadia Maps", + "appSettings_stadiaApiKeyConfigured": "Configurado: {maskedKey}", + "@appSettings_stadiaApiKeyConfigured": { + "placeholders": { + "maskedKey": { + "type": "String" + } + } + }, + "appSettings_stadiaApiKeyDialogDescription": "Insira sua chave da API Stadia Maps. O aplicativo a usa para solicitações de blocos raster.", "appSettings_offlineMapCache": "Cache de Mapa Offline", "appSettings_noAreaSelected": "Nenhuma área selecionada", "appSettings_areaSelectedZoom": "Área selecionada (zoom {minZoom}-{maxZoom})", @@ -769,6 +784,56 @@ } } }, + "mapCache_cachedTilesLabel": "Cached tiles", + "mapCache_cachedTileSummaryLabel": "Cached tile summary", + "mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.", + "@mapCache_bulkDownloadDisabledForSource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.", + "@mapCache_bulkDownloadDisabledInConfig": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summarySource": "Source: {source}", + "@mapCache_summarySource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}", + "@mapCache_summaryCachedTilesForSource": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}", + "@mapCache_summaryCachedInSelection": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryApproxCacheSize": "Approx cache size: {size}", + "@mapCache_summaryApproxCacheSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "@mapCache_boundsLabel": { "placeholders": { diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 72b02fb3..485cc641 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -77,6 +77,8 @@ "settings_locationIntervalInvalid": "Интервал должен составлять не менее 60 секунд и не более 86400 секунд.", "settings_latitude": "Широта", "settings_longitude": "Долгота", + "settings_autoZeroHopAdvertOnGpsUpdate": "Авто-объявление без хопов при обновлении GPS", + "settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Когда GPS-местоположение меняется, отправлять объявление без хопов (требуется геопозиция в объявлении).", "settings_privacyMode": "Режим конфиденциальности", "settings_privacyModeSubtitle": "Скрыть имя/позицию в анонсировании", "settings_privacyModeToggle": "Включите режим конфиденциальности, чтобы скрыть свое имя и местоположение в анонсировании.", @@ -190,6 +192,19 @@ "appSettings_last6Hours": "Последние 6 часов", "appSettings_last24Hours": "Последние 24 часа", "appSettings_lastWeek": "Последнюю неделю", + "appSettings_rasterTileSource": "Источник растровых тайлов", + "appSettings_stadiaEndpoint": "Конечная точка Stadia", + "appSettings_stadiaApiKey": "Ключ API Stadia", + "appSettings_stadiaApiKeyRequired": "Требуется для использования Stadia Maps", + "appSettings_stadiaApiKeyConfigured": "Настроено: {maskedKey}", + "@appSettings_stadiaApiKeyConfigured": { + "placeholders": { + "maskedKey": { + "type": "String" + } + } + }, + "appSettings_stadiaApiKeyDialogDescription": "Введите свой ключ API Stadia Maps. Приложение использует его для запросов растровых тайлов.", "appSettings_offlineMapCache": "Кэш офлайн-карты", "appSettings_noAreaSelected": "Область не выбрана", "appSettings_areaSelectedZoom": "Область выбрана (масштаб {minZoom}–{maxZoom})", @@ -438,6 +453,56 @@ "mapCache_downloadTilesButton": "Загрузить плитки", "mapCache_clearCacheButton": "Очистить кэш", "mapCache_failedDownloads": "Неудачных загрузок: {count}", + "mapCache_cachedTilesLabel": "Cached tiles", + "mapCache_cachedTileSummaryLabel": "Cached tile summary", + "mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.", + "@mapCache_bulkDownloadDisabledForSource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.", + "@mapCache_bulkDownloadDisabledInConfig": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summarySource": "Source: {source}", + "@mapCache_summarySource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}", + "@mapCache_summaryCachedTilesForSource": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}", + "@mapCache_summaryCachedInSelection": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryApproxCacheSize": "Approx cache size: {size}", + "@mapCache_summaryApproxCacheSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, "mapCache_boundsLabel": "С {north}, Ю {south}, В {east}, З {west}", "time_justNow": "Только что", "time_minutesAgo": "{minutes} мин назад", diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 9e7bf4cc..90cc0413 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -100,6 +100,8 @@ "settings_locationInvalid": "Neplatná šírka alebo dĺžka.", "settings_latitude": "Súradnica", "settings_longitude": "Dĺžka", + "settings_autoZeroHopAdvertOnGpsUpdate": "Automatický zero-hop inzerát pri aktualizácii GPS", + "settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Keď sa GPS poloha zmení, odoslať zero-hop inzerát (vyžaduje polohu v inzeráte).", "settings_privacyMode": "Režim ochrany súkromia", "settings_privacyModeSubtitle": "Skryť meno/poloha v reklamách", "settings_privacyModeToggle": "Prepínač súkromného režimu skryje vaše meno a polohu v reklamách.", @@ -240,6 +242,19 @@ "appSettings_last6Hours": "Posledné 6 hodín", "appSettings_last24Hours": "Posledných 24 hodín", "appSettings_lastWeek": "Minul týždeň", + "appSettings_rasterTileSource": "Zdroj rastrových dlaždíc", + "appSettings_stadiaEndpoint": "Koncový bod Stadia", + "appSettings_stadiaApiKey": "Kľúč API Stadia", + "appSettings_stadiaApiKeyRequired": "Vyžaduje sa na používanie Stadia Maps", + "appSettings_stadiaApiKeyConfigured": "Nakonfigurované: {maskedKey}", + "@appSettings_stadiaApiKeyConfigured": { + "placeholders": { + "maskedKey": { + "type": "String" + } + } + }, + "appSettings_stadiaApiKeyDialogDescription": "Zadajte svoj kľúč API pre Stadia Maps. Aplikácia ho používa na požiadavky na rastrové dlaždice.", "appSettings_offlineMapCache": "Offline Mapa Pamäť", "appSettings_noAreaSelected": "Neoznačila sa žiadna oblasť", "appSettings_areaSelectedZoom": "Vyberená oblasť (zoom {minZoom}-{maxZoom})", @@ -769,6 +784,56 @@ } } }, + "mapCache_cachedTilesLabel": "Cached tiles", + "mapCache_cachedTileSummaryLabel": "Cached tile summary", + "mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.", + "@mapCache_bulkDownloadDisabledForSource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.", + "@mapCache_bulkDownloadDisabledInConfig": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summarySource": "Source: {source}", + "@mapCache_summarySource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}", + "@mapCache_summaryCachedTilesForSource": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}", + "@mapCache_summaryCachedInSelection": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryApproxCacheSize": "Approx cache size: {size}", + "@mapCache_summaryApproxCacheSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "@mapCache_boundsLabel": { "placeholders": { diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 329d64aa..35062061 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -100,6 +100,8 @@ "settings_locationInvalid": "Neveljavna zemeljska širina ali dolžina.", "settings_latitude": "Širina", "settings_longitude": "Dolžina", + "settings_autoZeroHopAdvertOnGpsUpdate": "Samodejni zero-hop oglas ob posodobitvi GPS", + "settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Ko se GPS lokacija spremeni, pošlji zero-hop oglas (zahteva lokacijo v oglasu).", "settings_privacyMode": "Zasebnost", "settings_privacyModeSubtitle": "Skrita imena/lokacije v oglasih", "settings_privacyModeToggle": "Omogoči način zasebnosti, da skrijemo tvoje ime in lokacijo v oglasih.", @@ -240,6 +242,19 @@ "appSettings_last6Hours": "Zadnjih 6 ur", "appSettings_last24Hours": "Zadnjih 24 ur", "appSettings_lastWeek": "Prejšnji teden", + "appSettings_rasterTileSource": "Vir rastrskih ploščic", + "appSettings_stadiaEndpoint": "Končna točka Stadia", + "appSettings_stadiaApiKey": "Ključ API Stadia", + "appSettings_stadiaApiKeyRequired": "Obvezno za uporabo Stadia Maps", + "appSettings_stadiaApiKeyConfigured": "Nastavljeno: {maskedKey}", + "@appSettings_stadiaApiKeyConfigured": { + "placeholders": { + "maskedKey": { + "type": "String" + } + } + }, + "appSettings_stadiaApiKeyDialogDescription": "Vnesite svoj ključ API za Stadia Maps. Aplikacija ga uporablja za zahteve rastrskih ploščic.", "appSettings_offlineMapCache": "Shramba zemljevidov brez povezave", "appSettings_noAreaSelected": "Območje ni izbrano", "appSettings_areaSelectedZoom": "Izbrano območje (povečava {minZoom}-{maxZoom})", @@ -769,6 +784,56 @@ } } }, + "mapCache_cachedTilesLabel": "Cached tiles", + "mapCache_cachedTileSummaryLabel": "Cached tile summary", + "mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.", + "@mapCache_bulkDownloadDisabledForSource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.", + "@mapCache_bulkDownloadDisabledInConfig": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summarySource": "Source: {source}", + "@mapCache_summarySource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}", + "@mapCache_summaryCachedTilesForSource": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}", + "@mapCache_summaryCachedInSelection": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryApproxCacheSize": "Approx cache size: {size}", + "@mapCache_summaryApproxCacheSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "@mapCache_boundsLabel": { "placeholders": { diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 9f291e2c..487e8f96 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -100,6 +100,8 @@ "settings_locationInvalid": "Ogiltig latitud eller longitud.", "settings_latitude": "Latitud", "settings_longitude": "Längdgrad", + "settings_autoZeroHopAdvertOnGpsUpdate": "Automatisk zero-hop-annons vid GPS-uppdatering", + "settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "När GPS-positionen ändras, skicka en zero-hop-annons (kräver plats i annonsen).", "settings_privacyMode": "Privatläge", "settings_privacyModeSubtitle": "Dölj namn/plats i annonser", "settings_privacyModeToggle": "Aktivera privatläge för att dölja ditt namn och din plats i annonser.", @@ -240,6 +242,19 @@ "appSettings_last6Hours": "De senaste 6 timmarna", "appSettings_last24Hours": "De senaste 24 timmarna", "appSettings_lastWeek": "Förra veckan", + "appSettings_rasterTileSource": "Källa för rasterplattor", + "appSettings_stadiaEndpoint": "Stadia-slutpunkt", + "appSettings_stadiaApiKey": "Stadia API-nyckel", + "appSettings_stadiaApiKeyRequired": "Krävs för att använda Stadia Maps", + "appSettings_stadiaApiKeyConfigured": "Konfigurerad: {maskedKey}", + "@appSettings_stadiaApiKeyConfigured": { + "placeholders": { + "maskedKey": { + "type": "String" + } + } + }, + "appSettings_stadiaApiKeyDialogDescription": "Ange din Stadia Maps API-nyckel. Appen använder den för förfrågningar om rasterplattor.", "appSettings_offlineMapCache": "Offline Kartcache", "appSettings_noAreaSelected": "Ingen area markerad", "appSettings_areaSelectedZoom": "Område markerat (zoom {minZoom}-{maxZoom})", @@ -769,6 +784,56 @@ } } }, + "mapCache_cachedTilesLabel": "Cached tiles", + "mapCache_cachedTileSummaryLabel": "Cached tile summary", + "mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.", + "@mapCache_bulkDownloadDisabledForSource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.", + "@mapCache_bulkDownloadDisabledInConfig": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summarySource": "Source: {source}", + "@mapCache_summarySource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}", + "@mapCache_summaryCachedTilesForSource": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}", + "@mapCache_summaryCachedInSelection": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryApproxCacheSize": "Approx cache size: {size}", + "@mapCache_summaryApproxCacheSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "@mapCache_boundsLabel": { "placeholders": { diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index fea48c5b..870fb06d 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -101,6 +101,8 @@ "settings_locationInvalid": "Некоректна широта або довгота.", "settings_latitude": "Широта", "settings_longitude": "Довгота", + "settings_autoZeroHopAdvertOnGpsUpdate": "Автооголошення без хопів при оновленні GPS", + "settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Коли GPS-локація змінюється, надсилати оголошення без хопів (потрібна геопозиція в оголошенні).", "settings_privacyMode": "Режим приватності", "settings_privacyModeSubtitle": "Приховати ім'я/геопозицію в оголошеннях", "settings_privacyModeToggle": "Увімкніть режим приватності, щоб приховати своє ім'я та геопозицію в оголошеннях.", @@ -242,6 +244,19 @@ "appSettings_last6Hours": "Останні 6 годин", "appSettings_last24Hours": "Останні 24 години", "appSettings_lastWeek": "Минулий тиждень", + "appSettings_rasterTileSource": "Джерело растрових тайлів", + "appSettings_stadiaEndpoint": "Кінцева точка Stadia", + "appSettings_stadiaApiKey": "Ключ API Stadia", + "appSettings_stadiaApiKeyRequired": "Потрібно для використання Stadia Maps", + "appSettings_stadiaApiKeyConfigured": "Налаштовано: {maskedKey}", + "@appSettings_stadiaApiKeyConfigured": { + "placeholders": { + "maskedKey": { + "type": "String" + } + } + }, + "appSettings_stadiaApiKeyDialogDescription": "Введіть свій ключ API Stadia Maps. Програма використовує його для запитів растрових тайлів.", "appSettings_offlineMapCache": "Офлайн-кеш карти", "appSettings_noAreaSelected": "Область не вибрано", "appSettings_areaSelectedZoom": "Вибрана область (зум {minZoom}-{maxZoom})", @@ -779,6 +794,56 @@ } } }, + "mapCache_cachedTilesLabel": "Cached tiles", + "mapCache_cachedTileSummaryLabel": "Cached tile summary", + "mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.", + "@mapCache_bulkDownloadDisabledForSource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.", + "@mapCache_bulkDownloadDisabledInConfig": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summarySource": "Source: {source}", + "@mapCache_summarySource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}", + "@mapCache_summaryCachedTilesForSource": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}", + "@mapCache_summaryCachedInSelection": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryApproxCacheSize": "Approx cache size: {size}", + "@mapCache_summaryApproxCacheSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, "mapCache_boundsLabel": "Пн {north}, Пд {south}, Сх {east}, Зх {west}", "@mapCache_boundsLabel": { "placeholders": { diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 222eb7e9..b0dd5b6b 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -105,6 +105,8 @@ "settings_locationIntervalInvalid": "间隔时间必须至少为 60 秒,但不超过 86400 秒。", "settings_latitude": "纬度", "settings_longitude": "经度", + "settings_autoZeroHopAdvertOnGpsUpdate": "GPS 更新时自动发送零跳广告", + "settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "当 GPS 位置变化时,发送零跳广告(需要在广告中包含位置)。", "settings_privacyMode": "隐私模式", "settings_privacyModeSubtitle": "在广告中隐藏姓名/位置", "settings_privacyModeToggle": "切换隐私模式以在广告中隐藏姓名和位置,保护个人信息。", @@ -254,6 +256,19 @@ "appSettings_last6Hours": "过去6小时", "appSettings_last24Hours": "过去24小时", "appSettings_lastWeek": "上周", + "appSettings_rasterTileSource": "栅格瓦片源", + "appSettings_stadiaEndpoint": "Stadia 端点", + "appSettings_stadiaApiKey": "Stadia API 密钥", + "appSettings_stadiaApiKeyRequired": "使用 Stadia Maps 时必需", + "appSettings_stadiaApiKeyConfigured": "已配置:{maskedKey}", + "@appSettings_stadiaApiKeyConfigured": { + "placeholders": { + "maskedKey": { + "type": "String" + } + } + }, + "appSettings_stadiaApiKeyDialogDescription": "请输入你的 Stadia Maps API 密钥。该应用会使用它来请求栅格瓦片。", "appSettings_offlineMapCache": "离线地图缓存", "appSettings_noAreaSelected": "未选择任何区域", "appSettings_areaSelectedZoom": "已选择区域(缩放 {minZoom} - {maxZoom})", @@ -796,6 +811,56 @@ } } }, + "mapCache_cachedTilesLabel": "Cached tiles", + "mapCache_cachedTileSummaryLabel": "Cached tile summary", + "mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.", + "@mapCache_bulkDownloadDisabledForSource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.", + "@mapCache_bulkDownloadDisabledInConfig": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summarySource": "Source: {source}", + "@mapCache_summarySource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}", + "@mapCache_summaryCachedTilesForSource": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}", + "@mapCache_summaryCachedInSelection": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryApproxCacheSize": "Approx cache size: {size}", + "@mapCache_summaryApproxCacheSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, "mapCache_boundsLabel": "北 {north}, 南 {south}, 东 {east}, 西 {west}", "@mapCache_boundsLabel": { "placeholders": { diff --git a/lib/main.dart b/lib/main.dart index 1445bdde..7cabda17 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -51,7 +51,9 @@ void main() async { final bleDebugLogService = BleDebugLogService(); final appDebugLogService = AppDebugLogService(); final backgroundService = BackgroundService(); - final mapTileCacheService = MapTileCacheService(); + final mapTileCacheService = MapTileCacheService( + appSettingsService: appSettingsService, + ); final chatTextScaleService = ChatTextScaleService(); final translationService = TranslationService(appSettingsService); final uiViewStateService = UiViewStateService(); @@ -183,7 +185,7 @@ class MeshCoreApp extends StatelessWidget { ChangeNotifierProvider.value(value: translationService), ChangeNotifierProvider.value(value: uiViewStateService), Provider.value(value: storage), - Provider.value(value: mapTileCacheService), + ChangeNotifierProvider.value(value: mapTileCacheService), ChangeNotifierProvider.value(value: timeoutPredictionService), ], child: Consumer( diff --git a/lib/models/app_settings.dart b/lib/models/app_settings.dart index 3fd0d05f..a6f82100 100644 --- a/lib/models/app_settings.dart +++ b/lib/models/app_settings.dart @@ -76,6 +76,8 @@ class Cyr2LatProfile { class AppSettings { static const Object _unset = Object(); + static const String stadiaDemo = + '51bd0381-4685-4666-bae8-48940f6d77c0'; final bool clearPathOnMaxRetry; final bool mapShowRepeaters; @@ -91,10 +93,15 @@ class AppSettings { final Map? mapCacheBounds; final int mapCacheMinZoom; final int mapCacheMaxZoom; + final String mapRasterSourceId; + final String mapTileEndpointId; + final String? mapTileApiKey; final bool notificationsEnabled; final bool notifyOnNewMessage; final bool notifyOnNewChannelMessage; final bool notifyOnNewAdvert; + final bool autoSendZeroHopAdvertOnGpsUpdate; + final int gpsIntervalSeconds; final bool autoRouteRotationEnabled; final double maxRouteWeight; final double initialRouteWeight; @@ -122,6 +129,17 @@ class AppSettings { final List cyr2latProfiles; final String selectedCyr2latProfileId; + String get effectiveMapTileApiKey { + final apiKey = mapTileApiKey?.trim(); + if (apiKey == null || apiKey.isEmpty) { + return stadiaDemo; + } + return apiKey; + } + + bool get usesstadiaDemo => + effectiveMapTileApiKey == stadiaDemo; + Map get cyr2latCharMap { final profile = cyr2latProfiles.firstWhere( (p) => p.id == selectedCyr2latProfileId, @@ -145,10 +163,15 @@ class AppSettings { this.mapCacheBounds, this.mapCacheMinZoom = 10, this.mapCacheMaxZoom = 15, + this.mapRasterSourceId = 'osm_auto', + this.mapTileEndpointId = 'standard_2x', + this.mapTileApiKey, this.notificationsEnabled = true, this.notifyOnNewMessage = true, this.notifyOnNewChannelMessage = true, this.notifyOnNewAdvert = true, + this.autoSendZeroHopAdvertOnGpsUpdate = false, + this.gpsIntervalSeconds = 900, this.autoRouteRotationEnabled = true, this.maxRouteWeight = 5.0, this.initialRouteWeight = 3.0, @@ -206,10 +229,16 @@ class AppSettings { 'map_cache_bounds': mapCacheBounds, 'map_cache_min_zoom': mapCacheMinZoom, 'map_cache_max_zoom': mapCacheMaxZoom, + 'map_raster_source_id': mapRasterSourceId, + 'map_tile_endpoint_id': mapTileEndpointId, + 'map_tile_api_key': mapTileApiKey, 'notifications_enabled': notificationsEnabled, 'notify_on_new_message': notifyOnNewMessage, 'notify_on_new_channel_message': notifyOnNewChannelMessage, 'notify_on_new_advert': notifyOnNewAdvert, + 'auto_send_zero_hop_advert_on_gps_update': + autoSendZeroHopAdvertOnGpsUpdate, + 'gps_interval_seconds': gpsIntervalSeconds, 'auto_route_rotation_enabled': autoRouteRotationEnabled, 'max_route_weight': maxRouteWeight, 'initial_route_weight': initialRouteWeight, @@ -270,11 +299,18 @@ class AppSettings { ), mapCacheMinZoom: json['map_cache_min_zoom'] as int? ?? 10, mapCacheMaxZoom: json['map_cache_max_zoom'] as int? ?? 15, + mapRasterSourceId: json['map_raster_source_id'] as String? ?? 'osm_auto', + mapTileEndpointId: json['map_tile_endpoint_id'] as String? ?? 'standard', + mapTileApiKey: json['map_tile_api_key'] as String?, notificationsEnabled: json['notifications_enabled'] as bool? ?? true, notifyOnNewMessage: json['notify_on_new_message'] as bool? ?? true, notifyOnNewChannelMessage: json['notify_on_new_channel_message'] as bool? ?? true, notifyOnNewAdvert: json['notify_on_new_advert'] as bool? ?? true, + autoSendZeroHopAdvertOnGpsUpdate: + json['auto_send_zero_hop_advert_on_gps_update'] as bool? ?? false, + gpsIntervalSeconds: + (json['gps_interval_seconds'] as num?)?.toInt() ?? 900, autoRouteRotationEnabled: json['auto_route_rotation_enabled'] as bool? ?? true, maxRouteWeight: (json['max_route_weight'] as num?)?.toDouble() ?? 5.0, @@ -379,10 +415,15 @@ class AppSettings { Object? mapCacheBounds = _unset, int? mapCacheMinZoom, int? mapCacheMaxZoom, + String? mapRasterSourceId, + String? mapTileEndpointId, + Object? mapTileApiKey = _unset, bool? notificationsEnabled, bool? notifyOnNewMessage, bool? notifyOnNewChannelMessage, bool? notifyOnNewAdvert, + bool? autoSendZeroHopAdvertOnGpsUpdate, + int? gpsIntervalSeconds, bool? autoRouteRotationEnabled, double? maxRouteWeight, double? initialRouteWeight, @@ -428,11 +469,20 @@ class AppSettings { : mapCacheBounds as Map?, mapCacheMinZoom: mapCacheMinZoom ?? this.mapCacheMinZoom, mapCacheMaxZoom: mapCacheMaxZoom ?? this.mapCacheMaxZoom, + mapRasterSourceId: mapRasterSourceId ?? this.mapRasterSourceId, + mapTileEndpointId: mapTileEndpointId ?? this.mapTileEndpointId, + mapTileApiKey: mapTileApiKey == _unset + ? this.mapTileApiKey + : mapTileApiKey as String?, notificationsEnabled: notificationsEnabled ?? this.notificationsEnabled, notifyOnNewMessage: notifyOnNewMessage ?? this.notifyOnNewMessage, notifyOnNewChannelMessage: notifyOnNewChannelMessage ?? this.notifyOnNewChannelMessage, notifyOnNewAdvert: notifyOnNewAdvert ?? this.notifyOnNewAdvert, + autoSendZeroHopAdvertOnGpsUpdate: + autoSendZeroHopAdvertOnGpsUpdate ?? + this.autoSendZeroHopAdvertOnGpsUpdate, + gpsIntervalSeconds: gpsIntervalSeconds ?? this.gpsIntervalSeconds, autoRouteRotationEnabled: autoRouteRotationEnabled ?? this.autoRouteRotationEnabled, maxRouteWeight: maxRouteWeight ?? this.maxRouteWeight, diff --git a/lib/models/community.dart b/lib/models/community.dart index 7261ddf9..c0abf7f0 100644 --- a/lib/models/community.dart +++ b/lib/models/community.dart @@ -1,8 +1,8 @@ import 'dart:convert'; -import 'dart:math'; import 'dart:typed_data'; import 'package:crypto/crypto.dart' as crypto; +import 'package:meshcore_open/utils/keys.dart'; import 'channel.dart'; @@ -37,11 +37,7 @@ class Community { /// Generate a new community with a random 32-byte secret factory Community.create({required String id, required String name}) { - final random = Random.secure(); - final secret = Uint8List(32); - for (int i = 0; i < 32; i++) { - secret[i] = random.nextInt(256); - } + final secret = randomBytes(32); return Community( id: id, name: name, @@ -210,12 +206,7 @@ class Community { /// Create a copy of this community with a regenerated random secret Community withRegeneratedSecret() { - final random = Random.secure(); - final newSecret = Uint8List(32); - for (int i = 0; i < 32; i++) { - newSecret[i] = random.nextInt(256); - } - return withNewSecret(newSecret); + return withNewSecret(randomBytes(32)); } /// Extract secret from QR data (for updating existing community) diff --git a/lib/models/radio_settings.dart b/lib/models/radio_settings.dart index e74cf209..6f5911e0 100644 --- a/lib/models/radio_settings.dart +++ b/lib/models/radio_settings.dart @@ -81,6 +81,16 @@ class RadioSettings { txPowerDbm: 20, ), ), + ( + 'Australia (Mid)', + RadioSettings( + frequencyMHz: 915.075, + bandwidth: LoRaBandwidth.bw125, + spreadingFactor: LoRaSpreadingFactor.sf9, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 20, + ), + ), ( 'Australia SA, WA, QLD', RadioSettings( diff --git a/lib/screens/app_settings_screen.dart b/lib/screens/app_settings_screen.dart index e54687c9..90a0a132 100644 --- a/lib/screens/app_settings_screen.dart +++ b/lib/screens/app_settings_screen.dart @@ -8,6 +8,7 @@ import '../l10n/l10n.dart'; import '../models/app_settings.dart'; import '../models/translation_support.dart'; import '../services/app_settings_service.dart'; +import '../services/map_tile_cache_service.dart'; import '../services/notification_service.dart'; import '../services/translation_service.dart'; import '../theme/mesh_theme.dart'; @@ -699,52 +700,229 @@ class AppSettingsScreen extends StatelessWidget { ) { final scheme = Theme.of(context).colorScheme; final textTheme = Theme.of(context).textTheme; - return Column( - children: [ - SwitchListTile( - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 4, + final children = [ + SwitchListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + secondary: const Icon(Icons.router_outlined, size: 20), + title: Text(context.l10n.appSettings_showRepeaters), + subtitle: Text(context.l10n.appSettings_showRepeatersSubtitle), + value: settingsService.settings.mapShowRepeaters, + onChanged: (value) => settingsService.setMapShowRepeaters(value), + ), + const Divider(height: 1, indent: 16), + SwitchListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + secondary: const Icon(Icons.chat_outlined, size: 20), + title: Text(context.l10n.appSettings_showChatNodes), + subtitle: Text(context.l10n.appSettings_showChatNodesSubtitle), + value: settingsService.settings.mapShowChatNodes, + onChanged: (value) => settingsService.setMapShowChatNodes(value), + ), + const Divider(height: 1, indent: 16), + SwitchListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + secondary: const Icon(Icons.people_outline, size: 20), + title: Text(context.l10n.appSettings_showOtherNodes), + subtitle: Text(context.l10n.appSettings_showOtherNodesSubtitle), + value: settingsService.settings.mapShowOtherNodes, + onChanged: (value) => settingsService.setMapShowOtherNodes(value), + ), + const Divider(height: 1, indent: 16), + InkWell( + onTap: () => _showTimeFilterSheet(context, settingsService), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + Icon( + Icons.timer_outlined, + size: 20, + color: scheme.onSurfaceVariant, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.appSettings_timeFilter, + style: textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 2), + Text( + settingsService.settings.mapTimeFilterHours == 0 + ? context.l10n.appSettings_timeFilterShowAll + : context.l10n.appSettings_timeFilterShowLast( + settingsService.settings.mapTimeFilterHours + .toInt(), + ), + style: textTheme.bodySmall?.copyWith( + color: scheme.onSurfaceVariant, + ), + ), + ], + ), + ), + Icon( + Icons.chevron_right, + color: scheme.onSurfaceVariant, + size: 16, + ), + ], ), - secondary: const Icon(Icons.router_outlined, size: 20), - title: Text(context.l10n.appSettings_showRepeaters), - subtitle: Text(context.l10n.appSettings_showRepeatersSubtitle), - value: settingsService.settings.mapShowRepeaters, - onChanged: (value) => settingsService.setMapShowRepeaters(value), ), - const Divider(height: 1, indent: 16), - SwitchListTile( - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 4, + ), + const Divider(height: 1, indent: 16), + InkWell( + onTap: () => _showUnitsSheet(context, settingsService), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + Icon(Icons.straighten, size: 20, color: scheme.onSurfaceVariant), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.appSettings_unitsTitle, + style: textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 2), + Text( + settingsService.settings.unitSystem == UnitSystem.imperial + ? context.l10n.appSettings_unitsImperial + : context.l10n.appSettings_unitsMetric, + style: textTheme.bodySmall?.copyWith( + color: scheme.onSurfaceVariant, + ), + ), + ], + ), + ), + Icon( + Icons.chevron_right, + color: scheme.onSurfaceVariant, + size: 16, + ), + ], ), - secondary: const Icon(Icons.chat_outlined, size: 20), - title: Text(context.l10n.appSettings_showChatNodes), - subtitle: Text(context.l10n.appSettings_showChatNodesSubtitle), - value: settingsService.settings.mapShowChatNodes, - onChanged: (value) => settingsService.setMapShowChatNodes(value), ), - const Divider(height: 1, indent: 16), - SwitchListTile( - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 4, + ), + const Divider(height: 1, indent: 16), + InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const MapCacheScreen()), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + Icon( + Icons.download_outlined, + size: 20, + color: scheme.onSurfaceVariant, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.appSettings_offlineMapCache, + style: textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 2), + Text( + settingsService.settings.mapCacheBounds == null + ? context.l10n.appSettings_noAreaSelected + : context.l10n.appSettings_areaSelectedZoom( + settingsService.settings.mapCacheMinZoom, + settingsService.settings.mapCacheMaxZoom, + ), + style: textTheme.bodySmall?.copyWith( + color: scheme.onSurfaceVariant, + ), + ), + ], + ), + ), + Icon( + Icons.chevron_right, + color: scheme.onSurfaceVariant, + size: 16, + ), + ], ), - secondary: const Icon(Icons.people_outline, size: 20), - title: Text(context.l10n.appSettings_showOtherNodes), - subtitle: Text(context.l10n.appSettings_showOtherNodesSubtitle), - value: settingsService.settings.mapShowOtherNodes, - onChanged: (value) => settingsService.setMapShowOtherNodes(value), ), + ), + const Divider(height: 1, indent: 16), + InkWell( + onTap: () => _showMapRasterSourceDialog(context, settingsService), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + Icon( + Icons.layers_outlined, + size: 20, + color: scheme.onSurfaceVariant, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.appSettings_rasterTileSource, + style: textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 2), + Text( + _mapRasterSourceSummary(settingsService.settings), + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: textTheme.bodySmall?.copyWith( + color: scheme.onSurfaceVariant, + ), + ), + ], + ), + ), + Icon( + Icons.chevron_right, + color: scheme.onSurfaceVariant, + size: 16, + ), + ], + ), + ), + ), + ]; + + if (_isStadiaSource(settingsService.settings)) { + children.addAll([ const Divider(height: 1, indent: 16), InkWell( - onTap: () => _showTimeFilterSheet(context, settingsService), + onTap: () => _showMapRasterEndpointDialog(context, settingsService), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ Icon( - Icons.timer_outlined, + Icons.public_outlined, size: 20, color: scheme.onSurfaceVariant, ), @@ -754,19 +932,14 @@ class AppSettingsScreen extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - context.l10n.appSettings_timeFilter, + context.l10n.appSettings_stadiaEndpoint, style: textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(height: 2), Text( - settingsService.settings.mapTimeFilterHours == 0 - ? context.l10n.appSettings_timeFilterShowAll - : context.l10n.appSettings_timeFilterShowLast( - settingsService.settings.mapTimeFilterHours - .toInt(), - ), + _mapRasterEndpointSummary(settingsService.settings), style: textTheme.bodySmall?.copyWith( color: scheme.onSurfaceVariant, ), @@ -785,13 +958,13 @@ class AppSettingsScreen extends StatelessWidget { ), const Divider(height: 1, indent: 16), InkWell( - onTap: () => _showUnitsSheet(context, settingsService), + onTap: () => _showMapApiKeyDialog(context, settingsService), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ Icon( - Icons.straighten, + Icons.key_outlined, size: 20, color: scheme.onSurfaceVariant, ), @@ -801,17 +974,14 @@ class AppSettingsScreen extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - context.l10n.appSettings_unitsTitle, + context.l10n.appSettings_stadiaApiKey, style: textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(height: 2), Text( - settingsService.settings.unitSystem == - UnitSystem.imperial - ? context.l10n.appSettings_unitsImperial - : context.l10n.appSettings_unitsMetric, + _mapApiKeySummary(context, settingsService.settings), style: textTheme.bodySmall?.copyWith( color: scheme.onSurfaceVariant, ), @@ -828,59 +998,210 @@ class AppSettingsScreen extends StatelessWidget { ), ), ), - const Divider(height: 1, indent: 16), - InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const MapCacheScreen()), - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: Row( - children: [ - Icon( - Icons.download_outlined, - size: 20, - color: scheme.onSurfaceVariant, - ), - const SizedBox(width: 12), - Expanded( + ]); + } + + return Column(children: children); + } + + String _mapRasterSourceSummary(AppSettings settings) { + final source = MapRasterSourceCatalog.fromSettings(settings); + return '${source.label} - ${source.description}'; + } + + bool _isStadiaSource(AppSettings settings) { + return MapRasterSourceCatalog.fromSettings(settings).isStadia; + } + + String _mapRasterEndpointSummary(AppSettings settings) { + final endpoint = MapRasterEndpointCatalog.fromSettings(settings); + return '${endpoint.label} - ${endpoint.description}'; + } + + String _mapApiKeySummary(BuildContext context, AppSettings settings) { + return context.l10n.appSettings_stadiaApiKeyConfigured( + _maskApiKey(settings.effectiveMapTileApiKey), + ); + } + + String _maskApiKey(String value) { + if (value.length <= 8) return '********'; + return '${value.substring(0, 4)}...${value.substring(value.length - 4)}'; + } + + void _showMapRasterSourceDialog( + BuildContext context, + AppSettingsService settingsService, + ) { + String selectedId = settingsService.settings.mapRasterSourceId; + showDialog( + context: context, + builder: (dialogContext) => StatefulBuilder( + builder: (dialogContext, setState) => AlertDialog( + title: Text(context.l10n.appSettings_rasterTileSource), + content: SizedBox( + width: 360, + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(dialogContext).size.height * 0.6, + ), + child: SingleChildScrollView( + child: RadioGroup( + groupValue: selectedId, + onChanged: (value) { + if (value == null) return; + setState(() { + selectedId = value; + }); + }, child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, children: [ - Text( - context.l10n.appSettings_offlineMapCache, - style: textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w600, + for (final preset in MapRasterSourcePreset.values) + Builder( + builder: (context) { + final option = MapRasterSourceCatalog.fromPreset( + preset, + ); + return RadioListTile( + value: preset.id, + title: Text(option.label), + subtitle: Text(option.description), + ); + }, ), - ), - const SizedBox(height: 2), - Text( - settingsService.settings.mapCacheBounds == null - ? context.l10n.appSettings_noAreaSelected - : context.l10n.appSettings_areaSelectedZoom( - settingsService.settings.mapCacheMinZoom, - settingsService.settings.mapCacheMaxZoom, - ), - style: textTheme.bodySmall?.copyWith( - color: scheme.onSurfaceVariant, - ), - ), ], ), ), - Icon( - Icons.chevron_right, - color: scheme.onSurfaceVariant, - size: 16, - ), - ], + ), ), ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialogContext), + child: Text(context.l10n.common_cancel), + ), + TextButton( + onPressed: () async { + await settingsService.setMapRasterSourceId(selectedId); + if (!dialogContext.mounted) return; + Navigator.pop(dialogContext); + }, + child: Text(context.l10n.common_save), + ), + ], ), - ], + ), + ); + } + + void _showMapRasterEndpointDialog( + BuildContext context, + AppSettingsService settingsService, + ) { + String selectedId = settingsService.settings.mapTileEndpointId; + showDialog( + context: context, + builder: (dialogContext) => StatefulBuilder( + builder: (dialogContext, setState) => AlertDialog( + title: Text(context.l10n.appSettings_stadiaEndpoint), + content: SizedBox( + width: 360, + child: RadioGroup( + groupValue: selectedId, + onChanged: (value) { + if (value == null) return; + setState(() { + selectedId = value; + }); + }, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + for (final option in MapRasterEndpointCatalog.presets) + RadioListTile( + value: option.id, + title: Text(option.label), + subtitle: Text(option.description), + ), + ], + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialogContext), + child: Text(context.l10n.common_cancel), + ), + TextButton( + onPressed: () async { + await settingsService.setMapTileEndpointId(selectedId); + if (!dialogContext.mounted) return; + Navigator.pop(dialogContext); + }, + child: Text(context.l10n.common_save), + ), + ], + ), + ), + ); + } + + void _showMapApiKeyDialog( + BuildContext context, + AppSettingsService settingsService, + ) { + final currentApiKey = settingsService.settings.mapTileApiKey?.trim() ?? ''; + final maskedApiKey = _maskApiKey( + currentApiKey.isEmpty + ? AppSettings.stadiaDemo + : currentApiKey, + ); + final controller = TextEditingController(text: maskedApiKey); + showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: Text(context.l10n.appSettings_stadiaApiKey), + content: SizedBox( + width: 420, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(context.l10n.appSettings_stadiaApiKeyDialogDescription), + const SizedBox(height: 12), + TextField( + controller: controller, + autofocus: true, + autocorrect: false, + enableSuggestions: false, + autofillHints: const [AutofillHints.password], + decoration: const InputDecoration( + border: OutlineInputBorder(), + hintText: '4e1bf343-3d91-4d9c-a8e1-1234567890ab', + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialogContext), + child: Text(context.l10n.common_cancel), + ), + TextButton( + onPressed: () async { + final apiKey = controller.text.trim(); + await settingsService.setMapTileApiKey( + apiKey == maskedApiKey ? currentApiKey : apiKey, + ); + if (!dialogContext.mounted) return; + Navigator.pop(dialogContext); + }, + child: Text(context.l10n.common_save), + ), + ], + ), ); } diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index a1397f52..ce170aed 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -578,10 +578,10 @@ class _ChannelChatScreenState extends State { mainAxisAlignment: isOutgoing ? MainAxisAlignment.end : MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.start, children: [ if (!isOutgoing) ...[ - _buildAvatar(message.senderName), + _buildAvatar(message.senderName, textScale), const SizedBox(width: 6), ], Flexible( @@ -619,10 +619,10 @@ class _ChannelChatScreenState extends State { : EdgeInsets.zero, child: Text( message.senderName, - style: MeshTheme.mono( - fontSize: 11, + style: TextStyle( + fontSize: 13 * textScale, fontWeight: FontWeight.w700, - color: _colorForName(message.senderName), + color: textColor, ), ), ), @@ -1047,8 +1047,11 @@ class _ChannelChatScreenState extends State { ); } - Widget _buildAvatar(String senderName) { - return AvatarCircle(name: senderName, size: 32); + Widget _buildAvatar(String senderName, double textScale) { + return AvatarCircle( + name: senderName, + size: (32 * textScale).clamp(28.0, 56.0), + ); } Widget _buildReplyBanner(double textScale) { @@ -1212,7 +1215,7 @@ class _ChannelChatScreenState extends State { hintText: context.l10n.chat_typeMessage, border: OutlineInputBorder( borderRadius: BorderRadius.circular( - MeshRadii.pill, + MeshRadii.md, ), borderSide: BorderSide( color: scheme.outlineVariant, @@ -1220,7 +1223,7 @@ class _ChannelChatScreenState extends State { ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular( - MeshRadii.pill, + MeshRadii.md, ), borderSide: BorderSide( color: scheme.outlineVariant, @@ -1228,7 +1231,7 @@ class _ChannelChatScreenState extends State { ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular( - MeshRadii.pill, + MeshRadii.md, ), borderSide: BorderSide( color: scheme.primary, @@ -1602,23 +1605,6 @@ class _ChannelChatScreenState extends State { return PathHelper.splitPathBytes(pathBytes, pathHashByteWidth).length; } - /// Deterministic name-to-hue mapping consistent with [AvatarCircle]. - Color _colorForName(String name) { - const hues = [ - MeshPalette.blue, - MeshPalette.magenta, - MeshPalette.signal, - MeshPalette.warn, - Color(0xFF8FA8F0), - Color(0xFF6FD9CE), - ]; - var h = 0; - for (final c in name.codeUnits) { - h = (h * 31 + c) & 0x7fffffff; - } - return hues[h % hues.length]; - } - Future openRegionSelectDialog(Channel channel) async { // The AppBar subtitle reads the region from the connector inside a // Consumer, so setChannelRegion's notifyListeners refreshes it directly — diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index 98a9c874..3132bb46 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -23,7 +23,6 @@ import '../theme/mesh_theme.dart'; import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/mesh_ui.dart'; import '../widgets/path_map_ui.dart'; -import '../widgets/themed_map_tile_layer.dart'; class ChannelMessagePathScreen extends StatelessWidget { final ChannelMessage message; @@ -1014,7 +1013,7 @@ class _ChannelMessagePathMapScreenState }, ), children: [ - ThemedMapTileLayer(tileCache: tileCache), + tileCache.buildTileLayer(context), AnimatedBuilder( animation: _playback, builder: (context, _) { diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index d9846db3..fb5039d2 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -1,9 +1,9 @@ import 'dart:async'; -import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:meshcore_open/storage/channel_message_store.dart'; +import 'package:meshcore_open/utils/keys.dart'; import 'package:meshcore_open/utils/platform_info.dart'; import 'package:meshcore_open/widgets/app_bar.dart'; import 'package:provider/provider.dart'; @@ -27,6 +27,7 @@ import '../widgets/qr_code_display.dart'; import '../widgets/quick_switch_bar.dart'; import '../widgets/sync_progress_overlay.dart'; import '../widgets/unread_badge.dart'; +import '../helpers/gif_helper.dart'; import '../helpers/snack_bar_builder.dart'; import 'channel_chat_screen.dart'; import 'community_qr_scanner_screen.dart'; @@ -410,7 +411,11 @@ class _ChannelsScreenState extends State // Last message preview final messages = connector.getChannelMessages(channel); final lastMessage = messages.isNotEmpty ? messages.last : null; - final lastPreview = lastMessage?.text ?? ''; + final lastMessageText = lastMessage?.text ?? ''; + final lastPreview = lastMessageText.isNotEmpty && + GifHelper.parseGif(lastMessageText) != null + ? context.l10n.chat_receivedGif + : lastMessageText; final lastTime = lastMessage?.timestamp; final channelLabel = channel.name.isEmpty @@ -453,7 +458,7 @@ class _ChannelsScreenState extends State ) : null, child: Row( - crossAxisAlignment: CrossAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, children: [ // Leading avatar with optional community badge Stack( @@ -511,10 +516,12 @@ class _ChannelsScreenState extends State ), ), const SizedBox(width: 6), - StatusChip( - label: 'CH ${channel.index}', - color: MeshPalette.blue, - fontSize: 10, + Text( + 'CH ${channel.index}', + style: MeshTheme.mono( + fontSize: 11, + color: scheme.onSurfaceVariant.withValues(alpha: 0.7), + ), ), ], ), @@ -579,10 +586,13 @@ class _ChannelsScreenState extends State const SizedBox(width: 4), ReorderableDragStartListener( index: dragIndex, + // Top-aligned with the "CH n" / time line. Bottom padding keeps + // a comfortable drag target without pushing the icon down. child: Padding( - padding: const EdgeInsets.all(8), + padding: const EdgeInsets.only(left: 8, right: 8, bottom: 16), child: Icon( Icons.drag_handle, + size: 18, color: scheme.onSurfaceVariant, ), ), @@ -936,11 +946,7 @@ class _ChannelsScreenState extends State ); return; } - final random = Random.secure(); - final psk = Uint8List(16); - for (int i = 0; i < 16; i++) { - psk[i] = random.nextInt(256); - } + final psk = randomBytes(16); Navigator.pop(sheetContext); await connector.setChannel( nextIndex, @@ -1569,11 +1575,7 @@ class _ChannelsScreenState extends State icon: const Icon(Icons.casino), tooltip: sheetContext.l10n.channels_generateRandomPsk, onPressed: () { - final random = Random.secure(); - final bytes = Uint8List(16); - for (int i = 0; i < 16; i++) { - bytes[i] = random.nextInt(256); - } + final bytes = randomBytes(16); pskController.text = Channel.formatPskHex(bytes); }, ), diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index a6b82af8..bfe5a381 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -547,15 +547,15 @@ class _ChatScreenState extends State { decoration: InputDecoration( hintText: context.l10n.chat_typeMessage, border: OutlineInputBorder( - borderRadius: BorderRadius.circular(MeshRadii.pill), + borderRadius: BorderRadius.circular(MeshRadii.md), borderSide: BorderSide(color: scheme.outlineVariant), ), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(MeshRadii.pill), + borderRadius: BorderRadius.circular(MeshRadii.md), borderSide: BorderSide(color: scheme.outlineVariant), ), focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(MeshRadii.pill), + borderRadius: BorderRadius.circular(MeshRadii.md), borderSide: BorderSide( color: scheme.primary, width: 1.5, diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index fe935e43..7117affa 100644 --- a/lib/screens/line_of_sight_map_screen.dart +++ b/lib/screens/line_of_sight_map_screen.dart @@ -20,7 +20,6 @@ import '../widgets/app_bar.dart'; import '../widgets/quick_switch_bar.dart'; import '../icons/los_icon.dart'; import '../theme/mesh_theme.dart'; -import '../widgets/themed_map_tile_layer.dart'; class LineOfSightEndpoint { final String label; @@ -357,7 +356,7 @@ class _LineOfSightMapScreenState extends State { Widget build(BuildContext context) { final settings = context.watch().settings; final isImperial = settings.unitSystem == UnitSystem.imperial; - final tileCache = context.read(); + final tileCache = context.watch(); final endpoints = _visibleEndpoints(); final mapPoints = [ if (_start != null) _start!.point, @@ -443,8 +442,8 @@ class _LineOfSightMapScreenState extends State { }, ), children: [ - ThemedMapTileLayer( - tileCache: tileCache, + tileCache.buildTileLayer( + context, opacity: _showTerrainLayer ? 1 : 0.72, ), if (_result != null && _result!.segments.isNotEmpty) diff --git a/lib/screens/map_cache_screen.dart b/lib/screens/map_cache_screen.dart index 6455c15c..9cb45d36 100644 --- a/lib/screens/map_cache_screen.dart +++ b/lib/screens/map_cache_screen.dart @@ -12,7 +12,6 @@ import '../widgets/adaptive_app_bar_title.dart'; import '../helpers/snack_bar_builder.dart'; import '../theme/mesh_theme.dart'; import '../widgets/mesh_ui.dart'; -import '../widgets/themed_map_tile_layer.dart'; class MapCacheScreen extends StatefulWidget { const MapCacheScreen({super.key}); @@ -34,6 +33,11 @@ class _MapCacheScreenState extends State { bool _isDownloading = false; int _completedTiles = 0; int _failedTiles = 0; + List _cachedTiles = const []; + int _cachedTileBytes = 0; + bool _isLoadingCachedTiles = false; + double _overlayZoom = 2.0; + LatLngBounds? _visibleBounds; @override void initState() { @@ -123,8 +127,10 @@ class _MapCacheScreenState extends State { _minZoom = safeMin; _maxZoom = safeMax; _selectedBounds = bounds; + _visibleBounds = bounds; }); _updateEstimate(); + _refreshCachedTiles(); if (bounds != null) { _mapController.fitCamera( CameraFit.bounds(bounds: bounds, padding: const EdgeInsets.all(48)), @@ -181,6 +187,7 @@ class _MapCacheScreenState extends State { Future _startDownload() async { final bounds = _selectedBounds; + final cacheService = context.read(); if (bounds == null) { showDismissibleSnackBar( context, @@ -197,6 +204,18 @@ class _MapCacheScreenState extends State { return; } + if (!cacheService.source.allowsBulkDownload) { + showDismissibleSnackBar( + context, + content: Text( + context.l10n.mapCache_bulkDownloadDisabledInConfig( + cacheService.source.label, + ), + ), + ); + return; + } + final confirmed = await showDialog( context: context, builder: (dialogContext) => AlertDialog( @@ -219,8 +238,6 @@ class _MapCacheScreenState extends State { if (confirmed != true || !mounted) return; - final cacheService = context.read(); - setState(() { _isDownloading = true; _completedTiles = 0; @@ -254,6 +271,8 @@ class _MapCacheScreenState extends State { result.failed, ) : context.l10n.mapCache_cachedTiles(result.downloaded); + await _refreshCachedTiles(); + if (!mounted) return; showDismissibleSnackBar(context, content: Text(message)); } @@ -280,16 +299,61 @@ class _MapCacheScreenState extends State { final cacheService = context.read(); await cacheService.clearCache(); if (!mounted) return; + await _refreshCachedTiles(); + if (!mounted) return; showDismissibleSnackBar( context, content: Text(context.l10n.mapCache_offlineCacheCleared), ); } + Future _refreshCachedTiles() async { + if (!mounted) return; + setState(() { + _isLoadingCachedTiles = true; + }); + final cacheService = context.read(); + final inventory = await cacheService.getCachedTileInventory(); + if (!mounted) return; + setState(() { + _cachedTiles = inventory.tiles; + _cachedTileBytes = inventory.totalBytes; + _isLoadingCachedTiles = false; + }); + } + + String _formatBytes(int bytes) { + if (bytes < 1024) return '$bytes B'; + if (bytes < 1024 * 1024) { + return '${(bytes / 1024).toStringAsFixed(1)} KB'; + } + return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB'; + } + @override Widget build(BuildContext context) { - final tileCache = context.read(); + final tileCache = context.watch(); + final source = tileCache.source; final selectedBounds = _selectedBounds; + final activeCachedTiles = tileCache.filterTilesForActiveSource( + _cachedTiles, + ); + final cachedInSelection = tileCache.countTilesForBounds( + activeCachedTiles, + bounds: selectedBounds, + minZoom: _minZoom, + maxZoom: _maxZoom, + ); + final overlayPolygons = tileCache.buildCachedTilePolygons( + activeCachedTiles, + zoom: _overlayZoom.round().clamp(0, 19).toInt(), + visibleBounds: _visibleBounds, + ); + final cacheSummary = + '${context.l10n.mapCache_summarySource(source.label)}\n' + '${context.l10n.mapCache_summaryCachedTilesForSource(activeCachedTiles.length)}\n' + '${context.l10n.mapCache_summaryCachedInSelection(cachedInSelection)}\n' + '${context.l10n.mapCache_summaryApproxCacheSize(_formatBytes(_cachedTileBytes))}'; final l10n = context.l10n; final scheme = Theme.of(context).colorScheme; final isDesktop = _isDesktopPlatform(defaultTargetPlatform); @@ -327,9 +391,23 @@ class _MapCacheScreenState extends State { ) : const KeyboardOptions.disabled(), ), + onPositionChanged: (camera, hasGesture) { + final nextZoom = camera.zoom; + final nextBounds = camera.visibleBounds; + if (_overlayZoom != nextZoom || + _visibleBounds?.north != nextBounds.north || + _visibleBounds?.south != nextBounds.south || + _visibleBounds?.east != nextBounds.east || + _visibleBounds?.west != nextBounds.west) { + setState(() { + _overlayZoom = nextZoom; + _visibleBounds = nextBounds; + }); + } + }, ), children: [ - ThemedMapTileLayer(tileCache: tileCache), + tileCache.buildTileLayer(context), if (selectedBounds != null) PolygonLayer( polygons: [ @@ -341,9 +419,24 @@ class _MapCacheScreenState extends State { ), ], ), + if (overlayPolygons.isNotEmpty) + PolygonLayer(polygons: overlayPolygons), ], ), if (isDesktop) _buildDesktopMapControls(), + Positioned( + top: 12, + left: isDesktop ? 84 : 12, + child: Card( + child: Padding( + padding: const EdgeInsets.all(8), + child: Text( + 'Z: ${_overlayZoom.round()}:', + style: const TextStyle(fontSize: 12), + ), + ), + ), + ), Positioned( top: 12, right: 12, @@ -445,6 +538,34 @@ class _MapCacheScreenState extends State { color: scheme.onSurfaceVariant, ), ), + const SizedBox(height: 12), + TextFormField( + key: ValueKey( + '$cacheSummary|${activeCachedTiles.length}|$cachedInSelection|$_cachedTileBytes', + ), + initialValue: cacheSummary, + readOnly: true, + minLines: 4, + maxLines: 4, + decoration: InputDecoration( + labelText: _isLoadingCachedTiles + ? l10n.mapCache_cachedTilesLabel + : l10n.mapCache_cachedTileSummaryLabel, + border: const OutlineInputBorder(), + ), + ), + if (!source.allowsBulkDownload) ...[ + const SizedBox(height: 8), + Text( + l10n.mapCache_bulkDownloadDisabledForSource( + source.label, + ), + style: MeshTheme.mono( + fontSize: 12, + color: MeshPalette.alert, + ), + ), + ], if (_isDownloading) ...[ const SizedBox(height: 8), LinearProgressIndicator( @@ -471,7 +592,10 @@ class _MapCacheScreenState extends State { child: ElevatedButton.icon( icon: const Icon(Icons.download), label: Text(l10n.mapCache_downloadTilesButton), - onPressed: _isDownloading || selectedBounds == null + onPressed: + _isDownloading || + selectedBounds == null || + !source.allowsBulkDownload ? null : _startDownload, ), diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index ef8798a9..b283cd88 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -26,7 +26,6 @@ import '../utils/battery_utils.dart'; import '../utils/route_transitions.dart'; import '../widgets/quick_switch_bar.dart'; import '../widgets/sync_progress_overlay.dart'; -import '../widgets/themed_map_tile_layer.dart'; import '../icons/los_icon.dart'; import 'channels_screen.dart'; import 'chat_screen.dart'; @@ -109,6 +108,31 @@ class _MapScreenState extends State { super.dispose(); } + ColorScheme get _overlayScheme => Theme.of(context).colorScheme; + + bool get _useDarkOverlay => Theme.of(context).brightness == Brightness.dark; + + Color get _overlayPanelColor => _useDarkOverlay + ? MapPalette.panelDark + : _overlayScheme.surfaceContainerLow.withValues(alpha: 0.96); + + Color get _overlayPrimaryTextColor => + _useDarkOverlay ? MapPalette.textPrimary : _overlayScheme.onSurface; + + Color get _overlaySecondaryTextColor => _useDarkOverlay + ? MapPalette.textSecondary + : _overlayScheme.onSurfaceVariant; + + Color get _overlayMutedTextColor => + _useDarkOverlay ? MapPalette.textMuted : _overlayScheme.onSurfaceVariant; + + Color get _overlayBorderColor => + _useDarkOverlay ? MapPalette.border : _overlayScheme.outlineVariant; + + Color get _overlayShadowColor => _useDarkOverlay + ? MapPalette.markerShadow + : Colors.black.withValues(alpha: 0.18); + _NodeAge _ageOf(Contact contact) { final d = DateTime.now().difference(contact.lastSeen); if (d.inMinutes <= 60) return _NodeAge.online; @@ -235,12 +259,12 @@ class _MapScreenState extends State { bottom: 96, child: DecoratedBox( decoration: BoxDecoration( - color: MapPalette.panelDark, + color: _overlayPanelColor, borderRadius: BorderRadius.circular(MeshRadii.md), - border: Border.all(color: MapPalette.border), - boxShadow: const [ + border: Border.all(color: _overlayBorderColor), + boxShadow: [ BoxShadow( - color: MapPalette.markerShadow, + color: _overlayShadowColor, blurRadius: 8, offset: Offset(0, 3), ), @@ -252,20 +276,20 @@ class _MapScreenState extends State { mainAxisSize: MainAxisSize.min, children: [ IconButton( - color: MapPalette.textPrimary, + color: _overlayPrimaryTextColor, icon: const Icon(Icons.add), visualDensity: VisualDensity.standard, tooltip: context.l10n.map_zoomIn, onPressed: () => _zoomMapBy(1), ), IconButton( - color: MapPalette.textPrimary, + color: _overlayPrimaryTextColor, icon: const Icon(Icons.remove), tooltip: context.l10n.map_zoomOut, onPressed: () => _zoomMapBy(-1), ), IconButton( - color: MapPalette.textPrimary, + color: _overlayPrimaryTextColor, icon: const Icon(Icons.crop_free), tooltip: context.l10n.map_centerMap, onPressed: () => _mapController.move(center, zoom), @@ -330,6 +354,7 @@ class _MapScreenState extends State { final settingsService = context.read(); final pathHistory = context.read(); final tileCache = context.read(); + final scheme = Theme.of(context).colorScheme; final isDesktop = _isDesktopPlatform(defaultTargetPlatform); final allContacts = connector.allContacts; @@ -585,8 +610,8 @@ class _MapScreenState extends State { canPop: allowBack, child: Scaffold( appBar: AppBar( - backgroundColor: MapPalette.panelDark, - foregroundColor: MapPalette.textPrimary, + backgroundColor: scheme.surface, + foregroundColor: scheme.onSurface, title: AppBarTitle(context.l10n.map_title), centerTitle: true, automaticallyImplyLeading: false, @@ -759,7 +784,7 @@ class _MapScreenState extends State { }, ), children: [ - ThemedMapTileLayer(tileCache: tileCache), + tileCache.buildTileLayer(context), if (_polylines.isNotEmpty && _isBuildingPathTrace) PolylineLayer(polylines: _polylines), if (sharedMarkerPolylines.isNotEmpty) @@ -912,7 +937,7 @@ class _MapScreenState extends State { _handleQuickSwitch(index, context), contactsUnreadCount: connector.getTotalContactsUnreadCount(), channelsUnreadCount: connector.getTotalChannelsUnreadCount(), - highContrast: true, + highContrast: _useDarkOverlay, ), ), floatingActionButton: @@ -1211,14 +1236,14 @@ class _MapScreenState extends State { height: 36, decoration: BoxDecoration( shape: BoxShape.circle, - color: MapPalette.panelDark, + color: _overlayPanelColor, border: Border.all( - color: guess.highConfidence ? color : MapPalette.textMuted, + color: guess.highConfidence ? color : _overlayMutedTextColor, width: guess.highConfidence ? 2.5 : 2, ), - boxShadow: const [ + boxShadow: [ BoxShadow( - color: MapPalette.markerShadow, + color: _overlayShadowColor, blurRadius: 7, offset: Offset(0, 2), ), @@ -1227,7 +1252,7 @@ class _MapScreenState extends State { alignment: Alignment.center, child: Icon( Icons.not_listed_location, - color: MapPalette.textPrimary, + color: _overlayPrimaryTextColor, size: 19, ), ), @@ -1563,12 +1588,12 @@ class _MapScreenState extends State { child: Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( - color: MapPalette.panelDark, + color: _overlayPanelColor, borderRadius: BorderRadius.circular(MeshRadii.xs), - border: Border.all(color: MapPalette.border), - boxShadow: const [ + border: Border.all(color: _overlayBorderColor), + boxShadow: [ BoxShadow( - color: MapPalette.markerShadow, + color: _overlayShadowColor, blurRadius: 4, offset: Offset(0, 1), ), @@ -1582,7 +1607,7 @@ class _MapScreenState extends State { style: MeshTheme.mono( fontSize: 10, fontWeight: FontWeight.w700, - color: MapPalette.textPrimary, + color: _overlayPrimaryTextColor, ), ), ), @@ -1703,7 +1728,7 @@ class _MapScreenState extends State { decoration: BoxDecoration( shape: BoxShape.circle, color: statusColor, - border: Border.all(color: MapPalette.panelDark, width: 2), + border: Border.all(color: _overlayPanelColor, width: 2), ), alignment: Alignment.center, child: batteryLow @@ -1726,10 +1751,10 @@ class _MapScreenState extends State { Expanded( child: Text( label, - style: const TextStyle( + style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, - color: MapPalette.textSecondary, + color: _overlaySecondaryTextColor, ), overflow: TextOverflow.ellipsis, ), @@ -1787,9 +1812,9 @@ class _MapScreenState extends State { children: [ Expanded( child: Material( - color: MapPalette.panelDark, + color: _overlayPanelColor, shape: StadiumBorder( - side: const BorderSide(color: MapPalette.border), + side: BorderSide(color: _overlayBorderColor), ), clipBehavior: Clip.antiAlias, child: TextField( @@ -1797,17 +1822,15 @@ class _MapScreenState extends State { focusNode: _searchFocus, decoration: InputDecoration( hintText: context.l10n.map_searchHint, - hintStyle: const TextStyle( - color: MapPalette.textSecondary, - ), - prefixIcon: const Icon( + hintStyle: TextStyle(color: _overlaySecondaryTextColor), + prefixIcon: Icon( Icons.search, size: 20, - color: MapPalette.textPrimary, + color: _overlayPrimaryTextColor, ), suffixIcon: hasQuery ? IconButton( - color: MapPalette.textPrimary, + color: _overlayPrimaryTextColor, icon: const Icon(Icons.close, size: 18), onPressed: () { setState(() { @@ -1827,9 +1850,9 @@ class _MapScreenState extends State { vertical: 12, ), ), - style: const TextStyle( + style: TextStyle( fontSize: 14, - color: MapPalette.textPrimary, + color: _overlayPrimaryTextColor, fontWeight: FontWeight.w600, ), cursorColor: MapPalette.selected, @@ -1841,9 +1864,9 @@ class _MapScreenState extends State { ), const SizedBox(width: 8), Material( - color: MapPalette.panelDark, + color: _overlayPanelColor, shape: StadiumBorder( - side: const BorderSide(color: MapPalette.border), + side: BorderSide(color: _overlayBorderColor), ), clipBehavior: Clip.antiAlias, child: InkWell( @@ -1867,17 +1890,17 @@ class _MapScreenState extends State { style: MeshTheme.mono( fontSize: 13, fontWeight: FontWeight.w700, - color: MapPalette.textPrimary, + color: _overlayPrimaryTextColor, ), ), const SizedBox(width: 2), AnimatedRotation( turns: _statsExpanded ? 0.5 : 0, duration: const Duration(milliseconds: 200), - child: const Icon( + child: Icon( Icons.expand_more, size: 16, - color: MapPalette.textPrimary, + color: _overlayPrimaryTextColor, ), ), ], @@ -1976,12 +1999,12 @@ class _MapScreenState extends State { color: selected ? Color.alphaBlend( accent.withValues(alpha: 0.34), - MapPalette.panelDark, + _overlayPanelColor, ) - : MapPalette.panelDark, + : _overlayPanelColor, shape: StadiumBorder( side: BorderSide( - color: selected ? accent : MapPalette.border, + color: selected ? accent : _overlayBorderColor, width: selected ? 1.5 : 1, ), ), @@ -1997,11 +2020,7 @@ class _MapScreenState extends State { mainAxisSize: MainAxisSize.min, children: [ if (selected) ...[ - const Icon( - Icons.check, - size: 13, - color: MapPalette.textPrimary, - ), + Icon(Icons.check, size: 13, color: _overlayPrimaryTextColor), const SizedBox(width: 4), ], Text( @@ -2010,8 +2029,8 @@ class _MapScreenState extends State { fontSize: 12.5, fontWeight: FontWeight.w600, color: selected - ? MapPalette.textPrimary - : MapPalette.textSecondary, + ? _overlayPrimaryTextColor + : _overlaySecondaryTextColor, ), ), ], @@ -2041,14 +2060,14 @@ class _MapScreenState extends State { margin: const EdgeInsets.only(top: 6), constraints: const BoxConstraints(maxHeight: 300), decoration: BoxDecoration( - color: MapPalette.panelDark, + color: _overlayPanelColor, borderRadius: BorderRadius.circular(MeshRadii.md), - border: Border.all(color: MapPalette.border), - boxShadow: const [ + border: Border.all(color: _overlayBorderColor), + boxShadow: [ BoxShadow( - color: MapPalette.markerShadow, + color: _overlayShadowColor, blurRadius: 10, - offset: Offset(0, 4), + offset: const Offset(0, 4), ), ], ), @@ -2057,8 +2076,8 @@ class _MapScreenState extends State { padding: const EdgeInsets.all(16), child: Text( context.l10n.map_noResults, - style: const TextStyle( - color: MapPalette.textSecondary, + style: TextStyle( + color: _overlaySecondaryTextColor, fontSize: 13, ), ), @@ -2068,7 +2087,7 @@ class _MapScreenState extends State { padding: const EdgeInsets.symmetric(vertical: 4), itemCount: results.length, separatorBuilder: (_, _) => - const Divider(height: 1, color: MapPalette.border), + Divider(height: 1, color: _overlayBorderColor), itemBuilder: (context, index) { final c = results[index]; final color = _getNodeColor(c.type); @@ -2089,10 +2108,10 @@ class _MapScreenState extends State { children: [ Text( c.name, - style: const TextStyle( + style: TextStyle( fontSize: 13.5, fontWeight: FontWeight.w600, - color: MapPalette.textPrimary, + color: _overlayPrimaryTextColor, ), overflow: TextOverflow.ellipsis, ), @@ -2100,7 +2119,7 @@ class _MapScreenState extends State { c.publicKeyHex.substring(0, 12), style: MeshTheme.mono( fontSize: 10.5, - color: MapPalette.textSecondary, + color: _overlaySecondaryTextColor, ), ), ], @@ -2110,13 +2129,13 @@ class _MapScreenState extends State { Icon( Icons.chevron_right, size: 18, - color: MapPalette.textSecondary, + color: _overlaySecondaryTextColor, ) else Text( context.l10n.map_noGps.toUpperCase(), style: MeshTheme.accentLabel( - color: MapPalette.textMuted, + color: _overlayMutedTextColor, fontSize: 8.5, ), ), @@ -2176,14 +2195,14 @@ class _MapScreenState extends State { width: 230, padding: const EdgeInsets.fromLTRB(14, 12, 14, 12), decoration: BoxDecoration( - color: MapPalette.panelDark, + color: _overlayPanelColor, borderRadius: BorderRadius.circular(MeshRadii.md), - border: Border.all(color: MapPalette.border), - boxShadow: const [ + border: Border.all(color: _overlayBorderColor), + boxShadow: [ BoxShadow( - color: MapPalette.markerShadow, + color: _overlayShadowColor, blurRadius: 10, - offset: Offset(0, 4), + offset: const Offset(0, 4), ), ], ), @@ -2200,7 +2219,7 @@ class _MapScreenState extends State { ), _statRow(context.l10n.map_hidden, hiddenCount, MapPalette.offline), _statRow(context.l10n.map_markers, pinCount, MapPalette.shared), - const Divider(height: 16, color: MapPalette.border), + Divider(height: 16, color: _overlayBorderColor), _buildLegendItem( Icons.person, context.l10n.map_chat, @@ -2230,7 +2249,7 @@ class _MapScreenState extends State { _buildLegendItem( Icons.not_listed_location, context.l10n.map_guessedLocation, - MapPalette.textMuted, + _overlayMutedTextColor, ), ], ), @@ -2251,7 +2270,10 @@ class _MapScreenState extends State { Expanded( child: Text( label, - style: TextStyle(fontSize: 12.5, color: MapPalette.textSecondary), + style: TextStyle( + fontSize: 12.5, + color: _overlaySecondaryTextColor, + ), overflow: TextOverflow.ellipsis, ), ), @@ -2260,7 +2282,7 @@ class _MapScreenState extends State { style: MeshTheme.mono( fontSize: 13, fontWeight: FontWeight.w700, - color: MapPalette.textPrimary, + color: _overlayPrimaryTextColor, ), ), ], @@ -2293,8 +2315,8 @@ class _MapScreenState extends State { child: MeshCard( margin: EdgeInsets.zero, padding: const EdgeInsets.fromLTRB(14, 12, 8, 12), - color: MapPalette.panelDark, - borderColor: MapPalette.border, + color: _overlayPanelColor, + borderColor: _overlayBorderColor, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -2317,10 +2339,10 @@ class _MapScreenState extends State { Flexible( child: Text( contact.name, - style: const TextStyle( + style: TextStyle( fontSize: 15, fontWeight: FontWeight.w700, - color: MapPalette.textPrimary, + color: _overlayPrimaryTextColor, ), overflow: TextOverflow.ellipsis, ), @@ -2348,9 +2370,9 @@ class _MapScreenState extends State { Flexible( child: Text( contact.typeLabel(context.l10n), - style: const TextStyle( + style: TextStyle( fontSize: 11.5, - color: MapPalette.textSecondary, + color: _overlaySecondaryTextColor, ), overflow: TextOverflow.ellipsis, ), @@ -2362,13 +2384,13 @@ class _MapScreenState extends State { ), if (pos != null) IconButton( - color: MapPalette.textPrimary, + color: _overlayPrimaryTextColor, icon: const Icon(Icons.center_focus_strong, size: 20), tooltip: context.l10n.map_centerOnNode, onPressed: () => _mapController.move(pos, max(_zoom, 15)), ), IconButton( - color: MapPalette.textPrimary, + color: _overlayPrimaryTextColor, icon: const Icon(Icons.close, size: 20), onPressed: _clearSelection, ), @@ -2432,14 +2454,17 @@ class _MapScreenState extends State { Text( label.toUpperCase(), style: MeshTheme.accentLabel( - color: MapPalette.textMuted, + color: _overlayMutedTextColor, fontSize: 8, ), ), const SizedBox(height: 1), Text( value, - style: MeshTheme.mono(fontSize: 11.5, color: MapPalette.textPrimary), + style: MeshTheme.mono( + fontSize: 11.5, + color: _overlayPrimaryTextColor, + ), ), ], ); @@ -3608,14 +3633,14 @@ class _MapScreenState extends State { right: 16, child: DecoratedBox( decoration: BoxDecoration( - color: MapPalette.panelDark, + color: _overlayPanelColor, borderRadius: BorderRadius.circular(MeshRadii.md), - border: Border.all(color: MapPalette.border), - boxShadow: const [ + border: Border.all(color: _overlayBorderColor), + boxShadow: [ BoxShadow( - color: MapPalette.markerShadow, + color: _overlayShadowColor, blurRadius: 10, - offset: Offset(0, 4), + offset: const Offset(0, 4), ), ], ), @@ -3628,18 +3653,27 @@ class _MapScreenState extends State { children: [ Text( l10n.contacts_pathTrace, - style: TextStyle(fontWeight: FontWeight.bold), + style: TextStyle( + fontWeight: FontWeight.bold, + color: _overlayPrimaryTextColor, + ), ), if (_pathTrace.isEmpty) const SizedBox(height: 8), if (_pathTrace.isEmpty) - Text(l10n.map_tapToAdd, style: TextStyle(fontSize: 12)), + Text( + l10n.map_tapToAdd, + style: TextStyle( + fontSize: 12, + color: _overlaySecondaryTextColor, + ), + ), const SizedBox(height: 6), if (_pathTrace.isNotEmpty) Text( "${l10n.path_currentPathLabel} ${formatDistance(getPathDistanceMeters(_points), isImperial: isImperial)}", style: MeshTheme.mono( fontSize: 12, - color: MapPalette.textSecondary, + color: _overlaySecondaryTextColor, ), ), SelectableText( diff --git a/lib/screens/neighbors_screen.dart b/lib/screens/neighbors_screen.dart index 529a00dd..219afa6e 100644 --- a/lib/screens/neighbors_screen.dart +++ b/lib/screens/neighbors_screen.dart @@ -276,7 +276,7 @@ class _NeighborsScreenState extends State { return Scaffold( appBar: AppBar( title: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ Text( @@ -296,7 +296,7 @@ class _NeighborsScreenState extends State { ), ], ), - centerTitle: false, + centerTitle: true, actions: [ IconButton( icon: Icon(isFloodMode ? Icons.waves : Icons.route), diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index 6d095a04..3d4032c8 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -20,7 +20,6 @@ import 'package:meshcore_open/services/path_history_service.dart'; import 'package:meshcore_open/utils/app_logger.dart'; import 'package:meshcore_open/widgets/path_map_ui.dart'; import 'package:meshcore_open/widgets/snr_indicator.dart'; -import 'package:meshcore_open/widgets/themed_map_tile_layer.dart'; import 'package:provider/provider.dart'; import '../theme/mesh_theme.dart'; @@ -1576,7 +1575,7 @@ class _PathTraceMapScreenState extends State }, ), children: [ - ThemedMapTileLayer(tileCache: tileCache), + tileCache.buildTileLayer(context), AnimatedBuilder( animation: _playback, builder: (context, _) { diff --git a/lib/screens/repeater_settings_screen.dart b/lib/screens/repeater_settings_screen.dart index dca0d7f4..6c6d633a 100644 --- a/lib/screens/repeater_settings_screen.dart +++ b/lib/screens/repeater_settings_screen.dart @@ -1,6 +1,7 @@ import 'dart:async'; -import 'dart:typed_data'; +import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../l10n/l10n.dart'; import '../models/contact.dart'; @@ -11,6 +12,7 @@ import '../services/storage_service.dart'; import '../theme/mesh_theme.dart'; import '../widgets/mesh_ui.dart'; import '../widgets/routing_sheet.dart'; +import '../utils/keys.dart'; import '../helpers/snack_bar_builder.dart'; class RepeaterSettingsScreen extends StatefulWidget { @@ -44,6 +46,7 @@ enum _SettingField { advertInterval, floodAdvertInterval, pathHashMode, + prvKey, txDelay, directTxDelay, intThresh, @@ -109,6 +112,8 @@ class _RepeaterSettingsScreenState extends State { bool _refreshingIntThresh = false; bool _refreshingAgcResetInterval = false; bool _runningAction = false; + bool _searchingForKeyPair = false; + bool _stopSearchingForKeyPair = false; StreamSubscription? _frameSubscription; RepeaterCommandService? _commandService; @@ -154,6 +159,11 @@ class _RepeaterSettingsScreenState extends State { // Advanced int _pathHashMode = 0; // 0-2 + final TextEditingController _prvKeyController = TextEditingController(); + final TextEditingController _pubKeyController = TextEditingController(); + final TextEditingController _prvKeyPrefixController = TextEditingController(); + final KeyPairSearcher _keyPairSearcher = KeyPairSearcher(); + final int _searchChunkTime = 30; // time in ms to search before yielding final TextEditingController _txDelayController = TextEditingController(); final TextEditingController _directTxDelayController = TextEditingController(); @@ -206,6 +216,10 @@ class _RepeaterSettingsScreenState extends State { _txDelayController.dispose(); _directTxDelayController.dispose(); _intThreshController.dispose(); + _prvKeyController.dispose(); + _pubKeyController.dispose(); + _prvKeyPrefixController.dispose(); + _stopSearchingForKeyPair = true; super.dispose(); } @@ -860,6 +874,12 @@ class _RepeaterSettingsScreenState extends State { command: 'set path.hash.mode $_pathHashMode', )); } + if (_dirtyFields.contains(_SettingField.prvKey)) { + final v = _prvKeyController.text.trim(); + if (v != "") { + pending.add((field: _SettingField.prvKey, command: 'set prv.key $v')); + } + } if (_dirtyFields.contains(_SettingField.txDelay) && _txDelayController.text.isNotEmpty) { final v = double.tryParse(_txDelayController.text.trim()); @@ -1005,6 +1025,36 @@ class _RepeaterSettingsScreenState extends State { } } + void _startSearchingForKeyPair() async { + if (_searchingForKeyPair) return; + setState(() { + _searchingForKeyPair = true; + _stopSearchingForKeyPair = false; + }); + _searchForKeyPair(); + } + + void _searchForKeyPair() async { + if (_stopSearchingForKeyPair) { + if (mounted) setState(() => _searchingForKeyPair = false); + return; + } + final String prefix = _prvKeyPrefixController.text.trim(); + MCKeyPair? keys = await _keyPairSearcher.findMatchingKeyPair( + prefix, + _searchChunkTime, + ); + if (keys == null) { + // Ran out of time; schedule another attempt. + Timer.run(_searchForKeyPair); + } else { + setState(() => _searchingForKeyPair = false); + _prvKeyController.text = pubKeyToHex(keys.private); + _pubKeyController.text = pubKeyToHex(keys.public); + _markChanged(_SettingField.prvKey); + } + } + Widget _buildInlineRefreshButton({ required bool isRefreshing, required VoidCallback onRefresh, @@ -1069,6 +1119,7 @@ class _RepeaterSettingsScreenState extends State { _buildOwnerInfoCard(), _buildActionsCard(), _buildAdvancedCard(), + _buildKeysCard(), const SizedBox(height: 16), _buildDangerZoneCard(), ], @@ -1984,6 +2035,127 @@ class _RepeaterSettingsScreenState extends State { ); } + Widget _buildKeysCard() { + final l10n = context.l10n; + return MeshCard( + child: ExpansionTile( + leading: const Icon(Icons.vpn_key), + title: Text( + l10n.repeater_keySettings, + style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600), + ), + subtitle: Text(l10n.repeater_keySettingsSubtitle), + childrenPadding: const EdgeInsets.fromLTRB(0, 8, 0, 4), + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: TextField( + controller: _prvKeyController, + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp("[0-9a-fA-F]")), + ], + decoration: InputDecoration( + labelText: l10n.repeater_prvKey, + helperText: l10n.repeater_prvKeyHelper, + helperMaxLines: 3, + border: const OutlineInputBorder(), + ), + onChanged: (_) { + _pubKeyController.text = ""; + if (_prvKeyController.text.isNotEmpty) { + _markChanged(_SettingField.prvKey); + } + }, + ), + ), + const SizedBox(width: 8), + Padding( + padding: const EdgeInsets.only(top: 8), + child: _searchingForKeyPair + ? IconButton( + icon: const Icon(Icons.cancel, size: 24), + onPressed: (() => _stopSearchingForKeyPair = true), + tooltip: l10n.repeater_stopGeneratingPrvKey, + visualDensity: VisualDensity.compact, + ) + : IconButton( + icon: const Icon(Icons.casino_rounded, size: 24), + onPressed: () { + _startSearchingForKeyPair(); + }, + tooltip: l10n.repeater_generatePrvKey, + visualDensity: VisualDensity.compact, + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: TextField( + readOnly: true, + controller: _pubKeyController, + style: TextStyle( + color: Theme.of( + context, + ).textTheme.headlineSmall?.color?.withValues(alpha: 0.6), + ), + decoration: InputDecoration( + labelText: l10n.repeater_pubKey, + helperText: l10n.repeater_pubKeyHelper, + helperMaxLines: 3, + border: const OutlineInputBorder(), + ), + ), + ), + const SizedBox(width: 16), + Padding( + padding: const EdgeInsets.only(top: 12), + child: SizedBox( + width: 24, + height: 24, + child: _searchingForKeyPair + ? CircularProgressIndicator(strokeWidth: 2) + : null, + ), + ), + const SizedBox(width: 8), + ], + ), + const SizedBox(height: 16), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: TextField( + controller: _prvKeyPrefixController, + maxLength: 4, + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp("[0-9a-fA-F]")), + ], + onChanged: (_) => setState(() => {}), // updates helper text + decoration: InputDecoration( + labelText: l10n.repeater_pubKeyPrefix, + helperText: l10n.repeater_pubKeyPrefixHelper( + pow(16, _prvKeyPrefixController.text.length).toInt(), + ), + helperMaxLines: 3, + border: const OutlineInputBorder(), + ), + ), + ), + const SizedBox(width: 48), + ], + ), + ], + ), + ); + } + Widget _buildDangerZoneCard() { final l10n = context.l10n; return MeshCard( diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 381a3335..17cca639 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -9,6 +9,7 @@ import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; import '../l10n/l10n.dart'; import '../models/radio_settings.dart'; +import '../services/app_settings_service.dart'; import '../services/app_debug_log_service.dart'; import '../theme/mesh_theme.dart'; import '../widgets/app_bar.dart'; @@ -862,6 +863,7 @@ class _SettingsScreenState extends State { void _editLocation(BuildContext context, MeshCoreConnector connector) { final l10n = context.l10n; + final settingsService = context.read(); final latController = TextEditingController(); final lonController = TextEditingController(); final intervalController = TextEditingController(); @@ -873,9 +875,9 @@ class _SettingsScreenState extends State { final bool hasGPS = customVars.containsKey("gps"); bool isGPSEnabled = customVars["gps"] == "1"; - // Read current interval or default to 900 (15 minutes) - final currentInterval = - int.tryParse(customVars["gps_interval"] ?? "") ?? 900; + final currentInterval = settingsService.resolvedGpsIntervalSeconds( + customVars, + ); intervalController.text = currentInterval.toString(); String? intervalError; @@ -973,7 +975,11 @@ class _SettingsScreenState extends State { Navigator.pop(context); if (interval != null) { - await connector.setCustomVar("gps_interval:$interval"); + await settingsService.setGpsIntervalSeconds( + interval, + writeToDevice: (value) => + connector.setCustomVar("gps_interval:$value"), + ); await connector.refreshDeviceInfo(); if (!context.mounted) return; showDismissibleSnackBar( @@ -1273,12 +1279,15 @@ class _SettingsScreenState extends State { void _privacySettings(BuildContext context, MeshCoreConnector connector) { final l10n = context.l10n; + final settingsService = context.read(); int telemetryMode = connector.telemetryModeBase; int telemetryLocMode = connector.telemetryModeLoc; int telemetryEnvMode = connector.telemetryModeEnv; bool advertLocPolicy = connector.advertLocationPolicy == 0 ? false : true; int multiAcks = connector.multiAcks; + bool autoZeroHopAdvertOnGpsUpdate = + settingsService.settings.autoSendZeroHopAdvertOnGpsUpdate; final telemModeBase = [ DropdownMenuItem(value: teleModeDeny, child: Text(l10n.settings_denyAll)), @@ -1313,6 +1322,20 @@ void _privacySettings(BuildContext context, MeshCoreConnector connector) { }, ), const SizedBox(height: 8), + FeatureToggleRow( + title: l10n.settings_autoZeroHopAdvertOnGpsUpdate, + subtitle: l10n.settings_autoZeroHopAdvertOnGpsUpdateSubtitle, + value: autoZeroHopAdvertOnGpsUpdate, + enabled: advertLocPolicy, + onChanged: advertLocPolicy + ? (value) { + setDialogState( + () => autoZeroHopAdvertOnGpsUpdate = value, + ); + } + : null, + ), + const SizedBox(height: 8), SwitchListTile( title: Text(l10n.settings_multiAck), value: multiAcks == 1, @@ -1381,6 +1404,9 @@ void _privacySettings(BuildContext context, MeshCoreConnector connector) { advertLocPolicy ? 1 : 0, multiAcks, ); + await settingsService.setAutoSendZeroHopAdvertOnGpsUpdate( + autoZeroHopAdvertOnGpsUpdate, + ); await connector.refreshDeviceInfo(); if (!context.mounted) return; showDismissibleSnackBar( diff --git a/lib/screens/telemetry_screen.dart b/lib/screens/telemetry_screen.dart index fafa68f2..96a59c75 100644 --- a/lib/screens/telemetry_screen.dart +++ b/lib/screens/telemetry_screen.dart @@ -334,7 +334,7 @@ class _TelemetryScreenState extends State { return Scaffold( appBar: AppBar( title: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ Text( @@ -350,7 +350,7 @@ class _TelemetryScreenState extends State { ), ], ), - centerTitle: false, + centerTitle: true, bottom: const SyncProgressAppBarBottom(), actions: [ IconButton( diff --git a/lib/services/app_settings_service.dart b/lib/services/app_settings_service.dart index 88ff0f47..e695ba99 100644 --- a/lib/services/app_settings_service.dart +++ b/lib/services/app_settings_service.dart @@ -13,6 +13,14 @@ class AppSettingsService extends ChangeNotifier { AppSettings get settings => _settings; + int resolvedGpsIntervalSeconds(Map? deviceCustomVars) { + final deviceValue = int.tryParse(deviceCustomVars?['gps_interval'] ?? ''); + if (deviceValue != null && deviceValue >= 0) { + return deviceValue; + } + return _settings.gpsIntervalSeconds; + } + String batteryChemistryForDevice(String deviceId) { final stored = _settings.batteryChemistryByDeviceId[deviceId]; if (stored == 'liion') return 'nmc'; @@ -112,6 +120,25 @@ class AppSettingsService extends ChangeNotifier { ); } + Future setMapRasterSourceId(String value) async { + await updateSettings(_settings.copyWith(mapRasterSourceId: value)); + } + + Future setMapTileEndpointId(String value) async { + await updateSettings(_settings.copyWith(mapTileEndpointId: value)); + } + + Future setMapTileApiKey(String? value) async { + final normalized = value?.trim(); + await updateSettings( + _settings.copyWith( + mapTileApiKey: (normalized == null || normalized.isEmpty) + ? null + : normalized, + ), + ); + } + Future setNotificationsEnabled(bool value) async { await updateSettings(_settings.copyWith(notificationsEnabled: value)); } @@ -128,6 +155,28 @@ class AppSettingsService extends ChangeNotifier { await updateSettings(_settings.copyWith(notifyOnNewAdvert: value)); } + Future setAutoSendZeroHopAdvertOnGpsUpdate(bool value) async { + await updateSettings( + _settings.copyWith(autoSendZeroHopAdvertOnGpsUpdate: value), + ); + } + + Future setGpsIntervalSeconds( + int value, { + Future Function(int value)? writeToDevice, + }) async { + await updateSettings(_settings.copyWith(gpsIntervalSeconds: value)); + if (writeToDevice == null) return; + try { + await writeToDevice(value); + } catch (e) { + appLogger.warn( + 'Failed to write GPS interval to device: $e', + tag: 'AppSettings', + ); + } + } + Future setAutoRouteRotationEnabled(bool value) async { await updateSettings(_settings.copyWith(autoRouteRotationEnabled: value)); } diff --git a/lib/services/map_tile_cache_service.dart b/lib/services/map_tile_cache_service.dart index e0d4e573..07584132 100644 --- a/lib/services/map_tile_cache_service.dart +++ b/lib/services/map_tile_cache_service.dart @@ -1,12 +1,232 @@ import 'dart:math' as math; import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; +import 'package:flutter_map/flutter_map.dart'; -const String kMapTileUrlTemplate = - 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'; +import '../models/app_settings.dart'; +import 'app_settings_service.dart'; + +enum MapRasterSourcePreset { + osmAuto('osm_auto'), + osmStandard('osm_standard'), + osmDark('osm_dark'), + stamenTerrain('stamen_terrain'), + alidadeSmoothDark('alidade_smooth_dark'), + outdoors('outdoors'), + osmBright('osm_bright'), + outdoorsDark('outdoors_dark'), + osmBrightDark('osm_bright_dark'); + + const MapRasterSourcePreset(this.id); + + final String id; + + static MapRasterSourcePreset fromId(String id) { + for (final value in values) { + if (value.id == id) return value; + } + return MapRasterSourcePreset.osmAuto; + } +} + +enum MapRasterEndpointPreset { + standard('standard'), + standard2x('standard_2x'), + eu('eu'), + eu2x('eu_2x'); + + const MapRasterEndpointPreset(this.id); + + final String id; + + static MapRasterEndpointPreset fromId(String id) { + for (final value in values) { + if (value.id == id) return value; + } + return MapRasterEndpointPreset.standard; + } +} + +@immutable +class MapRasterSourceDefinition { + const MapRasterSourceDefinition({ + required this.id, + required this.label, + required this.description, + this.isStadia = false, + this.allowsBulkDownload = false, + }); + + final String id; + final String label; + final String description; + final bool isStadia; + final bool allowsBulkDownload; +} + +@immutable +class MapRasterEndpointDefinition { + const MapRasterEndpointDefinition({ + required this.id, + required this.label, + required this.description, + required this.host, + this.scaleSuffix = '', + }); + + final String id; + final String label; + final String description; + final String host; + final String scaleSuffix; +} + +class MapRasterSourceCatalog { + static const MapRasterSourceDefinition osmAuto = MapRasterSourceDefinition( + id: 'osm_auto', + label: 'OpenStreetMap Auto', + description: + 'Automatically uses OpenStreetMap Standard or Dark from the app theme', + ); + static const MapRasterSourceDefinition osmStandard = + MapRasterSourceDefinition( + id: 'osm_standard', + label: 'OpenStreetMap Standard', + description: 'Direct tiles from tile.openstreetmap.org', + ); + static const MapRasterSourceDefinition osmDark = MapRasterSourceDefinition( + id: 'osm_dark', + label: 'OpenStreetMap Dark', + description: 'Standard OpenStreetMap tiles with an inverted dark filter', + ); + static const MapRasterSourceDefinition stamenTerrain = + MapRasterSourceDefinition( + id: 'stamen_terrain', + label: 'Stamen Terrain', + description: 'Terrain-focused style with hill shading', + isStadia: true, + allowsBulkDownload: true, + ); + static const MapRasterSourceDefinition alidadeSmoothDark = + MapRasterSourceDefinition( + id: 'alidade_smooth_dark', + label: 'Alidade Smooth Dark', + description: 'Dark basemap with smooth contrast', + isStadia: true, + allowsBulkDownload: true, + ); + static const MapRasterSourceDefinition outdoors = MapRasterSourceDefinition( + id: 'outdoors', + label: 'Outdoors', + description: 'Outdoor-focused map with trails and terrain context', + isStadia: true, + allowsBulkDownload: true, + ); + static const MapRasterSourceDefinition osmBright = MapRasterSourceDefinition( + id: 'osm_bright', + label: 'OSM Bright', + description: 'Bright general-purpose OpenStreetMap style', + isStadia: true, + allowsBulkDownload: true, + ); + static const MapRasterSourceDefinition outdoorsDark = + MapRasterSourceDefinition( + id: 'outdoors', + label: 'Outdoors Dark', + description: 'Dark version of the Outdoors map style', + isStadia: true, + allowsBulkDownload: true, + ); + static const MapRasterSourceDefinition osmBrightDark = + MapRasterSourceDefinition( + id: 'osm_bright', + label: 'OSM Bright Dark', + description: 'Dark version of the OSM Bright map style', + isStadia: true, + allowsBulkDownload: true, + ); + + static MapRasterSourceDefinition fromPreset(MapRasterSourcePreset preset) { + switch (preset) { + case MapRasterSourcePreset.osmAuto: + return osmAuto; + case MapRasterSourcePreset.osmStandard: + return osmStandard; + case MapRasterSourcePreset.osmDark: + return osmDark; + case MapRasterSourcePreset.alidadeSmoothDark: + return alidadeSmoothDark; + case MapRasterSourcePreset.outdoors: + return outdoors; + case MapRasterSourcePreset.outdoorsDark: + return outdoorsDark; + case MapRasterSourcePreset.osmBright: + return osmBright; + case MapRasterSourcePreset.osmBrightDark: + return osmBrightDark; + case MapRasterSourcePreset.stamenTerrain: + return stamenTerrain; + } + } + + static MapRasterSourceDefinition fromSettings(AppSettings settings) { + return fromPreset(MapRasterSourcePreset.fromId(settings.mapRasterSourceId)); + } +} + +class MapRasterEndpointCatalog { + static const MapRasterEndpointDefinition standard = + MapRasterEndpointDefinition( + id: 'standard', + label: 'Standard Endpoint', + description: 'Global CDN routing to the fastest Stadia server', + host: 'tiles.stadiamaps.com', + ); + static const MapRasterEndpointDefinition standard2x = + MapRasterEndpointDefinition( + id: 'standard_2x', + label: 'Standard Endpoint (@2x)', + description: 'Global Stadia endpoint with HiDPI raster tiles', + host: 'tiles.stadiamaps.com', + scaleSuffix: '@2x', + ); + static const MapRasterEndpointDefinition eu = MapRasterEndpointDefinition( + id: 'eu', + label: 'EU Endpoint', + description: 'Route tile requests to Stadia EU servers', + host: 'tiles-eu.stadiamaps.com', + ); + static const MapRasterEndpointDefinition eu2x = MapRasterEndpointDefinition( + id: 'eu_2x', + label: 'EU Endpoint (@2x)', + description: 'EU Stadia endpoint with HiDPI raster tiles', + host: 'tiles-eu.stadiamaps.com', + scaleSuffix: '@2x', + ); + + static const List presets = [ + standard, + standard2x, + eu, + eu2x, + ]; + + static MapRasterEndpointDefinition fromSettings(AppSettings settings) { + final preset = MapRasterEndpointPreset.fromId(settings.mapTileEndpointId); + switch (preset) { + case MapRasterEndpointPreset.standard2x: + return standard2x; + case MapRasterEndpointPreset.eu: + return eu; + case MapRasterEndpointPreset.eu2x: + return eu2x; + case MapRasterEndpointPreset.standard: + return standard; + } + } +} class MapTileCacheProgress { final int completed; @@ -32,36 +252,253 @@ class MapTileCacheResult { }); } -class MapTileCacheService { +class CachedTileInfo { + final String key; + final String host; + final String sourceId; + final int zoom; + final int x; + final int y; + final int length; + + const CachedTileInfo({ + required this.key, + required this.host, + required this.sourceId, + required this.zoom, + required this.x, + required this.y, + required this.length, + }); +} + +class CachedTileInventory { + final List tiles; + final int totalBytes; + + const CachedTileInventory({required this.tiles, required this.totalBytes}); +} + +class MapTileCacheService extends ChangeNotifier { static const String cacheKey = 'map_tile_cache'; static const String userAgentPackageName = 'com.meshcore.open'; static const int defaultMinZoom = 10; static const int defaultMaxZoom = 15; + final AppSettingsService appSettingsService; final BaseCacheManager cacheManager; late final TileProvider tileProvider; - MapTileCacheService({BaseCacheManager? cacheManager}) - : cacheManager = - cacheManager ?? - CacheManager( - Config( - cacheKey, - stalePeriod: const Duration(days: 365), - maxNrOfCacheObjects: 200000, - ), - ) { + MapTileCacheService({ + required this.appSettingsService, + BaseCacheManager? cacheManager, + }) : cacheManager = + cacheManager ?? + CacheManager( + Config( + cacheKey, + stalePeriod: const Duration(days: 365), + maxNrOfCacheObjects: 200000, + ), + ) { tileProvider = CachedNetworkTileProvider(cacheManager: this.cacheManager); + appSettingsService.addListener(_handleSettingsChanged); } + MapRasterSourceDefinition get source { + final selectedSource = MapRasterSourceCatalog.fromSettings( + appSettingsService.settings, + ); + if (!selectedSource.allowsBulkDownload || + !appSettingsService.settings.usesstadiaDemo) { + return selectedSource; + } + return MapRasterSourceDefinition( + id: selectedSource.id, + label: selectedSource.label, + description: selectedSource.description, + isStadia: selectedSource.isStadia, + allowsBulkDownload: false, // Explicitly disable bulk download for demo. + ); + } + + MapRasterEndpointDefinition get endpoint => + MapRasterEndpointCatalog.fromSettings(appSettingsService.settings); + + String get urlTemplate => _buildUrlTemplate(appSettingsService.settings); + + TileBuilder? get tileBuilder => null; + + static bool shouldApplyDarkFilterForSettings( + AppSettings settings, + Brightness brightness, + ) { + switch (MapRasterSourcePreset.fromId(settings.mapRasterSourceId)) { + case MapRasterSourcePreset.osmDark: + case MapRasterSourcePreset.outdoorsDark: + case MapRasterSourcePreset.osmBrightDark: + return true; + case MapRasterSourcePreset.osmAuto: + return brightness == Brightness.dark; + case MapRasterSourcePreset.osmStandard: + case MapRasterSourcePreset.stamenTerrain: + case MapRasterSourcePreset.alidadeSmoothDark: + case MapRasterSourcePreset.outdoors: + case MapRasterSourcePreset.osmBright: + return false; + } + } + + static const ColorFilter _darkMapFilter = ColorFilter.matrix([ + -0.0850, + -0.2861, + -0.0289, + 0, + 120, + -0.0957, + -0.3218, + -0.0325, + 0, + 140, + -0.1169, + -0.3934, + -0.0397, + 0, + 170, + 0, + 0, + 0, + 1, + 0, + ]); + + CacheManager get _concreteCacheManager => cacheManager as CacheManager; + Map get defaultHeaders => { 'User-Agent': 'flutter_map ($userAgentPackageName)', }; + Widget buildTileLayer(BuildContext context, {double opacity = 1}) { + Widget layer = TileLayer( + urlTemplate: urlTemplate, + tileProvider: tileProvider, + tileBuilder: tileBuilder, + userAgentPackageName: userAgentPackageName, + maxZoom: 19, + ); + + final shouldApplyDarkFilter = shouldApplyDarkFilterForSettings( + appSettingsService.settings, + Theme.of(context).brightness, + ); + + if (shouldApplyDarkFilter) { + layer = ColorFiltered(colorFilter: _darkMapFilter, child: layer); + } + + if (opacity < 1) { + layer = Opacity(opacity: opacity, child: layer); + } + + return layer; + } + Future clearCache() async { await cacheManager.emptyCache(); } + Future getCachedTileInventory() async { + final repo = _concreteCacheManager.config.repo; + await repo.open(); + final objects = await repo.getAllObjects(); + final tiles = []; + int totalBytes = 0; + + for (final object in objects) { + totalBytes += object.length ?? 0; + final tile = _parseCachedTile(object); + if (tile != null) { + tiles.add(tile); + } + } + + return CachedTileInventory(tiles: tiles, totalBytes: totalBytes); + } + + List filterTilesForActiveSource( + Iterable tiles, + ) { + final activeSource = source; + if (!activeSource.isStadia) { + return tiles + .where( + (tile) => + tile.sourceId == MapRasterSourceCatalog.osmStandard.id && + tile.host == 'tile.openstreetmap.org', + ) + .toList(); + } + + final activeEndpoint = endpoint; + return tiles + .where( + (tile) => + tile.sourceId == activeSource.id && + tile.host == activeEndpoint.host, + ) + .toList(); + } + + int countTilesForBounds( + Iterable tiles, { + LatLngBounds? bounds, + required int minZoom, + required int maxZoom, + }) { + if (bounds == null) return 0; + final safeMin = math.min(minZoom, maxZoom); + final safeMax = math.max(minZoom, maxZoom); + return tiles.where((tile) { + if (tile.zoom < safeMin || tile.zoom > safeMax) { + return false; + } + final tileBounds = _tileBoundsForTile(tile.x, tile.y, tile.zoom); + return _boundsIntersect(bounds, tileBounds); + }).length; + } + + List buildCachedTilePolygons( + Iterable tiles, { + required int zoom, + LatLngBounds? visibleBounds, + int limit = 250, + }) { + final polygons = []; + for (final tile in tiles) { + if (tile.zoom != zoom) continue; + final tileBounds = _tileBoundsForTile(tile.x, tile.y, tile.zoom); + if (visibleBounds != null && + !_boundsIntersect(visibleBounds, tileBounds)) { + continue; + } + polygons.add( + Polygon( + points: [ + tileBounds.northWest, + tileBounds.northEast, + tileBounds.southEast, + tileBounds.southWest, + ], + borderStrokeWidth: 0.6, + color: const Color(0x5532A852), + borderColor: const Color(0xCC2F8F46), + ), + ); + if (polygons.length >= limit) break; + } + return polygons; + } + int estimateTileCount(LatLngBounds bounds, int minZoom, int maxZoom) { final safeMin = math.min(minZoom, maxZoom); final safeMax = math.max(minZoom, maxZoom); @@ -89,6 +526,7 @@ class MapTileCacheService { final total = estimateTileCount(bounds, safeMin, safeMax); final authHeaders = headers ?? defaultHeaders; final safeConcurrency = math.max(1, concurrentDownloads); + final currentTemplate = urlTemplate; int completed = 0; int failed = 0; @@ -124,7 +562,7 @@ class MapTileCacheService { final tileBounds = _tileBoundsForBounds(bounds, zoom); for (int x = tileBounds.minX; x <= tileBounds.maxX; x++) { for (int y = tileBounds.minY; y <= tileBounds.maxY; y++) { - final url = _buildTileUrl(x, y, zoom); + final url = _buildTileUrl(x, y, zoom, urlTemplate: currentTemplate); await queueDownload(url); } } @@ -167,6 +605,16 @@ class MapTileCacheService { ); } + void _handleSettingsChanged() { + notifyListeners(); + } + + @override + void dispose() { + appSettingsService.removeListener(_handleSettingsChanged); + super.dispose(); + } + _TileBounds _tileBoundsForBounds(LatLngBounds bounds, int zoom) { final north = _clampLatitude(bounds.north); final south = _clampLatitude(bounds.south); @@ -185,6 +633,56 @@ class MapTileCacheService { ); } + String _buildUrlTemplate(AppSettings settings) { + final source = MapRasterSourceCatalog.fromSettings(settings); + if (!source.isStadia) { + return 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'; + } + final endpoint = MapRasterEndpointCatalog.fromSettings(settings); + final apiKey = settings.effectiveMapTileApiKey; + final base = + 'https://${endpoint.host}/tiles/${source.id}/{z}/{x}/{y}${endpoint.scaleSuffix}.png'; + final query = Uri(queryParameters: {'api_key': apiKey}).query; + return '$base?$query'; + } + + CachedTileInfo? _parseCachedTile(CacheObject object) { + final uri = Uri.tryParse(object.key); + if (uri == null) return null; + final segments = uri.pathSegments; + + if (segments.length >= 3 && + segments[segments.length - 3].isNotEmpty && + segments[segments.length - 2].isNotEmpty) { + final zoom = int.tryParse(segments[segments.length - 3]); + final x = int.tryParse(segments[segments.length - 2]); + final ySegment = segments.last; + final yString = ySegment.split('.').first.replaceAll('@2x', ''); + final y = int.tryParse(yString); + + if (zoom == null || x == null || y == null) { + return null; + } + + var sourceId = MapRasterSourceCatalog.osmStandard.id; + if (segments.length >= 5 && segments[0] == 'tiles') { + sourceId = segments[1]; + } + + return CachedTileInfo( + key: object.key, + host: uri.host, + sourceId: sourceId, + zoom: zoom, + x: x, + y: y, + length: object.length ?? 0, + ); + } + + return null; + } + int _lonToTileX(double lon, int zoom, int maxIndex) { final n = 1 << zoom; final value = ((lon + 180.0) / 360.0 * n).floor(); @@ -205,8 +703,34 @@ class MapTileCacheService { return lat.clamp(-maxLat, maxLat); } - String _buildTileUrl(int x, int y, int zoom) { - return kMapTileUrlTemplate + LatLngBounds _tileBoundsForTile(int x, int y, int zoom) { + return LatLngBounds.unsafe( + north: _tileYToLat(y, zoom), + south: _tileYToLat(y + 1, zoom), + east: _tileXToLon(x + 1, zoom), + west: _tileXToLon(x, zoom), + ); + } + + double _tileXToLon(int x, int zoom) { + final n = 1 << zoom; + return x / n * 360.0 - 180.0; + } + + double _tileYToLat(int y, int zoom) { + final n = math.pi - (2.0 * math.pi * y) / (1 << zoom); + return 180.0 / math.pi * math.atan(0.5 * (math.exp(n) - math.exp(-n))); + } + + bool _boundsIntersect(LatLngBounds a, LatLngBounds b) { + return a.west <= b.east && + a.east >= b.west && + a.south <= b.north && + a.north >= b.south; + } + + String _buildTileUrl(int x, int y, int zoom, {required String urlTemplate}) { + return urlTemplate .replaceAll('{z}', zoom.toString()) .replaceAll('{x}', x.toString()) .replaceAll('{y}', y.toString()); diff --git a/lib/theme/mesh_theme.dart b/lib/theme/mesh_theme.dart index fcb15b1b..281b1145 100644 --- a/lib/theme/mesh_theme.dart +++ b/lib/theme/mesh_theme.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart' show CupertinoPageTransitionsBuilder; import 'package:flutter/material.dart'; /// MeshCore palette — high-contrast slate surfaces with sky-blue accents. @@ -12,16 +11,16 @@ class MeshPalette { static const bg3 = Color(0xFF1E293B); static const bg4 = Color(0xFF334155); - // Lines - static const line = Color(0xFF1E293B); - static const line2 = Color(0xFF334155); - static const line3 = Color(0xFF475569); + // Lines — lifted for clearer element separation on the near-black surface + static const line = Color(0xFF2A3850); + static const line2 = Color(0xFF3B4A61); + static const line3 = Color(0xFF546376); - // Ink + // Ink — muted tones brightened for readable secondary/tertiary text in dark static const ink = Color(0xFFF8FAFC); - static const ink2 = Color(0xFFCBD5E1); - static const ink3 = Color(0xFF94A3B8); - static const ink4 = Color(0xFF64748B); + static const ink2 = Color(0xFFD5DEE9); + static const ink3 = Color(0xFFAAB6C6); + static const ink4 = Color(0xFF828FA3); // Signal-quality green (used only for SNR coloring, not UI chrome) static const signal = Color(0xFF22C55E); @@ -395,9 +394,9 @@ class MeshTheme { labelTextStyle: WidgetStateProperty.resolveWith((states) { final selected = states.contains(WidgetState.selected); return TextStyle( - fontFamily: MeshFonts.mono, - fontFamilyFallback: MeshFonts.monoFallback, - fontSize: 10, + fontFamily: MeshFonts.sans, + fontFamilyFallback: MeshFonts.sansFallback, + fontSize: 11.5, fontWeight: selected ? FontWeight.w700 : FontWeight.w500, letterSpacing: 0.1, color: selected ? scheme.onPrimary : scheme.onSurfaceVariant, @@ -448,7 +447,7 @@ class MeshTheme { pageTransitionsTheme: const PageTransitionsTheme( builders: { TargetPlatform.android: FadeForwardsPageTransitionsBuilder(), - TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), + TargetPlatform.iOS: FadeForwardsPageTransitionsBuilder(), TargetPlatform.linux: FadeForwardsPageTransitionsBuilder(), TargetPlatform.macOS: FadeForwardsPageTransitionsBuilder(), TargetPlatform.windows: FadeForwardsPageTransitionsBuilder(), @@ -583,14 +582,15 @@ class MeshTheme { ); } - /// Small-caps mono label — used for section accents and chip labels. + /// Section-accent / chip label — sans for legibility, with light tracking + /// to keep the "label" feel that section headers rely on. static TextStyle accentLabel({Color? color, double? fontSize}) { return TextStyle( - fontFamily: MeshFonts.mono, - fontFamilyFallback: MeshFonts.monoFallback, - fontSize: fontSize ?? 9.5, - fontWeight: FontWeight.w600, - letterSpacing: 1.8, + fontFamily: MeshFonts.sans, + fontFamilyFallback: MeshFonts.sansFallback, + fontSize: fontSize ?? 11, + fontWeight: FontWeight.w700, + letterSpacing: 0.6, color: color, ); } diff --git a/lib/utils/keys.dart b/lib/utils/keys.dart new file mode 100644 index 00000000..488223f7 --- /dev/null +++ b/lib/utils/keys.dart @@ -0,0 +1,87 @@ +import 'dart:async'; +import 'dart:math'; +import 'dart:typed_data'; +import 'package:convert/convert.dart'; +import 'package:cryptography/cryptography.dart'; + +Uint8List randomBytes(int length) { + final random = Random.secure(); + final bytes = Uint8List(length); + for (int i = 0; i < length; i++) { + bytes[i] = random.nextInt(256); + } + return bytes; +} + +class MCKeyPair { + final Uint8List public; + final Uint8List private; + MCKeyPair(this.public, this.private); +} + +Future generateKeyPair() async { + final algo = Ed25519(); + final seed = randomBytes(32); + final SimpleKeyPair keys = await algo.newKeyPairFromSeed(seed); + final Uint8List pubKeyBytes = Uint8List.fromList( + (await keys.extractPublicKey()).bytes, + ); + // Ed25519 gives 32-byte public and private keys, but MeshCore + // uses an expanded 64-byte private key, so we have to make it + // ourselves. Luckily, we can do it such that it has the same public + // key, which we'd otherwise be unable to compute, since although the + // crypto library implements the elliptic curve computations we need, + // it doesn't make them available. + final Uint8List hash = Uint8List.fromList((await Sha512().hash(seed)).bytes); + hash[0] &= 248; // Clamp the scalar. This keeps the chosen point in the + hash[31] &= 63; // large elliptic curve subgroup, and is demanded both by + hash[31] |= 64; // Ed25519 and by the MeshCore repeater key validation code. + return MCKeyPair(pubKeyBytes, hash); +} + +class _KeyHashPrefix { + Uint8List _bytes = Uint8List(0); + Uint8List _mask = Uint8List(0); + _KeyHashPrefix(String prefix) { + final bool lengthIsOdd = (prefix.length % 2 == 1) ? true : false; + final String decodableString = lengthIsOdd ? "${prefix}0" : prefix; + _bytes = hex.decoder.convert(decodableString); + _mask = Uint8List(_bytes.length); + for (var i = 0; i < _mask.length; i++) { + _mask[i] = 0xff; + } + if (lengthIsOdd) { + _mask[_mask.length - 1] = 0xf0; + } + } + + bool matches(List keyBytes) { + if (keyBytes.length < _bytes.length) { + return false; + } + for (var i = 0; i < _bytes.length; i++) { + if (keyBytes[i] & _mask[i] != _bytes[i]) { + return false; + } + } + return true; + } +} + +class KeyPairSearcher { + Future findMatchingKeyPair( + String prefixStr, + int maxMilliseconds, + ) async { + final int now = DateTime.now().millisecondsSinceEpoch; + final int endTime = now + maxMilliseconds; + final _KeyHashPrefix keyHashPrefix = _KeyHashPrefix(prefixStr); + while (DateTime.now().millisecondsSinceEpoch < endTime) { + final MCKeyPair keys = await generateKeyPair(); + if (keyHashPrefix.matches(keys.public)) { + return keys; + } + } + return null; + } +} diff --git a/lib/utils/route_transitions.dart b/lib/utils/route_transitions.dart index 7de5a9c5..2d68bbdf 100644 --- a/lib/utils/route_transitions.dart +++ b/lib/utils/route_transitions.dart @@ -6,20 +6,15 @@ Route buildQuickSwitchRoute(Widget page) { transitionDuration: const Duration(milliseconds: 220), reverseTransitionDuration: const Duration(milliseconds: 200), transitionsBuilder: (context, animation, secondaryAnimation, child) { - final curved = CurvedAnimation( - parent: animation, - curve: Curves.easeOutCubic, - reverseCurve: Curves.easeInCubic, - ); + // Pure crossfade — no positional slide, so the shared top chrome + // (app bar + Contacts/Channels toggle) stays rock-steady while switching. return FadeTransition( - opacity: curved, - child: SlideTransition( - position: Tween( - begin: const Offset(0.02, 0), - end: Offset.zero, - ).animate(curved), - child: child, + opacity: CurvedAnimation( + parent: animation, + curve: Curves.easeOut, + reverseCurve: Curves.easeIn, ), + child: child, ); }, ); diff --git a/lib/widgets/elements_ui.dart b/lib/widgets/elements_ui.dart index a4995c73..f77b90a6 100644 --- a/lib/widgets/elements_ui.dart +++ b/lib/widgets/elements_ui.dart @@ -4,6 +4,7 @@ class FeatureToggleRow extends StatefulWidget { final String title; final String subtitle; final bool value; + final bool enabled; final bool hasRefreshing; final bool isRefreshing; final ValueChanged? onChanged; @@ -15,6 +16,7 @@ class FeatureToggleRow extends StatefulWidget { required this.title, required this.subtitle, required this.value, + this.enabled = true, this.hasRefreshing = false, this.isRefreshing = false, this.onChanged, @@ -31,6 +33,13 @@ class _FeatureToggleRow extends State { Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; final textTheme = Theme.of(context).textTheme; + final isEnabled = widget.enabled; + final titleColor = isEnabled + ? scheme.onSurface + : scheme.onSurfaceVariant.withValues(alpha: 0.7); + final subtitleColor = isEnabled + ? scheme.onSurfaceVariant + : scheme.onSurfaceVariant.withValues(alpha: 0.55); return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), @@ -44,14 +53,13 @@ class _FeatureToggleRow extends State { widget.title, style: textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, + color: titleColor, ), ), const SizedBox(height: 2), Text( widget.subtitle, - style: textTheme.bodySmall?.copyWith( - color: scheme.onSurfaceVariant, - ), + style: textTheme.bodySmall?.copyWith(color: subtitleColor), ), ], ), @@ -60,7 +68,18 @@ class _FeatureToggleRow extends State { Row( mainAxisSize: MainAxisSize.min, children: [ - Switch(value: widget.value, onChanged: widget.onChanged), + Switch( + value: widget.value, + onChanged: isEnabled ? widget.onChanged : null, + thumbColor: !isEnabled + ? WidgetStatePropertyAll(scheme.onSurfaceVariant) + : null, + trackColor: !isEnabled + ? WidgetStatePropertyAll( + scheme.onSurfaceVariant.withValues(alpha: 0.35), + ) + : null, + ), if (widget.hasRefreshing) ...[ const SizedBox(width: 4), widget.isRefreshing @@ -74,7 +93,7 @@ class _FeatureToggleRow extends State { ) : IconButton( icon: const Icon(Icons.refresh, size: 18), - onPressed: widget.onRefresh, + onPressed: isEnabled ? widget.onRefresh : null, tooltip: widget.refreshTooltip, visualDensity: VisualDensity.compact, padding: EdgeInsets.zero, diff --git a/lib/widgets/quick_switch_bar.dart b/lib/widgets/quick_switch_bar.dart index 90b90b40..917ceced 100644 --- a/lib/widgets/quick_switch_bar.dart +++ b/lib/widgets/quick_switch_bar.dart @@ -26,9 +26,16 @@ class QuickSwitchBar extends StatelessWidget { final colorScheme = theme.colorScheme; final labelStyle = theme.textTheme.labelMedium ?? const TextStyle(); final background = highContrast ? MapPalette.panelDark : Colors.transparent; - final selectedColor = highContrast + // The selected icon sits on the primary indicator pill, so it uses + // onPrimary. The label sits below on the bar background, so it must use a + // foreground color that contrasts with the surface (not onPrimary, which + // is white-on-white in the light theme). + final selectedIconColor = highContrast ? MapPalette.textPrimary : colorScheme.onPrimary; + final selectedLabelColor = highContrast + ? MapPalette.textPrimary + : colorScheme.onSurface; final unselectedColor = highContrast ? MapPalette.textSecondary : colorScheme.onSurfaceVariant; @@ -59,13 +66,13 @@ class QuickSwitchBar extends StatelessWidget { final isSelected = states.contains(WidgetState.selected); return labelStyle.copyWith( fontWeight: isSelected ? FontWeight.w700 : FontWeight.w500, - color: isSelected ? selectedColor : unselectedColor, + color: isSelected ? selectedLabelColor : unselectedColor, ); }), iconTheme: WidgetStateProperty.resolveWith((states) { final isSelected = states.contains(WidgetState.selected); return IconThemeData( - color: isSelected ? selectedColor : unselectedColor, + color: isSelected ? selectedIconColor : unselectedColor, ); }), ), diff --git a/lib/widgets/telemetry_location_map.dart b/lib/widgets/telemetry_location_map.dart index f8983c04..537867c5 100644 --- a/lib/widgets/telemetry_location_map.dart +++ b/lib/widgets/telemetry_location_map.dart @@ -11,7 +11,6 @@ import '../models/app_settings.dart'; import '../models/contact.dart'; import '../services/app_settings_service.dart'; import '../services/map_tile_cache_service.dart'; -import 'themed_map_tile_layer.dart'; class TelemetryLocationMap extends StatefulWidget { final double latitude; @@ -115,7 +114,7 @@ class _TelemetryLocationMapState extends State { ), ), children: [ - ThemedMapTileLayer(tileCache: tileCache), + tileCache.buildTileLayer(context), MarkerLayer( markers: [ ...contacts.map(_buildContactMarker), diff --git a/lib/widgets/themed_map_tile_layer.dart b/lib/widgets/themed_map_tile_layer.dart deleted file mode 100644 index c6d2eaad..00000000 --- a/lib/widgets/themed_map_tile_layer.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_map/flutter_map.dart'; - -import '../services/map_tile_cache_service.dart'; - -/// Shared cached map tiles with an automatic dark-mode treatment. -/// -/// The dark style transforms the existing OpenStreetMap raster tiles, so light -/// and dark maps share the same offline cache and network requests. -class ThemedMapTileLayer extends StatelessWidget { - final MapTileCacheService tileCache; - final double opacity; - - const ThemedMapTileLayer({ - super.key, - required this.tileCache, - this.opacity = 1, - }); - - static const ColorFilter _darkMapFilter = ColorFilter.matrix([ - -0.0850, - -0.2861, - -0.0289, - 0, - 120, - -0.0957, - -0.3218, - -0.0325, - 0, - 140, - -0.1169, - -0.3934, - -0.0397, - 0, - 170, - 0, - 0, - 0, - 1, - 0, - ]); - - @override - Widget build(BuildContext context) { - Widget layer = TileLayer( - urlTemplate: kMapTileUrlTemplate, - tileProvider: tileCache.tileProvider, - userAgentPackageName: MapTileCacheService.userAgentPackageName, - maxZoom: 19, - ); - - if (Theme.of(context).brightness == Brightness.dark) { - layer = ColorFiltered(colorFilter: _darkMapFilter, child: layer); - } - if (opacity < 1) { - layer = Opacity(opacity: opacity, child: layer); - } - return layer; - } -} diff --git a/lib/widgets/unread_badge.dart b/lib/widgets/unread_badge.dart index 1cf5c1d6..dd2e2ab3 100644 --- a/lib/widgets/unread_badge.dart +++ b/lib/widgets/unread_badge.dart @@ -11,18 +11,19 @@ class UnreadBadge extends StatelessWidget { Widget build(BuildContext context) { final display = count > 9999 ? '9999+' : count.toString(); return Container( - padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2), + constraints: const BoxConstraints(minWidth: 20), + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + alignment: Alignment.center, decoration: BoxDecoration( - color: MeshPalette.blue.withValues(alpha: 0.18), + color: MeshPalette.alert, borderRadius: BorderRadius.circular(MeshRadii.pill), - border: Border.all(color: MeshPalette.blue.withValues(alpha: 0.45)), ), child: Text( display, style: MeshTheme.mono( fontSize: 11, fontWeight: FontWeight.w700, - color: MeshPalette.blue, + color: Colors.white, ), ), ); diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index ffc8c590..3ba5e9d1 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,7 @@ import FlutterMacOS import Foundation +import cryptography_flutter import flutter_blue_plus_darwin import flutter_local_notifications import mobile_scanner @@ -15,6 +16,7 @@ import sqflite_darwin import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + CryptographyFlutterPlugin.register(with: registry.registrar(forPlugin: "CryptographyFlutterPlugin")) FlutterBluePlusPlugin.register(with: registry.registrar(forPlugin: "FlutterBluePlusPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) diff --git a/pubspec.yaml b/pubspec.yaml index dcefc016..eaa5e5ac 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -73,6 +73,9 @@ dependencies: ml_dataframe: ^1.0.0 llamadart: ^0.8.0 flutter_langdetect: ^0.0.1 + cryptography: ^2.9.0 + cryptography_flutter: ^2.3.4 + convert: ^3.1.2 hooks: user_defines: @@ -111,12 +114,25 @@ flutter: assets: - assets/images/ - assets/icons/ + - assets/fonts/ + + fonts: + - family: Inter + fonts: + - asset: assets/fonts/Inter.ttf + - asset: assets/fonts/InterItalic.ttf + style: italic + - family: JetBrains Mono + fonts: + - asset: assets/fonts/JetBrainsMono.ttf flutter_launcher_icons: android: true ios: true image_path: "mesh-icon.png" remove_alpha_ios: true + adaptive_icon_background: "#9F9D95" + adaptive_icon_foreground: "assets/icons/ic_foreground.png" # To add assets to your application, add an assets section, like this: # assets: diff --git a/test/models/model_changes_test.dart b/test/models/model_changes_test.dart index 1bb6bf2d..cc218c95 100644 --- a/test/models/model_changes_test.dart +++ b/test/models/model_changes_test.dart @@ -2,17 +2,18 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:meshcore_open/connector/meshcore_protocol.dart'; import 'package:meshcore_open/models/channel_message.dart'; import 'package:meshcore_open/models/contact.dart'; import 'package:meshcore_open/models/path_history.dart'; import 'package:meshcore_open/models/app_settings.dart'; -import 'package:meshcore_open/storage/prefs_manager.dart'; import 'package:meshcore_open/storage/contact_store.dart'; -import 'package:meshcore_open/storage/message_store.dart'; import 'package:meshcore_open/storage/channel_message_store.dart'; import 'package:meshcore_open/storage/contact_discovery_store.dart'; +import 'package:meshcore_open/services/app_settings_service.dart'; +import 'package:meshcore_open/storage/prefs_manager.dart'; +import 'package:meshcore_open/storage/message_store.dart'; +import 'package:shared_preferences/shared_preferences.dart'; // Builds a valid contact frame with the given pathLen and optional overrides. // Frame layout: [respCode(1)][pubKey(32)][type(1)][flags(1)][pathLen(1)][path(64)][name(32)][timestamp(4)][lat(4)][lon(4)] @@ -51,6 +52,7 @@ Uint8List _buildChannelMessageFrameV3({ required bool hasPath, int channelIndex = 7, String senderName = 'Alice', + String text = 'Hello world', int txtType = txtTypePlain, }) { @@ -77,6 +79,14 @@ Uint8List _buildChannelMessageFrameV3({ } void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() async { + SharedPreferences.setMockInitialValues({}); + PrefsManager.reset(); + await PrefsManager.initialize(); + }); + group('ChannelMessage.fromFrame — V3 packed path decoding', () { test( 'hasPath reads width, hop count, path bytes, and txtType correctly', @@ -467,11 +477,6 @@ void main() { }); group('Storage migration — multi-byte paths compatibility', () { - setUp(() async { - SharedPreferences.setMockInitialValues({}); - await PrefsManager.initialize(); - }); - test( 'ContactStore decodes and migrates legacy mode-encoded paths', () async { @@ -638,5 +643,46 @@ void main() { expect(contacts.first.path, equals(Uint8List.fromList([0x11, 0x22]))); }, ); + + }); + + group('AppSettingsService — gps interval fallback', () { + test('resolvedGpsIntervalSeconds prefers device custom var', () { + final service = AppSettingsService(); + + expect( + service.resolvedGpsIntervalSeconds(const {'gps_interval': '120'}), + equals(120), + ); + }); + + test('resolvedGpsIntervalSeconds falls back to stored value', () async { + final service = AppSettingsService(); + await service.updateSettings(AppSettings(gpsIntervalSeconds: 900)); + + expect(service.resolvedGpsIntervalSeconds(null), equals(900)); + expect( + service.resolvedGpsIntervalSeconds(const {'gps_interval': 'bad'}), + equals(900), + ); + }); + + test('resolvedGpsIntervalSeconds keeps an explicit device zero', () async { + final service = AppSettingsService(); + await service.updateSettings(AppSettings(gpsIntervalSeconds: 900)); + + expect( + service.resolvedGpsIntervalSeconds(const {'gps_interval': '0'}), + equals(0), + ); + }); + + test('toJson/fromJson preserves gpsIntervalSeconds', () { + final settings = AppSettings(gpsIntervalSeconds: 321); + + final restored = AppSettings.fromJson(settings.toJson()); + + expect(restored.gpsIntervalSeconds, equals(321)); + }); }); } diff --git a/untranslated.json b/untranslated.json index bacd26ce..14f7f7cc 100644 --- a/untranslated.json +++ b/untranslated.json @@ -19,7 +19,18 @@ "channels_regionSetTo", "channels_regionNotSet", "channels_regionSelect_Title", - "channels_clearRegion" + "channels_clearRegion", + "chat_receivedGif", + "repeater_keySettings", + "repeater_keySettingsSubtitle", + "repeater_prvKey", + "repeater_prvKeyHelper", + "repeater_generatePrvKey", + "repeater_stopGeneratingPrvKey", + "repeater_pubKey", + "repeater_pubKeyHelper", + "repeater_pubKeyPrefix", + "repeater_pubKeyPrefixHelper" ], "de": [ @@ -29,7 +40,18 @@ "repeater_pathHashModeOption0", "repeater_pathHashModeOption1", "repeater_pathHashModeOption2", - "repeater_pathHashModeOption3" + "repeater_pathHashModeOption3", + "chat_receivedGif", + "repeater_keySettings", + "repeater_keySettingsSubtitle", + "repeater_prvKey", + "repeater_prvKeyHelper", + "repeater_generatePrvKey", + "repeater_stopGeneratingPrvKey", + "repeater_pubKey", + "repeater_pubKeyHelper", + "repeater_pubKeyPrefix", + "repeater_pubKeyPrefixHelper" ], "es": [ @@ -52,7 +74,18 @@ "channels_regionSetTo", "channels_regionNotSet", "channels_regionSelect_Title", - "channels_clearRegion" + "channels_clearRegion", + "chat_receivedGif", + "repeater_keySettings", + "repeater_keySettingsSubtitle", + "repeater_prvKey", + "repeater_prvKeyHelper", + "repeater_generatePrvKey", + "repeater_stopGeneratingPrvKey", + "repeater_pubKey", + "repeater_pubKeyHelper", + "repeater_pubKeyPrefix", + "repeater_pubKeyPrefixHelper" ], "fr": [ @@ -75,7 +108,18 @@ "channels_regionSetTo", "channels_regionNotSet", "channels_regionSelect_Title", - "channels_clearRegion" + "channels_clearRegion", + "chat_receivedGif", + "repeater_keySettings", + "repeater_keySettingsSubtitle", + "repeater_prvKey", + "repeater_prvKeyHelper", + "repeater_generatePrvKey", + "repeater_stopGeneratingPrvKey", + "repeater_pubKey", + "repeater_pubKeyHelper", + "repeater_pubKeyPrefix", + "repeater_pubKeyPrefixHelper" ], "hu": [ @@ -98,7 +142,18 @@ "channels_regionSetTo", "channels_regionNotSet", "channels_regionSelect_Title", - "channels_clearRegion" + "channels_clearRegion", + "chat_receivedGif", + "repeater_keySettings", + "repeater_keySettingsSubtitle", + "repeater_prvKey", + "repeater_prvKeyHelper", + "repeater_generatePrvKey", + "repeater_stopGeneratingPrvKey", + "repeater_pubKey", + "repeater_pubKeyHelper", + "repeater_pubKeyPrefix", + "repeater_pubKeyPrefixHelper" ], "it": [ @@ -121,7 +176,18 @@ "channels_regionSetTo", "channels_regionNotSet", "channels_regionSelect_Title", - "channels_clearRegion" + "channels_clearRegion", + "chat_receivedGif", + "repeater_keySettings", + "repeater_keySettingsSubtitle", + "repeater_prvKey", + "repeater_prvKeyHelper", + "repeater_generatePrvKey", + "repeater_stopGeneratingPrvKey", + "repeater_pubKey", + "repeater_pubKeyHelper", + "repeater_pubKeyPrefix", + "repeater_pubKeyPrefixHelper" ], "ja": [ @@ -144,7 +210,18 @@ "channels_regionSetTo", "channels_regionNotSet", "channels_regionSelect_Title", - "channels_clearRegion" + "channels_clearRegion", + "chat_receivedGif", + "repeater_keySettings", + "repeater_keySettingsSubtitle", + "repeater_prvKey", + "repeater_prvKeyHelper", + "repeater_generatePrvKey", + "repeater_stopGeneratingPrvKey", + "repeater_pubKey", + "repeater_pubKeyHelper", + "repeater_pubKeyPrefix", + "repeater_pubKeyPrefixHelper" ], "ko": [ @@ -167,7 +244,18 @@ "channels_regionSetTo", "channels_regionNotSet", "channels_regionSelect_Title", - "channels_clearRegion" + "channels_clearRegion", + "chat_receivedGif", + "repeater_keySettings", + "repeater_keySettingsSubtitle", + "repeater_prvKey", + "repeater_prvKeyHelper", + "repeater_generatePrvKey", + "repeater_stopGeneratingPrvKey", + "repeater_pubKey", + "repeater_pubKeyHelper", + "repeater_pubKeyPrefix", + "repeater_pubKeyPrefixHelper" ], "nl": [ @@ -190,7 +278,18 @@ "channels_regionSetTo", "channels_regionNotSet", "channels_regionSelect_Title", - "channels_clearRegion" + "channels_clearRegion", + "chat_receivedGif", + "repeater_keySettings", + "repeater_keySettingsSubtitle", + "repeater_prvKey", + "repeater_prvKeyHelper", + "repeater_generatePrvKey", + "repeater_stopGeneratingPrvKey", + "repeater_pubKey", + "repeater_pubKeyHelper", + "repeater_pubKeyPrefix", + "repeater_pubKeyPrefixHelper" ], "pl": [ @@ -213,7 +312,18 @@ "channels_regionSetTo", "channels_regionNotSet", "channels_regionSelect_Title", - "channels_clearRegion" + "channels_clearRegion", + "chat_receivedGif", + "repeater_keySettings", + "repeater_keySettingsSubtitle", + "repeater_prvKey", + "repeater_prvKeyHelper", + "repeater_generatePrvKey", + "repeater_stopGeneratingPrvKey", + "repeater_pubKey", + "repeater_pubKeyHelper", + "repeater_pubKeyPrefix", + "repeater_pubKeyPrefixHelper" ], "pt": [ @@ -236,7 +346,18 @@ "channels_regionSetTo", "channels_regionNotSet", "channels_regionSelect_Title", - "channels_clearRegion" + "channels_clearRegion", + "chat_receivedGif", + "repeater_keySettings", + "repeater_keySettingsSubtitle", + "repeater_prvKey", + "repeater_prvKeyHelper", + "repeater_generatePrvKey", + "repeater_stopGeneratingPrvKey", + "repeater_pubKey", + "repeater_pubKeyHelper", + "repeater_pubKeyPrefix", + "repeater_pubKeyPrefixHelper" ], "ru": [ @@ -259,7 +380,18 @@ "channels_regionSetTo", "channels_regionNotSet", "channels_regionSelect_Title", - "channels_clearRegion" + "channels_clearRegion", + "chat_receivedGif", + "repeater_keySettings", + "repeater_keySettingsSubtitle", + "repeater_prvKey", + "repeater_prvKeyHelper", + "repeater_generatePrvKey", + "repeater_stopGeneratingPrvKey", + "repeater_pubKey", + "repeater_pubKeyHelper", + "repeater_pubKeyPrefix", + "repeater_pubKeyPrefixHelper" ], "sk": [ @@ -282,7 +414,18 @@ "channels_regionSetTo", "channels_regionNotSet", "channels_regionSelect_Title", - "channels_clearRegion" + "channels_clearRegion", + "chat_receivedGif", + "repeater_keySettings", + "repeater_keySettingsSubtitle", + "repeater_prvKey", + "repeater_prvKeyHelper", + "repeater_generatePrvKey", + "repeater_stopGeneratingPrvKey", + "repeater_pubKey", + "repeater_pubKeyHelper", + "repeater_pubKeyPrefix", + "repeater_pubKeyPrefixHelper" ], "sl": [ @@ -305,7 +448,18 @@ "channels_regionSetTo", "channels_regionNotSet", "channels_regionSelect_Title", - "channels_clearRegion" + "channels_clearRegion", + "chat_receivedGif", + "repeater_keySettings", + "repeater_keySettingsSubtitle", + "repeater_prvKey", + "repeater_prvKeyHelper", + "repeater_generatePrvKey", + "repeater_stopGeneratingPrvKey", + "repeater_pubKey", + "repeater_pubKeyHelper", + "repeater_pubKeyPrefix", + "repeater_pubKeyPrefixHelper" ], "sv": [ @@ -328,7 +482,18 @@ "channels_regionSetTo", "channels_regionNotSet", "channels_regionSelect_Title", - "channels_clearRegion" + "channels_clearRegion", + "chat_receivedGif", + "repeater_keySettings", + "repeater_keySettingsSubtitle", + "repeater_prvKey", + "repeater_prvKeyHelper", + "repeater_generatePrvKey", + "repeater_stopGeneratingPrvKey", + "repeater_pubKey", + "repeater_pubKeyHelper", + "repeater_pubKeyPrefix", + "repeater_pubKeyPrefixHelper" ], "uk": [ @@ -351,7 +516,18 @@ "channels_regionSetTo", "channels_regionNotSet", "channels_regionSelect_Title", - "channels_clearRegion" + "channels_clearRegion", + "chat_receivedGif", + "repeater_keySettings", + "repeater_keySettingsSubtitle", + "repeater_prvKey", + "repeater_prvKeyHelper", + "repeater_generatePrvKey", + "repeater_stopGeneratingPrvKey", + "repeater_pubKey", + "repeater_pubKeyHelper", + "repeater_pubKeyPrefix", + "repeater_pubKeyPrefixHelper" ], "zh": [ @@ -374,6 +550,17 @@ "channels_regionSetTo", "channels_regionNotSet", "channels_regionSelect_Title", - "channels_clearRegion" + "channels_clearRegion", + "chat_receivedGif", + "repeater_keySettings", + "repeater_keySettingsSubtitle", + "repeater_prvKey", + "repeater_prvKeyHelper", + "repeater_generatePrvKey", + "repeater_stopGeneratingPrvKey", + "repeater_pubKey", + "repeater_pubKeyHelper", + "repeater_pubKeyPrefix", + "repeater_pubKeyPrefixHelper" ] }