ML-42 Implement equipment profiles creating (#45)

* added Equipment section placeholder

* get iso & nd values from equipment profile

* use photography values from remote repo

* removed equipment section

* wip

* moved `EquipmentProfileProvider` from iap repo

* wip

* moved equipment profiles screen from iap

* improved equipment profiles screen

* mock add/delete

* collapse on expand

* add profile with name

* show selected values count (wip)

* fixed profile update

* cleanup

* Update pubspec.yaml

* made `AnimatedDialogPicker` more generic

* switched to local `Dimens`

* fixed `MeteringTopBarShape`

* rename

* animated `EquipmentProfileContainer`

* added default equipment profile

* change equipment profile name via dialog

* fixed profile selection

* filter equipment profile update/delete

* removed `enabled` param from settings section

* non-null `EquipmentProfile`

* fixed duplicate GlobalKeys

* animated equipment list

* Update ci.yml

* fixed shutter speed anchor issue

* autofocus

* added firebase to project

* save/restore equipment profiles

* unified `SliverList`

* added SSH key to iap repo

* Update ci.yml

* ci recursive submodules

* try full url

* Revert "try full url"

This reverts commit a9b692b60e.

* restore firebase_options.dart

* changed runner to macos

* restore options earlier

* removed problematic file from analysis :)

* removed launch_app

* textoverflow

* implemented `DialogRangePicker`

* add iap repo to cd

* typo

* added    workflow_dispatch to crowdin push

* removed `equipmentProfileValuesCount` from intl

* fr & ru translations

* style

* removed iap
This commit is contained in:
Vadim 2023-03-30 22:24:18 +03:00 committed by GitHub
parent 6ffd164171
commit 6bf059ed4d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
57 changed files with 1427 additions and 534 deletions

View file

@ -23,7 +23,17 @@ jobs:
timeout-minutes: 30
steps:
- uses: shaunco/ssh-agent@git-repo-mapping
with:
ssh-private-key: |
${{ secrets.M3_LIGHTMETER_IAP_KEY }}
repo-mappings: |
github.com/vodemn/m3_lightmeter_iap
- uses: actions/checkout@v3
with:
submodules: recursive
- uses: actions/setup-java@v2
with:
distribution: "zulu"
@ -41,6 +51,14 @@ jobs:
echo -n "$KEYSTORE_PROPERTIES" | base64 --decode --output $KEYSTORE_PROPERTIES_PATH
cp $KEYSTORE_PROPERTIES_PATH ./android
- name: Restore firebase_options.dart
env:
KEYSTORE: ${{ secrets.FIREBASE_OPTIONS }}
run: |
FIREBASE_OPTIONS_PATH=$RUNNER_TEMP/firebase_options.dart
echo -n "$FIREBASE_OPTIONS" | base64 --decode --output $FIREBASE_OPTIONS_PATH
cp $FIREBASE_OPTIONS_PATH ./lib
- name: Install Flutter
uses: subosito/flutter-action@v2
with:

View file

@ -14,7 +14,17 @@ jobs:
timeout-minutes: 30
steps:
- uses: shaunco/ssh-agent@git-repo-mapping
with:
ssh-private-key: |
${{ secrets.M3_LIGHTMETER_IAP_KEY }}
repo-mappings: |
github.com/vodemn/m3_lightmeter_iap
- uses: actions/checkout@v3
with:
submodules: recursive
- uses: actions/setup-java@v2
with:
distribution: "zulu"
@ -32,6 +42,14 @@ jobs:
echo -n "$KEYSTORE_PROPERTIES" | base64 --decode --output $KEYSTORE_PROPERTIES_PATH
cp $KEYSTORE_PROPERTIES_PATH ./android
- name: Restore firebase_options.dart
env:
KEYSTORE: ${{ secrets.FIREBASE_OPTIONS }}
run: |
FIREBASE_OPTIONS_PATH=$RUNNER_TEMP/firebase_options.dart
echo -n "$FIREBASE_OPTIONS" | base64 --decode --output $FIREBASE_OPTIONS_PATH
cp $FIREBASE_OPTIONS_PATH ./lib
- name: Install Flutter
uses: subosito/flutter-action@v2
with:

View file

@ -7,17 +7,27 @@ name: Pull Request check
on:
push:
branches: [ "main" ]
branches: ["main"]
pull_request:
branches: [ "main" ]
branches: ["main"]
jobs:
build:
runs-on: ubuntu-latest
runs-on: macos-11
timeout-minutes: 5
steps:
- uses: shaunco/ssh-agent@git-repo-mapping
with:
ssh-private-key: |
${{ secrets.M3_LIGHTMETER_IAP_KEY }}
repo-mappings: |
github.com/vodemn/m3_lightmeter_iap
- uses: actions/checkout@v3
with:
submodules: recursive
- uses: subosito/flutter-action@v2
with:
channel: "stable"
@ -33,6 +43,5 @@ jobs:
- name: Analyze project source
run: flutter analyze lib --fatal-infos
- name: Run tests
run: flutter test
# - name: Run tests
# run: flutter test

6
.gitignore vendored
View file

@ -52,3 +52,9 @@ pubspec.lock
/ios/Podfile.lock
.fvm/
.jks
keystore.properties
android/app/google-services.json
ios/firebase_app_id_file.json
ios/Runner/GoogleService-Info.plist
lib/firebase_options.dart

1
.vscode/launch.json vendored
View file

