Compare commits

..

No commits in common. "bed4535910cd2302b7b6bf442a6dd252c6553b67" and "32dc310a66a1f8aeb594d1a6ab29e720cc88907d" have entirely different histories.

21 changed files with 150 additions and 382 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 c -t lib/main_$FLAVOR.dart run: flutter build apk --release --flavor $FLAVOR --dart-define cameraPreviewAspectRatio=2/3 -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=240/320 -t lib/main_prod.dart BUILD_ARGS: --release --flavor prod --dart-define cameraPreviewAspectRatio=2/3 -t lib/main_prod.dart
jobs: jobs:
build: build:
@ -123,12 +123,16 @@ jobs:
create-release: create-release:
name: Create Github release name: Create Github release
needs: [update-version-in-repo] needs: [build, 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:
@ -153,6 +157,10 @@ 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:
@ -182,31 +190,14 @@ jobs:
create-google-play-release: create-google-play-release:
if: false if: false
name: Create Google Play release name: Create Google Play release
needs: [extract-merged-native-libs] needs: [build, extract-merged-native-libs]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Download app bundle & merged native libs - uses: actions/checkout@v3
with:
submodules: recursive
- name: Download app bundle
uses: actions/download-artifact@v3 uses: actions/download-artifact@v3
with: with:
name: | name: m3_lightmeter_bundle
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=240/320", "cameraPreviewAspectRatio=2/3",
], ],
"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=240/320", "cameraPreviewAspectRatio=2/3",
], ],
"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=240/320", "cameraPreviewAspectRatio=2/3",
"-t", "-t",
"lib/main_dev.dart", "lib/main_dev.dart",
], ],
@ -28,7 +28,7 @@
"prod", "prod",
"--release", "--release",
"--dart-define", "--dart-define",
"cameraPreviewAspectRatio=240/320", "cameraPreviewAspectRatio=2/3",
"-t", "-t",
"lib/main_prod.dart", "lib/main_prod.dart",
], ],
@ -44,7 +44,7 @@
"prod", "prod",
"--release", "--release",
"--dart-define", "--dart-define",
"cameraPreviewAspectRatio=240/320", "cameraPreviewAspectRatio=2/3",
"-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 (Android) ### 4. Build
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=240/320 -t lib/main_$FLAVOR.dart flutter build apk --release --flavor $FLAVOR --dart-define cameraPreviewAspectRatio=2/3 -t lib/main_$FLAVOR.dart
``` ```
Just replace `$FLAVOR` with `dev` or `prod`. Just replace `$FLAVOR` with `dev` or `prod`.

View file

@ -1,13 +1,11 @@
enum MeteringScreenLayoutFeature { extremeExposurePairs, filmPicker, histogram } enum MeteringScreenLayoutFeature { extremeExposurePairs, filmPicker }
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) => static MeteringScreenLayoutConfig fromJson(Map<String, dynamic> data) => data.map(
<MeteringScreenLayoutFeature, bool>{ (key, value) => MapEntry(MeteringScreenLayoutFeature.values[int.parse(key)], value as 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,7 +97,6 @@ class UserPreferencesService {
return { return {
MeteringScreenLayoutFeature.extremeExposurePairs: true, MeteringScreenLayoutFeature.extremeExposurePairs: true,
MeteringScreenLayoutFeature.filmPicker: true, MeteringScreenLayoutFeature.filmPicker: true,
MeteringScreenLayoutFeature.histogram: true,
}; };
} }
} }

View file

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

View file

@ -38,7 +38,6 @@
"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.low, ResolutionPreset.medium,
enableAudio: false, enableAudio: false,
); );

View file

@ -1,132 +0,0 @@
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

@ -1,62 +0,0 @@
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

@ -14,12 +14,10 @@ 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: value.isInitialized child: RotatedBox(
? RotatedBox(
quarterTurns: _getQuarterTurns(value), quarterTurns: _getQuarterTurns(value),
child: controller.buildPreview(), child: controller.buildPreview(),
) ),
: const SizedBox.shrink(),
), ),
); );
} }

View file

