mirror of
https://github.com/vodemn/m3_lightmeter.git
synced 2025-07-07 06:30:42 +00:00
Compare commits
No commits in common. "e27ad9da8573c64bd39ecc7bfe6492a22e437137" and "602e31f425d13f40693581560a46ebb13b1ec11b" have entirely different histories.
e27ad9da85
...
602e31f425
26 changed files with 355 additions and 876 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -60,4 +60,3 @@ ios/Runner/GoogleService-Info.plist
|
|||
/lib/firebase_options.dart
|
||||
|
||||
coverage/
|
||||
screenshots/
|
Binary file not shown.
Before Width: | Height: | Size: 899 KiB |
|
@ -6,26 +6,8 @@ enum IAPProductStatus {
|
|||
|
||||
enum IAPProductType { paidFeatures }
|
||||
|
||||
class IAPProduct {
|
||||
final String storeId;
|
||||
final IAPProductStatus status;
|
||||
abstract class IAPProduct {
|
||||
const IAPProduct._();
|
||||
|
||||
const IAPProduct({
|
||||
required this.storeId,
|
||||
this.status = IAPProductStatus.purchasable,
|
||||
});
|
||||
|
||||
IAPProduct copyWith({IAPProductStatus? status}) => IAPProduct(
|
||||
storeId: storeId,
|
||||
status: status ?? this.status,
|
||||
);
|
||||
}
|
||||
|
||||
extension IAPProductTypeExtension on IAPProductType {
|
||||
String get storeId {
|
||||
switch (this) {
|
||||
case IAPProductType.paidFeatures:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
IAPProductStatus get status => IAPProductStatus.purchasable;
|
||||
}
|
||||
|
|
|
@ -18,12 +18,7 @@ class IAPProductsProviderState extends State<IAPProductsProvider> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IAPProducts(
|
||||
products: [
|
||||
IAPProduct(
|
||||
storeId: IAPProductType.paidFeatures.storeId,
|
||||
status: IAPProductStatus.purchased,
|
||||
)
|
||||
],
|
||||
products: const [],
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
@ -40,28 +35,13 @@ class IAPProducts extends InheritedModel<IAPProductType> {
|
|||
super.key,
|
||||
});
|
||||
|
||||
static IAPProduct? productOf(BuildContext context, IAPProductType type) {
|
||||
final IAPProducts? result = InheritedModel.inheritFrom<IAPProducts>(context, aspect: type);
|
||||
return result!._findProduct(type);
|
||||
}
|
||||
static IAPProduct? productOf(BuildContext context, IAPProductType type) => null;
|
||||
|
||||
static bool isPurchased(BuildContext context, IAPProductType type) {
|
||||
final IAPProducts? result = InheritedModel.inheritFrom<IAPProducts>(context, aspect: type);
|
||||
return result!._findProduct(type)?.status == IAPProductStatus.purchased;
|
||||
}
|
||||
static bool isPurchased(BuildContext context, IAPProductType type) => false;
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(IAPProducts oldWidget) => false;
|
||||
|
||||
@override
|
||||
bool updateShouldNotifyDependent(IAPProducts oldWidget, Set<IAPProductType> dependencies) =>
|
||||
false;
|
||||
|
||||
IAPProduct? _findProduct(IAPProductType type) {
|
||||
try {
|
||||
return products.firstWhere((element) => element.storeId == type.storeId);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
bool updateShouldNotifyDependent(covariant IAPProducts oldWidget, Set<IAPProductType> dependencies) => false;
|
||||
}
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
timeline_name="$1"
|
||||
csv_filename="$2"
|
||||
|
||||
if [[ -n "$timeline_name" ]]; then
|
||||
if [[ -z "$csv_filename" ]]; then
|
||||
csv_filename="${timeline_name}"
|
||||
fi
|
||||
|
||||
echo "====== Extracting & merging ${timeline_name} timelines ======"
|
||||
echo "" >>${csv_filename}.csv
|
||||
echo "${timeline_name}" >>${csv_filename}.csv
|
||||
|
||||
extent_micros="timeExtentMicros"
|
||||
timelines=$(find ./build -maxdepth 1 -name "${timeline_name}*.timeline.json" -print)
|
||||
for i in "${timelines[@]}"; do
|
||||
benchextract $i
|
||||
extent_micros+="$(grep -A0 -h '"timeExtentMicros":' $i | grep -o " [0-9]*")"
|
||||
done
|
||||
echo $extent_micros | tr ' ' ',' >>${csv_filename}.csv
|
||||
|
||||
metrics=(
|
||||
"average_frame_build_time_millis"
|
||||
"90th_percentile_frame_build_time_millis"
|
||||
"99th_percentile_frame_build_time_millis"
|
||||
"worst_frame_build_time_millis"
|
||||
"average_rasterizer_time_millis"
|
||||
"90th_percentile_rasterizer_time_millis"
|
||||
"99th_percentile_rasterizer_time_millis"
|
||||
"worst_frame_rasterizer_time_millis"
|
||||
)
|
||||
|
||||
timeline_summaries=$(find ./build -maxdepth 1 -name "${timeline_name}*.timeline_summary.json" -print)
|
||||
for i in "${timeline_summaries[@]}"; do
|
||||
metrics[0]+="$(grep -A0 -h '"average_frame_build_time_millis":' $i | grep -o " [0-9.]*")"
|
||||
metrics[1]+="$(grep -A0 -h '"90th_percentile_frame_build_time_millis":' $i | grep -o " [0-9.]*")"
|
||||
metrics[2]+="$(grep -A0 -h '"99th_percentile_frame_build_time_millis":' $i | grep -o " [0-9.]*")"
|
||||
metrics[3]+="$(grep -A0 -h '"worst_frame_build_time_millis":' $i | grep -o " [0-9.]*")"
|
||||
metrics[4]+="$(grep -A0 -h '"average_frame_rasterizer_time_millis":' $i | grep -o " [0-9.]*")"
|
||||
metrics[5]+="$(grep -A0 -h '"90th_percentile_frame_rasterizer_time_millis":' $i | grep -o " [0-9.]*")"
|
||||
metrics[6]+="$(grep -A0 -h '"99th_percentile_frame_rasterizer_time_millis":' $i | grep -o " [0-9.]*")"
|
||||
metrics[7]+="$(grep -A0 -h '"worst_frame_rasterizer_time_millis":' $i | grep -o " [0-9.]*")"
|
||||
done
|
||||
for metric in "${metrics[@]}"; do
|
||||
echo $metric | tr ' ' ',' >>${csv_filename}.csv
|
||||
done
|
||||
|
||||
benchmarks=$(find ./build -maxdepth 1 -name "${timeline_name}*.timeline.benchmark" -print)
|
||||
benchmerge $benchmarks
|
||||
else
|
||||
echo "Provide the timeline name"
|
||||
fi
|
|
@ -1,249 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:lightmeter/application.dart';
|
||||
import 'package:lightmeter/data/caffeine_service.dart';
|
||||
import 'package:lightmeter/data/haptics_service.dart';
|
||||
import 'package:lightmeter/data/light_sensor_service.dart';
|
||||
import 'package:lightmeter/data/models/ev_source_type.dart';
|
||||
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
|
||||
import 'package:lightmeter/data/models/supported_locale.dart';
|
||||
import 'package:lightmeter/data/models/theme_type.dart';
|
||||
import 'package:lightmeter/data/models/volume_action.dart';
|
||||
import 'package:lightmeter/data/permissions_service.dart';
|
||||
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||
import 'package:lightmeter/data/volume_events_service.dart';
|
||||
import 'package:lightmeter/environment.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/res/dimens.dart';
|
||||
import 'package:lightmeter/res/theme.dart';
|
||||
import 'package:lightmeter/screens/metering/components/bottom_controls/components/measure_button/widget_button_measure.dart';
|
||||
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/iso_picker/widget_picker_iso.dart';
|
||||
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/dialog_picker/widget_picker_dialog.dart';
|
||||
import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart';
|
||||
import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart';
|
||||
import 'package:lightmeter/screens/settings/screen_settings.dart';
|
||||
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'utils/widget_tester_extension.dart';
|
||||
|
||||
class _MockSharedPreferences extends Mock implements SharedPreferences {}
|
||||
|
||||
class _MockUserPreferencesService extends Mock implements UserPreferencesService {}
|
||||
|
||||
class _MockCaffeineService extends Mock implements CaffeineService {}
|
||||
|
||||
class _MockHapticsService extends Mock implements HapticsService {}
|
||||
|
||||
class _MockPermissionsService extends Mock implements PermissionsService {}
|
||||
|
||||
class _MockLightSensorService extends Mock implements LightSensorService {}
|
||||
|
||||
class _MockVolumeEventsService extends Mock implements VolumeEventsService {}
|
||||
|
||||
//https://stackoverflow.com/a/67186625/13167574
|
||||
void main() {
|
||||
late _MockUserPreferencesService mockUserPreferencesService;
|
||||
late _MockCaffeineService mockCaffeineService;
|
||||
late _MockHapticsService mockHapticsService;
|
||||
late _MockPermissionsService mockPermissionsService;
|
||||
late _MockLightSensorService mockLightSensorService;
|
||||
late _MockVolumeEventsService mockVolumeEventsService;
|
||||
|
||||
final binding = IntegrationTestWidgetsFlutterBinding();
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUpAll(() {
|
||||
mockUserPreferencesService = _MockUserPreferencesService();
|
||||
when(() => mockUserPreferencesService.evSourceType).thenReturn(EvSourceType.camera);
|
||||
when(() => mockUserPreferencesService.stopType).thenReturn(StopType.third);
|
||||
when(() => mockUserPreferencesService.locale).thenReturn(SupportedLocale.en);
|
||||
when(() => mockUserPreferencesService.caffeine).thenReturn(true);
|
||||
when(() => mockUserPreferencesService.volumeAction).thenReturn(VolumeAction.shutter);
|
||||
when(() => mockUserPreferencesService.cameraEvCalibration).thenReturn(0.0);
|
||||
when(() => mockUserPreferencesService.lightSensorEvCalibration).thenReturn(0.0);
|
||||
when(() => mockUserPreferencesService.iso).thenReturn(const IsoValue(400, StopType.full));
|
||||
when(() => mockUserPreferencesService.ndFilter).thenReturn(NdValue.values.first);
|
||||
when(() => mockUserPreferencesService.haptics).thenReturn(true);
|
||||
when(() => mockUserPreferencesService.meteringScreenLayout).thenReturn({
|
||||
MeteringScreenLayoutFeature.equipmentProfiles: true,
|
||||
MeteringScreenLayoutFeature.extremeExposurePairs: true,
|
||||
MeteringScreenLayoutFeature.filmPicker: true,
|
||||
MeteringScreenLayoutFeature.histogram: false,
|
||||
});
|
||||
when(() => mockUserPreferencesService.themeType).thenReturn(ThemeType.light);
|
||||
when(() => mockUserPreferencesService.dynamicColor).thenReturn(false);
|
||||
|
||||
mockCaffeineService = _MockCaffeineService();
|
||||
when(() => mockCaffeineService.isKeepScreenOn()).thenAnswer((_) async => false);
|
||||
when(() => mockCaffeineService.keepScreenOn(true)).thenAnswer((_) async => true);
|
||||
when(() => mockCaffeineService.keepScreenOn(false)).thenAnswer((_) async => false);
|
||||
|
||||
mockHapticsService = _MockHapticsService();
|
||||
when(() => mockHapticsService.quickVibration()).thenAnswer((_) async {});
|
||||
when(() => mockHapticsService.responseVibration()).thenAnswer((_) async {});
|
||||
when(() => mockHapticsService.errorVibration()).thenAnswer((_) async {});
|
||||
|
||||
mockPermissionsService = _MockPermissionsService();
|
||||
when(() => mockPermissionsService.requestCameraPermission())
|
||||
.thenAnswer((_) async => PermissionStatus.granted);
|
||||
when(() => mockPermissionsService.checkCameraPermission())
|
||||
.thenAnswer((_) async => PermissionStatus.granted);
|
||||
|
||||
mockLightSensorService = _MockLightSensorService();
|
||||
when(() => mockLightSensorService.hasSensor()).thenAnswer((_) async => true);
|
||||
when(() => mockLightSensorService.luxStream()).thenAnswer((_) => Stream.fromIterable([100]));
|
||||
|
||||
mockVolumeEventsService = _MockVolumeEventsService();
|
||||
when(() => mockVolumeEventsService.setVolumeHandling(true)).thenAnswer((_) async => true);
|
||||
when(() => mockVolumeEventsService.setVolumeHandling(false)).thenAnswer((_) async => false);
|
||||
when(() => mockVolumeEventsService.volumeButtonsEventStream())
|
||||
.thenAnswer((_) => const Stream<int>.empty());
|
||||
|
||||
when(() => mockHapticsService.quickVibration()).thenAnswer((_) async {});
|
||||
when(() => mockHapticsService.responseVibration()).thenAnswer((_) async {});
|
||||
});
|
||||
|
||||
Future<void> pumpApplication(WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
IAPProviders(
|
||||
sharedPreferences: _MockSharedPreferences(),
|
||||
child: EquipmentProfiles(
|
||||
selected: _mockEquipmentProfiles[0],
|
||||
values: _mockEquipmentProfiles,
|
||||
child: Films(
|
||||
selected: const Film('Ilford HP5+', 400),
|
||||
values: const [Film.other(), Film('Ilford HP5+', 400)],
|
||||
filmsInUse: const [Film.other(), Film('Ilford HP5+', 400)],
|
||||
child: ServicesProvider(
|
||||
environment: const Environment.prod().copyWith(hasLightSensor: true),
|
||||
userPreferencesService: mockUserPreferencesService,
|
||||
caffeineService: mockCaffeineService,
|
||||
hapticsService: mockHapticsService,
|
||||
permissionsService: mockPermissionsService,
|
||||
lightSensorService: mockLightSensorService,
|
||||
volumeEventsService: mockVolumeEventsService,
|
||||
child: const UserPreferencesProvider(child: Application()),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
/// Generates several screenshots with the light theme
|
||||
/// and the additionally the first one with the dark theme
|
||||
void generateScreenshots(Color color) {
|
||||
testWidgets('${color.value}_light', (tester) async {
|
||||
when(() => mockUserPreferencesService.themeType).thenReturn(ThemeType.light);
|
||||
when(() => mockUserPreferencesService.primaryColor).thenReturn(color);
|
||||
await pumpApplication(tester);
|
||||
|
||||
await tester.takePhoto();
|
||||
await tester.takeScreenshot(binding, '${color.value}_metering_reflected');
|
||||
|
||||
await tester.tap(find.byTooltip(S.current.tooltipUseLightSensor));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byType(MeteringMeasureButton));
|
||||
await tester.tap(find.byType(MeteringMeasureButton));
|
||||
await tester.takeScreenshot(binding, '${color.value}_metering_incident');
|
||||
|
||||
expect(find.byType(IsoValuePicker), findsOneWidget);
|
||||
await tester.tap(find.byType(IsoValuePicker));
|
||||
await tester.pumpAndSettle(Dimens.durationL);
|
||||
expect(find.byType(DialogPicker<IsoValue>), findsOneWidget);
|
||||
await tester.takeScreenshot(binding, '${color.value}_metering_iso_picker');
|
||||
|
||||
await tester.tapCancelButton();
|
||||
expect(find.byType(DialogPicker<IsoValue>), findsNothing);
|
||||
await tester.openSettings();
|
||||
await tester.takeScreenshot(binding, '${color.value}_settings');
|
||||
|
||||
await tester.tapListTile(S.current.meteringScreenLayout);
|
||||
await tester.takeScreenshot(binding, '${color.value}_settings_metering_screen_layout');
|
||||
|
||||
await tester.tapCancelButton();
|
||||
await tester.tapListTile(S.current.equipmentProfiles);
|
||||
expect(find.byType(EquipmentProfilesScreen), findsOneWidget);
|
||||
await tester.tap(find.byType(EquipmentProfileContainer).first);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.takeScreenshot(binding, '${color.value}-equipment_profiles');
|
||||
|
||||
await tester.tap(find.byIcon(Icons.iso).first);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.takeScreenshot(binding, '${color.value}_equipment_profiles_iso_picker');
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'${color.value}_dark',
|
||||
(tester) async {
|
||||
when(() => mockUserPreferencesService.themeType).thenReturn(ThemeType.dark);
|
||||
when(() => mockUserPreferencesService.primaryColor).thenReturn(color);
|
||||
await pumpApplication(tester);
|
||||
|
||||
await tester.takePhoto();
|
||||
await tester.takeScreenshot(binding, '${color.value}_metering_reflected_dark');
|
||||
|
||||
await tester.tap(find.byTooltip(S.current.tooltipUseLightSensor));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byType(MeteringMeasureButton));
|
||||
await tester.tap(find.byType(MeteringMeasureButton));
|
||||
await tester.takeScreenshot(binding, '${color.value}_metering_incident_dark');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
generateScreenshots(primaryColorsList[5]);
|
||||
generateScreenshots(primaryColorsList[3]);
|
||||
generateScreenshots(primaryColorsList[9]);
|
||||
}
|
||||
|
||||
final _mockEquipmentProfiles = [
|
||||
const EquipmentProfile(
|
||||
id: '',
|
||||
name: '',
|
||||
apertureValues: ApertureValue.values,
|
||||
ndValues: NdValue.values,
|
||||
shutterSpeedValues: ShutterSpeedValue.values,
|
||||
isoValues: IsoValue.values,
|
||||
),
|
||||
EquipmentProfile(
|
||||
id: '1',
|
||||
name: 'Praktica + Zenitar',
|
||||
apertureValues: ApertureValue.values.sublist(
|
||||
ApertureValue.values.indexOf(const ApertureValue(1.7, StopType.half)),
|
||||
ApertureValue.values.indexOf(const ApertureValue(16, StopType.full)) + 1,
|
||||
),
|
||||
ndValues: NdValue.values.sublist(0, 3),
|
||||
shutterSpeedValues: ShutterSpeedValue.values.sublist(
|
||||
ShutterSpeedValue.values.indexOf(const ShutterSpeedValue(1000, true, StopType.full)),
|
||||
ShutterSpeedValue.values.indexOf(const ShutterSpeedValue(16, false, StopType.full)) + 1,
|
||||
),
|
||||
isoValues: const [
|
||||
IsoValue(50, StopType.full),
|
||||
IsoValue(100, StopType.full),
|
||||
IsoValue(200, StopType.full),
|
||||
IsoValue(250, StopType.third),
|
||||
IsoValue(400, StopType.full),
|
||||
IsoValue(500, StopType.third),
|
||||
IsoValue(800, StopType.full),
|
||||
IsoValue(1600, StopType.full),
|
||||
IsoValue(3200, StopType.full),
|
||||
],
|
||||
),
|
||||
const EquipmentProfile(
|
||||
id: '2',
|
||||
name: 'Praktica + Jupiter',
|
||||
apertureValues: ApertureValue.values,
|
||||
ndValues: NdValue.values,
|
||||
shutterSpeedValues: ShutterSpeedValue.values,
|
||||
isoValues: IsoValue.values,
|
||||
),
|
||||
];
|
|
@ -1,10 +0,0 @@
|
|||
flutter drive \
|
||||
--dart-define="cameraPreviewAspectRatio=240/320" \
|
||||
--dart-define="cameraStubImage=assets/camera_stub_image.jpg" \
|
||||
--driver=test_driver/screenshot_driver.dart \
|
||||
--target=integration_test/generate_screenshots.dart \
|
||||
--profile \
|
||||
--flavor=dev \
|
||||
--no-dds \
|
||||
--endless-trace-buffer \
|
||||
--purge-persistent-cache
|
|
@ -1,24 +0,0 @@
|
|||
flutter --version
|
||||
fvm flutter build apk \
|
||||
--dart-define="cameraPreviewAspectRatio=240/320" \
|
||||
--dart-define="cameraStubImage=assets/camera_stub_image.jpg" \
|
||||
--target=integration_test/generate_screenshots.dart \
|
||||
--profile \
|
||||
--flavor=dev
|
||||
|
||||
for n in {1..5}; do
|
||||
echo "============ Run number ${n} ============"
|
||||
|
||||
flutter drive \
|
||||
--dart-define="cameraPreviewAspectRatio=240/320" \
|
||||
--dart-define="cameraStubImage=assets/camera_stub_image.jpg" \
|
||||
--driver=test_driver/screenshot_driver.dart \
|
||||
--target=integration_test/generate_screenshots.dart \
|
||||
--profile \
|
||||
--flavor=dev \
|
||||
--no-dds \
|
||||
--endless-trace-buffer \
|
||||
--purge-persistent-cache \
|
||||
--use-application-binary=build/app/outputs/flutter-apk/app-dev-profile.apk
|
||||
done
|
||||
|
|
@ -1,209 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:lightmeter/application.dart';
|
||||
import 'package:lightmeter/data/caffeine_service.dart';
|
||||
import 'package:lightmeter/data/haptics_service.dart';
|
||||
import 'package:lightmeter/data/light_sensor_service.dart';
|
||||
import 'package:lightmeter/data/models/ev_source_type.dart';
|
||||
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
|
||||
import 'package:lightmeter/data/models/supported_locale.dart';
|
||||
import 'package:lightmeter/data/models/theme_type.dart';
|
||||
import 'package:lightmeter/data/models/volume_action.dart';
|
||||
import 'package:lightmeter/data/permissions_service.dart';
|
||||
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||
import 'package:lightmeter/data/volume_events_service.dart';
|
||||
import 'package:lightmeter/environment.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/res/dimens.dart';
|
||||
import 'package:lightmeter/res/theme.dart';
|
||||
import 'package:lightmeter/screens/metering/components/bottom_controls/components/measure_button/widget_button_measure.dart';
|
||||
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/iso_picker/widget_picker_iso.dart';
|
||||
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/dialog_picker/widget_picker_dialog.dart';
|
||||
import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart';
|
||||
import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart';
|
||||
import 'package:lightmeter/screens/settings/screen_settings.dart';
|
||||
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'utils/widget_tester_extension.dart';
|
||||
|
||||
class _MockSharedPreferences extends Mock implements SharedPreferences {}
|
||||
|
||||
class _MockUserPreferencesService extends Mock implements UserPreferencesService {}
|
||||
|
||||
class _MockCaffeineService extends Mock implements CaffeineService {}
|
||||
|
||||
class _MockHapticsService extends Mock implements HapticsService {}
|
||||
|
||||
class _MockPermissionsService extends Mock implements PermissionsService {}
|
||||
|
||||
class _MockLightSensorService extends Mock implements LightSensorService {}
|
||||
|
||||
class _MockVolumeEventsService extends Mock implements VolumeEventsService {}
|
||||
|
||||
//https://stackoverflow.com/a/67186625/13167574
|
||||
void main() {
|
||||
late _MockUserPreferencesService mockUserPreferencesService;
|
||||
late _MockCaffeineService mockCaffeineService;
|
||||
late _MockHapticsService mockHapticsService;
|
||||
late _MockPermissionsService mockPermissionsService;
|
||||
late _MockLightSensorService mockLightSensorService;
|
||||
late _MockVolumeEventsService mockVolumeEventsService;
|
||||
|
||||
final binding = IntegrationTestWidgetsFlutterBinding();
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUpAll(() {
|
||||
mockUserPreferencesService = _MockUserPreferencesService();
|
||||
when(() => mockUserPreferencesService.evSourceType).thenReturn(EvSourceType.camera);
|
||||
when(() => mockUserPreferencesService.stopType).thenReturn(StopType.third);
|
||||
when(() => mockUserPreferencesService.locale).thenReturn(SupportedLocale.en);
|
||||
when(() => mockUserPreferencesService.caffeine).thenReturn(true);
|
||||
when(() => mockUserPreferencesService.volumeAction).thenReturn(VolumeAction.shutter);
|
||||
when(() => mockUserPreferencesService.cameraEvCalibration).thenReturn(0.0);
|
||||
when(() => mockUserPreferencesService.lightSensorEvCalibration).thenReturn(0.0);
|
||||
when(() => mockUserPreferencesService.iso).thenReturn(const IsoValue(400, StopType.full));
|
||||
when(() => mockUserPreferencesService.ndFilter).thenReturn(NdValue.values.first);
|
||||
when(() => mockUserPreferencesService.haptics).thenReturn(true);
|
||||
when(() => mockUserPreferencesService.meteringScreenLayout).thenReturn({
|
||||
MeteringScreenLayoutFeature.equipmentProfiles: true,
|
||||
MeteringScreenLayoutFeature.extremeExposurePairs: true,
|
||||
MeteringScreenLayoutFeature.filmPicker: true,
|
||||
MeteringScreenLayoutFeature.histogram: false,
|
||||
});
|
||||
when(() => mockUserPreferencesService.themeType).thenReturn(ThemeType.light);
|
||||
when(() => mockUserPreferencesService.primaryColor).thenReturn(primaryColorsList[5]);
|
||||
when(() => mockUserPreferencesService.dynamicColor).thenReturn(false);
|
||||
|
||||
mockCaffeineService = _MockCaffeineService();
|
||||
when(() => mockCaffeineService.isKeepScreenOn()).thenAnswer((_) async => false);
|
||||
when(() => mockCaffeineService.keepScreenOn(true)).thenAnswer((_) async => true);
|
||||
when(() => mockCaffeineService.keepScreenOn(false)).thenAnswer((_) async => false);
|
||||
|
||||
mockHapticsService = _MockHapticsService();
|
||||
when(() => mockHapticsService.quickVibration()).thenAnswer((_) async {});
|
||||
when(() => mockHapticsService.responseVibration()).thenAnswer((_) async {});
|
||||
when(() => mockHapticsService.errorVibration()).thenAnswer((_) async {});
|
||||
|
||||
mockPermissionsService = _MockPermissionsService();
|
||||
when(() => mockPermissionsService.requestCameraPermission())
|
||||
.thenAnswer((_) async => PermissionStatus.granted);
|
||||
when(() => mockPermissionsService.checkCameraPermission())
|
||||
.thenAnswer((_) async => PermissionStatus.granted);
|
||||
|
||||
mockLightSensorService = _MockLightSensorService();
|
||||
when(() => mockLightSensorService.hasSensor()).thenAnswer((_) async => true);
|
||||
when(() => mockLightSensorService.luxStream()).thenAnswer((_) => Stream.fromIterable([100]));
|
||||
|
||||
mockVolumeEventsService = _MockVolumeEventsService();
|
||||
when(() => mockVolumeEventsService.setVolumeHandling(true)).thenAnswer((_) async => true);
|
||||
when(() => mockVolumeEventsService.setVolumeHandling(false)).thenAnswer((_) async => false);
|
||||
when(() => mockVolumeEventsService.volumeButtonsEventStream())
|
||||
.thenAnswer((_) => const Stream<int>.empty());
|
||||
|
||||
when(() => mockHapticsService.quickVibration()).thenAnswer((_) async {});
|
||||
when(() => mockHapticsService.responseVibration()).thenAnswer((_) async {});
|
||||
});
|
||||
|
||||
Future<void> pumpApplication(WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
IAPProviders(
|
||||
sharedPreferences: _MockSharedPreferences(),
|
||||
child: EquipmentProfiles(
|
||||
selected: _mockEquipmentProfiles[0],
|
||||
values: _mockEquipmentProfiles,
|
||||
child: Films(
|
||||
selected: const Film('Ilford HP5+', 400),
|
||||
values: const [Film.other(), Film('Ilford HP5+', 400)],
|
||||
filmsInUse: const [Film.other(), Film('Ilford HP5+', 400)],
|
||||
child: ServicesProvider(
|
||||
environment: const Environment.prod().copyWith(hasLightSensor: true),
|
||||
userPreferencesService: mockUserPreferencesService,
|
||||
caffeineService: mockCaffeineService,
|
||||
hapticsService: mockHapticsService,
|
||||
permissionsService: mockPermissionsService,
|
||||
lightSensorService: mockLightSensorService,
|
||||
volumeEventsService: mockVolumeEventsService,
|
||||
child: const UserPreferencesProvider(child: Application()),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
testWidgets('Pickers test', (tester) async {
|
||||
await pumpApplication(tester);
|
||||
|
||||
await tester.takePhoto();
|
||||
|
||||
expect(find.byType(IsoValuePicker), findsOneWidget);
|
||||
await binding.traceActionNamed(
|
||||
() async {
|
||||
await tester.tap(find.byType(IsoValuePicker));
|
||||
await tester.pumpAndSettle(Dimens.durationL);
|
||||
},
|
||||
"open_iso_picker",
|
||||
);
|
||||
expect(find.byType(DialogPicker<IsoValue>), findsOneWidget);
|
||||
await tester.tapCancelButton();
|
||||
expect(find.byType(DialogPicker<IsoValue>), findsNothing);
|
||||
});
|
||||
}
|
||||
|
||||
extension on IntegrationTestWidgetsFlutterBinding {
|
||||
Future<void> traceActionNamed(Future<dynamic> Function() action, String timelineName) async {
|
||||
final nowString = DateTime.now().toIso8601String().replaceAll(':', '-');
|
||||
await traceAction(action, reportKey: "${timelineName}_$nowString");
|
||||
}
|
||||
}
|
||||
|
||||
final _mockEquipmentProfiles = [
|
||||
const EquipmentProfile(
|
||||
id: '',
|
||||
name: '',
|
||||
apertureValues: ApertureValue.values,
|
||||
ndValues: NdValue.values,
|
||||
shutterSpeedValues: ShutterSpeedValue.values,
|
||||
isoValues: IsoValue.values,
|
||||
),
|
||||
EquipmentProfile(
|
||||
id: '1',
|
||||
name: 'Praktica + Zenitar',
|
||||
apertureValues: ApertureValue.values.sublist(
|
||||
ApertureValue.values.indexOf(const ApertureValue(1.7, StopType.half)),
|
||||
ApertureValue.values.indexOf(const ApertureValue(16, StopType.full)) + 1,
|
||||
),
|
||||
ndValues: NdValue.values.sublist(0, 3),
|
||||
shutterSpeedValues: ShutterSpeedValue.values.sublist(
|
||||
ShutterSpeedValue.values.indexOf(const ShutterSpeedValue(1000, true, StopType.full)),
|
||||
ShutterSpeedValue.values.indexOf(const ShutterSpeedValue(16, false, StopType.full)) + 1,
|
||||
),
|
||||
isoValues: const [
|
||||
IsoValue(50, StopType.full),
|
||||
IsoValue(100, StopType.full),
|
||||
IsoValue(200, StopType.full),
|
||||
IsoValue(250, StopType.third),
|
||||
IsoValue(400, StopType.full),
|
||||
IsoValue(500, StopType.third),
|
||||
IsoValue(800, StopType.full),
|
||||
IsoValue(1600, StopType.full),
|
||||
IsoValue(3200, StopType.full),
|
||||
],
|
||||
),
|
||||
const EquipmentProfile(
|
||||
id: '2',
|
||||
name: 'Praktica + Jupiter',
|
||||
apertureValues: ApertureValue.values,
|
||||
ndValues: NdValue.values,
|
||||
shutterSpeedValues: ShutterSpeedValue.values,
|
||||
isoValues: IsoValue.values,
|
||||
),
|
||||
];
|
61
integration_test/mocks/application_mock.dart
Normal file
61
integration_test/mocks/application_mock.dart
Normal file
|
@ -0,0 +1,61 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:lightmeter/data/models/theme_type.dart';
|
||||
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||
import 'package:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/providers/theme_provider.dart';
|
||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
class _MockUserPreferencesService extends Mock implements UserPreferencesService {}
|
||||
|
||||
class ApplicationMock extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
const ApplicationMock({required this.child, super.key});
|
||||
|
||||
@override
|
||||
State<ApplicationMock> createState() => _ApplicationMockState();
|
||||
}
|
||||
|
||||
class _ApplicationMockState extends State<ApplicationMock> {
|
||||
late final _MockUserPreferencesService userPreferencesService;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
userPreferencesService = _MockUserPreferencesService();
|
||||
when(() => userPreferencesService.themeType).thenReturn(ThemeType.light);
|
||||
when(() => userPreferencesService.primaryColor)
|
||||
.thenReturn(ThemeProvider.primaryColorsList.first);
|
||||
when(() => userPreferencesService.dynamicColor).thenReturn(false);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InheritedWidgetBase<UserPreferencesService>(
|
||||
data: userPreferencesService,
|
||||
child: ThemeProvider(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
return MaterialApp(
|
||||
theme: context.listen<ThemeData>(),
|
||||
localizationsDelegates: const [
|
||||
S.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: S.delegate.supportedLocales,
|
||||
builder: (context, child) => MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
|
||||
child: child!,
|
||||
),
|
||||
home: widget.child,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
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/generated/l10n.dart';
|
||||
import 'package:lightmeter/screens/metering/components/bottom_controls/components/measure_button/widget_button_measure.dart';
|
||||
import 'package:lightmeter/screens/settings/screen_settings.dart';
|
||||
|
||||
extension WidgetTesterFinder on WidgetTester {
|
||||
Future<void> takeScreenshot(IntegrationTestWidgetsFlutterBinding binding, String name) async {
|
||||
if (Platform.isAndroid) {
|
||||
await binding.convertFlutterSurfaceToImage();
|
||||
await pumpAndSettle();
|
||||
}
|
||||
await binding.takeScreenshot(name);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> takePhoto() async {
|
||||
await tap(find.byType(MeteringMeasureButton));
|
||||
await pump(const Duration(seconds: 2)); // wait for circular progress indicator
|
||||
await pump(const Duration(seconds: 1)); // wait for circular progress indicator
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> tapCancelButton() async {
|
||||
final cancelButton = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is TextButton &&
|
||||
widget.child is Text &&
|
||||
(widget.child as Text?)?.data == S.current.cancel,
|
||||
);
|
||||
expect(cancelButton, findsOneWidget);
|
||||
await tap(cancelButton);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> tapListTile(String title) async {
|
||||
final listTile = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is ListTile && widget.title is Text && (widget.title as Text?)?.data == title,
|
||||
);
|
||||
expect(listTile, findsOneWidget);
|
||||
await tap(listTile);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> openSettings() async {
|
||||
final settingsButton = find.byTooltip(S.current.tooltipOpenSettings);
|
||||
expect(settingsButton, findsOneWidget);
|
||||
await tap(settingsButton);
|
||||
await pumpAndSettle();
|
||||
expect(find.byType(SettingsScreen), findsOneWidget);
|
||||
}
|
||||
}
|
94
integration_test/widget_dialog_animated_test.dart
Normal file
94
integration_test/widget_dialog_animated_test.dart
Normal file
|
@ -0,0 +1,94 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:lightmeter/data/models/film.dart';
|
||||
import 'package:lightmeter/res/dimens.dart';
|
||||
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/animated_dialog_picker/widget_picker_dialog_animated.dart';
|
||||
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/reading_value_container/widget_container_reading_value.dart';
|
||||
|
||||
import 'mocks/application_mock.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const ApplicationMock(child: AnimatedPickerTest()));
|
||||
}
|
||||
|
||||
void main2() {
|
||||
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('AnimatedDialogPicker test', () {
|
||||
testWidgets('Tap on `ReadingValueContainer`, verify opened', (tester) async {
|
||||
await tester.pumpWidget(const ApplicationMock(child: AnimatedPickerTest()));
|
||||
expect(find.text('Film'), findsOneWidget);
|
||||
expect(find.text('None'), findsOneWidget);
|
||||
|
||||
await binding.traceAction(
|
||||
() async {
|
||||
await tester.tap(find.byType(AnimatedDialogPicker<Film>));
|
||||
await tester.pumpAndSettle(Dimens.durationL);
|
||||
},
|
||||
reportKey: 'dialog_opening_timeline',
|
||||
);
|
||||
|
||||
expect(find.text('Film'), findsNWidgets(3));
|
||||
expect(find.text('None'), findsNWidgets(3));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class AnimatedPickerTest extends StatefulWidget {
|
||||
const AnimatedPickerTest({super.key});
|
||||
|
||||
@override
|
||||
State<AnimatedPickerTest> createState() => _AnimatedPickerTestState();
|
||||
}
|
||||
|
||||
class _AnimatedPickerTestState extends State<AnimatedPickerTest> {
|
||||
Film _selectedFilm = Film.values.first;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: _FilmPicker(
|
||||
values: Film.values,
|
||||
selectedValue: _selectedFilm,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedFilm = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FilmPicker extends StatelessWidget {
|
||||
final List<Film> values;
|
||||
final Film selectedValue;
|
||||
final ValueChanged<Film> onChanged;
|
||||
|
||||
const _FilmPicker({
|
||||
required this.values,
|
||||
required this.selectedValue,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedDialogPicker<Film>(
|
||||
icon: Icons.camera_roll,
|
||||
title: "Film",
|
||||
selectedValue: selectedValue,
|
||||
values: values,
|
||||
itemTitleBuilder: (_, value) => Text(value.name.isEmpty ? 'None' : value.name),
|
||||
onChanged: onChanged,
|
||||
closedChild: ReadingValueContainer.singleValue(
|
||||
value: ReadingValue(
|
||||
label: "Film",
|
||||
value: selectedValue.name.isEmpty ? 'None' : selectedValue.name,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,47 +1,154 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:lightmeter/data/caffeine_service.dart';
|
||||
import 'package:lightmeter/data/haptics_service.dart';
|
||||
import 'package:lightmeter/data/light_sensor_service.dart';
|
||||
import 'package:lightmeter/data/models/supported_locale.dart';
|
||||
import 'package:lightmeter/data/permissions_service.dart';
|
||||
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||
import 'package:lightmeter/data/volume_events_service.dart';
|
||||
import 'package:lightmeter/environment.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/metering/flow_metering.dart';
|
||||
import 'package:lightmeter/screens/settings/flow_settings.dart';
|
||||
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
||||
import 'package:platform/platform.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class Application extends StatelessWidget {
|
||||
const Application({super.key});
|
||||
final Environment env;
|
||||
|
||||
const Application(this.env, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = UserPreferencesProvider.themeOf(context);
|
||||
final systemIconsBrightness = ThemeData.estimateBrightnessForColor(theme.colorScheme.onSurface);
|
||||
return AnnotatedRegion(
|
||||
value: SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarBrightness:
|
||||
systemIconsBrightness == Brightness.light ? Brightness.dark : Brightness.light,
|
||||
statusBarIconBrightness: systemIconsBrightness,
|
||||
systemNavigationBarColor: Colors.transparent,
|
||||
systemNavigationBarIconBrightness: systemIconsBrightness,
|
||||
),
|
||||
child: MaterialApp(
|
||||
theme: theme,
|
||||
locale: Locale(UserPreferencesProvider.localeOf(context).intlName),
|
||||
localizationsDelegates: const [
|
||||
S.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: S.delegate.supportedLocales,
|
||||
builder: (context, child) => MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
|
||||
child: child!,
|
||||
return FutureBuilder(
|
||||
future: Future.wait([
|
||||
SharedPreferences.getInstance(),
|
||||
const LightSensorService(LocalPlatform()).hasSensor(),
|
||||
]),
|
||||
builder: (_, snapshot) {
|
||||
if (snapshot.data != null) {
|
||||
return IAPProviders(
|
||||
sharedPreferences: snapshot.data![0] as SharedPreferences,
|
||||
child: ServicesProvider(
|
||||
caffeineService: const CaffeineService(),
|
||||
environment: env.copyWith(hasLightSensor: snapshot.data![1] as bool),
|
||||
hapticsService: const HapticsService(),
|
||||
lightSensorService: const LightSensorService(LocalPlatform()),
|
||||
permissionsService: const PermissionsService(),
|
||||
userPreferencesService:
|
||||
UserPreferencesService(snapshot.data![0] as SharedPreferences),
|
||||
volumeEventsService: const VolumeEventsService(LocalPlatform()),
|
||||
child: UserPreferencesProvider(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final theme = UserPreferencesProvider.themeOf(context);
|
||||
final systemIconsBrightness =
|
||||
ThemeData.estimateBrightnessForColor(theme.colorScheme.onSurface);
|
||||
return AnnotatedRegion(
|
||||
value: SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarBrightness: systemIconsBrightness == Brightness.light
|
||||
? Brightness.dark
|
||||
: Brightness.light,
|
||||
statusBarIconBrightness: systemIconsBrightness,
|
||||
systemNavigationBarColor: Colors.transparent,
|
||||
systemNavigationBarIconBrightness: systemIconsBrightness,
|
||||
),
|
||||
child: MaterialApp(
|
||||
theme: theme,
|
||||
locale: Locale(UserPreferencesProvider.localeOf(context).intlName),
|
||||
localizationsDelegates: const [
|
||||
S.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: S.delegate.supportedLocales,
|
||||
builder: (context, child) => MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
|
||||
child: child!,
|
||||
),
|
||||
initialRoute: "metering",
|
||||
routes: {
|
||||
"metering": (context) => const MeteringFlow(),
|
||||
"settings": (context) => const SettingsFlow(),
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (snapshot.error != null) {
|
||||
return Center(child: Text(snapshot.error!.toString()));
|
||||
}
|
||||
|
||||
// TODO(@vodemn): maybe user splashscreen instead
|
||||
return const SizedBox();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AnimatedPickerTest extends StatefulWidget {
|
||||
const AnimatedPickerTest({super.key});
|
||||
|
||||
@override
|
||||
State<AnimatedPickerTest> createState() => _AnimatedPickerTestState();
|
||||
}
|
||||
|
||||
class _AnimatedPickerTestState extends State<AnimatedPickerTest> {
|
||||
Film _selectedFilm = Film.values.first;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: _FilmPicker(
|
||||
values: Film.values,
|
||||
selectedValue: _selectedFilm,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedFilm = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FilmPicker extends StatelessWidget {
|
||||
final List<Film> values;
|
||||
final Film selectedValue;
|
||||
final ValueChanged<Film> onChanged;
|
||||
|
||||
const _FilmPicker({
|
||||
required this.values,
|
||||
required this.selectedValue,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedDialogPicker<Film>(
|
||||
icon: Icons.camera_roll,
|
||||
title: "Film",
|
||||
selectedValue: selectedValue,
|
||||
values: values,
|
||||
itemTitleBuilder: (_, value) => Text(value.name.isEmpty ? 'None' : value.name),
|
||||
onChanged: onChanged,
|
||||
closedChild: ReadingValueContainer.singleValue(
|
||||
value: ReadingValue(
|
||||
label: "Film",
|
||||
value: selectedValue.name.isEmpty ? 'None' : selectedValue.name,
|
||||
),
|
||||
initialRoute: "metering",
|
||||
routes: {
|
||||
"metering": (context) => const MeteringFlow(),
|
||||
"settings": (context) => const SettingsFlow(),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/data/caffeine_service.dart';
|
||||
import 'package:lightmeter/data/haptics_service.dart';
|
||||
import 'package:lightmeter/data/light_sensor_service.dart';
|
||||
import 'package:lightmeter/data/permissions_service.dart';
|
||||
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||
import 'package:lightmeter/data/volume_events_service.dart';
|
||||
import 'package:lightmeter/environment.dart';
|
||||
import 'package:lightmeter/providers/services_provider.dart';
|
||||
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
||||
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
||||
import 'package:platform/platform.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class ApplicationWrapper extends StatelessWidget {
|
||||
final Environment env;
|
||||
final Widget child;
|
||||
|
||||
const ApplicationWrapper(this.env, {required this.child, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder(
|
||||
future: Future.wait([
|
||||
SharedPreferences.getInstance(),
|
||||
const LightSensorService(LocalPlatform()).hasSensor(),
|
||||
]),
|
||||
builder: (_, snapshot) {
|
||||
if (snapshot.data != null) {
|
||||
return IAPProviders(
|
||||
sharedPreferences: snapshot.data![0] as SharedPreferences,
|
||||
child: ServicesProvider(
|
||||
caffeineService: const CaffeineService(),
|
||||
environment: env.copyWith(hasLightSensor: snapshot.data![1] as bool),
|
||||
hapticsService: const HapticsService(),
|
||||
lightSensorService: const LightSensorService(LocalPlatform()),
|
||||
permissionsService: const PermissionsService(),
|
||||
userPreferencesService:
|
||||
UserPreferencesService(snapshot.data![0] as SharedPreferences),
|
||||
volumeEventsService: const VolumeEventsService(LocalPlatform()),
|
||||
child: UserPreferencesProvider(
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (snapshot.error != null) {
|
||||
return Center(child: Text(snapshot.error!.toString()));
|
||||
}
|
||||
|
||||
// TODO(@vodemn): maybe user splashscreen instead
|
||||
return const SizedBox();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -43,7 +43,7 @@
|
|||
"film": "胶片",
|
||||
"filmPush": "胶片 (push)",
|
||||
"filmPull": "胶片 (pull)",
|
||||
"filmReciprocityHint": "对快门速度超过 1 秒的情况进行修正",
|
||||
"filmReciprocityHint": "Applies correction for shutter speeds grater than 1 second",
|
||||
"equipmentProfileName": "设备配置名称",
|
||||
"equipmentProfileNameHint": "Praktica MTL5B",
|
||||
"equipmentProfileAllValues": "全部",
|
||||
|
@ -54,12 +54,12 @@
|
|||
"shutterSpeedValues": "快门速度",
|
||||
"shutterSpeedValuesFilterDescription": "选择要显示的快门速度范围。这通常由您使用的相机机身决定。",
|
||||
"isoValues": "ISO",
|
||||
"isoValuesFilterDescription": "选择要显示的 ISO 。这些值可能是您最常用的值,也可能是相机支持的值。",
|
||||
"isoValuesFilterDescription": "选择要显示的 ISO。这些值可能是您最常用的值,也可能是相机支持的值。",
|
||||
"equipmentProfile": "设备配置",
|
||||
"equipmentProfiles": "设备配置",
|
||||
"tapToAdd": "點擊添加",
|
||||
"filmsInUse": "使用的胶片",
|
||||
"filmsInUseDescription": "选择你使用的胶片",
|
||||
"filmsInUse": "Films in use",
|
||||
"filmsInUseDescription": "Select films which you use.",
|
||||
"general": "通用",
|
||||
"keepScreenOn": "保持屏幕常亮",
|
||||
"haptics": "震动",
|
||||
|
@ -92,20 +92,20 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"buyLightmeterPro": "购买 Lightmeter Pro",
|
||||
"buyLightmeterPro": "Buy Lightmeter Pro",
|
||||
"lightmeterPro": "Lightmeter Pro",
|
||||
"lightmeterProDescription": "购买以解锁额外功能。例如包含光圈、快门速度等参数的配置文件;以及一个胶卷预设列表来提供倒易率失效时的曝光补偿。\n\n您可以在 GitHub 上获取 Lightmeter 的源代码,欢迎自行编译。不过,如果您想支持开发并获得新功能和更新,请考虑购买 Lightmeter Pro。",
|
||||
"buy": "购买",
|
||||
"tooltipAdd": "添加",
|
||||
"tooltipClose": "关闭",
|
||||
"tooltipExpand": "展开",
|
||||
"tooltipCollapse": "崩溃",
|
||||
"tooltipCopy": "复制",
|
||||
"tooltipDelete": "删除",
|
||||
"tooltipSelectAll": "全选",
|
||||
"tooltipDesecelectAll": "取消全选",
|
||||
"resetToZero": "重置为零",
|
||||
"tooltipUseLightSensor": "使用光线传感器",
|
||||
"tooltipUseCamera": "使用摄像头",
|
||||
"tooltipOpenSettings": "打开设置"
|
||||
"lightmeterProDescription": "Unlocks extra features, such as equipment profiles containing filters for aperture, shutter speed, and more; and a list of films with compensation for what's known as reciprocity failure.\n\nThe source code of Lightmeter is available on GitHub. You are welcome to compile it yourself. However, if you want to support the development and receive new features and updates, consider purchasing Lightmeter Pro.",
|
||||
"buy": "Buy",
|
||||
"tooltipAdd": "Add",
|
||||
"tooltipClose": "Close",
|
||||
"tooltipExpand": "Expand",
|
||||
"tooltipCollapse": "Collapse",
|
||||
"tooltipCopy": "Copy",
|
||||
"tooltipDelete": "Delete",
|
||||
"tooltipSelectAll": "Select all",
|
||||
"tooltipDesecelectAll": "Deselect all",
|
||||
"resetToZero": "Reset to zero",
|
||||
"tooltipUseLightSensor": "Use lightsensor",
|
||||
"tooltipUseCamera": "Use camera",
|
||||
"tooltipOpenSettings": "Open settings"
|
||||
}
|
|
@ -1,9 +1,10 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:lightmeter/application.dart';
|
||||
import 'package:lightmeter/application_wrapper.dart';
|
||||
import 'package:lightmeter/environment.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
runApp(const ApplicationWrapper(Environment.dev(), child: Application()));
|
||||
//debugRepaintRainbowEnabled = true;
|
||||
runApp(const Application(Environment.dev()));
|
||||
}
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/application.dart';
|
||||
import 'package:lightmeter/application_wrapper.dart';
|
||||
import 'package:lightmeter/environment.dart';
|
||||
import 'package:lightmeter/firebase.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await initializeFirebase(handleErrors: true);
|
||||
runApp(const ApplicationWrapper(Environment.prod(), child: Application()));
|
||||
runApp(const Application(Environment.prod()));
|
||||
}
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/application.dart';
|
||||
import 'package:lightmeter/application_wrapper.dart';
|
||||
import 'package:lightmeter/environment.dart';
|
||||
import 'package:lightmeter/firebase.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await initializeFirebase(handleErrors: false);
|
||||
runApp(const ApplicationWrapper(Environment.prod(), child: Application()));
|
||||
runApp(const Application(Environment.prod()));
|
||||
}
|
||||
|
|
|
@ -5,6 +5,4 @@ class PlatformConfig {
|
|||
final rational = const String.fromEnvironment('cameraPreviewAspectRatio').split('/');
|
||||
return int.parse(rational[0]) / int.parse(rational[1]);
|
||||
}
|
||||
|
||||
static String get cameraStubImage => const String.fromEnvironment('cameraStubImage');
|
||||
}
|
||||
|
|
|
@ -2,14 +2,13 @@ import 'dart:async';
|
|||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:typed_data';
|
||||
|
||||
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';
|
||||
import 'package:lightmeter/interactors/metering_interactor.dart';
|
||||
import 'package:lightmeter/platform_config.dart';
|
||||
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
|
||||
import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart'
|
||||
as communication_event;
|
||||
|
@ -33,7 +32,7 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
|
|||
|
||||
static const _exposureMaxRange = RangeValues(-4, 4);
|
||||
RangeValues? _exposureOffsetRange;
|
||||
double _exposureStep = 0.1;
|
||||
double _exposureStep = 0.0;
|
||||
double _currentExposureOffset = 0.0;
|
||||
|
||||
double? _ev100 = 0.0;
|
||||
|
@ -200,28 +199,21 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
|
|||
);
|
||||
}
|
||||
|
||||
bool get _canTakePhoto =>
|
||||
PlatformConfig.cameraStubImage.isNotEmpty ||
|
||||
!(_cameraController == null ||
|
||||
!_cameraController!.value.isInitialized ||
|
||||
_cameraController!.value.isTakingPicture);
|
||||
bool get _canTakePhoto => !(_cameraController == null ||
|
||||
!_cameraController!.value.isInitialized ||
|
||||
_cameraController!.value.isTakingPicture);
|
||||
|
||||
Future<double?> _takePhoto() async {
|
||||
try {
|
||||
// https://github.com/flutter/flutter/issues/84957#issuecomment-1661155095
|
||||
await _cameraController!.setFocusMode(FocusMode.locked);
|
||||
await _cameraController!.setExposureMode(ExposureMode.locked);
|
||||
final file = await _cameraController!.takePicture();
|
||||
await _cameraController!.setFocusMode(FocusMode.auto);
|
||||
await _cameraController!.setExposureMode(ExposureMode.auto);
|
||||
|
||||
late final Uint8List bytes;
|
||||
if (PlatformConfig.cameraStubImage.isNotEmpty) {
|
||||
bytes = (await rootBundle.load(PlatformConfig.cameraStubImage)).buffer.asUint8List();
|
||||
} else {
|
||||
await _cameraController!.setFocusMode(FocusMode.locked);
|
||||
await _cameraController!.setExposureMode(ExposureMode.locked);
|
||||
final file = await _cameraController!.takePicture();
|
||||
await _cameraController!.setFocusMode(FocusMode.auto);
|
||||
await _cameraController!.setExposureMode(ExposureMode.auto);
|
||||
bytes = await file.readAsBytes();
|
||||
Directory(file.path).deleteSync(recursive: true);
|
||||
}
|
||||
final Uint8List bytes = await file.readAsBytes();
|
||||
Directory(file.path).deleteSync(recursive: true);
|
||||
|
||||
final tags = await readExifFromBytes(bytes);
|
||||
final iso = double.tryParse("${tags["EXIF ISOSpeedRatings"]}");
|
||||
|
|
|
@ -69,9 +69,6 @@ class _CameraPreviewBuilderState extends State<_CameraPreviewBuilder> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (PlatformConfig.cameraStubImage.isNotEmpty) {
|
||||
return Image.asset(PlatformConfig.cameraStubImage);
|
||||
}
|
||||
return ValueListenableBuilder<bool>(
|
||||
valueListenable: _initializedNotifier,
|
||||
builder: (context, value, child) => value
|
||||
|
|
|
@ -34,26 +34,6 @@ class _DialogFilterState<T> extends State<DialogFilter<T>> {
|
|||
bool get _hasAnySelected => checkboxValues.contains(true);
|
||||
bool get _hasAnyUnselected => checkboxValues.contains(false);
|
||||
|
||||
late final ScrollController _scrollController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
int i = 0;
|
||||
for (; i < checkboxValues.length; i++) {
|
||||
if (checkboxValues[i]) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
_scrollController = ScrollController(initialScrollOffset: Dimens.grid56 * i);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
|
@ -70,7 +50,6 @@ class _DialogFilterState<T> extends State<DialogFilter<T>> {
|
|||
const Divider(),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
controller: _scrollController,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
|
|
@ -42,7 +42,7 @@ dependencies:
|
|||
|
||||
dev_dependencies:
|
||||
bloc_test: 9.1.3
|
||||
build_runner: 2.4.6
|
||||
build_runner: ^2.1.7
|
||||
flutter_driver:
|
||||
sdk: flutter
|
||||
flutter_launcher_icons: 0.11.0
|
||||
|
@ -58,8 +58,6 @@ dev_dependencies:
|
|||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
assets:
|
||||
- assets/camera_stub_image.jpg
|
||||
|
||||
flutter_intl:
|
||||
enabled: true
|
||||
|
|
|
@ -1,25 +1,26 @@
|
|||
import 'package:flutter_driver/flutter_driver.dart' as driver;
|
||||
import 'package:integration_test/integration_test_driver.dart';
|
||||
|
||||
import 'utils/android_camera_permission.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
await grandCameraPermission();
|
||||
await integrationDriver(
|
||||
Future<void> main() {
|
||||
return integrationDriver(
|
||||
responseDataCallback: (data) async {
|
||||
if (data != null) {
|
||||
for (final String timelineName in data.keys) {
|
||||
final timeline = driver.Timeline.fromJson(data[timelineName] as Map<String, dynamic>);
|
||||
final summary = driver.TimelineSummary.summarize(timeline);
|
||||
final timeline =
|
||||
driver.Timeline.fromJson(data['dialog_opening_timeline'] as Map<String, dynamic>);
|
||||
|
||||
// Write the entire timeline and summary to disk in a json format.
|
||||
// This file can be opened in the Chrome browser's tracing tools
|
||||
// found by navigating to chrome://tracing.
|
||||
await summary.writeTimelineToFile(
|
||||
timelineName,
|
||||
pretty: true,
|
||||
);
|
||||
}
|
||||
// Convert the Timeline into a TimelineSummary that's easier to
|
||||
// read and understand.
|
||||
final summary = driver.TimelineSummary.summarize(timeline);
|
||||
|
||||
// Then, write the entire timeline to disk in a json format.
|
||||
// This file can be opened in the Chrome browser's tracing tools
|
||||
// found by navigating to chrome://tracing.
|
||||
// Optionally, save the summary to disk by setting includeSummary
|
||||
// to true
|
||||
await summary.writeTimelineToFile(
|
||||
'dialog_opening_timeline(fade)',
|
||||
pretty: true,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
import 'package:integration_test/integration_test_driver_extended.dart';
|
||||
|
||||
import 'utils/android_camera_permission.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
try {
|
||||
await grandCameraPermission();
|
||||
await integrationDriver(
|
||||
onScreenshot: (name, bytes, [args]) async {
|
||||
final File image = await File('screenshots/$name.png').create(recursive: true);
|
||||
image.writeAsBytesSync(bytes);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
log('Error occured: $e');
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
Future<void> grandCameraPermission() async {
|
||||
try {
|
||||
final bool adbExists = Process.runSync('which', <String>['adb']).exitCode == 0;
|
||||
if (!adbExists) {
|
||||
log(r'This test needs ADB to exist on the $PATH. Skipping...');
|
||||
exit(0);
|
||||
}
|
||||
final deviceId = await Process.run('adb', ["-s", 'shell', 'devices']).then((value) {
|
||||
if (value.stdout is String) {
|
||||
return RegExp(r"(?:List of devices attached\n)([A-Z0-9]*)(?:\sdevice\n)")
|
||||
.firstMatch(value.stdout as String)!
|
||||
.group(1);
|
||||
}
|
||||
});
|
||||
if (deviceId == null) {
|
||||
log('This test needs at least one device connected');
|
||||
exit(0);
|
||||
}
|
||||
await Process.run('adb', [
|
||||
"-s",
|
||||
deviceId, // https://github.com/flutter/flutter/issues/86295#issuecomment-1192766368
|
||||
'shell',
|
||||
'pm',
|
||||
'grant',
|
||||
'com.vodemn.lightmeter.dev',
|
||||
'android.permission.CAMERA'
|
||||
]);
|
||||
} catch (e) {
|
||||
log('Error occured: $e');
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue