Compare commits

..

No commits in common. "354cc07e6e5996fc00ff4ded15d1866032fa0775" and "ab762713871df1e2ac0dfe293059ea95054a0a5b" have entirely different histories.

30 changed files with 196 additions and 554 deletions

View file

@ -68,7 +68,6 @@ 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,7 +70,6 @@ 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,18 +12,14 @@ on:
branches: ["main"] branches: ["main"]
jobs: jobs:
analyze_and_test: build:
name: Analyze & test
runs-on: macos-11 runs-on: macos-11
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- uses: shaunco/ssh-agent@git-repo-mapping - uses: webfactory/ssh-agent@v0.8.0
with: with:
ssh-private-key: | ssh-private-key: ${{ secrets.M3_LIGHTMETER_IAP_KEY }}
${{ secrets.M3_LIGHTMETER_IAP_KEY }}
repo-mappings: |
github.com/vodemn/m3_lightmeter_iap
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
@ -32,13 +28,23 @@ 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: Check flutter version
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: |
flutter --version FIREBASE_OPTIONS_PATH=$RUNNER_TEMP/firebase_options.dart
flutter pub get echo -n "$FIREBASE_OPTIONS" | base64 --decode --output $FIREBASE_OPTIONS_PATH
flutter pub run intl_utils:generate cp $FIREBASE_OPTIONS_PATH ./lib
- 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)
- [Development](#development) - [Build](#build)
- [Contribution](#contribution) - [Contribution](#contribution)
- [iOS Limitations](#ios-limitations) - [iOS Limitations](#ios-limitations)
@ -27,61 +27,22 @@ 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>
# Development # Build
### 1. Install Flutter 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.
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 feel free to open a Pull Request, but you need to follow this [style guide](doc/style_guide.md). In case you want to help develop this project 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
View file

@ -1,36 +0,0 @@
# 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

View file

@ -1,10 +0,0 @@
# 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

View file

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

View file

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

View file

@ -1,30 +0,0 @@
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

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

View file

@ -1,79 +0,0 @@
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

@ -1,47 +0,0 @@
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;
}

View file

@ -1,25 +0,0 @@
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, zh } enum SupportedLocale { en, fr, ru }
extension SupportedLocaleExtension on SupportedLocale { extension SupportedLocaleExtension on SupportedLocale {
String get intlName => toString().replaceAll("SupportedLocale.", ""); String get intlName => toString().replaceAll("SupportedLocale.", "");
@ -11,8 +11,6 @@ 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,6 +7,7 @@ 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';
@ -147,4 +148,14 @@ 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;
}
} }

View file

@ -1,68 +0,0 @@
// 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: '',
);
}

View file

