mirror of
https://github.com/vodemn/m3_lightmeter.git
synced 2025-07-01 03:30:41 +00:00
Merge remote-tracking branch 'origin' into feature/iap
This commit is contained in:
commit
7358bc8e3e
64 changed files with 1416 additions and 1251 deletions
|
@ -19,8 +19,9 @@ on:
|
|||
|
||||
jobs:
|
||||
build:
|
||||
name: Build .apk
|
||||
runs-on: macos-11
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- uses: webfactory/ssh-agent@v0.8.0
|
||||
|
@ -79,7 +80,7 @@ jobs:
|
|||
- 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 c -t lib/main_$FLAVOR.dart
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
156
.github/workflows/cd_prod.yml
vendored
156
.github/workflows/cd_prod.yml
vendored
|
@ -1,156 +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: webfactory/ssh-agent@v0.8.0
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.M3_LIGHTMETER_IAP_KEY }}
|
||||
|
||||
- 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"
|
||||
flutter-version: '3.10.0'
|
||||
|
||||
- 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 }}"
|
276
.github/workflows/create_release.yml
vendored
Normal file
276
.github/workflows/create_release.yml
vendored
Normal file
|
@ -0,0 +1,276 @@
|
|||
# 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
|
||||
|
||||
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
|
||||
|
||||
- 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: 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"
|
||||
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: 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-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
|
4
.vscode/launch.json
vendored
4
.vscode/launch.json
vendored
|
@ -12,7 +12,7 @@
|
|||
"--flavor",
|
||||
"dev",
|
||||
"--dart-define",
|
||||
"cameraPreviewAspectRatio=2/3",
|
||||
"cameraPreviewAspectRatio=240/320",
|
||||
],
|
||||
"program": "${workspaceFolder}/lib/main_dev.dart",
|
||||
},
|
||||
|
@ -37,7 +37,7 @@
|
|||
"--flavor",
|
||||
"prod",
|
||||
"--dart-define",
|
||||
"cameraPreviewAspectRatio=2/3",
|
||||
"cameraPreviewAspectRatio=240/320",
|
||||
],
|
||||
"program": "${workspaceFolder}/lib/main_prod.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",
|
||||
],
|
||||
|
|
|
@ -60,11 +60,11 @@ flutter pub get
|
|||
flutter pub run intl_utils:generate
|
||||
```
|
||||
|
||||
### 4. Build
|
||||
### 4. Build (Android)
|
||||
|
||||
You can build an apk by running the following command from the root of the repository:
|
||||
```console
|
||||
flutter build apk --release --flavor $FLAVOR --dart-define cameraPreviewAspectRatio=2/3 -t lib/main_$FLAVOR.dart
|
||||
flutter build apk --release --flavor $FLAVOR --dart-define cameraPreviewAspectRatio=240/320 -t lib/main_$FLAVOR.dart
|
||||
```
|
||||
Just replace `$FLAVOR` with `dev` or `prod`.
|
||||
|
||||
|
|
|
@ -1,13 +1,22 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:lightmeter/data/caffeine_service.dart';
|
||||
import 'package:lightmeter/data/haptics_service.dart';
|
||||
import 'package:lightmeter/data/light_sensor_service.dart';
|
||||
import 'package:lightmeter/data/models/supported_locale.dart';
|
||||
import 'package:lightmeter/data/permissions_service.dart';
|
||||
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||
import 'package:lightmeter/data/volume_events_service.dart';
|
||||
import 'package:lightmeter/environment.dart';
|
||||
import 'package:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/providers.dart';
|
||||
import 'package:lightmeter/providers/equipment_profile_provider.dart';
|
||||
import 'package:lightmeter/providers/services_provider.dart';
|
||||
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
||||
import 'package:lightmeter/screens/metering/flow_metering.dart';
|
||||
import 'package:lightmeter/screens/settings/flow_settings.dart';
|
||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||
import 'package:platform/platform.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class Application extends StatelessWidget {
|
||||
final Environment env;
|
||||
|
@ -16,56 +25,71 @@ class Application extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LightmeterProviders(
|
||||
env: env,
|
||||
builder: (context, ready) => ready
|
||||
? _AnnotatedRegionWrapper(
|
||||
child: MaterialApp(
|
||||
theme: context.listen<ThemeData>(),
|
||||
locale: Locale(context.listen<SupportedLocale>().intlName),
|
||||
localizationsDelegates: const [
|
||||
S.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: S.delegate.supportedLocales,
|
||||
builder: (context, child) => MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
|
||||
child: child!,
|
||||
return FutureBuilder(
|
||||
future: Future.wait([
|
||||
SharedPreferences.getInstance(),
|
||||
const LightSensorService(LocalPlatform()).hasSensor(),
|
||||
]),
|
||||
builder: (_, snapshot) {
|
||||
if (snapshot.data != null) {
|
||||
return 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: EquipmentProfileProvider(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final theme = UserPreferencesProvider.themeOf(context);
|
||||
final systemIconsBrightness =
|
||||
ThemeData.estimateBrightnessForColor(theme.colorScheme.onSurface);
|
||||
return AnnotatedRegion(
|
||||
value: SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarBrightness: systemIconsBrightness == Brightness.light
|
||||
? Brightness.dark
|
||||
: Brightness.light,
|
||||
statusBarIconBrightness: systemIconsBrightness,
|
||||
systemNavigationBarColor: Colors.transparent,
|
||||
systemNavigationBarIconBrightness: systemIconsBrightness,
|
||||
),
|
||||
child: MaterialApp(
|
||||
theme: theme,
|
||||
locale: Locale(UserPreferencesProvider.localeOf(context).intlName),
|
||||
localizationsDelegates: const [
|
||||
S.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: S.delegate.supportedLocales,
|
||||
builder: (context, child) => MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
|
||||
child: child!,
|
||||
),
|
||||
initialRoute: "metering",
|
||||
routes: {
|
||||
"metering": (context) => const MeteringFlow(),
|
||||
"settings": (context) => const SettingsFlow(),
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
initialRoute: "metering",
|
||||
routes: {
|
||||
"metering": (context) => const MeteringFlow(),
|
||||
"settings": (context) => const SettingsFlow(),
|
||||
},
|
||||
),
|
||||
)
|
||||
: const SizedBox(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AnnotatedRegionWrapper extends StatelessWidget {
|
||||
final Widget child;
|
||||
|
||||
const _AnnotatedRegionWrapper({required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final systemIconsBrightness = ThemeData.estimateBrightnessForColor(
|
||||
context.listen<ThemeData>().colorScheme.onSurface,
|
||||
);
|
||||
return AnnotatedRegion(
|
||||
value: SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarBrightness:
|
||||
systemIconsBrightness == Brightness.light ? Brightness.dark : Brightness.light,
|
||||
statusBarIconBrightness: systemIconsBrightness,
|
||||
systemNavigationBarColor: Colors.transparent,
|
||||
systemNavigationBarIconBrightness: systemIconsBrightness,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
} else if (snapshot.error != null) {
|
||||
return Center(child: Text(snapshot.error!.toString()));
|
||||
}
|
||||
|
||||
// TODO(@vodemn): maybe user splashscreen instead
|
||||
return const SizedBox();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
enum MeteringScreenLayoutFeature {
|
||||
extremeExposurePairs,
|
||||
filmPicker,
|
||||
histogram,
|
||||
equipmentProfiles,
|
||||
}
|
||||
|
||||
|
|
|
@ -98,6 +98,7 @@ class UserPreferencesService {
|
|||
MeteringScreenLayoutFeature.equipmentProfiles: true,
|
||||
MeteringScreenLayoutFeature.extremeExposurePairs: true,
|
||||
MeteringScreenLayoutFeature.filmPicker: true,
|
||||
MeteringScreenLayoutFeature.histogram: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
"meteringScreenLayoutHint": "Hide elements on the metering screen that you don't need so that they don't waste exposure pairs list space.",
|
||||
"meteringScreenFeatureExtremeExposurePairs": "Fastest & shortest exposure pairs",
|
||||
"meteringScreenFeatureFilmPicker": "Film picker",
|
||||
"meteringScreenFeatureHistogram": "Histogram",
|
||||
"film": "Film",
|
||||
"equipment": "Equipment",
|
||||
"equipmentProfileName": "Equipment profile name",
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
"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.",
|
||||
"meteringScreenFeatureExtremeExposurePairs": "Paires d'exposition les plus rapides et les plus courtes",
|
||||
"meteringScreenFeatureFilmPicker": "Sélecteur de film",
|
||||
"meteringScreenFeatureHistogram": "Histogramme",
|
||||
"film": "Pellicule",
|
||||
"equipment": "Équipement",
|
||||
"equipmentProfileName": "Nom du profil de l'équipement",
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
"meteringScreenLayoutHint": "Здесь вы можете скрыть некоторые ненужные или неиспользуемые элементы с главного экрана.",
|
||||
"meteringScreenFeatureExtremeExposurePairs": "Длинная и короткая выдержки",
|
||||
"meteringScreenFeatureFilmPicker": "Выбор пленки",
|
||||
"meteringScreenFeatureHistogram": "Гистограмма",
|
||||
"film": "Пленка",
|
||||
"equipment": "Оборудование",
|
||||
"equipmentProfileName": "Название профиля",
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
"meteringScreenLayoutHint": "隐藏不需要的元素,以免浪费曝光列表空间",
|
||||
"meteringScreenFeatureExtremeExposurePairs": "最快 & 最慢曝光组合",
|
||||
"meteringScreenFeatureFilmPicker": "胶片选择",
|
||||
"meteringScreenFeatureHistogram": "直方图",
|
||||
"film": "胶片",
|
||||
"equipment": "设备",
|
||||
"equipmentProfileName": "设备配置名称",
|
||||
|
|
|
@ -1,80 +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/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:m3_lightmeter_iap/m3_lightmeter_iap.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) {
|
||||
final sharedPrefs = snapshot.data![0] as SharedPreferences;
|
||||
return IAPProviders(
|
||||
sharedPreferences: sharedPrefs,
|
||||
child: InheritedWidgetBase<Environment>(
|
||||
data: env.copyWith(hasLightSensor: snapshot.data![1] as bool),
|
||||
child: InheritedWidgetBase<UserPreferencesService>(
|
||||
data: UserPreferencesService(sharedPrefs),
|
||||
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: 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,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;
|
||||
}
|
||||
}
|
|
@ -25,6 +25,7 @@ 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;
|
||||
|
|
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),
|
||||
);
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/data/models/ev_source_type.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,7 +42,7 @@ 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,
|
||||
),
|
||||
|
|
|
@ -123,7 +123,7 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
|
|||
(camera) => camera.lensDirection == CameraLensDirection.back,
|
||||
orElse: () => cameras.last,
|
||||
),
|
||||
ResolutionPreset.medium,
|
||||
ResolutionPreset.low,
|
||||
enableAudio: false,
|
||||
);
|
||||
|
||||
|
@ -205,7 +205,13 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
|
|||
|
||||
Future<double?> _takePhoto() async {
|
||||
try {
|
||||
// https://github.com/flutter/flutter/issues/84957#issuecomment-1661155095
|
||||
await _cameraController!.setFocusMode(FocusMode.locked);
|
||||
await _cameraController!.setExposureMode(ExposureMode.locked);
|
||||
final file = await _cameraController!.takePicture();
|
||||
await _cameraController!.setFocusMode(FocusMode.auto);
|
||||
await _cameraController!.setExposureMode(ExposureMode.auto);
|
||||
|
||||
final Uint8List bytes = await file.readAsBytes();
|
||||
Directory(file.path).deleteSync(recursive: true);
|
||||
|
||||
|
|
|
@ -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,62 @@
|
|||
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
|
||||
? ValueListenableBuilder<CameraValue>(
|
||||
valueListenable: widget.controller!,
|
||||
builder: (_, __, ___) => widget.controller!.value.isInitialized
|
||||
? 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(),
|
||||
)
|
||||
: CameraViewPlaceholder(error: widget.error),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -2,12 +2,11 @@ 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 {
|
||||
|
@ -39,7 +38,7 @@ 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(
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
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/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';
|
||||
|
@ -45,63 +46,97 @@ 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;
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: MeteringTopBar(
|
||||
readingsContainer: ReadingsContainer(
|
||||
fastest: fastest,
|
||||
slowest: slowest,
|
||||
film: film,
|
||||
iso: iso,
|
||||
nd: nd,
|
||||
onFilmChanged: onFilmChanged,
|
||||
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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (MeteringScreenLayout.featureOf(
|
||||
double _meteringContainerHeight(BuildContext context) {
|
||||
double enabledFeaturesHeight = 0;
|
||||
if (UserPreferencesProvider.meteringScreenFeatureOf(
|
||||
context,
|
||||
MeteringScreenLayoutFeature.equipmentProfiles,
|
||||
)) {
|
||||
topBarOverflow += Dimens.readingContainerSingleValueHeight;
|
||||
topBarOverflow += Dimens.paddingS;
|
||||
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.of(context).size.width - Dimens.grid8 - 2 * Dimens.paddingM) / 2) /
|
||||
PlatformConfig.cameraPreviewAspectRatio;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -110,20 +145,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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -164,11 +190,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,
|
||||
);
|
||||
},
|
||||
|
@ -176,43 +204,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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,11 +2,10 @@ 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 {
|
||||
|
@ -38,7 +37,7 @@ class LightSensorContainerProvider extends StatelessWidget {
|
|||
return BlocProvider(
|
||||
lazy: false,
|
||||
create: (context) => LightSensorContainerBloc(
|
||||
context.get<MeteringInteractor>(),
|
||||
MeteringInteractorProvider.of(context),
|
||||
context.read<MeteringCommunicationBloc>(),
|
||||
),
|
||||
child: LightSensorContainer(
|
||||
|
|
|
@ -12,70 +12,72 @@ 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
|
||||
? const EmptyExposurePairsList()
|
||||
: 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(
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
|
|
|
@ -3,8 +3,7 @@ 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/generated/l10n.dart';
|
||||
|
||||
import 'package:lightmeter/providers/metering_screen_layout_provider.dart';
|
||||
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
||||
import 'package:lightmeter/res/dimens.dart';
|
||||
import 'package: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';
|
||||
|
@ -38,14 +37,14 @@ class ReadingsContainer extends StatelessWidget {
|
|||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (MeteringScreenLayout.featureOf(
|
||||
if (UserPreferencesProvider.meteringScreenFeatureOf(
|
||||
context,
|
||||
MeteringScreenLayoutFeature.equipmentProfiles,
|
||||
MeteringScreenLayoutFeature.extremeExposurePairs,
|
||||
)) ...[
|
||||
const _EquipmentProfilePicker(),
|
||||
const _InnerPadding(),
|
||||
],
|
||||
if (MeteringScreenLayout.featureOf(
|
||||
if (UserPreferencesProvider.meteringScreenFeatureOf(
|
||||
context,
|
||||
MeteringScreenLayoutFeature.extremeExposurePairs,
|
||||
)) ...[
|
||||
|
@ -63,7 +62,7 @@ class ReadingsContainer extends StatelessWidget {
|
|||
),
|
||||
const _InnerPadding(),
|
||||
],
|
||||
if (MeteringScreenLayout.featureOf(
|
||||
if (UserPreferencesProvider.meteringScreenFeatureOf(
|
||||
context,
|
||||
MeteringScreenLayoutFeature.filmPicker,
|
||||
)) ...[
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -6,17 +6,17 @@ import 'package:lightmeter/data/models/ev_source_type.dart';
|
|||
import 'package:lightmeter/data/models/exposure_pair.dart';
|
||||
import 'package:lightmeter/data/models/film.dart';
|
||||
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
|
||||
import 'package:lightmeter/environment.dart';
|
||||
import 'package:lightmeter/providers/ev_source_type_provider.dart';
|
||||
import 'package:lightmeter/providers/equipment_profile_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/screens/metering/utils/equipment_profile_listener.dart';
|
||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.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_resources/m3_lightmeter_resources.dart';
|
||||
|
||||
class MeteringScreen extends StatelessWidget {
|
||||
|
@ -47,8 +47,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: () {
|
||||
|
@ -77,15 +77,15 @@ class _InheritedListeners extends StatelessWidget {
|
|||
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()));
|
||||
}
|
||||
},
|
||||
child: InheritedModelAspectListener<MeteringScreenLayoutFeature, bool>(
|
||||
aspect: MeteringScreenLayoutFeature.equipmentProfiles,
|
||||
child: MeteringScreenLayoutFeatureListener(
|
||||
feature: MeteringScreenLayoutFeature.equipmentProfiles,
|
||||
onDidChangeDependencies: (value) {
|
||||
if (!value) {
|
||||
EquipmentProfileProvider.of(context).setProfile(EquipmentProfiles.of(context).first);
|
||||
|
@ -122,14 +122,15 @@ class MeteringContainerBuidler extends StatelessWidget {
|
|||
final exposurePairs = ev != null
|
||||
? buildExposureValues(
|
||||
ev!,
|
||||
context.listen<StopType>(),
|
||||
UserPreferencesProvider.stopTypeOf(context),
|
||||
EquipmentProfiles.selectedOf(context),
|
||||
film,
|
||||
)
|
||||
: <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,
|
||||
|
|
|
@ -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:lightmeter/providers/equipment_profile_provider.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/providers/user_preferences_provider.dart';
|
||||
import 'package:lightmeter/screens/settings/components/shared/dialog_picker.dart/widget_dialog_picker.dart';
|
||||
import 'package:lightmeter/utils/inherited_generics.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(),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:lightmeter/interactors/settings_interactor.dart';
|
||||
|
||||
import 'package:lightmeter/screens/settings/components/metering/components/calibration/components/calibration_dialog/bloc_dialog_calibration.dart';
|
||||
import 'package:lightmeter/screens/settings/components/metering/components/calibration/components/calibration_dialog/widget_dialog_calibration.dart';
|
||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||
import 'package:lightmeter/screens/settings/flow_settings.dart';
|
||||
|
||||
class CalibrationDialogProvider extends StatelessWidget {
|
||||
const CalibrationDialogProvider({super.key});
|
||||
|
@ -12,7 +11,7 @@ class CalibrationDialogProvider extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => CalibrationDialogBloc(context.get<SettingsInteractor>()),
|
||||
create: (context) => CalibrationDialogBloc(SettingsInteractorProvider.of(context)),
|
||||
child: const CalibrationDialog(),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:lightmeter/environment.dart';
|
||||
import 'package:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/providers/services_provider.dart';
|
||||
import 'package:lightmeter/res/dimens.dart';
|
||||
import 'package:lightmeter/screens/settings/components/metering/components/calibration/components/calibration_dialog/bloc_dialog_calibration.dart';
|
||||
import 'package:lightmeter/screens/settings/components/metering/components/calibration/components/calibration_dialog/event_dialog_calibration.dart';
|
||||
import 'package:lightmeter/screens/settings/components/metering/components/calibration/components/calibration_dialog/state_dialog_calibration.dart';
|
||||
import 'package:lightmeter/screens/shared/centered_slider/widget_slider_centered.dart';
|
||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||
import 'package:lightmeter/utils/to_string_signed.dart';
|
||||
|
||||
class CalibrationDialog extends StatelessWidget {
|
||||
|
@ -15,7 +14,7 @@ class CalibrationDialog extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool hasLightSensor = context.get<Environment>().hasLightSensor;
|
||||
final bool hasLightSensor = ServicesProvider.of(context).environment.hasLightSensor;
|
||||
return AlertDialog(
|
||||
icon: const Icon(Icons.settings_brightness),
|
||||
titlePadding: Dimens.dialogIconTitlePadding,
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/interactors/settings_interactor.dart';
|
||||
import 'package:lightmeter/screens/settings/components/metering/components/calibration/components/calibration_dialog/provider_dialog_calibration.dart';
|
||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||
import 'package:lightmeter/screens/settings/flow_settings.dart';
|
||||
|
||||
class CalibrationListTile extends StatelessWidget {
|
||||
const CalibrationListTile({super.key});
|
||||
|
@ -15,8 +14,8 @@ class CalibrationListTile extends StatelessWidget {
|
|||
onTap: () {
|
||||
showDialog<double>(
|
||||
context: context,
|
||||
builder: (_) => InheritedWidgetBase(
|
||||
data: context.get<SettingsInteractor>(),
|
||||
builder: (_) => SettingsInteractorProvider(
|
||||
data: SettingsInteractorProvider.of(context),
|
||||
child: const CalibrationDialogProvider(),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/providers/equipment_profile_provider.dart';
|
||||
|
||||
import 'package:lightmeter/res/dimens.dart';
|
||||
import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart';
|
||||
import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_name_dialog/widget_dialog_equipment_profile_name.dart';
|
||||
import 'package:lightmeter/screens/shared/sliver_screen/screen_sliver.dart';
|
||||
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||
|
||||
class EquipmentProfilesScreen extends StatefulWidget {
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/providers/stop_type_provider.dart';
|
||||
import 'package:lightmeter/providers/user_preferences_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:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||
|
||||
class StopTypeListTile extends StatelessWidget {
|
||||
|
@ -13,20 +12,20 @@ class StopTypeListTile extends StatelessWidget {
|
|||
return ListTile(
|
||||
leading: const Icon(Icons.straighten),
|
||||
title: Text(S.of(context).fractionalStops),
|
||||
trailing: Text(_typeToString(context, context.listen<StopType>())),
|
||||
trailing: Text(_typeToString(context, UserPreferencesProvider.stopTypeOf(context))),
|
||||
onTap: () {
|
||||
showDialog<StopType>(
|
||||
context: context,
|
||||
builder: (_) => DialogPicker<StopType>(
|
||||
icon: Icons.straighten,
|
||||
title: S.of(context).showFractionalStops,
|
||||
selectedValue: context.get<StopType>(),
|
||||
selectedValue: UserPreferencesProvider.stopTypeOf(context),
|
||||
values: StopType.values,
|
||||
titleAdapter: _typeToString,
|
||||
),
|
||||
).then((value) {
|
||||
if (value != null) {
|
||||
StopTypeProvider.of(context).set(value);
|
||||
UserPreferencesProvider.of(context).setStopType(value);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
|
||||
import 'package:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/providers/metering_screen_layout_provider.dart';
|
||||
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
||||
import 'package:lightmeter/res/dimens.dart';
|
||||
|
||||
class MeteringScreenLayoutFeaturesDialog extends StatefulWidget {
|
||||
|
@ -14,7 +14,7 @@ class MeteringScreenLayoutFeaturesDialog extends StatefulWidget {
|
|||
|
||||
class _MeteringScreenLayoutFeaturesDialogState extends State<MeteringScreenLayoutFeaturesDialog> {
|
||||
late final _features =
|
||||
MeteringScreenLayoutConfig.from(MeteringScreenLayout.of(context, listen: false));
|
||||
MeteringScreenLayoutConfig.from(UserPreferencesProvider.meteringScreenConfigOf(context));
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -47,7 +47,7 @@ class _MeteringScreenLayoutFeaturesDialogState extends State<MeteringScreenLayou
|
|||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
MeteringScreenLayoutProvider.of(context).updateFeatures(_features);
|
||||
UserPreferencesProvider.of(context).setMeteringScreenLayout(_features);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(S.of(context).save),
|
||||
|
@ -77,6 +77,8 @@ class _MeteringScreenLayoutFeaturesDialogState extends State<MeteringScreenLayou
|
|||
return S.of(context).meteringScreenFeatureExtremeExposurePairs;
|
||||
case MeteringScreenLayoutFeature.filmPicker:
|
||||
return S.of(context).meteringScreenFeatureFilmPicker;
|
||||
case MeteringScreenLayoutFeature.histogram:
|
||||
return S.of(context).meteringScreenFeatureHistogram;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/data/models/dynamic_colors_state.dart';
|
||||
import 'package:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/providers/theme_provider.dart';
|
||||
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
||||
import 'package:lightmeter/res/dimens.dart';
|
||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||
|
||||
class DynamicColorListTile extends StatelessWidget {
|
||||
const DynamicColorListTile({super.key});
|
||||
|
@ -13,8 +12,8 @@ class DynamicColorListTile extends StatelessWidget {
|
|||
return SwitchListTile(
|
||||
secondary: const Icon(Icons.colorize),
|
||||
title: Text(S.of(context).dynamicColor),
|
||||
value: context.listen<DynamicColorState>() == DynamicColorState.enabled,
|
||||
onChanged: ThemeProvider.of(context).enableDynamicColor,
|
||||
value: UserPreferencesProvider.dynamicColorStateOf(context) == DynamicColorState.enabled,
|
||||
onChanged: UserPreferencesProvider.of(context).enableDynamicColor,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/providers/theme_provider.dart';
|
||||
import 'package:lightmeter/res/dimens.dart';
|
||||
import 'package:lightmeter/res/theme.dart';
|
||||
import 'package:lightmeter/screens/shared/filled_circle/widget_circle_filled.dart';
|
||||
|
||||
class PrimaryColorDialogPicker extends StatefulWidget {
|
||||
|
@ -38,9 +38,9 @@ class _PrimaryColorDialogPickerState extends State<PrimaryColorDialogPicker> {
|
|||
padding: EdgeInsets.zero,
|
||||
child: Row(
|
||||
children: List.generate(
|
||||
ThemeProvider.primaryColorsList.length,
|
||||
primaryColorsList.length,
|
||||
(index) {
|
||||
final color = ThemeProvider.primaryColorsList[index];
|
||||
final color = primaryColorsList[index];
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(left: index == 0 ? 0 : Dimens.paddingS),
|
||||
child: _SelectableColorItem(
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/data/models/dynamic_colors_state.dart';
|
||||
import 'package:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/providers/theme_provider.dart';
|
||||
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
||||
import 'package:lightmeter/res/dimens.dart';
|
||||
import 'package:lightmeter/screens/settings/components/theme/components/primary_color/components/primary_color_picker_dialog/widget_dialog_picker_primary_color.dart';
|
||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||
|
||||
class PrimaryColorListTile extends StatelessWidget {
|
||||
const PrimaryColorListTile({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (context.listen<DynamicColorState>() == DynamicColorState.enabled) {
|
||||
if (UserPreferencesProvider.dynamicColorStateOf(context) == DynamicColorState.enabled) {
|
||||
return Opacity(
|
||||
opacity: Dimens.disabledOpacity,
|
||||
child: IgnorePointer(
|
||||
|
@ -31,7 +30,7 @@ class PrimaryColorListTile extends StatelessWidget {
|
|||
builder: (_) => const PrimaryColorDialogPicker(),
|
||||
).then((value) {
|
||||
if (value != null) {
|
||||
ThemeProvider.of(context).setPrimaryColor(value);
|
||||
UserPreferencesProvider.of(context).setPrimaryColor(value);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/data/models/theme_type.dart';
|
||||
import 'package:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/providers/theme_provider.dart';
|
||||
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
||||
import 'package:lightmeter/screens/settings/components/shared/dialog_picker.dart/widget_dialog_picker.dart';
|
||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||
|
||||
class ThemeTypeListTile extends StatelessWidget {
|
||||
const ThemeTypeListTile({super.key});
|
||||
|
@ -13,20 +12,20 @@ class ThemeTypeListTile extends StatelessWidget {
|
|||
return ListTile(
|
||||
leading: const Icon(Icons.brightness_6),
|
||||
title: Text(S.of(context).theme),
|
||||
trailing: Text(_typeToString(context, context.listen<ThemeType>())),
|
||||
trailing: Text(_typeToString(context, UserPreferencesProvider.themeTypeOf(context))),
|
||||
onTap: () {
|
||||
showDialog<ThemeType>(
|
||||
context: context,
|
||||
builder: (_) => DialogPicker<ThemeType>(
|
||||
icon: Icons.brightness_6,
|
||||
title: S.of(context).chooseTheme,
|
||||
selectedValue: context.get<ThemeType>(),
|
||||
selectedValue: UserPreferencesProvider.themeTypeOf(context),
|
||||
values: ThemeType.values,
|
||||
titleAdapter: _typeToString,
|
||||
),
|
||||
).then((value) {
|
||||
if (value != null) {
|
||||
ThemeProvider.of(context).setThemeType(value);
|
||||
UserPreferencesProvider.of(context).setThemeType(value);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/data/models/dynamic_colors_state.dart';
|
||||
import 'package:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
||||
import 'package:lightmeter/screens/settings/components/shared/settings_section/widget_settings_section.dart';
|
||||
import 'package:lightmeter/screens/settings/components/theme/components/dynamic_color/widget_list_tile_dynamic_color.dart';
|
||||
import 'package:lightmeter/screens/settings/components/theme/components/primary_color/widget_list_tile_primary_color.dart';
|
||||
import 'package:lightmeter/screens/settings/components/theme/components/theme_type/widget_list_tile_theme_type.dart';
|
||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||
|
||||
class ThemeSettingsSection extends StatelessWidget {
|
||||
const ThemeSettingsSection({super.key});
|
||||
|
@ -17,7 +17,7 @@ class ThemeSettingsSection extends StatelessWidget {
|
|||
children: [
|
||||
const ThemeTypeListTile(),
|
||||
const PrimaryColorListTile(),
|
||||
if (context.get<DynamicColorState>() != DynamicColorState.unavailable)
|
||||
if (UserPreferencesProvider.dynamicColorStateOf(context) != DynamicColorState.unavailable)
|
||||
const DynamicColorListTile(),
|
||||
],
|
||||
);
|
||||
|
|
|
@ -1,25 +1,38 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/data/caffeine_service.dart';
|
||||
import 'package:lightmeter/data/haptics_service.dart';
|
||||
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||
import 'package:lightmeter/data/volume_events_service.dart';
|
||||
import 'package:lightmeter/interactors/settings_interactor.dart';
|
||||
import 'package:lightmeter/providers/services_provider.dart';
|
||||
import 'package:lightmeter/screens/settings/screen_settings.dart';
|
||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||
|
||||
class SettingsFlow extends StatelessWidget {
|
||||
const SettingsFlow({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InheritedWidgetBase<SettingsInteractor>(
|
||||
return SettingsInteractorProvider(
|
||||
data: SettingsInteractor(
|
||||
context.get<UserPreferencesService>(),
|
||||
context.get<CaffeineService>(),
|
||||
context.get<HapticsService>(),
|
||||
context.get<VolumeEventsService>(),
|
||||
ServicesProvider.of(context).userPreferencesService,
|
||||
ServicesProvider.of(context).caffeineService,
|
||||
ServicesProvider.of(context).hapticsService,
|
||||
ServicesProvider.of(context).volumeEventsService,
|
||||
),
|
||||
child: const SettingsScreen(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsInteractorProvider extends InheritedWidget {
|
||||
final SettingsInteractor data;
|
||||
|
||||
const SettingsInteractorProvider({
|
||||
required this.data,
|
||||
required super.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
static SettingsInteractor of(BuildContext context) {
|
||||
return context.findAncestorWidgetOfExactType<SettingsInteractorProvider>()!.data;
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(SettingsInteractorProvider oldWidget) => false;
|
||||
}
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/interactors/settings_interactor.dart';
|
||||
import 'package:lightmeter/screens/settings/components/about/widget_settings_section_about.dart';
|
||||
import 'package:lightmeter/screens/settings/components/general/widget_settings_section_general.dart';
|
||||
import 'package:lightmeter/screens/settings/components/metering/widget_settings_section_metering.dart';
|
||||
import 'package:lightmeter/screens/settings/components/theme/widget_settings_section_theme.dart';
|
||||
import 'package:lightmeter/screens/settings/flow_settings.dart';
|
||||
import 'package:lightmeter/screens/shared/sliver_screen/screen_sliver.dart';
|
||||
import 'package:lightmeter/utils/inherited_generics.dart';
|
||||
|
||||
class SettingsScreen extends StatefulWidget {
|
||||
const SettingsScreen({super.key});
|
||||
|
@ -19,12 +18,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
context.get<SettingsInteractor>().disableVolumeHandling();
|
||||
SettingsInteractorProvider.of(context).disableVolumeHandling();
|
||||
}
|
||||
|
||||
@override
|
||||
void deactivate() {
|
||||
context.get<SettingsInteractor>().restoreVolumeHandling();
|
||||
SettingsInteractorProvider.of(context).restoreVolumeHandling();
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,171 +0,0 @@
|
|||
import 'package:flutter/widgets.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 InheritedWidgetListener<T> extends StatefulWidget {
|
||||
final ValueChanged<T> onDidChangeDependencies;
|
||||
final Widget child;
|
||||
|
||||
const InheritedWidgetListener({
|
||||
required this.onDidChangeDependencies,
|
||||
required this.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<InheritedWidgetListener<T>> createState() => _InheritedWidgetListenerState<T>();
|
||||
}
|
||||
|
||||
class _InheritedWidgetListenerState<T> extends State<InheritedWidgetListener<T>> {
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
widget.onDidChangeDependencies(context.listen<T>());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.child;
|
||||
}
|
||||
}
|
||||
|
||||
class InheritedWidgetBase<T> extends InheritedWidget {
|
||||
final T data;
|
||||
|
||||
const InheritedWidgetBase({
|
||||
required this.data,
|
||||
required super.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
static T of<T>(BuildContext context, {bool listen = true}) {
|
||||
if (listen) {
|
||||
return context.dependOnInheritedWidgetOfExactType<InheritedWidgetBase<T>>()!.data;
|
||||
} else {
|
||||
return context.findAncestorWidgetOfExactType<InheritedWidgetBase<T>>()!.data;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(InheritedWidgetBase<T> oldWidget) => true;
|
||||
}
|
||||
|
||||
extension InheritedWidgetBaseContext on BuildContext {
|
||||
T get<T>() {
|
||||
return InheritedWidgetBase.of<T>(this, listen: false);
|
||||
}
|
||||
|
||||
T listen<T>() {
|
||||
return InheritedWidgetBase.of<T>(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 InheritedModelAspectListener<A extends Object, T> extends StatefulWidget {
|
||||
final A aspect;
|
||||
final ValueChanged<T> onDidChangeDependencies;
|
||||
final Widget child;
|
||||
|
||||
const InheritedModelAspectListener({
|
||||
required this.aspect,
|
||||
required this.onDidChangeDependencies,
|
||||
required this.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<InheritedModelAspectListener<A, T>> createState() =>
|
||||
_InheritedModelAspectListenerState<A, T>();
|
||||
}
|
||||
|
||||
class _InheritedModelAspectListenerState<A extends Object, T>
|
||||
extends State<InheritedModelAspectListener<A, T>> {
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
widget.onDidChangeDependencies(context.listenModelFeature<A, T>(widget.aspect));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.child;
|
||||
}
|
||||
}
|
||||
|
||||
class InheritedModelBase<A, T> extends InheritedModel<A> {
|
||||
final Map<A, T> data;
|
||||
|
||||
const InheritedModelBase({
|
||||
required this.data,
|
||||
required super.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
static Map<A, T> of<A, T>(BuildContext context, {bool listen = true}) {
|
||||
if (listen) {
|
||||
return context.dependOnInheritedWidgetOfExactType<InheritedModelBase<A, T>>()!.data;
|
||||
} else {
|
||||
return context.findAncestorWidgetOfExactType<InheritedModelBase<A, T>>()!.data;
|
||||
}
|
||||
}
|
||||
|
||||
static T featureOf<A extends Object, T>(BuildContext context, A aspect) {
|
||||
return InheritedModel.inheritFrom<InheritedModelBase<A, T>>(context, aspect: aspect)!
|
||||
.data[aspect]!;
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(InheritedModelBase oldWidget) => true;
|
||||
|
||||
@override
|
||||
bool updateShouldNotifyDependent(
|
||||
InheritedModelBase<A, T> oldWidget,
|
||||
Set<A> dependencies,
|
||||
) {
|
||||
for (final dependecy in dependencies) {
|
||||
if (oldWidget.data[dependecy] != data[dependecy]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
extension InheritedModelBaseContext on BuildContext {
|
||||
Map<A, T> getModel<A, T>() {
|
||||
return InheritedModelBase.of<A, T>(this, listen: false);
|
||||
}
|
||||
|
||||
Map<A, T> listenModel<A, T>() {
|
||||
return InheritedModelBase.of<A, T>(this);
|
||||
}
|
||||
|
||||
T listenModelFeature<A extends Object, T>(A aspect) {
|
||||
return InheritedModelBase.featureOf<A, T>(this, aspect);
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
name: lightmeter
|
||||
description: Lightmeter app inspired by Material 3 design system.
|
||||
publish_to: "none"
|
||||
version: 0.12.2+33
|
||||
version: 0.13.1+37
|
||||
|
||||
environment:
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
|
|
|
@ -12,11 +12,30 @@ void main() {
|
|||
'0': true,
|
||||
'1': true,
|
||||
'2': true,
|
||||
'3': true,
|
||||
},
|
||||
),
|
||||
{
|
||||
MeteringScreenLayoutFeature.extremeExposurePairs: true,
|
||||
MeteringScreenLayoutFeature.filmPicker: true,
|
||||
MeteringScreenLayoutFeature.histogram: true,
|
||||
MeteringScreenLayoutFeature.equipmentProfiles: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('Legacy (no histogram & equipment profiles)', () {
|
||||
expect(
|
||||
MeteringScreenLayoutConfigJson.fromJson(
|
||||
{
|
||||
'0': false,
|
||||
'1': false,
|
||||
},
|
||||
),
|
||||
{
|
||||
MeteringScreenLayoutFeature.extremeExposurePairs: false,
|
||||
MeteringScreenLayoutFeature.filmPicker: false,
|
||||
MeteringScreenLayoutFeature.histogram: true,
|
||||
MeteringScreenLayoutFeature.equipmentProfiles: true,
|
||||
},
|
||||
);
|
||||
|
@ -26,13 +45,15 @@ void main() {
|
|||
expect(
|
||||
MeteringScreenLayoutConfigJson.fromJson(
|
||||
{
|
||||
'0': true,
|
||||
'1': true,
|
||||
'0': false,
|
||||
'1': false,
|
||||
'2': false,
|
||||
},
|
||||
),
|
||||
{
|
||||
MeteringScreenLayoutFeature.extremeExposurePairs: true,
|
||||
MeteringScreenLayoutFeature.filmPicker: true,
|
||||
MeteringScreenLayoutFeature.extremeExposurePairs: false,
|
||||
MeteringScreenLayoutFeature.filmPicker: false,
|
||||
MeteringScreenLayoutFeature.histogram: false,
|
||||
MeteringScreenLayoutFeature.equipmentProfiles: true,
|
||||
},
|
||||
);
|
||||
|
@ -46,11 +67,13 @@ void main() {
|
|||
MeteringScreenLayoutFeature.equipmentProfiles: true,
|
||||
MeteringScreenLayoutFeature.extremeExposurePairs: true,
|
||||
MeteringScreenLayoutFeature.filmPicker: true,
|
||||
MeteringScreenLayoutFeature.histogram: true,
|
||||
}.toJson(),
|
||||
{
|
||||
'2': true,
|
||||
'3': true,
|
||||
'0': true,
|
||||
'1': true,
|
||||
'2': true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@ 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/providers/theme_provider.dart';
|
||||
import 'package:lightmeter/res/theme.dart';
|
||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
@ -194,6 +194,7 @@ void main() {
|
|||
MeteringScreenLayoutFeature.extremeExposurePairs: true,
|
||||
MeteringScreenLayoutFeature.filmPicker: true,
|
||||
MeteringScreenLayoutFeature.equipmentProfiles: true,
|
||||
MeteringScreenLayoutFeature.histogram: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
@ -208,6 +209,7 @@ void main() {
|
|||
MeteringScreenLayoutFeature.extremeExposurePairs: false,
|
||||
MeteringScreenLayoutFeature.filmPicker: true,
|
||||
MeteringScreenLayoutFeature.equipmentProfiles: true,
|
||||
MeteringScreenLayoutFeature.histogram: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
@ -216,17 +218,19 @@ void main() {
|
|||
when(
|
||||
() => sharedPreferences.setString(
|
||||
UserPreferencesService.meteringScreenLayoutKey,
|
||||
"""{"0":false,"1":true}""",
|
||||
"""{"0":false,"1":true,"2":true,"3":true}""",
|
||||
),
|
||||
).thenAnswer((_) => Future.value(true));
|
||||
service.meteringScreenLayout = {
|
||||
MeteringScreenLayoutFeature.extremeExposurePairs: false,
|
||||
MeteringScreenLayoutFeature.filmPicker: true,
|
||||
MeteringScreenLayoutFeature.histogram: true,
|
||||
MeteringScreenLayoutFeature.equipmentProfiles: true,
|
||||
};
|
||||
verify(
|
||||
() => sharedPreferences.setString(
|
||||
UserPreferencesService.meteringScreenLayoutKey,
|
||||
"""{"0":false,"1":true}""",
|
||||
"""{"0":false,"1":true,"2":true,"3":true}""",
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
@ -347,13 +351,13 @@ void main() {
|
|||
group('primaryColor', () {
|
||||
test('get default', () {
|
||||
when(() => sharedPreferences.getInt(UserPreferencesService.primaryColorKey)).thenReturn(null);
|
||||
expect(service.primaryColor, ThemeProvider.primaryColorsList[5]);
|
||||
expect(service.primaryColor, primaryColorsList[5]);
|
||||
});
|
||||
|
||||
test('get', () {
|
||||
when(() => sharedPreferences.getInt(UserPreferencesService.primaryColorKey))
|
||||
.thenReturn(0xff9c27b0);
|
||||
expect(service.primaryColor, ThemeProvider.primaryColorsList[2]);
|
||||
expect(service.primaryColor, primaryColorsList[2]);
|
||||
});
|
||||
|
||||
test('set', () {
|
||||
|
|
Loading…
Reference in a new issue