mirror of
https://github.com/vodemn/m3_lightmeter.git
synced 2024-11-23 07:50:42 +00:00
Show Lightmeter Pro price before purchase (#183)
* Upgraded `targetSdkVersion` to 34
* added price to `IAPProduct`
* implemented `ProFeaturesScreen` (wip)
* finalized `ProFeaturesScreen` layout
* replaced `ProFeaturesDialog` with `ProFeaturesScreen`
* added translations
* fixed feature checkbox width calculation
* fixed tests
* separated android & ios features
* NPE
* changed "get pro" tile colors
* unified Lightmeter Pro related naming
* typo
* updated golden tests
* use iap 0.11.0
* revert unrelated changes
This reverts commit bae5ead8f0
.
* lint
* adjusted eng translation
* updated goldens
This commit is contained in:
parent
1e2cd8b5d2
commit
f0d707b071
34 changed files with 527 additions and 211 deletions
|
@ -9,15 +9,18 @@ enum IAPProductType { paidFeatures }
|
||||||
class IAPProduct {
|
class IAPProduct {
|
||||||
final String storeId;
|
final String storeId;
|
||||||
final IAPProductStatus status;
|
final IAPProductStatus status;
|
||||||
|
final String price;
|
||||||
|
|
||||||
const IAPProduct({
|
const IAPProduct({
|
||||||
required this.storeId,
|
required this.storeId,
|
||||||
this.status = IAPProductStatus.purchasable,
|
this.status = IAPProductStatus.purchasable,
|
||||||
|
required this.price,
|
||||||
});
|
});
|
||||||
|
|
||||||
IAPProduct copyWith({IAPProductStatus? status}) => IAPProduct(
|
IAPProduct copyWith({IAPProductStatus? status}) => IAPProduct(
|
||||||
storeId: storeId,
|
storeId: storeId,
|
||||||
status: status ?? this.status,
|
status: status ?? this.status,
|
||||||
|
price: price,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ class IAPProductsProviderState extends State<IAPProductsProvider> {
|
||||||
IAPProduct(
|
IAPProduct(
|
||||||
storeId: IAPProductType.paidFeatures.storeId,
|
storeId: IAPProductType.paidFeatures.storeId,
|
||||||
status: IAPProductStatus.purchased,
|
status: IAPProductStatus.purchased,
|
||||||
|
price: '0.0\$',
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
|
|
|
@ -27,6 +27,7 @@ class MockIAPProductsProviderState extends State<MockIAPProductsProvider> {
|
||||||
IAPProduct(
|
IAPProduct(
|
||||||
storeId: IAPProductType.paidFeatures.storeId,
|
storeId: IAPProductType.paidFeatures.storeId,
|
||||||
status: _purchased ? IAPProductStatus.purchased : IAPProductStatus.purchasable,
|
status: _purchased ? IAPProductStatus.purchased : IAPProductStatus.purchasable,
|
||||||
|
price: '0.0\$',
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
|
|
|
@ -5,6 +5,7 @@ import 'package:lightmeter/data/models/supported_locale.dart';
|
||||||
import 'package:lightmeter/generated/l10n.dart';
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
import 'package:lightmeter/platform_config.dart';
|
import 'package:lightmeter/platform_config.dart';
|
||||||
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
||||||
|
import 'package:lightmeter/screens/lightmeter_pro/screen_lightmeter_pro.dart';
|
||||||
import 'package:lightmeter/screens/metering/flow_metering.dart';
|
import 'package:lightmeter/screens/metering/flow_metering.dart';
|
||||||
import 'package:lightmeter/screens/settings/flow_settings.dart';
|
import 'package:lightmeter/screens/settings/flow_settings.dart';
|
||||||
import 'package:lightmeter/screens/shared/release_notes_dialog/flow_dialog_release_notes.dart';
|
import 'package:lightmeter/screens/shared/release_notes_dialog/flow_dialog_release_notes.dart';
|
||||||
|
@ -44,6 +45,7 @@ class Application extends StatelessWidget {
|
||||||
routes: {
|
routes: {
|
||||||
"metering": (_) => const ReleaseNotesFlow(child: MeteringFlow()),
|
"metering": (_) => const ReleaseNotesFlow(child: MeteringFlow()),
|
||||||
"settings": (_) => const SettingsFlow(),
|
"settings": (_) => const SettingsFlow(),
|
||||||
|
"lightmeterPro": (_) => LightmeterProScreen(),
|
||||||
"timer": (context) => TimerFlow(args: ModalRoute.of(context)!.settings.arguments! as TimerFlowArgs),
|
"timer": (context) => TimerFlow(args: ModalRoute.of(context)!.settings.arguments! as TimerFlowArgs),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
56
lib/data/models/app_feature.dart
Normal file
56
lib/data/models/app_feature.dart
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
|
|
||||||
|
enum AppFeature {
|
||||||
|
reflectedLightMetering,
|
||||||
|
incidedntLightMetering,
|
||||||
|
isoAndNdValues,
|
||||||
|
themeEngine,
|
||||||
|
spotMetering,
|
||||||
|
histogram,
|
||||||
|
listOfFilms,
|
||||||
|
equipmentProfiles,
|
||||||
|
timer,
|
||||||
|
mainScreenCustomization;
|
||||||
|
|
||||||
|
static List<AppFeature> get androidFeatures => values;
|
||||||
|
|
||||||
|
static List<AppFeature> get iosFeatures => values.where((f) => f != AppFeature.incidedntLightMetering).toList();
|
||||||
|
|
||||||
|
String name(BuildContext context) {
|
||||||
|
switch (this) {
|
||||||
|
case AppFeature.reflectedLightMetering:
|
||||||
|
return S.of(context).featureReflectedLightMetering;
|
||||||
|
case AppFeature.incidedntLightMetering:
|
||||||
|
return S.of(context).featureIncidentLightMetering;
|
||||||
|
case AppFeature.isoAndNdValues:
|
||||||
|
return S.of(context).featureIsoAndNdValues;
|
||||||
|
case AppFeature.themeEngine:
|
||||||
|
return S.of(context).featureTheme;
|
||||||
|
case AppFeature.spotMetering:
|
||||||
|
return S.of(context).featureSpotMetering;
|
||||||
|
case AppFeature.histogram:
|
||||||
|
return S.of(context).featureHistogram;
|
||||||
|
case AppFeature.listOfFilms:
|
||||||
|
return S.of(context).featureListOfFilms;
|
||||||
|
case AppFeature.equipmentProfiles:
|
||||||
|
return S.of(context).featureEquipmentProfiles;
|
||||||
|
case AppFeature.timer:
|
||||||
|
return S.of(context).featureTimer;
|
||||||
|
case AppFeature.mainScreenCustomization:
|
||||||
|
return S.of(context).featureMeteringScreenLayout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isFree {
|
||||||
|
switch (this) {
|
||||||
|
case AppFeature.reflectedLightMetering:
|
||||||
|
case AppFeature.incidedntLightMetering:
|
||||||
|
case AppFeature.isoAndNdValues:
|
||||||
|
case AppFeature.themeEngine:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -103,10 +103,31 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"proFeatures": "Pro features",
|
"proFeaturesTitle": "Lightmeter Pro",
|
||||||
"unlockProFeatures": "Unlock Pro features",
|
"getPro": "Get Pro",
|
||||||
"unlockProFeaturesDescription": "Unlock professional features:\n \u2022 Equipment profiles containing filters for aperture, shutter speed, and more\n \u2022 List of films with compensation for what's known as reciprocity failure\n \u2022 Spot metering & Histogram\n \u2022 And more!\n\nBy unlocking Pro features you support the development and make it possible to add new features to the app.",
|
"featuresFree": "Free",
|
||||||
"unlock": "Unlock",
|
"featuresPro": "Pro",
|
||||||
|
"proFeaturesPromoText": "Lightmeter Pro delivers everything you need to get the best shots!",
|
||||||
|
"proFeaturesWhatsIncluded": "What's included?",
|
||||||
|
"featureReflectedLightMetering": "Reflected light metering",
|
||||||
|
"featureIncidentLightMetering": "Incident light metering",
|
||||||
|
"featureIsoAndNdValues": "Wide range of ISO and ND filters values",
|
||||||
|
"featureTheme": "Theme customization",
|
||||||
|
"featureSpotMetering": "Spot metering",
|
||||||
|
"featureHistogram": "Histogram",
|
||||||
|
"featureListOfFilms": "List of 20+ films with reciprocity formulas",
|
||||||
|
"featureEquipmentProfiles": "Equipment profiles",
|
||||||
|
"featureTimer": "Built-in timer for long exposure",
|
||||||
|
"featureMeteringScreenLayout": "Customizable main screen",
|
||||||
|
"proFeaturesSupportText": "By purchasing Lightmeter Pro you support the development and make it possible to add new features to the app.",
|
||||||
|
"getNowFor": "Get now for {price}",
|
||||||
|
"@getNowFor": {
|
||||||
|
"price": {
|
||||||
|
"version": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"tooltipAdd": "Add",
|
"tooltipAdd": "Add",
|
||||||
"tooltipClose": "Close",
|
"tooltipClose": "Close",
|
||||||
"tooltipExpand": "Expand",
|
"tooltipExpand": "Expand",
|
||||||
|
|
|
@ -103,10 +103,32 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"proFeatures": "Fonctionnalités professionnelles",
|
"proFeaturesTitle": "Lightmeter Pro",
|
||||||
"unlockProFeatures": "Déverrouiller les fonctionnalités professionnelles",
|
"getPro": "Acheter Pro",
|
||||||
"unlockProFeaturesDescription": "Déverrouillez des fonctions professionnelles:\n \u2022 Profils d'équipement contenant des filtres pour l'ouverture, la vitesse d'obturation et plus encore, ainsi qu'une liste de films avec compensation pour ce que l'on appelle l'échec de réciprocité\n \u2022 Mesure spot & Histogramme\n \u2022 Et plus encore!\n\nEn débloquant les fonctionnalités Pro, vous soutenez le développement et permettez d'ajouter de nouvelles fonctionnalités à l'application.",
|
|
||||||
"unlock": "Déverrouiller",
|
"unlock": "Déverrouiller",
|
||||||
|
"featuresFree": "Gratuit",
|
||||||
|
"featuresPro": "Pro",
|
||||||
|
"proFeaturesPromoText": "Lightmeter Pro offre tout ce dont vous avez besoin pour obtenir les meilleurs clichés!",
|
||||||
|
"proFeaturesWhatsIncluded": "Qu'est-ce qui est inclus?",
|
||||||
|
"featureReflectedLightMetering": "Mesure de la lumière réfléchie",
|
||||||
|
"featureIncidentLightMetering": "Mesure de la lumière incidente",
|
||||||
|
"featureIsoAndNdValues": "Large gamme de valeurs ISO et de filtres ND",
|
||||||
|
"featureTheme": "Personnalisation du thème",
|
||||||
|
"featureSpotMetering": "Mesure spot",
|
||||||
|
"featureHistogram": "Histogramme",
|
||||||
|
"featureListOfFilms": "Liste de plus de 20 films avec des formules de correction",
|
||||||
|
"featureEquipmentProfiles": "Profils de l'équipement",
|
||||||
|
"featureTimer": "Minuteur intégré pour longues expositions",
|
||||||
|
"featureMeteringScreenLayout": "Écran principal personnalisable",
|
||||||
|
"proFeaturesSupportText": "En achetant Lightmeter Pro, vous soutenez le développement et permettez l'ajout de nouvelles fonctionnalités à l'application.",
|
||||||
|
"getNowFor": "Acheter maintenant {price}",
|
||||||
|
"@getNowFor": {
|
||||||
|
"price": {
|
||||||
|
"version": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"tooltipAdd": "Ajouter",
|
"tooltipAdd": "Ajouter",
|
||||||
"tooltipClose": "Fermer",
|
"tooltipClose": "Fermer",
|
||||||
"tooltipExpand": "Élargir",
|
"tooltipExpand": "Élargir",
|
||||||
|
|
|
@ -67,8 +67,8 @@
|
||||||
"equipmentProfile": "Оборудование",
|
"equipmentProfile": "Оборудование",
|
||||||
"equipmentProfiles": "Профили оборудования",
|
"equipmentProfiles": "Профили оборудования",
|
||||||
"tapToAdd": "Нажмите, чтобы добавить",
|
"tapToAdd": "Нажмите, чтобы добавить",
|
||||||
"filmsInUse": "Используемые пленки",
|
"filmsInUse": "Используемые плёнки",
|
||||||
"filmsInUseDescription": "Выберите пленки, которыми вы пользуетесь.",
|
"filmsInUseDescription": "Выберите плёнки, которыми вы пользуетесь.",
|
||||||
"general": "Общие",
|
"general": "Общие",
|
||||||
"keepScreenOn": "Запрет блокировки",
|
"keepScreenOn": "Запрет блокировки",
|
||||||
"haptics": "Вибрация",
|
"haptics": "Вибрация",
|
||||||
|
@ -103,10 +103,31 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"proFeatures": "Профессиональные настройки",
|
"proFeaturesTitle": "Lightmeter Pro",
|
||||||
"unlockProFeatures": "Разблокировать профессиональные настройки",
|
"getPro": "Купить Pro",
|
||||||
"unlockProFeaturesDescription": "Вы можете разблокировать профессиональные настройки:\n \u2022 Профили оборудования, содержащие фильтры для диафрагмы, выдержки и других значений\n \u2022 Список пленок с компенсацией эффекта Шварцшильда\n \u2022 Точечный замер и гистограмма\n \u2022 И другие возможности!\n\nПолучая доступ к профессиональным настройкам, вы поддерживаете разработку и делаете возможным появление новых функций в приложении.",
|
"featuresFree": "Бесплатно",
|
||||||
"unlock": "Разблокировать",
|
"featuresPro": "Pro",
|
||||||
|
"proFeaturesPromoText": "Lightmeter Pro предоставляет все необходимое для получения лучших снимков!",
|
||||||
|
"proFeaturesWhatsIncluded": "Что включено?",
|
||||||
|
"featureReflectedLightMetering": "Замер отраженного света",
|
||||||
|
"featureIncidentLightMetering": "Замер падающего света",
|
||||||
|
"featureIsoAndNdValues": "Широкий диапазон значений ISO и фильтров ND",
|
||||||
|
"featureTheme": "Настройка темы",
|
||||||
|
"featureSpotMetering": "Точечный замер",
|
||||||
|
"featureHistogram": "Гистограмма",
|
||||||
|
"featureListOfFilms": "Список из 20+ плёнок с формулами коррекции",
|
||||||
|
"featureEquipmentProfiles": "Профили оборудования",
|
||||||
|
"featureTimer": "Встроенный таймер для длинных выдержек",
|
||||||
|
"featureMeteringScreenLayout": "Настраиваемый главный экран",
|
||||||
|
"proFeaturesSupportText": "Покупая Lightmeter Pro, вы поддерживаете разработку и делаете возможным добавление новых функций в приложение.",
|
||||||
|
"getNowFor": "Купить за {price}",
|
||||||
|
"@getNowFor": {
|
||||||
|
"price": {
|
||||||
|
"version": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"tooltipAdd": "Добавить",
|
"tooltipAdd": "Добавить",
|
||||||
"tooltipClose": "Закрыть",
|
"tooltipClose": "Закрыть",
|
||||||
"tooltipExpand": "Развернуть",
|
"tooltipExpand": "Развернуть",
|
||||||
|
|
|
@ -103,10 +103,30 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"proFeatures": "专业功能",
|
"getPro": "购买专业版",
|
||||||
"unlockProFeatures": "解锁专业功能",
|
"featuresFree": "免费",
|
||||||
"unlockProFeaturesDescription": "\n \u2022 配置文件,其中包含光圈、快门速度等参数\n \u2022 胶卷列表,对胶片倒易率失效进行曝光补偿\n \u2022 点测光和直方图\n \u2022 和更多\n\n通过解锁专业版功能,您可以支持开发工作,帮助为应用程序添加新功能。",
|
"featuresPro": "专业版",
|
||||||
"unlock": "解锁",
|
"proFeaturesPromoText": "Lightmeter Pro 提供您需要的一切,助您拍出最佳照片!",
|
||||||
|
"proFeaturesWhatsIncluded": "包括哪些内容?",
|
||||||
|
"featureReflectedLightMetering": "反射光测光",
|
||||||
|
"featureIncidentLightMetering": "入射光测光",
|
||||||
|
"featureIsoAndNdValues": "广泛的ISO和ND滤镜值范围",
|
||||||
|
"featureTheme": "主题自定义",
|
||||||
|
"featureSpotMetering": "点测光",
|
||||||
|
"featureHistogram": "直方图",
|
||||||
|
"featureListOfFilms": "20多部电影的修正公式列表",
|
||||||
|
"featureEquipmentProfiles": "设备配置文件",
|
||||||
|
"featureTimer": "内置长曝光计时器",
|
||||||
|
"featureMeteringScreenLayout": "可自定义的主屏幕",
|
||||||
|
"proFeaturesSupportText": "通过购买Lightmeter Pro,您支持开发工作,并使添加新功能成为可能。",
|
||||||
|
"getNowFor": "立即获取 {price}",
|
||||||
|
"@getNowFor": {
|
||||||
|
"price": {
|
||||||
|
"version": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"tooltipAdd": "添加",
|
"tooltipAdd": "添加",
|
||||||
"tooltipClose": "关闭",
|
"tooltipClose": "关闭",
|
||||||
"tooltipExpand": "展开",
|
"tooltipExpand": "展开",
|
||||||
|
|
|
@ -25,7 +25,12 @@ Future<void> runLightmeterApp(Environment env) async {
|
||||||
runApp(
|
runApp(
|
||||||
env.buildType == BuildType.dev
|
env.buildType == BuildType.dev
|
||||||
? IAPProducts(
|
? IAPProducts(
|
||||||
products: [IAPProduct(storeId: IAPProductType.paidFeatures.storeId)],
|
products: [
|
||||||
|
IAPProduct(
|
||||||
|
storeId: IAPProductType.paidFeatures.storeId,
|
||||||
|
price: '0.0\$',
|
||||||
|
),
|
||||||
|
],
|
||||||
child: application,
|
child: application,
|
||||||
)
|
)
|
||||||
: IAPProductsProvider(
|
: IAPProductsProvider(
|
||||||
|
|
229
lib/screens/lightmeter_pro/screen_lightmeter_pro.dart
Normal file
229
lib/screens/lightmeter_pro/screen_lightmeter_pro.dart
Normal file
|
@ -0,0 +1,229 @@
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:lightmeter/data/models/app_feature.dart';
|
||||||
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
|
import 'package:lightmeter/providers/services_provider.dart';
|
||||||
|
import 'package:lightmeter/res/dimens.dart';
|
||||||
|
import 'package:lightmeter/res/theme.dart';
|
||||||
|
import 'package:lightmeter/screens/shared/sliver_screen/screen_sliver.dart';
|
||||||
|
import 'package:lightmeter/utils/text_height.dart';
|
||||||
|
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
||||||
|
|
||||||
|
class LightmeterProScreen extends StatelessWidget {
|
||||||
|
final features =
|
||||||
|
defaultTargetPlatform == TargetPlatform.android ? AppFeature.androidFeatures : AppFeature.iosFeatures;
|
||||||
|
|
||||||
|
LightmeterProScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: SliverScreen(
|
||||||
|
title: S.of(context).proFeaturesTitle,
|
||||||
|
slivers: [
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(Dimens.paddingM),
|
||||||
|
child: Text(
|
||||||
|
S.of(context).proFeaturesPromoText,
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(
|
||||||
|
Dimens.paddingM,
|
||||||
|
0,
|
||||||
|
Dimens.paddingM,
|
||||||
|
Dimens.paddingS,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
S.of(context).proFeaturesWhatsIncluded,
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SliverToBoxAdapter(child: _FeaturesHeader()),
|
||||||
|
SliverList.separated(
|
||||||
|
itemCount: features.length,
|
||||||
|
itemBuilder: (_, index) => _FeatureItem(feature: features[index]),
|
||||||
|
separatorBuilder: (_, __) => const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: Dimens.paddingM),
|
||||||
|
child: Divider(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(Dimens.paddingM),
|
||||||
|
child: Text(S.of(context).proFeaturesSupportText),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceElevated1,
|
||||||
|
width: MediaQuery.sizeOf(context).width,
|
||||||
|
padding: EdgeInsets.fromLTRB(
|
||||||
|
Dimens.paddingM,
|
||||||
|
Dimens.paddingM,
|
||||||
|
Dimens.paddingM,
|
||||||
|
Dimens.paddingM + MediaQuery.paddingOf(context).bottom,
|
||||||
|
),
|
||||||
|
child: FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
ServicesProvider.maybeOf(context)
|
||||||
|
?.analytics
|
||||||
|
.setCustomKey('iap_product_type', IAPProductType.paidFeatures.storeId);
|
||||||
|
IAPProductsProvider.maybeOf(context)?.buy(IAPProductType.paidFeatures);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: Text(S.of(context).getNowFor(IAPProducts.productOf(context, IAPProductType.paidFeatures)!.price)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FeaturesHeader extends StatelessWidget {
|
||||||
|
const _FeaturesHeader();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Spacer(),
|
||||||
|
_FeatureHighlight(child: Text(S.of(context).featuresFree)),
|
||||||
|
_FeatureHighlight(
|
||||||
|
roundedTop: true,
|
||||||
|
highlight: true,
|
||||||
|
child: Text(
|
||||||
|
S.of(context).featuresPro,
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyMedium!
|
||||||
|
.copyWith(color: Theme.of(context).colorScheme.onSecondaryContainer),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FeatureItem extends StatelessWidget {
|
||||||
|
final AppFeature feature;
|
||||||
|
|
||||||
|
const _FeatureItem({
|
||||||
|
required this.feature,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(minHeight: Dimens.grid48),
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: Dimens.paddingM,
|
||||||
|
vertical: Dimens.paddingS,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
feature.name(context),
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Opacity(
|
||||||
|
opacity: feature.isFree ? 1 : 0,
|
||||||
|
child: const _FeatureHighlight(
|
||||||
|
child: _CheckBox(highlight: false),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_FeatureHighlight(
|
||||||
|
highlight: true,
|
||||||
|
roundedBottom: feature == AppFeature.values.last,
|
||||||
|
child: const _CheckBox(highlight: true),
|
||||||
|
),
|
||||||
|
const SizedBox(width: Dimens.grid16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FeatureHighlight extends StatelessWidget {
|
||||||
|
final bool highlight;
|
||||||
|
final bool roundedTop;
|
||||||
|
final bool roundedBottom;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const _FeatureHighlight({
|
||||||
|
this.highlight = false,
|
||||||
|
this.roundedTop = false,
|
||||||
|
this.roundedBottom = false,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minWidth: textSize(
|
||||||
|
highlight ? S.of(context).featuresPro : S.of(context).featuresFree,
|
||||||
|
Theme.of(context).textTheme.bodyMedium,
|
||||||
|
MediaQuery.sizeOf(context).width,
|
||||||
|
).width +
|
||||||
|
Dimens.paddingM * 2,
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: Dimens.paddingM,
|
||||||
|
vertical: Dimens.paddingS,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: highlight ? Theme.of(context).colorScheme.secondaryContainer : null,
|
||||||
|
borderRadius: roundedTop
|
||||||
|
? const BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(Dimens.borderRadiusM),
|
||||||
|
topRight: Radius.circular(Dimens.borderRadiusM),
|
||||||
|
)
|
||||||
|
: roundedBottom
|
||||||
|
? const BorderRadius.only(
|
||||||
|
bottomLeft: Radius.circular(Dimens.borderRadiusM),
|
||||||
|
bottomRight: Radius.circular(Dimens.borderRadiusM),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CheckBox extends StatelessWidget {
|
||||||
|
final bool highlight;
|
||||||
|
|
||||||
|
const _CheckBox({required this.highlight});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Icon(
|
||||||
|
Icons.check_outlined,
|
||||||
|
color: highlight ? Theme.of(context).colorScheme.onSecondaryContainer : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,27 +1,26 @@
|
||||||
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/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart';
|
|
||||||
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/reading_value_container/widget_container_reading_value.dart';
|
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/reading_value_container/widget_container_reading_value.dart';
|
||||||
import 'package:lightmeter/screens/shared/pro_features_dialog/widget_dialog_pro_features.dart';
|
|
||||||
|
|
||||||
class LightmeterProAnimatedDialog extends StatelessWidget {
|
class LightmeterProAnimatedDialog extends StatelessWidget {
|
||||||
const LightmeterProAnimatedDialog({super.key});
|
const LightmeterProAnimatedDialog({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AnimatedDialog(
|
return GestureDetector(
|
||||||
closedChild: ReadingValueContainer(
|
onTap: () {
|
||||||
color: Theme.of(context).colorScheme.errorContainer,
|
Navigator.of(context).pushNamed("lightmeterPro");
|
||||||
textColor: Theme.of(context).colorScheme.onErrorContainer,
|
},
|
||||||
|
child: ReadingValueContainer(
|
||||||
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
|
textColor: Theme.of(context).colorScheme.onSecondary,
|
||||||
values: [
|
values: [
|
||||||
ReadingValue(
|
ReadingValue(
|
||||||
label: S.of(context).proFeatures,
|
label: S.of(context).proFeaturesTitle,
|
||||||
value: S.of(context).unlock,
|
value: S.of(context).getPro,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
openedChild: const ProFeaturesDialog(),
|
|
||||||
openedSize: Size.fromHeight(const ProFeaturesDialog().height(context)),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ class RestorePurchasesListTile extends StatelessWidget {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: const Icon(Icons.restore_outlined),
|
leading: const Icon(Icons.restore_outlined),
|
||||||
title: Text(S.of(context).restorePurchases),
|
title: Text(S.of(context).restorePurchases),
|
||||||
onTap: IAPProductsProvider.of(context).restorePurchases,
|
onTap: IAPProductsProvider.maybeOf(context)?.restorePurchases,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
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/shared/pro_features_dialog/widget_dialog_pro_features.dart';
|
|
||||||
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
||||||
|
|
||||||
class BuyProListTile extends StatelessWidget {
|
class BuyProListTile extends StatelessWidget {
|
||||||
|
@ -12,14 +11,11 @@ class BuyProListTile extends StatelessWidget {
|
||||||
final status = IAPProducts.productOf(context, IAPProductType.paidFeatures)?.status;
|
final status = IAPProducts.productOf(context, IAPProductType.paidFeatures)?.status;
|
||||||
final isPending = status == IAPProductStatus.purchased || status == null;
|
final isPending = status == IAPProductStatus.purchased || status == null;
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: const Icon(Icons.star_outlined),
|
leading: const Icon(Icons.bolt),
|
||||||
title: Text(S.of(context).unlockProFeatures),
|
title: Text(S.of(context).getPro),
|
||||||
onTap: !isPending
|
onTap: !isPending
|
||||||
? () {
|
? () {
|
||||||
showDialog(
|
Navigator.of(context).pushNamed("lightmeterPro");
|
||||||
context: context,
|
|
||||||
builder: (_) => const Dialog(child: ProFeaturesDialog()),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
trailing: isPending
|
trailing: isPending
|
||||||
|
|
|
@ -9,7 +9,9 @@ class LightmeterProSettingsSection extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SettingsSection(
|
return SettingsSection(
|
||||||
title: S.of(context).proFeatures,
|
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||||
|
foregroundColor: Theme.of(context).colorScheme.onSecondary,
|
||||||
|
title: S.of(context).proFeaturesTitle,
|
||||||
children: const [BuyProListTile()],
|
children: const [BuyProListTile()],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,14 @@ 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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -21,8 +25,16 @@ 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(
|
||||||
|
data: Theme.of(context).copyWith(
|
||||||
|
listTileTheme: Theme.of(context).listTileTheme.copyWith(
|
||||||
|
iconColor: foregroundColor,
|
||||||
|
textColor: foregroundColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
@ -34,7 +46,7 @@ class SettingsSection extends StatelessWidget {
|
||||||
style: Theme.of(context)
|
style: Theme.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
.labelLarge
|
.labelLarge
|
||||||
?.copyWith(color: Theme.of(context).colorScheme.onSurface),
|
?.copyWith(color: foregroundColor ?? Theme.of(context).colorScheme.onSurface),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
...children,
|
...children,
|
||||||
|
@ -42,6 +54,7 @@ class SettingsSection extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:lightmeter/generated/l10n.dart';
|
|
||||||
import 'package:lightmeter/providers/services_provider.dart';
|
|
||||||
import 'package:lightmeter/res/dimens.dart';
|
|
||||||
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart';
|
|
||||||
import 'package:lightmeter/screens/shared/transparent_dialog/widget_dialog_transparent.dart';
|
|
||||||
import 'package:lightmeter/utils/text_height.dart';
|
|
||||||
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
|
||||||
|
|
||||||
class ProFeaturesDialog extends StatelessWidget {
|
|
||||||
const ProFeaturesDialog({super.key});
|
|
||||||
|
|
||||||
double height(BuildContext context) => TransparentDialog.height(
|
|
||||||
context,
|
|
||||||
title: S.of(context).proFeatures,
|
|
||||||
contextHeight: dialogTextHeight(
|
|
||||||
context,
|
|
||||||
S.of(context).unlockProFeaturesDescription,
|
|
||||||
Theme.of(context).textTheme.bodyMedium,
|
|
||||||
Dimens.paddingL * 2,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return TransparentDialog(
|
|
||||||
icon: Icons.star_outlined,
|
|
||||||
title: S.of(context).proFeatures,
|
|
||||||
scrollableContent: false,
|
|
||||||
content: Flexible(
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingL),
|
|
||||||
child: Text(
|
|
||||||
S.of(context).unlockProFeaturesDescription,
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => _close(context),
|
|
||||||
child: Text(S.of(context).cancel),
|
|
||||||
),
|
|
||||||
FilledButton(
|
|
||||||
onPressed: () {
|
|
||||||
_close(context).then((_) {
|
|
||||||
ServicesProvider.maybeOf(context)
|
|
||||||
?.analytics
|
|
||||||
.setCustomKey('iap_product_type', IAPProductType.paidFeatures.storeId);
|
|
||||||
IAPProductsProvider.maybeOf(context)?.buy(IAPProductType.paidFeatures);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
child: Text(S.of(context).unlock),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _close(BuildContext context) async => AnimatedDialog.maybeClose(context) ?? Navigator.of(context).pop();
|
|
||||||
}
|
|
|
@ -17,6 +17,13 @@ double textHeight(
|
||||||
String text,
|
String text,
|
||||||
TextStyle? style,
|
TextStyle? style,
|
||||||
double maxWidth,
|
double maxWidth,
|
||||||
|
) =>
|
||||||
|
textSize(text, style, maxWidth).height;
|
||||||
|
|
||||||
|
Size textSize(
|
||||||
|
String text,
|
||||||
|
TextStyle? style,
|
||||||
|
double maxWidth,
|
||||||
) {
|
) {
|
||||||
final TextPainter titlePainter = TextPainter(
|
final TextPainter titlePainter = TextPainter(
|
||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
|
@ -25,5 +32,5 @@ double textHeight(
|
||||||
),
|
),
|
||||||
textDirection: TextDirection.ltr,
|
textDirection: TextDirection.ltr,
|
||||||
)..layout(maxWidth: maxWidth);
|
)..layout(maxWidth: maxWidth);
|
||||||
return titlePainter.height;
|
return titlePainter.size;
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ dependencies:
|
||||||
m3_lightmeter_iap:
|
m3_lightmeter_iap:
|
||||||
git:
|
git:
|
||||||
url: "https://github.com/vodemn/m3_lightmeter_iap"
|
url: "https://github.com/vodemn/m3_lightmeter_iap"
|
||||||
ref: v0.10.0
|
ref: v0.11.0
|
||||||
m3_lightmeter_resources:
|
m3_lightmeter_resources:
|
||||||
git:
|
git:
|
||||||
url: "https://github.com/vodemn/m3_lightmeter_resources"
|
url: "https://github.com/vodemn/m3_lightmeter_resources"
|
||||||
|
|
|
@ -91,6 +91,7 @@ class _GoldenTestApplicationMockState extends State<GoldenTestApplicationMock> {
|
||||||
IAPProduct(
|
IAPProduct(
|
||||||
storeId: IAPProductType.paidFeatures.storeId,
|
storeId: IAPProductType.paidFeatures.storeId,
|
||||||
status: widget.productStatus,
|
status: widget.productStatus,
|
||||||
|
price: '0.0\$',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: ApplicationWrapper(
|
child: ApplicationWrapper(
|
||||||
|
|
|
@ -26,6 +26,7 @@ void main() {
|
||||||
IAPProduct(
|
IAPProduct(
|
||||||
storeId: IAPProductType.paidFeatures.storeId,
|
storeId: IAPProductType.paidFeatures.storeId,
|
||||||
status: productStatus,
|
status: productStatus,
|
||||||
|
price: '0.0\$',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: EquipmentProfileProvider(
|
child: EquipmentProfileProvider(
|
||||||
|
|
|
@ -26,6 +26,7 @@ void main() {
|
||||||
IAPProduct(
|
IAPProduct(
|
||||||
storeId: IAPProductType.paidFeatures.storeId,
|
storeId: IAPProductType.paidFeatures.storeId,
|
||||||
status: productStatus,
|
status: productStatus,
|
||||||
|
price: '0.0\$',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: FilmsProvider(
|
child: FilmsProvider(
|
||||||
|
|
BIN
test/screens/lightmeter_pro/goldens/lightmeter_pro_screen.png
Normal file
BIN
test/screens/lightmeter_pro/goldens/lightmeter_pro_screen.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 301 KiB |
|
@ -0,0 +1,49 @@
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:golden_toolkit/golden_toolkit.dart';
|
||||||
|
import 'package:lightmeter/data/models/theme_type.dart';
|
||||||
|
import 'package:lightmeter/screens/lightmeter_pro/screen_lightmeter_pro.dart';
|
||||||
|
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import '../../application_mock.dart';
|
||||||
|
import '../../utils/golden_test_set_theme.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
setUpAll(() {
|
||||||
|
SharedPreferences.setMockInitialValues({});
|
||||||
|
});
|
||||||
|
|
||||||
|
testGoldens(
|
||||||
|
'LightmeterProScreen golden test',
|
||||||
|
(tester) async {
|
||||||
|
final builder = DeviceBuilder();
|
||||||
|
builder.addScenario(
|
||||||
|
name: 'Get Pro',
|
||||||
|
widget: const _MockLightmeterProFlow(),
|
||||||
|
onCreate: (scenarioWidgetKey) async {
|
||||||
|
if (scenarioWidgetKey.toString().contains('Dark')) {
|
||||||
|
await setTheme<LightmeterProScreen>(tester, scenarioWidgetKey, ThemeType.dark);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await tester.pumpDeviceBuilder(builder);
|
||||||
|
await screenMatchesGolden(
|
||||||
|
tester,
|
||||||
|
'lightmeter_pro_screen',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MockLightmeterProFlow extends StatelessWidget {
|
||||||
|
const _MockLightmeterProFlow();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GoldenTestApplicationMock(
|
||||||
|
productStatus: IAPProductStatus.purchasable,
|
||||||
|
child: LightmeterProScreen(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,6 +28,7 @@ void main() {
|
||||||
IAPProduct(
|
IAPProduct(
|
||||||
storeId: IAPProductType.paidFeatures.storeId,
|
storeId: IAPProductType.paidFeatures.storeId,
|
||||||
status: IAPProductStatus.purchased,
|
status: IAPProductStatus.purchased,
|
||||||
|
price: '0.0\$',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: EquipmentProfileProvider(
|
child: EquipmentProfileProvider(
|
||||||
|
|
|
@ -27,6 +27,7 @@ void main() {
|
||||||
IAPProduct(
|
IAPProduct(
|
||||||
storeId: IAPProductType.paidFeatures.storeId,
|
storeId: IAPProductType.paidFeatures.storeId,
|
||||||
status: IAPProductStatus.purchased,
|
status: IAPProductStatus.purchased,
|
||||||
|
price: '0.0\$',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: FilmsProvider(
|
child: FilmsProvider(
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.6 MiB |
|
@ -15,6 +15,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import '../../../integration_test/utils/finder_actions.dart';
|
import '../../../integration_test/utils/finder_actions.dart';
|
||||||
import '../../../integration_test/utils/platform_channel_mock.dart';
|
import '../../../integration_test/utils/platform_channel_mock.dart';
|
||||||
import '../../application_mock.dart';
|
import '../../application_mock.dart';
|
||||||
|
import '../../utils/golden_test_set_theme.dart';
|
||||||
|
|
||||||
class _MeteringScreenConfig {
|
class _MeteringScreenConfig {
|
||||||
final IAPProductStatus iapProductStatus;
|
final IAPProductStatus iapProductStatus;
|
||||||
|
@ -54,16 +55,6 @@ void main() {
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setTheme(WidgetTester tester, Key scenarioWidgetKey, ThemeType themeType) async {
|
|
||||||
final flow = find.descendant(
|
|
||||||
of: find.byKey(scenarioWidgetKey),
|
|
||||||
matching: find.byType(MeteringFlow),
|
|
||||||
);
|
|
||||||
final BuildContext context = tester.element(flow);
|
|
||||||
UserPreferencesProvider.of(context).setThemeType(themeType);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> takePhoto(WidgetTester tester, Key scenarioWidgetKey) async {
|
Future<void> takePhoto(WidgetTester tester, Key scenarioWidgetKey) async {
|
||||||
final button = find.descendant(
|
final button = find.descendant(
|
||||||
of: find.byKey(scenarioWidgetKey),
|
of: find.byKey(scenarioWidgetKey),
|
||||||
|
@ -110,7 +101,7 @@ void main() {
|
||||||
onCreate: (scenarioWidgetKey) async {
|
onCreate: (scenarioWidgetKey) async {
|
||||||
await setEvSource(tester, scenarioWidgetKey, scenario.evSourceType);
|
await setEvSource(tester, scenarioWidgetKey, scenario.evSourceType);
|
||||||
if (scenarioWidgetKey.toString().contains('Dark')) {
|
if (scenarioWidgetKey.toString().contains('Dark')) {
|
||||||
await setTheme(tester, scenarioWidgetKey, ThemeType.dark);
|
await setTheme<MeteringFlow>(tester, scenarioWidgetKey, ThemeType.dark);
|
||||||
}
|
}
|
||||||
if (scenario.evSourceType == EvSourceType.camera) {
|
if (scenario.evSourceType == EvSourceType.camera) {
|
||||||
await takePhoto(tester, scenarioWidgetKey);
|
await takePhoto(tester, scenarioWidgetKey);
|
||||||
|
|
|
@ -28,6 +28,7 @@ void main() {
|
||||||
IAPProduct(
|
IAPProduct(
|
||||||
storeId: IAPProductType.paidFeatures.storeId,
|
storeId: IAPProductType.paidFeatures.storeId,
|
||||||
status: IAPProductStatus.purchased,
|
status: IAPProductStatus.purchased,
|
||||||
|
price: '0.0\$',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: EquipmentProfileProvider(
|
child: EquipmentProfileProvider(
|
||||||
|
|
|
@ -1,60 +0,0 @@
|
||||||
import 'package:flutter_test/flutter_test.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/widget_settings_section_lightmeter_pro.dart';
|
|
||||||
import 'package:lightmeter/screens/shared/transparent_dialog/widget_dialog_transparent.dart';
|
|
||||||
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
|
||||||
|
|
||||||
import '../../../application_mock.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
Future<void> pumpApplication(WidgetTester tester) async {
|
|
||||||
await tester.pumpWidget(
|
|
||||||
IAPProducts(
|
|
||||||
products: [
|
|
||||||
IAPProduct(
|
|
||||||
storeId: IAPProductType.paidFeatures.storeId,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
child: const WidgetTestApplicationMock(
|
|
||||||
child: LightmeterProSettingsSection(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
}
|
|
||||||
|
|
||||||
testWidgets(
|
|
||||||
'`showBuyProDialog` and buy',
|
|
||||||
(tester) async {
|
|
||||||
await pumpApplication(tester);
|
|
||||||
await tester.tap(find.byType(BuyProListTile));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(find.byType(TransparentDialog), findsOneWidget);
|
|
||||||
expect(find.text(S.current.proFeatures), findsNWidgets(2));
|
|
||||||
expect(find.text(S.current.cancel), findsOneWidget);
|
|
||||||
expect(find.text(S.current.unlock), findsOneWidget);
|
|
||||||
|
|
||||||
await tester.tap(find.text(S.current.unlock));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(find.byType(TransparentDialog), findsNothing);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
testWidgets(
|
|
||||||
'`showBuyProDialog` and cancel',
|
|
||||||
(tester) async {
|
|
||||||
await pumpApplication(tester);
|
|
||||||
await tester.tap(find.byType(BuyProListTile));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(find.byType(TransparentDialog), findsOneWidget);
|
|
||||||
expect(find.text(S.current.proFeatures), findsNWidgets(2));
|
|
||||||
expect(find.text(S.current.cancel), findsOneWidget);
|
|
||||||
expect(find.text(S.current.unlock), findsOneWidget);
|
|
||||||
|
|
||||||
await tester.tap(find.text(S.current.cancel));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(find.byType(TransparentDialog), findsNothing);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
Binary file not shown.
Before Width: | Height: | Size: 498 KiB After Width: | Height: | Size: 493 KiB |
|
@ -7,13 +7,13 @@ import 'package:lightmeter/data/models/ev_source_type.dart';
|
||||||
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
|
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
|
||||||
import 'package:lightmeter/data/models/theme_type.dart';
|
import 'package:lightmeter/data/models/theme_type.dart';
|
||||||
import 'package:lightmeter/data/shared_prefs_service.dart';
|
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||||
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
|
||||||
import 'package:lightmeter/screens/settings/flow_settings.dart';
|
import 'package:lightmeter/screens/settings/flow_settings.dart';
|
||||||
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import '../../application_mock.dart';
|
import '../../application_mock.dart';
|
||||||
|
import '../../utils/golden_test_set_theme.dart';
|
||||||
|
|
||||||
class _SettingsScreenConfig {
|
class _SettingsScreenConfig {
|
||||||
final IAPProductStatus iapProductStatus;
|
final IAPProductStatus iapProductStatus;
|
||||||
|
@ -33,16 +33,6 @@ final _testScenarios = [IAPProductStatus.purchased, IAPProductStatus.purchasable
|
||||||
);
|
);
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
Future<void> setTheme(WidgetTester tester, Key scenarioWidgetKey, ThemeType themeType) async {
|
|
||||||
final flow = find.descendant(
|
|
||||||
of: find.byKey(scenarioWidgetKey),
|
|
||||||
matching: find.byType(SettingsFlow),
|
|
||||||
);
|
|
||||||
final BuildContext context = tester.element(flow);
|
|
||||||
UserPreferencesProvider.of(context).setThemeType(themeType);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
}
|
|
||||||
|
|
||||||
setUpAll(() {
|
setUpAll(() {
|
||||||
SharedPreferences.setMockInitialValues({
|
SharedPreferences.setMockInitialValues({
|
||||||
UserPreferencesService.evSourceTypeKey: EvSourceType.camera.index,
|
UserPreferencesService.evSourceTypeKey: EvSourceType.camera.index,
|
||||||
|
@ -73,7 +63,7 @@ void main() {
|
||||||
widget: _MockSettingsFlow(productStatus: scenario.iapProductStatus),
|
widget: _MockSettingsFlow(productStatus: scenario.iapProductStatus),
|
||||||
onCreate: (scenarioWidgetKey) async {
|
onCreate: (scenarioWidgetKey) async {
|
||||||
if (scenarioWidgetKey.toString().contains('Dark')) {
|
if (scenarioWidgetKey.toString().contains('Dark')) {
|
||||||
await setTheme(tester, scenarioWidgetKey, ThemeType.dark);
|
await setTheme<SettingsFlow>(tester, scenarioWidgetKey, ThemeType.dark);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,7 +4,6 @@ import 'package:golden_toolkit/golden_toolkit.dart';
|
||||||
import 'package:lightmeter/data/models/exposure_pair.dart';
|
import 'package:lightmeter/data/models/exposure_pair.dart';
|
||||||
import 'package:lightmeter/data/models/theme_type.dart';
|
import 'package:lightmeter/data/models/theme_type.dart';
|
||||||
import 'package:lightmeter/data/shared_prefs_service.dart';
|
import 'package:lightmeter/data/shared_prefs_service.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/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/timer/flow_timer.dart';
|
import 'package:lightmeter/screens/timer/flow_timer.dart';
|
||||||
|
@ -13,6 +12,7 @@ import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import '../../application_mock.dart';
|
import '../../application_mock.dart';
|
||||||
|
import '../../utils/golden_test_set_theme.dart';
|
||||||
|
|
||||||
class _TimerScreenConfig {
|
class _TimerScreenConfig {
|
||||||
final ShutterSpeedValue shutterSpeedValue;
|
final ShutterSpeedValue shutterSpeedValue;
|
||||||
|
@ -52,16 +52,6 @@ final _testScenarios = [
|
||||||
);
|
);
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
Future<void> setTheme(WidgetTester tester, Key scenarioWidgetKey, ThemeType themeType) async {
|
|
||||||
final flow = find.descendant(
|
|
||||||
of: find.byKey(scenarioWidgetKey),
|
|
||||||
matching: find.byType(TimerFlow),
|
|
||||||
);
|
|
||||||
final BuildContext context = tester.element(flow);
|
|
||||||
UserPreferencesProvider.of(context).setThemeType(themeType);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> toggleTimer(WidgetTester tester, Key scenarioWidgetKey) async {
|
Future<void> toggleTimer(WidgetTester tester, Key scenarioWidgetKey) async {
|
||||||
final button = find.descendant(
|
final button = find.descendant(
|
||||||
of: find.byKey(scenarioWidgetKey),
|
of: find.byKey(scenarioWidgetKey),
|
||||||
|
@ -98,7 +88,7 @@ void main() {
|
||||||
widget: _MockTimerFlow(scenario.shutterSpeedValue),
|
widget: _MockTimerFlow(scenario.shutterSpeedValue),
|
||||||
onCreate: (scenarioWidgetKey) async {
|
onCreate: (scenarioWidgetKey) async {
|
||||||
if (scenarioWidgetKey.toString().contains('Dark')) {
|
if (scenarioWidgetKey.toString().contains('Dark')) {
|
||||||
await setTheme(tester, scenarioWidgetKey, ThemeType.dark);
|
await setTheme<TimerFlow>(tester, scenarioWidgetKey, ThemeType.dark);
|
||||||
}
|
}
|
||||||
if (!scenario.isStopped) {
|
if (!scenario.isStopped) {
|
||||||
await toggleTimer(tester, scenarioWidgetKey);
|
await toggleTimer(tester, scenarioWidgetKey);
|
||||||
|
|
14
test/utils/golden_test_set_theme.dart
Normal file
14
test/utils/golden_test_set_theme.dart
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:lightmeter/data/models/theme_type.dart';
|
||||||
|
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
||||||
|
|
||||||
|
Future<void> setTheme<T>(WidgetTester tester, Key scenarioWidgetKey, ThemeType themeType) async {
|
||||||
|
final flow = find.descendant(
|
||||||
|
of: find.byKey(scenarioWidgetKey),
|
||||||
|
matching: find.byType(T),
|
||||||
|
);
|
||||||
|
final BuildContext context = tester.element(flow);
|
||||||
|
UserPreferencesProvider.of(context).setThemeType(themeType);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
}
|
Loading…
Reference in a new issue