This commit is contained in:
Vadim 2025-08-03 19:19:38 +02:00
parent 6cb7ec7bae
commit 260af158ab
6 changed files with 179 additions and 100 deletions

View file

@ -64,6 +64,16 @@ ThemeData themeFrom(Color primaryColor, Brightness brightness) {
scaffoldBackgroundColor: scheme.surface,
);
return theme.copyWith(
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
minimumSize: const Size.square(Dimens.grid56),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(Dimens.borderRadiusM),
),
backgroundColor: theme.colorScheme.primary,
foregroundColor: theme.colorScheme.onPrimary,
),
),
listTileTheme: ListTileThemeData(
style: ListTileStyle.list,
iconColor: scheme.onSurface,

View file

@ -1,19 +1,19 @@
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/res/theme.dart';
import 'package:lightmeter/screens/shared/button/widget_button_filled_large.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
class LightmeterProBottomControls extends StatefulWidget {
const LightmeterProBottomControls({super.key});
class LightmeterProOffering extends StatefulWidget {
const LightmeterProOffering({super.key});
@override
State<LightmeterProBottomControls> createState() => _LightmeterProBottomControlsState();
State<LightmeterProOffering> createState() => _LightmeterProOfferingState();
}
class _LightmeterProBottomControlsState extends State<LightmeterProBottomControls> {
class _LightmeterProOfferingState extends State<LightmeterProOffering> {
late final Future<List<IAPProduct>> productsFuture;
bool _isLoading = true;
IAPProduct? monthly;
@ -36,10 +36,24 @@ class _LightmeterProBottomControlsState extends State<LightmeterProBottomControl
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (IAPProducts.isPro(context)) {
Navigator.of(context).pop();
}
}
@override
Widget build(BuildContext context) {
return Container(
color: Theme.of(context).colorScheme.surfaceElevated1,
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(Dimens.borderRadiusL),
topRight: Radius.circular(Dimens.borderRadiusL),
),
color: Theme.of(context).colorScheme.surfaceElevated1,
),
width: MediaQuery.sizeOf(context).width,
padding: EdgeInsets.fromLTRB(
Dimens.paddingM,
@ -49,6 +63,7 @@ class _LightmeterProBottomControlsState extends State<LightmeterProBottomControl
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
AnimatedSwitcher(
duration: Dimens.durationM,
@ -65,25 +80,19 @@ class _LightmeterProBottomControlsState extends State<LightmeterProBottomControl
},
),
),
const SizedBox(height: Dimens.grid16),
FilledButton(
const SizedBox(height: Dimens.grid8),
FilledButtonLarge(
title: S.of(context).continuePurchase,
onPressed: selected != null
? () {
IAPProductsProvider.of(context).buyPro(selected!);
}
: null,
child: Text("Continue"),
),
const _RestorePurchasesButton(),
],
),
);
}
@override
void dispose() {
super.dispose();
}
}
class _Products extends StatelessWidget {
@ -102,17 +111,20 @@ class _Products extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
if (monthly case final monthly?)
_OfferingItems(
product: monthly,
_ProductItem(
title: S.of(context).monthly,
price: S.of(context).pricePerMonth(monthly.price),
isSelected: selected == monthly,
onPressed: () => onProductSelected(monthly),
),
const SizedBox(height: Dimens.grid8),
if (lifetime case final lifetime?)
_OfferingItems(
product: lifetime,
_ProductItem(
title: S.of(context).lifetime,
price: lifetime.price,
isSelected: selected == lifetime,
onPressed: () => onProductSelected(lifetime),
),
@ -121,45 +133,66 @@ class _Products extends StatelessWidget {
}
}
class _OfferingItems extends StatelessWidget {
const _OfferingItems({
required this.product,
class _ProductItem extends StatelessWidget {
const _ProductItem({
required this.title,
required this.price,
required this.isSelected,
required this.onPressed,
});
final IAPProduct product;
final String title;
final String price;
final bool isSelected;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return Card(
color: Theme.of(context).colorScheme.primaryContainer,
shape: isSelected
? RoundedRectangleBorder(
borderRadius: BorderRadius.circular(Dimens.borderRadiusL),
side: BorderSide(
return DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(Dimens.borderRadiusM),
/// TODO: fix color alteration. It is a bit darker here for some reason
/// than reading containers
color: isSelected
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.primaryContainer,
border: isSelected
? Border.all(
color: Theme.of(context).colorScheme.primary,
width: 3,
),
)
: null,
width: 2,
)
: null,
),
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: onPressed,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: Dimens.paddingS),
child: ListTile(
title: AnimatedDefaultTextStyle(
duration: Dimens.durationM,
style: Theme.of(context).textTheme.bodyLarge!.boldIfSelected(isSelected),
child: Text(product.type.name),
),
trailing: AnimatedDefaultTextStyle(
duration: Dimens.durationM,
style: Theme.of(context).textTheme.bodyMedium!.boldIfSelected(isSelected),
child: Text(product.price),
),
/// [Radio] has 12pt paddings around the button.
/// [Dimens.paddingM] - 12pt = 4pt
padding: const EdgeInsets.fromLTRB(
Dimens.grid4,
Dimens.grid4,
Dimens.paddingM,
Dimens.grid4,
),
child: Row(
children: [
Radio(
value: isSelected,
groupValue: true,
onChanged: (_) => onPressed(),
),
_ProductAnimatedText(
title,
isSelected: isSelected,
),
const Spacer(),
_ProductAnimatedText(
price,
isSelected: isSelected,
),
],
),
),
),
@ -167,18 +200,25 @@ class _OfferingItems extends StatelessWidget {
}
}
class _RestorePurchasesButton extends StatelessWidget {
const _RestorePurchasesButton();
class _ProductAnimatedText extends StatelessWidget {
const _ProductAnimatedText(this.text, {required this.isSelected});
final String text;
final bool isSelected;
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: () {},
child: Text(S.of(context).restorePurchases),
return AnimatedDefaultTextStyle(
duration: Dimens.durationM,
style: Theme.of(context).textTheme.titleMedium!.copyWith(
fontWeight: FontWeight.w500,
color:
isSelected ? Theme.of(context).colorScheme.onPrimaryContainer : Theme.of(context).colorScheme.onSurface,
),
child: Text(text),
);
}
}
extension on TextStyle {
TextStyle boldIfSelected(bool isSelected) => copyWith(fontWeight: isSelected ? FontWeight.w500 : FontWeight.normal);
}
/// rgba(212,227,252,255) #d4e3fc
///

View file

@ -6,6 +6,7 @@ import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/lightmeter_pro/components/offering/widget_offering_lightmeter_pro.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 =
@ -15,55 +16,56 @@ class LightmeterProScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(
child: SliverScreen(
title: Text(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),
),
),
],
return SliverScreen(
title: Text(S.of(context).proFeaturesTitle),
appBarActions: [
IconButton(
onPressed: IAPProductsProvider.of(context).restorePurchases,
icon: const Icon(Icons.restore),
tooltip: S.of(context).restorePurchases,
),
],
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),
),
),
const LightmeterProBottomControls(),
],
bottomNavigationBar: const LightmeterProOffering(),
);
}
}

