diff --git a/.vscode/launch.json b/.vscode/launch.json index ff372ab..37497fb 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -61,5 +61,18 @@ ], "program": "${workspaceFolder}/lib/main_dev.dart", }, + { + "name": "integration-test", + "request": "launch", + "type": "dart", + "flutterMode": "debug", + "args": [ + "--flavor", + "dev", + "--dart-define", + "cameraStubImage=assets/camera_stub_image.jpg" + ], + "program": "${workspaceFolder}/integration_test/run_all_tests.dart", + }, ], } \ No newline at end of file diff --git a/integration_test/e2e_test.dart b/integration_test/e2e_test.dart index 39306a4..e84f73b 100644 --- a/integration_test/e2e_test.dart +++ b/integration_test/e2e_test.dart @@ -16,6 +16,7 @@ import 'package:lightmeter/screens/metering/components/shared/readings_container import 'package:lightmeter/screens/settings/components/shared/dialog_filter/widget_dialog_filter.dart'; import 'package:lightmeter/screens/settings/components/shared/dialog_range_picker/widget_dialog_picker_range.dart'; import 'package:lightmeter/screens/settings/screen_settings.dart'; +import 'package:lightmeter/utils/double_to_zoom.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:meta/meta.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -56,6 +57,8 @@ void testE2E(String description) { await tester.setNdValues(0, mockEquipmentProfiles[0].ndValues); await tester.setApertureValues(0, mockEquipmentProfiles[0].apertureValues); await tester.setShutterSpeedValues(0, mockEquipmentProfiles[0].shutterSpeedValues); + await tester.setZoomValue(0, mockEquipmentProfiles[0].lensZoom); + expect(find.text('x1.91'), findsOneWidget); expect(find.text('f/1.7 - f/16'), findsOneWidget); expect(find.text('1/1000 - 16"'), findsOneWidget); @@ -65,6 +68,8 @@ void testE2E(String description) { await tester.setProfileName(mockEquipmentProfiles[1].name); await tester.expandEquipmentProfileContainer(mockEquipmentProfiles[1].name); await tester.setApertureValues(1, mockEquipmentProfiles[1].apertureValues); + await tester.setZoomValue(1, mockEquipmentProfiles[1].lensZoom); + expect(find.text('x5.02'), findsOneWidget); expect(find.text('f/3.5 - f/22'), findsOneWidget); expect(find.text('1/1000 - 16"'), findsNWidgets(2)); await tester.navigatorPop(); @@ -171,6 +176,9 @@ extension EquipmentProfileActions on WidgetTester { Future setShutterSpeedValues(int profileIndex, List values) => _setDialogRangePickerValues(profileIndex, S.current.shutterSpeedValues, values); + + Future setZoomValue(int profileIndex, double value) => + _setDialogSliderPickerValue(profileIndex, S.current.lensZoom, value); } extension on WidgetTester { @@ -235,6 +243,30 @@ extension on WidgetTester { await tapSaveButton(); } + + Future _setDialogSliderPickerValue( + int profileIndex, + String listTileTitle, + double value, + ) async { + await tap(find.text(listTileTitle).at(profileIndex)); + await pumpAndSettle(); + + final sliderFinder = find.byType(Slider); + final trackWidth = getSize(sliderFinder).width - (2 * Dimens.paddingL); + final trackStep = trackWidth / (widget(sliderFinder).max - widget(sliderFinder).min); + + final oldValue = widget(sliderFinder).value; + final oldStart = (oldValue - 1) * trackStep; + final newStart = (value - 1) * trackStep; + await dragFrom( + getTopLeft(sliderFinder) + Offset(Dimens.paddingL + oldStart, getSize(sliderFinder).height / 2), + Offset(newStart - oldStart, 0), + ); + await pump(); + + await tapSaveButton(); + } } Future _expectMeteringState( @@ -257,6 +289,7 @@ Future _expectMeteringState( await tester.scrollToTheLastExposurePair(equipmentProfile: equipmentProfile); expectExposurePairsListItem(tester, slowest.split(' - ')[0], slowest.split(' - ')[1]); expectMeasureButton(ev); + expect(find.text(equipmentProfile.lensZoom.toZoom()), findsOneWidget); } Future _expectMeteringStateAndMeasure( diff --git a/integration_test/mocks/paid_features_mock.dart b/integration_test/mocks/paid_features_mock.dart index 03402dd..8d4a287 100644 --- a/integration_test/mocks/paid_features_mock.dart +++ b/integration_test/mocks/paid_features_mock.dart @@ -91,6 +91,7 @@ final mockEquipmentProfiles = [ IsoValue(1600, StopType.full), IsoValue(3200, StopType.full), ], + lensZoom: 1.91, ), EquipmentProfile( id: '2', @@ -120,6 +121,7 @@ final mockEquipmentProfiles = [ IsoValue(1600, StopType.full), IsoValue(3200, StopType.full), ], + lensZoom: 5.02, ), ]; diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 2f12693..ba429c9 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -60,6 +60,8 @@ "shutterSpeedValuesFilterDescription": "Select the range of shutter speed values to display. This is usually determined by the camera body you are using.", "isoValues": "ISO values", "isoValuesFilterDescription": "Select the ISO values to display. These may be your most commonly used values or those supported by your camera.", + "lensZoom": "Lens zoom", + "lensZoomDescription": "Set the zoom level relative to the phone's camera to match your camera's viewfinder.", "equipmentProfile": "Equipment profile", "equipmentProfiles": "Equipment profiles", "tapToAdd": "Tap to add", diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index e1c7161..fe0f831 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -60,6 +60,8 @@ "shutterSpeedValuesFilterDescription": "Sélectionnez la plage de valeurs de vitesse d'obturation à afficher. Cela est généralement déterminé par le corps de l'appareil que vous utilisez.", "isoValues": "Valeurs ISO", "isoValuesFilterDescription": "Sélectionnez les valeurs ISO à afficher. Ce sont peut-être vos valeurs les plus couramment utilisées ou celles prises en charge par votre caméra.", + "lensZoom": "Zoom sur l'objectif", + "lensZoomDescription": "Réglez le niveau de zoom par rapport à l'appareil photo du téléphone pour qu'il corresponde au viseur de votre appareil photo.", "equipmentProfile": "Profil de l'équipement", "equipmentProfiles": "Profils de l'équipement", "tapToAdd": "Appuie pour ajouter", diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index 912b65c..9e4784c 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -60,6 +60,8 @@ "shutterSpeedValuesFilterDescription": "Выберите диапазон значений выдержки. Обычно ограничивается возможностями вашей камеры.", "isoValues": "Значения ISO", "isoValuesFilterDescription": "Выберите значения ISO для отображения. Это может быть наиболее часто используемые значения или значения, поддерживаемые вашей камерой.", + "lensZoom": "Зум объектива", + "lensZoomDescription": "Установите уровень зума относительно камеры телефона, чтобы он соответствовал видоискателю вашей камеры.", "equipmentProfile": "Оборудование", "equipmentProfiles": "Профили оборудования", "tapToAdd": "Нажмите, чтобы добавить", diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb index 7f81014..e389eaa 100644 --- a/lib/l10n/intl_zh.arb +++ b/lib/l10n/intl_zh.arb @@ -60,6 +60,8 @@ "shutterSpeedValuesFilterDescription": "选择要显示的快门速度范围。这通常由您使用的相机机身决定。", "isoValues": "ISO", "isoValuesFilterDescription": "选择要显示的 ISO 。这些可能是您常用的ISO值,也可以是相机支持的ISO范围。", + "lensZoom": "镜头变焦", + "lensZoomDescription": "设置相对于手机摄像头的变焦级别,使其与相机取景器相匹配。", "equipmentProfile": "设备配置", "equipmentProfiles": "设备配置", "tapToAdd": "点击添加", @@ -113,4 +115,4 @@ "tooltipUseLightSensor": "使用光线传感器", "tooltipUseCamera": "使用摄像头", "tooltipOpenSettings": "打开设置" -} +} \ No newline at end of file diff --git a/lib/res/dimens.dart b/lib/res/dimens.dart index e3eed95..430d0d5 100644 --- a/lib/res/dimens.dart +++ b/lib/res/dimens.dart @@ -38,7 +38,8 @@ class Dimens { // `CenteredSlider` static const double cameraSliderTrackHeight = grid4; static const double cameraSliderTrackRadius = cameraSliderTrackHeight / 2; - static const double cameraSliderHandleSize = 32; + static const double cameraSliderHandleArea = 32; + static const double cameraSliderHandleSize = 24; static const double cameraSliderHandleIconSize = cameraSliderHandleSize * 2 / 3; // Dialog diff --git a/lib/screens/metering/components/camera_container/bloc_container_camera.dart b/lib/screens/metering/components/camera_container/bloc_container_camera.dart index 56dec48..08bfee4 100644 --- a/lib/screens/metering/components/camera_container/bloc_container_camera.dart +++ b/lib/screens/metering/components/camera_container/bloc_container_camera.dart @@ -169,9 +169,10 @@ class CameraContainerBloc extends EvSourceBlocBase _onZoomChanged(ZoomChangedEvent event, Emitter emit) async { - if (_cameraController != null && event.value >= _zoomRange!.start && event.value <= _zoomRange!.end) { - _cameraController!.setZoomLevel(event.value); - _currentZoom = event.value; + if (_cameraController != null) { + final double zoom = event.value.clamp(_zoomRange!.start, _zoomRange!.end); + _cameraController!.setZoomLevel(zoom); + _currentZoom = zoom; _emitActiveState(emit); } } diff --git a/lib/screens/metering/components/camera_container/components/camera_controls/components/exposure_offset_slider/widget_slider_exposure_offset.dart b/lib/screens/metering/components/camera_container/components/camera_controls/components/exposure_offset_slider/widget_slider_exposure_offset.dart index 40ec1f0..bd1e71c 100644 --- a/lib/screens/metering/components/camera_container/components/camera_controls/components/exposure_offset_slider/widget_slider_exposure_offset.dart +++ b/lib/screens/metering/components/camera_container/components/camera_controls/components/exposure_offset_slider/widget_slider_exposure_offset.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/res/dimens.dart'; -import 'package:lightmeter/screens/shared/centered_slider/widget_slider_centered.dart'; +import 'package:lightmeter/screens/shared/ruler_slider/widget_slider_ruler.dart'; import 'package:lightmeter/utils/to_string_signed.dart'; class ExposureOffsetSlider extends StatelessWidget { @@ -18,76 +17,14 @@ class ExposureOffsetSlider extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - children: [ - IconButton( - icon: const Icon(Icons.sync), - onPressed: value != 0.0 ? () => onChanged(0.0) : null, - tooltip: S.of(context).tooltipResetToZero, - ), - Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: Dimens.grid8), - child: _Ruler( - range.start, - range.end, - ), - ), - CenteredSlider( - isVertical: true, - icon: const Icon(Icons.light_mode), - value: value, - min: range.start, - max: range.end, - onChanged: onChanged, - ), - ], - ), - ), - ], - ); - } -} - -class _Ruler extends StatelessWidget { - final double min; - final double max; - - const _Ruler(this.min, this.max); - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: List.generate( - (max - min + 1).toInt(), - (index) { - final bool showValue = index % 2 == 0.0 || index == 0.0; - return Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (showValue) - Text( - (index + min).toStringSignedAsFixed(0), - style: Theme.of(context).textTheme.bodyLarge, - ), - const SizedBox(width: Dimens.grid8), - ColoredBox( - color: Theme.of(context).colorScheme.onBackground, - child: SizedBox( - height: 1, - width: showValue ? Dimens.grid16 : Dimens.grid8, - ), - ), - const SizedBox(width: Dimens.grid8), - ], - ); - }, - ).reversed.toList(), + return RulerSlider( + range: range, + value: value, + onChanged: onChanged, + icon: Icons.light_mode, + defaultValue: 0, + rulerValueAdapter: (value) => value.toStringSignedAsFixed(0), + valueAdapter: (value) => S.of(context).evValue(value.toStringSignedAsFixed(1)), ); } } diff --git a/lib/screens/metering/components/camera_container/components/camera_controls/components/zoom_slider/widget_slider_zoom.dart b/lib/screens/metering/components/camera_container/components/camera_controls/components/zoom_slider/widget_slider_zoom.dart index e3619df..80cd4bd 100644 --- a/lib/screens/metering/components/camera_container/components/camera_controls/components/zoom_slider/widget_slider_zoom.dart +++ b/lib/screens/metering/components/camera_container/components/camera_controls/components/zoom_slider/widget_slider_zoom.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:lightmeter/screens/shared/centered_slider/widget_slider_centered.dart'; +import 'package:lightmeter/providers/equipment_profile_provider.dart'; +import 'package:lightmeter/screens/shared/ruler_slider/widget_slider_ruler.dart'; +import 'package:lightmeter/utils/double_to_zoom.dart'; -class ZoomSlider extends StatelessWidget { +class ZoomSlider extends StatefulWidget { final RangeValues range; final double value; final ValueChanged onChanged; @@ -13,14 +15,27 @@ class ZoomSlider extends StatelessWidget { super.key, }); + @override + State createState() => _ZoomSliderState(); +} + +class _ZoomSliderState extends State { + @override + void didChangeDependencies() { + super.didChangeDependencies(); + widget.onChanged(EquipmentProfiles.selectedOf(context).lensZoom); + } + @override Widget build(BuildContext context) { - return CenteredSlider( - icon: const Icon(Icons.search), - value: value, - min: range.start, - max: range.end, - onChanged: onChanged, + return RulerSlider( + range: widget.range, + value: widget.value, + onChanged: widget.onChanged, + icon: Icons.search, + defaultValue: EquipmentProfiles.selectedOf(context).lensZoom, + rulerValueAdapter: (value) => value.toStringAsFixed(0), + valueAdapter: (value) => value.toZoom(), ); } } diff --git a/lib/screens/metering/components/camera_container/components/camera_controls/widget_camera_controls.dart b/lib/screens/metering/components/camera_container/components/camera_controls/widget_camera_controls.dart index 493bbef..672d316 100644 --- a/lib/screens/metering/components/camera_container/components/camera_controls/widget_camera_controls.dart +++ b/lib/screens/metering/components/camera_container/components/camera_controls/widget_camera_controls.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/screens/metering/components/camera_container/components/camera_controls/components/exposure_offset_slider/widget_slider_exposure_offset.dart'; import 'package:lightmeter/screens/metering/components/camera_container/components/camera_controls/components/zoom_slider/widget_slider_zoom.dart'; @@ -25,16 +24,14 @@ class CameraControls extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - Expanded( - child: ExposureOffsetSlider( - range: exposureOffsetRange, - value: exposureOffsetValue, - onChanged: onExposureOffsetChanged, - ), + ExposureOffsetSlider( + range: exposureOffsetRange, + value: exposureOffsetValue, + onChanged: onExposureOffsetChanged, ), - const SizedBox(height: Dimens.grid24), ZoomSlider( range: zoomRange, value: zoomValue, diff --git a/lib/screens/metering/components/camera_container/widget_container_camera.dart b/lib/screens/metering/components/camera_container/widget_container_camera.dart index ceaca91..95cff57 100644 --- a/lib/screens/metering/components/camera_container/widget_container_camera.dart +++ b/lib/screens/metering/components/camera_container/widget_container_camera.dart @@ -157,7 +157,12 @@ class _CameraControlsBuilder extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.symmetric(vertical: Dimens.paddingM), + padding: const EdgeInsets.fromLTRB( + Dimens.paddingL, + Dimens.paddingL, + 0, + Dimens.paddingM, + ), child: BlocBuilder( builder: (context, state) { late final Widget child; diff --git a/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/widget_list_tiles_equipments.dart b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/widget_list_tiles_equipments.dart deleted file mode 100644 index bb3651d..0000000 --- a/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/widget_list_tiles_equipments.dart +++ /dev/null @@ -1,135 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/screens/settings/components/shared/dialog_filter/widget_dialog_filter.dart'; -import 'package:lightmeter/screens/settings/components/shared/dialog_range_picker/widget_dialog_picker_range.dart'; -import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; - -class EquipmentListTiles extends StatelessWidget { - final List selectedApertureValues; - final List selectedIsoValues; - final List selectedNdValues; - final List selectedShutterSpeedValues; - final ValueChanged> onApertureValuesSelected; - final ValueChanged> onIsoValuesSelecred; - final ValueChanged> onNdValuesSelected; - final ValueChanged> onShutterSpeedValuesSelected; - - const EquipmentListTiles({ - required this.selectedApertureValues, - required this.selectedIsoValues, - required this.selectedNdValues, - required this.selectedShutterSpeedValues, - required this.onApertureValuesSelected, - required this.onIsoValuesSelecred, - required this.onNdValuesSelected, - required this.onShutterSpeedValuesSelected, - super.key, - }); - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - _EquipmentListTile( - icon: Icons.iso, - title: S.of(context).isoValues, - description: S.of(context).isoValuesFilterDescription, - values: IsoValue.values, - selectedValues: selectedIsoValues, - rangeSelect: false, - onChanged: onIsoValuesSelecred, - ), - _EquipmentListTile( - icon: Icons.filter_b_and_w, - title: S.of(context).ndFilters, - description: S.of(context).ndFiltersFilterDescription, - values: NdValue.values, - selectedValues: selectedNdValues, - rangeSelect: false, - onChanged: onNdValuesSelected, - ), - _EquipmentListTile( - icon: Icons.camera, - title: S.of(context).apertureValues, - description: S.of(context).apertureValuesFilterDescription, - values: ApertureValue.values, - selectedValues: selectedApertureValues, - rangeSelect: true, - onChanged: onApertureValuesSelected, - ), - _EquipmentListTile( - icon: Icons.shutter_speed, - title: S.of(context).shutterSpeedValues, - description: S.of(context).shutterSpeedValuesFilterDescription, - values: ShutterSpeedValue.values, - selectedValues: selectedShutterSpeedValues, - rangeSelect: true, - onChanged: onShutterSpeedValuesSelected, - ), - ], - ); - } -} - -class _EquipmentListTile extends StatelessWidget { - final IconData icon; - final String title; - final String description; - final List selectedValues; - final List values; - final ValueChanged> onChanged; - final bool rangeSelect; - - const _EquipmentListTile({ - required this.icon, - required this.title, - required this.description, - required this.selectedValues, - required this.values, - required this.onChanged, - required this.rangeSelect, - super.key, - }); - - @override - Widget build(BuildContext context) { - return ListTile( - leading: Icon(icon), - title: Text(title), - trailing: rangeSelect - ? Text("${selectedValues.first} - ${selectedValues.last}") - : Text( - values.length == selectedValues.length - ? S.of(context).equipmentProfileAllValues - : selectedValues.length.toString(), - ), - onTap: () { - showDialog>( - context: context, - builder: (_) => rangeSelect - ? DialogRangePicker( - icon: Icon(icon), - title: title, - description: description, - values: values, - selectedValues: selectedValues, - titleAdapter: (_, value) => value.toString(), - ) - : DialogFilter( - icon: Icon(icon), - title: title, - description: description, - values: values, - selectedValues: selectedValues, - titleAdapter: (_, value) => value.toString(), - ), - ).then((values) { - if (values != null) { - onChanged(values); - } - }); - }, - ); - } -} diff --git a/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/filter_list_tile/widget_list_tile_filter.dart b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/filter_list_tile/widget_list_tile_filter.dart new file mode 100644 index 0000000..238f395 --- /dev/null +++ b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/filter_list_tile/widget_list_tile_filter.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/screens/settings/components/shared/dialog_filter/widget_dialog_filter.dart'; +import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; + +class FilterListTile extends StatelessWidget { + final IconData icon; + final String title; + final String description; + final List selectedValues; + final List values; + final ValueChanged> onChanged; + + const FilterListTile({ + required this.icon, + required this.title, + required this.description, + required this.selectedValues, + required this.values, + required this.onChanged, + super.key, + }); + + @override + Widget build(BuildContext context) { + return ListTile( + leading: Icon(icon), + title: Text(title), + trailing: Text( + values.length == selectedValues.length + ? S.of(context).equipmentProfileAllValues + : selectedValues.length.toString(), + ), + onTap: () { + showDialog>( + context: context, + builder: (_) => DialogFilter( + icon: Icon(icon), + title: title, + description: description, + values: values, + selectedValues: selectedValues, + titleAdapter: (_, value) => value.toString(), + ), + ).then((values) { + if (values != null) { + onChanged(values); + } + }); + }, + ); + } +} diff --git a/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/range_picker_list_tile/widget_list_tile_range_picker.dart b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/range_picker_list_tile/widget_list_tile_range_picker.dart new file mode 100644 index 0000000..bad6081 --- /dev/null +++ b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/range_picker_list_tile/widget_list_tile_range_picker.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/screens/settings/components/shared/dialog_range_picker/widget_dialog_picker_range.dart'; +import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; + +class RangePickerListTile extends StatelessWidget { + final IconData icon; + final String title; + final String description; + final List selectedValues; + final List values; + final ValueChanged> onChanged; + + const RangePickerListTile({ + required this.icon, + required this.title, + required this.description, + required this.selectedValues, + required this.values, + required this.onChanged, + super.key, + }); + + @override + Widget build(BuildContext context) { + return ListTile( + leading: Icon(icon), + title: Text(title), + trailing: Text("${selectedValues.first} - ${selectedValues.last}"), + onTap: () { + showDialog>( + context: context, + builder: (_) => DialogRangePicker( + icon: Icon(icon), + title: title, + description: description, + values: values, + selectedValues: selectedValues, + titleAdapter: (_, value) => value.toString(), + ), + ).then((values) { + if (values != null) { + onChanged(values); + } + }); + }, + ); + } +} diff --git a/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/slider_picker_list_tile/widget_list_tile_slider_picker.dart b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/slider_picker_list_tile/widget_list_tile_slider_picker.dart new file mode 100644 index 0000000..48d5caf --- /dev/null +++ b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/slider_picker_list_tile/widget_list_tile_slider_picker.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/screens/settings/components/shared/dialog_slider_picker/widget_dialog_slider_picker.dart'; + +class SliderPickerListTile extends StatelessWidget { + final IconData icon; + final String title; + final String description; + final double value; + final RangeValues range; + final ValueChanged onChanged; + final String Function(BuildContext, double) valueAdapter; + + const SliderPickerListTile({ + required this.icon, + required this.title, + required this.description, + required this.value, + required this.range, + required this.onChanged, + required this.valueAdapter, + super.key, + }); + + @override + Widget build(BuildContext context) { + return ListTile( + leading: Icon(icon), + title: Text(title), + trailing: Text(valueAdapter(context, value)), + onTap: () { + showDialog( + context: context, + builder: (_) => DialogSliderPicker( + icon: Icon(icon), + title: title, + description: description, + value: value, + range: range, + valueAdapter: valueAdapter, + ), + ).then((value) { + if (value != null) { + onChanged(value); + } + }); + }, + ); + } +} diff --git a/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart index 48a9c80..bb1cc72 100644 --- a/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart +++ b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart @@ -4,8 +4,11 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/res/dimens.dart'; -import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/widget_list_tiles_equipments.dart'; +import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/filter_list_tile/widget_list_tile_filter.dart'; +import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/range_picker_list_tile/widget_list_tile_range_picker.dart'; +import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/slider_picker_list_tile/widget_list_tile_slider_picker.dart'; import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_name_dialog/widget_dialog_equipment_profile_name.dart'; +import 'package:lightmeter/utils/double_to_zoom.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class EquipmentProfileContainer extends StatefulWidget { @@ -28,8 +31,7 @@ class EquipmentProfileContainer extends StatefulWidget { State createState() => EquipmentProfileContainerState(); } -class EquipmentProfileContainerState extends State - with TickerProviderStateMixin { +class EquipmentProfileContainerState extends State with TickerProviderStateMixin { late EquipmentProfile _equipmentData = EquipmentProfile( id: widget.data.id, name: widget.data.name, @@ -37,6 +39,7 @@ class EquipmentProfileContainerState extends State ndValues: widget.data.ndValues, shutterSpeedValues: widget.data.shutterSpeedValues, isoValues: widget.data.isoValues, + lensZoom: widget.data.lensZoom, ); late final AnimationController _controller = AnimationController( @@ -55,6 +58,7 @@ class EquipmentProfileContainerState extends State ndValues: widget.data.ndValues, shutterSpeedValues: widget.data.shutterSpeedValues, isoValues: widget.data.isoValues, + lensZoom: widget.data.lensZoom, ); } @@ -113,6 +117,10 @@ class EquipmentProfileContainerState extends State _equipmentData = _equipmentData.copyWith(shutterSpeedValues: value); widget.onUpdate(_equipmentData); }, + onLensZoomChanged: (value) { + _equipmentData = _equipmentData.copyWith(lensZoom: value); + widget.onUpdate(_equipmentData); + }, onCopy: widget.onCopy, onDelete: widget.onDelete, ), @@ -154,8 +162,7 @@ class EquipmentProfileContainerState extends State } class _AnimatedNameLeading extends AnimatedWidget { - const _AnimatedNameLeading({required AnimationController controller}) - : super(listenable: controller); + const _AnimatedNameLeading({required AnimationController controller}) : super(listenable: controller); Animation get _progress => listenable as Animation; @@ -200,6 +207,7 @@ class _AnimatedEquipmentListTiles extends AnimatedWidget { final ValueChanged> onIsoValuesSelecred; final ValueChanged> onNdValuesSelected; final ValueChanged> onShutterSpeedValuesSelected; + final ValueChanged onLensZoomChanged; final VoidCallback onCopy; final VoidCallback onDelete; @@ -210,6 +218,7 @@ class _AnimatedEquipmentListTiles extends AnimatedWidget { required this.onIsoValuesSelecred, required this.onNdValuesSelected, required this.onShutterSpeedValuesSelected, + required this.onLensZoomChanged, required this.onCopy, required this.onDelete, }) : super(listenable: controller); @@ -222,22 +231,53 @@ class _AnimatedEquipmentListTiles extends AnimatedWidget { alignment: Alignment.topCenter, size: Size( double.maxFinite, - _progress.value * Dimens.grid56 * 5, + _progress.value * Dimens.grid56 * 6, ), // https://github.com/gskinnerTeam/flutter-folio/pull/62 child: Opacity( opacity: _progress.value, child: Column( children: [ - EquipmentListTiles( - selectedApertureValues: equipmentData.apertureValues, - selectedIsoValues: equipmentData.isoValues, - selectedNdValues: equipmentData.ndValues, - selectedShutterSpeedValues: equipmentData.shutterSpeedValues, - onApertureValuesSelected: onApertureValuesSelected, - onIsoValuesSelecred: onIsoValuesSelecred, - onNdValuesSelected: onNdValuesSelected, - onShutterSpeedValuesSelected: onShutterSpeedValuesSelected, + FilterListTile( + icon: Icons.iso, + title: S.of(context).isoValues, + description: S.of(context).isoValuesFilterDescription, + values: IsoValue.values, + selectedValues: equipmentData.isoValues, + onChanged: onIsoValuesSelecred, + ), + FilterListTile( + icon: Icons.filter_b_and_w, + title: S.of(context).ndFilters, + description: S.of(context).ndFiltersFilterDescription, + values: NdValue.values, + selectedValues: equipmentData.ndValues, + onChanged: onNdValuesSelected, + ), + RangePickerListTile( + icon: Icons.camera, + title: S.of(context).apertureValues, + description: S.of(context).apertureValuesFilterDescription, + values: ApertureValue.values, + selectedValues: equipmentData.apertureValues, + onChanged: onApertureValuesSelected, + ), + RangePickerListTile( + icon: Icons.shutter_speed, + title: S.of(context).shutterSpeedValues, + description: S.of(context).shutterSpeedValuesFilterDescription, + values: ShutterSpeedValue.values, + selectedValues: equipmentData.shutterSpeedValues, + onChanged: onShutterSpeedValuesSelected, + ), + SliderPickerListTile( + icon: Icons.zoom_in, + title: S.of(context).lensZoom, + description: S.of(context).lensZoomDescription, + value: equipmentData.lensZoom, + range: const RangeValues(1, 7), + onChanged: onLensZoomChanged, + valueAdapter: (_, value) => value.toZoom(), ), ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM), diff --git a/lib/screens/settings/components/shared/dialog_slider_picker/widget_dialog_slider_picker.dart b/lib/screens/settings/components/shared/dialog_slider_picker/widget_dialog_slider_picker.dart new file mode 100644 index 0000000..7923e95 --- /dev/null +++ b/lib/screens/settings/components/shared/dialog_slider_picker/widget_dialog_slider_picker.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/res/dimens.dart'; + +class DialogSliderPicker extends StatefulWidget { + final Icon icon; + final String title; + final String description; + final double value; + final RangeValues range; + final String Function(BuildContext context, double value) valueAdapter; + + const DialogSliderPicker({ + required this.icon, + required this.title, + required this.description, + required this.value, + required this.range, + required this.valueAdapter, + super.key, + }); + + @override + State createState() => _DialogSliderPickerState(); +} + +class _DialogSliderPickerState extends State { + late double value = widget.value; + + @override + Widget build(BuildContext context) { + return AlertDialog( + icon: widget.icon, + titlePadding: Dimens.dialogIconTitlePadding, + title: Text(widget.title), + contentPadding: EdgeInsets.zero, + content: SizedBox( + width: double.maxFinite, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: Dimens.dialogIconTitlePadding, + child: Text(widget.description), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingL), + child: Text( + widget.valueAdapter(context, value), + style: Theme.of(context).textTheme.bodyLarge, + ), + ), + Row( + children: [ + Expanded( + child: Slider( + value: value, + min: widget.range.start, + max: widget.range.end, + onChanged: (value) { + setState(() { + this.value = value; + }); + }, + ), + ), + ], + ), + ], + ), + ), + actionsPadding: Dimens.dialogActionsPadding, + actions: [ + TextButton( + onPressed: Navigator.of(context).pop, + child: Text(S.of(context).cancel), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(value), + child: Text(S.of(context).save), + ), + ], + ); + } +} diff --git a/lib/screens/shared/centered_slider/widget_slider_centered.dart b/lib/screens/shared/centered_slider/widget_slider_centered.dart index 56c2c91..f2cc66d 100644 --- a/lib/screens/shared/centered_slider/widget_slider_centered.dart +++ b/lib/screens/shared/centered_slider/widget_slider_centered.dart @@ -41,12 +41,12 @@ class _CenteredSliderState extends State { @override Widget build(BuildContext context) { return SizedBox( - height: widget.isVertical ? double.maxFinite : Dimens.cameraSliderHandleSize, - width: !widget.isVertical ? double.maxFinite : Dimens.cameraSliderHandleSize, + height: widget.isVertical ? double.maxFinite : Dimens.cameraSliderHandleArea, + width: !widget.isVertical ? double.maxFinite : Dimens.cameraSliderHandleArea, child: LayoutBuilder( builder: (context, constraints) { final biggestSize = widget.isVertical ? constraints.maxHeight : constraints.maxWidth; - final handleDistance = biggestSize - Dimens.cameraSliderHandleSize; + final handleDistance = biggestSize - Dimens.cameraSliderHandleArea; return RotatedBox( quarterTurns: widget.isVertical ? -1 : 0, child: GestureDetector( @@ -60,11 +60,11 @@ class _CenteredSliderState extends State { handleDistance, ), child: SizedBox( - height: Dimens.cameraSliderHandleSize, + height: Dimens.cameraSliderHandleArea, width: biggestSize, child: _Slider( handleDistance: handleDistance, - handleSize: Dimens.cameraSliderHandleSize, + handleSize: Dimens.cameraSliderHandleArea, trackThickness: Dimens.cameraSliderTrackHeight, value: relativeValue, icon: RotatedBox( @@ -81,12 +81,12 @@ class _CenteredSliderState extends State { } void _updateHandlePosition(double offset, double handleDistance) { - if (offset <= Dimens.cameraSliderHandleSize / 2) { + if (offset <= Dimens.cameraSliderHandleArea / 2) { relativeValue = 0; - } else if (offset >= handleDistance + Dimens.cameraSliderHandleSize / 2) { + } else if (offset >= handleDistance + Dimens.cameraSliderHandleArea / 2) { relativeValue = 1; } else { - relativeValue = (offset - Dimens.cameraSliderHandleSize / 2) / handleDistance; + relativeValue = (offset - Dimens.cameraSliderHandleArea / 2) / handleDistance; } setState(() {}); widget.onChanged(relativeValue * (widget.max - widget.min) + widget.min); @@ -124,7 +124,7 @@ class _Slider extends StatelessWidget { ), ), AnimatedPositioned.fromRect( - duration: Dimens.durationM, + duration: Dimens.durationS, rect: Rect.fromCenter( center: Offset( handleSize / 2 + handleDistance * value, @@ -133,15 +133,17 @@ class _Slider extends StatelessWidget { width: handleSize, height: handleSize, ), - child: _Handle( - color: Theme.of(context).colorScheme.primary, - size: handleSize, - child: IconTheme( - data: Theme.of(context).iconTheme.copyWith( - color: Theme.of(context).colorScheme.onPrimary, - size: Dimens.cameraSliderHandleIconSize, - ), - child: icon, + child: Center( + child: _Handle( + color: Theme.of(context).colorScheme.primary, + size: Dimens.cameraSliderHandleSize, + child: IconTheme( + data: Theme.of(context).iconTheme.copyWith( + color: Theme.of(context).colorScheme.onPrimary, + size: Dimens.cameraSliderHandleIconSize, + ), + child: icon, + ), ), ), ), @@ -163,15 +165,15 @@ class _Handle extends StatelessWidget { @override Widget build(BuildContext context) { - return ClipRRect( - borderRadius: BorderRadius.circular(size / 2), - child: SizedBox( - height: size, - width: size, - child: ColoredBox( + return SizedBox( + height: size, + width: size, + child: DecoratedBox( + decoration: BoxDecoration( color: color, - child: child, + shape: BoxShape.circle, ), + child: child, ), ); } diff --git a/lib/screens/shared/ruler_slider/widget_slider_ruler.dart b/lib/screens/shared/ruler_slider/widget_slider_ruler.dart new file mode 100644 index 0000000..f9f37ff --- /dev/null +++ b/lib/screens/shared/ruler_slider/widget_slider_ruler.dart @@ -0,0 +1,121 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/res/dimens.dart'; +import 'package:lightmeter/screens/shared/centered_slider/widget_slider_centered.dart'; + +class RulerSlider extends StatelessWidget { + final IconData icon; + final RangeValues range; + final double value; + final double defaultValue; + final ValueChanged onChanged; + final String Function(double value) rulerValueAdapter; + final String Function(double value) valueAdapter; + + const RulerSlider({ + required this.icon, + required this.range, + required this.value, + required this.defaultValue, + required this.onChanged, + required this.rulerValueAdapter, + required this.valueAdapter, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + valueAdapter(value), + style: Theme.of(context).textTheme.labelLarge, + ), + const SizedBox(height: Dimens.grid4), + Expanded( + child: Row( + children: [ + _Ruler( + range.start, + range.end, + rulerValueAdapter, + ), + CenteredSlider( + isVertical: true, + icon: Icon(icon), + value: value, + min: range.start, + max: range.end, + onChanged: onChanged, + ), + ], + ), + ), + IconButton( + icon: const Icon(Icons.sync), + onPressed: value != defaultValue ? () => onChanged(defaultValue) : null, + tooltip: S.of(context).tooltipResetToZero, + ), + ], + ); + } +} + +class _Ruler extends StatelessWidget { + final double min; + final double max; + final String Function(double value) rulerValueAdapter; + late final int mainTicksCount = (max - min + 1).toInt(); + late final int itemsCount = mainTicksCount * 2 - 1; + + _Ruler( + this.min, + this.max, + this.rulerValueAdapter, + ); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final bool showAllMainTicks = Dimens.cameraSliderHandleArea * mainTicksCount <= constraints.maxHeight; + return Column( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: List.generate( + itemsCount, + (index) { + final bool isMainTick = index % 2 == 0.0; + if (!showAllMainTicks && !isMainTick) { + return const SizedBox(); + } + final bool showValue = (index % (showAllMainTicks ? 2 : 4) == 0.0); + return SizedBox( + height: index == itemsCount - 1 || showValue ? Dimens.cameraSliderHandleArea : 1, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (showValue) + Text( + rulerValueAdapter(index / 2 + min), + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(width: Dimens.grid4), + ColoredBox( + color: Theme.of(context).colorScheme.onBackground, + child: SizedBox( + height: 1, + width: isMainTick ? Dimens.grid8 : Dimens.grid4, + ), + ), + ], + ), + ); + }, + ).reversed.toList(), + ); + }, + ); + } +} diff --git a/lib/utils/double_to_zoom.dart b/lib/utils/double_to_zoom.dart new file mode 100644 index 0000000..6d2b99f --- /dev/null +++ b/lib/utils/double_to_zoom.dart @@ -0,0 +1,3 @@ +extension DoubleToZoom on double { + String toZoom() => 'x${toStringAsFixed(2)}'; +}