From 5adcee00dd8b93af5c5f96315945259bd023dddc Mon Sep 17 00:00:00 2001 From: Vadim <44135514+vodemn@users.noreply.github.com> Date: Mon, 14 Aug 2023 12:25:37 +0200 Subject: [PATCH 01/23] ML-105 Hide providers from the widget tree (#106) * Added `ServiceProviders` widget * Added `EnumProviders` widget for enum values * Moved `ThemeProvider` functionality to `EnumProviders` * Style * `EnumProviders` -> `UserPreferencesProvider` * `ServiceProviders` -> `ServiceProvider` * Moved `MeteringScreenLayoutProvider` functionality to `UserPreferencesProvider` * typo * Removed `InheritedModelAspectListener` * TODO * Removed Inherited Generics * Removed redundant `LightmeterProviders` * Removed redundant methods from `ServicesProvider` * `_inheritFrom` -> `_inheritFromEnumsModel` * Fixed `MeteringScreenLayoutConfig` updates * Separated `_ThemeModel` * typo * `_EnumsModel` -> `_UserPreferencesModel` --- lib/application.dart | 124 +++++--- lib/providers.dart | 77 ----- lib/providers/equipment_profile_provider.dart | 69 +++- lib/providers/ev_source_type_provider.dart | 63 ---- .../metering_screen_layout_provider.dart | 60 ---- lib/providers/services_provider.dart | 36 +++ lib/providers/stop_type_provider.dart | 42 --- lib/providers/supported_locale_provider.dart | 53 ---- lib/providers/theme_provider.dart | 257 --------------- lib/providers/user_preferences_provider.dart | 295 ++++++++++++++++++ lib/res/theme.dart | 97 ++++++ .../widget_bottom_controls.dart | 4 +- .../camera_preview/widget_camera_preview.dart | 4 +- .../provider_container_camera.dart | 5 +- .../widget_container_camera.dart | 6 +- .../provider_container_light_sensor.dart | 5 +- .../widget_container_readings.dart | 19 +- lib/screens/metering/flow_metering.dart | 64 ++-- lib/screens/metering/screen_metering.dart | 25 +- .../listener_metering_layout_feature.dart | 52 +++ .../utils/listsner_equipment_profiles.dart | 30 ++ .../widget_list_tile_report_issue.dart | 5 +- .../widget_list_tile_source_code.dart | 5 +- .../widget_list_tile_write_email.dart | 5 +- .../caffeine/provider_list_tile_caffeine.dart | 5 +- .../haptics/provider_list_tile_haptics.dart | 5 +- .../language/widget_list_tile_language.dart | 9 +- .../provider_list_tile_volume_actions.dart | 5 +- .../provider_dialog_calibration.dart | 5 +- .../widget_dialog_calibration.dart | 5 +- .../widget_list_tile_calibration.dart | 7 +- .../screen_equipment_profile.dart | 19 +- .../widget_list_tile_fractional_stops.dart | 9 +- ...ialog_metering_screen_layout_features.dart | 6 +- .../widget_list_tile_dynamic_color.dart | 7 +- .../widget_dialog_picker_primary_color.dart | 6 +- .../widget_list_tile_primary_color.dart | 7 +- .../widget_list_tile_theme_type.dart | 9 +- .../theme/widget_settings_section_theme.dart | 4 +- lib/screens/settings/flow_settings.dart | 33 +- lib/screens/settings/screen_settings.dart | 7 +- lib/utils/inherited_generics.dart | 171 ---------- test/data/shared_prefs_service_test.dart | 6 +- 43 files changed, 790 insertions(+), 937 deletions(-) delete mode 100644 lib/providers.dart delete mode 100644 lib/providers/ev_source_type_provider.dart delete mode 100644 lib/providers/metering_screen_layout_provider.dart create mode 100644 lib/providers/services_provider.dart delete mode 100644 lib/providers/stop_type_provider.dart delete mode 100644 lib/providers/supported_locale_provider.dart delete mode 100644 lib/providers/theme_provider.dart create mode 100644 lib/providers/user_preferences_provider.dart create mode 100644 lib/res/theme.dart create mode 100644 lib/screens/metering/utils/listener_metering_layout_feature.dart create mode 100644 lib/screens/metering/utils/listsner_equipment_profiles.dart delete mode 100644 lib/utils/inherited_generics.dart diff --git a/lib/application.dart b/lib/application.dart index 3880a08..1249ed9 100644 --- a/lib/application.dart +++ b/lib/application.dart @@ -1,13 +1,22 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:lightmeter/data/caffeine_service.dart'; +import 'package:lightmeter/data/haptics_service.dart'; +import 'package:lightmeter/data/light_sensor_service.dart'; import 'package:lightmeter/data/models/supported_locale.dart'; +import 'package:lightmeter/data/permissions_service.dart'; +import 'package:lightmeter/data/shared_prefs_service.dart'; +import 'package:lightmeter/data/volume_events_service.dart'; import 'package:lightmeter/environment.dart'; import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/providers.dart'; +import 'package:lightmeter/providers/equipment_profile_provider.dart'; +import 'package:lightmeter/providers/services_provider.dart'; +import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/screens/metering/flow_metering.dart'; import 'package:lightmeter/screens/settings/flow_settings.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; +import 'package:platform/platform.dart'; +import 'package:shared_preferences/shared_preferences.dart'; class Application extends StatelessWidget { final Environment env; @@ -16,56 +25,71 @@ class Application extends StatelessWidget { @override Widget build(BuildContext context) { - return LightmeterProviders( - env: env, - builder: (context, ready) => ready - ? _AnnotatedRegionWrapper( - child: MaterialApp( - theme: context.listen(), - locale: Locale(context.listen().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!, + return FutureBuilder( + future: Future.wait([ + SharedPreferences.getInstance(), + const LightSensorService(LocalPlatform()).hasSensor(), + ]), + builder: (_, snapshot) { + if (snapshot.data != null) { + return ServicesProvider( + caffeineService: const CaffeineService(), + environment: env.copyWith(hasLightSensor: snapshot.data![1] as bool), + hapticsService: const HapticsService(), + lightSensorService: const LightSensorService(LocalPlatform()), + permissionsService: const PermissionsService(), + userPreferencesService: UserPreferencesService(snapshot.data![0] as SharedPreferences), + volumeEventsService: const VolumeEventsService(LocalPlatform()), + child: UserPreferencesProvider( + child: EquipmentProfileProvider( + child: Builder( + builder: (context) { + final theme = UserPreferencesProvider.themeOf(context); + final systemIconsBrightness = + ThemeData.estimateBrightnessForColor(theme.colorScheme.onSurface); + return AnnotatedRegion( + value: SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarBrightness: systemIconsBrightness == Brightness.light + ? Brightness.dark + : Brightness.light, + statusBarIconBrightness: systemIconsBrightness, + systemNavigationBarColor: Colors.transparent, + systemNavigationBarIconBrightness: systemIconsBrightness, + ), + child: MaterialApp( + theme: theme, + locale: Locale(UserPreferencesProvider.localeOf(context).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(), - }, ), - ) - : const SizedBox(), - ); - } -} - -class _AnnotatedRegionWrapper extends StatelessWidget { - final Widget child; - - const _AnnotatedRegionWrapper({required this.child}); - - @override - Widget build(BuildContext context) { - final systemIconsBrightness = ThemeData.estimateBrightnessForColor( - context.listen().colorScheme.onSurface, - ); - return AnnotatedRegion( - value: SystemUiOverlayStyle( - statusBarColor: Colors.transparent, - statusBarBrightness: - systemIconsBrightness == Brightness.light ? Brightness.dark : Brightness.light, - statusBarIconBrightness: systemIconsBrightness, - systemNavigationBarColor: Colors.transparent, - systemNavigationBarIconBrightness: systemIconsBrightness, - ), - child: child, + ), + ); + } else if (snapshot.error != null) { + return Center(child: Text(snapshot.error!.toString())); + } + + // TODO(@vodemn): maybe user splashscreen instead + return const SizedBox(); + }, ); } } diff --git a/lib/providers.dart b/lib/providers.dart deleted file mode 100644 index d7907ab..0000000 --- a/lib/providers.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lightmeter/data/caffeine_service.dart'; -import 'package:lightmeter/data/haptics_service.dart'; -import 'package:lightmeter/data/light_sensor_service.dart'; -import 'package:lightmeter/data/permissions_service.dart'; -import 'package:lightmeter/data/shared_prefs_service.dart'; -import 'package:lightmeter/data/volume_events_service.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:lightmeter/providers/stop_type_provider.dart'; -import 'package:lightmeter/providers/supported_locale_provider.dart'; -import 'package:lightmeter/providers/theme_provider.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; -import 'package:platform/platform.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -class LightmeterProviders extends StatelessWidget { - final Environment env; - final Widget Function(BuildContext context, bool ready) builder; - - const LightmeterProviders({required this.env, required this.builder, super.key}); - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: Future.wait([ - SharedPreferences.getInstance(), - const LightSensorService(LocalPlatform()).hasSensor(), - ]), - builder: (_, snapshot) { - if (snapshot.data != null) { - return InheritedWidgetBase( - data: env.copyWith(hasLightSensor: snapshot.data![1] as bool), - child: InheritedWidgetBase( - data: UserPreferencesService(snapshot.data![0] as SharedPreferences), - child: InheritedWidgetBase( - data: const LightSensorService(LocalPlatform()), - child: InheritedWidgetBase( - data: const CaffeineService(), - child: InheritedWidgetBase( - data: const HapticsService(), - child: InheritedWidgetBase( - data: const VolumeEventsService(LocalPlatform()), - child: InheritedWidgetBase( - data: const PermissionsService(), - child: MeteringScreenLayoutProvider( - child: StopTypeProvider( - child: EquipmentProfileProvider( - child: EvSourceTypeProvider( - child: SupportedLocaleProvider( - child: ThemeProvider( - child: Builder( - builder: (context) => builder(context, true), - ), - ), - ), - ), - ), - ), - ), - ), - ), - ), - ), - ), - ), - ); - } else if (snapshot.error != null) { - return Center(child: Text(snapshot.error!.toString())); - } - return builder(context, false); - }, - ); - } -} diff --git a/lib/providers/equipment_profile_provider.dart b/lib/providers/equipment_profile_provider.dart index 85c9377..c0294fa 100644 --- a/lib/providers/equipment_profile_provider.dart +++ b/lib/providers/equipment_profile_provider.dart @@ -1,11 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:lightmeter/data/shared_prefs_service.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; +import 'package:lightmeter/providers/services_provider.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:uuid/uuid.dart'; -typedef EquipmentProfiles = List; - +// TODO(@vodemn): This will be removed in #89 class EquipmentProfileProvider extends StatefulWidget { final Widget child; @@ -35,7 +33,8 @@ class EquipmentProfileProviderState extends State { EquipmentProfile get _selectedProfile => _customProfiles.firstWhere( (e) => e.id == _selectedId, orElse: () { - context.get().selectedEquipmentProfileId = _defaultProfile.id; + ServicesProvider.of(context).userPreferencesService.selectedEquipmentProfileId = + _defaultProfile.id; return _defaultProfile; }, ); @@ -43,18 +42,16 @@ class EquipmentProfileProviderState extends State { @override void initState() { super.initState(); - _selectedId = context.get().selectedEquipmentProfileId; - _customProfiles = context.get().equipmentProfiles; + _selectedId = ServicesProvider.of(context).userPreferencesService.selectedEquipmentProfileId; + _customProfiles = ServicesProvider.of(context).userPreferencesService.equipmentProfiles; } @override Widget build(BuildContext context) { - return InheritedWidgetBase>( - data: [_defaultProfile] + _customProfiles, - child: InheritedWidgetBase( - data: _selectedProfile, - child: widget.child, - ), + return EquipmentProfiles( + profiles: [_defaultProfile] + _customProfiles, + selected: _selectedProfile, + child: widget.child, ); } @@ -62,7 +59,8 @@ class EquipmentProfileProviderState extends State { setState(() { _selectedId = data.id; }); - context.get().selectedEquipmentProfileId = _selectedProfile.id; + ServicesProvider.of(context).userPreferencesService.selectedEquipmentProfileId = + _selectedProfile.id; } /// Creates a default equipment profile @@ -94,7 +92,48 @@ class EquipmentProfileProviderState extends State { } void _refreshSavedProfiles() { - context.get().equipmentProfiles = _customProfiles; + ServicesProvider.of(context).userPreferencesService.equipmentProfiles = _customProfiles; setState(() {}); } } + +// Copied from #89 +enum EquipmentProfilesAspect { list, selected } + +class EquipmentProfiles extends InheritedModel { + const EquipmentProfiles({ + super.key, + required this.profiles, + required this.selected, + required super.child, + }); + + final List profiles; + final EquipmentProfile selected; + + static List of(BuildContext context) { + return InheritedModel.inheritFrom( + context, + aspect: EquipmentProfilesAspect.list, + )! + .profiles; + } + + static EquipmentProfile selectedOf(BuildContext context) { + return InheritedModel.inheritFrom( + context, + aspect: EquipmentProfilesAspect.selected, + )! + .selected; + } + + @override + bool updateShouldNotify(EquipmentProfiles oldWidget) => false; + + @override + bool updateShouldNotifyDependent( + EquipmentProfiles oldWidget, + Set dependencies, + ) => + false; +} diff --git a/lib/providers/ev_source_type_provider.dart b/lib/providers/ev_source_type_provider.dart deleted file mode 100644 index aed873b..0000000 --- a/lib/providers/ev_source_type_provider.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lightmeter/data/models/ev_source_type.dart'; -import 'package:lightmeter/data/shared_prefs_service.dart'; -import 'package:lightmeter/environment.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; - -class EvSourceTypeProvider extends StatefulWidget { - final Widget child; - - const EvSourceTypeProvider({required this.child, super.key}); - - static EvSourceTypeProviderState of(BuildContext context) { - return context.findAncestorStateOfType()!; - } - - @override - State createState() => EvSourceTypeProviderState(); -} - -class EvSourceTypeProviderState extends State { - late final ValueNotifier valueListenable; - - @override - void initState() { - super.initState(); - final evSourceType = context.get().evSourceType; - valueListenable = ValueNotifier( - evSourceType == EvSourceType.sensor && !context.get().hasLightSensor - ? EvSourceType.camera - : evSourceType, - ); - } - - @override - void dispose() { - valueListenable.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: valueListenable, - builder: (_, value, child) => InheritedWidgetBase( - data: value, - child: child!, - ), - child: widget.child, - ); - } - - void toggleType() { - switch (valueListenable.value) { - case EvSourceType.camera: - if (context.get().hasLightSensor) { - valueListenable.value = EvSourceType.sensor; - } - case EvSourceType.sensor: - valueListenable.value = EvSourceType.camera; - } - context.get().evSourceType = valueListenable.value; - } -} diff --git a/lib/providers/metering_screen_layout_provider.dart b/lib/providers/metering_screen_layout_provider.dart deleted file mode 100644 index 405c1a7..0000000 --- a/lib/providers/metering_screen_layout_provider.dart +++ /dev/null @@ -1,60 +0,0 @@ -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:lightmeter/utils/inherited_generics.dart'; - -class MeteringScreenLayoutProvider extends StatefulWidget { - final Widget child; - - const MeteringScreenLayoutProvider({required this.child, super.key}); - - static MeteringScreenLayoutProviderState of(BuildContext context) { - return context.findAncestorStateOfType()!; - } - - @override - State createState() => MeteringScreenLayoutProviderState(); -} - -class MeteringScreenLayoutProviderState extends State { - late final MeteringScreenLayoutConfig _config = - context.get().meteringScreenLayout; - - @override - Widget build(BuildContext context) { - return InheritedModelBase( - data: MeteringScreenLayoutConfig.from(_config), - child: widget.child, - ); - } - - void updateFeatures(MeteringScreenLayoutConfig config) { - setState(() { - config.forEach((key, value) { - _config.update( - key, - (_) => value, - ifAbsent: () => value, - ); - }); - }); - context.get().meteringScreenLayout = _config; - } -} - -typedef _MeteringScreenLayoutModel = InheritedModelBase; - -extension MeteringScreenLayout on InheritedModelBase { - static MeteringScreenLayoutConfig of(BuildContext context, {bool listen = true}) { - if (listen) { - return context.dependOnInheritedWidgetOfExactType<_MeteringScreenLayoutModel>()!.data; - } else { - return context.findAncestorWidgetOfExactType<_MeteringScreenLayoutModel>()!.data; - } - } - - static bool featureOf(BuildContext context, MeteringScreenLayoutFeature aspect) { - return InheritedModel.inheritFrom<_MeteringScreenLayoutModel>(context, aspect: aspect)! - .data[aspect]!; - } -} diff --git a/lib/providers/services_provider.dart b/lib/providers/services_provider.dart new file mode 100644 index 0000000..c2c548f --- /dev/null +++ b/lib/providers/services_provider.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/data/caffeine_service.dart'; +import 'package:lightmeter/data/haptics_service.dart'; +import 'package:lightmeter/data/light_sensor_service.dart'; +import 'package:lightmeter/data/permissions_service.dart'; +import 'package:lightmeter/data/shared_prefs_service.dart'; +import 'package:lightmeter/data/volume_events_service.dart'; +import 'package:lightmeter/environment.dart'; + +class ServicesProvider extends InheritedWidget { + final CaffeineService caffeineService; + final Environment environment; + final HapticsService hapticsService; + final LightSensorService lightSensorService; + final PermissionsService permissionsService; + final UserPreferencesService userPreferencesService; + final VolumeEventsService volumeEventsService; + + const ServicesProvider({ + required this.caffeineService, + required this.environment, + required this.hapticsService, + required this.lightSensorService, + required this.permissionsService, + required this.userPreferencesService, + required this.volumeEventsService, + required super.child, + }); + + static ServicesProvider of(BuildContext context) { + return context.findAncestorWidgetOfExactType()!; + } + + @override + bool updateShouldNotify(ServicesProvider oldWidget) => false; +} diff --git a/lib/providers/stop_type_provider.dart b/lib/providers/stop_type_provider.dart deleted file mode 100644 index 3b20dd5..0000000 --- a/lib/providers/stop_type_provider.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lightmeter/data/shared_prefs_service.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; -import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; - -class StopTypeProvider extends StatefulWidget { - final Widget child; - - const StopTypeProvider({required this.child, super.key}); - - static StopTypeProviderState of(BuildContext context) { - return context.findAncestorStateOfType()!; - } - - @override - State createState() => StopTypeProviderState(); -} - -class StopTypeProviderState extends State { - late StopType _stopType; - - @override - void initState() { - super.initState(); - _stopType = context.get().stopType; - } - - @override - Widget build(BuildContext context) { - return InheritedWidgetBase( - data: _stopType, - child: widget.child, - ); - } - - void set(StopType type) { - setState(() { - _stopType = type; - }); - context.get().stopType = type; - } -} diff --git a/lib/providers/supported_locale_provider.dart b/lib/providers/supported_locale_provider.dart deleted file mode 100644 index caa7ced..0000000 --- a/lib/providers/supported_locale_provider.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lightmeter/data/models/supported_locale.dart'; -import 'package:lightmeter/data/shared_prefs_service.dart'; -import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; - -class SupportedLocaleProvider extends StatefulWidget { - final Widget child; - - const SupportedLocaleProvider({required this.child, super.key}); - - static SupportedLocaleProviderState of(BuildContext context) { - return context.findAncestorStateOfType()!; - } - - @override - State createState() => SupportedLocaleProviderState(); -} - -class SupportedLocaleProviderState extends State { - late final ValueNotifier valueListenable; - - @override - void initState() { - super.initState(); - valueListenable = ValueNotifier(context.get().locale); - } - - @override - void dispose() { - valueListenable.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: valueListenable, - builder: (_, value, child) => InheritedWidgetBase( - data: value, - child: child!, - ), - child: widget.child, - ); - } - - void setLocale(SupportedLocale locale) { - S.load(Locale(locale.intlName)).then((value) { - valueListenable.value = locale; - context.get().locale = locale; - }); - } -} diff --git a/lib/providers/theme_provider.dart b/lib/providers/theme_provider.dart deleted file mode 100644 index 9773df1..0000000 --- a/lib/providers/theme_provider.dart +++ /dev/null @@ -1,257 +0,0 @@ -import 'package:dynamic_color/dynamic_color.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:lightmeter/data/models/dynamic_colors_state.dart'; -import 'package:lightmeter/data/models/theme_type.dart'; -import 'package:lightmeter/data/shared_prefs_service.dart'; -import 'package:lightmeter/res/dimens.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; -import 'package:material_color_utilities/material_color_utilities.dart'; - -class ThemeProvider extends StatefulWidget { - final Widget child; - - const ThemeProvider({ - required this.child, - super.key, - }); - - static ThemeProviderState of(BuildContext context) { - return context.findAncestorStateOfType()!; - } - - static const primaryColorsList = [ - Color(0xfff44336), - Color(0xffe91e63), - Color(0xff9c27b0), - Color(0xff673ab7), - Color(0xff3f51b5), - Color(0xff2196f3), - Color(0xff03a9f4), - Color(0xff00bcd4), - Color(0xff009688), - Color(0xff4caf50), - Color(0xff8bc34a), - Color(0xffcddc39), - Color(0xffffeb3b), - Color(0xffffc107), - Color(0xffff9800), - Color(0xffff5722), - ]; - - @override - State createState() => ThemeProviderState(); -} - -class ThemeProviderState extends State with WidgetsBindingObserver { - UserPreferencesService get _prefs => context.get(); - - late final _themeTypeNotifier = ValueNotifier(_prefs.themeType); - late final _dynamicColorNotifier = ValueNotifier(_prefs.dynamicColor); - late final _primaryColorNotifier = ValueNotifier(_prefs.primaryColor); - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addObserver(this); - } - - @override - void didChangePlatformBrightness() { - super.didChangePlatformBrightness(); - setState(() {}); - } - - @override - void dispose() { - WidgetsBinding.instance.removeObserver(this); - _themeTypeNotifier.dispose(); - _dynamicColorNotifier.dispose(); - _primaryColorNotifier.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: _themeTypeNotifier, - builder: (_, themeType, __) => InheritedWidgetBase( - data: themeType, - child: ValueListenableBuilder( - valueListenable: _dynamicColorNotifier, - builder: (_, useDynamicColor, __) => _DynamicColorProvider( - useDynamicColor: useDynamicColor, - themeBrightness: _themeBrightness, - builder: (_, dynamicPrimaryColor) => ValueListenableBuilder( - valueListenable: _primaryColorNotifier, - builder: (_, primaryColor, __) => _ThemeDataProvider( - primaryColor: dynamicPrimaryColor ?? primaryColor, - brightness: _themeBrightness, - child: widget.child, - ), - ), - ), - ), - ), - ); - } - - void setThemeType(ThemeType themeType) { - _themeTypeNotifier.value = themeType; - _prefs.themeType = themeType; - } - - Brightness get _themeBrightness { - switch (_themeTypeNotifier.value) { - case ThemeType.light: - return Brightness.light; - case ThemeType.dark: - return Brightness.dark; - case ThemeType.systemDefault: - return SchedulerBinding.instance.platformDispatcher.platformBrightness; - } - } - - void setPrimaryColor(Color color) { - _primaryColorNotifier.value = color; - _prefs.primaryColor = color; - } - - void enableDynamicColor(bool enable) { - _dynamicColorNotifier.value = enable; - _prefs.dynamicColor = enable; - } -} - -class _DynamicColorProvider extends StatelessWidget { - final bool useDynamicColor; - final Brightness themeBrightness; - final Widget Function(BuildContext context, Color? primaryColor) builder; - - const _DynamicColorProvider({ - required this.useDynamicColor, - required this.themeBrightness, - required this.builder, - }); - - @override - Widget build(BuildContext context) { - return DynamicColorBuilder( - builder: (lightDynamic, darkDynamic) { - late final DynamicColorState state; - late final Color? dynamicPrimaryColor; - if (lightDynamic != null && darkDynamic != null) { - if (useDynamicColor) { - dynamicPrimaryColor = - (themeBrightness == Brightness.light ? lightDynamic : darkDynamic).primary; - state = DynamicColorState.enabled; - } else { - dynamicPrimaryColor = null; - state = DynamicColorState.disabled; - } - } else { - dynamicPrimaryColor = null; - state = DynamicColorState.unavailable; - } - return InheritedWidgetBase( - data: state, - child: builder(context, dynamicPrimaryColor), - ); - }, - ); - } -} - -class _ThemeDataProvider extends StatelessWidget { - final Color primaryColor; - final Brightness brightness; - final Widget child; - - const _ThemeDataProvider({ - required this.primaryColor, - required this.brightness, - required this.child, - }); - - @override - Widget build(BuildContext context) { - return InheritedWidgetBase( - data: _themeFromColorScheme(_colorSchemeFromColor()), - child: child, - ); - } - - ThemeData _themeFromColorScheme(ColorScheme scheme) { - return ThemeData( - useMaterial3: true, - brightness: scheme.brightness, - primaryColor: primaryColor, - colorScheme: scheme, - appBarTheme: AppBarTheme( - elevation: 4, - color: scheme.surface, - surfaceTintColor: scheme.surfaceTint, - ), - cardTheme: CardTheme( - clipBehavior: Clip.antiAlias, - color: scheme.surface, - elevation: 4, - margin: EdgeInsets.zero, - shadowColor: Colors.transparent, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(Dimens.borderRadiusL)), - surfaceTintColor: scheme.surfaceTint, - ), - dialogBackgroundColor: scheme.surface, - dialogTheme: DialogTheme( - backgroundColor: scheme.surface, - surfaceTintColor: scheme.surfaceTint, - elevation: 6, - ), - dividerColor: scheme.outlineVariant, - dividerTheme: DividerThemeData( - color: scheme.outlineVariant, - space: 0, - ), - listTileTheme: ListTileThemeData( - style: ListTileStyle.list, - iconColor: scheme.onSurface, - textColor: scheme.onSurface, - ), - scaffoldBackgroundColor: scheme.surface, - ); - } - - ColorScheme _colorSchemeFromColor() { - final scheme = brightness == Brightness.light - ? Scheme.light(primaryColor.value) - : Scheme.dark(primaryColor.value); - - return ColorScheme( - brightness: brightness, - background: Color(scheme.background), - error: Color(scheme.error), - errorContainer: Color(scheme.errorContainer), - onBackground: Color(scheme.onBackground), - onError: Color(scheme.onError), - onErrorContainer: Color(scheme.onErrorContainer), - primary: Color(scheme.primary), - onPrimary: Color(scheme.onPrimary), - primaryContainer: Color(scheme.primaryContainer), - onPrimaryContainer: Color(scheme.onPrimaryContainer), - secondary: Color(scheme.secondary), - onSecondary: Color(scheme.onSecondary), - surface: Color.alphaBlend( - Color(scheme.primary).withOpacity(0.05), - Color(scheme.background), - ), - onSurface: Color(scheme.onSurface), - surfaceVariant: Color.alphaBlend( - Color(scheme.primary).withOpacity(0.5), - Color(scheme.background), - ), - onSurfaceVariant: Color(scheme.onSurfaceVariant), - outline: Color(scheme.outline), - outlineVariant: Color(scheme.outlineVariant), - ); - } -} diff --git a/lib/providers/user_preferences_provider.dart b/lib/providers/user_preferences_provider.dart new file mode 100644 index 0000000..af644d1 --- /dev/null +++ b/lib/providers/user_preferences_provider.dart @@ -0,0 +1,295 @@ +import 'package:dynamic_color/dynamic_color.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:lightmeter/data/models/dynamic_colors_state.dart'; +import 'package:lightmeter/data/models/ev_source_type.dart'; +import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; +import 'package:lightmeter/data/models/supported_locale.dart'; +import 'package:lightmeter/data/models/theme_type.dart'; +import 'package:lightmeter/data/shared_prefs_service.dart'; +import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/providers/services_provider.dart'; +import 'package:lightmeter/res/theme.dart'; +import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; + +class UserPreferencesProvider extends StatefulWidget { + final Widget child; + + const UserPreferencesProvider({required this.child, super.key}); + + static _UserPreferencesProviderState of(BuildContext context) { + return context.findAncestorStateOfType<_UserPreferencesProviderState>()!; + } + + static DynamicColorState dynamicColorStateOf(BuildContext context) { + return _inheritFromEnumsModel(context, _Aspect.dynamicColorState).dynamicColorState; + } + + static EvSourceType evSourceTypeOf(BuildContext context) { + return _inheritFromEnumsModel(context, _Aspect.evSourceType).evSourceType; + } + + static SupportedLocale localeOf(BuildContext context) { + return _inheritFromEnumsModel(context, _Aspect.locale).locale; + } + + static MeteringScreenLayoutConfig meteringScreenConfigOf(BuildContext context) { + return context.findAncestorWidgetOfExactType<_MeteringScreenLayoutModel>()!.data; + } + + static bool meteringScreenFeatureOf(BuildContext context, MeteringScreenLayoutFeature feature) { + return InheritedModel.inheritFrom<_MeteringScreenLayoutModel>(context, aspect: feature)! + .data[feature]!; + } + + static StopType stopTypeOf(BuildContext context) { + return _inheritFromEnumsModel(context, _Aspect.stopType).stopType; + } + + static ThemeData themeOf(BuildContext context) { + return _inheritFromEnumsModel(context, _Aspect.theme).theme; + } + + static ThemeType themeTypeOf(BuildContext context) { + return _inheritFromEnumsModel(context, _Aspect.themeType).themeType; + } + + static _UserPreferencesModel _inheritFromEnumsModel( + BuildContext context, + _Aspect aspect, + ) { + return InheritedModel.inheritFrom<_UserPreferencesModel>(context, aspect: aspect)!; + } + + @override + State createState() => _UserPreferencesProviderState(); +} + +class _UserPreferencesProviderState extends State + with WidgetsBindingObserver { + UserPreferencesService get userPreferencesService => + ServicesProvider.of(context).userPreferencesService; + + late bool dynamicColor = userPreferencesService.dynamicColor; + late EvSourceType evSourceType; + late MeteringScreenLayoutConfig meteringScreenLayout = + userPreferencesService.meteringScreenLayout; + late Color primaryColor = userPreferencesService.primaryColor; + late StopType stopType = userPreferencesService.stopType; + late SupportedLocale locale = userPreferencesService.locale; + late ThemeType themeType = userPreferencesService.themeType; + + @override + void initState() { + super.initState(); + evSourceType = userPreferencesService.evSourceType; + evSourceType = evSourceType == EvSourceType.sensor && + !ServicesProvider.of(context).environment.hasLightSensor + ? EvSourceType.camera + : evSourceType; + WidgetsBinding.instance.addObserver(this); + } + + @override + void didChangePlatformBrightness() { + super.didChangePlatformBrightness(); + setState(() {}); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return DynamicColorBuilder( + builder: (lightDynamic, darkDynamic) { + late final DynamicColorState state; + late final Color? dynamicPrimaryColor; + if (lightDynamic != null && darkDynamic != null) { + if (dynamicColor) { + dynamicPrimaryColor = + (_themeBrightness == Brightness.light ? lightDynamic : darkDynamic).primary; + state = DynamicColorState.enabled; + } else { + dynamicPrimaryColor = null; + state = DynamicColorState.disabled; + } + } else { + dynamicPrimaryColor = null; + state = DynamicColorState.unavailable; + } + return _UserPreferencesModel( + brightness: _themeBrightness, + dynamicColorState: state, + evSourceType: evSourceType, + locale: locale, + primaryColor: dynamicPrimaryColor ?? primaryColor, + stopType: stopType, + themeType: themeType, + child: _MeteringScreenLayoutModel( + data: meteringScreenLayout, + child: widget.child, + ), + ); + }, + ); + } + + void enableDynamicColor(bool enable) { + setState(() { + dynamicColor = enable; + }); + userPreferencesService.dynamicColor = enable; + } + + void toggleEvSourceType() { + if (!ServicesProvider.of(context).environment.hasLightSensor) { + return; + } + setState(() { + switch (evSourceType) { + case EvSourceType.camera: + evSourceType = EvSourceType.sensor; + case EvSourceType.sensor: + evSourceType = EvSourceType.camera; + } + }); + userPreferencesService.evSourceType = evSourceType; + } + + void setLocale(SupportedLocale locale) { + S.load(Locale(locale.intlName)).then((value) { + setState(() { + this.locale = locale; + }); + userPreferencesService.locale = locale; + }); + } + + void setMeteringScreenLayout(MeteringScreenLayoutConfig config) { + setState(() { + meteringScreenLayout = config; + }); + userPreferencesService.meteringScreenLayout = meteringScreenLayout; + } + + void setPrimaryColor(Color primaryColor) { + setState(() { + this.primaryColor = primaryColor; + }); + userPreferencesService.primaryColor = primaryColor; + } + + void setStopType(StopType stopType) { + setState(() { + this.stopType = stopType; + }); + userPreferencesService.stopType = stopType; + } + + void setThemeType(ThemeType themeType) { + setState(() { + this.themeType = themeType; + }); + userPreferencesService.themeType = themeType; + } + + Brightness get _themeBrightness { + switch (themeType) { + case ThemeType.light: + return Brightness.light; + case ThemeType.dark: + return Brightness.dark; + case ThemeType.systemDefault: + return SchedulerBinding.instance.platformDispatcher.platformBrightness; + } + } +} + +enum _Aspect { + dynamicColorState, + evSourceType, + locale, + stopType, + theme, + themeType, +} + +class _UserPreferencesModel extends InheritedModel<_Aspect> { + final DynamicColorState dynamicColorState; + final EvSourceType evSourceType; + final SupportedLocale locale; + final StopType stopType; + final ThemeType themeType; + + final Brightness _brightness; + final Color _primaryColor; + + const _UserPreferencesModel({ + required Brightness brightness, + required this.dynamicColorState, + required this.evSourceType, + required this.locale, + required Color primaryColor, + required this.stopType, + required this.themeType, + required super.child, + }) : _brightness = brightness, + _primaryColor = primaryColor; + + ThemeData get theme => themeFrom(_primaryColor, _brightness); + + @override + bool updateShouldNotify(_UserPreferencesModel oldWidget) { + return _brightness != oldWidget._brightness || + dynamicColorState != oldWidget.dynamicColorState || + evSourceType != oldWidget.evSourceType || + locale != oldWidget.locale || + _primaryColor != oldWidget._primaryColor || + stopType != oldWidget.stopType || + themeType != oldWidget.themeType; + } + + @override + bool updateShouldNotifyDependent( + _UserPreferencesModel oldWidget, + Set<_Aspect> dependencies, + ) { + return (dependencies.contains(_Aspect.dynamicColorState) && + dynamicColorState != oldWidget.dynamicColorState) || + (dependencies.contains(_Aspect.evSourceType) && evSourceType != oldWidget.evSourceType) || + (dependencies.contains(_Aspect.locale) && locale != oldWidget.locale) || + (dependencies.contains(_Aspect.stopType) && stopType != oldWidget.stopType) || + (dependencies.contains(_Aspect.theme) && + (_brightness != oldWidget._brightness || _primaryColor != oldWidget._primaryColor)) || + (dependencies.contains(_Aspect.themeType) && themeType != oldWidget.themeType); + } +} + +class _MeteringScreenLayoutModel extends InheritedModel { + final Map data; + + const _MeteringScreenLayoutModel({ + required this.data, + required super.child, + }); + + @override + bool updateShouldNotify(_MeteringScreenLayoutModel oldWidget) => oldWidget.data != data; + + @override + bool updateShouldNotifyDependent( + _MeteringScreenLayoutModel oldWidget, + Set dependencies, + ) { + for (final dependecy in dependencies) { + if (oldWidget.data[dependecy] != data[dependecy]) { + return true; + } + } + return false; + } +} diff --git a/lib/res/theme.dart b/lib/res/theme.dart new file mode 100644 index 0000000..a6320c1 --- /dev/null +++ b/lib/res/theme.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/res/dimens.dart'; +import 'package:material_color_utilities/material_color_utilities.dart'; + +const primaryColorsList = [ + Color(0xfff44336), + Color(0xffe91e63), + Color(0xff9c27b0), + Color(0xff673ab7), + Color(0xff3f51b5), + Color(0xff2196f3), + Color(0xff03a9f4), + Color(0xff00bcd4), + Color(0xff009688), + Color(0xff4caf50), + Color(0xff8bc34a), + Color(0xffcddc39), + Color(0xffffeb3b), + Color(0xffffc107), + Color(0xffff9800), + Color(0xffff5722), +]; + +ThemeData themeFrom(Color primaryColor, Brightness brightness) { + final scheme = _colorSchemeFromColor(primaryColor, brightness); + return ThemeData( + useMaterial3: true, + brightness: scheme.brightness, + primaryColor: primaryColor, + colorScheme: scheme, + appBarTheme: AppBarTheme( + elevation: 4, + color: scheme.surface, + surfaceTintColor: scheme.surfaceTint, + ), + cardTheme: CardTheme( + clipBehavior: Clip.antiAlias, + color: scheme.surface, + elevation: 4, + margin: EdgeInsets.zero, + shadowColor: Colors.transparent, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(Dimens.borderRadiusL)), + surfaceTintColor: scheme.surfaceTint, + ), + dialogBackgroundColor: scheme.surface, + dialogTheme: DialogTheme( + backgroundColor: scheme.surface, + surfaceTintColor: scheme.surfaceTint, + elevation: 6, + ), + dividerColor: scheme.outlineVariant, + dividerTheme: DividerThemeData( + color: scheme.outlineVariant, + space: 0, + ), + listTileTheme: ListTileThemeData( + style: ListTileStyle.list, + iconColor: scheme.onSurface, + textColor: scheme.onSurface, + ), + scaffoldBackgroundColor: scheme.surface, + ); +} + +ColorScheme _colorSchemeFromColor(Color primaryColor, Brightness brightness) { + final scheme = brightness == Brightness.light + ? Scheme.light(primaryColor.value) + : Scheme.dark(primaryColor.value); + + return ColorScheme( + brightness: brightness, + background: Color(scheme.background), + error: Color(scheme.error), + errorContainer: Color(scheme.errorContainer), + onBackground: Color(scheme.onBackground), + onError: Color(scheme.onError), + onErrorContainer: Color(scheme.onErrorContainer), + primary: Color(scheme.primary), + onPrimary: Color(scheme.onPrimary), + primaryContainer: Color(scheme.primaryContainer), + onPrimaryContainer: Color(scheme.onPrimaryContainer), + secondary: Color(scheme.secondary), + onSecondary: Color(scheme.onSecondary), + surface: Color.alphaBlend( + Color(scheme.primary).withOpacity(0.05), + Color(scheme.background), + ), + onSurface: Color(scheme.onSurface), + surfaceVariant: Color.alphaBlend( + Color(scheme.primary).withOpacity(0.5), + Color(scheme.background), + ), + onSurfaceVariant: Color(scheme.onSurfaceVariant), + outline: Color(scheme.outline), + outlineVariant: Color(scheme.outlineVariant), + ); +} diff --git a/lib/screens/metering/components/bottom_controls/widget_bottom_controls.dart b/lib/screens/metering/components/bottom_controls/widget_bottom_controls.dart index 9989494..54ea810 100644 --- a/lib/screens/metering/components/bottom_controls/widget_bottom_controls.dart +++ b/lib/screens/metering/components/bottom_controls/widget_bottom_controls.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/data/models/ev_source_type.dart'; +import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/screens/metering/components/bottom_controls/components/measure_button/widget_button_measure.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; class MeteringBottomControls extends StatelessWidget { final double? ev; @@ -42,7 +42,7 @@ class MeteringBottomControls extends StatelessWidget { child: IconButton( onPressed: onSwitchEvSourceType, icon: Icon( - context.listen() != EvSourceType.camera + UserPreferencesProvider.evSourceTypeOf(context) != EvSourceType.camera ? Icons.camera_rear : Icons.wb_incandescent, ), diff --git a/lib/screens/metering/components/camera_container/components/camera_preview/widget_camera_preview.dart b/lib/screens/metering/components/camera_container/components/camera_preview/widget_camera_preview.dart index 288c678..ecfcb42 100644 --- a/lib/screens/metering/components/camera_container/components/camera_preview/widget_camera_preview.dart +++ b/lib/screens/metering/components/camera_container/components/camera_preview/widget_camera_preview.dart @@ -2,7 +2,7 @@ import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; import 'package:lightmeter/platform_config.dart'; -import 'package:lightmeter/providers/metering_screen_layout_provider.dart'; +import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/screens/metering/components/camera_container/components/camera_preview/components/camera_view/widget_camera_view.dart'; import 'package:lightmeter/screens/metering/components/camera_container/components/camera_preview/components/camera_view_placeholder/widget_placeholder_camera_view.dart'; @@ -38,7 +38,7 @@ class _CameraPreviewState extends State { alignment: Alignment.bottomCenter, children: [ CameraView(controller: widget.controller!), - if (MeteringScreenLayout.featureOf( + if (UserPreferencesProvider.meteringScreenFeatureOf( context, MeteringScreenLayoutFeature.histogram, )) diff --git a/lib/screens/metering/components/camera_container/provider_container_camera.dart b/lib/screens/metering/components/camera_container/provider_container_camera.dart index 344f573..0e27700 100644 --- a/lib/screens/metering/components/camera_container/provider_container_camera.dart +++ b/lib/screens/metering/components/camera_container/provider_container_camera.dart @@ -2,12 +2,11 @@ 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/interactors/metering_interactor.dart'; import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart'; import 'package:lightmeter/screens/metering/components/camera_container/bloc_container_camera.dart'; import 'package:lightmeter/screens/metering/components/camera_container/event_container_camera.dart'; import 'package:lightmeter/screens/metering/components/camera_container/widget_container_camera.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; +import 'package:lightmeter/screens/metering/flow_metering.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class CameraContainerProvider extends StatelessWidget { @@ -39,7 +38,7 @@ class CameraContainerProvider extends StatelessWidget { return BlocProvider( lazy: false, create: (context) => CameraContainerBloc( - context.get(), + MeteringInteractorProvider.of(context), context.read(), )..add(const RequestPermissionEvent()), child: CameraContainer( diff --git a/lib/screens/metering/components/camera_container/widget_container_camera.dart b/lib/screens/metering/components/camera_container/widget_container_camera.dart index e42991c..c510bc8 100644 --- a/lib/screens/metering/components/camera_container/widget_container_camera.dart +++ b/lib/screens/metering/components/camera_container/widget_container_camera.dart @@ -7,7 +7,7 @@ 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/providers/user_preferences_provider.dart'; import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/screens/metering/components/camera_container/bloc_container_camera.dart'; import 'package:lightmeter/screens/metering/components/camera_container/components/camera_controls/widget_camera_controls.dart'; @@ -114,14 +114,14 @@ class CameraContainer extends StatelessWidget { enabledFeaturesHeight += Dimens.readingContainerSingleValueHeight; enabledFeaturesHeight += Dimens.paddingS; } - if (MeteringScreenLayout.featureOf( + if (UserPreferencesProvider.meteringScreenFeatureOf( context, MeteringScreenLayoutFeature.extremeExposurePairs, )) { enabledFeaturesHeight += Dimens.readingContainerDoubleValueHeight; enabledFeaturesHeight += Dimens.paddingS; } - if (MeteringScreenLayout.featureOf( + if (UserPreferencesProvider.meteringScreenFeatureOf( context, MeteringScreenLayoutFeature.filmPicker, )) { diff --git a/lib/screens/metering/components/light_sensor_container/provider_container_light_sensor.dart b/lib/screens/metering/components/light_sensor_container/provider_container_light_sensor.dart index c7423fc..aa27504 100644 --- a/lib/screens/metering/components/light_sensor_container/provider_container_light_sensor.dart +++ b/lib/screens/metering/components/light_sensor_container/provider_container_light_sensor.dart @@ -2,11 +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/interactors/metering_interactor.dart'; import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart'; import 'package:lightmeter/screens/metering/components/light_sensor_container/bloc_container_light_sensor.dart'; import 'package:lightmeter/screens/metering/components/light_sensor_container/widget_container_light_sensor.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; +import 'package:lightmeter/screens/metering/flow_metering.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class LightSensorContainerProvider extends StatelessWidget { @@ -38,7 +37,7 @@ class LightSensorContainerProvider extends StatelessWidget { return BlocProvider( lazy: false, create: (context) => LightSensorContainerBloc( - context.get(), + MeteringInteractorProvider.of(context), context.read(), ), child: LightSensorContainer( diff --git a/lib/screens/metering/components/shared/readings_container/widget_container_readings.dart b/lib/screens/metering/components/shared/readings_container/widget_container_readings.dart index 0893305..d31380c 100644 --- a/lib/screens/metering/components/shared/readings_container/widget_container_readings.dart +++ b/lib/screens/metering/components/shared/readings_container/widget_container_readings.dart @@ -5,11 +5,10 @@ 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/providers/user_preferences_provider.dart'; import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/screens/metering/components/shared/readings_container/components/animated_dialog_picker/widget_picker_dialog_animated.dart'; import 'package:lightmeter/screens/metering/components/shared/readings_container/components/reading_value_container/widget_container_reading_value.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class ReadingsContainer extends StatelessWidget { @@ -43,7 +42,7 @@ class ReadingsContainer extends StatelessWidget { const _EquipmentProfilePicker(), const _InnerPadding(), ], - if (MeteringScreenLayout.featureOf( + if (UserPreferencesProvider.meteringScreenFeatureOf( context, MeteringScreenLayoutFeature.extremeExposurePairs, )) ...[ @@ -61,7 +60,7 @@ class ReadingsContainer extends StatelessWidget { ), const _InnerPadding(), ], - if (MeteringScreenLayout.featureOf( + if (UserPreferencesProvider.meteringScreenFeatureOf( context, MeteringScreenLayoutFeature.filmPicker, )) ...[ @@ -77,7 +76,7 @@ class ReadingsContainer extends StatelessWidget { Expanded( child: _IsoValuePicker( selectedValue: iso, - values: context.listen().isoValues, + values: EquipmentProfiles.selectedOf(context).isoValues, onChanged: onIsoChanged, ), ), @@ -85,7 +84,7 @@ class ReadingsContainer extends StatelessWidget { Expanded( child: _NdValuePicker( selectedValue: nd, - values: context.listen().ndValues, + values: EquipmentProfiles.selectedOf(context).ndValues, onChanged: onNdChanged, ), ), @@ -108,16 +107,16 @@ class _EquipmentProfilePicker extends StatelessWidget { return AnimatedDialogPicker( icon: Icons.camera, title: S.of(context).equipmentProfile, - selectedValue: context.listen(), - values: context.listen(), + selectedValue: EquipmentProfiles.selectedOf(context), + values: EquipmentProfiles.of(context), itemTitleBuilder: (_, value) => Text(value.id.isEmpty ? S.of(context).none : value.name), onChanged: EquipmentProfileProvider.of(context).setProfile, closedChild: ReadingValueContainer.singleValue( value: ReadingValue( label: S.of(context).equipmentProfile, - value: context.listen().id.isEmpty + value: EquipmentProfiles.selectedOf(context).id.isEmpty ? S.of(context).none - : context.listen().name, + : EquipmentProfiles.selectedOf(context).name, ), ), ); diff --git a/lib/screens/metering/flow_metering.dart b/lib/screens/metering/flow_metering.dart index 1caef02..cca5675 100644 --- a/lib/screens/metering/flow_metering.dart +++ b/lib/screens/metering/flow_metering.dart @@ -1,17 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:lightmeter/data/caffeine_service.dart'; -import 'package:lightmeter/data/haptics_service.dart'; -import 'package:lightmeter/data/light_sensor_service.dart'; -import 'package:lightmeter/data/permissions_service.dart'; -import 'package:lightmeter/data/shared_prefs_service.dart'; -import 'package:lightmeter/data/volume_events_service.dart'; import 'package:lightmeter/interactors/metering_interactor.dart'; +import 'package:lightmeter/providers/services_provider.dart'; import 'package:lightmeter/screens/metering/bloc_metering.dart'; import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart'; import 'package:lightmeter/screens/metering/components/shared/volume_keys_notifier/notifier_volume_keys.dart'; import 'package:lightmeter/screens/metering/screen_metering.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; class MeteringFlow extends StatefulWidget { const MeteringFlow({super.key}); @@ -23,31 +17,45 @@ class MeteringFlow extends StatefulWidget { class _MeteringFlowState extends State { @override Widget build(BuildContext context) { - return InheritedWidgetBase( + return MeteringInteractorProvider( data: MeteringInteractor( - context.get(), - context.get(), - context.get(), - context.get(), - context.get(), - context.get(), + ServicesProvider.of(context).userPreferencesService, + ServicesProvider.of(context).caffeineService, + ServicesProvider.of(context).hapticsService, + ServicesProvider.of(context).permissionsService, + ServicesProvider.of(context).lightSensorService, + ServicesProvider.of(context).volumeEventsService, )..initialize(), - child: InheritedWidgetBase( - data: VolumeKeysNotifier(context.get()), - child: MultiBlocProvider( - providers: [ - BlocProvider(create: (_) => MeteringCommunicationBloc()), - BlocProvider( - create: (context) => MeteringBloc( - context.get(), - context.get(), - context.read(), - ), + child: MultiBlocProvider( + providers: [ + BlocProvider(create: (_) => MeteringCommunicationBloc()), + BlocProvider( + create: (context) => MeteringBloc( + MeteringInteractorProvider.of(context), + VolumeKeysNotifier(ServicesProvider.of(context).volumeEventsService), + context.read(), ), - ], - child: const MeteringScreen(), - ), + ), + ], + child: const MeteringScreen(), ), ); } } + +class MeteringInteractorProvider extends InheritedWidget { + final MeteringInteractor data; + + const MeteringInteractorProvider({ + required this.data, + required super.child, + super.key, + }); + + static MeteringInteractor of(BuildContext context) { + return context.findAncestorWidgetOfExactType()!.data; + } + + @override + bool updateShouldNotify(MeteringInteractorProvider oldWidget) => false; +} diff --git a/lib/screens/metering/screen_metering.dart b/lib/screens/metering/screen_metering.dart index bbfebfc..ba4dbea 100644 --- a/lib/screens/metering/screen_metering.dart +++ b/lib/screens/metering/screen_metering.dart @@ -6,15 +6,17 @@ 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/ev_source_type_provider.dart'; +import 'package:lightmeter/providers/equipment_profile_provider.dart'; +import 'package:lightmeter/providers/services_provider.dart'; +import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/screens/metering/bloc_metering.dart'; import 'package:lightmeter/screens/metering/components/bottom_controls/provider_bottom_controls.dart'; import 'package:lightmeter/screens/metering/components/camera_container/provider_container_camera.dart'; import 'package:lightmeter/screens/metering/components/light_sensor_container/provider_container_light_sensor.dart'; import 'package:lightmeter/screens/metering/event_metering.dart'; import 'package:lightmeter/screens/metering/state_metering.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; +import 'package:lightmeter/screens/metering/utils/listener_metering_layout_feature.dart'; +import 'package:lightmeter/screens/metering/utils/listsner_equipment_profiles.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class MeteringScreen extends StatelessWidget { @@ -45,8 +47,8 @@ class MeteringScreen extends StatelessWidget { builder: (context, state) => MeteringBottomControlsProvider( ev: state is MeteringDataState ? state.ev : null, isMetering: state.isMetering, - onSwitchEvSourceType: context.get().hasLightSensor - ? EvSourceTypeProvider.of(context).toggleType + onSwitchEvSourceType: ServicesProvider.of(context).environment.hasLightSensor + ? UserPreferencesProvider.of(context).toggleEvSourceType : null, onMeasure: () => context.read().add(const MeasureEvent()), onSettings: () { @@ -71,12 +73,12 @@ class _InheritedListeners extends StatelessWidget { @override Widget build(BuildContext context) { - return InheritedWidgetListener( + return EquipmentProfileListener( onDidChangeDependencies: (value) { context.read().add(EquipmentProfileChangedEvent(value)); }, - child: InheritedModelAspectListener( - aspect: MeteringScreenLayoutFeature.filmPicker, + child: MeteringScreenLayoutFeatureListener( + feature: MeteringScreenLayoutFeature.filmPicker, onDidChangeDependencies: (value) { if (!value) context.read().add(const FilmChangedEvent(Film.other())); }, @@ -110,7 +112,8 @@ class _MeteringContainerBuidler extends StatelessWidget { final exposurePairs = ev != null ? buildExposureValues(context, ev!, film) : []; final fastest = exposurePairs.isNotEmpty ? exposurePairs.first : null; final slowest = exposurePairs.isNotEmpty ? exposurePairs.last : null; - return context.listen() == EvSourceType.camera + // Doubled build here when switching evSourceType. As new source bloc fires a new state on init + return UserPreferencesProvider.evSourceTypeOf(context) == EvSourceType.camera ? CameraContainerProvider( fastest: fastest, slowest: slowest, @@ -141,10 +144,10 @@ class _MeteringContainerBuidler extends StatelessWidget { } /// Depending on the `stopType` the exposure pairs list length is multiplied by 1,2 or 3 - final StopType stopType = context.listen(); + final StopType stopType = UserPreferencesProvider.stopTypeOf(context); final int evSteps = (ev * (stopType.index + 1)).round(); - final EquipmentProfile equipmentProfile = context.listen(); + final EquipmentProfile equipmentProfile = EquipmentProfiles.selectedOf(context); final List apertureValues = equipmentProfile.apertureValues.whereStopType(stopType); final List shutterSpeedValues = diff --git a/lib/screens/metering/utils/listener_metering_layout_feature.dart b/lib/screens/metering/utils/listener_metering_layout_feature.dart new file mode 100644 index 0000000..c245ec3 --- /dev/null +++ b/lib/screens/metering/utils/listener_metering_layout_feature.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; +import 'package:lightmeter/providers/user_preferences_provider.dart'; + +/// Listening to multiple dependencies at the same time causes firing an event for all dependencies +/// even though some of them didn't change: +/// ```dart +/// @override +/// void didChangeDependencies() { +/// super.didChangeDependencies(); +/// _bloc.add(EquipmentProfileChangedEvent(EquipmentProfile.of(context))); +/// if (!MeteringScreenLayout.featureStatusOf(context, MeteringScreenLayoutFeature.filmPicker)) { +/// _bloc.add(const FilmChangedEvent(Film.other())); +/// } +/// } +/// ``` +/// To overcome this issue I've decided to create a generic listener, +/// that will listen to each dependency separately. +class MeteringScreenLayoutFeatureListener extends StatefulWidget { + final MeteringScreenLayoutFeature feature; + final ValueChanged onDidChangeDependencies; + final Widget child; + + const MeteringScreenLayoutFeatureListener({ + required this.feature, + required this.onDidChangeDependencies, + required this.child, + super.key, + }); + + @override + State createState() => + _MeteringScreenLayoutFeatureListenerState(); +} + +class _MeteringScreenLayoutFeatureListenerState extends State { + @override + void didChangeDependencies() { + super.didChangeDependencies(); + widget.onDidChangeDependencies( + UserPreferencesProvider.meteringScreenFeatureOf( + context, + widget.feature, + ), + ); + } + + @override + Widget build(BuildContext context) { + return widget.child; + } +} diff --git a/lib/screens/metering/utils/listsner_equipment_profiles.dart b/lib/screens/metering/utils/listsner_equipment_profiles.dart new file mode 100644 index 0000000..ec604ce --- /dev/null +++ b/lib/screens/metering/utils/listsner_equipment_profiles.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/providers/equipment_profile_provider.dart'; +import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; + +class EquipmentProfileListener extends StatefulWidget { + final ValueChanged onDidChangeDependencies; + final Widget child; + + const EquipmentProfileListener({ + required this.onDidChangeDependencies, + required this.child, + super.key, + }); + + @override + State createState() => _EquipmentProfileListenerState(); +} + +class _EquipmentProfileListenerState extends State { + @override + void didChangeDependencies() { + super.didChangeDependencies(); + widget.onDidChangeDependencies(EquipmentProfiles.selectedOf(context)); + } + + @override + Widget build(BuildContext context) { + return widget.child; + } +} diff --git a/lib/screens/settings/components/about/components/report_issue/widget_list_tile_report_issue.dart b/lib/screens/settings/components/about/components/report_issue/widget_list_tile_report_issue.dart index 4c477f8..72bc1b5 100644 --- a/lib/screens/settings/components/about/components/report_issue/widget_list_tile_report_issue.dart +++ b/lib/screens/settings/components/about/components/report_issue/widget_list_tile_report_issue.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:lightmeter/environment.dart'; import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; +import 'package:lightmeter/providers/services_provider.dart'; import 'package:url_launcher/url_launcher.dart'; class ReportIssueListTile extends StatelessWidget { @@ -14,7 +13,7 @@ class ReportIssueListTile extends StatelessWidget { title: Text(S.of(context).reportIssue), onTap: () { launchUrl( - Uri.parse(context.get().issuesReportUrl), + Uri.parse(ServicesProvider.of(context).environment.issuesReportUrl), mode: LaunchMode.externalApplication, ); }, diff --git a/lib/screens/settings/components/about/components/source_code/widget_list_tile_source_code.dart b/lib/screens/settings/components/about/components/source_code/widget_list_tile_source_code.dart index 42a73e8..4327332 100644 --- a/lib/screens/settings/components/about/components/source_code/widget_list_tile_source_code.dart +++ b/lib/screens/settings/components/about/components/source_code/widget_list_tile_source_code.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:lightmeter/environment.dart'; import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; +import 'package:lightmeter/providers/services_provider.dart'; import 'package:url_launcher/url_launcher.dart'; class SourceCodeListTile extends StatelessWidget { @@ -14,7 +13,7 @@ class SourceCodeListTile extends StatelessWidget { title: Text(S.of(context).sourceCode), onTap: () { launchUrl( - Uri.parse(context.get().sourceCodeUrl), + Uri.parse(ServicesProvider.of(context).environment.sourceCodeUrl), mode: LaunchMode.externalApplication, ); }, diff --git a/lib/screens/settings/components/about/components/write_email/widget_list_tile_write_email.dart b/lib/screens/settings/components/about/components/write_email/widget_list_tile_write_email.dart index b12d0d4..b0a4391 100644 --- a/lib/screens/settings/components/about/components/write_email/widget_list_tile_write_email.dart +++ b/lib/screens/settings/components/about/components/write_email/widget_list_tile_write_email.dart @@ -1,8 +1,7 @@ import 'package:clipboard/clipboard.dart'; import 'package:flutter/material.dart'; -import 'package:lightmeter/environment.dart'; import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; +import 'package:lightmeter/providers/services_provider.dart'; import 'package:url_launcher/url_launcher.dart'; class WriteEmailListTile extends StatelessWidget { @@ -14,7 +13,7 @@ class WriteEmailListTile extends StatelessWidget { leading: const Icon(Icons.email), title: Text(S.of(context).writeEmail), onTap: () { - final email = context.get().contactEmail; + final email = ServicesProvider.of(context).environment.contactEmail; final mailToUrl = Uri.parse('mailto:$email?subject=M3 Lightmeter'); canLaunchUrl(mailToUrl).then((canLaunch) { if (canLaunch) { diff --git a/lib/screens/settings/components/general/components/caffeine/provider_list_tile_caffeine.dart b/lib/screens/settings/components/general/components/caffeine/provider_list_tile_caffeine.dart index 4509d20..8d9bcfb 100644 --- a/lib/screens/settings/components/general/components/caffeine/provider_list_tile_caffeine.dart +++ b/lib/screens/settings/components/general/components/caffeine/provider_list_tile_caffeine.dart @@ -1,10 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:lightmeter/interactors/settings_interactor.dart'; import 'package:lightmeter/screens/settings/components/general/components/caffeine/bloc_list_tile_caffeine.dart'; import 'package:lightmeter/screens/settings/components/general/components/caffeine/widget_list_tile_caffeine.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; +import 'package:lightmeter/screens/settings/flow_settings.dart'; class CaffeineListTileProvider extends StatelessWidget { const CaffeineListTileProvider({super.key}); @@ -12,7 +11,7 @@ class CaffeineListTileProvider extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => CaffeineListTileBloc(context.get()), + create: (context) => CaffeineListTileBloc(SettingsInteractorProvider.of(context)), child: const CaffeineListTile(), ); } diff --git a/lib/screens/settings/components/general/components/haptics/provider_list_tile_haptics.dart b/lib/screens/settings/components/general/components/haptics/provider_list_tile_haptics.dart index beb0462..34cdeab 100644 --- a/lib/screens/settings/components/general/components/haptics/provider_list_tile_haptics.dart +++ b/lib/screens/settings/components/general/components/haptics/provider_list_tile_haptics.dart @@ -1,10 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:lightmeter/interactors/settings_interactor.dart'; import 'package:lightmeter/screens/settings/components/general/components/haptics/bloc_list_tile_haptics.dart'; import 'package:lightmeter/screens/settings/components/general/components/haptics/widget_list_tile_haptics.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; +import 'package:lightmeter/screens/settings/flow_settings.dart'; class HapticsListTileProvider extends StatelessWidget { const HapticsListTileProvider({super.key}); @@ -12,7 +11,7 @@ class HapticsListTileProvider extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => HapticsListTileBloc(context.get()), + create: (context) => HapticsListTileBloc(SettingsInteractorProvider.of(context)), child: const HapticsListTile(), ); } diff --git a/lib/screens/settings/components/general/components/language/widget_list_tile_language.dart b/lib/screens/settings/components/general/components/language/widget_list_tile_language.dart index 381f286..663c634 100644 --- a/lib/screens/settings/components/general/components/language/widget_list_tile_language.dart +++ b/lib/screens/settings/components/general/components/language/widget_list_tile_language.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/data/models/supported_locale.dart'; import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/providers/supported_locale_provider.dart'; +import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/screens/settings/components/shared/dialog_picker.dart/widget_dialog_picker.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; class LanguageListTile extends StatelessWidget { const LanguageListTile({super.key}); @@ -13,20 +12,20 @@ class LanguageListTile extends StatelessWidget { return ListTile( leading: const Icon(Icons.language), title: Text(S.of(context).language), - trailing: Text(context.listen().localizedName), + trailing: Text(UserPreferencesProvider.localeOf(context).localizedName), onTap: () { showDialog( context: context, builder: (_) => DialogPicker( icon: Icons.language, title: S.of(context).chooseLanguage, - selectedValue: context.get(), + selectedValue: UserPreferencesProvider.localeOf(context), values: SupportedLocale.values, titleAdapter: (context, value) => value.localizedName, ), ).then((value) { if (value != null) { - SupportedLocaleProvider.of(context).setLocale(value); + UserPreferencesProvider.of(context).setLocale(value); } }); }, diff --git a/lib/screens/settings/components/general/components/volume_actions/provider_list_tile_volume_actions.dart b/lib/screens/settings/components/general/components/volume_actions/provider_list_tile_volume_actions.dart index 790ad4f..f99505f 100644 --- a/lib/screens/settings/components/general/components/volume_actions/provider_list_tile_volume_actions.dart +++ b/lib/screens/settings/components/general/components/volume_actions/provider_list_tile_volume_actions.dart @@ -1,10 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:lightmeter/interactors/settings_interactor.dart'; import 'package:lightmeter/screens/settings/components/general/components/volume_actions/bloc_list_tile_volume_actions.dart'; import 'package:lightmeter/screens/settings/components/general/components/volume_actions/widget_list_tile_volume_actions.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; +import 'package:lightmeter/screens/settings/flow_settings.dart'; class VolumeActionsListTileProvider extends StatelessWidget { const VolumeActionsListTileProvider({super.key}); @@ -12,7 +11,7 @@ class VolumeActionsListTileProvider extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => VolumeActionsListTileBloc(context.get()), + create: (context) => VolumeActionsListTileBloc(SettingsInteractorProvider.of(context)), child: const VolumeActionsListTile(), ); } diff --git a/lib/screens/settings/components/metering/components/calibration/components/calibration_dialog/provider_dialog_calibration.dart b/lib/screens/settings/components/metering/components/calibration/components/calibration_dialog/provider_dialog_calibration.dart index a95ec1b..a26a6cf 100644 --- a/lib/screens/settings/components/metering/components/calibration/components/calibration_dialog/provider_dialog_calibration.dart +++ b/lib/screens/settings/components/metering/components/calibration/components/calibration_dialog/provider_dialog_calibration.dart @@ -1,10 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:lightmeter/interactors/settings_interactor.dart'; import 'package:lightmeter/screens/settings/components/metering/components/calibration/components/calibration_dialog/bloc_dialog_calibration.dart'; import 'package:lightmeter/screens/settings/components/metering/components/calibration/components/calibration_dialog/widget_dialog_calibration.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; +import 'package:lightmeter/screens/settings/flow_settings.dart'; class CalibrationDialogProvider extends StatelessWidget { const CalibrationDialogProvider({super.key}); @@ -12,7 +11,7 @@ class CalibrationDialogProvider extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => CalibrationDialogBloc(context.get()), + create: (context) => CalibrationDialogBloc(SettingsInteractorProvider.of(context)), child: const CalibrationDialog(), ); } diff --git a/lib/screens/settings/components/metering/components/calibration/components/calibration_dialog/widget_dialog_calibration.dart b/lib/screens/settings/components/metering/components/calibration/components/calibration_dialog/widget_dialog_calibration.dart index 4c91c87..69a4f00 100644 --- a/lib/screens/settings/components/metering/components/calibration/components/calibration_dialog/widget_dialog_calibration.dart +++ b/lib/screens/settings/components/metering/components/calibration/components/calibration_dialog/widget_dialog_calibration.dart @@ -1,13 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:lightmeter/environment.dart'; import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/providers/services_provider.dart'; import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/screens/settings/components/metering/components/calibration/components/calibration_dialog/bloc_dialog_calibration.dart'; import 'package:lightmeter/screens/settings/components/metering/components/calibration/components/calibration_dialog/event_dialog_calibration.dart'; import 'package:lightmeter/screens/settings/components/metering/components/calibration/components/calibration_dialog/state_dialog_calibration.dart'; import 'package:lightmeter/screens/shared/centered_slider/widget_slider_centered.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; import 'package:lightmeter/utils/to_string_signed.dart'; class CalibrationDialog extends StatelessWidget { @@ -15,7 +14,7 @@ class CalibrationDialog extends StatelessWidget { @override Widget build(BuildContext context) { - final bool hasLightSensor = context.get().hasLightSensor; + final bool hasLightSensor = ServicesProvider.of(context).environment.hasLightSensor; return AlertDialog( icon: const Icon(Icons.settings_brightness), titlePadding: Dimens.dialogIconTitlePadding, diff --git a/lib/screens/settings/components/metering/components/calibration/widget_list_tile_calibration.dart b/lib/screens/settings/components/metering/components/calibration/widget_list_tile_calibration.dart index e591e4b..61690c4 100644 --- a/lib/screens/settings/components/metering/components/calibration/widget_list_tile_calibration.dart +++ b/lib/screens/settings/components/metering/components/calibration/widget_list_tile_calibration.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/interactors/settings_interactor.dart'; import 'package:lightmeter/screens/settings/components/metering/components/calibration/components/calibration_dialog/provider_dialog_calibration.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; +import 'package:lightmeter/screens/settings/flow_settings.dart'; class CalibrationListTile extends StatelessWidget { const CalibrationListTile({super.key}); @@ -15,8 +14,8 @@ class CalibrationListTile extends StatelessWidget { onTap: () { showDialog( context: context, - builder: (_) => InheritedWidgetBase( - data: context.get(), + builder: (_) => SettingsInteractorProvider( + data: SettingsInteractorProvider.of(context), child: const CalibrationDialogProvider(), ), ); diff --git a/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart index 4872d79..3120b38 100644 --- a/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart +++ b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/providers/equipment_profile_provider.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/widget_container_equipment_profile.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/shared/sliver_screen/screen_sliver.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class EquipmentProfilesScreen extends StatefulWidget { @@ -19,13 +19,12 @@ class _EquipmentProfilesScreenState extends State { static const maxProfiles = 5 + 1; // replace with a constant from iap late List> profileContainersKeys = []; - int get profilesCount => context.listen().length; + int get profilesCount => EquipmentProfiles.of(context).length; @override - void initState() { - super.initState(); - profileContainersKeys = context - .get() + void didChangeDependencies() { + super.didChangeDependencies(); + profileContainersKeys = EquipmentProfiles.of(context) .map((e) => GlobalKey(debugLabel: e.id)) .toList(); } @@ -58,14 +57,14 @@ class _EquipmentProfilesScreenState extends State { ), child: EquipmentProfileContainer( key: profileContainersKeys[index], - data: context.listen()[index], + data: EquipmentProfiles.of(context)[index], onExpand: () => _keepExpandedAt(index), onUpdate: (profileData) => _updateProfileAt(profileData, index), onDelete: () => _removeProfileAt(index), ), ) : const SizedBox.shrink(), - childCount: profileContainersKeys.length, + childCount: profilesCount, ), ), ], @@ -79,7 +78,6 @@ class _EquipmentProfilesScreenState extends State { ).then((value) { if (value != null) { EquipmentProfileProvider.of(context).addProfile(value); - profileContainersKeys.add(GlobalKey()); } }); } @@ -89,8 +87,7 @@ class _EquipmentProfilesScreenState extends State { } void _removeProfileAt(int index) { - EquipmentProfileProvider.of(context).deleteProfile(context.listen()[index]); - profileContainersKeys.removeAt(index); + EquipmentProfileProvider.of(context).deleteProfile(EquipmentProfiles.of(context)[index]); } void _keepExpandedAt(int index) { diff --git a/lib/screens/settings/components/metering/components/fractional_stops/widget_list_tile_fractional_stops.dart b/lib/screens/settings/components/metering/components/fractional_stops/widget_list_tile_fractional_stops.dart index 2a27a56..1bcf6bc 100644 --- a/lib/screens/settings/components/metering/components/fractional_stops/widget_list_tile_fractional_stops.dart +++ b/lib/screens/settings/components/metering/components/fractional_stops/widget_list_tile_fractional_stops.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/providers/stop_type_provider.dart'; +import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/screens/settings/components/shared/dialog_picker.dart/widget_dialog_picker.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class StopTypeListTile extends StatelessWidget { @@ -13,20 +12,20 @@ class StopTypeListTile extends StatelessWidget { return ListTile( leading: const Icon(Icons.straighten), title: Text(S.of(context).fractionalStops), - trailing: Text(_typeToString(context, context.listen())), + trailing: Text(_typeToString(context, UserPreferencesProvider.stopTypeOf(context))), onTap: () { showDialog( context: context, builder: (_) => DialogPicker( icon: Icons.straighten, title: S.of(context).showFractionalStops, - selectedValue: context.get(), + selectedValue: UserPreferencesProvider.stopTypeOf(context), values: StopType.values, titleAdapter: _typeToString, ), ).then((value) { if (value != null) { - StopTypeProvider.of(context).set(value); + UserPreferencesProvider.of(context).setStopType(value); } }); }, diff --git a/lib/screens/settings/components/metering/components/metering_screen_layout/components/meterins_screen_layout_features_dialog/widget_dialog_metering_screen_layout_features.dart b/lib/screens/settings/components/metering/components/metering_screen_layout/components/meterins_screen_layout_features_dialog/widget_dialog_metering_screen_layout_features.dart index d43f011..c60abab 100644 --- a/lib/screens/settings/components/metering/components/metering_screen_layout/components/meterins_screen_layout_features_dialog/widget_dialog_metering_screen_layout_features.dart +++ b/lib/screens/settings/components/metering/components/metering_screen_layout/components/meterins_screen_layout_features_dialog/widget_dialog_metering_screen_layout_features.dart @@ -1,7 +1,7 @@ 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/providers/user_preferences_provider.dart'; import 'package:lightmeter/res/dimens.dart'; class MeteringScreenLayoutFeaturesDialog extends StatefulWidget { @@ -14,7 +14,7 @@ class MeteringScreenLayoutFeaturesDialog extends StatefulWidget { class _MeteringScreenLayoutFeaturesDialogState extends State { late final _features = - MeteringScreenLayoutConfig.from(MeteringScreenLayout.of(context, listen: false)); + MeteringScreenLayoutConfig.from(UserPreferencesProvider.meteringScreenConfigOf(context)); @override Widget build(BuildContext context) { @@ -56,7 +56,7 @@ class _MeteringScreenLayoutFeaturesDialogState extends State() == DynamicColorState.enabled, - onChanged: ThemeProvider.of(context).enableDynamicColor, + value: UserPreferencesProvider.dynamicColorStateOf(context) == DynamicColorState.enabled, + onChanged: UserPreferencesProvider.of(context).enableDynamicColor, contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM), ); } diff --git a/lib/screens/settings/components/theme/components/primary_color/components/primary_color_picker_dialog/widget_dialog_picker_primary_color.dart b/lib/screens/settings/components/theme/components/primary_color/components/primary_color_picker_dialog/widget_dialog_picker_primary_color.dart index 434336a..380faa1 100644 --- a/lib/screens/settings/components/theme/components/primary_color/components/primary_color_picker_dialog/widget_dialog_picker_primary_color.dart +++ b/lib/screens/settings/components/theme/components/primary_color/components/primary_color_picker_dialog/widget_dialog_picker_primary_color.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/providers/theme_provider.dart'; import 'package:lightmeter/res/dimens.dart'; +import 'package:lightmeter/res/theme.dart'; import 'package:lightmeter/screens/shared/filled_circle/widget_circle_filled.dart'; class PrimaryColorDialogPicker extends StatefulWidget { @@ -38,9 +38,9 @@ class _PrimaryColorDialogPickerState extends State { padding: EdgeInsets.zero, child: Row( children: List.generate( - ThemeProvider.primaryColorsList.length, + primaryColorsList.length, (index) { - final color = ThemeProvider.primaryColorsList[index]; + final color = primaryColorsList[index]; return Padding( padding: EdgeInsets.only(left: index == 0 ? 0 : Dimens.paddingS), child: _SelectableColorItem( diff --git a/lib/screens/settings/components/theme/components/primary_color/widget_list_tile_primary_color.dart b/lib/screens/settings/components/theme/components/primary_color/widget_list_tile_primary_color.dart index afc1a7b..44de22e 100644 --- a/lib/screens/settings/components/theme/components/primary_color/widget_list_tile_primary_color.dart +++ b/lib/screens/settings/components/theme/components/primary_color/widget_list_tile_primary_color.dart @@ -1,17 +1,16 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/data/models/dynamic_colors_state.dart'; import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/providers/theme_provider.dart'; +import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/screens/settings/components/theme/components/primary_color/components/primary_color_picker_dialog/widget_dialog_picker_primary_color.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; class PrimaryColorListTile extends StatelessWidget { const PrimaryColorListTile({super.key}); @override Widget build(BuildContext context) { - if (context.listen() == DynamicColorState.enabled) { + if (UserPreferencesProvider.dynamicColorStateOf(context) == DynamicColorState.enabled) { return Opacity( opacity: Dimens.disabledOpacity, child: IgnorePointer( @@ -31,7 +30,7 @@ class PrimaryColorListTile extends StatelessWidget { builder: (_) => const PrimaryColorDialogPicker(), ).then((value) { if (value != null) { - ThemeProvider.of(context).setPrimaryColor(value); + UserPreferencesProvider.of(context).setPrimaryColor(value); } }); }, diff --git a/lib/screens/settings/components/theme/components/theme_type/widget_list_tile_theme_type.dart b/lib/screens/settings/components/theme/components/theme_type/widget_list_tile_theme_type.dart index 46e1b86..8a95161 100644 --- a/lib/screens/settings/components/theme/components/theme_type/widget_list_tile_theme_type.dart +++ b/lib/screens/settings/components/theme/components/theme_type/widget_list_tile_theme_type.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/data/models/theme_type.dart'; import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/providers/theme_provider.dart'; +import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/screens/settings/components/shared/dialog_picker.dart/widget_dialog_picker.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; class ThemeTypeListTile extends StatelessWidget { const ThemeTypeListTile({super.key}); @@ -13,20 +12,20 @@ class ThemeTypeListTile extends StatelessWidget { return ListTile( leading: const Icon(Icons.brightness_6), title: Text(S.of(context).theme), - trailing: Text(_typeToString(context, context.listen())), + trailing: Text(_typeToString(context, UserPreferencesProvider.themeTypeOf(context))), onTap: () { showDialog( context: context, builder: (_) => DialogPicker( icon: Icons.brightness_6, title: S.of(context).chooseTheme, - selectedValue: context.get(), + selectedValue: UserPreferencesProvider.themeTypeOf(context), values: ThemeType.values, titleAdapter: _typeToString, ), ).then((value) { if (value != null) { - ThemeProvider.of(context).setThemeType(value); + UserPreferencesProvider.of(context).setThemeType(value); } }); }, diff --git a/lib/screens/settings/components/theme/widget_settings_section_theme.dart b/lib/screens/settings/components/theme/widget_settings_section_theme.dart index d137469..ee03c54 100644 --- a/lib/screens/settings/components/theme/widget_settings_section_theme.dart +++ b/lib/screens/settings/components/theme/widget_settings_section_theme.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/data/models/dynamic_colors_state.dart'; import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/screens/settings/components/shared/settings_section/widget_settings_section.dart'; import 'package:lightmeter/screens/settings/components/theme/components/dynamic_color/widget_list_tile_dynamic_color.dart'; import 'package:lightmeter/screens/settings/components/theme/components/primary_color/widget_list_tile_primary_color.dart'; import 'package:lightmeter/screens/settings/components/theme/components/theme_type/widget_list_tile_theme_type.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; class ThemeSettingsSection extends StatelessWidget { const ThemeSettingsSection({super.key}); @@ -17,7 +17,7 @@ class ThemeSettingsSection extends StatelessWidget { children: [ const ThemeTypeListTile(), const PrimaryColorListTile(), - if (context.get() != DynamicColorState.unavailable) + if (UserPreferencesProvider.dynamicColorStateOf(context) != DynamicColorState.unavailable) const DynamicColorListTile(), ], ); diff --git a/lib/screens/settings/flow_settings.dart b/lib/screens/settings/flow_settings.dart index 3195c25..f3c8156 100644 --- a/lib/screens/settings/flow_settings.dart +++ b/lib/screens/settings/flow_settings.dart @@ -1,25 +1,38 @@ import 'package:flutter/material.dart'; -import 'package:lightmeter/data/caffeine_service.dart'; -import 'package:lightmeter/data/haptics_service.dart'; -import 'package:lightmeter/data/shared_prefs_service.dart'; -import 'package:lightmeter/data/volume_events_service.dart'; import 'package:lightmeter/interactors/settings_interactor.dart'; +import 'package:lightmeter/providers/services_provider.dart'; import 'package:lightmeter/screens/settings/screen_settings.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; class SettingsFlow extends StatelessWidget { const SettingsFlow({super.key}); @override Widget build(BuildContext context) { - return InheritedWidgetBase( + return SettingsInteractorProvider( data: SettingsInteractor( - context.get(), - context.get(), - context.get(), - context.get(), + ServicesProvider.of(context).userPreferencesService, + ServicesProvider.of(context).caffeineService, + ServicesProvider.of(context).hapticsService, + ServicesProvider.of(context).volumeEventsService, ), child: const SettingsScreen(), ); } } + +class SettingsInteractorProvider extends InheritedWidget { + final SettingsInteractor data; + + const SettingsInteractorProvider({ + required this.data, + required super.child, + super.key, + }); + + static SettingsInteractor of(BuildContext context) { + return context.findAncestorWidgetOfExactType()!.data; + } + + @override + bool updateShouldNotify(SettingsInteractorProvider oldWidget) => false; +} diff --git a/lib/screens/settings/screen_settings.dart b/lib/screens/settings/screen_settings.dart index 3c745bd..9332d3f 100644 --- a/lib/screens/settings/screen_settings.dart +++ b/lib/screens/settings/screen_settings.dart @@ -1,12 +1,11 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/interactors/settings_interactor.dart'; import 'package:lightmeter/screens/settings/components/about/widget_settings_section_about.dart'; import 'package:lightmeter/screens/settings/components/general/widget_settings_section_general.dart'; import 'package:lightmeter/screens/settings/components/metering/widget_settings_section_metering.dart'; import 'package:lightmeter/screens/settings/components/theme/widget_settings_section_theme.dart'; +import 'package:lightmeter/screens/settings/flow_settings.dart'; import 'package:lightmeter/screens/shared/sliver_screen/screen_sliver.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; class SettingsScreen extends StatefulWidget { const SettingsScreen({super.key}); @@ -19,12 +18,12 @@ class _SettingsScreenState extends State { @override void didChangeDependencies() { super.didChangeDependencies(); - context.get().disableVolumeHandling(); + SettingsInteractorProvider.of(context).disableVolumeHandling(); } @override void deactivate() { - context.get().restoreVolumeHandling(); + SettingsInteractorProvider.of(context).restoreVolumeHandling(); super.deactivate(); } diff --git a/lib/utils/inherited_generics.dart b/lib/utils/inherited_generics.dart deleted file mode 100644 index 5f71ec6..0000000 --- a/lib/utils/inherited_generics.dart +++ /dev/null @@ -1,171 +0,0 @@ -import 'package:flutter/widgets.dart'; - -/// Listening to multiple dependencies at the same time causes firing an event for all dependencies -/// even though some of them didn't change: -/// ```dart -/// @override -/// void didChangeDependencies() { -/// super.didChangeDependencies(); -/// _bloc.add(EquipmentProfileChangedEvent(EquipmentProfile.of(context))); -/// if (!MeteringScreenLayout.featureStatusOf(context, MeteringScreenLayoutFeature.filmPicker)) { -/// _bloc.add(const FilmChangedEvent(Film.other())); -/// } -/// } -/// ``` -/// To overcome this issue I've decided to create a generic listener, -/// that will listen to each dependency separately. -class InheritedWidgetListener extends StatefulWidget { - final ValueChanged onDidChangeDependencies; - final Widget child; - - const InheritedWidgetListener({ - required this.onDidChangeDependencies, - required this.child, - super.key, - }); - - @override - State> createState() => _InheritedWidgetListenerState(); -} - -class _InheritedWidgetListenerState extends State> { - @override - void didChangeDependencies() { - super.didChangeDependencies(); - widget.onDidChangeDependencies(context.listen()); - } - - @override - Widget build(BuildContext context) { - return widget.child; - } -} - -class InheritedWidgetBase extends InheritedWidget { - final T data; - - const InheritedWidgetBase({ - required this.data, - required super.child, - super.key, - }); - - static T of(BuildContext context, {bool listen = true}) { - if (listen) { - return context.dependOnInheritedWidgetOfExactType>()!.data; - } else { - return context.findAncestorWidgetOfExactType>()!.data; - } - } - - @override - bool updateShouldNotify(InheritedWidgetBase oldWidget) => true; -} - -extension InheritedWidgetBaseContext on BuildContext { - T get() { - return InheritedWidgetBase.of(this, listen: false); - } - - T listen() { - return InheritedWidgetBase.of(this); - } -} - -/// Listening to multiple dependencies at the same time causes firing an event for all dependencies -/// even though some of them didn't change: -/// ```dart -/// @override -/// void didChangeDependencies() { -/// super.didChangeDependencies(); -/// _bloc.add(EquipmentProfileChangedEvent(EquipmentProfile.of(context))); -/// if (!MeteringScreenLayout.featureStatusOf(context, MeteringScreenLayoutFeature.filmPicker)) { -/// _bloc.add(const FilmChangedEvent(Film.other())); -/// } -/// } -/// ``` -/// To overcome this issue I've decided to create a generic listener, -/// that will listen to each dependency separately. -class InheritedModelAspectListener extends StatefulWidget { - final A aspect; - final ValueChanged onDidChangeDependencies; - final Widget child; - - const InheritedModelAspectListener({ - required this.aspect, - required this.onDidChangeDependencies, - required this.child, - super.key, - }); - - @override - State> createState() => - _InheritedModelAspectListenerState(); -} - -class _InheritedModelAspectListenerState - extends State> { - @override - void didChangeDependencies() { - super.didChangeDependencies(); - widget.onDidChangeDependencies(context.listenModelFeature(widget.aspect)); - } - - @override - Widget build(BuildContext context) { - return widget.child; - } -} - -class InheritedModelBase extends InheritedModel { - final Map data; - - const InheritedModelBase({ - required this.data, - required super.child, - super.key, - }); - - static Map of(BuildContext context, {bool listen = true}) { - if (listen) { - return context.dependOnInheritedWidgetOfExactType>()!.data; - } else { - return context.findAncestorWidgetOfExactType>()!.data; - } - } - - static T featureOf(BuildContext context, A aspect) { - return InheritedModel.inheritFrom>(context, aspect: aspect)! - .data[aspect]!; - } - - @override - bool updateShouldNotify(InheritedModelBase oldWidget) => true; - - @override - bool updateShouldNotifyDependent( - InheritedModelBase oldWidget, - Set dependencies, - ) { - for (final dependecy in dependencies) { - if (oldWidget.data[dependecy] != data[dependecy]) { - return true; - } - } - return false; - } -} - -extension InheritedModelBaseContext on BuildContext { - Map getModel() { - return InheritedModelBase.of(this, listen: false); - } - - Map listenModel() { - return InheritedModelBase.of(this); - } - - T listenModelFeature(A aspect) { - return InheritedModelBase.featureOf(this, aspect); - } -} diff --git a/test/data/shared_prefs_service_test.dart b/test/data/shared_prefs_service_test.dart index 514629f..692cb81 100644 --- a/test/data/shared_prefs_service_test.dart +++ b/test/data/shared_prefs_service_test.dart @@ -6,7 +6,7 @@ import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; import 'package:lightmeter/data/models/supported_locale.dart'; import 'package:lightmeter/data/models/theme_type.dart'; import 'package:lightmeter/data/shared_prefs_service.dart'; -import 'package:lightmeter/providers/theme_provider.dart'; +import 'package:lightmeter/res/theme.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:mocktail/mocktail.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -348,13 +348,13 @@ void main() { group('primaryColor', () { test('get default', () { when(() => sharedPreferences.getInt(UserPreferencesService.primaryColorKey)).thenReturn(null); - expect(service.primaryColor, ThemeProvider.primaryColorsList[5]); + expect(service.primaryColor, primaryColorsList[5]); }); test('get', () { when(() => sharedPreferences.getInt(UserPreferencesService.primaryColorKey)) .thenReturn(0xff9c27b0); - expect(service.primaryColor, ThemeProvider.primaryColorsList[2]); + expect(service.primaryColor, primaryColorsList[2]); }); test('set', () { From aee527dccaac9cf8b632c1a53247c38cfccf123e Mon Sep 17 00:00:00 2001 From: Vadim <44135514+vodemn@users.noreply.github.com> Date: Sun, 20 Aug 2023 11:14:37 +0200 Subject: [PATCH 02/23] Create FUNDING.yml --- .github/FUNDING.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..9375169 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: [vodemn] From a9f8223e4a1f7548b45e1a24e74d9d41ed6542be Mon Sep 17 00:00:00 2001 From: Vadim Date: Mon, 28 Aug 2023 11:39:20 +0200 Subject: [PATCH 03/23] Added Firebase Analytics --- android/app/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/android/app/build.gradle b/android/app/build.gradle index 53774dc..5ccd388 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -109,4 +109,5 @@ flutter { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "com.android.billingclient:billing-ktx:6.0.0" + implementation "com.google.firebase:firebase-analytics:17.4.1" } From e1c320b8047c9dd01383aa36af3cba287ad0a902 Mon Sep 17 00:00:00 2001 From: Vadim Date: Mon, 28 Aug 2023 16:01:04 +0200 Subject: [PATCH 04/23] Replaced user config with github-actions[bot] --- .github/workflows/create_release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml index 07f76c8..3d238b4 100644 --- a/.github/workflows/create_release.yml +++ b/.github/workflows/create_release.yml @@ -149,8 +149,8 @@ jobs: - name: Commit changes run: | - git config --global user.name "vodemn" - git config --global user.email "vadim.turko@gmail.com" + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" git add -A git commit -m "Version bump" From d364de44864a836f422544565e334ddc6fb55ce5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 28 Aug 2023 15:55:12 +0000 Subject: [PATCH 05/23] Version bump --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 7f4fcb5..f8ba217 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: lightmeter description: A new Flutter project. publish_to: "none" -version: 0.13.1+37 +version: 0.13.2+38 environment: sdk: ">=3.0.0 <4.0.0" From 4bb080a144e9233bd40ac4a5eeac6d3126256ae4 Mon Sep 17 00:00:00 2001 From: Vadim <44135514+vodemn@users.noreply.github.com> Date: Sat, 2 Sep 2023 10:32:08 +0200 Subject: [PATCH 06/23] Implemented IAP & Equipment profiles (#89) * added equipment profiles to layout config * calculate layout height based on `MeteringScreenLayoutFeature` * Update cd_dev.yml * Fixed equipment profile tile padding * import * `webfactory/ssh-agent` * Update pubspec.yaml * fixed `MeteringScreenLayoutConfigJson` tests * fixed `UserPreferencesService` tests * reset selected equipment profile when layout feature is disabled * `IAPProductType.equipment` -> `IAPProductType.paidFeatures` * updated packages versions * Update shared_prefs_service.dart * Fixed & tested exposure pairs list builder * typo * typo * added iap repo stub * Renamed `EquipmentProfileData` ->`EquipmentProfile` * Moved `EquipmentProfileProvider` to iap repo * Update README.md * Fixed `EquipmentProfileListener` * Improved `EquipmentProfilesListTile` statuses visualization * Update README.md * Update ci.yml * Post-merge fixes * typo * Added workflow checks * more sophisticated iap icons * Include IAP by default * added loader for `IAPProductStatus.pending` * typo * Added equipment profiles list placeholder * typo * separated `IconPlaceholder` * improved `buildExposureValues` testing * cleanup --- .github/workflows/build_apk.yml | 16 +- .github/workflows/create_release.yml | 17 +- .github/workflows/pr_check.yml | 20 +- .vscode/launch.json | 2 + README.md | 19 +- iap/.gitignore | 36 + iap/.metadata | 10 + iap/LICENSE | 1 + iap/analysis_options.yaml | 4 + iap/lib/m3_lightmeter_iap.dart | 30 + iap/lib/src/data/models/iap_product.dart | 5 + .../providers/equipment_profile_provider.dart | 79 ++ .../src/providers/iap_products_provider.dart | 47 + iap/pubspec.yaml | 25 + ios/Runner.xcodeproj/project.pbxproj | 16 +- lib/application.dart | 24 +- lib/data/models/exposure_pair.dart | 12 + lib/data/models/film.dart | 2 + .../models/metering_screen_layout_config.dart | 9 +- lib/data/shared_prefs_service.dart | 7 +- lib/features.dart | 3 - lib/l10n/intl_en.arb | 3 +- lib/l10n/intl_fr.arb | 3 +- lib/l10n/intl_ru.arb | 3 +- lib/l10n/intl_zh.arb | 1 + lib/providers/equipment_profile_provider.dart | 139 --- lib/res/dimens.dart | 3 +- .../bloc_container_camera.dart | 2 +- .../widget_container_camera.dart | 8 +- .../bloc_container_light_sensor.dart | 2 +- .../widget_list_exposure_pairs.dart | 8 +- .../widget_container_readings.dart | 8 +- lib/screens/metering/screen_metering.dart | 94 +- .../utils/equipment_profile_listener.dart | 30 + .../utils/listsner_equipment_profiles.dart | 2 +- .../widget_container_equipment_profile.dart | 3 +- .../screen_equipment_profile.dart | 88 +- .../widget_list_tile_equipment_profiles.dart | 30 +- ...ialog_metering_screen_layout_features.dart | 31 +- .../widget_settings_section_metering.dart | 3 +- lib/screens/settings/screen_settings.dart | 1 + .../widget_icon_placeholder.dart} | 18 +- .../shared/sliver_screen/screen_sliver.dart | 3 +- lib/utils/log_2.dart | 3 - pubspec.yaml | 24 +- .../metering_screen_layout_config_test.dart | 35 +- test/data/shared_prefs_service_test.dart | 7 +- .../metering/screen_metering_test.dart | 966 ++++++++++++++++++ 48 files changed, 1608 insertions(+), 294 deletions(-) create mode 100644 iap/.gitignore create mode 100644 iap/.metadata create mode 100644 iap/LICENSE create mode 100644 iap/analysis_options.yaml create mode 100644 iap/lib/m3_lightmeter_iap.dart create mode 100644 iap/lib/src/data/models/iap_product.dart create mode 100644 iap/lib/src/providers/equipment_profile_provider.dart create mode 100644 iap/lib/src/providers/iap_products_provider.dart create mode 100644 iap/pubspec.yaml delete mode 100644 lib/features.dart delete mode 100644 lib/providers/equipment_profile_provider.dart create mode 100644 lib/screens/metering/utils/equipment_profile_listener.dart rename lib/screens/{metering/components/shared/exposure_pairs_list/components/empty_exposure_pairs_list/widget_list_exposure_pairs_empty.dart => shared/icon_placeholder/widget_icon_placeholder.dart} (68%) delete mode 100644 lib/utils/log_2.dart create mode 100644 test/screens/metering/screen_metering_test.dart diff --git a/.github/workflows/build_apk.yml b/.github/workflows/build_apk.yml index 9157f90..e61b38b 100644 --- a/.github/workflows/build_apk.yml +++ b/.github/workflows/build_apk.yml @@ -16,14 +16,28 @@ on: - dev - prod default: 'dev' + include-iap: + type: boolean + description: Include IAP package + default: true jobs: build: name: Build .apk runs-on: macos-11 timeout-minutes: 15 - steps: + - name: Connect private iap package + uses: webfactory/ssh-agent@v0.8.0 + if: ${{ inputs.include-iap }} + with: + ssh-private-key: ${{ secrets.M3_LIGHTMETER_IAP_KEY }} + + - name: Override iap package with stub + if: ${{ !inputs.include-iap }} + run: | + echo "\ndependency_overrides:\n m3_lightmeter_iap:\n path: iap" >> pubspec.yaml + - uses: actions/checkout@v3 with: submodules: recursive diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml index 3d238b4..e0281e3 100644 --- a/.github/workflows/create_release.yml +++ b/.github/workflows/create_release.yml @@ -31,6 +31,10 @@ on: type: boolean description: Create Google Play release default: true + include-iap: + type: boolean + description: Include IAP package + default: true env: BUILD_ARGS: --release --flavor prod --dart-define cameraPreviewAspectRatio=240/320 -t lib/main_prod.dart @@ -38,10 +42,21 @@ env: jobs: build: name: Build .apk & .aab - if: ${{ inputs.github-release }} || ${{ inputs.google-play-release }} + if: ${{ inputs.github-release || inputs.google-play-release }} runs-on: macos-11 timeout-minutes: 30 steps: + - name: Connect private iap package + uses: webfactory/ssh-agent@v0.8.0 + if: ${{ inputs.include-iap }} + with: + ssh-private-key: ${{ secrets.M3_LIGHTMETER_IAP_KEY }} + + - name: Override iap package with stub + if: ${{ !inputs.include-iap }} + run: | + echo "\ndependency_overrides:\n m3_lightmeter_iap:\n path: iap" >> pubspec.yaml + - uses: actions/checkout@v3 with: submodules: recursive diff --git a/.github/workflows/pr_check.yml b/.github/workflows/pr_check.yml index 9022170..e6f0294 100644 --- a/.github/workflows/pr_check.yml +++ b/.github/workflows/pr_check.yml @@ -11,13 +11,27 @@ on: pull_request: branches: ["main"] +env: + # Stub iap package if this worlflow is running from the PR from a fork + STUB_IAP: ${{ github.event.pull_request.head.repo.full_name != github.repository }} + jobs: analyze_and_test: name: Analyze & test runs-on: macos-11 timeout-minutes: 10 - steps: + - name: Connect private iap package + uses: webfactory/ssh-agent@v0.8.0 + if: !env.STUB_IAP + with: + ssh-private-key: ${{ secrets.M3_LIGHTMETER_IAP_KEY }} + + - name: Override iap package with stub + if: env.STUB_IAP + run: | + echo "\ndependency_overrides:\n m3_lightmeter_iap:\n path: iap" >> pubspec.yaml + - uses: actions/checkout@v3 with: submodules: recursive @@ -25,7 +39,7 @@ jobs: - uses: subosito/flutter-action@v2 with: channel: "stable" - flutter-version: '3.10.0' + flutter-version: "3.10.0" - name: Prepare flutter project run: | @@ -37,4 +51,4 @@ jobs: run: flutter analyze lib --fatal-infos - name: Run tests - run: flutter test \ No newline at end of file + run: flutter test diff --git a/.vscode/launch.json b/.vscode/launch.json index 822d34d..6cbc2bc 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -31,6 +31,7 @@ { "name": "prod (android)", "request": "launch", + //"flutterMode": "release", "type": "dart", "args": [ "--flavor", @@ -43,6 +44,7 @@ { "name": "prod (ios)", "request": "launch", + //"flutterMode": "release", "type": "dart", "args": [ "--flavor", diff --git a/README.md b/README.md index f629aa2..c4af94a 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,22 @@ Out of the box Firebase Crashlytics won't work. If you want to add Crashlytics t ### 3. Get packages -Fetch all the neccessary dependencies and generate translation files by running the following commands: +As part of the app's functionallity is in the private repo, you have to replace these lines in _pubspec.yaml_: + +```yaml +m3_lightmeter_iap: + git: + url: "https://github.com/vodemn/m3_lightmeter_iap" + ref: main +``` +with these: +```yaml +m3_lightmeter_iap: + path: iap +``` +and run `flutter pub get` from the _iap/_ folder. + +Then you can fetch all the neccessary dependencies and generate translation files by running the following commands: ```console flutter pub get flutter pub run intl_utils:generate @@ -69,4 +84,4 @@ Apple does not provide API for reading Lux stream form the ambient light sensor. ## Volume buttons action -This can be [implemented](https://stackoverflow.com/questions/70161271/ios-override-hardware-volume-buttons-same-as-zello) but the app will be rejected due to [2.5.9](https://developer.apple.com/app-store/review/guidelines/#software-requirements) \ No newline at end of file +This can be [implemented](https://stackoverflow.com/questions/70161271/ios-override-hardware-volume-buttons-same-as-zello) but the app will be rejected due to [2.5.9](https://developer.apple.com/app-store/review/guidelines/#software-requirements) diff --git a/iap/.gitignore b/iap/.gitignore new file mode 100644 index 0000000..205884d --- /dev/null +++ b/iap/.gitignore @@ -0,0 +1,36 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ + +.fvm/ +*.properties +ios/Flutter/ +.flutter-plugins +.flutter-plugins-dependencies \ No newline at end of file diff --git a/iap/.metadata b/iap/.metadata new file mode 100644 index 0000000..acbef51 --- /dev/null +++ b/iap/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 9944297138845a94256f1cf37beb88ff9a8e811a + channel: stable + +project_type: package diff --git a/iap/LICENSE b/iap/LICENSE new file mode 100644 index 0000000..ba75c69 --- /dev/null +++ b/iap/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/iap/analysis_options.yaml b/iap/analysis_options.yaml new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/iap/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/iap/lib/m3_lightmeter_iap.dart b/iap/lib/m3_lightmeter_iap.dart new file mode 100644 index 0000000..8fe8aa5 --- /dev/null +++ b/iap/lib/m3_lightmeter_iap.dart @@ -0,0 +1,30 @@ +library m3_lightmeter_iap; + +import 'package:flutter/material.dart'; +import 'package:m3_lightmeter_iap/src/providers/equipment_profile_provider.dart'; +import 'package:m3_lightmeter_iap/src/providers/iap_products_provider.dart'; + +export 'src/data/models/iap_product.dart'; + +export 'src/providers/equipment_profile_provider.dart' hide EquipmentProfilesAspect; +export 'src/providers/iap_products_provider.dart'; + +class IAPProviders extends StatelessWidget { + final Object sharedPreferences; + final Widget child; + + const IAPProviders({ + required this.sharedPreferences, + required this.child, + super.key, + }); + + @override + Widget build(BuildContext context) { + return IAPProductsProvider( + child: EquipmentProfileProvider( + child: child, + ), + ); + } +} diff --git a/iap/lib/src/data/models/iap_product.dart b/iap/lib/src/data/models/iap_product.dart new file mode 100644 index 0000000..706e3f1 --- /dev/null +++ b/iap/lib/src/data/models/iap_product.dart @@ -0,0 +1,5 @@ +enum IAPProductType { paidFeatures } + +class IAPProduct { + IAPProduct(); +} diff --git a/iap/lib/src/providers/equipment_profile_provider.dart b/iap/lib/src/providers/equipment_profile_provider.dart new file mode 100644 index 0000000..4f7aa0f --- /dev/null +++ b/iap/lib/src/providers/equipment_profile_provider.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; + +class EquipmentProfileProvider extends StatefulWidget { + final Widget child; + + const EquipmentProfileProvider({required this.child, super.key}); + + static EquipmentProfileProviderState of(BuildContext context) { + return context.findAncestorStateOfType()!; + } + + @override + State createState() => EquipmentProfileProviderState(); +} + +class EquipmentProfileProviderState extends State { + static const EquipmentProfile _defaultProfile = EquipmentProfile( + id: '', + name: '', + apertureValues: ApertureValue.values, + ndValues: NdValue.values, + shutterSpeedValues: ShutterSpeedValue.values, + isoValues: IsoValue.values, + ); + + @override + Widget build(BuildContext context) { + return EquipmentProfiles( + profiles: const [_defaultProfile], + selected: _defaultProfile, + child: widget.child, + ); + } + + void setProfile(EquipmentProfile data) {} + + void addProfile(String name) {} + + void updateProdile(EquipmentProfile data) {} + + void deleteProfile(EquipmentProfile data) {} +} + +enum EquipmentProfilesAspect { list, selected } + +class EquipmentProfiles extends InheritedModel { + const EquipmentProfiles({ + super.key, + required this.profiles, + required this.selected, + required super.child, + }); + + final List profiles; + final EquipmentProfile selected; + + static List of(BuildContext context) { + return InheritedModel.inheritFrom( + context, + aspect: EquipmentProfilesAspect.list, + )! + .profiles; + } + + static EquipmentProfile selectedOf(BuildContext context) { + return InheritedModel.inheritFrom( + context, + aspect: EquipmentProfilesAspect.selected, + )! + .selected; + } + + @override + bool updateShouldNotify(EquipmentProfiles oldWidget) => false; + + @override + bool updateShouldNotifyDependent(EquipmentProfiles oldWidget, Set dependencies) => false; +} diff --git a/iap/lib/src/providers/iap_products_provider.dart b/iap/lib/src/providers/iap_products_provider.dart new file mode 100644 index 0000000..4ea3c98 --- /dev/null +++ b/iap/lib/src/providers/iap_products_provider.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:m3_lightmeter_iap/src/data/models/iap_product.dart'; + +class IAPProductsProvider extends StatefulWidget { + final Widget child; + + const IAPProductsProvider({required this.child, super.key}); + + static IAPProductsProviderState of(BuildContext context) { + return context.findAncestorStateOfType()!; + } + + @override + State createState() => IAPProductsProviderState(); +} + +class IAPProductsProviderState extends State { + @override + Widget build(BuildContext context) { + return IAPProducts( + products: const [], + child: widget.child, + ); + } + + Future buy(IAPProductType type) async {} +} + +class IAPProducts extends InheritedModel { + final List products; + + const IAPProducts({ + required this.products, + required super.child, + super.key, + }); + + static IAPProduct? of(BuildContext context, IAPProductType type) => null; + + static bool isPurchased(BuildContext context, IAPProductType type) => false; + + @override + bool updateShouldNotify(IAPProducts oldWidget) => false; + + @override + bool updateShouldNotifyDependent(covariant IAPProducts oldWidget, Set dependencies) => false; +} diff --git a/iap/pubspec.yaml b/iap/pubspec.yaml new file mode 100644 index 0000000..6aed37e --- /dev/null +++ b/iap/pubspec.yaml @@ -0,0 +1,25 @@ +name: m3_lightmeter_iap +description: IAP stubs for the M3 Lightmeter app. +version: 0.2.0 +publish_to: 'none' + +environment: + sdk: '>=2.19.2 <3.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + m3_lightmeter_resources: + git: + url: "https://github.com/vodemn/m3_lightmeter_resources" + ref: main + shared_preferences: 2.2.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + +flutter: + uses-material-design: true diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index a858962..73ce339 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -237,6 +237,7 @@ }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -268,6 +269,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -371,7 +373,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 74JQ9DBXY6; + DEVELOPMENT_TEAM = 489Z6UQMGN; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -500,7 +502,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 74JQ9DBXY6; + DEVELOPMENT_TEAM = 489Z6UQMGN; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -523,7 +525,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 74JQ9DBXY6; + DEVELOPMENT_TEAM = 489Z6UQMGN; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -600,7 +602,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 74JQ9DBXY6; + DEVELOPMENT_TEAM = 489Z6UQMGN; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -675,7 +677,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 74JQ9DBXY6; + DEVELOPMENT_TEAM = 489Z6UQMGN; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -747,7 +749,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 74JQ9DBXY6; + DEVELOPMENT_TEAM = 489Z6UQMGN; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/lib/application.dart b/lib/application.dart index 1249ed9..38ef0bf 100644 --- a/lib/application.dart +++ b/lib/application.dart @@ -10,11 +10,11 @@ import 'package:lightmeter/data/shared_prefs_service.dart'; import 'package:lightmeter/data/volume_events_service.dart'; import 'package:lightmeter/environment.dart'; import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/providers/equipment_profile_provider.dart'; import 'package:lightmeter/providers/services_provider.dart'; import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/screens/metering/flow_metering.dart'; import 'package:lightmeter/screens/settings/flow_settings.dart'; +import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:platform/platform.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -32,16 +32,18 @@ class Application extends StatelessWidget { ]), builder: (_, snapshot) { if (snapshot.data != null) { - return ServicesProvider( - caffeineService: const CaffeineService(), - environment: env.copyWith(hasLightSensor: snapshot.data![1] as bool), - hapticsService: const HapticsService(), - lightSensorService: const LightSensorService(LocalPlatform()), - permissionsService: const PermissionsService(), - userPreferencesService: UserPreferencesService(snapshot.data![0] as SharedPreferences), - volumeEventsService: const VolumeEventsService(LocalPlatform()), - child: UserPreferencesProvider( - child: EquipmentProfileProvider( + return IAPProviders( + sharedPreferences: snapshot.data![0] as SharedPreferences, + child: ServicesProvider( + caffeineService: const CaffeineService(), + environment: env.copyWith(hasLightSensor: snapshot.data![1] as bool), + hapticsService: const HapticsService(), + lightSensorService: const LightSensorService(LocalPlatform()), + permissionsService: const PermissionsService(), + userPreferencesService: + UserPreferencesService(snapshot.data![0] as SharedPreferences), + volumeEventsService: const VolumeEventsService(LocalPlatform()), + child: UserPreferencesProvider( child: Builder( builder: (context) { final theme = UserPreferencesProvider.themeOf(context); diff --git a/lib/data/models/exposure_pair.dart b/lib/data/models/exposure_pair.dart index 9df3ada..d3aa823 100644 --- a/lib/data/models/exposure_pair.dart +++ b/lib/data/models/exposure_pair.dart @@ -8,4 +8,16 @@ class ExposurePair { @override String toString() => '$aperture - $shutterSpeed'; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + return other is ExposurePair && + other.aperture == aperture && + other.shutterSpeed == shutterSpeed; + } + + @override + int get hashCode => Object.hash(aperture, shutterSpeed, runtimeType); } diff --git a/lib/data/models/film.dart b/lib/data/models/film.dart index 2da1c9b..ab651e8 100644 --- a/lib/data/models/film.dart +++ b/lib/data/models/film.dart @@ -13,6 +13,8 @@ double log10polynomian( ) => a * pow(log10(x), 2) + b * log10(x) + c; +typedef ReciprocityFailureBuilder = ShutterSpeedValue Function(ShutterSpeedValue shutterSpeed); + /// Only Ilford films have reciprocity formulas provided by the manufacturer: /// https://www.ilfordphoto.com/wp/wp-content/uploads/2017/06/Reciprocity-Failure-Compensation.pdf /// diff --git a/lib/data/models/metering_screen_layout_config.dart b/lib/data/models/metering_screen_layout_config.dart index 0aae055..7802195 100644 --- a/lib/data/models/metering_screen_layout_config.dart +++ b/lib/data/models/metering_screen_layout_config.dart @@ -1,9 +1,14 @@ -enum MeteringScreenLayoutFeature { extremeExposurePairs, filmPicker, histogram } +enum MeteringScreenLayoutFeature { + extremeExposurePairs, + filmPicker, + histogram, + equipmentProfiles, +} typedef MeteringScreenLayoutConfig = Map; extension MeteringScreenLayoutConfigJson on MeteringScreenLayoutConfig { - static MeteringScreenLayoutConfig fromJson(Map data) => + static MeteringScreenLayoutConfig fromJson(Map data) => { for (final f in MeteringScreenLayoutFeature.values) f: data[f.index.toString()] as bool? ?? true diff --git a/lib/data/shared_prefs_service.dart b/lib/data/shared_prefs_service.dart index 8ae5487..812f0e6 100644 --- a/lib/data/shared_prefs_service.dart +++ b/lib/data/shared_prefs_service.dart @@ -95,6 +95,7 @@ class UserPreferencesService { ); } else { return { + MeteringScreenLayoutFeature.equipmentProfiles: true, MeteringScreenLayoutFeature.extremeExposurePairs: true, MeteringScreenLayoutFeature.filmPicker: true, MeteringScreenLayoutFeature.histogram: true, @@ -147,10 +148,4 @@ class UserPreferencesService { orElse: () => Film.values.first, ); set film(Film value) => _sharedPreferences.setString(filmKey, value.name); - - String get selectedEquipmentProfileId => ''; // coverage:ignore-line - set selectedEquipmentProfileId(String id) {} // coverage:ignore-line - - List get equipmentProfiles => []; // coverage:ignore-line - set equipmentProfiles(List profiles) {} // coverage:ignore-line } diff --git a/lib/features.dart b/lib/features.dart deleted file mode 100644 index deede30..0000000 --- a/lib/features.dart +++ /dev/null @@ -1,3 +0,0 @@ -class FeaturesConfig { - static const bool equipmentProfilesEnabled = false; -} diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 1bfe8ed..925d280 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -54,6 +54,7 @@ "isoValuesFilterDescription": "Select the ISO values to display. These may be your most commonly used values or those supported by your camera.", "equipmentProfile": "Equipment profile", "equipmentProfiles": "Equipment profiles", + "tapToAdd": "Tap to add", "general": "General", "keepScreenOn": "Keep screen on", "haptics": "Haptics", @@ -86,4 +87,4 @@ } } } -} \ No newline at end of file +} diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index f75d76b..c199d1e 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -42,6 +42,7 @@ "film": "Pellicule", "equipment": "Équipement", "equipmentProfileName": "Nom du profil de l'équipement", + "tapToAdd": "Appuie pour ajouter", "equipmentProfileNameHint": "Praktica MTL5B", "equipmentProfileAllValues": "Tout", "apertureValues": "Valeurs Aperture", @@ -86,4 +87,4 @@ } } } -} \ No newline at end of file +} diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index 3a67fc2..d20c4bd 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -54,6 +54,7 @@ "isoValuesFilterDescription": "Выберите значения ISO для отображения. Это может быть наиболее часто используемые значения или значения, поддерживаемые вашей камерой.", "equipmentProfile": "Оборудование", "equipmentProfiles": "Профили оборудования", + "tapToAdd": "Нажмите, чтобы добавить", "general": "Общие", "keepScreenOn": "Запрет блокировки", "haptics": "Вибрация", @@ -86,4 +87,4 @@ } } } -} \ No newline at end of file +} diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb index 5d020cf..02bdf6c 100644 --- a/lib/l10n/intl_zh.arb +++ b/lib/l10n/intl_zh.arb @@ -54,6 +54,7 @@ "isoValuesFilterDescription": "选择要显示的 ISO。这些值可能是您最常用的值,也可能是相机支持的值。", "equipmentProfile": "设备配置", "equipmentProfiles": "设备配置", + "tapToAdd": "點擊添加", "general": "通用", "keepScreenOn": "保持屏幕常亮", "haptics": "震动", diff --git a/lib/providers/equipment_profile_provider.dart b/lib/providers/equipment_profile_provider.dart deleted file mode 100644 index c0294fa..0000000 --- a/lib/providers/equipment_profile_provider.dart +++ /dev/null @@ -1,139 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lightmeter/providers/services_provider.dart'; -import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; -import 'package:uuid/uuid.dart'; - -// TODO(@vodemn): This will be removed in #89 -class EquipmentProfileProvider extends StatefulWidget { - final Widget child; - - const EquipmentProfileProvider({required this.child, super.key}); - - static EquipmentProfileProviderState of(BuildContext context) { - return context.findAncestorStateOfType()!; - } - - @override - State createState() => EquipmentProfileProviderState(); -} - -class EquipmentProfileProviderState extends State { - static const EquipmentProfile _defaultProfile = EquipmentProfile( - id: '', - name: '', - apertureValues: ApertureValue.values, - ndValues: NdValue.values, - shutterSpeedValues: ShutterSpeedValue.values, - isoValues: IsoValue.values, - ); - - List _customProfiles = []; - String _selectedId = ''; - - EquipmentProfile get _selectedProfile => _customProfiles.firstWhere( - (e) => e.id == _selectedId, - orElse: () { - ServicesProvider.of(context).userPreferencesService.selectedEquipmentProfileId = - _defaultProfile.id; - return _defaultProfile; - }, - ); - - @override - void initState() { - super.initState(); - _selectedId = ServicesProvider.of(context).userPreferencesService.selectedEquipmentProfileId; - _customProfiles = ServicesProvider.of(context).userPreferencesService.equipmentProfiles; - } - - @override - Widget build(BuildContext context) { - return EquipmentProfiles( - profiles: [_defaultProfile] + _customProfiles, - selected: _selectedProfile, - child: widget.child, - ); - } - - void setProfile(EquipmentProfile data) { - setState(() { - _selectedId = data.id; - }); - ServicesProvider.of(context).userPreferencesService.selectedEquipmentProfileId = - _selectedProfile.id; - } - - /// Creates a default equipment profile - void addProfile(String name) { - _customProfiles.add( - EquipmentProfile( - id: const Uuid().v1(), - name: name, - apertureValues: ApertureValue.values, - ndValues: NdValue.values, - shutterSpeedValues: ShutterSpeedValue.values, - isoValues: IsoValue.values, - ), - ); - _refreshSavedProfiles(); - } - - void updateProdile(EquipmentProfile data) { - final indexToUpdate = _customProfiles.indexWhere((element) => element.id == data.id); - if (indexToUpdate >= 0) { - _customProfiles[indexToUpdate] = data; - _refreshSavedProfiles(); - } - } - - void deleteProfile(EquipmentProfile data) { - _customProfiles.remove(data); - _refreshSavedProfiles(); - } - - void _refreshSavedProfiles() { - ServicesProvider.of(context).userPreferencesService.equipmentProfiles = _customProfiles; - setState(() {}); - } -} - -// Copied from #89 -enum EquipmentProfilesAspect { list, selected } - -class EquipmentProfiles extends InheritedModel { - const EquipmentProfiles({ - super.key, - required this.profiles, - required this.selected, - required super.child, - }); - - final List profiles; - final EquipmentProfile selected; - - static List of(BuildContext context) { - return InheritedModel.inheritFrom( - context, - aspect: EquipmentProfilesAspect.list, - )! - .profiles; - } - - static EquipmentProfile selectedOf(BuildContext context) { - return InheritedModel.inheritFrom( - context, - aspect: EquipmentProfilesAspect.selected, - )! - .selected; - } - - @override - bool updateShouldNotify(EquipmentProfiles oldWidget) => false; - - @override - bool updateShouldNotifyDependent( - EquipmentProfiles oldWidget, - Set dependencies, - ) => - false; -} diff --git a/lib/res/dimens.dart b/lib/res/dimens.dart index a1bb780..e3eed95 100644 --- a/lib/res/dimens.dart +++ b/lib/res/dimens.dart @@ -14,7 +14,6 @@ class Dimens { static const double grid48 = 48; static const double grid56 = 56; static const double grid72 = 72; - static const double grid168 = 168; static const double paddingS = 8; static const double paddingM = 16; @@ -30,6 +29,8 @@ class Dimens { static const double enabledOpacity = 1.0; static const double disabledOpacity = 0.38; + static const double sliverAppBarExpandedHeight = 168; + // TopBar static const double readingContainerDoubleValueHeight = 128; static const double readingContainerSingleValueHeight = 76; diff --git a/lib/screens/metering/components/camera_container/bloc_container_camera.dart b/lib/screens/metering/components/camera_container/bloc_container_camera.dart index 991bdf6..0313391 100644 --- a/lib/screens/metering/components/camera_container/bloc_container_camera.dart +++ b/lib/screens/metering/components/camera_container/bloc_container_camera.dart @@ -18,7 +18,7 @@ import 'package:lightmeter/screens/metering/components/camera_container/event_co import 'package:lightmeter/screens/metering/components/camera_container/models/camera_error_type.dart'; import 'package:lightmeter/screens/metering/components/camera_container/state_container_camera.dart'; import 'package:lightmeter/screens/metering/components/shared/ev_source_base/bloc_base_ev_source.dart'; -import 'package:lightmeter/utils/log_2.dart'; +import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class CameraContainerBloc extends EvSourceBlocBase { final MeteringInteractor _meteringInteractor; diff --git a/lib/screens/metering/components/camera_container/widget_container_camera.dart b/lib/screens/metering/components/camera_container/widget_container_camera.dart index c510bc8..0514d92 100644 --- a/lib/screens/metering/components/camera_container/widget_container_camera.dart +++ b/lib/screens/metering/components/camera_container/widget_container_camera.dart @@ -5,7 +5,6 @@ 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/user_preferences_provider.dart'; import 'package:lightmeter/res/dimens.dart'; @@ -110,7 +109,10 @@ class CameraContainer extends StatelessWidget { double _meteringContainerHeight(BuildContext context) { double enabledFeaturesHeight = 0; - if (FeaturesConfig.equipmentProfilesEnabled) { + if (UserPreferencesProvider.meteringScreenFeatureOf( + context, + MeteringScreenLayoutFeature.equipmentProfiles, + )) { enabledFeaturesHeight += Dimens.readingContainerSingleValueHeight; enabledFeaturesHeight += Dimens.paddingS; } @@ -133,7 +135,7 @@ class CameraContainer extends StatelessWidget { } double _cameraPreviewHeight(BuildContext context) { - return ((MediaQuery.of(context).size.width - Dimens.grid8 - 2 * Dimens.paddingM) / 2) / + return ((MediaQuery.sizeOf(context).width - Dimens.grid8 - 2 * Dimens.paddingM) / 2) / PlatformConfig.cameraPreviewAspectRatio; } } diff --git a/lib/screens/metering/components/light_sensor_container/bloc_container_light_sensor.dart b/lib/screens/metering/components/light_sensor_container/bloc_container_light_sensor.dart index 23cd796..c76727b 100644 --- a/lib/screens/metering/components/light_sensor_container/bloc_container_light_sensor.dart +++ b/lib/screens/metering/components/light_sensor_container/bloc_container_light_sensor.dart @@ -10,7 +10,7 @@ import 'package:lightmeter/screens/metering/communication/state_communication_me import 'package:lightmeter/screens/metering/components/light_sensor_container/event_container_light_sensor.dart'; import 'package:lightmeter/screens/metering/components/light_sensor_container/state_container_light_sensor.dart'; import 'package:lightmeter/screens/metering/components/shared/ev_source_base/bloc_base_ev_source.dart'; -import 'package:lightmeter/utils/log_2.dart'; +import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class LightSensorContainerBloc extends EvSourceBlocBase { diff --git a/lib/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart b/lib/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart index 71b293e..3f33d03 100644 --- a/lib/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart +++ b/lib/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/data/models/exposure_pair.dart'; +import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/res/dimens.dart'; -import 'package:lightmeter/screens/metering/components/shared/exposure_pairs_list/components/empty_exposure_pairs_list/widget_list_exposure_pairs_empty.dart'; import 'package:lightmeter/screens/metering/components/shared/exposure_pairs_list/components/exposure_pairs_list_item/widget_item_list_exposure_pairs.dart'; +import 'package:lightmeter/screens/shared/icon_placeholder/widget_icon_placeholder.dart'; class ExposurePairsList extends StatelessWidget { final List exposurePairs; @@ -15,7 +16,10 @@ class ExposurePairsList extends StatelessWidget { return AnimatedSwitcher( duration: Dimens.switchDuration, child: exposurePairs.isEmpty - ? const EmptyExposurePairsList() + ? IconPlaceholder( + icon: Icons.not_interested, + text: S.of(context).noExposurePairs, + ) : Stack( alignment: Alignment.center, children: [ diff --git a/lib/screens/metering/components/shared/readings_container/widget_container_readings.dart b/lib/screens/metering/components/shared/readings_container/widget_container_readings.dart index d31380c..c9f6f54 100644 --- a/lib/screens/metering/components/shared/readings_container/widget_container_readings.dart +++ b/lib/screens/metering/components/shared/readings_container/widget_container_readings.dart @@ -2,13 +2,12 @@ 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/user_preferences_provider.dart'; import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/screens/metering/components/shared/readings_container/components/animated_dialog_picker/widget_picker_dialog_animated.dart'; import 'package:lightmeter/screens/metering/components/shared/readings_container/components/reading_value_container/widget_container_reading_value.dart'; +import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class ReadingsContainer extends StatelessWidget { @@ -38,7 +37,10 @@ class ReadingsContainer extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (FeaturesConfig.equipmentProfilesEnabled) ...[ + if (UserPreferencesProvider.meteringScreenFeatureOf( + context, + MeteringScreenLayoutFeature.equipmentProfiles, + )) ...[ const _EquipmentProfilePicker(), const _InnerPadding(), ], diff --git a/lib/screens/metering/screen_metering.dart b/lib/screens/metering/screen_metering.dart index ba4dbea..e9a77b5 100644 --- a/lib/screens/metering/screen_metering.dart +++ b/lib/screens/metering/screen_metering.dart @@ -6,7 +6,6 @@ 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/providers/equipment_profile_provider.dart'; import 'package:lightmeter/providers/services_provider.dart'; import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/screens/metering/bloc_metering.dart'; @@ -17,6 +16,7 @@ import 'package:lightmeter/screens/metering/event_metering.dart'; import 'package:lightmeter/screens/metering/state_metering.dart'; import 'package:lightmeter/screens/metering/utils/listener_metering_layout_feature.dart'; import 'package:lightmeter/screens/metering/utils/listsner_equipment_profiles.dart'; +import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class MeteringScreen extends StatelessWidget { @@ -31,7 +31,7 @@ class MeteringScreen extends StatelessWidget { children: [ Expanded( child: BlocBuilder( - builder: (_, state) => _MeteringContainerBuidler( + builder: (_, state) => MeteringContainerBuidler( ev: state is MeteringDataState ? state.ev : null, film: state.film, iso: state.iso, @@ -80,15 +80,25 @@ class _InheritedListeners extends StatelessWidget { child: MeteringScreenLayoutFeatureListener( feature: MeteringScreenLayoutFeature.filmPicker, onDidChangeDependencies: (value) { - if (!value) context.read().add(const FilmChangedEvent(Film.other())); + if (!value) { + context.read().add(const FilmChangedEvent(Film.other())); + } }, - child: child, + child: MeteringScreenLayoutFeatureListener( + feature: MeteringScreenLayoutFeature.equipmentProfiles, + onDidChangeDependencies: (value) { + if (!value) { + EquipmentProfileProvider.of(context).setProfile(EquipmentProfiles.of(context).first); + } + }, + child: child, + ), ), ); } } -class _MeteringContainerBuidler extends StatelessWidget { +class MeteringContainerBuidler extends StatelessWidget { final double? ev; final Film film; final IsoValue iso; @@ -97,7 +107,7 @@ class _MeteringContainerBuidler extends StatelessWidget { final ValueChanged onIsoChanged; final ValueChanged onNdChanged; - const _MeteringContainerBuidler({ + const MeteringContainerBuidler({ required this.ev, required this.film, required this.iso, @@ -109,7 +119,14 @@ class _MeteringContainerBuidler extends StatelessWidget { @override Widget build(BuildContext context) { - final exposurePairs = ev != null ? buildExposureValues(context, ev!, film) : []; + final exposurePairs = ev != null + ? buildExposureValues( + ev!, + UserPreferencesProvider.stopTypeOf(context), + EquipmentProfiles.selectedOf(context), + film, + ) + : []; final fastest = exposurePairs.isNotEmpty ? exposurePairs.first : null; final slowest = exposurePairs.isNotEmpty ? exposurePairs.last : null; // Doubled build here when switching evSourceType. As new source bloc fires a new state on init @@ -138,39 +155,28 @@ class _MeteringContainerBuidler extends StatelessWidget { ); } - List buildExposureValues(BuildContext context, double ev, Film film) { + @visibleForTesting + static List buildExposureValues( + double ev, + StopType stopType, + EquipmentProfile equipmentProfile, + Film film, + ) { if (ev.isNaN || ev.isInfinite) { return List.empty(); } /// Depending on the `stopType` the exposure pairs list length is multiplied by 1,2 or 3 - final StopType stopType = UserPreferencesProvider.stopTypeOf(context); final int evSteps = (ev * (stopType.index + 1)).round(); - final EquipmentProfile equipmentProfile = EquipmentProfiles.selectedOf(context); - final List apertureValues = - equipmentProfile.apertureValues.whereStopType(stopType); - final List shutterSpeedValues = - equipmentProfile.shutterSpeedValues.whereStopType(stopType); + final apertureValues = ApertureValue.values.whereStopType(stopType); + final shutterSpeedValues = ShutterSpeedValue.values.whereStopType(stopType); /// Basically we use 1" shutter speed as an anchor point for building the exposure pairs list. /// But user can exclude this value from the list using custom equipment profile. /// So we have to restore the index of the anchor value. - const ShutterSpeedValue anchorShutterSpeed = ShutterSpeedValue(1, false, StopType.full); - int anchorIndex = shutterSpeedValues.indexOf(anchorShutterSpeed); - if (anchorIndex < 0) { - final filteredFullList = ShutterSpeedValue.values.whereStopType(stopType); - final customListStartIndex = filteredFullList.indexOf(shutterSpeedValues.first); - final fullListAnchor = filteredFullList.indexOf(anchorShutterSpeed); - if (customListStartIndex < fullListAnchor) { - /// This means, that user excluded anchor value at the end, - /// i.e. all shutter speed values are shorter than 1". - anchorIndex = fullListAnchor - customListStartIndex; - } else { - /// In case user excludes anchor value at the start, - /// we can do no adjustment. - } - } + const anchorShutterSpeed = ShutterSpeedValue(1, false, StopType.full); + final int anchorIndex = shutterSpeedValues.indexOf(anchorShutterSpeed); final int evOffset = anchorIndex - evSteps; late final int apertureOffset; @@ -189,10 +195,11 @@ class _MeteringContainerBuidler extends StatelessWidget { ) - max(apertureOffset, shutterSpeedOffset); - if (itemsCount < 0) { + if (itemsCount <= 0) { return List.empty(); } - return List.generate( + + final exposurePairs = List.generate( itemsCount, (index) => ExposurePair( apertureValues[index + apertureOffset], @@ -200,5 +207,30 @@ class _MeteringContainerBuidler extends StatelessWidget { ), growable: false, ); + + /// Full equipment profile, nothing to cut + if (equipmentProfile.id == "") { + return exposurePairs; + } + + final equipmentApertureValues = equipmentProfile.apertureValues.whereStopType(stopType); + final equipmentShutterSpeedValues = equipmentProfile.shutterSpeedValues.whereStopType(stopType); + + final startCutEV = max( + exposurePairs.first.aperture.difference(equipmentApertureValues.first), + exposurePairs.first.shutterSpeed.difference(equipmentShutterSpeedValues.first), + ); + final endCutEV = max( + equipmentApertureValues.last.difference(exposurePairs.last.aperture), + equipmentShutterSpeedValues.last.difference(exposurePairs.last.shutterSpeed), + ); + + final startCut = (startCutEV * (stopType.index + 1)).round().clamp(0, itemsCount); + final endCut = (endCutEV * (stopType.index + 1)).round().clamp(0, itemsCount); + + if (startCut > itemsCount - endCut) { + return const []; + } + return exposurePairs.sublist(startCut, itemsCount - endCut); } } diff --git a/lib/screens/metering/utils/equipment_profile_listener.dart b/lib/screens/metering/utils/equipment_profile_listener.dart new file mode 100644 index 0000000..68d03dc --- /dev/null +++ b/lib/screens/metering/utils/equipment_profile_listener.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; +import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; + +class EquipmentProfileListener extends StatefulWidget { + final ValueChanged onDidChangeDependencies; + final Widget child; + + const EquipmentProfileListener({ + required this.onDidChangeDependencies, + required this.child, + super.key, + }); + + @override + State createState() => _EquipmentProfileListenerState(); +} + +class _EquipmentProfileListenerState extends State { + @override + void didChangeDependencies() { + super.didChangeDependencies(); + widget.onDidChangeDependencies(EquipmentProfiles.selectedOf(context)); + } + + @override + Widget build(BuildContext context) { + return widget.child; + } +} diff --git a/lib/screens/metering/utils/listsner_equipment_profiles.dart b/lib/screens/metering/utils/listsner_equipment_profiles.dart index ec604ce..68d03dc 100644 --- a/lib/screens/metering/utils/listsner_equipment_profiles.dart +++ b/lib/screens/metering/utils/listsner_equipment_profiles.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:lightmeter/providers/equipment_profile_provider.dart'; +import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class EquipmentProfileListener extends StatefulWidget { diff --git a/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart index 1d9b7bf..e4a9e95 100644 --- a/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart +++ b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart @@ -71,6 +71,7 @@ class EquipmentProfileContainerState extends State mainAxisSize: MainAxisSize.min, children: [ ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM), title: Row( children: [ _AnimatedNameLeading(controller: _controller), @@ -163,7 +164,7 @@ class _AnimatedNameLeading extends AnimatedWidget { @override Widget build(BuildContext context) { return Padding( - padding: EdgeInsets.only(right: _progress.value * Dimens.grid24), + padding: EdgeInsets.only(right: _progress.value * Dimens.grid8), child: Icon( Icons.edit, size: _progress.value * Dimens.grid24, diff --git a/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart index 3120b38..0168608 100644 --- a/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart +++ b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart @@ -1,11 +1,12 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/providers/equipment_profile_provider.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/widget_container_equipment_profile.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/shared/icon_placeholder/widget_icon_placeholder.dart'; import 'package:lightmeter/screens/shared/sliver_screen/screen_sliver.dart'; +import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class EquipmentProfilesScreen extends StatefulWidget { @@ -44,30 +45,38 @@ class _EquipmentProfilesScreenState extends State { icon: const Icon(Icons.close), ), ], - slivers: [ - SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) => index > 0 - ? Padding( - padding: EdgeInsets.fromLTRB( - Dimens.paddingM, - index == 0 ? Dimens.paddingM : 0, - Dimens.paddingM, - Dimens.paddingM, - ), - child: EquipmentProfileContainer( - key: profileContainersKeys[index], - data: EquipmentProfiles.of(context)[index], - onExpand: () => _keepExpandedAt(index), - onUpdate: (profileData) => _updateProfileAt(profileData, index), - onDelete: () => _removeProfileAt(index), - ), - ) - : const SizedBox.shrink(), - childCount: profilesCount, - ), - ), - ], + slivers: profilesCount == 1 + ? [ + SliverFillRemaining( + hasScrollBody: false, + child: _EquipmentProfilesListPlaceholder(onTap: _addProfile), + ) + ] + : [ + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) => index > 0 // skip default + ? Padding( + padding: EdgeInsets.fromLTRB( + Dimens.paddingM, + index == 0 ? Dimens.paddingM : 0, + Dimens.paddingM, + Dimens.paddingM, + ), + child: EquipmentProfileContainer( + key: profileContainersKeys[index], + data: EquipmentProfiles.of(context)[index], + onExpand: () => _keepExpandedAt(index), + onUpdate: (profileData) => _updateProfileAt(profileData, index), + onDelete: () => _removeProfileAt(index), + ), + ) + : const SizedBox.shrink(), + childCount: profilesCount, + ), + ), + SliverToBoxAdapter(child: SizedBox(height: MediaQuery.paddingOf(context).bottom)), + ], ); } @@ -99,3 +108,32 @@ class _EquipmentProfilesScreenState extends State { }); } } + +class _EquipmentProfilesListPlaceholder extends StatelessWidget { + final VoidCallback onTap; + + const _EquipmentProfilesListPlaceholder({required this.onTap}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: Dimens.sliverAppBarExpandedHeight), + child: FractionallySizedBox( + widthFactor: 1 / 1.618, + child: Center( + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: onTap, + child: Padding( + padding: const EdgeInsets.all(Dimens.paddingL), + child: IconPlaceholder( + icon: Icons.add, + text: S.of(context).tapToAdd, + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/screens/settings/components/metering/components/equipment_profiles/widget_list_tile_equipment_profiles.dart b/lib/screens/settings/components/metering/components/equipment_profiles/widget_list_tile_equipment_profiles.dart index d1a6ef3..774e215 100644 --- a/lib/screens/settings/components/metering/components/equipment_profiles/widget_list_tile_equipment_profiles.dart +++ b/lib/screens/settings/components/metering/components/equipment_profiles/widget_list_tile_equipment_profiles.dart @@ -1,6 +1,10 @@ +import 'dart:developer'; + import 'package:flutter/material.dart'; import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart'; +import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class EquipmentProfilesListTile extends StatelessWidget { @@ -8,13 +12,31 @@ class EquipmentProfilesListTile extends StatelessWidget { @override Widget build(BuildContext context) { + final paidStatus = IAPProducts.productOf(context, IAPProductType.paidFeatures)?.status ?? + IAPProductStatus.pending; + log(paidStatus.toString()); return ListTile( leading: const Icon(Icons.camera), title: Text(S.of(context).equipmentProfiles), - onTap: () { - Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const EquipmentProfilesScreen()), - ); + onTap: switch (paidStatus) { + IAPProductStatus.purchased => () { + Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const EquipmentProfilesScreen()), + ); + }, + IAPProductStatus.pending => null, + _ => () { + IAPProductsProvider.of(context).buy(IAPProductType.paidFeatures); + }, + }, + trailing: switch (paidStatus) { + IAPProductStatus.purchasable => const Icon(Icons.lock), + IAPProductStatus.pending => const SizedBox( + height: Dimens.grid24, + width: Dimens.grid24, + child: CircularProgressIndicator(), + ), + _ => null, }, ); } diff --git a/lib/screens/settings/components/metering/components/metering_screen_layout/components/meterins_screen_layout_features_dialog/widget_dialog_metering_screen_layout_features.dart b/lib/screens/settings/components/metering/components/metering_screen_layout/components/meterins_screen_layout_features_dialog/widget_dialog_metering_screen_layout_features.dart index c60abab..a043a12 100644 --- a/lib/screens/settings/components/metering/components/metering_screen_layout/components/meterins_screen_layout_features_dialog/widget_dialog_metering_screen_layout_features.dart +++ b/lib/screens/settings/components/metering/components/metering_screen_layout/components/meterins_screen_layout_features_dialog/widget_dialog_metering_screen_layout_features.dart @@ -33,18 +33,10 @@ class _MeteringScreenLayoutFeaturesDialogState extends State SwitchListTile( - contentPadding: EdgeInsets.symmetric(horizontal: Dimens.dialogTitlePadding.left), - title: Text(_toStringLocalized(context, f)), - value: _features[f]!, - onChanged: (value) { - setState(() { - _features.update(f, (_) => value); - }); - }, - ), - ), + _featureListTile(MeteringScreenLayoutFeature.equipmentProfiles), + _featureListTile(MeteringScreenLayoutFeature.extremeExposurePairs), + _featureListTile(MeteringScreenLayoutFeature.filmPicker), + _featureListTile(MeteringScreenLayoutFeature.histogram), ], ), ), @@ -65,8 +57,23 @@ class _MeteringScreenLayoutFeaturesDialogState extends State value); + }); + }, + ); + } + String _toStringLocalized(BuildContext context, MeteringScreenLayoutFeature feature) { switch (feature) { + case MeteringScreenLayoutFeature.equipmentProfiles: + return S.of(context).equipmentProfiles; case MeteringScreenLayoutFeature.extremeExposurePairs: return S.of(context).meteringScreenFeatureExtremeExposurePairs; case MeteringScreenLayoutFeature.filmPicker: diff --git a/lib/screens/settings/components/metering/widget_settings_section_metering.dart b/lib/screens/settings/components/metering/widget_settings_section_metering.dart index a033cbd..9f86709 100644 --- a/lib/screens/settings/components/metering/widget_settings_section_metering.dart +++ b/lib/screens/settings/components/metering/widget_settings_section_metering.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:lightmeter/features.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/screens/settings/components/metering/components/calibration/widget_list_tile_calibration.dart'; import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/widget_list_tile_equipment_profiles.dart'; @@ -18,7 +17,7 @@ class MeteringSettingsSection extends StatelessWidget { StopTypeListTile(), CalibrationListTile(), MeteringScreenLayoutListTile(), - if (FeaturesConfig.equipmentProfilesEnabled) EquipmentProfilesListTile(), + EquipmentProfilesListTile(), ], ); } diff --git a/lib/screens/settings/screen_settings.dart b/lib/screens/settings/screen_settings.dart index 9332d3f..38256e8 100644 --- a/lib/screens/settings/screen_settings.dart +++ b/lib/screens/settings/screen_settings.dart @@ -50,6 +50,7 @@ class _SettingsScreenState extends State { ], ), ), + SliverToBoxAdapter(child: SizedBox(height: MediaQuery.paddingOf(context).bottom)), ], ), ); diff --git a/lib/screens/metering/components/shared/exposure_pairs_list/components/empty_exposure_pairs_list/widget_list_exposure_pairs_empty.dart b/lib/screens/shared/icon_placeholder/widget_icon_placeholder.dart similarity index 68% rename from lib/screens/metering/components/shared/exposure_pairs_list/components/empty_exposure_pairs_list/widget_list_exposure_pairs_empty.dart rename to lib/screens/shared/icon_placeholder/widget_icon_placeholder.dart index ea6728f..f11128a 100644 --- a/lib/screens/metering/components/shared/exposure_pairs_list/components/empty_exposure_pairs_list/widget_list_exposure_pairs_empty.dart +++ b/lib/screens/shared/icon_placeholder/widget_icon_placeholder.dart @@ -1,24 +1,30 @@ import 'package:flutter/material.dart'; -import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/res/dimens.dart'; -class EmptyExposurePairsList extends StatelessWidget { - const EmptyExposurePairsList({super.key}); +class IconPlaceholder extends StatelessWidget { + final IconData icon; + final String text; + + const IconPlaceholder({ + required this.icon, + required this.text, + super.key, + }); @override Widget build(BuildContext context) { return ConstrainedBox( - constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width / 2), + constraints: BoxConstraints(maxWidth: MediaQuery.sizeOf(context).width / 2), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( - Icons.not_interested, + icon, color: Theme.of(context).colorScheme.onBackground, ), const SizedBox(height: Dimens.grid8), Text( - S.of(context).noExposurePairs, + text, style: Theme.of(context) .textTheme .bodyMedium diff --git a/lib/screens/shared/sliver_screen/screen_sliver.dart b/lib/screens/shared/sliver_screen/screen_sliver.dart index d4542b1..20d2e66 100644 --- a/lib/screens/shared/sliver_screen/screen_sliver.dart +++ b/lib/screens/shared/sliver_screen/screen_sliver.dart @@ -24,7 +24,7 @@ class SliverScreen extends StatelessWidget { SliverAppBar( pinned: true, automaticallyImplyLeading: false, - expandedHeight: Dimens.grid168, + expandedHeight: Dimens.sliverAppBarExpandedHeight, flexibleSpace: FlexibleSpaceBar( centerTitle: false, titlePadding: const EdgeInsets.all(Dimens.paddingM), @@ -39,7 +39,6 @@ class SliverScreen extends StatelessWidget { actions: appBarActions, ), ...slivers, - SliverToBoxAdapter(child: SizedBox(height: MediaQuery.of(context).padding.bottom)), ], ), ), diff --git a/lib/utils/log_2.dart b/lib/utils/log_2.dart deleted file mode 100644 index 37bcc92..0000000 --- a/lib/utils/log_2.dart +++ /dev/null @@ -1,3 +0,0 @@ -import 'dart:math'; - -double log2(num x) => log(x) / log(2); diff --git a/pubspec.yaml b/pubspec.yaml index f8ba217..0a2bfb6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: lightmeter -description: A new Flutter project. +description: Lightmeter app inspired by Material 3 design system. publish_to: "none" version: 0.13.2+38 @@ -11,10 +11,10 @@ dependencies: bloc_concurrency: 0.2.2 camera: 0.10.5+2 clipboard: 0.1.3 - dynamic_color: 1.6.5 + dynamic_color: 1.6.6 exif: 3.1.4 - firebase_core: 2.13.0 - firebase_crashlytics: 3.3.1 + firebase_core: 2.14.0 + firebase_crashlytics: 3.3.3 flutter: sdk: flutter flutter_bloc: 8.1.3 @@ -23,22 +23,26 @@ dependencies: intl: 0.18.0 intl_utils: 2.8.2 light_sensor: 2.0.2 + m3_lightmeter_iap: + git: + url: "https://github.com/vodemn/m3_lightmeter_iap" + ref: main m3_lightmeter_resources: git: url: "https://github.com/vodemn/m3_lightmeter_resources" ref: main material_color_utilities: 0.2.0 - package_info_plus: 4.0.1 - permission_handler: 10.2.0 + package_info_plus: 4.0.2 + permission_handler: 10.4.3 platform: 3.1.0 - shared_preferences: 2.1.1 - url_launcher: 6.1.11 + shared_preferences: 2.2.0 + url_launcher: 6.1.12 uuid: 3.0.7 - vibration: 1.7.7 + vibration: 1.8.1 dev_dependencies: bloc_test: 9.1.3 - build_runner: ^2.1.7 + build_runner: 2.4.6 flutter_launcher_icons: 0.11.0 flutter_native_splash: 2.2.16 flutter_test: diff --git a/test/data/models/metering_screen_layout_config_test.dart b/test/data/models/metering_screen_layout_config_test.dart index d763b08..9e9393e 100644 --- a/test/data/models/metering_screen_layout_config_test.dart +++ b/test/data/models/metering_screen_layout_config_test.dart @@ -12,12 +12,31 @@ void main() { '0': true, '1': true, '2': true, + '3': true, }, ), { MeteringScreenLayoutFeature.extremeExposurePairs: true, MeteringScreenLayoutFeature.filmPicker: true, MeteringScreenLayoutFeature.histogram: true, + MeteringScreenLayoutFeature.equipmentProfiles: true, + }, + ); + }); + + test('Legacy (no histogram & equipment profiles)', () { + expect( + MeteringScreenLayoutConfigJson.fromJson( + { + '0': false, + '1': false, + }, + ), + { + MeteringScreenLayoutFeature.extremeExposurePairs: false, + MeteringScreenLayoutFeature.filmPicker: false, + MeteringScreenLayoutFeature.histogram: true, + MeteringScreenLayoutFeature.equipmentProfiles: true, }, ); }); @@ -26,28 +45,32 @@ void main() { expect( MeteringScreenLayoutConfigJson.fromJson( { - '0': true, - '1': true, + '0': false, + '1': false, + '2': false, }, ), { - MeteringScreenLayoutFeature.extremeExposurePairs: true, - MeteringScreenLayoutFeature.filmPicker: true, - MeteringScreenLayoutFeature.histogram: true, + MeteringScreenLayoutFeature.extremeExposurePairs: false, + MeteringScreenLayoutFeature.filmPicker: false, + MeteringScreenLayoutFeature.histogram: false, + MeteringScreenLayoutFeature.equipmentProfiles: true, }, ); }); }, ); - test('toJson', () { + test('toJson()', () { expect( { + MeteringScreenLayoutFeature.equipmentProfiles: true, MeteringScreenLayoutFeature.extremeExposurePairs: true, MeteringScreenLayoutFeature.filmPicker: true, MeteringScreenLayoutFeature.histogram: true, }.toJson(), { + '3': true, '0': true, '1': true, '2': true, diff --git a/test/data/shared_prefs_service_test.dart b/test/data/shared_prefs_service_test.dart index 692cb81..2a63fe5 100644 --- a/test/data/shared_prefs_service_test.dart +++ b/test/data/shared_prefs_service_test.dart @@ -193,6 +193,7 @@ void main() { { MeteringScreenLayoutFeature.extremeExposurePairs: true, MeteringScreenLayoutFeature.filmPicker: true, + MeteringScreenLayoutFeature.equipmentProfiles: true, MeteringScreenLayoutFeature.histogram: true, }, ); @@ -207,6 +208,7 @@ void main() { { MeteringScreenLayoutFeature.extremeExposurePairs: false, MeteringScreenLayoutFeature.filmPicker: true, + MeteringScreenLayoutFeature.equipmentProfiles: true, MeteringScreenLayoutFeature.histogram: true, }, ); @@ -216,18 +218,19 @@ void main() { when( () => sharedPreferences.setString( UserPreferencesService.meteringScreenLayoutKey, - """{"0":false,"1":true,"2":true}""", + """{"0":false,"1":true,"2":true,"3":true}""", ), ).thenAnswer((_) => Future.value(true)); service.meteringScreenLayout = { MeteringScreenLayoutFeature.extremeExposurePairs: false, MeteringScreenLayoutFeature.filmPicker: true, MeteringScreenLayoutFeature.histogram: true, + MeteringScreenLayoutFeature.equipmentProfiles: true, }; verify( () => sharedPreferences.setString( UserPreferencesService.meteringScreenLayoutKey, - """{"0":false,"1":true,"2":true}""", + """{"0":false,"1":true,"2":true,"3":true}""", ), ).called(1); }); diff --git a/test/screens/metering/screen_metering_test.dart b/test/screens/metering/screen_metering_test.dart new file mode 100644 index 0000000..bb50168 --- /dev/null +++ b/test/screens/metering/screen_metering_test.dart @@ -0,0 +1,966 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:lightmeter/data/models/exposure_pair.dart'; +import 'package:lightmeter/data/models/film.dart'; +import 'package:lightmeter/screens/metering/screen_metering.dart'; +import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; + +void main() { + const defaultEquipmentProfile = EquipmentProfile( + id: "", + name: 'Default', + apertureValues: ApertureValue.values, + ndValues: NdValue.values, + shutterSpeedValues: ShutterSpeedValue.values, + isoValues: IsoValue.values, + ); + + group('Empty list', () { + List exposurePairsFull(double ev) => MeteringContainerBuidler.buildExposureValues( + ev, + StopType.full, + defaultEquipmentProfile, + const Film.other(), + ); + + test('isNan', () { + expect(exposurePairsFull(double.nan), const []); + }); + + test('isInifinity', () { + expect(exposurePairsFull(double.infinity), const []); + }); + + test('Big ass number', () { + expect(exposurePairsFull(23), const []); + }); + }); + + group('Default equipment profile', () { + group("StopType.full", () { + List exposurePairsFull(double ev) => + MeteringContainerBuidler.buildExposureValues( + ev, + StopType.full, + defaultEquipmentProfile, + const Film.other(), + ); + + test('EV 1', () { + final exposurePairs = exposurePairsFull(1); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(5.6, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.3', () { + final exposurePairs = exposurePairsFull(1.3); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(5.6, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.5', () { + final exposurePairs = exposurePairsFull(1.5); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(4, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(8, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.7', () { + final exposurePairs = exposurePairsFull(1.7); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(4, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(8, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 2', () { + final exposurePairs = exposurePairsFull(2); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(4, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(8, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + }); + + group("StopType.half", () { + List exposurePairsFull(double ev) => + MeteringContainerBuidler.buildExposureValues( + ev, + StopType.half, + defaultEquipmentProfile, + const Film.other(), + ); + + test('EV 1', () { + final exposurePairs = exposurePairsFull(1); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(5.6, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.3', () { + final exposurePairs = exposurePairsFull(1.3); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(3, true, StopType.half), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(6.7, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.5', () { + final exposurePairs = exposurePairsFull(1.5); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(3, true, StopType.half), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(6.7, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.7', () { + final exposurePairs = exposurePairsFull(1.7); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(3, true, StopType.half), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(6.7, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 2', () { + final exposurePairs = exposurePairsFull(2); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(4, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(8, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + }); + + group("StopType.third", () { + List exposurePairsFull(double ev) => + MeteringContainerBuidler.buildExposureValues( + ev, + StopType.third, + defaultEquipmentProfile, + const Film.other(), + ); + + test('EV 1', () { + final exposurePairs = exposurePairsFull(1); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(5.6, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.3', () { + final exposurePairs = exposurePairsFull(1.3); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(2.5, true, StopType.third), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(6.3, StopType.third), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.5', () { + final exposurePairs = exposurePairsFull(1.5); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(3, true, StopType.third), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(7.1, StopType.third), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.7', () { + final exposurePairs = exposurePairsFull(1.7); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(3, true, StopType.third), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(7.1, StopType.third), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 2', () { + final exposurePairs = exposurePairsFull(2); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(4, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(8, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + }); + }); + + group('Shutter speed 1/1000-1/2"', () { + final equipmentProfile = EquipmentProfile( + id: "1", + name: 'Test1', + apertureValues: ApertureValue.values, + ndValues: NdValue.values, + shutterSpeedValues: ShutterSpeedValue.values.sublist( + ShutterSpeedValue.values.indexOf(const ShutterSpeedValue(1000, true, StopType.full)), + ShutterSpeedValue.values.indexOf(const ShutterSpeedValue(2, true, StopType.full)) + 1, + ), + isoValues: IsoValue.values, + ); + + group("StopType.full", () { + List exposurePairsFull(double ev) => + MeteringContainerBuidler.buildExposureValues( + ev, + StopType.full, + equipmentProfile, + const Film.other(), + ); + + test('EV 1', () { + final exposurePairs = exposurePairsFull(1); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.0, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(1.0, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + }); + + test('EV 1.3', () { + final exposurePairs = exposurePairsFull(1.3); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.0, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(1.0, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + }); + + test('EV 1.5', () { + final exposurePairs = exposurePairsFull(1.5); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.0, StopType.full), + ShutterSpeedValue(4, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(1.4, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + }); + + test('EV 1.7', () { + final exposurePairs = exposurePairsFull(1.7); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.0, StopType.full), + ShutterSpeedValue(4, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(1.4, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + }); + + test('EV 2', () { + final exposurePairs = exposurePairsFull(2); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.0, StopType.full), + ShutterSpeedValue(4, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(1.4, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + }); + }); + + group("StopType.half", () { + List exposurePairsFull(double ev) => + MeteringContainerBuidler.buildExposureValues( + ev, + StopType.half, + equipmentProfile, + const Film.other(), + ); + + test('EV 1', () { + final exposurePairs = exposurePairsFull(1); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.0, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(1.0, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + }); + + test('EV 1.3', () { + final exposurePairs = exposurePairsFull(1.3); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.0, StopType.full), + ShutterSpeedValue(3, true, StopType.half), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(1.2, StopType.half), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + }); + + test('EV 1.5', () { + final exposurePairs = exposurePairsFull(1.5); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.0, StopType.full), + ShutterSpeedValue(3, true, StopType.half), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(1.2, StopType.half), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + }); + + test('EV 1.7', () { + final exposurePairs = exposurePairsFull(1.7); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.0, StopType.full), + ShutterSpeedValue(3, true, StopType.half), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(1.2, StopType.half), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + }); + + test('EV 2', () { + final exposurePairs = exposurePairsFull(2); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.0, StopType.full), + ShutterSpeedValue(4, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(1.4, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + }); + }); + + group("StopType.third", () { + List exposurePairsFull(double ev) => + MeteringContainerBuidler.buildExposureValues( + ev, + StopType.third, + equipmentProfile, + const Film.other(), + ); + + test('EV 1', () { + final exposurePairs = exposurePairsFull(1); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.0, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(1.0, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + }); + + test('EV 1.3', () { + final exposurePairs = exposurePairsFull(1.3); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.0, StopType.full), + ShutterSpeedValue(2.5, true, StopType.third), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(1.1, StopType.third), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + }); + + test('EV 1.5', () { + final exposurePairs = exposurePairsFull(1.5); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.0, StopType.full), + ShutterSpeedValue(3, true, StopType.third), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(1.2, StopType.third), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + }); + + test('EV 1.7', () { + final exposurePairs = exposurePairsFull(1.7); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.0, StopType.full), + ShutterSpeedValue(3, true, StopType.third), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(1.2, StopType.third), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + }); + + test('EV 2', () { + final exposurePairs = exposurePairsFull(2); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.0, StopType.full), + ShutterSpeedValue(4, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(1.4, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + }); + }); + }); + + group('Shutter speed 2"-16"', () { + final equipmentProfile = EquipmentProfile( + id: "1", + name: 'Test1', + apertureValues: ApertureValue.values.sublist(4), + ndValues: NdValue.values, + shutterSpeedValues: ShutterSpeedValue.values.sublist( + ShutterSpeedValue.values.indexOf(const ShutterSpeedValue(2, false, StopType.full)), + ), + isoValues: IsoValue.values, + ); + + group("StopType.full", () { + List exposurePairsFull(double ev) => + MeteringContainerBuidler.buildExposureValues( + ev, + StopType.full, + equipmentProfile, + const Film.other(), + ); + + test('EV 1', () { + final exposurePairs = exposurePairsFull(1); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(2.0, StopType.full), + ShutterSpeedValue(2, false, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(5.6, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.3', () { + final exposurePairs = exposurePairsFull(1.3); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(2.0, StopType.full), + ShutterSpeedValue(2, false, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(5.6, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.5', () { + final exposurePairs = exposurePairsFull(1.5); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(2.8, StopType.full), + ShutterSpeedValue(2, false, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(8, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.7', () { + final exposurePairs = exposurePairsFull(1.7); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(2.8, StopType.full), + ShutterSpeedValue(2, false, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(8, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 2', () { + final exposurePairs = exposurePairsFull(2); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(2.8, StopType.full), + ShutterSpeedValue(2, false, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(8, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + }); + + group("StopType.half", () { + List exposurePairsFull(double ev) => + MeteringContainerBuidler.buildExposureValues( + ev, + StopType.half, + equipmentProfile, + const Film.other(), + ); + + test('EV 1', () { + final exposurePairs = exposurePairsFull(1); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(2.0, StopType.full), + ShutterSpeedValue(2, false, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(5.6, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.3', () { + final exposurePairs = exposurePairsFull(1.3); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(2.4, StopType.half), + ShutterSpeedValue(2, false, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(6.7, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.5', () { + final exposurePairs = exposurePairsFull(1.5); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(2.4, StopType.half), + ShutterSpeedValue(2, false, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(6.7, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.7', () { + final exposurePairs = exposurePairsFull(1.7); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(2.4, StopType.half), + ShutterSpeedValue(2, false, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(6.7, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 2', () { + final exposurePairs = exposurePairsFull(2); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(2.8, StopType.full), + ShutterSpeedValue(2, false, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(8, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + }); + + group("StopType.third", () { + List exposurePairsFull(double ev) => + MeteringContainerBuidler.buildExposureValues( + ev, + StopType.third, + equipmentProfile, + const Film.other(), + ); + + test('EV 1', () { + final exposurePairs = exposurePairsFull(1); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(2.0, StopType.full), + ShutterSpeedValue(2, false, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(5.6, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.3', () { + final exposurePairs = exposurePairsFull(1.3); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(2.2, StopType.full), + ShutterSpeedValue(2, false, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(6.3, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.5', () { + final exposurePairs = exposurePairsFull(1.5); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(2.4, StopType.full), + ShutterSpeedValue(2, false, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(7.1, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.7', () { + final exposurePairs = exposurePairsFull(1.7); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(2.4, StopType.full), + ShutterSpeedValue(2, false, StopType.third), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(7.1, StopType.third), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 2', () { + final exposurePairs = exposurePairsFull(2); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(2.8, StopType.full), + ShutterSpeedValue(2, false, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(8, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + }); + }); +} From da4fcfc85769f432e692d44bb9458857d22d9047 Mon Sep 17 00:00:00 2001 From: Vadim <44135514+vodemn@users.noreply.github.com> Date: Sat, 2 Sep 2023 12:45:57 +0200 Subject: [PATCH 07/23] Fixed IAP stub in workflows (#110) * try using script for iap stub * typo * typo * typo * removed working dir * added comment to stub_iap.sh * checkout first * increment build number by script * Update increment_build_number.sh * fixed iap repo * stub --- .github/scripts/increment_build_number.sh | 8 ++++++++ .github/scripts/stub_iap.sh | 2 ++ .github/workflows/build_apk.yml | 13 ++++++------- .github/workflows/create_release.yml | 15 +++++++-------- .github/workflows/pr_check.yml | 19 +++++++------------ iap/lib/src/data/models/iap_product.dart | 12 ++++++++++-- .../src/providers/iap_products_provider.dart | 2 +- 7 files changed, 41 insertions(+), 30 deletions(-) create mode 100644 .github/scripts/increment_build_number.sh create mode 100644 .github/scripts/stub_iap.sh diff --git a/.github/scripts/increment_build_number.sh b/.github/scripts/increment_build_number.sh new file mode 100644 index 0000000..520455f --- /dev/null +++ b/.github/scripts/increment_build_number.sh @@ -0,0 +1,8 @@ +export newVersion="$1" + +if [[ -n "$newVersion" ]]; then + #https://stackoverflow.com/a/30214769/13167574 + perl -i -pe 's/^(version:\s+)(\d+\.\d+\.\d+)(\+)(\d+)$/$1.$ENV{'newVersion'}.$3.($4+1)/e' pubspec.yaml +else + echo "argument error" +fi diff --git a/.github/scripts/stub_iap.sh b/.github/scripts/stub_iap.sh new file mode 100644 index 0000000..210e1ea --- /dev/null +++ b/.github/scripts/stub_iap.sh @@ -0,0 +1,2 @@ +# https://unix.stackexchange.com/questions/435708/regex-multiline-pattern-and-substitution-replacement +perl -0777 -i -pe 's/( m3_lightmeter_iap:\n)( git:\n url: "https:\/\/github.com\/vodemn\/m3_lightmeter_iap"\n ref: main)/$1 path: iap/sg' pubspec.yaml \ No newline at end of file diff --git a/.github/workflows/build_apk.yml b/.github/workflows/build_apk.yml index e61b38b..6acb464 100644 --- a/.github/workflows/build_apk.yml +++ b/.github/workflows/build_apk.yml @@ -27,6 +27,10 @@ jobs: runs-on: macos-11 timeout-minutes: 15 steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + - name: Connect private iap package uses: webfactory/ssh-agent@v0.8.0 if: ${{ inputs.include-iap }} @@ -35,13 +39,8 @@ jobs: - name: Override iap package with stub if: ${{ !inputs.include-iap }} - run: | - echo "\ndependency_overrides:\n m3_lightmeter_iap:\n path: iap" >> pubspec.yaml - - - uses: actions/checkout@v3 - with: - submodules: recursive - + run: bash ./.github/scripts/stub_iap.sh + - uses: actions/setup-java@v2 with: distribution: "zulu" diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml index e0281e3..69a672b 100644 --- a/.github/workflows/create_release.yml +++ b/.github/workflows/create_release.yml @@ -46,6 +46,10 @@ jobs: runs-on: macos-11 timeout-minutes: 30 steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + - name: Connect private iap package uses: webfactory/ssh-agent@v0.8.0 if: ${{ inputs.include-iap }} @@ -54,12 +58,7 @@ jobs: - name: Override iap package with stub if: ${{ !inputs.include-iap }} - run: | - echo "\ndependency_overrides:\n m3_lightmeter_iap:\n path: iap" >> pubspec.yaml - - - uses: actions/checkout@v3 - with: - submodules: recursive + run: bash ./.github/scripts/stub_iap.sh - uses: actions/setup-java@v3 with: @@ -98,7 +97,7 @@ jobs: # Therefore here we have to increment it as well to build an apk with the same build number. - name: Increment build number & replace version number if: ${{ inputs.github-release }} - run: perl -i -pe 's/^(version:\s+)(\d+\.\d+\.\d+)(\+)(\d+)$/$1."${{ github.event.inputs.version }}".$3.($4+1)/e' pubspec.yaml + run: bash ./.github/scripts/increment_build_number.sh ${{ github.event.inputs.version }} - name: Install Flutter uses: subosito/flutter-action@v2 @@ -160,7 +159,7 @@ jobs: submodules: recursive - name: Increment build number & replace version number - run: perl -i -pe 's/^(version:\s+)(\d+\.\d+\.\d+)(\+)(\d+)$/$1."${{ github.event.inputs.version }}".$3.($4+1)/e' pubspec.yaml + run: bash ./.github/scripts/increment_build_number.sh ${{ github.event.inputs.version }} - name: Commit changes run: | diff --git a/.github/workflows/pr_check.yml b/.github/workflows/pr_check.yml index e6f0294..70e6fa4 100644 --- a/.github/workflows/pr_check.yml +++ b/.github/workflows/pr_check.yml @@ -11,30 +11,25 @@ on: pull_request: branches: ["main"] -env: - # Stub iap package if this worlflow is running from the PR from a fork - STUB_IAP: ${{ github.event.pull_request.head.repo.full_name != github.repository }} - jobs: analyze_and_test: name: Analyze & test runs-on: macos-11 timeout-minutes: 10 steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + - name: Connect private iap package uses: webfactory/ssh-agent@v0.8.0 - if: !env.STUB_IAP + if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} with: ssh-private-key: ${{ secrets.M3_LIGHTMETER_IAP_KEY }} - name: Override iap package with stub - if: env.STUB_IAP - run: | - echo "\ndependency_overrides:\n m3_lightmeter_iap:\n path: iap" >> pubspec.yaml - - - uses: actions/checkout@v3 - with: - submodules: recursive + if: ${{ github.event.pull_request.head.repo.full_name != github.repository }} + run: bash ./.github/scripts/stub_iap.sh - uses: subosito/flutter-action@v2 with: diff --git a/iap/lib/src/data/models/iap_product.dart b/iap/lib/src/data/models/iap_product.dart index 706e3f1..ba24c17 100644 --- a/iap/lib/src/data/models/iap_product.dart +++ b/iap/lib/src/data/models/iap_product.dart @@ -1,5 +1,13 @@ +enum IAPProductStatus { + purchasable, + pending, + purchased, +} + enum IAPProductType { paidFeatures } -class IAPProduct { - IAPProduct(); +abstract class IAPProduct { + const IAPProduct._(); + + IAPProductStatus get status => IAPProductStatus.purchasable; } diff --git a/iap/lib/src/providers/iap_products_provider.dart b/iap/lib/src/providers/iap_products_provider.dart index 4ea3c98..3014cd9 100644 --- a/iap/lib/src/providers/iap_products_provider.dart +++ b/iap/lib/src/providers/iap_products_provider.dart @@ -35,7 +35,7 @@ class IAPProducts extends InheritedModel { super.key, }); - static IAPProduct? of(BuildContext context, IAPProductType type) => null; + static IAPProduct? productOf(BuildContext context, IAPProductType type) => null; static bool isPurchased(BuildContext context, IAPProductType type) => false; From bf3c8aa7c7396238ae01a43ed6619c83b506e093 Mon Sep 17 00:00:00 2001 From: Vadim <44135514+vodemn@users.noreply.github.com> Date: Sat, 2 Sep 2023 13:08:58 +0200 Subject: [PATCH 08/23] Fixed _Build .apk_ flow --- .github/workflows/build_apk.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_apk.yml b/.github/workflows/build_apk.yml index 6acb464..d39bd31 100644 --- a/.github/workflows/build_apk.yml +++ b/.github/workflows/build_apk.yml @@ -86,10 +86,10 @@ jobs: flutter pub get flutter pub run intl_utils:generate - - name: Build Apk + - name: Build .apk env: FLAVOR: ${{ github.event.inputs.flavor }} - run: flutter build apk --release --flavor $FLAVOR --dart-define c -t lib/main_$FLAVOR.dart + run: flutter build apk --release --flavor $FLAVOR --dart-define cameraPreviewAspectRatio=240/320 -t lib/main_$FLAVOR.dart - name: Upload artifact uses: actions/upload-artifact@v3 From f39177919ce9d7b2ce8f65da22b0640b8e5cc102 Mon Sep 17 00:00:00 2001 From: Vadim <44135514+vodemn@users.noreply.github.com> Date: Sat, 2 Sep 2023 22:29:35 +0200 Subject: [PATCH 09/23] Equipment profiles issues (#112) * Fixed equipment profiles sections collapsing * Fixed range picker dialog * Refined equipment profiles sections handling --- .../widget_dialog_picker_range.dart | 4 +- .../widget_container_equipment_profile.dart | 11 ++- .../screen_equipment_profile.dart | 96 ++++++++++++------- 3 files changed, 70 insertions(+), 41 deletions(-) diff --git a/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/components/dialog_range_picker/widget_dialog_picker_range.dart b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/components/dialog_range_picker/widget_dialog_picker_range.dart index 24f684a..cc3f4ca 100644 --- a/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/components/dialog_range_picker/widget_dialog_picker_range.dart +++ b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/components/dialog_range_picker/widget_dialog_picker_range.dart @@ -68,8 +68,8 @@ class _DialogRangePickerState extends State widget.onExpand(); _controller.forward(); SchedulerBinding.instance.addPostFrameCallback((_) { - Scrollable.ensureVisible( - context, - alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd, - ); + Future.delayed(_controller.duration!).then((_) { + Scrollable.ensureVisible( + context, + alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd, + duration: _controller.duration!, + ); + }); }); } diff --git a/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart index 0168608..5307145 100644 --- a/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart +++ b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart @@ -17,17 +17,13 @@ class EquipmentProfilesScreen extends StatefulWidget { } class _EquipmentProfilesScreenState extends State { - static const maxProfiles = 5 + 1; // replace with a constant from iap - - late List> profileContainersKeys = []; - int get profilesCount => EquipmentProfiles.of(context).length; + final Map> keysMap = {}; + int get profilesCount => keysMap.length; @override void didChangeDependencies() { super.didChangeDependencies(); - profileContainersKeys = EquipmentProfiles.of(context) - .map((e) => GlobalKey(debugLabel: e.id)) - .toList(); + _updateProfilesKeys(); } @override @@ -35,11 +31,10 @@ class _EquipmentProfilesScreenState extends State { return SliverScreen( title: S.of(context).equipmentProfiles, appBarActions: [ - if (profilesCount < maxProfiles) - IconButton( - onPressed: _addProfile, - icon: const Icon(Icons.add), - ), + IconButton( + onPressed: _addProfile, + icon: const Icon(Icons.add), + ), IconButton( onPressed: Navigator.of(context).pop, icon: const Icon(Icons.close), @@ -55,24 +50,30 @@ class _EquipmentProfilesScreenState extends State { : [ SliverList( delegate: SliverChildBuilderDelegate( - (context, index) => index > 0 // skip default - ? Padding( - padding: EdgeInsets.fromLTRB( - Dimens.paddingM, - index == 0 ? Dimens.paddingM : 0, - Dimens.paddingM, - Dimens.paddingM, - ), - child: EquipmentProfileContainer( - key: profileContainersKeys[index], - data: EquipmentProfiles.of(context)[index], - onExpand: () => _keepExpandedAt(index), - onUpdate: (profileData) => _updateProfileAt(profileData, index), - onDelete: () => _removeProfileAt(index), - ), - ) - : const SizedBox.shrink(), - childCount: profilesCount, + (context, index) { + if (index == 0) { + // skip default profile + return const SizedBox.shrink(); + } + + final profile = EquipmentProfiles.of(context)[index]; + return Padding( + padding: EdgeInsets.fromLTRB( + Dimens.paddingM, + index == 0 ? Dimens.paddingM : 0, + Dimens.paddingM, + Dimens.paddingM, + ), + child: EquipmentProfileContainer( + key: keysMap[profile.id], + data: profile, + onExpand: () => _keepExpandedAt(index), + onUpdate: _updateProfileAt, + onDelete: () => _removeProfileAt(profile), + ), + ); + }, + childCount: EquipmentProfiles.of(context).length, ), ), SliverToBoxAdapter(child: SizedBox(height: MediaQuery.paddingOf(context).bottom)), @@ -91,22 +92,47 @@ class _EquipmentProfilesScreenState extends State { }); } - void _updateProfileAt(EquipmentProfile data, int index) { + void _updateProfileAt(EquipmentProfile data) { EquipmentProfileProvider.of(context).updateProdile(data); } - void _removeProfileAt(int index) { - EquipmentProfileProvider.of(context).deleteProfile(EquipmentProfiles.of(context)[index]); + void _removeProfileAt(EquipmentProfile data) { + EquipmentProfileProvider.of(context).deleteProfile(data); } void _keepExpandedAt(int index) { - profileContainersKeys.getRange(0, index).forEach((element) { + keysMap.values.toList().getRange(0, index).forEach((element) { element.currentState?.collapse(); }); - profileContainersKeys.getRange(index + 1, profilesCount).forEach((element) { + keysMap.values.toList().getRange(index + 1, profilesCount).forEach((element) { element.currentState?.collapse(); }); } + + void _updateProfilesKeys() { + final profiles = EquipmentProfiles.of(context); + if (profiles.length > keysMap.length) { + // profile added + final List idsToAdd = []; + for (final profile in profiles) { + if (!keysMap.keys.contains(profile.id)) idsToAdd.add(profile.id); + } + for (final id in idsToAdd) { + keysMap[id] = GlobalKey(debugLabel: id); + } + idsToAdd.clear(); + } else if (profiles.length < keysMap.length) { + // profile deleted + final List idsToDelete = []; + for (final id in keysMap.keys) { + if (!profiles.any((p) => p.id == id)) idsToDelete.add(id); + } + idsToDelete.forEach(keysMap.remove); + idsToDelete.clear(); + } else { + // profile updated, no need to updated keys + } + } } class _EquipmentProfilesListPlaceholder extends StatelessWidget { From 429c0a53a21438f061555d22e00f2b9ac489d59f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 2 Sep 2023 21:13:17 +0000 Subject: [PATCH 10/23] Version bump --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 0a2bfb6..97beb09 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: lightmeter description: Lightmeter app inspired by Material 3 design system. publish_to: "none" -version: 0.13.2+38 +version: 0.14.0+39 environment: sdk: ">=3.0.0 <4.0.0" From 4201d36abb202787691121ea48b3dbb38cde2e09 Mon Sep 17 00:00:00 2001 From: Vadim <44135514+vodemn@users.noreply.github.com> Date: Sun, 3 Sep 2023 13:06:30 +0200 Subject: [PATCH 11/23] Refined launch options --- .gitignore | 2 +- .vscode/launch.json | 66 ++++++++++++++++++++++++++++++------------- README.md | 11 ++++++-- lib/firebase.dart | 22 ++++++++++----- lib/main_prod.dart | 8 +----- lib/main_release.dart | 10 +++++++ 6 files changed, 82 insertions(+), 37 deletions(-) create mode 100644 lib/main_release.dart diff --git a/.gitignore b/.gitignore index ee0aebd..3fd9832 100644 --- a/.gitignore +++ b/.gitignore @@ -57,6 +57,6 @@ keystore.properties android/app/google-services.json ios/firebase_app_id_file.json ios/Runner/GoogleService-Info.plist -lib/firebase_options.dart +/lib/firebase_options.dart coverage/ \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 6cbc2bc..40dbfac 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,9 +5,10 @@ "version": "0.2.0", "configurations": [ { - "name": "dev (android)", + "name": "dev-debug (android)", "request": "launch", "type": "dart", + "flutterMode": "debug", "args": [ "--flavor", "dev", @@ -17,9 +18,49 @@ "program": "${workspaceFolder}/lib/main_dev.dart", }, { - "name": "dev (ios)", + "name": "dev-profile (android)", "request": "launch", "type": "dart", + "flutterMode": "profile", + "args": [ + "--flavor", + "dev", + "--dart-define", + "cameraPreviewAspectRatio=240/320", + ], + "program": "${workspaceFolder}/lib/main_dev.dart", + }, + { + "name": "prod-debug (android)", + "request": "launch", + "type": "dart", + "flutterMode": "debug", + "args": [ + "--flavor", + "prod", + "--dart-define", + "cameraPreviewAspectRatio=240/320", + ], + "program": "${workspaceFolder}/lib/main_release.dart", + }, + { + "name": "prod-profile (android)", + "request": "launch", + "type": "dart", + "flutterMode": "profile", + "args": [ + "--flavor", + "prod", + "--dart-define", + "cameraPreviewAspectRatio=240/320", + ], + "program": "${workspaceFolder}/lib/main_release.dart", + }, + { + "name": "dev-debug (ios)", + "request": "launch", + "type": "dart", + "flutterMode": "debug", "args": [ "--flavor", "dev", @@ -29,30 +70,17 @@ "program": "${workspaceFolder}/lib/main_dev.dart", }, { - "name": "prod (android)", + "name": "dev-profile (ios)", "request": "launch", - //"flutterMode": "release", + "flutterMode": "profile", "type": "dart", "args": [ "--flavor", - "prod", - "--dart-define", - "cameraPreviewAspectRatio=240/320", - ], - "program": "${workspaceFolder}/lib/main_prod.dart", - }, - { - "name": "prod (ios)", - "request": "launch", - //"flutterMode": "release", - "type": "dart", - "args": [ - "--flavor", - "prod", + "dev", "--dart-define", "cameraPreviewAspectRatio=3/4", ], - "program": "${workspaceFolder}/lib/main_prod.dart", + "program": "${workspaceFolder}/lib/main_dev.dart", }, ], } \ No newline at end of file diff --git a/README.md b/README.md index c4af94a..08e5780 100644 --- a/README.md +++ b/README.md @@ -60,13 +60,18 @@ flutter pub get flutter pub run intl_utils:generate ``` -### 4. Build (Android) +### 4. Build + +#### Android You can build an apk by running the following command from the root of the repository: ```console -flutter build apk --release --flavor $FLAVOR --dart-define cameraPreviewAspectRatio=240/320 -t lib/main_$FLAVOR.dart +flutter build apk --release --flavor dev --dart-define cameraPreviewAspectRatio=240/320 -t lib/main_dev.dart ``` -Just replace `$FLAVOR` with `dev` or `prod`. + +### iOS + +TBD # Contribution diff --git a/lib/firebase.dart b/lib/firebase.dart index f75ae90..0a0e6b0 100644 --- a/lib/firebase.dart +++ b/lib/firebase.dart @@ -1,14 +1,22 @@ +import 'dart:developer'; + import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/foundation.dart'; import 'package:lightmeter/firebase_options.dart'; -Future initializeFirebase() async { - await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); - FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError; - PlatformDispatcher.instance.onError = (error, stack) { - FirebaseCrashlytics.instance.recordError(error, stack, fatal: true); - return true; - }; +Future initializeFirebase({required bool handleErrors}) async { + try { + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + if (handleErrors) { + FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError; + PlatformDispatcher.instance.onError = (error, stack) { + FirebaseCrashlytics.instance.recordError(error, stack, fatal: true); + return true; + }; + } + } catch (e) { + log(e.toString()); + } } diff --git a/lib/main_prod.dart b/lib/main_prod.dart index fc1700a..2ccc397 100644 --- a/lib/main_prod.dart +++ b/lib/main_prod.dart @@ -1,5 +1,3 @@ -import 'dart:developer'; - import 'package:flutter/material.dart'; import 'package:lightmeter/application.dart'; import 'package:lightmeter/environment.dart'; @@ -7,10 +5,6 @@ import 'package:lightmeter/firebase.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); - try { - await initializeFirebase(); - } catch (e) { - log(e.toString()); - } + await initializeFirebase(handleErrors: true); runApp(const Application(Environment.prod())); } diff --git a/lib/main_release.dart b/lib/main_release.dart new file mode 100644 index 0000000..b87dff8 --- /dev/null +++ b/lib/main_release.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/application.dart'; +import 'package:lightmeter/environment.dart'; +import 'package:lightmeter/firebase.dart'; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + await initializeFirebase(handleErrors: false); + runApp(const Application(Environment.prod())); +} From cf4373d854d7750dd365c1beb53aedbb404d9c7c Mon Sep 17 00:00:00 2001 From: Vadim <44135514+vodemn@users.noreply.github.com> Date: Fri, 8 Sep 2023 22:25:13 +0200 Subject: [PATCH 12/23] Equipment profiles issues (#115) * update equipment profile from dialog * updated `MeteringScreenLayoutFeaturesDialog` translations --- lib/l10n/intl_en.arb | 1 + lib/l10n/intl_fr.arb | 1 + lib/l10n/intl_ru.arb | 1 + lib/l10n/intl_zh.arb | 1 + lib/screens/metering/screen_metering.dart | 10 +------- ...ialog_metering_screen_layout_features.dart | 23 +++++++++++++------ 6 files changed, 21 insertions(+), 16 deletions(-) diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 925d280..3d3ec6b 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -36,6 +36,7 @@ "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.", + "meteringScreenLayoutHintEquipmentProfiles": "Equipment profile picker", "meteringScreenFeatureExtremeExposurePairs": "Fastest & shortest exposure pairs", "meteringScreenFeatureFilmPicker": "Film picker", "meteringScreenFeatureHistogram": "Histogram", diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index c199d1e..4c6f353 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -36,6 +36,7 @@ "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.", + "meteringScreenLayoutHintEquipmentProfiles": "Sélecteur de profil de l'équipement", "meteringScreenFeatureExtremeExposurePairs": "Paires d'exposition les plus rapides et les plus courtes", "meteringScreenFeatureFilmPicker": "Sélecteur de film", "meteringScreenFeatureHistogram": "Histogramme", diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index d20c4bd..7a7c772 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -36,6 +36,7 @@ "lightSensor": "Датчик освещённости", "meteringScreenLayout": "Элементы главного экрана", "meteringScreenLayoutHint": "Здесь вы можете скрыть некоторые ненужные или неиспользуемые элементы с главного экрана.", + "meteringScreenLayoutHintEquipmentProfiles": "Выбор профиля оборудования", "meteringScreenFeatureExtremeExposurePairs": "Длинная и короткая выдержки", "meteringScreenFeatureFilmPicker": "Выбор пленки", "meteringScreenFeatureHistogram": "Гистограмма", diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb index 02bdf6c..8c4fb07 100644 --- a/lib/l10n/intl_zh.arb +++ b/lib/l10n/intl_zh.arb @@ -36,6 +36,7 @@ "lightSensor": "光传感器", "meteringScreenLayout": "布局", "meteringScreenLayoutHint": "隐藏不需要的元素,以免浪费曝光列表空间", + "meteringScreenLayoutHintEquipmentProfiles": "设备配置选择", "meteringScreenFeatureExtremeExposurePairs": "最快 & 最慢曝光组合", "meteringScreenFeatureFilmPicker": "胶片选择", "meteringScreenFeatureHistogram": "直方图", diff --git a/lib/screens/metering/screen_metering.dart b/lib/screens/metering/screen_metering.dart index e9a77b5..93d515e 100644 --- a/lib/screens/metering/screen_metering.dart +++ b/lib/screens/metering/screen_metering.dart @@ -84,15 +84,7 @@ class _InheritedListeners extends StatelessWidget { context.read().add(const FilmChangedEvent(Film.other())); } }, - child: MeteringScreenLayoutFeatureListener( - feature: MeteringScreenLayoutFeature.equipmentProfiles, - onDidChangeDependencies: (value) { - if (!value) { - EquipmentProfileProvider.of(context).setProfile(EquipmentProfiles.of(context).first); - } - }, - child: child, - ), + child: child, ), ); } diff --git a/lib/screens/settings/components/metering/components/metering_screen_layout/components/meterins_screen_layout_features_dialog/widget_dialog_metering_screen_layout_features.dart b/lib/screens/settings/components/metering/components/metering_screen_layout/components/meterins_screen_layout_features_dialog/widget_dialog_metering_screen_layout_features.dart index a043a12..57aaf24 100644 --- a/lib/screens/settings/components/metering/components/metering_screen_layout/components/meterins_screen_layout_features_dialog/widget_dialog_metering_screen_layout_features.dart +++ b/lib/screens/settings/components/metering/components/metering_screen_layout/components/meterins_screen_layout_features_dialog/widget_dialog_metering_screen_layout_features.dart @@ -3,6 +3,7 @@ import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/res/dimens.dart'; +import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; class MeteringScreenLayoutFeaturesDialog extends StatefulWidget { const MeteringScreenLayoutFeaturesDialog({super.key}); @@ -25,18 +26,23 @@ class _MeteringScreenLayoutFeaturesDialogState extends State Date: Fri, 8 Sep 2023 23:46:31 +0200 Subject: [PATCH 13/23] ML-114 Metering container issues (#116) * fixed top bar cutout shape * update closed offset on layout change --- .../shape_top_bar_metering.dart | 119 ++++++++---------- .../widget_top_bar_metering.dart | 4 +- .../widget_dialog_animated.dart | 78 +++++++----- 3 files changed, 100 insertions(+), 101 deletions(-) diff --git a/lib/screens/metering/components/shared/metering_top_bar/shape_top_bar_metering.dart b/lib/screens/metering/components/shared/metering_top_bar/shape_top_bar_metering.dart index c8f04fa..acc5986 100644 --- a/lib/screens/metering/components/shared/metering_top_bar/shape_top_bar_metering.dart +++ b/lib/screens/metering/components/shared/metering_top_bar/shape_top_bar_metering.dart @@ -31,10 +31,23 @@ class MeteringTopBarShape extends CustomPainter { @override void paint(Canvas canvas, Size size) { final paint = Paint()..color = color; - final path = Path(); - const circularRadius = Radius.circular(Dimens.borderRadiusL); + late final Path path; if (appendixHeight == 0 || appendixWidth == 0) { - path.addRRect( + path = _drawNoAppendix(size, Dimens.borderRadiusL); + } else if (appendixHeight < 0) { + path = _drawAppendixOnLeft(size, Dimens.borderRadiusL); + canvas.scale(-1, 1); + canvas.translate(-size.width, 0); + } else { + path = _drawAppendixOnLeft(size, Dimens.borderRadiusL); + } + canvas.drawPath(path, paint); + } + + Path _drawNoAppendix(Size size, double bottomRadius) { + final circularRadius = Radius.circular(bottomRadius); + return Path() + ..addRRect( RRect.fromLTRBAndCorners( 0, 0, @@ -43,73 +56,51 @@ class MeteringTopBarShape extends CustomPainter { bottomLeft: circularRadius, bottomRight: circularRadius, ), - ); - } else if (appendixHeight < 0) { - // Left side with bottom corner - path.lineTo(0, size.height + appendixHeight - Dimens.borderRadiusL); - path.arcToPoint( - Offset(Dimens.borderRadiusL, size.height + appendixHeight), - radius: circularRadius, - clockwise: false, - ); + ) + ..close(); + } - // Bottom side with step - final allowedRadius = min(appendixHeight.abs() / 2, Dimens.borderRadiusL); - path.lineTo(appendixWidth - allowedRadius, size.height + appendixHeight); - path.arcToPoint( - Offset(appendixWidth, size.height + appendixHeight + allowedRadius), - radius: circularRadius, - ); - path.lineTo(appendixWidth, size.height - allowedRadius); - path.arcToPoint( - Offset(appendixWidth + allowedRadius, size.height), - radius: circularRadius, - clockwise: false, - ); + Path _drawAppendixOnLeft(Size size, double bottomRadius) { + final path = Path(); + final circularRadius = Radius.circular(bottomRadius); + final appendixHeight = this.appendixHeight.abs(); - // Right side with bottom corner - path.lineTo(size.width - Dimens.borderRadiusL, size.height); - path.arcToPoint( - Offset(size.width, size.height - Dimens.borderRadiusL), - radius: circularRadius, - clockwise: false, - ); - } else { - // Left side with bottom corner - path.lineTo(0, size.height - Dimens.borderRadiusL); - path.arcToPoint( - Offset(Dimens.borderRadiusL, size.height), - radius: circularRadius, - clockwise: false, - ); + // Left side with bottom corner + path.lineTo(0, size.height - bottomRadius); + path.arcToPoint( + Offset(bottomRadius, size.height), + radius: circularRadius, + clockwise: false, + ); - // Bottom side with step - final allowedRadius = min(appendixHeight.abs() / 2, Dimens.borderRadiusL); - path.relativeLineTo(appendixWidth - allowedRadius * 2, 0); - path.relativeArcToPoint( - Offset(allowedRadius, -allowedRadius), - radius: Radius.circular(allowedRadius), - rotation: 90, - clockwise: false, - ); - path.relativeLineTo(0, -appendixHeight + allowedRadius * 2); - path.relativeArcToPoint( - Offset(allowedRadius, -allowedRadius), - radius: Radius.circular(allowedRadius), - rotation: 90, - ); + // Bottom side with step + final allowedRadius = min(appendixHeight.abs() / 2, bottomRadius); + path.lineTo(appendixWidth - allowedRadius, size.height); + path.arcToPoint( + Offset(appendixWidth, size.height - allowedRadius), + radius: Radius.circular(allowedRadius), + rotation: 90, + clockwise: false, + ); + path.lineTo(appendixWidth, size.height - appendixHeight + allowedRadius); + path.arcToPoint( + Offset(appendixWidth + allowedRadius, size.height - appendixHeight), + radius: Radius.circular(allowedRadius), + rotation: 90, + ); + + // Right side with bottom corner + path.lineTo(size.width - bottomRadius, size.height - appendixHeight); + path.arcToPoint( + Offset(size.width, size.height - appendixHeight - bottomRadius), + radius: circularRadius, + clockwise: false, + ); - // Right side with bottom corner - path.lineTo(size.width - Dimens.borderRadiusL, size.height - appendixHeight); - path.arcToPoint( - Offset(size.width, size.height - appendixHeight - Dimens.borderRadiusL), - radius: circularRadius, - clockwise: false, - ); - } path.lineTo(size.width, 0); path.close(); - canvas.drawPath(path, paint); + + return path; } @override diff --git a/lib/screens/metering/components/shared/metering_top_bar/widget_top_bar_metering.dart b/lib/screens/metering/components/shared/metering_top_bar/widget_top_bar_metering.dart index 0682e7e..a005b85 100644 --- a/lib/screens/metering/components/shared/metering_top_bar/widget_top_bar_metering.dart +++ b/lib/screens/metering/components/shared/metering_top_bar/widget_top_bar_metering.dart @@ -20,9 +20,7 @@ class MeteringTopBar extends StatelessWidget { return CustomPaint( painter: MeteringTopBarShape( color: Theme.of(context).colorScheme.surface, - appendixWidth: appendixHeight > 0 - ? MediaQuery.of(context).size.width / 2 - Dimens.grid8 + Dimens.paddingM - : MediaQuery.of(context).size.width / 2 + Dimens.grid8 - Dimens.paddingM, + appendixWidth: MediaQuery.of(context).size.width / 2 - Dimens.grid8 / 2 + Dimens.paddingM, appendixHeight: appendixHeight, ), child: Padding( diff --git a/lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart b/lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart index cea97da..72e770b 100644 --- a/lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart +++ b/lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart @@ -22,15 +22,15 @@ class AnimatedDialog extends StatefulWidget { class AnimatedDialogState extends State with SingleTickerProviderStateMixin { final GlobalKey _key = GlobalKey(); - late final Size _closedSize; - late final Offset _closedOffset; + late Size _closedSize; + late Offset _closedOffset; late final AnimationController _animationController; late final CurvedAnimation _defaultCurvedAnimation; late final Animation _barrierColorAnimation; - late final SizeTween _sizeTween; - late final Animation _sizeAnimation; - late final Animation _offsetAnimation; + late SizeTween _sizeTween; + late Animation _sizeAnimation; + late Animation _offsetAnimation; late final Animation _borderRadiusAnimation; late final Animation _closedOpacityAnimation; late final Animation _openedOpacityAnimation; @@ -88,35 +88,7 @@ class AnimatedDialogState extends State with SingleTickerProvide ), ); - WidgetsBinding.instance.addPostFrameCallback((_) { - final mediaQuery = MediaQuery.of(context); - - _closedSize = _key.currentContext!.size!; - _sizeTween = SizeTween( - begin: _closedSize, - end: widget.openedSize ?? - Size( - mediaQuery.size.width - - mediaQuery.padding.horizontal - - Dimens.dialogMargin.horizontal, - mediaQuery.size.height - mediaQuery.padding.vertical - Dimens.dialogMargin.vertical, - ), - ); - _sizeAnimation = _sizeTween.animate(_defaultCurvedAnimation); - - final renderBox = _key.currentContext!.findRenderObject()! as RenderBox; - _closedOffset = renderBox.localToGlobal(Offset.zero); - _offsetAnimation = SizeTween( - begin: Size( - _closedOffset.dx + _closedSize.width / 2, - _closedOffset.dy + _closedSize.height / 2, - ), - end: Size( - mediaQuery.size.width / 2, - mediaQuery.size.height / 2 + mediaQuery.padding.top / 2 - mediaQuery.padding.bottom / 2, - ), - ).animate(_defaultCurvedAnimation); - }); + _setClosedOffset(); } @override @@ -133,6 +105,12 @@ class AnimatedDialogState extends State with SingleTickerProvide ).animate(_defaultCurvedAnimation); } + @override + void didUpdateWidget(covariant AnimatedDialog oldWidget) { + super.didUpdateWidget(oldWidget); + _setClosedOffset(); + } + @override void dispose() { _animationController.dispose(); @@ -151,6 +129,38 @@ class AnimatedDialogState extends State with SingleTickerProvide ); } + void _setClosedOffset() { + WidgetsBinding.instance.addPostFrameCallback((_) { + final renderBox = _key.currentContext?.findRenderObject()! as RenderBox?; + if (renderBox != null) { + final size = MediaQuery.sizeOf(context); + final padding = MediaQuery.paddingOf(context); + _closedSize = _key.currentContext!.size!; + _sizeTween = SizeTween( + begin: _closedSize, + end: widget.openedSize ?? + Size( + size.width - padding.horizontal - Dimens.dialogMargin.horizontal, + size.height - padding.vertical - Dimens.dialogMargin.vertical, + ), + ); + _sizeAnimation = _sizeTween.animate(_defaultCurvedAnimation); + + _closedOffset = renderBox.localToGlobal(Offset.zero); + _offsetAnimation = SizeTween( + begin: Size( + _closedOffset.dx + _closedSize.width / 2, + _closedOffset.dy + _closedSize.height / 2, + ), + end: Size( + size.width / 2, + size.height / 2 + padding.top / 2 - padding.bottom / 2, + ), + ).animate(_defaultCurvedAnimation); + } + }); + } + void _openDialog() { Navigator.of(context).push( PageRouteBuilder( From 1be7c3be48df5c91c4fce41df9f258e3e9224fd2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 8 Sep 2023 22:03:01 +0000 Subject: [PATCH 14/23] Version bump --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 97beb09..22d5ddb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: lightmeter description: Lightmeter app inspired by Material 3 design system. publish_to: "none" -version: 0.14.0+39 +version: 0.14.1+40 environment: sdk: ">=3.0.0 <4.0.0" From cc9f162933eb5aa34615fb0c0d7ad329a1084580 Mon Sep 17 00:00:00 2001 From: Vadim <44135514+vodemn@users.noreply.github.com> Date: Thu, 14 Sep 2023 16:59:16 +0200 Subject: [PATCH 15/23] ML-107 Films filter (#118) * added stub `FilmsProvider` * moved dialogs to the shared folder * typo * separated `EquipmentSettingsSection` * copy * `IAPBuilder` -> `IAPListTile` * moved `Film` to resources repo * fixed films selection * untied iso and selected film * removed film from exposure pairs building * indicate push/pull * copy * Update .gitignore * fixed extreme exposure pairs reciprocity display * sync with iap changes * sync iap stub with iap changes * added reciprocity description * added workspace file * Update .gitignore --- .github/workflows/pr_check.yml | 9 +- README.md | 25 +- iap/lib/m3_lightmeter_iap.dart | 10 +- .../providers/equipment_profile_provider.dart | 32 +-- iap/lib/src/providers/films_provider.dart | 65 +++++ .../src/providers/selectable_provider.dart | 29 +++ lib/data/models/film.dart | 235 ------------------ lib/data/shared_prefs_service.dart | 8 - lib/interactors/metering_interactor.dart | 4 - lib/l10n/intl_en.arb | 5 + lib/l10n/intl_fr.arb | 7 +- lib/l10n/intl_ru.arb | 5 + lib/l10n/intl_zh.arb | 7 +- lib/screens/metering/bloc_metering.dart | 45 ---- .../camera_preview/widget_camera_preview.dart | 78 ++++-- .../provider_container_camera.dart | 7 - .../widget_container_camera.dart | 7 - .../provider_container_light_sensor.dart | 7 - .../widget_container_light_sensor.dart | 7 - .../widget_list_exposure_pairs.dart | 4 +- .../widget_picker_equipment_profiles.dart | 30 +++ ...dget_container_extreme_exposure_pairs.dart | 40 +++ .../film_picker/widget_picker_film.dart | 53 ++++ .../iso_picker/widget_picker_iso.dart | 40 +++ .../nd_picker/widget_picker_nd.dart | 42 ++++ .../widget_dialog_animated.dart | 0 .../dialog_picker/widget_picker_dialog.dart | 0 .../widget_picker_dialog_animated.dart | 5 +- .../widget_container_reading_value.dart | 0 .../widget_container_readings.dart | 165 +----------- lib/screens/metering/event_metering.dart | 7 - lib/screens/metering/screen_metering.dart | 18 +- lib/screens/metering/state_metering.dart | 5 - .../utils/equipment_profile_listener.dart | 30 --- .../widget_list_tiles_equipments.dart | 4 +- .../widget_container_equipment_profile.dart | 4 +- .../widget_dialog_equipment_profile_name.dart | 0 .../screen_equipment_profile.dart | 4 +- .../widget_list_tile_equipment_profiles.dart | 22 ++ .../films/widget_list_tile_films.dart | 35 +++ .../widget_settings_section_equipment.dart | 20 ++ .../language/widget_list_tile_language.dart | 2 +- .../widget_list_tile_equipment_profiles.dart | 43 ---- .../widget_list_tile_fractional_stops.dart | 2 +- .../widget_settings_section_metering.dart | 2 - .../dialog_filter/widget_dialog_filter.dart | 7 +- .../widget_dialog_picker.dart | 0 .../widget_dialog_picker_range.dart | 0 .../iap_list_tile/widget_list_tile_iap.dart | 33 +++ .../widget_settings_section.dart | 35 +-- .../widget_list_tile_theme_type.dart | 2 +- lib/screens/settings/screen_settings.dart | 2 + m3_lightmeter.code-workspace | 11 + test/data/models/film_test.dart | 121 --------- test/data/shared_prefs_service_test.dart | 23 -- .../interactors/metering_interactor_test.dart | 14 -- test/screens/metering/bloc_metering_test.dart | 142 ----------- .../metering/screen_metering_test.dart | 11 - 58 files changed, 592 insertions(+), 978 deletions(-) create mode 100644 iap/lib/src/providers/films_provider.dart create mode 100644 iap/lib/src/providers/selectable_provider.dart delete mode 100644 lib/data/models/film.dart create mode 100644 lib/screens/metering/components/shared/readings_container/components/equipment_profile_picker/widget_picker_equipment_profiles.dart create mode 100644 lib/screens/metering/components/shared/readings_container/components/extreme_exposure_pairs_container/widget_container_extreme_exposure_pairs.dart create mode 100644 lib/screens/metering/components/shared/readings_container/components/film_picker/widget_picker_film.dart create mode 100644 lib/screens/metering/components/shared/readings_container/components/iso_picker/widget_picker_iso.dart create mode 100644 lib/screens/metering/components/shared/readings_container/components/nd_picker/widget_picker_nd.dart rename lib/screens/metering/components/shared/readings_container/components/{ => shared}/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart (100%) rename lib/screens/metering/components/shared/readings_container/components/{ => shared}/animated_dialog_picker/components/dialog_picker/widget_picker_dialog.dart (100%) rename lib/screens/metering/components/shared/readings_container/components/{ => shared}/animated_dialog_picker/widget_picker_dialog_animated.dart (89%) rename lib/screens/metering/components/shared/readings_container/components/{ => shared}/reading_value_container/widget_container_reading_value.dart (100%) delete mode 100644 lib/screens/metering/utils/equipment_profile_listener.dart rename lib/screens/settings/components/{metering => equipment}/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/widget_list_tiles_equipments.dart (90%) rename lib/screens/settings/components/{metering => equipment}/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart (94%) rename lib/screens/settings/components/{metering => equipment}/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_name_dialog/widget_dialog_equipment_profile_name.dart (100%) rename lib/screens/settings/components/{metering => equipment}/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart (92%) create mode 100644 lib/screens/settings/components/equipment/components/equipment_profiles/widget_list_tile_equipment_profiles.dart create mode 100644 lib/screens/settings/components/equipment/components/films/widget_list_tile_films.dart create mode 100644 lib/screens/settings/components/equipment/widget_settings_section_equipment.dart delete mode 100644 lib/screens/settings/components/metering/components/equipment_profiles/widget_list_tile_equipment_profiles.dart rename lib/screens/settings/components/{metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/components => shared}/dialog_filter/widget_dialog_filter.dart (92%) rename lib/screens/settings/components/shared/{dialog_picker.dart => dialog_picker}/widget_dialog_picker.dart (100%) rename lib/screens/settings/components/{metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/components => shared}/dialog_range_picker/widget_dialog_picker_range.dart (100%) create mode 100644 lib/screens/settings/components/shared/iap_list_tile/widget_list_tile_iap.dart create mode 100644 m3_lightmeter.code-workspace delete mode 100644 test/data/models/film_test.dart diff --git a/.github/workflows/pr_check.yml b/.github/workflows/pr_check.yml index 70e6fa4..ac420da 100644 --- a/.github/workflows/pr_check.yml +++ b/.github/workflows/pr_check.yml @@ -15,7 +15,7 @@ jobs: analyze_and_test: name: Analyze & test runs-on: macos-11 - timeout-minutes: 10 + timeout-minutes: 5 steps: - uses: actions/checkout@v3 with: @@ -47,3 +47,10 @@ jobs: - name: Run tests run: flutter test + + - name: Analyze project source with stub + if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} + run: | + bash ./.github/scripts/stub_iap.sh + flutter pub get + flutter analyze lib --fatal-infos diff --git a/README.md b/README.md index 08e5780..dae7291 100644 --- a/README.md +++ b/README.md @@ -33,11 +33,7 @@ Without further delay behold my new Lightmeter app inspired by Material You (a.k To build this app you need to install Flutter 3.10.0 stable. [How to install](https://docs.flutter.dev/get-started/install). -### 2. (Optional) Install Firebase - -Out of the box Firebase Crashlytics won't work. If you want to add Crashlytics to your local build please follow [this guide](https://firebase.google.com/docs/flutter/setup). - -### 3. Get packages +### 3. Project setup As part of the app's functionallity is in the private repo, you have to replace these lines in _pubspec.yaml_: @@ -47,24 +43,39 @@ m3_lightmeter_iap: url: "https://github.com/vodemn/m3_lightmeter_iap" ref: main ``` + with these: + ```yaml m3_lightmeter_iap: path: iap ``` -and run `flutter pub get` from the _iap/_ folder. + +You can do it simply by running the script: + +```console +sh .github/scripts/stub_iap.sh +``` + +> If you are using VSCode, you can open the workspace like so: _File -> Open Workspace from File -> m3_lightmeter.code-workspace_. Otherwise you have to run `flutter pub get` command from the iap folder. Then you can fetch all the neccessary dependencies and generate translation files by running the following commands: + ```console flutter pub get flutter pub run intl_utils:generate ``` -### 4. Build +### 4. (Optional) Install Firebase + +Out of the box Firebase Crashlytics won't work. If you want to add Crashlytics to your local build please follow [this guide](https://firebase.google.com/docs/flutter/setup). + +### 5. Build #### Android You can build an apk by running the following command from the root of the repository: + ```console flutter build apk --release --flavor dev --dart-define cameraPreviewAspectRatio=240/320 -t lib/main_dev.dart ``` diff --git a/iap/lib/m3_lightmeter_iap.dart b/iap/lib/m3_lightmeter_iap.dart index 8fe8aa5..171fe47 100644 --- a/iap/lib/m3_lightmeter_iap.dart +++ b/iap/lib/m3_lightmeter_iap.dart @@ -2,11 +2,13 @@ library m3_lightmeter_iap; import 'package:flutter/material.dart'; import 'package:m3_lightmeter_iap/src/providers/equipment_profile_provider.dart'; +import 'package:m3_lightmeter_iap/src/providers/films_provider.dart'; import 'package:m3_lightmeter_iap/src/providers/iap_products_provider.dart'; export 'src/data/models/iap_product.dart'; -export 'src/providers/equipment_profile_provider.dart' hide EquipmentProfilesAspect; +export 'src/providers/equipment_profile_provider.dart'; +export 'src/providers/films_provider.dart'; export 'src/providers/iap_products_provider.dart'; class IAPProviders extends StatelessWidget { @@ -22,8 +24,10 @@ class IAPProviders extends StatelessWidget { @override Widget build(BuildContext context) { return IAPProductsProvider( - child: EquipmentProfileProvider( - child: child, + child: FilmsProvider( + child: EquipmentProfileProvider( + child: child, + ), ), ); } diff --git a/iap/lib/src/providers/equipment_profile_provider.dart b/iap/lib/src/providers/equipment_profile_provider.dart index 4f7aa0f..92ba8a2 100644 --- a/iap/lib/src/providers/equipment_profile_provider.dart +++ b/iap/lib/src/providers/equipment_profile_provider.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:m3_lightmeter_iap/src/providers/selectable_provider.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class EquipmentProfileProvider extends StatefulWidget { @@ -27,7 +28,7 @@ class EquipmentProfileProviderState extends State { @override Widget build(BuildContext context) { return EquipmentProfiles( - profiles: const [_defaultProfile], + values: const [_defaultProfile], selected: _defaultProfile, child: widget.child, ); @@ -42,38 +43,19 @@ class EquipmentProfileProviderState extends State { void deleteProfile(EquipmentProfile data) {} } -enum EquipmentProfilesAspect { list, selected } - -class EquipmentProfiles extends InheritedModel { +class EquipmentProfiles extends SelectableInheritedModel { const EquipmentProfiles({ super.key, - required this.profiles, - required this.selected, + required super.values, + required super.selected, required super.child, }); - final List profiles; - final EquipmentProfile selected; - static List of(BuildContext context) { - return InheritedModel.inheritFrom( - context, - aspect: EquipmentProfilesAspect.list, - )! - .profiles; + return InheritedModel.inheritFrom(context, aspect: SelectableAspect.list)!.values; } static EquipmentProfile selectedOf(BuildContext context) { - return InheritedModel.inheritFrom( - context, - aspect: EquipmentProfilesAspect.selected, - )! - .selected; + return InheritedModel.inheritFrom(context, aspect: SelectableAspect.selected)!.selected; } - - @override - bool updateShouldNotify(EquipmentProfiles oldWidget) => false; - - @override - bool updateShouldNotifyDependent(EquipmentProfiles oldWidget, Set dependencies) => false; } diff --git a/iap/lib/src/providers/films_provider.dart b/iap/lib/src/providers/films_provider.dart new file mode 100644 index 0000000..e75ccd3 --- /dev/null +++ b/iap/lib/src/providers/films_provider.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:m3_lightmeter_iap/src/providers/selectable_provider.dart'; +import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; + +class FilmsProvider extends StatefulWidget { + final Widget child; + + const FilmsProvider({ + required this.child, + super.key, + }); + + static FilmsProviderState of(BuildContext context) { + return context.findAncestorStateOfType()!; + } + + @override + State createState() => FilmsProviderState(); +} + +class FilmsProviderState extends State { + @override + Widget build(BuildContext context) { + return Films( + values: const [Film.other()], + filmsInUse: const [Film.other()], + selected: const Film.other(), + child: widget.child, + ); + } + + void setFilm(Film film) {} + + void saveFilms(List films) {} +} + +class Films extends SelectableInheritedModel { + final List filmsInUse; + + const Films({ + super.key, + required super.values, + required this.filmsInUse, + required super.selected, + required super.child, + }); + + /// [Film.other()] + all the custom fields with actual reciprocity formulas + static List of(BuildContext context) { + return InheritedModel.inheritFrom(context)!.values; + } + + /// [Film.other()] + films in use selected by user + static List inUseOf(BuildContext context) { + return InheritedModel.inheritFrom( + context, + aspect: SelectableAspect.list, + )! + .filmsInUse; + } + + static Film selectedOf(BuildContext context) { + return InheritedModel.inheritFrom(context, aspect: SelectableAspect.selected)!.selected; + } +} diff --git a/iap/lib/src/providers/selectable_provider.dart b/iap/lib/src/providers/selectable_provider.dart new file mode 100644 index 0000000..c18f998 --- /dev/null +++ b/iap/lib/src/providers/selectable_provider.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +enum SelectableAspect { list, selected } + +class SelectableInheritedModel extends InheritedModel { + const SelectableInheritedModel({ + super.key, + required this.values, + required this.selected, + required super.child, + }); + + final List values; + final T selected; + + @override + bool updateShouldNotify(SelectableInheritedModel oldWidget) => true; + + @override + bool updateShouldNotifyDependent(SelectableInheritedModel oldWidget, Set dependencies) { + if (dependencies.contains(SelectableAspect.list)) { + return true; + } else if (dependencies.contains(SelectableAspect.selected)) { + return selected != oldWidget.selected; + } else { + return false; + } + } +} diff --git a/lib/data/models/film.dart b/lib/data/models/film.dart deleted file mode 100644 index ab651e8..0000000 --- a/lib/data/models/film.dart +++ /dev/null @@ -1,235 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/foundation.dart'; -import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; - -double log10(double x) => log(x) / log(10); - -double log10polynomian( - double x, - double a, - double b, - double c, -) => - a * pow(log10(x), 2) + b * log10(x) + c; - -typedef ReciprocityFailureBuilder = ShutterSpeedValue Function(ShutterSpeedValue shutterSpeed); - -/// Only Ilford films have reciprocity formulas provided by the manufacturer: -/// https://www.ilfordphoto.com/wp/wp-content/uploads/2017/06/Reciprocity-Failure-Compensation.pdf -/// -/// Reciprocity formulas for Fomapan films and Kodak films are from here: -/// https://www.flickr.com/groups/86738082@N00/discuss/72157626050157470/ -/// -/// Cinema films like Kodak 5222/7222 Double-X and respective CineStill films (cause they are basically a modification of Kodak) -/// do not have any reciprocity failure information, as these films are ment to be used in cinema -/// with appropriate light and pretty short shutter speeds. -/// -/// Because of this: https://github.com/dart-lang/sdk/issues/38934#issuecomment-803938315 -/// `super` calls are ignored in test coverage -class Film { - final String name; - final int iso; - - const Film(this.name, this.iso); - - const Film.other() - : name = '', - iso = 0; - - @override - String toString() => name; - - ShutterSpeedValue reciprocityFailure(ShutterSpeedValue shutterSpeed) { - if (shutterSpeed.isFraction) { - return shutterSpeed; - } else { - return ShutterSpeedValue( - reciprocityFormula(shutterSpeed.rawValue), - shutterSpeed.isFraction, - shutterSpeed.stopType, - ); - } - } - - @protected - double reciprocityFormula(double t) => t; - - static const List values = [ - Film.other(), - FomapanFilm.creative100(), - FomapanFilm.creative200(), - FomapanFilm.action400(), - IlfordFilm.ortho(), - IlfordFilm.delta100(), - IlfordFilm.delta400(), - IlfordFilm.delta3200(), - IlfordFilm.fp4(), - IlfordFilm.hp5(), - IlfordFilm.panf(), - IlfordFilm.sfx200(), - IlfordFilm.xp2super(), - IlfordFilm.pan100(), - IlfordFilm.pan400(), - KodakFilm.tmax100(), - KodakFilm.tmax400(), - KodakFilm.tmax3200(), - KodakFilm.trix320(), - KodakFilm.trix400(), - ]; -} - -/// https://www.tate.org.uk/documents/598/page_6_7_agfa_stocks_0.pdf -/// https://www.filmwasters.com/forum/index.php?topic=5298.0 -// {{1,1.87},{2,3.73},{3,8.06},{4,13.93},{5,21.28},{6,23.00},{7,30.12},{8,38.05},{9,44.75},{10,50.12},{20,117},{30,202},{40,293},{50,413},{60,547},{70,694},{80,853},{90,1022},{100,1202}}; -// class AgfaFilm extends Film { -// final double a; -// final double b; -// final double c; - -// const AgfaFilm.apx100() -// : a = 1, -// b = 5, -// c = 2, -// super('Agfa APX 100', 100); // coverage:ignore-line - -// const AgfaFilm.apx400() -// : a = 1.5, -// b = 4.5, -// c = 3, -// super('Agfa APX 400', 400); // coverage:ignore-line - -// @override -// double reciprocityFormula(double t) => t * log10polynomian(t, a, b, c); -// } - -class FomapanFilm extends Film { - final double a; - final double b; - final double c; - - /// https://www.foma.cz/en/fomapan-100 - const FomapanFilm.creative100() - : a = 1, - b = 5, - c = 2, - super('Fomapan CREATIVE 100', 100); // coverage:ignore-line - - /// https://www.foma.cz/en/fomapan-200 - const FomapanFilm.creative200() - : a = 1.5, - b = 4.5, - c = 3, - super('Fomapan CREATIVE 200', 200); // coverage:ignore-line - - /// https://www.foma.cz/en/fomapan-100 - const FomapanFilm.action400() - : a = -1.25, // coverage:ignore-line - b = 5.75, - c = 1.5, - super('Fomapan ACTION 400', 400); // coverage:ignore-line - - @override - double reciprocityFormula(double t) => t * log10polynomian(t, a, b, c); -} - -class IlfordFilm extends Film { - final double reciprocityPower; - - /// https://www.ilfordphoto.com/amfile/file/download/file/1948/product/1650/ - const IlfordFilm.ortho() - : reciprocityPower = 1.25, - super('Ilford ORTHO+', 80); // coverage:ignore-line - - /// https://www.ilfordphoto.com/amfile/file/download/file/1919/product/686/ - const IlfordFilm.fp4() - : reciprocityPower = 1.26, - super('Ilford FP4+', 125); // coverage:ignore-line - - /// https://www.ilfordphoto.com/amfile/file/download/file/1903/product/691/ - const IlfordFilm.hp5() - : reciprocityPower = 1.31, - super('Ilford HP5+', 400); // coverage:ignore-line - - /// https://www.ilfordphoto.com/amfile/file/download/file/3/product/679/ - const IlfordFilm.delta100() - : reciprocityPower = 1.26, - super('Ilford DELTA 100', 100); // coverage:ignore-line - - /// https://www.ilfordphoto.com/amfile/file/download/file/1915/product/684/ - const IlfordFilm.delta400() - : reciprocityPower = 1.41, - super('Ilford DELTA 400', 400); // coverage:ignore-line - - /// https://www.ilfordphoto.com/amfile/file/download/file/1913/product/682/ - const IlfordFilm.delta3200() - : reciprocityPower = 1.33, - super('Ilford DELTA 3200', 3200); // coverage:ignore-line - - /// https://www.ilfordphoto.com/amfile/file/download/file/1905/product/699/ - const IlfordFilm.panf() - : reciprocityPower = 1.33, - super('Ilford Pan F+', 50); // coverage:ignore-line - - /// https://www.ilfordphoto.com/amfile/file/download/file/1907/product/701/ - const IlfordFilm.sfx200() - : reciprocityPower = 1.31, - super('Ilford SFX 200', 200); // coverage:ignore-line - - /// https://www.ilfordphoto.com/amfile/file/download/file/1909/product/703/ - const IlfordFilm.xp2super() - : reciprocityPower = 1.31, - super('Ilford XP2 SUPER', 400); // coverage:ignore-line - - /// https://www.ilfordphoto.com/amfile/file/download/file/1958/product/696/ - const IlfordFilm.pan100() - : reciprocityPower = 1.26, - super('Kentemere 100', 100); // coverage:ignore-line - - /// https://www.ilfordphoto.com/amfile/file/download/file/1959/product/697/ - const IlfordFilm.pan400() - : reciprocityPower = 1.30, - super('Kentemere 400', 400); // coverage:ignore-line - - @override - double reciprocityFormula(double t) => pow(t, reciprocityPower).toDouble(); -} - -class KodakFilm extends Film { - final double a; - final double b; - final double c; - - const KodakFilm.tmax100() - : a = 1 / 6, // coverage:ignore-line - b = 0, // coverage:ignore-line - c = 4 / 3, // coverage:ignore-line - super('Kodak T-MAX 100', 100); // coverage:ignore-line - - const KodakFilm.tmax400() - : a = 2 / 3, // coverage:ignore-line - b = -1 / 2, // coverage:ignore-line - c = 4 / 3, // coverage:ignore-line - super('Kodak T-MAX 400', 400); // coverage:ignore-line - - const KodakFilm.tmax3200() - : a = 7 / 6, // coverage:ignore-line - b = -1, // coverage:ignore-line - c = 4 / 3, // coverage:ignore-line - super('Kodak T-MAX 3200', 3200); // coverage:ignore-line - - const KodakFilm.trix320() - : a = 2, - b = 1, - c = 2, - super('Kodak TRI-X 320', 320); // coverage:ignore-line - - const KodakFilm.trix400() - : a = 2, - b = 1, - c = 2, - super('Kodak TRI-X 400', 400); // coverage:ignore-line - - @override - double reciprocityFormula(double t) => t * log10polynomian(t, a, b, c); -} diff --git a/lib/data/shared_prefs_service.dart b/lib/data/shared_prefs_service.dart index 812f0e6..96d7d9d 100644 --- a/lib/data/shared_prefs_service.dart +++ b/lib/data/shared_prefs_service.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:lightmeter/data/models/ev_source_type.dart'; -import 'package:lightmeter/data/models/film.dart'; import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; import 'package:lightmeter/data/models/supported_locale.dart'; import 'package:lightmeter/data/models/theme_type.dart'; @@ -19,7 +18,6 @@ class UserPreferencesService { static const cameraEvCalibrationKey = "cameraEvCalibration"; static const lightSensorEvCalibrationKey = "lightSensorEvCalibration"; static const meteringScreenLayoutKey = "meteringScreenLayout"; - static const filmKey = "film"; static const caffeineKey = "caffeine"; static const hapticsKey = "haptics"; @@ -142,10 +140,4 @@ class UserPreferencesService { bool get dynamicColor => _sharedPreferences.getBool(dynamicColorKey) ?? false; set dynamicColor(bool value) => _sharedPreferences.setBool(dynamicColorKey, value); - - Film get film => Film.values.firstWhere( - (e) => e.name == _sharedPreferences.getString(filmKey), - orElse: () => Film.values.first, - ); - set film(Film value) => _sharedPreferences.setString(filmKey, value.name); } diff --git a/lib/interactors/metering_interactor.dart b/lib/interactors/metering_interactor.dart index 7ea25ac..b4427de 100644 --- a/lib/interactors/metering_interactor.dart +++ b/lib/interactors/metering_interactor.dart @@ -2,7 +2,6 @@ import 'package:app_settings/app_settings.dart'; import 'package:lightmeter/data/caffeine_service.dart'; import 'package:lightmeter/data/haptics_service.dart'; import 'package:lightmeter/data/light_sensor_service.dart'; -import 'package:lightmeter/data/models/film.dart'; import 'package:lightmeter/data/models/volume_action.dart'; import 'package:lightmeter/data/permissions_service.dart'; import 'package:lightmeter/data/shared_prefs_service.dart'; @@ -45,9 +44,6 @@ class MeteringInteractor { NdValue get ndFilter => _userPreferencesService.ndFilter; set ndFilter(NdValue value) => _userPreferencesService.ndFilter = value; - Film get film => _userPreferencesService.film; - set film(Film value) => _userPreferencesService.film = value; - VolumeAction get volumeAction => _userPreferencesService.volumeAction; /// Executes vibration if haptics are enabled in settings diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 3d3ec6b..364deec 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -41,6 +41,9 @@ "meteringScreenFeatureFilmPicker": "Film picker", "meteringScreenFeatureHistogram": "Histogram", "film": "Film", + "filmPush": "Film (push)", + "filmPull": "Film (pull)", + "filmReciprocityHint": "Applies correction for shutter speeds grater than 1 second", "equipment": "Equipment", "equipmentProfileName": "Equipment profile name", "equipmentProfileNameHint": "Praktica MTL5B", @@ -56,6 +59,8 @@ "equipmentProfile": "Equipment profile", "equipmentProfiles": "Equipment profiles", "tapToAdd": "Tap to add", + "filmsInUse": "Films in use", + "filmsInUseDescription": "Select films which you use.", "general": "General", "keepScreenOn": "Keep screen on", "haptics": "Haptics", diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index 4c6f353..22b0356 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -41,9 +41,11 @@ "meteringScreenFeatureFilmPicker": "Sélecteur de film", "meteringScreenFeatureHistogram": "Histogramme", "film": "Pellicule", + "filmPush": "Pellicule (push)", + "filmPull": "Pellicule (pull)", + "filmReciprocityHint": "La correction s'applique aux vitesses d'obturation supérieures à 1 seconde", "equipment": "Équipement", "equipmentProfileName": "Nom du profil de l'équipement", - "tapToAdd": "Appuie pour ajouter", "equipmentProfileNameHint": "Praktica MTL5B", "equipmentProfileAllValues": "Tout", "apertureValues": "Valeurs Aperture", @@ -56,6 +58,9 @@ "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.", "equipmentProfile": "Profil de l'équipement", "equipmentProfiles": "Profils de l'équipement", + "tapToAdd": "Appuie pour ajouter", + "filmsInUse": "Films en usage", + "filmsInUseDescription": "Sélectionnez les films que vous utilisez.", "general": "Général", "keepScreenOn": "Garder l'écran allumé", "haptics": "Haptiques", diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index 7a7c772..9ce0d9b 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -41,6 +41,9 @@ "meteringScreenFeatureFilmPicker": "Выбор пленки", "meteringScreenFeatureHistogram": "Гистограмма", "film": "Пленка", + "filmPush": "Пленка (push)", + "filmPull": "Пленка (pull)", + "filmReciprocityHint": "Применяет коррекцию для выдержек длиннее 1 секунды", "equipment": "Оборудование", "equipmentProfileName": "Название профиля", "equipmentProfileNameHint": "Praktica MTL5B", @@ -56,6 +59,8 @@ "equipmentProfile": "Оборудование", "equipmentProfiles": "Профили оборудования", "tapToAdd": "Нажмите, чтобы добавить", + "filmsInUse": "Используемые пленки", + "filmsInUseDescription": "Выберите пленки, которыми вы пользуетесь.", "general": "Общие", "keepScreenOn": "Запрет блокировки", "haptics": "Вибрация", diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb index 8c4fb07..357ef95 100644 --- a/lib/l10n/intl_zh.arb +++ b/lib/l10n/intl_zh.arb @@ -31,7 +31,7 @@ "thirdStops": "1/3", "calibration": "校准", "calibrationMessage": "此应用测量读数的准确性完全取决于设备的硬件。因此,请考虑测试此应用并手动设置 EV 校准,以获得准确的测量结果。", - "calibrationMessageCameraOnly": "此应用程序测量读数的准确性完全取决于设备的后置摄像头。因此,请考虑测试此应用并手动设置 EV 校准,以获得准确的测量结果。", + "calibrationMessageCameraOnly": "此应用程序测量读数的准确s性完全取决于设备的后置摄像头。因此,请考虑测试此应用并手动设置 EV 校准,以获得准确的测量结果。", "camera": "摄像头", "lightSensor": "光传感器", "meteringScreenLayout": "布局", @@ -41,6 +41,9 @@ "meteringScreenFeatureFilmPicker": "胶片选择", "meteringScreenFeatureHistogram": "直方图", "film": "胶片", + "filmPush": "胶片 (push)", + "filmPull": "胶片 (pull)", + "filmReciprocityHint": "Applies correction for shutter speeds grater than 1 second", "equipment": "设备", "equipmentProfileName": "设备配置名称", "equipmentProfileNameHint": "Praktica MTL5B", @@ -56,6 +59,8 @@ "equipmentProfile": "设备配置", "equipmentProfiles": "设备配置", "tapToAdd": "點擊添加", + "filmsInUse": "Films in use", + "filmsInUseDescription": "Select films which you use.", "general": "通用", "keepScreenOn": "保持屏幕常亮", "haptics": "震动", diff --git a/lib/screens/metering/bloc_metering.dart b/lib/screens/metering/bloc_metering.dart index 8e4a2f6..cbb0356 100644 --- a/lib/screens/metering/bloc_metering.dart +++ b/lib/screens/metering/bloc_metering.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:bloc_concurrency/bloc_concurrency.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:lightmeter/data/models/film.dart'; import 'package:lightmeter/data/models/volume_action.dart'; import 'package:lightmeter/interactors/metering_interactor.dart'; import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart'; @@ -29,7 +28,6 @@ class MeteringBloc extends Bloc { ) : super( MeteringDataState( ev100: null, - film: _meteringInteractor.film, iso: _meteringInteractor.iso, nd: _meteringInteractor.ndFilter, isMetering: false, @@ -42,7 +40,6 @@ class MeteringBloc extends Bloc { .listen(onCommunicationState); on(_onEquipmentProfileChanged); - on(_onFilmChanged); on(_onIsoChanged); on(_onNdChanged); on(_onMeasure, transformer: droppable()); @@ -92,12 +89,9 @@ class MeteringBloc extends Bloc { /// Update selected ISO value and discard selected film, if selected equipment profile /// doesn't contain currently selected value IsoValue iso = state.iso; - Film film = state.film; if (!event.equipmentProfileData.isoValues.any((v) => state.iso.value == v.value)) { _meteringInteractor.iso = event.equipmentProfileData.isoValues.first; iso = event.equipmentProfileData.isoValues.first; - _meteringInteractor.film = Film.values.first; - film = Film.values.first; willUpdateMeasurements = true; } @@ -113,7 +107,6 @@ class MeteringBloc extends Bloc { emit( MeteringDataState( ev100: state.ev100, - film: film, iso: iso, nd: nd, isMetering: state.isMetering, @@ -122,46 +115,12 @@ class MeteringBloc extends Bloc { } } - void _onFilmChanged(FilmChangedEvent event, Emitter emit) { - if (state.film.name != event.film.name) { - _meteringInteractor.film = event.film; - - /// Find `IsoValue` with matching value - IsoValue iso = state.iso; - if (state.iso.value != event.film.iso && event.film != const Film.other()) { - iso = IsoValue.values.firstWhere( - (e) => e.value == event.film.iso, - orElse: () => state.iso, - ); - _meteringInteractor.iso = iso; - } - - /// If user selects 'Other' film we preserve currently selected ISO - /// and therefore only discard reciprocity formula - emit( - MeteringDataState( - ev100: state.ev100, - film: event.film, - iso: iso, - nd: state.nd, - isMetering: state.isMetering, - ), - ); - } - } - void _onIsoChanged(IsoChangedEvent event, Emitter emit) { - /// Discard currently selected film even if ISO is the same, - /// because, for example, Fomapan 400 and any Ilford 400 - /// have different reciprocity formulas - _meteringInteractor.film = Film.values.first; - if (state.iso != event.isoValue) { _meteringInteractor.iso = event.isoValue; emit( MeteringDataState( ev100: state.ev100, - film: Film.values.first, iso: event.isoValue, nd: state.nd, isMetering: state.isMetering, @@ -176,7 +135,6 @@ class MeteringBloc extends Bloc { emit( MeteringDataState( ev100: state.ev100, - film: state.film, iso: state.iso, nd: event.ndValue, isMetering: state.isMetering, @@ -190,7 +148,6 @@ class MeteringBloc extends Bloc { _communicationBloc.add(const communication_events.MeasureEvent()); emit( LoadingState( - film: state.film, iso: state.iso, nd: state.nd, ), @@ -209,7 +166,6 @@ class MeteringBloc extends Bloc { emit( MeteringDataState( ev100: event.ev100, - film: state.film, iso: state.iso, nd: state.nd, isMetering: event.isMetering, @@ -221,7 +177,6 @@ class MeteringBloc extends Bloc { emit( MeteringDataState( ev100: null, - film: state.film, iso: state.iso, nd: state.nd, isMetering: event.isMetering, diff --git a/lib/screens/metering/components/camera_container/components/camera_preview/widget_camera_preview.dart b/lib/screens/metering/components/camera_container/components/camera_preview/widget_camera_preview.dart index ecfcb42..1ae0ca9 100644 --- a/lib/screens/metering/components/camera_container/components/camera_preview/widget_camera_preview.dart +++ b/lib/screens/metering/components/camera_container/components/camera_preview/widget_camera_preview.dart @@ -31,27 +31,7 @@ class _CameraPreviewState extends State { AnimatedSwitcher( duration: Dimens.switchDuration, child: widget.controller != null - ? ValueListenableBuilder( - valueListenable: widget.controller!, - builder: (_, __, ___) => widget.controller!.value.isInitialized - ? Stack( - alignment: Alignment.bottomCenter, - children: [ - CameraView(controller: widget.controller!), - if (UserPreferencesProvider.meteringScreenFeatureOf( - context, - MeteringScreenLayoutFeature.histogram, - )) - Positioned( - left: Dimens.grid8, - right: Dimens.grid8, - bottom: Dimens.grid16, - child: CameraHistogram(controller: widget.controller!), - ), - ], - ) - : const SizedBox.shrink(), - ) + ? _CameraPreviewBuilder(controller: widget.controller!) : CameraViewPlaceholder(error: widget.error), ), ], @@ -60,3 +40,59 @@ class _CameraPreviewState extends State { ); } } + +class _CameraPreviewBuilder extends StatefulWidget { + final CameraController controller; + + const _CameraPreviewBuilder({required this.controller}); + + @override + State<_CameraPreviewBuilder> createState() => _CameraPreviewBuilderState(); +} + +class _CameraPreviewBuilderState extends State<_CameraPreviewBuilder> { + late final ValueNotifier _initializedNotifier = + ValueNotifier(widget.controller.value.isInitialized); + + @override + void initState() { + super.initState(); + widget.controller.addListener(_update); + } + + @override + void dispose() { + widget.controller.removeListener(_update); + _initializedNotifier.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: _initializedNotifier, + builder: (context, value, child) => value + ? Stack( + alignment: Alignment.bottomCenter, + children: [ + CameraView(controller: widget.controller), + if (UserPreferencesProvider.meteringScreenFeatureOf( + context, + MeteringScreenLayoutFeature.histogram, + )) + Positioned( + left: Dimens.grid8, + right: Dimens.grid8, + bottom: Dimens.grid16, + child: CameraHistogram(controller: widget.controller), + ), + ], + ) + : const SizedBox.shrink(), + ); + } + + void _update() { + _initializedNotifier.value = widget.controller.value.isInitialized; + } +} diff --git a/lib/screens/metering/components/camera_container/provider_container_camera.dart b/lib/screens/metering/components/camera_container/provider_container_camera.dart index 0e27700..1d6d8c0 100644 --- a/lib/screens/metering/components/camera_container/provider_container_camera.dart +++ b/lib/screens/metering/components/camera_container/provider_container_camera.dart @@ -1,7 +1,6 @@ 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/screens/metering/communication/bloc_communication_metering.dart'; import 'package:lightmeter/screens/metering/components/camera_container/bloc_container_camera.dart'; import 'package:lightmeter/screens/metering/components/camera_container/event_container_camera.dart'; @@ -12,10 +11,8 @@ import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class CameraContainerProvider extends StatelessWidget { final ExposurePair? fastest; final ExposurePair? slowest; - final Film film; final IsoValue iso; final NdValue nd; - final ValueChanged onFilmChanged; final ValueChanged onIsoChanged; final ValueChanged onNdChanged; final List exposurePairs; @@ -23,10 +20,8 @@ class CameraContainerProvider extends StatelessWidget { const CameraContainerProvider({ required this.fastest, required this.slowest, - required this.film, required this.iso, required this.nd, - required this.onFilmChanged, required this.onIsoChanged, required this.onNdChanged, required this.exposurePairs, @@ -44,10 +39,8 @@ class CameraContainerProvider extends StatelessWidget { child: CameraContainer( fastest: fastest, slowest: slowest, - film: film, iso: iso, nd: nd, - onFilmChanged: onFilmChanged, onIsoChanged: onIsoChanged, onNdChanged: onNdChanged, exposurePairs: exposurePairs, diff --git a/lib/screens/metering/components/camera_container/widget_container_camera.dart b/lib/screens/metering/components/camera_container/widget_container_camera.dart index 0514d92..944aa34 100644 --- a/lib/screens/metering/components/camera_container/widget_container_camera.dart +++ b/lib/screens/metering/components/camera_container/widget_container_camera.dart @@ -3,7 +3,6 @@ import 'dart:math'; 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/platform_config.dart'; import 'package:lightmeter/providers/user_preferences_provider.dart'; @@ -23,10 +22,8 @@ import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class CameraContainer extends StatelessWidget { final ExposurePair? fastest; final ExposurePair? slowest; - final Film film; final IsoValue iso; final NdValue nd; - final ValueChanged onFilmChanged; final ValueChanged onIsoChanged; final ValueChanged onNdChanged; final List exposurePairs; @@ -34,10 +31,8 @@ class CameraContainer extends StatelessWidget { const CameraContainer({ required this.fastest, required this.slowest, - required this.film, required this.iso, required this.nd, - required this.onFilmChanged, required this.onIsoChanged, required this.onNdChanged, required this.exposurePairs, @@ -60,10 +55,8 @@ class CameraContainer extends StatelessWidget { readingsContainer: ReadingsContainer( fastest: fastest, slowest: slowest, - film: film, iso: iso, nd: nd, - onFilmChanged: onFilmChanged, onIsoChanged: onIsoChanged, onNdChanged: onNdChanged, ), diff --git a/lib/screens/metering/components/light_sensor_container/provider_container_light_sensor.dart b/lib/screens/metering/components/light_sensor_container/provider_container_light_sensor.dart index aa27504..1d7f822 100644 --- a/lib/screens/metering/components/light_sensor_container/provider_container_light_sensor.dart +++ b/lib/screens/metering/components/light_sensor_container/provider_container_light_sensor.dart @@ -1,7 +1,6 @@ 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/screens/metering/communication/bloc_communication_metering.dart'; import 'package:lightmeter/screens/metering/components/light_sensor_container/bloc_container_light_sensor.dart'; import 'package:lightmeter/screens/metering/components/light_sensor_container/widget_container_light_sensor.dart'; @@ -11,10 +10,8 @@ import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class LightSensorContainerProvider extends StatelessWidget { final ExposurePair? fastest; final ExposurePair? slowest; - final Film film; final IsoValue iso; final NdValue nd; - final ValueChanged onFilmChanged; final ValueChanged onIsoChanged; final ValueChanged onNdChanged; final List exposurePairs; @@ -22,10 +19,8 @@ class LightSensorContainerProvider extends StatelessWidget { const LightSensorContainerProvider({ required this.fastest, required this.slowest, - required this.film, required this.iso, required this.nd, - required this.onFilmChanged, required this.onIsoChanged, required this.onNdChanged, required this.exposurePairs, @@ -43,10 +38,8 @@ class LightSensorContainerProvider extends StatelessWidget { child: LightSensorContainer( fastest: fastest, slowest: slowest, - film: film, iso: iso, nd: nd, - onFilmChanged: onFilmChanged, onIsoChanged: onIsoChanged, onNdChanged: onNdChanged, exposurePairs: exposurePairs, diff --git a/lib/screens/metering/components/light_sensor_container/widget_container_light_sensor.dart b/lib/screens/metering/components/light_sensor_container/widget_container_light_sensor.dart index 4380f02..2151ee0 100644 --- a/lib/screens/metering/components/light_sensor_container/widget_container_light_sensor.dart +++ b/lib/screens/metering/components/light_sensor_container/widget_container_light_sensor.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/data/models/exposure_pair.dart'; -import 'package:lightmeter/data/models/film.dart'; import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart'; import 'package:lightmeter/screens/metering/components/shared/metering_top_bar/widget_top_bar_metering.dart'; @@ -10,10 +9,8 @@ import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class LightSensorContainer extends StatelessWidget { final ExposurePair? fastest; final ExposurePair? slowest; - final Film film; final IsoValue iso; final NdValue nd; - final ValueChanged onFilmChanged; final ValueChanged onIsoChanged; final ValueChanged onNdChanged; final List exposurePairs; @@ -21,10 +18,8 @@ class LightSensorContainer extends StatelessWidget { const LightSensorContainer({ required this.fastest, required this.slowest, - required this.film, required this.iso, required this.nd, - required this.onFilmChanged, required this.onIsoChanged, required this.onNdChanged, required this.exposurePairs, @@ -39,10 +34,8 @@ class LightSensorContainer extends StatelessWidget { readingsContainer: ReadingsContainer( fastest: fastest, slowest: slowest, - film: film, iso: iso, nd: nd, - onFilmChanged: onFilmChanged, onIsoChanged: onIsoChanged, onNdChanged: onNdChanged, ), diff --git a/lib/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart b/lib/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart index 3f33d03..4051c5e 100644 --- a/lib/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart +++ b/lib/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart @@ -5,6 +5,7 @@ import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/screens/metering/components/shared/exposure_pairs_list/components/exposure_pairs_list_item/widget_item_list_exposure_pairs.dart'; import 'package:lightmeter/screens/shared/icon_placeholder/widget_icon_placeholder.dart'; +import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; class ExposurePairsList extends StatelessWidget { final List exposurePairs; @@ -47,7 +48,8 @@ class ExposurePairsList extends StatelessWidget { child: Align( alignment: Alignment.centerLeft, child: ExposurePairsListItem( - exposurePairs[index].shutterSpeed, + Films.selectedOf(context) + .reciprocityFailure(exposurePairs[index].shutterSpeed), tickOnTheLeft: true, ), ), diff --git a/lib/screens/metering/components/shared/readings_container/components/equipment_profile_picker/widget_picker_equipment_profiles.dart b/lib/screens/metering/components/shared/readings_container/components/equipment_profile_picker/widget_picker_equipment_profiles.dart new file mode 100644 index 0000000..f47cd53 --- /dev/null +++ b/lib/screens/metering/components/shared/readings_container/components/equipment_profile_picker/widget_picker_equipment_profiles.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/widget_picker_dialog_animated.dart'; +import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/reading_value_container/widget_container_reading_value.dart'; +import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; +import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; + +class EquipmentProfilePicker extends StatelessWidget { + const EquipmentProfilePicker(); + + @override + Widget build(BuildContext context) { + return AnimatedDialogPicker( + icon: Icons.camera, + title: S.of(context).equipmentProfile, + selectedValue: EquipmentProfiles.selectedOf(context), + values: EquipmentProfiles.of(context), + itemTitleBuilder: (_, value) => Text(value.id.isEmpty ? S.of(context).none : value.name), + onChanged: EquipmentProfileProvider.of(context).setProfile, + closedChild: ReadingValueContainer.singleValue( + value: ReadingValue( + label: S.of(context).equipmentProfile, + value: EquipmentProfiles.selectedOf(context).id.isEmpty + ? S.of(context).none + : EquipmentProfiles.selectedOf(context).name, + ), + ), + ); + } +} diff --git a/lib/screens/metering/components/shared/readings_container/components/extreme_exposure_pairs_container/widget_container_extreme_exposure_pairs.dart b/lib/screens/metering/components/shared/readings_container/components/extreme_exposure_pairs_container/widget_container_extreme_exposure_pairs.dart new file mode 100644 index 0000000..54c786c --- /dev/null +++ b/lib/screens/metering/components/shared/readings_container/components/extreme_exposure_pairs_container/widget_container_extreme_exposure_pairs.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/data/models/exposure_pair.dart'; +import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/reading_value_container/widget_container_reading_value.dart'; +import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; + +class ExtremeExposurePairsContainer extends StatelessWidget { + final ExposurePair? fastest; + final ExposurePair? slowest; + + const ExtremeExposurePairsContainer({ + required this.fastest, + required this.slowest, + super.key, + }); + + @override + Widget build(BuildContext context) { + return ReadingValueContainer( + values: [ + ReadingValue( + label: S.of(context).fastestExposurePair, + value: _exposurePairToString(context, fastest), + ), + ReadingValue( + label: S.of(context).slowestExposurePair, + value: _exposurePairToString(context, slowest), + ), + ], + ); + } + + String _exposurePairToString(BuildContext context, ExposurePair? pair) { + if (pair == null) { + return '-'; + } + + return '${pair.aperture} - ${Films.selectedOf(context).reciprocityFailure(pair.shutterSpeed)}'; + } +} diff --git a/lib/screens/metering/components/shared/readings_container/components/film_picker/widget_picker_film.dart b/lib/screens/metering/components/shared/readings_container/components/film_picker/widget_picker_film.dart new file mode 100644 index 0000000..ae1e6fe --- /dev/null +++ b/lib/screens/metering/components/shared/readings_container/components/film_picker/widget_picker_film.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/widget_picker_dialog_animated.dart'; +import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/reading_value_container/widget_container_reading_value.dart'; +import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; +import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; + +class FilmPicker extends StatelessWidget { + final IsoValue selectedIso; + + const FilmPicker({required this.selectedIso}); + + @override + Widget build(BuildContext context) { + return AnimatedDialogPicker( + icon: Icons.camera_roll, + title: S.of(context).film, + subtitle: S.of(context).filmReciprocityHint, + selectedValue: Films.selectedOf(context), + values: Films.inUseOf(context), + itemTitleBuilder: (_, value) => Text(value.name.isEmpty ? S.of(context).none : value.name), + onChanged: FilmsProvider.of(context).setFilm, + closedChild: ReadingValueContainer.singleValue( + value: ReadingValue( + label: _label(context), + value: Films.selectedOf(context).name.isEmpty + ? S.of(context).none + : Films.selectedOf(context).name, + ), + ), + ); + } + + String _label(BuildContext context) { + if (Films.selectedOf(context) == const Film.other() || + Films.selectedOf(context).iso == selectedIso.value) { + return S.of(context).film; + } + + final evDiff = IsoValue( + Films.selectedOf(context).iso, + StopType.full, + ).difference(selectedIso); + + if (evDiff > 0) { + return S.of(context).filmPush; + } else if (evDiff < 0) { + return S.of(context).filmPull; + } else { + return S.of(context).film; + } + } +} diff --git a/lib/screens/metering/components/shared/readings_container/components/iso_picker/widget_picker_iso.dart b/lib/screens/metering/components/shared/readings_container/components/iso_picker/widget_picker_iso.dart new file mode 100644 index 0000000..a85b30c --- /dev/null +++ b/lib/screens/metering/components/shared/readings_container/components/iso_picker/widget_picker_iso.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/widget_picker_dialog_animated.dart'; +import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/reading_value_container/widget_container_reading_value.dart'; +import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; + +class IsoValuePicker extends StatelessWidget { + final List values; + final IsoValue selectedValue; + final ValueChanged onChanged; + + const IsoValuePicker({ + required this.selectedValue, + required this.values, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + return AnimatedDialogPicker( + icon: Icons.iso, + title: S.of(context).iso, + subtitle: S.of(context).filmSpeed, + selectedValue: selectedValue, + values: values, + itemTitleBuilder: (_, value) => Text(value.value.toString()), + // using ascending order, because increase in film speed rises EV + itemTrailingBuilder: (selected, value) => value.value != selected.value + ? Text(S.of(context).evValue(selected.toStringDifference(value))) + : null, + onChanged: onChanged, + closedChild: ReadingValueContainer.singleValue( + value: ReadingValue( + label: S.of(context).iso, + value: selectedValue.value.toString(), + ), + ), + ); + } +} diff --git a/lib/screens/metering/components/shared/readings_container/components/nd_picker/widget_picker_nd.dart b/lib/screens/metering/components/shared/readings_container/components/nd_picker/widget_picker_nd.dart new file mode 100644 index 0000000..eda016c --- /dev/null +++ b/lib/screens/metering/components/shared/readings_container/components/nd_picker/widget_picker_nd.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/widget_picker_dialog_animated.dart'; +import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/reading_value_container/widget_container_reading_value.dart'; +import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; + +class NdValuePicker extends StatelessWidget { + final List values; + final NdValue selectedValue; + final ValueChanged onChanged; + + const NdValuePicker({ + required this.selectedValue, + required this.values, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + return AnimatedDialogPicker( + icon: Icons.filter_b_and_w, + title: S.of(context).nd, + subtitle: S.of(context).ndFilterFactor, + selectedValue: selectedValue, + values: values, + itemTitleBuilder: (_, value) => Text( + value.value == 0 ? S.of(context).none : value.value.toString(), + ), + // using descending order, because ND filter darkens image & lowers EV + itemTrailingBuilder: (selected, value) => value.value != selected.value + ? Text(S.of(context).evValue(value.toStringDifference(selected))) + : null, + onChanged: onChanged, + closedChild: ReadingValueContainer.singleValue( + value: ReadingValue( + label: S.of(context).nd, + value: selectedValue.value.toString(), + ), + ), + ); + } +} diff --git a/lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart b/lib/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart similarity index 100% rename from lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart rename to lib/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart diff --git a/lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/components/dialog_picker/widget_picker_dialog.dart b/lib/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/dialog_picker/widget_picker_dialog.dart similarity index 100% rename from lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/components/dialog_picker/widget_picker_dialog.dart rename to lib/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/dialog_picker/widget_picker_dialog.dart diff --git a/lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/widget_picker_dialog_animated.dart b/lib/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/widget_picker_dialog_animated.dart similarity index 89% rename from lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/widget_picker_dialog_animated.dart rename to lib/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/widget_picker_dialog_animated.dart index d123d99..eeeec52 100644 --- a/lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/widget_picker_dialog_animated.dart +++ b/lib/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/widget_picker_dialog_animated.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; - -import 'package:lightmeter/screens/metering/components/shared/readings_container/components/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart'; -import 'package:lightmeter/screens/metering/components/shared/readings_container/components/animated_dialog_picker/components/dialog_picker/widget_picker_dialog.dart'; +import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart'; +import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/dialog_picker/widget_picker_dialog.dart'; // Has to be stateful, so that [GlobalKey] is not recreated. // Otherwise use will no be able to close the dialog after EV value has changed. diff --git a/lib/screens/metering/components/shared/readings_container/components/reading_value_container/widget_container_reading_value.dart b/lib/screens/metering/components/shared/readings_container/components/shared/reading_value_container/widget_container_reading_value.dart similarity index 100% rename from lib/screens/metering/components/shared/readings_container/components/reading_value_container/widget_container_reading_value.dart rename to lib/screens/metering/components/shared/readings_container/components/shared/reading_value_container/widget_container_reading_value.dart diff --git a/lib/screens/metering/components/shared/readings_container/widget_container_readings.dart b/lib/screens/metering/components/shared/readings_container/widget_container_readings.dart index c9f6f54..f10546d 100644 --- a/lib/screens/metering/components/shared/readings_container/widget_container_readings.dart +++ b/lib/screens/metering/components/shared/readings_container/widget_container_readings.dart @@ -1,32 +1,29 @@ 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/generated/l10n.dart'; import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/res/dimens.dart'; -import 'package:lightmeter/screens/metering/components/shared/readings_container/components/animated_dialog_picker/widget_picker_dialog_animated.dart'; -import 'package:lightmeter/screens/metering/components/shared/readings_container/components/reading_value_container/widget_container_reading_value.dart'; +import 'package:lightmeter/screens/metering/components/shared/readings_container/components/equipment_profile_picker/widget_picker_equipment_profiles.dart'; +import 'package:lightmeter/screens/metering/components/shared/readings_container/components/extreme_exposure_pairs_container/widget_container_extreme_exposure_pairs.dart'; +import 'package:lightmeter/screens/metering/components/shared/readings_container/components/film_picker/widget_picker_film.dart'; +import 'package:lightmeter/screens/metering/components/shared/readings_container/components/iso_picker/widget_picker_iso.dart'; +import 'package:lightmeter/screens/metering/components/shared/readings_container/components/nd_picker/widget_picker_nd.dart'; import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class ReadingsContainer extends StatelessWidget { final ExposurePair? fastest; final ExposurePair? slowest; - final Film film; final IsoValue iso; final NdValue nd; - final ValueChanged onFilmChanged; final ValueChanged onIsoChanged; final ValueChanged onNdChanged; const ReadingsContainer({ required this.fastest, required this.slowest, - required this.film, required this.iso, required this.nd, - required this.onFilmChanged, required this.onIsoChanged, required this.onNdChanged, super.key, @@ -41,24 +38,16 @@ class ReadingsContainer extends StatelessWidget { context, MeteringScreenLayoutFeature.equipmentProfiles, )) ...[ - const _EquipmentProfilePicker(), + const EquipmentProfilePicker(), const _InnerPadding(), ], if (UserPreferencesProvider.meteringScreenFeatureOf( 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() : '-', - ), - ], + ExtremeExposurePairsContainer( + fastest: fastest, + slowest: slowest, ), const _InnerPadding(), ], @@ -66,17 +55,13 @@ class ReadingsContainer extends StatelessWidget { context, MeteringScreenLayoutFeature.filmPicker, )) ...[ - _FilmPicker( - values: Film.values, - selectedValue: film, - onChanged: onFilmChanged, - ), + FilmPicker(selectedIso: iso), const _InnerPadding(), ], Row( children: [ Expanded( - child: _IsoValuePicker( + child: IsoValuePicker( selectedValue: iso, values: EquipmentProfiles.selectedOf(context).isoValues, onChanged: onIsoChanged, @@ -84,7 +69,7 @@ class ReadingsContainer extends StatelessWidget { ), const _InnerPadding(), Expanded( - child: _NdValuePicker( + child: NdValuePicker( selectedValue: nd, values: EquipmentProfiles.selectedOf(context).ndValues, onChanged: onNdChanged, @@ -100,129 +85,3 @@ class ReadingsContainer extends StatelessWidget { class _InnerPadding extends SizedBox { const _InnerPadding() : super(height: Dimens.grid8, width: Dimens.grid8); } - -class _EquipmentProfilePicker extends StatelessWidget { - const _EquipmentProfilePicker(); - - @override - Widget build(BuildContext context) { - return AnimatedDialogPicker( - icon: Icons.camera, - title: S.of(context).equipmentProfile, - selectedValue: EquipmentProfiles.selectedOf(context), - values: EquipmentProfiles.of(context), - itemTitleBuilder: (_, value) => Text(value.id.isEmpty ? S.of(context).none : value.name), - onChanged: EquipmentProfileProvider.of(context).setProfile, - closedChild: ReadingValueContainer.singleValue( - value: ReadingValue( - label: S.of(context).equipmentProfile, - value: EquipmentProfiles.selectedOf(context).id.isEmpty - ? S.of(context).none - : EquipmentProfiles.selectedOf(context).name, - ), - ), - ); - } -} - -class _FilmPicker extends StatelessWidget { - final List values; - final Film selectedValue; - final ValueChanged onChanged; - - const _FilmPicker({ - required this.values, - required this.selectedValue, - required this.onChanged, - }); - - @override - Widget build(BuildContext context) { - return AnimatedDialogPicker( - icon: Icons.camera_roll, - title: S.of(context).film, - selectedValue: selectedValue, - values: values, - itemTitleBuilder: (_, value) => Text(value.name.isEmpty ? S.of(context).none : value.name), - onChanged: onChanged, - closedChild: ReadingValueContainer.singleValue( - value: ReadingValue( - label: S.of(context).film, - value: selectedValue.name.isEmpty ? S.of(context).none : selectedValue.name, - ), - ), - ); - } -} - -class _IsoValuePicker extends StatelessWidget { - final List values; - final IsoValue selectedValue; - final ValueChanged onChanged; - - const _IsoValuePicker({ - required this.selectedValue, - required this.values, - required this.onChanged, - }); - - @override - Widget build(BuildContext context) { - return AnimatedDialogPicker( - icon: Icons.iso, - title: S.of(context).iso, - subtitle: S.of(context).filmSpeed, - selectedValue: selectedValue, - values: values, - itemTitleBuilder: (_, value) => Text(value.value.toString()), - // using ascending order, because increase in film speed rises EV - itemTrailingBuilder: (selected, value) => value.value != selected.value - ? Text(S.of(context).evValue(selected.toStringDifference(value))) - : null, - onChanged: onChanged, - closedChild: ReadingValueContainer.singleValue( - value: ReadingValue( - label: S.of(context).iso, - value: selectedValue.value.toString(), - ), - ), - ); - } -} - -class _NdValuePicker extends StatelessWidget { - final List values; - final NdValue selectedValue; - final ValueChanged onChanged; - - const _NdValuePicker({ - required this.selectedValue, - required this.values, - required this.onChanged, - }); - - @override - Widget build(BuildContext context) { - return AnimatedDialogPicker( - icon: Icons.filter_b_and_w, - title: S.of(context).nd, - subtitle: S.of(context).ndFilterFactor, - selectedValue: selectedValue, - values: values, - itemTitleBuilder: (_, value) => Text( - value.value == 0 ? S.of(context).none : value.value.toString(), - ), - // using descending order, because ND filter darkens image & lowers EV - itemTrailingBuilder: (selected, value) => value.value != selected.value - ? Text(S.of(context).evValue(value.toStringDifference(selected))) - : null, - onChanged: onChanged, - closedChild: ReadingValueContainer.singleValue( - value: ReadingValue( - label: S.of(context).nd, - value: selectedValue.value.toString(), - ), - ), - ); - } -} diff --git a/lib/screens/metering/event_metering.dart b/lib/screens/metering/event_metering.dart index bf3a22d..0a39844 100644 --- a/lib/screens/metering/event_metering.dart +++ b/lib/screens/metering/event_metering.dart @@ -1,4 +1,3 @@ -import 'package:lightmeter/data/models/film.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; sealed class MeteringEvent { @@ -11,12 +10,6 @@ class EquipmentProfileChangedEvent extends MeteringEvent { const EquipmentProfileChangedEvent(this.equipmentProfileData); } -class FilmChangedEvent extends MeteringEvent { - final Film film; - - const FilmChangedEvent(this.film); -} - class IsoChangedEvent extends MeteringEvent { final IsoValue isoValue; diff --git a/lib/screens/metering/screen_metering.dart b/lib/screens/metering/screen_metering.dart index 93d515e..f1d11fb 100644 --- a/lib/screens/metering/screen_metering.dart +++ b/lib/screens/metering/screen_metering.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; 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/providers/services_provider.dart'; import 'package:lightmeter/providers/user_preferences_provider.dart'; @@ -33,11 +32,8 @@ class MeteringScreen extends StatelessWidget { child: BlocBuilder( builder: (_, state) => MeteringContainerBuidler( ev: state is MeteringDataState ? state.ev : null, - film: state.film, iso: state.iso, nd: state.nd, - onFilmChanged: (value) => - context.read().add(FilmChangedEvent(value)), onIsoChanged: (value) => context.read().add(IsoChangedEvent(value)), onNdChanged: (value) => context.read().add(NdChangedEvent(value)), ), @@ -81,7 +77,7 @@ class _InheritedListeners extends StatelessWidget { feature: MeteringScreenLayoutFeature.filmPicker, onDidChangeDependencies: (value) { if (!value) { - context.read().add(const FilmChangedEvent(Film.other())); + FilmsProvider.of(context).setFilm(const Film.other()); } }, child: child, @@ -92,19 +88,15 @@ class _InheritedListeners extends StatelessWidget { class MeteringContainerBuidler extends StatelessWidget { final double? ev; - final Film film; final IsoValue iso; final NdValue nd; - final ValueChanged onFilmChanged; final ValueChanged onIsoChanged; final ValueChanged onNdChanged; const MeteringContainerBuidler({ required this.ev, - required this.film, required this.iso, required this.nd, - required this.onFilmChanged, required this.onIsoChanged, required this.onNdChanged, }); @@ -116,7 +108,6 @@ class MeteringContainerBuidler extends StatelessWidget { ev!, UserPreferencesProvider.stopTypeOf(context), EquipmentProfiles.selectedOf(context), - film, ) : []; final fastest = exposurePairs.isNotEmpty ? exposurePairs.first : null; @@ -126,10 +117,8 @@ class MeteringContainerBuidler extends StatelessWidget { ? CameraContainerProvider( fastest: fastest, slowest: slowest, - film: film, iso: iso, nd: nd, - onFilmChanged: onFilmChanged, onIsoChanged: onIsoChanged, onNdChanged: onNdChanged, exposurePairs: exposurePairs, @@ -137,10 +126,8 @@ class MeteringContainerBuidler extends StatelessWidget { : LightSensorContainerProvider( fastest: fastest, slowest: slowest, - film: film, iso: iso, nd: nd, - onFilmChanged: onFilmChanged, onIsoChanged: onIsoChanged, onNdChanged: onNdChanged, exposurePairs: exposurePairs, @@ -152,7 +139,6 @@ class MeteringContainerBuidler extends StatelessWidget { double ev, StopType stopType, EquipmentProfile equipmentProfile, - Film film, ) { if (ev.isNaN || ev.isInfinite) { return List.empty(); @@ -195,7 +181,7 @@ class MeteringContainerBuidler extends StatelessWidget { itemsCount, (index) => ExposurePair( apertureValues[index + apertureOffset], - film.reciprocityFailure(shutterSpeedValues[index + shutterSpeedOffset]), + shutterSpeedValues[index + shutterSpeedOffset], ), growable: false, ); diff --git a/lib/screens/metering/state_metering.dart b/lib/screens/metering/state_metering.dart index 909895d..74dc792 100644 --- a/lib/screens/metering/state_metering.dart +++ b/lib/screens/metering/state_metering.dart @@ -1,18 +1,15 @@ import 'package:flutter/material.dart'; -import 'package:lightmeter/data/models/film.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; @immutable abstract class MeteringState { final double? ev100; - final Film film; final IsoValue iso; final NdValue nd; final bool isMetering; const MeteringState({ this.ev100, - required this.film, required this.iso, required this.nd, required this.isMetering, @@ -21,7 +18,6 @@ abstract class MeteringState { class LoadingState extends MeteringState { const LoadingState({ - required super.film, required super.iso, required super.nd, }) : super(isMetering: true); @@ -30,7 +26,6 @@ class LoadingState extends MeteringState { class MeteringDataState extends MeteringState { const MeteringDataState({ required super.ev100, - required super.film, required super.iso, required super.nd, required super.isMetering, diff --git a/lib/screens/metering/utils/equipment_profile_listener.dart b/lib/screens/metering/utils/equipment_profile_listener.dart deleted file mode 100644 index 68d03dc..0000000 --- a/lib/screens/metering/utils/equipment_profile_listener.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; -import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; - -class EquipmentProfileListener extends StatefulWidget { - final ValueChanged onDidChangeDependencies; - final Widget child; - - const EquipmentProfileListener({ - required this.onDidChangeDependencies, - required this.child, - super.key, - }); - - @override - State createState() => _EquipmentProfileListenerState(); -} - -class _EquipmentProfileListenerState extends State { - @override - void didChangeDependencies() { - super.didChangeDependencies(); - widget.onDidChangeDependencies(EquipmentProfiles.selectedOf(context)); - } - - @override - Widget build(BuildContext context) { - return widget.child; - } -} diff --git a/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/widget_list_tiles_equipments.dart b/lib/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/widget_list_tiles_equipments.dart similarity index 90% rename from lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/widget_list_tiles_equipments.dart rename to lib/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/widget_list_tiles_equipments.dart index 695e4e3..482c370 100644 --- a/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/widget_list_tiles_equipments.dart +++ b/lib/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/widget_list_tiles_equipments.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/components/dialog_filter/widget_dialog_filter.dart'; -import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/components/dialog_range_picker/widget_dialog_picker_range.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 { diff --git a/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart b/lib/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart similarity index 94% rename from lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart rename to lib/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart index c11cd2c..244ddce 100644 --- a/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart +++ b/lib/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart @@ -3,8 +3,8 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.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_name_dialog/widget_dialog_equipment_profile_name.dart'; +import 'package:lightmeter/screens/settings/components/equipment/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/equipment/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_name_dialog/widget_dialog_equipment_profile_name.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class EquipmentProfileContainer extends StatefulWidget { diff --git a/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_name_dialog/widget_dialog_equipment_profile_name.dart b/lib/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_name_dialog/widget_dialog_equipment_profile_name.dart similarity index 100% rename from lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_name_dialog/widget_dialog_equipment_profile_name.dart rename to lib/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_name_dialog/widget_dialog_equipment_profile_name.dart diff --git a/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart b/lib/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart similarity index 92% rename from lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart rename to lib/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart index 5307145..a5b0eca 100644 --- a/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart +++ b/lib/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/res/dimens.dart'; -import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.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/equipment/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart'; +import 'package:lightmeter/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_name_dialog/widget_dialog_equipment_profile_name.dart'; import 'package:lightmeter/screens/shared/icon_placeholder/widget_icon_placeholder.dart'; import 'package:lightmeter/screens/shared/sliver_screen/screen_sliver.dart'; import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; diff --git a/lib/screens/settings/components/equipment/components/equipment_profiles/widget_list_tile_equipment_profiles.dart b/lib/screens/settings/components/equipment/components/equipment_profiles/widget_list_tile_equipment_profiles.dart new file mode 100644 index 0000000..0cb5631 --- /dev/null +++ b/lib/screens/settings/components/equipment/components/equipment_profiles/widget_list_tile_equipment_profiles.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart'; +import 'package:lightmeter/screens/settings/components/shared/iap_list_tile/widget_list_tile_iap.dart'; +import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; + +class EquipmentProfilesListTile extends StatelessWidget { + const EquipmentProfilesListTile({super.key}); + + @override + Widget build(BuildContext context) { + return IAPListTile( + leading: const Icon(Icons.camera), + title: Text(S.of(context).equipmentProfiles), + onTap: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const EquipmentProfilesScreen()), + ); + }, + ); + } +} diff --git a/lib/screens/settings/components/equipment/components/films/widget_list_tile_films.dart b/lib/screens/settings/components/equipment/components/films/widget_list_tile_films.dart new file mode 100644 index 0000000..c343e2b --- /dev/null +++ b/lib/screens/settings/components/equipment/components/films/widget_list_tile_films.dart @@ -0,0 +1,35 @@ +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/iap_list_tile/widget_list_tile_iap.dart'; +import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; +import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; + +class FilmsListTile extends StatelessWidget { + const FilmsListTile({super.key}); + + @override + Widget build(BuildContext context) { + return IAPListTile( + leading: const Icon(Icons.camera_roll), + title: Text(S.of(context).filmsInUse), + onTap: () { + showDialog>( + context: context, + builder: (_) => DialogFilter( + icon: const Icon(Icons.camera_roll), + title: S.of(context).filmsInUse, + description: S.of(context).filmsInUseDescription, + values: Films.of(context).sublist(1), + selectedValues: Films.inUseOf(context), + titleAdapter: (_, value) => value.name, + ), + ).then((values) { + if (values != null) { + FilmsProvider.of(context).saveFilms(values); + } + }); + }, + ); + } +} diff --git a/lib/screens/settings/components/equipment/widget_settings_section_equipment.dart b/lib/screens/settings/components/equipment/widget_settings_section_equipment.dart new file mode 100644 index 0000000..ba3b975 --- /dev/null +++ b/lib/screens/settings/components/equipment/widget_settings_section_equipment.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/screens/settings/components/equipment/components/equipment_profiles/widget_list_tile_equipment_profiles.dart'; +import 'package:lightmeter/screens/settings/components/equipment/components/films/widget_list_tile_films.dart'; +import 'package:lightmeter/screens/settings/components/shared/settings_section/widget_settings_section.dart'; + +class EquipmentSettingsSection extends StatelessWidget { + const EquipmentSettingsSection({super.key}); + + @override + Widget build(BuildContext context) { + return SettingsSection( + title: S.of(context).equipment, + children: const [ + EquipmentProfilesListTile(), + FilmsListTile(), + ], + ); + } +} diff --git a/lib/screens/settings/components/general/components/language/widget_list_tile_language.dart b/lib/screens/settings/components/general/components/language/widget_list_tile_language.dart index 663c634..eeaaf4c 100644 --- a/lib/screens/settings/components/general/components/language/widget_list_tile_language.dart +++ b/lib/screens/settings/components/general/components/language/widget_list_tile_language.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/data/models/supported_locale.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/providers/user_preferences_provider.dart'; -import 'package:lightmeter/screens/settings/components/shared/dialog_picker.dart/widget_dialog_picker.dart'; +import 'package:lightmeter/screens/settings/components/shared/dialog_picker/widget_dialog_picker.dart'; class LanguageListTile extends StatelessWidget { const LanguageListTile({super.key}); diff --git a/lib/screens/settings/components/metering/components/equipment_profiles/widget_list_tile_equipment_profiles.dart b/lib/screens/settings/components/metering/components/equipment_profiles/widget_list_tile_equipment_profiles.dart deleted file mode 100644 index 774e215..0000000 --- a/lib/screens/settings/components/metering/components/equipment_profiles/widget_list_tile_equipment_profiles.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'dart:developer'; - -import 'package:flutter/material.dart'; -import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/res/dimens.dart'; -import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart'; -import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; -import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; - -class EquipmentProfilesListTile extends StatelessWidget { - const EquipmentProfilesListTile({super.key}); - - @override - Widget build(BuildContext context) { - final paidStatus = IAPProducts.productOf(context, IAPProductType.paidFeatures)?.status ?? - IAPProductStatus.pending; - log(paidStatus.toString()); - return ListTile( - leading: const Icon(Icons.camera), - title: Text(S.of(context).equipmentProfiles), - onTap: switch (paidStatus) { - IAPProductStatus.purchased => () { - Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const EquipmentProfilesScreen()), - ); - }, - IAPProductStatus.pending => null, - _ => () { - IAPProductsProvider.of(context).buy(IAPProductType.paidFeatures); - }, - }, - trailing: switch (paidStatus) { - IAPProductStatus.purchasable => const Icon(Icons.lock), - IAPProductStatus.pending => const SizedBox( - height: Dimens.grid24, - width: Dimens.grid24, - child: CircularProgressIndicator(), - ), - _ => null, - }, - ); - } -} diff --git a/lib/screens/settings/components/metering/components/fractional_stops/widget_list_tile_fractional_stops.dart b/lib/screens/settings/components/metering/components/fractional_stops/widget_list_tile_fractional_stops.dart index 1bcf6bc..e8b99d1 100644 --- a/lib/screens/settings/components/metering/components/fractional_stops/widget_list_tile_fractional_stops.dart +++ b/lib/screens/settings/components/metering/components/fractional_stops/widget_list_tile_fractional_stops.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/providers/user_preferences_provider.dart'; -import 'package:lightmeter/screens/settings/components/shared/dialog_picker.dart/widget_dialog_picker.dart'; +import 'package:lightmeter/screens/settings/components/shared/dialog_picker/widget_dialog_picker.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class StopTypeListTile extends StatelessWidget { diff --git a/lib/screens/settings/components/metering/widget_settings_section_metering.dart b/lib/screens/settings/components/metering/widget_settings_section_metering.dart index 9f86709..c5b9e1d 100644 --- a/lib/screens/settings/components/metering/widget_settings_section_metering.dart +++ b/lib/screens/settings/components/metering/widget_settings_section_metering.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/screens/settings/components/metering/components/calibration/widget_list_tile_calibration.dart'; -import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/widget_list_tile_equipment_profiles.dart'; import 'package:lightmeter/screens/settings/components/metering/components/fractional_stops/widget_list_tile_fractional_stops.dart'; import 'package:lightmeter/screens/settings/components/metering/components/metering_screen_layout/widget_list_tile_metering_screen_layout.dart'; import 'package:lightmeter/screens/settings/components/shared/settings_section/widget_settings_section.dart'; @@ -17,7 +16,6 @@ class MeteringSettingsSection extends StatelessWidget { StopTypeListTile(), CalibrationListTile(), MeteringScreenLayoutListTile(), - EquipmentProfilesListTile(), ], ); } diff --git a/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/components/dialog_filter/widget_dialog_filter.dart b/lib/screens/settings/components/shared/dialog_filter/widget_dialog_filter.dart similarity index 92% rename from lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/components/dialog_filter/widget_dialog_filter.dart rename to lib/screens/settings/components/shared/dialog_filter/widget_dialog_filter.dart index d91752e..8bafcae 100644 --- a/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/components/dialog_filter/widget_dialog_filter.dart +++ b/lib/screens/settings/components/shared/dialog_filter/widget_dialog_filter.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/res/dimens.dart'; -import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; -class DialogFilter extends StatefulWidget { +class DialogFilter extends StatefulWidget { final Icon icon; final String title; final String description; @@ -25,10 +24,10 @@ class DialogFilter extends StatefulWidget { State> createState() => _DialogFilterState(); } -class _DialogFilterState extends State> { +class _DialogFilterState extends State> { late final List checkboxValues = List.generate( widget.values.length, - (index) => widget.selectedValues.any((element) => element.value == widget.values[index].value), + (index) => widget.selectedValues.any((element) => element == widget.values[index]), growable: false, ); diff --git a/lib/screens/settings/components/shared/dialog_picker.dart/widget_dialog_picker.dart b/lib/screens/settings/components/shared/dialog_picker/widget_dialog_picker.dart similarity index 100% rename from lib/screens/settings/components/shared/dialog_picker.dart/widget_dialog_picker.dart rename to lib/screens/settings/components/shared/dialog_picker/widget_dialog_picker.dart diff --git a/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/components/dialog_range_picker/widget_dialog_picker_range.dart b/lib/screens/settings/components/shared/dialog_range_picker/widget_dialog_picker_range.dart similarity index 100% rename from lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/components/dialog_range_picker/widget_dialog_picker_range.dart rename to lib/screens/settings/components/shared/dialog_range_picker/widget_dialog_picker_range.dart diff --git a/lib/screens/settings/components/shared/iap_list_tile/widget_list_tile_iap.dart b/lib/screens/settings/components/shared/iap_list_tile/widget_list_tile_iap.dart new file mode 100644 index 0000000..8c79f37 --- /dev/null +++ b/lib/screens/settings/components/shared/iap_list_tile/widget_list_tile_iap.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; + +/// Depends on the product status and replaces [onTap] with purchase callback +/// if the product is purchasable. +class IAPListTile extends StatelessWidget { + final IAPProductType product; + final Icon leading; + final Text title; + final VoidCallback onTap; + + const IAPListTile({ + this.product = IAPProductType.paidFeatures, + required this.leading, + required this.title, + required this.onTap, + super.key, + }); + + @override + Widget build(BuildContext context) { + return ListTile( + leading: leading, + title: title, + onTap: switch (IAPProducts.productOf(context, product)?.status) { + IAPProductStatus.purchasable => () => IAPProductsProvider.of(context).buy(product), + IAPProductStatus.pending => null, + IAPProductStatus.purchased => onTap, + null => null, + }, + ); + } +} diff --git a/lib/screens/settings/components/shared/settings_section/widget_settings_section.dart b/lib/screens/settings/components/shared/settings_section/widget_settings_section.dart index be62847..9f88d7d 100644 --- a/lib/screens/settings/components/shared/settings_section/widget_settings_section.dart +++ b/lib/screens/settings/components/shared/settings_section/widget_settings_section.dart @@ -4,10 +4,12 @@ import 'package:lightmeter/res/dimens.dart'; class SettingsSection extends StatelessWidget { final String title; final List children; + final bool enabled; const SettingsSection({ required this.title, required this.children, + this.enabled = true, super.key, }); @@ -23,22 +25,25 @@ class SettingsSection extends StatelessWidget { child: Card( child: Padding( padding: const EdgeInsets.symmetric(vertical: Dimens.paddingM), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM), - child: Text( - title, - style: Theme.of(context) - .textTheme - .labelLarge - ?.copyWith(color: Theme.of(context).colorScheme.onSurface), + child: Opacity( + opacity: enabled ? Dimens.enabledOpacity : Dimens.disabledOpacity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM), + child: Text( + title, + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith(color: Theme.of(context).colorScheme.onSurface), + ), ), - ), - ...children, - ], + ...children, + ], + ), ), ), ), diff --git a/lib/screens/settings/components/theme/components/theme_type/widget_list_tile_theme_type.dart b/lib/screens/settings/components/theme/components/theme_type/widget_list_tile_theme_type.dart index 8a95161..cfcd694 100644 --- a/lib/screens/settings/components/theme/components/theme_type/widget_list_tile_theme_type.dart +++ b/lib/screens/settings/components/theme/components/theme_type/widget_list_tile_theme_type.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/data/models/theme_type.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/providers/user_preferences_provider.dart'; -import 'package:lightmeter/screens/settings/components/shared/dialog_picker.dart/widget_dialog_picker.dart'; +import 'package:lightmeter/screens/settings/components/shared/dialog_picker/widget_dialog_picker.dart'; class ThemeTypeListTile extends StatelessWidget { const ThemeTypeListTile({super.key}); diff --git a/lib/screens/settings/screen_settings.dart b/lib/screens/settings/screen_settings.dart index 38256e8..7d057cf 100644 --- a/lib/screens/settings/screen_settings.dart +++ b/lib/screens/settings/screen_settings.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/screens/settings/components/about/widget_settings_section_about.dart'; +import 'package:lightmeter/screens/settings/components/equipment/widget_settings_section_equipment.dart'; import 'package:lightmeter/screens/settings/components/general/widget_settings_section_general.dart'; import 'package:lightmeter/screens/settings/components/metering/widget_settings_section_metering.dart'; import 'package:lightmeter/screens/settings/components/theme/widget_settings_section_theme.dart'; @@ -43,6 +44,7 @@ class _SettingsScreenState extends State { delegate: SliverChildListDelegate( [ const MeteringSettingsSection(), + const EquipmentSettingsSection(), const GeneralSettingsSection(), const ThemeSettingsSection(), const AboutSettingsSection(), diff --git a/m3_lightmeter.code-workspace b/m3_lightmeter.code-workspace new file mode 100644 index 0000000..35520eb --- /dev/null +++ b/m3_lightmeter.code-workspace @@ -0,0 +1,11 @@ +{ + "folders": [ + { + "path": "iap" + }, + { + "path": "." + } + ], + "settings": {} +} \ No newline at end of file diff --git a/test/data/models/film_test.dart b/test/data/models/film_test.dart deleted file mode 100644 index 2feb690..0000000 --- a/test/data/models/film_test.dart +++ /dev/null @@ -1,121 +0,0 @@ -import 'package:lightmeter/data/models/film.dart'; -import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; -import 'package:test/test.dart'; - -void main() { - test('iso', () { - expect(const Film.other().iso, 0); - expect(const FomapanFilm.creative100().iso, 100); - expect(const FomapanFilm.creative200().iso, 200); - expect(const FomapanFilm.action400().iso, 400); - expect(const IlfordFilm.ortho().iso, 80); - expect(const IlfordFilm.delta100().iso, 100); - expect(const IlfordFilm.delta400().iso, 400); - expect(const IlfordFilm.delta3200().iso, 3200); - expect(const IlfordFilm.fp4().iso, 125); - expect(const IlfordFilm.hp5().iso, 400); - expect(const IlfordFilm.panf().iso, 50); - expect(const IlfordFilm.sfx200().iso, 200); - expect(const IlfordFilm.xp2super().iso, 400); - expect(const IlfordFilm.pan100().iso, 100); - expect(const IlfordFilm.pan400().iso, 400); - expect(const KodakFilm.tmax100().iso, 100); - expect(const KodakFilm.tmax400().iso, 400); - expect(const KodakFilm.tmax3200().iso, 3200); - expect(const KodakFilm.trix320().iso, 320); - expect(const KodakFilm.trix400().iso, 400); - }); - - test('toString()', () { - expect(const Film.other().toString(), ""); - expect(const FomapanFilm.creative100().toString(), "Fomapan CREATIVE 100"); - expect(const FomapanFilm.creative200().toString(), "Fomapan CREATIVE 200"); - expect(const FomapanFilm.action400().toString(), "Fomapan ACTION 400"); - expect(const IlfordFilm.ortho().toString(), "Ilford ORTHO+"); - expect(const IlfordFilm.delta100().toString(), "Ilford DELTA 100"); - expect(const IlfordFilm.delta400().toString(), "Ilford DELTA 400"); - expect(const IlfordFilm.delta3200().toString(), "Ilford DELTA 3200"); - expect(const IlfordFilm.fp4().toString(), "Ilford FP4+"); - expect(const IlfordFilm.hp5().toString(), "Ilford HP5+"); - expect(const IlfordFilm.panf().toString(), "Ilford Pan F+"); - expect(const IlfordFilm.sfx200().toString(), "Ilford SFX 200"); - expect(const IlfordFilm.xp2super().toString(), "Ilford XP2 SUPER"); - expect(const IlfordFilm.pan100().toString(), "Kentemere 100"); - expect(const IlfordFilm.pan400().toString(), "Kentemere 400"); - expect(const KodakFilm.tmax100().toString(), "Kodak T-MAX 100"); - expect(const KodakFilm.tmax400().toString(), "Kodak T-MAX 400"); - expect(const KodakFilm.tmax3200().toString(), "Kodak T-MAX 3200"); - expect(const KodakFilm.trix320().toString(), "Kodak TRI-X 320"); - expect(const KodakFilm.trix400().toString(), "Kodak TRI-X 400"); - }); - - group( - 'reciprocityFailure', - () { - const inputSpeeds = [ - ShutterSpeedValue(1000, true, StopType.full), - ShutterSpeedValue(1, false, StopType.full), - ShutterSpeedValue(16, false, StopType.full) - ]; - test('No change `Film.other()`', () { - expect( - const Film.other().reciprocityFailure(inputSpeeds[0]), - const ShutterSpeedValue(1000, true, StopType.full), - ); - expect( - const Film.other().reciprocityFailure(inputSpeeds[1]), - const ShutterSpeedValue(1, false, StopType.full), - ); - expect( - const Film.other().reciprocityFailure(inputSpeeds[2]), - const ShutterSpeedValue(16, false, StopType.full), - ); - }); - - test('pow `IlfordFilm.delta100()`', () { - expect( - const IlfordFilm.delta100().reciprocityFailure(inputSpeeds[0]), - const ShutterSpeedValue(1000, true, StopType.full), - ); - expect( - const IlfordFilm.delta100().reciprocityFailure(inputSpeeds[1]), - const ShutterSpeedValue(1, false, StopType.full), - ); - expect( - const IlfordFilm.delta100().reciprocityFailure(inputSpeeds[2]), - const ShutterSpeedValue(32.899642452994128, false, StopType.full), - ); - }); - - test('log10polynomian `FomapanFilm.creative100()`', () { - expect( - const FomapanFilm.creative100().reciprocityFailure(inputSpeeds[0]), - const ShutterSpeedValue(1000, true, StopType.full), - ); - expect( - const FomapanFilm.creative100().reciprocityFailure(inputSpeeds[1]), - const ShutterSpeedValue(2, false, StopType.full), - ); - expect( - const FomapanFilm.creative100().reciprocityFailure(inputSpeeds[2]), - const ShutterSpeedValue(151.52807753457483, false, StopType.full), - ); - }); - - test('log10polynomian `Kodak.tmax400()`', () { - expect( - const KodakFilm.tmax400().reciprocityFailure(inputSpeeds[0]), - const ShutterSpeedValue(1000, true, StopType.full), - ); - expect( - const KodakFilm.tmax400().reciprocityFailure(inputSpeeds[1]), - const ShutterSpeedValue(1.3333333333333333, false, StopType.full), - ); - expect( - const KodakFilm.tmax400().reciprocityFailure(inputSpeeds[2]), - const ShutterSpeedValue(27.166026086819844, false, StopType.full), - ); - }); - }, - ); -} diff --git a/test/data/shared_prefs_service_test.dart b/test/data/shared_prefs_service_test.dart index 2a63fe5..0896626 100644 --- a/test/data/shared_prefs_service_test.dart +++ b/test/data/shared_prefs_service_test.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:lightmeter/data/models/ev_source_type.dart'; -import 'package:lightmeter/data/models/film.dart'; import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; import 'package:lightmeter/data/models/supported_locale.dart'; import 'package:lightmeter/data/models/theme_type.dart'; @@ -392,26 +391,4 @@ void main() { .called(1); }); }); - - group('film', () { - test('get default', () { - when(() => sharedPreferences.getString(UserPreferencesService.filmKey)).thenReturn(null); - expect(service.film, Film.values.first); - }); - - test('get', () { - when(() => sharedPreferences.getString(UserPreferencesService.filmKey)) - .thenReturn('Fomapan ACTION 400'); - expect(service.film, const FomapanFilm.action400()); - }); - - test('set', () { - when(() => sharedPreferences.setString(UserPreferencesService.filmKey, 'Fomapan ACTION 400')) - .thenAnswer((_) => Future.value(true)); - service.film = const FomapanFilm.action400(); - verify( - () => sharedPreferences.setString(UserPreferencesService.filmKey, 'Fomapan ACTION 400'), - ).called(1); - }); - }); } diff --git a/test/interactors/metering_interactor_test.dart b/test/interactors/metering_interactor_test.dart index bd6a169..1f0b05a 100644 --- a/test/interactors/metering_interactor_test.dart +++ b/test/interactors/metering_interactor_test.dart @@ -2,7 +2,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:lightmeter/data/caffeine_service.dart'; import 'package:lightmeter/data/haptics_service.dart'; import 'package:lightmeter/data/light_sensor_service.dart'; -import 'package:lightmeter/data/models/film.dart'; import 'package:lightmeter/data/models/volume_action.dart'; import 'package:lightmeter/data/permissions_service.dart'; import 'package:lightmeter/data/shared_prefs_service.dart'; @@ -124,19 +123,6 @@ void main() { interactor.ndFilter = NdValue.values.first; verify(() => mockUserPreferencesService.ndFilter = NdValue.values.first).called(1); }); - - test('film - get', () async { - when(() => mockUserPreferencesService.film).thenReturn(Film.values.first); - expect(interactor.film, Film.values.first); - verify(() => mockUserPreferencesService.film).called(1); - }); - - test('film - set', () async { - when(() => mockUserPreferencesService.film = Film.values.first) - .thenReturn(Film.values.first); - interactor.film = Film.values.first; - verify(() => mockUserPreferencesService.film = Film.values.first).called(1); - }); }, ); diff --git a/test/screens/metering/bloc_metering_test.dart b/test/screens/metering/bloc_metering_test.dart index 45b9291..fbffbbc 100644 --- a/test/screens/metering/bloc_metering_test.dart +++ b/test/screens/metering/bloc_metering_test.dart @@ -1,5 +1,4 @@ import 'package:bloc_test/bloc_test.dart'; -import 'package:lightmeter/data/models/film.dart'; import 'package:lightmeter/data/models/volume_action.dart'; import 'package:lightmeter/interactors/metering_interactor.dart'; import 'package:lightmeter/screens/metering/bloc_metering.dart'; @@ -34,7 +33,6 @@ void main() { meteringInteractor = _MockMeteringInteractor(); when(() => meteringInteractor.iso).thenReturn(iso100); when(() => meteringInteractor.ndFilter).thenReturn(NdValue.values.first); - when(() => meteringInteractor.film).thenReturn(Film.values.first); when(meteringInteractor.quickVibration).thenAnswer((_) async {}); when(meteringInteractor.responseVibration).thenAnswer((_) async {}); when(meteringInteractor.errorVibration).thenAnswer((_) async {}); @@ -157,7 +155,6 @@ void main() { build: () => bloc, seed: () => MeteringDataState( ev100: 1.0, - film: Film.values[1], iso: const IsoValue(100, StopType.full), nd: NdValue.values.first, isMetering: false, @@ -166,14 +163,12 @@ void main() { bloc.add(const IsoChangedEvent(IsoValue(200, StopType.full))); }, verify: (_) { - verify(() => meteringInteractor.film = Film.values.first).called(1); verify(() => meteringInteractor.iso = const IsoValue(200, StopType.full)).called(1); }, expect: () => [ isA() .having((state) => state.ev100, 'ev100', 1.0) .having((state) => state.ev, 'ev', 2.0) - .having((state) => state.film, 'film', Film.values.first) .having((state) => state.iso, 'iso', const IsoValue(200, StopType.full)) .having((state) => state.nd, 'nd', NdValue.values.first) .having((state) => state.isMetering, 'isMetering', false), @@ -185,7 +180,6 @@ void main() { build: () => bloc, seed: () => MeteringDataState( ev100: null, - film: Film.values[1], iso: const IsoValue(100, StopType.full), nd: NdValue.values.first, isMetering: false, @@ -194,14 +188,12 @@ void main() { bloc.add(const IsoChangedEvent(IsoValue(200, StopType.full))); }, verify: (_) { - verify(() => meteringInteractor.film = Film.values.first).called(1); verify(() => meteringInteractor.iso = const IsoValue(200, StopType.full)).called(1); }, expect: () => [ isA() .having((state) => state.ev100, 'ev100', null) .having((state) => state.ev, 'ev', null) - .having((state) => state.film, 'film', Film.values.first) .having((state) => state.iso, 'iso', const IsoValue(200, StopType.full)) .having((state) => state.nd, 'nd', NdValue.values.first) .having((state) => state.isMetering, 'isMetering', false), @@ -213,7 +205,6 @@ void main() { build: () => bloc, seed: () => MeteringDataState( ev100: 1.0, - film: Film.values[1], iso: const IsoValue(100, StopType.full), nd: NdValue.values.first, isMetering: false, @@ -222,7 +213,6 @@ void main() { bloc.add(const IsoChangedEvent(IsoValue(100, StopType.full))); }, verify: (_) { - verify(() => meteringInteractor.film = Film.values.first).called(1); verifyNever(() => meteringInteractor.iso = const IsoValue(100, StopType.full)); }, expect: () => [], @@ -233,7 +223,6 @@ void main() { build: () => bloc, seed: () => MeteringDataState( ev100: 1.0, - film: Film.values[1], iso: const IsoValue(100, StopType.full), nd: NdValue.values.first, isMetering: false, @@ -244,14 +233,12 @@ void main() { bloc.onCommunicationState(const communication_states.MeteringEndedState(2)); }, verify: (_) { - verify(() => meteringInteractor.film = Film.values.first).called(1); verify(() => meteringInteractor.iso = const IsoValue(200, StopType.full)).called(1); }, expect: () => [ isA() .having((state) => state.ev100, 'ev100', 1.0) .having((state) => state.ev, 'ev', 2.0) - .having((state) => state.film, 'film', Film.values.first) .having((state) => state.iso, 'iso', const IsoValue(200, StopType.full)) .having((state) => state.nd, 'nd', NdValue.values.first) .having((state) => state.isMetering, 'isMetering', false), @@ -259,7 +246,6 @@ void main() { isA() .having((state) => state.ev100, 'ev100', 2.0) .having((state) => state.ev, 'ev', 3.0) - .having((state) => state.film, 'film', Film.values.first) .having((state) => state.iso, 'iso', const IsoValue(200, StopType.full)) .having((state) => state.nd, 'nd', NdValue.values.first) .having((state) => state.isMetering, 'isMetering', false), @@ -276,7 +262,6 @@ void main() { build: () => bloc, seed: () => MeteringDataState( ev100: 1.0, - film: Film.values[1], iso: const IsoValue(100, StopType.full), nd: NdValue.values.first, isMetering: false, @@ -291,7 +276,6 @@ void main() { isA() .having((state) => state.ev100, 'ev100', 1.0) .having((state) => state.ev, 'ev', 0.0) - .having((state) => state.film, 'film', Film.values[1]) .having((state) => state.iso, 'iso', const IsoValue(100, StopType.full)) .having((state) => state.nd, 'nd', const NdValue(2)) .having((state) => state.isMetering, 'isMetering', false), @@ -303,7 +287,6 @@ void main() { build: () => bloc, seed: () => MeteringDataState( ev100: null, - film: Film.values[1], iso: const IsoValue(100, StopType.full), nd: NdValue.values.first, isMetering: false, @@ -318,7 +301,6 @@ void main() { isA() .having((state) => state.ev100, 'ev100', null) .having((state) => state.ev, 'ev', null) - .having((state) => state.film, 'film', Film.values[1]) .having((state) => state.iso, 'iso', const IsoValue(100, StopType.full)) .having((state) => state.nd, 'nd', const NdValue(2)) .having((state) => state.isMetering, 'isMetering', false), @@ -330,7 +312,6 @@ void main() { build: () => bloc, seed: () => MeteringDataState( ev100: 1.0, - film: Film.values[1], iso: const IsoValue(100, StopType.full), nd: NdValue.values.first, isMetering: false, @@ -349,7 +330,6 @@ void main() { build: () => bloc, seed: () => MeteringDataState( ev100: 1.0, - film: Film.values[1], iso: const IsoValue(100, StopType.full), nd: NdValue.values.first, isMetering: false, @@ -366,7 +346,6 @@ void main() { isA() .having((state) => state.ev100, 'ev100', 1.0) .having((state) => state.ev, 'ev', 0.0) - .having((state) => state.film, 'film', Film.values[1]) .having((state) => state.iso, 'iso', const IsoValue(100, StopType.full)) .having((state) => state.nd, 'nd', const NdValue(2)) .having((state) => state.isMetering, 'isMetering', false), @@ -374,7 +353,6 @@ void main() { isA() .having((state) => state.ev100, 'ev100', 2.0) .having((state) => state.ev, 'ev', 1.0) - .having((state) => state.film, 'film', Film.values[1]) .having((state) => state.iso, 'iso', const IsoValue(100, StopType.full)) .having((state) => state.nd, 'nd', const NdValue(2)) .having((state) => state.isMetering, 'isMetering', false), @@ -383,115 +361,6 @@ void main() { }, ); - group( - '`FilmChangedEvent`', - () { - blocTest( - 'Pick different film with different ISO', - build: () => bloc, - seed: () => MeteringDataState( - ev100: 1.0, - film: const FomapanFilm.creative100(), - iso: const IsoValue(100, StopType.full), - nd: NdValue.values.first, - isMetering: false, - ), - act: (bloc) async { - bloc.add(const FilmChangedEvent(FomapanFilm.creative200())); - }, - verify: (_) { - verify(() => meteringInteractor.film = const FomapanFilm.creative200()).called(1); - verify(() => meteringInteractor.iso = const IsoValue(200, StopType.full)).called(1); - }, - expect: () => [ - isA() - .having((state) => state.ev100, 'ev100', 1.0) - .having((state) => state.ev, 'ev', 2.0) - .having((state) => state.film, 'film', const FomapanFilm.creative200()) - .having((state) => state.iso, 'iso', const IsoValue(200, StopType.full)) - .having((state) => state.nd, 'nd', NdValue.values.first) - .having((state) => state.isMetering, 'isMetering', false), - ], - ); - - blocTest( - 'Pick different film with same ISO', - build: () => bloc, - seed: () => MeteringDataState( - ev100: 1.0, - film: const FomapanFilm.creative100(), - iso: const IsoValue(100, StopType.full), - nd: NdValue.values.first, - isMetering: false, - ), - act: (bloc) async { - bloc.add(const FilmChangedEvent(IlfordFilm.delta100())); - }, - verify: (_) { - verify(() => meteringInteractor.film = const IlfordFilm.delta100()).called(1); - verifyNever(() => meteringInteractor.iso = const IsoValue(100, StopType.full)); - }, - expect: () => [ - isA() - .having((state) => state.ev100, 'ev100', 1.0) - .having((state) => state.ev, 'ev', 1.0) - .having((state) => state.film, 'film', const IlfordFilm.delta100()) - .having((state) => state.iso, 'iso', const IsoValue(100, StopType.full)) - .having((state) => state.nd, 'nd', NdValue.values.first) - .having((state) => state.isMetering, 'isMetering', false), - ], - ); - - blocTest( - 'Pick same film', - build: () => bloc, - seed: () => MeteringDataState( - ev100: 1.0, - film: const FomapanFilm.creative100(), - iso: const IsoValue(100, StopType.full), - nd: NdValue.values.first, - isMetering: false, - ), - act: (bloc) async { - bloc.add(const FilmChangedEvent(FomapanFilm.creative100())); - }, - verify: (_) { - verifyNever(() => meteringInteractor.film = const FomapanFilm.creative100()); - }, - expect: () => [], - ); - - blocTest( - 'Pick `Film.other()`', - build: () => bloc, - seed: () => MeteringDataState( - ev100: 1.0, - film: const FomapanFilm.creative100(), - iso: const IsoValue(100, StopType.full), - nd: NdValue.values.first, - isMetering: false, - ), - act: (bloc) async { - bloc.add(const FilmChangedEvent(Film.other())); - }, - verify: (_) { - verify(() => meteringInteractor.film = const Film.other()).called(1); - verifyNever(() => meteringInteractor.iso = const IsoValue(0, StopType.full)); - verifyNever(() => meteringInteractor.responseVibration()); - }, - expect: () => [ - isA() - .having((state) => state.ev100, 'ev100', 1.0) - .having((state) => state.ev, 'ev', 1.0) - .having((state) => state.film, 'film', const Film.other()) - .having((state) => state.iso, 'iso', const IsoValue(100, StopType.full)) - .having((state) => state.nd, 'nd', NdValue.values.first) - .having((state) => state.isMetering, 'isMetering', false), - ], - ); - }, - ); - group( '`EquipmentProfileChangedEvent`', () { @@ -509,7 +378,6 @@ void main() { build: () => bloc, seed: () => MeteringDataState( ev100: 1.0, - film: Film.values[1], iso: const IsoValue(100, StopType.full), nd: NdValue.values.first, isMetering: false, @@ -518,7 +386,6 @@ void main() { bloc.add(EquipmentProfileChangedEvent(reducedProfile)); }, verify: (_) { - verifyNever(() => meteringInteractor.film = const Film.other()); verifyNever(() => meteringInteractor.iso = reducedProfile.isoValues.first); verifyNever(() => meteringInteractor.ndFilter = reducedProfile.ndValues.first); verifyNever(() => meteringInteractor.responseVibration()); @@ -531,7 +398,6 @@ void main() { build: () => bloc, seed: () => MeteringDataState( ev100: 1.0, - film: Film.values[1], iso: IsoValue.values[2], nd: NdValue.values.first, isMetering: false, @@ -540,7 +406,6 @@ void main() { bloc.add(EquipmentProfileChangedEvent(reducedProfile)); }, verify: (_) { - verify(() => meteringInteractor.film = const Film.other()).called(1); verify(() => meteringInteractor.iso = reducedProfile.isoValues.first).called(1); verifyNever(() => meteringInteractor.ndFilter = reducedProfile.ndValues.first); verify(() => meteringInteractor.responseVibration()).called(1); @@ -548,7 +413,6 @@ void main() { expect: () => [ isA() .having((state) => state.ev100, 'ev100', 1.0) - .having((state) => state.film, 'film', const Film.other()) .having((state) => state.iso, 'iso', reducedProfile.isoValues.first) .having((state) => state.nd, 'nd', NdValue.values.first) .having((state) => state.isMetering, 'isMetering', false), @@ -560,7 +424,6 @@ void main() { build: () => bloc, seed: () => MeteringDataState( ev100: 1.0, - film: Film.values[1], iso: const IsoValue(100, StopType.full), nd: NdValue.values[4], isMetering: false, @@ -569,7 +432,6 @@ void main() { bloc.add(EquipmentProfileChangedEvent(reducedProfile)); }, verify: (_) { - verifyNever(() => meteringInteractor.film = const Film.other()); verifyNever(() => meteringInteractor.iso = reducedProfile.isoValues.first); verify(() => meteringInteractor.ndFilter = reducedProfile.ndValues.first).called(1); verify(() => meteringInteractor.responseVibration()).called(1); @@ -577,7 +439,6 @@ void main() { expect: () => [ isA() .having((state) => state.ev100, 'ev100', 1.0) - .having((state) => state.film, 'film', Film.values[1]) .having((state) => state.iso, 'iso', const IsoValue(100, StopType.full)) .having((state) => state.nd, 'nd', reducedProfile.ndValues.first) .having((state) => state.isMetering, 'isMetering', false), @@ -589,7 +450,6 @@ void main() { build: () => bloc, seed: () => MeteringDataState( ev100: 1.0, - film: Film.values[1], iso: IsoValue.values[2], nd: NdValue.values[4], isMetering: false, @@ -598,7 +458,6 @@ void main() { bloc.add(EquipmentProfileChangedEvent(reducedProfile)); }, verify: (_) { - verify(() => meteringInteractor.film = const Film.other()).called(1); verify(() => meteringInteractor.iso = reducedProfile.isoValues.first).called(1); verify(() => meteringInteractor.ndFilter = reducedProfile.ndValues.first).called(1); verify(() => meteringInteractor.responseVibration()).called(1); @@ -606,7 +465,6 @@ void main() { expect: () => [ isA() .having((state) => state.ev100, 'ev100', 1.0) - .having((state) => state.film, 'film', const Film.other()) .having((state) => state.iso, 'iso', reducedProfile.isoValues.first) .having((state) => state.nd, 'nd', reducedProfile.ndValues.first) .having((state) => state.isMetering, 'isMetering', false), diff --git a/test/screens/metering/screen_metering_test.dart b/test/screens/metering/screen_metering_test.dart index bb50168..ffcca8d 100644 --- a/test/screens/metering/screen_metering_test.dart +++ b/test/screens/metering/screen_metering_test.dart @@ -1,6 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:lightmeter/data/models/exposure_pair.dart'; -import 'package:lightmeter/data/models/film.dart'; import 'package:lightmeter/screens/metering/screen_metering.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; @@ -19,7 +18,6 @@ void main() { ev, StopType.full, defaultEquipmentProfile, - const Film.other(), ); test('isNan', () { @@ -42,7 +40,6 @@ void main() { ev, StopType.full, defaultEquipmentProfile, - const Film.other(), ); test('EV 1', () { @@ -142,7 +139,6 @@ void main() { ev, StopType.half, defaultEquipmentProfile, - const Film.other(), ); test('EV 1', () { @@ -242,7 +238,6 @@ void main() { ev, StopType.third, defaultEquipmentProfile, - const Film.other(), ); test('EV 1', () { @@ -356,7 +351,6 @@ void main() { ev, StopType.full, equipmentProfile, - const Film.other(), ); test('EV 1', () { @@ -456,7 +450,6 @@ void main() { ev, StopType.half, equipmentProfile, - const Film.other(), ); test('EV 1', () { @@ -556,7 +549,6 @@ void main() { ev, StopType.third, equipmentProfile, - const Film.other(), ); test('EV 1', () { @@ -669,7 +661,6 @@ void main() { ev, StopType.full, equipmentProfile, - const Film.other(), ); test('EV 1', () { @@ -769,7 +760,6 @@ void main() { ev, StopType.half, equipmentProfile, - const Film.other(), ); test('EV 1', () { @@ -869,7 +859,6 @@ void main() { ev, StopType.third, equipmentProfile, - const Film.other(), ); test('EV 1', () { From 0fbf252d9e65f9344ae0bfbe4c8af50eeb64eff0 Mon Sep 17 00:00:00 2001 From: Vadim <44135514+vodemn@users.noreply.github.com> Date: Sun, 17 Sep 2023 22:29:02 +0200 Subject: [PATCH 16/23] ML-117 Improve description of paid features (#119) * wip * added `LightmeterProSettingsSection` * hide Pro section on purchase * `ElevatedButton` -> `FilledButton` * moved Pro description to iap/README.md * intl * disable only list tiles * show iap dialog on every iap list tile --- iap/README.md | 17 +++++++++ lib/l10n/intl_en.arb | 8 +++-- lib/l10n/intl_fr.arb | 8 +++-- lib/l10n/intl_ru.arb | 8 +++-- lib/l10n/intl_zh.arb | 8 +++-- .../buy_pro/widget_list_tile_buy_pro.dart | 20 +++++++++++ ...idget_settings_section_lightmeter_pro.dart | 16 +++++++++ .../iap_list_tile/widget_list_tile_iap.dart | 17 +++++++-- .../widget_settings_section.dart | 35 ++++++++----------- .../components/utils/show_buy_pro_dialog.dart | 31 ++++++++++++++++ lib/screens/settings/screen_settings.dart | 4 +++ 11 files changed, 142 insertions(+), 30 deletions(-) create mode 100644 iap/README.md create mode 100644 lib/screens/settings/components/lightmeter_pro/components/buy_pro/widget_list_tile_buy_pro.dart create mode 100644 lib/screens/settings/components/lightmeter_pro/widget_settings_section_lightmeter_pro.dart create mode 100644 lib/screens/settings/components/utils/show_buy_pro_dialog.dart diff --git a/iap/README.md b/iap/README.md new file mode 100644 index 0000000..d978bb0 --- /dev/null +++ b/iap/README.md @@ -0,0 +1,17 @@ +# Lightmeter Pro + +### Equipment profiles + +Each equipment profile allows you to select: + +- Aperture values and shutter speeds, that your lens and camera have +- ND filters, that fit the chosen lens +- ISO values, that your camera supports + +Creating multiple profiles for different cameras and lenses allows you to easily switch between them and always have the relevant readings. + +### Films in use + +Select the films that you usually use. Selecting one will apply a correction to shutter speeds greater than 1" to compensate for the reciprocity failure. + +Each equipment profile allows you to select:\n- Aperture values and shutter speeds, that your lens and camera have\n- ND filters, that fit the chosen lens\n- ISO values, that your camera supports\nCreating multiple profiles for different cameras and lenses allows you to easily switch between them and always have the relevant readings! \ No newline at end of file diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 364deec..4e12ad8 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -92,5 +92,9 @@ "type": "String" } } - } -} + }, + "buyLightmeterPro": "Buy Lightmeter Pro", + "lightmeterPro": "Lightmeter Pro", + "lightmeterProDescription": "Unlocks extra features, such as equipment profiles containing filters for aperture, shutter speed, and more; and a list of films with compensation for what's known as reciprocity failure.\n\nThe source code of Lightmeter is available on GitHub. You are welcome to compile it yourself. However, if you want to support the development and receive new features and updates, consider purchasing Lightmeter Pro.", + "buy": "Buy" +} \ No newline at end of file diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index 22b0356..5946d5b 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -92,5 +92,9 @@ "type": "String" } } - } -} + }, + "buyLightmeterPro": "Acheter Lightmeter Pro", + "lightmeterPro": "Lightmeter Pro", + "lightmeterProDescription": "Déverrouille des fonctionnalités supplémentaires, telles que des profils d'équipement contenant des filtres pour l'ouverture, la vitesse d'obturation et plus encore, ainsi qu'une liste de films avec une compensation pour ce que l'on appelle l'échec de réciprocité.\n\nLe code source du Lightmeter est disponible sur GitHub. Vous pouvez le compiler vous-même. Cependant, si vous souhaitez soutenir le développement et recevoir de nouvelles fonctionnalités et mises à jour, envisagez d'acheter Lightmeter Pro.", + "buy": "Acheter" +} \ No newline at end of file diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index 9ce0d9b..9f428c4 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -92,5 +92,9 @@ "type": "String" } } - } -} + }, + "buyLightmeterPro": "Купить Lightmeter Pro", + "lightmeterPro": "Lightmeter Pro", + "lightmeterProDescription": "Даёт доступ к таким функциям как профили оборудования, содержащие фильтры для диафрагмы, выдержки и других значений, а также набору пленок с компенсацией эффекта Шварцшильда.\n\nИсходный код Lightmeter доступен на GitHub. Вы можете собрать его самостоятельно. Однако если вы хотите поддержать разработку и получать новые функции и обновления, то приобретите Lightmeter Pro.", + "buy": "Купить" +} \ No newline at end of file diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb index 357ef95..92f19ee 100644 --- a/lib/l10n/intl_zh.arb +++ b/lib/l10n/intl_zh.arb @@ -92,5 +92,9 @@ "type": "String" } } - } -} + }, + "buyLightmeterPro": "Buy Lightmeter Pro", + "lightmeterPro": "Lightmeter Pro", + "lightmeterProDescription": "Unlocks extra features, such as equipment profiles containing filters for aperture, shutter speed, and more; and a list of films with compensation for what's known as reciprocity failure.\n\nThe source code of Lightmeter is available on GitHub. You are welcome to compile it yourself. However, if you want to support the development and receive new features and updates, consider purchasing Lightmeter Pro.", + "buy": "Buy" +} \ No newline at end of file diff --git a/lib/screens/settings/components/lightmeter_pro/components/buy_pro/widget_list_tile_buy_pro.dart b/lib/screens/settings/components/lightmeter_pro/components/buy_pro/widget_list_tile_buy_pro.dart new file mode 100644 index 0000000..11e8a4f --- /dev/null +++ b/lib/screens/settings/components/lightmeter_pro/components/buy_pro/widget_list_tile_buy_pro.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/screens/settings/components/shared/iap_list_tile/widget_list_tile_iap.dart'; +import 'package:lightmeter/screens/settings/components/utils/show_buy_pro_dialog.dart'; + +class BuyProListTile extends StatelessWidget { + const BuyProListTile({super.key}); + + @override + Widget build(BuildContext context) { + return IAPListTile( + leading: const Icon(Icons.star), + title: Text(S.of(context).buyLightmeterPro), + onTap: () { + showBuyProDialog(context); + }, + showPendingTrailing: true, + ); + } +} diff --git a/lib/screens/settings/components/lightmeter_pro/widget_settings_section_lightmeter_pro.dart b/lib/screens/settings/components/lightmeter_pro/widget_settings_section_lightmeter_pro.dart new file mode 100644 index 0000000..c060dc1 --- /dev/null +++ b/lib/screens/settings/components/lightmeter_pro/widget_settings_section_lightmeter_pro.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/screens/settings/components/lightmeter_pro/components/buy_pro/widget_list_tile_buy_pro.dart'; +import 'package:lightmeter/screens/settings/components/shared/settings_section/widget_settings_section.dart'; + +class LightmeterProSettingsSection extends StatelessWidget { + const LightmeterProSettingsSection({super.key}); + + @override + Widget build(BuildContext context) { + return SettingsSection( + title: S.of(context).lightmeterPro, + children: const [BuyProListTile()], + ); + } +} diff --git a/lib/screens/settings/components/shared/iap_list_tile/widget_list_tile_iap.dart b/lib/screens/settings/components/shared/iap_list_tile/widget_list_tile_iap.dart index 8c79f37..cf65ade 100644 --- a/lib/screens/settings/components/shared/iap_list_tile/widget_list_tile_iap.dart +++ b/lib/screens/settings/components/shared/iap_list_tile/widget_list_tile_iap.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:lightmeter/res/dimens.dart'; +import 'package:lightmeter/screens/settings/components/utils/show_buy_pro_dialog.dart'; import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; /// Depends on the product status and replaces [onTap] with purchase callback @@ -8,26 +10,37 @@ class IAPListTile extends StatelessWidget { final Icon leading; final Text title; final VoidCallback onTap; + final bool showPendingTrailing; const IAPListTile({ this.product = IAPProductType.paidFeatures, required this.leading, required this.title, required this.onTap, + this.showPendingTrailing = false, super.key, }); @override Widget build(BuildContext context) { + final status = IAPProducts.productOf(context, IAPProductType.paidFeatures)?.status; + final isPending = status == IAPProductStatus.purchased || status == null; return ListTile( leading: leading, title: title, - onTap: switch (IAPProducts.productOf(context, product)?.status) { - IAPProductStatus.purchasable => () => IAPProductsProvider.of(context).buy(product), + onTap: switch (status) { + IAPProductStatus.purchasable => () => showBuyProDialog(context), IAPProductStatus.pending => null, IAPProductStatus.purchased => onTap, null => null, }, + trailing: showPendingTrailing && isPending + ? const SizedBox( + height: Dimens.grid24, + width: Dimens.grid24, + child: CircularProgressIndicator(), + ) + : null, ); } } diff --git a/lib/screens/settings/components/shared/settings_section/widget_settings_section.dart b/lib/screens/settings/components/shared/settings_section/widget_settings_section.dart index 9f88d7d..be62847 100644 --- a/lib/screens/settings/components/shared/settings_section/widget_settings_section.dart +++ b/lib/screens/settings/components/shared/settings_section/widget_settings_section.dart @@ -4,12 +4,10 @@ import 'package:lightmeter/res/dimens.dart'; class SettingsSection extends StatelessWidget { final String title; final List children; - final bool enabled; const SettingsSection({ required this.title, required this.children, - this.enabled = true, super.key, }); @@ -25,25 +23,22 @@ class SettingsSection extends StatelessWidget { child: Card( child: Padding( padding: const EdgeInsets.symmetric(vertical: Dimens.paddingM), - child: Opacity( - opacity: enabled ? Dimens.enabledOpacity : Dimens.disabledOpacity, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM), - child: Text( - title, - style: Theme.of(context) - .textTheme - .labelLarge - ?.copyWith(color: Theme.of(context).colorScheme.onSurface), - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM), + child: Text( + title, + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith(color: Theme.of(context).colorScheme.onSurface), ), - ...children, - ], - ), + ), + ...children, + ], ), ), ), diff --git a/lib/screens/settings/components/utils/show_buy_pro_dialog.dart b/lib/screens/settings/components/utils/show_buy_pro_dialog.dart new file mode 100644 index 0000000..0333570 --- /dev/null +++ b/lib/screens/settings/components/utils/show_buy_pro_dialog.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/res/dimens.dart'; +import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; + +Future showBuyProDialog(BuildContext context) { + return showDialog( + context: context, + builder: (_) => AlertDialog( + icon: const Icon(Icons.star), + titlePadding: Dimens.dialogIconTitlePadding, + title: Text(S.of(context).lightmeterPro), + contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingL), + content: SingleChildScrollView(child: Text(S.of(context).lightmeterProDescription)), + actionsPadding: Dimens.dialogActionsPadding, + actions: [ + TextButton( + onPressed: Navigator.of(context).pop, + child: Text(S.of(context).cancel), + ), + FilledButton( + onPressed: () { + Navigator.of(context).pop(); + IAPProductsProvider.of(context).buy(IAPProductType.paidFeatures); + }, + child: Text(S.of(context).buy), + ), + ], + ), + ); +} diff --git a/lib/screens/settings/screen_settings.dart b/lib/screens/settings/screen_settings.dart index 7d057cf..3f1c0a6 100644 --- a/lib/screens/settings/screen_settings.dart +++ b/lib/screens/settings/screen_settings.dart @@ -3,10 +3,12 @@ import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/screens/settings/components/about/widget_settings_section_about.dart'; import 'package:lightmeter/screens/settings/components/equipment/widget_settings_section_equipment.dart'; import 'package:lightmeter/screens/settings/components/general/widget_settings_section_general.dart'; +import 'package:lightmeter/screens/settings/components/lightmeter_pro/widget_settings_section_lightmeter_pro.dart'; import 'package:lightmeter/screens/settings/components/metering/widget_settings_section_metering.dart'; import 'package:lightmeter/screens/settings/components/theme/widget_settings_section_theme.dart'; import 'package:lightmeter/screens/settings/flow_settings.dart'; import 'package:lightmeter/screens/shared/sliver_screen/screen_sliver.dart'; +import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; class SettingsScreen extends StatefulWidget { const SettingsScreen({super.key}); @@ -43,6 +45,8 @@ class _SettingsScreenState extends State { SliverList( delegate: SliverChildListDelegate( [ + if (!IAPProducts.isPurchased(context, IAPProductType.paidFeatures)) + const LightmeterProSettingsSection(), const MeteringSettingsSection(), const EquipmentSettingsSection(), const GeneralSettingsSection(), From abbc4c92de97f4eede0dc20c436b5725bebdd416 Mon Sep 17 00:00:00 2001 From: Vadim <44135514+vodemn@users.noreply.github.com> Date: Mon, 18 Sep 2023 16:59:53 +0200 Subject: [PATCH 17/23] ML-113 Add ability to copy equipment profile (#120) * Display range values instead of values count * copy equipment profile * added `IconButton` tooltips --- .../providers/equipment_profile_provider.dart | 2 +- lib/l10n/intl_en.arb | 14 +++- lib/l10n/intl_fr.arb | 14 +++- lib/l10n/intl_ru.arb | 14 +++- lib/l10n/intl_zh.arb | 14 +++- .../widget_bottom_controls.dart | 6 ++ .../widget_slider_exposure_offset.dart | 2 + .../widget_list_tiles_equipments.dart | 22 ++---- .../widget_container_equipment_profile.dart | 70 +++++++++++++------ .../screen_equipment_profile.dart | 14 ++-- .../widget_dialog_calibration.dart | 1 + .../dialog_filter/widget_dialog_filter.dart | 3 + lib/screens/settings/screen_settings.dart | 6 -- .../shared/sliver_screen/screen_sliver.dart | 13 +++- 14 files changed, 136 insertions(+), 59 deletions(-) diff --git a/iap/lib/src/providers/equipment_profile_provider.dart b/iap/lib/src/providers/equipment_profile_provider.dart index 92ba8a2..0a037a9 100644 --- a/iap/lib/src/providers/equipment_profile_provider.dart +++ b/iap/lib/src/providers/equipment_profile_provider.dart @@ -36,7 +36,7 @@ class EquipmentProfileProviderState extends State { void setProfile(EquipmentProfile data) {} - void addProfile(String name) {} + void addProfile(String name, [EquipmentProfile? copyFrom]) {} void updateProdile(EquipmentProfile data) {} diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 4e12ad8..259fd0c 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -96,5 +96,17 @@ "buyLightmeterPro": "Buy Lightmeter Pro", "lightmeterPro": "Lightmeter Pro", "lightmeterProDescription": "Unlocks extra features, such as equipment profiles containing filters for aperture, shutter speed, and more; and a list of films with compensation for what's known as reciprocity failure.\n\nThe source code of Lightmeter is available on GitHub. You are welcome to compile it yourself. However, if you want to support the development and receive new features and updates, consider purchasing Lightmeter Pro.", - "buy": "Buy" + "buy": "Buy", + "tooltipAdd": "Add", + "tooltipClose": "Close", + "tooltipExpand": "Expand", + "tooltipCollapse": "Collapse", + "tooltipCopy": "Copy", + "tooltipDelete": "Delete", + "tooltipSelectAll": "Select all", + "tooltipDesecelectAll": "Deselect all", + "tooltipResetToZero": "Reset to zero", + "tooltipUseLightSensor": "Use lightsensor", + "tooltipUseCamera": "Use camera", + "tooltipOpenSettings": "Open settings" } \ No newline at end of file diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index 5946d5b..2d6d889 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -96,5 +96,17 @@ "buyLightmeterPro": "Acheter Lightmeter Pro", "lightmeterPro": "Lightmeter Pro", "lightmeterProDescription": "Déverrouille des fonctionnalités supplémentaires, telles que des profils d'équipement contenant des filtres pour l'ouverture, la vitesse d'obturation et plus encore, ainsi qu'une liste de films avec une compensation pour ce que l'on appelle l'échec de réciprocité.\n\nLe code source du Lightmeter est disponible sur GitHub. Vous pouvez le compiler vous-même. Cependant, si vous souhaitez soutenir le développement et recevoir de nouvelles fonctionnalités et mises à jour, envisagez d'acheter Lightmeter Pro.", - "buy": "Acheter" + "buy": "Acheter", + "tooltipAdd": "Ajouter", + "tooltipClose": "Fermer", + "tooltipExpand": "Élargir", + "tooltipCollapse": "Effondrement", + "tooltipCopy": "Copie", + "tooltipDelete": "Supprimer", + "tooltipSelectAll": "Tout sélectionner", + "tooltipDesecelectAll": "Désélectionner tout", + "tooltipResetToZero": "Remise à zéro", + "tooltipUseLightSensor": "Utiliser un capteur de lumière", + "tooltipUseCamera": "Utiliser la caméra", + "tooltipOpenSettings": "Ouvrir les paramètres" } \ No newline at end of file diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index 9f428c4..72c42f1 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -96,5 +96,17 @@ "buyLightmeterPro": "Купить Lightmeter Pro", "lightmeterPro": "Lightmeter Pro", "lightmeterProDescription": "Даёт доступ к таким функциям как профили оборудования, содержащие фильтры для диафрагмы, выдержки и других значений, а также набору пленок с компенсацией эффекта Шварцшильда.\n\nИсходный код Lightmeter доступен на GitHub. Вы можете собрать его самостоятельно. Однако если вы хотите поддержать разработку и получать новые функции и обновления, то приобретите Lightmeter Pro.", - "buy": "Купить" + "buy": "Купить", + "tooltipAdd": "Добавить", + "tooltipClose": "Закрыть", + "tooltipExpand": "Развернуть", + "tooltipCollapse": "Свернуть", + "tooltipCopy": "Скопировать", + "tooltipDelete": "Удалить", + "tooltipSelectAll": "Выбрать все", + "tooltipDesecelectAll": "Отменить все", + "tooltipResetToZero": "Сбросить до 0", + "tooltipUseLightSensor": "Использовать датчик освещенности", + "tooltipUseCamera": "Использовать камеру", + "tooltipOpenSettings": "Открыть настройки" } \ No newline at end of file diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb index 92f19ee..391e268 100644 --- a/lib/l10n/intl_zh.arb +++ b/lib/l10n/intl_zh.arb @@ -96,5 +96,17 @@ "buyLightmeterPro": "Buy Lightmeter Pro", "lightmeterPro": "Lightmeter Pro", "lightmeterProDescription": "Unlocks extra features, such as equipment profiles containing filters for aperture, shutter speed, and more; and a list of films with compensation for what's known as reciprocity failure.\n\nThe source code of Lightmeter is available on GitHub. You are welcome to compile it yourself. However, if you want to support the development and receive new features and updates, consider purchasing Lightmeter Pro.", - "buy": "Buy" + "buy": "Buy", + "tooltipAdd": "Add", + "tooltipClose": "Close", + "tooltipExpand": "Expand", + "tooltipCollapse": "Collapse", + "tooltipCopy": "Copy", + "tooltipDelete": "Delete", + "tooltipSelectAll": "Select all", + "tooltipDesecelectAll": "Deselect all", + "resetToZero": "Reset to zero", + "tooltipUseLightSensor": "Use lightsensor", + "tooltipUseCamera": "Use camera", + "tooltipOpenSettings": "Open settings" } \ No newline at end of file diff --git a/lib/screens/metering/components/bottom_controls/widget_bottom_controls.dart b/lib/screens/metering/components/bottom_controls/widget_bottom_controls.dart index 54ea810..31ecac4 100644 --- a/lib/screens/metering/components/bottom_controls/widget_bottom_controls.dart +++ b/lib/screens/metering/components/bottom_controls/widget_bottom_controls.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/data/models/ev_source_type.dart'; +import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/screens/metering/components/bottom_controls/components/measure_button/widget_button_measure.dart'; @@ -46,6 +47,10 @@ class MeteringBottomControls extends StatelessWidget { ? Icons.camera_rear : Icons.wb_incandescent, ), + tooltip: + UserPreferencesProvider.evSourceTypeOf(context) != EvSourceType.camera + ? S.of(context).tooltipUseCamera + : S.of(context).tooltipUseLightSensor, ), ), ) @@ -61,6 +66,7 @@ class MeteringBottomControls extends StatelessWidget { child: IconButton( onPressed: onSettings, icon: const Icon(Icons.settings), + tooltip: S.of(context).tooltipOpenSettings, ), ), ), diff --git a/lib/screens/metering/components/camera_container/components/camera_controls/components/exposure_offset_slider/widget_slider_exposure_offset.dart b/lib/screens/metering/components/camera_container/components/camera_controls/components/exposure_offset_slider/widget_slider_exposure_offset.dart index 87539ee..484ee4b 100644 --- a/lib/screens/metering/components/camera_container/components/camera_controls/components/exposure_offset_slider/widget_slider_exposure_offset.dart +++ b/lib/screens/metering/components/camera_container/components/camera_controls/components/exposure_offset_slider/widget_slider_exposure_offset.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/screens/shared/centered_slider/widget_slider_centered.dart'; import 'package:lightmeter/utils/to_string_signed.dart'; @@ -22,6 +23,7 @@ class ExposureOffsetSlider extends StatelessWidget { IconButton( icon: const Icon(Icons.sync), onPressed: value != 0.0 ? () => onChanged(0.0) : null, + tooltip: S.of(context).tooltipResetToZero, ), Expanded( child: Row( diff --git a/lib/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/widget_list_tiles_equipments.dart b/lib/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/widget_list_tiles_equipments.dart index 482c370..bb3651d 100644 --- a/lib/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/widget_list_tiles_equipments.dart +++ b/lib/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/widget_list_tiles_equipments.dart @@ -36,9 +36,6 @@ class EquipmentListTiles extends StatelessWidget { title: S.of(context).isoValues, description: S.of(context).isoValuesFilterDescription, values: IsoValue.values, - valuesCount: selectedIsoValues.length == IsoValue.values.length - ? S.of(context).equipmentProfileAllValues - : selectedIsoValues.length.toString(), selectedValues: selectedIsoValues, rangeSelect: false, onChanged: onIsoValuesSelecred, @@ -48,9 +45,6 @@ class EquipmentListTiles extends StatelessWidget { title: S.of(context).ndFilters, description: S.of(context).ndFiltersFilterDescription, values: NdValue.values, - valuesCount: selectedNdValues.length == NdValue.values.length - ? S.of(context).equipmentProfileAllValues - : selectedNdValues.length.toString(), selectedValues: selectedNdValues, rangeSelect: false, onChanged: onNdValuesSelected, @@ -60,9 +54,6 @@ class EquipmentListTiles extends StatelessWidget { title: S.of(context).apertureValues, description: S.of(context).apertureValuesFilterDescription, values: ApertureValue.values, - valuesCount: selectedApertureValues.length == ApertureValue.values.length - ? S.of(context).equipmentProfileAllValues - : selectedApertureValues.length.toString(), selectedValues: selectedApertureValues, rangeSelect: true, onChanged: onApertureValuesSelected, @@ -72,9 +63,6 @@ class EquipmentListTiles extends StatelessWidget { title: S.of(context).shutterSpeedValues, description: S.of(context).shutterSpeedValuesFilterDescription, values: ShutterSpeedValue.values, - valuesCount: selectedShutterSpeedValues.length == ShutterSpeedValue.values.length - ? S.of(context).equipmentProfileAllValues - : selectedShutterSpeedValues.length.toString(), selectedValues: selectedShutterSpeedValues, rangeSelect: true, onChanged: onShutterSpeedValuesSelected, @@ -87,7 +75,6 @@ class EquipmentListTiles extends StatelessWidget { class _EquipmentListTile extends StatelessWidget { final IconData icon; final String title; - final String valuesCount; final String description; final List selectedValues; final List values; @@ -97,7 +84,6 @@ class _EquipmentListTile extends StatelessWidget { const _EquipmentListTile({ required this.icon, required this.title, - required this.valuesCount, required this.description, required this.selectedValues, required this.values, @@ -111,7 +97,13 @@ class _EquipmentListTile extends StatelessWidget { return ListTile( leading: Icon(icon), title: Text(title), - trailing: Text(valuesCount), + trailing: rangeSelect + ? Text("${selectedValues.first} - ${selectedValues.last}") + : Text( + values.length == selectedValues.length + ? S.of(context).equipmentProfileAllValues + : selectedValues.length.toString(), + ), onTap: () { showDialog>( context: context, diff --git a/lib/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart b/lib/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart index 244ddce..bca15d8 100644 --- a/lib/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart +++ b/lib/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; +import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/screens/settings/components/equipment/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/equipment/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_name_dialog/widget_dialog_equipment_profile_name.dart'; @@ -10,12 +11,14 @@ import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class EquipmentProfileContainer extends StatefulWidget { final EquipmentProfile data; final ValueChanged onUpdate; + final VoidCallback onCopy; final VoidCallback onDelete; final VoidCallback onExpand; const EquipmentProfileContainer({ required this.data, required this.onUpdate, + required this.onCopy, required this.onDelete, required this.onExpand, super.key, @@ -85,19 +88,9 @@ class EquipmentProfileContainerState extends State ), ], ), - trailing: Row( - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - _AnimatedArrowButton( - controller: _controller, - onPressed: () => _expanded ? collapse() : expand(), - ), - IconButton( - onPressed: widget.onDelete, - icon: const Icon(Icons.delete), - ), - ], + trailing: _AnimatedArrowButton( + controller: _controller, + onPressed: () => _expanded ? collapse() : expand(), ), onTap: () => _expanded ? _showNameDialog() : expand(), ), @@ -120,6 +113,8 @@ class EquipmentProfileContainerState extends State _equipmentData = _equipmentData.copyWith(shutterSpeedValues: value); widget.onUpdate(_equipmentData); }, + onCopy: widget.onCopy, + onDelete: widget.onDelete, ), ], ), @@ -194,6 +189,7 @@ class _AnimatedArrowButton extends AnimatedWidget { angle: _progress.value * pi, child: const Icon(Icons.keyboard_arrow_down), ), + tooltip: _progress.value == 0 ? S.of(context).tooltipExpand : S.of(context).tooltipCollapse, ); } } @@ -204,6 +200,8 @@ class _AnimatedEquipmentListTiles extends AnimatedWidget { final ValueChanged> onIsoValuesSelecred; final ValueChanged> onNdValuesSelected; final ValueChanged> onShutterSpeedValuesSelected; + final VoidCallback onCopy; + final VoidCallback onDelete; const _AnimatedEquipmentListTiles({ required AnimationController controller, @@ -212,6 +210,8 @@ class _AnimatedEquipmentListTiles extends AnimatedWidget { required this.onIsoValuesSelecred, required this.onNdValuesSelected, required this.onShutterSpeedValuesSelected, + required this.onCopy, + required this.onDelete, }) : super(listenable: controller); Animation get _progress => listenable as Animation; @@ -222,19 +222,43 @@ class _AnimatedEquipmentListTiles extends AnimatedWidget { alignment: Alignment.topCenter, size: Size( double.maxFinite, - _progress.value * Dimens.grid56 * 4, + _progress.value * Dimens.grid56 * 5, ), + // https://github.com/gskinnerTeam/flutter-folio/pull/62 child: Opacity( opacity: _progress.value, - child: EquipmentListTiles( - selectedApertureValues: equipmentData.apertureValues, - selectedIsoValues: equipmentData.isoValues, - selectedNdValues: equipmentData.ndValues, - selectedShutterSpeedValues: equipmentData.shutterSpeedValues, - onApertureValuesSelected: onApertureValuesSelected, - onIsoValuesSelecred: onIsoValuesSelecred, - onNdValuesSelected: onNdValuesSelected, - onShutterSpeedValuesSelected: onShutterSpeedValuesSelected, + child: Column( + children: [ + EquipmentListTiles( + selectedApertureValues: equipmentData.apertureValues, + selectedIsoValues: equipmentData.isoValues, + selectedNdValues: equipmentData.ndValues, + selectedShutterSpeedValues: equipmentData.shutterSpeedValues, + onApertureValuesSelected: onApertureValuesSelected, + onIsoValuesSelecred: onIsoValuesSelecred, + onNdValuesSelected: onNdValuesSelected, + onShutterSpeedValuesSelected: onShutterSpeedValuesSelected, + ), + ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM), + trailing: Row( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: onCopy, + icon: const Icon(Icons.copy), + tooltip: S.of(context).tooltipCopy, + ), + IconButton( + onPressed: onDelete, + icon: const Icon(Icons.delete), + tooltip: S.of(context).tooltipDelete, + ), + ], + ), + ), + ], ), ), ); diff --git a/lib/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart b/lib/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart index a5b0eca..abacf7c 100644 --- a/lib/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart +++ b/lib/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart @@ -34,10 +34,7 @@ class _EquipmentProfilesScreenState extends State { IconButton( onPressed: _addProfile, icon: const Icon(Icons.add), - ), - IconButton( - onPressed: Navigator.of(context).pop, - icon: const Icon(Icons.close), + tooltip: S.of(context).tooltipAdd, ), ], slivers: profilesCount == 1 @@ -69,6 +66,7 @@ class _EquipmentProfilesScreenState extends State { data: profile, onExpand: () => _keepExpandedAt(index), onUpdate: _updateProfileAt, + onCopy: () => _addProfile(profile), onDelete: () => _removeProfileAt(profile), ), ); @@ -81,13 +79,13 @@ class _EquipmentProfilesScreenState extends State { ); } - void _addProfile() { + void _addProfile([EquipmentProfile? copyFrom]) { showDialog( context: context, builder: (_) => const EquipmentProfileNameDialog(), - ).then((value) { - if (value != null) { - EquipmentProfileProvider.of(context).addProfile(value); + ).then((name) { + if (name != null) { + EquipmentProfileProvider.of(context).addProfile(name, copyFrom); } }); } diff --git a/lib/screens/settings/components/metering/components/calibration/components/calibration_dialog/widget_dialog_calibration.dart b/lib/screens/settings/components/metering/components/calibration/components/calibration_dialog/widget_dialog_calibration.dart index 69a4f00..56c4ed0 100644 --- a/lib/screens/settings/components/metering/components/calibration/components/calibration_dialog/widget_dialog_calibration.dart +++ b/lib/screens/settings/components/metering/components/calibration/components/calibration_dialog/widget_dialog_calibration.dart @@ -117,6 +117,7 @@ class _CalibrationUnit extends StatelessWidget { IconButton( onPressed: onReset, icon: const Icon(Icons.sync), + tooltip: S.of(context).tooltipResetToZero, ), ], ) diff --git a/lib/screens/settings/components/shared/dialog_filter/widget_dialog_filter.dart b/lib/screens/settings/components/shared/dialog_filter/widget_dialog_filter.dart index 8bafcae..af2b35d 100644 --- a/lib/screens/settings/components/shared/dialog_filter/widget_dialog_filter.dart +++ b/lib/screens/settings/components/shared/dialog_filter/widget_dialog_filter.dart @@ -85,6 +85,9 @@ class _DialogFilterState extends State> { padding: EdgeInsets.zero, icon: Icon(_hasAnyUnselected ? Icons.select_all : Icons.deselect), onPressed: _toggleAll, + tooltip: _hasAnyUnselected + ? S.of(context).tooltipSelectAll + : S.of(context).tooltipDesecelectAll, ), ), const Spacer(), diff --git a/lib/screens/settings/screen_settings.dart b/lib/screens/settings/screen_settings.dart index 3f1c0a6..4102282 100644 --- a/lib/screens/settings/screen_settings.dart +++ b/lib/screens/settings/screen_settings.dart @@ -35,12 +35,6 @@ class _SettingsScreenState extends State { return ScaffoldMessenger( child: SliverScreen( title: S.of(context).settings, - appBarActions: [ - IconButton( - onPressed: Navigator.of(context).pop, - icon: const Icon(Icons.close), - ), - ], slivers: [ SliverList( delegate: SliverChildListDelegate( diff --git a/lib/screens/shared/sliver_screen/screen_sliver.dart b/lib/screens/shared/sliver_screen/screen_sliver.dart index 20d2e66..97ed830 100644 --- a/lib/screens/shared/sliver_screen/screen_sliver.dart +++ b/lib/screens/shared/sliver_screen/screen_sliver.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/res/dimens.dart'; class SliverScreen extends StatelessWidget { @@ -8,7 +9,7 @@ class SliverScreen extends StatelessWidget { const SliverScreen({ required this.title, - required this.appBarActions, + this.appBarActions = const [], required this.slivers, super.key, }); @@ -36,7 +37,15 @@ class SliverScreen extends StatelessWidget { ), ), ), - actions: appBarActions, + actions: [ + ...appBarActions, + if (Navigator.of(context).canPop()) + IconButton( + onPressed: Navigator.of(context).pop, + icon: const Icon(Icons.close), + tooltip: S.of(context).tooltipClose, + ), + ], ), ...slivers, ], From 90293796975e8a595b20ab4057a905437076f91d Mon Sep 17 00:00:00 2001 From: Vadim <44135514+vodemn@users.noreply.github.com> Date: Tue, 19 Sep 2023 17:25:25 +0200 Subject: [PATCH 18/23] Merged Equipment section into Metering section --- lib/l10n/intl_en.arb | 1 - lib/l10n/intl_fr.arb | 1 - lib/l10n/intl_ru.arb | 1 - lib/l10n/intl_zh.arb | 1 - .../widget_settings_section_equipment.dart | 20 ------------------- .../widget_list_tiles_equipments.dart | 0 .../widget_container_equipment_profile.dart | 4 ++-- .../widget_dialog_equipment_profile_name.dart | 0 .../screen_equipment_profile.dart | 4 ++-- .../widget_list_tile_equipment_profiles.dart | 2 +- .../films/widget_list_tile_films.dart | 0 .../widget_settings_section_metering.dart | 4 ++++ lib/screens/settings/screen_settings.dart | 2 -- 13 files changed, 9 insertions(+), 31 deletions(-) delete mode 100644 lib/screens/settings/components/equipment/widget_settings_section_equipment.dart rename lib/screens/settings/components/{equipment => metering}/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/widget_list_tiles_equipments.dart (100%) rename lib/screens/settings/components/{equipment => metering}/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart (94%) rename lib/screens/settings/components/{equipment => metering}/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_name_dialog/widget_dialog_equipment_profile_name.dart (100%) rename lib/screens/settings/components/{equipment => metering}/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart (92%) rename lib/screens/settings/components/{equipment => metering}/components/equipment_profiles/widget_list_tile_equipment_profiles.dart (81%) rename lib/screens/settings/components/{equipment => metering}/components/films/widget_list_tile_films.dart (100%) diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 259fd0c..2f2fa27 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -44,7 +44,6 @@ "filmPush": "Film (push)", "filmPull": "Film (pull)", "filmReciprocityHint": "Applies correction for shutter speeds grater than 1 second", - "equipment": "Equipment", "equipmentProfileName": "Equipment profile name", "equipmentProfileNameHint": "Praktica MTL5B", "equipmentProfileAllValues": "All", diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index 2d6d889..895b8e2 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -44,7 +44,6 @@ "filmPush": "Pellicule (push)", "filmPull": "Pellicule (pull)", "filmReciprocityHint": "La correction s'applique aux vitesses d'obturation supérieures à 1 seconde", - "equipment": "Équipement", "equipmentProfileName": "Nom du profil de l'équipement", "equipmentProfileNameHint": "Praktica MTL5B", "equipmentProfileAllValues": "Tout", diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index 72c42f1..f4c7b0b 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -44,7 +44,6 @@ "filmPush": "Пленка (push)", "filmPull": "Пленка (pull)", "filmReciprocityHint": "Применяет коррекцию для выдержек длиннее 1 секунды", - "equipment": "Оборудование", "equipmentProfileName": "Название профиля", "equipmentProfileNameHint": "Praktica MTL5B", "equipmentProfileAllValues": "Все", diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb index 391e268..3aab33e 100644 --- a/lib/l10n/intl_zh.arb +++ b/lib/l10n/intl_zh.arb @@ -44,7 +44,6 @@ "filmPush": "胶片 (push)", "filmPull": "胶片 (pull)", "filmReciprocityHint": "Applies correction for shutter speeds grater than 1 second", - "equipment": "设备", "equipmentProfileName": "设备配置名称", "equipmentProfileNameHint": "Praktica MTL5B", "equipmentProfileAllValues": "全部", diff --git a/lib/screens/settings/components/equipment/widget_settings_section_equipment.dart b/lib/screens/settings/components/equipment/widget_settings_section_equipment.dart deleted file mode 100644 index ba3b975..0000000 --- a/lib/screens/settings/components/equipment/widget_settings_section_equipment.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/screens/settings/components/equipment/components/equipment_profiles/widget_list_tile_equipment_profiles.dart'; -import 'package:lightmeter/screens/settings/components/equipment/components/films/widget_list_tile_films.dart'; -import 'package:lightmeter/screens/settings/components/shared/settings_section/widget_settings_section.dart'; - -class EquipmentSettingsSection extends StatelessWidget { - const EquipmentSettingsSection({super.key}); - - @override - Widget build(BuildContext context) { - return SettingsSection( - title: S.of(context).equipment, - children: const [ - EquipmentProfilesListTile(), - FilmsListTile(), - ], - ); - } -} diff --git a/lib/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/widget_list_tiles_equipments.dart b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/widget_list_tiles_equipments.dart similarity index 100% rename from lib/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/widget_list_tiles_equipments.dart rename to lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/widget_list_tiles_equipments.dart diff --git a/lib/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart similarity index 94% rename from lib/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart rename to lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart index bca15d8..48a9c80 100644 --- a/lib/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart +++ b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart @@ -4,8 +4,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/res/dimens.dart'; -import 'package:lightmeter/screens/settings/components/equipment/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/equipment/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_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_name_dialog/widget_dialog_equipment_profile_name.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class EquipmentProfileContainer extends StatefulWidget { diff --git a/lib/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_name_dialog/widget_dialog_equipment_profile_name.dart b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_name_dialog/widget_dialog_equipment_profile_name.dart similarity index 100% rename from lib/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_name_dialog/widget_dialog_equipment_profile_name.dart rename to lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_name_dialog/widget_dialog_equipment_profile_name.dart diff --git a/lib/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart similarity index 92% rename from lib/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart rename to lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart index abacf7c..3c72918 100644 --- a/lib/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart +++ b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/res/dimens.dart'; -import 'package:lightmeter/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart'; -import 'package:lightmeter/screens/settings/components/equipment/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_container/widget_container_equipment_profile.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/shared/icon_placeholder/widget_icon_placeholder.dart'; import 'package:lightmeter/screens/shared/sliver_screen/screen_sliver.dart'; import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; diff --git a/lib/screens/settings/components/equipment/components/equipment_profiles/widget_list_tile_equipment_profiles.dart b/lib/screens/settings/components/metering/components/equipment_profiles/widget_list_tile_equipment_profiles.dart similarity index 81% rename from lib/screens/settings/components/equipment/components/equipment_profiles/widget_list_tile_equipment_profiles.dart rename to lib/screens/settings/components/metering/components/equipment_profiles/widget_list_tile_equipment_profiles.dart index 0cb5631..740918f 100644 --- a/lib/screens/settings/components/equipment/components/equipment_profiles/widget_list_tile_equipment_profiles.dart +++ b/lib/screens/settings/components/metering/components/equipment_profiles/widget_list_tile_equipment_profiles.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart'; +import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart'; import 'package:lightmeter/screens/settings/components/shared/iap_list_tile/widget_list_tile_iap.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; diff --git a/lib/screens/settings/components/equipment/components/films/widget_list_tile_films.dart b/lib/screens/settings/components/metering/components/films/widget_list_tile_films.dart similarity index 100% rename from lib/screens/settings/components/equipment/components/films/widget_list_tile_films.dart rename to lib/screens/settings/components/metering/components/films/widget_list_tile_films.dart diff --git a/lib/screens/settings/components/metering/widget_settings_section_metering.dart b/lib/screens/settings/components/metering/widget_settings_section_metering.dart index c5b9e1d..90de68d 100644 --- a/lib/screens/settings/components/metering/widget_settings_section_metering.dart +++ b/lib/screens/settings/components/metering/widget_settings_section_metering.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/screens/settings/components/metering/components/calibration/widget_list_tile_calibration.dart'; +import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/widget_list_tile_equipment_profiles.dart'; +import 'package:lightmeter/screens/settings/components/metering/components/films/widget_list_tile_films.dart'; import 'package:lightmeter/screens/settings/components/metering/components/fractional_stops/widget_list_tile_fractional_stops.dart'; import 'package:lightmeter/screens/settings/components/metering/components/metering_screen_layout/widget_list_tile_metering_screen_layout.dart'; import 'package:lightmeter/screens/settings/components/shared/settings_section/widget_settings_section.dart'; @@ -16,6 +18,8 @@ class MeteringSettingsSection extends StatelessWidget { StopTypeListTile(), CalibrationListTile(), MeteringScreenLayoutListTile(), + EquipmentProfilesListTile(), + FilmsListTile(), ], ); } diff --git a/lib/screens/settings/screen_settings.dart b/lib/screens/settings/screen_settings.dart index 4102282..ea8e1c5 100644 --- a/lib/screens/settings/screen_settings.dart +++ b/lib/screens/settings/screen_settings.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/screens/settings/components/about/widget_settings_section_about.dart'; -import 'package:lightmeter/screens/settings/components/equipment/widget_settings_section_equipment.dart'; import 'package:lightmeter/screens/settings/components/general/widget_settings_section_general.dart'; import 'package:lightmeter/screens/settings/components/lightmeter_pro/widget_settings_section_lightmeter_pro.dart'; import 'package:lightmeter/screens/settings/components/metering/widget_settings_section_metering.dart'; @@ -42,7 +41,6 @@ class _SettingsScreenState extends State { if (!IAPProducts.isPurchased(context, IAPProductType.paidFeatures)) const LightmeterProSettingsSection(), const MeteringSettingsSection(), - const EquipmentSettingsSection(), const GeneralSettingsSection(), const ThemeSettingsSection(), const AboutSettingsSection(), From a5ad3912739cdb6bd4fa2cc476b8ac28ccd78801 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 19 Sep 2023 18:45:24 +0000 Subject: [PATCH 19/23] Version bump --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 22d5ddb..72225c4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: lightmeter description: Lightmeter app inspired by Material 3 design system. publish_to: "none" -version: 0.14.1+40 +version: 0.15.0+41 environment: sdk: ">=3.0.0 <4.0.0" From 4288be7d57d1504528d8b6802c3925a1b255f53d Mon Sep 17 00:00:00 2001 From: Vadim <44135514+vodemn@users.noreply.github.com> Date: Wed, 20 Sep 2023 11:10:54 +0200 Subject: [PATCH 20/23] Use iap v0.4.0 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 72225c4..7cdec3c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -26,7 +26,7 @@ dependencies: m3_lightmeter_iap: git: url: "https://github.com/vodemn/m3_lightmeter_iap" - ref: main + ref: v0.4.0 m3_lightmeter_resources: git: url: "https://github.com/vodemn/m3_lightmeter_resources" From cc660de0c4e5ec47f0ebccba3e3437191a97ee44 Mon Sep 17 00:00:00 2001 From: Vadim <44135514+vodemn@users.noreply.github.com> Date: Wed, 20 Sep 2023 11:58:04 +0200 Subject: [PATCH 21/23] Fixed PR check (#122) * updated stub script to work with tags * depend on step conclusion * check PR number --- .github/scripts/stub_iap.sh | 2 +- .github/workflows/pr_check.yml | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/scripts/stub_iap.sh b/.github/scripts/stub_iap.sh index 210e1ea..5ed6a48 100644 --- a/.github/scripts/stub_iap.sh +++ b/.github/scripts/stub_iap.sh @@ -1,2 +1,2 @@ # https://unix.stackexchange.com/questions/435708/regex-multiline-pattern-and-substitution-replacement -perl -0777 -i -pe 's/( m3_lightmeter_iap:\n)( git:\n url: "https:\/\/github.com\/vodemn\/m3_lightmeter_iap"\n ref: main)/$1 path: iap/sg' pubspec.yaml \ No newline at end of file +perl -0777 -i -pe 's/( m3_lightmeter_iap:\n)( git:\n url: "https:\/\/github.com\/vodemn\/m3_lightmeter_iap"\n ref: v\d{1,2}.\d{1,2}.\d{1,2})/$1 path: iap/sg' pubspec.yaml \ No newline at end of file diff --git a/.github/workflows/pr_check.yml b/.github/workflows/pr_check.yml index ac420da..caf7a97 100644 --- a/.github/workflows/pr_check.yml +++ b/.github/workflows/pr_check.yml @@ -17,18 +17,23 @@ jobs: runs-on: macos-11 timeout-minutes: 5 steps: + - uses: 8BitJonny/gh-get-current-pr@2.2.0 + id: PR + - uses: actions/checkout@v3 with: submodules: recursive - name: Connect private iap package uses: webfactory/ssh-agent@v0.8.0 - if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} + id: fetch-iap + if: steps.PR.outputs.number == 'null' || github.event.pull_request.head.repo.full_name == github.repository with: ssh-private-key: ${{ secrets.M3_LIGHTMETER_IAP_KEY }} - name: Override iap package with stub - if: ${{ github.event.pull_request.head.repo.full_name != github.repository }} + id: override-iap + if: steps.fetch-iap.conclusion != 'success' run: bash ./.github/scripts/stub_iap.sh - uses: subosito/flutter-action@v2 @@ -49,8 +54,8 @@ jobs: run: flutter test - name: Analyze project source with stub - if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} + if: steps.override-iap.conclusion != 'success' run: | bash ./.github/scripts/stub_iap.sh flutter pub get - flutter analyze lib --fatal-infos + flutter analyze lib --fatal-infos \ No newline at end of file From 2a3c6b0b0910992ca34dd3006fe11d7b41381e89 Mon Sep 17 00:00:00 2001 From: Vadim <44135514+vodemn@users.noreply.github.com> Date: Wed, 20 Sep 2023 12:11:23 +0200 Subject: [PATCH 22/23] Added user feedback label to issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- .github/ISSUE_TEMPLATE/feature-request-or-improvement.md | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index ba40f08..65d6487 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -2,7 +2,7 @@ name: Bug report about: Create a bug report to help improve the app title: '' -labels: bug +labels: bug, user feedback assignees: vodemn --- diff --git a/.github/ISSUE_TEMPLATE/feature-request-or-improvement.md b/.github/ISSUE_TEMPLATE/feature-request-or-improvement.md index 381e939..e8b2b0c 100644 --- a/.github/ISSUE_TEMPLATE/feature-request-or-improvement.md +++ b/.github/ISSUE_TEMPLATE/feature-request-or-improvement.md @@ -2,19 +2,16 @@ name: Feature request or improvement about: Suggest an idea for this project title: '' -labels: feature +labels: feature, user feedback assignees: vodemn --- -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] +**Describe the feature or the problem it solves** +A clear and concise description of what the problem is. **Describe the solution you'd like** A clear and concise description of what you want to happen. -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - **Additional context** Add any other context or screenshots about the feature request here. From 79105ab4f185ea462f84d1c13523e8086af73ade Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 20 Sep 2023 10:31:50 +0000 Subject: [PATCH 23/23] Version bump --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 7cdec3c..5454e81 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: lightmeter description: Lightmeter app inspired by Material 3 design system. publish_to: "none" -version: 0.15.0+41 +version: 0.15.1+42 environment: sdk: ">=3.0.0 <4.0.0"