Merge branch 'main' of https://github.com/vodemn/m3_lightmeter into feature/ML-220

This commit is contained in:
Vadim 2025-05-05 18:09:35 +02:00
commit 383fbab7d9
24 changed files with 435 additions and 137 deletions

View file

@ -136,7 +136,7 @@ jobs:
- name: Build .ipa
run: |
flutter build ipa ${{ inputs.upload-artifact && '' || '--no-codesign' }} \
flutter build ipa \
--release \
--flavor $FLAVOR \
--target lib/main_$FLAVOR.dart \

View file

@ -38,41 +38,18 @@ To build this app you need to install Flutter 3.24.5 stable. [How to install](ht
### 2. Project setup
#### Restore _constants.dart_ file
#### Restore git-ignored files:
Create a file _lib/constants.dart_ and paste the following content:
```dart
const String contactEmail = '';
const String iapServerUrl = '';
const String issuesReportUrl = '';
const String sourceCodeUrl = '';
```
#### Stub IAP package
As part of the app's functionallity is in the private repo, you have to replace these lines in _pubspec.yaml_:
```yaml
m3_lightmeter_iap:
git:
url: "https://github.com/vodemn/m3_lightmeter_iap"
ref: main
```
with these:
```yaml
m3_lightmeter_iap:
path: iap
```
You can do it simply by running the script:
For macOS you can just run the following script:
```console
sh .github/scripts/stub_iap.sh
sh scripts/setup_fork.sh
```
Or create the files manually using the contents from the script.
#### Get dependencies
> If you are using VSCode, you can open the workspace like so: _File -> Open Workspace from File -> m3_lightmeter.code-workspace_. Otherwise you have to run `flutter pub get` command from the iap folder.
Then you can fetch all the neccessary dependencies and generate translation files by running the following commands:
@ -82,11 +59,7 @@ flutter pub get
flutter pub run intl_utils:generate
```
### 3. (Optional) Install Firebase
Out of the box Firebase Crashlytics won't work. If you want to add Crashlytics to your local build please follow [this guide](https://firebase.google.com/docs/flutter/setup).
### 4. Build
### 3. Build
- Checkout [Build .apk](.github/workflows/build_apk.yml) workflow for Android
- Checkout [Build .ipa](.github/workflows/build_ipa.yml) workflow for iOS

View file

@ -0,0 +1,3 @@
- Fixed histogram being affected by spot metering.
- Improved text fields focus handling.
- Added German translation.

View file

@ -109,17 +109,17 @@ void testE2E(String description) {
);
/// Add ND to shoot another scene
await tester.openPickerAndSelect<NdValuePicker, NdValue>('2');
await _expectMeteringStateAndMeasure(
tester,
equipmentProfile: mockEquipmentProfiles[0],
film: mockFilms[0],
fastest: 'f/1.8 - 1/200',
slowest: 'f/16 - 1/2.5',
iso: '400',
nd: '2',
ev: mockPhotoEv100 + 2 - 1,
);
// await tester.openPickerAndSelect<NdValuePicker, NdValue>('2');
// await _expectMeteringStateAndMeasure(
// tester,
// equipmentProfile: mockEquipmentProfiles[0],
// film: mockFilms[0],
// fastest: 'f/1.8 - 1/200',
// slowest: 'f/16 - 1/2.5',
// iso: '400',
// nd: '2',
// ev: mockPhotoEv100 + 2 - 1,
// );
/// Select another lens without ND
await tester.openPickerAndSelect<EquipmentProfilePicker, EquipmentProfile>(mockEquipmentProfiles[1].name);

View file

@ -1,10 +1,12 @@
enum SupportedLocale { en, fr, ru, zh }
enum SupportedLocale { de, en, fr, ru, zh }
extension SupportedLocaleExtension on SupportedLocale {
String get intlName => toString().replaceAll("SupportedLocale.", "");
String get localizedName {
switch (this) {
case SupportedLocale.de:
return 'Deutsch';
case SupportedLocale.en:
return 'English';
case SupportedLocale.fr:

165
lib/l10n/intl_de.arb Normal file
View file

@ -0,0 +1,165 @@
{
"@@locale": "de",
"fastestExposurePair": "Schnellstes",
"slowestExposurePair": "Langsamstes",
"ev": "EV",
"evValue": "{value} EV",
"@evValue": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"iso": "ISO",
"filmSpeed": "Film Empfindlichkeit",
"nd": "ND",
"ndFilterFactor": "Neutraldichtefilter-Faktor",
"noExposurePairs": "Es gibt keine Belichtungspaare für die ausgewählten Einstellungen.",
"noCamerasDetected": "Scheinbar sind keine Kameras an das Gerät angeschlossen.",
"noCameraPermission": "Kamera-Erlaubnis nicht erteilt.",
"otherCameraError": "Beim Verbinden der Kamera ist ein Fehler aufgetreten.",
"none": "Keiner",
"cancel": "Abbrechen",
"select": "Auswählen",
"save": "Speichern",
"settings": "Einstellungen",
"metering": "Messung",
"fractionalStops": "Zwischenstufen",
"showFractionalStops": "Zwischenstufen anzeigen",
"halfStops": "1/2",
"thirdStops": "1/3",
"calibration": "Kalibration",
"calibrationMessage": "Die Genauigkeit der Messungen sind vollständig von der Hardware des Geräts abhängig. Deshalb ist es empfehlenswert diese App zu testen und EV-Kalibrationswerte einzustellen, die korrekte Messungen produzieren.",
"calibrationMessageCameraOnly": "Die Genauigkeit der Messungen sind vollständig von der Kamera des Geräts abhängig. Deshalb ist es empfehlenswert diese App zu testen und EV-Kalibrationswerte einzustellen, die korrekte Messungen produzieren.",
"camera": "Kamera",
"lightSensor": "Lichtsensor",
"showEv100": "EV\u2081\u2080\u2080 anzeigen",
"meteringScreenLayout": "Messansicht Layout",
"meteringScreenLayoutHint": "Verstecke Elemente von der Messansicht, damit sie nicht den Platz für Belichtungspaare verschwenden.",
"meteringScreenLayoutHintEquipmentProfiles": "Ausrüstungsprofil Auswahl",
"meteringScreenFeatureExtremeExposurePairs": "Schnellste & langsamste Belichtungspaare",
"meteringScreenFeatureFilmPicker": "Film Auswahl",
"cameraFeatures": "Kamerafunktionen",
"cameraFeatureSpotMetering": "Punkt-Messung",
"cameraFeatureSpotMeteringHint": "Halte die Kameraansicht gedrückt um den Messpunkt zu entfernen",
"cameraFeatureHistogram": "Histogramm",
"cameraFeatureHistogramHint": "Verwendung des Histogramms kann den Batterieverbrauch erhöhen",
"film": "Film",
"filmPush": "Film (push)",
"filmPull": "Film (pull)",
"filmReciprocityHint": "Korrigiert Belichtungszeiten länger als 1 Sekunde",
"equipmentProfileName": "Ausrüstungsprofilname",
"equipmentProfileNameHint": "Praktica MTL5B",
"equipmentProfileAllValues": "Alle",
"apertureValues": "Blend-Werte",
"apertureValuesFilterDescription": "Wähle die anzuzeigenden Blend-Werte aus. Die Werte sind normalerweise von dem verwendeten Objektiv bestimmt.",
"ndFilters": "ND Filter",
"ndFiltersFilterDescription": "Wähle die anzuzeigenden ND Filter aus. (Beispielsweise die Meistverwendeten)",
"shutterSpeedValues": "Belichtungszeiten",
"shutterSpeedValuesFilterDescription": "Wähle die anzuzeigenden Belichtungszeiten aus. Die Werte sind normalerweise von der Kamera bestimmt.",
"shutterSpeedManualShort": "B",
"shutterSpeedManual": "Manuell",
"isoValues": "ISO Werte",
"isoValuesFilterDescription": "Wähle die anzuzeigenden ISO Werte aus. (Beispielsweise die Meistverwendeten)",
"lensZoom": "Objektiv-Zoom",
"lensZoomDescription": "Wähle den Zoom, relativ zur Handykamera, dass mit dem Sucher der Kamera übereinstimmt.",
"equipmentProfile": "Ausrüstungsprofil",
"equipmentProfiles": "Ausrüstungsprofile",
"tapToAdd": "Tippe zum Hinzufügen",
"general": "Allgemein",
"keepScreenOn": "Bildschirm anbehalten",
"haptics": "Haptik",
"autostartTimer": "Timer auto-starten",
"volumeKeysAction": "Auslösen durch Lautstärketasten",
"language": "Sprache",
"chooseLanguage": "Sprache auswählen",
"theme": "Theme",
"chooseTheme": "Theme auswählen",
"themeLight": "Hell",
"themeDark": "Dunkel",
"themeSystemDefault": "Systemeinstellung",
"dynamicColor": "Dynamische Farbe",
"primaryColor": "Akzentfarbe",
"choosePrimaryColor": "Akzentfarbe auswählen",
"about": "Info",
"restorePurchases": "Käufe wiederherstellen",
"sourceCode": "Source code",
"reportIssue": "Problem melden",
"writeEmail": "Email schreiben",
"youDontHaveMailApp": "Es ist keine Email App installiert.",
"copyEmail": "Email kopieren",
"version": "Version",
"versionNumber": "{version} ({buildNumber})",
"@versionNumber": {
"placeholders": {
"version": {
"type": "String"
},
"buildNumber": {
"type": "String"
}
}
},
"proFeaturesTitle": "Lightmeter Pro",
"getPro": "Pro kaufen",
"featuresFree": "Gratis",
"featuresPro": "Pro",
"proFeaturesPromoText": "Lightmeter Pro liefert alles, was Sie brauchen, um die besten Aufnahmen zu machen!",
"proFeaturesWhatsIncluded": "Was ist enthalten?",
"featureReflectedLightMetering": "Messung von reflektiertem Licht",
"featureIncidentLightMetering": "Messung von einfallendem Licht",
"featureIsoAndNdValues": "Große Auswahl von ISO und ND Filtern",
"featureTheme": "Theme Anpassung",
"featureSpotMetering": "Punktmessung",
"featureHistogram": "Histogramm",
"featureListOfFilms": "Liste von 20+ Filmen mit Reziprozitätsformeln",
"featureCustomFilms": "Eigene Filme erstellen",
"featureEquipmentProfiles": "Ausrüstungsprofile",
"featureTimer": "Eingebauter Timer für Langzeitbelichtungen",
"featureMeteringScreenLayout": "Anpassbare Messansicht",
"proFeaturesSupportText": "Durch den Kauf von Lightmeter Pro unterstützen Sie den Entwickler und ermöglichen das Hinzufügen weiterer Funktionen.",
"getNowFor": "Jetzt für {price} kaufen",
"@getNowFor": {
"price": {
"version": {
"type": "String"
}
}
},
"tooltipAdd": "Hinzufügen",
"tooltipClose": "Schließen",
"tooltipExpand": "Erweitern",
"tooltipCollapse": "Schrumpfen",
"tooltipCopy": "Kopieren",
"tooltipDelete": "Löschen",
"tooltipSelectAll": "Alle auswählen",
"tooltipDesecelectAll": "Keine auswählen",
"tooltipResetToZero": "Auf null zurücksetzen",
"tooltipUseLightSensor": "Lichtsensor verwenden",
"tooltipUseCamera": "Kamera verwenden",
"tooltipOpenSettings": "Einstellungen öffnen",
"exposurePair": "Belichtungspaar",
"whatsnew": "Was ist neu?",
"changesInVersion": "Änderungen in Version {version}:",
"@changesInVersion": {
"placeholders": {
"version": {
"type": "String"
}
}
},
"close": "Schließen",
"films": "Filme",
"filmsInUse": "Verwendete Filme",
"filmsCustom": "Eigene Filme",
"addFilmTitle": "Film hinzufügen",
"editFilmTitle": "Film bearbeiten",
"filmFormula": "Formel",
"filmFormulaExponential": "T=t^Rf",
"filmFormulaExponentialRf": "Rf",
"filmFormulaExponentialRfPlaceholder": "1.3",
"name": "Name",
"addEquipmentProfileTitle": "Ausrüstung hinzufügen",
"editEquipmentProfileTitle": "Ausrüstung bearbeiten"
}

View file

@ -137,6 +137,7 @@ class _NameFieldBuilder extends StatelessWidget {
bottom: Dimens.paddingS / 2,
),
child: LightmeterTextField(
autofocus: true,
initialValue: state.name,
maxLength: 48,
hintText: S.of(context).name,

View file

@ -8,6 +8,7 @@ class MeteringCommunicationBloc extends Bloc<MeteringCommunicationEvent, Meterin
// `MeasureState` is not const, so that `Bloc` treats each state as new and updates state stream
// ignore: prefer_const_constructors
on<MeasureEvent>((_, emit) => emit(MeasureState()));
on<EquipmentProfileChangedEvent>((event, emit) => emit(EquipmentProfileChangedState(event.profile)));
on<MeteringInProgressEvent>((event, emit) => emit(MeteringInProgressState(event.ev100)));
on<MeteringEndedEvent>((event, emit) => emit(MeteringEndedState(event.ev100)));
on<ScreenOnTopOpenedEvent>((_, emit) => emit(const SettingsOpenedState()));

View file

@ -1,19 +1,29 @@
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
abstract class MeteringCommunicationEvent {
const MeteringCommunicationEvent();
}
abstract class SourceEvent extends MeteringCommunicationEvent {
const SourceEvent();
}
/// Events sent by the screen to the current metering source.
abstract class ScreenEvent extends MeteringCommunicationEvent {
const ScreenEvent();
}
/// Event sent by the current metering source in response for the screen events.
abstract class SourceEvent extends MeteringCommunicationEvent {
const SourceEvent();
}
class MeasureEvent extends ScreenEvent {
const MeasureEvent();
}
class EquipmentProfileChangedEvent extends ScreenEvent {
final EquipmentProfile profile;
const EquipmentProfileChangedEvent(this.profile);
}
abstract class MeasuredEvent extends SourceEvent {
final double? ev100;

View file

@ -1,3 +1,5 @@
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
sealed class MeteringCommunicationState {
const MeteringCommunicationState();
}
@ -18,6 +20,12 @@ class MeasureState extends SourceState {
const MeasureState();
}
class EquipmentProfileChangedState extends SourceState {
final EquipmentProfile profile;
const EquipmentProfileChangedState(this.profile);
}
sealed class MeasuredState extends ScreenState {
final double? ev100;

View file

@ -27,11 +27,12 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
final LightmeterAnalytics _analytics;
late final _WidgetsBindingObserver _observer;
CameraDescription? _camera;
CameraController? _cameraController;
static const _maxZoom = 7.0;
RangeValues? _zoomRange;
double _currentZoom = 0.0;
double _currentZoom = 1.0;
static const _exposureMaxRange = RangeValues(-4, 4);
RangeValues? _exposureOffsetRange;
@ -86,6 +87,12 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
}
});
}
case final communication_states.EquipmentProfileChangedState communicationState:
if (state is CameraActiveState) {
add(ZoomChangedEvent(communicationState.profile.lensZoom));
} else {
_currentZoom = communicationState.profile.lensZoom;
}
case communication_states.SettingsOpenedState():
_settingsOpened = true;
add(const DeinitializeEvent());
@ -117,46 +124,66 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
}
try {
final cameras = await availableCameras();
if (cameras.isEmpty) {
emit(const CameraErrorState(CameraErrorType.noCamerasDetected));
return;
if (_camera == null) {
final cameras = await availableCameras();
if (cameras.isEmpty) {
emit(const CameraErrorState(CameraErrorType.noCamerasDetected));
return;
} else {
_camera = cameras.firstWhere(
(camera) => camera.lensDirection == CameraLensDirection.back,
orElse: () => cameras.last,
);
}
}
_cameraController = CameraController(
cameras.firstWhere(
(camera) => camera.lensDirection == CameraLensDirection.back,
orElse: () => cameras.last,
),
final cameraController = CameraController(
_camera!,
ResolutionPreset.low,
enableAudio: false,
);
await cameraController.initialize();
await cameraController.setFlashMode(FlashMode.off);
await cameraController.lockCaptureOrientation(DeviceOrientation.portraitUp);
await _cameraController!.initialize();
await _cameraController!.setFlashMode(FlashMode.off);
await _cameraController!.lockCaptureOrientation(DeviceOrientation.portraitUp);
if (_exposureOffsetRange == null) {
await Future.wait<double>([
cameraController.getMinExposureOffset(),
cameraController.getMaxExposureOffset(),
cameraController.getExposureOffsetStepSize(),
]).then((value) {
_exposureOffsetRange = RangeValues(
math.max(_exposureMaxRange.start, value[0]),
math.min(_exposureMaxRange.end, value[1]),
);
_currentExposureOffset = 0.0;
_exposureStep = value[2] == 0 ? 0.1 : value[2];
});
}
_zoomRange = await Future.wait<double>([
_cameraController!.getMinZoomLevel(),
_cameraController!.getMaxZoomLevel(),
]).then((levels) => RangeValues(math.max(1.0, levels[0]), math.min(_maxZoom, levels[1])));
_currentZoom = _zoomRange!.start;
if (_zoomRange == null) {
await Future.wait<double>([
cameraController.getMinZoomLevel(),
cameraController.getMaxZoomLevel(),
]).then((value) {
_zoomRange = RangeValues(
math.max(1.0, value[0]),
math.min(_maxZoom, value[1]),
);
if (_currentZoom < _zoomRange!.start || _currentZoom > _zoomRange!.end) {
_currentZoom = _zoomRange!.start;
}
});
}
_exposureOffsetRange = await Future.wait<double>([
_cameraController!.getMinExposureOffset(),
_cameraController!.getMaxExposureOffset(),
]).then(
(levels) => RangeValues(
math.max(_exposureMaxRange.start, levels[0]),
math.min(_exposureMaxRange.end, levels[1]),
),
);
await _cameraController!.getExposureOffsetStepSize().then((value) {
_exposureStep = value == 0 ? 0.1 : value;
});
_currentExposureOffset = 0.0;
emit(CameraInitializedState(_cameraController!));
/// For app startup initialization this effectively isn't executed.
await Future.wait<void>([
if (_currentZoom != 1.0) cameraController.setZoomLevel(_currentZoom),
if (_currentExposureOffset != 0.0) cameraController.setExposureOffset(_currentExposureOffset),
]);
_cameraController = cameraController;
emit(CameraInitializedState(cameraController));
_emitActiveState(emit);
} catch (e, stackTrace) {
_analytics.logCrash(e, stackTrace);
@ -171,7 +198,7 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
}
Future<void> _onZoomChanged(ZoomChangedEvent event, Emitter emit) async {
if (_cameraController != null) {
if (_cameraController != null && _zoomRange != null) {
final double zoom = event.value.clamp(_zoomRange!.start, _zoomRange!.end);
_cameraController!.setZoomLevel(zoom);
_currentZoom = zoom;

View file

@ -3,7 +3,7 @@ import 'package:lightmeter/providers/equipment_profile_provider.dart';
import 'package:lightmeter/screens/shared/ruler_slider/widget_slider_ruler.dart';
import 'package:lightmeter/utils/double_to_zoom.dart';
class ZoomSlider extends StatefulWidget {
class ZoomSlider extends StatelessWidget {
final RangeValues range;
final double value;
final ValueChanged<double> onChanged;
@ -15,23 +15,12 @@ class ZoomSlider extends StatefulWidget {
super.key,
});
@override
State<ZoomSlider> createState() => _ZoomSliderState();
}
class _ZoomSliderState extends State<ZoomSlider> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
widget.onChanged(EquipmentProfiles.selectedOf(context).lensZoom);
}
@override
Widget build(BuildContext context) {
return RulerSlider(
range: widget.range,
value: widget.value,
onChanged: widget.onChanged,
range: range,
value: value,
onChanged: onChanged,
icon: Icons.search_outlined,
defaultValue: EquipmentProfiles.selectedOf(context).lensZoom,
rulerValueAdapter: (value) => value.toStringAsFixed(0),

View file

@ -132,7 +132,7 @@ class _ProFeaturesOverlay extends StatelessWidget {
);
final bool hasSpotMetering = UserPreferencesProvider.cameraFeatureOf(
context,
CameraFeature.histogram,
CameraFeature.spotMetering,
);
return Stack(
alignment: Alignment.bottomCenter,

View file

@ -1,13 +1,28 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/providers/equipment_profile_provider.dart';
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/widget_picker_dialog_animated.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/reading_value_container/widget_container_reading_value.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class EquipmentProfilePicker extends StatelessWidget {
class EquipmentProfilePicker extends StatefulWidget {
const EquipmentProfilePicker();
@override
State<EquipmentProfilePicker> createState() => _EquipmentProfilePickerState();
}
class _EquipmentProfilePickerState extends State<EquipmentProfilePicker> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
final profile = EquipmentProfiles.selectedOf(context);
context.read<MeteringCommunicationBloc>().add(EquipmentProfileChangedEvent(profile));
}
@override
Widget build(BuildContext context) {
return AnimatedDialogPicker<EquipmentProfile>(
@ -16,7 +31,10 @@ class EquipmentProfilePicker extends StatelessWidget {
selectedValue: EquipmentProfiles.selectedOf(context),
values: EquipmentProfiles.inUseOf(context),
itemTitleBuilder: (_, value) => Text(value.id.isEmpty ? S.of(context).none : value.name),
onChanged: EquipmentProfilesProvider.of(context).selectProfile,
onChanged: (profile) {
EquipmentProfilesProvider.of(context).selectProfile(profile);
context.read<MeteringCommunicationBloc>().add(EquipmentProfileChangedEvent(profile));
},
closedChild: ReadingValueContainer.singleValue(
value: ReadingValue(
label: S.of(context).equipmentProfile,

View file

@ -1,33 +1,74 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class LightmeterTextField extends TextFormField {
LightmeterTextField({
super.controller,
super.autofocus,
super.initialValue,
super.inputFormatters,
super.maxLength,
super.onChanged,
super.style,
super.textAlign,
Widget? leading,
String? hintText,
}) : super(
autovalidateMode: AutovalidateMode.onUserInteraction,
maxLines: 1,
decoration: InputDecoration(
counter: const SizedBox(),
contentPadding: EdgeInsets.zero,
errorStyle: const TextStyle(fontSize: 0),
icon: leading,
hintText: hintText,
),
validator: (value) {
if (value == null || value.isEmpty) {
return '';
} else {
return null;
}
},
);
class LightmeterTextField extends StatefulWidget {
const LightmeterTextField({
this.autofocus = false,
this.controller,
this.hintText,
this.initialValue,
this.inputFormatters,
this.leading,
this.maxLength,
this.onChanged,
this.style,
this.textAlign = TextAlign.start,
});
final bool autofocus;
final TextEditingController? controller;
final String? hintText;
final String? initialValue;
final List<TextInputFormatter>? inputFormatters;
final Widget? leading;
final int? maxLength;
final void Function(String)? onChanged;
final TextStyle? style;
final TextAlign textAlign;
@override
State<LightmeterTextField> createState() => _LightmeterTextFieldState();
}
class _LightmeterTextFieldState extends State<LightmeterTextField> {
late final focusNode = FocusNode(debugLabel: widget.hintText);
@override
Widget build(BuildContext context) {
return TextFormField(
autofocus: widget.autofocus,
autovalidateMode: AutovalidateMode.onUserInteraction,
controller: widget.controller,
focusNode: focusNode,
initialValue: widget.initialValue,
inputFormatters: widget.inputFormatters,
maxLength: widget.maxLength,
onChanged: widget.onChanged,
style: widget.style,
textAlign: widget.textAlign,
decoration: InputDecoration(
counter: const SizedBox(),
contentPadding: EdgeInsets.zero,
errorStyle: const TextStyle(fontSize: 0),
icon: widget.leading,
hintText: widget.hintText,
),
onTapOutside: (event) {
focusNode.unfocus();
},
validator: (value) {
if (value == null || value.isEmpty) {
return '';
} else {
return null;
}
},
);
}
@override
void dispose() {
focusNode.dispose();
super.dispose();
}
}

View file

@ -1,7 +1,7 @@
name: lightmeter
description: Lightmeter app inspired by Material 3 design system.
publish_to: "none"
version: 1.0.3+58
version: 1.0.4+59
environment:
sdk: ">=3.0.0 <4.0.0"

View file

@ -0,0 +1,4 @@
const String contactEmail = '';
const String iapServerUrl = '';
const String issuesReportUrl = 'https://github.com/vodemn/m3_lightmeter/issues/new/choose';
const String sourceCodeUrl = 'https://github.com/vodemn/m3_lightmeter/';

View file

@ -0,0 +1,30 @@
{
"flutter": {
"platforms": {
"android": {
"default": {
"projectId": "mockproject-1234",
"appId": "1:123456789000:android:f1bf012572b04063",
"fileOutput": "android/app/google-services.json"
}
},
"ios": {
"default": {
"projectId": "mockproject-1234",
"appId": "1:123456789000:ios:f1bf012572b04063",
"uploadDebugSymbols": true,
"fileOutput": "ios/Runner/GoogleService-Info.plist"
}
},
"dart": {
"lib/firebase_options.dart": {
"projectId": "mockproject-1234",
"configurations": {
"android": "1:123456789000:android:f1bf012572b04063",
"ios": "1:123456789000:ios:f1bf012572b04063"
}
}
}
}
}
}

View file

@ -0,0 +1,7 @@
// ignore_for_file: type=lint
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform =>
FirebaseOptions(apiKey: '', appId: '', messagingSenderId: '', projectId: '');
}

12
scripts/setup_fork.sh Normal file
View file

@ -0,0 +1,12 @@
cp "scripts/mocks/mock_constants.dart" "lib/constants.dart"
cp "scripts/mocks/mock_firebase_options.dart" "lib/firebase_options.dart"
cp "scripts/mocks/mock_firebase.json" "firebase.json"
curl -H 'Accept: application/vnd.github.v3.raw' \
-o "android/app/google-services.json" \
-L "https://api.github.com/repos/firebase/quickstart-android/contents/mock-google-services.json"
curl -H 'Accept: application/vnd.github.v3.raw' \
-o "ios/Runner/GoogleService-Info.plist" \
-L "https://api.github.com/repos/firebase/quickstart-ios/contents/mock-GoogleService-Info.plist"
sh .github/scripts/stub_iap.sh

View file

@ -7,6 +7,7 @@ void main() {
expect(SupportedLocale.fr.intlName, 'fr');
expect(SupportedLocale.ru.intlName, 'ru');
expect(SupportedLocale.zh.intlName, 'zh');
expect(SupportedLocale.de.intlName, 'de');
});
test('localizedName', () {
@ -14,5 +15,6 @@ void main() {
expect(SupportedLocale.fr.localizedName, 'Français');
expect(SupportedLocale.ru.localizedName, 'Русский');
expect(SupportedLocale.zh.localizedName, '简体中文');
expect(SupportedLocale.de.localizedName, 'Deutsch');
});
}

View file

@ -1,7 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/providers/equipment_profile_provider.dart';
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/equipment_profile_picker/widget_picker_equipment_profiles.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
@ -33,8 +35,11 @@ void main() {
],
child: EquipmentProfilesProvider(
storageService: storageService,
child: const WidgetTestApplicationMock(
child: Row(children: [Expanded(child: EquipmentProfilePicker())]),
child: WidgetTestApplicationMock(
child: BlocProvider(
create: (_) => MeteringCommunicationBloc(),
child: const Row(children: [Expanded(child: EquipmentProfilePicker())]),
),
),
),
),