Merge remote-tracking branch 'origin' into feature/iap

This commit is contained in:
Vadim 2023-08-15 11:59:27 +02:00
commit 7358bc8e3e
64 changed files with 1416 additions and 1251 deletions

View file

@ -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

View file

@ -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
View 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
View file

@ -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
View file

@ -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",
],

View file

@ -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`.

View file

@ -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();
},
);
}
}

View file

@ -1,6 +1,7 @@
enum MeteringScreenLayoutFeature {
extremeExposurePairs,
filmPicker,
histogram,
equipmentProfiles,
}

View file

@ -98,6 +98,7 @@ class UserPreferencesService {
MeteringScreenLayoutFeature.equipmentProfiles: true,
MeteringScreenLayoutFeature.extremeExposurePairs: true,
MeteringScreenLayoutFeature.filmPicker: true,
MeteringScreenLayoutFeature.histogram: true,
};
}
}

View file

@ -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",

View file

@ -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",

View file

@ -38,6 +38,7 @@
"meteringScreenLayoutHint": "Здесь вы можете скрыть некоторые ненужные или неиспользуемые элементы с главного экрана.",
"meteringScreenFeatureExtremeExposurePairs": "Длинная и короткая выдержки",
"meteringScreenFeatureFilmPicker": "Выбор пленки",
"meteringScreenFeatureHistogram": "Гистограмма",
"film": "Пленка",
"equipment": "Оборудование",
"equipmentProfileName": "Название профиля",

View file

@ -38,6 +38,7 @@
"meteringScreenLayoutHint": "隐藏不需要的元素,以免浪费曝光列表空间",
"meteringScreenFeatureExtremeExposurePairs": "最快 & 最慢曝光组合",
"meteringScreenFeatureFilmPicker": "胶片选择",
"meteringScreenFeatureHistogram": "直方图",
"film": "胶片",
"equipment": "设备",
"equipmentProfileName": "设备配置名称",

View file

@ -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);
},
);
}
}

View file

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

View file

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

View file

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

View file

@ -1,42 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/utils/inherited_generics.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class StopTypeProvider extends StatefulWidget {
final Widget child;
const StopTypeProvider({required this.child, super.key});
static StopTypeProviderState of(BuildContext context) {
return context.findAncestorStateOfType<StopTypeProviderState>()!;
}
@override
State<StopTypeProvider> createState() => StopTypeProviderState();
}
class StopTypeProviderState extends State<StopTypeProvider> {
late StopType _stopType;
@override
void initState() {
super.initState();
_stopType = context.get<UserPreferencesService>().stopType;
}
@override
Widget build(BuildContext context) {
return InheritedWidgetBase<StopType>(
data: _stopType,
child: widget.child,
);
}
void set(StopType type) {
setState(() {
_stopType = type;
});
context.get<UserPreferencesService>().stopType = type;
}
}

View file

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

View file

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

View file

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

View file

@ -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
View file

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

View file

@ -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,
),

View file

@ -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);

View file

@ -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(),
),
);
}

View file

@ -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),
);
}
}

View file

@ -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(),
],
);
},
);
}
}

View file

@ -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),
),
],
),
),
);
}
}

View file

@ -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(

View file

@ -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,
),
),
],
),
),
);
}
}

View file

@ -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(

View file

@ -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),
),
),
],
),
),
),
],
),
),
),
],
);
}
}

View file

@ -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,
),
)
],
);

View file

@ -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,
)) ...[

View file

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

View file

@ -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,

View file

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

View file

@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
import 'package: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;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,9 +1,8 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/supported_locale.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/providers/supported_locale_provider.dart';
import 'package:lightmeter/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);
}
});
},

View file

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

View file

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

View file

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

View file

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

View file

@ -1,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 {

View file

@ -1,8 +1,7 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/providers/stop_type_provider.dart';
import 'package:lightmeter/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);
}
});
},

View file

@ -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;
}
}
}

View file

@ -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),
);
}

View file

@ -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(

View file

@ -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);
}
});
},

View file

@ -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);
}
});
},

View file

@ -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(),
],
);

View file

@ -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;
}

View file

@ -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();
}

View file

@ -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);
}
}

View file

@ -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"

View file

@ -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,
},
);
});

View file

@ -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', () {