This commit is contained in:
Vadim 2025-08-11 10:31:55 +00:00 committed by GitHub
commit bce487a788
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 266 additions and 277 deletions

View file

@ -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/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/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/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/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/components/shared/disable/widget_disable.dart';
import 'package:lightmeter/screens/settings/screen_settings.dart'; import 'package:lightmeter/screens/settings/screen_settings.dart';
@ -77,7 +77,7 @@ void testPurchases(String description) {
} }
void _expectProMeteringScreen({required bool enabled}) { 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(EquipmentProfilePicker), enabled ? findsOneWidget : findsNothing);
expect(find.byType(ExtremeExposurePairsContainer), findsOneWidget); expect(find.byType(ExtremeExposurePairsContainer), findsOneWidget);
expect(find.byType(FilmPicker), enabled ? findsOneWidget : findsNothing); expect(find.byType(FilmPicker), enabled ? findsOneWidget : findsNothing);

View file

@ -26,7 +26,7 @@ Future<void> runLightmeterApp(Environment env) async {
runApp( runApp(
env.buildType == BuildType.dev env.buildType == BuildType.dev
? IAPProducts( ? IAPProducts(
isPro: true, isPro: false,
child: application, child: application,
) )
: IAPProductsProvider(child: application), : IAPProductsProvider(child: application),

View file

@ -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/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_placeholder/widget_sliver_placeholder.dart';
import 'package:lightmeter/screens/shared/sliver_screen/screen_sliver.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'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class EquipmentProfilesScreen extends StatefulWidget { class EquipmentProfilesScreen extends StatefulWidget {
@ -41,9 +42,14 @@ class _EquipmentProfilesScreenState extends State<EquipmentProfilesScreen> with
} }
void _addProfile() { void _addProfile() {
Navigator.of(context).pushNamed( guardProTap(
NavigationRoutes.equipmentProfileEditScreen.name, context,
arguments: const EquipmentProfileEditArgs(editType: EquipmentProfileEditType.add), () {
Navigator.of(context).pushNamed(
NavigationRoutes.equipmentProfileEditScreen.name,
arguments: const EquipmentProfileEditArgs(editType: EquipmentProfileEditType.add),
);
},
); );
} }

View file

@ -107,24 +107,18 @@ class _Products extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (monthly case final monthly?) if (monthly case final monthly?)
Padding( _ProductItem(
padding: const EdgeInsets.only(bottom: Dimens.paddingS), title: S.of(context).monthly,
child: _ProductItem( price: S.of(context).pricePerMonth(monthly.price),
title: S.of(context).monthly, isSelected: selected == monthly,
price: S.of(context).pricePerMonth(monthly.price), onPressed: () => onProductSelected(monthly),
isSelected: selected == monthly,
onPressed: () => onProductSelected(monthly),
),
), ),
if (yearly case final yearly?) if (yearly case final yearly?)
Padding( _ProductItem(
padding: const EdgeInsets.only(bottom: Dimens.paddingS), title: S.of(context).yearly,
child: _ProductItem( price: S.of(context).pricePerYear(yearly.price),
title: S.of(context).yearly, isSelected: selected == yearly,
price: S.of(context).pricePerYear(yearly.price), onPressed: () => onProductSelected(yearly),
isSelected: selected == yearly,
onPressed: () => onProductSelected(yearly),
),
), ),
if (lifetime case final lifetime?) if (lifetime case final lifetime?)
_ProductItem( _ProductItem(
@ -133,7 +127,7 @@ class _Products extends StatelessWidget {
isSelected: selected == lifetime, isSelected: selected == lifetime,
onPressed: () => onProductSelected(lifetime), onPressed: () => onProductSelected(lifetime),
), ),
], ].intersperse(const SizedBox(height: Dimens.grid8)).toList(growable: false),
); );
} }
} }
@ -220,3 +214,16 @@ class _ProductAnimatedText extends StatelessWidget {
); );
} }
} }
extension on List<Widget> {
Iterable<Widget> intersperse(Widget element) sync* {
final iterator = this.iterator;
if (iterator.moveNext()) {
yield iterator.current;
while (iterator.moveNext()) {
yield element;
yield iterator.current;
}
}
}
}

View file

@ -256,10 +256,7 @@ class _FeatureHighlight extends StatelessWidget {
).width + ).width +
Dimens.paddingM * 2, Dimens.paddingM * 2,
), ),
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(vertical: Dimens.paddingS),
horizontal: Dimens.paddingM,
vertical: Dimens.paddingS,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: highlight ? Theme.of(context).colorScheme.secondaryContainer : null, color: highlight ? Theme.of(context).colorScheme.secondaryContainer : null,
borderRadius: roundedTop borderRadius: roundedTop
@ -274,7 +271,7 @@ class _FeatureHighlight extends StatelessWidget {
) )
: null, : null,
), ),
child: child, child: Center(child: child),
); );
} }
} }

View file

@ -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/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/icon_placeholder/widget_icon_placeholder.dart';
import 'package:lightmeter/screens/shared/sliver_screen/screen_sliver.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'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class LogbookPhotosScreen extends StatefulWidget { class LogbookPhotosScreen extends StatefulWidget {
@ -30,8 +32,15 @@ class _LogbookPhotosScreenState extends State<LogbookPhotosScreen> with SingleTi
child: SwitchListTile( child: SwitchListTile(
secondary: const Icon(Icons.book_outlined), secondary: const Icon(Icons.book_outlined),
title: Text(S.of(context).saveNewPhotos), title: Text(S.of(context).saveNewPhotos),
value: LogbookPhotos.isEnabledOf(context), value: LogbookPhotos.isEnabledOf(context) && context.isPro,
onChanged: LogbookPhotosProvider.of(context).saveLogbookPhotos, onChanged: (value) {
guardProTap(
context,
() {
Navigator.of(context).pushNamed(NavigationRoutes.proFeaturesScreen.name);
},
);
},
), ),
), ),
), ),

View file

