mirror of
https://github.com/vodemn/m3_lightmeter.git
synced 2025-07-06 22:20:42 +00:00
Compare commits
No commits in common. "383fbab7d9ae4d82c13456ded2eb90d6d98f04d3" and "c1a7ad0fa8307cc5c8545227edd7c819083b7595" have entirely different histories.
383fbab7d9
...
c1a7ad0fa8
24 changed files with 136 additions and 434 deletions
2
.github/workflows/build_ipa.yml
vendored
2
.github/workflows/build_ipa.yml
vendored
|
@ -136,7 +136,7 @@ jobs:
|
||||||
|
|
||||||
- name: Build .ipa
|
- name: Build .ipa
|
||||||
run: |
|
run: |
|
||||||
flutter build ipa \
|
flutter build ipa ${{ inputs.upload-artifact && '' || '--no-codesign' }} \
|
||||||
--release \
|
--release \
|
||||||
--flavor $FLAVOR \
|
--flavor $FLAVOR \
|
||||||
--target lib/main_$FLAVOR.dart \
|
--target lib/main_$FLAVOR.dart \
|
||||||
|
|
41
README.md
41
README.md
|
@ -38,17 +38,40 @@ To build this app you need to install Flutter 3.24.5 stable. [How to install](ht
|
||||||
|
|
||||||
### 2. Project setup
|
### 2. Project setup
|
||||||
|
|
||||||
#### Restore git-ignored files:
|
#### Restore _constants.dart_ file
|
||||||
|
|
||||||
For macOS you can just run the following script:
|
Create a file _lib/constants.dart_ and paste the following content:
|
||||||
|
|
||||||
```console
|
```dart
|
||||||
sh scripts/setup_fork.sh
|
const String contactEmail = '';
|
||||||
|
const String iapServerUrl = '';
|
||||||
|
const String issuesReportUrl = '';
|
||||||
|
const String sourceCodeUrl = '';
|
||||||
```
|
```
|
||||||
|
|
||||||
Or create the files manually using the contents from the script.
|
#### Stub IAP package
|
||||||
|
|
||||||
#### Get dependencies
|
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:
|
||||||
|
|
||||||
|
```console
|
||||||
|
sh .github/scripts/stub_iap.sh
|
||||||
|
```
|
||||||
|
|
||||||
> 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.
|
> 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.
|
||||||
|
|
||||||
|
@ -59,7 +82,11 @@ flutter pub get
|
||||||
flutter pub run intl_utils:generate
|
flutter pub run intl_utils:generate
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Build
|
### 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
|
||||||
|
|
||||||
- Checkout [Build .apk](.github/workflows/build_apk.yml) workflow for Android
|
- Checkout [Build .apk](.github/workflows/build_apk.yml) workflow for Android
|
||||||
- Checkout [Build .ipa](.github/workflows/build_ipa.yml) workflow for iOS
|
- Checkout [Build .ipa](.github/workflows/build_ipa.yml) workflow for iOS
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
- Fixed histogram being affected by spot metering.
|
|
||||||
- Improved text fields focus handling.
|
|
||||||
- Added German translation.
|
|
|
@ -109,17 +109,17 @@ void testE2E(String description) {
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Add ND to shoot another scene
|
/// Add ND to shoot another scene
|
||||||
// await tester.openPickerAndSelect<NdValuePicker, NdValue>('2');
|
await tester.openPickerAndSelect<NdValuePicker, NdValue>('2');
|
||||||
// await _expectMeteringStateAndMeasure(
|
await _expectMeteringStateAndMeasure(
|
||||||
// tester,
|
tester,
|
||||||
// equipmentProfile: mockEquipmentProfiles[0],
|
equipmentProfile: mockEquipmentProfiles[0],
|
||||||
// film: mockFilms[0],
|
film: mockFilms[0],
|
||||||
// fastest: 'f/1.8 - 1/200',
|
fastest: 'f/1.8 - 1/200',
|
||||||
// slowest: 'f/16 - 1/2.5',
|
slowest: 'f/16 - 1/2.5',
|
||||||
// iso: '400',
|
iso: '400',
|
||||||
// nd: '2',
|
nd: '2',
|
||||||
// ev: mockPhotoEv100 + 2 - 1,
|
ev: mockPhotoEv100 + 2 - 1,
|
||||||
// );
|
);
|
||||||
|
|
||||||
/// Select another lens without ND
|
/// Select another lens without ND
|
||||||
await tester.openPickerAndSelect<EquipmentProfilePicker, EquipmentProfile>(mockEquipmentProfiles[1].name);
|
await tester.openPickerAndSelect<EquipmentProfilePicker, EquipmentProfile>(mockEquipmentProfiles[1].name);
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
enum SupportedLocale { de, en, fr, ru, zh }
|
enum SupportedLocale { en, fr, ru, zh }
|
||||||
|
|
||||||
extension SupportedLocaleExtension on SupportedLocale {
|
extension SupportedLocaleExtension on SupportedLocale {
|
||||||
String get intlName => toString().replaceAll("SupportedLocale.", "");
|
String get intlName => toString().replaceAll("SupportedLocale.", "");
|
||||||
|
|
||||||
String get localizedName {
|
String get localizedName {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
case SupportedLocale.de:
|
|
||||||
return 'Deutsch';
|
|
||||||
case SupportedLocale.en:
|
case SupportedLocale.en:
|
||||||
return 'English';
|
return 'English';
|
||||||
case SupportedLocale.fr:
|
case SupportedLocale.fr:
|
||||||
|
|
|
@ -1,165 +0,0 @@
|
||||||
{
|
|
||||||
"@@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"
|
|
||||||
}
|
|
|
@ -137,7 +137,6 @@ class _NameFieldBuilder extends StatelessWidget {
|
||||||
bottom: Dimens.paddingS / 2,
|
bottom: Dimens.paddingS / 2,
|
||||||
),
|
),
|
||||||
child: LightmeterTextField(
|
child: LightmeterTextField(
|
||||||
autofocus: true,
|
|
||||||
initialValue: state.name,
|
initialValue: state.name,
|
||||||
maxLength: 48,
|
maxLength: 48,
|
||||||
hintText: S.of(context).name,
|
hintText: S.of(context).name,
|
||||||
|
|
|
@ -8,7 +8,6 @@ class MeteringCommunicationBloc extends Bloc<MeteringCommunicationEvent, Meterin
|
||||||
// `MeasureState` is not const, so that `Bloc` treats each state as new and updates state stream
|
// `MeasureState` is not const, so that `Bloc` treats each state as new and updates state stream
|
||||||
// ignore: prefer_const_constructors
|
// ignore: prefer_const_constructors
|
||||||
on<MeasureEvent>((_, emit) => emit(MeasureState()));
|
on<MeasureEvent>((_, emit) => emit(MeasureState()));
|
||||||
on<EquipmentProfileChangedEvent>((event, emit) => emit(EquipmentProfileChangedState(event.profile)));
|
|
||||||
on<MeteringInProgressEvent>((event, emit) => emit(MeteringInProgressState(event.ev100)));
|
on<MeteringInProgressEvent>((event, emit) => emit(MeteringInProgressState(event.ev100)));
|
||||||
on<MeteringEndedEvent>((event, emit) => emit(MeteringEndedState(event.ev100)));
|
on<MeteringEndedEvent>((event, emit) => emit(MeteringEndedState(event.ev100)));
|
||||||
on<ScreenOnTopOpenedEvent>((_, emit) => emit(const SettingsOpenedState()));
|
on<ScreenOnTopOpenedEvent>((_, emit) => emit(const SettingsOpenedState()));
|
||||||
|
|
|
@ -1,27 +1,17 @@
|
||||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
|
||||||
|
|
||||||
abstract class MeteringCommunicationEvent {
|
abstract class MeteringCommunicationEvent {
|
||||||
const MeteringCommunicationEvent();
|
const MeteringCommunicationEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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 {
|
abstract class SourceEvent extends MeteringCommunicationEvent {
|
||||||
const SourceEvent();
|
const SourceEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
class MeasureEvent extends ScreenEvent {
|
abstract class ScreenEvent extends MeteringCommunicationEvent {
|
||||||
const MeasureEvent();
|
const ScreenEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
class EquipmentProfileChangedEvent extends ScreenEvent {
|
class MeasureEvent extends ScreenEvent {
|
||||||
final EquipmentProfile profile;
|
const MeasureEvent();
|
||||||
|
|
||||||
const EquipmentProfileChangedEvent(this.profile);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class MeasuredEvent extends SourceEvent {
|
abstract class MeasuredEvent extends SourceEvent {
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
|
||||||
|
|
||||||
sealed class MeteringCommunicationState {
|
sealed class MeteringCommunicationState {
|
||||||
const MeteringCommunicationState();
|
const MeteringCommunicationState();
|
||||||
}
|
}
|
||||||
|
@ -20,12 +18,6 @@ class MeasureState extends SourceState {
|
||||||
const MeasureState();
|
const MeasureState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class EquipmentProfileChangedState extends SourceState {
|
|
||||||
final EquipmentProfile profile;
|
|
||||||
|
|
||||||
const EquipmentProfileChangedState(this.profile);
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class MeasuredState extends ScreenState {
|
sealed class MeasuredState extends ScreenState {
|
||||||
final double? ev100;
|
final double? ev100;
|
||||||
|
|
||||||
|
|
|
@ -27,12 +27,11 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
|
||||||
final LightmeterAnalytics _analytics;
|
final LightmeterAnalytics _analytics;
|
||||||
late final _WidgetsBindingObserver _observer;
|
late final _WidgetsBindingObserver _observer;
|
||||||
|
|
||||||
CameraDescription? _camera;
|
|
||||||
CameraController? _cameraController;
|
CameraController? _cameraController;
|
||||||
|
|
||||||
static const _maxZoom = 7.0;
|
static const _maxZoom = 7.0;
|
||||||
RangeValues? _zoomRange;
|
RangeValues? _zoomRange;
|
||||||
double _currentZoom = 1.0;
|
double _currentZoom = 0.0;
|
||||||
|
|
||||||
static const _exposureMaxRange = RangeValues(-4, 4);
|
static const _exposureMaxRange = RangeValues(-4, 4);
|
||||||
RangeValues? _exposureOffsetRange;
|
RangeValues? _exposureOffsetRange;
|
||||||
|
@ -87,12 +86,6 @@ 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():
|
case communication_states.SettingsOpenedState():
|
||||||
_settingsOpened = true;
|
_settingsOpened = true;
|
||||||
add(const DeinitializeEvent());
|
add(const DeinitializeEvent());
|
||||||
|
@ -124,66 +117,46 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (_camera == null) {
|
|
||||||
final cameras = await availableCameras();
|
final cameras = await availableCameras();
|
||||||
if (cameras.isEmpty) {
|
if (cameras.isEmpty) {
|
||||||
emit(const CameraErrorState(CameraErrorType.noCamerasDetected));
|
emit(const CameraErrorState(CameraErrorType.noCamerasDetected));
|
||||||
return;
|
return;
|
||||||
} else {
|
}
|
||||||
_camera = cameras.firstWhere(
|
_cameraController = CameraController(
|
||||||
|
cameras.firstWhere(
|
||||||
(camera) => camera.lensDirection == CameraLensDirection.back,
|
(camera) => camera.lensDirection == CameraLensDirection.back,
|
||||||
orElse: () => cameras.last,
|
orElse: () => cameras.last,
|
||||||
);
|
),
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final cameraController = CameraController(
|
|
||||||
_camera!,
|
|
||||||
ResolutionPreset.low,
|
ResolutionPreset.low,
|
||||||
enableAudio: false,
|
enableAudio: false,
|
||||||
);
|
);
|
||||||
await cameraController.initialize();
|
|
||||||
await cameraController.setFlashMode(FlashMode.off);
|
|
||||||
await cameraController.lockCaptureOrientation(DeviceOrientation.portraitUp);
|
|
||||||
|
|
||||||
if (_exposureOffsetRange == null) {
|
await _cameraController!.initialize();
|
||||||
await Future.wait<double>([
|
await _cameraController!.setFlashMode(FlashMode.off);
|
||||||
cameraController.getMinExposureOffset(),
|
await _cameraController!.lockCaptureOrientation(DeviceOrientation.portraitUp);
|
||||||
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];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_zoomRange == null) {
|
_zoomRange = await Future.wait<double>([
|
||||||
await Future.wait<double>([
|
_cameraController!.getMinZoomLevel(),
|
||||||
cameraController.getMinZoomLevel(),
|
_cameraController!.getMaxZoomLevel(),
|
||||||
cameraController.getMaxZoomLevel(),
|
]).then((levels) => RangeValues(math.max(1.0, levels[0]), math.min(_maxZoom, levels[1])));
|
||||||
]).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;
|
_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;
|
||||||
|
|
||||||
/// For app startup initialization this effectively isn't executed.
|
emit(CameraInitializedState(_cameraController!));
|
||||||
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);
|
_emitActiveState(emit);
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
_analytics.logCrash(e, stackTrace);
|
_analytics.logCrash(e, stackTrace);
|
||||||
|
@ -198,7 +171,7 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onZoomChanged(ZoomChangedEvent event, Emitter emit) async {
|
Future<void> _onZoomChanged(ZoomChangedEvent event, Emitter emit) async {
|
||||||
if (_cameraController != null && _zoomRange != null) {
|
if (_cameraController != null) {
|
||||||
final double zoom = event.value.clamp(_zoomRange!.start, _zoomRange!.end);
|
final double zoom = event.value.clamp(_zoomRange!.start, _zoomRange!.end);
|
||||||
_cameraController!.setZoomLevel(zoom);
|
_cameraController!.setZoomLevel(zoom);
|
||||||
_currentZoom = zoom;
|
_currentZoom = zoom;
|
||||||
|
|
|
@ -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/screens/shared/ruler_slider/widget_slider_ruler.dart';
|
||||||
import 'package:lightmeter/utils/double_to_zoom.dart';
|
import 'package:lightmeter/utils/double_to_zoom.dart';
|
||||||
|
|
||||||
class ZoomSlider extends StatelessWidget {
|
class ZoomSlider extends StatefulWidget {
|
||||||
final RangeValues range;
|
final RangeValues range;
|
||||||
final double value;
|
final double value;
|
||||||
final ValueChanged<double> onChanged;
|
final ValueChanged<double> onChanged;
|
||||||
|
@ -15,12 +15,23 @@ class ZoomSlider extends StatelessWidget {
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ZoomSlider> createState() => _ZoomSliderState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ZoomSliderState extends State<ZoomSlider> {
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
widget.onChanged(EquipmentProfiles.selectedOf(context).lensZoom);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return RulerSlider(
|
return RulerSlider(
|
||||||
range: range,
|
range: widget.range,
|
||||||
value: value,
|
value: widget.value,
|
||||||
onChanged: onChanged,
|
onChanged: widget.onChanged,
|
||||||
icon: Icons.search_outlined,
|
icon: Icons.search_outlined,
|
||||||
defaultValue: EquipmentProfiles.selectedOf(context).lensZoom,
|
defaultValue: EquipmentProfiles.selectedOf(context).lensZoom,
|
||||||
rulerValueAdapter: (value) => value.toStringAsFixed(0),
|
rulerValueAdapter: (value) => value.toStringAsFixed(0),
|
||||||
|
|
|
@ -132,7 +132,7 @@ class _ProFeaturesOverlay extends StatelessWidget {
|
||||||
);
|
);
|
||||||
final bool hasSpotMetering = UserPreferencesProvider.cameraFeatureOf(
|
final bool hasSpotMetering = UserPreferencesProvider.cameraFeatureOf(
|
||||||
context,
|
context,
|
||||||
CameraFeature.spotMetering,
|
CameraFeature.histogram,
|
||||||
);
|
);
|
||||||
return Stack(
|
return Stack(
|
||||||
alignment: Alignment.bottomCenter,
|
alignment: Alignment.bottomCenter,
|
||||||
|
|
|
@ -1,28 +1,13 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:lightmeter/generated/l10n.dart';
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
import 'package:lightmeter/providers/equipment_profile_provider.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/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: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';
|
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||||
|
|
||||||
class EquipmentProfilePicker extends StatefulWidget {
|
class EquipmentProfilePicker extends StatelessWidget {
|
||||||
const EquipmentProfilePicker();
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AnimatedDialogPicker<EquipmentProfile>(
|
return AnimatedDialogPicker<EquipmentProfile>(
|
||||||
|
@ -31,10 +16,7 @@ class _EquipmentProfilePickerState extends State<EquipmentProfilePicker> {
|
||||||
selectedValue: EquipmentProfiles.selectedOf(context),
|
selectedValue: EquipmentProfiles.selectedOf(context),
|
||||||
values: EquipmentProfiles.inUseOf(context),
|
values: EquipmentProfiles.inUseOf(context),
|
||||||
itemTitleBuilder: (_, value) => Text(value.id.isEmpty ? S.of(context).none : value.name),
|
itemTitleBuilder: (_, value) => Text(value.id.isEmpty ? S.of(context).none : value.name),
|
||||||
onChanged: (profile) {
|
onChanged: EquipmentProfilesProvider.of(context).selectProfile,
|
||||||
EquipmentProfilesProvider.of(context).selectProfile(profile);
|
|
||||||
context.read<MeteringCommunicationBloc>().add(EquipmentProfileChangedEvent(profile));
|
|
||||||
},
|
|
||||||
closedChild: ReadingValueContainer.singleValue(
|
closedChild: ReadingValueContainer.singleValue(
|
||||||
value: ReadingValue(
|
value: ReadingValue(
|
||||||
label: S.of(context).equipmentProfile,
|
label: S.of(context).equipmentProfile,
|
||||||
|
|
|
@ -1,61 +1,27 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
|
|
||||||
class LightmeterTextField extends StatefulWidget {
|
class LightmeterTextField extends TextFormField {
|
||||||
const LightmeterTextField({
|
LightmeterTextField({
|
||||||
this.autofocus = false,
|
super.controller,
|
||||||
this.controller,
|
super.autofocus,
|
||||||
this.hintText,
|
super.initialValue,
|
||||||
this.initialValue,
|
super.inputFormatters,
|
||||||
this.inputFormatters,
|
super.maxLength,
|
||||||
this.leading,
|
super.onChanged,
|
||||||
this.maxLength,
|
super.style,
|
||||||
this.onChanged,
|
super.textAlign,
|
||||||
this.style,
|
Widget? leading,
|
||||||
this.textAlign = TextAlign.start,
|
String? hintText,
|
||||||
});
|
}) : super(
|
||||||
|
|
||||||
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,
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
controller: widget.controller,
|
maxLines: 1,
|
||||||
focusNode: focusNode,
|
|
||||||
initialValue: widget.initialValue,
|
|
||||||
inputFormatters: widget.inputFormatters,
|
|
||||||
maxLength: widget.maxLength,
|
|
||||||
onChanged: widget.onChanged,
|
|
||||||
style: widget.style,
|
|
||||||
textAlign: widget.textAlign,
|
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
counter: const SizedBox(),
|
counter: const SizedBox(),
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
errorStyle: const TextStyle(fontSize: 0),
|
errorStyle: const TextStyle(fontSize: 0),
|
||||||
icon: widget.leading,
|
icon: leading,
|
||||||
hintText: widget.hintText,
|
hintText: hintText,
|
||||||
),
|
),
|
||||||
onTapOutside: (event) {
|
|
||||||
focusNode.unfocus();
|
|
||||||
},
|
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value == null || value.isEmpty) {
|
if (value == null || value.isEmpty) {
|
||||||
return '';
|
return '';
|
||||||
|
@ -64,11 +30,4 @@ class _LightmeterTextFieldState extends State<LightmeterTextField> {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
focusNode.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
name: lightmeter
|
name: lightmeter
|
||||||
description: Lightmeter app inspired by Material 3 design system.
|
description: Lightmeter app inspired by Material 3 design system.
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
version: 1.0.4+59
|
version: 1.0.3+58
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.0.0 <4.0.0"
|
sdk: ">=3.0.0 <4.0.0"
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
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/';
|
|
|
@ -1,30 +0,0 @@
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
// 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: '');
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
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
|
|
|
@ -7,7 +7,6 @@ void main() {
|
||||||
expect(SupportedLocale.fr.intlName, 'fr');
|
expect(SupportedLocale.fr.intlName, 'fr');
|
||||||
expect(SupportedLocale.ru.intlName, 'ru');
|
expect(SupportedLocale.ru.intlName, 'ru');
|
||||||
expect(SupportedLocale.zh.intlName, 'zh');
|
expect(SupportedLocale.zh.intlName, 'zh');
|
||||||
expect(SupportedLocale.de.intlName, 'de');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('localizedName', () {
|
test('localizedName', () {
|
||||||
|
@ -15,6 +14,5 @@ void main() {
|
||||||
expect(SupportedLocale.fr.localizedName, 'Français');
|
expect(SupportedLocale.fr.localizedName, 'Français');
|
||||||
expect(SupportedLocale.ru.localizedName, 'Русский');
|
expect(SupportedLocale.ru.localizedName, 'Русский');
|
||||||
expect(SupportedLocale.zh.localizedName, '简体中文');
|
expect(SupportedLocale.zh.localizedName, '简体中文');
|
||||||
expect(SupportedLocale.de.localizedName, 'Deutsch');
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:lightmeter/generated/l10n.dart';
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
import 'package:lightmeter/providers/equipment_profile_provider.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: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_iap/m3_lightmeter_iap.dart';
|
||||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||||
|
@ -35,11 +33,8 @@ void main() {
|
||||||
],
|
],
|
||||||
child: EquipmentProfilesProvider(
|
child: EquipmentProfilesProvider(
|
||||||
storageService: storageService,
|
storageService: storageService,
|
||||||
child: WidgetTestApplicationMock(
|
child: const WidgetTestApplicationMock(
|
||||||
child: BlocProvider(
|
child: Row(children: [Expanded(child: EquipmentProfilePicker())]),
|
||||||
create: (_) => MeteringCommunicationBloc(),
|
|
||||||
child: const Row(children: [Expanded(child: EquipmentProfilePicker())]),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
Loading…
Reference in a new issue