mirror of
https://github.com/vodemn/m3_lightmeter.git
synced 2024-11-21 15:00:40 +00:00
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:
parent
6ffd164171
commit
6bf059ed4d
57 changed files with 1427 additions and 534 deletions
18
.github/workflows/cd_dev.yml
vendored
18
.github/workflows/cd_dev.yml
vendored
|
@ -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:
|
||||
|
|
18
.github/workflows/cd_prod.yml
vendored
18
.github/workflows/cd_prod.yml
vendored
|
@ -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:
|
||||
|
|
23
.github/workflows/ci.yml
vendored
23
.github/workflows/ci.yml
vendored
|
@ -7,21 +7,31 @@ 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"
|
||||
|
||||
|
||||
- name: Check flutter version
|
||||
run: flutter --version
|
||||
|
||||
|
@ -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
|
||||
|
|
8
.gitignore
vendored
8
.gitignore
vendored
|
@ -51,4 +51,10 @@ app.*.map.json
|
|||
pubspec.lock
|
||||
/ios/Podfile.lock
|
||||
|
||||
.fvm/
|
||||
.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
1
.vscode/launch.json
vendored
|
@ -21,6 +21,7 @@
|
|||
"name": "dev (ios)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
//"flutterMode": "release",
|
||||
"args": [
|
||||
"--flavor",
|
||||
"dev",
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
include: package:flutter_lints/flutter.yaml
|
||||
analyzer:
|
||||
exclude: [lib/main_prod.dart]
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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),
|
||||
];
|
|
@ -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),
|
||||
];
|
|
@ -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),
|
||||
];
|
|
@ -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);
|
||||
}
|
|
@ -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),
|
||||
];
|
|
@ -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) {}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "Вибрация",
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'application.dart';
|
||||
import 'environment.dart';
|
||||
|
||||
void launchApp(Environment env) {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
runApp(Application(env));
|
||||
}
|
|
@ -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()));
|
||||
}
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
|
|
138
lib/providers/equipment_profile_provider.dart
Normal file
138
lib/providers/equipment_profile_provider.dart
Normal 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;
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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(() {
|
|
@ -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();
|
||||
},
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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>(),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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>()));
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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()));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
48
lib/screens/shared/sliver_screen/screen_sliver.dart
Normal file
48
lib/screens/shared/sliver_screen/screen_sliver.dart
Normal 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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue