Compare commits

..

No commits in common. "da152dcd370a0497e5733ec3a7b1004805ca1ee5" and "c50baa4802a9e178142046c228e099bfcd6968d5" have entirely different histories.

22 changed files with 226 additions and 205 deletions

View file

@ -1,10 +1,14 @@
<img src="resources/social_preview.png" width="100%" /> <p align="center">
<img src="assets/launcher_icon_circle.png" width="100" height="100">
</p>
<p align="center", style="font-size:60px;">
<b>Material Lightmeter</b>
</p>
# Table of contents # Table of contents
- [Table of contents](#table-of-contents) - [Table of contents](#table-of-contents)
- [Backstory](#backstory) - [Backstory](#backstory)
- [Screenshots](#screenshots)
- [Build](#build) - [Build](#build)
- [Contribution](#contribution) - [Contribution](#contribution)
- [iOS Limitations](#ios-limitations) - [iOS Limitations](#ios-limitations)
@ -17,7 +21,7 @@ But as the existing repo contained some sensitive data, that I've pushed due to
Without further delay behold my new Lightmeter app inspired by Material You (a.k.a. M3) Without further delay behold my new Lightmeter app inspired by Material You (a.k.a. M3)
# Screenshots # Features
<p float="center"> <p float="center">
<img src="https://lh3.googleusercontent.com/8Sd-pmNcQ0xAr5opuTeJKWr2OXeQvCoFSdVDSoKQSHHKeNmqF71hqeAdm3yjunY12zY" width="18.8%" /> <img src="https://lh3.googleusercontent.com/8Sd-pmNcQ0xAr5opuTeJKWr2OXeQvCoFSdVDSoKQSHHKeNmqF71hqeAdm3yjunY12zY" width="18.8%" />

View file

@ -17,10 +17,5 @@ class LightSensorService {
} }
} }
Stream<int> luxStream() { Stream<int> luxStream() => LightSensor.lightSensorStream;
if (!localPlatform.isAndroid) {
return const Stream<int>.empty();
}
return LightSensor.lightSensorStream;
}
} }

View file

@ -1,3 +1,3 @@
enum VolumeAction { shutter, none } enum VolumeAction { shutter, zoom, none }
enum VolumeKey { up, down } enum VolumeKey { up, down }

View file

@ -56,7 +56,9 @@
"general": "General", "general": "General",
"keepScreenOn": "Keep screen on", "keepScreenOn": "Keep screen on",
"haptics": "Haptics", "haptics": "Haptics",
"volumeKeysAction": "Shutter by volume keys", "volumeKeysAction": "Volume keys action",
"shutter": "Shutter",
"zoom": "Zoom",
"language": "Language", "language": "Language",
"chooseLanguage": "Choose language", "chooseLanguage": "Choose language",
"theme": "Theme", "theme": "Theme",

View file

@ -56,7 +56,9 @@
"general": "Général", "general": "Général",
"keepScreenOn": "Garder l'écran allumé", "keepScreenOn": "Garder l'écran allumé",
"haptics": "Haptiques", "haptics": "Haptiques",
"volumeKeysAction": "Obturateur par boutons de volume", "volumeKeysAction": "Action des touches de volume",
"shutter": "Obturateur",
"zoom": "Zoom",
"language": "Langue", "language": "Langue",
"chooseLanguage": "Choisissez la langue", "chooseLanguage": "Choisissez la langue",
"theme": "Thème", "theme": "Thème",

View file

@ -56,7 +56,9 @@
"general": "Общие", "general": "Общие",
"keepScreenOn": "Запрет блокировки", "keepScreenOn": "Запрет блокировки",
"haptics": "Вибрация", "haptics": "Вибрация",
"volumeKeysAction": "Затвор по кнопкам громкости", "volumeKeysAction": "Кнопки регулировки громкости",
"shutter": "Затвор",
"zoom": "Зум",
"language": "Язык", "language": "Язык",
"chooseLanguage": "Выберите язык", "chooseLanguage": "Выберите язык",
"theme": "Тема", "theme": "Тема",

View file

@ -8,6 +8,7 @@ import 'package:camera/camera.dart';
import 'package:exif/exif.dart'; import 'package:exif/exif.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/data/models/volume_action.dart';
import 'package:lightmeter/interactors/metering_interactor.dart'; import 'package:lightmeter/interactors/metering_interactor.dart';
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart'; import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart' import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart'
@ -18,10 +19,12 @@ 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/screens/metering/components/shared/volume_keys_notifier/notifier_volume_keys.dart';
import 'package:lightmeter/utils/log_2.dart'; import 'package:lightmeter/utils/log_2.dart';
class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraContainerState> { class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraContainerState> {
final MeteringInteractor _meteringInteractor; final MeteringInteractor _meteringInteractor;
final VolumeKeysNotifier _volumeKeysNotifier;
late final _WidgetsBindingObserver _observer; late final _WidgetsBindingObserver _observer;
CameraController? _cameraController; CameraController? _cameraController;
@ -41,11 +44,13 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
CameraContainerBloc( CameraContainerBloc(
this._meteringInteractor, this._meteringInteractor,
this._volumeKeysNotifier,
MeteringCommunicationBloc communicationBloc, MeteringCommunicationBloc communicationBloc,
) : super( ) : super(
communicationBloc, communicationBloc,
const CameraInitState(), const CameraInitState(),
) { ) {
_volumeKeysNotifier.addListener(onVolumeKey);
_observer = _WidgetsBindingObserver(_appLifecycleStateObserver); _observer = _WidgetsBindingObserver(_appLifecycleStateObserver);
WidgetsBinding.instance.addObserver(_observer); WidgetsBinding.instance.addObserver(_observer);
@ -61,6 +66,7 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
@override @override
Future<void> close() async { Future<void> close() async {
WidgetsBinding.instance.removeObserver(_observer); WidgetsBinding.instance.removeObserver(_observer);
_volumeKeysNotifier.removeListener(onVolumeKey);
unawaited(_cameraController?.dispose().then((_) => _cameraController = null)); unawaited(_cameraController?.dispose().then((_) => _cameraController = null));
communicationBloc.add(communication_event.MeteringEndedEvent(_ev100)); communicationBloc.add(communication_event.MeteringEndedEvent(_ev100));
return super.close(); return super.close();
@ -159,8 +165,7 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
} }
Future<void> _onDeinitialize(DeinitializeEvent _, Emitter emit) async { Future<void> _onDeinitialize(DeinitializeEvent _, Emitter emit) async {
emit(const CameraInitState()); emit(const CameraLoadingState());
communicationBloc.add(communication_event.MeteringEndedEvent(_ev100));
await _cameraController?.dispose().then((_) => _cameraController = null); await _cameraController?.dispose().then((_) => _cameraController = null);
} }
@ -240,6 +245,18 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
} }
} }
} }
@visibleForTesting
void onVolumeKey() {
if (_meteringInteractor.volumeAction == VolumeAction.zoom) {
switch (_volumeKeysNotifier.value) {
case VolumeKey.up:
add(ZoomChangedEvent(_currentZoom + 0.5));
case VolumeKey.down:
add(ZoomChangedEvent(_currentZoom - 0.5));
}
}
}
} }
/// This is needed only because we cannot use `with` with mixins /// This is needed only because we cannot use `with` with mixins

