mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-10-22 15:28:23 +02:00
Compare commits
44
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9d82d6de7 | ||
|
|
38eee8d125 | ||
|
|
f8632b6a06 | ||
|
|
be2fe84db5 | ||
|
|
2cddcbef7d | ||
|
|
60c17c41bf | ||
|
|
ee8ead6695 | ||
|
|
4d6bea8cfe | ||
|
|
25c1611fd3 | ||
|
|
7ed1d7386d | ||
|
|
9268ef2eb2 | ||
|
|
1e2eba53a9 | ||
|
|
d009803215 | ||
|
|
2dc5b6efa1 | ||
|
|
0800f1ed45 | ||
|
|
c844cff67b | ||
|
|
3ff9ff2352 | ||
|
|
12ffe92df8 | ||
|
|
0b812bdaea | ||
|
|
d87a112c0a | ||
|
|
41479e7ee4 | ||
|
|
036ac8c5e6 | ||
|
|
191b5ccae0 | ||
|
|
480f99b360 | ||
|
|
74d8214dda | ||
|
|
8aab359f22 | ||
|
|
b00e44e3bb | ||
|
|
f438c21166 | ||
|
|
e0b2a6a19c | ||
|
|
329974a8f0 | ||
|
|
5b55171080 | ||
|
|
2ba0667a76 | ||
|
|
8a3ece630c | ||
|
|
c2b0f6f9fd | ||
|
|
273bccc5eb | ||
|
|
d7be022a11 | ||
|
|
157e72011f | ||
|
|
bd71f88452 | ||
|
|
6e2fa3a04c | ||
|
|
843d377b87 | ||
|
|
3857023d43 | ||
|
|
ddf7f543ff | ||
|
|
9f72ec9a76 | ||
|
|
16e34b614f |
No files matched your search
@@ -27,7 +27,7 @@ jobs:
|
|||||||
id: set-info
|
id: set-info
|
||||||
run: |
|
run: |
|
||||||
echo "is_prerelease=${{ github.event.release.prerelease }}" >> $GITHUB_OUTPUT
|
echo "is_prerelease=${{ github.event.release.prerelease }}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
|
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
|
||||||
echo "should_upload_to_stores=false" >> $GITHUB_OUTPUT
|
echo "should_upload_to_stores=false" >> $GITHUB_OUTPUT
|
||||||
echo "✅ Pre-release - will build and attach assets, no store uploads"
|
echo "✅ Pre-release - will build and attach assets, no store uploads"
|
||||||
@@ -187,7 +187,7 @@ jobs:
|
|||||||
# import certificate to keychain
|
# import certificate to keychain
|
||||||
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
||||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||||
|
|
||||||
# Set this keychain as the default
|
# Set this keychain as the default
|
||||||
security list-keychain -d user -s $KEYCHAIN_PATH
|
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||||
security default-keychain -s $KEYCHAIN_PATH
|
security default-keychain -s $KEYCHAIN_PATH
|
||||||
@@ -195,7 +195,7 @@ jobs:
|
|||||||
# install provisioning profile
|
# install provisioning profile
|
||||||
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
|
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
|
||||||
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles/61f9fdb9-bf2d-4d94-b249-63155ee71e74.mobileprovision
|
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles/61f9fdb9-bf2d-4d94-b249-63155ee71e74.mobileprovision
|
||||||
|
|
||||||
# Also install using the profile's internal UUID for better compatibility
|
# Also install using the profile's internal UUID for better compatibility
|
||||||
UUID=$(security cms -D -i $PP_PATH | plutil -extract UUID xml1 -o - - | xmllint --xpath "//string/text()" -)
|
UUID=$(security cms -D -i $PP_PATH | plutil -extract UUID xml1 -o - - | xmllint --xpath "//string/text()" -)
|
||||||
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles/$UUID.mobileprovision
|
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles/$UUID.mobileprovision
|
||||||
@@ -329,7 +329,8 @@ jobs:
|
|||||||
--type ios \
|
--type ios \
|
||||||
--file Runner.ipa \
|
--file Runner.ipa \
|
||||||
--apiKey $APP_STORE_CONNECT_API_KEY_ID \
|
--apiKey $APP_STORE_CONNECT_API_KEY_ID \
|
||||||
--apiIssuer $APP_STORE_CONNECT_ISSUER_ID
|
--apiIssuer $APP_STORE_CONNECT_ISSUER_ID \
|
||||||
|
--apple-id 6752760780
|
||||||
|
|
||||||
# Clean up sensitive files
|
# Clean up sensitive files
|
||||||
rm -rf ~/private_keys
|
rm -rf ~/private_keys
|
||||||
|
|||||||
+5
-4
@@ -38,13 +38,14 @@ ios/Flutter/Flutter.framework
|
|||||||
ios/Pods/
|
ios/Pods/
|
||||||
ios/.generated/
|
ios/.generated/
|
||||||
ios/.idea/
|
ios/.idea/
|
||||||
ios/Runner.xcworkspace/
|
ios/Runner.xcworkspace/xcuserdata/
|
||||||
|
ios/Runner.xcworkspace/project.xcworkspace/xcuserdata/
|
||||||
macos/Pods/
|
macos/Pods/
|
||||||
macos/.generated/
|
macos/.generated/
|
||||||
macos/Flutter/ephemeral/
|
macos/Flutter/ephemeral/
|
||||||
# CocoaPods – commit Podfile.lock if you need reproducible iOS builds
|
|
||||||
Podfile.lock
|
|
||||||
Pods/
|
Pods/
|
||||||
|
Podfile
|
||||||
|
Podfile.lock
|
||||||
|
|
||||||
# Generated icons and splash screens
|
# Generated icons and splash screens
|
||||||
ios/Runner/Assets.xcassets/AppIcon.appiconset/*
|
ios/Runner/Assets.xcassets/AppIcon.appiconset/*
|
||||||
@@ -103,4 +104,4 @@ build_keys.conf
|
|||||||
linux/
|
linux/
|
||||||
macos/
|
macos/
|
||||||
web/
|
web/
|
||||||
windows/
|
windows/
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
---
|
|
||||||
An alternative approach to addressing this issue could be adjusting the `optionsBuilder` logic to avoid returning any suggestions when the input text field is empty, rather than guarding `onFieldSubmitted`. For instance:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
optionsBuilder: (TextEditingValue textEditingValue) {
|
|
||||||
if (textEditingValue.text.isEmpty) return <String>[];
|
|
||||||
return suggestions.where((s) => s.contains(textEditingValue.text));
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This ensures that the `RawAutocomplete` widget doesn't offer any options to auto-select on submission when the field is cleared, potentially simplifying the implementation and avoiding the need for additional boolean flags (`guardOnSubmitted`). This pattern can be seen in some implementations "in the wild."
|
|
||||||
+17
-13
@@ -297,26 +297,23 @@ These are internal app tags, not OSM tags. The underscore prefix makes this expl
|
|||||||
- **Sandbox (OSM API)**: Zoom ≥ 13 (stricter due to bbox limits)
|
- **Sandbox (OSM API)**: Zoom ≥ 13 (stricter due to bbox limits)
|
||||||
|
|
||||||
**Smart error handling & splitting:**
|
**Smart error handling & splitting:**
|
||||||
- **50k node limit**: Automatically splits query into 4 quadrants, recursively up to 3 levels deep
|
- **50k node limit**: Automatically splits query into 4 quadrants, recursively up to 3 levels deep. This is a deterministic result-set size limit, so shrinking the queried area is the correct fix.
|
||||||
- **Timeout errors**: Also triggers splitting (dense areas with many profiles)
|
- **Timeout errors**: Treated as a plain retryable network error — no splitting. Timeouts usually indicate server load or an expensive query, not "too much area", so firing off more sub-requests would only add load to an already-struggling server.
|
||||||
- **Rate limiting**: Extended backoff (30s), no splitting (would make it worse)
|
- **Rate limiting (HTTP 429)**: Extended backoff (30s), no splitting (would make it worse). Still surfaces the same "slow" status indicator as the node-limit-splitting case, so the user sees consistent feedback either way.
|
||||||
- **Surgical detection**: Only splits on actual limit errors, not network issues
|
- **Surgical detection**: Only splits on the actual 50k node limit, not timeouts or other network issues.
|
||||||
|
|
||||||
**Query optimization & deduplication:**
|
|
||||||
- **Pre-fetch limit**: 4x user's display limit (e.g., 1000 nodes for 250 display limit)
|
|
||||||
- **Profile deduplication**: Automatically removes redundant profiles from queries using subsumption analysis
|
|
||||||
- **User-initiated detection**: Only reports loading status for user-facing operations
|
|
||||||
- **Background operations**: Pre-fetch runs silently, doesn't trigger loading states
|
|
||||||
|
|
||||||
**Profile subsumption optimization (v2.1.1+):**
|
**Query optimization & deduplication (in `OverpassService._buildQuery`):**
|
||||||
To reduce Overpass query complexity, profiles are deduplicated before query generation:
|
Profiles are deduplicated before query generation via subsumption analysis:
|
||||||
- **Subsumption rule**: Profile A subsumes profile B if all of A's non-empty tags exist in B with identical values
|
- **Subsumption rule**: Profile A subsumes profile B if all of A's non-empty tags exist in B with identical values
|
||||||
- **Example**: `Generic ALPR` (tags: `man_made=surveillance, surveillance:type=ALPR`) subsumes `Flock` (same tags + `manufacturer=Flock Safety`)
|
- **Example**: `Generic ALPR` (tags: `man_made=surveillance, surveillance:type=ALPR`) subsumes `Flock` (same tags + `manufacturer=Flock Safety`)
|
||||||
- **Result**: Default profile set reduces from ~11 to ~2 query clauses (Generic ALPR + Generic Gunshot)
|
- **Result**: Default profile set reduces from ~11 to ~2 query clauses (Generic ALPR + Generic Gunshot)
|
||||||
- **UI unchanged**: All enabled profiles still used for post-query filtering and display matching
|
- **UI unchanged**: All enabled profiles still used for post-query filtering and display matching (see `NodeProviderWithCache._matchesAnyProfile`), independent of which clauses were sent to Overpass
|
||||||
|
- **Safety fallback**: if dedup would remove every profile (e.g. all profiles have only empty tags), the original full list is used instead of sending an empty query
|
||||||
|
|
||||||
**Why this approach:**
|
**Why this approach:**
|
||||||
Dense urban areas (SF, NYC) with many profiles enabled can easily exceed both 50k node limits and 25s timeouts. Profile deduplication reduces query complexity by ~80% for default setups, while automatic splitting handles remaining edge cases. Surgical error detection avoids unnecessary API load from network issues.
|
Dense urban areas (SF, NYC) with many profiles enabled can easily exceed both 50k node limits and 45s timeouts. Profile deduplication reduces query complexity by ~80% for default setups. Surgical error detection avoids unnecessary API load from network issues.
|
||||||
|
|
||||||
|
|
||||||
### 6. Uploader Service Architecture (Refactored v1.5.3)
|
### 6. Uploader Service Architecture (Refactored v1.5.3)
|
||||||
|
|
||||||
@@ -854,6 +851,13 @@ To run the app with working OSM authentication, register OAuth2 applications:
|
|||||||
|
|
||||||
1. **Production OSM**: https://www.openstreetmap.org/oauth2/applications
|
1. **Production OSM**: https://www.openstreetmap.org/oauth2/applications
|
||||||
2. **Sandbox OSM**: https://master.apis.dev.openstreetmap.org/oauth2/applications
|
2. **Sandbox OSM**: https://master.apis.dev.openstreetmap.org/oauth2/applications
|
||||||
|
3. **OAuth2 Application Permissions**
|
||||||
|
* Uncheck "Confidential application?"
|
||||||
|
* Read user preferences (read_prefs)
|
||||||
|
* Modify the map (write_api)
|
||||||
|
* Read, update status and delete user messages (consume_messages)
|
||||||
|
4. **Redirect URIs**
|
||||||
|
* deflockapp://auth
|
||||||
|
|
||||||
For local builds, create `build_keys.conf` (gitignored):
|
For local builds, create `build_keys.conf` (gitignored):
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ android {
|
|||||||
compileSdk = 36
|
compileSdk = 36
|
||||||
|
|
||||||
// NDK only needed if you build native plugins; keep your pinned version
|
// NDK only needed if you build native plugins; keep your pinned version
|
||||||
ndkVersion = "27.0.12077973"
|
ndkVersion = "28.2.13676358"
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
<category android:name="android.intent.category.DEFAULT"/>
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
<category android:name="android.intent.category.BROWSABLE"/>
|
<category android:name="android.intent.category.BROWSABLE"/>
|
||||||
<data android:scheme="deflockapp" android:host="profiles"/>
|
<data android:scheme="deflockapp" android:host="profiles"/>
|
||||||
|
<data android:scheme="deflockapp" android:host="node"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,23 @@
|
|||||||
{
|
{
|
||||||
|
"2.10.2": {
|
||||||
|
"content": [
|
||||||
|
"• Added tap and hold gesture on empty map area to add a node at that location",
|
||||||
|
"• Added ability to accept direct links to a specific node for callbacks from e.g. FlockHopper",
|
||||||
|
"• Fixed app hang during startup when suspected locations database needed updating - refresh now happens in background"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"2.10.1": {
|
||||||
|
"content": [
|
||||||
|
"• Operator profiles are now reorderable",
|
||||||
|
"• Support operator profile import via deflockapp:// links",
|
||||||
|
"• Makes operator profile UI consistent with device profiles"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"2.10.0": {
|
||||||
|
"content": [
|
||||||
|
"• Simplified profile FOVs; there is now only a checkbox for '360'"
|
||||||
|
]
|
||||||
|
},
|
||||||
"2.9.2": {
|
"2.9.2": {
|
||||||
"content": [
|
"content": [
|
||||||
"• Moved 'About OpenStreetMap' section from OSM Account page to Settings > About for better organization",
|
"• Moved 'About OpenStreetMap' section from OSM Account page to Settings > About for better organization",
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
|
||||||
#include "Generated.xcconfig"
|
#include "Generated.xcconfig"
|
||||||
@@ -1,2 +1 @@
|
|||||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
|
||||||
#include "Generated.xcconfig"
|
#include "Generated.xcconfig"
|
||||||
-43
@@ -1,43 +0,0 @@
|
|||||||
# Uncomment this line to define a global platform for your project
|
|
||||||
# platform :ios, '13.0'
|
|
||||||
|
|
||||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
|
||||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
|
||||||
|
|
||||||
project 'Runner', {
|
|
||||||
'Debug' => :debug,
|
|
||||||
'Profile' => :release,
|
|
||||||
'Release' => :release,
|
|
||||||
}
|
|
||||||
|
|
||||||
def flutter_root
|
|
||||||
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
|
|
||||||
unless File.exist?(generated_xcode_build_settings_path)
|
|
||||||
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
|
|
||||||
end
|
|
||||||
|
|
||||||
File.foreach(generated_xcode_build_settings_path) do |line|
|
|
||||||
matches = line.match(/FLUTTER_ROOT\=(.*)/)
|
|
||||||
return matches[1].strip if matches
|
|
||||||
end
|
|
||||||
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
|
|
||||||
end
|
|
||||||
|
|
||||||
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
|
|
||||||
|
|
||||||
flutter_ios_podfile_setup
|
|
||||||
|
|
||||||
target 'Runner' do
|
|
||||||
use_frameworks!
|
|
||||||
|
|
||||||
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
|
||||||
target 'RunnerTests' do
|
|
||||||
inherit! :search_paths
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
post_install do |installer|
|
|
||||||
installer.pods_project.targets.each do |target|
|
|
||||||
flutter_additional_ios_build_settings(target)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -10,9 +10,8 @@
|
|||||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||||
5B624583B69E961F78575427 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C9D0FF4AAF30A10DCC7546BD /* Pods_RunnerTests.framework */; };
|
|
||||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||||
80E9445C8798AC7271AAC8BA /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 015ABBACED80649D4AFF850D /* Pods_Runner.framework */; };
|
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
||||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||||
@@ -42,20 +41,15 @@
|
|||||||
/* End PBXCopyFilesBuildPhase section */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
015ABBACED80649D4AFF850D /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
||||||
045F89E14FC4F02010A83367 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
|
||||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||||
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||||
570CEB9EF97E9EE9B3436119 /* 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 = "<group>"; };
|
|
||||||
731D4782D191731330541A7E /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
|
||||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
7884B7AB945386E2395EFF39 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
|
||||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||||
8DDE785A4593E6CA5CA43172 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
|
|
||||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||||
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
@@ -63,8 +57,6 @@
|
|||||||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
C9D0FF4AAF30A10DCC7546BD /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
||||||
F596A88B00A1B31C43A2B089 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -72,7 +64,6 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
5B624583B69E961F78575427 /* Pods_RunnerTests.framework in Frameworks */,
|
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -80,27 +71,13 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
80E9445C8798AC7271AAC8BA /* Pods_Runner.framework in Frameworks */,
|
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
2C653E01607E349213D63C91 /* Pods */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
F596A88B00A1B31C43A2B089 /* Pods-Runner.debug.xcconfig */,
|
|
||||||
7884B7AB945386E2395EFF39 /* Pods-Runner.release.xcconfig */,
|
|
||||||
570CEB9EF97E9EE9B3436119 /* Pods-Runner.profile.xcconfig */,
|
|
||||||
8DDE785A4593E6CA5CA43172 /* Pods-RunnerTests.debug.xcconfig */,
|
|
||||||
045F89E14FC4F02010A83367 /* Pods-RunnerTests.release.xcconfig */,
|
|
||||||
731D4782D191731330541A7E /* Pods-RunnerTests.profile.xcconfig */,
|
|
||||||
);
|
|
||||||
name = Pods;
|
|
||||||
path = Pods;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
331C8082294A63A400263BE5 /* RunnerTests */ = {
|
331C8082294A63A400263BE5 /* RunnerTests */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -109,18 +86,10 @@
|
|||||||
path = RunnerTests;
|
path = RunnerTests;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
968347D6C1E8FCB1D43AE8AF /* Frameworks */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
015ABBACED80649D4AFF850D /* Pods_Runner.framework */,
|
|
||||||
C9D0FF4AAF30A10DCC7546BD /* Pods_RunnerTests.framework */,
|
|
||||||
);
|
|
||||||
name = Frameworks;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
|
||||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||||
@@ -136,8 +105,6 @@
|
|||||||
97C146F01CF9000F007C117D /* Runner */,
|
97C146F01CF9000F007C117D /* Runner */,
|
||||||
97C146EF1CF9000F007C117D /* Products */,
|
97C146EF1CF9000F007C117D /* Products */,
|
||||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||||
2C653E01607E349213D63C91 /* Pods */,
|
|
||||||
968347D6C1E8FCB1D43AE8AF /* Frameworks */,
|
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -172,7 +139,6 @@
|
|||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
4C9A553A911C8019A2A14C90 /* [CP] Check Pods Manifest.lock */,
|
|
||||||
331C807D294A63A400263BE5 /* Sources */,
|
331C807D294A63A400263BE5 /* Sources */,
|
||||||
331C807F294A63A400263BE5 /* Resources */,
|
331C807F294A63A400263BE5 /* Resources */,
|
||||||
1789E739C02C24F676C90644 /* Frameworks */,
|
1789E739C02C24F676C90644 /* Frameworks */,
|
||||||
@@ -191,20 +157,21 @@
|
|||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
EF132DD72459C72ECA32FCAD /* [CP] Check Pods Manifest.lock */,
|
|
||||||
9740EEB61CF901F6004384FC /* Run Script */,
|
9740EEB61CF901F6004384FC /* Run Script */,
|
||||||
97C146EA1CF9000F007C117D /* Sources */,
|
97C146EA1CF9000F007C117D /* Sources */,
|
||||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||||
97C146EC1CF9000F007C117D /* Resources */,
|
97C146EC1CF9000F007C117D /* Resources */,
|
||||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||||
1553428839027C2A5102CCB3 /* [CP] Embed Pods Frameworks */,
|
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
dependencies = (
|
dependencies = (
|
||||||
);
|
);
|
||||||
name = Runner;
|
name = Runner;
|
||||||
|
packageProductDependencies = (
|
||||||
|
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
|
||||||
|
);
|
||||||
productName = Runner;
|
productName = Runner;
|
||||||
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
@@ -238,6 +205,9 @@
|
|||||||
Base,
|
Base,
|
||||||
);
|
);
|
||||||
mainGroup = 97C146E51CF9000F007C117D;
|
mainGroup = 97C146E51CF9000F007C117D;
|
||||||
|
packageReferences = (
|
||||||
|
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */,
|
||||||
|
);
|
||||||
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
@@ -270,23 +240,6 @@
|
|||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXShellScriptBuildPhase section */
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
1553428839027C2A5102CCB3 /* [CP] Embed Pods Frameworks */ = {
|
|
||||||
isa = PBXShellScriptBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
inputFileListPaths = (
|
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
|
||||||
);
|
|
||||||
name = "[CP] Embed Pods Frameworks";
|
|
||||||
outputFileListPaths = (
|
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
shellPath = /bin/sh;
|
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
|
||||||
showEnvVarsInLog = 0;
|
|
||||||
};
|
|
||||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
alwaysOutOfDate = 1;
|
alwaysOutOfDate = 1;
|
||||||
@@ -303,28 +256,6 @@
|
|||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||||
};
|
};
|
||||||
4C9A553A911C8019A2A14C90 /* [CP] Check Pods Manifest.lock */ = {
|
|
||||||
isa = PBXShellScriptBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
inputFileListPaths = (
|
|
||||||
);
|
|
||||||
inputPaths = (
|
|
||||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
|
||||||
"${PODS_ROOT}/Manifest.lock",
|
|
||||||
);
|
|
||||||
name = "[CP] Check Pods Manifest.lock";
|
|
||||||
outputFileListPaths = (
|
|
||||||
);
|
|
||||||
outputPaths = (
|
|
||||||
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
shellPath = /bin/sh;
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
alwaysOutOfDate = 1;
|
alwaysOutOfDate = 1;
|
||||||
@@ -340,28 +271,6 @@
|
|||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||||
};
|
};
|
||||||
EF132DD72459C72ECA32FCAD /* [CP] Check Pods Manifest.lock */ = {
|
|
||||||
isa = PBXShellScriptBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
inputFileListPaths = (
|
|
||||||
);
|
|
||||||
inputPaths = (
|
|
||||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
|
||||||
"${PODS_ROOT}/Manifest.lock",
|
|
||||||
);
|
|
||||||
name = "[CP] Check Pods Manifest.lock";
|
|
||||||
outputFileListPaths = (
|
|
||||||
);
|
|
||||||
outputPaths = (
|
|
||||||
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
shellPath = /bin/sh;
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
/* End PBXShellScriptBuildPhase section */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
@@ -491,7 +400,6 @@
|
|||||||
};
|
};
|
||||||
331C8088294A63A400263BE5 /* Debug */ = {
|
331C8088294A63A400263BE5 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 8DDE785A4593E6CA5CA43172 /* Pods-RunnerTests.debug.xcconfig */;
|
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
@@ -509,7 +417,6 @@
|
|||||||
};
|
};
|
||||||
331C8089294A63A400263BE5 /* Release */ = {
|
331C8089294A63A400263BE5 /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 045F89E14FC4F02010A83367 /* Pods-RunnerTests.release.xcconfig */;
|
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
@@ -525,7 +432,6 @@
|
|||||||
};
|
};
|
||||||
331C808A294A63A400263BE5 /* Profile */ = {
|
331C808A294A63A400263BE5 /* Profile */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 731D4782D191731330541A7E /* Pods-RunnerTests.profile.xcconfig */;
|
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
@@ -735,6 +641,20 @@
|
|||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
/* End XCConfigurationList section */
|
/* 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 */;
|
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,24 @@
|
|||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
buildImplicitDependencies = "YES">
|
buildImplicitDependencies = "YES">
|
||||||
|
<PreActions>
|
||||||
|
<ExecutionAction
|
||||||
|
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||||
|
<ActionContent
|
||||||
|
title = "Run Prepare Flutter Framework Script"
|
||||||
|
scriptText = "/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" prepare ">
|
||||||
|
<EnvironmentBuildable>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</EnvironmentBuildable>
|
||||||
|
</ActionContent>
|
||||||
|
</ExecutionAction>
|
||||||
|
</PreActions>
|
||||||
<BuildActionEntries>
|
<BuildActionEntries>
|
||||||
<BuildActionEntry
|
<BuildActionEntry
|
||||||
buildForTesting = "YES"
|
buildForTesting = "YES"
|
||||||
|
|||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "group:Runner.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
<FileRef
|
||||||
|
location = "group:Pods/Pods.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
+17
-1
@@ -156,6 +156,7 @@ class AppState extends ChangeNotifier {
|
|||||||
int get maxNodes => _settingsState.maxNodes;
|
int get maxNodes => _settingsState.maxNodes;
|
||||||
UploadMode get uploadMode => _settingsState.uploadMode;
|
UploadMode get uploadMode => _settingsState.uploadMode;
|
||||||
FollowMeMode get followMeMode => _settingsState.followMeMode;
|
FollowMeMode get followMeMode => _settingsState.followMeMode;
|
||||||
|
bool get keepScreenAwake => _settingsState.keepScreenAwake;
|
||||||
|
|
||||||
bool get proximityAlertsEnabled => _settingsState.proximityAlertsEnabled;
|
bool get proximityAlertsEnabled => _settingsState.proximityAlertsEnabled;
|
||||||
int get proximityAlertDistance => _settingsState.proximityAlertDistance;
|
int get proximityAlertDistance => _settingsState.proximityAlertDistance;
|
||||||
@@ -172,7 +173,6 @@ class AppState extends ChangeNotifier {
|
|||||||
TileType? get selectedTileType => _settingsState.selectedTileType;
|
TileType? get selectedTileType => _settingsState.selectedTileType;
|
||||||
TileProvider? get selectedTileProvider => _settingsState.selectedTileProvider;
|
TileProvider? get selectedTileProvider => _settingsState.selectedTileProvider;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Upload queue state
|
// Upload queue state
|
||||||
int get pendingCount => _uploadQueueState.pendingCount;
|
int get pendingCount => _uploadQueueState.pendingCount;
|
||||||
@@ -250,6 +250,9 @@ class AppState extends ChangeNotifier {
|
|||||||
|
|
||||||
_isInitialized = true;
|
_isInitialized = true;
|
||||||
|
|
||||||
|
// Start background refresh of suspected locations if needed (non-blocking)
|
||||||
|
_suspectedLocationState.initBackgroundRefresh(offlineMode: _settingsState.offlineMode);
|
||||||
|
|
||||||
// Check for initial deep link after a small delay to let navigation settle
|
// Check for initial deep link after a small delay to let navigation settle
|
||||||
Future.delayed(const Duration(milliseconds: 500), () {
|
Future.delayed(const Duration(milliseconds: 500), () {
|
||||||
DeepLinkService().checkInitialLink();
|
DeepLinkService().checkInitialLink();
|
||||||
@@ -414,6 +417,11 @@ class AppState extends ChangeNotifier {
|
|||||||
void deleteProfile(NodeProfile p) {
|
void deleteProfile(NodeProfile p) {
|
||||||
_profileState.deleteProfile(p);
|
_profileState.deleteProfile(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reload all profiles from storage (useful after migrations modify stored profiles)
|
||||||
|
Future<void> reloadProfiles() async {
|
||||||
|
await _profileState.reloadFromStorage();
|
||||||
|
}
|
||||||
|
|
||||||
// Callback when a profile is deleted - clear any stale session references
|
// Callback when a profile is deleted - clear any stale session references
|
||||||
void _onProfileDeleted(NodeProfile deletedProfile) {
|
void _onProfileDeleted(NodeProfile deletedProfile) {
|
||||||
@@ -437,6 +445,10 @@ class AppState extends ChangeNotifier {
|
|||||||
_operatorProfileState.deleteProfile(p);
|
_operatorProfileState.deleteProfile(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void reorderOperatorProfiles(int oldIndex, int newIndex) {
|
||||||
|
_operatorProfileState.reorderProfiles(oldIndex, newIndex);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- Session Methods ----------
|
// ---------- Session Methods ----------
|
||||||
void startAddSession() {
|
void startAddSession() {
|
||||||
_sessionState.startAddSession(enabledProfiles);
|
_sessionState.startAddSession(enabledProfiles);
|
||||||
@@ -669,6 +681,10 @@ class AppState extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setKeepScreenAwake(bool enabled) async {
|
||||||
|
await _settingsState.setKeepScreenAwake(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> setPauseQueueProcessing(bool enabled) async {
|
Future<void> setPauseQueueProcessing(bool enabled) async {
|
||||||
await _settingsState.setPauseQueueProcessing(enabled);
|
await _settingsState.setPauseQueueProcessing(enabled);
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
|
|||||||
+9
-8
@@ -82,6 +82,9 @@ const bool kEnableNodeEdits = true; // Set to false to temporarily disable node
|
|||||||
// Node extraction features - set to false to hide extract functionality for constrained nodes
|
// Node extraction features - set to false to hide extract functionality for constrained nodes
|
||||||
const bool kEnableNodeExtraction = false; // Set to true to enable extract from way/relation feature (WIP)
|
const bool kEnableNodeExtraction = false; // Set to true to enable extract from way/relation feature (WIP)
|
||||||
|
|
||||||
|
// Profile FOV features - set to false to restrict profiles to 360° FOV only
|
||||||
|
const bool kEnableNon360FOVs = false; // Set to true to allow custom FOV values in profiles
|
||||||
|
|
||||||
/// Navigation availability: only dev builds, and only when online
|
/// Navigation availability: only dev builds, and only when online
|
||||||
bool enableNavigationFeatures({required bool offlineMode}) {
|
bool enableNavigationFeatures({required bool offlineMode}) {
|
||||||
return kEnableNavigationFeatures && !offlineMode;
|
return kEnableNavigationFeatures && !offlineMode;
|
||||||
@@ -93,15 +96,13 @@ const int kOsmApiMinZoomLevel = 13; // Minimum zoom for OSM API bbox queries (sa
|
|||||||
const int kMinZoomForNodeEditingSheets = 16; // Minimum zoom to open add/edit node sheets
|
const int kMinZoomForNodeEditingSheets = 16; // Minimum zoom to open add/edit node sheets
|
||||||
const int kMinZoomForOfflineDownload = 10; // Minimum zoom to download offline areas (prevents large area crashes)
|
const int kMinZoomForOfflineDownload = 10; // Minimum zoom to download offline areas (prevents large area crashes)
|
||||||
const Duration kMarkerTapTimeout = Duration(milliseconds: 250);
|
const Duration kMarkerTapTimeout = Duration(milliseconds: 250);
|
||||||
|
const Duration kMapLongPressTimeout = Duration(milliseconds: 600); // Duration to trigger "add node here" on empty map area
|
||||||
const Duration kDebounceCameraRefresh = Duration(milliseconds: 500);
|
const Duration kDebounceCameraRefresh = Duration(milliseconds: 500);
|
||||||
|
|
||||||
// Pre-fetch area configuration
|
|
||||||
const double kPreFetchAreaExpansionMultiplier = 3.0; // Expand visible bounds by this factor for pre-fetching
|
|
||||||
const double kNodeRenderingBoundsExpansion = 1.3; // Expand visible bounds by this factor for node rendering to prevent edge blinking
|
const double kNodeRenderingBoundsExpansion = 1.3; // Expand visible bounds by this factor for node rendering to prevent edge blinking
|
||||||
const double kRouteProximityThresholdMeters = 500.0; // Distance threshold for determining if user is near route when resuming navigation
|
const double kRouteProximityThresholdMeters = 500.0; // Distance threshold for determining if user is near route when resuming navigation
|
||||||
const double kResumeNavigationZoomLevel = 16.0; // Zoom level when resuming navigation
|
const double kResumeNavigationZoomLevel = 16.0; // Zoom level when resuming navigation
|
||||||
const int kPreFetchZoomLevel = 10; // Always pre-fetch at this zoom level for consistent area sizes
|
|
||||||
const int kMaxPreFetchSplitDepth = 3; // Maximum recursive splits when hitting Overpass node limit
|
|
||||||
|
|
||||||
// Data refresh configuration
|
// Data refresh configuration
|
||||||
const int kDataRefreshIntervalSeconds = 60; // Refresh cached data after this many seconds
|
const int kDataRefreshIntervalSeconds = 60; // Refresh cached data after this many seconds
|
||||||
@@ -161,11 +162,11 @@ const int kAbsoluteMaxTileCount = 50000;
|
|||||||
const int kAbsoluteMaxZoom = 23;
|
const int kAbsoluteMaxZoom = 23;
|
||||||
|
|
||||||
// Node icon configuration
|
// Node icon configuration
|
||||||
const double kNodeIconDiameter = 18.0;
|
const double kNodeIconDiameter = 17.0;
|
||||||
const double _kNodeRingThicknessBase = 2.5;
|
const double _kNodeRingThicknessBase = 2.6;
|
||||||
const double kNodeDotOpacity = 0.3; // Opacity for the grey dot interior
|
const double kNodeDotOpacity = 0.28; // Opacity for the grey dot interior
|
||||||
const Color kNodeRingColorReal = Color(0xFF3036F0); // Real nodes from OSM - blue
|
const Color kNodeRingColorReal = Color(0xFF3036F0); // Real nodes from OSM - blue
|
||||||
const Color kNodeRingColorMock = Color(0xD0FFFFFF); // Add node mock point - white
|
const Color kNodeRingColorMock = Color(0xE044BB55); // Add node mock point - white
|
||||||
const Color kNodeRingColorPending = Color(0xD09C27B0); // Submitted/pending nodes - purple
|
const Color kNodeRingColorPending = Color(0xD09C27B0); // Submitted/pending nodes - purple
|
||||||
const Color kNodeRingColorEditing = Color(0xD0FF9800); // Node being edited - orange
|
const Color kNodeRingColorEditing = Color(0xD0FF9800); // Node being edited - orange
|
||||||
const Color kNodeRingColorPendingEdit = Color(0xD0757575); // Original node with pending edit - grey
|
const Color kNodeRingColorPendingEdit = Color(0xD0757575); // Original node with pending edit - grey
|
||||||
|
|||||||
@@ -74,8 +74,13 @@
|
|||||||
"advancedSettings": "Erweiterte Einstellungen",
|
"advancedSettings": "Erweiterte Einstellungen",
|
||||||
"advancedSettingsSubtitle": "Leistungs-, Warnungs- und Kachelanbieter-Einstellungen",
|
"advancedSettingsSubtitle": "Leistungs-, Warnungs- und Kachelanbieter-Einstellungen",
|
||||||
"proximityAlerts": "Näherungswarnungen",
|
"proximityAlerts": "Näherungswarnungen",
|
||||||
|
"keepScreenAwake": "Bildschirm Aktiv Lassen",
|
||||||
"networkStatusIndicator": "Netzwerkstatus-Anzeige"
|
"networkStatusIndicator": "Netzwerkstatus-Anzeige"
|
||||||
},
|
},
|
||||||
|
"keepScreenAwake": {
|
||||||
|
"title": "Bildschirm während der App-Nutzung aktiv lassen",
|
||||||
|
"subtitle": "Verhindert, dass sich der Bildschirm während der Nutzung der App automatisch ausschaltet"
|
||||||
|
},
|
||||||
"proximityAlerts": {
|
"proximityAlerts": {
|
||||||
"getNotified": "Benachrichtigung erhalten beim Annähern an Überwachungsgeräte",
|
"getNotified": "Benachrichtigung erhalten beim Annähern an Überwachungsgeräte",
|
||||||
"batteryUsage": "Verbraucht zusätzlich Batterie für kontinuierliche Standortüberwachung",
|
"batteryUsage": "Verbraucht zusätzlich Batterie für kontinuierliche Standortüberwachung",
|
||||||
@@ -324,6 +329,8 @@
|
|||||||
"fovHint": "Sichtfeld in Grad (leer lassen für Standard)",
|
"fovHint": "Sichtfeld in Grad (leer lassen für Standard)",
|
||||||
"fovSubtitle": "Kamera-Sichtfeld - verwendet für Kegelbreite und Bereichsübertragungsformat",
|
"fovSubtitle": "Kamera-Sichtfeld - verwendet für Kegelbreite und Bereichsübertragungsformat",
|
||||||
"fovInvalid": "Sichtfeld muss zwischen 1 und 360 Grad liegen",
|
"fovInvalid": "Sichtfeld muss zwischen 1 und 360 Grad liegen",
|
||||||
|
"fov360": "360° Sichtfeld",
|
||||||
|
"fov360Subtitle": "Kamera hat 360-Grad omnidirektionales Sichtfeld",
|
||||||
"submittable": "Übertragbar",
|
"submittable": "Übertragbar",
|
||||||
"submittableSubtitle": "Ob dieses Profil für Kamera-Übertragungen verwendet werden kann",
|
"submittableSubtitle": "Ob dieses Profil für Kamera-Übertragungen verwendet werden kann",
|
||||||
"osmTags": "OSM-Tags",
|
"osmTags": "OSM-Tags",
|
||||||
@@ -524,7 +531,7 @@
|
|||||||
"dataSourceDescription": "Versorgungsgenehmigungsdaten, die auf potenzielle Installationsstandorte für Überwachungsinfrastruktur hinweisen",
|
"dataSourceDescription": "Versorgungsgenehmigungsdaten, die auf potenzielle Installationsstandorte für Überwachungsinfrastruktur hinweisen",
|
||||||
"dataSourceCredit": "Datensammlung und -hosting bereitgestellt von alprwatch.org",
|
"dataSourceCredit": "Datensammlung und -hosting bereitgestellt von alprwatch.org",
|
||||||
"minimumDistance": "Mindestabstand zu echten Geräten",
|
"minimumDistance": "Mindestabstand zu echten Geräten",
|
||||||
"minimumDistanceSubtitle": "Verdächtige Standorte innerhalb von {}m vorhandener Überwachungsgeräte ausblenden",
|
"minimumDistanceSubtitle": "Verdächtige Standorte innerhalb von {} vorhandener Überwachungsgeräte ausblenden",
|
||||||
"updating": "Verdächtige Standorte werden aktualisiert",
|
"updating": "Verdächtige Standorte werden aktualisiert",
|
||||||
"downloadingAndProcessing": "Daten werden heruntergeladen und verarbeitet...",
|
"downloadingAndProcessing": "Daten werden heruntergeladen und verarbeitet...",
|
||||||
"updateSuccess": "Verdächtige Standorte erfolgreich aktualisiert",
|
"updateSuccess": "Verdächtige Standorte erfolgreich aktualisiert",
|
||||||
|
|||||||
@@ -111,7 +111,12 @@
|
|||||||
"advancedSettings": "Advanced Settings",
|
"advancedSettings": "Advanced Settings",
|
||||||
"advancedSettingsSubtitle": "Performance, alerts, and tile provider settings",
|
"advancedSettingsSubtitle": "Performance, alerts, and tile provider settings",
|
||||||
"proximityAlerts": "Proximity Alerts",
|
"proximityAlerts": "Proximity Alerts",
|
||||||
"networkStatusIndicator": "Network Status Indicator"
|
"networkStatusIndicator": "Network Status Indicator",
|
||||||
|
"keepScreenAwake": "Keep Screen Awake"
|
||||||
|
},
|
||||||
|
"keepScreenAwake": {
|
||||||
|
"title": "Keep screen awake while using app",
|
||||||
|
"subtitle": "Prevents the screen from turning off automatically while using the app"
|
||||||
},
|
},
|
||||||
"proximityAlerts": {
|
"proximityAlerts": {
|
||||||
"getNotified": "Get notified when approaching surveillance devices",
|
"getNotified": "Get notified when approaching surveillance devices",
|
||||||
@@ -361,6 +366,8 @@
|
|||||||
"fovHint": "FOV in degrees (leave empty for default)",
|
"fovHint": "FOV in degrees (leave empty for default)",
|
||||||
"fovSubtitle": "Camera field of view - used for cone width and range submission format",
|
"fovSubtitle": "Camera field of view - used for cone width and range submission format",
|
||||||
"fovInvalid": "FOV must be between 1 and 360 degrees",
|
"fovInvalid": "FOV must be between 1 and 360 degrees",
|
||||||
|
"fov360": "360 FOV",
|
||||||
|
"fov360Subtitle": "Camera has 360-degree omnidirectional field of view",
|
||||||
"submittable": "Submittable",
|
"submittable": "Submittable",
|
||||||
"submittableSubtitle": "Whether this profile can be used for camera submissions",
|
"submittableSubtitle": "Whether this profile can be used for camera submissions",
|
||||||
"osmTags": "OSM Tags",
|
"osmTags": "OSM Tags",
|
||||||
@@ -524,7 +531,7 @@
|
|||||||
"dataSourceDescription": "Utility permit data indicating potential surveillance infrastructure installation sites",
|
"dataSourceDescription": "Utility permit data indicating potential surveillance infrastructure installation sites",
|
||||||
"dataSourceCredit": "Data collection and hosting provided by alprwatch.org",
|
"dataSourceCredit": "Data collection and hosting provided by alprwatch.org",
|
||||||
"minimumDistance": "Minimum Distance from Real Nodes",
|
"minimumDistance": "Minimum Distance from Real Nodes",
|
||||||
"minimumDistanceSubtitle": "Hide suspected locations within {}m of existing surveillance devices",
|
"minimumDistanceSubtitle": "Hide suspected locations within {} of existing surveillance devices",
|
||||||
"updating": "Updating Suspected Locations",
|
"updating": "Updating Suspected Locations",
|
||||||
"downloadingAndProcessing": "Downloading and processing data...",
|
"downloadingAndProcessing": "Downloading and processing data...",
|
||||||
"updateSuccess": "Suspected locations updated successfully",
|
"updateSuccess": "Suspected locations updated successfully",
|
||||||
|
|||||||
@@ -111,8 +111,13 @@
|
|||||||
"advancedSettings": "Configuración Avanzada",
|
"advancedSettings": "Configuración Avanzada",
|
||||||
"advancedSettingsSubtitle": "Configuración de rendimiento, alertas y proveedores de teselas",
|
"advancedSettingsSubtitle": "Configuración de rendimiento, alertas y proveedores de teselas",
|
||||||
"proximityAlerts": "Alertas de Proximidad",
|
"proximityAlerts": "Alertas de Proximidad",
|
||||||
|
"keepScreenAwake": "Mantener la Pantalla Encendida",
|
||||||
"networkStatusIndicator": "Indicador de Estado de Red"
|
"networkStatusIndicator": "Indicador de Estado de Red"
|
||||||
},
|
},
|
||||||
|
"keepScreenAwake": {
|
||||||
|
"title": "Mantener la pantalla encendida mientras se usa la app",
|
||||||
|
"subtitle": "Evita que la pantalla se apague automáticamente mientras se usa la app"
|
||||||
|
},
|
||||||
"proximityAlerts": {
|
"proximityAlerts": {
|
||||||
"getNotified": "Recibe notificaciones al acercarte a dispositivos de vigilancia",
|
"getNotified": "Recibe notificaciones al acercarte a dispositivos de vigilancia",
|
||||||
"batteryUsage": "Usa batería extra para monitoreo continuo de ubicación",
|
"batteryUsage": "Usa batería extra para monitoreo continuo de ubicación",
|
||||||
@@ -361,6 +366,8 @@
|
|||||||
"fovHint": "Campo de visión en grados (dejar vacío para el predeterminado)",
|
"fovHint": "Campo de visión en grados (dejar vacío para el predeterminado)",
|
||||||
"fovSubtitle": "Campo de visión de la cámara - usado para el ancho del cono y formato de envío por rango",
|
"fovSubtitle": "Campo de visión de la cámara - usado para el ancho del cono y formato de envío por rango",
|
||||||
"fovInvalid": "El campo de visión debe estar entre 1 y 360 grados",
|
"fovInvalid": "El campo de visión debe estar entre 1 y 360 grados",
|
||||||
|
"fov360": "Campo de Visión 360°",
|
||||||
|
"fov360Subtitle": "La cámara tiene un campo de visión omnidireccional de 360 grados",
|
||||||
"submittable": "Envíable",
|
"submittable": "Envíable",
|
||||||
"submittableSubtitle": "Si este perfil puede usarse para envíos de cámaras",
|
"submittableSubtitle": "Si este perfil puede usarse para envíos de cámaras",
|
||||||
"osmTags": "Etiquetas OSM",
|
"osmTags": "Etiquetas OSM",
|
||||||
@@ -524,7 +531,7 @@
|
|||||||
"dataSourceDescription": "Datos de permisos de servicios públicos que indican posibles sitios de instalación de infraestructura de vigilancia",
|
"dataSourceDescription": "Datos de permisos de servicios públicos que indican posibles sitios de instalación de infraestructura de vigilancia",
|
||||||
"dataSourceCredit": "Recopilación y alojamiento de datos proporcionado por alprwatch.org",
|
"dataSourceCredit": "Recopilación y alojamiento de datos proporcionado por alprwatch.org",
|
||||||
"minimumDistance": "Distancia Mínima de Nodos Reales",
|
"minimumDistance": "Distancia Mínima de Nodos Reales",
|
||||||
"minimumDistanceSubtitle": "Ocultar ubicaciones sospechosas dentro de {}m de dispositivos de vigilancia existentes",
|
"minimumDistanceSubtitle": "Ocultar ubicaciones sospechosas dentro de {} de dispositivos de vigilancia existentes",
|
||||||
"updating": "Actualizando Ubicaciones Sospechosas",
|
"updating": "Actualizando Ubicaciones Sospechosas",
|
||||||
"downloadingAndProcessing": "Descargando y procesando datos...",
|
"downloadingAndProcessing": "Descargando y procesando datos...",
|
||||||
"updateSuccess": "Ubicaciones sospechosas actualizadas exitosamente",
|
"updateSuccess": "Ubicaciones sospechosas actualizadas exitosamente",
|
||||||
|
|||||||
@@ -111,8 +111,13 @@
|
|||||||
"advancedSettings": "Paramètres Avancés",
|
"advancedSettings": "Paramètres Avancés",
|
||||||
"advancedSettingsSubtitle": "Paramètres de performance, alertes et fournisseurs de tuiles",
|
"advancedSettingsSubtitle": "Paramètres de performance, alertes et fournisseurs de tuiles",
|
||||||
"proximityAlerts": "Alertes de Proximité",
|
"proximityAlerts": "Alertes de Proximité",
|
||||||
|
"keepScreenAwake": "Maintenir l'écran Allumé",
|
||||||
"networkStatusIndicator": "Indicateur de Statut Réseau"
|
"networkStatusIndicator": "Indicateur de Statut Réseau"
|
||||||
},
|
},
|
||||||
|
"keepScreenAwake": {
|
||||||
|
"title": "Maintenir l'écran allumé lors de l'utilisation de l'application",
|
||||||
|
"subtitle": "Empêche l'écran de s'éteindre automatiquement pendant l'utilisation de l'application"
|
||||||
|
},
|
||||||
"proximityAlerts": {
|
"proximityAlerts": {
|
||||||
"getNotified": "Recevoir des notifications en s'approchant de dispositifs de surveillance",
|
"getNotified": "Recevoir des notifications en s'approchant de dispositifs de surveillance",
|
||||||
"batteryUsage": "Utilise de la batterie supplémentaire pour la surveillance continue de la localisation",
|
"batteryUsage": "Utilise de la batterie supplémentaire pour la surveillance continue de la localisation",
|
||||||
@@ -361,6 +366,8 @@
|
|||||||
"fovHint": "Champ de vision en degrés (laisser vide pour la valeur par défaut)",
|
"fovHint": "Champ de vision en degrés (laisser vide pour la valeur par défaut)",
|
||||||
"fovSubtitle": "Champ de vision de la caméra - utilisé pour la largeur du cône et le format de soumission par plage",
|
"fovSubtitle": "Champ de vision de la caméra - utilisé pour la largeur du cône et le format de soumission par plage",
|
||||||
"fovInvalid": "Le champ de vision doit être entre 1 et 360 degrés",
|
"fovInvalid": "Le champ de vision doit être entre 1 et 360 degrés",
|
||||||
|
"fov360": "Champ de Vision 360°",
|
||||||
|
"fov360Subtitle": "La caméra a un champ de vision omnidirectionnel de 360 degrés",
|
||||||
"submittable": "Soumissible",
|
"submittable": "Soumissible",
|
||||||
"submittableSubtitle": "Si ce profil peut être utilisé pour les soumissions de caméras",
|
"submittableSubtitle": "Si ce profil peut être utilisé pour les soumissions de caméras",
|
||||||
"osmTags": "Balises OSM",
|
"osmTags": "Balises OSM",
|
||||||
@@ -524,7 +531,7 @@
|
|||||||
"dataSourceDescription": "Données de permis de services publics indiquant des sites d'installation potentiels d'infrastructure de surveillance",
|
"dataSourceDescription": "Données de permis de services publics indiquant des sites d'installation potentiels d'infrastructure de surveillance",
|
||||||
"dataSourceCredit": "Collecte et hébergement des données fournis par alprwatch.org",
|
"dataSourceCredit": "Collecte et hébergement des données fournis par alprwatch.org",
|
||||||
"minimumDistance": "Distance Minimale des Nœuds Réels",
|
"minimumDistance": "Distance Minimale des Nœuds Réels",
|
||||||
"minimumDistanceSubtitle": "Masquer les emplacements suspects dans un rayon de {}m des dispositifs de surveillance existants",
|
"minimumDistanceSubtitle": "Masquer les emplacements suspects dans un rayon de {} des dispositifs de surveillance existants",
|
||||||
"updating": "Mise à Jour des Emplacements Suspects",
|
"updating": "Mise à Jour des Emplacements Suspects",
|
||||||
"downloadingAndProcessing": "Téléchargement et traitement des données...",
|
"downloadingAndProcessing": "Téléchargement et traitement des données...",
|
||||||
"updateSuccess": "Emplacements suspects mis à jour avec succès",
|
"updateSuccess": "Emplacements suspects mis à jour avec succès",
|
||||||
|
|||||||
@@ -111,8 +111,13 @@
|
|||||||
"advancedSettings": "Impostazioni Avanzate",
|
"advancedSettings": "Impostazioni Avanzate",
|
||||||
"advancedSettingsSubtitle": "Impostazioni di prestazioni, avvisi e fornitori di tessere",
|
"advancedSettingsSubtitle": "Impostazioni di prestazioni, avvisi e fornitori di tessere",
|
||||||
"proximityAlerts": "Avvisi di Prossimità",
|
"proximityAlerts": "Avvisi di Prossimità",
|
||||||
|
"keepScreenAwake": "Mantieni lo Schermo Attivo",
|
||||||
"networkStatusIndicator": "Indicatore di Stato di Rete"
|
"networkStatusIndicator": "Indicatore di Stato di Rete"
|
||||||
},
|
},
|
||||||
|
"keepScreenAwake": {
|
||||||
|
"title": "Mantieni lo schermo attivo durante l'utilizzo dell'app",
|
||||||
|
"subtitle": "Impedisce allo schermo di spegnersi automaticamente durante l'utilizzo dell'app"
|
||||||
|
},
|
||||||
"proximityAlerts": {
|
"proximityAlerts": {
|
||||||
"getNotified": "Ricevi notifiche quando ti avvicini a dispositivi di sorveglianza",
|
"getNotified": "Ricevi notifiche quando ti avvicini a dispositivi di sorveglianza",
|
||||||
"batteryUsage": "Utilizza batteria extra per il monitoraggio continuo della posizione",
|
"batteryUsage": "Utilizza batteria extra per il monitoraggio continuo della posizione",
|
||||||
@@ -361,6 +366,8 @@
|
|||||||
"fovHint": "Campo visivo in gradi (lasciare vuoto per il valore predefinito)",
|
"fovHint": "Campo visivo in gradi (lasciare vuoto per il valore predefinito)",
|
||||||
"fovSubtitle": "Campo visivo della telecamera - utilizzato per la larghezza del cono e il formato di invio per intervallo",
|
"fovSubtitle": "Campo visivo della telecamera - utilizzato per la larghezza del cono e il formato di invio per intervallo",
|
||||||
"fovInvalid": "Il campo visivo deve essere tra 1 e 360 gradi",
|
"fovInvalid": "Il campo visivo deve essere tra 1 e 360 gradi",
|
||||||
|
"fov360": "Campo Visivo 360°",
|
||||||
|
"fov360Subtitle": "La telecamera ha un campo visivo omnidirezionale di 360 gradi",
|
||||||
"submittable": "Inviabile",
|
"submittable": "Inviabile",
|
||||||
"submittableSubtitle": "Se questo profilo può essere usato per invii di telecamere",
|
"submittableSubtitle": "Se questo profilo può essere usato per invii di telecamere",
|
||||||
"osmTags": "Tag OSM",
|
"osmTags": "Tag OSM",
|
||||||
@@ -524,7 +531,7 @@
|
|||||||
"dataSourceDescription": "Dati dei permessi dei servizi pubblici che indicano potenziali siti di installazione di infrastrutture di sorveglianza",
|
"dataSourceDescription": "Dati dei permessi dei servizi pubblici che indicano potenziali siti di installazione di infrastrutture di sorveglianza",
|
||||||
"dataSourceCredit": "Raccolta e hosting dei dati forniti da alprwatch.org",
|
"dataSourceCredit": "Raccolta e hosting dei dati forniti da alprwatch.org",
|
||||||
"minimumDistance": "Distanza Minima dai Nodi Reali",
|
"minimumDistance": "Distanza Minima dai Nodi Reali",
|
||||||
"minimumDistanceSubtitle": "Nascondi posizioni sospette entro {}m dai dispositivi di sorveglianza esistenti",
|
"minimumDistanceSubtitle": "Nascondi posizioni sospette entro {} dai dispositivi di sorveglianza esistenti",
|
||||||
"updating": "Aggiornamento Posizioni Sospette",
|
"updating": "Aggiornamento Posizioni Sospette",
|
||||||
"downloadingAndProcessing": "Scaricamento e elaborazione dati...",
|
"downloadingAndProcessing": "Scaricamento e elaborazione dati...",
|
||||||
"updateSuccess": "Posizioni sospette aggiornate con successo",
|
"updateSuccess": "Posizioni sospette aggiornate con successo",
|
||||||
|
|||||||
@@ -111,8 +111,13 @@
|
|||||||
"advancedSettings": "Geavanceerde Instellingen",
|
"advancedSettings": "Geavanceerde Instellingen",
|
||||||
"advancedSettingsSubtitle": "Prestaties, waarschuwingen en tile provider instellingen",
|
"advancedSettingsSubtitle": "Prestaties, waarschuwingen en tile provider instellingen",
|
||||||
"proximityAlerts": "Nabijheids Waarschuwingen",
|
"proximityAlerts": "Nabijheids Waarschuwingen",
|
||||||
|
"keepScreenAwake": "Scherm Actief Houden",
|
||||||
"networkStatusIndicator": "Netwerk Status Indicator"
|
"networkStatusIndicator": "Netwerk Status Indicator"
|
||||||
},
|
},
|
||||||
|
"keepScreenAwake": {
|
||||||
|
"title": "Houd het scherm actief tijdens het gebruik van de app",
|
||||||
|
"subtitle": "Voorkomt dat het scherm automatisch uitschakelt tijdens het gebruik van de app"
|
||||||
|
},
|
||||||
"proximityAlerts": {
|
"proximityAlerts": {
|
||||||
"getNotified": "Krijg meldingen wanneer u surveillance apparaten nadert",
|
"getNotified": "Krijg meldingen wanneer u surveillance apparaten nadert",
|
||||||
"batteryUsage": "Gebruikt extra batterij voor continue locatie monitoring",
|
"batteryUsage": "Gebruikt extra batterij voor continue locatie monitoring",
|
||||||
@@ -361,6 +366,8 @@
|
|||||||
"fovHint": "FOV in graden (laat leeg voor standaard)",
|
"fovHint": "FOV in graden (laat leeg voor standaard)",
|
||||||
"fovSubtitle": "Camera gezichtsveld - gebruikt voor kegel breedte en bereik inzending formaat",
|
"fovSubtitle": "Camera gezichtsveld - gebruikt voor kegel breedte en bereik inzending formaat",
|
||||||
"fovInvalid": "FOV moet tussen 1 en 360 graden zijn",
|
"fovInvalid": "FOV moet tussen 1 en 360 graden zijn",
|
||||||
|
"fov360": "360° Gezichtsveld",
|
||||||
|
"fov360Subtitle": "Camera heeft een 360-graden omnidirectioneel gezichtsveld",
|
||||||
"submittable": "Indienbaar",
|
"submittable": "Indienbaar",
|
||||||
"submittableSubtitle": "Of dit profiel gebruikt kan worden voor camera inzendingen",
|
"submittableSubtitle": "Of dit profiel gebruikt kan worden voor camera inzendingen",
|
||||||
"osmTags": "OSM Tags",
|
"osmTags": "OSM Tags",
|
||||||
@@ -524,7 +531,7 @@
|
|||||||
"dataSourceDescription": "Nutsbedrijf vergunning data die mogelijke surveillance infrastructuur installatie sites aangeeft",
|
"dataSourceDescription": "Nutsbedrijf vergunning data die mogelijke surveillance infrastructuur installatie sites aangeeft",
|
||||||
"dataSourceCredit": "Gegevens verzameling en hosting geleverd door alprwatch.org",
|
"dataSourceCredit": "Gegevens verzameling en hosting geleverd door alprwatch.org",
|
||||||
"minimumDistance": "Minimum Afstand van Echte Nodes",
|
"minimumDistance": "Minimum Afstand van Echte Nodes",
|
||||||
"minimumDistanceSubtitle": "Verberg verdachte locaties binnen {}m van bestaande surveillance apparaten",
|
"minimumDistanceSubtitle": "Verberg verdachte locaties binnen {} van bestaande surveillance apparaten",
|
||||||
"updating": "Verdachte Locaties Bijwerken",
|
"updating": "Verdachte Locaties Bijwerken",
|
||||||
"downloadingAndProcessing": "Data downloaden en verwerken...",
|
"downloadingAndProcessing": "Data downloaden en verwerken...",
|
||||||
"updateSuccess": "Verdachte locaties succesvol bijgewerkt",
|
"updateSuccess": "Verdachte locaties succesvol bijgewerkt",
|
||||||
|
|||||||
@@ -111,8 +111,13 @@
|
|||||||
"advancedSettings": "Ustawienia Zaawansowane",
|
"advancedSettings": "Ustawienia Zaawansowane",
|
||||||
"advancedSettingsSubtitle": "Wydajność, alerty i ustawienia dostawców kafelków",
|
"advancedSettingsSubtitle": "Wydajność, alerty i ustawienia dostawców kafelków",
|
||||||
"proximityAlerts": "Alerty Bliskości",
|
"proximityAlerts": "Alerty Bliskości",
|
||||||
|
"keepScreenAwake": "Nie Wygaszaj Ekranu",
|
||||||
"networkStatusIndicator": "Wskaźnik Stanu Sieci"
|
"networkStatusIndicator": "Wskaźnik Stanu Sieci"
|
||||||
},
|
},
|
||||||
|
"keepScreenAwake": {
|
||||||
|
"title": "Nie wygaszaj ekranu podczas korzystania z aplikacji",
|
||||||
|
"subtitle": "Zapobiega automatycznemu wygaszaniu ekranu podczas korzystania z aplikacji"
|
||||||
|
},
|
||||||
"proximityAlerts": {
|
"proximityAlerts": {
|
||||||
"getNotified": "Otrzymuj powiadomienia przy zbliżaniu się do urządzeń nadzoru",
|
"getNotified": "Otrzymuj powiadomienia przy zbliżaniu się do urządzeń nadzoru",
|
||||||
"batteryUsage": "Używa dodatkowej baterii do ciągłego monitorowania lokalizacji",
|
"batteryUsage": "Używa dodatkowej baterii do ciągłego monitorowania lokalizacji",
|
||||||
@@ -361,6 +366,8 @@
|
|||||||
"fovHint": "FOV w stopniach (zostaw puste dla domyślnego)",
|
"fovHint": "FOV w stopniach (zostaw puste dla domyślnego)",
|
||||||
"fovSubtitle": "Pole widzenia kamery - używane dla szerokości stożka i formatu zgłaszania zasięgu",
|
"fovSubtitle": "Pole widzenia kamery - używane dla szerokości stożka i formatu zgłaszania zasięgu",
|
||||||
"fovInvalid": "FOV musi być między 1 a 360 stopniami",
|
"fovInvalid": "FOV musi być między 1 a 360 stopniami",
|
||||||
|
"fov360": "Pole Widzenia 360°",
|
||||||
|
"fov360Subtitle": "Kamera ma wielokierunkowe pole widzenia o zasięgu 360 stopni",
|
||||||
"submittable": "Możliwy do Zgłoszenia",
|
"submittable": "Możliwy do Zgłoszenia",
|
||||||
"submittableSubtitle": "Czy ten profil może być używany do zgłoszeń kamer",
|
"submittableSubtitle": "Czy ten profil może być używany do zgłoszeń kamer",
|
||||||
"osmTags": "Tagi OSM",
|
"osmTags": "Tagi OSM",
|
||||||
@@ -524,7 +531,7 @@
|
|||||||
"dataSourceDescription": "Dane pozwoleń użyteczności publicznej wskazujące potencjalne miejsca instalacji infrastruktury nadzoru",
|
"dataSourceDescription": "Dane pozwoleń użyteczności publicznej wskazujące potencjalne miejsca instalacji infrastruktury nadzoru",
|
||||||
"dataSourceCredit": "Zbieranie danych i hosting zapewnione przez alprwatch.org",
|
"dataSourceCredit": "Zbieranie danych i hosting zapewnione przez alprwatch.org",
|
||||||
"minimumDistance": "Minimalna Odległość od Rzeczywistych Węzłów",
|
"minimumDistance": "Minimalna Odległość od Rzeczywistych Węzłów",
|
||||||
"minimumDistanceSubtitle": "Ukryj podejrzane lokalizacje w promieniu {}m od istniejących urządzeń nadzoru",
|
"minimumDistanceSubtitle": "Ukryj podejrzane lokalizacje w promieniu {} od istniejących urządzeń nadzoru",
|
||||||
"updating": "Aktualizowanie Podejrzanych Lokalizacji",
|
"updating": "Aktualizowanie Podejrzanych Lokalizacji",
|
||||||
"downloadingAndProcessing": "Pobieranie i przetwarzanie danych...",
|
"downloadingAndProcessing": "Pobieranie i przetwarzanie danych...",
|
||||||
"updateSuccess": "Podejrzane lokalizacje zaktualizowane pomyślnie",
|
"updateSuccess": "Podejrzane lokalizacje zaktualizowane pomyślnie",
|
||||||
|
|||||||
@@ -111,8 +111,13 @@
|
|||||||
"advancedSettings": "Configurações Avançadas",
|
"advancedSettings": "Configurações Avançadas",
|
||||||
"advancedSettingsSubtitle": "Configurações de desempenho, alertas e provedores de mapas",
|
"advancedSettingsSubtitle": "Configurações de desempenho, alertas e provedores de mapas",
|
||||||
"proximityAlerts": "Alertas de Proximidade",
|
"proximityAlerts": "Alertas de Proximidade",
|
||||||
|
"keepScreenAwake": "Manter Ecrã Ativa",
|
||||||
"networkStatusIndicator": "Indicador de Status de Rede"
|
"networkStatusIndicator": "Indicador de Status de Rede"
|
||||||
},
|
},
|
||||||
|
"keepScreenAwake": {
|
||||||
|
"title": "Manter o ecrã ativo durante o uso do app",
|
||||||
|
"subtitle": "Impede que o ecrã se desligue automaticamente enquanto a app estiver em uso"
|
||||||
|
},
|
||||||
"proximityAlerts": {
|
"proximityAlerts": {
|
||||||
"getNotified": "Receba notificações ao se aproximar de dispositivos de vigilância",
|
"getNotified": "Receba notificações ao se aproximar de dispositivos de vigilância",
|
||||||
"batteryUsage": "Usa bateria extra para monitoramento contínuo de localização",
|
"batteryUsage": "Usa bateria extra para monitoramento contínuo de localização",
|
||||||
@@ -361,6 +366,8 @@
|
|||||||
"fovHint": "Campo de visão em graus (deixar vazio para o padrão)",
|
"fovHint": "Campo de visão em graus (deixar vazio para o padrão)",
|
||||||
"fovSubtitle": "Campo de visão da câmera - usado para largura do cone e formato de envio por intervalo",
|
"fovSubtitle": "Campo de visão da câmera - usado para largura do cone e formato de envio por intervalo",
|
||||||
"fovInvalid": "Campo de visão deve estar entre 1 e 360 graus",
|
"fovInvalid": "Campo de visão deve estar entre 1 e 360 graus",
|
||||||
|
"fov360": "Campo de Visão 360°",
|
||||||
|
"fov360Subtitle": "A câmera tem um campo de visão omnidirecional de 360 graus",
|
||||||
"submittable": "Enviável",
|
"submittable": "Enviável",
|
||||||
"submittableSubtitle": "Se este perfil pode ser usado para envios de câmeras",
|
"submittableSubtitle": "Se este perfil pode ser usado para envios de câmeras",
|
||||||
"osmTags": "Tags OSM",
|
"osmTags": "Tags OSM",
|
||||||
@@ -524,7 +531,7 @@
|
|||||||
"dataSourceDescription": "Dados de licenças de serviços públicos indicando possíveis locais de instalação de infraestrutura de vigilância",
|
"dataSourceDescription": "Dados de licenças de serviços públicos indicando possíveis locais de instalação de infraestrutura de vigilância",
|
||||||
"dataSourceCredit": "Coleta e hospedagem de dados fornecidas por alprwatch.org",
|
"dataSourceCredit": "Coleta e hospedagem de dados fornecidas por alprwatch.org",
|
||||||
"minimumDistance": "Distância Mínima de Nós Reais",
|
"minimumDistance": "Distância Mínima de Nós Reais",
|
||||||
"minimumDistanceSubtitle": "Ocultar localizações suspeitas dentro de {}m de dispositivos de vigilância existentes",
|
"minimumDistanceSubtitle": "Ocultar localizações suspeitas dentro de {} de dispositivos de vigilância existentes",
|
||||||
"updating": "Atualizando Localizações Suspeitas",
|
"updating": "Atualizando Localizações Suspeitas",
|
||||||
"downloadingAndProcessing": "Baixando e processando dados...",
|
"downloadingAndProcessing": "Baixando e processando dados...",
|
||||||
"updateSuccess": "Localizações suspeitas atualizadas com sucesso",
|
"updateSuccess": "Localizações suspeitas atualizadas com sucesso",
|
||||||
|
|||||||
@@ -111,8 +111,13 @@
|
|||||||
"advancedSettings": "Gelişmiş Ayarlar",
|
"advancedSettings": "Gelişmiş Ayarlar",
|
||||||
"advancedSettingsSubtitle": "Performans, uyarılar ve döşeme sağlayıcı ayarları",
|
"advancedSettingsSubtitle": "Performans, uyarılar ve döşeme sağlayıcı ayarları",
|
||||||
"proximityAlerts": "Yakınlık Uyarıları",
|
"proximityAlerts": "Yakınlık Uyarıları",
|
||||||
|
"keepScreenAwake": "Ekranı Açık Tut",
|
||||||
"networkStatusIndicator": "Ağ Durumu Göstergesi"
|
"networkStatusIndicator": "Ağ Durumu Göstergesi"
|
||||||
},
|
},
|
||||||
|
"keepScreenAwake": {
|
||||||
|
"title": "Uygulamayı kullanırken ekranı açık tut",
|
||||||
|
"subtitle": "Uygulamayı kullanırken ekranın otomatik olarak kapanmasını engeller"
|
||||||
|
},
|
||||||
"proximityAlerts": {
|
"proximityAlerts": {
|
||||||
"getNotified": "Gözetleme cihazlarına yaklaşırken bildirim al",
|
"getNotified": "Gözetleme cihazlarına yaklaşırken bildirim al",
|
||||||
"batteryUsage": "Sürekli konum izleme için ekstra batarya kullanır",
|
"batteryUsage": "Sürekli konum izleme için ekstra batarya kullanır",
|
||||||
@@ -361,6 +366,8 @@
|
|||||||
"fovHint": "FOV derece cinsinden (varsayılan için boş bırakın)",
|
"fovHint": "FOV derece cinsinden (varsayılan için boş bırakın)",
|
||||||
"fovSubtitle": "Kamera görüş alanı - koni genişliği ve aralık gönderim formatı için kullanılır",
|
"fovSubtitle": "Kamera görüş alanı - koni genişliği ve aralık gönderim formatı için kullanılır",
|
||||||
"fovInvalid": "FOV 1 ile 360 derece arasında olmalıdır",
|
"fovInvalid": "FOV 1 ile 360 derece arasında olmalıdır",
|
||||||
|
"fov360": "360° Görüş Alanı",
|
||||||
|
"fov360Subtitle": "Kamera 360 derece çok yönlü görüş alanına sahiptir",
|
||||||
"submittable": "Gönderilebilir",
|
"submittable": "Gönderilebilir",
|
||||||
"submittableSubtitle": "Bu profilin kamera gönderimlerinde kullanılıp kullanılamayacağı",
|
"submittableSubtitle": "Bu profilin kamera gönderimlerinde kullanılıp kullanılamayacağı",
|
||||||
"osmTags": "OSM Etiketleri",
|
"osmTags": "OSM Etiketleri",
|
||||||
@@ -524,7 +531,7 @@
|
|||||||
"dataSourceDescription": "Potansiyel gözetleme altyapısı kurulum sitelerini gösteren altyapı izin verileri",
|
"dataSourceDescription": "Potansiyel gözetleme altyapısı kurulum sitelerini gösteren altyapı izin verileri",
|
||||||
"dataSourceCredit": "Veri toplama ve barındırma alprwatch.org tarafından sağlanır",
|
"dataSourceCredit": "Veri toplama ve barındırma alprwatch.org tarafından sağlanır",
|
||||||
"minimumDistance": "Gerçek Düğümlerden Minimum Mesafe",
|
"minimumDistance": "Gerçek Düğümlerden Minimum Mesafe",
|
||||||
"minimumDistanceSubtitle": "Mevcut gözetleme cihazlarının {}m yakınındaki şüpheli konumları gizle",
|
"minimumDistanceSubtitle": "Mevcut gözetleme cihazlarının {} yakınındaki şüpheli konumları gizle",
|
||||||
"updating": "Şüpheli Konumlar Güncelleniyor",
|
"updating": "Şüpheli Konumlar Güncelleniyor",
|
||||||
"downloadingAndProcessing": "Veri indiriliyor ve işleniyor...",
|
"downloadingAndProcessing": "Veri indiriliyor ve işleniyor...",
|
||||||
"updateSuccess": "Şüpheli konumlar başarıyla güncellendi",
|
"updateSuccess": "Şüpheli konumlar başarıyla güncellendi",
|
||||||
|
|||||||
@@ -111,8 +111,13 @@
|
|||||||
"advancedSettings": "Розширені Налаштування",
|
"advancedSettings": "Розширені Налаштування",
|
||||||
"advancedSettingsSubtitle": "Продуктивність, сповіщення та налаштування постачальників плиток",
|
"advancedSettingsSubtitle": "Продуктивність, сповіщення та налаштування постачальників плиток",
|
||||||
"proximityAlerts": "Сповіщення Про Близькість",
|
"proximityAlerts": "Сповіщення Про Близькість",
|
||||||
|
"keepScreenAwake": "Тримати екран активним",
|
||||||
"networkStatusIndicator": "Індикатор Стану Мережі"
|
"networkStatusIndicator": "Індикатор Стану Мережі"
|
||||||
},
|
},
|
||||||
|
"keepScreenAwake": {
|
||||||
|
"title": "Тримати екран активним під час використання програми",
|
||||||
|
"subtitle": "Запобігає автоматичному вимкненню екрана під час використання програми"
|
||||||
|
},
|
||||||
"proximityAlerts": {
|
"proximityAlerts": {
|
||||||
"getNotified": "Отримувати сповіщення при наближенні до пристроїв спостереження",
|
"getNotified": "Отримувати сповіщення при наближенні до пристроїв спостереження",
|
||||||
"batteryUsage": "Використовує додаткову батарею для безперервного моніторингу місцезнаходження",
|
"batteryUsage": "Використовує додаткову батарею для безперервного моніторингу місцезнаходження",
|
||||||
@@ -361,6 +366,8 @@
|
|||||||
"fovHint": "FOV в градусах (залиште порожнім для значення за замовчуванням)",
|
"fovHint": "FOV в градусах (залиште порожнім для значення за замовчуванням)",
|
||||||
"fovSubtitle": "Поле зору камери - використовується для ширини конуса та формату подання діапазону",
|
"fovSubtitle": "Поле зору камери - використовується для ширини конуса та формату подання діапазону",
|
||||||
"fovInvalid": "FOV повинно бути між 1 і 360 градусами",
|
"fovInvalid": "FOV повинно бути між 1 і 360 градусами",
|
||||||
|
"fov360": "Поле Зору 360°",
|
||||||
|
"fov360Subtitle": "Камера має всенаправлене поле зору на 360 градусів",
|
||||||
"submittable": "Можна Подавати",
|
"submittable": "Можна Подавати",
|
||||||
"submittableSubtitle": "Чи можна використовувати цей профіль для подань камер",
|
"submittableSubtitle": "Чи можна використовувати цей профіль для подань камер",
|
||||||
"osmTags": "OSM Теги",
|
"osmTags": "OSM Теги",
|
||||||
@@ -524,7 +531,7 @@
|
|||||||
"dataSourceDescription": "Дані дозволів комунальних служб, що вказують на потенційні сайти встановлення інфраструктури спостереження",
|
"dataSourceDescription": "Дані дозволів комунальних служб, що вказують на потенційні сайти встановлення інфраструктури спостереження",
|
||||||
"dataSourceCredit": "Збір даних та хостинг надається alprwatch.org",
|
"dataSourceCredit": "Збір даних та хостинг надається alprwatch.org",
|
||||||
"minimumDistance": "Мінімальна Відстань від Реальних Вузлів",
|
"minimumDistance": "Мінімальна Відстань від Реальних Вузлів",
|
||||||
"minimumDistanceSubtitle": "Приховати підозрілі локації в межах {}м від існуючих пристроїв спостереження",
|
"minimumDistanceSubtitle": "Приховати підозрілі локації в межах {} від існуючих пристроїв спостереження",
|
||||||
"updating": "Оновлення Підозрілих Локацій",
|
"updating": "Оновлення Підозрілих Локацій",
|
||||||
"downloadingAndProcessing": "Завантаження та обробка даних...",
|
"downloadingAndProcessing": "Завантаження та обробка даних...",
|
||||||
"updateSuccess": "Підозрілі локації успішно оновлено",
|
"updateSuccess": "Підозрілі локації успішно оновлено",
|
||||||
|
|||||||
@@ -111,8 +111,13 @@
|
|||||||
"advancedSettings": "高级设置",
|
"advancedSettings": "高级设置",
|
||||||
"advancedSettingsSubtitle": "性能、警报和地图提供商设置",
|
"advancedSettingsSubtitle": "性能、警报和地图提供商设置",
|
||||||
"proximityAlerts": "邻近警报",
|
"proximityAlerts": "邻近警报",
|
||||||
|
"keepScreenAwake": "保持屏幕常亮",
|
||||||
"networkStatusIndicator": "网络状态指示器"
|
"networkStatusIndicator": "网络状态指示器"
|
||||||
},
|
},
|
||||||
|
"keepScreenAwake": {
|
||||||
|
"title": "使用应用时保持屏幕常亮",
|
||||||
|
"subtitle": "防止在使用应用期间屏幕自动熄灭"
|
||||||
|
},
|
||||||
"proximityAlerts": {
|
"proximityAlerts": {
|
||||||
"getNotified": "接近监控设备时接收通知",
|
"getNotified": "接近监控设备时接收通知",
|
||||||
"batteryUsage": "使用额外电量进行连续位置监控",
|
"batteryUsage": "使用额外电量进行连续位置监控",
|
||||||
@@ -361,6 +366,8 @@
|
|||||||
"fovHint": "视场角度数(留空使用默认值)",
|
"fovHint": "视场角度数(留空使用默认值)",
|
||||||
"fovSubtitle": "摄像头视场角 - 用于锥体宽度和范围提交格式",
|
"fovSubtitle": "摄像头视场角 - 用于锥体宽度和范围提交格式",
|
||||||
"fovInvalid": "视场角必须在1到360度之间",
|
"fovInvalid": "视场角必须在1到360度之间",
|
||||||
|
"fov360": "360°视场角",
|
||||||
|
"fov360Subtitle": "摄像头具有360度全方向视场角",
|
||||||
"submittable": "可提交",
|
"submittable": "可提交",
|
||||||
"submittableSubtitle": "此配置文件是否可用于摄像头提交",
|
"submittableSubtitle": "此配置文件是否可用于摄像头提交",
|
||||||
"osmTags": "OSM 标签",
|
"osmTags": "OSM 标签",
|
||||||
@@ -524,7 +531,7 @@
|
|||||||
"dataSourceDescription": "公用事业许可数据,表明潜在的监控基础设施安装站点",
|
"dataSourceDescription": "公用事业许可数据,表明潜在的监控基础设施安装站点",
|
||||||
"dataSourceCredit": "数据收集和托管由 alprwatch.org 提供",
|
"dataSourceCredit": "数据收集和托管由 alprwatch.org 提供",
|
||||||
"minimumDistance": "与真实节点的最小距离",
|
"minimumDistance": "与真实节点的最小距离",
|
||||||
"minimumDistanceSubtitle": "隐藏现有监控设备{}米范围内的疑似位置",
|
"minimumDistanceSubtitle": "隐藏现有监控设备{}范围内的疑似位置",
|
||||||
"updating": "正在更新疑似位置",
|
"updating": "正在更新疑似位置",
|
||||||
"downloadingAndProcessing": "正在下载和处理数据...",
|
"downloadingAndProcessing": "正在下载和处理数据...",
|
||||||
"updateSuccess": "疑似位置更新成功",
|
"updateSuccess": "疑似位置更新成功",
|
||||||
|
|||||||
@@ -92,6 +92,15 @@ class DeFlockApp extends StatelessWidget {
|
|||||||
'/settings/release-notes': (context) => const ReleaseNotesScreen(),
|
'/settings/release-notes': (context) => const ReleaseNotesScreen(),
|
||||||
},
|
},
|
||||||
initialRoute: '/',
|
initialRoute: '/',
|
||||||
|
onUnknownRoute: (settings) {
|
||||||
|
// Handle deep link routes that Flutter tries to process before app_links
|
||||||
|
// This catches routes like "/?id=13939398601" from "deflockapp://node?id=13939398601"
|
||||||
|
debugPrint('[Navigation] Unknown route intercepted: ${settings.name} (likely deep link, will be handled by DeepLinkService)');
|
||||||
|
return MaterialPageRoute(
|
||||||
|
settings: settings,
|
||||||
|
builder: (_) => const HomeScreen(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'app_state.dart';
|
|||||||
import 'services/profile_service.dart';
|
import 'services/profile_service.dart';
|
||||||
import 'services/suspected_location_cache.dart';
|
import 'services/suspected_location_cache.dart';
|
||||||
import 'widgets/nuclear_reset_dialog.dart';
|
import 'widgets/nuclear_reset_dialog.dart';
|
||||||
|
import 'dev_config.dart';
|
||||||
|
|
||||||
/// One-time migrations that run when users upgrade to specific versions.
|
/// One-time migrations that run when users upgrade to specific versions.
|
||||||
/// Each migration function is named after the version where it should run.
|
/// Each migration function is named after the version where it should run.
|
||||||
@@ -142,6 +143,52 @@ class OneTimeMigrations {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Clear non-360 FOV values from all profiles (v2.10.0)
|
||||||
|
static Future<void> migrate_2_10_0(AppState appState) async {
|
||||||
|
try {
|
||||||
|
// Only perform this migration if non-360 FOVs are disabled
|
||||||
|
if (kEnableNon360FOVs) {
|
||||||
|
debugPrint('[Migration] 2.10.0: Non-360 FOVs enabled, skipping FOV cleanup');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load all profiles from storage
|
||||||
|
final profiles = await ProfileService().load();
|
||||||
|
bool anyProfileChanged = false;
|
||||||
|
int profilesCleared = 0;
|
||||||
|
|
||||||
|
// Clear non-360 FOV values from all profiles
|
||||||
|
final updatedProfiles = profiles.map((profile) {
|
||||||
|
if (profile.fov != null) {
|
||||||
|
// Use approximation to handle floating point precision issues
|
||||||
|
final fovValue = profile.fov!;
|
||||||
|
final is360 = (fovValue - 360.0).abs() < 0.01; // Within 0.01 degrees of 360
|
||||||
|
|
||||||
|
if (!is360) {
|
||||||
|
debugPrint('[Migration] 2.10.0: Clearing FOV $fovValue from profile: ${profile.name}');
|
||||||
|
anyProfileChanged = true;
|
||||||
|
profilesCleared++;
|
||||||
|
return profile.copyWith(fov: null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return profile;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
// Save updated profiles back to storage if any changes were made
|
||||||
|
if (anyProfileChanged) {
|
||||||
|
await ProfileService().save(updatedProfiles);
|
||||||
|
await appState.reloadProfiles();
|
||||||
|
debugPrint('[Migration] 2.10.0: Cleared FOV from $profilesCleared profiles');
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint('[Migration] 2.10.0 completed: cleared non-360 FOV values from profiles');
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
debugPrint('[Migration] 2.10.0 ERROR: Failed to clear non-360 FOV values: $e');
|
||||||
|
debugPrint('[Migration] 2.10.0 ERROR: Stack trace: $stackTrace');
|
||||||
|
// Don't rethrow - this is non-critical, FOV restrictions will still apply going forward
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the migration function for a specific version
|
/// Get the migration function for a specific version
|
||||||
static Future<void> Function(AppState)? getMigrationForVersion(String version) {
|
static Future<void> Function(AppState)? getMigrationForVersion(String version) {
|
||||||
switch (version) {
|
switch (version) {
|
||||||
@@ -157,6 +204,8 @@ class OneTimeMigrations {
|
|||||||
return migrate_2_1_0;
|
return migrate_2_1_0;
|
||||||
case '2.7.3':
|
case '2.7.3':
|
||||||
return migrate_2_7_3;
|
return migrate_2_7_3;
|
||||||
|
case '2.10.0':
|
||||||
|
return migrate_2_10_0;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
import '../dev_config.dart';
|
||||||
import '../services/localization_service.dart';
|
import '../services/localization_service.dart';
|
||||||
|
import '../services/nuclear_reset_service.dart';
|
||||||
import '../widgets/welcome_dialog.dart';
|
import '../widgets/welcome_dialog.dart';
|
||||||
import '../widgets/submission_guide_dialog.dart';
|
import '../widgets/submission_guide_dialog.dart';
|
||||||
|
|
||||||
@@ -83,6 +86,13 @@ class AboutScreen extends StatelessWidget {
|
|||||||
_buildDialogButtons(context),
|
_buildDialogButtons(context),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
_buildHelpLinks(context),
|
_buildHelpLinks(context),
|
||||||
|
|
||||||
|
// Dev-only nuclear reset button at very bottom
|
||||||
|
if (kDebugMode || kEnableDevelopmentModes) ...[
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
_buildDevNuclearResetButton(context),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -203,5 +213,231 @@ class AboutScreen extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Dev-only nuclear reset button (only visible in debug mode)
|
||||||
|
Widget _buildDevNuclearResetButton(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
color: Theme.of(context).colorScheme.errorContainer.withValues(alpha: 0.1),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.developer_mode,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'Developer Tools',
|
||||||
|
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
'These tools are only available in debug mode for development and troubleshooting.',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: OutlinedButton.icon(
|
||||||
|
onPressed: () => _showNuclearResetConfirmation(context),
|
||||||
|
icon: const Icon(Icons.delete_forever, color: Colors.red),
|
||||||
|
label: const Text(
|
||||||
|
'Nuclear Reset (Clear All Data)',
|
||||||
|
style: TextStyle(color: Colors.red),
|
||||||
|
),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
side: const BorderSide(color: Colors.red),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show confirmation dialog for nuclear reset
|
||||||
|
Future<void> _showNuclearResetConfirmation(BuildContext context) async {
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.warning, color: Colors.red),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text('Nuclear Reset'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
content: const Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'This will completely clear ALL app data:',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
SizedBox(height: 12),
|
||||||
|
Text('• All settings and preferences'),
|
||||||
|
Text('• OAuth login credentials'),
|
||||||
|
Text('• Custom profiles and operators'),
|
||||||
|
Text('• Upload queue and cached data'),
|
||||||
|
Text('• Downloaded offline areas'),
|
||||||
|
Text('• Everything else'),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'The app will behave exactly like a fresh install after this operation.',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
'This action cannot be undone.',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||||
|
child: const Text('Nuclear Reset'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirmed == true && context.mounted) {
|
||||||
|
await _performNuclearReset(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Perform the nuclear reset operation
|
||||||
|
Future<void> _performNuclearReset(BuildContext context) async {
|
||||||
|
// Show progress dialog
|
||||||
|
if (!context.mounted) return;
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (context) => const AlertDialog(
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
CircularProgressIndicator(),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text('Clearing all app data...'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Perform the nuclear reset
|
||||||
|
await NuclearResetService.clearEverything();
|
||||||
|
|
||||||
|
if (!context.mounted) return;
|
||||||
|
|
||||||
|
// Close progress dialog
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
|
// Show completion dialog
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.check_circle, color: Colors.green),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text('Reset Complete'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
content: const Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'All app data has been cleared successfully.',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
'Please close and restart the app to continue with a fresh state.',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('OK'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
|
||||||
|
// Close progress dialog if it's still open
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
|
// Show error dialog
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.error, color: Colors.red),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text('Reset Failed'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'An error occurred during the nuclear reset:',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
e.toString(),
|
||||||
|
style: const TextStyle(fontFamily: 'monospace'),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const Text(
|
||||||
|
'Some data may have been partially cleared. You may want to manually clear app data through device settings.',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('OK'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ import 'settings/sections/max_nodes_section.dart';
|
|||||||
import 'settings/sections/proximity_alerts_section.dart';
|
import 'settings/sections/proximity_alerts_section.dart';
|
||||||
import 'settings/sections/suspected_locations_section.dart';
|
import 'settings/sections/suspected_locations_section.dart';
|
||||||
import 'settings/sections/tile_provider_section.dart';
|
import 'settings/sections/tile_provider_section.dart';
|
||||||
|
import 'settings/sections/keep_screen_awake_section.dart';
|
||||||
import '../services/localization_service.dart';
|
import '../services/localization_service.dart';
|
||||||
|
|
||||||
class AdvancedSettingsScreen extends StatelessWidget {
|
class AdvancedSettingsScreen extends StatelessWidget {
|
||||||
@@ -32,6 +33,8 @@ class AdvancedSettingsScreen extends StatelessWidget {
|
|||||||
Divider(),
|
Divider(),
|
||||||
SuspectedLocationsSection(),
|
SuspectedLocationsSection(),
|
||||||
Divider(),
|
Divider(),
|
||||||
|
KeepScreenAwakeSection(),
|
||||||
|
Divider(),
|
||||||
// NetworkStatusSection(), // Commented out - network status indicator now defaults to enabled
|
// NetworkStatusSection(), // Commented out - network status indicator now defaults to enabled
|
||||||
// Divider(),
|
// Divider(),
|
||||||
TileProviderSection(),
|
TileProviderSection(),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_map_animations/flutter_map_animations.dart';
|
import 'package:flutter_map_animations/flutter_map_animations.dart';
|
||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../../app_state.dart';
|
import '../../app_state.dart';
|
||||||
@@ -125,4 +126,50 @@ class MapInteractionHandler {
|
|||||||
// Clear suspected location selection
|
// Clear suspected location selection
|
||||||
appState.clearSuspectedLocationSelection();
|
appState.clearSuspectedLocationSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handle long press on empty map area (add node here)
|
||||||
|
void handleMapLongPress({
|
||||||
|
required BuildContext context,
|
||||||
|
required LatLng tapLocation,
|
||||||
|
required AnimatedMapController mapController,
|
||||||
|
required VoidCallback onAddNode,
|
||||||
|
}) {
|
||||||
|
final appState = context.read<AppState>();
|
||||||
|
|
||||||
|
debugPrint('[MapInteractionHandler] Long press at: ${tapLocation.latitude}, ${tapLocation.longitude}');
|
||||||
|
|
||||||
|
// Check if we should handle this long press
|
||||||
|
// Don't handle if any sheet is open or if in search mode
|
||||||
|
if (appState.isInSearchMode || appState.showingOverview) {
|
||||||
|
debugPrint('[MapInteractionHandler] Long press ignored - in search/navigation mode');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't handle if any session is active (add/edit sheets are open)
|
||||||
|
if (appState.session != null || appState.editSession != null) {
|
||||||
|
debugPrint('[MapInteractionHandler] Long press ignored - node sheet already open');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable follow-me when user long presses
|
||||||
|
appState.setFollowMeMode(FollowMeMode.off);
|
||||||
|
|
||||||
|
// First, center the map on the tap location
|
||||||
|
try {
|
||||||
|
mapController.animateTo(
|
||||||
|
dest: tapLocation,
|
||||||
|
zoom: mapController.mapController.camera.zoom,
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[MapInteractionHandler] Could not center map on long press location: $e');
|
||||||
|
}
|
||||||
|
|
||||||
|
// After a short delay to let the map center, open the add node sheet
|
||||||
|
// This matches the existing pattern used for node taps
|
||||||
|
Future.delayed(const Duration(milliseconds: 350), () {
|
||||||
|
onAddNode();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_map_animations/flutter_map_animations.dart';
|
import 'package:flutter_map_animations/flutter_map_animations.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../app_state.dart';
|
import '../app_state.dart';
|
||||||
@@ -19,6 +20,7 @@ import '../models/osm_node.dart';
|
|||||||
import '../models/suspected_location.dart';
|
import '../models/suspected_location.dart';
|
||||||
import '../models/search_result.dart';
|
import '../models/search_result.dart';
|
||||||
import '../services/changelog_service.dart';
|
import '../services/changelog_service.dart';
|
||||||
|
import '../services/deep_link_service.dart';
|
||||||
import 'coordinators/sheet_coordinator.dart';
|
import 'coordinators/sheet_coordinator.dart';
|
||||||
import 'coordinators/navigation_coordinator.dart';
|
import 'coordinators/navigation_coordinator.dart';
|
||||||
import 'coordinators/map_interaction_handler.dart';
|
import 'coordinators/map_interaction_handler.dart';
|
||||||
@@ -57,10 +59,12 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
|||||||
_sheetCoordinator = SheetCoordinator();
|
_sheetCoordinator = SheetCoordinator();
|
||||||
_navigationCoordinator = NavigationCoordinator();
|
_navigationCoordinator = NavigationCoordinator();
|
||||||
_mapInteractionHandler = MapInteractionHandler();
|
_mapInteractionHandler = MapInteractionHandler();
|
||||||
|
DeepLinkService().onNodeDeepLink = _handleNodeDeepLink;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
DeepLinkService().onNodeDeepLink = null;
|
||||||
_mapController.dispose();
|
_mapController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@@ -286,6 +290,40 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onMapLongPress(LatLng location) {
|
||||||
|
// Don't handle long press if tag sheet is open
|
||||||
|
if (_sheetCoordinator.tagSheetHeight > 0) {
|
||||||
|
debugPrint('[HomeScreen] Long press ignored - tag sheet is open');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_mapInteractionHandler.handleMapLongPress(
|
||||||
|
context: context,
|
||||||
|
tapLocation: location,
|
||||||
|
mapController: _mapController,
|
||||||
|
onAddNode: _openAddNodeSheet,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleNodeDeepLink(OsmNode node) {
|
||||||
|
try {
|
||||||
|
_mapController.animateTo(
|
||||||
|
dest: node.coord,
|
||||||
|
zoom: 16.0,
|
||||||
|
duration: const Duration(milliseconds: 600),
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[HomeScreen] Could not animate to deep link node: $e');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future.delayed(const Duration(milliseconds: 700), () {
|
||||||
|
if (mounted) {
|
||||||
|
openNodeTagSheet(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void openNodeTagSheet(OsmNode node) {
|
void openNodeTagSheet(OsmNode node) {
|
||||||
// Handle the map interaction (centering and follow-me disable)
|
// Handle the map interaction (centering and follow-me disable)
|
||||||
_mapInteractionHandler.handleNodeTap(
|
_mapInteractionHandler.handleNodeTap(
|
||||||
@@ -498,6 +536,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
|||||||
// Re-render when location status changes (for follow-me button state)
|
// Re-render when location status changes (for follow-me button state)
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
|
onMapLongPress: _onMapLongPress,
|
||||||
onUserGesture: () {
|
onUserGesture: () {
|
||||||
// Only clear selected node if tag sheet is not open
|
// Only clear selected node if tag sheet is not open
|
||||||
// This prevents nodes from losing their grey-out when map is moved while viewing tags
|
// This prevents nodes from losing their grey-out when map is moved while viewing tags
|
||||||
|
|||||||
@@ -155,8 +155,13 @@ class _OperatorProfileEditorState extends State<OperatorProfileEditor> {
|
|||||||
|
|
||||||
final tagMap = <String, String>{};
|
final tagMap = <String, String>{};
|
||||||
for (final e in _tags) {
|
for (final e in _tags) {
|
||||||
if (e.key.trim().isEmpty) continue; // Skip only if key is empty
|
final key = e.key.trim();
|
||||||
tagMap[e.key.trim()] = e.value.trim(); // Allow empty values for refinement
|
final value = e.value.trim();
|
||||||
|
|
||||||
|
// Skip if key is empty OR value is empty (no empty values for operator profiles)
|
||||||
|
if (key.isEmpty || value.isEmpty) continue;
|
||||||
|
|
||||||
|
tagMap[key] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
final newProfile = widget.profile.copyWith(
|
final newProfile = widget.profile.copyWith(
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import '../models/node_profile.dart';
|
|||||||
import '../app_state.dart';
|
import '../app_state.dart';
|
||||||
import '../services/localization_service.dart';
|
import '../services/localization_service.dart';
|
||||||
import '../widgets/nsi_tag_value_field.dart';
|
import '../widgets/nsi_tag_value_field.dart';
|
||||||
|
import '../dev_config.dart';
|
||||||
|
|
||||||
class ProfileEditor extends StatefulWidget {
|
class ProfileEditor extends StatefulWidget {
|
||||||
const ProfileEditor({super.key, required this.profile});
|
const ProfileEditor({super.key, required this.profile});
|
||||||
@@ -22,6 +23,7 @@ class _ProfileEditorState extends State<ProfileEditor> {
|
|||||||
late bool _requiresDirection;
|
late bool _requiresDirection;
|
||||||
late bool _submittable;
|
late bool _submittable;
|
||||||
late TextEditingController _fovCtrl;
|
late TextEditingController _fovCtrl;
|
||||||
|
late bool _is360Fov;
|
||||||
|
|
||||||
static const _defaultTags = [
|
static const _defaultTags = [
|
||||||
MapEntry('man_made', 'surveillance'),
|
MapEntry('man_made', 'surveillance'),
|
||||||
@@ -41,6 +43,7 @@ class _ProfileEditorState extends State<ProfileEditor> {
|
|||||||
_requiresDirection = widget.profile.requiresDirection;
|
_requiresDirection = widget.profile.requiresDirection;
|
||||||
_submittable = widget.profile.submittable;
|
_submittable = widget.profile.submittable;
|
||||||
_fovCtrl = TextEditingController(text: widget.profile.fov?.toString() ?? '');
|
_fovCtrl = TextEditingController(text: widget.profile.fov?.toString() ?? '');
|
||||||
|
_is360Fov = widget.profile.fov == 360.0;
|
||||||
|
|
||||||
if (widget.profile.tags.isEmpty) {
|
if (widget.profile.tags.isEmpty) {
|
||||||
// New profile → start with sensible defaults
|
// New profile → start with sensible defaults
|
||||||
@@ -94,28 +97,68 @@ class _ProfileEditorState extends State<ProfileEditor> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
if (widget.profile.editable) ...[
|
if (widget.profile.editable) ...[
|
||||||
CheckboxListTile(
|
// Direction and FOV configuration - show different UI based on dev config
|
||||||
title: Text(locService.t('profileEditor.requiresDirection')),
|
if (kEnableNon360FOVs) ...[
|
||||||
subtitle: Text(locService.t('profileEditor.requiresDirectionSubtitle')),
|
// Old UI: direction required + optional custom FOV text field (development mode)
|
||||||
value: _requiresDirection,
|
CheckboxListTile(
|
||||||
onChanged: (value) => setState(() => _requiresDirection = value ?? true),
|
title: Text(locService.t('profileEditor.requiresDirection')),
|
||||||
controlAffinity: ListTileControlAffinity.leading,
|
subtitle: Text(locService.t('profileEditor.requiresDirectionSubtitle')),
|
||||||
),
|
value: _requiresDirection,
|
||||||
if (_requiresDirection) Padding(
|
onChanged: (value) => setState(() => _requiresDirection = value ?? true),
|
||||||
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 8),
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
child: TextField(
|
|
||||||
controller: _fovCtrl,
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: locService.t('profileEditor.fov'),
|
|
||||||
hintText: locService.t('profileEditor.fovHint'),
|
|
||||||
helperText: locService.t('profileEditor.fovSubtitle'),
|
|
||||||
errorText: _validateFov(),
|
|
||||||
suffixText: '°',
|
|
||||||
),
|
|
||||||
onChanged: (value) => setState(() {}), // Trigger validation
|
|
||||||
),
|
),
|
||||||
),
|
if (_requiresDirection) Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 8),
|
||||||
|
child: TextField(
|
||||||
|
controller: _fovCtrl,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: locService.t('profileEditor.fov'),
|
||||||
|
hintText: locService.t('profileEditor.fovHint'),
|
||||||
|
helperText: locService.t('profileEditor.fovSubtitle'),
|
||||||
|
errorText: _validateFov(),
|
||||||
|
suffixText: '°',
|
||||||
|
),
|
||||||
|
onChanged: (value) => setState(() {}), // Trigger validation
|
||||||
|
),
|
||||||
|
),
|
||||||
|
] else ...[
|
||||||
|
// New UI: mutually exclusive direction vs 360° FOV checkboxes (production mode)
|
||||||
|
CheckboxListTile(
|
||||||
|
title: Text(locService.t('profileEditor.requiresDirection')),
|
||||||
|
subtitle: Text(locService.t('profileEditor.requiresDirectionSubtitle')),
|
||||||
|
value: _requiresDirection,
|
||||||
|
onChanged: widget.profile.editable
|
||||||
|
? (value) {
|
||||||
|
setState(() {
|
||||||
|
_requiresDirection = value ?? false;
|
||||||
|
// Make mutually exclusive with 360° FOV
|
||||||
|
if (_requiresDirection) {
|
||||||
|
_is360Fov = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
|
),
|
||||||
|
CheckboxListTile(
|
||||||
|
title: Text(locService.t('profileEditor.fov360')),
|
||||||
|
subtitle: Text(locService.t('profileEditor.fov360Subtitle')),
|
||||||
|
value: _is360Fov,
|
||||||
|
onChanged: widget.profile.editable
|
||||||
|
? (value) {
|
||||||
|
setState(() {
|
||||||
|
_is360Fov = value ?? false;
|
||||||
|
// Make mutually exclusive with direction requirement
|
||||||
|
if (_is360Fov) {
|
||||||
|
_requiresDirection = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
|
),
|
||||||
|
],
|
||||||
CheckboxListTile(
|
CheckboxListTile(
|
||||||
title: Text(locService.t('profileEditor.submittable')),
|
title: Text(locService.t('profileEditor.submittable')),
|
||||||
subtitle: Text(locService.t('profileEditor.submittableSubtitle')),
|
subtitle: Text(locService.t('profileEditor.submittableSubtitle')),
|
||||||
@@ -199,6 +242,9 @@ class _ProfileEditorState extends State<ProfileEditor> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String? _validateFov() {
|
String? _validateFov() {
|
||||||
|
// Only validate when using text field mode
|
||||||
|
if (!kEnableNon360FOVs) return null;
|
||||||
|
|
||||||
final text = _fovCtrl.text.trim();
|
final text = _fovCtrl.text.trim();
|
||||||
if (text.isEmpty) return null; // Optional field
|
if (text.isEmpty) return null; // Optional field
|
||||||
|
|
||||||
@@ -219,14 +265,21 @@ class _ProfileEditorState extends State<ProfileEditor> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate FOV if provided
|
// Validate FOV if using text field mode
|
||||||
if (_validateFov() != null) {
|
if (kEnableNon360FOVs && _validateFov() != null) {
|
||||||
return; // Don't save if FOV validation fails
|
return; // Don't save if FOV validation fails
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse FOV
|
// Parse FOV based on dev config mode
|
||||||
final fovText = _fovCtrl.text.trim();
|
double? fov;
|
||||||
final fov = fovText.isEmpty ? null : double.tryParse(fovText);
|
if (kEnableNon360FOVs) {
|
||||||
|
// Old mode: parse from text field
|
||||||
|
final fovText = _fovCtrl.text.trim();
|
||||||
|
fov = fovText.isEmpty ? null : double.tryParse(fovText);
|
||||||
|
} else {
|
||||||
|
// New mode: use checkbox state
|
||||||
|
fov = _is360Fov ? 360.0 : null;
|
||||||
|
}
|
||||||
|
|
||||||
final tagMap = <String, String>{};
|
final tagMap = <String, String>{};
|
||||||
for (final e in _tags) {
|
for (final e in _tags) {
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import '../../../app_state.dart';
|
||||||
|
import '../../../services/localization_service.dart';
|
||||||
|
|
||||||
|
class KeepScreenAwakeSection extends StatelessWidget {
|
||||||
|
const KeepScreenAwakeSection({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// Standard Pattern: Use AnimatedBuilder to respond to language/localization changes
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: LocalizationService.instance,
|
||||||
|
builder: (context, child) {
|
||||||
|
final locService = LocalizationService.instance;
|
||||||
|
final appState = context.watch<AppState>();
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Bold Section Header (matches Max Nodes, Proximity, etc.)
|
||||||
|
Text(
|
||||||
|
locService.t('settings.keepScreenAwake'),
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
// The Functional Toggle
|
||||||
|
SwitchListTile(
|
||||||
|
contentPadding: EdgeInsets.zero, // Aligns switch text with the bold header
|
||||||
|
title: Text(locService.t('keepScreenAwake.title')),
|
||||||
|
subtitle: Text(
|
||||||
|
locService.t('keepScreenAwake.subtitle'),
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
value: appState.keepScreenAwake,
|
||||||
|
onChanged: (bool value) {
|
||||||
|
appState.setKeepScreenAwake(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,7 +38,7 @@ class NodeProfilesSection extends StatelessWidget {
|
|||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
itemCount: appState.profiles.length,
|
itemCount: appState.profiles.length,
|
||||||
onReorder: (oldIndex, newIndex) {
|
onReorderItem: (oldIndex, newIndex) {
|
||||||
appState.reorderProfiles(oldIndex, newIndex);
|
appState.reorderProfiles(oldIndex, newIndex);
|
||||||
},
|
},
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
|
|||||||
@@ -54,47 +54,64 @@ class OperatorProfilesSection extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
...appState.operatorProfiles.map(
|
ReorderableListView.builder(
|
||||||
(p) => ListTile(
|
shrinkWrap: true,
|
||||||
title: Text(p.name),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
subtitle: Text(locService.t('operatorProfiles.tagsCount', params: [p.tags.length.toString()])),
|
itemCount: appState.operatorProfiles.length,
|
||||||
trailing: PopupMenuButton(
|
onReorderItem: (oldIndex, newIndex) {
|
||||||
itemBuilder: (context) => [
|
appState.reorderOperatorProfiles(oldIndex, newIndex);
|
||||||
PopupMenuItem(
|
},
|
||||||
value: 'edit',
|
itemBuilder: (context, index) {
|
||||||
child: Row(
|
final p = appState.operatorProfiles[index];
|
||||||
children: [
|
return ListTile(
|
||||||
const Icon(Icons.edit),
|
key: ValueKey(p.id),
|
||||||
const SizedBox(width: 8),
|
leading: ReorderableDragStartListener(
|
||||||
Text(locService.t('actions.edit')),
|
index: index,
|
||||||
],
|
child: const Icon(
|
||||||
),
|
Icons.drag_handle,
|
||||||
|
color: Colors.grey,
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
),
|
||||||
value: 'delete',
|
title: Text(p.name),
|
||||||
child: Row(
|
subtitle: Text(locService.t('operatorProfiles.tagsCount', params: [p.tags.length.toString()])),
|
||||||
children: [
|
trailing: PopupMenuButton(
|
||||||
const Icon(Icons.delete, color: Colors.red),
|
itemBuilder: (context) => [
|
||||||
const SizedBox(width: 8),
|
PopupMenuItem(
|
||||||
Text(locService.t('operatorProfiles.deleteOperatorProfile'), style: const TextStyle(color: Colors.red)),
|
value: 'edit',
|
||||||
],
|
child: Row(
|
||||||
),
|
children: [
|
||||||
),
|
const Icon(Icons.edit),
|
||||||
],
|
const SizedBox(width: 8),
|
||||||
onSelected: (value) {
|
Text(locService.t('actions.edit')),
|
||||||
if (value == 'edit') {
|
],
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (_) => OperatorProfileEditor(profile: p),
|
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
} else if (value == 'delete') {
|
PopupMenuItem(
|
||||||
_showDeleteProfileDialog(context, p);
|
value: 'delete',
|
||||||
}
|
child: Row(
|
||||||
},
|
children: [
|
||||||
),
|
const Icon(Icons.delete, color: Colors.red),
|
||||||
),
|
const SizedBox(width: 8),
|
||||||
|
Text(locService.t('operatorProfiles.deleteOperatorProfile'), style: const TextStyle(color: Colors.red)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onSelected: (value) {
|
||||||
|
if (value == 'edit') {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => OperatorProfileEditor(profile: p),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (value == 'delete') {
|
||||||
|
_showDeleteProfileDialog(context, p);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -86,12 +86,12 @@ class AuthService {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
final token = await _helper.getToken();
|
final token = await _helper.getToken();
|
||||||
if (token?.accessToken == null) {
|
if (token.accessToken == null) {
|
||||||
log('OAuth error: token null or missing accessToken');
|
log('OAuth error: token null or missing accessToken');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final tokenMap = {
|
final tokenMap = {
|
||||||
'accessToken': token!.accessToken,
|
'accessToken': token.accessToken,
|
||||||
'refreshToken': token.refreshToken,
|
'refreshToken': token.refreshToken,
|
||||||
};
|
};
|
||||||
final tokenJson = jsonEncode(tokenMap);
|
final tokenJson = jsonEncode(tokenMap);
|
||||||
|
|||||||
@@ -225,10 +225,22 @@ class ChangelogService {
|
|||||||
versionsNeedingMigration.add('1.6.3');
|
versionsNeedingMigration.add('1.6.3');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (needsMigration(lastSeenVersion, currentVersion, '1.8.0')) {
|
||||||
|
versionsNeedingMigration.add('1.8.0');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsMigration(lastSeenVersion, currentVersion, '2.1.0')) {
|
||||||
|
versionsNeedingMigration.add('2.1.0');
|
||||||
|
}
|
||||||
|
|
||||||
if (needsMigration(lastSeenVersion, currentVersion, '2.7.3')) {
|
if (needsMigration(lastSeenVersion, currentVersion, '2.7.3')) {
|
||||||
versionsNeedingMigration.add('2.7.3');
|
versionsNeedingMigration.add('2.7.3');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (needsMigration(lastSeenVersion, currentVersion, '2.10.0')) {
|
||||||
|
versionsNeedingMigration.add('2.10.0');
|
||||||
|
}
|
||||||
|
|
||||||
// Future versions can be added here
|
// Future versions can be added here
|
||||||
// if (needsMigration(lastSeenVersion, currentVersion, '2.0.0')) {
|
// if (needsMigration(lastSeenVersion, currentVersion, '2.0.0')) {
|
||||||
// versionsNeedingMigration.add('2.0.0');
|
// versionsNeedingMigration.add('2.0.0');
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'package:app_links/app_links.dart';
|
import 'package:app_links/app_links.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
|
|
||||||
import '../models/node_profile.dart';
|
import '../models/node_profile.dart';
|
||||||
|
import '../models/operator_profile.dart';
|
||||||
|
import '../models/osm_node.dart';
|
||||||
|
import 'http_client.dart';
|
||||||
import 'profile_import_service.dart';
|
import 'profile_import_service.dart';
|
||||||
|
import 'operator_profile_import_service.dart';
|
||||||
import '../screens/profile_editor.dart';
|
import '../screens/profile_editor.dart';
|
||||||
|
import '../screens/operator_profile_editor.dart';
|
||||||
|
|
||||||
class DeepLinkService {
|
class DeepLinkService {
|
||||||
static final DeepLinkService _instance = DeepLinkService._internal();
|
static final DeepLinkService _instance = DeepLinkService._internal();
|
||||||
@@ -13,6 +20,9 @@ class DeepLinkService {
|
|||||||
|
|
||||||
late AppLinks _appLinks;
|
late AppLinks _appLinks;
|
||||||
StreamSubscription<Uri>? _linkSubscription;
|
StreamSubscription<Uri>? _linkSubscription;
|
||||||
|
|
||||||
|
/// Callback for HomeScreen to receive node deep links
|
||||||
|
void Function(OsmNode node)? onNodeDeepLink;
|
||||||
|
|
||||||
/// Initialize deep link handling (sets up stream listener only)
|
/// Initialize deep link handling (sets up stream listener only)
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
@@ -42,6 +52,9 @@ class DeepLinkService {
|
|||||||
case 'profiles':
|
case 'profiles':
|
||||||
_handleProfilesLink(uri);
|
_handleProfilesLink(uri);
|
||||||
break;
|
break;
|
||||||
|
case 'node':
|
||||||
|
_handleNodeLink(uri);
|
||||||
|
break;
|
||||||
case 'auth':
|
case 'auth':
|
||||||
// OAuth links are handled by flutter_web_auth_2
|
// OAuth links are handled by flutter_web_auth_2
|
||||||
debugPrint('[DeepLinkService] OAuth link handled by flutter_web_auth_2');
|
debugPrint('[DeepLinkService] OAuth link handled by flutter_web_auth_2');
|
||||||
@@ -68,6 +81,59 @@ class DeepLinkService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handle node deep link: `deflockapp://node?id=<nodeId>`
|
||||||
|
Future<void> _handleNodeLink(Uri uri) async {
|
||||||
|
final idStr = uri.queryParameters['id'];
|
||||||
|
final nodeId = int.tryParse(idStr ?? '');
|
||||||
|
if (nodeId == null) {
|
||||||
|
_showError('Invalid node link: missing or invalid ID');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final node = await _fetchNodeById(nodeId);
|
||||||
|
if (node == null) {
|
||||||
|
_showError('Node $nodeId not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onNodeDeepLink != null) {
|
||||||
|
onNodeDeepLink!(node);
|
||||||
|
} else {
|
||||||
|
debugPrint('[DeepLinkService] No node deep link handler registered');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetch an OSM node by ID from the OpenStreetMap API
|
||||||
|
Future<OsmNode?> _fetchNodeById(int nodeId) async {
|
||||||
|
try {
|
||||||
|
final url = Uri.parse('https://api.openstreetmap.org/api/0.6/node/$nodeId.json');
|
||||||
|
final client = UserAgentClient();
|
||||||
|
final response = await client.get(url);
|
||||||
|
if (response.statusCode != 200) return null;
|
||||||
|
|
||||||
|
final json = jsonDecode(response.body);
|
||||||
|
final elements = json['elements'] as List?;
|
||||||
|
if (elements == null || elements.isEmpty) return null;
|
||||||
|
|
||||||
|
final e = elements[0];
|
||||||
|
final tags = <String, String>{};
|
||||||
|
if (e['tags'] != null) {
|
||||||
|
(e['tags'] as Map<String, dynamic>).forEach((k, v) {
|
||||||
|
tags[k] = v.toString();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return OsmNode(
|
||||||
|
id: e['id'] as int,
|
||||||
|
coord: LatLng((e['lat'] as num).toDouble(), (e['lon'] as num).toDouble()),
|
||||||
|
tags: tags,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[DeepLinkService] Failed to fetch node $nodeId: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Handle profile-related deep links
|
/// Handle profile-related deep links
|
||||||
void _handleProfilesLink(Uri uri) {
|
void _handleProfilesLink(Uri uri) {
|
||||||
final segments = uri.pathSegments;
|
final segments = uri.pathSegments;
|
||||||
@@ -86,8 +152,16 @@ class DeepLinkService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle profile add deep link: `deflockapp://profiles/add?p=<base64>`
|
/// Handle profile add deep link: `deflockapp://profiles/add?p=<base64>` or `deflockapp://profiles/add?op=<base64>`
|
||||||
void _handleAddProfileLink(Uri uri) {
|
void _handleAddProfileLink(Uri uri) {
|
||||||
|
// Check for operator profile parameter first
|
||||||
|
final operatorBase64Data = uri.queryParameters['op'];
|
||||||
|
if (operatorBase64Data != null && operatorBase64Data.isNotEmpty) {
|
||||||
|
_handleOperatorProfileImport(operatorBase64Data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise check for device profile parameter
|
||||||
final base64Data = uri.queryParameters['p'];
|
final base64Data = uri.queryParameters['p'];
|
||||||
|
|
||||||
if (base64Data == null || base64Data.isEmpty) {
|
if (base64Data == null || base64Data.isEmpty) {
|
||||||
@@ -107,6 +181,20 @@ class DeepLinkService {
|
|||||||
_navigateToProfileEditor(profile);
|
_navigateToProfileEditor(profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handle operator profile import from deep link
|
||||||
|
void _handleOperatorProfileImport(String base64Data) {
|
||||||
|
// Parse operator profile from base64
|
||||||
|
final operatorProfile = OperatorProfileImportService.parseProfileFromBase64(base64Data);
|
||||||
|
|
||||||
|
if (operatorProfile == null) {
|
||||||
|
_showError('Invalid operator profile data');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate to operator profile editor with the imported profile
|
||||||
|
_navigateToOperatorProfileEditor(operatorProfile);
|
||||||
|
}
|
||||||
|
|
||||||
/// Navigate to profile editor with pre-filled profile data
|
/// Navigate to profile editor with pre-filled profile data
|
||||||
void _navigateToProfileEditor(NodeProfile profile) {
|
void _navigateToProfileEditor(NodeProfile profile) {
|
||||||
final context = _navigatorKey?.currentContext;
|
final context = _navigatorKey?.currentContext;
|
||||||
@@ -124,6 +212,23 @@ class DeepLinkService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Navigate to operator profile editor with pre-filled operator profile data
|
||||||
|
void _navigateToOperatorProfileEditor(OperatorProfile operatorProfile) {
|
||||||
|
final context = _navigatorKey?.currentContext;
|
||||||
|
|
||||||
|
if (context == null) {
|
||||||
|
debugPrint('[DeepLinkService] No navigator context available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => OperatorProfileEditor(profile: operatorProfile),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Show error message to user
|
/// Show error message to user
|
||||||
void _showError(String message) {
|
void _showError(String message) {
|
||||||
final context = _navigatorKey?.currentContext;
|
final context = _navigatorKey?.currentContext;
|
||||||
|
|||||||
@@ -54,8 +54,8 @@ class MapDataProvider {
|
|||||||
required List<NodeProfile> profiles,
|
required List<NodeProfile> profiles,
|
||||||
UploadMode uploadMode = UploadMode.production,
|
UploadMode uploadMode = UploadMode.production,
|
||||||
int maxResults = 0, // 0 = no limit for offline downloads
|
int maxResults = 0, // 0 = no limit for offline downloads
|
||||||
int maxTries = 3,
|
|
||||||
}) async {
|
}) async {
|
||||||
|
|
||||||
if (AppState.instance.offlineMode) {
|
if (AppState.instance.offlineMode) {
|
||||||
throw OfflineModeException("Cannot fetch remote nodes for offline area download in offline mode.");
|
throw OfflineModeException("Cannot fetch remote nodes for offline area download in offline mode.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -215,7 +215,18 @@ class NodeDataManager extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch nodes with automatic area splitting if needed
|
/// Fetch nodes, splitting the query area into quadrants only when Overpass
|
||||||
|
/// reports the query would exceed its hard 50k node limit ([NodeLimitError]).
|
||||||
|
///
|
||||||
|
/// Splitting is deliberately NOT used for other failure modes:
|
||||||
|
/// - [RateLimitError] (HTTP 429): the server explicitly asked us to slow
|
||||||
|
/// down, so firing off up to 4x more requests would be counterproductive.
|
||||||
|
/// We back off instead, while still surfacing the "slow" status
|
||||||
|
/// indicator to the user (same UI feedback as the splitting case).
|
||||||
|
/// - Timeouts / other transient failures now surface as [NetworkError]
|
||||||
|
/// (see [OverpassService]) and are not caught here at all — they're
|
||||||
|
/// handled by the normal retry/backoff/fallback logic and reported as a
|
||||||
|
/// plain timeout/error status by the caller.
|
||||||
Future<List<OsmNode>> fetchWithSplitting(
|
Future<List<OsmNode>> fetchWithSplitting(
|
||||||
LatLngBounds bounds,
|
LatLngBounds bounds,
|
||||||
List<NodeProfile> profiles, {
|
List<NodeProfile> profiles, {
|
||||||
@@ -238,7 +249,7 @@ class NodeDataManager extends ChangeNotifier {
|
|||||||
return nodes;
|
return nodes;
|
||||||
|
|
||||||
} on NodeLimitError {
|
} on NodeLimitError {
|
||||||
// Hit node limit or timeout - split area if not too deep
|
// Hit the deterministic 50k node limit - split area if not too deep
|
||||||
if (splitDepth >= maxSplitDepth) {
|
if (splitDepth >= maxSplitDepth) {
|
||||||
debugPrint('[NodeDataManager] Max split depth reached, giving up');
|
debugPrint('[NodeDataManager] Max split depth reached, giving up');
|
||||||
return [];
|
return [];
|
||||||
@@ -254,13 +265,20 @@ class NodeDataManager extends ChangeNotifier {
|
|||||||
return _fetchSplitAreas(bounds, profiles, splitDepth + 1, isUserInitiated: isUserInitiated);
|
return _fetchSplitAreas(bounds, profiles, splitDepth + 1, isUserInitiated: isUserInitiated);
|
||||||
|
|
||||||
} on RateLimitError {
|
} on RateLimitError {
|
||||||
// Rate limited - wait and return empty
|
// Rate limited - back off without splitting the area. Splitting here
|
||||||
debugPrint('[NodeDataManager] Rate limited, backing off');
|
// would multiply request volume against a server that just told us to
|
||||||
|
// slow down. We still show the same "slow" status indicator so the
|
||||||
|
// user gets feedback, but there is no recursive quadrant fetching.
|
||||||
|
debugPrint('[NodeDataManager] Rate limited, backing off (no area splitting)');
|
||||||
|
if (isUserInitiated) {
|
||||||
|
NetworkStatus.instance.setSplitting();
|
||||||
|
}
|
||||||
await Future.delayed(const Duration(seconds: 30));
|
await Future.delayed(const Duration(seconds: 30));
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Fetch data by splitting area into quadrants
|
/// Fetch data by splitting area into quadrants
|
||||||
Future<List<OsmNode>> _fetchSplitAreas(
|
Future<List<OsmNode>> _fetchSplitAreas(
|
||||||
LatLngBounds bounds,
|
LatLngBounds bounds,
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
|
import '../models/operator_profile.dart';
|
||||||
|
|
||||||
|
class OperatorProfileImportService {
|
||||||
|
// Maximum size for base64 encoded profile data (approx 50KB decoded)
|
||||||
|
static const int maxBase64Length = 70000;
|
||||||
|
|
||||||
|
/// Parse and validate an operator profile from a base64-encoded JSON string
|
||||||
|
/// Returns null if parsing/validation fails
|
||||||
|
static OperatorProfile? parseProfileFromBase64(String base64Data) {
|
||||||
|
try {
|
||||||
|
// Basic size validation before expensive decode
|
||||||
|
if (base64Data.length > maxBase64Length) {
|
||||||
|
debugPrint('[OperatorProfileImportService] Base64 data too large: ${base64Data.length} characters');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode base64
|
||||||
|
final jsonBytes = base64Decode(base64Data);
|
||||||
|
final jsonString = utf8.decode(jsonBytes);
|
||||||
|
|
||||||
|
// Parse JSON
|
||||||
|
final jsonData = jsonDecode(jsonString) as Map<String, dynamic>;
|
||||||
|
|
||||||
|
// Validate and sanitize the profile data
|
||||||
|
final sanitizedProfile = _validateAndSanitizeProfile(jsonData);
|
||||||
|
return sanitizedProfile;
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[OperatorProfileImportService] Failed to parse profile from base64: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate operator profile structure and sanitize all string values
|
||||||
|
static OperatorProfile? _validateAndSanitizeProfile(Map<String, dynamic> data) {
|
||||||
|
try {
|
||||||
|
// Extract and sanitize required fields
|
||||||
|
final name = _sanitizeString(data['name']);
|
||||||
|
if (name == null || name.isEmpty) {
|
||||||
|
debugPrint('[OperatorProfileImportService] Operator profile name is required');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract and sanitize tags
|
||||||
|
final tagsData = data['tags'];
|
||||||
|
if (tagsData is! Map<String, dynamic>) {
|
||||||
|
debugPrint('[OperatorProfileImportService] Operator profile tags must be a map');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final sanitizedTags = <String, String>{};
|
||||||
|
for (final entry in tagsData.entries) {
|
||||||
|
final key = _sanitizeString(entry.key);
|
||||||
|
final value = _sanitizeString(entry.value);
|
||||||
|
|
||||||
|
if (key != null && key.isNotEmpty) {
|
||||||
|
// Allow empty values for refinement purposes
|
||||||
|
sanitizedTags[key] = value ?? '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sanitizedTags.isEmpty) {
|
||||||
|
debugPrint('[OperatorProfileImportService] Operator profile must have at least one valid tag');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return OperatorProfile(
|
||||||
|
id: const Uuid().v4(), // Always generate new ID for imported profiles
|
||||||
|
name: name,
|
||||||
|
tags: sanitizedTags,
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[OperatorProfileImportService] Failed to validate operator profile: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sanitize a string value by trimming and removing potentially harmful characters
|
||||||
|
static String? _sanitizeString(dynamic value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
|
||||||
|
final str = value.toString().trim();
|
||||||
|
|
||||||
|
// Remove control characters and limit length
|
||||||
|
final sanitized = str.replaceAll(RegExp(r'[\x00-\x1F\x7F]'), '');
|
||||||
|
|
||||||
|
// Limit length to prevent abuse
|
||||||
|
const maxLength = 500;
|
||||||
|
if (sanitized.length > maxLength) {
|
||||||
|
return sanitized.substring(0, maxLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sanitized;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
/// Exception thrown when Overpass API returns an error indicating too many nodes were requested.
|
|
||||||
/// This typically happens when querying large areas that would return more than 50k nodes.
|
|
||||||
class OverpassNodeLimitException implements Exception {
|
|
||||||
final String message;
|
|
||||||
final String? serverResponse;
|
|
||||||
|
|
||||||
OverpassNodeLimitException(this.message, {this.serverResponse});
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => 'OverpassNodeLimitException: $message';
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Exception thrown when Overpass API rate limits the request.
|
|
||||||
/// Should trigger longer backoff delays, not area splitting.
|
|
||||||
class OverpassRateLimitException implements Exception {
|
|
||||||
final String message;
|
|
||||||
final String? serverResponse;
|
|
||||||
|
|
||||||
OverpassRateLimitException(this.message, {this.serverResponse});
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => 'OverpassRateLimitException: $message';
|
|
||||||
}
|
|
||||||
@@ -32,7 +32,20 @@ class OverpassService {
|
|||||||
String get _primaryEndpoint => _endpointOverride ?? defaultEndpoint;
|
String get _primaryEndpoint => _endpointOverride ?? defaultEndpoint;
|
||||||
|
|
||||||
/// Fetch surveillance nodes from Overpass API with retry and fallback.
|
/// Fetch surveillance nodes from Overpass API with retry and fallback.
|
||||||
/// Throws NetworkError for retryable failures, NodeLimitError for area splitting.
|
///
|
||||||
|
/// Throws:
|
||||||
|
/// - [NodeLimitError] when the query would exceed Overpass's hard 50k node
|
||||||
|
/// limit. This is the *only* case where the caller should split the
|
||||||
|
/// query area into smaller regions, since it's a deterministic function
|
||||||
|
/// of how much data lives in the requested bounds.
|
||||||
|
/// - [RateLimitError] when rate limited (HTTP 429 or equivalent). Callers
|
||||||
|
/// should back off, not split/retry — splitting would only amplify load
|
||||||
|
/// on a server that just told us to slow down.
|
||||||
|
/// - [NetworkError] for timeouts and other retryable HTTP/network failures.
|
||||||
|
/// Timeouts are usually a sign of server load or an expensive query, not
|
||||||
|
/// "too much area" — retrying with a hail of smaller sub-requests makes
|
||||||
|
/// the problem worse, so timeouts are treated like any other transient
|
||||||
|
/// network failure (handled by the existing retry/backoff/fallback logic).
|
||||||
Future<List<OsmNode>> fetchNodes({
|
Future<List<OsmNode>> fetchNodes({
|
||||||
required LatLngBounds bounds,
|
required LatLngBounds bounds,
|
||||||
required List<NodeProfile> profiles,
|
required List<NodeProfile> profiles,
|
||||||
@@ -70,22 +83,16 @@ class OverpassService {
|
|||||||
|
|
||||||
final errorBody = response.body;
|
final errorBody = response.body;
|
||||||
|
|
||||||
// Node limit error - caller should split area
|
// Node limit error - a deterministic result-set size limit. This is
|
||||||
|
// the one case where splitting the query area into smaller regions is
|
||||||
|
// the correct fix, since it directly reduces nodes-per-request.
|
||||||
if (response.statusCode == 400 &&
|
if (response.statusCode == 400 &&
|
||||||
(errorBody.contains('too many nodes') && errorBody.contains('50000'))) {
|
(errorBody.contains('too many nodes') && errorBody.contains('50000'))) {
|
||||||
debugPrint('[OverpassService] Node limit exceeded, area should be split');
|
debugPrint('[OverpassService] Node limit exceeded (50k), area should be split');
|
||||||
throw NodeLimitError('Query exceeded 50k node limit');
|
throw NodeLimitError('Query exceeded 50k node limit');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timeout error - also try splitting (complex query)
|
// Rate limit - back off, don't split or hammer the server with more requests.
|
||||||
if (errorBody.contains('timeout') ||
|
|
||||||
errorBody.contains('runtime limit exceeded') ||
|
|
||||||
errorBody.contains('Query timed out')) {
|
|
||||||
debugPrint('[OverpassService] Query timeout, area should be split');
|
|
||||||
throw NodeLimitError('Query timed out - area too complex');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rate limit
|
|
||||||
if (response.statusCode == 429 ||
|
if (response.statusCode == 429 ||
|
||||||
errorBody.contains('rate limited') ||
|
errorBody.contains('rate limited') ||
|
||||||
errorBody.contains('too many requests')) {
|
errorBody.contains('too many requests')) {
|
||||||
@@ -93,6 +100,17 @@ class OverpassService {
|
|||||||
throw RateLimitError('Rate limited by Overpass API');
|
throw RateLimitError('Rate limited by Overpass API');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Timeout / runtime limit exceeded - treat as a plain retryable network
|
||||||
|
// error. This is usually server load or query cost, not "area too
|
||||||
|
// big" — splitting into up to 64 sub-requests would only make load
|
||||||
|
// on the server worse.
|
||||||
|
if (errorBody.contains('timeout') ||
|
||||||
|
errorBody.contains('runtime limit exceeded') ||
|
||||||
|
errorBody.contains('Query timed out')) {
|
||||||
|
debugPrint('[OverpassService] Query timed out');
|
||||||
|
throw NetworkError('Query timed out');
|
||||||
|
}
|
||||||
|
|
||||||
throw NetworkError('HTTP ${response.statusCode}: $errorBody');
|
throw NetworkError('HTTP ${response.statusCode}: $errorBody');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e is NodeLimitError || e is RateLimitError || e is NetworkError) {
|
if (e is NodeLimitError || e is RateLimitError || e is NetworkError) {
|
||||||
@@ -108,9 +126,24 @@ class OverpassService {
|
|||||||
return ErrorDisposition.retry;
|
return ErrorDisposition.retry;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build Overpass QL query for given bounds and profiles
|
/// Build Overpass QL query for given bounds and profiles.
|
||||||
|
///
|
||||||
|
/// Profiles are deduplicated first: if one profile's non-empty tags are a
|
||||||
|
/// subset of another's (with identical values), the broader profile's query
|
||||||
|
/// clause already returns every node the narrower one would, so the
|
||||||
|
/// narrower clause is redundant and can be dropped. This directly reduces
|
||||||
|
/// query size/complexity without changing the result set — client-side
|
||||||
|
/// filtering (matching returned nodes against the full profile list) still
|
||||||
|
/// happens independently, so nothing is lost.
|
||||||
String _buildQuery(LatLngBounds bounds, List<NodeProfile> profiles) {
|
String _buildQuery(LatLngBounds bounds, List<NodeProfile> profiles) {
|
||||||
final nodeClauses = profiles.map((profile) {
|
final profilesToQuery = _deduplicateProfilesForQuery(profiles);
|
||||||
|
|
||||||
|
if (profilesToQuery.length < profiles.length) {
|
||||||
|
debugPrint(
|
||||||
|
'[OverpassService] Deduplicated ${profiles.length} profiles to ${profilesToQuery.length} for query efficiency');
|
||||||
|
}
|
||||||
|
|
||||||
|
final nodeClauses = profilesToQuery.map((profile) {
|
||||||
// Convert profile tags to Overpass filter format, excluding empty values
|
// Convert profile tags to Overpass filter format, excluding empty values
|
||||||
final tagFilters = profile.tags.entries
|
final tagFilters = profile.tags.entries
|
||||||
.where((entry) => entry.value.trim().isNotEmpty)
|
.where((entry) => entry.value.trim().isNotEmpty)
|
||||||
@@ -126,14 +159,79 @@ class OverpassService {
|
|||||||
$nodeClauses
|
$nodeClauses
|
||||||
);
|
);
|
||||||
out body;
|
out body;
|
||||||
(
|
<;
|
||||||
way(bn);
|
out ids;
|
||||||
rel(bn);
|
|
||||||
);
|
|
||||||
out skel;
|
|
||||||
''';
|
''';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Deduplicate profiles for Overpass queries by removing profiles that are
|
||||||
|
/// subsumed by others. A profile A subsumes profile B if all of A's
|
||||||
|
/// non-empty tags exist in B with identical values — meaning every node
|
||||||
|
/// matching B's filter also matches A's filter, so B's clause is redundant.
|
||||||
|
List<NodeProfile> _deduplicateProfilesForQuery(List<NodeProfile> profiles) {
|
||||||
|
if (profiles.length <= 1) return profiles;
|
||||||
|
|
||||||
|
final result = <NodeProfile>[];
|
||||||
|
|
||||||
|
for (final candidate in profiles) {
|
||||||
|
// Skip profiles that only have empty tags - they would match
|
||||||
|
// everything and shouldn't be treated as subsuming or subsumable.
|
||||||
|
final candidateNonEmptyTags = candidate.tags.entries
|
||||||
|
.where((entry) => entry.value.trim().isNotEmpty)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (candidateNonEmptyTags.isEmpty) continue;
|
||||||
|
|
||||||
|
// Check if any existing profile in our result subsumes this candidate
|
||||||
|
bool isSubsumed = false;
|
||||||
|
for (final existing in result) {
|
||||||
|
if (_profileSubsumes(existing, candidate)) {
|
||||||
|
isSubsumed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isSubsumed) {
|
||||||
|
// This candidate is not subsumed, so add it. But first, remove any
|
||||||
|
// existing profiles that this candidate subsumes (candidate is
|
||||||
|
// broader, so those clauses are now redundant).
|
||||||
|
result.removeWhere((existing) => _profileSubsumes(candidate, existing));
|
||||||
|
result.add(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safety check: if dedup removed everything (e.g. all-empty-tag
|
||||||
|
// profiles), fall back to the original list rather than sending an
|
||||||
|
// empty query.
|
||||||
|
return result.isNotEmpty ? result : profiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if [broaderProfile] subsumes [specificProfile]: true if all
|
||||||
|
/// non-empty tags in [broaderProfile] exist in [specificProfile] with
|
||||||
|
/// identical values.
|
||||||
|
bool _profileSubsumes(NodeProfile broaderProfile, NodeProfile specificProfile) {
|
||||||
|
final broaderTags = Map.fromEntries(
|
||||||
|
broaderProfile.tags.entries.where((entry) => entry.value.trim().isNotEmpty),
|
||||||
|
);
|
||||||
|
final specificTags = Map.fromEntries(
|
||||||
|
specificProfile.tags.entries.where((entry) => entry.value.trim().isNotEmpty),
|
||||||
|
);
|
||||||
|
|
||||||
|
// If broader has no non-empty tags, it doesn't subsume anything (it
|
||||||
|
// would match everything, which isn't a meaningful "broader filter").
|
||||||
|
if (broaderTags.isEmpty) return false;
|
||||||
|
|
||||||
|
// If broader has more non-empty tags than specific, it can't subsume it.
|
||||||
|
if (broaderTags.length > specificTags.length) return false;
|
||||||
|
|
||||||
|
for (final entry in broaderTags.entries) {
|
||||||
|
if (specificTags[entry.key] != entry.value) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Parse Overpass JSON response into OsmNode objects
|
/// Parse Overpass JSON response into OsmNode objects
|
||||||
List<OsmNode> _parseResponse(String responseBody) {
|
List<OsmNode> _parseResponse(String responseBody) {
|
||||||
final data = jsonDecode(responseBody) as Map<String, dynamic>;
|
final data = jsonDecode(responseBody) as Map<String, dynamic>;
|
||||||
@@ -176,7 +274,9 @@ out skel;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Error thrown when query exceeds node limits or is too complex - area should be split
|
/// Error thrown when a query would exceed Overpass's 50k node limit.
|
||||||
|
/// The caller should split the query area into smaller regions to resolve this
|
||||||
|
/// — it's a deterministic function of how much data lives within the bounds.
|
||||||
class NodeLimitError extends Error {
|
class NodeLimitError extends Error {
|
||||||
final String message;
|
final String message;
|
||||||
NodeLimitError(this.message);
|
NodeLimitError(this.message);
|
||||||
@@ -192,7 +292,7 @@ class RateLimitError extends Error {
|
|||||||
String toString() => 'RateLimitError: $message';
|
String toString() => 'RateLimitError: $message';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Error thrown for network/HTTP issues - retryable
|
/// Error thrown for network/HTTP issues (including timeouts) - retryable
|
||||||
class NetworkError extends Error {
|
class NetworkError extends Error {
|
||||||
final String message;
|
final String message;
|
||||||
NetworkError(this.message);
|
NetworkError(this.message);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
import '../models/node_profile.dart';
|
import '../models/node_profile.dart';
|
||||||
|
import '../dev_config.dart';
|
||||||
|
|
||||||
class ProfileImportService {
|
class ProfileImportService {
|
||||||
// Maximum size for base64 encoded profile data (approx 50KB decoded)
|
// Maximum size for base64 encoded profile data (approx 50KB decoded)
|
||||||
@@ -72,13 +73,17 @@ class ProfileImportService {
|
|||||||
final requiresDirection = data['requiresDirection'] ?? true;
|
final requiresDirection = data['requiresDirection'] ?? true;
|
||||||
final submittable = data['submittable'] ?? true;
|
final submittable = data['submittable'] ?? true;
|
||||||
|
|
||||||
// Parse FOV if provided
|
// Parse FOV if provided, applying restrictions based on dev config
|
||||||
double? fov;
|
double? fov;
|
||||||
if (data['fov'] != null) {
|
if (data['fov'] != null) {
|
||||||
if (data['fov'] is num) {
|
if (data['fov'] is num) {
|
||||||
final fovValue = (data['fov'] as num).toDouble();
|
final fovValue = (data['fov'] as num).toDouble();
|
||||||
if (fovValue > 0 && fovValue <= 360) {
|
if (fovValue > 0 && fovValue <= 360) {
|
||||||
fov = fovValue;
|
// If non-360 FOVs are disabled, only allow 360° FOV
|
||||||
|
if (kEnableNon360FOVs || fovValue == 360.0) {
|
||||||
|
fov = fovValue;
|
||||||
|
}
|
||||||
|
// Otherwise, clear non-360 FOV values (set fov to null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,13 +58,18 @@ class SuspectedLocationDatabase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create database tables
|
/// Create database tables
|
||||||
Future<void> _createTables(Database db, int version) async {
|
///
|
||||||
|
/// Takes a [DatabaseExecutor] (rather than [Database]) so this can be
|
||||||
|
/// called either directly on the database (e.g. from [onCreate]) or inside
|
||||||
|
/// a transaction (see [insertBatch]).
|
||||||
|
Future<void> _createTables(DatabaseExecutor db, int version) async {
|
||||||
debugPrint('[SuspectedLocationDatabase] Creating tables...');
|
debugPrint('[SuspectedLocationDatabase] Creating tables...');
|
||||||
|
|
||||||
// Main suspected locations table
|
// Main suspected locations table
|
||||||
await db.execute('''
|
await db.execute('''
|
||||||
CREATE TABLE $_tableName (
|
CREATE TABLE $_tableName (
|
||||||
$_columnTicketNo TEXT PRIMARY KEY,
|
$_columnTicketNo TEXT PRIMARY KEY,
|
||||||
|
|
||||||
$_columnCentroidLat REAL NOT NULL,
|
$_columnCentroidLat REAL NOT NULL,
|
||||||
$_columnCentroidLng REAL NOT NULL,
|
$_columnCentroidLng REAL NOT NULL,
|
||||||
$_columnBounds TEXT,
|
$_columnBounds TEXT,
|
||||||
|
|||||||
@@ -35,16 +35,31 @@ class SuspectedLocationService {
|
|||||||
// Load cache data
|
// Load cache data
|
||||||
await _cache.loadFromStorage();
|
await _cache.loadFromStorage();
|
||||||
|
|
||||||
// Only auto-fetch if enabled, data is stale or missing, and we are not offline
|
// Note: Auto-refresh logic moved to initBackgroundRefresh() to avoid blocking app startup
|
||||||
if (_isEnabled && (await _shouldRefresh()) && !offlineMode) {
|
if (_isEnabled && (await _shouldRefresh()) && offlineMode) {
|
||||||
debugPrint('[SuspectedLocationService] Auto-refreshing CSV data on startup (older than $_maxAge or missing)');
|
|
||||||
await _fetchData();
|
|
||||||
} else if (_isEnabled && (await _shouldRefresh()) && offlineMode) {
|
|
||||||
final lastFetch = await _cache.lastFetchTime;
|
final lastFetch = await _cache.lastFetchTime;
|
||||||
debugPrint('[SuspectedLocationService] Skipping auto-refresh due to offline mode - data is ${lastFetch != null ? 'outdated' : 'missing'}');
|
debugPrint('[SuspectedLocationService] Skipping auto-refresh due to offline mode - data is ${lastFetch != null ? 'outdated' : 'missing'}');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if background refresh should happen and start it (non-blocking)
|
||||||
|
Future<void> initBackgroundRefresh({bool offlineMode = false}) async {
|
||||||
|
// Only auto-fetch if enabled, data is stale or missing, and we are not offline
|
||||||
|
if (_isEnabled && (await _shouldRefresh()) && !offlineMode) {
|
||||||
|
debugPrint('[SuspectedLocationService] Starting background auto-refresh of CSV data (older than $_maxAge or missing)');
|
||||||
|
// Don't await - let it run in background
|
||||||
|
_fetchData().then((success) {
|
||||||
|
if (success) {
|
||||||
|
debugPrint('[SuspectedLocationService] Background auto-refresh completed successfully');
|
||||||
|
} else {
|
||||||
|
debugPrint('[SuspectedLocationService] Background auto-refresh failed');
|
||||||
|
}
|
||||||
|
}).catchError((error) {
|
||||||
|
debugPrint('[SuspectedLocationService] Background auto-refresh error: $error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Enable or disable suspected locations
|
/// Enable or disable suspected locations
|
||||||
Future<void> setEnabled(bool enabled) async {
|
Future<void> setEnabled(bool enabled) async {
|
||||||
_isEnabled = enabled;
|
_isEnabled = enabled;
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import '../models/operator_profile.dart';
|
import '../models/operator_profile.dart';
|
||||||
import '../services/operator_profile_service.dart';
|
import '../services/operator_profile_service.dart';
|
||||||
|
|
||||||
class OperatorProfileState extends ChangeNotifier {
|
class OperatorProfileState extends ChangeNotifier {
|
||||||
|
static const String _profileOrderPrefsKey = 'operator_profile_order';
|
||||||
|
|
||||||
final List<OperatorProfile> _profiles = [];
|
final List<OperatorProfile> _profiles = [];
|
||||||
|
List<String> _customOrder = []; // List of profile IDs in user's preferred order
|
||||||
|
|
||||||
List<OperatorProfile> get profiles => List.unmodifiable(_profiles);
|
List<OperatorProfile> get profiles => List.unmodifiable(_getOrderedProfiles());
|
||||||
|
|
||||||
Future<void> init({bool addDefaults = false}) async {
|
Future<void> init({bool addDefaults = false}) async {
|
||||||
_profiles.addAll(await OperatorProfileService().load());
|
_profiles.addAll(await OperatorProfileService().load());
|
||||||
@@ -16,6 +20,10 @@ class OperatorProfileState extends ChangeNotifier {
|
|||||||
_profiles.addAll(OperatorProfile.getDefaults());
|
_profiles.addAll(OperatorProfile.getDefaults());
|
||||||
await OperatorProfileService().save(_profiles);
|
await OperatorProfileService().save(_profiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load custom order from prefs
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
_customOrder = prefs.getStringList(_profileOrderPrefsKey) ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
void addOrUpdateProfile(OperatorProfile p) {
|
void addOrUpdateProfile(OperatorProfile p) {
|
||||||
@@ -34,4 +42,53 @@ class OperatorProfileState extends ChangeNotifier {
|
|||||||
OperatorProfileService().save(_profiles);
|
OperatorProfileService().save(_profiles);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reorder profiles (for drag-and-drop in settings)
|
||||||
|
void reorderProfiles(int oldIndex, int newIndex) {
|
||||||
|
final orderedProfiles = _getOrderedProfiles();
|
||||||
|
|
||||||
|
// With onReorderItem callback, newIndex is already adjusted
|
||||||
|
final item = orderedProfiles.removeAt(oldIndex);
|
||||||
|
orderedProfiles.insert(newIndex, item);
|
||||||
|
|
||||||
|
// Update custom order with new sequence
|
||||||
|
_customOrder = orderedProfiles.map((p) => p.id).toList();
|
||||||
|
_saveCustomOrder();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get profiles in custom order, with unordered profiles at the end
|
||||||
|
List<OperatorProfile> _getOrderedProfiles() {
|
||||||
|
if (_customOrder.isEmpty) {
|
||||||
|
return List.from(_profiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
final ordered = <OperatorProfile>[];
|
||||||
|
final profilesById = {for (final p in _profiles) p.id: p};
|
||||||
|
|
||||||
|
// Add profiles in custom order
|
||||||
|
for (final id in _customOrder) {
|
||||||
|
final profile = profilesById[id];
|
||||||
|
if (profile != null) {
|
||||||
|
ordered.add(profile);
|
||||||
|
profilesById.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add any remaining profiles that weren't in the custom order
|
||||||
|
ordered.addAll(profilesById.values);
|
||||||
|
|
||||||
|
return ordered;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save custom order to disk
|
||||||
|
Future<void> _saveCustomOrder() async {
|
||||||
|
try {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setStringList(_profileOrderPrefsKey, _customOrder);
|
||||||
|
} catch (e) {
|
||||||
|
// Fail gracefully in tests or if SharedPreferences isn't available
|
||||||
|
debugPrint('[OperatorProfileState] Failed to save custom order: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -57,6 +57,28 @@ class ProfileState extends ChangeNotifier {
|
|||||||
_customOrder = prefs.getStringList(_profileOrderPrefsKey) ?? [];
|
_customOrder = prefs.getStringList(_profileOrderPrefsKey) ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reload all profiles from storage (useful after migrations modify stored profiles)
|
||||||
|
Future<void> reloadFromStorage() async {
|
||||||
|
// Preserve enabled state by ID
|
||||||
|
final enabledIds = _enabled.map((p) => p.id).toSet();
|
||||||
|
|
||||||
|
// Clear and reload profiles
|
||||||
|
_profiles.clear();
|
||||||
|
_profiles.addAll(await ProfileService().load());
|
||||||
|
|
||||||
|
// Restore enabled state for profiles that still exist
|
||||||
|
_enabled.clear();
|
||||||
|
_enabled.addAll(_profiles.where((p) => enabledIds.contains(p.id)));
|
||||||
|
|
||||||
|
// Safety: Always have at least one enabled profile
|
||||||
|
if (_enabled.isEmpty && _profiles.isNotEmpty) {
|
||||||
|
final builtIn = _profiles.firstWhere((profile) => profile.builtin, orElse: () => _profiles.first);
|
||||||
|
_enabled.add(builtIn);
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
void toggleProfile(NodeProfile p, bool e) {
|
void toggleProfile(NodeProfile p, bool e) {
|
||||||
if (e) {
|
if (e) {
|
||||||
_enabled.add(p);
|
_enabled.add(p);
|
||||||
@@ -107,10 +129,7 @@ class ProfileState extends ChangeNotifier {
|
|||||||
void reorderProfiles(int oldIndex, int newIndex) {
|
void reorderProfiles(int oldIndex, int newIndex) {
|
||||||
final orderedProfiles = _getOrderedProfiles();
|
final orderedProfiles = _getOrderedProfiles();
|
||||||
|
|
||||||
// Standard Flutter reordering logic
|
// With onReorderItem callback, newIndex is already adjusted
|
||||||
if (oldIndex < newIndex) {
|
|
||||||
newIndex -= 1;
|
|
||||||
}
|
|
||||||
final item = orderedProfiles.removeAt(oldIndex);
|
final item = orderedProfiles.removeAt(oldIndex);
|
||||||
orderedProfiles.insert(newIndex, item);
|
orderedProfiles.insert(newIndex, item);
|
||||||
|
|
||||||
|
|||||||
@@ -39,9 +39,11 @@ class SettingsState extends ChangeNotifier {
|
|||||||
static const String _pauseQueueProcessingPrefsKey = 'pause_queue_processing';
|
static const String _pauseQueueProcessingPrefsKey = 'pause_queue_processing';
|
||||||
static const String _navigationAvoidanceDistancePrefsKey = 'navigation_avoidance_distance';
|
static const String _navigationAvoidanceDistancePrefsKey = 'navigation_avoidance_distance';
|
||||||
static const String _distanceUnitPrefsKey = 'distance_unit';
|
static const String _distanceUnitPrefsKey = 'distance_unit';
|
||||||
|
static const String _keepScreenAwakePrefsKey = 'keep_screen_awake';
|
||||||
|
|
||||||
bool _offlineMode = false;
|
bool _offlineMode = false;
|
||||||
bool _pauseQueueProcessing = false;
|
bool _pauseQueueProcessing = false;
|
||||||
|
bool _keepScreenAwake = false;
|
||||||
int _maxNodes = kDefaultMaxNodes;
|
int _maxNodes = kDefaultMaxNodes;
|
||||||
// Default must account for missing secrets (preview builds) even before init() runs
|
// Default must account for missing secrets (preview builds) even before init() runs
|
||||||
UploadMode _uploadMode = (kEnableDevelopmentModes || !kHasOsmSecrets) ? UploadMode.simulate : UploadMode.production;
|
UploadMode _uploadMode = (kEnableDevelopmentModes || !kHasOsmSecrets) ? UploadMode.simulate : UploadMode.production;
|
||||||
@@ -65,6 +67,7 @@ class SettingsState extends ChangeNotifier {
|
|||||||
int get proximityAlertDistance => _proximityAlertDistance;
|
int get proximityAlertDistance => _proximityAlertDistance;
|
||||||
bool get networkStatusIndicatorEnabled => _networkStatusIndicatorEnabled;
|
bool get networkStatusIndicatorEnabled => _networkStatusIndicatorEnabled;
|
||||||
int get suspectedLocationMinDistance => _suspectedLocationMinDistance;
|
int get suspectedLocationMinDistance => _suspectedLocationMinDistance;
|
||||||
|
bool get keepScreenAwake => _keepScreenAwake;
|
||||||
List<TileProvider> get tileProviders => List.unmodifiable(_tileProviders);
|
List<TileProvider> get tileProviders => List.unmodifiable(_tileProviders);
|
||||||
String get selectedTileTypeId => _selectedTileTypeId;
|
String get selectedTileTypeId => _selectedTileTypeId;
|
||||||
int get navigationAvoidanceDistance => _navigationAvoidanceDistance;
|
int get navigationAvoidanceDistance => _navigationAvoidanceDistance;
|
||||||
@@ -138,6 +141,9 @@ class SettingsState extends ChangeNotifier {
|
|||||||
|
|
||||||
// Load suspected location minimum distance
|
// Load suspected location minimum distance
|
||||||
_suspectedLocationMinDistance = prefs.getInt(_suspectedLocationMinDistancePrefsKey) ?? 100;
|
_suspectedLocationMinDistance = prefs.getInt(_suspectedLocationMinDistancePrefsKey) ?? 100;
|
||||||
|
|
||||||
|
// Load keep screen awake setting
|
||||||
|
_keepScreenAwake = prefs.getBool(_keepScreenAwakePrefsKey) ?? false;
|
||||||
|
|
||||||
// Load upload mode (including migration from old test_mode bool)
|
// Load upload mode (including migration from old test_mode bool)
|
||||||
if (prefs.containsKey(_uploadModePrefsKey)) {
|
if (prefs.containsKey(_uploadModePrefsKey)) {
|
||||||
@@ -397,7 +403,17 @@ class SettingsState extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set distance for avoidance of nodes during navigation
|
/// Set keep screen awake enabled/disabled
|
||||||
|
Future<void> setKeepScreenAwake(bool enabled) async {
|
||||||
|
if (_keepScreenAwake != enabled) {
|
||||||
|
_keepScreenAwake = enabled;
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setBool(_keepScreenAwakePrefsKey, enabled);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set distance for avoidance of nodes during navigation
|
||||||
Future<void> setNavigationAvoidanceDistance(int distance) async {
|
Future<void> setNavigationAvoidanceDistance(int distance) async {
|
||||||
if (_navigationAvoidanceDistance != distance) {
|
if (_navigationAvoidanceDistance != distance) {
|
||||||
_navigationAvoidanceDistance = distance;
|
_navigationAvoidanceDistance = distance;
|
||||||
|
|||||||
@@ -61,6 +61,11 @@ class SuspectedLocationState extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Start background refresh if needed (non-blocking)
|
||||||
|
Future<void> initBackgroundRefresh({bool offlineMode = false}) async {
|
||||||
|
await _service.initBackgroundRefresh(offlineMode: offlineMode);
|
||||||
|
}
|
||||||
|
|
||||||
/// Enable or disable suspected locations
|
/// Enable or disable suspected locations
|
||||||
Future<void> setEnabled(bool enabled) async {
|
Future<void> setEnabled(bool enabled) async {
|
||||||
await _service.setEnabled(enabled);
|
await _service.setEnabled(enabled);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_map_animations/flutter_map_animations.dart';
|
import 'package:flutter_map_animations/flutter_map_animations.dart';
|
||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
|
import 'package:flutter/foundation.dart' show defaultTargetPlatform, TargetPlatform;
|
||||||
|
|
||||||
import '../../dev_config.dart';
|
import '../../dev_config.dart';
|
||||||
import '../../app_state.dart' show FollowMeMode;
|
import '../../app_state.dart' show FollowMeMode;
|
||||||
@@ -145,15 +146,21 @@ class GpsController {
|
|||||||
void _startPositionStream() {
|
void _startPositionStream() {
|
||||||
final followMeMode = _getCurrentFollowMeMode?.call() ?? FollowMeMode.off;
|
final followMeMode = _getCurrentFollowMeMode?.call() ?? FollowMeMode.off;
|
||||||
final distanceFilter = followMeMode == FollowMeMode.off ? 5 : 1; // 5m normal, 1m follow-me
|
final distanceFilter = followMeMode == FollowMeMode.off ? 5 : 1; // 5m normal, 1m follow-me
|
||||||
|
|
||||||
debugPrint('[GpsController] Starting GPS position stream (${distanceFilter}m filter)');
|
debugPrint('[GpsController] Starting GPS position stream (${distanceFilter}m filter)');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
_positionSub = Geolocator.getPositionStream(
|
_positionSub = Geolocator.getPositionStream(
|
||||||
locationSettings: LocationSettings(
|
locationSettings: defaultTargetPlatform == TargetPlatform.android
|
||||||
accuracy: LocationAccuracy.high, // Request best, accept what we get
|
? AndroidSettings(
|
||||||
distanceFilter: distanceFilter,
|
accuracy: LocationAccuracy.high,
|
||||||
),
|
distanceFilter: distanceFilter,
|
||||||
|
forceLocationManager: true,
|
||||||
|
)
|
||||||
|
: LocationSettings(
|
||||||
|
accuracy: LocationAccuracy.high,
|
||||||
|
distanceFilter: distanceFilter,
|
||||||
|
),
|
||||||
).listen(
|
).listen(
|
||||||
_onPositionReceived,
|
_onPositionReceived,
|
||||||
onError: _onPositionError,
|
onError: _onPositionError,
|
||||||
@@ -358,4 +365,4 @@ class GpsController {
|
|||||||
_onMapMovedProgrammatically = null;
|
_onMapMovedProgrammatically = null;
|
||||||
_isUserInteracting = null;
|
_isUserInteracting = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ import 'package:flutter_map/flutter_map.dart';
|
|||||||
import 'package:flutter_map_animations/flutter_map_animations.dart';
|
import 'package:flutter_map_animations/flutter_map_animations.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||||
|
|
||||||
import '../app_state.dart' show AppState, FollowMeMode;
|
import '../app_state.dart' show AppState, FollowMeMode;
|
||||||
import '../services/offline_area_service.dart';
|
import '../services/offline_area_service.dart';
|
||||||
@@ -42,6 +43,7 @@ class MapView extends StatefulWidget {
|
|||||||
this.onSearchPressed,
|
this.onSearchPressed,
|
||||||
this.onNodeLimitChanged,
|
this.onNodeLimitChanged,
|
||||||
this.onLocationStatusChanged,
|
this.onLocationStatusChanged,
|
||||||
|
this.onMapLongPress,
|
||||||
});
|
});
|
||||||
|
|
||||||
final FollowMeMode followMeMode;
|
final FollowMeMode followMeMode;
|
||||||
@@ -53,6 +55,7 @@ class MapView extends StatefulWidget {
|
|||||||
final VoidCallback? onSearchPressed;
|
final VoidCallback? onSearchPressed;
|
||||||
final void Function(bool isLimited)? onNodeLimitChanged;
|
final void Function(bool isLimited)? onNodeLimitChanged;
|
||||||
final VoidCallback? onLocationStatusChanged;
|
final VoidCallback? onLocationStatusChanged;
|
||||||
|
final void Function(LatLng)? onMapLongPress;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<MapView> createState() => MapViewState();
|
State<MapView> createState() => MapViewState();
|
||||||
@@ -84,7 +87,20 @@ class MapViewState extends State<MapView> {
|
|||||||
// Track active pointers to suppress follow-me animations during touch
|
// Track active pointers to suppress follow-me animations during touch
|
||||||
int _activePointers = 0;
|
int _activePointers = 0;
|
||||||
|
|
||||||
|
bool _isWakelockEnabled = false;
|
||||||
|
|
||||||
|
void _updateWakelock(bool shouldBeAwake) {
|
||||||
|
if (_isWakelockEnabled != shouldBeAwake) {
|
||||||
|
_isWakelockEnabled = shouldBeAwake;
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (shouldBeAwake) {
|
||||||
|
WakelockPlus.enable();
|
||||||
|
} else {
|
||||||
|
WakelockPlus.disable();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -213,6 +229,7 @@ class MapViewState extends State<MapView> {
|
|||||||
_nodeController.dispose();
|
_nodeController.dispose();
|
||||||
_tileManager.dispose();
|
_tileManager.dispose();
|
||||||
_gpsController.dispose();
|
_gpsController.dispose();
|
||||||
|
WakelockPlus.disable();
|
||||||
// PrefetchAreaService no longer used - replaced with NodeDataManager
|
// PrefetchAreaService no longer used - replaced with NodeDataManager
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@@ -278,6 +295,9 @@ class MapViewState extends State<MapView> {
|
|||||||
final session = appState.session;
|
final session = appState.session;
|
||||||
final editSession = appState.editSession;
|
final editSession = appState.editSession;
|
||||||
|
|
||||||
|
// Keep screen awake based on user setting
|
||||||
|
_updateWakelock(appState.keepScreenAwake);
|
||||||
|
|
||||||
// Check if enabled profiles changed and refresh nodes if needed
|
// Check if enabled profiles changed and refresh nodes if needed
|
||||||
_nodeController.checkAndHandleProfileChanges(
|
_nodeController.checkAndHandleProfileChanges(
|
||||||
currentEnabledProfiles: appState.enabledProfiles,
|
currentEnabledProfiles: appState.enabledProfiles,
|
||||||
@@ -496,6 +516,15 @@ class MapViewState extends State<MapView> {
|
|||||||
_dataManager.showZoomWarningIfNeeded(context, pos.zoom, appState.uploadMode);
|
_dataManager.showZoomWarningIfNeeded(context, pos.zoom, appState.uploadMode);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onTap: (tapPosition, point) {
|
||||||
|
// Handle tap on empty map area - currently no action needed
|
||||||
|
debugPrint('[MapView] Tap at: $point');
|
||||||
|
},
|
||||||
|
onLongPress: (tapPosition, point) {
|
||||||
|
// Handle long press on empty map area - add node here
|
||||||
|
debugPrint('[MapView] Long press at: $point');
|
||||||
|
widget.onMapLongPress?.call(point);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
_tileManager.buildTileLayer(
|
_tileManager.buildTileLayer(
|
||||||
|
|||||||
+166
-134
@@ -13,10 +13,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: app_links
|
name: app_links
|
||||||
sha256: "3462d9defc61565fde4944858b59bec5be2b9d5b05f20aed190adb3ad08a7abc"
|
sha256: "9d3c82f634c7f5b5c752f7ee46b67724246043f5e1d5fc1b433dd5b38d780dbe"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.0"
|
version: "7.2.0"
|
||||||
app_links_linux:
|
app_links_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -29,10 +29,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: app_links_platform_interface
|
name: app_links_platform_interface
|
||||||
sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f"
|
sha256: "78a18580eecac98108d1eef52a7db668bc317714f5205e616973363326efe333"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.2"
|
version: "2.0.3"
|
||||||
app_links_web:
|
app_links_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -61,10 +61,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: async
|
name: async
|
||||||
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.13.0"
|
version: "2.13.1"
|
||||||
boolean_selector:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -109,10 +109,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: code_assets
|
name: code_assets
|
||||||
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
|
sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
version: "1.2.1"
|
||||||
collection:
|
collection:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -165,18 +165,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: dbus
|
name: dbus
|
||||||
sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270
|
sha256: "0ce9b0a839e6dee59a37a623d2fc26a35bbbe6404213e419b0d6411023d62645"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.12"
|
version: "0.7.14"
|
||||||
desktop_webview_window:
|
desktop_webview_window:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: desktop_webview_window
|
name: desktop_webview_window
|
||||||
sha256: "57cf20d81689d5cbb1adfd0017e96b669398a669d927906073b0e42fc64111c0"
|
sha256: b6fdae2cbf9571879b1761c12f27facaf82e22d0bdc74d049907c2a09a432957
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.3"
|
version: "0.3.0"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@@ -242,34 +242,42 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_local_notifications
|
name: flutter_local_notifications
|
||||||
sha256: "674173fd3c9eda9d4c8528da2ce0ea69f161577495a9cc835a2a4ecd7eadeb35"
|
sha256: "19ffb0a8bb7407875555e5e98d7343a633bb73707bae6c6a5f37c90014077875"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "17.2.4"
|
version: "19.5.0"
|
||||||
flutter_local_notifications_linux:
|
flutter_local_notifications_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_local_notifications_linux
|
name: flutter_local_notifications_linux
|
||||||
sha256: c49bd06165cad9beeb79090b18cd1eb0296f4bf4b23b84426e37dd7c027fc3af
|
sha256: e3c277b2daab8e36ac5a6820536668d07e83851aeeb79c446e525a70710770a5
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.1"
|
version: "6.0.0"
|
||||||
flutter_local_notifications_platform_interface:
|
flutter_local_notifications_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_local_notifications_platform_interface
|
name: flutter_local_notifications_platform_interface
|
||||||
sha256: "85f8d07fe708c1bdcf45037f2c0109753b26ae077e9d9e899d55971711a4ea66"
|
sha256: "277d25d960c15674ce78ca97f57d0bae2ee401c844b6ac80fcd972a9c99d09fe"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.2.0"
|
version: "9.1.0"
|
||||||
|
flutter_local_notifications_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_local_notifications_windows
|
||||||
|
sha256: "8d658f0d367c48bd420e7cf2d26655e2d1130147bca1eea917e576ca76668aaf"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.3"
|
||||||
flutter_map:
|
flutter_map:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_map
|
name: flutter_map
|
||||||
sha256: "391e7dc95cc3f5190748210a69d4cfeb5d8f84dcdfa9c3235d0a9d7742ccb3f8"
|
sha256: "03b71c02806ff20c3718d108cbbb3638142ebafe368d8ce2dd22a33344bcb02b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.2.2"
|
version: "8.3.0"
|
||||||
flutter_map_animations:
|
flutter_map_animations:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -282,34 +290,34 @@ packages:
|
|||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: flutter_native_splash
|
name: flutter_native_splash
|
||||||
sha256: "4fb9f4113350d3a80841ce05ebf1976a36de622af7d19aca0ca9a9911c7ff002"
|
sha256: "9db4b80b044e9af17cc4b1272137fc7ace0054d879ef8210a76adc34aaf4cdff"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.7"
|
version: "2.4.8"
|
||||||
flutter_secure_storage:
|
flutter_secure_storage:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_secure_storage
|
name: flutter_secure_storage
|
||||||
sha256: da922f2aab2d733db7e011a6bcc4a825b844892d4edd6df83ff156b09a9b2e40
|
sha256: "7686b1d6a29985dcbb808c59518226e603e3bfa7c0ddfd1a0d00e4cda77c868e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.0.0"
|
version: "10.3.1"
|
||||||
flutter_secure_storage_darwin:
|
flutter_secure_storage_darwin:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_secure_storage_darwin
|
name: flutter_secure_storage_darwin
|
||||||
sha256: "8878c25136a79def1668c75985e8e193d9d7d095453ec28730da0315dc69aee3"
|
sha256: "82329fa5cdf343773b1b6897dea959105a29f092454259edff92f9f6637e8149"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.0"
|
version: "0.3.2"
|
||||||
flutter_secure_storage_linux:
|
flutter_secure_storage_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_secure_storage_linux
|
name: flutter_secure_storage_linux
|
||||||
sha256: "2b5c76dce569ab752d55a1cee6a2242bcc11fdba927078fb88c503f150767cda"
|
sha256: a5f35ddab43cf5c8215d2feb4ce1957851f28c5c37e6f04335066a0602087bf5
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
version: "3.0.1"
|
||||||
flutter_secure_storage_platform_interface:
|
flutter_secure_storage_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -322,10 +330,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_secure_storage_web
|
name: flutter_secure_storage_web
|
||||||
sha256: "6a1137df62b84b54261dca582c1c09ea72f4f9a4b2fcee21b025964132d5d0c3"
|
sha256: "073a62b3aeb866ab4ce795f960413948e51e5a42a9b0c8333b6daf5bb3208a1c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.1"
|
||||||
flutter_secure_storage_windows:
|
flutter_secure_storage_windows:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -338,10 +346,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_svg
|
name: flutter_svg
|
||||||
sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95"
|
sha256: "35882981abcbfb8c15b286f0cd690ff25bac12d95eff3e25ee207f37d4c42e7f"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.3"
|
version: "2.3.0"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -351,10 +359,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_web_auth_2
|
name: flutter_web_auth_2
|
||||||
sha256: "432ff8c7b2834eaeec3378d99e24a0210b9ac2f453b3f7a7d739a5c09069fba3"
|
sha256: "8f9303471dcd96670878c9b7c0c4e14c37595b2add67465f6a868f17a5872dfc"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.1"
|
version: "5.0.3"
|
||||||
flutter_web_auth_2_platform_interface:
|
flutter_web_auth_2_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -388,18 +396,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: geolocator_apple
|
name: geolocator_apple
|
||||||
sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22
|
sha256: "853803d6bb1713c094e935b4a5ae5f19c0308acf81da13fa9ff84fb4c70c0b73"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.13"
|
version: "2.3.14"
|
||||||
geolocator_platform_interface:
|
geolocator_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: geolocator_platform_interface
|
name: geolocator_platform_interface
|
||||||
sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67"
|
sha256: cdb082e4f048b69da244117b7914cc60d2a8897546ffaa4f2529c786ded7aee2
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.2.6"
|
version: "4.2.8"
|
||||||
geolocator_web:
|
geolocator_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -416,30 +424,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.5"
|
version: "0.2.5"
|
||||||
glob:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: glob
|
|
||||||
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.3"
|
|
||||||
gtk:
|
gtk:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: gtk
|
name: gtk
|
||||||
sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c
|
sha256: "4ff85b2a16724029dd9e5bbb5a94b6918f9973f74ba571c949d2002801879cf5"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.2.0"
|
||||||
hooks:
|
hooks:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: hooks
|
name: hooks
|
||||||
sha256: "7a08a0d684cb3b8fb604b78455d5d352f502b68079f7b80b831c62220ab0a4f6"
|
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.1"
|
version: "2.0.2"
|
||||||
html:
|
html:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -480,14 +480,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.20.2"
|
version: "0.20.2"
|
||||||
|
jni:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: jni
|
||||||
|
sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
|
jni_flutter:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: jni_flutter
|
||||||
|
sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
json_annotation:
|
json_annotation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: json_annotation
|
name: json_annotation
|
||||||
sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8
|
sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.11.0"
|
version: "4.12.0"
|
||||||
latlong2:
|
latlong2:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -536,22 +552,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.0"
|
version: "6.1.0"
|
||||||
lists:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: lists
|
|
||||||
sha256: "4ca5c19ae4350de036a7e996cdd1ee39c93ac0a2b840f4915459b7d0a7d4ab27"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.1"
|
|
||||||
logger:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: logger
|
|
||||||
sha256: a7967e31b703831a893bbc3c3dd11db08126fe5f369b5c648a36f821979f5be3
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.6.2"
|
|
||||||
logging:
|
logging:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -580,34 +580,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.0"
|
version: "1.18.0"
|
||||||
mgrs_dart:
|
mgrs_dart:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: mgrs_dart
|
name: mgrs_dart
|
||||||
sha256: fb89ae62f05fa0bb90f70c31fc870bcbcfd516c843fb554452ab3396f78586f7
|
sha256: "385e7168ecc77eb545220223c49eef8ab249da7bf57f22781c40a04d23fb196f"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "3.0.0"
|
||||||
mocktail:
|
mocktail:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: mocktail
|
name: mocktail
|
||||||
sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8"
|
sha256: "5e1bf53cc7baa8062a33b84424deb61513858ea05c601b8509e683815b5914aa"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.4"
|
version: "1.0.5"
|
||||||
native_toolchain_c:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: native_toolchain_c
|
|
||||||
sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.17.4"
|
|
||||||
nested:
|
nested:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -620,26 +612,34 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: oauth2_client
|
name: oauth2_client
|
||||||
sha256: "6667da827518047d99ce82cf7b23043ea4a4bac99fc6681d4a1bf6ee1dd9579f"
|
sha256: "74d1024ce3bd8c6012d079e075068c94d64d804a40bcb3d4adb7348bf0c278f7"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.2.3"
|
version: "4.3.2"
|
||||||
objective_c:
|
objective_c:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: objective_c
|
name: objective_c
|
||||||
sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52"
|
sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "9.3.0"
|
version: "9.4.1"
|
||||||
|
package_config:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: package_config
|
||||||
|
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
package_info_plus:
|
package_info_plus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: package_info_plus
|
name: package_info_plus
|
||||||
sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d
|
sha256: "468c26b4254ab01979fa5e4a98cb343ea3631b9acee6f21028997419a80e1a20"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "9.0.0"
|
version: "9.0.1"
|
||||||
package_info_plus_platform_interface:
|
package_info_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -668,18 +668,18 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: path_provider
|
name: path_provider
|
||||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.5"
|
version: "2.1.6"
|
||||||
path_provider_android:
|
path_provider_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_android
|
name: path_provider_android
|
||||||
sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e
|
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.22"
|
version: "2.3.1"
|
||||||
path_provider_foundation:
|
path_provider_foundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -700,10 +700,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_platform_interface
|
name: path_provider_platform_interface
|
||||||
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.1.3"
|
||||||
path_provider_windows:
|
path_provider_windows:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -748,10 +748,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: proj4dart
|
name: proj4dart
|
||||||
sha256: c8a659ac9b6864aa47c171e78d41bbe6f5e1d7bd790a5814249e6b68bc44324e
|
sha256: ddcedc1f7876e62717de43ab3491e2829bdad0b028261805f94aa080967e5859
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "3.0.0"
|
||||||
provider:
|
provider:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -776,22 +776,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.1"
|
version: "2.3.1"
|
||||||
|
record_use:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: record_use
|
||||||
|
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.0"
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: shared_preferences
|
name: shared_preferences
|
||||||
sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64"
|
sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.4"
|
version: "2.5.5"
|
||||||
shared_preferences_android:
|
shared_preferences_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_android
|
name: shared_preferences_android
|
||||||
sha256: cbc40be9be1c5af4dab4d6e0de4d5d3729e6f3d65b89d21e1815d57705644a6f
|
sha256: "93ae5884a9df5d3bb696825bceb3a17590754548b5d740eba51500afc8d088f5"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.20"
|
version: "2.4.26"
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -812,10 +820,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_platform_interface
|
name: shared_preferences_platform_interface
|
||||||
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
|
sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.1"
|
version: "2.4.2"
|
||||||
shared_preferences_web:
|
shared_preferences_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -832,6 +840,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.1"
|
version: "2.4.1"
|
||||||
|
simple_sparse_list:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: simple_sparse_list
|
||||||
|
sha256: aa648fd240fa39b49dcd11c19c266990006006de6699a412de485695910fbc1f
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.4"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -849,42 +865,42 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: sqflite
|
name: sqflite
|
||||||
sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03
|
sha256: "58a799e6ac17dd32fbab93813d39ed835a75ccc0f8f85b8955fe318c6712b082"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.2"
|
version: "2.4.3"
|
||||||
sqflite_android:
|
sqflite_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: sqflite_android
|
name: sqflite_android
|
||||||
sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88
|
sha256: d0548f9d7422a2dae99ec6f8b0a3074463b132d216fa5ba0d230eeefc901983b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.2+2"
|
version: "2.4.3"
|
||||||
sqflite_common:
|
sqflite_common:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: sqflite_common
|
name: sqflite_common
|
||||||
sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6"
|
sha256: "5bf6a55c166e73bf651ba7ec3ed486e577620e3dc8f3a9c6a258a8031b624590"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.6"
|
version: "2.5.11"
|
||||||
sqflite_darwin:
|
sqflite_darwin:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: sqflite_darwin
|
name: sqflite_darwin
|
||||||
sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3"
|
sha256: "164a5d73ab87a134566057219988bafde837029a64264e61f1f04376ef3cfcd2"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.2"
|
version: "2.4.3"
|
||||||
sqflite_platform_interface:
|
sqflite_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: sqflite_platform_interface
|
name: sqflite_platform_interface
|
||||||
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
|
sha256: f84939f84350d92d04416f8bc4dc52d3896aec7716cc9e80cf0146342139dc50
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.0"
|
version: "2.4.1"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -913,10 +929,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: synchronized
|
name: synchronized
|
||||||
sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0
|
sha256: "93b153dcb6a26dcddee6ca087dd634b53e38c10b5aa163e8e49501a776456153"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.4.0"
|
version: "3.4.1"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -929,18 +945,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.10"
|
version: "0.7.11"
|
||||||
timezone:
|
timezone:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: timezone
|
name: timezone
|
||||||
sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d"
|
sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.9.4"
|
version: "0.10.1"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -953,10 +969,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: unicode
|
name: unicode
|
||||||
sha256: "0f69e46593d65245774d4f17125c6084d2c20b4e473a983f6e21b7d7762218f1"
|
sha256: a6f7bcfc8ea1d5ce1f6c0b1c39117a9919f4953edd9fd7a64090a9796c499b57
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.1"
|
version: "1.1.9"
|
||||||
universal_io:
|
universal_io:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -977,10 +993,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_android
|
name: url_launcher_android
|
||||||
sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611"
|
sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.3.28"
|
version: "6.3.32"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1017,10 +1033,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_web
|
name: url_launcher_web
|
||||||
sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f
|
sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.2"
|
version: "2.4.3"
|
||||||
url_launcher_windows:
|
url_launcher_windows:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1041,10 +1057,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vector_graphics
|
name: vector_graphics
|
||||||
sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6
|
sha256: "2306c03da2ba81724afeb589c351ebbc0aa7d86005925be8f8735856dbe5e42d"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.19"
|
version: "1.2.2"
|
||||||
vector_graphics_codec:
|
vector_graphics_codec:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1057,10 +1073,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vector_graphics_compiler
|
name: vector_graphics_compiler
|
||||||
sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74"
|
sha256: "142a9146f447d15b10bdc00e21d5f4d83e5b32bb5f8f8f5a04c75311344923a3"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "1.2.6"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1073,10 +1089,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vm_service
|
name: vm_service
|
||||||
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
|
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "15.0.2"
|
version: "15.2.0"
|
||||||
|
wakelock_plus:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: wakelock_plus
|
||||||
|
sha256: ddf3db70eaa10c37558ff817519b85d527dbd21034fd5d8e1c2e85f31588f1c1
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.5.2"
|
||||||
|
wakelock_plus_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: wakelock_plus_platform_interface
|
||||||
|
sha256: b13f99e992e7ae6a152e16c5559d3c07ff445b13330192662494e614ca3e7d7b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.5.1"
|
||||||
web:
|
web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1097,10 +1129,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: window_to_front
|
name: window_to_front
|
||||||
sha256: "7aef379752b7190c10479e12b5fd7c0b9d92adc96817d9e96c59937929512aee"
|
sha256: "14fad8984db4415e2eeb30b04bb77140b180e260d6cb66b26de126a8657a9241"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.0.3"
|
version: "0.0.4"
|
||||||
wkt_parser:
|
wkt_parser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1134,5 +1166,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.3"
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.10.3 <4.0.0"
|
dart: ">=3.12.0 <4.0.0"
|
||||||
flutter: ">=3.38.4"
|
flutter: ">=3.44.0"
|
||||||
+4
-2
@@ -1,10 +1,11 @@
|
|||||||
name: deflockapp
|
name: deflockapp
|
||||||
description: Map public surveillance infrastructure with OpenStreetMap
|
description: Map public surveillance infrastructure with OpenStreetMap
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
version: 2.9.2+53 # The thing after the + is the version code, incremented with each release
|
version: 2.10.2+57 # The thing after the + is the version code, incremented with each release
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.10.3 <4.0.0" # Resolved dependency floor (Dart 3.10.3 = Flutter 3.38+)
|
sdk: ">=3.10.3 <4.0.0" # Resolved dependency floor (Dart 3.10.3 = Flutter 3.38+)
|
||||||
|
flutter: ">=3.44.0" # Mandate Flutter 3.44.0 for SPM default
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
@@ -19,7 +20,7 @@ dependencies:
|
|||||||
http: ^1.2.1
|
http: ^1.2.1
|
||||||
flutter_svg: ^2.0.10
|
flutter_svg: ^2.0.10
|
||||||
xml: ^6.4.2
|
xml: ^6.4.2
|
||||||
flutter_local_notifications: ^17.2.2
|
flutter_local_notifications: ^19.0.0
|
||||||
url_launcher: ^6.3.0
|
url_launcher: ^6.3.0
|
||||||
flutter_linkify: ^6.0.0
|
flutter_linkify: ^6.0.0
|
||||||
app_links: ^7.0.0
|
app_links: ^7.0.0
|
||||||
@@ -38,6 +39,7 @@ dependencies:
|
|||||||
package_info_plus: ^9.0.0
|
package_info_plus: ^9.0.0
|
||||||
csv: ^6.0.0
|
csv: ^6.0.0
|
||||||
collection: ^1.18.0
|
collection: ^1.18.0
|
||||||
|
wakelock_plus: ^1.5.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Node deep link URL parsing', () {
|
||||||
|
test('extracts node ID from valid link', () {
|
||||||
|
final uri = Uri.parse('deflockapp://node?id=1234567890');
|
||||||
|
expect(uri.host, 'node');
|
||||||
|
expect(uri.queryParameters['id'], '1234567890');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null for missing id param', () {
|
||||||
|
final uri = Uri.parse('deflockapp://node');
|
||||||
|
expect(uri.queryParameters['id'], isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null for empty id param', () {
|
||||||
|
final uri = Uri.parse('deflockapp://node?id=');
|
||||||
|
expect(uri.queryParameters['id'], '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null for non-numeric id', () {
|
||||||
|
final uri = Uri.parse('deflockapp://node?id=abc');
|
||||||
|
expect(int.tryParse(uri.queryParameters['id'] ?? ''), isNull);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -221,13 +221,13 @@ void main() {
|
|||||||
// Initial order should be A, B, C
|
// Initial order should be A, B, C
|
||||||
expect(profileState.profiles.map((p) => p.id), equals(['a', 'b', 'c']));
|
expect(profileState.profiles.map((p) => p.id), equals(['a', 'b', 'c']));
|
||||||
|
|
||||||
// Move profile at index 0 (A) to index 2 (should become B, A, C due to Flutter's reorder logic)
|
// Move profile at index 0 (A) to index 2 (with onReorderItem, newIndex is pre-adjusted)
|
||||||
profileState.reorderProfiles(0, 2);
|
profileState.reorderProfiles(0, 2);
|
||||||
expect(profileState.profiles.map((p) => p.id), equals(['b', 'a', 'c']));
|
expect(profileState.profiles.map((p) => p.id), equals(['b', 'c', 'a']));
|
||||||
|
|
||||||
// Move profile at index 1 (A) to index 0 (should become A, B, C)
|
// Move profile at index 1 (C) to index 0 (should become C, B, A)
|
||||||
profileState.reorderProfiles(1, 0);
|
profileState.reorderProfiles(1, 0);
|
||||||
expect(profileState.profiles.map((p) => p.id), equals(['a', 'b', 'c']));
|
expect(profileState.profiles.map((p) => p.id), equals(['c', 'b', 'a']));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should maintain enabled status after reordering', () {
|
test('should maintain enabled status after reordering', () {
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ void main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
group('query building', () {
|
group('query building', () {
|
||||||
test('uses out skel for way/relation pass, out body for node pass',
|
test('uses out ids for way/relation pass, out body for node pass',
|
||||||
() async {
|
() async {
|
||||||
stubOverpassResponse([]);
|
stubOverpassResponse([]);
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ void main() {
|
|||||||
|
|
||||||
final query = (captured.last as Map<String, String>)['data']!;
|
final query = (captured.last as Map<String, String>)['data']!;
|
||||||
expect(query, contains('out body;'));
|
expect(query, contains('out body;'));
|
||||||
expect(query, contains('out skel;'));
|
expect(query, contains('out ids;'));
|
||||||
expect(query, isNot(contains('out meta;')));
|
expect(query, isNot(contains('out meta;')));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -99,6 +99,129 @@ void main() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('profile deduplication (subsumption)', () {
|
||||||
|
test('a specific profile subsumed by a generic one is dropped', () async {
|
||||||
|
final generic = NodeProfile(
|
||||||
|
id: 'generic-alpr',
|
||||||
|
name: 'Generic ALPR',
|
||||||
|
tags: const {
|
||||||
|
'man_made': 'surveillance',
|
||||||
|
'surveillance:type': 'ALPR',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final specific = NodeProfile(
|
||||||
|
id: 'flock',
|
||||||
|
name: 'Flock',
|
||||||
|
tags: const {
|
||||||
|
'man_made': 'surveillance',
|
||||||
|
'surveillance': 'public',
|
||||||
|
'surveillance:type': 'ALPR',
|
||||||
|
'surveillance:zone': 'traffic',
|
||||||
|
'camera:type': 'fixed',
|
||||||
|
'manufacturer': 'Flock Safety',
|
||||||
|
'manufacturer:wikidata': 'Q108485435',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
stubOverpassResponse([]);
|
||||||
|
|
||||||
|
await service.fetchNodes(bounds: bounds, profiles: [generic, specific]);
|
||||||
|
|
||||||
|
final captured = verify(
|
||||||
|
() => mockClient.post(any(), body: captureAny(named: 'body')),
|
||||||
|
).captured;
|
||||||
|
final query = (captured.last as Map<String, String>)['data']!;
|
||||||
|
|
||||||
|
// Only the generic clause should remain.
|
||||||
|
expect(query, contains('["man_made"="surveillance"]["surveillance:type"="ALPR"]'));
|
||||||
|
expect(query, isNot(contains('manufacturer')));
|
||||||
|
expect(query, isNot(contains('Flock Safety')));
|
||||||
|
|
||||||
|
// Only one node clause total.
|
||||||
|
final nodeClauseCount = RegExp(r'node\[').allMatches(query).length;
|
||||||
|
expect(nodeClauseCount, equals(1));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('unrelated profiles are all kept', () async {
|
||||||
|
final alprGeneric = NodeProfile(
|
||||||
|
id: 'generic-alpr',
|
||||||
|
name: 'Generic ALPR',
|
||||||
|
tags: const {
|
||||||
|
'man_made': 'surveillance',
|
||||||
|
'surveillance:type': 'ALPR',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final gunshotGeneric = NodeProfile(
|
||||||
|
id: 'generic-gunshot',
|
||||||
|
name: 'Generic Gunshot Detector',
|
||||||
|
tags: const {
|
||||||
|
'man_made': 'surveillance',
|
||||||
|
'surveillance:type': 'gunshot_detector',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
stubOverpassResponse([]);
|
||||||
|
|
||||||
|
await service.fetchNodes(bounds: bounds, profiles: [alprGeneric, gunshotGeneric]);
|
||||||
|
|
||||||
|
final captured = verify(
|
||||||
|
() => mockClient.post(any(), body: captureAny(named: 'body')),
|
||||||
|
).captured;
|
||||||
|
final query = (captured.last as Map<String, String>)['data']!;
|
||||||
|
|
||||||
|
expect(query, contains('["surveillance:type"="ALPR"]'));
|
||||||
|
expect(query, contains('["surveillance:type"="gunshot_detector"]'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('full default profile set reduces to the two generic clauses', () async {
|
||||||
|
final profiles = NodeProfile.getDefaults();
|
||||||
|
|
||||||
|
stubOverpassResponse([]);
|
||||||
|
|
||||||
|
await service.fetchNodes(bounds: bounds, profiles: profiles);
|
||||||
|
|
||||||
|
final captured = verify(
|
||||||
|
() => mockClient.post(any(), body: captureAny(named: 'body')),
|
||||||
|
).captured;
|
||||||
|
final query = (captured.last as Map<String, String>)['data']!;
|
||||||
|
|
||||||
|
final nodeClauseCount = RegExp(r'node\[').allMatches(query).length;
|
||||||
|
expect(nodeClauseCount, equals(2));
|
||||||
|
|
||||||
|
expect(query, contains('["man_made"="surveillance"]["surveillance:type"="ALPR"]'));
|
||||||
|
expect(query, contains('["man_made"="surveillance"]["surveillance:type"="gunshot_detector"]'));
|
||||||
|
// Brand-specific tags should not appear anywhere in the reduced query.
|
||||||
|
expect(query, isNot(contains('manufacturer')));
|
||||||
|
expect(query, isNot(contains('ShotSpotter')));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('profiles with only empty tags do not subsume or break the query', () async {
|
||||||
|
final emptyTagsProfile = NodeProfile(
|
||||||
|
id: 'empty',
|
||||||
|
name: 'Empty',
|
||||||
|
tags: const {'camera:mount': ''},
|
||||||
|
);
|
||||||
|
final normalProfile = NodeProfile(
|
||||||
|
id: 'normal',
|
||||||
|
name: 'Normal',
|
||||||
|
tags: const {'man_made': 'surveillance'},
|
||||||
|
);
|
||||||
|
|
||||||
|
stubOverpassResponse([]);
|
||||||
|
|
||||||
|
await service.fetchNodes(bounds: bounds, profiles: [emptyTagsProfile, normalProfile]);
|
||||||
|
|
||||||
|
final captured = verify(
|
||||||
|
() => mockClient.post(any(), body: captureAny(named: 'body')),
|
||||||
|
).captured;
|
||||||
|
final query = (captured.last as Map<String, String>)['data']!;
|
||||||
|
|
||||||
|
expect(query, contains('["man_made"="surveillance"]'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
group('response parsing — constraint detection', () {
|
group('response parsing — constraint detection', () {
|
||||||
test('nodes referenced by a way are constrained', () async {
|
test('nodes referenced by a way are constrained', () async {
|
||||||
stubOverpassResponse([
|
stubOverpassResponse([
|
||||||
@@ -255,27 +378,28 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('response with "timeout" throws NodeLimitError', () async {
|
test('response with "timeout" throws NetworkError (not split)', () async {
|
||||||
stubErrorResponse(400, 'runtime error: timeout in query execution');
|
stubErrorResponse(400, 'runtime error: timeout in query execution');
|
||||||
|
|
||||||
await expectLater(
|
await expectLater(
|
||||||
() => service.fetchNodes(
|
() => service.fetchNodes(
|
||||||
bounds: bounds, profiles: profiles, policy: const ResiliencePolicy(maxRetries: 0)),
|
bounds: bounds, profiles: profiles, policy: const ResiliencePolicy(maxRetries: 0)),
|
||||||
throwsA(isA<NodeLimitError>()),
|
throwsA(isA<NetworkError>()),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('response with "runtime limit exceeded" throws NodeLimitError',
|
test('response with "runtime limit exceeded" throws NetworkError (not split)',
|
||||||
() async {
|
() async {
|
||||||
stubErrorResponse(400, 'runtime limit exceeded');
|
stubErrorResponse(400, 'runtime limit exceeded');
|
||||||
|
|
||||||
await expectLater(
|
await expectLater(
|
||||||
() => service.fetchNodes(
|
() => service.fetchNodes(
|
||||||
bounds: bounds, profiles: profiles, policy: const ResiliencePolicy(maxRetries: 0)),
|
bounds: bounds, profiles: profiles, policy: const ResiliencePolicy(maxRetries: 0)),
|
||||||
throwsA(isA<NodeLimitError>()),
|
throwsA(isA<NetworkError>()),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
test('HTTP 429 throws RateLimitError', () async {
|
test('HTTP 429 throws RateLimitError', () async {
|
||||||
stubErrorResponse(429, 'Too Many Requests');
|
stubErrorResponse(429, 'Too Many Requests');
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user