@ -1,5 +1,3 @@
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';
@ -12,7 +10,8 @@ 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_preview/widget_camera_preview.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_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';
@ -47,17 +46,34 @@ class CameraContainer extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final double meteringContainerHeight = _meteringContainerHeight(context); final double cameraViewHeight =
final double cameraPreviewHeight = _cameraPreviewHeight(context); ((MediaQuery.of(context).size.width - Dimens.grid8 - 2 * Dimens.paddingM) / 2) /
final double topBarOverflow = meteringContainerHeight - cameraPreviewHeight; PlatformConfig.cameraPreviewAspectRatio;
return Stack( double topBarOverflow = Dimens.readingContainerSingleValueHeight + // ISO & ND
-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: [
Positioned( MeteringTopBar(
left: 0,
top: 0,
right: 0,
child: MeteringTopBar(
readingsContainer: ReadingsContainer( readingsContainer: ReadingsContainer(
fastest: fastest, fastest: fastest,
slowest: slowest, slowest: slowest,
@ -71,71 +87,19 @@ 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: Row( child: _MiddleContentWrapper(
children: [ topBarOverflow: topBarOverflow,
Expanded( leftContent: ExposurePairsList(exposurePairs),
child: Padding( rightContent: const _CameraControlsBuilder(),
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 {
@ -143,11 +107,20 @@ class _CameraViewBuilder extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<CameraContainerBloc, CameraContainerState>( return AspectRatio(
aspectRatio: PlatformConfig.cameraPreviewAspectRatio,
child: BlocBuilder<CameraContainerBloc, CameraContainerState>(
buildWhen: (previous, current) => current is! CameraActiveState, buildWhen: (previous, current) => current is! CameraActiveState,
builder: (context, state) => CameraPreview( builder: (context, state) => Center(
controller: state is CameraInitializedState ? state.controller : null, child: AnimatedSwitcher(
error: state is CameraErrorState ? state.error : null, duration: Dimens.durationM,
child: switch (state) {
CameraInitializedState() => CameraView(controller: state.controller),
CameraErrorState() => CameraViewPlaceholder(error: state.error),
_ => const CameraViewPlaceholder(error: null),
},
),
),
), ),
); );
} }
@ -188,9 +161,7 @@ class _CameraControlsBuilder extends StatelessWidget {
}, },
); );
} else { } else {
child = const Column( child = const Column(children: [Expanded(child: SizedBox.shrink())],);
children: [Expanded(child: SizedBox.shrink())],
);
} }
return AnimatedSwitcher( return AnimatedSwitcher(
@ -202,3 +173,43 @@ 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,8 +71,6 @@ 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.13.0+36 version: 0.12.4+35
environment: environment:
sdk: ">=3.0.0 <4.0.0" sdk: ">=3.0.0 <4.0.0"

View file

@ -2,56 +2,30 @@ import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
void main() { void main() {
group( test('fromJson', () {
'fromJson()',
() {
test('All keys', () {
expect( expect(
MeteringScreenLayoutConfigJson.fromJson( MeteringScreenLayoutConfigJson.fromJson({'0': true, '1': true}),
{
'0': true,
'1': true,
'2': true,
},
),
{ {
MeteringScreenLayoutFeature.extremeExposurePairs: true, MeteringScreenLayoutFeature.extremeExposurePairs: true,
MeteringScreenLayoutFeature.filmPicker: true, MeteringScreenLayoutFeature.filmPicker: true,
MeteringScreenLayoutFeature.histogram: true,
}, },
); );
});
test('Legacy (no equipment profiles)', () {
expect( expect(
MeteringScreenLayoutConfigJson.fromJson( MeteringScreenLayoutConfigJson.fromJson({'0': false, '1': false}),
{ {
'0': true, MeteringScreenLayoutFeature.extremeExposurePairs: false,
'1': true, MeteringScreenLayoutFeature.filmPicker: false,
},
),
{
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,7 +193,6 @@ void main() {
{ {
MeteringScreenLayoutFeature.extremeExposurePairs: true, MeteringScreenLayoutFeature.extremeExposurePairs: true,
MeteringScreenLayoutFeature.filmPicker: true, MeteringScreenLayoutFeature.filmPicker: true,
MeteringScreenLayoutFeature.histogram: true,
}, },
); );
}); });
@ -207,7 +206,6 @@ void main() {
{ {
MeteringScreenLayoutFeature.extremeExposurePairs: false, MeteringScreenLayoutFeature.extremeExposurePairs: false,
MeteringScreenLayoutFeature.filmPicker: true, MeteringScreenLayoutFeature.filmPicker: true,
MeteringScreenLayoutFeature.histogram: true,
}, },
); );
}); });
@ -216,18 +214,17 @@ void main() {
when( when(
() => sharedPreferences.setString( () => sharedPreferences.setString(
UserPreferencesService.meteringScreenLayoutKey, UserPreferencesService.meteringScreenLayoutKey,
"""{"0":false,"1":true,"2":true}""", """{"0":false,"1":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,"2":true}""", """{"0":false,"1":true}""",
), ),
).called(1); ).called(1);
}); });