View file

@ -7,6 +7,7 @@ import 'package:lightmeter/screens/metering/communication/bloc_communication_met
import 'package:lightmeter/screens/metering/components/camera_container/bloc_container_camera.dart'; import 'package:lightmeter/screens/metering/components/camera_container/bloc_container_camera.dart';
import 'package:lightmeter/screens/metering/components/camera_container/event_container_camera.dart'; import 'package:lightmeter/screens/metering/components/camera_container/event_container_camera.dart';
import 'package:lightmeter/screens/metering/components/camera_container/widget_container_camera.dart'; import 'package:lightmeter/screens/metering/components/camera_container/widget_container_camera.dart';
import 'package:lightmeter/screens/metering/components/shared/volume_keys_notifier/notifier_volume_keys.dart';
import 'package:lightmeter/utils/inherited_generics.dart'; import 'package:lightmeter/utils/inherited_generics.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
@ -40,6 +41,7 @@ class CameraContainerProvider extends StatelessWidget {
lazy: false, lazy: false,
create: (context) => CameraContainerBloc( create: (context) => CameraContainerBloc(
context.get<MeteringInteractor>(), context.get<MeteringInteractor>(),
context.get<VolumeKeysNotifier>(),
context.read<MeteringCommunicationBloc>(), context.read<MeteringCommunicationBloc>(),
)..add(const RequestPermissionEvent()), )..add(const RequestPermissionEvent()),
child: CameraContainer( child: CameraContainer(

View file

@ -25,7 +25,6 @@ class LightSensorContainerBloc
communicationBloc, communicationBloc,
const LightSensorContainerState(null), const LightSensorContainerState(null),
) { ) {
on<StartLuxMeteringEvent>(_onStartLuxMeteringEvent);
on<LuxMeteringEvent>(_onLuxMeteringEvent); on<LuxMeteringEvent>(_onLuxMeteringEvent);
on<CancelLuxMeteringEvent>(_onCancelLuxMeteringEvent); on<CancelLuxMeteringEvent>(_onCancelLuxMeteringEvent);
} }
@ -35,27 +34,22 @@ class LightSensorContainerBloc
switch (communicationState) { switch (communicationState) {
case communication_states.MeasureState(): case communication_states.MeasureState():
if (_luxSubscriptions == null) { if (_luxSubscriptions == null) {
add(const StartLuxMeteringEvent()); _startMetering();
} else { } else {
add(const CancelLuxMeteringEvent()); _cancelMetering();
} }
case communication_states.SettingsOpenedState(): case communication_states.SettingsOpenedState():
add(const CancelLuxMeteringEvent()); _cancelMetering();
default: default:
} }
} }
@override @override
Future<void> close() async { Future<void> close() async {
communicationBloc.add(communication_event.MeteringEndedEvent(state.ev100)); _cancelMetering();
_luxSubscriptions?.cancel().then((_) => _luxSubscriptions = null);
return super.close(); return super.close();
} }
void _onStartLuxMeteringEvent(StartLuxMeteringEvent event, _) {
_luxSubscriptions = _meteringInteractor.luxStream().listen((lux) => add(LuxMeteringEvent(lux)));
}
void _onLuxMeteringEvent(LuxMeteringEvent event, Emitter<LightSensorContainerState> emit) { void _onLuxMeteringEvent(LuxMeteringEvent event, Emitter<LightSensorContainerState> emit) {
final ev100 = log2(event.lux.toDouble() / 2.5) + _meteringInteractor.lightSensorEvCalibration; final ev100 = log2(event.lux.toDouble() / 2.5) + _meteringInteractor.lightSensorEvCalibration;
emit(LightSensorContainerState(ev100)); emit(LightSensorContainerState(ev100));
@ -63,6 +57,14 @@ class LightSensorContainerBloc
} }
void _onCancelLuxMeteringEvent(CancelLuxMeteringEvent event, _) { void _onCancelLuxMeteringEvent(CancelLuxMeteringEvent event, _) {
_cancelMetering();
}
void _startMetering() {
_luxSubscriptions = _meteringInteractor.luxStream().listen((lux) => add(LuxMeteringEvent(lux)));
}
void _cancelMetering() {
communicationBloc.add(communication_event.MeteringEndedEvent(state.ev100)); communicationBloc.add(communication_event.MeteringEndedEvent(state.ev100));
_luxSubscriptions?.cancel().then((_) => _luxSubscriptions = null); _luxSubscriptions?.cancel().then((_) => _luxSubscriptions = null);
} }

View file

@ -2,10 +2,6 @@ abstract class LightSensorContainerEvent {
const LightSensorContainerEvent(); const LightSensorContainerEvent();
} }
class StartLuxMeteringEvent extends LightSensorContainerEvent {
const StartLuxMeteringEvent();
}
class LuxMeteringEvent extends LightSensorContainerEvent { class LuxMeteringEvent extends LightSensorContainerEvent {
final int lux; final int lux;

View file

@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/settings/components/general/components/caffeine/bloc_list_tile_caffeine.dart'; import 'package:lightmeter/screens/settings/components/general/components/caffeine/bloc_list_tile_caffeine.dart';
@ -16,7 +15,6 @@ class CaffeineListTile extends StatelessWidget {
title: Text(S.of(context).keepScreenOn), title: Text(S.of(context).keepScreenOn),
value: state, value: state,
onChanged: context.read<CaffeineListTileBloc>().onCaffeineChanged, onChanged: context.read<CaffeineListTileBloc>().onCaffeineChanged,
contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
), ),
); );
} }

View file

@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/settings/components/general/components/haptics/bloc_list_tile_haptics.dart'; import 'package:lightmeter/screens/settings/components/general/components/haptics/bloc_list_tile_haptics.dart';
@ -16,7 +15,6 @@ class HapticsListTile extends StatelessWidget {
title: Text(S.of(context).haptics), title: Text(S.of(context).haptics),
value: state, value: state,
onChanged: context.read<HapticsListTileBloc>().onHapticsChanged, onChanged: context.read<HapticsListTileBloc>().onHapticsChanged,
contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
), ),
); );
} }

