diff options
| author | Rishi-k-s <rishikrishna.sr@gmail.com> | 2025-09-20 14:41:51 +0530 |
|---|---|---|
| committer | Rishi-k-s <rishikrishna.sr@gmail.com> | 2025-09-20 14:41:51 +0530 |
| commit | 74eb9c806279bd1bb284fb3021733031752af9ae (patch) | |
| tree | 17ce949d82de6f44556b44b631bf0fa0ce6533b0 | |
| parent | 2979cf18e1f9036ee092859d224e410026d9b4f5 (diff) | |
readme
| -rw-r--r-- | README.md | 139 | ||||
| -rw-r--r-- | lib/main.dart | 66 | ||||
| -rw-r--r-- | lib/models/place_prediction.dart | 12 | ||||
| -rw-r--r-- | lib/services/location_cache_service.dart | 64 | ||||
| -rw-r--r-- | macos/Flutter/GeneratedPluginRegistrant.swift | 2 | ||||
| -rw-r--r-- | pubspec.lock | 104 | ||||
| -rw-r--r-- | pubspec.yaml | 2 |
7 files changed, 363 insertions, 26 deletions
@@ -1,16 +1,137 @@ -# engotta_app +# Engotta (എങ്ങോട്ടാ) - Smart Navigation Companion -A new Flutter project. + + + +## What is Engotta? + +"എങ്ങോട്ടാ" (Engotta) - Malayalam for "Where to?" - is an innovative open hardware navigation solution designed specifically for two-wheeler riders. Born out of the frustration of constantly stopping to check maps and missing turns, Engotta provides a seamless navigation experience through a handlebar-mounted display. + +## Features + +### Companion App +- 🎯 Real-time location tracking +- 🗺️ Intelligent route planning +- 📍 Location search with smart suggestions +- 💾 Recent locations caching +- 📱 User-friendly interface +- 🔄 Current location detection +- 🏃 Performance optimized with debouncing + +### Hardware Component (Coming Soon) +- 📺 Handlebar-mounted display +- 🧭 Turn-by-turn navigation +- 🛠️ Custom PCB design +- 🖨️ 3D printed mounting case +- 🔋 Weather-resistant design +- 📡 Bluetooth connectivity + +## App Screenshots +[Coming Soon] ## Getting Started -This project is a starting point for a Flutter application. +### Prerequisites +- Flutter SDK ^3.8.1 +- Android Studio / VS Code +- Google Maps API Key + +### Installation + +1. Clone the repository +```bash +git clone https://github.com/Rishi-k-s/engotta_app.git +``` + +2. Navigate to project directory +```bash +cd engotta_app +``` + +3. Install dependencies +```bash +flutter pub get +``` + +4. Add your Google Maps API key + - Create `lib/config/api_keys.dart` + - Add your API key: +```dart +class ApiKeys { + static const String googlePlacesApi = 'YOUR_API_KEY'; +} +``` + +5. Run the app +```bash +flutter run +``` + +## Hardware Component + +The hardware component is currently under development. It will include: +- Custom PCB design files +- 3D printable case models +- Assembly instructions +- Component list +- Wiring diagrams + +[Coming Soon] + +## Contributing + +We welcome contributions! Whether it's: +- 🐛 Bug fixes +- ✨ New features +- 📚 Documentation improvements +- 🎨 UI/UX enhancements + +Please read our [Contributing Guidelines](CONTRIBUTING.md) before making a pull request. + +## Roadmap + +- [x] Initial app development +- [x] Location services integration +- [x] Search functionality +- [x] Location caching +- [ ] Hardware prototype +- [ ] Custom PCB design +- [ ] 3D printed case +- [ ] Bluetooth connectivity +- [ ] Turn-by-turn navigation +- [ ] Weather resistance testing + +## Tech Stack + +### App +- Flutter +- Google Maps API +- Geolocator +- SharedPreferences +- HTTP + +### Hardware (Planned) +- Custom PCB +- ESP32 +- LED Display +- 3D Printed Components + +## About the Project + +Engotta was born from a simple yet common problem - the need to check maps repeatedly while riding. As a rider myself, I found it frustrating and potentially dangerous to keep stopping to check directions. This project aims to solve this problem with a simple, effective, and affordable solution. + +The name "എങ്ങോട്ടാ" (Engotta) comes from Malayalam, meaning "Where to?" - a common question we ask fellow riders. It perfectly encapsulates the purpose of this project - helping riders reach their destination safely and efficiently. + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## Contact + +Rishi K S - [@YourTwitter](https://twitter.com/YourTwitter) -A few resources to get you started if this is your first Flutter project: +Project Link: [https://github.com/Rishi-k-s/engotta_app](https://github.com/Rishi-k-s/engotta_app) -- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) +--- -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. +<p align="center">Made with ❤️ for the riding community</p> diff --git a/lib/main.dart b/lib/main.dart index 4c8d838..eb14d61 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'services/places_service.dart'; import 'services/location_service.dart'; +import 'services/location_cache_service.dart'; import 'models/place_prediction.dart'; import 'utils/debouncer.dart'; @@ -41,13 +42,36 @@ class _MapSampleState extends State<MapSample> { final PlacesService _placesService = PlacesService(); final LocationService _locationService = LocationService(); final Debouncer _searchDebouncer = Debouncer(); + late LocationCacheService _locationCacheService; Future<Iterable<PlacePrediction>> _getLocationSuggestions(String query) async { - // Return empty list if query is empty or less than 3 characters - if (query.isEmpty || query.length < 3) { + final normalizedQuery = query.toLowerCase(); + + // Handle empty query - show current location and recent locations + if (query.isEmpty) { + final recentLocations = await _locationCacheService.getCachedLocations(); + return [ + PlacePrediction.currentLocation(), + ...recentLocations, + ]; + } + + // Show current location if query matches + if ('current location'.contains(normalizedQuery)) { + return [PlacePrediction.currentLocation()]; + } + + // For other queries, require minimum 3 characters + if (query.length < 3) { return const Iterable<PlacePrediction>.empty(); } + // Check if query matches any recent locations + final recentLocations = await _locationCacheService.getCachedLocations(); + final matchingRecent = recentLocations.where((location) => + location.mainText.toLowerCase().contains(normalizedQuery) || + location.secondaryText.toLowerCase().contains(normalizedQuery)); + Completer<Iterable<PlacePrediction>> completer = Completer(); _searchDebouncer.call(() async { @@ -58,12 +82,22 @@ class _MapSampleState extends State<MapSample> { longitude: _center.longitude, ); if (!completer.isCompleted) { - completer.complete(predictions); + final results = [ + if ('current location'.contains(normalizedQuery)) + PlacePrediction.currentLocation(), + ...matchingRecent, + ...predictions.where((p) => !matchingRecent.any((r) => r.placeId == p.placeId)) + ]; + completer.complete(results); } } catch (e) { print('Error getting predictions: $e'); if (!completer.isCompleted) { - completer.complete(const Iterable<PlacePrediction>.empty()); + if (matchingRecent.isNotEmpty) { + completer.complete(matchingRecent); + } else { + completer.complete(const Iterable<PlacePrediction>.empty()); + } } } }); @@ -75,7 +109,12 @@ class _MapSampleState extends State<MapSample> { void initState() { super.initState(); _center = LocationService.defaultLocation; - _initializeLocation(); + _initializeServices(); + } + + Future<void> _initializeServices() async { + _locationCacheService = await LocationCacheService.create(); + await _initializeLocation(); } Future<void> _initializeLocation() async { @@ -110,9 +149,20 @@ class _MapSampleState extends State<MapSample> { optionsBuilder: (TextEditingValue textEditingValue) async { return await _getLocationSuggestions(textEditingValue.text); }, - onSelected: (PlacePrediction selection) { - _fromController.text = selection.mainText; - // Here you would typically update the map position + onSelected: (PlacePrediction selection) async { + if (selection.isCurrentLocation) { + final currentLocation = await _locationService.getCurrentLocation(); + setState(() { + _center = currentLocation; + }); + mapController.animateCamera(CameraUpdate.newLatLng(_center)); + _fromController.text = 'Current Location'; + } else { + _fromController.text = selection.mainText; + // Cache the selected location + await _locationCacheService.addToCache(selection); + // Here you would typically update the map position + } }, displayStringForOption: (PlacePrediction option) => '${option.mainText}, ${option.secondaryText}', diff --git a/lib/models/place_prediction.dart b/lib/models/place_prediction.dart index 95fc5c6..2d019d7 100644 --- a/lib/models/place_prediction.dart +++ b/lib/models/place_prediction.dart @@ -3,14 +3,26 @@ class PlacePrediction { final String mainText; final String secondaryText; final List<String> types; + final bool isCurrentLocation; PlacePrediction({ required this.placeId, required this.mainText, required this.secondaryText, required this.types, + this.isCurrentLocation = false, }); + factory PlacePrediction.currentLocation() { + return PlacePrediction( + placeId: 'current_location', + mainText: '📍 Current Location', + secondaryText: 'Use my current location', + types: ['current_location'], + isCurrentLocation: true, + ); + } + factory PlacePrediction.fromJson(Map<String, dynamic> json) { final prediction = json['placePrediction']; return PlacePrediction( diff --git a/lib/services/location_cache_service.dart b/lib/services/location_cache_service.dart new file mode 100644 index 0000000..e914fda --- /dev/null +++ b/lib/services/location_cache_service.dart @@ -0,0 +1,64 @@ +import 'dart:convert'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../models/place_prediction.dart'; + +class LocationCacheService { + static const String _cacheKey = 'recent_locations'; + static const int _maxCacheSize = 5; + final SharedPreferences _prefs; + + LocationCacheService(this._prefs); + + static Future<LocationCacheService> create() async { + final prefs = await SharedPreferences.getInstance(); + return LocationCacheService(prefs); + } + + Future<List<PlacePrediction>> getCachedLocations() async { + final jsonList = _prefs.getStringList(_cacheKey) ?? []; + return jsonList.map((jsonStr) { + final map = json.decode(jsonStr); + return PlacePrediction( + placeId: map['placeId'], + mainText: map['mainText'], + secondaryText: map['secondaryText'], + types: List<String>.from(map['types']), + ); + }).toList(); + } + + Future<void> addToCache(PlacePrediction location) async { + if (location.isCurrentLocation) return; // Don't cache current location option + + final jsonList = _prefs.getStringList(_cacheKey) ?? []; + final locations = jsonList.map((jsonStr) => json.decode(jsonStr)).toList(); + + // Remove if location already exists (to move it to top) + locations.removeWhere((loc) => loc['placeId'] == location.placeId); + + // Add new location at the beginning + locations.insert(0, { + 'placeId': location.placeId, + 'mainText': location.mainText, + 'secondaryText': location.secondaryText, + 'types': location.types, + }); + + // Keep only the most recent locations + if (locations.length > _maxCacheSize) { + locations.removeRange(_maxCacheSize, locations.length); + } + + + + // Save back to preferences + await _prefs.setStringList( + _cacheKey, + locations.map((loc) => json.encode(loc)).toList(), + ); + } + + Future<void> clearCache() async { + await _prefs.remove(_cacheKey); + } +}
\ No newline at end of file diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index d7ee0bc..8679ef3 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,8 +7,10 @@ import Foundation import geolocator_apple import package_info_plus +import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index b928664..2df882c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -81,14 +81,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.11" - dropdown_search: - dependency: "direct main" - description: - name: dropdown_search - sha256: "55106e8290acaa97ed15bea1fdad82c3cf0c248dd410e651f5a8ac6870f783ab" - url: "https://pub.dev" - source: hosted - version: "5.0.6" fake_async: dependency: transitive description: @@ -105,6 +97,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" fixnum: dependency: transitive description: @@ -368,6 +368,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" petitparser: dependency: transitive description: @@ -376,6 +400,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -392,6 +424,62 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: a2608114b1ffdcbc9c120eb71a0e207c71da56202852d4aab8a5e30a82269e74 + url: "https://pub.dev" + source: hosted + version: "2.4.12" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" sky_engine: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 36fbe0f..78b178d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,8 +36,8 @@ dependencies: cupertino_icons: ^1.0.8 google_maps_flutter: ^2.13.1 http: ^1.5.0 - dropdown_search: ^5.0.6 geolocator: ^14.0.2 + shared_preferences: ^2.5.3 dev_dependencies: flutter_test: |
