mirror of
https://github.com/vodemn/m3_lightmeter.git
synced 2024-11-21 15:00:40 +00:00
ML-141 Prepare iOS release (#144)
* implemented `MockCameraContainerBloc` to stub camera on simulator * [iOS] fixed camera preview aspect ratio * place screenshots in platform-specific folders * [iOS] updated buildable name * [iOS] fixed stub image cover fit * [iOS] implemented screenshots generator for all target devices * store screenshots in _generated_ folder * Update .gitignore * Created "Build Prod .ipa" workflow * added. certs to .ipa workflow * test ipa building * fixed provision cert path * set provision profile in XCode * set automatic signing for dev builds * set ios version in Podfile * renamed provision file * renamed provision profile * fixed cert folder... * changed provision path * typo * typo * try automatic signing * use manual profile installation * added export options * typo * increased timeout * increased ipa timeout * Update README.md * typo * [iOS] separated camera handling logic * [iOS] fixed vibration * migrated to http server iap * [iOS] fixed histogram * replaced distribution profile with development profile * removed constants from env to the separate file * removed duplicate launch schema * fixed PR check workflow * [iOS] set `ITSAppUsesNonExemptEncryption` to NO * [iOS] removed java reference from "Build .ipa" workflow
This commit is contained in:
parent
a1ce17d675
commit
134af8ad28
34 changed files with 363 additions and 169 deletions
14
.github/scripts/restore_from_base64.sh
vendored
Normal file
14
.github/scripts/restore_from_base64.sh
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
content="$1"
|
||||||
|
filename="$2"
|
||||||
|
|
||||||
|
if [[ ! -n "$content" ]]; then
|
||||||
|
echo "Provide file content"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -n "$filename" ]]; then
|
||||||
|
echo "Provide a path to an output file"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -n "$content" | base64 --decode --output "$filename"
|
14
.github/workflows/build_apk.yml
vendored
14
.github/workflows/build_apk.yml
vendored
|
@ -67,12 +67,10 @@ jobs:
|
||||||
cp $GOOGLE_SERVICES_JSON_ANDROID_PATH ./android/app
|
cp $GOOGLE_SERVICES_JSON_ANDROID_PATH ./android/app
|
||||||
|
|
||||||
- name: Restore firebase_options.dart
|
- name: Restore firebase_options.dart
|
||||||
env:
|
run: bash .github/scripts/restore_from_base64.sh "${{ secrets.FIREBASE_OPTIONS }}" "lib/firebase_options.dart"
|
||||||
FIREBASE_OPTIONS: ${{ secrets.FIREBASE_OPTIONS }}
|
|
||||||
run: |
|
- name: Restore constants.dart
|
||||||
FIREBASE_OPTIONS_PATH=$RUNNER_TEMP/firebase_options.dart
|
run: bash .github/scripts/restore_from_base64.sh "${{ secrets.CONSTANTS }}" "lib/constants.dart"
|
||||||
echo -n "$FIREBASE_OPTIONS" | base64 --decode --output $FIREBASE_OPTIONS_PATH
|
|
||||||
cp $FIREBASE_OPTIONS_PATH ./lib
|
|
||||||
|
|
||||||
- name: Install Flutter
|
- name: Install Flutter
|
||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
|
@ -89,10 +87,10 @@ jobs:
|
||||||
- name: Build .apk
|
- name: Build .apk
|
||||||
env:
|
env:
|
||||||
FLAVOR: ${{ github.event.inputs.flavor }}
|
FLAVOR: ${{ github.event.inputs.flavor }}
|
||||||
run: flutter build apk --release --flavor $FLAVOR --dart-define cameraPreviewAspectRatio=240/320 -t lib/main_$FLAVOR.dart
|
run: flutter build apk --release --flavor $FLAVOR -t lib/main_$FLAVOR.dart
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: m3_lightmeter_${{ github.event.inputs.flavor }}
|
name: m3_lightmeter_${{ github.event.inputs.flavor }}_apk
|
||||||
path: build/app/outputs/flutter-apk/app-${{ github.event.inputs.flavor }}-release.apk
|
path: build/app/outputs/flutter-apk/app-${{ github.event.inputs.flavor }}-release.apk
|
||||||
|
|
97
.github/workflows/build_ipa.yml
vendored
Normal file
97
.github/workflows/build_ipa.yml
vendored
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
# This workflow uses actions that are not certified by GitHub.
|
||||||
|
# They are provided by a third-party and are governed by
|
||||||
|
# separate terms of service, privacy policy, and support
|
||||||
|
# documentation.
|
||||||
|
|
||||||
|
name: Build Prod .ipa
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
FLAVOR: "prod"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build .ipa
|
||||||
|
runs-on: macos-11
|
||||||
|
timeout-minutes: 60
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Connect private iap package
|
||||||
|
uses: webfactory/ssh-agent@v0.8.0
|
||||||
|
with:
|
||||||
|
ssh-private-key: ${{ secrets.M3_LIGHTMETER_IAP_KEY }}
|
||||||
|
|
||||||
|
- name: Install the Apple certificate and provisioning profile
|
||||||
|
env:
|
||||||
|
APP_STORE_P12: ${{ secrets.APP_STORE_P12 }}
|
||||||
|
APP_STORE_P12_PASSWORD: ${{ secrets.APP_STORE_P12_PASSWORD }}
|
||||||
|
APP_STORE_PROVISION_PROD: ${{ secrets.APP_STORE_PROVISION_PROD }}
|
||||||
|
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
# create variables
|
||||||
|
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
|
||||||
|
PROVISION_PATH=$RUNNER_TEMP/build_provision.mobileprovision
|
||||||
|
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
||||||
|
|
||||||
|
# import certificate and provisioning profile from secrets
|
||||||
|
echo -n "$APP_STORE_P12" | base64 --decode -o $CERTIFICATE_PATH
|
||||||
|
echo -n "$APP_STORE_PROVISION_PROD" | base64 --decode -o $PROVISION_PATH
|
||||||
|
|
||||||
|
# create temporary keychain
|
||||||
|
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||||
|
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
|
||||||
|
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||||
|
|
||||||
|
# import certificate to keychain
|
||||||
|
security import $CERTIFICATE_PATH -P "$APP_STORE_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
||||||
|
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||||
|
|
||||||
|
# apply provisioning profile
|
||||||
|
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
|
||||||
|
cp $PROVISION_PATH ~/Library/MobileDevice/Provisioning\ Profiles
|
||||||
|
|
||||||
|
- name: Restore ios/Runner/ExportOptions.plist
|
||||||
|
run: bash .github/scripts/restore_from_base64.sh "${{ secrets.APP_STORE_EXPORT_OPTIONS }}" "ios/Runner/ExportOptions.plist"
|
||||||
|
|
||||||
|
- name: Restore firebase_options.dart
|
||||||
|
run: bash .github/scripts/restore_from_base64.sh "${{ secrets.FIREBASE_OPTIONS }}" "lib/firebase_options.dart"
|
||||||
|
|
||||||
|
- name: Restore constants.dart
|
||||||
|
run: bash .github/scripts/restore_from_base64.sh "${{ secrets.CONSTANTS }}" "lib/constants.dart"
|
||||||
|
|
||||||
|
- name: Install Flutter
|
||||||
|
uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: "stable"
|
||||||
|
flutter-version: "3.10.0"
|
||||||
|
|
||||||
|
- name: Prepare flutter project
|
||||||
|
run: |
|
||||||
|
flutter --version
|
||||||
|
flutter pub get
|
||||||
|
flutter pub run intl_utils:generate
|
||||||
|
|
||||||
|
- name: Build .ipa
|
||||||
|
run: |
|
||||||
|
flutter build ipa \
|
||||||
|
--release \
|
||||||
|
--flavor $FLAVOR \
|
||||||
|
--target lib/main_$FLAVOR.dart \
|
||||||
|
--export-options-plist=ios/Runner/ExportOptions.plist
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: m3_lightmeter_$FLAVOR_ipa
|
||||||
|
path: build/ios/ipa/lightmeter.ipa
|
||||||
|
|
||||||
|
- name: Clean up keychain and provisioning profile
|
||||||
|
if: ${{ always() }}
|
||||||
|
run: |
|
||||||
|
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db
|
||||||
|
rm ~/Library/MobileDevice/Provisioning\ Profiles/build_provision.mobileprovision
|
12
.github/workflows/create_release.yml
vendored
12
.github/workflows/create_release.yml
vendored
|
@ -37,7 +37,7 @@ on:
|
||||||
default: true
|
default: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
BUILD_ARGS: --release --flavor prod --dart-define cameraPreviewAspectRatio=240/320 -t lib/main_prod.dart
|
BUILD_ARGS: --release --flavor prod -t lib/main_prod.dart
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
@ -86,12 +86,10 @@ jobs:
|
||||||
cp $GOOGLE_SERVICES_JSON_ANDROID_PATH ./android/app
|
cp $GOOGLE_SERVICES_JSON_ANDROID_PATH ./android/app
|
||||||
|
|
||||||
- name: Restore firebase_options.dart
|
- name: Restore firebase_options.dart
|
||||||
env:
|
run: bash .github/scripts/restore_from_base64.sh "${{ secrets.FIREBASE_OPTIONS }}" "lib/firebase_options.dart"
|
||||||
FIREBASE_OPTIONS: ${{ secrets.FIREBASE_OPTIONS }}
|
|
||||||
run: |
|
- name: Restore constants.dart
|
||||||
FIREBASE_OPTIONS_PATH=$RUNNER_TEMP/firebase_options.dart
|
run: bash .github/scripts/restore_from_base64.sh "${{ secrets.CONSTANTS }}" "lib/constants.dart"
|
||||||
echo -n "$FIREBASE_OPTIONS" | base64 --decode --output $FIREBASE_OPTIONS_PATH
|
|
||||||
cp $FIREBASE_OPTIONS_PATH ./lib
|
|
||||||
|
|
||||||
# This step makes sense when Github release is enabled because this release increments the build number.
|
# This step makes sense when Github release is enabled because this release increments the build number.
|
||||||
# Therefore here we have to increment it as well to build an apk with the same build number.
|
# Therefore here we have to increment it as well to build an apk with the same build number.
|
||||||
|
|
3
.github/workflows/pr_check.yml
vendored
3
.github/workflows/pr_check.yml
vendored
|
@ -36,6 +36,9 @@ jobs:
|
||||||
if: steps.fetch-iap.conclusion != 'success'
|
if: steps.fetch-iap.conclusion != 'success'
|
||||||
run: bash ./.github/scripts/stub_iap.sh
|
run: bash ./.github/scripts/stub_iap.sh
|
||||||
|
|
||||||
|
- name: Restore constants.dart
|
||||||
|
run: bash .github/scripts/restore_from_base64.sh "${{ secrets.CONSTANTS }}" "lib/constants.dart"
|
||||||
|
|
||||||
- uses: subosito/flutter-action@v2
|
- uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
channel: "stable"
|
channel: "stable"
|
||||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -58,7 +58,8 @@ android/app/google-services.json
|
||||||
ios/firebase_app_id_file.json
|
ios/firebase_app_id_file.json
|
||||||
ios/Runner/GoogleService-Info.plist
|
ios/Runner/GoogleService-Info.plist
|
||||||
/lib/firebase_options.dart
|
/lib/firebase_options.dart
|
||||||
|
/lib/constants.dart
|
||||||
|
|
||||||
coverage/
|
coverage/
|
||||||
test/coverage_helper_test.dart
|
test/coverage_helper_test.dart
|
||||||
screenshots/*.png
|
screenshots/generated/
|
44
.vscode/launch.json
vendored
44
.vscode/launch.json
vendored
|
@ -5,83 +5,49 @@
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "dev-debug (android)",
|
"name": "dev-debug",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"type": "dart",
|
"type": "dart",
|
||||||
"flutterMode": "debug",
|
"flutterMode": "debug",
|
||||||
"args": [
|
"args": [
|
||||||
"--flavor",
|
"--flavor",
|
||||||
"dev",
|
"dev",
|
||||||
"--dart-define",
|
|
||||||
"cameraPreviewAspectRatio=240/320",
|
|
||||||
],
|
],
|
||||||
"program": "${workspaceFolder}/lib/main_dev.dart",
|
"program": "${workspaceFolder}/lib/main_dev.dart",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "dev-profile (android)",
|
"name": "dev-profile",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"type": "dart",
|
"type": "dart",
|
||||||
"flutterMode": "profile",
|
"flutterMode": "profile",
|
||||||
"args": [
|
"args": [
|
||||||
"--flavor",
|
"--flavor",
|
||||||
"dev",
|
"dev",
|
||||||
"--dart-define",
|
|
||||||
"cameraPreviewAspectRatio=240/320",
|
|
||||||
],
|
],
|
||||||
"program": "${workspaceFolder}/lib/main_dev.dart",
|
"program": "${workspaceFolder}/lib/main_dev.dart",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "prod-debug (android)",
|
"name": "prod-debug",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"type": "dart",
|
"type": "dart",
|
||||||
"flutterMode": "debug",
|
"flutterMode": "debug",
|
||||||
"args": [
|
"args": [
|
||||||
"--flavor",
|
"--flavor",
|
||||||
"prod",
|
"prod",
|
||||||
"--dart-define",
|
|
||||||
"cameraPreviewAspectRatio=240/320",
|
|
||||||
],
|
],
|
||||||
"program": "${workspaceFolder}/lib/main_prod.dart",
|
"program": "${workspaceFolder}/lib/main_prod.dart",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "prod-profile (android)",
|
"name": "prod-profile",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"type": "dart",
|
"type": "dart",
|
||||||
"flutterMode": "profile",
|
"flutterMode": "profile",
|
||||||
"args": [
|
"args": [
|
||||||
"--flavor",
|
"--flavor",
|
||||||
"prod",
|
"prod",
|
||||||
"--dart-define",
|
|
||||||
"cameraPreviewAspectRatio=240/320",
|
|
||||||
],
|
],
|
||||||
"program": "${workspaceFolder}/lib/main_prod.dart",
|
"program": "${workspaceFolder}/lib/main_prod.dart",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "dev-debug (ios)",
|
|
||||||
"request": "launch",
|
|
||||||
"type": "dart",
|
|
||||||
"flutterMode": "debug",
|
|
||||||
"args": [
|
|
||||||
"--flavor",
|
|
||||||
"dev",
|
|
||||||
"--dart-define",
|
|
||||||
"cameraPreviewAspectRatio=3/4",
|
|
||||||
],
|
|
||||||
"program": "${workspaceFolder}/lib/main_dev.dart",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "dev-profile (ios)",
|
|
||||||
"request": "launch",
|
|
||||||
"flutterMode": "profile",
|
|
||||||
"type": "dart",
|
|
||||||
"args": [
|
|
||||||
"--flavor",
|
|
||||||
"dev",
|
|
||||||
"--dart-define",
|
|
||||||
"cameraPreviewAspectRatio=3/4",
|
|
||||||
],
|
|
||||||
"program": "${workspaceFolder}/lib/main_dev.dart",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "dev-simulator",
|
"name": "dev-simulator",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
|
@ -91,8 +57,6 @@
|
||||||
"--flavor",
|
"--flavor",
|
||||||
"dev",
|
"dev",
|
||||||
"--dart-define",
|
"--dart-define",
|
||||||
"cameraPreviewAspectRatio=240/320",
|
|
||||||
"--dart-define",
|
|
||||||
"cameraStubImage=assets/camera_stub_image.jpg"
|
"cameraStubImage=assets/camera_stub_image.jpg"
|
||||||
],
|
],
|
||||||
"program": "${workspaceFolder}/lib/main_dev.dart",
|
"program": "${workspaceFolder}/lib/main_dev.dart",
|
||||||
|
|
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
@ -16,7 +16,7 @@
|
||||||
"editor.suggest.snippetsPreventQuickSuggestions": false,
|
"editor.suggest.snippetsPreventQuickSuggestions": false,
|
||||||
"editor.suggestSelection": "first",
|
"editor.suggestSelection": "first",
|
||||||
"editor.tabCompletion": "onlySnippets",
|
"editor.tabCompletion": "onlySnippets",
|
||||||
"editor.wordBasedSuggestions": false
|
"editor.wordBasedSuggestions": "off"
|
||||||
},
|
},
|
||||||
"dart.doNotFormat": [
|
"dart.doNotFormat": [
|
||||||
"**/generated/**",
|
"**/generated/**",
|
||||||
|
|
6
.vscode/tasks.json
vendored
6
.vscode/tasks.json
vendored
|
@ -11,8 +11,6 @@
|
||||||
"--flavor",
|
"--flavor",
|
||||||
"dev",
|
"dev",
|
||||||
"--release",
|
"--release",
|
||||||
"--dart-define",
|
|
||||||
"cameraPreviewAspectRatio=240/320",
|
|
||||||
"-t",
|
"-t",
|
||||||
"lib/main_dev.dart",
|
"lib/main_dev.dart",
|
||||||
],
|
],
|
||||||
|
@ -27,8 +25,6 @@
|
||||||
"--flavor",
|
"--flavor",
|
||||||
"prod",
|
"prod",
|
||||||
"--release",
|
"--release",
|
||||||
"--dart-define",
|
|
||||||
"cameraPreviewAspectRatio=240/320",
|
|
||||||
"-t",
|
"-t",
|
||||||
"lib/main_prod.dart",
|
"lib/main_prod.dart",
|
||||||
],
|
],
|
||||||
|
@ -43,8 +39,6 @@
|
||||||
"--flavor",
|
"--flavor",
|
||||||
"prod",
|
"prod",
|
||||||
"--release",
|
"--release",
|
||||||
"--dart-define",
|
|
||||||
"cameraPreviewAspectRatio=240/320",
|
|
||||||
"-t",
|
"-t",
|
||||||
"lib/main_prod.dart",
|
"lib/main_prod.dart",
|
||||||
],
|
],
|
||||||
|
|
26
README.md
26
README.md
|
@ -38,6 +38,19 @@ To build this app you need to install Flutter 3.10.0 stable. [How to install](ht
|
||||||
|
|
||||||
### 2. Project setup
|
### 2. Project setup
|
||||||
|
|
||||||
|
#### Restore _constants.dart_ file
|
||||||
|
|
||||||
|
Create a file _lib/constants.dart_ and paste the following content:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
const String contactEmail = '';
|
||||||
|
const String iapServerUrl = '';
|
||||||
|
const String issuesReportUrl = '';
|
||||||
|
const String sourceCodeUrl = '';
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Stub IAP package
|
||||||
|
|
||||||
As part of the app's functionallity is in the private repo, you have to replace these lines in _pubspec.yaml_:
|
As part of the app's functionallity is in the private repo, you have to replace these lines in _pubspec.yaml_:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
@ -75,17 +88,8 @@ Out of the box Firebase Crashlytics won't work. If you want to add Crashlytics t
|
||||||
|
|
||||||
### 4. Build
|
### 4. Build
|
||||||
|
|
||||||
#### Android
|
- Checkout [Build .apk](.github/workflows/build_apk.yml) workflow for Android
|
||||||
|
- Checkout [Build .ipa](.github/workflows/build_ipa.yml) workflow for iOS
|
||||||
You can build an apk by running the following command from the root of the repository:
|
|
||||||
|
|
||||||
```console
|
|
||||||
flutter build apk --release --flavor dev --dart-define cameraPreviewAspectRatio=240/320 -t lib/main_dev.dart
|
|
||||||
```
|
|
||||||
|
|
||||||
### iOS
|
|
||||||
|
|
||||||
TBD
|
|
||||||
|
|
||||||
# Support
|
# Support
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,10 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:m3_lightmeter_iap/src/data/models/iap_product.dart';
|
import 'package:m3_lightmeter_iap/src/data/models/iap_product.dart';
|
||||||
|
|
||||||
class IAPProductsProvider extends StatefulWidget {
|
class IAPProductsProvider extends StatefulWidget {
|
||||||
|
final String apiUrl;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
const IAPProductsProvider({required this.child, super.key});
|
const IAPProductsProvider({required this.apiUrl, required this.child, super.key});
|
||||||
|
|
||||||
static IAPProductsProviderState of(BuildContext context) => IAPProductsProvider.maybeOf(context)!;
|
static IAPProductsProviderState of(BuildContext context) => IAPProductsProvider.maybeOf(context)!;
|
||||||
|
|
||||||
|
|
|
@ -8,12 +8,16 @@ import 'package:mocktail/mocktail.dart';
|
||||||
class _MockIAPStorageService extends Mock implements IAPStorageService {}
|
class _MockIAPStorageService extends Mock implements IAPStorageService {}
|
||||||
|
|
||||||
class MockIAPProviders extends StatefulWidget {
|
class MockIAPProviders extends StatefulWidget {
|
||||||
|
final List<EquipmentProfile> equipmentProfiles;
|
||||||
final String selectedEquipmentProfileId;
|
final String selectedEquipmentProfileId;
|
||||||
|
final List<Film> films;
|
||||||
final Film selectedFilm;
|
final Film selectedFilm;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
const MockIAPProviders({
|
const MockIAPProviders({
|
||||||
|
this.equipmentProfiles = const [],
|
||||||
this.selectedEquipmentProfileId = '',
|
this.selectedEquipmentProfileId = '',
|
||||||
|
this.films = mockFilms,
|
||||||
this.selectedFilm = const Film.other(),
|
this.selectedFilm = const Film.other(),
|
||||||
required this.child,
|
required this.child,
|
||||||
super.key,
|
super.key,
|
||||||
|
@ -66,7 +70,12 @@ final mockEquipmentProfiles = [
|
||||||
ApertureValue.values.indexOf(const ApertureValue(1.7, StopType.half)),
|
ApertureValue.values.indexOf(const ApertureValue(1.7, StopType.half)),
|
||||||
ApertureValue.values.indexOf(const ApertureValue(16, StopType.full)) + 1,
|
ApertureValue.values.indexOf(const ApertureValue(16, StopType.full)) + 1,
|
||||||
),
|
),
|
||||||
ndValues: NdValue.values.sublist(0, 3),
|
ndValues: const [
|
||||||
|
NdValue(0),
|
||||||
|
NdValue(2),
|
||||||
|
NdValue(4),
|
||||||
|
NdValue(8),
|
||||||
|
],
|
||||||
shutterSpeedValues: ShutterSpeedValue.values.sublist(
|
shutterSpeedValues: ShutterSpeedValue.values.sublist(
|
||||||
ShutterSpeedValue.values.indexOf(const ShutterSpeedValue(1000, true, StopType.full)),
|
ShutterSpeedValue.values.indexOf(const ShutterSpeedValue(1000, true, StopType.full)),
|
||||||
ShutterSpeedValue.values.indexOf(const ShutterSpeedValue(16, false, StopType.full)) + 1,
|
ShutterSpeedValue.values.indexOf(const ShutterSpeedValue(16, false, StopType.full)) + 1,
|
||||||
|
|
|
@ -63,6 +63,7 @@ extension WidgetTesterListTileActions on WidgetTester {
|
||||||
/// Useful for tapping a specific [ListTile] inside a specific screen or dialog
|
/// Useful for tapping a specific [ListTile] inside a specific screen or dialog
|
||||||
Future<void> tapDescendantTextOf<T>(String text) async {
|
Future<void> tapDescendantTextOf<T>(String text) async {
|
||||||
await tap(find.descendant(of: find.byType(T), matching: find.text(text)));
|
await tap(find.descendant(of: find.byType(T), matching: find.text(text)));
|
||||||
|
await pumpAndSettle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# Uncomment this line to define a global platform for your project
|
# Uncomment this line to define a global platform for your project
|
||||||
# platform :ios, '11.0'
|
platform :ios, '11.0'
|
||||||
|
|
||||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
|
|
|
@ -399,9 +399,11 @@
|
||||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
|
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = 489Z6UQMGN;
|
DEVELOPMENT_TEAM = "";
|
||||||
|
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 489Z6UQMGN;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = Lightmeter;
|
INFOPLIST_KEY_CFBundleDisplayName = Lightmeter;
|
||||||
|
@ -412,6 +414,7 @@
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.vodemn.lightmeter;
|
PRODUCT_BUNDLE_IDENTIFIER = com.vodemn.lightmeter;
|
||||||
PRODUCT_NAME = Lightmeter;
|
PRODUCT_NAME = Lightmeter;
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Lightmeter Development";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
@ -533,9 +536,11 @@
|
||||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
|
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = 489Z6UQMGN;
|
DEVELOPMENT_TEAM = "";
|
||||||
|
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 489Z6UQMGN;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = Lightmeter;
|
INFOPLIST_KEY_CFBundleDisplayName = Lightmeter;
|
||||||
|
@ -546,6 +551,7 @@
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.vodemn.lightmeter;
|
PRODUCT_BUNDLE_IDENTIFIER = com.vodemn.lightmeter;
|
||||||
PRODUCT_NAME = Lightmeter;
|
PRODUCT_NAME = Lightmeter;
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Lightmeter Development";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
|
@ -561,9 +567,11 @@
|
||||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
|
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = 489Z6UQMGN;
|
DEVELOPMENT_TEAM = "";
|
||||||
|
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 489Z6UQMGN;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = Lightmeter;
|
INFOPLIST_KEY_CFBundleDisplayName = Lightmeter;
|
||||||
|
@ -574,6 +582,7 @@
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.vodemn.lightmeter;
|
PRODUCT_BUNDLE_IDENTIFIER = com.vodemn.lightmeter;
|
||||||
PRODUCT_NAME = Lightmeter;
|
PRODUCT_NAME = Lightmeter;
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Lightmeter Development";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
BuildableName = "Runner.app"
|
BuildableName = "Lightmeter.app"
|
||||||
BlueprintName = "Runner"
|
BlueprintName = "Runner"
|
||||||
ReferencedContainer = "container:Runner.xcodeproj">
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
|
@ -45,7 +45,7 @@
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
BuildableName = "Runner.app"
|
BuildableName = "Lightmeter.app"
|
||||||
BlueprintName = "Runner"
|
BlueprintName = "Runner"
|
||||||
ReferencedContainer = "container:Runner.xcodeproj">
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
|
@ -62,7 +62,7 @@
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
BuildableName = "Runner.app"
|
BuildableName = "Lightmeter.app"
|
||||||
BlueprintName = "Runner"
|
BlueprintName = "Runner"
|
||||||
ReferencedContainer = "container:Runner.xcodeproj">
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
BuildableName = "Runner.app"
|
BuildableName = "Lightmeter.app"
|
||||||
BlueprintName = "Runner"
|
BlueprintName = "Runner"
|
||||||
ReferencedContainer = "container:Runner.xcodeproj">
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
|
@ -45,7 +45,7 @@
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
BuildableName = "Runner.app"
|
BuildableName = "Lightmeter.app"
|
||||||
BlueprintName = "Runner"
|
BlueprintName = "Runner"
|
||||||
ReferencedContainer = "container:Runner.xcodeproj">
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
|
@ -62,7 +62,7 @@
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
BuildableName = "Runner.app"
|
BuildableName = "Lightmeter.app"
|
||||||
BlueprintName = "Runner"
|
BlueprintName = "Runner"
|
||||||
ReferencedContainer = "container:Runner.xcodeproj">
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
|
|
|
@ -2,6 +2,12 @@
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
<false/>
|
||||||
|
<key>LSApplicationCategoryType</key>
|
||||||
|
<string></string>
|
||||||
|
<key>Enc</key>
|
||||||
|
<string></string>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:vibration/vibration.dart';
|
import 'package:vibration/vibration.dart';
|
||||||
|
|
||||||
class HapticsService {
|
class HapticsService {
|
||||||
|
@ -11,10 +13,17 @@ class HapticsService {
|
||||||
|
|
||||||
Future<void> _tryVibrate({required int duration, required int amplitude}) async {
|
Future<void> _tryVibrate({required int duration, required int amplitude}) async {
|
||||||
if (await _canVibrate()) {
|
if (await _canVibrate()) {
|
||||||
await Vibration.vibrate(
|
if (Platform.isAndroid) {
|
||||||
duration: duration,
|
await Vibration.vibrate(
|
||||||
amplitude: amplitude,
|
duration: duration,
|
||||||
);
|
amplitude: amplitude,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await Vibration.vibrate(
|
||||||
|
pattern: [duration],
|
||||||
|
intensities: [amplitude],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,39 +2,23 @@ enum BuildType { dev, prod }
|
||||||
|
|
||||||
class Environment {
|
class Environment {
|
||||||
final BuildType buildType;
|
final BuildType buildType;
|
||||||
final String sourceCodeUrl;
|
|
||||||
final String issuesReportUrl;
|
|
||||||
final String contactEmail;
|
|
||||||
|
|
||||||
final bool hasLightSensor;
|
final bool hasLightSensor;
|
||||||
|
|
||||||
const Environment({
|
const Environment({
|
||||||
required this.buildType,
|
required this.buildType,
|
||||||
required this.sourceCodeUrl,
|
|
||||||
required this.issuesReportUrl,
|
|
||||||
required this.contactEmail,
|
|
||||||
this.hasLightSensor = false,
|
this.hasLightSensor = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const Environment.dev()
|
const Environment.dev()
|
||||||
: buildType = BuildType.dev,
|
: buildType = BuildType.dev,
|
||||||
sourceCodeUrl = 'https://github.com/vodemn/m3_lightmeter',
|
|
||||||
issuesReportUrl = 'https://github.com/vodemn/m3_lightmeter/issues/new/choose',
|
|
||||||
contactEmail = 'contact.vodemn@gmail.com',
|
|
||||||
hasLightSensor = false;
|
hasLightSensor = false;
|
||||||
|
|
||||||
const Environment.prod()
|
const Environment.prod()
|
||||||
: buildType = BuildType.prod,
|
: buildType = BuildType.prod,
|
||||||
sourceCodeUrl = 'https://github.com/vodemn/m3_lightmeter',
|
|
||||||
issuesReportUrl = 'https://github.com/vodemn/m3_lightmeter/issues/new/choose',
|
|
||||||
contactEmail = 'contact.vodemn@gmail.com',
|
|
||||||
hasLightSensor = false;
|
hasLightSensor = false;
|
||||||
|
|
||||||
Environment copyWith({bool? hasLightSensor}) => Environment(
|
Environment copyWith({bool? hasLightSensor}) => Environment(
|
||||||
buildType: buildType,
|
buildType: buildType,
|
||||||
sourceCodeUrl: sourceCodeUrl,
|
|
||||||
issuesReportUrl: issuesReportUrl,
|
|
||||||
contactEmail: contactEmail,
|
|
||||||
hasLightSensor: hasLightSensor ?? this.hasLightSensor,
|
hasLightSensor: hasLightSensor ?? this.hasLightSensor,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
class PlatformConfig {
|
class PlatformConfig {
|
||||||
const PlatformConfig._();
|
const PlatformConfig._();
|
||||||
|
|
||||||
static double get cameraPreviewAspectRatio {
|
static double get cameraPreviewAspectRatio => Platform.isAndroid ? 240 / 320 : 288 / 352;
|
||||||
final rational = const String.fromEnvironment('cameraPreviewAspectRatio').split('/');
|
|
||||||
return int.parse(rational[0]) / int.parse(rational[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
static String get cameraStubImage => const String.fromEnvironment('cameraStubImage');
|
static String get cameraStubImage => const String.fromEnvironment('cameraStubImage');
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import 'package:firebase_core/firebase_core.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:lightmeter/application.dart';
|
import 'package:lightmeter/application.dart';
|
||||||
import 'package:lightmeter/application_wrapper.dart';
|
import 'package:lightmeter/application_wrapper.dart';
|
||||||
|
import 'package:lightmeter/constants.dart';
|
||||||
import 'package:lightmeter/data/analytics/analytics.dart';
|
import 'package:lightmeter/data/analytics/analytics.dart';
|
||||||
import 'package:lightmeter/data/analytics/api/analytics_firebase.dart';
|
import 'package:lightmeter/data/analytics/api/analytics_firebase.dart';
|
||||||
import 'package:lightmeter/environment.dart';
|
import 'package:lightmeter/environment.dart';
|
||||||
|
@ -27,7 +28,10 @@ Future<void> runLightmeterApp(Environment env) async {
|
||||||
products: [IAPProduct(storeId: IAPProductType.paidFeatures.storeId)],
|
products: [IAPProduct(storeId: IAPProductType.paidFeatures.storeId)],
|
||||||
child: application,
|
child: application,
|
||||||
)
|
)
|
||||||
: IAPProductsProvider(child: application),
|
: IAPProductsProvider(
|
||||||
|
apiUrl: iapServerUrl,
|
||||||
|
child: application,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
_errorsLogger.logCrash,
|
_errorsLogger.logCrash,
|
||||||
|
|
|
@ -4,6 +4,7 @@ import 'dart:io';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:camera/camera.dart';
|
import 'package:camera/camera.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
@ -253,12 +254,29 @@ class _WidgetsBindingObserver with WidgetsBindingObserver {
|
||||||
|
|
||||||
_WidgetsBindingObserver(this.onLifecycleStateChanged);
|
_WidgetsBindingObserver(this.onLifecycleStateChanged);
|
||||||
|
|
||||||
|
/// Revoking camera permissions results in app being killed both on Android and iOS
|
||||||
@override
|
@override
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
if (_prevState == AppLifecycleState.inactive && state == AppLifecycleState.resumed) {
|
switch (defaultTargetPlatform) {
|
||||||
return;
|
/// On Android opening a dialog results in [AppLifecycleState.inactive]
|
||||||
|
case TargetPlatform.android:
|
||||||
|
if (_prevState == AppLifecycleState.inactive && state == AppLifecycleState.resumed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_prevState = state;
|
||||||
|
onLifecycleStateChanged(state);
|
||||||
|
|
||||||
|
/// When coming from the app's settings iOS fires paused -> inactive -> resumed state which falls into this condition.
|
||||||
|
/// So the inactive state is skipped.
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
if (state == AppLifecycleState.inactive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_prevState != state) {
|
||||||
|
_prevState = state;
|
||||||
|
onLifecycleStateChanged(state);
|
||||||
|
}
|
||||||
|
default:
|
||||||
}
|
}
|
||||||
_prevState = state;
|
|
||||||
onLifecycleStateChanged(state);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:camera/camera.dart';
|
import 'package:camera/camera.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:lightmeter/res/dimens.dart';
|
import 'package:lightmeter/res/dimens.dart';
|
||||||
|
|
||||||
|
@ -64,31 +65,58 @@ class _CameraHistogramState extends State<CameraHistogram> {
|
||||||
histogramG = List.filled(256, 0);
|
histogramG = List.filled(256, 0);
|
||||||
histogramB = List.filled(256, 0);
|
histogramB = List.filled(256, 0);
|
||||||
|
|
||||||
final int uvRowStride = image.planes[1].bytesPerRow;
|
switch (defaultTargetPlatform) {
|
||||||
final int uvPixelStride = image.planes[1].bytesPerPixel!;
|
case TargetPlatform.android:
|
||||||
|
_yuv420toRgb(image);
|
||||||
for (int x = 0; x < image.width; x++) {
|
case TargetPlatform.iOS:
|
||||||
for (int y = 0; y < image.height; y++) {
|
_bgra8888toRgb(image);
|
||||||
final int uvIndex = uvPixelStride * (x / 2).floor() + uvRowStride * (y / 2).floor();
|
default:
|
||||||
final int index = y * image.width + x;
|
|
||||||
|
|
||||||
final yp = image.planes[0].bytes[index];
|
|
||||||
final up = image.planes[1].bytes[uvIndex];
|
|
||||||
final vp = image.planes[2].bytes[uvIndex];
|
|
||||||
|
|
||||||
final r = yp + vp * 1436 / 1024 - 179;
|
|
||||||
final g = yp - up * 46549 / 131072 + 44 - vp * 93604 / 131072 + 91;
|
|
||||||
final b = yp + up * 1814 / 1024 - 227;
|
|
||||||
|
|
||||||
histogramR[r.round().clamp(0, 255)]++;
|
|
||||||
histogramG[g.round().clamp(0, 255)]++;
|
|
||||||
histogramB[b.round().clamp(0, 255)]++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mounted) setState(() {});
|
if (mounted) setState(() {});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _yuv420toRgb(CameraImage image) {
|
||||||
|
final int uvRowStride = image.planes[1].bytesPerRow;
|
||||||
|
final int uvPixelStride = image.planes[1].bytesPerPixel!;
|
||||||
|
|
||||||
|
for (int x = 0; x < image.width; x++) {
|
||||||
|
for (int y = 0; y < image.height; y++) {
|
||||||
|
final int uvIndex = uvPixelStride * (x / 2).floor() + uvRowStride * (y / 2).floor();
|
||||||
|
final int index = y * image.width + x;
|
||||||
|
|
||||||
|
final yp = image.planes[0].bytes[index];
|
||||||
|
final up = image.planes[1].bytes[uvIndex];
|
||||||
|
final vp = image.planes[2].bytes[uvIndex];
|
||||||
|
|
||||||
|
final r = yp + vp * 1436 / 1024 - 179;
|
||||||
|
final g = yp - up * 46549 / 131072 + 44 - vp * 93604 / 131072 + 91;
|
||||||
|
final b = yp + up * 1814 / 1024 - 227;
|
||||||
|
|
||||||
|
histogramR[r.round().clamp(0, 255)]++;
|
||||||
|
histogramG[g.round().clamp(0, 255)]++;
|
||||||
|
histogramB[b.round().clamp(0, 255)]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _bgra8888toRgb(CameraImage image) {
|
||||||
|
for (int i = 0; i < image.planes.first.bytes.length; i++) {
|
||||||
|
final int channel = i % 4;
|
||||||
|
switch (channel) {
|
||||||
|
case 3:
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
histogramR[image.planes.first.bytes[i]]++;
|
||||||
|
case 1:
|
||||||
|
histogramG[image.planes.first.bytes[i]]++;
|
||||||
|
case 0:
|
||||||
|
histogramB[image.planes.first.bytes[i]]++;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class HistogramChannel extends StatelessWidget {
|
class HistogramChannel extends StatelessWidget {
|
||||||
|
|
|
@ -84,7 +84,16 @@ class _CameraPreviewBuilderState extends State<_CameraPreviewBuilder> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (PlatformConfig.cameraStubImage.isNotEmpty) {
|
if (PlatformConfig.cameraStubImage.isNotEmpty) {
|
||||||
return Image.asset(PlatformConfig.cameraStubImage);
|
return Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: Image.asset(
|
||||||
|
PlatformConfig.cameraStubImage,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return ValueListenableBuilder<bool>(
|
return ValueListenableBuilder<bool>(
|
||||||
valueListenable: _initializedNotifier,
|
valueListenable: _initializedNotifier,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:lightmeter/constants.dart';
|
||||||
import 'package:lightmeter/generated/l10n.dart';
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
import 'package:lightmeter/providers/services_provider.dart';
|
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
class ReportIssueListTile extends StatelessWidget {
|
class ReportIssueListTile extends StatelessWidget {
|
||||||
|
@ -13,7 +13,7 @@ class ReportIssueListTile extends StatelessWidget {
|
||||||
title: Text(S.of(context).reportIssue),
|
title: Text(S.of(context).reportIssue),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
launchUrl(
|
launchUrl(
|
||||||
Uri.parse(ServicesProvider.of(context).environment.issuesReportUrl),
|
Uri.parse(issuesReportUrl),
|
||||||
mode: LaunchMode.externalApplication,
|
mode: LaunchMode.externalApplication,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:lightmeter/constants.dart';
|
||||||
import 'package:lightmeter/generated/l10n.dart';
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
import 'package:lightmeter/providers/services_provider.dart';
|
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
class SourceCodeListTile extends StatelessWidget {
|
class SourceCodeListTile extends StatelessWidget {
|
||||||
|
@ -13,7 +13,7 @@ class SourceCodeListTile extends StatelessWidget {
|
||||||
title: Text(S.of(context).sourceCode),
|
title: Text(S.of(context).sourceCode),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
launchUrl(
|
launchUrl(
|
||||||
Uri.parse(ServicesProvider.of(context).environment.sourceCodeUrl),
|
Uri.parse(sourceCodeUrl),
|
||||||
mode: LaunchMode.externalApplication,
|
mode: LaunchMode.externalApplication,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import 'package:clipboard/clipboard.dart';
|
import 'package:clipboard/clipboard.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:lightmeter/constants.dart';
|
||||||
import 'package:lightmeter/generated/l10n.dart';
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
import 'package:lightmeter/providers/services_provider.dart';
|
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
class WriteEmailListTile extends StatelessWidget {
|
class WriteEmailListTile extends StatelessWidget {
|
||||||
|
@ -13,8 +13,7 @@ class WriteEmailListTile extends StatelessWidget {
|
||||||
leading: const Icon(Icons.email),
|
leading: const Icon(Icons.email),
|
||||||
title: Text(S.of(context).writeEmail),
|
title: Text(S.of(context).writeEmail),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
final email = ServicesProvider.of(context).environment.contactEmail;
|
final mailToUrl = Uri.parse('mailto:$contactEmail?subject=M3 Lightmeter');
|
||||||
final mailToUrl = Uri.parse('mailto:$email?subject=M3 Lightmeter');
|
|
||||||
canLaunchUrl(mailToUrl).then((canLaunch) {
|
canLaunchUrl(mailToUrl).then((canLaunch) {
|
||||||
if (canLaunch) {
|
if (canLaunch) {
|
||||||
launchUrl(
|
launchUrl(
|
||||||
|
@ -29,7 +28,7 @@ class WriteEmailListTile extends StatelessWidget {
|
||||||
action: SnackBarAction(
|
action: SnackBarAction(
|
||||||
label: S.of(context).copyEmail,
|
label: S.of(context).copyEmail,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
FlutterClipboard.copy(email).then((_) {
|
FlutterClipboard.copy(contactEmail).then((_) {
|
||||||
ScaffoldMessenger.of(context).clearSnackBars();
|
ScaffoldMessenger.of(context).clearSnackBars();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -28,7 +28,7 @@ dependencies:
|
||||||
m3_lightmeter_iap:
|
m3_lightmeter_iap:
|
||||||
git:
|
git:
|
||||||
url: "https://github.com/vodemn/m3_lightmeter_iap"
|
url: "https://github.com/vodemn/m3_lightmeter_iap"
|
||||||
ref: v0.7.2
|
ref: v0.8.1
|
||||||
m3_lightmeter_resources:
|
m3_lightmeter_resources:
|
||||||
git:
|
git:
|
||||||
url: "https://github.com/vodemn/m3_lightmeter_resources"
|
url: "https://github.com/vodemn/m3_lightmeter_resources"
|
||||||
|
|
|
@ -10,8 +10,8 @@ As a user I want to see the most relevant screenshots in the store, so that I ca
|
||||||
|
|
||||||
- Metering screen
|
- Metering screen
|
||||||
|
|
||||||
1. Reflected light metering mode*
|
1. Reflected light metering mode\*
|
||||||
2. Incident light metering mode* **
|
2. Incident light metering mode\* \*\*
|
||||||
3. Opened ISO picker
|
3. Opened ISO picker
|
||||||
|
|
||||||
- Settings screen
|
- Settings screen
|
||||||
|
@ -24,14 +24,41 @@ As a user I want to see the most relevant screenshots in the store, so that I ca
|
||||||
1. Just the screen
|
1. Just the screen
|
||||||
2. Opened equipment profile ISO picker
|
2. Opened equipment profile ISO picker
|
||||||
|
|
||||||
> *also in dark mode
|
> \*also in dark mode
|
||||||
|
|
||||||
> **Android only
|
> \*\*Android only
|
||||||
|
|
||||||
## Run the generator
|
## Run the generator
|
||||||
|
|
||||||
|
Screenshots will be stored in the _screenshots/generated/\<platform\>/_ folder.
|
||||||
|
|
||||||
|
### Android
|
||||||
|
|
||||||
```console
|
```console
|
||||||
sh screenshots/generate_screenshots.sh
|
sh screenshots/generate_screenshots.sh <deviceName>
|
||||||
```
|
```
|
||||||
|
|
||||||
Screenshots will be stored in the _screenshots/_ folder.
|
### iOS
|
||||||
|
|
||||||
|
Apple requires screenshots a specific list of devices, so we can implement a custom generator to cover all those devices.
|
||||||
|
|
||||||
|
Can be run on Simulator.
|
||||||
|
|
||||||
|
```console
|
||||||
|
sh screenshots/generate_ios_screenshots.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## List of devices
|
||||||
|
|
||||||
|
### Android
|
||||||
|
|
||||||
|
- Pixel 6
|
||||||
|
|
||||||
|
### iOS
|
||||||
|
|
||||||
|
- iPhone 8 Plus
|
||||||
|
- iPhone 13 Pro
|
||||||
|
- iPhone 13 Pro Max
|
||||||
|
- iPhone 15 Pro
|
||||||
|
- iPhone 15 Pro Max
|
||||||
|
- iPad Pro (12.9-inch) (6th generation)
|
||||||
|
|
|
@ -70,37 +70,37 @@ void main() {
|
||||||
await tester.pumpApplication();
|
await tester.pumpApplication();
|
||||||
|
|
||||||
await tester.takePhoto();
|
await tester.takePhoto();
|
||||||
await tester.takeScreenshot(binding, '${lightThemeColor.value}_metering_reflected');
|
await tester.takeScreenshot(binding, 'light-metering_reflected');
|
||||||
|
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
await tester.tap(find.byTooltip(S.current.tooltipUseLightSensor));
|
await tester.tap(find.byTooltip(S.current.tooltipUseLightSensor));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.toggleIncidentMetering(7.3);
|
await tester.toggleIncidentMetering(7.3);
|
||||||
await tester.takeScreenshot(binding, '${lightThemeColor.value}_metering_incident');
|
await tester.takeScreenshot(binding, 'light-metering_incident');
|
||||||
}
|
}
|
||||||
|
|
||||||
await tester.openAnimatedPicker<IsoValuePicker>();
|
await tester.openAnimatedPicker<IsoValuePicker>();
|
||||||
await tester.takeScreenshot(binding, '${lightThemeColor.value}_metering_iso_picker');
|
await tester.takeScreenshot(binding, 'light-metering_iso_picker');
|
||||||
|
|
||||||
await tester.tapCancelButton();
|
await tester.tapCancelButton();
|
||||||
await tester.tap(find.byTooltip(S.current.tooltipOpenSettings));
|
await tester.tap(find.byTooltip(S.current.tooltipOpenSettings));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.takeScreenshot(binding, '${lightThemeColor.value}_settings');
|
await tester.takeScreenshot(binding, 'light-settings');
|
||||||
|
|
||||||
await tester.tapDescendantTextOf<SettingsScreen>(S.current.meteringScreenLayout);
|
await tester.tapDescendantTextOf<SettingsScreen>(S.current.meteringScreenLayout);
|
||||||
await tester.takeScreenshot(binding, '${lightThemeColor.value}_settings_metering_screen_layout');
|
await tester.takeScreenshot(binding, 'light-settings_metering_screen_layout');
|
||||||
|
|
||||||
await tester.tapCancelButton();
|
await tester.tapCancelButton();
|
||||||
await tester.tapDescendantTextOf<SettingsScreen>(S.current.equipmentProfiles);
|
await tester.tapDescendantTextOf<SettingsScreen>(S.current.equipmentProfiles);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.tapDescendantTextOf<EquipmentProfilesScreen>(mockEquipmentProfiles.first.name);
|
await tester.tapDescendantTextOf<EquipmentProfilesScreen>(mockEquipmentProfiles.first.name);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.takeScreenshot(binding, '${lightThemeColor.value}-equipment_profiles');
|
await tester.takeScreenshot(binding, 'light-equipment_profiles');
|
||||||
|
|
||||||
await tester.tap(find.byIcon(Icons.iso).first);
|
await tester.tap(find.byIcon(Icons.iso).first);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.takeScreenshot(binding, '${lightThemeColor.value}_equipment_profiles_iso_picker');
|
await tester.takeScreenshot(binding, 'light-equipment_profiles_iso_picker');
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
/// and the additionally the first one with the dark theme
|
/// and the additionally the first one with the dark theme
|
||||||
|
@ -111,25 +111,27 @@ void main() {
|
||||||
await tester.pumpApplication();
|
await tester.pumpApplication();
|
||||||
|
|
||||||
await tester.takePhoto();
|
await tester.takePhoto();
|
||||||
await tester.takeScreenshot(binding, '${darkThemeColor.value}_metering_reflected');
|
await tester.takeScreenshot(binding, 'dark-metering_reflected');
|
||||||
|
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
await tester.tap(find.byTooltip(S.current.tooltipUseLightSensor));
|
await tester.tap(find.byTooltip(S.current.tooltipUseLightSensor));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.toggleIncidentMetering(7.3);
|
await tester.toggleIncidentMetering(7.3);
|
||||||
await tester.takeScreenshot(binding, '${darkThemeColor.value}_metering_incident');
|
await tester.takeScreenshot(binding, 'dark-metering_incident');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final String _platformFolder = Platform.isAndroid ? 'android' : 'ios';
|
||||||
|
|
||||||
extension on WidgetTester {
|
extension on WidgetTester {
|
||||||
Future<void> takeScreenshot(IntegrationTestWidgetsFlutterBinding binding, String name) async {
|
Future<void> takeScreenshot(IntegrationTestWidgetsFlutterBinding binding, String name) async {
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
await binding.convertFlutterSurfaceToImage();
|
await binding.convertFlutterSurfaceToImage();
|
||||||
await pumpAndSettle();
|
await pumpAndSettle();
|
||||||
}
|
}
|
||||||
await binding.takeScreenshot(name);
|
await binding.takeScreenshot("$_platformFolder/${const String.fromEnvironment('deviceName')}/$name");
|
||||||
await pumpAndSettle();
|
await pumpAndSettle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
13
screenshots/scripts/generate_ios_screenshots.sh
Normal file
13
screenshots/scripts/generate_ios_screenshots.sh
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
devices_array=("iPhone 8 Plus" "iPhone 13 Pro" "iPhone 13 Pro Max" "iPhone 15 Pro" "iPhone 15 Pro Max" "iPad Pro (12.9-inch) (6th generation)")
|
||||||
|
|
||||||
|
open -a Simulator
|
||||||
|
|
||||||
|
for i in "${devices_array[@]}"; do # https://www.baeldung.com/linux/shell-script-iterate-over-string-list#2-understanding--and--special-indices
|
||||||
|
echo "$i"
|
||||||
|
xcrun simctl boot "$i"
|
||||||
|
#uid=$(echo "$(fvm flutter devices)" | sed -n -r "s/$i \(mobile\) • (.*) • .* • .*\(simulator\)/\1/p")
|
||||||
|
#echo $uid
|
||||||
|
sh screenshots/scripts/generate_screenshots.sh "$i"
|
||||||
|
done
|
||||||
|
|
||||||
|
killall 'Simulator'
|
|
@ -1,9 +1,12 @@
|
||||||
flutter drive \
|
deviceName="$1"
|
||||||
--dart-define="cameraPreviewAspectRatio=240/320" \
|
|
||||||
|
fvm flutter drive \
|
||||||
|
-d "$deviceName" \
|
||||||
--dart-define="cameraStubImage=assets/camera_stub_image.jpg" \
|
--dart-define="cameraStubImage=assets/camera_stub_image.jpg" \
|
||||||
|
--dart-define="deviceName=$deviceName" \
|
||||||
--driver=test_driver/screenshot_driver.dart \
|
--driver=test_driver/screenshot_driver.dart \
|
||||||
--target=screenshots/generate_screenshots.dart \
|
--target=screenshots/generate_screenshots.dart \
|
||||||
--profile \
|
--debug \
|
||||||
--flavor=dev \
|
--flavor=dev \
|
||||||
--no-dds \
|
--no-dds \
|
||||||
--endless-trace-buffer \
|
--endless-trace-buffer \
|
|
@ -7,7 +7,7 @@ Future<void> main() async {
|
||||||
await grantCameraPermission();
|
await grantCameraPermission();
|
||||||
await integrationDriver(
|
await integrationDriver(
|
||||||
onScreenshot: (name, bytes, [args]) async {
|
onScreenshot: (name, bytes, [args]) async {
|
||||||
final File image = await File('screenshots/$name.png').create(recursive: true);
|
final File image = await File('screenshots/generated/$name.png').create(recursive: true);
|
||||||
image.writeAsBytesSync(bytes);
|
image.writeAsBytesSync(bytes);
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue