ML-44 Save zoom level for equipment profile (#164)

* made zoom slider vertical & added more ticks to ruler

* show sliders values

* increased slider tappable area

* more accurate sliders values

* added zoom slider to equipment profiles settings

* split `EquipmentListTiles` widget

* set zoom on equipment profile change

* clamp zoom to the nearest value

* added missing translations

* added zoom checks to e2e test

* removed unused import
This commit is contained in:
Vadim 2024-04-07 10:54:57 +02:00 committed by GitHub
parent bfd0bfe531
commit 2117df2f11
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 548 additions and 270 deletions

13
.vscode/launch.json vendored
View file

@ -61,5 +61,18 @@
], ],
"program": "${workspaceFolder}/lib/main_dev.dart", "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",
},
], ],
} }

View file

@ -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_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/components/shared/dialog_range_picker/widget_dialog_picker_range.dart';
import 'package:lightmeter/screens/settings/screen_settings.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:m3_lightmeter_resources/m3_lightmeter_resources.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:shared_preferences/shared_preferences.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.setNdValues(0, mockEquipmentProfiles[0].ndValues);
await tester.setApertureValues(0, mockEquipmentProfiles[0].apertureValues); await tester.setApertureValues(0, mockEquipmentProfiles[0].apertureValues);
await tester.setShutterSpeedValues(0, mockEquipmentProfiles[0].shutterSpeedValues); 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('f/1.7 - f/16'), findsOneWidget);
expect(find.text('1/1000 - 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.setProfileName(mockEquipmentProfiles[1].name);
await tester.expandEquipmentProfileContainer(mockEquipmentProfiles[1].name); await tester.expandEquipmentProfileContainer(mockEquipmentProfiles[1].name);
await tester.setApertureValues(1, mockEquipmentProfiles[1].apertureValues); 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('f/3.5 - f/22'), findsOneWidget);
expect(find.text('1/1000 - 16"'), findsNWidgets(2)); expect(find.text('1/1000 - 16"'), findsNWidgets(2));
await tester.navigatorPop(); await tester.navigatorPop();
@ -171,6 +176,9 @@ extension EquipmentProfileActions on WidgetTester {
Future<void> setShutterSpeedValues(int profileIndex, List<ShutterSpeedValue> values) => Future<void> setShutterSpeedValues(int profileIndex, List<ShutterSpeedValue> values) =>
_setDialogRangePickerValues<ShutterSpeedValue>(profileIndex, S.current.shutterSpeedValues, values); _setDialogRangePickerValues<ShutterSpeedValue>(profileIndex, S.current.shutterSpeedValues, values);
Future<void> setZoomValue(int profileIndex, double value) =>
_setDialogSliderPickerValue(profileIndex, S.current.lensZoom, value);
} }
extension on WidgetTester { extension on WidgetTester {
@ -235,6 +243,30 @@ extension on WidgetTester {
await tapSaveButton(); await tapSaveButton();
} }
Future<void> _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<Slider>(sliderFinder).max - widget<Slider>(sliderFinder).min);
final oldValue = widget<Slider>(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<void> _expectMeteringState( Future<void> _expectMeteringState(
@ -257,6 +289,7 @@ Future<void> _expectMeteringState(
await tester.scrollToTheLastExposurePair(equipmentProfile: equipmentProfile); await tester.scrollToTheLastExposurePair(equipmentProfile: equipmentProfile);
expectExposurePairsListItem(tester, slowest.split(' - ')[0], slowest.split(' - ')[1]); expectExposurePairsListItem(tester, slowest.split(' - ')[0], slowest.split(' - ')[1]);
expectMeasureButton(ev); expectMeasureButton(ev);
expect(find.text(equipmentProfile.lensZoom.toZoom()), findsOneWidget);
} }
Future<void> _expectMeteringStateAndMeasure( Future<void> _expectMeteringStateAndMeasure(

View file

@ -91,6 +91,7 @@ final mockEquipmentProfiles = [
IsoValue(1600, StopType.full), IsoValue(1600, StopType.full),
IsoValue(3200, StopType.full), IsoValue(3200, StopType.full),
], ],
lensZoom: 1.91,
), ),
EquipmentProfile( EquipmentProfile(
id: '2', id: '2',
@ -120,6 +121,7 @@ final mockEquipmentProfiles = [
IsoValue(1600, StopType.full), IsoValue(1600, StopType.full),
IsoValue(3200, StopType.full), IsoValue(3200, StopType.full),
], ],
lensZoom: 5.02,
), ),
]; ];

