mirror of
https://github.com/vodemn/m3_lightmeter.git
synced 2024-11-23 07:50:42 +00:00
ML-173 Add a timer for long exposures (#174)
* 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
This commit is contained in:
parent
bc7e6e14d0
commit
5c27f726c5
54 changed files with 1403 additions and 355 deletions
|
@ -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/data/shared_prefs_service.dart';
|
||||||
import 'package:lightmeter/generated/l10n.dart';
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
import 'package:lightmeter/res/dimens.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/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/film_picker/widget_picker_film.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/iso_picker/widget_picker_iso.dart';
|
||||||
|
@ -329,7 +329,7 @@ Future<void> _expectMeteringStateAndMeasure(
|
||||||
|
|
||||||
void expectMeasureButton(double ev) {
|
void expectMeasureButton(double ev) {
|
||||||
find.descendant(
|
find.descendant(
|
||||||
of: find.byType(MeteringMeasureButton),
|
of: find.byType(MeteringBottomControls),
|
||||||
matching: find.text('${ev.toStringAsFixed(1)}\n${S.current.ev}'),
|
matching: find.text('${ev.toStringAsFixed(1)}\n${S.current.ev}'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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/models/metering_screen_layout_config.dart';
|
||||||
import 'package:lightmeter/data/shared_prefs_service.dart';
|
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||||
import 'package:lightmeter/generated/l10n.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/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/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';
|
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/film_picker/widget_picker_film.dart';
|
||||||
|
@ -21,6 +20,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import '../integration_test/utils/widget_tester_actions.dart';
|
import '../integration_test/utils/widget_tester_actions.dart';
|
||||||
import 'mocks/iap_products_mock.dart';
|
import 'mocks/iap_products_mock.dart';
|
||||||
|
import 'utils/finder_actions.dart';
|
||||||
|
|
||||||
@isTest
|
@isTest
|
||||||
void testPurchases(String description) {
|
void testPurchases(String description) {
|
||||||
|
@ -84,7 +84,7 @@ void _expectProMeteringScreen({required bool enabled}) {
|
||||||
expect(find.byType(NdValuePicker), findsOneWidget);
|
expect(find.byType(NdValuePicker), findsOneWidget);
|
||||||
expect(
|
expect(
|
||||||
find.descendant(
|
find.descendant(
|
||||||
of: find.byType(MeteringMeasureButton),
|
of: find.measureButton(),
|
||||||
matching: find.byWidgetPredicate((widget) => widget is Text && widget.data!.contains('\u2081\u2080\u2080')),
|
matching: find.byWidgetPredicate((widget) => widget is Text && widget.data!.contains('\u2081\u2080\u2080')),
|
||||||
),
|
),
|
||||||
enabled ? findsOneWidget : findsNothing,
|
enabled ? findsOneWidget : findsNothing,
|
||||||
|
|
10
integration_test/utils/finder_actions.dart
Normal file
10
integration_test/utils/finder_actions.dart
Normal file
|
@ -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),
|
||||||
|
);
|
||||||
|
}
|
|
@ -5,7 +5,6 @@ import 'package:lightmeter/application_wrapper.dart';
|
||||||
import 'package:lightmeter/environment.dart';
|
import 'package:lightmeter/environment.dart';
|
||||||
import 'package:lightmeter/generated/l10n.dart';
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
import 'package:lightmeter/res/dimens.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/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart';
|
||||||
import 'package:lightmeter/screens/metering/screen_metering.dart';
|
import 'package:lightmeter/screens/metering/screen_metering.dart';
|
||||||
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
||||||
|
@ -13,6 +12,7 @@ import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||||
|
|
||||||
import '../mocks/iap_products_mock.dart';
|
import '../mocks/iap_products_mock.dart';
|
||||||
import '../mocks/paid_features_mock.dart';
|
import '../mocks/paid_features_mock.dart';
|
||||||
|
import 'finder_actions.dart';
|
||||||
import 'platform_channel_mock.dart';
|
import 'platform_channel_mock.dart';
|
||||||
|
|
||||||
const mockPhotoEv100 = 8.3;
|
const mockPhotoEv100 = 8.3;
|
||||||
|
@ -46,16 +46,16 @@ extension WidgetTesterCommonActions on WidgetTester {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> takePhoto() async {
|
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: 2)); // wait for circular progress indicator
|
||||||
await pump(const Duration(seconds: 1)); // wait for circular progress indicator
|
await pump(const Duration(seconds: 1)); // wait for circular progress indicator
|
||||||
await pumpAndSettle();
|
await pumpAndSettle();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> toggleIncidentMetering(double ev) async {
|
Future<void> toggleIncidentMetering(double ev) async {
|
||||||
await tap(find.byType(MeteringMeasureButton));
|
await tap(find.measureButton());
|
||||||
await sendMockIncidentEv(ev);
|
await sendMockIncidentEv(ev);
|
||||||
await tap(find.byType(MeteringMeasureButton));
|
await tap(find.measureButton());
|
||||||
await pumpAndSettle();
|
await pumpAndSettle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import 'package:lightmeter/platform_config.dart';
|
||||||
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
||||||
import 'package:lightmeter/screens/metering/flow_metering.dart';
|
import 'package:lightmeter/screens/metering/flow_metering.dart';
|
||||||
import 'package:lightmeter/screens/settings/flow_settings.dart';
|
import 'package:lightmeter/screens/settings/flow_settings.dart';
|
||||||
|
import 'package:lightmeter/screens/timer/flow_timer.dart';
|
||||||
|
|
||||||
class Application extends StatelessWidget {
|
class Application extends StatelessWidget {
|
||||||
const Application({super.key});
|
const Application({super.key});
|
||||||
|
@ -40,8 +41,9 @@ class Application extends StatelessWidget {
|
||||||
),
|
),
|
||||||
initialRoute: "metering",
|
initialRoute: "metering",
|
||||||
routes: {
|
routes: {
|
||||||
"metering": (context) => const MeteringFlow(),
|
"metering": (_) => const MeteringFlow(),
|
||||||
"settings": (context) => const SettingsFlow(),
|
"settings": (_) => const SettingsFlow(),
|
||||||
|
"timer": (context) => TimerFlow(args: ModalRoute.of(context)!.settings.arguments! as TimerFlowArgs),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -9,7 +9,7 @@ class HapticsService {
|
||||||
|
|
||||||
Future<void> responseVibration() async => _tryVibrate(duration: 50, amplitude: 128);
|
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 {
|
Future<void> _tryVibrate({required int duration, required int amplitude}) async {
|
||||||
if (await _canVibrate()) {
|
if (await _canVibrate()) {
|
||||||
|
|
|
@ -24,6 +24,7 @@ class UserPreferencesService {
|
||||||
|
|
||||||
static const caffeineKey = "caffeine";
|
static const caffeineKey = "caffeine";
|
||||||
static const hapticsKey = "haptics";
|
static const hapticsKey = "haptics";
|
||||||
|
static const autostartTimerKey = "autostartTimer";
|
||||||
static const volumeActionKey = "volumeAction";
|
static const volumeActionKey = "volumeAction";
|
||||||
static const localeKey = "locale";
|
static const localeKey = "locale";
|
||||||
|
|
||||||
|
@ -127,6 +128,9 @@ class UserPreferencesService {
|
||||||
bool get haptics => _sharedPreferences.getBool(hapticsKey) ?? true;
|
bool get haptics => _sharedPreferences.getBool(hapticsKey) ?? true;
|
||||||
set haptics(bool value) => _sharedPreferences.setBool(hapticsKey, value);
|
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(
|
VolumeAction get volumeAction => VolumeAction.values.firstWhere(
|
||||||
(e) => e.toString() == _sharedPreferences.getString(volumeActionKey),
|
(e) => e.toString() == _sharedPreferences.getString(volumeActionKey),
|
||||||
orElse: () => VolumeAction.shutter,
|
orElse: () => VolumeAction.shutter,
|
||||||
|
|
|
@ -21,8 +21,7 @@ class SettingsInteractor {
|
||||||
void setCameraEvCalibration(double value) => _userPreferencesService.cameraEvCalibration = value;
|
void setCameraEvCalibration(double value) => _userPreferencesService.cameraEvCalibration = value;
|
||||||
|
|
||||||
double get lightSensorEvCalibration => _userPreferencesService.lightSensorEvCalibration;
|
double get lightSensorEvCalibration => _userPreferencesService.lightSensorEvCalibration;
|
||||||
void setLightSensorEvCalibration(double value) =>
|
void setLightSensorEvCalibration(double value) => _userPreferencesService.lightSensorEvCalibration = value;
|
||||||
_userPreferencesService.lightSensorEvCalibration = value;
|
|
||||||
|
|
||||||
bool get isCaffeineEnabled => _userPreferencesService.caffeine;
|
bool get isCaffeineEnabled => _userPreferencesService.caffeine;
|
||||||
Future<void> enableCaffeine(bool enable) async {
|
Future<void> enableCaffeine(bool enable) async {
|
||||||
|
@ -31,12 +30,15 @@ class SettingsInteractor {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get isAutostartTimerEnabled => _userPreferencesService.autostartTimer;
|
||||||
|
void enableAutostartTimer(bool enable) => _userPreferencesService.autostartTimer = enable;
|
||||||
|
|
||||||
Future<void> disableVolumeHandling() async {
|
Future<void> disableVolumeHandling() async {
|
||||||
await _volumeEventsService.setVolumeHandling(false);
|
await _volumeEventsService.setVolumeHandling(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> restoreVolumeHandling() async {
|
Future<void> restoreVolumeHandling() async {
|
||||||
await _volumeEventsService
|
await _volumeEventsService.setVolumeHandling(_userPreferencesService.volumeAction != VolumeAction.none);
|
||||||
.setVolumeHandling(_userPreferencesService.volumeAction != VolumeAction.none);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
VolumeAction get volumeAction => _userPreferencesService.volumeAction;
|
VolumeAction get volumeAction => _userPreferencesService.volumeAction;
|
||||||
|
|
24
lib/interactors/timer_interactor.dart
Normal file
24
lib/interactors/timer_interactor.dart
Normal file
|
@ -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;
|
||||||
|
}
|
|
@ -72,6 +72,7 @@
|
||||||
"general": "General",
|
"general": "General",
|
||||||
"keepScreenOn": "Keep screen on",
|
"keepScreenOn": "Keep screen on",
|
||||||
"haptics": "Haptics",
|
"haptics": "Haptics",
|
||||||
|
"autostartTimer": "Autostart timer",
|
||||||
"volumeKeysAction": "Shutter by volume keys",
|
"volumeKeysAction": "Shutter by volume keys",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
"chooseLanguage": "Choose language",
|
"chooseLanguage": "Choose language",
|
||||||
|
@ -117,5 +118,6 @@
|
||||||
"tooltipResetToZero": "Reset to zero",
|
"tooltipResetToZero": "Reset to zero",
|
||||||
"tooltipUseLightSensor": "Use lightsensor",
|
"tooltipUseLightSensor": "Use lightsensor",
|
||||||
"tooltipUseCamera": "Use camera",
|
"tooltipUseCamera": "Use camera",
|
||||||
"tooltipOpenSettings": "Open settings"
|
"tooltipOpenSettings": "Open settings",
|
||||||
|
"exposurePair": "Exposure pair"
|
||||||
}
|
}
|
|
@ -72,6 +72,7 @@
|
||||||
"general": "Général",
|
"general": "Général",
|
||||||
"keepScreenOn": "Garder l'écran allumé",
|
"keepScreenOn": "Garder l'écran allumé",
|
||||||
"haptics": "Haptiques",
|
"haptics": "Haptiques",
|
||||||
|
"autostartTimer": "Minuterie de démarrage automatique",
|
||||||
"volumeKeysAction": "Obturateur par boutons de volume",
|
"volumeKeysAction": "Obturateur par boutons de volume",
|
||||||
"language": "Langue",
|
"language": "Langue",
|
||||||
"chooseLanguage": "Choisissez la langue",
|
"chooseLanguage": "Choisissez la langue",
|
||||||
|
@ -117,5 +118,6 @@
|
||||||
"tooltipResetToZero": "Remise à zéro",
|
"tooltipResetToZero": "Remise à zéro",
|
||||||
"tooltipUseLightSensor": "Utiliser un capteur de lumière",
|
"tooltipUseLightSensor": "Utiliser un capteur de lumière",
|
||||||
"tooltipUseCamera": "Utiliser la caméra",
|
"tooltipUseCamera": "Utiliser la caméra",
|
||||||
"tooltipOpenSettings": "Ouvrir les paramètres"
|
"tooltipOpenSettings": "Ouvrir les paramètres",
|
||||||
|
"exposurePair": "Paire d'exposition"
|
||||||
}
|
}
|
|
@ -72,6 +72,7 @@
|
||||||
"general": "Общие",
|
"general": "Общие",
|
||||||
"keepScreenOn": "Запрет блокировки",
|
"keepScreenOn": "Запрет блокировки",
|
||||||
"haptics": "Вибрация",
|
"haptics": "Вибрация",
|
||||||
|
"autostartTimer": "Автозапуск таймера",
|
||||||
"volumeKeysAction": "Затвор по кнопкам громкости",
|
"volumeKeysAction": "Затвор по кнопкам громкости",
|
||||||
"language": "Язык",
|
"language": "Язык",
|
||||||
"chooseLanguage": "Выберите язык",
|
"chooseLanguage": "Выберите язык",
|
||||||
|
@ -117,5 +118,6 @@
|
||||||
"tooltipResetToZero": "Сбросить до 0",
|
"tooltipResetToZero": "Сбросить до 0",
|
||||||
"tooltipUseLightSensor": "Использовать датчик освещенности",
|
"tooltipUseLightSensor": "Использовать датчик освещенности",
|
||||||
"tooltipUseCamera": "Использовать камеру",
|
"tooltipUseCamera": "Использовать камеру",
|
||||||
"tooltipOpenSettings": "Открыть настройки"
|
"tooltipOpenSettings": "Открыть настройки",
|
||||||
|
"exposurePair": "Пара экспозиции"
|
||||||
}
|
}
|
|
@ -72,6 +72,7 @@
|
||||||
"general": "通用",
|
"general": "通用",
|
||||||
"keepScreenOn": "保持屏幕常亮",
|
"keepScreenOn": "保持屏幕常亮",
|
||||||
"haptics": "震动",
|
"haptics": "震动",
|
||||||
|
"autostartTimer": "自动启动定时器",
|
||||||
"volumeKeysAction": "音量键快门",
|
"volumeKeysAction": "音量键快门",
|
||||||
"language": "语言",
|
"language": "语言",
|
||||||
"chooseLanguage": "选择语言",
|
"chooseLanguage": "选择语言",
|
||||||
|
@ -117,5 +118,6 @@
|
||||||
"resetToZero": "重置为零",
|
"resetToZero": "重置为零",
|
||||||
"tooltipUseLightSensor": "使用光线传感器",
|
"tooltipUseLightSensor": "使用光线传感器",
|
||||||
"tooltipUseCamera": "使用摄像头",
|
"tooltipUseCamera": "使用摄像头",
|
||||||
"tooltipOpenSettings": "打开设置"
|
"tooltipOpenSettings": "打开设置",
|
||||||
|
"exposurePair": "曝光对"
|
||||||
}
|
}
|
|
@ -6,10 +6,8 @@ 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/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' as communication_events;
|
||||||
as communication_events;
|
import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart' as communication_states;
|
||||||
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/event_metering.dart';
|
||||||
import 'package:lightmeter/screens/metering/state_metering.dart';
|
import 'package:lightmeter/screens/metering/state_metering.dart';
|
||||||
import 'package:lightmeter/screens/metering/utils/notifier_volume_keys.dart';
|
import 'package:lightmeter/screens/metering/utils/notifier_volume_keys.dart';
|
||||||
|
@ -45,8 +43,8 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
|
||||||
on<MeasureEvent>(_onMeasure, transformer: droppable());
|
on<MeasureEvent>(_onMeasure, transformer: droppable());
|
||||||
on<MeasuredEvent>(_onMeasured);
|
on<MeasuredEvent>(_onMeasured);
|
||||||
on<MeasureErrorEvent>(_onMeasureError);
|
on<MeasureErrorEvent>(_onMeasureError);
|
||||||
on<SettingsOpenedEvent>(_onSettingsOpened);
|
on<ScreenOnTopOpenedEvent>(_onSettingsOpened);
|
||||||
on<SettingsClosedEvent>(_onSettingsClosed);
|
on<ScreenOnTopClosedEvent>(_onSettingsClosed);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -191,11 +189,11 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSettingsOpened(SettingsOpenedEvent _, Emitter __) {
|
void _onSettingsOpened(ScreenOnTopOpenedEvent _, Emitter __) {
|
||||||
_communicationBloc.add(const communication_events.SettingsOpenedEvent());
|
_communicationBloc.add(const communication_events.ScreenOnTopOpenedEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSettingsClosed(SettingsClosedEvent _, Emitter __) {
|
void _onSettingsClosed(ScreenOnTopClosedEvent _, Emitter __) {
|
||||||
_communicationBloc.add(const communication_events.SettingsClosedEvent());
|
_communicationBloc.add(const communication_events.ScreenOnTopClosedEvent());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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/event_communication_metering.dart';
|
||||||
import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart';
|
import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart';
|
||||||
|
|
||||||
class MeteringCommunicationBloc
|
class MeteringCommunicationBloc extends Bloc<MeteringCommunicationEvent, MeteringCommunicationState> {
|
||||||
extends Bloc<MeteringCommunicationEvent, MeteringCommunicationState> {
|
|
||||||
MeteringCommunicationBloc() : super(const InitState()) {
|
MeteringCommunicationBloc() : super(const InitState()) {
|
||||||
// `MeasureState` is not const, so that `Bloc` treats each state as new and updates state stream
|
// `MeasureState` is not const, so that `Bloc` treats each state as new and updates state stream
|
||||||
// ignore: prefer_const_constructors
|
// ignore: prefer_const_constructors
|
||||||
on<MeasureEvent>((_, emit) => emit(MeasureState()));
|
on<MeasureEvent>((_, emit) => emit(MeasureState()));
|
||||||
on<MeteringInProgressEvent>((event, emit) => emit(MeteringInProgressState(event.ev100)));
|
on<MeteringInProgressEvent>((event, emit) => emit(MeteringInProgressState(event.ev100)));
|
||||||
on<MeteringEndedEvent>((event, emit) => emit(MeteringEndedState(event.ev100)));
|
on<MeteringEndedEvent>((event, emit) => emit(MeteringEndedState(event.ev100)));
|
||||||
on<SettingsOpenedEvent>((_, emit) => emit(const SettingsOpenedState()));
|
on<ScreenOnTopOpenedEvent>((_, emit) => emit(const SettingsOpenedState()));
|
||||||
on<SettingsClosedEvent>((_, emit) => emit(const SettingsClosedState()));
|
on<ScreenOnTopClosedEvent>((_, emit) => emit(const SettingsClosedState()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,10 +48,10 @@ class MeteringEndedEvent extends MeasuredEvent {
|
||||||
int get hashCode => Object.hash(ev100, runtimeType);
|
int get hashCode => Object.hash(ev100, runtimeType);
|
||||||
}
|
}
|
||||||
|
|
||||||
class SettingsOpenedEvent extends ScreenEvent {
|
class ScreenOnTopOpenedEvent extends ScreenEvent {
|
||||||
const SettingsOpenedEvent();
|
const ScreenOnTopOpenedEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
class SettingsClosedEvent extends ScreenEvent {
|
class ScreenOnTopClosedEvent extends ScreenEvent {
|
||||||
const SettingsClosedEvent();
|
const ScreenOnTopClosedEvent();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,120 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:lightmeter/generated/l10n.dart';
|
|
||||||
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
|
||||||
import 'package:lightmeter/res/dimens.dart';
|
|
||||||
import 'package:lightmeter/screens/shared/filled_circle/widget_circle_filled.dart';
|
|
||||||
import 'package:lightmeter/utils/context_utils.dart';
|
|
||||||
|
|
||||||
const String _subscript100 = '\u2081\u2080\u2080';
|
|
||||||
|
|
||||||
class MeteringMeasureButton extends StatefulWidget {
|
|
||||||
final double? ev;
|
|
||||||
final double? ev100;
|
|
||||||
final bool isMetering;
|
|
||||||
final VoidCallback onTap;
|
|
||||||
|
|
||||||
const MeteringMeasureButton({
|
|
||||||
required this.ev,
|
|
||||||
required this.ev100,
|
|
||||||
required this.isMetering,
|
|
||||||
required this.onTap,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<MeteringMeasureButton> createState() => _MeteringMeasureButtonState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MeteringMeasureButtonState extends State<MeteringMeasureButton> {
|
|
||||||
bool _isPressed = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(covariant MeteringMeasureButton oldWidget) {
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
if (oldWidget.isMetering != widget.isMetering) {
|
|
||||||
_isPressed = widget.isMetering;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: widget.onTap,
|
|
||||||
onTapDown: (_) {
|
|
||||||
setState(() {
|
|
||||||
_isPressed = true;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onTapUp: (_) {
|
|
||||||
setState(() {
|
|
||||||
_isPressed = false;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onTapCancel: () {
|
|
||||||
setState(() {
|
|
||||||
_isPressed = false;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
child: SizedBox.fromSize(
|
|
||||||
size: const Size.square(Dimens.grid72),
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
Center(
|
|
||||||
child: AnimatedScale(
|
|
||||||
duration: Dimens.durationS,
|
|
||||||
scale: _isPressed ? 0.9 : 1.0,
|
|
||||||
child: FilledCircle(
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
size: Dimens.grid72 - Dimens.grid8,
|
|
||||||
child: Center(
|
|
||||||
child: widget.ev != null ? _EvValueText(ev: widget.ev!, ev100: widget.ev100!) : null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned.fill(
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
/// This key is needed to make indicator start from the same point every time
|
|
||||||
key: ValueKey(widget.isMetering),
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
value: widget.isMetering ? null : 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _EvValueText extends StatelessWidget {
|
|
||||||
final double ev;
|
|
||||||
final double ev100;
|
|
||||||
|
|
||||||
const _EvValueText({
|
|
||||||
required this.ev,
|
|
||||||
required this.ev100,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
return Text(
|
|
||||||
_text(context),
|
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.surface),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String _text(BuildContext context) {
|
|
||||||
final bool showEv100 = context.isPro && UserPreferencesProvider.showEv100Of(context);
|
|
||||||
final StringBuffer buffer = StringBuffer()
|
|
||||||
..writeAll([
|
|
||||||
(showEv100 ? ev100 : ev).toStringAsFixed(1),
|
|
||||||
'\n',
|
|
||||||
S.of(context).ev,
|
|
||||||
if (showEv100) _subscript100,
|
|
||||||
]);
|
|
||||||
return buffer.toString();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,48 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:lightmeter/res/dimens.dart';
|
|
||||||
|
|
||||||
import 'package:lightmeter/screens/metering/components/bottom_controls/widget_bottom_controls.dart';
|
|
||||||
|
|
||||||
class MeteringBottomControlsProvider extends StatelessWidget {
|
|
||||||
final double? ev;
|
|
||||||
final double? ev100;
|
|
||||||
final bool isMetering;
|
|
||||||
final VoidCallback? onSwitchEvSourceType;
|
|
||||||
final VoidCallback onMeasure;
|
|
||||||
final VoidCallback onSettings;
|
|
||||||
|
|
||||||
const MeteringBottomControlsProvider({
|
|
||||||
required this.ev,
|
|
||||||
required this.ev100,
|
|
||||||
required this.isMetering,
|
|
||||||
required this.onSwitchEvSourceType,
|
|
||||||
required this.onMeasure,
|
|
||||||
required this.onSettings,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final scheme = Theme.of(context).colorScheme;
|
|
||||||
return IconButtonTheme(
|
|
||||||
data: IconButtonThemeData(
|
|
||||||
style: ButtonStyle(
|
|
||||||
backgroundColor: MaterialStatePropertyAll(scheme.surface),
|
|
||||||
elevation: const MaterialStatePropertyAll(4),
|
|
||||||
iconColor: MaterialStatePropertyAll(scheme.onSurface),
|
|
||||||
shadowColor: const MaterialStatePropertyAll(Colors.transparent),
|
|
||||||
surfaceTintColor: MaterialStatePropertyAll(scheme.surfaceTint),
|
|
||||||
fixedSize: const MaterialStatePropertyAll(Size(Dimens.grid48, Dimens.grid48)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: MeteringBottomControls(
|
|
||||||
ev: ev,
|
|
||||||
ev100: ev100,
|
|
||||||
isMetering: isMetering,
|
|
||||||
onSwitchEvSourceType: onSwitchEvSourceType,
|
|
||||||
onMeasure: onMeasure,
|
|
||||||
onSettings: onSettings,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,8 +2,9 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:lightmeter/data/models/ev_source_type.dart';
|
import 'package:lightmeter/data/models/ev_source_type.dart';
|
||||||
import 'package:lightmeter/generated/l10n.dart';
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
||||||
import 'package:lightmeter/res/dimens.dart';
|
import 'package:lightmeter/screens/shared/animated_circular_button/widget_button_circular_animated.dart';
|
||||||
import 'package:lightmeter/screens/metering/components/bottom_controls/components/measure_button/widget_button_measure.dart';
|
import 'package:lightmeter/screens/shared/bottom_controls_bar/widget_bottom_controls_bar.dart';
|
||||||
|
import 'package:lightmeter/utils/context_utils.dart';
|
||||||
|
|
||||||
class MeteringBottomControls extends StatelessWidget {
|
class MeteringBottomControls extends StatelessWidget {
|
||||||
final double? ev;
|
final double? ev;
|
||||||
|
@ -25,59 +26,64 @@ class MeteringBottomControls extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ClipRRect(
|
return BottomControlsBar(
|
||||||
borderRadius: const BorderRadius.only(
|
left: onSwitchEvSourceType != null
|
||||||
topLeft: Radius.circular(Dimens.borderRadiusL),
|
? IconButton(
|
||||||
topRight: Radius.circular(Dimens.borderRadiusL),
|
|
||||||
),
|
|
||||||
child: ColoredBox(
|
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: SafeArea(
|
|
||||||
top: false,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: Dimens.paddingL),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
||||||
children: [
|
|
||||||
if (onSwitchEvSourceType != null)
|
|
||||||
Expanded(
|
|
||||||
child: Center(
|
|
||||||
child: IconButton(
|
|
||||||
onPressed: onSwitchEvSourceType,
|
onPressed: onSwitchEvSourceType,
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
UserPreferencesProvider.evSourceTypeOf(context) != EvSourceType.camera
|
UserPreferencesProvider.evSourceTypeOf(context) != EvSourceType.camera
|
||||||
? Icons.camera_rear
|
? Icons.camera_rear
|
||||||
: Icons.wb_incandescent,
|
: Icons.wb_incandescent,
|
||||||
),
|
),
|
||||||
tooltip:
|
tooltip: UserPreferencesProvider.evSourceTypeOf(context) != EvSourceType.camera
|
||||||
UserPreferencesProvider.evSourceTypeOf(context) != EvSourceType.camera
|
|
||||||
? S.of(context).tooltipUseCamera
|
? S.of(context).tooltipUseCamera
|
||||||
: S.of(context).tooltipUseLightSensor,
|
: S.of(context).tooltipUseLightSensor,
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
else
|
: null,
|
||||||
const Spacer(),
|
center: AnimatedCircluarButton(
|
||||||
MeteringMeasureButton(
|
progress: isMetering ? null : 1.0,
|
||||||
ev: ev,
|
isPressed: isMetering,
|
||||||
ev100: ev100,
|
onPressed: onMeasure,
|
||||||
isMetering: isMetering,
|
child: ev != null ? _EvValueText(ev: ev!, ev100: ev100!) : null,
|
||||||
onTap: onMeasure,
|
|
||||||
),
|
),
|
||||||
Expanded(
|
right: IconButton(
|
||||||
child: Center(
|
|
||||||
child: IconButton(
|
|
||||||
onPressed: onSettings,
|
onPressed: onSettings,
|
||||||
icon: const Icon(Icons.settings),
|
icon: const Icon(Icons.settings),
|
||||||
tooltip: S.of(context).tooltipOpenSettings,
|
tooltip: S.of(context).tooltipOpenSettings,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _EvValueText extends StatelessWidget {
|
||||||
|
static const String _subscript100 = '\u2081\u2080\u2080';
|
||||||
|
final double ev;
|
||||||
|
final double ev100;
|
||||||
|
|
||||||
|
const _EvValueText({
|
||||||
|
required this.ev,
|
||||||
|
required this.ev100,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
return Text(
|
||||||
|
_text(context),
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.surface),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _text(BuildContext context) {
|
||||||
|
final bool showEv100 = context.isPro && UserPreferencesProvider.showEv100Of(context);
|
||||||
|
final StringBuffer buffer = StringBuffer()
|
||||||
|
..writeAll([
|
||||||
|
(showEv100 ? ev100 : ev).toStringAsFixed(1),
|
||||||
|
'\n',
|
||||||
|
S.of(context).ev,
|
||||||
|
if (showEv100) _subscript100,
|
||||||
|
]);
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ class CameraContainerProvider extends StatelessWidget {
|
||||||
final ValueChanged<IsoValue> onIsoChanged;
|
final ValueChanged<IsoValue> onIsoChanged;
|
||||||
final ValueChanged<NdValue> onNdChanged;
|
final ValueChanged<NdValue> onNdChanged;
|
||||||
final List<ExposurePair> exposurePairs;
|
final List<ExposurePair> exposurePairs;
|
||||||
|
final ValueChanged<ExposurePair> onExposurePairTap;
|
||||||
|
|
||||||
const CameraContainerProvider({
|
const CameraContainerProvider({
|
||||||
required this.fastest,
|
required this.fastest,
|
||||||
|
@ -27,6 +28,7 @@ class CameraContainerProvider extends StatelessWidget {
|
||||||
required this.onIsoChanged,
|
required this.onIsoChanged,
|
||||||
required this.onNdChanged,
|
required this.onNdChanged,
|
||||||
required this.exposurePairs,
|
required this.exposurePairs,
|
||||||
|
required this.onExposurePairTap,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -54,6 +56,7 @@ class CameraContainerProvider extends StatelessWidget {
|
||||||
onIsoChanged: onIsoChanged,
|
onIsoChanged: onIsoChanged,
|
||||||
onNdChanged: onNdChanged,
|
onNdChanged: onNdChanged,
|
||||||
exposurePairs: exposurePairs,
|
exposurePairs: exposurePairs,
|
||||||
|
onExposurePairTap: onExposurePairTap,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ class CameraContainer extends StatelessWidget {
|
||||||
final ValueChanged<IsoValue> onIsoChanged;
|
final ValueChanged<IsoValue> onIsoChanged;
|
||||||
final ValueChanged<NdValue> onNdChanged;
|
final ValueChanged<NdValue> onNdChanged;
|
||||||
final List<ExposurePair> exposurePairs;
|
final List<ExposurePair> exposurePairs;
|
||||||
|
final ValueChanged<ExposurePair> onExposurePairTap;
|
||||||
|
|
||||||
const CameraContainer({
|
const CameraContainer({
|
||||||
required this.fastest,
|
required this.fastest,
|
||||||
|
@ -38,6 +39,7 @@ class CameraContainer extends StatelessWidget {
|
||||||
required this.onIsoChanged,
|
required this.onIsoChanged,
|
||||||
required this.onNdChanged,
|
required this.onNdChanged,
|
||||||
required this.exposurePairs,
|
required this.exposurePairs,
|
||||||
|
required this.onExposurePairTap,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -81,7 +83,10 @@ class CameraContainer extends StatelessWidget {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.only(top: topBarOverflow >= 0 ? topBarOverflow : 0),
|
padding: EdgeInsets.only(top: topBarOverflow >= 0 ? topBarOverflow : 0),
|
||||||
child: ExposurePairsList(exposurePairs),
|
child: ExposurePairsList(
|
||||||
|
exposurePairs,
|
||||||
|
onExposurePairTap: onExposurePairTap,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: Dimens.grid8),
|
const SizedBox(width: Dimens.grid8),
|
||||||
|
|
|
@ -15,6 +15,7 @@ class LightSensorContainerProvider extends StatelessWidget {
|
||||||
final ValueChanged<IsoValue> onIsoChanged;
|
final ValueChanged<IsoValue> onIsoChanged;
|
||||||
final ValueChanged<NdValue> onNdChanged;
|
final ValueChanged<NdValue> onNdChanged;
|
||||||
final List<ExposurePair> exposurePairs;
|
final List<ExposurePair> exposurePairs;
|
||||||
|
final ValueChanged<ExposurePair> onExposurePairTap;
|
||||||
|
|
||||||
const LightSensorContainerProvider({
|
const LightSensorContainerProvider({
|
||||||
required this.fastest,
|
required this.fastest,
|
||||||
|
@ -24,6 +25,7 @@ class LightSensorContainerProvider extends StatelessWidget {
|
||||||
required this.onIsoChanged,
|
required this.onIsoChanged,
|
||||||
required this.onNdChanged,
|
required this.onNdChanged,
|
||||||
required this.exposurePairs,
|
required this.exposurePairs,
|
||||||
|
required this.onExposurePairTap,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -43,6 +45,7 @@ class LightSensorContainerProvider extends StatelessWidget {
|
||||||
onIsoChanged: onIsoChanged,
|
onIsoChanged: onIsoChanged,
|
||||||
onNdChanged: onNdChanged,
|
onNdChanged: onNdChanged,
|
||||||
exposurePairs: exposurePairs,
|
exposurePairs: exposurePairs,
|
||||||
|
onExposurePairTap: onExposurePairTap,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ class LightSensorContainer extends StatelessWidget {
|
||||||
final ValueChanged<IsoValue> onIsoChanged;
|
final ValueChanged<IsoValue> onIsoChanged;
|
||||||
final ValueChanged<NdValue> onNdChanged;
|
final ValueChanged<NdValue> onNdChanged;
|
||||||
final List<ExposurePair> exposurePairs;
|
final List<ExposurePair> exposurePairs;
|
||||||
|
final ValueChanged<ExposurePair> onExposurePairTap;
|
||||||
|
|
||||||
const LightSensorContainer({
|
const LightSensorContainer({
|
||||||
required this.fastest,
|
required this.fastest,
|
||||||
|
@ -23,6 +24,7 @@ class LightSensorContainer extends StatelessWidget {
|
||||||
required this.onIsoChanged,
|
required this.onIsoChanged,
|
||||||
required this.onNdChanged,
|
required this.onNdChanged,
|
||||||
required this.exposurePairs,
|
required this.exposurePairs,
|
||||||
|
required this.onExposurePairTap,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -43,7 +45,12 @@ class LightSensorContainer extends StatelessWidget {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
|
padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
|
||||||
child: Center(child: ExposurePairsList(exposurePairs)),
|
child: Center(
|
||||||
|
child: ExposurePairsList(
|
||||||
|
exposurePairs,
|
||||||
|
onExposurePairTap: onExposurePairTap,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -44,9 +44,8 @@ class ExposurePairsListItem<T extends PhotographyStopValue> extends StatelessWid
|
||||||
case StopType.full:
|
case StopType.full:
|
||||||
return Theme.of(context).textTheme.bodyLarge!;
|
return Theme.of(context).textTheme.bodyLarge!;
|
||||||
case StopType.half:
|
case StopType.half:
|
||||||
return Theme.of(context).textTheme.bodyMedium!;
|
|
||||||
case StopType.third:
|
case StopType.third:
|
||||||
return Theme.of(context).textTheme.bodySmall!;
|
return Theme.of(context).textTheme.bodyMedium!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,7 +54,6 @@ class ExposurePairsListItem<T extends PhotographyStopValue> extends StatelessWid
|
||||||
case StopType.full:
|
case StopType.full:
|
||||||
return Dimens.grid16;
|
return Dimens.grid16;
|
||||||
case StopType.half:
|
case StopType.half:
|
||||||
return Dimens.grid8;
|
|
||||||
case StopType.third:
|
case StopType.third:
|
||||||
return Dimens.grid8;
|
return Dimens.grid8;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,13 @@ import 'package:lightmeter/providers/films_provider.dart';
|
||||||
import 'package:lightmeter/res/dimens.dart';
|
import 'package:lightmeter/res/dimens.dart';
|
||||||
import 'package:lightmeter/screens/metering/components/shared/exposure_pairs_list/components/exposure_pairs_list_item/widget_item_list_exposure_pairs.dart';
|
import 'package:lightmeter/screens/metering/components/shared/exposure_pairs_list/components/exposure_pairs_list_item/widget_item_list_exposure_pairs.dart';
|
||||||
import 'package:lightmeter/screens/shared/icon_placeholder/widget_icon_placeholder.dart';
|
import 'package:lightmeter/screens/shared/icon_placeholder/widget_icon_placeholder.dart';
|
||||||
|
import 'package:lightmeter/utils/context_utils.dart';
|
||||||
|
|
||||||
class ExposurePairsList extends StatelessWidget {
|
class ExposurePairsList extends StatelessWidget {
|
||||||
final List<ExposurePair> exposurePairs;
|
final List<ExposurePair> exposurePairs;
|
||||||
|
final ValueChanged<ExposurePair> onExposurePairTap;
|
||||||
|
|
||||||
const ExposurePairsList(this.exposurePairs, {super.key});
|
const ExposurePairsList(this.exposurePairs, {required this.onExposurePairTap, super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -28,10 +30,17 @@ class ExposurePairsList extends StatelessWidget {
|
||||||
key: ValueKey(exposurePairs.hashCode),
|
key: ValueKey(exposurePairs.hashCode),
|
||||||
padding: const EdgeInsets.symmetric(vertical: Dimens.paddingL),
|
padding: const EdgeInsets.symmetric(vertical: Dimens.paddingL),
|
||||||
itemCount: exposurePairs.length,
|
itemCount: exposurePairs.length,
|
||||||
itemBuilder: (_, index) => Stack(
|
itemBuilder: (_, index) {
|
||||||
|
final exposurePair = ExposurePair(
|
||||||
|
exposurePairs[index].aperture,
|
||||||
|
Films.selectedOf(context).reciprocityFailure(exposurePairs[index].shutterSpeed),
|
||||||
|
);
|
||||||
|
return Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
GestureDetector(
|
||||||
|
onTap: context.isPro ? () => onExposurePairTap(exposurePair) : null,
|
||||||
|
child: Row(
|
||||||
key: ValueKey(index),
|
key: ValueKey(index),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
@ -39,7 +48,7 @@ class ExposurePairsList extends StatelessWidget {
|
||||||
child: Align(
|
child: Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: ExposurePairsListItem(
|
child: ExposurePairsListItem(
|
||||||
exposurePairs[index].aperture,
|
exposurePair.aperture,
|
||||||
tickOnTheLeft: false,
|
tickOnTheLeft: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -48,14 +57,14 @@ class ExposurePairsList extends StatelessWidget {
|
||||||
child: Align(
|
child: Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: ExposurePairsListItem(
|
child: ExposurePairsListItem(
|
||||||
Films.selectedOf(context)
|
exposurePair.shutterSpeed,
|
||||||
.reciprocityFailure(exposurePairs[index].shutterSpeed),
|
|
||||||
tickOnTheLeft: true,
|
tickOnTheLeft: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 0,
|
top: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
|
@ -63,9 +72,7 @@ class ExposurePairsList extends StatelessWidget {
|
||||||
builder: (context, constraints) => Align(
|
builder: (context, constraints) => Align(
|
||||||
alignment: index == 0
|
alignment: index == 0
|
||||||
? Alignment.bottomCenter
|
? Alignment.bottomCenter
|
||||||
: (index == exposurePairs.length - 1
|
: (index == exposurePairs.length - 1 ? Alignment.topCenter : Alignment.center),
|
||||||
? Alignment.topCenter
|
|
||||||
: Alignment.center),
|
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: index == 0 || index == exposurePairs.length - 1
|
height: index == 0 || index == exposurePairs.length - 1
|
||||||
? constraints.maxHeight / 2
|
? constraints.maxHeight / 2
|
||||||
|
@ -79,7 +86,8 @@ class ExposurePairsList extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -14,11 +14,13 @@ class ReadingValue {
|
||||||
|
|
||||||
class ReadingValueContainer extends StatelessWidget implements AnimatedDialogClosedChild {
|
class ReadingValueContainer extends StatelessWidget implements AnimatedDialogClosedChild {
|
||||||
late final List<Widget> _items;
|
late final List<Widget> _items;
|
||||||
|
final bool locked;
|
||||||
final Color? color;
|
final Color? color;
|
||||||
final Color? textColor;
|
final Color? textColor;
|
||||||
|
|
||||||
ReadingValueContainer({
|
ReadingValueContainer({
|
||||||
required List<ReadingValue> values,
|
required List<ReadingValue> values,
|
||||||
|
this.locked = false,
|
||||||
this.color,
|
this.color,
|
||||||
this.textColor,
|
this.textColor,
|
||||||
super.key,
|
super.key,
|
||||||
|
@ -34,6 +36,7 @@ class ReadingValueContainer extends StatelessWidget implements AnimatedDialogClo
|
||||||
|
|
||||||
ReadingValueContainer.singleValue({
|
ReadingValueContainer.singleValue({
|
||||||
required ReadingValue value,
|
required ReadingValue value,
|
||||||
|
this.locked = false,
|
||||||
this.color,
|
this.color,
|
||||||
this.textColor,
|
this.textColor,
|
||||||
super.key,
|
super.key,
|
||||||
|
@ -50,11 +53,25 @@ class ReadingValueContainer extends StatelessWidget implements AnimatedDialogClo
|
||||||
color: backgroundColor(context),
|
color: backgroundColor(context),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(Dimens.paddingM),
|
padding: const EdgeInsets.all(Dimens.paddingM),
|
||||||
child: Column(
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
if (locked)
|
||||||
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Icon(
|
||||||
|
Icons.lock,
|
||||||
|
size: Theme.of(context).textTheme.labelMedium!.fontSize,
|
||||||
|
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: _items,
|
children: _items,
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -39,10 +39,10 @@ class MeasureErrorEvent extends MeteringEvent {
|
||||||
const MeasureErrorEvent({required this.isMetering});
|
const MeasureErrorEvent({required this.isMetering});
|
||||||
}
|
}
|
||||||
|
|
||||||
class SettingsOpenedEvent extends MeteringEvent {
|
class ScreenOnTopOpenedEvent extends MeteringEvent {
|
||||||
const SettingsOpenedEvent();
|
const ScreenOnTopOpenedEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
class SettingsClosedEvent extends MeteringEvent {
|
class ScreenOnTopClosedEvent extends MeteringEvent {
|
||||||
const SettingsClosedEvent();
|
const ScreenOnTopClosedEvent();
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,12 +8,13 @@ import 'package:lightmeter/providers/equipment_profile_provider.dart';
|
||||||
import 'package:lightmeter/providers/services_provider.dart';
|
import 'package:lightmeter/providers/services_provider.dart';
|
||||||
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
||||||
import 'package:lightmeter/screens/metering/bloc_metering.dart';
|
import 'package:lightmeter/screens/metering/bloc_metering.dart';
|
||||||
import 'package:lightmeter/screens/metering/components/bottom_controls/provider_bottom_controls.dart';
|
import 'package:lightmeter/screens/metering/components/bottom_controls/widget_bottom_controls.dart';
|
||||||
import 'package:lightmeter/screens/metering/components/camera_container/provider_container_camera.dart';
|
import 'package:lightmeter/screens/metering/components/camera_container/provider_container_camera.dart';
|
||||||
import 'package:lightmeter/screens/metering/components/light_sensor_container/provider_container_light_sensor.dart';
|
import 'package:lightmeter/screens/metering/components/light_sensor_container/provider_container_light_sensor.dart';
|
||||||
import 'package:lightmeter/screens/metering/event_metering.dart';
|
import 'package:lightmeter/screens/metering/event_metering.dart';
|
||||||
import 'package:lightmeter/screens/metering/state_metering.dart';
|
import 'package:lightmeter/screens/metering/state_metering.dart';
|
||||||
import 'package:lightmeter/screens/metering/utils/listener_equipment_profiles.dart';
|
import 'package:lightmeter/screens/metering/utils/listener_equipment_profiles.dart';
|
||||||
|
import 'package:lightmeter/screens/timer/flow_timer.dart';
|
||||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||||
|
|
||||||
class MeteringScreen extends StatelessWidget {
|
class MeteringScreen extends StatelessWidget {
|
||||||
|
@ -34,11 +35,20 @@ class MeteringScreen extends StatelessWidget {
|
||||||
nd: state.nd,
|
nd: state.nd,
|
||||||
onIsoChanged: (value) => context.read<MeteringBloc>().add(IsoChangedEvent(value)),
|
onIsoChanged: (value) => context.read<MeteringBloc>().add(IsoChangedEvent(value)),
|
||||||
onNdChanged: (value) => context.read<MeteringBloc>().add(NdChangedEvent(value)),
|
onNdChanged: (value) => context.read<MeteringBloc>().add(NdChangedEvent(value)),
|
||||||
|
onExposurePairTap: (value) => pushNamed(
|
||||||
|
context,
|
||||||
|
'timer',
|
||||||
|
arguments: TimerFlowArgs(
|
||||||
|
exposurePair: value,
|
||||||
|
isoValue: state.iso,
|
||||||
|
ndValue: state.nd,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
BlocBuilder<MeteringBloc, MeteringState>(
|
BlocBuilder<MeteringBloc, MeteringState>(
|
||||||
builder: (context, state) => MeteringBottomControlsProvider(
|
builder: (context, state) => MeteringBottomControls(
|
||||||
ev: state is MeteringDataState ? state.ev : null,
|
ev: state is MeteringDataState ? state.ev : null,
|
||||||
ev100: state is MeteringDataState ? state.ev100 : null,
|
ev100: state is MeteringDataState ? state.ev100 : null,
|
||||||
isMetering: state.isMetering,
|
isMetering: state.isMetering,
|
||||||
|
@ -46,12 +56,7 @@ class MeteringScreen extends StatelessWidget {
|
||||||
? UserPreferencesProvider.of(context).toggleEvSourceType
|
? UserPreferencesProvider.of(context).toggleEvSourceType
|
||||||
: null,
|
: null,
|
||||||
onMeasure: () => context.read<MeteringBloc>().add(const MeasureEvent()),
|
onMeasure: () => context.read<MeteringBloc>().add(const MeasureEvent()),
|
||||||
onSettings: () {
|
onSettings: () => pushNamed(context, 'settings'),
|
||||||
context.read<MeteringBloc>().add(const SettingsOpenedEvent());
|
|
||||||
Navigator.pushNamed(context, 'settings').then((value) {
|
|
||||||
context.read<MeteringBloc>().add(const SettingsClosedEvent());
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -59,6 +64,13 @@ class MeteringScreen extends StatelessWidget {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void pushNamed(BuildContext context, String routeName, {Object? arguments}) {
|
||||||
|
context.read<MeteringBloc>().add(const ScreenOnTopOpenedEvent());
|
||||||
|
Navigator.pushNamed(context, routeName, arguments: arguments).then((_) {
|
||||||
|
context.read<MeteringBloc>().add(const ScreenOnTopClosedEvent());
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _InheritedListeners extends StatelessWidget {
|
class _InheritedListeners extends StatelessWidget {
|
||||||
|
@ -83,6 +95,7 @@ class MeteringContainerBuidler extends StatelessWidget {
|
||||||
final NdValue nd;
|
final NdValue nd;
|
||||||
final ValueChanged<IsoValue> onIsoChanged;
|
final ValueChanged<IsoValue> onIsoChanged;
|
||||||
final ValueChanged<NdValue> onNdChanged;
|
final ValueChanged<NdValue> onNdChanged;
|
||||||
|
final ValueChanged<ExposurePair> onExposurePairTap;
|
||||||
|
|
||||||
const MeteringContainerBuidler({
|
const MeteringContainerBuidler({
|
||||||
required this.ev,
|
required this.ev,
|
||||||
|
@ -90,6 +103,7 @@ class MeteringContainerBuidler extends StatelessWidget {
|
||||||
required this.nd,
|
required this.nd,
|
||||||
required this.onIsoChanged,
|
required this.onIsoChanged,
|
||||||
required this.onNdChanged,
|
required this.onNdChanged,
|
||||||
|
required this.onExposurePairTap,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -113,6 +127,7 @@ class MeteringContainerBuidler extends StatelessWidget {
|
||||||
onIsoChanged: onIsoChanged,
|
onIsoChanged: onIsoChanged,
|
||||||
onNdChanged: onNdChanged,
|
onNdChanged: onNdChanged,
|
||||||
exposurePairs: exposurePairs,
|
exposurePairs: exposurePairs,
|
||||||
|
onExposurePairTap: onExposurePairTap,
|
||||||
)
|
)
|
||||||
: LightSensorContainerProvider(
|
: LightSensorContainerProvider(
|
||||||
fastest: fastest,
|
fastest: fastest,
|
||||||
|
@ -122,6 +137,7 @@ class MeteringContainerBuidler extends StatelessWidget {
|
||||||
onIsoChanged: onIsoChanged,
|
onIsoChanged: onIsoChanged,
|
||||||
onNdChanged: onNdChanged,
|
onNdChanged: onNdChanged,
|
||||||
exposurePairs: exposurePairs,
|
exposurePairs: exposurePairs,
|
||||||
|
onExposurePairTap: onExposurePairTap,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:lightmeter/interactors/settings_interactor.dart';
|
||||||
|
|
||||||
|
class TimerListTileBloc extends Cubit<bool> {
|
||||||
|
final SettingsInteractor _settingsInteractor;
|
||||||
|
|
||||||
|
TimerListTileBloc(
|
||||||
|
this._settingsInteractor,
|
||||||
|
) : super(_settingsInteractor.isAutostartTimerEnabled);
|
||||||
|
|
||||||
|
void onChanged(bool value) {
|
||||||
|
_settingsInteractor.enableAutostartTimer(value);
|
||||||
|
emit(value);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
import 'package:lightmeter/screens/settings/components/general/components/timer/bloc_list_tile_timer.dart';
|
||||||
|
import 'package:lightmeter/screens/settings/components/general/components/timer/widget_list_tile_timer.dart';
|
||||||
|
import 'package:lightmeter/screens/settings/flow_settings.dart';
|
||||||
|
|
||||||
|
class TimerListTileProvider extends StatelessWidget {
|
||||||
|
const TimerListTileProvider({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider(
|
||||||
|
create: (context) => TimerListTileBloc(SettingsInteractorProvider.of(context)),
|
||||||
|
child: const TimerListTile(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
|
import 'package:lightmeter/res/dimens.dart';
|
||||||
|
import 'package:lightmeter/screens/settings/components/general/components/timer/bloc_list_tile_timer.dart';
|
||||||
|
import 'package:lightmeter/screens/settings/components/shared/disable/widget_disable.dart';
|
||||||
|
import 'package:lightmeter/utils/context_utils.dart';
|
||||||
|
|
||||||
|
class TimerListTile extends StatelessWidget {
|
||||||
|
const TimerListTile({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Disable(
|
||||||
|
disable: !context.isPro,
|
||||||
|
child: BlocBuilder<TimerListTileBloc, bool>(
|
||||||
|
builder: (context, state) => SwitchListTile(
|
||||||
|
secondary: const Icon(Icons.timer_outlined),
|
||||||
|
title: Text(S.of(context).autostartTimer),
|
||||||
|
value: state && context.isPro,
|
||||||
|
onChanged: context.read<TimerListTileBloc>().onChanged,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import 'package:lightmeter/generated/l10n.dart';
|
||||||
import 'package:lightmeter/screens/settings/components/general/components/caffeine/provider_list_tile_caffeine.dart';
|
import 'package:lightmeter/screens/settings/components/general/components/caffeine/provider_list_tile_caffeine.dart';
|
||||||
import 'package:lightmeter/screens/settings/components/general/components/haptics/provider_list_tile_haptics.dart';
|
import 'package:lightmeter/screens/settings/components/general/components/haptics/provider_list_tile_haptics.dart';
|
||||||
import 'package:lightmeter/screens/settings/components/general/components/language/widget_list_tile_language.dart';
|
import 'package:lightmeter/screens/settings/components/general/components/language/widget_list_tile_language.dart';
|
||||||
|
import 'package:lightmeter/screens/settings/components/general/components/timer/provider_list_tile_timer.dart';
|
||||||
import 'package:lightmeter/screens/settings/components/general/components/volume_actions/provider_list_tile_volume_actions.dart';
|
import 'package:lightmeter/screens/settings/components/general/components/volume_actions/provider_list_tile_volume_actions.dart';
|
||||||
import 'package:lightmeter/screens/settings/components/shared/settings_section/widget_settings_section.dart';
|
import 'package:lightmeter/screens/settings/components/shared/settings_section/widget_settings_section.dart';
|
||||||
|
|
||||||
|
@ -18,6 +19,7 @@ class GeneralSettingsSection extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
const CaffeineListTileProvider(),
|
const CaffeineListTileProvider(),
|
||||||
const HapticsListTileProvider(),
|
const HapticsListTileProvider(),
|
||||||
|
const TimerListTileProvider(),
|
||||||
if (Platform.isAndroid) const VolumeActionsListTileProvider(),
|
if (Platform.isAndroid) const VolumeActionsListTileProvider(),
|
||||||
const LanguageListTile(),
|
const LanguageListTile(),
|
||||||
],
|
],
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:lightmeter/res/dimens.dart';
|
||||||
|
import 'package:lightmeter/screens/shared/filled_circle/widget_circle_filled.dart';
|
||||||
|
|
||||||
|
class AnimatedCircluarButton extends StatefulWidget {
|
||||||
|
final double? progress;
|
||||||
|
final bool isPressed;
|
||||||
|
final VoidCallback onPressed;
|
||||||
|
final Widget? child;
|
||||||
|
|
||||||
|
const AnimatedCircluarButton({
|
||||||
|
this.progress = 1.0,
|
||||||
|
required this.isPressed,
|
||||||
|
required this.onPressed,
|
||||||
|
this.child,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AnimatedCircluarButton> createState() => _AnimatedCircluarButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AnimatedCircluarButtonState extends State<AnimatedCircluarButton> {
|
||||||
|
bool _isPressed = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant AnimatedCircluarButton oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (oldWidget.isPressed != widget.isPressed) {
|
||||||
|
_isPressed = widget.isPressed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: widget.onPressed,
|
||||||
|
onTapDown: (_) {
|
||||||
|
setState(() {
|
||||||
|
_isPressed = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onTapUp: (_) {
|
||||||
|
setState(() {
|
||||||
|
_isPressed = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onTapCancel: () {
|
||||||
|
setState(() {
|
||||||
|
_isPressed = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: AnimatedScale(
|
||||||
|
duration: Dimens.durationS,
|
||||||
|
scale: _isPressed ? 0.9 : 1.0,
|
||||||
|
child: FilledCircle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
size: Dimens.grid72 - Dimens.grid8,
|
||||||
|
child: Center(
|
||||||
|
child: widget.child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned.fill(
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
/// This key is needed to make indicator start from the same point every time
|
||||||
|
key: ValueKey(widget.progress),
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
value: widget.progress,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:lightmeter/res/dimens.dart';
|
||||||
|
|
||||||
|
class BottomControlsBar extends StatelessWidget {
|
||||||
|
final Widget center;
|
||||||
|
final Widget? left;
|
||||||
|
final Widget? right;
|
||||||
|
|
||||||
|
const BottomControlsBar({
|
||||||
|
required this.center,
|
||||||
|
this.left,
|
||||||
|
this.right,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final scheme = Theme.of(context).colorScheme;
|
||||||
|
return IconButtonTheme(
|
||||||
|
data: IconButtonThemeData(
|
||||||
|
style: ButtonStyle(
|
||||||
|
backgroundColor: MaterialStatePropertyAll(scheme.surface),
|
||||||
|
elevation: const MaterialStatePropertyAll(4),
|
||||||
|
iconColor: MaterialStatePropertyAll(scheme.onSurface),
|
||||||
|
shadowColor: const MaterialStatePropertyAll(Colors.transparent),
|
||||||
|
surfaceTintColor: MaterialStatePropertyAll(scheme.surfaceTint),
|
||||||
|
fixedSize: const MaterialStatePropertyAll(Size(Dimens.grid48, Dimens.grid48)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(Dimens.borderRadiusL),
|
||||||
|
topRight: Radius.circular(Dimens.borderRadiusL),
|
||||||
|
),
|
||||||
|
child: ColoredBox(
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
child: SafeArea(
|
||||||
|
top: false,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: Dimens.paddingL),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
if (left != null) Expanded(child: Center(child: left)) else const Spacer(),
|
||||||
|
SizedBox.fromSize(
|
||||||
|
size: const Size.square(Dimens.grid72),
|
||||||
|
child: center,
|
||||||
|
),
|
||||||
|
if (right != null) Expanded(child: Center(child: right)) else const Spacer(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
15
lib/screens/shared/close_button/widget_button_close.dart
Normal file
15
lib/screens/shared/close_button/widget_button_close.dart
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
|
|
||||||
|
class CloseButton extends StatelessWidget {
|
||||||
|
const CloseButton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: Navigator.of(context).pop,
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
tooltip: S.of(context).tooltipClose,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
41
lib/screens/timer/bloc_timer.dart
Normal file
41
lib/screens/timer/bloc_timer.dart
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:lightmeter/interactors/timer_interactor.dart';
|
||||||
|
import 'package:lightmeter/screens/timer/event_timer.dart';
|
||||||
|
import 'package:lightmeter/screens/timer/state_timer.dart';
|
||||||
|
|
||||||
|
class TimerBloc extends Bloc<TimerEvent, TimerState> {
|
||||||
|
final TimerInteractor _timerInteractor;
|
||||||
|
final Duration duration;
|
||||||
|
|
||||||
|
TimerBloc(this._timerInteractor, this.duration) : super(const TimerStoppedState()) {
|
||||||
|
on<StartTimerEvent>(_onStartTimer);
|
||||||
|
on<TimerEndedEvent>(_onTimerEnded);
|
||||||
|
on<StopTimerEvent>(_onStopTimer);
|
||||||
|
on<ResetTimerEvent>(_onResetTimer);
|
||||||
|
|
||||||
|
if (_timerInteractor.isAutostartTimerEnabled) add(const StartTimerEvent());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onStartTimer(StartTimerEvent _, Emitter emit) async {
|
||||||
|
_timerInteractor.startVibration();
|
||||||
|
emit(const TimerResumedState());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onTimerEnded(TimerEndedEvent event, Emitter emit) async {
|
||||||
|
if (state is! TimerResetState) {
|
||||||
|
_timerInteractor.endVibration();
|
||||||
|
emit(const TimerStoppedState());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onStopTimer(StopTimerEvent _, Emitter emit) async {
|
||||||
|
_timerInteractor.startVibration();
|
||||||
|
emit(const TimerStoppedState());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onResetTimer(ResetTimerEvent _, Emitter emit) async {
|
||||||
|
emit(const TimerResetState());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:lightmeter/data/models/exposure_pair.dart';
|
||||||
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
|
import 'package:lightmeter/res/dimens.dart';
|
||||||
|
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/reading_value_container/widget_container_reading_value.dart';
|
||||||
|
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||||
|
|
||||||
|
class TimerMeteringConfig extends StatelessWidget {
|
||||||
|
final ExposurePair exposurePair;
|
||||||
|
final IsoValue isoValue;
|
||||||
|
final NdValue ndValue;
|
||||||
|
|
||||||
|
const TimerMeteringConfig({
|
||||||
|
required this.exposurePair,
|
||||||
|
required this.isoValue,
|
||||||
|
required this.ndValue,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
bottomLeft: Radius.circular(Dimens.borderRadiusL),
|
||||||
|
bottomRight: Radius.circular(Dimens.borderRadiusL),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.all(Dimens.paddingM),
|
||||||
|
child: SafeArea(
|
||||||
|
bottom: false,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: ReadingValueContainer.singleValue(
|
||||||
|
value: ReadingValue(
|
||||||
|
label: S.of(context).exposurePair,
|
||||||
|
value: exposurePair.toString(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: Dimens.grid8),
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: ReadingValueContainer.singleValue(
|
||||||
|
value: ReadingValue(
|
||||||
|
label: S.of(context).iso,
|
||||||
|
value: isoValue.toString(),
|
||||||
|
),
|
||||||
|
locked: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: Dimens.grid8),
|
||||||
|
Expanded(
|
||||||
|
child: ReadingValueContainer.singleValue(
|
||||||
|
value: ReadingValue(
|
||||||
|
label: S.of(context).nd,
|
||||||
|
value: ndValue.value == 0 ? S.of(context).none : ndValue.value.toString(),
|
||||||
|
),
|
||||||
|
locked: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
56
lib/screens/timer/components/text/widget_text_timer.dart
Normal file
56
lib/screens/timer/components/text/widget_text_timer.dart
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class TimerText extends StatelessWidget {
|
||||||
|
final Duration timeLeft;
|
||||||
|
final Duration duration;
|
||||||
|
|
||||||
|
const TimerText({
|
||||||
|
required this.timeLeft,
|
||||||
|
required this.duration,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
parseSeconds(),
|
||||||
|
style: Theme.of(context).textTheme.displayMedium,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
addZeroIfNeeded(timeLeft.inMilliseconds % 1000, 3),
|
||||||
|
style: Theme.of(context).textTheme.displaySmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String parseSeconds() {
|
||||||
|
final buffer = StringBuffer();
|
||||||
|
int remainingMs = timeLeft.inMilliseconds;
|
||||||
|
// longer than 1 hours
|
||||||
|
if (duration.inMilliseconds >= Duration.millisecondsPerHour) {
|
||||||
|
final hours = remainingMs ~/ Duration.millisecondsPerHour;
|
||||||
|
buffer.writeAll([addZeroIfNeeded(hours), ':']);
|
||||||
|
remainingMs -= hours * Duration.millisecondsPerHour;
|
||||||
|
}
|
||||||
|
// longer than 1 minute
|
||||||
|
final minutes = remainingMs ~/ Duration.millisecondsPerMinute;
|
||||||
|
buffer.writeAll([addZeroIfNeeded(minutes), ':']);
|
||||||
|
remainingMs -= minutes * Duration.millisecondsPerMinute;
|
||||||
|
|
||||||
|
// longer than 1 second
|
||||||
|
final seconds = remainingMs ~/ Duration.millisecondsPerSecond;
|
||||||
|
buffer.writeAll([addZeroIfNeeded(seconds)]);
|
||||||
|
remainingMs -= seconds * Duration.millisecondsPerSecond;
|
||||||
|
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
String addZeroIfNeeded(int value, [int charactersCount = 2]) {
|
||||||
|
final zerosCount = charactersCount - value.toString().length;
|
||||||
|
return '${"0" * zerosCount}$value';
|
||||||
|
}
|
||||||
|
}
|
142
lib/screens/timer/components/timeline/widget_timeline_timer.dart
Normal file
142
lib/screens/timer/components/timeline/widget_timeline_timer.dart
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:lightmeter/res/dimens.dart';
|
||||||
|
|
||||||
|
class TimerTimeline extends StatelessWidget {
|
||||||
|
final double progress;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const TimerTimeline({
|
||||||
|
required this.progress,
|
||||||
|
required this.child,
|
||||||
|
}) : assert(progress >= 0 && progress <= 1);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return CustomPaint(
|
||||||
|
painter: _TimelinePainter(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
|
progressColor: Theme.of(context).colorScheme.primary,
|
||||||
|
progress: progress,
|
||||||
|
),
|
||||||
|
willChange: true,
|
||||||
|
child: Center(child: child),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TimelinePainter extends CustomPainter {
|
||||||
|
final Color progressColor;
|
||||||
|
final Color backgroundColor;
|
||||||
|
final double progress;
|
||||||
|
|
||||||
|
late final double timelineEdgeRadius = strokeWidth / 2;
|
||||||
|
static const double radiansQuarterTurn = -pi / 2;
|
||||||
|
static const double strokeWidth = Dimens.grid8;
|
||||||
|
|
||||||
|
_TimelinePainter({
|
||||||
|
required this.progressColor,
|
||||||
|
required this.backgroundColor,
|
||||||
|
required this.progress,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
late final double radiansProgress = 2 * pi * progress;
|
||||||
|
final radius = size.height / 2;
|
||||||
|
final timerCenter = Offset(radius, radius);
|
||||||
|
|
||||||
|
final timelineSegmentPath = Path();
|
||||||
|
if (progress == 1) {
|
||||||
|
timelineSegmentPath.addOval(
|
||||||
|
Rect.fromCenter(
|
||||||
|
center: timerCenter,
|
||||||
|
height: size.height,
|
||||||
|
width: size.width,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
timelineSegmentPath
|
||||||
|
..arcTo(
|
||||||
|
Rect.fromCenter(
|
||||||
|
center: timerCenter,
|
||||||
|
height: size.height,
|
||||||
|
width: size.width,
|
||||||
|
),
|
||||||
|
radiansQuarterTurn,
|
||||||
|
radiansProgress,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
..lineTo(radius, radius)
|
||||||
|
..lineTo(radius, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
final timelinePath = Path.combine(
|
||||||
|
PathOperation.difference,
|
||||||
|
timelineSegmentPath,
|
||||||
|
Path()
|
||||||
|
..addOval(
|
||||||
|
Rect.fromCircle(
|
||||||
|
center: timerCenter,
|
||||||
|
radius: radius - strokeWidth,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final smoothEdgesPath = Path.combine(
|
||||||
|
PathOperation.union,
|
||||||
|
Path()
|
||||||
|
..addOval(
|
||||||
|
Rect.fromCircle(
|
||||||
|
center: Offset(radius, timelineEdgeRadius),
|
||||||
|
radius: timelineEdgeRadius,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Path()
|
||||||
|
..addOval(
|
||||||
|
Rect.fromCircle(
|
||||||
|
center: Offset(
|
||||||
|
(radius - timelineEdgeRadius) * cos(radiansProgress + radiansQuarterTurn) + radius,
|
||||||
|
(radius - timelineEdgeRadius) * sin(radiansProgress + radiansQuarterTurn) + radius,
|
||||||
|
),
|
||||||
|
radius: timelineEdgeRadius,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
canvas.drawPath(
|
||||||
|
Path.combine(
|
||||||
|
PathOperation.difference,
|
||||||
|
Path()
|
||||||
|
..addOval(
|
||||||
|
Rect.fromCircle(
|
||||||
|
center: timerCenter,
|
||||||
|
radius: radius,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Path()
|
||||||
|
..addOval(
|
||||||
|
Rect.fromCircle(
|
||||||
|
center: timerCenter,
|
||||||
|
radius: radius - strokeWidth,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Paint()..color = backgroundColor,
|
||||||
|
);
|
||||||
|
|
||||||
|
canvas.drawPath(
|
||||||
|
timelinePath,
|
||||||
|
Paint()..color = progressColor,
|
||||||
|
);
|
||||||
|
|
||||||
|
canvas.drawPath(
|
||||||
|
smoothEdgesPath,
|
||||||
|
Paint()..color = progressColor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(_TimelinePainter oldDelegate) => oldDelegate.progress != progress;
|
||||||
|
}
|
19
lib/screens/timer/event_timer.dart
Normal file
19
lib/screens/timer/event_timer.dart
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
sealed class TimerEvent {
|
||||||
|
const TimerEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class StartTimerEvent extends TimerEvent {
|
||||||
|
const StartTimerEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class StopTimerEvent extends TimerEvent {
|
||||||
|
const StopTimerEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class TimerEndedEvent extends TimerEvent {
|
||||||
|
const TimerEndedEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ResetTimerEvent extends TimerEvent {
|
||||||
|
const ResetTimerEvent();
|
||||||
|
}
|
67
lib/screens/timer/flow_timer.dart
Normal file
67
lib/screens/timer/flow_timer.dart
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:lightmeter/data/models/exposure_pair.dart';
|
||||||
|
import 'package:lightmeter/interactors/timer_interactor.dart';
|
||||||
|
import 'package:lightmeter/providers/services_provider.dart';
|
||||||
|
import 'package:lightmeter/screens/timer/bloc_timer.dart';
|
||||||
|
import 'package:lightmeter/screens/timer/screen_timer.dart';
|
||||||
|
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||||
|
|
||||||
|
class TimerFlowArgs {
|
||||||
|
final ExposurePair exposurePair;
|
||||||
|
final IsoValue isoValue;
|
||||||
|
final NdValue ndValue;
|
||||||
|
|
||||||
|
const TimerFlowArgs({
|
||||||
|
required this.exposurePair,
|
||||||
|
required this.isoValue,
|
||||||
|
required this.ndValue,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class TimerFlow extends StatelessWidget {
|
||||||
|
final TimerFlowArgs args;
|
||||||
|
late final _duration =
|
||||||
|
Duration(milliseconds: (args.exposurePair.shutterSpeed.value * Duration.millisecondsPerSecond).toInt());
|
||||||
|
|
||||||
|
TimerFlow({required this.args, super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return TimerInteractorProvider(
|
||||||
|
data: TimerInteractor(
|
||||||
|
ServicesProvider.of(context).userPreferencesService,
|
||||||
|
ServicesProvider.of(context).hapticsService,
|
||||||
|
),
|
||||||
|
child: BlocProvider(
|
||||||
|
create: (context) => TimerBloc(
|
||||||
|
TimerInteractorProvider.of(context),
|
||||||
|
_duration,
|
||||||
|
),
|
||||||
|
child: TimerScreen(
|
||||||
|
exposurePair: args.exposurePair,
|
||||||
|
isoValue: args.isoValue,
|
||||||
|
ndValue: args.ndValue,
|
||||||
|
duration: _duration,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TimerInteractorProvider extends InheritedWidget {
|
||||||
|
final TimerInteractor data;
|
||||||
|
|
||||||
|
const TimerInteractorProvider({
|
||||||
|
required this.data,
|
||||||
|
required super.child,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
static TimerInteractor of(BuildContext context) {
|
||||||
|
return context.findAncestorWidgetOfExactType<TimerInteractorProvider>()!.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool updateShouldNotify(TimerInteractorProvider oldWidget) => false;
|
||||||
|
}
|
144
lib/screens/timer/screen_timer.dart
Normal file
144
lib/screens/timer/screen_timer.dart
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:lightmeter/data/models/exposure_pair.dart';
|
||||||
|
import 'package:lightmeter/res/dimens.dart';
|
||||||
|
import 'package:lightmeter/screens/shared/animated_circular_button/widget_button_circular_animated.dart';
|
||||||
|
import 'package:lightmeter/screens/shared/bottom_controls_bar/widget_bottom_controls_bar.dart';
|
||||||
|
import 'package:lightmeter/screens/timer/bloc_timer.dart';
|
||||||
|
import 'package:lightmeter/screens/timer/components/metering_config/widget_metering_config_timer.dart';
|
||||||
|
import 'package:lightmeter/screens/timer/components/text/widget_text_timer.dart';
|
||||||
|
import 'package:lightmeter/screens/timer/components/timeline/widget_timeline_timer.dart';
|
||||||
|
import 'package:lightmeter/screens/timer/event_timer.dart';
|
||||||
|
import 'package:lightmeter/screens/timer/state_timer.dart';
|
||||||
|
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||||
|
|
||||||
|
class TimerScreen extends StatefulWidget {
|
||||||
|
final ExposurePair exposurePair;
|
||||||
|
final IsoValue isoValue;
|
||||||
|
final NdValue ndValue;
|
||||||
|
final Duration duration;
|
||||||
|
|
||||||
|
const TimerScreen({
|
||||||
|
required this.exposurePair,
|
||||||
|
required this.isoValue,
|
||||||
|
required this.ndValue,
|
||||||
|
required this.duration,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<TimerScreen> createState() => TimerScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
class TimerScreenState extends State<TimerScreen> with TickerProviderStateMixin {
|
||||||
|
late AnimationController timelineController;
|
||||||
|
late Animation<double> timelineAnimation;
|
||||||
|
late AnimationController startStopIconController;
|
||||||
|
late Animation<double> startStopIconAnimation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
timelineController = AnimationController(vsync: this, duration: widget.duration);
|
||||||
|
timelineAnimation = Tween<double>(begin: 1, end: 0).animate(timelineController);
|
||||||
|
timelineController.addStatusListener((status) {
|
||||||
|
if (status == AnimationStatus.completed) {
|
||||||
|
context.read<TimerBloc>().add(const TimerEndedEvent());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
startStopIconController = AnimationController(vsync: this, duration: Dimens.durationS);
|
||||||
|
startStopIconAnimation = Tween<double>(begin: 0, end: 1).animate(startStopIconController);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
timelineController.dispose();
|
||||||
|
startStopIconController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocListener<TimerBloc, TimerState>(
|
||||||
|
listenWhen: (previous, current) => previous.runtimeType != current.runtimeType,
|
||||||
|
listener: (context, state) => _updateAnimations(state),
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.background,
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
TimerMeteringConfig(
|
||||||
|
exposurePair: widget.exposurePair,
|
||||||
|
isoValue: widget.isoValue,
|
||||||
|
ndValue: widget.ndValue,
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(Dimens.paddingL),
|
||||||
|
child: SizedBox.fromSize(
|
||||||
|
size: Size.square(MediaQuery.sizeOf(context).width - Dimens.paddingL * 4),
|
||||||
|
child: ValueListenableBuilder(
|
||||||
|
valueListenable: timelineAnimation,
|
||||||
|
builder: (_, value, child) => TimerTimeline(
|
||||||
|
progress: value,
|
||||||
|
child: TimerText(
|
||||||
|
timeLeft: Duration(milliseconds: (widget.duration.inMilliseconds * value).toInt()),
|
||||||
|
duration: widget.duration,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
BottomControlsBar(
|
||||||
|
left: IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
context.read<TimerBloc>().add(const ResetTimerEvent());
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.restore),
|
||||||
|
),
|
||||||
|
center: BlocBuilder<TimerBloc, TimerState>(
|
||||||
|
builder: (_, state) => AnimatedCircluarButton(
|
||||||
|
isPressed: state is TimerResumedState,
|
||||||
|
onPressed: () {
|
||||||
|
if (timelineAnimation.value == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final event = state is TimerStoppedState ? const StartTimerEvent() : const StopTimerEvent();
|
||||||
|
context.read<TimerBloc>().add(event);
|
||||||
|
},
|
||||||
|
child: AnimatedIcon(
|
||||||
|
icon: AnimatedIcons.play_pause,
|
||||||
|
progress: startStopIconAnimation,
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
right: const CloseButton(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateAnimations(TimerState state) {
|
||||||
|
switch (state) {
|
||||||
|
case TimerResetState():
|
||||||
|
startStopIconController.reverse();
|
||||||
|
timelineController.stop();
|
||||||
|
timelineController.animateTo(0, duration: Dimens.durationS);
|
||||||
|
case TimerResumedState():
|
||||||
|
startStopIconController.forward();
|
||||||
|
timelineController.forward();
|
||||||
|
case TimerStoppedState():
|
||||||
|
startStopIconController.reverse();
|
||||||
|
timelineController.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
lib/screens/timer/state_timer.dart
Normal file
18
lib/screens/timer/state_timer.dart
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
sealed class TimerState {
|
||||||
|
const TimerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class TimerStoppedState extends TimerState {
|
||||||
|
const TimerStoppedState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class TimerResumedState extends TimerState {
|
||||||
|
const TimerResumedState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class TimerResetState extends TimerStoppedState {
|
||||||
|
const TimerResetState();
|
||||||
|
}
|
|
@ -326,6 +326,26 @@ void main() {
|
||||||
verify(() => sharedPreferences.setBool(UserPreferencesService.hapticsKey, false)).called(1);
|
verify(() => sharedPreferences.setBool(UserPreferencesService.hapticsKey, false)).called(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('autostartTimer', () {
|
||||||
|
test('get default', () {
|
||||||
|
when(() => sharedPreferences.getBool(UserPreferencesService.autostartTimerKey)).thenReturn(null);
|
||||||
|
expect(service.autostartTimer, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('get', () {
|
||||||
|
when(() => sharedPreferences.getBool(UserPreferencesService.autostartTimerKey)).thenReturn(true);
|
||||||
|
expect(service.autostartTimer, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('set', () {
|
||||||
|
when(() => sharedPreferences.setBool(UserPreferencesService.autostartTimerKey, false))
|
||||||
|
.thenAnswer((_) => Future.value(true));
|
||||||
|
service.autostartTimer = false;
|
||||||
|
verify(() => sharedPreferences.setBool(UserPreferencesService.autostartTimerKey, false)).called(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
group('volumeAction', () {
|
group('volumeAction', () {
|
||||||
test('get default', () {
|
test('get default', () {
|
||||||
when(() => sharedPreferences.getBool(UserPreferencesService.volumeActionKey)).thenReturn(null);
|
when(() => sharedPreferences.getBool(UserPreferencesService.volumeActionKey)).thenReturn(null);
|
||||||
|
|
|
@ -85,6 +85,28 @@ void main() {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
group(
|
||||||
|
'AutostartTimer',
|
||||||
|
() {
|
||||||
|
test('isAutostartTimerEnabled', () {
|
||||||
|
when(() => mockUserPreferencesService.autostartTimer).thenReturn(true);
|
||||||
|
expect(interactor.isAutostartTimerEnabled, true);
|
||||||
|
when(() => mockUserPreferencesService.autostartTimer).thenReturn(false);
|
||||||
|
expect(interactor.isAutostartTimerEnabled, false);
|
||||||
|
verify(() => mockUserPreferencesService.autostartTimer).called(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('enableAutostartTimer(true)', () {
|
||||||
|
when(() => mockUserPreferencesService.autostartTimer = true).thenReturn(true);
|
||||||
|
interactor.enableAutostartTimer(true);
|
||||||
|
verify(() => mockUserPreferencesService.autostartTimer = true).called(1);
|
||||||
|
when(() => mockUserPreferencesService.autostartTimer = true).thenReturn(false);
|
||||||
|
interactor.enableAutostartTimer(false);
|
||||||
|
verify(() => mockUserPreferencesService.autostartTimer = false).called(1);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
group(
|
group(
|
||||||
'Volume action',
|
'Volume action',
|
||||||
() {
|
() {
|
||||||
|
@ -123,8 +145,7 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('setVolumeAction(VolumeAction.shutter)', () async {
|
test('setVolumeAction(VolumeAction.shutter)', () async {
|
||||||
when(() => mockUserPreferencesService.volumeAction = VolumeAction.shutter)
|
when(() => mockUserPreferencesService.volumeAction = VolumeAction.shutter).thenReturn(VolumeAction.shutter);
|
||||||
.thenReturn(VolumeAction.shutter);
|
|
||||||
when(() => mockVolumeEventsService.setVolumeHandling(true)).thenAnswer((_) async => true);
|
when(() => mockVolumeEventsService.setVolumeHandling(true)).thenAnswer((_) async => true);
|
||||||
expectLater(interactor.setVolumeAction(VolumeAction.shutter), isA<Future<void>>());
|
expectLater(interactor.setVolumeAction(VolumeAction.shutter), isA<Future<void>>());
|
||||||
verify(() => mockVolumeEventsService.setVolumeHandling(true)).called(1);
|
verify(() => mockVolumeEventsService.setVolumeHandling(true)).called(1);
|
||||||
|
@ -132,8 +153,7 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('setVolumeAction(VolumeAction.none)', () async {
|
test('setVolumeAction(VolumeAction.none)', () async {
|
||||||
when(() => mockUserPreferencesService.volumeAction = VolumeAction.none)
|
when(() => mockUserPreferencesService.volumeAction = VolumeAction.none).thenReturn(VolumeAction.none);
|
||||||
.thenReturn(VolumeAction.none);
|
|
||||||
when(() => mockVolumeEventsService.setVolumeHandling(false)).thenAnswer((_) async => false);
|
when(() => mockVolumeEventsService.setVolumeHandling(false)).thenAnswer((_) async => false);
|
||||||
expectLater(interactor.setVolumeAction(VolumeAction.none), isA<Future<void>>());
|
expectLater(interactor.setVolumeAction(VolumeAction.none), isA<Future<void>>());
|
||||||
verify(() => mockVolumeEventsService.setVolumeHandling(false)).called(1);
|
verify(() => mockVolumeEventsService.setVolumeHandling(false)).called(1);
|
||||||
|
|
74
test/interactors/timer_interactor_test.dart
Normal file
74
test/interactors/timer_interactor_test.dart
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:lightmeter/data/haptics_service.dart';
|
||||||
|
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||||
|
import 'package:lightmeter/interactors/timer_interactor.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
|
class _MockUserPreferencesService extends Mock implements UserPreferencesService {}
|
||||||
|
|
||||||
|
class _MockHapticsService extends Mock implements HapticsService {}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late _MockUserPreferencesService mockUserPreferencesService;
|
||||||
|
late _MockHapticsService mockHapticsService;
|
||||||
|
|
||||||
|
late TimerInteractor interactor;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
mockUserPreferencesService = _MockUserPreferencesService();
|
||||||
|
mockHapticsService = _MockHapticsService();
|
||||||
|
|
||||||
|
interactor = TimerInteractor(
|
||||||
|
mockUserPreferencesService,
|
||||||
|
mockHapticsService,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
group(
|
||||||
|
'Haptics',
|
||||||
|
() {
|
||||||
|
test('startVibration() - true', () async {
|
||||||
|
when(() => mockUserPreferencesService.haptics).thenReturn(true);
|
||||||
|
when(() => mockHapticsService.quickVibration()).thenAnswer((_) async {});
|
||||||
|
interactor.startVibration();
|
||||||
|
verify(() => mockUserPreferencesService.haptics).called(1);
|
||||||
|
verify(() => mockHapticsService.quickVibration()).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('startVibration() - false', () async {
|
||||||
|
when(() => mockUserPreferencesService.haptics).thenReturn(false);
|
||||||
|
when(() => mockHapticsService.quickVibration()).thenAnswer((_) async {});
|
||||||
|
interactor.startVibration();
|
||||||
|
verify(() => mockUserPreferencesService.haptics).called(1);
|
||||||
|
verifyNever(() => mockHapticsService.quickVibration());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('endVibration() - true', () async {
|
||||||
|
when(() => mockUserPreferencesService.haptics).thenReturn(true);
|
||||||
|
when(() => mockHapticsService.errorVibration()).thenAnswer((_) async {});
|
||||||
|
interactor.endVibration();
|
||||||
|
verify(() => mockUserPreferencesService.haptics).called(1);
|
||||||
|
verify(() => mockHapticsService.errorVibration()).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('endVibration() - false', () async {
|
||||||
|
when(() => mockUserPreferencesService.haptics).thenReturn(false);
|
||||||
|
when(() => mockHapticsService.errorVibration()).thenAnswer((_) async {});
|
||||||
|
interactor.endVibration();
|
||||||
|
verify(() => mockUserPreferencesService.haptics).called(1);
|
||||||
|
verifyNever(() => mockHapticsService.errorVibration());
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
group(
|
||||||
|
'AutostartTimer',
|
||||||
|
() {
|
||||||
|
test('isAutostartTimerEnabled', () {
|
||||||
|
when(() => mockUserPreferencesService.autostartTimer).thenReturn(true);
|
||||||
|
expect(interactor.isAutostartTimerEnabled, true);
|
||||||
|
verify(() => mockUserPreferencesService.autostartTimer).called(1);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
|
@ -3,10 +3,8 @@ 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/bloc_metering.dart';
|
import 'package:lightmeter/screens/metering/bloc_metering.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' as communication_events;
|
||||||
as communication_events;
|
import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart' as communication_states;
|
||||||
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/event_metering.dart';
|
||||||
import 'package:lightmeter/screens/metering/state_metering.dart';
|
import 'package:lightmeter/screens/metering/state_metering.dart';
|
||||||
import 'package:lightmeter/screens/metering/utils/notifier_volume_keys.dart';
|
import 'package:lightmeter/screens/metering/utils/notifier_volume_keys.dart';
|
||||||
|
@ -18,9 +16,9 @@ class _MockMeteringInteractor extends Mock implements MeteringInteractor {}
|
||||||
|
|
||||||
class _MockVolumeKeysNotifier extends Mock implements VolumeKeysNotifier {}
|
class _MockVolumeKeysNotifier extends Mock implements VolumeKeysNotifier {}
|
||||||
|
|
||||||
class _MockMeteringCommunicationBloc extends MockBloc<
|
class _MockMeteringCommunicationBloc
|
||||||
communication_events.MeteringCommunicationEvent,
|
extends MockBloc<communication_events.MeteringCommunicationEvent, communication_states.MeteringCommunicationState>
|
||||||
communication_states.MeteringCommunicationState> implements MeteringCommunicationBloc {}
|
implements MeteringCommunicationBloc {}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
late _MockMeteringInteractor meteringInteractor;
|
late _MockMeteringInteractor meteringInteractor;
|
||||||
|
@ -515,20 +513,18 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
group(
|
group(
|
||||||
'`SettingOpenedEvent`/`SettingsClosedEvent`',
|
'`ScreenOnTopOpenedEvent`/`ScreenOnTopClosedEvent`',
|
||||||
() {
|
() {
|
||||||
blocTest<MeteringBloc, MeteringState>(
|
blocTest<MeteringBloc, MeteringState>(
|
||||||
'Settings opened & closed',
|
'Settings opened & closed',
|
||||||
build: () => bloc,
|
build: () => bloc,
|
||||||
act: (bloc) async {
|
act: (bloc) async {
|
||||||
bloc.add(const SettingsOpenedEvent());
|
bloc.add(const ScreenOnTopOpenedEvent());
|
||||||
bloc.add(const SettingsClosedEvent());
|
bloc.add(const ScreenOnTopClosedEvent());
|
||||||
},
|
},
|
||||||
verify: (_) {
|
verify: (_) {
|
||||||
verify(() => communicationBloc.add(const communication_events.SettingsOpenedEvent()))
|
verify(() => communicationBloc.add(const communication_events.ScreenOnTopOpenedEvent())).called(1);
|
||||||
.called(1);
|
verify(() => communicationBloc.add(const communication_events.ScreenOnTopClosedEvent())).called(1);
|
||||||
verify(() => communicationBloc.add(const communication_events.SettingsClosedEvent()))
|
|
||||||
.called(1);
|
|
||||||
},
|
},
|
||||||
expect: () => [],
|
expect: () => [],
|
||||||
);
|
);
|
||||||
|
|
|
@ -100,20 +100,20 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
group(
|
group(
|
||||||
'`SettingsOpenedEvent`/`SettingsClosedEvent`',
|
'`ScreenOnTopOpenedEvent`/`ScreenOnTopClosedEvent`',
|
||||||
() {
|
() {
|
||||||
blocTest<MeteringCommunicationBloc, MeteringCommunicationState>(
|
blocTest<MeteringCommunicationBloc, MeteringCommunicationState>(
|
||||||
'Multiple consequtive settings events',
|
'Multiple consequtive settings events',
|
||||||
build: () => bloc,
|
build: () => bloc,
|
||||||
act: (bloc) async {
|
act: (bloc) async {
|
||||||
bloc.add(const SettingsOpenedEvent());
|
bloc.add(const ScreenOnTopOpenedEvent());
|
||||||
bloc.add(const SettingsOpenedEvent());
|
bloc.add(const ScreenOnTopOpenedEvent());
|
||||||
bloc.add(const SettingsOpenedEvent());
|
bloc.add(const ScreenOnTopOpenedEvent());
|
||||||
bloc.add(const SettingsClosedEvent());
|
bloc.add(const ScreenOnTopClosedEvent());
|
||||||
bloc.add(const SettingsClosedEvent());
|
bloc.add(const ScreenOnTopClosedEvent());
|
||||||
bloc.add(const SettingsClosedEvent());
|
bloc.add(const ScreenOnTopClosedEvent());
|
||||||
bloc.add(const SettingsOpenedEvent());
|
bloc.add(const ScreenOnTopOpenedEvent());
|
||||||
bloc.add(const SettingsClosedEvent());
|
bloc.add(const ScreenOnTopClosedEvent());
|
||||||
},
|
},
|
||||||
expect: () => [
|
expect: () => [
|
||||||
isA<SettingsOpenedState>(),
|
isA<SettingsOpenedState>(),
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.5 MiB |
|
@ -8,11 +8,11 @@ import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
|
||||||
import 'package:lightmeter/data/models/theme_type.dart';
|
import 'package:lightmeter/data/models/theme_type.dart';
|
||||||
import 'package:lightmeter/data/shared_prefs_service.dart';
|
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||||
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
||||||
import 'package:lightmeter/screens/metering/components/bottom_controls/components/measure_button/widget_button_measure.dart';
|
|
||||||
import 'package:lightmeter/screens/metering/flow_metering.dart';
|
import 'package:lightmeter/screens/metering/flow_metering.dart';
|
||||||
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import '../../../integration_test/utils/finder_actions.dart';
|
||||||
import '../../../integration_test/utils/platform_channel_mock.dart';
|
import '../../../integration_test/utils/platform_channel_mock.dart';
|
||||||
import '../../application_mock.dart';
|
import '../../application_mock.dart';
|
||||||
|
|
||||||
|
@ -67,7 +67,7 @@ void main() {
|
||||||
Future<void> takePhoto(WidgetTester tester, Key scenarioWidgetKey) async {
|
Future<void> takePhoto(WidgetTester tester, Key scenarioWidgetKey) async {
|
||||||
final button = find.descendant(
|
final button = find.descendant(
|
||||||
of: find.byKey(scenarioWidgetKey),
|
of: find.byKey(scenarioWidgetKey),
|
||||||
matching: find.byType(MeteringMeasureButton),
|
matching: find.measureButton(),
|
||||||
);
|
);
|
||||||
await tester.tap(button);
|
await tester.tap(button);
|
||||||
await tester.pump(const Duration(seconds: 2)); // wait for circular progress indicator
|
await tester.pump(const Duration(seconds: 2)); // wait for circular progress indicator
|
||||||
|
@ -78,7 +78,7 @@ void main() {
|
||||||
Future<void> toggleIncidentMetering(WidgetTester tester, Key scenarioWidgetKey, double ev) async {
|
Future<void> toggleIncidentMetering(WidgetTester tester, Key scenarioWidgetKey, double ev) async {
|
||||||
final button = find.descendant(
|
final button = find.descendant(
|
||||||
of: find.byKey(scenarioWidgetKey),
|
of: find.byKey(scenarioWidgetKey),
|
||||||
matching: find.byType(MeteringMeasureButton),
|
matching: find.measureButton(),
|
||||||
);
|
);
|
||||||
await tester.tap(button);
|
await tester.tap(button);
|
||||||
await sendMockIncidentEv(ev);
|
await sendMockIncidentEv(ev);
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 488 KiB After Width: | Height: | Size: 496 KiB |
77
test/screens/timer/bloc_timer_test.dart
Normal file
77
test/screens/timer/bloc_timer_test.dart
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import 'package:bloc_test/bloc_test.dart';
|
||||||
|
import 'package:lightmeter/interactors/timer_interactor.dart';
|
||||||
|
import 'package:lightmeter/screens/timer/bloc_timer.dart';
|
||||||
|
import 'package:lightmeter/screens/timer/event_timer.dart';
|
||||||
|
import 'package:lightmeter/screens/timer/state_timer.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
class _MockTimerInteractor extends Mock implements TimerInteractor {}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late _MockTimerInteractor timerInteractor;
|
||||||
|
|
||||||
|
setUpAll(() {
|
||||||
|
timerInteractor = _MockTimerInteractor();
|
||||||
|
when(() => timerInteractor.isAutostartTimerEnabled).thenReturn(true);
|
||||||
|
when(timerInteractor.startVibration).thenAnswer((_) async {});
|
||||||
|
when(timerInteractor.endVibration).thenAnswer((_) async {});
|
||||||
|
});
|
||||||
|
|
||||||
|
blocTest<TimerBloc, TimerState>(
|
||||||
|
'Autostart',
|
||||||
|
build: () => TimerBloc(timerInteractor, const Duration(seconds: 1)),
|
||||||
|
verify: (_) {
|
||||||
|
verify(() => timerInteractor.startVibration()).called(1);
|
||||||
|
},
|
||||||
|
expect: () => [
|
||||||
|
isA<TimerResumedState>(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
blocTest<TimerBloc, TimerState>(
|
||||||
|
'Start -> wait till the end -> reset',
|
||||||
|
build: () => TimerBloc(timerInteractor, const Duration(seconds: 1)),
|
||||||
|
setUp: () {
|
||||||
|
when(() => timerInteractor.isAutostartTimerEnabled).thenReturn(false);
|
||||||
|
},
|
||||||
|
act: (bloc) {
|
||||||
|
bloc.add(const StartTimerEvent());
|
||||||
|
bloc.add(const TimerEndedEvent());
|
||||||
|
bloc.add(const ResetTimerEvent());
|
||||||
|
},
|
||||||
|
verify: (_) {
|
||||||
|
verify(() => timerInteractor.startVibration()).called(1);
|
||||||
|
verify(() => timerInteractor.endVibration()).called(1);
|
||||||
|
},
|
||||||
|
expect: () => [
|
||||||
|
isA<TimerResumedState>(),
|
||||||
|
isA<TimerStoppedState>(),
|
||||||
|
isA<TimerResetState>(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
blocTest<TimerBloc, TimerState>(
|
||||||
|
'Start -> stop -> start -> wait till the end',
|
||||||
|
build: () => TimerBloc(timerInteractor, const Duration(seconds: 1)),
|
||||||
|
setUp: () {
|
||||||
|
when(() => timerInteractor.isAutostartTimerEnabled).thenReturn(false);
|
||||||
|
},
|
||||||
|
act: (bloc) async {
|
||||||
|
bloc.add(const StartTimerEvent());
|
||||||
|
bloc.add(const StopTimerEvent());
|
||||||
|
bloc.add(const StartTimerEvent());
|
||||||
|
bloc.add(const TimerEndedEvent());
|
||||||
|
},
|
||||||
|
verify: (_) {
|
||||||
|
verify(() => timerInteractor.startVibration()).called(3);
|
||||||
|
verify(() => timerInteractor.endVibration()).called(1);
|
||||||
|
},
|
||||||
|
expect: () => [
|
||||||
|
isA<TimerResumedState>(),
|
||||||
|
isA<TimerStoppedState>(),
|
||||||
|
isA<TimerResumedState>(),
|
||||||
|
isA<TimerStoppedState>(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
BIN
test/screens/timer/goldens/timer_screen.png
Normal file
BIN
test/screens/timer/goldens/timer_screen.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 854 KiB |
143
test/screens/timer/screen_timer_golden_test.dart
Normal file
143
test/screens/timer/screen_timer_golden_test.dart
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:golden_toolkit/golden_toolkit.dart';
|
||||||
|
import 'package:lightmeter/data/models/exposure_pair.dart';
|
||||||
|
import 'package:lightmeter/data/models/theme_type.dart';
|
||||||
|
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||||
|
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
||||||
|
import 'package:lightmeter/res/dimens.dart';
|
||||||
|
import 'package:lightmeter/screens/shared/animated_circular_button/widget_button_circular_animated.dart';
|
||||||
|
import 'package:lightmeter/screens/timer/flow_timer.dart';
|
||||||
|
import 'package:lightmeter/screens/timer/screen_timer.dart';
|
||||||
|
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import '../../application_mock.dart';
|
||||||
|
|
||||||
|
class _TimerScreenConfig {
|
||||||
|
final ShutterSpeedValue shutterSpeedValue;
|
||||||
|
final bool isStopped;
|
||||||
|
|
||||||
|
_TimerScreenConfig({
|
||||||
|
required this.shutterSpeedValue,
|
||||||
|
required this.isStopped,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
final buffer = StringBuffer();
|
||||||
|
buffer.write(shutterSpeedValue.toString());
|
||||||
|
buffer.write(' - ');
|
||||||
|
buffer.write(isStopped ? 'stopped' : 'resumed');
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final _testScenarios = [
|
||||||
|
const ShutterSpeedValue(
|
||||||
|
74,
|
||||||
|
false,
|
||||||
|
StopType.full,
|
||||||
|
),
|
||||||
|
const ShutterSpeedValue(
|
||||||
|
3642,
|
||||||
|
false,
|
||||||
|
StopType.full,
|
||||||
|
),
|
||||||
|
].expand(
|
||||||
|
(shutterSpeedValue) => [
|
||||||
|
_TimerScreenConfig(shutterSpeedValue: shutterSpeedValue, isStopped: true),
|
||||||
|
_TimerScreenConfig(shutterSpeedValue: shutterSpeedValue, isStopped: false),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
Future<void> setTheme(WidgetTester tester, Key scenarioWidgetKey, ThemeType themeType) async {
|
||||||
|
final flow = find.descendant(
|
||||||
|
of: find.byKey(scenarioWidgetKey),
|
||||||
|
matching: find.byType(TimerFlow),
|
||||||
|
);
|
||||||
|
final BuildContext context = tester.element(flow);
|
||||||
|
UserPreferencesProvider.of(context).setThemeType(themeType);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> toggleTimer(WidgetTester tester, Key scenarioWidgetKey) async {
|
||||||
|
final button = find.descendant(
|
||||||
|
of: find.byKey(scenarioWidgetKey),
|
||||||
|
matching: find.byType(AnimatedCircluarButton),
|
||||||
|
);
|
||||||
|
await tester.tap(button);
|
||||||
|
await tester.pump(Dimens.durationS);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> mockResumedState(WidgetTester tester, Key scenarioWidgetKey) async {
|
||||||
|
final screen = find.descendant(
|
||||||
|
of: find.byKey(scenarioWidgetKey),
|
||||||
|
matching: find.byType(TimerScreen),
|
||||||
|
);
|
||||||
|
final TimerScreenState state = tester.state(screen);
|
||||||
|
state.startStopIconController.stop();
|
||||||
|
state.timelineController.stop();
|
||||||
|
await tester.pump();
|
||||||
|
}
|
||||||
|
|
||||||
|
setUpAll(() {
|
||||||
|
SharedPreferences.setMockInitialValues({
|
||||||
|
UserPreferencesService.autostartTimerKey: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
testGoldens(
|
||||||
|
'TimerScreen golden test',
|
||||||
|
(tester) async {
|
||||||
|
final builder = DeviceBuilder();
|
||||||
|
for (final scenario in _testScenarios) {
|
||||||
|
builder.addScenario(
|
||||||
|
name: scenario.toString(),
|
||||||
|
widget: _MockTimerFlow(scenario.shutterSpeedValue),
|
||||||
|
onCreate: (scenarioWidgetKey) async {
|
||||||
|
if (scenarioWidgetKey.toString().contains('Dark')) {
|
||||||
|
await setTheme(tester, scenarioWidgetKey, ThemeType.dark);
|
||||||
|
}
|
||||||
|
if (!scenario.isStopped) {
|
||||||
|
await toggleTimer(tester, scenarioWidgetKey);
|
||||||
|
late final skipTimerDuration = Duration(
|
||||||
|
milliseconds: (scenario.shutterSpeedValue.value * 0.35 * Duration.millisecondsPerSecond).toInt(),
|
||||||
|
);
|
||||||
|
await tester.pump(skipTimerDuration);
|
||||||
|
await mockResumedState(tester, scenarioWidgetKey);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await tester.pumpDeviceBuilder(builder);
|
||||||
|
await screenMatchesGolden(
|
||||||
|
tester,
|
||||||
|
'timer_screen',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MockTimerFlow extends StatelessWidget {
|
||||||
|
final ShutterSpeedValue shutterSpeedValue;
|
||||||
|
|
||||||
|
const _MockTimerFlow(this.shutterSpeedValue);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GoldenTestApplicationMock(
|
||||||
|
child: TimerFlow(
|
||||||
|
args: TimerFlowArgs(
|
||||||
|
exposurePair: ExposurePair(
|
||||||
|
ApertureValue.values.first,
|
||||||
|
shutterSpeedValue,
|
||||||
|
),
|
||||||
|
isoValue: const IsoValue(100, StopType.full),
|
||||||
|
ndValue: const NdValue(0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue