Compare commits

...

4 Commits

Author SHA1 Message Date
stopflock
ee26576c5e Update changelog 2025-11-21 16:51:35 -06:00
stopflock
d6419d5b7c Turn off dev mode 2025-11-21 16:43:40 -06:00
stopflock
026ece2e29 Update roadmap, bump version to 1.4.3, changelog still needs de-vibing 2025-11-21 15:42:32 -06:00
stopflock
3c996c78c9 Two nodes too close together warning 2025-11-21 15:35:12 -06:00
16 changed files with 388 additions and 43 deletions

View File

@@ -98,24 +98,21 @@ cp lib/keys.dart.example lib/keys.dart
## Roadmap
### Needed Bugfixes
- Update node cache to reflect cleared queue entries
- Decide what to do for extracting nodes attached to a way/relation:
- Auto extract (how?)
- Leave it alone (wrong answer unless user chooses intentionally)
- Manual cleanup (cognitive load for users)
- Delete the old one (also wrong answer unless user chooses intentionally)
- Give multiple of these options??
- Two nodes too close together warning
- Nav start+end too close together error (warning + disable submit button?)
- Improve/retune tile fetching backoff/retry
- Disable deletes on nodes belonging to ways/relations
- Support FOV range notation: 0-360, 90-270, 10-45;90-125
- Add some builtin satellite tile provider
- Link to "my changes" on osm (username edit history)
- Are offline areas preferred for fast loading even when online? Check working.
- Fix network indicator - only done when fetch queue is empty!
### Current Development
- Persistent cache for MY submissions: clean up when we see that node appear in overpass/OSM results or when older than 24h
- Persistent cache for MY submissions: assume submissions worked, cache,clean up when we see that node appear in overpass/OSM results or when older than 24h
- Dropdown on "refine tags" page to select acceptable options for camera:mount= (is this a boolean property of a profile?)
- Tutorial / info guide before submitting first node, info and links before creating first profile
- Option to pull in profiles from NSI (man_made=surveillance only?)

View File

