mirror of
https://github.com/vodemn/m3_lightmeter.git
synced 2024-12-04 13:20:39 +00:00
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:
parent
bfd0bfe531
commit
2117df2f11
22 changed files with 548 additions and 270 deletions
13
.vscode/launch.json
vendored
13
.vscode/launch.json
vendored
|
@ -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",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -60,6 +60,8 @@
|
||||||
"shutterSpeedValuesFilterDescription": "Выберите диапазон значений выдержки. Обычно ограничивается возможностями вашей камеры.",
|
"shutterSpeedValuesFilterDescription": "Выберите диапазон значений выдержки. Обычно ограничивается возможностями вашей камеры.",
|
||||||
"isoValues": "Значения ISO",
|
"isoValues": "Значения ISO",
|
||||||
"isoValuesFilterDescription": "Выберите значения ISO для отображения. Это может быть наиболее часто используемые значения или значения, поддерживаемые вашей камерой.",
|
"isoValuesFilterDescription": "Выберите значения ISO для отображения. Это может быть наиболее часто используемые значения или значения, поддерживаемые вашей камерой.",
|
||||||
|
"lensZoom": "Зум объектива",
|
||||||
|
"lensZoomDescription": "Установите уровень зума относительно камеры телефона, чтобы он соответствовал видоискателю вашей камеры.",
|
||||||
"equipmentProfile": "Оборудование",
|
"equipmentProfile": "Оборудование",
|
||||||
"equipmentProfiles": "Профили оборудования",
|
"equipmentProfiles": "Профили оборудования",
|
||||||
"tapToAdd": "Нажмите, чтобы добавить",
|
"tapToAdd": "Нажмите, чтобы добавить",
|
||||||
|
|
|
@ -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": "点击添加",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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),
|
||||||
|
|
|
@ -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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
121
lib/screens/shared/ruler_slider/widget_slider_ruler.dart
Normal file
121
lib/screens/shared/ruler_slider/widget_slider_ruler.dart
Normal 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(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
3
lib/utils/double_to_zoom.dart
Normal file
3
lib/utils/double_to_zoom.dart
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
extension DoubleToZoom on double {
|
||||||
|
String toZoom() => 'x${toStringAsFixed(2)}';
|
||||||
|
}
|
Loading…
Reference in a new issue