Compare commits

..

15 commits

Author SHA1 Message Date
Vadim
354cc07e6e Update ci.yml 2023-07-30 10:51:05 +02:00
Vadim
5dc2f9d18d Update README.md 2023-07-30 10:20:06 +02:00
Vadim
2e0811a357 Merge branch 'main' of https://github.com/vodemn/m3_lightmeter into feature/iap 2023-07-30 10:15:11 +02:00
Vadim
40c670ad30
Updated README Build section (#94)
* Update README.md

* Set exact Flutter version for workflows

* Added stub `DefaultFirebaseOptions`

* Fixed `rm`

* Removed `rm`

* Update .gitignore

* Added readable name to ci workflow

* Build -> Development

* Update ci.yml
2023-07-25 17:31:01 +02:00
vodemn
119e079554 Version bump 2023-07-24 11:04:23 +00:00
ScaredCube
b02b50bac3
Fixed Chinese translation (#93) 2023-07-24 12:16:35 +02:00
vodemn
dd5f551fd2 Version bump 2023-07-24 07:54:38 +00:00
ScaredCube
bb9b023fa7
Add Chinese language support (#91)
* Add Chinese language support

* Update intl_cn.arb

* Fixed some bugs

* Add Chinese language support

* renamed `cn` to `zh`

---------

Co-authored-by: Vadim <44135514+vodemn@users.noreply.github.com>
Co-authored-by: Vadim <vadim.turko@gmail.com>
2023-07-24 09:35:30 +02:00
Vadim
dbf1f09eb6 Renamed EquipmentProfileData -> EquipmentProfile 2023-07-24 09:08:37 +02:00
Vadim
99eebff9a4 Improved EquipmentProfilesListTile statuses visualization 2023-07-21 11:59:08 +02:00
Vadim
88ec733596 Fixed EquipmentProfileListener 2023-07-19 16:59:17 +02:00
Vadim
154fd9c56d Update README.md 2023-07-19 16:38:20 +02:00
Vadim
f5135d00eb Moved EquipmentProfileProvider to iap repo 2023-07-19 16:30:29 +02:00
Vadim
8595aae00f Renamed EquipmentProfileData ->EquipmentProfile 2023-07-19 16:29:56 +02:00
Vadim
cb675e43e1 added iap repo stub 2023-07-19 10:35:31 +02:00
30 changed files with 554 additions and 196 deletions

View file

@ -68,6 +68,7 @@ jobs:
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
channel: "stable" channel: "stable"
flutter-version: '3.10.0'
- name: Prepare flutter project - name: Prepare flutter project
run: | run: |

View file

@ -70,6 +70,7 @@ jobs:
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
channel: "stable" channel: "stable"
flutter-version: '3.10.0'
- name: Prepare flutter project - name: Prepare flutter project
run: | run: |

View file

@ -12,14 +12,18 @@ on:
branches: ["main"] branches: ["main"]
jobs: jobs:
build: analyze_and_test:
name: Analyze & test
runs-on: macos-11 runs-on: macos-11
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- uses: webfactory/ssh-agent@v0.8.0 - uses: shaunco/ssh-agent@git-repo-mapping
with: with:
ssh-private-key: ${{ secrets.M3_LIGHTMETER_IAP_KEY }} ssh-private-key: |
${{ secrets.M3_LIGHTMETER_IAP_KEY }}
repo-mappings: |
github.com/vodemn/m3_lightmeter_iap
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
@ -28,23 +32,13 @@ jobs:
- uses: subosito/flutter-action@v2 - uses: subosito/flutter-action@v2
with: with:
channel: "stable" channel: "stable"
flutter-version: '3.10.0'
- name: Check flutter version - name: Prepare flutter project
run: flutter --version
- name: Install dependencies
run: flutter pub get
- name: Generate intl
run: flutter pub run intl_utils:generate
- name: Restore firebase_options.dart
env:
FIREBASE_OPTIONS: ${{ secrets.FIREBASE_OPTIONS }}
run: | run: |
FIREBASE_OPTIONS_PATH=$RUNNER_TEMP/firebase_options.dart flutter --version
echo -n "$FIREBASE_OPTIONS" | base64 --decode --output $FIREBASE_OPTIONS_PATH flutter pub get
cp $FIREBASE_OPTIONS_PATH ./lib flutter pub run intl_utils:generate
- name: Analyze project source - name: Analyze project source
run: flutter analyze lib --fatal-infos run: flutter analyze lib --fatal-infos

View file

@ -5,7 +5,7 @@
- [Table of contents](#table-of-contents) - [Table of contents](#table-of-contents)
- [Backstory](#backstory) - [Backstory](#backstory)
- [Screenshots](#screenshots) - [Screenshots](#screenshots)
- [Build](#build) - [Development](#development)
- [Contribution](#contribution) - [Contribution](#contribution)
- [iOS Limitations](#ios-limitations) - [iOS Limitations](#ios-limitations)
@ -27,22 +27,61 @@ Without further delay behold my new Lightmeter app inspired by Material You (a.k
<img src="https://lh3.googleusercontent.com/15g_SPV8knDLFbz1_-wGNJFsJeyVWZ_y--TGHpk75MaaIdMDyTXY2_TL-Aw8bpOhpw" width="18.8%" /> <img src="https://lh3.googleusercontent.com/15g_SPV8knDLFbz1_-wGNJFsJeyVWZ_y--TGHpk75MaaIdMDyTXY2_TL-Aw8bpOhpw" width="18.8%" />
</p> </p>
# Build # Development
As part of this project is private, you will be able to run this app from the _main_dev.dart_ file (i.e. --flavor dev). Also to avoid fatal errors the _main_prod.dart_ file is excluded from analysis. ### 1. Install Flutter
To build this app you need to install Flutter 3.10.0 stable. [How to install](https://docs.flutter.dev/get-started/install).
### 2. (Optional) Install Firebase
Out of the box Firebase Crashlytics won't work. If you want to add Crashlytics to your local build please follow [this guide](https://firebase.google.com/docs/flutter/setup).
### 3. Get packages
As part of the app's functionallity is in the private repo, you have to replace these lines in _pubspec.yaml_:
```yaml
m3_lightmeter_iap:
git:
url: "https://github.com/vodemn/m3_lightmeter_iap"
ref: main
```
with these:
```yaml
m3_lightmeter_iap:
path: iap
```
and run `flutter pub get` from the _iap/_ folder.
Then you can fetch all the neccessary dependencies and generate translation files by running the following commands:
```console
flutter pub get
flutter pub run intl_utils:generate
```
### 4. Build
You can build an apk by running the following command from the root of the repository:
```console
flutter build apk --release --flavor $FLAVOR --dart-define cameraPreviewAspectRatio=2/3 -t lib/main_$FLAVOR.dart
```
Just replace `$FLAVOR` with `dev` or `prod`.
# Contribution # Contribution
To report a bug or suggest a new feature open a new [issue](https://github.com/vodemn/m3_lightmeter/issues). To report a bug or suggest a new feature open a new [issue](https://github.com/vodemn/m3_lightmeter/issues).
In case you want to help develop this project you need to follow this [style guide](doc/style_guide.md). In case you want to help develop this project feel free to open a Pull Request, but you need to follow this [style guide](doc/style_guide.md).
# iOS Limitations # iOS Limitations
A list of features, that Android version of the app has and that iOS does not. A list of features, that Android version of the app has and that iOS does not.
## Incident light metering ## Incident light metering
Apple does not provide API for reading Lux stream form the ambient light sensor. Lux can be calculated based on front camera image stream, but this would be a reflected light. So there is no way incident light metering can be implemented on iOS. Apple does not provide API for reading Lux stream form the ambient light sensor. Lux can be calculated based on front camera image stream, but this would be a reflected light. So there is no way incident light metering can be implemented on iOS.
## Volume buttons action ## Volume buttons action
This can be [implemented](https://stackoverflow.com/questions/70161271/ios-override-hardware-volume-buttons-same-as-zello) but the app will be rejected due to [2.5.9](https://developer.apple.com/app-store/review/guidelines/#software-requirements) This can be [implemented](https://stackoverflow.com/questions/70161271/ios-override-hardware-volume-buttons-same-as-zello) but the app will be rejected due to [2.5.9](https://developer.apple.com/app-store/review/guidelines/#software-requirements)

36
iap/.gitignore vendored Normal file
View file

@ -0,0 +1,36 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
.packages
build/
.fvm/
*.properties
ios/Flutter/
.flutter-plugins
.flutter-plugins-dependencies

10
iap/.metadata Normal file
View file

@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: 9944297138845a94256f1cf37beb88ff9a8e811a
channel: stable
project_type: package

1
iap/LICENSE Normal file
View file

@ -0,0 +1 @@
TODO: Add your license here.

View file

@ -0,0 +1,4 @@
include: package:flutter_lints/flutter.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View file

@ -0,0 +1,30 @@
library m3_lightmeter_iap;
import 'package:flutter/material.dart';
import 'package:m3_lightmeter_iap/src/providers/equipment_profile_provider.dart';
import 'package:m3_lightmeter_iap/src/providers/iap_products_provider.dart';
export 'src/data/models/iap_product.dart';
export 'src/providers/equipment_profile_provider.dart' hide EquipmentProfilesAspect;
export 'src/providers/iap_products_provider.dart';
class IAPProviders extends StatelessWidget {
final Object sharedPreferences;
final Widget child;
const IAPProviders({
required this.sharedPreferences,
required this.child,
super.key,
});
@override
Widget build(BuildContext context) {
return IAPProductsProvider(
child: EquipmentProfileProvider(
child: child,
),
);
}
}

View file

@ -0,0 +1,5 @@
enum IAPProductType { paidFeatures }
class IAPProduct {
IAPProduct();
}

View file

@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class EquipmentProfileProvider extends StatefulWidget {
final Widget child;
const EquipmentProfileProvider({required this.child, super.key});
static EquipmentProfileProviderState of(BuildContext context) {
return context.findAncestorStateOfType<EquipmentProfileProviderState>()!;
}
@override
State<EquipmentProfileProvider> createState() => EquipmentProfileProviderState();
}
class EquipmentProfileProviderState extends State<EquipmentProfileProvider> {
static const EquipmentProfile _defaultProfile = EquipmentProfile(
id: '',
name: '',
apertureValues: ApertureValue.values,
ndValues: NdValue.values,
shutterSpeedValues: ShutterSpeedValue.values,
isoValues: IsoValue.values,
);
@override
Widget build(BuildContext context) {
return EquipmentProfiles(
profiles: const [_defaultProfile],
selected: _defaultProfile,
child: widget.child,
);
}
void setProfile(EquipmentProfile data) {}
void addProfile(String name) {}
void updateProdile(EquipmentProfile data) {}
void deleteProfile(EquipmentProfile data) {}
}
enum EquipmentProfilesAspect { list, selected }
class EquipmentProfiles extends InheritedModel<EquipmentProfilesAspect> {
const EquipmentProfiles({
super.key,
required this.profiles,
required this.selected,
required super.child,
});
final List<EquipmentProfile> profiles;
final EquipmentProfile selected;
static List<EquipmentProfile> of(BuildContext context) {
return InheritedModel.inheritFrom<EquipmentProfiles>(
context,
aspect: EquipmentProfilesAspect.list,
)!
.profiles;
}
static EquipmentProfile selectedOf(BuildContext context) {
return InheritedModel.inheritFrom<EquipmentProfiles>(
context,
aspect: EquipmentProfilesAspect.selected,
)!
.selected;
}
@override
bool updateShouldNotify(EquipmentProfiles oldWidget) => false;
@override
bool updateShouldNotifyDependent(EquipmentProfiles oldWidget, Set<EquipmentProfilesAspect> dependencies) => false;
}

View file

@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
import 'package:m3_lightmeter_iap/src/data/models/iap_product.dart';
class IAPProductsProvider extends StatefulWidget {
final Widget child;
const IAPProductsProvider({required this.child, super.key});
static IAPProductsProviderState of(BuildContext context) {
return context.findAncestorStateOfType<IAPProductsProviderState>()!;
}
@override
State<IAPProductsProvider> createState() => IAPProductsProviderState();
}
class IAPProductsProviderState extends State<IAPProductsProvider> {
@override
Widget build(BuildContext context) {
return IAPProducts(
products: const [],
child: widget.child,
);
}
Future<void> buy(IAPProductType type) async {}
}
class IAPProducts extends InheritedModel<IAPProductType> {
final List<IAPProduct> products;
const IAPProducts({
required this.products,
required super.child,
super.key,
});
static IAPProduct? of(BuildContext context, IAPProductType type) => null;
static bool isPurchased(BuildContext context, IAPProductType type) => false;
@override
bool updateShouldNotify(IAPProducts oldWidget) => false;
@override
bool updateShouldNotifyDependent(covariant IAPProducts oldWidget, Set<IAPProductType> dependencies) => false;
}

25
iap/pubspec.yaml Normal file
View file

@ -0,0 +1,25 @@
name: m3_lightmeter_iap
description: IAP stubs for the M3 Lightmeter app.
version: 0.2.0
publish_to: 'none'
environment:
sdk: '>=2.19.2 <3.0.0'
flutter: ">=1.17.0"
dependencies:
flutter:
sdk: flutter
m3_lightmeter_resources:
git:
url: "https://github.com/vodemn/m3_lightmeter_resources"
ref: main
shared_preferences: 2.2.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
flutter:
uses-material-design: true

View file

@ -1,4 +1,4 @@
enum SupportedLocale { en, fr, ru } enum SupportedLocale { en, fr, ru, zh }
extension SupportedLocaleExtension on SupportedLocale { extension SupportedLocaleExtension on SupportedLocale {
String get intlName => toString().replaceAll("SupportedLocale.", ""); String get intlName => toString().replaceAll("SupportedLocale.", "");
@ -11,6 +11,8 @@ extension SupportedLocaleExtension on SupportedLocale {
return 'Français'; return 'Français';
case SupportedLocale.ru: case SupportedLocale.ru:
return 'Русский'; return 'Русский';
case SupportedLocale.zh:
return '简体中文';
} }
} }
} }

View file

@ -7,7 +7,6 @@ import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
import 'package:lightmeter/data/models/supported_locale.dart'; import 'package:lightmeter/data/models/supported_locale.dart';
import 'package:lightmeter/data/models/theme_type.dart'; import 'package:lightmeter/data/models/theme_type.dart';
import 'package:lightmeter/data/models/volume_action.dart'; import 'package:lightmeter/data/models/volume_action.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@ -148,14 +147,4 @@ class UserPreferencesService {
orElse: () => Film.values.first, orElse: () => Film.values.first,
); );
set film(Film value) => _sharedPreferences.setString(filmKey, value.name); set film(Film value) => _sharedPreferences.setString(filmKey, value.name);
String get selectedEquipmentProfileId => _sharedPreferences.selectedEquipmentProfileId;
set selectedEquipmentProfileId(String id) {
_sharedPreferences.selectedEquipmentProfileId = id;
}
List<EquipmentProfileData> get equipmentProfiles => _sharedPreferences.equipmentProfiles;
set equipmentProfiles(List<EquipmentProfileData> profiles) {
_sharedPreferences.equipmentProfiles = profiles;
}
} }

68
lib/firebase_options.dart Normal file
View file

@ -0,0 +1,68 @@
// File generated by FlutterFire CLI.
// ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart' show defaultTargetPlatform, kIsWeb, TargetPlatform;
/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options.dart';
/// // ...
/// await Firebase.initializeApp(
/// options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for web - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
case TargetPlatform.iOS:
return ios;
case TargetPlatform.macOS:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for macos - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.windows:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for windows - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.linux:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for linux - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
static const FirebaseOptions android = FirebaseOptions(
apiKey: '',
appId: '',
messagingSenderId: '',
projectId: '',
storageBucket: '',
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: '',
appId: '',
messagingSenderId: '',
projectId: '',
storageBucket: '',
iosClientId: '',
iosBundleId: '',
);
}

88
lib/l10n/intl_zh.arb Normal file
View file

@ -0,0 +1,88 @@
{
"@@locale": "zh",
"fastestExposurePair": "最快曝光组合",
"slowestExposurePair": "最慢曝光组合",
"ev": "EV",
"evValue": "{value} EV",
"@evValue": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"iso": "ISO",
"filmSpeed": "胶片感光度",
"nd": "ND",
"ndFilterFactor": "ND 滤镜系数",
"noExposurePairs": "所选设置没有曝光配对",
"noCamerasDetected": "您的设备似乎没有连接到任何摄像头",
"noCameraPermission": "未获得摄像头权限",
"otherCameraError": "连接摄像头时发生错误",
"none": "无",
"cancel": "取消",
"select": "选择",
"save": "保存",
"settings": "设置",
"metering": "测量",
"fractionalStops": "EV 步进值",
"showFractionalStops": "显示 EV 步进值",
"halfStops": "1/2",
"thirdStops": "1/3",
"calibration": "校准",
"calibrationMessage": "此应用测量读数的准确性完全取决于设备的硬件。因此,请考虑测试此应用并手动设置 EV 校准,以获得准确的测量结果。",
"calibrationMessageCameraOnly": "此应用程序测量读数的准确性完全取决于设备的后置摄像头。因此,请考虑测试此应用并手动设置 EV 校准,以获得准确的测量结果。",
"camera": "摄像头",
"lightSensor": "光传感器",
"meteringScreenLayout": "布局",
"meteringScreenLayoutHint": "隐藏不需要的元素,以免浪费曝光列表空间",
"meteringScreenFeatureExtremeExposurePairs": "最快 & 最慢曝光组合",
"meteringScreenFeatureFilmPicker": "胶片选择",
"film": "胶片",
"equipment": "设备",
"equipmentProfileName": "设备配置名称",
"equipmentProfileNameHint": "Praktica MTL5B",
"equipmentProfileAllValues": "全部",
"apertureValues": "光圈值",
"apertureValuesFilterDescription": "选择要显示的光圈值范围。这通常由您使用的镜头决定。",
"ndFilters": "ND 滤镜",
"ndFiltersFilterDescription": "选择要显示的 ND 滤镜系数。这些可能是您最常用的 ND 滤镜,也可能是适合您镜头的滤光镜。",
"shutterSpeedValues": "快门速度",
"shutterSpeedValuesFilterDescription": "选择要显示的快门速度范围。这通常由您使用的相机机身决定。",
"isoValues": "ISO",
"isoValuesFilterDescription": "选择要显示的 ISO。这些值可能是您最常用的值也可能是相机支持的值。",
"equipmentProfile": "设备配置",
"equipmentProfiles": "设备配置",
"general": "通用",
"keepScreenOn": "保持屏幕常亮",
"haptics": "震动",
"volumeKeysAction": "音量键快门",
"language": "语言",
"chooseLanguage": "选择语言",
"theme": "主题",
"chooseTheme": "选择主题",
"themeLight": "亮色",
"themeDark": "暗色",
"themeSystemDefault": "跟随系统",
"dynamicColor": "动态颜色",
"primaryColor": "主题颜色",
"choosePrimaryColor": "选择主题颜色",
"about": "关于",
"sourceCode": "源代码",
"reportIssue": "报告问题",
"writeEmail": "Email",
"youDontHaveMailApp": "您没有安装任何邮件App。",
"copyEmail": "复制电子邮件",
"version": "Version",
"versionNumber": "{version} ({buildNumber})",
"@versionNumber": {
"placeholders": {
"version": {
"type": "String"
},
"buildNumber": {
"type": "String"
}
}
}
}

View file

@ -1,3 +1,5 @@
import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lightmeter/application.dart'; import 'package:lightmeter/application.dart';
import 'package:lightmeter/environment.dart'; import 'package:lightmeter/environment.dart';
@ -5,6 +7,10 @@ import 'package:lightmeter/firebase.dart';
Future<void> main() async { Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
try {
await initializeFirebase(); await initializeFirebase();
} catch (e) {
log(e.toString());
}
runApp(const Application(Environment.prod())); runApp(const Application(Environment.prod()));
} }

View file

@ -6,7 +6,7 @@ import 'package:lightmeter/data/permissions_service.dart';
import 'package:lightmeter/data/shared_prefs_service.dart'; import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/data/volume_events_service.dart'; import 'package:lightmeter/data/volume_events_service.dart';
import 'package:lightmeter/environment.dart'; import 'package:lightmeter/environment.dart';
import 'package:lightmeter/providers/equipment_profile_provider.dart';
import 'package:lightmeter/providers/ev_source_type_provider.dart'; import 'package:lightmeter/providers/ev_source_type_provider.dart';
import 'package:lightmeter/providers/metering_screen_layout_provider.dart'; import 'package:lightmeter/providers/metering_screen_layout_provider.dart';
import 'package:lightmeter/providers/stop_type_provider.dart'; import 'package:lightmeter/providers/stop_type_provider.dart';
@ -32,11 +32,13 @@ class LightmeterProviders extends StatelessWidget {
]), ]),
builder: (_, snapshot) { builder: (_, snapshot) {
if (snapshot.data != null) { if (snapshot.data != null) {
return IAPProductsProvider( final sharedPrefs = snapshot.data![0] as SharedPreferences;
return IAPProviders(
sharedPreferences: sharedPrefs,
child: InheritedWidgetBase<Environment>( child: InheritedWidgetBase<Environment>(
data: env.copyWith(hasLightSensor: snapshot.data![1] as bool), data: env.copyWith(hasLightSensor: snapshot.data![1] as bool),
child: InheritedWidgetBase<UserPreferencesService>( child: InheritedWidgetBase<UserPreferencesService>(
data: UserPreferencesService(snapshot.data![0] as SharedPreferences), data: UserPreferencesService(sharedPrefs),
child: InheritedWidgetBase<LightSensorService>( child: InheritedWidgetBase<LightSensorService>(
data: const LightSensorService(LocalPlatform()), data: const LightSensorService(LocalPlatform()),
child: InheritedWidgetBase<CaffeineService>( child: InheritedWidgetBase<CaffeineService>(
@ -49,7 +51,6 @@ class LightmeterProviders extends StatelessWidget {
data: const PermissionsService(), data: const PermissionsService(),
child: MeteringScreenLayoutProvider( child: MeteringScreenLayoutProvider(
child: StopTypeProvider( child: StopTypeProvider(
child: EquipmentProfileProvider(
child: EvSourceTypeProvider( child: EvSourceTypeProvider(
child: SupportedLocaleProvider( child: SupportedLocaleProvider(
child: ThemeProvider( child: ThemeProvider(
@ -68,7 +69,6 @@ class LightmeterProviders extends StatelessWidget {
), ),
), ),
), ),
),
); );
} else if (snapshot.error != null) { } else if (snapshot.error != null) {
return Center(child: Text(snapshot.error!.toString())); return Center(child: Text(snapshot.error!.toString()));

View file

@ -1,101 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/utils/inherited_generics.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
import 'package:uuid/uuid.dart';
typedef EquipmentProfiles = List<EquipmentProfileData>;
typedef EquipmentProfile = EquipmentProfileData;
class EquipmentProfileProvider extends StatefulWidget {
final Widget child;
const EquipmentProfileProvider({required this.child, super.key});
static EquipmentProfileProviderState of(BuildContext context) {
return context.findAncestorStateOfType<EquipmentProfileProviderState>()!;
}
@override
State<EquipmentProfileProvider> createState() => EquipmentProfileProviderState();
}
class EquipmentProfileProviderState extends State<EquipmentProfileProvider> {
static const EquipmentProfileData _defaultProfile = EquipmentProfileData(
id: '',
name: '',
apertureValues: ApertureValue.values,
ndValues: NdValue.values,
shutterSpeedValues: ShutterSpeedValue.values,
isoValues: IsoValue.values,
);
List<EquipmentProfileData> _customProfiles = [];
String _selectedId = '';
EquipmentProfileData get _selectedProfile => _customProfiles.firstWhere(
(e) => e.id == _selectedId,
orElse: () {
context.get<UserPreferencesService>().selectedEquipmentProfileId = _defaultProfile.id;
return _defaultProfile;
},
);
@override
void initState() {
super.initState();
_selectedId = context.get<UserPreferencesService>().selectedEquipmentProfileId;
_customProfiles = context.get<UserPreferencesService>().equipmentProfiles;
}
@override
Widget build(BuildContext context) {
return InheritedWidgetBase<List<EquipmentProfileData>>(
data: [_defaultProfile] + _customProfiles,
child: InheritedWidgetBase<EquipmentProfileData>(
data: _selectedProfile,
child: widget.child,
),
);
}
void setProfile(EquipmentProfileData data) {
setState(() {
_selectedId = data.id;
});
context.get<UserPreferencesService>().selectedEquipmentProfileId = _selectedProfile.id;
}
/// Creates a default equipment profile
void addProfile(String name) {
_customProfiles.add(
EquipmentProfileData(
id: const Uuid().v1(),
name: name,
apertureValues: ApertureValue.values,
ndValues: NdValue.values,
shutterSpeedValues: ShutterSpeedValue.values,
isoValues: IsoValue.values,
),
);
_refreshSavedProfiles();
}
void updateProdile(EquipmentProfileData data) {
final indexToUpdate = _customProfiles.indexWhere((element) => element.id == data.id);
if (indexToUpdate >= 0) {
_customProfiles[indexToUpdate] = data;
_refreshSavedProfiles();
}
}
void deleteProfile(EquipmentProfileData data) {
_customProfiles.remove(data);
_refreshSavedProfiles();
}
void _refreshSavedProfiles() {
context.get<UserPreferencesService>().equipmentProfiles = _customProfiles;
setState(() {});
}
}

View file

@ -3,12 +3,12 @@ import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/data/models/film.dart'; import 'package:lightmeter/data/models/film.dart';
import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/providers/equipment_profile_provider.dart';
import 'package:lightmeter/providers/metering_screen_layout_provider.dart'; import 'package:lightmeter/providers/metering_screen_layout_provider.dart';
import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/animated_dialog_picker/widget_picker_dialog_animated.dart'; import 'package:lightmeter/screens/metering/components/shared/readings_container/components/animated_dialog_picker/widget_picker_dialog_animated.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/reading_value_container/widget_container_reading_value.dart'; import 'package:lightmeter/screens/metering/components/shared/readings_container/components/reading_value_container/widget_container_reading_value.dart';
import 'package:lightmeter/utils/inherited_generics.dart'; import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class ReadingsContainer extends StatelessWidget { class ReadingsContainer extends StatelessWidget {
@ -79,7 +79,7 @@ class ReadingsContainer extends StatelessWidget {
Expanded( Expanded(
child: _IsoValuePicker( child: _IsoValuePicker(
selectedValue: iso, selectedValue: iso,
values: context.listen<EquipmentProfile>().isoValues, values: EquipmentProfiles.selectedOf(context).isoValues,
onChanged: onIsoChanged, onChanged: onIsoChanged,
), ),
), ),
@ -87,7 +87,7 @@ class ReadingsContainer extends StatelessWidget {
Expanded( Expanded(
child: _NdValuePicker( child: _NdValuePicker(
selectedValue: nd, selectedValue: nd,
values: context.listen<EquipmentProfile>().ndValues, values: EquipmentProfiles.selectedOf(context).ndValues,
onChanged: onNdChanged, onChanged: onNdChanged,
), ),
), ),
@ -107,19 +107,19 @@ class _EquipmentProfilePicker extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AnimatedDialogPicker<EquipmentProfileData>( return AnimatedDialogPicker<EquipmentProfile>(
icon: Icons.camera, icon: Icons.camera,
title: S.of(context).equipmentProfile, title: S.of(context).equipmentProfile,
selectedValue: context.listen<EquipmentProfile>(), selectedValue: EquipmentProfiles.selectedOf(context),
values: context.listen<EquipmentProfiles>(), values: EquipmentProfiles.of(context),
itemTitleBuilder: (_, value) => Text(value.id.isEmpty ? S.of(context).none : value.name), itemTitleBuilder: (_, value) => Text(value.id.isEmpty ? S.of(context).none : value.name),
onChanged: EquipmentProfileProvider.of(context).setProfile, onChanged: EquipmentProfileProvider.of(context).setProfile,
closedChild: ReadingValueContainer.singleValue( closedChild: ReadingValueContainer.singleValue(
value: ReadingValue( value: ReadingValue(
label: S.of(context).equipmentProfile, label: S.of(context).equipmentProfile,
value: context.listen<EquipmentProfile>().id.isEmpty value: EquipmentProfiles.selectedOf(context).id.isEmpty
? S.of(context).none ? S.of(context).none
: context.listen<EquipmentProfile>().name, : EquipmentProfiles.selectedOf(context).name,
), ),
), ),
); );

View file

@ -6,7 +6,7 @@ sealed class MeteringEvent {
} }
class EquipmentProfileChangedEvent extends MeteringEvent { class EquipmentProfileChangedEvent extends MeteringEvent {
final EquipmentProfileData equipmentProfileData; final EquipmentProfile equipmentProfileData;
const EquipmentProfileChangedEvent(this.equipmentProfileData); const EquipmentProfileChangedEvent(this.equipmentProfileData);
} }

View file

@ -7,7 +7,6 @@ import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/data/models/film.dart'; import 'package:lightmeter/data/models/film.dart';
import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
import 'package:lightmeter/environment.dart'; import 'package:lightmeter/environment.dart';
import 'package:lightmeter/providers/equipment_profile_provider.dart';
import 'package:lightmeter/providers/ev_source_type_provider.dart'; import 'package:lightmeter/providers/ev_source_type_provider.dart';
import 'package:lightmeter/screens/metering/bloc_metering.dart'; import 'package:lightmeter/screens/metering/bloc_metering.dart';
import 'package:lightmeter/screens/metering/components/bottom_controls/provider_bottom_controls.dart'; import 'package:lightmeter/screens/metering/components/bottom_controls/provider_bottom_controls.dart';
@ -15,7 +14,9 @@ import 'package:lightmeter/screens/metering/components/camera_container/provider
import 'package:lightmeter/screens/metering/components/light_sensor_container/provider_container_light_sensor.dart'; import 'package:lightmeter/screens/metering/components/light_sensor_container/provider_container_light_sensor.dart';
import 'package:lightmeter/screens/metering/event_metering.dart'; import 'package:lightmeter/screens/metering/event_metering.dart';
import 'package:lightmeter/screens/metering/state_metering.dart'; import 'package:lightmeter/screens/metering/state_metering.dart';
import 'package:lightmeter/screens/metering/utils/equipment_profile_listener.dart';
import 'package:lightmeter/utils/inherited_generics.dart'; import 'package:lightmeter/utils/inherited_generics.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class MeteringScreen extends StatelessWidget { class MeteringScreen extends StatelessWidget {
@ -72,7 +73,7 @@ class _InheritedListeners extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return InheritedWidgetListener<EquipmentProfile>( return EquipmentProfileListener(
onDidChangeDependencies: (value) { onDidChangeDependencies: (value) {
context.read<MeteringBloc>().add(EquipmentProfileChangedEvent(value)); context.read<MeteringBloc>().add(EquipmentProfileChangedEvent(value));
}, },
@ -87,8 +88,7 @@ class _InheritedListeners extends StatelessWidget {
aspect: MeteringScreenLayoutFeature.equipmentProfiles, aspect: MeteringScreenLayoutFeature.equipmentProfiles,
onDidChangeDependencies: (value) { onDidChangeDependencies: (value) {
if (!value) { if (!value) {
EquipmentProfileProvider.of(context) EquipmentProfileProvider.of(context).setProfile(EquipmentProfiles.of(context).first);
.setProfile(context.get<EquipmentProfiles>().first);
} }
}, },
child: child, child: child,
@ -123,7 +123,7 @@ class MeteringContainerBuidler extends StatelessWidget {
? buildExposureValues( ? buildExposureValues(
ev!, ev!,
context.listen<StopType>(), context.listen<StopType>(),
context.listen<EquipmentProfile>(), EquipmentProfiles.selectedOf(context),
film, film,
) )
: <ExposurePair>[]; : <ExposurePair>[];

View file

@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class EquipmentProfileListener extends StatefulWidget {
final ValueChanged<EquipmentProfile> onDidChangeDependencies;
final Widget child;
const EquipmentProfileListener({
required this.onDidChangeDependencies,
required this.child,
super.key,
});
@override
State<EquipmentProfileListener> createState() => _EquipmentProfileListenerState();
}
class _EquipmentProfileListenerState extends State<EquipmentProfileListener> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
widget.onDidChangeDependencies(EquipmentProfiles.selectedOf(context));
}
@override
Widget build(BuildContext context) {
return widget.child;
}
}

View file

@ -8,8 +8,8 @@ import 'package:lightmeter/screens/settings/components/metering/components/equip
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class EquipmentProfileContainer extends StatefulWidget { class EquipmentProfileContainer extends StatefulWidget {
final EquipmentProfileData data; final EquipmentProfile data;
final ValueChanged<EquipmentProfileData> onUpdate; final ValueChanged<EquipmentProfile> onUpdate;
final VoidCallback onDelete; final VoidCallback onDelete;
final VoidCallback onExpand; final VoidCallback onExpand;
@ -27,7 +27,7 @@ class EquipmentProfileContainer extends StatefulWidget {
class EquipmentProfileContainerState extends State<EquipmentProfileContainer> class EquipmentProfileContainerState extends State<EquipmentProfileContainer>
with TickerProviderStateMixin { with TickerProviderStateMixin {
late EquipmentProfileData _equipmentData = EquipmentProfileData( late EquipmentProfile _equipmentData = EquipmentProfile(
id: widget.data.id, id: widget.data.id,
name: widget.data.name, name: widget.data.name,
apertureValues: widget.data.apertureValues, apertureValues: widget.data.apertureValues,
@ -45,7 +45,7 @@ class EquipmentProfileContainerState extends State<EquipmentProfileContainer>
@override @override
void didUpdateWidget(EquipmentProfileContainer oldWidget) { void didUpdateWidget(EquipmentProfileContainer oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
_equipmentData = EquipmentProfileData( _equipmentData = EquipmentProfile(
id: widget.data.id, id: widget.data.id,
name: widget.data.name, name: widget.data.name,
apertureValues: widget.data.apertureValues, apertureValues: widget.data.apertureValues,
@ -196,7 +196,7 @@ class _AnimatedArrowButton extends AnimatedWidget {
} }
class _AnimatedEquipmentListTiles extends AnimatedWidget { class _AnimatedEquipmentListTiles extends AnimatedWidget {
final EquipmentProfileData equipmentData; final EquipmentProfile equipmentData;
final ValueChanged<List<ApertureValue>> onApertureValuesSelected; final ValueChanged<List<ApertureValue>> onApertureValuesSelected;
final ValueChanged<List<IsoValue>> onIsoValuesSelecred; final ValueChanged<List<IsoValue>> onIsoValuesSelecred;
final ValueChanged<List<NdValue>> onNdValuesSelected; final ValueChanged<List<NdValue>> onNdValuesSelected;

View file

@ -1,11 +1,11 @@
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/providers/equipment_profile_provider.dart';
import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart'; import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart';
import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_name_dialog/widget_dialog_equipment_profile_name.dart'; import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_name_dialog/widget_dialog_equipment_profile_name.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/inherited_generics.dart'; import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class EquipmentProfilesScreen extends StatefulWidget { class EquipmentProfilesScreen extends StatefulWidget {
@ -19,13 +19,12 @@ class _EquipmentProfilesScreenState extends State<EquipmentProfilesScreen> {
static const maxProfiles = 5 + 1; // replace with a constant from iap static const maxProfiles = 5 + 1; // replace with a constant from iap
late List<GlobalKey<EquipmentProfileContainerState>> profileContainersKeys = []; late List<GlobalKey<EquipmentProfileContainerState>> profileContainersKeys = [];
int get profilesCount => context.listen<EquipmentProfiles>().length; int get profilesCount => EquipmentProfiles.of(context).length;
@override @override
void initState() { void didChangeDependencies() {
super.initState(); super.didChangeDependencies();
profileContainersKeys = context profileContainersKeys = EquipmentProfiles.of(context)
.get<EquipmentProfiles>()
.map((e) => GlobalKey<EquipmentProfileContainerState>(debugLabel: e.id)) .map((e) => GlobalKey<EquipmentProfileContainerState>(debugLabel: e.id))
.toList(); .toList();
} }
@ -58,14 +57,14 @@ class _EquipmentProfilesScreenState extends State<EquipmentProfilesScreen> {
), ),
child: EquipmentProfileContainer( child: EquipmentProfileContainer(
key: profileContainersKeys[index], key: profileContainersKeys[index],
data: context.listen<EquipmentProfiles>()[index], data: EquipmentProfiles.of(context)[index],
onExpand: () => _keepExpandedAt(index), onExpand: () => _keepExpandedAt(index),
onUpdate: (profileData) => _updateProfileAt(profileData, index), onUpdate: (profileData) => _updateProfileAt(profileData, index),
onDelete: () => _removeProfileAt(index), onDelete: () => _removeProfileAt(index),
), ),
) )
: const SizedBox.shrink(), : const SizedBox.shrink(),
childCount: profileContainersKeys.length, childCount: profilesCount,
), ),
), ),
], ],
@ -79,18 +78,16 @@ class _EquipmentProfilesScreenState extends State<EquipmentProfilesScreen> {
).then((value) { ).then((value) {
if (value != null) { if (value != null) {
EquipmentProfileProvider.of(context).addProfile(value); EquipmentProfileProvider.of(context).addProfile(value);
profileContainersKeys.add(GlobalKey<EquipmentProfileContainerState>());
} }
}); });
} }
void _updateProfileAt(EquipmentProfileData data, int index) { void _updateProfileAt(EquipmentProfile data, int index) {
EquipmentProfileProvider.of(context).updateProdile(data); EquipmentProfileProvider.of(context).updateProdile(data);
} }
void _removeProfileAt(int index) { void _removeProfileAt(int index) {
EquipmentProfileProvider.of(context).deleteProfile(context.listen<EquipmentProfiles>()[index]); EquipmentProfileProvider.of(context).deleteProfile(EquipmentProfiles.of(context)[index]);
profileContainersKeys.removeAt(index);
} }
void _keepExpandedAt(int index) { void _keepExpandedAt(int index) {

View file

@ -1,8 +1,8 @@
import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lightmeter/environment.dart';
import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart'; import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart';
import 'package:lightmeter/utils/inherited_generics.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
@ -11,18 +11,25 @@ class EquipmentProfilesListTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final paidStatus = IAPProducts.productOf(context, IAPProductType.paidFeatures)?.status;
log(paidStatus.toString());
return ListTile( return ListTile(
leading: const Icon(Icons.camera), leading: const Icon(Icons.camera),
title: Text(S.of(context).equipmentProfiles), title: Text(S.of(context).equipmentProfiles),
onTap: () { onTap: switch (paidStatus) {
if (context.get<Environment>().buildType == BuildType.dev || IAPProductStatus.purchased => () {
IAPProducts.isPurchased(context, IAPProductType.paidFeatures)) { Navigator.of(context).push<EquipmentProfile>(
Navigator.of(context).push<EquipmentProfileData>(
MaterialPageRoute(builder: (_) => const EquipmentProfilesScreen()), MaterialPageRoute(builder: (_) => const EquipmentProfilesScreen()),
); );
} else { },
IAPProductStatus.purchasable => () {
IAPProductsProvider.of(context).buy(IAPProductType.paidFeatures); IAPProductsProvider.of(context).buy(IAPProductType.paidFeatures);
} },
_ => null,
},
trailing: switch (paidStatus) {
IAPProductStatus.purchasable => const Icon(Icons.lock),
_ => null,
}, },
); );
} }

View file

@ -1,7 +1,7 @@
name: lightmeter name: lightmeter
description: Lightmeter app inspired by Material 3 design system. description: Lightmeter app inspired by Material 3 design system.
publish_to: "none" publish_to: "none"
version: 0.12.0+31 version: 0.12.2+33
environment: environment:
sdk: ">=3.0.0 <4.0.0" sdk: ">=3.0.0 <4.0.0"

View file

@ -495,7 +495,7 @@ void main() {
group( group(
'`EquipmentProfileChangedEvent`', '`EquipmentProfileChangedEvent`',
() { () {
final reducedProfile = EquipmentProfileData( final reducedProfile = EquipmentProfile(
id: '0', id: '0',
name: 'Reduced', name: 'Reduced',
apertureValues: ApertureValue.values, apertureValues: ApertureValue.values,

View file

@ -5,7 +5,7 @@ import 'package:lightmeter/screens/metering/screen_metering.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
void main() { void main() {
const defaultEquipmentProfile = EquipmentProfileData( const defaultEquipmentProfile = EquipmentProfile(
id: "", id: "",
name: 'Default', name: 'Default',
apertureValues: ApertureValue.values, apertureValues: ApertureValue.values,
@ -334,7 +334,7 @@ void main() {
}); });
group('Reduced equipment profile', () { group('Reduced equipment profile', () {
final equipmentProfile = EquipmentProfileData( final equipmentProfile = EquipmentProfile(
id: "1", id: "1",
name: 'Test1', name: 'Test1',
apertureValues: ApertureValue.values.sublist(4), apertureValues: ApertureValue.values.sublist(4),