@@ -1,30 +1,24 @@
{
"1.4.3": {
"content": [
"• NEW: Proximity warning when placing nodes too close together - prevents accidental duplicate submissions"
]
},
"1.4.2": {
"content": [
"• NEW: Dedicated 'Upload Queue' page - queue items are now shown in a proper list view instead of a popup",
"• NEW: 'Clear Upload Queue' button is always visible at the top of queue page, greyed out when empty",
"• NEW: 'OpenStreetMap Account' page for managing OSM login and account settings",
"• NEW: 'View My Edits on OSM' button takes you directly to your edit history on OpenStreetMap",
"• IMPROVED: Settings page organization with dedicated pages for upload management and OSM account",
"• IMPROVED: Better empty queue state with helpful messaging",
"• UX: Cleaner settings page layout with auth and queue sections moved to their own dedicated pages",
"• UX: Added informational content about OpenStreetMap on the account page"
"• NEW: 'View My Edits on OSM' button takes you directly to your edit history on OpenStreetMap"
]
},
"1.4.1": {
"content": [
"• NEW: 'Extract node from way/relation' option for constrained nodes",
"• When editing nodes that are part of ways or relations, you can now check 'Extract node from way' to create a new node with the same tags at a new location",
"• This preserves the original node in its way/relation while creating an independent copy that can be moved freely",
"• Useful for cases where surveillance equipment has been relocated but the original node must remain for mapping accuracy",
"• Extraction creates a separate OSM changeset and node, leaving the original node untouched"
"• NEW: 'Extract node from way/relation' option for constrained nodes (currently disabled while we decide what that means)"
]
},
"1.4.0": {
"content": [
"• IMPROVED: Advanced editing options now only show apps available on your platform (iOS/Android)",
"• IMPROVED: When an OSM editor app isn't installed, automatically redirect to the appropriate app store",
"• IMPROVED: Better error handling for external editor launches with app store fallback",
"• Supported editors: Vespucci (Android), StreetComplete (Android), EveryDoor (both), Go Map!! (iOS)",
"• Web editors (iD, RapiD) remain available on all platforms as before"
]
@@ -33,27 +27,17 @@
"content": [
"• NEW: 'Pause Upload Queue' toggle in Offline Settings - stops uploads while keeping live data access",
"• Useful for metered connections or when you want to batch uploads later",
"• Upload queue is now disabled if either full offline mode OR pause queue processing is enabled",
"• FIXED: Sheet buttons now remain visible when rotating from portrait to landscape mode",
"• FIXED: Sheets now properly resize when rotating between orientations without requiring user interaction",
"• IMPROVED: Tag list height adapts automatically for landscape orientation to prevent covering map",
"• IMPROVED: Sheets with few tags now shrink to appropriate size rather than maintaining fixed height",
"• IMPROVED: More reliable sheet layout using proper flexible height constraints",
"• CLEANED: Fixed minor code formatting inconsistencies"
"• FIXED: Sheets now resize when rotating between orientations"
]
},
"1.3.3": {
"content": [
"• UX: Edits re-enabled. Only nodes which are part of ways/relations cannot be moved",
"• NEW: Added builtin surveillance device profiles for Rekor and Axis Communications ALPR cameras",
"• Both profiles include proper OSM tags for manufacturer identification and require direction setting",
"• NEW: Advanced editing options - access iD Editor, RapiD, Vespucci, StreetComplete, and other OSM editors",
"• NEW: 'View on OSM' links to see nodes directly on OpenStreetMap website",
"• UX: Constrained nodes (part of ways/relations) cannot be moved to prevent data corruption",
"• UX: Auto-clickable URLs in all tag values - any URL becomes a tappable link",
"• UX: Tag lists now scroll with max height to keep buttons and map visible",
"• UX: Improved button layout on mobile with two rows for better accessibility",
"• UX: Localized network status messages in all supported languages",
"• FIXED: Duplicate changelog service calls eliminated"
"• UX: Tag lists now scroll with max height to keep buttons and map visible"
]
},
"1.3.2": {

View File

@@ -17,6 +17,8 @@ import 'services/changelog_service.dart';
import 'services/operator_profile_service.dart';
import 'services/profile_service.dart';
import 'widgets/camera_provider_with_cache.dart';
import 'widgets/proximity_warning_dialog.dart';
import 'dev_config.dart';
import 'state/auth_state.dart';
import 'state/navigation_state.dart';
import 'state/operator_profile_state.dart';

View File

@@ -57,7 +57,7 @@ const String kClientName = 'DeFlock';
const String kSuspectedLocationsCsvUrl = 'https://stopflock.com/app/flock_utilities_mini_latest.csv';
// Development/testing features - set to false for production builds
const bool kEnableDevelopmentModes = true; // Set to false to hide sandbox/simulate modes and force production mode
const bool kEnableDevelopmentModes = false; // Set to false to hide sandbox/simulate modes and force production mode
// Navigation features - set to false to hide navigation UI elements while in development
const bool kEnableNavigationFeatures = kEnableDevelopmentModes; // Hide navigation until fully implemented
@@ -112,6 +112,9 @@ const int kProximityAlertMinDistance = 50; // meters
const int kProximityAlertMaxDistance = 1600; // meters
const Duration kProximityAlertCooldown = Duration(minutes: 10); // Cooldown between alerts for same node
// Node proximity warning configuration (for new/edited nodes that are too close to existing ones)
const double kNodeProximityWarningDistance = 15.0; // meters - distance threshold to show warning
// Map interaction configuration
const double kNodeDoubleTapZoomDelta = 1.0; // How much to zoom in when double-tapping nodes (was 1.0)
const double kScrollWheelVelocity = 0.01; // Mouse scroll wheel zoom speed (default 0.005)

View File

@@ -21,6 +21,24 @@
"advanced": "Erweitert",
"useAdvancedEditor": "Erweiterten Editor verwenden"
},
"proximityWarning": {
"title": "Knoten sehr nah an vorhandenem Gerät",
"message": "Dieser Knoten ist nur {} Meter von einem vorhandenen Überwachungsgerät entfernt.",
"suggestion": "Wenn mehrere Geräte am selben Mast sind, verwenden Sie bitte mehrere Richtungen auf einem einzigen Knoten, anstatt separate Knoten zu erstellen.",
"nearbyNodes": "Nahegelegene Gerät(e) gefunden ({}):",
"nodeInfo": "Knoten #{} - {}",
"andMore": "...und {} weitere",
"goBack": "Zurück",
"submitAnyway": "Trotzdem senden",
"nodeType": {
"alpr": "ALPR/ANPR Kamera",
"publicCamera": "Öffentliche Überwachungskamera",
"camera": "Überwachungskamera",
"amenity": "{}",
"device": "{} Gerät",
"unknown": "Unbekanntes Gerät"
}
},
"followMe": {
"off": "Verfolgung aktivieren",
"follow": "Verfolgung aktivieren (Rotation)",

View File

@@ -39,6 +39,24 @@
"advanced": "Advanced",
"useAdvancedEditor": "Use Advanced Editor"
},
"proximityWarning": {
"title": "Node Very Close to Existing Device",
"message": "This node is only {} meters from an existing surveillance device.",
"suggestion": "If multiple devices are on the same pole, please use multiple directions on a single node instead of creating separate nodes.",
"nearbyNodes": "Nearby device(s) found ({}):",
"nodeInfo": "Node #{} - {}",
"andMore": "...and {} more",
"goBack": "Go Back",
"submitAnyway": "Submit Anyway",
"nodeType": {
"alpr": "ALPR/ANPR Camera",
"publicCamera": "Public Surveillance Camera",
"camera": "Surveillance Camera",
"amenity": "{}",
"device": "{} Device",
"unknown": "Unknown Device"
}
},
"followMe": {
"off": "Enable follow-me",
"follow": "Enable follow-me (rotating)",

View File

@@ -39,6 +39,24 @@
"advanced": "Avanzado",
"useAdvancedEditor": "Usar Editor Avanzado"
},
"proximityWarning": {
"title": "Nodo Muy Cerca de Dispositivo Existente",
"message": "Este nodo está a solo {} metros de un dispositivo de vigilancia existente.",
"suggestion": "Si hay múltiples dispositivos en el mismo poste, use múltiples direcciones en un solo nodo en lugar de crear nodos separados.",
"nearbyNodes": "Dispositivo(s) cercano(s) encontrado(s) ({}):",
"nodeInfo": "Nodo #{} - {}",
"andMore": "...y {} más",
"goBack": "Volver",
"submitAnyway": "Enviar de Todas Formas",
"nodeType": {
"alpr": "Cámara ALPR/ANPR",
"publicCamera": "Cámara de Vigilancia Pública",
"camera": "Cámara de Vigilancia",
"amenity": "{}",
"device": "Dispositivo {}",
"unknown": "Dispositivo Desconocido"
}
},
"followMe": {
"off": "Activar seguimiento",
"follow": "Activar seguimiento (rotación)",

View File

@@ -39,6 +39,24 @@
"advanced": "Avancé",
"useAdvancedEditor": "Utiliser l'Éditeur Avancé"
},
"proximityWarning": {
"title": "Nœud Très Proche d'un Dispositif Existant",
"message": "Ce nœud n'est qu'à {} mètres d'un dispositif de surveillance existant.",
"suggestion": "Si plusieurs dispositifs se trouvent sur le même poteau, veuillez utiliser plusieurs directions sur un seul nœud au lieu de créer des nœuds séparés.",
"nearbyNodes": "Dispositif(s) proche(s) trouvé(s) ({}) :",
"nodeInfo": "Nœud #{} - {}",
"andMore": "...et {} de plus",
"goBack": "Retour",
"submitAnyway": "Soumettre Quand Même",
"nodeType": {
"alpr": "Caméra ALPR/ANPR",
"publicCamera": "Caméra de Surveillance Publique",
"camera": "Caméra de Surveillance",
"amenity": "{}",
"device": "Dispositif {}",
"unknown": "Dispositif Inconnu"
}
},
"followMe": {
"off": "Activer le suivi",
"follow": "Activer le suivi (rotation)",

View File

@@ -39,6 +39,24 @@
"advanced": "Avanzato",
"useAdvancedEditor": "Usa Editor Avanzato"
},
"proximityWarning": {
"title": "Nodo Molto Vicino a Dispositivo Esistente",
"message": "Questo nodo è a soli {} metri da un dispositivo di sorveglianza esistente.",
"suggestion": "Se ci sono più dispositivi sullo stesso palo, utilizzare più direzioni su un singolo nodo invece di creare nodi separati.",
"nearbyNodes": "Dispositivo/i vicino/i trovato/i ({}):",
"nodeInfo": "Nodo #{} - {}",
"andMore": "...e altri {}",
"goBack": "Torna Indietro",
"submitAnyway": "Invia Comunque",
"nodeType": {
"alpr": "Telecamera ALPR/ANPR",
"publicCamera": "Telecamera di Sorveglianza Pubblica",
"camera": "Telecamera di Sorveglianza",
"amenity": "{}",
"device": "Dispositivo {}",
"unknown": "Dispositivo Sconosciuto"
}
},
"followMe": {
"off": "Attiva seguimi",
"follow": "Attiva seguimi (rotazione)",

View File

@@ -39,6 +39,24 @@
"advanced": "Avançado",
"useAdvancedEditor": "Usar Editor Avançado"
},
"proximityWarning": {
"title": "Nó Muito Próximo de Dispositivo Existente",
"message": "Este nó está a apenas {} metros de um dispositivo de vigilância existente.",
"suggestion": "Se vários dispositivos estão no mesmo poste, use várias direções em um único nó em vez de criar nós separados.",
"nearbyNodes": "Dispositivo(s) próximo(s) encontrado(s) ({}):",
"nodeInfo": "Nó #{} - {}",
"andMore": "...e mais {}",
"goBack": "Voltar",
"submitAnyway": "Enviar Mesmo Assim",
"nodeType": {
"alpr": "Câmera ALPR/ANPR",
"publicCamera": "Câmera de Vigilância Pública",
"camera": "Câmera de Vigilância",
"amenity": "{}",
"device": "Dispositivo {}",
"unknown": "Dispositivo Desconhecido"
}
},
"followMe": {
"off": "Ativar seguir-me",
"follow": "Ativar seguir-me (rotação)",

View File

@@ -39,6 +39,24 @@
"advanced": "高级",
"useAdvancedEditor": "使用高级编辑器"
},
"proximityWarning": {
"title": "节点过于靠近现有设备",
"message": "此节点距离现有监控设备仅 {} 米。",
"suggestion": "如果同一根杆上有多个设备,请在单个节点上使用多个方向,而不是创建单独的节点。",
"nearbyNodes": "发现附近设备 ({})",
"nodeInfo": "节点 #{} - {}",
"andMore": "...还有 {} 个",
"goBack": "返回",
"submitAnyway": "仍然提交",
"nodeType": {
"alpr": "ALPR/ANPR 摄像头",
"publicCamera": "公共监控摄像头",
"camera": "监控摄像头",
"amenity": "{}",
"device": "{} 设备",
"unknown": "未知设备"
}
},
"followMe": {
"off": "启用跟随模式",
"follow": "启用跟随模式(旋转)",

View File

@@ -2,6 +2,8 @@ import 'package:latlong2/latlong.dart';
import '../models/osm_node.dart';
import 'package:flutter_map/flutter_map.dart' show LatLngBounds;
const Distance _distance = Distance();
class NodeCache {
// Singleton instance
static final NodeCache instance = NodeCache._internal();
@@ -103,6 +105,34 @@ class NodeCache {
(coord1.longitude - coord2.longitude).abs() < tolerance;
}
/// Find nodes within the specified distance (in meters) of the given coordinate
/// Excludes nodes with the excludeNodeId (useful when checking proximity for edited nodes)
List<OsmNode> findNodesWithinDistance(LatLng coord, double distanceMeters, {int? excludeNodeId}) {
final nearbyNodes = <OsmNode>[];
for (final node in _nodes.values) {
// Skip the excluded node (typically the node being edited)
if (excludeNodeId != null && node.id == excludeNodeId) {
continue;
}
// Skip temporary nodes (negative IDs) with pending upload/edit/deletion markers
if (node.id < 0 && (
node.tags.containsKey('_pending_upload') ||
node.tags.containsKey('_pending_edit') ||
node.tags.containsKey('_pending_deletion'))) {
continue;
}
final distance = _distance.as(LengthUnit.Meter, coord, node.coord);
if (distance <= distanceMeters) {
nearbyNodes.add(node);
}
}
return nearbyNodes;
}
/// Utility: point-in-bounds for coordinates
bool _inBounds(LatLng coord, LatLngBounds bounds) {
return coord.latitude >= bounds.southWest.latitude &&

View File

@@ -6,13 +6,58 @@ import '../dev_config.dart';
import '../models/node_profile.dart';
import '../models/operator_profile.dart';
import '../services/localization_service.dart';
import '../services/node_cache.dart';
import 'refine_tags_sheet.dart';
import 'proximity_warning_dialog.dart';
class AddNodeSheet extends StatelessWidget {
const AddNodeSheet({super.key, required this.session});
final AddNodeSession session;
void _checkProximityAndCommit(BuildContext context, AppState appState, LocalizationService locService) {
// Only check proximity if we have a target location
if (session.target == null) {
_commitWithoutCheck(context, appState, locService);
return;
}
// Check for nearby nodes within the configured distance
final nearbyNodes = NodeCache.instance.findNodesWithinDistance(
session.target!,
kNodeProximityWarningDistance,
);
if (nearbyNodes.isNotEmpty) {
// Show proximity warning dialog
showDialog<void>(
context: context,
builder: (context) => ProximityWarningDialog(
nearbyNodes: nearbyNodes,
distance: kNodeProximityWarningDistance,
onGoBack: () {
Navigator.of(context).pop(); // Close dialog
},
onSubmitAnyway: () {
Navigator.of(context).pop(); // Close dialog
_commitWithoutCheck(context, appState, locService);
},
),
);
} else {
// No nearby nodes, proceed with commit
_commitWithoutCheck(context, appState, locService);
}
}
void _commitWithoutCheck(BuildContext context, AppState appState, LocalizationService locService) {
appState.commitSession();
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(locService.t('node.queuedForUpload'))),
);
}
Widget _buildDirectionControls(BuildContext context, AppState appState, AddNodeSession session, LocalizationService locService) {
final requiresDirection = session.profile != null && session.profile!.requiresDirection;
@@ -144,11 +189,7 @@ class AddNodeSheet extends StatelessWidget {
final appState = context.watch<AppState>();
void _commit() {
appState.commitSession();
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(locService.t('node.queuedForUpload'))),
);
_checkProximityAndCommit(context, appState, locService);
}
void _cancel() {

View File

@@ -6,15 +6,55 @@ import '../dev_config.dart';
import '../models/node_profile.dart';
import '../models/operator_profile.dart';
import '../services/localization_service.dart';
import '../services/node_cache.dart';
import '../state/settings_state.dart';
import 'refine_tags_sheet.dart';
import 'advanced_edit_options_sheet.dart';
import 'proximity_warning_dialog.dart';
class EditNodeSheet extends StatelessWidget {
const EditNodeSheet({super.key, required this.session});
final EditNodeSession session;
void _checkProximityAndCommit(BuildContext context, AppState appState, LocalizationService locService) {
// Check for nearby nodes within the configured distance, excluding the node being edited
final nearbyNodes = NodeCache.instance.findNodesWithinDistance(
session.target,
kNodeProximityWarningDistance,
excludeNodeId: session.originalNode.id,
);
if (nearbyNodes.isNotEmpty) {
// Show proximity warning dialog
showDialog<void>(
context: context,
builder: (context) => ProximityWarningDialog(
nearbyNodes: nearbyNodes,
distance: kNodeProximityWarningDistance,
onGoBack: () {
Navigator.of(context).pop(); // Close dialog
},
onSubmitAnyway: () {
Navigator.of(context).pop(); // Close dialog
_commitWithoutCheck(context, appState, locService);
},
),
);
} else {
// No nearby nodes, proceed with commit
_commitWithoutCheck(context, appState, locService);
}
}
void _commitWithoutCheck(BuildContext context, AppState appState, LocalizationService locService) {
appState.commitEditSession();
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(locService.t('node.editQueuedForUpload'))),
);
}
Widget _buildDirectionControls(BuildContext context, AppState appState, EditNodeSession session, LocalizationService locService) {
final requiresDirection = session.profile != null && session.profile!.requiresDirection;
@@ -146,11 +186,7 @@ class EditNodeSheet extends StatelessWidget {
final appState = context.watch<AppState>();
void _commit() {
appState.commitEditSession();
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(locService.t('node.editQueuedForUpload'))),
);
_checkProximityAndCommit(context, appState, locService);
}
void _cancel() {

View File

@@ -0,0 +1,126 @@
import 'package:flutter/material.dart';
import 'package:latlong2/latlong.dart';
import '../models/osm_node.dart';
import '../services/localization_service.dart';
class ProximityWarningDialog extends StatelessWidget {
final List<OsmNode> nearbyNodes;
final double distance;
final VoidCallback onGoBack;
final VoidCallback onSubmitAnyway;
const ProximityWarningDialog({
super.key,
required this.nearbyNodes,
required this.distance,
required this.onGoBack,
required this.onSubmitAnyway,
});
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: LocalizationService.instance,
builder: (context, child) {
final locService = LocalizationService.instance;
return AlertDialog(
icon: const Icon(
Icons.warning_amber_rounded,
color: Colors.orange,
size: 32,
),
title: Text(locService.t('proximityWarning.title')),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
locService.t('proximityWarning.message',
params: [distance.toStringAsFixed(1)]),
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 16),
Text(
locService.t('proximityWarning.suggestion'),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontStyle: FontStyle.italic,
),
),
const SizedBox(height: 16),
Text(
locService.t('proximityWarning.nearbyNodes',
params: [nearbyNodes.length.toString()]),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
...nearbyNodes.take(3).map((node) => Padding(
padding: const EdgeInsets.only(left: 8.0, bottom: 4.0),
child: Text(
'${locService.t('proximityWarning.nodeInfo', params: [
node.id.toString(),
_getNodeTypeDescription(node, locService),
])}',
style: Theme.of(context).textTheme.bodySmall,
),
)),
if (nearbyNodes.length > 3)
Padding(
padding: const EdgeInsets.only(left: 8.0, top: 4.0),
child: Text(
locService.t('proximityWarning.andMore',
params: [(nearbyNodes.length - 3).toString()]),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontStyle: FontStyle.italic,
),
),
),
],
),
actions: [
TextButton(
onPressed: onGoBack,
child: Text(locService.t('proximityWarning.goBack')),
),
ElevatedButton(
onPressed: onSubmitAnyway,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
foregroundColor: Colors.white,
),
child: Text(locService.t('proximityWarning.submitAnyway')),
),
],
);
},
);
}
String _getNodeTypeDescription(OsmNode node, LocalizationService locService) {
// Try to get a meaningful description from the node's tags
final manMade = node.tags['man_made'];
final amenity = node.tags['amenity'];
final surveillance = node.tags['surveillance'];
final surveillanceType = node.tags['surveillance:type'];
final manufacturer = node.tags['manufacturer'];
if (manMade == 'surveillance') {
if (surveillanceType == 'ALPR' || surveillanceType == 'ANPR') {
return locService.t('proximityWarning.nodeType.alpr');
} else if (surveillance == 'public') {
return locService.t('proximityWarning.nodeType.publicCamera');
} else {
return locService.t('proximityWarning.nodeType.camera');
}
} else if (amenity != null) {
return locService.t('proximityWarning.nodeType.amenity', params: [amenity]);
} else if (manufacturer != null) {
return locService.t('proximityWarning.nodeType.device', params: [manufacturer]);
} else {
return locService.t('proximityWarning.nodeType.unknown');
}
}
}

View File

@@ -1,7 +1,7 @@
name: deflockapp
description: Map public surveillance infrastructure with OpenStreetMap
publish_to: "none"
version: 1.4.1+14 # The thing after the + is the version code, incremented with each release
version: 1.4.3+14 # The thing after the + is the version code, incremented with each release
environment:
sdk: ">=3.5.0 <4.0.0" # oauth2_client 4.x needs Dart 3.5+