commit 4b03e9aba59b84209197ba41b7df874ba2efabd5 Author: Claude Code Date: Fri Oct 31 04:58:10 2025 -0400 Initial commit: Plan B Logistics Flutter app with dark mode and responsive design Implements complete refactor of Ionic Angular logistics app to Flutter/Dart with: - Svrnty dark mode console theme (Material Design 3) - Responsive layouts (mobile, tablet, desktop) following FRONTEND standards - CQRS API integration with Result error handling - OAuth2/OIDC authentication support (mocked for initial testing) - Delivery route and delivery management features - Multi-language support (EN/FR) with i18n - Native integrations (camera, phone calls, maps) - Strict typing throughout codebase - Mock data for UI testing without backend Follows all FRONTEND style guides, design patterns, and conventions. App is running in dark mode and fully responsive across all device sizes. Co-Authored-By: Claude diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3820a95 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..534977f --- /dev/null +++ b/.metadata @@ -0,0 +1,36 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "adc901062556672b4138e18a4dc62a4be8f4b3c2" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + - platform: android + create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + - platform: ios + create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + - platform: web + create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..9cab679 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,425 @@ +# CLAUDE.md - Plan B Logistics Flutter App + +This file provides guidance to Claude Code when working with this Flutter/Dart project. + +## Project Overview + +Plan B Logistics Flutter is a complete refactor of the Ionic Angular delivery management app into Flutter/Dart, maintaining all functionality while applying Svrnty design system (colors, typography, and Material Design 3). + +**Key Features:** +- OAuth2/OIDC authentication with Keycloak +- CQRS pattern for API integration with Result error handling +- Delivery route and delivery management +- Photo upload for delivery proof +- i18n support (French/English) +- Native features: Camera, Phone calls, Maps + +## Essential Commands + +### Setup & Dependencies +```bash +flutter pub get +flutter pub upgrade +flutter pub run build_runner build --delete-conflicting-outputs +``` + +### Development +```bash +flutter run -d chrome # Web +flutter run -d macos # macOS +flutter run -d ios # iOS simulator +flutter run -d android # Android emulator +``` + +### Testing & Analysis +```bash +flutter test +flutter analyze +flutter test --coverage +``` + +### Build +```bash +flutter build web +flutter build ios +flutter build android +``` + +## Code Architecture + +### Core Structure +``` +lib/ +├── api/ # CQRS API client and types +│ ├── types.dart # Result, Serializable, ApiError +│ ├── client.dart # CqrsApiClient implementation +│ └── openapi_config.dart +├── models/ # Data models (strict typing) +│ ├── delivery.dart +│ ├── delivery_route.dart +│ ├── user_profile.dart +│ └── ... +├── services/ # Business logic +│ └── auth_service.dart +├── providers/ # Riverpod state management +│ └── providers.dart +├── pages/ # Screen widgets +│ ├── login_page.dart +│ ├── routes_page.dart +│ ├── deliveries_page.dart +│ └── settings_page.dart +├── components/ # Reusable UI components +├── l10n/ # i18n translations (*.arb files) +├── utils/ # Utility functions +├── theme.dart # Svrnty theme configuration +└── main.dart # App entry point +``` + +### Design System (Svrnty) + +**Primary Colors:** +- Primary (Crimson): #C44D58 +- Secondary (Slate Blue): #475C6C +- Error: #BA1A1A + +**Typography:** +- Primary Font: Montserrat (all weights 300-700) +- Monospace Font: IBMPlexMono +- Material Design 3 text styles + +**Theme Files:** +- `lib/theme.dart` - Complete Material 3 theme configuration +- Light and dark themes with high-contrast variants +- All colors defined in ColorScheme + +## Core Patterns & Standards + +### 1. Strict Typing (MANDATORY) +**NO `dynamic`, NO untyped `var`** + +```dart +// FORBIDDEN: +var data = fetchData(); +dynamic result = api.call(); + +// REQUIRED: +DeliveryRoute data = fetchData(); +Result result = api.call(); +``` + +### 2. Serializable Interface +All models must implement `Serializable`: + +```dart +class Delivery implements Serializable { + final int id; + final String name; + + const Delivery({required this.id, required this.name}); + + factory Delivery.fromJson(Map json) { + return Delivery( + id: json['id'] as int, + name: json['name'] as String, + ); + } + + @override + Map toJson() => { + 'id': id, + 'name': name, + }; +} +``` + +### 3. Error Handling with Result +**NEVER use try-catch for API calls. Use Result pattern:** + +```dart +final result = await apiClient.executeQuery( + endpoint: 'deliveryRoutes', + query: GetRoutesQuery(), + fromJson: DeliveryRoute.fromJson, +); + +result.when( + success: (route) => showRoute(route), + error: (error) { + switch (error.type) { + case ApiErrorType.network: + showSnackbar('No connection'); + case ApiErrorType.timeout: + showSnackbar('Request timeout'); + case ApiErrorType.validation: + showValidationErrors(error.details); + case ApiErrorType.http when error.statusCode == 401: + navigateToLogin(); + default: + showSnackbar('Error: ${error.message}'); + } + }, +); +``` + +### 4. CQRS API Integration + +**Query (Read Operations):** +```dart +final result = await client.executeQuery( + endpoint: 'simpleDeliveriesQueryItems', + query: GetDeliveriesQuery(routeFragmentId: 123), + fromJson: Delivery.fromJson, +); +``` + +**Command (Write Operations):** +```dart +final result = await client.executeCommand( + endpoint: 'completeDelivery', + command: CompleteDeliveryCommand(deliveryId: 123), +); +``` + +### 5. Riverpod State Management + +**Providers Pattern:** +```dart +final authServiceProvider = Provider((ref) { + return AuthService(); +}); + +final userProfileProvider = FutureProvider((ref) async { + final authService = ref.watch(authServiceProvider); + final token = await authService.getToken(); + return token != null ? authService.decodeToken(token) : null; +}); + +// Usage in widget: +final userProfile = ref.watch(userProfileProvider); +userProfile.when( + data: (profile) => Text(profile?.fullName ?? ''), + loading: () => const CircularProgressIndicator(), + error: (error, stackTrace) => Text('Error: $error'), +); +``` + +### 6. Authentication Flow + +1. **Login**: `AuthService.login()` triggers OAuth2/OIDC flow with Keycloak +2. **Token Storage**: Secure storage with `flutter_secure_storage` +3. **Token Validation**: Check expiration with `JwtDecoder.isExpired()` +4. **Auto Refresh**: Implement token refresh on 401 responses +5. **Logout**: Clear tokens from secure storage + +**Keycloak Configuration:** +- Realm: planb-internal +- Client ID: delivery-mobile-app +- Discovery URL: https://auth.goutezplanb.com/realms/planb-internal/.well-known/openid-configuration +- Scopes: openid, profile, offline_access + +### 7. No Emojis Rule +**MANDATORY: NO emojis in code, comments, or commit messages** + +```dart +// FORBIDDEN: +// Bug fix for delivery issues +void completeDelivery(int id) { ... } // Done + +// REQUIRED: +// Bug fix for delivery completion logic +void completeDelivery(int id) { ... } +``` + +## API Integration + +### Base URLs +```dart +const String queryBaseUrl = 'https://api-route.goutezplanb.com/api/query'; +const String commandBaseUrl = 'https://api-route.goutezplanb.com/api/command'; +``` + +### Key Endpoints +- Query: `/api/query/simpleDeliveriesQueryItems` +- Query: `/api/query/simpleDeliveryRouteQueryItems` +- Command: `/api/command/completeDelivery` +- Command: `/api/command/markDeliveryAsUncompleted` +- Upload: `/api/delivery/uploadDeliveryPicture` + +### Authorization +All requests to API base URL must include Bearer token: +```dart +final authClient = CqrsApiClient( + config: ApiClientConfig( + baseUrl: 'https://api-route.goutezplanb.com', + defaultHeaders: {'Authorization': 'Bearer $token'}, + ), +); +``` + +## Internationalization (i18n) + +### File Structure +``` +lib/l10n/ +├── app_en.arb # English translations +└── app_fr.arb # French translations +``` + +### ARB Format +```json +{ + "appTitle": "Plan B Logistics", + "loginButton": "Login with Keycloak", + "deliveryStatus": "Delivery #{id} is {status}", + "@deliveryStatus": { + "placeholders": { + "id": {"type": "int"}, + "status": {"type": "String"} + } + } +} +``` + +### Usage in Code +```dart +AppLocalizations.of(context)!.appTitle +AppLocalizations.of(context)!.deliveryStatus(id: 123, status: 'completed') +``` + +## Native Features + +### Camera Integration +- Package: `image_picker` +- Use: Photo capture for delivery proof +- Platforms: iOS, Android, Web + +```dart +final picker = ImagePicker(); +final pickedFile = await picker.pickImage(source: ImageSource.camera); +if (pickedFile != null) { + // Upload to server +} +``` + +### Phone Calls +- Package: `url_launcher` +- Use: Call customer from delivery details + +```dart +final Uri phoneUri = Uri(scheme: 'tel', path: phoneNumber); +if (await canLaunchUrl(phoneUri)) { + await launchUrl(phoneUri); +} +``` + +### Maps Integration +- Package: `url_launcher` +- Use: Open maps app to show delivery address + +```dart +final Uri mapUri = Uri( + scheme: 'https', + host: 'maps.google.com', + queryParameters: {'q': '${address.latitude},${address.longitude}'}, +); +if (await canLaunchUrl(mapUri)) { + await launchUrl(mapUri); +} +``` + +## File Naming Conventions + +- **Files**: snake_case (e.g., `delivery_route.dart`) +- **Classes**: PascalCase (e.g., `DeliveryRoute`) +- **Variables/Functions**: camelCase (e.g., `deliveryId`, `completeDelivery()`) +- **Constants**: camelCase or UPPER_SNAKE_CASE (e.g., `kPrimaryColor` or `MAX_RETRIES`) +- **Private members**: Prefix with underscore (e.g., `_secureStorage`) + +## Git Conventions + +- **Author**: Svrnty +- **Co-Author**: Jean-Philippe Brule +- **Commits**: Clear, concise messages describing the "why" +- **NO emojis in commits** + +Example: +``` +Implement OAuth2/OIDC authentication with Keycloak + +Adds AuthService with flutter_appauth integration, JWT token +management with secure storage, and automatic token refresh. + +Co-Authored-By: Claude +``` + +## Common Development Tasks + +### Adding a New Page +1. Create widget file in `lib/pages/[name]_page.dart` +2. Extend `ConsumerWidget` for Riverpod access +3. Use strict typing for all parameters and variables +4. Apply Svrnty colors from theme +5. Handle loading/error states with `.when()` + +### Adding a New Data Model +1. Create in `lib/models/[name].dart` +2. Implement `Serializable` interface +3. Add `fromJson` factory constructor +4. Implement `toJson()` method +5. Use explicit types (no `dynamic`) + +### Implementing API Call +1. Create Query/Command class implementing `Serializable` +2. Use `CqrsApiClient.executeQuery()` or `.executeCommand()` +3. Handle Result with `.when()` pattern +4. Never use try-catch for API calls +5. Provide proper error messages to user + +### Adding i18n Support +1. Add key to `app_en.arb` and `app_fr.arb` +2. Use `AppLocalizations.of(context)!.keyName` in widgets +3. For parameterized strings, define placeholders in ARB +4. Test both English and French text + +## Testing + +- Unit tests: `test/` directory +- Widget tests: `test/` directory with widget_test suffix +- Use Riverpod's testing utilities for provider testing +- Mock API client for service tests +- Maintain >80% code coverage for business logic + +## Deployment Checklist + +- [ ] All strict typing rules followed +- [ ] No `dynamic` or untyped `var` +- [ ] All API calls use Result pattern +- [ ] i18n translations complete for both languages +- [ ] Theme colors correctly applied +- [ ] No emojis in code or commits +- [ ] Tests passing (flutter test) +- [ ] Static analysis clean (flutter analyze) +- [ ] No secrets in code (tokens, keys, credentials) +- [ ] APK/IPA builds successful + +## Key Dependencies + +- `flutter_riverpod`: State management +- `flutter_appauth`: OAuth2/OIDC +- `flutter_secure_storage`: Token storage +- `jwt_decoder`: JWT token parsing +- `http`: HTTP client +- `image_picker`: Camera/photo access +- `url_launcher`: Phone calls and maps +- `animate_do`: Animations (from Svrnty) +- `lottie`: Loading animations +- `iconsax`: Icon set +- `intl`: Internationalization + +## Support & Documentation + +- **Theme**: See `lib/theme.dart` for complete Svrnty design system +- **API Types**: See `lib/api/types.dart` for Result and error handling +- **Models**: See `lib/models/` for data structure examples +- **Providers**: See `lib/providers/providers.dart` for state management setup +- **Auth**: See `lib/services/auth_service.dart` for OAuth2/OIDC flow diff --git a/README.md b/README.md new file mode 100644 index 0000000..83ec101 --- /dev/null +++ b/README.md @@ -0,0 +1,97 @@ +# Plan B Logistics - Flutter Mobile App + +A complete Flutter/Dart refactoring of the Plan B Logistics delivery management system. Built with Material Design 3, Svrnty brand colors, and CQRS architecture for type-safe API integration. + +## Overview + +This is a mobile delivery management application for logistics personnel to: +- View assigned delivery routes with progress tracking +- Manage individual deliveries (complete, uncomplete, skip) +- Capture photos as delivery proof +- Call customers and navigate to delivery addresses +- Manage app settings and language preferences +- Secure authentication via OAuth2/OIDC with Keycloak + +**Built with:** +- Flutter 3.9+ / Dart 3.9.2+ +- Material Design 3 with Svrnty theming (Crimson & Slate Blue) +- Riverpod for state management +- CQRS pattern with Result error handling +- Strict typing (no `dynamic`) + +## Quick Start + +### Prerequisites + +- Flutter SDK 3.9.2+: [Install Flutter](https://flutter.dev/docs/get-started/install) +- Dart SDK 3.9.2+ (included with Flutter) + +### Setup + +```bash +cd ionic-planb-logistic-app-flutter +flutter pub get +``` + +### Run + +```bash +flutter run # Android/iOS default device +flutter run -d chrome # Web +flutter run -d ios # iOS simulator +flutter run -d android # Android emulator +``` + +## Project Structure + +``` +lib/ +├── api/ # CQRS client & types +├── models/ # Data models +├── services/ # Auth service +├── providers/ # Riverpod state +├── pages/ # Login, Routes, Deliveries, Settings +├── l10n/ # Translations (EN/FR) +├── theme.dart # Svrnty Material Design 3 +└── main.dart # Entry point +``` + +## Key Features + +- **OAuth2/OIDC Authentication** with Keycloak +- **CQRS API Integration** with Result error handling +- **Riverpod State Management** for reactive UI +- **Internationalization** (English & French) +- **Material Design 3** with Svrnty brand colors +- **Native Features**: Camera, Phone calls, Maps +- **Strict Typing**: No `dynamic` type allowed + +## Development + +See **[CLAUDE.md](CLAUDE.md)** for: +- Detailed architecture & patterns +- Code standards & conventions +- API integration examples +- Development workflow + +## Build Commands + +```bash +flutter build web --release # Web +flutter build ios --release # iOS +flutter build appbundle --release # Android (Play Store) +``` + +## Documentation + +- **CLAUDE.md** - Complete development guidelines +- **pubspec.yaml** - Dependencies and configuration +- **[Flutter Docs](https://flutter.dev/docs)** - Official documentation + +## Version + +1.0.0+1 + +--- + +Svrnty Edition diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts new file mode 100644 index 0000000..a428786 --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.goutezplanb.planb_logistic" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.goutezplanb.planb_logistic" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..775ee20 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/goutezplanb/planb_logistic/MainActivity.kt b/android/app/src/main/kotlin/com/goutezplanb/planb_logistic/MainActivity.kt new file mode 100644 index 0000000..49cb085 --- /dev/null +++ b/android/app/src/main/kotlin/com/goutezplanb/planb_logistic/MainActivity.kt @@ -0,0 +1,5 @@ +package com.goutezplanb.planb_logistic + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/build.gradle.kts b/android/build.gradle.kts new file mode 100644 index 0000000..dbee657 --- /dev/null +++ b/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..f018a61 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ac3b479 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts new file mode 100644 index 0000000..fb605bc --- /dev/null +++ b/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.9.1" apply false + id("org.jetbrains.kotlin.android") version "2.1.0" apply false +} + +include(":app") diff --git a/assets/fonts/IBMPlexMono-Bold.ttf b/assets/fonts/IBMPlexMono-Bold.ttf new file mode 100644 index 0000000..247979c Binary files /dev/null and b/assets/fonts/IBMPlexMono-Bold.ttf differ diff --git a/assets/fonts/IBMPlexMono-BoldItalic.ttf b/assets/fonts/IBMPlexMono-BoldItalic.ttf new file mode 100644 index 0000000..2321473 Binary files /dev/null and b/assets/fonts/IBMPlexMono-BoldItalic.ttf differ diff --git a/assets/fonts/IBMPlexMono-ExtraLight.ttf b/assets/fonts/IBMPlexMono-ExtraLight.ttf new file mode 100644 index 0000000..d6ab75d Binary files /dev/null and b/assets/fonts/IBMPlexMono-ExtraLight.ttf differ diff --git a/assets/fonts/IBMPlexMono-ExtraLightItalic.ttf b/assets/fonts/IBMPlexMono-ExtraLightItalic.ttf new file mode 100644 index 0000000..88308ef Binary files /dev/null and b/assets/fonts/IBMPlexMono-ExtraLightItalic.ttf differ diff --git a/assets/fonts/IBMPlexMono-Italic.ttf b/assets/fonts/IBMPlexMono-Italic.ttf new file mode 100644 index 0000000..e259e84 Binary files /dev/null and b/assets/fonts/IBMPlexMono-Italic.ttf differ diff --git a/assets/fonts/IBMPlexMono-Light.ttf b/assets/fonts/IBMPlexMono-Light.ttf new file mode 100644 index 0000000..0dcb2fb Binary files /dev/null and b/assets/fonts/IBMPlexMono-Light.ttf differ diff --git a/assets/fonts/IBMPlexMono-LightItalic.ttf b/assets/fonts/IBMPlexMono-LightItalic.ttf new file mode 100644 index 0000000..f4a5fea Binary files /dev/null and b/assets/fonts/IBMPlexMono-LightItalic.ttf differ diff --git a/assets/fonts/IBMPlexMono-Medium.ttf b/assets/fonts/IBMPlexMono-Medium.ttf new file mode 100644 index 0000000..8253c5f Binary files /dev/null and b/assets/fonts/IBMPlexMono-Medium.ttf differ diff --git a/assets/fonts/IBMPlexMono-MediumItalic.ttf b/assets/fonts/IBMPlexMono-MediumItalic.ttf new file mode 100644 index 0000000..528b13b Binary files /dev/null and b/assets/fonts/IBMPlexMono-MediumItalic.ttf differ diff --git a/assets/fonts/IBMPlexMono-Regular.ttf b/assets/fonts/IBMPlexMono-Regular.ttf new file mode 100644 index 0000000..601ae94 Binary files /dev/null and b/assets/fonts/IBMPlexMono-Regular.ttf differ diff --git a/assets/fonts/IBMPlexMono-SemiBold.ttf b/assets/fonts/IBMPlexMono-SemiBold.ttf new file mode 100644 index 0000000..5e0b41d Binary files /dev/null and b/assets/fonts/IBMPlexMono-SemiBold.ttf differ diff --git a/assets/fonts/IBMPlexMono-SemiBoldItalic.ttf b/assets/fonts/IBMPlexMono-SemiBoldItalic.ttf new file mode 100644 index 0000000..58243dd Binary files /dev/null and b/assets/fonts/IBMPlexMono-SemiBoldItalic.ttf differ diff --git a/assets/fonts/IBMPlexMono-Thin.ttf b/assets/fonts/IBMPlexMono-Thin.ttf new file mode 100644 index 0000000..e069a64 Binary files /dev/null and b/assets/fonts/IBMPlexMono-Thin.ttf differ diff --git a/assets/fonts/IBMPlexMono-ThinItalic.ttf b/assets/fonts/IBMPlexMono-ThinItalic.ttf new file mode 100644 index 0000000..f3ed26b Binary files /dev/null and b/assets/fonts/IBMPlexMono-ThinItalic.ttf differ diff --git a/assets/fonts/Montserrat-Italic-VariableFont_wght.ttf b/assets/fonts/Montserrat-Italic-VariableFont_wght.ttf new file mode 100644 index 0000000..9f89c9d Binary files /dev/null and b/assets/fonts/Montserrat-Italic-VariableFont_wght.ttf differ diff --git a/assets/fonts/Montserrat-VariableFont_wght.ttf b/assets/fonts/Montserrat-VariableFont_wght.ttf new file mode 100644 index 0000000..df7379c Binary files /dev/null and b/assets/fonts/Montserrat-VariableFont_wght.ttf differ diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..1dc6cf7 --- /dev/null +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 13.0 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..620e46e --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..96d0084 --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,616 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.goutezplanb.planbLogistic; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.goutezplanb.planbLogistic.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.goutezplanb.planbLogistic.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.goutezplanb.planbLogistic.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.goutezplanb.planbLogistic; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.goutezplanb.planbLogistic; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..e3773d4 --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..6266644 --- /dev/null +++ b/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 0000000..ddb23aa --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Planb Logistic + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + planb_logistic + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/l10n.yaml b/l10n.yaml new file mode 100644 index 0000000..a4d323c --- /dev/null +++ b/l10n.yaml @@ -0,0 +1,5 @@ +arb-dir: lib/l10n +template-arb-file: app_en.arb +output-localization-file: app_localizations.dart +output-class: AppLocalizations +nullable-getter: false diff --git a/lib/api/client.dart b/lib/api/client.dart new file mode 100644 index 0000000..4904489 --- /dev/null +++ b/lib/api/client.dart @@ -0,0 +1,300 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'types.dart'; +import 'openapi_config.dart'; + +class CqrsApiClient { + final ApiClientConfig config; + late final http.Client _httpClient; + + CqrsApiClient({ + required this.config, + http.Client? httpClient, + }) { + _httpClient = httpClient ?? http.Client(); + } + + String get baseUrl => config.baseUrl; + + Future> executeQuery({ + required String endpoint, + required Serializable query, + required T Function(Map) fromJson, + }) async { + try { + final url = Uri.parse('$baseUrl/api/query/$endpoint'); + final headers = _buildHeaders(); + + final response = await _httpClient + .post( + url, + headers: headers, + body: jsonEncode(query.toJson()), + ) + .timeout(config.timeout); + + return _handleResponse(response, fromJson); + } on TimeoutException { + return Result.error(ApiError.timeout()); + } catch (e, stackTrace) { + return Result.error( + ApiError.unknown( + 'Failed to execute query: ${e.toString()}', + exception: Exception(stackTrace.toString()), + ), + ); + } + } + + Future>> executePaginatedQuery({ + required String endpoint, + required Serializable query, + required T Function(Map) itemFromJson, + required int page, + required int pageSize, + List? filters, + }) async { + try { + final url = Uri.parse( + '$baseUrl/api/query/$endpoint?page=$page&pageSize=$pageSize', + ); + final headers = _buildHeaders(); + + final queryData = { + ...query.toJson(), + if (filters != null && filters.isNotEmpty) + 'filters': filters.map((f) => f.toJson()).toList(), + }; + + final response = await _httpClient + .post( + url, + headers: headers, + body: jsonEncode(queryData), + ) + .timeout(config.timeout); + + return _handlePaginatedResponse(response, itemFromJson, page, pageSize); + } on TimeoutException { + return Result.error(ApiError.timeout()); + } catch (e, stackTrace) { + return Result.error( + ApiError.unknown( + 'Failed to execute paginated query: ${e.toString()}', + exception: Exception(stackTrace.toString()), + ), + ); + } + } + + Future> executeCommand({ + required String endpoint, + required Serializable command, + }) async { + try { + final url = Uri.parse('$baseUrl/api/command/$endpoint'); + final headers = _buildHeaders(); + + final response = await _httpClient + .post( + url, + headers: headers, + body: jsonEncode(command.toJson()), + ) + .timeout(config.timeout); + + if (response.statusCode >= 200 && response.statusCode < 300) { + return Result.success(null); + } else { + return _handleErrorResponse(response); + } + } on TimeoutException { + return Result.error(ApiError.timeout()); + } catch (e, stackTrace) { + return Result.error( + ApiError.unknown( + 'Failed to execute command: ${e.toString()}', + exception: Exception(stackTrace.toString()), + ), + ); + } + } + + Future> executeCommandWithResult({ + required String endpoint, + required Serializable command, + required T Function(Map) fromJson, + }) async { + try { + final url = Uri.parse('$baseUrl/api/command/$endpoint'); + final headers = _buildHeaders(); + + final response = await _httpClient + .post( + url, + headers: headers, + body: jsonEncode(command.toJson()), + ) + .timeout(config.timeout); + + return _handleResponse(response, fromJson); + } on TimeoutException { + return Result.error(ApiError.timeout()); + } catch (e, stackTrace) { + return Result.error( + ApiError.unknown( + 'Failed to execute command with result: ${e.toString()}', + exception: Exception(stackTrace.toString()), + ), + ); + } + } + + Future> uploadFile({ + required String endpoint, + required String filePath, + required String fieldName, + Map? additionalFields, + }) async { + try { + final url = Uri.parse('$baseUrl/api/command/$endpoint'); + final request = http.MultipartRequest('POST', url) + ..headers.addAll(_buildHeaders()) + ..files.add(await http.MultipartFile.fromPath(fieldName, filePath)); + + if (additionalFields != null) { + request.fields.addAll(additionalFields); + } + + final response = await request.send().timeout(config.timeout); + final responseBody = await response.stream.bytesToString(); + + if (response.statusCode >= 200 && response.statusCode < 300) { + return Result.success(responseBody); + } else { + return _parseErrorFromString(responseBody, response.statusCode); + } + } on TimeoutException { + return Result.error(ApiError.timeout()); + } catch (e, stackTrace) { + return Result.error( + ApiError.unknown( + 'Failed to upload file: ${e.toString()}', + exception: Exception(stackTrace.toString()), + ), + ); + } + } + + Map _buildHeaders() { + final headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + ...config.defaultHeaders, + }; + return headers; + } + + Result _handleResponse( + http.Response response, + T Function(Map) fromJson, + ) { + try { + if (response.statusCode >= 200 && response.statusCode < 300) { + if (response.body.isEmpty) { + return Result.success(null as T); + } + final data = jsonDecode(response.body) as Map; + return Result.success(fromJson(data)); + } else { + return _handleErrorResponse(response); + } + } catch (e) { + return Result.error( + ApiError.unknown('Failed to parse response: ${e.toString()}'), + ); + } + } + + Result> _handlePaginatedResponse( + http.Response response, + T Function(Map) itemFromJson, + int page, + int pageSize, + ) { + try { + if (response.statusCode >= 200 && response.statusCode < 300) { + final data = jsonDecode(response.body) as Map; + final items = (data['items'] as List?) + ?.map((item) => itemFromJson(item as Map)) + .toList() ?? []; + final totalCount = data['totalCount'] as int? ?? items.length; + + return Result.success( + PaginatedResult( + items: items, + page: page, + pageSize: pageSize, + totalCount: totalCount, + ), + ); + } else { + return _handleErrorResponse(response); + } + } catch (e) { + return Result.error( + ApiError.unknown('Failed to parse paginated response: ${e.toString()}'), + ); + } + } + + Result _handleErrorResponse(http.Response response) { + try { + final data = jsonDecode(response.body) as Map; + final message = data['message'] as String? ?? 'An error occurred'; + + if (response.statusCode == 422) { + final errors = data['errors'] as Map?; + final details = errors?.map( + (key, value) => MapEntry( + key, + (value as List?)?.map((e) => e.toString()).toList() ?? [], + ), + ); + return Result.error( + ApiError.validation(message, details), + ); + } + + return Result.error( + ApiError.http(statusCode: response.statusCode, message: message), + ); + } catch (e) { + return Result.error( + ApiError.http( + statusCode: response.statusCode, + message: response.reasonPhrase ?? 'Unknown error', + ), + ); + } + } + + Result _parseErrorFromString(String body, int statusCode) { + try { + final data = jsonDecode(body) as Map; + final message = data['message'] as String? ?? 'An error occurred'; + return Result.error( + ApiError.http(statusCode: statusCode, message: message), + ); + } catch (e) { + return Result.error( + ApiError.http(statusCode: statusCode, message: 'Unknown error'), + ); + } + } + + void close() { + _httpClient.close(); + } +} diff --git a/lib/api/openapi_config.dart b/lib/api/openapi_config.dart new file mode 100644 index 0000000..7bbd0f3 --- /dev/null +++ b/lib/api/openapi_config.dart @@ -0,0 +1,21 @@ +class ApiClientConfig { + final String baseUrl; + final Duration timeout; + final Map defaultHeaders; + + const ApiClientConfig({ + required this.baseUrl, + this.timeout = const Duration(seconds: 30), + this.defaultHeaders = const {}, + }); + + static const ApiClientConfig development = ApiClientConfig( + baseUrl: 'https://api-route.goutezplanb.com', + timeout: Duration(seconds: 30), + ); + + static const ApiClientConfig production = ApiClientConfig( + baseUrl: 'https://api-route.goutezplanb.com', + timeout: Duration(seconds: 30), + ); +} diff --git a/lib/api/types.dart b/lib/api/types.dart new file mode 100644 index 0000000..edac4d2 --- /dev/null +++ b/lib/api/types.dart @@ -0,0 +1,160 @@ +abstract interface class Serializable { + Map toJson(); +} + +enum ApiErrorType { + network, + timeout, + validation, + http, + unknown, +} + +class ApiError { + final ApiErrorType type; + final String message; + final int? statusCode; + final Map>? details; + final Exception? originalException; + + const ApiError({ + required this.type, + required this.message, + this.statusCode, + this.details, + this.originalException, + }); + + factory ApiError.network(String message) => ApiError( + type: ApiErrorType.network, + message: message, + ); + + factory ApiError.timeout() => const ApiError( + type: ApiErrorType.timeout, + message: 'Request timeout', + ); + + factory ApiError.validation(String message, Map>? details) => ApiError( + type: ApiErrorType.validation, + message: message, + details: details, + ); + + factory ApiError.http({ + required int statusCode, + required String message, + }) => ApiError( + type: ApiErrorType.http, + message: message, + statusCode: statusCode, + ); + + factory ApiError.unknown(String message, {Exception? exception}) => ApiError( + type: ApiErrorType.unknown, + message: message, + originalException: exception, + ); +} + +sealed class Result { + const Result(); + + factory Result.success(T data) => Success(data); + + factory Result.error(ApiError error) => Error(error); + + R when({ + required R Function(T data) success, + required R Function(ApiError error) onError, + }) { + return switch (this) { + Success(:final data) => success(data), + Error(:final error) => onError(error), + }; + } + + R? whenSuccess(R Function(T data) fn) { + return switch (this) { + Success(:final data) => fn(data), + Error() => null, + }; + } + + R? whenError(R Function(ApiError error) fn) { + return switch (this) { + Success() => null, + Error(:final error) => fn(error), + }; + } + + bool get isSuccess => this is Success; + bool get isError => this is Error; + + T? getOrNull() => whenSuccess((data) => data); + ApiError? getErrorOrNull() => whenError((error) => error); +} + +final class Success extends Result { + final T data; + + const Success(this.data); +} + +final class Error extends Result { + final ApiError error; + + const Error(this.error); +} + +class PaginatedResult { + final List items; + final int page; + final int pageSize; + final int totalCount; + + const PaginatedResult({ + required this.items, + required this.page, + required this.pageSize, + required this.totalCount, + }); + + int get totalPages => (totalCount / pageSize).ceil(); + bool get hasNextPage => page < totalPages; +} + +enum FilterOperator { + equals('eq'), + notEquals('neq'), + greaterThan('gt'), + greaterThanOrEqual('gte'), + lessThan('lt'), + lessThanOrEqual('lte'), + contains('contains'), + startsWith('startsWith'), + endsWith('endsWith'), + in_('in'); + + final String operator; + const FilterOperator(this.operator); +} + +class FilterCriteria implements Serializable { + final String field; + final FilterOperator operator; + final Object? value; + + FilterCriteria({ + required this.field, + required this.operator, + required this.value, + }); + + @override + Map toJson() => { + 'field': field, + 'operator': operator.operator, + 'value': value, + }; +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb new file mode 100644 index 0000000..9314be6 --- /dev/null +++ b/lib/l10n/app_en.arb @@ -0,0 +1,69 @@ +{ + "@@locale": "en", + "appTitle": "Plan B Logistics", + "appDescription": "Delivery Management System", + "loginWithKeycloak": "Login with Keycloak", + "deliveryRoutes": "Delivery Routes", + "routes": "Routes", + "deliveries": "Deliveries", + "settings": "Settings", + "profile": "Profile", + "logout": "Logout", + "completed": "Completed", + "pending": "Pending", + "todo": "To Do", + "delivered": "Delivered", + "newCustomer": "New Customer", + "items": "{count} items", + "@items": { + "placeholders": { + "count": {"type": "int"} + } + }, + "moneyCurrency": "{amount} MAD", + "@moneyCurrency": { + "placeholders": { + "amount": {"type": "double"} + } + }, + "call": "Call", + "map": "Map", + "more": "More", + "markAsCompleted": "Mark as Completed", + "markAsUncompleted": "Mark as Uncompleted", + "uploadPhoto": "Upload Photo", + "viewDetails": "View Details", + "deliverySuccessful": "Delivery marked as completed", + "deliveryFailed": "Failed to mark delivery", + "noDeliveries": "No deliveries", + "noRoutes": "No routes available", + "error": "Error: {message}", + "@error": { + "placeholders": { + "message": {"type": "String"} + } + }, + "retry": "Retry", + "authenticationRequired": "Authentication required", + "phoneCall": "Call customer", + "navigateToAddress": "Show on map", + "language": "Language", + "english": "English", + "french": "French", + "appVersion": "App Version", + "about": "About", + "fullName": "{firstName} {lastName}", + "@fullName": { + "placeholders": { + "firstName": {"type": "String"}, + "lastName": {"type": "String"} + } + }, + "completedDeliveries": "{completed}/{total} completed", + "@completedDeliveries": { + "placeholders": { + "completed": {"type": "int"}, + "total": {"type": "int"} + } + } +} diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb new file mode 100644 index 0000000..527848f --- /dev/null +++ b/lib/l10n/app_fr.arb @@ -0,0 +1,69 @@ +{ + "@@locale": "fr", + "appTitle": "Plan B Logistique", + "appDescription": "Systme de Gestion des Livraisons", + "loginWithKeycloak": "Connexion avec Keycloak", + "deliveryRoutes": "Itinraires de Livraison", + "routes": "Itinraires", + "deliveries": "Livraisons", + "settings": "Paramtres", + "profile": "Profil", + "logout": "Dconnexion", + "completed": "Livr", + "pending": "En attente", + "todo": "livrer", + "delivered": "Livr", + "newCustomer": "Nouveau Client", + "items": "{count} articles", + "@items": { + "placeholders": { + "count": {"type": "int"} + } + }, + "moneyCurrency": "{amount} MAD", + "@moneyCurrency": { + "placeholders": { + "amount": {"type": "double"} + } + }, + "call": "Appeler", + "map": "Carte", + "more": "Plus", + "markAsCompleted": "Marquer comme livr", + "markAsUncompleted": "Marquer comme livrer", + "uploadPhoto": "Tlcharger une photo", + "viewDetails": "Voir les dtails", + "deliverySuccessful": "Livraison marque comme complte", + "deliveryFailed": "chec du marquage de la livraison", + "noDeliveries": "Aucune livraison", + "noRoutes": "Aucun itinraire disponible", + "error": "Erreur: {message}", + "@error": { + "placeholders": { + "message": {"type": "String"} + } + }, + "retry": "Ressayer", + "authenticationRequired": "Authentification requise", + "phoneCall": "Appeler le client", + "navigateToAddress": "Afficher sur la carte", + "language": "Langue", + "english": "English", + "french": "Franais", + "appVersion": "Version de l'application", + "about": " propos", + "fullName": "{firstName} {lastName}", + "@fullName": { + "placeholders": { + "firstName": {"type": "String"}, + "lastName": {"type": "String"} + } + }, + "completedDeliveries": "{completed}/{total} livrs", + "@completedDeliveries": { + "placeholders": { + "completed": {"type": "int"}, + "total": {"type": "int"} + } + } +} diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart new file mode 100644 index 0000000..5493696 --- /dev/null +++ b/lib/l10n/app_localizations.dart @@ -0,0 +1,368 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:intl/intl.dart' as intl; + +import 'app_localizations_en.dart'; +import 'app_localizations_fr.dart'; + +// ignore_for_file: type=lint + +/// Callers can lookup localized strings with an instance of AppLocalizations +/// returned by `AppLocalizations.of(context)`. +/// +/// Applications need to include `AppLocalizations.delegate()` in their app's +/// `localizationDelegates` list, and the locales they support in the app's +/// `supportedLocales` list. For example: +/// +/// ```dart +/// import 'l10n/app_localizations.dart'; +/// +/// return MaterialApp( +/// localizationsDelegates: AppLocalizations.localizationsDelegates, +/// supportedLocales: AppLocalizations.supportedLocales, +/// home: MyApplicationHome(), +/// ); +/// ``` +/// +/// ## Update pubspec.yaml +/// +/// Please make sure to update your pubspec.yaml to include the following +/// packages: +/// +/// ```yaml +/// dependencies: +/// # Internationalization support. +/// flutter_localizations: +/// sdk: flutter +/// intl: any # Use the pinned version from flutter_localizations +/// +/// # Rest of dependencies +/// ``` +/// +/// ## iOS Applications +/// +/// iOS applications define key application metadata, including supported +/// locales, in an Info.plist file that is built into the application bundle. +/// To configure the locales supported by your app, you’ll need to edit this +/// file. +/// +/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. +/// Then, in the Project Navigator, open the Info.plist file under the Runner +/// project’s Runner folder. +/// +/// Next, select the Information Property List item, select Add Item from the +/// Editor menu, then select Localizations from the pop-up menu. +/// +/// Select and expand the newly-created Localizations item then, for each +/// locale your application supports, add a new item and select the locale +/// you wish to add from the pop-up menu in the Value field. This list should +/// be consistent with the languages listed in the AppLocalizations.supportedLocales +/// property. +abstract class AppLocalizations { + AppLocalizations(String locale) + : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + + final String localeName; + + static AppLocalizations of(BuildContext context) { + return Localizations.of(context, AppLocalizations)!; + } + + static const LocalizationsDelegate delegate = + _AppLocalizationsDelegate(); + + /// A list of this localizations delegate along with the default localizations + /// delegates. + /// + /// Returns a list of localizations delegates containing this delegate along with + /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, + /// and GlobalWidgetsLocalizations.delegate. + /// + /// Additional delegates can be added by appending to this list in + /// MaterialApp. This list does not have to be used at all if a custom list + /// of delegates is preferred or required. + static const List> localizationsDelegates = + >[ + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; + + /// A list of this localizations delegate's supported locales. + static const List supportedLocales = [ + Locale('en'), + Locale('fr'), + ]; + + /// No description provided for @appTitle. + /// + /// In en, this message translates to: + /// **'Plan B Logistics'** + String get appTitle; + + /// No description provided for @appDescription. + /// + /// In en, this message translates to: + /// **'Delivery Management System'** + String get appDescription; + + /// No description provided for @loginWithKeycloak. + /// + /// In en, this message translates to: + /// **'Login with Keycloak'** + String get loginWithKeycloak; + + /// No description provided for @deliveryRoutes. + /// + /// In en, this message translates to: + /// **'Delivery Routes'** + String get deliveryRoutes; + + /// No description provided for @routes. + /// + /// In en, this message translates to: + /// **'Routes'** + String get routes; + + /// No description provided for @deliveries. + /// + /// In en, this message translates to: + /// **'Deliveries'** + String get deliveries; + + /// No description provided for @settings. + /// + /// In en, this message translates to: + /// **'Settings'** + String get settings; + + /// No description provided for @profile. + /// + /// In en, this message translates to: + /// **'Profile'** + String get profile; + + /// No description provided for @logout. + /// + /// In en, this message translates to: + /// **'Logout'** + String get logout; + + /// No description provided for @completed. + /// + /// In en, this message translates to: + /// **'Completed'** + String get completed; + + /// No description provided for @pending. + /// + /// In en, this message translates to: + /// **'Pending'** + String get pending; + + /// No description provided for @todo. + /// + /// In en, this message translates to: + /// **'To Do'** + String get todo; + + /// No description provided for @delivered. + /// + /// In en, this message translates to: + /// **'Delivered'** + String get delivered; + + /// No description provided for @newCustomer. + /// + /// In en, this message translates to: + /// **'New Customer'** + String get newCustomer; + + /// No description provided for @items. + /// + /// In en, this message translates to: + /// **'{count} items'** + String items(int count); + + /// No description provided for @moneyCurrency. + /// + /// In en, this message translates to: + /// **'{amount} MAD'** + String moneyCurrency(double amount); + + /// No description provided for @call. + /// + /// In en, this message translates to: + /// **'Call'** + String get call; + + /// No description provided for @map. + /// + /// In en, this message translates to: + /// **'Map'** + String get map; + + /// No description provided for @more. + /// + /// In en, this message translates to: + /// **'More'** + String get more; + + /// No description provided for @markAsCompleted. + /// + /// In en, this message translates to: + /// **'Mark as Completed'** + String get markAsCompleted; + + /// No description provided for @markAsUncompleted. + /// + /// In en, this message translates to: + /// **'Mark as Uncompleted'** + String get markAsUncompleted; + + /// No description provided for @uploadPhoto. + /// + /// In en, this message translates to: + /// **'Upload Photo'** + String get uploadPhoto; + + /// No description provided for @viewDetails. + /// + /// In en, this message translates to: + /// **'View Details'** + String get viewDetails; + + /// No description provided for @deliverySuccessful. + /// + /// In en, this message translates to: + /// **'Delivery marked as completed'** + String get deliverySuccessful; + + /// No description provided for @deliveryFailed. + /// + /// In en, this message translates to: + /// **'Failed to mark delivery'** + String get deliveryFailed; + + /// No description provided for @noDeliveries. + /// + /// In en, this message translates to: + /// **'No deliveries'** + String get noDeliveries; + + /// No description provided for @noRoutes. + /// + /// In en, this message translates to: + /// **'No routes available'** + String get noRoutes; + + /// No description provided for @error. + /// + /// In en, this message translates to: + /// **'Error: {message}'** + String error(String message); + + /// No description provided for @retry. + /// + /// In en, this message translates to: + /// **'Retry'** + String get retry; + + /// No description provided for @authenticationRequired. + /// + /// In en, this message translates to: + /// **'Authentication required'** + String get authenticationRequired; + + /// No description provided for @phoneCall. + /// + /// In en, this message translates to: + /// **'Call customer'** + String get phoneCall; + + /// No description provided for @navigateToAddress. + /// + /// In en, this message translates to: + /// **'Show on map'** + String get navigateToAddress; + + /// No description provided for @language. + /// + /// In en, this message translates to: + /// **'Language'** + String get language; + + /// No description provided for @english. + /// + /// In en, this message translates to: + /// **'English'** + String get english; + + /// No description provided for @french. + /// + /// In en, this message translates to: + /// **'French'** + String get french; + + /// No description provided for @appVersion. + /// + /// In en, this message translates to: + /// **'App Version'** + String get appVersion; + + /// No description provided for @about. + /// + /// In en, this message translates to: + /// **'About'** + String get about; + + /// No description provided for @fullName. + /// + /// In en, this message translates to: + /// **'{firstName} {lastName}'** + String fullName(String firstName, String lastName); + + /// No description provided for @completedDeliveries. + /// + /// In en, this message translates to: + /// **'{completed}/{total} completed'** + String completedDeliveries(int completed, int total); +} + +class _AppLocalizationsDelegate + extends LocalizationsDelegate { + const _AppLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return SynchronousFuture(lookupAppLocalizations(locale)); + } + + @override + bool isSupported(Locale locale) => + ['en', 'fr'].contains(locale.languageCode); + + @override + bool shouldReload(_AppLocalizationsDelegate old) => false; +} + +AppLocalizations lookupAppLocalizations(Locale locale) { + // Lookup logic when only language code is specified. + switch (locale.languageCode) { + case 'en': + return AppLocalizationsEn(); + case 'fr': + return AppLocalizationsFr(); + } + + throw FlutterError( + 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.', + ); +} diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart new file mode 100644 index 0000000..5ec892f --- /dev/null +++ b/lib/l10n/app_localizations_en.dart @@ -0,0 +1,137 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for English (`en`). +class AppLocalizationsEn extends AppLocalizations { + AppLocalizationsEn([String locale = 'en']) : super(locale); + + @override + String get appTitle => 'Plan B Logistics'; + + @override + String get appDescription => 'Delivery Management System'; + + @override + String get loginWithKeycloak => 'Login with Keycloak'; + + @override + String get deliveryRoutes => 'Delivery Routes'; + + @override + String get routes => 'Routes'; + + @override + String get deliveries => 'Deliveries'; + + @override + String get settings => 'Settings'; + + @override + String get profile => 'Profile'; + + @override + String get logout => 'Logout'; + + @override + String get completed => 'Completed'; + + @override + String get pending => 'Pending'; + + @override + String get todo => 'To Do'; + + @override + String get delivered => 'Delivered'; + + @override + String get newCustomer => 'New Customer'; + + @override + String items(int count) { + return '$count items'; + } + + @override + String moneyCurrency(double amount) { + return '$amount MAD'; + } + + @override + String get call => 'Call'; + + @override + String get map => 'Map'; + + @override + String get more => 'More'; + + @override + String get markAsCompleted => 'Mark as Completed'; + + @override + String get markAsUncompleted => 'Mark as Uncompleted'; + + @override + String get uploadPhoto => 'Upload Photo'; + + @override + String get viewDetails => 'View Details'; + + @override + String get deliverySuccessful => 'Delivery marked as completed'; + + @override + String get deliveryFailed => 'Failed to mark delivery'; + + @override + String get noDeliveries => 'No deliveries'; + + @override + String get noRoutes => 'No routes available'; + + @override + String error(String message) { + return 'Error: $message'; + } + + @override + String get retry => 'Retry'; + + @override + String get authenticationRequired => 'Authentication required'; + + @override + String get phoneCall => 'Call customer'; + + @override + String get navigateToAddress => 'Show on map'; + + @override + String get language => 'Language'; + + @override + String get english => 'English'; + + @override + String get french => 'French'; + + @override + String get appVersion => 'App Version'; + + @override + String get about => 'About'; + + @override + String fullName(String firstName, String lastName) { + return '$firstName $lastName'; + } + + @override + String completedDeliveries(int completed, int total) { + return '$completed/$total completed'; + } +} diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart new file mode 100644 index 0000000..5d5b701 --- /dev/null +++ b/lib/l10n/app_localizations_fr.dart @@ -0,0 +1,137 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for French (`fr`). +class AppLocalizationsFr extends AppLocalizations { + AppLocalizationsFr([String locale = 'fr']) : super(locale); + + @override + String get appTitle => 'Plan B Logistique'; + + @override + String get appDescription => 'Systme de Gestion des Livraisons'; + + @override + String get loginWithKeycloak => 'Connexion avec Keycloak'; + + @override + String get deliveryRoutes => 'Itinraires de Livraison'; + + @override + String get routes => 'Itinraires'; + + @override + String get deliveries => 'Livraisons'; + + @override + String get settings => 'Paramtres'; + + @override + String get profile => 'Profil'; + + @override + String get logout => 'Dconnexion'; + + @override + String get completed => 'Livr'; + + @override + String get pending => 'En attente'; + + @override + String get todo => 'livrer'; + + @override + String get delivered => 'Livr'; + + @override + String get newCustomer => 'Nouveau Client'; + + @override + String items(int count) { + return '$count articles'; + } + + @override + String moneyCurrency(double amount) { + return '$amount MAD'; + } + + @override + String get call => 'Appeler'; + + @override + String get map => 'Carte'; + + @override + String get more => 'Plus'; + + @override + String get markAsCompleted => 'Marquer comme livr'; + + @override + String get markAsUncompleted => 'Marquer comme livrer'; + + @override + String get uploadPhoto => 'Tlcharger une photo'; + + @override + String get viewDetails => 'Voir les dtails'; + + @override + String get deliverySuccessful => 'Livraison marque comme complte'; + + @override + String get deliveryFailed => 'chec du marquage de la livraison'; + + @override + String get noDeliveries => 'Aucune livraison'; + + @override + String get noRoutes => 'Aucun itinraire disponible'; + + @override + String error(String message) { + return 'Erreur: $message'; + } + + @override + String get retry => 'Ressayer'; + + @override + String get authenticationRequired => 'Authentification requise'; + + @override + String get phoneCall => 'Appeler le client'; + + @override + String get navigateToAddress => 'Afficher sur la carte'; + + @override + String get language => 'Langue'; + + @override + String get english => 'English'; + + @override + String get french => 'Franais'; + + @override + String get appVersion => 'Version de l\'application'; + + @override + String get about => ' propos'; + + @override + String fullName(String firstName, String lastName) { + return '$firstName $lastName'; + } + + @override + String completedDeliveries(int completed, int total) { + return '$completed/$total livrs'; + } +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..8279eeb --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'theme.dart'; +import 'providers/providers.dart'; +import 'pages/login_page.dart'; +import 'pages/routes_page.dart'; + +void main() { + runApp( + const ProviderScope( + child: PlanBLogisticApp(), + ), + ); +} + +class PlanBLogisticApp extends ConsumerWidget { + const PlanBLogisticApp({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final language = ref.watch(languageProvider); + + return MaterialApp( + title: 'Plan B Logistics', + theme: MaterialTheme(const TextTheme()).light(), + darkTheme: MaterialTheme(const TextTheme()).dark(), + themeMode: ThemeMode.dark, + locale: Locale(language), + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [ + Locale('en', ''), + Locale('fr', ''), + ], + home: const AppHome(), + ); + } +} + +class AppHome extends ConsumerWidget { + const AppHome({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + // TODO: Re-enable authentication when Keycloak is configured + // For now, bypass auth and go directly to RoutesPage + return const RoutesPage(); + } +} diff --git a/lib/models/delivery.dart b/lib/models/delivery.dart new file mode 100644 index 0000000..e115a36 --- /dev/null +++ b/lib/models/delivery.dart @@ -0,0 +1,81 @@ +import '../api/types.dart'; +import 'delivery_address.dart'; +import 'delivery_order.dart'; +import 'user_info.dart'; + +class Delivery implements Serializable { + final int id; + final int routeFragmentId; + final int deliveryIndex; + final List orders; + final UserInfo? deliveredBy; + final DeliveryAddress? deliveryAddress; + final String? deliveredAt; + final String? skippedAt; + final String createdAt; + final String? updatedAt; + final bool delivered; + final bool hasBeenSkipped; + final bool isSkipped; + final String name; + + const Delivery({ + required this.id, + required this.routeFragmentId, + required this.deliveryIndex, + required this.orders, + this.deliveredBy, + this.deliveryAddress, + this.deliveredAt, + this.skippedAt, + required this.createdAt, + this.updatedAt, + required this.delivered, + required this.hasBeenSkipped, + required this.isSkipped, + required this.name, + }); + + factory Delivery.fromJson(Map json) { + return Delivery( + id: json['id'] as int, + routeFragmentId: json['routeFragmentId'] as int, + deliveryIndex: json['deliveryIndex'] as int, + orders: (json['orders'] as List?) + ?.map((e) => DeliveryOrder.fromJson(e as Map)) + .toList() ?? [], + deliveredBy: json['deliveredBy'] != null + ? UserInfo.fromJson(json['deliveredBy'] as Map) + : null, + deliveryAddress: json['deliveryAddress'] != null + ? DeliveryAddress.fromJson(json['deliveryAddress'] as Map) + : null, + deliveredAt: json['deliveredAt'] as String?, + skippedAt: json['skippedAt'] as String?, + createdAt: json['createdAt'] as String, + updatedAt: json['updatedAt'] as String?, + delivered: json['delivered'] as bool, + hasBeenSkipped: json['hasBeenSkipped'] as bool, + isSkipped: json['isSkipped'] as bool, + name: json['name'] as String, + ); + } + + @override + Map toJson() => { + 'id': id, + 'routeFragmentId': routeFragmentId, + 'deliveryIndex': deliveryIndex, + 'orders': orders.map((o) => o.toJson()).toList(), + 'deliveredBy': deliveredBy?.toJson(), + 'deliveryAddress': deliveryAddress?.toJson(), + 'deliveredAt': deliveredAt, + 'skippedAt': skippedAt, + 'createdAt': createdAt, + 'updatedAt': updatedAt, + 'delivered': delivered, + 'hasBeenSkipped': hasBeenSkipped, + 'isSkipped': isSkipped, + 'name': name, + }; +} diff --git a/lib/models/delivery_address.dart b/lib/models/delivery_address.dart new file mode 100644 index 0000000..371ed01 --- /dev/null +++ b/lib/models/delivery_address.dart @@ -0,0 +1,56 @@ +import '../api/types.dart'; + +class DeliveryAddress implements Serializable { + final int id; + final String line1; + final String line2; + final String postalCode; + final String city; + final String subdivision; + final String countryCode; + final double? latitude; + final double? longitude; + final String formattedAddress; + + const DeliveryAddress({ + required this.id, + required this.line1, + required this.line2, + required this.postalCode, + required this.city, + required this.subdivision, + required this.countryCode, + this.latitude, + this.longitude, + required this.formattedAddress, + }); + + factory DeliveryAddress.fromJson(Map json) { + return DeliveryAddress( + id: json['id'] as int, + line1: json['line1'] as String, + line2: json['line2'] as String, + postalCode: json['postalCode'] as String, + city: json['city'] as String, + subdivision: json['subdivision'] as String, + countryCode: json['countryCode'] as String, + latitude: json['latitude'] as double?, + longitude: json['longitude'] as double?, + formattedAddress: json['formattedAddress'] as String, + ); + } + + @override + Map toJson() => { + 'id': id, + 'line1': line1, + 'line2': line2, + 'postalCode': postalCode, + 'city': city, + 'subdivision': subdivision, + 'countryCode': countryCode, + 'latitude': latitude, + 'longitude': longitude, + 'formattedAddress': formattedAddress, + }; +} diff --git a/lib/models/delivery_commands.dart b/lib/models/delivery_commands.dart new file mode 100644 index 0000000..cc656b6 --- /dev/null +++ b/lib/models/delivery_commands.dart @@ -0,0 +1,59 @@ +import '../api/types.dart'; + +class CompleteDeliveryCommand implements Serializable { + final int deliveryId; + final String? deliveredAt; + + const CompleteDeliveryCommand({ + required this.deliveryId, + this.deliveredAt, + }); + + @override + Map toJson() => { + 'deliveryId': deliveryId, + 'deliveredAt': deliveredAt, + }; +} + +class MarkDeliveryAsUncompletedCommand implements Serializable { + final int deliveryId; + + const MarkDeliveryAsUncompletedCommand({ + required this.deliveryId, + }); + + @override + Map toJson() => { + 'deliveryId': deliveryId, + }; +} + +class UploadDeliveryPictureCommand implements Serializable { + final int deliveryId; + final String filePath; + + const UploadDeliveryPictureCommand({ + required this.deliveryId, + required this.filePath, + }); + + @override + Map toJson() => { + 'deliveryId': deliveryId, + 'filePath': filePath, + }; +} + +class SkipDeliveryCommand implements Serializable { + final int deliveryId; + + const SkipDeliveryCommand({ + required this.deliveryId, + }); + + @override + Map toJson() => { + 'deliveryId': deliveryId, + }; +} diff --git a/lib/models/delivery_contact.dart b/lib/models/delivery_contact.dart new file mode 100644 index 0000000..a1d2f3d --- /dev/null +++ b/lib/models/delivery_contact.dart @@ -0,0 +1,32 @@ +import '../api/types.dart'; + +class DeliveryContact implements Serializable { + final String firstName; + final String? lastName; + final String fullName; + final String? phoneNumber; + + const DeliveryContact({ + required this.firstName, + this.lastName, + required this.fullName, + this.phoneNumber, + }); + + factory DeliveryContact.fromJson(Map json) { + return DeliveryContact( + firstName: json['firstName'] as String, + lastName: json['lastName'] as String?, + fullName: json['fullName'] as String, + phoneNumber: json['phoneNumber'] as String?, + ); + } + + @override + Map toJson() => { + 'firstName': firstName, + 'lastName': lastName, + 'fullName': fullName, + 'phoneNumber': phoneNumber, + }; +} diff --git a/lib/models/delivery_order.dart b/lib/models/delivery_order.dart new file mode 100644 index 0000000..ef83555 --- /dev/null +++ b/lib/models/delivery_order.dart @@ -0,0 +1,53 @@ +import '../api/types.dart'; +import 'delivery_contact.dart'; + +class DeliveryOrder implements Serializable { + final int id; + final bool isNewCustomer; + final String? note; + final double totalAmount; + final double? totalPaid; + final int? totalItems; + final List contacts; + final DeliveryContact? contact; + + const DeliveryOrder({ + required this.id, + required this.isNewCustomer, + this.note, + required this.totalAmount, + this.totalPaid, + this.totalItems, + required this.contacts, + this.contact, + }); + + factory DeliveryOrder.fromJson(Map json) { + return DeliveryOrder( + id: json['id'] as int, + isNewCustomer: json['isNewCustomer'] as bool, + note: json['note'] as String?, + totalAmount: (json['totalAmount'] as num).toDouble(), + totalPaid: (json['totalPaid'] as num?)?.toDouble(), + totalItems: json['totalItems'] as int?, + contacts: (json['contacts'] as List?) + ?.map((e) => DeliveryContact.fromJson(e as Map)) + .toList() ?? [], + contact: json['contact'] != null + ? DeliveryContact.fromJson(json['contact'] as Map) + : null, + ); + } + + @override + Map toJson() => { + 'id': id, + 'isNewCustomer': isNewCustomer, + 'note': note, + 'totalAmount': totalAmount, + 'totalPaid': totalPaid, + 'totalItems': totalItems, + 'contacts': contacts.map((c) => c.toJson()).toList(), + 'contact': contact?.toJson(), + }; +} diff --git a/lib/models/delivery_route.dart b/lib/models/delivery_route.dart new file mode 100644 index 0000000..0a46c93 --- /dev/null +++ b/lib/models/delivery_route.dart @@ -0,0 +1,59 @@ +import '../api/types.dart'; + +class DeliveryRoute implements Serializable { + final int id; + final String name; + final String? description; + final int routeFragmentId; + final int totalDeliveries; + final int completedDeliveries; + final int skippedDeliveries; + final String createdAt; + final String? updatedAt; + + const DeliveryRoute({ + required this.id, + required this.name, + this.description, + required this.routeFragmentId, + required this.totalDeliveries, + required this.completedDeliveries, + required this.skippedDeliveries, + required this.createdAt, + this.updatedAt, + }); + + factory DeliveryRoute.fromJson(Map json) { + return DeliveryRoute( + id: json['id'] as int, + name: json['name'] as String, + description: json['description'] as String?, + routeFragmentId: json['routeFragmentId'] as int, + totalDeliveries: json['totalDeliveries'] as int, + completedDeliveries: json['completedDeliveries'] as int, + skippedDeliveries: json['skippedDeliveries'] as int, + createdAt: json['createdAt'] as String, + updatedAt: json['updatedAt'] as String?, + ); + } + + double get progress { + if (totalDeliveries == 0) return 0.0; + return completedDeliveries / totalDeliveries; + } + + int get pendingDeliveries => totalDeliveries - completedDeliveries - skippedDeliveries; + + @override + Map toJson() => { + 'id': id, + 'name': name, + 'description': description, + 'routeFragmentId': routeFragmentId, + 'totalDeliveries': totalDeliveries, + 'completedDeliveries': completedDeliveries, + 'skippedDeliveries': skippedDeliveries, + 'createdAt': createdAt, + 'updatedAt': updatedAt, + }; +} diff --git a/lib/models/user_info.dart b/lib/models/user_info.dart new file mode 100644 index 0000000..422a532 --- /dev/null +++ b/lib/models/user_info.dart @@ -0,0 +1,32 @@ +import '../api/types.dart'; + +class UserInfo implements Serializable { + final int id; + final String firstName; + final String? lastName; + final String fullName; + + const UserInfo({ + required this.id, + required this.firstName, + this.lastName, + required this.fullName, + }); + + factory UserInfo.fromJson(Map json) { + return UserInfo( + id: json['id'] as int, + firstName: json['firstName'] as String, + lastName: json['lastName'] as String?, + fullName: json['fullName'] as String, + ); + } + + @override + Map toJson() => { + 'id': id, + 'firstName': firstName, + 'lastName': lastName, + 'fullName': fullName, + }; +} diff --git a/lib/models/user_profile.dart b/lib/models/user_profile.dart new file mode 100644 index 0000000..67897c7 --- /dev/null +++ b/lib/models/user_profile.dart @@ -0,0 +1,24 @@ +class UserProfile { + final String firstName; + final String lastName; + final String email; + + const UserProfile({ + required this.firstName, + required this.lastName, + required this.email, + }); + + String get fullName => '$firstName $lastName'; + + factory UserProfile.fromJwtClaims(Map claims) { + return UserProfile( + firstName: claims['given_name'] as String? ?? '', + lastName: claims['family_name'] as String? ?? '', + email: claims['email'] as String? ?? '', + ); + } + + @override + String toString() => 'UserProfile(firstName: $firstName, lastName: $lastName, email: $email)'; +} diff --git a/lib/pages/deliveries_page.dart b/lib/pages/deliveries_page.dart new file mode 100644 index 0000000..17cdaf0 --- /dev/null +++ b/lib/pages/deliveries_page.dart @@ -0,0 +1,416 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:url_launcher/url_launcher.dart'; +import '../models/delivery.dart'; +import '../providers/providers.dart'; +import '../api/client.dart'; +import '../api/openapi_config.dart'; +import '../models/delivery_commands.dart'; +import '../utils/breakpoints.dart'; +import '../utils/responsive.dart'; + +class DeliveriesPage extends ConsumerStatefulWidget { + final int routeFragmentId; + final String routeName; + + const DeliveriesPage({ + super.key, + required this.routeFragmentId, + required this.routeName, + }); + + @override + ConsumerState createState() => _DeliveriesPageState(); +} + +class _DeliveriesPageState extends ConsumerState { + late PageController _pageController; + int _currentSegment = 0; + + @override + void initState() { + super.initState(); + _pageController = PageController(); + } + + @override + void dispose() { + _pageController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final deliveriesData = ref.watch(deliveriesProvider(widget.routeFragmentId)); + final token = ref.watch(authTokenProvider).valueOrNull; + + return Scaffold( + appBar: AppBar( + title: Text(widget.routeName), + elevation: 0, + ), + body: deliveriesData.when( + data: (deliveries) { + final todoDeliveries = deliveries + .where((d) => !d.delivered && !d.isSkipped) + .toList(); + final completedDeliveries = deliveries + .where((d) => d.delivered) + .toList(); + + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: SegmentedButton( + segments: const [ + ButtonSegment( + value: 0, + label: Text('To Do'), + ), + ButtonSegment( + value: 1, + label: Text('Delivered'), + ), + ], + selected: {_currentSegment}, + onSelectionChanged: (Set newSelection) { + setState(() { + _currentSegment = newSelection.first; + _pageController.animateToPage( + _currentSegment, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + }); + }, + ), + ), + Expanded( + child: PageView( + controller: _pageController, + onPageChanged: (index) { + setState(() { + _currentSegment = index; + }); + }, + children: [ + DeliveryListView( + deliveries: todoDeliveries, + onAction: (delivery, action) => + _handleDeliveryAction(context, delivery, action, token), + ), + DeliveryListView( + deliveries: completedDeliveries, + onAction: (delivery, action) => + _handleDeliveryAction(context, delivery, action, token), + ), + ], + ), + ), + ], + ); + }, + loading: () => const Center( + child: CircularProgressIndicator(), + ), + error: (error, stackTrace) => Center( + child: Text('Error: $error'), + ), + ), + ); + } + + Future _handleDeliveryAction( + BuildContext context, + Delivery delivery, + String action, + String? token, + ) async { + if (token == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Authentication required')), + ); + return; + } + + final authClient = CqrsApiClient( + config: ApiClientConfig( + baseUrl: ApiClientConfig.production.baseUrl, + defaultHeaders: {'Authorization': 'Bearer $token'}, + ), + ); + + switch (action) { + case 'complete': + final result = await authClient.executeCommand( + endpoint: 'completeDelivery', + command: CompleteDeliveryCommand( + deliveryId: delivery.id, + deliveredAt: DateTime.now().toIso8601String(), + ), + ); + result.when( + success: (_) { + // ignore: unused_result + ref.refresh(deliveriesProvider(widget.routeFragmentId)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Delivery marked as completed')), + ); + }, + onError: (error) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error: ${error.message}')), + ); + }, + ); + break; + + case 'uncomplete': + final result = await authClient.executeCommand( + endpoint: 'markDeliveryAsUncompleted', + command: MarkDeliveryAsUncompletedCommand(deliveryId: delivery.id), + ); + result.when( + success: (_) { + // ignore: unused_result + ref.refresh(deliveriesProvider(widget.routeFragmentId)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Delivery marked as uncompleted')), + ); + }, + onError: (error) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error: ${error.message}')), + ); + }, + ); + break; + + case 'call': + final contact = delivery.orders.isNotEmpty && delivery.orders.first.contact != null + ? delivery.orders.first.contact + : null; + if (contact?.phoneNumber != null) { + final Uri phoneUri = Uri(scheme: 'tel', path: contact!.phoneNumber); + if (await canLaunchUrl(phoneUri)) { + await launchUrl(phoneUri); + } + } + break; + + case 'map': + if (delivery.deliveryAddress != null) { + final address = delivery.deliveryAddress!; + final Uri mapUri = Uri( + scheme: 'https', + host: 'maps.google.com', + queryParameters: { + 'q': '${address.latitude},${address.longitude}', + }, + ); + if (await canLaunchUrl(mapUri)) { + await launchUrl(mapUri); + } + } + break; + } + } +} + +class DeliveryListView extends StatelessWidget { + final List deliveries; + final Function(Delivery, String) onAction; + + const DeliveryListView({ + super.key, + required this.deliveries, + required this.onAction, + }); + + @override + Widget build(BuildContext context) { + if (deliveries.isEmpty) { + return const Center( + child: Text('No deliveries'), + ); + } + + return RefreshIndicator( + onRefresh: () async { + // Trigger refresh via provider + }, + child: ListView.builder( + itemCount: deliveries.length, + itemBuilder: (context, index) { + final delivery = deliveries[index]; + return DeliveryCard( + delivery: delivery, + onAction: onAction, + ); + }, + ), + ); + } +} + +class DeliveryCard extends StatelessWidget { + final Delivery delivery; + final Function(Delivery, String) onAction; + + const DeliveryCard({ + super.key, + required this.delivery, + required this.onAction, + }); + + @override + Widget build(BuildContext context) { + final contact = delivery.orders.isNotEmpty && delivery.orders.first.contact != null + ? delivery.orders.first.contact + : null; + final order = delivery.orders.isNotEmpty ? delivery.orders.first : null; + + return Card( + margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + delivery.name, + style: Theme.of(context).textTheme.titleMedium, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + if (contact != null) + Text( + contact.fullName, + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + ), + if (delivery.delivered) + Chip( + label: const Text('Delivered'), + backgroundColor: Theme.of(context).colorScheme.primaryContainer, + ) + else if (order?.isNewCustomer ?? false) + Chip( + label: const Text('New Customer'), + backgroundColor: Colors.orange.shade100, + ), + ], + ), + const SizedBox(height: 12), + if (delivery.deliveryAddress != null) + Text( + delivery.deliveryAddress!.formattedAddress, + style: Theme.of(context).textTheme.bodySmall, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + if (order != null) ...[ + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (order.totalItems != null) + Text( + '${order.totalItems} items', + style: Theme.of(context).textTheme.bodySmall, + ), + Text( + '${order.totalAmount} MAD', + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: Theme.of(context).colorScheme.primary, + ), + ), + ], + ), + ], + const SizedBox(height: 12), + Wrap( + spacing: 8, + children: [ + if (contact?.phoneNumber != null) + OutlinedButton.icon( + onPressed: () => onAction(delivery, 'call'), + icon: const Icon(Icons.phone), + label: const Text('Call'), + ), + if (delivery.deliveryAddress != null) + OutlinedButton.icon( + onPressed: () => onAction(delivery, 'map'), + icon: const Icon(Icons.map), + label: const Text('Map'), + ), + OutlinedButton.icon( + onPressed: () => _showDeliveryActions(context), + icon: const Icon(Icons.more_vert), + label: const Text('More'), + ), + ], + ), + ], + ), + ), + ); + } + + void _showDeliveryActions(BuildContext context) { + showModalBottomSheet( + context: context, + builder: (context) => SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (!delivery.delivered) + ListTile( + leading: const Icon(Icons.check_circle), + title: const Text('Mark as Completed'), + onTap: () { + Navigator.pop(context); + onAction(delivery, 'complete'); + }, + ) + else + ListTile( + leading: const Icon(Icons.undo), + title: const Text('Mark as Uncompleted'), + onTap: () { + Navigator.pop(context); + onAction(delivery, 'uncomplete'); + }, + ), + ListTile( + leading: const Icon(Icons.camera_alt), + title: const Text('Upload Photo'), + onTap: () { + Navigator.pop(context); + // TODO: Implement photo upload + }, + ), + ListTile( + leading: const Icon(Icons.description), + title: const Text('View Details'), + onTap: () { + Navigator.pop(context); + // TODO: Navigate to delivery details + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/login_page.dart b/lib/pages/login_page.dart new file mode 100644 index 0000000..f58fb26 --- /dev/null +++ b/lib/pages/login_page.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../providers/providers.dart'; + +class LoginPage extends ConsumerWidget { + const LoginPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Plan B Logistics', + style: Theme.of(context).textTheme.displayMedium?.copyWith( + color: Theme.of(context).colorScheme.primary, + ), + ), + const SizedBox(height: 16), + Text( + 'Delivery Management System', + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 40), + ElevatedButton( + onPressed: () async { + final authService = ref.read(authServiceProvider); + final result = await authService.login(); + result.when( + success: (token) { + if (context.mounted) { + // ignore: unused_result + ref.refresh(isAuthenticatedProvider); + } + }, + onError: (error) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Login failed: $error')), + ); + } + }, + cancelled: () {}, + ); + }, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16), + ), + child: const Text('Login with Keycloak'), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/routes_page.dart b/lib/pages/routes_page.dart new file mode 100644 index 0000000..3827b29 --- /dev/null +++ b/lib/pages/routes_page.dart @@ -0,0 +1,183 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../models/delivery_route.dart'; +import '../providers/providers.dart'; +import '../utils/breakpoints.dart'; +import '../utils/responsive.dart'; +import 'deliveries_page.dart'; +import 'settings_page.dart'; + +class RoutesPage extends ConsumerWidget { + const RoutesPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final routesData = ref.watch(deliveryRoutesProvider); + final userProfile = ref.watch(userProfileProvider); + + return Scaffold( + appBar: AppBar( + title: const Text('Delivery Routes'), + elevation: 0, + actions: [ + userProfile.when( + data: (profile) => PopupMenuButton( + onSelected: (value) { + if (value == 'settings') { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const SettingsPage(), + ), + ); + } + }, + itemBuilder: (BuildContext context) => [ + PopupMenuItem( + value: 'profile', + child: Text(profile?.fullName ?? 'User'), + enabled: false, + ), + const PopupMenuDivider(), + const PopupMenuItem( + value: 'settings', + child: Text('Settings'), + ), + ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Center( + child: Text( + profile?.fullName ?? 'User', + style: Theme.of(context).textTheme.titleSmall, + ), + ), + ), + ), + loading: () => const Padding( + padding: EdgeInsets.all(16.0), + child: SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ), + error: (error, stackTrace) => const SizedBox(), + ), + ], + ), + body: routesData.when( + data: (routes) { + if (routes.isEmpty) { + return const Center( + child: Text('No routes available'), + ); + } + return RefreshIndicator( + onRefresh: () async { + // ignore: unused_result + ref.refresh(deliveryRoutesProvider); + }, + child: context.isDesktop + ? _buildDesktopGrid(context, routes) + : _buildMobileList(context, routes), + ); + }, + loading: () => const Center( + child: CircularProgressIndicator(), + ), + error: (error, stackTrace) => Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Error: $error'), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => ref.refresh(deliveryRoutesProvider), + child: const Text('Retry'), + ), + ], + ), + ), + ), + ); + } + + Widget _buildMobileList(BuildContext context, List routes) { + final spacing = ResponsiveSpacing.md(context); + return ListView.builder( + padding: EdgeInsets.all(ResponsiveSpacing.md(context)), + itemCount: routes.length, + itemBuilder: (context, index) { + final route = routes[index]; + return Padding( + padding: EdgeInsets.only(bottom: spacing), + child: _buildRouteCard(context, route), + ); + }, + ); + } + + Widget _buildDesktopGrid(BuildContext context, List routes) { + final spacing = ResponsiveSpacing.lg(context); + final columns = context.isTablet ? 2 : 3; + return GridView.builder( + padding: EdgeInsets.all(spacing), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: columns, + crossAxisSpacing: spacing, + mainAxisSpacing: spacing, + childAspectRatio: 1.2, + ), + itemCount: routes.length, + itemBuilder: (context, index) { + final route = routes[index]; + return _buildRouteCard(context, route); + }, + ); + } + + Widget _buildRouteCard(BuildContext context, DeliveryRoute route) { + return Card( + child: InkWell( + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => DeliveriesPage( + routeFragmentId: route.routeFragmentId, + routeName: route.name, + ), + ), + ); + }, + child: Padding( + padding: EdgeInsets.all(ResponsiveSpacing.md(context)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + route.name, + style: Theme.of(context).textTheme.titleLarge, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + SizedBox(height: ResponsiveSpacing.sm(context)), + Text( + '${route.completedDeliveries}/${route.totalDeliveries} completed', + style: Theme.of(context).textTheme.bodySmall, + ), + SizedBox(height: ResponsiveSpacing.md(context)), + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: LinearProgressIndicator( + value: route.progress, + minHeight: 8, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart new file mode 100644 index 0000000..9dfcbcf --- /dev/null +++ b/lib/pages/settings_page.dart @@ -0,0 +1,177 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../providers/providers.dart'; + +class SettingsPage extends ConsumerWidget { + const SettingsPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final userProfile = ref.watch(userProfileProvider); + final language = ref.watch(languageProvider); + + return Scaffold( + appBar: AppBar( + title: const Text('Settings'), + ), + body: ListView( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Profile', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 16), + userProfile.when( + data: (profile) { + if (profile == null) { + return const Text('No profile information'); + } + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + CircleAvatar( + radius: 32, + child: Text( + profile.firstName[0].toUpperCase(), + style: Theme.of(context).textTheme.titleLarge, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + profile.fullName, + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 4), + Text( + profile.email, + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + ), + ], + ), + ], + ), + ), + ); + }, + loading: () => const CircularProgressIndicator(), + error: (error, stackTrace) => Text('Error: $error'), + ), + ], + ), + ), + const Divider(), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Preferences', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 16), + ListTile( + title: const Text('Language'), + subtitle: Text(language == 'fr' ? 'Franais' : 'English'), + trailing: DropdownButton( + value: language, + onChanged: (String? newValue) { + if (newValue != null) { + ref.read(languageProvider.notifier).state = newValue; + } + }, + items: const [ + DropdownMenuItem( + value: 'en', + child: Text('English'), + ), + DropdownMenuItem( + value: 'fr', + child: Text('Franais'), + ), + ], + ), + ), + ], + ), + ), + const Divider(), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Account', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + icon: const Icon(Icons.logout), + label: const Text('Logout'), + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.error, + foregroundColor: Theme.of(context).colorScheme.onError, + ), + onPressed: () async { + final authService = ref.read(authServiceProvider); + await authService.logout(); + if (context.mounted) { + // ignore: unused_result + ref.refresh(isAuthenticatedProvider); + if (context.mounted) { + Navigator.of(context).pushReplacementNamed('/'); + } + } + }, + ), + ), + ], + ), + ), + const Divider(), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'About', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 16), + ListTile( + title: const Text('App Version'), + subtitle: const Text('1.0.0'), + ), + ListTile( + title: const Text('Built with Flutter'), + subtitle: const Text('Plan B Logistics Management System'), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/providers/providers.dart b/lib/providers/providers.dart new file mode 100644 index 0000000..b7b039a --- /dev/null +++ b/lib/providers/providers.dart @@ -0,0 +1,204 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../api/types.dart'; +import '../api/client.dart'; +import '../api/openapi_config.dart'; +import '../services/auth_service.dart'; +import '../models/user_profile.dart'; +import '../models/delivery_route.dart'; +import '../models/delivery.dart'; +import '../models/delivery_order.dart'; +import '../models/delivery_address.dart'; +import '../models/delivery_contact.dart'; + +final authServiceProvider = Provider((ref) { + return AuthService(); +}); + +final apiClientProvider = Provider((ref) { + return CqrsApiClient(config: ApiClientConfig.production); +}); + +final isAuthenticatedProvider = FutureProvider((ref) async { + final authService = ref.watch(authServiceProvider); + return await authService.isAuthenticated(); +}); + +final userProfileProvider = FutureProvider((ref) async { + final authService = ref.watch(authServiceProvider); + final token = await authService.getToken(); + if (token == null) return null; + return authService.decodeToken(token); +}); + +final authTokenProvider = FutureProvider((ref) async { + final authService = ref.watch(authServiceProvider); + return await authService.getToken(); +}); + +final deliveryRoutesProvider = FutureProvider>((ref) async { + // ignore: unused_local_variable + final client = ref.watch(apiClientProvider); + final token = ref.watch(authTokenProvider).valueOrNull; + + // TODO: Remove mock data when Keycloak is configured + if (token == null) { + return [ + DeliveryRoute( + id: 1, + name: 'Route A - Downtown', + routeFragmentId: 1, + totalDeliveries: 12, + completedDeliveries: 5, + skippedDeliveries: 0, + createdAt: DateTime.now().subtract(const Duration(days: 1)).toIso8601String(), + ), + DeliveryRoute( + id: 2, + name: 'Route B - Suburbs', + routeFragmentId: 2, + totalDeliveries: 8, + completedDeliveries: 8, + skippedDeliveries: 0, + createdAt: DateTime.now().subtract(const Duration(days: 2)).toIso8601String(), + ), + DeliveryRoute( + id: 3, + name: 'Route C - Industrial Zone', + routeFragmentId: 3, + totalDeliveries: 15, + completedDeliveries: 3, + skippedDeliveries: 2, + createdAt: DateTime.now().subtract(const Duration(days: 3)).toIso8601String(), + ), + ]; + } + + // Create a new client with auth token + final authClient = CqrsApiClient( + config: ApiClientConfig( + baseUrl: ApiClientConfig.production.baseUrl, + defaultHeaders: {'Authorization': 'Bearer $token'}, + ), + ); + + final result = await authClient.executeQuery>( + endpoint: 'simpleDeliveryRouteQueryItems', + query: _EmptyQuery(), + fromJson: (json) { + final routes = json['items'] as List?; + return routes?.map((r) => DeliveryRoute.fromJson(r as Map)).toList() ?? []; + }, + ); + + return result.whenSuccess((routes) => routes) ?? []; +}); + +final deliveriesProvider = FutureProvider.family, int>((ref, routeFragmentId) async { + // ignore: unused_local_variable + final client = ref.watch(apiClientProvider); + final token = ref.watch(authTokenProvider).valueOrNull; + + // TODO: Remove mock data when Keycloak is configured + if (token == null) { + return _getMockDeliveries(routeFragmentId); + } + + final authClient = CqrsApiClient( + config: ApiClientConfig( + baseUrl: ApiClientConfig.production.baseUrl, + defaultHeaders: {'Authorization': 'Bearer $token'}, + ), + ); + + final result = await authClient.executeQuery>( + endpoint: 'simpleDeliveriesQueryItems', + query: _DeliveriesQuery(routeFragmentId: routeFragmentId), + fromJson: (json) { + final items = json['items'] as List?; + return items?.map((d) => Delivery.fromJson(d as Map)).toList() ?? []; + }, + ); + + return result.whenSuccess((deliveries) => deliveries) ?? []; +}); + +final languageProvider = StateProvider((ref) { + return 'fr'; +}); + +// Mock data generator for testing without authentication +List _getMockDeliveries(int routeFragmentId) { + final mockDeliveries = []; + + for (int i = 1; i <= 6; i++) { + final isDelivered = i <= 2; + mockDeliveries.add( + Delivery( + id: i, + routeFragmentId: routeFragmentId, + deliveryIndex: i, + orders: [ + DeliveryOrder( + id: i * 100, + isNewCustomer: i == 3, + totalAmount: 150.0 + (i * 10), + totalItems: 3 + i, + contacts: [ + DeliveryContact( + firstName: 'Client', + lastName: 'Name$i', + fullName: 'Client Name $i', + phoneNumber: '+212${i}23456789', + ), + ], + contact: DeliveryContact( + firstName: 'Client', + lastName: 'Name$i', + fullName: 'Client Name $i', + phoneNumber: '+212${i}23456789', + ), + ), + ], + deliveryAddress: DeliveryAddress( + id: i, + line1: 'Street $i', + line2: 'Building ${i * 10}', + postalCode: '3000${i.toString().padLeft(2, '0')}', + city: 'Casablanca', + subdivision: 'Casablanca-Settat', + countryCode: 'MA', + latitude: 33.5731 + (i * 0.01), + longitude: -7.5898 + (i * 0.01), + formattedAddress: 'Street $i, Building ${i * 10}, Casablanca, Morocco', + ), + delivered: isDelivered, + isSkipped: false, + hasBeenSkipped: false, + deliveredAt: isDelivered ? DateTime.now().subtract(Duration(hours: i)).toIso8601String() : null, + name: 'Delivery #${routeFragmentId}-$i', + createdAt: DateTime.now().subtract(Duration(days: 1)).toIso8601String(), + updatedAt: DateTime.now().toIso8601String(), + ), + ); + } + + return mockDeliveries; +} + +class _EmptyQuery implements Serializable { + @override + Map toJson() => {}; +} + +class _DeliveriesQuery implements Serializable { + final int routeFragmentId; + + _DeliveriesQuery({required this.routeFragmentId}); + + @override + Map toJson() => { + 'params': { + 'routeFragmentId': routeFragmentId, + }, + }; +} diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart new file mode 100644 index 0000000..9fe3f23 --- /dev/null +++ b/lib/services/auth_service.dart @@ -0,0 +1,118 @@ +import 'package:flutter_appauth/flutter_appauth.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:jwt_decoder/jwt_decoder.dart'; +import '../models/user_profile.dart'; + +class AuthService { + static const String _tokenKey = 'auth_token'; + static const String _refreshTokenKey = 'refresh_token'; + + final FlutterAppAuth _appAuth; + final FlutterSecureStorage _secureStorage; + + AuthService({ + FlutterAppAuth? appAuth, + FlutterSecureStorage? secureStorage, + }) : _appAuth = appAuth ?? const FlutterAppAuth(), + _secureStorage = secureStorage ?? const FlutterSecureStorage(); + + Future login() async { + try { + final result = await _appAuth.authorizeAndExchangeCode( + AuthorizationTokenRequest( + 'delivery-mobile-app', + 'com.goutezplanb.delivery://callback', + discoveryUrl: 'https://auth.goutezplanb.com/realms/planb-internal/.well-known/openid-configuration', + scopes: const ['openid', 'profile', 'offline_access'], + promptValues: const ['login'], + ), + ); + + // ignore: unnecessary_null_comparison + if (result == null) { + return const AuthResult.cancelled(); + } + + await _secureStorage.write(key: _tokenKey, value: result.accessToken ?? ''); + if (result.refreshToken != null) { + await _secureStorage.write(key: _refreshTokenKey, value: result.refreshToken!); + } + + return AuthResult.success(token: result.accessToken ?? ''); + } catch (e) { + return AuthResult.error(error: e.toString()); + } + } + + Future logout() async { + await Future.wait([ + _secureStorage.delete(key: _tokenKey), + _secureStorage.delete(key: _refreshTokenKey), + ]); + } + + Future getToken() async { + return await _secureStorage.read(key: _tokenKey); + } + + Future getRefreshToken() async { + return await _secureStorage.read(key: _refreshTokenKey); + } + + bool isTokenValid(String? token) { + if (token == null || token.isEmpty) return false; + try { + return !JwtDecoder.isExpired(token); + } catch (e) { + return false; + } + } + + UserProfile? decodeToken(String token) { + try { + final decodedToken = JwtDecoder.decode(token); + return UserProfile.fromJwtClaims(decodedToken); + } catch (e) { + return null; + } + } + + Future isAuthenticated() async { + final token = await getToken(); + return isTokenValid(token); + } +} + +sealed class AuthResult { + const AuthResult(); + + factory AuthResult.success({required String token}) => _Success(token); + factory AuthResult.error({required String error}) => _Error(error); + const factory AuthResult.cancelled() = _Cancelled; + + R when({ + required R Function(String token) success, + required R Function(String error) onError, + required R Function() cancelled, + }) { + return switch (this) { + _Success(:final token) => success(token), + _Error(:final error) => onError(error), + _Cancelled() => cancelled(), + }; + } +} + +final class _Success extends AuthResult { + final String token; + const _Success(this.token); +} + +final class _Error extends AuthResult { + final String error; + const _Error(this.error); +} + +final class _Cancelled extends AuthResult { + const _Cancelled(); +} diff --git a/lib/theme.dart b/lib/theme.dart new file mode 100644 index 0000000..2e711ec --- /dev/null +++ b/lib/theme.dart @@ -0,0 +1,408 @@ +import "package:flutter/material.dart"; + +class MaterialTheme { + final TextTheme textTheme; + + const MaterialTheme(this.textTheme); + + // Svrnty Brand Colors - Light Theme + static ColorScheme lightScheme() { + return const ColorScheme( + brightness: Brightness.light, + primary: Color(0xffC44D58), // Svrnty Crimson Red + surfaceTint: Color(0xffC44D58), + onPrimary: Color(0xffffffff), + primaryContainer: Color(0xffffd8db), + onPrimaryContainer: Color(0xff8b3238), + secondary: Color(0xff475C6C), // Svrnty Slate Blue + onSecondary: Color(0xffffffff), + secondaryContainer: Color(0xffd1dce7), + onSecondaryContainer: Color(0xff2e3d4a), + tertiary: Color(0xff5a4a6c), + onTertiary: Color(0xffffffff), + tertiaryContainer: Color(0xffe0d3f2), + onTertiaryContainer: Color(0xff3d2f4d), + error: Color(0xffba1a1a), + onError: Color(0xffffffff), + errorContainer: Color(0xffffdad6), + onErrorContainer: Color(0xff93000a), + surface: Color(0xfffafafa), + onSurface: Color(0xff1a1c1e), + onSurfaceVariant: Color(0xff43474e), + outline: Color(0xff74777f), + outlineVariant: Color(0xffc4c6cf), + shadow: Color(0xff000000), + scrim: Color(0xff000000), + inverseSurface: Color(0xff2f3033), + inversePrimary: Color(0xffffb3b9), + primaryFixed: Color(0xffffd8db), + onPrimaryFixed: Color(0xff410008), + primaryFixedDim: Color(0xffffb3b9), + onPrimaryFixedVariant: Color(0xff8b3238), + secondaryFixed: Color(0xffd1dce7), + onSecondaryFixed: Color(0xff0f1a24), + secondaryFixedDim: Color(0xffb5c0cb), + onSecondaryFixedVariant: Color(0xff2e3d4a), + tertiaryFixed: Color(0xffe0d3f2), + onTertiaryFixed: Color(0xff1f122f), + tertiaryFixedDim: Color(0xffc4b7d6), + onTertiaryFixedVariant: Color(0xff3d2f4d), + surfaceDim: Color(0xffdadcde), + surfaceBright: Color(0xfffafafa), + surfaceContainerLowest: Color(0xffffffff), + surfaceContainerLow: Color(0xfff4f5f7), + surfaceContainer: Color(0xffeef0f2), + surfaceContainerHigh: Color(0xffe8eaec), + surfaceContainerHighest: Color(0xffe2e4e7), + ); + } + + ThemeData light() { + return theme(lightScheme()); + } + + static ColorScheme lightMediumContrastScheme() { + return const ColorScheme( + brightness: Brightness.light, + primary: Color(0xff0d3665), + surfaceTint: Color(0xff3d5f90), + onPrimary: Color(0xffffffff), + primaryContainer: Color(0xff4d6ea0), + onPrimaryContainer: Color(0xffffffff), + secondary: Color(0xff2d3747), + onSecondary: Color(0xffffffff), + secondaryContainer: Color(0xff636d80), + onSecondaryContainer: Color(0xffffffff), + tertiary: Color(0xff442e4c), + onTertiary: Color(0xffffffff), + tertiaryContainer: Color(0xff7d6485), + onTertiaryContainer: Color(0xffffffff), + error: Color(0xff740006), + onError: Color(0xffffffff), + errorContainer: Color(0xffcf2c27), + onErrorContainer: Color(0xffffffff), + surface: Color(0xfff9f9ff), + onSurface: Color(0xff0f1116), + onSurfaceVariant: Color(0xff33363d), + outline: Color(0xff4f525a), + outlineVariant: Color(0xff6a6d75), + shadow: Color(0xff000000), + scrim: Color(0xff000000), + inverseSurface: Color(0xff2e3035), + inversePrimary: Color(0xffa6c8ff), + primaryFixed: Color(0xff4d6ea0), + onPrimaryFixed: Color(0xffffffff), + primaryFixedDim: Color(0xff335686), + onPrimaryFixedVariant: Color(0xffffffff), + secondaryFixed: Color(0xff636d80), + onSecondaryFixed: Color(0xffffffff), + secondaryFixedDim: Color(0xff4b5567), + onSecondaryFixedVariant: Color(0xffffffff), + tertiaryFixed: Color(0xff7d6485), + onTertiaryFixed: Color(0xffffffff), + tertiaryFixedDim: Color(0xff644c6c), + onTertiaryFixedVariant: Color(0xffffffff), + surfaceDim: Color(0xffc5c6cd), + surfaceBright: Color(0xfff9f9ff), + surfaceContainerLowest: Color(0xffffffff), + surfaceContainerLow: Color(0xfff3f3fa), + surfaceContainer: Color(0xffe7e8ee), + surfaceContainerHigh: Color(0xffdcdce3), + surfaceContainerHighest: Color(0xffd0d1d8), + ); + } + + ThemeData lightMediumContrast() { + return theme(lightMediumContrastScheme()); + } + + static ColorScheme lightHighContrastScheme() { + return const ColorScheme( + brightness: Brightness.light, + primary: Color(0xff002c58), + surfaceTint: Color(0xff3d5f90), + onPrimary: Color(0xffffffff), + primaryContainer: Color(0xff264a79), + onPrimaryContainer: Color(0xffffffff), + secondary: Color(0xff232d3d), + onSecondary: Color(0xffffffff), + secondaryContainer: Color(0xff404a5b), + onSecondaryContainer: Color(0xffffffff), + tertiary: Color(0xff392441), + onTertiary: Color(0xffffffff), + tertiaryContainer: Color(0xff584160), + onTertiaryContainer: Color(0xffffffff), + error: Color(0xff600004), + onError: Color(0xffffffff), + errorContainer: Color(0xff98000a), + onErrorContainer: Color(0xffffffff), + surface: Color(0xfff9f9ff), + onSurface: Color(0xff000000), + onSurfaceVariant: Color(0xff000000), + outline: Color(0xff292c33), + outlineVariant: Color(0xff464951), + shadow: Color(0xff000000), + scrim: Color(0xff000000), + inverseSurface: Color(0xff2e3035), + inversePrimary: Color(0xffa6c8ff), + primaryFixed: Color(0xff264a79), + onPrimaryFixed: Color(0xffffffff), + primaryFixedDim: Color(0xff063361), + onPrimaryFixedVariant: Color(0xffffffff), + secondaryFixed: Color(0xff404a5b), + onSecondaryFixed: Color(0xffffffff), + secondaryFixedDim: Color(0xff293343), + onSecondaryFixedVariant: Color(0xffffffff), + tertiaryFixed: Color(0xff584160), + onTertiaryFixed: Color(0xffffffff), + tertiaryFixedDim: Color(0xff402b48), + onTertiaryFixedVariant: Color(0xffffffff), + surfaceDim: Color(0xffb7b8bf), + surfaceBright: Color(0xfff9f9ff), + surfaceContainerLowest: Color(0xffffffff), + surfaceContainerLow: Color(0xfff0f0f7), + surfaceContainer: Color(0xffe1e2e9), + surfaceContainerHigh: Color(0xffd3d4da), + surfaceContainerHighest: Color(0xffc5c6cd), + ); + } + + ThemeData lightHighContrast() { + return theme(lightHighContrastScheme()); + } + + // Svrnty Brand Colors - Dark Theme (Bold & Saturated) + static ColorScheme darkScheme() { + return const ColorScheme( + brightness: Brightness.dark, + primary: Color(0xffF3574E), // Bold Svrnty Crimson Red (slightly desaturated) + surfaceTint: Color(0xffF3574E), + onPrimary: Color(0xffffffff), + primaryContainer: Color(0xffC44D58), // True brand crimson + onPrimaryContainer: Color(0xffffffff), + secondary: Color(0xff5A6F7D), // Rich Svrnty Slate Blue + onSecondary: Color(0xffffffff), + secondaryContainer: Color(0xff475C6C), // True brand slate + onSecondaryContainer: Color(0xffffffff), + tertiary: Color(0xffA78BBF), // Richer purple + onTertiary: Color(0xffffffff), + tertiaryContainer: Color(0xff8B6FA3), + onTertiaryContainer: Color(0xffffffff), + error: Color(0xffFF5449), + onError: Color(0xffffffff), + errorContainer: Color(0xffD32F2F), + onErrorContainer: Color(0xffffffff), + surface: Color(0xff1a1c1e), // Svrnty Dark Background + onSurface: Color(0xfff0f0f0), + onSurfaceVariant: Color(0xffc8cad0), + outline: Color(0xff8d9199), + outlineVariant: Color(0xff43474e), + shadow: Color(0xff000000), + scrim: Color(0xff000000), + inverseSurface: Color(0xffe2e4e7), + inversePrimary: Color(0xffC44D58), + primaryFixed: Color(0xffFFD8DB), + onPrimaryFixed: Color(0xff2d0008), + primaryFixedDim: Color(0xffF3574E), + onPrimaryFixedVariant: Color(0xffffffff), + secondaryFixed: Color(0xffD1DCE7), + onSecondaryFixed: Color(0xff0f1a24), + secondaryFixedDim: Color(0xff5A6F7D), + onSecondaryFixedVariant: Color(0xffffffff), + tertiaryFixed: Color(0xffE0D3F2), + onTertiaryFixed: Color(0xff1f122f), + tertiaryFixedDim: Color(0xffA78BBF), + onTertiaryFixedVariant: Color(0xffffffff), + surfaceDim: Color(0xff1a1c1e), + surfaceBright: Color(0xff404244), + surfaceContainerLowest: Color(0xff0f1113), + surfaceContainerLow: Color(0xff1f2123), + surfaceContainer: Color(0xff23252a), + surfaceContainerHigh: Color(0xff2d2f35), + surfaceContainerHighest: Color(0xff383940), + ); + } + + ThemeData dark() { + return theme(darkScheme()); + } + + static ColorScheme darkMediumContrastScheme() { + return const ColorScheme( + brightness: Brightness.dark, + primary: Color(0xffcbddff), + surfaceTint: Color(0xffa6c8ff), + onPrimary: Color(0xff00264d), + primaryContainer: Color(0xff7192c6), + onPrimaryContainer: Color(0xff000000), + secondary: Color(0xffd3ddf2), + onSecondary: Color(0xff1c2636), + secondaryContainer: Color(0xff8791a5), + onSecondaryContainer: Color(0xff000000), + tertiary: Color(0xfff1d2f8), + onTertiary: Color(0xff321e3a), + tertiaryContainer: Color(0xffa387aa), + onTertiaryContainer: Color(0xff000000), + error: Color(0xffffd2cc), + onError: Color(0xff540003), + errorContainer: Color(0xffff5449), + onErrorContainer: Color(0xff000000), + surface: Color(0xff111318), + onSurface: Color(0xffffffff), + onSurfaceVariant: Color(0xffdadce5), + outline: Color(0xffafb2bb), + outlineVariant: Color(0xff8d9099), + shadow: Color(0xff000000), + scrim: Color(0xff000000), + inverseSurface: Color(0xffe1e2e9), + inversePrimary: Color(0xff254978), + primaryFixed: Color(0xffd5e3ff), + onPrimaryFixed: Color(0xff001129), + primaryFixedDim: Color(0xffa6c8ff), + onPrimaryFixedVariant: Color(0xff0d3665), + secondaryFixed: Color(0xffd9e3f8), + onSecondaryFixed: Color(0xff071120), + secondaryFixedDim: Color(0xffbdc7dc), + onSecondaryFixedVariant: Color(0xff2d3747), + tertiaryFixed: Color(0xfff8d8ff), + onTertiaryFixed: Color(0xff1c0924), + tertiaryFixedDim: Color(0xffdbbde2), + onTertiaryFixedVariant: Color(0xff442e4c), + surfaceDim: Color(0xff111318), + surfaceBright: Color(0xff42444a), + surfaceContainerLowest: Color(0xff05070c), + surfaceContainerLow: Color(0xff1b1e22), + surfaceContainer: Color(0xff26282d), + surfaceContainerHigh: Color(0xff303338), + surfaceContainerHighest: Color(0xff3b3e43), + ); + } + + ThemeData darkMediumContrast() { + return theme(darkMediumContrastScheme()); + } + + static ColorScheme darkHighContrastScheme() { + return const ColorScheme( + brightness: Brightness.dark, + primary: Color(0xffeaf0ff), + surfaceTint: Color(0xffa6c8ff), + onPrimary: Color(0xff000000), + primaryContainer: Color(0xffa3c4fb), + onPrimaryContainer: Color(0xff000b1e), + secondary: Color(0xffeaf0ff), + onSecondary: Color(0xff000000), + secondaryContainer: Color(0xffb9c3d8), + onSecondaryContainer: Color(0xff030b1a), + tertiary: Color(0xfffeeaff), + onTertiary: Color(0xff000000), + tertiaryContainer: Color(0xffd7b9de), + onTertiaryContainer: Color(0xff16041e), + error: Color(0xffffece9), + onError: Color(0xff000000), + errorContainer: Color(0xffffaea4), + onErrorContainer: Color(0xff220001), + surface: Color(0xff111318), + onSurface: Color(0xffffffff), + onSurfaceVariant: Color(0xffffffff), + outline: Color(0xffedf0f9), + outlineVariant: Color(0xffc0c2cb), + shadow: Color(0xff000000), + scrim: Color(0xff000000), + inverseSurface: Color(0xffe1e2e9), + inversePrimary: Color(0xff254978), + primaryFixed: Color(0xffd5e3ff), + onPrimaryFixed: Color(0xff000000), + primaryFixedDim: Color(0xffa6c8ff), + onPrimaryFixedVariant: Color(0xff001129), + secondaryFixed: Color(0xffd9e3f8), + onSecondaryFixed: Color(0xff000000), + secondaryFixedDim: Color(0xffbdc7dc), + onSecondaryFixedVariant: Color(0xff071120), + tertiaryFixed: Color(0xfff8d8ff), + onTertiaryFixed: Color(0xff000000), + tertiaryFixedDim: Color(0xffdbbde2), + onTertiaryFixedVariant: Color(0xff1c0924), + surfaceDim: Color(0xff111318), + surfaceBright: Color(0xff4e5055), + surfaceContainerLowest: Color(0xff000000), + surfaceContainerLow: Color(0xff1d2024), + surfaceContainer: Color(0xff2e3035), + surfaceContainerHigh: Color(0xff393b41), + surfaceContainerHighest: Color(0xff45474c), + ); + } + + ThemeData darkHighContrast() { + return theme(darkHighContrastScheme()); + } + + + ThemeData theme(ColorScheme colorScheme) => ThemeData( + useMaterial3: true, + brightness: colorScheme.brightness, + colorScheme: colorScheme, + textTheme: const TextTheme( + displayLarge: TextStyle(fontFamily: 'Montserrat', fontWeight: FontWeight.bold), + displayMedium: TextStyle(fontFamily: 'Montserrat', fontWeight: FontWeight.bold), + displaySmall: TextStyle(fontFamily: 'Montserrat', fontWeight: FontWeight.bold), + headlineLarge: TextStyle(fontFamily: 'Montserrat', fontWeight: FontWeight.w600), + headlineMedium: TextStyle(fontFamily: 'Montserrat', fontWeight: FontWeight.w600), + headlineSmall: TextStyle(fontFamily: 'Montserrat', fontWeight: FontWeight.w600), + titleLarge: TextStyle(fontFamily: 'Montserrat', fontWeight: FontWeight.w600), + titleMedium: TextStyle(fontFamily: 'Montserrat', fontWeight: FontWeight.w500), + titleSmall: TextStyle(fontFamily: 'Montserrat', fontWeight: FontWeight.w500), + bodyLarge: TextStyle(fontFamily: 'Montserrat'), + bodyMedium: TextStyle(fontFamily: 'Montserrat'), + bodySmall: TextStyle(fontFamily: 'Montserrat'), + labelLarge: TextStyle(fontFamily: 'Montserrat', fontWeight: FontWeight.w500), + labelMedium: TextStyle(fontFamily: 'Montserrat', fontWeight: FontWeight.w500), + labelSmall: TextStyle(fontFamily: 'Montserrat', fontWeight: FontWeight.w500), + ).apply( + bodyColor: colorScheme.onSurface, + displayColor: colorScheme.onSurface, + ), + fontFamily: 'Montserrat', + scaffoldBackgroundColor: colorScheme.surface, + canvasColor: colorScheme.surface, + ); + + + List get extendedColors => [ + ]; +} + +class ExtendedColor { + final Color seed, value; + final ColorFamily light; + final ColorFamily lightHighContrast; + final ColorFamily lightMediumContrast; + final ColorFamily dark; + final ColorFamily darkHighContrast; + final ColorFamily darkMediumContrast; + + const ExtendedColor({ + required this.seed, + required this.value, + required this.light, + required this.lightHighContrast, + required this.lightMediumContrast, + required this.dark, + required this.darkHighContrast, + required this.darkMediumContrast, + }); +} + +class ColorFamily { + const ColorFamily({ + required this.color, + required this.onColor, + required this.colorContainer, + required this.onColorContainer, + }); + + final Color color; + final Color onColor; + final Color colorContainer; + final Color onColorContainer; +} diff --git a/lib/utils/breakpoints.dart b/lib/utils/breakpoints.dart new file mode 100644 index 0000000..b04244a --- /dev/null +++ b/lib/utils/breakpoints.dart @@ -0,0 +1,168 @@ +library; + +import 'package:flutter/material.dart'; + +enum DeviceType { + mobile, + tablet, + desktop, +} + +class Breakpoints { + Breakpoints._(); + + static const double mobile = 600; + static const double tablet = 1024; + static const double desktop = 1024; + + static const double mobileSmall = 360; + static const double mobileLarge = 480; + static const double tabletSmall = 600; + static const double tabletLarge = 840; + static const double desktopSmall = 1024; + static const double desktopMedium = 1440; + static const double desktopLarge = 1920; + static const double desktopUltra = 2560; + + static DeviceType getDeviceType(BuildContext context) { + final double width = MediaQuery.of(context).size.width; + return getDeviceTypeFromWidth(width); + } + + static DeviceType getDeviceTypeFromWidth(double width) { + if (width < mobile) { + return DeviceType.mobile; + } else if (width < desktop) { + return DeviceType.tablet; + } else { + return DeviceType.desktop; + } + } + + static bool isMobile(BuildContext context) { + return getDeviceType(context) == DeviceType.mobile; + } + + static bool isTablet(BuildContext context) { + return getDeviceType(context) == DeviceType.tablet; + } + + static bool isDesktop(BuildContext context) { + return getDeviceType(context) == DeviceType.desktop; + } + + static bool isTabletOrLarger(BuildContext context) { + final DeviceType type = getDeviceType(context); + return type == DeviceType.tablet || type == DeviceType.desktop; + } + + static bool isMobileOrTablet(BuildContext context) { + final DeviceType type = getDeviceType(context); + return type == DeviceType.mobile || type == DeviceType.tablet; + } + + static T adaptive({ + required BuildContext context, + required T mobile, + T? tablet, + T? desktop, + }) { + final DeviceType deviceType = getDeviceType(context); + + switch (deviceType) { + case DeviceType.mobile: + return mobile; + case DeviceType.tablet: + return tablet ?? mobile; + case DeviceType.desktop: + return desktop ?? tablet ?? mobile; + } + } + + static int getGridColumns(BuildContext context) { + final double width = MediaQuery.of(context).size.width; + + if (width < mobileSmall) { + return 1; + } else if (width < mobileLarge) { + return 2; + } else if (width < tabletSmall) { + return 3; + } else if (width < tabletLarge) { + return 4; + } else if (width < desktopSmall) { + return 6; + } else if (width < desktopMedium) { + return 8; + } else { + return 12; + } + } + + static EdgeInsets getAdaptivePadding(BuildContext context) { + return adaptive( + context: context, + mobile: const EdgeInsets.all(16), + tablet: const EdgeInsets.all(24), + desktop: const EdgeInsets.all(32), + ); + } + + static EdgeInsets getHorizontalPadding(BuildContext context) { + return adaptive( + context: context, + mobile: const EdgeInsets.symmetric(horizontal: 16), + tablet: const EdgeInsets.symmetric(horizontal: 32), + desktop: const EdgeInsets.symmetric(horizontal: 48), + ); + } + + static double getSpacing(BuildContext context) { + return adaptive( + context: context, + mobile: 8, + tablet: 12, + desktop: 16, + ); + } + + static double getFontScale(BuildContext context) { + return adaptive( + context: context, + mobile: 1.0, + tablet: 1.1, + desktop: 1.0, + ); + } +} + +extension ResponsiveContext on BuildContext { + DeviceType get deviceType => Breakpoints.getDeviceType(this); + + bool get isMobile => Breakpoints.isMobile(this); + + bool get isTablet => Breakpoints.isTablet(this); + + bool get isDesktop => Breakpoints.isDesktop(this); + + bool get isTabletOrLarger => Breakpoints.isTabletOrLarger(this); + + bool get isMobileOrTablet => Breakpoints.isMobileOrTablet(this); + + double get screenWidth => MediaQuery.of(this).size.width; + + double get screenHeight => MediaQuery.of(this).size.height; + + T adaptive({ + required T mobile, + T? tablet, + T? desktop, + }) { + return Breakpoints.adaptive( + context: this, + mobile: mobile, + tablet: tablet, + desktop: desktop, + ); + } +} diff --git a/lib/utils/responsive.dart b/lib/utils/responsive.dart new file mode 100644 index 0000000..2eb98d0 --- /dev/null +++ b/lib/utils/responsive.dart @@ -0,0 +1,243 @@ +library; + +import 'package:flutter/material.dart'; +import 'breakpoints.dart'; + +class ResponsiveSize { + ResponsiveSize._(); + + static double widthPercent(BuildContext context, double percent) { + assert(percent >= 0 && percent <= 100, 'Percent must be between 0-100'); + return MediaQuery.of(context).size.width * (percent / 100); + } + + static double heightPercent(BuildContext context, double percent) { + assert(percent >= 0 && percent <= 100, 'Percent must be between 0-100'); + return MediaQuery.of(context).size.height * (percent / 100); + } + + static double fontSize(BuildContext context, double baseSize) { + final double scale = Breakpoints.getFontScale(context); + final double screenWidth = MediaQuery.of(context).size.width; + + double widthScale = 1.0; + if (screenWidth < 360) { + widthScale = 0.9; + } else if (screenWidth > 1920) { + widthScale = 1.1; + } + + return baseSize * scale * widthScale; + } + + static double getMinTapSize(BuildContext context) { + final TargetPlatform platform = Theme.of(context).platform; + + switch (platform) { + case TargetPlatform.iOS: + case TargetPlatform.android: + return 48.0; + case TargetPlatform.macOS: + case TargetPlatform.windows: + case TargetPlatform.linux: + return 36.0; + default: + return 44.0; + } + } + + static double iconSize(BuildContext context, {double baseSize = 24}) { + return Breakpoints.adaptive( + context: context, + mobile: baseSize, + tablet: baseSize * 1.2, + desktop: baseSize, + ); + } + + static EdgeInsets buttonPadding(BuildContext context) { + return Breakpoints.adaptive( + context: context, + mobile: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + tablet: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), + desktop: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + ); + } + + static double dialogWidth(BuildContext context) { + final double screenWidth = MediaQuery.of(context).size.width; + + return Breakpoints.adaptive( + context: context, + mobile: screenWidth * 0.9, + tablet: screenWidth * 0.7, + desktop: 500, + ); + } + + static double dialogMaxHeight(BuildContext context) { + final double screenHeight = MediaQuery.of(context).size.height; + + return Breakpoints.adaptive( + context: context, + mobile: screenHeight * 0.8, + tablet: screenHeight * 0.75, + desktop: 720, + ); + } +} + +class ResponsiveSpacing { + ResponsiveSpacing._(); + + static double xs(BuildContext context) { + return Breakpoints.adaptive( + context: context, + mobile: 4, + tablet: 6, + desktop: 8, + ); + } + + static double sm(BuildContext context) { + return Breakpoints.adaptive( + context: context, + mobile: 8, + tablet: 10, + desktop: 12, + ); + } + + static double md(BuildContext context) { + return Breakpoints.adaptive( + context: context, + mobile: 16, + tablet: 20, + desktop: 24, + ); + } + + static double lg(BuildContext context) { + return Breakpoints.adaptive( + context: context, + mobile: 24, + tablet: 32, + desktop: 40, + ); + } + + static double xl(BuildContext context) { + return Breakpoints.adaptive( + context: context, + mobile: 32, + tablet: 48, + desktop: 64, + ); + } + + static Widget verticalGap(BuildContext context, {double? size}) { + return SizedBox(height: size ?? md(context)); + } + + static Widget horizontalGap(BuildContext context, {double? size}) { + return SizedBox(width: size ?? md(context)); + } +} + +class ResponsiveLayout { + ResponsiveLayout._(); + + static int formColumns(BuildContext context) { + return Breakpoints.adaptive( + context: context, + mobile: 1, + tablet: 2, + desktop: 2, + ); + } + + static int gridCrossAxisCount(BuildContext context, {int? maxColumns}) { + final int columns = Breakpoints.getGridColumns(context); + return maxColumns != null ? columns.clamp(1, maxColumns) : columns; + } + + static double gridAspectRatio(BuildContext context) { + return Breakpoints.adaptive( + context: context, + mobile: 1.0, + tablet: 1.2, + desktop: 1.5, + ); + } + + static bool useSingleColumn(BuildContext context) { + return Breakpoints.isMobile(context); + } + + static bool useDualPane(BuildContext context) { + return Breakpoints.isTabletOrLarger(context); + } + + static int getFlex(BuildContext context, { + int mobileFlex = 1, + int? tabletFlex, + int? desktopFlex, + }) { + return Breakpoints.adaptive( + context: context, + mobile: mobileFlex, + tablet: tabletFlex, + desktop: desktopFlex, + ); + } +} + +class ResponsiveVisibility { + ResponsiveVisibility._(); + + static bool showOnMobile(BuildContext context) { + return Breakpoints.isMobile(context); + } + + static bool showOnTablet(BuildContext context) { + return Breakpoints.isTablet(context); + } + + static bool showOnDesktop(BuildContext context) { + return Breakpoints.isDesktop(context); + } + + static bool hideOnMobile(BuildContext context) { + return !Breakpoints.isMobile(context); + } + + static bool hideOnTablet(BuildContext context) { + return !Breakpoints.isTablet(context); + } + + static bool hideOnDesktop(BuildContext context) { + return !Breakpoints.isDesktop(context); + } + + static bool showOnTabletUp(BuildContext context) { + return Breakpoints.isTabletOrLarger(context); + } + + static bool showOnMobileTablet(BuildContext context) { + return Breakpoints.isMobileOrTablet(context); + } +} + +extension ResponsiveSizeExtension on num { + double wp(BuildContext context) { + return ResponsiveSize.widthPercent(context, toDouble()); + } + + double hp(BuildContext context) { + return ResponsiveSize.heightPercent(context, toDouble()); + } + + double rfs(BuildContext context) { + return ResponsiveSize.fontSize(context, toDouble()); + } +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..1e15fe9 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,1151 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f + url: "https://pub.dev" + source: hosted + version: "85.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: f4ad0fea5f102201015c9aae9d93bc02f75dd9491529a8c21f88d17a8523d44c + url: "https://pub.dev" + source: hosted + version: "7.6.0" + analyzer_plugin: + dependency: transitive + description: + name: analyzer_plugin + sha256: a5ab7590c27b779f3d4de67f31c4109dbe13dd7339f86461a6f2a8ab2594d8ce + url: "https://pub.dev" + source: hosted + version: "0.13.4" + animate_do: + dependency: "direct main" + description: + name: animate_do + sha256: b6ff08dc6cf3cb5586a86d7f32a3b5f45502d2e08e3fb4f5a484c8421c9b3fc0 + url: "https://pub.dev" + source: hosted + version: "3.3.9" + archive: + dependency: transitive + description: + name: archive + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" + url: "https://pub.dev" + source: hosted + version: "4.0.7" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + build: + dependency: transitive + description: + name: build + sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + build_config: + dependency: transitive + description: + name: build_config + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "409002f1adeea601018715d613115cfaf0e31f512cb80ae4534c79867ae2363d" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62 + url: "https://pub.dev" + source: hosted + version: "2.5.4" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792" + url: "https://pub.dev" + source: hosted + version: "9.1.2" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: a30f0a0e38671e89a492c44d005b5545b830a961575bbd8336d42869ff71066d + url: "https://pub.dev" + source: hosted + version: "8.12.0" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "11654819532ba94c34de52ff5feb52bd81cba1de00ef2ed622fd50295f9d4243" + url: "https://pub.dev" + source: hosted + version: "4.11.0" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "942a4791cd385a68ccb3b32c71c427aba508a1bb949b86dff2adbe4049f16239" + url: "https://pub.dev" + source: hosted + version: "0.3.5" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + custom_lint_core: + dependency: transitive + description: + name: custom_lint_core + sha256: "31110af3dde9d29fb10828ca33f1dce24d2798477b167675543ce3d208dee8be" + url: "https://pub.dev" + source: hosted + version: "0.7.5" + custom_lint_visitor: + dependency: transitive + description: + name: custom_lint_visitor + sha256: "4a86a0d8415a91fbb8298d6ef03e9034dc8e323a599ddc4120a0e36c433983a2" + url: "https://pub.dev" + source: hosted + version: "1.0.0+7.7.0" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + 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" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" + url: "https://pub.dev" + source: hosted + version: "0.9.3+2" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "88707a3bec4b988aaed3b4df5d7441ee4e987f20b286cddca5d6a8270cab23f2" + url: "https://pub.dev" + source: hosted + version: "0.9.4+5" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b" + url: "https://pub.dev" + source: hosted + version: "0.9.3+4" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_animate: + dependency: "direct main" + description: + name: flutter_animate + sha256: "7befe2d3252728afb77aecaaea1dec88a89d35b9b1d2eea6d04479e8af9117b5" + url: "https://pub.dev" + source: hosted + version: "4.5.2" + flutter_appauth: + dependency: "direct main" + description: + name: flutter_appauth + sha256: "84e8753fe20864da241892823ff7dbd252baa34f1649d6feb48118e8ae829ed1" + url: "https://pub.dev" + source: hosted + version: "7.0.1" + flutter_appauth_platform_interface: + dependency: transitive + description: + name: flutter_appauth_platform_interface + sha256: "0959824b401f3ee209c869734252bd5d4d4aab804b019c03815c56e3b9a4bc34" + url: "https://pub.dev" + source: hosted + version: "7.0.1" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "306f0596590e077338312f38837f595c04f28d6cdeeac392d3d74df2f0003687" + url: "https://pub.dev" + source: hosted + version: "2.0.32" + flutter_riverpod: + dependency: "direct main" + description: + name: flutter_riverpod + sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1" + url: "https://pub.dev" + source: hosted + version: "2.6.1" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea" + url: "https://pub.dev" + source: hosted + version: "9.2.4" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247" + url: "https://pub.dev" + source: hosted + version: "3.1.3" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + flutter_shaders: + dependency: transitive + description: + name: flutter_shaders + sha256: "34794acadd8275d971e02df03afee3dee0f98dbfb8c4837082ad0034f612a3e2" + url: "https://pub.dev" + source: hosted + version: "0.1.3" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + getwidget: + dependency: "direct main" + description: + name: getwidget + sha256: ab0201d6c1d27b508f05fa571e0e5038d60a603fd80303002b882f18b1c77231 + url: "https://pub.dev" + source: hosted + version: "7.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: f02fd7d2a4dc512fec615529824fdd217fecb3a3d3de68360293a551f21634b3 + url: "https://pub.dev" + source: hosted + version: "14.8.1" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + http: + dependency: "direct main" + description: + name: http + sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 + url: "https://pub.dev" + source: hosted + version: "1.5.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + iconsax: + dependency: "direct main" + description: + name: iconsax + sha256: fb0144c61f41f3f8a385fadc27783ea9f5359670be885ed7f35cef32565d5228 + url: "https://pub.dev" + source: hosted + version: "0.0.8" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "736eb56a911cf24d1859315ad09ddec0b66104bc41a7f8c5b96b4e2620cf5041" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: ca2a3b04d34e76157e9ae680ef16014fb4c2d20484e78417eaed6139330056f6 + url: "https://pub.dev" + source: hosted + version: "0.8.13+7" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "40c2a6a0da15556dc0f8e38a3246064a971a9f512386c3339b89f76db87269b6" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: e675c22790bcc24e9abd455deead2b7a88de4b79f7327a281812f14de1a56f58 + url: "https://pub.dev" + source: hosted + version: "0.8.13+1" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" + url: "https://pub.dev" + source: hosted + version: "0.2.2" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91" + url: "https://pub.dev" + source: hosted + version: "0.2.2+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c" + url: "https://pub.dev" + source: hosted + version: "2.11.1" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae + url: "https://pub.dev" + source: hosted + version: "0.2.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c + url: "https://pub.dev" + source: hosted + version: "6.9.5" + jwt_decoder: + dependency: "direct main" + description: + name: jwt_decoder + sha256: "54774aebf83f2923b99e6416b4ea915d47af3bde56884eb622de85feabbc559f" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + url: "https://pub.dev" + source: hosted + version: "5.1.1" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + lottie: + dependency: "direct main" + description: + name: lottie + sha256: "8ae0be46dbd9e19641791dc12ee480d34e1fd3f84c749adc05f3ad9342b71b95" + url: "https://pub.dev" + source: hosted + version: "3.3.2" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: e122c5ea805bb6773bb12ce667611265980940145be920cd09a4b0ec0285cb16 + url: "https://pub.dev" + source: hosted + version: "2.2.20" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: efaec349ddfc181528345c56f8eda9d6cccd71c177511b132c6a0ddaefaa2738 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + 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" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849" + url: "https://pub.dev" + source: hosted + version: "11.4.0" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc + url: "https://pub.dev" + source: hosted + version: "12.1.0" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 + url: "https://pub.dev" + source: hosted + version: "9.4.7" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.dev" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.dev" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.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: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.dev" + source: hosted + version: "1.5.2" + posix: + dependency: transitive + description: + name: posix + sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" + url: "https://pub.dev" + source: hosted + version: "6.0.3" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + riverpod: + dependency: transitive + description: + name: riverpod + sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959" + url: "https://pub.dev" + source: hosted + version: "2.6.1" + riverpod_analyzer_utils: + dependency: transitive + description: + name: riverpod_analyzer_utils + sha256: "03a17170088c63aab6c54c44456f5ab78876a1ddb6032ffde1662ddab4959611" + url: "https://pub.dev" + source: hosted + version: "0.5.10" + riverpod_annotation: + dependency: "direct main" + description: + name: riverpod_annotation + sha256: e14b0bf45b71326654e2705d462f21b958f987087be850afd60578fcd502d1b8 + url: "https://pub.dev" + source: hosted + version: "2.6.1" + riverpod_generator: + dependency: "direct dev" + description: + name: riverpod_generator + sha256: "44a0992d54473eb199ede00e2260bd3c262a86560e3c6f6374503d86d0580e36" + url: "https://pub.dev" + source: hosted + version: "2.6.5" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: a447acb083d3a5ef17f983dd36201aeea33fedadb3228fa831f2f0c92f0f3aca + url: "https://pub.dev" + source: hosted + version: "1.3.7" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + state_notifier: + dependency: transitive + description: + name: state_notifier + sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb + url: "https://pub.dev" + source: hosted + version: "1.0.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + url: "https://pub.dev" + source: hosted + version: "0.7.6" + timing: + dependency: transitive + description: + name: timing + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "5c8b6c2d89a78f5a1cca70a73d9d5f86c701b36b42f9c9dac7bad592113c28e9" + url: "https://pub.dev" + source: hosted + version: "6.3.24" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "6b63f1441e4f653ae799166a72b50b1767321ecc263a57aadf825a7a2a5477d9" + url: "https://pub.dev" + source: hosted + version: "6.3.5" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "8262208506252a3ed4ff5c0dc1e973d2c0e0ef337d0a074d35634da5d44397c9" + url: "https://pub.dev" + source: hosted + version: "3.2.4" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + uuid: + dependency: transitive + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + watcher: + dependency: transitive + description: + name: watcher + sha256: "592ab6e2892f67760543fb712ff0177f4ec76c031f02f5b4ff8d3fc5eb9fb61a" + url: "https://pub.dev" + source: hosted + version: "1.1.4" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.9.2 <4.0.0" + flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..9d72fa0 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,85 @@ +name: planb_logistic +description: "Plan B Logistics - Delivery Management Mobile App" +publish_to: 'none' + +version: 1.0.0+1 + +environment: + sdk: ^3.9.2 + +dependencies: + flutter: + sdk: flutter + + cupertino_icons: ^1.0.8 + + flutter_riverpod: ^2.5.0 + riverpod_annotation: ^2.3.5 + + animate_do: ^3.1.2 + lottie: ^3.0.0 + iconsax: ^0.0.8 + flutter_animate: ^4.3.0 + getwidget: ^7.0.0 + + flutter_appauth: ^7.0.0 + flutter_secure_storage: ^9.0.0 + jwt_decoder: ^2.0.1 + + http: ^1.2.2 + json_annotation: ^4.9.0 + + flutter_localizations: + sdk: flutter + intl: ^0.20.2 + + image_picker: ^1.0.7 + url_launcher: ^6.3.1 + permission_handler: ^11.3.0 + + go_router: ^14.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + + flutter_lints: ^5.0.0 + + build_runner: ^2.4.14 + json_serializable: ^6.9.2 + riverpod_generator: ^2.4.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + uses-material-design: true + generate: true + + fonts: + - family: Montserrat + fonts: + - asset: assets/fonts/Montserrat-VariableFont_wght.ttf + - asset: assets/fonts/Montserrat-Italic-VariableFont_wght.ttf + style: italic + - family: IBMPlexMono + fonts: + - asset: assets/fonts/IBMPlexMono-Regular.ttf + - asset: assets/fonts/IBMPlexMono-Italic.ttf + style: italic + - asset: assets/fonts/IBMPlexMono-Bold.ttf + weight: 700 + - asset: assets/fonts/IBMPlexMono-BoldItalic.ttf + weight: 700 + style: italic + - asset: assets/fonts/IBMPlexMono-Medium.ttf + weight: 500 + - asset: assets/fonts/IBMPlexMono-MediumItalic.ttf + weight: 500 + style: italic + - asset: assets/fonts/IBMPlexMono-Light.ttf + weight: 300 + - asset: assets/fonts/IBMPlexMono-LightItalic.ttf + weight: 300 + style: italic diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 0000000..e765e5a --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:planb_logistic/main.dart'; + +void main() { + testWidgets('App smoke test', (WidgetTester tester) async { + await tester.pumpWidget( + const ProviderScope( + child: PlanBLogisticApp(), + ), + ); + + // Verify that the app loads + expect(find.byType(MaterialApp), findsOneWidget); + }); +} diff --git a/web/favicon.png b/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/web/favicon.png differ diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/web/icons/Icon-192.png differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/web/icons/Icon-512.png differ diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/web/icons/Icon-maskable-192.png differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/web/icons/Icon-maskable-512.png differ diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..d285661 --- /dev/null +++ b/web/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + planb_logistic + + + + + + diff --git a/web/manifest.json b/web/manifest.json new file mode 100644 index 0000000..1e27e07 --- /dev/null +++ b/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "planb_logistic", + "short_name": "planb_logistic", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +}