diff --git a/integration_test/purchases_test.dart b/integration_test/purchases_test.dart index e54c0d5..99f27ac 100644 --- a/integration_test/purchases_test.dart +++ b/integration_test/purchases_test.dart @@ -10,7 +10,7 @@ import 'package:lightmeter/screens/metering/components/shared/readings_container 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/lightmeter_pro/widget_lightmeter_pro.dart'; +import 'package:lightmeter/screens/metering/components/shared/readings_container/components/lightmeter_pro_badge/widget_badge_lightmeter_pro.dart'; import 'package:lightmeter/screens/metering/components/shared/readings_container/components/nd_picker/widget_picker_nd.dart'; import 'package:lightmeter/screens/settings/components/shared/disable/widget_disable.dart'; import 'package:lightmeter/screens/settings/screen_settings.dart'; @@ -77,7 +77,7 @@ void testPurchases(String description) { } void _expectProMeteringScreen({required bool enabled}) { - expect(find.byType(LightmeterProAnimatedDialog), !enabled ? findsOneWidget : findsNothing); + expect(find.byType(LightmeterProBadge), !enabled ? findsOneWidget : findsNothing); expect(find.byType(EquipmentProfilePicker), enabled ? findsOneWidget : findsNothing); expect(find.byType(ExtremeExposurePairsContainer), findsOneWidget); expect(find.byType(FilmPicker), enabled ? findsOneWidget : findsNothing); diff --git a/lib/runner.dart b/lib/runner.dart index 16f7255..0cde502 100644 --- a/lib/runner.dart +++ b/lib/runner.dart @@ -26,7 +26,7 @@ Future runLightmeterApp(Environment env) async { runApp( env.buildType == BuildType.dev ? IAPProducts( - isPro: true, + isPro: false, child: application, ) : IAPProductsProvider(child: application), diff --git a/lib/screens/equipment_profiles/screen_equipment_profiles.dart b/lib/screens/equipment_profiles/screen_equipment_profiles.dart index af57d45..4fb1afa 100644 --- a/lib/screens/equipment_profiles/screen_equipment_profiles.dart +++ b/lib/screens/equipment_profiles/screen_equipment_profiles.dart @@ -6,6 +6,7 @@ import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/screens/equipment_profile_edit/flow_equipment_profile_edit.dart'; import 'package:lightmeter/screens/shared/sliver_placeholder/widget_sliver_placeholder.dart'; import 'package:lightmeter/screens/shared/sliver_screen/screen_sliver.dart'; +import 'package:lightmeter/utils/guard_pro_tap.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class EquipmentProfilesScreen extends StatefulWidget { @@ -41,9 +42,14 @@ class _EquipmentProfilesScreenState extends State with } void _addProfile() { - Navigator.of(context).pushNamed( - NavigationRoutes.equipmentProfileEditScreen.name, - arguments: const EquipmentProfileEditArgs(editType: EquipmentProfileEditType.add), + guardProTap( + context, + () { + Navigator.of(context).pushNamed( + NavigationRoutes.equipmentProfileEditScreen.name, + arguments: const EquipmentProfileEditArgs(editType: EquipmentProfileEditType.add), + ); + }, ); } diff --git a/lib/screens/lightmeter_pro/components/offering/widget_offering_lightmeter_pro.dart b/lib/screens/lightmeter_pro/components/offering/widget_offering_lightmeter_pro.dart index b3a8f6d..f3d96ff 100644 --- a/lib/screens/lightmeter_pro/components/offering/widget_offering_lightmeter_pro.dart +++ b/lib/screens/lightmeter_pro/components/offering/widget_offering_lightmeter_pro.dart @@ -107,24 +107,18 @@ class _Products extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ if (monthly case final monthly?) - Padding( - padding: const EdgeInsets.only(bottom: Dimens.paddingS), - child: _ProductItem( - title: S.of(context).monthly, - price: S.of(context).pricePerMonth(monthly.price), - isSelected: selected == monthly, - onPressed: () => onProductSelected(monthly), - ), + _ProductItem( + title: S.of(context).monthly, + price: S.of(context).pricePerMonth(monthly.price), + isSelected: selected == monthly, + onPressed: () => onProductSelected(monthly), ), if (yearly case final yearly?) - Padding( - padding: const EdgeInsets.only(bottom: Dimens.paddingS), - child: _ProductItem( - title: S.of(context).yearly, - price: S.of(context).pricePerYear(yearly.price), - isSelected: selected == yearly, - onPressed: () => onProductSelected(yearly), - ), + _ProductItem( + title: S.of(context).yearly, + price: S.of(context).pricePerYear(yearly.price), + isSelected: selected == yearly, + onPressed: () => onProductSelected(yearly), ), if (lifetime case final lifetime?) _ProductItem( @@ -133,7 +127,7 @@ class _Products extends StatelessWidget { isSelected: selected == lifetime, onPressed: () => onProductSelected(lifetime), ), - ], + ].intersperse(const SizedBox(height: Dimens.grid8)).toList(growable: false), ); } } @@ -220,3 +214,16 @@ class _ProductAnimatedText extends StatelessWidget { ); } } + +extension on List { + Iterable intersperse(Widget element) sync* { + final iterator = this.iterator; + if (iterator.moveNext()) { + yield iterator.current; + while (iterator.moveNext()) { + yield element; + yield iterator.current; + } + } + } +} diff --git a/lib/screens/lightmeter_pro/screen_lightmeter_pro.dart b/lib/screens/lightmeter_pro/screen_lightmeter_pro.dart index 00ba81d..80ca149 100644 --- a/lib/screens/lightmeter_pro/screen_lightmeter_pro.dart +++ b/lib/screens/lightmeter_pro/screen_lightmeter_pro.dart @@ -256,10 +256,7 @@ class _FeatureHighlight extends StatelessWidget { ).width + Dimens.paddingM * 2, ), - padding: const EdgeInsets.symmetric( - horizontal: Dimens.paddingM, - vertical: Dimens.paddingS, - ), + padding: const EdgeInsets.symmetric(vertical: Dimens.paddingS), decoration: BoxDecoration( color: highlight ? Theme.of(context).colorScheme.secondaryContainer : null, borderRadius: roundedTop @@ -274,7 +271,7 @@ class _FeatureHighlight extends StatelessWidget { ) : null, ), - child: child, + child: Center(child: child), ); } } diff --git a/lib/screens/logbook_photos/screen_logbook_photos.dart b/lib/screens/logbook_photos/screen_logbook_photos.dart index f351e23..22be0f5 100644 --- a/lib/screens/logbook_photos/screen_logbook_photos.dart +++ b/lib/screens/logbook_photos/screen_logbook_photos.dart @@ -7,6 +7,8 @@ import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/screens/logbook_photos/components/grid_tile/widget_grid_tile_logbook_photo.dart'; import 'package:lightmeter/screens/shared/icon_placeholder/widget_icon_placeholder.dart'; import 'package:lightmeter/screens/shared/sliver_screen/screen_sliver.dart'; +import 'package:lightmeter/utils/context_utils.dart'; +import 'package:lightmeter/utils/guard_pro_tap.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class LogbookPhotosScreen extends StatefulWidget { @@ -30,8 +32,15 @@ class _LogbookPhotosScreenState extends State with SingleTi child: SwitchListTile( secondary: const Icon(Icons.book_outlined), title: Text(S.of(context).saveNewPhotos), - value: LogbookPhotos.isEnabledOf(context), - onChanged: LogbookPhotosProvider.of(context).saveLogbookPhotos, + value: LogbookPhotos.isEnabledOf(context) && context.isPro, + onChanged: (value) { + guardProTap( + context, + () { + Navigator.of(context).pushNamed(NavigationRoutes.proFeaturesScreen.name); + }, + ); + }, ), ), ), 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 2584783..fcc7556 100644 --- a/lib/screens/metering/components/bottom_controls/widget_bottom_controls.dart +++ b/lib/screens/metering/components/bottom_controls/widget_bottom_controls.dart @@ -4,7 +4,6 @@ import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/screens/shared/animated_circular_button/widget_button_circular_animated.dart'; import 'package:lightmeter/screens/shared/bottom_controls_bar/widget_bottom_controls_bar.dart'; -import 'package:lightmeter/utils/context_utils.dart'; class MeteringBottomControls extends StatelessWidget { final double? ev; @@ -76,7 +75,7 @@ class _EvValueText extends StatelessWidget { } String _text(BuildContext context) { - final bool showEv100 = context.isPro && UserPreferencesProvider.showEv100Of(context); + final bool showEv100 = UserPreferencesProvider.showEv100Of(context); final StringBuffer buffer = StringBuffer() ..writeAll([ (showEv100 ? ev100 : ev).toStringAsFixed(1), 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 e5477f8..8dd55ef 100644 --- a/lib/screens/metering/components/camera_container/widget_container_camera.dart +++ b/lib/screens/metering/components/camera_container/widget_container_camera.dart @@ -17,6 +17,7 @@ import 'package:lightmeter/screens/metering/components/camera_container/models/c import 'package:lightmeter/screens/metering/components/camera_container/state_container_camera.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'; +import 'package:lightmeter/screens/metering/components/shared/readings_container/components/lightmeter_pro_badge/widget_badge_lightmeter_pro.dart'; import 'package:lightmeter/screens/metering/components/shared/readings_container/widget_container_readings.dart'; import 'package:lightmeter/utils/context_utils.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; @@ -112,20 +113,17 @@ class CameraContainer extends StatelessWidget { double _meteringContainerHeight(BuildContext context) { double enabledFeaturesHeight = 0; - if (!context.isPro) { - if (RemoteConfig.isEnabled(context, Feature.showUnlockProOnMainScreen)) { - enabledFeaturesHeight += Dimens.readingContainerSingleValueHeight; - enabledFeaturesHeight += Dimens.paddingS; - } - } else { - if (context.meteringFeature(MeteringScreenLayoutFeature.equipmentProfiles)) { - enabledFeaturesHeight += Dimens.readingContainerSingleValueHeight; - enabledFeaturesHeight += Dimens.paddingS; - } - if (context.meteringFeature(MeteringScreenLayoutFeature.filmPicker)) { - enabledFeaturesHeight += Dimens.readingContainerSingleValueHeight; - enabledFeaturesHeight += Dimens.paddingS; - } + if (!context.isPro && RemoteConfig.isEnabled(context, Feature.showUnlockProOnMainScreen)) { + enabledFeaturesHeight += LightmeterProBadge.height(context); + enabledFeaturesHeight += Dimens.paddingS; + } + if (context.meteringFeature(MeteringScreenLayoutFeature.equipmentProfiles)) { + enabledFeaturesHeight += Dimens.readingContainerSingleValueHeight; + enabledFeaturesHeight += Dimens.paddingS; + } + if (context.meteringFeature(MeteringScreenLayoutFeature.filmPicker)) { + enabledFeaturesHeight += Dimens.readingContainerSingleValueHeight; + enabledFeaturesHeight += Dimens.paddingS; } if (context.meteringFeature(MeteringScreenLayoutFeature.extremeExposurePairs)) { enabledFeaturesHeight += Dimens.readingContainerDoubleValueHeight; diff --git a/lib/screens/metering/components/shared/readings_container/components/lightmeter_pro/widget_lightmeter_pro.dart b/lib/screens/metering/components/shared/readings_container/components/lightmeter_pro/widget_lightmeter_pro.dart deleted file mode 100644 index f1b77ff..0000000 --- a/lib/screens/metering/components/shared/readings_container/components/lightmeter_pro/widget_lightmeter_pro.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/navigation/routes.dart'; -import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/reading_value_container/widget_container_reading_value.dart'; - -class LightmeterProAnimatedDialog extends StatelessWidget { - const LightmeterProAnimatedDialog({super.key}); - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () { - Navigator.of(context).pushNamed(NavigationRoutes.proFeaturesScreen.name); - }, - child: ReadingValueContainer( - color: Theme.of(context).colorScheme.secondary, - textColor: Theme.of(context).colorScheme.onSecondary, - values: [ - ReadingValue( - label: S.of(context).proFeaturesTitle, - value: S.of(context).getPro, - ), - ], - ), - ); - } -} diff --git a/lib/screens/metering/components/shared/readings_container/components/lightmeter_pro_badge/widget_badge_lightmeter_pro.dart b/lib/screens/metering/components/shared/readings_container/components/lightmeter_pro_badge/widget_badge_lightmeter_pro.dart new file mode 100644 index 0000000..fe9066e --- /dev/null +++ b/lib/screens/metering/components/shared/readings_container/components/lightmeter_pro_badge/widget_badge_lightmeter_pro.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/navigation/routes.dart'; +import 'package:lightmeter/res/dimens.dart'; +import 'package:lightmeter/utils/text_height.dart'; + +class LightmeterProBadge extends StatelessWidget { + const LightmeterProBadge({super.key}); + + static double height(BuildContext context) { + if (Theme.of(context).textTheme.titleMedium?.lineHeight case final lineHeight?) { + return Dimens.paddingS * 2 + lineHeight; + } else { + return 40; + } + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + Navigator.of(context).pushNamed(NavigationRoutes.proFeaturesScreen.name); + }, + child: Container( + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(Dimens.borderRadiusM), + color: Theme.of(context).colorScheme.secondaryContainer, + ), + padding: const EdgeInsets.symmetric( + horizontal: Dimens.paddingM, + vertical: Dimens.paddingS, + ), + child: Text( + S.of(context).getPro, + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(color: Theme.of(context).colorScheme.onSecondaryContainer), + ), + ), + ); + } +} 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 1b9a1c4..fe8c8b0 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 @@ -9,7 +9,7 @@ import 'package:lightmeter/screens/metering/components/shared/readings_container 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/lightmeter_pro/widget_lightmeter_pro.dart'; +import 'package:lightmeter/screens/metering/components/shared/readings_container/components/lightmeter_pro_badge/widget_badge_lightmeter_pro.dart'; import 'package:lightmeter/screens/metering/components/shared/readings_container/components/nd_picker/widget_picker_nd.dart'; import 'package:lightmeter/utils/context_utils.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; @@ -38,10 +38,10 @@ class ReadingsContainer extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (!context.isPro && RemoteConfig.isEnabled(context, Feature.showUnlockProOnMainScreen)) ...[ - const LightmeterProAnimatedDialog(), + const LightmeterProBadge(), const _InnerPadding(), ], - if (context.isPro && context.meteringFeature(MeteringScreenLayoutFeature.equipmentProfiles)) ...[ + if (context.meteringFeature(MeteringScreenLayoutFeature.equipmentProfiles)) ...[ const EquipmentProfilePicker(), const _InnerPadding(), ], @@ -52,7 +52,7 @@ class ReadingsContainer extends StatelessWidget { ), const _InnerPadding(), ], - if (context.isPro && context.meteringFeature(MeteringScreenLayoutFeature.filmPicker)) ...[ + if (context.meteringFeature(MeteringScreenLayoutFeature.filmPicker)) ...[ FilmPicker(selectedIso: iso), const _InnerPadding(), ], diff --git a/lib/screens/settings/components/lightmeter_pro/components/restore_purchases/widget_list_tile_restore_purchases.dart b/lib/screens/settings/components/about/components/restore_purchases/widget_list_tile_restore_purchases.dart similarity index 100% rename from lib/screens/settings/components/lightmeter_pro/components/restore_purchases/widget_list_tile_restore_purchases.dart rename to lib/screens/settings/components/about/components/restore_purchases/widget_list_tile_restore_purchases.dart diff --git a/lib/screens/settings/components/about/widget_settings_section_about.dart b/lib/screens/settings/components/about/widget_settings_section_about.dart index d911ae8..19defe4 100644 --- a/lib/screens/settings/components/about/widget_settings_section_about.dart +++ b/lib/screens/settings/components/about/widget_settings_section_about.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/screens/settings/components/about/components/report_issue/widget_list_tile_report_issue.dart'; +import 'package:lightmeter/screens/settings/components/about/components/restore_purchases/widget_list_tile_restore_purchases.dart'; import 'package:lightmeter/screens/settings/components/about/components/source_code/widget_list_tile_source_code.dart'; import 'package:lightmeter/screens/settings/components/about/components/version/widget_list_tile_version.dart'; import 'package:lightmeter/screens/settings/components/about/components/write_email/widget_list_tile_write_email.dart'; import 'package:lightmeter/screens/settings/components/shared/settings_section/widget_settings_section.dart'; +import 'package:lightmeter/utils/context_utils.dart'; class AboutSettingsSection extends StatelessWidget { const AboutSettingsSection({super.key}); @@ -13,11 +15,12 @@ class AboutSettingsSection extends StatelessWidget { Widget build(BuildContext context) { return SettingsSection( title: S.of(context).about, - children: const [ - SourceCodeListTile(), - ReportIssueListTile(), - WriteEmailListTile(), - VersionListTile(), + children: [ + const SourceCodeListTile(), + const ReportIssueListTile(), + const WriteEmailListTile(), + const VersionListTile(), + if (context.isPro) const RestorePurchasesListTile(), ], ); } diff --git a/lib/screens/settings/components/camera/camera_features/widget_list_tile_camera_features.dart b/lib/screens/settings/components/camera/camera_features/widget_list_tile_camera_features.dart index 00ec9fd..86f86c3 100644 --- a/lib/screens/settings/components/camera/camera_features/widget_list_tile_camera_features.dart +++ b/lib/screens/settings/components/camera/camera_features/widget_list_tile_camera_features.dart @@ -4,39 +4,53 @@ import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/providers/services_provider.dart'; import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/screens/settings/components/shared/dialog_switch/widget_dialog_switch.dart'; -import 'package:lightmeter/screens/settings/components/shared/iap_list_tile/widget_list_tile_iap.dart'; class CameraFeaturesListTile extends StatelessWidget { const CameraFeaturesListTile({super.key}); @override Widget build(BuildContext context) { - return IAPListTile( + return ListTile( leading: const Icon(Icons.camera_alt_outlined), title: Text(S.of(context).cameraFeatures), onTap: () { + UserPreferencesProvider.cameraConfigOf(context).entries.map( + (entry) => DialogSwitchListItem( + value: CameraFeature.spotMetering, + title: S.of(context).cameraFeatureSpotMetering, + subtitle: S.of(context).cameraFeatureSpotMeteringHint, + initialValue: UserPreferencesProvider.cameraFeatureOf(context, CameraFeature.spotMetering), + isProRequired: true, + ), + ); showDialog( context: context, builder: (_) => DialogSwitch( icon: Icons.camera_alt_outlined, title: S.of(context).cameraFeatures, - values: UserPreferencesProvider.cameraConfigOf(context), - enabledAdapter: (feature) => switch (feature) { - CameraFeature.spotMetering => true, - CameraFeature.histogram => true, - CameraFeature.showFocalLength => - ServicesProvider.of(context).userPreferencesService.cameraFocalLength != null, - }, - titleAdapter: (context, feature) => switch (feature) { - CameraFeature.spotMetering => S.of(context).cameraFeatureSpotMetering, - CameraFeature.histogram => S.of(context).cameraFeatureHistogram, - CameraFeature.showFocalLength => S.of(context).cameraFeaturesShowFocalLength, - }, - subtitleAdapter: (context, feature) => switch (feature) { - CameraFeature.spotMetering => S.of(context).cameraFeatureSpotMeteringHint, - CameraFeature.histogram => S.of(context).cameraFeatureHistogramHint, - CameraFeature.showFocalLength => S.of(context).cameraFeaturesShowFocalLengthHint, - }, + items: [ + DialogSwitchListItem( + value: CameraFeature.showFocalLength, + title: S.of(context).cameraFeaturesShowFocalLength, + subtitle: S.of(context).cameraFeaturesShowFocalLengthHint, + initialValue: UserPreferencesProvider.cameraFeatureOf(context, CameraFeature.showFocalLength), + isEnabled: ServicesProvider.of(context).userPreferencesService.cameraFocalLength != null, + ), + DialogSwitchListItem( + value: CameraFeature.spotMetering, + title: S.of(context).cameraFeatureSpotMetering, + subtitle: S.of(context).cameraFeatureSpotMeteringHint, + initialValue: UserPreferencesProvider.cameraFeatureOf(context, CameraFeature.spotMetering), + isProRequired: true, + ), + DialogSwitchListItem( + value: CameraFeature.histogram, + title: S.of(context).cameraFeatureHistogram, + subtitle: S.of(context).cameraFeatureHistogramHint, + initialValue: UserPreferencesProvider.cameraFeatureOf(context, CameraFeature.histogram), + isProRequired: true, + ), + ], onSave: UserPreferencesProvider.of(context).setCameraFeature, ), ); diff --git a/lib/screens/settings/components/camera/logbook/widget_list_tile_logbook.dart b/lib/screens/settings/components/camera/logbook/widget_list_tile_logbook.dart index 1beb4bc..d853f93 100644 --- a/lib/screens/settings/components/camera/logbook/widget_list_tile_logbook.dart +++ b/lib/screens/settings/components/camera/logbook/widget_list_tile_logbook.dart @@ -1,14 +1,13 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/navigation/routes.dart'; -import 'package:lightmeter/screens/settings/components/shared/iap_list_tile/widget_list_tile_iap.dart'; class LogbookListTile extends StatelessWidget { const LogbookListTile({super.key}); @override Widget build(BuildContext context) { - return IAPListTile( + return ListTile( leading: const Icon(Icons.book_outlined), title: Text(S.of(context).logbook), onTap: () { diff --git a/lib/screens/settings/components/general/components/timer/widget_list_tile_timer.dart b/lib/screens/settings/components/general/components/timer/widget_list_tile_timer.dart index 0fdaf52..05bf5d6 100644 --- a/lib/screens/settings/components/general/components/timer/widget_list_tile_timer.dart +++ b/lib/screens/settings/components/general/components/timer/widget_list_tile_timer.dart @@ -3,24 +3,26 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/screens/settings/components/general/components/timer/bloc_list_tile_timer.dart'; -import 'package:lightmeter/screens/settings/components/shared/disable/widget_disable.dart'; import 'package:lightmeter/utils/context_utils.dart'; +import 'package:lightmeter/utils/guard_pro_tap.dart'; class TimerListTile extends StatelessWidget { const TimerListTile({super.key}); @override Widget build(BuildContext context) { - return Disable( - disable: !context.isPro, - child: BlocBuilder( - builder: (context, state) => SwitchListTile( - secondary: const Icon(Icons.timer_outlined), - title: Text(S.of(context).autostartTimer), - value: state && context.isPro, - onChanged: context.read().onChanged, - contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM), - ), + return BlocBuilder( + builder: (context, state) => SwitchListTile( + secondary: const Icon(Icons.timer_outlined), + title: Text(S.of(context).autostartTimer), + value: context.isPro && state, + contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM), + onChanged: (value) { + guardProTap( + context, + () => context.read().onChanged(value), + ); + }, ), ); } 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 deleted file mode 100644 index 160dcc0..0000000 --- a/lib/screens/settings/components/lightmeter_pro/components/buy_pro/widget_list_tile_buy_pro.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/navigation/routes.dart'; - -class BuyProListTile extends StatelessWidget { - const BuyProListTile({super.key}); - - @override - Widget build(BuildContext context) { - // TODO: implement pending handling via REvenueCat - return ListTile( - leading: const Icon(Icons.bolt), - title: Text(S.of(context).getPro), - onTap: () { - Navigator.of(context).pushNamed(NavigationRoutes.proFeaturesScreen.name); - }, - ); - } -} 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 deleted file mode 100644 index b8a0a91..0000000 --- a/lib/screens/settings/components/lightmeter_pro/widget_settings_section_lightmeter_pro.dart +++ /dev/null @@ -1,22 +0,0 @@ -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/lightmeter_pro/components/restore_purchases/widget_list_tile_restore_purchases.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( - backgroundColor: Theme.of(context).colorScheme.secondary, - foregroundColor: Theme.of(context).colorScheme.onSecondary, - title: S.of(context).proFeaturesTitle, - children: const [ - BuyProListTile(), - RestorePurchasesListTile(), - ], - ); - } -} 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 1c8c6db..2864cb2 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,14 +1,13 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/navigation/routes.dart'; -import 'package:lightmeter/screens/settings/components/shared/iap_list_tile/widget_list_tile_iap.dart'; class EquipmentProfilesListTile extends StatelessWidget { const EquipmentProfilesListTile({super.key}); @override Widget build(BuildContext context) { - return IAPListTile( + return ListTile( leading: const Icon(Icons.camera_outlined), title: Text(S.of(context).equipmentProfiles), onTap: () { diff --git a/lib/screens/settings/components/metering/components/films/widget_list_tile_films.dart b/lib/screens/settings/components/metering/components/films/widget_list_tile_films.dart index 0cc1733..b6f5684 100644 --- a/lib/screens/settings/components/metering/components/films/widget_list_tile_films.dart +++ b/lib/screens/settings/components/metering/components/films/widget_list_tile_films.dart @@ -1,17 +1,24 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/navigation/routes.dart'; -import 'package:lightmeter/screens/settings/components/shared/iap_list_tile/widget_list_tile_iap.dart'; +import 'package:lightmeter/utils/guard_pro_tap.dart'; class FilmsListTile extends StatelessWidget { const FilmsListTile({super.key}); @override Widget build(BuildContext context) { - return IAPListTile( + return ListTile( leading: const Icon(Icons.camera_roll_outlined), title: Text(S.of(context).films), - onTap: () => Navigator.of(context).pushNamed(NavigationRoutes.filmsListScreen.name), + onTap: () { + guardProTap( + context, + () { + Navigator.of(context).pushNamed(NavigationRoutes.filmsListScreen.name); + }, + ); + }, ); } } diff --git a/lib/screens/settings/components/metering/components/metering_screen_layout/widget_list_tile_metering_screen_layout.dart b/lib/screens/settings/components/metering/components/metering_screen_layout/widget_list_tile_metering_screen_layout.dart index 14bf0fb..0898541 100644 --- a/lib/screens/settings/components/metering/components/metering_screen_layout/widget_list_tile_metering_screen_layout.dart +++ b/lib/screens/settings/components/metering/components/metering_screen_layout/widget_list_tile_metering_screen_layout.dart @@ -5,7 +5,6 @@ import 'package:lightmeter/providers/equipment_profile_provider.dart'; import 'package:lightmeter/providers/films_provider.dart'; import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/screens/settings/components/shared/dialog_switch/widget_dialog_switch.dart'; -import 'package:lightmeter/utils/context_utils.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class MeteringScreenLayoutListTile extends StatelessWidget { @@ -23,17 +22,16 @@ class MeteringScreenLayoutListTile extends StatelessWidget { icon: Icons.layers_outlined, title: S.of(context).meteringScreenLayout, description: S.of(context).meteringScreenLayoutHint, - values: UserPreferencesProvider.meteringScreenConfigOf(context), - titleAdapter: _toStringLocalized, - enabledAdapter: (value) { - switch (value) { - case MeteringScreenLayoutFeature.equipmentProfiles: - case MeteringScreenLayoutFeature.filmPicker: - return context.isPro; - default: - return true; - } - }, + items: UserPreferencesProvider.meteringScreenConfigOf(context) + .entries + .map( + (entry) => DialogSwitchListItem( + value: entry.key, + title: _toStringLocalized(context, entry.key), + initialValue: UserPreferencesProvider.meteringScreenFeatureOf(context, entry.key), + ), + ) + .toList(growable: false), onSave: (value) { if (!value[MeteringScreenLayoutFeature.equipmentProfiles]!) { EquipmentProfilesProvider.of(context).selectProfile(EquipmentProfiles.of(context).first); diff --git a/lib/screens/settings/components/metering/components/show_ev_100/widget_list_tile_show_ev_100.dart b/lib/screens/settings/components/metering/components/show_ev_100/widget_list_tile_show_ev_100.dart index e0701f4..27e46fd 100644 --- a/lib/screens/settings/components/metering/components/show_ev_100/widget_list_tile_show_ev_100.dart +++ b/lib/screens/settings/components/metering/components/show_ev_100/widget_list_tile_show_ev_100.dart @@ -2,23 +2,18 @@ import 'package:flutter/material.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/settings/components/shared/disable/widget_disable.dart'; -import 'package:lightmeter/utils/context_utils.dart'; class ShowEv100ListTile extends StatelessWidget { const ShowEv100ListTile({super.key}); @override Widget build(BuildContext context) { - return Disable( - disable: !context.isPro, - child: SwitchListTile( - secondary: const Icon(Icons.adjust_outlined), - title: Text(S.of(context).showEv100), - value: context.isPro && UserPreferencesProvider.showEv100Of(context), - onChanged: (_) => UserPreferencesProvider.of(context).toggleShowEv100(), - contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM), - ), + return SwitchListTile( + secondary: const Icon(Icons.adjust_outlined), + title: Text(S.of(context).showEv100), + value: UserPreferencesProvider.showEv100Of(context), + contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM), + onChanged: (_) => UserPreferencesProvider.of(context).toggleShowEv100(), ); } } diff --git a/lib/screens/settings/components/shared/dialog_switch/widget_dialog_switch.dart b/lib/screens/settings/components/shared/dialog_switch/widget_dialog_switch.dart index dc43017..8b66209 100644 --- a/lib/screens/settings/components/shared/dialog_switch/widget_dialog_switch.dart +++ b/lib/screens/settings/components/shared/dialog_switch/widget_dialog_switch.dart @@ -1,28 +1,41 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/res/dimens.dart'; -import 'package:lightmeter/screens/settings/components/shared/disable/widget_disable.dart'; +import 'package:lightmeter/utils/context_utils.dart'; +import 'package:lightmeter/utils/guard_pro_tap.dart'; typedef StringAdapter = String Function(BuildContext context, T value); +class DialogSwitchListItem { + final T value; + final String title; + final String? subtitle; + final bool initialValue; + final bool isEnabled; + final bool isProRequired; + + const DialogSwitchListItem({ + required this.value, + required this.title, + this.subtitle, + required this.initialValue, + this.isEnabled = true, + this.isProRequired = false, + }); +} + class DialogSwitch extends StatefulWidget { final IconData icon; final String title; final String? description; - final Map values; - final StringAdapter titleAdapter; - final StringAdapter? subtitleAdapter; - final bool Function(T value)? enabledAdapter; + final List> items; final ValueChanged> onSave; const DialogSwitch({ required this.icon, required this.title, this.description, - required this.values, - required this.titleAdapter, - this.subtitleAdapter, - this.enabledAdapter, + required this.items, required this.onSave, super.key, }); @@ -32,7 +45,11 @@ class DialogSwitch extends StatefulWidget { } class _DialogSwitchState extends State> { - late final Map _features = Map.from(widget.values); + late final Map _features = Map.fromEntries( + widget.items.map( + (item) => MapEntry(item.value, item.initialValue), + ), + ); @override Widget build(BuildContext context) { @@ -55,27 +72,15 @@ class _DialogSwitchState extends State> { ], ListView( shrinkWrap: true, - children: _features.entries.map( - (entry) { - final isEnabled = widget.enabledAdapter?.call(entry.key) ?? true; - return Disable( - disable: !isEnabled, - child: SwitchListTile( - contentPadding: EdgeInsets.symmetric(horizontal: Dimens.dialogTitlePadding.left), - title: Text(widget.titleAdapter(context, entry.key)), - subtitle: widget.subtitleAdapter != null - ? Text( - widget.subtitleAdapter!.call(context, entry.key), - style: Theme.of(context).listTileTheme.subtitleTextStyle, - ) - : null, - value: isEnabled && _features[entry.key]!, - onChanged: (value) { - setState(() { - _features.update(entry.key, (_) => value); - }); - }, - ), + children: widget.items.map( + (item) { + final value = _features[item.value]!; + return SwitchListTile( + contentPadding: EdgeInsets.symmetric(horizontal: Dimens.dialogTitlePadding.left), + title: Text(item.title), + subtitle: item.subtitle != null ? Text(item.subtitle!) : null, + value: item.isProRequired ? context.isPro && value : value, + onChanged: item.isEnabled ? (value) => _setItem(item, value) : null, ); }, ).toList(), @@ -99,4 +104,18 @@ class _DialogSwitchState extends State> { ], ); } + + void _setItem(DialogSwitchListItem item, bool value) { + void setItemState() { + setState(() { + _features.update(item.value, (_) => value); + }); + } + + if (item.isProRequired) { + guardProTap(context, setItemState); + } else { + setItemState(); + } + } } 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 deleted file mode 100644 index d44c872..0000000 --- a/lib/screens/settings/components/shared/iap_list_tile/widget_list_tile_iap.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lightmeter/screens/settings/components/shared/disable/widget_disable.dart'; -import 'package:lightmeter/utils/context_utils.dart'; - -/// Depends on the product status and replaces [onTap] with purchase callback -/// if the product is purchasable. -class IAPListTile extends StatelessWidget { - final Icon leading; - final Text title; - final VoidCallback onTap; - final bool showPendingTrailing; - - const IAPListTile({ - required this.leading, - required this.title, - required this.onTap, - this.showPendingTrailing = false, - super.key, - }); - - @override - Widget build(BuildContext context) { - return Disable( - disable: !context.isPro, - child: ListTile( - leading: leading, - title: title, - onTap: onTap, - ), - ); - } -} 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 718c67c..53cba80 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,14 +4,10 @@ import 'package:lightmeter/res/dimens.dart'; class SettingsSection extends StatelessWidget { final String title; final List children; - final Color? backgroundColor; - final Color? foregroundColor; const SettingsSection({ required this.title, required this.children, - this.backgroundColor, - this.foregroundColor, super.key, }); @@ -25,33 +21,22 @@ class SettingsSection extends StatelessWidget { Dimens.paddingM, ), child: Card( - color: backgroundColor, child: Padding( padding: const EdgeInsets.symmetric(vertical: Dimens.paddingM), - child: Theme( - data: Theme.of(context).copyWith( - listTileTheme: Theme.of(context).listTileTheme.copyWith( - iconColor: foregroundColor, - textColor: foregroundColor, - ), - ), - 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: foregroundColor ?? 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/screen_settings.dart b/lib/screens/settings/screen_settings.dart index c6c7788..e6e05b2 100644 --- a/lib/screens/settings/screen_settings.dart +++ b/lib/screens/settings/screen_settings.dart @@ -3,12 +3,10 @@ import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/screens/settings/components/about/widget_settings_section_about.dart'; import 'package:lightmeter/screens/settings/components/camera/widget_settings_section_camera.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:lightmeter/utils/context_utils.dart'; import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; class SettingsScreen extends StatefulWidget { @@ -41,7 +39,6 @@ class _SettingsScreenState extends State { SliverList( delegate: SliverChildListDelegate( [ - if (!context.isPro) const LightmeterProSettingsSection(), const MeteringSettingsSection(), const CameraSettingsSection(), const GeneralSettingsSection(), diff --git a/lib/utils/guard_pro_tap.dart b/lib/utils/guard_pro_tap.dart new file mode 100644 index 0000000..624bd05 --- /dev/null +++ b/lib/utils/guard_pro_tap.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/navigation/routes.dart'; +import 'package:lightmeter/utils/context_utils.dart'; + +void guardProTap(BuildContext context, VoidCallback callback) { + if (context.isPro) { + callback(); + } else { + Navigator.of(context).pushNamed(NavigationRoutes.proFeaturesScreen.name); + } +} diff --git a/test/screens/lightmeter_pro/goldens/lightmeter_pro_screen.png b/test/screens/lightmeter_pro/goldens/lightmeter_pro_screen.png index d04fdec..5b0d0d7 100644 Binary files a/test/screens/lightmeter_pro/goldens/lightmeter_pro_screen.png and b/test/screens/lightmeter_pro/goldens/lightmeter_pro_screen.png differ diff --git a/test/screens/metering/goldens/metering_screen.png b/test/screens/metering/goldens/metering_screen.png index 2eddf2b..35a57b5 100644 Binary files a/test/screens/metering/goldens/metering_screen.png and b/test/screens/metering/goldens/metering_screen.png differ diff --git a/test/screens/settings/goldens/settings_screen.png b/test/screens/settings/goldens/settings_screen.png index 1faad6a..cd805dc 100644 Binary files a/test/screens/settings/goldens/settings_screen.png and b/test/screens/settings/goldens/settings_screen.png differ