@ -1,88 +0,0 @@
{
"@@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,5 +1,3 @@
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';
@ -7,10 +5,6 @@ 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,13 +32,11 @@ class LightmeterProviders extends StatelessWidget {
]), ]),
builder: (_, snapshot) { builder: (_, snapshot) {
if (snapshot.data != null) { if (snapshot.data != null) {
final sharedPrefs = snapshot.data![0] as SharedPreferences; return IAPProductsProvider(
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(sharedPrefs), data: UserPreferencesService(snapshot.data![0] as SharedPreferences),
child: InheritedWidgetBase<LightSensorService>( child: InheritedWidgetBase<LightSensorService>(
data: const LightSensorService(LocalPlatform()), data: const LightSensorService(LocalPlatform()),
child: InheritedWidgetBase<CaffeineService>( child: InheritedWidgetBase<CaffeineService>(
@ -51,6 +49,7 @@ 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(
@ -69,6 +68,7 @@ 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

@ -0,0 +1,101 @@
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:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:lightmeter/utils/inherited_generics.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: EquipmentProfiles.selectedOf(context).isoValues, values: context.listen<EquipmentProfile>().isoValues,
onChanged: onIsoChanged, onChanged: onIsoChanged,
), ),
), ),
@ -87,7 +87,7 @@ class ReadingsContainer extends StatelessWidget {
Expanded( Expanded(
child: _NdValuePicker( child: _NdValuePicker(
selectedValue: nd, selectedValue: nd,
values: EquipmentProfiles.selectedOf(context).ndValues, values: context.listen<EquipmentProfile>().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<EquipmentProfile>( return AnimatedDialogPicker<EquipmentProfileData>(
icon: Icons.camera, icon: Icons.camera,
title: S.of(context).equipmentProfile, title: S.of(context).equipmentProfile,
selectedValue: EquipmentProfiles.selectedOf(context), selectedValue: context.listen<EquipmentProfile>(),
values: EquipmentProfiles.of(context), values: context.listen<EquipmentProfiles>(),
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: EquipmentProfiles.selectedOf(context).id.isEmpty value: context.listen<EquipmentProfile>().id.isEmpty
? S.of(context).none ? S.of(context).none
: EquipmentProfiles.selectedOf(context).name, : context.listen<EquipmentProfile>().name,
), ),
), ),
); );

View file

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

View file

@ -7,6 +7,7 @@ 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';
@ -14,9 +15,7 @@ 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 {
@ -73,7 +72,7 @@ class _InheritedListeners extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return EquipmentProfileListener( return InheritedWidgetListener<EquipmentProfile>(
onDidChangeDependencies: (value) { onDidChangeDependencies: (value) {
context.read<MeteringBloc>().add(EquipmentProfileChangedEvent(value)); context.read<MeteringBloc>().add(EquipmentProfileChangedEvent(value));
}, },
@ -88,7 +87,8 @@ class _InheritedListeners extends StatelessWidget {
aspect: MeteringScreenLayoutFeature.equipmentProfiles, aspect: MeteringScreenLayoutFeature.equipmentProfiles,
onDidChangeDependencies: (value) { onDidChangeDependencies: (value) {
if (!value) { if (!value) {
EquipmentProfileProvider.of(context).setProfile(EquipmentProfiles.of(context).first); EquipmentProfileProvider.of(context)
.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>(),
EquipmentProfiles.selectedOf(context), context.listen<EquipmentProfile>(),
film, film,
) )
: <ExposurePair>[]; : <ExposurePair>[];

View file

@ -1,30 +0,0 @@
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 EquipmentProfile data; final EquipmentProfileData data;
final ValueChanged<EquipmentProfile> onUpdate; final ValueChanged<EquipmentProfileData> 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 EquipmentProfile _equipmentData = EquipmentProfile( late EquipmentProfileData _equipmentData = EquipmentProfileData(
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 = EquipmentProfile( _equipmentData = EquipmentProfileData(
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 EquipmentProfile equipmentData; final EquipmentProfileData 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:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:lightmeter/utils/inherited_generics.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,12 +19,13 @@ 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 => EquipmentProfiles.of(context).length; int get profilesCount => context.listen<EquipmentProfiles>().length;
@override @override
void didChangeDependencies() { void initState() {
super.didChangeDependencies(); super.initState();
profileContainersKeys = EquipmentProfiles.of(context) profileContainersKeys = context
.get<EquipmentProfiles>()
.map((e) => GlobalKey<EquipmentProfileContainerState>(debugLabel: e.id)) .map((e) => GlobalKey<EquipmentProfileContainerState>(debugLabel: e.id))
.toList(); .toList();
} }
@ -57,14 +58,14 @@ class _EquipmentProfilesScreenState extends State<EquipmentProfilesScreen> {
), ),
child: EquipmentProfileContainer( child: EquipmentProfileContainer(
key: profileContainersKeys[index], key: profileContainersKeys[index],
data: EquipmentProfiles.of(context)[index], data: context.listen<EquipmentProfiles>()[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: profilesCount, childCount: profileContainersKeys.length,
), ),
), ),
], ],
@ -78,16 +79,18 @@ 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(EquipmentProfile data, int index) { void _updateProfileAt(EquipmentProfileData 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(EquipmentProfiles.of(context)[index]); EquipmentProfileProvider.of(context).deleteProfile(context.listen<EquipmentProfiles>()[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,25 +11,18 @@ 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: switch (paidStatus) { onTap: () {
IAPProductStatus.purchased => () { if (context.get<Environment>().buildType == BuildType.dev ||
Navigator.of(context).push<EquipmentProfile>( IAPProducts.isPurchased(context, IAPProductType.paidFeatures)) {
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.2+33 version: 0.12.0+31
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 = EquipmentProfile( final reducedProfile = EquipmentProfileData(
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 = EquipmentProfile( const defaultEquipmentProfile = EquipmentProfileData(
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 = EquipmentProfile( final equipmentProfile = EquipmentProfileData(
id: "1", id: "1",
name: 'Test1', name: 'Test1',
apertureValues: ApertureValue.values.sublist(4), apertureValues: ApertureValue.values.sublist(4),