View file

@ -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.", "shutterSpeedValuesFilterDescription": "Select the range of shutter speed values to display. This is usually determined by the camera body you are using.",
"isoValues": "ISO values", "isoValues": "ISO values",
"isoValuesFilterDescription": "Select the ISO values to display. These may be your most commonly used values or those supported by your camera.", "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", "equipmentProfile": "Equipment profile",
"equipmentProfiles": "Equipment profiles", "equipmentProfiles": "Equipment profiles",
"tapToAdd": "Tap to add", "tapToAdd": "Tap to add",

View file

@ -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.", "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", "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.", "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", "equipmentProfile": "Profil de l'équipement",
"equipmentProfiles": "Profils de l'équipement", "equipmentProfiles": "Profils de l'équipement",
"tapToAdd": "Appuie pour ajouter", "tapToAdd": "Appuie pour ajouter",

View file

@ -60,6 +60,8 @@
"shutterSpeedValuesFilterDescription": "Выберите диапазон значений выдержки. Обычно ограничивается возможностями вашей камеры.", "shutterSpeedValuesFilterDescription": "Выберите диапазон значений выдержки. Обычно ограничивается возможностями вашей камеры.",
"isoValues": "Значения ISO", "isoValues": "Значения ISO",
"isoValuesFilterDescription": "Выберите значения ISO для отображения. Это может быть наиболее часто используемые значения или значения, поддерживаемые вашей камерой.", "isoValuesFilterDescription": "Выберите значения ISO для отображения. Это может быть наиболее часто используемые значения или значения, поддерживаемые вашей камерой.",
"lensZoom": "Зум объектива",
"lensZoomDescription": "Установите уровень зума относительно камеры телефона, чтобы он соответствовал видоискателю вашей камеры.",
"equipmentProfile": "Оборудование", "equipmentProfile": "Оборудование",
"equipmentProfiles": "Профили оборудования", "equipmentProfiles": "Профили оборудования",
"tapToAdd": "Нажмите, чтобы добавить", "tapToAdd": "Нажмите, чтобы добавить",

View file

@ -60,6 +60,8 @@
"shutterSpeedValuesFilterDescription": "选择要显示的快门速度范围。这通常由您使用的相机机身决定。", "shutterSpeedValuesFilterDescription": "选择要显示的快门速度范围。这通常由您使用的相机机身决定。",
"isoValues": "ISO", "isoValues": "ISO",
"isoValuesFilterDescription": "选择要显示的 ISO 。这些可能是您常用的ISO值也可以是相机支持的ISO范围。", "isoValuesFilterDescription": "选择要显示的 ISO 。这些可能是您常用的ISO值也可以是相机支持的ISO范围。",
"lensZoom": "镜头变焦",
"lensZoomDescription": "设置相对于手机摄像头的变焦级别,使其与相机取景器相匹配。",
"equipmentProfile": "设备配置", "equipmentProfile": "设备配置",
"equipmentProfiles": "设备配置", "equipmentProfiles": "设备配置",
"tapToAdd": "点击添加", "tapToAdd": "点击添加",
@ -113,4 +115,4 @@
"tooltipUseLightSensor": "使用光线传感器", "tooltipUseLightSensor": "使用光线传感器",
"tooltipUseCamera": "使用摄像头", "tooltipUseCamera": "使用摄像头",
"tooltipOpenSettings": "打开设置" "tooltipOpenSettings": "打开设置"
} }

View file

@ -38,7 +38,8 @@ class Dimens {
// `CenteredSlider` // `CenteredSlider`
static const double cameraSliderTrackHeight = grid4; static const double cameraSliderTrackHeight = grid4;
static const double cameraSliderTrackRadius = cameraSliderTrackHeight / 2; 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; static const double cameraSliderHandleIconSize = cameraSliderHandleSize * 2 / 3;
// Dialog // Dialog

View file

@ -169,9 +169,10 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
} }
Future<void> _onZoomChanged(ZoomChangedEvent event, Emitter emit) async { Future<void> _onZoomChanged(ZoomChangedEvent event, Emitter emit) async {
if (_cameraController != null && event.value >= _zoomRange!.start && event.value <= _zoomRange!.end) { if (_cameraController != null) {
_cameraController!.setZoomLevel(event.value); final double zoom = event.value.clamp(_zoomRange!.start, _zoomRange!.end);
_currentZoom = event.value; _cameraController!.setZoomLevel(zoom);
_currentZoom = zoom;
_emitActiveState(emit); _emitActiveState(emit);
} }
} }

