Release version 1.0.0
							
								
								
									
										62
									
								
								.gitea/workflows/build.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,62 @@
 | 
			
		||||
name: Build and Release APK
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    tags:
 | 
			
		||||
      - "v*.*.*" # Runs only when a version tag is pushed
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  release:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout
 | 
			
		||||
        uses: actions/checkout@v3
 | 
			
		||||
 | 
			
		||||
      - name: Install jq
 | 
			
		||||
        run: apt-get update && apt-get install -y jq
 | 
			
		||||
 | 
			
		||||
      - name: Set up Flutter
 | 
			
		||||
        uses: subosito/flutter-action@v2
 | 
			
		||||
        with:
 | 
			
		||||
          flutter-version: "3.29.3"
 | 
			
		||||
 | 
			
		||||
      - name: Mark Flutter SDK as safe
 | 
			
		||||
        run: |
 | 
			
		||||
          git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.29.3-x64
 | 
			
		||||
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: flutter pub get
 | 
			
		||||
 | 
			
		||||
      - name: Build APK
 | 
			
		||||
        run: flutter build apk --release
 | 
			
		||||
 | 
			
		||||
      - name: Create Gitea release
 | 
			
		||||
        env:
 | 
			
		||||
          TOKEN_GITEA: ${{ secrets.TOKEN_GITEA }}
 | 
			
		||||
          TAG_NAME: ${{ github.ref_name }}
 | 
			
		||||
        run: |
 | 
			
		||||
          curl -X POST https://git.filiprojek.cz/api/v1/repos/fr/android_fuelstats/releases \
 | 
			
		||||
            -H "Authorization: token $TOKEN_GITEA" \
 | 
			
		||||
            -H "Content-Type: application/json" \
 | 
			
		||||
            -d '{
 | 
			
		||||
              "tag_name": "'"$TAG_NAME"'",
 | 
			
		||||
              "name": "'"$TAG_NAME"'",
 | 
			
		||||
              "body": "Automated release for version '"$TAG_NAME"'",
 | 
			
		||||
              "draft": false,
 | 
			
		||||
              "prerelease": false
 | 
			
		||||
            }'
 | 
			
		||||
 | 
			
		||||
      - name: Upload APK to release
 | 
			
		||||
        env:
 | 
			
		||||
          TOKEN_GITEA: ${{ secrets.TOKEN_GITEA }}
 | 
			
		||||
          TAG_NAME: ${{ github.ref_name }}
 | 
			
		||||
        run: |
 | 
			
		||||
          # Get release ID
 | 
			
		||||
          RELEASE_ID=$(curl -s -H "Authorization: token $TOKEN_GITEA" \
 | 
			
		||||
            https://git.filiprojek.cz/api/v1/repos/fr/android_fuelstats/releases/tags/$TAG_NAME \
 | 
			
		||||
            | jq -r '.id')
 | 
			
		||||
 | 
			
		||||
          curl -X POST \
 | 
			
		||||
            -H "Authorization: token $TOKEN_GITEA" \
 | 
			
		||||
            -F "attachment=@build/app/outputs/flutter-apk/app-release.apk" \
 | 
			
		||||
            https://git.filiprojek.cz/api/v1/repos/fr/android_fuelstats/releases/$RELEASE_ID/assets
 | 
			
		||||
							
								
								
									
										43
									
								
								.github/workflows/build-profile.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,43 @@
 | 
			
		||||
name: Build Profile APK
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches: ["dev"]
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  build:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout
 | 
			
		||||
        uses: actions/checkout@v4
 | 
			
		||||
 | 
			
		||||
      - name: Setup Java
 | 
			
		||||
        uses: actions/setup-java@v3
 | 
			
		||||
        with:
 | 
			
		||||
          distribution: 'temurin'
 | 
			
		||||
          java-version: '17'
 | 
			
		||||
 | 
			
		||||
      - name: Setup Flutter
 | 
			
		||||
        uses: subosito/flutter-action@v2
 | 
			
		||||
        with:
 | 
			
		||||
          channel: stable
 | 
			
		||||
 | 
			
		||||
      - name: Determine version
 | 
			
		||||
        run: |
 | 
			
		||||
          VERSION_NAME=$(grep '^version:' pubspec.yaml | sed 's/version: //')
 | 
			
		||||
          BUILD_NUMBER=$(git rev-list --count HEAD)
 | 
			
		||||
          echo "VERSION_NAME=$VERSION_NAME" >> $GITHUB_ENV
 | 
			
		||||
          echo "BUILD_NUMBER=$BUILD_NUMBER" >> $GITHUB_ENV
 | 
			
		||||
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: flutter pub get
 | 
			
		||||
 | 
			
		||||
      - name: Build profile APK
 | 
			
		||||
        run: flutter build apk --profile --build-name="$VERSION_NAME" --build-number="$BUILD_NUMBER"
 | 
			
		||||
 | 
			
		||||
      - name: Upload artifact
 | 
			
		||||
        uses: actions/upload-artifact@v4
 | 
			
		||||
        with:
 | 
			
		||||
          name: profile-apk
 | 
			
		||||
          path: build/app/outputs/flutter-apk/app-profile.apk
 | 
			
		||||
							
								
								
									
										45
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -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
 | 
			
		||||
.flutter-plugins-dependencies
 | 
			
		||||
.pub-cache/
 | 
			
		||||
.pub/
 | 
			
		||||
/build/
 | 
			
		||||
 | 
			
		||||
# 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
 | 
			
		||||
							
								
								
									
										45
									
								
								.metadata
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,45 @@
 | 
			
		||||
# 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: "ea121f8859e4b13e47a8f845e4586164519588bc"
 | 
			
		||||
  channel: "stable"
 | 
			
		||||
 | 
			
		||||
project_type: app
 | 
			
		||||
 | 
			
		||||
# Tracks metadata for the flutter migrate command
 | 
			
		||||
migration:
 | 
			
		||||
  platforms:
 | 
			
		||||
    - platform: root
 | 
			
		||||
      create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
 | 
			
		||||
      base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
 | 
			
		||||
    - platform: android
 | 
			
		||||
      create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
 | 
			
		||||
      base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
 | 
			
		||||
    - platform: ios
 | 
			
		||||
      create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
 | 
			
		||||
      base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
 | 
			
		||||
    - platform: linux
 | 
			
		||||
      create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
 | 
			
		||||
      base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
 | 
			
		||||
    - platform: macos
 | 
			
		||||
      create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
 | 
			
		||||
      base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
 | 
			
		||||
    - platform: web
 | 
			
		||||
      create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
 | 
			
		||||
      base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
 | 
			
		||||
    - platform: windows
 | 
			
		||||
      create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
 | 
			
		||||
      base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
 | 
			
		||||
 | 
			
		||||
  # 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'
 | 
			
		||||
							
								
								
									
										38
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,38 @@
 | 
			
		||||
# Fuel Stats
 | 
			
		||||
 | 
			
		||||
Fuel Stats is a Flutter application for tracking vehicle fuel consumption and service history. It lets you record refuels and maintenance, manage multiple vehicles, and visualise trends in cost and efficiency.
 | 
			
		||||
 | 
			
		||||
## Features
 | 
			
		||||
- Email-based authentication with login and sign-up flows
 | 
			
		||||
- Manage vehicles and choose a default one
 | 
			
		||||
- Log refuels with liters, price, mileage and notes
 | 
			
		||||
- Record maintenance/service events, including cost and optional photos
 | 
			
		||||
- Stats dashboard with consumption figures and kilometers driven
 | 
			
		||||
- Charts for gas price and fuel consumption trends
 | 
			
		||||
 | 
			
		||||
## Platform Support
 | 
			
		||||
The app is tested only on Android. A web build should also work, but iOS, macOS, Windows, and Linux have not been tested.
 | 
			
		||||
 | 
			
		||||
## Configuration
 | 
			
		||||
The app communicates with a Node.js backend maintained in a separate project: [Fuel Stats Server](https://github.com/filiprojek/fuelstats-server).
 | 
			
		||||
Provide the server's base URL at build time using a compile-time define:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
flutter run --dart-define=API_BASE_URL=https://api.example.com
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Getting Started
 | 
			
		||||
1. Install [Flutter](https://flutter.dev) (3.7 or later).
 | 
			
		||||
2. Fetch dependencies:
 | 
			
		||||
   ```bash
 | 
			
		||||
   flutter pub get
 | 
			
		||||
   ```
 | 
			
		||||
3. Launch the application:
 | 
			
		||||
   ```bash
 | 
			
		||||
   flutter run --dart-define=API_BASE_URL=http://localhost:6060
 | 
			
		||||
   ```
 | 
			
		||||
 | 
			
		||||
## CI/CD
 | 
			
		||||
A GitHub Actions workflow builds a profile APK on each push to the `dev` branch. The workflow reads the version from `pubspec.yaml`, uses the repository commit count as the build number, and uploads the APK as an artifact.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										28
									
								
								analysis_options.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -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
 | 
			
		||||
							
								
								
									
										14
									
								
								android/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -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
 | 
			
		||||
							
								
								
									
										46
									
								
								android/app/build.gradle.kts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,46 @@
 | 
			
		||||
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 = "cz.filiprojek.fuelstats"
 | 
			
		||||
    compileSdk = flutter.compileSdkVersion
 | 
			
		||||
    //ndkVersion = flutter.ndkVersion
 | 
			
		||||
    ndkVersion = "27.0.12077973"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    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 = "cz.filiprojek.fuelstats"
 | 
			
		||||
        // 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 = "../.."
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								android/app/src/debug/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,7 @@
 | 
			
		||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
 | 
			
		||||
    <!-- The INTERNET permission is required for development. Specifically,
 | 
			
		||||
         the Flutter tool needs it to communicate with the running application
 | 
			
		||||
         to allow setting breakpoints, to provide hot reload, etc.
 | 
			
		||||
    -->
 | 
			
		||||
    <uses-permission android:name="android.permission.INTERNET"/>
 | 
			
		||||
</manifest>
 | 
			
		||||
							
								
								
									
										30
									
								
								android/app/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,30 @@
 | 
			
		||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
 | 
			
		||||
    <application android:label="Fuel Stats" android:name="${applicationName}" android:icon="@mipmap/ic_launcher" android:usesCleartextTraffic="true">
 | 
			
		||||
        <activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTop" android:taskAffinity="" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">
 | 
			
		||||
            <!-- Specifies an Android theme to apply to this Activity as soon as
 | 
			
		||||
                 the Android process has started. This theme is visible to the user
 | 
			
		||||
                 while the Flutter UI initializes. After that, this theme continues
 | 
			
		||||
                 to determine the Window background behind the Flutter UI. -->
 | 
			
		||||
            <meta-data android:name="io.flutter.embedding.android.NormalTheme" android:resource="@style/NormalTheme"/>
 | 
			
		||||
            <intent-filter>
 | 
			
		||||
                <action android:name="android.intent.action.MAIN"/>
 | 
			
		||||
                <category android:name="android.intent.category.LAUNCHER"/>
 | 
			
		||||
            </intent-filter>
 | 
			
		||||
        </activity>
 | 
			
		||||
        <!-- Don't delete the meta-data below.
 | 
			
		||||
             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
 | 
			
		||||
        <meta-data android:name="flutterEmbedding" android:value="2"/>
 | 
			
		||||
    </application>
 | 
			
		||||
    <!-- Required to query activities that can process text, see:
 | 
			
		||||
         https://developer.android.com/training/package-visibility and
 | 
			
		||||
         https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
 | 
			
		||||
 | 
			
		||||
         In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
 | 
			
		||||
    <queries>
 | 
			
		||||
        <intent>
 | 
			
		||||
            <action android:name="android.intent.action.PROCESS_TEXT"/>
 | 
			
		||||
            <data android:mimeType="text/plain"/>
 | 
			
		||||
        </intent>
 | 
			
		||||
    </queries>
 | 
			
		||||
    <uses-permission android:name="android.permission.INTERNET" />
 | 
			
		||||
</manifest>
 | 
			
		||||
@@ -0,0 +1,5 @@
 | 
			
		||||
package cz.filiprojek.fuelstats
 | 
			
		||||
 | 
			
		||||
import io.flutter.embedding.android.FlutterActivity
 | 
			
		||||
 | 
			
		||||
class MainActivity : FlutterActivity()
 | 
			
		||||
| 
		 After Width: | Height: | Size: 4.7 KiB  | 
| 
		 After Width: | Height: | Size: 3.2 KiB  | 
							
								
								
									
										12
									
								
								android/app/src/main/res/drawable-v21/launch_background.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,12 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!-- Modify this file to customize your launch splash screen -->
 | 
			
		||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
 | 
			
		||||
    <item android:drawable="?android:colorBackground" />
 | 
			
		||||
 | 
			
		||||
    <!-- You can insert your own image assets here -->
 | 
			
		||||
    <!-- <item>
 | 
			
		||||
        <bitmap
 | 
			
		||||
            android:gravity="center"
 | 
			
		||||
            android:src="@mipmap/launch_image" />
 | 
			
		||||
    </item> -->
 | 
			
		||||
</layer-list>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 6.4 KiB  | 
| 
		 After Width: | Height: | Size: 9.5 KiB  | 
| 
		 After Width: | Height: | Size: 13 KiB  | 
							
								
								
									
										12
									
								
								android/app/src/main/res/drawable/launch_background.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,12 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!-- Modify this file to customize your launch splash screen -->
 | 
			
		||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
 | 
			
		||||
    <item android:drawable="@android:color/white" />
 | 
			
		||||
 | 
			
		||||
    <!-- You can insert your own image assets here -->
 | 
			
		||||
    <!-- <item>
 | 
			
		||||
        <bitmap
 | 
			
		||||
            android:gravity="center"
 | 
			
		||||
            android:src="@mipmap/launch_image" />
 | 
			
		||||
    </item> -->
 | 
			
		||||
</layer-list>
 | 
			
		||||
@@ -0,0 +1,9 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
 | 
			
		||||
  <background android:drawable="@color/ic_launcher_background"/>
 | 
			
		||||
  <foreground>
 | 
			
		||||
      <inset
 | 
			
		||||
          android:drawable="@drawable/ic_launcher_foreground"
 | 
			
		||||
          android:inset="16%" />
 | 
			
		||||
  </foreground>
 | 
			
		||||
</adaptive-icon>
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								android/app/src/main/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 2.2 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								android/app/src/main/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.4 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 2.9 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 4.3 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 5.6 KiB  | 
							
								
								
									
										18
									
								
								android/app/src/main/res/values-night/styles.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,18 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<resources>
 | 
			
		||||
    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
 | 
			
		||||
    <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
 | 
			
		||||
        <!-- Show a splash screen on the activity. Automatically removed when
 | 
			
		||||
             the Flutter engine draws its first frame -->
 | 
			
		||||
        <item name="android:windowBackground">@drawable/launch_background</item>
 | 
			
		||||
    </style>
 | 
			
		||||
    <!-- Theme applied to the Android Window as soon as the process has started.
 | 
			
		||||
         This theme determines the color of the Android Window while your
 | 
			
		||||
         Flutter UI initializes, as well as behind your Flutter UI while its
 | 
			
		||||
         running.
 | 
			
		||||
 | 
			
		||||
         This Theme is only used starting with V2 of Flutter's Android embedding. -->
 | 
			
		||||
    <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
 | 
			
		||||
        <item name="android:windowBackground">?android:colorBackground</item>
 | 
			
		||||
    </style>
 | 
			
		||||
</resources>
 | 
			
		||||
							
								
								
									
										4
									
								
								android/app/src/main/res/values/colors.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,4 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<resources>
 | 
			
		||||
    <color name="ic_launcher_background">#FFFFFF</color>
 | 
			
		||||
</resources>
 | 
			
		||||
							
								
								
									
										18
									
								
								android/app/src/main/res/values/styles.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,18 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<resources>
 | 
			
		||||
    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
 | 
			
		||||
    <style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
 | 
			
		||||
        <!-- Show a splash screen on the activity. Automatically removed when
 | 
			
		||||
             the Flutter engine draws its first frame -->
 | 
			
		||||
        <item name="android:windowBackground">@drawable/launch_background</item>
 | 
			
		||||
    </style>
 | 
			
		||||
    <!-- Theme applied to the Android Window as soon as the process has started.
 | 
			
		||||
         This theme determines the color of the Android Window while your
 | 
			
		||||
         Flutter UI initializes, as well as behind your Flutter UI while its
 | 
			
		||||
         running.
 | 
			
		||||
 | 
			
		||||
         This Theme is only used starting with V2 of Flutter's Android embedding. -->
 | 
			
		||||
    <style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
 | 
			
		||||
        <item name="android:windowBackground">?android:colorBackground</item>
 | 
			
		||||
    </style>
 | 
			
		||||
</resources>
 | 
			
		||||
							
								
								
									
										7
									
								
								android/app/src/profile/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,7 @@
 | 
			
		||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
 | 
			
		||||
    <!-- The INTERNET permission is required for development. Specifically,
 | 
			
		||||
         the Flutter tool needs it to communicate with the running application
 | 
			
		||||
         to allow setting breakpoints, to provide hot reload, etc.
 | 
			
		||||
    -->
 | 
			
		||||
    <uses-permission android:name="android.permission.INTERNET"/>
 | 
			
		||||
</manifest>
 | 
			
		||||
							
								
								
									
										21
									
								
								android/build.gradle.kts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,21 @@
 | 
			
		||||
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<Delete>("clean") {
 | 
			
		||||
    delete(rootProject.layout.buildDirectory)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										3
									
								
								android/gradle.properties
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,3 @@
 | 
			
		||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
 | 
			
		||||
android.useAndroidX=true
 | 
			
		||||
android.enableJetifier=true
 | 
			
		||||
							
								
								
									
										5
									
								
								android/gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -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.10.2-all.zip
 | 
			
		||||
							
								
								
									
										25
									
								
								android/settings.gradle.kts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,25 @@
 | 
			
		||||
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.7.0" apply false
 | 
			
		||||
    id("org.jetbrains.kotlin.android") version "1.8.22" apply false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
include(":app")
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								assets/icon/app_icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 40 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								assets/icon/app_icon.png.old
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 41 KiB  | 
							
								
								
									
										1
									
								
								assets/icon/app_icon.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
			
		||||
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="1024" height="1024"><style>.a{fill:#042449}.b{fill:#99aab5}.c{fill:#ffac33}.d{fill:#aab8c2}.e{fill:#be1931}.f{fill:#e1e8ed}.g{fill:#66757f}.h{fill:none;stroke:#aab8c2;stroke-linecap:round;stroke-linejoin:round;stroke-width:50}.i{fill:#e1e8ed;stroke:#aab8c2;stroke-linecap:round;stroke-linejoin:round;stroke-width:50}</style><path fill-rule="evenodd" class="a" d="m1024 0v1024h-1024v-1024z"/><path class="b" d="m483.9 788.7c-35.3 0-73-24.3-73-92.3 0-38.2 16.3-64.1 32.1-89.2 6.2-9.8 12.4-19.7 18.1-30.7-22.7-26.2-47.7-46.1-64.8-46.1v-36.8c23.7 0 52.6 20.9 78.6 49.2 5.5-18.4 9-40.2 9-67.7h29.2c0 40-6.6 70.4-15.8 95 21.1 28.4 37.8 59.6 44.2 83.6 10 38.1 7 78-7.8 104.1-11.4 19.9-29.1 30.9-49.8 30.9zm-1.9-185.4c-5.4 9.7-10.9 18.6-16.1 26.9-14.4 22.8-25.8 40.9-25.8 66.2 0 45.8 23.8 55.4 43.8 55.4 11.4 0 20.2-5.3 26.1-15.6 9.3-16.4 10.8-44.2 3.8-70.8-4.8-18.1-16.8-40.6-31.8-62.1z"/><path class="c" d="m356.3 364.4h-178.8c-32.2 0-56.6 33.1-54.5 73.8l17 332c2.1 40.8 25.9 73.8 53.2 73.8h147.4c27.2 0 51-33 53.2-73.8l17-332c2.1-40.7-22.3-73.8-54.5-73.8z"/><path class="d" d="m425.5 788.7h-320.9c-1.9 0-3.8 0.4-5.6 1.4-1.8 0.9-3.4 2.3-4.7 4-1.4 1.7-2.5 3.7-3.2 6-0.7 2.2-1.1 4.6-1.1 7v36.9h350.1v-36.9c0-2.4-0.4-4.8-1.1-7-0.7-2.3-1.8-4.3-3.2-6-1.3-1.7-2.9-3.1-4.7-4-1.8-1-3.7-1.4-5.6-1.4z"/><path class="c" d="m498.5 364.4c-2 0-3.9-0.4-5.6-1.4-1.8-0.9-3.4-2.2-4.8-4-1.3-1.7-2.4-3.7-3.1-5.9-0.7-2.3-1.1-4.7-1.1-7.1v-59.7l30.7-77.7c1.7-4.3 4.8-7.7 8.4-9.2 3.7-1.5 7.7-1.2 11.2 1 3.4 2.2 6 6 7.3 10.7 1.2 4.6 0.9 9.7-0.8 14.1l-27.6 69.8v51c0 2.4-0.4 4.8-1.2 7.1-0.7 2.2-1.8 4.2-3.1 5.9-1.4 1.8-3 3.1-4.8 4-1.7 1-3.6 1.4-5.5 1.4z"/><path class="e" d="m534.9 475.1h-11.3c2.5-5.4 4-11.7 4-18.4v-73.8c0-13.6-5.9-25.4-14.5-31.8v-5.1c0-4.9-1.6-9.6-4.3-13-2.8-3.5-6.5-5.4-10.3-5.4-3.9 0-7.6 1.9-10.4 5.4-2.7 3.4-4.2 8.1-4.2 13v5.1c-2.2 1.6-4.3 3.6-6.1 5.8-1.8 2.3-3.3 4.8-4.6 7.6-1.3 2.8-2.2 5.8-2.9 8.9-0.7 3.1-1 6.3-1 9.5v73.8c0 6.7 1.5 13 4 18.4h-11.3c-1.9 0-3.8 1-5.2 2.7-1.3 1.7-2.1 4.1-2.1 6.5 0 2.5 0.8 4.8 2.1 6.6 1.4 1.7 3.3 2.7 5.2 2.7h72.9c2 0 3.8-1 5.2-2.7 1.4-1.8 2.1-4.1 2.1-6.6 0-2.4-0.7-4.8-2.1-6.5-1.4-1.7-3.2-2.7-5.2-2.7z"/><path class="b" d="m410.9 438.2c0 2.4 0.4 4.8 1.1 7.1 0.8 2.2 1.9 4.3 3.2 6 1.4 1.7 3 3 4.7 4 1.8 0.9 3.7 1.4 5.6 1.4h73c3.8 0 7.5-2 10.3-5.4 2.7-3.5 4.3-8.2 4.3-13.1 0-4.9-1.6-9.6-4.3-13-2.8-3.5-6.5-5.4-10.3-5.4h-73c-1.9 0-3.8 0.5-5.6 1.4-1.7 0.9-3.3 2.3-4.7 4-1.3 1.7-2.4 3.7-3.2 6-0.7 2.2-1.1 4.6-1.1 7z"/><path class="f" d="m440.1 438.2c0 9.7-1.5 19.3-4.4 28.3-3 8.9-7.3 17-12.7 23.9-5.4 6.8-11.8 12.3-18.9 16-7.1 3.7-14.7 5.6-22.3 5.6h-233.4c-7.7 0-15.3-1.9-22.4-5.6-7.1-3.7-13.5-9.2-18.9-16-5.4-6.9-9.7-15-12.7-23.9-2.9-9-4.4-18.6-4.4-28.3v-184.4c0-9.7 1.5-19.3 4.4-28.3 3-8.9 7.3-17 12.7-23.9 5.4-6.8 11.8-12.3 18.9-16 7.1-3.7 14.7-5.6 22.4-5.6h233.4c7.6 0 15.2 1.9 22.3 5.6 7.1 3.7 13.5 9.2 18.9 16 5.4 6.9 9.7 15 12.7 23.9 2.9 9 4.4 18.6 4.4 28.3z"/><path class="g" d="m410.9 438.2c0 4.9-0.7 9.7-2.2 14.1-1.5 4.5-3.6 8.6-6.3 12-2.7 3.4-5.9 6.1-9.5 8-3.5 1.9-7.3 2.8-11.1 2.8h-233.4c-3.9 0-7.7-0.9-11.2-2.8-3.6-1.9-6.8-4.6-9.5-8-2.7-3.4-4.8-7.5-6.3-12-1.5-4.4-2.2-9.2-2.2-14.1v-184.4c0-4.9 0.7-9.7 2.2-14.1 1.5-4.5 3.6-8.6 6.3-12 2.7-3.4 5.9-6.1 9.5-8 3.5-1.9 7.3-2.8 11.2-2.8h233.4c3.8 0 7.6 0.9 11.1 2.8 3.6 1.9 6.8 4.6 9.5 8 2.7 3.4 4.8 7.5 6.3 12 1.5 4.4 2.2 9.2 2.2 14.1z"/><path class="d" d="m367.2 272.2v55.4h-131.3v-55.4z"/><path class="h" d="m603.7 815.3h346.6"/><path class="i" d="m738 450.1v365.2h78v-365.2c0-22.4-7.8-40.6-31.2-40.6h-15.6c-23.4 0-31.2 18.2-31.2 40.6z"/><path class="i" d="m621 571.8v243.5h69.3v-243.5c0-22.3-6.9-40.6-27.7-40.6h-13.9c-20.8 0-27.7 18.3-27.7 40.6z"/><path class="i" d="m863.7 673.3v142h69.3v-142c0-22.3-6.9-40.6-27.7-40.6h-13.9c-20.8 0-27.7 18.3-27.7 40.6z"/></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 3.7 KiB  | 
							
								
								
									
										34
									
								
								ios/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -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
 | 
			
		||||
							
								
								
									
										26
									
								
								ios/Flutter/AppFrameworkInfo.plist
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,26 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 | 
			
		||||
<plist version="1.0">
 | 
			
		||||
<dict>
 | 
			
		||||
  <key>CFBundleDevelopmentRegion</key>
 | 
			
		||||
  <string>en</string>
 | 
			
		||||
  <key>CFBundleExecutable</key>
 | 
			
		||||
  <string>App</string>
 | 
			
		||||
  <key>CFBundleIdentifier</key>
 | 
			
		||||
  <string>io.flutter.flutter.app</string>
 | 
			
		||||
  <key>CFBundleInfoDictionaryVersion</key>
 | 
			
		||||
  <string>6.0</string>
 | 
			
		||||
  <key>CFBundleName</key>
 | 
			
		||||
  <string>App</string>
 | 
			
		||||
  <key>CFBundlePackageType</key>
 | 
			
		||||
  <string>FMWK</string>
 | 
			
		||||
  <key>CFBundleShortVersionString</key>
 | 
			
		||||
  <string>1.0</string>
 | 
			
		||||
  <key>CFBundleSignature</key>
 | 
			
		||||
  <string>????</string>
 | 
			
		||||
  <key>CFBundleVersion</key>
 | 
			
		||||
  <string>1.0</string>
 | 
			
		||||
  <key>MinimumOSVersion</key>
 | 
			
		||||
  <string>12.0</string>
 | 
			
		||||
</dict>
 | 
			
		||||
</plist>
 | 
			
		||||
							
								
								
									
										1
									
								
								ios/Flutter/Debug.xcconfig
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
			
		||||
#include "Generated.xcconfig"
 | 
			
		||||
							
								
								
									
										1
									
								
								ios/Flutter/Release.xcconfig
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
			
		||||
#include "Generated.xcconfig"
 | 
			
		||||
							
								
								
									
										616
									
								
								ios/Runner.xcodeproj/project.pbxproj
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -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 = "<group>"; };
 | 
			
		||||
		1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
 | 
			
		||||
		331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
 | 
			
		||||
		331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
 | 
			
		||||
		3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
 | 
			
		||||
		74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
 | 
			
		||||
		74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
 | 
			
		||||
		7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
 | 
			
		||||
		9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
 | 
			
		||||
		9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
 | 
			
		||||
		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 = "<group>"; };
 | 
			
		||||
		97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
 | 
			
		||||
		97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
 | 
			
		||||
		97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 | 
			
		||||
/* 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 = "<group>";
 | 
			
		||||
		};
 | 
			
		||||
		9740EEB11CF90186004384FC /* Flutter */ = {
 | 
			
		||||
			isa = PBXGroup;
 | 
			
		||||
			children = (
 | 
			
		||||
				3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
 | 
			
		||||
				9740EEB21CF90195004384FC /* Debug.xcconfig */,
 | 
			
		||||
				7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
 | 
			
		||||
				9740EEB31CF90195004384FC /* Generated.xcconfig */,
 | 
			
		||||
			);
 | 
			
		||||
			name = Flutter;
 | 
			
		||||
			sourceTree = "<group>";
 | 
			
		||||
		};
 | 
			
		||||
		97C146E51CF9000F007C117D = {
 | 
			
		||||
			isa = PBXGroup;
 | 
			
		||||
			children = (
 | 
			
		||||
				9740EEB11CF90186004384FC /* Flutter */,
 | 
			
		||||
				97C146F01CF9000F007C117D /* Runner */,
 | 
			
		||||
				97C146EF1CF9000F007C117D /* Products */,
 | 
			
		||||
				331C8082294A63A400263BE5 /* RunnerTests */,
 | 
			
		||||
			);
 | 
			
		||||
			sourceTree = "<group>";
 | 
			
		||||
		};
 | 
			
		||||
		97C146EF1CF9000F007C117D /* Products */ = {
 | 
			
		||||
			isa = PBXGroup;
 | 
			
		||||
			children = (
 | 
			
		||||
				97C146EE1CF9000F007C117D /* Runner.app */,
 | 
			
		||||
				331C8081294A63A400263BE5 /* RunnerTests.xctest */,
 | 
			
		||||
			);
 | 
			
		||||
			name = Products;
 | 
			
		||||
			sourceTree = "<group>";
 | 
			
		||||
		};
 | 
			
		||||
		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 = "<group>";
 | 
			
		||||
		};
 | 
			
		||||
/* 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 = "<group>";
 | 
			
		||||
		};
 | 
			
		||||
		97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
 | 
			
		||||
			isa = PBXVariantGroup;
 | 
			
		||||
			children = (
 | 
			
		||||
				97C147001CF9000F007C117D /* Base */,
 | 
			
		||||
			);
 | 
			
		||||
			name = LaunchScreen.storyboard;
 | 
			
		||||
			sourceTree = "<group>";
 | 
			
		||||
		};
 | 
			
		||||