@ -4,7 +4,6 @@ import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/providers/user_preferences_provider.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/animated_circular_button/widget_button_circular_animated.dart';
import 'package:lightmeter/screens/shared/bottom_controls_bar/widget_bottom_controls_bar.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 { class MeteringBottomControls extends StatelessWidget {
final double? ev; final double? ev;
@ -76,7 +75,7 @@ class _EvValueText extends StatelessWidget {
} }
String _text(BuildContext context) { String _text(BuildContext context) {
final bool showEv100 = context.isPro && UserPreferencesProvider.showEv100Of(context); final bool showEv100 = UserPreferencesProvider.showEv100Of(context);
final StringBuffer buffer = StringBuffer() final StringBuffer buffer = StringBuffer()
..writeAll([ ..writeAll([
(showEv100 ? ev100 : ev).toStringAsFixed(1), (showEv100 ? ev100 : ev).toStringAsFixed(1),

View file

@ -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/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/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/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/screens/metering/components/shared/readings_container/widget_container_readings.dart';
import 'package:lightmeter/utils/context_utils.dart'; import 'package:lightmeter/utils/context_utils.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
@ -112,20 +113,17 @@ class CameraContainer extends StatelessWidget {
double _meteringContainerHeight(BuildContext context) { double _meteringContainerHeight(BuildContext context) {
double enabledFeaturesHeight = 0; double enabledFeaturesHeight = 0;
if (!context.isPro) { if (!context.isPro && RemoteConfig.isEnabled(context, Feature.showUnlockProOnMainScreen)) {
if (RemoteConfig.isEnabled(context, Feature.showUnlockProOnMainScreen)) { enabledFeaturesHeight += LightmeterProBadge.height(context);
enabledFeaturesHeight += Dimens.readingContainerSingleValueHeight; enabledFeaturesHeight += Dimens.paddingS;
enabledFeaturesHeight += Dimens.paddingS; }
} if (context.meteringFeature(MeteringScreenLayoutFeature.equipmentProfiles)) {
} else { enabledFeaturesHeight += Dimens.readingContainerSingleValueHeight;
if (context.meteringFeature(MeteringScreenLayoutFeature.equipmentProfiles)) { enabledFeaturesHeight += Dimens.paddingS;
enabledFeaturesHeight += Dimens.readingContainerSingleValueHeight; }
enabledFeaturesHeight += Dimens.paddingS; if (context.meteringFeature(MeteringScreenLayoutFeature.filmPicker)) {
} enabledFeaturesHeight += Dimens.readingContainerSingleValueHeight;
if (context.meteringFeature(MeteringScreenLayoutFeature.filmPicker)) { enabledFeaturesHeight += Dimens.paddingS;
enabledFeaturesHeight += Dimens.readingContainerSingleValueHeight;
enabledFeaturesHeight += Dimens.paddingS;
}
} }
if (context.meteringFeature(MeteringScreenLayoutFeature.extremeExposurePairs)) { if (context.meteringFeature(MeteringScreenLayoutFeature.extremeExposurePairs)) {
enabledFeaturesHeight += Dimens.readingContainerDoubleValueHeight; enabledFeaturesHeight += Dimens.readingContainerDoubleValueHeight;

View file

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

View file

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

View file

@ -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/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/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/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/metering/components/shared/readings_container/components/nd_picker/widget_picker_nd.dart';
import 'package:lightmeter/utils/context_utils.dart'; import 'package:lightmeter/utils/context_utils.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
@ -38,10 +38,10 @@ class ReadingsContainer extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
if (!context.isPro && RemoteConfig.isEnabled(context, Feature.showUnlockProOnMainScreen)) ...[ if (!context.isPro && RemoteConfig.isEnabled(context, Feature.showUnlockProOnMainScreen)) ...[
const LightmeterProAnimatedDialog(), const LightmeterProBadge(),
const _InnerPadding(), const _InnerPadding(),
], ],
if (context.isPro && context.meteringFeature(MeteringScreenLayoutFeature.equipmentProfiles)) ...[ if (context.meteringFeature(MeteringScreenLayoutFeature.equipmentProfiles)) ...[
const EquipmentProfilePicker(), const EquipmentProfilePicker(),
const _InnerPadding(), const _InnerPadding(),
], ],
@ -52,7 +52,7 @@ class ReadingsContainer extends StatelessWidget {
), ),
const _InnerPadding(), const _InnerPadding(),
], ],
if (context.isPro && context.meteringFeature(MeteringScreenLayoutFeature.filmPicker)) ...[ if (context.meteringFeature(MeteringScreenLayoutFeature.filmPicker)) ...[
FilmPicker(selectedIso: iso), FilmPicker(selectedIso: iso),
const _InnerPadding(), const _InnerPadding(),
], ],

View file

@ -1,10 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/screens/settings/components/about/components/report_issue/widget_list_tile_report_issue.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/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/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/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/screens/settings/components/shared/settings_section/widget_settings_section.dart';
import 'package:lightmeter/utils/context_utils.dart';
class AboutSettingsSection extends StatelessWidget { class AboutSettingsSection extends StatelessWidget {
const AboutSettingsSection({super.key}); const AboutSettingsSection({super.key});
@ -13,11 +15,12 @@ class AboutSettingsSection extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SettingsSection( return SettingsSection(
title: S.of(context).about, title: S.of(context).about,
children: const [ children: [
SourceCodeListTile(), const SourceCodeListTile(),
ReportIssueListTile(), const ReportIssueListTile(),
WriteEmailListTile(), const WriteEmailListTile(),
VersionListTile(), const VersionListTile(),
if (context.isPro) const RestorePurchasesListTile(),
], ],
); );
} }

View file

@ -4,39 +4,53 @@ import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/providers/services_provider.dart'; import 'package:lightmeter/providers/services_provider.dart';
import 'package:lightmeter/providers/user_preferences_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/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 { class CameraFeaturesListTile extends StatelessWidget {
const CameraFeaturesListTile({super.key}); const CameraFeaturesListTile({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return IAPListTile( return ListTile(
leading: const Icon(Icons.camera_alt_outlined), leading: const Icon(Icons.camera_alt_outlined),
title: Text(S.of(context).cameraFeatures), title: Text(S.of(context).cameraFeatures),
onTap: () { 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( showDialog(
context: context, context: context,
builder: (_) => DialogSwitch<CameraFeature>( builder: (_) => DialogSwitch<CameraFeature>(
icon: Icons.camera_alt_outlined, icon: Icons.camera_alt_outlined,
title: S.of(context).cameraFeatures, title: S.of(context).cameraFeatures,
values: UserPreferencesProvider.cameraConfigOf(context), items: [
enabledAdapter: (feature) => switch (feature) { DialogSwitchListItem(
CameraFeature.spotMetering => true, value: CameraFeature.showFocalLength,
CameraFeature.histogram => true, title: S.of(context).cameraFeaturesShowFocalLength,
CameraFeature.showFocalLength => subtitle: S.of(context).cameraFeaturesShowFocalLengthHint,
ServicesProvider.of(context).userPreferencesService.cameraFocalLength != null, initialValue: UserPreferencesProvider.cameraFeatureOf(context, CameraFeature.showFocalLength),
}, isEnabled: ServicesProvider.of(context).userPreferencesService.cameraFocalLength != null,
titleAdapter: (context, feature) => switch (feature) { ),
CameraFeature.spotMetering => S.of(context).cameraFeatureSpotMetering, DialogSwitchListItem(
CameraFeature.histogram => S.of(context).cameraFeatureHistogram, value: CameraFeature.spotMetering,
CameraFeature.showFocalLength => S.of(context).cameraFeaturesShowFocalLength, title: S.of(context).cameraFeatureSpotMetering,
}, subtitle: S.of(context).cameraFeatureSpotMeteringHint,
subtitleAdapter: (context, feature) => switch (feature) { initialValue: UserPreferencesProvider.cameraFeatureOf(context, CameraFeature.spotMetering),
CameraFeature.spotMetering => S.of(context).cameraFeatureSpotMeteringHint, isProRequired: true,
CameraFeature.histogram => S.of(context).cameraFeatureHistogramHint, ),
CameraFeature.showFocalLength => S.of(context).cameraFeaturesShowFocalLengthHint, 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, onSave: UserPreferencesProvider.of(context).setCameraFeature,
), ),
); );

View file

@ -1,14 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/navigation/routes.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 { class LogbookListTile extends StatelessWidget {
const LogbookListTile({super.key}); const LogbookListTile({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return IAPListTile( return ListTile(
leading: const Icon(Icons.book_outlined), leading: const Icon(Icons.book_outlined),
title: Text(S.of(context).logbook), title: Text(S.of(context).logbook),
onTap: () { onTap: () {

View file

@ -3,24 +3,26 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/settings/components/general/components/timer/bloc_list_tile_timer.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/context_utils.dart';
import 'package:lightmeter/utils/guard_pro_tap.dart';
class TimerListTile extends StatelessWidget { class TimerListTile extends StatelessWidget {
const TimerListTile({super.key}); const TimerListTile({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Disable( return BlocBuilder<TimerListTileBloc, bool>(
disable: !context.isPro, builder: (context, state) => SwitchListTile(
child: BlocBuilder<TimerListTileBloc, bool>( secondary: const Icon(Icons.timer_outlined),
builder: (context, state) => SwitchListTile( title: Text(S.of(context).autostartTimer),
secondary: const Icon(Icons.timer_outlined), value: context.isPro && state,
title: Text(S.of(context).autostartTimer), contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
value: state && context.isPro, onChanged: (value) {
onChanged: context.read<TimerListTileBloc>().onChanged, guardProTap(
contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM), context,
), () => context.read<TimerListTileBloc>().onChanged(value),
);
},
), ),
); );
} }

View file

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

View file

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

View file

@ -1,14 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/navigation/routes.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 { class EquipmentProfilesListTile extends StatelessWidget {
const EquipmentProfilesListTile({super.key}); const EquipmentProfilesListTile({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return IAPListTile( return ListTile(
leading: const Icon(Icons.camera_outlined), leading: const Icon(Icons.camera_outlined),
title: Text(S.of(context).equipmentProfiles), title: Text(S.of(context).equipmentProfiles),
onTap: () { onTap: () {

View file

@ -1,17 +1,24 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/navigation/routes.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 { class FilmsListTile extends StatelessWidget {
const FilmsListTile({super.key}); const FilmsListTile({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return IAPListTile( return ListTile(
leading: const Icon(Icons.camera_roll_outlined), leading: const Icon(Icons.camera_roll_outlined),
title: Text(S.of(context).films), title: Text(S.of(context).films),
onTap: () => Navigator.of(context).pushNamed(NavigationRoutes.filmsListScreen.name), onTap: () {
guardProTap(
context,
() {
Navigator.of(context).pushNamed(NavigationRoutes.filmsListScreen.name);
},
);
},
); );
} }
} }

View file

@ -5,7 +5,6 @@ import 'package:lightmeter/providers/equipment_profile_provider.dart';
import 'package:lightmeter/providers/films_provider.dart'; import 'package:lightmeter/providers/films_provider.dart';
import 'package:lightmeter/providers/user_preferences_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/dialog_switch/widget_dialog_switch.dart';
import 'package:lightmeter/utils/context_utils.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class MeteringScreenLayoutListTile extends StatelessWidget { class MeteringScreenLayoutListTile extends StatelessWidget {
@ -23,17 +22,16 @@ class MeteringScreenLayoutListTile extends StatelessWidget {
icon: Icons.layers_outlined, icon: Icons.layers_outlined,
title: S.of(context).meteringScreenLayout, title: S.of(context).meteringScreenLayout,
description: S.of(context).meteringScreenLayoutHint, description: S.of(context).meteringScreenLayoutHint,
values: UserPreferencesProvider.meteringScreenConfigOf(context), items: UserPreferencesProvider.meteringScreenConfigOf(context)
titleAdapter: _toStringLocalized, .entries
enabledAdapter: (value) { .map(
switch (value) { (entry) => DialogSwitchListItem(
case MeteringScreenLayoutFeature.equipmentProfiles: value: entry.key,
case MeteringScreenLayoutFeature.filmPicker: title: _toStringLocalized(context, entry.key),
return context.isPro; initialValue: UserPreferencesProvider.meteringScreenFeatureOf(context, entry.key),
default: ),
return true; )
} .toList(growable: false),
},
onSave: (value) { onSave: (value) {
if (!value[MeteringScreenLayoutFeature.equipmentProfiles]!) { if (!value[MeteringScreenLayoutFeature.equipmentProfiles]!) {
EquipmentProfilesProvider.of(context).selectProfile(EquipmentProfiles.of(context).first); EquipmentProfilesProvider.of(context).selectProfile(EquipmentProfiles.of(context).first);

View file

@ -2,23 +2,18 @@ import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/providers/user_preferences_provider.dart';
import 'package:lightmeter/res/dimens.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 { class ShowEv100ListTile extends StatelessWidget {
const ShowEv100ListTile({super.key}); const ShowEv100ListTile({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Disable( return SwitchListTile(
disable: !context.isPro, secondary: const Icon(Icons.adjust_outlined),
child: SwitchListTile( title: Text(S.of(context).showEv100),
secondary: const Icon(Icons.adjust_outlined), value: UserPreferencesProvider.showEv100Of(context),
title: Text(S.of(context).showEv100), contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
value: context.isPro && UserPreferencesProvider.showEv100Of(context), onChanged: (_) => UserPreferencesProvider.of(context).toggleShowEv100(),
onChanged: (_) => UserPreferencesProvider.of(context).toggleShowEv100(),
contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
),
); );
} }
} }

View file

@ -1,28 +1,41 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/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<T> = String Function(BuildContext context, T value); typedef StringAdapter<T> = String Function(BuildContext context, T value);
class DialogSwitchListItem<T> {
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<T> extends StatefulWidget { class DialogSwitch<T> extends StatefulWidget {
final IconData icon; final IconData icon;
final String title; final String title;
final String? description; final String? description;
final Map<T, bool> values; final List<DialogSwitchListItem<T>> items;
final StringAdapter<T> titleAdapter;
final StringAdapter<T>? subtitleAdapter;
final bool Function(T value)? enabledAdapter;
final ValueChanged<Map<T, bool>> onSave; final ValueChanged<Map<T, bool>> onSave;
const DialogSwitch({ const DialogSwitch({
required this.icon, required this.icon,
required this.title, required this.title,
this.description, this.description,
required this.values, required this.items,
required this.titleAdapter,
this.subtitleAdapter,
this.enabledAdapter,
required this.onSave, required this.onSave,
super.key, super.key,
}); });
@ -32,7 +45,11 @@ class DialogSwitch<T> extends StatefulWidget {
} }
class _DialogSwitchState<T> extends State<DialogSwitch<T>> { class _DialogSwitchState<T> extends State<DialogSwitch<T>> {
late final Map<T, bool> _features = Map.from(widget.values); late final Map<T, bool> _features = Map.fromEntries(
widget.items.map(
(item) => MapEntry(item.value, item.initialValue),
),
);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -55,27 +72,15 @@ class _DialogSwitchState<T> extends State<DialogSwitch<T>> {
], ],
ListView( ListView(
shrinkWrap: true, shrinkWrap: true,
children: _features.entries.map( children: widget.items.map(
(entry) { (item) {
final isEnabled = widget.enabledAdapter?.call(entry.key) ?? true; final value = _features[item.value]!;
return Disable( return SwitchListTile(
disable: !isEnabled, contentPadding: EdgeInsets.symmetric(horizontal: Dimens.dialogTitlePadding.left),
child: SwitchListTile( title: Text(item.title),
contentPadding: EdgeInsets.symmetric(horizontal: Dimens.dialogTitlePadding.left), subtitle: item.subtitle != null ? Text(item.subtitle!) : null,
title: Text(widget.titleAdapter(context, entry.key)), value: item.isProRequired ? context.isPro && value : value,
subtitle: widget.subtitleAdapter != null onChanged: item.isEnabled ? (value) => _setItem(item, value) : 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);
});
},
),
); );
}, },
).toList(), ).toList(),
@ -99,4 +104,18 @@ class _DialogSwitchState<T> extends State<DialogSwitch<T>> {
], ],
); );
} }
void _setItem(DialogSwitchListItem<T> item, bool value) {
void setItemState() {
setState(() {
_features.update(item.value, (_) => value);
});
}
if (item.isProRequired) {
guardProTap(context, setItemState);
} else {
setItemState();
}
}
} }

View file

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

View file

@ -4,14 +4,10 @@ import 'package:lightmeter/res/dimens.dart';
class SettingsSection extends StatelessWidget { class SettingsSection extends StatelessWidget {
final String title; final String title;
final List<Widget> children; final List<Widget> children;
final Color? backgroundColor;
final Color? foregroundColor;
const SettingsSection({ const SettingsSection({
required this.title, required this.title,
required this.children, required this.children,
this.backgroundColor,
this.foregroundColor,
super.key, super.key,
}); });
@ -25,33 +21,22 @@ class SettingsSection extends StatelessWidget {
Dimens.paddingM, Dimens.paddingM,
), ),
child: Card( child: Card(
color: backgroundColor,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: Dimens.paddingM), padding: const EdgeInsets.symmetric(vertical: Dimens.paddingM),
child: Theme( child: Column(
data: Theme.of(context).copyWith( crossAxisAlignment: CrossAxisAlignment.start,
listTileTheme: Theme.of(context).listTileTheme.copyWith( mainAxisSize: MainAxisSize.min,
iconColor: foregroundColor, children: [
textColor: foregroundColor, Padding(
), padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
), child: Text(
child: Column( title,
crossAxisAlignment: CrossAxisAlignment.start, style:
mainAxisSize: MainAxisSize.min, Theme.of(context).textTheme.labelLarge?.copyWith(color: Theme.of(context).colorScheme.onSurface),
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),
),
), ),
...children, ),
], ...children,
), ],
), ),
), ),
), ),

View file

@ -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/about/widget_settings_section_about.dart';
import 'package:lightmeter/screens/settings/components/camera/widget_settings_section_camera.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/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/metering/widget_settings_section_metering.dart';
import 'package:lightmeter/screens/settings/components/theme/widget_settings_section_theme.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/settings/flow_settings.dart';
import 'package:lightmeter/screens/shared/sliver_screen/screen_sliver.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'; import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
class SettingsScreen extends StatefulWidget { class SettingsScreen extends StatefulWidget {
@ -41,7 +39,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
SliverList( SliverList(
delegate: SliverChildListDelegate( delegate: SliverChildListDelegate(
<Widget>[ <Widget>[
if (!context.isPro) const LightmeterProSettingsSection(),
const MeteringSettingsSection(), const MeteringSettingsSection(),
const CameraSettingsSection(), const CameraSettingsSection(),
const GeneralSettingsSection(), const GeneralSettingsSection(),

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 298 KiB

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 473 KiB

After

Width:  |  Height:  |  Size: 474 KiB