diff --git a/lib/constants/storage_keys.dart b/lib/constants/storage_keys.dart index 387d13e..143255e 100644 --- a/lib/constants/storage_keys.dart +++ b/lib/constants/storage_keys.dart @@ -1 +1,2 @@ const CACHE_KEY = '_cache'; +const SETTINGS_KEY = 'settings'; diff --git a/lib/constants/themes.dart b/lib/constants/themes.dart index 6f6c94b..3742827 100644 --- a/lib/constants/themes.dart +++ b/lib/constants/themes.dart @@ -1,5 +1,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:quid_faciam_hodie/constants/spacing.dart'; final LIGHT_THEME_MATERIAL = ThemeData( textTheme: ThemeData().textTheme.copyWith( @@ -13,7 +14,7 @@ final LIGHT_THEME_MATERIAL = ThemeData( helperMaxLines: 10, errorMaxLines: 10, border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(MEDIUM_SPACE), ), ), ); @@ -30,7 +31,7 @@ final DARK_THEME_MATERIAL = ThemeData.dark().copyWith( helperMaxLines: 10, errorMaxLines: 10, border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(MEDIUM_SPACE), ), ), ); diff --git a/lib/enum_mapping/resolution_preset/texts.dart b/lib/enum_mapping/resolution_preset/texts.dart new file mode 100644 index 0000000..0249ccb --- /dev/null +++ b/lib/enum_mapping/resolution_preset/texts.dart @@ -0,0 +1,19 @@ +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +Map getResolutionTextMapping( + final BuildContext context) { + final localizations = AppLocalizations.of(context)!; + + return { + ResolutionPreset.low: localizations.enumMapping_ResolutionPreset_low, + ResolutionPreset.medium: localizations.enumMapping_ResolutionPreset_medium, + ResolutionPreset.high: localizations.enumMapping_ResolutionPreset_high, + ResolutionPreset.veryHigh: + localizations.enumMapping_ResolutionPreset_veryHigh, + ResolutionPreset.ultraHigh: + localizations.enumMapping_ResolutionPreset_ultraHigh, + ResolutionPreset.max: localizations.enumMapping_ResolutionPreset_max, + }; +} diff --git a/lib/locale/l10n/app_de.arb b/lib/locale/l10n/app_de.arb index 5eeb6f4..9af540e 100644 --- a/lib/locale/l10n/app_de.arb +++ b/lib/locale/l10n/app_de.arb @@ -79,5 +79,15 @@ "settingsScreenDangerSectionTitle": "Gefahrbereich", "settingsScreenDangerSectionDeleteAccountLabel": "Account löschen", "settingsScreenDeleteAccountDescription": "Bist du dir sicher, dass du deinen Account löschen möchtest? Diese Aktion kann nicht rückgangig gemacht werden! Deine Erfahrungen werden ebenfalls gelöscht.", - "settingsScreenDeleteAccountConfirmLabel": "Account jetzt löschen" + "settingsScreenDeleteAccountConfirmLabel": "Account jetzt löschen", + "settingsScreenGeneralSectionTitle": "General", + "settingsScreenGeneralSectionQualityLabel": "Quality", + + + "enumMapping_ResolutionPreset_low": "Niedrig", + "enumMapping_ResolutionPreset_medium": "Medium", + "enumMapping_ResolutionPreset_high": "Hoch", + "enumMapping_ResolutionPreset_veryHigh": "Sehr Hoch", + "enumMapping_ResolutionPreset_ultraHigh": "Ultra Hoch", + "enumMapping_ResolutionPreset_max": "Max" } \ No newline at end of file diff --git a/lib/locale/l10n/app_en.arb b/lib/locale/l10n/app_en.arb index dc34770..0937dbf 100644 --- a/lib/locale/l10n/app_en.arb +++ b/lib/locale/l10n/app_en.arb @@ -108,5 +108,15 @@ "settingsScreenDangerSectionTitle": "Danger Zone", "settingsScreenDangerSectionDeleteAccountLabel": "Delete Account", "settingsScreenDeleteAccountDescription": "Are you sure you want to delete your account? This action cannot be undone! All your memories will be deleted as well.", - "settingsScreenDeleteAccountConfirmLabel": "Delete Account now" + "settingsScreenDeleteAccountConfirmLabel": "Delete Account now", + "settingsScreenGeneralSectionTitle": "General", + "settingsScreenGeneralSectionQualityLabel": "Quality", + + + "enumMapping_ResolutionPreset_low": "Low", + "enumMapping_ResolutionPreset_medium": "Medium", + "enumMapping_ResolutionPreset_high": "High", + "enumMapping_ResolutionPreset_veryHigh": "Very High", + "enumMapping_ResolutionPreset_ultraHigh": "Ultra High", + "enumMapping_ResolutionPreset_max": "Max" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 5d24f20..afa9c76 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -27,7 +27,7 @@ void main() async { DeviceOrientation.portraitDown, ]); - GlobalValuesManager.initializeServer(); + GlobalValuesManager.initialize(); runApp(const MyApp()); } @@ -52,7 +52,7 @@ class _MyAppState extends State { } Future watchAuthenticationStatus() async { - await GlobalValuesManager.waitForServerInitialization(); + await GlobalValuesManager.watchForInitialization(); Supabase.instance.client.auth.onAuthStateChange((event, session) { switch (event) { diff --git a/lib/managers/file_manager.dart b/lib/managers/file_manager.dart index 9e03c5d..cda2baf 100644 --- a/lib/managers/file_manager.dart +++ b/lib/managers/file_manager.dart @@ -17,7 +17,7 @@ final supabase = Supabase.instance.client; class FileManager { static Future getMemoryMetadata(final String id) async { - await GlobalValuesManager.waitForServerInitialization(); + await GlobalValuesManager.watchForInitialization(); final response = await supabase .from('memories') @@ -38,7 +38,7 @@ class FileManager { final File file, { LocationData? locationData, }) async { - await GlobalValuesManager.waitForServerInitialization(); + await GlobalValuesManager.watchForInitialization(); final basename = uuid.v4(); final extension = file.path.split('.').last; @@ -108,7 +108,7 @@ class FileManager { final bool disableDownloadCache = false, final bool disableFileCache = false, }) async { - await GlobalValuesManager.waitForServerInitialization(); + await GlobalValuesManager.watchForInitialization(); final tempDirectory = await getTemporaryDirectory(); final filename = '${tempDirectory.path}/$path'; @@ -129,7 +129,7 @@ class FileManager { } static Future deleteFile(final String path) async { - await GlobalValuesManager.waitForServerInitialization(); + await GlobalValuesManager.watchForInitialization(); final response = await supabase.from('memories').delete().eq('location', path).execute(); diff --git a/lib/managers/global_values_manager.dart b/lib/managers/global_values_manager.dart index d1fce63..4c6fcc3 100644 --- a/lib/managers/global_values_manager.dart +++ b/lib/managers/global_values_manager.dart @@ -2,15 +2,19 @@ import 'package:camera/camera.dart'; import 'package:flutter/foundation.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:quid_faciam_hodie/constants/apis.dart'; +import 'package:quid_faciam_hodie/models/settings.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; class GlobalValuesManager { static Future? _serverInitializationFuture; + static Future? _settingsInitializationFuture; static bool _isServerInitialized = false; static List _cameras = []; + static Settings? _settings; static List get cameras => [..._cameras]; static bool get isServerInitialized => _isServerInitialized; + static Settings? get settings => _settings; static void setCameras(List cameras) { if (_cameras.isNotEmpty) { @@ -20,7 +24,7 @@ class GlobalValuesManager { _cameras = cameras; } - static void initializeServer() { + static void _initializeServer() { if (_isServerInitialized || _serverInitializationFuture != null) { return; } @@ -35,16 +39,41 @@ class GlobalValuesManager { }); } - static Future waitForServerInitialization() async { + static void _initializeSettings() { + _settingsInitializationFuture = Settings.restore() + ..then((settings) { + _settings = settings; + _settingsInitializationFuture = null; + }); + } + + static void initialize() { + _initializeServer(); + _initializeSettings(); + } + + static Future watchForInitialization() async { + // Server initialization if (_serverInitializationFuture == null) { if (_isServerInitialized) { return; } else { throw Exception('Server has not been initialized yet'); } + } else { + await _serverInitializationFuture; } - await _serverInitializationFuture; + // Settings initialization + if (_settingsInitializationFuture == null) { + if (_settings == null) { + throw Exception('Settings have not been initialized yet'); + } else { + return; + } + } else { + await _settingsInitializationFuture; + } } static Future hasGrantedPermissions() async => diff --git a/lib/models/memories.dart b/lib/models/memories.dart index 246915b..40b6d23 100644 --- a/lib/models/memories.dart +++ b/lib/models/memories.dart @@ -109,7 +109,7 @@ class Memories extends PropertyChangeNotifier { } Future _loadInitialData() async { - await GlobalValuesManager.waitForServerInitialization(); + await GlobalValuesManager.watchForInitialization(); final response = await supabase .from('memories') diff --git a/lib/models/settings.dart b/lib/models/settings.dart new file mode 100644 index 0000000..472a2ca --- /dev/null +++ b/lib/models/settings.dart @@ -0,0 +1,52 @@ +import 'dart:convert'; + +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:quid_faciam_hodie/constants/storage_keys.dart'; + +const secure = FlutterSecureStorage(); + +class Settings extends ChangeNotifier { + ResolutionPreset _resolution = ResolutionPreset.high; + + Settings({final ResolutionPreset resolution = ResolutionPreset.high}) + : _resolution = resolution; + + ResolutionPreset get resolution => _resolution; + + Map toJSONData() => { + 'resolution': _resolution.toString(), + }; + + Future save() async { + final data = toJSONData(); + + await secure.write( + key: SETTINGS_KEY, + value: jsonEncode(data), + ); + } + + static Future restore() async { + final rawData = await secure.read(key: SETTINGS_KEY); + + if (rawData == null) { + return Settings(); + } + + final data = jsonDecode(rawData); + final resolution = ResolutionPreset.values.firstWhere( + (preset) => preset.toString() == data['resolution'], + ); + return Settings( + resolution: resolution, + ); + } + + void setResolution(final ResolutionPreset value) { + _resolution = value; + notifyListeners(); + save(); + } +} diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index 0e4956d..d97e309 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -71,6 +71,7 @@ class _MainScreenState extends AuthRequiredState with Loadable { void initState() { super.initState(); + loadSettings(); loadCameras(); } @@ -85,6 +86,18 @@ class _MainScreenState extends AuthRequiredState with Loadable { _updateCamera(state); } + Future loadSettings() async { + final settings = GlobalValuesManager.settings!; + + settings.addListener(() { + if (!mounted || controller == null) { + return; + } + + onNewCameraSelected(controller!.description); + }); + } + Future loadCameras() async { GlobalValuesManager.setCameras(await availableCameras()); @@ -116,11 +129,13 @@ class _MainScreenState extends AuthRequiredState with Loadable { } void onNewCameraSelected(final CameraDescription cameraDescription) async { + final settings = GlobalValuesManager.settings!; final previousCameraController = controller; + // Instantiating the camera controller final CameraController cameraController = CameraController( cameraDescription, - ResolutionPreset.high, + settings.resolution, imageFormatGroup: ImageFormatGroup.jpeg, ); cameraController.setFlashMode(FlashMode.off); @@ -192,13 +207,6 @@ class _MainScreenState extends AuthRequiredState with Loadable { } final file = File((await controller!.takePicture()).path); - LocationData? locationData; - - if (Platform.isAndroid && (await Permission.location.isGranted)) { - locationData = await Location().getLocation(); - - await tagLocationToImage(file, locationData); - } setState(() { uploadingPhotoAnimation = file.readAsBytesSync(); @@ -209,6 +217,14 @@ class _MainScreenState extends AuthRequiredState with Loadable { message: localizations.mainScreenTakePhotoActionUploadingPhoto, ); + LocationData? locationData; + + if (Platform.isAndroid && (await Permission.location.isGranted)) { + locationData = await Location().getLocation(); + + await tagLocationToImage(file, locationData); + } + try { await FileManager.uploadFile(_user, file, locationData: locationData); } catch (error) { diff --git a/lib/screens/server_loading_screen.dart b/lib/screens/server_loading_screen.dart index 93aca73..45264a6 100644 --- a/lib/screens/server_loading_screen.dart +++ b/lib/screens/server_loading_screen.dart @@ -43,7 +43,7 @@ class _ServerLoadingScreenState extends State { ); } - await GlobalValuesManager.waitForServerInitialization(); + await GlobalValuesManager.watchForInitialization(); final memories = context.read(); final session = Supabase.instance.client.auth.session(); diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 32e6e41..c370bea 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -1,3 +1,4 @@ +import 'package:camera/camera.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -5,7 +6,9 @@ import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:intl/intl.dart'; import 'package:quid_faciam_hodie/constants/spacing.dart'; +import 'package:quid_faciam_hodie/enum_mapping/resolution_preset/texts.dart'; import 'package:quid_faciam_hodie/extensions/snackbar.dart'; +import 'package:quid_faciam_hodie/managers/global_values_manager.dart'; import 'package:quid_faciam_hodie/screens/welcome_screen.dart'; import 'package:quid_faciam_hodie/utils/auth_required.dart'; import 'package:quid_faciam_hodie/utils/loadable.dart'; @@ -30,6 +33,18 @@ class _SettingsScreenState extends AuthRequiredState with Loadable { User? user; + @override + void initState() { + super.initState(); + + final settings = GlobalValuesManager.settings!; + + // Update UI when settings change + settings.addListener(() { + setState(() {}); + }); + } + @override void onAuthenticated(Session session) { if (session.user != null) { @@ -68,7 +83,9 @@ class _SettingsScreenState extends AuthRequiredState @override Widget build(BuildContext context) { + final settings = GlobalValuesManager.settings!; final localizations = AppLocalizations.of(context)!; + final resolutionTextMapping = getResolutionTextMapping(context); return PlatformScaffold( appBar: PlatformAppBar( @@ -141,6 +158,36 @@ class _SettingsScreenState extends AuthRequiredState ) ], ), + SettingsSection( + title: Text( + localizations.settingsScreenGeneralSectionTitle, + ), + tiles: [ + SettingsTile( + leading: Text( + localizations + .settingsScreenGeneralSectionQualityLabel, + ), + title: DropdownButtonFormField( + value: settings.resolution, + onChanged: (value) async { + if (value == null) { + return; + } + + settings.setResolution(value); + }, + items: ResolutionPreset.values + .map((value) => + DropdownMenuItem( + value: value, + child: Text(resolutionTextMapping[value]!), + )) + .toList(), + ), + ) + ], + ), SettingsSection( title: Text(localizations.settingsScreenDangerSectionTitle), tiles: [