View file

@ -2,15 +2,15 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/data/models/volume_action.dart'; import 'package:lightmeter/data/models/volume_action.dart';
import 'package:lightmeter/interactors/settings_interactor.dart'; import 'package:lightmeter/interactors/settings_interactor.dart';
class VolumeActionsListTileBloc extends Cubit<bool> { class VolumeActionsListTileBloc extends Cubit<VolumeAction> {
final SettingsInteractor _settingsInteractor; final SettingsInteractor _settingsInteractor;
VolumeActionsListTileBloc( VolumeActionsListTileBloc(
this._settingsInteractor, this._settingsInteractor,
) : super(_settingsInteractor.volumeAction == VolumeAction.shutter); ) : super(_settingsInteractor.volumeAction);
void onVolumeActionChanged(bool value) { void onVolumeActionChanged(VolumeAction value) {
_settingsInteractor.setVolumeAction(value ? VolumeAction.shutter : VolumeAction.none); _settingsInteractor.setVolumeAction(value);
// while in settings we allow system to handle volume // while in settings we allow system to handle volume
// so that volume keys action works only when necessary - on the metering screen // so that volume keys action works only when necessary - on the metering screen

View file

@ -1,22 +1,48 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/data/models/volume_action.dart';
import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/settings/components/general/components/volume_actions/bloc_list_tile_volume_actions.dart'; import 'package:lightmeter/screens/settings/components/general/components/volume_actions/bloc_list_tile_volume_actions.dart';
import 'package:lightmeter/screens/settings/components/shared/dialog_picker.dart/widget_dialog_picker.dart';
class VolumeActionsListTile extends StatelessWidget { class VolumeActionsListTile extends StatelessWidget {
const VolumeActionsListTile({super.key}); const VolumeActionsListTile({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<VolumeActionsListTileBloc, bool>( return BlocBuilder<VolumeActionsListTileBloc, VolumeAction>(
builder: (context, state) => SwitchListTile( builder: (context, state) => ListTile(
secondary: const Icon(Icons.volume_up), leading: const Icon(Icons.volume_up),
title: Text(S.of(context).volumeKeysAction), title: Text(S.of(context).volumeKeysAction),
value: state, trailing: Text(actionToString(context, state)),
onChanged: context.read<VolumeActionsListTileBloc>().onVolumeActionChanged, onTap: () {
contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM), showDialog<VolumeAction>(
context: context,
builder: (_) => DialogPicker<VolumeAction>(
icon: Icons.volume_up,
title: S.of(context).volumeKeysAction,
selectedValue: state,
values: VolumeAction.values,
titleAdapter: (context, value) => actionToString(context, value),
),
).then((value) {
if (value != null) {
context.read<VolumeActionsListTileBloc>().onVolumeActionChanged(value);
}
});
},
), ),
); );
} }
String actionToString(BuildContext context, VolumeAction themeType) {
switch (themeType) {
case VolumeAction.shutter:
return S.of(context).shutter;
case VolumeAction.zoom:
return S.of(context).zoom;
case VolumeAction.none:
return S.of(context).none;
}
}
} }

View file

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/dynamic_colors_state.dart'; import 'package:lightmeter/data/models/dynamic_colors_state.dart';
import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/providers/theme_provider.dart'; import 'package:lightmeter/providers/theme_provider.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/utils/inherited_generics.dart'; import 'package:lightmeter/utils/inherited_generics.dart';
class DynamicColorListTile extends StatelessWidget { class DynamicColorListTile extends StatelessWidget {
@ -15,7 +14,6 @@ class DynamicColorListTile extends StatelessWidget {
title: Text(S.of(context).dynamicColor), title: Text(S.of(context).dynamicColor),
value: context.listen<DynamicColorState>() == DynamicColorState.enabled, value: context.listen<DynamicColorState>() == DynamicColorState.enabled,
onChanged: ThemeProvider.of(context).enableDynamicColor, onChanged: ThemeProvider.of(context).enableDynamicColor,
contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
); );
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

View file

@ -83,16 +83,4 @@ void main() {
}); });
}, },
); );
group('luxStream', () {
// test('Android', () async {
// when(() => localPlatform.isAndroid).thenReturn(true);
// expect(service.luxStream(), const Stream.empty());
// });
test('iOS', () async {
when(() => localPlatform.isAndroid).thenReturn(false);
expect(service.luxStream(), const Stream<int>.empty());
});
});
} }