View file

@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/screens/shared/ruler_slider/widget_slider_ruler.dart';
import 'package:lightmeter/screens/shared/centered_slider/widget_slider_centered.dart';
import 'package:lightmeter/utils/to_string_signed.dart'; import 'package:lightmeter/utils/to_string_signed.dart';
class ExposureOffsetSlider extends StatelessWidget { class ExposureOffsetSlider extends StatelessWidget {
@ -18,76 +17,14 @@ class ExposureOffsetSlider extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return RulerSlider(
children: [ range: range,
IconButton( value: value,
icon: const Icon(Icons.sync), onChanged: onChanged,
onPressed: value != 0.0 ? () => onChanged(0.0) : null, icon: Icons.light_mode,
tooltip: S.of(context).tooltipResetToZero, defaultValue: 0,
), rulerValueAdapter: (value) => value.toStringSignedAsFixed(0),
Expanded( valueAdapter: (value) => S.of(context).evValue(value.toStringSignedAsFixed(1)),
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(),
); );
} }
} }

View file

@ -1,7 +1,9 @@
import 'package:flutter/material.dart'; 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 RangeValues range;
final double value; final double value;
final ValueChanged<double> onChanged; final ValueChanged<double> onChanged;
@ -13,14 +15,27 @@ class ZoomSlider extends StatelessWidget {
super.key, super.key,
}); });
@override
State<ZoomSlider> createState() => _ZoomSliderState();
}
class _ZoomSliderState extends State<ZoomSlider> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
widget.onChanged(EquipmentProfiles.selectedOf(context).lensZoom);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return CenteredSlider( return RulerSlider(
icon: const Icon(Icons.search), range: widget.range,
value: value, value: widget.value,
min: range.start, onChanged: widget.onChanged,
max: range.end, icon: Icons.search,
onChanged: onChanged, defaultValue: EquipmentProfiles.selectedOf(context).lensZoom,
rulerValueAdapter: (value) => value.toStringAsFixed(0),
valueAdapter: (value) => value.toZoom(),
); );
} }
} }

View file

@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; 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/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'; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
Expanded( ExposureOffsetSlider(
child: ExposureOffsetSlider( range: exposureOffsetRange,
range: exposureOffsetRange, value: exposureOffsetValue,
value: exposureOffsetValue, onChanged: onExposureOffsetChanged,
onChanged: onExposureOffsetChanged,
),
), ),
const SizedBox(height: Dimens.grid24),
ZoomSlider( ZoomSlider(
range: zoomRange, range: zoomRange,
value: zoomValue, value: zoomValue,

View file

@ -157,7 +157,12 @@ class _CameraControlsBuilder extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: Dimens.paddingM), padding: const EdgeInsets.fromLTRB(
Dimens.paddingL,
Dimens.paddingL,
0,
Dimens.paddingM,
),
child: BlocBuilder<CameraContainerBloc, CameraContainerState>( child: BlocBuilder<CameraContainerBloc, CameraContainerState>(
builder: (context, state) { builder: (context, state) {
late final Widget child; late final Widget child;

View file

@ -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<ApertureValue> selectedApertureValues;
final List<IsoValue> selectedIsoValues;
final List<NdValue> selectedNdValues;
final List<ShutterSpeedValue> selectedShutterSpeedValues;
final ValueChanged<List<ApertureValue>> onApertureValuesSelected;
final ValueChanged<List<IsoValue>> onIsoValuesSelecred;
final ValueChanged<List<NdValue>> onNdValuesSelected;
final ValueChanged<List<ShutterSpeedValue>> 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<IsoValue>(
icon: Icons.iso,
title: S.of(context).isoValues,
description: S.of(context).isoValuesFilterDescription,
values: IsoValue.values,
selectedValues: selectedIsoValues,
rangeSelect: false,
onChanged: onIsoValuesSelecred,
),
_EquipmentListTile<NdValue>(
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<ApertureValue>(
icon: Icons.camera,
title: S.of(context).apertureValues,
description: S.of(context).apertureValuesFilterDescription,
values: ApertureValue.values,
selectedValues: selectedApertureValues,
rangeSelect: true,
onChanged: onApertureValuesSelected,
),
_EquipmentListTile<ShutterSpeedValue>(
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<T extends PhotographyValue> extends StatelessWidget {
final IconData icon;
final String title;
final String description;
final List<T> selectedValues;
final List<T> values;
final ValueChanged<List<T>> 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<List<T>>(
context: context,
builder: (_) => rangeSelect
? DialogRangePicker<T>(
icon: Icon(icon),
title: title,
description: description,
values: values,
selectedValues: selectedValues,
titleAdapter: (_, value) => value.toString(),
)
: DialogFilter<T>(
icon: Icon(icon),
title: title,
description: description,
values: values,
selectedValues: selectedValues,
titleAdapter: (_, value) => value.toString(),
),
).then((values) {
if (values != null) {
onChanged(values);
}
});
},
);
}
}

View file

@ -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<T extends PhotographyValue> extends StatelessWidget {
final IconData icon;
final String title;
final String description;
final List<T> selectedValues;
final List<T> values;
final ValueChanged<List<T>> 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<List<T>>(
context: context,
builder: (_) => DialogFilter<T>(
icon: Icon(icon),
title: title,
description: description,
values: values,
selectedValues: selectedValues,
titleAdapter: (_, value) => value.toString(),
),
).then((values) {
if (values != null) {
onChanged(values);
}
});
},
);
}
}

View file

@ -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<T extends PhotographyValue> extends StatelessWidget {
final IconData icon;
final String title;
final String description;
final List<T> selectedValues;
final List<T> values;
final ValueChanged<List<T>> 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<List<T>>(
context: context,
builder: (_) => DialogRangePicker<T>(
icon: Icon(icon),
title: title,
description: description,
values: values,
selectedValues: selectedValues,
titleAdapter: (_, value) => value.toString(),
),
).then((values) {
if (values != null) {
onChanged(values);
}
});
},
);
}
}

View file

@ -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<double> 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<double>(
context: context,
builder: (_) => DialogSliderPicker(
icon: Icon(icon),
title: title,
description: description,
value: value,
range: range,
valueAdapter: valueAdapter,
),
).then((value) {
if (value != null) {
onChanged(value);
}
});
},
);
}
}

View file

@ -4,8 +4,11 @@ import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.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/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/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'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class EquipmentProfileContainer extends StatefulWidget { class EquipmentProfileContainer extends StatefulWidget {
@ -28,8 +31,7 @@ class EquipmentProfileContainer extends StatefulWidget {
State<EquipmentProfileContainer> createState() => EquipmentProfileContainerState(); State<EquipmentProfileContainer> createState() => EquipmentProfileContainerState();
} }
class EquipmentProfileContainerState extends State<EquipmentProfileContainer> class EquipmentProfileContainerState extends State<EquipmentProfileContainer> with TickerProviderStateMixin {
with TickerProviderStateMixin {
late EquipmentProfile _equipmentData = EquipmentProfile( late EquipmentProfile _equipmentData = EquipmentProfile(
id: widget.data.id, id: widget.data.id,
name: widget.data.name, name: widget.data.name,
@ -37,6 +39,7 @@ class EquipmentProfileContainerState extends State<EquipmentProfileContainer>
ndValues: widget.data.ndValues, ndValues: widget.data.ndValues,
shutterSpeedValues: widget.data.shutterSpeedValues, shutterSpeedValues: widget.data.shutterSpeedValues,
isoValues: widget.data.isoValues, isoValues: widget.data.isoValues,
lensZoom: widget.data.lensZoom,
); );
late final AnimationController _controller = AnimationController( late final AnimationController _controller = AnimationController(
@ -55,6 +58,7 @@ class EquipmentProfileContainerState extends State<EquipmentProfileContainer>
ndValues: widget.data.ndValues, ndValues: widget.data.ndValues,
shutterSpeedValues: widget.data.shutterSpeedValues, shutterSpeedValues: widget.data.shutterSpeedValues,
isoValues: widget.data.isoValues, isoValues: widget.data.isoValues,
lensZoom: widget.data.lensZoom,
); );
} }
@ -113,6 +117,10 @@ class EquipmentProfileContainerState extends State<EquipmentProfileContainer>
_equipmentData = _equipmentData.copyWith(shutterSpeedValues: value); _equipmentData = _equipmentData.copyWith(shutterSpeedValues: value);
widget.onUpdate(_equipmentData); widget.onUpdate(_equipmentData);
}, },
onLensZoomChanged: (value) {
_equipmentData = _equipmentData.copyWith(lensZoom: value);
widget.onUpdate(_equipmentData);
},
onCopy: widget.onCopy, onCopy: widget.onCopy,
onDelete: widget.onDelete, onDelete: widget.onDelete,
), ),
@ -154,8 +162,7 @@ class EquipmentProfileContainerState extends State<EquipmentProfileContainer>
} }
class _AnimatedNameLeading extends AnimatedWidget { class _AnimatedNameLeading extends AnimatedWidget {
const _AnimatedNameLeading({required AnimationController controller}) const _AnimatedNameLeading({required AnimationController controller}) : super(listenable: controller);
: super(listenable: controller);
Animation<double> get _progress => listenable as Animation<double>; Animation<double> get _progress => listenable as Animation<double>;
@ -200,6 +207,7 @@ class _AnimatedEquipmentListTiles extends AnimatedWidget {
final ValueChanged<List<IsoValue>> onIsoValuesSelecred; final ValueChanged<List<IsoValue>> onIsoValuesSelecred;
final ValueChanged<List<NdValue>> onNdValuesSelected; final ValueChanged<List<NdValue>> onNdValuesSelected;
final ValueChanged<List<ShutterSpeedValue>> onShutterSpeedValuesSelected; final ValueChanged<List<ShutterSpeedValue>> onShutterSpeedValuesSelected;
final ValueChanged<double> onLensZoomChanged;
final VoidCallback onCopy; final VoidCallback onCopy;
final VoidCallback onDelete; final VoidCallback onDelete;
@ -210,6 +218,7 @@ class _AnimatedEquipmentListTiles extends AnimatedWidget {
required this.onIsoValuesSelecred, required this.onIsoValuesSelecred,
required this.onNdValuesSelected, required this.onNdValuesSelected,
required this.onShutterSpeedValuesSelected, required this.onShutterSpeedValuesSelected,
required this.onLensZoomChanged,
required this.onCopy, required this.onCopy,
required this.onDelete, required this.onDelete,
}) : super(listenable: controller); }) : super(listenable: controller);
@ -222,22 +231,53 @@ class _AnimatedEquipmentListTiles extends AnimatedWidget {
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
size: Size( size: Size(
double.maxFinite, double.maxFinite,
_progress.value * Dimens.grid56 * 5, _progress.value * Dimens.grid56 * 6,
), ),
// https://github.com/gskinnerTeam/flutter-folio/pull/62 // https://github.com/gskinnerTeam/flutter-folio/pull/62
child: Opacity( child: Opacity(
opacity: _progress.value, opacity: _progress.value,
child: Column( child: Column(
children: [ children: [
EquipmentListTiles( FilterListTile<IsoValue>(
selectedApertureValues: equipmentData.apertureValues, icon: Icons.iso,
selectedIsoValues: equipmentData.isoValues, title: S.of(context).isoValues,
selectedNdValues: equipmentData.ndValues, description: S.of(context).isoValuesFilterDescription,
selectedShutterSpeedValues: equipmentData.shutterSpeedValues, values: IsoValue.values,
onApertureValuesSelected: onApertureValuesSelected, selectedValues: equipmentData.isoValues,
onIsoValuesSelecred: onIsoValuesSelecred, onChanged: onIsoValuesSelecred,
onNdValuesSelected: onNdValuesSelected, ),
onShutterSpeedValuesSelected: onShutterSpeedValuesSelected, FilterListTile<NdValue>(
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<ApertureValue>(
icon: Icons.camera,
title: S.of(context).apertureValues,
description: S.of(context).apertureValuesFilterDescription,
values: ApertureValue.values,
selectedValues: equipmentData.apertureValues,
onChanged: onApertureValuesSelected,
),
RangePickerListTile<ShutterSpeedValue>(
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( ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM), contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),

View file

@ -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<DialogSliderPicker> createState() => _DialogSliderPickerState();
}
class _DialogSliderPickerState extends State<DialogSliderPicker> {
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),
),
],
);
}
}

View file

@ -41,12 +41,12 @@ class _CenteredSliderState extends State<CenteredSlider> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return SizedBox(
height: widget.isVertical ? double.maxFinite : Dimens.cameraSliderHandleSize, height: widget.isVertical ? double.maxFinite : Dimens.cameraSliderHandleArea,
width: !widget.isVertical ? double.maxFinite : Dimens.cameraSliderHandleSize, width: !widget.isVertical ? double.maxFinite : Dimens.cameraSliderHandleArea,
child: LayoutBuilder( child: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final biggestSize = widget.isVertical ? constraints.maxHeight : constraints.maxWidth; final biggestSize = widget.isVertical ? constraints.maxHeight : constraints.maxWidth;
final handleDistance = biggestSize - Dimens.cameraSliderHandleSize; final handleDistance = biggestSize - Dimens.cameraSliderHandleArea;
return RotatedBox( return RotatedBox(
quarterTurns: widget.isVertical ? -1 : 0, quarterTurns: widget.isVertical ? -1 : 0,
child: GestureDetector( child: GestureDetector(
@ -60,11 +60,11 @@ class _CenteredSliderState extends State<CenteredSlider> {
handleDistance, handleDistance,
), ),
child: SizedBox( child: SizedBox(
height: Dimens.cameraSliderHandleSize, height: Dimens.cameraSliderHandleArea,
width: biggestSize, width: biggestSize,
child: _Slider( child: _Slider(
handleDistance: handleDistance, handleDistance: handleDistance,
handleSize: Dimens.cameraSliderHandleSize, handleSize: Dimens.cameraSliderHandleArea,
trackThickness: Dimens.cameraSliderTrackHeight, trackThickness: Dimens.cameraSliderTrackHeight,
value: relativeValue, value: relativeValue,
icon: RotatedBox( icon: RotatedBox(
@ -81,12 +81,12 @@ class _CenteredSliderState extends State<CenteredSlider> {
} }
void _updateHandlePosition(double offset, double handleDistance) { void _updateHandlePosition(double offset, double handleDistance) {
if (offset <= Dimens.cameraSliderHandleSize / 2) { if (offset <= Dimens.cameraSliderHandleArea / 2) {
relativeValue = 0; relativeValue = 0;
} else if (offset >= handleDistance + Dimens.cameraSliderHandleSize / 2) { } else if (offset >= handleDistance + Dimens.cameraSliderHandleArea / 2) {
relativeValue = 1; relativeValue = 1;
} else { } else {
relativeValue = (offset - Dimens.cameraSliderHandleSize / 2) / handleDistance; relativeValue = (offset - Dimens.cameraSliderHandleArea / 2) / handleDistance;
} }
setState(() {}); setState(() {});
widget.onChanged(relativeValue * (widget.max - widget.min) + widget.min); widget.onChanged(relativeValue * (widget.max - widget.min) + widget.min);
@ -124,7 +124,7 @@ class _Slider extends StatelessWidget {
), ),
), ),
AnimatedPositioned.fromRect( AnimatedPositioned.fromRect(
duration: Dimens.durationM, duration: Dimens.durationS,
rect: Rect.fromCenter( rect: Rect.fromCenter(
center: Offset( center: Offset(
handleSize / 2 + handleDistance * value, handleSize / 2 + handleDistance * value,
@ -133,15 +133,17 @@ class _Slider extends StatelessWidget {
width: handleSize, width: handleSize,
height: handleSize, height: handleSize,
), ),
child: _Handle( child: Center(
color: Theme.of(context).colorScheme.primary, child: _Handle(
size: handleSize, color: Theme.of(context).colorScheme.primary,
child: IconTheme( size: Dimens.cameraSliderHandleSize,
data: Theme.of(context).iconTheme.copyWith( child: IconTheme(
color: Theme.of(context).colorScheme.onPrimary, data: Theme.of(context).iconTheme.copyWith(
size: Dimens.cameraSliderHandleIconSize, color: Theme.of(context).colorScheme.onPrimary,
), size: Dimens.cameraSliderHandleIconSize,
child: icon, ),
child: icon,
),
), ),
), ),
), ),
@ -163,15 +165,15 @@ class _Handle extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ClipRRect( return SizedBox(
borderRadius: BorderRadius.circular(size / 2), height: size,
child: SizedBox( width: size,
height: size, child: DecoratedBox(
width: size, decoration: BoxDecoration(
child: ColoredBox(
color: color, color: color,
child: child, shape: BoxShape.circle,
), ),
child: child,
), ),
); );
} }

View file

@ -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<double> 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(),
);
},
);
}
}

View file

@ -0,0 +1,3 @@
extension DoubleToZoom on double {
String toZoom() => 'x${toStringAsFixed(2)}';
}