mirror of
https://github.com/vodemn/m3_lightmeter.git
synced 2025-07-07 14:40:40 +00:00
Compare commits
22 commits
0b472b7776
...
32dc310a66
Author | SHA1 | Date | |
---|---|---|---|
![]() |
32dc310a66 | ||
![]() |
1b86defb2d | ||
![]() |
6d4ad7bc4d | ||
![]() |
8a71c8db13 | ||
![]() |
6e1aaf5acf | ||
![]() |
c12cfb1697 | ||
![]() |
50c2460f16 | ||
![]() |
6a9036ce5e | ||
![]() |
40c670ad30 | ||
![]() |
119e079554 | ||
![]() |
b02b50bac3 | ||
![]() |
dd5f551fd2 | ||
![]() |
bb9b023fa7 | ||
![]() |
dbf1f09eb6 | ||
![]() |
b13acedebd | ||
![]() |
bd784d1827 | ||
![]() |
47f7b61230 | ||
![]() |
37fe6a4a5d | ||
![]() |
a25ccc0fad | ||
![]() |
b53603b3f5 | ||
![]() |
e001c153fb | ||
![]() |
ed83540dde |
64 changed files with 1706 additions and 321 deletions
30
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
30
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a bug report to help improve the app
|
||||||
|
title: ''
|
||||||
|
labels: bug
|
||||||
|
assignees: vodemn
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Device:**
|
||||||
|
- Device: [e.g. Pixel 6]
|
||||||
|
- OS: [e.g. Android 12]
|
||||||
|
|
||||||
|
**App version**
|
20
.github/ISSUE_TEMPLATE/feature-request-or-improvement.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature-request-or-improvement.md
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
---
|
||||||
|
name: Feature request or improvement
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: feature
|
||||||
|
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 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.
|
|
@ -19,17 +19,11 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
name: Build .apk
|
||||||
runs-on: macos-11
|
runs-on: macos-11
|
||||||
timeout-minutes: 30
|
timeout-minutes: 15
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
# - uses: shaunco/ssh-agent@git-repo-mapping
|
|
||||||
# with:
|
|
||||||
# ssh-private-key: |
|
|
||||||
# ${{ secrets.M3_LIGHTMETER_IAP_KEY }}
|
|
||||||
# repo-mappings: |
|
|
||||||
# github.com/vodemn/m3_lightmeter_iap
|
|
||||||
|
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
@ -71,6 +65,7 @@ jobs:
|
||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
channel: "stable"
|
channel: "stable"
|
||||||
|
flutter-version: '3.10.0'
|
||||||
|
|
||||||
- name: Prepare flutter project
|
- name: Prepare flutter project
|
||||||
run: |
|
run: |
|
56
.github/workflows/ci.yml
vendored
56
.github/workflows/ci.yml
vendored
|
@ -1,56 +0,0 @@
|
||||||
# This workflow uses actions that are not certified by GitHub.
|
|
||||||
# They are provided by a third-party and are governed by
|
|
||||||
# separate terms of service, privacy policy, and support
|
|
||||||
# documentation.
|
|
||||||
|
|
||||||
name: PR check
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: ["main"]
|
|
||||||
pull_request:
|
|
||||||
branches: ["main"]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: macos-11
|
|
||||||
timeout-minutes: 10
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: shaunco/ssh-agent@git-repo-mapping
|
|
||||||
with:
|
|
||||||
ssh-private-key: |
|
|
||||||
${{ secrets.M3_LIGHTMETER_IAP_KEY }}
|
|
||||||
repo-mappings: |
|
|
||||||
github.com/vodemn/m3_lightmeter_iap
|
|
||||||
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- uses: subosito/flutter-action@v2
|
|
||||||
with:
|
|
||||||
channel: "stable"
|
|
||||||
|
|
||||||
- name: Check flutter version
|
|
||||||
run: flutter --version
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: flutter pub get
|
|
||||||
|
|
||||||
- name: Generate intl
|
|
||||||
run: flutter pub run intl_utils:generate
|
|
||||||
|
|
||||||
- name: Restore firebase_options.dart
|
|
||||||
env:
|
|
||||||
FIREBASE_OPTIONS: ${{ secrets.FIREBASE_OPTIONS }}
|
|
||||||
run: |
|
|
||||||
FIREBASE_OPTIONS_PATH=$RUNNER_TEMP/firebase_options.dart
|
|
||||||
echo -n "$FIREBASE_OPTIONS" | base64 --decode --output $FIREBASE_OPTIONS_PATH
|
|
||||||
cp $FIREBASE_OPTIONS_PATH ./lib
|
|
||||||
|
|
||||||
- name: Analyze project source
|
|
||||||
run: flutter analyze lib --fatal-infos
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: flutter test
|
|
|
@ -3,7 +3,9 @@
|
||||||
# separate terms of service, privacy policy, and support
|
# separate terms of service, privacy policy, and support
|
||||||
# documentation.
|
# documentation.
|
||||||
|
|
||||||
name: Build prod .aab & .apk
|
name: Create new release
|
||||||
|
|
||||||
|
run-name: Release v${{ inputs.version }}
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
@ -20,15 +22,9 @@ jobs:
|
||||||
build:
|
build:
|
||||||
name: Build .apk & .aab
|
name: Build .apk & .aab
|
||||||
runs-on: macos-11
|
runs-on: macos-11
|
||||||
timeout-minutes: 30
|
timeout-minutes: 15
|
||||||
steps:
|
|
||||||
# - uses: shaunco/ssh-agent@git-repo-mapping
|
|
||||||
# with:
|
|
||||||
# ssh-private-key: |
|
|
||||||
# ${{ secrets.M3_LIGHTMETER_IAP_KEY }}
|
|
||||||
# repo-mappings: |
|
|
||||||
# github.com/vodemn/m3_lightmeter_iap
|
|
||||||
|
|
||||||
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
@ -73,6 +69,7 @@ jobs:
|
||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
channel: "stable"
|
channel: "stable"
|
||||||
|
flutter-version: '3.10.0'
|
||||||
|
|
||||||
- name: Prepare flutter project
|
- name: Prepare flutter project
|
||||||
run: |
|
run: |
|
||||||
|
@ -141,18 +138,66 @@ jobs:
|
||||||
with:
|
with:
|
||||||
name: m3_lightmeter_apk
|
name: m3_lightmeter_apk
|
||||||
|
|
||||||
|
- name: Rename apk
|
||||||
|
run: mv app-prod-release.apk m3_lightmeter.apk
|
||||||
|
|
||||||
|
- uses: ncipollo/release-action@v1.12.0
|
||||||
|
with:
|
||||||
|
artifacts: "m3_lightmeter.apk"
|
||||||
|
skipIfReleaseExists: true
|
||||||
|
tag: "v${{ github.event.inputs.version }}"
|
||||||
|
|
||||||
|
- name: Delete no longer used apk artifact
|
||||||
|
uses: geekyeggo/delete-artifact@v2
|
||||||
|
with:
|
||||||
|
name: m3_lightmeter_apk
|
||||||
|
|
||||||
|
extract-merged-native-libs:
|
||||||
|
name: Extract merged native libraries
|
||||||
|
needs: [build]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Download app bundle
|
- name: Download app bundle
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: m3_lightmeter_bundle
|
name: m3_lightmeter_bundle
|
||||||
|
|
||||||
- name: Rename artifacts
|
- name: Extract & zip merged_native_libs
|
||||||
run: |
|
run: |
|
||||||
mv app-prod-release.apk m3_lightmeter.apk
|
unzip app-prod-release.aab
|
||||||
mv app-prod-release.aab m3_lightmeter.aab
|
(cd base/lib && zip -r "$OLDPWD/merged_native_libs.zip" .)
|
||||||
|
|
||||||
- uses: ncipollo/release-action@v1.12.0
|
- name: Zip app bundle and merged_native_libs
|
||||||
|
run: zip m3_lightmeter_release.zip app-prod-release.aab merged_native_libs.zip
|
||||||
|
|
||||||
|
- name: Upload merged_native_libs.zip to artifacts
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
artifacts: "m3_lightmeter.apk, m3_lightmeter.aab"
|
name: m3_lightmeter_release
|
||||||
skipIfReleaseExists: true
|
path: m3_lightmeter_release.zip
|
||||||
tag: "v${{ github.event.inputs.version }}"
|
|
||||||
|
# TODO: this should be moved to `create-google-play-release` step when it is implemented
|
||||||
|
- name: Delete no longer used app bundle artifact
|
||||||
|
uses: geekyeggo/delete-artifact@v2
|
||||||
|
with:
|
||||||
|
name: m3_lightmeter_bundle
|
||||||
|
|
||||||
|
# TODO: Automate Google Play releases creation
|
||||||
|
create-google-play-release:
|
||||||
|
if: false
|
||||||
|
name: Create Google Play release
|
||||||
|
needs: [build, extract-merged-native-libs]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Download app bundle
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: m3_lightmeter_bundle
|
40
.github/workflows/pr_check.yml
vendored
Normal file
40
.github/workflows/pr_check.yml
vendored
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
# This workflow uses actions that are not certified by GitHub.
|
||||||
|
# They are provided by a third-party and are governed by
|
||||||
|
# separate terms of service, privacy policy, and support
|
||||||
|
# documentation.
|
||||||
|
|
||||||
|
name: PR check
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
pull_request:
|
||||||
|
branches: ["main"]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze_and_test:
|
||||||
|
name: Analyze & test
|
||||||
|
runs-on: macos-11
|
||||||
|
timeout-minutes: 10
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: "stable"
|
||||||
|
flutter-version: '3.10.0'
|
||||||
|
|
||||||
|
- name: Prepare flutter project
|
||||||
|
run: |
|
||||||
|
flutter --version
|
||||||
|
flutter pub get
|
||||||
|
flutter pub run intl_utils:generate
|
||||||
|
|
||||||
|
- name: Analyze project source
|
||||||
|
run: flutter analyze lib --fatal-infos
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: flutter test
|
80
README.md
80
README.md
|
@ -1,58 +1,72 @@
|
||||||
<p align="center">
|
<img src="resources/social_preview.png" width="100%" />
|
||||||
<img src="assets/launcher_icon_circle.png" width="100" height="100">
|
|
||||||
</p>
|
|
||||||
<p align="center", style="font-size:60px;">
|
|
||||||
<b>Material Lightmeter</b>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
# Table of contents
|
# Table of contents
|
||||||
|
|
||||||
- [Table of contents](#table-of-contents)
|
- [Table of contents](#table-of-contents)
|
||||||
- [Backstory](#backstory)
|
- [Backstory](#backstory)
|
||||||
- [Legacy features](#legacy-features)
|
- [Screenshots](#screenshots)
|
||||||
- [Build](#build)
|
- [Development](#development)
|
||||||
- [Contribution](#contribution)
|
- [Contribution](#contribution)
|
||||||
|
- [iOS Limitations](#ios-limitations)
|
||||||
|
|
||||||
# Backstory
|
# Backstory
|
||||||
|
|
||||||
Some time ago I've started developing the [Material Lightmeter](https://play.google.com/store/apps/details?id=com.vodemn.lightmeter&hl=en&gl=US) app. Unfortunately, the last update of this app was almost a year prior to creation of this repo. So after reading some positive review on Google Play saying that "this is an excellent app, too bad it is no longer updated", I've decided to make an update and also make this app open source. Maybe someone sometime will decide to contribute to this project.
|
Some time ago I've started developing the [Material Lightmeter](https://play.google.com/store/apps/details?id=com.vodemn.lightmeter&hl=en&gl=US) app. Unfortunately, the last update of this app was almost a year prior to creation of this repo. So after reading some positive review on Google Play saying that "this is an excellent app, too bad it is no longer updated", I've decided to make an update and also make this app open source. Maybe someone sometime will decide to contribute to this project.
|
||||||
|
|
||||||
But as the existing repo contained some sensitive data, that I've pushed due to lack of experience, I had to make a new one. And if creating a new repo, why not rewrite the app from scratch?)
|
But as the existing repo contained some sensitive data, that I've pushed due to lack of experience, I had to make a new one. And if creating a new repo, why not rewrite the app from scratch?
|
||||||
|
|
||||||
Without further delay behold my new Lightmeter app inspired by Material You (a.k.a. M3)
|
Without further delay behold my new Lightmeter app inspired by Material You (a.k.a. M3)
|
||||||
|
|
||||||
# Legacy features
|
# Screenshots
|
||||||
|
|
||||||
The list of features that the old lightmeter app has and that have to be implemeneted in the M3 lightmeter.
|
<p float="center">
|
||||||
|
<img src="https://lh3.googleusercontent.com/8Sd-pmNcQ0xAr5opuTeJKWr2OXeQvCoFSdVDSoKQSHHKeNmqF71hqeAdm3yjunY12zY" width="18.8%" />
|
||||||
|
<img src="https://lh3.googleusercontent.com/rqBv8pT0AdcBy0xEgQY2unV-YEQ5KfUkandAxJ62yYCiSF72HClA_tkb4JT_3UPaIfFP" width="18.8%" />
|
||||||
|
<img src="https://lh3.googleusercontent.com/-SnYbYSugVfdwYi6m_rd9CzpCZMCIfudhnq0zRIlzEtLSXhrwziWVd2hotygfqiSofI" width="18.8%" />
|
||||||
|
<img src="https://lh3.googleusercontent.com/UXxptL_dAIJDtrmpEZuSz39Iq4HuPb3ZPeuANfE9XH0De0uZQT83LNdu1AObBPobpg" width="18.8%" />
|
||||||
|
<img src="https://lh3.googleusercontent.com/15g_SPV8knDLFbz1_-wGNJFsJeyVWZ_y--TGHpk75MaaIdMDyTXY2_TL-Aw8bpOhpw" width="18.8%" />
|
||||||
|
</p>
|
||||||
|
|
||||||
### Metering
|
# Development
|
||||||
- [x] ISO selecting
|
|
||||||
- [x] Reciprocity for different films
|
|
||||||
- [x] Reflected light metering
|
|
||||||
- [x] Incident light metering
|
|
||||||
|
|
||||||
### Adjust
|
### 1. Install Flutter
|
||||||
- [x] Light sources EV calibration
|
|
||||||
- [ ] Customizable aperture range
|
|
||||||
- [ ] Customizable shutter speed range
|
|
||||||
- [x] ND filter select
|
|
||||||
|
|
||||||
### General
|
To build this app you need to install Flutter 3.10.0 stable. [How to install](https://docs.flutter.dev/get-started/install).
|
||||||
- [x] Caffeine
|
|
||||||
- [x] Vibration
|
|
||||||
- [ ] Volume button actions
|
|
||||||
|
|
||||||
### Theme
|
### 2. (Optional) Install Firebase
|
||||||
- [x] Dark theme
|
|
||||||
- [x] Picking primary color
|
|
||||||
- [x] Russian language
|
|
||||||
|
|
||||||
## Build
|
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 this project is private, you will be able to run this app from the _main_dev.dart_ file (i.e. --flavor dev). Also to avoid fatal errors the _main_prod.dart_ file is excluded from analysis.
|
### 3. Get packages
|
||||||
|
|
||||||
## Contribution
|
Fetch all the neccessary dependencies and generate translation files by running the following commands:
|
||||||
|
```console
|
||||||
|
flutter pub get
|
||||||
|
flutter pub run intl_utils:generate
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Build
|
||||||
|
|
||||||
|
You can build an apk by running the following command from the root of the repository:
|
||||||
|
```console
|
||||||
|
flutter build apk --release --flavor $FLAVOR --dart-define cameraPreviewAspectRatio=2/3 -t lib/main_$FLAVOR.dart
|
||||||
|
```
|
||||||
|
Just replace `$FLAVOR` with `dev` or `prod`.
|
||||||
|
|
||||||
|
# Contribution
|
||||||
|
|
||||||
To report a bug or suggest a new feature open a new [issue](https://github.com/vodemn/m3_lightmeter/issues).
|
To report a bug or suggest a new feature open a new [issue](https://github.com/vodemn/m3_lightmeter/issues).
|
||||||
|
|
||||||
In case you want to help develop this project you need to follow this [style guide](doc/style_guide.md).
|
In case you want to help develop this project feel free to open a Pull Request, but you need to follow this [style guide](doc/style_guide.md).
|
||||||
|
|
||||||
|
# iOS Limitations
|
||||||
|
|
||||||
|
A list of features, that Android version of the app has and that iOS does not.
|
||||||
|
|
||||||
|
## Incident light metering
|
||||||
|
|
||||||
|
Apple does not provide API for reading Lux stream form the ambient light sensor. Lux can be calculated based on front camera image stream, but this would be a reflected light. So there is no way incident light metering can be implemented on iOS.
|
||||||
|
|
||||||
|
## 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)
|
|
@ -1,6 +1,5 @@
|
||||||
include: package:lint/strict.yaml
|
include: package:lint/strict.yaml
|
||||||
|
|
||||||
|
|
||||||
linter:
|
linter:
|
||||||
rules:
|
rules:
|
||||||
use_setters_to_change_properties: false
|
use_setters_to_change_properties: false
|
||||||
|
|
|
@ -1,13 +1,22 @@
|
||||||
package com.vodemn.lightmeter
|
package com.vodemn.lightmeter
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.KeyEvent
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
|
import io.flutter.plugin.common.EventChannel
|
||||||
|
import io.flutter.plugin.common.EventChannel.EventSink
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
|
||||||
class MainActivity : FlutterActivity() {
|
class MainActivity : FlutterActivity() {
|
||||||
|
private lateinit var keepScreenOnChannel: MethodChannel
|
||||||
|
private lateinit var volumeHandlingChannel: MethodChannel
|
||||||
|
private lateinit var volumeEventChannel: EventChannel
|
||||||
|
private var volumeEventsEmitter: EventSink? = null
|
||||||
|
private var handleVolume = false
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
@ -15,10 +24,11 @@ class MainActivity : FlutterActivity() {
|
||||||
|
|
||||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
super.configureFlutterEngine(flutterEngine)
|
super.configureFlutterEngine(flutterEngine)
|
||||||
MethodChannel(
|
keepScreenOnChannel = MethodChannel(
|
||||||
flutterEngine.dartExecutor.binaryMessenger,
|
flutterEngine.dartExecutor.binaryMessenger,
|
||||||
"com.vodemn.lightmeter/keepScreenOn"
|
"com.vodemn.lightmeter/keepScreenOn"
|
||||||
).setMethodCallHandler { call, result ->
|
)
|
||||||
|
keepScreenOnChannel.setMethodCallHandler { call, result ->
|
||||||
when (call.method) {
|
when (call.method) {
|
||||||
"isKeepScreenOn" -> result.success((window.attributes.flags and WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) != 0)
|
"isKeepScreenOn" -> result.success((window.attributes.flags and WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) != 0)
|
||||||
"setKeepScreenOn" -> {
|
"setKeepScreenOn" -> {
|
||||||
|
@ -33,5 +43,53 @@ class MainActivity : FlutterActivity() {
|
||||||
else -> result.notImplemented()
|
else -> result.notImplemented()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
volumeHandlingChannel = MethodChannel(
|
||||||
|
flutterEngine.dartExecutor.binaryMessenger,
|
||||||
|
"com.vodemn.lightmeter/volumeHandling"
|
||||||
|
)
|
||||||
|
volumeHandlingChannel.setMethodCallHandler { call, result ->
|
||||||
|
when (call.method) {
|
||||||
|
"setVolumeHandling" -> {
|
||||||
|
handleVolume = call.arguments as Boolean
|
||||||
|
result.success(handleVolume)
|
||||||
|
}
|
||||||
|
else -> result.notImplemented()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
volumeEventChannel = EventChannel(
|
||||||
|
flutterEngine.dartExecutor.binaryMessenger,
|
||||||
|
"com.vodemn.lightmeter/volumeEvents"
|
||||||
|
)
|
||||||
|
volumeEventChannel.setStreamHandler(object : EventChannel.StreamHandler {
|
||||||
|
override fun onListen(listener: Any?, eventSink: EventSink) {
|
||||||
|
volumeEventsEmitter = eventSink
|
||||||
|
}
|
||||||
|
override fun onCancel(listener: Any?) {
|
||||||
|
volumeEventsEmitter = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
keepScreenOnChannel.setMethodCallHandler(null)
|
||||||
|
volumeHandlingChannel.setMethodCallHandler(null)
|
||||||
|
volumeEventChannel.setStreamHandler(null)
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onKeyDown(code: Int, event: KeyEvent): Boolean {
|
||||||
|
return when (val keyCode: Int = event.keyCode) {
|
||||||
|
KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN -> {
|
||||||
|
if (handleVolume) {
|
||||||
|
volumeEventsEmitter?.success(keyCode)
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
super.onKeyDown(code, event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> super.onKeyDown(code, event)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
import 'package:light_sensor/light_sensor.dart';
|
import 'package:light_sensor/light_sensor.dart';
|
||||||
|
import 'package:platform/platform.dart';
|
||||||
|
|
||||||
class LightSensorService {
|
class LightSensorService {
|
||||||
const LightSensorService();
|
final LocalPlatform localPlatform;
|
||||||
|
|
||||||
|
const LightSensorService(this.localPlatform);
|
||||||
|
|
||||||
Future<bool> hasSensor() async {
|
Future<bool> hasSensor() async {
|
||||||
|
if (!localPlatform.isAndroid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
return await LightSensor.hasSensor ?? false;
|
return await LightSensor.hasSensor ?? false;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
|
@ -11,5 +17,10 @@ class LightSensorService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<int> luxStream() => LightSensor.lightSensorStream;
|
Stream<int> luxStream() {
|
||||||
|
if (!localPlatform.isAndroid) {
|
||||||
|
return const Stream<int>.empty();
|
||||||
|
}
|
||||||
|
return LightSensor.lightSensorStream;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
enum SupportedLocale { en, fr, ru }
|
enum SupportedLocale { en, fr, ru, zh }
|
||||||
|
|
||||||
extension SupportedLocaleExtension on SupportedLocale {
|
extension SupportedLocaleExtension on SupportedLocale {
|
||||||
String get intlName => toString().replaceAll("SupportedLocale.", "");
|
String get intlName => toString().replaceAll("SupportedLocale.", "");
|
||||||
|
@ -11,6 +11,8 @@ extension SupportedLocaleExtension on SupportedLocale {
|
||||||
return 'Français';
|
return 'Français';
|
||||||
case SupportedLocale.ru:
|
case SupportedLocale.ru:
|
||||||
return 'Русский';
|
return 'Русский';
|
||||||
|
case SupportedLocale.zh:
|
||||||
|
return '简体中文';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
3
lib/data/models/volume_action.dart
Normal file
3
lib/data/models/volume_action.dart
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
enum VolumeAction { shutter, none }
|
||||||
|
|
||||||
|
enum VolumeKey { up, down }
|
|
@ -6,6 +6,7 @@ import 'package:lightmeter/data/models/film.dart';
|
||||||
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
|
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
|
||||||
import 'package:lightmeter/data/models/supported_locale.dart';
|
import 'package:lightmeter/data/models/supported_locale.dart';
|
||||||
import 'package:lightmeter/data/models/theme_type.dart';
|
import 'package:lightmeter/data/models/theme_type.dart';
|
||||||
|
import 'package:lightmeter/data/models/volume_action.dart';
|
||||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
@ -22,6 +23,7 @@ class UserPreferencesService {
|
||||||
|
|
||||||
static const caffeineKey = "caffeine";
|
static const caffeineKey = "caffeine";
|
||||||
static const hapticsKey = "haptics";
|
static const hapticsKey = "haptics";
|
||||||
|
static const volumeActionKey = "volumeAction";
|
||||||
static const localeKey = "locale";
|
static const localeKey = "locale";
|
||||||
|
|
||||||
static const themeTypeKey = "themeType";
|
static const themeTypeKey = "themeType";
|
||||||
|
@ -82,9 +84,6 @@ class UserPreferencesService {
|
||||||
EvSourceType.values[_sharedPreferences.getInt(evSourceTypeKey) ?? 0];
|
EvSourceType.values[_sharedPreferences.getInt(evSourceTypeKey) ?? 0];
|
||||||
set evSourceType(EvSourceType value) => _sharedPreferences.setInt(evSourceTypeKey, value.index);
|
set evSourceType(EvSourceType value) => _sharedPreferences.setInt(evSourceTypeKey, value.index);
|
||||||
|
|
||||||
bool get caffeine => _sharedPreferences.getBool(caffeineKey) ?? false;
|
|
||||||
set caffeine(bool value) => _sharedPreferences.setBool(caffeineKey, value);
|
|
||||||
|
|
||||||
StopType get stopType => StopType.values[_sharedPreferences.getInt(stopTypeKey) ?? 2];
|
StopType get stopType => StopType.values[_sharedPreferences.getInt(stopTypeKey) ?? 2];
|
||||||
set stopType(StopType value) => _sharedPreferences.setInt(stopTypeKey, value.index);
|
set stopType(StopType value) => _sharedPreferences.setInt(stopTypeKey, value.index);
|
||||||
|
|
||||||
|
@ -105,9 +104,19 @@ class UserPreferencesService {
|
||||||
set meteringScreenLayout(MeteringScreenLayoutConfig value) =>
|
set meteringScreenLayout(MeteringScreenLayoutConfig value) =>
|
||||||
_sharedPreferences.setString(meteringScreenLayoutKey, json.encode(value.toJson()));
|
_sharedPreferences.setString(meteringScreenLayoutKey, json.encode(value.toJson()));
|
||||||
|
|
||||||
|
bool get caffeine => _sharedPreferences.getBool(caffeineKey) ?? false;
|
||||||
|
set caffeine(bool value) => _sharedPreferences.setBool(caffeineKey, value);
|
||||||
|
|
||||||
bool get haptics => _sharedPreferences.getBool(hapticsKey) ?? true;
|
bool get haptics => _sharedPreferences.getBool(hapticsKey) ?? true;
|
||||||
set haptics(bool value) => _sharedPreferences.setBool(hapticsKey, value);
|
set haptics(bool value) => _sharedPreferences.setBool(hapticsKey, value);
|
||||||
|
|
||||||
|
VolumeAction get volumeAction => VolumeAction.values.firstWhere(
|
||||||
|
(e) => e.toString() == _sharedPreferences.getString(volumeActionKey),
|
||||||
|
orElse: () => VolumeAction.shutter,
|
||||||
|
);
|
||||||
|
set volumeAction(VolumeAction value) =>
|
||||||
|
_sharedPreferences.setString(volumeActionKey, value.toString());
|
||||||
|
|
||||||
SupportedLocale get locale => SupportedLocale.values.firstWhere(
|
SupportedLocale get locale => SupportedLocale.values.firstWhere(
|
||||||
(e) => e.toString() == _sharedPreferences.getString(localeKey),
|
(e) => e.toString() == _sharedPreferences.getString(localeKey),
|
||||||
orElse: () => SupportedLocale.en,
|
orElse: () => SupportedLocale.en,
|
||||||
|
@ -141,6 +150,6 @@ class UserPreferencesService {
|
||||||
String get selectedEquipmentProfileId => ''; // coverage:ignore-line
|
String get selectedEquipmentProfileId => ''; // coverage:ignore-line
|
||||||
set selectedEquipmentProfileId(String id) {} // coverage:ignore-line
|
set selectedEquipmentProfileId(String id) {} // coverage:ignore-line
|
||||||
|
|
||||||
List<EquipmentProfileData> get equipmentProfiles => []; // coverage:ignore-line
|
List<EquipmentProfile> get equipmentProfiles => []; // coverage:ignore-line
|
||||||
set equipmentProfiles(List<EquipmentProfileData> profiles) {} // coverage:ignore-line
|
set equipmentProfiles(List<EquipmentProfile> profiles) {} // coverage:ignore-line
|
||||||
}
|
}
|
||||||
|
|
40
lib/data/volume_events_service.dart
Normal file
40
lib/data/volume_events_service.dart
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:platform/platform.dart';
|
||||||
|
|
||||||
|
class VolumeEventsService {
|
||||||
|
final LocalPlatform localPlatform;
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
static const volumeHandlingChannel = MethodChannel("com.vodemn.lightmeter/volumeHandling");
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
static const volumeEventsChannel = EventChannel("com.vodemn.lightmeter/volumeEvents");
|
||||||
|
|
||||||
|
const VolumeEventsService(this.localPlatform);
|
||||||
|
|
||||||
|
/// If set to `false` we allow system to handle key events.
|
||||||
|
/// Returns current status of volume handling.
|
||||||
|
Future<bool> setVolumeHandling(bool enableHandling) async {
|
||||||
|
if (!localPlatform.isAndroid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return volumeHandlingChannel
|
||||||
|
.invokeMethod<bool>("setVolumeHandling", enableHandling)
|
||||||
|
.then((value) => value!);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emits new events on
|
||||||
|
/// KEYCODE_VOLUME_UP = 24;
|
||||||
|
/// KEYCODE_VOLUME_DOWN = 25;
|
||||||
|
/// pressed
|
||||||
|
Stream<int> volumeButtonsEventStream() {
|
||||||
|
if (!localPlatform.isAndroid) {
|
||||||
|
return const Stream.empty();
|
||||||
|
}
|
||||||
|
return volumeEventsChannel
|
||||||
|
.receiveBroadcastStream()
|
||||||
|
.cast<int>()
|
||||||
|
.where((event) => event == 24 || event == 25);
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,14 +19,14 @@ class Environment {
|
||||||
const Environment.dev()
|
const Environment.dev()
|
||||||
: buildType = BuildType.dev,
|
: buildType = BuildType.dev,
|
||||||
sourceCodeUrl = 'https://github.com/vodemn/m3_lightmeter',
|
sourceCodeUrl = 'https://github.com/vodemn/m3_lightmeter',
|
||||||
issuesReportUrl = 'https://github.com/vodemn/m3_lightmeter/issues',
|
issuesReportUrl = 'https://github.com/vodemn/m3_lightmeter/issues/new/choose',
|
||||||
contactEmail = 'contact.vodemn@gmail.com',
|
contactEmail = 'contact.vodemn@gmail.com',
|
||||||
hasLightSensor = false;
|
hasLightSensor = false;
|
||||||
|
|
||||||
const Environment.prod()
|
const Environment.prod()
|
||||||
: buildType = BuildType.prod,
|
: buildType = BuildType.prod,
|
||||||
sourceCodeUrl = 'https://github.com/vodemn/m3_lightmeter',
|
sourceCodeUrl = 'https://github.com/vodemn/m3_lightmeter',
|
||||||
issuesReportUrl = 'https://github.com/vodemn/m3_lightmeter/issues',
|
issuesReportUrl = 'https://github.com/vodemn/m3_lightmeter/issues/new/choose',
|
||||||
contactEmail = 'contact.vodemn@gmail.com',
|
contactEmail = 'contact.vodemn@gmail.com',
|
||||||
hasLightSensor = false;
|
hasLightSensor = false;
|
||||||
|
|
||||||
|
|
68
lib/firebase_options.dart
Normal file
68
lib/firebase_options.dart
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
// File generated by FlutterFire CLI.
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members
|
||||||
|
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
|
||||||
|
import 'package:flutter/foundation.dart' show defaultTargetPlatform, kIsWeb, TargetPlatform;
|
||||||
|
|
||||||
|
/// Default [FirebaseOptions] for use with your Firebase apps.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```dart
|
||||||
|
/// import 'firebase_options.dart';
|
||||||
|
/// // ...
|
||||||
|
/// await Firebase.initializeApp(
|
||||||
|
/// options: DefaultFirebaseOptions.currentPlatform,
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
class DefaultFirebaseOptions {
|
||||||
|
static FirebaseOptions get currentPlatform {
|
||||||
|
if (kIsWeb) {
|
||||||
|
throw UnsupportedError(
|
||||||
|
'DefaultFirebaseOptions have not been configured for web - '
|
||||||
|
'you can reconfigure this by running the FlutterFire CLI again.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
switch (defaultTargetPlatform) {
|
||||||
|
case TargetPlatform.android:
|
||||||
|
return android;
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
return ios;
|
||||||
|
case TargetPlatform.macOS:
|
||||||
|
throw UnsupportedError(
|
||||||
|
'DefaultFirebaseOptions have not been configured for macos - '
|
||||||
|
'you can reconfigure this by running the FlutterFire CLI again.',
|
||||||
|
);
|
||||||
|
case TargetPlatform.windows:
|
||||||
|
throw UnsupportedError(
|
||||||
|
'DefaultFirebaseOptions have not been configured for windows - '
|
||||||
|
'you can reconfigure this by running the FlutterFire CLI again.',
|
||||||
|
);
|
||||||
|
case TargetPlatform.linux:
|
||||||
|
throw UnsupportedError(
|
||||||
|
'DefaultFirebaseOptions have not been configured for linux - '
|
||||||
|
'you can reconfigure this by running the FlutterFire CLI again.',
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
throw UnsupportedError(
|
||||||
|
'DefaultFirebaseOptions are not supported for this platform.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static const FirebaseOptions android = FirebaseOptions(
|
||||||
|
apiKey: '',
|
||||||
|
appId: '',
|
||||||
|
messagingSenderId: '',
|
||||||
|
projectId: '',
|
||||||
|
storageBucket: '',
|
||||||
|
);
|
||||||
|
|
||||||
|
static const FirebaseOptions ios = FirebaseOptions(
|
||||||
|
apiKey: '',
|
||||||
|
appId: '',
|
||||||
|
messagingSenderId: '',
|
||||||
|
projectId: '',
|
||||||
|
storageBucket: '',
|
||||||
|
iosClientId: '',
|
||||||
|
iosBundleId: '',
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,12 +1,12 @@
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:app_settings/app_settings.dart';
|
import 'package:app_settings/app_settings.dart';
|
||||||
import 'package:lightmeter/data/caffeine_service.dart';
|
import 'package:lightmeter/data/caffeine_service.dart';
|
||||||
import 'package:lightmeter/data/haptics_service.dart';
|
import 'package:lightmeter/data/haptics_service.dart';
|
||||||
import 'package:lightmeter/data/light_sensor_service.dart';
|
import 'package:lightmeter/data/light_sensor_service.dart';
|
||||||
import 'package:lightmeter/data/models/film.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/permissions_service.dart';
|
||||||
import 'package:lightmeter/data/shared_prefs_service.dart';
|
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||||
|
import 'package:lightmeter/data/volume_events_service.dart';
|
||||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ class MeteringInteractor {
|
||||||
final HapticsService _hapticsService;
|
final HapticsService _hapticsService;
|
||||||
final PermissionsService _permissionsService;
|
final PermissionsService _permissionsService;
|
||||||
final LightSensorService _lightSensorService;
|
final LightSensorService _lightSensorService;
|
||||||
|
final VolumeEventsService _volumeEventsService;
|
||||||
|
|
||||||
MeteringInteractor(
|
MeteringInteractor(
|
||||||
this._userPreferencesService,
|
this._userPreferencesService,
|
||||||
|
@ -23,10 +24,15 @@ class MeteringInteractor {
|
||||||
this._hapticsService,
|
this._hapticsService,
|
||||||
this._permissionsService,
|
this._permissionsService,
|
||||||
this._lightSensorService,
|
this._lightSensorService,
|
||||||
) {
|
this._volumeEventsService,
|
||||||
|
);
|
||||||
|
|
||||||
|
void initialize() {
|
||||||
if (_userPreferencesService.caffeine) {
|
if (_userPreferencesService.caffeine) {
|
||||||
_caffeineService.keepScreenOn(true);
|
_caffeineService.keepScreenOn(true);
|
||||||
}
|
}
|
||||||
|
_volumeEventsService
|
||||||
|
.setVolumeHandling(_userPreferencesService.volumeAction != VolumeAction.none);
|
||||||
}
|
}
|
||||||
|
|
||||||
double get cameraEvCalibration => _userPreferencesService.cameraEvCalibration;
|
double get cameraEvCalibration => _userPreferencesService.cameraEvCalibration;
|
||||||
|
@ -42,6 +48,8 @@ class MeteringInteractor {
|
||||||
Film get film => _userPreferencesService.film;
|
Film get film => _userPreferencesService.film;
|
||||||
set film(Film value) => _userPreferencesService.film = value;
|
set film(Film value) => _userPreferencesService.film = value;
|
||||||
|
|
||||||
|
VolumeAction get volumeAction => _userPreferencesService.volumeAction;
|
||||||
|
|
||||||
/// Executes vibration if haptics are enabled in settings
|
/// Executes vibration if haptics are enabled in settings
|
||||||
Future<void> quickVibration() async {
|
Future<void> quickVibration() async {
|
||||||
if (_userPreferencesService.haptics) await _hapticsService.quickVibration();
|
if (_userPreferencesService.haptics) await _hapticsService.quickVibration();
|
||||||
|
@ -63,7 +71,7 @@ class MeteringInteractor {
|
||||||
.then((value) => value == PermissionStatus.granted);
|
.then((value) => value == PermissionStatus.granted);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> requestPermission() async {
|
Future<bool> requestCameraPermission() async {
|
||||||
return _permissionsService
|
return _permissionsService
|
||||||
.requestCameraPermission()
|
.requestCameraPermission()
|
||||||
.then((value) => value == PermissionStatus.granted);
|
.then((value) => value == PermissionStatus.granted);
|
||||||
|
@ -73,13 +81,7 @@ class MeteringInteractor {
|
||||||
AppSettings.openAppSettings();
|
AppSettings.openAppSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> hasAmbientLightSensor() async {
|
Future<bool> hasAmbientLightSensor() async => _lightSensorService.hasSensor();
|
||||||
if (Platform.isAndroid) {
|
|
||||||
return _lightSensorService.hasSensor();
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Stream<int> luxStream() => _lightSensorService.luxStream();
|
Stream<int> luxStream() => _lightSensorService.luxStream();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,20 @@
|
||||||
import 'package:lightmeter/data/caffeine_service.dart';
|
import 'package:lightmeter/data/caffeine_service.dart';
|
||||||
import 'package:lightmeter/data/haptics_service.dart';
|
import 'package:lightmeter/data/haptics_service.dart';
|
||||||
|
import 'package:lightmeter/data/models/volume_action.dart';
|
||||||
import 'package:lightmeter/data/shared_prefs_service.dart';
|
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||||
|
import 'package:lightmeter/data/volume_events_service.dart';
|
||||||
|
|
||||||
class SettingsInteractor {
|
class SettingsInteractor {
|
||||||
final UserPreferencesService _userPreferencesService;
|
final UserPreferencesService _userPreferencesService;
|
||||||
final CaffeineService _caffeineService;
|
final CaffeineService _caffeineService;
|
||||||
final HapticsService _hapticsService;
|
final HapticsService _hapticsService;
|
||||||
|
final VolumeEventsService _volumeEventsService;
|
||||||
|
|
||||||
const SettingsInteractor(
|
const SettingsInteractor(
|
||||||
this._userPreferencesService,
|
this._userPreferencesService,
|
||||||
this._caffeineService,
|
this._caffeineService,
|
||||||
this._hapticsService,
|
this._hapticsService,
|
||||||
|
this._volumeEventsService,
|
||||||
);
|
);
|
||||||
|
|
||||||
double get cameraEvCalibration => _userPreferencesService.cameraEvCalibration;
|
double get cameraEvCalibration => _userPreferencesService.cameraEvCalibration;
|
||||||
|
@ -27,6 +31,20 @@ class SettingsInteractor {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> disableVolumeHandling() async {
|
||||||
|
await _volumeEventsService.setVolumeHandling(false);
|
||||||
|
}
|
||||||
|
Future<void> restoreVolumeHandling() async {
|
||||||
|
await _volumeEventsService
|
||||||
|
.setVolumeHandling(_userPreferencesService.volumeAction != VolumeAction.none);
|
||||||
|
}
|
||||||
|
|
||||||
|
VolumeAction get volumeAction => _userPreferencesService.volumeAction;
|
||||||
|
Future<void> setVolumeAction(VolumeAction value) async {
|
||||||
|
_userPreferencesService.volumeAction = value;
|
||||||
|
await _volumeEventsService.setVolumeHandling(value != VolumeAction.none);
|
||||||
|
}
|
||||||
|
|
||||||
bool get isHapticsEnabled => _userPreferencesService.haptics;
|
bool get isHapticsEnabled => _userPreferencesService.haptics;
|
||||||
void enableHaptics(bool enable) {
|
void enableHaptics(bool enable) {
|
||||||
_userPreferencesService.haptics = enable;
|
_userPreferencesService.haptics = enable;
|
||||||
|
|
|
@ -56,6 +56,7 @@
|
||||||
"general": "General",
|
"general": "General",
|
||||||
"keepScreenOn": "Keep screen on",
|
"keepScreenOn": "Keep screen on",
|
||||||
"haptics": "Haptics",
|
"haptics": "Haptics",
|
||||||
|
"volumeKeysAction": "Shutter by volume keys",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
"chooseLanguage": "Choose language",
|
"chooseLanguage": "Choose language",
|
||||||
"theme": "Theme",
|
"theme": "Theme",
|
||||||
|
|
|
@ -56,6 +56,7 @@
|
||||||
"general": "Général",
|
"general": "Général",
|
||||||
"keepScreenOn": "Garder l'écran allumé",
|
"keepScreenOn": "Garder l'écran allumé",
|
||||||
"haptics": "Haptiques",
|
"haptics": "Haptiques",
|
||||||
|
"volumeKeysAction": "Obturateur par boutons de volume",
|
||||||
"language": "Langue",
|
"language": "Langue",
|
||||||
"chooseLanguage": "Choisissez la langue",
|
"chooseLanguage": "Choisissez la langue",
|
||||||
"theme": "Thème",
|
"theme": "Thème",
|
||||||
|
|
|
@ -56,6 +56,7 @@
|
||||||
"general": "Общие",
|
"general": "Общие",
|
||||||
"keepScreenOn": "Запрет блокировки",
|
"keepScreenOn": "Запрет блокировки",
|
||||||
"haptics": "Вибрация",
|
"haptics": "Вибрация",
|
||||||
|
"volumeKeysAction": "Затвор по кнопкам громкости",
|
||||||
"language": "Язык",
|
"language": "Язык",
|
||||||
"chooseLanguage": "Выберите язык",
|
"chooseLanguage": "Выберите язык",
|
||||||
"theme": "Тема",
|
"theme": "Тема",
|
||||||
|
|
88
lib/l10n/intl_zh.arb
Normal file
88
lib/l10n/intl_zh.arb
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
{
|
||||||
|
"@@locale": "zh",
|
||||||
|
"fastestExposurePair": "最快曝光组合",
|
||||||
|
"slowestExposurePair": "最慢曝光组合",
|
||||||
|
"ev": "EV",
|
||||||
|
"evValue": "{value} EV",
|
||||||
|
"@evValue": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"iso": "ISO",
|
||||||
|
"filmSpeed": "胶片感光度",
|
||||||
|
"nd": "ND",
|
||||||
|
"ndFilterFactor": "ND 滤镜系数",
|
||||||
|
"noExposurePairs": "所选设置没有曝光配对",
|
||||||
|
"noCamerasDetected": "您的设备似乎没有连接到任何摄像头",
|
||||||
|
"noCameraPermission": "未获得摄像头权限",
|
||||||
|
"otherCameraError": "连接摄像头时发生错误",
|
||||||
|
"none": "无",
|
||||||
|
"cancel": "取消",
|
||||||
|
"select": "选择",
|
||||||
|
"save": "保存",
|
||||||
|
"settings": "设置",
|
||||||
|
"metering": "测量",
|
||||||
|
"fractionalStops": "EV 步进值",
|
||||||
|
"showFractionalStops": "显示 EV 步进值",
|
||||||
|
"halfStops": "1/2",
|
||||||
|
"thirdStops": "1/3",
|
||||||
|
"calibration": "校准",
|
||||||
|
"calibrationMessage": "此应用测量读数的准确性完全取决于设备的硬件。因此,请考虑测试此应用并手动设置 EV 校准,以获得准确的测量结果。",
|
||||||
|
"calibrationMessageCameraOnly": "此应用程序测量读数的准确性完全取决于设备的后置摄像头。因此,请考虑测试此应用并手动设置 EV 校准,以获得准确的测量结果。",
|
||||||
|
"camera": "摄像头",
|
||||||
|
"lightSensor": "光传感器",
|
||||||
|
"meteringScreenLayout": "布局",
|
||||||
|
"meteringScreenLayoutHint": "隐藏不需要的元素,以免浪费曝光列表空间",
|
||||||
|
"meteringScreenFeatureExtremeExposurePairs": "最快 & 最慢曝光组合",
|
||||||
|
"meteringScreenFeatureFilmPicker": "胶片选择",
|
||||||
|
"film": "胶片",
|
||||||
|
"equipment": "设备",
|
||||||
|
"equipmentProfileName": "设备配置名称",
|
||||||
|
"equipmentProfileNameHint": "Praktica MTL5B",
|
||||||
|
"equipmentProfileAllValues": "全部",
|
||||||
|
"apertureValues": "光圈值",
|
||||||
|
"apertureValuesFilterDescription": "选择要显示的光圈值范围。这通常由您使用的镜头决定。",
|
||||||
|
"ndFilters": "ND 滤镜",
|
||||||
|
"ndFiltersFilterDescription": "选择要显示的 ND 滤镜系数。这些可能是您最常用的 ND 滤镜,也可能是适合您镜头的滤光镜。",
|
||||||
|
"shutterSpeedValues": "快门速度",
|
||||||
|
"shutterSpeedValuesFilterDescription": "选择要显示的快门速度范围。这通常由您使用的相机机身决定。",
|
||||||
|
"isoValues": "ISO",
|
||||||
|
"isoValuesFilterDescription": "选择要显示的 ISO。这些值可能是您最常用的值,也可能是相机支持的值。",
|
||||||
|
"equipmentProfile": "设备配置",
|
||||||
|
"equipmentProfiles": "设备配置",
|
||||||
|
"general": "通用",
|
||||||
|
"keepScreenOn": "保持屏幕常亮",
|
||||||
|
"haptics": "震动",
|
||||||
|
"volumeKeysAction": "音量键快门",
|
||||||
|
"language": "语言",
|
||||||
|
"chooseLanguage": "选择语言",
|
||||||
|
"theme": "主题",
|
||||||
|
"chooseTheme": "选择主题",
|
||||||
|
"themeLight": "亮色",
|
||||||
|
"themeDark": "暗色",
|
||||||
|
"themeSystemDefault": "跟随系统",
|
||||||
|
"dynamicColor": "动态颜色",
|
||||||
|
"primaryColor": "主题颜色",
|
||||||
|
"choosePrimaryColor": "选择主题颜色",
|
||||||
|
"about": "关于",
|
||||||
|
"sourceCode": "源代码",
|
||||||
|
"reportIssue": "报告问题",
|
||||||
|
"writeEmail": "Email",
|
||||||
|
"youDontHaveMailApp": "您没有安装任何邮件App。",
|
||||||
|
"copyEmail": "复制电子邮件",
|
||||||
|
"version": "Version",
|
||||||
|
"versionNumber": "{version} ({buildNumber})",
|
||||||
|
"@versionNumber": {
|
||||||
|
"placeholders": {
|
||||||
|
"version": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"buildNumber": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:lightmeter/application.dart';
|
import 'package:lightmeter/application.dart';
|
||||||
import 'package:lightmeter/environment.dart';
|
import 'package:lightmeter/environment.dart';
|
||||||
|
@ -5,7 +7,10 @@ import 'package:lightmeter/firebase.dart';
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
try {
|
||||||
await initializeFirebase();
|
await initializeFirebase();
|
||||||
|
} catch (e) {
|
||||||
|
log(e.toString());
|
||||||
|
}
|
||||||
runApp(const Application(Environment.prod()));
|
runApp(const Application(Environment.prod()));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:lightmeter/data/caffeine_service.dart';
|
import 'package:lightmeter/data/caffeine_service.dart';
|
||||||
import 'package:lightmeter/data/haptics_service.dart';
|
import 'package:lightmeter/data/haptics_service.dart';
|
||||||
import 'package:lightmeter/data/light_sensor_service.dart';
|
import 'package:lightmeter/data/light_sensor_service.dart';
|
||||||
import 'package:lightmeter/data/permissions_service.dart';
|
import 'package:lightmeter/data/permissions_service.dart';
|
||||||
import 'package:lightmeter/data/shared_prefs_service.dart';
|
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||||
|
import 'package:lightmeter/data/volume_events_service.dart';
|
||||||
import 'package:lightmeter/environment.dart';
|
import 'package:lightmeter/environment.dart';
|
||||||
import 'package:lightmeter/providers/equipment_profile_provider.dart';
|
import 'package:lightmeter/providers/equipment_profile_provider.dart';
|
||||||
import 'package:lightmeter/providers/ev_source_type_provider.dart';
|
import 'package:lightmeter/providers/ev_source_type_provider.dart';
|
||||||
|
@ -14,6 +13,7 @@ import 'package:lightmeter/providers/stop_type_provider.dart';
|
||||||
import 'package:lightmeter/providers/supported_locale_provider.dart';
|
import 'package:lightmeter/providers/supported_locale_provider.dart';
|
||||||
import 'package:lightmeter/providers/theme_provider.dart';
|
import 'package:lightmeter/providers/theme_provider.dart';
|
||||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||||
|
import 'package:platform/platform.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
class LightmeterProviders extends StatelessWidget {
|
class LightmeterProviders extends StatelessWidget {
|
||||||
|
@ -27,7 +27,7 @@ class LightmeterProviders extends StatelessWidget {
|
||||||
return FutureBuilder(
|
return FutureBuilder(
|
||||||
future: Future.wait([
|
future: Future.wait([
|
||||||
SharedPreferences.getInstance(),
|
SharedPreferences.getInstance(),
|
||||||
if (Platform.isAndroid) const LightSensorService().hasSensor() else Future.value(false),
|
const LightSensorService(LocalPlatform()).hasSensor(),
|
||||||
]),
|
]),
|
||||||
builder: (_, snapshot) {
|
builder: (_, snapshot) {
|
||||||
if (snapshot.data != null) {
|
if (snapshot.data != null) {
|
||||||
|
@ -36,11 +36,13 @@ class LightmeterProviders extends StatelessWidget {
|
||||||
child: InheritedWidgetBase<UserPreferencesService>(
|
child: InheritedWidgetBase<UserPreferencesService>(
|
||||||
data: UserPreferencesService(snapshot.data![0] as SharedPreferences),
|
data: UserPreferencesService(snapshot.data![0] as SharedPreferences),
|
||||||
child: InheritedWidgetBase<LightSensorService>(
|
child: InheritedWidgetBase<LightSensorService>(
|
||||||
data: const LightSensorService(),
|
data: const LightSensorService(LocalPlatform()),
|
||||||
child: InheritedWidgetBase<CaffeineService>(
|
child: InheritedWidgetBase<CaffeineService>(
|
||||||
data: const CaffeineService(),
|
data: const CaffeineService(),
|
||||||
child: InheritedWidgetBase<HapticsService>(
|
child: InheritedWidgetBase<HapticsService>(
|
||||||
data: const HapticsService(),
|
data: const HapticsService(),
|
||||||
|
child: InheritedWidgetBase<VolumeEventsService>(
|
||||||
|
data: const VolumeEventsService(LocalPlatform()),
|
||||||
child: InheritedWidgetBase<PermissionsService>(
|
child: InheritedWidgetBase<PermissionsService>(
|
||||||
data: const PermissionsService(),
|
data: const PermissionsService(),
|
||||||
child: MeteringScreenLayoutProvider(
|
child: MeteringScreenLayoutProvider(
|
||||||
|
@ -63,6 +65,7 @@ class LightmeterProviders extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else if (snapshot.error != null) {
|
} else if (snapshot.error != null) {
|
||||||
return Center(child: Text(snapshot.error!.toString()));
|
return Center(child: Text(snapshot.error!.toString()));
|
||||||
|
|
|
@ -4,8 +4,7 @@ import 'package:lightmeter/utils/inherited_generics.dart';
|
||||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
typedef EquipmentProfiles = List<EquipmentProfileData>;
|
typedef EquipmentProfiles = List<EquipmentProfile>;
|
||||||
typedef EquipmentProfile = EquipmentProfileData;
|
|
||||||
|
|
||||||
class EquipmentProfileProvider extends StatefulWidget {
|
class EquipmentProfileProvider extends StatefulWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
@ -21,7 +20,7 @@ class EquipmentProfileProvider extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class EquipmentProfileProviderState extends State<EquipmentProfileProvider> {
|
class EquipmentProfileProviderState extends State<EquipmentProfileProvider> {
|
||||||
static const EquipmentProfileData _defaultProfile = EquipmentProfileData(
|
static const EquipmentProfile _defaultProfile = EquipmentProfile(
|
||||||
id: '',
|
id: '',
|
||||||
name: '',
|
name: '',
|
||||||
apertureValues: ApertureValue.values,
|
apertureValues: ApertureValue.values,
|
||||||
|
@ -30,10 +29,10 @@ class EquipmentProfileProviderState extends State<EquipmentProfileProvider> {
|
||||||
isoValues: IsoValue.values,
|
isoValues: IsoValue.values,
|
||||||
);
|
);
|
||||||
|
|
||||||
List<EquipmentProfileData> _customProfiles = [];
|
List<EquipmentProfile> _customProfiles = [];
|
||||||
String _selectedId = '';
|
String _selectedId = '';
|
||||||
|
|
||||||
EquipmentProfileData get _selectedProfile => _customProfiles.firstWhere(
|
EquipmentProfile get _selectedProfile => _customProfiles.firstWhere(
|
||||||
(e) => e.id == _selectedId,
|
(e) => e.id == _selectedId,
|
||||||
orElse: () {
|
orElse: () {
|
||||||
context.get<UserPreferencesService>().selectedEquipmentProfileId = _defaultProfile.id;
|
context.get<UserPreferencesService>().selectedEquipmentProfileId = _defaultProfile.id;
|
||||||
|
@ -50,16 +49,16 @@ class EquipmentProfileProviderState extends State<EquipmentProfileProvider> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return InheritedWidgetBase<List<EquipmentProfileData>>(
|
return InheritedWidgetBase<List<EquipmentProfile>>(
|
||||||
data: [_defaultProfile] + _customProfiles,
|
data: [_defaultProfile] + _customProfiles,
|
||||||
child: InheritedWidgetBase<EquipmentProfileData>(
|
child: InheritedWidgetBase<EquipmentProfile>(
|
||||||
data: _selectedProfile,
|
data: _selectedProfile,
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setProfile(EquipmentProfileData data) {
|
void setProfile(EquipmentProfile data) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedId = data.id;
|
_selectedId = data.id;
|
||||||
});
|
});
|
||||||
|
@ -69,7 +68,7 @@ class EquipmentProfileProviderState extends State<EquipmentProfileProvider> {
|
||||||
/// Creates a default equipment profile
|
/// Creates a default equipment profile
|
||||||
void addProfile(String name) {
|
void addProfile(String name) {
|
||||||
_customProfiles.add(
|
_customProfiles.add(
|
||||||
EquipmentProfileData(
|
EquipmentProfile(
|
||||||
id: const Uuid().v1(),
|
id: const Uuid().v1(),
|
||||||
name: name,
|
name: name,
|
||||||
apertureValues: ApertureValue.values,
|
apertureValues: ApertureValue.values,
|
||||||
|
@ -81,7 +80,7 @@ class EquipmentProfileProviderState extends State<EquipmentProfileProvider> {
|
||||||
_refreshSavedProfiles();
|
_refreshSavedProfiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateProdile(EquipmentProfileData data) {
|
void updateProdile(EquipmentProfile data) {
|
||||||
final indexToUpdate = _customProfiles.indexWhere((element) => element.id == data.id);
|
final indexToUpdate = _customProfiles.indexWhere((element) => element.id == data.id);
|
||||||
if (indexToUpdate >= 0) {
|
if (indexToUpdate >= 0) {
|
||||||
_customProfiles[indexToUpdate] = data;
|
_customProfiles[indexToUpdate] = data;
|
||||||
|
@ -89,7 +88,7 @@ class EquipmentProfileProviderState extends State<EquipmentProfileProvider> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void deleteProfile(EquipmentProfileData data) {
|
void deleteProfile(EquipmentProfile data) {
|
||||||
_customProfiles.remove(data);
|
_customProfiles.remove(data);
|
||||||
_refreshSavedProfiles();
|
_refreshSavedProfiles();
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ class Dimens {
|
||||||
static const Duration durationM = Duration(milliseconds: 200);
|
static const Duration durationM = Duration(milliseconds: 200);
|
||||||
static const Duration durationML = Duration(milliseconds: 250);
|
static const Duration durationML = Duration(milliseconds: 250);
|
||||||
static const Duration durationL = Duration(milliseconds: 300);
|
static const Duration durationL = Duration(milliseconds: 300);
|
||||||
|
static const Duration switchDuration = Duration(milliseconds: 100);
|
||||||
|
|
||||||
static const double enabledOpacity = 1.0;
|
static const double enabledOpacity = 1.0;
|
||||||
static const double disabledOpacity = 0.38;
|
static const double disabledOpacity = 0.38;
|
||||||
|
|
|
@ -4,23 +4,27 @@ import 'package:bloc_concurrency/bloc_concurrency.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:lightmeter/data/models/film.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/interactors/metering_interactor.dart';
|
||||||
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
|
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
|
||||||
import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart'
|
import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart'
|
||||||
as communication_events;
|
as communication_events;
|
||||||
import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart'
|
import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart'
|
||||||
as communication_states;
|
as communication_states;
|
||||||
|
import 'package:lightmeter/screens/metering/components/shared/volume_keys_notifier/notifier_volume_keys.dart';
|
||||||
import 'package:lightmeter/screens/metering/event_metering.dart';
|
import 'package:lightmeter/screens/metering/event_metering.dart';
|
||||||
import 'package:lightmeter/screens/metering/state_metering.dart';
|
import 'package:lightmeter/screens/metering/state_metering.dart';
|
||||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||||
|
|
||||||
class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
|
class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
|
||||||
final MeteringInteractor _meteringInteractor;
|
final MeteringInteractor _meteringInteractor;
|
||||||
|
final VolumeKeysNotifier _volumeKeysNotifier;
|
||||||
final MeteringCommunicationBloc _communicationBloc;
|
final MeteringCommunicationBloc _communicationBloc;
|
||||||
late final StreamSubscription<communication_states.ScreenState> _communicationSubscription;
|
late final StreamSubscription<communication_states.ScreenState> _communicationSubscription;
|
||||||
|
|
||||||
MeteringBloc(
|
MeteringBloc(
|
||||||
this._meteringInteractor,
|
this._meteringInteractor,
|
||||||
|
this._volumeKeysNotifier,
|
||||||
this._communicationBloc,
|
this._communicationBloc,
|
||||||
) : super(
|
) : super(
|
||||||
MeteringDataState(
|
MeteringDataState(
|
||||||
|
@ -31,6 +35,7 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
|
||||||
isMetering: false,
|
isMetering: false,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
|
_volumeKeysNotifier.addListener(onVolumeKey);
|
||||||
_communicationSubscription = _communicationBloc.stream
|
_communicationSubscription = _communicationBloc.stream
|
||||||
.where((state) => state is communication_states.ScreenState)
|
.where((state) => state is communication_states.ScreenState)
|
||||||
.map((state) => state as communication_states.ScreenState)
|
.map((state) => state as communication_states.ScreenState)
|
||||||
|
@ -43,6 +48,8 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
|
||||||
on<MeasureEvent>(_onMeasure, transformer: droppable());
|
on<MeasureEvent>(_onMeasure, transformer: droppable());
|
||||||
on<MeasuredEvent>(_onMeasured);
|
on<MeasuredEvent>(_onMeasured);
|
||||||
on<MeasureErrorEvent>(_onMeasureError);
|
on<MeasureErrorEvent>(_onMeasureError);
|
||||||
|
on<SettingsOpenedEvent>(_onSettingsOpened);
|
||||||
|
on<SettingsClosedEvent>(_onSettingsClosed);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -64,6 +71,7 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> close() async {
|
Future<void> close() async {
|
||||||
|
_volumeKeysNotifier.removeListener(onVolumeKey);
|
||||||
await _communicationSubscription.cancel();
|
await _communicationSubscription.cancel();
|
||||||
return super.close();
|
return super.close();
|
||||||
}
|
}
|
||||||
|
@ -220,4 +228,19 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
void onVolumeKey() {
|
||||||
|
if (_meteringInteractor.volumeAction == VolumeAction.shutter) {
|
||||||
|
add(const MeasureEvent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSettingsOpened(SettingsOpenedEvent _, Emitter __) {
|
||||||
|
_communicationBloc.add(const communication_events.SettingsOpenedEvent());
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSettingsClosed(SettingsClosedEvent _, Emitter __) {
|
||||||
|
_communicationBloc.add(const communication_events.SettingsClosedEvent());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,5 +11,7 @@ class MeteringCommunicationBloc
|
||||||
on<MeasureEvent>((_, emit) => emit(MeasureState()));
|
on<MeasureEvent>((_, emit) => emit(MeasureState()));
|
||||||
on<MeteringInProgressEvent>((event, emit) => emit(MeteringInProgressState(event.ev100)));
|
on<MeteringInProgressEvent>((event, emit) => emit(MeteringInProgressState(event.ev100)));
|
||||||
on<MeteringEndedEvent>((event, emit) => emit(MeteringEndedState(event.ev100)));
|
on<MeteringEndedEvent>((event, emit) => emit(MeteringEndedState(event.ev100)));
|
||||||
|
on<SettingsOpenedEvent>((_, emit) => emit(const SettingsOpenedState()));
|
||||||
|
on<SettingsClosedEvent>((_, emit) => emit(const SettingsClosedState()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,3 +47,11 @@ class MeteringEndedEvent extends MeasuredEvent {
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(ev100, runtimeType);
|
int get hashCode => Object.hash(ev100, runtimeType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SettingsOpenedEvent extends ScreenEvent {
|
||||||
|
const SettingsOpenedEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class SettingsClosedEvent extends ScreenEvent {
|
||||||
|
const SettingsClosedEvent();
|
||||||
|
}
|
||||||
|
|
|
@ -51,3 +51,11 @@ class MeteringEndedState extends MeasuredState {
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(ev100, runtimeType);
|
int get hashCode => Object.hash(ev100, runtimeType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SettingsOpenedState extends SourceState {
|
||||||
|
const SettingsOpenedState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class SettingsClosedState extends SourceState {
|
||||||
|
const SettingsClosedState();
|
||||||
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import 'package:lightmeter/utils/log_2.dart';
|
||||||
class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraContainerState> {
|
class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraContainerState> {
|
||||||
final MeteringInteractor _meteringInteractor;
|
final MeteringInteractor _meteringInteractor;
|
||||||
late final _WidgetsBindingObserver _observer;
|
late final _WidgetsBindingObserver _observer;
|
||||||
|
|
||||||
CameraController? _cameraController;
|
CameraController? _cameraController;
|
||||||
|
|
||||||
static const _maxZoom = 7.0;
|
static const _maxZoom = 7.0;
|
||||||
|
@ -36,6 +37,8 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
|
||||||
|
|
||||||
double? _ev100 = 0.0;
|
double? _ev100 = 0.0;
|
||||||
|
|
||||||
|
bool _settingsOpened = false;
|
||||||
|
|
||||||
CameraContainerBloc(
|
CameraContainerBloc(
|
||||||
this._meteringInteractor,
|
this._meteringInteractor,
|
||||||
MeteringCommunicationBloc communicationBloc,
|
MeteringCommunicationBloc communicationBloc,
|
||||||
|
@ -65,7 +68,8 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onCommunicationState(communication_states.SourceState communicationState) {
|
void onCommunicationState(communication_states.SourceState communicationState) {
|
||||||
if (communicationState is communication_states.MeasureState) {
|
switch (communicationState) {
|
||||||
|
case communication_states.MeasureState():
|
||||||
if (_canTakePhoto) {
|
if (_canTakePhoto) {
|
||||||
_takePhoto().then((ev100Raw) {
|
_takePhoto().then((ev100Raw) {
|
||||||
if (ev100Raw != null) {
|
if (ev100Raw != null) {
|
||||||
|
@ -77,11 +81,18 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
case communication_states.SettingsOpenedState():
|
||||||
|
_settingsOpened = true;
|
||||||
|
add(const DeinitializeEvent());
|
||||||
|
case communication_states.SettingsClosedState():
|
||||||
|
_settingsOpened = false;
|
||||||
|
add(const InitializeEvent());
|
||||||
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onRequestPermission(_, Emitter emit) async {
|
Future<void> _onRequestPermission(_, Emitter emit) async {
|
||||||
final hasPermission = await _meteringInteractor.requestPermission();
|
final hasPermission = await _meteringInteractor.requestCameraPermission();
|
||||||
if (!hasPermission) {
|
if (!hasPermission) {
|
||||||
emit(const CameraErrorState(CameraErrorType.permissionNotGranted));
|
emit(const CameraErrorState(CameraErrorType.permissionNotGranted));
|
||||||
} else {
|
} else {
|
||||||
|
@ -148,12 +159,15 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onDeinitialize(DeinitializeEvent _, Emitter emit) async {
|
Future<void> _onDeinitialize(DeinitializeEvent _, Emitter emit) async {
|
||||||
emit(const CameraLoadingState());
|
emit(const CameraInitState());
|
||||||
unawaited(_cameraController?.dispose().then((_) => _cameraController = null));
|
communicationBloc.add(communication_event.MeteringEndedEvent(_ev100));
|
||||||
|
await _cameraController?.dispose().then((_) => _cameraController = null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onZoomChanged(ZoomChangedEvent event, Emitter emit) async {
|
Future<void> _onZoomChanged(ZoomChangedEvent event, Emitter emit) async {
|
||||||
if (_cameraController != null) {
|
if (_cameraController != null &&
|
||||||
|
event.value >= _zoomRange!.start &&
|
||||||
|
event.value <= _zoomRange!.end) {
|
||||||
_cameraController!.setZoomLevel(event.value);
|
_cameraController!.setZoomLevel(event.value);
|
||||||
_currentZoom = event.value;
|
_currentZoom = event.value;
|
||||||
_emitActiveState(emit);
|
_emitActiveState(emit);
|
||||||
|
@ -191,7 +205,13 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
|
||||||
|
|
||||||
Future<double?> _takePhoto() async {
|
Future<double?> _takePhoto() async {
|
||||||
try {
|
try {
|
||||||
|
// https://github.com/flutter/flutter/issues/84957#issuecomment-1661155095
|
||||||
|
await _cameraController!.setFocusMode(FocusMode.locked);
|
||||||
|
await _cameraController!.setExposureMode(ExposureMode.locked);
|
||||||
final file = await _cameraController!.takePicture();
|
final file = await _cameraController!.takePicture();
|
||||||
|
await _cameraController!.setFocusMode(FocusMode.auto);
|
||||||
|
await _cameraController!.setExposureMode(ExposureMode.auto);
|
||||||
|
|
||||||
final Uint8List bytes = await file.readAsBytes();
|
final Uint8List bytes = await file.readAsBytes();
|
||||||
Directory(file.path).deleteSync(recursive: true);
|
Directory(file.path).deleteSync(recursive: true);
|
||||||
|
|
||||||
|
@ -215,6 +235,7 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _appLifecycleStateObserver(AppLifecycleState state) async {
|
Future<void> _appLifecycleStateObserver(AppLifecycleState state) async {
|
||||||
|
if (!_settingsOpened) {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case AppLifecycleState.resumed:
|
case AppLifecycleState.resumed:
|
||||||
add(const InitializeEvent());
|
add(const InitializeEvent());
|
||||||
|
@ -225,6 +246,7 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// This is needed only because we cannot use `with` with mixins
|
/// This is needed only because we cannot use `with` with mixins
|
||||||
class _WidgetsBindingObserver with WidgetsBindingObserver {
|
class _WidgetsBindingObserver with WidgetsBindingObserver {
|
||||||
|
|
|
@ -10,10 +10,9 @@ class CameraViewPlaceholder extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Card(
|
return Card(
|
||||||
|
color: error != null ? null : Colors.black,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(Dimens.borderRadiusM)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(Dimens.borderRadiusM)),
|
||||||
child: Center(
|
child: Center(child: error != null ? const Icon(Icons.no_photography) : null),
|
||||||
child: error != null ? const Icon(Icons.no_photography) : const CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -110,18 +110,18 @@ class _CameraViewBuilder extends StatelessWidget {
|
||||||
return AspectRatio(
|
return AspectRatio(
|
||||||
aspectRatio: PlatformConfig.cameraPreviewAspectRatio,
|
aspectRatio: PlatformConfig.cameraPreviewAspectRatio,
|
||||||
child: BlocBuilder<CameraContainerBloc, CameraContainerState>(
|
child: BlocBuilder<CameraContainerBloc, CameraContainerState>(
|
||||||
buildWhen: (previous, current) =>
|
buildWhen: (previous, current) => current is! CameraActiveState,
|
||||||
current is CameraLoadingState ||
|
builder: (context, state) => Center(
|
||||||
current is CameraInitializedState ||
|
child: AnimatedSwitcher(
|
||||||
current is CameraErrorState,
|
duration: Dimens.durationM,
|
||||||
builder: (context, state) {
|
child: switch (state) {
|
||||||
if (state is CameraInitializedState) {
|
CameraInitializedState() => CameraView(controller: state.controller),
|
||||||
return Center(child: CameraView(controller: state.controller));
|
CameraErrorState() => CameraViewPlaceholder(error: state.error),
|
||||||
} else {
|
_ => const CameraViewPlaceholder(error: null),
|
||||||
return CameraViewPlaceholder(error: state is CameraErrorState ? state.error : null);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -161,11 +161,11 @@ class _CameraControlsBuilder extends StatelessWidget {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
child = const SizedBox.shrink();
|
child = const Column(children: [Expanded(child: SizedBox.shrink())],);
|
||||||
}
|
}
|
||||||
|
|
||||||
return AnimatedSwitcher(
|
return AnimatedSwitcher(
|
||||||
duration: Dimens.durationS,
|
duration: Dimens.switchDuration,
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -25,37 +25,44 @@ class LightSensorContainerBloc
|
||||||
communicationBloc,
|
communicationBloc,
|
||||||
const LightSensorContainerState(null),
|
const LightSensorContainerState(null),
|
||||||
) {
|
) {
|
||||||
|
on<StartLuxMeteringEvent>(_onStartLuxMeteringEvent);
|
||||||
on<LuxMeteringEvent>(_onLuxMeteringEvent);
|
on<LuxMeteringEvent>(_onLuxMeteringEvent);
|
||||||
|
on<CancelLuxMeteringEvent>(_onCancelLuxMeteringEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onCommunicationState(communication_states.SourceState communicationState) {
|
void onCommunicationState(communication_states.SourceState communicationState) {
|
||||||
if (communicationState is communication_states.MeasureState) {
|
switch (communicationState) {
|
||||||
|
case communication_states.MeasureState():
|
||||||
if (_luxSubscriptions == null) {
|
if (_luxSubscriptions == null) {
|
||||||
_startMetering();
|
add(const StartLuxMeteringEvent());
|
||||||
} else {
|
} else {
|
||||||
_cancelMetering();
|
add(const CancelLuxMeteringEvent());
|
||||||
}
|
}
|
||||||
|
case communication_states.SettingsOpenedState():
|
||||||
|
add(const CancelLuxMeteringEvent());
|
||||||
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> close() async {
|
Future<void> close() async {
|
||||||
_cancelMetering();
|
communicationBloc.add(communication_event.MeteringEndedEvent(state.ev100));
|
||||||
|
_luxSubscriptions?.cancel().then((_) => _luxSubscriptions = null);
|
||||||
return super.close();
|
return super.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onStartLuxMeteringEvent(StartLuxMeteringEvent event, _) {
|
||||||
|
_luxSubscriptions = _meteringInteractor.luxStream().listen((lux) => add(LuxMeteringEvent(lux)));
|
||||||
|
}
|
||||||
|
|
||||||
void _onLuxMeteringEvent(LuxMeteringEvent event, Emitter<LightSensorContainerState> emit) {
|
void _onLuxMeteringEvent(LuxMeteringEvent event, Emitter<LightSensorContainerState> emit) {
|
||||||
final ev100 = log2(event.lux.toDouble() / 2.5) + _meteringInteractor.lightSensorEvCalibration;
|
final ev100 = log2(event.lux.toDouble() / 2.5) + _meteringInteractor.lightSensorEvCalibration;
|
||||||
emit(LightSensorContainerState(ev100));
|
emit(LightSensorContainerState(ev100));
|
||||||
communicationBloc.add(communication_event.MeteringInProgressEvent(ev100));
|
communicationBloc.add(communication_event.MeteringInProgressEvent(ev100));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _startMetering() {
|
void _onCancelLuxMeteringEvent(CancelLuxMeteringEvent event, _) {
|
||||||
_luxSubscriptions = _meteringInteractor.luxStream().listen((lux) => add(LuxMeteringEvent(lux)));
|
|
||||||
}
|
|
||||||
|
|
||||||
void _cancelMetering() {
|
|
||||||
communicationBloc.add(communication_event.MeteringEndedEvent(state.ev100));
|
communicationBloc.add(communication_event.MeteringEndedEvent(state.ev100));
|
||||||
_luxSubscriptions?.cancel().then((_) => _luxSubscriptions = null);
|
_luxSubscriptions?.cancel().then((_) => _luxSubscriptions = null);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,16 @@ abstract class LightSensorContainerEvent {
|
||||||
const LightSensorContainerEvent();
|
const LightSensorContainerEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class StartLuxMeteringEvent extends LightSensorContainerEvent {
|
||||||
|
const StartLuxMeteringEvent();
|
||||||
|
}
|
||||||
|
|
||||||
class LuxMeteringEvent extends LightSensorContainerEvent {
|
class LuxMeteringEvent extends LightSensorContainerEvent {
|
||||||
final int lux;
|
final int lux;
|
||||||
|
|
||||||
const LuxMeteringEvent(this.lux);
|
const LuxMeteringEvent(this.lux);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class CancelLuxMeteringEvent extends LightSensorContainerEvent {
|
||||||
|
const CancelLuxMeteringEvent();
|
||||||
|
}
|
||||||
|
|
|
@ -12,10 +12,11 @@ class ExposurePairsList extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (exposurePairs.isEmpty) {
|
return AnimatedSwitcher(
|
||||||
return const EmptyExposurePairsList();
|
duration: Dimens.switchDuration,
|
||||||
}
|
child: exposurePairs.isEmpty
|
||||||
return Stack(
|
? const EmptyExposurePairsList()
|
||||||
|
: Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
|
@ -76,6 +77,7 @@ class ExposurePairsList extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,12 +72,15 @@ class _ReadingValueBuilder extends StatelessWidget {
|
||||||
softWrap: false,
|
softWrap: false,
|
||||||
),
|
),
|
||||||
const SizedBox(height: Dimens.grid4),
|
const SizedBox(height: Dimens.grid4),
|
||||||
Text(
|
AnimatedSwitcher(
|
||||||
|
duration: Dimens.switchDuration,
|
||||||
|
child: Text(
|
||||||
reading.value,
|
reading.value,
|
||||||
style: textTheme.titleMedium?.copyWith(color: textColor),
|
style: textTheme.titleMedium?.copyWith(color: textColor),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
softWrap: false,
|
softWrap: false,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
|
@ -105,7 +105,7 @@ class _EquipmentProfilePicker extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AnimatedDialogPicker<EquipmentProfileData>(
|
return AnimatedDialogPicker<EquipmentProfile>(
|
||||||
icon: Icons.camera,
|
icon: Icons.camera,
|
||||||
title: S.of(context).equipmentProfile,
|
title: S.of(context).equipmentProfile,
|
||||||
selectedValue: context.listen<EquipmentProfile>(),
|
selectedValue: context.listen<EquipmentProfile>(),
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:lightmeter/data/models/volume_action.dart';
|
||||||
|
import 'package:lightmeter/data/volume_events_service.dart';
|
||||||
|
|
||||||
|
class VolumeKeysNotifier extends ChangeNotifier with RouteAware {
|
||||||
|
final VolumeEventsService volumeEventsService;
|
||||||
|
late final StreamSubscription<VolumeKey> _volumeKeysSubscription;
|
||||||
|
VolumeKey _value = VolumeKey.up;
|
||||||
|
|
||||||
|
VolumeKeysNotifier(this.volumeEventsService) {
|
||||||
|
_volumeKeysSubscription = volumeEventsService
|
||||||
|
.volumeButtonsEventStream()
|
||||||
|
.map((event) => event == 24 ? VolumeKey.up : VolumeKey.down)
|
||||||
|
.listen((event) {
|
||||||
|
value = event;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
VolumeKey get value => _value;
|
||||||
|
set value(VolumeKey newValue) {
|
||||||
|
_value = newValue;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> dispose() async {
|
||||||
|
await _volumeKeysSubscription.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,7 +6,7 @@ sealed class MeteringEvent {
|
||||||
}
|
}
|
||||||
|
|
||||||
class EquipmentProfileChangedEvent extends MeteringEvent {
|
class EquipmentProfileChangedEvent extends MeteringEvent {
|
||||||
final EquipmentProfileData equipmentProfileData;
|
final EquipmentProfile equipmentProfileData;
|
||||||
|
|
||||||
const EquipmentProfileChangedEvent(this.equipmentProfileData);
|
const EquipmentProfileChangedEvent(this.equipmentProfileData);
|
||||||
}
|
}
|
||||||
|
@ -45,3 +45,11 @@ class MeasureErrorEvent extends MeteringEvent {
|
||||||
|
|
||||||
const MeasureErrorEvent({required this.isMetering});
|
const MeasureErrorEvent({required this.isMetering});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SettingsOpenedEvent extends MeteringEvent {
|
||||||
|
const SettingsOpenedEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class SettingsClosedEvent extends MeteringEvent {
|
||||||
|
const SettingsClosedEvent();
|
||||||
|
}
|
||||||
|
|
|
@ -5,9 +5,11 @@ import 'package:lightmeter/data/haptics_service.dart';
|
||||||
import 'package:lightmeter/data/light_sensor_service.dart';
|
import 'package:lightmeter/data/light_sensor_service.dart';
|
||||||
import 'package:lightmeter/data/permissions_service.dart';
|
import 'package:lightmeter/data/permissions_service.dart';
|
||||||
import 'package:lightmeter/data/shared_prefs_service.dart';
|
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||||
|
import 'package:lightmeter/data/volume_events_service.dart';
|
||||||
import 'package:lightmeter/interactors/metering_interactor.dart';
|
import 'package:lightmeter/interactors/metering_interactor.dart';
|
||||||
import 'package:lightmeter/screens/metering/bloc_metering.dart';
|
import 'package:lightmeter/screens/metering/bloc_metering.dart';
|
||||||
import 'package:lightmeter/screens/metering/communication/bloc_communication_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/screens/metering/screen_metering.dart';
|
||||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||||
|
|
||||||
|
@ -28,19 +30,24 @@ class _MeteringFlowState extends State<MeteringFlow> {
|
||||||
context.get<HapticsService>(),
|
context.get<HapticsService>(),
|
||||||
context.get<PermissionsService>(),
|
context.get<PermissionsService>(),
|
||||||
context.get<LightSensorService>(),
|
context.get<LightSensorService>(),
|
||||||
),
|
context.get<VolumeEventsService>(),
|
||||||
|
)..initialize(),
|
||||||
|
child: InheritedWidgetBase<VolumeKeysNotifier>(
|
||||||
|
data: VolumeKeysNotifier(context.get<VolumeEventsService>()),
|
||||||
child: MultiBlocProvider(
|
child: MultiBlocProvider(
|
||||||
providers: [
|
providers: [
|
||||||
BlocProvider(create: (_) => MeteringCommunicationBloc()),
|
BlocProvider(create: (_) => MeteringCommunicationBloc()),
|
||||||
BlocProvider(
|
BlocProvider(
|
||||||
create: (context) => MeteringBloc(
|
create: (context) => MeteringBloc(
|
||||||
context.get<MeteringInteractor>(),
|
context.get<MeteringInteractor>(),
|
||||||
|
context.get<VolumeKeysNotifier>(),
|
||||||
context.read<MeteringCommunicationBloc>(),
|
context.read<MeteringCommunicationBloc>(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: const MeteringScreen(),
|
child: const MeteringScreen(),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ import 'package:lightmeter/data/models/exposure_pair.dart';
|
||||||
import 'package:lightmeter/data/models/film.dart';
|
import 'package:lightmeter/data/models/film.dart';
|
||||||
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
|
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
|
||||||
import 'package:lightmeter/environment.dart';
|
import 'package:lightmeter/environment.dart';
|
||||||
import 'package:lightmeter/providers/equipment_profile_provider.dart';
|
|
||||||
import 'package:lightmeter/providers/ev_source_type_provider.dart';
|
import 'package:lightmeter/providers/ev_source_type_provider.dart';
|
||||||
import 'package:lightmeter/screens/metering/bloc_metering.dart';
|
import 'package:lightmeter/screens/metering/bloc_metering.dart';
|
||||||
import 'package:lightmeter/screens/metering/components/bottom_controls/provider_bottom_controls.dart';
|
import 'package:lightmeter/screens/metering/components/bottom_controls/provider_bottom_controls.dart';
|
||||||
|
@ -50,7 +49,12 @@ class MeteringScreen extends StatelessWidget {
|
||||||
? EvSourceTypeProvider.of(context).toggleType
|
? EvSourceTypeProvider.of(context).toggleType
|
||||||
: null,
|
: null,
|
||||||
onMeasure: () => context.read<MeteringBloc>().add(const MeasureEvent()),
|
onMeasure: () => context.read<MeteringBloc>().add(const MeasureEvent()),
|
||||||
onSettings: () => Navigator.pushNamed(context, 'settings'),
|
onSettings: () {
|
||||||
|
context.read<MeteringBloc>().add(const SettingsOpenedEvent());
|
||||||
|
Navigator.pushNamed(context, 'settings').then((value) {
|
||||||
|
context.read<MeteringBloc>().add(const SettingsClosedEvent());
|
||||||
|
});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:lightmeter/generated/l10n.dart';
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
|
import 'package:lightmeter/res/dimens.dart';
|
||||||
|
|
||||||
import 'package:lightmeter/screens/settings/components/general/components/caffeine/bloc_list_tile_caffeine.dart';
|
import 'package:lightmeter/screens/settings/components/general/components/caffeine/bloc_list_tile_caffeine.dart';
|
||||||
|
|
||||||
|
@ -15,6 +16,7 @@ class CaffeineListTile extends StatelessWidget {
|
||||||
title: Text(S.of(context).keepScreenOn),
|
title: Text(S.of(context).keepScreenOn),
|
||||||
value: state,
|
value: state,
|
||||||
onChanged: context.read<CaffeineListTileBloc>().onCaffeineChanged,
|
onChanged: context.read<CaffeineListTileBloc>().onCaffeineChanged,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:lightmeter/generated/l10n.dart';
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
|
import 'package:lightmeter/res/dimens.dart';
|
||||||
|
|
||||||
import 'package:lightmeter/screens/settings/components/general/components/haptics/bloc_list_tile_haptics.dart';
|
import 'package:lightmeter/screens/settings/components/general/components/haptics/bloc_list_tile_haptics.dart';
|
||||||
|
|
||||||
|
@ -15,6 +16,7 @@ class HapticsListTile extends StatelessWidget {
|
||||||
title: Text(S.of(context).haptics),
|
title: Text(S.of(context).haptics),
|
||||||
value: state,
|
value: state,
|
||||||
onChanged: context.read<HapticsListTileBloc>().onHapticsChanged,
|
onChanged: context.read<HapticsListTileBloc>().onHapticsChanged,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:lightmeter/data/models/volume_action.dart';
|
||||||
|
import 'package:lightmeter/interactors/settings_interactor.dart';
|
||||||
|
|
||||||
|
class VolumeActionsListTileBloc extends Cubit<bool> {
|
||||||
|
final SettingsInteractor _settingsInteractor;
|
||||||
|
|
||||||
|
VolumeActionsListTileBloc(
|
||||||
|
this._settingsInteractor,
|
||||||
|
) : super(_settingsInteractor.volumeAction == VolumeAction.shutter);
|
||||||
|
|
||||||
|
void onVolumeActionChanged(bool value) {
|
||||||
|
_settingsInteractor.setVolumeAction(value ? VolumeAction.shutter : VolumeAction.none);
|
||||||
|
|
||||||
|
// while in settings we allow system to handle volume
|
||||||
|
// so that volume keys action works only when necessary - on the metering screen
|
||||||
|
_settingsInteractor.disableVolumeHandling();
|
||||||
|
emit(value);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
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';
|
||||||
|
|
||||||
|
class VolumeActionsListTileProvider extends StatelessWidget {
|
||||||
|
const VolumeActionsListTileProvider({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider(
|
||||||
|
create: (context) => VolumeActionsListTileBloc(context.get<SettingsInteractor>()),
|
||||||
|
child: const VolumeActionsListTile(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
|
import 'package:lightmeter/res/dimens.dart';
|
||||||
|
import 'package:lightmeter/screens/settings/components/general/components/volume_actions/bloc_list_tile_volume_actions.dart';
|
||||||
|
|
||||||
|
class VolumeActionsListTile extends StatelessWidget {
|
||||||
|
const VolumeActionsListTile({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<VolumeActionsListTileBloc, bool>(
|
||||||
|
builder: (context, state) => SwitchListTile(
|
||||||
|
secondary: const Icon(Icons.volume_up),
|
||||||
|
title: Text(S.of(context).volumeKeysAction),
|
||||||
|
value: state,
|
||||||
|
onChanged: context.read<VolumeActionsListTileBloc>().onVolumeActionChanged,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,11 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:lightmeter/generated/l10n.dart';
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
import 'package:lightmeter/screens/settings/components/general/components/caffeine/provider_list_tile_caffeine.dart';
|
import 'package:lightmeter/screens/settings/components/general/components/caffeine/provider_list_tile_caffeine.dart';
|
||||||
import 'package:lightmeter/screens/settings/components/general/components/haptics/provider_list_tile_haptics.dart';
|
import 'package:lightmeter/screens/settings/components/general/components/haptics/provider_list_tile_haptics.dart';
|
||||||
import 'package:lightmeter/screens/settings/components/general/components/language/widget_list_tile_language.dart';
|
import 'package:lightmeter/screens/settings/components/general/components/language/widget_list_tile_language.dart';
|
||||||
|
import 'package:lightmeter/screens/settings/components/general/components/volume_actions/provider_list_tile_volume_actions.dart';
|
||||||
import 'package:lightmeter/screens/settings/components/shared/settings_section/widget_settings_section.dart';
|
import 'package:lightmeter/screens/settings/components/shared/settings_section/widget_settings_section.dart';
|
||||||
|
|
||||||
class GeneralSettingsSection extends StatelessWidget {
|
class GeneralSettingsSection extends StatelessWidget {
|
||||||
|
@ -12,10 +15,11 @@ class GeneralSettingsSection extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SettingsSection(
|
return SettingsSection(
|
||||||
title: S.of(context).general,
|
title: S.of(context).general,
|
||||||
children: const [
|
children: [
|
||||||
CaffeineListTileProvider(),
|
const CaffeineListTileProvider(),
|
||||||
HapticsListTileProvider(),
|
const HapticsListTileProvider(),
|
||||||
LanguageListTile(),
|
if (Platform.isAndroid) const VolumeActionsListTileProvider(),
|
||||||
|
const LanguageListTile(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,8 @@ import 'package:lightmeter/screens/settings/components/metering/components/equip
|
||||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||||
|
|
||||||
class EquipmentProfileContainer extends StatefulWidget {
|
class EquipmentProfileContainer extends StatefulWidget {
|
||||||
final EquipmentProfileData data;
|
final EquipmentProfile data;
|
||||||
final ValueChanged<EquipmentProfileData> onUpdate;
|
final ValueChanged<EquipmentProfile> onUpdate;
|
||||||
final VoidCallback onDelete;
|
final VoidCallback onDelete;
|
||||||
final VoidCallback onExpand;
|
final VoidCallback onExpand;
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ class EquipmentProfileContainer extends StatefulWidget {
|
||||||
|
|
||||||
class EquipmentProfileContainerState extends State<EquipmentProfileContainer>
|
class EquipmentProfileContainerState extends State<EquipmentProfileContainer>
|
||||||
with TickerProviderStateMixin {
|
with TickerProviderStateMixin {
|
||||||
late EquipmentProfileData _equipmentData = EquipmentProfileData(
|
late EquipmentProfile _equipmentData = EquipmentProfile(
|
||||||
id: widget.data.id,
|
id: widget.data.id,
|
||||||
name: widget.data.name,
|
name: widget.data.name,
|
||||||
apertureValues: widget.data.apertureValues,
|
apertureValues: widget.data.apertureValues,
|
||||||
|
@ -45,7 +45,7 @@ class EquipmentProfileContainerState extends State<EquipmentProfileContainer>
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(EquipmentProfileContainer oldWidget) {
|
void didUpdateWidget(EquipmentProfileContainer oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
_equipmentData = EquipmentProfileData(
|
_equipmentData = EquipmentProfile(
|
||||||
id: widget.data.id,
|
id: widget.data.id,
|
||||||
name: widget.data.name,
|
name: widget.data.name,
|
||||||
apertureValues: widget.data.apertureValues,
|
apertureValues: widget.data.apertureValues,
|
||||||
|
@ -195,7 +195,7 @@ class _AnimatedArrowButton extends AnimatedWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AnimatedEquipmentListTiles extends AnimatedWidget {
|
class _AnimatedEquipmentListTiles extends AnimatedWidget {
|
||||||
final EquipmentProfileData equipmentData;
|
final EquipmentProfile equipmentData;
|
||||||
final ValueChanged<List<ApertureValue>> onApertureValuesSelected;
|
final ValueChanged<List<ApertureValue>> onApertureValuesSelected;
|
||||||
final ValueChanged<List<IsoValue>> onIsoValuesSelecred;
|
final ValueChanged<List<IsoValue>> onIsoValuesSelecred;
|
||||||
final ValueChanged<List<NdValue>> onNdValuesSelected;
|
final ValueChanged<List<NdValue>> onNdValuesSelected;
|
||||||
|
|
|
@ -84,7 +84,7 @@ class _EquipmentProfilesScreenState extends State<EquipmentProfilesScreen> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateProfileAt(EquipmentProfileData data, int index) {
|
void _updateProfileAt(EquipmentProfile data, int index) {
|
||||||
EquipmentProfileProvider.of(context).updateProdile(data);
|
EquipmentProfileProvider.of(context).updateProdile(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ class EquipmentProfilesListTile extends StatelessWidget {
|
||||||
leading: const Icon(Icons.camera),
|
leading: const Icon(Icons.camera),
|
||||||
title: Text(S.of(context).equipmentProfiles),
|
title: Text(S.of(context).equipmentProfiles),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).push<EquipmentProfileData>(
|
Navigator.of(context).push<EquipmentProfile>(
|
||||||
MaterialPageRoute(builder: (_) => const EquipmentProfilesScreen()),
|
MaterialPageRoute(builder: (_) => const EquipmentProfilesScreen()),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:lightmeter/data/models/dynamic_colors_state.dart';
|
import 'package:lightmeter/data/models/dynamic_colors_state.dart';
|
||||||
import 'package:lightmeter/generated/l10n.dart';
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
import 'package:lightmeter/providers/theme_provider.dart';
|
import 'package:lightmeter/providers/theme_provider.dart';
|
||||||
|
import 'package:lightmeter/res/dimens.dart';
|
||||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||||
|
|
||||||
class DynamicColorListTile extends StatelessWidget {
|
class DynamicColorListTile extends StatelessWidget {
|
||||||
|
@ -14,6 +15,7 @@ class DynamicColorListTile extends StatelessWidget {
|
||||||
title: Text(S.of(context).dynamicColor),
|
title: Text(S.of(context).dynamicColor),
|
||||||
value: context.listen<DynamicColorState>() == DynamicColorState.enabled,
|
value: context.listen<DynamicColorState>() == DynamicColorState.enabled,
|
||||||
onChanged: ThemeProvider.of(context).enableDynamicColor,
|
onChanged: ThemeProvider.of(context).enableDynamicColor,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:lightmeter/data/caffeine_service.dart';
|
import 'package:lightmeter/data/caffeine_service.dart';
|
||||||
import 'package:lightmeter/data/haptics_service.dart';
|
import 'package:lightmeter/data/haptics_service.dart';
|
||||||
import 'package:lightmeter/data/shared_prefs_service.dart';
|
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||||
|
import 'package:lightmeter/data/volume_events_service.dart';
|
||||||
import 'package:lightmeter/interactors/settings_interactor.dart';
|
import 'package:lightmeter/interactors/settings_interactor.dart';
|
||||||
import 'package:lightmeter/screens/settings/screen_settings.dart';
|
import 'package:lightmeter/screens/settings/screen_settings.dart';
|
||||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||||
|
@ -16,6 +17,7 @@ class SettingsFlow extends StatelessWidget {
|
||||||
context.get<UserPreferencesService>(),
|
context.get<UserPreferencesService>(),
|
||||||
context.get<CaffeineService>(),
|
context.get<CaffeineService>(),
|
||||||
context.get<HapticsService>(),
|
context.get<HapticsService>(),
|
||||||
|
context.get<VolumeEventsService>(),
|
||||||
),
|
),
|
||||||
child: const SettingsScreen(),
|
child: const SettingsScreen(),
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,14 +1,33 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:lightmeter/generated/l10n.dart';
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
|
import 'package:lightmeter/interactors/settings_interactor.dart';
|
||||||
import 'package:lightmeter/screens/settings/components/about/widget_settings_section_about.dart';
|
import 'package:lightmeter/screens/settings/components/about/widget_settings_section_about.dart';
|
||||||
import 'package:lightmeter/screens/settings/components/general/widget_settings_section_general.dart';
|
import 'package:lightmeter/screens/settings/components/general/widget_settings_section_general.dart';
|
||||||
import 'package:lightmeter/screens/settings/components/metering/widget_settings_section_metering.dart';
|
import 'package:lightmeter/screens/settings/components/metering/widget_settings_section_metering.dart';
|
||||||
import 'package:lightmeter/screens/settings/components/theme/widget_settings_section_theme.dart';
|
import 'package:lightmeter/screens/settings/components/theme/widget_settings_section_theme.dart';
|
||||||
import 'package:lightmeter/screens/shared/sliver_screen/screen_sliver.dart';
|
import 'package:lightmeter/screens/shared/sliver_screen/screen_sliver.dart';
|
||||||
|
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||||
|
|
||||||
class SettingsScreen extends StatelessWidget {
|
class SettingsScreen extends StatefulWidget {
|
||||||
const SettingsScreen({super.key});
|
const SettingsScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SettingsScreen> createState() => _SettingsScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SettingsScreenState extends State<SettingsScreen> {
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
context.get<SettingsInteractor>().disableVolumeHandling();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void deactivate() {
|
||||||
|
context.get<SettingsInteractor>().restoreVolumeHandling();
|
||||||
|
super.deactivate();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ScaffoldMessenger(
|
return ScaffoldMessenger(
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
name: lightmeter
|
name: lightmeter
|
||||||
description: A new Flutter project.
|
description: A new Flutter project.
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
version: 0.11.8+30
|
version: 0.12.4+35
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.0.0 <4.0.0"
|
sdk: ">=3.0.0 <4.0.0"
|
||||||
|
@ -9,7 +9,7 @@ environment:
|
||||||
dependencies:
|
dependencies:
|
||||||
app_settings: 4.2.0
|
app_settings: 4.2.0
|
||||||
bloc_concurrency: 0.2.2
|
bloc_concurrency: 0.2.2
|
||||||
camera: 0.10.5
|
camera: 0.10.5+2
|
||||||
clipboard: 0.1.3
|
clipboard: 0.1.3
|
||||||
dynamic_color: 1.6.5
|
dynamic_color: 1.6.5
|
||||||
exif: 3.1.4
|
exif: 3.1.4
|
||||||
|
@ -30,6 +30,7 @@ dependencies:
|
||||||
material_color_utilities: 0.2.0
|
material_color_utilities: 0.2.0
|
||||||
package_info_plus: 4.0.1
|
package_info_plus: 4.0.1
|
||||||
permission_handler: 10.2.0
|
permission_handler: 10.2.0
|
||||||
|
platform: 3.1.0
|
||||||
shared_preferences: 2.1.1
|
shared_preferences: 2.1.1
|
||||||
url_launcher: 6.1.11
|
url_launcher: 6.1.11
|
||||||
uuid: 3.0.7
|
uuid: 3.0.7
|
||||||
|
|
BIN
resources/social_preview.png
Normal file
BIN
resources/social_preview.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 47 KiB |
|
@ -1,10 +1,15 @@
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:lightmeter/data/light_sensor_service.dart';
|
import 'package:lightmeter/data/light_sensor_service.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
import 'package:platform/platform.dart';
|
||||||
|
|
||||||
|
class _MockLocalPlatform extends Mock implements LocalPlatform {}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
TestWidgetsFlutterBinding.ensureInitialized();
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
late _MockLocalPlatform localPlatform;
|
||||||
late LightSensorService service;
|
late LightSensorService service;
|
||||||
|
|
||||||
const methodChannel = MethodChannel('system_feature');
|
const methodChannel = MethodChannel('system_feature');
|
||||||
|
@ -12,7 +17,8 @@ void main() {
|
||||||
//const eventChannel = EventChannel('light.eventChannel');
|
//const eventChannel = EventChannel('light.eventChannel');
|
||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
service = const LightSensorService();
|
localPlatform = _MockLocalPlatform();
|
||||||
|
service = LightSensorService(localPlatform);
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDown(() {
|
tearDown(() {
|
||||||
|
@ -23,7 +29,8 @@ void main() {
|
||||||
group(
|
group(
|
||||||
'hasSensor()',
|
'hasSensor()',
|
||||||
() {
|
() {
|
||||||
test('true', () async {
|
test('true - Android', () async {
|
||||||
|
when(() => localPlatform.isAndroid).thenReturn(true);
|
||||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||||
.setMockMethodCallHandler(methodChannel, null);
|
.setMockMethodCallHandler(methodChannel, null);
|
||||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||||
|
@ -38,7 +45,8 @@ void main() {
|
||||||
expectLater(service.hasSensor(), completion(true));
|
expectLater(service.hasSensor(), completion(true));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('false', () async {
|
test('false - Android', () async {
|
||||||
|
when(() => localPlatform.isAndroid).thenReturn(true);
|
||||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||||
.setMockMethodCallHandler(methodChannel, null);
|
.setMockMethodCallHandler(methodChannel, null);
|
||||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||||
|
@ -52,7 +60,9 @@ void main() {
|
||||||
});
|
});
|
||||||
expectLater(service.hasSensor(), completion(false));
|
expectLater(service.hasSensor(), completion(false));
|
||||||
});
|
});
|
||||||
test('null', () async {
|
|
||||||
|
test('null - Android', () async {
|
||||||
|
when(() => localPlatform.isAndroid).thenReturn(true);
|
||||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||||
.setMockMethodCallHandler(methodChannel, null);
|
.setMockMethodCallHandler(methodChannel, null);
|
||||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||||
|
@ -66,6 +76,23 @@ void main() {
|
||||||
});
|
});
|
||||||
expectLater(service.hasSensor(), completion(false));
|
expectLater(service.hasSensor(), completion(false));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('false - iOS', () async {
|
||||||
|
when(() => localPlatform.isAndroid).thenReturn(false);
|
||||||
|
expectLater(service.hasSensor(), completion(false));
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
group('luxStream', () {
|
||||||
|
// test('Android', () async {
|
||||||
|
// when(() => localPlatform.isAndroid).thenReturn(true);
|
||||||
|
// expect(service.luxStream(), const Stream.empty());
|
||||||
|
// });
|
||||||
|
|
||||||
|
test('iOS', () async {
|
||||||
|
when(() => localPlatform.isAndroid).thenReturn(false);
|
||||||
|
expect(service.luxStream(), const Stream<int>.empty());
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
73
test/data/volume_events_service_test.dart
Normal file
73
test/data/volume_events_service_test.dart
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:lightmeter/data/volume_events_service.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
import 'package:platform/platform.dart';
|
||||||
|
|
||||||
|
class _MockLocalPlatform extends Mock implements LocalPlatform {}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
late _MockLocalPlatform localPlatform;
|
||||||
|
late VolumeEventsService service;
|
||||||
|
|
||||||
|
Future<Object?>? methodCallSuccessHandler(MethodCall methodCall) async {
|
||||||
|
switch (methodCall.method) {
|
||||||
|
case "setVolumeHandling":
|
||||||
|
return methodCall.arguments as bool;
|
||||||
|
default:
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
localPlatform = _MockLocalPlatform();
|
||||||
|
service = VolumeEventsService(localPlatform);
|
||||||
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||||
|
VolumeEventsService.volumeHandlingChannel,
|
||||||
|
methodCallSuccessHandler,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() {
|
||||||
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||||
|
VolumeEventsService.volumeHandlingChannel,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('setVolumeHandling', () {
|
||||||
|
test('true - Android', () async {
|
||||||
|
when(() => localPlatform.isAndroid).thenReturn(true);
|
||||||
|
expectLater(service.setVolumeHandling(true), completion(true));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('true - iOS', () async {
|
||||||
|
when(() => localPlatform.isAndroid).thenReturn(false);
|
||||||
|
expectLater(service.setVolumeHandling(true), completion(false));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('false - Android', () async {
|
||||||
|
when(() => localPlatform.isAndroid).thenReturn(true);
|
||||||
|
expectLater(service.setVolumeHandling(false), completion(false));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('false - iOS', () async {
|
||||||
|
when(() => localPlatform.isAndroid).thenReturn(false);
|
||||||
|
expectLater(service.setVolumeHandling(false), completion(false));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('volumeButtonsEventStream', () {
|
||||||
|
// test('Android', () async {
|
||||||
|
// when(() => localPlatform.isAndroid).thenReturn(true);
|
||||||
|
// expect(service.volumeButtonsEventStream(), const Stream.empty());
|
||||||
|
// });
|
||||||
|
|
||||||
|
test('iOS', () async {
|
||||||
|
when(() => localPlatform.isAndroid).thenReturn(false);
|
||||||
|
expect(service.volumeButtonsEventStream(), const Stream<int>.empty());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
274
test/interactors/metering_interactor_test.dart
Normal file
274
test/interactors/metering_interactor_test.dart
Normal file
|
@ -0,0 +1,274 @@
|
||||||
|
import 'package:flutter_test/flutter_test.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';
|
||||||
|
import 'package:lightmeter/data/volume_events_service.dart';
|
||||||
|
import 'package:lightmeter/interactors/metering_interactor.dart';
|
||||||
|
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|
||||||
|
class _MockUserPreferencesService extends Mock implements UserPreferencesService {}
|
||||||
|
|
||||||
|
class _MockCaffeineService extends Mock implements CaffeineService {}
|
||||||
|
|
||||||
|
class _MockHapticsService extends Mock implements HapticsService {}
|
||||||
|
|
||||||
|
class _MockPermissionsService extends Mock implements PermissionsService {}
|
||||||
|
|
||||||
|
class _MockLightSensorService extends Mock implements LightSensorService {}
|
||||||
|
|
||||||
|
class _MockVolumeEventsService extends Mock implements VolumeEventsService {}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late _MockUserPreferencesService mockUserPreferencesService;
|
||||||
|
late _MockCaffeineService mockCaffeineService;
|
||||||
|
late _MockHapticsService mockHapticsService;
|
||||||
|
late _MockPermissionsService mockPermissionsService;
|
||||||
|
late _MockLightSensorService mockLightSensorService;
|
||||||
|
late _MockVolumeEventsService mockVolumeEventsService;
|
||||||
|
|
||||||
|
late MeteringInteractor interactor;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
mockUserPreferencesService = _MockUserPreferencesService();
|
||||||
|
mockCaffeineService = _MockCaffeineService();
|
||||||
|
mockHapticsService = _MockHapticsService();
|
||||||
|
mockPermissionsService = _MockPermissionsService();
|
||||||
|
mockLightSensorService = _MockLightSensorService();
|
||||||
|
mockVolumeEventsService = _MockVolumeEventsService();
|
||||||
|
|
||||||
|
interactor = MeteringInteractor(
|
||||||
|
mockUserPreferencesService,
|
||||||
|
mockCaffeineService,
|
||||||
|
mockHapticsService,
|
||||||
|
mockPermissionsService,
|
||||||
|
mockLightSensorService,
|
||||||
|
mockVolumeEventsService,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
group(
|
||||||
|
'Initalization',
|
||||||
|
() {
|
||||||
|
test('caffeine - true', () async {
|
||||||
|
when(() => mockUserPreferencesService.caffeine).thenReturn(true);
|
||||||
|
when(() => mockCaffeineService.keepScreenOn(true)).thenAnswer((_) async => true);
|
||||||
|
when(() => mockUserPreferencesService.volumeAction).thenReturn(VolumeAction.shutter);
|
||||||
|
when(() => mockVolumeEventsService.setVolumeHandling(true)).thenAnswer((_) async => true);
|
||||||
|
interactor.initialize();
|
||||||
|
verify(() => mockUserPreferencesService.caffeine).called(1);
|
||||||
|
verify(() => mockCaffeineService.keepScreenOn(true)).called(1);
|
||||||
|
verify(() => mockVolumeEventsService.setVolumeHandling(true)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('caffeine - false', () async {
|
||||||
|
when(() => mockUserPreferencesService.caffeine).thenReturn(false);
|
||||||
|
when(() => mockCaffeineService.keepScreenOn(false)).thenAnswer((_) async => false);
|
||||||
|
when(() => mockUserPreferencesService.volumeAction).thenReturn(VolumeAction.shutter);
|
||||||
|
when(() => mockVolumeEventsService.setVolumeHandling(true)).thenAnswer((_) async => true);
|
||||||
|
interactor.initialize();
|
||||||
|
verify(() => mockUserPreferencesService.caffeine).called(1);
|
||||||
|
verifyNever(() => mockCaffeineService.keepScreenOn(false));
|
||||||
|
verify(() => mockVolumeEventsService.setVolumeHandling(true)).called(1);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
group(
|
||||||
|
'Calibration',
|
||||||
|
() {
|
||||||
|
test('cameraEvCalibration', () async {
|
||||||
|
when(() => mockUserPreferencesService.cameraEvCalibration).thenReturn(0.0);
|
||||||
|
expect(interactor.cameraEvCalibration, 0.0);
|
||||||
|
verify(() => mockUserPreferencesService.cameraEvCalibration).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('lightSensorEvCalibration', () async {
|
||||||
|
when(() => mockUserPreferencesService.lightSensorEvCalibration).thenReturn(0.0);
|
||||||
|
expect(interactor.lightSensorEvCalibration, 0.0);
|
||||||
|
verify(() => mockUserPreferencesService.lightSensorEvCalibration).called(1);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
group(
|
||||||
|
'Equipment',
|
||||||
|
() {
|
||||||
|
test('iso - get', () async {
|
||||||
|
when(() => mockUserPreferencesService.iso).thenReturn(IsoValue.values.first);
|
||||||
|
expect(interactor.iso, IsoValue.values.first);
|
||||||
|
verify(() => mockUserPreferencesService.iso).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('iso - set', () async {
|
||||||
|
when(() => mockUserPreferencesService.iso = IsoValue.values.first)
|
||||||
|
.thenReturn(IsoValue.values.first);
|
||||||
|
interactor.iso = IsoValue.values.first;
|
||||||
|
verify(() => mockUserPreferencesService.iso = IsoValue.values.first).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ndFilter - get', () async {
|
||||||
|
when(() => mockUserPreferencesService.ndFilter).thenReturn(NdValue.values.first);
|
||||||
|
expect(interactor.ndFilter, NdValue.values.first);
|
||||||
|
verify(() => mockUserPreferencesService.ndFilter).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ndFilter - set', () async {
|
||||||
|
when(() => mockUserPreferencesService.ndFilter = NdValue.values.first)
|
||||||
|
.thenReturn(NdValue.values.first);
|
||||||
|
interactor.ndFilter = NdValue.values.first;
|
||||||
|
verify(() => mockUserPreferencesService.ndFilter = NdValue.values.first).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('film - get', () async {
|
||||||
|
when(() => mockUserPreferencesService.film).thenReturn(Film.values.first);
|
||||||
|
expect(interactor.film, Film.values.first);
|
||||||
|
verify(() => mockUserPreferencesService.film).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('film - set', () async {
|
||||||
|
when(() => mockUserPreferencesService.film = Film.values.first)
|
||||||
|
.thenReturn(Film.values.first);
|
||||||
|
interactor.film = Film.values.first;
|
||||||
|
verify(() => mockUserPreferencesService.film = Film.values.first).called(1);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
group(
|
||||||
|
'Volume action',
|
||||||
|
() {
|
||||||
|
test('volumeAction - VolumeAction.shutter', () async {
|
||||||
|
when(() => mockUserPreferencesService.volumeAction).thenReturn(VolumeAction.shutter);
|
||||||
|
expect(interactor.volumeAction, VolumeAction.shutter);
|
||||||
|
verify(() => mockUserPreferencesService.volumeAction).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('volumeAction - VolumeAction.none', () async {
|
||||||
|
when(() => mockUserPreferencesService.volumeAction).thenReturn(VolumeAction.none);
|
||||||
|
expect(interactor.volumeAction, VolumeAction.none);
|
||||||
|
verify(() => mockUserPreferencesService.volumeAction).called(1);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
group(
|
||||||
|
'Haptics',
|
||||||
|
() {
|
||||||
|
test('isHapticsEnabled', () async {
|
||||||
|
when(() => mockUserPreferencesService.haptics).thenReturn(true);
|
||||||
|
expect(interactor.isHapticsEnabled, true);
|
||||||
|
verify(() => mockUserPreferencesService.haptics).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('quickVibration() - true', () async {
|
||||||
|
when(() => mockUserPreferencesService.haptics).thenReturn(true);
|
||||||
|
when(() => mockHapticsService.quickVibration()).thenAnswer((_) async {});
|
||||||
|
interactor.quickVibration();
|
||||||
|
verify(() => mockUserPreferencesService.haptics).called(1);
|
||||||
|
verify(() => mockHapticsService.quickVibration()).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('quickVibration() - false', () async {
|
||||||
|
when(() => mockUserPreferencesService.haptics).thenReturn(false);
|
||||||
|
when(() => mockHapticsService.quickVibration()).thenAnswer((_) async {});
|
||||||
|
interactor.quickVibration();
|
||||||
|
verify(() => mockUserPreferencesService.haptics).called(1);
|
||||||
|
verifyNever(() => mockHapticsService.quickVibration());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('responseVibration() - true', () async {
|
||||||
|
when(() => mockUserPreferencesService.haptics).thenReturn(true);
|
||||||
|
when(() => mockHapticsService.responseVibration()).thenAnswer((_) async {});
|
||||||
|
interactor.responseVibration();
|
||||||
|
verify(() => mockUserPreferencesService.haptics).called(1);
|
||||||
|
verify(() => mockHapticsService.responseVibration()).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('responseVibration() - false', () async {
|
||||||
|
when(() => mockUserPreferencesService.haptics).thenReturn(false);
|
||||||
|
when(() => mockHapticsService.responseVibration()).thenAnswer((_) async {});
|
||||||
|
interactor.responseVibration();
|
||||||
|
verify(() => mockUserPreferencesService.haptics).called(1);
|
||||||
|
verifyNever(() => mockHapticsService.responseVibration());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('errorVibration() - true', () async {
|
||||||
|
when(() => mockUserPreferencesService.haptics).thenReturn(true);
|
||||||
|
when(() => mockHapticsService.errorVibration()).thenAnswer((_) async {});
|
||||||
|
interactor.errorVibration();
|
||||||
|
verify(() => mockUserPreferencesService.haptics).called(1);
|
||||||
|
verify(() => mockHapticsService.errorVibration()).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('errorVibration() - false', () async {
|
||||||
|
when(() => mockUserPreferencesService.haptics).thenReturn(false);
|
||||||
|
when(() => mockHapticsService.errorVibration()).thenAnswer((_) async {});
|
||||||
|
interactor.errorVibration();
|
||||||
|
verify(() => mockUserPreferencesService.haptics).called(1);
|
||||||
|
verifyNever(() => mockHapticsService.errorVibration());
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
group(
|
||||||
|
'Permissions',
|
||||||
|
() {
|
||||||
|
test('checkCameraPermission() - granted', () async {
|
||||||
|
when(() => mockPermissionsService.checkCameraPermission())
|
||||||
|
.thenAnswer((_) async => PermissionStatus.granted);
|
||||||
|
expectLater(interactor.checkCameraPermission(), completion(true));
|
||||||
|
verify(() => mockPermissionsService.checkCameraPermission()).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('checkCameraPermission() - denied', () async {
|
||||||
|
when(() => mockPermissionsService.checkCameraPermission())
|
||||||
|
.thenAnswer((_) async => PermissionStatus.denied);
|
||||||
|
expectLater(interactor.checkCameraPermission(), completion(false));
|
||||||
|
verify(() => mockPermissionsService.checkCameraPermission()).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('requestCameraPermission() - granted', () async {
|
||||||
|
when(() => mockPermissionsService.requestCameraPermission())
|
||||||
|
.thenAnswer((_) async => PermissionStatus.granted);
|
||||||
|
expectLater(interactor.requestCameraPermission(), completion(true));
|
||||||
|
verify(() => mockPermissionsService.requestCameraPermission()).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('requestCameraPermission() - denied', () async {
|
||||||
|
when(() => mockPermissionsService.requestCameraPermission())
|
||||||
|
.thenAnswer((_) async => PermissionStatus.denied);
|
||||||
|
expectLater(interactor.requestCameraPermission(), completion(false));
|
||||||
|
verify(() => mockPermissionsService.requestCameraPermission()).called(1);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
group(
|
||||||
|
'Haptics',
|
||||||
|
() {
|
||||||
|
test('hasAmbientLightSensor() - true', () async {
|
||||||
|
when(() => mockLightSensorService.hasSensor()).thenAnswer((_) async => true);
|
||||||
|
expectLater(interactor.hasAmbientLightSensor(), completion(true));
|
||||||
|
verify(() => mockLightSensorService.hasSensor()).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('hasAmbientLightSensor() - false', () async {
|
||||||
|
when(() => mockLightSensorService.hasSensor()).thenAnswer((_) async => false);
|
||||||
|
expectLater(interactor.hasAmbientLightSensor(), completion(false));
|
||||||
|
verify(() => mockLightSensorService.hasSensor()).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('luxStream()', () async {
|
||||||
|
when(() => mockLightSensorService.luxStream()).thenAnswer((_) => const Stream<int>.empty());
|
||||||
|
expect(interactor.luxStream(), const Stream<int>.empty());
|
||||||
|
verify(() => mockLightSensorService.luxStream()).called(1);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
205
test/interactors/settings_interactor_test.dart
Normal file
205
test/interactors/settings_interactor_test.dart
Normal file
|
@ -0,0 +1,205 @@
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:lightmeter/data/caffeine_service.dart';
|
||||||
|
import 'package:lightmeter/data/haptics_service.dart';
|
||||||
|
import 'package:lightmeter/data/models/volume_action.dart';
|
||||||
|
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||||
|
import 'package:lightmeter/data/volume_events_service.dart';
|
||||||
|
import 'package:lightmeter/interactors/settings_interactor.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
|
class _MockUserPreferencesService extends Mock implements UserPreferencesService {}
|
||||||
|
|
||||||
|
class _MockCaffeineService extends Mock implements CaffeineService {}
|
||||||
|
|
||||||
|
class _MockHapticsService extends Mock implements HapticsService {}
|
||||||
|
|
||||||
|
class _MockVolumeEventsService extends Mock implements VolumeEventsService {}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late _MockUserPreferencesService mockUserPreferencesService;
|
||||||
|
late _MockCaffeineService mockCaffeineService;
|
||||||
|
late _MockHapticsService mockHapticsService;
|
||||||
|
late _MockVolumeEventsService mockVolumeEventsService;
|
||||||
|
|
||||||
|
late SettingsInteractor interactor;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
mockUserPreferencesService = _MockUserPreferencesService();
|
||||||
|
mockCaffeineService = _MockCaffeineService();
|
||||||
|
mockHapticsService = _MockHapticsService();
|
||||||
|
mockVolumeEventsService = _MockVolumeEventsService();
|
||||||
|
|
||||||
|
interactor = SettingsInteractor(
|
||||||
|
mockUserPreferencesService,
|
||||||
|
mockCaffeineService,
|
||||||
|
mockHapticsService,
|
||||||
|
mockVolumeEventsService,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
group(
|
||||||
|
'Calibration',
|
||||||
|
() {
|
||||||
|
test('cameraEvCalibration - get', () async {
|
||||||
|
when(() => mockUserPreferencesService.cameraEvCalibration).thenReturn(0.0);
|
||||||
|
expect(interactor.cameraEvCalibration, 0.0);
|
||||||
|
verify(() => mockUserPreferencesService.cameraEvCalibration).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cameraEvCalibration - set', () async {
|
||||||
|
when(() => mockUserPreferencesService.cameraEvCalibration = 0.0).thenReturn(0.0);
|
||||||
|
interactor.setCameraEvCalibration(0.0);
|
||||||
|
verify(() => mockUserPreferencesService.cameraEvCalibration = 0.0).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('lightSensorEvCalibration - get', () async {
|
||||||
|
when(() => mockUserPreferencesService.lightSensorEvCalibration).thenReturn(0.0);
|
||||||
|
expect(interactor.lightSensorEvCalibration, 0.0);
|
||||||
|
verify(() => mockUserPreferencesService.lightSensorEvCalibration).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('lightSensorEvCalibration - set', () async {
|
||||||
|
when(() => mockUserPreferencesService.lightSensorEvCalibration = 0.0).thenReturn(0.0);
|
||||||
|
interactor.setLightSensorEvCalibration(0.0);
|
||||||
|
verify(() => mockUserPreferencesService.lightSensorEvCalibration = 0.0).called(1);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
group(
|
||||||
|
'Caffeine',
|
||||||
|
() {
|
||||||
|
test('isCaffeineEnabled', () async {
|
||||||
|
when(() => mockUserPreferencesService.caffeine).thenReturn(true);
|
||||||
|
expect(interactor.isCaffeineEnabled, true);
|
||||||
|
verify(() => mockUserPreferencesService.caffeine).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('enableCaffeine(true)', () async {
|
||||||
|
when(() => mockCaffeineService.keepScreenOn(true)).thenAnswer((_) async => true);
|
||||||
|
when(() => mockUserPreferencesService.caffeine = true).thenReturn(true);
|
||||||
|
await interactor.enableCaffeine(true);
|
||||||
|
verify(() => mockCaffeineService.keepScreenOn(true)).called(1);
|
||||||
|
verify(() => mockUserPreferencesService.caffeine = true).called(1);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
group(
|
||||||
|
'Volume action',
|
||||||
|
() {
|
||||||
|
test('disableVolumeHandling()', () async {
|
||||||
|
when(() => mockVolumeEventsService.setVolumeHandling(false)).thenAnswer((_) async => false);
|
||||||
|
expectLater(interactor.disableVolumeHandling(), isA<Future<void>>());
|
||||||
|
verify(() => mockVolumeEventsService.setVolumeHandling(false)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('restoreVolumeHandling() - VolumeAction.shutter', () async {
|
||||||
|
when(() => mockUserPreferencesService.volumeAction).thenReturn(VolumeAction.shutter);
|
||||||
|
when(() => mockVolumeEventsService.setVolumeHandling(true)).thenAnswer((_) async => true);
|
||||||
|
expectLater(interactor.restoreVolumeHandling(), isA<Future<void>>());
|
||||||
|
verify(() => mockUserPreferencesService.volumeAction).called(1);
|
||||||
|
verify(() => mockVolumeEventsService.setVolumeHandling(true)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('restoreVolumeHandling() - VolumeAction.none', () async {
|
||||||
|
when(() => mockUserPreferencesService.volumeAction).thenReturn(VolumeAction.none);
|
||||||
|
when(() => mockVolumeEventsService.setVolumeHandling(false)).thenAnswer((_) async => false);
|
||||||
|
expectLater(interactor.restoreVolumeHandling(), isA<Future<void>>());
|
||||||
|
verify(() => mockUserPreferencesService.volumeAction).called(1);
|
||||||
|
verify(() => mockVolumeEventsService.setVolumeHandling(false)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('volumeAction - VolumeAction.shutter', () async {
|
||||||
|
when(() => mockUserPreferencesService.volumeAction).thenReturn(VolumeAction.shutter);
|
||||||
|
expect(interactor.volumeAction, VolumeAction.shutter);
|
||||||
|
verify(() => mockUserPreferencesService.volumeAction).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('volumeAction - VolumeAction.none', () async {
|
||||||
|
when(() => mockUserPreferencesService.volumeAction).thenReturn(VolumeAction.none);
|
||||||
|
expect(interactor.volumeAction, VolumeAction.none);
|
||||||
|
verify(() => mockUserPreferencesService.volumeAction).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('setVolumeAction(VolumeAction.shutter)', () async {
|
||||||
|
when(() => mockUserPreferencesService.volumeAction = VolumeAction.shutter)
|
||||||
|
.thenReturn(VolumeAction.shutter);
|
||||||
|
when(() => mockVolumeEventsService.setVolumeHandling(true)).thenAnswer((_) async => true);
|
||||||
|
expectLater(interactor.setVolumeAction(VolumeAction.shutter), isA<Future<void>>());
|
||||||
|
verify(() => mockVolumeEventsService.setVolumeHandling(true)).called(1);
|
||||||
|
verify(() => mockUserPreferencesService.volumeAction = VolumeAction.shutter).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('setVolumeAction(VolumeAction.none)', () async {
|
||||||
|
when(() => mockUserPreferencesService.volumeAction = VolumeAction.none)
|
||||||
|
.thenReturn(VolumeAction.none);
|
||||||
|
when(() => mockVolumeEventsService.setVolumeHandling(false)).thenAnswer((_) async => false);
|
||||||
|
expectLater(interactor.setVolumeAction(VolumeAction.none), isA<Future<void>>());
|
||||||
|
verify(() => mockVolumeEventsService.setVolumeHandling(false)).called(1);
|
||||||
|
verify(() => mockUserPreferencesService.volumeAction = VolumeAction.none).called(1);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
group(
|
||||||
|
'Haptics',
|
||||||
|
() {
|
||||||
|
test('isHapticsEnabled', () async {
|
||||||
|
when(() => mockUserPreferencesService.haptics).thenReturn(true);
|
||||||
|
expect(interactor.isHapticsEnabled, true);
|
||||||
|
verify(() => mockUserPreferencesService.haptics).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('enableHaptics() - true', () async {
|
||||||
|
when(() => mockUserPreferencesService.haptics = true).thenReturn(true);
|
||||||
|
when(() => mockUserPreferencesService.haptics).thenReturn(true);
|
||||||
|
when(() => mockHapticsService.quickVibration()).thenAnswer((_) async {});
|
||||||
|
interactor.enableHaptics(true);
|
||||||
|
verify(() => mockUserPreferencesService.haptics).called(1);
|
||||||
|
verify(() => mockHapticsService.quickVibration()).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('enableHaptics() - false', () async {
|
||||||
|
when(() => mockUserPreferencesService.haptics = false).thenReturn(false);
|
||||||
|
when(() => mockUserPreferencesService.haptics).thenReturn(false);
|
||||||
|
when(() => mockHapticsService.quickVibration()).thenAnswer((_) async {});
|
||||||
|
interactor.enableHaptics(false);
|
||||||
|
verify(() => mockUserPreferencesService.haptics).called(1);
|
||||||
|
verifyNever(() => mockHapticsService.quickVibration());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('quickVibration() - true', () async {
|
||||||
|
when(() => mockUserPreferencesService.haptics).thenReturn(true);
|
||||||
|
when(() => mockHapticsService.quickVibration()).thenAnswer((_) async {});
|
||||||
|
interactor.quickVibration();
|
||||||
|
verify(() => mockUserPreferencesService.haptics).called(1);
|
||||||
|
verify(() => mockHapticsService.quickVibration()).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('quickVibration() - false', () async {
|
||||||
|
when(() => mockUserPreferencesService.haptics).thenReturn(false);
|
||||||
|
when(() => mockHapticsService.quickVibration()).thenAnswer((_) async {});
|
||||||
|
interactor.quickVibration();
|
||||||
|
verify(() => mockUserPreferencesService.haptics).called(1);
|
||||||
|
verifyNever(() => mockHapticsService.quickVibration());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('responseVibration() - true', () async {
|
||||||
|
when(() => mockUserPreferencesService.haptics).thenReturn(true);
|
||||||
|
when(() => mockHapticsService.responseVibration()).thenAnswer((_) async {});
|
||||||
|
interactor.responseVibration();
|
||||||
|
verify(() => mockUserPreferencesService.haptics).called(1);
|
||||||
|
verify(() => mockHapticsService.responseVibration()).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('responseVibration() - false', () async {
|
||||||
|
when(() => mockUserPreferencesService.haptics).thenReturn(false);
|
||||||
|
when(() => mockHapticsService.responseVibration()).thenAnswer((_) async {});
|
||||||
|
interactor.responseVibration();
|
||||||
|
verify(() => mockUserPreferencesService.haptics).called(1);
|
||||||
|
verifyNever(() => mockHapticsService.responseVibration());
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:bloc_test/bloc_test.dart';
|
import 'package:bloc_test/bloc_test.dart';
|
||||||
import 'package:lightmeter/data/models/film.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/interactors/metering_interactor.dart';
|
||||||
import 'package:lightmeter/screens/metering/bloc_metering.dart';
|
import 'package:lightmeter/screens/metering/bloc_metering.dart';
|
||||||
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
|
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
|
||||||
|
@ -7,20 +8,24 @@ import 'package:lightmeter/screens/metering/communication/event_communication_me
|
||||||
as communication_events;
|
as communication_events;
|
||||||
import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart'
|
import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart'
|
||||||
as communication_states;
|
as communication_states;
|
||||||
|
import 'package:lightmeter/screens/metering/components/shared/volume_keys_notifier/notifier_volume_keys.dart';
|
||||||
import 'package:lightmeter/screens/metering/event_metering.dart';
|
import 'package:lightmeter/screens/metering/event_metering.dart';
|
||||||
import 'package:lightmeter/screens/metering/state_metering.dart';
|
import 'package:lightmeter/screens/metering/state_metering.dart';
|
||||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
class _MockMeteringInteractor extends Mock implements MeteringInteractor {}
|
||||||
|
|
||||||
|
class _MockVolumeKeysNotifier extends Mock implements VolumeKeysNotifier {}
|
||||||
|
|
||||||
class _MockMeteringCommunicationBloc extends MockBloc<
|
class _MockMeteringCommunicationBloc extends MockBloc<
|
||||||
communication_events.MeteringCommunicationEvent,
|
communication_events.MeteringCommunicationEvent,
|
||||||
communication_states.MeteringCommunicationState> implements MeteringCommunicationBloc {}
|
communication_states.MeteringCommunicationState> implements MeteringCommunicationBloc {}
|
||||||
|
|
||||||
class _MockMeteringInteractor extends Mock implements MeteringInteractor {}
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
late _MockMeteringInteractor meteringInteractor;
|
late _MockMeteringInteractor meteringInteractor;
|
||||||
|
late _MockVolumeKeysNotifier volumeKeysNotifier;
|
||||||
late _MockMeteringCommunicationBloc communicationBloc;
|
late _MockMeteringCommunicationBloc communicationBloc;
|
||||||
late MeteringBloc bloc;
|
late MeteringBloc bloc;
|
||||||
const iso100 = IsoValue(100, StopType.full);
|
const iso100 = IsoValue(100, StopType.full);
|
||||||
|
@ -34,16 +39,19 @@ void main() {
|
||||||
when(meteringInteractor.responseVibration).thenAnswer((_) async {});
|
when(meteringInteractor.responseVibration).thenAnswer((_) async {});
|
||||||
when(meteringInteractor.errorVibration).thenAnswer((_) async {});
|
when(meteringInteractor.errorVibration).thenAnswer((_) async {});
|
||||||
|
|
||||||
|
volumeKeysNotifier = _MockVolumeKeysNotifier();
|
||||||
communicationBloc = _MockMeteringCommunicationBloc();
|
communicationBloc = _MockMeteringCommunicationBloc();
|
||||||
|
|
||||||
bloc = MeteringBloc(
|
bloc = MeteringBloc(
|
||||||
meteringInteractor,
|
meteringInteractor,
|
||||||
|
volumeKeysNotifier,
|
||||||
communicationBloc,
|
communicationBloc,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDown(() {
|
tearDown(() {
|
||||||
bloc.close();
|
bloc.close();
|
||||||
|
//volumeKeysNotifier.dispose();
|
||||||
communicationBloc.close();
|
communicationBloc.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -487,7 +495,7 @@ void main() {
|
||||||
group(
|
group(
|
||||||
'`EquipmentProfileChangedEvent`',
|
'`EquipmentProfileChangedEvent`',
|
||||||
() {
|
() {
|
||||||
final reducedProfile = EquipmentProfileData(
|
final reducedProfile = EquipmentProfile(
|
||||||
id: '0',
|
id: '0',
|
||||||
name: 'Reduced',
|
name: 'Reduced',
|
||||||
apertureValues: ApertureValue.values,
|
apertureValues: ApertureValue.values,
|
||||||
|
@ -606,4 +614,66 @@ void main() {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
group(
|
||||||
|
'`Volume keys shutter action`',
|
||||||
|
() {
|
||||||
|
blocTest<MeteringBloc, MeteringState>(
|
||||||
|
'Add/remove listener',
|
||||||
|
build: () => bloc,
|
||||||
|
verify: (_) {
|
||||||
|
verify(() => volumeKeysNotifier.addListener(bloc.onVolumeKey)).called(1);
|
||||||
|
verify(() => volumeKeysNotifier.removeListener(bloc.onVolumeKey)).called(1);
|
||||||
|
},
|
||||||
|
expect: () => [],
|
||||||
|
);
|
||||||
|
|
||||||
|
blocTest<MeteringBloc, MeteringState>(
|
||||||
|
'onVolumeKey & VolumeAction.shutter',
|
||||||
|
build: () => bloc,
|
||||||
|
act: (bloc) async {
|
||||||
|
bloc.onVolumeKey();
|
||||||
|
},
|
||||||
|
setUp: () {
|
||||||
|
when(() => meteringInteractor.volumeAction).thenReturn(VolumeAction.shutter);
|
||||||
|
},
|
||||||
|
verify: (_) {},
|
||||||
|
expect: () => [isA<LoadingState>()],
|
||||||
|
);
|
||||||
|
|
||||||
|
blocTest<MeteringBloc, MeteringState>(
|
||||||
|
'onVolumeKey & VolumeAction.none',
|
||||||
|
build: () => bloc,
|
||||||
|
act: (bloc) async {
|
||||||
|
bloc.onVolumeKey();
|
||||||
|
},
|
||||||
|
setUp: () {
|
||||||
|
when(() => meteringInteractor.volumeAction).thenReturn(VolumeAction.none);
|
||||||
|
},
|
||||||
|
verify: (_) {},
|
||||||
|
expect: () => [],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
group(
|
||||||
|
'`SettingOpenedEvent`/`SettingsClosedEvent`',
|
||||||
|
() {
|
||||||
|
blocTest<MeteringBloc, MeteringState>(
|
||||||
|
'Settings opened & closed',
|
||||||
|
build: () => bloc,
|
||||||
|
act: (bloc) async {
|
||||||
|
bloc.add(const SettingsOpenedEvent());
|
||||||
|
bloc.add(const SettingsClosedEvent());
|
||||||
|
},
|
||||||
|
verify: (_) {
|
||||||
|
verify(() => communicationBloc.add(const communication_events.SettingsOpenedEvent()))
|
||||||
|
.called(1);
|
||||||
|
verify(() => communicationBloc.add(const communication_events.SettingsClosedEvent()))
|
||||||
|
.called(1);
|
||||||
|
},
|
||||||
|
expect: () => [],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,4 +98,30 @@ void main() {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
group(
|
||||||
|
'`SettingsOpenedEvent`/`SettingsClosedEvent`',
|
||||||
|
() {
|
||||||
|
blocTest<MeteringCommunicationBloc, MeteringCommunicationState>(
|
||||||
|
'Multiple consequtive settings events',
|
||||||
|
build: () => bloc,
|
||||||
|
act: (bloc) async {
|
||||||
|
bloc.add(const SettingsOpenedEvent());
|
||||||
|
bloc.add(const SettingsOpenedEvent());
|
||||||
|
bloc.add(const SettingsOpenedEvent());
|
||||||
|
bloc.add(const SettingsClosedEvent());
|
||||||
|
bloc.add(const SettingsClosedEvent());
|
||||||
|
bloc.add(const SettingsClosedEvent());
|
||||||
|
bloc.add(const SettingsOpenedEvent());
|
||||||
|
bloc.add(const SettingsClosedEvent());
|
||||||
|
},
|
||||||
|
expect: () => [
|
||||||
|
isA<SettingsOpenedState>(),
|
||||||
|
isA<SettingsClosedState>(),
|
||||||
|
isA<SettingsOpenedState>(),
|
||||||
|
isA<SettingsClosedState>(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,12 +14,12 @@ import 'package:lightmeter/screens/metering/components/camera_container/models/c
|
||||||
import 'package:lightmeter/screens/metering/components/camera_container/state_container_camera.dart';
|
import 'package:lightmeter/screens/metering/components/camera_container/state_container_camera.dart';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
|
class _MockMeteringInteractor extends Mock implements MeteringInteractor {}
|
||||||
|
|
||||||
class _MockMeteringCommunicationBloc extends MockBloc<
|
class _MockMeteringCommunicationBloc extends MockBloc<
|
||||||
communication_events.MeteringCommunicationEvent,
|
communication_events.MeteringCommunicationEvent,
|
||||||
communication_states.MeteringCommunicationState> implements MeteringCommunicationBloc {}
|
communication_states.MeteringCommunicationState> implements MeteringCommunicationBloc {}
|
||||||
|
|
||||||
class _MockMeteringInteractor extends Mock implements MeteringInteractor {}
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
TestWidgetsFlutterBinding.ensureInitialized();
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
@ -140,11 +140,11 @@ void main() {
|
||||||
'Request denied',
|
'Request denied',
|
||||||
build: () => bloc,
|
build: () => bloc,
|
||||||
setUp: () {
|
setUp: () {
|
||||||
when(() => meteringInteractor.requestPermission()).thenAnswer((_) async => false);
|
when(() => meteringInteractor.requestCameraPermission()).thenAnswer((_) async => false);
|
||||||
},
|
},
|
||||||
act: (bloc) => bloc.add(const RequestPermissionEvent()),
|
act: (bloc) => bloc.add(const RequestPermissionEvent()),
|
||||||
verify: (_) {
|
verify: (_) {
|
||||||
verify(() => meteringInteractor.requestPermission()).called(1);
|
verify(() => meteringInteractor.requestCameraPermission()).called(1);
|
||||||
},
|
},
|
||||||
expect: () => [
|
expect: () => [
|
||||||
isA<CameraErrorState>()
|
isA<CameraErrorState>()
|
||||||
|
@ -156,12 +156,12 @@ void main() {
|
||||||
'Request granted -> check denied',
|
'Request granted -> check denied',
|
||||||
build: () => bloc,
|
build: () => bloc,
|
||||||
setUp: () {
|
setUp: () {
|
||||||
when(() => meteringInteractor.requestPermission()).thenAnswer((_) async => true);
|
when(() => meteringInteractor.requestCameraPermission()).thenAnswer((_) async => true);
|
||||||
when(() => meteringInteractor.checkCameraPermission()).thenAnswer((_) async => false);
|
when(() => meteringInteractor.checkCameraPermission()).thenAnswer((_) async => false);
|
||||||
},
|
},
|
||||||
act: (bloc) => bloc.add(const RequestPermissionEvent()),
|
act: (bloc) => bloc.add(const RequestPermissionEvent()),
|
||||||
verify: (_) {
|
verify: (_) {
|
||||||
verify(() => meteringInteractor.requestPermission()).called(1);
|
verify(() => meteringInteractor.requestCameraPermission()).called(1);
|
||||||
verify(() => meteringInteractor.checkCameraPermission()).called(1);
|
verify(() => meteringInteractor.checkCameraPermission()).called(1);
|
||||||
},
|
},
|
||||||
expect: () => [
|
expect: () => [
|
||||||
|
@ -175,12 +175,12 @@ void main() {
|
||||||
'Request granted -> check granted',
|
'Request granted -> check granted',
|
||||||
build: () => bloc,
|
build: () => bloc,
|
||||||
setUp: () {
|
setUp: () {
|
||||||
when(() => meteringInteractor.requestPermission()).thenAnswer((_) async => true);
|
when(() => meteringInteractor.requestCameraPermission()).thenAnswer((_) async => true);
|
||||||
when(() => meteringInteractor.checkCameraPermission()).thenAnswer((_) async => true);
|
when(() => meteringInteractor.checkCameraPermission()).thenAnswer((_) async => true);
|
||||||
},
|
},
|
||||||
act: (bloc) => bloc.add(const RequestPermissionEvent()),
|
act: (bloc) => bloc.add(const RequestPermissionEvent()),
|
||||||
verify: (_) {
|
verify: (_) {
|
||||||
verify(() => meteringInteractor.requestPermission()).called(1);
|
verify(() => meteringInteractor.requestCameraPermission()).called(1);
|
||||||
verify(() => meteringInteractor.checkCameraPermission()).called(1);
|
verify(() => meteringInteractor.checkCameraPermission()).called(1);
|
||||||
},
|
},
|
||||||
expect: () => initializedStateSequence,
|
expect: () => initializedStateSequence,
|
||||||
|
@ -310,6 +310,30 @@ void main() {
|
||||||
},
|
},
|
||||||
expect: () => [
|
expect: () => [
|
||||||
...initializedStateSequence,
|
...initializedStateSequence,
|
||||||
|
const CameraInitState(),
|
||||||
|
...initializedStateSequence,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
blocTest<CameraContainerBloc, CameraContainerState>(
|
||||||
|
'onCommunicationState',
|
||||||
|
setUp: () {
|
||||||
|
when(() => meteringInteractor.checkCameraPermission()).thenAnswer((_) async => true);
|
||||||
|
},
|
||||||
|
build: () => bloc,
|
||||||
|
act: (bloc) async {
|
||||||
|
bloc.add(const InitializeEvent());
|
||||||
|
await Future.delayed(Duration.zero);
|
||||||
|
bloc.onCommunicationState(const communication_states.SettingsOpenedState());
|
||||||
|
await Future.delayed(Duration.zero);
|
||||||
|
bloc.onCommunicationState(const communication_states.SettingsClosedState());
|
||||||
|
},
|
||||||
|
verify: (_) {
|
||||||
|
verify(() => meteringInteractor.checkCameraPermission()).called(2);
|
||||||
|
},
|
||||||
|
expect: () => [
|
||||||
|
...initializedStateSequence,
|
||||||
|
const CameraInitState(),
|
||||||
...initializedStateSequence,
|
...initializedStateSequence,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
|
@ -78,4 +78,67 @@ void main() {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
group(
|
||||||
|
'`communication_states.SettingsOpenedState()`',
|
||||||
|
() {
|
||||||
|
const List<int> luxIterable = [1, 2, 2, 2, 3];
|
||||||
|
final List<double> resultList = luxIterable.map((lux) => log2(lux / 2.5)).toList();
|
||||||
|
blocTest<LightSensorContainerBloc, LightSensorContainerState>(
|
||||||
|
'Metering is already canceled',
|
||||||
|
build: () => bloc,
|
||||||
|
setUp: () {
|
||||||
|
when(() => meteringInteractor.luxStream())
|
||||||
|
.thenAnswer((_) => Stream.fromIterable(luxIterable));
|
||||||
|
when(() => meteringInteractor.lightSensorEvCalibration).thenReturn(0.0);
|
||||||
|
},
|
||||||
|
act: (bloc) async {
|
||||||
|
bloc.onCommunicationState(const communication_states.SettingsOpenedState());
|
||||||
|
},
|
||||||
|
verify: (_) {
|
||||||
|
verifyNever(() => meteringInteractor.luxStream().listen((_) {}));
|
||||||
|
verifyNever(() => meteringInteractor.lightSensorEvCalibration);
|
||||||
|
verify(() {
|
||||||
|
communicationBloc.add(const communication_events.MeteringEndedEvent(null));
|
||||||
|
}).called(2); // +1 from dispose
|
||||||
|
},
|
||||||
|
expect: () => [],
|
||||||
|
);
|
||||||
|
|
||||||
|
blocTest<LightSensorContainerBloc, LightSensorContainerState>(
|
||||||
|
'Metering is in progress',
|
||||||
|
build: () => bloc,
|
||||||
|
setUp: () {
|
||||||
|
when(() => meteringInteractor.luxStream())
|
||||||
|
.thenAnswer((_) => Stream.fromIterable(luxIterable));
|
||||||
|
when(() => meteringInteractor.lightSensorEvCalibration).thenReturn(0.0);
|
||||||
|
},
|
||||||
|
act: (bloc) async {
|
||||||
|
bloc.onCommunicationState(const communication_states.MeasureState());
|
||||||
|
await Future.delayed(Duration.zero);
|
||||||
|
bloc.onCommunicationState(const communication_states.SettingsOpenedState());
|
||||||
|
bloc.onCommunicationState(const communication_states.SettingsClosedState());
|
||||||
|
},
|
||||||
|
verify: (_) {
|
||||||
|
verify(() => meteringInteractor.luxStream().listen((_) {})).called(1);
|
||||||
|
verify(() => meteringInteractor.lightSensorEvCalibration).called(5);
|
||||||
|
verify(() {
|
||||||
|
communicationBloc.add(communication_events.MeteringInProgressEvent(resultList.first));
|
||||||
|
}).called(1);
|
||||||
|
verify(() {
|
||||||
|
communicationBloc.add(communication_events.MeteringInProgressEvent(resultList[1]));
|
||||||
|
}).called(3);
|
||||||
|
verify(() {
|
||||||
|
communicationBloc.add(communication_events.MeteringInProgressEvent(resultList.last));
|
||||||
|
}).called(1);
|
||||||
|
verify(() {
|
||||||
|
communicationBloc.add(communication_events.MeteringEndedEvent(resultList.last));
|
||||||
|
}).called(3); // +1 from settings closed, +1 from dispose
|
||||||
|
},
|
||||||
|
expect: () => resultList.map(
|
||||||
|
(e) => isA<LightSensorContainerState>().having((state) => state.ev100, 'ev100', e),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue