From 6bf059ed4d278f93abc2eda2178a5d476c06f0cb Mon Sep 17 00:00:00 2001 From: Vadim <44135514+vodemn@users.noreply.github.com> Date: Thu, 30 Mar 2023 22:24:18 +0300 Subject: [PATCH] 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 a9b692b60ea5b2e88188a5d497467708becb4a02. * 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 --- .github/workflows/cd_dev.yml | 18 ++ .github/workflows/cd_prod.yml | 18 ++ .github/workflows/ci.yml | 23 +- .gitignore | 8 +- .vscode/launch.json | 1 + README.md | 4 +- analysis_options.yaml | 2 + android/app/build.gradle | 2 + android/build.gradle | 1 + ios/Runner.xcodeproj/project.pbxproj | 4 + lib/application.dart | 47 ++-- lib/data/models/exposure_pair.dart | 3 +- .../photography_values/aperture_value.dart | 64 ----- .../models/photography_values/iso_value.dart | 45 ---- .../models/photography_values/nd_value.dart | 34 --- .../photography_values/photography_value.dart | 51 ---- .../shutter_speed_value.dart | 87 ------- lib/data/shared_prefs_service.dart | 27 +- lib/l10n/intl_en.arb | 14 + lib/l10n/intl_fr.arb | 14 + lib/l10n/intl_ru.arb | 14 + lib/launch_app.dart | 9 - lib/main_dev.dart | 8 +- lib/main_prod.dart | 11 +- lib/providers/equipment_profile_provider.dart | 138 ++++++++++ lib/res/dimens.dart | 14 +- lib/screens/metering/bloc_metering.dart | 59 ++++- .../provider_container_camera.dart | 6 +- .../widget_container_camera.dart | 19 +- .../provider_container_light_sensor.dart | 6 +- .../widget_container_light_sensor.dart | 9 +- .../widget_item_list_exposure_pairs.dart | 2 +- .../shape_top_bar_metering.dart | 64 +++-- .../widget_picker_dialog.dart} | 44 ++-- ...art => widget_picker_dialog_animated.dart} | 19 +- .../widget_container_reading_value.dart | 4 +- .../widget_container_readings.dart | 101 ++++++-- lib/screens/metering/event_metering.dart | 10 +- lib/screens/metering/flow_metering.dart | 4 +- lib/screens/metering/screen_metering.dart | 6 +- lib/screens/metering/state_metering.dart | 3 +- .../dialog_filter/widget_dialog_filter.dart | 127 +++++++++ .../widget_dialog_picker_range.dart | 95 +++++++ .../widget_list_tiles_equipments.dart | 143 +++++++++++ .../widget_container_equipment_profile.dart | 240 ++++++++++++++++++ .../widget_dialog_equipment_profile_name.dart | 46 ++++ .../screen_equipment_profile.dart | 103 ++++++++ .../widget_list_tile_equipment_profiles.dart | 21 ++ .../widget_list_tile_fractional_stops.dart | 2 +- .../widget_settings_section_metering.dart | 2 + .../widget_settings_section.dart | 2 +- .../widget_list_tile_primary_color.dart | 3 +- lib/screens/settings/screen_settings.dart | 63 ++--- .../shared/sliver_screen/screen_sliver.dart | 48 ++++ lib/utils/stop_type_provider.dart | 2 +- pubspec.yaml | 6 + test/photograpy_values_test.dart | 41 --- 57 files changed, 1427 insertions(+), 534 deletions(-) delete mode 100644 lib/data/models/photography_values/aperture_value.dart delete mode 100644 lib/data/models/photography_values/iso_value.dart delete mode 100644 lib/data/models/photography_values/nd_value.dart delete mode 100644 lib/data/models/photography_values/photography_value.dart delete mode 100644 lib/data/models/photography_values/shutter_speed_value.dart delete mode 100644 lib/launch_app.dart create mode 100644 lib/providers/equipment_profile_provider.dart rename lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/components/{photography_value_picker_dialog/widget_dialog_picker_photography_value.dart => dialog_picker/widget_picker_dialog.dart} (67%) rename lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/{widget_dialog_animated_picker.dart => widget_picker_dialog_animated.dart} (63%) create mode 100644 lib/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 create mode 100644 lib/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 create mode 100644 lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/widget_list_tiles_equipments.dart create mode 100644 lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart create mode 100644 lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_name_dialog/widget_dialog_equipment_profile_name.dart create mode 100644 lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart create mode 100644 lib/screens/settings/components/metering/components/equipment_profiles/widget_list_tile_equipment_profiles.dart create mode 100644 lib/screens/shared/sliver_screen/screen_sliver.dart delete mode 100644 test/photograpy_values_test.dart diff --git a/.github/workflows/cd_dev.yml b/.github/workflows/cd_dev.yml index 95ee345..76a7045 100644 --- a/.github/workflows/cd_dev.yml +++ b/.github/workflows/cd_dev.yml @@ -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: diff --git a/.github/workflows/cd_prod.yml b/.github/workflows/cd_prod.yml index a8b5031..34dd32f 100644 --- a/.github/workflows/cd_prod.yml +++ b/.github/workflows/cd_prod.yml @@ -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: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 023ef9c..40703d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.gitignore b/.gitignore index 4fbcff0..301183f 100644 --- a/.gitignore +++ b/.gitignore @@ -51,4 +51,10 @@ app.*.map.json pubspec.lock /ios/Podfile.lock -.fvm/ \ No newline at end of file +.fvm/ +.jks +keystore.properties +android/app/google-services.json +ios/firebase_app_id_file.json +ios/Runner/GoogleService-Info.plist +lib/firebase_options.dart diff --git a/.vscode/launch.json b/.vscode/launch.json index a9059b5..db52a43 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -21,6 +21,7 @@ "name": "dev (ios)", "request": "launch", "type": "dart", + //"flutterMode": "release", "args": [ "--flavor", "dev", diff --git a/README.md b/README.md index 210adb9..89ed428 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/analysis_options.yaml b/analysis_options.yaml index f9b3034..fb80863 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1 +1,3 @@ include: package:flutter_lints/flutter.yaml +analyzer: + exclude: [lib/main_prod.dart] \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index 9f8259d..5f24621 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -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" } diff --git a/android/build.gradle b/android/build.gradle index 83ae220..bb34f99 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -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" } } diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 589478f..a858962 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -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 = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 2913B1E428DD3F7921E898BC /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; @@ -86,6 +88,7 @@ 97C146EF1CF9000F007C117D /* Products */, CFB3DEB969CB62463CE0ACDF /* Pods */, A5DCEB322972D36722A63973 /* Frameworks */, + 2913B1E428DD3F7921E898BC /* GoogleService-Info.plist */, ); sourceTree = ""; }; @@ -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; }; diff --git a/lib/application.dart b/lib/application.dart index f7ca38e..ed596a5 100644 --- a/lib/application.dart +++ b/lib/application.dart @@ -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(), - locale: Locale(context.watch().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(), + locale: Locale(context.watch().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(), - }, ), ), ), diff --git a/lib/data/models/exposure_pair.dart b/lib/data/models/exposure_pair.dart index bff3112..d1907a0 100644 --- a/lib/data/models/exposure_pair.dart +++ b/lib/data/models/exposure_pair.dart @@ -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; diff --git a/lib/data/models/photography_values/aperture_value.dart b/lib/data/models/photography_values/aperture_value.dart deleted file mode 100644 index d790001..0000000 --- a/lib/data/models/photography_values/aperture_value.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'photography_value.dart'; - -class ApertureValue extends PhotographyStopValue { - 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 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), -]; diff --git a/lib/data/models/photography_values/iso_value.dart b/lib/data/models/photography_values/iso_value.dart deleted file mode 100644 index 536cda6..0000000 --- a/lib/data/models/photography_values/iso_value.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'photography_value.dart'; - -class IsoValue extends PhotographyStopValue { - const IsoValue(super.rawValue, super.stopType); - - @override - String toString() => value.toString(); -} - -const List 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), -]; diff --git a/lib/data/models/photography_values/nd_value.dart b/lib/data/models/photography_values/nd_value.dart deleted file mode 100644 index 6c19f7c..0000000 --- a/lib/data/models/photography_values/nd_value.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:lightmeter/utils/log_2.dart'; - -import 'photography_value.dart'; - -class NdValue extends PhotographyValue { - 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 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), -]; diff --git a/lib/data/models/photography_values/photography_value.dart b/lib/data/models/photography_values/photography_value.dart deleted file mode 100644 index 45da5f9..0000000 --- a/lib/data/models/photography_values/photography_value.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'dart:math'; - -import 'package:lightmeter/utils/log_2.dart'; - -enum StopType { full, half, third } - -abstract class PhotographyValue { - 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 extends PhotographyValue { - final StopType stopType; - - const PhotographyStopValue(super.rawValue, this.stopType); -} - -extension PhotographyStopValues on List { - List 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 fullStops() => whereStopType(StopType.full); - - List halfStops() => whereStopType(StopType.half); - - List thirdStops() => whereStopType(StopType.third); -} diff --git a/lib/data/models/photography_values/shutter_speed_value.dart b/lib/data/models/photography_values/shutter_speed_value.dart deleted file mode 100644 index 9e18c9b..0000000 --- a/lib/data/models/photography_values/shutter_speed_value.dart +++ /dev/null @@ -1,87 +0,0 @@ -import 'photography_value.dart'; - -class ShutterSpeedValue extends PhotographyStopValue { - 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 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), -]; diff --git a/lib/data/shared_prefs_service.dart b/lib/data/shared_prefs_service.dart index 6c76e76..ab8f115 100644 --- a/lib/data/shared_prefs_service.dart +++ b/lib/data/shared_prefs_service.dart @@ -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 get equipmentProfiles => []; + set equipmentProfiles(List profiles) {} } diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index aded824..5a6d06e 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -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", diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index 2dd9b2e..7e19d56 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -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", diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index 4d792d3..f6d201f 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -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": "Вибрация", diff --git a/lib/launch_app.dart b/lib/launch_app.dart deleted file mode 100644 index f26dcca..0000000 --- a/lib/launch_app.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'application.dart'; -import 'environment.dart'; - -void launchApp(Environment env) { - WidgetsFlutterBinding.ensureInitialized(); - runApp(Application(env)); -} diff --git a/lib/main_dev.dart b/lib/main_dev.dart index fb3d8e3..e74e87b 100644 --- a/lib/main_dev.dart +++ b/lib/main_dev.dart @@ -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 main() async { + WidgetsFlutterBinding.ensureInitialized(); + runApp(const Application(Environment.dev())); +} diff --git a/lib/main_prod.dart b/lib/main_prod.dart index a1839f6..aefaa2f 100644 --- a/lib/main_prod.dart +++ b/lib/main_prod.dart @@ -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 main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + runApp(const Application(Environment.prod())); +} diff --git a/lib/providers/equipment_profile_provider.dart b/lib/providers/equipment_profile_provider.dart new file mode 100644 index 0000000..df38277 --- /dev/null +++ b/lib/providers/equipment_profile_provider.dart @@ -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()!; + } + + @override + State createState() => EquipmentProfileProviderState(); +} + +class EquipmentProfileProviderState extends State { + static const EquipmentProfileData _defaultProfile = EquipmentProfileData( + id: '', + name: '', + apertureValues: apertureValues, + ndValues: ndValues, + shutterSpeedValues: shutterSpeedValues, + isoValues: isoValues, + ); + + List _customProfiles = []; + String _selectedId = ''; + + EquipmentProfileData get _selectedProfile => _customProfiles.firstWhere( + (e) => e.id == _selectedId, + orElse: () { + context.read().selectedEquipmentProfileId = _defaultProfile.id; + return _defaultProfile; + }, + ); + + @override + void initState() { + super.initState(); + _selectedId = context.read().selectedEquipmentProfileId; + _customProfiles = context.read().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().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().equipmentProfiles = _customProfiles; + setState(() {}); + } +} + +class EquipmentProfiles extends InheritedWidget { + final List profiles; + + const EquipmentProfiles({ + required this.profiles, + required super.child, + super.key, + }); + + static List of(BuildContext context, {bool listen = true}) { + if (listen) { + return context.dependOnInheritedWidgetOfExactType()!.profiles; + } else { + return context.findAncestorWidgetOfExactType()!.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()!.data; + } else { + return context.findAncestorWidgetOfExactType()!.data; + } + } + + @override + bool updateShouldNotify(EquipmentProfile oldWidget) => true; +} diff --git a/lib/res/dimens.dart b/lib/res/dimens.dart index 09c05a3..ce931e2 100644 --- a/lib/res/dimens.dart +++ b/lib/res/dimens.dart @@ -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, diff --git a/lib/screens/metering/bloc_metering.dart b/lib/screens/metering/bloc_metering.dart index 9f122cc..332bace 100644 --- a/lib/screens/metering/bloc_metering.dart +++ b/lib/screens/metering/bloc_metering.dart @@ -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 { final MeteringInteractor _meteringInteractor; late final StreamSubscription _communicationSubscription; - List get _apertureValues => apertureValues.whereStopType(stopType); - List get _shutterSpeedValues => shutterSpeedValues.whereStopType(stopType); + List get _apertureValues => + _equipmentProfileData.apertureValues.whereStopType(stopType); + List get _shutterSpeedValues => + _equipmentProfileData.shutterSpeedValues.whereStopType(stopType); + EquipmentProfileData _equipmentProfileData; StopType stopType; late IsoValue _iso = _userPreferencesService.iso; @@ -40,6 +38,7 @@ class MeteringBloc extends Bloc { this._communicationBloc, this._userPreferencesService, this._meteringInteractor, + this._equipmentProfileData, this.stopType, ) : super( MeteringEndedState( @@ -54,6 +53,7 @@ class MeteringBloc extends Bloc { .map((state) => state as communication_states.ScreenState) .listen(_onCommunicationState); + on(_onEquipmentProfileChanged); on(_onStopTypeChanged); on(_onIsoChanged); on(_onNdChanged); @@ -79,6 +79,27 @@ class MeteringBloc extends Bloc { _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 { List _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; diff --git a/lib/screens/metering/components/camera_container/provider_container_camera.dart b/lib/screens/metering/components/camera_container/provider_container_camera.dart index 729322e..82957e1 100644 --- a/lib/screens/metering/components/camera_container/provider_container_camera.dart +++ b/lib/screens/metering/components/camera_container/provider_container_camera.dart @@ -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, diff --git a/lib/screens/metering/components/camera_container/widget_container_camera.dart b/lib/screens/metering/components/camera_container/widget_container_camera.dart index 34906aa..8aacb4e 100644 --- a/lib/screens/metering/components/camera_container/widget_container_camera.dart +++ b/lib/screens/metering/components/camera_container/widget_container_camera.dart @@ -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 isoValues; final IsoValue iso; + final List ndValues; final NdValue nd; final ValueChanged onIsoChanged; final ValueChanged 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, diff --git a/lib/screens/metering/components/light_sensor_container/provider_container_light_sensor.dart b/lib/screens/metering/components/light_sensor_container/provider_container_light_sensor.dart index 5d75647..bab0c7b 100644 --- a/lib/screens/metering/components/light_sensor_container/provider_container_light_sensor.dart +++ b/lib/screens/metering/components/light_sensor_container/provider_container_light_sensor.dart @@ -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, diff --git a/lib/screens/metering/components/light_sensor_container/widget_container_light_sensor.dart b/lib/screens/metering/components/light_sensor_container/widget_container_light_sensor.dart index e8019d0..34018ac 100644 --- a/lib/screens/metering/components/light_sensor_container/widget_container_light_sensor.dart +++ b/lib/screens/metering/components/light_sensor_container/widget_container_light_sensor.dart @@ -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 isoValues; final IsoValue iso; + final List ndValues; final NdValue nd; final ValueChanged onIsoChanged; final ValueChanged 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, diff --git a/lib/screens/metering/components/shared/exposure_pairs_list/components/exposure_pairs_list_item/widget_item_list_exposure_pairs.dart b/lib/screens/metering/components/shared/exposure_pairs_list/components/exposure_pairs_list_item/widget_item_list_exposure_pairs.dart index 1b0fc02..9514fbe 100644 --- a/lib/screens/metering/components/shared/exposure_pairs_list/components/exposure_pairs_list_item/widget_item_list_exposure_pairs.dart +++ b/lib/screens/metering/components/shared/exposure_pairs_list/components/exposure_pairs_list_item/widget_item_list_exposure_pairs.dart @@ -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 extends StatelessWidget { final T value; diff --git a/lib/screens/metering/components/shared/metering_top_bar/shape_top_bar_metering.dart b/lib/screens/metering/components/shared/metering_top_bar/shape_top_bar_metering.dart index 90d6718..28a9226 100644 --- a/lib/screens/metering/components/shared/metering_top_bar/shape_top_bar_metering.dart +++ b/lib/screens/metering/components/shared/metering_top_bar/shape_top_bar_metering.dart @@ -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); } diff --git a/lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/components/photography_value_picker_dialog/widget_dialog_picker_photography_value.dart b/lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/components/dialog_picker/widget_picker_dialog.dart similarity index 67% rename from lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/components/photography_value_picker_dialog/widget_dialog_picker_photography_value.dart rename to lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/components/dialog_picker/widget_picker_dialog.dart index acf9663..62ba265 100644 --- a/lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/components/photography_value_picker_dialog/widget_dialog_picker_photography_value.dart +++ b/lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/components/dialog_picker/widget_picker_dialog.dart @@ -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 = Widget Function(BuildContext, T); -typedef DialogPickerEvDifferenceBuilder = String Function( - T selected, T other); +typedef DialogPickerItemTitleBuilder = Widget Function(BuildContext context, T value); +typedef DialogPickerItemTrailingBuilder = Widget? Function(T selected, T value); -class PhotographyValuePickerDialog extends StatefulWidget { +class DialogPicker extends StatefulWidget { final String title; - final String subtitle; + final String? subtitle; final T initialValue; final List values; - final DialogPickerItemBuilder itemTitleBuilder; - final DialogPickerEvDifferenceBuilder evDifferenceBuilder; + final DialogPickerItemTitleBuilder itemTitleBuilder; + final DialogPickerItemTrailingBuilder? 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> createState() => _PhotographyValuePickerDialogState(); + State> createState() => _DialogPickerState(); } -class _PhotographyValuePickerDialogState - extends State> { +class _DialogPickerState extends State> { late T _selectedValue = widget.initialValue; late final _scrollController = ScrollController(initialScrollOffset: Dimens.grid56 * widget.values.indexOf(_selectedValue)); @@ -59,12 +56,14 @@ class _PhotographyValuePickerDialogState 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 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(() { diff --git a/lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/widget_dialog_animated_picker.dart b/lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/widget_picker_dialog_animated.dart similarity index 63% rename from lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/widget_dialog_animated_picker.dart rename to lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/widget_picker_dialog_animated.dart index 0d729a7..0bbdabc 100644 --- a/lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/widget_dialog_animated_picker.dart +++ b/lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/widget_picker_dialog_animated.dart @@ -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 extends StatelessWidget { +class AnimatedDialogPicker extends StatelessWidget { final _key = GlobalKey(); final String title; - final String subtitle; + final String? subtitle; final T selectedValue; final List values; - final DialogPickerItemBuilder itemTitleBuilder; - final DialogPickerEvDifferenceBuilder evDifferenceBuilder; + final DialogPickerItemTitleBuilder itemTitleBuilder; + final DialogPickerItemTrailingBuilder? itemTrailingBuilder; final ValueChanged 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 extends StatelessWidget { return AnimatedDialog( key: _key, closedChild: closedChild, - openedChild: PhotographyValuePickerDialog( + openedChild: DialogPicker( title: title, subtitle: subtitle, initialValue: selectedValue, values: values, itemTitleBuilder: itemTitleBuilder, - evDifferenceBuilder: evDifferenceBuilder, + itemTrailingBuilder: itemTrailingBuilder, onCancel: () { _key.currentState?.close(); }, diff --git a/lib/screens/metering/components/shared/readings_container/components/reading_value_container/widget_container_reading_value.dart b/lib/screens/metering/components/shared/readings_container/components/reading_value_container/widget_container_reading_value.dart index d1c55c7..25b1217 100644 --- a/lib/screens/metering/components/shared/readings_container/components/reading_value_container/widget_container_reading_value.dart +++ b/lib/screens/metering/components/shared/readings_container/components/reading_value_container/widget_container_reading_value.dart @@ -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, - ), + ) ], ); } diff --git a/lib/screens/metering/components/shared/readings_container/widget_container_readings.dart b/lib/screens/metering/components/shared/readings_container/widget_container_readings.dart index 9815034..88b86f9 100644 --- a/lib/screens/metering/components/shared/readings_container/widget_container_readings.dart +++ b/lib/screens/metering/components/shared/readings_container/widget_container_readings.dart @@ -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 isoValues; final IsoValue iso; + final List ndValues; final NdValue nd; final ValueChanged onIsoChanged; final ValueChanged 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 values; + final EquipmentProfileData selectedValue; + final ValueChanged onChanged; + + const _EquipmentProfilePicker({ + required this.selectedValue, + required this.values, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + return AnimatedDialogPicker( + 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 values; + final IsoValue selectedValue; final ValueChanged 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( 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 values; + final NdValue selectedValue; final ValueChanged 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( 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(), ), ), ); diff --git a/lib/screens/metering/event_metering.dart b/lib/screens/metering/event_metering.dart index f1fd5c4..95166dd 100644 --- a/lib/screens/metering/event_metering.dart +++ b/lib/screens/metering/event_metering.dart @@ -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; diff --git a/lib/screens/metering/flow_metering.dart b/lib/screens/metering/flow_metering.dart index 7dbb05c..c376c7d 100644 --- a/lib/screens/metering/flow_metering.dart +++ b/lib/screens/metering/flow_metering.dart @@ -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 { context.read(), context.read(), context.read(), + EquipmentProfile.of(context, listen: false), context.read(), ), ), diff --git a/lib/screens/metering/screen_metering.dart b/lib/screens/metering/screen_metering.dart index 8abf2fc..5b8d8e1 100644 --- a/lib/screens/metering/screen_metering.dart +++ b/lib/screens/metering/screen_metering.dart @@ -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 { @override void didChangeDependencies() { super.didChangeDependencies(); + _bloc.add(EquipmentProfileChangedEvent(EquipmentProfile.of(context))); _bloc.add(StopTypeChangedEvent(context.watch())); } diff --git a/lib/screens/metering/state_metering.dart b/lib/screens/metering/state_metering.dart index cde6b13..7a103b8 100644 --- a/lib/screens/metering/state_metering.dart +++ b/lib/screens/metering/state_metering.dart @@ -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(); diff --git a/lib/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 b/lib/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 new file mode 100644 index 0000000..48b81d2 --- /dev/null +++ b/lib/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 @@ -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 extends StatefulWidget { + final Icon icon; + final String title; + final String description; + final List values; + final List 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> createState() => _DialogFilterState(); +} + +class _DialogFilterState extends State> { + late final List 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 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); + } + }); + } +} diff --git a/lib/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 b/lib/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 new file mode 100644 index 0000000..a1c8bc1 --- /dev/null +++ b/lib/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 @@ -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 extends StatefulWidget { + final Icon icon; + final String title; + final String description; + final List values; + final List 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> createState() => _DialogRangePickerState(); +} + +class _DialogRangePickerState extends State> { + 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), + ), + ], + ); + } +} diff --git a/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/widget_list_tiles_equipments.dart b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/widget_list_tiles_equipments.dart new file mode 100644 index 0000000..38f25e8 --- /dev/null +++ b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/widget_list_tiles_equipments.dart @@ -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 selectedApertureValues; + final List selectedIsoValues; + final List selectedNdValues; + final List selectedShutterSpeedValues; + final ValueChanged> onApertureValuesSelected; + final ValueChanged> onIsoValuesSelecred; + final ValueChanged> onNdValuesSelected; + final ValueChanged> 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( + 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( + 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( + 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( + 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 extends StatelessWidget { + final IconData icon; + final String title; + final String valuesCount; + final String description; + final List selectedValues; + final List values; + final ValueChanged> 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>( + context: context, + builder: (_) => rangeSelect + ? DialogRangePicker( + icon: Icon(icon), + title: title, + description: description, + values: values, + selectedValues: selectedValues, + titleAdapter: (_, value) => value.toString(), + ) + : DialogFilter( + icon: Icon(icon), + title: title, + description: description, + values: values, + selectedValues: selectedValues, + titleAdapter: (_, value) => value.toString(), + ), + ).then((values) { + if (values != null) { + onChanged(values); + } + }); + }, + ); + } +} diff --git a/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart new file mode 100644 index 0000000..5e546d5 --- /dev/null +++ b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart @@ -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 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 createState() => EquipmentProfileContainerState(); +} + +class EquipmentProfileContainerState extends State + 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( + 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 get _progress => listenable as Animation; + + @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 get _progress => listenable as Animation; + + @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> onApertureValuesSelected; + final ValueChanged> onIsoValuesSelecred; + final ValueChanged> onNdValuesSelected; + final ValueChanged> 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 get _progress => listenable as Animation; + + @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, + ), + ), + ); + } +} diff --git a/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_name_dialog/widget_dialog_equipment_profile_name.dart b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_name_dialog/widget_dialog_equipment_profile_name.dart new file mode 100644 index 0000000..d202b94 --- /dev/null +++ b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_name_dialog/widget_dialog_equipment_profile_name.dart @@ -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 createState() => _EquipmentProfileNameDialogState(); +} + +class _EquipmentProfileNameDialogState extends State { + 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), + ), + ), + ], + ); + } +} diff --git a/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart new file mode 100644 index 0000000..af15f4a --- /dev/null +++ b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart @@ -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 createState() => _EquipmentProfilesScreenState(); +} + +class _EquipmentProfilesScreenState extends State { + static const maxProfiles = 5 + 1; // replace with a constant from iap + + late List> profileContainersKeys = []; + int get profilesCount => EquipmentProfiles.of(context).length; + + @override + void initState() { + super.initState(); + profileContainersKeys = EquipmentProfiles.of(context, listen: false) + .map((e) => GlobalKey(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( + context: context, + builder: (_) => const EquipmentProfileNameDialog(), + ).then((value) { + if (value != null) { + EquipmentProfileProvider.of(context).addProfile(value); + profileContainersKeys.add(GlobalKey()); + } + }); + } + + 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(); + }); + } +} diff --git a/lib/screens/settings/components/metering/components/equipment_profiles/widget_list_tile_equipment_profiles.dart b/lib/screens/settings/components/metering/components/equipment_profiles/widget_list_tile_equipment_profiles.dart new file mode 100644 index 0000000..c8fa3a8 --- /dev/null +++ b/lib/screens/settings/components/metering/components/equipment_profiles/widget_list_tile_equipment_profiles.dart @@ -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( + MaterialPageRoute(builder: (_) => const EquipmentProfilesScreen())); + }, + ); + } +} diff --git a/lib/screens/settings/components/metering/components/fractional_stops/widget_list_tile_fractional_stops.dart b/lib/screens/settings/components/metering/components/fractional_stops/widget_list_tile_fractional_stops.dart index b02149a..d164430 100644 --- a/lib/screens/settings/components/metering/components/fractional_stops/widget_list_tile_fractional_stops.dart +++ b/lib/screens/settings/components/metering/components/fractional_stops/widget_list_tile_fractional_stops.dart @@ -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 { diff --git a/lib/screens/settings/components/metering/widget_settings_section_metering.dart b/lib/screens/settings/components/metering/widget_settings_section_metering.dart index d313597..db30e84 100644 --- a/lib/screens/settings/components/metering/widget_settings_section_metering.dart +++ b/lib/screens/settings/components/metering/widget_settings_section_metering.dart @@ -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(), ], ); } diff --git a/lib/screens/settings/components/shared/settings_section/widget_settings_section.dart b/lib/screens/settings/components/shared/settings_section/widget_settings_section.dart index 3bce4d8..be62847 100644 --- a/lib/screens/settings/components/shared/settings_section/widget_settings_section.dart +++ b/lib/screens/settings/components/shared/settings_section/widget_settings_section.dart @@ -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) diff --git a/lib/screens/settings/components/theme/components/primary_color/widget_list_tile_primary_color.dart b/lib/screens/settings/components/theme/components/primary_color/widget_list_tile_primary_color.dart index 2ed527b..760e567 100644 --- a/lib/screens/settings/components/theme/components/primary_color/widget_list_tile_primary_color.dart +++ b/lib/screens/settings/components/theme/components/primary_color/widget_list_tile_primary_color.dart @@ -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.enabled) { return Opacity( - opacity: 0.5, + opacity: Dimens.disabledOpacity, child: IgnorePointer( child: ListTile( leading: const Icon(Icons.palette), diff --git a/lib/screens/settings/screen_settings.dart b/lib/screens/settings/screen_settings.dart index 6f225ff..81a569a 100644 --- a/lib/screens/settings/screen_settings.dart +++ b/lib/screens/settings/screen_settings.dart @@ -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: [ - 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( - [ - 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( + [ + const MeteringSettingsSection(), + const GeneralSettingsSection(), + const ThemeSettingsSection(), + const AboutSettingsSection(), + SizedBox(height: MediaQuery.of(context).padding.bottom), + ], + ), + ), + ], ); } } diff --git a/lib/screens/shared/sliver_screen/screen_sliver.dart b/lib/screens/shared/sliver_screen/screen_sliver.dart new file mode 100644 index 0000000..d4542b1 --- /dev/null +++ b/lib/screens/shared/sliver_screen/screen_sliver.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/res/dimens.dart'; + +class SliverScreen extends StatelessWidget { + final String title; + final List appBarActions; + final List 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: [ + 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)), + ], + ), + ), + ); + } +} diff --git a/lib/utils/stop_type_provider.dart b/lib/utils/stop_type_provider.dart index caff28e..a6bbe1a 100644 --- a/lib/utils/stop_type_provider.dart +++ b/lib/utils/stop_type_provider.dart @@ -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 { diff --git a/pubspec.yaml b/pubspec.yaml index e137275..394d7d6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: diff --git a/test/photograpy_values_test.dart b/test/photograpy_values_test.dart deleted file mode 100644 index 106754b..0000000 --- a/test/photograpy_values_test.dart +++ /dev/null @@ -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); - }); -}