Skip to content

Commit

Permalink
ML-173 Add a timer for long exposures (#174)
Browse files Browse the repository at this point in the history
* wip

* added start/stop button

* animated timeline

* fixed timer stop state

* added reset button (wip)

* added `onExposurePairTap` callback

* integrated `TimerScreen` to navigation

* separated `TimerTimeline`

* fixed timeline flickering

* added milliseconds to timer

* synchronized timeline with actual timer

* reused `BottomControlsBar`

* fixed default scaffold background color

* moved center button size to the bar itself

* display selected exposure pair on timer screen

* separated reusable `AnimatedCircluarButton`

* release camera when timer is opened

* added `TimerInteractor`

* added `TimerBloc` test

* fixed hours parsing

* added scenarios for timer golden test

* adjusted timer timeline colors

* show iso & nd values on timer screen

* automatically close timer screen after timeout

* added timer autostart

* reverted theme changes

* updated goldens

* typo

* removed timer screen auto-dismiss

* increased timer vibration duration

* replaced outlined locks

* increased 1/3 values font size
  • Loading branch information
vodemn committed May 7, 2024
1 parent bc7e6e1 commit 5c27f72
Show file tree
Hide file tree
Showing 54 changed files with 1,403 additions and 355 deletions.
4 changes: 2 additions & 2 deletions integration_test/e2e_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/metering/components/bottom_controls/components/measure_button/widget_button_measure.dart';
import 'package:lightmeter/screens/metering/components/bottom_controls/widget_bottom_controls.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/equipment_profile_picker/widget_picker_equipment_profiles.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/film_picker/widget_picker_film.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/iso_picker/widget_picker_iso.dart';
Expand Down Expand Up @@ -329,7 +329,7 @@ Future<void> _expectMeteringStateAndMeasure(

void expectMeasureButton(double ev) {
find.descendant(
of: find.byType(MeteringMeasureButton),
of: find.byType(MeteringBottomControls),
matching: find.text('${ev.toStringAsFixed(1)}\n${S.current.ev}'),
);
}
4 changes: 2 additions & 2 deletions integration_test/purchases_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import 'package:lightmeter/data/models/ev_source_type.dart';
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
import 'package:lightmeter/data/shared_prefs_service.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/metering/components/shared/readings_container/components/equipment_profile_picker/widget_picker_equipment_profiles.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/extreme_exposure_pairs_container/widget_container_extreme_exposure_pairs.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/film_picker/widget_picker_film.dart';
Expand All @@ -21,6 +20,7 @@ import 'package:shared_preferences/shared_preferences.dart';

import '../integration_test/utils/widget_tester_actions.dart';
import 'mocks/iap_products_mock.dart';
import 'utils/finder_actions.dart';

@isTest
void testPurchases(String description) {
Expand Down Expand Up @@ -84,7 +84,7 @@ void _expectProMeteringScreen({required bool enabled}) {
expect(find.byType(NdValuePicker), findsOneWidget);
expect(
find.descendant(
of: find.byType(MeteringMeasureButton),
of: find.measureButton(),
matching: find.byWidgetPredicate((widget) => widget is Text && widget.data!.contains('\u2081\u2080\u2080')),
),
enabled ? findsOneWidget : findsNothing,
Expand Down
10 changes: 10 additions & 0 deletions integration_test/utils/finder_actions.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:lightmeter/screens/metering/components/bottom_controls/widget_bottom_controls.dart';
import 'package:lightmeter/screens/shared/animated_circular_button/widget_button_circular_animated.dart';

extension CommonFindersExtension on CommonFinders {
Finder measureButton() => find.descendant(
of: find.byType(MeteringBottomControls),
matching: find.byType(AnimatedCircluarButton),
);
}
8 changes: 4 additions & 4 deletions integration_test/utils/widget_tester_actions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import 'package:lightmeter/application_wrapper.dart';
import 'package:lightmeter/environment.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/metering/components/bottom_controls/components/measure_button/widget_button_measure.dart';
import 'package:lightmeter/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart';
import 'package:lightmeter/screens/metering/screen_metering.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';

import '../mocks/iap_products_mock.dart';
import '../mocks/paid_features_mock.dart';
import 'finder_actions.dart';
import 'platform_channel_mock.dart';

const mockPhotoEv100 = 8.3;
Expand Down Expand Up @@ -46,16 +46,16 @@ extension WidgetTesterCommonActions on WidgetTester {
}

Future<void> takePhoto() async {
await tap(find.byType(MeteringMeasureButton));
await tap(find.measureButton());
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> toggleIncidentMetering(double ev) async {
await tap(find.byType(MeteringMeasureButton));
await tap(find.measureButton());
await sendMockIncidentEv(ev);
await tap(find.byType(MeteringMeasureButton));
await tap(find.measureButton());
await pumpAndSettle();
}

Expand Down
6 changes: 4 additions & 2 deletions lib/application.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:lightmeter/platform_config.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:lightmeter/screens/timer/flow_timer.dart';

class Application extends StatelessWidget {
const Application({super.key});
Expand Down Expand Up @@ -40,8 +41,9 @@ class Application extends StatelessWidget {
),
initialRoute: "metering",
routes: {
"metering": (context) => const MeteringFlow(),
"settings": (context) => const SettingsFlow(),
"metering": (_) => const MeteringFlow(),
"settings": (_) => const SettingsFlow(),
"timer": (context) => TimerFlow(args: ModalRoute.of(context)!.settings.arguments! as TimerFlowArgs),
},
),
);
Expand Down
2 changes: 1 addition & 1 deletion lib/data/haptics_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class HapticsService {

Future<void> responseVibration() async => _tryVibrate(duration: 50, amplitude: 128);

Future<void> errorVibration() async => _tryVibrate(duration: 100, amplitude: 128);
Future<void> errorVibration() async => _tryVibrate(duration: 500, amplitude: 128);

Future<void> _tryVibrate({required int duration, required int amplitude}) async {
if (await _canVibrate()) {
Expand Down
4 changes: 4 additions & 0 deletions lib/data/shared_prefs_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class UserPreferencesService {

static const caffeineKey = "caffeine";
static const hapticsKey = "haptics";
static const autostartTimerKey = "autostartTimer";
static const volumeActionKey = "volumeAction";
static const localeKey = "locale";

Expand Down Expand Up @@ -127,6 +128,9 @@ class UserPreferencesService {
bool get haptics => _sharedPreferences.getBool(hapticsKey) ?? true;
set haptics(bool value) => _sharedPreferences.setBool(hapticsKey, value);

bool get autostartTimer => _sharedPreferences.getBool(autostartTimerKey) ?? true;
set autostartTimer(bool value) => _sharedPreferences.setBool(autostartTimerKey, value);

VolumeAction get volumeAction => VolumeAction.values.firstWhere(
(e) => e.toString() == _sharedPreferences.getString(volumeActionKey),
orElse: () => VolumeAction.shutter,
Expand Down
10 changes: 6 additions & 4 deletions lib/interactors/settings_interactor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ class SettingsInteractor {
void setCameraEvCalibration(double value) => _userPreferencesService.cameraEvCalibration = value;

double get lightSensorEvCalibration => _userPreferencesService.lightSensorEvCalibration;
void setLightSensorEvCalibration(double value) =>
_userPreferencesService.lightSensorEvCalibration = value;
void setLightSensorEvCalibration(double value) => _userPreferencesService.lightSensorEvCalibration = value;

bool get isCaffeineEnabled => _userPreferencesService.caffeine;
Future<void> enableCaffeine(bool enable) async {
Expand All @@ -31,12 +30,15 @@ class SettingsInteractor {
});
}

bool get isAutostartTimerEnabled => _userPreferencesService.autostartTimer;
void enableAutostartTimer(bool enable) => _userPreferencesService.autostartTimer = enable;

Future<void> disableVolumeHandling() async {
await _volumeEventsService.setVolumeHandling(false);
}

Future<void> restoreVolumeHandling() async {
await _volumeEventsService
.setVolumeHandling(_userPreferencesService.volumeAction != VolumeAction.none);
await _volumeEventsService.setVolumeHandling(_userPreferencesService.volumeAction != VolumeAction.none);
}

VolumeAction get volumeAction => _userPreferencesService.volumeAction;
Expand Down
24 changes: 24 additions & 0 deletions lib/interactors/timer_interactor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import 'package:lightmeter/data/haptics_service.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';

class TimerInteractor {
final UserPreferencesService _userPreferencesService;
final HapticsService _hapticsService;

TimerInteractor(
this._userPreferencesService,
this._hapticsService,
);

/// Executes vibration if haptics are enabled in settings
Future<void> startVibration() async {
if (_userPreferencesService.haptics) await _hapticsService.quickVibration();
}

/// Executes vibration if haptics are enabled in settings
Future<void> endVibration() async {
if (_userPreferencesService.haptics) await _hapticsService.errorVibration();
}

bool get isAutostartTimerEnabled => _userPreferencesService.autostartTimer;
}
4 changes: 3 additions & 1 deletion lib/l10n/intl_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"general": "General",
"keepScreenOn": "Keep screen on",
"haptics": "Haptics",
"autostartTimer": "Autostart timer",
"volumeKeysAction": "Shutter by volume keys",
"language": "Language",
"chooseLanguage": "Choose language",
Expand Down Expand Up @@ -117,5 +118,6 @@
"tooltipResetToZero": "Reset to zero",
"tooltipUseLightSensor": "Use lightsensor",
"tooltipUseCamera": "Use camera",
"tooltipOpenSettings": "Open settings"
"tooltipOpenSettings": "Open settings",
"exposurePair": "Exposure pair"
}
4 changes: 3 additions & 1 deletion lib/l10n/intl_fr.arb
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"general": "Général",
"keepScreenOn": "Garder l'écran allumé",
"haptics": "Haptiques",
"autostartTimer": "Minuterie de démarrage automatique",
"volumeKeysAction": "Obturateur par boutons de volume",
"language": "Langue",
"chooseLanguage": "Choisissez la langue",
Expand Down Expand Up @@ -117,5 +118,6 @@
"tooltipResetToZero": "Remise à zéro",
"tooltipUseLightSensor": "Utiliser un capteur de lumière",
"tooltipUseCamera": "Utiliser la caméra",
"tooltipOpenSettings": "Ouvrir les paramètres"
"tooltipOpenSettings": "Ouvrir les paramètres",
"exposurePair": "Paire d'exposition"
}
4 changes: 3 additions & 1 deletion lib/l10n/intl_ru.arb
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"general": "Общие",
"keepScreenOn": "Запрет блокировки",
"haptics": "Вибрация",
"autostartTimer": "Автозапуск таймера",
"volumeKeysAction": "Затвор по кнопкам громкости",
"language": "Язык",
"chooseLanguage": "Выберите язык",
Expand Down Expand Up @@ -117,5 +118,6 @@
"tooltipResetToZero": "Сбросить до 0",
"tooltipUseLightSensor": "Использовать датчик освещенности",
"tooltipUseCamera": "Использовать камеру",
"tooltipOpenSettings": "Открыть настройки"
"tooltipOpenSettings": "Открыть настройки",
"exposurePair": "Пара экспозиции"
}
4 changes: 3 additions & 1 deletion lib/l10n/intl_zh.arb
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"general": "通用",
"keepScreenOn": "保持屏幕常亮",
"haptics": "震动",
"autostartTimer": "自动启动定时器",
"volumeKeysAction": "音量键快门",
"language": "语言",
"chooseLanguage": "选择语言",
Expand Down Expand Up @@ -117,5 +118,6 @@
"resetToZero": "重置为零",
"tooltipUseLightSensor": "使用光线传感器",
"tooltipUseCamera": "使用摄像头",
"tooltipOpenSettings": "打开设置"
"tooltipOpenSettings": "打开设置",
"exposurePair": "曝光对"
}
18 changes: 8 additions & 10 deletions lib/screens/metering/bloc_metering.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@ 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/screens/metering/communication/bloc_communication_metering.dart';
import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart'
as communication_events;
import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart'
as communication_states;
import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart' as communication_events;
import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart' as communication_states;
import 'package:lightmeter/screens/metering/event_metering.dart';
import 'package:lightmeter/screens/metering/state_metering.dart';
import 'package:lightmeter/screens/metering/utils/notifier_volume_keys.dart';
Expand Down Expand Up @@ -45,8 +43,8 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
on<MeasureEvent>(_onMeasure, transformer: droppable());
on<MeasuredEvent>(_onMeasured);
on<MeasureErrorEvent>(_onMeasureError);
on<SettingsOpenedEvent>(_onSettingsOpened);
on<SettingsClosedEvent>(_onSettingsClosed);
on<ScreenOnTopOpenedEvent>(_onSettingsOpened);
on<ScreenOnTopClosedEvent>(_onSettingsClosed);
}

@override
Expand Down Expand Up @@ -191,11 +189,11 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
}
}

void _onSettingsOpened(SettingsOpenedEvent _, Emitter __) {
_communicationBloc.add(const communication_events.SettingsOpenedEvent());
void _onSettingsOpened(ScreenOnTopOpenedEvent _, Emitter __) {
_communicationBloc.add(const communication_events.ScreenOnTopOpenedEvent());
}

void _onSettingsClosed(SettingsClosedEvent _, Emitter __) {
_communicationBloc.add(const communication_events.SettingsClosedEvent());
void _onSettingsClosed(ScreenOnTopClosedEvent _, Emitter __) {
_communicationBloc.add(const communication_events.ScreenOnTopClosedEvent());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart';
import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart';

class MeteringCommunicationBloc
extends Bloc<MeteringCommunicationEvent, MeteringCommunicationState> {
class MeteringCommunicationBloc extends Bloc<MeteringCommunicationEvent, MeteringCommunicationState> {
MeteringCommunicationBloc() : super(const InitState()) {
// `MeasureState` is not const, so that `Bloc` treats each state as new and updates state stream
// ignore: prefer_const_constructors
on<MeasureEvent>((_, emit) => emit(MeasureState()));
on<MeteringInProgressEvent>((event, emit) => emit(MeteringInProgressState(event.ev100)));
on<MeteringEndedEvent>((event, emit) => emit(MeteringEndedState(event.ev100)));
on<SettingsOpenedEvent>((_, emit) => emit(const SettingsOpenedState()));
on<SettingsClosedEvent>((_, emit) => emit(const SettingsClosedState()));
on<ScreenOnTopOpenedEvent>((_, emit) => emit(const SettingsOpenedState()));
on<ScreenOnTopClosedEvent>((_, emit) => emit(const SettingsClosedState()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@ class MeteringEndedEvent extends MeasuredEvent {
int get hashCode => Object.hash(ev100, runtimeType);
}

class SettingsOpenedEvent extends ScreenEvent {
const SettingsOpenedEvent();
class ScreenOnTopOpenedEvent extends ScreenEvent {
const ScreenOnTopOpenedEvent();
}

class SettingsClosedEvent extends ScreenEvent {
const SettingsClosedEvent();
class ScreenOnTopClosedEvent extends ScreenEvent {
const ScreenOnTopClosedEvent();
}
Loading

0 comments on commit 5c27f72

Please sign in to comment.