checkpoint
This commit is contained in:
parent
bbcd6d9bf7
commit
d46ac9dc14
@ -33,6 +33,11 @@
|
|||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
|
<!-- Disable Impeller (Vulkan) rendering for better GPU compatibility -->
|
||||||
|
<!-- Use OpenGL rendering instead, which works better with Mali GPUs -->
|
||||||
|
<meta-data
|
||||||
|
android:name="io.flutter.embedding.android.EnableImpeller"
|
||||||
|
android:value="false" />
|
||||||
</application>
|
</application>
|
||||||
<!-- Required to query activities that can process text, see:
|
<!-- Required to query activities that can process text, see:
|
||||||
https://developer.android.com/training/package-visibility and
|
https://developer.android.com/training/package-visibility and
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
<style name="LaunchTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||||
<!-- Show a splash screen on the activity. Automatically removed when
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
the Flutter engine draws its first frame -->
|
the Flutter engine draws its first frame -->
|
||||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
@ -12,7 +12,7 @@
|
|||||||
running.
|
running.
|
||||||
|
|
||||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
<style name="NormalTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||||
<item name="android:windowBackground">?android:colorBackground</item>
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
295
docs/IMPELLER_MAP_GLITCH.md
Normal file
295
docs/IMPELLER_MAP_GLITCH.md
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
# Impeller Map Rendering Issue Documentation
|
||||||
|
|
||||||
|
## Issue Overview
|
||||||
|
|
||||||
|
The Plan B Logistics Flutter app experiences map rendering glitches when using Flutter's Impeller rendering engine on Android. This requires the app to run with the `--no-enable-impeller` flag to function correctly.
|
||||||
|
|
||||||
|
## Affected Components
|
||||||
|
|
||||||
|
- **Component**: Google Maps Navigation SDK for Flutter
|
||||||
|
- **Platform**: Android (device: KM10 - Android physical device)
|
||||||
|
- **Rendering Engine**: Impeller (Flutter's new rendering backend)
|
||||||
|
- **Symptoms**: Visual glitches and rendering artifacts on the map view
|
||||||
|
|
||||||
|
## Technical Background
|
||||||
|
|
||||||
|
### What is Impeller?
|
||||||
|
|
||||||
|
Impeller is Flutter's next-generation rendering engine designed to replace the Skia backend. It provides:
|
||||||
|
- Predictable performance by precompiling shaders
|
||||||
|
- Reduced jank and frame drops
|
||||||
|
- Better graphics API utilization (Metal on iOS, Vulkan on Android)
|
||||||
|
|
||||||
|
However, Impeller is still being stabilized and some third-party plugins (particularly those with native platform views) may experience compatibility issues.
|
||||||
|
|
||||||
|
### Why Google Maps Has Issues with Impeller
|
||||||
|
|
||||||
|
Google Maps Navigation SDK uses native platform views (SurfaceView on Android) that render the map content. The interaction between:
|
||||||
|
1. Flutter's rendering pipeline (Impeller)
|
||||||
|
2. Native Android views (Platform Views)
|
||||||
|
3. Complex map rendering (Google Maps SDK)
|
||||||
|
|
||||||
|
Can cause rendering glitches, z-index issues, or visual artifacts.
|
||||||
|
|
||||||
|
## Current Workaround
|
||||||
|
|
||||||
|
### Running with Impeller Disabled
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```bash
|
||||||
|
flutter run -d KM10 --no-enable-impeller
|
||||||
|
```
|
||||||
|
|
||||||
|
**Effect:** Forces Flutter to use the legacy Skia rendering backend instead of Impeller.
|
||||||
|
|
||||||
|
### Deprecation Warning
|
||||||
|
|
||||||
|
When running with `--no-enable-impeller`, Flutter displays the following warning:
|
||||||
|
|
||||||
|
```
|
||||||
|
[IMPORTANT:flutter/shell/common/shell.cc(527)] [Action Required]: Impeller opt-out deprecated.
|
||||||
|
The application opted out of Impeller by either using the
|
||||||
|
`--no-enable-impeller` flag or the
|
||||||
|
`io.flutter.embedding.android.EnableImpeller` `AndroidManifest.xml` entry.
|
||||||
|
These options are going to go away in an upcoming Flutter release. Remove
|
||||||
|
the explicit opt-out. If you need to opt-out, please report a bug describing
|
||||||
|
the issue.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important:** This flag will be removed in a future Flutter release, meaning this workaround is temporary.
|
||||||
|
|
||||||
|
## Observed Symptoms (When Impeller is Enabled)
|
||||||
|
|
||||||
|
When running with Impeller enabled (default behavior), the following issues occur:
|
||||||
|
|
||||||
|
1. **Map Rendering Glitches**
|
||||||
|
- Visual artifacts on the map surface
|
||||||
|
- Possible z-index layering issues between Flutter widgets and native map view
|
||||||
|
- Inconsistent rendering of map tiles or navigation elements
|
||||||
|
|
||||||
|
2. **Performance Issues**
|
||||||
|
- Frame skipping: "Skipped 128 frames! The application may be doing too much work on its main thread"
|
||||||
|
- Possible GPU rendering conflicts between Impeller and Google Maps' native rendering
|
||||||
|
|
||||||
|
3. **Platform View Integration Issues**
|
||||||
|
- The map uses a SurfaceView (native Android view) embedded in Flutter's widget tree
|
||||||
|
- Impeller's composition may conflict with how Flutter manages platform views
|
||||||
|
|
||||||
|
## File References
|
||||||
|
|
||||||
|
### Map Component Implementation
|
||||||
|
|
||||||
|
**File:** `lib/components/dark_mode_map.dart`
|
||||||
|
- Implements Google Maps Navigation SDK integration
|
||||||
|
- Uses AndroidView platform view for native map rendering
|
||||||
|
- Lines 403-408, 421-426: Auto-recenter functionality
|
||||||
|
- Configures map settings for performance optimization
|
||||||
|
|
||||||
|
**Key Code Sections:**
|
||||||
|
```dart
|
||||||
|
// Platform view creation (Android)
|
||||||
|
body: Platform.isAndroid
|
||||||
|
? AndroidView(
|
||||||
|
viewType: 'google_navigation_flutter',
|
||||||
|
onPlatformViewCreated: _onViewCreated,
|
||||||
|
creationParams: _viewCreationParams,
|
||||||
|
creationParamsCodec: const StandardMessageCodec(),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Potential Solutions
|
||||||
|
|
||||||
|
### 1. Wait for Upstream Fixes (Recommended)
|
||||||
|
|
||||||
|
**Action:** Monitor the following repositories for updates:
|
||||||
|
- [Flutter Engine Issues](https://github.com/flutter/flutter/issues)
|
||||||
|
- [Google Maps Flutter Navigation Plugin](https://github.com/googlemaps/flutter-navigation-sdk/issues)
|
||||||
|
|
||||||
|
**Search Terms:**
|
||||||
|
- "Impeller platform view glitch"
|
||||||
|
- "Google Maps Impeller rendering"
|
||||||
|
- "AndroidView Impeller artifacts"
|
||||||
|
|
||||||
|
### 2. Report Issue to Flutter Team
|
||||||
|
|
||||||
|
If not already reported, file a bug report with:
|
||||||
|
- Device: KM10 Android device
|
||||||
|
- Flutter version: (check with `flutter --version`)
|
||||||
|
- Google Maps Navigation SDK version
|
||||||
|
- Detailed reproduction steps
|
||||||
|
- Screenshots/video of the glitch
|
||||||
|
|
||||||
|
**Report at:** https://github.com/flutter/flutter/issues/new?template=02_bug.yml
|
||||||
|
|
||||||
|
### 3. Permanent AndroidManifest Configuration (Temporary)
|
||||||
|
|
||||||
|
If needed for production builds before Flutter removes the flag, add to `android/app/src/main/AndroidManifest.xml`:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<application
|
||||||
|
...>
|
||||||
|
<meta-data
|
||||||
|
android:name="io.flutter.embedding.android.EnableImpeller"
|
||||||
|
android:value="false" />
|
||||||
|
</application>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Warning:** This will also be deprecated and removed in future Flutter releases.
|
||||||
|
|
||||||
|
### 4. Investigate Hybrid Composition (Alternative)
|
||||||
|
|
||||||
|
Try switching to Hybrid Composition mode for platform views:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// In android/app/src/main/AndroidManifest.xml
|
||||||
|
<meta-data
|
||||||
|
android:name="io.flutter.embedded_views_preview"
|
||||||
|
android:value="true" />
|
||||||
|
```
|
||||||
|
|
||||||
|
This changes how Flutter composites native views and may resolve rendering conflicts with Impeller.
|
||||||
|
|
||||||
|
### 5. Monitor Flutter Stable Channel Updates
|
||||||
|
|
||||||
|
As Flutter's Impeller implementation matures, future stable releases will include fixes for platform view rendering issues. Regularly update Flutter and test with Impeller enabled:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flutter upgrade
|
||||||
|
flutter run -d KM10 # Test without --no-enable-impeller flag
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
When testing Impeller compatibility in future Flutter versions:
|
||||||
|
|
||||||
|
- [ ] Map renders correctly without visual artifacts
|
||||||
|
- [ ] Delivery list sidebar doesn't interfere with map rendering
|
||||||
|
- [ ] Map markers and navigation UI elements display properly
|
||||||
|
- [ ] No z-index issues between Flutter widgets and map view
|
||||||
|
- [ ] Smooth scrolling and panning without frame drops
|
||||||
|
- [ ] Navigation route rendering works correctly
|
||||||
|
- [ ] Camera transitions are smooth
|
||||||
|
- [ ] Auto-recenter functionality works
|
||||||
|
- [ ] Performance is acceptable (check frame timing in DevTools)
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
### Current Recommendation
|
||||||
|
|
||||||
|
**For Android development:**
|
||||||
|
```bash
|
||||||
|
# Use this until Impeller compatibility is confirmed
|
||||||
|
flutter run -d KM10 --no-enable-impeller
|
||||||
|
```
|
||||||
|
|
||||||
|
**For iOS development:**
|
||||||
|
```bash
|
||||||
|
# iOS Impeller is more stable, can use default
|
||||||
|
flutter run -d ios
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hot Reload
|
||||||
|
|
||||||
|
Hot reload works normally with Impeller disabled:
|
||||||
|
```bash
|
||||||
|
# Press 'r' in terminal or
|
||||||
|
echo "r" | nc localhost 7182
|
||||||
|
```
|
||||||
|
|
||||||
|
### Release Builds
|
||||||
|
|
||||||
|
**Current Android release builds should also disable Impeller:**
|
||||||
|
|
||||||
|
In `android/app/build.gradle`:
|
||||||
|
```gradle
|
||||||
|
android {
|
||||||
|
defaultConfig {
|
||||||
|
// Add Impeller opt-out for release builds
|
||||||
|
manifestPlaceholders = [
|
||||||
|
enableImpeller: 'false'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then reference in AndroidManifest.xml:
|
||||||
|
```xml
|
||||||
|
<meta-data
|
||||||
|
android:name="io.flutter.embedding.android.EnableImpeller"
|
||||||
|
android:value="${enableImpeller}" />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Timeline and Impact
|
||||||
|
|
||||||
|
### Short-term (Current)
|
||||||
|
- **Status:** Using `--no-enable-impeller` flag for all Android development
|
||||||
|
- **Impact:** No user-facing issues, development proceeds normally
|
||||||
|
- **Risk:** None, Skia backend is stable and well-tested
|
||||||
|
|
||||||
|
### Medium-term (Next 3-6 months)
|
||||||
|
- **Status:** Monitor Flutter stable releases for Impeller improvements
|
||||||
|
- **Action:** Test each stable release with Impeller enabled
|
||||||
|
- **Risk:** Low, workaround is stable
|
||||||
|
|
||||||
|
### Long-term (6-12 months)
|
||||||
|
- **Status:** Impeller opt-out will be removed from Flutter
|
||||||
|
- **Action:** Must resolve compatibility or report blocking issues
|
||||||
|
- **Risk:** Medium - may need to migrate away from Google Maps if incompatibility persists
|
||||||
|
|
||||||
|
## Related Issues
|
||||||
|
|
||||||
|
### Performance Warnings
|
||||||
|
|
||||||
|
The following warnings appear in logs but are expected during development:
|
||||||
|
|
||||||
|
```
|
||||||
|
I/Choreographer(22365): Skipped 128 frames! The application may be doing too much work on its main thread.
|
||||||
|
```
|
||||||
|
|
||||||
|
This is related to initial app load and map initialization, not specifically to the Impeller workaround.
|
||||||
|
|
||||||
|
### Platform View Warnings
|
||||||
|
|
||||||
|
```
|
||||||
|
E/FrameEvents(22365): updateAcquireFence: Did not find frame.
|
||||||
|
W/Parcel(22365): Expecting binder but got null!
|
||||||
|
```
|
||||||
|
|
||||||
|
These warnings appear with both Skia and Impeller backends and are Android platform view quirks, not critical errors.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Flutter Impeller Documentation](https://docs.flutter.dev/perf/impeller)
|
||||||
|
- [Flutter Platform Views](https://docs.flutter.dev/platform-integration/android/platform-views)
|
||||||
|
- [Google Maps Flutter Navigation SDK](https://developers.google.com/maps/documentation/navigation/flutter/reference)
|
||||||
|
- [Flutter Issue: Impeller platform view support](https://github.com/flutter/flutter/issues?q=is%3Aissue+impeller+platform+view)
|
||||||
|
|
||||||
|
## Last Updated
|
||||||
|
|
||||||
|
**Date:** 2025-11-25
|
||||||
|
**Flutter Version:** (Run `flutter --version` to check)
|
||||||
|
**Device:** KM10 Android
|
||||||
|
**Status:** Workaround active, monitoring for upstream fixes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes for Future Developers
|
||||||
|
|
||||||
|
If you're reading this documentation:
|
||||||
|
|
||||||
|
1. **First, try without the flag** - Impeller may have been fixed in your Flutter version:
|
||||||
|
```bash
|
||||||
|
flutter run -d android
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **If you see map glitches**, add the flag:
|
||||||
|
```bash
|
||||||
|
flutter run -d android --no-enable-impeller
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **If the flag is removed and maps still glitch**, search for:
|
||||||
|
- "Flutter Impeller Google Maps" on GitHub Issues
|
||||||
|
- Alternative map solutions (Apple Maps on iOS, alternative SDKs)
|
||||||
|
- Platform view composition modes
|
||||||
|
|
||||||
|
4. **Consider this a temporary workaround**, not a permanent solution.
|
||||||
@ -5,6 +5,7 @@ import 'package:http_interceptor/http_interceptor.dart';
|
|||||||
import 'types.dart';
|
import 'types.dart';
|
||||||
import 'openapi_config.dart';
|
import 'openapi_config.dart';
|
||||||
import '../utils/logging_interceptor.dart';
|
import '../utils/logging_interceptor.dart';
|
||||||
|
import '../utils/http_client_factory.dart';
|
||||||
|
|
||||||
class CqrsApiClient {
|
class CqrsApiClient {
|
||||||
final ApiClientConfig config;
|
final ApiClientConfig config;
|
||||||
@ -16,6 +17,9 @@ class CqrsApiClient {
|
|||||||
}) {
|
}) {
|
||||||
_httpClient = httpClient ?? InterceptedClient.build(
|
_httpClient = httpClient ?? InterceptedClient.build(
|
||||||
interceptors: [LoggingInterceptor()],
|
interceptors: [LoggingInterceptor()],
|
||||||
|
client: HttpClientFactory.createClient(
|
||||||
|
allowSelfSigned: config.allowSelfSignedCertificate,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,16 +2,19 @@ class ApiClientConfig {
|
|||||||
final String baseUrl;
|
final String baseUrl;
|
||||||
final Duration timeout;
|
final Duration timeout;
|
||||||
final Map<String, String> defaultHeaders;
|
final Map<String, String> defaultHeaders;
|
||||||
|
final bool allowSelfSignedCertificate;
|
||||||
|
|
||||||
const ApiClientConfig({
|
const ApiClientConfig({
|
||||||
required this.baseUrl,
|
required this.baseUrl,
|
||||||
this.timeout = const Duration(seconds: 30),
|
this.timeout = const Duration(seconds: 30),
|
||||||
this.defaultHeaders = const {},
|
this.defaultHeaders = const {},
|
||||||
|
this.allowSelfSignedCertificate = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
static const ApiClientConfig development = ApiClientConfig(
|
static const ApiClientConfig development = ApiClientConfig(
|
||||||
baseUrl: 'https://api-route.goutezplanb.com',
|
baseUrl: 'https://localhost:7182',
|
||||||
timeout: Duration(seconds: 30),
|
timeout: Duration(seconds: 30),
|
||||||
|
allowSelfSignedCertificate: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
static const ApiClientConfig production = ApiClientConfig(
|
static const ApiClientConfig production = ApiClientConfig(
|
||||||
|
|||||||
@ -1,14 +1,16 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import '../models/delivery_route.dart';
|
import '../models/delivery_route.dart';
|
||||||
import '../theme/spacing_system.dart';
|
import '../theme/spacing_system.dart';
|
||||||
import '../theme/size_system.dart';
|
import '../theme/size_system.dart';
|
||||||
import '../theme/animation_system.dart';
|
import '../theme/animation_system.dart';
|
||||||
import '../theme/color_system.dart';
|
import '../theme/color_system.dart';
|
||||||
import '../utils/breakpoints.dart';
|
import '../utils/breakpoints.dart';
|
||||||
|
import '../providers/providers.dart';
|
||||||
import 'route_list_item.dart';
|
import 'route_list_item.dart';
|
||||||
|
|
||||||
|
|
||||||
class CollapsibleRoutesSidebar extends StatefulWidget {
|
class CollapsibleRoutesSidebar extends ConsumerStatefulWidget {
|
||||||
final List<DeliveryRoute> routes;
|
final List<DeliveryRoute> routes;
|
||||||
final DeliveryRoute? selectedRoute;
|
final DeliveryRoute? selectedRoute;
|
||||||
final ValueChanged<DeliveryRoute> onRouteSelected;
|
final ValueChanged<DeliveryRoute> onRouteSelected;
|
||||||
@ -21,14 +23,13 @@ class CollapsibleRoutesSidebar extends StatefulWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<CollapsibleRoutesSidebar> createState() =>
|
ConsumerState<CollapsibleRoutesSidebar> createState() =>
|
||||||
_CollapsibleRoutesSidebarState();
|
_CollapsibleRoutesSidebarState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CollapsibleRoutesSidebarState extends State<CollapsibleRoutesSidebar>
|
class _CollapsibleRoutesSidebarState extends ConsumerState<CollapsibleRoutesSidebar>
|
||||||
with SingleTickerProviderStateMixin {
|
with SingleTickerProviderStateMixin {
|
||||||
late AnimationController _animationController;
|
late AnimationController _animationController;
|
||||||
bool _isExpanded = true;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -37,9 +38,15 @@ class _CollapsibleRoutesSidebarState extends State<CollapsibleRoutesSidebar>
|
|||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
vsync: this,
|
vsync: this,
|
||||||
);
|
);
|
||||||
if (_isExpanded) {
|
// Set initial animation state based on provider value
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
final isExpanded = ref.read(collapseStateProvider);
|
||||||
|
if (isExpanded) {
|
||||||
_animationController.forward();
|
_animationController.forward();
|
||||||
|
} else {
|
||||||
|
_animationController.value = 0;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -49,10 +56,11 @@ class _CollapsibleRoutesSidebarState extends State<CollapsibleRoutesSidebar>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _toggleSidebar() {
|
void _toggleSidebar() {
|
||||||
setState(() {
|
// Use shared provider state
|
||||||
_isExpanded = !_isExpanded;
|
ref.read(collapseStateProvider.notifier).toggle();
|
||||||
});
|
final isExpanded = ref.read(collapseStateProvider);
|
||||||
if (_isExpanded) {
|
|
||||||
|
if (isExpanded) {
|
||||||
_animationController.forward();
|
_animationController.forward();
|
||||||
} else {
|
} else {
|
||||||
_animationController.reverse();
|
_animationController.reverse();
|
||||||
@ -63,6 +71,7 @@ class _CollapsibleRoutesSidebarState extends State<CollapsibleRoutesSidebar>
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isMobile = context.isMobile;
|
final isMobile = context.isMobile;
|
||||||
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
final isExpanded = ref.watch(collapseStateProvider);
|
||||||
|
|
||||||
// On mobile, always show as collapsible
|
// On mobile, always show as collapsible
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
@ -84,19 +93,15 @@ class _CollapsibleRoutesSidebarState extends State<CollapsibleRoutesSidebar>
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
if (_isExpanded)
|
if (isExpanded)
|
||||||
Text(
|
Text(
|
||||||
'Routes',
|
'Routes',
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: AnimatedRotation(
|
icon: Icon(isExpanded ? Icons.menu_open : Icons.menu),
|
||||||
turns: _isExpanded ? 0 : -0.25,
|
|
||||||
duration: Duration(
|
|
||||||
milliseconds: AppAnimations.durationFast.inMilliseconds,
|
|
||||||
),
|
|
||||||
child: const Icon(Icons.chevron_right),
|
|
||||||
),
|
|
||||||
onPressed: _toggleSidebar,
|
onPressed: _toggleSidebar,
|
||||||
iconSize: AppSizes.iconMd,
|
iconSize: AppSizes.iconMd,
|
||||||
),
|
),
|
||||||
@ -104,18 +109,18 @@ class _CollapsibleRoutesSidebarState extends State<CollapsibleRoutesSidebar>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Collapsible content
|
// Collapsible content
|
||||||
if (_isExpanded)
|
if (isExpanded)
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _buildRoutesList(context),
|
child: _buildRoutesList(context, isExpanded),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// On tablet/desktop, show full sidebar with toggle (expanded: 420px, collapsed: 80px for badge)
|
// On tablet/desktop, show full sidebar with toggle (expanded: 300px, collapsed: 80px for badge)
|
||||||
return Container(
|
return Container(
|
||||||
width: _isExpanded ? 420 : 80,
|
width: isExpanded ? 300 : 80,
|
||||||
color: isDarkMode ? SvrntyColors.almostBlack : Colors.white,
|
color: isDarkMode ? SvrntyColors.almostBlack : Colors.white,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@ -132,13 +137,15 @@ class _CollapsibleRoutesSidebarState extends State<CollapsibleRoutesSidebar>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: _isExpanded ? MainAxisAlignment.spaceBetween : MainAxisAlignment.center,
|
mainAxisAlignment: isExpanded ? MainAxisAlignment.spaceBetween : MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
if (_isExpanded)
|
if (isExpanded)
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
'Routes',
|
'Routes',
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -146,13 +153,7 @@ class _CollapsibleRoutesSidebarState extends State<CollapsibleRoutesSidebar>
|
|||||||
width: AppSizes.buttonHeightMd,
|
width: AppSizes.buttonHeightMd,
|
||||||
height: AppSizes.buttonHeightMd,
|
height: AppSizes.buttonHeightMd,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: AnimatedRotation(
|
icon: Icon(isExpanded ? Icons.menu_open : Icons.menu),
|
||||||
turns: _isExpanded ? 0 : -0.5,
|
|
||||||
duration: Duration(
|
|
||||||
milliseconds: AppAnimations.durationFast.inMilliseconds,
|
|
||||||
),
|
|
||||||
child: const Icon(Icons.chevron_right),
|
|
||||||
),
|
|
||||||
onPressed: _toggleSidebar,
|
onPressed: _toggleSidebar,
|
||||||
iconSize: AppSizes.iconMd,
|
iconSize: AppSizes.iconMd,
|
||||||
),
|
),
|
||||||
@ -161,15 +162,15 @@ class _CollapsibleRoutesSidebarState extends State<CollapsibleRoutesSidebar>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Routes list
|
// Routes list
|
||||||
Expanded(
|
Flexible(
|
||||||
child: _buildRoutesList(context),
|
child: _buildRoutesList(context, isExpanded),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildRoutesList(BuildContext context) {
|
Widget _buildRoutesList(BuildContext context, bool isExpanded) {
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
padding: const EdgeInsets.only(top: 4, bottom: 8),
|
padding: const EdgeInsets.only(top: 4, bottom: 8),
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
@ -183,7 +184,7 @@ class _CollapsibleRoutesSidebarState extends State<CollapsibleRoutesSidebar>
|
|||||||
isSelected: isSelected,
|
isSelected: isSelected,
|
||||||
onTap: () => widget.onRouteSelected(route),
|
onTap: () => widget.onRouteSelected(route),
|
||||||
animationIndex: index,
|
animationIndex: index,
|
||||||
isCollapsed: !_isExpanded,
|
isCollapsed: !isExpanded,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -34,6 +34,7 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
|
|||||||
String _loadingMessage = 'Initializing...';
|
String _loadingMessage = 'Initializing...';
|
||||||
Brightness? _lastBrightness;
|
Brightness? _lastBrightness;
|
||||||
bool _isMapViewReady = false;
|
bool _isMapViewReady = false;
|
||||||
|
bool _isDisposed = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -41,6 +42,13 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
|
|||||||
_initializeNavigation();
|
_initializeNavigation();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_isDisposed = true;
|
||||||
|
_navigationController = null;
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didChangeDependencies() {
|
void didChangeDependencies() {
|
||||||
super.didChangeDependencies();
|
super.didChangeDependencies();
|
||||||
@ -49,7 +57,8 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
|
|||||||
final currentBrightness = Theme.of(context).brightness;
|
final currentBrightness = Theme.of(context).brightness;
|
||||||
if (_lastBrightness != null &&
|
if (_lastBrightness != null &&
|
||||||
_lastBrightness != currentBrightness &&
|
_lastBrightness != currentBrightness &&
|
||||||
_navigationController != null) {
|
_navigationController != null &&
|
||||||
|
!_isDisposed) {
|
||||||
_applyDarkModeStyle();
|
_applyDarkModeStyle();
|
||||||
}
|
}
|
||||||
_lastBrightness = currentBrightness;
|
_lastBrightness = currentBrightness;
|
||||||
@ -135,39 +144,13 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
|
|||||||
|
|
||||||
Future<void> _applyDarkModeStyle() async {
|
Future<void> _applyDarkModeStyle() async {
|
||||||
// Check if widget is still mounted and controller exists
|
// Check if widget is still mounted and controller exists
|
||||||
if (!mounted || _navigationController == null) return;
|
if (!mounted || _navigationController == null || _isDisposed || !_isMapViewReady) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!mounted) return;
|
if (!mounted || _isDisposed) return;
|
||||||
|
|
||||||
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
// Always use default (light) map style
|
||||||
if (isDarkMode) {
|
|
||||||
// Dark mode style - Note: Currently only supported on Android
|
|
||||||
const simpleDarkStyle = '''[
|
|
||||||
{
|
|
||||||
"elementType": "geometry",
|
|
||||||
"stylers": [{"color": "#242424"}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"elementType": "labels.text.fill",
|
|
||||||
"stylers": [{"color": "#746855"}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"elementType": "labels.text.stroke",
|
|
||||||
"stylers": [{"color": "#242424"}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"featureType": "water",
|
|
||||||
"elementType": "geometry",
|
|
||||||
"stylers": [{"color": "#17263c"}]
|
|
||||||
}
|
|
||||||
]''';
|
|
||||||
|
|
||||||
await _navigationController!.setMapStyle(simpleDarkStyle);
|
|
||||||
} else {
|
|
||||||
// Reset to default light style
|
|
||||||
await _navigationController!.setMapStyle(null);
|
await _navigationController!.setMapStyle(null);
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
debugPrint('Error applying map style: $e');
|
debugPrint('Error applying map style: $e');
|
||||||
@ -358,14 +341,20 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
|
|||||||
// This is critical for iOS to display turn-by-turn directions, ETA, distance
|
// This is critical for iOS to display turn-by-turn directions, ETA, distance
|
||||||
initialNavigationUIEnabledPreference: NavigationUIEnabledPreference.automatic,
|
initialNavigationUIEnabledPreference: NavigationUIEnabledPreference.automatic,
|
||||||
onViewCreated: (controller) async {
|
onViewCreated: (controller) async {
|
||||||
|
// Early exit if widget is already disposed
|
||||||
|
if (_isDisposed || !mounted) return;
|
||||||
|
|
||||||
_navigationController = controller;
|
_navigationController = controller;
|
||||||
|
|
||||||
// Wait longer for the map to be fully initialized on Android
|
// Wait longer for the map to be fully initialized on Android
|
||||||
// This helps prevent crashes when the view is disposed during initialization
|
// This helps prevent crashes when the view is disposed during initialization
|
||||||
await Future.delayed(const Duration(milliseconds: 1000));
|
await Future.delayed(const Duration(milliseconds: 1500));
|
||||||
|
|
||||||
// Safety check: ensure widget is still mounted before proceeding
|
// Safety check: ensure widget is still mounted before proceeding
|
||||||
if (!mounted) return;
|
if (!mounted || _isDisposed) {
|
||||||
|
_navigationController = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Mark map as ready only after the delay
|
// Mark map as ready only after the delay
|
||||||
_isMapViewReady = true;
|
_isMapViewReady = true;
|
||||||
@ -373,34 +362,68 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
|
|||||||
// Enable navigation UI elements (header with turn directions, footer with ETA/distance)
|
// Enable navigation UI elements (header with turn directions, footer with ETA/distance)
|
||||||
// This is required for iOS to show trip info, duration, and ETA
|
// This is required for iOS to show trip info, duration, and ETA
|
||||||
try {
|
try {
|
||||||
|
if (!mounted || _isDisposed) return;
|
||||||
await controller.setNavigationUIEnabled(true);
|
await controller.setNavigationUIEnabled(true);
|
||||||
|
if (!mounted || _isDisposed) return;
|
||||||
await controller.setNavigationHeaderEnabled(true);
|
await controller.setNavigationHeaderEnabled(true);
|
||||||
|
if (!mounted || _isDisposed) return;
|
||||||
await controller.setNavigationFooterEnabled(true);
|
await controller.setNavigationFooterEnabled(true);
|
||||||
|
if (!mounted || _isDisposed) return;
|
||||||
await controller.setNavigationTripProgressBarEnabled(true);
|
await controller.setNavigationTripProgressBarEnabled(true);
|
||||||
|
if (!mounted || _isDisposed) return;
|
||||||
|
// Disable report incident button
|
||||||
|
await controller.setReportIncidentButtonEnabled(false);
|
||||||
debugPrint('Navigation UI elements enabled');
|
debugPrint('Navigation UI elements enabled');
|
||||||
|
|
||||||
|
// Configure map settings to reduce GPU load for devices with limited graphics capabilities
|
||||||
|
if (!mounted || _isDisposed) return;
|
||||||
|
await controller.settings.setTrafficEnabled(true);
|
||||||
|
if (!mounted || _isDisposed) return;
|
||||||
|
await controller.settings.setRotateGesturesEnabled(true);
|
||||||
|
if (!mounted || _isDisposed) return;
|
||||||
|
await controller.settings.setTiltGesturesEnabled(false);
|
||||||
|
if (!mounted || _isDisposed) return;
|
||||||
|
debugPrint('Map settings configured for performance');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Error enabling navigation UI: $e');
|
debugPrint('Error configuring map: $e');
|
||||||
|
if (_isDisposed || !mounted) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!mounted || _isDisposed) return;
|
||||||
await _applyDarkModeStyle();
|
await _applyDarkModeStyle();
|
||||||
|
|
||||||
// Wrap camera animation in try-catch to handle "No valid view found" errors
|
// Wrap camera animation in try-catch to handle "No valid view found" errors
|
||||||
// This can happen on Android when the view isn't fully ready
|
// This can happen on Android when the view isn't fully ready
|
||||||
try {
|
try {
|
||||||
if (mounted && _navigationController != null && _isMapViewReady) {
|
if (mounted && _navigationController != null && _isMapViewReady && !_isDisposed) {
|
||||||
await controller.animateCamera(
|
await controller.animateCamera(
|
||||||
CameraUpdate.newLatLngZoom(initialPosition, 12),
|
CameraUpdate.newLatLngZoom(initialPosition, 12),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Auto-recenter to current location after initial setup
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
if (mounted && _navigationController != null && !_isDisposed) {
|
||||||
|
await _recenterMap();
|
||||||
|
debugPrint('Auto-recentered map to current location on initialization');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Camera animation error (view may not be ready): $e');
|
debugPrint('Camera animation error (view may not be ready): $e');
|
||||||
|
if (_isDisposed || !mounted) return;
|
||||||
// Retry once after a longer delay
|
// Retry once after a longer delay
|
||||||
await Future.delayed(const Duration(milliseconds: 1000));
|
await Future.delayed(const Duration(milliseconds: 1500));
|
||||||
if (mounted && _navigationController != null && _isMapViewReady) {
|
if (mounted && _navigationController != null && _isMapViewReady && !_isDisposed) {
|
||||||
try {
|
try {
|
||||||
await controller.animateCamera(
|
await controller.animateCamera(
|
||||||
CameraUpdate.newLatLngZoom(initialPosition, 12),
|
CameraUpdate.newLatLngZoom(initialPosition, 12),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Auto-recenter to current location after retry
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
if (mounted && _navigationController != null && !_isDisposed) {
|
||||||
|
await _recenterMap();
|
||||||
|
debugPrint('Auto-recentered map to current location on initialization (retry)');
|
||||||
|
}
|
||||||
} catch (e2) {
|
} catch (e2) {
|
||||||
debugPrint('Camera animation retry failed: $e2');
|
debugPrint('Camera animation retry failed: $e2');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,7 +54,7 @@ class _DeliveryListItemState extends State<DeliveryListItem>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
_slideAnimation = Tween<double>(begin: 20, end: 0).animate(
|
_slideAnimation = Tween<double>(begin: 0, end: 0).animate(
|
||||||
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
|
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -154,16 +154,14 @@ class _DeliveryListItemState extends State<DeliveryListItem>
|
|||||||
child: AnimatedContainer(
|
child: AnimatedContainer(
|
||||||
duration: AppAnimations.durationFast,
|
duration: AppAnimations.durationFast,
|
||||||
margin: const EdgeInsets.symmetric(
|
margin: const EdgeInsets.symmetric(
|
||||||
horizontal: 16,
|
horizontal: 2,
|
||||||
vertical: 10,
|
vertical: 4,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(8),
|
||||||
color: widget.delivery.delivered
|
color: widget.delivery.delivered
|
||||||
? Colors.green.withValues(alpha: 0.15)
|
? Colors.green.withValues(alpha: 0.15)
|
||||||
: (_isHovered || widget.isSelected
|
: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
? Theme.of(context).colorScheme.surfaceContainer
|
|
||||||
: Colors.transparent),
|
|
||||||
boxShadow: (_isHovered || widget.isSelected) && !widget.delivery.delivered
|
boxShadow: (_isHovered || widget.isSelected) && !widget.delivery.delivered
|
||||||
? [
|
? [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
@ -176,7 +174,7 @@ class _DeliveryListItemState extends State<DeliveryListItem>
|
|||||||
]
|
]
|
||||||
: [],
|
: [],
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 24),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Main delivery info row
|
// Main delivery info row
|
||||||
@ -185,63 +183,63 @@ class _DeliveryListItemState extends State<DeliveryListItem>
|
|||||||
children: [
|
children: [
|
||||||
// Order number badge (left of status bar)
|
// Order number badge (left of status bar)
|
||||||
Container(
|
Container(
|
||||||
width: 60,
|
width: 40,
|
||||||
height: 60,
|
height: 40,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: statusColor,
|
color: statusColor,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'${widget.delivery.deliveryIndex + 1}',
|
'${widget.delivery.deliveryIndex + 1}',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 26,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 8),
|
||||||
// Left accent bar (vertical status bar)
|
// Left accent bar (vertical status bar)
|
||||||
Container(
|
Container(
|
||||||
width: 6,
|
width: 4,
|
||||||
height: 80,
|
height: 50,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: statusColor,
|
color: statusColor,
|
||||||
borderRadius: BorderRadius.circular(3),
|
borderRadius: BorderRadius.circular(2),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 10),
|
||||||
// Delivery info
|
// Delivery info
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Customer Name (20% larger - 24px)
|
// Customer Name
|
||||||
Text(
|
Text(
|
||||||
widget.delivery.name,
|
widget.delivery.name,
|
||||||
style: Theme.of(context)
|
style: Theme.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
.titleLarge
|
.titleMedium
|
||||||
?.copyWith(
|
?.copyWith(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
fontSize: 24,
|
fontSize: 16,
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 4),
|
||||||
// Address (20% larger - 18px)
|
// Address
|
||||||
Text(
|
Text(
|
||||||
widget.delivery.deliveryAddress
|
widget.delivery.deliveryAddress
|
||||||
?.formattedAddress ??
|
?.formattedAddress ??
|
||||||
'No address',
|
'No address',
|
||||||
style: Theme.of(context)
|
style: Theme.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
.bodyLarge
|
.bodyMedium
|
||||||
?.copyWith(
|
?.copyWith(
|
||||||
fontSize: 18,
|
fontSize: 13,
|
||||||
),
|
),
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
|
|||||||
@ -149,11 +149,11 @@ class _RouteListItemState extends State<RouteListItem>
|
|||||||
child: AnimatedContainer(
|
child: AnimatedContainer(
|
||||||
duration: AppAnimations.durationFast,
|
duration: AppAnimations.durationFast,
|
||||||
margin: const EdgeInsets.symmetric(
|
margin: const EdgeInsets.symmetric(
|
||||||
horizontal: 16,
|
horizontal: 2,
|
||||||
vertical: 10,
|
vertical: 6,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(8),
|
||||||
color: widget.route.completed
|
color: widget.route.completed
|
||||||
? Colors.green.withValues(alpha: 0.15)
|
? Colors.green.withValues(alpha: 0.15)
|
||||||
: (_isHovered || widget.isSelected
|
: (_isHovered || widget.isSelected
|
||||||
@ -171,7 +171,7 @@ class _RouteListItemState extends State<RouteListItem>
|
|||||||
]
|
]
|
||||||
: [],
|
: [],
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 24),
|
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 12),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Main route info row
|
// Main route info row
|
||||||
@ -180,61 +180,61 @@ class _RouteListItemState extends State<RouteListItem>
|
|||||||
children: [
|
children: [
|
||||||
// Route number badge (left of status bar)
|
// Route number badge (left of status bar)
|
||||||
Container(
|
Container(
|
||||||
width: 60,
|
width: 45,
|
||||||
height: 60,
|
height: 45,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: statusColor,
|
color: statusColor,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'${(widget.animationIndex ?? 0) + 1}',
|
'${(widget.animationIndex ?? 0) + 1}',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 26,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 8),
|
||||||
// Left accent bar (vertical status bar)
|
// Left accent bar (vertical status bar)
|
||||||
Container(
|
Container(
|
||||||
width: 6,
|
width: 4,
|
||||||
height: 80,
|
height: 50,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: statusColor,
|
color: statusColor,
|
||||||
borderRadius: BorderRadius.circular(3),
|
borderRadius: BorderRadius.circular(2),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 10),
|
||||||
// Route info
|
// Route info
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Route Name (24px)
|
// Route Name
|
||||||
Text(
|
Text(
|
||||||
widget.route.name,
|
widget.route.name,
|
||||||
style: Theme.of(context)
|
style: Theme.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
.titleLarge
|
.titleMedium
|
||||||
?.copyWith(
|
?.copyWith(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
fontSize: 24,
|
fontSize: 16,
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 4),
|
||||||
// Route details (18px)
|
// Route details
|
||||||
Text(
|
Text(
|
||||||
'${widget.route.deliveredCount}/${widget.route.deliveriesCount} deliveries',
|
'${widget.route.deliveredCount}/${widget.route.deliveriesCount} deliveries',
|
||||||
style: Theme.of(context)
|
style: Theme.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
.bodyLarge
|
.bodyMedium
|
||||||
?.copyWith(
|
?.copyWith(
|
||||||
fontSize: 18,
|
fontSize: 13,
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
|
|||||||
@ -42,8 +42,8 @@ class PlanBLogisticApp extends ConsumerWidget {
|
|||||||
GlobalCupertinoLocalizations.delegate,
|
GlobalCupertinoLocalizations.delegate,
|
||||||
],
|
],
|
||||||
supportedLocales: const [
|
supportedLocales: const [
|
||||||
Locale('en', ''),
|
Locale('en', 'CA'),
|
||||||
Locale('fr', ''),
|
Locale('fr', 'CA'),
|
||||||
],
|
],
|
||||||
home: const AppHome(),
|
home: const AppHome(),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -13,11 +13,17 @@ import '../components/delivery_list_item.dart';
|
|||||||
class DeliveriesPage extends ConsumerStatefulWidget {
|
class DeliveriesPage extends ConsumerStatefulWidget {
|
||||||
final int routeFragmentId;
|
final int routeFragmentId;
|
||||||
final String routeName;
|
final String routeName;
|
||||||
|
final VoidCallback? onBack;
|
||||||
|
final bool showAsEmbedded;
|
||||||
|
final ValueChanged<Delivery?>? onDeliverySelected;
|
||||||
|
|
||||||
const DeliveriesPage({
|
const DeliveriesPage({
|
||||||
super.key,
|
super.key,
|
||||||
required this.routeFragmentId,
|
required this.routeFragmentId,
|
||||||
required this.routeName,
|
required this.routeName,
|
||||||
|
this.onBack,
|
||||||
|
this.showAsEmbedded = false,
|
||||||
|
this.onDeliverySelected,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -60,10 +66,114 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final deliveriesData = ref.watch(deliveriesProvider(widget.routeFragmentId));
|
final deliveriesData = ref.watch(deliveriesProvider(widget.routeFragmentId));
|
||||||
final routesData = ref.watch(deliveryRoutesProvider);
|
|
||||||
final tokenAsync = ref.watch(authTokenProvider);
|
final tokenAsync = ref.watch(authTokenProvider);
|
||||||
final token = tokenAsync.hasValue ? tokenAsync.value : null;
|
final token = tokenAsync.hasValue ? tokenAsync.value : null;
|
||||||
|
|
||||||
|
// When embedded in sidebar, show only the delivery list with back button
|
||||||
|
// This is a responsive sidebar that collapses like routes
|
||||||
|
if (widget.showAsEmbedded) {
|
||||||
|
final isExpanded = ref.watch(collapseStateProvider);
|
||||||
|
|
||||||
|
return deliveriesData.when(
|
||||||
|
data: (deliveries) {
|
||||||
|
// Auto-scroll to first pending delivery when page loads or route changes
|
||||||
|
if (_lastRouteFragmentId != widget.routeFragmentId) {
|
||||||
|
_lastRouteFragmentId = widget.routeFragmentId;
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_autoScrollToFirstPending(deliveries);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive sidebar that changes width when collapsed (420px expanded, 80px collapsed)
|
||||||
|
return AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
width: isExpanded ? 420 : 80,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Header with back button
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: isExpanded ? 12 : 8,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: isExpanded
|
||||||
|
? MainAxisAlignment.start
|
||||||
|
: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
if (isExpanded) ...[
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: widget.onBack,
|
||||||
|
tooltip: 'Back to routes',
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
widget.routeName,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(isExpanded ? Icons.menu_open : Icons.menu),
|
||||||
|
onPressed: () {
|
||||||
|
ref.read(collapseStateProvider.notifier).toggle();
|
||||||
|
},
|
||||||
|
tooltip: isExpanded ? 'Collapse' : 'Expand',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Delivery list
|
||||||
|
Expanded(
|
||||||
|
child: UnifiedDeliveryListView(
|
||||||
|
deliveries: deliveries,
|
||||||
|
selectedDelivery: _selectedDelivery,
|
||||||
|
scrollController: _listScrollController,
|
||||||
|
onDeliverySelected: (delivery) {
|
||||||
|
setState(() {
|
||||||
|
_selectedDelivery = delivery;
|
||||||
|
});
|
||||||
|
// Notify parent about delivery selection
|
||||||
|
widget.onDeliverySelected?.call(delivery);
|
||||||
|
},
|
||||||
|
onItemAction: (delivery, action) {
|
||||||
|
_handleDeliveryAction(context, delivery, action, token);
|
||||||
|
_autoScrollToFirstPending(deliveries);
|
||||||
|
},
|
||||||
|
isCollapsed: !isExpanded,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
loading: () => const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
error: (error, stackTrace) => Center(
|
||||||
|
child: Text('Error: $error'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// When not embedded, show full page with map
|
||||||
|
final routesData = ref.watch(deliveryRoutesProvider);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(widget.routeName),
|
title: Text(widget.routeName),
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import '../models/delivery.dart';
|
||||||
import '../models/delivery_route.dart';
|
import '../models/delivery_route.dart';
|
||||||
import '../providers/providers.dart';
|
import '../providers/providers.dart';
|
||||||
import '../utils/breakpoints.dart';
|
import '../utils/breakpoints.dart';
|
||||||
@ -18,6 +19,8 @@ class RoutesPage extends ConsumerStatefulWidget {
|
|||||||
|
|
||||||
class _RoutesPageState extends ConsumerState<RoutesPage> {
|
class _RoutesPageState extends ConsumerState<RoutesPage> {
|
||||||
late LocationPermissionService _permissionService;
|
late LocationPermissionService _permissionService;
|
||||||
|
DeliveryRoute? _selectedRoute;
|
||||||
|
Delivery? _selectedDelivery;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -51,15 +54,17 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _navigateToDeliveries(BuildContext context, DeliveryRoute route) {
|
void _selectRoute(DeliveryRoute route) {
|
||||||
Navigator.of(context).push(
|
setState(() {
|
||||||
MaterialPageRoute(
|
_selectedRoute = route;
|
||||||
builder: (context) => DeliveriesPage(
|
});
|
||||||
routeFragmentId: route.id,
|
}
|
||||||
routeName: route.name,
|
|
||||||
),
|
void _backToRoutes() {
|
||||||
),
|
setState(() {
|
||||||
);
|
_selectedRoute = null;
|
||||||
|
_selectedDelivery = null;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -73,6 +78,14 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
|||||||
title: const Text('Delivery Routes'),
|
title: const Text('Delivery Routes'),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
actions: [
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
onPressed: () {
|
||||||
|
ref.refresh(deliveryRoutesProvider);
|
||||||
|
ref.refresh(allDeliveriesProvider);
|
||||||
|
},
|
||||||
|
tooltip: 'Refresh',
|
||||||
|
),
|
||||||
userProfile.when(
|
userProfile.when(
|
||||||
data: (profile) => PopupMenuButton<String>(
|
data: (profile) => PopupMenuButton<String>(
|
||||||
onSelected: (value) {
|
onSelected: (value) {
|
||||||
@ -134,41 +147,39 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
|||||||
// ignore: unused_result
|
// ignore: unused_result
|
||||||
ref.refresh(allDeliveriesProvider);
|
ref.refresh(allDeliveriesProvider);
|
||||||
},
|
},
|
||||||
child: context.isDesktop
|
child: Row(
|
||||||
? Row(
|
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: DarkModeMapComponent(
|
child: DarkModeMapComponent(
|
||||||
deliveries: allDeliveries,
|
deliveries: allDeliveries,
|
||||||
selectedDelivery: null,
|
selectedDelivery: _selectedDelivery,
|
||||||
onDeliverySelected: null,
|
onDeliverySelected: (delivery) {
|
||||||
),
|
setState(() {
|
||||||
),
|
_selectedDelivery = delivery;
|
||||||
CollapsibleRoutesSidebar(
|
});
|
||||||
routes: routes,
|
|
||||||
selectedRoute: null,
|
|
||||||
onRouteSelected: (route) {
|
|
||||||
_navigateToDeliveries(context, route);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
|
_selectedRoute == null
|
||||||
|
? CollapsibleRoutesSidebar(
|
||||||
|
routes: routes,
|
||||||
|
selectedRoute: null,
|
||||||
|
onRouteSelected: _selectRoute,
|
||||||
)
|
)
|
||||||
: Column(
|
: SizedBox(
|
||||||
children: [
|
width: 300,
|
||||||
Expanded(
|
child: DeliveriesPage(
|
||||||
child: DarkModeMapComponent(
|
routeFragmentId: _selectedRoute!.id,
|
||||||
deliveries: allDeliveries,
|
routeName: _selectedRoute!.name,
|
||||||
selectedDelivery: null,
|
onBack: _backToRoutes,
|
||||||
onDeliverySelected: null,
|
showAsEmbedded: true,
|
||||||
),
|
onDeliverySelected: (delivery) {
|
||||||
),
|
setState(() {
|
||||||
CollapsibleRoutesSidebar(
|
_selectedDelivery = delivery;
|
||||||
routes: routes,
|
});
|
||||||
selectedRoute: null,
|
|
||||||
onRouteSelected: (route) {
|
|
||||||
_navigateToDeliveries(context, route);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -9,11 +9,11 @@ import '../models/delivery_route.dart';
|
|||||||
import '../models/delivery.dart';
|
import '../models/delivery.dart';
|
||||||
|
|
||||||
final authServiceProvider = Provider<AuthService>((ref) {
|
final authServiceProvider = Provider<AuthService>((ref) {
|
||||||
return AuthService();
|
return AuthService(config: AuthConfig.development);
|
||||||
});
|
});
|
||||||
|
|
||||||
final apiClientProvider = Provider<CqrsApiClient>((ref) {
|
final apiClientProvider = Provider<CqrsApiClient>((ref) {
|
||||||
return CqrsApiClient(config: ApiClientConfig.production);
|
return CqrsApiClient(config: ApiClientConfig.development);
|
||||||
});
|
});
|
||||||
|
|
||||||
final isAuthenticatedProvider = FutureProvider<bool>((ref) async {
|
final isAuthenticatedProvider = FutureProvider<bool>((ref) async {
|
||||||
@ -43,8 +43,9 @@ final deliveryRoutesProvider = FutureProvider<List<DeliveryRoute>>((ref) async {
|
|||||||
// Create a new client with auth token
|
// Create a new client with auth token
|
||||||
final authClient = CqrsApiClient(
|
final authClient = CqrsApiClient(
|
||||||
config: ApiClientConfig(
|
config: ApiClientConfig(
|
||||||
baseUrl: ApiClientConfig.production.baseUrl,
|
baseUrl: ApiClientConfig.development.baseUrl,
|
||||||
defaultHeaders: {'Authorization': 'Bearer $token'},
|
defaultHeaders: {'Authorization': 'Bearer $token'},
|
||||||
|
allowSelfSignedCertificate: ApiClientConfig.development.allowSelfSignedCertificate,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -75,8 +76,9 @@ final deliveriesProvider = FutureProvider.family<List<Delivery>, int>((ref, rout
|
|||||||
|
|
||||||
final authClient = CqrsApiClient(
|
final authClient = CqrsApiClient(
|
||||||
config: ApiClientConfig(
|
config: ApiClientConfig(
|
||||||
baseUrl: ApiClientConfig.production.baseUrl,
|
baseUrl: ApiClientConfig.development.baseUrl,
|
||||||
defaultHeaders: {'Authorization': 'Bearer $token'},
|
defaultHeaders: {'Authorization': 'Bearer $token'},
|
||||||
|
allowSelfSignedCertificate: ApiClientConfig.development.allowSelfSignedCertificate,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -106,18 +108,17 @@ final allDeliveriesProvider = FutureProvider<List<Delivery>>((ref) async {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch deliveries for all routes
|
// Fetch deliveries for all routes in parallel using Future.wait
|
||||||
final deliveriesFutures = routes.map((route) {
|
final deliveriesFutures = routes.map((route) {
|
||||||
return ref.watch(deliveriesProvider(route.id)).when(
|
return ref.read(deliveriesProvider(route.id).future);
|
||||||
data: (deliveries) => deliveries,
|
}).toList();
|
||||||
loading: () => <Delivery>[],
|
|
||||||
error: (_, __) => <Delivery>[],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Combine all deliveries
|
// Wait for all futures to complete
|
||||||
|
final deliveriesLists = await Future.wait(deliveriesFutures);
|
||||||
|
|
||||||
|
// Combine all deliveries into a single list
|
||||||
final allDeliveries = <Delivery>[];
|
final allDeliveries = <Delivery>[];
|
||||||
for (final deliveries in deliveriesFutures) {
|
for (final deliveries in deliveriesLists) {
|
||||||
allDeliveries.addAll(deliveries);
|
allDeliveries.addAll(deliveries);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,6 +149,19 @@ final themeModeProvider = NotifierProvider<ThemeModeNotifier, ThemeMode>(() {
|
|||||||
return ThemeModeNotifier();
|
return ThemeModeNotifier();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Collapse state notifier for sidebar persistence
|
||||||
|
class CollapseStateNotifier extends Notifier<bool> {
|
||||||
|
@override
|
||||||
|
bool build() => true; // Default: expanded
|
||||||
|
|
||||||
|
void toggle() => state = !state;
|
||||||
|
void setExpanded(bool expanded) => state = expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
final collapseStateProvider = NotifierProvider<CollapseStateNotifier, bool>(() {
|
||||||
|
return CollapseStateNotifier();
|
||||||
|
});
|
||||||
|
|
||||||
class _EmptyQuery implements Serializable {
|
class _EmptyQuery implements Serializable {
|
||||||
@override
|
@override
|
||||||
Map<String, Object?> toJson() => {};
|
Map<String, Object?> toJson() => {};
|
||||||
|
|||||||
@ -5,20 +5,51 @@ import 'package:http_interceptor/http_interceptor.dart';
|
|||||||
import 'package:jwt_decoder/jwt_decoder.dart';
|
import 'package:jwt_decoder/jwt_decoder.dart';
|
||||||
import '../models/user_profile.dart';
|
import '../models/user_profile.dart';
|
||||||
import '../utils/logging_interceptor.dart';
|
import '../utils/logging_interceptor.dart';
|
||||||
|
import '../utils/http_client_factory.dart';
|
||||||
|
|
||||||
|
class AuthConfig {
|
||||||
|
final String realm;
|
||||||
|
final String authServerUrl;
|
||||||
|
final String clientId;
|
||||||
|
final bool allowSelfSignedCertificate;
|
||||||
|
|
||||||
|
const AuthConfig({
|
||||||
|
required this.realm,
|
||||||
|
required this.authServerUrl,
|
||||||
|
required this.clientId,
|
||||||
|
this.allowSelfSignedCertificate = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
static const AuthConfig development = AuthConfig(
|
||||||
|
realm: 'dev',
|
||||||
|
authServerUrl: 'https://auth.goutezplanb.com',
|
||||||
|
clientId: 'delivery-mobile-app',
|
||||||
|
allowSelfSignedCertificate: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const AuthConfig production = AuthConfig(
|
||||||
|
realm: 'planb-internal',
|
||||||
|
authServerUrl: 'https://auth.goutezplanb.com',
|
||||||
|
clientId: 'delivery-mobile-app',
|
||||||
|
);
|
||||||
|
|
||||||
|
String get tokenEndpoint => '$authServerUrl/realms/$realm/protocol/openid-connect/token';
|
||||||
|
}
|
||||||
|
|
||||||
class AuthService {
|
class AuthService {
|
||||||
static const String _tokenKey = 'auth_token';
|
static const String _tokenKey = 'auth_token';
|
||||||
static const String _refreshTokenKey = 'refresh_token';
|
static const String _refreshTokenKey = 'refresh_token';
|
||||||
static const String _tokenEndpoint = 'https://auth.goutezplanb.com/realms/planb-internal/protocol/openid-connect/token';
|
|
||||||
static const String _clientId = 'delivery-mobile-app';
|
|
||||||
|
|
||||||
|
final AuthConfig _config;
|
||||||
final FlutterSecureStorage _secureStorage;
|
final FlutterSecureStorage _secureStorage;
|
||||||
final http.Client _httpClient;
|
final http.Client _httpClient;
|
||||||
|
|
||||||
AuthService({
|
AuthService({
|
||||||
|
AuthConfig config = AuthConfig.development,
|
||||||
FlutterSecureStorage? secureStorage,
|
FlutterSecureStorage? secureStorage,
|
||||||
http.Client? httpClient,
|
http.Client? httpClient,
|
||||||
}) : _secureStorage = secureStorage ?? const FlutterSecureStorage(
|
}) : _config = config,
|
||||||
|
_secureStorage = secureStorage ?? const FlutterSecureStorage(
|
||||||
aOptions: AndroidOptions(
|
aOptions: AndroidOptions(
|
||||||
encryptedSharedPreferences: true,
|
encryptedSharedPreferences: true,
|
||||||
),
|
),
|
||||||
@ -31,6 +62,9 @@ class AuthService {
|
|||||||
),
|
),
|
||||||
_httpClient = httpClient ?? InterceptedClient.build(
|
_httpClient = httpClient ?? InterceptedClient.build(
|
||||||
interceptors: [LoggingInterceptor()],
|
interceptors: [LoggingInterceptor()],
|
||||||
|
client: HttpClientFactory.createClient(
|
||||||
|
allowSelfSigned: config.allowSelfSignedCertificate,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<AuthResult> login({
|
Future<AuthResult> login({
|
||||||
@ -39,13 +73,13 @@ class AuthService {
|
|||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
final response = await _httpClient.post(
|
final response = await _httpClient.post(
|
||||||
Uri.parse(_tokenEndpoint),
|
Uri.parse(_config.tokenEndpoint),
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
'grant_type': 'password',
|
'grant_type': 'password',
|
||||||
'client_id': _clientId,
|
'client_id': _config.clientId,
|
||||||
'username': username,
|
'username': username,
|
||||||
'password': password,
|
'password': password,
|
||||||
'scope': 'openid profile offline_access',
|
'scope': 'openid profile offline_access',
|
||||||
@ -81,13 +115,13 @@ class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final response = await _httpClient.post(
|
final response = await _httpClient.post(
|
||||||
Uri.parse(_tokenEndpoint),
|
Uri.parse(_config.tokenEndpoint),
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
'grant_type': 'refresh_token',
|
'grant_type': 'refresh_token',
|
||||||
'client_id': _clientId,
|
'client_id': _config.clientId,
|
||||||
'refresh_token': refreshToken,
|
'refresh_token': refreshToken,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
14
lib/utils/http_client_factory.dart
Normal file
14
lib/utils/http_client_factory.dart
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:http/io_client.dart';
|
||||||
|
|
||||||
|
class HttpClientFactory {
|
||||||
|
static http.Client createClient({bool allowSelfSigned = false}) {
|
||||||
|
if (allowSelfSigned) {
|
||||||
|
final ioClient = HttpClient()
|
||||||
|
..badCertificateCallback = (X509Certificate cert, String host, int port) => true;
|
||||||
|
return IOClient(ioClient);
|
||||||
|
}
|
||||||
|
return http.Client();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -465,10 +465,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: google_navigation_flutter
|
name: google_navigation_flutter
|
||||||
sha256: f1a892e58c94601716ed5879e07c2a31031fcd609088705f5c97dc9c28748717
|
sha256: fdf79ddeda8bbba9d8b9218c41551b918447032004cbe72ea7365287c9d9bf80
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.5"
|
version: "0.7.0"
|
||||||
graphs:
|
graphs:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -40,7 +40,7 @@ dependencies:
|
|||||||
go_router: ^17.0.0
|
go_router: ^17.0.0
|
||||||
shared_preferences: ^2.5.3
|
shared_preferences: ^2.5.3
|
||||||
http_interceptor: ^2.0.0
|
http_interceptor: ^2.0.0
|
||||||
google_navigation_flutter: ^0.6.5
|
google_navigation_flutter: ^0.7.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user