Compare commits

..

8 commits

Author SHA1 Message Date
Vadim
bed4535910 GP release (wip) 2023-08-07 22:06:33 +02:00
Vadim
ccb94541ed Removed redundant jobs requirements 2023-08-07 22:04:40 +02:00
Vadim
0854ddd952 Removed redundant checkouts 2023-08-07 22:04:16 +02:00
Vadim
fa98eace58 Merge remote-tracking branch 'origin' into feature/ML-61 2023-08-07 21:48:48 +02:00
vodemn
9c11401175 Version bump 2023-08-07 14:59:47 +00:00
Vadim
737a9aa2c2
ML-98 Metering top bar cutout doesn't pass through taps (#99)
* replaced `OverflowBox` with `Stack`
2023-08-07 12:56:29 +02:00
Vadim
886188bb9e
ML-95 Live histogram (#97)
* Added histogram and separated camera view builder

* Added histogram to `MeteringScreenLayoutConfig`

* `ResolutionPreset.medium` -> `ResolutionPreset.low`

* Adjusted histogram paddings
2023-08-06 16:28:20 +02:00
Vadim
1310b78a54
ML-61 Delete artefacts after release creation (#96)
* Replaced "Build ..." flow with "Create new release"

* Renamed other flows
2023-08-05 21:11:23 +02:00
21 changed files with 382 additions and 150 deletions

View file

@ -76,7 +76,7 @@ jobs:
- name: Build Apk - name: Build Apk
env: env:
FLAVOR: ${{ github.event.inputs.flavor }} FLAVOR: ${{ github.event.inputs.flavor }}
run: flutter build apk --release --flavor $FLAVOR --dart-define cameraPreviewAspectRatio=2/3 -t lib/main_$FLAVOR.dart run: flutter build apk --release --flavor $FLAVOR --dart-define c -t lib/main_$FLAVOR.dart
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3

View file

@ -16,7 +16,7 @@ on:
type: string type: string
env: env:
BUILD_ARGS: --release --flavor prod --dart-define cameraPreviewAspectRatio=2/3 -t lib/main_prod.dart BUILD_ARGS: --release --flavor prod --dart-define cameraPreviewAspectRatio=240/320 -t lib/main_prod.dart
jobs: jobs:
build: build:
@ -123,16 +123,12 @@ jobs:
create-release: create-release:
name: Create Github release name: Create Github release
needs: [build, update-version-in-repo] needs: [update-version-in-repo]
if: github.ref_name == 'main' if: github.ref_name == 'main'
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: write contents: write
steps: steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Download apk - name: Download apk
uses: actions/download-artifact@v3 uses: actions/download-artifact@v3
with: with:
@ -157,10 +153,6 @@ jobs:
needs: [build] needs: [build]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Download app bundle - name: Download app bundle
uses: actions/download-artifact@v3 uses: actions/download-artifact@v3
with: with:
@ -190,14 +182,31 @@ jobs:
create-google-play-release: create-google-play-release:
if: false if: false
name: Create Google Play release name: Create Google Play release
needs: [build, extract-merged-native-libs] needs: [extract-merged-native-libs]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - name: Download app bundle & merged native libs
with:
submodules: recursive
- name: Download app bundle
uses: actions/download-artifact@v3 uses: actions/download-artifact@v3
with: with:
name: m3_lightmeter_bundle name: |
m3_lightmeter_bundle
merged_native_libs
- name: Create Google Play release
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: #TODO:
packageName: com.vodemn.lightmeter
releaseFiles: app-prod-release.aab
track: production
status: inProgress
userFraction: 1.0
whatsNewDirectory: #TODO:
debugSymbols: merged_native_libs.zip
- name: Delete no longer used app bundle & merged native libs artifacts
uses: geekyeggo/delete-artifact@v2
with:
name: |
m3_lightmeter_bundle
merged_native_libs

4
.vscode/launch.json vendored
View file

@ -12,7 +12,7 @@
"--flavor", "--flavor",
"dev", "dev",
"--dart-define", "--dart-define",
"cameraPreviewAspectRatio=2/3", "cameraPreviewAspectRatio=240/320",
], ],
"program": "${workspaceFolder}/lib/main_dev.dart", "program": "${workspaceFolder}/lib/main_dev.dart",
}, },
@ -36,7 +36,7 @@
"--flavor", "--flavor",
"prod", "prod",
"--dart-define", "--dart-define",
"cameraPreviewAspectRatio=2/3", "cameraPreviewAspectRatio=240/320",
], ],
"program": "${workspaceFolder}/lib/main_prod.dart", "program": "${workspaceFolder}/lib/main_prod.dart",
}, },

6
.vscode/tasks.json vendored
View file

@ -12,7 +12,7 @@
"dev", "dev",
"--release", "--release",
"--dart-define", "--dart-define",
"cameraPreviewAspectRatio=2/3", "cameraPreviewAspectRatio=240/320",
"-t", "-t",
"lib/main_dev.dart", "lib/main_dev.dart",
], ],
@ -28,7 +28,7 @@
"prod", "prod",
"--release", "--release",
"--dart-define", "--dart-define",
"cameraPreviewAspectRatio=2/3", "cameraPreviewAspectRatio=240/320",
"-t", "-t",
"lib/main_prod.dart", "lib/main_prod.dart",
], ],
@ -44,7 +44,7 @@
"prod", "prod",
"--release", "--release",
"--dart-define", "--dart-define",
"cameraPreviewAspectRatio=2/3", "cameraPreviewAspectRatio=240/320",
"-t", "-t",
"lib/main_prod.dart", "lib/main_prod.dart",
], ],

View file

@ -45,11 +45,11 @@ flutter pub get
flutter pub run intl_utils:generate flutter pub run intl_utils:generate
``` ```
### 4. Build ### 4. Build (Android)
You can build an apk by running the following command from the root of the repository: You can build an apk by running the following command from the root of the repository:
```console ```console
flutter build apk --release --flavor $FLAVOR --dart-define cameraPreviewAspectRatio=2/3 -t lib/main_$FLAVOR.dart flutter build apk --release --flavor $FLAVOR --dart-define cameraPreviewAspectRatio=240/320 -t lib/main_$FLAVOR.dart
``` ```
Just replace `$FLAVOR` with `dev` or `prod`. Just replace `$FLAVOR` with `dev` or `prod`.

View file

@ -1,11 +1,13 @@
enum MeteringScreenLayoutFeature { extremeExposurePairs, filmPicker } enum MeteringScreenLayoutFeature { extremeExposurePairs, filmPicker, histogram }
typedef MeteringScreenLayoutConfig = Map<MeteringScreenLayoutFeature, bool>; typedef MeteringScreenLayoutConfig = Map<MeteringScreenLayoutFeature, bool>;
extension MeteringScreenLayoutConfigJson on MeteringScreenLayoutConfig { extension MeteringScreenLayoutConfigJson on MeteringScreenLayoutConfig {
static MeteringScreenLayoutConfig fromJson(Map<String, dynamic> data) => data.map( static MeteringScreenLayoutConfig fromJson(Map<String, dynamic> data) =>
(key, value) => MapEntry(MeteringScreenLayoutFeature.values[int.parse(key)], value as bool), <MeteringScreenLayoutFeature, bool>{
); for (final f in MeteringScreenLayoutFeature.values)
f: data[f.index.toString()] as bool? ?? true
};
Map<String, dynamic> toJson() => map((key, value) => MapEntry(key.index.toString(), value)); Map<String, dynamic> toJson() => map((key, value) => MapEntry(key.index.toString(), value));
} }

View file

@ -97,6 +97,7 @@ class UserPreferencesService {
return { return {
MeteringScreenLayoutFeature.extremeExposurePairs: true, MeteringScreenLayoutFeature.extremeExposurePairs: true,
MeteringScreenLayoutFeature.filmPicker: true, MeteringScreenLayoutFeature.filmPicker: true,
MeteringScreenLayoutFeature.histogram: true,
}; };
} }
} }

View file

@ -38,6 +38,7 @@
"meteringScreenLayoutHint": "Hide elements on the metering screen that you don't need so that they don't waste exposure pairs list space.", "meteringScreenLayoutHint": "Hide elements on the metering screen that you don't need so that they don't waste exposure pairs list space.",
"meteringScreenFeatureExtremeExposurePairs": "Fastest & shortest exposure pairs", "meteringScreenFeatureExtremeExposurePairs": "Fastest & shortest exposure pairs",
"meteringScreenFeatureFilmPicker": "Film picker", "meteringScreenFeatureFilmPicker": "Film picker",
"meteringScreenFeatureHistogram": "Histogram",
"film": "Film", "film": "Film",
"equipment": "Equipment", "equipment": "Equipment",
"equipmentProfileName": "Equipment profile name", "equipmentProfileName": "Equipment profile name",

View file

@ -38,6 +38,7 @@
"meteringScreenLayoutHint": "Masquer les éléments sur l'écran de mesure dont vous n'avez pas besoin pour qu'ils ne gaspillent pas de l'espace dans les paires d'exposition.", "meteringScreenLayoutHint": "Masquer les éléments sur l'écran de mesure dont vous n'avez pas besoin pour qu'ils ne gaspillent pas de l'espace dans les paires d'exposition.",
"meteringScreenFeatureExtremeExposurePairs": "Paires d'exposition les plus rapides et les plus courtes", "meteringScreenFeatureExtremeExposurePairs": "Paires d'exposition les plus rapides et les plus courtes",
"meteringScreenFeatureFilmPicker": "Sélecteur de film", "meteringScreenFeatureFilmPicker": "Sélecteur de film",
"meteringScreenFeatureHistogram": "Histogramme",
"film": "Pellicule", "film": "Pellicule",
"equipment": "Équipement", "equipment": "Équipement",
"equipmentProfileName": "Nom du profil de l'équipement", "equipmentProfileName": "Nom du profil de l'équipement",

View file

@ -38,6 +38,7 @@
"meteringScreenLayoutHint": "Здесь вы можете скрыть некоторые ненужные или неиспользуемые элементы с главного экрана.", "meteringScreenLayoutHint": "Здесь вы можете скрыть некоторые ненужные или неиспользуемые элементы с главного экрана.",
"meteringScreenFeatureExtremeExposurePairs": "Длинная и короткая выдержки", "meteringScreenFeatureExtremeExposurePairs": "Длинная и короткая выдержки",
"meteringScreenFeatureFilmPicker": "Выбор пленки", "meteringScreenFeatureFilmPicker": "Выбор пленки",
"meteringScreenFeatureHistogram": "Гистограмма",
"film": "Пленка", "film": "Пленка",
"equipment": "Оборудование", "equipment": "Оборудование",
"equipmentProfileName": "Название профиля", "equipmentProfileName": "Название профиля",

View file

@ -38,6 +38,7 @@
"meteringScreenLayoutHint": "隐藏不需要的元素,以免浪费曝光列表空间", "meteringScreenLayoutHint": "隐藏不需要的元素,以免浪费曝光列表空间",
"meteringScreenFeatureExtremeExposurePairs": "最快 & 最慢曝光组合", "meteringScreenFeatureExtremeExposurePairs": "最快 & 最慢曝光组合",
"meteringScreenFeatureFilmPicker": "胶片选择", "meteringScreenFeatureFilmPicker": "胶片选择",
"meteringScreenFeatureHistogram": "直方图",
"film": "胶片", "film": "胶片",
"equipment": "设备", "equipment": "设备",
"equipmentProfileName": "设备配置名称", "equipmentProfileName": "设备配置名称",

View file

@ -123,7 +123,7 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
(camera) => camera.lensDirection == CameraLensDirection.back, (camera) => camera.lensDirection == CameraLensDirection.back,
orElse: () => cameras.last, orElse: () => cameras.last,
), ),
ResolutionPreset.medium, ResolutionPreset.low,
enableAudio: false, enableAudio: false,
); );

View file

@ -14,10 +14,12 @@ class CameraView extends StatelessWidget {
valueListenable: controller, valueListenable: controller,
builder: (_, __, ___) => AspectRatio( builder: (_, __, ___) => AspectRatio(
aspectRatio: _isLandscape(value) ? value.aspectRatio : (1 / value.aspectRatio), aspectRatio: _isLandscape(value) ? value.aspectRatio : (1 / value.aspectRatio),
child: RotatedBox( child: value.isInitialized
? RotatedBox(
quarterTurns: _getQuarterTurns(value), quarterTurns: _getQuarterTurns(value),
child: controller.buildPreview(), child: controller.buildPreview(),
), )
: const SizedBox.shrink(),
), ),
); );
} }

View file

@ -0,0 +1,132 @@
import 'dart:math';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:lightmeter/res/dimens.dart';
class CameraHistogram extends StatefulWidget {
final CameraController controller;
const CameraHistogram({required this.controller, super.key});
@override
_CameraHistogramState createState() => _CameraHistogramState();
}
class _CameraHistogramState extends State<CameraHistogram> {
List<int> histogramR = List.filled(256, 0);
List<int> histogramG = List.filled(256, 0);
List<int> histogramB = List.filled(256, 0);
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_startImageStream();
});
}
@override
void dispose() {
widget.controller.stopImageStream();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
HistogramChannel(
color: Colors.red,
values: histogramR,
),
const SizedBox(height: Dimens.grid4),
HistogramChannel(
color: Colors.green,
values: histogramG,
),
const SizedBox(height: Dimens.grid4),
HistogramChannel(
color: Colors.blue,
values: histogramB,
),
],
);
}
void _startImageStream() {
widget.controller.startImageStream((CameraImage image) {
histogramR = List.filled(256, 0);
histogramG = List.filled(256, 0);
histogramB = List.filled(256, 0);
final int uvRowStride = image.planes[1].bytesPerRow;
final int uvPixelStride = image.planes[1].bytesPerPixel!;
for (int x = 0; x < image.width; x++) {
for (int y = 0; y < image.height; y++) {
final int uvIndex = uvPixelStride * (x / 2).floor() + uvRowStride * (y / 2).floor();
final int index = y * image.width + x;
final yp = image.planes[0].bytes[index];
final up = image.planes[1].bytes[uvIndex];
final vp = image.planes[2].bytes[uvIndex];
final r = yp + vp * 1436 / 1024 - 179;
final g = yp - up * 46549 / 131072 + 44 - vp * 93604 / 131072 + 91;
final b = yp + up * 1814 / 1024 - 227;
histogramR[r.round().clamp(0, 255)]++;
histogramG[g.round().clamp(0, 255)]++;
histogramB[b.round().clamp(0, 255)]++;
}
}
if (mounted) setState(() {});
});
}
}
class HistogramChannel extends StatelessWidget {
final List<int> values;
final Color color;
final int _maxOccurences;
HistogramChannel({
required this.values,
required this.color,
super.key,
}) : _maxOccurences = values.reduce((value, element) => max(value, element));
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final pixelWidth = constraints.maxWidth / values.length;
return Column(
children: [
SizedBox(
height: Dimens.grid16,
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: values
.map(
(e) => SizedBox(
height: _maxOccurences == 0 ? 0 : Dimens.grid16 * (e / _maxOccurences),
width: pixelWidth,
child: ColoredBox(color: color),
),
)
.toList(),
),
),
const Divider(),
],
);
},
);
}
}

View file

@ -0,0 +1,62 @@
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
import 'package:lightmeter/platform_config.dart';
import 'package:lightmeter/providers/metering_screen_layout_provider.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/metering/components/camera_container/components/camera_preview/components/camera_view/widget_camera_view.dart';
import 'package:lightmeter/screens/metering/components/camera_container/components/camera_preview/components/camera_view_placeholder/widget_placeholder_camera_view.dart';
import 'package:lightmeter/screens/metering/components/camera_container/components/camera_preview/components/histogram/widget_histogram.dart';
import 'package:lightmeter/screens/metering/components/camera_container/models/camera_error_type.dart';
class CameraPreview extends StatefulWidget {
final CameraController? controller;
final CameraErrorType? error;
const CameraPreview({this.controller, this.error, super.key});
@override
State<CameraPreview> createState() => _CameraPreviewState();
}
class _CameraPreviewState extends State<CameraPreview> {
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: PlatformConfig.cameraPreviewAspectRatio,
child: Center(
child: Stack(
children: [
const CameraViewPlaceholder(error: null),
AnimatedSwitcher(
duration: Dimens.switchDuration,
child: widget.controller != null
? ValueListenableBuilder<CameraValue>(
valueListenable: widget.controller!,
builder: (_, __, ___) => widget.controller!.value.isInitialized
? Stack(
alignment: Alignment.bottomCenter,
children: [
CameraView(controller: widget.controller!),
if (MeteringScreenLayout.featureOf(
context,
MeteringScreenLayoutFeature.histogram,
))
Positioned(
left: Dimens.grid8,
right: Dimens.grid8,
bottom: Dimens.grid16,
child: CameraHistogram(controller: widget.controller!),
),
],
)
: const SizedBox.shrink(),
)
: CameraViewPlaceholder(error: widget.error),
),
],
),
),
);
}
}

