This commit is contained in:
Vadim 2025-05-12 14:32:19 +00:00 committed by GitHub
commit 450b613a99
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
60 changed files with 579 additions and 264 deletions

View file

@ -2,20 +2,17 @@ package com.vodemn.lightmeter
import android.os.Bundle import android.os.Bundle
import android.view.KeyEvent import android.view.KeyEvent
import android.view.WindowManager
import androidx.core.view.WindowCompat 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.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine 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() { class MainActivity : FlutterActivity() {
private lateinit var keepScreenOnChannel: MethodChannel private val caffeinePlatformChannel = CaffeinePlatformChannel()
private lateinit var volumeHandlingChannel: MethodChannel private val cameraInfoPlatformChannel = CameraInfoPlatformChannel()
private lateinit var volumeEventChannel: EventChannel private val volumePlatformChannel = VolumePlatformChannel()
private var volumeEventsEmitter: EventSink? = null
private var handleVolume = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -24,72 +21,24 @@ class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine) super.configureFlutterEngine(flutterEngine)
keepScreenOnChannel = MethodChannel( val binaryMessenger = flutterEngine.dartExecutor.binaryMessenger
flutterEngine.dartExecutor.binaryMessenger, caffeinePlatformChannel.onAttachedToEngine(binaryMessenger, window)
"com.vodemn.lightmeter/keepScreenOn" cameraInfoPlatformChannel.onAttachedToEngine(binaryMessenger, context)
) volumePlatformChannel.onAttachedToEngine(binaryMessenger)
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
}
})
} }
override fun onDestroy() { override fun onDestroy() {
keepScreenOnChannel.setMethodCallHandler(null) caffeinePlatformChannel.onDestroy()
volumeHandlingChannel.setMethodCallHandler(null) cameraInfoPlatformChannel.onDestroy()
volumeEventChannel.setStreamHandler(null) volumePlatformChannel.onDestroy()
super.onDestroy() super.onDestroy()
} }
override fun onKeyDown(code: Int, event: KeyEvent): Boolean { override fun onKeyDown(code: Int, event: KeyEvent): Boolean {
return when (val keyCode: Int = event.keyCode) { return if (volumePlatformChannel.onKeyDown(code, event)) {
KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN -> { true
if (handleVolume) { } else {
volumeEventsEmitter?.success(keyCode) super.onKeyDown(code, event)
true
} else {
super.onKeyDown(code, event)
}
}
else -> super.onKeyDown(code, event)
} }
} }
} }

View file

@ -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)
}
}

View file

@ -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
}
}
}

View file

@ -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
}
}
}

View file

@ -64,7 +64,7 @@ void testE2E(String description) {
await tester.setApertureValues(mockEquipmentProfiles[0].apertureValues); await tester.setApertureValues(mockEquipmentProfiles[0].apertureValues);
await tester.setShutterSpeedValues(mockEquipmentProfiles[0].shutterSpeedValues); await tester.setShutterSpeedValues(mockEquipmentProfiles[0].shutterSpeedValues);
await tester.setZoomValue(mockEquipmentProfiles[0].lensZoom); 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('f/1.7 - f/16'), findsOneWidget);
expect(find.text('1/1000 - B'), findsOneWidget); expect(find.text('1/1000 - B'), findsOneWidget);
await tester.saveEdits(); await tester.saveEdits();
@ -77,7 +77,7 @@ void testE2E(String description) {
await tester.enterProfileName(mockEquipmentProfiles[1].name); await tester.enterProfileName(mockEquipmentProfiles[1].name);
await tester.setApertureValues(mockEquipmentProfiles[1].apertureValues); await tester.setApertureValues(mockEquipmentProfiles[1].apertureValues);
await tester.setZoomValue(mockEquipmentProfiles[1].lensZoom); 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('f/3.5 - f/22'), findsOneWidget);
expect(find.text('1/1000 - B'), findsNWidgets(1)); expect(find.text('1/1000 - B'), findsNWidgets(1));
await tester.saveEdits(); await tester.saveEdits();
@ -305,7 +305,8 @@ Future<void> _expectMeteringState(
await tester.scrollToTheLastExposurePair(equipmentProfile: equipmentProfile); await tester.scrollToTheLastExposurePair(equipmentProfile: equipmentProfile);
expectExposurePairsListItem(tester, slowest.split(' - ')[0], slowest.split(' - ')[1]); expectExposurePairsListItem(tester, slowest.split(' - ')[0], slowest.split(' - ')[1]);
expectMeasureButton(ev); 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<void> _expectMeteringStateAndMeasure( Future<void> _expectMeteringStateAndMeasure(

View file

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lightmeter/providers/equipment_profile_provider.dart'; import 'package:lightmeter/providers/equipment_profile_provider.dart';
import 'package:lightmeter/providers/films_provider.dart'; import 'package:lightmeter/providers/films_provider.dart';
@ -115,7 +117,9 @@ final mockEquipmentProfiles = [
IsoValue(1600, StopType.full), IsoValue(1600, StopType.full),
IsoValue(3200, StopType.full), IsoValue(3200, StopType.full),
], ],
lensZoom: 1.91, lensZoom: Platform.isAndroid
? 2.083333 // Pixel 6
: 1.923, // iPhone 13 Pro
), ),
EquipmentProfile( EquipmentProfile(
id: '2', id: '2',
@ -145,7 +149,9 @@ final mockEquipmentProfiles = [
IsoValue(1600, StopType.full), IsoValue(1600, StopType.full),
IsoValue(3200, StopType.full), IsoValue(3200, StopType.full),
], ],
lensZoom: 5.02, lensZoom: Platform.isAndroid
? 5.625 // Pixel 6
: 5.1923, // iPhone 13 Pro
), ),
]; ];

View file

