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, scaffoldBackgroundColor: scheme.surface,
); );
return theme.copyWith( 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( listTileTheme: ListTileThemeData(
style: ListTileStyle.list, style: ListTileStyle.list,
iconColor: scheme.onSurface, iconColor: scheme.onSurface,

View file

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

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/lightmeter_pro/components/offering/widget_offering_lightmeter_pro.dart';
import 'package:lightmeter/screens/shared/sliver_screen/screen_sliver.dart'; import 'package:lightmeter/screens/shared/sliver_screen/screen_sliver.dart';
import 'package:lightmeter/utils/text_height.dart'; import 'package:lightmeter/utils/text_height.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
class LightmeterProScreen extends StatelessWidget { class LightmeterProScreen extends StatelessWidget {
final features = final features =
@ -15,11 +16,15 @@ class LightmeterProScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return SliverScreen(
children: [
Expanded(
child: SliverScreen(
title: Text(S.of(context).proFeaturesTitle), title: Text(S.of(context).proFeaturesTitle),
appBarActions: [
IconButton(
onPressed: IAPProductsProvider.of(context).restorePurchases,
icon: const Icon(Icons.restore),
tooltip: S.of(context).restorePurchases,
),
],
slivers: [ slivers: [
SliverToBoxAdapter( SliverToBoxAdapter(
child: Padding( child: Padding(
@ -60,10 +65,7 @@ class LightmeterProScreen extends StatelessWidget {
), ),
), ),
], ],
), bottomNavigationBar: const LightmeterProOffering(),
),
const LightmeterProBottomControls(),
],
); );
} }
} }

View file

@ -128,7 +128,7 @@ class AnimatedDialogState extends State<AnimatedDialog> with SingleTickerProvide
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return InkWell( return GestureDetector(
key: _key, key: _key,
onTap: _openDialog, onTap: _openDialog,
child: Opacity( 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 List<Widget> appBarActions;
final PreferredSizeWidget? bottom; final PreferredSizeWidget? bottom;
final List<Widget> slivers; final List<Widget> slivers;
final Widget? bottomNavigationBar;
const SliverScreen({ const SliverScreen({
this.title, this.title,
this.appBarActions = const [], this.appBarActions = const [],
this.bottom, this.bottom,
required this.slivers, required this.slivers,
this.bottomNavigationBar,
super.key, super.key,
}); });
@ -34,6 +36,7 @@ class SliverScreen extends StatelessWidget {
], ],
), ),
), ),
bottomNavigationBar: bottomNavigationBar,
); );
} }
} }