From ed83540ddea241170acc88de8d2c0b0d6d582a02 Mon Sep 17 00:00:00 2001
From: Vadim <44135514+vodemn@users.noreply.github.com>
Date: Wed, 28 Jun 2023 17:53:54 +0200
Subject: [PATCH 1/6] ML-61 Allow pushes from Github Action to protected branch
(#85)
* Replaced zipping action
thedoctor0/zip-release@0.7.1 -> vimtor/action-zip@v1.1
* typo
* recursive: false
* typo
* typo
* debugSymbolLevel 'FULL'
* Update build.gradle
* Version bump
* wip
* wip
* `create-release` job
* removed changelog input
* added `needs`
* Version bump
* typo
* returned to macos-11 runner
* reverted pubspec version
* Version bump
* download artifacts
* Version bump
* extended artifacts path
* Version bump
* added LS
* Version bump
* Version bump
* rename files
* Version bump
* removed ls
* Version bump
* revert version
* typo
* added push to protected branch action
* run push on ubuntu-latest
* added branch name conditions
* Version bump
* typo
* Version bump
---
.github/workflows/cd_prod.yml | 14 ++++++++++----
pubspec.yaml | 2 +-
2 files changed, 11 insertions(+), 5 deletions(-)
diff --git a/.github/workflows/cd_prod.yml b/.github/workflows/cd_prod.yml
index f2f8ca2..9ff3d6e 100644
--- a/.github/workflows/cd_prod.yml
+++ b/.github/workflows/cd_prod.yml
@@ -101,7 +101,7 @@ jobs:
update-version-in-repo:
name: Update repo version
needs: [build]
- runs-on: macos-11
+ runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
@@ -110,18 +110,24 @@ jobs:
- 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 and push changes
+ - 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"
- git push
+
+ - 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
diff --git a/pubspec.yaml b/pubspec.yaml
index 8c35dd6..8a230c8 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,7 +1,7 @@
name: lightmeter
description: A new Flutter project.
publish_to: "none"
-version: 0.11.5+28
+version: 0.11.8+30
environment:
sdk: ">=3.0.0 <4.0.0"
From e001c153fb6041048196b432e602306564b53533 Mon Sep 17 00:00:00 2001
From: Vadim <44135514+vodemn@users.noreply.github.com>
Date: Sun, 9 Jul 2023 13:39:33 +0200
Subject: [PATCH 2/6] ML-11 Implement volume buttons actions (#86)
* [Android] wip
* implemented `VolumeEventsService`
* implemented `VolumeKeysListener` (wip)
* Added screenshots links
* [Android] nullable typo
* implemented `VolumeKeysNotifier`
* deinitialize camera when on Settings screen
* disable volume handling when on Settings screen
* used "platform" package to mock `isAndroid`
* init/deinit camera on settings open
* allow volume action override only on metering screen
* lints
* cleanup
* await dispose
* tests
* reduced `SwitchListTile.contentPadding`
* fixed tests
* removed `VolumeAction.zoom`
* added social preview
* typo
* fixed `CameraContainerBloc` tests
* added `Stream.empty()` tests
---
README.md | 58 ++++++-------
.../com/vodemn/lightmeter/MainActivity.kt | 62 +++++++++++++-
lib/data/light_sensor_service.dart | 15 +++-
lib/data/models/volume_action.dart | 3 +
lib/data/shared_prefs_service.dart | 15 +++-
lib/data/volume_events_service.dart | 40 +++++++++
lib/interactors/metering_interactor.dart | 18 ++---
lib/interactors/settings_interactor.dart | 18 +++++
lib/l10n/intl_en.arb | 1 +
lib/l10n/intl_fr.arb | 1 +
lib/l10n/intl_ru.arb | 1 +
lib/main_prod.dart | 1 -
lib/providers.dart | 31 +++----
lib/screens/metering/bloc_metering.dart | 23 ++++++
.../bloc_communication_metering.dart | 2 +
.../event_communication_metering.dart | 8 ++
.../state_communication_metering.dart | 8 ++
.../bloc_container_camera.dart | 60 +++++++++-----
.../bloc_container_light_sensor.dart | 31 ++++---
.../event_container_light_sensor.dart | 8 ++
.../notifier_volume_keys.dart | 32 ++++++++
lib/screens/metering/event_metering.dart | 8 ++
lib/screens/metering/flow_metering.dart | 27 ++++---
lib/screens/metering/screen_metering.dart | 7 +-
.../caffeine/widget_list_tile_caffeine.dart | 2 +
.../haptics/widget_list_tile_haptics.dart | 2 +
.../bloc_list_tile_volume_actions.dart | 20 +++++
.../provider_list_tile_volume_actions.dart | 19 +++++
.../widget_list_tile_volume_actions.dart | 22 +++++
.../widget_settings_section_general.dart | 12 ++-
.../widget_list_tile_dynamic_color.dart | 2 +
lib/screens/settings/flow_settings.dart | 2 +
lib/screens/settings/screen_settings.dart | 21 ++++-
pubspec.yaml | 1 +
resources/social_preview.png | Bin 0 -> 48068 bytes
test/data/light_sensor_service_test.dart | 35 +++++++-
test/data/volume_events_service_test.dart | 73 +++++++++++++++++
test/screens/metering/bloc_metering_test.dart | 76 +++++++++++++++++-
.../bloc_communication_metering_test.dart | 26 ++++++
.../camera/bloc_container_camera_test.dart | 28 ++++++-
.../bloc_container_light_sensor_test.dart | 63 +++++++++++++++
41 files changed, 758 insertions(+), 124 deletions(-)
create mode 100644 lib/data/models/volume_action.dart
create mode 100644 lib/data/volume_events_service.dart
create mode 100644 lib/screens/metering/components/shared/volume_keys_notifier/notifier_volume_keys.dart
create mode 100644 lib/screens/settings/components/general/components/volume_actions/bloc_list_tile_volume_actions.dart
create mode 100644 lib/screens/settings/components/general/components/volume_actions/provider_list_tile_volume_actions.dart
create mode 100644 lib/screens/settings/components/general/components/volume_actions/widget_list_tile_volume_actions.dart
create mode 100644 resources/social_preview.png
create mode 100644 test/data/volume_events_service_test.dart
diff --git a/README.md b/README.md
index c801a9c..0b4dae8 100644
--- a/README.md
+++ b/README.md
@@ -1,58 +1,48 @@
-
-
-
-
- Material Lightmeter
-
+
# Table of contents
- [Table of contents](#table-of-contents)
- [Backstory](#backstory)
-- [Legacy features](#legacy-features)
+- [Screenshots](#screenshots)
- [Build](#build)
- [Contribution](#contribution)
+- [iOS Limitations](#ios-limitations)
# Backstory
Some time ago I've started developing the [Material Lightmeter](https://play.google.com/store/apps/details?id=com.vodemn.lightmeter&hl=en&gl=US) app. Unfortunately, the last update of this app was almost a year prior to creation of this repo. So after reading some positive review on Google Play saying that "this is an excellent app, too bad it is no longer updated", I've decided to make an update and also make this app open source. Maybe someone sometime will decide to contribute to this project.
-But as the existing repo contained some sensitive data, that I've pushed due to lack of experience, I had to make a new one. And if creating a new repo, why not rewrite the app from scratch?)
+But as the existing repo contained some sensitive data, that I've pushed due to lack of experience, I had to make a new one. And if creating a new repo, why not rewrite the app from scratch?
Without further delay behold my new Lightmeter app inspired by Material You (a.k.a. M3)
-# Legacy features
+# Screenshots
-The list of features that the old lightmeter app has and that have to be implemeneted in the M3 lightmeter.
+
+
+
+
+
+
+
-### Metering
-- [x] ISO selecting
-- [x] Reciprocity for different films
-- [x] Reflected light metering
-- [x] Incident light metering
-
-### Adjust
-- [x] Light sources EV calibration
-- [ ] Customizable aperture range
-- [ ] Customizable shutter speed range
-- [x] ND filter select
-
-### General
-- [x] Caffeine
-- [x] Vibration
-- [ ] Volume button actions
-
-### Theme
-- [x] Dark theme
-- [x] Picking primary color
-- [x] Russian language
-
-## Build
+# Build
As part of this project is private, you will be able to run this app from the _main_dev.dart_ file (i.e. --flavor dev). Also to avoid fatal errors the _main_prod.dart_ file is excluded from analysis.
-## Contribution
+# Contribution
To report a bug or suggest a new feature open a new [issue](https://github.com/vodemn/m3_lightmeter/issues).
In case you want to help develop this project you need to follow this [style guide](doc/style_guide.md).
+
+# iOS Limitations
+
+A list of features, that Android version of the app has and that iOS does not.
+
+## Incident light metering
+Apple does not provide API for reading Lux stream form the ambient light sensor. Lux can be calculated based on front camera image stream, but this would be a reflected light. So there is no way incident light metering can be implemented on iOS.
+
+## Volume buttons action
+This can be [implemented](https://stackoverflow.com/questions/70161271/ios-override-hardware-volume-buttons-same-as-zello) but the app will be rejected due to [2.5.9](https://developer.apple.com/app-store/review/guidelines/#software-requirements)
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/com/vodemn/lightmeter/MainActivity.kt b/android/app/src/main/kotlin/com/vodemn/lightmeter/MainActivity.kt
index ea040d6..af458c4 100644
--- a/android/app/src/main/kotlin/com/vodemn/lightmeter/MainActivity.kt
+++ b/android/app/src/main/kotlin/com/vodemn/lightmeter/MainActivity.kt
@@ -1,13 +1,22 @@
package com.vodemn.lightmeter
import android.os.Bundle
+import android.view.KeyEvent
import android.view.WindowManager
import androidx.core.view.WindowCompat
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
+import io.flutter.plugin.common.EventChannel
+import io.flutter.plugin.common.EventChannel.EventSink
import io.flutter.plugin.common.MethodChannel
class MainActivity : FlutterActivity() {
+ private lateinit var keepScreenOnChannel: MethodChannel
+ private lateinit var volumeHandlingChannel: MethodChannel
+ private lateinit var volumeEventChannel: EventChannel
+ private var volumeEventsEmitter: EventSink? = null
+ private var handleVolume = false
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
@@ -15,10 +24,11 @@ class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
- MethodChannel(
+ keepScreenOnChannel = MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
"com.vodemn.lightmeter/keepScreenOn"
- ).setMethodCallHandler { call, result ->
+ )
+ keepScreenOnChannel.setMethodCallHandler { call, result ->
when (call.method) {
"isKeepScreenOn" -> result.success((window.attributes.flags and WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) != 0)
"setKeepScreenOn" -> {
@@ -33,5 +43,53 @@ class MainActivity : FlutterActivity() {
else -> result.notImplemented()
}
}
+
+ volumeHandlingChannel = MethodChannel(
+ flutterEngine.dartExecutor.binaryMessenger,
+ "com.vodemn.lightmeter/volumeHandling"
+ )
+ volumeHandlingChannel.setMethodCallHandler { call, result ->
+ when (call.method) {
+ "setVolumeHandling" -> {
+ handleVolume = call.arguments as Boolean
+ result.success(handleVolume)
+ }
+ else -> result.notImplemented()
+ }
+ }
+
+ volumeEventChannel = EventChannel(
+ flutterEngine.dartExecutor.binaryMessenger,
+ "com.vodemn.lightmeter/volumeEvents"
+ )
+ volumeEventChannel.setStreamHandler(object : EventChannel.StreamHandler {
+ override fun onListen(listener: Any?, eventSink: EventSink) {
+ volumeEventsEmitter = eventSink
+ }
+ override fun onCancel(listener: Any?) {
+ volumeEventsEmitter = null
+ }
+ })
+ }
+
+ override fun onDestroy() {
+ keepScreenOnChannel.setMethodCallHandler(null)
+ volumeHandlingChannel.setMethodCallHandler(null)
+ volumeEventChannel.setStreamHandler(null)
+ super.onDestroy()
+ }
+
+ override fun onKeyDown(code: Int, event: KeyEvent): Boolean {
+ return when (val keyCode: Int = event.keyCode) {
+ KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN -> {
+ if (handleVolume) {
+ volumeEventsEmitter?.success(keyCode)
+ true
+ } else {
+ super.onKeyDown(code, event)
+ }
+ }
+ else -> super.onKeyDown(code, event)
+ }
}
}
diff --git a/lib/data/light_sensor_service.dart b/lib/data/light_sensor_service.dart
index 69b55c5..c837dde 100644
--- a/lib/data/light_sensor_service.dart
+++ b/lib/data/light_sensor_service.dart
@@ -1,9 +1,15 @@
import 'package:light_sensor/light_sensor.dart';
+import 'package:platform/platform.dart';
class LightSensorService {
- const LightSensorService();
+ final LocalPlatform localPlatform;
+
+ const LightSensorService(this.localPlatform);
Future hasSensor() async {
+ if (!localPlatform.isAndroid) {
+ return false;
+ }
try {
return await LightSensor.hasSensor ?? false;
} catch (_) {
@@ -11,5 +17,10 @@ class LightSensorService {
}
}
- Stream luxStream() => LightSensor.lightSensorStream;
+ Stream luxStream() {
+ if (!localPlatform.isAndroid) {
+ return const Stream.empty();
+ }
+ return LightSensor.lightSensorStream;
+ }
}
diff --git a/lib/data/models/volume_action.dart b/lib/data/models/volume_action.dart
new file mode 100644
index 0000000..b28f9dc
--- /dev/null
+++ b/lib/data/models/volume_action.dart
@@ -0,0 +1,3 @@
+enum VolumeAction { shutter, none }
+
+enum VolumeKey { up, down }
diff --git a/lib/data/shared_prefs_service.dart b/lib/data/shared_prefs_service.dart
index 8fbb6af..76f9045 100644
--- a/lib/data/shared_prefs_service.dart
+++ b/lib/data/shared_prefs_service.dart
@@ -6,6 +6,7 @@ import 'package:lightmeter/data/models/film.dart';
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
import 'package:lightmeter/data/models/supported_locale.dart';
import 'package:lightmeter/data/models/theme_type.dart';
+import 'package:lightmeter/data/models/volume_action.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -22,6 +23,7 @@ class UserPreferencesService {
static const caffeineKey = "caffeine";
static const hapticsKey = "haptics";
+ static const volumeActionKey = "volumeAction";
static const localeKey = "locale";
static const themeTypeKey = "themeType";
@@ -82,9 +84,6 @@ class UserPreferencesService {
EvSourceType.values[_sharedPreferences.getInt(evSourceTypeKey) ?? 0];
set evSourceType(EvSourceType value) => _sharedPreferences.setInt(evSourceTypeKey, value.index);
- bool get caffeine => _sharedPreferences.getBool(caffeineKey) ?? false;
- set caffeine(bool value) => _sharedPreferences.setBool(caffeineKey, value);
-
StopType get stopType => StopType.values[_sharedPreferences.getInt(stopTypeKey) ?? 2];
set stopType(StopType value) => _sharedPreferences.setInt(stopTypeKey, value.index);
@@ -105,9 +104,19 @@ class UserPreferencesService {
set meteringScreenLayout(MeteringScreenLayoutConfig value) =>
_sharedPreferences.setString(meteringScreenLayoutKey, json.encode(value.toJson()));
+ bool get caffeine => _sharedPreferences.getBool(caffeineKey) ?? false;
+ set caffeine(bool value) => _sharedPreferences.setBool(caffeineKey, value);
+
bool get haptics => _sharedPreferences.getBool(hapticsKey) ?? true;
set haptics(bool value) => _sharedPreferences.setBool(hapticsKey, value);
+ VolumeAction get volumeAction => VolumeAction.values.firstWhere(
+ (e) => e.toString() == _sharedPreferences.getString(volumeActionKey),
+ orElse: () => VolumeAction.shutter,
+ );
+ set volumeAction(VolumeAction value) =>
+ _sharedPreferences.setString(volumeActionKey, value.toString());
+
SupportedLocale get locale => SupportedLocale.values.firstWhere(
(e) => e.toString() == _sharedPreferences.getString(localeKey),
orElse: () => SupportedLocale.en,
diff --git a/lib/data/volume_events_service.dart b/lib/data/volume_events_service.dart
new file mode 100644
index 0000000..d57936a
--- /dev/null
+++ b/lib/data/volume_events_service.dart
@@ -0,0 +1,40 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter/services.dart';
+import 'package:platform/platform.dart';
+
+class VolumeEventsService {
+ final LocalPlatform localPlatform;
+
+ @visibleForTesting
+ static const volumeHandlingChannel = MethodChannel("com.vodemn.lightmeter/volumeHandling");
+
+ @visibleForTesting
+ static const volumeEventsChannel = EventChannel("com.vodemn.lightmeter/volumeEvents");
+
+ const VolumeEventsService(this.localPlatform);
+
+ /// If set to `false` we allow system to handle key events.
+ /// Returns current status of volume handling.
+ Future setVolumeHandling(bool enableHandling) async {
+ if (!localPlatform.isAndroid) {
+ return false;
+ }
+ return volumeHandlingChannel
+ .invokeMethod("setVolumeHandling", enableHandling)
+ .then((value) => value!);
+ }
+
+ /// Emits new events on
+ /// KEYCODE_VOLUME_UP = 24;
+ /// KEYCODE_VOLUME_DOWN = 25;
+ /// pressed
+ Stream volumeButtonsEventStream() {
+ if (!localPlatform.isAndroid) {
+ return const Stream.empty();
+ }
+ return volumeEventsChannel
+ .receiveBroadcastStream()
+ .cast()
+ .where((event) => event == 24 || event == 25);
+ }
+}
diff --git a/lib/interactors/metering_interactor.dart b/lib/interactors/metering_interactor.dart
index ca94f8e..34b03ec 100644
--- a/lib/interactors/metering_interactor.dart
+++ b/lib/interactors/metering_interactor.dart
@@ -1,12 +1,12 @@
-import 'dart:io';
-
import 'package:app_settings/app_settings.dart';
import 'package:lightmeter/data/caffeine_service.dart';
import 'package:lightmeter/data/haptics_service.dart';
import 'package:lightmeter/data/light_sensor_service.dart';
import 'package:lightmeter/data/models/film.dart';
+import 'package:lightmeter/data/models/volume_action.dart';
import 'package:lightmeter/data/permissions_service.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
+import 'package:lightmeter/data/volume_events_service.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
import 'package:permission_handler/permission_handler.dart';
@@ -16,6 +16,7 @@ class MeteringInteractor {
final HapticsService _hapticsService;
final PermissionsService _permissionsService;
final LightSensorService _lightSensorService;
+ final VolumeEventsService _volumeEventsService;
MeteringInteractor(
this._userPreferencesService,
@@ -23,10 +24,13 @@ class MeteringInteractor {
this._hapticsService,
this._permissionsService,
this._lightSensorService,
+ this._volumeEventsService,
) {
if (_userPreferencesService.caffeine) {
_caffeineService.keepScreenOn(true);
}
+ _volumeEventsService
+ .setVolumeHandling(_userPreferencesService.volumeAction != VolumeAction.none);
}
double get cameraEvCalibration => _userPreferencesService.cameraEvCalibration;
@@ -42,6 +46,8 @@ class MeteringInteractor {
Film get film => _userPreferencesService.film;
set film(Film value) => _userPreferencesService.film = value;
+ VolumeAction get volumeAction => _userPreferencesService.volumeAction;
+
/// Executes vibration if haptics are enabled in settings
Future quickVibration() async {
if (_userPreferencesService.haptics) await _hapticsService.quickVibration();
@@ -73,13 +79,7 @@ class MeteringInteractor {
AppSettings.openAppSettings();
}
- Future hasAmbientLightSensor() async {
- if (Platform.isAndroid) {
- return _lightSensorService.hasSensor();
- } else {
- return false;
- }
- }
+ Future hasAmbientLightSensor() async => _lightSensorService.hasSensor();
Stream luxStream() => _lightSensorService.luxStream();
}
diff --git a/lib/interactors/settings_interactor.dart b/lib/interactors/settings_interactor.dart
index db99a7a..4eeb8b9 100644
--- a/lib/interactors/settings_interactor.dart
+++ b/lib/interactors/settings_interactor.dart
@@ -1,16 +1,20 @@
import 'package:lightmeter/data/caffeine_service.dart';
import 'package:lightmeter/data/haptics_service.dart';
+import 'package:lightmeter/data/models/volume_action.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
+import 'package:lightmeter/data/volume_events_service.dart';
class SettingsInteractor {
final UserPreferencesService _userPreferencesService;
final CaffeineService _caffeineService;
final HapticsService _hapticsService;
+ final VolumeEventsService _volumeEventsService;
const SettingsInteractor(
this._userPreferencesService,
this._caffeineService,
this._hapticsService,
+ this._volumeEventsService,
);
double get cameraEvCalibration => _userPreferencesService.cameraEvCalibration;
@@ -27,6 +31,20 @@ class SettingsInteractor {
});
}
+ Future disableVolumeHandling() async {
+ await _volumeEventsService.setVolumeHandling(false);
+ }
+ Future restoreVolumeHandling() async {
+ await _volumeEventsService
+ .setVolumeHandling(_userPreferencesService.volumeAction != VolumeAction.none);
+ }
+
+ VolumeAction get volumeAction => _userPreferencesService.volumeAction;
+ Future setVolumeAction(VolumeAction value) async {
+ await _volumeEventsService.setVolumeHandling(value != VolumeAction.none);
+ _userPreferencesService.volumeAction = value;
+ }
+
bool get isHapticsEnabled => _userPreferencesService.haptics;
void enableHaptics(bool enable) {
_userPreferencesService.haptics = enable;
diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb
index c404d33..39700b8 100644
--- a/lib/l10n/intl_en.arb
+++ b/lib/l10n/intl_en.arb
@@ -56,6 +56,7 @@
"general": "General",
"keepScreenOn": "Keep screen on",
"haptics": "Haptics",
+ "volumeKeysAction": "Shutter by volume keys",
"language": "Language",
"chooseLanguage": "Choose language",
"theme": "Theme",
diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb
index 5102a59..5957d0d 100644
--- a/lib/l10n/intl_fr.arb
+++ b/lib/l10n/intl_fr.arb
@@ -56,6 +56,7 @@
"general": "Général",
"keepScreenOn": "Garder l'écran allumé",
"haptics": "Haptiques",
+ "volumeKeysAction": "Obturateur par boutons de volume",
"language": "Langue",
"chooseLanguage": "Choisissez la langue",
"theme": "Thème",
diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb
index ca746dd..f6b3833 100644
--- a/lib/l10n/intl_ru.arb
+++ b/lib/l10n/intl_ru.arb
@@ -56,6 +56,7 @@
"general": "Общие",
"keepScreenOn": "Запрет блокировки",
"haptics": "Вибрация",
+ "volumeKeysAction": "Затвор по кнопкам громкости",
"language": "Язык",
"chooseLanguage": "Выберите язык",
"theme": "Тема",
diff --git a/lib/main_prod.dart b/lib/main_prod.dart
index bf02374..a47d421 100644
--- a/lib/main_prod.dart
+++ b/lib/main_prod.dart
@@ -6,6 +6,5 @@ import 'package:lightmeter/firebase.dart';
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
await initializeFirebase();
-
runApp(const Application(Environment.prod()));
}
diff --git a/lib/providers.dart b/lib/providers.dart
index 7805e69..d7907ab 100644
--- a/lib/providers.dart
+++ b/lib/providers.dart
@@ -1,11 +1,10 @@
-import 'dart:io';
-
import 'package:flutter/material.dart';
import 'package:lightmeter/data/caffeine_service.dart';
import 'package:lightmeter/data/haptics_service.dart';
import 'package:lightmeter/data/light_sensor_service.dart';
import 'package:lightmeter/data/permissions_service.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
+import 'package:lightmeter/data/volume_events_service.dart';
import 'package:lightmeter/environment.dart';
import 'package:lightmeter/providers/equipment_profile_provider.dart';
import 'package:lightmeter/providers/ev_source_type_provider.dart';
@@ -14,6 +13,7 @@ import 'package:lightmeter/providers/stop_type_provider.dart';
import 'package:lightmeter/providers/supported_locale_provider.dart';
import 'package:lightmeter/providers/theme_provider.dart';
import 'package:lightmeter/utils/inherited_generics.dart';
+import 'package:platform/platform.dart';
import 'package:shared_preferences/shared_preferences.dart';
class LightmeterProviders extends StatelessWidget {
@@ -27,7 +27,7 @@ class LightmeterProviders extends StatelessWidget {
return FutureBuilder(
future: Future.wait([
SharedPreferences.getInstance(),
- if (Platform.isAndroid) const LightSensorService().hasSensor() else Future.value(false),
+ const LightSensorService(LocalPlatform()).hasSensor(),
]),
builder: (_, snapshot) {
if (snapshot.data != null) {
@@ -36,21 +36,24 @@ class LightmeterProviders extends StatelessWidget {
child: InheritedWidgetBase(
data: UserPreferencesService(snapshot.data![0] as SharedPreferences),
child: InheritedWidgetBase(
- data: const LightSensorService(),
+ data: const LightSensorService(LocalPlatform()),
child: InheritedWidgetBase(
data: const CaffeineService(),
child: InheritedWidgetBase(
data: const HapticsService(),
- child: InheritedWidgetBase(
- data: const PermissionsService(),
- child: MeteringScreenLayoutProvider(
- child: StopTypeProvider(
- child: EquipmentProfileProvider(
- child: EvSourceTypeProvider(
- child: SupportedLocaleProvider(
- child: ThemeProvider(
- child: Builder(
- builder: (context) => builder(context, true),
+ child: InheritedWidgetBase(
+ data: const VolumeEventsService(LocalPlatform()),
+ child: InheritedWidgetBase(
+ data: const PermissionsService(),
+ child: MeteringScreenLayoutProvider(
+ child: StopTypeProvider(
+ child: EquipmentProfileProvider(
+ child: EvSourceTypeProvider(
+ child: SupportedLocaleProvider(
+ child: ThemeProvider(
+ child: Builder(
+ builder: (context) => builder(context, true),
+ ),
),
),
),
diff --git a/lib/screens/metering/bloc_metering.dart b/lib/screens/metering/bloc_metering.dart
index 042991a..8e4a2f6 100644
--- a/lib/screens/metering/bloc_metering.dart
+++ b/lib/screens/metering/bloc_metering.dart
@@ -4,23 +4,27 @@ import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/data/models/film.dart';
+import 'package:lightmeter/data/models/volume_action.dart';
import 'package:lightmeter/interactors/metering_interactor.dart';
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart'
as communication_events;
import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart'
as communication_states;
+import 'package:lightmeter/screens/metering/components/shared/volume_keys_notifier/notifier_volume_keys.dart';
import 'package:lightmeter/screens/metering/event_metering.dart';
import 'package:lightmeter/screens/metering/state_metering.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class MeteringBloc extends Bloc {
final MeteringInteractor _meteringInteractor;
+ final VolumeKeysNotifier _volumeKeysNotifier;
final MeteringCommunicationBloc _communicationBloc;
late final StreamSubscription _communicationSubscription;
MeteringBloc(
this._meteringInteractor,
+ this._volumeKeysNotifier,
this._communicationBloc,
) : super(
MeteringDataState(
@@ -31,6 +35,7 @@ class MeteringBloc extends Bloc {
isMetering: false,
),
) {
+ _volumeKeysNotifier.addListener(onVolumeKey);
_communicationSubscription = _communicationBloc.stream
.where((state) => state is communication_states.ScreenState)
.map((state) => state as communication_states.ScreenState)
@@ -43,6 +48,8 @@ class MeteringBloc extends Bloc {
on(_onMeasure, transformer: droppable());
on(_onMeasured);
on(_onMeasureError);
+ on(_onSettingsOpened);
+ on(_onSettingsClosed);
}
@override
@@ -64,6 +71,7 @@ class MeteringBloc extends Bloc {
@override
Future close() async {
+ _volumeKeysNotifier.removeListener(onVolumeKey);
await _communicationSubscription.cancel();
return super.close();
}
@@ -220,4 +228,19 @@ class MeteringBloc extends Bloc {
),
);
}
+
+ @visibleForTesting
+ void onVolumeKey() {
+ if (_meteringInteractor.volumeAction == VolumeAction.shutter) {
+ add(const MeasureEvent());
+ }
+ }
+
+ void _onSettingsOpened(SettingsOpenedEvent _, Emitter __) {
+ _communicationBloc.add(const communication_events.SettingsOpenedEvent());
+ }
+
+ void _onSettingsClosed(SettingsClosedEvent _, Emitter __) {
+ _communicationBloc.add(const communication_events.SettingsClosedEvent());
+ }
}
diff --git a/lib/screens/metering/communication/bloc_communication_metering.dart b/lib/screens/metering/communication/bloc_communication_metering.dart
index 1c54dc3..11ebe37 100644
--- a/lib/screens/metering/communication/bloc_communication_metering.dart
+++ b/lib/screens/metering/communication/bloc_communication_metering.dart
@@ -11,5 +11,7 @@ class MeteringCommunicationBloc
on((_, emit) => emit(MeasureState()));
on((event, emit) => emit(MeteringInProgressState(event.ev100)));
on((event, emit) => emit(MeteringEndedState(event.ev100)));
+ on((_, emit) => emit(const SettingsOpenedState()));
+ on((_, emit) => emit(const SettingsClosedState()));
}
}
diff --git a/lib/screens/metering/communication/event_communication_metering.dart b/lib/screens/metering/communication/event_communication_metering.dart
index ac63b57..c7e0fd8 100644
--- a/lib/screens/metering/communication/event_communication_metering.dart
+++ b/lib/screens/metering/communication/event_communication_metering.dart
@@ -47,3 +47,11 @@ class MeteringEndedEvent extends MeasuredEvent {
@override
int get hashCode => Object.hash(ev100, runtimeType);
}
+
+class SettingsOpenedEvent extends ScreenEvent {
+ const SettingsOpenedEvent();
+}
+
+class SettingsClosedEvent extends ScreenEvent {
+ const SettingsClosedEvent();
+}
diff --git a/lib/screens/metering/communication/state_communication_metering.dart b/lib/screens/metering/communication/state_communication_metering.dart
index 2923cf1..2d3991e 100644
--- a/lib/screens/metering/communication/state_communication_metering.dart
+++ b/lib/screens/metering/communication/state_communication_metering.dart
@@ -51,3 +51,11 @@ class MeteringEndedState extends MeasuredState {
@override
int get hashCode => Object.hash(ev100, runtimeType);
}
+
+class SettingsOpenedState extends SourceState {
+ const SettingsOpenedState();
+}
+
+class SettingsClosedState extends SourceState {
+ const SettingsClosedState();
+}
diff --git a/lib/screens/metering/components/camera_container/bloc_container_camera.dart b/lib/screens/metering/components/camera_container/bloc_container_camera.dart
index 2bb8ed1..7f00256 100644
--- a/lib/screens/metering/components/camera_container/bloc_container_camera.dart
+++ b/lib/screens/metering/components/camera_container/bloc_container_camera.dart
@@ -23,6 +23,7 @@ import 'package:lightmeter/utils/log_2.dart';
class CameraContainerBloc extends EvSourceBlocBase {
final MeteringInteractor _meteringInteractor;
late final _WidgetsBindingObserver _observer;
+
CameraController? _cameraController;
static const _maxZoom = 7.0;
@@ -36,6 +37,8 @@ class CameraContainerBloc extends EvSourceBlocBase _onDeinitialize(DeinitializeEvent _, Emitter emit) async {
- emit(const CameraLoadingState());
- unawaited(_cameraController?.dispose().then((_) => _cameraController = null));
+ emit(const CameraInitState());
+ communicationBloc.add(communication_event.MeteringEndedEvent(_ev100));
+ await _cameraController?.dispose().then((_) => _cameraController = null);
}
Future _onZoomChanged(ZoomChangedEvent event, Emitter emit) async {
- if (_cameraController != null) {
+ if (_cameraController != null &&
+ event.value >= _zoomRange!.start &&
+ event.value <= _zoomRange!.end) {
_cameraController!.setZoomLevel(event.value);
_currentZoom = event.value;
_emitActiveState(emit);
@@ -215,13 +229,15 @@ class CameraContainerBloc extends EvSourceBlocBase _appLifecycleStateObserver(AppLifecycleState state) async {
- switch (state) {
- case AppLifecycleState.resumed:
- add(const InitializeEvent());
- case AppLifecycleState.paused:
- case AppLifecycleState.detached:
- add(const DeinitializeEvent());
- default:
+ if (!_settingsOpened) {
+ switch (state) {
+ case AppLifecycleState.resumed:
+ add(const InitializeEvent());
+ case AppLifecycleState.paused:
+ case AppLifecycleState.detached:
+ add(const DeinitializeEvent());
+ default:
+ }
}
}
}
diff --git a/lib/screens/metering/components/light_sensor_container/bloc_container_light_sensor.dart b/lib/screens/metering/components/light_sensor_container/bloc_container_light_sensor.dart
index d0e4031..23cd796 100644
--- a/lib/screens/metering/components/light_sensor_container/bloc_container_light_sensor.dart
+++ b/lib/screens/metering/components/light_sensor_container/bloc_container_light_sensor.dart
@@ -25,37 +25,44 @@ class LightSensorContainerBloc
communicationBloc,
const LightSensorContainerState(null),
) {
+ on(_onStartLuxMeteringEvent);
on(_onLuxMeteringEvent);
+ on(_onCancelLuxMeteringEvent);
}
@override
void onCommunicationState(communication_states.SourceState communicationState) {
- if (communicationState is communication_states.MeasureState) {
- if (_luxSubscriptions == null) {
- _startMetering();
- } else {
- _cancelMetering();
- }
+ switch (communicationState) {
+ case communication_states.MeasureState():
+ if (_luxSubscriptions == null) {
+ add(const StartLuxMeteringEvent());
+ } else {
+ add(const CancelLuxMeteringEvent());
+ }
+ case communication_states.SettingsOpenedState():
+ add(const CancelLuxMeteringEvent());
+ default:
}
}
@override
Future close() async {
- _cancelMetering();
+ communicationBloc.add(communication_event.MeteringEndedEvent(state.ev100));
+ _luxSubscriptions?.cancel().then((_) => _luxSubscriptions = null);
return super.close();
}
+ void _onStartLuxMeteringEvent(StartLuxMeteringEvent event, _) {
+ _luxSubscriptions = _meteringInteractor.luxStream().listen((lux) => add(LuxMeteringEvent(lux)));
+ }
+
void _onLuxMeteringEvent(LuxMeteringEvent event, Emitter emit) {
final ev100 = log2(event.lux.toDouble() / 2.5) + _meteringInteractor.lightSensorEvCalibration;
emit(LightSensorContainerState(ev100));
communicationBloc.add(communication_event.MeteringInProgressEvent(ev100));
}
- void _startMetering() {
- _luxSubscriptions = _meteringInteractor.luxStream().listen((lux) => add(LuxMeteringEvent(lux)));
- }
-
- void _cancelMetering() {
+ void _onCancelLuxMeteringEvent(CancelLuxMeteringEvent event, _) {
communicationBloc.add(communication_event.MeteringEndedEvent(state.ev100));
_luxSubscriptions?.cancel().then((_) => _luxSubscriptions = null);
}
diff --git a/lib/screens/metering/components/light_sensor_container/event_container_light_sensor.dart b/lib/screens/metering/components/light_sensor_container/event_container_light_sensor.dart
index 8db83b3..bffcadd 100644
--- a/lib/screens/metering/components/light_sensor_container/event_container_light_sensor.dart
+++ b/lib/screens/metering/components/light_sensor_container/event_container_light_sensor.dart
@@ -2,8 +2,16 @@ abstract class LightSensorContainerEvent {
const LightSensorContainerEvent();
}
+class StartLuxMeteringEvent extends LightSensorContainerEvent {
+ const StartLuxMeteringEvent();
+}
+
class LuxMeteringEvent extends LightSensorContainerEvent {
final int lux;
const LuxMeteringEvent(this.lux);
}
+
+class CancelLuxMeteringEvent extends LightSensorContainerEvent {
+ const CancelLuxMeteringEvent();
+}
diff --git a/lib/screens/metering/components/shared/volume_keys_notifier/notifier_volume_keys.dart b/lib/screens/metering/components/shared/volume_keys_notifier/notifier_volume_keys.dart
new file mode 100644
index 0000000..df64fdf
--- /dev/null
+++ b/lib/screens/metering/components/shared/volume_keys_notifier/notifier_volume_keys.dart
@@ -0,0 +1,32 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:lightmeter/data/models/volume_action.dart';
+import 'package:lightmeter/data/volume_events_service.dart';
+
+class VolumeKeysNotifier extends ChangeNotifier with RouteAware {
+ final VolumeEventsService volumeEventsService;
+ late final StreamSubscription _volumeKeysSubscription;
+ VolumeKey _value = VolumeKey.up;
+
+ VolumeKeysNotifier(this.volumeEventsService) {
+ _volumeKeysSubscription = volumeEventsService
+ .volumeButtonsEventStream()
+ .map((event) => event == 24 ? VolumeKey.up : VolumeKey.down)
+ .listen((event) {
+ value = event;
+ });
+ }
+
+ VolumeKey get value => _value;
+ set value(VolumeKey newValue) {
+ _value = newValue;
+ notifyListeners();
+ }
+
+ @override
+ Future dispose() async {
+ await _volumeKeysSubscription.cancel();
+ super.dispose();
+ }
+}
diff --git a/lib/screens/metering/event_metering.dart b/lib/screens/metering/event_metering.dart
index e12ba35..852b5e4 100644
--- a/lib/screens/metering/event_metering.dart
+++ b/lib/screens/metering/event_metering.dart
@@ -45,3 +45,11 @@ class MeasureErrorEvent extends MeteringEvent {
const MeasureErrorEvent({required this.isMetering});
}
+
+class SettingsOpenedEvent extends MeteringEvent {
+ const SettingsOpenedEvent();
+}
+
+class SettingsClosedEvent extends MeteringEvent {
+ const SettingsClosedEvent();
+}
diff --git a/lib/screens/metering/flow_metering.dart b/lib/screens/metering/flow_metering.dart
index ac6cb0d..780f537 100644
--- a/lib/screens/metering/flow_metering.dart
+++ b/lib/screens/metering/flow_metering.dart
@@ -5,9 +5,11 @@ 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/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';
@@ -28,18 +30,23 @@ class _MeteringFlowState extends State {
context.get(),
context.get(),
context.get(),
+ context.get(),
),
- child: MultiBlocProvider(
- providers: [
- BlocProvider(create: (_) => MeteringCommunicationBloc()),
- BlocProvider(
- create: (context) => MeteringBloc(
- context.get(),
- context.read(),
+ child: InheritedWidgetBase(
+ data: VolumeKeysNotifier(context.get()),
+ child: MultiBlocProvider(
+ providers: [
+ BlocProvider(create: (_) => MeteringCommunicationBloc()),
+ BlocProvider(
+ create: (context) => MeteringBloc(
+ context.get(),
+ context.get(),
+ context.read(),
+ ),
),
- ),
- ],
- child: const MeteringScreen(),
+ ],
+ child: const MeteringScreen(),
+ ),
),
);
}
diff --git a/lib/screens/metering/screen_metering.dart b/lib/screens/metering/screen_metering.dart
index 134d789..4fb1e85 100644
--- a/lib/screens/metering/screen_metering.dart
+++ b/lib/screens/metering/screen_metering.dart
@@ -50,7 +50,12 @@ class MeteringScreen extends StatelessWidget {
? EvSourceTypeProvider.of(context).toggleType
: null,
onMeasure: () => context.read().add(const MeasureEvent()),
- onSettings: () => Navigator.pushNamed(context, 'settings'),
+ onSettings: () {
+ context.read().add(const SettingsOpenedEvent());
+ Navigator.pushNamed(context, 'settings').then((value) {
+ context.read().add(const SettingsClosedEvent());
+ });
+ },
),
),
],
diff --git a/lib/screens/settings/components/general/components/caffeine/widget_list_tile_caffeine.dart b/lib/screens/settings/components/general/components/caffeine/widget_list_tile_caffeine.dart
index 7de7450..ee0483d 100644
--- a/lib/screens/settings/components/general/components/caffeine/widget_list_tile_caffeine.dart
+++ b/lib/screens/settings/components/general/components/caffeine/widget_list_tile_caffeine.dart
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/generated/l10n.dart';
+import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/settings/components/general/components/caffeine/bloc_list_tile_caffeine.dart';
@@ -15,6 +16,7 @@ class CaffeineListTile extends StatelessWidget {
title: Text(S.of(context).keepScreenOn),
value: state,
onChanged: context.read().onCaffeineChanged,
+ contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
),
);
}
diff --git a/lib/screens/settings/components/general/components/haptics/widget_list_tile_haptics.dart b/lib/screens/settings/components/general/components/haptics/widget_list_tile_haptics.dart
index a33b1ce..ec7ec60 100644
--- a/lib/screens/settings/components/general/components/haptics/widget_list_tile_haptics.dart
+++ b/lib/screens/settings/components/general/components/haptics/widget_list_tile_haptics.dart
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/generated/l10n.dart';
+import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/settings/components/general/components/haptics/bloc_list_tile_haptics.dart';
@@ -15,6 +16,7 @@ class HapticsListTile extends StatelessWidget {
title: Text(S.of(context).haptics),
value: state,
onChanged: context.read().onHapticsChanged,
+ contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
),
);
}
diff --git a/lib/screens/settings/components/general/components/volume_actions/bloc_list_tile_volume_actions.dart b/lib/screens/settings/components/general/components/volume_actions/bloc_list_tile_volume_actions.dart
new file mode 100644
index 0000000..a8b8e3e
--- /dev/null
+++ b/lib/screens/settings/components/general/components/volume_actions/bloc_list_tile_volume_actions.dart
@@ -0,0 +1,20 @@
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:lightmeter/data/models/volume_action.dart';
+import 'package:lightmeter/interactors/settings_interactor.dart';
+
+class VolumeActionsListTileBloc extends Cubit {
+ final SettingsInteractor _settingsInteractor;
+
+ VolumeActionsListTileBloc(
+ this._settingsInteractor,
+ ) : super(_settingsInteractor.volumeAction == VolumeAction.shutter);
+
+ void onVolumeActionChanged(bool value) {
+ _settingsInteractor.setVolumeAction(value ? VolumeAction.shutter : VolumeAction.none);
+
+ // while in settings we allow system to handle volume
+ // so that volume keys action works only when necessary - on the metering screen
+ _settingsInteractor.disableVolumeHandling();
+ emit(value);
+ }
+}
diff --git a/lib/screens/settings/components/general/components/volume_actions/provider_list_tile_volume_actions.dart b/lib/screens/settings/components/general/components/volume_actions/provider_list_tile_volume_actions.dart
new file mode 100644
index 0000000..790ad4f
--- /dev/null
+++ b/lib/screens/settings/components/general/components/volume_actions/provider_list_tile_volume_actions.dart
@@ -0,0 +1,19 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:lightmeter/interactors/settings_interactor.dart';
+
+import 'package:lightmeter/screens/settings/components/general/components/volume_actions/bloc_list_tile_volume_actions.dart';
+import 'package:lightmeter/screens/settings/components/general/components/volume_actions/widget_list_tile_volume_actions.dart';
+import 'package:lightmeter/utils/inherited_generics.dart';
+
+class VolumeActionsListTileProvider extends StatelessWidget {
+ const VolumeActionsListTileProvider({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return BlocProvider(
+ create: (context) => VolumeActionsListTileBloc(context.get()),
+ child: const VolumeActionsListTile(),
+ );
+ }
+}
diff --git a/lib/screens/settings/components/general/components/volume_actions/widget_list_tile_volume_actions.dart b/lib/screens/settings/components/general/components/volume_actions/widget_list_tile_volume_actions.dart
new file mode 100644
index 0000000..0502b60
--- /dev/null
+++ b/lib/screens/settings/components/general/components/volume_actions/widget_list_tile_volume_actions.dart
@@ -0,0 +1,22 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:lightmeter/generated/l10n.dart';
+import 'package:lightmeter/res/dimens.dart';
+import 'package:lightmeter/screens/settings/components/general/components/volume_actions/bloc_list_tile_volume_actions.dart';
+
+class VolumeActionsListTile extends StatelessWidget {
+ const VolumeActionsListTile({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return BlocBuilder(
+ builder: (context, state) => SwitchListTile(
+ secondary: const Icon(Icons.volume_up),
+ title: Text(S.of(context).volumeKeysAction),
+ value: state,
+ onChanged: context.read().onVolumeActionChanged,
+ contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
+ ),
+ );
+ }
+}
diff --git a/lib/screens/settings/components/general/widget_settings_section_general.dart b/lib/screens/settings/components/general/widget_settings_section_general.dart
index b873d3f..bc123d7 100644
--- a/lib/screens/settings/components/general/widget_settings_section_general.dart
+++ b/lib/screens/settings/components/general/widget_settings_section_general.dart
@@ -1,8 +1,11 @@
+import 'dart:io';
+
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/screens/settings/components/general/components/caffeine/provider_list_tile_caffeine.dart';
import 'package:lightmeter/screens/settings/components/general/components/haptics/provider_list_tile_haptics.dart';
import 'package:lightmeter/screens/settings/components/general/components/language/widget_list_tile_language.dart';
+import 'package:lightmeter/screens/settings/components/general/components/volume_actions/provider_list_tile_volume_actions.dart';
import 'package:lightmeter/screens/settings/components/shared/settings_section/widget_settings_section.dart';
class GeneralSettingsSection extends StatelessWidget {
@@ -12,10 +15,11 @@ class GeneralSettingsSection extends StatelessWidget {
Widget build(BuildContext context) {
return SettingsSection(
title: S.of(context).general,
- children: const [
- CaffeineListTileProvider(),
- HapticsListTileProvider(),
- LanguageListTile(),
+ children: [
+ const CaffeineListTileProvider(),
+ const HapticsListTileProvider(),
+ if (Platform.isAndroid) const VolumeActionsListTileProvider(),
+ const LanguageListTile(),
],
);
}
diff --git a/lib/screens/settings/components/theme/components/dynamic_color/widget_list_tile_dynamic_color.dart b/lib/screens/settings/components/theme/components/dynamic_color/widget_list_tile_dynamic_color.dart
index f15186e..d230f63 100644
--- a/lib/screens/settings/components/theme/components/dynamic_color/widget_list_tile_dynamic_color.dart
+++ b/lib/screens/settings/components/theme/components/dynamic_color/widget_list_tile_dynamic_color.dart
@@ -2,6 +2,7 @@ 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/res/dimens.dart';
import 'package:lightmeter/utils/inherited_generics.dart';
class DynamicColorListTile extends StatelessWidget {
@@ -14,6 +15,7 @@ class DynamicColorListTile extends StatelessWidget {
title: Text(S.of(context).dynamicColor),
value: context.listen() == DynamicColorState.enabled,
onChanged: ThemeProvider.of(context).enableDynamicColor,
+ contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
);
}
}
diff --git a/lib/screens/settings/flow_settings.dart b/lib/screens/settings/flow_settings.dart
index 5b9d5b3..3195c25 100644
--- a/lib/screens/settings/flow_settings.dart
+++ b/lib/screens/settings/flow_settings.dart
@@ -2,6 +2,7 @@ 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/screens/settings/screen_settings.dart';
import 'package:lightmeter/utils/inherited_generics.dart';
@@ -16,6 +17,7 @@ class SettingsFlow extends StatelessWidget {
context.get(),
context.get(),
context.get(),
+ context.get(),
),
child: const SettingsScreen(),
);
diff --git a/lib/screens/settings/screen_settings.dart b/lib/screens/settings/screen_settings.dart
index 2adf906..3c745bd 100644
--- a/lib/screens/settings/screen_settings.dart
+++ b/lib/screens/settings/screen_settings.dart
@@ -1,14 +1,33 @@
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/shared/sliver_screen/screen_sliver.dart';
+import 'package:lightmeter/utils/inherited_generics.dart';
-class SettingsScreen extends StatelessWidget {
+class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key});
+ @override
+ State createState() => _SettingsScreenState();
+}
+
+class _SettingsScreenState extends State {
+ @override
+ void didChangeDependencies() {
+ super.didChangeDependencies();
+ context.get().disableVolumeHandling();
+ }
+
+ @override
+ void deactivate() {
+ context.get().restoreVolumeHandling();
+ super.deactivate();
+ }
+
@override
Widget build(BuildContext context) {
return ScaffoldMessenger(
diff --git a/pubspec.yaml b/pubspec.yaml
index 8a230c8..bd72758 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -30,6 +30,7 @@ dependencies:
material_color_utilities: 0.2.0
package_info_plus: 4.0.1
permission_handler: 10.2.0
+ platform: 3.1.0
shared_preferences: 2.1.1
url_launcher: 6.1.11
uuid: 3.0.7
diff --git a/resources/social_preview.png b/resources/social_preview.png
new file mode 100644
index 0000000000000000000000000000000000000000..90dc77b56de62c65182804b557c98d71bcc3c52b
GIT binary patch
literal 48068
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ℑXSU($I18-;PS@I%hz`
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