mirror of
https://github.com/vodemn/m3_lightmeter.git
synced 2025-05-09 17:50:40 +00:00
Merge branch 'main' of https://github.com/vodemn/m3_lightmeter into feature/ML-62
This commit is contained in:
commit
49a0da1403
138 changed files with 4586 additions and 2592 deletions
.github
.gitignore.vscode
README.mdandroid/app
assets
iap
integration_test
ios/Runner.xcodeproj
lib
application.dartapplication_wrapper.dart
data
features.dartfirebase.dartfirebase_options.dartinteractors
l10n
main_dev.dartmain_prod.dartmain_release.dartplatform_config.dartproviders.dartproviders
equipment_profile_provider.dartev_source_type_provider.dartmetering_screen_layout_provider.dartservices_provider.dartstop_type_provider.dartsupported_locale_provider.darttheme_provider.dartuser_preferences_provider.dart
res
screens
metering
bloc_metering.dartwidget_container_readings.dartevent_metering.dartflow_metering.dartscreen_metering.dartstate_metering.dart
components
bottom_controls
camera_container
bloc_container_camera.dartprovider_container_camera.dartwidget_container_camera.dart
components
camera_controls/components/exposure_offset_slider
camera_preview
light_sensor_container
bloc_container_light_sensor.dartprovider_container_light_sensor.dartwidget_container_light_sensor.dart
shared
exposure_pairs_list
metering_top_bar
readings_container
components
equipment_profile_picker
extreme_exposure_pairs_container
film_picker
iso_picker
nd_picker
shared
utils
settings/components
about/components
report_issue
source_code
write_email
general/components
caffeine
haptics
language
volume_actions
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
github: [vodemn]
|
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -2,7 +2,7 @@
|
|||
name: Bug report
|
||||
about: Create a bug report to help improve the app
|
||||
title: ''
|
||||
labels: bug
|
||||
labels: bug, user feedback
|
||||
assignees: vodemn
|
||||
|
||||
---
|
||||
|
|
|
@ -2,19 +2,16 @@
|
|||
name: Feature request or improvement
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: feature
|
||||
labels: feature, user feedback
|
||||
assignees: vodemn
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
**Describe the feature or the problem it solves**
|
||||
A clear and concise description of what the problem is.
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
|
|
8
.github/scripts/increment_build_number.sh
vendored
Normal file
8
.github/scripts/increment_build_number.sh
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
export newVersion="$1"
|
||||
|
||||
if [[ -n "$newVersion" ]]; then
|
||||
#https://stackoverflow.com/a/30214769/13167574
|
||||
perl -i -pe 's/^(version:\s+)(\d+\.\d+\.\d+)(\+)(\d+)$/$1.$ENV{'newVersion'}.$3.($4+1)/e' pubspec.yaml
|
||||
else
|
||||
echo "argument error"
|
||||
fi
|
2
.github/scripts/stub_iap.sh
vendored
Normal file
2
.github/scripts/stub_iap.sh
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
# https://unix.stackexchange.com/questions/435708/regex-multiline-pattern-and-substitution-replacement
|
||||
perl -0777 -i -pe 's/( m3_lightmeter_iap:\n)( git:\n url: "https:\/\/github.com\/vodemn\/m3_lightmeter_iap"\n ref: v\d{1,2}.\d{1,2}.\d{1,2})/$1 path: iap/sg' pubspec.yaml
|
|
@ -16,24 +16,31 @@ on:
|
|||
- dev
|
||||
- prod
|
||||
default: 'dev'
|
||||
include-iap:
|
||||
type: boolean
|
||||
description: Include IAP package
|
||||
default: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build .apk
|
||||
runs-on: macos-11
|
||||
timeout-minutes: 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
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Connect private iap package
|
||||
uses: webfactory/ssh-agent@v0.8.0
|
||||
if: ${{ inputs.include-iap }}
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.M3_LIGHTMETER_IAP_KEY }}
|
||||
|
||||
- name: Override iap package with stub
|
||||
if: ${{ !inputs.include-iap }}
|
||||
run: bash ./.github/scripts/stub_iap.sh
|
||||
|
||||
- uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: "zulu"
|
||||
|
@ -71,6 +78,7 @@ jobs:
|
|||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: '3.10.0'
|
||||
|
||||
- name: Prepare flutter project
|
||||
run: |
|
||||
|
@ -78,10 +86,10 @@ jobs:
|
|||
flutter pub get
|
||||
flutter pub run intl_utils:generate
|
||||
|
||||
- name: Build Apk
|
||||
- name: Build .apk
|
||||
env:
|
||||
FLAVOR: ${{ github.event.inputs.flavor }}
|
||||
run: flutter build apk --release --flavor $FLAVOR --dart-define cameraPreviewAspectRatio=2/3 -t lib/main_$FLAVOR.dart
|
||||
run: flutter build apk --release --flavor $FLAVOR --dart-define cameraPreviewAspectRatio=240/320 -t lib/main_$FLAVOR.dart
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
158
.github/workflows/cd_prod.yml
vendored
158
.github/workflows/cd_prod.yml
vendored
|
@ -1,158 +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: Build prod .aab & .apk
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
env:
|
||||
BUILD_ARGS: --release --flavor prod --dart-define cameraPreviewAspectRatio=2/3 -t lib/main_prod.dart
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build .apk & .aab
|
||||
runs-on: macos-11
|
||||
timeout-minutes: 30
|
||||
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: actions/setup-java@v2
|
||||
with:
|
||||
distribution: "zulu"
|
||||
java-version: "11"
|
||||
|
||||
- name: Restore Android keystore .jsk and .properties files
|
||||
env:
|
||||
KEYSTORE: ${{ secrets.KEYSTORE }}
|
||||
KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }}
|
||||
run: |
|
||||
KEYSTORE_PATH=$RUNNER_TEMP/keystore.jks
|
||||
echo -n "$KEYSTORE" | base64 --decode --output $KEYSTORE_PATH
|
||||
cp $KEYSTORE_PATH ./android/app
|
||||
KEYSTORE_PROPERTIES_PATH=$RUNNER_TEMP/key.properties
|
||||
echo -n "$KEYSTORE_PROPERTIES" | base64 --decode --output $KEYSTORE_PROPERTIES_PATH
|
||||
cp $KEYSTORE_PROPERTIES_PATH ./android
|
||||
|
||||
- name: Restore android/app/google-services.json
|
||||
env:
|
||||
GOOGLE_SERVICES_JSON_ANDROID: ${{ secrets.GOOGLE_SERVICES_JSON_ANDROID }}
|
||||
run: |
|
||||
GOOGLE_SERVICES_JSON_ANDROID_PATH=$RUNNER_TEMP/google-services.json
|
||||
echo -n "$GOOGLE_SERVICES_JSON_ANDROID" | base64 --decode --output $GOOGLE_SERVICES_JSON_ANDROID_PATH
|
||||
cp $GOOGLE_SERVICES_JSON_ANDROID_PATH ./android/app
|
||||
|
||||
- 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: Increment build number & replace version number
|
||||
run: perl -i -pe 's/^(version:\s+)(\d+\.\d+\.\d+)(\+)(\d+)$/$1."${{ github.event.inputs.version }}".$3.($4+1)/e' pubspec.yaml
|
||||
|
||||
- name: Install Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
|
||||
- name: Prepare flutter project
|
||||
run: |
|
||||
flutter --version
|
||||
flutter pub get
|
||||
flutter pub run intl_utils:generate
|
||||
|
||||
- name: Build apk
|
||||
run: flutter build apk $BUILD_ARGS
|
||||
|
||||
- name: Upload apk to artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: m3_lightmeter_apk
|
||||
path: build/app/outputs/flutter-apk/app-prod-release.apk
|
||||
|
||||
- name: Build appbundle
|
||||
run: flutter build appbundle $BUILD_ARGS
|
||||
|
||||
- name: Upload app bundle to artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: m3_lightmeter_bundle
|
||||
path: build/app/outputs/bundle/prodRelease/app-prod-release.aab
|
||||
|
||||
update-version-in-repo:
|
||||
name: Update repo version
|
||||
needs: [build]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Increment build number & replace version number
|
||||
run: perl -i -pe 's/^(version:\s+)(\d+\.\d+\.\d+)(\+)(\d+)$/$1."${{ github.event.inputs.version }}".$3.($4+1)/e' pubspec.yaml
|
||||
|
||||
- name: Commit changes
|
||||
run: |
|
||||
git config --global user.name "vodemn"
|
||||
git config --global user.email "vadim.turko@gmail.com"
|
||||
git add -A
|
||||
git commit -m "Version bump"
|
||||
|
||||
- name: Push to main
|
||||
uses: CasperWA/push-protected@v2
|
||||
with:
|
||||
token: ${{ secrets.PUSH_TO_MAIN_TOKEN }}
|
||||
branch: ${{ github.ref_name }}
|
||||
unprotect_reviews: true
|
||||
|
||||
create-release:
|
||||
name: Create Github release
|
||||
needs: [build, update-version-in-repo]
|
||||
if: github.ref_name == 'main'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Download apk
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: m3_lightmeter_apk
|
||||
|
||||
- name: Download app bundle
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: m3_lightmeter_bundle
|
||||
|
||||
- name: Rename artifacts
|
||||
run: |
|
||||
mv app-prod-release.apk m3_lightmeter.apk
|
||||
mv app-prod-release.aab m3_lightmeter.aab
|
||||
|
||||
- uses: ncipollo/release-action@v1.12.0
|
||||
with:
|
||||
artifacts: "m3_lightmeter.apk, m3_lightmeter.aab"
|
||||
skipIfReleaseExists: true
|
||||
tag: "v${{ github.event.inputs.version }}"
|
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
|
290
.github/workflows/create_release.yml
vendored
Normal file
290
.github/workflows/create_release.yml
vendored
Normal file
|
@ -0,0 +1,290 @@
|
|||
# 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.
|
||||
|
||||
|
||||
# This workflow uses perl regex. For better syntaxis understading see these docs:
|
||||
# https://perldoc.perl.org/perlrequick#Search-and-replace
|
||||
# https://perldoc.perl.org/perlre#Other-Modifiers
|
||||
|
||||
name: Create new release
|
||||
|
||||
run-name: Release v${{ inputs.version }}
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version"
|
||||
required: true
|
||||
type: string
|
||||
release-notes:
|
||||
description: "Release notes"
|
||||
required: true
|
||||
type: string
|
||||
github-release:
|
||||
type: boolean
|
||||
description: Create Github release
|
||||
default: true
|
||||
google-play-release:
|
||||
type: boolean
|
||||
description: Create Google Play release
|
||||
default: true
|
||||
include-iap:
|
||||
type: boolean
|
||||
description: Include IAP package
|
||||
default: true
|
||||
|
||||
env:
|
||||
BUILD_ARGS: --release --flavor prod --dart-define cameraPreviewAspectRatio=240/320 -t lib/main_prod.dart
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build .apk & .aab
|
||||
if: ${{ inputs.github-release || inputs.google-play-release }}
|
||||
runs-on: macos-11
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Connect private iap package
|
||||
uses: webfactory/ssh-agent@v0.8.0
|
||||
if: ${{ inputs.include-iap }}
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.M3_LIGHTMETER_IAP_KEY }}
|
||||
|
||||
- name: Override iap package with stub
|
||||
if: ${{ !inputs.include-iap }}
|
||||
run: bash ./.github/scripts/stub_iap.sh
|
||||
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: "zulu"
|
||||
java-version: "11"
|
||||
|
||||
- name: Restore Android keystore .jsk and .properties files
|
||||
env:
|
||||
KEYSTORE: ${{ secrets.KEYSTORE }}
|
||||
KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }}
|
||||
run: |
|
||||
KEYSTORE_PATH=$RUNNER_TEMP/keystore.jks
|
||||
echo -n "$KEYSTORE" | base64 --decode --output $KEYSTORE_PATH
|
||||
cp $KEYSTORE_PATH ./android/app
|
||||
KEYSTORE_PROPERTIES_PATH=$RUNNER_TEMP/key.properties
|
||||
echo -n "$KEYSTORE_PROPERTIES" | base64 --decode --output $KEYSTORE_PROPERTIES_PATH
|
||||
cp $KEYSTORE_PROPERTIES_PATH ./android
|
||||
|
||||
- name: Restore android/app/google-services.json
|
||||
env:
|
||||
GOOGLE_SERVICES_JSON_ANDROID: ${{ secrets.GOOGLE_SERVICES_JSON_ANDROID }}
|
||||
run: |
|
||||
GOOGLE_SERVICES_JSON_ANDROID_PATH=$RUNNER_TEMP/google-services.json
|
||||
echo -n "$GOOGLE_SERVICES_JSON_ANDROID" | base64 --decode --output $GOOGLE_SERVICES_JSON_ANDROID_PATH
|
||||
cp $GOOGLE_SERVICES_JSON_ANDROID_PATH ./android/app
|
||||
|
||||
- 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
|
||||
|
||||
# This step makes sense when Github release is enabled because this release increments the build number.
|
||||
# Therefore here we have to increment it as well to build an apk with the same build number.
|
||||
- name: Increment build number & replace version number
|
||||
if: ${{ inputs.github-release }}
|
||||
run: bash ./.github/scripts/increment_build_number.sh ${{ github.event.inputs.version }}
|
||||
|
||||
- name: Install Flutter
|
||||
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: Build apk
|
||||
if: ${{ inputs.github-release }}
|
||||
run: flutter build apk $BUILD_ARGS
|
||||
|
||||
- name: Upload apk to artifacts
|
||||
if: ${{ inputs.github-release }}
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: m3_lightmeter_apk
|
||||
path: build/app/outputs/flutter-apk/app-prod-release.apk
|
||||
|
||||
- name: Build appbundle
|
||||
if: ${{ inputs.google-play-release }}
|
||||
run: flutter build appbundle $BUILD_ARGS
|
||||
|
||||
- name: Upload app bundle to artifacts
|
||||
if: ${{ inputs.google-play-release }}
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: m3_lightmeter_bundle
|
||||
path: build/app/outputs/bundle/prodRelease/app-prod-release.aab
|
||||
|
||||
generate-release-notes:
|
||||
name: Generate release notes
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Generate release notes
|
||||
run: |
|
||||
echo ${{ inputs.release-notes }} > whatsnew-en-US.md
|
||||
perl -i -pe 's/\s{1}(-{1})/\n$1/g' whatsnew-en-US.md
|
||||
|
||||
- name: Upload merged_native_libs.zip to artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: whatsnew-en-US
|
||||
path: whatsnew-en-US.md
|
||||
|
||||
update-version-in-repo:
|
||||
name: Update repo version
|
||||
if: ${{ inputs.github-release }}
|
||||
needs: [build]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Increment build number & replace version number
|
||||
run: bash ./.github/scripts/increment_build_number.sh ${{ github.event.inputs.version }}
|
||||
|
||||
- name: Commit changes
|
||||
run: |
|
||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git add -A
|
||||
git commit -m "Version bump"
|
||||
|
||||
- name: Push to main
|
||||
uses: CasperWA/push-protected@v2
|
||||
with:
|
||||
token: ${{ secrets.PUSH_TO_MAIN_TOKEN }}
|
||||
branch: ${{ github.ref_name }}
|
||||
unprotect_reviews: true
|
||||
|
||||
create-github-release:
|
||||
name: Create Github release
|
||||
if: ${{ inputs.github-release }}
|
||||
needs: [build, generate-release-notes, update-version-in-repo]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Download apk
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: m3_lightmeter_apk
|
||||
|
||||
- name: Rename apk
|
||||
run: mv app-prod-release.apk m3_lightmeter.apk
|
||||
|
||||
- name: Download release notes
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: whatsnew-en-US
|
||||
|
||||
- uses: ncipollo/release-action@v1.12.0
|
||||
with:
|
||||
artifacts: "m3_lightmeter.apk"
|
||||
skipIfReleaseExists: true
|
||||
tag: "v${{ github.event.inputs.version }}"
|
||||
bodyFile: "whatsnew-en-US.md"
|
||||
|
||||
- name: Delete apk artifact
|
||||
uses: geekyeggo/delete-artifact@v2
|
||||
with:
|
||||
name: m3_lightmeter_apk
|
||||
|
||||
create-google-play-release:
|
||||
name: Create Google Play release
|
||||
if: ${{ inputs.google-play-release }}
|
||||
needs: [build, generate-release-notes]
|
||||
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
|
||||
|
||||
- name: Extract & zip merged_native_libs
|
||||
run: |
|
||||
unzip app-prod-release.aab
|
||||
(cd base/lib && zip -r "$OLDPWD/merged_native_libs.zip" .)
|
||||
|
||||
- name: Download release notes
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: whatsnew-en-US
|
||||
|
||||
- name: Move release notes to a folder
|
||||
run: |
|
||||
mv whatsnew-en-US.md whatsnew-en-US
|
||||
mkdir whatsnew
|
||||
mv whatsnew-en-US whatsnew
|
||||
|
||||
# https://unix.stackexchange.com/questions/13466/can-grep-output-only-specified-groupings-that-match'
|
||||
# https://stackoverflow.com/questions/74353311/github-workflow-unable-to-process-file-command-env-successfully
|
||||
- name: Create Google Play release name
|
||||
id: release-name
|
||||
run: |
|
||||
RELEASE_NAME=$(echo "$(cat pubspec.yaml)" | sed -n -r "s/^version:\s{1}(.*)[+](.*)$/700\2 (\1)/p")
|
||||
echo "release_name=$RELEASE_NAME" >> $GITHUB_ENV
|
||||
|
||||
- name: Create Google Play release
|
||||
id: create-google-play-release-step
|
||||
uses: r0adkll/upload-google-play@v1.1.1
|
||||
with:
|
||||
serviceAccountJsonPlainText: ${{ secrets.GH_ACTIONS_SERVICE_ACCOUNT_JSON }}
|
||||
packageName: com.vodemn.lightmeter
|
||||
releaseFiles: app-prod-release.aab
|
||||
releaseName: ${{ env.release_name }}
|
||||
track: production
|
||||
status: completed
|
||||
debugSymbols: merged_native_libs.zip
|
||||
whatsNewDirectory: whatsnew
|
||||
|
||||
# https://docs.github.com/en/actions/learn-github-actions/expressions#failure-with-conditions
|
||||
- name: Zip app bundle and merged_native_libs
|
||||
if: ${{ failure() && steps.create-google-play-release-step.conclusion == 'failure' }}
|
||||
run: zip m3_lightmeter_release.zip app-prod-release.aab merged_native_libs.zip
|
||||
|
||||
- name: Upload release zip to artifacts
|
||||
if: ${{ failure() && steps.create-google-play-release-step.conclusion == 'failure' }}
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: m3_lightmeter_release
|
||||
path: m3_lightmeter_release.zip
|
||||
|
||||
- name: Delete app bundle & merged native libs artifacts
|
||||
if: ${{ always() }}
|
||||
uses: geekyeggo/delete-artifact@v2
|
||||
with:
|
||||
name: m3_lightmeter_bundle
|
||||
|
||||
cleanup:
|
||||
name: Cleanup
|
||||
if: ${{ always() }}
|
||||
needs: [create-github-release, create-google-play-release]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Delete release notes artifact
|
||||
uses: geekyeggo/delete-artifact@v2
|
||||
with:
|
||||
name: whatsnew-en-US
|
61
.github/workflows/pr_check.yml
vendored
Normal file
61
.github/workflows/pr_check.yml
vendored
Normal file
|
@ -0,0 +1,61 @@
|
|||
# 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: 5
|
||||
steps:
|
||||
- uses: 8BitJonny/gh-get-current-pr@2.2.0
|
||||
id: PR
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Connect private iap package
|
||||
uses: webfactory/ssh-agent@v0.8.0
|
||||
id: fetch-iap
|
||||
if: steps.PR.outputs.number == 'null' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.M3_LIGHTMETER_IAP_KEY }}
|
||||
|
||||
- name: Override iap package with stub
|
||||
id: override-iap
|
||||
if: steps.fetch-iap.conclusion != 'success'
|
||||
run: bash ./.github/scripts/stub_iap.sh
|
||||
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: "3.10.0"
|
||||
|
||||
- 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
|
||||
|
||||
- name: Analyze project source with stub
|
||||
if: steps.override-iap.conclusion != 'success'
|
||||
run: |
|
||||
bash ./.github/scripts/stub_iap.sh
|
||||
flutter pub get
|
||||
flutter analyze lib --fatal-infos
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -57,6 +57,7 @@ keystore.properties
|
|||
android/app/google-services.json
|
||||
ios/firebase_app_id_file.json
|
||||
ios/Runner/GoogleService-Info.plist
|
||||
lib/firebase_options.dart
|
||||
/lib/firebase_options.dart
|
||||
|
||||
coverage/
|
||||
coverage/
|
||||
screenshots/
|
66
.vscode/launch.json
vendored
66
.vscode/launch.json
vendored
|
@ -5,21 +5,62 @@
|
|||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "dev (android)",
|
||||
"name": "dev-debug (android)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "debug",
|
||||
"args": [
|
||||
"--flavor",
|
||||
"dev",
|
||||
"--dart-define",
|
||||
"cameraPreviewAspectRatio=2/3",
|
||||
"cameraPreviewAspectRatio=240/320",
|
||||
],
|
||||
"program": "${workspaceFolder}/lib/main_dev.dart",
|
||||
},
|
||||
{
|
||||
"name": "dev (ios)",
|
||||
"name": "dev-profile (android)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "profile",
|
||||
"args": [
|
||||
"--flavor",
|
||||
"dev",
|
||||
"--dart-define",
|
||||
"cameraPreviewAspectRatio=240/320",
|
||||
],
|
||||
"program": "${workspaceFolder}/lib/main_dev.dart",
|
||||
},
|
||||
{
|
||||
"name": "prod-debug (android)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "debug",
|
||||
"args": [
|
||||
"--flavor",
|
||||
"prod",
|
||||
"--dart-define",
|
||||
"cameraPreviewAspectRatio=240/320",
|
||||
],
|
||||
"program": "${workspaceFolder}/lib/main_release.dart",
|
||||
},
|
||||
{
|
||||
"name": "prod-profile (android)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "profile",
|
||||
"args": [
|
||||
"--flavor",
|
||||
"prod",
|
||||
"--dart-define",
|
||||
"cameraPreviewAspectRatio=240/320",
|
||||
],
|
||||
"program": "${workspaceFolder}/lib/main_release.dart",
|
||||
},
|
||||
{
|
||||
"name": "dev-debug (ios)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "debug",
|
||||
"args": [
|
||||
"--flavor",
|
||||
"dev",
|
||||
|
@ -29,28 +70,17 @@
|
|||
"program": "${workspaceFolder}/lib/main_dev.dart",
|
||||
},
|
||||
{
|
||||
"name": "prod (android)",
|
||||
"name": "dev-profile (ios)",
|
||||
"request": "launch",
|
||||
"flutterMode": "profile",
|
||||
"type": "dart",
|
||||
"args": [
|
||||
"--flavor",
|
||||
"prod",
|
||||
"--dart-define",
|
||||
"cameraPreviewAspectRatio=2/3",
|
||||
],
|
||||
"program": "${workspaceFolder}/lib/main_prod.dart",
|
||||
},
|
||||
{
|
||||
"name": "prod (ios)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"args": [
|
||||
"--flavor",
|
||||
"prod",
|
||||
"dev",
|
||||
"--dart-define",
|
||||
"cameraPreviewAspectRatio=3/4",
|
||||
],
|
||||
"program": "${workspaceFolder}/lib/main_prod.dart",
|
||||
"program": "${workspaceFolder}/lib/main_dev.dart",
|
||||
},
|
||||
],
|
||||
}
|
6
.vscode/tasks.json
vendored
6
.vscode/tasks.json
vendored
|
@ -12,7 +12,7 @@
|
|||
"dev",
|
||||
"--release",
|
||||
"--dart-define",
|
||||
"cameraPreviewAspectRatio=2/3",
|
||||
"cameraPreviewAspectRatio=240/320",
|
||||
"-t",
|
||||
"lib/main_dev.dart",
|
||||
],
|
||||
|
@ -28,7 +28,7 @@
|
|||
"prod",
|
||||
"--release",
|
||||
"--dart-define",
|
||||
"cameraPreviewAspectRatio=2/3",
|
||||
"cameraPreviewAspectRatio=240/320",
|
||||
"-t",
|
||||
"lib/main_prod.dart",
|
||||
],
|
||||
|
@ -44,7 +44,7 @@
|
|||
"prod",
|
||||
"--release",
|
||||
"--dart-define",
|
||||
"cameraPreviewAspectRatio=2/3",
|
||||
"cameraPreviewAspectRatio=240/320",
|
||||
"-t",
|
||||
"lib/main_prod.dart",
|
||||
],
|
||||
|
|
65
README.md
65
README.md
|
@ -5,7 +5,7 @@
|
|||
- [Table of contents](#table-of-contents)
|
||||
- [Backstory](#backstory)
|
||||
- [Screenshots](#screenshots)
|
||||
- [Build](#build)
|
||||
- [Development](#development)
|
||||
- [Contribution](#contribution)
|
||||
- [iOS Limitations](#ios-limitations)
|
||||
|
||||
|
@ -27,22 +27,77 @@ Without further delay behold my new Lightmeter app inspired by Material You (a.k
|
|||
<img src="https://lh3.googleusercontent.com/15g_SPV8knDLFbz1_-wGNJFsJeyVWZ_y--TGHpk75MaaIdMDyTXY2_TL-Aw8bpOhpw" width="18.8%" />
|
||||
</p>
|
||||
|
||||
# Build
|
||||
# Development
|
||||
|
||||
As part of this project is private, you will be able to run this app from the _main_dev.dart_ file (i.e. --flavor dev). Also to avoid fatal errors the _main_prod.dart_ file is excluded from analysis.
|
||||
### 1. Install Flutter
|
||||
|
||||
To build this app you need to install Flutter 3.10.0 stable. [How to install](https://docs.flutter.dev/get-started/install).
|
||||
|
||||
### 3. Project setup
|
||||
|
||||
As part of the app's functionallity is in the private repo, you have to replace these lines in _pubspec.yaml_:
|
||||
|
||||
```yaml
|
||||
m3_lightmeter_iap:
|
||||
git:
|
||||
url: "https://github.com/vodemn/m3_lightmeter_iap"
|
||||
ref: main
|
||||
```
|
||||
|
||||
with these:
|
||||
|
||||
```yaml
|
||||
m3_lightmeter_iap:
|
||||
path: iap
|
||||
```
|
||||
|
||||
You can do it simply by running the script:
|
||||
|
||||
```console
|
||||
sh .github/scripts/stub_iap.sh
|
||||
```
|
||||
|
||||
> If you are using VSCode, you can open the workspace like so: _File -> Open Workspace from File -> m3_lightmeter.code-workspace_. Otherwise you have to run `flutter pub get` command from the iap folder.
|
||||
|
||||
Then you can fetch all the neccessary dependencies and generate translation files by running the following commands:
|
||||
|
||||
```console
|
||||
flutter pub get
|
||||
flutter pub run intl_utils:generate
|
||||
```
|
||||
|
||||
### 4. (Optional) Install Firebase
|
||||
|
||||
Out of the box Firebase Crashlytics won't work. If you want to add Crashlytics to your local build please follow [this guide](https://firebase.google.com/docs/flutter/setup).
|
||||
|
||||
### 5. Build
|
||||
|
||||
#### Android
|
||||
|
||||
You can build an apk by running the following command from the root of the repository:
|
||||
|
||||
```console
|
||||
flutter build apk --release --flavor dev --dart-define cameraPreviewAspectRatio=240/320 -t lib/main_dev.dart
|
||||
```
|
||||
|
||||
### iOS
|
||||
|
||||
TBD
|
||||
|
||||
# Contribution
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
|
|
@ -109,4 +109,5 @@ flutter {
|
|||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation "com.android.billingclient:billing-ktx:6.0.0"
|
||||
implementation "com.google.firebase:firebase-analytics:17.4.1"
|
||||
}
|
||||
|
|
BIN
assets/camera_stub_image.jpg
Normal file
BIN
assets/camera_stub_image.jpg
Normal file
Binary file not shown.
After ![]() (image error) Size: 899 KiB |
36
iap/.gitignore
vendored
Normal file
36
iap/.gitignore
vendored
Normal file
|
@ -0,0 +1,36 @@
|
|||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||
/pubspec.lock
|
||||
**/doc/api/
|
||||
.dart_tool/
|
||||
.packages
|
||||
build/
|
||||
|
||||
.fvm/
|
||||
*.properties
|
||||
ios/Flutter/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
10
iap/.metadata
Normal file
10
iap/.metadata
Normal file
|
@ -0,0 +1,10 @@
|
|||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: 9944297138845a94256f1cf37beb88ff9a8e811a
|
||||
channel: stable
|
||||
|
||||
project_type: package
|
1
iap/LICENSE
Normal file
1
iap/LICENSE
Normal file
|
@ -0,0 +1 @@
|
|||
TODO: Add your license here.
|
17
iap/README.md
Normal file
17
iap/README.md
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Lightmeter Pro
|
||||
|
||||
### Equipment profiles
|
||||
|
||||
Each equipment profile allows you to select:
|
||||
|
||||
- Aperture values and shutter speeds, that your lens and camera have
|
||||
- ND filters, that fit the chosen lens
|
||||
- ISO values, that your camera supports
|
||||
|
||||
Creating multiple profiles for different cameras and lenses allows you to easily switch between them and always have the relevant readings.
|
||||
|
||||
### Films in use
|
||||
|
||||
Select the films that you usually use. Selecting one will apply a correction to shutter speeds greater than 1" to compensate for the reciprocity failure.
|
||||
|
||||
Each equipment profile allows you to select:\n- Aperture values and shutter speeds, that your lens and camera have\n- ND filters, that fit the chosen lens\n- ISO values, that your camera supports\nCreating multiple profiles for different cameras and lenses allows you to easily switch between them and always have the relevant readings!
|
4
iap/analysis_options.yaml
Normal file
4
iap/analysis_options.yaml
Normal file
|
@ -0,0 +1,4 @@
|
|||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
34
iap/lib/m3_lightmeter_iap.dart
Normal file
34
iap/lib/m3_lightmeter_iap.dart
Normal file
|
@ -0,0 +1,34 @@
|
|||
library m3_lightmeter_iap;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:m3_lightmeter_iap/src/providers/equipment_profile_provider.dart';
|
||||
import 'package:m3_lightmeter_iap/src/providers/films_provider.dart';
|
||||
import 'package:m3_lightmeter_iap/src/providers/iap_products_provider.dart';
|
||||
|
||||
export 'src/data/models/iap_product.dart';
|
||||
|
||||
export 'src/providers/equipment_profile_provider.dart';
|
||||
export 'src/providers/films_provider.dart';
|
||||
export 'src/providers/iap_products_provider.dart';
|
||||
|
||||
class IAPProviders extends StatelessWidget {
|
||||
final Object sharedPreferences;
|
||||
final Widget child;
|
||||
|
||||
const IAPProviders({
|
||||
required this.sharedPreferences,
|
||||
required this.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IAPProductsProvider(
|
||||
child: FilmsProvider(
|
||||
child: EquipmentProfileProvider(
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
31
iap/lib/src/data/models/iap_product.dart
Normal file
31
iap/lib/src/data/models/iap_product.dart
Normal file
|
@ -0,0 +1,31 @@
|
|||
enum IAPProductStatus {
|
||||
purchasable,
|
||||
pending,
|
||||
purchased,
|
||||
}
|
||||
|
||||
enum IAPProductType { paidFeatures }
|
||||
|
||||
class IAPProduct {
|
||||
final String storeId;
|
||||
final IAPProductStatus status;
|
||||
|
||||
const IAPProduct({
|
||||
required this.storeId,
|
||||
this.status = IAPProductStatus.purchasable,
|
||||
});
|
||||
|
||||
IAPProduct copyWith({IAPProductStatus? status}) => IAPProduct(
|
||||
storeId: storeId,
|
||||
status: status ?? this.status,
|
||||
);
|
||||
}
|
||||
|
||||
extension IAPProductTypeExtension on IAPProductType {
|
||||
String get storeId {
|
||||
switch (this) {
|
||||
case IAPProductType.paidFeatures:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
61
iap/lib/src/providers/equipment_profile_provider.dart
Normal file
61
iap/lib/src/providers/equipment_profile_provider.dart
Normal file
|
@ -0,0 +1,61 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:m3_lightmeter_iap/src/providers/selectable_provider.dart';
|
||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||
|
||||
class EquipmentProfileProvider extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
const EquipmentProfileProvider({required this.child, super.key});
|
||||
|
||||
static EquipmentProfileProviderState of(BuildContext context) {
|
||||
return context.findAncestorStateOfType<EquipmentProfileProviderState>()!;
|
||||
}
|
||||
|
||||
@override
|
||||
State<EquipmentProfileProvider> createState() => EquipmentProfileProviderState();
|
||||
}
|
||||
|
||||
class EquipmentProfileProviderState extends State<EquipmentProfileProvider> {
|
||||
static const EquipmentProfile _defaultProfile = EquipmentProfile(
|
||||
id: '',
|
||||
name: '',
|
||||
apertureValues: ApertureValue.values,
|
||||
ndValues: NdValue.values,
|
||||
shutterSpeedValues: ShutterSpeedValue.values,
|
||||
isoValues: IsoValue.values,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return EquipmentProfiles(
|
||||
values: const [_defaultProfile],
|
||||
selected: _defaultProfile,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
void setProfile(EquipmentProfile data) {}
|
||||
|
||||
void addProfile(String name, [EquipmentProfile? copyFrom]) {}
|
||||
|
||||
void updateProdile(EquipmentProfile data) {}
|
||||
|
||||
void deleteProfile(EquipmentProfile data) {}
|
||||
}
|
||||
|
||||
class EquipmentProfiles extends SelectableInheritedModel<EquipmentProfile> {
|
||||
const EquipmentProfiles({
|
||||
super.key,
|
||||
required super.values,
|
||||
required super.selected,
|
||||
required super.child,
|
||||
});
|
||||
|
||||
static List<EquipmentProfile> of(BuildContext context) {
|
||||
return InheritedModel.inheritFrom<EquipmentProfiles>(context, aspect: SelectableAspect.list)!.values;
|
||||
}
|
||||
|
||||
static EquipmentProfile selectedOf(BuildContext context) {
|
||||
return InheritedModel.inheritFrom<EquipmentProfiles>(context, aspect: SelectableAspect.selected)!.selected;
|
||||
}
|
||||
}
|
65
iap/lib/src/providers/films_provider.dart
Normal file
65
iap/lib/src/providers/films_provider.dart
Normal file
|
@ -0,0 +1,65 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:m3_lightmeter_iap/src/providers/selectable_provider.dart';
|
||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||
|
||||
class FilmsProvider extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
const FilmsProvider({
|
||||
required this.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
static FilmsProviderState of(BuildContext context) {
|
||||
return context.findAncestorStateOfType<FilmsProviderState>()!;
|
||||
}
|
||||
|
||||
@override
|
||||
State<FilmsProvider> createState() => FilmsProviderState();
|
||||
}
|
||||
|
||||
class FilmsProviderState extends State<FilmsProvider> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Films(
|
||||
values: const [Film.other()],
|
||||
filmsInUse: const [Film.other()],
|
||||
selected: const Film.other(),
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
void setFilm(Film film) {}
|
||||
|
||||
void saveFilms(List<Film> films) {}
|
||||
}
|
||||
|
||||
class Films extends SelectableInheritedModel<Film> {
|
||||
final List<Film> filmsInUse;
|
||||
|
||||
const Films({
|
||||
super.key,
|
||||
required super.values,
|
||||
required this.filmsInUse,
|
||||
required super.selected,
|
||||
required super.child,
|
||||
});
|
||||
|
||||
/// [Film.other()] + all the custom fields with actual reciprocity formulas
|
||||
static List<Film> of(BuildContext context) {
|
||||
return InheritedModel.inheritFrom<Films>(context)!.values;
|
||||
}
|
||||
|
||||
/// [Film.other()] + films in use selected by user
|
||||
static List<Film> inUseOf<T>(BuildContext context) {
|
||||
return InheritedModel.inheritFrom<Films>(
|
||||
context,
|
||||
aspect: SelectableAspect.list,
|
||||
)!
|
||||
.filmsInUse;
|
||||
}
|
||||
|
||||
static Film selectedOf(BuildContext context) {
|
||||
return InheritedModel.inheritFrom<Films>(context, aspect: SelectableAspect.selected)!.selected;
|
||||
}
|
||||
}
|
67
iap/lib/src/providers/iap_products_provider.dart
Normal file
67
iap/lib/src/providers/iap_products_provider.dart
Normal file
|
@ -0,0 +1,67 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:m3_lightmeter_iap/src/data/models/iap_product.dart';
|
||||
|
||||
class IAPProductsProvider extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
const IAPProductsProvider({required this.child, super.key});
|
||||
|
||||
static IAPProductsProviderState of(BuildContext context) {
|
||||
return context.findAncestorStateOfType<IAPProductsProviderState>()!;
|
||||
}
|
||||
|
||||
@override
|
||||
State<IAPProductsProvider> createState() => IAPProductsProviderState();
|
||||
}
|
||||
|
||||
class IAPProductsProviderState extends State<IAPProductsProvider> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IAPProducts(
|
||||
products: [
|
||||
IAPProduct(
|
||||
storeId: IAPProductType.paidFeatures.storeId,
|
||||
status: IAPProductStatus.purchased,
|
||||
)
|
||||
],
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> buy(IAPProductType type) async {}
|
||||
}
|
||||
|
||||
class IAPProducts extends InheritedModel<IAPProductType> {
|
||||
final List<IAPProduct> products;
|
||||
|
||||
const IAPProducts({
|
||||
required this.products,
|
||||
required super.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
static IAPProduct? productOf(BuildContext context, IAPProductType type) {
|
||||
final IAPProducts? result = InheritedModel.inheritFrom<IAPProducts>(context, aspect: type);
|
||||
return result!._findProduct(type);
|
||||
}
|
||||
|
||||
static bool isPurchased(BuildContext context, IAPProductType type) {
|
||||
final IAPProducts? result = InheritedModel.inheritFrom<IAPProducts>(context, aspect: type);
|
||||
return result!._findProduct(type)?.status == IAPProductStatus.purchased;
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(IAPProducts oldWidget) => false;
|
||||
|
||||
@override
|
||||
bool updateShouldNotifyDependent(IAPProducts oldWidget, Set<IAPProductType> dependencies) =>
|
||||
false;
|
||||
|
||||
IAPProduct? _findProduct(IAPProductType type) {
|
||||
try {
|
||||
return products.firstWhere((element) => element.storeId == type.storeId);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
29
iap/lib/src/providers/selectable_provider.dart
Normal file
29
iap/lib/src/providers/selectable_provider.dart
Normal file
|
@ -0,0 +1,29 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
enum SelectableAspect { list, selected }
|
||||
|
||||
class SelectableInheritedModel<T> extends InheritedModel<SelectableAspect> {
|
||||
const SelectableInheritedModel({
|
||||
super.key,
|
||||
required this.values,
|
||||
required this.selected,
|
||||
required super.child,
|
||||
});
|
||||
|
||||
final List<T> values;
|
||||
final T selected;
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(SelectableInheritedModel oldWidget) => true;
|
||||
|
||||
@override
|
||||
bool updateShouldNotifyDependent(SelectableInheritedModel oldWidget, Set<SelectableAspect> dependencies) {
|
||||
if (dependencies.contains(SelectableAspect.list)) {
|
||||
return true;
|
||||
} else if (dependencies.contains(SelectableAspect.selected)) {
|
||||
return selected != oldWidget.selected;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
25
iap/pubspec.yaml
Normal file
25
iap/pubspec.yaml
Normal file
|
@ -0,0 +1,25 @@
|
|||
name: m3_lightmeter_iap
|
||||
description: IAP stubs for the M3 Lightmeter app.
|
||||
version: 0.2.0
|
||||
publish_to: 'none'
|
||||
|
||||
environment:
|
||||
sdk: '>=2.19.2 <3.0.0'
|
||||
flutter: ">=1.17.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
m3_lightmeter_resources:
|
||||
git:
|
||||
url: "https://github.com/vodemn/m3_lightmeter_resources"
|
||||
ref: main
|
||||
shared_preferences: 2.2.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^2.0.0
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
294
integration_test/generate_screenshots.dart
Normal file
294
integration_test/generate_screenshots.dart
Normal file
|
@ -0,0 +1,294 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:lightmeter/application.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/ev_source_type.dart';
|
||||
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
|
||||
import 'package:lightmeter/data/models/supported_locale.dart';
|
||||
import 'package:lightmeter/data/models/theme_type.dart';
|
||||
import 'package:lightmeter/data/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/environment.dart';
|
||||
import 'package:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/providers/services_provider.dart';
|
||||
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
||||
import 'package:lightmeter/res/dimens.dart';
|
||||
import 'package:lightmeter/res/theme.dart';
|
||||
import 'package:lightmeter/screens/metering/components/bottom_controls/components/measure_button/widget_button_measure.dart';
|
||||
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/iso_picker/widget_picker_iso.dart';
|
||||
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/dialog_picker/widget_picker_dialog.dart';
|
||||
import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart';
|
||||
import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart';
|
||||
import 'package:lightmeter/screens/settings/screen_settings.dart';
|
||||
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class _MockSharedPreferences extends Mock implements SharedPreferences {}
|
||||
|
||||
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 {}
|
||||
|
||||
//https://stackoverflow.com/a/67186625/13167574
|
||||
void main() {
|
||||
late _MockUserPreferencesService mockUserPreferencesService;
|
||||
late _MockCaffeineService mockCaffeineService;
|
||||
late _MockHapticsService mockHapticsService;
|
||||
late _MockPermissionsService mockPermissionsService;
|
||||
late _MockLightSensorService mockLightSensorService;
|
||||
late _MockVolumeEventsService mockVolumeEventsService;
|
||||
|
||||
final binding = IntegrationTestWidgetsFlutterBinding();
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUpAll(() {
|
||||
mockUserPreferencesService = _MockUserPreferencesService();
|
||||
when(() => mockUserPreferencesService.evSourceType).thenReturn(EvSourceType.camera);
|
||||
when(() => mockUserPreferencesService.stopType).thenReturn(StopType.third);
|
||||
when(() => mockUserPreferencesService.locale).thenReturn(SupportedLocale.en);
|
||||
when(() => mockUserPreferencesService.caffeine).thenReturn(true);
|
||||
when(() => mockUserPreferencesService.volumeAction).thenReturn(VolumeAction.shutter);
|
||||
when(() => mockUserPreferencesService.cameraEvCalibration).thenReturn(0.0);
|
||||
when(() => mockUserPreferencesService.lightSensorEvCalibration).thenReturn(0.0);
|
||||
when(() => mockUserPreferencesService.iso).thenReturn(const IsoValue(400, StopType.full));
|
||||
when(() => mockUserPreferencesService.ndFilter).thenReturn(NdValue.values.first);
|
||||
when(() => mockUserPreferencesService.haptics).thenReturn(true);
|
||||
when(() => mockUserPreferencesService.meteringScreenLayout).thenReturn({
|
||||
MeteringScreenLayoutFeature.equipmentProfiles: true,
|
||||
MeteringScreenLayoutFeature.extremeExposurePairs: true,
|
||||
MeteringScreenLayoutFeature.filmPicker: true,
|
||||
MeteringScreenLayoutFeature.histogram: false,
|
||||
});
|
||||
when(() => mockUserPreferencesService.themeType).thenReturn(ThemeType.light);
|
||||
when(() => mockUserPreferencesService.dynamicColor).thenReturn(false);
|
||||
|
||||
mockCaffeineService = _MockCaffeineService();
|
||||
when(() => mockCaffeineService.isKeepScreenOn()).thenAnswer((_) async => false);
|
||||
when(() => mockCaffeineService.keepScreenOn(true)).thenAnswer((_) async => true);
|
||||
when(() => mockCaffeineService.keepScreenOn(false)).thenAnswer((_) async => false);
|
||||
|
||||
mockHapticsService = _MockHapticsService();
|
||||
when(() => mockHapticsService.quickVibration()).thenAnswer((_) async {});
|
||||
when(() => mockHapticsService.responseVibration()).thenAnswer((_) async {});
|
||||
when(() => mockHapticsService.errorVibration()).thenAnswer((_) async {});
|
||||
|
||||
mockPermissionsService = _MockPermissionsService();
|
||||
when(() => mockPermissionsService.requestCameraPermission())
|
||||
.thenAnswer((_) async => PermissionStatus.granted);
|
||||
when(() => mockPermissionsService.checkCameraPermission())
|
||||
.thenAnswer((_) async => PermissionStatus.granted);
|
||||
|
||||
mockLightSensorService = _MockLightSensorService();
|
||||
when(() => mockLightSensorService.hasSensor()).thenAnswer((_) async => true);
|
||||
when(() => mockLightSensorService.luxStream()).thenAnswer((_) => Stream.fromIterable([100]));
|
||||
|
||||
mockVolumeEventsService = _MockVolumeEventsService();
|
||||
when(() => mockVolumeEventsService.setVolumeHandling(true)).thenAnswer((_) async => true);
|
||||
when(() => mockVolumeEventsService.setVolumeHandling(false)).thenAnswer((_) async => false);
|
||||
when(() => mockVolumeEventsService.volumeButtonsEventStream())
|
||||
.thenAnswer((_) => const Stream<int>.empty());
|
||||
|
||||
when(() => mockHapticsService.quickVibration()).thenAnswer((_) async {});
|
||||
when(() => mockHapticsService.responseVibration()).thenAnswer((_) async {});
|
||||
});
|
||||
|
||||
Future<void> pumpApplication(WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
IAPProviders(
|
||||
sharedPreferences: _MockSharedPreferences(),
|
||||
child: EquipmentProfiles(
|
||||
selected: _mockEquipmentProfiles[0],
|
||||
values: _mockEquipmentProfiles,
|
||||
child: Films(
|
||||
selected: const Film('Ilford HP5+', 400),
|
||||
values: const [Film.other(), Film('Ilford HP5+', 400)],
|
||||
filmsInUse: const [Film.other(), Film('Ilford HP5+', 400)],
|
||||
child: ServicesProvider(
|
||||
environment: const Environment.prod().copyWith(hasLightSensor: true),
|
||||
userPreferencesService: mockUserPreferencesService,
|
||||
caffeineService: mockCaffeineService,
|
||||
hapticsService: mockHapticsService,
|
||||
permissionsService: mockPermissionsService,
|
||||
lightSensorService: mockLightSensorService,
|
||||
volumeEventsService: mockVolumeEventsService,
|
||||
child: const UserPreferencesProvider(
|
||||
child: Application(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
/// Generates several screenshots with the light theme
|
||||
/// and the additionally the first one with the dark theme
|
||||
void generateScreenshots(Color color) {
|
||||
testWidgets('${color.value}_light', (tester) async {
|
||||
when(() => mockUserPreferencesService.themeType).thenReturn(ThemeType.light);
|
||||
when(() => mockUserPreferencesService.primaryColor).thenReturn(color);
|
||||
await pumpApplication(tester);
|
||||
|
||||
await tester.takePhoto();
|
||||
await tester.takeScreenshot(binding, '${color.value}_metering_reflected');
|
||||
|
||||
await tester.tap(find.byTooltip(S.current.tooltipUseLightSensor));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byType(MeteringMeasureButton));
|
||||
await tester.tap(find.byType(MeteringMeasureButton));
|
||||
await tester.takeScreenshot(binding, '${color.value}_metering_incident');
|
||||
|
||||
expect(find.byType(IsoValuePicker), findsOneWidget);
|
||||
await tester.tap(find.byType(IsoValuePicker));
|
||||
await tester.pumpAndSettle(Dimens.durationL);
|
||||
expect(find.byType(DialogPicker<IsoValue>), findsOneWidget);
|
||||
await tester.takeScreenshot(binding, '${color.value}_metering_iso_picker');
|
||||
|
||||
await tester.tapCancelButton();
|
||||
expect(find.byType(DialogPicker<IsoValue>), findsNothing);
|
||||
expect(find.byTooltip(S.current.tooltipOpenSettings), findsOneWidget);
|
||||
await tester.tap(find.byTooltip(S.current.tooltipOpenSettings));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(SettingsScreen), findsOneWidget);
|
||||
await tester.takeScreenshot(binding, '${color.value}_settings');
|
||||
|
||||
await tester.tapListTile(S.current.meteringScreenLayout);
|
||||
await tester.takeScreenshot(binding, '${color.value}_settings_metering_screen_layout');
|
||||
|
||||
await tester.tapCancelButton();
|
||||
await tester.tapListTile(S.current.equipmentProfiles);
|
||||
expect(find.byType(EquipmentProfilesScreen), findsOneWidget);
|
||||
await tester.tap(find.byType(EquipmentProfileContainer).first);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.takeScreenshot(binding, '${color.value}-equipment_profiles');
|
||||
|
||||
await tester.tap(find.byIcon(Icons.iso).first);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.takeScreenshot(binding, '${color.value}_equipment_profiles_iso_picker');
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'${color.value}_dark',
|
||||
(tester) async {
|
||||
when(() => mockUserPreferencesService.themeType).thenReturn(ThemeType.dark);
|
||||
when(() => mockUserPreferencesService.primaryColor).thenReturn(color);
|
||||
await pumpApplication(tester);
|
||||
|
||||
await tester.takePhoto();
|
||||
await tester.takeScreenshot(binding, '${color.value}_metering_reflected_dark');
|
||||
|
||||
await tester.tap(find.byTooltip(S.current.tooltipUseLightSensor));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byType(MeteringMeasureButton));
|
||||
await tester.tap(find.byType(MeteringMeasureButton));
|
||||
await tester.takeScreenshot(binding, '${color.value}_metering_incident_dark');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
generateScreenshots(primaryColorsList[5]);
|
||||
generateScreenshots(primaryColorsList[3]);
|
||||
generateScreenshots(primaryColorsList[9]);
|
||||
}
|
||||
|
||||
extension on WidgetTester {
|
||||
Future<void> takeScreenshot(IntegrationTestWidgetsFlutterBinding binding, String name) async {
|
||||
if (Platform.isAndroid) {
|
||||
await binding.convertFlutterSurfaceToImage();
|
||||
await pumpAndSettle();
|
||||
}
|
||||
await binding.takeScreenshot(name);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> takePhoto() async {
|
||||
await tap(find.byType(MeteringMeasureButton));
|
||||
await pump(const Duration(seconds: 2)); // wait for circular progress indicator
|
||||
await pump(const Duration(seconds: 1)); // wait for circular progress indicator
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> tapCancelButton() async {
|
||||
final cancelButton = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is TextButton &&
|
||||
widget.child is Text &&
|
||||
(widget.child as Text?)?.data == S.current.cancel,
|
||||
);
|
||||
expect(cancelButton, findsOneWidget);
|
||||
await tap(cancelButton);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> tapListTile(String title) async {
|
||||
final listTile = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is ListTile && widget.title is Text && (widget.title as Text?)?.data == title,
|
||||
);
|
||||
expect(listTile, findsOneWidget);
|
||||
await tap(listTile);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
}
|
||||
|
||||
final _mockEquipmentProfiles = [
|
||||
const EquipmentProfile(
|
||||
id: '',
|
||||
name: '',
|
||||
apertureValues: ApertureValue.values,
|
||||
ndValues: NdValue.values,
|
||||
shutterSpeedValues: ShutterSpeedValue.values,
|
||||
isoValues: IsoValue.values,
|
||||
),
|
||||
EquipmentProfile(
|
||||
id: '1',
|
||||
name: 'Praktica + Zenitar',
|
||||
apertureValues: ApertureValue.values.sublist(
|
||||
ApertureValue.values.indexOf(const ApertureValue(1.7, StopType.half)),
|
||||
ApertureValue.values.indexOf(const ApertureValue(16, StopType.full)) + 1,
|
||||
),
|
||||
ndValues: NdValue.values.sublist(0, 3),
|
||||
shutterSpeedValues: ShutterSpeedValue.values.sublist(
|
||||
ShutterSpeedValue.values.indexOf(const ShutterSpeedValue(1000, true, StopType.full)),
|
||||
ShutterSpeedValue.values.indexOf(const ShutterSpeedValue(16, false, StopType.full)) + 1,
|
||||
),
|
||||
isoValues: const [
|
||||
IsoValue(50, StopType.full),
|
||||
IsoValue(100, StopType.full),
|
||||
IsoValue(200, StopType.full),
|
||||
IsoValue(250, StopType.third),
|
||||
IsoValue(400, StopType.full),
|
||||
IsoValue(500, StopType.third),
|
||||
IsoValue(800, StopType.full),
|
||||
IsoValue(1600, StopType.full),
|
||||
IsoValue(3200, StopType.full),
|
||||
],
|
||||
),
|
||||
const EquipmentProfile(
|
||||
id: '2',
|
||||
name: 'Praktica + Jupiter',
|
||||
apertureValues: ApertureValue.values,
|
||||
ndValues: NdValue.values,
|
||||
shutterSpeedValues: ShutterSpeedValue.values,
|
||||
isoValues: IsoValue.values,
|
||||
),
|
||||
];
|
10
integration_test/generate_screenshots.sh
Normal file
10
integration_test/generate_screenshots.sh
Normal file
|
@ -0,0 +1,10 @@
|
|||
flutter drive \
|
||||
--dart-define="cameraPreviewAspectRatio=240/320" \
|
||||
--dart-define="cameraStubImage=assets/camera_stub_image.jpg" \
|
||||
--driver=test_driver/screenshot_driver.dart \
|
||||
--target=integration_test/generate_screenshots.dart \
|
||||
--profile \
|
||||
--flavor=dev \
|
||||
--no-dds \
|
||||
--endless-trace-buffer \
|
||||
--purge-persistent-cache
|
|
@ -3,7 +3,7 @@
|
|||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 51;
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
|
@ -237,6 +237,7 @@
|
|||
};
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
|
@ -268,6 +269,7 @@
|
|||
};
|
||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
|
@ -371,7 +373,7 @@
|
|||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 74JQ9DBXY6;
|
||||
DEVELOPMENT_TEAM = 489Z6UQMGN;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
@ -500,7 +502,7 @@
|
|||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 74JQ9DBXY6;
|
||||
DEVELOPMENT_TEAM = 489Z6UQMGN;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
@ -523,7 +525,7 @@
|
|||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 74JQ9DBXY6;
|
||||
DEVELOPMENT_TEAM = 489Z6UQMGN;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
@ -600,7 +602,7 @@
|
|||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 74JQ9DBXY6;
|
||||
DEVELOPMENT_TEAM = 489Z6UQMGN;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
@ -675,7 +677,7 @@
|
|||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 74JQ9DBXY6;
|
||||
DEVELOPMENT_TEAM = 489Z6UQMGN;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
@ -747,7 +749,7 @@
|
|||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 74JQ9DBXY6;
|
||||
DEVELOPMENT_TEAM = 489Z6UQMGN;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
|
|
@ -2,60 +2,18 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:lightmeter/data/models/supported_locale.dart';
|
||||
import 'package:lightmeter/environment.dart';
|
||||
import 'package:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/providers.dart';
|
||||
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
||||
import 'package:lightmeter/screens/metering/flow_metering.dart';
|
||||
import 'package:lightmeter/screens/settings/flow_settings.dart';
|
||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||
|
||||
class Application extends StatelessWidget {
|
||||
final Environment env;
|
||||
|
||||
const Application(this.env, {super.key});
|
||||
const Application({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LightmeterProviders(
|
||||
env: env,
|
||||
builder: (context, ready) => ready
|
||||
? _AnnotatedRegionWrapper(
|
||||
child: MaterialApp(
|
||||
theme: context.listen<ThemeData>(),
|
||||
locale: Locale(context.listen<SupportedLocale>().intlName),
|
||||
localizationsDelegates: const [
|
||||
S.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: S.delegate.supportedLocales,
|
||||
builder: (context, child) => MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
|
||||
child: child!,
|
||||
),
|
||||
initialRoute: "metering",
|
||||
routes: {
|
||||
"metering": (context) => const MeteringFlow(),
|
||||
"settings": (context) => const SettingsFlow(),
|
||||
},
|
||||
),
|
||||
)
|
||||
: const SizedBox(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AnnotatedRegionWrapper extends StatelessWidget {
|
||||
final Widget child;
|
||||
|
||||
const _AnnotatedRegionWrapper({required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final systemIconsBrightness = ThemeData.estimateBrightnessForColor(
|
||||
context.listen<ThemeData>().colorScheme.onSurface,
|
||||
);
|
||||
final theme = UserPreferencesProvider.themeOf(context);
|
||||
final systemIconsBrightness = ThemeData.estimateBrightnessForColor(theme.colorScheme.onSurface);
|
||||
return AnnotatedRegion(
|
||||
value: SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
|
@ -65,7 +23,26 @@ class _AnnotatedRegionWrapper extends StatelessWidget {
|
|||
systemNavigationBarColor: Colors.transparent,
|
||||
systemNavigationBarIconBrightness: systemIconsBrightness,
|
||||
),
|
||||
child: child,
|
||||
child: MaterialApp(
|
||||
theme: theme,
|
||||
locale: Locale(UserPreferencesProvider.localeOf(context).intlName),
|
||||
localizationsDelegates: const [
|
||||
S.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: S.delegate.supportedLocales,
|
||||
builder: (context, child) => MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
|
||||
child: child!,
|
||||
),
|
||||
initialRoute: "metering",
|
||||
routes: {
|
||||
"metering": (context) => const MeteringFlow(),
|
||||
"settings": (context) => const SettingsFlow(),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
55
lib/application_wrapper.dart
Normal file
55
lib/application_wrapper.dart
Normal file
|
@ -0,0 +1,55 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/data/caffeine_service.dart';
|
||||
import 'package:lightmeter/data/haptics_service.dart';
|
||||
import 'package:lightmeter/data/light_sensor_service.dart';
|
||||
import 'package:lightmeter/data/permissions_service.dart';
|
||||
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||
import 'package:lightmeter/data/volume_events_service.dart';
|
||||
import 'package:lightmeter/environment.dart';
|
||||
import 'package:lightmeter/providers/services_provider.dart';
|
||||
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
||||
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
||||
import 'package:platform/platform.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class ApplicationWrapper extends StatelessWidget {
|
||||
final Environment env;
|
||||
final Widget child;
|
||||
|
||||
const ApplicationWrapper(this.env, {required this.child, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder(
|
||||
future: Future.wait([
|
||||
SharedPreferences.getInstance(),
|
||||
const LightSensorService(LocalPlatform()).hasSensor(),
|
||||
]),
|
||||
builder: (_, snapshot) {
|
||||
if (snapshot.data != null) {
|
||||
return IAPProviders(
|
||||
sharedPreferences: snapshot.data![0] as SharedPreferences,
|
||||
child: ServicesProvider(
|
||||
caffeineService: const CaffeineService(),
|
||||
environment: env.copyWith(hasLightSensor: snapshot.data![1] as bool),
|
||||
hapticsService: const HapticsService(),
|
||||
lightSensorService: const LightSensorService(LocalPlatform()),
|
||||
permissionsService: const PermissionsService(),
|
||||
userPreferencesService:
|
||||
UserPreferencesService(snapshot.data![0] as SharedPreferences),
|
||||
volumeEventsService: const VolumeEventsService(LocalPlatform()),
|
||||
child: UserPreferencesProvider(
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (snapshot.error != null) {
|
||||
return Center(child: Text(snapshot.error!.toString()));
|
||||
}
|
||||
|
||||
// TODO(@vodemn): maybe user splashscreen instead
|
||||
return const SizedBox();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -8,4 +8,16 @@ class ExposurePair {
|
|||
|
||||
@override
|
||||
String toString() => '$aperture - $shutterSpeed';
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other.runtimeType != runtimeType) return false;
|
||||
return other is ExposurePair &&
|
||||
other.aperture == aperture &&
|
||||
other.shutterSpeed == shutterSpeed;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(aperture, shutterSpeed, runtimeType);
|
||||
}
|
||||
|
|
|
@ -1,233 +0,0 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||
|
||||
double log10(double x) => log(x) / log(10);
|
||||
|
||||
double log10polynomian(
|
||||
double x,
|
||||
double a,
|
||||
double b,
|
||||
double c,
|
||||
) =>
|
||||
a * pow(log10(x), 2) + b * log10(x) + c;
|
||||
|
||||
/// Only Ilford films have reciprocity formulas provided by the manufacturer:
|
||||
/// https://www.ilfordphoto.com/wp/wp-content/uploads/2017/06/Reciprocity-Failure-Compensation.pdf
|
||||
///
|
||||
/// Reciprocity formulas for Fomapan films and Kodak films are from here:
|
||||
/// https://www.flickr.com/groups/86738082@N00/discuss/72157626050157470/
|
||||
///
|
||||
/// Cinema films like Kodak 5222/7222 Double-X and respective CineStill films (cause they are basically a modification of Kodak)
|
||||
/// do not have any reciprocity failure information, as these films are ment to be used in cinema
|
||||
/// with appropriate light and pretty short shutter speeds.
|
||||
///
|
||||
/// Because of this: https://github.com/dart-lang/sdk/issues/38934#issuecomment-803938315
|
||||
/// `super` calls are ignored in test coverage
|
||||
class Film {
|
||||
final String name;
|
||||
final int iso;
|
||||
|
||||
const Film(this.name, this.iso);
|
||||
|
||||
const Film.other()
|
||||
: name = '',
|
||||
iso = 0;
|
||||
|
||||
@override
|
||||
String toString() => name;
|
||||
|
||||
ShutterSpeedValue reciprocityFailure(ShutterSpeedValue shutterSpeed) {
|
||||
if (shutterSpeed.isFraction) {
|
||||
return shutterSpeed;
|
||||
} else {
|
||||
return ShutterSpeedValue(
|
||||
reciprocityFormula(shutterSpeed.rawValue),
|
||||
shutterSpeed.isFraction,
|
||||
shutterSpeed.stopType,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@protected
|
||||
double reciprocityFormula(double t) => t;
|
||||
|
||||
static const List<Film> values = [
|
||||
Film.other(),
|
||||
FomapanFilm.creative100(),
|
||||
FomapanFilm.creative200(),
|
||||
FomapanFilm.action400(),
|
||||
IlfordFilm.ortho(),
|
||||
IlfordFilm.delta100(),
|
||||
IlfordFilm.delta400(),
|
||||
IlfordFilm.delta3200(),
|
||||
IlfordFilm.fp4(),
|
||||
IlfordFilm.hp5(),
|
||||
IlfordFilm.panf(),
|
||||
IlfordFilm.sfx200(),
|
||||
IlfordFilm.xp2super(),
|
||||
IlfordFilm.pan100(),
|
||||
IlfordFilm.pan400(),
|
||||
KodakFilm.tmax100(),
|
||||
KodakFilm.tmax400(),
|
||||
KodakFilm.tmax3200(),
|
||||
KodakFilm.trix320(),
|
||||
KodakFilm.trix400(),
|
||||
];
|
||||
}
|
||||
|
||||
/// https://www.tate.org.uk/documents/598/page_6_7_agfa_stocks_0.pdf
|
||||
/// https://www.filmwasters.com/forum/index.php?topic=5298.0
|
||||
// {{1,1.87},{2,3.73},{3,8.06},{4,13.93},{5,21.28},{6,23.00},{7,30.12},{8,38.05},{9,44.75},{10,50.12},{20,117},{30,202},{40,293},{50,413},{60,547},{70,694},{80,853},{90,1022},{100,1202}};
|
||||
// class AgfaFilm extends Film {
|
||||
// final double a;
|
||||
// final double b;
|
||||
// final double c;
|
||||
|
||||
// const AgfaFilm.apx100()
|
||||
// : a = 1,
|
||||
// b = 5,
|
||||
// c = 2,
|
||||
// super('Agfa APX 100', 100); // coverage:ignore-line
|
||||
|
||||
// const AgfaFilm.apx400()
|
||||
// : a = 1.5,
|
||||
// b = 4.5,
|
||||
// c = 3,
|
||||
// super('Agfa APX 400', 400); // coverage:ignore-line
|
||||
|
||||
// @override
|
||||
// double reciprocityFormula(double t) => t * log10polynomian(t, a, b, c);
|
||||
// }
|
||||
|
||||
class FomapanFilm extends Film {
|
||||
final double a;
|
||||
final double b;
|
||||
final double c;
|
||||
|
||||
/// https://www.foma.cz/en/fomapan-100
|
||||
const FomapanFilm.creative100()
|
||||
: a = 1,
|
||||
b = 5,
|
||||
c = 2,
|
||||
super('Fomapan CREATIVE 100', 100); // coverage:ignore-line
|
||||
|
||||
/// https://www.foma.cz/en/fomapan-200
|
||||
const FomapanFilm.creative200()
|
||||
: a = 1.5,
|
||||
b = 4.5,
|
||||
c = 3,
|
||||
super('Fomapan CREATIVE 200', 200); // coverage:ignore-line
|
||||
|
||||
/// https://www.foma.cz/en/fomapan-100
|
||||
const FomapanFilm.action400()
|
||||
: a = -1.25, // coverage:ignore-line
|
||||
b = 5.75,
|
||||
c = 1.5,
|
||||
super('Fomapan ACTION 400', 400); // coverage:ignore-line
|
||||
|
||||
@override
|
||||
double reciprocityFormula(double t) => t * log10polynomian(t, a, b, c);
|
||||
}
|
||||
|
||||
class IlfordFilm extends Film {
|
||||
final double reciprocityPower;
|
||||
|
||||
/// https://www.ilfordphoto.com/amfile/file/download/file/1948/product/1650/
|
||||
const IlfordFilm.ortho()
|
||||
: reciprocityPower = 1.25,
|
||||
super('Ilford ORTHO+', 80); // coverage:ignore-line
|
||||
|
||||
/// https://www.ilfordphoto.com/amfile/file/download/file/1919/product/686/
|
||||
const IlfordFilm.fp4()
|
||||
: reciprocityPower = 1.26,
|
||||
super('Ilford FP4+', 125); // coverage:ignore-line
|
||||
|
||||
/// https://www.ilfordphoto.com/amfile/file/download/file/1903/product/691/
|
||||
const IlfordFilm.hp5()
|
||||
: reciprocityPower = 1.31,
|
||||
super('Ilford HP5+', 400); // coverage:ignore-line
|
||||
|
||||
/// https://www.ilfordphoto.com/amfile/file/download/file/3/product/679/
|
||||
const IlfordFilm.delta100()
|
||||
: reciprocityPower = 1.26,
|
||||
super('Ilford DELTA 100', 100); // coverage:ignore-line
|
||||
|
||||
/// https://www.ilfordphoto.com/amfile/file/download/file/1915/product/684/
|
||||
const IlfordFilm.delta400()
|
||||
: reciprocityPower = 1.41,
|
||||
super('Ilford DELTA 400', 400); // coverage:ignore-line
|
||||
|
||||
/// https://www.ilfordphoto.com/amfile/file/download/file/1913/product/682/
|
||||
const IlfordFilm.delta3200()
|
||||
: reciprocityPower = 1.33,
|
||||
super('Ilford DELTA 3200', 3200); // coverage:ignore-line
|
||||
|
||||
/// https://www.ilfordphoto.com/amfile/file/download/file/1905/product/699/
|
||||
const IlfordFilm.panf()
|
||||
: reciprocityPower = 1.33,
|
||||
super('Ilford Pan F+', 50); // coverage:ignore-line
|
||||
|
||||
/// https://www.ilfordphoto.com/amfile/file/download/file/1907/product/701/
|
||||
const IlfordFilm.sfx200()
|
||||
: reciprocityPower = 1.31,
|
||||
super('Ilford SFX 200', 200); // coverage:ignore-line
|
||||
|
||||
/// https://www.ilfordphoto.com/amfile/file/download/file/1909/product/703/
|
||||
const IlfordFilm.xp2super()
|
||||
: reciprocityPower = 1.31,
|
||||
super('Ilford XP2 SUPER', 400); // coverage:ignore-line
|
||||
|
||||
/// https://www.ilfordphoto.com/amfile/file/download/file/1958/product/696/
|
||||
const IlfordFilm.pan100()
|
||||
: reciprocityPower = 1.26,
|
||||
super('Kentemere 100', 100); // coverage:ignore-line
|
||||
|
||||
/// https://www.ilfordphoto.com/amfile/file/download/file/1959/product/697/
|
||||
const IlfordFilm.pan400()
|
||||
: reciprocityPower = 1.30,
|
||||
super('Kentemere 400', 400); // coverage:ignore-line
|
||||
|
||||
@override
|
||||
double reciprocityFormula(double t) => pow(t, reciprocityPower).toDouble();
|
||||
}
|
||||
|
||||
class KodakFilm extends Film {
|
||||
final double a;
|
||||
final double b;
|
||||
final double c;
|
||||
|
||||
const KodakFilm.tmax100()
|
||||
: a = 1 / 6, // coverage:ignore-line
|
||||
b = 0, // coverage:ignore-line
|
||||
c = 4 / 3, // coverage:ignore-line
|
||||
super('Kodak T-MAX 100', 100); // coverage:ignore-line
|
||||
|
||||
const KodakFilm.tmax400()
|
||||
: a = 2 / 3, // coverage:ignore-line
|
||||
b = -1 / 2, // coverage:ignore-line
|
||||
c = 4 / 3, // coverage:ignore-line
|
||||
super('Kodak T-MAX 400', 400); // coverage:ignore-line
|
||||
|
||||
const KodakFilm.tmax3200()
|
||||
: a = 7 / 6, // coverage:ignore-line
|
||||
b = -1, // coverage:ignore-line
|
||||
c = 4 / 3, // coverage:ignore-line
|
||||
super('Kodak T-MAX 3200', 3200); // coverage:ignore-line
|
||||
|
||||
const KodakFilm.trix320()
|
||||
: a = 2,
|
||||
b = 1,
|
||||
c = 2,
|
||||
super('Kodak TRI-X 320', 320); // coverage:ignore-line
|
||||
|
||||
const KodakFilm.trix400()
|
||||
: a = 2,
|
||||
b = 1,
|
||||
c = 2,
|
||||
super('Kodak TRI-X 400', 400); // coverage:ignore-line
|
||||
|
||||
@override
|
||||
double reciprocityFormula(double t) => t * log10polynomian(t, a, b, c);
|
||||
}
|
|
@ -1,11 +1,18 @@
|
|||
enum MeteringScreenLayoutFeature { extremeExposurePairs, filmPicker }
|
||||
enum MeteringScreenLayoutFeature {
|
||||
extremeExposurePairs,
|
||||
filmPicker,
|
||||
histogram,
|
||||
equipmentProfiles,
|
||||
}
|
||||
|
||||
typedef MeteringScreenLayoutConfig = Map<MeteringScreenLayoutFeature, bool>;
|
||||
|
||||
extension MeteringScreenLayoutConfigJson on MeteringScreenLayoutConfig {
|
||||
static MeteringScreenLayoutConfig fromJson(Map<String, dynamic> data) => data.map(
|
||||
(key, value) => MapEntry(MeteringScreenLayoutFeature.values[int.parse(key)], value as bool),
|
||||
);
|
||||
static MeteringScreenLayoutConfig fromJson(Map<String, dynamic> data) =>
|
||||
<MeteringScreenLayoutFeature, bool>{
|
||||
for (final f in MeteringScreenLayoutFeature.values)
|
||||
f: data[f.index.toString()] as bool? ?? true
|
||||
};
|
||||
|
||||
Map<String, dynamic> toJson() => map((key, value) => MapEntry(key.index.toString(), value));
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
enum SupportedLocale { en, fr, ru }
|
||||
enum SupportedLocale { en, fr, ru, zh }
|
||||
|
||||
extension SupportedLocaleExtension on SupportedLocale {
|
||||
String get intlName => toString().replaceAll("SupportedLocale.", "");
|
||||
|
@ -11,6 +11,8 @@ extension SupportedLocaleExtension on SupportedLocale {
|
|||
return 'Français';
|
||||
case SupportedLocale.ru:
|
||||
return 'Русский';
|
||||
case SupportedLocale.zh:
|
||||
return '简体中文';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ import 'dart:convert';
|
|||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/data/models/ev_source_type.dart';
|
||||
import 'package:lightmeter/data/models/film.dart';
|
||||
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
|
||||
import 'package:lightmeter/data/models/supported_locale.dart';
|
||||
import 'package:lightmeter/data/models/theme_type.dart';
|
||||
|
@ -19,7 +18,6 @@ class UserPreferencesService {
|
|||
static const cameraEvCalibrationKey = "cameraEvCalibration";
|
||||
static const lightSensorEvCalibrationKey = "lightSensorEvCalibration";
|
||||
static const meteringScreenLayoutKey = "meteringScreenLayout";
|
||||
static const filmKey = "film";
|
||||
|
||||
static const caffeineKey = "caffeine";
|
||||
static const hapticsKey = "haptics";
|
||||
|
@ -95,8 +93,10 @@ class UserPreferencesService {
|
|||
);
|
||||
} else {
|
||||
return {
|
||||
MeteringScreenLayoutFeature.equipmentProfiles: true,
|
||||
MeteringScreenLayoutFeature.extremeExposurePairs: true,
|
||||
MeteringScreenLayoutFeature.filmPicker: true,
|
||||
MeteringScreenLayoutFeature.histogram: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -140,16 +140,4 @@ class UserPreferencesService {
|
|||
|
||||
bool get dynamicColor => _sharedPreferences.getBool(dynamicColorKey) ?? false;
|
||||
set dynamicColor(bool value) => _sharedPreferences.setBool(dynamicColorKey, value);
|
||||
|
||||
Film get film => Film.values.firstWhere(
|
||||
(e) => e.name == _sharedPreferences.getString(filmKey),
|
||||
orElse: () => Film.values.first,
|
||||
);
|
||||
set film(Film value) => _sharedPreferences.setString(filmKey, value.name);
|
||||
|
||||
String get selectedEquipmentProfileId => ''; // coverage:ignore-line
|
||||
set selectedEquipmentProfileId(String id) {} // coverage:ignore-line
|
||||
|
||||
List<EquipmentProfileData> get equipmentProfiles => []; // coverage:ignore-line
|
||||
set equipmentProfiles(List<EquipmentProfileData> profiles) {} // coverage:ignore-line
|
||||
}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
class FeaturesConfig {
|
||||
static const bool equipmentProfilesEnabled = false;
|
||||
}
|
|
@ -1,14 +1,22 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:lightmeter/firebase_options.dart';
|
||||
|
||||
Future<void> initializeFirebase() async {
|
||||
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
||||
FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;
|
||||
PlatformDispatcher.instance.onError = (error, stack) {
|
||||
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
|
||||
return true;
|
||||
};
|
||||
Future<void> initializeFirebase({required bool handleErrors}) async {
|
||||
try {
|
||||
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
||||
if (handleErrors) {
|
||||
FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;
|
||||
PlatformDispatcher.instance.onError = (error, stack) {
|
||||
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
}
|
||||
}
|
||||
|
|
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: '',
|
||||
);
|
||||
}
|
|
@ -2,7 +2,6 @@ import 'package:app_settings/app_settings.dart';
|
|||
import 'package:lightmeter/data/caffeine_service.dart';
|
||||
import 'package:lightmeter/data/haptics_service.dart';
|
||||
import 'package:lightmeter/data/light_sensor_service.dart';
|
||||
import 'package:lightmeter/data/models/film.dart';
|
||||
import 'package:lightmeter/data/models/volume_action.dart';
|
||||
import 'package:lightmeter/data/permissions_service.dart';
|
||||
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||
|
@ -45,9 +44,6 @@ class MeteringInteractor {
|
|||
NdValue get ndFilter => _userPreferencesService.ndFilter;
|
||||
set ndFilter(NdValue value) => _userPreferencesService.ndFilter = value;
|
||||
|
||||
Film get film => _userPreferencesService.film;
|
||||
set film(Film value) => _userPreferencesService.film = value;
|
||||
|
||||
VolumeAction get volumeAction => _userPreferencesService.volumeAction;
|
||||
|
||||
/// Executes vibration if haptics are enabled in settings
|
||||
|
|
|
@ -36,10 +36,14 @@
|
|||
"lightSensor": "Light sensor",
|
||||
"meteringScreenLayout": "Metering screen layout",
|
||||
"meteringScreenLayoutHint": "Hide elements on the metering screen that you don't need so that they don't waste exposure pairs list space.",
|
||||
"meteringScreenLayoutHintEquipmentProfiles": "Equipment profile picker",
|
||||
"meteringScreenFeatureExtremeExposurePairs": "Fastest & shortest exposure pairs",
|
||||
"meteringScreenFeatureFilmPicker": "Film picker",
|
||||
"meteringScreenFeatureHistogram": "Histogram",
|
||||
"film": "Film",
|
||||
"equipment": "Equipment",
|
||||
"filmPush": "Film (push)",
|
||||
"filmPull": "Film (pull)",
|
||||
"filmReciprocityHint": "Applies correction for shutter speeds grater than 1 second",
|
||||
"equipmentProfileName": "Equipment profile name",
|
||||
"equipmentProfileNameHint": "Praktica MTL5B",
|
||||
"equipmentProfileAllValues": "All",
|
||||
|
@ -53,6 +57,9 @@
|
|||
"isoValuesFilterDescription": "Select the ISO values to display. These may be your most commonly used values or those supported by your camera.",
|
||||
"equipmentProfile": "Equipment profile",
|
||||
"equipmentProfiles": "Equipment profiles",
|
||||
"tapToAdd": "Tap to add",
|
||||
"filmsInUse": "Films in use",
|
||||
"filmsInUseDescription": "Select films which you use.",
|
||||
"general": "General",
|
||||
"keepScreenOn": "Keep screen on",
|
||||
"haptics": "Haptics",
|
||||
|
@ -84,5 +91,21 @@
|
|||
"type": "String"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"buyLightmeterPro": "Buy Lightmeter Pro",
|
||||
"lightmeterPro": "Lightmeter Pro",
|
||||
"lightmeterProDescription": "Unlocks extra features, such as equipment profiles containing filters for aperture, shutter speed, and more; and a list of films with compensation for what's known as reciprocity failure.\n\nThe source code of Lightmeter is available on GitHub. You are welcome to compile it yourself. However, if you want to support the development and receive new features and updates, consider purchasing Lightmeter Pro.",
|
||||
"buy": "Buy",
|
||||
"tooltipAdd": "Add",
|
||||
"tooltipClose": "Close",
|
||||
"tooltipExpand": "Expand",
|
||||
"tooltipCollapse": "Collapse",
|
||||
"tooltipCopy": "Copy",
|
||||
"tooltipDelete": "Delete",
|
||||
"tooltipSelectAll": "Select all",
|
||||
"tooltipDesecelectAll": "Deselect all",
|
||||
"tooltipResetToZero": "Reset to zero",
|
||||
"tooltipUseLightSensor": "Use lightsensor",
|
||||
"tooltipUseCamera": "Use camera",
|
||||
"tooltipOpenSettings": "Open settings"
|
||||
}
|
|
@ -36,10 +36,14 @@
|
|||
"lightSensor": "Capteur de lumière",
|
||||
"meteringScreenLayout": "Disposition de l'écran de mesure",
|
||||
"meteringScreenLayoutHint": "Masquer les éléments sur l'écran de mesure dont vous n'avez pas besoin pour qu'ils ne gaspillent pas de l'espace dans les paires d'exposition.",
|
||||
"meteringScreenLayoutHintEquipmentProfiles": "Sélecteur de profil de l'équipement",
|
||||
"meteringScreenFeatureExtremeExposurePairs": "Paires d'exposition les plus rapides et les plus courtes",
|
||||
"meteringScreenFeatureFilmPicker": "Sélecteur de film",
|
||||
"meteringScreenFeatureHistogram": "Histogramme",
|
||||
"film": "Pellicule",
|
||||
"equipment": "Équipement",
|
||||
"filmPush": "Pellicule (push)",
|
||||
"filmPull": "Pellicule (pull)",
|
||||
"filmReciprocityHint": "La correction s'applique aux vitesses d'obturation supérieures à 1 seconde",
|
||||
"equipmentProfileName": "Nom du profil de l'équipement",
|
||||
"equipmentProfileNameHint": "Praktica MTL5B",
|
||||
"equipmentProfileAllValues": "Tout",
|
||||
|
@ -53,6 +57,9 @@
|
|||
"isoValuesFilterDescription": "Sélectionnez les valeurs ISO à afficher. Ce sont peut-être vos valeurs les plus couramment utilisées ou celles prises en charge par votre caméra.",
|
||||
"equipmentProfile": "Profil de l'équipement",
|
||||
"equipmentProfiles": "Profils de l'équipement",
|
||||
"tapToAdd": "Appuie pour ajouter",
|
||||
"filmsInUse": "Films en usage",
|
||||
"filmsInUseDescription": "Sélectionnez les films que vous utilisez.",
|
||||
"general": "Général",
|
||||
"keepScreenOn": "Garder l'écran allumé",
|
||||
"haptics": "Haptiques",
|
||||
|
@ -84,5 +91,21 @@
|
|||
"type": "String"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"buyLightmeterPro": "Acheter Lightmeter Pro",
|
||||
"lightmeterPro": "Lightmeter Pro",
|
||||
"lightmeterProDescription": "Déverrouille des fonctionnalités supplémentaires, telles que des profils d'équipement contenant des filtres pour l'ouverture, la vitesse d'obturation et plus encore, ainsi qu'une liste de films avec une compensation pour ce que l'on appelle l'échec de réciprocité.\n\nLe code source du Lightmeter est disponible sur GitHub. Vous pouvez le compiler vous-même. Cependant, si vous souhaitez soutenir le développement et recevoir de nouvelles fonctionnalités et mises à jour, envisagez d'acheter Lightmeter Pro.",
|
||||
"buy": "Acheter",
|
||||
"tooltipAdd": "Ajouter",
|
||||
"tooltipClose": "Fermer",
|
||||
"tooltipExpand": "Élargir",
|
||||
"tooltipCollapse": "Effondrement",
|
||||
"tooltipCopy": "Copie",
|
||||
"tooltipDelete": "Supprimer",
|
||||
"tooltipSelectAll": "Tout sélectionner",
|
||||
"tooltipDesecelectAll": "Désélectionner tout",
|
||||
"tooltipResetToZero": "Remise à zéro",
|
||||
"tooltipUseLightSensor": "Utiliser un capteur de lumière",
|
||||
"tooltipUseCamera": "Utiliser la caméra",
|
||||
"tooltipOpenSettings": "Ouvrir les paramètres"
|
||||
}
|
|
@ -36,10 +36,14 @@
|
|||
"lightSensor": "Датчик освещённости",
|
||||
"meteringScreenLayout": "Элементы главного экрана",
|
||||
"meteringScreenLayoutHint": "Здесь вы можете скрыть некоторые ненужные или неиспользуемые элементы с главного экрана.",
|
||||
"meteringScreenLayoutHintEquipmentProfiles": "Выбор профиля оборудования",
|
||||
"meteringScreenFeatureExtremeExposurePairs": "Длинная и короткая выдержки",
|
||||
"meteringScreenFeatureFilmPicker": "Выбор пленки",
|
||||
"meteringScreenFeatureHistogram": "Гистограмма",
|
||||
"film": "Пленка",
|
||||
"equipment": "Оборудование",
|
||||
"filmPush": "Пленка (push)",
|
||||
"filmPull": "Пленка (pull)",
|
||||
"filmReciprocityHint": "Применяет коррекцию для выдержек длиннее 1 секунды",
|
||||
"equipmentProfileName": "Название профиля",
|
||||
"equipmentProfileNameHint": "Praktica MTL5B",
|
||||
"equipmentProfileAllValues": "Все",
|
||||
|
@ -53,6 +57,9 @@
|
|||
"isoValuesFilterDescription": "Выберите значения ISO для отображения. Это может быть наиболее часто используемые значения или значения, поддерживаемые вашей камерой.",
|
||||
"equipmentProfile": "Оборудование",
|
||||
"equipmentProfiles": "Профили оборудования",
|
||||
"tapToAdd": "Нажмите, чтобы добавить",
|
||||
"filmsInUse": "Используемые пленки",
|
||||
"filmsInUseDescription": "Выберите пленки, которыми вы пользуетесь.",
|
||||
"general": "Общие",
|
||||
"keepScreenOn": "Запрет блокировки",
|
||||
"haptics": "Вибрация",
|
||||
|
@ -84,5 +91,21 @@
|
|||
"type": "String"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"buyLightmeterPro": "Купить Lightmeter Pro",
|
||||
"lightmeterPro": "Lightmeter Pro",
|
||||
"lightmeterProDescription": "Даёт доступ к таким функциям как профили оборудования, содержащие фильтры для диафрагмы, выдержки и других значений, а также набору пленок с компенсацией эффекта Шварцшильда.\n\nИсходный код Lightmeter доступен на GitHub. Вы можете собрать его самостоятельно. Однако если вы хотите поддержать разработку и получать новые функции и обновления, то приобретите Lightmeter Pro.",
|
||||
"buy": "Купить",
|
||||
"tooltipAdd": "Добавить",
|
||||
"tooltipClose": "Закрыть",
|
||||
"tooltipExpand": "Развернуть",
|
||||
"tooltipCollapse": "Свернуть",
|
||||
"tooltipCopy": "Скопировать",
|
||||
"tooltipDelete": "Удалить",
|
||||
"tooltipSelectAll": "Выбрать все",
|
||||
"tooltipDesecelectAll": "Отменить все",
|
||||
"tooltipResetToZero": "Сбросить до 0",
|
||||
"tooltipUseLightSensor": "Использовать датчик освещенности",
|
||||
"tooltipUseCamera": "Использовать камеру",
|
||||
"tooltipOpenSettings": "Открыть настройки"
|
||||
}
|
111
lib/l10n/intl_zh.arb
Normal file
111
lib/l10n/intl_zh.arb
Normal file
|
@ -0,0 +1,111 @@
|
|||
{
|
||||
"@@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": "此应用程序测量读数的准确s性完全取决于设备的后置摄像头。因此,请考虑测试此应用并手动设置 EV 校准,以获得准确的测量结果。",
|
||||
"camera": "摄像头",
|
||||
"lightSensor": "光传感器",
|
||||
"meteringScreenLayout": "布局",
|
||||
"meteringScreenLayoutHint": "隐藏不需要的元素,以免浪费曝光列表空间",
|
||||
"meteringScreenLayoutHintEquipmentProfiles": "设备配置选择",
|
||||
"meteringScreenFeatureExtremeExposurePairs": "最快 & 最慢曝光组合",
|
||||
"meteringScreenFeatureFilmPicker": "胶片选择",
|
||||
"meteringScreenFeatureHistogram": "直方图",
|
||||
"film": "胶片",
|
||||
"filmPush": "胶片 (push)",
|
||||
"filmPull": "胶片 (pull)",
|
||||
"filmReciprocityHint": "对快门速度超过 1 秒的情况进行修正",
|
||||
"equipmentProfileName": "设备配置名称",
|
||||
"equipmentProfileNameHint": "Praktica MTL5B",
|
||||
"equipmentProfileAllValues": "全部",
|
||||
"apertureValues": "光圈值",
|
||||
"apertureValuesFilterDescription": "选择要显示的光圈值范围。这通常由您使用的镜头决定。",
|
||||
"ndFilters": "ND 滤镜",
|
||||
"ndFiltersFilterDescription": "选择要显示的 ND 滤镜系数。这些可能是您最常用的 ND 滤镜,也可能是适合您镜头的滤光镜。",
|
||||
"shutterSpeedValues": "快门速度",
|
||||
"shutterSpeedValuesFilterDescription": "选择要显示的快门速度范围。这通常由您使用的相机机身决定。",
|
||||
"isoValues": "ISO",
|
||||
"isoValuesFilterDescription": "选择要显示的 ISO 。这些值可能是您最常用的值,也可能是相机支持的值。",
|
||||
"equipmentProfile": "设备配置",
|
||||
"equipmentProfiles": "设备配置",
|
||||
"tapToAdd": "點擊添加",
|
||||
"filmsInUse": "使用的胶片",
|
||||
"filmsInUseDescription": "选择你使用的胶片",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"buyLightmeterPro": "购买 Lightmeter Pro",
|
||||
"lightmeterPro": "Lightmeter Pro",
|
||||
"lightmeterProDescription": "购买以解锁额外功能。例如包含光圈、快门速度等参数的配置文件;以及一个胶卷预设列表来提供倒易率失效时的曝光补偿。\n\n您可以在 GitHub 上获取 Lightmeter 的源代码,欢迎自行编译。不过,如果您想支持开发并获得新功能和更新,请考虑购买 Lightmeter Pro。",
|
||||
"buy": "购买",
|
||||
"tooltipAdd": "添加",
|
||||
"tooltipClose": "关闭",
|
||||
"tooltipExpand": "展开",
|
||||
"tooltipCollapse": "崩溃",
|
||||
"tooltipCopy": "复制",
|
||||
"tooltipDelete": "删除",
|
||||
"tooltipSelectAll": "全选",
|
||||
"tooltipDesecelectAll": "取消全选",
|
||||
"resetToZero": "重置为零",
|
||||
"tooltipUseLightSensor": "使用光线传感器",
|
||||
"tooltipUseCamera": "使用摄像头",
|
||||
"tooltipOpenSettings": "打开设置"
|
||||
}
|
|
@ -1,8 +1,9 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/application.dart';
|
||||
import 'package:lightmeter/application_wrapper.dart';
|
||||
import 'package:lightmeter/environment.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
runApp(const Application(Environment.dev()));
|
||||
runApp(const ApplicationWrapper(Environment.dev(), child: Application()));
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/application.dart';
|
||||
import 'package:lightmeter/application_wrapper.dart';
|
||||
import 'package:lightmeter/environment.dart';
|
||||
import 'package:lightmeter/firebase.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await initializeFirebase();
|
||||
runApp(const Application(Environment.prod()));
|
||||
await initializeFirebase(handleErrors: true);
|
||||
runApp(const ApplicationWrapper(Environment.prod(), child: Application()));
|
||||
}
|
||||
|
|
11
lib/main_release.dart
Normal file
11
lib/main_release.dart
Normal file
|
@ -0,0 +1,11 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/application.dart';
|
||||
import 'package:lightmeter/application_wrapper.dart';
|
||||
import 'package:lightmeter/environment.dart';
|
||||
import 'package:lightmeter/firebase.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await initializeFirebase(handleErrors: false);
|
||||
runApp(const ApplicationWrapper(Environment.prod(), child: Application()));
|
||||
}
|
|
@ -5,4 +5,6 @@ class PlatformConfig {
|
|||
final rational = const String.fromEnvironment('cameraPreviewAspectRatio').split('/');
|
||||
return int.parse(rational[0]) / int.parse(rational[1]);
|
||||
}
|
||||
|
||||
static String get cameraStubImage => const String.fromEnvironment('cameraStubImage');
|
||||
}
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/data/caffeine_service.dart';
|
||||
import 'package:lightmeter/data/haptics_service.dart';
|
||||
import 'package:lightmeter/data/light_sensor_service.dart';
|
||||
import 'package:lightmeter/data/permissions_service.dart';
|
||||
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||
import 'package:lightmeter/data/volume_events_service.dart';
|
||||
import 'package:lightmeter/environment.dart';
|
||||
import 'package:lightmeter/providers/equipment_profile_provider.dart';
|
||||
import 'package:lightmeter/providers/ev_source_type_provider.dart';
|
||||
import 'package:lightmeter/providers/metering_screen_layout_provider.dart';
|
||||
import 'package:lightmeter/providers/stop_type_provider.dart';
|
||||
import 'package:lightmeter/providers/supported_locale_provider.dart';
|
||||
import 'package:lightmeter/providers/theme_provider.dart';
|
||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||
import 'package:platform/platform.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class LightmeterProviders extends StatelessWidget {
|
||||
final Environment env;
|
||||
final Widget Function(BuildContext context, bool ready) builder;
|
||||
|
||||
const LightmeterProviders({required this.env, required this.builder, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder(
|
||||
future: Future.wait([
|
||||
SharedPreferences.getInstance(),
|
||||
const LightSensorService(LocalPlatform()).hasSensor(),
|
||||
]),
|
||||
builder: (_, snapshot) {
|
||||
if (snapshot.data != null) {
|
||||
return InheritedWidgetBase<Environment>(
|
||||
data: env.copyWith(hasLightSensor: snapshot.data![1] as bool),
|
||||
child: InheritedWidgetBase<UserPreferencesService>(
|
||||
data: UserPreferencesService(snapshot.data![0] as SharedPreferences),
|
||||
child: InheritedWidgetBase<LightSensorService>(
|
||||
data: const LightSensorService(LocalPlatform()),
|
||||
child: InheritedWidgetBase<CaffeineService>(
|
||||
data: const CaffeineService(),
|
||||
child: InheritedWidgetBase<HapticsService>(
|
||||
data: const HapticsService(),
|
||||
child: InheritedWidgetBase<VolumeEventsService>(
|
||||
data: const VolumeEventsService(LocalPlatform()),
|
||||
child: InheritedWidgetBase<PermissionsService>(
|
||||
data: const PermissionsService(),
|
||||
child: MeteringScreenLayoutProvider(
|
||||
child: StopTypeProvider(
|
||||
child: EquipmentProfileProvider(
|
||||
child: EvSourceTypeProvider(
|
||||
child: SupportedLocaleProvider(
|
||||
child: ThemeProvider(
|
||||
child: Builder(
|
||||
builder: (context) => builder(context, true),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (snapshot.error != null) {
|
||||
return Center(child: Text(snapshot.error!.toString()));
|
||||
}
|
||||
return builder(context, false);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,101 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
typedef EquipmentProfiles = List<EquipmentProfileData>;
|
||||
typedef EquipmentProfile = EquipmentProfileData;
|
||||
|
||||
class EquipmentProfileProvider extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
const EquipmentProfileProvider({required this.child, super.key});
|
||||
|
||||
static EquipmentProfileProviderState of(BuildContext context) {
|
||||
return context.findAncestorStateOfType<EquipmentProfileProviderState>()!;
|
||||
}
|
||||
|
||||
@override
|
||||
State<EquipmentProfileProvider> createState() => EquipmentProfileProviderState();
|
||||
}
|
||||
|
||||
class EquipmentProfileProviderState extends State<EquipmentProfileProvider> {
|
||||
static const EquipmentProfileData _defaultProfile = EquipmentProfileData(
|
||||
id: '',
|
||||
name: '',
|
||||
apertureValues: ApertureValue.values,
|
||||
ndValues: NdValue.values,
|
||||
shutterSpeedValues: ShutterSpeedValue.values,
|
||||
isoValues: IsoValue.values,
|
||||
);
|
||||
|
||||
List<EquipmentProfileData> _customProfiles = [];
|
||||
String _selectedId = '';
|
||||
|
||||
EquipmentProfileData get _selectedProfile => _customProfiles.firstWhere(
|
||||
(e) => e.id == _selectedId,
|
||||
orElse: () {
|
||||
context.get<UserPreferencesService>().selectedEquipmentProfileId = _defaultProfile.id;
|
||||
return _defaultProfile;
|
||||
},
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedId = context.get<UserPreferencesService>().selectedEquipmentProfileId;
|
||||
_customProfiles = context.get<UserPreferencesService>().equipmentProfiles;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InheritedWidgetBase<List<EquipmentProfileData>>(
|
||||
data: [_defaultProfile] + _customProfiles,
|
||||
child: InheritedWidgetBase<EquipmentProfileData>(
|
||||
data: _selectedProfile,
|
||||
child: widget.child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void setProfile(EquipmentProfileData data) {
|
||||
setState(() {
|
||||
_selectedId = data.id;
|
||||
});
|
||||
context.get<UserPreferencesService>().selectedEquipmentProfileId = _selectedProfile.id;
|
||||
}
|
||||
|
||||
/// Creates a default equipment profile
|
||||
void addProfile(String name) {
|
||||
_customProfiles.add(
|
||||
EquipmentProfileData(
|
||||
id: const Uuid().v1(),
|
||||
name: name,
|
||||
apertureValues: ApertureValue.values,
|
||||
ndValues: NdValue.values,
|
||||
shutterSpeedValues: ShutterSpeedValue.values,
|
||||
isoValues: IsoValue.values,
|
||||
),
|
||||
);
|
||||
_refreshSavedProfiles();
|
||||
}
|
||||
|
||||
void updateProdile(EquipmentProfileData data) {
|
||||
final indexToUpdate = _customProfiles.indexWhere((element) => element.id == data.id);
|
||||
if (indexToUpdate >= 0) {
|
||||
_customProfiles[indexToUpdate] = data;
|
||||
_refreshSavedProfiles();
|
||||
}
|
||||
}
|
||||
|
||||
void deleteProfile(EquipmentProfileData data) {
|
||||
_customProfiles.remove(data);
|
||||
_refreshSavedProfiles();
|
||||
}
|
||||
|
||||
void _refreshSavedProfiles() {
|
||||
context.get<UserPreferencesService>().equipmentProfiles = _customProfiles;
|
||||
setState(() {});
|
||||
}
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/data/models/ev_source_type.dart';
|
||||
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||
import 'package:lightmeter/environment.dart';
|
||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||
|
||||
class EvSourceTypeProvider extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
const EvSourceTypeProvider({required this.child, super.key});
|
||||
|
||||
static EvSourceTypeProviderState of(BuildContext context) {
|
||||
return context.findAncestorStateOfType<EvSourceTypeProviderState>()!;
|
||||
}
|
||||
|
||||
@override
|
||||
State<EvSourceTypeProvider> createState() => EvSourceTypeProviderState();
|
||||
}
|
||||
|
||||
class EvSourceTypeProviderState extends State<EvSourceTypeProvider> {
|
||||
late final ValueNotifier<EvSourceType> valueListenable;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final evSourceType = context.get<UserPreferencesService>().evSourceType;
|
||||
valueListenable = ValueNotifier(
|
||||
evSourceType == EvSourceType.sensor && !context.get<Environment>().hasLightSensor
|
||||
? EvSourceType.camera
|
||||
: evSourceType,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
valueListenable.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: valueListenable,
|
||||
builder: (_, value, child) => InheritedWidgetBase<EvSourceType>(
|
||||
data: value,
|
||||
child: child!,
|
||||
),
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
void toggleType() {
|
||||
switch (valueListenable.value) {
|
||||
case EvSourceType.camera:
|
||||
if (context.get<Environment>().hasLightSensor) {
|
||||
valueListenable.value = EvSourceType.sensor;
|
||||
}
|
||||
case EvSourceType.sensor:
|
||||
valueListenable.value = EvSourceType.camera;
|
||||
}
|
||||
context.get<UserPreferencesService>().evSourceType = valueListenable.value;
|
||||
}
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
|
||||
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||
|
||||
class MeteringScreenLayoutProvider extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
const MeteringScreenLayoutProvider({required this.child, super.key});
|
||||
|
||||
static MeteringScreenLayoutProviderState of(BuildContext context) {
|
||||
return context.findAncestorStateOfType<MeteringScreenLayoutProviderState>()!;
|
||||
}
|
||||
|
||||
@override
|
||||
State<MeteringScreenLayoutProvider> createState() => MeteringScreenLayoutProviderState();
|
||||
}
|
||||
|
||||
class MeteringScreenLayoutProviderState extends State<MeteringScreenLayoutProvider> {
|
||||
late final MeteringScreenLayoutConfig _config =
|
||||
context.get<UserPreferencesService>().meteringScreenLayout;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InheritedModelBase<MeteringScreenLayoutFeature, bool>(
|
||||
data: MeteringScreenLayoutConfig.from(_config),
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
void updateFeatures(MeteringScreenLayoutConfig config) {
|
||||
setState(() {
|
||||
config.forEach((key, value) {
|
||||
_config.update(
|
||||
key,
|
||||
(_) => value,
|
||||
ifAbsent: () => value,
|
||||
);
|
||||
});
|
||||
});
|
||||
context.get<UserPreferencesService>().meteringScreenLayout = _config;
|
||||
}
|
||||
}
|
||||
|
||||
typedef _MeteringScreenLayoutModel = InheritedModelBase<MeteringScreenLayoutFeature, bool>;
|
||||
|
||||
extension MeteringScreenLayout on InheritedModelBase<MeteringScreenLayoutFeature, bool> {
|
||||
static MeteringScreenLayoutConfig of(BuildContext context, {bool listen = true}) {
|
||||
if (listen) {
|
||||
return context.dependOnInheritedWidgetOfExactType<_MeteringScreenLayoutModel>()!.data;
|
||||
} else {
|
||||
return context.findAncestorWidgetOfExactType<_MeteringScreenLayoutModel>()!.data;
|
||||
}
|
||||
}
|
||||
|
||||
static bool featureOf(BuildContext context, MeteringScreenLayoutFeature aspect) {
|
||||
return InheritedModel.inheritFrom<_MeteringScreenLayoutModel>(context, aspect: aspect)!
|
||||
.data[aspect]!;
|
||||
}
|
||||
}
|
36
lib/providers/services_provider.dart
Normal file
36
lib/providers/services_provider.dart
Normal file
|
@ -0,0 +1,36 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/data/caffeine_service.dart';
|
||||
import 'package:lightmeter/data/haptics_service.dart';
|
||||
import 'package:lightmeter/data/light_sensor_service.dart';
|
||||
import 'package:lightmeter/data/permissions_service.dart';
|
||||
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||
import 'package:lightmeter/data/volume_events_service.dart';
|
||||
import 'package:lightmeter/environment.dart';
|
||||
|
||||
class ServicesProvider extends InheritedWidget {
|
||||
final CaffeineService caffeineService;
|
||||
final Environment environment;
|
||||
final HapticsService hapticsService;
|
||||
final LightSensorService lightSensorService;
|
||||
final PermissionsService permissionsService;
|
||||
final UserPreferencesService userPreferencesService;
|
||||
final VolumeEventsService volumeEventsService;
|
||||
|
||||
const ServicesProvider({
|
||||
required this.caffeineService,
|
||||
required this.environment,
|
||||
required this.hapticsService,
|
||||
required this.lightSensorService,
|
||||
required this.permissionsService,
|
||||
required this.userPreferencesService,
|
||||
required this.volumeEventsService,
|
||||
required super.child,
|
||||
});
|
||||
|
||||
static ServicesProvider of(BuildContext context) {
|
||||
return context.findAncestorWidgetOfExactType<ServicesProvider>()!;
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(ServicesProvider oldWidget) => false;
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||
|
||||
class StopTypeProvider extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
const StopTypeProvider({required this.child, super.key});
|
||||
|
||||
static StopTypeProviderState of(BuildContext context) {
|
||||
return context.findAncestorStateOfType<StopTypeProviderState>()!;
|
||||
}
|
||||
|
||||
@override
|
||||
State<StopTypeProvider> createState() => StopTypeProviderState();
|
||||
}
|
||||
|
||||
class StopTypeProviderState extends State<StopTypeProvider> {
|
||||
late StopType _stopType;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_stopType = context.get<UserPreferencesService>().stopType;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InheritedWidgetBase<StopType>(
|
||||
data: _stopType,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
void set(StopType type) {
|
||||
setState(() {
|
||||
_stopType = type;
|
||||
});
|
||||
context.get<UserPreferencesService>().stopType = type;
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/data/models/supported_locale.dart';
|
||||
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||
import 'package:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||
|
||||
class SupportedLocaleProvider extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
const SupportedLocaleProvider({required this.child, super.key});
|
||||
|
||||
static SupportedLocaleProviderState of(BuildContext context) {
|
||||
return context.findAncestorStateOfType<SupportedLocaleProviderState>()!;
|
||||
}
|
||||
|
||||
@override
|
||||
State<SupportedLocaleProvider> createState() => SupportedLocaleProviderState();
|
||||
}
|
||||
|
||||
class SupportedLocaleProviderState extends State<SupportedLocaleProvider> {
|
||||
late final ValueNotifier<SupportedLocale> valueListenable;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
valueListenable = ValueNotifier(context.get<UserPreferencesService>().locale);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
valueListenable.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: valueListenable,
|
||||
builder: (_, value, child) => InheritedWidgetBase<SupportedLocale>(
|
||||
data: value,
|
||||
child: child!,
|
||||
),
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
void setLocale(SupportedLocale locale) {
|
||||
S.load(Locale(locale.intlName)).then((value) {
|
||||
valueListenable.value = locale;
|
||||
context.get<UserPreferencesService>().locale = locale;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,257 +0,0 @@
|
|||
import 'package:dynamic_color/dynamic_color.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:lightmeter/data/models/dynamic_colors_state.dart';
|
||||
import 'package:lightmeter/data/models/theme_type.dart';
|
||||
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||
import 'package:lightmeter/res/dimens.dart';
|
||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||
import 'package:material_color_utilities/material_color_utilities.dart';
|
||||
|
||||
class ThemeProvider extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
const ThemeProvider({
|
||||
required this.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
static ThemeProviderState of(BuildContext context) {
|
||||
return context.findAncestorStateOfType<ThemeProviderState>()!;
|
||||
}
|
||||
|
||||
static const primaryColorsList = [
|
||||
Color(0xfff44336),
|
||||
Color(0xffe91e63),
|
||||
Color(0xff9c27b0),
|
||||
Color(0xff673ab7),
|
||||
Color(0xff3f51b5),
|
||||
Color(0xff2196f3),
|
||||
Color(0xff03a9f4),
|
||||
Color(0xff00bcd4),
|
||||
Color(0xff009688),
|
||||
Color(0xff4caf50),
|
||||
Color(0xff8bc34a),
|
||||
Color(0xffcddc39),
|
||||
Color(0xffffeb3b),
|
||||
Color(0xffffc107),
|
||||
Color(0xffff9800),
|
||||
Color(0xffff5722),
|
||||
];
|
||||
|
||||
@override
|
||||
State<ThemeProvider> createState() => ThemeProviderState();
|
||||
}
|
||||
|
||||
class ThemeProviderState extends State<ThemeProvider> with WidgetsBindingObserver {
|
||||
UserPreferencesService get _prefs => context.get<UserPreferencesService>();
|
||||
|
||||
late final _themeTypeNotifier = ValueNotifier<ThemeType>(_prefs.themeType);
|
||||
late final _dynamicColorNotifier = ValueNotifier<bool>(_prefs.dynamicColor);
|
||||
late final _primaryColorNotifier = ValueNotifier<Color>(_prefs.primaryColor);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangePlatformBrightness() {
|
||||
super.didChangePlatformBrightness();
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_themeTypeNotifier.dispose();
|
||||
_dynamicColorNotifier.dispose();
|
||||
_primaryColorNotifier.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: _themeTypeNotifier,
|
||||
builder: (_, themeType, __) => InheritedWidgetBase<ThemeType>(
|
||||
data: themeType,
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: _dynamicColorNotifier,
|
||||
builder: (_, useDynamicColor, __) => _DynamicColorProvider(
|
||||
useDynamicColor: useDynamicColor,
|
||||
themeBrightness: _themeBrightness,
|
||||
builder: (_, dynamicPrimaryColor) => ValueListenableBuilder(
|
||||
valueListenable: _primaryColorNotifier,
|
||||
builder: (_, primaryColor, __) => _ThemeDataProvider(
|
||||
primaryColor: dynamicPrimaryColor ?? primaryColor,
|
||||
brightness: _themeBrightness,
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void setThemeType(ThemeType themeType) {
|
||||
_themeTypeNotifier.value = themeType;
|
||||
_prefs.themeType = themeType;
|
||||
}
|
||||
|
||||
Brightness get _themeBrightness {
|
||||
switch (_themeTypeNotifier.value) {
|
||||
case ThemeType.light:
|
||||
return Brightness.light;
|
||||
case ThemeType.dark:
|
||||
return Brightness.dark;
|
||||
case ThemeType.systemDefault:
|
||||
return SchedulerBinding.instance.platformDispatcher.platformBrightness;
|
||||
}
|
||||
}
|
||||
|
||||
void setPrimaryColor(Color color) {
|
||||
_primaryColorNotifier.value = color;
|
||||
_prefs.primaryColor = color;
|
||||
}
|
||||
|
||||
void enableDynamicColor(bool enable) {
|
||||
_dynamicColorNotifier.value = enable;
|
||||
_prefs.dynamicColor = enable;
|
||||
}
|
||||
}
|
||||
|
||||
class _DynamicColorProvider extends StatelessWidget {
|
||||
final bool useDynamicColor;
|
||||
final Brightness themeBrightness;
|
||||
final Widget Function(BuildContext context, Color? primaryColor) builder;
|
||||
|
||||
const _DynamicColorProvider({
|
||||
required this.useDynamicColor,
|
||||
required this.themeBrightness,
|
||||
required this.builder,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DynamicColorBuilder(
|
||||
builder: (lightDynamic, darkDynamic) {
|
||||
late final DynamicColorState state;
|
||||
late final Color? dynamicPrimaryColor;
|
||||
if (lightDynamic != null && darkDynamic != null) {
|
||||
if (useDynamicColor) {
|
||||
dynamicPrimaryColor =
|
||||
(themeBrightness == Brightness.light ? lightDynamic : darkDynamic).primary;
|
||||
state = DynamicColorState.enabled;
|
||||
} else {
|
||||
dynamicPrimaryColor = null;
|
||||
state = DynamicColorState.disabled;
|
||||
}
|
||||
} else {
|
||||
dynamicPrimaryColor = null;
|
||||
state = DynamicColorState.unavailable;
|
||||
}
|
||||
return InheritedWidgetBase<DynamicColorState>(
|
||||
data: state,
|
||||
child: builder(context, dynamicPrimaryColor),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ThemeDataProvider extends StatelessWidget {
|
||||
final Color primaryColor;
|
||||
final Brightness brightness;
|
||||
final Widget child;
|
||||
|
||||
const _ThemeDataProvider({
|
||||
required this.primaryColor,
|
||||
required this.brightness,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InheritedWidgetBase<ThemeData>(
|
||||
data: _themeFromColorScheme(_colorSchemeFromColor()),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
ThemeData _themeFromColorScheme(ColorScheme scheme) {
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: scheme.brightness,
|
||||
primaryColor: primaryColor,
|
||||
colorScheme: scheme,
|
||||
appBarTheme: AppBarTheme(
|
||||
elevation: 4,
|
||||
color: scheme.surface,
|
||||
surfaceTintColor: scheme.surfaceTint,
|
||||
),
|
||||
cardTheme: CardTheme(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
color: scheme.surface,
|
||||
elevation: 4,
|
||||
margin: EdgeInsets.zero,
|
||||
shadowColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(Dimens.borderRadiusL)),
|
||||
surfaceTintColor: scheme.surfaceTint,
|
||||
),
|
||||
dialogBackgroundColor: scheme.surface,
|
||||
dialogTheme: DialogTheme(
|
||||
backgroundColor: scheme.surface,
|
||||
surfaceTintColor: scheme.surfaceTint,
|
||||
elevation: 6,
|
||||
),
|
||||
dividerColor: scheme.outlineVariant,
|
||||
dividerTheme: DividerThemeData(
|
||||
color: scheme.outlineVariant,
|
||||
space: 0,
|
||||
),
|
||||
listTileTheme: ListTileThemeData(
|
||||
style: ListTileStyle.list,
|
||||
iconColor: scheme.onSurface,
|
||||
textColor: scheme.onSurface,
|
||||
),
|
||||
scaffoldBackgroundColor: scheme.surface,
|
||||
);
|
||||
}
|
||||
|
||||
ColorScheme _colorSchemeFromColor() {
|
||||
final scheme = brightness == Brightness.light
|
||||
? Scheme.light(primaryColor.value)
|
||||
: Scheme.dark(primaryColor.value);
|
||||
|
||||
return ColorScheme(
|
||||
brightness: brightness,
|
||||
background: Color(scheme.background),
|
||||
error: Color(scheme.error),
|
||||
errorContainer: Color(scheme.errorContainer),
|
||||
onBackground: Color(scheme.onBackground),
|
||||
onError: Color(scheme.onError),
|
||||
onErrorContainer: Color(scheme.onErrorContainer),
|
||||
primary: Color(scheme.primary),
|
||||
onPrimary: Color(scheme.onPrimary),
|
||||
primaryContainer: Color(scheme.primaryContainer),
|
||||
onPrimaryContainer: Color(scheme.onPrimaryContainer),
|
||||
secondary: Color(scheme.secondary),
|
||||
onSecondary: Color(scheme.onSecondary),
|
||||
surface: Color.alphaBlend(
|
||||
Color(scheme.primary).withOpacity(0.05),
|
||||
Color(scheme.background),
|
||||
),
|
||||
onSurface: Color(scheme.onSurface),
|
||||
surfaceVariant: Color.alphaBlend(
|
||||
Color(scheme.primary).withOpacity(0.5),
|
||||
Color(scheme.background),
|
||||
),
|
||||
onSurfaceVariant: Color(scheme.onSurfaceVariant),
|
||||
outline: Color(scheme.outline),
|
||||
outlineVariant: Color(scheme.outlineVariant),
|
||||
);
|
||||
}
|
||||
}
|
295
lib/providers/user_preferences_provider.dart
Normal file
295
lib/providers/user_preferences_provider.dart
Normal file
|
@ -0,0 +1,295 @@
|
|||
import 'package:dynamic_color/dynamic_color.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:lightmeter/data/models/dynamic_colors_state.dart';
|
||||
import 'package:lightmeter/data/models/ev_source_type.dart';
|
||||
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
|
||||
import 'package:lightmeter/data/models/supported_locale.dart';
|
||||
import 'package:lightmeter/data/models/theme_type.dart';
|
||||
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||
import 'package:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/providers/services_provider.dart';
|
||||
import 'package:lightmeter/res/theme.dart';
|
||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||
|
||||
class UserPreferencesProvider extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
const UserPreferencesProvider({required this.child, super.key});
|
||||
|
||||
static _UserPreferencesProviderState of(BuildContext context) {
|
||||
return context.findAncestorStateOfType<_UserPreferencesProviderState>()!;
|
||||
}
|
||||
|
||||
static DynamicColorState dynamicColorStateOf(BuildContext context) {
|
||||
return _inheritFromEnumsModel(context, _Aspect.dynamicColorState).dynamicColorState;
|
||||
}
|
||||
|
||||
static EvSourceType evSourceTypeOf(BuildContext context) {
|
||||
return _inheritFromEnumsModel(context, _Aspect.evSourceType).evSourceType;
|
||||
}
|
||||
|
||||
static SupportedLocale localeOf(BuildContext context) {
|
||||
return _inheritFromEnumsModel(context, _Aspect.locale).locale;
|
||||
}
|
||||
|
||||
static MeteringScreenLayoutConfig meteringScreenConfigOf(BuildContext context) {
|
||||
return context.findAncestorWidgetOfExactType<_MeteringScreenLayoutModel>()!.data;
|
||||
}
|
||||
|
||||
static bool meteringScreenFeatureOf(BuildContext context, MeteringScreenLayoutFeature feature) {
|
||||
return InheritedModel.inheritFrom<_MeteringScreenLayoutModel>(context, aspect: feature)!
|
||||
.data[feature]!;
|
||||
}
|
||||
|
||||
static StopType stopTypeOf(BuildContext context) {
|
||||
return _inheritFromEnumsModel(context, _Aspect.stopType).stopType;
|
||||
}
|
||||
|
||||
static ThemeData themeOf(BuildContext context) {
|
||||
return _inheritFromEnumsModel(context, _Aspect.theme).theme;
|
||||
}
|
||||
|
||||
static ThemeType themeTypeOf(BuildContext context) {
|
||||
return _inheritFromEnumsModel(context, _Aspect.themeType).themeType;
|
||||
}
|
||||
|
||||
static _UserPreferencesModel _inheritFromEnumsModel(
|
||||
BuildContext context,
|
||||
_Aspect aspect,
|
||||
) {
|
||||
return InheritedModel.inheritFrom<_UserPreferencesModel>(context, aspect: aspect)!;
|
||||
}
|
||||
|
||||
@override
|
||||
State<UserPreferencesProvider> createState() => _UserPreferencesProviderState();
|
||||
}
|
||||
|
||||
class _UserPreferencesProviderState extends State<UserPreferencesProvider>
|
||||
with WidgetsBindingObserver {
|
||||
UserPreferencesService get userPreferencesService =>
|
||||
ServicesProvider.of(context).userPreferencesService;
|
||||
|
||||
late bool dynamicColor = userPreferencesService.dynamicColor;
|
||||
late EvSourceType evSourceType;
|
||||
late MeteringScreenLayoutConfig meteringScreenLayout =
|
||||
userPreferencesService.meteringScreenLayout;
|
||||
late Color primaryColor = userPreferencesService.primaryColor;
|
||||
late StopType stopType = userPreferencesService.stopType;
|
||||
late SupportedLocale locale = userPreferencesService.locale;
|
||||
late ThemeType themeType = userPreferencesService.themeType;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
evSourceType = userPreferencesService.evSourceType;
|
||||
evSourceType = evSourceType == EvSourceType.sensor &&
|
||||
!ServicesProvider.of(context).environment.hasLightSensor
|
||||
? EvSourceType.camera
|
||||
: evSourceType;
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangePlatformBrightness() {
|
||||
super.didChangePlatformBrightness();
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DynamicColorBuilder(
|
||||
builder: (lightDynamic, darkDynamic) {
|
||||
late final DynamicColorState state;
|
||||
late final Color? dynamicPrimaryColor;
|
||||
if (lightDynamic != null && darkDynamic != null) {
|
||||
if (dynamicColor) {
|
||||
dynamicPrimaryColor =
|
||||
(_themeBrightness == Brightness.light ? lightDynamic : darkDynamic).primary;
|
||||
state = DynamicColorState.enabled;
|
||||
} else {
|
||||
dynamicPrimaryColor = null;
|
||||
state = DynamicColorState.disabled;
|
||||
}
|
||||
} else {
|
||||
dynamicPrimaryColor = null;
|
||||
state = DynamicColorState.unavailable;
|
||||
}
|
||||
return _UserPreferencesModel(
|
||||
brightness: _themeBrightness,
|
||||
dynamicColorState: state,
|
||||
evSourceType: evSourceType,
|
||||
locale: locale,
|
||||
primaryColor: dynamicPrimaryColor ?? primaryColor,
|
||||
stopType: stopType,
|
||||
themeType: themeType,
|
||||
child: _MeteringScreenLayoutModel(
|
||||
data: meteringScreenLayout,
|
||||
child: widget.child,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void enableDynamicColor(bool enable) {
|
||||
setState(() {
|
||||
dynamicColor = enable;
|
||||
});
|
||||
userPreferencesService.dynamicColor = enable;
|
||||
}
|
||||
|
||||
void toggleEvSourceType() {
|
||||
if (!ServicesProvider.of(context).environment.hasLightSensor) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
switch (evSourceType) {
|
||||
case EvSourceType.camera:
|
||||
evSourceType = EvSourceType.sensor;
|
||||
case EvSourceType.sensor:
|
||||
evSourceType = EvSourceType.camera;
|
||||
}
|
||||
});
|
||||
userPreferencesService.evSourceType = evSourceType;
|
||||
}
|
||||
|
||||
void setLocale(SupportedLocale locale) {
|
||||
S.load(Locale(locale.intlName)).then((value) {
|
||||
setState(() {
|
||||
this.locale = locale;
|
||||
});
|
||||
userPreferencesService.locale = locale;
|
||||
});
|
||||
}
|
||||
|
||||
void setMeteringScreenLayout(MeteringScreenLayoutConfig config) {
|
||||
setState(() {
|
||||
meteringScreenLayout = config;
|
||||
});
|
||||
userPreferencesService.meteringScreenLayout = meteringScreenLayout;
|
||||
}
|
||||
|
||||
void setPrimaryColor(Color primaryColor) {
|
||||
setState(() {
|
||||
this.primaryColor = primaryColor;
|
||||
});
|
||||
userPreferencesService.primaryColor = primaryColor;
|
||||
}
|
||||
|
||||
void setStopType(StopType stopType) {
|
||||
setState(() {
|
||||
this.stopType = stopType;
|
||||
});
|
||||
userPreferencesService.stopType = stopType;
|
||||
}
|
||||
|
||||
void setThemeType(ThemeType themeType) {
|
||||
setState(() {
|
||||
this.themeType = themeType;
|
||||
});
|
||||
userPreferencesService.themeType = themeType;
|
||||
}
|
||||
|
||||
Brightness get _themeBrightness {
|
||||
switch (themeType) {
|
||||
case ThemeType.light:
|
||||
return Brightness.light;
|
||||
case ThemeType.dark:
|
||||
return Brightness.dark;
|
||||
case ThemeType.systemDefault:
|
||||
return SchedulerBinding.instance.platformDispatcher.platformBrightness;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum _Aspect {
|
||||
dynamicColorState,
|
||||
evSourceType,
|
||||
locale,
|
||||
stopType,
|
||||
theme,
|
||||
themeType,
|
||||
}
|
||||
|
||||
class _UserPreferencesModel extends InheritedModel<_Aspect> {
|
||||
final DynamicColorState dynamicColorState;
|
||||
final EvSourceType evSourceType;
|
||||
final SupportedLocale locale;
|
||||
final StopType stopType;
|
||||
final ThemeType themeType;
|
||||
|
||||
final Brightness _brightness;
|
||||
final Color _primaryColor;
|
||||
|
||||
const _UserPreferencesModel({
|
||||
required Brightness brightness,
|
||||
required this.dynamicColorState,
|
||||
required this.evSourceType,
|
||||
required this.locale,
|
||||
required Color primaryColor,
|
||||
required this.stopType,
|
||||
required this.themeType,
|
||||
required super.child,
|
||||
}) : _brightness = brightness,
|
||||
_primaryColor = primaryColor;
|
||||
|
||||
ThemeData get theme => themeFrom(_primaryColor, _brightness);
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(_UserPreferencesModel oldWidget) {
|
||||
return _brightness != oldWidget._brightness ||
|
||||
dynamicColorState != oldWidget.dynamicColorState ||
|
||||
evSourceType != oldWidget.evSourceType ||
|
||||
locale != oldWidget.locale ||
|
||||
_primaryColor != oldWidget._primaryColor ||
|
||||
stopType != oldWidget.stopType ||
|
||||
themeType != oldWidget.themeType;
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotifyDependent(
|
||||
_UserPreferencesModel oldWidget,
|
||||
Set<_Aspect> dependencies,
|
||||
) {
|
||||
return (dependencies.contains(_Aspect.dynamicColorState) &&
|
||||
dynamicColorState != oldWidget.dynamicColorState) ||
|
||||
(dependencies.contains(_Aspect.evSourceType) && evSourceType != oldWidget.evSourceType) ||
|
||||
(dependencies.contains(_Aspect.locale) && locale != oldWidget.locale) ||
|
||||
(dependencies.contains(_Aspect.stopType) && stopType != oldWidget.stopType) ||
|
||||
(dependencies.contains(_Aspect.theme) &&
|
||||
(_brightness != oldWidget._brightness || _primaryColor != oldWidget._primaryColor)) ||
|
||||
(dependencies.contains(_Aspect.themeType) && themeType != oldWidget.themeType);
|
||||
}
|
||||
}
|
||||
|
||||
class _MeteringScreenLayoutModel extends InheritedModel<MeteringScreenLayoutFeature> {
|
||||
final Map<MeteringScreenLayoutFeature, bool> data;
|
||||
|
||||
const _MeteringScreenLayoutModel({
|
||||
required this.data,
|
||||
required super.child,
|
||||
});
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(_MeteringScreenLayoutModel oldWidget) => oldWidget.data != data;
|
||||
|
||||
@override
|
||||
bool updateShouldNotifyDependent(
|
||||
_MeteringScreenLayoutModel oldWidget,
|
||||
Set<MeteringScreenLayoutFeature> dependencies,
|
||||
) {
|
||||
for (final dependecy in dependencies) {
|
||||
if (oldWidget.data[dependecy] != data[dependecy]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -14,7 +14,6 @@ class Dimens {
|
|||
static const double grid48 = 48;
|
||||
static const double grid56 = 56;
|
||||
static const double grid72 = 72;
|
||||
static const double grid168 = 168;
|
||||
|
||||
static const double paddingS = 8;
|
||||
static const double paddingM = 16;
|
||||
|
@ -25,10 +24,13 @@ class Dimens {
|
|||
static const Duration durationM = Duration(milliseconds: 200);
|
||||
static const Duration durationML = Duration(milliseconds: 250);
|
||||
static const Duration durationL = Duration(milliseconds: 300);
|
||||
static const Duration switchDuration = Duration(milliseconds: 100);
|
||||
|
||||
static const double enabledOpacity = 1.0;
|
||||
static const double disabledOpacity = 0.38;
|
||||
|
||||
static const double sliverAppBarExpandedHeight = 168;
|
||||
|
||||
// TopBar
|
||||
static const double readingContainerDoubleValueHeight = 128;
|
||||
static const double readingContainerSingleValueHeight = 76;
|
||||
|
|
97
lib/res/theme.dart
Normal file
97
lib/res/theme.dart
Normal file
|
@ -0,0 +1,97 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/res/dimens.dart';
|
||||
import 'package:material_color_utilities/material_color_utilities.dart';
|
||||
|
||||
const primaryColorsList = [
|
||||
Color(0xfff44336),
|
||||
Color(0xffe91e63),
|
||||
Color(0xff9c27b0),
|
||||
Color(0xff673ab7),
|
||||
Color(0xff3f51b5),
|
||||
Color(0xff2196f3),
|
||||
Color(0xff03a9f4),
|
||||
Color(0xff00bcd4),
|
||||
Color(0xff009688),
|
||||
Color(0xff4caf50),
|
||||
Color(0xff8bc34a),
|
||||
Color(0xffcddc39),
|
||||
Color(0xffffeb3b),
|
||||
Color(0xffffc107),
|
||||
Color(0xffff9800),
|
||||
Color(0xffff5722),
|
||||
];
|
||||
|
||||
ThemeData themeFrom(Color primaryColor, Brightness brightness) {
|
||||
final scheme = _colorSchemeFromColor(primaryColor, brightness);
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: scheme.brightness,
|
||||
primaryColor: primaryColor,
|
||||
colorScheme: scheme,
|
||||
appBarTheme: AppBarTheme(
|
||||
elevation: 4,
|
||||
color: scheme.surface,
|
||||
surfaceTintColor: scheme.surfaceTint,
|
||||
),
|
||||
cardTheme: CardTheme(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
color: scheme.surface,
|
||||
elevation: 4,
|
||||
margin: EdgeInsets.zero,
|
||||
shadowColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(Dimens.borderRadiusL)),
|
||||
surfaceTintColor: scheme.surfaceTint,
|
||||
),
|
||||
dialogBackgroundColor: scheme.surface,
|
||||
dialogTheme: DialogTheme(
|
||||
backgroundColor: scheme.surface,
|
||||
surfaceTintColor: scheme.surfaceTint,
|
||||
elevation: 6,
|
||||
),
|
||||
dividerColor: scheme.outlineVariant,
|
||||
dividerTheme: DividerThemeData(
|
||||
color: scheme.outlineVariant,
|
||||
space: 0,
|
||||
),
|
||||
listTileTheme: ListTileThemeData(
|
||||
style: ListTileStyle.list,
|
||||
iconColor: scheme.onSurface,
|
||||
textColor: scheme.onSurface,
|
||||
),
|
||||
scaffoldBackgroundColor: scheme.surface,
|
||||
);
|
||||
}
|
||||
|
||||
ColorScheme _colorSchemeFromColor(Color primaryColor, Brightness brightness) {
|
||||
final scheme = brightness == Brightness.light
|
||||
? Scheme.light(primaryColor.value)
|
||||
: Scheme.dark(primaryColor.value);
|
||||
|
||||
return ColorScheme(
|
||||
brightness: brightness,
|
||||
background: Color(scheme.background),
|
||||
error: Color(scheme.error),
|
||||
errorContainer: Color(scheme.errorContainer),
|
||||
onBackground: Color(scheme.onBackground),
|
||||
onError: Color(scheme.onError),
|
||||
onErrorContainer: Color(scheme.onErrorContainer),
|
||||
primary: Color(scheme.primary),
|
||||
onPrimary: Color(scheme.onPrimary),
|
||||
primaryContainer: Color(scheme.primaryContainer),
|
||||
onPrimaryContainer: Color(scheme.onPrimaryContainer),
|
||||
secondary: Color(scheme.secondary),
|
||||
onSecondary: Color(scheme.onSecondary),
|
||||
surface: Color.alphaBlend(
|
||||
Color(scheme.primary).withOpacity(0.05),
|
||||
Color(scheme.background),
|
||||
),
|
||||
onSurface: Color(scheme.onSurface),
|
||||
surfaceVariant: Color.alphaBlend(
|
||||
Color(scheme.primary).withOpacity(0.5),
|
||||
Color(scheme.background),
|
||||
),
|
||||
onSurfaceVariant: Color(scheme.onSurfaceVariant),
|
||||
outline: Color(scheme.outline),
|
||||
outlineVariant: Color(scheme.outlineVariant),
|
||||
);
|
||||
}
|
|
@ -3,7 +3,6 @@ import 'dart:async';
|
|||
import 'package:bloc_concurrency/bloc_concurrency.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:lightmeter/data/models/film.dart';
|
||||
import 'package:lightmeter/data/models/volume_action.dart';
|
||||
import 'package:lightmeter/interactors/metering_interactor.dart';
|
||||
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
|
||||
|
@ -29,7 +28,6 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
|
|||
) : super(
|
||||
MeteringDataState(
|
||||
ev100: null,
|
||||
film: _meteringInteractor.film,
|
||||
iso: _meteringInteractor.iso,
|
||||
nd: _meteringInteractor.ndFilter,
|
||||
isMetering: false,
|
||||
|
@ -42,7 +40,6 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
|
|||
.listen(onCommunicationState);
|
||||
|
||||
on<EquipmentProfileChangedEvent>(_onEquipmentProfileChanged);
|
||||
on<FilmChangedEvent>(_onFilmChanged);
|
||||
on<IsoChangedEvent>(_onIsoChanged);
|
||||
on<NdChangedEvent>(_onNdChanged);
|
||||
on<MeasureEvent>(_onMeasure, transformer: droppable());
|
||||
|
@ -92,12 +89,9 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
|
|||
/// Update selected ISO value and discard selected film, if selected equipment profile
|
||||
/// doesn't contain currently selected value
|
||||
IsoValue iso = state.iso;
|
||||
Film film = state.film;
|
||||
if (!event.equipmentProfileData.isoValues.any((v) => state.iso.value == v.value)) {
|
||||
_meteringInteractor.iso = event.equipmentProfileData.isoValues.first;
|
||||
iso = event.equipmentProfileData.isoValues.first;
|
||||
_meteringInteractor.film = Film.values.first;
|
||||
film = Film.values.first;
|
||||
willUpdateMeasurements = true;
|
||||
}
|
||||
|
||||
|
@ -113,7 +107,6 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
|
|||
emit(
|
||||
MeteringDataState(
|
||||
ev100: state.ev100,
|
||||
film: film,
|
||||
iso: iso,
|
||||
nd: nd,
|
||||
isMetering: state.isMetering,
|
||||
|
@ -122,46 +115,12 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
|
|||
}
|
||||
}
|
||||
|
||||
void _onFilmChanged(FilmChangedEvent event, Emitter emit) {
|
||||
if (state.film.name != event.film.name) {
|
||||
_meteringInteractor.film = event.film;
|
||||
|
||||
/// Find `IsoValue` with matching value
|
||||
IsoValue iso = state.iso;
|
||||
if (state.iso.value != event.film.iso && event.film != const Film.other()) {
|
||||
iso = IsoValue.values.firstWhere(
|
||||
(e) => e.value == event.film.iso,
|
||||
orElse: () => state.iso,
|
||||
);
|
||||
_meteringInteractor.iso = iso;
|
||||
}
|
||||
|
||||
/// If user selects 'Other' film we preserve currently selected ISO
|
||||
/// and therefore only discard reciprocity formula
|
||||
emit(
|
||||
MeteringDataState(
|
||||
ev100: state.ev100,
|
||||
film: event.film,
|
||||
iso: iso,
|
||||
nd: state.nd,
|
||||
isMetering: state.isMetering,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onIsoChanged(IsoChangedEvent event, Emitter emit) {
|
||||
/// Discard currently selected film even if ISO is the same,
|
||||
/// because, for example, Fomapan 400 and any Ilford 400
|
||||
/// have different reciprocity formulas
|
||||
_meteringInteractor.film = Film.values.first;
|
||||
|
||||
if (state.iso != event.isoValue) {
|
||||
_meteringInteractor.iso = event.isoValue;
|
||||
emit(
|
||||
MeteringDataState(
|
||||
ev100: state.ev100,
|
||||
film: Film.values.first,
|
||||
iso: event.isoValue,
|
||||
nd: state.nd,
|
||||
isMetering: state.isMetering,
|
||||
|
@ -176,7 +135,6 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
|
|||
emit(
|
||||
MeteringDataState(
|
||||
ev100: state.ev100,
|
||||
film: state.film,
|
||||
iso: state.iso,
|
||||
nd: event.ndValue,
|
||||
isMetering: state.isMetering,
|
||||
|
@ -190,7 +148,6 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
|
|||
_communicationBloc.add(const communication_events.MeasureEvent());
|
||||
emit(
|
||||
LoadingState(
|
||||
film: state.film,
|
||||
iso: state.iso,
|
||||
nd: state.nd,
|
||||
),
|
||||
|
@ -209,7 +166,6 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
|
|||
emit(
|
||||
MeteringDataState(
|
||||
ev100: event.ev100,
|
||||
film: state.film,
|
||||
iso: state.iso,
|
||||
nd: state.nd,
|
||||
isMetering: event.isMetering,
|
||||
|
@ -221,7 +177,6 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
|
|||
emit(
|
||||
MeteringDataState(
|
||||
ev100: null,
|
||||
film: state.film,
|
||||
iso: state.iso,
|
||||
nd: state.nd,
|
||||
isMetering: event.isMetering,
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/data/models/ev_source_type.dart';
|
||||
import 'package:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
||||
import 'package:lightmeter/res/dimens.dart';
|
||||
import 'package:lightmeter/screens/metering/components/bottom_controls/components/measure_button/widget_button_measure.dart';
|
||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||
|
||||
class MeteringBottomControls extends StatelessWidget {
|
||||
final double? ev;
|
||||
|
@ -42,10 +43,14 @@ class MeteringBottomControls extends StatelessWidget {
|
|||
child: IconButton(
|
||||
onPressed: onSwitchEvSourceType,
|
||||
icon: Icon(
|
||||
context.listen<EvSourceType>() != EvSourceType.camera
|
||||
UserPreferencesProvider.evSourceTypeOf(context) != EvSourceType.camera
|
||||
? Icons.camera_rear
|
||||
: Icons.wb_incandescent,
|
||||
),
|
||||
tooltip:
|
||||
UserPreferencesProvider.evSourceTypeOf(context) != EvSourceType.camera
|
||||
? S.of(context).tooltipUseCamera
|
||||
: S.of(context).tooltipUseLightSensor,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
@ -61,6 +66,7 @@ class MeteringBottomControls extends StatelessWidget {
|
|||
child: IconButton(
|
||||
onPressed: onSettings,
|
||||
icon: const Icon(Icons.settings),
|
||||
tooltip: S.of(context).tooltipOpenSettings,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -2,13 +2,14 @@ import 'dart:async';
|
|||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:exif/exif.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:lightmeter/interactors/metering_interactor.dart';
|
||||
import 'package:lightmeter/platform_config.dart';
|
||||
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
|
||||
import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart'
|
||||
as communication_event;
|
||||
|
@ -18,7 +19,7 @@ import 'package:lightmeter/screens/metering/components/camera_container/event_co
|
|||
import 'package:lightmeter/screens/metering/components/camera_container/models/camera_error_type.dart';
|
||||
import 'package:lightmeter/screens/metering/components/camera_container/state_container_camera.dart';
|
||||
import 'package:lightmeter/screens/metering/components/shared/ev_source_base/bloc_base_ev_source.dart';
|
||||
import 'package:lightmeter/utils/log_2.dart';
|
||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||
|
||||
class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraContainerState> {
|
||||
final MeteringInteractor _meteringInteractor;
|
||||
|
@ -32,7 +33,7 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
|
|||
|
||||
static const _exposureMaxRange = RangeValues(-4, 4);
|
||||
RangeValues? _exposureOffsetRange;
|
||||
double _exposureStep = 0.0;
|
||||
double _exposureStep = 0.1;
|
||||
double _currentExposureOffset = 0.0;
|
||||
|
||||
double? _ev100 = 0.0;
|
||||
|
@ -123,7 +124,7 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
|
|||
(camera) => camera.lensDirection == CameraLensDirection.back,
|
||||
orElse: () => cameras.last,
|
||||
),
|
||||
ResolutionPreset.medium,
|
||||
ResolutionPreset.low,
|
||||
enableAudio: false,
|
||||
);
|
||||
|
||||
|
@ -199,15 +200,28 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
|
|||
);
|
||||
}
|
||||
|
||||
bool get _canTakePhoto => !(_cameraController == null ||
|
||||
!_cameraController!.value.isInitialized ||
|
||||
_cameraController!.value.isTakingPicture);
|
||||
bool get _canTakePhoto =>
|
||||
PlatformConfig.cameraStubImage.isNotEmpty ||
|
||||
!(_cameraController == null ||
|
||||
!_cameraController!.value.isInitialized ||
|
||||
_cameraController!.value.isTakingPicture);
|
||||
|
||||
Future<double?> _takePhoto() async {
|
||||
try {
|
||||
final file = await _cameraController!.takePicture();
|
||||
final Uint8List bytes = await file.readAsBytes();
|
||||
Directory(file.path).deleteSync(recursive: true);
|
||||
// https://github.com/flutter/flutter/issues/84957#issuecomment-1661155095
|
||||
|
||||
late final Uint8List bytes;
|
||||
if (PlatformConfig.cameraStubImage.isNotEmpty) {
|
||||
bytes = (await rootBundle.load(PlatformConfig.cameraStubImage)).buffer.asUint8List();
|
||||
} else {
|
||||
await _cameraController!.setFocusMode(FocusMode.locked);
|
||||
await _cameraController!.setExposureMode(ExposureMode.locked);
|
||||
final file = await _cameraController!.takePicture();
|
||||
await _cameraController!.setFocusMode(FocusMode.auto);
|
||||
await _cameraController!.setExposureMode(ExposureMode.auto);
|
||||
bytes = await file.readAsBytes();
|
||||
Directory(file.path).deleteSync(recursive: true);
|
||||
}
|
||||
|
||||
final tags = await readExifFromBytes(bytes);
|
||||
final iso = double.tryParse("${tags["EXIF ISOSpeedRatings"]}");
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/res/dimens.dart';
|
||||
import 'package:lightmeter/screens/shared/centered_slider/widget_slider_centered.dart';
|
||||
import 'package:lightmeter/utils/to_string_signed.dart';
|
||||
|
@ -22,6 +23,7 @@ class ExposureOffsetSlider extends StatelessWidget {
|
|||
IconButton(
|
||||
icon: const Icon(Icons.sync),
|
||||
onPressed: value != 0.0 ? () => onChanged(0.0) : null,
|
||||
tooltip: S.of(context).tooltipResetToZero,
|
||||
),
|
||||
Expanded(
|
||||
child: Row(
|
||||
|
|
|
@ -14,10 +14,12 @@ class CameraView extends StatelessWidget {
|
|||
valueListenable: controller,
|
||||
builder: (_, __, ___) => AspectRatio(
|
||||
aspectRatio: _isLandscape(value) ? value.aspectRatio : (1 / value.aspectRatio),
|
||||
child: RotatedBox(
|
||||
quarterTurns: _getQuarterTurns(value),
|
||||
child: controller.buildPreview(),
|
||||
),
|
||||
child: value.isInitialized
|
||||
? RotatedBox(
|
||||
quarterTurns: _getQuarterTurns(value),
|
||||
child: controller.buildPreview(),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
);
|
||||
}
|
|
@ -10,10 +10,9 @@ class CameraViewPlaceholder extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
color: error != null ? null : Colors.black,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(Dimens.borderRadiusM)),
|
||||
child: Center(
|
||||
child: error != null ? const Icon(Icons.no_photography) : const CircularProgressIndicator(),
|
||||
),
|
||||
child: Center(child: error != null ? const Icon(Icons.no_photography) : null),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/res/dimens.dart';
|
||||
|
||||
class CameraHistogram extends StatefulWidget {
|
||||
final CameraController controller;
|
||||
|
||||
const CameraHistogram({required this.controller, super.key});
|
||||
|
||||
@override
|
||||
_CameraHistogramState createState() => _CameraHistogramState();
|
||||
}
|
||||
|
||||
class _CameraHistogramState extends State<CameraHistogram> {
|
||||
List<int> histogramR = List.filled(256, 0);
|
||||
List<int> histogramG = List.filled(256, 0);
|
||||
List<int> histogramB = List.filled(256, 0);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_startImageStream();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
/// There is no need to stop image stream here,
|
||||
/// because this widget will be disposed when CameraController is disposed
|
||||
/// widget.controller.stopImageStream();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
HistogramChannel(
|
||||
color: Colors.red,
|
||||
values: histogramR,
|
||||
),
|
||||
const SizedBox(height: Dimens.grid4),
|
||||
HistogramChannel(
|
||||
color: Colors.green,
|
||||
values: histogramG,
|
||||
),
|
||||
const SizedBox(height: Dimens.grid4),
|
||||
HistogramChannel(
|
||||
color: Colors.blue,
|
||||
values: histogramB,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _startImageStream() {
|
||||
widget.controller.startImageStream((CameraImage image) {
|
||||
histogramR = List.filled(256, 0);
|
||||
histogramG = List.filled(256, 0);
|
||||
histogramB = List.filled(256, 0);
|
||||
|
||||
final int uvRowStride = image.planes[1].bytesPerRow;
|
||||
final int uvPixelStride = image.planes[1].bytesPerPixel!;
|
||||
|
||||
for (int x = 0; x < image.width; x++) {
|
||||
for (int y = 0; y < image.height; y++) {
|
||||
final int uvIndex = uvPixelStride * (x / 2).floor() + uvRowStride * (y / 2).floor();
|
||||
final int index = y * image.width + x;
|
||||
|
||||
final yp = image.planes[0].bytes[index];
|
||||
final up = image.planes[1].bytes[uvIndex];
|
||||
final vp = image.planes[2].bytes[uvIndex];
|
||||
|
||||
final r = yp + vp * 1436 / 1024 - 179;
|
||||
final g = yp - up * 46549 / 131072 + 44 - vp * 93604 / 131072 + 91;
|
||||
final b = yp + up * 1814 / 1024 - 227;
|
||||
|
||||
histogramR[r.round().clamp(0, 255)]++;
|
||||
histogramG[g.round().clamp(0, 255)]++;
|
||||
histogramB[b.round().clamp(0, 255)]++;
|
||||
}
|
||||
}
|
||||
|
||||
if (mounted) setState(() {});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class HistogramChannel extends StatelessWidget {
|
||||
final List<int> values;
|
||||
final Color color;
|
||||
|
||||
final int _maxOccurences;
|
||||
|
||||
HistogramChannel({
|
||||
required this.values,
|
||||
required this.color,
|
||||
super.key,
|
||||
}) : _maxOccurences = values.reduce((value, element) => max(value, element));
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final pixelWidth = constraints.maxWidth / values.length;
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: Dimens.grid16,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: values
|
||||
.map(
|
||||
(e) => SizedBox(
|
||||
height: _maxOccurences == 0 ? 0 : Dimens.grid16 * (e / _maxOccurences),
|
||||
width: pixelWidth,
|
||||
child: ColoredBox(color: color),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
|
||||
import 'package:lightmeter/platform_config.dart';
|
||||
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
||||
import 'package:lightmeter/res/dimens.dart';
|
||||
import 'package:lightmeter/screens/metering/components/camera_container/components/camera_preview/components/camera_view/widget_camera_view.dart';
|
||||
import 'package:lightmeter/screens/metering/components/camera_container/components/camera_preview/components/camera_view_placeholder/widget_placeholder_camera_view.dart';
|
||||
import 'package:lightmeter/screens/metering/components/camera_container/components/camera_preview/components/histogram/widget_histogram.dart';
|
||||
import 'package:lightmeter/screens/metering/components/camera_container/models/camera_error_type.dart';
|
||||
|
||||
class CameraPreview extends StatefulWidget {
|
||||
final CameraController? controller;
|
||||
final CameraErrorType? error;
|
||||
|
||||
const CameraPreview({this.controller, this.error, super.key});
|
||||
|
||||
@override
|
||||
State<CameraPreview> createState() => _CameraPreviewState();
|
||||
}
|
||||
|
||||
class _CameraPreviewState extends State<CameraPreview> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AspectRatio(
|
||||
aspectRatio: PlatformConfig.cameraPreviewAspectRatio,
|
||||
child: Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
const CameraViewPlaceholder(error: null),
|
||||
AnimatedSwitcher(
|
||||
duration: Dimens.switchDuration,
|
||||
child: widget.controller != null
|
||||
? _CameraPreviewBuilder(controller: widget.controller!)
|
||||
: CameraViewPlaceholder(error: widget.error),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CameraPreviewBuilder extends StatefulWidget {
|
||||
final CameraController controller;
|
||||
|
||||
const _CameraPreviewBuilder({required this.controller});
|
||||
|
||||
@override
|
||||
State<_CameraPreviewBuilder> createState() => _CameraPreviewBuilderState();
|
||||
}
|
||||
|
||||
class _CameraPreviewBuilderState extends State<_CameraPreviewBuilder> {
|
||||
late final ValueNotifier<bool> _initializedNotifier =
|
||||
ValueNotifier<bool>(widget.controller.value.isInitialized);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.controller.addListener(_update);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.controller.removeListener(_update);
|
||||
_initializedNotifier.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (PlatformConfig.cameraStubImage.isNotEmpty) {
|
||||
return Image.asset(PlatformConfig.cameraStubImage);
|
||||
}
|
||||
return ValueListenableBuilder<bool>(
|
||||
valueListenable: _initializedNotifier,
|
||||
builder: (context, value, child) => value
|
||||
? Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
CameraView(controller: widget.controller),
|
||||
if (UserPreferencesProvider.meteringScreenFeatureOf(
|
||||
context,
|
||||
MeteringScreenLayoutFeature.histogram,
|
||||
))
|
||||
Positioned(
|
||||
left: Dimens.grid8,
|
||||
right: Dimens.grid8,
|
||||
bottom: Dimens.grid16,
|
||||
child: CameraHistogram(controller: widget.controller),
|
||||
),
|
||||
],
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
);
|
||||
}
|
||||
|
||||
void _update() {
|
||||
_initializedNotifier.value = widget.controller.value.isInitialized;
|
||||
}
|
||||
}
|
|
@ -1,22 +1,18 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:lightmeter/data/models/exposure_pair.dart';
|
||||
import 'package:lightmeter/data/models/film.dart';
|
||||
import 'package:lightmeter/interactors/metering_interactor.dart';
|
||||
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
|
||||
import 'package:lightmeter/screens/metering/components/camera_container/bloc_container_camera.dart';
|
||||
import 'package:lightmeter/screens/metering/components/camera_container/event_container_camera.dart';
|
||||
import 'package:lightmeter/screens/metering/components/camera_container/widget_container_camera.dart';
|
||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||
import 'package:lightmeter/screens/metering/flow_metering.dart';
|
||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||
|
||||
class CameraContainerProvider extends StatelessWidget {
|
||||
final ExposurePair? fastest;
|
||||
final ExposurePair? slowest;
|
||||
final Film film;
|
||||
final IsoValue iso;
|
||||
final NdValue nd;
|
||||
final ValueChanged<Film> onFilmChanged;
|
||||
final ValueChanged<IsoValue> onIsoChanged;
|
||||
final ValueChanged<NdValue> onNdChanged;
|
||||
final List<ExposurePair> exposurePairs;
|
||||
|
@ -24,10 +20,8 @@ class CameraContainerProvider extends StatelessWidget {
|
|||
const CameraContainerProvider({
|
||||
required this.fastest,
|
||||
required this.slowest,
|
||||
required this.film,
|
||||
required this.iso,
|
||||
required this.nd,
|
||||
required this.onFilmChanged,
|
||||
required this.onIsoChanged,
|
||||
required this.onNdChanged,
|
||||
required this.exposurePairs,
|
||||
|
@ -39,16 +33,14 @@ class CameraContainerProvider extends StatelessWidget {
|
|||
return BlocProvider(
|
||||
lazy: false,
|
||||
create: (context) => CameraContainerBloc(
|
||||
context.get<MeteringInteractor>(),
|
||||
MeteringInteractorProvider.of(context),
|
||||
context.read<MeteringCommunicationBloc>(),
|
||||
)..add(const RequestPermissionEvent()),
|
||||
child: CameraContainer(
|
||||
fastest: fastest,
|
||||
slowest: slowest,
|
||||
film: film,
|
||||
iso: iso,
|
||||
nd: nd,
|
||||
onFilmChanged: onFilmChanged,
|
||||
onIsoChanged: onIsoChanged,
|
||||
onNdChanged: onNdChanged,
|
||||
exposurePairs: exposurePairs,
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:lightmeter/data/models/exposure_pair.dart';
|
||||
import 'package:lightmeter/data/models/film.dart';
|
||||
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
|
||||
import 'package:lightmeter/features.dart';
|
||||
import 'package:lightmeter/platform_config.dart';
|
||||
import 'package:lightmeter/providers/metering_screen_layout_provider.dart';
|
||||
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
||||
import 'package:lightmeter/res/dimens.dart';
|
||||
import 'package:lightmeter/screens/metering/components/camera_container/bloc_container_camera.dart';
|
||||
import 'package:lightmeter/screens/metering/components/camera_container/components/camera_controls/widget_camera_controls.dart';
|
||||
import 'package:lightmeter/screens/metering/components/camera_container/components/camera_controls_placeholder/widget_placeholder_camera_controls.dart';
|
||||
import 'package:lightmeter/screens/metering/components/camera_container/components/camera_view/widget_camera_view.dart';
|
||||
import 'package:lightmeter/screens/metering/components/camera_container/components/camera_view_placeholder/widget_placeholder_camera_view.dart';
|
||||
import 'package:lightmeter/screens/metering/components/camera_container/components/camera_preview/widget_camera_preview.dart';
|
||||
import 'package:lightmeter/screens/metering/components/camera_container/event_container_camera.dart';
|
||||
import 'package:lightmeter/screens/metering/components/camera_container/models/camera_error_type.dart';
|
||||
import 'package:lightmeter/screens/metering/components/camera_container/state_container_camera.dart';
|
||||
|
@ -23,10 +22,8 @@ import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
|||
class CameraContainer extends StatelessWidget {
|
||||
final ExposurePair? fastest;
|
||||
final ExposurePair? slowest;
|
||||
final Film film;
|
||||
final IsoValue iso;
|
||||
final NdValue nd;
|
||||
final ValueChanged<Film> onFilmChanged;
|
||||
final ValueChanged<IsoValue> onIsoChanged;
|
||||
final ValueChanged<NdValue> onNdChanged;
|
||||
final List<ExposurePair> exposurePairs;
|
||||
|
@ -34,10 +31,8 @@ class CameraContainer extends StatelessWidget {
|
|||
const CameraContainer({
|
||||
required this.fastest,
|
||||
required this.slowest,
|
||||
required this.film,
|
||||
required this.iso,
|
||||
required this.nd,
|
||||
required this.onFilmChanged,
|
||||
required this.onIsoChanged,
|
||||
required this.onNdChanged,
|
||||
required this.exposurePairs,
|
||||
|
@ -46,59 +41,95 @@ class CameraContainer extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final double cameraViewHeight =
|
||||
((MediaQuery.of(context).size.width - Dimens.grid8 - 2 * Dimens.paddingM) / 2) /
|
||||
PlatformConfig.cameraPreviewAspectRatio;
|
||||
final double meteringContainerHeight = _meteringContainerHeight(context);
|
||||
final double cameraPreviewHeight = _cameraPreviewHeight(context);
|
||||
final double topBarOverflow = meteringContainerHeight - cameraPreviewHeight;
|
||||
|
||||
double topBarOverflow = Dimens.readingContainerSingleValueHeight + // ISO & ND
|
||||
-cameraViewHeight;
|
||||
if (FeaturesConfig.equipmentProfilesEnabled) {
|
||||
topBarOverflow += Dimens.readingContainerSingleValueHeight;
|
||||
topBarOverflow += Dimens.paddingS;
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: MeteringTopBar(
|
||||
readingsContainer: ReadingsContainer(
|
||||
fastest: fastest,
|
||||
slowest: slowest,
|
||||
iso: iso,
|
||||
nd: nd,
|
||||
onIsoChanged: onIsoChanged,
|
||||
onNdChanged: onNdChanged,
|
||||
),
|
||||
appendixHeight: topBarOverflow,
|
||||
preview: const _CameraViewBuilder(),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
bottom: false,
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: min(meteringContainerHeight, cameraPreviewHeight) + Dimens.paddingM * 2,
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: topBarOverflow >= 0 ? topBarOverflow : 0),
|
||||
child: ExposurePairsList(exposurePairs),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: Dimens.grid8),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: topBarOverflow <= 0 ? -topBarOverflow : 0),
|
||||
child: const _CameraControlsBuilder(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
double _meteringContainerHeight(BuildContext context) {
|
||||
double enabledFeaturesHeight = 0;
|
||||
if (UserPreferencesProvider.meteringScreenFeatureOf(
|
||||
context,
|
||||
MeteringScreenLayoutFeature.equipmentProfiles,
|
||||
)) {
|
||||
enabledFeaturesHeight += Dimens.readingContainerSingleValueHeight;
|
||||
enabledFeaturesHeight += Dimens.paddingS;
|
||||
}
|
||||
if (MeteringScreenLayout.featureOf(
|
||||
if (UserPreferencesProvider.meteringScreenFeatureOf(
|
||||
context,
|
||||
MeteringScreenLayoutFeature.extremeExposurePairs,
|
||||
)) {
|
||||
topBarOverflow += Dimens.readingContainerDoubleValueHeight;
|
||||
topBarOverflow += Dimens.paddingS;
|
||||
enabledFeaturesHeight += Dimens.readingContainerDoubleValueHeight;
|
||||
enabledFeaturesHeight += Dimens.paddingS;
|
||||
}
|
||||
if (MeteringScreenLayout.featureOf(
|
||||
if (UserPreferencesProvider.meteringScreenFeatureOf(
|
||||
context,
|
||||
MeteringScreenLayoutFeature.filmPicker,
|
||||
)) {
|
||||
topBarOverflow += Dimens.readingContainerSingleValueHeight;
|
||||
topBarOverflow += Dimens.paddingS;
|
||||
enabledFeaturesHeight += Dimens.readingContainerSingleValueHeight;
|
||||
enabledFeaturesHeight += Dimens.paddingS;
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
MeteringTopBar(
|
||||
readingsContainer: ReadingsContainer(
|
||||
fastest: fastest,
|
||||
slowest: slowest,
|
||||
film: film,
|
||||
iso: iso,
|
||||
nd: nd,
|
||||
onFilmChanged: onFilmChanged,
|
||||
onIsoChanged: onIsoChanged,
|
||||
onNdChanged: onNdChanged,
|
||||
),
|
||||
appendixHeight: topBarOverflow,
|
||||
preview: const _CameraViewBuilder(),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
|
||||
child: _MiddleContentWrapper(
|
||||
topBarOverflow: topBarOverflow,
|
||||
leftContent: ExposurePairsList(exposurePairs),
|
||||
rightContent: const _CameraControlsBuilder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
return enabledFeaturesHeight + Dimens.readingContainerSingleValueHeight; // ISO & ND
|
||||
}
|
||||
|
||||
double _cameraPreviewHeight(BuildContext context) {
|
||||
return ((MediaQuery.sizeOf(context).width - Dimens.grid8 - 2 * Dimens.paddingM) / 2) /
|
||||
PlatformConfig.cameraPreviewAspectRatio;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -107,20 +138,11 @@ class _CameraViewBuilder extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AspectRatio(
|
||||
aspectRatio: PlatformConfig.cameraPreviewAspectRatio,
|
||||
child: BlocBuilder<CameraContainerBloc, CameraContainerState>(
|
||||
buildWhen: (previous, current) =>
|
||||
current is CameraLoadingState ||
|
||||
current is CameraInitializedState ||
|
||||
current is CameraErrorState,
|
||||
builder: (context, state) {
|
||||
if (state is CameraInitializedState) {
|
||||
return Center(child: CameraView(controller: state.controller));
|
||||
} else {
|
||||
return CameraViewPlaceholder(error: state is CameraErrorState ? state.error : null);
|
||||
}
|
||||
},
|
||||
return BlocBuilder<CameraContainerBloc, CameraContainerState>(
|
||||
buildWhen: (previous, current) => current is! CameraActiveState,
|
||||
builder: (context, state) => CameraPreview(
|
||||
controller: state is CameraInitializedState ? state.controller : null,
|
||||
error: state is CameraErrorState ? state.error : null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -161,11 +183,13 @@ class _CameraControlsBuilder extends StatelessWidget {
|
|||
},
|
||||
);
|
||||
} else {
|
||||
child = const SizedBox.shrink();
|
||||
child = const Column(
|
||||
children: [Expanded(child: SizedBox.shrink())],
|
||||
);
|
||||
}
|
||||
|
||||
return AnimatedSwitcher(
|
||||
duration: Dimens.durationS,
|
||||
duration: Dimens.switchDuration,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
|
@ -173,43 +197,3 @@ class _CameraControlsBuilder extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MiddleContentWrapper extends StatelessWidget {
|
||||
final double topBarOverflow;
|
||||
final Widget leftContent;
|
||||
final Widget rightContent;
|
||||
|
||||
const _MiddleContentWrapper({
|
||||
required this.topBarOverflow,
|
||||
required this.leftContent,
|
||||
required this.rightContent,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) => OverflowBox(
|
||||
alignment: Alignment.bottomCenter,
|
||||
maxHeight: constraints.maxHeight + topBarOverflow.abs(),
|
||||
maxWidth: constraints.maxWidth,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: topBarOverflow >= 0 ? topBarOverflow : 0),
|
||||
child: leftContent,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: Dimens.grid8),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: topBarOverflow <= 0 ? -topBarOverflow : 0),
|
||||
child: rightContent,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import 'package:lightmeter/screens/metering/communication/state_communication_me
|
|||
import 'package:lightmeter/screens/metering/components/light_sensor_container/event_container_light_sensor.dart';
|
||||
import 'package:lightmeter/screens/metering/components/light_sensor_container/state_container_light_sensor.dart';
|
||||
import 'package:lightmeter/screens/metering/components/shared/ev_source_base/bloc_base_ev_source.dart';
|
||||
import 'package:lightmeter/utils/log_2.dart';
|
||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||
|
||||
class LightSensorContainerBloc
|
||||
extends EvSourceBlocBase<LightSensorContainerEvent, LightSensorContainerState> {
|
||||
|
|
|
@ -1,21 +1,17 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:lightmeter/data/models/exposure_pair.dart';
|
||||
import 'package:lightmeter/data/models/film.dart';
|
||||
import 'package:lightmeter/interactors/metering_interactor.dart';
|
||||
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
|
||||
import 'package:lightmeter/screens/metering/components/light_sensor_container/bloc_container_light_sensor.dart';
|
||||
import 'package:lightmeter/screens/metering/components/light_sensor_container/widget_container_light_sensor.dart';
|
||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||
import 'package:lightmeter/screens/metering/flow_metering.dart';
|
||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||
|
||||
class LightSensorContainerProvider extends StatelessWidget {
|
||||
final ExposurePair? fastest;
|
||||
final ExposurePair? slowest;
|
||||
final Film film;
|
||||
final IsoValue iso;
|
||||
final NdValue nd;
|
||||
final ValueChanged<Film> onFilmChanged;
|
||||
final ValueChanged<IsoValue> onIsoChanged;
|
||||
final ValueChanged<NdValue> onNdChanged;
|
||||
final List<ExposurePair> exposurePairs;
|
||||
|
@ -23,10 +19,8 @@ class LightSensorContainerProvider extends StatelessWidget {
|
|||
const LightSensorContainerProvider({
|
||||
required this.fastest,
|
||||
required this.slowest,
|
||||
required this.film,
|
||||
required this.iso,
|
||||
required this.nd,
|
||||
required this.onFilmChanged,
|
||||
required this.onIsoChanged,
|
||||
required this.onNdChanged,
|
||||
required this.exposurePairs,
|
||||
|
@ -38,16 +32,14 @@ class LightSensorContainerProvider extends StatelessWidget {
|
|||
return BlocProvider(
|
||||
lazy: false,
|
||||
create: (context) => LightSensorContainerBloc(
|
||||
context.get<MeteringInteractor>(),
|
||||
MeteringInteractorProvider.of(context),
|
||||
context.read<MeteringCommunicationBloc>(),
|
||||
),
|
||||
child: LightSensorContainer(
|
||||
fastest: fastest,
|
||||
slowest: slowest,
|
||||
film: film,
|
||||
iso: iso,
|
||||
nd: nd,
|
||||
onFilmChanged: onFilmChanged,
|
||||
onIsoChanged: onIsoChanged,
|
||||
onNdChanged: onNdChanged,
|
||||
exposurePairs: exposurePairs,
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/data/models/exposure_pair.dart';
|
||||
import 'package:lightmeter/data/models/film.dart';
|
||||
import 'package:lightmeter/res/dimens.dart';
|
||||
import 'package:lightmeter/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart';
|
||||
import 'package:lightmeter/screens/metering/components/shared/metering_top_bar/widget_top_bar_metering.dart';
|
||||
|
@ -10,10 +9,8 @@ import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
|||
class LightSensorContainer extends StatelessWidget {
|
||||
final ExposurePair? fastest;
|
||||
final ExposurePair? slowest;
|
||||
final Film film;
|
||||
final IsoValue iso;
|
||||
final NdValue nd;
|
||||
final ValueChanged<Film> onFilmChanged;
|
||||
final ValueChanged<IsoValue> onIsoChanged;
|
||||
final ValueChanged<NdValue> onNdChanged;
|
||||
final List<ExposurePair> exposurePairs;
|
||||
|
@ -21,10 +18,8 @@ class LightSensorContainer extends StatelessWidget {
|
|||
const LightSensorContainer({
|
||||
required this.fastest,
|
||||
required this.slowest,
|
||||
required this.film,
|
||||
required this.iso,
|
||||
required this.nd,
|
||||
required this.onFilmChanged,
|
||||
required this.onIsoChanged,
|
||||
required this.onNdChanged,
|
||||
required this.exposurePairs,
|
||||
|
@ -39,10 +34,8 @@ class LightSensorContainer extends StatelessWidget {
|
|||
readingsContainer: ReadingsContainer(
|
||||
fastest: fastest,
|
||||
slowest: slowest,
|
||||
film: film,
|
||||
iso: iso,
|
||||
nd: nd,
|
||||
onFilmChanged: onFilmChanged,
|
||||
onIsoChanged: onIsoChanged,
|
||||
onNdChanged: onNdChanged,
|
||||
),
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/data/models/exposure_pair.dart';
|
||||
import 'package:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/res/dimens.dart';
|
||||
|
||||
import 'package:lightmeter/screens/metering/components/shared/exposure_pairs_list/components/empty_exposure_pairs_list/widget_list_exposure_pairs_empty.dart';
|
||||
import 'package:lightmeter/screens/metering/components/shared/exposure_pairs_list/components/exposure_pairs_list_item/widget_item_list_exposure_pairs.dart';
|
||||
import 'package:lightmeter/screens/shared/icon_placeholder/widget_icon_placeholder.dart';
|
||||
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
||||
|
||||
class ExposurePairsList extends StatelessWidget {
|
||||
final List<ExposurePair> exposurePairs;
|
||||
|
@ -12,70 +14,76 @@ class ExposurePairsList extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (exposurePairs.isEmpty) {
|
||||
return const EmptyExposurePairsList();
|
||||
}
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: ListView.builder(
|
||||
key: ValueKey(exposurePairs.hashCode),
|
||||
padding: const EdgeInsets.symmetric(vertical: Dimens.paddingL),
|
||||
itemCount: exposurePairs.length,
|
||||
itemBuilder: (_, index) => Stack(
|
||||
return AnimatedSwitcher(
|
||||
duration: Dimens.switchDuration,
|
||||
child: exposurePairs.isEmpty
|
||||
? IconPlaceholder(
|
||||
icon: Icons.not_interested,
|
||||
text: S.of(context).noExposurePairs,
|
||||
)
|
||||
: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: ExposurePairsListItem(
|
||||
exposurePairs[index].aperture,
|
||||
tickOnTheLeft: false,
|
||||
Positioned.fill(
|
||||
child: ListView.builder(
|
||||
key: ValueKey(exposurePairs.hashCode),
|
||||
padding: const EdgeInsets.symmetric(vertical: Dimens.paddingL),
|
||||
itemCount: exposurePairs.length,
|
||||
itemBuilder: (_, index) => Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: ExposurePairsListItem(
|
||||
exposurePairs[index].aperture,
|
||||
tickOnTheLeft: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: ExposurePairsListItem(
|
||||
Films.selectedOf(context)
|
||||
.reciprocityFailure(exposurePairs[index].shutterSpeed),
|
||||
tickOnTheLeft: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: ExposurePairsListItem(
|
||||
exposurePairs[index].shutterSpeed,
|
||||
tickOnTheLeft: true,
|
||||
Positioned(
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) => Align(
|
||||
alignment: index == 0
|
||||
? Alignment.bottomCenter
|
||||
: (index == exposurePairs.length - 1
|
||||
? Alignment.topCenter
|
||||
: Alignment.center),
|
||||
child: SizedBox(
|
||||
height: index == 0 || index == exposurePairs.length - 1
|
||||
? constraints.maxHeight / 2
|
||||
: constraints.maxHeight,
|
||||
child: ColoredBox(
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
child: const SizedBox(width: 1),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) => Align(
|
||||
alignment: index == 0
|
||||
? Alignment.bottomCenter
|
||||
: (index == exposurePairs.length - 1
|
||||
? Alignment.topCenter
|
||||
: Alignment.center),
|
||||
child: SizedBox(
|
||||
height: index == 0 || index == exposurePairs.length - 1
|
||||
? constraints.maxHeight / 2
|
||||
: constraints.maxHeight,
|
||||
child: ColoredBox(
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
child: const SizedBox(width: 1),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,10 +31,23 @@ class MeteringTopBarShape extends CustomPainter {
|
|||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint()..color = color;
|
||||
final path = Path();
|
||||
const circularRadius = Radius.circular(Dimens.borderRadiusL);
|
||||
late final Path path;
|
||||
if (appendixHeight == 0 || appendixWidth == 0) {
|
||||
path.addRRect(
|
||||
path = _drawNoAppendix(size, Dimens.borderRadiusL);
|
||||
} else if (appendixHeight < 0) {
|
||||
path = _drawAppendixOnLeft(size, Dimens.borderRadiusL);
|
||||
canvas.scale(-1, 1);
|
||||
canvas.translate(-size.width, 0);
|
||||
} else {
|
||||
path = _drawAppendixOnLeft(size, Dimens.borderRadiusL);
|
||||
}
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
Path _drawNoAppendix(Size size, double bottomRadius) {
|
||||
final circularRadius = Radius.circular(bottomRadius);
|
||||
return Path()
|
||||
..addRRect(
|
||||
RRect.fromLTRBAndCorners(
|
||||
0,
|
||||
0,
|
||||
|
@ -43,73 +56,51 @@ class MeteringTopBarShape extends CustomPainter {
|
|||
bottomLeft: circularRadius,
|
||||
bottomRight: circularRadius,
|
||||
),
|
||||
);
|
||||
} else if (appendixHeight < 0) {
|
||||
// Left side with bottom corner
|
||||
path.lineTo(0, size.height + appendixHeight - Dimens.borderRadiusL);
|
||||
path.arcToPoint(
|
||||
Offset(Dimens.borderRadiusL, size.height + appendixHeight),
|
||||
radius: circularRadius,
|
||||
clockwise: false,
|
||||
);
|
||||
)
|
||||
..close();
|
||||
}
|
||||
|
||||
// Bottom side with step
|
||||
final allowedRadius = min(appendixHeight.abs() / 2, Dimens.borderRadiusL);
|
||||
path.lineTo(appendixWidth - allowedRadius, size.height + appendixHeight);
|
||||
path.arcToPoint(
|
||||
Offset(appendixWidth, size.height + appendixHeight + allowedRadius),
|
||||
radius: circularRadius,
|
||||
);
|
||||
path.lineTo(appendixWidth, size.height - allowedRadius);
|
||||
path.arcToPoint(
|
||||
Offset(appendixWidth + allowedRadius, size.height),
|
||||
radius: circularRadius,
|
||||
clockwise: false,
|
||||
);
|
||||
Path _drawAppendixOnLeft(Size size, double bottomRadius) {
|
||||
final path = Path();
|
||||
final circularRadius = Radius.circular(bottomRadius);
|
||||
final appendixHeight = this.appendixHeight.abs();
|
||||
|
||||
// Right side with bottom corner
|
||||
path.lineTo(size.width - Dimens.borderRadiusL, size.height);
|
||||
path.arcToPoint(
|
||||
Offset(size.width, size.height - Dimens.borderRadiusL),
|
||||
radius: circularRadius,
|
||||
clockwise: false,
|
||||
);
|
||||
} else {
|
||||
// Left side with bottom corner
|
||||
path.lineTo(0, size.height - Dimens.borderRadiusL);
|
||||
path.arcToPoint(
|
||||
Offset(Dimens.borderRadiusL, size.height),
|
||||
radius: circularRadius,
|
||||
clockwise: false,
|
||||
);
|
||||
// Left side with bottom corner
|
||||
path.lineTo(0, size.height - bottomRadius);
|
||||
path.arcToPoint(
|
||||
Offset(bottomRadius, size.height),
|
||||
radius: circularRadius,
|
||||
clockwise: false,
|
||||
);
|
||||
|
||||
// Bottom side with step
|
||||
final allowedRadius = min(appendixHeight.abs() / 2, Dimens.borderRadiusL);
|
||||
path.relativeLineTo(appendixWidth - allowedRadius * 2, 0);
|
||||
path.relativeArcToPoint(
|
||||
Offset(allowedRadius, -allowedRadius),
|
||||
radius: Radius.circular(allowedRadius),
|
||||
rotation: 90,
|
||||
clockwise: false,
|
||||
);
|
||||
path.relativeLineTo(0, -appendixHeight + allowedRadius * 2);
|
||||
path.relativeArcToPoint(
|
||||
Offset(allowedRadius, -allowedRadius),
|
||||
radius: Radius.circular(allowedRadius),
|
||||
rotation: 90,
|
||||
);
|
||||
// Bottom side with step
|
||||
final allowedRadius = min(appendixHeight.abs() / 2, bottomRadius);
|
||||
path.lineTo(appendixWidth - allowedRadius, size.height);
|
||||
path.arcToPoint(
|
||||
Offset(appendixWidth, size.height - allowedRadius),
|
||||
radius: Radius.circular(allowedRadius),
|
||||
rotation: 90,
|
||||
clockwise: false,
|
||||
);
|
||||
path.lineTo(appendixWidth, size.height - appendixHeight + allowedRadius);
|
||||
path.arcToPoint(
|
||||
Offset(appendixWidth + allowedRadius, size.height - appendixHeight),
|
||||
radius: Radius.circular(allowedRadius),
|
||||
rotation: 90,
|
||||
);
|
||||
|
||||
// Right side with bottom corner
|
||||
path.lineTo(size.width - bottomRadius, size.height - appendixHeight);
|
||||
path.arcToPoint(
|
||||
Offset(size.width, size.height - appendixHeight - bottomRadius),
|
||||
radius: circularRadius,
|
||||
clockwise: false,
|
||||
);
|
||||
|
||||
// Right side with bottom corner
|
||||
path.lineTo(size.width - Dimens.borderRadiusL, size.height - appendixHeight);
|
||||
path.arcToPoint(
|
||||
Offset(size.width, size.height - appendixHeight - Dimens.borderRadiusL),
|
||||
radius: circularRadius,
|
||||
clockwise: false,
|
||||
);
|
||||
}
|
||||
path.lineTo(size.width, 0);
|
||||
path.close();
|
||||
canvas.drawPath(path, paint);
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -20,9 +20,7 @@ class MeteringTopBar extends StatelessWidget {
|
|||
return CustomPaint(
|
||||
painter: MeteringTopBarShape(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
appendixWidth: appendixHeight > 0
|
||||
? MediaQuery.of(context).size.width / 2 - Dimens.grid8 + Dimens.paddingM
|
||||
: MediaQuery.of(context).size.width / 2 + Dimens.grid8 - Dimens.paddingM,
|
||||
appendixWidth: MediaQuery.of(context).size.width / 2 - Dimens.grid8 / 2 + Dimens.paddingM,
|
||||
appendixHeight: appendixHeight,
|
||||
),
|
||||
child: Padding(
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/widget_picker_dialog_animated.dart';
|
||||
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/reading_value_container/widget_container_reading_value.dart';
|
||||
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||
|
||||
class EquipmentProfilePicker extends StatelessWidget {
|
||||
const EquipmentProfilePicker();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedDialogPicker<EquipmentProfile>(
|
||||
icon: Icons.camera,
|
||||
title: S.of(context).equipmentProfile,
|
||||
selectedValue: EquipmentProfiles.selectedOf(context),
|
||||
values: EquipmentProfiles.of(context),
|
||||
itemTitleBuilder: (_, value) => Text(value.id.isEmpty ? S.of(context).none : value.name),
|
||||
onChanged: EquipmentProfileProvider.of(context).setProfile,
|
||||
closedChild: ReadingValueContainer.singleValue(
|
||||
value: ReadingValue(
|
||||
label: S.of(context).equipmentProfile,
|
||||
value: EquipmentProfiles.selectedOf(context).id.isEmpty
|
||||
? S.of(context).none
|
||||
: EquipmentProfiles.selectedOf(context).name,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/data/models/exposure_pair.dart';
|
||||
import 'package:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/reading_value_container/widget_container_reading_value.dart';
|
||||
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
||||
|
||||
class ExtremeExposurePairsContainer extends StatelessWidget {
|
||||
final ExposurePair? fastest;
|
||||
final ExposurePair? slowest;
|
||||
|
||||
const ExtremeExposurePairsContainer({
|
||||
required this.fastest,
|
||||
required this.slowest,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ReadingValueContainer(
|
||||
values: [
|
||||
ReadingValue(
|
||||
label: S.of(context).fastestExposurePair,
|
||||
value: _exposurePairToString(context, fastest),
|
||||
),
|
||||
ReadingValue(
|
||||
label: S.of(context).slowestExposurePair,
|
||||
value: _exposurePairToString(context, slowest),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _exposurePairToString(BuildContext context, ExposurePair? pair) {
|
||||
if (pair == null) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
return '${pair.aperture} - ${Films.selectedOf(context).reciprocityFailure(pair.shutterSpeed)}';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/widget_picker_dialog_animated.dart';
|
||||
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/reading_value_container/widget_container_reading_value.dart';
|
||||
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||
|
||||
class FilmPicker extends StatelessWidget {
|
||||
final IsoValue selectedIso;
|
||||
|
||||
const FilmPicker({required this.selectedIso});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedDialogPicker<Film>(
|
||||
icon: Icons.camera_roll,
|
||||
title: S.of(context).film,
|
||||
subtitle: S.of(context).filmReciprocityHint,
|
||||
selectedValue: Films.selectedOf(context),
|
||||
values: Films.inUseOf(context),
|
||||
itemTitleBuilder: (_, value) => Text(value.name.isEmpty ? S.of(context).none : value.name),
|
||||
onChanged: FilmsProvider.of(context).setFilm,
|
||||
closedChild: ReadingValueContainer.singleValue(
|
||||
value: ReadingValue(
|
||||
label: _label(context),
|
||||
value: Films.selectedOf(context).name.isEmpty
|
||||
? S.of(context).none
|
||||
: Films.selectedOf(context).name,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _label(BuildContext context) {
|
||||
if (Films.selectedOf(context) == const Film.other() ||
|
||||
Films.selectedOf(context).iso == selectedIso.value) {
|
||||
return S.of(context).film;
|
||||
}
|
||||
|
||||
final evDiff = IsoValue(
|
||||
Films.selectedOf(context).iso,
|
||||
StopType.full,
|
||||
).difference(selectedIso);
|
||||
|
||||
if (evDiff > 0) {
|
||||
return S.of(context).filmPush;
|
||||
} else if (evDiff < 0) {
|
||||
return S.of(context).filmPull;
|
||||
} else {
|
||||
return S.of(context).film;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/widget_picker_dialog_animated.dart';
|
||||
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/reading_value_container/widget_container_reading_value.dart';
|
||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||
|
||||
class IsoValuePicker extends StatelessWidget {
|
||||
final List<IsoValue> values;
|
||||
final IsoValue selectedValue;
|
||||
final ValueChanged<IsoValue> onChanged;
|
||||
|
||||
const IsoValuePicker({
|
||||
required this.selectedValue,
|
||||
required this.values,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedDialogPicker<IsoValue>(
|
||||
icon: Icons.iso,
|
||||
title: S.of(context).iso,
|
||||
subtitle: S.of(context).filmSpeed,
|
||||
selectedValue: selectedValue,
|
||||
values: values,
|
||||
itemTitleBuilder: (_, value) => Text(value.value.toString()),
|
||||
// using ascending order, because increase in film speed rises EV
|
||||
itemTrailingBuilder: (selected, value) => value.value != selected.value
|
||||
? Text(S.of(context).evValue(selected.toStringDifference(value)))
|
||||
: null,
|
||||
onChanged: onChanged,
|
||||
closedChild: ReadingValueContainer.singleValue(
|
||||
value: ReadingValue(
|
||||
label: S.of(context).iso,
|
||||
value: selectedValue.value.toString(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/widget_picker_dialog_animated.dart';
|
||||
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/reading_value_container/widget_container_reading_value.dart';
|
||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||
|
||||
class NdValuePicker extends StatelessWidget {
|
||||
final List<NdValue> values;
|
||||
final NdValue selectedValue;
|
||||
final ValueChanged<NdValue> onChanged;
|
||||
|
||||
const NdValuePicker({
|
||||
required this.selectedValue,
|
||||
required this.values,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedDialogPicker<NdValue>(
|
||||
icon: Icons.filter_b_and_w,
|
||||
title: S.of(context).nd,
|
||||
subtitle: S.of(context).ndFilterFactor,
|
||||
selectedValue: selectedValue,
|
||||
values: values,
|
||||
itemTitleBuilder: (_, value) => Text(
|
||||
value.value == 0 ? S.of(context).none : value.value.toString(),
|
||||
),
|
||||
// using descending order, because ND filter darkens image & lowers EV
|
||||
itemTrailingBuilder: (selected, value) => value.value != selected.value
|
||||
? Text(S.of(context).evValue(value.toStringDifference(selected)))
|
||||
: null,
|
||||
onChanged: onChanged,
|
||||
closedChild: ReadingValueContainer.singleValue(
|
||||
value: ReadingValue(
|
||||
label: S.of(context).nd,
|
||||
value: selectedValue.value.toString(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -22,15 +22,15 @@ class AnimatedDialog extends StatefulWidget {
|
|||
class AnimatedDialogState extends State<AnimatedDialog> with SingleTickerProviderStateMixin {
|
||||
final GlobalKey _key = GlobalKey();
|
||||
|
||||
late final Size _closedSize;
|
||||
late final Offset _closedOffset;
|
||||
late Size _closedSize;
|
||||
late Offset _closedOffset;
|
||||
|
||||
late final AnimationController _animationController;
|
||||
late final CurvedAnimation _defaultCurvedAnimation;
|
||||
late final Animation<Color?> _barrierColorAnimation;
|
||||
late final SizeTween _sizeTween;
|
||||
late final Animation<Size?> _sizeAnimation;
|
||||
late final Animation<Size?> _offsetAnimation;
|
||||
late SizeTween _sizeTween;
|
||||
late Animation<Size?> _sizeAnimation;
|
||||
late Animation<Size?> _offsetAnimation;
|
||||
late final Animation<double> _borderRadiusAnimation;
|
||||
late final Animation<double> _closedOpacityAnimation;
|
||||
late final Animation<double> _openedOpacityAnimation;
|
||||
|
@ -88,35 +88,7 @@ class AnimatedDialogState extends State<AnimatedDialog> with SingleTickerProvide
|
|||
),
|
||||
);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final mediaQuery = MediaQuery.of(context);
|
||||
|
||||
_closedSize = _key.currentContext!.size!;
|
||||
_sizeTween = SizeTween(
|
||||
begin: _closedSize,
|
||||
end: widget.openedSize ??
|
||||
Size(
|
||||
mediaQuery.size.width -
|
||||
mediaQuery.padding.horizontal -
|
||||
Dimens.dialogMargin.horizontal,
|
||||
mediaQuery.size.height - mediaQuery.padding.vertical - Dimens.dialogMargin.vertical,
|
||||
),
|
||||
);
|
||||
_sizeAnimation = _sizeTween.animate(_defaultCurvedAnimation);
|
||||
|
||||
final renderBox = _key.currentContext!.findRenderObject()! as RenderBox;
|
||||
_closedOffset = renderBox.localToGlobal(Offset.zero);
|
||||
_offsetAnimation = SizeTween(
|
||||
begin: Size(
|
||||
_closedOffset.dx + _closedSize.width / 2,
|
||||
_closedOffset.dy + _closedSize.height / 2,
|
||||
),
|
||||
end: Size(
|
||||
mediaQuery.size.width / 2,
|
||||
mediaQuery.size.height / 2 + mediaQuery.padding.top / 2 - mediaQuery.padding.bottom / 2,
|
||||
),
|
||||
).animate(_defaultCurvedAnimation);
|
||||
});
|
||||
_setClosedOffset();
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -133,6 +105,12 @@ class AnimatedDialogState extends State<AnimatedDialog> with SingleTickerProvide
|
|||
).animate(_defaultCurvedAnimation);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant AnimatedDialog oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_setClosedOffset();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
|
@ -151,6 +129,38 @@ class AnimatedDialogState extends State<AnimatedDialog> with SingleTickerProvide
|
|||
);
|
||||
}
|
||||
|
||||
void _setClosedOffset() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final renderBox = _key.currentContext?.findRenderObject()! as RenderBox?;
|
||||
if (renderBox != null) {
|
||||
final size = MediaQuery.sizeOf(context);
|
||||
final padding = MediaQuery.paddingOf(context);
|
||||
_closedSize = _key.currentContext!.size!;
|
||||
_sizeTween = SizeTween(
|
||||
begin: _closedSize,
|
||||
end: widget.openedSize ??
|
||||
Size(
|
||||
size.width - padding.horizontal - Dimens.dialogMargin.horizontal,
|
||||
size.height - padding.vertical - Dimens.dialogMargin.vertical,
|
||||
),
|
||||
);
|
||||
_sizeAnimation = _sizeTween.animate(_defaultCurvedAnimation);
|
||||
|
||||
_closedOffset = renderBox.localToGlobal(Offset.zero);
|
||||
_offsetAnimation = SizeTween(
|
||||
begin: Size(
|
||||
_closedOffset.dx + _closedSize.width / 2,
|
||||
_closedOffset.dy + _closedSize.height / 2,
|
||||
),
|
||||
end: Size(
|
||||
size.width / 2,
|
||||
size.height / 2 + padding.top / 2 - padding.bottom / 2,
|
||||
),
|
||||
).animate(_defaultCurvedAnimation);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _openDialog() {
|
||||
Navigator.of(context).push(
|
||||
PageRouteBuilder(
|
|
@ -1,7 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart';
|
||||
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/animated_dialog_picker/components/dialog_picker/widget_picker_dialog.dart';
|
||||
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart';
|
||||
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/dialog_picker/widget_picker_dialog.dart';
|
||||
|
||||
// Has to be stateful, so that [GlobalKey] is not recreated.
|
||||
// Otherwise use will no be able to close the dialog after EV value has changed.
|
|
@ -72,12 +72,15 @@ class _ReadingValueBuilder extends StatelessWidget {
|
|||
softWrap: false,
|
||||
),
|
||||
const SizedBox(height: Dimens.grid4),
|
||||
Text(
|
||||
reading.value,
|
||||
style: textTheme.titleMedium?.copyWith(color: textColor),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: false,
|
||||
AnimatedSwitcher(
|
||||
duration: Dimens.switchDuration,
|
||||
child: Text(
|
||||
reading.value,
|
||||
style: textTheme.titleMedium?.copyWith(color: textColor),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: false,
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
|
@ -1,34 +1,29 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/data/models/exposure_pair.dart';
|
||||
import 'package:lightmeter/data/models/film.dart';
|
||||
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
|
||||
import 'package:lightmeter/features.dart';
|
||||
import 'package:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/providers/equipment_profile_provider.dart';
|
||||
import 'package:lightmeter/providers/metering_screen_layout_provider.dart';
|
||||
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
||||
import 'package:lightmeter/res/dimens.dart';
|
||||
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/animated_dialog_picker/widget_picker_dialog_animated.dart';
|
||||
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/reading_value_container/widget_container_reading_value.dart';
|
||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/equipment_profile_picker/widget_picker_equipment_profiles.dart';
|
||||
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/extreme_exposure_pairs_container/widget_container_extreme_exposure_pairs.dart';
|
||||
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/film_picker/widget_picker_film.dart';
|
||||
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/iso_picker/widget_picker_iso.dart';
|
||||
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/nd_picker/widget_picker_nd.dart';
|
||||
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||
|
||||
class ReadingsContainer extends StatelessWidget {
|
||||
final ExposurePair? fastest;
|
||||
final ExposurePair? slowest;
|
||||
final Film film;
|
||||
final IsoValue iso;
|
||||
final NdValue nd;
|
||||
final ValueChanged<Film> onFilmChanged;
|
||||
final ValueChanged<IsoValue> onIsoChanged;
|
||||
final ValueChanged<NdValue> onNdChanged;
|
||||
|
||||
const ReadingsContainer({
|
||||
required this.fastest,
|
||||
required this.slowest,
|
||||
required this.film,
|
||||
required this.iso,
|
||||
required this.nd,
|
||||
required this.onFilmChanged,
|
||||
required this.onIsoChanged,
|
||||
required this.onNdChanged,
|
||||
super.key,
|
||||
|
@ -39,53 +34,44 @@ class ReadingsContainer extends StatelessWidget {
|
|||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (FeaturesConfig.equipmentProfilesEnabled) ...[
|
||||
const _EquipmentProfilePicker(),
|
||||
if (UserPreferencesProvider.meteringScreenFeatureOf(
|
||||
context,
|
||||
MeteringScreenLayoutFeature.equipmentProfiles,
|
||||
)) ...[
|
||||
const EquipmentProfilePicker(),
|
||||
const _InnerPadding(),
|
||||
],
|
||||
if (MeteringScreenLayout.featureOf(
|
||||
if (UserPreferencesProvider.meteringScreenFeatureOf(
|
||||
context,
|
||||
MeteringScreenLayoutFeature.extremeExposurePairs,
|
||||
)) ...[
|
||||
ReadingValueContainer(
|
||||
values: [
|
||||
ReadingValue(
|
||||
label: S.of(context).fastestExposurePair,
|
||||
value: fastest != null ? fastest!.toString() : '-',
|
||||
),
|
||||
ReadingValue(
|
||||
label: S.of(context).slowestExposurePair,
|
||||
value: fastest != null ? slowest!.toString() : '-',
|
||||
),
|
||||
],
|
||||
ExtremeExposurePairsContainer(
|
||||
fastest: fastest,
|
||||
slowest: slowest,
|
||||
),
|
||||
const _InnerPadding(),
|
||||
],
|
||||
if (MeteringScreenLayout.featureOf(
|
||||
if (UserPreferencesProvider.meteringScreenFeatureOf(
|
||||
context,
|
||||
MeteringScreenLayoutFeature.filmPicker,
|
||||
)) ...[
|
||||
_FilmPicker(
|
||||
values: Film.values,
|
||||
selectedValue: film,
|
||||
onChanged: onFilmChanged,
|
||||
),
|
||||
FilmPicker(selectedIso: iso),
|
||||
const _InnerPadding(),
|
||||
],
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _IsoValuePicker(
|
||||
child: IsoValuePicker(
|
||||
selectedValue: iso,
|
||||
values: context.listen<EquipmentProfile>().isoValues,
|
||||
values: EquipmentProfiles.selectedOf(context).isoValues,
|
||||
onChanged: onIsoChanged,
|
||||
),
|
||||
),
|
||||
const _InnerPadding(),
|
||||
Expanded(
|
||||
child: _NdValuePicker(
|
||||
child: NdValuePicker(
|
||||
selectedValue: nd,
|
||||
values: context.listen<EquipmentProfile>().ndValues,
|
||||
values: EquipmentProfiles.selectedOf(context).ndValues,
|
||||
onChanged: onNdChanged,
|
||||
),
|
||||
),
|
||||
|
@ -99,129 +85,3 @@ class ReadingsContainer extends StatelessWidget {
|
|||
class _InnerPadding extends SizedBox {
|
||||
const _InnerPadding() : super(height: Dimens.grid8, width: Dimens.grid8);
|
||||
}
|
||||
|
||||
class _EquipmentProfilePicker extends StatelessWidget {
|
||||
const _EquipmentProfilePicker();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedDialogPicker<EquipmentProfileData>(
|
||||
icon: Icons.camera,
|
||||
title: S.of(context).equipmentProfile,
|
||||
selectedValue: context.listen<EquipmentProfile>(),
|
||||
values: context.listen<EquipmentProfiles>(),
|
||||
itemTitleBuilder: (_, value) => Text(value.id.isEmpty ? S.of(context).none : value.name),
|
||||
onChanged: EquipmentProfileProvider.of(context).setProfile,
|
||||
closedChild: ReadingValueContainer.singleValue(
|
||||
value: ReadingValue(
|
||||
label: S.of(context).equipmentProfile,
|
||||
value: context.listen<EquipmentProfile>().id.isEmpty
|
||||
? S.of(context).none
|
||||
: context.listen<EquipmentProfile>().name,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FilmPicker extends StatelessWidget {
|
||||
final List<Film> values;
|
||||
final Film selectedValue;
|
||||
final ValueChanged<Film> onChanged;
|
||||
|
||||
const _FilmPicker({
|
||||
required this.values,
|
||||
required this.selectedValue,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedDialogPicker<Film>(
|
||||
icon: Icons.camera_roll,
|
||||
title: S.of(context).film,
|
||||
selectedValue: selectedValue,
|
||||
values: values,
|
||||
itemTitleBuilder: (_, value) => Text(value.name.isEmpty ? S.of(context).none : value.name),
|
||||
onChanged: onChanged,
|
||||
closedChild: ReadingValueContainer.singleValue(
|
||||
value: ReadingValue(
|
||||
label: S.of(context).film,
|
||||
value: selectedValue.name.isEmpty ? S.of(context).none : selectedValue.name,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _IsoValuePicker extends StatelessWidget {
|
||||
final List<IsoValue> values;
|
||||
final IsoValue selectedValue;
|
||||
final ValueChanged<IsoValue> onChanged;
|
||||
|
||||
const _IsoValuePicker({
|
||||
required this.selectedValue,
|
||||
required this.values,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedDialogPicker<IsoValue>(
|
||||
icon: Icons.iso,
|
||||
title: S.of(context).iso,
|
||||
subtitle: S.of(context).filmSpeed,
|
||||
selectedValue: selectedValue,
|
||||
values: values,
|
||||
itemTitleBuilder: (_, value) => Text(value.value.toString()),
|
||||
// using ascending order, because increase in film speed rises EV
|
||||
itemTrailingBuilder: (selected, value) => value.value != selected.value
|
||||
? Text(S.of(context).evValue(selected.toStringDifference(value)))
|
||||
: null,
|
||||
onChanged: onChanged,
|
||||
closedChild: ReadingValueContainer.singleValue(
|
||||
value: ReadingValue(
|
||||
label: S.of(context).iso,
|
||||
value: selectedValue.value.toString(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NdValuePicker extends StatelessWidget {
|
||||
final List<NdValue> values;
|
||||
final NdValue selectedValue;
|
||||
final ValueChanged<NdValue> onChanged;
|
||||
|
||||
const _NdValuePicker({
|
||||
required this.selectedValue,
|
||||
required this.values,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedDialogPicker<NdValue>(
|
||||
icon: Icons.filter_b_and_w,
|
||||
title: S.of(context).nd,
|
||||
subtitle: S.of(context).ndFilterFactor,
|
||||
selectedValue: selectedValue,
|
||||
values: values,
|
||||
itemTitleBuilder: (_, value) => Text(
|
||||
value.value == 0 ? S.of(context).none : value.value.toString(),
|
||||
),
|
||||
// using descending order, because ND filter darkens image & lowers EV
|
||||
itemTrailingBuilder: (selected, value) => value.value != selected.value
|
||||
? Text(S.of(context).evValue(value.toStringDifference(selected)))
|
||||
: null,
|
||||
onChanged: onChanged,
|
||||
closedChild: ReadingValueContainer.singleValue(
|
||||
value: ReadingValue(
|
||||
label: S.of(context).nd,
|
||||
value: selectedValue.value.toString(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import 'package:lightmeter/data/models/film.dart';
|
||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||
|
||||
sealed class MeteringEvent {
|
||||
|
@ -6,17 +5,11 @@ sealed class MeteringEvent {
|
|||
}
|
||||
|
||||
class EquipmentProfileChangedEvent extends MeteringEvent {
|
||||
final EquipmentProfileData equipmentProfileData;
|
||||
final EquipmentProfile equipmentProfileData;
|
||||
|
||||
const EquipmentProfileChangedEvent(this.equipmentProfileData);
|
||||
}
|
||||
|
||||
class FilmChangedEvent extends MeteringEvent {
|
||||
final Film film;
|
||||
|
||||
const FilmChangedEvent(this.film);
|
||||
}
|
||||
|
||||
class IsoChangedEvent extends MeteringEvent {
|
||||
final IsoValue isoValue;
|
||||
|
||||
|
|
|
@ -1,17 +1,11 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:lightmeter/data/caffeine_service.dart';
|
||||
import 'package:lightmeter/data/haptics_service.dart';
|
||||
import 'package:lightmeter/data/light_sensor_service.dart';
|
||||
import 'package:lightmeter/data/permissions_service.dart';
|
||||
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||
import 'package:lightmeter/data/volume_events_service.dart';
|
||||
import 'package:lightmeter/interactors/metering_interactor.dart';
|
||||
import 'package:lightmeter/providers/services_provider.dart';
|
||||
import 'package:lightmeter/screens/metering/bloc_metering.dart';
|
||||
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
|
||||
import 'package:lightmeter/screens/metering/components/shared/volume_keys_notifier/notifier_volume_keys.dart';
|
||||
import 'package:lightmeter/screens/metering/screen_metering.dart';
|
||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||
|
||||
class MeteringFlow extends StatefulWidget {
|
||||
const MeteringFlow({super.key});
|
||||
|
@ -23,31 +17,45 @@ class MeteringFlow extends StatefulWidget {
|
|||
class _MeteringFlowState extends State<MeteringFlow> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InheritedWidgetBase<MeteringInteractor>(
|
||||
return MeteringInteractorProvider(
|
||||
data: MeteringInteractor(
|
||||
context.get<UserPreferencesService>(),
|
||||
context.get<CaffeineService>(),
|
||||
context.get<HapticsService>(),
|
||||
context.get<PermissionsService>(),
|
||||
context.get<LightSensorService>(),
|
||||
context.get<VolumeEventsService>(),
|
||||
ServicesProvider.of(context).userPreferencesService,
|
||||
ServicesProvider.of(context).caffeineService,
|
||||
ServicesProvider.of(context).hapticsService,
|
||||
ServicesProvider.of(context).permissionsService,
|
||||
ServicesProvider.of(context).lightSensorService,
|
||||
ServicesProvider.of(context).volumeEventsService,
|
||||
)..initialize(),
|
||||
child: InheritedWidgetBase<VolumeKeysNotifier>(
|
||||
data: VolumeKeysNotifier(context.get<VolumeEventsService>()),
|
||||
child: MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(create: (_) => MeteringCommunicationBloc()),
|
||||
BlocProvider(
|
||||
create: (context) => MeteringBloc(
|
||||
context.get<MeteringInteractor>(),
|
||||
context.get<VolumeKeysNotifier>(),
|
||||
context.read<MeteringCommunicationBloc>(),
|
||||
),
|
||||
child: MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(create: (_) => MeteringCommunicationBloc()),
|
||||
BlocProvider(
|
||||
create: (context) => MeteringBloc(
|
||||
MeteringInteractorProvider.of(context),
|
||||
VolumeKeysNotifier(ServicesProvider.of(context).volumeEventsService),
|
||||
context.read<MeteringCommunicationBloc>(),
|
||||
),
|
||||
],
|
||||
child: const MeteringScreen(),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: const MeteringScreen(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MeteringInteractorProvider extends InheritedWidget {
|
||||
final MeteringInteractor data;
|
||||
|
||||
const MeteringInteractorProvider({
|
||||
required this.data,
|
||||
required super.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
static MeteringInteractor of(BuildContext context) {
|
||||
return context.findAncestorWidgetOfExactType<MeteringInteractorProvider>()!.data;
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(MeteringInteractorProvider oldWidget) => false;
|
||||
}
|
||||
|
|
|
@ -4,18 +4,18 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:lightmeter/data/models/ev_source_type.dart';
|
||||
import 'package:lightmeter/data/models/exposure_pair.dart';
|
||||
import 'package:lightmeter/data/models/film.dart';
|
||||
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
|
||||
import 'package:lightmeter/environment.dart';
|
||||
import 'package:lightmeter/providers/equipment_profile_provider.dart';
|
||||
import 'package:lightmeter/providers/ev_source_type_provider.dart';
|
||||
import 'package:lightmeter/providers/services_provider.dart';
|
||||
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
||||
import 'package:lightmeter/screens/metering/bloc_metering.dart';
|
||||
import 'package:lightmeter/screens/metering/components/bottom_controls/provider_bottom_controls.dart';
|
||||
import 'package:lightmeter/screens/metering/components/camera_container/provider_container_camera.dart';
|
||||
import 'package:lightmeter/screens/metering/components/light_sensor_container/provider_container_light_sensor.dart';
|
||||
import 'package:lightmeter/screens/metering/event_metering.dart';
|
||||
import 'package:lightmeter/screens/metering/state_metering.dart';
|
||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||
import 'package:lightmeter/screens/metering/utils/listener_metering_layout_feature.dart';
|
||||
import 'package:lightmeter/screens/metering/utils/listsner_equipment_profiles.dart';
|
||||
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||
|
||||
class MeteringScreen extends StatelessWidget {
|
||||
|
@ -30,13 +30,10 @@ class MeteringScreen extends StatelessWidget {
|
|||
children: [
|
||||
Expanded(
|
||||
child: BlocBuilder<MeteringBloc, MeteringState>(
|
||||
builder: (_, state) => _MeteringContainerBuidler(
|
||||
builder: (_, state) => MeteringContainerBuidler(
|
||||
ev: state is MeteringDataState ? state.ev : null,
|
||||
film: state.film,
|
||||
iso: state.iso,
|
||||
nd: state.nd,
|
||||
onFilmChanged: (value) =>
|
||||
context.read<MeteringBloc>().add(FilmChangedEvent(value)),
|
||||
onIsoChanged: (value) => context.read<MeteringBloc>().add(IsoChangedEvent(value)),
|
||||
onNdChanged: (value) => context.read<MeteringBloc>().add(NdChangedEvent(value)),
|
||||
),
|
||||
|
@ -46,8 +43,8 @@ class MeteringScreen extends StatelessWidget {
|
|||
builder: (context, state) => MeteringBottomControlsProvider(
|
||||
ev: state is MeteringDataState ? state.ev : null,
|
||||
isMetering: state.isMetering,
|
||||
onSwitchEvSourceType: context.get<Environment>().hasLightSensor
|
||||
? EvSourceTypeProvider.of(context).toggleType
|
||||
onSwitchEvSourceType: ServicesProvider.of(context).environment.hasLightSensor
|
||||
? UserPreferencesProvider.of(context).toggleEvSourceType
|
||||
: null,
|
||||
onMeasure: () => context.read<MeteringBloc>().add(const MeasureEvent()),
|
||||
onSettings: () {
|
||||
|
@ -72,14 +69,16 @@ class _InheritedListeners extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InheritedWidgetListener<EquipmentProfile>(
|
||||
return EquipmentProfileListener(
|
||||
onDidChangeDependencies: (value) {
|
||||
context.read<MeteringBloc>().add(EquipmentProfileChangedEvent(value));
|
||||
},
|
||||
child: InheritedModelAspectListener<MeteringScreenLayoutFeature, bool>(
|
||||
aspect: MeteringScreenLayoutFeature.filmPicker,
|
||||
child: MeteringScreenLayoutFeatureListener(
|
||||
feature: MeteringScreenLayoutFeature.filmPicker,
|
||||
onDidChangeDependencies: (value) {
|
||||
if (!value) context.read<MeteringBloc>().add(const FilmChangedEvent(Film.other()));
|
||||
if (!value) {
|
||||
FilmsProvider.of(context).setFilm(const Film.other());
|
||||
}
|
||||
},
|
||||
child: child,
|
||||
),
|
||||
|
@ -87,38 +86,39 @@ class _InheritedListeners extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class _MeteringContainerBuidler extends StatelessWidget {
|
||||
class MeteringContainerBuidler extends StatelessWidget {
|
||||
final double? ev;
|
||||
final Film film;
|
||||
final IsoValue iso;
|
||||
final NdValue nd;
|
||||
final ValueChanged<Film> onFilmChanged;
|
||||
final ValueChanged<IsoValue> onIsoChanged;
|
||||
final ValueChanged<NdValue> onNdChanged;
|
||||
|
||||
const _MeteringContainerBuidler({
|
||||
const MeteringContainerBuidler({
|
||||
required this.ev,
|
||||
required this.film,
|
||||
required this.iso,
|
||||
required this.nd,
|
||||
required this.onFilmChanged,
|
||||
required this.onIsoChanged,
|
||||
required this.onNdChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final exposurePairs = ev != null ? buildExposureValues(context, ev!, film) : <ExposurePair>[];
|
||||
final exposurePairs = ev != null
|
||||
? buildExposureValues(
|
||||
ev!,
|
||||
UserPreferencesProvider.stopTypeOf(context),
|
||||
EquipmentProfiles.selectedOf(context),
|
||||
)
|
||||
: <ExposurePair>[];
|
||||
final fastest = exposurePairs.isNotEmpty ? exposurePairs.first : null;
|
||||
final slowest = exposurePairs.isNotEmpty ? exposurePairs.last : null;
|
||||
return context.listen<EvSourceType>() == EvSourceType.camera
|
||||
// Doubled build here when switching evSourceType. As new source bloc fires a new state on init
|
||||
return UserPreferencesProvider.evSourceTypeOf(context) == EvSourceType.camera
|
||||
? CameraContainerProvider(
|
||||
fastest: fastest,
|
||||
slowest: slowest,
|
||||
film: film,
|
||||
iso: iso,
|
||||
nd: nd,
|
||||
onFilmChanged: onFilmChanged,
|
||||
onIsoChanged: onIsoChanged,
|
||||
onNdChanged: onNdChanged,
|
||||
exposurePairs: exposurePairs,
|
||||
|
@ -126,49 +126,35 @@ class _MeteringContainerBuidler extends StatelessWidget {
|
|||
: LightSensorContainerProvider(
|
||||
fastest: fastest,
|
||||
slowest: slowest,
|
||||
film: film,
|
||||
iso: iso,
|
||||
nd: nd,
|
||||
onFilmChanged: onFilmChanged,
|
||||
onIsoChanged: onIsoChanged,
|
||||
onNdChanged: onNdChanged,
|
||||
exposurePairs: exposurePairs,
|
||||
);
|
||||
}
|
||||
|
||||
List<ExposurePair> buildExposureValues(BuildContext context, double ev, Film film) {
|
||||
@visibleForTesting
|
||||
static List<ExposurePair> buildExposureValues(
|
||||
double ev,
|
||||
StopType stopType,
|
||||
EquipmentProfile equipmentProfile,
|
||||
) {
|
||||
if (ev.isNaN || ev.isInfinite) {
|
||||
return List.empty();
|
||||
}
|
||||
|
||||
/// Depending on the `stopType` the exposure pairs list length is multiplied by 1,2 or 3
|
||||
final StopType stopType = context.listen<StopType>();
|
||||
final int evSteps = (ev * (stopType.index + 1)).round();
|
||||
|
||||
final EquipmentProfile equipmentProfile = context.listen<EquipmentProfile>();
|
||||
final List<ApertureValue> apertureValues =
|
||||
equipmentProfile.apertureValues.whereStopType(stopType);
|
||||
final List<ShutterSpeedValue> shutterSpeedValues =
|
||||
equipmentProfile.shutterSpeedValues.whereStopType(stopType);
|
||||
final apertureValues = ApertureValue.values.whereStopType(stopType);
|
||||
final shutterSpeedValues = ShutterSpeedValue.values.whereStopType(stopType);
|
||||
|
||||
/// Basically we use 1" shutter speed as an anchor point for building the exposure pairs list.
|
||||
/// But user can exclude this value from the list using custom equipment profile.
|
||||
/// So we have to restore the index of the anchor value.
|
||||
const ShutterSpeedValue anchorShutterSpeed = ShutterSpeedValue(1, false, StopType.full);
|
||||
int anchorIndex = shutterSpeedValues.indexOf(anchorShutterSpeed);
|
||||
if (anchorIndex < 0) {
|
||||
final filteredFullList = ShutterSpeedValue.values.whereStopType(stopType);
|
||||
final customListStartIndex = filteredFullList.indexOf(shutterSpeedValues.first);
|
||||
final fullListAnchor = filteredFullList.indexOf(anchorShutterSpeed);
|
||||
if (customListStartIndex < fullListAnchor) {
|
||||
/// This means, that user excluded anchor value at the end,
|
||||
/// i.e. all shutter speed values are shorter than 1".
|
||||
anchorIndex = fullListAnchor - customListStartIndex;
|
||||
} else {
|
||||
/// In case user excludes anchor value at the start,
|
||||
/// we can do no adjustment.
|
||||
}
|
||||
}
|
||||
const anchorShutterSpeed = ShutterSpeedValue(1, false, StopType.full);
|
||||
final int anchorIndex = shutterSpeedValues.indexOf(anchorShutterSpeed);
|
||||
final int evOffset = anchorIndex - evSteps;
|
||||
|
||||
late final int apertureOffset;
|
||||
|
@ -187,16 +173,42 @@ class _MeteringContainerBuidler extends StatelessWidget {
|
|||
) -
|
||||
max(apertureOffset, shutterSpeedOffset);
|
||||
|
||||
if (itemsCount < 0) {
|
||||
if (itemsCount <= 0) {
|
||||
return List.empty();
|
||||
}
|
||||
return List.generate(
|
||||
|
||||
final exposurePairs = List.generate(
|
||||
itemsCount,
|
||||
(index) => ExposurePair(
|
||||
apertureValues[index + apertureOffset],
|
||||
film.reciprocityFailure(shutterSpeedValues[index + shutterSpeedOffset]),
|
||||
shutterSpeedValues[index + shutterSpeedOffset],
|
||||
),
|
||||
growable: false,
|
||||
);
|
||||
|
||||
/// Full equipment profile, nothing to cut
|
||||
if (equipmentProfile.id == "") {
|
||||
return exposurePairs;
|
||||
}
|
||||
|
||||
final equipmentApertureValues = equipmentProfile.apertureValues.whereStopType(stopType);
|
||||
final equipmentShutterSpeedValues = equipmentProfile.shutterSpeedValues.whereStopType(stopType);
|
||||
|
||||
final startCutEV = max(
|
||||
exposurePairs.first.aperture.difference(equipmentApertureValues.first),
|
||||
exposurePairs.first.shutterSpeed.difference(equipmentShutterSpeedValues.first),
|
||||
);
|
||||
final endCutEV = max(
|
||||
equipmentApertureValues.last.difference(exposurePairs.last.aperture),
|
||||
equipmentShutterSpeedValues.last.difference(exposurePairs.last.shutterSpeed),
|
||||
);
|
||||
|
||||
final startCut = (startCutEV * (stopType.index + 1)).round().clamp(0, itemsCount);
|
||||
final endCut = (endCutEV * (stopType.index + 1)).round().clamp(0, itemsCount);
|
||||
|
||||
if (startCut > itemsCount - endCut) {
|
||||
return const [];
|
||||
}
|
||||
return exposurePairs.sublist(startCut, itemsCount - endCut);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,15 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/data/models/film.dart';
|
||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||
|
||||
@immutable
|
||||
abstract class MeteringState {
|
||||
final double? ev100;
|
||||
final Film film;
|
||||
final IsoValue iso;
|
||||
final NdValue nd;
|
||||
final bool isMetering;
|
||||
|
||||
const MeteringState({
|
||||
this.ev100,
|
||||
required this.film,
|
||||
required this.iso,
|
||||
required this.nd,
|
||||
required this.isMetering,
|
||||
|
@ -21,7 +18,6 @@ abstract class MeteringState {
|
|||
|
||||
class LoadingState extends MeteringState {
|
||||
const LoadingState({
|
||||
required super.film,
|
||||
required super.iso,
|
||||
required super.nd,
|
||||
}) : super(isMetering: true);
|
||||
|
@ -30,7 +26,6 @@ class LoadingState extends MeteringState {
|
|||
class MeteringDataState extends MeteringState {
|
||||
const MeteringDataState({
|
||||
required super.ev100,
|
||||
required super.film,
|
||||
required super.iso,
|
||||
required super.nd,
|
||||
required super.isMetering,
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
|
||||
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
||||
|
||||
/// Listening to multiple dependencies at the same time causes firing an event for all dependencies
|
||||
/// even though some of them didn't change:
|
||||
/// ```dart
|
||||
/// @override
|
||||
/// void didChangeDependencies() {
|
||||
/// super.didChangeDependencies();
|
||||
/// _bloc.add(EquipmentProfileChangedEvent(EquipmentProfile.of(context)));
|
||||
/// if (!MeteringScreenLayout.featureStatusOf(context, MeteringScreenLayoutFeature.filmPicker)) {
|
||||
/// _bloc.add(const FilmChangedEvent(Film.other()));
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
/// To overcome this issue I've decided to create a generic listener,
|
||||
/// that will listen to each dependency separately.
|
||||
class MeteringScreenLayoutFeatureListener extends StatefulWidget {
|
||||
final MeteringScreenLayoutFeature feature;
|
||||
final ValueChanged<bool> onDidChangeDependencies;
|
||||
final Widget child;
|
||||
|
||||
const MeteringScreenLayoutFeatureListener({
|
||||
required this.feature,
|
||||
required this.onDidChangeDependencies,
|
||||
required this.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<MeteringScreenLayoutFeatureListener> createState() =>
|
||||
_MeteringScreenLayoutFeatureListenerState();
|
||||
}
|
||||
|
||||
class _MeteringScreenLayoutFeatureListenerState extends State<MeteringScreenLayoutFeatureListener> {
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
widget.onDidChangeDependencies(
|
||||
UserPreferencesProvider.meteringScreenFeatureOf(
|
||||
context,
|
||||
widget.feature,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.child;
|
||||
}
|
||||
}
|
30
lib/screens/metering/utils/listsner_equipment_profiles.dart
Normal file
30
lib/screens/metering/utils/listsner_equipment_profiles.dart
Normal file
|
@ -0,0 +1,30 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||
|
||||
class EquipmentProfileListener extends StatefulWidget {
|
||||
final ValueChanged<EquipmentProfile> onDidChangeDependencies;
|
||||
final Widget child;
|
||||
|
||||
const EquipmentProfileListener({
|
||||
required this.onDidChangeDependencies,
|
||||
required this.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<EquipmentProfileListener> createState() => _EquipmentProfileListenerState();
|
||||
}
|
||||
|
||||
class _EquipmentProfileListenerState extends State<EquipmentProfileListener> {
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
widget.onDidChangeDependencies(EquipmentProfiles.selectedOf(context));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.child;
|
||||
}
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/environment.dart';
|
||||
import 'package:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||
import 'package:lightmeter/providers/services_provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class ReportIssueListTile extends StatelessWidget {
|
||||
|
@ -14,7 +13,7 @@ class ReportIssueListTile extends StatelessWidget {
|
|||
title: Text(S.of(context).reportIssue),
|
||||
onTap: () {
|
||||
launchUrl(
|
||||
Uri.parse(context.get<Environment>().issuesReportUrl),
|
||||
Uri.parse(ServicesProvider.of(context).environment.issuesReportUrl),
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
},
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/environment.dart';
|
||||
import 'package:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||
import 'package:lightmeter/providers/services_provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class SourceCodeListTile extends StatelessWidget {
|
||||
|
@ -14,7 +13,7 @@ class SourceCodeListTile extends StatelessWidget {
|
|||
title: Text(S.of(context).sourceCode),
|
||||
onTap: () {
|
||||
launchUrl(
|
||||
Uri.parse(context.get<Environment>().sourceCodeUrl),
|
||||
Uri.parse(ServicesProvider.of(context).environment.sourceCodeUrl),
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
},
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import 'package:clipboard/clipboard.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/environment.dart';
|
||||
import 'package:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||
import 'package:lightmeter/providers/services_provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class WriteEmailListTile extends StatelessWidget {
|
||||
|
@ -14,7 +13,7 @@ class WriteEmailListTile extends StatelessWidget {
|
|||
leading: const Icon(Icons.email),
|
||||
title: Text(S.of(context).writeEmail),
|
||||
onTap: () {
|
||||
final email = context.get<Environment>().contactEmail;
|
||||
final email = ServicesProvider.of(context).environment.contactEmail;
|
||||
final mailToUrl = Uri.parse('mailto:$email?subject=M3 Lightmeter');
|
||||
canLaunchUrl(mailToUrl).then((canLaunch) {
|
||||
if (canLaunch) {
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:lightmeter/interactors/settings_interactor.dart';
|
||||
|
||||
import 'package:lightmeter/screens/settings/components/general/components/caffeine/bloc_list_tile_caffeine.dart';
|
||||
import 'package:lightmeter/screens/settings/components/general/components/caffeine/widget_list_tile_caffeine.dart';
|
||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||
import 'package:lightmeter/screens/settings/flow_settings.dart';
|
||||
|
||||
class CaffeineListTileProvider extends StatelessWidget {
|
||||
const CaffeineListTileProvider({super.key});
|
||||
|
@ -12,7 +11,7 @@ class CaffeineListTileProvider extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => CaffeineListTileBloc(context.get<SettingsInteractor>()),
|
||||
create: (context) => CaffeineListTileBloc(SettingsInteractorProvider.of(context)),
|
||||
child: const CaffeineListTile(),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:lightmeter/interactors/settings_interactor.dart';
|
||||
|
||||
import 'package:lightmeter/screens/settings/components/general/components/haptics/bloc_list_tile_haptics.dart';
|
||||
import 'package:lightmeter/screens/settings/components/general/components/haptics/widget_list_tile_haptics.dart';
|
||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||
import 'package:lightmeter/screens/settings/flow_settings.dart';
|
||||
|
||||
class HapticsListTileProvider extends StatelessWidget {
|
||||
const HapticsListTileProvider({super.key});
|
||||
|
@ -12,7 +11,7 @@ class HapticsListTileProvider extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => HapticsListTileBloc(context.get<SettingsInteractor>()),
|
||||
create: (context) => HapticsListTileBloc(SettingsInteractorProvider.of(context)),
|
||||
child: const HapticsListTile(),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/data/models/supported_locale.dart';
|
||||
import 'package:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/providers/supported_locale_provider.dart';
|
||||
import 'package:lightmeter/screens/settings/components/shared/dialog_picker.dart/widget_dialog_picker.dart';
|
||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
||||
import 'package:lightmeter/screens/settings/components/shared/dialog_picker/widget_dialog_picker.dart';
|
||||
|
||||
class LanguageListTile extends StatelessWidget {
|
||||
const LanguageListTile({super.key});
|
||||
|
@ -13,20 +12,20 @@ class LanguageListTile extends StatelessWidget {
|
|||
return ListTile(
|
||||
leading: const Icon(Icons.language),
|
||||
title: Text(S.of(context).language),
|
||||
trailing: Text(context.listen<SupportedLocale>().localizedName),
|
||||
trailing: Text(UserPreferencesProvider.localeOf(context).localizedName),
|
||||
onTap: () {
|
||||
showDialog<SupportedLocale>(
|
||||
context: context,
|
||||
builder: (_) => DialogPicker<SupportedLocale>(
|
||||
icon: Icons.language,
|
||||
title: S.of(context).chooseLanguage,
|
||||
selectedValue: context.get<SupportedLocale>(),
|
||||
selectedValue: UserPreferencesProvider.localeOf(context),
|
||||
values: SupportedLocale.values,
|
||||
titleAdapter: (context, value) => value.localizedName,
|
||||
),
|
||||
).then((value) {
|
||||
if (value != null) {
|
||||
SupportedLocaleProvider.of(context).setLocale(value);
|
||||
UserPreferencesProvider.of(context).setLocale(value);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:lightmeter/interactors/settings_interactor.dart';
|
||||
|
||||
import 'package:lightmeter/screens/settings/components/general/components/volume_actions/bloc_list_tile_volume_actions.dart';
|
||||
import 'package:lightmeter/screens/settings/components/general/components/volume_actions/widget_list_tile_volume_actions.dart';
|
||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||
import 'package:lightmeter/screens/settings/flow_settings.dart';
|
||||
|
||||
class VolumeActionsListTileProvider extends StatelessWidget {
|
||||
const VolumeActionsListTileProvider({super.key});
|
||||
|
@ -12,7 +11,7 @@ class VolumeActionsListTileProvider extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => VolumeActionsListTileBloc(context.get<SettingsInteractor>()),
|
||||
create: (context) => VolumeActionsListTileBloc(SettingsInteractorProvider.of(context)),
|
||||
child: const VolumeActionsListTile(),
|
||||
);
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue