ML-48 Allow users to hide fastest/shortest exposure pairs #48 (#49)

* implemented `MeteringScreenLayoutProvider`

* refined topbar height difference calculation

* implemented `MeteringScreenLayoutFeaturesDialog`

* added icons to all dialogs

* save & restore `MeteringScreenLayoutConfig`

* reset film on film picker disabling

* fixed Fomapan reciprocity

* fixed dependencies

* added translations
This commit is contained in:
Vadim 2023-04-05 22:15:11 +03:00 committed by GitHub
parent be0617a99c
commit aaadd1ded6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 376 additions and 83 deletions

View file

@ -18,6 +18,7 @@ import 'environment.dart';
import 'generated/l10n.dart';
import 'providers/equipment_profile_provider.dart';
import 'providers/ev_source_type_provider.dart';
import 'providers/metering_screen_layout_provider.dart';
import 'providers/theme_provider.dart';
import 'screens/metering/flow_metering.dart';
import 'screens/settings/flow_settings.dart';
@ -47,31 +48,33 @@ class Application extends StatelessWidget {
Provider(create: (_) => PermissionsService()),
Provider(create: (_) => const LightSensorService()),
],
child: StopTypeProvider(
child: EquipmentProfileProvider(
child: EvSourceTypeProvider(
child: SupportedLocaleProvider(
child: ThemeProvider(
builder: (context, _) => _AnnotatedRegionWrapper(
child: MaterialApp(
theme: context.watch<ThemeData>(),
locale: Locale(context.watch<SupportedLocale>().intlName),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
builder: (context, child) => MediaQuery(
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
child: child!,
child: MeteringScreenLayoutProvider(
child: StopTypeProvider(
child: EquipmentProfileProvider(
child: EvSourceTypeProvider(
child: SupportedLocaleProvider(
child: ThemeProvider(
builder: (context, _) => _AnnotatedRegionWrapper(
child: MaterialApp(
theme: context.watch<ThemeData>(),
locale: Locale(context.watch<SupportedLocale>().intlName),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
builder: (context, child) => MediaQuery(
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
child: child!,
),
initialRoute: "metering",
routes: {
"metering": (context) => const MeteringFlow(),
"settings": (context) => const SettingsFlow(),
},
),
initialRoute: "metering",
routes: {
"metering": (context) => const MeteringFlow(),
"settings": (context) => const SettingsFlow(),
},
),
),
),

View file

@ -124,6 +124,9 @@ class FomapanFilm extends Film {
b = 5.75,
c = 1.5,
super('Fomapan ACTION 400', 400);
@override
double reciprocityFormula(double t) => t * log10polynomian(t, a, b, c);
}
class IlfordFilm extends Film {

View file

@ -0,0 +1,10 @@
enum MeteringScreenLayoutFeature { extremeExposurePairs, filmPicker }
typedef MeteringScreenLayoutConfig = Map<MeteringScreenLayoutFeature, bool>;
extension MeteringScreenLayoutConfigJson on MeteringScreenLayoutConfig {
static MeteringScreenLayoutConfig fromJson(Map<String, dynamic> data) => data.map(
(key, value) => MapEntry(MeteringScreenLayoutFeature.values[int.parse(key)], value as bool));
Map<String, dynamic> toJson() => map((key, value) => MapEntry(key.index.toString(), value));
}

View file

@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/supported_locale.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
@ -5,6 +7,7 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'models/ev_source_type.dart';
import 'models/film.dart';
import 'models/metering_screen_layout_config.dart';
import 'models/theme_type.dart';
class UserPreferencesService {
@ -14,6 +17,7 @@ class UserPreferencesService {
static const _evSourceTypeKey = "evSourceType";
static const _cameraEvCalibrationKey = "cameraEvCalibration";
static const _lightSensorEvCalibrationKey = "lightSensorEvCalibration";
static const _meteringScreenLayoutKey = "meteringScreenLayout";
static const _filmKey = "film";
static const _caffeineKey = "caffeine";
@ -33,15 +37,15 @@ class UserPreferencesService {
Future<void> _migrateOldKeys() async {
final legacyIsoIndex = _sharedPreferences.getInt("curIsoIndex");
if (legacyIsoIndex != null) {
iso = isoValues[legacyIsoIndex];
iso = IsoValue.values[legacyIsoIndex];
await _sharedPreferences.remove("curIsoIndex");
}
final legacyNdIndex = _sharedPreferences.getInt("curndIndex");
if (legacyNdIndex != null) {
/// Legacy ND list has 1 extra value at the end, so this check is needed
if (legacyNdIndex < ndValues.length) {
ndFilter = ndValues[legacyNdIndex];
if (legacyNdIndex < NdValue.values.length) {
ndFilter = NdValue.values[legacyNdIndex];
}
await _sharedPreferences.remove("curndIndex");
}
@ -66,11 +70,11 @@ class UserPreferencesService {
}
IsoValue get iso =>
isoValues.firstWhere((v) => v.value == (_sharedPreferences.getInt(_isoKey) ?? 100));
IsoValue.values.firstWhere((v) => v.value == (_sharedPreferences.getInt(_isoKey) ?? 100));
set iso(IsoValue value) => _sharedPreferences.setInt(_isoKey, value.value);
NdValue get ndFilter =>
ndValues.firstWhere((v) => v.value == (_sharedPreferences.getInt(_ndFilterKey) ?? 0));
NdValue.values.firstWhere((v) => v.value == (_sharedPreferences.getInt(_ndFilterKey) ?? 0));
set ndFilter(NdValue value) => _sharedPreferences.setInt(_ndFilterKey, value.value);
EvSourceType get evSourceType =>
@ -80,6 +84,21 @@ class UserPreferencesService {
bool get caffeine => _sharedPreferences.getBool(_caffeineKey) ?? false;
set caffeine(bool value) => _sharedPreferences.setBool(_caffeineKey, value);
MeteringScreenLayoutConfig get meteringScreenLayout {
final configJson = _sharedPreferences.getString(_meteringScreenLayoutKey);
if (configJson != null) {
return MeteringScreenLayoutConfigJson.fromJson(json.decode(configJson));
} else {
return {
MeteringScreenLayoutFeature.extremeExposurePairs: true,
MeteringScreenLayoutFeature.filmPicker: true,
};
}
}
set meteringScreenLayout(MeteringScreenLayoutConfig value) =>
_sharedPreferences.setString(_meteringScreenLayoutKey, json.encode(value.toJson()));
bool get haptics => _sharedPreferences.getBool(_hapticsKey) ?? true;
set haptics(bool value) => _sharedPreferences.setBool(_hapticsKey, value);

View file

@ -34,6 +34,10 @@
"calibrationMessageCameraOnly": "The accuracy of the readings measured by this application depends entirely on the rear camera of the device. Therefore, consider testing this application and setting up an EV calibration value that will give you the desired measurement results.",
"camera": "Camera",
"lightSensor": "Light sensor",
"meteringScreenLayout": "Metering screen layout",
"meteringScreenLayoutHint": "Hide elements on the metering screen that you don't need so that they don't waste exposure pairs list space.",
"meteringScreenFeatureExtremeExposurePairs": "Fastest & shortest exposure pairs",
"meteringScreenFeatureFilmPicker": "Film picker",
"film": "Film",
"equipment": "Equipment",
"equipmentProfileName": "Equipment profile name",

View file

@ -34,6 +34,10 @@
"calibrationMessageCameraOnly": "La précision des lectures mesurées par cette application dépend entièrement de la caméra arrière de l'appareil. Par conséquent, envisagez de tester cette application et de configurer une valeur d'étalonnage EV qui vous donnera les résultats de mesure souhaités.",
"camera": "Caméra",
"lightSensor": "Capteur de lumière",
"meteringScreenLayout": "Disposition de l'écran de mesure",
"meteringScreenLayoutHint": "Masquer les éléments sur l'écran de mesure dont vous n'avez pas besoin pour qu'ils ne gaspillent pas de l'espace dans les paires d'exposition.",
"meteringScreenFeatureExtremeExposurePairs": "Paires d'exposition les plus rapides et les plus courtes",
"meteringScreenFeatureFilmPicker": "Sélecteur de film",
"film": "Pellicule",
"equipment": "Équipement",
"equipmentProfileName": "Nom du profil de l'équipement",

View file

@ -34,6 +34,10 @@
"calibrationMessageCameraOnly": "Точность измерений данного приложения полностью зависит от точности камеры вашего устройства. Поэтому рекомендуется самостоятельно подобрать калибровочное значение, которое даст желаемый результат измерений.",
"camera": "Камера",
"lightSensor": "Датчик освещённости",
"meteringScreenLayout": "Элементы главного экрана",
"meteringScreenLayoutHint": "Здесь вы можете скрыть некоторые ненужные или неиспользуемые элементы с главного экрана.",
"meteringScreenFeatureExtremeExposurePairs": "Длинная и короткая выдержки",
"meteringScreenFeatureFilmPicker": "Выбор пленки",
"film": "Пленка",
"equipment": "Оборудование",
"equipmentProfileName": "Название профиля",

View file

@ -21,10 +21,10 @@ class EquipmentProfileProviderState extends State<EquipmentProfileProvider> {
static const EquipmentProfileData _defaultProfile = EquipmentProfileData(
id: '',
name: '',
apertureValues: apertureValues,
ndValues: ndValues,
shutterSpeedValues: shutterSpeedValues,
isoValues: isoValues,
apertureValues: ApertureValue.values,
ndValues: NdValue.values,
shutterSpeedValues: ShutterSpeedValue.values,
isoValues: IsoValue.values,
);
List<EquipmentProfileData> _customProfiles = [];
@ -68,10 +68,10 @@ class EquipmentProfileProviderState extends State<EquipmentProfileProvider> {
_customProfiles.add(EquipmentProfileData(
id: const Uuid().v1(),
name: name,
apertureValues: apertureValues,
ndValues: ndValues,
shutterSpeedValues: shutterSpeedValues,
isoValues: isoValues,
apertureValues: ApertureValue.values,
ndValues: NdValue.values,
shutterSpeedValues: ShutterSpeedValue.values,
isoValues: IsoValue.values,
));
_refreshSavedProfiles();
}

View file

@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:provider/provider.dart';
class MeteringScreenLayoutProvider extends StatefulWidget {
final Widget child;
const MeteringScreenLayoutProvider({required this.child, super.key});
static MeteringScreenLayoutProviderState of(BuildContext context) {
return context.findAncestorStateOfType<MeteringScreenLayoutProviderState>()!;
}
@override
State<MeteringScreenLayoutProvider> createState() => MeteringScreenLayoutProviderState();
}
class MeteringScreenLayoutProviderState extends State<MeteringScreenLayoutProvider> {
late final MeteringScreenLayoutConfig _config =
context.read<UserPreferencesService>().meteringScreenLayout;
@override
Widget build(BuildContext context) {
return MeteringScreenLayout(
config: MeteringScreenLayoutConfig.from(_config),
child: widget.child,
);
}
void updateFeatures(MeteringScreenLayoutConfig config) {
setState(() {
config.forEach((key, value) {
_config.update(
key,
(_) => value,
ifAbsent: () => value,
);
});
});
context.read<UserPreferencesService>().meteringScreenLayout = _config;
}
}
class MeteringScreenLayout extends InheritedModel<MeteringScreenLayoutFeature> {
final MeteringScreenLayoutConfig config;
const MeteringScreenLayout({
required this.config,
required super.child,
super.key,
});
static MeteringScreenLayoutConfig of(BuildContext context, {bool listen = true}) {
if (listen) {
return context.dependOnInheritedWidgetOfExactType<MeteringScreenLayout>()!.config;
} else {
return context.findAncestorWidgetOfExactType<MeteringScreenLayout>()!.config;
}
}
static bool featureStatusOf(BuildContext context, MeteringScreenLayoutFeature feature) {
return InheritedModel.inheritFrom<MeteringScreenLayout>(context, aspect: feature)!
.config[feature]!;
}
@override
bool updateShouldNotify(MeteringScreenLayout oldWidget) => true;
@override
bool updateShouldNotifyDependent(
MeteringScreenLayout oldWidget,
Set<MeteringScreenLayoutFeature> dependencies,
) {
for (final dependecy in dependencies) {
if (oldWidget.config[dependecy] != config[dependecy]) {
return true;
}
}
return false;
}
}

View file

@ -30,9 +30,8 @@ class Dimens {
static const double disabledOpacity = 0.38;
// TopBar
/// Probably this is a bad practice, but with text size locked, the height is always 212
static const double readingContainerDoubleValueHeight = 128;
static const double readingContainerSingleValueHeight = 76;
static const double readingContainerDefaultHeight = 288;
// `CenteredSlider`
static const double cameraSliderTrackHeight = grid4;

View file

@ -106,7 +106,7 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
void _onFilmChanged(FilmChangedEvent event, Emitter emit) {
if (_iso.value != event.data.iso) {
final newIso = isoValues.firstWhere(
final newIso = IsoValue.values.firstWhere(
(e) => e.value == event.data.iso,
orElse: () => _iso,
);
@ -175,7 +175,7 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
const ShutterSpeedValue anchorShutterSpeed = ShutterSpeedValue(1, false, StopType.full);
int anchorIndex = _shutterSpeedValues.indexOf(anchorShutterSpeed);
if (anchorIndex < 0) {
final filteredFullList = shutterSpeedValues.whereStopType(stopType);
final filteredFullList = ShutterSpeedValue.values.whereStopType(stopType);
final customListStartIndex = filteredFullList.indexOf(_shutterSpeedValues.first);
final fullListAnchor = filteredFullList.indexOf(anchorShutterSpeed);
if (customListStartIndex < fullListAnchor) {

View file

@ -2,8 +2,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/data/models/film.dart';
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
import 'package:lightmeter/features.dart';
import 'package:lightmeter/platform_config.dart';
import 'package:lightmeter/providers/metering_screen_layout_provider.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/metering/components/camera_container/components/camera_view/widget_camera_view.dart';
import 'package:lightmeter/screens/metering/components/camera_container/models/camera_error_type.dart';
@ -49,11 +51,26 @@ class CameraContainer extends StatelessWidget {
((MediaQuery.of(context).size.width - Dimens.grid8 - 2 * Dimens.paddingM) / 2) /
PlatformConfig.cameraPreviewAspectRatio;
double topBarOverflow = Dimens.readingContainerDefaultHeight - cameraViewHeight;
double topBarOverflow = Dimens.readingContainerSingleValueHeight + // ISO & ND
-cameraViewHeight;
if (FeaturesConfig.equipmentProfilesEnabled) {
topBarOverflow += Dimens.readingContainerSingleValueHeight;
topBarOverflow += Dimens.paddingS;
}
if (MeteringScreenLayout.featureStatusOf(
context,
MeteringScreenLayoutFeature.extremeExposurePairs,
)) {
topBarOverflow += Dimens.readingContainerDoubleValueHeight;
topBarOverflow += Dimens.paddingS;
}
if (MeteringScreenLayout.featureStatusOf(
context,
MeteringScreenLayoutFeature.filmPicker,
)) {
topBarOverflow += Dimens.readingContainerSingleValueHeight;
topBarOverflow += Dimens.paddingS;
}
return Column(
children: [

View file

@ -6,6 +6,7 @@ typedef DialogPickerItemTitleBuilder<T> = Widget Function(BuildContext context,
typedef DialogPickerItemTrailingBuilder<T> = Widget? Function(T selected, T value);
class DialogPicker<T> extends StatefulWidget {
final IconData icon;
final String title;
final String? subtitle;
final T initialValue;
@ -16,6 +17,7 @@ class DialogPicker<T> extends StatefulWidget {
final ValueChanged onSelect;
const DialogPicker({
required this.icon,
required this.title,
this.subtitle,
required this.initialValue,
@ -47,25 +49,36 @@ class _DialogPickerState<T> extends State<DialogPicker<T>> {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: Dimens.dialogTitlePadding,
child: Column(
children: [
Text(
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: Dimens.dialogTitlePadding,
child: Icon(widget.icon),
),
Padding(
padding: Dimens.dialogIconTitlePadding,
child: Text(
widget.title,
style: Theme.of(context).textTheme.headlineSmall!,
textAlign: TextAlign.center,
),
if (widget.subtitle != null) ...[
const SizedBox(height: Dimens.grid16),
Text(
),
if (widget.subtitle != null)
Padding(
padding: const EdgeInsets.fromLTRB(
Dimens.paddingL,
0,
Dimens.paddingL,
Dimens.paddingM,
),
child: Text(
widget.subtitle!,
style: Theme.of(context).textTheme.bodyMedium!,
textAlign: TextAlign.center,
),
]
],
),
),
],
),
const Divider(),
Expanded(

View file

@ -5,6 +5,7 @@ import 'components/dialog_picker/widget_picker_dialog.dart';
class AnimatedDialogPicker<T> extends StatelessWidget {
final _key = GlobalKey<AnimatedDialogState>();
final IconData icon;
final String title;
final String? subtitle;
final T selectedValue;
@ -15,6 +16,7 @@ class AnimatedDialogPicker<T> extends StatelessWidget {
final Widget closedChild;
AnimatedDialogPicker({
required this.icon,
required this.title,
this.subtitle,
required this.selectedValue,
@ -32,6 +34,7 @@ class AnimatedDialogPicker<T> extends StatelessWidget {
key: _key,
closedChild: closedChild,
openedChild: DialogPicker<T>(
icon: icon,
title: title,
subtitle: subtitle,
initialValue: selectedValue,

View file

@ -1,9 +1,11 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/data/models/film.dart';
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
import 'package:lightmeter/features.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/providers/equipment_profile_provider.dart';
import 'package:lightmeter/providers/metering_screen_layout_provider.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
@ -42,25 +44,35 @@ class ReadingsContainer extends StatelessWidget {
const _EquipmentProfilePicker(),
const _InnerPadding(),
],
ReadingValueContainer(
values: [
ReadingValue(
label: S.of(context).fastestExposurePair,
value: fastest != null ? fastest!.toString() : '-',
),
ReadingValue(
label: S.of(context).slowestExposurePair,
value: fastest != null ? slowest!.toString() : '-',
),
],
),
const _InnerPadding(),
_FilmPicker(
values: Film.values,
selectedValue: film,
onChanged: onFilmChanged,
),
const _InnerPadding(),
if (MeteringScreenLayout.featureStatusOf(
context,
MeteringScreenLayoutFeature.extremeExposurePairs,
)) ...[
ReadingValueContainer(
values: [
ReadingValue(
label: S.of(context).fastestExposurePair,
value: fastest != null ? fastest!.toString() : '-',
),
ReadingValue(
label: S.of(context).slowestExposurePair,
value: fastest != null ? slowest!.toString() : '-',
),
],
),
const _InnerPadding(),
],
if (MeteringScreenLayout.featureStatusOf(
context,
MeteringScreenLayoutFeature.filmPicker,
)) ...[
_FilmPicker(
values: Film.values,
selectedValue: film,
onChanged: onFilmChanged,
),
const _InnerPadding(),
],
Row(
children: [
Expanded(
@ -95,6 +107,7 @@ class _EquipmentProfilePicker extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AnimatedDialogPicker<EquipmentProfileData>(
icon: Icons.camera,
title: S.of(context).equipmentProfile,
selectedValue: EquipmentProfile.of(context),
values: EquipmentProfiles.of(context),
@ -126,6 +139,7 @@ class _FilmPicker extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AnimatedDialogPicker<Film>(
icon: Icons.camera_roll,
title: S.of(context).film,
selectedValue: selectedValue,
values: values,
@ -155,6 +169,7 @@ class _IsoValuePicker extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AnimatedDialogPicker<IsoValue>(
icon: Icons.iso,
title: S.of(context).iso,
subtitle: S.of(context).filmSpeed,
selectedValue: selectedValue,
@ -189,6 +204,7 @@ class _NdValuePicker extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AnimatedDialogPicker<NdValue>(
icon: Icons.filter_b_and_w,
title: S.of(context).nd,
subtitle: S.of(context).ndFilterFactor,
selectedValue: selectedValue,

View file

@ -3,9 +3,11 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/data/models/ev_source_type.dart';
import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/data/models/film.dart';
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
import 'package:lightmeter/environment.dart';
import 'package:lightmeter/providers/equipment_profile_provider.dart';
import 'package:lightmeter/providers/ev_source_type_provider.dart';
import 'package:lightmeter/providers/metering_screen_layout_provider.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
import 'components/bottom_controls/provider_bottom_controls.dart';
@ -30,6 +32,9 @@ class _MeteringScreenState extends State<MeteringScreen> {
super.didChangeDependencies();
_bloc.add(EquipmentProfileChangedEvent(EquipmentProfile.of(context)));
_bloc.add(StopTypeChangedEvent(context.watch<StopType>()));
if (!MeteringScreenLayout.featureStatusOf(context, MeteringScreenLayoutFeature.filmPicker)) {
_bloc.add(const FilmChangedEvent(Film.other()));
}
}
@override

View file

@ -18,6 +18,7 @@ class LanguageListTile extends StatelessWidget {
showDialog<SupportedLocale>(
context: context,
builder: (_) => DialogPicker<SupportedLocale>(
icon: Icons.language,
title: S.of(context).chooseLanguage,
selectedValue: context.read<SupportedLocale>(),
values: SupportedLocale.values,

View file

@ -17,7 +17,8 @@ class CalibrationDialog extends StatelessWidget {
Widget build(BuildContext context) {
final bool hasLightSensor = context.read<Environment>().hasLightSensor;
return AlertDialog(
titlePadding: Dimens.dialogTitlePadding,
icon: const Icon(Icons.settings_brightness),
titlePadding: Dimens.dialogIconTitlePadding,
title: Text(S.of(context).calibration),
contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingL),
content: SingleChildScrollView(

View file

@ -35,8 +35,8 @@ class EquipmentListTiles extends StatelessWidget {
icon: Icons.iso,
title: S.of(context).isoValues,
description: S.of(context).isoValuesFilterDescription,
values: isoValues,
valuesCount: selectedIsoValues.length == isoValues.length
values: IsoValue.values,
valuesCount: selectedIsoValues.length == IsoValue.values.length
? S.of(context).equipmentProfileAllValues
: selectedIsoValues.length.toString(),
selectedValues: selectedIsoValues,
@ -47,8 +47,8 @@ class EquipmentListTiles extends StatelessWidget {
icon: Icons.filter_b_and_w,
title: S.of(context).ndFilters,
description: S.of(context).ndFiltersFilterDescription,
values: ndValues,
valuesCount: selectedNdValues.length == ndValues.length
values: NdValue.values,
valuesCount: selectedNdValues.length == NdValue.values.length
? S.of(context).equipmentProfileAllValues
: selectedNdValues.length.toString(),
selectedValues: selectedNdValues,
@ -59,8 +59,8 @@ class EquipmentListTiles extends StatelessWidget {
icon: Icons.camera,
title: S.of(context).apertureValues,
description: S.of(context).apertureValuesFilterDescription,
values: apertureValues,
valuesCount: selectedApertureValues.length == apertureValues.length
values: ApertureValue.values,
valuesCount: selectedApertureValues.length == ApertureValue.values.length
? S.of(context).equipmentProfileAllValues
: selectedApertureValues.length.toString(),
selectedValues: selectedApertureValues,
@ -71,8 +71,8 @@ class EquipmentListTiles extends StatelessWidget {
icon: Icons.shutter_speed,
title: S.of(context).shutterSpeedValues,
description: S.of(context).shutterSpeedValuesFilterDescription,
values: shutterSpeedValues,
valuesCount: selectedShutterSpeedValues.length == shutterSpeedValues.length
values: ShutterSpeedValue.values,
valuesCount: selectedShutterSpeedValues.length == ShutterSpeedValue.values.length
? S.of(context).equipmentProfileAllValues
: selectedShutterSpeedValues.length.toString(),
selectedValues: selectedShutterSpeedValues,

View file

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/dimens.dart';
class EquipmentProfileNameDialog extends StatefulWidget {
final String initialValue;
@ -22,6 +23,8 @@ class _EquipmentProfileNameDialogState extends State<EquipmentProfileNameDialog>
@override
Widget build(BuildContext context) {
return AlertDialog(
icon: const Icon(Icons.edit),
titlePadding: Dimens.dialogIconTitlePadding,
title: Text(S.of(context).equipmentProfileName),
content: TextField(
autofocus: true,

View file

@ -18,6 +18,7 @@ class StopTypeListTile extends StatelessWidget {
showDialog<StopType>(
context: context,
builder: (_) => DialogPicker<StopType>(
icon: Icons.straighten,
title: S.of(context).showFractionalStops,
selectedValue: context.read<StopType>(),
values: StopType.values,

View file

@ -0,0 +1,76 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/providers/metering_screen_layout_provider.dart';
import 'package:lightmeter/res/dimens.dart';
class MeteringScreenLayoutFeaturesDialog extends StatefulWidget {
const MeteringScreenLayoutFeaturesDialog({super.key});
@override
State<MeteringScreenLayoutFeaturesDialog> createState() =>
_MeteringScreenLayoutFeaturesDialogState();
}
class _MeteringScreenLayoutFeaturesDialogState extends State<MeteringScreenLayoutFeaturesDialog> {
late final _features =
MeteringScreenLayoutConfig.from(MeteringScreenLayout.of(context, listen: false));
@override
Widget build(BuildContext context) {
return AlertDialog(
icon: const Icon(Icons.layers_outlined),
titlePadding: Dimens.dialogIconTitlePadding,
title: Text(S.of(context).meteringScreenLayout),
contentPadding: EdgeInsets.zero,
content: SizedBox(
width: double.maxFinite,
child: ListView(
shrinkWrap: true,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingL),
child: Text(S.of(context).meteringScreenLayoutHint),
),
const SizedBox(height: Dimens.grid16),
...MeteringScreenLayoutFeature.values.map(
(f) => SwitchListTile(
contentPadding: EdgeInsets.symmetric(horizontal: Dimens.dialogTitlePadding.left),
title: Text(_toStringLocalized(context, f)),
value: _features[f]!,
onChanged: (value) {
setState(() {
_features.update(f, (_) => value);
});
},
),
),
],
),
),
actionsPadding: Dimens.dialogActionsPadding,
actions: [
TextButton(
onPressed: Navigator.of(context).pop,
child: Text(S.of(context).cancel),
),
TextButton(
onPressed: () {
MeteringScreenLayoutProvider.of(context).updateFeatures(_features);
Navigator.of(context).pop();
},
child: Text(S.of(context).save),
),
],
);
}
String _toStringLocalized(BuildContext context, MeteringScreenLayoutFeature feature) {
switch (feature) {
case MeteringScreenLayoutFeature.extremeExposurePairs:
return S.of(context).meteringScreenFeatureExtremeExposurePairs;
case MeteringScreenLayoutFeature.filmPicker:
return S.of(context).meteringScreenFeatureFilmPicker;
}
}
}

View file

@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'components/meterins_screen_layout_features_dialog/widget_dialog_metering_screen_layout_features.dart';
class MeteringScreenLayoutListTile extends StatelessWidget {
const MeteringScreenLayoutListTile({super.key});
@override
Widget build(BuildContext context) {
return ListTile(
leading: const Icon(Icons.layers_outlined),
title: Text(S.of(context).meteringScreenLayout),
onTap: () {
showDialog(
context: context,
builder: (_) => const MeteringScreenLayoutFeaturesDialog(),
);
},
);
}
}

View file

@ -4,6 +4,7 @@ import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/screens/settings/components/shared/settings_section/widget_settings_section.dart';
import 'components/calibration/widget_list_tile_calibration.dart';
import 'components/metering_screen_layout/widget_list_tile_metering_screen_layout.dart';
import 'components/equipment_profiles/widget_list_tile_equipment_profiles.dart';
import 'components/fractional_stops/widget_list_tile_fractional_stops.dart';
@ -17,6 +18,7 @@ class MeteringSettingsSection extends StatelessWidget {
children: const [
StopTypeListTile(),
CalibrationListTile(),
MeteringScreenLayoutListTile(),
if (FeaturesConfig.equipmentProfilesEnabled) EquipmentProfilesListTile(),
],
);

View file

@ -3,12 +3,14 @@ import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/dimens.dart';
class DialogPicker<T> extends StatefulWidget {
final IconData icon;
final String title;
final T selectedValue;
final List<T> values;
final String Function(BuildContext context, T value) titleAdapter;
const DialogPicker({
required this.icon,
required this.title,
required this.selectedValue,
required this.values,
@ -26,7 +28,8 @@ class _DialogPickerState<T> extends State<DialogPicker<T>> {
@override
Widget build(BuildContext context) {
return AlertDialog(
titlePadding: Dimens.dialogTitlePadding,
icon: Icon(widget.icon),
titlePadding: Dimens.dialogIconTitlePadding,
title: Text(widget.title),
contentPadding: EdgeInsets.zero,
content: Column(

View file

@ -24,7 +24,8 @@ class _PrimaryColorDialogPickerState extends State<PrimaryColorDialogPicker> {
@override
Widget build(BuildContext context) {
return AlertDialog(
titlePadding: Dimens.dialogTitlePadding,
icon: const Icon(Icons.palette),
titlePadding: Dimens.dialogIconTitlePadding,
title: Text(S.of(context).choosePrimaryColor),
content: SizedBox(
height: Dimens.grid48,

View file

@ -18,6 +18,7 @@ class ThemeTypeListTile extends StatelessWidget {
showDialog<ThemeType>(
context: context,
builder: (_) => DialogPicker<ThemeType>(
icon: Icons.brightness_6,
title: S.of(context).chooseTheme,
selectedValue: context.read<ThemeType>(),
values: ThemeType.values,