View file

@ -58,16 +58,4 @@ void main() {
expectLater(service.setVolumeHandling(false), completion(false)); expectLater(service.setVolumeHandling(false), completion(false));
}); });
}); });
group('volumeButtonsEventStream', () {
// test('Android', () async {
// when(() => localPlatform.isAndroid).thenReturn(true);
// expect(service.volumeButtonsEventStream(), const Stream.empty());
// });
test('iOS', () async {
when(() => localPlatform.isAndroid).thenReturn(false);
expect(service.volumeButtonsEventStream(), const Stream<int>.empty());
});
});
} }

View file

@ -641,6 +641,19 @@ void main() {
expect: () => [isA<LoadingState>()], expect: () => [isA<LoadingState>()],
); );
blocTest<MeteringBloc, MeteringState>(
'onVolumeKey & VolumeAction.zoom',
build: () => bloc,
act: (bloc) async {
bloc.onVolumeKey();
},
setUp: () {
when(() => meteringInteractor.volumeAction).thenReturn(VolumeAction.zoom);
},
verify: (_) {},
expect: () => [],
);
blocTest<MeteringBloc, MeteringState>( blocTest<MeteringBloc, MeteringState>(
'onVolumeKey & VolumeAction.none', 'onVolumeKey & VolumeAction.none',
build: () => bloc, build: () => bloc,
@ -655,25 +668,4 @@ void main() {
); );
}, },
); );
group(
'`SettingOpenedEvent`/`SettingsClosedEvent`',
() {
blocTest<MeteringBloc, MeteringState>(
'Settings opened & closed',
build: () => bloc,
act: (bloc) async {
bloc.add(const SettingsOpenedEvent());
bloc.add(const SettingsClosedEvent());
},
verify: (_) {
verify(() => communicationBloc.add(const communication_events.SettingsOpenedEvent()))
.called(1);
verify(() => communicationBloc.add(const communication_events.SettingsClosedEvent()))
.called(1);
},
expect: () => [],
);
},
);
} }

