Merge branch 'main' of https://github.com/vodemn/m3_lightmeter into feature/ML-104

This commit is contained in:
Vadim 2023-09-21 20:33:53 +02:00
commit e554b2ef2e
123 changed files with 3285 additions and 2228 deletions
.github
.gitignore
.vscode
README.md
android/app
iap
ios/Runner.xcodeproj
lib
application.dart
data
features.dartfirebase.dart
interactors
l10n
main_prod.dartmain_release.dartproviders.dart
providers
res
screens
metering
settings/components

3
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1,3 @@
# These are supported funding model platforms
github: [vodemn]

View file

@ -2,7 +2,7 @@
name: Bug report
about: Create a bug report to help improve the app
title: ''
labels: bug
labels: bug, user feedback
assignees: vodemn
---

View file

@ -2,19 +2,16 @@
name: Feature request or improvement
about: Suggest an idea for this project
title: ''
labels: feature
labels: feature, user feedback
assignees: vodemn
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the feature or the problem it solves**
A clear and concise description of what the problem is.
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View file

@ -0,0 +1,8 @@
export newVersion="$1"
if [[ -n "$newVersion" ]]; then
#https://stackoverflow.com/a/30214769/13167574
perl -i -pe 's/^(version:\s+)(\d+\.\d+\.\d+)(\+)(\d+)$/$1.$ENV{'newVersion'}.$3.($4+1)/e' pubspec.yaml
else
echo "argument error"
fi

2
.github/scripts/stub_iap.sh vendored Normal file
View file

@ -0,0 +1,2 @@
# https://unix.stackexchange.com/questions/435708/regex-multiline-pattern-and-substitution-replacement
perl -0777 -i -pe 's/( m3_lightmeter_iap:\n)( git:\n url: "https:\/\/github.com\/vodemn\/m3_lightmeter_iap"\n ref: v\d{1,2}.\d{1,2}.\d{1,2})/$1 path: iap/sg' pubspec.yaml

View file

@ -16,18 +16,31 @@ on:
- dev
- prod
default: 'dev'
include-iap:
type: boolean
description: Include IAP package
default: true
jobs:
build:
name: Build .apk
runs-on: macos-11
timeout-minutes: 15
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Connect private iap package
uses: webfactory/ssh-agent@v0.8.0
if: ${{ inputs.include-iap }}
with:
ssh-private-key: ${{ secrets.M3_LIGHTMETER_IAP_KEY }}
- name: Override iap package with stub
if: ${{ !inputs.include-iap }}
run: bash ./.github/scripts/stub_iap.sh
- uses: actions/setup-java@v2
with:
distribution: "zulu"
@ -73,10 +86,10 @@ jobs:
flutter pub get
flutter pub run intl_utils:generate
- name: Build Apk
- name: Build .apk
env:
FLAVOR: ${{ github.event.inputs.flavor }}
run: flutter build apk --release --flavor $FLAVOR --dart-define c -t lib/main_$FLAVOR.dart
run: flutter build apk --release --flavor $FLAVOR --dart-define cameraPreviewAspectRatio=240/320 -t lib/main_$FLAVOR.dart
- name: Upload artifact
uses: actions/upload-artifact@v3

View file

@ -31,6 +31,10 @@ on:
type: boolean
description: Create Google Play release
default: true
include-iap:
type: boolean
description: Include IAP package
default: true
env:
BUILD_ARGS: --release --flavor prod --dart-define cameraPreviewAspectRatio=240/320 -t lib/main_prod.dart
@ -38,7 +42,7 @@ env:
jobs:
build:
name: Build .apk & .aab
if: ${{ inputs.github-release }} || ${{ inputs.google-play-release }}
if: ${{ inputs.github-release || inputs.google-play-release }}
runs-on: macos-11
timeout-minutes: 30
steps:
@ -46,6 +50,16 @@ jobs:
with:
submodules: recursive
- name: Connect private iap package
uses: webfactory/ssh-agent@v0.8.0
if: ${{ inputs.include-iap }}
with:
ssh-private-key: ${{ secrets.M3_LIGHTMETER_IAP_KEY }}
- name: Override iap package with stub
if: ${{ !inputs.include-iap }}
run: bash ./.github/scripts/stub_iap.sh
- uses: actions/setup-java@v3
with:
distribution: "zulu"
@ -83,7 +97,7 @@ jobs:
# Therefore here we have to increment it as well to build an apk with the same build number.
- name: Increment build number & replace version number
if: ${{ inputs.github-release }}
run: perl -i -pe 's/^(version:\s+)(\d+\.\d+\.\d+)(\+)(\d+)$/$1."${{ github.event.inputs.version }}".$3.($4+1)/e' pubspec.yaml
run: bash ./.github/scripts/increment_build_number.sh ${{ github.event.inputs.version }}
- name: Install Flutter
uses: subosito/flutter-action@v2
@ -145,12 +159,12 @@ jobs:
submodules: recursive
- name: Increment build number & replace version number
run: perl -i -pe 's/^(version:\s+)(\d+\.\d+\.\d+)(\+)(\d+)$/$1."${{ github.event.inputs.version }}".$3.($4+1)/e' pubspec.yaml
run: bash ./.github/scripts/increment_build_number.sh ${{ github.event.inputs.version }}
- name: Commit changes
run: |
git config --global user.name "vodemn"
git config --global user.email "vadim.turko@gmail.com"
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add -A
git commit -m "Version bump"

View file

@ -15,17 +15,31 @@ jobs:
analyze_and_test:
name: Analyze & test
runs-on: macos-11
timeout-minutes: 10
timeout-minutes: 5
steps:
- uses: 8BitJonny/gh-get-current-pr@2.2.0
id: PR
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Connect private iap package
uses: webfactory/ssh-agent@v0.8.0
id: fetch-iap
if: steps.PR.outputs.number == 'null' || github.event.pull_request.head.repo.full_name == github.repository
with:
ssh-private-key: ${{ secrets.M3_LIGHTMETER_IAP_KEY }}
- name: Override iap package with stub
id: override-iap
if: steps.fetch-iap.conclusion != 'success'
run: bash ./.github/scripts/stub_iap.sh
- uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: '3.10.0'
flutter-version: "3.10.0"
- name: Prepare flutter project
run: |
@ -37,4 +51,11 @@ jobs:
run: flutter analyze lib --fatal-infos
- name: Run tests
run: flutter test
run: flutter test
- name: Analyze project source with stub
if: steps.override-iap.conclusion != 'success'
run: |
bash ./.github/scripts/stub_iap.sh
flutter pub get
flutter analyze lib --fatal-infos

2
.gitignore vendored
View file

@ -57,6 +57,6 @@ keystore.properties
android/app/google-services.json
ios/firebase_app_id_file.json
ios/Runner/GoogleService-Info.plist
lib/firebase_options.dart
/lib/firebase_options.dart
coverage/

63
.vscode/launch.json vendored
View file

@ -5,7 +5,20 @@
"version": "0.2.0",
"configurations": [
{
"name": "dev (android)",
"name": "dev-debug (android)",
"request": "launch",
"type": "dart",
"flutterMode": "debug",
"args": [
"--flavor",
"dev",
"--dart-define",
"cameraPreviewAspectRatio=240/320",
],
"program": "${workspaceFolder}/lib/main_dev.dart",
},
{
"name": "dev-profile (android)",
"request": "launch",
"type": "dart",
"flutterMode": "profile",
@ -18,9 +31,36 @@
"program": "${workspaceFolder}/lib/main_dev.dart",
},
{
"name": "dev (ios)",
"name": "prod-debug (android)",
"request": "launch",
"type": "dart",
"flutterMode": "debug",
"args": [
"--flavor",
"prod",
"--dart-define",
"cameraPreviewAspectRatio=240/320",
],
"program": "${workspaceFolder}/lib/main_release.dart",
},
{
"name": "prod-profile (android)",
"request": "launch",
"type": "dart",
"flutterMode": "profile",
"args": [
"--flavor",
"prod",
"--dart-define",
"cameraPreviewAspectRatio=240/320",
],
"program": "${workspaceFolder}/lib/main_release.dart",
},
{
"name": "dev-debug (ios)",
"request": "launch",
"type": "dart",
"flutterMode": "debug",
"args": [
"--flavor",
"dev",
@ -30,28 +70,17 @@
"program": "${workspaceFolder}/lib/main_dev.dart",
},
{
"name": "prod (android)",
"name": "dev-profile (ios)",
"request": "launch",
"flutterMode": "profile",
"type": "dart",
"args": [
"--flavor",
"prod",
"--dart-define",
"cameraPreviewAspectRatio=240/320",
],
"program": "${workspaceFolder}/lib/main_prod.dart",
},
{
"name": "prod (ios)",
"request": "launch",
"type": "dart",
"args": [
"--flavor",
"prod",
"dev",
"--dart-define",
"cameraPreviewAspectRatio=3/4",
],
"program": "${workspaceFolder}/lib/main_prod.dart",
"program": "${workspaceFolder}/lib/main_dev.dart",
},
{
"name": "Integration Test",

View file

@ -33,25 +33,56 @@ Without further delay behold my new Lightmeter app inspired by Material You (a.k
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
### 3. Project setup
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).
As part of the app's functionallity is in the private repo, you have to replace these lines in _pubspec.yaml_:
### 3. Get packages
```yaml
m3_lightmeter_iap:
git:
url: "https://github.com/vodemn/m3_lightmeter_iap"
ref: main
```
with these:
```yaml
m3_lightmeter_iap:
path: iap
```
You can do it simply by running the script:
```console
sh .github/scripts/stub_iap.sh
```
> If you are using VSCode, you can open the workspace like so: _File -> Open Workspace from File -> m3_lightmeter.code-workspace_. Otherwise you have to run `flutter pub get` command from the iap folder.
Then you can fetch all the neccessary dependencies and generate translation files by running the following commands:
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 (Android)
### 4. (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).
### 5. Build
#### Android
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=240/320 -t lib/main_$FLAVOR.dart
flutter build apk --release --flavor dev --dart-define cameraPreviewAspectRatio=240/320 -t lib/main_dev.dart
```
Just replace `$FLAVOR` with `dev` or `prod`.
### iOS
TBD
# Contribution
@ -69,4 +100,4 @@ Apple does not provide API for reading Lux stream form the ambient light sensor.
## 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)

View file

@ -109,4 +109,5 @@ flutter {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "com.android.billingclient:billing-ktx:6.0.0"
implementation "com.google.firebase:firebase-analytics:17.4.1"
}

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.

17
iap/README.md Normal file
View file

@ -0,0 +1,17 @@
# Lightmeter Pro
### Equipment profiles
Each equipment profile allows you to select:
- Aperture values and shutter speeds, that your lens and camera have
- ND filters, that fit the chosen lens
- ISO values, that your camera supports
Creating multiple profiles for different cameras and lenses allows you to easily switch between them and always have the relevant readings.
### Films in use
Select the films that you usually use. Selecting one will apply a correction to shutter speeds greater than 1" to compensate for the reciprocity failure.
Each equipment profile allows you to select:\n- Aperture values and shutter speeds, that your lens and camera have\n- ND filters, that fit the chosen lens\n- ISO values, that your camera supports\nCreating multiple profiles for different cameras and lenses allows you to easily switch between them and always have the relevant readings!

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,34 @@
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/films_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';
export 'src/providers/films_provider.dart';
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: FilmsProvider(
child: EquipmentProfileProvider(
child: child,
),
),
);
}
}

View file

@ -0,0 +1,13 @@
enum IAPProductStatus {
purchasable,
pending,
purchased,
}
enum IAPProductType { paidFeatures }
abstract class IAPProduct {
const IAPProduct._();
IAPProductStatus get status => IAPProductStatus.purchasable;
}

View file

@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import 'package:m3_lightmeter_iap/src/providers/selectable_provider.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(
values: const [_defaultProfile],
selected: _defaultProfile,
child: widget.child,
);
}
void setProfile(EquipmentProfile data) {}
void addProfile(String name, [EquipmentProfile? copyFrom]) {}
void updateProdile(EquipmentProfile data) {}
void deleteProfile(EquipmentProfile data) {}
}
class EquipmentProfiles extends SelectableInheritedModel<EquipmentProfile> {
const EquipmentProfiles({
super.key,
required super.values,
required super.selected,
required super.child,
});
static List<EquipmentProfile> of(BuildContext context) {
return InheritedModel.inheritFrom<EquipmentProfiles>(context, aspect: SelectableAspect.list)!.values;
}
static EquipmentProfile selectedOf(BuildContext context) {
return InheritedModel.inheritFrom<EquipmentProfiles>(context, aspect: SelectableAspect.selected)!.selected;
}
}

View file

@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
import 'package:m3_lightmeter_iap/src/providers/selectable_provider.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class FilmsProvider extends StatefulWidget {
final Widget child;
const FilmsProvider({
required this.child,
super.key,
});
static FilmsProviderState of(BuildContext context) {
return context.findAncestorStateOfType<FilmsProviderState>()!;
}
@override
State<FilmsProvider> createState() => FilmsProviderState();
}
class FilmsProviderState extends State<FilmsProvider> {
@override
Widget build(BuildContext context) {
return Films(
values: const [Film.other()],
filmsInUse: const [Film.other()],
selected: const Film.other(),
child: widget.child,
);
}
void setFilm(Film film) {}
void saveFilms(List<Film> films) {}
}
class Films extends SelectableInheritedModel<Film> {
final List<Film> filmsInUse;
const Films({
super.key,
required super.values,
required this.filmsInUse,
required super.selected,
required super.child,
});
/// [Film.other()] + all the custom fields with actual reciprocity formulas
static List<Film> of(BuildContext context) {
return InheritedModel.inheritFrom<Films>(context)!.values;
}
/// [Film.other()] + films in use selected by user
static List<Film> inUseOf<T>(BuildContext context) {
return InheritedModel.inheritFrom<Films>(
context,
aspect: SelectableAspect.list,
)!
.filmsInUse;
}
static Film selectedOf(BuildContext context) {
return InheritedModel.inheritFrom<Films>(context, aspect: SelectableAspect.selected)!.selected;
}
}

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? productOf(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

@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
enum SelectableAspect { list, selected }
class SelectableInheritedModel<T> extends InheritedModel<SelectableAspect> {
const SelectableInheritedModel({
super.key,
required this.values,
required this.selected,
required super.child,
});
final List<T> values;
final T selected;
@override
bool updateShouldNotify(SelectableInheritedModel oldWidget) => true;
@override
bool updateShouldNotifyDependent(SelectableInheritedModel oldWidget, Set<SelectableAspect> dependencies) {
if (dependencies.contains(SelectableAspect.list)) {
return true;
} else if (dependencies.contains(SelectableAspect.selected)) {
return selected != oldWidget.selected;
} else {
return 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

@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 51;
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
@ -237,6 +237,7 @@
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
@ -268,6 +269,7 @@
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
@ -371,7 +373,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 74JQ9DBXY6;
DEVELOPMENT_TEAM = 489Z6UQMGN;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@ -500,7 +502,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 74JQ9DBXY6;
DEVELOPMENT_TEAM = 489Z6UQMGN;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@ -523,7 +525,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 74JQ9DBXY6;
DEVELOPMENT_TEAM = 489Z6UQMGN;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@ -600,7 +602,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 74JQ9DBXY6;
DEVELOPMENT_TEAM = 489Z6UQMGN;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@ -675,7 +677,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 74JQ9DBXY6;
DEVELOPMENT_TEAM = 489Z6UQMGN;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@ -747,7 +749,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 74JQ9DBXY6;
DEVELOPMENT_TEAM = 489Z6UQMGN;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (

View file

@ -1,16 +1,22 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:lightmeter/data/models/film.dart';
import 'package:lightmeter/data/caffeine_service.dart';
import 'package:lightmeter/data/haptics_service.dart';
import 'package:lightmeter/data/light_sensor_service.dart';
import 'package:lightmeter/data/models/supported_locale.dart';
import 'package:lightmeter/data/permissions_service.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/data/volume_events_service.dart';
import 'package:lightmeter/environment.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/providers.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/providers/services_provider.dart';
import 'package:lightmeter/providers/user_preferences_provider.dart';
import 'package:lightmeter/screens/metering/flow_metering.dart';
import 'package:lightmeter/screens/settings/flow_settings.dart';
import 'package:lightmeter/utils/inherited_generics.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:platform/platform.dart';
import 'package:shared_preferences/shared_preferences.dart';
class Application extends StatelessWidget {
final Environment env;
@ -19,56 +25,73 @@ class Application extends StatelessWidget {
@override
Widget build(BuildContext context) {
return LightmeterProviders(
env: env,
builder: (context, ready) => ready
? _AnnotatedRegionWrapper(
child: MaterialApp(
theme: context.listen<ThemeData>(),
locale: Locale(context.listen<SupportedLocale>().intlName),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
builder: (context, child) => MediaQuery(
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
child: child!,
return FutureBuilder(
future: Future.wait([
SharedPreferences.getInstance(),
const LightSensorService(LocalPlatform()).hasSensor(),
]),
builder: (_, snapshot) {
if (snapshot.data != null) {
return IAPProviders(
sharedPreferences: snapshot.data![0] as SharedPreferences,
child: ServicesProvider(
caffeineService: const CaffeineService(),
environment: env.copyWith(hasLightSensor: snapshot.data![1] as bool),
hapticsService: const HapticsService(),
lightSensorService: const LightSensorService(LocalPlatform()),
permissionsService: const PermissionsService(),
userPreferencesService:
UserPreferencesService(snapshot.data![0] as SharedPreferences),
volumeEventsService: const VolumeEventsService(LocalPlatform()),
child: UserPreferencesProvider(
child: Builder(
builder: (context) {
final theme = UserPreferencesProvider.themeOf(context);
final systemIconsBrightness =
ThemeData.estimateBrightnessForColor(theme.colorScheme.onSurface);
return AnnotatedRegion(
value: SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarBrightness: systemIconsBrightness == Brightness.light
? Brightness.dark
: Brightness.light,
statusBarIconBrightness: systemIconsBrightness,
systemNavigationBarColor: Colors.transparent,
systemNavigationBarIconBrightness: systemIconsBrightness,
),
child: MaterialApp(
theme: theme,
locale: Locale(UserPreferencesProvider.localeOf(context).intlName),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
builder: (context, child) => MediaQuery(
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
child: child!,
),
initialRoute: "metering",
routes: {
"metering": (context) => const MeteringFlow(),
"settings": (context) => const SettingsFlow(),
},
),
);
},
),
initialRoute: "metering",
routes: {
"metering": (context) => const MeteringFlow(),
"settings": (context) => const SettingsFlow(),
},
),
)
: const SizedBox(),
);
}
}
),
);
} else if (snapshot.error != null) {
return Center(child: Text(snapshot.error!.toString()));
}
class _AnnotatedRegionWrapper extends StatelessWidget {
final Widget child;
const _AnnotatedRegionWrapper({required this.child});
@override
Widget build(BuildContext context) {
final systemIconsBrightness = ThemeData.estimateBrightnessForColor(
context.listen<ThemeData>().colorScheme.onSurface,
);
return AnnotatedRegion(
value: SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarBrightness:
systemIconsBrightness == Brightness.light ? Brightness.dark : Brightness.light,
statusBarIconBrightness: systemIconsBrightness,
systemNavigationBarColor: Colors.transparent,
systemNavigationBarIconBrightness: systemIconsBrightness,
),
child: child,
// TODO(@vodemn): maybe user splashscreen instead
return const SizedBox();
},
);
}
}

View file

@ -8,4 +8,16 @@ class ExposurePair {
@override
String toString() => '$aperture - $shutterSpeed';
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other.runtimeType != runtimeType) return false;
return other is ExposurePair &&
other.aperture == aperture &&
other.shutterSpeed == shutterSpeed;
}
@override
int get hashCode => Object.hash(aperture, shutterSpeed, runtimeType);
}

View file

@ -1,233 +0,0 @@
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
double log10(double x) => log(x) / log(10);
double log10polynomian(
double x,
double a,
double b,
double c,
) =>
a * pow(log10(x), 2) + b * log10(x) + c;
/// Only Ilford films have reciprocity formulas provided by the manufacturer:
/// https://www.ilfordphoto.com/wp/wp-content/uploads/2017/06/Reciprocity-Failure-Compensation.pdf
///
/// Reciprocity formulas for Fomapan films and Kodak films are from here:
/// https://www.flickr.com/groups/86738082@N00/discuss/72157626050157470/
///
/// Cinema films like Kodak 5222/7222 Double-X and respective CineStill films (cause they are basically a modification of Kodak)
/// do not have any reciprocity failure information, as these films are ment to be used in cinema
/// with appropriate light and pretty short shutter speeds.
///
/// Because of this: https://github.com/dart-lang/sdk/issues/38934#issuecomment-803938315
/// `super` calls are ignored in test coverage
class Film {
final String name;
final int iso;
const Film(this.name, this.iso);
const Film.other()
: name = '',
iso = 0;
@override
String toString() => name;
ShutterSpeedValue reciprocityFailure(ShutterSpeedValue shutterSpeed) {
if (shutterSpeed.isFraction) {
return shutterSpeed;
} else {
return ShutterSpeedValue(
reciprocityFormula(shutterSpeed.rawValue),
shutterSpeed.isFraction,
shutterSpeed.stopType,
);
}
}
@protected
double reciprocityFormula(double t) => t;
static const List<Film> values = [
Film.other(),
FomapanFilm.creative100(),
FomapanFilm.creative200(),
FomapanFilm.action400(),
IlfordFilm.ortho(),
IlfordFilm.delta100(),
IlfordFilm.delta400(),
IlfordFilm.delta3200(),
IlfordFilm.fp4(),
IlfordFilm.hp5(),
IlfordFilm.panf(),
IlfordFilm.sfx200(),
IlfordFilm.xp2super(),
IlfordFilm.pan100(),
IlfordFilm.pan400(),
KodakFilm.tmax100(),
KodakFilm.tmax400(),
KodakFilm.tmax3200(),
KodakFilm.trix320(),
KodakFilm.trix400(),
];
}
/// https://www.tate.org.uk/documents/598/page_6_7_agfa_stocks_0.pdf
/// https://www.filmwasters.com/forum/index.php?topic=5298.0
// {{1,1.87},{2,3.73},{3,8.06},{4,13.93},{5,21.28},{6,23.00},{7,30.12},{8,38.05},{9,44.75},{10,50.12},{20,117},{30,202},{40,293},{50,413},{60,547},{70,694},{80,853},{90,1022},{100,1202}};
// class AgfaFilm extends Film {
// final double a;
// final double b;
// final double c;
// const AgfaFilm.apx100()
// : a = 1,
// b = 5,
// c = 2,
// super('Agfa APX 100', 100); // coverage:ignore-line
// const AgfaFilm.apx400()
// : a = 1.5,
// b = 4.5,
// c = 3,
// super('Agfa APX 400', 400); // coverage:ignore-line
// @override
// double reciprocityFormula(double t) => t * log10polynomian(t, a, b, c);
// }
class FomapanFilm extends Film {
final double a;
final double b;
final double c;
/// https://www.foma.cz/en/fomapan-100
const FomapanFilm.creative100()
: a = 1,
b = 5,
c = 2,
super('Fomapan CREATIVE 100', 100); // coverage:ignore-line
/// https://www.foma.cz/en/fomapan-200
const FomapanFilm.creative200()
: a = 1.5,
b = 4.5,
c = 3,
super('Fomapan CREATIVE 200', 200); // coverage:ignore-line
/// https://www.foma.cz/en/fomapan-100
const FomapanFilm.action400()
: a = -1.25, // coverage:ignore-line
b = 5.75,
c = 1.5,
super('Fomapan ACTION 400', 400); // coverage:ignore-line
@override
double reciprocityFormula(double t) => t * log10polynomian(t, a, b, c);
}
class IlfordFilm extends Film {
final double reciprocityPower;
/// https://www.ilfordphoto.com/amfile/file/download/file/1948/product/1650/
const IlfordFilm.ortho()
: reciprocityPower = 1.25,
super('Ilford ORTHO+', 80); // coverage:ignore-line
/// https://www.ilfordphoto.com/amfile/file/download/file/1919/product/686/
const IlfordFilm.fp4()
: reciprocityPower = 1.26,
super('Ilford FP4+', 125); // coverage:ignore-line
/// https://www.ilfordphoto.com/amfile/file/download/file/1903/product/691/
const IlfordFilm.hp5()
: reciprocityPower = 1.31,
super('Ilford HP5+', 400); // coverage:ignore-line
/// https://www.ilfordphoto.com/amfile/file/download/file/3/product/679/
const IlfordFilm.delta100()
: reciprocityPower = 1.26,
super('Ilford DELTA 100', 100); // coverage:ignore-line
/// https://www.ilfordphoto.com/amfile/file/download/file/1915/product/684/
const IlfordFilm.delta400()
: reciprocityPower = 1.41,
super('Ilford DELTA 400', 400); // coverage:ignore-line
/// https://www.ilfordphoto.com/amfile/file/download/file/1913/product/682/
const IlfordFilm.delta3200()
: reciprocityPower = 1.33,
super('Ilford DELTA 3200', 3200); // coverage:ignore-line
/// https://www.ilfordphoto.com/amfile/file/download/file/1905/product/699/
const IlfordFilm.panf()
: reciprocityPower = 1.33,
super('Ilford Pan F+', 50); // coverage:ignore-line
/// https://www.ilfordphoto.com/amfile/file/download/file/1907/product/701/
const IlfordFilm.sfx200()
: reciprocityPower = 1.31,
super('Ilford SFX 200', 200); // coverage:ignore-line
/// https://www.ilfordphoto.com/amfile/file/download/file/1909/product/703/
const IlfordFilm.xp2super()
: reciprocityPower = 1.31,
super('Ilford XP2 SUPER', 400); // coverage:ignore-line
/// https://www.ilfordphoto.com/amfile/file/download/file/1958/product/696/
const IlfordFilm.pan100()
: reciprocityPower = 1.26,
super('Kentemere 100', 100); // coverage:ignore-line
/// https://www.ilfordphoto.com/amfile/file/download/file/1959/product/697/
const IlfordFilm.pan400()
: reciprocityPower = 1.30,
super('Kentemere 400', 400); // coverage:ignore-line
@override
double reciprocityFormula(double t) => pow(t, reciprocityPower).toDouble();
}
class KodakFilm extends Film {
final double a;
final double b;
final double c;
const KodakFilm.tmax100()
: a = 1 / 6, // coverage:ignore-line
b = 0, // coverage:ignore-line
c = 4 / 3, // coverage:ignore-line
super('Kodak T-MAX 100', 100); // coverage:ignore-line
const KodakFilm.tmax400()
: a = 2 / 3, // coverage:ignore-line
b = -1 / 2, // coverage:ignore-line
c = 4 / 3, // coverage:ignore-line
super('Kodak T-MAX 400', 400); // coverage:ignore-line
const KodakFilm.tmax3200()
: a = 7 / 6, // coverage:ignore-line
b = -1, // coverage:ignore-line
c = 4 / 3, // coverage:ignore-line
super('Kodak T-MAX 3200', 3200); // coverage:ignore-line
const KodakFilm.trix320()
: a = 2,
b = 1,
c = 2,
super('Kodak TRI-X 320', 320); // coverage:ignore-line
const KodakFilm.trix400()
: a = 2,
b = 1,
c = 2,
super('Kodak TRI-X 400', 400); // coverage:ignore-line
@override
double reciprocityFormula(double t) => t * log10polynomian(t, a, b, c);
}

View file

@ -1,9 +1,14 @@
enum MeteringScreenLayoutFeature { extremeExposurePairs, filmPicker, histogram }
enum MeteringScreenLayoutFeature {
extremeExposurePairs,
filmPicker,
histogram,
equipmentProfiles,
}
typedef MeteringScreenLayoutConfig = Map<MeteringScreenLayoutFeature, bool>;
extension MeteringScreenLayoutConfigJson on MeteringScreenLayoutConfig {
static MeteringScreenLayoutConfig fromJson(Map<String, dynamic> data) =>
static MeteringScreenLayoutConfig fromJson(Map<String, dynamic> data) =>
<MeteringScreenLayoutFeature, bool>{
for (final f in MeteringScreenLayoutFeature.values)
f: data[f.index.toString()] as bool? ?? true

View file

@ -2,7 +2,6 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/ev_source_type.dart';
import 'package:lightmeter/data/models/film.dart';
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
import 'package:lightmeter/data/models/supported_locale.dart';
import 'package:lightmeter/data/models/theme_type.dart';
@ -19,7 +18,6 @@ class UserPreferencesService {
static const cameraEvCalibrationKey = "cameraEvCalibration";
static const lightSensorEvCalibrationKey = "lightSensorEvCalibration";
static const meteringScreenLayoutKey = "meteringScreenLayout";
static const filmKey = "film";
static const caffeineKey = "caffeine";
static const hapticsKey = "haptics";
@ -95,6 +93,7 @@ class UserPreferencesService {
);
} else {
return {
MeteringScreenLayoutFeature.equipmentProfiles: true,
MeteringScreenLayoutFeature.extremeExposurePairs: true,
MeteringScreenLayoutFeature.filmPicker: true,
MeteringScreenLayoutFeature.histogram: true,
@ -141,16 +140,4 @@ class UserPreferencesService {
bool get dynamicColor => _sharedPreferences.getBool(dynamicColorKey) ?? false;
set dynamicColor(bool value) => _sharedPreferences.setBool(dynamicColorKey, value);
Film get film => Film.values.firstWhere(
(e) => e.name == _sharedPreferences.getString(filmKey),
orElse: () => Film.values.first,
);
set film(Film value) => _sharedPreferences.setString(filmKey, value.name);
String get selectedEquipmentProfileId => ''; // coverage:ignore-line
set selectedEquipmentProfileId(String id) {} // coverage:ignore-line
List<EquipmentProfile> get equipmentProfiles => []; // coverage:ignore-line
set equipmentProfiles(List<EquipmentProfile> profiles) {} // coverage:ignore-line
}

View file

@ -1,3 +0,0 @@
class FeaturesConfig {
static const bool equipmentProfilesEnabled = false;
}

View file

@ -1,14 +1,22 @@
import 'dart:developer';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/foundation.dart';
import 'package:lightmeter/firebase_options.dart';
Future<void> initializeFirebase() async {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;
PlatformDispatcher.instance.onError = (error, stack) {
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
return true;
};
Future<void> initializeFirebase({required bool handleErrors}) async {
try {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
if (handleErrors) {
FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;
PlatformDispatcher.instance.onError = (error, stack) {
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
return true;
};
}
} catch (e) {
log(e.toString());
}
}

View file

@ -2,7 +2,6 @@ import 'package:app_settings/app_settings.dart';
import 'package:lightmeter/data/caffeine_service.dart';
import 'package:lightmeter/data/haptics_service.dart';
import 'package:lightmeter/data/light_sensor_service.dart';
import 'package:lightmeter/data/models/film.dart';
import 'package:lightmeter/data/models/volume_action.dart';
import 'package:lightmeter/data/permissions_service.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
@ -45,9 +44,6 @@ class MeteringInteractor {
NdValue get ndFilter => _userPreferencesService.ndFilter;
set ndFilter(NdValue value) => _userPreferencesService.ndFilter = value;
Film get film => _userPreferencesService.film;
set film(Film value) => _userPreferencesService.film = value;
VolumeAction get volumeAction => _userPreferencesService.volumeAction;
/// Executes vibration if haptics are enabled in settings

View file

@ -36,11 +36,14 @@
"lightSensor": "Light sensor",
"meteringScreenLayout": "Metering screen layout",
"meteringScreenLayoutHint": "Hide elements on the metering screen that you don't need so that they don't waste exposure pairs list space.",
"meteringScreenLayoutHintEquipmentProfiles": "Equipment profile picker",
"meteringScreenFeatureExtremeExposurePairs": "Fastest & shortest exposure pairs",
"meteringScreenFeatureFilmPicker": "Film picker",
"meteringScreenFeatureHistogram": "Histogram",
"film": "Film",
"equipment": "Equipment",
"filmPush": "Film (push)",
"filmPull": "Film (pull)",
"filmReciprocityHint": "Applies correction for shutter speeds grater than 1 second",
"equipmentProfileName": "Equipment profile name",
"equipmentProfileNameHint": "Praktica MTL5B",
"equipmentProfileAllValues": "All",
@ -54,6 +57,9 @@
"isoValuesFilterDescription": "Select the ISO values to display. These may be your most commonly used values or those supported by your camera.",
"equipmentProfile": "Equipment profile",
"equipmentProfiles": "Equipment profiles",
"tapToAdd": "Tap to add",
"filmsInUse": "Films in use",
"filmsInUseDescription": "Select films which you use.",
"general": "General",
"keepScreenOn": "Keep screen on",
"haptics": "Haptics",
@ -85,5 +91,21 @@
"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",
"tooltipAdd": "Add",
"tooltipClose": "Close",
"tooltipExpand": "Expand",
"tooltipCollapse": "Collapse",
"tooltipCopy": "Copy",
"tooltipDelete": "Delete",
"tooltipSelectAll": "Select all",
"tooltipDesecelectAll": "Deselect all",
"tooltipResetToZero": "Reset to zero",
"tooltipUseLightSensor": "Use lightsensor",
"tooltipUseCamera": "Use camera",
"tooltipOpenSettings": "Open settings"
}

View file

@ -36,11 +36,14 @@
"lightSensor": "Capteur de lumière",
"meteringScreenLayout": "Disposition de l'écran de mesure",
"meteringScreenLayoutHint": "Masquer les éléments sur l'écran de mesure dont vous n'avez pas besoin pour qu'ils ne gaspillent pas de l'espace dans les paires d'exposition.",
"meteringScreenLayoutHintEquipmentProfiles": "Sélecteur de profil de l'équipement",
"meteringScreenFeatureExtremeExposurePairs": "Paires d'exposition les plus rapides et les plus courtes",
"meteringScreenFeatureFilmPicker": "Sélecteur de film",
"meteringScreenFeatureHistogram": "Histogramme",
"film": "Pellicule",
"equipment": "Équipement",
"filmPush": "Pellicule (push)",
"filmPull": "Pellicule (pull)",
"filmReciprocityHint": "La correction s'applique aux vitesses d'obturation supérieures à 1 seconde",
"equipmentProfileName": "Nom du profil de l'équipement",
"equipmentProfileNameHint": "Praktica MTL5B",
"equipmentProfileAllValues": "Tout",
@ -54,6 +57,9 @@
"isoValuesFilterDescription": "Sélectionnez les valeurs ISO à afficher. Ce sont peut-être vos valeurs les plus couramment utilisées ou celles prises en charge par votre caméra.",
"equipmentProfile": "Profil de l'équipement",
"equipmentProfiles": "Profils de l'équipement",
"tapToAdd": "Appuie pour ajouter",
"filmsInUse": "Films en usage",
"filmsInUseDescription": "Sélectionnez les films que vous utilisez.",
"general": "Général",
"keepScreenOn": "Garder l'écran allumé",
"haptics": "Haptiques",
@ -85,5 +91,21 @@
"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",
"tooltipAdd": "Ajouter",
"tooltipClose": "Fermer",
"tooltipExpand": "Élargir",
"tooltipCollapse": "Effondrement",
"tooltipCopy": "Copie",
"tooltipDelete": "Supprimer",
"tooltipSelectAll": "Tout sélectionner",
"tooltipDesecelectAll": "Désélectionner tout",
"tooltipResetToZero": "Remise à zéro",
"tooltipUseLightSensor": "Utiliser un capteur de lumière",
"tooltipUseCamera": "Utiliser la caméra",
"tooltipOpenSettings": "Ouvrir les paramètres"
}

View file

@ -36,11 +36,14 @@
"lightSensor": "Датчик освещённости",
"meteringScreenLayout": "Элементы главного экрана",
"meteringScreenLayoutHint": "Здесь вы можете скрыть некоторые ненужные или неиспользуемые элементы с главного экрана.",
"meteringScreenLayoutHintEquipmentProfiles": "Выбор профиля оборудования",
"meteringScreenFeatureExtremeExposurePairs": "Длинная и короткая выдержки",
"meteringScreenFeatureFilmPicker": "Выбор пленки",
"meteringScreenFeatureHistogram": "Гистограмма",
"film": "Пленка",
"equipment": "Оборудование",
"filmPush": "Пленка (push)",
"filmPull": "Пленка (pull)",
"filmReciprocityHint": "Применяет коррекцию для выдержек длиннее 1 секунды",
"equipmentProfileName": "Название профиля",
"equipmentProfileNameHint": "Praktica MTL5B",
"equipmentProfileAllValues": "Все",
@ -54,6 +57,9 @@
"isoValuesFilterDescription": "Выберите значения ISO для отображения. Это может быть наиболее часто используемые значения или значения, поддерживаемые вашей камерой.",
"equipmentProfile": "Оборудование",
"equipmentProfiles": "Профили оборудования",
"tapToAdd": "Нажмите, чтобы добавить",
"filmsInUse": "Используемые пленки",
"filmsInUseDescription": "Выберите пленки, которыми вы пользуетесь.",
"general": "Общие",
"keepScreenOn": "Запрет блокировки",
"haptics": "Вибрация",
@ -85,5 +91,21 @@
"type": "String"
}
}
}
},
"buyLightmeterPro": "Купить Lightmeter Pro",
"lightmeterPro": "Lightmeter Pro",
"lightmeterProDescription": "Даёт доступ к таким функциям как профили оборудования, содержащие фильтры для диафрагмы, выдержки и других значений, а также набору пленок с компенсацией эффекта Шварцшильда.\n\nИсходный код Lightmeter доступен на GitHub. Вы можете собрать его самостоятельно. Однако если вы хотите поддержать разработку и получать новые функции и обновления, то приобретите Lightmeter Pro.",
"buy": "Купить",
"tooltipAdd": "Добавить",
"tooltipClose": "Закрыть",
"tooltipExpand": "Развернуть",
"tooltipCollapse": "Свернуть",
"tooltipCopy": "Скопировать",
"tooltipDelete": "Удалить",
"tooltipSelectAll": "Выбрать все",
"tooltipDesecelectAll": "Отменить все",
"tooltipResetToZero": "Сбросить до 0",
"tooltipUseLightSensor": "Использовать датчик освещенности",
"tooltipUseCamera": "Использовать камеру",
"tooltipOpenSettings": "Открыть настройки"
}

View file

@ -31,16 +31,19 @@
"thirdStops": "1/3",
"calibration": "校准",
"calibrationMessage": "此应用测量读数的准确性完全取决于设备的硬件。因此,请考虑测试此应用并手动设置 EV 校准,以获得准确的测量结果。",
"calibrationMessageCameraOnly": "此应用程序测量读数的准确性完全取决于设备的后置摄像头。因此,请考虑测试此应用并手动设置 EV 校准,以获得准确的测量结果。",
"calibrationMessageCameraOnly": "此应用程序测量读数的准确s性完全取决于设备的后置摄像头。因此,请考虑测试此应用并手动设置 EV 校准,以获得准确的测量结果。",
"camera": "摄像头",
"lightSensor": "光传感器",
"meteringScreenLayout": "布局",
"meteringScreenLayoutHint": "隐藏不需要的元素,以免浪费曝光列表空间",
"meteringScreenLayoutHintEquipmentProfiles": "设备配置选择",
"meteringScreenFeatureExtremeExposurePairs": "最快 & 最慢曝光组合",
"meteringScreenFeatureFilmPicker": "胶片选择",
"meteringScreenFeatureHistogram": "直方图",
"film": "胶片",
"equipment": "设备",
"filmPush": "胶片 (push)",
"filmPull": "胶片 (pull)",
"filmReciprocityHint": "Applies correction for shutter speeds grater than 1 second",
"equipmentProfileName": "设备配置名称",
"equipmentProfileNameHint": "Praktica MTL5B",
"equipmentProfileAllValues": "全部",
@ -54,6 +57,9 @@
"isoValuesFilterDescription": "选择要显示的 ISO。这些值可能是您最常用的值也可能是相机支持的值。",
"equipmentProfile": "设备配置",
"equipmentProfiles": "设备配置",
"tapToAdd": "點擊添加",
"filmsInUse": "Films in use",
"filmsInUseDescription": "Select films which you use.",
"general": "通用",
"keepScreenOn": "保持屏幕常亮",
"haptics": "震动",
@ -85,5 +91,21 @@
"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",
"tooltipAdd": "Add",
"tooltipClose": "Close",
"tooltipExpand": "Expand",
"tooltipCollapse": "Collapse",
"tooltipCopy": "Copy",
"tooltipDelete": "Delete",
"tooltipSelectAll": "Select all",
"tooltipDesecelectAll": "Deselect all",
"resetToZero": "Reset to zero",
"tooltipUseLightSensor": "Use lightsensor",
"tooltipUseCamera": "Use camera",
"tooltipOpenSettings": "Open settings"
}

View file

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

10
lib/main_release.dart Normal file
View file

@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/application.dart';
import 'package:lightmeter/environment.dart';
import 'package:lightmeter/firebase.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await initializeFirebase(handleErrors: false);
runApp(const Application(Environment.prod()));
}

View file

@ -1,77 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/caffeine_service.dart';
import 'package:lightmeter/data/haptics_service.dart';
import 'package:lightmeter/data/light_sensor_service.dart';
import 'package:lightmeter/data/permissions_service.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/data/volume_events_service.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/metering_screen_layout_provider.dart';
import 'package:lightmeter/providers/stop_type_provider.dart';
import 'package:lightmeter/providers/supported_locale_provider.dart';
import 'package:lightmeter/providers/theme_provider.dart';
import 'package:lightmeter/utils/inherited_generics.dart';
import 'package:platform/platform.dart';
import 'package:shared_preferences/shared_preferences.dart';
class LightmeterProviders extends StatelessWidget {
final Environment env;
final Widget Function(BuildContext context, bool ready) builder;
const LightmeterProviders({required this.env, required this.builder, super.key});
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: Future.wait([
SharedPreferences.getInstance(),
const LightSensorService(LocalPlatform()).hasSensor(),
]),
builder: (_, snapshot) {
if (snapshot.data != null) {
return InheritedWidgetBase<Environment>(
data: env.copyWith(hasLightSensor: snapshot.data![1] as bool),
child: InheritedWidgetBase<UserPreferencesService>(
data: UserPreferencesService(snapshot.data![0] as SharedPreferences),
child: InheritedWidgetBase<LightSensorService>(
data: const LightSensorService(LocalPlatform()),
child: InheritedWidgetBase<CaffeineService>(
data: const CaffeineService(),
child: InheritedWidgetBase<HapticsService>(
data: const HapticsService(),
child: InheritedWidgetBase<VolumeEventsService>(
data: const VolumeEventsService(LocalPlatform()),
child: InheritedWidgetBase<PermissionsService>(
data: const PermissionsService(),
child: MeteringScreenLayoutProvider(
child: StopTypeProvider(
child: EquipmentProfileProvider(
child: EvSourceTypeProvider(
child: SupportedLocaleProvider(
child: ThemeProvider(
child: Builder(
builder: (context) => builder(context, true),
),
),
),
),
),
),
),
),
),
),
),
),
),
);
} else if (snapshot.error != null) {
return Center(child: Text(snapshot.error!.toString()));
}
return builder(context, false);
},
);
}
}

View file

@ -1,100 +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<EquipmentProfile>;
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,
);
List<EquipmentProfile> _customProfiles = [];
String _selectedId = '';
EquipmentProfile 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<EquipmentProfile>>(
data: [_defaultProfile] + _customProfiles,
child: InheritedWidgetBase<EquipmentProfile>(
data: _selectedProfile,
child: widget.child,
),
);
}
void setProfile(EquipmentProfile data) {
setState(() {
_selectedId = data.id;
});
context.get<UserPreferencesService>().selectedEquipmentProfileId = _selectedProfile.id;
}
/// Creates a default equipment profile
void addProfile(String name) {
_customProfiles.add(
EquipmentProfile(
id: const Uuid().v1(),
name: name,
apertureValues: ApertureValue.values,
ndValues: NdValue.values,
shutterSpeedValues: ShutterSpeedValue.values,
isoValues: IsoValue.values,
),
);
_refreshSavedProfiles();
}
void updateProdile(EquipmentProfile data) {
final indexToUpdate = _customProfiles.indexWhere((element) => element.id == data.id);
if (indexToUpdate >= 0) {
_customProfiles[indexToUpdate] = data;
_refreshSavedProfiles();
}
}
void deleteProfile(EquipmentProfile data) {
_customProfiles.remove(data);
_refreshSavedProfiles();
}
void _refreshSavedProfiles() {
context.get<UserPreferencesService>().equipmentProfiles = _customProfiles;
setState(() {});
}
}

View file

@ -1,63 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/ev_source_type.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/environment.dart';
import 'package:lightmeter/utils/inherited_generics.dart';
class EvSourceTypeProvider extends StatefulWidget {
final Widget child;
const EvSourceTypeProvider({required this.child, super.key});
static EvSourceTypeProviderState of(BuildContext context) {
return context.findAncestorStateOfType<EvSourceTypeProviderState>()!;
}
@override
State<EvSourceTypeProvider> createState() => EvSourceTypeProviderState();
}
class EvSourceTypeProviderState extends State<EvSourceTypeProvider> {
late final ValueNotifier<EvSourceType> valueListenable;
@override
void initState() {
super.initState();
final evSourceType = context.get<UserPreferencesService>().evSourceType;
valueListenable = ValueNotifier(
evSourceType == EvSourceType.sensor && !context.get<Environment>().hasLightSensor
? EvSourceType.camera
: evSourceType,
);
}
@override
void dispose() {
valueListenable.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: valueListenable,
builder: (_, value, child) => InheritedWidgetBase<EvSourceType>(
data: value,
child: child!,
),
child: widget.child,
);
}
void toggleType() {
switch (valueListenable.value) {
case EvSourceType.camera:
if (context.get<Environment>().hasLightSensor) {
valueListenable.value = EvSourceType.sensor;
}
case EvSourceType.sensor:
valueListenable.value = EvSourceType.camera;
}
context.get<UserPreferencesService>().evSourceType = valueListenable.value;
}
}

View file

@ -1,60 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/utils/inherited_generics.dart';
class MeteringScreenLayoutProvider extends StatefulWidget {
final Widget child;
const MeteringScreenLayoutProvider({required this.child, super.key});
static MeteringScreenLayoutProviderState of(BuildContext context) {
return context.findAncestorStateOfType<MeteringScreenLayoutProviderState>()!;
}
@override
State<MeteringScreenLayoutProvider> createState() => MeteringScreenLayoutProviderState();
}
class MeteringScreenLayoutProviderState extends State<MeteringScreenLayoutProvider> {
late final MeteringScreenLayoutConfig _config =
context.get<UserPreferencesService>().meteringScreenLayout;
@override
Widget build(BuildContext context) {
return InheritedModelBase<MeteringScreenLayoutFeature, bool>(
data: MeteringScreenLayoutConfig.from(_config),
child: widget.child,
);
}
void updateFeatures(MeteringScreenLayoutConfig config) {
setState(() {
config.forEach((key, value) {
_config.update(
key,
(_) => value,
ifAbsent: () => value,
);
});
});
context.get<UserPreferencesService>().meteringScreenLayout = _config;
}
}
typedef _MeteringScreenLayoutModel = InheritedModelBase<MeteringScreenLayoutFeature, bool>;
extension MeteringScreenLayout on InheritedModelBase<MeteringScreenLayoutFeature, bool> {
static MeteringScreenLayoutConfig of(BuildContext context, {bool listen = true}) {
if (listen) {
return context.dependOnInheritedWidgetOfExactType<_MeteringScreenLayoutModel>()!.data;
} else {
return context.findAncestorWidgetOfExactType<_MeteringScreenLayoutModel>()!.data;
}
}
static bool featureOf(BuildContext context, MeteringScreenLayoutFeature aspect) {
return InheritedModel.inheritFrom<_MeteringScreenLayoutModel>(context, aspect: aspect)!
.data[aspect]!;
}
}

View file

@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/caffeine_service.dart';
import 'package:lightmeter/data/haptics_service.dart';
import 'package:lightmeter/data/light_sensor_service.dart';
import 'package:lightmeter/data/permissions_service.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/data/volume_events_service.dart';
import 'package:lightmeter/environment.dart';
class ServicesProvider extends InheritedWidget {
final CaffeineService caffeineService;
final Environment environment;
final HapticsService hapticsService;
final LightSensorService lightSensorService;
final PermissionsService permissionsService;
final UserPreferencesService userPreferencesService;
final VolumeEventsService volumeEventsService;
const ServicesProvider({
required this.caffeineService,
required this.environment,
required this.hapticsService,
required this.lightSensorService,
required this.permissionsService,
required this.userPreferencesService,
required this.volumeEventsService,
required super.child,
});
static ServicesProvider of(BuildContext context) {
return context.findAncestorWidgetOfExactType<ServicesProvider>()!;
}
@override
bool updateShouldNotify(ServicesProvider oldWidget) => false;
}

View file

@ -1,42 +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';
class StopTypeProvider extends StatefulWidget {
final Widget child;
const StopTypeProvider({required this.child, super.key});
static StopTypeProviderState of(BuildContext context) {
return context.findAncestorStateOfType<StopTypeProviderState>()!;
}
@override
State<StopTypeProvider> createState() => StopTypeProviderState();
}
class StopTypeProviderState extends State<StopTypeProvider> {
late StopType _stopType;
@override
void initState() {
super.initState();
_stopType = context.get<UserPreferencesService>().stopType;
}
@override
Widget build(BuildContext context) {
return InheritedWidgetBase<StopType>(
data: _stopType,
child: widget.child,
);
}
void set(StopType type) {
setState(() {
_stopType = type;
});
context.get<UserPreferencesService>().stopType = type;
}
}

View file

@ -1,53 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/supported_locale.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/utils/inherited_generics.dart';
class SupportedLocaleProvider extends StatefulWidget {
final Widget child;
const SupportedLocaleProvider({required this.child, super.key});
static SupportedLocaleProviderState of(BuildContext context) {
return context.findAncestorStateOfType<SupportedLocaleProviderState>()!;
}
@override
State<SupportedLocaleProvider> createState() => SupportedLocaleProviderState();
}
class SupportedLocaleProviderState extends State<SupportedLocaleProvider> {
late final ValueNotifier<SupportedLocale> valueListenable;
@override
void initState() {
super.initState();
valueListenable = ValueNotifier(context.get<UserPreferencesService>().locale);
}
@override
void dispose() {
valueListenable.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: valueListenable,
builder: (_, value, child) => InheritedWidgetBase<SupportedLocale>(
data: value,
child: child!,
),
child: widget.child,
);
}
void setLocale(SupportedLocale locale) {
S.load(Locale(locale.intlName)).then((value) {
valueListenable.value = locale;
context.get<UserPreferencesService>().locale = locale;
});
}
}

View file

@ -1,257 +0,0 @@
import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:lightmeter/data/models/dynamic_colors_state.dart';
import 'package:lightmeter/data/models/theme_type.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/utils/inherited_generics.dart';
import 'package:material_color_utilities/material_color_utilities.dart';
class ThemeProvider extends StatefulWidget {
final Widget child;
const ThemeProvider({
required this.child,
super.key,
});
static ThemeProviderState of(BuildContext context) {
return context.findAncestorStateOfType<ThemeProviderState>()!;
}
static const primaryColorsList = [
Color(0xfff44336),
Color(0xffe91e63),
Color(0xff9c27b0),
Color(0xff673ab7),
Color(0xff3f51b5),
Color(0xff2196f3),
Color(0xff03a9f4),
Color(0xff00bcd4),
Color(0xff009688),
Color(0xff4caf50),
Color(0xff8bc34a),
Color(0xffcddc39),
Color(0xffffeb3b),
Color(0xffffc107),
Color(0xffff9800),
Color(0xffff5722),
];
@override
State<ThemeProvider> createState() => ThemeProviderState();
}
class ThemeProviderState extends State<ThemeProvider> with WidgetsBindingObserver {
UserPreferencesService get _prefs => context.get<UserPreferencesService>();
late final _themeTypeNotifier = ValueNotifier<ThemeType>(_prefs.themeType);
late final _dynamicColorNotifier = ValueNotifier<bool>(_prefs.dynamicColor);
late final _primaryColorNotifier = ValueNotifier<Color>(_prefs.primaryColor);
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void didChangePlatformBrightness() {
super.didChangePlatformBrightness();
setState(() {});
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_themeTypeNotifier.dispose();
_dynamicColorNotifier.dispose();
_primaryColorNotifier.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: _themeTypeNotifier,
builder: (_, themeType, __) => InheritedWidgetBase<ThemeType>(
data: themeType,
child: ValueListenableBuilder(
valueListenable: _dynamicColorNotifier,
builder: (_, useDynamicColor, __) => _DynamicColorProvider(
useDynamicColor: useDynamicColor,
themeBrightness: _themeBrightness,
builder: (_, dynamicPrimaryColor) => ValueListenableBuilder(
valueListenable: _primaryColorNotifier,
builder: (_, primaryColor, __) => _ThemeDataProvider(
primaryColor: dynamicPrimaryColor ?? primaryColor,
brightness: _themeBrightness,
child: widget.child,
),
),
),
),
),
);
}
void setThemeType(ThemeType themeType) {
_themeTypeNotifier.value = themeType;
_prefs.themeType = themeType;
}
Brightness get _themeBrightness {
switch (_themeTypeNotifier.value) {
case ThemeType.light:
return Brightness.light;
case ThemeType.dark:
return Brightness.dark;
case ThemeType.systemDefault:
return SchedulerBinding.instance.platformDispatcher.platformBrightness;
}
}
void setPrimaryColor(Color color) {
_primaryColorNotifier.value = color;
_prefs.primaryColor = color;
}
void enableDynamicColor(bool enable) {
_dynamicColorNotifier.value = enable;
_prefs.dynamicColor = enable;
}
}
class _DynamicColorProvider extends StatelessWidget {
final bool useDynamicColor;
final Brightness themeBrightness;
final Widget Function(BuildContext context, Color? primaryColor) builder;
const _DynamicColorProvider({
required this.useDynamicColor,
required this.themeBrightness,
required this.builder,
});
@override
Widget build(BuildContext context) {
return DynamicColorBuilder(
builder: (lightDynamic, darkDynamic) {
late final DynamicColorState state;
late final Color? dynamicPrimaryColor;
if (lightDynamic != null && darkDynamic != null) {
if (useDynamicColor) {
dynamicPrimaryColor =
(themeBrightness == Brightness.light ? lightDynamic : darkDynamic).primary;
state = DynamicColorState.enabled;
} else {
dynamicPrimaryColor = null;
state = DynamicColorState.disabled;
}
} else {
dynamicPrimaryColor = null;
state = DynamicColorState.unavailable;
}
return InheritedWidgetBase<DynamicColorState>(
data: state,
child: builder(context, dynamicPrimaryColor),
);
},
);
}
}
class _ThemeDataProvider extends StatelessWidget {
final Color primaryColor;
final Brightness brightness;
final Widget child;
const _ThemeDataProvider({
required this.primaryColor,
required this.brightness,
required this.child,
});
@override
Widget build(BuildContext context) {
return InheritedWidgetBase<ThemeData>(
data: _themeFromColorScheme(_colorSchemeFromColor()),
child: child,
);
}
ThemeData _themeFromColorScheme(ColorScheme scheme) {
return ThemeData(
useMaterial3: true,
brightness: scheme.brightness,
primaryColor: primaryColor,
colorScheme: scheme,
appBarTheme: AppBarTheme(
elevation: 4,
color: scheme.surface,
surfaceTintColor: scheme.surfaceTint,
),
cardTheme: CardTheme(
clipBehavior: Clip.antiAlias,
color: scheme.surface,
elevation: 4,
margin: EdgeInsets.zero,
shadowColor: Colors.transparent,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(Dimens.borderRadiusL)),
surfaceTintColor: scheme.surfaceTint,
),
dialogBackgroundColor: scheme.surface,
dialogTheme: DialogTheme(
backgroundColor: scheme.surface,
surfaceTintColor: scheme.surfaceTint,
elevation: 6,
),
dividerColor: scheme.outlineVariant,
dividerTheme: DividerThemeData(
color: scheme.outlineVariant,
space: 0,
),
listTileTheme: ListTileThemeData(
style: ListTileStyle.list,
iconColor: scheme.onSurface,
textColor: scheme.onSurface,
),
scaffoldBackgroundColor: scheme.surface,
);
}
ColorScheme _colorSchemeFromColor() {
final scheme = brightness == Brightness.light
? Scheme.light(primaryColor.value)
: Scheme.dark(primaryColor.value);
return ColorScheme(
brightness: brightness,
background: Color(scheme.background),
error: Color(scheme.error),
errorContainer: Color(scheme.errorContainer),
onBackground: Color(scheme.onBackground),
onError: Color(scheme.onError),
onErrorContainer: Color(scheme.onErrorContainer),
primary: Color(scheme.primary),
onPrimary: Color(scheme.onPrimary),
primaryContainer: Color(scheme.primaryContainer),
onPrimaryContainer: Color(scheme.onPrimaryContainer),
secondary: Color(scheme.secondary),
onSecondary: Color(scheme.onSecondary),
surface: Color.alphaBlend(
Color(scheme.primary).withOpacity(0.05),
Color(scheme.background),
),
onSurface: Color(scheme.onSurface),
surfaceVariant: Color.alphaBlend(
Color(scheme.primary).withOpacity(0.5),
Color(scheme.background),
),
onSurfaceVariant: Color(scheme.onSurfaceVariant),
outline: Color(scheme.outline),
outlineVariant: Color(scheme.outlineVariant),
);
}
}

View file

@ -0,0 +1,295 @@
import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:lightmeter/data/models/dynamic_colors_state.dart';
import 'package:lightmeter/data/models/ev_source_type.dart';
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
import 'package:lightmeter/data/models/supported_locale.dart';
import 'package:lightmeter/data/models/theme_type.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/providers/services_provider.dart';
import 'package:lightmeter/res/theme.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class UserPreferencesProvider extends StatefulWidget {
final Widget child;
const UserPreferencesProvider({required this.child, super.key});
static _UserPreferencesProviderState of(BuildContext context) {
return context.findAncestorStateOfType<_UserPreferencesProviderState>()!;
}
static DynamicColorState dynamicColorStateOf(BuildContext context) {
return _inheritFromEnumsModel(context, _Aspect.dynamicColorState).dynamicColorState;
}
static EvSourceType evSourceTypeOf(BuildContext context) {
return _inheritFromEnumsModel(context, _Aspect.evSourceType).evSourceType;
}
static SupportedLocale localeOf(BuildContext context) {
return _inheritFromEnumsModel(context, _Aspect.locale).locale;
}
static MeteringScreenLayoutConfig meteringScreenConfigOf(BuildContext context) {
return context.findAncestorWidgetOfExactType<_MeteringScreenLayoutModel>()!.data;
}
static bool meteringScreenFeatureOf(BuildContext context, MeteringScreenLayoutFeature feature) {
return InheritedModel.inheritFrom<_MeteringScreenLayoutModel>(context, aspect: feature)!
.data[feature]!;
}
static StopType stopTypeOf(BuildContext context) {
return _inheritFromEnumsModel(context, _Aspect.stopType).stopType;
}
static ThemeData themeOf(BuildContext context) {
return _inheritFromEnumsModel(context, _Aspect.theme).theme;
}
static ThemeType themeTypeOf(BuildContext context) {
return _inheritFromEnumsModel(context, _Aspect.themeType).themeType;
}
static _UserPreferencesModel _inheritFromEnumsModel(
BuildContext context,
_Aspect aspect,
) {
return InheritedModel.inheritFrom<_UserPreferencesModel>(context, aspect: aspect)!;
}
@override
State<UserPreferencesProvider> createState() => _UserPreferencesProviderState();
}
class _UserPreferencesProviderState extends State<UserPreferencesProvider>
with WidgetsBindingObserver {
UserPreferencesService get userPreferencesService =>
ServicesProvider.of(context).userPreferencesService;
late bool dynamicColor = userPreferencesService.dynamicColor;
late EvSourceType evSourceType;
late MeteringScreenLayoutConfig meteringScreenLayout =
userPreferencesService.meteringScreenLayout;
late Color primaryColor = userPreferencesService.primaryColor;
late StopType stopType = userPreferencesService.stopType;
late SupportedLocale locale = userPreferencesService.locale;
late ThemeType themeType = userPreferencesService.themeType;
@override
void initState() {
super.initState();
evSourceType = userPreferencesService.evSourceType;
evSourceType = evSourceType == EvSourceType.sensor &&
!ServicesProvider.of(context).environment.hasLightSensor
? EvSourceType.camera
: evSourceType;
WidgetsBinding.instance.addObserver(this);
}
@override
void didChangePlatformBrightness() {
super.didChangePlatformBrightness();
setState(() {});
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
Widget build(BuildContext context) {
return DynamicColorBuilder(
builder: (lightDynamic, darkDynamic) {
late final DynamicColorState state;
late final Color? dynamicPrimaryColor;
if (lightDynamic != null && darkDynamic != null) {
if (dynamicColor) {
dynamicPrimaryColor =
(_themeBrightness == Brightness.light ? lightDynamic : darkDynamic).primary;
state = DynamicColorState.enabled;
} else {
dynamicPrimaryColor = null;
state = DynamicColorState.disabled;
}
} else {
dynamicPrimaryColor = null;
state = DynamicColorState.unavailable;
}
return _UserPreferencesModel(
brightness: _themeBrightness,
dynamicColorState: state,
evSourceType: evSourceType,
locale: locale,
primaryColor: dynamicPrimaryColor ?? primaryColor,
stopType: stopType,
themeType: themeType,
child: _MeteringScreenLayoutModel(
data: meteringScreenLayout,
child: widget.child,
),
);
},
);
}
void enableDynamicColor(bool enable) {
setState(() {
dynamicColor = enable;
});
userPreferencesService.dynamicColor = enable;
}
void toggleEvSourceType() {
if (!ServicesProvider.of(context).environment.hasLightSensor) {
return;
}
setState(() {
switch (evSourceType) {
case EvSourceType.camera:
evSourceType = EvSourceType.sensor;
case EvSourceType.sensor:
evSourceType = EvSourceType.camera;
}
});
userPreferencesService.evSourceType = evSourceType;
}
void setLocale(SupportedLocale locale) {
S.load(Locale(locale.intlName)).then((value) {
setState(() {
this.locale = locale;
});
userPreferencesService.locale = locale;
});
}
void setMeteringScreenLayout(MeteringScreenLayoutConfig config) {
setState(() {
meteringScreenLayout = config;
});
userPreferencesService.meteringScreenLayout = meteringScreenLayout;
}
void setPrimaryColor(Color primaryColor) {
setState(() {
this.primaryColor = primaryColor;
});
userPreferencesService.primaryColor = primaryColor;
}
void setStopType(StopType stopType) {
setState(() {
this.stopType = stopType;
});
userPreferencesService.stopType = stopType;
}
void setThemeType(ThemeType themeType) {
setState(() {
this.themeType = themeType;
});
userPreferencesService.themeType = themeType;
}
Brightness get _themeBrightness {
switch (themeType) {
case ThemeType.light:
return Brightness.light;
case ThemeType.dark:
return Brightness.dark;
case ThemeType.systemDefault:
return SchedulerBinding.instance.platformDispatcher.platformBrightness;
}
}
}
enum _Aspect {
dynamicColorState,
evSourceType,
locale,
stopType,
theme,
themeType,
}
class _UserPreferencesModel extends InheritedModel<_Aspect> {
final DynamicColorState dynamicColorState;
final EvSourceType evSourceType;
final SupportedLocale locale;
final StopType stopType;
final ThemeType themeType;
final Brightness _brightness;
final Color _primaryColor;
const _UserPreferencesModel({
required Brightness brightness,
required this.dynamicColorState,
required this.evSourceType,
required this.locale,
required Color primaryColor,
required this.stopType,
required this.themeType,
required super.child,
}) : _brightness = brightness,
_primaryColor = primaryColor;
ThemeData get theme => themeFrom(_primaryColor, _brightness);
@override
bool updateShouldNotify(_UserPreferencesModel oldWidget) {
return _brightness != oldWidget._brightness ||
dynamicColorState != oldWidget.dynamicColorState ||
evSourceType != oldWidget.evSourceType ||
locale != oldWidget.locale ||
_primaryColor != oldWidget._primaryColor ||
stopType != oldWidget.stopType ||
themeType != oldWidget.themeType;
}
@override
bool updateShouldNotifyDependent(
_UserPreferencesModel oldWidget,
Set<_Aspect> dependencies,
) {
return (dependencies.contains(_Aspect.dynamicColorState) &&
dynamicColorState != oldWidget.dynamicColorState) ||
(dependencies.contains(_Aspect.evSourceType) && evSourceType != oldWidget.evSourceType) ||
(dependencies.contains(_Aspect.locale) && locale != oldWidget.locale) ||
(dependencies.contains(_Aspect.stopType) && stopType != oldWidget.stopType) ||
(dependencies.contains(_Aspect.theme) &&
(_brightness != oldWidget._brightness || _primaryColor != oldWidget._primaryColor)) ||
(dependencies.contains(_Aspect.themeType) && themeType != oldWidget.themeType);
}
}
class _MeteringScreenLayoutModel extends InheritedModel<MeteringScreenLayoutFeature> {
final Map<MeteringScreenLayoutFeature, bool> data;
const _MeteringScreenLayoutModel({
required this.data,
required super.child,
});
@override
bool updateShouldNotify(_MeteringScreenLayoutModel oldWidget) => oldWidget.data != data;
@override
bool updateShouldNotifyDependent(
_MeteringScreenLayoutModel oldWidget,
Set<MeteringScreenLayoutFeature> dependencies,
) {
for (final dependecy in dependencies) {
if (oldWidget.data[dependecy] != data[dependecy]) {
return true;
}
}
return false;
}
}

View file

@ -14,7 +14,6 @@ class Dimens {
static const double grid48 = 48;
static const double grid56 = 56;
static const double grid72 = 72;
static const double grid168 = 168;
static const double paddingS = 8;
static const double paddingM = 16;
@ -30,6 +29,8 @@ class Dimens {
static const double enabledOpacity = 1.0;
static const double disabledOpacity = 0.38;
static const double sliverAppBarExpandedHeight = 168;
// TopBar
static const double readingContainerDoubleValueHeight = 128;
static const double readingContainerSingleValueHeight = 76;

97
lib/res/theme.dart Normal file
View file

@ -0,0 +1,97 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:material_color_utilities/material_color_utilities.dart';
const primaryColorsList = [
Color(0xfff44336),
Color(0xffe91e63),
Color(0xff9c27b0),
Color(0xff673ab7),
Color(0xff3f51b5),
Color(0xff2196f3),
Color(0xff03a9f4),
Color(0xff00bcd4),
Color(0xff009688),
Color(0xff4caf50),
Color(0xff8bc34a),
Color(0xffcddc39),
Color(0xffffeb3b),
Color(0xffffc107),
Color(0xffff9800),
Color(0xffff5722),
];
ThemeData themeFrom(Color primaryColor, Brightness brightness) {
final scheme = _colorSchemeFromColor(primaryColor, brightness);
return ThemeData(
useMaterial3: true,
brightness: scheme.brightness,
primaryColor: primaryColor,
colorScheme: scheme,
appBarTheme: AppBarTheme(
elevation: 4,
color: scheme.surface,
surfaceTintColor: scheme.surfaceTint,
),
cardTheme: CardTheme(
clipBehavior: Clip.antiAlias,
color: scheme.surface,
elevation: 4,
margin: EdgeInsets.zero,
shadowColor: Colors.transparent,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(Dimens.borderRadiusL)),
surfaceTintColor: scheme.surfaceTint,
),
dialogBackgroundColor: scheme.surface,
dialogTheme: DialogTheme(
backgroundColor: scheme.surface,
surfaceTintColor: scheme.surfaceTint,
elevation: 6,
),
dividerColor: scheme.outlineVariant,
dividerTheme: DividerThemeData(
color: scheme.outlineVariant,
space: 0,
),
listTileTheme: ListTileThemeData(
style: ListTileStyle.list,
iconColor: scheme.onSurface,
textColor: scheme.onSurface,
),
scaffoldBackgroundColor: scheme.surface,
);
}
ColorScheme _colorSchemeFromColor(Color primaryColor, Brightness brightness) {
final scheme = brightness == Brightness.light
? Scheme.light(primaryColor.value)
: Scheme.dark(primaryColor.value);
return ColorScheme(
brightness: brightness,
background: Color(scheme.background),
error: Color(scheme.error),
errorContainer: Color(scheme.errorContainer),
onBackground: Color(scheme.onBackground),
onError: Color(scheme.onError),
onErrorContainer: Color(scheme.onErrorContainer),
primary: Color(scheme.primary),
onPrimary: Color(scheme.onPrimary),
primaryContainer: Color(scheme.primaryContainer),
onPrimaryContainer: Color(scheme.onPrimaryContainer),
secondary: Color(scheme.secondary),
onSecondary: Color(scheme.onSecondary),
surface: Color.alphaBlend(
Color(scheme.primary).withOpacity(0.05),
Color(scheme.background),
),
onSurface: Color(scheme.onSurface),
surfaceVariant: Color.alphaBlend(
Color(scheme.primary).withOpacity(0.5),
Color(scheme.background),
),
onSurfaceVariant: Color(scheme.onSurfaceVariant),
outline: Color(scheme.outline),
outlineVariant: Color(scheme.outlineVariant),
);
}

View file

@ -3,7 +3,6 @@ import 'dart:async';
import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/data/models/film.dart';
import 'package:lightmeter/data/models/volume_action.dart';
import 'package:lightmeter/interactors/metering_interactor.dart';
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
@ -29,7 +28,6 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
) : super(
MeteringDataState(
ev100: null,
film: _meteringInteractor.film,
iso: _meteringInteractor.iso,
nd: _meteringInteractor.ndFilter,
isMetering: false,
@ -42,7 +40,6 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
.listen(onCommunicationState);
on<EquipmentProfileChangedEvent>(_onEquipmentProfileChanged);
on<FilmChangedEvent>(_onFilmChanged);
on<IsoChangedEvent>(_onIsoChanged);
on<NdChangedEvent>(_onNdChanged);
on<MeasureEvent>(_onMeasure, transformer: droppable());
@ -92,12 +89,9 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
/// Update selected ISO value and discard selected film, if selected equipment profile
/// doesn't contain currently selected value
IsoValue iso = state.iso;
Film film = state.film;
if (!event.equipmentProfileData.isoValues.any((v) => state.iso.value == v.value)) {
_meteringInteractor.iso = event.equipmentProfileData.isoValues.first;
iso = event.equipmentProfileData.isoValues.first;
_meteringInteractor.film = Film.values.first;
film = Film.values.first;
willUpdateMeasurements = true;
}
@ -113,7 +107,6 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
emit(
MeteringDataState(
ev100: state.ev100,
film: film,
iso: iso,
nd: nd,
isMetering: state.isMetering,
@ -122,46 +115,12 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
}
}
void _onFilmChanged(FilmChangedEvent event, Emitter emit) {
if (state.film.name != event.film.name) {
_meteringInteractor.film = event.film;
/// Find `IsoValue` with matching value
IsoValue iso = state.iso;
if (state.iso.value != event.film.iso && event.film != const Film.other()) {
iso = IsoValue.values.firstWhere(
(e) => e.value == event.film.iso,
orElse: () => state.iso,
);
_meteringInteractor.iso = iso;
}
/// If user selects 'Other' film we preserve currently selected ISO
/// and therefore only discard reciprocity formula
emit(
MeteringDataState(
ev100: state.ev100,
film: event.film,
iso: iso,
nd: state.nd,
isMetering: state.isMetering,
),
);
}
}
void _onIsoChanged(IsoChangedEvent event, Emitter emit) {
/// Discard currently selected film even if ISO is the same,
/// because, for example, Fomapan 400 and any Ilford 400
/// have different reciprocity formulas
_meteringInteractor.film = Film.values.first;
if (state.iso != event.isoValue) {
_meteringInteractor.iso = event.isoValue;
emit(
MeteringDataState(
ev100: state.ev100,
film: Film.values.first,
iso: event.isoValue,
nd: state.nd,
isMetering: state.isMetering,
@ -176,7 +135,6 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
emit(
MeteringDataState(
ev100: state.ev100,
film: state.film,
iso: state.iso,
nd: event.ndValue,
isMetering: state.isMetering,
@ -190,7 +148,6 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
_communicationBloc.add(const communication_events.MeasureEvent());
emit(
LoadingState(
film: state.film,
iso: state.iso,
nd: state.nd,
),
@ -209,7 +166,6 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
emit(
MeteringDataState(
ev100: event.ev100,
film: state.film,
iso: state.iso,
nd: state.nd,
isMetering: event.isMetering,
@ -221,7 +177,6 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
emit(
MeteringDataState(
ev100: null,
film: state.film,
iso: state.iso,
nd: state.nd,
isMetering: event.isMetering,

View file

@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/ev_source_type.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/providers/user_preferences_provider.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/metering/components/bottom_controls/components/measure_button/widget_button_measure.dart';
import 'package:lightmeter/utils/inherited_generics.dart';
class MeteringBottomControls extends StatelessWidget {
final double? ev;
@ -37,12 +38,20 @@ class MeteringBottomControls extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
if (onSwitchEvSourceType != null)
_SideIcon(
onPressed: onSwitchEvSourceType!,
icon: Icon(
context.listen<EvSourceType>() != EvSourceType.camera
? Icons.camera_rear
: Icons.wb_incandescent,
Expanded(
child: Center(
child: IconButton(
onPressed: onSwitchEvSourceType,
icon: Icon(
UserPreferencesProvider.evSourceTypeOf(context) != EvSourceType.camera
? Icons.camera_rear
: Icons.wb_incandescent,
),
tooltip:
UserPreferencesProvider.evSourceTypeOf(context) != EvSourceType.camera
? S.of(context).tooltipUseCamera
: S.of(context).tooltipUseLightSensor,
),
),
)
else
@ -52,9 +61,14 @@ class MeteringBottomControls extends StatelessWidget {
isMetering: isMetering,
onTap: onMeasure,
),
_SideIcon(
onPressed: onSettings,
icon: const Icon(Icons.settings),
Expanded(
child: Center(
child: IconButton(
onPressed: onSettings,
icon: const Icon(Icons.settings),
tooltip: S.of(context).tooltipOpenSettings,
),
),
),
],
),
@ -64,27 +78,3 @@ class MeteringBottomControls extends StatelessWidget {
);
}
}
class _SideIcon extends StatelessWidget {
final Icon icon;
final VoidCallback onPressed;
const _SideIcon({
required this.icon,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return Expanded(
child: Center(
child: RepaintBoundary(
child: IconButton(
onPressed: onPressed,
icon: icon,
),
),
),
);
}
}

View file

@ -18,7 +18,7 @@ import 'package:lightmeter/screens/metering/components/camera_container/event_co
import 'package:lightmeter/screens/metering/components/camera_container/models/camera_error_type.dart';
import 'package:lightmeter/screens/metering/components/camera_container/state_container_camera.dart';
import 'package:lightmeter/screens/metering/components/shared/ev_source_base/bloc_base_ev_source.dart';
import 'package:lightmeter/utils/log_2.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraContainerState> {
final MeteringInteractor _meteringInteractor;

View file

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/shared/centered_slider/widget_slider_centered.dart';
import 'package:lightmeter/utils/to_string_signed.dart';
@ -22,6 +23,7 @@ class ExposureOffsetSlider extends StatelessWidget {
IconButton(
icon: const Icon(Icons.sync),
onPressed: value != 0.0 ? () => onChanged(0.0) : null,
tooltip: S.of(context).tooltipResetToZero,
),
Expanded(
child: Row(

View file

@ -2,7 +2,7 @@ import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
import 'package:lightmeter/platform_config.dart';
import 'package:lightmeter/providers/metering_screen_layout_provider.dart';
import 'package:lightmeter/providers/user_preferences_provider.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/metering/components/camera_container/components/camera_preview/components/camera_view/widget_camera_view.dart';
import 'package:lightmeter/screens/metering/components/camera_container/components/camera_preview/components/camera_view_placeholder/widget_placeholder_camera_view.dart';
@ -22,43 +22,77 @@ class CameraPreview extends StatefulWidget {
class _CameraPreviewState extends State<CameraPreview> {
@override
Widget build(BuildContext context) {
return RepaintBoundary(
child: AspectRatio(
aspectRatio: PlatformConfig.cameraPreviewAspectRatio,
child: Center(
child: Stack(
children: [
const CameraViewPlaceholder(error: null),
AnimatedSwitcher(
duration: Dimens.switchDuration,
child: widget.controller != null
? ValueListenableBuilder<CameraValue>(
valueListenable: widget.controller!,
builder: (_, __, ___) => widget.controller!.value.isInitialized
? Stack(
alignment: Alignment.bottomCenter,
children: [
CameraView(controller: widget.controller!),
if (MeteringScreenLayout.featureOf(
context,
MeteringScreenLayoutFeature.histogram,
))
Positioned(
left: Dimens.grid8,
right: Dimens.grid8,
bottom: Dimens.grid16,
child: CameraHistogram(controller: widget.controller!),
),
],
)
: const SizedBox.shrink(),
)
: CameraViewPlaceholder(error: widget.error),
),
],
),
return AspectRatio(
aspectRatio: PlatformConfig.cameraPreviewAspectRatio,
child: Center(
child: Stack(
children: [
const CameraViewPlaceholder(error: null),
AnimatedSwitcher(
duration: Dimens.switchDuration,
child: widget.controller != null
? _CameraPreviewBuilder(controller: widget.controller!)
: CameraViewPlaceholder(error: widget.error),
),
],
),
),
);
}
}
class _CameraPreviewBuilder extends StatefulWidget {
final CameraController controller;
const _CameraPreviewBuilder({required this.controller});
@override
State<_CameraPreviewBuilder> createState() => _CameraPreviewBuilderState();
}
class _CameraPreviewBuilderState extends State<_CameraPreviewBuilder> {
late final ValueNotifier<bool> _initializedNotifier =
ValueNotifier<bool>(widget.controller.value.isInitialized);
@override
void initState() {
super.initState();
widget.controller.addListener(_update);
}
@override
void dispose() {
widget.controller.removeListener(_update);
_initializedNotifier.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<bool>(
valueListenable: _initializedNotifier,
builder: (context, value, child) => value
? Stack(
alignment: Alignment.bottomCenter,
children: [
CameraView(controller: widget.controller),
if (UserPreferencesProvider.meteringScreenFeatureOf(
context,
MeteringScreenLayoutFeature.histogram,
))
Positioned(
left: Dimens.grid8,
right: Dimens.grid8,
bottom: Dimens.grid16,
child: CameraHistogram(controller: widget.controller),
),
],
)
: const SizedBox.shrink(),
);
}
void _update() {
_initializedNotifier.value = widget.controller.value.isInitialized;
}
}

View file

@ -1,22 +1,18 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/data/models/film.dart';
import 'package:lightmeter/interactors/metering_interactor.dart';
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
import 'package:lightmeter/screens/metering/components/camera_container/bloc_container_camera.dart';
import 'package:lightmeter/screens/metering/components/camera_container/event_container_camera.dart';
import 'package:lightmeter/screens/metering/components/camera_container/widget_container_camera.dart';
import 'package:lightmeter/utils/inherited_generics.dart';
import 'package:lightmeter/screens/metering/flow_metering.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class CameraContainerProvider extends StatelessWidget {
final ExposurePair? fastest;
final ExposurePair? slowest;
final Film film;
final IsoValue iso;
final NdValue nd;
final ValueChanged<Film> onFilmChanged;
final ValueChanged<IsoValue> onIsoChanged;
final ValueChanged<NdValue> onNdChanged;
final List<ExposurePair> exposurePairs;
@ -24,10 +20,8 @@ class CameraContainerProvider extends StatelessWidget {
const CameraContainerProvider({
required this.fastest,
required this.slowest,
required this.film,
required this.iso,
required this.nd,
required this.onFilmChanged,
required this.onIsoChanged,
required this.onNdChanged,
required this.exposurePairs,
@ -39,16 +33,14 @@ class CameraContainerProvider extends StatelessWidget {
return BlocProvider(
lazy: false,
create: (context) => CameraContainerBloc(
context.get<MeteringInteractor>(),
MeteringInteractorProvider.of(context),
context.read<MeteringCommunicationBloc>(),
)..add(const RequestPermissionEvent()),
child: CameraContainer(
fastest: fastest,
slowest: slowest,
film: film,
iso: iso,
nd: nd,
onFilmChanged: onFilmChanged,
onIsoChanged: onIsoChanged,
onNdChanged: onNdChanged,
exposurePairs: exposurePairs,

View file

@ -3,11 +3,9 @@ import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/data/models/film.dart';
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
import 'package:lightmeter/features.dart';
import 'package:lightmeter/platform_config.dart';
import 'package:lightmeter/providers/metering_screen_layout_provider.dart';
import 'package:lightmeter/providers/user_preferences_provider.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/metering/components/camera_container/bloc_container_camera.dart';
import 'package:lightmeter/screens/metering/components/camera_container/components/camera_controls/widget_camera_controls.dart';
@ -24,10 +22,8 @@ import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class CameraContainer extends StatelessWidget {
final ExposurePair? fastest;
final ExposurePair? slowest;
final Film film;
final IsoValue iso;
final NdValue nd;
final ValueChanged<Film> onFilmChanged;
final ValueChanged<IsoValue> onIsoChanged;
final ValueChanged<NdValue> onNdChanged;
final List<ExposurePair> exposurePairs;
@ -35,10 +31,8 @@ class CameraContainer extends StatelessWidget {
const CameraContainer({
required this.fastest,
required this.slowest,
required this.film,
required this.iso,
required this.nd,
required this.onFilmChanged,
required this.onIsoChanged,
required this.onNdChanged,
required this.exposurePairs,
@ -61,10 +55,8 @@ class CameraContainer extends StatelessWidget {
readingsContainer: ReadingsContainer(
fastest: fastest,
slowest: slowest,
film: film,
iso: iso,
nd: nd,
onFilmChanged: onFilmChanged,
onIsoChanged: onIsoChanged,
onNdChanged: onNdChanged,
),
@ -110,18 +102,21 @@ class CameraContainer extends StatelessWidget {
double _meteringContainerHeight(BuildContext context) {
double enabledFeaturesHeight = 0;
if (FeaturesConfig.equipmentProfilesEnabled) {
if (UserPreferencesProvider.meteringScreenFeatureOf(
context,
MeteringScreenLayoutFeature.equipmentProfiles,
)) {
enabledFeaturesHeight += Dimens.readingContainerSingleValueHeight;
enabledFeaturesHeight += Dimens.paddingS;
}
if (MeteringScreenLayout.featureOf(
if (UserPreferencesProvider.meteringScreenFeatureOf(
context,
MeteringScreenLayoutFeature.extremeExposurePairs,
)) {
enabledFeaturesHeight += Dimens.readingContainerDoubleValueHeight;
enabledFeaturesHeight += Dimens.paddingS;
}
if (MeteringScreenLayout.featureOf(
if (UserPreferencesProvider.meteringScreenFeatureOf(
context,
MeteringScreenLayoutFeature.filmPicker,
)) {
@ -133,7 +128,7 @@ class CameraContainer extends StatelessWidget {
}
double _cameraPreviewHeight(BuildContext context) {
return ((MediaQuery.of(context).size.width - Dimens.grid8 - 2 * Dimens.paddingM) / 2) /
return ((MediaQuery.sizeOf(context).width - Dimens.grid8 - 2 * Dimens.paddingM) / 2) /
PlatformConfig.cameraPreviewAspectRatio;
}
}

View file

@ -10,7 +10,7 @@ import 'package:lightmeter/screens/metering/communication/state_communication_me
import 'package:lightmeter/screens/metering/components/light_sensor_container/event_container_light_sensor.dart';
import 'package:lightmeter/screens/metering/components/light_sensor_container/state_container_light_sensor.dart';
import 'package:lightmeter/screens/metering/components/shared/ev_source_base/bloc_base_ev_source.dart';
import 'package:lightmeter/utils/log_2.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class LightSensorContainerBloc
extends EvSourceBlocBase<LightSensorContainerEvent, LightSensorContainerState> {

View file

@ -1,21 +1,17 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/data/models/film.dart';
import 'package:lightmeter/interactors/metering_interactor.dart';
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
import 'package:lightmeter/screens/metering/components/light_sensor_container/bloc_container_light_sensor.dart';
import 'package:lightmeter/screens/metering/components/light_sensor_container/widget_container_light_sensor.dart';
import 'package:lightmeter/utils/inherited_generics.dart';
import 'package:lightmeter/screens/metering/flow_metering.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class LightSensorContainerProvider extends StatelessWidget {
final ExposurePair? fastest;
final ExposurePair? slowest;
final Film film;
final IsoValue iso;
final NdValue nd;
final ValueChanged<Film> onFilmChanged;
final ValueChanged<IsoValue> onIsoChanged;
final ValueChanged<NdValue> onNdChanged;
final List<ExposurePair> exposurePairs;
@ -23,10 +19,8 @@ class LightSensorContainerProvider extends StatelessWidget {
const LightSensorContainerProvider({
required this.fastest,
required this.slowest,
required this.film,
required this.iso,
required this.nd,
required this.onFilmChanged,
required this.onIsoChanged,
required this.onNdChanged,
required this.exposurePairs,
@ -38,16 +32,14 @@ class LightSensorContainerProvider extends StatelessWidget {
return BlocProvider(
lazy: false,
create: (context) => LightSensorContainerBloc(
context.get<MeteringInteractor>(),
MeteringInteractorProvider.of(context),
context.read<MeteringCommunicationBloc>(),
),
child: LightSensorContainer(
fastest: fastest,
slowest: slowest,
film: film,
iso: iso,
nd: nd,
onFilmChanged: onFilmChanged,
onIsoChanged: onIsoChanged,
onNdChanged: onNdChanged,
exposurePairs: exposurePairs,

View file

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/data/models/film.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart';
import 'package:lightmeter/screens/metering/components/shared/metering_top_bar/widget_top_bar_metering.dart';
@ -10,10 +9,8 @@ import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class LightSensorContainer extends StatelessWidget {
final ExposurePair? fastest;
final ExposurePair? slowest;
final Film film;
final IsoValue iso;
final NdValue nd;
final ValueChanged<Film> onFilmChanged;
final ValueChanged<IsoValue> onIsoChanged;
final ValueChanged<NdValue> onNdChanged;
final List<ExposurePair> exposurePairs;
@ -21,10 +18,8 @@ class LightSensorContainer extends StatelessWidget {
const LightSensorContainer({
required this.fastest,
required this.slowest,
required this.film,
required this.iso,
required this.nd,
required this.onFilmChanged,
required this.onIsoChanged,
required this.onNdChanged,
required this.exposurePairs,
@ -39,10 +34,8 @@ class LightSensorContainer extends StatelessWidget {
readingsContainer: ReadingsContainer(
fastest: fastest,
slowest: slowest,
film: film,
iso: iso,
nd: nd,
onFilmChanged: onFilmChanged,
onIsoChanged: onIsoChanged,
onNdChanged: onNdChanged,
),

View file

@ -1,9 +1,11 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/metering/components/shared/exposure_pairs_list/components/empty_exposure_pairs_list/widget_list_exposure_pairs_empty.dart';
import 'package:lightmeter/screens/metering/components/shared/exposure_pairs_list/components/exposure_pairs_list_item/widget_item_list_exposure_pairs.dart';
import 'package:lightmeter/screens/shared/icon_placeholder/widget_icon_placeholder.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
class ExposurePairsList extends StatelessWidget {
final List<ExposurePair> exposurePairs;
@ -15,7 +17,10 @@ class ExposurePairsList extends StatelessWidget {
return AnimatedSwitcher(
duration: Dimens.switchDuration,
child: exposurePairs.isEmpty
? const EmptyExposurePairsList()
? IconPlaceholder(
icon: Icons.not_interested,
text: S.of(context).noExposurePairs,
)
: Stack(
alignment: Alignment.center,
children: [
@ -43,7 +48,8 @@ class ExposurePairsList extends StatelessWidget {
child: Align(
alignment: Alignment.centerLeft,
child: ExposurePairsListItem(
exposurePairs[index].shutterSpeed,
Films.selectedOf(context)
.reciprocityFailure(exposurePairs[index].shutterSpeed),
tickOnTheLeft: true,
),
),

View file

@ -31,10 +31,23 @@ class MeteringTopBarShape extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()..color = color;
final path = Path();
const circularRadius = Radius.circular(Dimens.borderRadiusL);
late final Path path;
if (appendixHeight == 0 || appendixWidth == 0) {
path.addRRect(
path = _drawNoAppendix(size, Dimens.borderRadiusL);
} else if (appendixHeight < 0) {
path = _drawAppendixOnLeft(size, Dimens.borderRadiusL);
canvas.scale(-1, 1);
canvas.translate(-size.width, 0);
} else {
path = _drawAppendixOnLeft(size, Dimens.borderRadiusL);
}
canvas.drawPath(path, paint);
}
Path _drawNoAppendix(Size size, double bottomRadius) {
final circularRadius = Radius.circular(bottomRadius);
return Path()
..addRRect(
RRect.fromLTRBAndCorners(
0,
0,
@ -43,73 +56,51 @@ class MeteringTopBarShape extends CustomPainter {
bottomLeft: circularRadius,
bottomRight: circularRadius,
),
);
} else if (appendixHeight < 0) {
// Left side with bottom corner
path.lineTo(0, size.height + appendixHeight - Dimens.borderRadiusL);
path.arcToPoint(
Offset(Dimens.borderRadiusL, size.height + appendixHeight),
radius: circularRadius,
clockwise: false,
);
)
..close();
}
// Bottom side with step
final allowedRadius = min(appendixHeight.abs() / 2, Dimens.borderRadiusL);
path.lineTo(appendixWidth - allowedRadius, size.height + appendixHeight);
path.arcToPoint(
Offset(appendixWidth, size.height + appendixHeight + allowedRadius),
radius: circularRadius,
);
path.lineTo(appendixWidth, size.height - allowedRadius);
path.arcToPoint(
Offset(appendixWidth + allowedRadius, size.height),
radius: circularRadius,
clockwise: false,
);
Path _drawAppendixOnLeft(Size size, double bottomRadius) {
final path = Path();
final circularRadius = Radius.circular(bottomRadius);
final appendixHeight = this.appendixHeight.abs();
// Right side with bottom corner
path.lineTo(size.width - Dimens.borderRadiusL, size.height);
path.arcToPoint(
Offset(size.width, size.height - Dimens.borderRadiusL),
radius: circularRadius,
clockwise: false,
);
} else {
// Left side with bottom corner
path.lineTo(0, size.height - Dimens.borderRadiusL);
path.arcToPoint(
Offset(Dimens.borderRadiusL, size.height),
radius: circularRadius,
clockwise: false,
);
// Left side with bottom corner
path.lineTo(0, size.height - bottomRadius);
path.arcToPoint(
Offset(bottomRadius, size.height),
radius: circularRadius,
clockwise: false,
);
// Bottom side with step
final allowedRadius = min(appendixHeight.abs() / 2, Dimens.borderRadiusL);
path.relativeLineTo(appendixWidth - allowedRadius * 2, 0);
path.relativeArcToPoint(
Offset(allowedRadius, -allowedRadius),
radius: Radius.circular(allowedRadius),
rotation: 90,
clockwise: false,
);
path.relativeLineTo(0, -appendixHeight + allowedRadius * 2);
path.relativeArcToPoint(
Offset(allowedRadius, -allowedRadius),
radius: Radius.circular(allowedRadius),
rotation: 90,
);
// Bottom side with step
final allowedRadius = min(appendixHeight.abs() / 2, bottomRadius);
path.lineTo(appendixWidth - allowedRadius, size.height);
path.arcToPoint(
Offset(appendixWidth, size.height - allowedRadius),
radius: Radius.circular(allowedRadius),
rotation: 90,
clockwise: false,
);
path.lineTo(appendixWidth, size.height - appendixHeight + allowedRadius);
path.arcToPoint(
Offset(appendixWidth + allowedRadius, size.height - appendixHeight),
radius: Radius.circular(allowedRadius),
rotation: 90,
);
// Right side with bottom corner
path.lineTo(size.width - bottomRadius, size.height - appendixHeight);
path.arcToPoint(
Offset(size.width, size.height - appendixHeight - bottomRadius),
radius: circularRadius,
clockwise: false,
);
// Right side with bottom corner
path.lineTo(size.width - Dimens.borderRadiusL, size.height - appendixHeight);
path.arcToPoint(
Offset(size.width, size.height - appendixHeight - Dimens.borderRadiusL),
radius: circularRadius,
clockwise: false,
);
}
path.lineTo(size.width, 0);
path.close();
canvas.drawPath(path, paint);
return path;
}
@override

View file

@ -20,9 +20,7 @@ class MeteringTopBar extends StatelessWidget {
return CustomPaint(
painter: MeteringTopBarShape(
color: Theme.of(context).colorScheme.surface,
appendixWidth: appendixHeight > 0
? MediaQuery.of(context).size.width / 2 - Dimens.grid8 + Dimens.paddingM
: MediaQuery.of(context).size.width / 2 + Dimens.grid8 - Dimens.paddingM,
appendixWidth: MediaQuery.of(context).size.width / 2 - Dimens.grid8 / 2 + Dimens.paddingM,
appendixHeight: appendixHeight,
),
child: Padding(

View file

@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/widget_picker_dialog_animated.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/reading_value_container/widget_container_reading_value.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class EquipmentProfilePicker extends StatelessWidget {
const EquipmentProfilePicker();
@override
Widget build(BuildContext context) {
return AnimatedDialogPicker<EquipmentProfile>(
icon: Icons.camera,
title: S.of(context).equipmentProfile,
selectedValue: EquipmentProfiles.selectedOf(context),
values: EquipmentProfiles.of(context),
itemTitleBuilder: (_, value) => Text(value.id.isEmpty ? S.of(context).none : value.name),
onChanged: EquipmentProfileProvider.of(context).setProfile,
closedChild: ReadingValueContainer.singleValue(
value: ReadingValue(
label: S.of(context).equipmentProfile,
value: EquipmentProfiles.selectedOf(context).id.isEmpty
? S.of(context).none
: EquipmentProfiles.selectedOf(context).name,
),
),
);
}
}

View file

@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/reading_value_container/widget_container_reading_value.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
class ExtremeExposurePairsContainer extends StatelessWidget {
final ExposurePair? fastest;
final ExposurePair? slowest;
const ExtremeExposurePairsContainer({
required this.fastest,
required this.slowest,
super.key,
});
@override
Widget build(BuildContext context) {
return ReadingValueContainer(
values: [
ReadingValue(
label: S.of(context).fastestExposurePair,
value: _exposurePairToString(context, fastest),
),
ReadingValue(
label: S.of(context).slowestExposurePair,
value: _exposurePairToString(context, slowest),
),
],
);
}
String _exposurePairToString(BuildContext context, ExposurePair? pair) {
if (pair == null) {
return '-';
}
return '${pair.aperture} - ${Films.selectedOf(context).reciprocityFailure(pair.shutterSpeed)}';
}
}

View file

@ -0,0 +1,53 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/widget_picker_dialog_animated.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/reading_value_container/widget_container_reading_value.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class FilmPicker extends StatelessWidget {
final IsoValue selectedIso;
const FilmPicker({required this.selectedIso});
@override
Widget build(BuildContext context) {
return AnimatedDialogPicker<Film>(
icon: Icons.camera_roll,
title: S.of(context).film,
subtitle: S.of(context).filmReciprocityHint,
selectedValue: Films.selectedOf(context),
values: Films.inUseOf(context),
itemTitleBuilder: (_, value) => Text(value.name.isEmpty ? S.of(context).none : value.name),
onChanged: FilmsProvider.of(context).setFilm,
closedChild: ReadingValueContainer.singleValue(
value: ReadingValue(
label: _label(context),
value: Films.selectedOf(context).name.isEmpty
? S.of(context).none
: Films.selectedOf(context).name,
),
),
);
}
String _label(BuildContext context) {
if (Films.selectedOf(context) == const Film.other() ||
Films.selectedOf(context).iso == selectedIso.value) {
return S.of(context).film;
}
final evDiff = IsoValue(
Films.selectedOf(context).iso,
StopType.full,
).difference(selectedIso);
if (evDiff > 0) {
return S.of(context).filmPush;
} else if (evDiff < 0) {
return S.of(context).filmPull;
} else {
return S.of(context).film;
}
}
}

View file

@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/widget_picker_dialog_animated.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/reading_value_container/widget_container_reading_value.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class IsoValuePicker extends StatelessWidget {
final List<IsoValue> values;
final IsoValue selectedValue;
final ValueChanged<IsoValue> onChanged;
const IsoValuePicker({
required this.selectedValue,
required this.values,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return AnimatedDialogPicker<IsoValue>(
icon: Icons.iso,
title: S.of(context).iso,
subtitle: S.of(context).filmSpeed,
selectedValue: selectedValue,
values: values,
itemTitleBuilder: (_, value) => Text(value.value.toString()),
// using ascending order, because increase in film speed rises EV
itemTrailingBuilder: (selected, value) => value.value != selected.value
? Text(S.of(context).evValue(selected.toStringDifference(value)))
: null,
onChanged: onChanged,
closedChild: ReadingValueContainer.singleValue(
value: ReadingValue(
label: S.of(context).iso,
value: selectedValue.value.toString(),
),
),
);
}
}

View file

@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/widget_picker_dialog_animated.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/reading_value_container/widget_container_reading_value.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class NdValuePicker extends StatelessWidget {
final List<NdValue> values;
final NdValue selectedValue;
final ValueChanged<NdValue> onChanged;
const NdValuePicker({
required this.selectedValue,
required this.values,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return AnimatedDialogPicker<NdValue>(
icon: Icons.filter_b_and_w,
title: S.of(context).nd,
subtitle: S.of(context).ndFilterFactor,
selectedValue: selectedValue,
values: values,
itemTitleBuilder: (_, value) => Text(
value.value == 0 ? S.of(context).none : value.value.toString(),
),
// using descending order, because ND filter darkens image & lowers EV
itemTrailingBuilder: (selected, value) => value.value != selected.value
? Text(S.of(context).evValue(value.toStringDifference(selected)))
: null,
onChanged: onChanged,
closedChild: ReadingValueContainer.singleValue(
value: ReadingValue(
label: S.of(context).nd,
value: selectedValue.value.toString(),
),
),
);
}
}

View file

@ -22,9 +22,15 @@ class AnimatedDialog extends StatefulWidget {
class AnimatedDialogState extends State<AnimatedDialog> with SingleTickerProviderStateMixin {
final GlobalKey _key = GlobalKey();
late Size _closedSize;
late Offset _closedOffset;
late final AnimationController _animationController;
late final CurvedAnimation _defaultCurvedAnimation;
late final Animation<Color?> _barrierColorAnimation;
late SizeTween _sizeTween;
late Animation<Size?> _sizeAnimation;
late Animation<Size?> _offsetAnimation;
late final Animation<double> _borderRadiusAnimation;
late final Animation<double> _closedOpacityAnimation;
late final Animation<double> _openedOpacityAnimation;
@ -81,6 +87,8 @@ class AnimatedDialogState extends State<AnimatedDialog> with SingleTickerProvide
),
),
);
_setClosedOffset();
}
@override
@ -97,6 +105,12 @@ class AnimatedDialogState extends State<AnimatedDialog> with SingleTickerProvide
).animate(_defaultCurvedAnimation);
}
@override
void didUpdateWidget(covariant AnimatedDialog oldWidget) {
super.didUpdateWidget(oldWidget);
_setClosedOffset();
}
@override
void dispose() {
_animationController.dispose();
@ -115,6 +129,38 @@ class AnimatedDialogState extends State<AnimatedDialog> with SingleTickerProvide
);
}
void _setClosedOffset() {
WidgetsBinding.instance.addPostFrameCallback((_) {
final renderBox = _key.currentContext?.findRenderObject()! as RenderBox?;
if (renderBox != null) {
final size = MediaQuery.sizeOf(context);
final padding = MediaQuery.paddingOf(context);
_closedSize = _key.currentContext!.size!;
_sizeTween = SizeTween(
begin: _closedSize,
end: widget.openedSize ??
Size(
size.width - padding.horizontal - Dimens.dialogMargin.horizontal,
size.height - padding.vertical - Dimens.dialogMargin.vertical,
),
);
_sizeAnimation = _sizeTween.animate(_defaultCurvedAnimation);
_closedOffset = renderBox.localToGlobal(Offset.zero);
_offsetAnimation = SizeTween(
begin: Size(
_closedOffset.dx + _closedSize.width / 2,
_closedOffset.dy + _closedSize.height / 2,
),
end: Size(
size.width / 2,
size.height / 2 + padding.top / 2 - padding.bottom / 2,
),
).animate(_defaultCurvedAnimation);
}
});
}
void _openDialog() {
final mediaQuery = MediaQuery.of(context);
final closedSize = _key.currentContext!.size!;

View file

@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/animated_dialog_picker/components/dialog_picker/widget_picker_dialog.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/dialog_picker/widget_picker_dialog.dart';
// Has to be stateful, so that [GlobalKey] is not recreated.
// Otherwise use will no be able to close the dialog after EV value has changed.

View file

@ -1,34 +1,29 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/data/models/film.dart';
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
import 'package:lightmeter/features.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/user_preferences_provider.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/reading_value_container/widget_container_reading_value.dart';
import 'package:lightmeter/utils/inherited_generics.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/equipment_profile_picker/widget_picker_equipment_profiles.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/extreme_exposure_pairs_container/widget_container_extreme_exposure_pairs.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/film_picker/widget_picker_film.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/iso_picker/widget_picker_iso.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/nd_picker/widget_picker_nd.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class ReadingsContainer extends StatelessWidget {
final ExposurePair? fastest;
final ExposurePair? slowest;
final Film film;
final IsoValue iso;
final NdValue nd;
final ValueChanged<Film> onFilmChanged;
final ValueChanged<IsoValue> onIsoChanged;
final ValueChanged<NdValue> onNdChanged;
const ReadingsContainer({
required this.fastest,
required this.slowest,
required this.film,
required this.iso,
required this.nd,
required this.onFilmChanged,
required this.onIsoChanged,
required this.onNdChanged,
super.key,
@ -39,53 +34,44 @@ class ReadingsContainer extends StatelessWidget {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (FeaturesConfig.equipmentProfilesEnabled) ...[
const _EquipmentProfilePicker(),
if (UserPreferencesProvider.meteringScreenFeatureOf(
context,
MeteringScreenLayoutFeature.equipmentProfiles,
)) ...[
const EquipmentProfilePicker(),
const _InnerPadding(),
],
if (MeteringScreenLayout.featureOf(
if (UserPreferencesProvider.meteringScreenFeatureOf(
context,
MeteringScreenLayoutFeature.extremeExposurePairs,
)) ...[
ReadingValueContainer(
values: [
ReadingValue(
label: S.of(context).fastestExposurePair,
value: fastest != null ? fastest!.toString() : '-',
),
ReadingValue(
label: S.of(context).slowestExposurePair,
value: fastest != null ? slowest!.toString() : '-',
),
],
ExtremeExposurePairsContainer(
fastest: fastest,
slowest: slowest,
),
const _InnerPadding(),
],
if (MeteringScreenLayout.featureOf(
if (UserPreferencesProvider.meteringScreenFeatureOf(
context,
MeteringScreenLayoutFeature.filmPicker,
)) ...[
_FilmPicker(
values: Film.values,
selectedValue: film,
onChanged: onFilmChanged,
),
FilmPicker(selectedIso: iso),
const _InnerPadding(),
],
Row(
children: [
Expanded(
child: _IsoValuePicker(
child: IsoValuePicker(
selectedValue: iso,
values: context.listen<EquipmentProfile>().isoValues,
values: EquipmentProfiles.selectedOf(context).isoValues,
onChanged: onIsoChanged,
),
),
const _InnerPadding(),
Expanded(
child: _NdValuePicker(
child: NdValuePicker(
selectedValue: nd,
values: context.listen<EquipmentProfile>().ndValues,
values: EquipmentProfiles.selectedOf(context).ndValues,
onChanged: onNdChanged,
),
),
@ -99,129 +85,3 @@ class ReadingsContainer extends StatelessWidget {
class _InnerPadding extends SizedBox {
const _InnerPadding() : super(height: Dimens.grid8, width: Dimens.grid8);
}
class _EquipmentProfilePicker extends StatelessWidget {
const _EquipmentProfilePicker();
@override
Widget build(BuildContext context) {
return AnimatedDialogPicker<EquipmentProfile>(
icon: Icons.camera,
title: S.of(context).equipmentProfile,
selectedValue: context.listen<EquipmentProfile>(),
values: context.listen<EquipmentProfiles>(),
itemTitleBuilder: (_, value) => Text(value.id.isEmpty ? S.of(context).none : value.name),
onChanged: EquipmentProfileProvider.of(context).setProfile,
closedChild: ReadingValueContainer.singleValue(
value: ReadingValue(
label: S.of(context).equipmentProfile,
value: context.listen<EquipmentProfile>().id.isEmpty
? S.of(context).none
: context.listen<EquipmentProfile>().name,
),
),
);
}
}
class _FilmPicker extends StatelessWidget {
final List<Film> values;
final Film selectedValue;
final ValueChanged<Film> onChanged;
const _FilmPicker({
required this.values,
required this.selectedValue,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return AnimatedDialogPicker<Film>(
icon: Icons.camera_roll,
title: S.of(context).film,
selectedValue: selectedValue,
values: values,
itemTitleBuilder: (_, value) => Text(value.name.isEmpty ? S.of(context).none : value.name),
onChanged: onChanged,
closedChild: ReadingValueContainer.singleValue(
value: ReadingValue(
label: S.of(context).film,
value: selectedValue.name.isEmpty ? S.of(context).none : selectedValue.name,
),
),
);
}
}
class _IsoValuePicker extends StatelessWidget {
final List<IsoValue> values;
final IsoValue selectedValue;
final ValueChanged<IsoValue> onChanged;
const _IsoValuePicker({
required this.selectedValue,
required this.values,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return AnimatedDialogPicker<IsoValue>(
icon: Icons.iso,
title: S.of(context).iso,
subtitle: S.of(context).filmSpeed,
selectedValue: selectedValue,
values: values,
itemTitleBuilder: (_, value) => Text(value.value.toString()),
// using ascending order, because increase in film speed rises EV
itemTrailingBuilder: (selected, value) => value.value != selected.value
? Text(S.of(context).evValue(selected.toStringDifference(value)))
: null,
onChanged: onChanged,
closedChild: ReadingValueContainer.singleValue(
value: ReadingValue(
label: S.of(context).iso,
value: selectedValue.value.toString(),
),
),
);
}
}
class _NdValuePicker extends StatelessWidget {
final List<NdValue> values;
final NdValue selectedValue;
final ValueChanged<NdValue> onChanged;
const _NdValuePicker({
required this.selectedValue,
required this.values,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return AnimatedDialogPicker<NdValue>(
icon: Icons.filter_b_and_w,
title: S.of(context).nd,
subtitle: S.of(context).ndFilterFactor,
selectedValue: selectedValue,
values: values,
itemTitleBuilder: (_, value) => Text(
value.value == 0 ? S.of(context).none : value.value.toString(),
),
// using descending order, because ND filter darkens image & lowers EV
itemTrailingBuilder: (selected, value) => value.value != selected.value
? Text(S.of(context).evValue(value.toStringDifference(selected)))
: null,
onChanged: onChanged,
closedChild: ReadingValueContainer.singleValue(
value: ReadingValue(
label: S.of(context).nd,
value: selectedValue.value.toString(),
),
),
);
}
}

View file

@ -1,4 +1,3 @@
import 'package:lightmeter/data/models/film.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
sealed class MeteringEvent {
@ -11,12 +10,6 @@ class EquipmentProfileChangedEvent extends MeteringEvent {
const EquipmentProfileChangedEvent(this.equipmentProfileData);
}
class FilmChangedEvent extends MeteringEvent {
final Film film;
const FilmChangedEvent(this.film);
}
class IsoChangedEvent extends MeteringEvent {
final IsoValue isoValue;

View file

@ -1,17 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/data/caffeine_service.dart';
import 'package:lightmeter/data/haptics_service.dart';
import 'package:lightmeter/data/light_sensor_service.dart';
import 'package:lightmeter/data/permissions_service.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/data/volume_events_service.dart';
import 'package:lightmeter/interactors/metering_interactor.dart';
import 'package:lightmeter/providers/services_provider.dart';
import 'package:lightmeter/screens/metering/bloc_metering.dart';
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
import 'package:lightmeter/screens/metering/components/shared/volume_keys_notifier/notifier_volume_keys.dart';
import 'package:lightmeter/screens/metering/screen_metering.dart';
import 'package:lightmeter/utils/inherited_generics.dart';
class MeteringFlow extends StatefulWidget {
const MeteringFlow({super.key});
@ -23,31 +17,45 @@ class MeteringFlow extends StatefulWidget {
class _MeteringFlowState extends State<MeteringFlow> {
@override
Widget build(BuildContext context) {
return InheritedWidgetBase<MeteringInteractor>(
return MeteringInteractorProvider(
data: MeteringInteractor(
context.get<UserPreferencesService>(),
context.get<CaffeineService>(),
context.get<HapticsService>(),
context.get<PermissionsService>(),
context.get<LightSensorService>(),
context.get<VolumeEventsService>(),
ServicesProvider.of(context).userPreferencesService,
ServicesProvider.of(context).caffeineService,
ServicesProvider.of(context).hapticsService,
ServicesProvider.of(context).permissionsService,
ServicesProvider.of(context).lightSensorService,
ServicesProvider.of(context).volumeEventsService,
)..initialize(),
child: InheritedWidgetBase<VolumeKeysNotifier>(
data: VolumeKeysNotifier(context.get<VolumeEventsService>()),
child: MultiBlocProvider(
providers: [
BlocProvider(create: (_) => MeteringCommunicationBloc()),
BlocProvider(
create: (context) => MeteringBloc(
context.get<MeteringInteractor>(),
context.get<VolumeKeysNotifier>(),
context.read<MeteringCommunicationBloc>(),
),
child: MultiBlocProvider(
providers: [
BlocProvider(create: (_) => MeteringCommunicationBloc()),
BlocProvider(
create: (context) => MeteringBloc(
MeteringInteractorProvider.of(context),
VolumeKeysNotifier(ServicesProvider.of(context).volumeEventsService),
context.read<MeteringCommunicationBloc>(),
),
],
child: const MeteringScreen(),
),
),
],
child: const MeteringScreen(),
),
);
}
}
class MeteringInteractorProvider extends InheritedWidget {
final MeteringInteractor data;
const MeteringInteractorProvider({
required this.data,
required super.child,
super.key,
});
static MeteringInteractor of(BuildContext context) {
return context.findAncestorWidgetOfExactType<MeteringInteractorProvider>()!.data;
}
@override
bool updateShouldNotify(MeteringInteractorProvider oldWidget) => false;
}

View file

@ -4,17 +4,18 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/data/models/ev_source_type.dart';
import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/data/models/film.dart';
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
import 'package:lightmeter/environment.dart';
import 'package:lightmeter/providers/ev_source_type_provider.dart';
import 'package:lightmeter/providers/services_provider.dart';
import 'package:lightmeter/providers/user_preferences_provider.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/camera_container/provider_container_camera.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/state_metering.dart';
import 'package:lightmeter/utils/inherited_generics.dart';
import 'package:lightmeter/screens/metering/utils/listener_metering_layout_feature.dart';
import 'package:lightmeter/screens/metering/utils/listsner_equipment_profiles.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class MeteringScreen extends StatelessWidget {
@ -29,13 +30,10 @@ class MeteringScreen extends StatelessWidget {
children: [
Expanded(
child: BlocBuilder<MeteringBloc, MeteringState>(
builder: (_, state) => _MeteringContainerBuidler(
builder: (_, state) => MeteringContainerBuidler(
ev: state is MeteringDataState ? state.ev : null,
film: state.film,
iso: state.iso,
nd: state.nd,
onFilmChanged: (value) =>
context.read<MeteringBloc>().add(FilmChangedEvent(value)),
onIsoChanged: (value) => context.read<MeteringBloc>().add(IsoChangedEvent(value)),
onNdChanged: (value) => context.read<MeteringBloc>().add(NdChangedEvent(value)),
),
@ -45,8 +43,8 @@ class MeteringScreen extends StatelessWidget {
builder: (context, state) => MeteringBottomControlsProvider(
ev: state is MeteringDataState ? state.ev : null,
isMetering: state.isMetering,
onSwitchEvSourceType: context.get<Environment>().hasLightSensor
? EvSourceTypeProvider.of(context).toggleType
onSwitchEvSourceType: ServicesProvider.of(context).environment.hasLightSensor
? UserPreferencesProvider.of(context).toggleEvSourceType
: null,
onMeasure: () => context.read<MeteringBloc>().add(const MeasureEvent()),
onSettings: () {
@ -71,14 +69,16 @@ class _InheritedListeners extends StatelessWidget {
@override
Widget build(BuildContext context) {
return InheritedWidgetListener<EquipmentProfile>(
return EquipmentProfileListener(
onDidChangeDependencies: (value) {
context.read<MeteringBloc>().add(EquipmentProfileChangedEvent(value));
},
child: InheritedModelAspectListener<MeteringScreenLayoutFeature, bool>(
aspect: MeteringScreenLayoutFeature.filmPicker,
child: MeteringScreenLayoutFeatureListener(
feature: MeteringScreenLayoutFeature.filmPicker,
onDidChangeDependencies: (value) {
if (!value) context.read<MeteringBloc>().add(const FilmChangedEvent(Film.other()));
if (!value) {
FilmsProvider.of(context).setFilm(const Film.other());
}
},
child: child,
),
@ -86,38 +86,39 @@ class _InheritedListeners extends StatelessWidget {
}
}
class _MeteringContainerBuidler extends StatelessWidget {
class MeteringContainerBuidler extends StatelessWidget {
final double? ev;
final Film film;
final IsoValue iso;
final NdValue nd;
final ValueChanged<Film> onFilmChanged;
final ValueChanged<IsoValue> onIsoChanged;
final ValueChanged<NdValue> onNdChanged;
const _MeteringContainerBuidler({
const MeteringContainerBuidler({
required this.ev,
required this.film,
required this.iso,
required this.nd,
required this.onFilmChanged,
required this.onIsoChanged,
required this.onNdChanged,
});
@override
Widget build(BuildContext context) {
final exposurePairs = ev != null ? buildExposureValues(context, ev!, film) : <ExposurePair>[];
final exposurePairs = ev != null
? buildExposureValues(
ev!,
UserPreferencesProvider.stopTypeOf(context),
EquipmentProfiles.selectedOf(context),
)
: <ExposurePair>[];
final fastest = exposurePairs.isNotEmpty ? exposurePairs.first : null;
final slowest = exposurePairs.isNotEmpty ? exposurePairs.last : null;
return context.listen<EvSourceType>() == EvSourceType.camera
// Doubled build here when switching evSourceType. As new source bloc fires a new state on init
return UserPreferencesProvider.evSourceTypeOf(context) == EvSourceType.camera
? CameraContainerProvider(
fastest: fastest,
slowest: slowest,
film: film,
iso: iso,
nd: nd,
onFilmChanged: onFilmChanged,
onIsoChanged: onIsoChanged,
onNdChanged: onNdChanged,
exposurePairs: exposurePairs,
@ -125,50 +126,36 @@ class _MeteringContainerBuidler extends StatelessWidget {
: LightSensorContainerProvider(
fastest: fastest,
slowest: slowest,
film: film,
iso: iso,
nd: nd,
onFilmChanged: onFilmChanged,
onIsoChanged: onIsoChanged,
onNdChanged: onNdChanged,
exposurePairs: exposurePairs,
);
}
List<ExposurePair> buildExposureValues(BuildContext context, double ev, Film film) {
@visibleForTesting
static List<ExposurePair> buildExposureValues(
double ev,
StopType stopType,
EquipmentProfile equipmentProfile,
) {
if (ev.isNaN || ev.isInfinite) {
return List.empty();
}
return List.empty();
/// Depending on the `stopType` the exposure pairs list length is multiplied by 1,2 or 3
final StopType stopType = context.listen<StopType>();
final int evSteps = (ev * (stopType.index + 1)).round();
final EquipmentProfile equipmentProfile = context.listen<EquipmentProfile>();
final List<ApertureValue> apertureValues =
equipmentProfile.apertureValues.whereStopType(stopType);
final List<ShutterSpeedValue> shutterSpeedValues =
equipmentProfile.shutterSpeedValues.whereStopType(stopType);
final apertureValues = ApertureValue.values.whereStopType(stopType);
final shutterSpeedValues = ShutterSpeedValue.values.whereStopType(stopType);
/// Basically we use 1" shutter speed as an anchor point for building the exposure pairs list.
/// But user can exclude this value from the list using custom equipment profile.
/// So we have to restore the index of the anchor value.
const ShutterSpeedValue anchorShutterSpeed = ShutterSpeedValue(1, false, StopType.full);
int anchorIndex = shutterSpeedValues.indexOf(anchorShutterSpeed);
if (anchorIndex < 0) {
final filteredFullList = ShutterSpeedValue.values.whereStopType(stopType);
final customListStartIndex = filteredFullList.indexOf(shutterSpeedValues.first);
final fullListAnchor = filteredFullList.indexOf(anchorShutterSpeed);
if (customListStartIndex < fullListAnchor) {
/// This means, that user excluded anchor value at the end,
/// i.e. all shutter speed values are shorter than 1".
anchorIndex = fullListAnchor - customListStartIndex;
} else {
/// In case user excludes anchor value at the start,
/// we can do no adjustment.
}
}
const anchorShutterSpeed = ShutterSpeedValue(1, false, StopType.full);
final int anchorIndex = shutterSpeedValues.indexOf(anchorShutterSpeed);
final int evOffset = anchorIndex - evSteps;
late final int apertureOffset;
@ -187,16 +174,42 @@ class _MeteringContainerBuidler extends StatelessWidget {
) -
max(apertureOffset, shutterSpeedOffset);
if (itemsCount < 0) {
if (itemsCount <= 0) {
return List.empty();
}
return List.generate(
final exposurePairs = List.generate(
itemsCount,
(index) => ExposurePair(
apertureValues[index + apertureOffset],
film.reciprocityFailure(shutterSpeedValues[index + shutterSpeedOffset]),
shutterSpeedValues[index + shutterSpeedOffset],
),
growable: false,
);
/// Full equipment profile, nothing to cut
if (equipmentProfile.id == "") {
return exposurePairs;
}
final equipmentApertureValues = equipmentProfile.apertureValues.whereStopType(stopType);
final equipmentShutterSpeedValues = equipmentProfile.shutterSpeedValues.whereStopType(stopType);
final startCutEV = max(
exposurePairs.first.aperture.difference(equipmentApertureValues.first),
exposurePairs.first.shutterSpeed.difference(equipmentShutterSpeedValues.first),
);
final endCutEV = max(
equipmentApertureValues.last.difference(exposurePairs.last.aperture),
equipmentShutterSpeedValues.last.difference(exposurePairs.last.shutterSpeed),
);
final startCut = (startCutEV * (stopType.index + 1)).round().clamp(0, itemsCount);
final endCut = (endCutEV * (stopType.index + 1)).round().clamp(0, itemsCount);
if (startCut > itemsCount - endCut) {
return const [];
}
return exposurePairs.sublist(startCut, itemsCount - endCut);
}
}

View file

@ -1,18 +1,15 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/film.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
@immutable
abstract class MeteringState {
final double? ev100;
final Film film;
final IsoValue iso;
final NdValue nd;
final bool isMetering;
const MeteringState({
this.ev100,
required this.film,
required this.iso,
required this.nd,
required this.isMetering,
@ -21,7 +18,6 @@ abstract class MeteringState {
class LoadingState extends MeteringState {
const LoadingState({
required super.film,
required super.iso,
required super.nd,
}) : super(isMetering: true);
@ -30,7 +26,6 @@ class LoadingState extends MeteringState {
class MeteringDataState extends MeteringState {
const MeteringDataState({
required super.ev100,
required super.film,
required super.iso,
required super.nd,
required super.isMetering,

View file

@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
import 'package:lightmeter/providers/user_preferences_provider.dart';
/// Listening to multiple dependencies at the same time causes firing an event for all dependencies
/// even though some of them didn't change:
/// ```dart
/// @override
/// void didChangeDependencies() {
/// super.didChangeDependencies();
/// _bloc.add(EquipmentProfileChangedEvent(EquipmentProfile.of(context)));
/// if (!MeteringScreenLayout.featureStatusOf(context, MeteringScreenLayoutFeature.filmPicker)) {
/// _bloc.add(const FilmChangedEvent(Film.other()));
/// }
/// }
/// ```
/// To overcome this issue I've decided to create a generic listener,
/// that will listen to each dependency separately.
class MeteringScreenLayoutFeatureListener extends StatefulWidget {
final MeteringScreenLayoutFeature feature;
final ValueChanged<bool> onDidChangeDependencies;
final Widget child;
const MeteringScreenLayoutFeatureListener({
required this.feature,
required this.onDidChangeDependencies,
required this.child,
super.key,
});
@override
State<MeteringScreenLayoutFeatureListener> createState() =>
_MeteringScreenLayoutFeatureListenerState();
}
class _MeteringScreenLayoutFeatureListenerState extends State<MeteringScreenLayoutFeatureListener> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
widget.onDidChangeDependencies(
UserPreferencesProvider.meteringScreenFeatureOf(
context,
widget.feature,
),
);
}
@override
Widget build(BuildContext context) {
return widget.child;
}
}

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

@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/environment.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/utils/inherited_generics.dart';
import 'package:lightmeter/providers/services_provider.dart';
import 'package:url_launcher/url_launcher.dart';
class ReportIssueListTile extends StatelessWidget {
@ -14,7 +13,7 @@ class ReportIssueListTile extends StatelessWidget {
title: Text(S.of(context).reportIssue),
onTap: () {
launchUrl(
Uri.parse(context.get<Environment>().issuesReportUrl),
Uri.parse(ServicesProvider.of(context).environment.issuesReportUrl),
mode: LaunchMode.externalApplication,
);
},

View file

@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/environment.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/utils/inherited_generics.dart';
import 'package:lightmeter/providers/services_provider.dart';
import 'package:url_launcher/url_launcher.dart';
class SourceCodeListTile extends StatelessWidget {
@ -14,7 +13,7 @@ class SourceCodeListTile extends StatelessWidget {
title: Text(S.of(context).sourceCode),
onTap: () {
launchUrl(
Uri.parse(context.get<Environment>().sourceCodeUrl),
Uri.parse(ServicesProvider.of(context).environment.sourceCodeUrl),
mode: LaunchMode.externalApplication,
);
},

View file

@ -1,8 +1,7 @@
import 'package:clipboard/clipboard.dart';
import 'package:flutter/material.dart';
import 'package:lightmeter/environment.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/utils/inherited_generics.dart';
import 'package:lightmeter/providers/services_provider.dart';
import 'package:url_launcher/url_launcher.dart';
class WriteEmailListTile extends StatelessWidget {
@ -14,7 +13,7 @@ class WriteEmailListTile extends StatelessWidget {
leading: const Icon(Icons.email),
title: Text(S.of(context).writeEmail),
onTap: () {
final email = context.get<Environment>().contactEmail;
final email = ServicesProvider.of(context).environment.contactEmail;
final mailToUrl = Uri.parse('mailto:$email?subject=M3 Lightmeter');
canLaunchUrl(mailToUrl).then((canLaunch) {
if (canLaunch) {

View file

@ -1,10 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/interactors/settings_interactor.dart';
import 'package:lightmeter/screens/settings/components/general/components/caffeine/bloc_list_tile_caffeine.dart';
import 'package:lightmeter/screens/settings/components/general/components/caffeine/widget_list_tile_caffeine.dart';
import 'package:lightmeter/utils/inherited_generics.dart';
import 'package:lightmeter/screens/settings/flow_settings.dart';
class CaffeineListTileProvider extends StatelessWidget {
const CaffeineListTileProvider({super.key});
@ -12,7 +11,7 @@ class CaffeineListTileProvider extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => CaffeineListTileBloc(context.get<SettingsInteractor>()),
create: (context) => CaffeineListTileBloc(SettingsInteractorProvider.of(context)),
child: const CaffeineListTile(),
);
}

View file

@ -1,10 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/interactors/settings_interactor.dart';
import 'package:lightmeter/screens/settings/components/general/components/haptics/bloc_list_tile_haptics.dart';
import 'package:lightmeter/screens/settings/components/general/components/haptics/widget_list_tile_haptics.dart';
import 'package:lightmeter/utils/inherited_generics.dart';
import 'package:lightmeter/screens/settings/flow_settings.dart';
class HapticsListTileProvider extends StatelessWidget {
const HapticsListTileProvider({super.key});
@ -12,7 +11,7 @@ class HapticsListTileProvider extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => HapticsListTileBloc(context.get<SettingsInteractor>()),
create: (context) => HapticsListTileBloc(SettingsInteractorProvider.of(context)),
child: const HapticsListTile(),
);
}

View file

@ -1,9 +1,8 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/supported_locale.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/providers/supported_locale_provider.dart';
import 'package:lightmeter/screens/settings/components/shared/dialog_picker.dart/widget_dialog_picker.dart';
import 'package:lightmeter/utils/inherited_generics.dart';
import 'package:lightmeter/providers/user_preferences_provider.dart';
import 'package:lightmeter/screens/settings/components/shared/dialog_picker/widget_dialog_picker.dart';
class LanguageListTile extends StatelessWidget {
const LanguageListTile({super.key});
@ -13,20 +12,20 @@ class LanguageListTile extends StatelessWidget {
return ListTile(
leading: const Icon(Icons.language),
title: Text(S.of(context).language),
trailing: Text(context.listen<SupportedLocale>().localizedName),
trailing: Text(UserPreferencesProvider.localeOf(context).localizedName),
onTap: () {
showDialog<SupportedLocale>(
context: context,
builder: (_) => DialogPicker<SupportedLocale>(
icon: Icons.language,
title: S.of(context).chooseLanguage,
selectedValue: context.get<SupportedLocale>(),
selectedValue: UserPreferencesProvider.localeOf(context),
values: SupportedLocale.values,
titleAdapter: (context, value) => value.localizedName,
),
).then((value) {
if (value != null) {
SupportedLocaleProvider.of(context).setLocale(value);
UserPreferencesProvider.of(context).setLocale(value);
}
});
},

View file

@ -1,10 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/interactors/settings_interactor.dart';
import 'package:lightmeter/screens/settings/components/general/components/volume_actions/bloc_list_tile_volume_actions.dart';
import 'package:lightmeter/screens/settings/components/general/components/volume_actions/widget_list_tile_volume_actions.dart';
import 'package:lightmeter/utils/inherited_generics.dart';
import 'package:lightmeter/screens/settings/flow_settings.dart';
class VolumeActionsListTileProvider extends StatelessWidget {
const VolumeActionsListTileProvider({super.key});
@ -12,7 +11,7 @@ class VolumeActionsListTileProvider extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => VolumeActionsListTileBloc(context.get<SettingsInteractor>()),
create: (context) => VolumeActionsListTileBloc(SettingsInteractorProvider.of(context)),
child: const VolumeActionsListTile(),
);
}

View file

@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/screens/settings/components/shared/iap_list_tile/widget_list_tile_iap.dart';
import 'package:lightmeter/screens/settings/components/utils/show_buy_pro_dialog.dart';
class BuyProListTile extends StatelessWidget {
const BuyProListTile({super.key});
@override
Widget build(BuildContext context) {
return IAPListTile(
leading: const Icon(Icons.star),
title: Text(S.of(context).buyLightmeterPro),
onTap: () {
showBuyProDialog(context);
},
showPendingTrailing: true,
);
}
}

View file

@ -0,0 +1,16 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/screens/settings/components/lightmeter_pro/components/buy_pro/widget_list_tile_buy_pro.dart';
import 'package:lightmeter/screens/settings/components/shared/settings_section/widget_settings_section.dart';
class LightmeterProSettingsSection extends StatelessWidget {
const LightmeterProSettingsSection({super.key});
@override
Widget build(BuildContext context) {
return SettingsSection(
title: S.of(context).lightmeterPro,
children: const [BuyProListTile()],
);
}
}

View file

@ -1,10 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/interactors/settings_interactor.dart';
import 'package:lightmeter/screens/settings/components/metering/components/calibration/components/calibration_dialog/bloc_dialog_calibration.dart';
import 'package:lightmeter/screens/settings/components/metering/components/calibration/components/calibration_dialog/widget_dialog_calibration.dart';
import 'package:lightmeter/utils/inherited_generics.dart';
import 'package:lightmeter/screens/settings/flow_settings.dart';
class CalibrationDialogProvider extends StatelessWidget {
const CalibrationDialogProvider({super.key});
@ -12,7 +11,7 @@ class CalibrationDialogProvider extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => CalibrationDialogBloc(context.get<SettingsInteractor>()),
create: (context) => CalibrationDialogBloc(SettingsInteractorProvider.of(context)),
child: const CalibrationDialog(),
);
}

View file

@ -1,13 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/environment.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/providers/services_provider.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/settings/components/metering/components/calibration/components/calibration_dialog/bloc_dialog_calibration.dart';
import 'package:lightmeter/screens/settings/components/metering/components/calibration/components/calibration_dialog/event_dialog_calibration.dart';
import 'package:lightmeter/screens/settings/components/metering/components/calibration/components/calibration_dialog/state_dialog_calibration.dart';
import 'package:lightmeter/screens/shared/centered_slider/widget_slider_centered.dart';
import 'package:lightmeter/utils/inherited_generics.dart';
import 'package:lightmeter/utils/to_string_signed.dart';
class CalibrationDialog extends StatelessWidget {
@ -15,7 +14,7 @@ class CalibrationDialog extends StatelessWidget {
@override
Widget build(BuildContext context) {
final bool hasLightSensor = context.get<Environment>().hasLightSensor;
final bool hasLightSensor = ServicesProvider.of(context).environment.hasLightSensor;
return AlertDialog(
icon: const Icon(Icons.settings_brightness),
titlePadding: Dimens.dialogIconTitlePadding,
@ -118,6 +117,7 @@ class _CalibrationUnit extends StatelessWidget {
IconButton(
onPressed: onReset,
icon: const Icon(Icons.sync),
tooltip: S.of(context).tooltipResetToZero,
),
],
)

View file

@ -1,8 +1,7 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/interactors/settings_interactor.dart';
import 'package:lightmeter/screens/settings/components/metering/components/calibration/components/calibration_dialog/provider_dialog_calibration.dart';
import 'package:lightmeter/utils/inherited_generics.dart';
import 'package:lightmeter/screens/settings/flow_settings.dart';
class CalibrationListTile extends StatelessWidget {
const CalibrationListTile({super.key});
@ -15,8 +14,8 @@ class CalibrationListTile extends StatelessWidget {
onTap: () {
showDialog<double>(
context: context,
builder: (_) => InheritedWidgetBase(
data: context.get<SettingsInteractor>(),
builder: (_) => SettingsInteractorProvider(
data: SettingsInteractorProvider.of(context),
child: const CalibrationDialogProvider(),
),
);

View file

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/components/dialog_filter/widget_dialog_filter.dart';
import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/components/dialog_range_picker/widget_dialog_picker_range.dart';
import 'package:lightmeter/screens/settings/components/shared/dialog_filter/widget_dialog_filter.dart';
import 'package:lightmeter/screens/settings/components/shared/dialog_range_picker/widget_dialog_picker_range.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class EquipmentListTiles extends StatelessWidget {
@ -36,9 +36,6 @@ class EquipmentListTiles extends StatelessWidget {
title: S.of(context).isoValues,
description: S.of(context).isoValuesFilterDescription,
values: IsoValue.values,
valuesCount: selectedIsoValues.length == IsoValue.values.length
? S.of(context).equipmentProfileAllValues
: selectedIsoValues.length.toString(),
selectedValues: selectedIsoValues,
rangeSelect: false,
onChanged: onIsoValuesSelecred,
@ -48,9 +45,6 @@ class EquipmentListTiles extends StatelessWidget {
title: S.of(context).ndFilters,
description: S.of(context).ndFiltersFilterDescription,
values: NdValue.values,
valuesCount: selectedNdValues.length == NdValue.values.length
? S.of(context).equipmentProfileAllValues
: selectedNdValues.length.toString(),
selectedValues: selectedNdValues,
rangeSelect: false,
onChanged: onNdValuesSelected,
@ -60,9 +54,6 @@ class EquipmentListTiles extends StatelessWidget {
title: S.of(context).apertureValues,
description: S.of(context).apertureValuesFilterDescription,
values: ApertureValue.values,
valuesCount: selectedApertureValues.length == ApertureValue.values.length
? S.of(context).equipmentProfileAllValues
: selectedApertureValues.length.toString(),
selectedValues: selectedApertureValues,
rangeSelect: true,
onChanged: onApertureValuesSelected,
@ -72,9 +63,6 @@ class EquipmentListTiles extends StatelessWidget {
title: S.of(context).shutterSpeedValues,
description: S.of(context).shutterSpeedValuesFilterDescription,
values: ShutterSpeedValue.values,
valuesCount: selectedShutterSpeedValues.length == ShutterSpeedValue.values.length
? S.of(context).equipmentProfileAllValues
: selectedShutterSpeedValues.length.toString(),
selectedValues: selectedShutterSpeedValues,
rangeSelect: true,
onChanged: onShutterSpeedValuesSelected,
@ -87,7 +75,6 @@ class EquipmentListTiles extends StatelessWidget {
class _EquipmentListTile<T extends PhotographyValue> extends StatelessWidget {
final IconData icon;
final String title;
final String valuesCount;
final String description;
final List<T> selectedValues;
final List<T> values;
@ -97,7 +84,6 @@ class _EquipmentListTile<T extends PhotographyValue> extends StatelessWidget {
const _EquipmentListTile({
required this.icon,
required this.title,
required this.valuesCount,
required this.description,
required this.selectedValues,
required this.values,
@ -111,7 +97,13 @@ class _EquipmentListTile<T extends PhotographyValue> extends StatelessWidget {
return ListTile(
leading: Icon(icon),
title: Text(title),
trailing: Text(valuesCount),
trailing: rangeSelect
? Text("${selectedValues.first} - ${selectedValues.last}")
: Text(
values.length == selectedValues.length
? S.of(context).equipmentProfileAllValues
: selectedValues.length.toString(),
),
onTap: () {
showDialog<List<T>>(
context: context,

View file

@ -2,6 +2,7 @@ import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:lightmeter/generated/l10n.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/components/equipment_list_tiles/widget_list_tiles_equipments.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';
@ -10,12 +11,14 @@ import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class EquipmentProfileContainer extends StatefulWidget {
final EquipmentProfile data;
final ValueChanged<EquipmentProfile> onUpdate;
final VoidCallback onCopy;
final VoidCallback onDelete;
final VoidCallback onExpand;
const EquipmentProfileContainer({
required this.data,
required this.onUpdate,
required this.onCopy,
required this.onDelete,
required this.onExpand,
super.key,
@ -71,6 +74,7 @@ class EquipmentProfileContainerState extends State<EquipmentProfileContainer>
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
title: Row(
children: [
_AnimatedNameLeading(controller: _controller),
@ -84,19 +88,9 @@ class EquipmentProfileContainerState extends State<EquipmentProfileContainer>
),
],
),
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),
),
],
trailing: _AnimatedArrowButton(
controller: _controller,
onPressed: () => _expanded ? collapse() : expand(),
),
onTap: () => _expanded ? _showNameDialog() : expand(),
),
@ -119,6 +113,8 @@ class EquipmentProfileContainerState extends State<EquipmentProfileContainer>
_equipmentData = _equipmentData.copyWith(shutterSpeedValues: value);
widget.onUpdate(_equipmentData);
},
onCopy: widget.onCopy,
onDelete: widget.onDelete,
),
],
),
@ -142,10 +138,13 @@ class EquipmentProfileContainerState extends State<EquipmentProfileContainer>
widget.onExpand();
_controller.forward();
SchedulerBinding.instance.addPostFrameCallback((_) {
Scrollable.ensureVisible(
context,
alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd,
);
Future.delayed(_controller.duration!).then((_) {
Scrollable.ensureVisible(
context,
alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd,
duration: _controller.duration!,
);
});
});
}
@ -163,7 +162,7 @@ class _AnimatedNameLeading extends AnimatedWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(right: _progress.value * Dimens.grid24),
padding: EdgeInsets.only(right: _progress.value * Dimens.grid8),
child: Icon(
Icons.edit,
size: _progress.value * Dimens.grid24,
@ -190,6 +189,7 @@ class _AnimatedArrowButton extends AnimatedWidget {
angle: _progress.value * pi,
child: const Icon(Icons.keyboard_arrow_down),
),
tooltip: _progress.value == 0 ? S.of(context).tooltipExpand : S.of(context).tooltipCollapse,
);
}
}
@ -200,6 +200,8 @@ class _AnimatedEquipmentListTiles extends AnimatedWidget {
final ValueChanged<List<IsoValue>> onIsoValuesSelecred;
final ValueChanged<List<NdValue>> onNdValuesSelected;
final ValueChanged<List<ShutterSpeedValue>> onShutterSpeedValuesSelected;
final VoidCallback onCopy;
final VoidCallback onDelete;
const _AnimatedEquipmentListTiles({
required AnimationController controller,
@ -208,6 +210,8 @@ class _AnimatedEquipmentListTiles extends AnimatedWidget {
required this.onIsoValuesSelecred,
required this.onNdValuesSelected,
required this.onShutterSpeedValuesSelected,
required this.onCopy,
required this.onDelete,
}) : super(listenable: controller);
Animation<double> get _progress => listenable as Animation<double>;
@ -218,19 +222,43 @@ class _AnimatedEquipmentListTiles extends AnimatedWidget {
alignment: Alignment.topCenter,
size: Size(
double.maxFinite,
_progress.value * Dimens.grid56 * 4,
_progress.value * Dimens.grid56 * 5,
),
// https://github.com/gskinnerTeam/flutter-folio/pull/62
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,
child: Column(
children: [
EquipmentListTiles(
selectedApertureValues: equipmentData.apertureValues,
selectedIsoValues: equipmentData.isoValues,
selectedNdValues: equipmentData.ndValues,
selectedShutterSpeedValues: equipmentData.shutterSpeedValues,
onApertureValuesSelected: onApertureValuesSelected,
onIsoValuesSelecred: onIsoValuesSelecred,
onNdValuesSelected: onNdValuesSelected,
onShutterSpeedValuesSelected: onShutterSpeedValuesSelected,
),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
trailing: Row(
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: onCopy,
icon: const Icon(Icons.copy),
tooltip: S.of(context).tooltipCopy,
),
IconButton(
onPressed: onDelete,
icon: const Icon(Icons.delete),
tooltip: S.of(context).tooltipDelete,
),
],
),
),
],
),
),
);

View file

@ -1,11 +1,12 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/providers/equipment_profile_provider.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_name_dialog/widget_dialog_equipment_profile_name.dart';
import 'package:lightmeter/screens/shared/icon_placeholder/widget_icon_placeholder.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';
class EquipmentProfilesScreen extends StatefulWidget {
@ -16,18 +17,13 @@ class EquipmentProfilesScreen extends StatefulWidget {
}
class _EquipmentProfilesScreenState extends State<EquipmentProfilesScreen> {
static const maxProfiles = 5 + 1; // replace with a constant from iap
late List<GlobalKey<EquipmentProfileContainerState>> profileContainersKeys = [];
int get profilesCount => context.listen<EquipmentProfiles>().length;
final Map<String, GlobalKey<EquipmentProfileContainerState>> keysMap = {};
int get profilesCount => keysMap.length;
@override
void initState() {
super.initState();
profileContainersKeys = context
.get<EquipmentProfiles>()
.map((e) => GlobalKey<EquipmentProfileContainerState>(debugLabel: e.id))
.toList();
void didChangeDependencies() {
super.didChangeDependencies();
_updateProfilesKeys();
}
@override
@ -35,70 +31,133 @@ class _EquipmentProfilesScreenState extends State<EquipmentProfilesScreen> {
return SliverScreen(
title: S.of(context).equipmentProfiles,
appBarActions: [
if (profilesCount < maxProfiles)
IconButton(
onPressed: _addProfile,
icon: const Icon(Icons.add),
),
IconButton(
onPressed: Navigator.of(context).pop,
icon: const Icon(Icons.close),
),
],
slivers: [
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => index > 0
? Padding(
padding: EdgeInsets.fromLTRB(
Dimens.paddingM,
index == 0 ? Dimens.paddingM : 0,
Dimens.paddingM,
Dimens.paddingM,
),
child: EquipmentProfileContainer(
key: profileContainersKeys[index],
data: context.listen<EquipmentProfiles>()[index],
onExpand: () => _keepExpandedAt(index),
onUpdate: (profileData) => _updateProfileAt(profileData, index),
onDelete: () => _removeProfileAt(index),
),
)
: const SizedBox.shrink(),
childCount: profileContainersKeys.length,
),
onPressed: _addProfile,
icon: const Icon(Icons.add),
tooltip: S.of(context).tooltipAdd,
),
],
slivers: profilesCount == 1
? [
SliverFillRemaining(
hasScrollBody: false,
child: _EquipmentProfilesListPlaceholder(onTap: _addProfile),
)
]
: [
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
if (index == 0) {
// skip default profile
return const SizedBox.shrink();
}
final profile = EquipmentProfiles.of(context)[index];
return Padding(
padding: EdgeInsets.fromLTRB(
Dimens.paddingM,
index == 0 ? Dimens.paddingM : 0,
Dimens.paddingM,
Dimens.paddingM,
),
child: EquipmentProfileContainer(
key: keysMap[profile.id],
data: profile,
onExpand: () => _keepExpandedAt(index),
onUpdate: _updateProfileAt,
onCopy: () => _addProfile(profile),
onDelete: () => _removeProfileAt(profile),
),
);
},
childCount: EquipmentProfiles.of(context).length,
),
),
SliverToBoxAdapter(child: SizedBox(height: MediaQuery.paddingOf(context).bottom)),
],
);
}
void _addProfile() {
void _addProfile([EquipmentProfile? copyFrom]) {
showDialog<String>(
context: context,
builder: (_) => const EquipmentProfileNameDialog(),
).then((value) {
if (value != null) {
EquipmentProfileProvider.of(context).addProfile(value);
profileContainersKeys.add(GlobalKey<EquipmentProfileContainerState>());
).then((name) {
if (name != null) {
EquipmentProfileProvider.of(context).addProfile(name, copyFrom);
}
});
}
void _updateProfileAt(EquipmentProfile data, int index) {
void _updateProfileAt(EquipmentProfile data) {
EquipmentProfileProvider.of(context).updateProdile(data);
}
void _removeProfileAt(int index) {
EquipmentProfileProvider.of(context).deleteProfile(context.listen<EquipmentProfiles>()[index]);
profileContainersKeys.removeAt(index);
void _removeProfileAt(EquipmentProfile data) {
EquipmentProfileProvider.of(context).deleteProfile(data);
}
void _keepExpandedAt(int index) {
profileContainersKeys.getRange(0, index).forEach((element) {
keysMap.values.toList().getRange(0, index).forEach((element) {
element.currentState?.collapse();
});
profileContainersKeys.getRange(index + 1, profilesCount).forEach((element) {
keysMap.values.toList().getRange(index + 1, profilesCount).forEach((element) {
element.currentState?.collapse();
});
}
void _updateProfilesKeys() {
final profiles = EquipmentProfiles.of(context);
if (profiles.length > keysMap.length) {
// profile added
final List<String> idsToAdd = [];
for (final profile in profiles) {
if (!keysMap.keys.contains(profile.id)) idsToAdd.add(profile.id);
}
for (final id in idsToAdd) {
keysMap[id] = GlobalKey<EquipmentProfileContainerState>(debugLabel: id);
}
idsToAdd.clear();
} else if (profiles.length < keysMap.length) {
// profile deleted
final List<String> idsToDelete = [];
for (final id in keysMap.keys) {
if (!profiles.any((p) => p.id == id)) idsToDelete.add(id);
}
idsToDelete.forEach(keysMap.remove);
idsToDelete.clear();
} else {
// profile updated, no need to updated keys
}
}
}
class _EquipmentProfilesListPlaceholder extends StatelessWidget {
final VoidCallback onTap;
const _EquipmentProfilesListPlaceholder({required this.onTap});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: Dimens.sliverAppBarExpandedHeight),
child: FractionallySizedBox(
widthFactor: 1 / 1.618,
child: Center(
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(Dimens.paddingL),
child: IconPlaceholder(
icon: Icons.add,
text: S.of(context).tapToAdd,
),
),
),
),
),
);
}
}

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.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/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: () {

View file

@ -0,0 +1,35 @@
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';
class FilmsListTile extends StatelessWidget {
const FilmsListTile({super.key});
@override
Widget build(BuildContext context) {
return IAPListTile(
leading: const Icon(Icons.camera_roll),
title: Text(S.of(context).filmsInUse),
onTap: () {
showDialog<List<Film>>(
context: context,
builder: (_) => DialogFilter<Film>(
icon: const Icon(Icons.camera_roll),
title: S.of(context).filmsInUse,
description: S.of(context).filmsInUseDescription,
values: Films.of(context).sublist(1),
selectedValues: Films.inUseOf(context),
titleAdapter: (_, value) => value.name,
),
).then((values) {
if (values != null) {
FilmsProvider.of(context).saveFilms(values);
}
});
},
);
}
}

View file

@ -1,8 +1,7 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/providers/stop_type_provider.dart';
import 'package:lightmeter/screens/settings/components/shared/dialog_picker.dart/widget_dialog_picker.dart';
import 'package:lightmeter/utils/inherited_generics.dart';
import 'package:lightmeter/providers/user_preferences_provider.dart';
import 'package:lightmeter/screens/settings/components/shared/dialog_picker/widget_dialog_picker.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class StopTypeListTile extends StatelessWidget {
@ -13,20 +12,20 @@ class StopTypeListTile extends StatelessWidget {
return ListTile(
leading: const Icon(Icons.straighten),
title: Text(S.of(context).fractionalStops),
trailing: Text(_typeToString(context, context.listen<StopType>())),
trailing: Text(_typeToString(context, UserPreferencesProvider.stopTypeOf(context))),
onTap: () {
showDialog<StopType>(
context: context,
builder: (_) => DialogPicker<StopType>(
icon: Icons.straighten,
title: S.of(context).showFractionalStops,
selectedValue: context.get<StopType>(),
selectedValue: UserPreferencesProvider.stopTypeOf(context),
values: StopType.values,
titleAdapter: _typeToString,
),
).then((value) {
if (value != null) {
StopTypeProvider.of(context).set(value);
UserPreferencesProvider.of(context).setStopType(value);
}
});
},

View file

@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/providers/metering_screen_layout_provider.dart';
import 'package:lightmeter/providers/user_preferences_provider.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
class MeteringScreenLayoutFeaturesDialog extends StatefulWidget {
const MeteringScreenLayoutFeaturesDialog({super.key});
@ -14,7 +15,7 @@ class MeteringScreenLayoutFeaturesDialog extends StatefulWidget {
class _MeteringScreenLayoutFeaturesDialogState extends State<MeteringScreenLayoutFeaturesDialog> {
late final _features =
MeteringScreenLayoutConfig.from(MeteringScreenLayout.of(context, listen: false));
MeteringScreenLayoutConfig.from(UserPreferencesProvider.meteringScreenConfigOf(context));
@override
Widget build(BuildContext context) {
@ -25,25 +26,22 @@ class _MeteringScreenLayoutFeaturesDialogState extends State<MeteringScreenLayou
contentPadding: EdgeInsets.zero,
content: SizedBox(
width: double.maxFinite,
child: ListView(
shrinkWrap: true,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingL),
child: Text(S.of(context).meteringScreenLayoutHint),
),
const SizedBox(height: Dimens.grid16),
...MeteringScreenLayoutFeature.values.map(
(f) => SwitchListTile(
contentPadding: EdgeInsets.symmetric(horizontal: Dimens.dialogTitlePadding.left),
title: Text(_toStringLocalized(context, f)),
value: _features[f]!,
onChanged: (value) {
setState(() {
_features.update(f, (_) => value);
});
},
),
ListView(
shrinkWrap: true,
children: [
_featureListTile(MeteringScreenLayoutFeature.equipmentProfiles),
_featureListTile(MeteringScreenLayoutFeature.extremeExposurePairs),
_featureListTile(MeteringScreenLayoutFeature.filmPicker),
_featureListTile(MeteringScreenLayoutFeature.histogram),
],
),
],
),
@ -56,7 +54,10 @@ class _MeteringScreenLayoutFeaturesDialogState extends State<MeteringScreenLayou
),
TextButton(
onPressed: () {
MeteringScreenLayoutProvider.of(context).updateFeatures(_features);
if (!_features[MeteringScreenLayoutFeature.equipmentProfiles]!) {
EquipmentProfileProvider.of(context).setProfile(EquipmentProfiles.of(context).first);
}
UserPreferencesProvider.of(context).setMeteringScreenLayout(_features);
Navigator.of(context).pop();
},
child: Text(S.of(context).save),
@ -65,8 +66,23 @@ class _MeteringScreenLayoutFeaturesDialogState extends State<MeteringScreenLayou
);
}
Widget _featureListTile(MeteringScreenLayoutFeature f) {
return SwitchListTile(
contentPadding: EdgeInsets.symmetric(horizontal: Dimens.dialogTitlePadding.left),
title: Text(_toStringLocalized(context, f)),
value: _features[f]!,
onChanged: (value) {
setState(() {
_features.update(f, (_) => value);
});
},
);
}
String _toStringLocalized(BuildContext context, MeteringScreenLayoutFeature feature) {
switch (feature) {
case MeteringScreenLayoutFeature.equipmentProfiles:
return S.of(context).meteringScreenLayoutHintEquipmentProfiles;
case MeteringScreenLayoutFeature.extremeExposurePairs:
return S.of(context).meteringScreenFeatureExtremeExposurePairs;
case MeteringScreenLayoutFeature.filmPicker:

View file

@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/features.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/screens/settings/components/metering/components/calibration/widget_list_tile_calibration.dart';
import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/widget_list_tile_equipment_profiles.dart';
import 'package:lightmeter/screens/settings/components/metering/components/films/widget_list_tile_films.dart';
import 'package:lightmeter/screens/settings/components/metering/components/fractional_stops/widget_list_tile_fractional_stops.dart';
import 'package:lightmeter/screens/settings/components/metering/components/metering_screen_layout/widget_list_tile_metering_screen_layout.dart';
import 'package:lightmeter/screens/settings/components/shared/settings_section/widget_settings_section.dart';
@ -18,7 +18,8 @@ class MeteringSettingsSection extends StatelessWidget {
StopTypeListTile(),
CalibrationListTile(),
MeteringScreenLayoutListTile(),
if (FeaturesConfig.equipmentProfilesEnabled) EquipmentProfilesListTile(),
EquipmentProfilesListTile(),
FilmsListTile(),
],
);
}

View file

@ -1,9 +1,8 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class DialogFilter<T extends PhotographyValue> extends StatefulWidget {
class DialogFilter<T> extends StatefulWidget {
final Icon icon;
final String title;
final String description;
@ -25,10 +24,10 @@ class DialogFilter<T extends PhotographyValue> extends StatefulWidget {
State<DialogFilter<T>> createState() => _DialogFilterState<T>();
}
class _DialogFilterState<T extends PhotographyValue> extends State<DialogFilter<T>> {
class _DialogFilterState<T> extends State<DialogFilter<T>> {
late final List<bool> checkboxValues = List.generate(
widget.values.length,
(index) => widget.selectedValues.any((element) => element.value == widget.values[index].value),
(index) => widget.selectedValues.any((element) => element == widget.values[index]),
growable: false,
);
@ -86,6 +85,9 @@ class _DialogFilterState<T extends PhotographyValue> extends State<DialogFilter<
padding: EdgeInsets.zero,
icon: Icon(_hasAnyUnselected ? Icons.select_all : Icons.deselect),
onPressed: _toggleAll,
tooltip: _hasAnyUnselected
? S.of(context).tooltipSelectAll
: S.of(context).tooltipDesecelectAll,
),
),
const Spacer(),

Some files were not shown because too many files have changed in this diff Show more