View file

@ -1,3 +1,5 @@
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/data/models/exposure_pair.dart'; import 'package:lightmeter/data/models/exposure_pair.dart';
@ -10,8 +12,7 @@ import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/metering/components/camera_container/bloc_container_camera.dart'; import 'package:lightmeter/screens/metering/components/camera_container/bloc_container_camera.dart';
import 'package:lightmeter/screens/metering/components/camera_container/components/camera_controls/widget_camera_controls.dart'; import 'package:lightmeter/screens/metering/components/camera_container/components/camera_controls/widget_camera_controls.dart';
import 'package:lightmeter/screens/metering/components/camera_container/components/camera_controls_placeholder/widget_placeholder_camera_controls.dart'; import 'package:lightmeter/screens/metering/components/camera_container/components/camera_controls_placeholder/widget_placeholder_camera_controls.dart';
import 'package:lightmeter/screens/metering/components/camera_container/components/camera_view/widget_camera_view.dart'; import 'package:lightmeter/screens/metering/components/camera_container/components/camera_preview/widget_camera_preview.dart';
import 'package:lightmeter/screens/metering/components/camera_container/components/camera_view_placeholder/widget_placeholder_camera_view.dart';
import 'package:lightmeter/screens/metering/components/camera_container/event_container_camera.dart'; import 'package:lightmeter/screens/metering/components/camera_container/event_container_camera.dart';
import 'package:lightmeter/screens/metering/components/camera_container/models/camera_error_type.dart'; import 'package:lightmeter/screens/metering/components/camera_container/models/camera_error_type.dart';
import 'package:lightmeter/screens/metering/components/camera_container/state_container_camera.dart'; import 'package:lightmeter/screens/metering/components/camera_container/state_container_camera.dart';
@ -46,34 +47,17 @@ class CameraContainer extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final double cameraViewHeight = final double meteringContainerHeight = _meteringContainerHeight(context);
((MediaQuery.of(context).size.width - Dimens.grid8 - 2 * Dimens.paddingM) / 2) / final double cameraPreviewHeight = _cameraPreviewHeight(context);
PlatformConfig.cameraPreviewAspectRatio; final double topBarOverflow = meteringContainerHeight - cameraPreviewHeight;
double topBarOverflow = Dimens.readingContainerSingleValueHeight + // ISO & ND return Stack(
-cameraViewHeight;
if (FeaturesConfig.equipmentProfilesEnabled) {
topBarOverflow += Dimens.readingContainerSingleValueHeight;
topBarOverflow += Dimens.paddingS;
}
if (MeteringScreenLayout.featureOf(
context,
MeteringScreenLayoutFeature.extremeExposurePairs,
)) {
topBarOverflow += Dimens.readingContainerDoubleValueHeight;
topBarOverflow += Dimens.paddingS;
}
if (MeteringScreenLayout.featureOf(
context,
MeteringScreenLayoutFeature.filmPicker,
)) {
topBarOverflow += Dimens.readingContainerSingleValueHeight;
topBarOverflow += Dimens.paddingS;
}
return Column(
children: [ children: [
MeteringTopBar( Positioned(
left: 0,
top: 0,
right: 0,
child: MeteringTopBar(
readingsContainer: ReadingsContainer( readingsContainer: ReadingsContainer(
fastest: fastest, fastest: fastest,
slowest: slowest, slowest: slowest,
@ -87,19 +71,71 @@ class CameraContainer extends StatelessWidget {
appendixHeight: topBarOverflow, appendixHeight: topBarOverflow,
preview: const _CameraViewBuilder(), preview: const _CameraViewBuilder(),
), ),
),
SafeArea(
bottom: false,
child: Column(
children: [
SizedBox(
height: min(meteringContainerHeight, cameraPreviewHeight) + Dimens.paddingM * 2,
),
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM), padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
child: _MiddleContentWrapper( child: Row(
topBarOverflow: topBarOverflow, children: [
leftContent: ExposurePairsList(exposurePairs), Expanded(
rightContent: const _CameraControlsBuilder(), child: Padding(
padding: EdgeInsets.only(top: topBarOverflow >= 0 ? topBarOverflow : 0),
child: ExposurePairsList(exposurePairs),
),
),
const SizedBox(width: Dimens.grid8),
Expanded(
child: Padding(
padding: EdgeInsets.only(top: topBarOverflow <= 0 ? -topBarOverflow : 0),
child: const _CameraControlsBuilder(),
),
),
],
), ),
), ),
), ),
], ],
),
)
],
); );
} }
double _meteringContainerHeight(BuildContext context) {
double enabledFeaturesHeight = 0;
if (FeaturesConfig.equipmentProfilesEnabled) {
enabledFeaturesHeight += Dimens.readingContainerSingleValueHeight;
enabledFeaturesHeight += Dimens.paddingS;
}
if (MeteringScreenLayout.featureOf(
context,
MeteringScreenLayoutFeature.extremeExposurePairs,
)) {
enabledFeaturesHeight += Dimens.readingContainerDoubleValueHeight;
enabledFeaturesHeight += Dimens.paddingS;
}
if (MeteringScreenLayout.featureOf(
context,
MeteringScreenLayoutFeature.filmPicker,
)) {
enabledFeaturesHeight += Dimens.readingContainerSingleValueHeight;
enabledFeaturesHeight += Dimens.paddingS;
}
return enabledFeaturesHeight + Dimens.readingContainerSingleValueHeight; // ISO & ND
}
double _cameraPreviewHeight(BuildContext context) {
return ((MediaQuery.of(context).size.width - Dimens.grid8 - 2 * Dimens.paddingM) / 2) /
PlatformConfig.cameraPreviewAspectRatio;
}
} }
class _CameraViewBuilder extends StatelessWidget { class _CameraViewBuilder extends StatelessWidget {
@ -107,20 +143,11 @@ class _CameraViewBuilder extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AspectRatio( return BlocBuilder<CameraContainerBloc, CameraContainerState>(
aspectRatio: PlatformConfig.cameraPreviewAspectRatio,
child: BlocBuilder<CameraContainerBloc, CameraContainerState>(
buildWhen: (previous, current) => current is! CameraActiveState, buildWhen: (previous, current) => current is! CameraActiveState,
builder: (context, state) => Center( builder: (context, state) => CameraPreview(
child: AnimatedSwitcher( controller: state is CameraInitializedState ? state.controller : null,
duration: Dimens.durationM, error: state is CameraErrorState ? state.error : null,
child: switch (state) {
CameraInitializedState() => CameraView(controller: state.controller),
CameraErrorState() => CameraViewPlaceholder(error: state.error),
_ => const CameraViewPlaceholder(error: null),
},
),
),
), ),
); );
} }
@ -161,7 +188,9 @@ class _CameraControlsBuilder extends StatelessWidget {
}, },
); );
} else { } else {
child = const Column(children: [Expanded(child: SizedBox.shrink())],); child = const Column(
children: [Expanded(child: SizedBox.shrink())],
);
} }
return AnimatedSwitcher( return AnimatedSwitcher(
@ -173,43 +202,3 @@ class _CameraControlsBuilder extends StatelessWidget {
); );
} }
} }
class _MiddleContentWrapper extends StatelessWidget {
final double topBarOverflow;
final Widget leftContent;
final Widget rightContent;
const _MiddleContentWrapper({
required this.topBarOverflow,
required this.leftContent,
required this.rightContent,
});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) => OverflowBox(
alignment: Alignment.bottomCenter,
maxHeight: constraints.maxHeight + topBarOverflow.abs(),
maxWidth: constraints.maxWidth,
child: Row(
children: [
Expanded(
child: Padding(
padding: EdgeInsets.only(top: topBarOverflow >= 0 ? topBarOverflow : 0),
child: leftContent,
),
),
const SizedBox(width: Dimens.grid8),
Expanded(
child: Padding(
padding: EdgeInsets.only(top: topBarOverflow <= 0 ? -topBarOverflow : 0),
child: rightContent,
),
),
],
),
),
);
}
}

View file

@ -71,6 +71,8 @@ class _MeteringScreenLayoutFeaturesDialogState extends State<MeteringScreenLayou
return S.of(context).meteringScreenFeatureExtremeExposurePairs; return S.of(context).meteringScreenFeatureExtremeExposurePairs;
case MeteringScreenLayoutFeature.filmPicker: case MeteringScreenLayoutFeature.filmPicker:
return S.of(context).meteringScreenFeatureFilmPicker; return S.of(context).meteringScreenFeatureFilmPicker;
case MeteringScreenLayoutFeature.histogram:
return S.of(context).meteringScreenFeatureHistogram;
} }
} }
} }

View file

@ -1,7 +1,7 @@
name: lightmeter name: lightmeter
description: A new Flutter project. description: A new Flutter project.
publish_to: "none" publish_to: "none"
version: 0.12.4+35 version: 0.13.0+36
environment: environment:
sdk: ">=3.0.0 <4.0.0" sdk: ">=3.0.0 <4.0.0"

View file

@ -2,30 +2,56 @@ import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
void main() { void main() {
test('fromJson', () { group(
'fromJson()',
() {
test('All keys', () {
expect( expect(
MeteringScreenLayoutConfigJson.fromJson({'0': true, '1': true}), MeteringScreenLayoutConfigJson.fromJson(
{
'0': true,
'1': true,
'2': true,
},
),
{ {
MeteringScreenLayoutFeature.extremeExposurePairs: true, MeteringScreenLayoutFeature.extremeExposurePairs: true,
MeteringScreenLayoutFeature.filmPicker: true, MeteringScreenLayoutFeature.filmPicker: true,
}, MeteringScreenLayoutFeature.histogram: true,
);
expect(
MeteringScreenLayoutConfigJson.fromJson({'0': false, '1': false}),
{
MeteringScreenLayoutFeature.extremeExposurePairs: false,
MeteringScreenLayoutFeature.filmPicker: false,
}, },
); );
}); });
test('Legacy (no equipment profiles)', () {
expect(
MeteringScreenLayoutConfigJson.fromJson(
{
'0': true,
'1': true,
},
),
{
MeteringScreenLayoutFeature.extremeExposurePairs: true,
MeteringScreenLayoutFeature.filmPicker: true,
MeteringScreenLayoutFeature.histogram: true,
},
);
});
},
);
test('toJson', () { test('toJson', () {
expect( expect(
{ {
MeteringScreenLayoutFeature.extremeExposurePairs: true, MeteringScreenLayoutFeature.extremeExposurePairs: true,
MeteringScreenLayoutFeature.filmPicker: true, MeteringScreenLayoutFeature.filmPicker: true,
MeteringScreenLayoutFeature.histogram: true,
}.toJson(), }.toJson(),
{'0': true, '1': true}, {
'0': true,
'1': true,
'2': true,
},
); );
}); });
} }

View file

@ -193,6 +193,7 @@ void main() {
{ {
MeteringScreenLayoutFeature.extremeExposurePairs: true, MeteringScreenLayoutFeature.extremeExposurePairs: true,
MeteringScreenLayoutFeature.filmPicker: true, MeteringScreenLayoutFeature.filmPicker: true,
MeteringScreenLayoutFeature.histogram: true,
}, },
); );
}); });
@ -206,6 +207,7 @@ void main() {
{ {
MeteringScreenLayoutFeature.extremeExposurePairs: false, MeteringScreenLayoutFeature.extremeExposurePairs: false,
MeteringScreenLayoutFeature.filmPicker: true, MeteringScreenLayoutFeature.filmPicker: true,
MeteringScreenLayoutFeature.histogram: true,
}, },
); );
}); });
@ -214,17 +216,18 @@ void main() {
when( when(
() => sharedPreferences.setString( () => sharedPreferences.setString(
UserPreferencesService.meteringScreenLayoutKey, UserPreferencesService.meteringScreenLayoutKey,
"""{"0":false,"1":true}""", """{"0":false,"1":true,"2":true}""",
), ),
).thenAnswer((_) => Future.value(true)); ).thenAnswer((_) => Future.value(true));
service.meteringScreenLayout = { service.meteringScreenLayout = {
MeteringScreenLayoutFeature.extremeExposurePairs: false, MeteringScreenLayoutFeature.extremeExposurePairs: false,
MeteringScreenLayoutFeature.filmPicker: true, MeteringScreenLayoutFeature.filmPicker: true,
MeteringScreenLayoutFeature.histogram: true,
}; };
verify( verify(
() => sharedPreferences.setString( () => sharedPreferences.setString(
UserPreferencesService.meteringScreenLayoutKey, UserPreferencesService.meteringScreenLayoutKey,
"""{"0":false,"1":true}""", """{"0":false,"1":true,"2":true}""",
), ),
).called(1); ).called(1);
}); });