mirror of
https://github.com/vodemn/m3_lightmeter.git
synced 2024-11-22 07:20:39 +00:00
Compare commits
2 commits
81ca5549dd
...
613a0f18a6
Author | SHA1 | Date | |
---|---|---|---|
|
613a0f18a6 | ||
|
e1532736c2 |
11 changed files with 79 additions and 273 deletions
|
@ -95,6 +95,6 @@
|
|||
},
|
||||
"buyLightmeterPro": "Buy Lightmeter Pro",
|
||||
"lightmeterPro": "Lightmeter Pro",
|
||||
"lightmeterProDescription": "Unlocks extra features, such as equipment profiles containing aperture, shutter speed, ISO and ND values filters and films.\n\nThe source code of Lightmeter is available on GitHub. You are welcome to compile it yourself. However, if you want to support the development and receive new features and updates, consider purchasing Lightmeter Pro.",
|
||||
"lightmeterProDescription": "Unlocks extra features, such as equipment profiles containing filters for aperture, shutter speed, and more; and a list of films with compensation for what's known as reciprocity failure.\n\nThe source code of Lightmeter is available on GitHub. You are welcome to compile it yourself. However, if you want to support the development and receive new features and updates, consider purchasing Lightmeter Pro.",
|
||||
"buy": "Buy"
|
||||
}
|
|
@ -92,5 +92,9 @@
|
|||
"type": "String"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"buyLightmeterPro": "Acheter Lightmeter Pro",
|
||||
"lightmeterPro": "Lightmeter Pro",
|
||||
"lightmeterProDescription": "Déverrouille des fonctionnalités supplémentaires, telles que des profils d'équipement contenant des filtres pour l'ouverture, la vitesse d'obturation et plus encore, ainsi qu'une liste de films avec une compensation pour ce que l'on appelle l'échec de réciprocité.\n\nLe code source du Lightmeter est disponible sur GitHub. Vous pouvez le compiler vous-même. Cependant, si vous souhaitez soutenir le développement et recevoir de nouvelles fonctionnalités et mises à jour, envisagez d'acheter Lightmeter Pro.",
|
||||
"buy": "Acheter"
|
||||
}
|
|
@ -92,5 +92,9 @@
|
|||
"type": "String"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"buyLightmeterPro": "Купить Lightmeter Pro",
|
||||
"lightmeterPro": "Lightmeter Pro",
|
||||
"lightmeterProDescription": "Даёт доступ к таким функциям как профили оборудования, содержащие фильтры для диафрагмы, выдержки и других значений, а также набору пленок с компенсацией эффекта Шварцшильда.\n\nИсходный код Lightmeter доступен на GitHub. Вы можете собрать его самостоятельно. Однако если вы хотите поддержать разработку и получать новые функции и обновления, то приобретите Lightmeter Pro.",
|
||||
"buy": "Купить"
|
||||
}
|
|
@ -92,5 +92,9 @@
|
|||
"type": "String"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"buyLightmeterPro": "Buy Lightmeter Pro",
|
||||
"lightmeterPro": "Lightmeter Pro",
|
||||
"lightmeterProDescription": "Unlocks extra features, such as equipment profiles containing filters for aperture, shutter speed, and more; and a list of films with compensation for what's known as reciprocity failure.\n\nThe source code of Lightmeter is available on GitHub. You are welcome to compile it yourself. However, if you want to support the development and receive new features and updates, consider purchasing Lightmeter Pro.",
|
||||
"buy": "Buy"
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart';
|
||||
import 'package:lightmeter/screens/settings/components/shared/iap_list_tile/widget_list_tile_iap.dart';
|
||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||
|
||||
class EquipmentProfilesListTile extends StatelessWidget {
|
||||
|
@ -8,7 +9,7 @@ class EquipmentProfilesListTile extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
return IAPListTile(
|
||||
leading: const Icon(Icons.camera),
|
||||
title: Text(S.of(context).equipmentProfiles),
|
||||
onTap: () {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/screens/settings/components/shared/dialog_filter/widget_dialog_filter.dart';
|
||||
import 'package:lightmeter/screens/settings/components/shared/iap_list_tile/widget_list_tile_iap.dart';
|
||||
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||
|
||||
|
@ -9,7 +10,7 @@ class FilmsListTile extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
return IAPListTile(
|
||||
leading: const Icon(Icons.camera_roll),
|
||||
title: Text(S.of(context).filmsInUse),
|
||||
onTap: () {
|
||||
|
|
|
@ -3,7 +3,6 @@ import 'package:lightmeter/generated/l10n.dart';
|
|||
import 'package:lightmeter/screens/settings/components/equipment/components/equipment_profiles/widget_list_tile_equipment_profiles.dart';
|
||||
import 'package:lightmeter/screens/settings/components/equipment/components/films/widget_list_tile_films.dart';
|
||||
import 'package:lightmeter/screens/settings/components/shared/settings_section/widget_settings_section.dart';
|
||||
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
||||
|
||||
class EquipmentSettingsSection extends StatelessWidget {
|
||||
const EquipmentSettingsSection({super.key});
|
||||
|
@ -12,7 +11,6 @@ class EquipmentSettingsSection extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return SettingsSection(
|
||||
title: S.of(context).equipment,
|
||||
enabled: IAPProducts.isPurchased(context, IAPProductType.paidFeatures),
|
||||
children: const [
|
||||
EquipmentProfilesListTile(),
|
||||
FilmsListTile(),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/res/dimens.dart';
|
||||
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
||||
|
||||
class BuyProListTile extends StatelessWidget {
|
||||
const BuyProListTile({super.key});
|
||||
|
@ -28,6 +29,7 @@ class BuyProListTile extends StatelessWidget {
|
|||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
IAPProductsProvider.of(context).buy(IAPProductType.paidFeatures);
|
||||
},
|
||||
child: Text(S.of(context).buy),
|
||||
),
|
||||
|
|
|
@ -1,242 +0,0 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:lightmeter/res/dimens.dart';
|
||||
import 'package:lightmeter/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/widget_list_tiles_equipments.dart';
|
||||
import 'package:lightmeter/screens/settings/components/equipment/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_name_dialog/widget_dialog_equipment_profile_name.dart';
|
||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||
|
||||
class ExpandableSection extends StatefulWidget {
|
||||
final EquipmentProfile data;
|
||||
final ValueChanged<EquipmentProfile> onUpdate;
|
||||
final VoidCallback onDelete;
|
||||
final VoidCallback onExpand;
|
||||
|
||||
const ExpandableSection({
|
||||
required this.data,
|
||||
required this.onUpdate,
|
||||
required this.onDelete,
|
||||
required this.onExpand,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ExpandableSection> createState() => ExpandableSectionState();
|
||||
}
|
||||
|
||||
class ExpandableSectionState extends State<ExpandableSection>
|
||||
with TickerProviderStateMixin {
|
||||
late EquipmentProfile _equipmentData = EquipmentProfile(
|
||||
id: widget.data.id,
|
||||
name: widget.data.name,
|
||||
apertureValues: widget.data.apertureValues,
|
||||
ndValues: widget.data.ndValues,
|
||||
shutterSpeedValues: widget.data.shutterSpeedValues,
|
||||
isoValues: widget.data.isoValues,
|
||||
);
|
||||
|
||||
late final AnimationController _controller = AnimationController(
|
||||
duration: Dimens.durationM,
|
||||
vsync: this,
|
||||
);
|
||||
bool get _expanded => _controller.isCompleted;
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ExpandableSection oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_equipmentData = EquipmentProfile(
|
||||
id: widget.data.id,
|
||||
name: widget.data.name,
|
||||
apertureValues: widget.data.apertureValues,
|
||||
ndValues: widget.data.ndValues,
|
||||
shutterSpeedValues: widget.data.shutterSpeedValues,
|
||||
isoValues: widget.data.isoValues,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: Dimens.paddingM),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
|
||||
title: Row(
|
||||
children: [
|
||||
_AnimatedNameLeading(controller: _controller),
|
||||
const SizedBox(width: Dimens.grid8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
_equipmentData.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_AnimatedArrowButton(
|
||||
controller: _controller,
|
||||
onPressed: () => _expanded ? collapse() : expand(),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: widget.onDelete,
|
||||
icon: const Icon(Icons.delete),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () => _expanded ? _showNameDialog() : expand(),
|
||||
),
|
||||
_AnimatedEquipmentListTiles(
|
||||
controller: _controller,
|
||||
equipmentData: _equipmentData,
|
||||
onApertureValuesSelected: (value) {
|
||||
_equipmentData = _equipmentData.copyWith(apertureValues: value);
|
||||
widget.onUpdate(_equipmentData);
|
||||
},
|
||||
onIsoValuesSelecred: (value) {
|
||||
_equipmentData = _equipmentData.copyWith(isoValues: value);
|
||||
widget.onUpdate(_equipmentData);
|
||||
},
|
||||
onNdValuesSelected: (value) {
|
||||
_equipmentData = _equipmentData.copyWith(ndValues: value);
|
||||
widget.onUpdate(_equipmentData);
|
||||
},
|
||||
onShutterSpeedValuesSelected: (value) {
|
||||
_equipmentData = _equipmentData.copyWith(shutterSpeedValues: value);
|
||||
widget.onUpdate(_equipmentData);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showNameDialog() {
|
||||
showDialog<String>(
|
||||
context: context,
|
||||
builder: (_) => EquipmentProfileNameDialog(initialValue: _equipmentData.name),
|
||||
).then((value) {
|
||||
if (value != null) {
|
||||
_equipmentData = _equipmentData.copyWith(name: value);
|
||||
widget.onUpdate(_equipmentData);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void expand() {
|
||||
widget.onExpand();
|
||||
_controller.forward();
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
Future.delayed(_controller.duration!).then((_) {
|
||||
Scrollable.ensureVisible(
|
||||
context,
|
||||
alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd,
|
||||
duration: _controller.duration!,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void collapse() {
|
||||
_controller.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
class _AnimatedNameLeading extends AnimatedWidget {
|
||||
const _AnimatedNameLeading({required AnimationController controller})
|
||||
: super(listenable: controller);
|
||||
|
||||
Animation<double> get _progress => listenable as Animation<double>;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(right: _progress.value * Dimens.grid8),
|
||||
child: Icon(
|
||||
Icons.edit,
|
||||
size: _progress.value * Dimens.grid24,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AnimatedArrowButton extends AnimatedWidget {
|
||||
final VoidCallback onPressed;
|
||||
|
||||
const _AnimatedArrowButton({
|
||||
required AnimationController controller,
|
||||
required this.onPressed,
|
||||
}) : super(listenable: controller);
|
||||
|
||||
Animation<double> get _progress => listenable as Animation<double>;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton(
|
||||
onPressed: onPressed,
|
||||
icon: Transform.rotate(
|
||||
angle: _progress.value * pi,
|
||||
child: const Icon(Icons.keyboard_arrow_down),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AnimatedEquipmentListTiles extends AnimatedWidget {
|
||||
final EquipmentProfile equipmentData;
|
||||
final ValueChanged<List<ApertureValue>> onApertureValuesSelected;
|
||||
final ValueChanged<List<IsoValue>> onIsoValuesSelecred;
|
||||
final ValueChanged<List<NdValue>> onNdValuesSelected;
|
||||
final ValueChanged<List<ShutterSpeedValue>> onShutterSpeedValuesSelected;
|
||||
|
||||
const _AnimatedEquipmentListTiles({
|
||||
required AnimationController controller,
|
||||
required this.equipmentData,
|
||||
required this.onApertureValuesSelected,
|
||||
required this.onIsoValuesSelecred,
|
||||
required this.onNdValuesSelected,
|
||||
required this.onShutterSpeedValuesSelected,
|
||||
}) : super(listenable: controller);
|
||||
|
||||
Animation<double> get _progress => listenable as Animation<double>;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedOverflowBox(
|
||||
alignment: Alignment.topCenter,
|
||||
size: Size(
|
||||
double.maxFinite,
|
||||
_progress.value * Dimens.grid56 * 4,
|
||||
),
|
||||
child: Opacity(
|
||||
opacity: _progress.value,
|
||||
child: EquipmentListTiles(
|
||||
selectedApertureValues: equipmentData.apertureValues,
|
||||
selectedIsoValues: equipmentData.isoValues,
|
||||
selectedNdValues: equipmentData.ndValues,
|
||||
selectedShutterSpeedValues: equipmentData.shutterSpeedValues,
|
||||
onApertureValuesSelected: onApertureValuesSelected,
|
||||
onIsoValuesSelecred: onIsoValuesSelecred,
|
||||
onNdValuesSelected: onNdValuesSelected,
|
||||
onShutterSpeedValuesSelected: onShutterSpeedValuesSelected,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/res/dimens.dart';
|
||||
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
||||
|
||||
/// Depends on the product status and replaces [onTap] with purchase callback
|
||||
/// if the product is purchasable.
|
||||
class IAPListTile extends StatelessWidget {
|
||||
final IAPProductType product;
|
||||
final Icon leading;
|
||||
final Text title;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const IAPListTile({
|
||||
this.product = IAPProductType.paidFeatures,
|
||||
required this.leading,
|
||||
required this.title,
|
||||
required this.onTap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Opacity(
|
||||
opacity: IAPProducts.isPurchased(context, product)
|
||||
? Dimens.enabledOpacity
|
||||
: Dimens.disabledOpacity,
|
||||
child: ListTile(
|
||||
leading: leading,
|
||||
title: title,
|
||||
onTap: switch (IAPProducts.productOf(context, product)?.status) {
|
||||
IAPProductStatus.purchasable => () => IAPProductsProvider.of(context).buy(product),
|
||||
IAPProductStatus.pending => null,
|
||||
IAPProductStatus.purchased => onTap,
|
||||
null => null,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -4,12 +4,10 @@ import 'package:lightmeter/res/dimens.dart';
|
|||
class SettingsSection extends StatelessWidget {
|
||||
final String title;
|
||||
final List<Widget> children;
|
||||
final bool enabled;
|
||||
|
||||
const SettingsSection({
|
||||
required this.title,
|
||||
required this.children,
|
||||
this.enabled = true,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
@ -25,25 +23,22 @@ class SettingsSection extends StatelessWidget {
|
|||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: Dimens.paddingM),
|
||||
child: Opacity(
|
||||
opacity: enabled ? Dimens.enabledOpacity : Dimens.disabledOpacity,
|
||||
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),
|
||||
),
|
||||
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,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
Loading…
Reference in a new issue