diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 60bd3ee..005e3cf 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -32,4 +32,7 @@ android:name="flutterEmbedding" android:value="2" /> + + + diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..e9b0728 --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,53 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '11.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + + # Start of the permission_handler configuration + target.build_configurations.each do |config| + # Preprocessor definitions can be found in: https://github.com/Baseflow/flutter-permission-handler/blob/master/permission_handler_apple/ios/Classes/PermissionHandlerEnums.h + config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ + '$(inherited)', + 'PERMISSION_CAMERA=1', + + ## dart: PermissionGroup.sensors + # 'PERMISSION_SENSORS=1', + ] + end + end +end diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 5a40744..9b13e6e 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -1,49 +1,51 @@ - - CADisableMinimumFrameDurationOnPhone - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Lightmeter - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - lightmeter - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - UIApplicationSupportsIndirectInputEvents - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - - + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Lightmeter + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + lightmeter + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + NSCameraUsageDescription + Provide camera permissions in order to make measurements + + \ No newline at end of file diff --git a/lib/data/permissions_service.dart b/lib/data/permissions_service.dart new file mode 100644 index 0000000..fb04332 --- /dev/null +++ b/lib/data/permissions_service.dart @@ -0,0 +1,7 @@ +import 'package:permission_handler/permission_handler.dart'; + +class PermissionsService { + Future checkCameraPermission() async => await Permission.camera.status; + + Future requestCameraPermission() async => Permission.camera.request(); +} diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index f9d8167..434a72a 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -27,6 +27,11 @@ class MessageLookup extends MessageLookupByLibrary { "haptics": MessageLookupByLibrary.simpleMessage("Haptics"), "keepsScreenOn": MessageLookupByLibrary.simpleMessage("Keeps screen on"), + "openSettings": MessageLookupByLibrary.simpleMessage("Open settings"), + "permissionNeeded": + MessageLookupByLibrary.simpleMessage("Permission needed"), + "permissionNeededMessage": MessageLookupByLibrary.simpleMessage( + "To use Lightmeter, turn on Camera permissions."), "settings": MessageLookupByLibrary.simpleMessage("Settings"), "slowestExposurePair": MessageLookupByLibrary.simpleMessage("Slowest") }; diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 4fa1b88..8e6b760 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -50,6 +50,36 @@ class S { return Localizations.of(context, S); } + /// `Permission needed` + String get permissionNeeded { + return Intl.message( + 'Permission needed', + name: 'permissionNeeded', + desc: '', + args: [], + ); + } + + /// `To use Lightmeter, turn on Camera permissions.` + String get permissionNeededMessage { + return Intl.message( + 'To use Lightmeter, turn on Camera permissions.', + name: 'permissionNeededMessage', + desc: '', + args: [], + ); + } + + /// `Open settings` + String get openSettings { + return Intl.message( + 'Open settings', + name: 'openSettings', + desc: '', + args: [], + ); + } + /// `Fastest` String get fastestExposurePair { return Intl.message( diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 8c8f9e9..86b8586 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -1,5 +1,8 @@ { "@@locale": "en", + "permissionNeeded": "Permission needed", + "permissionNeededMessage": "To use Lightmeter, turn on Camera permissions.", + "openSettings": "Open settings", "fastestExposurePair": "Fastest", "slowestExposurePair": "Slowest", "settings": "Settings", diff --git a/lib/main.dart b/lib/main.dart index 12546a7..ed3afcd 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,14 +2,16 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:lightmeter/data/permissions_service.dart'; import 'package:lightmeter/screens/settings/settings_page_route_builder.dart'; +import 'package:provider/provider.dart'; import 'generated/l10n.dart'; import 'models/photography_value.dart'; import 'res/dimens.dart'; import 'res/theme.dart'; import 'screens/metering/metering_bloc.dart'; -import 'screens/metering/metering_screen.dart'; +import 'screens/permissions_check/flow_permissions_check.dart'; import 'utils/stop_type_provider.dart'; void main() { @@ -57,23 +59,27 @@ class _ApplicationState extends State with TickerProviderStateMixin @override Widget build(BuildContext context) { - return StopTypeProvider( - child: BlocProvider( - create: (context) => MeteringBloc(context.read()), - child: MaterialApp( - theme: ThemeData( - useMaterial3: true, - colorScheme: lightColorScheme, + return Provider( + create: (context) => PermissionsService(), + child: StopTypeProvider( + child: BlocProvider( + create: (context) => MeteringBloc(context.read()), + child: MaterialApp( + theme: ThemeData( + useMaterial3: true, + colorScheme: lightColorScheme, + ), + navigatorObservers: [_settingsRouteObserver], + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: S.delegate.supportedLocales, + home: const PermissionsCheckFlow(), + //home: MeteringScreen(animationController: _animationController), ), - navigatorObservers: [_settingsRouteObserver], - localizationsDelegates: const [ - S.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: S.delegate.supportedLocales, - home: MeteringScreen(animationController: _animationController), ), ), ); diff --git a/lib/res/dimens.dart b/lib/res/dimens.dart index 95f4396..34adfc3 100644 --- a/lib/res/dimens.dart +++ b/lib/res/dimens.dart @@ -8,8 +8,10 @@ class Dimens { static const double grid8 = 8; static const double grid16 = 16; static const double grid24 = 24; + static const double grid168 = 168; static const double paddingM = 16; + static const double paddingL = 24; static const Duration durationS = Duration(milliseconds: 100); static const Duration durationSM = Duration(milliseconds: 150); diff --git a/lib/screens/permissions_check/bloc_permissions_check.dart b/lib/screens/permissions_check/bloc_permissions_check.dart new file mode 100644 index 0000000..88efd42 --- /dev/null +++ b/lib/screens/permissions_check/bloc_permissions_check.dart @@ -0,0 +1,46 @@ +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 new file mode 100644 index 0000000..2d93f1d --- /dev/null +++ b/lib/screens/permissions_check/event_permissions_check.dart @@ -0,0 +1,11 @@ +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 new file mode 100644 index 0000000..7fb650c --- /dev/null +++ b/lib/screens/permissions_check/flow_permissions_check.dart @@ -0,0 +1,18 @@ +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 new file mode 100644 index 0000000..f3cff07 --- /dev/null +++ b/lib/screens/permissions_check/screen_permissions_check.dart @@ -0,0 +1,65 @@ +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/settings_screen.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).pushReplacement(MaterialPageRoute(builder: (_) => SettingsScreen())); + } + }, + 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 new file mode 100644 index 0000000..f94ac6c --- /dev/null +++ b/lib/screens/permissions_check/state_permissions_check.dart @@ -0,0 +1,15 @@ +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/lib/screens/settings/settings_screen.dart b/lib/screens/settings/settings_screen.dart index 85bbcd2..e4bd939 100644 --- a/lib/screens/settings/settings_screen.dart +++ b/lib/screens/settings/settings_screen.dart @@ -17,7 +17,7 @@ class SettingsScreen extends StatelessWidget { SliverAppBar( pinned: true, automaticallyImplyLeading: false, - expandedHeight: 160.0, + expandedHeight: Dimens.grid168, flexibleSpace: FlexibleSpaceBar( centerTitle: false, titlePadding: const EdgeInsets.all(Dimens.paddingM), diff --git a/pubspec.yaml b/pubspec.yaml index 234a493..513c9f8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,6 +14,7 @@ dependencies: sdk: flutter intl: ^0.17.0 material_color_utilities: ^0.2.0 + permission_handler: 10.2.0 provider: ^6.0.4 dev_dependencies: