ML-107 Films filter (#118)

* added stub `FilmsProvider`

* moved dialogs to the shared folder

* typo

* separated `EquipmentSettingsSection`

* copy

* `IAPBuilder` -> `IAPListTile`

* moved `Film` to resources repo

* fixed films selection

* untied iso and selected film

* removed film from exposure pairs building

* indicate push/pull

* copy

* Update .gitignore

* fixed extreme exposure pairs reciprocity display

* sync with iap changes

* sync iap stub with iap changes

* added reciprocity description

* added workspace file

* Update .gitignore
This commit is contained in:
Vadim 2023-09-14 16:59:16 +02:00 committed by GitHub
parent 1be7c3be48
commit cc9f162933
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 592 additions and 978 deletions

View file

@ -15,7 +15,7 @@ jobs:
analyze_and_test:
name: Analyze & test
runs-on: macos-11
timeout-minutes: 10
timeout-minutes: 5
steps:
- uses: actions/checkout@v3
with:
@ -47,3 +47,10 @@ jobs:
- name: Run tests
run: flutter test
- name: Analyze project source with stub
if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
run: |
bash ./.github/scripts/stub_iap.sh
flutter pub get
flutter analyze lib --fatal-infos

View file

@ -33,11 +33,7 @@ Without further delay behold my new Lightmeter app inspired by Material You (a.k
To build this app you need to install Flutter 3.10.0 stable. [How to install](https://docs.flutter.dev/get-started/install).
### 2. (Optional) Install Firebase
Out of the box Firebase Crashlytics won't work. If you want to add Crashlytics to your local build please follow [this guide](https://firebase.google.com/docs/flutter/setup).
### 3. Get packages
### 3. Project setup
As part of the app's functionallity is in the private repo, you have to replace these lines in _pubspec.yaml_:
@ -47,24 +43,39 @@ m3_lightmeter_iap:
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.
You can do it simply by running the script:
```console
sh .github/scripts/stub_iap.sh
```
> If you are using VSCode, you can open the workspace like so: _File -> Open Workspace from File -> m3_lightmeter.code-workspace_. Otherwise you have to run `flutter pub get` command 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
```
### 4. Build
### 4. (Optional) Install Firebase
Out of the box Firebase Crashlytics won't work. If you want to add Crashlytics to your local build please follow [this guide](https://firebase.google.com/docs/flutter/setup).
### 5. Build
#### Android
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
```

View file

@ -2,11 +2,13 @@ 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/films_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/equipment_profile_provider.dart';
export 'src/providers/films_provider.dart';
export 'src/providers/iap_products_provider.dart';
class IAPProviders extends StatelessWidget {
@ -22,9 +24,11 @@ class IAPProviders extends StatelessWidget {
@override
Widget build(BuildContext context) {
return IAPProductsProvider(
child: FilmsProvider(
child: EquipmentProfileProvider(
child: child,
),
),
);
}
}

View file

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:m3_lightmeter_iap/src/providers/selectable_provider.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class EquipmentProfileProvider extends StatefulWidget {
@ -27,7 +28,7 @@ class EquipmentProfileProviderState extends State<EquipmentProfileProvider> {
@override
Widget build(BuildContext context) {
return EquipmentProfiles(
profiles: const [_defaultProfile],
values: const [_defaultProfile],
selected: _defaultProfile,
child: widget.child,
);
@ -42,38 +43,19 @@ class EquipmentProfileProviderState extends State<EquipmentProfileProvider> {
void deleteProfile(EquipmentProfile data) {}
}
enum EquipmentProfilesAspect { list, selected }
class EquipmentProfiles extends InheritedModel<EquipmentProfilesAspect> {
class EquipmentProfiles extends SelectableInheritedModel<EquipmentProfile> {
const EquipmentProfiles({
super.key,
required this.profiles,
required this.selected,
required super.values,
required super.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;
return InheritedModel.inheritFrom<EquipmentProfiles>(context, aspect: SelectableAspect.list)!.values;
}
static EquipmentProfile selectedOf(BuildContext context) {
return InheritedModel.inheritFrom<EquipmentProfiles>(
context,
aspect: EquipmentProfilesAspect.selected,
)!
.selected;
return InheritedModel.inheritFrom<EquipmentProfiles>(context, aspect: SelectableAspect.selected)!.selected;
}
@override
bool updateShouldNotify(EquipmentProfiles oldWidget) => false;
@override
bool updateShouldNotifyDependent(EquipmentProfiles oldWidget, Set<EquipmentProfilesAspect> dependencies) => false;
}

View file

@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
import 'package:m3_lightmeter_iap/src/providers/selectable_provider.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class FilmsProvider extends StatefulWidget {
final Widget child;
const FilmsProvider({
required this.child,
super.key,
});
static FilmsProviderState of(BuildContext context) {
return context.findAncestorStateOfType<FilmsProviderState>()!;
}
@override
State<FilmsProvider> createState() => FilmsProviderState();
}
class FilmsProviderState extends State<FilmsProvider> {
@override
Widget build(BuildContext context) {
return Films(
values: const [Film.other()],
filmsInUse: const [Film.other()],
selected: const Film.other(),
child: widget.child,
);
}
void setFilm(Film film) {}
void saveFilms(List<Film> films) {}
}
class Films extends SelectableInheritedModel<Film> {
final List<Film> filmsInUse;
const Films({
super.key,
required super.values,
required this.filmsInUse,
required super.selected,
required super.child,
});
/// [Film.other()] + all the custom fields with actual reciprocity formulas
static List<Film> of(BuildContext context) {
return InheritedModel.inheritFrom<Films>(context)!.values;
}
/// [Film.other()] + films in use selected by user
static List<Film> inUseOf<T>(BuildContext context) {
return InheritedModel.inheritFrom<Films>(
context,
aspect: SelectableAspect.list,
)!
.filmsInUse;
}
static Film selectedOf(BuildContext context) {
return InheritedModel.inheritFrom<Films>(context, aspect: SelectableAspect.selected)!.selected;
}
}

View file

@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
enum SelectableAspect { list, selected }
class SelectableInheritedModel<T> extends InheritedModel<SelectableAspect> {
const SelectableInheritedModel({
super.key,
required this.values,
required this.selected,
required super.child,
});
final List<T> values;
final T selected;
@override
bool updateShouldNotify(SelectableInheritedModel oldWidget) => true;
@override
bool updateShouldNotifyDependent(SelectableInheritedModel oldWidget, Set<SelectableAspect> dependencies) {
if (dependencies.contains(SelectableAspect.list)) {
return true;
} else if (dependencies.contains(SelectableAspect.selected)) {
return selected != oldWidget.selected;
} else {
return false;
}
}
}

View file

@ -1,235 +0,0 @@
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
double log10(double x) => log(x) / log(10);
double log10polynomian(
double x,
double a,
double b,
double c,
) =>
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
///
/// Reciprocity formulas for Fomapan films and Kodak films are from here:
/// https://www.flickr.com/groups/86738082@N00/discuss/72157626050157470/
///
/// Cinema films like Kodak 5222/7222 Double-X and respective CineStill films (cause they are basically a modification of Kodak)
/// do not have any reciprocity failure information, as these films are ment to be used in cinema
/// with appropriate light and pretty short shutter speeds.
///
/// Because of this: https://github.com/dart-lang/sdk/issues/38934#issuecomment-803938315
/// `super` calls are ignored in test coverage
class Film {
final String name;
final int iso;
const Film(this.name, this.iso);
const Film.other()
: name = '',
iso = 0;
@override
String toString() => name;
ShutterSpeedValue reciprocityFailure(ShutterSpeedValue shutterSpeed) {
if (shutterSpeed.isFraction) {
return shutterSpeed;
} else {
return ShutterSpeedValue(
reciprocityFormula(shutterSpeed.rawValue),
shutterSpeed.isFraction,
shutterSpeed.stopType,
);
}
}
@protected
double reciprocityFormula(double t) => t;
static const List<Film> values = [
Film.other(),
FomapanFilm.creative100(),
FomapanFilm.creative200(),
FomapanFilm.action400(),
IlfordFilm.ortho(),
IlfordFilm.delta100(),
IlfordFilm.delta400(),
IlfordFilm.delta3200(),
IlfordFilm.fp4(),
IlfordFilm.hp5(),
IlfordFilm.panf(),
IlfordFilm.sfx200(),
IlfordFilm.xp2super(),
IlfordFilm.pan100(),
IlfordFilm.pan400(),
KodakFilm.tmax100(),
KodakFilm.tmax400(),
KodakFilm.tmax3200(),
KodakFilm.trix320(),
KodakFilm.trix400(),
];
}
/// https://www.tate.org.uk/documents/598/page_6_7_agfa_stocks_0.pdf
/// https://www.filmwasters.com/forum/index.php?topic=5298.0
// {{1,1.87},{2,3.73},{3,8.06},{4,13.93},{5,21.28},{6,23.00},{7,30.12},{8,38.05},{9,44.75},{10,50.12},{20,117},{30,202},{40,293},{50,413},{60,547},{70,694},{80,853},{90,1022},{100,1202}};
// class AgfaFilm extends Film {
// final double a;
// final double b;
// final double c;
// const AgfaFilm.apx100()
// : a = 1,
// b = 5,
// c = 2,
// super('Agfa APX 100', 100); // coverage:ignore-line
// const AgfaFilm.apx400()
// : a = 1.5,
// b = 4.5,
// c = 3,
// super('Agfa APX 400', 400); // coverage:ignore-line
// @override
// double reciprocityFormula(double t) => t * log10polynomian(t, a, b, c);
// }
class FomapanFilm extends Film {
final double a;
final double b;
final double c;
/// https://www.foma.cz/en/fomapan-100
const FomapanFilm.creative100()
: a = 1,
b = 5,
c = 2,
super('Fomapan CREATIVE 100', 100); // coverage:ignore-line
/// https://www.foma.cz/en/fomapan-200
const FomapanFilm.creative200()
: a = 1.5,
b = 4.5,
c = 3,
super('Fomapan CREATIVE 200', 200); // coverage:ignore-line
/// https://www.foma.cz/en/fomapan-100
const FomapanFilm.action400()
: a = -1.25, // coverage:ignore-line
b = 5.75,
c = 1.5,
super('Fomapan ACTION 400', 400); // coverage:ignore-line
@override
double reciprocityFormula(double t) => t * log10polynomian(t, a, b, c);
}
class IlfordFilm extends Film {
final double reciprocityPower;
/// https://www.ilfordphoto.com/amfile/file/download/file/1948/product/1650/
const IlfordFilm.ortho()
: reciprocityPower = 1.25,
super('Ilford ORTHO+', 80); // coverage:ignore-line
/// https://www.ilfordphoto.com/amfile/file/download/file/1919/product/686/
const IlfordFilm.fp4()
: reciprocityPower = 1.26,
super('Ilford FP4+', 125); // coverage:ignore-line
/// https://www.ilfordphoto.com/amfile/file/download/file/1903/product/691/
const IlfordFilm.hp5()
: reciprocityPower = 1.31,
super('Ilford HP5+', 400); // coverage:ignore-line
/// https://www.ilfordphoto.com/amfile/file/download/file/3/product/679/
const IlfordFilm.delta100()
: reciprocityPower = 1.26,
super('Ilford DELTA 100', 100); // coverage:ignore-line
/// https://www.ilfordphoto.com/amfile/file/download/file/1915/product/684/
const IlfordFilm.delta400()
: reciprocityPower = 1.41,
super('Ilford DELTA 400', 400); // coverage:ignore-line
/// https://www.ilfordphoto.com/amfile/file/download/file/1913/product/682/
const IlfordFilm.delta3200()
: reciprocityPower = 1.33,
super('Ilford DELTA 3200', 3200); // coverage:ignore-line
/// https://www.ilfordphoto.com/amfile/file/download/file/1905/product/699/
const IlfordFilm.panf()
: reciprocityPower = 1.33,
super('Ilford Pan F+', 50); // coverage:ignore-line
/// https://www.ilfordphoto.com/amfile/file/download/file/1907/product/701/
const IlfordFilm.sfx200()
: reciprocityPower = 1.31,
super('Ilford SFX 200', 200); // coverage:ignore-line
/// https://www.ilfordphoto.com/amfile/file/download/file/1909/product/703/
const IlfordFilm.xp2super()
: reciprocityPower = 1.31,
super('Ilford XP2 SUPER', 400); // coverage:ignore-line
/// https://www.ilfordphoto.com/amfile/file/download/file/1958/product/696/
const IlfordFilm.pan100()
: reciprocityPower = 1.26,
super('Kentemere 100', 100); // coverage:ignore-line
/// https://www.ilfordphoto.com/amfile/file/download/file/1959/product/697/
const IlfordFilm.pan400()
: reciprocityPower = 1.30,
super('Kentemere 400', 400); // coverage:ignore-line
@override
double reciprocityFormula(double t) => pow(t, reciprocityPower).toDouble();
}
class KodakFilm extends Film {
final double a;
final double b;
final double c;
const KodakFilm.tmax100()
: a = 1 / 6, // coverage:ignore-line
b = 0, // coverage:ignore-line
c = 4 / 3, // coverage:ignore-line
super('Kodak T-MAX 100', 100); // coverage:ignore-line
const KodakFilm.tmax400()
: a = 2 / 3, // coverage:ignore-line
b = -1 / 2, // coverage:ignore-line
c = 4 / 3, // coverage:ignore-line
super('Kodak T-MAX 400', 400); // coverage:ignore-line
const KodakFilm.tmax3200()
: a = 7 / 6, // coverage:ignore-line
b = -1, // coverage:ignore-line
c = 4 / 3, // coverage:ignore-line
super('Kodak T-MAX 3200', 3200); // coverage:ignore-line
const KodakFilm.trix320()
: a = 2,
b = 1,
c = 2,
super('Kodak TRI-X 320', 320); // coverage:ignore-line
const KodakFilm.trix400()
: a = 2,
b = 1,
c = 2,
super('Kodak TRI-X 400', 400); // coverage:ignore-line
@override
double reciprocityFormula(double t) => t * log10polynomian(t, a, b, c);
}

View file

@ -2,7 +2,6 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/ev_source_type.dart';
import 'package:lightmeter/data/models/film.dart';
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
import 'package:lightmeter/data/models/supported_locale.dart';
import 'package:lightmeter/data/models/theme_type.dart';
@ -19,7 +18,6 @@ class UserPreferencesService {
static const cameraEvCalibrationKey = "cameraEvCalibration";
static const lightSensorEvCalibrationKey = "lightSensorEvCalibration";
static const meteringScreenLayoutKey = "meteringScreenLayout";
static const filmKey = "film";
static const caffeineKey = "caffeine";
static const hapticsKey = "haptics";
@ -142,10 +140,4 @@ class UserPreferencesService {
bool get dynamicColor => _sharedPreferences.getBool(dynamicColorKey) ?? false;
set dynamicColor(bool value) => _sharedPreferences.setBool(dynamicColorKey, value);
Film get film => Film.values.firstWhere(
(e) => e.name == _sharedPreferences.getString(filmKey),
orElse: () => Film.values.first,
);
set film(Film value) => _sharedPreferences.setString(filmKey, value.name);
}

View file

@ -2,7 +2,6 @@ import 'package:app_settings/app_settings.dart';
import 'package:lightmeter/data/caffeine_service.dart';
import 'package:lightmeter/data/haptics_service.dart';
import 'package:lightmeter/data/light_sensor_service.dart';
import 'package:lightmeter/data/models/film.dart';
import 'package:lightmeter/data/models/volume_action.dart';
import 'package:lightmeter/data/permissions_service.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
@ -45,9 +44,6 @@ class MeteringInteractor {
NdValue get ndFilter => _userPreferencesService.ndFilter;
set ndFilter(NdValue value) => _userPreferencesService.ndFilter = value;
Film get film => _userPreferencesService.film;
set film(Film value) => _userPreferencesService.film = value;
VolumeAction get volumeAction => _userPreferencesService.volumeAction;
/// Executes vibration if haptics are enabled in settings

View file

@ -41,6 +41,9 @@
"meteringScreenFeatureFilmPicker": "Film picker",
"meteringScreenFeatureHistogram": "Histogram",
"film": "Film",
"filmPush": "Film (push)",
"filmPull": "Film (pull)",
"filmReciprocityHint": "Applies correction for shutter speeds grater than 1 second",
"equipment": "Equipment",
"equipmentProfileName": "Equipment profile name",
"equipmentProfileNameHint": "Praktica MTL5B",
@ -56,6 +59,8 @@
"equipmentProfile": "Equipment profile",
"equipmentProfiles": "Equipment profiles",
"tapToAdd": "Tap to add",
"filmsInUse": "Films in use",
"filmsInUseDescription": "Select films which you use.",
"general": "General",
"keepScreenOn": "Keep screen on",
"haptics": "Haptics",

View file

@ -41,9 +41,11 @@
"meteringScreenFeatureFilmPicker": "Sélecteur de film",
"meteringScreenFeatureHistogram": "Histogramme",
"film": "Pellicule",
"filmPush": "Pellicule (push)",
"filmPull": "Pellicule (pull)",
"filmReciprocityHint": "La correction s'applique aux vitesses d'obturation supérieures à 1 seconde",
"equipment": "Équipement",
"equipmentProfileName": "Nom du profil de l'équipement",
"tapToAdd": "Appuie pour ajouter",
"equipmentProfileNameHint": "Praktica MTL5B",
"equipmentProfileAllValues": "Tout",
"apertureValues": "Valeurs Aperture",
@ -56,6 +58,9 @@
"isoValuesFilterDescription": "Sélectionnez les valeurs ISO à afficher. Ce sont peut-être vos valeurs les plus couramment utilisées ou celles prises en charge par votre caméra.",
"equipmentProfile": "Profil de l'équipement",
"equipmentProfiles": "Profils de l'équipement",
"tapToAdd": "Appuie pour ajouter",
"filmsInUse": "Films en usage",
"filmsInUseDescription": "Sélectionnez les films que vous utilisez.",
"general": "Général",
"keepScreenOn": "Garder l'écran allumé",
"haptics": "Haptiques",

View file

@ -41,6 +41,9 @@
"meteringScreenFeatureFilmPicker": "Выбор пленки",
"meteringScreenFeatureHistogram": "Гистограмма",
"film": "Пленка",
"filmPush": "Пленка (push)",
"filmPull": "Пленка (pull)",
"filmReciprocityHint": "Применяет коррекцию для выдержек длиннее 1 секунды",
"equipment": "Оборудование",
"equipmentProfileName": "Название профиля",
"equipmentProfileNameHint": "Praktica MTL5B",
@ -56,6 +59,8 @@
"equipmentProfile": "Оборудование",
"equipmentProfiles": "Профили оборудования",
"tapToAdd": "Нажмите, чтобы добавить",
"filmsInUse": "Используемые пленки",
"filmsInUseDescription": "Выберите пленки, которыми вы пользуетесь.",
"general": "Общие",
"keepScreenOn": "Запрет блокировки",
"haptics": "Вибрация",

View file

@ -31,7 +31,7 @@
"thirdStops": "1/3",
"calibration": "校准",
"calibrationMessage": "此应用测量读数的准确性完全取决于设备的硬件。因此,请考虑测试此应用并手动设置 EV 校准,以获得准确的测量结果。",
"calibrationMessageCameraOnly": "此应用程序测量读数的准确性完全取决于设备的后置摄像头。因此,请考虑测试此应用并手动设置 EV 校准,以获得准确的测量结果。",
"calibrationMessageCameraOnly": "此应用程序测量读数的准确s性完全取决于设备的后置摄像头。因此,请考虑测试此应用并手动设置 EV 校准,以获得准确的测量结果。",
"camera": "摄像头",
"lightSensor": "光传感器",
"meteringScreenLayout": "布局",
@ -41,6 +41,9 @@
"meteringScreenFeatureFilmPicker": "胶片选择",
"meteringScreenFeatureHistogram": "直方图",
"film": "胶片",
"filmPush": "胶片 (push)",
"filmPull": "胶片 (pull)",
"filmReciprocityHint": "Applies correction for shutter speeds grater than 1 second",
"equipment": "设备",
"equipmentProfileName": "设备配置名称",
"equipmentProfileNameHint": "Praktica MTL5B",
@ -56,6 +59,8 @@
"equipmentProfile": "设备配置",
"equipmentProfiles": "设备配置",
"tapToAdd": "點擊添加",
"filmsInUse": "Films in use",
"filmsInUseDescription": "Select films which you use.",
"general": "通用",
"keepScreenOn": "保持屏幕常亮",
"haptics": "震动",

View file

@ -3,7 +3,6 @@ import 'dart:async';
import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/data/models/film.dart';
import 'package:lightmeter/data/models/volume_action.dart';
import 'package:lightmeter/interactors/metering_interactor.dart';
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
@ -29,7 +28,6 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
) : super(
MeteringDataState(
ev100: null,
film: _meteringInteractor.film,
iso: _meteringInteractor.iso,
nd: _meteringInteractor.ndFilter,
isMetering: false,
@ -42,7 +40,6 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
.listen(onCommunicationState);
on<EquipmentProfileChangedEvent>(_onEquipmentProfileChanged);
on<FilmChangedEvent>(_onFilmChanged);
on<IsoChangedEvent>(_onIsoChanged);
on<NdChangedEvent>(_onNdChanged);
on<MeasureEvent>(_onMeasure, transformer: droppable());
@ -92,12 +89,9 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
/// Update selected ISO value and discard selected film, if selected equipment profile
/// doesn't contain currently selected value
IsoValue iso = state.iso;
Film film = state.film;
if (!event.equipmentProfileData.isoValues.any((v) => state.iso.value == v.value)) {
_meteringInteractor.iso = event.equipmentProfileData.isoValues.first;
iso = event.equipmentProfileData.isoValues.first;
_meteringInteractor.film = Film.values.first;
film = Film.values.first;
willUpdateMeasurements = true;
}
@ -113,7 +107,6 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
emit(
MeteringDataState(
ev100: state.ev100,
film: film,
iso: iso,
nd: nd,
isMetering: state.isMetering,
@ -122,46 +115,12 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
}
}
void _onFilmChanged(FilmChangedEvent event, Emitter emit) {
if (state.film.name != event.film.name) {
_meteringInteractor.film = event.film;
/// Find `IsoValue` with matching value
IsoValue iso = state.iso;
if (state.iso.value != event.film.iso && event.film != const Film.other()) {
iso = IsoValue.values.firstWhere(
(e) => e.value == event.film.iso,
orElse: () => state.iso,
);
_meteringInteractor.iso = iso;
}
/// If user selects 'Other' film we preserve currently selected ISO
/// and therefore only discard reciprocity formula
emit(
MeteringDataState(
ev100: state.ev100,
film: event.film,
iso: iso,
nd: state.nd,
isMetering: state.isMetering,
),
);
}
}
void _onIsoChanged(IsoChangedEvent event, Emitter emit) {
/// Discard currently selected film even if ISO is the same,
/// because, for example, Fomapan 400 and any Ilford 400
/// have different reciprocity formulas
_meteringInteractor.film = Film.values.first;
if (state.iso != event.isoValue) {
_meteringInteractor.iso = event.isoValue;
emit(
MeteringDataState(
ev100: state.ev100,
film: Film.values.first,
iso: event.isoValue,
nd: state.nd,
isMetering: state.isMetering,
@ -176,7 +135,6 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
emit(
MeteringDataState(
ev100: state.ev100,
film: state.film,
iso: state.iso,
nd: event.ndValue,
isMetering: state.isMetering,
@ -190,7 +148,6 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
_communicationBloc.add(const communication_events.MeasureEvent());
emit(
LoadingState(
film: state.film,
iso: state.iso,
nd: state.nd,
),
@ -209,7 +166,6 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
emit(
MeteringDataState(
ev100: event.ev100,
film: state.film,
iso: state.iso,
nd: state.nd,
isMetering: event.isMetering,
@ -221,7 +177,6 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
emit(
MeteringDataState(
ev100: null,
film: state.film,
iso: state.iso,
nd: state.nd,
isMetering: event.isMetering,

View file

@ -31,27 +31,7 @@ class _CameraPreviewState extends State<CameraPreview> {
AnimatedSwitcher(
duration: Dimens.switchDuration,
child: widget.controller != null
? ValueListenableBuilder<CameraValue>(
valueListenable: widget.controller!,
builder: (_, __, ___) => widget.controller!.value.isInitialized
? Stack(
alignment: Alignment.bottomCenter,
children: [
CameraView(controller: widget.controller!),
if (UserPreferencesProvider.meteringScreenFeatureOf(
context,
MeteringScreenLayoutFeature.histogram,
))
Positioned(
left: Dimens.grid8,
right: Dimens.grid8,
bottom: Dimens.grid16,
child: CameraHistogram(controller: widget.controller!),
),
],
)
: const SizedBox.shrink(),
)
? _CameraPreviewBuilder(controller: widget.controller!)
: CameraViewPlaceholder(error: widget.error),
),
],
@ -60,3 +40,59 @@ class _CameraPreviewState extends State<CameraPreview> {
);
}
}
class _CameraPreviewBuilder extends StatefulWidget {
final CameraController controller;
const _CameraPreviewBuilder({required this.controller});
@override
State<_CameraPreviewBuilder> createState() => _CameraPreviewBuilderState();
}
class _CameraPreviewBuilderState extends State<_CameraPreviewBuilder> {
late final ValueNotifier<bool> _initializedNotifier =
ValueNotifier<bool>(widget.controller.value.isInitialized);
@override
void initState() {
super.initState();
widget.controller.addListener(_update);
}
@override
void dispose() {
widget.controller.removeListener(_update);
_initializedNotifier.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<bool>(
valueListenable: _initializedNotifier,
builder: (context, value, child) => value
? Stack(
alignment: Alignment.bottomCenter,
children: [
CameraView(controller: widget.controller),
if (UserPreferencesProvider.meteringScreenFeatureOf(
context,
MeteringScreenLayoutFeature.histogram,
))
Positioned(
left: Dimens.grid8,
right: Dimens.grid8,
bottom: Dimens.grid16,
child: CameraHistogram(controller: widget.controller),
),
],
)
: const SizedBox.shrink(),
);
}
void _update() {
_initializedNotifier.value = widget.controller.value.isInitialized;
}
}

View file

@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
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/screens/metering/communication/bloc_communication_metering.dart';
import 'package:lightmeter/screens/metering/components/camera_container/bloc_container_camera.dart';
import 'package:lightmeter/screens/metering/components/camera_container/event_container_camera.dart';
@ -12,10 +11,8 @@ import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class CameraContainerProvider extends StatelessWidget {
final ExposurePair? fastest;
final ExposurePair? slowest;
final Film film;
final IsoValue iso;
final NdValue nd;
final ValueChanged<Film> onFilmChanged;
final ValueChanged<IsoValue> onIsoChanged;
final ValueChanged<NdValue> onNdChanged;
final List<ExposurePair> exposurePairs;
@ -23,10 +20,8 @@ class CameraContainerProvider extends StatelessWidget {
const CameraContainerProvider({
required this.fastest,
required this.slowest,
required this.film,
required this.iso,
required this.nd,
required this.onFilmChanged,
required this.onIsoChanged,
required this.onNdChanged,
required this.exposurePairs,
@ -44,10 +39,8 @@ class CameraContainerProvider extends StatelessWidget {
child: CameraContainer(
fastest: fastest,
slowest: slowest,
film: film,
iso: iso,
nd: nd,
onFilmChanged: onFilmChanged,
onIsoChanged: onIsoChanged,
onNdChanged: onNdChanged,
exposurePairs: exposurePairs,

View file

@ -3,7 +3,6 @@ import 'dart:math';
import 'package:flutter/material.dart';
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/platform_config.dart';
import 'package:lightmeter/providers/user_preferences_provider.dart';
@ -23,10 +22,8 @@ import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class CameraContainer extends StatelessWidget {
final ExposurePair? fastest;
final ExposurePair? slowest;
final Film film;
final IsoValue iso;
final NdValue nd;
final ValueChanged<Film> onFilmChanged;
final ValueChanged<IsoValue> onIsoChanged;
final ValueChanged<NdValue> onNdChanged;
final List<ExposurePair> exposurePairs;
@ -34,10 +31,8 @@ class CameraContainer extends StatelessWidget {
const CameraContainer({
required this.fastest,
required this.slowest,
required this.film,
required this.iso,
required this.nd,
required this.onFilmChanged,
required this.onIsoChanged,
required this.onNdChanged,
required this.exposurePairs,
@ -60,10 +55,8 @@ class CameraContainer extends StatelessWidget {
readingsContainer: ReadingsContainer(
fastest: fastest,
slowest: slowest,
film: film,
iso: iso,
nd: nd,
onFilmChanged: onFilmChanged,
onIsoChanged: onIsoChanged,
onNdChanged: onNdChanged,
),

View file

@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
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/screens/metering/communication/bloc_communication_metering.dart';
import 'package:lightmeter/screens/metering/components/light_sensor_container/bloc_container_light_sensor.dart';
import 'package:lightmeter/screens/metering/components/light_sensor_container/widget_container_light_sensor.dart';
@ -11,10 +10,8 @@ import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class LightSensorContainerProvider extends StatelessWidget {
final ExposurePair? fastest;
final ExposurePair? slowest;
final Film film;
final IsoValue iso;
final NdValue nd;
final ValueChanged<Film> onFilmChanged;
final ValueChanged<IsoValue> onIsoChanged;
final ValueChanged<NdValue> onNdChanged;
final List<ExposurePair> exposurePairs;
@ -22,10 +19,8 @@ class LightSensorContainerProvider extends StatelessWidget {
const LightSensorContainerProvider({
required this.fastest,
required this.slowest,
required this.film,
required this.iso,
required this.nd,
required this.onFilmChanged,
required this.onIsoChanged,
required this.onNdChanged,
required this.exposurePairs,
@ -43,10 +38,8 @@ class LightSensorContainerProvider extends StatelessWidget {
child: LightSensorContainer(
fastest: fastest,
slowest: slowest,
film: film,
iso: iso,
nd: nd,
onFilmChanged: onFilmChanged,
onIsoChanged: onIsoChanged,
onNdChanged: onNdChanged,
exposurePairs: exposurePairs,

View file

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/data/models/film.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart';
import 'package:lightmeter/screens/metering/components/shared/metering_top_bar/widget_top_bar_metering.dart';
@ -10,10 +9,8 @@ import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class LightSensorContainer extends StatelessWidget {
final ExposurePair? fastest;
final ExposurePair? slowest;
final Film film;
final IsoValue iso;
final NdValue nd;
final ValueChanged<Film> onFilmChanged;
final ValueChanged<IsoValue> onIsoChanged;
final ValueChanged<NdValue> onNdChanged;
final List<ExposurePair> exposurePairs;
@ -21,10 +18,8 @@ class LightSensorContainer extends StatelessWidget {
const LightSensorContainer({
required this.fastest,
required this.slowest,
required this.film,
required this.iso,
required this.nd,
required this.onFilmChanged,
required this.onIsoChanged,
required this.onNdChanged,
required this.exposurePairs,
@ -39,10 +34,8 @@ class LightSensorContainer extends StatelessWidget {
readingsContainer: ReadingsContainer(
fastest: fastest,
slowest: slowest,
film: film,
iso: iso,
nd: nd,
onFilmChanged: onFilmChanged,
onIsoChanged: onIsoChanged,
onNdChanged: onNdChanged,
),

View file

@ -5,6 +5,7 @@ import 'package:lightmeter/res/dimens.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';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
class ExposurePairsList extends StatelessWidget {
final List<ExposurePair> exposurePairs;
@ -47,7 +48,8 @@ class ExposurePairsList extends StatelessWidget {
child: Align(
alignment: Alignment.centerLeft,
child: ExposurePairsListItem(
exposurePairs[index].shutterSpeed,
Films.selectedOf(context)
.reciprocityFailure(exposurePairs[index].shutterSpeed),
tickOnTheLeft: true,
),
),

View file

@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/widget_picker_dialog_animated.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/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 EquipmentProfilePicker extends StatelessWidget {
const EquipmentProfilePicker();
@override
Widget build(BuildContext context) {
return AnimatedDialogPicker<EquipmentProfile>(
icon: Icons.camera,
title: S.of(context).equipmentProfile,
selectedValue: EquipmentProfiles.selectedOf(context),
values: EquipmentProfiles.of(context),
itemTitleBuilder: (_, value) => Text(value.id.isEmpty ? S.of(context).none : value.name),
onChanged: EquipmentProfileProvider.of(context).setProfile,
closedChild: ReadingValueContainer.singleValue(
value: ReadingValue(
label: S.of(context).equipmentProfile,
value: EquipmentProfiles.selectedOf(context).id.isEmpty
? S.of(context).none
: EquipmentProfiles.selectedOf(context).name,
),
),
);
}
}

View file

@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/reading_value_container/widget_container_reading_value.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
class ExtremeExposurePairsContainer extends StatelessWidget {
final ExposurePair? fastest;
final ExposurePair? slowest;
const ExtremeExposurePairsContainer({
required this.fastest,
required this.slowest,
super.key,
});
@override
Widget build(BuildContext context) {
return ReadingValueContainer(
values: [
ReadingValue(
label: S.of(context).fastestExposurePair,
value: _exposurePairToString(context, fastest),
),
ReadingValue(
label: S.of(context).slowestExposurePair,
value: _exposurePairToString(context, slowest),
),
],
);
}
String _exposurePairToString(BuildContext context, ExposurePair? pair) {
if (pair == null) {
return '-';
}
return '${pair.aperture} - ${Films.selectedOf(context).reciprocityFailure(pair.shutterSpeed)}';
}
}

View file

@ -0,0 +1,53 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/widget_picker_dialog_animated.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/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 FilmPicker extends StatelessWidget {
final IsoValue selectedIso;
const FilmPicker({required this.selectedIso});
@override
Widget build(BuildContext context) {
return AnimatedDialogPicker<Film>(
icon: Icons.camera_roll,
title: S.of(context).film,
subtitle: S.of(context).filmReciprocityHint,
selectedValue: Films.selectedOf(context),
values: Films.inUseOf(context),
itemTitleBuilder: (_, value) => Text(value.name.isEmpty ? S.of(context).none : value.name),
onChanged: FilmsProvider.of(context).setFilm,
closedChild: ReadingValueContainer.singleValue(
value: ReadingValue(
label: _label(context),
value: Films.selectedOf(context).name.isEmpty
? S.of(context).none
: Films.selectedOf(context).name,
),
),
);
}
String _label(BuildContext context) {
if (Films.selectedOf(context) == const Film.other() ||
Films.selectedOf(context).iso == selectedIso.value) {
return S.of(context).film;
}
final evDiff = IsoValue(
Films.selectedOf(context).iso,
StopType.full,
).difference(selectedIso);
if (evDiff > 0) {
return S.of(context).filmPush;
} else if (evDiff < 0) {
return S.of(context).filmPull;
} else {
return S.of(context).film;
}
}
}

View file

@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/widget_picker_dialog_animated.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/reading_value_container/widget_container_reading_value.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class IsoValuePicker extends StatelessWidget {
final List<IsoValue> values;
final IsoValue selectedValue;
final ValueChanged<IsoValue> onChanged;
const IsoValuePicker({
required this.selectedValue,
required this.values,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return AnimatedDialogPicker<IsoValue>(
icon: Icons.iso,
title: S.of(context).iso,
subtitle: S.of(context).filmSpeed,
selectedValue: selectedValue,
values: values,
itemTitleBuilder: (_, value) => Text(value.value.toString()),
// using ascending order, because increase in film speed rises EV
itemTrailingBuilder: (selected, value) => value.value != selected.value
? Text(S.of(context).evValue(selected.toStringDifference(value)))
: null,
onChanged: onChanged,
closedChild: ReadingValueContainer.singleValue(
value: ReadingValue(
label: S.of(context).iso,
value: selectedValue.value.toString(),
),
),
);
}
}

View file

@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/widget_picker_dialog_animated.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/reading_value_container/widget_container_reading_value.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class NdValuePicker extends StatelessWidget {
final List<NdValue> values;
final NdValue selectedValue;
final ValueChanged<NdValue> onChanged;
const NdValuePicker({
required this.selectedValue,
required this.values,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return AnimatedDialogPicker<NdValue>(
icon: Icons.filter_b_and_w,
title: S.of(context).nd,
subtitle: S.of(context).ndFilterFactor,
selectedValue: selectedValue,
values: values,
itemTitleBuilder: (_, value) => Text(
value.value == 0 ? S.of(context).none : value.value.toString(),
),
// using descending order, because ND filter darkens image & lowers EV
itemTrailingBuilder: (selected, value) => value.value != selected.value
? Text(S.of(context).evValue(value.toStringDifference(selected)))
: null,
onChanged: onChanged,
closedChild: ReadingValueContainer.singleValue(
value: ReadingValue(
label: S.of(context).nd,
value: selectedValue.value.toString(),
),
),
);
}
}

View file

@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/animated_dialog_picker/components/dialog_picker/widget_picker_dialog.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/dialog_picker/widget_picker_dialog.dart';
// Has to be stateful, so that [GlobalKey] is not recreated.
// Otherwise use will no be able to close the dialog after EV value has changed.

View file

@ -1,32 +1,29 @@
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/generated/l10n.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:lightmeter/screens/metering/components/shared/readings_container/components/equipment_profile_picker/widget_picker_equipment_profiles.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/extreme_exposure_pairs_container/widget_container_extreme_exposure_pairs.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/film_picker/widget_picker_film.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/iso_picker/widget_picker_iso.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/nd_picker/widget_picker_nd.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class ReadingsContainer extends StatelessWidget {
final ExposurePair? fastest;
final ExposurePair? slowest;
final Film film;
final IsoValue iso;
final NdValue nd;
final ValueChanged<Film> onFilmChanged;
final ValueChanged<IsoValue> onIsoChanged;
final ValueChanged<NdValue> onNdChanged;
const ReadingsContainer({
required this.fastest,
required this.slowest,
required this.film,
required this.iso,
required this.nd,
required this.onFilmChanged,
required this.onIsoChanged,
required this.onNdChanged,
super.key,
@ -41,24 +38,16 @@ class ReadingsContainer extends StatelessWidget {
context,
MeteringScreenLayoutFeature.equipmentProfiles,
)) ...[
const _EquipmentProfilePicker(),
const EquipmentProfilePicker(),
const _InnerPadding(),
],
if (UserPreferencesProvider.meteringScreenFeatureOf(
context,
MeteringScreenLayoutFeature.extremeExposurePairs,
)) ...[
ReadingValueContainer(
values: [
ReadingValue(
label: S.of(context).fastestExposurePair,
value: fastest != null ? fastest!.toString() : '-',
),
ReadingValue(
label: S.of(context).slowestExposurePair,
value: fastest != null ? slowest!.toString() : '-',
),
],
ExtremeExposurePairsContainer(
fastest: fastest,
slowest: slowest,
),
const _InnerPadding(),
],
@ -66,17 +55,13 @@ class ReadingsContainer extends StatelessWidget {
context,
MeteringScreenLayoutFeature.filmPicker,
)) ...[
_FilmPicker(
values: Film.values,
selectedValue: film,
onChanged: onFilmChanged,
),
FilmPicker(selectedIso: iso),
const _InnerPadding(),
],
Row(
children: [
Expanded(
child: _IsoValuePicker(
child: IsoValuePicker(
selectedValue: iso,
values: EquipmentProfiles.selectedOf(context).isoValues,
onChanged: onIsoChanged,
@ -84,7 +69,7 @@ class ReadingsContainer extends StatelessWidget {
),
const _InnerPadding(),
Expanded(
child: _NdValuePicker(
child: NdValuePicker(
selectedValue: nd,
values: EquipmentProfiles.selectedOf(context).ndValues,
onChanged: onNdChanged,
@ -100,129 +85,3 @@ class ReadingsContainer extends StatelessWidget {
class _InnerPadding extends SizedBox {
const _InnerPadding() : super(height: Dimens.grid8, width: Dimens.grid8);
}
class _EquipmentProfilePicker extends StatelessWidget {
const _EquipmentProfilePicker();
@override
Widget build(BuildContext context) {
return AnimatedDialogPicker<EquipmentProfile>(
icon: Icons.camera,
title: S.of(context).equipmentProfile,
selectedValue: EquipmentProfiles.selectedOf(context),
values: EquipmentProfiles.of(context),
itemTitleBuilder: (_, value) => Text(value.id.isEmpty ? S.of(context).none : value.name),
onChanged: EquipmentProfileProvider.of(context).setProfile,
closedChild: ReadingValueContainer.singleValue(
value: ReadingValue(
label: S.of(context).equipmentProfile,
value: EquipmentProfiles.selectedOf(context).id.isEmpty
? S.of(context).none
: EquipmentProfiles.selectedOf(context).name,
),
),
);
}
}
class _FilmPicker extends StatelessWidget {
final List<Film> values;
final Film selectedValue;
final ValueChanged<Film> onChanged;
const _FilmPicker({
required this.values,
required this.selectedValue,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return AnimatedDialogPicker<Film>(
icon: Icons.camera_roll,
title: S.of(context).film,
selectedValue: selectedValue,
values: values,
itemTitleBuilder: (_, value) => Text(value.name.isEmpty ? S.of(context).none : value.name),
onChanged: onChanged,
closedChild: ReadingValueContainer.singleValue(
value: ReadingValue(
label: S.of(context).film,
value: selectedValue.name.isEmpty ? S.of(context).none : selectedValue.name,
),
),
);
}
}
class _IsoValuePicker extends StatelessWidget {
final List<IsoValue> values;
final IsoValue selectedValue;
final ValueChanged<IsoValue> onChanged;
const _IsoValuePicker({
required this.selectedValue,
required this.values,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return AnimatedDialogPicker<IsoValue>(
icon: Icons.iso,
title: S.of(context).iso,
subtitle: S.of(context).filmSpeed,
selectedValue: selectedValue,
values: values,
itemTitleBuilder: (_, value) => Text(value.value.toString()),
// using ascending order, because increase in film speed rises EV
itemTrailingBuilder: (selected, value) => value.value != selected.value
? Text(S.of(context).evValue(selected.toStringDifference(value)))
: null,
onChanged: onChanged,
closedChild: ReadingValueContainer.singleValue(
value: ReadingValue(
label: S.of(context).iso,
value: selectedValue.value.toString(),
),
),
);
}
}
class _NdValuePicker extends StatelessWidget {
final List<NdValue> values;
final NdValue selectedValue;
final ValueChanged<NdValue> onChanged;
const _NdValuePicker({
required this.selectedValue,
required this.values,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return AnimatedDialogPicker<NdValue>(
icon: Icons.filter_b_and_w,
title: S.of(context).nd,
subtitle: S.of(context).ndFilterFactor,
selectedValue: selectedValue,
values: values,
itemTitleBuilder: (_, value) => Text(
value.value == 0 ? S.of(context).none : value.value.toString(),
),
// using descending order, because ND filter darkens image & lowers EV
itemTrailingBuilder: (selected, value) => value.value != selected.value
? Text(S.of(context).evValue(value.toStringDifference(selected)))
: null,
onChanged: onChanged,
closedChild: ReadingValueContainer.singleValue(
value: ReadingValue(
label: S.of(context).nd,
value: selectedValue.value.toString(),
),
),
);
}
}

View file

@ -1,4 +1,3 @@
import 'package:lightmeter/data/models/film.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
sealed class MeteringEvent {
@ -11,12 +10,6 @@ class EquipmentProfileChangedEvent extends MeteringEvent {
const EquipmentProfileChangedEvent(this.equipmentProfileData);
}
class FilmChangedEvent extends MeteringEvent {
final Film film;
const FilmChangedEvent(this.film);
}
class IsoChangedEvent extends MeteringEvent {
final IsoValue isoValue;

View file

@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
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/services_provider.dart';
import 'package:lightmeter/providers/user_preferences_provider.dart';
@ -33,11 +32,8 @@ class MeteringScreen extends StatelessWidget {
child: BlocBuilder<MeteringBloc, MeteringState>(
builder: (_, state) => MeteringContainerBuidler(
ev: state is MeteringDataState ? state.ev : null,
film: state.film,
iso: state.iso,
nd: state.nd,
onFilmChanged: (value) =>
context.read<MeteringBloc>().add(FilmChangedEvent(value)),
onIsoChanged: (value) => context.read<MeteringBloc>().add(IsoChangedEvent(value)),
onNdChanged: (value) => context.read<MeteringBloc>().add(NdChangedEvent(value)),
),
@ -81,7 +77,7 @@ class _InheritedListeners extends StatelessWidget {
feature: MeteringScreenLayoutFeature.filmPicker,
onDidChangeDependencies: (value) {
if (!value) {
context.read<MeteringBloc>().add(const FilmChangedEvent(Film.other()));
FilmsProvider.of(context).setFilm(const Film.other());
}
},
child: child,
@ -92,19 +88,15 @@ class _InheritedListeners extends StatelessWidget {
class MeteringContainerBuidler extends StatelessWidget {
final double? ev;
final Film film;
final IsoValue iso;
final NdValue nd;
final ValueChanged<Film> onFilmChanged;
final ValueChanged<IsoValue> onIsoChanged;
final ValueChanged<NdValue> onNdChanged;
const MeteringContainerBuidler({
required this.ev,
required this.film,
required this.iso,
required this.nd,
required this.onFilmChanged,
required this.onIsoChanged,
required this.onNdChanged,
});
@ -116,7 +108,6 @@ class MeteringContainerBuidler extends StatelessWidget {
ev!,
UserPreferencesProvider.stopTypeOf(context),
EquipmentProfiles.selectedOf(context),
film,
)
: <ExposurePair>[];
final fastest = exposurePairs.isNotEmpty ? exposurePairs.first : null;
@ -126,10 +117,8 @@ class MeteringContainerBuidler extends StatelessWidget {
? CameraContainerProvider(
fastest: fastest,
slowest: slowest,
film: film,
iso: iso,
nd: nd,
onFilmChanged: onFilmChanged,
onIsoChanged: onIsoChanged,
onNdChanged: onNdChanged,
exposurePairs: exposurePairs,
@ -137,10 +126,8 @@ class MeteringContainerBuidler extends StatelessWidget {
: LightSensorContainerProvider(
fastest: fastest,
slowest: slowest,
film: film,
iso: iso,
nd: nd,
onFilmChanged: onFilmChanged,
onIsoChanged: onIsoChanged,
onNdChanged: onNdChanged,
exposurePairs: exposurePairs,
@ -152,7 +139,6 @@ class MeteringContainerBuidler extends StatelessWidget {
double ev,
StopType stopType,
EquipmentProfile equipmentProfile,
Film film,
) {
if (ev.isNaN || ev.isInfinite) {
return List.empty();
@ -195,7 +181,7 @@ class MeteringContainerBuidler extends StatelessWidget {
itemsCount,
(index) => ExposurePair(
apertureValues[index + apertureOffset],
film.reciprocityFailure(shutterSpeedValues[index + shutterSpeedOffset]),
shutterSpeedValues[index + shutterSpeedOffset],
),
growable: false,
);

View file

@ -1,18 +1,15 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/film.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
@immutable
abstract class MeteringState {
final double? ev100;
final Film film;
final IsoValue iso;
final NdValue nd;
final bool isMetering;
const MeteringState({
this.ev100,
required this.film,
required this.iso,
required this.nd,
required this.isMetering,
@ -21,7 +18,6 @@ abstract class MeteringState {
class LoadingState extends MeteringState {
const LoadingState({
required super.film,
required super.iso,
required super.nd,
}) : super(isMetering: true);
@ -30,7 +26,6 @@ class LoadingState extends MeteringState {
class MeteringDataState extends MeteringState {
const MeteringDataState({
required super.ev100,
required super.film,
required super.iso,
required super.nd,
required super.isMetering,

View file

@ -1,30 +0,0 @@
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,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/components/dialog_filter/widget_dialog_filter.dart';
import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/components/dialog_range_picker/widget_dialog_picker_range.dart';
import 'package:lightmeter/screens/settings/components/shared/dialog_filter/widget_dialog_filter.dart';
import 'package:lightmeter/screens/settings/components/shared/dialog_range_picker/widget_dialog_picker_range.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class EquipmentListTiles extends StatelessWidget {

View file

@ -3,8 +3,8 @@ import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.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/components/equipment_list_tiles/widget_list_tiles_equipments.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/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/widget_list_tiles_equipments.dart';
import 'package:lightmeter/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_name_dialog/widget_dialog_equipment_profile_name.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class EquipmentProfileContainer extends StatefulWidget {

View file

@ -2,8 +2,8 @@ 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/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/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart';
import 'package:lightmeter/screens/settings/components/equipment/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';

View file

@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart';
import 'package:lightmeter/screens/settings/components/shared/iap_list_tile/widget_list_tile_iap.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class EquipmentProfilesListTile extends StatelessWidget {
const EquipmentProfilesListTile({super.key});
@override
Widget build(BuildContext context) {
return IAPListTile(
leading: const Icon(Icons.camera),
title: Text(S.of(context).equipmentProfiles),
onTap: () {
Navigator.of(context).push<EquipmentProfile>(
MaterialPageRoute(builder: (_) => const EquipmentProfilesScreen()),
);
},
);
}
}

View file

@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/screens/settings/components/shared/dialog_filter/widget_dialog_filter.dart';
import 'package:lightmeter/screens/settings/components/shared/iap_list_tile/widget_list_tile_iap.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class FilmsListTile extends StatelessWidget {
const FilmsListTile({super.key});
@override
Widget build(BuildContext context) {
return IAPListTile(
leading: const Icon(Icons.camera_roll),
title: Text(S.of(context).filmsInUse),
onTap: () {
showDialog<List<Film>>(
context: context,
builder: (_) => DialogFilter<Film>(
icon: const Icon(Icons.camera_roll),
title: S.of(context).filmsInUse,
description: S.of(context).filmsInUseDescription,
values: Films.of(context).sublist(1),
selectedValues: Films.inUseOf(context),
titleAdapter: (_, value) => value.name,
),
).then((values) {
if (values != null) {
FilmsProvider.of(context).saveFilms(values);
}
});
},
);
}
}

View file

@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/screens/settings/components/equipment/components/equipment_profiles/widget_list_tile_equipment_profiles.dart';
import 'package:lightmeter/screens/settings/components/equipment/components/films/widget_list_tile_films.dart';
import 'package:lightmeter/screens/settings/components/shared/settings_section/widget_settings_section.dart';
class EquipmentSettingsSection extends StatelessWidget {
const EquipmentSettingsSection({super.key});
@override
Widget build(BuildContext context) {
return SettingsSection(
title: S.of(context).equipment,
children: const [
EquipmentProfilesListTile(),
FilmsListTile(),
],
);
}
}

View file

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/supported_locale.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/providers/user_preferences_provider.dart';
import 'package:lightmeter/screens/settings/components/shared/dialog_picker.dart/widget_dialog_picker.dart';
import 'package:lightmeter/screens/settings/components/shared/dialog_picker/widget_dialog_picker.dart';
class LanguageListTile extends StatelessWidget {
const LanguageListTile({super.key});

View file

@ -1,43 +0,0 @@
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 {
const EquipmentProfilesListTile({super.key});
@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: 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

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/providers/user_preferences_provider.dart';
import 'package:lightmeter/screens/settings/components/shared/dialog_picker.dart/widget_dialog_picker.dart';
import 'package:lightmeter/screens/settings/components/shared/dialog_picker/widget_dialog_picker.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class StopTypeListTile extends StatelessWidget {

View file

@ -1,7 +1,6 @@
import 'package:flutter/material.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';
import 'package:lightmeter/screens/settings/components/metering/components/fractional_stops/widget_list_tile_fractional_stops.dart';
import 'package:lightmeter/screens/settings/components/metering/components/metering_screen_layout/widget_list_tile_metering_screen_layout.dart';
import 'package:lightmeter/screens/settings/components/shared/settings_section/widget_settings_section.dart';
@ -17,7 +16,6 @@ class MeteringSettingsSection extends StatelessWidget {
StopTypeListTile(),
CalibrationListTile(),
MeteringScreenLayoutListTile(),
EquipmentProfilesListTile(),
],
);
}

View file

@ -1,9 +1,8 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class DialogFilter<T extends PhotographyValue> extends StatefulWidget {
class DialogFilter<T> extends StatefulWidget {
final Icon icon;
final String title;
final String description;
@ -25,10 +24,10 @@ class DialogFilter<T extends PhotographyValue> extends StatefulWidget {
State<DialogFilter<T>> createState() => _DialogFilterState<T>();
}
class _DialogFilterState<T extends PhotographyValue> extends State<DialogFilter<T>> {
class _DialogFilterState<T> extends State<DialogFilter<T>> {
late final List<bool> checkboxValues = List.generate(
widget.values.length,
(index) => widget.selectedValues.any((element) => element.value == widget.values[index].value),
(index) => widget.selectedValues.any((element) => element == widget.values[index]),
growable: false,
);

View file

@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
/// Depends on the product status and replaces [onTap] with purchase callback
/// if the product is purchasable.
class IAPListTile extends StatelessWidget {
final IAPProductType product;
final Icon leading;
final Text title;
final VoidCallback onTap;
const IAPListTile({
this.product = IAPProductType.paidFeatures,
required this.leading,
required this.title,
required this.onTap,
super.key,
});
@override
Widget build(BuildContext context) {
return ListTile(
leading: leading,
title: title,
onTap: switch (IAPProducts.productOf(context, product)?.status) {
IAPProductStatus.purchasable => () => IAPProductsProvider.of(context).buy(product),
IAPProductStatus.pending => null,
IAPProductStatus.purchased => onTap,
null => null,
},
);
}
}

View file

@ -4,10 +4,12 @@ import 'package:lightmeter/res/dimens.dart';
class SettingsSection extends StatelessWidget {
final String title;
final List<Widget> children;
final bool enabled;
const SettingsSection({
required this.title,
required this.children,
this.enabled = true,
super.key,
});
@ -23,6 +25,8 @@ class SettingsSection extends StatelessWidget {
child: Card(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: Dimens.paddingM),
child: Opacity(
opacity: enabled ? Dimens.enabledOpacity : Dimens.disabledOpacity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
@ -42,6 +46,7 @@ class SettingsSection extends StatelessWidget {
),
),
),
),
);
}
}

View file

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/theme_type.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/providers/user_preferences_provider.dart';
import 'package:lightmeter/screens/settings/components/shared/dialog_picker.dart/widget_dialog_picker.dart';
import 'package:lightmeter/screens/settings/components/shared/dialog_picker/widget_dialog_picker.dart';
class ThemeTypeListTile extends StatelessWidget {
const ThemeTypeListTile({super.key});

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/screens/settings/components/about/widget_settings_section_about.dart';
import 'package:lightmeter/screens/settings/components/equipment/widget_settings_section_equipment.dart';
import 'package:lightmeter/screens/settings/components/general/widget_settings_section_general.dart';
import 'package:lightmeter/screens/settings/components/metering/widget_settings_section_metering.dart';
import 'package:lightmeter/screens/settings/components/theme/widget_settings_section_theme.dart';
@ -43,6 +44,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
delegate: SliverChildListDelegate(
<Widget>[
const MeteringSettingsSection(),
const EquipmentSettingsSection(),
const GeneralSettingsSection(),
const ThemeSettingsSection(),
const AboutSettingsSection(),

View file

@ -0,0 +1,11 @@
{
"folders": [
{
"path": "iap"
},
{
"path": "."
}
],
"settings": {}
}

View file

@ -1,121 +0,0 @@
import 'package:lightmeter/data/models/film.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
import 'package:test/test.dart';
void main() {
test('iso', () {
expect(const Film.other().iso, 0);
expect(const FomapanFilm.creative100().iso, 100);
expect(const FomapanFilm.creative200().iso, 200);
expect(const FomapanFilm.action400().iso, 400);
expect(const IlfordFilm.ortho().iso, 80);
expect(const IlfordFilm.delta100().iso, 100);
expect(const IlfordFilm.delta400().iso, 400);
expect(const IlfordFilm.delta3200().iso, 3200);
expect(const IlfordFilm.fp4().iso, 125);
expect(const IlfordFilm.hp5().iso, 400);
expect(const IlfordFilm.panf().iso, 50);
expect(const IlfordFilm.sfx200().iso, 200);
expect(const IlfordFilm.xp2super().iso, 400);
expect(const IlfordFilm.pan100().iso, 100);
expect(const IlfordFilm.pan400().iso, 400);
expect(const KodakFilm.tmax100().iso, 100);
expect(const KodakFilm.tmax400().iso, 400);
expect(const KodakFilm.tmax3200().iso, 3200);
expect(const KodakFilm.trix320().iso, 320);
expect(const KodakFilm.trix400().iso, 400);
});
test('toString()', () {
expect(const Film.other().toString(), "");
expect(const FomapanFilm.creative100().toString(), "Fomapan CREATIVE 100");
expect(const FomapanFilm.creative200().toString(), "Fomapan CREATIVE 200");
expect(const FomapanFilm.action400().toString(), "Fomapan ACTION 400");
expect(const IlfordFilm.ortho().toString(), "Ilford ORTHO+");
expect(const IlfordFilm.delta100().toString(), "Ilford DELTA 100");
expect(const IlfordFilm.delta400().toString(), "Ilford DELTA 400");
expect(const IlfordFilm.delta3200().toString(), "Ilford DELTA 3200");
expect(const IlfordFilm.fp4().toString(), "Ilford FP4+");
expect(const IlfordFilm.hp5().toString(), "Ilford HP5+");
expect(const IlfordFilm.panf().toString(), "Ilford Pan F+");
expect(const IlfordFilm.sfx200().toString(), "Ilford SFX 200");
expect(const IlfordFilm.xp2super().toString(), "Ilford XP2 SUPER");
expect(const IlfordFilm.pan100().toString(), "Kentemere 100");
expect(const IlfordFilm.pan400().toString(), "Kentemere 400");
expect(const KodakFilm.tmax100().toString(), "Kodak T-MAX 100");
expect(const KodakFilm.tmax400().toString(), "Kodak T-MAX 400");
expect(const KodakFilm.tmax3200().toString(), "Kodak T-MAX 3200");
expect(const KodakFilm.trix320().toString(), "Kodak TRI-X 320");
expect(const KodakFilm.trix400().toString(), "Kodak TRI-X 400");
});
group(
'reciprocityFailure',
() {
const inputSpeeds = [
ShutterSpeedValue(1000, true, StopType.full),
ShutterSpeedValue(1, false, StopType.full),
ShutterSpeedValue(16, false, StopType.full)
];
test('No change `Film.other()`', () {
expect(
const Film.other().reciprocityFailure(inputSpeeds[0]),
const ShutterSpeedValue(1000, true, StopType.full),
);
expect(
const Film.other().reciprocityFailure(inputSpeeds[1]),
const ShutterSpeedValue(1, false, StopType.full),
);
expect(
const Film.other().reciprocityFailure(inputSpeeds[2]),
const ShutterSpeedValue(16, false, StopType.full),
);
});
test('pow `IlfordFilm.delta100()`', () {
expect(
const IlfordFilm.delta100().reciprocityFailure(inputSpeeds[0]),
const ShutterSpeedValue(1000, true, StopType.full),
);
expect(
const IlfordFilm.delta100().reciprocityFailure(inputSpeeds[1]),
const ShutterSpeedValue(1, false, StopType.full),
);
expect(
const IlfordFilm.delta100().reciprocityFailure(inputSpeeds[2]),
const ShutterSpeedValue(32.899642452994128, false, StopType.full),
);
});
test('log10polynomian `FomapanFilm.creative100()`', () {
expect(
const FomapanFilm.creative100().reciprocityFailure(inputSpeeds[0]),
const ShutterSpeedValue(1000, true, StopType.full),
);
expect(
const FomapanFilm.creative100().reciprocityFailure(inputSpeeds[1]),
const ShutterSpeedValue(2, false, StopType.full),
);
expect(
const FomapanFilm.creative100().reciprocityFailure(inputSpeeds[2]),
const ShutterSpeedValue(151.52807753457483, false, StopType.full),
);
});
test('log10polynomian `Kodak.tmax400()`', () {
expect(
const KodakFilm.tmax400().reciprocityFailure(inputSpeeds[0]),
const ShutterSpeedValue(1000, true, StopType.full),
);
expect(
const KodakFilm.tmax400().reciprocityFailure(inputSpeeds[1]),
const ShutterSpeedValue(1.3333333333333333, false, StopType.full),
);
expect(
const KodakFilm.tmax400().reciprocityFailure(inputSpeeds[2]),
const ShutterSpeedValue(27.166026086819844, false, StopType.full),
);
});
},
);
}

View file

@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:lightmeter/data/models/ev_source_type.dart';
import 'package:lightmeter/data/models/film.dart';
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
import 'package:lightmeter/data/models/supported_locale.dart';
import 'package:lightmeter/data/models/theme_type.dart';
@ -392,26 +391,4 @@ void main() {
.called(1);
});
});
group('film', () {
test('get default', () {
when(() => sharedPreferences.getString(UserPreferencesService.filmKey)).thenReturn(null);
expect(service.film, Film.values.first);
});
test('get', () {
when(() => sharedPreferences.getString(UserPreferencesService.filmKey))
.thenReturn('Fomapan ACTION 400');
expect(service.film, const FomapanFilm.action400());
});
test('set', () {
when(() => sharedPreferences.setString(UserPreferencesService.filmKey, 'Fomapan ACTION 400'))
.thenAnswer((_) => Future.value(true));
service.film = const FomapanFilm.action400();
verify(
() => sharedPreferences.setString(UserPreferencesService.filmKey, 'Fomapan ACTION 400'),
).called(1);
});
});
}

View file

@ -2,7 +2,6 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:lightmeter/data/caffeine_service.dart';
import 'package:lightmeter/data/haptics_service.dart';
import 'package:lightmeter/data/light_sensor_service.dart';
import 'package:lightmeter/data/models/film.dart';
import 'package:lightmeter/data/models/volume_action.dart';
import 'package:lightmeter/data/permissions_service.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
@ -124,19 +123,6 @@ void main() {
interactor.ndFilter = NdValue.values.first;
verify(() => mockUserPreferencesService.ndFilter = NdValue.values.first).called(1);
});
test('film - get', () async {
when(() => mockUserPreferencesService.film).thenReturn(Film.values.first);
expect(interactor.film, Film.values.first);
verify(() => mockUserPreferencesService.film).called(1);
});
test('film - set', () async {
when(() => mockUserPreferencesService.film = Film.values.first)
.thenReturn(Film.values.first);
interactor.film = Film.values.first;
verify(() => mockUserPreferencesService.film = Film.values.first).called(1);
});
},
);

View file

@ -1,5 +1,4 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:lightmeter/data/models/film.dart';
import 'package:lightmeter/data/models/volume_action.dart';
import 'package:lightmeter/interactors/metering_interactor.dart';
import 'package:lightmeter/screens/metering/bloc_metering.dart';
@ -34,7 +33,6 @@ void main() {
meteringInteractor = _MockMeteringInteractor();
when<IsoValue>(() => meteringInteractor.iso).thenReturn(iso100);
when<NdValue>(() => meteringInteractor.ndFilter).thenReturn(NdValue.values.first);
when<Film>(() => meteringInteractor.film).thenReturn(Film.values.first);
when(meteringInteractor.quickVibration).thenAnswer((_) async {});
when(meteringInteractor.responseVibration).thenAnswer((_) async {});
when(meteringInteractor.errorVibration).thenAnswer((_) async {});
@ -157,7 +155,6 @@ void main() {
build: () => bloc,
seed: () => MeteringDataState(
ev100: 1.0,
film: Film.values[1],
iso: const IsoValue(100, StopType.full),
nd: NdValue.values.first,
isMetering: false,
@ -166,14 +163,12 @@ void main() {
bloc.add(const IsoChangedEvent(IsoValue(200, StopType.full)));
},
verify: (_) {
verify(() => meteringInteractor.film = Film.values.first).called(1);
verify(() => meteringInteractor.iso = const IsoValue(200, StopType.full)).called(1);
},
expect: () => [
isA<MeteringDataState>()
.having((state) => state.ev100, 'ev100', 1.0)
.having((state) => state.ev, 'ev', 2.0)
.having((state) => state.film, 'film', Film.values.first)
.having((state) => state.iso, 'iso', const IsoValue(200, StopType.full))
.having((state) => state.nd, 'nd', NdValue.values.first)
.having((state) => state.isMetering, 'isMetering', false),
@ -185,7 +180,6 @@ void main() {
build: () => bloc,
seed: () => MeteringDataState(
ev100: null,
film: Film.values[1],
iso: const IsoValue(100, StopType.full),
nd: NdValue.values.first,
isMetering: false,
@ -194,14 +188,12 @@ void main() {
bloc.add(const IsoChangedEvent(IsoValue(200, StopType.full)));
},
verify: (_) {
verify(() => meteringInteractor.film = Film.values.first).called(1);
verify(() => meteringInteractor.iso = const IsoValue(200, StopType.full)).called(1);
},
expect: () => [
isA<MeteringDataState>()
.having((state) => state.ev100, 'ev100', null)
.having((state) => state.ev, 'ev', null)
.having((state) => state.film, 'film', Film.values.first)
.having((state) => state.iso, 'iso', const IsoValue(200, StopType.full))
.having((state) => state.nd, 'nd', NdValue.values.first)
.having((state) => state.isMetering, 'isMetering', false),
@ -213,7 +205,6 @@ void main() {
build: () => bloc,
seed: () => MeteringDataState(
ev100: 1.0,
film: Film.values[1],
iso: const IsoValue(100, StopType.full),
nd: NdValue.values.first,
isMetering: false,
@ -222,7 +213,6 @@ void main() {
bloc.add(const IsoChangedEvent(IsoValue(100, StopType.full)));
},
verify: (_) {
verify(() => meteringInteractor.film = Film.values.first).called(1);
verifyNever(() => meteringInteractor.iso = const IsoValue(100, StopType.full));
},
expect: () => [],
@ -233,7 +223,6 @@ void main() {
build: () => bloc,
seed: () => MeteringDataState(
ev100: 1.0,
film: Film.values[1],
iso: const IsoValue(100, StopType.full),
nd: NdValue.values.first,
isMetering: false,
@ -244,14 +233,12 @@ void main() {
bloc.onCommunicationState(const communication_states.MeteringEndedState(2));
},
verify: (_) {
verify(() => meteringInteractor.film = Film.values.first).called(1);
verify(() => meteringInteractor.iso = const IsoValue(200, StopType.full)).called(1);
},
expect: () => [
isA<MeteringDataState>()
.having((state) => state.ev100, 'ev100', 1.0)
.having((state) => state.ev, 'ev', 2.0)
.having((state) => state.film, 'film', Film.values.first)
.having((state) => state.iso, 'iso', const IsoValue(200, StopType.full))
.having((state) => state.nd, 'nd', NdValue.values.first)
.having((state) => state.isMetering, 'isMetering', false),
@ -259,7 +246,6 @@ void main() {
isA<MeteringDataState>()
.having((state) => state.ev100, 'ev100', 2.0)
.having((state) => state.ev, 'ev', 3.0)
.having((state) => state.film, 'film', Film.values.first)
.having((state) => state.iso, 'iso', const IsoValue(200, StopType.full))
.having((state) => state.nd, 'nd', NdValue.values.first)
.having((state) => state.isMetering, 'isMetering', false),
@ -276,7 +262,6 @@ void main() {
build: () => bloc,
seed: () => MeteringDataState(
ev100: 1.0,
film: Film.values[1],
iso: const IsoValue(100, StopType.full),
nd: NdValue.values.first,
isMetering: false,
@ -291,7 +276,6 @@ void main() {
isA<MeteringDataState>()
.having((state) => state.ev100, 'ev100', 1.0)
.having((state) => state.ev, 'ev', 0.0)
.having((state) => state.film, 'film', Film.values[1])
.having((state) => state.iso, 'iso', const IsoValue(100, StopType.full))
.having((state) => state.nd, 'nd', const NdValue(2))
.having((state) => state.isMetering, 'isMetering', false),
@ -303,7 +287,6 @@ void main() {
build: () => bloc,
seed: () => MeteringDataState(
ev100: null,
film: Film.values[1],
iso: const IsoValue(100, StopType.full),
nd: NdValue.values.first,
isMetering: false,
@ -318,7 +301,6 @@ void main() {
isA<MeteringDataState>()
.having((state) => state.ev100, 'ev100', null)
.having((state) => state.ev, 'ev', null)
.having((state) => state.film, 'film', Film.values[1])
.having((state) => state.iso, 'iso', const IsoValue(100, StopType.full))
.having((state) => state.nd, 'nd', const NdValue(2))
.having((state) => state.isMetering, 'isMetering', false),
@ -330,7 +312,6 @@ void main() {
build: () => bloc,
seed: () => MeteringDataState(
ev100: 1.0,
film: Film.values[1],
iso: const IsoValue(100, StopType.full),
nd: NdValue.values.first,
isMetering: false,
@ -349,7 +330,6 @@ void main() {
build: () => bloc,
seed: () => MeteringDataState(
ev100: 1.0,
film: Film.values[1],
iso: const IsoValue(100, StopType.full),
nd: NdValue.values.first,
isMetering: false,
@ -366,7 +346,6 @@ void main() {
isA<MeteringDataState>()
.having((state) => state.ev100, 'ev100', 1.0)
.having((state) => state.ev, 'ev', 0.0)
.having((state) => state.film, 'film', Film.values[1])
.having((state) => state.iso, 'iso', const IsoValue(100, StopType.full))
.having((state) => state.nd, 'nd', const NdValue(2))
.having((state) => state.isMetering, 'isMetering', false),
@ -374,7 +353,6 @@ void main() {
isA<MeteringDataState>()
.having((state) => state.ev100, 'ev100', 2.0)
.having((state) => state.ev, 'ev', 1.0)
.having((state) => state.film, 'film', Film.values[1])
.having((state) => state.iso, 'iso', const IsoValue(100, StopType.full))
.having((state) => state.nd, 'nd', const NdValue(2))
.having((state) => state.isMetering, 'isMetering', false),
@ -383,115 +361,6 @@ void main() {
},
);
group(
'`FilmChangedEvent`',
() {
blocTest<MeteringBloc, MeteringState>(
'Pick different film with different ISO',
build: () => bloc,
seed: () => MeteringDataState(
ev100: 1.0,
film: const FomapanFilm.creative100(),
iso: const IsoValue(100, StopType.full),
nd: NdValue.values.first,
isMetering: false,
),
act: (bloc) async {
bloc.add(const FilmChangedEvent(FomapanFilm.creative200()));
},
verify: (_) {
verify(() => meteringInteractor.film = const FomapanFilm.creative200()).called(1);
verify(() => meteringInteractor.iso = const IsoValue(200, StopType.full)).called(1);
},
expect: () => [
isA<MeteringDataState>()
.having((state) => state.ev100, 'ev100', 1.0)
.having((state) => state.ev, 'ev', 2.0)
.having((state) => state.film, 'film', const FomapanFilm.creative200())
.having((state) => state.iso, 'iso', const IsoValue(200, StopType.full))
.having((state) => state.nd, 'nd', NdValue.values.first)
.having((state) => state.isMetering, 'isMetering', false),
],
);
blocTest<MeteringBloc, MeteringState>(
'Pick different film with same ISO',
build: () => bloc,
seed: () => MeteringDataState(
ev100: 1.0,
film: const FomapanFilm.creative100(),
iso: const IsoValue(100, StopType.full),
nd: NdValue.values.first,
isMetering: false,
),
act: (bloc) async {
bloc.add(const FilmChangedEvent(IlfordFilm.delta100()));
},
verify: (_) {
verify(() => meteringInteractor.film = const IlfordFilm.delta100()).called(1);
verifyNever(() => meteringInteractor.iso = const IsoValue(100, StopType.full));
},
expect: () => [
isA<MeteringDataState>()
.having((state) => state.ev100, 'ev100', 1.0)
.having((state) => state.ev, 'ev', 1.0)
.having((state) => state.film, 'film', const IlfordFilm.delta100())
.having((state) => state.iso, 'iso', const IsoValue(100, StopType.full))
.having((state) => state.nd, 'nd', NdValue.values.first)
.having((state) => state.isMetering, 'isMetering', false),
],
);
blocTest<MeteringBloc, MeteringState>(
'Pick same film',
build: () => bloc,
seed: () => MeteringDataState(
ev100: 1.0,
film: const FomapanFilm.creative100(),
iso: const IsoValue(100, StopType.full),
nd: NdValue.values.first,
isMetering: false,
),
act: (bloc) async {
bloc.add(const FilmChangedEvent(FomapanFilm.creative100()));
},
verify: (_) {
verifyNever(() => meteringInteractor.film = const FomapanFilm.creative100());
},
expect: () => [],
);
blocTest<MeteringBloc, MeteringState>(
'Pick `Film.other()`',
build: () => bloc,
seed: () => MeteringDataState(
ev100: 1.0,
film: const FomapanFilm.creative100(),
iso: const IsoValue(100, StopType.full),
nd: NdValue.values.first,
isMetering: false,
),
act: (bloc) async {
bloc.add(const FilmChangedEvent(Film.other()));
},
verify: (_) {
verify(() => meteringInteractor.film = const Film.other()).called(1);
verifyNever(() => meteringInteractor.iso = const IsoValue(0, StopType.full));
verifyNever(() => meteringInteractor.responseVibration());
},
expect: () => [
isA<MeteringDataState>()
.having((state) => state.ev100, 'ev100', 1.0)
.having((state) => state.ev, 'ev', 1.0)
.having((state) => state.film, 'film', const Film.other())
.having((state) => state.iso, 'iso', const IsoValue(100, StopType.full))
.having((state) => state.nd, 'nd', NdValue.values.first)
.having((state) => state.isMetering, 'isMetering', false),
],
);
},
);
group(
'`EquipmentProfileChangedEvent`',
() {
@ -509,7 +378,6 @@ void main() {
build: () => bloc,
seed: () => MeteringDataState(
ev100: 1.0,
film: Film.values[1],
iso: const IsoValue(100, StopType.full),
nd: NdValue.values.first,
isMetering: false,
@ -518,7 +386,6 @@ void main() {
bloc.add(EquipmentProfileChangedEvent(reducedProfile));
},
verify: (_) {
verifyNever(() => meteringInteractor.film = const Film.other());
verifyNever(() => meteringInteractor.iso = reducedProfile.isoValues.first);
verifyNever(() => meteringInteractor.ndFilter = reducedProfile.ndValues.first);
verifyNever(() => meteringInteractor.responseVibration());
@ -531,7 +398,6 @@ void main() {
build: () => bloc,
seed: () => MeteringDataState(
ev100: 1.0,
film: Film.values[1],
iso: IsoValue.values[2],
nd: NdValue.values.first,
isMetering: false,
@ -540,7 +406,6 @@ void main() {
bloc.add(EquipmentProfileChangedEvent(reducedProfile));
},
verify: (_) {
verify(() => meteringInteractor.film = const Film.other()).called(1);
verify(() => meteringInteractor.iso = reducedProfile.isoValues.first).called(1);
verifyNever(() => meteringInteractor.ndFilter = reducedProfile.ndValues.first);
verify(() => meteringInteractor.responseVibration()).called(1);
@ -548,7 +413,6 @@ void main() {
expect: () => [
isA<MeteringDataState>()
.having((state) => state.ev100, 'ev100', 1.0)
.having((state) => state.film, 'film', const Film.other())
.having((state) => state.iso, 'iso', reducedProfile.isoValues.first)
.having((state) => state.nd, 'nd', NdValue.values.first)
.having((state) => state.isMetering, 'isMetering', false),
@ -560,7 +424,6 @@ void main() {
build: () => bloc,
seed: () => MeteringDataState(
ev100: 1.0,
film: Film.values[1],
iso: const IsoValue(100, StopType.full),
nd: NdValue.values[4],
isMetering: false,
@ -569,7 +432,6 @@ void main() {
bloc.add(EquipmentProfileChangedEvent(reducedProfile));
},
verify: (_) {
verifyNever(() => meteringInteractor.film = const Film.other());
verifyNever(() => meteringInteractor.iso = reducedProfile.isoValues.first);
verify(() => meteringInteractor.ndFilter = reducedProfile.ndValues.first).called(1);
verify(() => meteringInteractor.responseVibration()).called(1);
@ -577,7 +439,6 @@ void main() {
expect: () => [
isA<MeteringDataState>()
.having((state) => state.ev100, 'ev100', 1.0)
.having((state) => state.film, 'film', Film.values[1])
.having((state) => state.iso, 'iso', const IsoValue(100, StopType.full))
.having((state) => state.nd, 'nd', reducedProfile.ndValues.first)
.having((state) => state.isMetering, 'isMetering', false),
@ -589,7 +450,6 @@ void main() {
build: () => bloc,
seed: () => MeteringDataState(
ev100: 1.0,
film: Film.values[1],
iso: IsoValue.values[2],
nd: NdValue.values[4],
isMetering: false,
@ -598,7 +458,6 @@ void main() {
bloc.add(EquipmentProfileChangedEvent(reducedProfile));
},
verify: (_) {
verify(() => meteringInteractor.film = const Film.other()).called(1);
verify(() => meteringInteractor.iso = reducedProfile.isoValues.first).called(1);
verify(() => meteringInteractor.ndFilter = reducedProfile.ndValues.first).called(1);
verify(() => meteringInteractor.responseVibration()).called(1);
@ -606,7 +465,6 @@ void main() {
expect: () => [
isA<MeteringDataState>()
.having((state) => state.ev100, 'ev100', 1.0)
.having((state) => state.film, 'film', const Film.other())
.having((state) => state.iso, 'iso', reducedProfile.isoValues.first)
.having((state) => state.nd, 'nd', reducedProfile.ndValues.first)
.having((state) => state.isMetering, 'isMetering', false),

View file

@ -1,6 +1,5 @@
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';
@ -19,7 +18,6 @@ void main() {
ev,
StopType.full,
defaultEquipmentProfile,
const Film.other(),
);
test('isNan', () {
@ -42,7 +40,6 @@ void main() {
ev,
StopType.full,
defaultEquipmentProfile,
const Film.other(),
);
test('EV 1', () {
@ -142,7 +139,6 @@ void main() {
ev,
StopType.half,
defaultEquipmentProfile,
const Film.other(),
);
test('EV 1', () {
@ -242,7 +238,6 @@ void main() {
ev,
StopType.third,
defaultEquipmentProfile,
const Film.other(),
);
test('EV 1', () {
@ -356,7 +351,6 @@ void main() {
ev,
StopType.full,
equipmentProfile,
const Film.other(),
);
test('EV 1', () {
@ -456,7 +450,6 @@ void main() {
ev,
StopType.half,
equipmentProfile,
const Film.other(),
);
test('EV 1', () {
@ -556,7 +549,6 @@ void main() {
ev,
StopType.third,
equipmentProfile,
const Film.other(),
);
test('EV 1', () {
@ -669,7 +661,6 @@ void main() {
ev,
StopType.full,
equipmentProfile,
const Film.other(),
);
test('EV 1', () {
@ -769,7 +760,6 @@ void main() {
ev,
StopType.half,
equipmentProfile,
const Film.other(),
);
test('EV 1', () {
@ -869,7 +859,6 @@ void main() {
ev,
StopType.third,
equipmentProfile,
const Film.other(),
);
test('EV 1', () {