/* 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 = 12.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 = cz.filiprojek.fuelstats;
 | 
			
		||||
				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 = cz.filiprojek.fuelstats.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 = cz.filiprojek.fuelstats.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 = cz.filiprojek.fuelstats.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 = AppIcon;
 | 
			
		||||
				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 = 12.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 = AppIcon;
 | 
			
		||||
				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 = 12.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 = cz.filiprojek.fuelstats;
 | 
			
		||||
				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 = cz.filiprojek.fuelstats;
 | 
			
		||||
				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 */;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,7 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<Workspace
 | 
			
		||||
   version = "1.0">
 | 
			
		||||
   <FileRef
 | 
			
		||||
      location = "self:">
 | 
			
		||||
   </FileRef>
 | 
			
		||||
</Workspace>
 | 
			
		||||
@@ -0,0 +1,8 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 | 
			
		||||
<plist version="1.0">
 | 
			
		||||
<dict>
 | 
			
		||||
	<key>IDEDidComputeMac32BitWarning</key>
 | 
			
		||||
	<true/>
 | 
			
		||||
</dict>
 | 
			
		||||
</plist>
 | 
			
		||||
@@ -0,0 +1,8 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 | 
			
		||||
<plist version="1.0">
 | 
			
		||||
<dict>
 | 
			
		||||
	<key>PreviewsEnabled</key>
 | 
			
		||||
	<false/>
 | 
			
		||||
</dict>
 | 
			
		||||
</plist>
 | 
			
		||||
							
								
								
									
										99
									
								
								ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,99 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<Scheme
 | 
			
		||||
   LastUpgradeVersion = "1510"
 | 
			
		||||
   version = "1.3">
 | 
			
		||||
   <BuildAction
 | 
			
		||||
      parallelizeBuildables = "YES"
 | 
			
		||||
      buildImplicitDependencies = "YES">
 | 
			
		||||
      <BuildActionEntries>
 | 
			
		||||
         <BuildActionEntry
 | 
			
		||||
            buildForTesting = "YES"
 | 
			
		||||
            buildForRunning = "YES"
 | 
			
		||||
            buildForProfiling = "YES"
 | 
			
		||||
            buildForArchiving = "YES"
 | 
			
		||||
            buildForAnalyzing = "YES">
 | 
			
		||||
            <BuildableReference
 | 
			
		||||
               BuildableIdentifier = "primary"
 | 
			
		||||
               BlueprintIdentifier = "97C146ED1CF9000F007C117D"
 | 
			
		||||
               BuildableName = "Runner.app"
 | 
			
		||||
               BlueprintName = "Runner"
 | 
			
		||||
               ReferencedContainer = "container:Runner.xcodeproj">
 | 
			
		||||
            </BuildableReference>
 | 
			
		||||
         </BuildActionEntry>
 | 
			
		||||
      </BuildActionEntries>
 | 
			
		||||
   </BuildAction>
 | 
			
		||||
   <TestAction
 | 
			
		||||
      buildConfiguration = "Debug"
 | 
			
		||||
      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
 | 
			
		||||
      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
 | 
			
		||||
      shouldUseLaunchSchemeArgsEnv = "YES">
 | 
			
		||||
      <MacroExpansion>
 | 
			
		||||
         <BuildableReference
 | 
			
		||||
            BuildableIdentifier = "primary"
 | 
			
		||||
            BlueprintIdentifier = "97C146ED1CF9000F007C117D"
 | 
			
		||||
            BuildableName = "Runner.app"
 | 
			
		||||
            BlueprintName = "Runner"
 | 
			
		||||
            ReferencedContainer = "container:Runner.xcodeproj">
 | 
			
		||||
         </BuildableReference>
 | 
			
		||||
      </MacroExpansion>
 | 
			
		||||
      <Testables>
 | 
			
		||||
         <TestableReference
 | 
			
		||||
            skipped = "NO"
 | 
			
		||||
            parallelizable = "YES">
 | 
			
		||||
            <BuildableReference
 | 
			
		||||
               BuildableIdentifier = "primary"
 | 
			
		||||
               BlueprintIdentifier = "331C8080294A63A400263BE5"
 | 
			
		||||
               BuildableName = "RunnerTests.xctest"
 | 
			
		||||
               BlueprintName = "RunnerTests"
 | 
			
		||||
               ReferencedContainer = "container:Runner.xcodeproj">
 | 
			
		||||
            </BuildableReference>
 | 
			
		||||
         </TestableReference>
 | 
			
		||||
      </Testables>
 | 
			
		||||
   </TestAction>
 | 
			
		||||
   <LaunchAction
 | 
			
		||||
      buildConfiguration = "Debug"
 | 
			
		||||
      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
 | 
			
		||||
      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
 | 
			
		||||
      launchStyle = "0"
 | 
			
		||||
      useCustomWorkingDirectory = "NO"
 | 
			
		||||
      ignoresPersistentStateOnLaunch = "NO"
 | 
			
		||||
      debugDocumentVersioning = "YES"
 | 
			
		||||
      debugServiceExtension = "internal"
 | 
			
		||||
      enableGPUValidationMode = "1"
 | 
			
		||||
      allowLocationSimulation = "YES">
 | 
			
		||||
      <BuildableProductRunnable
 | 
			
		||||
         runnableDebuggingMode = "0">
 | 
			
		||||
         <BuildableReference
 | 
			
		||||
            BuildableIdentifier = "primary"
 | 
			
		||||
            BlueprintIdentifier = "97C146ED1CF9000F007C117D"
 | 
			
		||||
            BuildableName = "Runner.app"
 | 
			
		||||
            BlueprintName = "Runner"
 | 
			
		||||
            ReferencedContainer = "container:Runner.xcodeproj">
 | 
			
		||||
         </BuildableReference>
 | 
			
		||||
      </BuildableProductRunnable>
 | 
			
		||||
   </LaunchAction>
 | 
			
		||||
   <ProfileAction
 | 
			
		||||
      buildConfiguration = "Profile"
 | 
			
		||||
      shouldUseLaunchSchemeArgsEnv = "YES"
 | 
			
		||||
      savedToolIdentifier = ""
 | 
			
		||||
      useCustomWorkingDirectory = "NO"
 | 
			
		||||
      debugDocumentVersioning = "YES">
 | 
			
		||||
      <BuildableProductRunnable
 | 
			
		||||
         runnableDebuggingMode = "0">
 | 
			
		||||
         <BuildableReference
 | 
			
		||||
            BuildableIdentifier = "primary"
 | 
			
		||||
            BlueprintIdentifier = "97C146ED1CF9000F007C117D"
 | 
			
		||||
            BuildableName = "Runner.app"
 | 
			
		||||
            BlueprintName = "Runner"
 | 
			
		||||
            ReferencedContainer = "container:Runner.xcodeproj">
 | 
			
		||||
         </BuildableReference>
 | 
			
		||||
      </BuildableProductRunnable>
 | 
			
		||||
   </ProfileAction>
 | 
			
		||||
   <AnalyzeAction
 | 
			
		||||
      buildConfiguration = "Debug">
 | 
			
		||||
   </AnalyzeAction>
 | 
			
		||||
   <ArchiveAction
 | 
			
		||||
      buildConfiguration = "Release"
 | 
			
		||||
      revealArchiveInOrganizer = "YES">
 | 
			
		||||
   </ArchiveAction>
 | 
			
		||||
</Scheme>
 | 
			
		||||
							
								
								
									
										7
									
								
								ios/Runner.xcworkspace/contents.xcworkspacedata
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,7 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<Workspace
 | 
			
		||||
   version = "1.0">
 | 
			
		||||
   <FileRef
 | 
			
		||||
      location = "group:Runner.xcodeproj">
 | 
			
		||||
   </FileRef>
 | 
			
		||||
</Workspace>
 | 
			
		||||
@@ -0,0 +1,8 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 | 
			
		||||
<plist version="1.0">
 | 
			
		||||
<dict>
 | 
			
		||||
	<key>IDEDidComputeMac32BitWarning</key>
 | 
			
		||||
	<true/>
 | 
			
		||||
</dict>
 | 
			
		||||
</plist>
 | 
			
		||||
@@ -0,0 +1,8 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 | 
			
		||||
<plist version="1.0">
 | 
			
		||||
<dict>
 | 
			
		||||
	<key>PreviewsEnabled</key>
 | 
			
		||||
	<false/>
 | 
			
		||||
</dict>
 | 
			
		||||
