diff --git a/README.md b/README.md index 5eaee20..210adb9 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ The list of features that the old lightmeter app has and that have to be impleme ### Theme - [x] Dark theme - [x] Picking primary color -- [ ] Russian language +- [x] Russian language ## Build diff --git a/lib/application.dart b/lib/application.dart index 62615c3..f7ca38e 100644 --- a/lib/application.dart +++ b/lib/application.dart @@ -6,6 +6,8 @@ import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:light_sensor/light_sensor.dart'; import 'package:lightmeter/data/caffeine_service.dart'; import 'package:lightmeter/data/haptics_service.dart'; +import 'package:lightmeter/data/models/supported_locale.dart'; +import 'package:lightmeter/providers/supported_locale_provider.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -37,7 +39,8 @@ class Application extends StatelessWidget { return MultiProvider( providers: [ Provider.value(value: env.copyWith(hasLightSensor: snapshot.data![1] as bool)), - Provider(create: (_) => UserPreferencesService(snapshot.data![0] as SharedPreferences)), + Provider( + create: (_) => UserPreferencesService(snapshot.data![0] as SharedPreferences)), Provider(create: (_) => const CaffeineService()), Provider(create: (_) => const HapticsService()), Provider(create: (_) => PermissionsService()), @@ -45,23 +48,12 @@ class Application extends StatelessWidget { ], child: StopTypeProvider( child: EvSourceTypeProvider( - child: ThemeProvider( - builder: (context, _) { - final systemIconsBrightness = ThemeData.estimateBrightnessForColor( - context.watch().colorScheme.onSurface, - ); - return AnnotatedRegion( - value: SystemUiOverlayStyle( - statusBarColor: Colors.transparent, - statusBarBrightness: systemIconsBrightness == Brightness.light - ? Brightness.dark - : Brightness.light, - statusBarIconBrightness: systemIconsBrightness, - systemNavigationBarColor: Colors.transparent, - systemNavigationBarIconBrightness: systemIconsBrightness, - ), + child: SupportedLocaleProvider( + child: ThemeProvider( + builder: (context, _) => _AnnotatedRegionWrapper( child: MaterialApp( theme: context.watch(), + locale: Locale(context.watch().intlName), localizationsDelegates: const [ S.delegate, GlobalMaterialLocalizations.delegate, @@ -79,8 +71,8 @@ class Application extends StatelessWidget { "settings": (context) => const SettingsFlow(), }, ), - ); - }, + ), + ), ), ), ), @@ -91,3 +83,27 @@ class Application extends StatelessWidget { ); } } + +class _AnnotatedRegionWrapper extends StatelessWidget { + final Widget child; + + const _AnnotatedRegionWrapper({required this.child}); + + @override + Widget build(BuildContext context) { + final systemIconsBrightness = ThemeData.estimateBrightnessForColor( + context.watch().colorScheme.onSurface, + ); + return AnnotatedRegion( + value: SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarBrightness: + systemIconsBrightness == Brightness.light ? Brightness.dark : Brightness.light, + statusBarIconBrightness: systemIconsBrightness, + systemNavigationBarColor: Colors.transparent, + systemNavigationBarIconBrightness: systemIconsBrightness, + ), + child: child, + ); + } +} diff --git a/lib/data/models/supported_locale.dart b/lib/data/models/supported_locale.dart new file mode 100644 index 0000000..8cb4b50 --- /dev/null +++ b/lib/data/models/supported_locale.dart @@ -0,0 +1,28 @@ +import 'package:intl/intl.dart'; + +enum SupportedLocale { en, ru } + +SupportedLocale get currentLanguage { + switch (Intl.getCurrentLocale()) { + case "en": + return SupportedLocale.en; + case "ru": + return SupportedLocale.ru; + default: + return SupportedLocale.en; + } +} + +extension SupportedLocaleExtension on SupportedLocale { + String get intlName => toString().replaceAll("SupportedLocale.", ""); + + String get localizedName { + switch (this) { + case SupportedLocale.en: + return 'English'; + case SupportedLocale.ru: + return 'Русский'; + } + } +} + diff --git a/lib/data/shared_prefs_service.dart b/lib/data/shared_prefs_service.dart index d9861e7..e243d9b 100644 --- a/lib/data/shared_prefs_service.dart +++ b/lib/data/shared_prefs_service.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:lightmeter/data/models/supported_locale.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'models/ev_source_type.dart'; @@ -16,6 +17,7 @@ class UserPreferencesService { static const _caffeineKey = "caffeine"; static const _hapticsKey = "haptics"; + static const _localeKey = "locale"; static const _themeTypeKey = "themeType"; static const _primaryColorKey = "primaryColor"; @@ -40,6 +42,12 @@ class UserPreferencesService { bool get haptics => _sharedPreferences.getBool(_hapticsKey) ?? true; set haptics(bool value) => _sharedPreferences.setBool(_hapticsKey, value); + SupportedLocale get locale => SupportedLocale.values.firstWhere( + (e) => e.toString() == _sharedPreferences.getString(_localeKey), + orElse: () => SupportedLocale.en, + ); + set locale(SupportedLocale value) => _sharedPreferences.setString(_localeKey, value.toString()); + double get cameraEvCalibration => _sharedPreferences.getDouble(_cameraEvCalibrationKey) ?? 0.0; set cameraEvCalibration(double value) => _sharedPreferences.setDouble(_cameraEvCalibrationKey, value); diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 6d5dffc..9c91043 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -15,7 +15,7 @@ "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", + "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", @@ -36,6 +36,8 @@ "general": "General", "keepScreenOn": "Keep screen on", "haptics": "Haptics", + "language": "Language", + "chooseLanguage": "Choose language", "theme": "Theme", "chooseTheme": "Choose theme", "themeLight": "Light", diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb new file mode 100644 index 0000000..34e44f6 --- /dev/null +++ b/lib/l10n/intl_ru.arb @@ -0,0 +1,65 @@ +{ + "@@locale": "ru", + "fastestExposurePair": "Короткая выдержка", + "slowestExposurePair": "Длинная выдержка", + "ev": "{value} EV", + "@ev": { + "placeholders": { + "value": { + "type": "String" + } + } + }, + "iso": "ISO", + "filmSpeed": "Чувствительность плёнки", + "nd": "ND", + "ndFilterFactor": "Степень затемнения нейтрального фильтра", + "noExposurePairs": "Для выбранных настроек нет пар экспозиции.", + "noCamerasDetected": "Похоже, ваше устройство не имеет камеры.", + "noCameraPermission": "Нет разрешения на доступ к камере.", + "otherCameraError": "Произошла ошибка при подключении к камере.", + "none": "Нет", + "cancel": "Отменить", + "select": "Выбрать", + "save": "Сохранить", + "settings": "Настройки", + "metering": "Измерения", + "fractionalStops": "Дробные значения", + "showFractionalStops": "Показывать дробные значения", + "halfStops": "1/2", + "thirdStops": "1/3", + "calibration": "Калибровка", + "calibrationMessage": "Точность измерений данного приложения полностью зависит от точности камеры и датчика освещенности вашего устройства. Поэтому рекомендуется самостоятельно подобрать калибровочные значения, которые дадут желаемый результат измерений.", + "calibrationMessageCameraOnly": "Точность измерений данного приложения полностью зависит от точности камеры вашего устройства. Поэтому рекомендуется самостоятельно подобрать калибровочное значение, которое даст желаемый результат измерений.", + "camera": "Камера", + "lightSensor": "Датчик освещённости", + "general": "Общие", + "keepScreenOn": "Запрет блокировки", + "haptics": "Вибрация", + "language": "Язык", + "chooseLanguage": "Выберите язык", + "theme": "Тема", + "chooseTheme": "Выберите тему", + "themeLight": "Светлая", + "themeDark": "Тёмная", + "themeSystemDefault": "Системная", + "dynamicColor": "Динамический цвет", + "primaryColor": "Основной цвет", + "choosePrimaryColor": "Выберите основной цвет", + "about": "О приложении", + "sourceCode": "Исходный код", + "reportIssue": "Сообщить о проблеме", + "writeEmail": "Написать на почту", + "version": "Версия", + "versionNumber": "{version} ({buildNumber})", + "@versionNumber": { + "placeholders": { + "version": { + "type": "String" + }, + "buildNumber": { + "type": "String" + } + } + } +} \ No newline at end of file diff --git a/lib/providers/supported_locale_provider.dart b/lib/providers/supported_locale_provider.dart new file mode 100644 index 0000000..32ee912 --- /dev/null +++ b/lib/providers/supported_locale_provider.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/data/models/supported_locale.dart'; +import 'package:lightmeter/data/shared_prefs_service.dart'; +import 'package:lightmeter/generated/l10n.dart'; +import 'package:provider/provider.dart'; + +class SupportedLocaleProvider extends StatefulWidget { + final Widget child; + + const SupportedLocaleProvider({required this.child, super.key}); + + static SupportedLocaleProviderState of(BuildContext context) { + return context.findAncestorStateOfType()!; + } + + @override + State createState() => SupportedLocaleProviderState(); +} + +class SupportedLocaleProviderState extends State { + late final ValueNotifier valueListenable; + + @override + void initState() { + super.initState(); + valueListenable = ValueNotifier(context.read().locale); + } + + @override + void dispose() { + valueListenable.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: valueListenable, + builder: (_, value, child) => Provider.value( + value: value, + child: child, + ), + child: widget.child, + ); + } + + void setLocale(SupportedLocale locale) { + S.load(Locale(locale.intlName)).then((value) { + valueListenable.value = locale; + context.read().locale = locale; + }); + } +} diff --git a/lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/components/photography_value_picker_dialog/widget_dialog_picker_photography_value.dart b/lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/components/photography_value_picker_dialog/widget_dialog_picker_photography_value.dart index 0d30042..10f8b7f 100644 --- a/lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/components/photography_value_picker_dialog/widget_dialog_picker_photography_value.dart +++ b/lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/components/photography_value_picker_dialog/widget_dialog_picker_photography_value.dart @@ -57,11 +57,13 @@ class _PhotographyValuePickerDialogState Text( widget.title, style: Theme.of(context).textTheme.headlineSmall!, + textAlign: TextAlign.center, ), const SizedBox(height: Dimens.grid16), Text( widget.subtitle, style: Theme.of(context).textTheme.bodyMedium!, + textAlign: TextAlign.center, ), ], ), diff --git a/lib/screens/settings/components/language/widget_list_tile_language.dart b/lib/screens/settings/components/language/widget_list_tile_language.dart new file mode 100644 index 0000000..06c4e79 --- /dev/null +++ b/lib/screens/settings/components/language/widget_list_tile_language.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/data/models/supported_locale.dart'; +import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/providers/supported_locale_provider.dart'; +import 'package:lightmeter/screens/settings/components/shared/dialog_picker.dart/widget_dialog_picker.dart'; +import 'package:provider/provider.dart'; + +class LanguageListTile extends StatelessWidget { + const LanguageListTile({super.key}); + + @override + Widget build(BuildContext context) { + return ListTile( + leading: const Icon(Icons.language), + title: Text(S.of(context).language), + trailing: Text(context.watch().localizedName), + onTap: () { + showDialog( + context: context, + builder: (_) => DialogPicker( + title: S.of(context).chooseLanguage, + selectedValue: context.read(), + values: SupportedLocale.values, + titleAdapter: (context, value) => value.localizedName, + ), + ).then((value) { + if (value != null) { + SupportedLocaleProvider.of(context).setLocale(value); + } + }); + }, + ); + } +} diff --git a/lib/screens/settings/screen_settings.dart b/lib/screens/settings/screen_settings.dart index 402d393..a1f3242 100644 --- a/lib/screens/settings/screen_settings.dart +++ b/lib/screens/settings/screen_settings.dart @@ -7,6 +7,7 @@ import 'package:provider/provider.dart'; import 'components/caffeine/provider_list_tile_caffeine.dart'; import 'components/calibration/widget_list_tile_calibration.dart'; import 'components/haptics/provider_list_tile_haptics.dart'; +import 'components/language/widget_list_tile_language.dart'; import 'components/primary_color/widget_list_tile_primary_color.dart'; import 'components/report_issue/widget_list_tile_report_issue.dart'; import 'components/shared/settings_section/widget_settings_section.dart'; @@ -65,6 +66,7 @@ class SettingsScreen extends StatelessWidget { children: const [ CaffeineListTileProvider(), HapticsListTileProvider(), + LanguageListTile(), ], ), SettingsSection(