diff --git a/.vscode/launch.json b/.vscode/launch.json index fb960be..a9059b5 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,6 +8,7 @@ "name": "dev (android)", "request": "launch", "type": "dart", + //"flutterMode": "profile", "args": [ "--flavor", "dev", diff --git a/lib/interactors/metering_interactor.dart b/lib/interactors/metering_interactor.dart index e58446c..bbac538 100644 --- a/lib/interactors/metering_interactor.dart +++ b/lib/interactors/metering_interactor.dart @@ -1,17 +1,22 @@ import 'dart:io'; +import 'package:app_settings/app_settings.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:permission_handler/permission_handler.dart'; class MeteringInteractor { final UserPreferencesService _userPreferencesService; final HapticsService _hapticsService; + final PermissionsService _permissionsService; final LightSensorService _lightSensorService; const MeteringInteractor( this._userPreferencesService, this._hapticsService, + this._permissionsService, this._lightSensorService, ); @@ -30,6 +35,22 @@ class MeteringInteractor { if (_userPreferencesService.haptics) _hapticsService.responseVibration(); } + Future checkCameraPermission() async { + return _permissionsService + .checkCameraPermission() + .then((value) => value == PermissionStatus.granted); + } + + Future requestPermission() async { + return _permissionsService + .requestCameraPermission() + .then((value) => value == PermissionStatus.granted); + } + + void openAppSettings() { + AppSettings.openAppSettings(); + } + void enableHaptics(bool enable) => _userPreferencesService.haptics = enable; Future hasAmbientLightSensor() async { diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index c984bce..6b3a39d 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -18,6 +18,9 @@ "nd": "ND", "ndFilterFactor": "Neutral density filter factor", "noExposurePairs": "There are no exposure pairs for the selected settings.", + "noCamerasDetected": "Seems like your device doesn't have any cameras connected", + "noCameraPermission": "Camera permission is not granted.", + "otherCameraError": "An error occurred when connecting to the camera.", "none": "None", "cancel": "Cancel", "select": "Select", 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 34d4fb5..db69fcb 100644 --- a/lib/screens/metering/components/camera_container/bloc_container_camera.dart +++ b/lib/screens/metering/components/camera_container/bloc_container_camera.dart @@ -7,6 +7,7 @@ import 'package:exif/exif.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:lightmeter/interactors/metering_interactor.dart'; +import 'package:lightmeter/screens/metering/components/camera_container/models/camera_error_type.dart'; import 'package:lightmeter/screens/metering/components/shared/ev_source_base/bloc_base_ev_source.dart'; import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart'; import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart' @@ -43,12 +44,14 @@ class CameraContainerBloc extends EvSourceBlocBase(_onRequestPermission); + on(_onOpenAppSettings); on(_onInitialize); on(_onZoomChanged); on(_onExposureOffsetChanged); on(_onExposureOffsetResetEvent); - add(const InitializeEvent()); + add(const RequestPermissionEvent()); } @override @@ -71,10 +74,33 @@ class CameraContainerBloc extends EvSourceBlocBase _onRequestPermission(_, Emitter emit) async { + final hasPermission = await _meteringInteractor.requestPermission(); + if (!hasPermission) { + emit(const CameraErrorState(CameraErrorType.permissionNotGranted)); + } else { + add(const InitializeEvent()); + } + } + + Future _onOpenAppSettings(_, Emitter emit) async { + _meteringInteractor.openAppSettings(); + } + Future _onInitialize(_, Emitter emit) async { emit(const CameraLoadingState()); + final hasPermission = await _meteringInteractor.checkCameraPermission(); + if (!hasPermission) { + emit(const CameraErrorState(CameraErrorType.permissionNotGranted)); + return; + } + try { final cameras = await availableCameras(); + if (cameras.isEmpty) { + emit(const CameraErrorState(CameraErrorType.noCamerasDetected)); + return; + } _cameraController = CameraController( cameras.firstWhere( (camera) => camera.lensDirection == CameraLensDirection.back, @@ -111,7 +137,7 @@ class CameraContainerBloc extends EvSourceBlocBase onLifecycleStateChanged; + AppLifecycleState? _prevState; _WidgetsBindingObserver(this.onLifecycleStateChanged); @override void didChangeAppLifecycleState(AppLifecycleState state) { + if (_prevState == AppLifecycleState.inactive && state == AppLifecycleState.resumed) { + return; + } + _prevState = state; onLifecycleStateChanged(state); } } diff --git a/lib/screens/metering/components/camera_container/components/camera_controls_placeholder/widget_placeholder_camera_controls.dart b/lib/screens/metering/components/camera_container/components/camera_controls_placeholder/widget_placeholder_camera_controls.dart new file mode 100644 index 0000000..cb4d76b --- /dev/null +++ b/lib/screens/metering/components/camera_container/components/camera_controls_placeholder/widget_placeholder_camera_controls.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/res/dimens.dart'; +import 'package:lightmeter/screens/metering/components/camera_container/models/camera_error_type.dart'; + +class CameraControlsPlaceholder extends StatelessWidget { + final CameraErrorType error; + final VoidCallback onReset; + + const CameraControlsPlaceholder({ + required this.error, + required this.onReset, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: onReset, + icon: Icon(error == CameraErrorType.permissionNotGranted ? Icons.settings : Icons.sync), + ), + const SizedBox(height: Dimens.grid8), + Text( + error.toStringLocalized(context), + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(color: Theme.of(context).colorScheme.onBackground), + textAlign: TextAlign.center, + ), + ], + ); + } +} diff --git a/lib/screens/metering/components/camera_container/components/camera_view_placeholder/widget_placeholder_camera_view.dart b/lib/screens/metering/components/camera_container/components/camera_view_placeholder/widget_placeholder_camera_view.dart index 5b73922..058da74 100644 --- a/lib/screens/metering/components/camera_container/components/camera_view_placeholder/widget_placeholder_camera_view.dart +++ b/lib/screens/metering/components/camera_container/components/camera_view_placeholder/widget_placeholder_camera_view.dart @@ -1,14 +1,19 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/res/dimens.dart'; +import 'package:lightmeter/screens/metering/components/camera_container/models/camera_error_type.dart'; class CameraViewPlaceholder extends StatelessWidget { - const CameraViewPlaceholder({super.key}); + final CameraErrorType? error; + + const CameraViewPlaceholder({required this.error, super.key}); @override Widget build(BuildContext context) { return Card( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(Dimens.borderRadiusM)), - child: const Center(child: Icon(Icons.no_photography)), + child: Center( + child: error != null ? const Icon(Icons.no_photography) : const CircularProgressIndicator(), + ), ); } } diff --git a/lib/screens/metering/components/camera_container/event_container_camera.dart b/lib/screens/metering/components/camera_container/event_container_camera.dart index 9467a5c..e90aa0a 100644 --- a/lib/screens/metering/components/camera_container/event_container_camera.dart +++ b/lib/screens/metering/components/camera_container/event_container_camera.dart @@ -2,10 +2,22 @@ abstract class CameraContainerEvent { const CameraContainerEvent(); } +class RequestPermissionEvent extends CameraContainerEvent { + const RequestPermissionEvent(); +} + +class OpenAppSettingsEvent extends CameraContainerEvent { + const OpenAppSettingsEvent(); +} + class InitializeEvent extends CameraContainerEvent { const InitializeEvent(); } +class ReinitializeEvent extends CameraContainerEvent { + const ReinitializeEvent(); +} + class ZoomChangedEvent extends CameraContainerEvent { final double value; diff --git a/lib/screens/metering/components/camera_container/models/camera_error_type.dart b/lib/screens/metering/components/camera_container/models/camera_error_type.dart new file mode 100644 index 0000000..b7d7c78 --- /dev/null +++ b/lib/screens/metering/components/camera_container/models/camera_error_type.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/generated/l10n.dart'; + +enum CameraErrorType { noCamerasDetected, permissionNotGranted, other } + +extension CameraErrorTypeString on CameraErrorType { + String toStringLocalized(BuildContext context) { + switch (this) { + case CameraErrorType.noCamerasDetected: + return S.of(context).noCamerasDetected; + case CameraErrorType.permissionNotGranted: + return S.of(context).noCameraPermission; + default: + return S.of(context).otherCameraError; + } + } +} diff --git a/lib/screens/metering/components/camera_container/provider_container_camera.dart b/lib/screens/metering/components/camera_container/provider_container_camera.dart index 4b16363..729322e 100644 --- a/lib/screens/metering/components/camera_container/provider_container_camera.dart +++ b/lib/screens/metering/components/camera_container/provider_container_camera.dart @@ -32,6 +32,7 @@ class CameraContainerProvider extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( + lazy: false, create: (context) => CameraContainerBloc( context.read(), context.read(), diff --git a/lib/screens/metering/components/camera_container/state_container_camera.dart b/lib/screens/metering/components/camera_container/state_container_camera.dart index ccc54bc..b8e6173 100644 --- a/lib/screens/metering/components/camera_container/state_container_camera.dart +++ b/lib/screens/metering/components/camera_container/state_container_camera.dart @@ -1,5 +1,6 @@ import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; +import 'package:lightmeter/screens/metering/components/camera_container/models/camera_error_type.dart'; abstract class CameraContainerState { const CameraContainerState(); @@ -36,5 +37,7 @@ class CameraActiveState extends CameraContainerState { } class CameraErrorState extends CameraContainerState { - const CameraErrorState(); + final CameraErrorType error; + + const CameraErrorState(this.error); } diff --git a/lib/screens/metering/components/camera_container/widget_container_camera.dart b/lib/screens/metering/components/camera_container/widget_container_camera.dart index d73765f..34906aa 100644 --- a/lib/screens/metering/components/camera_container/widget_container_camera.dart +++ b/lib/screens/metering/components/camera_container/widget_container_camera.dart @@ -6,12 +6,14 @@ import 'package:lightmeter/data/models/photography_values/nd_value.dart'; import 'package:lightmeter/platform_config.dart'; import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/screens/metering/components/camera_container/components/camera_view/widget_camera_view.dart'; +import 'package:lightmeter/screens/metering/components/camera_container/models/camera_error_type.dart'; import 'package:lightmeter/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart'; import 'package:lightmeter/screens/metering/components/shared/metering_top_bar/widget_top_bar_metering.dart'; import 'package:lightmeter/screens/metering/components/shared/readings_container/widget_container_readings.dart'; import 'bloc_container_camera.dart'; import 'components/camera_controls/widget_camera_controls.dart'; +import 'components/camera_controls_placeholder/widget_placeholder_camera_controls.dart'; import 'components/camera_view_placeholder/widget_placeholder_camera_view.dart'; import 'event_container_camera.dart'; import 'state_container_camera.dart'; @@ -78,10 +80,17 @@ class _CameraViewBuilder extends StatelessWidget { return AspectRatio( aspectRatio: PlatformConfig.cameraPreviewAspectRatio, child: BlocBuilder( - buildWhen: (previous, current) => current is CameraInitializedState, - builder: (context, state) => state is CameraInitializedState - ? Center(child: CameraView(controller: state.controller)) - : const CameraViewPlaceholder(), + 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); + } + }, ), ); } @@ -95,23 +104,40 @@ class _CameraControlsBuilder extends StatelessWidget { return Padding( padding: const EdgeInsets.symmetric(vertical: Dimens.paddingM), child: BlocBuilder( - builder: (context, state) => AnimatedSwitcher( - duration: Dimens.durationS, - child: state is CameraActiveState - ? CameraControls( - exposureOffsetRange: state.exposureOffsetRange, - exposureOffsetValue: state.currentExposureOffset, - onExposureOffsetChanged: (value) { - context.read().add(ExposureOffsetChangedEvent(value)); - }, - zoomRange: state.zoomRange, - zoomValue: state.currentZoom, - onZoomChanged: (value) { - context.read().add(ZoomChangedEvent(value)); - }, - ) - : const SizedBox.shrink(), - ), + builder: (context, state) { + late final Widget child; + if (state is CameraActiveState) { + child = CameraControls( + exposureOffsetRange: state.exposureOffsetRange, + exposureOffsetValue: state.currentExposureOffset, + onExposureOffsetChanged: (value) { + context.read().add(ExposureOffsetChangedEvent(value)); + }, + zoomRange: state.zoomRange, + zoomValue: state.currentZoom, + onZoomChanged: (value) { + context.read().add(ZoomChangedEvent(value)); + }, + ); + } else if (state is CameraErrorState) { + child = CameraControlsPlaceholder( + error: state.error, + onReset: () { + context.read().add( + state.error == CameraErrorType.permissionNotGranted + ? const OpenAppSettingsEvent() + : const InitializeEvent()); + }, + ); + } else { + child = const SizedBox.shrink(); + } + + return AnimatedSwitcher( + duration: Dimens.durationS, + child: child, + ); + }, ), ); } diff --git a/lib/screens/metering/flow_metering.dart b/lib/screens/metering/flow_metering.dart index e0a6125..162800f 100644 --- a/lib/screens/metering/flow_metering.dart +++ b/lib/screens/metering/flow_metering.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:lightmeter/data/haptics_service.dart'; import 'package:lightmeter/data/light_sensor_service.dart'; import 'package:lightmeter/data/models/photography_values/photography_value.dart'; +import 'package:lightmeter/data/permissions_service.dart'; import 'package:lightmeter/data/shared_prefs_service.dart'; import 'package:lightmeter/interactors/metering_interactor.dart'; import 'package:provider/provider.dart'; @@ -25,6 +26,7 @@ class _MeteringFlowState extends State { create: (context) => MeteringInteractor( context.read(), context.read(), + context.read(), context.read(), ), child: MultiBlocProvider( diff --git a/lib/screens/permissions_check/bloc_permissions_check.dart b/lib/screens/permissions_check/bloc_permissions_check.dart deleted file mode 100644 index 88efd42..0000000 --- a/lib/screens/permissions_check/bloc_permissions_check.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:lightmeter/data/permissions_service.dart'; -import 'package:permission_handler/permission_handler.dart'; - -import 'event_permissions_check.dart'; -import 'state_permissions_check.dart'; - -class PermissionsCheckBloc extends Bloc { - final PermissionsService _permissionsService; - - PermissionsCheckBloc(this._permissionsService) : super(const LoadingState()) { - on((event, emit) => emit(const PermissionsGrantedState())); - on((event, emit) => emit(const PermissionsDeniedState())); - _checkAndRequestPermissions(); - } - - Future _checkAndRequestPermissions() async { - _permissionsService.checkCameraPermission().then((value) { - switch (value) { - case PermissionStatus.permanentlyDenied: - case PermissionStatus.restricted: - add(const PermissionsDeniedEvent()); - break; - case PermissionStatus.denied: - _permissionsService.requestCameraPermission().then((value) { - switch (value) { - case PermissionStatus.permanentlyDenied: - case PermissionStatus.restricted: - case PermissionStatus.denied: - add(const PermissionsDeniedEvent()); - break; - case PermissionStatus.limited: - case PermissionStatus.granted: - add(const PermissionsGrantedEvent()); - break; - } - }); - break; - case PermissionStatus.limited: - case PermissionStatus.granted: - add(const PermissionsGrantedEvent()); - break; - } - }); - } -} diff --git a/lib/screens/permissions_check/event_permissions_check.dart b/lib/screens/permissions_check/event_permissions_check.dart deleted file mode 100644 index 2d93f1d..0000000 --- a/lib/screens/permissions_check/event_permissions_check.dart +++ /dev/null @@ -1,11 +0,0 @@ -abstract class PermissionsCheckEvent { - const PermissionsCheckEvent(); -} - -class PermissionsDeniedEvent extends PermissionsCheckEvent { - const PermissionsDeniedEvent(); -} - -class PermissionsGrantedEvent extends PermissionsCheckEvent { - const PermissionsGrantedEvent(); -} diff --git a/lib/screens/permissions_check/flow_permissions_check.dart b/lib/screens/permissions_check/flow_permissions_check.dart deleted file mode 100644 index 7fb650c..0000000 --- a/lib/screens/permissions_check/flow_permissions_check.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:lightmeter/data/permissions_service.dart'; -import 'package:lightmeter/screens/permissions_check/screen_permissions_check.dart'; - -import 'bloc_permissions_check.dart'; - -class PermissionsCheckFlow extends StatelessWidget { - const PermissionsCheckFlow({super.key}); - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => PermissionsCheckBloc(context.read()), - child: const PermissionsCheckScreen(), - ); - } -} diff --git a/lib/screens/permissions_check/screen_permissions_check.dart b/lib/screens/permissions_check/screen_permissions_check.dart deleted file mode 100644 index 18dfca7..0000000 --- a/lib/screens/permissions_check/screen_permissions_check.dart +++ /dev/null @@ -1,64 +0,0 @@ -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 'bloc_permissions_check.dart'; -import 'state_permissions_check.dart'; - -class PermissionsCheckScreen extends StatelessWidget { - const PermissionsCheckScreen({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).colorScheme.surface, - body: SafeArea( - child: Padding( - padding: const EdgeInsets.all(Dimens.paddingM * 2), - child: Center( - child: BlocConsumer( - listener: (context, state) { - if (state is PermissionsGrantedState) { - Navigator.of(context).pushReplacementNamed("metering"); - } - }, - builder: (context, state) { - return AnimatedSwitcher( - duration: Dimens.durationS, - child: state is LoadingState - ? const CircularProgressIndicator() - : Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - S.of(context).permissionNeeded, - style: Theme.of(context).textTheme.headlineLarge, - textAlign: TextAlign.center, - ), - const SizedBox(height: Dimens.grid16), - Text( - S.of(context).permissionNeededMessage, - style: Theme.of(context).textTheme.bodyLarge, - textAlign: TextAlign.center, - ), - const SizedBox(height: Dimens.grid24), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.primary, - foregroundColor: Theme.of(context).colorScheme.onPrimary, - ), - onPressed: () {}, - child: Text(S.of(context).openSettings), - ), - ], - ), - ); - }, - ), - ), - ), - ), - ); - } -} diff --git a/lib/screens/permissions_check/state_permissions_check.dart b/lib/screens/permissions_check/state_permissions_check.dart deleted file mode 100644 index f94ac6c..0000000 --- a/lib/screens/permissions_check/state_permissions_check.dart +++ /dev/null @@ -1,15 +0,0 @@ -abstract class PermissionsCheckState { - const PermissionsCheckState(); -} - -class LoadingState extends PermissionsCheckState { - const LoadingState(); -} - -class PermissionsGrantedState extends PermissionsCheckState { - const PermissionsGrantedState(); -} - -class PermissionsDeniedState extends PermissionsCheckState { - const PermissionsDeniedState(); -} diff --git a/pubspec.yaml b/pubspec.yaml index 19abd02..7873a0a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,6 +7,7 @@ environment: sdk: ">=2.18.0 <3.0.0" dependencies: + app_settings: 4.2.0 camera: 0.10.0+4 exif: 3.1.2 dynamic_color: 1.5.4