</plist>
 | 
			
		||||
							
								
								
									
										13
									
								
								ios/Runner/AppDelegate.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -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)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1 @@
 | 
			
		||||
{"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":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"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":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@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"}}
 | 
			
		||||
| 
		 After Width: | Height: | Size: 33 KiB  | 
| 
		 After Width: | Height: | Size: 586 B  | 
| 
		 After Width: | Height: | Size: 1.2 KiB  | 
| 
		 After Width: | Height: | Size: 1.8 KiB  | 
| 
		 After Width: | Height: | Size: 837 B  | 
| 
		 After Width: | Height: | Size: 1.7 KiB  | 
| 
		 After Width: | Height: | Size: 2.7 KiB  | 
| 
		 After Width: | Height: | Size: 1.2 KiB  | 
| 
		 After Width: | Height: | Size: 2.4 KiB  | 
| 
		 After Width: | Height: | Size: 3.6 KiB  | 
| 
		 After Width: | Height: | Size: 1.5 KiB  | 
| 
		 After Width: | Height: | Size: 3.1 KiB  | 
| 
		 After Width: | Height: | Size: 1.6 KiB  | 
| 
		 After Width: | Height: | Size: 3.3 KiB  | 
| 
		 After Width: | Height: | Size: 3.6 KiB  | 
| 
		 After Width: | Height: | Size: 5.3 KiB  | 
| 
		 After Width: | Height: | Size: 2.2 KiB  | 
| 
		 After Width: | Height: | Size: 4.3 KiB  | 
| 
		 After Width: | Height: | Size: 2.3 KiB  | 
| 
		 After Width: | Height: | Size: 4.5 KiB  | 
| 
		 After Width: | Height: | Size: 4.9 KiB  | 
							
								
								
									
										23
									
								
								ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -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"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 68 B  | 
							
								
								
									
										
											BIN
										
									
								
								ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 68 B  | 
							
								
								
									
										
											BIN
										
									
								
								ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 68 B  | 
							
								
								
									
										5
									
								
								ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -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.
 | 
			
		||||
							
								
								
									
										37
									
								
								ios/Runner/Base.lproj/LaunchScreen.storyboard
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,37 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 | 
			
		||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
 | 
			
		||||
    <dependencies>
 | 
			
		||||
        <deployment identifier="iOS"/>
 | 
			
		||||
        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
 | 
			
		||||
    </dependencies>
 | 
			
		||||
    <scenes>
 | 
			
		||||
        <!--View Controller-->
 | 
			
		||||
        <scene sceneID="EHf-IW-A2E">
 | 
			
		||||
            <objects>
 | 
			
		||||
                <viewController id="01J-lp-oVM" sceneMemberID="viewController">
 | 
			
		||||
                    <layoutGuides>
 | 
			
		||||
                        <viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
 | 
			
		||||
                        <viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
 | 
			
		||||
                    </layoutGuides>
 | 
			
		||||
                    <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
 | 
			
		||||
                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
 | 
			
		||||
                        <subviews>
 | 
			
		||||
                            <imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
 | 
			
		||||
                            </imageView>
 | 
			
		||||
                        </subviews>
 | 
			
		||||
                        <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
 | 
			
		||||
                        <constraints>
 | 
			
		||||
                            <constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
 | 
			
		||||
                            <constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
 | 
			
		||||
                        </constraints>
 | 
			
		||||
                    </view>
 | 
			
		||||
                </viewController>
 | 
			
		||||
                <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
 | 
			
		||||
            </objects>
 | 
			
		||||
            <point key="canvasLocation" x="53" y="375"/>
 | 
			
		||||
        </scene>
 | 
			
		||||
    </scenes>
 | 
			
		||||
    <resources>
 | 
			
		||||
        <image name="LaunchImage" width="168" height="185"/>
 | 
			
		||||
    </resources>
 | 
			
		||||
</document>
 | 
			
		||||
							
								
								
									
										26
									
								
								ios/Runner/Base.lproj/Main.storyboard
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,26 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 | 
			
		||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
 | 
			
		||||
    <dependencies>
 | 
			
		||||
        <deployment identifier="iOS"/>
 | 
			
		||||
        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
 | 
			
		||||
    </dependencies>
 | 
			
		||||
    <scenes>
 | 
			
		||||
        <!--Flutter View Controller-->
 | 
			
		||||
        <scene sceneID="tne-QT-ifu">
 | 
			
		||||
            <objects>
 | 
			
		||||
                <viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
 | 
			
		||||
                    <layoutGuides>
 | 
			
		||||
                        <viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
 | 
			
		||||
                        <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
 | 
			
		||||
                    </layoutGuides>
 | 
			
		||||
                    <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
 | 
			
		||||
                        <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
 | 
			
		||||
                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
 | 
			
		||||
                        <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
 | 
			
		||||
                    </view>
 | 
			
		||||
                </viewController>
 | 
			
		||||
                <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
 | 
			
		||||
            </objects>
 | 
			
		||||
        </scene>
 | 
			
		||||
    </scenes>
 | 
			
		||||
</document>
 | 
			
		||||
							
								
								
									
										49
									
								
								ios/Runner/Info.plist
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,49 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 | 
			
		||||
<plist version="1.0">
 | 
			
		||||
  <dict>
 | 
			
		||||
    <key>CFBundleDevelopmentRegion</key>
 | 
			
		||||
    <string>$(DEVELOPMENT_LANGUAGE)</string>
 | 
			
		||||
    <key>CFBundleDisplayName</key>
 | 
			
		||||
    <string>Fuel Stats</string>
 | 
			
		||||
    <key>CFBundleExecutable</key>
 | 
			
		||||
    <string>$(EXECUTABLE_NAME)</string>
 | 
			
		||||
    <key>CFBundleIdentifier</key>
 | 
			
		||||
    <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
 | 
			
		||||
    <key>CFBundleInfoDictionaryVersion</key>
 | 
			
		||||
    <string>6.0</string>
 | 
			
		||||
    <key>CFBundleName</key>
 | 
			
		||||
    <string>Fuel Stats</string>
 | 
			
		||||
    <key>CFBundlePackageType</key>
 | 
			
		||||
    <string>APPL</string>
 | 
			
		||||
    <key>CFBundleShortVersionString</key>
 | 
			
		||||
    <string>$(FLUTTER_BUILD_NAME)</string>
 | 
			
		||||
    <key>CFBundleSignature</key>
 | 
			
		||||
    <string>????</string>
 | 
			
		||||
    <key>CFBundleVersion</key>
 | 
			
		||||
    <string>$(FLUTTER_BUILD_NUMBER)</string>
 | 
			
		||||
    <key>LSRequiresIPhoneOS</key>
 | 
			
		||||
    <true/>
 | 
			
		||||
    <key>UILaunchStoryboardName</key>
 | 
			
		||||
    <string>LaunchScreen</string>
 | 
			
		||||
    <key>UIMainStoryboardFile</key>
 | 
			
		||||
    <string>Main</string>
 | 
			
		||||
    <key>UISupportedInterfaceOrientations</key>
 | 
			
		||||
    <array>
 | 
			
		||||
      <string>UIInterfaceOrientationPortrait</string>
 | 
			
		||||
      <string>UIInterfaceOrientationLandscapeLeft</string>
 | 
			
		||||
      <string>UIInterfaceOrientationLandscapeRight</string>
 | 
			
		||||
    </array>
 | 
			
		||||
    <key>UISupportedInterfaceOrientations~ipad</key>
 | 
			
		||||
    <array>
 | 
			
		||||
      <string>UIInterfaceOrientationPortrait</string>
 | 
			
		||||
      <string>UIInterfaceOrientationPortraitUpsideDown</string>
 | 
			
		||||
      <string>UIInterfaceOrientationLandscapeLeft</string>
 | 
			
		||||
      <string>UIInterfaceOrientationLandscapeRight</string>
 | 
			
		||||
    </array>
 | 
			
		||||
    <key>CADisableMinimumFrameDurationOnPhone</key>
 | 
			
		||||
    <true/>
 | 
			
		||||
    <key>UIApplicationSupportsIndirectInputEvents</key>
 | 
			
		||||
    <true/>
 | 
			
		||||
  </dict>
 | 
			
		||||
</plist>
 | 
			
		||||
							
								
								
									
										1
									
								
								ios/Runner/Runner-Bridging-Header.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
			
		||||
#import "GeneratedPluginRegistrant.h"
 | 
			
		||||
							
								
								
									
										12
									
								
								ios/RunnerTests/RunnerTests.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -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.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										2
									
								
								lib/config.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,2 @@
 | 
			
		||||
const String apiBaseUrl =
 | 
			
		||||
    String.fromEnvironment('API_BASE_URL', defaultValue: 'https://fuelstats.filiprojek.cz');
 | 
			
		||||
							
								
								
									
										164
									
								
								lib/main.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,164 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import 'services/session_manager.dart';
 | 
			
		||||
import 'screens/home_screen.dart';
 | 
			
		||||
import 'screens/add_screen.dart';
 | 
			
		||||
import 'screens/vehicles_screen.dart';
 | 
			
		||||
import 'screens/history_screen.dart';
 | 
			
		||||
import 'screens/user_settings.dart';
 | 
			
		||||
import 'screens/login.dart';
 | 
			
		||||
import 'screens/signup.dart';
 | 
			
		||||
 | 
			
		||||
void main() async {
 | 
			
		||||
  WidgetsFlutterBinding.ensureInitialized();
 | 
			
		||||
  await SessionManager().init();
 | 
			
		||||
 | 
			
		||||
  runApp(
 | 
			
		||||
    ChangeNotifierProvider(
 | 
			
		||||
      create: (_) => SessionManager(),
 | 
			
		||||
      child: FuelStatsApp(),
 | 
			
		||||
    ),
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class FuelStatsApp extends StatelessWidget {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return MaterialApp(
 | 
			
		||||
      theme: ThemeData.dark(),
 | 
			
		||||
      themeMode: ThemeMode.dark,
 | 
			
		||||
      debugShowCheckedModeBanner: false,
 | 
			
		||||
      home: MainNavigation(),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class MainNavigation extends StatefulWidget {
 | 
			
		||||
  const MainNavigation({super.key});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  _MainNavigationState createState() => _MainNavigationState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _MainNavigationState extends State<MainNavigation> {
 | 
			
		||||
  int _currentIndex = 0;
 | 
			
		||||
 | 
			
		||||
  bool get isAuthScreen => _currentIndex == 5 || _currentIndex == 6;
 | 
			
		||||
 | 
			
		||||
  Widget get currentTitle {
 | 
			
		||||
    switch (_currentIndex) {
 | 
			
		||||
      case 0:
 | 
			
		||||
        return Text("Fuel Stats");
 | 
			
		||||
      case 1:
 | 
			
		||||
        return Text("Add record");
 | 
			
		||||
      case 2:
 | 
			
		||||
        return Text("Vehicles");
 | 
			
		||||
      case 3:
 | 
			
		||||
        return Text("History");
 | 
			
		||||
      case 4:
 | 
			
		||||
        return Text("Settings");
 | 
			
		||||
      case 5:
 | 
			
		||||
        return Text(""); // Empty title on login
 | 
			
		||||
      case 6:
 | 
			
		||||
        return Text(""); // Empty title on signup
 | 
			
		||||
      default:
 | 
			
		||||
        return Text("Fuel Stats");
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final session = Provider.of<SessionManager>(context);
 | 
			
		||||
 | 
			
		||||
    // Auto-redirect to login if not logged in and not already on auth screens
 | 
			
		||||
    Future.delayed(Duration(milliseconds: 100), () {
 | 
			
		||||
      if (!session.isLoggedIn && !isAuthScreen) {
 | 
			
		||||
        setState(() => _currentIndex = 5);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
      final screens = [
 | 
			
		||||
      HomeScreen(),
 | 
			
		||||
      AddScreen(
 | 
			
		||||
        onSaved: () {
 | 
			
		||||
          setState(() => _currentIndex = 0);
 | 
			
		||||
        },
 | 
			
		||||
      ),
 | 
			
		||||
      VehiclesScreen(),
 | 
			
		||||
      HistoryScreen(),
 | 
			
		||||
      UserSettingsScreen(
 | 
			
		||||
        onLogout: () {
 | 
			
		||||
          setState(() => _currentIndex = 5); // Go to login
 | 
			
		||||
        },
 | 
			
		||||
      ),
 | 
			
		||||
      LoginScreen(
 | 
			
		||||
        onSwitchToSignup: () {
 | 
			
		||||
          WidgetsBinding.instance.addPostFrameCallback((_) {
 | 
			
		||||
            setState(() => _currentIndex = 6);
 | 
			
		||||
          });
 | 
			
		||||
        },
 | 
			
		||||
        onLoginSuccess: () {
 | 
			
		||||
          WidgetsBinding.instance.addPostFrameCallback((_) {
 | 
			
		||||
            setState(() => _currentIndex = 0); // Go to Home
 | 
			
		||||
          });
 | 
			
		||||
        },
 | 
			
		||||
      ),
 | 
			
		||||
      SignupScreen(
 | 
			
		||||
        onSwitchToLogin: () {
 | 
			
		||||
          WidgetsBinding.instance.addPostFrameCallback((_) {
 | 
			
		||||
            setState(() => _currentIndex = 5);
 | 
			
		||||
          });
 | 
			
		||||
        },
 | 
			
		||||
        onSignupSuccess: () {
 | 
			
		||||
          WidgetsBinding.instance.addPostFrameCallback((_) {
 | 
			
		||||
            setState(() => _currentIndex = 0); // Go to Home
 | 
			
		||||
          });
 | 
			
		||||
        },
 | 
			
		||||
      ),
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: currentTitle,
 | 
			
		||||
        actions: !isAuthScreen
 | 
			
		||||
            ? [
 | 
			
		||||
                IconButton(
 | 
			
		||||
                  icon: const Icon(Icons.person),
 | 
			
		||||
                  tooltip: "User settings",
 | 
			
		||||
                  onPressed: () {
 | 
			
		||||
                    Navigator.push(
 | 
			
		||||
                      context,
 | 
			
		||||
                      MaterialPageRoute(
 | 
			
		||||
                        builder: (context) => UserSettingsScreen(
 | 
			
		||||
                          onLogout: () {
 | 
			
		||||
                            Navigator.pop(context); // Close settings
 | 
			
		||||
                            setState(() => _currentIndex = 5); // Go to Login
 | 
			
		||||
                          },
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                    );
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
              ]
 | 
			
		||||
            : null,
 | 
			
		||||
      ),
 | 
			
		||||
      body: screens[_currentIndex],
 | 
			
		||||
      bottomNavigationBar: !isAuthScreen
 | 
			
		||||
          ? BottomNavigationBar(
 | 
			
		||||
              currentIndex: _currentIndex <= 3 ? _currentIndex : 0,
 | 
			
		||||
              onTap: (index) => setState(() => _currentIndex = index),
 | 
			
		||||
              backgroundColor: Colors.grey[900],
 | 
			
		||||
              selectedItemColor: Colors.white,
 | 
			
		||||
              unselectedItemColor: Colors.grey,
 | 
			
		||||
              items: const [
 | 
			
		||||
                BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
 | 
			
		||||
                BottomNavigationBarItem(icon: Icon(Icons.add), label: 'Add'),
 | 
			
		||||
                BottomNavigationBarItem(icon: Icon(Icons.directions_car), label: 'Vehicles'),
 | 
			
		||||
                BottomNavigationBarItem(icon: Icon(Icons.history), label: 'History'),
 | 
			
		||||
              ],
 | 
			
		||||
            )
 | 
			
		||||
          : null,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										20
									
								
								lib/main.dart.old2
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,20 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
void main() {
 | 
			
		||||
  runApp(MaterialApp(home: Home()));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class Home extends StatelessWidget {
 | 
			
		||||
  const Home({super.key});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: Text("Fuel Stats"),
 | 
			
		||||
        backgroundColor: Colors.blue[700],
 | 
			
		||||
      ),
 | 
			
		||||
      body: Column(children: [Text("vim")]),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										113
									
								
								lib/models/refuel.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,113 @@
 | 
			
		||||
import 'dart:convert';
 | 
			
		||||
 | 
			
		||||
import 'vehicle.dart';
 | 
			
		||||
 | 
			
		||||
class Refuel {
 | 
			
		||||
  final String? id;
 | 
			
		||||
  final String vehicleId;
 | 
			
		||||
  final FuelType fuelType;
 | 
			
		||||
  final double liters;
 | 
			
		||||
  final double pricePerLiter;
 | 
			
		||||
  final double totalPrice;
 | 
			
		||||
  final int mileage;
 | 
			
		||||
  final String? note;
 | 
			
		||||
  final DateTime? createdAt;
 | 
			
		||||
 | 
			
		||||
  Refuel({
 | 
			
		||||
    this.id,
 | 
			
		||||
    required this.vehicleId,
 | 
			
		||||
    required this.fuelType,
 | 
			
		||||
    required this.liters,
 | 
			
		||||
    required this.pricePerLiter,
 | 
			
		||||
    required this.totalPrice,
 | 
			
		||||
    required this.mileage,
 | 
			
		||||
    this.note,
 | 
			
		||||
    this.createdAt,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  Refuel copyWith({
 | 
			
		||||
    String? id,
 | 
			
		||||
    String? vehicleId,
 | 
			
		||||
    FuelType? fuelType,
 | 
			
		||||
    double? liters,
 | 
			
		||||
    double? pricePerLiter,
 | 
			
		||||
    double? totalPrice,
 | 
			
		||||
    int? mileage,
 | 
			
		||||
    String? note,
 | 
			
		||||
    DateTime? createdAt,
 | 
			
		||||
  }) {
 | 
			
		||||
    return Refuel(
 | 
			
		||||
      id: id ?? this.id,
 | 
			
		||||
      vehicleId: vehicleId ?? this.vehicleId,
 | 
			
		||||
      fuelType: fuelType ?? this.fuelType,
 | 
			
		||||
      liters: liters ?? this.liters,
 | 
			
		||||
      pricePerLiter: pricePerLiter ?? this.pricePerLiter,
 | 
			
		||||
      totalPrice: totalPrice ?? this.totalPrice,
 | 
			
		||||
      mileage: mileage ?? this.mileage,
 | 
			
		||||
      note: note ?? this.note,
 | 
			
		||||
      createdAt: createdAt ?? this.createdAt,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Map<String, dynamic> toApiMap() {
 | 
			
		||||
    return {
 | 
			
		||||
      'vehicleId': vehicleId,
 | 
			
		||||
      'fuelType': fuelType.name,
 | 
			
		||||
      'note': note,
 | 
			
		||||
      'liters': liters,
 | 
			
		||||
      'pricePerLiter': pricePerLiter,
 | 
			
		||||
      'totalPrice': totalPrice,
 | 
			
		||||
      'mileage': mileage,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  factory Refuel.fromApi(Map<String, dynamic> map) {
 | 
			
		||||
    return Refuel(
 | 
			
		||||
      id: map['id'] as String?,
 | 
			
		||||
      vehicleId: map['vehicleId'] as String,
 | 
			
		||||
      fuelType:
 | 
			
		||||
          FuelType.values.firstWhere((e) => e.name == map['fuelType'] as String),
 | 
			
		||||
      note: map['note'] as String?,
 | 
			
		||||
      liters: (map['liters'] as num).toDouble(),
 | 
			
		||||
      pricePerLiter: (map['pricePerLiter'] as num).toDouble(),
 | 
			
		||||
      totalPrice: (map['totalPrice'] as num).toDouble(),
 | 
			
		||||
      mileage: (map['mileage'] as num).toInt(),
 | 
			
		||||
      createdAt: map['createdAt'] != null
 | 
			
		||||
          ? DateTime.tryParse(map['createdAt'] as String)
 | 
			
		||||
          : null,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Map<String, dynamic> toMap() => {
 | 
			
		||||
        'id': id,
 | 
			
		||||
        'vehicleId': vehicleId,
 | 
			
		||||
        'fuelType': fuelType.name,
 | 
			
		||||
        'note': note,
 | 
			
		||||
        'liters': liters,
 | 
			
		||||
        'pricePerLiter': pricePerLiter,
 | 
			
		||||
        'totalPrice': totalPrice,
 | 
			
		||||
        'mileage': mileage,
 | 
			
		||||
        'createdAt': createdAt?.toIso8601String(),
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
  factory Refuel.fromMap(Map<String, dynamic> map) {
 | 
			
		||||
    return Refuel(
 | 
			
		||||
      id: map['id'] as String?,
 | 
			
		||||
      vehicleId: map['vehicleId'] as String,
 | 
			
		||||
      fuelType:
 | 
			
		||||
          FuelType.values.firstWhere((e) => e.name == map['fuelType'] as String),
 | 
			
		||||
      note: map['note'] as String?,
 | 
			
		||||
      liters: (map['liters'] as num).toDouble(),
 | 
			
		||||
      pricePerLiter: (map['pricePerLiter'] as num).toDouble(),
 | 
			
		||||
      totalPrice: (map['totalPrice'] as num).toDouble(),
 | 
			
		||||
      mileage: (map['mileage'] as num).toInt(),
 | 
			
		||||
      createdAt: map['createdAt'] != null
 | 
			
		||||
          ? DateTime.tryParse(map['createdAt'] as String)
 | 
			
		||||
          : null,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  String toJson() => jsonEncode(toMap());
 | 
			
		||||
  factory Refuel.fromJson(String source) =>
 | 
			
		||||
      Refuel.fromMap(jsonDecode(source) as Map<String, dynamic>);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										235
									
								
								lib/models/service.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,235 @@
 | 
			
		||||
import 'dart:convert';
 | 
			
		||||
 | 
			
		||||
class ServiceRecord {
 | 
			
		||||
  final String? id;
 | 
			
		||||
  final String vehicleId;
 | 
			
		||||
  final ServiceType serviceType;
 | 
			
		||||
  final String? customType;
 | 
			
		||||
  final String? itemName;
 | 
			
		||||
  final double cost;
 | 
			
		||||
  final int mileage;
 | 
			
		||||
  final String? shop;
 | 
			
		||||
  final bool selfService;
 | 
			
		||||
  final String? note;
 | 
			
		||||
  final List<String> photos;
 | 
			
		||||
  final DateTime? date;
 | 
			
		||||
  final DateTime? createdAt;
 | 
			
		||||
 | 
			
		||||
  ServiceRecord({
 | 
			
		||||
    this.id,
 | 
			
		||||
    required this.vehicleId,
 | 
			
		||||
    required this.serviceType,
 | 
			
		||||
    this.customType,
 | 
			
		||||
    this.itemName,
 | 
			
		||||
    required this.cost,
 | 
			
		||||
    required this.mileage,
 | 
			
		||||
    this.shop,
 | 
			
		||||
    this.selfService = false,
 | 
			
		||||
    this.note,
 | 
			
		||||
    List<String>? photos,
 | 
			
		||||
    this.date,
 | 
			
		||||
    this.createdAt,
 | 
			
		||||
  }) : photos = photos ?? [];
 | 
			
		||||
 | 
			
		||||
  String get displayType =>
 | 
			
		||||
      serviceType == ServiceType.other && customType != null && customType!.isNotEmpty
 | 
			
		||||
          ? customType!
 | 
			
		||||
          : serviceType.label;
 | 
			
		||||
 | 
			
		||||
  ServiceRecord copyWith({
 | 
			
		||||
    String? id,
 | 
			
		||||
    String? vehicleId,
 | 
			
		||||
    ServiceType? serviceType,
 | 
			
		||||
    String? customType,
 | 
			
		||||
    String? itemName,
 | 
			
		||||
    double? cost,
 | 
			
		||||
    int? mileage,
 | 
			
		||||
    String? shop,
 | 
			
		||||
    bool? selfService,
 | 
			
		||||
    String? note,
 | 
			
		||||
    List<String>? photos,
 | 
			
		||||
    DateTime? date,
 | 
			
		||||
    DateTime? createdAt,
 | 
			
		||||
  }) {
 | 
			
		||||
    return ServiceRecord(
 | 
			
		||||
      id: id ?? this.id,
 | 
			
		||||
      vehicleId: vehicleId ?? this.vehicleId,
 | 
			
		||||
      serviceType: serviceType ?? this.serviceType,
 | 
			
		||||
      customType: customType ?? this.customType,
 | 
			
		||||
      itemName: itemName ?? this.itemName,
 | 
			
		||||
      cost: cost ?? this.cost,
 | 
			
		||||
      mileage: mileage ?? this.mileage,
 | 
			
		||||
      shop: shop ?? this.shop,
 | 
			
		||||
      selfService: selfService ?? this.selfService,
 | 
			
		||||
      note: note ?? this.note,
 | 
			
		||||
      photos: photos ?? List.of(this.photos),
 | 
			
		||||
      date: date ?? this.date,
 | 
			
		||||
      createdAt: createdAt ?? this.createdAt,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Map<String, dynamic> toApiMap() {
 | 
			
		||||
    return {
 | 
			
		||||
      'vehicleId': vehicleId,
 | 
			
		||||
      'serviceType': serviceType.apiValue,
 | 
			
		||||
      if (customType != null && customType!.isNotEmpty) 'customType': customType,
 | 
			
		||||
      if (itemName != null) 'itemName': itemName,
 | 
			
		||||
      'cost': cost,
 | 
			
		||||
      'mileage': mileage,
 | 
			
		||||
      if (shop != null && shop!.isNotEmpty) 'shop': shop,
 | 
			
		||||
      if (selfService) 'selfService': true,
 | 
			
		||||
      if (note != null) 'note': note,
 | 
			
		||||
      'photos': photos,
 | 
			
		||||
      if (date != null) 'date': date!.toUtc().toIso8601String(),
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  factory ServiceRecord.fromApi(Map<String, dynamic> map) {
 | 
			
		||||
    return ServiceRecord(
 | 
			
		||||
      id: map['id'] as String?,
 | 
			
		||||
      vehicleId: map['vehicleId'] as String,
 | 
			
		||||
      serviceType: ServiceTypeX.fromApi(map['serviceType'] as String),
 | 
			
		||||
      customType: map['customType'] as String?,
 | 
			
		||||
      itemName: map['itemName'] as String?,
 | 
			
		||||
      cost: (map['cost'] as num).toDouble(),
 | 
			
		||||
      mileage: (map['mileage'] as num).toInt(),
 | 
			
		||||
      shop: map['shop'] as String?,
 | 
			
		||||
      selfService: map['selfService'] as bool? ?? false,
 | 
			
		||||
      note: map['note'] as String?,
 | 
			
		||||
      photos: map['photos'] != null
 | 
			
		||||
          ? List<String>.from(map['photos'] as List)
 | 
			
		||||
          : [],
 | 
			
		||||
      date: map['date'] != null
 | 
			
		||||
          ? DateTime.tryParse(map['date'] as String)
 | 
			
		||||
          : null,
 | 
			
		||||
      createdAt: map['createdAt'] != null
 | 
			
		||||
          ? DateTime.tryParse(map['createdAt'] as String)
 | 
			
		||||
          : null,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Map<String, dynamic> toMap() => {
 | 
			
		||||
        'id': id,
 | 
			
		||||
        'vehicleId': vehicleId,
 | 
			
		||||
        'serviceType': serviceType.apiValue,
 | 
			
		||||
        'customType': customType,
 | 
			
		||||
        'itemName': itemName,
 | 
			
		||||
        'cost': cost,
 | 
			
		||||
        'mileage': mileage,
 | 
			
		||||
        'shop': shop,
 | 
			
		||||
        'selfService': selfService,
 | 
			
		||||
        'note': note,
 | 
			
		||||
        'photos': photos,
 | 
			
		||||
        'date': date?.toIso8601String(),
 | 
			
		||||
        'createdAt': createdAt?.toIso8601String(),
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
  factory ServiceRecord.fromMap(Map<String, dynamic> map) {
 | 
			
		||||
    return ServiceRecord(
 | 
			
		||||
      id: map['id'] as String?,
 | 
			
		||||
      vehicleId: map['vehicleId'] as String,
 | 
			
		||||
      serviceType: ServiceTypeX.fromApi(map['serviceType'] as String),
 | 
			
		||||
      customType: map['customType'] as String?,
 | 
			
		||||
      itemName: map['itemName'] as String?,
 | 
			
		||||
      cost: (map['cost'] as num).toDouble(),
 | 
			
		||||
      mileage: (map['mileage'] as num).toInt(),
 | 
			
		||||
      shop: map['shop'] as String?,
 | 
			
		||||
      selfService: map['selfService'] as bool? ?? false,
 | 
			
		||||
      note: map['note'] as String?,
 | 
			
		||||
      photos: map['photos'] != null
 | 
			
		||||
          ? List<String>.from(map['photos'] as List)
 | 
			
		||||
          : [],
 | 
			
		||||
      date: map['date'] != null
 | 
			
		||||
          ? DateTime.tryParse(map['date'] as String)
 | 
			
		||||
          : null,
 | 
			
		||||
      createdAt: map['createdAt'] != null
 | 
			
		||||
          ? DateTime.tryParse(map['createdAt'] as String)
 | 
			
		||||
          : null,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  String toJson() => jsonEncode(toMap());
 | 
			
		||||
  factory ServiceRecord.fromJson(String source) =>
 | 
			
		||||
      ServiceRecord.fromMap(jsonDecode(source) as Map<String, dynamic>);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum ServiceType {
 | 
			
		||||
  airFilter,
 | 
			
		||||
  oilFilter,
 | 
			
		||||
  fuelFilter,
 | 
			
		||||
  cabinFilter,
 | 
			
		||||
  motorOil,
 | 
			
		||||
  brakePadFront,
 | 
			
		||||
  brakePadRear,
 | 
			
		||||
  sparkPlug,
 | 
			
		||||
  coolant,
 | 
			
		||||
  tireChange,
 | 
			
		||||
  battery,
 | 
			
		||||
  other,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension ServiceTypeX on ServiceType {
 | 
			
		||||
  String get label {
 | 
			
		||||
    switch (this) {
 | 
			
		||||
      case ServiceType.airFilter:
 | 
			
		||||
        return 'Air Filter';
 | 
			
		||||
      case ServiceType.oilFilter:
 | 
			
		||||
        return 'Oil Filter';
 | 
			
		||||
      case ServiceType.fuelFilter:
 | 
			
		||||
        return 'Fuel Filter';
 | 
			
		||||
      case ServiceType.cabinFilter:
 | 
			
		||||
        return 'Cabin Filter';
 | 
			
		||||
      case ServiceType.motorOil:
 | 
			
		||||
        return 'Motor Oil';
 | 
			
		||||
      case ServiceType.brakePadFront:
 | 
			
		||||
        return 'Brake Pads (Front)';
 | 
			
		||||
      case ServiceType.brakePadRear:
 | 
			
		||||
        return 'Brake Pads (Rear)';
 | 
			
		||||
      case ServiceType.sparkPlug:
 | 
			
		||||
        return 'Spark Plugs';
 | 
			
		||||
      case ServiceType.coolant:
 | 
			
		||||
        return 'Coolant';
 | 
			
		||||
      case ServiceType.tireChange:
 | 
			
		||||
        return 'Tire Change';
 | 
			
		||||
      case ServiceType.battery:
 | 
			
		||||
        return 'Battery';
 | 
			
		||||
      case ServiceType.other:
 | 
			
		||||
        return 'Other';
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  String get apiValue {
 | 
			
		||||
    switch (this) {
 | 
			
		||||
      case ServiceType.airFilter:
 | 
			
		||||
        return 'air_filter';
 | 
			
		||||
      case ServiceType.oilFilter:
 | 
			
		||||
        return 'oil_filter';
 | 
			
		||||
      case ServiceType.fuelFilter:
 | 
			
		||||
        return 'fuel_filter';
 | 
			
		||||
      case ServiceType.cabinFilter:
 | 
			
		||||
        return 'cabin_filter';
 | 
			
		||||
      case ServiceType.motorOil:
 | 
			
		||||
        return 'motor_oil';
 | 
			
		||||
      case ServiceType.brakePadFront:
 | 
			
		||||
        return 'brake_pad_front';
 | 
			
		||||
      case ServiceType.brakePadRear:
 | 
			
		||||
        return 'brake_pad_rear';
 | 
			
		||||
      case ServiceType.sparkPlug:
 | 
			
		||||
        return 'spark_plug';
 | 
			
		||||
      case ServiceType.coolant:
 | 
			
		||||
        return 'coolant';
 | 
			
		||||
      case ServiceType.tireChange:
 | 
			
		||||
        return 'tire_change';
 | 
			
		||||
      case ServiceType.battery:
 | 
			
		||||
        return 'battery';
 | 
			
		||||
      case ServiceType.other:
 | 
			
		||||
        return 'other';
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static ServiceType fromApi(String value) {
 | 
			
		||||
    return ServiceType.values.firstWhere(
 | 
			
		||||
        (e) => e.apiValue == value,
 | 
			
		||||
        orElse: () => ServiceType.other);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										104
									
								
								lib/models/vehicle.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,104 @@
 | 
			
		||||
import 'dart:convert';
 | 
			
		||||
 | 
			
		||||
enum FuelType { diesel, gasoline95, gasoline98, other }
 | 
			
		||||
 | 
			
		||||
extension FuelTypeX on FuelType {
 | 
			
		||||
  String get label {
 | 
			
		||||
    switch (this) {
 | 
			
		||||
      case FuelType.diesel:
 | 
			
		||||
        return 'Diesel';
 | 
			
		||||
      case FuelType.gasoline95:
 | 
			
		||||
        return 'Gasoline 95';
 | 
			
		||||
      case FuelType.gasoline98:
 | 
			
		||||
        return 'Gasoline 98';
 | 
			
		||||
      case FuelType.other:
 | 
			
		||||
        return 'Other';
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static FuelType fromIndex(int index) => FuelType.values[index];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class Vehicle {
 | 
			
		||||
  final String? id;
 | 
			
		||||
  final String name;
 | 
			
		||||
  final String registrationPlate;
 | 
			
		||||
  final FuelType fuelType;
 | 
			
		||||
  final String? note;
 | 
			
		||||
  final bool isDefault;
 | 
			
		||||
 | 
			
		||||
  Vehicle({
 | 
			
		||||
    this.id,
 | 
			
		||||
    required this.name,
 | 
			
		||||
    required this.registrationPlate,
 | 
			
		||||
    required this.fuelType,
 | 
			
		||||
    this.note,
 | 
			
		||||
    this.isDefault = false,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  Vehicle copyWith({
 | 
			
		||||
    String? id,
 | 
			
		||||
    String? name,
 | 
			
		||||
    String? registrationPlate,
 | 
			
		||||
    FuelType? fuelType,
 | 
			
		||||
    String? note,
 | 
			
		||||
    bool? isDefault,
 | 
			
		||||
  }) {
 | 
			
		||||
    return Vehicle(
 | 
			
		||||
      id: id ?? this.id,
 | 
			
		||||
      name: name ?? this.name,
 | 
			
		||||
      registrationPlate: registrationPlate ?? this.registrationPlate,
 | 
			
		||||
      fuelType: fuelType ?? this.fuelType,
 | 
			
		||||
      note: note ?? this.note,
 | 
			
		||||
      isDefault: isDefault ?? this.isDefault,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Map<String, dynamic> toApiMap() {
 | 
			
		||||
    return {
 | 
			
		||||
      'name': name,
 | 
			
		||||
      'registrationPlate': registrationPlate,
 | 
			
		||||
      'fuelType': fuelType.name,
 | 
			
		||||
      'note': note,
 | 
			
		||||
      'isDefault': isDefault,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  factory Vehicle.fromApi(Map<String, dynamic> map) {
 | 
			
		||||
    return Vehicle(
 | 
			
		||||
      id: map['id'] as String?,
 | 
			
		||||
      name: map['name'] as String,
 | 
			
		||||
      registrationPlate: map['registrationPlate'] as String,
 | 
			
		||||
      fuelType: FuelType.values
 | 
			
		||||
          .firstWhere((e) => e.name == map['fuelType'] as String),
 | 
			
		||||
      note: map['note'] as String?,
 | 
			
		||||
      isDefault: map['isDefault'] as bool? ?? false,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Map<String, dynamic> toMap() => {
 | 
			
		||||
        'id': id,
 | 
			
		||||
        'name': name,
 | 
			
		||||
        'registrationPlate': registrationPlate,
 | 
			
		||||
        'fuelType': fuelType.index,
 | 
			
		||||
        'note': note,
 | 
			
		||||
        'isDefault': isDefault,
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
  factory Vehicle.fromMap(Map<String, dynamic> map) {
 | 
			
		||||
    return Vehicle(
 | 
			
		||||
      id: map['id'] as String?,
 | 
			
		||||
      name: map['name'] as String,
 | 
			
		||||
      registrationPlate: map['registrationPlate'] as String,
 | 
			
		||||
      fuelType: map['fuelType'] is int
 | 
			
		||||
          ? FuelTypeX.fromIndex(map['fuelType'] as int)
 | 
			
		||||
          : FuelType.values
 | 
			
		||||
              .firstWhere((e) => e.name == map['fuelType'] as String),
 | 
			
		||||
      note: map['note'] as String?,
 | 
			
		||||
      isDefault: map['isDefault'] as bool? ?? false,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  String toJson() => jsonEncode(toMap());
 | 
			
		||||
  factory Vehicle.fromJson(String source) => Vehicle.fromMap(jsonDecode(source));
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										279
									
								
								lib/screens/add_screen.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,279 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
 | 
			
		||||
import '../models/vehicle.dart';
 | 
			
		||||
import '../models/refuel.dart';
 | 
			
		||||
import '../services/session_manager.dart';
 | 
			
		||||
import 'add_service_screen.dart';
 | 
			
		||||
 | 
			
		||||
class AddScreen extends StatefulWidget {
 | 
			
		||||
  final Refuel? refuel;
 | 
			
		||||
  final VoidCallback? onSaved;
 | 
			
		||||
  final bool standalone;
 | 
			
		||||
 | 
			
		||||
  AddScreen({this.refuel, this.onSaved, this.standalone = false});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  _AddScreenState createState() => _AddScreenState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _AddScreenState extends State<AddScreen> {
 | 
			
		||||
  final _formKey = GlobalKey<FormState>();
 | 
			
		||||
 | 
			
		||||
  String? _selectedVehicleId;
 | 
			
		||||
  FuelType? _selectedFuelType;
 | 
			
		||||
  final TextEditingController _litersController = TextEditingController();
 | 
			
		||||
  final TextEditingController _pricePerLiterController =
 | 
			
		||||
      TextEditingController();
 | 
			
		||||
  final TextEditingController _totalPriceController = TextEditingController();
 | 
			
		||||
  final TextEditingController _mileageController = TextEditingController();
 | 
			
		||||
  final TextEditingController _noteController = TextEditingController();
 | 
			
		||||
 | 
			
		||||
  final FocusNode _litersFocus = FocusNode();
 | 
			
		||||
  final FocusNode _pricePerLiterFocus = FocusNode();
 | 
			
		||||
  final FocusNode _totalPriceFocus = FocusNode();
 | 
			
		||||
 | 
			
		||||
  bool _initialized = false;
 | 
			
		||||
  bool _isRecalculating = false;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    _litersController.addListener(_recalculate);
 | 
			
		||||
    _pricePerLiterController.addListener(_recalculate);
 | 
			
		||||
    _totalPriceController.addListener(_recalculate);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void didChangeDependencies() {
 | 
			
		||||
    super.didChangeDependencies();
 | 
			
		||||
    if (_initialized) return;
 | 
			
		||||
    final session = Provider.of<SessionManager>(context);
 | 
			
		||||
    if (widget.refuel != null) {
 | 
			
		||||
      final r = widget.refuel!;
 | 
			
		||||
      _selectedVehicleId = r.vehicleId;
 | 
			
		||||
      _selectedFuelType = r.fuelType;
 | 
			
		||||
      _litersController.text = r.liters.toString();
 | 
			
		||||
      _pricePerLiterController.text = r.pricePerLiter.toString();
 | 
			
		||||
      _totalPriceController.text = r.totalPrice.toString();
 | 
			
		||||
      _mileageController.text = r.mileage.toString();
 | 
			
		||||
      if (r.note != null) _noteController.text = r.note!;
 | 
			
		||||
    } else {
 | 
			
		||||
      _selectedVehicleId = session.defaultVehicle?.id;
 | 
			
		||||
      _selectedFuelType = session.defaultVehicle?.fuelType;
 | 
			
		||||
    }
 | 
			
		||||
    _initialized = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final session = Provider.of<SessionManager>(context);
 | 
			
		||||
    final vehicles = session.vehicles;
 | 
			
		||||
 | 
			
		||||
    final form = Padding(
 | 
			
		||||
      padding: const EdgeInsets.all(16.0),
 | 
			
		||||
      child: Form(
 | 
			
		||||
        key: _formKey,
 | 
			
		||||
        child: SingleChildScrollView(
 | 
			
		||||
          child: Column(
 | 
			
		||||
            crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
			
		||||
            children: [
 | 
			
		||||
              DropdownButtonFormField<String>(
 | 
			
		||||
                decoration: InputDecoration(labelText: 'Vehicle'),
 | 
			
		||||
                value: _selectedVehicleId,
 | 
			
		||||
                items: vehicles
 | 
			
		||||
                    .map(
 | 
			
		||||
                      (vehicle) => DropdownMenuItem(
 | 
			
		||||
                        value: vehicle.id,
 | 
			
		||||
                        child: Text(vehicle.name),
 | 
			
		||||
                      ),
 | 
			
		||||
                    )
 | 
			
		||||
                    .toList(),
 | 
			
		||||
                onChanged: (value) {
 | 
			
		||||
                  setState(() {
 | 
			
		||||
                    _selectedVehicleId = value;
 | 
			
		||||
                    final vehicle = vehicles.firstWhere((v) => v.id == value);
 | 
			
		||||
                    _selectedFuelType = vehicle.fuelType;
 | 
			
		||||
                  });
 | 
			
		||||
                },
 | 
			
		||||
                validator:
 | 
			
		||||
                    (value) => value == null ? 'Please select a vehicle' : null,
 | 
			
		||||
              ),
 | 
			
		||||
              SizedBox(height: 16),
 | 
			
		||||
              DropdownButtonFormField<FuelType>(
 | 
			
		||||
                decoration: InputDecoration(labelText: 'Fuel Type'),
 | 
			
		||||
                value: _selectedFuelType,
 | 
			
		||||
                items: FuelType.values
 | 
			
		||||
                    .map((fuel) => DropdownMenuItem(
 | 
			
		||||
                          value: fuel,
 | 
			
		||||
                          child: Text(fuel.label),
 | 
			
		||||
                        ))
 | 
			
		||||
                    .toList(),
 | 
			
		||||
                onChanged: (value) => setState(() => _selectedFuelType = value),
 | 
			
		||||
                validator:
 | 
			
		||||
                    (value) => value == null ? 'Please select a fuel type' : null,
 | 
			
		||||
              ),
 | 
			
		||||
              SizedBox(height: 16),
 | 
			
		||||
              TextFormField(
 | 
			
		||||
                  controller: _litersController,
 | 
			
		||||
                  focusNode: _litersFocus,
 | 
			
		||||
                  decoration: InputDecoration(labelText: 'Liters'),
 | 
			
		||||
                  keyboardType: TextInputType.number,
 | 
			
		||||
                  validator: _numberValidator,
 | 
			
		||||
                ),
 | 
			
		||||
                SizedBox(height: 16),
 | 
			
		||||
                TextFormField(
 | 
			
		||||
                  controller: _pricePerLiterController,
 | 
			
		||||
                  focusNode: _pricePerLiterFocus,
 | 
			
		||||
                  decoration: InputDecoration(labelText: 'Price per Liter'),
 | 
			
		||||
                  keyboardType: TextInputType.number,
 | 
			
		||||
                  validator: _numberValidator,
 | 
			
		||||
                ),
 | 
			
		||||
                SizedBox(height: 16),
 | 
			
		||||
                TextFormField(
 | 
			
		||||
                  controller: _totalPriceController,
 | 
			
		||||
                  focusNode: _totalPriceFocus,
 | 
			
		||||
                  decoration: InputDecoration(labelText: 'Total Price'),
 | 
			
		||||
                  keyboardType: TextInputType.number,
 | 
			
		||||
                  validator: _numberValidator,
 | 
			
		||||
                ),
 | 
			
		||||
                SizedBox(height: 16),
 | 
			
		||||
                TextFormField(
 | 
			
		||||
                  controller: _mileageController,
 | 
			
		||||
                  decoration: InputDecoration(labelText: 'Mileage'),
 | 
			
		||||
                  keyboardType: TextInputType.number,
 | 
			
		||||
                  validator: _numberValidator,
 | 
			
		||||
                ),
 | 
			
		||||
                SizedBox(height: 16),
 | 
			
		||||
                TextFormField(
 | 
			
		||||
                  controller: _noteController,
 | 
			
		||||
                  decoration: InputDecoration(labelText: 'Note'),
 | 
			
		||||
                  keyboardType: TextInputType.text,
 | 
			
		||||
                ),
 | 
			
		||||
                SizedBox(height: 24),
 | 
			
		||||
                ElevatedButton(
 | 
			
		||||
                  onPressed: _submitForm,
 | 
			
		||||
                  style: ElevatedButton.styleFrom(
 | 
			
		||||
                    backgroundColor: Colors.green,
 | 
			
		||||
                  ),
 | 
			
		||||
                  child: Text(widget.refuel == null
 | 
			
		||||
                      ? 'Create Fuel Record'
 | 
			
		||||
                      : 'Update Fuel Record'),
 | 
			
		||||
                ),
 | 
			
		||||
                if (!widget.standalone) ...[
 | 
			
		||||
                  SizedBox(height: 16),
 | 
			
		||||
                  ElevatedButton.icon(
 | 
			
		||||
                    onPressed: () {
 | 
			
		||||
                      Navigator.push(
 | 
			
		||||
                        context,
 | 
			
		||||
                        MaterialPageRoute(
 | 
			
		||||
                          builder: (_) => AddServiceScreen(standalone: true),
 | 
			
		||||
                        ),
 | 
			
		||||
                      );
 | 
			
		||||
                    },
 | 
			
		||||
                    icon: Icon(Icons.build),
 | 
			
		||||
                    label: Text('Add Service Record'),
 | 
			
		||||
                  ),
 | 
			
		||||
                ],
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    if (widget.standalone) {
 | 
			
		||||
      return Scaffold(
 | 
			
		||||
        appBar: AppBar(
 | 
			
		||||
          title: Text(widget.refuel == null
 | 
			
		||||
              ? 'Add Refuel Record'
 | 
			
		||||
              : 'Edit Refuel Record'),
 | 
			
		||||
        ),
 | 
			
		||||
        body: form,
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return form;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  String? _numberValidator(String? value) {
 | 
			
		||||
    if (value == null || value.isEmpty) return 'This field cannot be empty';
 | 
			
		||||
    if (double.tryParse(value) == null) return 'Enter a valid number';
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _recalculate() {
 | 
			
		||||
    if (_isRecalculating) return;
 | 
			
		||||
 | 
			
		||||
    final liters = double.tryParse(_litersController.text);
 | 
			
		||||
    final price = double.tryParse(_pricePerLiterController.text);
 | 
			
		||||
    final total = double.tryParse(_totalPriceController.text);
 | 
			
		||||
 | 
			
		||||
    _isRecalculating = true;
 | 
			
		||||
 | 
			
		||||
    if (!_totalPriceFocus.hasFocus && liters != null && price != null) {
 | 
			
		||||
      _totalPriceController.text = (liters * price).toStringAsFixed(2);
 | 
			
		||||
    } else if (!_pricePerLiterFocus.hasFocus && liters != null && total != null) {
 | 
			
		||||
      _pricePerLiterController.text = (total / liters).toStringAsFixed(2);
 | 
			
		||||
    } else if (!_litersFocus.hasFocus && price != null && total != null) {
 | 
			
		||||
      _litersController.text = (total / price).toStringAsFixed(2);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _isRecalculating = false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _submitForm() async {
 | 
			
		||||
    if (!(_formKey.currentState?.validate() ?? false)) return;
 | 
			
		||||
    final session = Provider.of<SessionManager>(context, listen: false);
 | 
			
		||||
    final refuel = Refuel(
 | 
			
		||||
      id: widget.refuel?.id,
 | 
			
		||||
      vehicleId: _selectedVehicleId!,
 | 
			
		||||
      fuelType: _selectedFuelType!,
 | 
			
		||||
      liters: double.parse(_litersController.text),
 | 
			
		||||
      pricePerLiter: double.parse(_pricePerLiterController.text),
 | 
			
		||||
      totalPrice: double.parse(_totalPriceController.text),
 | 
			
		||||
      mileage: int.parse(_mileageController.text),
 | 
			
		||||
      note: _noteController.text.isEmpty ? null : _noteController.text,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (widget.refuel == null) {
 | 
			
		||||
      await session.addRefuel(refuel);
 | 
			
		||||
    } else if (widget.refuel!.id != null) {
 | 
			
		||||
      await session.updateRefuel(widget.refuel!.id!, refuel);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!mounted) return;
 | 
			
		||||
 | 
			
		||||
    ScaffoldMessenger.of(context).showSnackBar(
 | 
			
		||||
      SnackBar(content: Text('Fuel record saved successfully!')),
 | 
			
		||||
    );
 | 
			
		||||
    _formKey.currentState?.reset();
 | 
			
		||||
    setState(() {
 | 
			
		||||
      _selectedVehicleId = session.defaultVehicle?.id;
 | 
			
		||||
      _selectedFuelType = session.defaultVehicle?.fuelType;
 | 
			
		||||
    });
 | 
			
		||||
    _litersController.clear();
 | 
			
		||||
    _pricePerLiterController.clear();
 | 
			
		||||
    _totalPriceController.clear();
 | 
			
		||||
    _mileageController.clear();
 | 
			
		||||
    _noteController.clear();
 | 
			
		||||
 | 
			
		||||
    if (Navigator.canPop(context)) {
 | 
			
		||||
      Navigator.pop(context);
 | 
			
		||||
    } else {
 | 
			
		||||
      widget.onSaved?.call();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void dispose() {
 | 
			
		||||
    _litersController.dispose();
 | 
			
		||||
    _pricePerLiterController.dispose();
 | 
			
		||||
    _totalPriceController.dispose();
 | 
			
		||||
    _mileageController.dispose();
 | 
			
		||||
    _noteController.dispose();
 | 
			
		||||
    _litersFocus.dispose();
 | 
			
		||||
    _pricePerLiterFocus.dispose();
 | 
			
		||||
    _totalPriceFocus.dispose();
 | 
			
		||||
    super.dispose();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										371
									
								
								lib/screens/add_service_screen.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,371 @@
 | 
			
		||||
import 'dart:convert';
 | 
			
		||||
import 'dart:typed_data';
 | 
			
		||||
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:image/image.dart' as img;
 | 
			
		||||
import 'package:image_picker/image_picker.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
 | 
			
		||||
import '../models/service.dart';
 | 
			
		||||
import '../models/vehicle.dart';
 | 
			
		||||
import '../services/session_manager.dart';
 | 
			
		||||
 | 
			
		||||
enum _ServicePerformer { shop, self }
 | 
			
		||||
 | 
			
		||||
class AddServiceScreen extends StatefulWidget {
 | 
			
		||||
  final ServiceRecord? service;
 | 
			
		||||
  final VoidCallback? onSaved;
 | 
			
		||||
  final bool standalone;
 | 
			
		||||
 | 
			
		||||
  AddServiceScreen({this.service, this.onSaved, this.standalone = false});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  _AddServiceScreenState createState() => _AddServiceScreenState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _AddServiceScreenState extends State<AddServiceScreen> {
 | 
			
		||||
  final _formKey = GlobalKey<FormState>();
 | 
			
		||||
 | 
			
		||||
  String? _selectedVehicleId;
 | 
			
		||||
  ServiceType? _selectedType;
 | 
			
		||||
  final TextEditingController _customTypeController = TextEditingController();
 | 
			
		||||
  final TextEditingController _itemNameController = TextEditingController();
 | 
			
		||||
  final TextEditingController _costController = TextEditingController();
 | 
			
		||||
  final TextEditingController _mileageController = TextEditingController();
 | 
			
		||||
  final TextEditingController _shopController = TextEditingController();
 | 
			
		||||
  _ServicePerformer _performer = _ServicePerformer.shop;
 | 
			
		||||
  final TextEditingController _noteController = TextEditingController();
 | 
			
		||||
  final List<Uint8List> _photos = [];
 | 
			
		||||
  DateTime? _selectedDate;
 | 
			
		||||
  final TextEditingController _dateController = TextEditingController();
 | 
			
		||||
 | 
			
		||||
  bool _initialized = false;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void didChangeDependencies() {
 | 
			
		||||
    super.didChangeDependencies();
 | 
			
		||||
    if (_initialized) return;
 | 
			
		||||
    final session = Provider.of<SessionManager>(context);
 | 
			
		||||
    if (widget.service != null) {
 | 
			
		||||
      final s = widget.service!;
 | 
			
		||||
      _selectedVehicleId = s.vehicleId;
 | 
			
		||||
      _selectedType = s.serviceType;
 | 
			
		||||
      if (s.customType != null) _customTypeController.text = s.customType!;
 | 
			
		||||
      if (s.itemName != null) _itemNameController.text = s.itemName!;
 | 
			
		||||
      _costController.text = s.cost.toString();
 | 
			
		||||
      _mileageController.text = s.mileage.toString();
 | 
			
		||||
      if (s.selfService) {
 | 
			
		||||
        _performer = _ServicePerformer.self;
 | 
			
		||||
      } else {
 | 
			
		||||
        _performer = _ServicePerformer.shop;
 | 
			
		||||
        if (s.shop != null) _shopController.text = s.shop!;
 | 
			
		||||
      }
 | 
			
		||||
      if (s.note != null) _noteController.text = s.note!;
 | 
			
		||||
      for (final p in s.photos) {
 | 
			
		||||
        try {
 | 
			
		||||
          final data = p.contains(',') ? p.split(',').last : p;
 | 
			
		||||
          _photos.add(base64Decode(data));
 | 
			
		||||
        } catch (_) {}
 | 
			
		||||
      }
 | 
			
		||||
      _selectedDate = s.date;
 | 
			
		||||
      if (s.date != null) {
 | 
			
		||||
        _dateController.text = _formatDate(s.date!);
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      _selectedVehicleId = session.defaultVehicle?.id;
 | 
			
		||||
    }
 | 
			
		||||
    _initialized = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  String _formatDate(DateTime date) {
 | 
			
		||||
    final d = date.toLocal();
 | 
			
		||||
    return '${d.year.toString().padLeft(4, '0')}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final session = Provider.of<SessionManager>(context);
 | 
			
		||||
    final vehicles = session.vehicles;
 | 
			
		||||
 | 
			
		||||
    final form = Padding(
 | 
			
		||||
      padding: const EdgeInsets.all(16.0),
 | 
			
		||||
      child: Form(
 | 
			
		||||
        key: _formKey,
 | 
			
		||||
        child: SingleChildScrollView(
 | 
			
		||||
          child: Column(
 | 
			
		||||
            crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
			
		||||
            children: [
 | 
			
		||||
              DropdownButtonFormField<String>(
 | 
			
		||||
                decoration: InputDecoration(labelText: 'Vehicle'),
 | 
			
		||||
                value: _selectedVehicleId,
 | 
			
		||||
                items: vehicles
 | 
			
		||||
                    .map((vehicle) => DropdownMenuItem(
 | 
			
		||||
                          value: vehicle.id,
 | 
			
		||||
                          child: Text(vehicle.name),
 | 
			
		||||
                        ))
 | 
			
		||||
                    .toList(),
 | 
			
		||||
                onChanged: (value) => setState(() => _selectedVehicleId = value),
 | 
			
		||||
                validator: (value) =>
 | 
			
		||||
                    value == null ? 'Please select a vehicle' : null,
 | 
			
		||||
              ),
 | 
			
		||||
              SizedBox(height: 16),
 | 
			
		||||
              DropdownButtonFormField<ServiceType>(
 | 
			
		||||
                decoration: InputDecoration(labelText: 'Service Type'),
 | 
			
		||||
                value: _selectedType,
 | 
			
		||||
                items: ServiceType.values
 | 
			
		||||
                    .map((t) => DropdownMenuItem(
 | 
			
		||||
                          value: t,
 | 
			
		||||
                          child: Text(t.label),
 | 
			
		||||
                        ))
 | 
			
		||||
                    .toList(),
 | 
			
		||||
                onChanged: (value) => setState(() => _selectedType = value),
 | 
			
		||||
                validator: (value) =>
 | 
			
		||||
                    value == null ? 'Please select a service type' : null,
 | 
			
		||||
              ),
 | 
			
		||||
              if (_selectedType == ServiceType.other) ...[
 | 
			
		||||
                SizedBox(height: 16),
 | 
			
		||||
                TextFormField(
 | 
			
		||||
                  controller: _customTypeController,
 | 
			
		||||
                  decoration: InputDecoration(labelText: 'Custom Type'),
 | 
			
		||||
                  validator: (v) =>
 | 
			
		||||
                      v == null || v.isEmpty ? 'Enter custom type' : null,
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
              SizedBox(height: 16),
 | 
			
		||||
              TextFormField(
 | 
			
		||||
                controller: _itemNameController,
 | 
			
		||||
                decoration: InputDecoration(labelText: 'Item Name'),
 | 
			
		||||
              ),
 | 
			
		||||
              SizedBox(height: 16),
 | 
			
		||||
              TextFormField(
 | 
			
		||||
                controller: _costController,
 | 
			
		||||
                decoration: InputDecoration(labelText: 'Cost'),
 | 
			
		||||
                keyboardType: TextInputType.number,
 | 
			
		||||
                validator: _numberValidator,
 | 
			
		||||
              ),
 | 
			
		||||
              SizedBox(height: 16),
 | 
			
		||||
              TextFormField(
 | 
			
		||||
                controller: _mileageController,
 | 
			
		||||
                decoration: InputDecoration(labelText: 'Mileage'),
 | 
			
		||||
                keyboardType: TextInputType.number,
 | 
			
		||||
                validator: _numberValidator,
 | 
			
		||||
              ),
 | 
			
		||||
              SizedBox(height: 16),
 | 
			
		||||
              RadioListTile<_ServicePerformer>(
 | 
			
		||||
                value: _ServicePerformer.shop,
 | 
			
		||||
                groupValue: _performer,
 | 
			
		||||
                title: Text('Performed at shop'),
 | 
			
		||||
                onChanged: (v) => setState(() => _performer = v!),
 | 
			
		||||
              ),
 | 
			
		||||
              RadioListTile<_ServicePerformer>(
 | 
			
		||||
                value: _ServicePerformer.self,
 | 
			
		||||
                groupValue: _performer,
 | 
			
		||||
                title: Text('Self Service'),
 | 
			
		||||
                onChanged: (v) => setState(() {
 | 
			
		||||
                  _performer = v!;
 | 
			
		||||
                  _shopController.clear();
 | 
			
		||||
                }),
 | 
			
		||||
              ),
 | 
			
		||||
              if (_performer == _ServicePerformer.shop) ...[
 | 
			
		||||
                SizedBox(height: 16),
 | 
			
		||||
                TextFormField(
 | 
			
		||||
                  controller: _shopController,
 | 
			
		||||
                  decoration: InputDecoration(labelText: 'Shop'),
 | 
			
		||||
                  validator: (v) =>
 | 
			
		||||
                      v == null || v.isEmpty ? 'Enter shop name' : null,
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
              SizedBox(height: 16),
 | 
			
		||||
              TextFormField(
 | 
			
		||||
                controller: _noteController,
 | 
			
		||||
                decoration: InputDecoration(labelText: 'Note'),
 | 
			
		||||
              ),
 | 
			
		||||
              if (_photos.isNotEmpty) ...[
 | 
			
		||||
                SizedBox(height: 16),
 | 
			
		||||
                Wrap(
 | 
			
		||||
                  spacing: 8,
 | 
			
		||||
                  runSpacing: 8,
 | 
			
		||||
                  children: List.generate(_photos.length, (i) {
 | 
			
		||||
                    return Stack(
 | 
			
		||||
                      children: [
 | 
			
		||||
                        Image.memory(
 | 
			
		||||
                          _photos[i],
 | 
			
		||||
                          width: 80,
 | 
			
		||||
                          height: 80,
 | 
			
		||||
                          fit: BoxFit.cover,
 | 
			
		||||
                        ),
 | 
			
		||||
                        Positioned(
 | 
			
		||||
                          right: 0,
 | 
			
		||||
                          top: 0,
 | 
			
		||||
                          child: GestureDetector(
 | 
			
		||||
                            onTap: () => setState(() => _photos.removeAt(i)),
 | 
			
		||||
                            child: Container(
 | 
			
		||||
                              color: Colors.black54,
 | 
			
		||||
                              child: Icon(Icons.close, color: Colors.white, size: 20),
 | 
			
		||||
                            ),
 | 
			
		||||
                          ),
 | 
			
		||||
                        ),
 | 
			
		||||
                      ],
 | 
			
		||||
                    );
 | 
			
		||||
                  }),
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
              SizedBox(height: 16),
 | 
			
		||||
              TextButton.icon(
 | 
			
		||||
                onPressed: _pickPhoto,
 | 
			
		||||
                icon: Icon(Icons.add_a_photo),
 | 
			
		||||
                label: Text('Add Photo'),
 | 
			
		||||
              ),
 | 
			
		||||
              SizedBox(height: 16),
 | 
			
		||||
              TextFormField(
 | 
			
		||||
                controller: _dateController,
 | 
			
		||||
                decoration: InputDecoration(labelText: 'Date'),
 | 
			
		||||
                readOnly: true,
 | 
			
		||||
                onTap: () async {
 | 
			
		||||
                  final now = DateTime.now();
 | 
			
		||||
                  final picked = await showDatePicker(
 | 
			
		||||
                    context: context,
 | 
			
		||||
                    initialDate: _selectedDate ?? now,
 | 
			
		||||
                    firstDate: DateTime(2000),
 | 
			
		||||
                    lastDate: DateTime(2100),
 | 
			
		||||
                  );
 | 
			
		||||
                  if (picked != null) {
 | 
			
		||||
                    setState(() {
 | 
			
		||||
                      _selectedDate = picked;
 | 
			
		||||
                      _dateController.text = _formatDate(picked);
 | 
			
		||||
                    });
 | 
			
		||||
                  }
 | 
			
		||||
                },
 | 
			
		||||
                validator: (v) => _selectedDate == null ? 'Select a date' : null,
 | 
			
		||||
              ),
 | 
			
		||||
              SizedBox(height: 24),
 | 
			
		||||
              ElevatedButton(
 | 
			
		||||
                onPressed: _submitForm,
 | 
			
		||||
                style: ElevatedButton.styleFrom(backgroundColor: Colors.green),
 | 
			
		||||
                child: Text(widget.service == null
 | 
			
		||||
                    ? 'Create Service Record'
 | 
			
		||||
                    : 'Update Service Record'),
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (widget.standalone) {
 | 
			
		||||
      return Scaffold(
 | 
			
		||||
        appBar: AppBar(
 | 
			
		||||
          title: Text(widget.service == null
 | 
			
		||||
              ? 'Add Service Record'
 | 
			
		||||
              : 'Edit Service Record'),
 | 
			
		||||
        ),
 | 
			
		||||
        body: form,
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    return form;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> _pickPhoto() async {
 | 
			
		||||
    final picker = ImagePicker();
 | 
			
		||||
    final source = await showModalBottomSheet<ImageSource>(
 | 
			
		||||
      context: context,
 | 
			
		||||
      builder: (context) => SafeArea(
 | 
			
		||||
        child: Wrap(
 | 
			
		||||
          children: [
 | 
			
		||||
            ListTile(
 | 
			
		||||
              leading: Icon(Icons.camera_alt),
 | 
			
		||||
              title: Text('Take Photo'),
 | 
			
		||||
              onTap: () => Navigator.pop(context, ImageSource.camera),
 | 
			
		||||
            ),
 | 
			
		||||
            ListTile(
 | 
			
		||||
              leading: Icon(Icons.photo_library),
 | 
			
		||||
              title: Text('Choose from Gallery'),
 | 
			
		||||
              onTap: () => Navigator.pop(context, ImageSource.gallery),
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
    if (source == null) return;
 | 
			
		||||
    final file = await picker.pickImage(source: source);
 | 
			
		||||
    if (file == null) return;
 | 
			
		||||
    var bytes = await file.readAsBytes();
 | 
			
		||||
    final decoded = img.decodeImage(bytes);
 | 
			
		||||
    if (decoded != null) {
 | 
			
		||||
      const maxDim = 800;
 | 
			
		||||
      img.Image resized;
 | 
			
		||||
      if (decoded.width > decoded.height) {
 | 
			
		||||
        resized = img.copyResize(decoded, width: maxDim);
 | 
			
		||||
      } else {
 | 
			
		||||
        resized = img.copyResize(decoded, height: maxDim);
 | 
			
		||||
      }
 | 
			
		||||
      bytes = Uint8List.fromList(img.encodeJpg(resized, quality: 70));
 | 
			
		||||
    }
 | 
			
		||||
    setState(() => _photos.add(bytes));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  String? _numberValidator(String? value) {
 | 
			
		||||
    if (value == null || value.isEmpty) return 'This field cannot be empty';
 | 
			
		||||
    if (double.tryParse(value) == null) return 'Enter a valid number';
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> _submitForm() async {
 | 
			
		||||
    if (!(_formKey.currentState?.validate() ?? false)) return;
 | 
			
		||||
    final session = Provider.of<SessionManager>(context, listen: false);
 | 
			
		||||
    final service = ServiceRecord(
 | 
			
		||||
      id: widget.service?.id,
 | 
			
		||||
      vehicleId: _selectedVehicleId!,
 | 
			
		||||
      serviceType: _selectedType!,
 | 
			
		||||
      customType: _selectedType == ServiceType.other
 | 
			
		||||
          ? _customTypeController.text
 | 
			
		||||
          : null,
 | 
			
		||||
      itemName: _itemNameController.text.isEmpty
 | 
			
		||||
          ? null
 | 
			
		||||
          : _itemNameController.text,
 | 
			
		||||
      cost: double.parse(_costController.text),
 | 
			
		||||
      mileage: int.parse(_mileageController.text),
 | 
			
		||||
      shop: _performer == _ServicePerformer.shop ? _shopController.text : null,
 | 
			
		||||
      selfService: _performer == _ServicePerformer.self,
 | 
			
		||||
      note: _noteController.text.isEmpty ? null : _noteController.text,
 | 
			
		||||
      photos: _photos.map((b) => base64Encode(b)).toList(),
 | 
			
		||||
      date: _selectedDate,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    bool success = false;
 | 
			
		||||
    if (widget.service == null) {
 | 
			
		||||
      success = await session.addService(service);
 | 
			
		||||
    } else if (widget.service!.id != null) {
 | 
			
		||||
      success = await session.updateService(widget.service!.id!, service);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!mounted) return;
 | 
			
		||||
 | 
			
		||||
    ScaffoldMessenger.of(context).showSnackBar(
 | 
			
		||||
      SnackBar(
 | 
			
		||||
          content: Text(success
 | 
			
		||||
              ? 'Service record saved successfully!'
 | 
			
		||||
              : 'Failed to save service record.')),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (success) {
 | 
			
		||||
      if (Navigator.canPop(context)) {
 | 
			
		||||
        Navigator.pop(context);
 | 
			
		||||
      } else {
 | 
			
		||||
        widget.onSaved?.call();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void dispose() {
 | 
			
		||||
    _customTypeController.dispose();
 | 
			
		||||
    _itemNameController.dispose();
 | 
			
		||||
    _costController.dispose();
 | 
			
		||||
    _mileageController.dispose();
 | 
			
		||||
    _shopController.dispose();
 | 
			
		||||
    _noteController.dispose();
 | 
			
		||||
    _dateController.dispose();
 | 
			
		||||
    super.dispose();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										428
									
								
								lib/screens/history_screen.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,428 @@
 | 
			
		||||
import 'dart:ui';
 | 
			
		||||
import 'dart:convert';
 | 
			
		||||
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
 | 
			
		||||
import '../models/refuel.dart';
 | 
			
		||||
import '../models/service.dart';
 | 
			
		||||
import '../models/vehicle.dart';
 | 
			
		||||
import '../services/session_manager.dart';
 | 
			
		||||
import 'add_screen.dart';
 | 
			
		||||
import 'add_service_screen.dart';
 | 
			
		||||
import '../config.dart';
 | 
			
		||||
 | 
			
		||||
class HistoryScreen extends StatefulWidget {
 | 
			
		||||
  @override
 | 
			
		||||
  _HistoryScreenState createState() => _HistoryScreenState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _HistoryScreenState extends State<HistoryScreen> {
 | 
			
		||||
  final TextEditingController _searchController = TextEditingController();
 | 
			
		||||
  String _filter = 'all';
 | 
			
		||||
  String _search = '';
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void dispose() {
 | 
			
		||||
    _searchController.dispose();
 | 
			
		||||
    super.dispose();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  String _formatNumber(num value) {
 | 
			
		||||
    if (value % 1 == 0) {
 | 
			
		||||
      return value.toInt().toString();
 | 
			
		||||
    }
 | 
			
		||||
    return value
 | 
			
		||||
        .toStringAsFixed(2)
 | 
			
		||||
        .replaceFirst(RegExp(r'0+$'), '')
 | 
			
		||||
        .replaceFirst(RegExp(r'[.]$'), '');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  String _formatCurrency(num value) => '${_formatNumber(value)},-';
 | 
			
		||||
 | 
			
		||||
  String _formatDateTime(DateTime? date) {
 | 
			
		||||
    if (date == null) return '';
 | 
			
		||||
    final d = date.toLocal();
 | 
			
		||||
    final day = d.day.toString().padLeft(2, '0');
 | 
			
		||||
    final month = d.month.toString().padLeft(2, '0');
 | 
			
		||||
    final hour = d.hour.toString().padLeft(2, '0');
 | 
			
		||||
    final minute = d.minute.toString().padLeft(2, '0');
 | 
			
		||||
    return '$day.$month.${d.year} $hour:$minute';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  TableRow _detailRow(String label, String value) {
 | 
			
		||||
    return TableRow(
 | 
			
		||||
      children: [
 | 
			
		||||
        Padding(
 | 
			
		||||
          padding: const EdgeInsets.only(top: 2, bottom: 2, right: 16),
 | 
			
		||||
          child: Text(label, style: TextStyle(fontWeight: FontWeight.bold)),
 | 
			
		||||
        ),
 | 
			
		||||
        Padding(
 | 
			
		||||
          padding: const EdgeInsets.symmetric(vertical: 2),
 | 
			
		||||
          child: Text(value),
 | 
			
		||||
        ),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _showFullImage(BuildContext context, String p) {
 | 
			
		||||
    Widget img;
 | 
			
		||||
    if (p.startsWith('http') || p.startsWith('/')) {
 | 
			
		||||
      final url = p.startsWith('http') ? p : '$apiBaseUrl$p';
 | 
			
		||||
      img = Image.network(url);
 | 
			
		||||
    } else {
 | 
			
		||||
      try {
 | 
			
		||||
        img = Image.memory(base64Decode(p));
 | 
			
		||||
      } catch (_) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    Navigator.push(
 | 
			
		||||
      context,
 | 
			
		||||
      MaterialPageRoute(
 | 
			
		||||
        builder: (_) => Scaffold(
 | 
			
		||||
          backgroundColor: Colors.black,
 | 
			
		||||
          body: GestureDetector(
 | 
			
		||||
            onTap: () => Navigator.pop(context),
 | 
			
		||||
            child: Center(child: InteractiveViewer(child: img)),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool _matchesSearch(Refuel? r, ServiceRecord? s) {
 | 
			
		||||
    final q = _search.toLowerCase();
 | 
			
		||||
    if (q.isEmpty) return true;
 | 
			
		||||
    if (r != null) {
 | 
			
		||||
      return (r.note ?? '').toLowerCase().contains(q) ||
 | 
			
		||||
          r.fuelType.label.toLowerCase().contains(q);
 | 
			
		||||
    } else if (s != null) {
 | 
			
		||||
      return s.displayType.toLowerCase().contains(q) ||
 | 
			
		||||
          (s.note ?? '').toLowerCase().contains(q) ||
 | 
			
		||||
          (s.shop ?? '').toLowerCase().contains(q);
 | 
			
		||||
    }
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Consumer<SessionManager>(
 | 
			
		||||
      builder: (context, session, _) {
 | 
			
		||||
        final items = <_HistoryItem>[];
 | 
			
		||||
        for (final r in session.refuels) {
 | 
			
		||||
          items.add(_HistoryItem(date: r.createdAt ?? DateTime.fromMillisecondsSinceEpoch(0), refuel: r));
 | 
			
		||||
        }
 | 
			
		||||
        for (final s in session.services) {
 | 
			
		||||
          items.add(_HistoryItem(date: s.date ?? s.createdAt ?? DateTime.fromMillisecondsSinceEpoch(0), service: s));
 | 
			
		||||
        }
 | 
			
		||||
        items.sort((a, b) => b.date.compareTo(a.date));
 | 
			
		||||
 | 
			
		||||
        final filtered = items.where((i) {
 | 
			
		||||
          if (_filter == 'refuel' && i.refuel == null) return false;
 | 
			
		||||
          if (_filter == 'service' && i.service == null) return false;
 | 
			
		||||
          return _matchesSearch(i.refuel, i.service);
 | 
			
		||||
        }).toList();
 | 
			
		||||
 | 
			
		||||
        return Scaffold(
 | 
			
		||||
          body: Column(
 | 
			
		||||
            children: [
 | 
			
		||||
              Padding(
 | 
			
		||||
                padding: const EdgeInsets.all(8.0),
 | 
			
		||||
                child: TextField(
 | 
			
		||||
                  controller: _searchController,
 | 
			
		||||
                  decoration: InputDecoration(
 | 
			
		||||
                    prefixIcon: Icon(Icons.search),
 | 
			
		||||
                    hintText: 'Search...',
 | 
			
		||||
                  ),
 | 
			
		||||
                  onChanged: (v) => setState(() => _search = v),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
              SingleChildScrollView(
 | 
			
		||||
                scrollDirection: Axis.horizontal,
 | 
			
		||||
                child: Row(
 | 
			
		||||
                  children: [
 | 
			
		||||
                    const SizedBox(width: 8),
 | 
			
		||||
                    ChoiceChip(
 | 
			
		||||
                      label: Text('All'),
 | 
			
		||||
                      selected: _filter == 'all',
 | 
			
		||||
                      onSelected: (_) => setState(() => _filter = 'all'),
 | 
			
		||||
                    ),
 | 
			
		||||
                    const SizedBox(width: 8),
 | 
			
		||||
                    ChoiceChip(
 | 
			
		||||
                      label: Text('Refuels'),
 | 
			
		||||
                      selected: _filter == 'refuel',
 | 
			
		||||
                      onSelected: (_) => setState(() => _filter = 'refuel'),
 | 
			
		||||
                    ),
 | 
			
		||||
                    const SizedBox(width: 8),
 | 
			
		||||
                    ChoiceChip(
 | 
			
		||||
                      label: Text('Services'),
 | 
			
		||||
                      selected: _filter == 'service',
 | 
			
		||||
                      onSelected: (_) => setState(() => _filter = 'service'),
 | 
			
		||||
                    ),
 | 
			
		||||
                    const SizedBox(width: 8),
 | 
			
		||||
                  ],
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
              Expanded(
 | 
			
		||||
                child: filtered.isEmpty
 | 
			
		||||
                    ? Center(child: Text('No history yet'))
 | 
			
		||||
                    : ListView.builder(
 | 
			
		||||
                        itemCount: filtered.length,
 | 
			
		||||
                        itemBuilder: (context, index) {
 | 
			
		||||
                          final item = filtered[index];
 | 
			
		||||
                          if (item.refuel != null) {
 | 
			
		||||
                            final r = item.refuel!;
 | 
			
		||||
                            final textStyle = const TextStyle(
 | 
			
		||||
                                fontFeatures: [FontFeature.tabularFigures()]);
 | 
			
		||||
                            return ListTile(
 | 
			
		||||
                              leading: Icon(Icons.local_gas_station, color: Colors.green),
 | 
			
		||||
                              title: Row(
 | 
			
		||||
                                children: [
 | 
			
		||||
                                  Expanded(
 | 
			
		||||
                                    child: Text(
 | 
			
		||||
                                      '${_formatNumber(r.liters)} L',
 | 
			
		||||
                                      style: textStyle,
 | 
			
		||||
                                    ),
 | 
			
		||||
                                  ),
 | 
			
		||||
                                  Expanded(
 | 
			
		||||
                                    child: Text(
 | 
			
		||||
                                      '${_formatCurrency(r.pricePerLiter)}/L',
 | 
			
		||||
                                      textAlign: TextAlign.center,
 | 
			
		||||
                                      style: textStyle,
 | 
			
		||||
                                    ),
 | 
			
		||||
                                  ),
 | 
			
		||||
                                  Expanded(
 | 
			
		||||
                                    child: Text(
 | 
			
		||||
                                      _formatCurrency(r.totalPrice),
 | 
			
		||||
                                      textAlign: TextAlign.end,
 | 
			
		||||
                                      style: textStyle,
 | 
			
		||||
                                    ),
 | 
			
		||||
                                  ),
 | 
			
		||||
                                ],
 | 
			
		||||
                              ),
 | 
			
		||||
                              subtitle: Text(_formatDateTime(r.createdAt), style: textStyle),
 | 
			
		||||
                              onTap: () {
 | 
			
		||||
                                showDialog(
 | 
			
		||||
                                  context: context,
 | 
			
		||||
                                  builder: (context) => AlertDialog(
 | 
			
		||||
                                    title: Text('Refuel Details'),
 | 
			
		||||
                                    content: Table(
 | 
			
		||||
                                      columnWidths: {0: IntrinsicColumnWidth()},
 | 
			
		||||
                                      children: [
 | 
			
		||||
                                        _detailRow('Total', _formatCurrency(r.totalPrice)),
 | 
			
		||||
                                        _detailRow('Price/L', '${_formatCurrency(r.pricePerLiter)}'),
 | 
			
		||||
                                        _detailRow('Liters', '${_formatNumber(r.liters)} L'),
 | 
			
		||||
                                        _detailRow('Mileage', '${_formatNumber(r.mileage)} km'),
 | 
			
		||||
                                        if (r.note != null && r.note!.isNotEmpty)
 | 
			
		||||
                                          _detailRow('Note', r.note!),
 | 
			
		||||
                                        _detailRow('Date', _formatDateTime(r.createdAt)),
 | 
			
		||||
                                      ],
 | 
			
		||||
                                    ),
 | 
			
		||||
                                    actions: [
 | 
			
		||||
                                      TextButton(
 | 
			
		||||
                                        onPressed: () => Navigator.pop(context),
 | 
			
		||||
                                        child: Text('Close'),
 | 
			
		||||
                                      ),
 | 
			
		||||
                                    ],
 | 
			
		||||
                                  ),
 | 
			
		||||
                                );
 | 
			
		||||
                              },
 | 
			
		||||
                              trailing: Row(
 | 
			
		||||
                                mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                                children: [
 | 
			
		||||
                                  IconButton(
 | 
			
		||||
                                    icon: Icon(Icons.edit, color: Colors.blue),
 | 
			
		||||
                                    onPressed: () {
 | 
			
		||||
                                      Navigator.push(
 | 
			
		||||
                                        context,
 | 
			
		||||
                                        MaterialPageRoute(
 | 
			
		||||
                                          builder: (_) => AddScreen(refuel: r, standalone: true),
 | 
			
		||||
                                        ),
 | 
			
		||||
                                      );
 | 
			
		||||
                                    },
 | 
			
		||||
                                  ),
 | 
			
		||||
                                  if (r.id != null)
 | 
			
		||||
                                    IconButton(
 | 
			
		||||
                                      icon: Icon(Icons.delete, color: Colors.red),
 | 
			
		||||
                                      onPressed: () async {
 | 
			
		||||
                                        final confirm = await showDialog<bool>(
 | 
			
		||||
                                          context: context,
 | 
			
		||||
                                          builder: (context) => AlertDialog(
 | 
			
		||||
                                            title: Text('Confirm Deletion'),
 | 
			
		||||
                                            content: Text('Are you sure you want to delete this record?'),
 | 
			
		||||
                                            actions: [
 | 
			
		||||
                                              TextButton(
 | 
			
		||||
                                                onPressed: () => Navigator.pop(context, false),
 | 
			
		||||
                                                child: Text('Cancel'),
 | 
			
		||||
                                              ),
 | 
			
		||||
                                              TextButton(
 | 
			
		||||
                                                onPressed: () => Navigator.pop(context, true),
 | 
			
		||||
                                                child: Text('Delete'),
 | 
			
		||||
                                              ),
 | 
			
		||||
                                            ],
 | 
			
		||||
                                          ),
 | 
			
		||||
                                        );
 | 
			
		||||
                                        if (confirm == true) {
 | 
			
		||||
                                          await session.removeRefuel(r.id!);
 | 
			
		||||
                                        }
 | 
			
		||||
                                      },
 | 
			
		||||
                                    ),
 | 
			
		||||
                                ],
 | 
			
		||||
                              ),
 | 
			
		||||
                            );
 | 
			
		||||
                          } else {
 | 
			
		||||
                            final s = item.service!;
 | 
			
		||||
                            final textStyle = const TextStyle(
 | 
			
		||||
                                fontFeatures: [FontFeature.tabularFigures()]);
 | 
			
		||||
                            return ListTile(
 | 
			
		||||
                              leading: Icon(Icons.build, color: Colors.orangeAccent),
 | 
			
		||||
                              title: Row(
 | 
			
		||||
                                children: [
 | 
			
		||||
                                  Expanded(
 | 
			
		||||
                                    child: Text(
 | 
			
		||||
                                      s.displayType,
 | 
			
		||||
                                      style: textStyle,
 | 
			
		||||
                                    ),
 | 
			
		||||
                                  ),
 | 
			
		||||
                                  Expanded(
 | 
			
		||||
                                    child: Text(
 | 
			
		||||
                                      _formatCurrency(s.cost),
 | 
			
		||||
                                      textAlign: TextAlign.end,
 | 
			
		||||
                                      style: textStyle,
 | 
			
		||||
                                    ),
 | 
			
		||||
                                  ),
 | 
			
		||||
                                ],
 | 
			
		||||
                              ),
 | 
			
		||||
                              subtitle: Text(_formatDateTime(s.date), style: textStyle),
 | 
			
		||||
                              onTap: () {
 | 
			
		||||
                                showDialog(
 | 
			
		||||
                                  context: context,
 | 
			
		||||
                                  builder: (context) => AlertDialog(
 | 
			
		||||
                                    title: Text('Service Details'),
 | 
			
		||||
                                    content: SingleChildScrollView(
 | 
			
		||||
                                      child: Column(
 | 
			
		||||
                                        mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                                        children: [
 | 
			
		||||
                                          Table(
 | 
			
		||||
                                            columnWidths: {0: IntrinsicColumnWidth()},
 | 
			
		||||
                                            children: [
 | 
			
		||||
                                              _detailRow('Type', s.displayType),
 | 
			
		||||
                                              _detailRow('Cost', _formatCurrency(s.cost)),
 | 
			
		||||
                                              _detailRow('Mileage', '${_formatNumber(s.mileage)} km'),
 | 
			
		||||
                                              if (s.shop != null && s.shop!.isNotEmpty)
 | 
			
		||||
                                                _detailRow('Shop', s.shop!),
 | 
			
		||||
                                              _detailRow('Self service', s.selfService ? 'Yes' : 'No'),
 | 
			
		||||
                                              if (s.note != null && s.note!.isNotEmpty)
 | 
			
		||||
                                                _detailRow('Note', s.note!),
 | 
			
		||||
                                              _detailRow('Date', _formatDateTime(s.date)),
 | 
			
		||||
                                            ],
 | 
			
		||||
                                          ),
 | 
			
		||||
                                          if (s.photos.isNotEmpty) ...[
 | 
			
		||||
                                            SizedBox(height: 12),
 | 
			
		||||
                                            Wrap(
 | 
			
		||||
                                              spacing: 8,
 | 
			
		||||
                                              runSpacing: 8,
 | 
			
		||||
                                              children: s.photos.map((p) {
 | 
			
		||||
                                                Widget img;
 | 
			
		||||
                                                if (p.startsWith('http') || p.startsWith('/')) {
 | 
			
		||||
                                                  final url =
 | 
			
		||||
                                                      p.startsWith('http') ? p : '$apiBaseUrl$p';
 | 
			
		||||
                                                  img = Image.network(
 | 
			
		||||
                                                    url,
 | 
			
		||||
                                                    width: 100,
 | 
			
		||||
                                                    height: 100,
 | 
			
		||||
                                                    fit: BoxFit.cover,
 | 
			
		||||
                                                  );
 | 
			
		||||
                                                } else {
 | 
			
		||||
                                                  try {
 | 
			
		||||
                                                    img = Image.memory(
 | 
			
		||||
                                                      base64Decode(p),
 | 
			
		||||
                                                      width: 100,
 | 
			
		||||
                                                      height: 100,
 | 
			
		||||
                                                      fit: BoxFit.cover,
 | 
			
		||||
                                                    );
 | 
			
		||||
                                                  } catch (_) {
 | 
			
		||||
                                                    img = SizedBox.shrink();
 | 
			
		||||
                                                  }
 | 
			
		||||
                                                }
 | 
			
		||||
                                                return GestureDetector(
 | 
			
		||||
                                                  onTap: () => _showFullImage(context, p),
 | 
			
		||||
                                                  child: img,
 | 
			
		||||
                                                );
 | 
			
		||||
                                              }).toList(),
 | 
			
		||||
                                            ),
 | 
			
		||||
                                          ],
 | 
			
		||||
                                        ],
 | 
			
		||||
                                      ),
 | 
			
		||||
                                    ),
 | 
			
		||||
                                    actions: [
 | 
			
		||||
                                      TextButton(
 | 
			
		||||
                                        onPressed: () => Navigator.pop(context),
 | 
			
		||||
                                        child: Text('Close'),
 | 
			
		||||
                                      ),
 | 
			
		||||
                                    ],
 | 
			
		||||
                                  ),
 | 
			
		||||
                                );
 | 
			
		||||
                              },
 | 
			
		||||
                              trailing: Row(
 | 
			
		||||
                                mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                                children: [
 | 
			
		||||
                                  IconButton(
 | 
			
		||||
                                    icon: Icon(Icons.edit, color: Colors.blue),
 | 
			
		||||
                                    onPressed: () {
 | 
			
		||||
                                      Navigator.push(
 | 
			
		||||
                                        context,
 | 
			
		||||
                                        MaterialPageRoute(
 | 
			
		||||
                                          builder: (_) => AddServiceScreen(service: s, standalone: true),
 | 
			
		||||
                                        ),
 | 
			
		||||
                                      );
 | 
			
		||||
                                    },
 | 
			
		||||
                                  ),
 | 
			
		||||
                                  if (s.id != null)
 | 
			
		||||
                                    IconButton(
 | 
			
		||||
                                      icon: Icon(Icons.delete, color: Colors.red),
 | 
			
		||||
                                      onPressed: () async {
 | 
			
		||||
                                        final confirm = await showDialog<bool>(
 | 
			
		||||
                                          context: context,
 | 
			
		||||
                                          builder: (context) => AlertDialog(
 | 
			
		||||
                                            title: Text('Confirm Deletion'),
 | 
			
		||||
                                            content: Text('Are you sure you want to delete this record?'),
 | 
			
		||||
                                            actions: [
 | 
			
		||||
                                              TextButton(
 | 
			
		||||
                                                onPressed: () => Navigator.pop(context, false),
 | 
			
		||||
                                                child: Text('Cancel'),
 | 
			
		||||
                                              ),
 | 
			
		||||
                                              TextButton(
 | 
			
		||||
                                                onPressed: () => Navigator.pop(context, true),
 | 
			
		||||
                                                child: Text('Delete'),
 | 
			
		||||
                                              ),
 | 
			
		||||
                                            ],
 | 
			
		||||
                                          ),
 | 
			
		||||
                                        );
 | 
			
		||||
                                        if (confirm == true) {
 | 
			
		||||
                                          await session.removeService(s.id!);
 | 
			
		||||
                                        }
 | 
			
		||||
                                      },
 | 
			
		||||
                                    ),
 | 
			
		||||
                                ],
 | 
			
		||||
                              ),
 | 
			
		||||
                            );
 | 
			
		||||
                          }
 | 
			
		||||
                        },
 | 
			
		||||
                      ),
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _HistoryItem {
 | 
			
		||||
  final DateTime date;
 | 
			
		||||
  final Refuel? refuel;
 | 
			
		||||
  final ServiceRecord? service;
 | 
			
		||||
  _HistoryItem({required this.date, this.refuel, this.service});
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										196
									
								
								lib/screens/home_screen.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,196 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
 | 
			
		||||
import '../models/refuel.dart';
 | 
			
		||||
import '../services/session_manager.dart';
 | 
			
		||||
import '../widgets/stat_card.dart';
 | 
			
		||||
import '../widgets/gas_price_chart.dart';
 | 
			
		||||
import '../widgets/consumption_chart.dart';
 | 
			
		||||
 | 
			
		||||
class HomeScreen extends StatelessWidget {
 | 
			
		||||
  const HomeScreen({super.key});
 | 
			
		||||
 | 
			
		||||
  String _formatDouble(double value) {
 | 
			
		||||
    var s = value.toStringAsFixed(2);
 | 
			
		||||
    if (s.endsWith('.00')) {
 | 
			
		||||
      s = s.substring(0, s.length - 3);
 | 
			
		||||
    } else if (s.endsWith('0')) {
 | 
			
		||||
      s = s.substring(0, s.length - 1);
 | 
			
		||||
    }
 | 
			
		||||
    return s;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  double? _allTimeConsumption(List<Refuel> refuels) {
 | 
			
		||||
    if (refuels.length < 2) return null;
 | 
			
		||||
    final distance = refuels.last.mileage - refuels.first.mileage;
 | 
			
		||||
    if (distance <= 0) return null;
 | 
			
		||||
    final liters =
 | 
			
		||||
        refuels.skip(1).fold<double>(0.0, (sum, r) => sum + r.liters);
 | 
			
		||||
    return liters / distance * 100;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  double? _lastConsumption(List<Refuel> refuels) {
 | 
			
		||||
    if (refuels.length < 2) return null;
 | 
			
		||||
    final last = refuels[refuels.length - 1];
 | 
			
		||||
    final prev = refuels[refuels.length - 2];
 | 
			
		||||
    final distance = last.mileage - prev.mileage;
 | 
			
		||||
    if (distance <= 0) return null;
 | 
			
		||||
    return last.liters / distance * 100;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  int? _kmSinceLast(List<Refuel> refuels) {
 | 
			
		||||
    if (refuels.length < 2) return null;
 | 
			
		||||
    return refuels.last.mileage - refuels[refuels.length - 2].mileage;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  int _kmForPeriod(List<Refuel> refuels, DateTime from) {
 | 
			
		||||
    final list = refuels
 | 
			
		||||
        .where((r) => r.createdAt != null && r.createdAt!.isAfter(from))
 | 
			
		||||
        .toList();
 | 
			
		||||
    if (list.length < 2) return 0;
 | 
			
		||||
    return list.last.mileage - list.first.mileage;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  int _kmAllTime(List<Refuel> refuels) {
 | 
			
		||||
    if (refuels.length < 2) return 0;
 | 
			
		||||
    return refuels.last.mileage - refuels.first.mileage;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Consumer<SessionManager>(
 | 
			
		||||
      builder: (context, session, _) {
 | 
			
		||||
        final vehicle = session.defaultVehicle;
 | 
			
		||||
        if (vehicle == null) {
 | 
			
		||||
          return const Scaffold(
 | 
			
		||||
            body: Center(child: Text('No default vehicle selected')),
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        final refuels = session.refuels
 | 
			
		||||
            .where((r) => r.vehicleId == vehicle.id)
 | 
			
		||||
            .toList();
 | 
			
		||||
        refuels.sort((a, b) => a.mileage.compareTo(b.mileage));
 | 
			
		||||
 | 
			
		||||
        final allCons = _allTimeConsumption(refuels);
 | 
			
		||||
        final lastCons = _lastConsumption(refuels);
 | 
			
		||||
        final kmLast = _kmSinceLast(refuels);
 | 
			
		||||
        final now = DateTime.now();
 | 
			
		||||
        final km1m = _kmForPeriod(refuels, now.subtract(const Duration(days: 30)));
 | 
			
		||||
        final km6m =
 | 
			
		||||
            _kmForPeriod(refuels, now.subtract(const Duration(days: 182)));
 | 
			
		||||
        final km1y =
 | 
			
		||||
            _kmForPeriod(refuels, now.subtract(const Duration(days: 365)));
 | 
			
		||||
        final kmAll = _kmAllTime(refuels);
 | 
			
		||||
 | 
			
		||||
        final lastRefuels =
 | 
			
		||||
            refuels.length > 14 ? refuels.sublist(refuels.length - 14) : refuels;
 | 
			
		||||
 | 
			
		||||
        return Scaffold(
 | 
			
		||||
          body: SafeArea(
 | 
			
		||||
            child: SingleChildScrollView(
 | 
			
		||||
              child: Padding(
 | 
			
		||||
                padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
 | 
			
		||||
                child: Column(
 | 
			
		||||
                  crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
                  children: [
 | 
			
		||||
                  Card(
 | 
			
		||||
                    color: Theme.of(context).colorScheme.secondaryContainer,
 | 
			
		||||
                    child: ListTile(
 | 
			
		||||
                      leading: const Icon(Icons.directions_car),
 | 
			
		||||
                      title: Text(vehicle.name,
 | 
			
		||||
                          style: const TextStyle(fontWeight: FontWeight.bold)),
 | 
			
		||||
                      subtitle: Text(vehicle.registrationPlate),
 | 
			
		||||
                      trailing:
 | 
			
		||||
                          const Icon(Icons.star, color: Colors.amber),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                  const SizedBox(height: 16),
 | 
			
		||||
                  const Text('Refuel stats',
 | 
			
		||||
                      style:
 | 
			
		||||
                          TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
 | 
			
		||||
                  const SizedBox(height: 8),
 | 
			
		||||
                  GridView.count(
 | 
			
		||||
                    shrinkWrap: true,
 | 
			
		||||
                    physics: const NeverScrollableScrollPhysics(),
 | 
			
		||||
                    crossAxisCount: 2,
 | 
			
		||||
                    crossAxisSpacing: 8,
 | 
			
		||||
                    mainAxisSpacing: 8,
 | 
			
		||||
                    childAspectRatio: 2,
 | 
			
		||||
                    children: [
 | 
			
		||||
                      StatCard(
 | 
			
		||||
                        title: 'Avg consumption (all time)',
 | 
			
		||||
                        value: allCons != null
 | 
			
		||||
                            ? '${_formatDouble(allCons)} L/100km'
 | 
			
		||||
                            : '-',
 | 
			
		||||
                      ),
 | 
			
		||||
                      StatCard(
 | 
			
		||||
                        title: 'Since last refuel',
 | 
			
		||||
                        value: lastCons != null
 | 
			
		||||
                            ? '${_formatDouble(lastCons)} L/100km'
 | 
			
		||||
                            : '-',
 | 
			
		||||
                      ),
 | 
			
		||||
                    ],
 | 
			
		||||
                  ),
 | 
			
		||||
                  const SizedBox(height: 24),
 | 
			
		||||
                  const Text('Kilometers driven',
 | 
			
		||||
                      style:
 | 
			
		||||
                          TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
 | 
			
		||||
                  Builder(builder: (context) {
 | 
			
		||||
                    final kmCards = <Widget>[
 | 
			
		||||
                      StatCard(
 | 
			
		||||
                        title: 'Since last refuel',
 | 
			
		||||
                        value: kmLast != null ? '$kmLast km' : '-',
 | 
			
		||||
                      ),
 | 
			
		||||
                      StatCard(title: 'Past month', value: '$km1m km'),
 | 
			
		||||
                      StatCard(title: 'Past 6 months', value: '$km6m km'),
 | 
			
		||||
                      StatCard(title: 'Past year', value: '$km1y km'),
 | 
			
		||||
                      StatCard(title: 'All time', value: '$kmAll km'),
 | 
			
		||||
                    ];
 | 
			
		||||
                    if (kmCards.length.isOdd) {
 | 
			
		||||
                      kmCards.add(const SizedBox.shrink());
 | 
			
		||||
                    }
 | 
			
		||||
                    return GridView.count(
 | 
			
		||||
                      shrinkWrap: true,
 | 
			
		||||
                      physics: const NeverScrollableScrollPhysics(),
 | 
			
		||||
                      crossAxisCount: 2,
 | 
			
		||||
                      crossAxisSpacing: 8,
 | 
			
		||||
                      mainAxisSpacing: 8,
 | 
			
		||||
                      childAspectRatio: 2,
 | 
			
		||||
                      children: kmCards,
 | 
			
		||||
                    );
 | 
			
		||||
                  }),
 | 
			
		||||
                  if (lastRefuels.isNotEmpty) ...[
 | 
			
		||||
                    const SizedBox(height: 24),
 | 
			
		||||
                    Text('Gas price (last ${lastRefuels.length} refuels)',
 | 
			
		||||
                        style: const TextStyle(
 | 
			
		||||
                            fontSize: 18, fontWeight: FontWeight.bold)),
 | 
			
		||||
                    const SizedBox(height: 8),
 | 
			
		||||
                    SizedBox(
 | 
			
		||||
                      height: 220,
 | 
			
		||||
                      child: GasPriceChart(refuels: lastRefuels),
 | 
			
		||||
                    ),
 | 
			
		||||
                    if (lastRefuels.length > 1) ...[
 | 
			
		||||
                      const SizedBox(height: 24),
 | 
			
		||||
                      const Text('Consumption trend',
 | 
			
		||||
                          style: TextStyle(
 | 
			
		||||
                              fontSize: 18, fontWeight: FontWeight.bold)),
 | 
			
		||||
                      const SizedBox(height: 8),
 | 
			
		||||
                      SizedBox(
 | 
			
		||||
                        height: 220,
 | 
			
		||||
                        child: ConsumptionChart(refuels: lastRefuels),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ],
 | 
			
		||||
                  ],
 | 
			
		||||
                  const SizedBox(height: 8),
 | 
			
		||||
                ],
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										152
									
								
								lib/screens/login.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,152 @@
 | 
			
		||||
import 'dart:convert';
 | 
			
		||||
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:http/http.dart' as http;
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
 | 
			
		||||
import '../config.dart';
 | 
			
		||||
import '../services/session_manager.dart';
 | 
			
		||||
 | 
			
		||||
class LoginScreen extends StatefulWidget {
 | 
			
		||||
  final VoidCallback onSwitchToSignup;
 | 
			
		||||
  final VoidCallback onLoginSuccess;
 | 
			
		||||
 | 
			
		||||
  const LoginScreen({
 | 
			
		||||
    required this.onSwitchToSignup,
 | 
			
		||||
    required this.onLoginSuccess,
 | 
			
		||||
    super.key,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<LoginScreen> createState() => _LoginScreenState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _LoginScreenState extends State<LoginScreen> {
 | 
			
		||||
  final _formKey = GlobalKey<FormState>();
 | 
			
		||||
  final _emailController = TextEditingController();
 | 
			
		||||
  final _passwordController = TextEditingController();
 | 
			
		||||
 | 
			
		||||
  Future<void> _login() async {
 | 
			
		||||
    if (_formKey.currentState!.validate()) {
 | 
			
		||||
      final email = _emailController.text;
 | 
			
		||||
      final password = _passwordController.text;
 | 
			
		||||
 | 
			
		||||
      try {
 | 
			
		||||
        final response = await http.post(
 | 
			
		||||
          Uri.parse('$apiBaseUrl/api/v1/auth/signin'),
 | 
			
		||||
          headers: {'Content-Type': 'application/json'},
 | 
			
		||||
          body: jsonEncode({'email': email, 'password': password}),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if (response.statusCode == 200) {
 | 
			
		||||
          final data = jsonDecode(response.body);
 | 
			
		||||
          final token = data['token'];
 | 
			
		||||
          final name = data['user']?['username'] ?? data['username'];
 | 
			
		||||
 | 
			
		||||
          await Provider.of<SessionManager>(context, listen: false).login(
 | 
			
		||||
            token: token,
 | 
			
		||||
            email: email,
 | 
			
		||||
            name: name,
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          widget.onLoginSuccess();
 | 
			
		||||
        } else {
 | 
			
		||||
          final data = jsonDecode(response.body);
 | 
			
		||||
          ScaffoldMessenger.of(context).showSnackBar(
 | 
			
		||||
            SnackBar(content: Text(data['message'] ?? 'Login failed')),
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        ScaffoldMessenger.of(context).showSnackBar(
 | 
			
		||||
          SnackBar(content: Text('Login error: $e')),
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      body: Padding(
 | 
			
		||||
        padding: const EdgeInsets.all(24.0),
 | 
			
		||||
        child: Form(
 | 
			
		||||
          key: _formKey,
 | 
			
		||||
          child: SingleChildScrollView(
 | 
			
		||||
            child: Column(
 | 
			
		||||
              children: [
 | 
			
		||||
                const SizedBox(height: 40),
 | 
			
		||||
                ClipRRect(
 | 
			
		||||
                  borderRadius: BorderRadius.circular(20),
 | 
			
		||||
                  child: Image.asset(
 | 
			
		||||
                    'assets/icon/app_icon.png',
 | 
			
		||||
                    width: 100,
 | 
			
		||||
                    height: 100,
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
                const SizedBox(height: 16),
 | 
			
		||||
                Text(
 | 
			
		||||
                  'Log in to Fuel Stats',
 | 
			
		||||
                  style: TextStyle(
 | 
			
		||||
                    fontSize: 24,
 | 
			
		||||
                    fontWeight: FontWeight.bold,
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
                const SizedBox(height: 32),
 | 
			
		||||
                  TextFormField(
 | 
			
		||||
                    controller: _emailController,
 | 
			
		||||
                    decoration: InputDecoration(
 | 
			
		||||
                      labelText: 'Email',
 | 
			
		||||
                      prefixIcon: Icon(Icons.email),
 | 
			
		||||
                      border: OutlineInputBorder(),
 | 
			
		||||
                    ),
 | 
			
		||||
                    keyboardType: TextInputType.emailAddress,
 | 
			
		||||
                    validator: (value) {
 | 
			
		||||
                      if (value == null || value.isEmpty)
 | 
			
		||||
                        return 'Please enter your email';
 | 
			
		||||
                      if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value))
 | 
			
		||||
                        return 'Enter a valid email';
 | 
			
		||||
                      return null;
 | 
			
		||||
                    },
 | 
			
		||||
                  ),
 | 
			
		||||
                  const SizedBox(height: 16),
 | 
			
		||||
                  TextFormField(
 | 
			
		||||
                    controller: _passwordController,
 | 
			
		||||
                    decoration: InputDecoration(
 | 
			
		||||
                      labelText: 'Password',
 | 
			
		||||
                      prefixIcon: Icon(Icons.lock),
 | 
			
		||||
                      border: OutlineInputBorder(),
 | 
			
		||||
                    ),
 | 
			
		||||
                    obscureText: true,
 | 
			
		||||
                    validator: (value) {
 | 
			
		||||
                      if (value == null || value.isEmpty)
 | 
			
		||||
                        return 'Please enter your password';
 | 
			
		||||
                      if (value.length < 6)
 | 
			
		||||
                        return 'Password must be at least 6 characters';
 | 
			
		||||
                      return null;
 | 
			
		||||
                    },
 | 
			
		||||
                  ),
 | 
			
		||||
                  const SizedBox(height: 24),
 | 
			
		||||
                  ElevatedButton.icon(
 | 
			
		||||
                    onPressed: _login,
 | 
			
		||||
                    icon: Icon(Icons.login),
 | 
			
		||||
                    label: Text('Log In'),
 | 
			
		||||
                    style: ElevatedButton.styleFrom(
 | 
			
		||||
                      backgroundColor: Colors.green,
 | 
			
		||||
                      foregroundColor: Colors.white,
 | 
			
		||||
                      padding: EdgeInsets.symmetric(horizontal: 32, vertical: 12),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                  const SizedBox(height: 12),
 | 
			
		||||
                  TextButton(
 | 
			
		||||
                    onPressed: widget.onSwitchToSignup,
 | 
			
		||||
                    child: Text("Don't have an account? Sign up"),
 | 
			
		||||
                  ),
 | 
			
		||||
                ],
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										186
									
								
								lib/screens/signup.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,186 @@
 | 
			
		||||
import 'dart:convert';
 | 
			
		||||
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:http/http.dart' as http;
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
 | 
			
		||||
import '../config.dart';
 | 
			
		||||
import '../services/session_manager.dart';
 | 
			
		||||
 | 
			
		||||
class SignupScreen extends StatefulWidget {
 | 
			
		||||
  final VoidCallback onSwitchToLogin;
 | 
			
		||||
  final VoidCallback onSignupSuccess;
 | 
			
		||||
 | 
			
		||||
  const SignupScreen({
 | 
			
		||||
    required this.onSwitchToLogin,
 | 
			
		||||
    required this.onSignupSuccess,
 | 
			
		||||
    super.key,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<SignupScreen> createState() => _SignupScreenState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _SignupScreenState extends State<SignupScreen> {
 | 
			
		||||
  final _formKey = GlobalKey<FormState>();
 | 
			
		||||
  final _usernameController = TextEditingController();
 | 
			
		||||
  final _emailController = TextEditingController();
 | 
			
		||||
  final _passwordController = TextEditingController();
 | 
			
		||||
 | 
			
		||||
  Future<void> _signup() async {
 | 
			
		||||
    if (_formKey.currentState!.validate()) {
 | 
			
		||||
      final username = _usernameController.text;
 | 
			
		||||
      final email = _emailController.text;
 | 
			
		||||
      final password = _passwordController.text;
 | 
			
		||||
 | 
			
		||||
      try {
 | 
			
		||||
        final signupResponse = await http.post(
 | 
			
		||||
          Uri.parse('$apiBaseUrl/api/v1/auth/signup'),
 | 
			
		||||
          headers: {'Content-Type': 'application/json'},
 | 
			
		||||
          body: jsonEncode({
 | 
			
		||||
            'username': username,
 | 
			
		||||
            'email': email,
 | 
			
		||||
            'password': password,
 | 
			
		||||
          }),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if (signupResponse.statusCode == 200 || signupResponse.statusCode == 201) {
 | 
			
		||||
          final signinResponse = await http.post(
 | 
			
		||||
            Uri.parse('$apiBaseUrl/api/v1/auth/signin'),
 | 
			
		||||
            headers: {'Content-Type': 'application/json'},
 | 
			
		||||
            body: jsonEncode({'email': email, 'password': password}),
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          if (signinResponse.statusCode == 200) {
 | 
			
		||||
            final data = jsonDecode(signinResponse.body);
 | 
			
		||||
            final token = data['token'];
 | 
			
		||||
            final name = data['user']?['username'] ?? data['username'] ?? username;
 | 
			
		||||
 | 
			
		||||
            await Provider.of<SessionManager>(context, listen: false).login(
 | 
			
		||||
              token: token,
 | 
			
		||||
              email: email,
 | 
			
		||||
              name: name,
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            widget.onSignupSuccess();
 | 
			
		||||
          } else {
 | 
			
		||||
            final data = jsonDecode(signinResponse.body);
 | 
			
		||||
            ScaffoldMessenger.of(context).showSnackBar(
 | 
			
		||||
              SnackBar(content: Text(data['message'] ?? 'Sign in failed')),
 | 
			
		||||
            );
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          final data = jsonDecode(signupResponse.body);
 | 
			
		||||
          ScaffoldMessenger.of(context).showSnackBar(
 | 
			
		||||
            SnackBar(content: Text(data['message'] ?? 'Signup failed')),
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        ScaffoldMessenger.of(context).showSnackBar(
 | 
			
		||||
          SnackBar(content: Text('Signup error: $e')),
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      //appBar: AppBar(title: Text('Sign Up')),
 | 
			
		||||
      body: Padding(
 | 
			
		||||
        padding: const EdgeInsets.all(24.0),
 | 
			
		||||
        child: Form(
 | 
			
		||||
          key: _formKey,
 | 
			
		||||
          child: SingleChildScrollView(
 | 
			
		||||
            child: Column(
 | 
			
		||||
              children: [
 | 
			
		||||
                const SizedBox(height: 40),
 | 
			
		||||
                ClipRRect(
 | 
			
		||||
                  borderRadius: BorderRadius.circular(20),
 | 
			
		||||
                  child: Image.asset(
 | 
			
		||||
                    'assets/icon/app_icon.png',
 | 
			
		||||
                    width: 100,
 | 
			
		||||
                    height: 100,
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
                const SizedBox(height: 16),
 | 
			
		||||
                Text(
 | 
			
		||||
                  'Create your Fuel Stats account',
 | 
			
		||||
                  style: TextStyle(
 | 
			
		||||
                    fontSize: 24,
 | 
			
		||||
                    fontWeight: FontWeight.bold,
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
                const SizedBox(height: 32),
 | 
			
		||||
                  TextFormField(
 | 
			
		||||
                    controller: _usernameController,
 | 
			
		||||
                    decoration: InputDecoration(
 | 
			
		||||
                      labelText: 'Username',
 | 
			
		||||
                      prefixIcon: Icon(Icons.person),
 | 
			
		||||
                      border: OutlineInputBorder(),
 | 
			
		||||
                    ),
 | 
			
		||||
                    validator: (value) {
 | 
			
		||||
                      if (value == null || value.isEmpty)
 | 
			
		||||
                        return 'Please enter a username';
 | 
			
		||||
                      return null;
 | 
			
		||||
                    },
 | 
			
		||||
                  ),
 | 
			
		||||
                  const SizedBox(height: 16),
 | 
			
		||||
                  TextFormField(
 | 
			
		||||
                    controller: _emailController,
 | 
			
		||||
                    decoration: InputDecoration(
 | 
			
		||||
                      labelText: 'Email',
 | 
			
		||||
                      prefixIcon: Icon(Icons.email),
 | 
			
		||||
                      border: OutlineInputBorder(),
 | 
			
		||||
                    ),
 | 
			
		||||
                    keyboardType: TextInputType.emailAddress,
 | 
			
		||||
                    validator: (value) {
 | 
			
		||||
                      if (value == null || value.isEmpty)
 | 
			
		||||
                        return 'Please enter an email';
 | 
			
		||||
                      if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value))
 | 
			
		||||
                        return 'Enter a valid email';
 | 
			
		||||
                      return null;
 | 
			
		||||
                    },
 | 
			
		||||
                  ),
 | 
			
		||||
                  const SizedBox(height: 16),
 | 
			
		||||
                  TextFormField(
 | 
			
		||||
                    controller: _passwordController,
 | 
			
		||||
                    decoration: InputDecoration(
 | 
			
		||||
                      labelText: 'Password',
 | 
			
		||||
                      prefixIcon: Icon(Icons.lock),
 | 
			
		||||
                      border: OutlineInputBorder(),
 | 
			
		||||
                    ),
 | 
			
		||||
                    obscureText: true,
 | 
			
		||||
                    validator: (value) {
 | 
			
		||||
                      if (value == null || value.isEmpty)
 | 
			
		||||
                        return 'Please enter a password';
 | 
			
		||||
                      if (value.length < 6)
 | 
			
		||||
                        return 'Password must be at least 6 characters';
 | 
			
		||||
                      return null;
 | 
			
		||||
                    },
 | 
			
		||||
                  ),
 | 
			
		||||
                  const SizedBox(height: 24),
 | 
			
		||||
                  ElevatedButton.icon(
 | 
			
		||||
                    onPressed: _signup,
 | 
			
		||||
                    icon: Icon(Icons.person_add),
 | 
			
		||||
                    label: Text('Sign Up'),
 | 
			
		||||
                    style: ElevatedButton.styleFrom(
 | 
			
		||||
                      backgroundColor: Colors.blue,
 | 
			
		||||
                      foregroundColor: Colors.white,
 | 
			
		||||
                      padding: EdgeInsets.symmetric(horizontal: 32, vertical: 12),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                  const SizedBox(height: 12),
 | 
			
		||||
                  TextButton(
 | 
			
		||||
                    onPressed: widget.onSwitchToLogin,
 | 
			
		||||
                    child: Text("Already have an account? Log in"),
 | 
			
		||||
                  ),
 | 
			
		||||
                ],
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										71
									
								
								lib/screens/user_settings.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,71 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:package_info_plus/package_info_plus.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import '../services/session_manager.dart';
 | 
			
		||||
 | 
			
		||||
class UserSettingsScreen extends StatelessWidget {
 | 
			
		||||
  final VoidCallback onLogout;
 | 
			
		||||
 | 
			
		||||
  const UserSettingsScreen({required this.onLogout, super.key});
 | 
			
		||||
 | 
			
		||||
  Future<String> _getVersion() async {
 | 
			
		||||
    final info = await PackageInfo.fromPlatform();
 | 
			
		||||
    return 'Version: ${info.version}+${info.buildNumber}';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final session = Provider.of<SessionManager>(context);
 | 
			
		||||
    final userName = session.name ?? "Unknown User"; // fallback just in case
 | 
			
		||||
    final userEmail = session.email ?? '';
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      appBar: AppBar(title: Text('User settings')),
 | 
			
		||||
      body: Center(
 | 
			
		||||
        child: Column(
 | 
			
		||||
          mainAxisSize: MainAxisSize.min,
 | 
			
		||||
          children: [
 | 
			
		||||
            Text(
 | 
			
		||||
              userName,
 | 
			
		||||
              style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
 | 
			
		||||
            ),
 | 
			
		||||
            SizedBox(height: 8),
 | 
			
		||||
            if (userEmail.isNotEmpty)
 | 
			
		||||
              Text(
 | 
			
		||||
                userEmail,
 | 
			
		||||
                style: TextStyle(fontSize: 16),
 | 
			
		||||
              ),
 | 
			
		||||
            SizedBox(height: 20),
 | 
			
		||||
            ElevatedButton.icon(
 | 
			
		||||
              onPressed: () async {
 | 
			
		||||
                await session.logout();
 | 
			
		||||
                onLogout();
 | 
			
		||||
              },
 | 
			
		||||
              icon: Icon(Icons.logout),
 | 
			
		||||
              label: Text("Sign Out"),
 | 
			
		||||
              style: ElevatedButton.styleFrom(
 | 
			
		||||
                backgroundColor: Colors.redAccent,
 | 
			
		||||
                foregroundColor: Colors.white,
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
            SizedBox(height: 8),
 | 
			
		||||
            FutureBuilder<String>(
 | 
			
		||||
              future: _getVersion(),
 | 
			
		||||
              builder: (context, snapshot) {
 | 
			
		||||
                if (snapshot.connectionState == ConnectionState.done) {
 | 
			
		||||
                  return Text(
 | 
			
		||||
                    snapshot.data ?? '',
 | 
			
		||||
                    style: TextStyle(color: Colors.grey),
 | 
			
		||||
                  );
 | 
			
		||||
                } else {
 | 
			
		||||
                  return SizedBox.shrink();
 | 
			
		||||
                }
 | 
			
		||||
              },
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										201
									
								
								lib/screens/vehicles_screen.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,201 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
 | 
			
		||||
import '../models/vehicle.dart';
 | 
			
		||||
import '../services/session_manager.dart';
 | 
			
		||||
 | 
			
		||||
class VehiclesScreen extends StatelessWidget {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final session = Provider.of<SessionManager>(context);
 | 
			
		||||
    final vehicles = session.vehicles;
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      body: vehicles.isEmpty
 | 
			
		||||
          ? Center(child: Text('No vehicles added yet.'))
 | 
			
		||||
          : ListView.builder(
 | 
			
		||||
              itemCount: vehicles.length,
 | 
			
		||||
              itemBuilder: (context, index) {
 | 
			
		||||
                final vehicle = vehicles[index];
 | 
			
		||||
                final isDefault = vehicle.isDefault;
 | 
			
		||||
                return ListTile(
 | 
			
		||||
                  tileColor: isDefault
 | 
			
		||||
                      ? Theme.of(context).colorScheme.secondaryContainer
 | 
			
		||||
                      : null,
 | 
			
		||||
                  leading: IconButton(
 | 
			
		||||
                    icon: Icon(Icons.star,
 | 
			
		||||
                        color: isDefault ? Colors.amber : Colors.grey),
 | 
			
		||||
                    tooltip: isDefault ? 'Unset default' : 'Set as default',
 | 
			
		||||
                    onPressed: () => session.setDefaultVehicle(
 | 
			
		||||
                        isDefault ? null : vehicle.id),
 | 
			
		||||
                  ),
 | 
			
		||||
                  title: Text(vehicle.name),
 | 
			
		||||
                  subtitle: Text(
 | 
			
		||||
                    '${vehicle.registrationPlate} • ${vehicle.fuelType.label}'
 | 
			
		||||
                    '${vehicle.note != null ? ' • ${vehicle.note}' : ''}',
 | 
			
		||||
                  ),
 | 
			
		||||
                  trailing: Row(
 | 
			
		||||
                    mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                    children: [
 | 
			
		||||
                      IconButton(
 | 
			
		||||
                        icon: Icon(Icons.edit, color: Colors.blue),
 | 
			
		||||
                        onPressed: () => _editVehicle(context, session, index, vehicle),
 | 
			
		||||
                      ),
 | 
			
		||||
                      IconButton(
 | 
			
		||||
                        icon: Icon(Icons.delete, color: Colors.red),
 | 
			
		||||
                        onPressed: () => _removeVehicle(context, session, index),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ],
 | 
			
		||||
                  ),
 | 
			
		||||
                );
 | 
			
		||||
              },
 | 
			
		||||
            ),
 | 
			
		||||
      floatingActionButton: FloatingActionButton(
 | 
			
		||||
        onPressed: () => _addVehicle(context, session),
 | 
			
		||||
        child: Icon(Icons.add),
 | 
			
		||||
        tooltip: 'Add Vehicle',
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _addVehicle(BuildContext context, SessionManager session) async {
 | 
			
		||||
    final newVehicle = await showDialog<Vehicle>(
 | 
			
		||||
      context: context,
 | 
			
		||||
      builder: (context) => _VehicleDialog(),
 | 
			
		||||
    );
 | 
			
		||||
    if (newVehicle != null) {
 | 
			
		||||
      await session.addVehicle(newVehicle);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _editVehicle(BuildContext context, SessionManager session, int index,
 | 
			
		||||
      Vehicle vehicle) async {
 | 
			
		||||
    final updatedVehicle = await showDialog<Vehicle>(
 | 
			
		||||
      context: context,
 | 
			
		||||
      builder: (context) => _VehicleDialog(vehicle: vehicle),
 | 
			
		||||
    );
 | 
			
		||||
    if (updatedVehicle != null) {
 | 
			
		||||
      await session.updateVehicle(index, updatedVehicle);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _removeVehicle(
 | 
			
		||||
      BuildContext context, SessionManager session, int index) async {
 | 
			
		||||
    final confirm = await showDialog<bool>(
 | 
			
		||||
      context: context,
 | 
			
		||||
      builder: (context) => AlertDialog(
 | 
			
		||||
        title: Text('Confirm Removal'),
 | 
			
		||||
        content: Text(
 | 
			
		||||
            'Are you sure you want to delete this vehicle? This action cannot be undone.'),
 | 
			
		||||
        actions: [
 | 
			
		||||
          TextButton(
 | 
			
		||||
            onPressed: () => Navigator.pop(context, false),
 | 
			
		||||
            child: Text('Cancel'),
 | 
			
		||||
          ),
 | 
			
		||||
          TextButton(
 | 
			
		||||
            onPressed: () => Navigator.pop(context, true),
 | 
			
		||||
            child: Text('Delete'),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (confirm == true) {
 | 
			
		||||
      await session.removeVehicle(index);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _VehicleDialog extends StatefulWidget {
 | 
			
		||||
  final Vehicle? vehicle;
 | 
			
		||||
  const _VehicleDialog({this.vehicle});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  _VehicleDialogState createState() => _VehicleDialogState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _VehicleDialogState extends State<_VehicleDialog> {
 | 
			
		||||
  final _formKey = GlobalKey<FormState>();
 | 
			
		||||
  late TextEditingController _nameController;
 | 
			
		||||
  late TextEditingController _plateController;
 | 
			
		||||
  late TextEditingController _noteController;
 | 
			
		||||
  FuelType? _selectedFuelType;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    _nameController = TextEditingController(text: widget.vehicle?.name ?? '');
 | 
			
		||||
    _plateController =
 | 
			
		||||
        TextEditingController(text: widget.vehicle?.registrationPlate ?? '');
 | 
			
		||||
    _noteController = TextEditingController(text: widget.vehicle?.note ?? '');
 | 
			
		||||
    _selectedFuelType = widget.vehicle?.fuelType;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return AlertDialog(
 | 
			
		||||
      title: Text(widget.vehicle == null ? 'Add New Vehicle' : 'Edit Vehicle'),
 | 
			
		||||
      content: SingleChildScrollView(
 | 
			
		||||
        child: Form(
 | 
			
		||||
          key: _formKey,
 | 
			
		||||
          child: Column(
 | 
			
		||||
            mainAxisSize: MainAxisSize.min,
 | 
			
		||||
            children: [
 | 
			
		||||
              TextFormField(
 | 
			
		||||
                controller: _nameController,
 | 
			
		||||
                decoration: InputDecoration(labelText: 'Vehicle Name'),
 | 
			
		||||
                validator: (value) =>
 | 
			
		||||
                    value == null || value.isEmpty ? 'Enter a vehicle name' : null,
 | 
			
		||||
              ),
 | 
			
		||||
              TextFormField(
 | 
			
		||||
                controller: _plateController,
 | 
			
		||||
                decoration: InputDecoration(labelText: 'Registration Plate'),
 | 
			
		||||
                validator: (value) =>
 | 
			
		||||
                    value == null || value.isEmpty ? 'Enter a plate number' : null,
 | 
			
		||||
              ),
 | 
			
		||||
              DropdownButtonFormField<FuelType>(
 | 
			
		||||
                decoration: InputDecoration(labelText: 'Fuel Type'),
 | 
			
		||||
                value: _selectedFuelType,
 | 
			
		||||
                items: FuelType.values
 | 
			
		||||
                    .map((fuelType) => DropdownMenuItem(
 | 
			
		||||
                          value: fuelType,
 | 
			
		||||
                          child: Text(fuelType.label),
 | 
			
		||||
                        ))
 | 
			
		||||
                    .toList(),
 | 
			
		||||
                onChanged: (value) => setState(() => _selectedFuelType = value),
 | 
			
		||||
                validator: (value) =>
 | 
			
		||||
                    value == null ? 'Please select a fuel type' : null,
 | 
			
		||||
              ),
 | 
			
		||||
              TextFormField(
 | 
			
		||||
                controller: _noteController,
 | 
			
		||||
                decoration: InputDecoration(labelText: 'Note (optional)'),
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
      actions: [
 | 
			
		||||
        TextButton(
 | 
			
		||||
          onPressed: () => Navigator.pop(context),
 | 
			
		||||
          child: Text('Cancel'),
 | 
			
		||||
        ),
 | 
			
		||||
        ElevatedButton(
 | 
			
		||||
          onPressed: () {
 | 
			
		||||
            if (_formKey.currentState?.validate() ?? false) {
 | 
			
		||||
              final vehicle = Vehicle(
 | 
			
		||||
                id: widget.vehicle?.id,
 | 
			
		||||
                name: _nameController.text,
 | 
			
		||||
                registrationPlate: _plateController.text,
 | 
			
		||||
                fuelType: _selectedFuelType!,
 | 
			
		||||
                note:
 | 
			
		||||
                    _noteController.text.isEmpty ? null : _noteController.text,
 | 
			
		||||
                isDefault: widget.vehicle?.isDefault ?? false,
 | 
			
		||||
              );
 | 
			
		||||
              Navigator.pop(context, vehicle);
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          child: Text(widget.vehicle == null ? 'Add' : 'Save'),
 | 
			
		||||
        ),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										342
									
								
								lib/services/session_manager.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,342 @@
 | 
			
		||||
import 'dart:convert';
 | 
			
		||||
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:shared_preferences/shared_preferences.dart';
 | 
			
		||||
import 'package:http/http.dart' as http;
 | 
			
		||||
 | 
			
		||||
import '../config.dart';
 | 
			
		||||
 | 
			
		||||
import '../models/vehicle.dart';
 | 
			
		||||
import '../models/refuel.dart';
 | 
			
		||||
import '../models/service.dart';
 | 
			
		||||
 | 
			
		||||
class SessionManager extends ChangeNotifier {
 | 
			
		||||
  static final SessionManager _instance = SessionManager._internal();
 | 
			
		||||
  factory SessionManager() => _instance;
 | 
			
		||||
  SessionManager._internal();
 | 
			
		||||
 | 
			
		||||
  bool _loggedIn = false;
 | 
			
		||||
  String? _token;
 | 
			
		||||
  String? _email;
 | 
			
		||||
  String? _name;
 | 
			
		||||
 | 
			
		||||
  List<Vehicle> _vehicles = [];
 | 
			
		||||
  List<Refuel> _refuels = [];
 | 
			
		||||
  List<ServiceRecord> _services = [];
 | 
			
		||||
 | 
			
		||||
  bool get isLoggedIn => _loggedIn;
 | 
			
		||||
  String? get token => _token;
 | 
			
		||||
  String? get email => _email;
 | 
			
		||||
  String? get name => _name;
 | 
			
		||||
 | 
			
		||||
  List<Vehicle> get vehicles => List.unmodifiable(_vehicles);
 | 
			
		||||
  List<Refuel> get refuels => List.unmodifiable(_refuels);
 | 
			
		||||
  List<ServiceRecord> get services => List.unmodifiable(_services);
 | 
			
		||||
  Vehicle? get defaultVehicle {
 | 
			
		||||
    try {
 | 
			
		||||
      return _vehicles.firstWhere((v) => v.isDefault);
 | 
			
		||||
    } catch (_) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  final _prefs = SharedPreferencesAsync(); // ✅ New API
 | 
			
		||||
 | 
			
		||||
  Future<void> init() async {
 | 
			
		||||
    _token = await _prefs.getString('token');
 | 
			
		||||
    _email = await _prefs.getString('email');
 | 
			
		||||
    _name = await _prefs.getString('name');
 | 
			
		||||
 | 
			
		||||
    if (_token != null) {
 | 
			
		||||
      final valid = await _validateToken();
 | 
			
		||||
      if (valid) {
 | 
			
		||||
        _loggedIn = true;
 | 
			
		||||
        await fetchVehicles();
 | 
			
		||||
        await fetchRefuels();
 | 
			
		||||
        await fetchServices();
 | 
			
		||||
        notifyListeners();
 | 
			
		||||
      } else {
 | 
			
		||||
        await logout();
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      _loggedIn = false;
 | 
			
		||||
      notifyListeners();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> login({
 | 
			
		||||
    required String token,
 | 
			
		||||
    required String email,
 | 
			
		||||
    String? name,
 | 
			
		||||
  }) async {
 | 
			
		||||
    await _prefs.setString('token', token);
 | 
			
		||||
    await _prefs.setString('email', email);
 | 
			
		||||
    if (name != null) await _prefs.setString('name', name);
 | 
			
		||||
 | 
			
		||||
    _token = token;
 | 
			
		||||
    _email = email;
 | 
			
		||||
    _name = name;
 | 
			
		||||
    _loggedIn = true;
 | 
			
		||||
 | 
			
		||||
    // Ensure we have the latest user info and that the token is valid
 | 
			
		||||
    await _validateToken();
 | 
			
		||||
 | 
			
		||||
    await fetchVehicles();
 | 
			
		||||
    await fetchRefuels();
 | 
			
		||||
    await fetchServices();
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> logout() async {
 | 
			
		||||
    await _prefs.remove('token');
 | 
			
		||||
    await _prefs.remove('email');
 | 
			
		||||
    await _prefs.remove('name');
 | 
			
		||||
    _token = null;
 | 
			
		||||
    _email = null;
 | 
			
		||||
    _name = null;
 | 
			
		||||
    _vehicles.clear();
 | 
			
		||||
    _refuels.clear();
 | 
			
		||||
    _services.clear();
 | 
			
		||||
    _loggedIn = false;
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Map<String, String> _authHeaders() => {
 | 
			
		||||
        'Content-Type': 'application/json',
 | 
			
		||||
        if (_token != null) 'Authorization': 'Bearer $_token',
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
  Future<bool> _validateToken() async {
 | 
			
		||||
    if (_token == null) return false;
 | 
			
		||||
    try {
 | 
			
		||||
      final response = await http.get(
 | 
			
		||||
        Uri.parse('$apiBaseUrl/api/v1/user/me'),
 | 
			
		||||
        headers: _authHeaders(),
 | 
			
		||||
      );
 | 
			
		||||
      if (response.statusCode == 200) {
 | 
			
		||||
        final data = jsonDecode(response.body);
 | 
			
		||||
        _email = data['email'] ?? _email;
 | 
			
		||||
        _name = data['username'] ?? data['name'] ?? _name;
 | 
			
		||||
        if (_email != null) await _prefs.setString('email', _email!);
 | 
			
		||||
        if (_name != null) await _prefs.setString('name', _name!);
 | 
			
		||||
        return true;
 | 
			
		||||
      }
 | 
			
		||||
    } catch (_) {}
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> fetchVehicles() async {
 | 
			
		||||
    try {
 | 
			
		||||
      final response = await http.get(
 | 
			
		||||
        Uri.parse('$apiBaseUrl/api/v1/vehicles'),
 | 
			
		||||
        headers: _authHeaders(),
 | 
			
		||||
      );
 | 
			
		||||
      if (response.statusCode == 200) {
 | 
			
		||||
        final List<dynamic> data = jsonDecode(response.body);
 | 
			
		||||
        _vehicles = data.map((e) => Vehicle.fromApi(e)).toList();
 | 
			
		||||
        notifyListeners();
 | 
			
		||||
      }
 | 
			
		||||
    } catch (_) {
 | 
			
		||||
      // ignore for now
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> addVehicle(Vehicle vehicle) async {
 | 
			
		||||
    try {
 | 
			
		||||
      final response = await http.post(
 | 
			
		||||
        Uri.parse('$apiBaseUrl/api/v1/vehicles'),
 | 
			
		||||
        headers: _authHeaders(),
 | 
			
		||||
        body: jsonEncode(vehicle.toApiMap()),
 | 
			
		||||
      );
 | 
			
		||||
      if (response.statusCode == 200 || response.statusCode == 201) {
 | 
			
		||||
        final data = jsonDecode(response.body);
 | 
			
		||||
        _vehicles.add(Vehicle.fromApi(data));
 | 
			
		||||
        notifyListeners();
 | 
			
		||||
      }
 | 
			
		||||
    } catch (_) {}
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> updateVehicle(int index, Vehicle vehicle) async {
 | 
			
		||||
    final id = _vehicles[index].id;
 | 
			
		||||
    if (id == null) return;
 | 
			
		||||
    try {
 | 
			
		||||
      final response = await http.put(
 | 
			
		||||
        Uri.parse('$apiBaseUrl/api/v1/vehicles/$id'),
 | 
			
		||||
        headers: _authHeaders(),
 | 
			
		||||
        body: jsonEncode(vehicle.toApiMap()),
 | 
			
		||||
      );
 | 
			
		||||
      if (response.statusCode == 200) {
 | 
			
		||||
        final data = jsonDecode(response.body);
 | 
			
		||||
        _vehicles[index] = Vehicle.fromApi(data);
 | 
			
		||||
        notifyListeners();
 | 
			
		||||
      }
 | 
			
		||||
    } catch (_) {}
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> removeVehicle(int index) async {
 | 
			
		||||
    final id = _vehicles[index].id;
 | 
			
		||||
    if (id == null) return;
 | 
			
		||||
    try {
 | 
			
		||||
      final response = await http.delete(
 | 
			
		||||
        Uri.parse('$apiBaseUrl/api/v1/vehicles/$id'),
 | 
			
		||||
        headers: _authHeaders(),
 | 
			
		||||
      );
 | 
			
		||||
      if (response.statusCode == 200 || response.statusCode == 204) {
 | 
			
		||||
        _vehicles.removeAt(index);
 | 
			
		||||
        notifyListeners();
 | 
			
		||||
      }
 | 
			
		||||
    } catch (_) {}
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> setDefaultVehicle(String? id) async {
 | 
			
		||||
    if (id == null) {
 | 
			
		||||
      // Unset default from any vehicle currently marked as default
 | 
			
		||||
      for (var i = 0; i < _vehicles.length; i++) {
 | 
			
		||||
        if (_vehicles[i].isDefault) {
 | 
			
		||||
          await updateVehicle(i, _vehicles[i].copyWith(isDefault: false));
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      // Clear default flag on all other vehicles first
 | 
			
		||||
      for (var i = 0; i < _vehicles.length; i++) {
 | 
			
		||||
        final vehicle = _vehicles[i];
 | 
			
		||||
        if (vehicle.isDefault && vehicle.id != id) {
 | 
			
		||||
          await updateVehicle(i, vehicle.copyWith(isDefault: false));
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      // Finally mark the selected vehicle as default
 | 
			
		||||
      final idx = _vehicles.indexWhere((v) => v.id == id);
 | 
			
		||||
      if (idx != -1) {
 | 
			
		||||
        await updateVehicle(idx, _vehicles[idx].copyWith(isDefault: true));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    await fetchVehicles();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Refuel records
 | 
			
		||||
  Future<void> fetchRefuels() async {
 | 
			
		||||
    try {
 | 
			
		||||
      final response = await http.get(
 | 
			
		||||
        Uri.parse('$apiBaseUrl/api/v1/refuels'),
 | 
			
		||||
        headers: _authHeaders(),
 | 
			
		||||
      );
 | 
			
		||||
      if (response.statusCode == 200) {
 | 
			
		||||
        final List<dynamic> data = jsonDecode(response.body);
 | 
			
		||||
        _refuels = data.map((e) => Refuel.fromApi(e)).toList();
 | 
			
		||||
        notifyListeners();
 | 
			
		||||
      }
 | 
			
		||||
    } catch (_) {}
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> addRefuel(Refuel refuel) async {
 | 
			
		||||
    try {
 | 
			
		||||
      final response = await http.post(
 | 
			
		||||
        Uri.parse('$apiBaseUrl/api/v1/refuels'),
 | 
			
		||||
        headers: _authHeaders(),
 | 
			
		||||
        body: jsonEncode(refuel.toApiMap()),
 | 
			
		||||
      );
 | 
			
		||||
      if (response.statusCode == 200 || response.statusCode == 201) {
 | 
			
		||||
        final data = jsonDecode(response.body);
 | 
			
		||||
        _refuels.add(Refuel.fromApi(data));
 | 
			
		||||
        notifyListeners();
 | 
			
		||||
      }
 | 
			
		||||
    } catch (_) {}
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> updateRefuel(String id, Refuel refuel) async {
 | 
			
		||||
    try {
 | 
			
		||||
      final response = await http.put(
 | 
			
		||||
        Uri.parse('$apiBaseUrl/api/v1/refuels/$id'),
 | 
			
		||||
        headers: _authHeaders(),
 | 
			
		||||
        body: jsonEncode(refuel.toApiMap()),
 | 
			
		||||
      );
 | 
			
		||||
      if (response.statusCode == 200) {
 | 
			
		||||
        final data = jsonDecode(response.body);
 | 
			
		||||
        final idx = _refuels.indexWhere((r) => r.id == id);
 | 
			
		||||
        if (idx != -1) {
 | 
			
		||||
          _refuels[idx] = Refuel.fromApi(data);
 | 
			
		||||
          notifyListeners();
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } catch (_) {}
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> removeRefuel(String id) async {
 | 
			
		||||
    try {
 | 
			
		||||
      final response = await http.delete(
 | 
			
		||||
        Uri.parse('$apiBaseUrl/api/v1/refuels/$id'),
 | 
			
		||||
        headers: _authHeaders(),
 | 
			
		||||
      );
 | 
			
		||||
      if (response.statusCode == 200 || response.statusCode == 204) {
 | 
			
		||||
        _refuels.removeWhere((r) => r.id == id);
 | 
			
		||||
        notifyListeners();
 | 
			
		||||
      }
 | 
			
		||||
    } catch (_) {}
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Service records
 | 
			
		||||
  Future<void> fetchServices() async {
 | 
			
		||||
    try {
 | 
			
		||||
      final response = await http.get(
 | 
			
		||||
        Uri.parse('$apiBaseUrl/api/v1/services'),
 | 
			
		||||
        headers: _authHeaders(),
 | 
			
		||||
      );
 | 
			
		||||
      if (response.statusCode == 200) {
 | 
			
		||||
        final List<dynamic> data = jsonDecode(response.body);
 | 
			
		||||
        _services = data.map((e) => ServiceRecord.fromApi(e)).toList();
 | 
			
		||||
        notifyListeners();
 | 
			
		||||
      }
 | 
			
		||||
    } catch (_) {}
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<bool> addService(ServiceRecord service) async {
 | 
			
		||||
    try {
 | 
			
		||||
      final response = await http.post(
 | 
			
		||||
        Uri.parse('$apiBaseUrl/api/v1/services'),
 | 
			
		||||
        headers: _authHeaders(),
 | 
			
		||||
        body: jsonEncode(service.toApiMap()),
 | 
			
		||||
      );
 | 
			
		||||
      if (response.statusCode == 200 || response.statusCode == 201) {
 | 
			
		||||
        final data = jsonDecode(response.body);
 | 
			
		||||
        _services.add(ServiceRecord.fromApi(data));
 | 
			
		||||
        notifyListeners();
 | 
			
		||||
        return true;
 | 
			
		||||
      }
 | 
			
		||||
    } catch (_) {}
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<bool> updateService(String id, ServiceRecord service) async {
 | 
			
		||||
    try {
 | 
			
		||||
      final response = await http.put(
 | 
			
		||||
        Uri.parse('$apiBaseUrl/api/v1/services/$id'),
 | 
			
		||||
        headers: _authHeaders(),
 | 
			
		||||
        body: jsonEncode(service.toApiMap()),
 | 
			
		||||
      );
 | 
			
		||||
      if (response.statusCode == 200) {
 | 
			
		||||
        final data = jsonDecode(response.body);
 | 
			
		||||
        final idx = _services.indexWhere((s) => s.id == id);
 | 
			
		||||
        if (idx != -1) {
 | 
			
		||||
          _services[idx] = ServiceRecord.fromApi(data);
 | 
			
		||||
          notifyListeners();
 | 
			
		||||
        }
 | 
			
		||||
        return true;
 | 
			
		||||
      }
 | 
			
		||||
    } catch (_) {}
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> removeService(String id) async {
 | 
			
		||||
    try {
 | 
			
		||||
      final response = await http.delete(
 | 
			
		||||
        Uri.parse('$apiBaseUrl/api/v1/services/$id'),
 | 
			
		||||
        headers: _authHeaders(),
 | 
			
		||||
      );
 | 
			
		||||
      if (response.statusCode == 200 || response.statusCode == 204) {
 | 
			
		||||
        _services.removeWhere((s) => s.id == id);
 | 
			
		||||
        notifyListeners();
 | 
			
		||||
      }
 | 
			
		||||
    } catch (_) {}
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										98
									
								
								lib/widgets/consumption_chart.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,98 @@
 | 
			
		||||
import 'package:fl_chart/fl_chart.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
import '../models/refuel.dart';
 | 
			
		||||
 | 
			
		||||
class ConsumptionChart extends StatelessWidget {
 | 
			
		||||
  final List<Refuel> refuels;
 | 
			
		||||
 | 
			
		||||
  const ConsumptionChart({super.key, required this.refuels});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final spots = <FlSpot>[];
 | 
			
		||||
    final labels = <String>[];
 | 
			
		||||
    for (var i = 1; i < refuels.length; i++) {
 | 
			
		||||
      final prev = refuels[i - 1];
 | 
			
		||||
      final curr = refuels[i];
 | 
			
		||||
      final distance = curr.mileage - prev.mileage;
 | 
			
		||||
      if (distance <= 0) continue;
 | 
			
		||||
      final consumption =
 | 
			
		||||
          double.parse((curr.liters / distance * 100).toStringAsFixed(2));
 | 
			
		||||
      spots.add(FlSpot(spots.length.toDouble(), consumption));
 | 
			
		||||
      final date = curr.createdAt;
 | 
			
		||||
      labels.add(date != null ? '${date.month}/${date.day}' : '');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return LineChart(
 | 
			
		||||
      LineChartData(
 | 
			
		||||
        lineBarsData: [
 | 
			
		||||
          LineChartBarData(
 | 
			
		||||
            spots: spots,
 | 
			
		||||
            isCurved: true,
 | 
			
		||||
            barWidth: 3,
 | 
			
		||||
            color: Theme.of(context).colorScheme.secondary,
 | 
			
		||||
            dotData: const FlDotData(show: true),
 | 
			
		||||
          )
 | 
			
		||||
        ],
 | 
			
		||||
        lineTouchData: LineTouchData(
 | 
			
		||||
          touchTooltipData: LineTouchTooltipData(
 | 
			
		||||
            tooltipBgColor: Colors.black87,
 | 
			
		||||
            tooltipMargin: 40,
 | 
			
		||||
            fitInsideHorizontally: true,
 | 
			
		||||
            fitInsideVertically: true,
 | 
			
		||||
            getTooltipItems: (spots) => spots
 | 
			
		||||
                .map((s) => LineTooltipItem(
 | 
			
		||||
                    '${s.y.toStringAsFixed(2)} L/100km',
 | 
			
		||||
                    const TextStyle(color: Colors.white)))
 | 
			
		||||
                .toList(),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
        titlesData: FlTitlesData(
 | 
			
		||||
          leftTitles: AxisTitles(
 | 
			
		||||
            axisNameSize: 28,
 | 
			
		||||
            axisNameWidget: const Padding(
 | 
			
		||||
              padding: EdgeInsets.only(right: 8),
 | 
			
		||||
              child: Text('L/100km'),
 | 
			
		||||
            ),
 | 
			
		||||
            sideTitles: SideTitles(
 | 
			
		||||
              showTitles: true,
 | 
			
		||||
              reservedSize: 50,
 | 
			
		||||
              getTitlesWidget: (value, meta) => Padding(
 | 
			
		||||
                padding: const EdgeInsets.only(right: 4),
 | 
			
		||||
                child: Text(value.toStringAsFixed(1),
 | 
			
		||||
                    style: const TextStyle(fontSize: 10)),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
          bottomTitles: AxisTitles(
 | 
			
		||||
            axisNameSize: 24,
 | 
			
		||||
            axisNameWidget: const Padding(
 | 
			
		||||
              padding: EdgeInsets.only(top: 8),
 | 
			
		||||
              child: Text('Date'),
 | 
			
		||||
            ),
 | 
			
		||||
            sideTitles: SideTitles(
 | 
			
		||||
              showTitles: true,
 | 
			
		||||
              reservedSize: 36,
 | 
			
		||||
              getTitlesWidget: (value, meta) {
 | 
			
		||||
                final index = value.toInt();
 | 
			
		||||
                if (index < 0 || index >= labels.length) {
 | 
			
		||||
                  return const SizedBox.shrink();
 | 
			
		||||
                }
 | 
			
		||||
                return Padding(
 | 
			
		||||
                  padding: const EdgeInsets.only(top: 4),
 | 
			
		||||
                  child: Text(labels[index],
 | 
			
		||||
                      style: const TextStyle(fontSize: 10)),
 | 
			
		||||
                );
 | 
			
		||||
              },
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
          rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
 | 
			
		||||
          topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
 | 
			
		||||
        ),
 | 
			
		||||
        gridData: const FlGridData(show: false),
 | 
			
		||||
        borderData: FlBorderData(show: false),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										19
									
								
								lib/widgets/data_section.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,19 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
class DataSection extends StatelessWidget {
 | 
			
		||||
  final String title;
 | 
			
		||||
 | 
			
		||||
  const DataSection({required this.title});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Card(
 | 
			
		||||
      margin: EdgeInsets.all(16),
 | 
			
		||||
      child: Container(
 | 
			
		||||
        height: 200,
 | 
			
		||||
        padding: EdgeInsets.all(16),
 | 
			
		||||
        child: Center(child: Text(title, style: TextStyle(fontSize: 18))),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										93
									
								
								lib/widgets/gas_price_chart.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,93 @@
 | 
			
		||||
import 'package:fl_chart/fl_chart.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
import '../models/refuel.dart';
 | 
			
		||||
 | 
			
		||||
class GasPriceChart extends StatelessWidget {
 | 
			
		||||
  final List<Refuel> refuels;
 | 
			
		||||
 | 
			
		||||
  const GasPriceChart({super.key, required this.refuels});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final spots = <FlSpot>[];
 | 
			
		||||
    final labels = <String>[];
 | 
			
		||||
    for (var i = 0; i < refuels.length; i++) {
 | 
			
		||||
      final refuel = refuels[i];
 | 
			
		||||
      spots.add(FlSpot(spots.length.toDouble(), refuel.pricePerLiter));
 | 
			
		||||
      final date = refuel.createdAt;
 | 
			
		||||
      labels.add(date != null ? '${date.month}/${date.day}' : '');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return LineChart(
 | 
			
		||||
      LineChartData(
 | 
			
		||||
        lineBarsData: [
 | 
			
		||||
          LineChartBarData(
 | 
			
		||||
            spots: spots,
 | 
			
		||||
            isCurved: true,
 | 
			
		||||
            barWidth: 3,
 | 
			
		||||
            color: Theme.of(context).colorScheme.primary,
 | 
			
		||||
            dotData: const FlDotData(show: true),
 | 
			
		||||
          )
 | 
			
		||||
        ],
 | 
			
		||||
        lineTouchData: LineTouchData(
 | 
			
		||||
          touchTooltipData: LineTouchTooltipData(
 | 
			
		||||
            tooltipBgColor: Colors.black87,
 | 
			
		||||
            tooltipMargin: 40,
 | 
			
		||||
            fitInsideHorizontally: true,
 | 
			
		||||
            fitInsideVertically: true,
 | 
			
		||||
            getTooltipItems: (spots) => spots
 | 
			
		||||
                .map((s) => LineTooltipItem(
 | 
			
		||||
                    s.y.toStringAsFixed(2),
 | 
			
		||||
                    const TextStyle(color: Colors.white)))
 | 
			
		||||
                .toList(),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
        titlesData: FlTitlesData(
 | 
			
		||||
          leftTitles: AxisTitles(
 | 
			
		||||
            axisNameSize: 28,
 | 
			
		||||
            axisNameWidget: const Padding(
 | 
			
		||||
              padding: EdgeInsets.only(right: 8),
 | 
			
		||||
              child: Text('Price/L'),
 | 
			
		||||
            ),
 | 
			
		||||
            sideTitles: SideTitles(
 | 
			
		||||
              showTitles: true,
 | 
			
		||||
              reservedSize: 50,
 | 
			
		||||
              getTitlesWidget: (value, meta) => Padding(
 | 
			
		||||
                padding: const EdgeInsets.only(right: 4),
 | 
			
		||||
                child: Text(value.toStringAsFixed(1),
 | 
			
		||||
                    style: const TextStyle(fontSize: 10)),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
          bottomTitles: AxisTitles(
 | 
			
		||||
            axisNameSize: 24,
 | 
			
		||||
            axisNameWidget: const Padding(
 | 
			
		||||
              padding: EdgeInsets.only(top: 8),
 | 
			
		||||
              child: Text('Date'),
 | 
			
		||||
            ),
 | 
			
		||||
            sideTitles: SideTitles(
 | 
			
		||||
              showTitles: true,
 | 
			
		||||
              reservedSize: 36,
 | 
			
		||||
              getTitlesWidget: (value, meta) {
 | 
			
		||||
                final index = value.toInt();
 | 
			
		||||
                if (index < 0 || index >= labels.length) {
 | 
			
		||||
                  return const SizedBox.shrink();
 | 
			
		||||
                }
 | 
			
		||||
                return Padding(
 | 
			
		||||
                  padding: const EdgeInsets.only(top: 4),
 | 
			
		||||
                  child: Text(labels[index],
 | 
			
		||||
                      style: const TextStyle(fontSize: 10)),
 | 
			
		||||
                );
 | 
			
		||||
              },
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
          rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
 | 
			
		||||
          topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
 | 
			
		||||
        ),
 | 
			
		||||
        gridData: const FlGridData(show: false),
 | 
			
		||||
        borderData: FlBorderData(show: false),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										29
									
								
								lib/widgets/stat_card.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,29 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
class StatCard extends StatelessWidget {
 | 
			
		||||
  final String title;
 | 
			
		||||
  final String value;
 | 
			
		||||
 | 
			
		||||
  const StatCard({super.key, required this.title, required this.value});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Card(
 | 
			
		||||
      child: Padding(
 | 
			
		||||
        padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 16),
 | 
			
		||||
        child: Column(
 | 
			
		||||
          mainAxisSize: MainAxisSize.min,
 | 
			
		||||
          mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
          children: [
 | 
			
		||||
            Text(
 | 
			
		||||
              value,
 | 
			
		||||
              style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
 | 
			
		||||
            ),
 | 
			
		||||
            const SizedBox(height: 2),
 | 
			
		||||
            Text(title, textAlign: TextAlign.center),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								linux/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
			
		||||
flutter/ephemeral
 | 
			
		||||