diff --git a/android/app/src/main/kotlin/com/vodemn/lightmeter/MainActivity.kt b/android/app/src/main/kotlin/com/vodemn/lightmeter/MainActivity.kt index af458c4..e120f88 100644 --- a/android/app/src/main/kotlin/com/vodemn/lightmeter/MainActivity.kt +++ b/android/app/src/main/kotlin/com/vodemn/lightmeter/MainActivity.kt @@ -2,20 +2,17 @@ package com.vodemn.lightmeter import android.os.Bundle import android.view.KeyEvent -import android.view.WindowManager import androidx.core.view.WindowCompat +import com.vodemn.lightmeter.PlatformChannels.CaffeinePlatformChannel +import com.vodemn.lightmeter.PlatformChannels.CameraInfoPlatformChannel +import com.vodemn.lightmeter.PlatformChannels.VolumePlatformChannel import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine -import io.flutter.plugin.common.EventChannel -import io.flutter.plugin.common.EventChannel.EventSink -import io.flutter.plugin.common.MethodChannel class MainActivity : FlutterActivity() { - private lateinit var keepScreenOnChannel: MethodChannel - private lateinit var volumeHandlingChannel: MethodChannel - private lateinit var volumeEventChannel: EventChannel - private var volumeEventsEmitter: EventSink? = null - private var handleVolume = false + private val caffeinePlatformChannel = CaffeinePlatformChannel() + private val cameraInfoPlatformChannel = CameraInfoPlatformChannel() + private val volumePlatformChannel = VolumePlatformChannel() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -24,72 +21,24 @@ class MainActivity : FlutterActivity() { override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) - keepScreenOnChannel = MethodChannel( - flutterEngine.dartExecutor.binaryMessenger, - "com.vodemn.lightmeter/keepScreenOn" - ) - keepScreenOnChannel.setMethodCallHandler { call, result -> - when (call.method) { - "isKeepScreenOn" -> result.success((window.attributes.flags and WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) != 0) - "setKeepScreenOn" -> { - if (call.arguments !is Boolean) { - result.error("invalid args", "Argument should be of type Bool for 'setKeepScreenOn' call", null) - } else { - if (call.arguments as Boolean) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - else window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - result.success(true) - } - } - else -> result.notImplemented() - } - } - - volumeHandlingChannel = MethodChannel( - flutterEngine.dartExecutor.binaryMessenger, - "com.vodemn.lightmeter/volumeHandling" - ) - volumeHandlingChannel.setMethodCallHandler { call, result -> - when (call.method) { - "setVolumeHandling" -> { - handleVolume = call.arguments as Boolean - result.success(handleVolume) - } - else -> result.notImplemented() - } - } - - volumeEventChannel = EventChannel( - flutterEngine.dartExecutor.binaryMessenger, - "com.vodemn.lightmeter/volumeEvents" - ) - volumeEventChannel.setStreamHandler(object : EventChannel.StreamHandler { - override fun onListen(listener: Any?, eventSink: EventSink) { - volumeEventsEmitter = eventSink - } - override fun onCancel(listener: Any?) { - volumeEventsEmitter = null - } - }) + val binaryMessenger = flutterEngine.dartExecutor.binaryMessenger + caffeinePlatformChannel.onAttachedToEngine(binaryMessenger, window) + cameraInfoPlatformChannel.onAttachedToEngine(binaryMessenger, context) + volumePlatformChannel.onAttachedToEngine(binaryMessenger) } override fun onDestroy() { - keepScreenOnChannel.setMethodCallHandler(null) - volumeHandlingChannel.setMethodCallHandler(null) - volumeEventChannel.setStreamHandler(null) + caffeinePlatformChannel.onDestroy() + cameraInfoPlatformChannel.onDestroy() + volumePlatformChannel.onDestroy() super.onDestroy() } override fun onKeyDown(code: Int, event: KeyEvent): Boolean { - return when (val keyCode: Int = event.keyCode) { - KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN -> { - if (handleVolume) { - volumeEventsEmitter?.success(keyCode) - true - } else { - super.onKeyDown(code, event) - } - } - else -> super.onKeyDown(code, event) + return if (volumePlatformChannel.onKeyDown(code, event)) { + true + } else { + super.onKeyDown(code, event) } } } diff --git a/android/app/src/main/kotlin/com/vodemn/lightmeter/PlatformChannels/CaffeinePlatformChannel.kt b/android/app/src/main/kotlin/com/vodemn/lightmeter/PlatformChannels/CaffeinePlatformChannel.kt new file mode 100644 index 0000000..233831f --- /dev/null +++ b/android/app/src/main/kotlin/com/vodemn/lightmeter/PlatformChannels/CaffeinePlatformChannel.kt @@ -0,0 +1,42 @@ +package com.vodemn.lightmeter.PlatformChannels + +import android.view.Window +import android.view.WindowManager +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.MethodChannel + +/** CaffeinePlatformChannel */ +class CaffeinePlatformChannel { + private lateinit var channel: MethodChannel + + fun onAttachedToEngine(binaryMessenger: BinaryMessenger, window: Window) { + channel = MethodChannel( + binaryMessenger, + "com.vodemn.lightmeter.CaffeinePlatformChannel.MethodChannel" + ) + channel.setMethodCallHandler { call, result -> + when (call.method) { + "isKeepScreenOn" -> result.success((window.attributes.flags and WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) != 0) + "setKeepScreenOn" -> { + if (call.arguments !is Boolean) { + result.error( + "invalid args", + "Argument should be of type Bool for 'setKeepScreenOn' call", + null + ) + } else { + if (call.arguments as Boolean) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + else window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + result.success(true) + } + } + + else -> result.notImplemented() + } + } + } + + fun onDestroy() { + channel.setMethodCallHandler(null) + } +} diff --git a/android/app/src/main/kotlin/com/vodemn/lightmeter/PlatformChannels/CameraInfoPlatformChannel.kt b/android/app/src/main/kotlin/com/vodemn/lightmeter/PlatformChannels/CameraInfoPlatformChannel.kt new file mode 100644 index 0000000..be831b1 --- /dev/null +++ b/android/app/src/main/kotlin/com/vodemn/lightmeter/PlatformChannels/CameraInfoPlatformChannel.kt @@ -0,0 +1,59 @@ +package com.vodemn.lightmeter.PlatformChannels + +import android.content.Context +import android.hardware.camera2.CameraCharacteristics +import android.hardware.camera2.CameraManager +import android.hardware.camera2.CameraMetadata.LENS_FACING_BACK +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.MethodChannel +import kotlin.math.pow +import kotlin.math.sqrt + +/** CameraInfoPlatformChannel */ +class CameraInfoPlatformChannel { + private lateinit var channel: MethodChannel + private lateinit var cameraManager: CameraManager + private var mainCameraEfl: Double? = null + + fun onAttachedToEngine(binaryMessenger: BinaryMessenger, context: Context) { + channel = MethodChannel( + binaryMessenger, + "com.vodemn.lightmeter.CameraInfoPlatformChannel.MethodChannel" + ) + cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager + channel.setMethodCallHandler { call, result -> + when (call.method) { + "mainCameraEfl" -> { + mainCameraEfl = mainCameraEfl ?: getMainCameraFocalLength35mm() + result.success(mainCameraEfl) + } + + else -> result.notImplemented() + } + } + } + + fun onDestroy() { + channel.setMethodCallHandler(null) + } + + private fun getMainCameraFocalLength35mm(): Double? { + return cameraManager.cameraIdList.map { + cameraManager.getCameraCharacteristics(it) + }.first { + it.get(CameraCharacteristics.LENS_FACING) == LENS_FACING_BACK + }.focalLength35mm() + } + + private fun CameraCharacteristics.focalLength35mm(): Double? { + val defaultFocalLength = + get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS)?.first() + val sensorSize = get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE) + return if (defaultFocalLength != null && sensorSize != null) { + // https://en.wikipedia.org/wiki/35_mm_equivalent_focal_length#Conversions + 43.27 * defaultFocalLength / sqrt(sensorSize.height.pow(2) + sensorSize.width.pow(2)) + } else { + null + } + } +} diff --git a/android/app/src/main/kotlin/com/vodemn/lightmeter/PlatformChannels/VolumePlatformChannel.kt b/android/app/src/main/kotlin/com/vodemn/lightmeter/PlatformChannels/VolumePlatformChannel.kt new file mode 100644 index 0000000..d9759d4 --- /dev/null +++ b/android/app/src/main/kotlin/com/vodemn/lightmeter/PlatformChannels/VolumePlatformChannel.kt @@ -0,0 +1,66 @@ +package com.vodemn.lightmeter.PlatformChannels + +import android.view.KeyEvent +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.EventChannel.EventSink +import io.flutter.plugin.common.MethodChannel + +/** VolumePlatformChannel */ +class VolumePlatformChannel { + private lateinit var volumeMethodChannel: MethodChannel + private lateinit var volumeEventChannel: EventChannel + private var volumeEventsEmitter: EventSink? = null + private var handleVolume = false + + fun onAttachedToEngine(binaryMessenger: BinaryMessenger) { + volumeMethodChannel = MethodChannel( + binaryMessenger, + "com.vodemn.lightmeter.VolumePlatformChannel.MethodChannel" + ) + volumeMethodChannel.setMethodCallHandler { call, result -> + when (call.method) { + "setVolumeHandling" -> { + handleVolume = call.arguments as Boolean + result.success(handleVolume) + } + + else -> result.notImplemented() + } + } + + volumeEventChannel = EventChannel( + binaryMessenger, + "com.vodemn.lightmeter.VolumePlatformChannel.EventChannel" + ) + volumeEventChannel.setStreamHandler(object : EventChannel.StreamHandler { + override fun onListen(listener: Any?, eventSink: EventSink) { + volumeEventsEmitter = eventSink + } + + override fun onCancel(listener: Any?) { + volumeEventsEmitter = null + } + }) + } + + fun onDestroy() { + volumeMethodChannel.setMethodCallHandler(null) + volumeEventChannel.setStreamHandler(null) + } + + fun onKeyDown(code: Int, event: KeyEvent): Boolean { + return when (val keyCode: Int = event.keyCode) { + KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN -> { + if (handleVolume) { + volumeEventsEmitter?.success(keyCode) + true + } else { + false + } + } + + else -> false + } + } +} \ No newline at end of file diff --git a/integration_test/e2e_test.dart b/integration_test/e2e_test.dart index 311b9a5..fc2cfc0 100644 --- a/integration_test/e2e_test.dart +++ b/integration_test/e2e_test.dart @@ -64,7 +64,7 @@ void testE2E(String description) { await tester.setApertureValues(mockEquipmentProfiles[0].apertureValues); await tester.setShutterSpeedValues(mockEquipmentProfiles[0].shutterSpeedValues); await tester.setZoomValue(mockEquipmentProfiles[0].lensZoom); - expect(find.text('x1.91'), findsOneWidget); + expect(find.text('50mm'), findsOneWidget); expect(find.text('f/1.7 - f/16'), findsOneWidget); expect(find.text('1/1000 - B'), findsOneWidget); await tester.saveEdits(); @@ -77,7 +77,7 @@ void testE2E(String description) { await tester.enterProfileName(mockEquipmentProfiles[1].name); await tester.setApertureValues(mockEquipmentProfiles[1].apertureValues); await tester.setZoomValue(mockEquipmentProfiles[1].lensZoom); - expect(find.text('x5.02'), findsOneWidget); + expect(find.text('135mm'), findsOneWidget); expect(find.text('f/3.5 - f/22'), findsOneWidget); expect(find.text('1/1000 - B'), findsNWidgets(1)); await tester.saveEdits(); @@ -305,7 +305,8 @@ Future _expectMeteringState( await tester.scrollToTheLastExposurePair(equipmentProfile: equipmentProfile); expectExposurePairsListItem(tester, slowest.split(' - ')[0], slowest.split(' - ')[1]); expectMeasureButton(ev); - expect(find.text(equipmentProfile.lensZoom.toZoom()), findsOneWidget); + final BuildContext context = tester.element(find.byType(IsoValuePicker)); + expect(find.text(equipmentProfile.lensZoom.toZoom(context)), findsOneWidget); } Future _expectMeteringStateAndMeasure( diff --git a/integration_test/mocks/paid_features_mock.dart b/integration_test/mocks/paid_features_mock.dart index d404a24..f401617 100644 --- a/integration_test/mocks/paid_features_mock.dart +++ b/integration_test/mocks/paid_features_mock.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:lightmeter/providers/equipment_profile_provider.dart'; import 'package:lightmeter/providers/films_provider.dart'; @@ -115,7 +117,9 @@ final mockEquipmentProfiles = [ IsoValue(1600, StopType.full), IsoValue(3200, StopType.full), ], - lensZoom: 1.91, + lensZoom: Platform.isAndroid + ? 2.083333 // Pixel 6 + : 1.923, // iPhone 13 Pro ), EquipmentProfile( id: '2', @@ -145,7 +149,9 @@ final mockEquipmentProfiles = [ IsoValue(1600, StopType.full), IsoValue(3200, StopType.full), ], - lensZoom: 5.02, + lensZoom: Platform.isAndroid + ? 5.625 // Pixel 6 + : 5.1923, // iPhone 13 Pro ), ]; diff --git a/integration_test/run_all_tests.dart b/integration_test/run_all_tests.dart index 112e60e..b0a623d 100644 --- a/integration_test/run_all_tests.dart +++ b/integration_test/run_all_tests.dart @@ -1,12 +1,22 @@ +import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'e2e_test.dart'; import 'metering_screen_layout_test.dart'; import 'purchases_test.dart'; +import 'utils/platform_channel_mock.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + setUpAll(() { + mockCameraFocalLength(); + }); + + tearDownAll(() { + mockCameraFocalLength(); + }); + testPurchases('Purchase & refund premium features'); testToggleLayoutFeatures('Toggle metering screen layout features'); testE2E('e2e'); diff --git a/integration_test/utils/platform_channel_mock.dart b/integration_test/utils/platform_channel_mock.dart index 383f243..a610502 100644 --- a/integration_test/utils/platform_channel_mock.dart +++ b/integration_test/utils/platform_channel_mock.dart @@ -1,8 +1,10 @@ +import 'dart:io'; import 'dart:math'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:light_sensor/light_sensor.dart'; +import 'package:lightmeter/data/camera_info_service.dart'; void setLightSensorAvilability({required bool hasSensor}) { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( @@ -57,3 +59,26 @@ void resetLightSensorStreamHandler() { null, ); } + +void mockCameraFocalLength() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( + CameraInfoService.cameraInfoPlatformChannel, + (methodCall) async { + switch (methodCall.method) { + case "mainCameraEfl": + return Platform.isAndroid + ? 24.0 // Pixel 6 + : 26.0; // iPhone 13 Pro + default: + return null; + } + }, + ); +} + +void resetCameraFocalLength() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( + CameraInfoService.cameraInfoPlatformChannel, + null, + ); +} diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 68a69e2..af1f354 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 001BDA042DD1252B00122957 /* CameraInfoPlatformChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 001BDA022DD0BBA700122957 /* CameraInfoPlatformChannel.swift */; }; + 0035E99F2DD0B07C0053508B /* CaffeinePlatformChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0035E99E2DD0B0700053508B /* CaffeinePlatformChannel.swift */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 5A1AF02F1E4619A6E479AA8B /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C991C89D5763E562E77E475E /* Pods_Runner.framework */; }; @@ -31,6 +33,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 001BDA022DD0BBA700122957 /* CameraInfoPlatformChannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraInfoPlatformChannel.swift; sourceTree = ""; }; + 0035E99E2DD0B0700053508B /* CaffeinePlatformChannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaffeinePlatformChannel.swift; sourceTree = ""; }; 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 = ""; }; @@ -69,6 +73,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 0035E99D2DD0B05F0053508B /* PlatformChannels */ = { + isa = PBXGroup; + children = ( + 0035E99E2DD0B0700053508B /* CaffeinePlatformChannel.swift */, + 001BDA022DD0BBA700122957 /* CameraInfoPlatformChannel.swift */, + ); + path = PlatformChannels; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -110,6 +123,7 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 0035E99D2DD0B05F0053508B /* PlatformChannels */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, ); path = Runner; @@ -156,7 +170,7 @@ 45F53C083F2EA48EF231DA16 /* [CP] Embed Pods Frameworks */, FF00F85CE432774850A0EDB7 /* [firebase_crashlytics] Crashlytics Upload Symbols */, 08127035D2CDEEEBA66FCDBB /* [CP] Copy Pods Resources */, - 051083B0AECD5071E5FA2A6F /* FlutterFire: "flutterfire upload-crashlytics-symbols" */, + 58ED09C740BAF94D922BAC2C /* FlutterFire: "flutterfire upload-crashlytics-symbols" */, ); buildRules = ( ); @@ -216,24 +230,6 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 051083B0AECD5071E5FA2A6F /* FlutterFire: "flutterfire upload-crashlytics-symbols" */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "FlutterFire: \"flutterfire upload-crashlytics-symbols\""; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\n#!/bin/bash\nPATH=\"${PATH}:$FLUTTER_ROOT/bin:$HOME/.pub-cache/bin\"\nflutterfire upload-crashlytics-symbols --upload-symbols-script-path=\"$PODS_ROOT/FirebaseCrashlytics/upload-symbols\" --platform=ios --apple-project-path=\"${SRCROOT}\" --env-platform-name=\"${PLATFORM_NAME}\" --env-configuration=\"${CONFIGURATION}\" --env-project-dir=\"${PROJECT_DIR}\" --env-built-products-dir=\"${BUILT_PRODUCTS_DIR}\" --env-dwarf-dsym-folder-path=\"${DWARF_DSYM_FOLDER_PATH}\" --env-dwarf-dsym-file-name=\"${DWARF_DSYM_FILE_NAME}\" --env-infoplist-path=\"${INFOPLIST_PATH}\" --default-config=default\n"; - }; 08127035D2CDEEEBA66FCDBB /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -306,6 +302,24 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; + 58ED09C740BAF94D922BAC2C /* FlutterFire: "flutterfire upload-crashlytics-symbols" */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 8; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "FlutterFire: \"flutterfire upload-crashlytics-symbols\""; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 1; + shellPath = /bin/sh; + shellScript = "\n#!/bin/bash\nPATH=\"${PATH}:$FLUTTER_ROOT/bin:$HOME/.pub-cache/bin\"\nflutterfire upload-crashlytics-symbols --upload-symbols-script-path=\"$PODS_ROOT/FirebaseCrashlytics/upload-symbols\" --platform=ios --apple-project-path=\"${SRCROOT}\" --env-platform-name=\"${PLATFORM_NAME}\" --env-configuration=\"${CONFIGURATION}\" --env-project-dir=\"${PROJECT_DIR}\" --env-built-products-dir=\"${BUILT_PRODUCTS_DIR}\" --env-dwarf-dsym-folder-path=\"${DWARF_DSYM_FOLDER_PATH}\" --env-dwarf-dsym-file-name=\"${DWARF_DSYM_FILE_NAME}\" --env-infoplist-path=\"${INFOPLIST_PATH}\" --default-config=default\n"; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -319,7 +333,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; }; FF00F85CE432774850A0EDB7 /* [firebase_crashlytics] Crashlytics Upload Symbols */ = { isa = PBXShellScriptBuildPhase; @@ -352,6 +366,8 @@ buildActionMask = 2147483647; files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 001BDA042DD1252B00122957 /* CameraInfoPlatformChannel.swift in Sources */, + 0035E99F2DD0B07C0053508B /* CaffeinePlatformChannel.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index b43b14e..d385809 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -8,25 +8,8 @@ import Flutter didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { let controller : FlutterViewController = window?.rootViewController as! FlutterViewController - let keepScreenOnChannel = FlutterMethodChannel(name: "com.vodemn.lightmeter/keepScreenOn", - binaryMessenger: controller.binaryMessenger) - keepScreenOnChannel.setMethodCallHandler({ - (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in - switch call.method { - case "isKeepScreenOn": - result(UIApplication.shared.isIdleTimerDisabled) - case "setKeepScreenOn": - guard let keepOn = call.arguments as? Bool else { - result(FlutterError(code: "invalid arguments", message: "Argument should be of type Bool for 'setKeepScreenOn' call", details: nil)) - return - } - UIApplication.shared.isIdleTimerDisabled = keepOn - result(true) - default: - result(FlutterMethodNotImplemented) - } - }) - + let caffeinePlatformChannel = CaffeinePlatformChannel(binaryMessenger: controller.binaryMessenger) + let cameraInfoPlatformChannel = CameraInfoPlatformChannel(binaryMessenger: controller.binaryMessenger) GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } diff --git a/ios/Runner/PlatformChannels/CaffeinePlatformChannel.swift b/ios/Runner/PlatformChannels/CaffeinePlatformChannel.swift new file mode 100644 index 0000000..7268a0c --- /dev/null +++ b/ios/Runner/PlatformChannels/CaffeinePlatformChannel.swift @@ -0,0 +1,35 @@ +// +// CaffeinePlatformChannel.swift +// Runner +// +// Created by Vadim Turko on 2025-05-11. +// +import Flutter + +public class CaffeinePlatformChannel: NSObject { + let methodChannel: FlutterMethodChannel + + init(binaryMessenger: FlutterBinaryMessenger) { + self.methodChannel = FlutterMethodChannel( + name: "com.vodemn.lightmeter.CaffeinePlatformChannel.MethodChannel", + binaryMessenger: binaryMessenger + ) + super.init() + methodChannel.setMethodCallHandler({ + (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in + switch call.method { + case "isKeepScreenOn": + result(UIApplication.shared.isIdleTimerDisabled) + case "setKeepScreenOn": + guard let keepOn = call.arguments as? Bool else { + result(FlutterError(code: "invalid arguments", message: "Argument should be of type Bool for 'setKeepScreenOn' call", details: nil)) + return + } + UIApplication.shared.isIdleTimerDisabled = keepOn + result(true) + default: + result(FlutterMethodNotImplemented) + } + }) + } +} diff --git a/ios/Runner/PlatformChannels/CameraInfoPlatformChannel.swift b/ios/Runner/PlatformChannels/CameraInfoPlatformChannel.swift new file mode 100644 index 0000000..e2922b0 --- /dev/null +++ b/ios/Runner/PlatformChannels/CameraInfoPlatformChannel.swift @@ -0,0 +1,45 @@ +// +// CameraInfoPlatformChannel.swift +// Runner +// +// Created by Vadim Turko on 2025-05-11. +// +import AVFoundation +import Flutter + +public class CameraInfoPlatformChannel: NSObject { + let methodChannel: FlutterMethodChannel + + init(binaryMessenger: FlutterBinaryMessenger) { + self.methodChannel = FlutterMethodChannel( + name: "com.vodemn.lightmeter.CameraInfoPlatformChannel.MethodChannel", + binaryMessenger: binaryMessenger + ) + super.init() + methodChannel.setMethodCallHandler({ + (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in + switch call.method { + case "mainCameraEfl": + if let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) { + result(self.get35mmEquivalentFocalLength(format: device.activeFormat)) + } else { + result(nil) + } + default: + result(FlutterMethodNotImplemented) + } + }) + } + + func get35mmEquivalentFocalLength(format : AVCaptureDevice.Format) -> Float { + // get reported field of view. Documentation says this is the horizontal field of view + var fov = format.videoFieldOfView + // convert to radians + fov *= Float.pi/180.0 + // angle and opposite of right angle triangle are half the fov and half the width of + // 35mm film (ie 18mm). The adjacent value of the right angle triangle is the equivalent + // focal length. Using some right angle triangle math you can work out focal length + let focalLen = 18 / tan(fov/2) + return focalLen + } +} diff --git a/lib/application_wrapper.dart b/lib/application_wrapper.dart index 611e1f7..e2c6136 100644 --- a/lib/application_wrapper.dart +++ b/lib/application_wrapper.dart @@ -5,6 +5,7 @@ import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:lightmeter/data/analytics/analytics.dart'; import 'package:lightmeter/data/analytics/api/analytics_firebase.dart'; import 'package:lightmeter/data/caffeine_service.dart'; +import 'package:lightmeter/data/camera_info_service.dart'; import 'package:lightmeter/data/haptics_service.dart'; import 'package:lightmeter/data/light_sensor_service.dart'; import 'package:lightmeter/data/permissions_service.dart'; @@ -99,11 +100,13 @@ class _ApplicationWrapperState extends State { await Future.wait([ SharedPreferences.getInstance(), const LightSensorService(LocalPlatform()).hasSensor(), + const CameraInfoService().mainCameraEfl(), remoteConfigService.activeAndFetchFeatures(), equipmentProfilesStorageService.init(), filmsStorageService.init(), ]).then((value) { - userPreferencesService = UserPreferencesService((value[0] as SharedPreferences?)!); + userPreferencesService = UserPreferencesService((value[0] as SharedPreferences?)!) + ..cameraFocalLength = value[2] as int?; hasLightSensor = value[1] as bool? ?? false; }); } diff --git a/lib/data/caffeine_service.dart b/lib/data/caffeine_service.dart index da36968..106438d 100644 --- a/lib/data/caffeine_service.dart +++ b/lib/data/caffeine_service.dart @@ -1,15 +1,17 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; class CaffeineService { - static const _methodChannel = MethodChannel("com.vodemn.lightmeter/keepScreenOn"); + @visibleForTesting + static const caffeineMethodChannel = MethodChannel("com.vodemn.lightmeter.CaffeinePlatformChannel.MethodChannel"); const CaffeineService(); Future isKeepScreenOn() async { - return _methodChannel.invokeMethod("isKeepScreenOn").then((value) => value!); + return caffeineMethodChannel.invokeMethod("isKeepScreenOn").then((value) => value!); } Future keepScreenOn(bool keep) async { - return _methodChannel.invokeMethod("setKeepScreenOn", keep).then((value) => value!); + return caffeineMethodChannel.invokeMethod("setKeepScreenOn", keep).then((value) => value!); } } diff --git a/lib/data/camera_info_service.dart b/lib/data/camera_info_service.dart new file mode 100644 index 0000000..44819b6 --- /dev/null +++ b/lib/data/camera_info_service.dart @@ -0,0 +1,16 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +class CameraInfoService { + @visibleForTesting + static const cameraInfoPlatformChannel = MethodChannel( + "com.vodemn.lightmeter.CameraInfoPlatformChannel.MethodChannel", + ); + + const CameraInfoService(); + + Future mainCameraEfl() async { + final focalLength = await cameraInfoPlatformChannel.invokeMethod('mainCameraEfl'); + return focalLength?.round(); + } +} diff --git a/lib/data/models/app_feature.dart b/lib/data/models/app_feature.dart index d6d916d..7b7e535 100644 --- a/lib/data/models/app_feature.dart +++ b/lib/data/models/app_feature.dart @@ -6,8 +6,8 @@ enum AppFeature { incidedntLightMetering, isoAndNdValues, themeEngine, - spotMetering, - histogram, + spotMeteringAndHistogram, + focalLength, listOfFilms, customFilms, equipmentProfiles, @@ -28,10 +28,10 @@ enum AppFeature { return S.of(context).featureIsoAndNdValues; case AppFeature.themeEngine: return S.of(context).featureTheme; - case AppFeature.spotMetering: - return S.of(context).featureSpotMetering; - case AppFeature.histogram: - return S.of(context).featureHistogram; + case AppFeature.spotMeteringAndHistogram: + return S.of(context).featureSpotMeteringAndHistorgram; + case AppFeature.focalLength: + return S.of(context).featureFocalLength35mm; case AppFeature.listOfFilms: return S.of(context).featureListOfFilms; case AppFeature.customFilms: diff --git a/lib/data/models/camera_feature.dart b/lib/data/models/camera_feature.dart index 20193bf..1f7c086 100644 --- a/lib/data/models/camera_feature.dart +++ b/lib/data/models/camera_feature.dart @@ -1,13 +1,26 @@ enum CameraFeature { spotMetering, histogram, + showFocalLength, } typedef CameraFeaturesConfig = Map; extension CameraFeaturesConfigJson on CameraFeaturesConfig { - static CameraFeaturesConfig fromJson(Map data) => - {for (final f in CameraFeature.values) f: data[f.name] as bool? ?? false}; + static CameraFeaturesConfig fromJson(Map data) { + MapEntry valueOrBool(CameraFeature feature, {bool defaultValue = true}) => MapEntry( + feature, + data[feature.name] as bool? ?? defaultValue, + ); + + return Map.fromEntries( + [ + valueOrBool(CameraFeature.spotMetering), + valueOrBool(CameraFeature.histogram, defaultValue: false), + valueOrBool(CameraFeature.showFocalLength), + ], + ); + } Map toJson() => map((key, value) => MapEntry(key.name, value)); } diff --git a/lib/data/shared_prefs_service.dart b/lib/data/shared_prefs_service.dart index 726fd63..e1144bf 100644 --- a/lib/data/shared_prefs_service.dart +++ b/lib/data/shared_prefs_service.dart @@ -22,6 +22,7 @@ class UserPreferencesService { static const lightSensorEvCalibrationKey = "lightSensorEvCalibration"; static const meteringScreenLayoutKey = "meteringScreenLayout"; static const cameraFeaturesKey = "cameraFeatures"; + static const cameraFocalLengthKey = "cameraFocalLength"; static const caffeineKey = "caffeine"; static const hapticsKey = "haptics"; @@ -93,38 +94,24 @@ class UserPreferencesService { set showEv100(bool value) => _sharedPreferences.setBool(showEv100Key, value); MeteringScreenLayoutConfig get meteringScreenLayout { - final configJson = _sharedPreferences.getString(meteringScreenLayoutKey); - if (configJson != null) { - return MeteringScreenLayoutConfigJson.fromJson( - json.decode(configJson) as Map, - ); - } else { - return { - MeteringScreenLayoutFeature.equipmentProfiles: true, - MeteringScreenLayoutFeature.extremeExposurePairs: true, - MeteringScreenLayoutFeature.filmPicker: true, - }; - } + final configJson = _sharedPreferences.getString(meteringScreenLayoutKey) ?? '{}'; + return MeteringScreenLayoutConfigJson.fromJson(json.decode(configJson) as Map); } set meteringScreenLayout(MeteringScreenLayoutConfig value) => _sharedPreferences.setString(meteringScreenLayoutKey, json.encode(value.toJson())); CameraFeaturesConfig get cameraFeatures { - final configJson = _sharedPreferences.getString(cameraFeaturesKey); - if (configJson != null) { - return CameraFeaturesConfigJson.fromJson(json.decode(configJson) as Map); - } else { - return { - CameraFeature.spotMetering: false, - CameraFeature.histogram: false, - }; - } + final configJson = _sharedPreferences.getString(cameraFeaturesKey) ?? '{}'; + return CameraFeaturesConfigJson.fromJson(json.decode(configJson) as Map); } set cameraFeatures(CameraFeaturesConfig value) => _sharedPreferences.setString(cameraFeaturesKey, json.encode(value.toJson())); + int? get cameraFocalLength => _sharedPreferences.getInt(cameraFocalLengthKey); + set cameraFocalLength(int? value) => _sharedPreferences.setInt(cameraFocalLengthKey, value!); + bool get caffeine => _sharedPreferences.getBool(caffeineKey) ?? false; set caffeine(bool value) => _sharedPreferences.setBool(caffeineKey, value); diff --git a/lib/data/volume_events_service.dart b/lib/data/volume_events_service.dart index 360de75..7dfde55 100644 --- a/lib/data/volume_events_service.dart +++ b/lib/data/volume_events_service.dart @@ -6,10 +6,10 @@ class VolumeEventsService { final LocalPlatform _localPlatform; @visibleForTesting - static const volumeHandlingChannel = MethodChannel("com.vodemn.lightmeter/volumeHandling"); + static const volumeMethodChannel = MethodChannel("com.vodemn.lightmeter.VolumePlatformChannel.MethodChannel"); @visibleForTesting - static const volumeEventsChannel = EventChannel("com.vodemn.lightmeter/volumeEvents"); + static const volumeEventsChannel = EventChannel("com.vodemn.lightmeter.VolumePlatformChannel.EventChannel"); const VolumeEventsService(this._localPlatform); @@ -19,9 +19,7 @@ class VolumeEventsService { if (!_localPlatform.isAndroid) { return false; } - return volumeHandlingChannel - .invokeMethod("setVolumeHandling", enableHandling) - .then((value) => value!); + return volumeMethodChannel.invokeMethod("setVolumeHandling", enableHandling).then((value) => value!); } /// Emits new events on @@ -32,9 +30,6 @@ class VolumeEventsService { if (!_localPlatform.isAndroid) { return const Stream.empty(); } - return volumeEventsChannel - .receiveBroadcastStream() - .cast() - .where((event) => event == 24 || event == 25); + return volumeEventsChannel.receiveBroadcastStream().cast().where((event) => event == 24 || event == 25); } } diff --git a/lib/interactors/metering_interactor.dart b/lib/interactors/metering_interactor.dart index b4427de..a2a0c49 100644 --- a/lib/interactors/metering_interactor.dart +++ b/lib/interactors/metering_interactor.dart @@ -30,8 +30,7 @@ class MeteringInteractor { if (_userPreferencesService.caffeine) { _caffeineService.keepScreenOn(true); } - _volumeEventsService - .setVolumeHandling(_userPreferencesService.volumeAction != VolumeAction.none); + _volumeEventsService.setVolumeHandling(_userPreferencesService.volumeAction != VolumeAction.none); } double get cameraEvCalibration => _userPreferencesService.cameraEvCalibration; @@ -62,15 +61,11 @@ class MeteringInteractor { } Future checkCameraPermission() async { - return _permissionsService - .checkCameraPermission() - .then((value) => value == PermissionStatus.granted); + return _permissionsService.checkCameraPermission().then((value) => value == PermissionStatus.granted); } Future requestCameraPermission() async { - return _permissionsService - .requestCameraPermission() - .then((value) => value == PermissionStatus.granted); + return _permissionsService.requestCameraPermission().then((value) => value == PermissionStatus.granted); } void openAppSettings() { diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 464656d..1169425 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -45,6 +45,8 @@ "cameraFeatureSpotMeteringHint": "Halte die Kameraansicht gedrückt um den Messpunkt zu entfernen", "cameraFeatureHistogram": "Histogramm", "cameraFeatureHistogramHint": "Verwendung des Histogramms kann den Batterieverbrauch erhöhen", + "cameraFeaturesShowFocalLength": "Brennweite anzeigen", + "cameraFeaturesShowFocalLengthHint": "Zeigt die 35mm-äquivalente Brennweite statt des Zoomfaktors an", "film": "Film", "filmPush": "Film (push)", "filmPull": "Film (pull)", @@ -111,8 +113,8 @@ "featureIncidentLightMetering": "Messung von einfallendem Licht", "featureIsoAndNdValues": "Große Auswahl von ISO und ND Filtern", "featureTheme": "Theme Anpassung", - "featureSpotMetering": "Punktmessung", - "featureHistogram": "Histogramm", + "featureSpotMeteringAndHistorgram": "Spotmessung und Histogramm", + "featureFocalLength35mm": "35mm-äquivalente Brennweite statt Zoom", "featureListOfFilms": "Liste von 20+ Filmen mit Reziprozitätsformeln", "featureCustomFilms": "Eigene Filme erstellen", "featureEquipmentProfiles": "Ausrüstungsprofile", diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 28d5242..eb5df18 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -45,6 +45,8 @@ "cameraFeatureSpotMeteringHint": "Long press the camera view to remove metering spot", "cameraFeatureHistogram": "Histogram", "cameraFeatureHistogramHint": "Enabling histogram can encrease battery drain", + "cameraFeaturesShowFocalLength": "Show Focal Length", + "cameraFeaturesShowFocalLengthHint": "Displays 35mm equivalent focal length instead of zoom factor", "film": "Film", "filmPush": "Film (push)", "filmPull": "Film (pull)", @@ -111,8 +113,8 @@ "featureIncidentLightMetering": "Incident light metering", "featureIsoAndNdValues": "Wide range of ISO and ND filters values", "featureTheme": "Theme customization", - "featureSpotMetering": "Spot metering", - "featureHistogram": "Histogram", + "featureSpotMeteringAndHistorgram": "Spot metering and histogram", + "featureFocalLength35mm": "35mm equivalent focal length instead of zoom", "featureListOfFilms": "List of 20+ films with reciprocity formulas", "featureCustomFilms": "Ability to create custom films", "featureEquipmentProfiles": "Equipment profiles", diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index f1226fe..2ef2281 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -45,6 +45,8 @@ "cameraFeatureSpotMeteringHint": "Appuyez longuement sur la vue de l'appareil photo pour supprimer le spot de mesure", "cameraFeatureHistogram": "Histogramme", "cameraFeatureHistogramHint": "L'activation de l'histogramme peut augmenter la consommation de la batterie", + "cameraFeaturesShowFocalLength": "Afficher la focale", + "cameraFeaturesShowFocalLengthHint": "Affiche la focale équivalente 35 mm au lieu du facteur de zoom", "film": "Pellicule", "filmPush": "Pellicule (push)", "filmPull": "Pellicule (pull)", @@ -112,8 +114,8 @@ "featureIncidentLightMetering": "Mesure de la lumière incidente", "featureIsoAndNdValues": "Large gamme de valeurs ISO et de filtres ND", "featureTheme": "Personnalisation du thème", - "featureSpotMetering": "Mesure spot", - "featureHistogram": "Histogramme", + "featureSpotMeteringAndHistorgram": "Mesure spot et histogramme", + "featureFocalLength35mm": "Focale équivalente 35 mm au lieu du zoom", "featureListOfFilms": "Liste de plus de 20 films avec des formules de correction", "featureCustomFilms": "Possibilité de créer des films personnalisés", "featureEquipmentProfiles": "Profils de l'équipement", diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index d9a813b..b25e8a3 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -45,6 +45,8 @@ "cameraFeatureSpotMeteringHint": "Используйте долгое нажатие, чтобы удалить точку замера", "cameraFeatureHistogram": "Гистограмма", "cameraFeatureHistogramHint": "Использование гистограммы может увеличить расход аккумулятора", + "cameraFeaturesShowFocalLength": "Показать фокусное расстояние", + "cameraFeaturesShowFocalLengthHint": "Показывает эквивалент фокусного расстояния (35 мм) вместо коэффициента зума", "film": "Пленка", "filmPush": "Пленка (push)", "filmPull": "Пленка (pull)", @@ -111,8 +113,8 @@ "featureIncidentLightMetering": "Замер падающего света", "featureIsoAndNdValues": "Широкий диапазон значений ISO и фильтров ND", "featureTheme": "Настройка темы", - "featureSpotMetering": "Точечный замер", - "featureHistogram": "Гистограмма", + "featureSpotMeteringAndHistorgram": "Точечный замер и гистограмма", + "featureFocalLength35mm": "Эквивалентное фокусное расстояние 35 мм вместо зума", "featureListOfFilms": "Список из 20+ плёнок с формулами коррекции", "featureCustomFilms": "Возможность создания собственных плёнок", "featureEquipmentProfiles": "Профили оборудования", diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb index d5a96a4..19ed915 100644 --- a/lib/l10n/intl_zh.arb +++ b/lib/l10n/intl_zh.arb @@ -45,6 +45,8 @@ "cameraFeatureSpotMeteringHint": "长按相机视图可移除测光点", "cameraFeatureHistogram": "直方图", "cameraFeatureHistogramHint": "启用直方图会增加电池消耗", + "cameraFeaturesShowFocalLength": "显示焦距", + "cameraFeaturesShowFocalLengthHint": "显示 35mm 等效焦距而非变焦倍数", "film": "胶片", "filmPush": "胶片 (push)", "filmPull": "胶片 (pull)", @@ -110,8 +112,8 @@ "featureIncidentLightMetering": "入射光测光", "featureIsoAndNdValues": "更广的 ISO 和 ND 滤镜系数范围", "featureTheme": "主题自定义", - "featureSpotMetering": "点测光", - "featureHistogram": "直方图", + "featureSpotMeteringAndHistorgram": "点测光与直方图", + "featureFocalLength35mm": "35mm 等效焦距替代变焦", "featureListOfFilms": "20多种胶片的补偿公式", "featureCustomFilms": "创建自定义胶片", "featureEquipmentProfiles": "设备配置文件", @@ -150,4 +152,4 @@ "filmFormulaExponentialRfPlaceholder": "1.3", "addEquipmentProfileTitle": "添加设备", "editEquipmentProfileTitle": "编辑设备" -} +} \ No newline at end of file diff --git a/lib/screens/equipment_profile_edit/screen_equipment_profile_edit.dart b/lib/screens/equipment_profile_edit/screen_equipment_profile_edit.dart index ecc441d..f2578c7 100644 --- a/lib/screens/equipment_profile_edit/screen_equipment_profile_edit.dart +++ b/lib/screens/equipment_profile_edit/screen_equipment_profile_edit.dart @@ -247,7 +247,7 @@ class _LensZoomListTileBuilder extends StatelessWidget { description: S.of(context).lensZoomDescription, value: state.lensZoom, range: const RangeValues(1, 7), - valueAdapter: (_, value) => value.toZoom(), + valueAdapter: (context, value) => value.toZoom(context), onChanged: (value) { context.read().add(EquipmentProfileLensZoomChangedEvent(value)); }, diff --git a/lib/screens/metering/components/camera_container/bloc_container_camera.dart b/lib/screens/metering/components/camera_container/bloc_container_camera.dart index 99d78a8..a5da36b 100644 --- a/lib/screens/metering/components/camera_container/bloc_container_camera.dart +++ b/lib/screens/metering/components/camera_container/bloc_container_camera.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'dart:math' as math; import 'package:camera/camera.dart'; +import 'package:exif/exif.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -17,7 +18,7 @@ import 'package:lightmeter/screens/metering/components/camera_container/event_co import 'package:lightmeter/screens/metering/components/camera_container/models/camera_error_type.dart'; import 'package:lightmeter/screens/metering/components/camera_container/state_container_camera.dart'; import 'package:lightmeter/screens/metering/components/shared/ev_source_base/bloc_base_ev_source.dart'; -import 'package:lightmeter/utils/ev_from_bytes.dart'; +import 'package:lightmeter/utils/exif_utils.dart'; part 'mock_bloc_container_camera.part.dart'; @@ -184,7 +185,8 @@ class CameraContainerBloc extends EvSourceBlocBase value.toStringAsFixed(0), - valueAdapter: (value) => value.toZoom(), + valueAdapter: (value) => value.toZoom(context), ); } } diff --git a/lib/screens/metering/components/camera_container/mock_bloc_container_camera.part.dart b/lib/screens/metering/components/camera_container/mock_bloc_container_camera.part.dart index 1806674..ff908cb 100644 --- a/lib/screens/metering/components/camera_container/mock_bloc_container_camera.part.dart +++ b/lib/screens/metering/components/camera_container/mock_bloc_container_camera.part.dart @@ -72,7 +72,8 @@ class MockCameraContainerBloc extends CameraContainerBloc { Future _takePhoto() async { try { final bytes = (await rootBundle.load(PlatformConfig.cameraStubImage)).buffer.asUint8List(); - return await evFromImage(bytes); + final tags = await readExifFromBytes(bytes); + return evFromTags(tags); } catch (e, stackTrace) { log(e.toString(), stackTrace: stackTrace); return null; diff --git a/lib/screens/settings/components/metering/components/camera_features/widget_list_tile_camera_features.dart b/lib/screens/settings/components/metering/components/camera_features/widget_list_tile_camera_features.dart index c779912..00ec9fd 100644 --- a/lib/screens/settings/components/metering/components/camera_features/widget_list_tile_camera_features.dart +++ b/lib/screens/settings/components/metering/components/camera_features/widget_list_tile_camera_features.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/data/models/camera_feature.dart'; import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/providers/services_provider.dart'; import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/screens/settings/components/shared/dialog_switch/widget_dialog_switch.dart'; import 'package:lightmeter/screens/settings/components/shared/iap_list_tile/widget_list_tile_iap.dart'; @@ -20,21 +21,21 @@ class CameraFeaturesListTile extends StatelessWidget { icon: Icons.camera_alt_outlined, title: S.of(context).cameraFeatures, values: UserPreferencesProvider.cameraConfigOf(context), - titleAdapter: (context, feature) { - switch (feature) { - case CameraFeature.spotMetering: - return S.of(context).cameraFeatureSpotMetering; - case CameraFeature.histogram: - return S.of(context).cameraFeatureHistogram; - } + enabledAdapter: (feature) => switch (feature) { + CameraFeature.spotMetering => true, + CameraFeature.histogram => true, + CameraFeature.showFocalLength => + ServicesProvider.of(context).userPreferencesService.cameraFocalLength != null, }, - subtitleAdapter: (context, feature) { - switch (feature) { - case CameraFeature.spotMetering: - return S.of(context).cameraFeatureSpotMeteringHint; - case CameraFeature.histogram: - return S.of(context).cameraFeatureHistogramHint; - } + titleAdapter: (context, feature) => switch (feature) { + CameraFeature.spotMetering => S.of(context).cameraFeatureSpotMetering, + CameraFeature.histogram => S.of(context).cameraFeatureHistogram, + CameraFeature.showFocalLength => S.of(context).cameraFeaturesShowFocalLength, + }, + subtitleAdapter: (context, feature) => switch (feature) { + CameraFeature.spotMetering => S.of(context).cameraFeatureSpotMeteringHint, + CameraFeature.histogram => S.of(context).cameraFeatureHistogramHint, + CameraFeature.showFocalLength => S.of(context).cameraFeaturesShowFocalLengthHint, }, onSave: UserPreferencesProvider.of(context).setCameraFeature, ), diff --git a/lib/utils/double_to_zoom.dart b/lib/utils/double_to_zoom.dart index 6d2b99f..075f42c 100644 --- a/lib/utils/double_to_zoom.dart +++ b/lib/utils/double_to_zoom.dart @@ -1,3 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/data/models/camera_feature.dart'; +import 'package:lightmeter/providers/services_provider.dart'; +import 'package:lightmeter/providers/user_preferences_provider.dart'; + extension DoubleToZoom on double { - String toZoom() => 'x${toStringAsFixed(2)}'; + String toZoom(BuildContext context) { + final showFocalLength = UserPreferencesProvider.cameraFeatureOf(context, CameraFeature.showFocalLength); + final cameraFocalLength = ServicesProvider.of(context).userPreferencesService.cameraFocalLength; + + if (showFocalLength && cameraFocalLength != null) { + ServicesProvider.of(context).userPreferencesService.cameraFocalLength; + final zoomedFocalLength = (this * cameraFocalLength).round(); + return '${zoomedFocalLength}mm'; + } else { + return 'x${toStringAsFixed(2)}'; + } + } } diff --git a/lib/utils/ev_from_bytes.dart b/lib/utils/exif_utils.dart similarity index 86% rename from lib/utils/ev_from_bytes.dart rename to lib/utils/exif_utils.dart index 9010402..b4f36bb 100644 --- a/lib/utils/ev_from_bytes.dart +++ b/lib/utils/exif_utils.dart @@ -1,22 +1,20 @@ import 'dart:math' as math; import 'package:exif/exif.dart'; -import 'package:flutter/foundation.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; const String _isoExifKey = 'EXIF ISOSpeedRatings'; const String _apertureExifKey = 'EXIF FNumber'; const String _shutterSpeedExifKey = 'EXIF ExposureTime'; -Future evFromImage(Uint8List bytes) async { - final tags = await readExifFromBytes(bytes); +double evFromTags(Map tags) { final iso = double.tryParse("${tags[_isoExifKey]}"); final apertureValueRatio = (tags[_apertureExifKey]?.values as IfdRatios?)?.ratios.first; final speedValueRatio = (tags[_shutterSpeedExifKey]?.values as IfdRatios?)?.ratios.first; if (iso == null || apertureValueRatio == null || speedValueRatio == null) { throw ArgumentError( - 'Error parsing EXIF', + 'Error calculating EV', [ if (iso == null) '$_isoExifKey: ${tags[_isoExifKey]?.printable} ${tags[_isoExifKey]?.printable.runtimeType}', if (apertureValueRatio == null) '$_apertureExifKey: $apertureValueRatio', diff --git a/screenshots/README.md b/screenshots/README.md index 8c6555e..5f9dc18 100644 --- a/screenshots/README.md +++ b/screenshots/README.md @@ -52,7 +52,7 @@ Apple requires screenshots a specific list of devices, so we can implement a cus Can be run on Simulator. ```console -sh screenshots/generate_ios_screenshots.sh +sh screenshots/scripts/generate_ios_screenshots.sh ``` ### Apply store constraints and text data diff --git a/screenshots/generate_screenshots.dart b/screenshots/generate_screenshots.dart index dd77757..b0ae283 100644 --- a/screenshots/generate_screenshots.dart +++ b/screenshots/generate_screenshots.dart @@ -6,6 +6,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; +import 'package:lightmeter/data/models/camera_feature.dart'; import 'package:lightmeter/data/models/ev_source_type.dart'; import 'package:lightmeter/data/models/exposure_pair.dart'; import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; @@ -27,6 +28,7 @@ import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../integration_test/mocks/paid_features_mock.dart'; +import '../integration_test/utils/platform_channel_mock.dart'; import '../integration_test/utils/widget_tester_actions.dart'; import 'models/screenshot_args.dart'; @@ -68,6 +70,14 @@ void main() { }.toJson(), ), + UserPreferencesService.cameraFeaturesKey: json.encode( + { + CameraFeature.spotMetering: false, + CameraFeature.histogram: false, + CameraFeature.showFocalLength: true, + }.toJson(), + ), + /// General settings UserPreferencesService.autostartTimerKey: false, UserPreferencesService.caffeineKey: true, @@ -86,6 +96,11 @@ void main() { setUpAll(() async { if (Platform.isAndroid) await binding.convertFlutterSurfaceToImage(); + mockCameraFocalLength(); + }); + + tearDownAll(() { + resetCameraFocalLength(); }); /// Generates several screenshots with the light theme @@ -119,6 +134,9 @@ void main() { await tester.pumpAndSettle(); await tester.tap(find.byIcon(Icons.edit_outlined).first); await tester.pumpAndSettle(); + await tester.tap(find.text(S.current.isoValues)); // open and close a dialog to hide keyboard + await tester.pumpAndSettle(); + await tester.tapCancelButton(); await tester.takeScreenshotLight(binding, 'equipment-profiles'); }); diff --git a/screenshots/generated/android/android/dark_metering-reflected.png b/screenshots/generated/android/android/dark_metering-reflected.png index d43d92c..47d1387 100644 Binary files a/screenshots/generated/android/android/dark_metering-reflected.png and b/screenshots/generated/android/android/dark_metering-reflected.png differ diff --git a/screenshots/generated/android/android/light_equipment-profiles.png b/screenshots/generated/android/android/light_equipment-profiles.png index 8d1f49e..c6f631f 100644 Binary files a/screenshots/generated/android/android/light_equipment-profiles.png and b/screenshots/generated/android/android/light_equipment-profiles.png differ diff --git a/screenshots/generated/android/android/light_metering-iso-picker.png b/screenshots/generated/android/android/light_metering-iso-picker.png index cc201df..fd37fc5 100644 Binary files a/screenshots/generated/android/android/light_metering-iso-picker.png and b/screenshots/generated/android/android/light_metering-iso-picker.png differ diff --git a/screenshots/generated/android/android/light_metering-reflected.png b/screenshots/generated/android/android/light_metering-reflected.png index 35e666b..0f98110 100644 Binary files a/screenshots/generated/android/android/light_metering-reflected.png and b/screenshots/generated/android/android/light_metering-reflected.png differ diff --git a/screenshots/generated/android/android/light_timer.png b/screenshots/generated/android/android/light_timer.png index 5357128..d8c49d0 100644 Binary files a/screenshots/generated/android/android/light_timer.png and b/screenshots/generated/android/android/light_timer.png differ diff --git a/screenshots/generated/ios/iphone55inch/dark_metering-reflected.png b/screenshots/generated/ios/iphone55inch/dark_metering-reflected.png index 8acf110..43f71df 100644 Binary files a/screenshots/generated/ios/iphone55inch/dark_metering-reflected.png and b/screenshots/generated/ios/iphone55inch/dark_metering-reflected.png differ diff --git a/screenshots/generated/ios/iphone55inch/light_equipment-profiles.png b/screenshots/generated/ios/iphone55inch/light_equipment-profiles.png index 06a05cf..9737b32 100644 Binary files a/screenshots/generated/ios/iphone55inch/light_equipment-profiles.png and b/screenshots/generated/ios/iphone55inch/light_equipment-profiles.png differ diff --git a/screenshots/generated/ios/iphone55inch/light_metering-iso-picker.png b/screenshots/generated/ios/iphone55inch/light_metering-iso-picker.png index 5b6c769..de7b7c5 100644 Binary files a/screenshots/generated/ios/iphone55inch/light_metering-iso-picker.png and b/screenshots/generated/ios/iphone55inch/light_metering-iso-picker.png differ diff --git a/screenshots/generated/ios/iphone55inch/light_metering-reflected.png b/screenshots/generated/ios/iphone55inch/light_metering-reflected.png index d1f1648..4a5fcfd 100644 Binary files a/screenshots/generated/ios/iphone55inch/light_metering-reflected.png and b/screenshots/generated/ios/iphone55inch/light_metering-reflected.png differ diff --git a/screenshots/generated/ios/iphone55inch/light_settings.png b/screenshots/generated/ios/iphone55inch/light_settings.png index 920d694..fc6b91a 100644 Binary files a/screenshots/generated/ios/iphone55inch/light_settings.png and b/screenshots/generated/ios/iphone55inch/light_settings.png differ diff --git a/screenshots/generated/ios/iphone55inch/light_timer.png b/screenshots/generated/ios/iphone55inch/light_timer.png index 4906114..8ee1b1a 100644 Binary files a/screenshots/generated/ios/iphone55inch/light_timer.png and b/screenshots/generated/ios/iphone55inch/light_timer.png differ diff --git a/screenshots/generated/ios/iphone65inch/dark_metering-reflected.png b/screenshots/generated/ios/iphone65inch/dark_metering-reflected.png index ef86fd8..41628bf 100644 Binary files a/screenshots/generated/ios/iphone65inch/dark_metering-reflected.png and b/screenshots/generated/ios/iphone65inch/dark_metering-reflected.png differ diff --git a/screenshots/generated/ios/iphone65inch/light_equipment-profiles.png b/screenshots/generated/ios/iphone65inch/light_equipment-profiles.png index 2b45495..a3cb016 100644 Binary files a/screenshots/generated/ios/iphone65inch/light_equipment-profiles.png and b/screenshots/generated/ios/iphone65inch/light_equipment-profiles.png differ diff --git a/screenshots/generated/ios/iphone65inch/light_metering-iso-picker.png b/screenshots/generated/ios/iphone65inch/light_metering-iso-picker.png index d69ca3a..6d1589b 100644 Binary files a/screenshots/generated/ios/iphone65inch/light_metering-iso-picker.png and b/screenshots/generated/ios/iphone65inch/light_metering-iso-picker.png differ diff --git a/screenshots/generated/ios/iphone65inch/light_metering-reflected.png b/screenshots/generated/ios/iphone65inch/light_metering-reflected.png index 9d2936f..3030aba 100644 Binary files a/screenshots/generated/ios/iphone65inch/light_metering-reflected.png and b/screenshots/generated/ios/iphone65inch/light_metering-reflected.png differ diff --git a/screenshots/generated/ios/iphone65inch/light_settings.png b/screenshots/generated/ios/iphone65inch/light_settings.png index 48f6014..b249ae7 100644 Binary files a/screenshots/generated/ios/iphone65inch/light_settings.png and b/screenshots/generated/ios/iphone65inch/light_settings.png differ diff --git a/screenshots/generated/ios/iphone65inch/light_timer.png b/screenshots/generated/ios/iphone65inch/light_timer.png index c1b91a0..5f6e1ce 100644 Binary files a/screenshots/generated/ios/iphone65inch/light_timer.png and b/screenshots/generated/ios/iphone65inch/light_timer.png differ diff --git a/test/data/caffeine_service_test.dart b/test/data/caffeine_service_test.dart index 0c80fac..fddfce1 100644 --- a/test/data/caffeine_service_test.dart +++ b/test/data/caffeine_service_test.dart @@ -7,7 +7,6 @@ void main() { late CaffeineService service; - const methodChannel = MethodChannel('com.vodemn.lightmeter/keepScreenOn'); Future? methodCallSuccessHandler(MethodCall methodCall) async { switch (methodCall.method) { case "isKeepScreenOn": @@ -21,45 +20,57 @@ void main() { setUp(() { service = const CaffeineService(); - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(methodChannel, methodCallSuccessHandler); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( + CaffeineService.caffeineMethodChannel, + methodCallSuccessHandler, + ); }); tearDown(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(methodChannel, null); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( + CaffeineService.caffeineMethodChannel, + null, + ); }); group( 'isKeepScreenOn()', () { test('true', () async { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(methodChannel, null); - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(methodChannel, (methodCall) async { - switch (methodCall.method) { - case "isKeepScreenOn": - return true; - default: - return null; - } - }); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( + CaffeineService.caffeineMethodChannel, + null, + ); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( + CaffeineService.caffeineMethodChannel, + (methodCall) async { + switch (methodCall.method) { + case "isKeepScreenOn": + return true; + default: + return null; + } + }, + ); expectLater(service.isKeepScreenOn(), completion(true)); }); test('false', () async { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(methodChannel, null); - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(methodChannel, (methodCall) async { - switch (methodCall.method) { - case "isKeepScreenOn": - return false; - default: - return null; - } - }); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( + CaffeineService.caffeineMethodChannel, + null, + ); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( + CaffeineService.caffeineMethodChannel, + (methodCall) async { + switch (methodCall.method) { + case "isKeepScreenOn": + return false; + default: + return null; + } + }, + ); expectLater(service.isKeepScreenOn(), completion(false)); }); }, diff --git a/test/data/models/camera_features_config_test.dart b/test/data/models/camera_features_config_test.dart index d7261a8..ceed378 100644 --- a/test/data/models/camera_features_config_test.dart +++ b/test/data/models/camera_features_config_test.dart @@ -11,21 +11,24 @@ void main() { { 'spotMetering': true, 'histogram': true, + 'showFocalLength': true, }, ), { CameraFeature.spotMetering: true, CameraFeature.histogram: true, + CameraFeature.showFocalLength: true, }, ); }); - test('Legacy (no spotMetering & histogram)', () { + test('Legacy', () { expect( CameraFeaturesConfigJson.fromJson({}), { - CameraFeature.spotMetering: false, + CameraFeature.spotMetering: true, CameraFeature.histogram: false, + CameraFeature.showFocalLength: true, }, ); }); @@ -37,10 +40,12 @@ void main() { { CameraFeature.spotMetering: true, CameraFeature.histogram: true, + CameraFeature.showFocalLength: true, }.toJson(), { 'spotMetering': true, 'histogram': true, + 'showFocalLength': true, }, ); }); diff --git a/test/data/shared_prefs_service_test.dart b/test/data/shared_prefs_service_test.dart index f4b480b..0ee45a9 100644 --- a/test/data/shared_prefs_service_test.dart +++ b/test/data/shared_prefs_service_test.dart @@ -270,8 +270,9 @@ void main() { expect( service.cameraFeatures, { - CameraFeature.spotMetering: false, + CameraFeature.spotMetering: true, CameraFeature.histogram: false, + CameraFeature.showFocalLength: true, }, ); }); @@ -284,6 +285,7 @@ void main() { { CameraFeature.spotMetering: false, CameraFeature.histogram: true, + CameraFeature.showFocalLength: true, }, ); }); @@ -292,17 +294,18 @@ void main() { when( () => sharedPreferences.setString( UserPreferencesService.cameraFeaturesKey, - """{"spotMetering":false,"histogram":true}""", + """{"spotMetering":false,"histogram":true,"showFocalLength":true}""", ), ).thenAnswer((_) => Future.value(true)); service.cameraFeatures = { CameraFeature.spotMetering: false, CameraFeature.histogram: true, + CameraFeature.showFocalLength: true, }; verify( () => sharedPreferences.setString( UserPreferencesService.cameraFeaturesKey, - """{"spotMetering":false,"histogram":true}""", + """{"spotMetering":false,"histogram":true,"showFocalLength":true}""", ), ).called(1); }); diff --git a/test/data/volume_events_service_test.dart b/test/data/volume_events_service_test.dart index f574e50..8dc994c 100644 --- a/test/data/volume_events_service_test.dart +++ b/test/data/volume_events_service_test.dart @@ -27,14 +27,14 @@ void main() { localPlatform = _MockLocalPlatform(); service = VolumeEventsService(localPlatform); TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( - VolumeEventsService.volumeHandlingChannel, + VolumeEventsService.volumeMethodChannel, methodCallSuccessHandler, ); }); tearDown(() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( - VolumeEventsService.volumeHandlingChannel, + VolumeEventsService.volumeMethodChannel, null, ); }); diff --git a/test/providers/user_preferences_provider_test.dart b/test/providers/user_preferences_provider_test.dart index 3ea3fc3..b80e1d0 100644 --- a/test/providers/user_preferences_provider_test.dart +++ b/test/providers/user_preferences_provider_test.dart @@ -37,6 +37,7 @@ void main() { when(() => mockUserPreferencesService.cameraFeatures).thenReturn({ CameraFeature.spotMetering: true, CameraFeature.histogram: true, + CameraFeature.showFocalLength: true, }); when(() => mockUserPreferencesService.locale).thenReturn(SupportedLocale.en); when(() => mockUserPreferencesService.themeType).thenReturn(ThemeType.light); @@ -227,13 +228,6 @@ void main() { expect(find.text("${MeteringScreenLayoutFeature.equipmentProfiles}: true"), findsNWidgets(2)); expect(find.text("${MeteringScreenLayoutFeature.extremeExposurePairs}: false"), findsNWidgets(2)); expect(find.text("${MeteringScreenLayoutFeature.filmPicker}: false"), findsNWidgets(2)); - verify( - () => mockUserPreferencesService.meteringScreenLayout = { - MeteringScreenLayoutFeature.extremeExposurePairs: false, - MeteringScreenLayoutFeature.filmPicker: false, - MeteringScreenLayoutFeature.equipmentProfiles: true, - }, - ).called(1); }, ); @@ -260,6 +254,7 @@ void main() { onPressed: () => UserPreferencesProvider.of(context).setCameraFeature({ CameraFeature.spotMetering: true, CameraFeature.histogram: false, + CameraFeature.showFocalLength: false, }), child: const Text(''), ), @@ -270,15 +265,18 @@ void main() { // Match `findsNWidgets(2)` to verify that `cameraFeatureOf` specific results are the same as the whole config expect(find.text("${CameraFeature.spotMetering}: true"), findsNWidgets(2)); expect(find.text("${CameraFeature.histogram}: true"), findsNWidgets(2)); + expect(find.text("${CameraFeature.showFocalLength}: true"), findsNWidgets(2)); await tester.tap(find.byType(ElevatedButton)); await tester.pumpAndSettle(); expect(find.text("${CameraFeature.spotMetering}: true"), findsNWidgets(2)); expect(find.text("${CameraFeature.histogram}: false"), findsNWidgets(2)); + expect(find.text("${CameraFeature.showFocalLength}: false"), findsNWidgets(2)); verify( () => mockUserPreferencesService.cameraFeatures = { CameraFeature.spotMetering: true, CameraFeature.histogram: false, + CameraFeature.showFocalLength: false, }, ).called(1); }, diff --git a/test/screens/lightmeter_pro/goldens/lightmeter_pro_screen.png b/test/screens/lightmeter_pro/goldens/lightmeter_pro_screen.png index c9dd802..20de97c 100644 Binary files a/test/screens/lightmeter_pro/goldens/lightmeter_pro_screen.png and b/test/screens/lightmeter_pro/goldens/lightmeter_pro_screen.png differ diff --git a/test/screens/metering/goldens/metering_screen.png b/test/screens/metering/goldens/metering_screen.png index deaa1f1..c08f2d4 100644 Binary files a/test/screens/metering/goldens/metering_screen.png and b/test/screens/metering/goldens/metering_screen.png differ diff --git a/test/utils/ev_from_bytes_test.dart b/test/utils/ev_from_bytes_test.dart deleted file mode 100644 index 488d77e..0000000 --- a/test/utils/ev_from_bytes_test.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'dart:io'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:lightmeter/utils/ev_from_bytes.dart'; - -void main() { - group('evFromImage', () { - test( - 'camera_stub_image.jpg', - () { - final bytes = File('assets/camera_stub_image.jpg').readAsBytesSync(); - expectLater(evFromImage(bytes), completion(8.25230310752341)); - }, - ); - - test( - 'no EXIF', - () { - final bytes = File('assets/launcher_icon_dev_512.png').readAsBytesSync(); - expectLater(evFromImage(bytes), throwsArgumentError); - }, - ); - }); -} diff --git a/test/utils/exif_utils_test.dart b/test/utils/exif_utils_test.dart new file mode 100644 index 0000000..8d40d4d --- /dev/null +++ b/test/utils/exif_utils_test.dart @@ -0,0 +1,27 @@ +import 'dart:io'; + +import 'package:exif/exif.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:lightmeter/utils/exif_utils.dart'; + +void main() { + group('evFromTags', () { + test( + 'camera_stub_image.jpg', + () async { + final bytes = File('assets/camera_stub_image.jpg').readAsBytesSync(); + final tags = await readExifFromBytes(bytes); + expect(evFromTags(tags), 8.25230310752341); + }, + ); + + test( + 'no EXIF', + () async { + final bytes = File('assets/launcher_icon_dev_512.png').readAsBytesSync(); + final tags = await readExifFromBytes(bytes); + expect(() => evFromTags(tags), throwsArgumentError); + }, + ); + }); +}