View file

@ -128,7 +128,7 @@ class AnimatedDialogState extends State<AnimatedDialog> with SingleTickerProvide
@override
Widget build(BuildContext context) {
return InkWell(
return GestureDetector(
key: _key,
onTap: _openDialog,
child: Opacity(

View file

@ -0,0 +1,24 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
class FilledButtonLarge extends StatelessWidget {
const FilledButtonLarge({
required this.title,
required this.onPressed,
});
final String title;
final VoidCallback? onPressed;
@override
Widget build(BuildContext context) {
return FilledButton(
style: Theme.of(context)
.filledButtonTheme
.style!
.copyWith(textStyle: WidgetStatePropertyAll(Theme.of(context).textTheme.titleMedium)),
onPressed: onPressed,
child: Text(S.of(context).continuePurchase),
);
}
}

View file

@ -8,12 +8,14 @@ class SliverScreen extends StatelessWidget {
final List<Widget> appBarActions;
final PreferredSizeWidget? bottom;
final List<Widget> slivers;
final Widget? bottomNavigationBar;
const SliverScreen({
this.title,
this.appBarActions = const [],
this.bottom,
required this.slivers,
this.bottomNavigationBar,
super.key,
});
@ -34,6 +36,7 @@ class SliverScreen extends StatelessWidget {
],
),
),
bottomNavigationBar: bottomNavigationBar,
);
}
}