@ -21,6 +21,7 @@
"name": "dev (ios)",
"request": "launch",
"type": "dart",
//"flutterMode": "release",
"args": [
"--flavor",
"dev",

View file

@ -49,9 +49,7 @@ The list of features that the old lightmeter app has and that have to be impleme
## Build
```
flutter build apk --flavor dev --dart-define cameraPreviewAspectRatio=2/3 -t lib/main_dev.dart
```
As part of this project is private, you will be able to run this app from the _main_dev.dart_ file (i.e. --flavor dev). Also to avoid fatal errors the _main_prod.dart_ file is excluded from analysis.
## Contribution

View file

@ -1 +1,3 @@
include: package:flutter_lints/flutter.yaml
analyzer:
exclude: [lib/main_prod.dart]

View file

@ -28,6 +28,7 @@ if (keystorePropertiesFile.exists()) {
}
apply plugin: 'com.android.application'
apply plugin: 'com.google.gms.google-services'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
@ -98,4 +99,5 @@ flutter {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "com.android.billingclient:billing-ktx:5.1.0"
}

View file

@ -7,6 +7,7 @@ buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:7.1.2'
classpath 'com.google.gms:google-services:4.3.10'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

View file

@ -11,6 +11,7 @@
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
5A1AF02F1E4619A6E479AA8B /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C991C89D5763E562E77E475E /* Pods_Runner.framework */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
7759853DDE1156498D18536B /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 2913B1E428DD3F7921E898BC /* GoogleService-Info.plist */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
@ -32,6 +33,7 @@
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
2913B1E428DD3F7921E898BC /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
4F64CFBF322918DEF6B858DA /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
5BEEF1AE48859B3E3AAAC421 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
@ -86,6 +88,7 @@
97C146EF1CF9000F007C117D /* Products */,
CFB3DEB969CB62463CE0ACDF /* Pods */,
A5DCEB322972D36722A63973 /* Frameworks */,
2913B1E428DD3F7921E898BC /* GoogleService-Info.plist */,
);
sourceTree = "<group>";
};
@ -203,6 +206,7 @@
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
7759853DDE1156498D18536B /* GoogleService-Info.plist in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View file

@ -16,6 +16,7 @@ import 'data/permissions_service.dart';
import 'data/shared_prefs_service.dart';
import 'environment.dart';
import 'generated/l10n.dart';
import 'providers/equipment_profile_provider.dart';
import 'providers/ev_source_type_provider.dart';
import 'providers/theme_provider.dart';
import 'screens/metering/flow_metering.dart';
@ -47,29 +48,31 @@ class Application extends StatelessWidget {
Provider(create: (_) => const LightSensorService()),
],
child: StopTypeProvider(
child: EvSourceTypeProvider(
child: SupportedLocaleProvider(
child: ThemeProvider(
builder: (context, _) => _AnnotatedRegionWrapper(
child: MaterialApp(
theme: context.watch<ThemeData>(),
locale: Locale(context.watch<SupportedLocale>().intlName),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
builder: (context, child) => MediaQuery(
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
child: child!,
child: EquipmentProfileProvider(
child: EvSourceTypeProvider(
child: SupportedLocaleProvider(
child: ThemeProvider(
builder: (context, _) => _AnnotatedRegionWrapper(
child: MaterialApp(
theme: context.watch<ThemeData>(),
locale: Locale(context.watch<SupportedLocale>().intlName),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
builder: (context, child) => MediaQuery(
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
child: child!,
),
initialRoute: "metering",
routes: {
"metering": (context) => const MeteringFlow(),
"settings": (context) => const SettingsFlow(),
},
),
initialRoute: "metering",
routes: {
"metering": (context) => const MeteringFlow(),
"settings": (context) => const SettingsFlow(),
},
),
),
),

View file

@ -1,5 +1,4 @@
import 'photography_values/aperture_value.dart';
import 'photography_values/shutter_speed_value.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class ExposurePair {
final ApertureValue aperture;

View file

@ -1,64 +0,0 @@
import 'photography_value.dart';
class ApertureValue extends PhotographyStopValue<double> {
const ApertureValue(super.rawValue, super.stopType);
@override
String toString() {
final buffer = StringBuffer("f/");
if (rawValue - rawValue.floor() == 0 && rawValue >= 8) {
buffer.write(rawValue.toInt().toString());
} else {
buffer.write(rawValue.toStringAsFixed(1));
}
return buffer.toString();
}
}
const List<ApertureValue> apertureValues = [
ApertureValue(1.0, StopType.full),
ApertureValue(1.1, StopType.third),
ApertureValue(1.2, StopType.half),
ApertureValue(1.2, StopType.third),
ApertureValue(1.4, StopType.full),
ApertureValue(1.6, StopType.third),
ApertureValue(1.7, StopType.half),
ApertureValue(1.8, StopType.third),
ApertureValue(2.0, StopType.full),
ApertureValue(2.2, StopType.third),
ApertureValue(2.4, StopType.half),
ApertureValue(2.4, StopType.third),
ApertureValue(2.8, StopType.full),
ApertureValue(3.2, StopType.third),
ApertureValue(3.3, StopType.half),
ApertureValue(3.5, StopType.third),
ApertureValue(4.0, StopType.full),
ApertureValue(4.5, StopType.third),
ApertureValue(4.8, StopType.half),
ApertureValue(5.0, StopType.third),
ApertureValue(5.6, StopType.full),
ApertureValue(6.3, StopType.third),
ApertureValue(6.7, StopType.half),
ApertureValue(7.1, StopType.third),
ApertureValue(8, StopType.full),
ApertureValue(9, StopType.third),
ApertureValue(9.5, StopType.half),
ApertureValue(10, StopType.third),
ApertureValue(11, StopType.full),
ApertureValue(13, StopType.third),
ApertureValue(13, StopType.half),
ApertureValue(14, StopType.third),
ApertureValue(16, StopType.full),
ApertureValue(18, StopType.third),
ApertureValue(19, StopType.half),
ApertureValue(20, StopType.third),
ApertureValue(22, StopType.full),
ApertureValue(25, StopType.third),
ApertureValue(27, StopType.half),
ApertureValue(29, StopType.third),
ApertureValue(32, StopType.full),
ApertureValue(36, StopType.third),
ApertureValue(38, StopType.half),
ApertureValue(42, StopType.third),
ApertureValue(45, StopType.full),
];

View file

@ -1,45 +0,0 @@
import 'photography_value.dart';
class IsoValue extends PhotographyStopValue<int> {
const IsoValue(super.rawValue, super.stopType);
@override
String toString() => value.toString();
}
const List<IsoValue> isoValues = [
IsoValue(3, StopType.full),
IsoValue(4, StopType.third),
IsoValue(5, StopType.third),
IsoValue(6, StopType.full),
IsoValue(8, StopType.third),
IsoValue(10, StopType.third),
IsoValue(12, StopType.full),
IsoValue(16, StopType.third),
IsoValue(20, StopType.third),
IsoValue(25, StopType.full),
IsoValue(32, StopType.third),
IsoValue(40, StopType.third),
IsoValue(50, StopType.full),
IsoValue(64, StopType.third),
IsoValue(80, StopType.third),
IsoValue(100, StopType.full),
IsoValue(125, StopType.third),
IsoValue(160, StopType.third),
IsoValue(200, StopType.full),
IsoValue(250, StopType.third),
IsoValue(320, StopType.third),
IsoValue(400, StopType.full),
IsoValue(500, StopType.third),
IsoValue(640, StopType.third),
IsoValue(800, StopType.full),
IsoValue(1000, StopType.third),
IsoValue(1250, StopType.third),
IsoValue(1600, StopType.full),
IsoValue(2000, StopType.third),
IsoValue(2500, StopType.third),
IsoValue(3200, StopType.full),
IsoValue(4000, StopType.third),
IsoValue(5000, StopType.third),
IsoValue(6400, StopType.full),
];

View file

@ -1,34 +0,0 @@
import 'package:lightmeter/utils/log_2.dart';
import 'photography_value.dart';
class NdValue extends PhotographyValue<int> {
const NdValue(super.rawValue);
double get stopReduction => value == 0 ? 0.0 : log2(value);
@override
String toString() => 'ND$value';
}
/// https://shuttermuse.com/neutral-density-filter-numbers-names/
const List<NdValue> ndValues = [
NdValue(0),
NdValue(2),
NdValue(4),
NdValue(8),
NdValue(16),
NdValue(32),
NdValue(64),
NdValue(100),
NdValue(128),
NdValue(256),
NdValue(400),
NdValue(512),
NdValue(1024),
NdValue(2048),
NdValue(4096),
NdValue(6310),
NdValue(8192),
NdValue(10000),
];

View file

@ -1,51 +0,0 @@
import 'dart:math';
import 'package:lightmeter/utils/log_2.dart';
enum StopType { full, half, third }
abstract class PhotographyValue<T extends num> {
final T rawValue;
const PhotographyValue(this.rawValue);
T get value => rawValue;
/// EV difference between `this` and `other`
double evDifference(PhotographyValue other) => log2(max(1, other.value) / max(1, value));
String toStringDifference(PhotographyValue other) {
final ev = log2(max(1, other.value) / max(1, value));
final buffer = StringBuffer();
if (ev > 0) {
buffer.write('+');
}
buffer.write(ev.toStringAsFixed(1));
return buffer.toString();
}
}
abstract class PhotographyStopValue<T extends num> extends PhotographyValue<T> {
final StopType stopType;
const PhotographyStopValue(super.rawValue, this.stopType);
}
extension PhotographyStopValues<T extends PhotographyStopValue> on List<T> {
List<T> whereStopType(StopType stopType) {
switch (stopType) {
case StopType.full:
return where((e) => e.stopType == StopType.full).toList();
case StopType.half:
return where((e) => e.stopType == StopType.full || e.stopType == StopType.half).toList();
case StopType.third:
return where((e) => e.stopType == StopType.full || e.stopType == StopType.third).toList();
}
}
List<T> fullStops() => whereStopType(StopType.full);
List<T> halfStops() => whereStopType(StopType.half);
List<T> thirdStops() => whereStopType(StopType.third);
}

View file

@ -1,87 +0,0 @@
import 'photography_value.dart';
class ShutterSpeedValue extends PhotographyStopValue<double> {
final bool isFraction;
const ShutterSpeedValue(super.rawValue, this.isFraction, super.stopType);
@override
double get value => isFraction ? 1 / rawValue : rawValue;
@override
String toString() {
final buffer = StringBuffer();
if (isFraction) buffer.write("1/");
if (rawValue - rawValue.floor() == 0) {
buffer.write(rawValue.toInt().toString());
} else {
buffer.write(rawValue.toStringAsFixed(1));
}
if (!isFraction) buffer.write("\"");
return buffer.toString();
}
}
const List<ShutterSpeedValue> shutterSpeedValues = [
ShutterSpeedValue(2000, true, StopType.full),
ShutterSpeedValue(1600, true, StopType.third),
ShutterSpeedValue(1500, true, StopType.half),
ShutterSpeedValue(1250, true, StopType.third),
ShutterSpeedValue(1000, true, StopType.full),
ShutterSpeedValue(800, true, StopType.third),
ShutterSpeedValue(750, true, StopType.half),
ShutterSpeedValue(640, true, StopType.third),
ShutterSpeedValue(500, true, StopType.full),
ShutterSpeedValue(400, true, StopType.third),
ShutterSpeedValue(350, true, StopType.half),
ShutterSpeedValue(320, true, StopType.third),
ShutterSpeedValue(250, true, StopType.full),
ShutterSpeedValue(200, true, StopType.third),
ShutterSpeedValue(180, true, StopType.half),
ShutterSpeedValue(160, true, StopType.third),
ShutterSpeedValue(125, true, StopType.full),
ShutterSpeedValue(100, true, StopType.third),
ShutterSpeedValue(90, true, StopType.half),
ShutterSpeedValue(80, true, StopType.third),
ShutterSpeedValue(60, true, StopType.full),
ShutterSpeedValue(50, true, StopType.third),
ShutterSpeedValue(45, true, StopType.half),
ShutterSpeedValue(40, true, StopType.third),
ShutterSpeedValue(30, true, StopType.full),
ShutterSpeedValue(25, true, StopType.third),
ShutterSpeedValue(20, true, StopType.half),
ShutterSpeedValue(20, true, StopType.third),
ShutterSpeedValue(15, true, StopType.full),
ShutterSpeedValue(13, true, StopType.third),
ShutterSpeedValue(10, true, StopType.half),
ShutterSpeedValue(10, true, StopType.third),
ShutterSpeedValue(8, true, StopType.full),
ShutterSpeedValue(6, true, StopType.third),
ShutterSpeedValue(6, true, StopType.half),
ShutterSpeedValue(5, true, StopType.third),
ShutterSpeedValue(4, true, StopType.full),
ShutterSpeedValue(3, true, StopType.third),
ShutterSpeedValue(3, true, StopType.half),
ShutterSpeedValue(2.5, true, StopType.third),
ShutterSpeedValue(2, true, StopType.full),
ShutterSpeedValue(1.6, true, StopType.third),
ShutterSpeedValue(1.5, true, StopType.half),
ShutterSpeedValue(1.3, true, StopType.third),
ShutterSpeedValue(1, false, StopType.full),
ShutterSpeedValue(1.3, false, StopType.third),
ShutterSpeedValue(1.5, false, StopType.half),
ShutterSpeedValue(1.6, false, StopType.third),
ShutterSpeedValue(2, false, StopType.full),
ShutterSpeedValue(2.5, false, StopType.third),
ShutterSpeedValue(3, false, StopType.half),
ShutterSpeedValue(3, false, StopType.third),
ShutterSpeedValue(4, false, StopType.full),
ShutterSpeedValue(5, false, StopType.third),
ShutterSpeedValue(6, false, StopType.half),
ShutterSpeedValue(6, false, StopType.third),
ShutterSpeedValue(8, false, StopType.full),
ShutterSpeedValue(10, false, StopType.third),
ShutterSpeedValue(12, false, StopType.half),
ShutterSpeedValue(13, false, StopType.third),
ShutterSpeedValue(16, false, StopType.full),
];

View file

@ -1,10 +1,9 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/supported_locale.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'models/ev_source_type.dart';
import 'models/photography_values/iso_value.dart';
import 'models/photography_values/nd_value.dart';
import 'models/theme_type.dart';
class UserPreferencesService {
@ -64,13 +63,16 @@ class UserPreferencesService {
}
}
IsoValue get iso => isoValues.firstWhere((v) => v.value == (_sharedPreferences.getInt(_isoKey) ?? 100));
IsoValue get iso =>
isoValues.firstWhere((v) => v.value == (_sharedPreferences.getInt(_isoKey) ?? 100));
set iso(IsoValue value) => _sharedPreferences.setInt(_isoKey, value.value);
NdValue get ndFilter => ndValues.firstWhere((v) => v.value == (_sharedPreferences.getInt(_ndFilterKey) ?? 0));
NdValue get ndFilter =>
ndValues.firstWhere((v) => v.value == (_sharedPreferences.getInt(_ndFilterKey) ?? 0));
set ndFilter(NdValue value) => _sharedPreferences.setInt(_ndFilterKey, value.value);
EvSourceType get evSourceType => EvSourceType.values[_sharedPreferences.getInt(_evSourceTypeKey) ?? 0];
EvSourceType get evSourceType =>
EvSourceType.values[_sharedPreferences.getInt(_evSourceTypeKey) ?? 0];
set evSourceType(EvSourceType value) => _sharedPreferences.setInt(_evSourceTypeKey, value.index);
bool get caffeine => _sharedPreferences.getBool(_caffeineKey) ?? false;
@ -86,10 +88,13 @@ class UserPreferencesService {
set locale(SupportedLocale value) => _sharedPreferences.setString(_localeKey, value.toString());
double get cameraEvCalibration => _sharedPreferences.getDouble(_cameraEvCalibrationKey) ?? 0.0;
set cameraEvCalibration(double value) => _sharedPreferences.setDouble(_cameraEvCalibrationKey, value);
set cameraEvCalibration(double value) =>
_sharedPreferences.setDouble(_cameraEvCalibrationKey, value);
double get lightSensorEvCalibration => _sharedPreferences.getDouble(_lightSensorEvCalibrationKey) ?? 0.0;
set lightSensorEvCalibration(double value) => _sharedPreferences.setDouble(_lightSensorEvCalibrationKey, value);
double get lightSensorEvCalibration =>
_sharedPreferences.getDouble(_lightSensorEvCalibrationKey) ?? 0.0;
set lightSensorEvCalibration(double value) =>
_sharedPreferences.setDouble(_lightSensorEvCalibrationKey, value);
ThemeType get themeType => ThemeType.values[_sharedPreferences.getInt(_themeTypeKey) ?? 0];
set themeType(ThemeType value) => _sharedPreferences.setInt(_themeTypeKey, value.index);
@ -99,4 +104,10 @@ class UserPreferencesService {
bool get dynamicColor => _sharedPreferences.getBool(_dynamicColorKey) ?? false;
set dynamicColor(bool value) => _sharedPreferences.setBool(_dynamicColorKey, value);
String get selectedEquipmentProfileId => '';
set selectedEquipmentProfileId(String id) {}
List<EquipmentProfileData> get equipmentProfiles => [];
set equipmentProfiles(List<EquipmentProfileData> profiles) {}
}

View file

@ -34,6 +34,20 @@
"calibrationMessageCameraOnly": "The accuracy of the readings measured by this application depends entirely on the rear camera of the device. Therefore, consider testing this application and setting up an EV calibration value that will give you the desired measurement results.",
"camera": "Camera",
"lightSensor": "Light sensor",
"equipment": "Equipment",
"equipmentProfileName": "Equipment profile name",
"equipmentProfileNameHint": "Praktica MTL5B",
"equipmentProfileAllValues": "All",
"apertureValues": "Aperture values",
"apertureValuesFilterDescription": "Select the range of aperture values to display. This is usually determined by the lens you are using.",
"ndFilters": "ND filters",
"ndFiltersFilterDescription": "Select the ND filters to display. These may be your most commonly used ND filters or the ones that fit your lens.",
"shutterSpeedValues": "Shutter speed values",
"shutterSpeedValuesFilterDescription": "Select the range of shutter speed values to display. This is usually determined by the camera body you are using.",
"isoValues": "ISO values",
"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",
"general": "General",
"keepScreenOn": "Keep screen on",
"haptics": "Haptics",

View file

@ -34,6 +34,20 @@
"calibrationMessageCameraOnly": "La précision des lectures mesurées par cette application dépend entièrement de la caméra arrière de l'appareil. Par conséquent, envisagez de tester cette application et de configurer une valeur d'étalonnage EV qui vous donnera les résultats de mesure souhaités.",
"camera": "Caméra",
"lightSensor": "Capteur de lumière",
"equipment": "Équipement",
"equipmentProfileName": "Nom du profil de l'équipement",
"equipmentProfileNameHint": "Praktica MTL5B",
"equipmentProfileAllValues": "Tout",
"apertureValues": "Valeurs Aperture",
"apertureValuesFilterDescription": "Sélectionnez la plage de valeurs d'ouverture à afficher. Cela est généralement déterminé par l'objectif que vous utilisez.",
"ndFilters": "Filtres ND",
"ndFiltersFilterDescription": "Sélectionnez les filtres ND à afficher. Ce sont peut-être vos filtres ND les plus couramment utilisés ou ceux qui correspondent à votre lentille.",
"shutterSpeedValues": "Valeurs de la vitesse d'obturation",
"shutterSpeedValuesFilterDescription": "Sélectionnez la plage de valeurs de vitesse d'obturation à afficher. Cela est généralement déterminé par le corps de l'appareil que vous utilisez.",
"isoValues": "Valeurs ISO",
"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",
"general": "Général",
"keepScreenOn": "Garder l'écran allumé",
"haptics": "Haptiques",

View file

@ -34,6 +34,20 @@
"calibrationMessageCameraOnly": "Точность измерений данного приложения полностью зависит от точности камеры вашего устройства. Поэтому рекомендуется самостоятельно подобрать калибровочное значение, которое даст желаемый результат измерений.",
"camera": "Камера",
"lightSensor": "Датчик освещённости",
"equipment": "Оборудование",
"equipmentProfileName": "Название профиля",
"equipmentProfileNameHint": "Praktica MTL5B",
"equipmentProfileAllValues": "Все",
"apertureValues": "Значения диафрагмы",
"apertureValuesFilterDescription": "Выберите диапазон значений диафрагмы для отображения. Обычно определяется объективом, который вы используете.",
"ndFilters": "ND фильтры",
"ndFiltersFilterDescription": "Выберите ND фильтры для отображения. Это могут быть наиболее часто используемые ND фильтры или фильтры, подходящие под ваш объектив.",
"shutterSpeedValues": "Значения выдержки",
"shutterSpeedValuesFilterDescription": "Выберите диапазон значений выдержки. Обычно ограничивается возможностями вашей камеры.",
"isoValues": "Значения ISO",
"isoValuesFilterDescription": "Выберите значения ISO для отображения. Это может быть наиболее часто используемые значения или значения, поддерживаемые вашей камерой.",
"equipmentProfile": "Оборудование",
"equipmentProfiles": "Профили оборудования",
"general": "Общие",
"keepScreenOn": "Запрет блокировки",
"haptics": "Вибрация",

View file

@ -1,9 +0,0 @@
import 'package:flutter/material.dart';
import 'application.dart';
import 'environment.dart';
void launchApp(Environment env) {
WidgetsFlutterBinding.ensureInitialized();
runApp(Application(env));
}

View file

@ -1,5 +1,9 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/environment.dart';
import 'launch_app.dart';
import 'application.dart';
void main() => launchApp(const Environment.dev());
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(const Application(Environment.dev()));
}

View file

@ -1,5 +1,12 @@
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:lightmeter/environment.dart';
import 'launch_app.dart';
import 'application.dart';
import 'firebase_options.dart';
void main() => launchApp(const Environment.prod());
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
runApp(const Application(Environment.prod()));
}

View file

@ -0,0 +1,138 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
import 'package:provider/provider.dart';
import 'package:uuid/uuid.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 EquipmentProfileData _defaultProfile = EquipmentProfileData(
id: '',
name: '',
apertureValues: apertureValues,
ndValues: ndValues,
shutterSpeedValues: shutterSpeedValues,
isoValues: isoValues,
);
List<EquipmentProfileData> _customProfiles = [];
String _selectedId = '';
EquipmentProfileData get _selectedProfile => _customProfiles.firstWhere(
(e) => e.id == _selectedId,
orElse: () {
context.read<UserPreferencesService>().selectedEquipmentProfileId = _defaultProfile.id;
return _defaultProfile;
},
);
@override
void initState() {
super.initState();
_selectedId = context.read<UserPreferencesService>().selectedEquipmentProfileId;
_customProfiles = context.read<UserPreferencesService>().equipmentProfiles;
}
@override
Widget build(BuildContext context) {
return EquipmentProfiles(
profiles: [_defaultProfile] + _customProfiles,
child: EquipmentProfile(
data: _selectedProfile,
child: widget.child,
),
);
}
void setProfile(EquipmentProfileData data) {
setState(() {
_selectedId = data.id;
});
context.read<UserPreferencesService>().selectedEquipmentProfileId = _selectedProfile.id;
}
/// Creates a default equipment profile
void addProfile(String name) {
_customProfiles.add(EquipmentProfileData(
id: const Uuid().v1(),
name: name,
apertureValues: apertureValues,
ndValues: ndValues,
shutterSpeedValues: shutterSpeedValues,
isoValues: isoValues,
));
_refreshSavedProfiles();
}
void updateProdile(EquipmentProfileData data) {
final indexToUpdate = _customProfiles.indexWhere((element) => element.id == data.id);
if (indexToUpdate >= 0) {
_customProfiles[indexToUpdate] = data;
_refreshSavedProfiles();
}
}
void deleteProfile(EquipmentProfileData data) {
_customProfiles.remove(data);
_refreshSavedProfiles();
}
void _refreshSavedProfiles() {
context.read<UserPreferencesService>().equipmentProfiles = _customProfiles;
setState(() {});
}
}
class EquipmentProfiles extends InheritedWidget {
final List<EquipmentProfileData> profiles;
const EquipmentProfiles({
required this.profiles,
required super.child,
super.key,
});
static List<EquipmentProfileData> of(BuildContext context, {bool listen = true}) {
if (listen) {
return context.dependOnInheritedWidgetOfExactType<EquipmentProfiles>()!.profiles;
} else {
return context.findAncestorWidgetOfExactType<EquipmentProfiles>()!.profiles;
}
}
@override
bool updateShouldNotify(EquipmentProfiles oldWidget) => true;
}
class EquipmentProfile extends InheritedWidget {
final EquipmentProfileData data;
const EquipmentProfile({
required this.data,
required super.child,
super.key,
});
static EquipmentProfileData of(BuildContext context, {bool listen = true}) {
if (listen) {
return context.dependOnInheritedWidgetOfExactType<EquipmentProfile>()!.data;
} else {
return context.findAncestorWidgetOfExactType<EquipmentProfile>()!.data;
}
}
@override
bool updateShouldNotify(EquipmentProfile oldWidget) => true;
}

View file

@ -26,9 +26,13 @@ class Dimens {
static const Duration durationML = Duration(milliseconds: 250);
static const Duration durationL = Duration(milliseconds: 300);
static const double enabledOpacity = 1.0;
static const double disabledOpacity = 0.38;
// TopBar
/// Probably this is a bad practice, but with text size locked, the height is always 212
static const double readingContainerHeight = 212;
static const double readingContainerSingleValueHeight = 76;
static const double readingContainerDefaultHeight = 212;
// `CenteredSlider`
static const double cameraSliderTrackHeight = grid4;
@ -44,8 +48,14 @@ class Dimens {
paddingL,
paddingM,
);
static const EdgeInsets dialogActionsPadding = EdgeInsets.fromLTRB(
static const EdgeInsets dialogIconTitlePadding = EdgeInsets.fromLTRB(
paddingL,
0,
paddingL,
paddingM,
);
static const EdgeInsets dialogActionsPadding = EdgeInsets.fromLTRB(
paddingM,
paddingM,
paddingL,
paddingL,

View file

@ -2,19 +2,14 @@ import 'dart:async';
import 'dart:math';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/data/models/photography_values/aperture_value.dart';
import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/data/models/photography_values/iso_value.dart';
import 'package:lightmeter/data/models/photography_values/nd_value.dart';
import 'package:lightmeter/data/models/photography_values/photography_value.dart';
import 'package:lightmeter/data/models/photography_values/shutter_speed_value.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/interactors/metering_interactor.dart';
import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart'
as communication_events;
import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart'
as communication_states;
import 'package:lightmeter/utils/log_2.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
import 'communication/bloc_communication_metering.dart';
import 'event_metering.dart';
@ -26,9 +21,12 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
final MeteringInteractor _meteringInteractor;
late final StreamSubscription<communication_states.ScreenState> _communicationSubscription;
List<ApertureValue> get _apertureValues => apertureValues.whereStopType(stopType);
List<ShutterSpeedValue> get _shutterSpeedValues => shutterSpeedValues.whereStopType(stopType);
List<ApertureValue> get _apertureValues =>
_equipmentProfileData.apertureValues.whereStopType(stopType);
List<ShutterSpeedValue> get _shutterSpeedValues =>
_equipmentProfileData.shutterSpeedValues.whereStopType(stopType);
EquipmentProfileData _equipmentProfileData;
StopType stopType;
late IsoValue _iso = _userPreferencesService.iso;
@ -40,6 +38,7 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
this._communicationBloc,
this._userPreferencesService,
this._meteringInteractor,
this._equipmentProfileData,
this.stopType,
) : super(
MeteringEndedState(
@ -54,6 +53,7 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
.map((state) => state as communication_states.ScreenState)
.listen(_onCommunicationState);
on<EquipmentProfileChangedEvent>(_onEquipmentProfileChanged);
on<StopTypeChangedEvent>(_onStopTypeChanged);
on<IsoChangedEvent>(_onIsoChanged);
on<NdChangedEvent>(_onNdChanged);
@ -79,6 +79,27 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
_emitMeasuredState(emit);
}
void _onEquipmentProfileChanged(EquipmentProfileChangedEvent event, Emitter emit) {
_equipmentProfileData = event.equipmentProfileData;
/// Update selected ISO value, if selected equipment profile
/// doesn't contain currently selected value
if (!event.equipmentProfileData.isoValues.any((v) => _iso.value == v.value)) {
_userPreferencesService.iso = event.equipmentProfileData.isoValues.first;
_ev = _ev + log2(event.equipmentProfileData.isoValues.first.value / _iso.value);
_iso = event.equipmentProfileData.isoValues.first;
}
/// The same for ND filter
if (!event.equipmentProfileData.ndValues.any((v) => _nd.value == v.value)) {
_userPreferencesService.ndFilter = event.equipmentProfileData.ndValues.first;
_ev = _ev - event.equipmentProfileData.ndValues.first.stopReduction + _nd.stopReduction;
_nd = event.equipmentProfileData.ndValues.first;
}
_emitMeasuredState(emit);
}
void _onIsoChanged(IsoChangedEvent event, Emitter emit) {
_userPreferencesService.iso = event.isoValue;
_ev = _ev + log2(event.isoValue.value / _iso.value);
@ -125,8 +146,26 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
List<ExposurePair> _buildExposureValues(double ev) {
/// Depending on the `stopType` the exposure pairs list length is multiplied by 1,2 or 3
final int evSteps = (ev * (stopType.index + 1)).round();
final int evOffset =
_shutterSpeedValues.indexOf(const ShutterSpeedValue(1, false, StopType.full)) - evSteps;
/// 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 = shutterSpeedValues.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.
}
}
final int evOffset = anchorIndex - evSteps;
late final int apertureOffset;
late final int shutterSpeedOffset;

View file

@ -1,10 +1,10 @@
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/photography_values/iso_value.dart';
import 'package:lightmeter/data/models/photography_values/nd_value.dart';
import 'package:lightmeter/interactors/metering_interactor.dart';
import 'package:lightmeter/providers/equipment_profile_provider.dart';
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
import 'bloc_container_camera.dart';
import 'widget_container_camera.dart';
@ -40,7 +40,9 @@ class CameraContainerProvider extends StatelessWidget {
child: CameraContainer(
fastest: fastest,
slowest: slowest,
isoValues: EquipmentProfile.of(context).isoValues,
iso: iso,
ndValues: EquipmentProfile.of(context).ndValues,
nd: nd,
onIsoChanged: onIsoChanged,
onNdChanged: onNdChanged,

View file

@ -1,15 +1,15 @@
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/photography_values/iso_value.dart';
import 'package:lightmeter/data/models/photography_values/nd_value.dart';
import 'package:lightmeter/platform_config.dart';
import 'package:lightmeter/providers/equipment_profile_provider.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/metering/components/camera_container/components/camera_view/widget_camera_view.dart';
import 'package:lightmeter/screens/metering/components/camera_container/models/camera_error_type.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';
import 'package:lightmeter/screens/metering/components/shared/readings_container/widget_container_readings.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
import 'bloc_container_camera.dart';
import 'components/camera_controls/widget_camera_controls.dart';
@ -21,7 +21,9 @@ import 'state_container_camera.dart';
class CameraContainer extends StatelessWidget {
final ExposurePair? fastest;
final ExposurePair? slowest;
final List<IsoValue> isoValues;
final IsoValue iso;
final List<NdValue> ndValues;
final NdValue nd;
final ValueChanged<IsoValue> onIsoChanged;
final ValueChanged<NdValue> onNdChanged;
@ -30,7 +32,9 @@ class CameraContainer extends StatelessWidget {
const CameraContainer({
required this.fastest,
required this.slowest,
required this.isoValues,
required this.iso,
required this.ndValues,
required this.nd,
required this.onIsoChanged,
required this.onNdChanged,
@ -40,16 +44,25 @@ class CameraContainer extends StatelessWidget {
@override
Widget build(BuildContext context) {
final topBarOverflow = Dimens.readingContainerHeight -
final double cameraViewHeight =
((MediaQuery.of(context).size.width - Dimens.grid8 - 2 * Dimens.paddingM) / 2) /
PlatformConfig.cameraPreviewAspectRatio;
double topBarOverflow = Dimens.readingContainerDefaultHeight - cameraViewHeight;
if (EquipmentProfiles.of(context).isNotEmpty) {
topBarOverflow += Dimens.readingContainerSingleValueHeight;
topBarOverflow += Dimens.paddingS;
}
return Column(
children: [
MeteringTopBar(
readingsContainer: ReadingsContainer(
fastest: fastest,
slowest: slowest,
isoValues: isoValues,
iso: iso,
ndValues: ndValues,
nd: nd,
onIsoChanged: onIsoChanged,
onNdChanged: onNdChanged,

View file

@ -1,10 +1,10 @@
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/photography_values/iso_value.dart';
import 'package:lightmeter/data/models/photography_values/nd_value.dart';
import 'package:lightmeter/interactors/metering_interactor.dart';
import 'package:lightmeter/providers/equipment_profile_provider.dart';
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
import 'bloc_container_light_sensor.dart';
import 'widget_container_light_sensor.dart';
@ -40,7 +40,9 @@ class LightSensorContainerProvider extends StatelessWidget {
child: LightSensorContainer(
fastest: fastest,
slowest: slowest,
isoValues: EquipmentProfile.of(context).isoValues,
iso: iso,
ndValues: EquipmentProfile.of(context).ndValues,
nd: nd,
onIsoChanged: onIsoChanged,
onNdChanged: onNdChanged,

View file

@ -1,16 +1,17 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/data/models/photography_values/iso_value.dart';
import 'package:lightmeter/data/models/photography_values/nd_value.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';
import 'package:lightmeter/screens/metering/components/shared/readings_container/widget_container_readings.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class LightSensorContainer extends StatelessWidget {
final ExposurePair? fastest;
final ExposurePair? slowest;
final List<IsoValue> isoValues;
final IsoValue iso;
final List<NdValue> ndValues;
final NdValue nd;
final ValueChanged<IsoValue> onIsoChanged;
final ValueChanged<NdValue> onNdChanged;
@ -19,7 +20,9 @@ class LightSensorContainer extends StatelessWidget {
const LightSensorContainer({
required this.fastest,
required this.slowest,
required this.isoValues,
required this.iso,
required this.ndValues,
required this.nd,
required this.onIsoChanged,
required this.onNdChanged,
@ -35,7 +38,9 @@ class LightSensorContainer extends StatelessWidget {
readingsContainer: ReadingsContainer(
fastest: fastest,
slowest: slowest,
isoValues: isoValues,
iso: iso,
ndValues: ndValues,
nd: nd,
onIsoChanged: onIsoChanged,
onNdChanged: onNdChanged,

View file

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/photography_values/photography_value.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class ExposurePairsListItem<T extends PhotographyStopValue> extends StatelessWidget {
final T value;

View file

@ -44,7 +44,7 @@ class MeteringTopBarShape extends CustomPainter {
bottomRight: circularRadius,
),
);
} else {
} else if (appendixHeight < 0) {
// Left side with bottom corner
path.lineTo(0, size.height + appendixHeight - Dimens.borderRadiusL);
path.arcToPoint(
@ -56,27 +56,16 @@ class MeteringTopBarShape extends CustomPainter {
// Bottom side with step
final allowedRadius = min(appendixHeight.abs() / 2, Dimens.borderRadiusL);
path.lineTo(appendixWidth - allowedRadius, size.height + appendixHeight);
final bool isCutout = appendixHeight < 0;
if (isCutout) {
path.arcToPoint(
Offset(appendixWidth, size.height + appendixHeight + allowedRadius),
radius: circularRadius,
clockwise: true,
);
path.lineTo(appendixWidth, size.height - allowedRadius);
} else {
path.arcToPoint(
Offset(appendixWidth, size.height + appendixHeight - allowedRadius),
radius: circularRadius,
clockwise: false,
);
path.lineTo(appendixWidth, size.height + allowedRadius);
}
path.arcToPoint(
Offset(appendixWidth, size.height + appendixHeight + allowedRadius),
radius: circularRadius,
clockwise: true,
);
path.lineTo(appendixWidth, size.height - allowedRadius);
path.arcToPoint(
Offset(appendixWidth + allowedRadius, size.height),
radius: circularRadius,
clockwise: !isCutout,
clockwise: false,
);
// Right side with bottom corner
@ -86,9 +75,42 @@ class MeteringTopBarShape extends CustomPainter {
radius: circularRadius,
clockwise: false,
);
path.lineTo(size.width, 0);
path.close();
} else {
// Left side with bottom corner
path.lineTo(0, size.height - Dimens.borderRadiusL);
path.arcToPoint(
Offset(Dimens.borderRadiusL, size.height),
radius: circularRadius,
clockwise: false,
);
// Bottom side with step
final allowedRadius = min(appendixHeight.abs() / 2, Dimens.borderRadiusL);
path.relativeLineTo(appendixWidth - allowedRadius * 2, 0);
path.relativeArcToPoint(
Offset(allowedRadius, -allowedRadius),
radius: Radius.circular(allowedRadius),
rotation: 90,
clockwise: false,
);
path.relativeLineTo(0, -appendixHeight + allowedRadius * 2);
path.relativeArcToPoint(
Offset(allowedRadius, -allowedRadius),
radius: Radius.circular(allowedRadius),
rotation: 90,
clockwise: true,
);
// Right side with bottom corner
path.lineTo(size.width - Dimens.borderRadiusL, size.height - appendixHeight);
path.arcToPoint(
Offset(size.width, size.height - appendixHeight - Dimens.borderRadiusL),
radius: circularRadius,
clockwise: false,
);
}
path.lineTo(size.width, 0);
path.close();
canvas.drawPath(path, paint);
}

View file

@ -1,40 +1,37 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/data/models/photography_values/photography_value.dart';
import 'package:lightmeter/res/dimens.dart';
typedef DialogPickerItemBuilder<T extends PhotographyValue> = Widget Function(BuildContext, T);
typedef DialogPickerEvDifferenceBuilder<T extends PhotographyValue> = String Function(
T selected, T other);
typedef DialogPickerItemTitleBuilder<T> = Widget Function(BuildContext context, T value);
typedef DialogPickerItemTrailingBuilder<T> = Widget? Function(T selected, T value);
class PhotographyValuePickerDialog<T extends PhotographyValue> extends StatefulWidget {
class DialogPicker<T> extends StatefulWidget {
final String title;
final String subtitle;
final String? subtitle;
final T initialValue;
final List<T> values;
final DialogPickerItemBuilder<T> itemTitleBuilder;
final DialogPickerEvDifferenceBuilder<T> evDifferenceBuilder;
final DialogPickerItemTitleBuilder<T> itemTitleBuilder;
final DialogPickerItemTrailingBuilder<T>? itemTrailingBuilder;
final VoidCallback onCancel;
final ValueChanged onSelect;
const PhotographyValuePickerDialog({
const DialogPicker({
required this.title,
required this.subtitle,
this.subtitle,
required this.initialValue,
required this.values,
required this.itemTitleBuilder,
required this.evDifferenceBuilder,
this.itemTrailingBuilder,
required this.onCancel,
required this.onSelect,
super.key,
});
@override
State<PhotographyValuePickerDialog<T>> createState() => _PhotographyValuePickerDialogState<T>();
State<DialogPicker<T>> createState() => _DialogPickerState<T>();
}
class _PhotographyValuePickerDialogState<T extends PhotographyValue>
extends State<PhotographyValuePickerDialog<T>> {
class _DialogPickerState<T> extends State<DialogPicker<T>> {
late T _selectedValue = widget.initialValue;
late final _scrollController =
ScrollController(initialScrollOffset: Dimens.grid56 * widget.values.indexOf(_selectedValue));
@ -59,12 +56,14 @@ class _PhotographyValuePickerDialogState<T extends PhotographyValue>
style: Theme.of(context).textTheme.headlineSmall!,
textAlign: TextAlign.center,
),
const SizedBox(height: Dimens.grid16),
Text(
widget.subtitle,
style: Theme.of(context).textTheme.bodyMedium!,
textAlign: TextAlign.center,
),
if (widget.subtitle != null) ...[
const SizedBox(height: Dimens.grid16),
Text(
widget.subtitle!,
style: Theme.of(context).textTheme.bodyMedium!,
textAlign: TextAlign.center,
),
]
],
),
),
@ -82,10 +81,7 @@ class _PhotographyValuePickerDialogState<T extends PhotographyValue>
style: Theme.of(context).textTheme.bodyLarge!,
child: widget.itemTitleBuilder(context, widget.values[index]),
),
secondary: widget.values[index].value != _selectedValue.value
? Text(S.of(context).evValue(
widget.evDifferenceBuilder.call(_selectedValue, widget.values[index])))
: null,
secondary: widget.itemTrailingBuilder?.call(_selectedValue, widget.values[index]),
onChanged: (value) {
if (value != null) {
setState(() {

View file

@ -1,27 +1,26 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/photography_values/photography_value.dart';
import 'components/animated_dialog/widget_dialog_animated.dart';
import 'components/photography_value_picker_dialog/widget_dialog_picker_photography_value.dart';
import 'components/dialog_picker/widget_picker_dialog.dart';
class AnimatedDialogPicker<T extends PhotographyValue> extends StatelessWidget {
class AnimatedDialogPicker<T> extends StatelessWidget {
final _key = GlobalKey<AnimatedDialogState>();
final String title;
final String subtitle;
final String? subtitle;
final T selectedValue;
final List<T> values;
final DialogPickerItemBuilder<T> itemTitleBuilder;
final DialogPickerEvDifferenceBuilder<T> evDifferenceBuilder;
final DialogPickerItemTitleBuilder<T> itemTitleBuilder;
final DialogPickerItemTrailingBuilder<T>? itemTrailingBuilder;
final ValueChanged<T> onChanged;
final Widget closedChild;
AnimatedDialogPicker({
required this.title,
required this.subtitle,
this.subtitle,
required this.selectedValue,
required this.values,
required this.itemTitleBuilder,
required this.evDifferenceBuilder,
this.itemTrailingBuilder,
required this.onChanged,
required this.closedChild,
super.key,
@ -32,13 +31,13 @@ class AnimatedDialogPicker<T extends PhotographyValue> extends StatelessWidget {
return AnimatedDialog(
key: _key,
closedChild: closedChild,
openedChild: PhotographyValuePickerDialog<T>(
openedChild: DialogPicker<T>(
title: title,
subtitle: subtitle,
initialValue: selectedValue,
values: values,
itemTitleBuilder: itemTitleBuilder,
evDifferenceBuilder: evDifferenceBuilder,
itemTrailingBuilder: itemTrailingBuilder,
onCancel: () {
_key.currentState?.close();
},

View file

@ -77,9 +77,9 @@ class _ReadingValueBuilder extends StatelessWidget {
reading.value,
style: textTheme.titleMedium?.copyWith(color: textColor),
maxLines: 1,
overflow: TextOverflow.visible,
overflow: TextOverflow.ellipsis,
softWrap: false,
),
)
],
);
}

View file

@ -1,18 +1,20 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/data/models/photography_values/iso_value.dart';
import 'package:lightmeter/data/models/photography_values/nd_value.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/providers/equipment_profile_provider.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
import 'components/animated_dialog_picker/widget_dialog_animated_picker.dart';
import 'components/animated_dialog_picker/widget_picker_dialog_animated.dart';
import 'components/reading_value_container/widget_container_reading_value.dart';
/// Contains a column of fastest & slowest exposure pairs + a row of ISO and ND pickers
class ReadingsContainer extends StatelessWidget {
final ExposurePair? fastest;
final ExposurePair? slowest;
final List<IsoValue> isoValues;
final IsoValue iso;
final List<NdValue> ndValues;
final NdValue nd;
final ValueChanged<IsoValue> onIsoChanged;
final ValueChanged<NdValue> onNdChanged;
@ -20,7 +22,9 @@ class ReadingsContainer extends StatelessWidget {
const ReadingsContainer({
required this.fastest,
required this.slowest,
required this.isoValues,
required this.iso,
required this.ndValues,
required this.nd,
required this.onIsoChanged,
required this.onNdChanged,
@ -32,6 +36,14 @@ class ReadingsContainer extends StatelessWidget {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (EquipmentProfiles.of(context).isNotEmpty) ...[
_EquipmentProfilePicker(
selectedValue: EquipmentProfile.of(context),
values: EquipmentProfiles.of(context),
onChanged: EquipmentProfileProvider.of(context).setProfile,
),
const _InnerPadding(),
],
ReadingValueContainer(
values: [
ReadingValue(
@ -48,20 +60,22 @@ class ReadingsContainer extends StatelessWidget {
Row(
children: [
Expanded(
child: _IsoValueTile(
value: iso,
child: _IsoValuePicker(
selectedValue: iso,
values: isoValues,
onChanged: onIsoChanged,
),
),
const _InnerPadding(),
Expanded(
child: _NdValueTile(
value: nd,
child: _NdValuePicker(
selectedValue: nd,
values: ndValues,
onChanged: onNdChanged,
),
),
],
)
),
],
);
}
@ -71,56 +85,99 @@ class _InnerPadding extends SizedBox {
const _InnerPadding() : super(height: Dimens.grid8, width: Dimens.grid8);
}
class _IsoValueTile extends StatelessWidget {
final IsoValue value;
class _EquipmentProfilePicker extends StatelessWidget {
final List<EquipmentProfileData> values;
final EquipmentProfileData selectedValue;
final ValueChanged<EquipmentProfileData> onChanged;
const _EquipmentProfilePicker({
required this.selectedValue,
required this.values,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return AnimatedDialogPicker<EquipmentProfileData>(
title: S.of(context).equipmentProfile,
selectedValue: selectedValue,
values: values,
itemTitleBuilder: (_, value) => Text(value.id.isEmpty ? S.of(context).none : value.name),
onChanged: onChanged,
closedChild: ReadingValueContainer.singleValue(
value: ReadingValue(
label: S.of(context).equipmentProfile,
value: selectedValue.id.isEmpty ? S.of(context).none : selectedValue.name,
),
),
);
}
}
class _IsoValuePicker extends StatelessWidget {
final List<IsoValue> values;
final IsoValue selectedValue;
final ValueChanged<IsoValue> onChanged;
const _IsoValueTile({required this.value, required this.onChanged});
const _IsoValuePicker({
required this.selectedValue,
required this.values,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return AnimatedDialogPicker<IsoValue>(
title: S.of(context).iso,
subtitle: S.of(context).filmSpeed,
selectedValue: value,
values: isoValues,
selectedValue: selectedValue,
values: values,
itemTitleBuilder: (_, value) => Text(value.value.toString()),
// using ascending order, because increase in film speed rises EV
evDifferenceBuilder: (selected, other) => selected.toStringDifference(other),
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: value.value.toString(),
value: selectedValue.value.toString(),
),
),
);
}
}
class _NdValueTile extends StatelessWidget {
final NdValue value;
class _NdValuePicker extends StatelessWidget {
final List<NdValue> values;
final NdValue selectedValue;
final ValueChanged<NdValue> onChanged;
const _NdValueTile({required this.value, required this.onChanged});
const _NdValuePicker({
required this.selectedValue,
required this.values,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return AnimatedDialogPicker<NdValue>(
title: S.of(context).nd,
subtitle: S.of(context).ndFilterFactor,
selectedValue: value,
values: ndValues,
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
evDifferenceBuilder: (selected, other) => other.toStringDifference(selected),
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: value.value.toString(),
value: selectedValue.value.toString(),
),
),
);

View file

@ -1,6 +1,4 @@
import 'package:lightmeter/data/models/photography_values/iso_value.dart';
import 'package:lightmeter/data/models/photography_values/nd_value.dart';
import 'package:lightmeter/data/models/photography_values/photography_value.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
abstract class MeteringEvent {
const MeteringEvent();
@ -12,6 +10,12 @@ class StopTypeChangedEvent extends MeteringEvent {
const StopTypeChangedEvent(this.stopType);
}
class EquipmentProfileChangedEvent extends MeteringEvent {
final EquipmentProfileData equipmentProfileData;
const EquipmentProfileChangedEvent(this.equipmentProfileData);
}
class IsoChangedEvent extends MeteringEvent {
final IsoValue isoValue;

View file

@ -3,10 +3,11 @@ import 'package:flutter_bloc/flutter_bloc.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/photography_values/photography_value.dart';
import 'package:lightmeter/data/permissions_service.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/interactors/metering_interactor.dart';
import 'package:lightmeter/providers/equipment_profile_provider.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
import 'package:provider/provider.dart';
import 'bloc_metering.dart';
@ -39,6 +40,7 @@ class _MeteringFlowState extends State<MeteringFlow> {
context.read<MeteringCommunicationBloc>(),
context.read<UserPreferencesService>(),
context.read<MeteringInteractor>(),
EquipmentProfile.of(context, listen: false),
context.read<StopType>(),
),
),

View file

@ -2,11 +2,10 @@ 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/photography_values/iso_value.dart';
import 'package:lightmeter/data/models/photography_values/nd_value.dart';
import 'package:lightmeter/data/models/photography_values/photography_value.dart';
import 'package:lightmeter/environment.dart';
import 'package:lightmeter/providers/equipment_profile_provider.dart';
import 'package:lightmeter/providers/ev_source_type_provider.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
import 'components/bottom_controls/provider_bottom_controls.dart';
import 'components/camera_container/provider_container_camera.dart';
@ -28,6 +27,7 @@ class _MeteringScreenState extends State<MeteringScreen> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
_bloc.add(EquipmentProfileChangedEvent(EquipmentProfile.of(context)));
_bloc.add(StopTypeChangedEvent(context.watch<StopType>()));
}

View file

@ -1,6 +1,5 @@
import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/data/models/photography_values/iso_value.dart';
import 'package:lightmeter/data/models/photography_values/nd_value.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
abstract class MeteringState {
const MeteringState();

View file

@ -0,0 +1,127 @@
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 {
final Icon icon;
final String title;
final String description;
final List<T> values;
final List<T> selectedValues;
final String Function(BuildContext context, T value) titleAdapter;
const DialogFilter({
required this.icon,
required this.title,
required this.description,
required this.values,
required this.selectedValues,
required this.titleAdapter,
super.key,
});
@override
State<DialogFilter<T>> createState() => _DialogFilterState<T>();
}
class _DialogFilterState<T extends PhotographyValue> 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),
growable: false,
);
bool get _hasAnySelected => checkboxValues.contains(true);
bool get _hasAnyUnselected => checkboxValues.contains(false);
@override
Widget build(BuildContext context) {
return AlertDialog(
icon: widget.icon,
titlePadding: Dimens.dialogIconTitlePadding,
title: Text(widget.title),
contentPadding: EdgeInsets.zero,
content: Column(
children: [
Padding(
padding: Dimens.dialogIconTitlePadding,
child: Text(widget.description),
),
const Divider(),
Expanded(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: List.generate(
widget.values.length,
(index) => CheckboxListTile(
value: checkboxValues[index],
controlAffinity: ListTileControlAffinity.leading,
title: Text(
widget.titleAdapter(context, widget.values[index]),
style: Theme.of(context).textTheme.bodyLarge!,
),
onChanged: (value) {
if (value != null) {
setState(() {
checkboxValues[index] = value;
});
}
},
),
),
),
),
),
const Divider(),
Padding(
padding: Dimens.dialogActionsPadding,
child: Row(
children: [
SizedBox(
width: 40,
child: IconButton(
padding: EdgeInsets.zero,
icon: Icon(_hasAnyUnselected ? Icons.select_all : Icons.deselect),
onPressed: _toggleAll,
),
),
const Spacer(),
TextButton(
onPressed: Navigator.of(context).pop,
child: Text(S.of(context).cancel),
),
TextButton(
onPressed: _hasAnySelected
? () {
List<T> selectedValues = [];
for (int i = 0; i < widget.values.length; i++) {
if (checkboxValues[i]) {
selectedValues.add(widget.values[i]);
}
}
Navigator.of(context).pop(selectedValues);
}
: null,
child: Text(S.of(context).save),
),
],
),
)
],
),
);
}
void _toggleAll() {
setState(() {
if (_hasAnyUnselected) {
checkboxValues.fillRange(0, checkboxValues.length, true);
} else {
checkboxValues.fillRange(0, checkboxValues.length, false);
}
});
}
}

View file

@ -0,0 +1,95 @@
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 DialogRangePicker<T extends PhotographyValue> extends StatefulWidget {
final Icon icon;
final String title;
final String description;
final List<T> values;
final List<T> selectedValues;
final String Function(BuildContext context, T value) titleAdapter;
const DialogRangePicker({
required this.icon,
required this.title,
required this.description,
required this.values,
required this.selectedValues,
required this.titleAdapter,
super.key,
});
@override
State<DialogRangePicker<T>> createState() => _DialogRangePickerState<T>();
}
class _DialogRangePickerState<T extends PhotographyValue> extends State<DialogRangePicker<T>> {
late int _start = widget.values.indexWhere((e) => e.value == widget.selectedValues.first.value);
late int _end = widget.values.indexWhere((e) => e.value == widget.selectedValues.last.value);
@override
Widget build(BuildContext context) {
return AlertDialog(
icon: widget.icon,
titlePadding: Dimens.dialogIconTitlePadding,
title: Text(widget.title),
contentPadding: EdgeInsets.zero,
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: Dimens.dialogIconTitlePadding,
child: Text(widget.description),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingL),
child: DefaultTextStyle(
style: Theme.of(context).textTheme.bodyLarge!,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(widget.values[_start].toString()),
Text(widget.values[_end].toString()),
],
),
),
),
Row(
children: [
Expanded(
child: RangeSlider(
values: RangeValues(
_start.toDouble(),
_end.toDouble(),
),
min: 0,
max: widget.values.length.toDouble() - 1,
divisions: widget.values.length - 1,
onChanged: (value) {
setState(() {
_start = value.start.toInt();
_end = value.end.toInt();
});
},
),
),
],
),
],
),
actionsPadding: Dimens.dialogActionsPadding,
actions: [
TextButton(
onPressed: Navigator.of(context).pop,
child: Text(S.of(context).cancel),
),
TextButton(
onPressed: () => Navigator.of(context).pop(widget.values.sublist(_start, _end + 1)),
child: Text(S.of(context).save),
),
],
);
}
}

View file

@ -0,0 +1,143 @@
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_range_picker/widget_dialog_picker_range.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.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';
class EquipmentListTiles extends StatelessWidget {
final List<ApertureValue> selectedApertureValues;
final List<IsoValue> selectedIsoValues;
final List<NdValue> selectedNdValues;
final List<ShutterSpeedValue> selectedShutterSpeedValues;
final ValueChanged<List<ApertureValue>> onApertureValuesSelected;
final ValueChanged<List<IsoValue>> onIsoValuesSelecred;
final ValueChanged<List<NdValue>> onNdValuesSelected;
final ValueChanged<List<ShutterSpeedValue>> onShutterSpeedValuesSelected;
const EquipmentListTiles({
required this.selectedApertureValues,
required this.selectedIsoValues,
required this.selectedNdValues,
required this.selectedShutterSpeedValues,
required this.onApertureValuesSelected,
required this.onIsoValuesSelecred,
required this.onNdValuesSelected,
required this.onShutterSpeedValuesSelected,
super.key,
});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
_EquipmentListTile<IsoValue>(
icon: Icons.iso,
title: S.of(context).isoValues,
description: S.of(context).isoValuesFilterDescription,
values: isoValues,
valuesCount: selectedIsoValues.length == isoValues.length
? S.of(context).equipmentProfileAllValues
: selectedIsoValues.length.toString(),
selectedValues: selectedIsoValues,
rangeSelect: false,
onChanged: onIsoValuesSelecred,
),
_EquipmentListTile<NdValue>(
icon: Icons.filter_b_and_w,
title: S.of(context).ndFilters,
description: S.of(context).ndFiltersFilterDescription,
values: ndValues,
valuesCount: selectedNdValues.length == ndValues.length
? S.of(context).equipmentProfileAllValues
: selectedNdValues.length.toString(),
selectedValues: selectedNdValues,
rangeSelect: false,
onChanged: onNdValuesSelected,
),
_EquipmentListTile<ApertureValue>(
icon: Icons.camera,
title: S.of(context).apertureValues,
description: S.of(context).apertureValuesFilterDescription,
values: apertureValues,
valuesCount: selectedApertureValues.length == apertureValues.length
? S.of(context).equipmentProfileAllValues
: selectedApertureValues.length.toString(),
selectedValues: selectedApertureValues,
rangeSelect: true,
onChanged: onApertureValuesSelected,
),
_EquipmentListTile<ShutterSpeedValue>(
icon: Icons.shutter_speed,
title: S.of(context).shutterSpeedValues,
description: S.of(context).shutterSpeedValuesFilterDescription,
values: shutterSpeedValues,
valuesCount: selectedShutterSpeedValues.length == shutterSpeedValues.length
? S.of(context).equipmentProfileAllValues
: selectedShutterSpeedValues.length.toString(),
selectedValues: selectedShutterSpeedValues,
rangeSelect: true,
onChanged: onShutterSpeedValuesSelected,
),
],
);
}
}
class _EquipmentListTile<T extends PhotographyValue> extends StatelessWidget {
final IconData icon;
final String title;
final String valuesCount;
final String description;
final List<T> selectedValues;
final List<T> values;
final ValueChanged<List<T>> onChanged;
final bool rangeSelect;
const _EquipmentListTile({
required this.icon,
required this.title,
required this.valuesCount,
required this.description,
required this.selectedValues,
required this.values,
required this.onChanged,
required this.rangeSelect,
super.key,
});
@override
Widget build(BuildContext context) {
return ListTile(
leading: Icon(icon),
title: Text(title),
trailing: Text(valuesCount),
onTap: () {
showDialog<List<T>>(
context: context,
builder: (_) => rangeSelect
? DialogRangePicker<T>(
icon: Icon(icon),
title: title,
description: description,
values: values,
selectedValues: selectedValues,
titleAdapter: (_, value) => value.toString(),
)
: DialogFilter<T>(
icon: Icon(icon),
title: title,
description: description,
values: values,
selectedValues: selectedValues,
titleAdapter: (_, value) => value.toString(),
),
).then((values) {
if (values != null) {
onChanged(values);
}
});
},
);
}
}

View file

@ -0,0 +1,240 @@
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_name_dialog/widget_dialog_equipment_profile_name.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
import 'components/equipment_list_tiles/widget_list_tiles_equipments.dart';
class EquipmentProfileContainer extends StatefulWidget {
final EquipmentProfileData data;
final ValueChanged<EquipmentProfileData> onUpdate;
final VoidCallback onDelete;
final VoidCallback onExpand;
const EquipmentProfileContainer({
required this.data,
required this.onUpdate,
required this.onDelete,
required this.onExpand,
super.key,
});
@override
State<EquipmentProfileContainer> createState() => EquipmentProfileContainerState();
}
class EquipmentProfileContainerState extends State<EquipmentProfileContainer>
with TickerProviderStateMixin {
late EquipmentProfileData _equipmentData = EquipmentProfileData(
id: widget.data.id,
name: widget.data.name,
apertureValues: widget.data.apertureValues,
ndValues: widget.data.ndValues,
shutterSpeedValues: widget.data.shutterSpeedValues,
isoValues: widget.data.isoValues,
);
late final AnimationController _controller = AnimationController(
duration: Dimens.durationM,
vsync: this,
);
bool get _expanded => _controller.isCompleted;
@override
void didUpdateWidget(EquipmentProfileContainer oldWidget) {
super.didUpdateWidget(oldWidget);
_equipmentData = EquipmentProfileData(
id: widget.data.id,
name: widget.data.name,
apertureValues: widget.data.apertureValues,
ndValues: widget.data.ndValues,
shutterSpeedValues: widget.data.shutterSpeedValues,
isoValues: widget.data.isoValues,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: Dimens.paddingM),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
_AnimatedNameLeading(controller: _controller),
const SizedBox(width: Dimens.grid8),
Flexible(
child: Text(
_equipmentData.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
trailing: Row(
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
_AnimatedArrowButton(
controller: _controller,
onPressed: () => _expanded ? collapse() : expand(),
),
IconButton(
onPressed: widget.onDelete,
icon: const Icon(Icons.delete),
),
],
),
onTap: () => _expanded ? _showNameDialog() : expand(),
),
_AnimatedEquipmentListTiles(
controller: _controller,
equipmentData: _equipmentData,
onApertureValuesSelected: (value) {
_equipmentData = _equipmentData.copyWith(apertureValues: value);
widget.onUpdate(_equipmentData);
},
onIsoValuesSelecred: (value) {
_equipmentData = _equipmentData.copyWith(isoValues: value);
widget.onUpdate(_equipmentData);
},
onNdValuesSelected: (value) {
_equipmentData = _equipmentData.copyWith(ndValues: value);
widget.onUpdate(_equipmentData);
},
onShutterSpeedValuesSelected: (value) {
_equipmentData = _equipmentData.copyWith(shutterSpeedValues: value);
widget.onUpdate(_equipmentData);
},
),
],
),
),
);
}
void _showNameDialog() {
showDialog<String>(
context: context,
builder: (_) => EquipmentProfileNameDialog(initialValue: _equipmentData.name),
).then((value) {
if (value != null) {
_equipmentData = _equipmentData.copyWith(name: value);
widget.onUpdate(_equipmentData);
}
});
}
void expand() {
widget.onExpand();
_controller.forward();
SchedulerBinding.instance.addPostFrameCallback((_) {
Scrollable.ensureVisible(
context,
alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd,
);
});
}
void collapse() {
_controller.reverse();
}
}
class _AnimatedNameLeading extends AnimatedWidget {
const _AnimatedNameLeading({required AnimationController controller})
: super(listenable: controller);
Animation<double> get _progress => listenable as Animation<double>;
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(right: _progress.value * Dimens.grid24),
child: Icon(
Icons.edit,
size: _progress.value * Dimens.grid24,
),
);
}
}
class _AnimatedArrowButton extends AnimatedWidget {
final VoidCallback onPressed;
const _AnimatedArrowButton({
required AnimationController controller,
required this.onPressed,
}) : super(listenable: controller);
Animation<double> get _progress => listenable as Animation<double>;
@override
Widget build(BuildContext context) {
return IconButton(
onPressed: onPressed,
icon: Transform.rotate(
angle: _progress.value * pi,
child: const Icon(Icons.keyboard_arrow_down),
),
);
}
}
class _AnimatedEquipmentListTiles extends AnimatedWidget {
final EquipmentProfileData equipmentData;
final ValueChanged<List<ApertureValue>> onApertureValuesSelected;
final ValueChanged<List<IsoValue>> onIsoValuesSelecred;
final ValueChanged<List<NdValue>> onNdValuesSelected;
final ValueChanged<List<ShutterSpeedValue>> onShutterSpeedValuesSelected;
const _AnimatedEquipmentListTiles({
required AnimationController controller,
required this.equipmentData,
required this.onApertureValuesSelected,
required this.onIsoValuesSelecred,
required this.onNdValuesSelected,
required this.onShutterSpeedValuesSelected,
}) : super(listenable: controller);
Animation<double> get _progress => listenable as Animation<double>;
@override
Widget build(BuildContext context) {
return SizedOverflowBox(
alignment: Alignment.topCenter,
size: Size(
double.maxFinite,
_progress.value * Dimens.grid56 * 4,
),
child: Opacity(
opacity: _progress.value,
child: EquipmentListTiles(
selectedApertureValues: equipmentData.apertureValues,
selectedIsoValues: equipmentData.isoValues,
selectedNdValues: equipmentData.ndValues,
selectedShutterSpeedValues: equipmentData.shutterSpeedValues,
onApertureValuesSelected: onApertureValuesSelected,
onIsoValuesSelecred: onIsoValuesSelecred,
onNdValuesSelected: onNdValuesSelected,
onShutterSpeedValuesSelected: onShutterSpeedValuesSelected,
),
),
);
}
}

View file

@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
class EquipmentProfileNameDialog extends StatefulWidget {
final String initialValue;
const EquipmentProfileNameDialog({this.initialValue = '', super.key});
@override
State<EquipmentProfileNameDialog> createState() => _EquipmentProfileNameDialogState();
}
class _EquipmentProfileNameDialogState extends State<EquipmentProfileNameDialog> {
late final _nameController = TextEditingController(text: widget.initialValue);
@override
void dispose() {
_nameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(S.of(context).equipmentProfileName),
content: TextField(
autofocus: true,
controller: _nameController,
decoration: InputDecoration(hintText: S.of(context).equipmentProfileNameHint),
),
actions: [
TextButton(
onPressed: Navigator.of(context).pop,
child: Text(S.of(context).cancel),
),
ValueListenableBuilder(
valueListenable: _nameController,
builder: (_, value, __) => TextButton(
onPressed: value.text.isNotEmpty ? () => Navigator.of(context).pop(value.text) : null,
child: Text(S.of(context).save),
),
),
],
);
}
}

View file

@ -0,0 +1,103 @@
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/shared/sliver_screen/screen_sliver.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
import 'components/equipment_profile_container/widget_container_equipment_profile.dart';
import 'components/equipment_profile_name_dialog/widget_dialog_equipment_profile_name.dart';
class EquipmentProfilesScreen extends StatefulWidget {
const EquipmentProfilesScreen({super.key});
@override
State<EquipmentProfilesScreen> createState() => _EquipmentProfilesScreenState();
}
class _EquipmentProfilesScreenState extends State<EquipmentProfilesScreen> {
static const maxProfiles = 5 + 1; // replace with a constant from iap
late List<GlobalKey<EquipmentProfileContainerState>> profileContainersKeys = [];
int get profilesCount => EquipmentProfiles.of(context).length;
@override
void initState() {
super.initState();
profileContainersKeys = EquipmentProfiles.of(context, listen: false)
.map((e) => GlobalKey<EquipmentProfileContainerState>(debugLabel: e.id))
.toList();
}
@override
Widget build(BuildContext context) {
return SliverScreen(
title: S.of(context).equipmentProfiles,
appBarActions: [
if (profilesCount < maxProfiles)
IconButton(
onPressed: _addProfile,
icon: const Icon(Icons.add),
),
IconButton(
onPressed: Navigator.of(context).pop,
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: profileContainersKeys.length,
),
),
],
);
}
void _addProfile() {
showDialog<String>(
context: context,
builder: (_) => const EquipmentProfileNameDialog(),
).then((value) {
if (value != null) {
EquipmentProfileProvider.of(context).addProfile(value);
profileContainersKeys.add(GlobalKey<EquipmentProfileContainerState>());
}
});
}
void _updateProfileAt(EquipmentProfileData data, int index) {
EquipmentProfileProvider.of(context).updateProdile(data);
}
void _removeProfileAt(int index) {
EquipmentProfileProvider.of(context).deleteProfile(EquipmentProfiles.of(context)[index]);
profileContainersKeys.removeAt(index);
}
void _keepExpandedAt(int index) {
profileContainersKeys.getRange(0, index).forEach((element) {
element.currentState?.collapse();
});
profileContainersKeys.getRange(index + 1, profilesCount).forEach((element) {
element.currentState?.collapse();
});
}
}

View file

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

View file

@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/photography_values/photography_value.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/screens/settings/components/shared/dialog_picker.dart/widget_dialog_picker.dart';
import 'package:lightmeter/utils/stop_type_provider.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
import 'package:provider/provider.dart';
class StopTypeListTile extends StatelessWidget {

View file

@ -3,6 +3,7 @@ import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/screens/settings/components/shared/settings_section/widget_settings_section.dart';
import 'components/calibration/widget_list_tile_calibration.dart';
import 'components/equipment_profiles/widget_list_tile_equipment_profiles.dart';
import 'components/fractional_stops/widget_list_tile_fractional_stops.dart';
class MeteringSettingsSection extends StatelessWidget {
@ -15,6 +16,7 @@ class MeteringSettingsSection extends StatelessWidget {
children: const [
StopTypeListTile(),
CalibrationListTile(),
EquipmentProfilesListTile(),
],
);
}

View file

@ -28,7 +28,7 @@ class SettingsSection extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(left: Dimens.paddingM),
padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
child: Text(
title,
style: Theme.of(context)

View file

@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/data/models/dynamic_colors_state.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/providers/theme_provider.dart';
import 'package:lightmeter/res/dimens.dart';
import 'components/primary_color_picker_dialog/widget_dialog_picker_primary_color.dart';
@ -13,7 +14,7 @@ class PrimaryColorListTile extends StatelessWidget {
Widget build(BuildContext context) {
if (context.watch<DynamicColorState>() == DynamicColorState.enabled) {
return Opacity(
opacity: 0.5,
opacity: Dimens.disabledOpacity,
child: IgnorePointer(
child: ListTile(
leading: const Icon(Icons.palette),

View file

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/shared/sliver_screen/screen_sliver.dart';
import 'components/about/widget_settings_section_about.dart';
import 'components/general/widget_settings_section_general.dart';
@ -12,48 +12,27 @@ class SettingsScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
top: false,
bottom: false,
child: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
pinned: true,
automaticallyImplyLeading: false,
expandedHeight: Dimens.grid168,
flexibleSpace: FlexibleSpaceBar(
centerTitle: false,
titlePadding: const EdgeInsets.all(Dimens.paddingM),
title: Text(
S.of(context).settings,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
fontSize: 24,
),
),
),
actions: [
IconButton(
onPressed: Navigator.of(context).pop,
icon: const Icon(Icons.close),
),
],
),
SliverList(
delegate: SliverChildListDelegate(
<Widget>[
const MeteringSettingsSection(),
const GeneralSettingsSection(),
const ThemeSettingsSection(),
const AboutSettingsSection(),
SizedBox(height: MediaQuery.of(context).padding.bottom),
],
),
),
],
return SliverScreen(
title: S.of(context).settings,
appBarActions: [
IconButton(
onPressed: Navigator.of(context).pop,
icon: const Icon(Icons.close),
),
),
],
slivers: [
SliverList(
delegate: SliverChildListDelegate(
<Widget>[
const MeteringSettingsSection(),
const GeneralSettingsSection(),
const ThemeSettingsSection(),
const AboutSettingsSection(),
SizedBox(height: MediaQuery.of(context).padding.bottom),
],
),
),
],
);
}
}

View file

@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/res/dimens.dart';
class SliverScreen extends StatelessWidget {
final String title;
final List<Widget> appBarActions;
final List<Widget> slivers;
const SliverScreen({
required this.title,
required this.appBarActions,
required this.slivers,
super.key,
});
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
top: false,
bottom: false,
child: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
pinned: true,
automaticallyImplyLeading: false,
expandedHeight: Dimens.grid168,
flexibleSpace: FlexibleSpaceBar(
centerTitle: false,
titlePadding: const EdgeInsets.all(Dimens.paddingM),
title: Text(
title,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
fontSize: Dimens.grid24,
),
),
),
actions: appBarActions,
),
...slivers,
SliverToBoxAdapter(child: SizedBox(height: MediaQuery.of(context).padding.bottom)),
],
),
),
);
}
}

View file

@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/photography_values/photography_value.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
import 'package:provider/provider.dart';
class StopTypeProvider extends StatefulWidget {

View file

@ -11,6 +11,7 @@ dependencies:
camera: 0.10.0+4
exif: 3.1.2
dynamic_color: 1.5.4
firebase_core: 2.7.0
flutter:
sdk: flutter
flutter_bloc: 8.1.1
@ -20,11 +21,16 @@ dependencies:
intl_utils: 2.8.1
light_sensor: 2.0.2
material_color_utilities: 0.2.0
m3_lightmeter_resources:
git:
url: "https://github.com/vodemn/m3_lightmeter_resources"
ref: main
package_info_plus: 3.0.2
permission_handler: 10.2.0
provider: 6.0.4
shared_preferences: 2.0.15
url_launcher: 6.1.8
uuid: 3.0.7
vibration: 1.7.6
dev_dependencies:

View file

@ -1,41 +0,0 @@
import 'package:lightmeter/data/models/photography_values/aperture_value.dart';
import 'package:lightmeter/data/models/photography_values/iso_value.dart';
import 'package:lightmeter/data/models/photography_values/photography_value.dart';
import 'package:lightmeter/data/models/photography_values/shutter_speed_value.dart';
import 'package:test/test.dart';
void main() {
// Stringify
test('Stringify aperture values', () {
expect(apertureValues.first.toString(), "f/1.0");
expect(apertureValues.last.toString(), "f/45");
});
test('Stringify iso values', () {
expect(isoValues.first.toString(), "3");
expect(isoValues.last.toString(), "6400");
});
test('Stringify shutter speed values', () {
expect(shutterSpeedValues.first.toString(), "1/2000");
expect(shutterSpeedValues.last.toString(), "16\"");
});
// Stops
test('Aperture values stops lists', () {
expect(apertureValues.fullStops().length, 12);
expect(apertureValues.halfStops().length, 12 + 11);
expect(apertureValues.thirdStops().length, 12 + 22);
});
test('Iso values stops lists', () {
expect(isoValues.fullStops().length, 12);
expect(isoValues.thirdStops().length, 12 + 22);
});
test('Shutter speed values stops lists', () {
expect(shutterSpeedValues.fullStops().length, 16);
expect(shutterSpeedValues.halfStops().length, 16 + 15);
expect(shutterSpeedValues.thirdStops().length, 16 + 30);
});
}