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/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);

View file

@ -26,7 +26,7 @@ Future<void> runLightmeterApp(Environment env) async {
runApp(
env.buildType == BuildType.dev
? IAPProducts(
isPro: true,
isPro: false,
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/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<EquipmentProfilesScreen> 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),
);
},
);
}

View file

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

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

View file

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

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/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;

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/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(),
],

View file

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

View file

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

View file

@ -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: () {

View file

@ -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<TimerListTileBloc, bool>(
builder: (context, state) => SwitchListTile(
secondary: const Icon(Icons.timer_outlined),
title: Text(S.of(context).autostartTimer),
value: state && context.isPro,
onChanged: context.read<TimerListTileBloc>().onChanged,
contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
),
return BlocBuilder<TimerListTileBloc, bool>(
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<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: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: () {

View file

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

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/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);

View file

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

View file

@ -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<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 {
final IconData icon;
final String title;
final String? description;
final Map<T, bool> values;
final StringAdapter<T> titleAdapter;
final StringAdapter<T>? subtitleAdapter;
final bool Function(T value)? enabledAdapter;
final List<DialogSwitchListItem<T>> items;
final ValueChanged<Map<T, bool>> 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<T> extends StatefulWidget {
}
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
Widget build(BuildContext context) {
@ -55,27 +72,15 @@ class _DialogSwitchState<T> extends State<DialogSwitch<T>> {
],
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<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 {
final String title;
final List<Widget> 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,
],
),
),
),

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/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<SettingsScreen> {
SliverList(
delegate: SliverChildListDelegate(
<Widget>[
if (!context.isPro) const LightmeterProSettingsSection(),
const MeteringSettingsSection(),
const CameraSettingsSection(),
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