@ -1,12 +1,22 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart'; import 'package:integration_test/integration_test.dart';
import 'e2e_test.dart'; import 'e2e_test.dart';
import 'metering_screen_layout_test.dart'; import 'metering_screen_layout_test.dart';
import 'purchases_test.dart'; import 'purchases_test.dart';
import 'utils/platform_channel_mock.dart';
void main() { void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized(); IntegrationTestWidgetsFlutterBinding.ensureInitialized();
setUpAll(() {
mockCameraFocalLength();
});
tearDownAll(() {
mockCameraFocalLength();
});
testPurchases('Purchase & refund premium features'); testPurchases('Purchase & refund premium features');
testToggleLayoutFeatures('Toggle metering screen layout features'); testToggleLayoutFeatures('Toggle metering screen layout features');
testE2E('e2e'); testE2E('e2e');

View file

@ -1,8 +1,10 @@
import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:light_sensor/light_sensor.dart'; import 'package:light_sensor/light_sensor.dart';
import 'package:lightmeter/data/camera_info_service.dart';
void setLightSensorAvilability({required bool hasSensor}) { void setLightSensorAvilability({required bool hasSensor}) {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
@ -57,3 +59,26 @@ void resetLightSensorStreamHandler() {
null, 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,
);
}

View file

@ -7,6 +7,8 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* 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 */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
5A1AF02F1E4619A6E479AA8B /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C991C89D5763E562E77E475E /* Pods_Runner.framework */; }; 5A1AF02F1E4619A6E479AA8B /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C991C89D5763E562E77E475E /* Pods_Runner.framework */; };
@ -31,6 +33,8 @@
/* End PBXCopyFilesBuildPhase section */ /* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
001BDA022DD0BBA700122957 /* CameraInfoPlatformChannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraInfoPlatformChannel.swift; sourceTree = "<group>"; };
0035E99E2DD0B0700053508B /* CaffeinePlatformChannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaffeinePlatformChannel.swift; sourceTree = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
2913B1E428DD3F7921E898BC /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = "<group>"; }; 2913B1E428DD3F7921E898BC /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = "<group>"; };
@ -69,6 +73,15 @@
/* End PBXFrameworksBuildPhase section */ /* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */ /* Begin PBXGroup section */
0035E99D2DD0B05F0053508B /* PlatformChannels */ = {
isa = PBXGroup;
children = (
0035E99E2DD0B0700053508B /* CaffeinePlatformChannel.swift */,
001BDA022DD0BBA700122957 /* CameraInfoPlatformChannel.swift */,
);
path = PlatformChannels;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = { 9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -110,6 +123,7 @@
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
0035E99D2DD0B05F0053508B /* PlatformChannels */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
); );
path = Runner; path = Runner;
@ -156,7 +170,7 @@
45F53C083F2EA48EF231DA16 /* [CP] Embed Pods Frameworks */, 45F53C083F2EA48EF231DA16 /* [CP] Embed Pods Frameworks */,
FF00F85CE432774850A0EDB7 /* [firebase_crashlytics] Crashlytics Upload Symbols */, FF00F85CE432774850A0EDB7 /* [firebase_crashlytics] Crashlytics Upload Symbols */,
08127035D2CDEEEBA66FCDBB /* [CP] Copy Pods Resources */, 08127035D2CDEEEBA66FCDBB /* [CP] Copy Pods Resources */,
051083B0AECD5071E5FA2A6F /* FlutterFire: "flutterfire upload-crashlytics-symbols" */, 58ED09C740BAF94D922BAC2C /* FlutterFire: "flutterfire upload-crashlytics-symbols" */,
); );
buildRules = ( buildRules = (
); );
@ -216,24 +230,6 @@
/* End PBXResourcesBuildPhase section */ /* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase 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 */ = { 08127035D2CDEEEBA66FCDBB /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@ -306,6 +302,24 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0; 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 */ = { 9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1; alwaysOutOfDate = 1;
@ -319,7 +333,7 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; 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 */ = { FF00F85CE432774850A0EDB7 /* [firebase_crashlytics] Crashlytics Upload Symbols */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
@ -352,6 +366,8 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
001BDA042DD1252B00122957 /* CameraInfoPlatformChannel.swift in Sources */,
0035E99F2DD0B07C0053508B /* CaffeinePlatformChannel.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;

View file

@ -8,25 +8,8 @@ import Flutter
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool { ) -> Bool {
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let keepScreenOnChannel = FlutterMethodChannel(name: "com.vodemn.lightmeter/keepScreenOn", let caffeinePlatformChannel = CaffeinePlatformChannel(binaryMessenger: controller.binaryMessenger)
binaryMessenger: controller.binaryMessenger) let cameraInfoPlatformChannel = CameraInfoPlatformChannel(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)
}
})
GeneratedPluginRegistrant.register(with: self) GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions) return super.application(application, didFinishLaunchingWithOptions: launchOptions)
} }

View file

@ -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)
}
})
}
}

View file

@ -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
}
}

View file

@ -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/analytics.dart';
import 'package:lightmeter/data/analytics/api/analytics_firebase.dart'; import 'package:lightmeter/data/analytics/api/analytics_firebase.dart';
import 'package:lightmeter/data/caffeine_service.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/haptics_service.dart';
import 'package:lightmeter/data/light_sensor_service.dart'; import 'package:lightmeter/data/light_sensor_service.dart';
import 'package:lightmeter/data/permissions_service.dart'; import 'package:lightmeter/data/permissions_service.dart';
@ -99,11 +100,13 @@ class _ApplicationWrapperState extends State<ApplicationWrapper> {
await Future.wait([ await Future.wait([
SharedPreferences.getInstance(), SharedPreferences.getInstance(),
const LightSensorService(LocalPlatform()).hasSensor(), const LightSensorService(LocalPlatform()).hasSensor(),
const CameraInfoService().mainCameraEfl(),
remoteConfigService.activeAndFetchFeatures(), remoteConfigService.activeAndFetchFeatures(),
equipmentProfilesStorageService.init(), equipmentProfilesStorageService.init(),
filmsStorageService.init(), filmsStorageService.init(),
]).then((value) { ]).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; hasLightSensor = value[1] as bool? ?? false;
}); });
} }

View file

@ -1,15 +1,17 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
class CaffeineService { class CaffeineService {
static const _methodChannel = MethodChannel("com.vodemn.lightmeter/keepScreenOn"); @visibleForTesting
static const caffeineMethodChannel = MethodChannel("com.vodemn.lightmeter.CaffeinePlatformChannel.MethodChannel");
const CaffeineService(); const CaffeineService();
Future<bool> isKeepScreenOn() async { Future<bool> isKeepScreenOn() async {
return _methodChannel.invokeMethod<bool>("isKeepScreenOn").then((value) => value!); return caffeineMethodChannel.invokeMethod<bool>("isKeepScreenOn").then((value) => value!);
} }
Future<bool> keepScreenOn(bool keep) async { Future<bool> keepScreenOn(bool keep) async {
return _methodChannel.invokeMethod<bool>("setKeepScreenOn", keep).then((value) => value!); return caffeineMethodChannel.invokeMethod<bool>("setKeepScreenOn", keep).then((value) => value!);
} }
} }

View file

@ -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<int?> mainCameraEfl() async {
final focalLength = await cameraInfoPlatformChannel.invokeMethod<double?>('mainCameraEfl');
return focalLength?.round();
}
}

View file

@ -6,8 +6,8 @@ enum AppFeature {
incidedntLightMetering, incidedntLightMetering,
isoAndNdValues, isoAndNdValues,
themeEngine, themeEngine,
spotMetering, spotMeteringAndHistogram,
histogram, focalLength,
listOfFilms, listOfFilms,
customFilms, customFilms,
equipmentProfiles, equipmentProfiles,
@ -28,10 +28,10 @@ enum AppFeature {
return S.of(context).featureIsoAndNdValues; return S.of(context).featureIsoAndNdValues;
case AppFeature.themeEngine: case AppFeature.themeEngine:
return S.of(context).featureTheme; return S.of(context).featureTheme;
case AppFeature.spotMetering: case AppFeature.spotMeteringAndHistogram:
return S.of(context).featureSpotMetering; return S.of(context).featureSpotMeteringAndHistorgram;
case AppFeature.histogram: case AppFeature.focalLength:
return S.of(context).featureHistogram; return S.of(context).featureFocalLength35mm;
case AppFeature.listOfFilms: case AppFeature.listOfFilms:
return S.of(context).featureListOfFilms; return S.of(context).featureListOfFilms;
case AppFeature.customFilms: case AppFeature.customFilms:

View file

@ -1,13 +1,26 @@
enum CameraFeature { enum CameraFeature {
spotMetering, spotMetering,
histogram, histogram,
showFocalLength,
} }
typedef CameraFeaturesConfig = Map<CameraFeature, bool>; typedef CameraFeaturesConfig = Map<CameraFeature, bool>;
extension CameraFeaturesConfigJson on CameraFeaturesConfig { extension CameraFeaturesConfigJson on CameraFeaturesConfig {
static CameraFeaturesConfig fromJson(Map<String, dynamic> data) => static CameraFeaturesConfig fromJson(Map<String, dynamic> data) {
<CameraFeature, bool>{for (final f in CameraFeature.values) f: data[f.name] as bool? ?? false}; MapEntry<CameraFeature, bool> 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<String, dynamic> toJson() => map((key, value) => MapEntry(key.name, value)); Map<String, dynamic> toJson() => map((key, value) => MapEntry(key.name, value));
} }

View file

@ -22,6 +22,7 @@ class UserPreferencesService {
static const lightSensorEvCalibrationKey = "lightSensorEvCalibration"; static const lightSensorEvCalibrationKey = "lightSensorEvCalibration";
static const meteringScreenLayoutKey = "meteringScreenLayout"; static const meteringScreenLayoutKey = "meteringScreenLayout";
static const cameraFeaturesKey = "cameraFeatures"; static const cameraFeaturesKey = "cameraFeatures";
static const cameraFocalLengthKey = "cameraFocalLength";
static const caffeineKey = "caffeine"; static const caffeineKey = "caffeine";
static const hapticsKey = "haptics"; static const hapticsKey = "haptics";
@ -93,38 +94,24 @@ class UserPreferencesService {
set showEv100(bool value) => _sharedPreferences.setBool(showEv100Key, value); set showEv100(bool value) => _sharedPreferences.setBool(showEv100Key, value);
MeteringScreenLayoutConfig get meteringScreenLayout { MeteringScreenLayoutConfig get meteringScreenLayout {
final configJson = _sharedPreferences.getString(meteringScreenLayoutKey); final configJson = _sharedPreferences.getString(meteringScreenLayoutKey) ?? '{}';
if (configJson != null) { return MeteringScreenLayoutConfigJson.fromJson(json.decode(configJson) as Map<String, dynamic>);
return MeteringScreenLayoutConfigJson.fromJson(
json.decode(configJson) as Map<String, dynamic>,
);
} else {
return {
MeteringScreenLayoutFeature.equipmentProfiles: true,
MeteringScreenLayoutFeature.extremeExposurePairs: true,
MeteringScreenLayoutFeature.filmPicker: true,
};
}
} }
set meteringScreenLayout(MeteringScreenLayoutConfig value) => set meteringScreenLayout(MeteringScreenLayoutConfig value) =>
_sharedPreferences.setString(meteringScreenLayoutKey, json.encode(value.toJson())); _sharedPreferences.setString(meteringScreenLayoutKey, json.encode(value.toJson()));
CameraFeaturesConfig get cameraFeatures { CameraFeaturesConfig get cameraFeatures {
final configJson = _sharedPreferences.getString(cameraFeaturesKey); final configJson = _sharedPreferences.getString(cameraFeaturesKey) ?? '{}';
if (configJson != null) { return CameraFeaturesConfigJson.fromJson(json.decode(configJson) as Map<String, dynamic>);
return CameraFeaturesConfigJson.fromJson(json.decode(configJson) as Map<String, dynamic>);
} else {
return {
CameraFeature.spotMetering: false,
CameraFeature.histogram: false,
};
}
} }
set cameraFeatures(CameraFeaturesConfig value) => set cameraFeatures(CameraFeaturesConfig value) =>
_sharedPreferences.setString(cameraFeaturesKey, json.encode(value.toJson())); _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; bool get caffeine => _sharedPreferences.getBool(caffeineKey) ?? false;
set caffeine(bool value) => _sharedPreferences.setBool(caffeineKey, value); set caffeine(bool value) => _sharedPreferences.setBool(caffeineKey, value);

View file

@ -6,10 +6,10 @@ class VolumeEventsService {
final LocalPlatform _localPlatform; final LocalPlatform _localPlatform;
@visibleForTesting @visibleForTesting
static const volumeHandlingChannel = MethodChannel("com.vodemn.lightmeter/volumeHandling"); static const volumeMethodChannel = MethodChannel("com.vodemn.lightmeter.VolumePlatformChannel.MethodChannel");
@visibleForTesting @visibleForTesting
static const volumeEventsChannel = EventChannel("com.vodemn.lightmeter/volumeEvents"); static const volumeEventsChannel = EventChannel("com.vodemn.lightmeter.VolumePlatformChannel.EventChannel");
const VolumeEventsService(this._localPlatform); const VolumeEventsService(this._localPlatform);
@ -19,9 +19,7 @@ class VolumeEventsService {
if (!_localPlatform.isAndroid) { if (!_localPlatform.isAndroid) {
return false; return false;
} }
return volumeHandlingChannel return volumeMethodChannel.invokeMethod<bool>("setVolumeHandling", enableHandling).then((value) => value!);
.invokeMethod<bool>("setVolumeHandling", enableHandling)
.then((value) => value!);
} }
/// Emits new events on /// Emits new events on
@ -32,9 +30,6 @@ class VolumeEventsService {
if (!_localPlatform.isAndroid) { if (!_localPlatform.isAndroid) {
return const Stream.empty(); return const Stream.empty();
} }
return volumeEventsChannel return volumeEventsChannel.receiveBroadcastStream().cast<int>().where((event) => event == 24 || event == 25);
.receiveBroadcastStream()
.cast<int>()
.where((event) => event == 24 || event == 25);
} }
} }

View file

@ -30,8 +30,7 @@ class MeteringInteractor {
if (_userPreferencesService.caffeine) { if (_userPreferencesService.caffeine) {
_caffeineService.keepScreenOn(true); _caffeineService.keepScreenOn(true);
} }
_volumeEventsService _volumeEventsService.setVolumeHandling(_userPreferencesService.volumeAction != VolumeAction.none);
.setVolumeHandling(_userPreferencesService.volumeAction != VolumeAction.none);
} }
double get cameraEvCalibration => _userPreferencesService.cameraEvCalibration; double get cameraEvCalibration => _userPreferencesService.cameraEvCalibration;
@ -62,15 +61,11 @@ class MeteringInteractor {
} }
Future<bool> checkCameraPermission() async { Future<bool> checkCameraPermission() async {
return _permissionsService return _permissionsService.checkCameraPermission().then((value) => value == PermissionStatus.granted);
.checkCameraPermission()
.then((value) => value == PermissionStatus.granted);
} }
Future<bool> requestCameraPermission() async { Future<bool> requestCameraPermission() async {
return _permissionsService return _permissionsService.requestCameraPermission().then((value) => value == PermissionStatus.granted);
.requestCameraPermission()
.then((value) => value == PermissionStatus.granted);
} }
void openAppSettings() { void openAppSettings() {

View file

@ -45,6 +45,8 @@
"cameraFeatureSpotMeteringHint": "Halte die Kameraansicht gedrückt um den Messpunkt zu entfernen", "cameraFeatureSpotMeteringHint": "Halte die Kameraansicht gedrückt um den Messpunkt zu entfernen",
"cameraFeatureHistogram": "Histogramm", "cameraFeatureHistogram": "Histogramm",
"cameraFeatureHistogramHint": "Verwendung des Histogramms kann den Batterieverbrauch erhöhen", "cameraFeatureHistogramHint": "Verwendung des Histogramms kann den Batterieverbrauch erhöhen",
"cameraFeaturesShowFocalLength": "Brennweite anzeigen",
"cameraFeaturesShowFocalLengthHint": "Zeigt die 35mm-äquivalente Brennweite statt des Zoomfaktors an",
"film": "Film", "film": "Film",
"filmPush": "Film (push)", "filmPush": "Film (push)",
"filmPull": "Film (pull)", "filmPull": "Film (pull)",
@ -111,8 +113,8 @@
"featureIncidentLightMetering": "Messung von einfallendem Licht", "featureIncidentLightMetering": "Messung von einfallendem Licht",
"featureIsoAndNdValues": "Große Auswahl von ISO und ND Filtern", "featureIsoAndNdValues": "Große Auswahl von ISO und ND Filtern",
"featureTheme": "Theme Anpassung", "featureTheme": "Theme Anpassung",
"featureSpotMetering": "Punktmessung", "featureSpotMeteringAndHistorgram": "Spotmessung und Histogramm",
"featureHistogram": "Histogramm", "featureFocalLength35mm": "35mm-äquivalente Brennweite statt Zoom",
"featureListOfFilms": "Liste von 20+ Filmen mit Reziprozitätsformeln", "featureListOfFilms": "Liste von 20+ Filmen mit Reziprozitätsformeln",
"featureCustomFilms": "Eigene Filme erstellen", "featureCustomFilms": "Eigene Filme erstellen",
"featureEquipmentProfiles": "Ausrüstungsprofile", "featureEquipmentProfiles": "Ausrüstungsprofile",

View file

@ -45,6 +45,8 @@
"cameraFeatureSpotMeteringHint": "Long press the camera view to remove metering spot", "cameraFeatureSpotMeteringHint": "Long press the camera view to remove metering spot",
"cameraFeatureHistogram": "Histogram", "cameraFeatureHistogram": "Histogram",
"cameraFeatureHistogramHint": "Enabling histogram can encrease battery drain", "cameraFeatureHistogramHint": "Enabling histogram can encrease battery drain",
"cameraFeaturesShowFocalLength": "Show Focal Length",
"cameraFeaturesShowFocalLengthHint": "Displays 35mm equivalent focal length instead of zoom factor",
"film": "Film", "film": "Film",
"filmPush": "Film (push)", "filmPush": "Film (push)",
"filmPull": "Film (pull)", "filmPull": "Film (pull)",
@ -111,8 +113,8 @@
"featureIncidentLightMetering": "Incident light metering", "featureIncidentLightMetering": "Incident light metering",
"featureIsoAndNdValues": "Wide range of ISO and ND filters values", "featureIsoAndNdValues": "Wide range of ISO and ND filters values",
"featureTheme": "Theme customization", "featureTheme": "Theme customization",
"featureSpotMetering": "Spot metering", "featureSpotMeteringAndHistorgram": "Spot metering and histogram",
"featureHistogram": "Histogram", "featureFocalLength35mm": "35mm equivalent focal length instead of zoom",
"featureListOfFilms": "List of 20+ films with reciprocity formulas", "featureListOfFilms": "List of 20+ films with reciprocity formulas",
"featureCustomFilms": "Ability to create custom films", "featureCustomFilms": "Ability to create custom films",
"featureEquipmentProfiles": "Equipment profiles", "featureEquipmentProfiles": "Equipment profiles",

View file

@ -45,6 +45,8 @@
"cameraFeatureSpotMeteringHint": "Appuyez longuement sur la vue de l'appareil photo pour supprimer le spot de mesure", "cameraFeatureSpotMeteringHint": "Appuyez longuement sur la vue de l'appareil photo pour supprimer le spot de mesure",
"cameraFeatureHistogram": "Histogramme", "cameraFeatureHistogram": "Histogramme",
"cameraFeatureHistogramHint": "L'activation de l'histogramme peut augmenter la consommation de la batterie", "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", "film": "Pellicule",
"filmPush": "Pellicule (push)", "filmPush": "Pellicule (push)",
"filmPull": "Pellicule (pull)", "filmPull": "Pellicule (pull)",
@ -112,8 +114,8 @@
"featureIncidentLightMetering": "Mesure de la lumière incidente", "featureIncidentLightMetering": "Mesure de la lumière incidente",
"featureIsoAndNdValues": "Large gamme de valeurs ISO et de filtres ND", "featureIsoAndNdValues": "Large gamme de valeurs ISO et de filtres ND",
"featureTheme": "Personnalisation du thème", "featureTheme": "Personnalisation du thème",
"featureSpotMetering": "Mesure spot", "featureSpotMeteringAndHistorgram": "Mesure spot et histogramme",
"featureHistogram": "Histogramme", "featureFocalLength35mm": "Focale équivalente 35 mm au lieu du zoom",
"featureListOfFilms": "Liste de plus de 20 films avec des formules de correction", "featureListOfFilms": "Liste de plus de 20 films avec des formules de correction",
"featureCustomFilms": "Possibilité de créer des films personnalisés", "featureCustomFilms": "Possibilité de créer des films personnalisés",
"featureEquipmentProfiles": "Profils de l'équipement", "featureEquipmentProfiles": "Profils de l'équipement",

View file

@ -45,6 +45,8 @@
"cameraFeatureSpotMeteringHint": "Используйте долгое нажатие, чтобы удалить точку замера", "cameraFeatureSpotMeteringHint": "Используйте долгое нажатие, чтобы удалить точку замера",
"cameraFeatureHistogram": "Гистограмма", "cameraFeatureHistogram": "Гистограмма",
"cameraFeatureHistogramHint": "Использование гистограммы может увеличить расход аккумулятора", "cameraFeatureHistogramHint": "Использование гистограммы может увеличить расход аккумулятора",
"cameraFeaturesShowFocalLength": "Показать фокусное расстояние",
"cameraFeaturesShowFocalLengthHint": "Показывает эквивалент фокусного расстояния (35 мм) вместо коэффициента зума",
"film": "Пленка", "film": "Пленка",
"filmPush": "Пленка (push)", "filmPush": "Пленка (push)",
"filmPull": "Пленка (pull)", "filmPull": "Пленка (pull)",
@ -111,8 +113,8 @@
"featureIncidentLightMetering": "Замер падающего света", "featureIncidentLightMetering": "Замер падающего света",
"featureIsoAndNdValues": "Широкий диапазон значений ISO и фильтров ND", "featureIsoAndNdValues": "Широкий диапазон значений ISO и фильтров ND",
"featureTheme": "Настройка темы", "featureTheme": "Настройка темы",
"featureSpotMetering": "Точечный замер", "featureSpotMeteringAndHistorgram": "Точечный замер и гистограмма",
"featureHistogram": "Гистограмма", "featureFocalLength35mm": "Эквивалентное фокусное расстояние 35 мм вместо зума",
"featureListOfFilms": "Список из 20+ плёнок с формулами коррекции", "featureListOfFilms": "Список из 20+ плёнок с формулами коррекции",
"featureCustomFilms": "Возможность создания собственных плёнок", "featureCustomFilms": "Возможность создания собственных плёнок",
"featureEquipmentProfiles": "Профили оборудования", "featureEquipmentProfiles": "Профили оборудования",

View file

@ -45,6 +45,8 @@
"cameraFeatureSpotMeteringHint": "长按相机视图可移除测光点", "cameraFeatureSpotMeteringHint": "长按相机视图可移除测光点",
"cameraFeatureHistogram": "直方图", "cameraFeatureHistogram": "直方图",
"cameraFeatureHistogramHint": "启用直方图会增加电池消耗", "cameraFeatureHistogramHint": "启用直方图会增加电池消耗",
"cameraFeaturesShowFocalLength": "显示焦距",
"cameraFeaturesShowFocalLengthHint": "显示 35mm 等效焦距而非变焦倍数",
"film": "胶片", "film": "胶片",
"filmPush": "胶片 (push)", "filmPush": "胶片 (push)",
"filmPull": "胶片 (pull)", "filmPull": "胶片 (pull)",
@ -110,8 +112,8 @@
"featureIncidentLightMetering": "入射光测光", "featureIncidentLightMetering": "入射光测光",
"featureIsoAndNdValues": "更广的 ISO 和 ND 滤镜系数范围", "featureIsoAndNdValues": "更广的 ISO 和 ND 滤镜系数范围",
"featureTheme": "主题自定义", "featureTheme": "主题自定义",
"featureSpotMetering": "点测光", "featureSpotMeteringAndHistorgram": "点测光与直方图",
"featureHistogram": "直方图", "featureFocalLength35mm": "35mm 等效焦距替代变焦",
"featureListOfFilms": "20多种胶片的补偿公式", "featureListOfFilms": "20多种胶片的补偿公式",
"featureCustomFilms": "创建自定义胶片", "featureCustomFilms": "创建自定义胶片",
"featureEquipmentProfiles": "设备配置文件", "featureEquipmentProfiles": "设备配置文件",
@ -150,4 +152,4 @@
"filmFormulaExponentialRfPlaceholder": "1.3", "filmFormulaExponentialRfPlaceholder": "1.3",
"addEquipmentProfileTitle": "添加设备", "addEquipmentProfileTitle": "添加设备",
"editEquipmentProfileTitle": "编辑设备" "editEquipmentProfileTitle": "编辑设备"
} }

View file

@ -247,7 +247,7 @@ class _LensZoomListTileBuilder extends StatelessWidget {
description: S.of(context).lensZoomDescription, description: S.of(context).lensZoomDescription,
value: state.lensZoom, value: state.lensZoom,
range: const RangeValues(1, 7), range: const RangeValues(1, 7),
valueAdapter: (_, value) => value.toZoom(), valueAdapter: (context, value) => value.toZoom(context),
onChanged: (value) { onChanged: (value) {
context.read<EquipmentProfileEditBloc>().add(EquipmentProfileLensZoomChangedEvent(value)); context.read<EquipmentProfileEditBloc>().add(EquipmentProfileLensZoomChangedEvent(value));
}, },

View file

@ -4,6 +4,7 @@ import 'dart:io';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:camera/camera.dart'; import 'package:camera/camera.dart';
import 'package:exif/exif.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.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/models/camera_error_type.dart';
import 'package:lightmeter/screens/metering/components/camera_container/state_container_camera.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/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'; part 'mock_bloc_container_camera.part.dart';
@ -184,7 +185,8 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
_cameraController = cameraController; _cameraController = cameraController;
emit(CameraInitializedState(cameraController)); emit(CameraInitializedState(cameraController));
_emitActiveState(emit); _emitActiveState(emit);
} catch (e) { } catch (e, stackTrace) {
_analytics.logCrash(e, stackTrace);
emit(const CameraErrorState(CameraErrorType.other)); emit(const CameraErrorState(CameraErrorType.other));
} }
} }
@ -247,7 +249,8 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
final file = await _cameraController!.takePicture(); final file = await _cameraController!.takePicture();
final bytes = await file.readAsBytes(); final bytes = await file.readAsBytes();
Directory(file.path).deleteSync(recursive: true); Directory(file.path).deleteSync(recursive: true);
return await evFromImage(bytes); final tags = await readExifFromBytes(bytes);
return evFromTags(tags);
} catch (e, stackTrace) { } catch (e, stackTrace) {
_analytics.logCrash(e, stackTrace); _analytics.logCrash(e, stackTrace);
return null; return null;

View file

@ -24,7 +24,7 @@ class ZoomSlider extends StatelessWidget {
icon: Icons.search_outlined, icon: Icons.search_outlined,
defaultValue: EquipmentProfiles.selectedOf(context).lensZoom, defaultValue: EquipmentProfiles.selectedOf(context).lensZoom,
rulerValueAdapter: (value) => value.toStringAsFixed(0), rulerValueAdapter: (value) => value.toStringAsFixed(0),
valueAdapter: (value) => value.toZoom(), valueAdapter: (value) => value.toZoom(context),
); );
} }
} }

View file

@ -72,7 +72,8 @@ class MockCameraContainerBloc extends CameraContainerBloc {
Future<double?> _takePhoto() async { Future<double?> _takePhoto() async {
try { try {
final bytes = (await rootBundle.load(PlatformConfig.cameraStubImage)).buffer.asUint8List(); final bytes = (await rootBundle.load(PlatformConfig.cameraStubImage)).buffer.asUint8List();
return await evFromImage(bytes); final tags = await readExifFromBytes(bytes);
return evFromTags(tags);
} catch (e, stackTrace) { } catch (e, stackTrace) {
log(e.toString(), stackTrace: stackTrace); log(e.toString(), stackTrace: stackTrace);
return null; return null;

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/camera_feature.dart'; import 'package:lightmeter/data/models/camera_feature.dart';
import 'package:lightmeter/generated/l10n.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/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/dialog_switch/widget_dialog_switch.dart';
import 'package:lightmeter/screens/settings/components/shared/iap_list_tile/widget_list_tile_iap.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, icon: Icons.camera_alt_outlined,
title: S.of(context).cameraFeatures, title: S.of(context).cameraFeatures,
values: UserPreferencesProvider.cameraConfigOf(context), values: UserPreferencesProvider.cameraConfigOf(context),
titleAdapter: (context, feature) { enabledAdapter: (feature) => switch (feature) {
switch (feature) { CameraFeature.spotMetering => true,
case CameraFeature.spotMetering: CameraFeature.histogram => true,
return S.of(context).cameraFeatureSpotMetering; CameraFeature.showFocalLength =>
case CameraFeature.histogram: ServicesProvider.of(context).userPreferencesService.cameraFocalLength != null,
return S.of(context).cameraFeatureHistogram;
}
}, },
subtitleAdapter: (context, feature) { titleAdapter: (context, feature) => switch (feature) {
switch (feature) { CameraFeature.spotMetering => S.of(context).cameraFeatureSpotMetering,
case CameraFeature.spotMetering: CameraFeature.histogram => S.of(context).cameraFeatureHistogram,
return S.of(context).cameraFeatureSpotMeteringHint; CameraFeature.showFocalLength => S.of(context).cameraFeaturesShowFocalLength,
case CameraFeature.histogram: },
return S.of(context).cameraFeatureHistogramHint; 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, onSave: UserPreferencesProvider.of(context).setCameraFeature,
), ),

View file

@ -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 { 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)}';
}
}
} }

View file

@ -1,22 +1,20 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:exif/exif.dart'; import 'package:exif/exif.dart';
import 'package:flutter/foundation.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
const String _isoExifKey = 'EXIF ISOSpeedRatings'; const String _isoExifKey = 'EXIF ISOSpeedRatings';
const String _apertureExifKey = 'EXIF FNumber'; const String _apertureExifKey = 'EXIF FNumber';
const String _shutterSpeedExifKey = 'EXIF ExposureTime'; const String _shutterSpeedExifKey = 'EXIF ExposureTime';
Future<double> evFromImage(Uint8List bytes) async { double evFromTags(Map<String, IfdTag> tags) {
final tags = await readExifFromBytes(bytes);
final iso = double.tryParse("${tags[_isoExifKey]}"); final iso = double.tryParse("${tags[_isoExifKey]}");
final apertureValueRatio = (tags[_apertureExifKey]?.values as IfdRatios?)?.ratios.first; final apertureValueRatio = (tags[_apertureExifKey]?.values as IfdRatios?)?.ratios.first;
final speedValueRatio = (tags[_shutterSpeedExifKey]?.values as IfdRatios?)?.ratios.first; final speedValueRatio = (tags[_shutterSpeedExifKey]?.values as IfdRatios?)?.ratios.first;
if (iso == null || apertureValueRatio == null || speedValueRatio == null) { if (iso == null || apertureValueRatio == null || speedValueRatio == null) {
throw ArgumentError( throw ArgumentError(
'Error parsing EXIF', 'Error calculating EV',
[ [
if (iso == null) '$_isoExifKey: ${tags[_isoExifKey]?.printable} ${tags[_isoExifKey]?.printable.runtimeType}', if (iso == null) '$_isoExifKey: ${tags[_isoExifKey]?.printable} ${tags[_isoExifKey]?.printable.runtimeType}',
if (apertureValueRatio == null) '$_apertureExifKey: $apertureValueRatio', if (apertureValueRatio == null) '$_apertureExifKey: $apertureValueRatio',

View file

@ -52,7 +52,7 @@ Apple requires screenshots a specific list of devices, so we can implement a cus
Can be run on Simulator. Can be run on Simulator.
```console ```console
sh screenshots/generate_ios_screenshots.sh sh screenshots/scripts/generate_ios_screenshots.sh
``` ```
### Apply store constraints and text data ### Apply store constraints and text data

View file

@ -6,6 +6,7 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_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/ev_source_type.dart';
import 'package:lightmeter/data/models/exposure_pair.dart'; import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/data/models/metering_screen_layout_config.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 'package:shared_preferences/shared_preferences.dart';
import '../integration_test/mocks/paid_features_mock.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 '../integration_test/utils/widget_tester_actions.dart';
import 'models/screenshot_args.dart'; import 'models/screenshot_args.dart';
@ -68,6 +70,14 @@ void main() {
}.toJson(), }.toJson(),
), ),
UserPreferencesService.cameraFeaturesKey: json.encode(
{
CameraFeature.spotMetering: false,
CameraFeature.histogram: false,
CameraFeature.showFocalLength: true,
}.toJson(),
),
/// General settings /// General settings
UserPreferencesService.autostartTimerKey: false, UserPreferencesService.autostartTimerKey: false,
UserPreferencesService.caffeineKey: true, UserPreferencesService.caffeineKey: true,
@ -86,6 +96,11 @@ void main() {
setUpAll(() async { setUpAll(() async {
if (Platform.isAndroid) await binding.convertFlutterSurfaceToImage(); if (Platform.isAndroid) await binding.convertFlutterSurfaceToImage();
mockCameraFocalLength();
});
tearDownAll(() {
resetCameraFocalLength();
}); });
/// Generates several screenshots with the light theme /// Generates several screenshots with the light theme
@ -119,6 +134,9 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.tap(find.byIcon(Icons.edit_outlined).first); await tester.tap(find.byIcon(Icons.edit_outlined).first);
await tester.pumpAndSettle(); 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'); await tester.takeScreenshotLight(binding, 'equipment-profiles');
}); });

Binary file not shown.

Before

Width:  |  Height:  |  Size: 469 KiB

After

Width:  |  Height:  |  Size: 469 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 KiB

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 223 KiB

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 478 KiB

After

Width:  |  Height:  |  Size: 478 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 220 KiB

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 348 KiB

After

Width:  |  Height:  |  Size: 348 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 347 KiB

After

Width:  |  Height:  |  Size: 347 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 511 KiB

After

Width:  |  Height:  |  Size: 511 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 254 KiB

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 505 KiB

After

Width:  |  Height:  |  Size: 504 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 238 KiB

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

After

Width:  |  Height:  |  Size: 207 KiB

View file

@ -7,7 +7,6 @@ void main() {
late CaffeineService service; late CaffeineService service;
const methodChannel = MethodChannel('com.vodemn.lightmeter/keepScreenOn');
Future<Object?>? methodCallSuccessHandler(MethodCall methodCall) async { Future<Object?>? methodCallSuccessHandler(MethodCall methodCall) async {
switch (methodCall.method) { switch (methodCall.method) {
case "isKeepScreenOn": case "isKeepScreenOn":
@ -21,45 +20,57 @@ void main() {
setUp(() { setUp(() {
service = const CaffeineService(); service = const CaffeineService();
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
.setMockMethodCallHandler(methodChannel, methodCallSuccessHandler); CaffeineService.caffeineMethodChannel,
methodCallSuccessHandler,
);
}); });
tearDown(() { tearDown(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
.setMockMethodCallHandler(methodChannel, null); CaffeineService.caffeineMethodChannel,
null,
);
}); });
group( group(
'isKeepScreenOn()', 'isKeepScreenOn()',
() { () {
test('true', () async { test('true', () async {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
.setMockMethodCallHandler(methodChannel, null); CaffeineService.caffeineMethodChannel,
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger null,
.setMockMethodCallHandler(methodChannel, (methodCall) async { );
switch (methodCall.method) { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
case "isKeepScreenOn": CaffeineService.caffeineMethodChannel,
return true; (methodCall) async {
default: switch (methodCall.method) {
return null; case "isKeepScreenOn":
} return true;
}); default:
return null;
}
},
);
expectLater(service.isKeepScreenOn(), completion(true)); expectLater(service.isKeepScreenOn(), completion(true));
}); });
test('false', () async { test('false', () async {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
.setMockMethodCallHandler(methodChannel, null); CaffeineService.caffeineMethodChannel,
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger null,
.setMockMethodCallHandler(methodChannel, (methodCall) async { );
switch (methodCall.method) { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
case "isKeepScreenOn": CaffeineService.caffeineMethodChannel,
return false; (methodCall) async {
default: switch (methodCall.method) {
return null; case "isKeepScreenOn":
} return false;
}); default:
return null;
}
},
);
expectLater(service.isKeepScreenOn(), completion(false)); expectLater(service.isKeepScreenOn(), completion(false));
}); });
}, },

View file

@ -11,21 +11,24 @@ void main() {
{ {
'spotMetering': true, 'spotMetering': true,
'histogram': true, 'histogram': true,
'showFocalLength': true,
}, },
), ),
{ {
CameraFeature.spotMetering: true, CameraFeature.spotMetering: true,
CameraFeature.histogram: true, CameraFeature.histogram: true,
CameraFeature.showFocalLength: true,
}, },
); );
}); });
test('Legacy (no spotMetering & histogram)', () { test('Legacy', () {
expect( expect(
CameraFeaturesConfigJson.fromJson({}), CameraFeaturesConfigJson.fromJson({}),
{ {
CameraFeature.spotMetering: false, CameraFeature.spotMetering: true,
CameraFeature.histogram: false, CameraFeature.histogram: false,
CameraFeature.showFocalLength: true,
}, },
); );
}); });
@ -37,10 +40,12 @@ void main() {
{ {
CameraFeature.spotMetering: true, CameraFeature.spotMetering: true,
CameraFeature.histogram: true, CameraFeature.histogram: true,
CameraFeature.showFocalLength: true,
}.toJson(), }.toJson(),
{ {
'spotMetering': true, 'spotMetering': true,
'histogram': true, 'histogram': true,
'showFocalLength': true,
}, },
); );
}); });

View file

@ -270,8 +270,9 @@ void main() {
expect( expect(
service.cameraFeatures, service.cameraFeatures,
{ {
CameraFeature.spotMetering: false, CameraFeature.spotMetering: true,
CameraFeature.histogram: false, CameraFeature.histogram: false,
CameraFeature.showFocalLength: true,
}, },
); );
}); });
@ -284,6 +285,7 @@ void main() {
{ {
CameraFeature.spotMetering: false, CameraFeature.spotMetering: false,
CameraFeature.histogram: true, CameraFeature.histogram: true,
CameraFeature.showFocalLength: true,
}, },
); );
}); });
@ -292,17 +294,18 @@ void main() {
when( when(
() => sharedPreferences.setString( () => sharedPreferences.setString(
UserPreferencesService.cameraFeaturesKey, UserPreferencesService.cameraFeaturesKey,
"""{"spotMetering":false,"histogram":true}""", """{"spotMetering":false,"histogram":true,"showFocalLength":true}""",
), ),
).thenAnswer((_) => Future.value(true)); ).thenAnswer((_) => Future.value(true));
service.cameraFeatures = { service.cameraFeatures = {
CameraFeature.spotMetering: false, CameraFeature.spotMetering: false,
CameraFeature.histogram: true, CameraFeature.histogram: true,
CameraFeature.showFocalLength: true,
}; };
verify( verify(
() => sharedPreferences.setString( () => sharedPreferences.setString(
UserPreferencesService.cameraFeaturesKey, UserPreferencesService.cameraFeaturesKey,
"""{"spotMetering":false,"histogram":true}""", """{"spotMetering":false,"histogram":true,"showFocalLength":true}""",
), ),
).called(1); ).called(1);
}); });

View file

@ -27,14 +27,14 @@ void main() {
localPlatform = _MockLocalPlatform(); localPlatform = _MockLocalPlatform();
service = VolumeEventsService(localPlatform); service = VolumeEventsService(localPlatform);
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
VolumeEventsService.volumeHandlingChannel, VolumeEventsService.volumeMethodChannel,
methodCallSuccessHandler, methodCallSuccessHandler,
); );
}); });
tearDown(() { tearDown(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
VolumeEventsService.volumeHandlingChannel, VolumeEventsService.volumeMethodChannel,
null, null,
); );
}); });

View file

@ -37,6 +37,7 @@ void main() {
when(() => mockUserPreferencesService.cameraFeatures).thenReturn({ when(() => mockUserPreferencesService.cameraFeatures).thenReturn({
CameraFeature.spotMetering: true, CameraFeature.spotMetering: true,
CameraFeature.histogram: true, CameraFeature.histogram: true,
CameraFeature.showFocalLength: true,
}); });
when(() => mockUserPreferencesService.locale).thenReturn(SupportedLocale.en); when(() => mockUserPreferencesService.locale).thenReturn(SupportedLocale.en);
when(() => mockUserPreferencesService.themeType).thenReturn(ThemeType.light); when(() => mockUserPreferencesService.themeType).thenReturn(ThemeType.light);
@ -227,13 +228,6 @@ void main() {
expect(find.text("${MeteringScreenLayoutFeature.equipmentProfiles}: true"), findsNWidgets(2)); expect(find.text("${MeteringScreenLayoutFeature.equipmentProfiles}: true"), findsNWidgets(2));
expect(find.text("${MeteringScreenLayoutFeature.extremeExposurePairs}: false"), findsNWidgets(2)); expect(find.text("${MeteringScreenLayoutFeature.extremeExposurePairs}: false"), findsNWidgets(2));
expect(find.text("${MeteringScreenLayoutFeature.filmPicker}: 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({ onPressed: () => UserPreferencesProvider.of(context).setCameraFeature({
CameraFeature.spotMetering: true, CameraFeature.spotMetering: true,
CameraFeature.histogram: false, CameraFeature.histogram: false,
CameraFeature.showFocalLength: false,
}), }),
child: const Text(''), 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 // 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.spotMetering}: true"), findsNWidgets(2));
expect(find.text("${CameraFeature.histogram}: 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.tap(find.byType(ElevatedButton));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text("${CameraFeature.spotMetering}: true"), findsNWidgets(2)); expect(find.text("${CameraFeature.spotMetering}: true"), findsNWidgets(2));
expect(find.text("${CameraFeature.histogram}: false"), findsNWidgets(2)); expect(find.text("${CameraFeature.histogram}: false"), findsNWidgets(2));
expect(find.text("${CameraFeature.showFocalLength}: false"), findsNWidgets(2));
verify( verify(
() => mockUserPreferencesService.cameraFeatures = { () => mockUserPreferencesService.cameraFeatures = {
CameraFeature.spotMetering: true, CameraFeature.spotMetering: true,
CameraFeature.histogram: false, CameraFeature.histogram: false,
CameraFeature.showFocalLength: false,
}, },
).called(1); ).called(1);
}, },

Binary file not shown.

Before

Width:  |  Height:  |  Size: 301 KiB

After

Width:  |  Height:  |  Size: 321 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

View file

@ -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);
},
);
});
}

View file

@ -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);
},
);
});
}