diff --git a/.fvmrc b/.fvmrc
new file mode 100644
index 0000000..984aec1
--- /dev/null
+++ b/.fvmrc
@@ -0,0 +1,3 @@
+{
+ "flutter": "3.32.4"
+}
\ No newline at end of file
diff --git a/.github/release.yml b/.github/release.yml
new file mode 100644
index 0000000..57cda2a
--- /dev/null
+++ b/.github/release.yml
@@ -0,0 +1,32 @@
+# .github/release.yml
+
+changelog:
+ exclude:
+ labels:
+ - wontfix
+ - translation
+ categories:
+ - title: Breaking Changes
+ labels:
+ - Semver-Major
+ - breaking
+ - title: Security Patches
+ labels:
+ - security
+ - title: New Features
+ labels:
+ - Semver-Minor
+ - enhancement
+ - title: Bug Fixes
+ labels:
+ - Semver-Patch
+ - bug
+ - title: Devops / Setup Changes
+ labels:
+ - docker
+ - setup
+ - demo
+ - CI
+ - title: Other Changes
+ labels:
+ - "*"
diff --git a/.github/workflows/android.yaml b/.github/workflows/android.yaml
index 32aed76..3147927 100644
--- a/.github/workflows/android.yaml
+++ b/.github/workflows/android.yaml
@@ -3,10 +3,10 @@
name: Android
on:
- push:
+ pull_request:
branches:
- master
- pull_request:
+ push:
branches:
- master
@@ -17,23 +17,43 @@ jobs:
steps:
- name: Checkout code
- uses: actions/checkout@v2
- with:
- submodules: recursive
+ uses: actions/checkout@v3
+
- name: Setup Java
- uses: actions/setup-java@v1
+ uses: actions/setup-java@v3
with:
- java-version: '12.x'
- - name: Setup Flutter
- uses: subosito/flutter-action@v1
+ distribution: 'temurin'
+ java-version: '17'
+
+ - name: Setup FVM
+ id: fvm-config-action
+ uses: kuhnroyal/flutter-fvm-config-action@v2
+
+ - uses: subosito/flutter-action@v2
with:
- flutter-version: '2.10.3'
+ flutter-version: ${{ steps.fvm-config-action.outputs.FLUTTER_VERSION }}
+ channel: ${{ steps.fvm-config-action.outputs.FLUTTER_CHANNEL }}
+ cache: false
+ cache-key: "flutter-:os:-:channel:-:version:-:arch:-:hash:"
+ cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:"
+ pub-cache-key: "flutter-pub:os:-:channel:-:version:-:arch:-:hash:"
+ pub-cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:"
+
+ - run: flutter --version
+
- name: Setup Gradle
- uses: gradle/gradle-build-action@v2
+ uses: gradle/gradle-build-action@v2.4.2
with:
- gradle-version: 6.1.1
+ gradle-version: 8.7
+
+ - name: Collect Translation Files
+ run: |
+ cd lib/l10n
+ python3 collect_translations.py
+
- name: Build for Android
run: |
- flutter pub get
- cp lib/dummy_dsn.dart lib/dsn.dart
- flutter build apk --debug
+ dart pub global activate fvm
+ fvm install
+ fvm flutter pub get
+ fvm flutter build apk --debug
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
new file mode 100644
index 0000000..c317876
--- /dev/null
+++ b/.github/workflows/ci.yaml
@@ -0,0 +1,93 @@
+name: CI
+
+on:
+ push:
+ branches:
+ - master
+ pull_request:
+ branches:
+ - master
+
+env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ INVENTREE_SITE_URL: http://localhost:8000
+ INVENTREE_DB_ENGINE: django.db.backends.sqlite3
+ INVENTREE_DB_NAME: ../inventree_unit_test_db.sqlite3
+ INVENTREE_MEDIA_ROOT: ../test_inventree_media
+ INVENTREE_STATIC_ROOT: ../test_inventree_static
+ INVENTREE_BACKUP_DIR: ../test_inventree_backup
+ INVENTREE_ADMIN_USER: testuser
+ INVENTREE_ADMIN_PASSWORD: testpassword
+ INVENTREE_ADMIN_EMAIL: test@test.com
+jobs:
+
+ test:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+ with:
+ submodules: recursive
+
+ - name: Install Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: 3.11
+
+ - name: Setup Java
+ uses: actions/setup-java@v3
+ with:
+ distribution: 'temurin'
+ java-version: '11'
+
+ - name: Setup Flutter and FVM
+ id: fvm-config-action
+ uses: kuhnroyal/flutter-fvm-config-action@v2
+
+ - uses: subosito/flutter-action@v2
+ with:
+ flutter-version: ${{ steps.fvm-config-action.outputs.FLUTTER_VERSION }}
+ channel: ${{ steps.fvm-config-action.outputs.FLUTTER_CHANNEL }}
+ cache: true
+ cache-key: "flutter-:os:-:channel:-:version:-:arch:-:hash:"
+ cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:"
+ pub-cache-key: "flutter-pub:os:-:channel:-:version:-:arch:-:hash:"
+ pub-cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:"
+
+ - name: Collect Translation Files
+ run: |
+ cd lib/l10n
+ python collect_translations.py
+
+ - name: Static Analysis Tests
+ working-directory: .
+ run: |
+ python ./find_dart_files.py
+ dart pub global activate fvm
+ fvm install
+ fvm flutter pub get
+ fvm flutter analyze
+ dart format --output=none --set-exit-if-changed .
+
+ - name: Start InvenTree Server
+ run: |
+ sudo apt-get install python3-dev python3-pip python3-venv python3-wheel g++
+ pip3 install invoke
+ git clone --depth 1 https://github.com/inventree/inventree ./inventree_server
+ cd inventree_server
+ invoke install
+ invoke migrate
+ invoke dev.import-fixtures
+ invoke dev.server -a 127.0.0.1:8000 &
+ invoke wait
+ sleep 30
+
+ - name: Unit Tests
+ run: |
+ fvm flutter test --coverage
+
+ - name: Coveralls
+ uses: coverallsapp/github-action@master
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/ios.yaml b/.github/workflows/ios.yaml
index 202c684..cdd51ed 100644
--- a/.github/workflows/ios.yaml
+++ b/.github/workflows/ios.yaml
@@ -3,10 +3,10 @@
name: iOS
on:
- push:
+ pull_request:
branches:
- master
- pull_request:
+ push:
branches:
- master
@@ -17,23 +17,44 @@ jobs:
steps:
- name: Checkout code
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
with:
submodules: recursive
+
- name: Setup Java
- uses: actions/setup-java@v1
+ uses: actions/setup-java@v3
with:
- java-version: '12.x'
+ distribution: 'temurin'
+ java-version: '11'
+
+ - name: Setup FVM
+ id: fvm-config-action
+ uses: kuhnroyal/flutter-fvm-config-action@v2
+
- name: Setup Flutter
- uses: subosito/flutter-action@v1
+ uses: subosito/flutter-action@v2
with:
- flutter-version: '2.10.3'
+ flutter-version: ${{ steps.fvm-config-action.outputs.FLUTTER_VERSION }}
+ channel: ${{ steps.fvm-config-action.outputs.FLUTTER_CHANNEL }}
+ cache: false
+ cache-key: "flutter-:os:-:channel:-:version:-:arch:-:hash:"
+ cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:"
+ pub-cache-key: "flutter-pub:os:-:channel:-:version:-:arch:-:hash:"
+ pub-cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:"
+
+ - name: Collect Translation Files
+ run: |
+ cd lib/l10n
+ python3 collect_translations.py
+
- name: Build for iOS
run: |
- flutter pub get
+ dart pub global activate fvm
+ fvm install
+ fvm flutter pub get
+ fvm flutter precache --ios
cd ios
pod repo update
pod install
cd ..
- cp lib/dummy_dsn.dart lib/dsn.dart
- flutter build ios --release --no-codesign
+ fvm flutter build ios --release --no-codesign --no-tree-shake-icons
diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml
deleted file mode 100644
index a9d3fe7..0000000
--- a/.github/workflows/lint.yaml
+++ /dev/null
@@ -1,37 +0,0 @@
-# Run flutter linting checks
-
-name: lint
-
-on:
- push:
- branches:
- - master
- pull_request:
- branches:
- - master
-
-jobs:
-
- lint:
- runs-on: ubuntu-latest
-
- env:
- SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
-
- steps:
- - name: Checkout code
- uses: actions/checkout@v2
- with:
- submodules: recursive
- - name: Setup Java
- uses: actions/setup-java@v1
- with:
- java-version: '12.x'
- - name: Setup Flutter
- uses: subosito/flutter-action@v1
- with:
- flutter-version: '2.10.3'
- - run: flutter pub get
- - run: cp lib/dummy_dsn.dart lib/dsn.dart
- - run: flutter analyze
- - run: flutter test --coverage
diff --git a/.gitignore b/.gitignore
index 2f25898..8effbf9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,8 +11,9 @@
coverage/*
-# Sentry API key
-lib/dsn.dart
+# This file is auto-generated as part of the CI process
+test/coverage_helper_test.dart
+InvenTreeSettings.db
# App signing key
android/key.properties
@@ -81,3 +82,6 @@ ios/Podfile.lock
!**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
+
+# FVM Version Cache
+.fvm/
\ No newline at end of file
diff --git a/.gitmodules b/.gitmodules
index 4de5b51..e69de29 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +0,0 @@
-[submodule "lib/l10n"]
- path = lib/l10n
- url = git@github.com:inventree/inventree-app-i18n.git
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..5dd1dc0
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,8 @@
+repos:
+ - repo: local
+ hooks:
+ - id: dart-format
+ name: Dart Format
+ entry: dart format
+ language: system
+ types: [dart]
\ No newline at end of file
diff --git a/BUILDING.md b/BUILDING.md
new file mode 100644
index 0000000..b04b60b
--- /dev/null
+++ b/BUILDING.md
@@ -0,0 +1,132 @@
+## InvenTree App Development
+
+For developers looking to contribute to the project, we use Flutter for app development. The project has been tested in Android Studio (on both Windows and Mac) and also VSCode.
+
+## Prerequisites
+
+To build the app from source, you will need the following tools installed on your system:
+
+- Android Studio or Visual Studio Code (with Flutter and Dart plugins)
+- [Flutter Version Management (FVM)](https://fvm.app/) - We use FVM to manage Flutter versions
+
+### iOS Development
+
+For iOS development, you will need a Mac system with XCode installed.
+
+### Java Version
+
+Some versions of Android Studio ship with a built-in version of the Java JDK. However, the InvenTree app requires [JDK 17](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html) to be installed.
+
+If you see any errors related to JDK version mismatch, download and install the correct version of the JDK (from the link above) and update your Android Studio settings to point to the correct JDK location:
+
+```bash
+fvm flutter config --jdk-dir /path/to/jdk
+```
+
+## Invoke Tasks
+
+We use the [invoke](https://www.pyinvoke.org) to run some core tasks - you will need python and invoke installed on your local system.
+
+## Flutter Version Management (FVM)
+
+This project uses [Flutter Version Management (FVM)](https://fvm.app/) to ensure consistent Flutter versions across development environments and CI/CD pipelines.
+
+For installation instructions, please refer to the [official FVM documentation](https://fvm.app/documentation/getting-started/installation).
+
+Once installed, FVM will automatically use the Flutter version specified in the `.fvmrc` file at the root of the project.
+
+### Visual Studio Code
+
+To set up Visual Studio Code, you will need to make sure the `.vscode` directory exists. Then run `fvm use` to ensure the correct Flutter version is used.
+
+```
+mkdir -p .vscode
+fvm use
+```
+
+#### What happens:
+- Downloads SDK if not cached
+- Creates `.fvm` directory with SDK symlink
+- Updates `.fvmrc` configuration
+- Configures IDE settings
+- Runs `flutter pub get`
+
+
+### Android Studio
+
+To set up Android Studio, run `fvm use` to ensure the correct Flutter version is used.
+
+```
+fvm use
+```
+
+#### What happens:
+- Downloads SDK if not cached
+- Creates `.fvm` directory with SDK symlink
+- Updates `.fvmrc` configuration
+- Runs `flutter pub get`
+
+Set Flutter SDK path in Android Studio:
+
+1. Open Android Studio
+2. Go to `File` -> `Settings` -> `Languages & Frameworks` -> `Flutter`
+3. Set `Flutter SDK path` to `.fvm/flutter_sdk`:
+
+
+
+
+## Getting Started
+
+Initial project setup (after you have installed all required dev tools) is as follows:
+
+Generate initial translation files:
+
+```
+invoke translate
+```
+
+Install required flutter packages:
+```
+fvm flutter pub get
+```
+
+You should now be ready to debug on a connected or emulated device!
+
+## Troubleshooting
+
+### Flutter Doctor
+
+If you're experiencing issues with the development environment, run Flutter Doctor to diagnose problems:
+
+```bash
+fvm flutter doctor -v
+```
+
+This will check your Flutter installation and identify any issues with your setup. Common issues include:
+
+- Missing Android SDK components
+- iOS development tools not properly configured
+- Missing dependencies
+
+Fix any identified issues before proceeding with development.
+
+
+## Building Release Versions
+
+Building release versions for target platforms (either android or iOS) is simplified using invoke:
+
+### Android
+
+Build Android release:
+
+```
+invoke android
+```
+
+### iOS
+
+Build iOS release:
+
+```
+invoke ios
+```
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..859ab60
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,50 @@
+# Contributing to InvenTree App
+
+Thank you for considering contributing to the InvenTree App! This document outlines some guidelines to ensure smooth collaboration.
+
+## Code Style and Formatting
+
+### Dart Formatting
+
+We enforce consistent code formatting using Dart's built-in formatter. Before submitting a pull request:
+
+1. Run the formatter on your code:
+ ```bash
+ fvm dart format .
+ ```
+
+2. Our CI pipeline will verify that all code follows the standard Flutter/Dart formatting rules. Pull requests with improper formatting will fail CI checks.
+
+### General Guidelines
+
+- Write clear, readable, and maintainable code
+- Include comments where necessary
+- Follow Flutter/Dart best practices
+- Write tests for new features when applicable
+
+## Pull Request Process
+
+1. Fork the repository and create a feature branch
+2. Make your changes
+3. Ensure your code passes all tests and linting
+4. Format your code using `invoke format`
+5. Submit a pull request with a clear description of the changes
+6. Address any review comments
+
+## Development Setup
+
+1. Ensure you have Flutter installed (we use Flutter Version Management)
+2. Check the required Flutter version in the `.fvmrc` file
+3. Install dependencies with `fvm flutter pub get`
+4. Run tests with `fvm flutter test`
+
+## Reporting Issues
+
+When reporting issues, please include:
+- Clear steps to reproduce the issue
+- Expected behavior
+- Actual behavior
+- Screenshots if applicable
+- Device/environment information
+
+Thank you for contributing to the InvenTree App!
diff --git a/README.md b/README.md
index 844a040..a2fdfe3 100644
--- a/README.md
+++ b/README.md
@@ -3,11 +3,36 @@
[](https://opensource.org/licenses/MIT)


+[](https://coveralls.io/github/inventree/inventree-app?branch=master)
The InvenTree mobile / tablet application is a companion app for the [InvenTree stock management system](https://github.com/inventree/InvenTree).
Written in the [Flutter](https://flutter.dev/) environment, the app provides native support for Android and iOS devices.
+
+
+
+
+## Installation
+
+You can install the app via the following channels:
+
+### Google Play Store (Android)
+
+Download and install from the [Google Play Store](https://play.google.com/store/apps/details?id=inventree.inventree_app&hl=en_AU)
+
+### Apple Store (iOS)
+
+Download and install from the [Apple App Store](https://apps.apple.com/au/app/inventree/id1581731101)
+
+### Direct Download (Android)
+
+We provide direct downloads for Android users - view our [download page via polar.sh](https://polar.sh/inventree/products/299bf0d5-af88-4e0f-becf-c007ad37ecf2)
+
## User Documentation
User documentation for the InvenTree mobile app can be found [within the InvenTree documentation](https://inventree.readthedocs.io/en/latest/app/app/).
+
+## Developer Documentation
+
+Refer to the [build instructions](BUILDING.md) for information on how to build the app from source.
diff --git a/RELEASE.md b/RELEASE.md
deleted file mode 100644
index 7f16021..0000000
--- a/RELEASE.md
+++ /dev/null
@@ -1,47 +0,0 @@
-# Release Process
-
-## Android Play Store
-
-[Reference](https://flutter.dev/docs/deployment/android#signing-the-app)
-
-### Key File
-
-Add a file `key.properties` under the android/ directory
-
-### Increment Build Number
-
-Make sure that the build number is incremented every time (or it will be rejected by Play Store).
-
-### Copy Translations
-
-Ensure that the translation files have been updated, and copied into the correct directory!!
-
-```
-cd lib/l10n
-python update_translations.py
-```
-
-### Build Appbundle
-
-`flutter build appbundle`
-
-### Upload Appbundle
-
-Upload the appbundle file to the Android developer website.
-
-## Apple Store
-
-Ref: https://flutter.dev/docs/deployment/ios
-
-### Build ipa
-
-```
-flutter clean
-flutter build ipa
-```
-
-### Validate and Distribute
-
-- Open `./build/ios/archive/Runner.xcarchive` in Xcode
-- Run "Validate App"
-- Run "Distribute App"
\ No newline at end of file
diff --git a/analysis_options.yaml b/analysis_options.yaml
index 29c06eb..eed06a4 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -6,8 +6,6 @@ analyzer:
- lib/generated/**
language:
strict-raw-types: true
- strong-mode:
- implicit-casts: false
linter:
rules:
@@ -21,6 +19,8 @@ linter:
prefer_double_quotes: true
+ unreachable_from_main: false
+
prefer_final_locals: false
prefer_const_constructors: false
@@ -74,4 +74,12 @@ linter:
avoid_dynamic_calls: false
- avoid_classes_with_only_static_members: false
\ No newline at end of file
+ avoid_classes_with_only_static_members: false
+
+ no_leading_underscores_for_local_identifiers: false
+
+ use_super_parameters: false
+
+ # TODO: Enable unnecessary_async and unawaited_futures rules
+ unnecessary_async: false
+
diff --git a/android/.gitignore b/android/.gitignore
new file mode 100644
index 0000000..97a5d13
--- /dev/null
+++ b/android/.gitignore
@@ -0,0 +1,14 @@
+gradle-wrapper.jar
+/.gradle
+/captures/
+/gradlew
+/gradlew.bat
+/local.properties
+GeneratedPluginRegistrant.java
+.cxx/
+
+# Remember to never publicly share your keystore.
+# See https://flutter.dev/to/reference-keystore
+key.properties
+**/*.keystore
+**/*.jks
\ No newline at end of file
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 6b6b83a..d133d64 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -1,3 +1,9 @@
+plugins {
+ id "com.android.application"
+ id "kotlin-android"
+ id "dev.flutter.flutter-gradle-plugin"
+}
+
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
@@ -6,11 +12,6 @@ if (localPropertiesFile.exists()) {
}
}
-def flutterRoot = localProperties.getProperty('flutter.sdk')
-if (flutterRoot == null) {
- throw new FileNotFoundException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
-}
-
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
@@ -21,10 +22,6 @@ if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
-apply plugin: 'com.android.application'
-apply plugin: 'kotlin-android'
-apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
-
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
@@ -32,7 +29,18 @@ if (keystorePropertiesFile.exists()) {
}
android {
- compileSdkVersion 31
+ namespace "inventree.inventree_app"
+ compileSdkVersion 35
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
+ }
+
+ // If using Kotlin
+ kotlinOptions {
+ jvmTarget = JavaVersion.VERSION_17
+ }
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
@@ -48,8 +56,8 @@ android {
defaultConfig {
applicationId "inventree.inventree_app"
- minSdkVersion 25
- targetSdkVersion 31
+ minSdkVersion 21
+ targetSdkVersion 35
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
@@ -83,7 +91,6 @@ dependencies {
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
androidTestImplementation 'com.android.support:multidex:2.0.1'
- implementation "androidx.core:core:1.5.0-rc01"
- implementation 'androidx.appcompat:appcompat:1.0.0'
- implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ implementation "androidx.core:core:1.9.0"
+ implementation 'androidx.appcompat:appcompat:1.6.0'
}
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 41a7000..3bf7ae2 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -1,4 +1,6 @@
-
-
-
-
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/values-v31/styles.xml b/android/app/src/main/res/values-v31/styles.xml
new file mode 100644
index 0000000..fb61beb
--- /dev/null
+++ b/android/app/src/main/res/values-v31/styles.xml
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml
index ca5f4fc..dabc4dc 100644
--- a/android/app/src/profile/AndroidManifest.xml
+++ b/android/app/src/profile/AndroidManifest.xml
@@ -1,5 +1,4 @@
-
+
diff --git a/android/build.gradle b/android/build.gradle
index af27b36..346e552 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -1,22 +1,8 @@
-buildscript {
-
- ext.kotlin_version = '1.5.10'
-
- repositories {
- google()
- jcenter()
- }
-
- dependencies {
- classpath 'com.android.tools.build:gradle:4.0.0'
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
- }
-}
allprojects {
repositories {
google()
- jcenter()
+ mavenCentral()
}
}
@@ -29,6 +15,6 @@ subprojects {
}
}
-task clean(type: Delete) {
+tasks.register("clean", Delete) {
delete rootProject.buildDir
}
diff --git a/android/gradle.properties b/android/gradle.properties
index 5f17537..f2b745e 100644
--- a/android/gradle.properties
+++ b/android/gradle.properties
@@ -1,4 +1,8 @@
-org.gradle.jvmargs=-Xmx1536M
-android.enableR8=true
+org.gradle.daemon=true
+org.gradle.parallel=true
+org.gradle.configureondemand=true
+org.gradle.caching=true
+org.gradle.jvmargs=-Xmx4096M
+android.enableD8=true
android.enableJetifier=true
android.useAndroidX=true
\ No newline at end of file
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
index 9b9d49f..3bfe7cf 100644
--- a/android/gradle/wrapper/gradle-wrapper.properties
+++ b/android/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Fri Jun 23 08:50:38 CEST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip
+networkTimeout=30000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
\ No newline at end of file
diff --git a/android/settings.gradle b/android/settings.gradle
index 5a2f14f..eb1096e 100644
--- a/android/settings.gradle
+++ b/android/settings.gradle
@@ -1,15 +1,25 @@
-include ':app'
+pluginManagement {
+ def flutterSdkPath = {
+ def properties = new Properties()
+ file("local.properties").withInputStream { properties.load(it) }
+ def flutterSdkPath = properties.getProperty("flutter.sdk")
+ assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
+ return flutterSdkPath
+ }()
-def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
+ includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
-def plugins = new Properties()
-def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
-if (pluginsFile.exists()) {
- pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
}
-plugins.each { name, path ->
- def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
- include ":$name"
- project(":$name").projectDir = pluginDirectory
+plugins {
+ id "dev.flutter.flutter-plugin-loader" version "1.0.0"
+ id "com.android.application" version "8.6.0" apply false
+ id "org.jetbrains.kotlin.android" version "1.9.25" apply false
}
+
+include ":app"
\ No newline at end of file
diff --git a/assets/credits.md b/assets/credits.md
index 525dc8d..31eb4f0 100644
--- a/assets/credits.md
+++ b/assets/credits.md
@@ -1,6 +1,23 @@
-## InvenTree App Credits
----
+## Contributors
-### Sound Files
+Thanks to the following contributors, for their work building this app!
-- Some sound files have been sourced from [https://www.zapsplat.com](https://www.zapsplat.com)
+- [SchrodingersGat](https://github.com/SchrodingersGat) (*Lead Developer*)
+- [cbenhagen](https://github.com/cbenhagen)
+- [Guusggg](https://github.com/Guusggg)
+- [GoryMoon](https://github.com/GoryMoon)
+- [simonkuehling](https://github.com/simonkuehling)
+- [Bobbe](https://github.com/30350n)
+- [awnz](https://github.com/awnz)
+- [joaomnuno](https://github.com/joaomnuno)
+- [Alex9779](https://github.com/Alex9779)
+--------
+
+## Assets
+
+The InvenTree App makes use of the following third party assets
+
+- Icons are provided by [tabler.io](https://tabler.io/icons)
+- Sound files have been sourced from [zapsplat](https://www.zapsplat.com)
+
+--------
diff --git a/assets/image/logo_transparent.png b/assets/image/logo_transparent.png
new file mode 100644
index 0000000..f5045cf
Binary files /dev/null and b/assets/image/logo_transparent.png differ
diff --git a/assets/release_notes.md b/assets/release_notes.md
index 77b02c4..72324c1 100644
--- a/assets/release_notes.md
+++ b/assets/release_notes.md
@@ -1,6 +1,468 @@
-## InvenTree App Release Notes
+### x.xx.x - Month Year
---
+- Support display of custom status codes
+- Fix default values for list sorting
+
+
+### 0.21.2 - January 2026
+---
+
+- Fixes bug which launched camera twice when uploading an attachment
+- Fixed bug related to list sorting and filtering
+
+### 0.21.1 - November 2025
+---
+
+- Fixed app freeze bug after form submission
+
+### 0.21.0 - November 2025
+---
+
+- Support label printing again, fixing issues with new printing API
+- Adds zoom controller for barcode scanner camera view
+- Display default stock location in Part detail page
+- Display stock information in SupplierPart detail page
+
+### 0.20.2 - November 2025
+---
+
+- Fixes URL for reporting issues on GitHub
+- Fix for uploading files against server with self-signed certificates
+
+### 0.20.1 - October 2025
+---
+
+- Bug fix for camera barcode scanner
+
+### 0.20.0 - October 2025
+---
+
+- View pending shipments from the home screen
+- Display detail view for shipments
+- Adds ability to ship pending outgoing shipments
+- Adds ability to mark outgoing shipments as "checked" or "unchecked"
+- Updated translations
+
+### 0.19.3 - September 2025
+---
+
+- Fixes incorrect priority of barcode scanner results
+
+### 0.19.2 - August 2025
+---
+
+- Allow purchase orders to be completed
+- Improved UX across the entire app
+- Fix bug which prevented display of part images for purchase order line items
+
+### 0.19.1 - July 2025
+---
+- Fixes bug related to barcode scanning with certain devices
+
+### 0.19.0 - June 2025
+---
+- Replace barcode scanning library for better performance
+- Display part pricing information
+- Updated theme support
+- Fix broken documentation link
+- Reduce frequency of notification checks
+- Updated translations
+- Add image cropping functionality
+
+### 0.18.1 - April 2025
+---
+- Fix bug associated with handling invalid URLs
+
+### 0.18.0 - April 2025
+---
+- Adds ability to create new companies from the app
+- Allow creation of line items against pending sales orders
+- Support "extra line items" for purchase orders
+- Support "extra line items" for sales orders
+- Display start date for purchase orders
+- Display start date for sales orders
+- Fix scrolling behaviour for some widgets
+- Updated search functionality
+- Updated translations
+
+### 0.17.4 - January 2025
+---
+- Display responsible owner for orders
+- Display completion date for orders
+- Updated translations
+
+### 0.17.3 - January 2025
+---
+
+- Fixes bug which prevent dialog boxes from being dismissed correctly
+- Enable editing of attachment comments
+- Updated translations
+
+### 0.17.2 - December 2024
+---
+
+- Fixed error message when printing a label to a remote machine
+- Prevent notification sounds from pause media playback
+- Display stock expiry information
+- Updated translations
+
+### 0.17.1 - December 2024
+---
+
+- Add support for ManufacturerPart model
+- Support barcode scanning for ManufacturerPart
+- Fix bugs in global search view
+- Fixes barcode scanning bug which prevents scanning of DataMatrix codes
+- Display "destination" information in PurchaseOrder detail view
+- Pre-fill "location" field when receiving items against PurchaseOrder
+- Fix display of part name in PurchaseOrderLineItem list
+- Adds "assigned to me" filter for Purchase Order list
+- Adds "assigned to me" filter for Sales Order list
+- Updated translations
+
+### 0.17.0 - December 2024
+---
+
+- Improved barcode scanning with new scanning library
+- Prevent screen turning off when scanning barcodes
+- Improved support for Stock Item test results
+- Enhanced home-screen display using grid-view
+- Improvements for image uploading
+- Provide "upload image" shortcut on Purchase Order detail view
+- Provide "upload image" shortcut on Sales Order detail view
+- Clearly indicate if a StockItem is unavailable
+- Improved list filtering management
+- Updated translations
+
+### 0.16.5 - September 2024
+---
+
+- Allow blank values to be entered into numerical fields
+- Updated translations
+
+### 0.16.4 - September 2024
+---
+
+- Fixes bug related to printing stock item labels
+
+### 0.16.3 - August 2024
+---
+
+- Fixes bug relating to viewing attachment files
+- Fixes bug relating to uploading attachment files
+
+
+### 0.16.2 - August 2024
+---
+
+- Support "ON_HOLD" status for Purchase Orders
+- Support "ON_HOLD" status for Sales Orders
+- Change base icon package from FontAwesome to TablerIcons
+- Bug fixes for barcode scanning
+- Translation updates
+
+### 0.16.1 - July 2024
+---
+
+- Update base packages for Android
+
+### 0.16.0 - June 2024
+---
+
+- Add support for new file attachments API
+- Drop support for legacy servers with API version < 100
+
+
+### 0.15.0 - June 2024
+---
+
+- Support modern label printing API
+- Improved display of stock item serial numbers
+- Updated translations
+
+### 0.14.3 - April 2024
+---
+
+- Support "active" field for Company model
+- Support "active" field for SupplierPart model
+- Adjustments to barcode scanning workflow
+- Updated translations
+
+### 0.14.2 - February 2024
+---
+
+- Updated error reporting
+- Support for updated server API endpoints
+- Updated translations
+
+### 0.14.1 - January 2024
+---
+
+- Squashing bugs
+
+### 0.14.0 - December 2023
+---
+
+- Adds support for Sales Orders
+- Adds option to pause and resume barcode scanning with camera
+- Adds option for "single shot" barcode scanning with camera
+- Fixes bug when removing entire quantity of a stock item
+- Add line items to purchase orders directly from the app
+- Add line items to purchase order using barcode scanner
+- Add line items to sales orders directly from the app
+- Add line items to sales order using barcode scanner
+- Allocate stock items against existing sales orders
+
+### 0.13.0 - October 2023
+---
+
+- Adds "wedge scanner" mode, allowing use with external barcode readers
+- Add ability to scan in received items using supplier barcodes
+- Store API token, rather than username:password
+- Ensure that user will lose access if token is revoked by server
+- Improve scroll-to-refresh behaviour across multiple widgets
+
+
+### 0.12.8 - September 2023
+---
+
+- Added extra options for transferring stock items
+- Fixes bug where API data was not fetched with correct locale
+
+### 0.12.7 - August 2023
+---
+
+- Bug fix for Supplier Part editing page
+- Bug fix for label printing (blank template names)
+- Updated translations
+
+### 0.12.6 - July 2023
+---
+
+- Enable label printing for stock locations
+- Enable label printing for parts
+- Updated translation support
+- Bug fixes
+
+### 0.12.5 - July 2023
+---
+
+- Adds extra filtering options for stock items
+- Updated translations
+
+### 0.12.4 - July 2023
+---
+
+- Pre-fill stock location when transferring stock amount
+- UX improvements for searching data
+- Updated translations
+
+### - 0.12.3 - June 2023
+---
+
+- Edit part parameters from within the app
+- Increase visibility of stock quantity in widgets
+- Improved filters for stock list
+- Bug fix for editing stock item purchase price
+
+### 0.12.2 - June 2023
+---
+
+- Adds options for configuring screen orientation
+- Improvements to barcode scanning
+- Translation updates
+- Bug fix for scrolling long lists
+
+### 0.12.1 - May 2023
+---
+
+- Fixes bug in purchase order form
+
+### 0.12.0 - April 2023
+---
+
+- Add support for Project Codes
+- Improve purchase order support
+- Fix action button colors
+- Improvements for stock item test result display
+- Added Norwegian translations
+- Fix serial number field when creating stock item
+
+### 0.11.5 - April 2023
+---
+
+- Fix background image transparency for dark mode
+- Fix link to Bill of Materials from Part screen
+- Improvements to supplier part detail screen
+- Add "notes" field to more models
+
+
+### 0.11.4 - April 2023
+---
+
+- Bug fix for stock history widget
+- Improved display of stock history widget
+- Theme improvements for dark mode
+
+### 0.11.3 - April 2023
+---
+
+- Fixes text color in dark mode
+
+### 0.11.2 - April 2023
+---
+
+- Adds "dark mode" display option
+- Add action to issue a purchase order
+- Add action to cancel a purchase order
+- Reimplement periodic checks for notifications
+
+
+### 0.11.1 - April 2023
+---
+
+- Fixes keyboard bug in search widget
+- Adds ability to create new purchase orders directly from the app
+- Adds support for the "contact" field to purchase orders
+- Improved rendering of status codes for stock items
+- Added rendering of status codes for purchase orders
+
+### 0.11.0 - April 2023
+---
+
+- Major UI updates - [see the documentation](https://docs.inventree.org/en/latest/app/app/)
+- Adds globally accessible action button for "search"
+- Adds globally accessible action button for "barcode scan"
+- Implement context actions using floating actions buttons
+- Support barcode scanning for purchase orders
+
+### 0.10.2 - March 2023
+---
+
+- Adds support for proper currency rendering
+- Fix icon for supplier part detail widget
+- Support global search API endpoint
+- Updated translations
+
+### 0.10.1 - February 2023
+---
+
+- Add support for attachments on Companies
+- Fix duplicate scanning of barcodes
+- Updated translations
+
+### 0.10.0 - February 2023
+---
+
+- Add support for Supplier Parts
+- Updated translations
+
+### 0.9.3 - February 2023
+---
+
+- Updates to match latest server API
+- Bug fix for empty HttpResponse from server
+
+### 0.9.2 - December 2022
+---
+
+- Support custom icons for part category
+- Support custom icons for stock location
+- Adjustments to notification messages
+- Assorted bug fixes
+- Updated translations
+
+### 0.9.1 - December 2022
+---
+
+- Bug fixes for custom barcode actions
+- Updated translations
+
+### 0.9.0 - December 2022
+---
+
+- Added support for custom barcodes for Parts
+- Added support for custom barcode for Stock Locations
+- Support Part parameters
+- Add support for structural part categories
+- Add support for structural stock locations
+- Allow deletion of attachments via app
+- Adds option for controlling BOM display
+- Updated translations
+
+
+### 0.8.3 - September 2022
+---
+
+- Display list of assemblies which components are used in
+- Fixes search input bug
+
+### 0.8.2 - August 2022
+---
+
+- Allow serial numbers to be specified when creating new stock items
+- Allow serial numbers to be edited for existing stock items
+- Allow app locale to be changed manually
+- Improved handling of certain errors
+
+### 0.8.1 - August 2022
+---
+
+- Added extra filtering options for PartCategory list
+- Added extra filtering options for StockLocation list
+- Fixed bug related to null widget context
+- Improved error handling and reporting
+
+### 0.8.0 - July 2022
+---
+
+- Display part variants in the part detail view
+- Display Bill of Materials in the part detail view
+- Indicate available quantity in stock detail view
+- Adds configurable filtering to various list views
+- Allow stock location to be "scanned" into another location using barcode
+- Improves server connection status indicator on home screen
+- Display loading indicator during long-running operations
+- Improved error handling and reporting
+
+### 0.7.3 - June 2022
+---
+
+- Adds ability to display link URLs in attachments view
+- Updated translations
+
+### 0.7.2 - June 2022
+---
+
+- Add "quarantined" status flag for stock items
+- Extends attachment support to stock items
+- Extends attachment support to purchase orders
+
+### 0.7.1 - May 2022
+---
+
+- Fixes issue which prevented text input in search window
+- Remove support for legacy stock adjustment API
+- App now requires server API version 20 (or newer)
+- Updated translation files
+
+### 0.7.0 - May 2022
+---
+
+- Refactor home screen display
+- Display notification messages from InvenTree server
+- Fixes duplicated display of units when showing stock quantity
+- Adds ability to locate / identify stock items or locations (requires server plugin)
+- Improve rendering of home screen when server is not connected
+- Adds ability to load global and user settings from the server
+- Translation updates
+
+### 0.6.2 - April 2022
+---
+
+- Fixes issues related to locale support (for specific locales)
+
### 0.6.1 - April 2022
---
diff --git a/crowdin.yml b/crowdin.yml
new file mode 100644
index 0000000..d354ec1
--- /dev/null
+++ b/crowdin.yml
@@ -0,0 +1,3 @@
+files:
+ - source: /lib/l10n/app_en.arb
+ translation: /lib/l10n/%locale_with_underscore%/app_%locale_with_underscore%.arb
diff --git a/docs/android_studio_fvm.png b/docs/android_studio_fvm.png
new file mode 100644
index 0000000..4d497b9
Binary files /dev/null and b/docs/android_studio_fvm.png differ
diff --git a/find_dart_files.py b/find_dart_files.py
new file mode 100644
index 0000000..80fba3a
--- /dev/null
+++ b/find_dart_files.py
@@ -0,0 +1,50 @@
+"""
+This script recursively finds any '.dart' files in the ./lib directory,
+and generates a 'test' file which includes all these files.
+
+This is to ensure that *all* .dart files are included in test coverage.
+By default, source files which are not touched by the unit tests are not included!
+
+Ref: https://github.com/flutter/flutter/issues/27997
+"""
+
+from pathlib import Path
+
+if __name__ == "__main__":
+ dart_files = Path("lib").rglob("*.dart")
+
+ print("Discovering dart files...");
+
+ with open("test/coverage_helper_test.dart", "w") as f:
+ f.write("// ignore_for_file: unused_import\n\n")
+ f.write("// dart format off\n\n")
+
+ skips = [
+ "generated",
+ "l10n",
+ "dsn.dart",
+ ]
+
+ for path in dart_files:
+ path = str(path)
+
+ if any([s in path for s in skips]):
+ continue
+
+ # Remove leading 'lib\' text
+ path = path[4:]
+ path = path.replace("\\", "/")
+ f.write(f'import "package:inventree/{path}";\n')
+
+ f.write("\n\n")
+
+ f.write(
+ "// DO NOT EDIT THIS FILE - it has been auto-generated by 'find_dart_files.py'\n"
+ )
+ f.write(
+ "// It has been created to ensure that *all* source file are included in coverage data\n"
+ )
+
+ f.write('import "package:test/test.dart";\n\n')
+ f.write("// Do not actually test anything!\n")
+ f.write("void main() {}\n")
diff --git a/ios/.gitignore b/ios/.gitignore
new file mode 100644
index 0000000..0ca5a97
--- /dev/null
+++ b/ios/.gitignore
@@ -0,0 +1,34 @@
+**/dgph
+*.mode1v3
+*.mode2v3
+*.moved-aside
+*.pbxuser
+*.perspectivev3
+**/*sync/
+.sconsign.dblite
+.tags*
+**/.vagrant/
+**/DerivedData/
+Icon?
+**/Pods/
+**/.symlinks/
+profile
+xcuserdata
+**/.generated/
+Flutter/App.framework
+Flutter/Flutter.framework
+Flutter/Flutter.podspec
+Flutter/Generated.xcconfig
+Flutter/ephemeral/
+Flutter/app.flx
+Flutter/app.zip
+Flutter/flutter_assets/
+Flutter/flutter_export_environment.sh
+ServiceDefinitions.json
+Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!default.mode1v3
+!default.mode2v3
+!default.pbxuser
+!default.perspectivev3
\ No newline at end of file
diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist
index 8d4492f..7c56964 100644
--- a/ios/Flutter/AppFrameworkInfo.plist
+++ b/ios/Flutter/AppFrameworkInfo.plist
@@ -21,6 +21,6 @@
CFBundleVersion
1.0
MinimumOSVersion
- 9.0
+ 12.0
diff --git a/ios/Flutter/ephemeral/flutter_lldb_helper.py b/ios/Flutter/ephemeral/flutter_lldb_helper.py
new file mode 100644
index 0000000..a88caf9
--- /dev/null
+++ b/ios/Flutter/ephemeral/flutter_lldb_helper.py
@@ -0,0 +1,32 @@
+#
+# Generated file, do not edit.
+#
+
+import lldb
+
+def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict):
+ """Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages."""
+ base = frame.register["x0"].GetValueAsAddress()
+ page_len = frame.register["x1"].GetValueAsUnsigned()
+
+ # Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the
+ # first page to see if handled it correctly. This makes diagnosing
+ # misconfiguration (e.g. missing breakpoint) easier.
+ data = bytearray(page_len)
+ data[0:8] = b'IHELPED!'
+
+ error = lldb.SBError()
+ frame.GetThread().GetProcess().WriteMemory(base, data, error)
+ if not error.Success():
+ print(f'Failed to write into {base}[+{page_len}]', error)
+ return
+
+def __lldb_init_module(debugger: lldb.SBDebugger, _):
+ target = debugger.GetDummyTarget()
+ # Caveat: must use BreakpointCreateByRegEx here and not
+ # BreakpointCreateByName. For some reasons callback function does not
+ # get carried over from dummy target for the later.
+ bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$")
+ bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__))
+ bp.SetAutoContinue(True)
+ print("-- LLDB integration loaded --")
diff --git a/ios/Flutter/ephemeral/flutter_lldbinit b/ios/Flutter/ephemeral/flutter_lldbinit
new file mode 100644
index 0000000..e3ba6fb
--- /dev/null
+++ b/ios/Flutter/ephemeral/flutter_lldbinit
@@ -0,0 +1,5 @@
+#
+# Generated file, do not edit.
+#
+
+command script import --relative-to-command-file flutter_lldb_helper.py
diff --git a/ios/Podfile b/ios/Podfile
index 11c9435..a2be4c0 100644
--- a/ios/Podfile
+++ b/ios/Podfile
@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your projects
-platform :ios, '9.0'
+platform :ios, '15.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
@@ -39,7 +39,7 @@ post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
- config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '9.0'
+ config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0'
end
end
-end
\ No newline at end of file
+end
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index e68a9d4..81f65ca 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -3,13 +3,13 @@
archiveVersion = 1;
classes = {
};
- objectVersion = 46;
+ objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
- 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; };
+ 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; };
97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
@@ -57,6 +57,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
D95D9CD46BE28F7F69DBC0F6 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -93,6 +94,7 @@
3B8B22940C363C2F0DDB698A /* Frameworks */,
);
sourceTree = "";
+ tabWidth = 5;
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
@@ -157,6 +159,9 @@
dependencies = (
);
name = Runner;
+ packageProductDependencies = (
+ 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
+ );
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
@@ -167,26 +172,28 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
- LastUpgradeCheck = 0910;
+ BuildIndependentTargetsInParallel = YES;
+ LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "The Chromium Authors";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
- DevelopmentTeam = A5RYN267BH;
ProvisioningStyle = Automatic;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 3.2";
- developmentRegion = English;
+ developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
- English,
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
+ packageReferences = (
+ 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
+ );
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
@@ -203,7 +210,6 @@
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
- 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
@@ -236,10 +242,12 @@
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
+ "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
@@ -250,6 +258,7 @@
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
@@ -271,47 +280,45 @@
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh",
"${BUILT_PRODUCTS_DIR}/DKImagePickerController/DKImagePickerController.framework",
"${BUILT_PRODUCTS_DIR}/DKPhotoGallery/DKPhotoGallery.framework",
- "${BUILT_PRODUCTS_DIR}/FMDB/FMDB.framework",
- "${BUILT_PRODUCTS_DIR}/MTBBarcodeScanner/MTBBarcodeScanner.framework",
"${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework",
"${BUILT_PRODUCTS_DIR}/Sentry/Sentry.framework",
"${BUILT_PRODUCTS_DIR}/SwiftyGif/SwiftyGif.framework",
- "${BUILT_PRODUCTS_DIR}/audioplayers/audioplayers.framework",
- "${BUILT_PRODUCTS_DIR}/camera/camera.framework",
+ "${BUILT_PRODUCTS_DIR}/audioplayers_darwin/audioplayers_darwin.framework",
+ "${BUILT_PRODUCTS_DIR}/camera_avfoundation/camera_avfoundation.framework",
"${BUILT_PRODUCTS_DIR}/device_info_plus/device_info_plus.framework",
"${BUILT_PRODUCTS_DIR}/file_picker/file_picker.framework",
- "${BUILT_PRODUCTS_DIR}/image_picker/image_picker.framework",
- "${BUILT_PRODUCTS_DIR}/open_file/open_file.framework",
+ "${BUILT_PRODUCTS_DIR}/image_picker_ios/image_picker_ios.framework",
+ "${BUILT_PRODUCTS_DIR}/mobile_scanner/mobile_scanner.framework",
+ "${BUILT_PRODUCTS_DIR}/open_filex/open_filex.framework",
"${BUILT_PRODUCTS_DIR}/package_info_plus/package_info_plus.framework",
- "${BUILT_PRODUCTS_DIR}/path_provider/path_provider.framework",
- "${BUILT_PRODUCTS_DIR}/qr_code_scanner/qr_code_scanner.framework",
+ "${BUILT_PRODUCTS_DIR}/path_provider_foundation/path_provider_foundation.framework",
"${BUILT_PRODUCTS_DIR}/sentry_flutter/sentry_flutter.framework",
- "${BUILT_PRODUCTS_DIR}/shared_preferences/shared_preferences.framework",
- "${BUILT_PRODUCTS_DIR}/sqflite/sqflite.framework",
- "${BUILT_PRODUCTS_DIR}/url_launcher/url_launcher.framework",
+ "${BUILT_PRODUCTS_DIR}/shared_preferences_foundation/shared_preferences_foundation.framework",
+ "${BUILT_PRODUCTS_DIR}/sqflite_darwin/sqflite_darwin.framework",
+ "${BUILT_PRODUCTS_DIR}/url_launcher_ios/url_launcher_ios.framework",
+ "${BUILT_PRODUCTS_DIR}/wakelock_plus/wakelock_plus.framework",
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DKImagePickerController.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DKPhotoGallery.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FMDB.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MTBBarcodeScanner.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Sentry.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyGif.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/audioplayers.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/camera.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/audioplayers_darwin.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/camera_avfoundation.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info_plus.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_picker.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_picker.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/open_file.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_picker_ios.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/mobile_scanner.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/open_filex.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info_plus.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/qr_code_scanner.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_foundation.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sentry_flutter.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqflite.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences_foundation.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqflite_darwin.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher_ios.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/wakelock_plus.framework",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
@@ -357,6 +364,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
@@ -366,14 +374,17 @@
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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -384,6 +395,7 @@
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;
@@ -392,7 +404,8 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ INFOPLIST_KEY_CFBundleDisplayName = InvenTree;
+ IPHONEOS_DEPLOYMENT_TARGET = 17.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
@@ -404,6 +417,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
@@ -415,8 +429,12 @@
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
- LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ INFOPLIST_KEY_CFBundleDisplayName = InvenTree;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.6;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
@@ -432,6 +450,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
@@ -441,14 +460,17 @@
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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -459,6 +481,7 @@
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;
@@ -473,7 +496,8 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ INFOPLIST_KEY_CFBundleDisplayName = InvenTree;
+ IPHONEOS_DEPLOYMENT_TARGET = 17.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -485,6 +509,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
@@ -494,14 +519,17 @@
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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -512,6 +540,7 @@
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;
@@ -520,7 +549,8 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ INFOPLIST_KEY_CFBundleDisplayName = InvenTree;
+ IPHONEOS_DEPLOYMENT_TARGET = 17.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
@@ -532,6 +562,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
@@ -543,8 +574,12 @@
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
- LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ INFOPLIST_KEY_CFBundleDisplayName = InvenTree;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.6;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
@@ -560,6 +595,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
@@ -571,8 +607,12 @@
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
- LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ INFOPLIST_KEY_CFBundleDisplayName = InvenTree;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.6;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
@@ -608,6 +648,20 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
+
+/* Begin XCLocalSwiftPackageReference section */
+ 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
+ isa = XCLocalSwiftPackageReference;
+ relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
+ };
+/* End XCLocalSwiftPackageReference section */
+
+/* Begin XCSwiftPackageProductDependency section */
+ 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
+ isa = XCSwiftPackageProductDependency;
+ productName = FlutterGeneratedPluginSwiftPackage;
+ };
+/* End XCSwiftPackageProductDependency section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}
diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..0c67376
--- /dev/null
+++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index 444bcdb..5db441f 100644
--- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -1,10 +1,28 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..0c67376
--- /dev/null
+++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 3d514f8..5cd05c6 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -12,23 +12,35 @@
6.0
CFBundleLocalizations
- de
- el
+ cs-CZ
+ da-DK
+ de-DE
+ el-GR
en
- es
- fr
- he
- it
- ja
- ko
- nl
- no
- pl
- ru
- sv
- tr
- vi
+ es-ES
+ es-MX
+ fa-IR
+ fi-FI
+ fr-FR
+ he-IL
+ hu-HU
+ id-ID
+ it-IT
+ ja-JP
+ ko-KR
+ nl-NL
+ no-NO
+ pl-PL
+ pt-BR
+ pt-PT
+ ru-RU
+ sl_SI
+ sv-SE
+ th-TH
+ tr-TR
+ vi-VN
zh-CN
+ zh-TW
CFBundleName
InvenTree
@@ -67,5 +79,16 @@
UIViewControllerBasedStatusBarAppearance
+ LSApplicationQueriesSchemes
+
+ http
+ https
+ mailto
+ tel
+
+ CADisableMinimumFrameDurationOnPhone
+
+ UIApplicationSupportsIndirectInputEvents
+
diff --git a/l10n.yaml b/l10n.yaml
index a5b40b9..7b9da73 100644
--- a/l10n.yaml
+++ b/l10n.yaml
@@ -1,4 +1,4 @@
-arb-dir: lib/l10n
+arb-dir: lib/l10n/collected
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-class: I18N
\ No newline at end of file
diff --git a/lib/api.dart b/lib/api.dart
index 71af9cb..61a6379 100644
--- a/lib/api.dart
+++ b/lib/api.dart
@@ -4,31 +4,42 @@ import "dart:io";
import "package:flutter/foundation.dart";
import "package:http/http.dart" as http;
+import "package:http/io_client.dart";
import "package:intl/intl.dart";
-import "package:inventree/app_colors.dart";
-import "package:inventree/app_settings.dart";
-
-import "package:open_file/open_file.dart";
+import "package:inventree/main.dart";
+import "package:inventree/widget/progress.dart";
+import "package:one_context/one_context.dart";
+import "package:open_filex/open_filex.dart";
import "package:cached_network_image/cached_network_image.dart";
import "package:flutter/material.dart";
-import "package:font_awesome_flutter/font_awesome_flutter.dart";
+import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
import "package:flutter_cache_manager/flutter_cache_manager.dart";
-
-import "package:inventree/widget/dialogs.dart";
-import "package:inventree/l10.dart";
-import "package:inventree/inventree/sentry.dart";
-import "package:inventree/inventree/model.dart";
-import "package:inventree/user_profile.dart";
-import "package:inventree/widget/snacks.dart";
import "package:path_provider/path_provider.dart";
+import "package:inventree/api_form.dart";
+import "package:inventree/app_colors.dart";
+import "package:inventree/preferences.dart";
+import "package:inventree/l10.dart";
+import "package:inventree/helpers.dart";
+import "package:inventree/inventree/model.dart";
+import "package:inventree/inventree/notification.dart";
+import "package:inventree/inventree/status_codes.dart";
+import "package:inventree/inventree/sentry.dart";
+import "package:inventree/user_profile.dart";
+import "package:inventree/widget/dialogs.dart";
+import "package:inventree/widget/snacks.dart";
/*
* Class representing an API response from the server
*/
class APIResponse {
-
- APIResponse({this.url = "", this.method = "", this.statusCode = -1, this.error = "", this.data = const {}});
+ APIResponse({
+ this.url = "",
+ this.method = "",
+ this.statusCode = -1,
+ this.error = "",
+ this.data = const {},
+ });
int statusCode = -1;
@@ -77,8 +88,26 @@ class APIResponse {
return [];
}
}
-}
+ /*
+ * Helper function to interpret response, and return a list.
+ * Handles case where the response is paginated, or a complete set of results
+ */
+ List resultsList() {
+ if (isList()) {
+ return asList();
+ } else if (isMap()) {
+ var response = asMap();
+ if (response.containsKey("results")) {
+ return response["results"] as List;
+ } else {
+ return [];
+ }
+ } else {
+ return [];
+ }
+ }
+}
/*
* Custom FileService for caching network images
@@ -86,13 +115,11 @@ class APIResponse {
* so we can accept "dodgy" (e.g. self-signed) certificates
*/
class InvenTreeFileService extends FileService {
-
InvenTreeFileService({HttpClient? client, bool strictHttps = false}) {
_client = client ?? HttpClient();
if (_client != null) {
_client!.badCertificateCallback = (cert, host, port) {
- print("BAD CERTIFICATE CALLBACK FOR IMAGE REQUEST");
return !strictHttps;
};
}
@@ -101,8 +128,10 @@ class InvenTreeFileService extends FileService {
HttpClient? _client;
@override
- Future get(String url,
- {Map? headers}) async {
+ Future get(
+ String url, {
+ Map? headers,
+ }) async {
final Uri resolved = Uri.base.resolve(url);
final HttpClientRequest req = await _client!.getUrl(resolved);
@@ -114,12 +143,17 @@ class InvenTreeFileService extends FileService {
}
final HttpClientResponse httpResponse = await req.close();
+
final http.StreamedResponse _response = http.StreamedResponse(
- httpResponse.timeout(Duration(seconds: 60)), httpResponse.statusCode,
- contentLength: httpResponse.contentLength,
+ httpResponse.timeout(Duration(seconds: 60)),
+ httpResponse.statusCode,
+ contentLength: httpResponse.contentLength < 0
+ ? 0
+ : httpResponse.contentLength,
reasonPhrase: httpResponse.reasonPhrase,
isRedirect: httpResponse.isRedirect,
);
+
return HttpGetResponse(_response);
}
}
@@ -131,31 +165,46 @@ class InvenTreeFileService extends FileService {
* initialised using a username:password combination.
*/
-
+/*
+ * API class which manages all communication with the InvenTree server
+ */
class InvenTreeAPI {
-
factory InvenTreeAPI() {
return _api;
}
InvenTreeAPI._internal();
+ // Ensure we only ever create a single instance of the API class
+ static final InvenTreeAPI _api = InvenTreeAPI._internal();
+
+ // List of callback functions to trigger when the connection status changes
+ List _statusCallbacks = [];
+
+ // Register a callback function to be notified when the connection status changes
+ void registerCallback(Function() func) => _statusCallbacks.add(func);
+
+ void _connectionStatusChanged() {
+ for (Function() func in _statusCallbacks) {
+ // Call the function
+ func();
+ }
+ }
+
// Minimum required API version for server
- static const _minApiVersion = 7;
+ // 2023-03-04
+ static const _minApiVersion = 100;
bool _strictHttps = false;
// Endpoint for requesting an API token
- static const _URL_GET_TOKEN = "user/token/";
-
- static const _URL_GET_ROLES = "user/roles/";
-
- // Base URL for InvenTree API e.g. http://192.168.120.10:8000
- String _BASE_URL = "";
+ static const _URL_TOKEN = "user/token/";
+ static const _URL_ROLES = "user/roles/";
+ static const _URL_ME = "user/me/";
// Accessors for various url endpoints
String get baseUrl {
- String url = _BASE_URL;
+ String url = profile?.server ?? "";
if (!url.endsWith("/")) {
url += "/";
@@ -165,7 +214,6 @@ class InvenTreeAPI {
}
String _makeUrl(String url) {
-
// Strip leading slash
if (url.startsWith("/")) {
url = url.substring(1, url.length);
@@ -193,23 +241,32 @@ class InvenTreeAPI {
UserProfile? profile;
+ // Available user roles are loaded when connecting to the server
Map roles = {};
- // Authentication token (initially empty, must be requested)
- String _token = "";
+ // Available user permissions are loaded when connecting to the server
+ Map permissions = {};
+
+ // Profile authentication token
+ String get token => profile?.token ?? "";
+
+ bool get hasToken => token.isNotEmpty;
+
+ String? get serverAddress {
+ return profile?.server;
+ }
/*
* Check server connection and display messages if not connected.
* Useful as a precursor check before performing operations.
*/
- bool checkConnection(BuildContext context) {
- // Firstly, is the server connected?
+ bool checkConnection() {
+ // Is the server connected?
if (!isConnected()) {
-
showSnackIcon(
L10().notConnected,
success: false,
- icon: FontAwesomeIcons.server
+ icon: TablerIcons.server,
);
return false;
@@ -219,23 +276,86 @@ class InvenTreeAPI {
return true;
}
- // Server instance information
- String instance = "";
+ // Map of user information
+ Map userInfo = {};
- // Server version information
- String _version = "";
+ String get username => (userInfo["username"] ?? "") as String;
- // API version of the connected server
- int _apiVersion = 1;
+ int get userId => (userInfo["pk"] ?? -1) as int;
- int get apiVersion => _apiVersion;
+ // Map of server information
+ Map serverInfo = {};
- // Are plugins enabled on the server?
- bool _pluginsEnabled = false;
+ String get serverInstance => (serverInfo["instance"] ?? "") as String;
+ String get serverVersion => (serverInfo["version"] ?? "") as String;
+ int get apiVersion => (serverInfo["apiVersion"] ?? 1) as int;
- // True plugin support requires API v34 or newer
- // Returns True only if the server API version is new enough, and plugins are enabled
- bool pluginsEnabled() => apiVersion >= 34 && _pluginsEnabled;
+ // Consolidated search request API v102 or newer
+ bool get supportsConsolidatedSearch => apiVersion >= 102;
+
+ // ReturnOrder supports API v104 or newer
+ bool get supportsReturnOrders => apiVersion >= 104;
+
+ // "Contact" model exposed to API
+ bool get supportsContactModel => apiVersion >= 104;
+
+ // Status label endpoints API v105 or newer
+ bool get supportsStatusLabelEndpoints => apiVersion >= 105;
+
+ // Regex search API v106 or newer
+ bool get supportsRegexSearch => apiVersion >= 106;
+
+ // Order barcodes API v107 or newer
+ bool get supportsOrderBarcodes => apiVersion >= 107;
+
+ // Project codes require v109 or newer
+ bool get supportsProjectCodes => apiVersion >= 109;
+
+ // Does the server support extra fields on stock adjustment actions?
+ bool get supportsStockAdjustExtraFields => apiVersion >= 133;
+
+ // Does the server support receiving items against a PO using barcodes?
+ bool get supportsBarcodePOReceiveEndpoint => apiVersion >= 139;
+
+ // Does the server support adding line items to a PO using barcodes?
+ bool get supportsBarcodePOAddLineEndpoint => apiVersion >= 153;
+
+ // Does the server support allocating stock to sales order using barcodes?
+ bool get supportsBarcodeSOAllocateEndpoint => apiVersion >= 160;
+
+ // Does the server support the "modern" test results API
+ // Ref: https://github.com/inventree/InvenTree/pull/6430/
+ bool get supportsModernTestResults => apiVersion >= 169;
+
+ // Does the server support "null" top-level filtering for PartCategory and StockLocation endpoints?
+ bool get supportsNullTopLevelFiltering => apiVersion < 174;
+
+ // Does the server support "active" status on Company and SupplierPart API endpoints?
+ bool get supportsCompanyActiveStatus => apiVersion >= 189;
+
+ // Does the server support the "modern" (consolidated) label printing API?
+ bool get supportsModernLabelPrinting => apiVersion >= 201;
+
+ // Does the server support the "modern" (consolidated) attachment API?
+ // Ref: https://github.com/inventree/InvenTree/pull/7420
+ bool get supportsModernAttachments => apiVersion >= 207;
+
+ bool get supportsUserPermissions => apiVersion >= 207;
+
+ // Does the server support the "destination" field on the PurchaseOrder model?
+ // Ref: https://github.com/inventree/InvenTree/pull/8403
+ bool get supportsPurchaseOrderDestination => apiVersion >= 276;
+
+ // Does the server support the "start_date" field for orders?
+ // Ref: https://github.com/inventree/InvenTree/pull/8966
+ bool get supportsStartDate => apiVersion >= 306;
+
+ // Supports separate search against "supplier" / "customer" / "manufacturer"
+ bool get supportsSplitCompanySearch => apiVersion >= 315;
+
+ // Does the server support the "modern" (consolidated) parameter API?
+ // Ref: https://github.com/inventree/InvenTree/pull/10699
+ bool get supportsModernParameters => apiVersion >= 429;
// Cached list of plugins (refreshed when we connect to the server)
List _plugins = [];
@@ -263,62 +383,81 @@ class InvenTreeAPI {
// Test if the provided plugin mixin is supported by any active plugins
bool supportsMixin(String mixin) => getPlugins(mixin: mixin).isNotEmpty;
- // Getter for server version information
- String get version => _version;
-
// Connection status flag - set once connection has been validated
bool _connected = false;
bool _connecting = false;
bool isConnected() {
- return profile != null && _connected && baseUrl.isNotEmpty && _token.isNotEmpty;
+ return profile != null && _connected && baseUrl.isNotEmpty && hasToken;
}
bool isConnecting() {
return !isConnected() && _connecting;
}
- // Ensure we only ever create a single instance of the API class
- static final InvenTreeAPI _api = InvenTreeAPI._internal();
+ /*
+ * Perform the required login steps, in sequence.
+ * Internal function, called by connectToServer()
+ *
+ * Performs the following steps:
+ *
+ * 1. Check the api/ endpoint to see if the sever exists
+ * 2. If no token available, perform user authentication
+ * 2. Check the api/user/me/ endpoint to see if the user is authenticated
+ * 3. If not authenticated, purge token, and exit
+ * 4. Request user roles
+ * 5. Request information on available plugins
+ */
+ Future _connectToServer() async {
+ if (!await _checkServer()) {
+ return false;
+ }
- // API endpoint for receiving purchase order line items was introduced in v12
- bool supportPoReceive() {
- return apiVersion >= 12;
- }
+ if (!hasToken) {
+ return false;
+ }
- // "Modern" API transactions were implemented in API v14
- bool supportModernStockTransactions() {
- return apiVersion >= 14;
+ if (!await _checkAuth()) {
+ showServerError(
+ _URL_ME,
+ L10().serverNotConnected,
+ L10().serverAuthenticationError,
+ );
+
+ // Invalidate the token
+ if (profile != null) {
+ profile!.token = "";
+ await UserProfileDBManager().updateProfile(profile!);
+ }
+
+ return false;
+ }
+
+ if (!await _fetchRoles()) {
+ return false;
+ }
+
+ if (!await _fetchPlugins()) {
+ return false;
+ }
+
+ // Finally, connected
+ return true;
}
/*
- * Connect to the remote InvenTree server:
- *
- * - Check that the InvenTree server exists
- * - Request user token from the server
- * - Request user roles from the server
+ * Check that the remote server is available.
+ * Ping the api/ endpoint, which does not require user authentication
*/
- Future _connect() async {
-
- if (profile == null) return false;
-
+ Future _checkServer() async {
String address = profile?.server ?? "";
- String username = profile?.username ?? "";
- String password = profile?.password ?? "";
- address = address.trim();
- username = username.trim();
- password = password.trim();
-
- // Cache the "strictHttps" setting, so we can use it later without async requirement
- _strictHttps = await InvenTreeSettingsManager().getValue(INV_STRICT_HTTPS, false) as bool;
-
- if (address.isEmpty || username.isEmpty || password.isEmpty) {
+ if (address.isEmpty) {
showSnackIcon(
L10().incompleteDetails,
- icon: FontAwesomeIcons.exclamationCircle,
- success: false
+ icon: TablerIcons.exclamation_circle,
+ success: false,
);
return false;
}
@@ -326,51 +465,38 @@ class InvenTreeAPI {
if (!address.endsWith("/")) {
address = address + "/";
}
- /* TODO: Better URL validation
- * - If not a valid URL, return error
- * - If no port supplied, append a default port
- */
- _BASE_URL = address;
+ // Cache the "strictHttps" setting, so we can use it later without async requirement
+ _strictHttps =
+ await InvenTreeSettingsManager().getValue(INV_STRICT_HTTPS, false)
+ as bool;
- // Clear the list of available plugins
- _plugins.clear();
+ debug("Connecting to ${apiUrl}");
- print("Connecting to ${apiUrl} -> username=${username}");
-
- APIResponse response;
-
- response = await get("", expectedStatusCode: 200);
+ APIResponse response = await get("", expectedStatusCode: 200);
if (!response.successful()) {
- showStatusCodeError(response.statusCode);
- return false;
- }
-
- var data = response.asMap();
-
- // We expect certain response from the server
- if (!data.containsKey("server") || !data.containsKey("version") || !data.containsKey("instance")) {
-
- showServerError(
- L10().missingData,
- L10().serverMissingData,
+ debug("Server returned invalid response: ${response.statusCode}");
+ showStatusCodeError(
+ apiUrl,
+ response.statusCode,
+ details: response.data.toString(),
);
+ return false;
+ }
+
+ Map _data = response.asMap();
+
+ serverInfo = {..._data};
+
+ if (serverVersion.isEmpty) {
+ showServerError(apiUrl, L10().missingData, L10().serverMissingData);
return false;
}
- // Record server information
- _version = (data["version"] ?? "") as String;
- instance = (data["instance"] ?? "") as String;
-
- // Default API version is 1 if not provided
- _apiVersion = (data["apiVersion"] ?? 1) as int;
- _pluginsEnabled = (data["plugins_enabled"] ?? false) as bool;
-
- if (_apiVersion < _minApiVersion) {
-
- String message = L10().serverApiVersion + ": ${_apiVersion}";
+ if (apiVersion < _minApiVersion) {
+ String message = L10().serverApiVersion + ": ${apiVersion}";
message += "\n";
message += L10().serverApiRequired + ": ${_minApiVersion}";
@@ -379,147 +505,241 @@ class InvenTreeAPI {
message += "Ensure your InvenTree server version is up to date!";
- showServerError(
- L10().serverOld,
- message,
- );
+ showServerError(apiUrl, L10().serverOld, message);
return false;
}
- /**
- * Request user token information from the server
- * This is the stage that we check username:password credentials!
- */
- // Clear the existing token value
- _token = "";
+ // At this point, we have a server which is responding
+ return true;
+ }
- response = await get(_URL_GET_TOKEN);
+ /*
+ * Check that the user is authenticated
+ * Fetch the user information
+ */
+ Future _checkAuth() async {
+ debug("Checking user auth @ ${_URL_ME}");
- // Invalid response
- if (!response.successful()) {
+ userInfo.clear();
- switch (response.statusCode) {
- case 401:
- case 403:
- showServerError(
- L10().serverAuthenticationError,
- L10().invalidUsernamePassword,
- );
- break;
- default:
- showStatusCodeError(response.statusCode);
- break;
+ final response = await get(_URL_ME);
+
+ if (response.successful() && response.statusCode == 200) {
+ userInfo = response.asMap();
+ return true;
+ } else {
+ debug(
+ "Auth request failed: Server returned status ${response.statusCode}",
+ );
+ if (response.data != null) {
+ debug("Server response: ${response.data.toString()}");
}
return false;
}
+ }
- data = response.asMap();
+ /*
+ * Fetch a token from the server,
+ * with a temporary authentication header
+ */
+ Future fetchToken(
+ UserProfile userProfile,
+ String username,
+ String password,
+ ) async {
+ debug("Fetching user token from ${userProfile.server}");
+
+ profile = userProfile;
+
+ // Form a name to request the token with
+ String platform_name = "inventree-mobile-app";
+
+ final deviceInfo = await getDeviceInfo();
+
+ if (Platform.isAndroid) {
+ platform_name += "-android";
+ } else if (Platform.isIOS) {
+ platform_name += "-ios";
+ } else if (Platform.isMacOS) {
+ platform_name += "-macos";
+ } else if (Platform.isLinux) {
+ platform_name += "-linux";
+ } else if (Platform.isWindows) {
+ platform_name += "-windows";
+ }
+
+ if (deviceInfo.containsKey("name")) {
+ platform_name += "-" + (deviceInfo["name"] as String);
+ }
+
+ if (deviceInfo.containsKey("model")) {
+ platform_name += "-" + (deviceInfo["model"] as String);
+ }
+
+ if (deviceInfo.containsKey("systemVersion")) {
+ platform_name += "-" + (deviceInfo["systemVersion"] as String);
+ }
+
+ // Construct auth header from username and password
+ String authHeader =
+ "Basic " + base64Encode(utf8.encode("${username}:${password}"));
+
+ // Perform request to get a token
+ final response = await get(
+ _URL_TOKEN,
+ params: {"name": platform_name},
+ headers: {HttpHeaders.authorizationHeader: authHeader},
+ );
+
+ // Invalid response
+ if (!response.successful()) {
+ switch (response.statusCode) {
+ case 401:
+ case 403:
+ showServerError(
+ apiUrl,
+ L10().serverAuthenticationError,
+ L10().invalidUsernamePassword,
+ );
+ default:
+ showStatusCodeError(apiUrl, response.statusCode);
+ }
+
+ debug("Token request failed: STATUS ${response.statusCode}");
+
+ if (response.data != null) {
+ debug("Response data: ${response.data.toString()}");
+ }
+ }
+
+ final data = response.asMap();
if (!data.containsKey("token")) {
showServerError(
- L10().tokenMissing,
- L10().tokenMissingFromResponse,
+ apiUrl,
+ L10().tokenMissing,
+ L10().tokenMissingFromResponse,
);
-
- return false;
}
- // Return the received token
- _token = (data["token"] ?? "") as String;
- print("Received token - $_token");
+ // Save the token to the user profile
+ userProfile.token = (data["token"] ?? "") as String;
- // Request user role information (async)
- getUserRoles();
+ debug("Received token from server: ${userProfile.token}");
- // Request plugin information (async)
- getPluginInformation();
-
- // Ok, probably pretty good...
- return true;
+ await UserProfileDBManager().updateProfile(userProfile);
+ return response;
}
void disconnectFromServer() {
- print("InvenTreeAPI().disconnectFromServer()");
+ debug("API : disconnectFromServer()");
_connected = false;
_connecting = false;
- _token = "";
profile = null;
+
+ // Clear received settings
+ _globalSettings.clear();
+ _userSettings.clear();
+
+ roles.clear();
+ _plugins.clear();
+ serverInfo.clear();
+ _connectionStatusChanged();
}
- Future connectToServer() async {
-
+ /* Public facing connection function.
+ */
+ Future connectToServer(UserProfile prf) async {
// Ensure server is first disconnected
disconnectFromServer();
- // Load selected profile
- profile = await UserProfileDBManager().getSelectedProfile();
+ profile = prf;
if (profile == null) {
showSnackIcon(
- L10().profileSelect,
- success: false,
- icon: FontAwesomeIcons.exclamationCircle
+ L10().profileSelect,
+ success: false,
+ icon: TablerIcons.exclamation_circle,
);
return false;
}
+ // Cancel notification timer
+ _notification_timer?.cancel();
+
_connecting = true;
+ _connectionStatusChanged();
- _connected = await _connect();
-
+ // Perform the actual connection routine
+ _connected = await _connectToServer();
_connecting = false;
if (_connected) {
showSnackIcon(
L10().serverConnected,
- icon: FontAwesomeIcons.server,
+ icon: TablerIcons.server,
success: true,
);
+
+ if (_notification_timer == null) {
+ debug("starting notification timer");
+ _notification_timer = Timer.periodic(Duration(seconds: 60), (timer) {
+ _refreshNotifications();
+ });
+ }
}
+ _connectionStatusChanged();
+
+ fetchStatusCodeData();
+
return _connected;
}
-
- Future getUserRoles() async {
-
+ /*
+ * Request the user roles (permissions) from the InvenTree server
+ */
+ Future _fetchRoles() async {
roles.clear();
- print("Requesting user role data");
+ debug("API: Requesting user role data");
- // Next we request the permissions assigned to the current user
- // Note: 2021-02-27 this "roles" feature for the API was just introduced.
- // Any "older" version of the server allows any API method for any logged in user!
- // We will return immediately, but request the user roles in the background
-
- final response = await get(_URL_GET_ROLES, expectedStatusCode: 200);
+ final response = await get(_URL_ROLES, expectedStatusCode: 200);
if (!response.successful()) {
- return;
+ return false;
}
var data = response.asMap();
- if (data.containsKey("roles")) {
- // Save a local copy of the user roles
- roles = (response.data["roles"] ?? {}) as Map;
+ if (!data.containsKey("roles")) {
+ roles = {};
+ permissions = {};
+
+ showServerError(apiUrl, L10().serverError, L10().errorUserRoles);
+ return false;
}
+
+ roles = (data["roles"] ?? {}) as Map;
+
+ if (supportsUserPermissions && data.containsKey("permissions")) {
+ permissions = (data["permissions"] ?? {}) as Map;
+ } else {
+ permissions = {};
+ }
+
+ return true;
}
// Request plugin information from the server
- Future getPluginInformation() async {
+ Future _fetchPlugins() async {
+ _plugins.clear();
- // The server does not support plugins, or they are not enabled
- if (!pluginsEnabled()) {
- _plugins.clear();
- return;
- }
-
- print("Requesting plugin information");
+ debug("API: getPluginInformation()");
// Request a list of plugins from the server
final List results = await InvenTreePlugin().list();
@@ -532,26 +752,33 @@ class InvenTreeAPI {
}
}
}
+
+ return true;
}
- bool checkPermission(String role, String permission) {
- /*
- * Check if the user has the given role.permission assigned
- *e
- * e.g. "part", "change"
- */
+ /*
+ * Check if the user has the given role.permission assigned
+ * e.g. "sales_order", "change"
+ */
+ bool checkRole(String role, String permission) {
+ if (!_connected) {
+ return false;
+ }
// If we do not have enough information, assume permission is allowed
if (roles.isEmpty) {
+ debug("checkRole - no roles defined!");
return true;
}
if (!roles.containsKey(role)) {
+ debug("checkRole - role '$role' not found!");
return true;
}
if (roles[role] == null) {
- return true;
+ debug("checkRole - role '$role' is null!");
+ return false;
}
try {
@@ -562,7 +789,16 @@ class InvenTreeAPI {
// Ignore TypeError
} else {
// Unknown error - report it!
- sentryReportError(error, stackTrace);
+ sentryReportError(
+ "api.checkRole",
+ error,
+ stackTrace,
+ context: {
+ "role": role,
+ "permission": permission,
+ "error": error.toString(),
+ },
+ );
}
// Unable to determine permission - assume true?
@@ -570,10 +806,61 @@ class InvenTreeAPI {
}
}
+ /*
+ * Check if the user has the particular model permission assigned
+ * e.g. "company", "add"
+ */
+ bool checkPermission(String model, String permission) {
+ if (!_connected) {
+ return false;
+ }
+
+ if (permissions.isEmpty) {
+ // Not enough information available - default to True
+ return true;
+ }
+
+ if (!permissions.containsKey(model)) {
+ debug("checkPermission - model '$model' not found!");
+ return false;
+ }
+
+ if (permissions[model] == null) {
+ debug("checkPermission - model '$model' is null!");
+ return false;
+ }
+
+ try {
+ List perms = List.from(permissions[model] as List);
+ return perms.contains(permission);
+ } catch (error, stackTrace) {
+ if (error is TypeError) {
+ // Ignore TypeError
+ } else {
+ // Unknown error - report it!
+ sentryReportError(
+ "api.checkPermission",
+ error,
+ stackTrace,
+ context: {
+ "model": model,
+ "permission": permission,
+ "error": error.toString(),
+ },
+ );
+ }
+
+ // Unable to determine permission - assume true?
+ return true;
+ }
+ }
// Perform a PATCH request
- Future patch(String url, {Map body = const {}, int? expectedStatusCode}) async {
-
+ Future patch(
+ String url, {
+ Map body = const {},
+ int? expectedStatusCode,
+ }) async {
Map _body = body;
HttpClientRequest? request = await apiRequest(url, "PATCH");
@@ -583,14 +870,14 @@ class InvenTreeAPI {
return APIResponse(
url: url,
method: "PATCH",
- error: "HttpClientRequest is null"
+ error: "HttpClientRequest is null",
);
}
return completeRequest(
request,
data: json.encode(_body),
- statusCode: expectedStatusCode
+ statusCode: expectedStatusCode,
);
}
@@ -598,14 +885,12 @@ class InvenTreeAPI {
* Download a file from the given URL
*/
Future downloadFile(String url, {bool openOnDownload = true}) async {
+ if (url.isEmpty) {
+ // No URL provided for download
+ return;
+ }
- showSnackIcon(
- L10().downloading,
- icon: FontAwesomeIcons.download,
- success: true
- );
-
- // Find the local downlods directory
+ // Find the local downloads directory
final Directory dir = await getTemporaryDirectory();
String filename = url.split("/").last;
@@ -615,48 +900,52 @@ class InvenTreeAPI {
Uri? _uri = Uri.tryParse(makeUrl(url));
if (_uri == null) {
- showServerError(L10().invalidHost, L10().invalidHostDetails);
+ showServerError(url, L10().invalidHost, L10().invalidHostDetails);
return;
}
if (_uri.host.isEmpty) {
- showServerError(L10().invalidHost, L10().invalidHostDetails);
+ showServerError(url, L10().invalidHost, L10().invalidHostDetails);
return;
}
HttpClientRequest? _request;
- final bool strictHttps = await InvenTreeSettingsManager().getValue(INV_STRICT_HTTPS, false) as bool;
+ final bool strictHttps =
+ await InvenTreeSettingsManager().getValue(INV_STRICT_HTTPS, false)
+ as bool;
- var client = createClient(strictHttps: strictHttps);
+ var client = createClient(url, strictHttps: strictHttps);
+
+ showLoadingOverlay();
// Attempt to open a connection to the server
try {
- _request = await client.openUrl("GET", _uri).timeout(Duration(seconds: 10));
+ _request = await client
+ .openUrl("GET", _uri)
+ .timeout(Duration(seconds: 10));
// Set headers
- _request.headers.set(HttpHeaders.authorizationHeader, _authorizationHeader());
- _request.headers.set(HttpHeaders.acceptHeader, "application/json");
- _request.headers.set(HttpHeaders.contentTypeHeader, "application/json");
- _request.headers.set(HttpHeaders.acceptLanguageHeader, Intl.getCurrentLocale());
-
+ defaultHeaders().forEach((key, value) {
+ _request?.headers.set(key, value);
+ });
} on SocketException catch (error) {
- print("SocketException at ${url}: ${error.toString()}");
- showServerError(L10().connectionRefused, error.toString());
+ debug("SocketException at ${url}: ${error.toString()}");
+ showServerError(url, L10().connectionRefused, error.toString());
return;
} on TimeoutException {
- print("TimeoutException at ${url}");
- showTimeoutError();
+ debug("TimeoutException at ${url}");
+ showTimeoutError(url);
return;
} on HandshakeException catch (error) {
- print("HandshakeException at ${url}:");
- print(error.toString());
- showServerError(L10().serverCertificateError, error.toString());
+ debug("HandshakeException at ${url}:");
+ debug(error.toString());
+ showServerError(url, L10().serverCertificateError, error.toString());
return;
} catch (error, stackTrace) {
- print("Server error at ${url}: ${error.toString()}");
- showServerError(L10().serverError, error.toString());
- sentryReportError(error, stackTrace);
+ debug("Server error at ${url}: ${error.toString()}");
+ showServerError(url, L10().serverError, error.toString());
+ sentryReportError("api.downloadFile : client.openUrl", error, stackTrace);
return;
}
@@ -671,36 +960,59 @@ class InvenTreeAPI {
await localFile.writeAsBytes(bytes);
if (openOnDownload) {
- OpenFile.open(local_path);
+ hideLoadingOverlay();
+ OpenFilex.open(local_path);
}
} else {
- showStatusCodeError(response.statusCode);
+ showStatusCodeError(url, response.statusCode);
}
} on SocketException catch (error) {
- showServerError(L10().connectionRefused, error.toString());
+ showServerError(url, L10().connectionRefused, error.toString());
} on TimeoutException {
- showTimeoutError();
- } catch (error) {
- print("Error downloading image:");
- print(error.toString());
- showServerError(L10().downloadError, error.toString());
+ showTimeoutError(url);
+ } catch (error, stackTrace) {
+ debug("Error downloading image:");
+ debug(error.toString());
+ showServerError(url, L10().downloadError, error.toString());
+ sentryReportError(
+ "api.downloadFile : client.closeRequest",
+ error,
+ stackTrace,
+ );
}
+
+ hideLoadingOverlay();
}
/*
* Upload a file to the given URL
*/
- Future uploadFile(String url, File f,
- {String name = "attachment", String method="POST", Map? fields}) async {
- var _url = makeApiUrl(url);
+ Future uploadFile(
+ String url,
+ File f, {
+ String name = "attachment",
+ String method = "POST",
+ Map? fields,
+ }) async {
+ bool strictHttps = await InvenTreeSettingsManager().getBool(
+ INV_STRICT_HTTPS,
+ false,
+ );
- var request = http.MultipartRequest(method, Uri.parse(_url));
+ // Create an IOClient wrapper for sending the MultipartRequest
+ final ioClient = IOClient(createClient(url, strictHttps: strictHttps));
- request.headers.addAll(defaultHeaders());
+ final uri = Uri.parse(makeApiUrl(url));
+ final request = http.MultipartRequest(method, uri);
+ // Default headers
+ defaultHeaders().forEach((key, value) {
+ request.headers[key] = value;
+ });
+
+ // Optional fields
if (fields != null) {
fields.forEach((String key, dynamic value) {
-
if (value == null) {
request.fields[key] = "";
} else {
@@ -709,65 +1021,67 @@ class InvenTreeAPI {
});
}
+ // Add file to upload
var _file = await http.MultipartFile.fromPath(name, f.path);
-
request.files.add(_file);
- APIResponse response = APIResponse(
- url: url,
- method: method,
- );
+ // Construct a response object to return
+ APIResponse response = APIResponse(url: url, method: method);
String jsondata = "";
try {
- var httpResponse = await request.send().timeout(Duration(seconds: 120));
+ var streamedResponse = await ioClient
+ .send(request)
+ .timeout(Duration(seconds: 120));
+ final httpResponse = await http.Response.fromStream(streamedResponse);
response.statusCode = httpResponse.statusCode;
- jsondata = await httpResponse.stream.bytesToString();
+ jsondata = httpResponse.body;
response.data = json.decode(jsondata);
// Report a server-side error
- if (response.statusCode >= 500) {
+ if (response.statusCode == 500) {
sentryReportMessage(
- "Server error in uploadFile()",
- context: {
- "url": url,
- "method": request.method,
- "name": name,
- "statusCode": response.statusCode.toString(),
- "requestHeaders": request.headers.toString(),
- "responseHeaders": httpResponse.headers.toString(),
- }
+ "Server error in uploadFile()",
+ context: {
+ "url": url,
+ "method": request.method,
+ "name": name,
+ "statusCode": response.statusCode.toString(),
+ "requestHeaders": request.headers.toString(),
+ "responseHeaders": httpResponse.headers.toString(),
+ },
);
}
} on SocketException catch (error) {
- showServerError(L10().connectionRefused, error.toString());
+ showServerError(url, L10().connectionRefused, error.toString());
response.error = "SocketException";
response.errorDetail = error.toString();
} on FormatException {
showServerError(
+ url,
L10().formatException,
- L10().formatExceptionJson + ":\n${jsondata}"
+ L10().formatExceptionJson + ":\n${jsondata}",
);
sentryReportMessage(
- "Error decoding JSON response from server",
- context: {
- "url": url,
- "statusCode": response.statusCode.toString(),
- "data": jsondata,
- }
+ "Error decoding JSON response from server",
+ context: {
+ "method": "uploadFile",
+ "url": url,
+ "statusCode": response.statusCode.toString(),
+ "data": jsondata,
+ },
);
-
} on TimeoutException {
- showTimeoutError();
+ showTimeoutError(url);
response.error = "TimeoutException";
} catch (error, stackTrace) {
- showServerError(L10().serverError, error.toString());
- sentryReportError(error, stackTrace);
+ showServerError(url, L10().serverError, error.toString());
+ sentryReportError("api.uploadFile", error, stackTrace);
response.error = "UnknownError";
response.errorDetail = error.toString();
}
@@ -781,16 +1095,19 @@ class InvenTreeAPI {
* We send this with the currently selected "locale",
* so that (hopefully) the field messages are correctly translated
*/
- Future options(String url) async {
-
- HttpClientRequest? request = await apiRequest(url, "OPTIONS");
+ Future options(
+ String url, {
+ Map params = const {},
+ }) async {
+ HttpClientRequest? request = await apiRequest(
+ url,
+ "OPTIONS",
+ urlParams: params,
+ );
if (request == null) {
// Return an "invalid" APIResponse
- return APIResponse(
- url: url,
- method: "OPTIONS"
- );
+ return APIResponse(url: url, method: "OPTIONS");
}
return completeRequest(request);
@@ -800,42 +1117,80 @@ class InvenTreeAPI {
* Perform a HTTP POST request
* Returns a json object (or null if unsuccessful)
*/
- Future post(String url, {Map body = const {}, int? expectedStatusCode=201}) async {
-
+ Future post(
+ String url, {
+ Map body = const {},
+ int? expectedStatusCode = 201,
+ }) async {
HttpClientRequest? request = await apiRequest(url, "POST");
if (request == null) {
// Return an "invalid" APIResponse
- return APIResponse(
- url: url,
- method: "POST"
- );
+ return APIResponse(url: url, method: "POST");
}
return completeRequest(
request,
data: json.encode(body),
- statusCode: expectedStatusCode
+ statusCode: expectedStatusCode,
);
}
- HttpClient createClient({bool strictHttps = false}) {
+ /*
+ * Perform a request to link a custom barcode to a particular item
+ */
+ Future linkBarcode(Map body) async {
+ HttpClientRequest? request = await apiRequest("/barcode/link/", "POST");
+ if (request == null) {
+ return false;
+ }
+
+ final response = await completeRequest(
+ request,
+ data: json.encode(body),
+ statusCode: 200,
+ );
+
+ return response.isValid() && response.statusCode == 200;
+ }
+
+ /*
+ * Perform a request to unlink a custom barcode from a particular item
+ */
+ Future unlinkBarcode(Map body) async {
+ HttpClientRequest? request = await apiRequest("/barcode/unlink/", "POST");
+
+ if (request == null) {
+ return false;
+ }
+
+ final response = await completeRequest(
+ request,
+ data: json.encode(body),
+ statusCode: 200,
+ );
+
+ return response.isValid() && response.statusCode == 200;
+ }
+
+ HttpClient createClient(String url, {bool strictHttps = false}) {
var client = HttpClient();
- client.badCertificateCallback = (X509Certificate cert, String host, int port) {
+ client.badCertificateCallback =
+ (X509Certificate cert, String host, int port) {
+ if (strictHttps) {
+ showServerError(
+ url,
+ L10().serverCertificateError,
+ L10().serverCertificateInvalid,
+ );
+ return false;
+ }
- if (strictHttps) {
- showServerError(
- L10().serverCertificateError,
- L10().serverCertificateInvalid,
- );
- return false;
- }
-
- // Strict HTTPs not enforced, so we'll ignore the bad cert
- return true;
- };
+ // Strict HTTPs not enforced, so we'll ignore the bad cert
+ return true;
+ };
// Set the connection timeout
client.connectionTimeout = Duration(seconds: 30);
@@ -850,10 +1205,19 @@ class InvenTreeAPI {
* @param method is the HTTP method e.g. "POST" / "PATCH" / "GET" etc;
* @param params is the request parameters
*/
- Future apiRequest(String url, String method, {Map urlParams = const {}}) async {
-
+ Future apiRequest(
+ String url,
+ String method, {
+ Map urlParams = const {},
+ Map headers = const {},
+ }) async {
var _url = makeApiUrl(url);
+ if (_url.isEmpty) {
+ showServerError(url, L10().invalidHost, L10().invalidHostDetails);
+ return null;
+ }
+
// Add any required query parameters to the URL using ?key=value notation
if (urlParams.isNotEmpty) {
String query = "?";
@@ -870,140 +1234,171 @@ class InvenTreeAPI {
Uri? _uri = Uri.tryParse(_url);
- if (_uri == null) {
- showServerError(L10().invalidHost, L10().invalidHostDetails);
- return null;
- }
-
- if (_uri.host.isEmpty) {
- showServerError(L10().invalidHost, L10().invalidHostDetails);
+ if (_uri == null || _uri.host.isEmpty) {
+ showServerError(_url, L10().invalidHost, L10().invalidHostDetails);
return null;
}
HttpClientRequest? _request;
- final bool strictHttps = await InvenTreeSettingsManager().getValue(INV_STRICT_HTTPS, false) as bool;
+ final bool strictHttps =
+ await InvenTreeSettingsManager().getValue(INV_STRICT_HTTPS, false)
+ as bool;
- var client = createClient(strictHttps: strictHttps);
+ var client = createClient(url, strictHttps: strictHttps);
// Attempt to open a connection to the server
try {
- _request = await client.openUrl(method, _uri).timeout(Duration(seconds: 10));
+ _request = await client
+ .openUrl(method, _uri)
+ .timeout(Duration(seconds: 10));
- // Set headers
- _request.headers.set(HttpHeaders.authorizationHeader, _authorizationHeader());
- _request.headers.set(HttpHeaders.acceptHeader, "application/json");
- _request.headers.set(HttpHeaders.contentTypeHeader, "application/json");
- _request.headers.set(HttpHeaders.acceptLanguageHeader, Intl.getCurrentLocale());
+ // Default headers
+ defaultHeaders().forEach((key, value) {
+ _request?.headers.set(key, value);
+ });
+
+ // Custom headers
+ headers.forEach((key, value) {
+ _request?.headers.set(key, value);
+ });
return _request;
} on SocketException catch (error) {
- print("SocketException at ${url}: ${error.toString()}");
- showServerError(L10().connectionRefused, error.toString());
+ debug("SocketException at ${url}: ${error.toString()}");
+ showServerError(url, L10().connectionRefused, error.toString());
return null;
} on TimeoutException {
- print("TimeoutException at ${url}");
- showTimeoutError();
+ debug("TimeoutException at ${url}");
+ showTimeoutError(url);
+ return null;
+ } on OSError catch (error) {
+ debug("OSError at ${url}: ${error.toString()}");
+ showServerError(url, L10().connectionRefused, error.toString());
return null;
} on CertificateException catch (error) {
- print("CertificateException at ${url}:");
- print(error.toString());
- showServerError(L10().serverCertificateError, error.toString());
+ debug("CertificateException at ${url}:");
+ debug(error.toString());
+ showServerError(url, L10().serverCertificateError, error.toString());
return null;
} on HandshakeException catch (error) {
- print("HandshakeException at ${url}:");
- print(error.toString());
- showServerError(L10().serverCertificateError, error.toString());
+ debug("HandshakeException at ${url}:");
+ debug(error.toString());
+ showServerError(url, L10().serverCertificateError, error.toString());
return null;
} catch (error, stackTrace) {
- print("Server error at ${url}: ${error.toString()}");
- showServerError(L10().serverError, error.toString());
- sentryReportError(error, stackTrace);
+ debug("Server error at ${url}: ${error.toString()}");
+ showServerError(url, L10().serverError, error.toString());
+ sentryReportError(
+ "api.apiRequest : openUrl",
+ error,
+ stackTrace,
+ context: {"url": url, "method": method},
+ );
+
return null;
}
}
-
/*
* Complete an API request, and return an APIResponse object
*/
- Future completeRequest(HttpClientRequest request, {String? data, int? statusCode, bool ignoreResponse = false}) async {
-
+ Future completeRequest(
+ HttpClientRequest request, {
+ String? data,
+ int? statusCode,
+ bool ignoreResponse = false,
+ }) async {
if (data != null && data.isNotEmpty) {
-
var encoded_data = utf8.encode(data);
- request.headers.set(HttpHeaders.contentLengthHeader, encoded_data.length.toString());
+ request.headers.set(
+ HttpHeaders.contentLengthHeader,
+ encoded_data.length.toString(),
+ );
request.add(encoded_data);
}
APIResponse response = APIResponse(
method: request.method,
- url: request.uri.toString()
+ url: request.uri.toString(),
);
+ String url = request.uri.toString();
+
try {
- HttpClientResponse? _response = await request.close().timeout(Duration(seconds: 10));
+ HttpClientResponse? _response = await request.close().timeout(
+ Duration(seconds: 10),
+ );
response.statusCode = _response.statusCode;
// If the server returns a server error code, alert the user
if (_response.statusCode >= 500) {
- showStatusCodeError(_response.statusCode);
+ showStatusCodeError(url, _response.statusCode);
- sentryReportMessage(
- "Server error",
- context: {
- "url": request.uri.toString(),
- "method": request.method,
- "statusCode": _response.statusCode.toString(),
- "requestHeaders": request.headers.toString(),
- "responseHeaders": _response.headers.toString(),
- "responseData": response.data.toString(),
- }
- );
-
- } else {
-
- if (ignoreResponse) {
- response.data = {};
- } else {
- response.data = await responseToJson(_response) ?? {};
+ // Some server errors are not ones for us to worry about!
+ switch (_response.statusCode) {
+ case 502: // Bad gateway
+ case 503: // Service unavailable
+ case 504: // Gateway timeout
+ break;
+ default: // Any other error code
+ sentryReportMessage(
+ "Server error",
+ context: {
+ "url": request.uri.toString(),
+ "method": request.method,
+ "statusCode": _response.statusCode.toString(),
+ "requestHeaders": request.headers.toString(),
+ "responseHeaders": _response.headers.toString(),
+ "responseData": response.data.toString(),
+ },
+ );
}
+ } else {
+ response.data = ignoreResponse
+ ? {}
+ : await responseToJson(url, _response) ?? {};
- if (statusCode != null) {
-
- // Expected status code not returned
- if (statusCode != _response.statusCode) {
- showStatusCodeError(_response.statusCode);
- }
+ // First check that the returned status code is what we expected
+ if (statusCode != null && statusCode != _response.statusCode) {
+ showStatusCodeError(
+ url,
+ _response.statusCode,
+ details: response.data.toString(),
+ );
}
}
-
+ } on HttpException catch (error) {
+ showServerError(url, L10().serverError, error.toString());
+ response.error = "HTTPException";
+ response.errorDetail = error.toString();
} on SocketException catch (error) {
- showServerError(L10().connectionRefused, error.toString());
+ showServerError(url, L10().connectionRefused, error.toString());
response.error = "SocketException";
response.errorDetail = error.toString();
-
+ } on CertificateException catch (error) {
+ debug("CertificateException at ${request.uri.toString()}:");
+ debug(error.toString());
+ showServerError(url, L10().serverCertificateError, error.toString());
} on TimeoutException {
- showTimeoutError();
+ showTimeoutError(url);
response.error = "TimeoutException";
} catch (error, stackTrace) {
- showServerError(L10().serverError, error.toString());
- sentryReportError(error, stackTrace);
+ showServerError(url, L10().serverError, error.toString());
+ sentryReportError("api.completeRequest", error, stackTrace);
response.error = "UnknownError";
response.errorDetail = error.toString();
}
return response;
-
}
/*
* Convert a HttpClientResponse response object to JSON
*/
- dynamic responseToJson(HttpClientResponse response) async {
-
+ dynamic responseToJson(String url, HttpClientResponse response) async {
String body = await response.transform(utf8.decoder).join();
try {
@@ -1011,37 +1406,56 @@ class InvenTreeAPI {
return data ?? {};
} on FormatException {
-
- sentryReportMessage(
- "Error decoding JSON response from server",
- context: {
- "headers": response.headers.toString(),
- "statusCode": response.statusCode.toString(),
- "data": body.toString(),
- }
- );
+ switch (response.statusCode) {
+ case 400:
+ case 401:
+ case 403:
+ case 404:
+ // Ignore for unauthorized pages
+ break;
+ case 502:
+ case 503:
+ case 504:
+ // Ignore for server errors
+ break;
+ default:
+ sentryReportMessage(
+ "Error decoding JSON response from server",
+ context: {
+ "headers": response.headers.toString(),
+ "statusCode": response.statusCode.toString(),
+ "data": body.toString(),
+ "endpoint": url,
+ },
+ );
+ }
showServerError(
+ url,
L10().formatException,
- L10().formatExceptionJson + ":\n${body}"
+ L10().formatExceptionJson + ":\n${body}",
);
// Return an empty map
return {};
}
-
}
/*
* Perform a HTTP GET request
* Returns a json object (or null if did not complete)
*/
- Future get(String url, {Map params = const {}, int? expectedStatusCode=200}) async {
-
+ Future get(
+ String url, {
+ Map params = const {},
+ Map headers = const {},
+ int? expectedStatusCode = 200,
+ }) async {
HttpClientRequest? request = await apiRequest(
url,
"GET",
urlParams: params,
+ headers: headers,
);
if (request == null) {
@@ -1060,11 +1474,7 @@ class InvenTreeAPI {
* Perform a HTTP DELETE request
*/
Future delete(String url) async {
-
- HttpClientRequest? request = await apiRequest(
- url,
- "DELETE",
- );
+ HttpClientRequest? request = await apiRequest(url, "DELETE");
if (request == null) {
// Return an "invalid" APIResponse object
@@ -1075,29 +1485,47 @@ class InvenTreeAPI {
);
}
- return completeRequest(
- request,
- ignoreResponse: true,
- );
+ return completeRequest(request, ignoreResponse: true);
+ }
+
+ // Find the current locale code for the running app
+ String get currentLocale {
+ if (hasContext()) {
+ // Try to get app context
+ BuildContext? context = OneContext().context;
+
+ if (context != null) {
+ Locale? locale = InvenTreeApp.of(context)?.locale;
+
+ if (locale != null) {
+ return locale.languageCode; //.toString();
+ }
+ }
+ }
+
+ // Fallback value
+ return Intl.getCurrentLocale();
}
// Return a list of request headers
Map defaultHeaders() {
Map headers = {};
- headers[HttpHeaders.authorizationHeader] = _authorizationHeader();
+ if (hasToken) {
+ headers[HttpHeaders.authorizationHeader] = _authorizationHeader();
+ }
+
headers[HttpHeaders.acceptHeader] = "application/json";
headers[HttpHeaders.contentTypeHeader] = "application/json";
- headers[HttpHeaders.acceptLanguageHeader] = Intl.getCurrentLocale();
+ headers[HttpHeaders.acceptLanguageHeader] = currentLocale;
return headers;
}
+ // Construct a token authorization header
String _authorizationHeader() {
- if (_token.isNotEmpty) {
- return "Token $_token";
- } else if (profile != null) {
- return "Basic " + base64Encode(utf8.encode("${profile?.username}:${profile?.password}"));
+ if (token.isNotEmpty) {
+ return "Token ${token}";
} else {
return "";
}
@@ -1107,11 +1535,34 @@ class InvenTreeAPI {
static String get staticThumb => "/static/img/blank_image.thumbnail.png";
+ CachedNetworkImage? getThumbnail(
+ String imageUrl, {
+ double size = 40,
+ bool hideIfNull = false,
+ }) {
+ if (hideIfNull) {
+ if (imageUrl.isEmpty) {
+ return null;
+ }
+ }
+
+ try {
+ return getImage(imageUrl, width: size, height: size);
+ } catch (error, stackTrace) {
+ sentryReportError("_getThumbnail", error, stackTrace);
+ return null;
+ }
+ }
+
/*
* Load image from the InvenTree server,
* or from local cache (if it has been cached!)
*/
- CachedNetworkImage getImage(String imageUrl, {double? height, double? width}) {
+ CachedNetworkImage getImage(
+ String imageUrl, {
+ double? height,
+ double? width,
+ }) {
if (imageUrl.isEmpty) {
imageUrl = staticImage;
}
@@ -1121,22 +1572,208 @@ class InvenTreeAPI {
const key = "inventree_network_image";
CacheManager manager = CacheManager(
- Config(
- key,
- fileService: InvenTreeFileService(
- strictHttps: _strictHttps,
- ),
- )
+ Config(key, fileService: InvenTreeFileService(strictHttps: _strictHttps)),
);
return CachedNetworkImage(
imageUrl: url,
placeholder: (context, url) => CircularProgressIndicator(),
- errorWidget: (context, url, error) => FaIcon(FontAwesomeIcons.timesCircle, color: COLOR_DANGER),
+ errorWidget: (context, url, error) {
+ print("CachedNetworkimage error: ${error.toString()}");
+ return GestureDetector(
+ child: Icon(TablerIcons.circle_x, color: COLOR_DANGER),
+ onTap: () => {
+ showSnackIcon(error.toString().split(",")[0], success: false),
+ },
+ );
+ },
httpHeaders: defaultHeaders(),
height: height,
width: width,
cacheManager: manager,
);
}
+
+ // Keep a record of which settings we have received from the server
+ Map _globalSettings = {};
+ Map _userSettings = {};
+
+ Future getGlobalSetting(String key) async {
+ InvenTreeGlobalSetting? setting = _globalSettings[key];
+
+ if ((setting != null) && setting.reloadedWithin(Duration(minutes: 5))) {
+ return setting.value;
+ }
+
+ final response = await InvenTreeGlobalSetting().getModel(key);
+
+ if (response is InvenTreeGlobalSetting) {
+ response.lastReload = DateTime.now();
+ _globalSettings[key] = response;
+ return response.value;
+ } else {
+ return "";
+ }
+ }
+
+ // Return a boolean global setting value
+ Future getGlobalBooleanSetting(
+ String key, {
+ bool backup = false,
+ }) async {
+ String value = await getGlobalSetting(key);
+
+ if (value.isEmpty) {
+ return backup;
+ }
+
+ return value.toLowerCase().trim() == "true";
+ }
+
+ Future getUserSetting(String key) async {
+ InvenTreeUserSetting? setting = _userSettings[key];
+
+ if ((setting != null) && setting.reloadedWithin(Duration(minutes: 5))) {
+ return setting.value;
+ }
+
+ final response = await InvenTreeUserSetting().getModel(key);
+
+ if (response is InvenTreeUserSetting) {
+ response.lastReload = DateTime.now();
+ _userSettings[key] = response;
+ return response.value;
+ } else {
+ return "";
+ }
+ }
+
+ // Return a boolean user setting value
+ Future getUserBooleanSetting(String key) async {
+ String value = await getUserSetting(key);
+ return value.toLowerCase().trim() == "true";
+ }
+
+ /*
+ * Send a request to the server to locate / identify either a StockItem or StockLocation
+ */
+ Future locateItemOrLocation(
+ BuildContext context, {
+ int? item,
+ int? location,
+ }) async {
+ var plugins = getPlugins(mixin: "locate");
+
+ if (plugins.isEmpty) {
+ // TODO: Error message
+ return;
+ }
+
+ String plugin_name = "";
+
+ if (plugins.length == 1) {
+ plugin_name = plugins.first.key;
+ } else {
+ // User selects which plugin to use
+ List