Implemented IAP & Equipment profiles (#89)

* added equipment profiles to layout config

* calculate layout height based on `MeteringScreenLayoutFeature`

* Update cd_dev.yml

* Fixed equipment profile tile padding

* import

* `webfactory/ssh-agent`

* Update pubspec.yaml

* fixed `MeteringScreenLayoutConfigJson` tests

* fixed `UserPreferencesService` tests

* reset selected equipment profile when layout feature is disabled

* `IAPProductType.equipment` -> `IAPProductType.paidFeatures`

* updated packages versions

* Update shared_prefs_service.dart

* Fixed & tested exposure pairs list builder

* typo

* typo

* added iap repo stub

* Renamed `EquipmentProfileData` ->`EquipmentProfile`

* Moved `EquipmentProfileProvider` to iap repo

* Update README.md

* Fixed `EquipmentProfileListener`

* Improved `EquipmentProfilesListTile` statuses visualization

* Update README.md

* Update ci.yml

* Post-merge fixes

* typo

* Added workflow checks

* more sophisticated iap icons

* Include IAP by default

* added loader for `IAPProductStatus.pending`

* typo

* Added equipment profiles list placeholder

* typo

* separated `IconPlaceholder`

* improved `buildExposureValues` testing

* cleanup
This commit is contained in:
Vadim 2023-09-02 10:32:08 +02:00 committed by GitHub
parent d364de4486
commit 4bb080a144
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 1608 additions and 294 deletions

View file

@ -16,14 +16,28 @@ on:
- dev
- prod
default: 'dev'
include-iap:
type: boolean
description: Include IAP package
default: true
jobs:
build:
name: Build .apk
runs-on: macos-11
timeout-minutes: 15
steps:
- name: Connect private iap package
uses: webfactory/ssh-agent@v0.8.0
if: ${{ inputs.include-iap }}
with:
ssh-private-key: ${{ secrets.M3_LIGHTMETER_IAP_KEY }}
- name: Override iap package with stub
if: ${{ !inputs.include-iap }}
run: |
echo "\ndependency_overrides:\n m3_lightmeter_iap:\n path: iap" >> pubspec.yaml
- uses: actions/checkout@v3
with:
submodules: recursive

View file

@ -31,6 +31,10 @@ on:
type: boolean
description: Create Google Play release
default: true
include-iap:
type: boolean
description: Include IAP package
default: true
env:
BUILD_ARGS: --release --flavor prod --dart-define cameraPreviewAspectRatio=240/320 -t lib/main_prod.dart
@ -38,10 +42,21 @@ env:
jobs:
build:
name: Build .apk & .aab
if: ${{ inputs.github-release }} || ${{ inputs.google-play-release }}
if: ${{ inputs.github-release || inputs.google-play-release }}
runs-on: macos-11
timeout-minutes: 30
steps:
- name: Connect private iap package
uses: webfactory/ssh-agent@v0.8.0
if: ${{ inputs.include-iap }}
with:
ssh-private-key: ${{ secrets.M3_LIGHTMETER_IAP_KEY }}
- name: Override iap package with stub
if: ${{ !inputs.include-iap }}
run: |
echo "\ndependency_overrides:\n m3_lightmeter_iap:\n path: iap" >> pubspec.yaml
- uses: actions/checkout@v3
with:
submodules: recursive

View file

@ -11,13 +11,27 @@ on:
pull_request:
branches: ["main"]
env:
# Stub iap package if this worlflow is running from the PR from a fork
STUB_IAP: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
jobs:
analyze_and_test:
name: Analyze & test
runs-on: macos-11
timeout-minutes: 10
steps:
- name: Connect private iap package
uses: webfactory/ssh-agent@v0.8.0
if: !env.STUB_IAP
with:
ssh-private-key: ${{ secrets.M3_LIGHTMETER_IAP_KEY }}
- name: Override iap package with stub
if: env.STUB_IAP
run: |
echo "\ndependency_overrides:\n m3_lightmeter_iap:\n path: iap" >> pubspec.yaml
- uses: actions/checkout@v3
with:
submodules: recursive
@ -25,7 +39,7 @@ jobs:
- uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: '3.10.0'
flutter-version: "3.10.0"
- name: Prepare flutter project
run: |
@ -37,4 +51,4 @@ jobs:
run: flutter analyze lib --fatal-infos
- name: Run tests
run: flutter test
run: flutter test

2
.vscode/launch.json vendored
View file

@ -31,6 +31,7 @@
{
"name": "prod (android)",
"request": "launch",
//"flutterMode": "release",
"type": "dart",
"args": [
"--flavor",
@ -43,6 +44,7 @@
{
"name": "prod (ios)",
"request": "launch",
//"flutterMode": "release",
"type": "dart",
"args": [
"--flavor",

View file

@ -39,7 +39,22 @@ Out of the box Firebase Crashlytics won't work. If you want to add Crashlytics t
### 3. Get packages
Fetch all the neccessary dependencies and generate translation files by running the following commands:
As part of the app's functionallity is in the private repo, you have to replace these lines in _pubspec.yaml_:
```yaml
m3_lightmeter_iap:
git:
url: "https://github.com/vodemn/m3_lightmeter_iap"
ref: main
```
with these:
```yaml
m3_lightmeter_iap:
path: iap
```
and run `flutter pub get` from the _iap/_ folder.
Then you can fetch all the neccessary dependencies and generate translation files by running the following commands:
```console
flutter pub get
flutter pub run intl_utils:generate
@ -69,4 +84,4 @@ Apple does not provide API for reading Lux stream form the ambient light sensor.
## Volume buttons action
This can be [implemented](https://stackoverflow.com/questions/70161271/ios-override-hardware-volume-buttons-same-as-zello) but the app will be rejected due to [2.5.9](https://developer.apple.com/app-store/review/guidelines/#software-requirements)
This can be [implemented](https://stackoverflow.com/questions/70161271/ios-override-hardware-volume-buttons-same-as-zello) but the app will be rejected due to [2.5.9](https://developer.apple.com/app-store/review/guidelines/#software-requirements)

36
iap/.gitignore vendored Normal file
View file

@ -0,0 +1,36 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
.packages
build/
.fvm/
*.properties
ios/Flutter/
.flutter-plugins
.flutter-plugins-dependencies

10
iap/.metadata Normal file
View file

@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: 9944297138845a94256f1cf37beb88ff9a8e811a
channel: stable
project_type: package

1
iap/LICENSE Normal file
View file

@ -0,0 +1 @@
TODO: Add your license here.

View file

@ -0,0 +1,4 @@
include: package:flutter_lints/flutter.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View file

@ -0,0 +1,30 @@
library m3_lightmeter_iap;
import 'package:flutter/material.dart';
import 'package:m3_lightmeter_iap/src/providers/equipment_profile_provider.dart';
import 'package:m3_lightmeter_iap/src/providers/iap_products_provider.dart';
export 'src/data/models/iap_product.dart';
export 'src/providers/equipment_profile_provider.dart' hide EquipmentProfilesAspect;
export 'src/providers/iap_products_provider.dart';
class IAPProviders extends StatelessWidget {
final Object sharedPreferences;
final Widget child;
const IAPProviders({
required this.sharedPreferences,
required this.child,
super.key,
});
@override
Widget build(BuildContext context) {
return IAPProductsProvider(
child: EquipmentProfileProvider(
child: child,
),
);
}
}

View file

@ -0,0 +1,5 @@
enum IAPProductType { paidFeatures }
class IAPProduct {
IAPProduct();
}

View file

@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class EquipmentProfileProvider extends StatefulWidget {
final Widget child;
const EquipmentProfileProvider({required this.child, super.key});
static EquipmentProfileProviderState of(BuildContext context) {
return context.findAncestorStateOfType<EquipmentProfileProviderState>()!;
}
@override
State<EquipmentProfileProvider> createState() => EquipmentProfileProviderState();
}
class EquipmentProfileProviderState extends State<EquipmentProfileProvider> {
static const EquipmentProfile _defaultProfile = EquipmentProfile(
id: '',
name: '',
apertureValues: ApertureValue.values,
ndValues: NdValue.values,
shutterSpeedValues: ShutterSpeedValue.values,
isoValues: IsoValue.values,
);
@override
Widget build(BuildContext context) {
return EquipmentProfiles(
profiles: const [_defaultProfile],
selected: _defaultProfile,
child: widget.child,
);
}
void setProfile(EquipmentProfile data) {}
void addProfile(String name) {}
void updateProdile(EquipmentProfile data) {}
void deleteProfile(EquipmentProfile data) {}
}
enum EquipmentProfilesAspect { list, selected }
class EquipmentProfiles extends InheritedModel<EquipmentProfilesAspect> {
const EquipmentProfiles({
super.key,
required this.profiles,
required this.selected,
required super.child,
});
final List<EquipmentProfile> profiles;
final EquipmentProfile selected;
static List<EquipmentProfile> of(BuildContext context) {
return InheritedModel.inheritFrom<EquipmentProfiles>(
context,
aspect: EquipmentProfilesAspect.list,
)!
.profiles;
}
static EquipmentProfile selectedOf(BuildContext context) {
return InheritedModel.inheritFrom<EquipmentProfiles>(
context,
aspect: EquipmentProfilesAspect.selected,
)!
.selected;
}
@override
bool updateShouldNotify(EquipmentProfiles oldWidget) => false;
@override
bool updateShouldNotifyDependent(EquipmentProfiles oldWidget, Set<EquipmentProfilesAspect> dependencies) => false;
}

View file

@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
import 'package:m3_lightmeter_iap/src/data/models/iap_product.dart';
class IAPProductsProvider extends StatefulWidget {
final Widget child;
const IAPProductsProvider({required this.child, super.key});
static IAPProductsProviderState of(BuildContext context) {
return context.findAncestorStateOfType<IAPProductsProviderState>()!;
}
@override
State<IAPProductsProvider> createState() => IAPProductsProviderState();
}
class IAPProductsProviderState extends State<IAPProductsProvider> {
@override
Widget build(BuildContext context) {
return IAPProducts(
products: const [],
child: widget.child,
);
}
Future<void> buy(IAPProductType type) async {}
}
class IAPProducts extends InheritedModel<IAPProductType> {
final List<IAPProduct> products;
const IAPProducts({
required this.products,
required super.child,
super.key,
});
static IAPProduct? of(BuildContext context, IAPProductType type) => null;
static bool isPurchased(BuildContext context, IAPProductType type) => false;
@override
bool updateShouldNotify(IAPProducts oldWidget) => false;
@override
bool updateShouldNotifyDependent(covariant IAPProducts oldWidget, Set<IAPProductType> dependencies) => false;
}

25
iap/pubspec.yaml Normal file
View file

@ -0,0 +1,25 @@
name: m3_lightmeter_iap
description: IAP stubs for the M3 Lightmeter app.
version: 0.2.0
publish_to: 'none'
environment:
sdk: '>=2.19.2 <3.0.0'
flutter: ">=1.17.0"
dependencies:
flutter:
sdk: flutter
m3_lightmeter_resources:
git:
url: "https://github.com/vodemn/m3_lightmeter_resources"
ref: main
shared_preferences: 2.2.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
flutter:
uses-material-design: true

View file

@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 51;
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
@ -237,6 +237,7 @@
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
@ -268,6 +269,7 @@
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
@ -371,7 +373,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 74JQ9DBXY6;
DEVELOPMENT_TEAM = 489Z6UQMGN;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@ -500,7 +502,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 74JQ9DBXY6;
DEVELOPMENT_TEAM = 489Z6UQMGN;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@ -523,7 +525,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 74JQ9DBXY6;
DEVELOPMENT_TEAM = 489Z6UQMGN;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@ -600,7 +602,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 74JQ9DBXY6;
DEVELOPMENT_TEAM = 489Z6UQMGN;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@ -675,7 +677,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 74JQ9DBXY6;
DEVELOPMENT_TEAM = 489Z6UQMGN;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@ -747,7 +749,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 74JQ9DBXY6;
DEVELOPMENT_TEAM = 489Z6UQMGN;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (

View file

@ -10,11 +10,11 @@ import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/data/volume_events_service.dart';
import 'package:lightmeter/environment.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/providers/equipment_profile_provider.dart';
import 'package:lightmeter/providers/services_provider.dart';
import 'package:lightmeter/providers/user_preferences_provider.dart';
import 'package:lightmeter/screens/metering/flow_metering.dart';
import 'package:lightmeter/screens/settings/flow_settings.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:platform/platform.dart';
import 'package:shared_preferences/shared_preferences.dart';
@ -32,16 +32,18 @@ class Application extends StatelessWidget {
]),
builder: (_, snapshot) {
if (snapshot.data != null) {
return ServicesProvider(
caffeineService: const CaffeineService(),
environment: env.copyWith(hasLightSensor: snapshot.data![1] as bool),
hapticsService: const HapticsService(),
lightSensorService: const LightSensorService(LocalPlatform()),
permissionsService: const PermissionsService(),
userPreferencesService: UserPreferencesService(snapshot.data![0] as SharedPreferences),
volumeEventsService: const VolumeEventsService(LocalPlatform()),
child: UserPreferencesProvider(
child: EquipmentProfileProvider(
return IAPProviders(
sharedPreferences: snapshot.data![0] as SharedPreferences,
child: ServicesProvider(
caffeineService: const CaffeineService(),
environment: env.copyWith(hasLightSensor: snapshot.data![1] as bool),
hapticsService: const HapticsService(),
lightSensorService: const LightSensorService(LocalPlatform()),
permissionsService: const PermissionsService(),
userPreferencesService:
UserPreferencesService(snapshot.data![0] as SharedPreferences),
volumeEventsService: const VolumeEventsService(LocalPlatform()),
child: UserPreferencesProvider(
child: Builder(
builder: (context) {
final theme = UserPreferencesProvider.themeOf(context);

View file

@ -8,4 +8,16 @@ class ExposurePair {
@override
String toString() => '$aperture - $shutterSpeed';
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other.runtimeType != runtimeType) return false;
return other is ExposurePair &&
other.aperture == aperture &&
other.shutterSpeed == shutterSpeed;
}
@override
int get hashCode => Object.hash(aperture, shutterSpeed, runtimeType);
}

View file

@ -13,6 +13,8 @@ double log10polynomian(
) =>
a * pow(log10(x), 2) + b * log10(x) + c;
typedef ReciprocityFailureBuilder = ShutterSpeedValue Function(ShutterSpeedValue shutterSpeed);
/// Only Ilford films have reciprocity formulas provided by the manufacturer:
/// https://www.ilfordphoto.com/wp/wp-content/uploads/2017/06/Reciprocity-Failure-Compensation.pdf
///

View file

@ -1,9 +1,14 @@
enum MeteringScreenLayoutFeature { extremeExposurePairs, filmPicker, histogram }
enum MeteringScreenLayoutFeature {
extremeExposurePairs,
filmPicker,
histogram,
equipmentProfiles,
}
typedef MeteringScreenLayoutConfig = Map<MeteringScreenLayoutFeature, bool>;
extension MeteringScreenLayoutConfigJson on MeteringScreenLayoutConfig {
static MeteringScreenLayoutConfig fromJson(Map<String, dynamic> data) =>
static MeteringScreenLayoutConfig fromJson(Map<String, dynamic> data) =>
<MeteringScreenLayoutFeature, bool>{
for (final f in MeteringScreenLayoutFeature.values)
f: data[f.index.toString()] as bool? ?? true

View file

@ -95,6 +95,7 @@ class UserPreferencesService {
);
} else {
return {
MeteringScreenLayoutFeature.equipmentProfiles: true,
MeteringScreenLayoutFeature.extremeExposurePairs: true,
MeteringScreenLayoutFeature.filmPicker: true,
MeteringScreenLayoutFeature.histogram: true,
@ -147,10 +148,4 @@ class UserPreferencesService {
orElse: () => Film.values.first,
);
set film(Film value) => _sharedPreferences.setString(filmKey, value.name);
String get selectedEquipmentProfileId => ''; // coverage:ignore-line
set selectedEquipmentProfileId(String id) {} // coverage:ignore-line
List<EquipmentProfile> get equipmentProfiles => []; // coverage:ignore-line
set equipmentProfiles(List<EquipmentProfile> profiles) {} // coverage:ignore-line
}

View file

@ -1,3 +0,0 @@
class FeaturesConfig {
static const bool equipmentProfilesEnabled = false;
}

View file

@ -54,6 +54,7 @@
"isoValuesFilterDescription": "Select the ISO values to display. These may be your most commonly used values or those supported by your camera.",
"equipmentProfile": "Equipment profile",
"equipmentProfiles": "Equipment profiles",
"tapToAdd": "Tap to add",
"general": "General",
"keepScreenOn": "Keep screen on",
"haptics": "Haptics",
@ -86,4 +87,4 @@
}
}
}
}
}

View file

@ -42,6 +42,7 @@
"film": "Pellicule",
"equipment": "Équipement",
"equipmentProfileName": "Nom du profil de l'équipement",
"tapToAdd": "Appuie pour ajouter",
"equipmentProfileNameHint": "Praktica MTL5B",
"equipmentProfileAllValues": "Tout",
"apertureValues": "Valeurs Aperture",
@ -86,4 +87,4 @@
}
}
}
}
}

View file

@ -54,6 +54,7 @@
"isoValuesFilterDescription": "Выберите значения ISO для отображения. Это может быть наиболее часто используемые значения или значения, поддерживаемые вашей камерой.",
"equipmentProfile": "Оборудование",
"equipmentProfiles": "Профили оборудования",
"tapToAdd": "Нажмите, чтобы добавить",
"general": "Общие",
"keepScreenOn": "Запрет блокировки",
"haptics": "Вибрация",
@ -86,4 +87,4 @@
}
}
}
}
}

View file

@ -54,6 +54,7 @@
"isoValuesFilterDescription": "选择要显示的 ISO。这些值可能是您最常用的值也可能是相机支持的值。",
"equipmentProfile": "设备配置",
"equipmentProfiles": "设备配置",
"tapToAdd": "點擊添加",
"general": "通用",
"keepScreenOn": "保持屏幕常亮",
"haptics": "震动",

View file

@ -1,139 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/providers/services_provider.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
import 'package:uuid/uuid.dart';
// TODO(@vodemn): This will be removed in #89
class EquipmentProfileProvider extends StatefulWidget {
final Widget child;
const EquipmentProfileProvider({required this.child, super.key});
static EquipmentProfileProviderState of(BuildContext context) {
return context.findAncestorStateOfType<EquipmentProfileProviderState>()!;
}
@override
State<EquipmentProfileProvider> createState() => EquipmentProfileProviderState();
}
class EquipmentProfileProviderState extends State<EquipmentProfileProvider> {
static const EquipmentProfile _defaultProfile = EquipmentProfile(
id: '',
name: '',
apertureValues: ApertureValue.values,
ndValues: NdValue.values,
shutterSpeedValues: ShutterSpeedValue.values,
isoValues: IsoValue.values,
);
List<EquipmentProfile> _customProfiles = [];
String _selectedId = '';
EquipmentProfile get _selectedProfile => _customProfiles.firstWhere(
(e) => e.id == _selectedId,
orElse: () {
ServicesProvider.of(context).userPreferencesService.selectedEquipmentProfileId =
_defaultProfile.id;
return _defaultProfile;
},
);
@override
void initState() {
super.initState();
_selectedId = ServicesProvider.of(context).userPreferencesService.selectedEquipmentProfileId;
_customProfiles = ServicesProvider.of(context).userPreferencesService.equipmentProfiles;
}
@override
Widget build(BuildContext context) {
return EquipmentProfiles(
profiles: [_defaultProfile] + _customProfiles,
selected: _selectedProfile,
child: widget.child,
);
}
void setProfile(EquipmentProfile data) {
setState(() {
_selectedId = data.id;
});
ServicesProvider.of(context).userPreferencesService.selectedEquipmentProfileId =
_selectedProfile.id;
}
/// Creates a default equipment profile
void addProfile(String name) {
_customProfiles.add(
EquipmentProfile(
id: const Uuid().v1(),
name: name,
apertureValues: ApertureValue.values,
ndValues: NdValue.values,
shutterSpeedValues: ShutterSpeedValue.values,
isoValues: IsoValue.values,
),
);
_refreshSavedProfiles();
}
void updateProdile(EquipmentProfile data) {
final indexToUpdate = _customProfiles.indexWhere((element) => element.id == data.id);
if (indexToUpdate >= 0) {
_customProfiles[indexToUpdate] = data;
_refreshSavedProfiles();
}
}
void deleteProfile(EquipmentProfile data) {
_customProfiles.remove(data);
_refreshSavedProfiles();
}
void _refreshSavedProfiles() {
ServicesProvider.of(context).userPreferencesService.equipmentProfiles = _customProfiles;
setState(() {});
}
}
// Copied from #89
enum EquipmentProfilesAspect { list, selected }
class EquipmentProfiles extends InheritedModel<EquipmentProfilesAspect> {
const EquipmentProfiles({
super.key,
required this.profiles,
required this.selected,
required super.child,
});
final List<EquipmentProfile> profiles;
final EquipmentProfile selected;
static List<EquipmentProfile> of(BuildContext context) {
return InheritedModel.inheritFrom<EquipmentProfiles>(
context,
aspect: EquipmentProfilesAspect.list,
)!
.profiles;
}
static EquipmentProfile selectedOf(BuildContext context) {
return InheritedModel.inheritFrom<EquipmentProfiles>(
context,
aspect: EquipmentProfilesAspect.selected,
)!
.selected;
}
@override
bool updateShouldNotify(EquipmentProfiles oldWidget) => false;
@override
bool updateShouldNotifyDependent(
EquipmentProfiles oldWidget,
Set<EquipmentProfilesAspect> dependencies,
) =>
false;
}

View file

@ -14,7 +14,6 @@ class Dimens {
static const double grid48 = 48;
static const double grid56 = 56;
static const double grid72 = 72;
static const double grid168 = 168;
static const double paddingS = 8;
static const double paddingM = 16;
@ -30,6 +29,8 @@ class Dimens {
static const double enabledOpacity = 1.0;
static const double disabledOpacity = 0.38;
static const double sliverAppBarExpandedHeight = 168;
// TopBar
static const double readingContainerDoubleValueHeight = 128;
static const double readingContainerSingleValueHeight = 76;

View file

@ -18,7 +18,7 @@ import 'package:lightmeter/screens/metering/components/camera_container/event_co
import 'package:lightmeter/screens/metering/components/camera_container/models/camera_error_type.dart';
import 'package:lightmeter/screens/metering/components/camera_container/state_container_camera.dart';
import 'package:lightmeter/screens/metering/components/shared/ev_source_base/bloc_base_ev_source.dart';
import 'package:lightmeter/utils/log_2.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraContainerState> {
final MeteringInteractor _meteringInteractor;

View file

@ -5,7 +5,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/data/models/film.dart';
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
import 'package:lightmeter/features.dart';
import 'package:lightmeter/platform_config.dart';
import 'package:lightmeter/providers/user_preferences_provider.dart';
import 'package:lightmeter/res/dimens.dart';
@ -110,7 +109,10 @@ class CameraContainer extends StatelessWidget {
double _meteringContainerHeight(BuildContext context) {
double enabledFeaturesHeight = 0;
if (FeaturesConfig.equipmentProfilesEnabled) {
if (UserPreferencesProvider.meteringScreenFeatureOf(
context,
MeteringScreenLayoutFeature.equipmentProfiles,
)) {
enabledFeaturesHeight += Dimens.readingContainerSingleValueHeight;
enabledFeaturesHeight += Dimens.paddingS;
}
@ -133,7 +135,7 @@ class CameraContainer extends StatelessWidget {
}
double _cameraPreviewHeight(BuildContext context) {
return ((MediaQuery.of(context).size.width - Dimens.grid8 - 2 * Dimens.paddingM) / 2) /
return ((MediaQuery.sizeOf(context).width - Dimens.grid8 - 2 * Dimens.paddingM) / 2) /
PlatformConfig.cameraPreviewAspectRatio;
}
}

View file

@ -10,7 +10,7 @@ import 'package:lightmeter/screens/metering/communication/state_communication_me
import 'package:lightmeter/screens/metering/components/light_sensor_container/event_container_light_sensor.dart';
import 'package:lightmeter/screens/metering/components/light_sensor_container/state_container_light_sensor.dart';
import 'package:lightmeter/screens/metering/components/shared/ev_source_base/bloc_base_ev_source.dart';
import 'package:lightmeter/utils/log_2.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class LightSensorContainerBloc
extends EvSourceBlocBase<LightSensorContainerEvent, LightSensorContainerState> {

View file

@ -1,9 +1,10 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/metering/components/shared/exposure_pairs_list/components/empty_exposure_pairs_list/widget_list_exposure_pairs_empty.dart';
import 'package:lightmeter/screens/metering/components/shared/exposure_pairs_list/components/exposure_pairs_list_item/widget_item_list_exposure_pairs.dart';
import 'package:lightmeter/screens/shared/icon_placeholder/widget_icon_placeholder.dart';
class ExposurePairsList extends StatelessWidget {
final List<ExposurePair> exposurePairs;
@ -15,7 +16,10 @@ class ExposurePairsList extends StatelessWidget {
return AnimatedSwitcher(
duration: Dimens.switchDuration,
child: exposurePairs.isEmpty
? const EmptyExposurePairsList()
? IconPlaceholder(
icon: Icons.not_interested,
text: S.of(context).noExposurePairs,
)
: Stack(
alignment: Alignment.center,
children: [

View file

@ -2,13 +2,12 @@ import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/data/models/film.dart';
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
import 'package:lightmeter/features.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/providers/equipment_profile_provider.dart';
import 'package:lightmeter/providers/user_preferences_provider.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/animated_dialog_picker/widget_picker_dialog_animated.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/reading_value_container/widget_container_reading_value.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class ReadingsContainer extends StatelessWidget {
@ -38,7 +37,10 @@ class ReadingsContainer extends StatelessWidget {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (FeaturesConfig.equipmentProfilesEnabled) ...[
if (UserPreferencesProvider.meteringScreenFeatureOf(
context,
MeteringScreenLayoutFeature.equipmentProfiles,
)) ...[
const _EquipmentProfilePicker(),
const _InnerPadding(),
],

View file

@ -6,7 +6,6 @@ import 'package:lightmeter/data/models/ev_source_type.dart';
import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/data/models/film.dart';
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
import 'package:lightmeter/providers/equipment_profile_provider.dart';
import 'package:lightmeter/providers/services_provider.dart';
import 'package:lightmeter/providers/user_preferences_provider.dart';
import 'package:lightmeter/screens/metering/bloc_metering.dart';
@ -17,6 +16,7 @@ import 'package:lightmeter/screens/metering/event_metering.dart';
import 'package:lightmeter/screens/metering/state_metering.dart';
import 'package:lightmeter/screens/metering/utils/listener_metering_layout_feature.dart';
import 'package:lightmeter/screens/metering/utils/listsner_equipment_profiles.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class MeteringScreen extends StatelessWidget {
@ -31,7 +31,7 @@ class MeteringScreen extends StatelessWidget {
children: [
Expanded(
child: BlocBuilder<MeteringBloc, MeteringState>(
builder: (_, state) => _MeteringContainerBuidler(
builder: (_, state) => MeteringContainerBuidler(
ev: state is MeteringDataState ? state.ev : null,
film: state.film,
iso: state.iso,
@ -80,15 +80,25 @@ class _InheritedListeners extends StatelessWidget {
child: MeteringScreenLayoutFeatureListener(
feature: MeteringScreenLayoutFeature.filmPicker,
onDidChangeDependencies: (value) {
if (!value) context.read<MeteringBloc>().add(const FilmChangedEvent(Film.other()));
if (!value) {
context.read<MeteringBloc>().add(const FilmChangedEvent(Film.other()));
}
},
child: child,
child: MeteringScreenLayoutFeatureListener(
feature: MeteringScreenLayoutFeature.equipmentProfiles,
onDidChangeDependencies: (value) {
if (!value) {
EquipmentProfileProvider.of(context).setProfile(EquipmentProfiles.of(context).first);
}
},
child: child,
),
),
);
}
}
class _MeteringContainerBuidler extends StatelessWidget {
class MeteringContainerBuidler extends StatelessWidget {
final double? ev;
final Film film;
final IsoValue iso;
@ -97,7 +107,7 @@ class _MeteringContainerBuidler extends StatelessWidget {
final ValueChanged<IsoValue> onIsoChanged;
final ValueChanged<NdValue> onNdChanged;
const _MeteringContainerBuidler({
const MeteringContainerBuidler({
required this.ev,
required this.film,
required this.iso,
@ -109,7 +119,14 @@ class _MeteringContainerBuidler extends StatelessWidget {
@override
Widget build(BuildContext context) {
final exposurePairs = ev != null ? buildExposureValues(context, ev!, film) : <ExposurePair>[];
final exposurePairs = ev != null
? buildExposureValues(
ev!,
UserPreferencesProvider.stopTypeOf(context),
EquipmentProfiles.selectedOf(context),
film,
)
: <ExposurePair>[];
final fastest = exposurePairs.isNotEmpty ? exposurePairs.first : null;
final slowest = exposurePairs.isNotEmpty ? exposurePairs.last : null;
// Doubled build here when switching evSourceType. As new source bloc fires a new state on init
@ -138,39 +155,28 @@ class _MeteringContainerBuidler extends StatelessWidget {
);
}
List<ExposurePair> buildExposureValues(BuildContext context, double ev, Film film) {
@visibleForTesting
static List<ExposurePair> buildExposureValues(
double ev,
StopType stopType,
EquipmentProfile equipmentProfile,
Film film,
) {
if (ev.isNaN || ev.isInfinite) {
return List.empty();
}
/// Depending on the `stopType` the exposure pairs list length is multiplied by 1,2 or 3
final StopType stopType = UserPreferencesProvider.stopTypeOf(context);
final int evSteps = (ev * (stopType.index + 1)).round();
final EquipmentProfile equipmentProfile = EquipmentProfiles.selectedOf(context);
final List<ApertureValue> apertureValues =
equipmentProfile.apertureValues.whereStopType(stopType);
final List<ShutterSpeedValue> shutterSpeedValues =
equipmentProfile.shutterSpeedValues.whereStopType(stopType);
final apertureValues = ApertureValue.values.whereStopType(stopType);
final shutterSpeedValues = ShutterSpeedValue.values.whereStopType(stopType);
/// Basically we use 1" shutter speed as an anchor point for building the exposure pairs list.
/// But user can exclude this value from the list using custom equipment profile.
/// So we have to restore the index of the anchor value.
const ShutterSpeedValue anchorShutterSpeed = ShutterSpeedValue(1, false, StopType.full);
int anchorIndex = shutterSpeedValues.indexOf(anchorShutterSpeed);
if (anchorIndex < 0) {
final filteredFullList = ShutterSpeedValue.values.whereStopType(stopType);
final customListStartIndex = filteredFullList.indexOf(shutterSpeedValues.first);
final fullListAnchor = filteredFullList.indexOf(anchorShutterSpeed);
if (customListStartIndex < fullListAnchor) {
/// This means, that user excluded anchor value at the end,
/// i.e. all shutter speed values are shorter than 1".
anchorIndex = fullListAnchor - customListStartIndex;
} else {
/// In case user excludes anchor value at the start,
/// we can do no adjustment.
}
}
const anchorShutterSpeed = ShutterSpeedValue(1, false, StopType.full);
final int anchorIndex = shutterSpeedValues.indexOf(anchorShutterSpeed);
final int evOffset = anchorIndex - evSteps;
late final int apertureOffset;
@ -189,10 +195,11 @@ class _MeteringContainerBuidler extends StatelessWidget {
) -
max(apertureOffset, shutterSpeedOffset);
if (itemsCount < 0) {
if (itemsCount <= 0) {
return List.empty();
}
return List.generate(
final exposurePairs = List.generate(
itemsCount,
(index) => ExposurePair(
apertureValues[index + apertureOffset],
@ -200,5 +207,30 @@ class _MeteringContainerBuidler extends StatelessWidget {
),
growable: false,
);
/// Full equipment profile, nothing to cut
if (equipmentProfile.id == "") {
return exposurePairs;
}
final equipmentApertureValues = equipmentProfile.apertureValues.whereStopType(stopType);
final equipmentShutterSpeedValues = equipmentProfile.shutterSpeedValues.whereStopType(stopType);
final startCutEV = max(
exposurePairs.first.aperture.difference(equipmentApertureValues.first),
exposurePairs.first.shutterSpeed.difference(equipmentShutterSpeedValues.first),
);
final endCutEV = max(
equipmentApertureValues.last.difference(exposurePairs.last.aperture),
equipmentShutterSpeedValues.last.difference(exposurePairs.last.shutterSpeed),
);
final startCut = (startCutEV * (stopType.index + 1)).round().clamp(0, itemsCount);
final endCut = (endCutEV * (stopType.index + 1)).round().clamp(0, itemsCount);
if (startCut > itemsCount - endCut) {
return const [];
}
return exposurePairs.sublist(startCut, itemsCount - endCut);
}
}

View file

@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class EquipmentProfileListener extends StatefulWidget {
final ValueChanged<EquipmentProfile> onDidChangeDependencies;
final Widget child;
const EquipmentProfileListener({
required this.onDidChangeDependencies,
required this.child,
super.key,
});
@override
State<EquipmentProfileListener> createState() => _EquipmentProfileListenerState();
}
class _EquipmentProfileListenerState extends State<EquipmentProfileListener> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
widget.onDidChangeDependencies(EquipmentProfiles.selectedOf(context));
}
@override
Widget build(BuildContext context) {
return widget.child;
}
}

View file

@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/providers/equipment_profile_provider.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class EquipmentProfileListener extends StatefulWidget {

View file

@ -71,6 +71,7 @@ class EquipmentProfileContainerState extends State<EquipmentProfileContainer>
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
title: Row(
children: [
_AnimatedNameLeading(controller: _controller),
@ -163,7 +164,7 @@ class _AnimatedNameLeading extends AnimatedWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(right: _progress.value * Dimens.grid24),
padding: EdgeInsets.only(right: _progress.value * Dimens.grid8),
child: Icon(
Icons.edit,
size: _progress.value * Dimens.grid24,

View file

@ -1,11 +1,12 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/providers/equipment_profile_provider.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart';
import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_name_dialog/widget_dialog_equipment_profile_name.dart';
import 'package:lightmeter/screens/shared/icon_placeholder/widget_icon_placeholder.dart';
import 'package:lightmeter/screens/shared/sliver_screen/screen_sliver.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class EquipmentProfilesScreen extends StatefulWidget {
@ -44,30 +45,38 @@ class _EquipmentProfilesScreenState extends State<EquipmentProfilesScreen> {
icon: const Icon(Icons.close),
),
],
slivers: [
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => index > 0
? Padding(
padding: EdgeInsets.fromLTRB(
Dimens.paddingM,
index == 0 ? Dimens.paddingM : 0,
Dimens.paddingM,
Dimens.paddingM,
),
child: EquipmentProfileContainer(
key: profileContainersKeys[index],
data: EquipmentProfiles.of(context)[index],
onExpand: () => _keepExpandedAt(index),
onUpdate: (profileData) => _updateProfileAt(profileData, index),
onDelete: () => _removeProfileAt(index),
),
)
: const SizedBox.shrink(),
childCount: profilesCount,
),
),
],
slivers: profilesCount == 1
? [
SliverFillRemaining(
hasScrollBody: false,
child: _EquipmentProfilesListPlaceholder(onTap: _addProfile),
)
]
: [
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => index > 0 // skip default
? Padding(
padding: EdgeInsets.fromLTRB(
Dimens.paddingM,
index == 0 ? Dimens.paddingM : 0,
Dimens.paddingM,
Dimens.paddingM,
),
child: EquipmentProfileContainer(
key: profileContainersKeys[index],
data: EquipmentProfiles.of(context)[index],
onExpand: () => _keepExpandedAt(index),
onUpdate: (profileData) => _updateProfileAt(profileData, index),
onDelete: () => _removeProfileAt(index),
),
)
: const SizedBox.shrink(),
childCount: profilesCount,
),
),
SliverToBoxAdapter(child: SizedBox(height: MediaQuery.paddingOf(context).bottom)),
],
);
}
@ -99,3 +108,32 @@ class _EquipmentProfilesScreenState extends State<EquipmentProfilesScreen> {
});
}
}
class _EquipmentProfilesListPlaceholder extends StatelessWidget {
final VoidCallback onTap;
const _EquipmentProfilesListPlaceholder({required this.onTap});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: Dimens.sliverAppBarExpandedHeight),
child: FractionallySizedBox(
widthFactor: 1 / 1.618,
child: Center(
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(Dimens.paddingL),
child: IconPlaceholder(
icon: Icons.add,
text: S.of(context).tapToAdd,
),
),
),
),
),
);
}
}

View file

@ -1,6 +1,10 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class EquipmentProfilesListTile extends StatelessWidget {
@ -8,13 +12,31 @@ class EquipmentProfilesListTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
final paidStatus = IAPProducts.productOf(context, IAPProductType.paidFeatures)?.status ??
IAPProductStatus.pending;
log(paidStatus.toString());
return ListTile(
leading: const Icon(Icons.camera),
title: Text(S.of(context).equipmentProfiles),
onTap: () {
Navigator.of(context).push<EquipmentProfile>(
MaterialPageRoute(builder: (_) => const EquipmentProfilesScreen()),
);
onTap: switch (paidStatus) {
IAPProductStatus.purchased => () {
Navigator.of(context).push<EquipmentProfile>(
MaterialPageRoute(builder: (_) => const EquipmentProfilesScreen()),
);
},
IAPProductStatus.pending => null,
_ => () {
IAPProductsProvider.of(context).buy(IAPProductType.paidFeatures);
},
},
trailing: switch (paidStatus) {
IAPProductStatus.purchasable => const Icon(Icons.lock),
IAPProductStatus.pending => const SizedBox(
height: Dimens.grid24,
width: Dimens.grid24,
child: CircularProgressIndicator(),
),
_ => null,
},
);
}

View file

@ -33,18 +33,10 @@ class _MeteringScreenLayoutFeaturesDialogState extends State<MeteringScreenLayou
child: Text(S.of(context).meteringScreenLayoutHint),
),
const SizedBox(height: Dimens.grid16),
...MeteringScreenLayoutFeature.values.map(
(f) => SwitchListTile(
contentPadding: EdgeInsets.symmetric(horizontal: Dimens.dialogTitlePadding.left),
title: Text(_toStringLocalized(context, f)),
value: _features[f]!,
onChanged: (value) {
setState(() {
_features.update(f, (_) => value);
});
},
),
),
_featureListTile(MeteringScreenLayoutFeature.equipmentProfiles),
_featureListTile(MeteringScreenLayoutFeature.extremeExposurePairs),
_featureListTile(MeteringScreenLayoutFeature.filmPicker),
_featureListTile(MeteringScreenLayoutFeature.histogram),
],
),
),
@ -65,8 +57,23 @@ class _MeteringScreenLayoutFeaturesDialogState extends State<MeteringScreenLayou
);
}
Widget _featureListTile(MeteringScreenLayoutFeature f) {
return SwitchListTile(
contentPadding: EdgeInsets.symmetric(horizontal: Dimens.dialogTitlePadding.left),
title: Text(_toStringLocalized(context, f)),
value: _features[f]!,
onChanged: (value) {
setState(() {
_features.update(f, (_) => value);
});
},
);
}
String _toStringLocalized(BuildContext context, MeteringScreenLayoutFeature feature) {
switch (feature) {
case MeteringScreenLayoutFeature.equipmentProfiles:
return S.of(context).equipmentProfiles;
case MeteringScreenLayoutFeature.extremeExposurePairs:
return S.of(context).meteringScreenFeatureExtremeExposurePairs;
case MeteringScreenLayoutFeature.filmPicker:

View file

@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/features.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/screens/settings/components/metering/components/calibration/widget_list_tile_calibration.dart';
import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/widget_list_tile_equipment_profiles.dart';
@ -18,7 +17,7 @@ class MeteringSettingsSection extends StatelessWidget {
StopTypeListTile(),
CalibrationListTile(),
MeteringScreenLayoutListTile(),
if (FeaturesConfig.equipmentProfilesEnabled) EquipmentProfilesListTile(),
EquipmentProfilesListTile(),
],
);
}

View file

@ -50,6 +50,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
],
),
),
SliverToBoxAdapter(child: SizedBox(height: MediaQuery.paddingOf(context).bottom)),
],
),
);

View file

@ -1,24 +1,30 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/dimens.dart';
class EmptyExposurePairsList extends StatelessWidget {
const EmptyExposurePairsList({super.key});
class IconPlaceholder extends StatelessWidget {
final IconData icon;
final String text;
const IconPlaceholder({
required this.icon,
required this.text,
super.key,
});
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width / 2),
constraints: BoxConstraints(maxWidth: MediaQuery.sizeOf(context).width / 2),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.not_interested,
icon,
color: Theme.of(context).colorScheme.onBackground,
),
const SizedBox(height: Dimens.grid8),
Text(
S.of(context).noExposurePairs,
text,
style: Theme.of(context)
.textTheme
.bodyMedium

View file

@ -24,7 +24,7 @@ class SliverScreen extends StatelessWidget {
SliverAppBar(
pinned: true,
automaticallyImplyLeading: false,
expandedHeight: Dimens.grid168,
expandedHeight: Dimens.sliverAppBarExpandedHeight,
flexibleSpace: FlexibleSpaceBar(
centerTitle: false,
titlePadding: const EdgeInsets.all(Dimens.paddingM),
@ -39,7 +39,6 @@ class SliverScreen extends StatelessWidget {
actions: appBarActions,
),
...slivers,
SliverToBoxAdapter(child: SizedBox(height: MediaQuery.of(context).padding.bottom)),
],
),
),

View file

@ -1,3 +0,0 @@
import 'dart:math';
double log2(num x) => log(x) / log(2);

View file

@ -1,5 +1,5 @@
name: lightmeter
description: A new Flutter project.
description: Lightmeter app inspired by Material 3 design system.
publish_to: "none"
version: 0.13.2+38
@ -11,10 +11,10 @@ dependencies:
bloc_concurrency: 0.2.2
camera: 0.10.5+2
clipboard: 0.1.3
dynamic_color: 1.6.5
dynamic_color: 1.6.6
exif: 3.1.4
firebase_core: 2.13.0
firebase_crashlytics: 3.3.1
firebase_core: 2.14.0
firebase_crashlytics: 3.3.3
flutter:
sdk: flutter
flutter_bloc: 8.1.3
@ -23,22 +23,26 @@ dependencies:
intl: 0.18.0
intl_utils: 2.8.2
light_sensor: 2.0.2
m3_lightmeter_iap:
git:
url: "https://github.com/vodemn/m3_lightmeter_iap"
ref: main
m3_lightmeter_resources:
git:
url: "https://github.com/vodemn/m3_lightmeter_resources"
ref: main
material_color_utilities: 0.2.0
package_info_plus: 4.0.1
permission_handler: 10.2.0
package_info_plus: 4.0.2
permission_handler: 10.4.3
platform: 3.1.0
shared_preferences: 2.1.1
url_launcher: 6.1.11
shared_preferences: 2.2.0
url_launcher: 6.1.12
uuid: 3.0.7
vibration: 1.7.7
vibration: 1.8.1
dev_dependencies:
bloc_test: 9.1.3
build_runner: ^2.1.7
build_runner: 2.4.6
flutter_launcher_icons: 0.11.0
flutter_native_splash: 2.2.16
flutter_test:

View file

@ -12,12 +12,31 @@ void main() {
'0': true,
'1': true,
'2': true,
'3': true,
},
),
{
MeteringScreenLayoutFeature.extremeExposurePairs: true,
MeteringScreenLayoutFeature.filmPicker: true,
MeteringScreenLayoutFeature.histogram: true,
MeteringScreenLayoutFeature.equipmentProfiles: true,
},
);
});
test('Legacy (no histogram & equipment profiles)', () {
expect(
MeteringScreenLayoutConfigJson.fromJson(
{
'0': false,
'1': false,
},
),
{
MeteringScreenLayoutFeature.extremeExposurePairs: false,
MeteringScreenLayoutFeature.filmPicker: false,
MeteringScreenLayoutFeature.histogram: true,
MeteringScreenLayoutFeature.equipmentProfiles: true,
},
);
});
@ -26,28 +45,32 @@ void main() {
expect(
MeteringScreenLayoutConfigJson.fromJson(
{
'0': true,
'1': true,
'0': false,
'1': false,
'2': false,
},
),
{
MeteringScreenLayoutFeature.extremeExposurePairs: true,
MeteringScreenLayoutFeature.filmPicker: true,
MeteringScreenLayoutFeature.histogram: true,
MeteringScreenLayoutFeature.extremeExposurePairs: false,
MeteringScreenLayoutFeature.filmPicker: false,
MeteringScreenLayoutFeature.histogram: false,
MeteringScreenLayoutFeature.equipmentProfiles: true,
},
);
});
},
);
test('toJson', () {
test('toJson()', () {
expect(
{
MeteringScreenLayoutFeature.equipmentProfiles: true,
MeteringScreenLayoutFeature.extremeExposurePairs: true,
MeteringScreenLayoutFeature.filmPicker: true,
MeteringScreenLayoutFeature.histogram: true,
}.toJson(),
{
'3': true,
'0': true,
'1': true,
'2': true,

View file

@ -193,6 +193,7 @@ void main() {
{
MeteringScreenLayoutFeature.extremeExposurePairs: true,
MeteringScreenLayoutFeature.filmPicker: true,
MeteringScreenLayoutFeature.equipmentProfiles: true,
MeteringScreenLayoutFeature.histogram: true,
},
);
@ -207,6 +208,7 @@ void main() {
{
MeteringScreenLayoutFeature.extremeExposurePairs: false,
MeteringScreenLayoutFeature.filmPicker: true,
MeteringScreenLayoutFeature.equipmentProfiles: true,
MeteringScreenLayoutFeature.histogram: true,
},
);
@ -216,18 +218,19 @@ void main() {
when(
() => sharedPreferences.setString(
UserPreferencesService.meteringScreenLayoutKey,
"""{"0":false,"1":true,"2":true}""",
"""{"0":false,"1":true,"2":true,"3":true}""",
),
).thenAnswer((_) => Future.value(true));
service.meteringScreenLayout = {
MeteringScreenLayoutFeature.extremeExposurePairs: false,
MeteringScreenLayoutFeature.filmPicker: true,
MeteringScreenLayoutFeature.histogram: true,
MeteringScreenLayoutFeature.equipmentProfiles: true,
};
verify(
() => sharedPreferences.setString(
UserPreferencesService.meteringScreenLayoutKey,
"""{"0":false,"1":true,"2":true}""",
"""{"0":false,"1":true,"2":true,"3":true}""",
),
).called(1);
});

View file

@ -0,0 +1,966 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/data/models/film.dart';
import 'package:lightmeter/screens/metering/screen_metering.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
void main() {
const defaultEquipmentProfile = EquipmentProfile(
id: "",
name: 'Default',
apertureValues: ApertureValue.values,
ndValues: NdValue.values,
shutterSpeedValues: ShutterSpeedValue.values,
isoValues: IsoValue.values,
);
group('Empty list', () {
List<ExposurePair> exposurePairsFull(double ev) => MeteringContainerBuidler.buildExposureValues(
ev,
StopType.full,
defaultEquipmentProfile,
const Film.other(),
);
test('isNan', () {
expect(exposurePairsFull(double.nan), const []);
});
test('isInifinity', () {
expect(exposurePairsFull(double.infinity), const []);
});
test('Big ass number', () {
expect(exposurePairsFull(23), const []);
});
});
group('Default equipment profile', () {
group("StopType.full", () {
List<ExposurePair> exposurePairsFull(double ev) =>
MeteringContainerBuidler.buildExposureValues(
ev,
StopType.full,
defaultEquipmentProfile,
const Film.other(),
);
test('EV 1', () {
final exposurePairs = exposurePairsFull(1);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(1, StopType.full),
ShutterSpeedValue(2, true, StopType.full),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(5.6, StopType.full),
ShutterSpeedValue(16, false, StopType.full),
),
);
});
test('EV 1.3', () {
final exposurePairs = exposurePairsFull(1.3);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(1, StopType.full),
ShutterSpeedValue(2, true, StopType.full),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(5.6, StopType.full),
ShutterSpeedValue(16, false, StopType.full),
),
);
});
test('EV 1.5', () {
final exposurePairs = exposurePairsFull(1.5);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(1, StopType.full),
ShutterSpeedValue(4, true, StopType.full),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(8, StopType.full),
ShutterSpeedValue(16, false, StopType.full),
),
);
});
test('EV 1.7', () {
final exposurePairs = exposurePairsFull(1.7);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(1, StopType.full),
ShutterSpeedValue(4, true, StopType.full),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(8, StopType.full),
ShutterSpeedValue(16, false, StopType.full),
),
);
});
test('EV 2', () {
final exposurePairs = exposurePairsFull(2);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(1, StopType.full),
ShutterSpeedValue(4, true, StopType.full),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(8, StopType.full),
ShutterSpeedValue(16, false, StopType.full),
),
);
});
});
group("StopType.half", () {
List<ExposurePair> exposurePairsFull(double ev) =>
MeteringContainerBuidler.buildExposureValues(
ev,
StopType.half,
defaultEquipmentProfile,
const Film.other(),
);
test('EV 1', () {
final exposurePairs = exposurePairsFull(1);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(1, StopType.full),
ShutterSpeedValue(2, true, StopType.full),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(5.6, StopType.full),
ShutterSpeedValue(16, false, StopType.full),
),
);
});
test('EV 1.3', () {
final exposurePairs = exposurePairsFull(1.3);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(1, StopType.full),
ShutterSpeedValue(3, true, StopType.half),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(6.7, StopType.full),
ShutterSpeedValue(16, false, StopType.full),
),
);
});
test('EV 1.5', () {
final exposurePairs = exposurePairsFull(1.5);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(1, StopType.full),
ShutterSpeedValue(3, true, StopType.half),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(6.7, StopType.full),
ShutterSpeedValue(16, false, StopType.full),
),
);
});
test('EV 1.7', () {
final exposurePairs = exposurePairsFull(1.7);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(1, StopType.full),
ShutterSpeedValue(3, true, StopType.half),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(6.7, StopType.full),
ShutterSpeedValue(16, false, StopType.full),
),
);
});
test('EV 2', () {
final exposurePairs = exposurePairsFull(2);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(1, StopType.full),
ShutterSpeedValue(4, true, StopType.full),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(8, StopType.full),
ShutterSpeedValue(16, false, StopType.full),
),
);
});
});
group("StopType.third", () {
List<ExposurePair> exposurePairsFull(double ev) =>
MeteringContainerBuidler.buildExposureValues(
ev,
StopType.third,
defaultEquipmentProfile,
const Film.other(),
);
test('EV 1', () {
final exposurePairs = exposurePairsFull(1);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(1, StopType.full),
ShutterSpeedValue(2, true, StopType.full),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(5.6, StopType.full),
ShutterSpeedValue(16, false, StopType.full),
),
);
});
test('EV 1.3', () {
final exposurePairs = exposurePairsFull(1.3);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(1, StopType.full),
ShutterSpeedValue(2.5, true, StopType.third),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(6.3, StopType.third),
ShutterSpeedValue(16, false, StopType.full),
),
);
});
test('EV 1.5', () {
final exposurePairs = exposurePairsFull(1.5);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(1, StopType.full),
ShutterSpeedValue(3, true, StopType.third),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(7.1, StopType.third),
ShutterSpeedValue(16, false, StopType.full),
),
);
});
test('EV 1.7', () {
final exposurePairs = exposurePairsFull(1.7);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(1, StopType.full),
ShutterSpeedValue(3, true, StopType.third),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(7.1, StopType.third),
ShutterSpeedValue(16, false, StopType.full),
),
);
});
test('EV 2', () {
final exposurePairs = exposurePairsFull(2);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(1, StopType.full),
ShutterSpeedValue(4, true, StopType.full),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(8, StopType.full),
ShutterSpeedValue(16, false, StopType.full),
),
);
});
});
});
group('Shutter speed 1/1000-1/2"', () {
final equipmentProfile = EquipmentProfile(
id: "1",
name: 'Test1',
apertureValues: ApertureValue.values,
ndValues: NdValue.values,
shutterSpeedValues: ShutterSpeedValue.values.sublist(
ShutterSpeedValue.values.indexOf(const ShutterSpeedValue(1000, true, StopType.full)),
ShutterSpeedValue.values.indexOf(const ShutterSpeedValue(2, true, StopType.full)) + 1,
),
isoValues: IsoValue.values,
);
group("StopType.full", () {
List<ExposurePair> exposurePairsFull(double ev) =>
MeteringContainerBuidler.buildExposureValues(
ev,
StopType.full,
equipmentProfile,
const Film.other(),
);
test('EV 1', () {
final exposurePairs = exposurePairsFull(1);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(1.0, StopType.full),
ShutterSpeedValue(2, true, StopType.full),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(1.0, StopType.full),
ShutterSpeedValue(2, true, StopType.full),
),
);
});
test('EV 1.3', () {
final exposurePairs = exposurePairsFull(1.3);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(1.0, StopType.full),
ShutterSpeedValue(2, true, StopType.full),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(1.0, StopType.full),
ShutterSpeedValue(2, true, StopType.full),
),
);
});
test('EV 1.5', () {
final exposurePairs = exposurePairsFull(1.5);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(1.0, StopType.full),
ShutterSpeedValue(4, true, StopType.full),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(1.4, StopType.full),
ShutterSpeedValue(2, true, StopType.full),
),
);
});
test('EV 1.7', () {
final exposurePairs = exposurePairsFull(1.7);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(1.0, StopType.full),
ShutterSpeedValue(4, true, StopType.full),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(1.4, StopType.full),
ShutterSpeedValue(2, true, StopType.full),
),
);
});
test('EV 2', () {
final exposurePairs = exposurePairsFull(2);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(1.0, StopType.full),
ShutterSpeedValue(4, true, StopType.full),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(1.4, StopType.full),
ShutterSpeedValue(2, true, StopType.full),
),
);
});
});
group("StopType.half", () {
List<ExposurePair> exposurePairsFull(double ev) =>
MeteringContainerBuidler.buildExposureValues(
ev,
StopType.half,
equipmentProfile,
const Film.other(),
);
test('EV 1', () {
final exposurePairs = exposurePairsFull(1);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(1.0, StopType.full),
ShutterSpeedValue(2, true, StopType.full),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(1.0, StopType.full),
ShutterSpeedValue(2, true, StopType.full),
),
);
});
test('EV 1.3', () {
final exposurePairs = exposurePairsFull(1.3);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(1.0, StopType.full),
ShutterSpeedValue(3, true, StopType.half),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(1.2, StopType.half),
ShutterSpeedValue(2, true, StopType.full),
),
);
});
test('EV 1.5', () {
final exposurePairs = exposurePairsFull(1.5);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(1.0, StopType.full),
ShutterSpeedValue(3, true, StopType.half),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(1.2, StopType.half),
ShutterSpeedValue(2, true, StopType.full),
),
);
});
test('EV 1.7', () {
final exposurePairs = exposurePairsFull(1.7);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(1.0, StopType.full),
ShutterSpeedValue(3, true, StopType.half),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(1.2, StopType.half),
ShutterSpeedValue(2, true, StopType.full),
),
);
});
test('EV 2', () {
final exposurePairs = exposurePairsFull(2);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(1.0, StopType.full),
ShutterSpeedValue(4, true, StopType.full),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(1.4, StopType.full),
ShutterSpeedValue(2, true, StopType.full),
),
);
});
});
group("StopType.third", () {
List<ExposurePair> exposurePairsFull(double ev) =>
MeteringContainerBuidler.buildExposureValues(
ev,
StopType.third,
equipmentProfile,
const Film.other(),
);
test('EV 1', () {
final exposurePairs = exposurePairsFull(1);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(1.0, StopType.full),
ShutterSpeedValue(2, true, StopType.full),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(1.0, StopType.full),
ShutterSpeedValue(2, true, StopType.full),
),
);
});
test('EV 1.3', () {
final exposurePairs = exposurePairsFull(1.3);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(1.0, StopType.full),
ShutterSpeedValue(2.5, true, StopType.third),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(1.1, StopType.third),
ShutterSpeedValue(2, true, StopType.full),
),
);
});
test('EV 1.5', () {
final exposurePairs = exposurePairsFull(1.5);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(1.0, StopType.full),
ShutterSpeedValue(3, true, StopType.third),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(1.2, StopType.third),
ShutterSpeedValue(2, true, StopType.full),
),
);
});
test('EV 1.7', () {
final exposurePairs = exposurePairsFull(1.7);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(1.0, StopType.full),
ShutterSpeedValue(3, true, StopType.third),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(1.2, StopType.third),
ShutterSpeedValue(2, true, StopType.full),
),
);
});
test('EV 2', () {
final exposurePairs = exposurePairsFull(2);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(1.0, StopType.full),
ShutterSpeedValue(4, true, StopType.full),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(1.4, StopType.full),
ShutterSpeedValue(2, true, StopType.full),
),
);
});
});
});
group('Shutter speed 2"-16"', () {
final equipmentProfile = EquipmentProfile(
id: "1",
name: 'Test1',
apertureValues: ApertureValue.values.sublist(4),
ndValues: NdValue.values,
shutterSpeedValues: ShutterSpeedValue.values.sublist(
ShutterSpeedValue.values.indexOf(const ShutterSpeedValue(2, false, StopType.full)),
),
isoValues: IsoValue.values,
);
group("StopType.full", () {
List<ExposurePair> exposurePairsFull(double ev) =>
MeteringContainerBuidler.buildExposureValues(
ev,
StopType.full,
equipmentProfile,
const Film.other(),
);
test('EV 1', () {
final exposurePairs = exposurePairsFull(1);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(2.0, StopType.full),
ShutterSpeedValue(2, false, StopType.full),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(5.6, StopType.full),
ShutterSpeedValue(16, false, StopType.full),
),
);
});
test('EV 1.3', () {
final exposurePairs = exposurePairsFull(1.3);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(2.0, StopType.full),
ShutterSpeedValue(2, false, StopType.full),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(5.6, StopType.full),
ShutterSpeedValue(16, false, StopType.full),
),
);
});
test('EV 1.5', () {
final exposurePairs = exposurePairsFull(1.5);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(2.8, StopType.full),
ShutterSpeedValue(2, false, StopType.full),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(8, StopType.full),
ShutterSpeedValue(16, false, StopType.full),
),
);
});
test('EV 1.7', () {
final exposurePairs = exposurePairsFull(1.7);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(2.8, StopType.full),
ShutterSpeedValue(2, false, StopType.full),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(8, StopType.full),
ShutterSpeedValue(16, false, StopType.full),
),
);
});
test('EV 2', () {
final exposurePairs = exposurePairsFull(2);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(2.8, StopType.full),
ShutterSpeedValue(2, false, StopType.full),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(8, StopType.full),
ShutterSpeedValue(16, false, StopType.full),
),
);
});
});
group("StopType.half", () {
List<ExposurePair> exposurePairsFull(double ev) =>
MeteringContainerBuidler.buildExposureValues(
ev,
StopType.half,
equipmentProfile,
const Film.other(),
);
test('EV 1', () {
final exposurePairs = exposurePairsFull(1);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(2.0, StopType.full),
ShutterSpeedValue(2, false, StopType.full),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(5.6, StopType.full),
ShutterSpeedValue(16, false, StopType.full),
),
);
});
test('EV 1.3', () {
final exposurePairs = exposurePairsFull(1.3);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(2.4, StopType.half),
ShutterSpeedValue(2, false, StopType.full),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(6.7, StopType.full),
ShutterSpeedValue(16, false, StopType.full),
),
);
});
test('EV 1.5', () {
final exposurePairs = exposurePairsFull(1.5);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(2.4, StopType.half),
ShutterSpeedValue(2, false, StopType.full),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(6.7, StopType.full),
ShutterSpeedValue(16, false, StopType.full),
),
);
});
test('EV 1.7', () {
final exposurePairs = exposurePairsFull(1.7);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(2.4, StopType.half),
ShutterSpeedValue(2, false, StopType.full),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(6.7, StopType.full),
ShutterSpeedValue(16, false, StopType.full),
),
);
});
test('EV 2', () {
final exposurePairs = exposurePairsFull(2);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(2.8, StopType.full),
ShutterSpeedValue(2, false, StopType.full),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(8, StopType.full),
ShutterSpeedValue(16, false, StopType.full),
),
);
});
});
group("StopType.third", () {
List<ExposurePair> exposurePairsFull(double ev) =>
MeteringContainerBuidler.buildExposureValues(
ev,
StopType.third,
equipmentProfile,
const Film.other(),
);
test('EV 1', () {
final exposurePairs = exposurePairsFull(1);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(2.0, StopType.full),
ShutterSpeedValue(2, false, StopType.full),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(5.6, StopType.full),
ShutterSpeedValue(16, false, StopType.full),
),
);
});
test('EV 1.3', () {
final exposurePairs = exposurePairsFull(1.3);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(2.2, StopType.full),
ShutterSpeedValue(2, false, StopType.full),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(6.3, StopType.full),
ShutterSpeedValue(16, false, StopType.full),
),
);
});
test('EV 1.5', () {
final exposurePairs = exposurePairsFull(1.5);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(2.4, StopType.full),
ShutterSpeedValue(2, false, StopType.full),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(7.1, StopType.full),
ShutterSpeedValue(16, false, StopType.full),
),
);
});
test('EV 1.7', () {
final exposurePairs = exposurePairsFull(1.7);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(2.4, StopType.full),
ShutterSpeedValue(2, false, StopType.third),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(7.1, StopType.third),
ShutterSpeedValue(16, false, StopType.full),
),
);
});
test('EV 2', () {
final exposurePairs = exposurePairsFull(2);
expect(
exposurePairs.first,
const ExposurePair(
ApertureValue(2.8, StopType.full),
ShutterSpeedValue(2, false, StopType.full),
),
);
expect(
exposurePairs.last,
const ExposurePair(
ApertureValue(8, StopType.full),
ShutterSpeedValue(16, false, StopType.full),
),
);
});
});
});
}