View file

@ -98,30 +98,4 @@ void main() {
); );
}, },
); );
group(
'`SettingsOpenedEvent`/`SettingsClosedEvent`',
() {
blocTest<MeteringCommunicationBloc, MeteringCommunicationState>(
'Multiple consequtive settings events',
build: () => bloc,
act: (bloc) async {
bloc.add(const SettingsOpenedEvent());
bloc.add(const SettingsOpenedEvent());
bloc.add(const SettingsOpenedEvent());
bloc.add(const SettingsClosedEvent());
bloc.add(const SettingsClosedEvent());
bloc.add(const SettingsClosedEvent());
bloc.add(const SettingsOpenedEvent());
bloc.add(const SettingsClosedEvent());
},
expect: () => [
isA<SettingsOpenedState>(),
isA<SettingsClosedState>(),
isA<SettingsOpenedState>(),
isA<SettingsClosedState>(),
],
);
},
);
} }

View file

@ -2,6 +2,7 @@ import 'package:bloc_test/bloc_test.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
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:lightmeter/data/models/volume_action.dart';
import 'package:lightmeter/interactors/metering_interactor.dart'; import 'package:lightmeter/interactors/metering_interactor.dart';
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart'; import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart' import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart'
@ -12,10 +13,13 @@ import 'package:lightmeter/screens/metering/components/camera_container/bloc_con
import 'package:lightmeter/screens/metering/components/camera_container/event_container_camera.dart'; import 'package:lightmeter/screens/metering/components/camera_container/event_container_camera.dart';
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/volume_keys_notifier/notifier_volume_keys.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
class _MockMeteringInteractor extends Mock implements MeteringInteractor {} class _MockMeteringInteractor extends Mock implements MeteringInteractor {}
class _MockVolumeKeysNotifier extends Mock implements VolumeKeysNotifier {}
class _MockMeteringCommunicationBloc extends MockBloc< class _MockMeteringCommunicationBloc extends MockBloc<
communication_events.MeteringCommunicationEvent, communication_events.MeteringCommunicationEvent,
communication_states.MeteringCommunicationState> implements MeteringCommunicationBloc {} communication_states.MeteringCommunicationState> implements MeteringCommunicationBloc {}
@ -24,6 +28,7 @@ void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
late _MockMeteringInteractor meteringInteractor; late _MockMeteringInteractor meteringInteractor;
late _MockVolumeKeysNotifier volumeKeysNotifier;
late _MockMeteringCommunicationBloc communicationBloc; late _MockMeteringCommunicationBloc communicationBloc;
late CameraContainerBloc bloc; late CameraContainerBloc bloc;
@ -112,6 +117,7 @@ void main() {
setUpAll(() { setUpAll(() {
meteringInteractor = _MockMeteringInteractor(); meteringInteractor = _MockMeteringInteractor();
volumeKeysNotifier = _MockVolumeKeysNotifier();
communicationBloc = _MockMeteringCommunicationBloc(); communicationBloc = _MockMeteringCommunicationBloc();
when(() => meteringInteractor.cameraEvCalibration).thenReturn(0.0); when(() => meteringInteractor.cameraEvCalibration).thenReturn(0.0);
@ -121,6 +127,7 @@ void main() {
setUp(() { setUp(() {
bloc = CameraContainerBloc( bloc = CameraContainerBloc(
meteringInteractor, meteringInteractor,
volumeKeysNotifier,
communicationBloc, communicationBloc,
); );
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
@ -310,30 +317,6 @@ void main() {
}, },
expect: () => [ expect: () => [
...initializedStateSequence, ...initializedStateSequence,
const CameraInitState(),
...initializedStateSequence,
],
);
blocTest<CameraContainerBloc, CameraContainerState>(
'onCommunicationState',
setUp: () {
when(() => meteringInteractor.checkCameraPermission()).thenAnswer((_) async => true);
},
build: () => bloc,
act: (bloc) async {
bloc.add(const InitializeEvent());
await Future.delayed(Duration.zero);
bloc.onCommunicationState(const communication_states.SettingsOpenedState());
await Future.delayed(Duration.zero);
bloc.onCommunicationState(const communication_states.SettingsClosedState());
},
verify: (_) {
verify(() => meteringInteractor.checkCameraPermission()).called(2);
},
expect: () => [
...initializedStateSequence,
const CameraInitState(),
...initializedStateSequence, ...initializedStateSequence,
], ],
); );
@ -500,6 +483,123 @@ void main() {
); );
}, },
); );
group(
'`Volume keys shutter action`',
() {
blocTest<CameraContainerBloc, CameraContainerState>(
'Add/remove listener',
build: () => bloc,
verify: (_) {
verify(() => volumeKeysNotifier.addListener(bloc.onVolumeKey)).called(1);
verify(() => volumeKeysNotifier.removeListener(bloc.onVolumeKey)).called(1);
},
expect: () => [],
);
blocTest<CameraContainerBloc, CameraContainerState>(
'onVolumeKey & VolumeAction.shutter',
build: () => bloc,
act: (bloc) async {
bloc.onVolumeKey();
},
setUp: () {
when(() => meteringInteractor.volumeAction).thenReturn(VolumeAction.shutter);
},
verify: (_) {},
expect: () => [],
);
blocTest<CameraContainerBloc, CameraContainerState>(
'onVolumeKey.up & VolumeAction.zoom',
build: () => bloc,
act: (bloc) async {
bloc.add(const InitializeEvent());
await Future.delayed(Duration.zero);
bloc.onVolumeKey();
await Future.delayed(Duration.zero);
bloc.onVolumeKey();
await Future.delayed(Duration.zero);
bloc.onVolumeKey();
},
setUp: () {
when(() => meteringInteractor.checkCameraPermission()).thenAnswer((_) async => true);
when(() => meteringInteractor.volumeAction).thenReturn(VolumeAction.zoom);
when(() => volumeKeysNotifier.value).thenReturn(VolumeKey.up);
},
verify: (_) {},
expect: () => [
...initializedStateSequence,
isA<CameraActiveState>()
.having((state) => state.zoomRange, 'zoomRange', const RangeValues(1.0, 7.0))
.having((state) => state.currentZoom, 'currentZoom', 1.5)
.having(
(state) => state.exposureOffsetRange,
'exposureOffsetRange',
const RangeValues(-4.0, 4.0),
)
.having((state) => state.exposureOffsetStep, 'exposureOffsetStep', 0.1666666)
.having((state) => state.currentExposureOffset, 'currentExposureOffset', 0.0),
isA<CameraActiveState>()
.having((state) => state.zoomRange, 'zoomRange', const RangeValues(1.0, 7.0))
.having((state) => state.currentZoom, 'currentZoom', 2.0)
.having(
(state) => state.exposureOffsetRange,
'exposureOffsetRange',
const RangeValues(-4.0, 4.0),
)
.having((state) => state.exposureOffsetStep, 'exposureOffsetStep', 0.1666666)
.having((state) => state.currentExposureOffset, 'currentExposureOffset', 0.0),
isA<CameraActiveState>()
.having((state) => state.zoomRange, 'zoomRange', const RangeValues(1.0, 7.0))
.having((state) => state.currentZoom, 'currentZoom', 2.5)
.having(
(state) => state.exposureOffsetRange,
'exposureOffsetRange',
const RangeValues(-4.0, 4.0),
)
.having((state) => state.exposureOffsetStep, 'exposureOffsetStep', 0.1666666)
.having((state) => state.currentExposureOffset, 'currentExposureOffset', 0.0),
],
);
blocTest<CameraContainerBloc, CameraContainerState>(
'onVolumeKey.down & VolumeAction.zoom',
build: () => bloc,
act: (bloc) async {
bloc.add(const InitializeEvent());
await Future.delayed(Duration.zero);
bloc.onVolumeKey();
await Future.delayed(Duration.zero);
bloc.onVolumeKey();
await Future.delayed(Duration.zero);
bloc.onVolumeKey();
},
setUp: () {
when(() => meteringInteractor.checkCameraPermission()).thenAnswer((_) async => true);
when(() => meteringInteractor.volumeAction).thenReturn(VolumeAction.zoom);
when(() => volumeKeysNotifier.value).thenReturn(VolumeKey.down);
},
verify: (_) {},
expect: () => [
...initializedStateSequence,
],
);
blocTest<CameraContainerBloc, CameraContainerState>(
'onVolumeKey & VolumeAction.none',
build: () => bloc,
act: (bloc) async {
bloc.onVolumeKey();
},
setUp: () {
when(() => meteringInteractor.volumeAction).thenReturn(VolumeAction.none);
},
verify: (_) {},
expect: () => [],
);
},
);
} }
extension _MethodChannelMock on MethodChannel { extension _MethodChannelMock on MethodChannel {

View file

@ -78,67 +78,4 @@ void main() {
); );
}, },
); );
group(
'`communication_states.SettingsOpenedState()`',
() {
const List<int> luxIterable = [1, 2, 2, 2, 3];
final List<double> resultList = luxIterable.map((lux) => log2(lux / 2.5)).toList();
blocTest<LightSensorContainerBloc, LightSensorContainerState>(
'Metering is already canceled',
build: () => bloc,
setUp: () {
when(() => meteringInteractor.luxStream())
.thenAnswer((_) => Stream.fromIterable(luxIterable));
when(() => meteringInteractor.lightSensorEvCalibration).thenReturn(0.0);
},
act: (bloc) async {
bloc.onCommunicationState(const communication_states.SettingsOpenedState());
},
verify: (_) {
verifyNever(() => meteringInteractor.luxStream().listen((_) {}));
verifyNever(() => meteringInteractor.lightSensorEvCalibration);
verify(() {
communicationBloc.add(const communication_events.MeteringEndedEvent(null));
}).called(2); // +1 from dispose
},
expect: () => [],
);
blocTest<LightSensorContainerBloc, LightSensorContainerState>(
'Metering is in progress',
build: () => bloc,
setUp: () {
when(() => meteringInteractor.luxStream())
.thenAnswer((_) => Stream.fromIterable(luxIterable));
when(() => meteringInteractor.lightSensorEvCalibration).thenReturn(0.0);
},
act: (bloc) async {
bloc.onCommunicationState(const communication_states.MeasureState());
await Future.delayed(Duration.zero);
bloc.onCommunicationState(const communication_states.SettingsOpenedState());
bloc.onCommunicationState(const communication_states.SettingsClosedState());
},
verify: (_) {
verify(() => meteringInteractor.luxStream().listen((_) {})).called(1);
verify(() => meteringInteractor.lightSensorEvCalibration).called(5);
verify(() {
communicationBloc.add(communication_events.MeteringInProgressEvent(resultList.first));
}).called(1);
verify(() {
communicationBloc.add(communication_events.MeteringInProgressEvent(resultList[1]));
}).called(3);
verify(() {
communicationBloc.add(communication_events.MeteringInProgressEvent(resultList.last));
}).called(1);
verify(() {
communicationBloc.add(communication_events.MeteringEndedEvent(resultList.last));
}).called(3); // +1 from settings closed, +1 from dispose
},
expect: () => resultList.map(
(e) => isA<LightSensorContainerState>().having((state) => state.ev100, 'ev100', e),
),
);
},
);
} }