Compare commits
19 commits
8cdb51b4b4
...
a3f8b49549
Author | SHA1 | Date | |
---|---|---|---|
|
a3f8b49549 | ||
|
1b3d6b2b90 | ||
|
98262fc4ee | ||
|
63ff6cc867 | ||
|
66a38f2969 | ||
|
9100ae3066 | ||
|
006158c731 | ||
|
9d5860a53f | ||
|
e53eca1831 | ||
|
0e09c28150 | ||
|
a0f55c4a95 | ||
|
f0d707b071 | ||
|
1e2cd8b5d2 | ||
|
90bfe7c7b8 | ||
|
7f8ea54c6e | ||
|
36d7e081f1 | ||
|
d4deae57ef | ||
|
881778b313 | ||
|
f62f658be8 |
2
.github/workflows/build_apk.yml
vendored
|
@ -68,7 +68,7 @@ env:
|
||||||
jobs:
|
jobs:
|
||||||
build-android:
|
build-android:
|
||||||
name: Build ${{ inputs.binary-type == 'apk' && '.apk' || '.aab' }}
|
name: Build ${{ inputs.binary-type == 'apk' && '.apk' || '.aab' }}
|
||||||
runs-on: macos-11
|
runs-on: macos-latest
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
35
.github/workflows/create_release.yml
vendored
|
@ -28,6 +28,9 @@ on:
|
||||||
type: boolean
|
type: boolean
|
||||||
default: true
|
default: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
RELEASE_NOTES_FILE: release_notes_en_${{ inputs.version }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
run-integration-tests:
|
run-integration-tests:
|
||||||
name: Run integration tests
|
name: Run integration tests
|
||||||
|
@ -67,14 +70,13 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Generate release notes
|
- name: Generate release notes
|
||||||
run: |
|
run: |
|
||||||
echo ${{ inputs.release-notes }} > whatsnew-en-US.md
|
echo ${{ inputs.release-notes }} > ${{ env.RELEASE_NOTES_FILE }}.md
|
||||||
perl -i -pe 's/\s{1}(-{1})/\n$1/g' whatsnew-en-US.md
|
perl -i -pe 's/\s{1}(-{1})/\n$1/g' ${{ env.RELEASE_NOTES_FILE }}.md
|
||||||
|
|
||||||
- name: Upload merged_native_libs.zip to artifacts
|
- name: Upload merged_native_libs.zip to artifacts
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: whatsnew-en-US
|
name: ${{ env.RELEASE_NOTES_FILE }}
|
||||||
path: whatsnew-en-US.md
|
path: ${{ env.RELEASE_NOTES_FILE }}.md
|
||||||
|
|
||||||
create-github-release:
|
create-github-release:
|
||||||
name: Create Github release
|
name: Create Github release
|
||||||
|
@ -90,6 +92,14 @@ jobs:
|
||||||
- name: Increment build number & replace version number
|
- name: Increment build number & replace version number
|
||||||
run: bash ./.github/scripts/increment_build_number.sh ${{ github.event.inputs.version }}
|
run: bash ./.github/scripts/increment_build_number.sh ${{ github.event.inputs.version }}
|
||||||
|
|
||||||
|
- name: Download release notes
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: ${{ env.RELEASE_NOTES_FILE }}
|
||||||
|
|
||||||
|
- name: Move release notes to a folder
|
||||||
|
run: mv assets/release_notes/${{ env.RELEASE_NOTES_FILE }}.md ${{ env.RELEASE_NOTES_FILE }}
|
||||||
|
|
||||||
- name: Commit changes
|
- name: Commit changes
|
||||||
run: |
|
run: |
|
||||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
@ -112,17 +122,12 @@ jobs:
|
||||||
- name: Rename apk
|
- name: Rename apk
|
||||||
run: mv app-prod-release.apk m3_lightmeter.apk
|
run: mv app-prod-release.apk m3_lightmeter.apk
|
||||||
|
|
||||||
- name: Download release notes
|
|
||||||
uses: actions/download-artifact@v3
|
|
||||||
with:
|
|
||||||
name: whatsnew-en-US
|
|
||||||
|
|
||||||
- uses: ncipollo/release-action@v1.12.0
|
- uses: ncipollo/release-action@v1.12.0
|
||||||
with:
|
with:
|
||||||
artifacts: "m3_lightmeter.apk"
|
artifacts: "m3_lightmeter.apk"
|
||||||
skipIfReleaseExists: true
|
skipIfReleaseExists: true
|
||||||
tag: "v${{ github.event.inputs.version }}"
|
tag: "v${{ github.event.inputs.version }}"
|
||||||
bodyFile: "whatsnew-en-US.md"
|
bodyFile: "${{ env.RELEASE_NOTES_FILE }}.md"
|
||||||
|
|
||||||
create-google-play-release:
|
create-google-play-release:
|
||||||
name: Create Google Play release
|
name: Create Google Play release
|
||||||
|
@ -146,13 +151,13 @@ jobs:
|
||||||
- name: Download release notes
|
- name: Download release notes
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: whatsnew-en-US
|
name: ${{ env.RELEASE_NOTES_FILE }}
|
||||||
|
|
||||||
- name: Move release notes to a folder
|
- name: Move release notes to a folder
|
||||||
run: |
|
run: |
|
||||||
mv whatsnew-en-US.md whatsnew-en-US
|
mv assets/release_notes/${{ env.RELEASE_NOTES_FILE }}.md ${{ env.RELEASE_NOTES_FILE }}
|
||||||
mkdir whatsnew
|
mkdir whatsnew
|
||||||
mv whatsnew-en-US whatsnew
|
mv ${{ env.RELEASE_NOTES_FILE }} whatsnew
|
||||||
|
|
||||||
# https://unix.stackexchange.com/questions/13466/can-grep-output-only-specified-groupings-that-match'
|
# https://unix.stackexchange.com/questions/13466/can-grep-output-only-specified-groupings-that-match'
|
||||||
# https://stackoverflow.com/questions/74353311/github-workflow-unable-to-process-file-command-env-successfully
|
# https://stackoverflow.com/questions/74353311/github-workflow-unable-to-process-file-command-env-successfully
|
||||||
|
@ -207,4 +212,4 @@ jobs:
|
||||||
m3_lightmeter_apk
|
m3_lightmeter_apk
|
||||||
m3_lightmeter_appbundle
|
m3_lightmeter_appbundle
|
||||||
m3_lightmeter_ipa
|
m3_lightmeter_ipa
|
||||||
whatsnew-en-US
|
${{ env.RELEASE_NOTES_FILE }}
|
||||||
|
|
2
.gitignore
vendored
|
@ -63,4 +63,4 @@ ios/Runner/GoogleService-Info.plist
|
||||||
coverage/
|
coverage/
|
||||||
test/coverage_helper_test.dart
|
test/coverage_helper_test.dart
|
||||||
**/failures/*.png
|
**/failures/*.png
|
||||||
screenshots/generated/
|
screenshots/generated/raw/
|
10
README.md
|
@ -23,11 +23,11 @@ Without further delay behold my new Lightmeter app inspired by Material You (a.k
|
||||||
# Screenshots
|
# Screenshots
|
||||||
|
|
||||||
<p float="center">
|
<p float="center">
|
||||||
<img src="https://lh3.googleusercontent.com/8Sd-pmNcQ0xAr5opuTeJKWr2OXeQvCoFSdVDSoKQSHHKeNmqF71hqeAdm3yjunY12zY" width="18.8%" />
|
<img src="screenshots/generated/android/android/light_metering-reflected.png" width="18.8%" />
|
||||||
<img src="https://lh3.googleusercontent.com/rqBv8pT0AdcBy0xEgQY2unV-YEQ5KfUkandAxJ62yYCiSF72HClA_tkb4JT_3UPaIfFP" width="18.8%" />
|
<img src="screenshots/generated/android/android/light_timer.png" width="18.8%" />
|
||||||
<img src="https://lh3.googleusercontent.com/-SnYbYSugVfdwYi6m_rd9CzpCZMCIfudhnq0zRIlzEtLSXhrwziWVd2hotygfqiSofI" width="18.8%" />
|
<img src="screenshots/generated/android/android/light_settings.png" width="18.8%" />
|
||||||
<img src="https://lh3.googleusercontent.com/UXxptL_dAIJDtrmpEZuSz39Iq4HuPb3ZPeuANfE9XH0De0uZQT83LNdu1AObBPobpg" width="18.8%" />
|
<img src="screenshots/generated/android/android/light_equipment-profiles.png" width="18.8%" />
|
||||||
<img src="https://lh3.googleusercontent.com/15g_SPV8knDLFbz1_-wGNJFsJeyVWZ_y--TGHpk75MaaIdMDyTXY2_TL-Aw8bpOhpw" width="18.8%" />
|
<img src="screenshots/generated/android/android/dark_metering-reflected.png" width="18.8%" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
# Development
|
# Development
|
||||||
|
|
|
@ -52,7 +52,7 @@ android {
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 33
|
targetSdkVersion 34
|
||||||
ndk {
|
ndk {
|
||||||
debugSymbolLevel 'FULL'
|
debugSymbolLevel 'FULL'
|
||||||
abiFilters 'armeabi-v7a', 'arm64-v8a'
|
abiFilters 'armeabi-v7a', 'arm64-v8a'
|
||||||
|
|
4
assets/release_notes/release_notes_en_0.19.0.md
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
- [Pro] Added a timer for shooting long exposures without leaving the app, just tap the necessary shutter speed in the list.
|
||||||
|
- Long exposure values are no longer limited by 1" and are generated to match the list of aperture values.
|
||||||
|
- Refined the app's color palette & unified icons across the app.
|
||||||
|
- Added release notes dialog.
|
3
assets/release_notes/release_notes_en_0.20.0.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
- Added ISO values up to 25600.
|
||||||
|
- Improved Pro features description.
|
||||||
|
- Made "Restore purchases" option more accessible.
|
|
@ -6,26 +6,19 @@ Lightmeter
|
||||||
|
|
||||||
## Short description
|
## Short description
|
||||||
|
|
||||||
Simple and powerful metering app inspired by Google's Material Design 3.
|
A simple and powerful metering app that can be used for any type of camera from film SLR to pinhole to cinematographic.
|
||||||
|
|
||||||
## Long description
|
## Long description
|
||||||
|
|
||||||
<b>Material Design</b>
|
A simple and easy to use metering app that can be used for any type of camera from film SLR to pinhole to cinematographic. The app contains the following features:
|
||||||
The user interface matches every single detail of the material design guidelines to ensure Lightmeter is an eye candy for you.
|
|
||||||
|
|
||||||
<b>Easy to Use</b>
|
- A reflected light meter with spot metering (using the device's camera)
|
||||||
No complicated or overblown menus but a familiar and clean interface.
|
- An incident light meter (using the device's light sensor)
|
||||||
|
- An in-built timer for shooting long exposures
|
||||||
|
- A wide range of ISO values sutable even for solarphotograpy
|
||||||
|
- Reciprocity calculations for a variety of films
|
||||||
|
|
||||||
<b>Customizability</b>
|
and many more!
|
||||||
There is an inbuilt theme engine with many different colors to choose from.
|
|
||||||
|
|
||||||
<b>Features</b>
|
|
||||||
• Incident light metering (uses lightsensor)
|
|
||||||
• Reflected light metering (needs camera)
|
|
||||||
• ISO range from 3 to 6400
|
|
||||||
• Pre-built reciprocity for some films
|
|
||||||
• Calibration & ND filters
|
|
||||||
and many more
|
|
||||||
|
|
||||||
<b>NOTE</b>
|
<b>NOTE</b>
|
||||||
The accuracy of the measurements depends on your decice's hardware.
|
The accuracy of the measurements depends on your decice's hardware.
|
||||||
|
@ -34,4 +27,4 @@ Email me, if you need help or detected bugs
|
||||||
|
|
||||||
## Graphics
|
## Graphics
|
||||||
|
|
||||||
[Figma](https://www.figma.com/file/X7pUAsxtjx19Rj8VtBV3Ft/Material-lightmeter?node-id=501%3A1586&t=cWNHaXm024KM4KYn-1)
|
[Figma](https://www.figma.com/file/X7pUAsxtjx19Rj8VtBV3Ft/Material-lightmeter?node-id=501%3A1586&t=cWNHaXm024KM4KYn-1)
|
||||||
|
|
|
@ -9,15 +9,18 @@ enum IAPProductType { paidFeatures }
|
||||||
class IAPProduct {
|
class IAPProduct {
|
||||||
final String storeId;
|
final String storeId;
|
||||||
final IAPProductStatus status;
|
final IAPProductStatus status;
|
||||||
|
final String price;
|
||||||
|
|
||||||
const IAPProduct({
|
const IAPProduct({
|
||||||
required this.storeId,
|
required this.storeId,
|
||||||
this.status = IAPProductStatus.purchasable,
|
this.status = IAPProductStatus.purchasable,
|
||||||
|
required this.price,
|
||||||
});
|
});
|
||||||
|
|
||||||
IAPProduct copyWith({IAPProductStatus? status}) => IAPProduct(
|
IAPProduct copyWith({IAPProductStatus? status}) => IAPProduct(
|
||||||
storeId: storeId,
|
storeId: storeId,
|
||||||
status: status ?? this.status,
|
status: status ?? this.status,
|
||||||
|
price: price,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ class IAPProductsProviderState extends State<IAPProductsProvider> {
|
||||||
IAPProduct(
|
IAPProduct(
|
||||||
storeId: IAPProductType.paidFeatures.storeId,
|
storeId: IAPProductType.paidFeatures.storeId,
|
||||||
status: IAPProductStatus.purchased,
|
status: IAPProductStatus.purchased,
|
||||||
|
price: '0.0\$',
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
|
|
|
@ -17,6 +17,7 @@ import 'package:lightmeter/screens/settings/components/shared/dialog_filter/widg
|
||||||
import 'package:lightmeter/screens/settings/components/shared/dialog_range_picker/widget_dialog_picker_range.dart';
|
import 'package:lightmeter/screens/settings/components/shared/dialog_range_picker/widget_dialog_picker_range.dart';
|
||||||
import 'package:lightmeter/screens/settings/screen_settings.dart';
|
import 'package:lightmeter/screens/settings/screen_settings.dart';
|
||||||
import 'package:lightmeter/utils/double_to_zoom.dart';
|
import 'package:lightmeter/utils/double_to_zoom.dart';
|
||||||
|
import 'package:lightmeter/utils/platform_utils.dart';
|
||||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
@ -27,7 +28,7 @@ import 'utils/expectations.dart';
|
||||||
|
|
||||||
@isTest
|
@isTest
|
||||||
void testE2E(String description) {
|
void testE2E(String description) {
|
||||||
setUp(() {
|
setUp(() async {
|
||||||
SharedPreferences.setMockInitialValues({
|
SharedPreferences.setMockInitialValues({
|
||||||
/// Metering values
|
/// Metering values
|
||||||
UserPreferencesService.evSourceTypeKey: EvSourceType.camera.index,
|
UserPreferencesService.evSourceTypeKey: EvSourceType.camera.index,
|
||||||
|
@ -38,6 +39,8 @@ void testE2E(String description) {
|
||||||
MeteringScreenLayoutFeature.filmPicker: true,
|
MeteringScreenLayoutFeature.filmPicker: true,
|
||||||
}.toJson(),
|
}.toJson(),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
UserPreferencesService.seenChangelogVersionKey: await const PlatformUtils().version,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -49,7 +52,7 @@ void testE2E(String description) {
|
||||||
/// Create Praktica + Zenitar profile from scratch
|
/// Create Praktica + Zenitar profile from scratch
|
||||||
await tester.openSettings();
|
await tester.openSettings();
|
||||||
await tester.tapDescendantTextOf<SettingsScreen>(S.current.equipmentProfiles);
|
await tester.tapDescendantTextOf<SettingsScreen>(S.current.equipmentProfiles);
|
||||||
await tester.tap(find.byIcon(Icons.add).first);
|
await tester.tap(find.byIcon(Icons.add_outlined).first);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.setProfileName(mockEquipmentProfiles[0].name);
|
await tester.setProfileName(mockEquipmentProfiles[0].name);
|
||||||
await tester.expandEquipmentProfileContainer(mockEquipmentProfiles[0].name);
|
await tester.expandEquipmentProfileContainer(mockEquipmentProfiles[0].name);
|
||||||
|
@ -63,7 +66,7 @@ void testE2E(String description) {
|
||||||
expect(find.text('1/1000 - B'), findsOneWidget);
|
expect(find.text('1/1000 - B'), findsOneWidget);
|
||||||
|
|
||||||
/// Create Praktica + Jupiter profile from Zenitar profile
|
/// Create Praktica + Jupiter profile from Zenitar profile
|
||||||
await tester.tap(find.byIcon(Icons.copy).first);
|
await tester.tap(find.byIcon(Icons.copy_outlined).first);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.setProfileName(mockEquipmentProfiles[1].name);
|
await tester.setProfileName(mockEquipmentProfiles[1].name);
|
||||||
await tester.expandEquipmentProfileContainer(mockEquipmentProfiles[1].name);
|
await tester.expandEquipmentProfileContainer(mockEquipmentProfiles[1].name);
|
||||||
|
@ -193,7 +196,7 @@ extension on WidgetTester {
|
||||||
bool deselectAll = true,
|
bool deselectAll = true,
|
||||||
}) async {
|
}) async {
|
||||||
if (deselectAll) {
|
if (deselectAll) {
|
||||||
await tap(find.byIcon(Icons.deselect));
|
await tap(find.byIcon(Icons.deselect_outlined));
|
||||||
await pump();
|
await pump();
|
||||||
}
|
}
|
||||||
for (final value in valuesToSelect) {
|
for (final value in valuesToSelect) {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import 'package:lightmeter/screens/metering/components/shared/readings_container
|
||||||
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/extreme_exposure_pairs_container/widget_container_extreme_exposure_pairs.dart';
|
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/extreme_exposure_pairs_container/widget_container_extreme_exposure_pairs.dart';
|
||||||
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/film_picker/widget_picker_film.dart';
|
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/film_picker/widget_picker_film.dart';
|
||||||
import 'package:lightmeter/screens/settings/screen_settings.dart';
|
import 'package:lightmeter/screens/settings/screen_settings.dart';
|
||||||
|
import 'package:lightmeter/utils/platform_utils.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
@ -22,7 +23,7 @@ void testToggleLayoutFeatures(String description) {
|
||||||
group(
|
group(
|
||||||
description,
|
description,
|
||||||
() {
|
() {
|
||||||
setUp(() {
|
setUp(() async {
|
||||||
SharedPreferences.setMockInitialValues({
|
SharedPreferences.setMockInitialValues({
|
||||||
/// Metering values
|
/// Metering values
|
||||||
UserPreferencesService.evSourceTypeKey: EvSourceType.camera.index,
|
UserPreferencesService.evSourceTypeKey: EvSourceType.camera.index,
|
||||||
|
@ -33,6 +34,7 @@ void testToggleLayoutFeatures(String description) {
|
||||||
MeteringScreenLayoutFeature.filmPicker: true,
|
MeteringScreenLayoutFeature.filmPicker: true,
|
||||||
}.toJson(),
|
}.toJson(),
|
||||||
),
|
),
|
||||||
|
UserPreferencesService.seenChangelogVersionKey: await const PlatformUtils().version,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ class MockIAPProductsProviderState extends State<MockIAPProductsProvider> {
|
||||||
IAPProduct(
|
IAPProduct(
|
||||||
storeId: IAPProductType.paidFeatures.storeId,
|
storeId: IAPProductType.paidFeatures.storeId,
|
||||||
status: _purchased ? IAPProductStatus.purchased : IAPProductStatus.purchasable,
|
status: _purchased ? IAPProductStatus.purchased : IAPProductStatus.purchasable,
|
||||||
|
price: '0.0\$',
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
|
|
|
@ -14,6 +14,7 @@ import 'package:lightmeter/screens/metering/components/shared/readings_container
|
||||||
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/nd_picker/widget_picker_nd.dart';
|
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/nd_picker/widget_picker_nd.dart';
|
||||||
import 'package:lightmeter/screens/settings/components/shared/disable/widget_disable.dart';
|
import 'package:lightmeter/screens/settings/components/shared/disable/widget_disable.dart';
|
||||||
import 'package:lightmeter/screens/settings/screen_settings.dart';
|
import 'package:lightmeter/screens/settings/screen_settings.dart';
|
||||||
|
import 'package:lightmeter/utils/platform_utils.dart';
|
||||||
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
@ -38,6 +39,7 @@ void testPurchases(String description) {
|
||||||
MeteringScreenLayoutFeature.filmPicker: true,
|
MeteringScreenLayoutFeature.filmPicker: true,
|
||||||
}.toJson(),
|
}.toJson(),
|
||||||
),
|
),
|
||||||
|
UserPreferencesService.seenChangelogVersionKey: await const PlatformUtils().version,
|
||||||
});
|
});
|
||||||
|
|
||||||
await tester.pumpApplication(productStatus: IAPProductStatus.purchasable);
|
await tester.pumpApplication(productStatus: IAPProductStatus.purchasable);
|
||||||
|
|
|
@ -5,8 +5,10 @@ import 'package:lightmeter/data/models/supported_locale.dart';
|
||||||
import 'package:lightmeter/generated/l10n.dart';
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
import 'package:lightmeter/platform_config.dart';
|
import 'package:lightmeter/platform_config.dart';
|
||||||
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
||||||
|
import 'package:lightmeter/screens/lightmeter_pro/screen_lightmeter_pro.dart';
|
||||||
import 'package:lightmeter/screens/metering/flow_metering.dart';
|
import 'package:lightmeter/screens/metering/flow_metering.dart';
|
||||||
import 'package:lightmeter/screens/settings/flow_settings.dart';
|
import 'package:lightmeter/screens/settings/flow_settings.dart';
|
||||||
|
import 'package:lightmeter/screens/shared/release_notes_dialog/flow_dialog_release_notes.dart';
|
||||||
import 'package:lightmeter/screens/timer/flow_timer.dart';
|
import 'package:lightmeter/screens/timer/flow_timer.dart';
|
||||||
|
|
||||||
class Application extends StatelessWidget {
|
class Application extends StatelessWidget {
|
||||||
|
@ -41,8 +43,9 @@ class Application extends StatelessWidget {
|
||||||
),
|
),
|
||||||
initialRoute: "metering",
|
initialRoute: "metering",
|
||||||
routes: {
|
routes: {
|
||||||
"metering": (_) => const MeteringFlow(),
|
"metering": (_) => const ReleaseNotesFlow(child: MeteringFlow()),
|
||||||
"settings": (_) => const SettingsFlow(),
|
"settings": (_) => const SettingsFlow(),
|
||||||
|
"lightmeterPro": (_) => LightmeterProScreen(),
|
||||||
"timer": (context) => TimerFlow(args: ModalRoute.of(context)!.settings.arguments! as TimerFlowArgs),
|
"timer": (context) => TimerFlow(args: ModalRoute.of(context)!.settings.arguments! as TimerFlowArgs),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
56
lib/data/models/app_feature.dart
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
|
|
||||||
|
enum AppFeature {
|
||||||
|
reflectedLightMetering,
|
||||||
|
incidedntLightMetering,
|
||||||
|
isoAndNdValues,
|
||||||
|
themeEngine,
|
||||||
|
spotMetering,
|
||||||
|
histogram,
|
||||||
|
listOfFilms,
|
||||||
|
equipmentProfiles,
|
||||||
|
timer,
|
||||||
|
mainScreenCustomization;
|
||||||
|
|
||||||
|
static List<AppFeature> get androidFeatures => values;
|
||||||
|
|
||||||
|
static List<AppFeature> get iosFeatures => values.where((f) => f != AppFeature.incidedntLightMetering).toList();
|
||||||
|
|
||||||
|
String name(BuildContext context) {
|
||||||
|
switch (this) {
|
||||||
|
case AppFeature.reflectedLightMetering:
|
||||||
|
return S.of(context).featureReflectedLightMetering;
|
||||||
|
case AppFeature.incidedntLightMetering:
|
||||||
|
return S.of(context).featureIncidentLightMetering;
|
||||||
|
case AppFeature.isoAndNdValues:
|
||||||
|
return S.of(context).featureIsoAndNdValues;
|
||||||
|
case AppFeature.themeEngine:
|
||||||
|
return S.of(context).featureTheme;
|
||||||
|
case AppFeature.spotMetering:
|
||||||
|
return S.of(context).featureSpotMetering;
|
||||||
|
case AppFeature.histogram:
|
||||||
|
return S.of(context).featureHistogram;
|
||||||
|
case AppFeature.listOfFilms:
|
||||||
|
return S.of(context).featureListOfFilms;
|
||||||
|
case AppFeature.equipmentProfiles:
|
||||||
|
return S.of(context).featureEquipmentProfiles;
|
||||||
|
case AppFeature.timer:
|
||||||
|
return S.of(context).featureTimer;
|
||||||
|
case AppFeature.mainScreenCustomization:
|
||||||
|
return S.of(context).featureMeteringScreenLayout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isFree {
|
||||||
|
switch (this) {
|
||||||
|
case AppFeature.reflectedLightMetering:
|
||||||
|
case AppFeature.incidedntLightMetering:
|
||||||
|
case AppFeature.isoAndNdValues:
|
||||||
|
case AppFeature.themeEngine:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,6 +32,8 @@ class UserPreferencesService {
|
||||||
static const primaryColorKey = "primaryColor";
|
static const primaryColorKey = "primaryColor";
|
||||||
static const dynamicColorKey = "dynamicColor";
|
static const dynamicColorKey = "dynamicColor";
|
||||||
|
|
||||||
|
static const seenChangelogVersionKey = "seenChangelogVersion";
|
||||||
|
|
||||||
final SharedPreferences _sharedPreferences;
|
final SharedPreferences _sharedPreferences;
|
||||||
|
|
||||||
UserPreferencesService(this._sharedPreferences) {
|
UserPreferencesService(this._sharedPreferences) {
|
||||||
|
@ -157,4 +159,7 @@ class UserPreferencesService {
|
||||||
|
|
||||||
bool get dynamicColor => _sharedPreferences.getBool(dynamicColorKey) ?? false;
|
bool get dynamicColor => _sharedPreferences.getBool(dynamicColorKey) ?? false;
|
||||||
set dynamicColor(bool value) => _sharedPreferences.setBool(dynamicColorKey, value);
|
set dynamicColor(bool value) => _sharedPreferences.setBool(dynamicColorKey, value);
|
||||||
|
|
||||||
|
String get seenChangelogVersion => _sharedPreferences.getString(seenChangelogVersionKey) ?? '';
|
||||||
|
set seenChangelogVersion(String value) => _sharedPreferences.setString(seenChangelogVersionKey, value);
|
||||||
}
|
}
|
||||||
|
|
|
@ -103,10 +103,31 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"proFeatures": "Pro features",
|
"proFeaturesTitle": "Lightmeter Pro",
|
||||||
"unlockProFeatures": "Unlock Pro features",
|
"getPro": "Get Pro",
|
||||||
"unlockProFeaturesDescription": "Unlock professional features:\n \u2022 Equipment profiles containing filters for aperture, shutter speed, and more\n \u2022 List of films with compensation for what's known as reciprocity failure\n \u2022 Spot metering & Histogram\n \u2022 And more!\n\nBy unlocking Pro features you support the development and make it possible to add new features to the app.",
|
"featuresFree": "Free",
|
||||||
"unlock": "Unlock",
|
"featuresPro": "Pro",
|
||||||
|
"proFeaturesPromoText": "Lightmeter Pro delivers everything you need to get the best shots!",
|
||||||
|
"proFeaturesWhatsIncluded": "What's included?",
|
||||||
|
"featureReflectedLightMetering": "Reflected light metering",
|
||||||
|
"featureIncidentLightMetering": "Incident light metering",
|
||||||
|
"featureIsoAndNdValues": "Wide range of ISO and ND filters values",
|
||||||
|
"featureTheme": "Theme customization",
|
||||||
|
"featureSpotMetering": "Spot metering",
|
||||||
|
"featureHistogram": "Histogram",
|
||||||
|
"featureListOfFilms": "List of 20+ films with reciprocity formulas",
|
||||||
|
"featureEquipmentProfiles": "Equipment profiles",
|
||||||
|
"featureTimer": "Built-in timer for long exposure",
|
||||||
|
"featureMeteringScreenLayout": "Customizable main screen",
|
||||||
|
"proFeaturesSupportText": "By purchasing Lightmeter Pro you support the development and make it possible to add new features to the app.",
|
||||||
|
"getNowFor": "Get now for {price}",
|
||||||
|
"@getNowFor": {
|
||||||
|
"price": {
|
||||||
|
"version": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"tooltipAdd": "Add",
|
"tooltipAdd": "Add",
|
||||||
"tooltipClose": "Close",
|
"tooltipClose": "Close",
|
||||||
"tooltipExpand": "Expand",
|
"tooltipExpand": "Expand",
|
||||||
|
@ -119,5 +140,15 @@
|
||||||
"tooltipUseLightSensor": "Use lightsensor",
|
"tooltipUseLightSensor": "Use lightsensor",
|
||||||
"tooltipUseCamera": "Use camera",
|
"tooltipUseCamera": "Use camera",
|
||||||
"tooltipOpenSettings": "Open settings",
|
"tooltipOpenSettings": "Open settings",
|
||||||
"exposurePair": "Exposure pair"
|
"exposurePair": "Exposure pair",
|
||||||
|
"whatsnew": "What's new?",
|
||||||
|
"changesInVersion": "Changes in version {version}:",
|
||||||
|
"@changesInVersion": {
|
||||||
|
"placeholders": {
|
||||||
|
"version": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"close": "Close"
|
||||||
}
|
}
|
|
@ -103,10 +103,32 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"proFeatures": "Fonctionnalités professionnelles",
|
"proFeaturesTitle": "Lightmeter Pro",
|
||||||
"unlockProFeatures": "Déverrouiller les fonctionnalités professionnelles",
|
"getPro": "Acheter Pro",
|
||||||
"unlockProFeaturesDescription": "Déverrouillez des fonctions professionnelles:\n \u2022 Profils d'équipement contenant des filtres pour l'ouverture, la vitesse d'obturation et plus encore, ainsi qu'une liste de films avec compensation pour ce que l'on appelle l'échec de réciprocité\n \u2022 Mesure spot & Histogramme\n \u2022 Et plus encore!\n\nEn débloquant les fonctionnalités Pro, vous soutenez le développement et permettez d'ajouter de nouvelles fonctionnalités à l'application.",
|
|
||||||
"unlock": "Déverrouiller",
|
"unlock": "Déverrouiller",
|
||||||
|
"featuresFree": "Gratuit",
|
||||||
|
"featuresPro": "Pro",
|
||||||
|
"proFeaturesPromoText": "Lightmeter Pro offre tout ce dont vous avez besoin pour obtenir les meilleurs clichés!",
|
||||||
|
"proFeaturesWhatsIncluded": "Qu'est-ce qui est inclus?",
|
||||||
|
"featureReflectedLightMetering": "Mesure de la lumière réfléchie",
|
||||||
|
"featureIncidentLightMetering": "Mesure de la lumière incidente",
|
||||||
|
"featureIsoAndNdValues": "Large gamme de valeurs ISO et de filtres ND",
|
||||||
|
"featureTheme": "Personnalisation du thème",
|
||||||
|
"featureSpotMetering": "Mesure spot",
|
||||||
|
"featureHistogram": "Histogramme",
|
||||||
|
"featureListOfFilms": "Liste de plus de 20 films avec des formules de correction",
|
||||||
|
"featureEquipmentProfiles": "Profils de l'équipement",
|
||||||
|
"featureTimer": "Minuteur intégré pour longues expositions",
|
||||||
|
"featureMeteringScreenLayout": "Écran principal personnalisable",
|
||||||
|
"proFeaturesSupportText": "En achetant Lightmeter Pro, vous soutenez le développement et permettez l'ajout de nouvelles fonctionnalités à l'application.",
|
||||||
|
"getNowFor": "Acheter maintenant {price}",
|
||||||
|
"@getNowFor": {
|
||||||
|
"price": {
|
||||||
|
"version": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"tooltipAdd": "Ajouter",
|
"tooltipAdd": "Ajouter",
|
||||||
"tooltipClose": "Fermer",
|
"tooltipClose": "Fermer",
|
||||||
"tooltipExpand": "Élargir",
|
"tooltipExpand": "Élargir",
|
||||||
|
|
|
@ -67,8 +67,8 @@
|
||||||
"equipmentProfile": "Оборудование",
|
"equipmentProfile": "Оборудование",
|
||||||
"equipmentProfiles": "Профили оборудования",
|
"equipmentProfiles": "Профили оборудования",
|
||||||
"tapToAdd": "Нажмите, чтобы добавить",
|
"tapToAdd": "Нажмите, чтобы добавить",
|
||||||
"filmsInUse": "Используемые пленки",
|
"filmsInUse": "Используемые плёнки",
|
||||||
"filmsInUseDescription": "Выберите пленки, которыми вы пользуетесь.",
|
"filmsInUseDescription": "Выберите плёнки, которыми вы пользуетесь.",
|
||||||
"general": "Общие",
|
"general": "Общие",
|
||||||
"keepScreenOn": "Запрет блокировки",
|
"keepScreenOn": "Запрет блокировки",
|
||||||
"haptics": "Вибрация",
|
"haptics": "Вибрация",
|
||||||
|
@ -103,10 +103,31 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"proFeatures": "Профессиональные настройки",
|
"proFeaturesTitle": "Lightmeter Pro",
|
||||||
"unlockProFeatures": "Разблокировать профессиональные настройки",
|
"getPro": "Купить Pro",
|
||||||
"unlockProFeaturesDescription": "Вы можете разблокировать профессиональные настройки:\n \u2022 Профили оборудования, содержащие фильтры для диафрагмы, выдержки и других значений\n \u2022 Список пленок с компенсацией эффекта Шварцшильда\n \u2022 Точечный замер и гистограмма\n \u2022 И другие возможности!\n\nПолучая доступ к профессиональным настройкам, вы поддерживаете разработку и делаете возможным появление новых функций в приложении.",
|
"featuresFree": "Бесплатно",
|
||||||
"unlock": "Разблокировать",
|
"featuresPro": "Pro",
|
||||||
|
"proFeaturesPromoText": "Lightmeter Pro предоставляет все необходимое для получения лучших снимков!",
|
||||||
|
"proFeaturesWhatsIncluded": "Что включено?",
|
||||||
|
"featureReflectedLightMetering": "Замер отраженного света",
|
||||||
|
"featureIncidentLightMetering": "Замер падающего света",
|
||||||
|
"featureIsoAndNdValues": "Широкий диапазон значений ISO и фильтров ND",
|
||||||
|
"featureTheme": "Настройка темы",
|
||||||
|
"featureSpotMetering": "Точечный замер",
|
||||||
|
"featureHistogram": "Гистограмма",
|
||||||
|
"featureListOfFilms": "Список из 20+ плёнок с формулами коррекции",
|
||||||
|
"featureEquipmentProfiles": "Профили оборудования",
|
||||||
|
"featureTimer": "Встроенный таймер для длинных выдержек",
|
||||||
|
"featureMeteringScreenLayout": "Настраиваемый главный экран",
|
||||||
|
"proFeaturesSupportText": "Покупая Lightmeter Pro, вы поддерживаете разработку и делаете возможным добавление новых функций в приложение.",
|
||||||
|
"getNowFor": "Купить за {price}",
|
||||||
|
"@getNowFor": {
|
||||||
|
"price": {
|
||||||
|
"version": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"tooltipAdd": "Добавить",
|
"tooltipAdd": "Добавить",
|
||||||
"tooltipClose": "Закрыть",
|
"tooltipClose": "Закрыть",
|
||||||
"tooltipExpand": "Развернуть",
|
"tooltipExpand": "Развернуть",
|
||||||
|
|
|
@ -103,10 +103,30 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"proFeatures": "专业功能",
|
"getPro": "购买专业版",
|
||||||
"unlockProFeatures": "解锁专业功能",
|
"featuresFree": "免费",
|
||||||
"unlockProFeaturesDescription": "\n \u2022 配置文件,其中包含光圈、快门速度等参数\n \u2022 胶卷列表,对胶片倒易率失效进行曝光补偿\n \u2022 点测光和直方图\n \u2022 和更多\n\n通过解锁专业版功能,您可以支持开发工作,帮助为应用程序添加新功能。",
|
"featuresPro": "专业版",
|
||||||
"unlock": "解锁",
|
"proFeaturesPromoText": "Lightmeter Pro 提供您需要的一切,助您拍出最佳照片!",
|
||||||
|
"proFeaturesWhatsIncluded": "包括哪些内容?",
|
||||||
|
"featureReflectedLightMetering": "反射光测光",
|
||||||
|
"featureIncidentLightMetering": "入射光测光",
|
||||||
|
"featureIsoAndNdValues": "广泛的ISO和ND滤镜值范围",
|
||||||
|
"featureTheme": "主题自定义",
|
||||||
|
"featureSpotMetering": "点测光",
|
||||||
|
"featureHistogram": "直方图",
|
||||||
|
"featureListOfFilms": "20多部电影的修正公式列表",
|
||||||
|
"featureEquipmentProfiles": "设备配置文件",
|
||||||
|
"featureTimer": "内置长曝光计时器",
|
||||||
|
"featureMeteringScreenLayout": "可自定义的主屏幕",
|
||||||
|
"proFeaturesSupportText": "通过购买Lightmeter Pro,您支持开发工作,并使添加新功能成为可能。",
|
||||||
|
"getNowFor": "立即获取 {price}",
|
||||||
|
"@getNowFor": {
|
||||||
|
"price": {
|
||||||
|
"version": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"tooltipAdd": "添加",
|
"tooltipAdd": "添加",
|
||||||
"tooltipClose": "关闭",
|
"tooltipClose": "关闭",
|
||||||
"tooltipExpand": "展开",
|
"tooltipExpand": "展开",
|
||||||
|
|
|
@ -68,4 +68,7 @@ class Dimens {
|
||||||
paddingL,
|
paddingL,
|
||||||
);
|
);
|
||||||
static const EdgeInsets dialogMargin = EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0);
|
static const EdgeInsets dialogMargin = EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0);
|
||||||
|
|
||||||
|
// TODO(@vodemn) constrain dialogs with this value
|
||||||
|
static const double tabletMaxWidth = 600;
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,12 @@ Future<void> runLightmeterApp(Environment env) async {
|
||||||
runApp(
|
runApp(
|
||||||
env.buildType == BuildType.dev
|
env.buildType == BuildType.dev
|
||||||
? IAPProducts(
|
? IAPProducts(
|
||||||
products: [IAPProduct(storeId: IAPProductType.paidFeatures.storeId)],
|
products: [
|
||||||
|
IAPProduct(
|
||||||
|
storeId: IAPProductType.paidFeatures.storeId,
|
||||||
|
price: '0.0\$',
|
||||||
|
),
|
||||||
|
],
|
||||||
child: application,
|
child: application,
|
||||||
)
|
)
|
||||||
: IAPProductsProvider(
|
: IAPProductsProvider(
|
||||||
|
|
229
lib/screens/lightmeter_pro/screen_lightmeter_pro.dart
Normal file
|
@ -0,0 +1,229 @@
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:lightmeter/data/models/app_feature.dart';
|
||||||
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
|
import 'package:lightmeter/providers/services_provider.dart';
|
||||||
|
import 'package:lightmeter/res/dimens.dart';
|
||||||
|
import 'package:lightmeter/res/theme.dart';
|
||||||
|
import 'package:lightmeter/screens/shared/sliver_screen/screen_sliver.dart';
|
||||||
|
import 'package:lightmeter/utils/text_height.dart';
|
||||||
|
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
||||||
|
|
||||||
|
class LightmeterProScreen extends StatelessWidget {
|
||||||
|
final features =
|
||||||
|
defaultTargetPlatform == TargetPlatform.android ? AppFeature.androidFeatures : AppFeature.iosFeatures;
|
||||||
|
|
||||||
|
LightmeterProScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: SliverScreen(
|
||||||
|
title: S.of(context).proFeaturesTitle,
|
||||||
|
slivers: [
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(Dimens.paddingM),
|
||||||
|
child: Text(
|
||||||
|
S.of(context).proFeaturesPromoText,
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(
|
||||||
|
Dimens.paddingM,
|
||||||
|
0,
|
||||||
|
Dimens.paddingM,
|
||||||
|
Dimens.paddingS,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
S.of(context).proFeaturesWhatsIncluded,
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SliverToBoxAdapter(child: _FeaturesHeader()),
|
||||||
|
SliverList.separated(
|
||||||
|
itemCount: features.length,
|
||||||
|
itemBuilder: (_, index) => _FeatureItem(feature: features[index]),
|
||||||
|
separatorBuilder: (_, __) => const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: Dimens.paddingM),
|
||||||
|
child: Divider(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(Dimens.paddingM),
|
||||||
|
child: Text(S.of(context).proFeaturesSupportText),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceElevated1,
|
||||||
|
width: MediaQuery.sizeOf(context).width,
|
||||||
|
padding: EdgeInsets.fromLTRB(
|
||||||
|
Dimens.paddingM,
|
||||||
|
Dimens.paddingM,
|
||||||
|
Dimens.paddingM,
|
||||||
|
Dimens.paddingM + MediaQuery.paddingOf(context).bottom,
|
||||||
|
),
|
||||||
|
child: FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
ServicesProvider.maybeOf(context)
|
||||||
|
?.analytics
|
||||||
|
.setCustomKey('iap_product_type', IAPProductType.paidFeatures.storeId);
|
||||||
|
IAPProductsProvider.maybeOf(context)?.buy(IAPProductType.paidFeatures);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: Text(S.of(context).getNowFor(IAPProducts.productOf(context, IAPProductType.paidFeatures)!.price)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FeaturesHeader extends StatelessWidget {
|
||||||
|
const _FeaturesHeader();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Spacer(),
|
||||||
|
_FeatureHighlight(child: Text(S.of(context).featuresFree)),
|
||||||
|
_FeatureHighlight(
|
||||||
|
roundedTop: true,
|
||||||
|
highlight: true,
|
||||||
|
child: Text(
|
||||||
|
S.of(context).featuresPro,
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyMedium!
|
||||||
|
.copyWith(color: Theme.of(context).colorScheme.onSecondaryContainer),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FeatureItem extends StatelessWidget {
|
||||||
|
final AppFeature feature;
|
||||||
|
|
||||||
|
const _FeatureItem({
|
||||||
|
required this.feature,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(minHeight: Dimens.grid48),
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: Dimens.paddingM,
|
||||||
|
vertical: Dimens.paddingS,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
feature.name(context),
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Opacity(
|
||||||
|
opacity: feature.isFree ? 1 : 0,
|
||||||
|
child: const _FeatureHighlight(
|
||||||
|
child: _CheckBox(highlight: false),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_FeatureHighlight(
|
||||||
|
highlight: true,
|
||||||
|
roundedBottom: feature == AppFeature.values.last,
|
||||||
|
child: const _CheckBox(highlight: true),
|
||||||
|
),
|
||||||
|
const SizedBox(width: Dimens.grid16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FeatureHighlight extends StatelessWidget {
|
||||||
|
final bool highlight;
|
||||||
|
final bool roundedTop;
|
||||||
|
final bool roundedBottom;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const _FeatureHighlight({
|
||||||
|
this.highlight = false,
|
||||||
|
this.roundedTop = false,
|
||||||
|
this.roundedBottom = false,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minWidth: textSize(
|
||||||
|
highlight ? S.of(context).featuresPro : S.of(context).featuresFree,
|
||||||
|
Theme.of(context).textTheme.bodyMedium,
|
||||||
|
MediaQuery.sizeOf(context).width,
|
||||||
|
).width +
|
||||||
|
Dimens.paddingM * 2,
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: Dimens.paddingM,
|
||||||
|
vertical: Dimens.paddingS,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: highlight ? Theme.of(context).colorScheme.secondaryContainer : null,
|
||||||
|
borderRadius: roundedTop
|
||||||
|
? const BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(Dimens.borderRadiusM),
|
||||||
|
topRight: Radius.circular(Dimens.borderRadiusM),
|
||||||
|
)
|
||||||
|
: roundedBottom
|
||||||
|
? const BorderRadius.only(
|
||||||
|
bottomLeft: Radius.circular(Dimens.borderRadiusM),
|
||||||
|
bottomRight: Radius.circular(Dimens.borderRadiusM),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CheckBox extends StatelessWidget {
|
||||||
|
final bool highlight;
|
||||||
|
|
||||||
|
const _CheckBox({required this.highlight});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Icon(
|
||||||
|
Icons.check_outlined,
|
||||||
|
color: highlight ? Theme.of(context).colorScheme.onSecondaryContainer : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -76,28 +76,31 @@ class CameraContainer extends StatelessWidget {
|
||||||
height: min(meteringContainerHeight, cameraPreviewHeight) + Dimens.paddingM * 2,
|
height: min(meteringContainerHeight, cameraPreviewHeight) + Dimens.paddingM * 2,
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Padding(
|
child: Row(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
|
children: [
|
||||||
child: Row(
|
Expanded(
|
||||||
children: [
|
child: Padding(
|
||||||
Expanded(
|
padding: EdgeInsets.only(
|
||||||
child: Padding(
|
left: Dimens.paddingM - Dimens.grid8,
|
||||||
padding: EdgeInsets.only(top: topBarOverflow >= 0 ? topBarOverflow : 0),
|
top: topBarOverflow >= 0 ? topBarOverflow : 0,
|
||||||
child: ExposurePairsList(
|
),
|
||||||
exposurePairs,
|
child: ExposurePairsList(
|
||||||
onExposurePairTap: onExposurePairTap,
|
exposurePairs,
|
||||||
),
|
onExposurePairTap: onExposurePairTap,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: Dimens.grid8),
|
),
|
||||||
Expanded(
|
SizedBox(
|
||||||
child: Padding(
|
width: MediaQuery.sizeOf(context).width / 2 - Dimens.grid4,
|
||||||
padding: EdgeInsets.only(top: topBarOverflow <= 0 ? -topBarOverflow : 0),
|
child: Padding(
|
||||||
child: const _CameraControlsBuilder(),
|
padding: EdgeInsets.only(
|
||||||
|
top: topBarOverflow <= 0 ? -topBarOverflow : 0,
|
||||||
|
right: Dimens.paddingM,
|
||||||
),
|
),
|
||||||
|
child: const _CameraControlsBuilder(),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:auto_size_text/auto_size_text.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:lightmeter/res/dimens.dart';
|
import 'package:lightmeter/res/dimens.dart';
|
||||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||||
|
@ -15,22 +16,9 @@ class ExposurePairsListItem<T extends PhotographyStopValue> extends StatelessWid
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final List<Widget> rowChildren = [
|
final List<Widget> rowChildren = [
|
||||||
Flexible(
|
_Title(value),
|
||||||
child: Text(
|
|
||||||
value.toString(),
|
|
||||||
style: labelTextStyle(context).copyWith(color: Theme.of(context).colorScheme.onBackground),
|
|
||||||
softWrap: false,
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: Dimens.grid8),
|
const SizedBox(width: Dimens.grid8),
|
||||||
ColoredBox(
|
if (value.stopType == StopType.full) const _Tick.full() else const _Tick.short(),
|
||||||
color: Theme.of(context).colorScheme.onBackground,
|
|
||||||
child: SizedBox(
|
|
||||||
height: 1,
|
|
||||||
width: tickLength(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (value.stopType != StopType.full) const SizedBox(width: Dimens.grid4),
|
if (value.stopType != StopType.full) const SizedBox(width: Dimens.grid4),
|
||||||
];
|
];
|
||||||
return Row(
|
return Row(
|
||||||
|
@ -59,3 +47,61 @@ class ExposurePairsListItem<T extends PhotographyStopValue> extends StatelessWid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _Title<T extends PhotographyStopValue> extends StatelessWidget {
|
||||||
|
final T value;
|
||||||
|
late final String _title = value.toString();
|
||||||
|
|
||||||
|
_Title(this.value, {super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Flexible(
|
||||||
|
child: _title.length > 5 // downsize text only for long values like 1/4000
|
||||||
|
? AutoSizeText(
|
||||||
|
value.toString(),
|
||||||
|
stepGranularity: 0.5,
|
||||||
|
minFontSize: 10,
|
||||||
|
style: labelTextStyle(context).copyWith(color: Theme.of(context).colorScheme.onBackground),
|
||||||
|
softWrap: false,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
maxLines: 1,
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
value.toString(),
|
||||||
|
style: labelTextStyle(context).copyWith(color: Theme.of(context).colorScheme.onBackground),
|
||||||
|
softWrap: false,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextStyle labelTextStyle(BuildContext context) {
|
||||||
|
switch (value.stopType) {
|
||||||
|
case StopType.full:
|
||||||
|
return Theme.of(context).textTheme.bodyLarge!;
|
||||||
|
case StopType.half:
|
||||||
|
case StopType.third:
|
||||||
|
return Theme.of(context).textTheme.bodyMedium!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Tick extends StatelessWidget {
|
||||||
|
final double _length;
|
||||||
|
|
||||||
|
const _Tick.full() : _length = Dimens.grid16;
|
||||||
|
const _Tick.short() : _length = Dimens.grid8;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ColoredBox(
|
||||||
|
color: Theme.of(context).colorScheme.onBackground,
|
||||||
|
child: SizedBox(
|
||||||
|
height: 1,
|
||||||
|
width: _length,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,27 +1,26 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:lightmeter/generated/l10n.dart';
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/animated_dialog/widget_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:lightmeter/screens/shared/pro_features_dialog/widget_dialog_pro_features.dart';
|
|
||||||
|
|
||||||
class LightmeterProAnimatedDialog extends StatelessWidget {
|
class LightmeterProAnimatedDialog extends StatelessWidget {
|
||||||
const LightmeterProAnimatedDialog({super.key});
|
const LightmeterProAnimatedDialog({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AnimatedDialog(
|
return GestureDetector(
|
||||||
closedChild: ReadingValueContainer(
|
onTap: () {
|
||||||
color: Theme.of(context).colorScheme.errorContainer,
|
Navigator.of(context).pushNamed("lightmeterPro");
|
||||||
textColor: Theme.of(context).colorScheme.onErrorContainer,
|
},
|
||||||
|
child: ReadingValueContainer(
|
||||||
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
|
textColor: Theme.of(context).colorScheme.onSecondary,
|
||||||
values: [
|
values: [
|
||||||
ReadingValue(
|
ReadingValue(
|
||||||
label: S.of(context).proFeatures,
|
label: S.of(context).proFeaturesTitle,
|
||||||
value: S.of(context).unlock,
|
value: S.of(context).getPro,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
openedChild: const ProFeaturesDialog(),
|
|
||||||
openedSize: Size.fromHeight(const ProFeaturesDialog().height(context)),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,25 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:lightmeter/generated/l10n.dart';
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:lightmeter/screens/shared/release_notes_dialog/widget_dialog_release_notes.dart';
|
||||||
|
import 'package:lightmeter/utils/platform_utils.dart';
|
||||||
|
|
||||||
class VersionListTile extends StatelessWidget {
|
class VersionListTile extends StatelessWidget {
|
||||||
const VersionListTile({super.key});
|
const VersionListTile({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListTile(
|
return FutureBuilder<({String buildNumber, String version})>(
|
||||||
leading: const Icon(Icons.info_outline),
|
future: const PlatformUtils().buildVersion,
|
||||||
title: Text(S.of(context).version),
|
builder: (context, snapshot) => ListTile(
|
||||||
trailing: FutureBuilder<PackageInfo>(
|
leading: const Icon(Icons.info_outline),
|
||||||
future: PackageInfo.fromPlatform(),
|
title: Text(S.of(context).version),
|
||||||
builder: (context, snapshot) => snapshot.data != null
|
onTap: snapshot.data != null
|
||||||
? Text(S.of(context).versionNumber(snapshot.data!.version, snapshot.data!.buildNumber))
|
? () => showDialog(
|
||||||
: const SizedBox.shrink(),
|
context: context,
|
||||||
|
builder: (_) => ReleaseNotesDialog(version: snapshot.data!.version),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
trailing: Text(S.of(context).versionNumber(snapshot.data?.version ?? '', snapshot.data?.buildNumber ?? '')),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:lightmeter/generated/l10n.dart';
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
import 'package:lightmeter/screens/settings/components/about/components/report_issue/widget_list_tile_report_issue.dart';
|
import 'package:lightmeter/screens/settings/components/about/components/report_issue/widget_list_tile_report_issue.dart';
|
||||||
import 'package:lightmeter/screens/settings/components/about/components/restore_purchases/widget_list_tile_restore_purchases.dart';
|
|
||||||
import 'package:lightmeter/screens/settings/components/about/components/source_code/widget_list_tile_source_code.dart';
|
import 'package:lightmeter/screens/settings/components/about/components/source_code/widget_list_tile_source_code.dart';
|
||||||
import 'package:lightmeter/screens/settings/components/about/components/version/widget_list_tile_version.dart';
|
import 'package:lightmeter/screens/settings/components/about/components/version/widget_list_tile_version.dart';
|
||||||
import 'package:lightmeter/screens/settings/components/about/components/write_email/widget_list_tile_write_email.dart';
|
import 'package:lightmeter/screens/settings/components/about/components/write_email/widget_list_tile_write_email.dart';
|
||||||
import 'package:lightmeter/screens/settings/components/shared/settings_section/widget_settings_section.dart';
|
import 'package:lightmeter/screens/settings/components/shared/settings_section/widget_settings_section.dart';
|
||||||
import 'package:lightmeter/utils/context_utils.dart';
|
|
||||||
|
|
||||||
class AboutSettingsSection extends StatelessWidget {
|
class AboutSettingsSection extends StatelessWidget {
|
||||||
const AboutSettingsSection({super.key});
|
const AboutSettingsSection({super.key});
|
||||||
|
@ -15,12 +13,11 @@ class AboutSettingsSection extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SettingsSection(
|
return SettingsSection(
|
||||||
title: S.of(context).about,
|
title: S.of(context).about,
|
||||||
children: [
|
children: const [
|
||||||
const SourceCodeListTile(),
|
SourceCodeListTile(),
|
||||||
if (!context.isPro) const RestorePurchasesListTile(),
|
ReportIssueListTile(),
|
||||||
const ReportIssueListTile(),
|
WriteEmailListTile(),
|
||||||
const WriteEmailListTile(),
|
VersionListTile(),
|
||||||
const VersionListTile(),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:lightmeter/generated/l10n.dart';
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
import 'package:lightmeter/res/dimens.dart';
|
import 'package:lightmeter/res/dimens.dart';
|
||||||
import 'package:lightmeter/screens/shared/pro_features_dialog/widget_dialog_pro_features.dart';
|
|
||||||
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
||||||
|
|
||||||
class BuyProListTile extends StatelessWidget {
|
class BuyProListTile extends StatelessWidget {
|
||||||
|
@ -12,14 +11,11 @@ class BuyProListTile extends StatelessWidget {
|
||||||
final status = IAPProducts.productOf(context, IAPProductType.paidFeatures)?.status;
|
final status = IAPProducts.productOf(context, IAPProductType.paidFeatures)?.status;
|
||||||
final isPending = status == IAPProductStatus.purchased || status == null;
|
final isPending = status == IAPProductStatus.purchased || status == null;
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: const Icon(Icons.star_outlined),
|
leading: const Icon(Icons.bolt),
|
||||||
title: Text(S.of(context).unlockProFeatures),
|
title: Text(S.of(context).getPro),
|
||||||
onTap: !isPending
|
onTap: !isPending
|
||||||
? () {
|
? () {
|
||||||
showDialog(
|
Navigator.of(context).pushNamed("lightmeterPro");
|
||||||
context: context,
|
|
||||||
builder: (_) => const Dialog(child: ProFeaturesDialog()),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
trailing: isPending
|
trailing: isPending
|
||||||
|
|
|
@ -10,7 +10,7 @@ class RestorePurchasesListTile extends StatelessWidget {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: const Icon(Icons.restore_outlined),
|
leading: const Icon(Icons.restore_outlined),
|
||||||
title: Text(S.of(context).restorePurchases),
|
title: Text(S.of(context).restorePurchases),
|
||||||
onTap: IAPProductsProvider.of(context).restorePurchases,
|
onTap: IAPProductsProvider.maybeOf(context)?.restorePurchases,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:lightmeter/generated/l10n.dart';
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
import 'package:lightmeter/screens/settings/components/lightmeter_pro/components/buy_pro/widget_list_tile_buy_pro.dart';
|
import 'package:lightmeter/screens/settings/components/lightmeter_pro/components/buy_pro/widget_list_tile_buy_pro.dart';
|
||||||
|
import 'package:lightmeter/screens/settings/components/lightmeter_pro/components/restore_purchases/widget_list_tile_restore_purchases.dart';
|
||||||
import 'package:lightmeter/screens/settings/components/shared/settings_section/widget_settings_section.dart';
|
import 'package:lightmeter/screens/settings/components/shared/settings_section/widget_settings_section.dart';
|
||||||
|
|
||||||
class LightmeterProSettingsSection extends StatelessWidget {
|
class LightmeterProSettingsSection extends StatelessWidget {
|
||||||
|
@ -9,8 +10,13 @@ class LightmeterProSettingsSection extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SettingsSection(
|
return SettingsSection(
|
||||||
title: S.of(context).proFeatures,
|
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||||
children: const [BuyProListTile()],
|
foregroundColor: Theme.of(context).colorScheme.onSecondary,
|
||||||
|
title: S.of(context).proFeaturesTitle,
|
||||||
|
children: const [
|
||||||
|
BuyProListTile(),
|
||||||
|
RestorePurchasesListTile(),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,14 @@ import 'package:lightmeter/res/dimens.dart';
|
||||||
class SettingsSection extends StatelessWidget {
|
class SettingsSection extends StatelessWidget {
|
||||||
final String title;
|
final String title;
|
||||||
final List<Widget> children;
|
final List<Widget> children;
|
||||||
|
final Color? backgroundColor;
|
||||||
|
final Color? foregroundColor;
|
||||||
|
|
||||||
const SettingsSection({
|
const SettingsSection({
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.children,
|
required this.children,
|
||||||
|
this.backgroundColor,
|
||||||
|
this.foregroundColor,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -21,24 +25,33 @@ class SettingsSection extends StatelessWidget {
|
||||||
Dimens.paddingM,
|
Dimens.paddingM,
|
||||||
),
|
),
|
||||||
child: Card(
|
child: Card(
|
||||||
|
color: backgroundColor,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: Dimens.paddingM),
|
padding: const EdgeInsets.symmetric(vertical: Dimens.paddingM),
|
||||||
child: Column(
|
child: Theme(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
data: Theme.of(context).copyWith(
|
||||||
mainAxisSize: MainAxisSize.min,
|
listTileTheme: Theme.of(context).listTileTheme.copyWith(
|
||||||
children: [
|
iconColor: foregroundColor,
|
||||||
Padding(
|
textColor: foregroundColor,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
|
),
|
||||||
child: Text(
|
),
|
||||||
title,
|
child: Column(
|
||||||
style: Theme.of(context)
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
.textTheme
|
mainAxisSize: MainAxisSize.min,
|
||||||
.labelLarge
|
children: [
|
||||||
?.copyWith(color: Theme.of(context).colorScheme.onSurface),
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.labelLarge
|
||||||
|
?.copyWith(color: foregroundColor ?? Theme.of(context).colorScheme.onSurface),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
...children,
|
||||||
...children,
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:lightmeter/generated/l10n.dart';
|
|
||||||
import 'package:lightmeter/providers/services_provider.dart';
|
|
||||||
import 'package:lightmeter/res/dimens.dart';
|
|
||||||
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart';
|
|
||||||
import 'package:lightmeter/screens/shared/transparent_dialog/widget_dialog_transparent.dart';
|
|
||||||
import 'package:lightmeter/utils/text_height.dart';
|
|
||||||
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
|
||||||
|
|
||||||
class ProFeaturesDialog extends StatelessWidget {
|
|
||||||
const ProFeaturesDialog({super.key});
|
|
||||||
|
|
||||||
double height(BuildContext context) => TransparentDialog.height(
|
|
||||||
context,
|
|
||||||
title: S.of(context).proFeatures,
|
|
||||||
contextHeight: dialogTextHeight(
|
|
||||||
context,
|
|
||||||
S.of(context).unlockProFeaturesDescription,
|
|
||||||
Theme.of(context).textTheme.bodyMedium,
|
|
||||||
Dimens.paddingL * 2,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return TransparentDialog(
|
|
||||||
icon: Icons.star_outlined,
|
|
||||||
title: S.of(context).proFeatures,
|
|
||||||
scrollableContent: false,
|
|
||||||
content: Flexible(
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingL),
|
|
||||||
child: Text(
|
|
||||||
S.of(context).unlockProFeaturesDescription,
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => _close(context),
|
|
||||||
child: Text(S.of(context).cancel),
|
|
||||||
),
|
|
||||||
FilledButton(
|
|
||||||
onPressed: () {
|
|
||||||
_close(context).then((_) {
|
|
||||||
ServicesProvider.maybeOf(context)
|
|
||||||
?.analytics
|
|
||||||
.setCustomKey('iap_product_type', IAPProductType.paidFeatures.storeId);
|
|
||||||
IAPProductsProvider.maybeOf(context)?.buy(IAPProductType.paidFeatures);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
child: Text(S.of(context).unlock),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _close(BuildContext context) async => AnimatedDialog.maybeClose(context) ?? Navigator.of(context).pop();
|
|
||||||
}
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||||
|
import 'package:lightmeter/screens/shared/release_notes_dialog/state_release_notes.dart';
|
||||||
|
import 'package:lightmeter/utils/platform_utils.dart';
|
||||||
|
|
||||||
|
class ReleaseNotesBloc extends Cubit<ReleaseNotesState> {
|
||||||
|
final UserPreferencesService _userPreferencesService;
|
||||||
|
final PlatformUtils _platformUtils;
|
||||||
|
|
||||||
|
ReleaseNotesBloc(
|
||||||
|
this._userPreferencesService,
|
||||||
|
this._platformUtils,
|
||||||
|
) : super(const HiddenReleaseNotesDialogState()) {
|
||||||
|
_showDialogIfNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showDialogIfNeeded() async {
|
||||||
|
_platformUtils.version.then((version) {
|
||||||
|
if (version != _userPreferencesService.seenChangelogVersion) {
|
||||||
|
emit(ShowReleaseNotesDialogState(version));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void setChangelogVersion() {
|
||||||
|
_userPreferencesService.seenChangelogVersion = (state as ShowReleaseNotesDialogState).version;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:lightmeter/providers/services_provider.dart';
|
||||||
|
import 'package:lightmeter/screens/shared/release_notes_dialog/bloc_release_notes.dart';
|
||||||
|
import 'package:lightmeter/screens/shared/release_notes_dialog/state_release_notes.dart';
|
||||||
|
import 'package:lightmeter/screens/shared/release_notes_dialog/widget_dialog_release_notes.dart';
|
||||||
|
import 'package:lightmeter/utils/platform_utils.dart';
|
||||||
|
|
||||||
|
class ReleaseNotesFlow extends StatelessWidget {
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const ReleaseNotesFlow({required this.child, super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider(
|
||||||
|
create: (context) => ReleaseNotesBloc(
|
||||||
|
ServicesProvider.of(context).userPreferencesService,
|
||||||
|
const PlatformUtils(),
|
||||||
|
),
|
||||||
|
child: BlocListener<ReleaseNotesBloc, ReleaseNotesState>(
|
||||||
|
listener: (context, state) {
|
||||||
|
if (state is ShowReleaseNotesDialogState) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => ReleaseNotesDialog(version: state.version),
|
||||||
|
).then((_) => context.read<ReleaseNotesBloc>().setChangelogVersion());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
sealed class ReleaseNotesState {
|
||||||
|
const ReleaseNotesState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class HiddenReleaseNotesDialogState extends ReleaseNotesState {
|
||||||
|
const HiddenReleaseNotesDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShowReleaseNotesDialogState extends ReleaseNotesState {
|
||||||
|
final String version;
|
||||||
|
|
||||||
|
const ShowReleaseNotesDialogState(this.version);
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart' show rootBundle;
|
||||||
|
import 'package:lightmeter/data/models/supported_locale.dart';
|
||||||
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
|
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
||||||
|
import 'package:lightmeter/res/dimens.dart';
|
||||||
|
|
||||||
|
class ReleaseNotesDialog extends StatelessWidget {
|
||||||
|
final String version;
|
||||||
|
|
||||||
|
const ReleaseNotesDialog({required this.version, super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(S.of(context).whatsnew),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
S.of(context).changesInVersion(version),
|
||||||
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
|
),
|
||||||
|
const SizedBox(height: Dimens.grid8),
|
||||||
|
FutureBuilder<String>(
|
||||||
|
future: loadReleaseNotes(context),
|
||||||
|
builder: (context, snapshot) => Text(snapshot.data ?? ''),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: Navigator.of(context).pop,
|
||||||
|
child: Text(S.of(context).close),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> loadReleaseNotes(BuildContext context) async {
|
||||||
|
late final String localeName;
|
||||||
|
|
||||||
|
switch (UserPreferencesProvider.localeOf(context)) {
|
||||||
|
case SupportedLocale.ru:
|
||||||
|
localeName = SupportedLocale.ru.name;
|
||||||
|
default:
|
||||||
|
localeName = SupportedLocale.en.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return rootBundle.loadString('assets/release_notes/release_notes_${localeName}_$version.md');
|
||||||
|
} catch (e) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -79,15 +79,18 @@ class TimerScreenState extends State<TimerScreen> with TickerProviderStateMixin
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(Dimens.paddingL),
|
padding: const EdgeInsets.all(Dimens.paddingL),
|
||||||
child: SizedBox.fromSize(
|
child: ConstrainedBox(
|
||||||
size: Size.square(MediaQuery.sizeOf(context).width - Dimens.paddingL * 4),
|
constraints: const BoxConstraints(maxHeight: Dimens.tabletMaxWidth, maxWidth: Dimens.tabletMaxWidth),
|
||||||
child: ValueListenableBuilder(
|
child: SizedBox.fromSize(
|
||||||
valueListenable: timelineAnimation,
|
size: Size.square(MediaQuery.sizeOf(context).width - Dimens.paddingL * 4),
|
||||||
builder: (_, value, child) => TimerTimeline(
|
child: ValueListenableBuilder(
|
||||||
progress: value,
|
valueListenable: timelineAnimation,
|
||||||
child: TimerText(
|
builder: (_, value, child) => TimerTimeline(
|
||||||
timeLeft: Duration(milliseconds: (widget.duration.inMilliseconds * value).toInt()),
|
progress: value,
|
||||||
duration: widget.duration,
|
child: TimerText(
|
||||||
|
timeLeft: Duration(milliseconds: (widget.duration.inMilliseconds * value).toInt()),
|
||||||
|
duration: widget.duration,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
10
lib/utils/platform_utils.dart
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
|
||||||
|
class PlatformUtils {
|
||||||
|
const PlatformUtils();
|
||||||
|
|
||||||
|
Future<String> get version async => (await PackageInfo.fromPlatform()).version;
|
||||||
|
|
||||||
|
Future<({String version, String buildNumber})> get buildVersion async =>
|
||||||
|
PackageInfo.fromPlatform().then((value) => (version: value.version, buildNumber: value.buildNumber));
|
||||||
|
}
|
|
@ -17,6 +17,13 @@ double textHeight(
|
||||||
String text,
|
String text,
|
||||||
TextStyle? style,
|
TextStyle? style,
|
||||||
double maxWidth,
|
double maxWidth,
|
||||||
|
) =>
|
||||||
|
textSize(text, style, maxWidth).height;
|
||||||
|
|
||||||
|
Size textSize(
|
||||||
|
String text,
|
||||||
|
TextStyle? style,
|
||||||
|
double maxWidth,
|
||||||
) {
|
) {
|
||||||
final TextPainter titlePainter = TextPainter(
|
final TextPainter titlePainter = TextPainter(
|
||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
|
@ -25,5 +32,5 @@ double textHeight(
|
||||||
),
|
),
|
||||||
textDirection: TextDirection.ltr,
|
textDirection: TextDirection.ltr,
|
||||||
)..layout(maxWidth: maxWidth);
|
)..layout(maxWidth: maxWidth);
|
||||||
return titlePainter.height;
|
return titlePainter.size;
|
||||||
}
|
}
|
||||||
|
|
15
pubspec.yaml
|
@ -1,13 +1,14 @@
|
||||||
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: 0.18.0+49
|
version: 0.20.0+51
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.0.0 <4.0.0"
|
sdk: ">=3.0.0 <4.0.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
app_settings: 4.2.0
|
app_settings: 4.2.0
|
||||||
|
auto_size_text: 3.0.0
|
||||||
bloc_concurrency: 0.2.2
|
bloc_concurrency: 0.2.2
|
||||||
camera: 0.10.5+2
|
camera: 0.10.5+2
|
||||||
camera_android_camerax: 0.6.1+1
|
camera_android_camerax: 0.6.1+1
|
||||||
|
@ -29,11 +30,11 @@ dependencies:
|
||||||
m3_lightmeter_iap:
|
m3_lightmeter_iap:
|
||||||
git:
|
git:
|
||||||
url: "https://github.com/vodemn/m3_lightmeter_iap"
|
url: "https://github.com/vodemn/m3_lightmeter_iap"
|
||||||
ref: v0.10.0
|
ref: v0.11.2
|
||||||
m3_lightmeter_resources:
|
m3_lightmeter_resources:
|
||||||
git:
|
git:
|
||||||
url: "https://github.com/vodemn/m3_lightmeter_resources"
|
url: "https://github.com/vodemn/m3_lightmeter_resources"
|
||||||
ref: v1.2.0
|
ref: v1.4.0
|
||||||
material_color_utilities: 0.5.0
|
material_color_utilities: 0.5.0
|
||||||
package_info_plus: 4.2.0
|
package_info_plus: 4.2.0
|
||||||
permission_handler: 10.4.3
|
permission_handler: 10.4.3
|
||||||
|
@ -44,6 +45,7 @@ dependencies:
|
||||||
vibration: 1.8.1
|
vibration: 1.8.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
args: 2.5.0
|
||||||
bloc_test: 9.1.3
|
bloc_test: 9.1.3
|
||||||
build_runner: 2.4.6
|
build_runner: 2.4.6
|
||||||
flutter_native_splash: 2.3.5
|
flutter_native_splash: 2.3.5
|
||||||
|
@ -51,20 +53,27 @@ dev_dependencies:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
golden_toolkit: 0.15.0
|
golden_toolkit: 0.15.0
|
||||||
google_fonts: 3.0.1
|
google_fonts: 3.0.1
|
||||||
|
image: 4.1.7
|
||||||
integration_test:
|
integration_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
lint: 2.1.2
|
lint: 2.1.2
|
||||||
|
logging: 1.2.0
|
||||||
meta: 1.9.1
|
meta: 1.9.1
|
||||||
mocktail: 0.3.0
|
mocktail: 0.3.0
|
||||||
test: 1.24.3
|
test: 1.24.3
|
||||||
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
|
m3_lightmeter_resources:
|
||||||
|
git:
|
||||||
|
url: "https://github.com/vodemn/m3_lightmeter_resources"
|
||||||
|
ref: v1.4.0
|
||||||
material_color_utilities: 0.11.1
|
material_color_utilities: 0.11.1
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
assets:
|
assets:
|
||||||
- assets/camera_stub_image.jpg
|
- assets/camera_stub_image.jpg
|
||||||
|
- assets/release_notes/
|
||||||
|
|
||||||
flutter_intl:
|
flutter_intl:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
|
@ -17,28 +17,35 @@ As a user I want to see the most relevant screenshots in the store, so that I ca
|
||||||
- Settings screen
|
- Settings screen
|
||||||
|
|
||||||
1. Just the screen
|
1. Just the screen
|
||||||
2. Opened metering screen layout features dialog
|
|
||||||
|
|
||||||
- Equipment profiles screen
|
- Equipment profiles screen
|
||||||
|
|
||||||
1. Just the screen
|
1. Just the screen
|
||||||
2. Opened equipment profile ISO picker
|
2. Opened equipment profile ISO picker
|
||||||
|
|
||||||
|
- Timer screen
|
||||||
|
|
||||||
|
1. Just the screen
|
||||||
|
|
||||||
> \*also in dark mode
|
> \*also in dark mode
|
||||||
|
|
||||||
> \*\*Android only
|
> \*\*Android only
|
||||||
|
|
||||||
## Run the generator
|
## Run the generator
|
||||||
|
|
||||||
Screenshots will be stored in the _screenshots/generated/\<platform\>/_ folder.
|
Release screenshots will be stored in the _screenshots/generated/\<platform\>/_ folder.
|
||||||
|
|
||||||
### Android
|
Raw screenshots will be stored in the _screenshots/generated/raw/\<platform\>/_ folder.
|
||||||
|
|
||||||
|
### Generate raw screenshots
|
||||||
|
|
||||||
|
#### Android
|
||||||
|
|
||||||
```console
|
```console
|
||||||
sh screenshots/generate_screenshots.sh <deviceName>
|
sh screenshots/generate_android_screenshots.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
### iOS
|
#### iOS
|
||||||
|
|
||||||
Apple requires screenshots a specific list of devices, so we can implement a custom generator to cover all those devices.
|
Apple requires screenshots a specific list of devices, so we can implement a custom generator to cover all those devices.
|
||||||
|
|
||||||
|
@ -48,6 +55,12 @@ Can be run on Simulator.
|
||||||
sh screenshots/generate_ios_screenshots.sh
|
sh screenshots/generate_ios_screenshots.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Apply store constraints and text data
|
||||||
|
|
||||||
|
```console
|
||||||
|
sh screenshots/scripts/convert_to_store_screenshots.sh
|
||||||
|
```
|
||||||
|
|
||||||
## List of devices
|
## List of devices
|
||||||
|
|
||||||
### Android
|
### Android
|
||||||
|
@ -56,9 +69,5 @@ sh screenshots/generate_ios_screenshots.sh
|
||||||
|
|
||||||
### iOS
|
### iOS
|
||||||
|
|
||||||
- iPhone 8 Plus
|
|
||||||
- iPhone 13 Pro
|
- iPhone 13 Pro
|
||||||
- iPhone 13 Pro Max
|
|
||||||
- iPhone 15 Pro
|
|
||||||
- iPhone 15 Pro Max
|
|
||||||
- iPad Pro (12.9-inch) (6th generation)
|
- iPad Pro (12.9-inch) (6th generation)
|
||||||
|
|
44
screenshots/assets/content/screenshot_titles_en.json
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
"screenshots": [
|
||||||
|
{
|
||||||
|
"screenshotName": "light_metering-reflected",
|
||||||
|
"title": "Quick & easy to use",
|
||||||
|
"subtitle": "with all the necessary controls\nunder your thumb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"screenshotName": "light_metering-incident",
|
||||||
|
"title": "Incident light metering",
|
||||||
|
"subtitle": "using the light sensor\nof your device"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"screenshotName": "light_metering-iso-picker",
|
||||||
|
"title": "Lots of ISO values",
|
||||||
|
"subtitle": "from 3 and up to 6400"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"screenshotName": "light_timer",
|
||||||
|
"title": "In-built timer",
|
||||||
|
"subtitle": "for the ease of shooting\nlong exposures"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"screenshotName": "light_settings",
|
||||||
|
"title": "Useful settings",
|
||||||
|
"subtitle": "to get the most accurate\nmetering results"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"screenshotName": "light_equipment-profiles",
|
||||||
|
"title": "Create multiple profiles",
|
||||||
|
"subtitle": "to match your\ncamera & lens setups"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"screenshotName": "light_equipment-profiles-iso-picker",
|
||||||
|
"title": "Fine-tune results",
|
||||||
|
"subtitle": "by selecting the values\nthat you use the most"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"screenshotName": "dark_metering-reflected",
|
||||||
|
"title": "Match your style",
|
||||||
|
"subtitle": "with various theme types and colors"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
BIN
screenshots/assets/fonts/SF-Pro-Display-Bold-dark.zip
Normal file
BIN
screenshots/assets/fonts/SF-Pro-Display-Bold.zip
Normal file
BIN
screenshots/assets/fonts/SF-Pro-Display-Regular-dark.zip
Normal file
BIN
screenshots/assets/fonts/SF-Pro-Display-Regular.zip
Normal file
BIN
screenshots/assets/frames/android/pixel_6_frame.png
Normal file
After Width: | Height: | Size: 198 KiB |
BIN
screenshots/assets/frames/ios/iphone_13_pro_frame.png
Normal file
After Width: | Height: | Size: 219 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 16 KiB |
189
screenshots/convert_to_store_screenshots.dart
Normal file
|
@ -0,0 +1,189 @@
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:args/args.dart';
|
||||||
|
import 'package:image/image.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
import 'models/screenshot_args.dart';
|
||||||
|
import 'models/screenshot_device.dart';
|
||||||
|
import 'models/screenshot_layout.dart';
|
||||||
|
import 'utils/parse_configs.dart';
|
||||||
|
|
||||||
|
final _configs = parseScreenshotConfigs();
|
||||||
|
|
||||||
|
Future<int> main(List<String> args) async {
|
||||||
|
final parser = ArgParser()
|
||||||
|
..addFlag('verbose', abbr: 'v', help: 'Verbose output')
|
||||||
|
..addOption('platform', abbr: 'p', help: 'Device platform', mandatory: true)
|
||||||
|
..addOption('device', abbr: 'd', help: 'device_snake_name', mandatory: true)
|
||||||
|
..addOption('layout', abbr: 'l', help: 'Device layout', mandatory: true);
|
||||||
|
final ArgResults argResults = parser.parse(args);
|
||||||
|
|
||||||
|
if (argResults['verbose'] as bool) {
|
||||||
|
Logger.root.level = Level.ALL;
|
||||||
|
} else {
|
||||||
|
Logger.root.level = Level.INFO;
|
||||||
|
}
|
||||||
|
|
||||||
|
final platform = argResults["platform"] as String;
|
||||||
|
final device = argResults["device"] as String;
|
||||||
|
final layout = ScreenshotLayout.values.firstWhere((e) => e.name == argResults["layout"] as String);
|
||||||
|
|
||||||
|
Directory('screenshots/generated/raw/$platform/$device').listSync().forEach((filePath) async {
|
||||||
|
final screenshotName = filePath.path.split('/').last.replaceAll('.png', '');
|
||||||
|
final screenshotBytes = File(filePath.path).readAsBytesSync();
|
||||||
|
final screenshot = decodePng(Uint8List.fromList(screenshotBytes))!;
|
||||||
|
|
||||||
|
final screenshotArgs = ScreenshotArgs.fromRawName(
|
||||||
|
name: screenshotName,
|
||||||
|
deviceName: device,
|
||||||
|
platformFolder: platform,
|
||||||
|
);
|
||||||
|
|
||||||
|
final file = await File(screenshotArgs.toPath(layout.name)).create(recursive: true);
|
||||||
|
file.writeAsBytesSync(
|
||||||
|
encodePng(
|
||||||
|
screenshot.convertToStoreScreenshot(
|
||||||
|
args: screenshotArgs,
|
||||||
|
layout: layout,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ScreenshotImage on Image {
|
||||||
|
Image convertToStoreScreenshot({
|
||||||
|
required ScreenshotArgs args,
|
||||||
|
required ScreenshotLayout layout,
|
||||||
|
}) {
|
||||||
|
if (_configs[args.nameWithTheme] == null) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return _addSystemOverlay(
|
||||||
|
screenshotDevices[args.deviceName]!,
|
||||||
|
isDark: args.isDark,
|
||||||
|
)
|
||||||
|
._addDeviceFrame(
|
||||||
|
screenshotDevices[args.deviceName]!,
|
||||||
|
args.backgroundColor,
|
||||||
|
)
|
||||||
|
._applyLayout(
|
||||||
|
layout,
|
||||||
|
_configs[args.nameWithTheme]!.title,
|
||||||
|
_configs[args.nameWithTheme]!.subtitle,
|
||||||
|
isDark: args.isDark,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Image _addSystemOverlay(ScreenshotDevice device, {required bool isDark}) {
|
||||||
|
final path = isDark ? device.systemOverlayPathDark : device.systemOverlayPathLight;
|
||||||
|
final statusBar = copyResize(
|
||||||
|
decodePng(File(path).readAsBytesSync())!,
|
||||||
|
width: width,
|
||||||
|
);
|
||||||
|
return compositeImage(this, statusBar);
|
||||||
|
}
|
||||||
|
|
||||||
|
Image _addDeviceFrame(ScreenshotDevice device, String color) {
|
||||||
|
final backgroundColor = ColorRgba8(
|
||||||
|
int.parse(color.substring(2, 4), radix: 16),
|
||||||
|
int.parse(color.substring(4, 6), radix: 16),
|
||||||
|
int.parse(color.substring(6, 8), radix: 16),
|
||||||
|
int.parse(color.substring(0, 2), radix: 16),
|
||||||
|
);
|
||||||
|
final screenshotRounded = copyCrop(
|
||||||
|
this,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
);
|
||||||
|
|
||||||
|
final frame = decodePng(File(device.deviceFramePath).readAsBytesSync())!;
|
||||||
|
final expandedScreenshot = copyExpandCanvas(
|
||||||
|
copyExpandCanvas(
|
||||||
|
screenshotRounded,
|
||||||
|
newWidth: screenshotRounded.width + device.screenshotFrameOffset.dx,
|
||||||
|
newHeight: screenshotRounded.height + device.screenshotFrameOffset.dy,
|
||||||
|
position: ExpandCanvasPosition.bottomRight,
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
),
|
||||||
|
newWidth: frame.width,
|
||||||
|
newHeight: frame.height,
|
||||||
|
position: ExpandCanvasPosition.topLeft,
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
);
|
||||||
|
|
||||||
|
return compositeImage(expandedScreenshot, frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
Image _applyLayout(ScreenshotLayout layout, String title, String subtitle, {required bool isDark}) {
|
||||||
|
final textImage = _drawTitles(layout, title, subtitle, isDark: isDark);
|
||||||
|
final maxFrameHeight =
|
||||||
|
layout.size.height - (layout.contentPadding.top + textImage.height + 84 + layout.contentPadding.bottom);
|
||||||
|
int maxFrameWidth = layout.size.width - (layout.contentPadding.left + layout.contentPadding.right);
|
||||||
|
if (maxFrameWidth * height / width > maxFrameHeight) {
|
||||||
|
maxFrameWidth = maxFrameHeight * width ~/ height;
|
||||||
|
}
|
||||||
|
final scaledScreenshot = copyResize(this, width: maxFrameWidth);
|
||||||
|
|
||||||
|
final draft = copyExpandCanvas(
|
||||||
|
copyExpandCanvas(
|
||||||
|
scaledScreenshot,
|
||||||
|
newWidth: scaledScreenshot.width + (layout.size.width - scaledScreenshot.width) ~/ 2,
|
||||||
|
newHeight: scaledScreenshot.height + layout.contentPadding.bottom,
|
||||||
|
position: ExpandCanvasPosition.topLeft,
|
||||||
|
backgroundColor: getPixel(0, 0),
|
||||||
|
),
|
||||||
|
newWidth: layout.size.width,
|
||||||
|
newHeight: layout.size.height,
|
||||||
|
position: ExpandCanvasPosition.bottomRight,
|
||||||
|
backgroundColor: getPixel(0, 0),
|
||||||
|
);
|
||||||
|
|
||||||
|
return compositeImage(
|
||||||
|
draft,
|
||||||
|
textImage,
|
||||||
|
dstX: layout.contentPadding.left,
|
||||||
|
dstY: layout.contentPadding.top,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Image _drawTitles(ScreenshotLayout layout, String title, String subtitle, {required bool isDark}) {
|
||||||
|
final titleFont =
|
||||||
|
BitmapFont.fromZip(File(isDark ? layout.titleFontDarkPath : layout.titleFontPath).readAsBytesSync());
|
||||||
|
final subtitleFont =
|
||||||
|
BitmapFont.fromZip(File(isDark ? layout.subtitleFontDarkPath : layout.subtitleFontPath).readAsBytesSync());
|
||||||
|
final textImage = fill(
|
||||||
|
Image(
|
||||||
|
height: titleFont.lineHeight + 36 + subtitleFont.lineHeight * 2,
|
||||||
|
width: layout.size.width - (layout.contentPadding.left + layout.contentPadding.right),
|
||||||
|
),
|
||||||
|
color: getPixel(0, 0),
|
||||||
|
);
|
||||||
|
|
||||||
|
drawString(
|
||||||
|
textImage,
|
||||||
|
title,
|
||||||
|
font: titleFont,
|
||||||
|
y: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
int subtitleDy = titleFont.lineHeight + 36;
|
||||||
|
subtitle.split('\n').forEach((line) {
|
||||||
|
drawString(
|
||||||
|
textImage,
|
||||||
|
line,
|
||||||
|
font: subtitleFont,
|
||||||
|
y: subtitleDy,
|
||||||
|
);
|
||||||
|
subtitleDy += subtitleFont.lineHeight;
|
||||||
|
});
|
||||||
|
|
||||||
|
return textImage;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,5 @@
|
||||||
|
// ignore_for_file: invalid_use_of_visible_for_testing_member
|
||||||
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
@ -5,44 +7,59 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:integration_test/integration_test.dart';
|
import 'package:integration_test/integration_test.dart';
|
||||||
import 'package:lightmeter/data/models/ev_source_type.dart';
|
import 'package:lightmeter/data/models/ev_source_type.dart';
|
||||||
|
import 'package:lightmeter/data/models/exposure_pair.dart';
|
||||||
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
|
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
|
||||||
import 'package:lightmeter/data/models/theme_type.dart';
|
import 'package:lightmeter/data/models/theme_type.dart';
|
||||||
import 'package:lightmeter/data/models/volume_action.dart';
|
import 'package:lightmeter/data/models/volume_action.dart';
|
||||||
import 'package:lightmeter/data/shared_prefs_service.dart';
|
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||||
import 'package:lightmeter/generated/l10n.dart';
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
|
import 'package:lightmeter/res/dimens.dart';
|
||||||
import 'package:lightmeter/res/theme.dart';
|
import 'package:lightmeter/res/theme.dart';
|
||||||
|
import 'package:lightmeter/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart';
|
||||||
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/iso_picker/widget_picker_iso.dart';
|
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/iso_picker/widget_picker_iso.dart';
|
||||||
|
import 'package:lightmeter/screens/metering/screen_metering.dart';
|
||||||
import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart';
|
import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart';
|
||||||
import 'package:lightmeter/screens/settings/screen_settings.dart';
|
import 'package:lightmeter/screens/settings/screen_settings.dart';
|
||||||
|
import 'package:lightmeter/screens/shared/animated_circular_button/widget_button_circular_animated.dart';
|
||||||
|
import 'package:lightmeter/screens/timer/screen_timer.dart';
|
||||||
|
import 'package:lightmeter/utils/platform_utils.dart';
|
||||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import '../integration_test/mocks/paid_features_mock.dart';
|
import '../integration_test/mocks/paid_features_mock.dart';
|
||||||
import '../integration_test/utils/widget_tester_actions.dart';
|
import '../integration_test/utils/widget_tester_actions.dart';
|
||||||
|
import 'models/screenshot_args.dart';
|
||||||
|
|
||||||
//https://stackoverflow.com/a/67186625/13167574
|
//https://stackoverflow.com/a/67186625/13167574
|
||||||
|
|
||||||
const _mockFilm = Film('Ilford HP5+', 400);
|
const _mockFilm = Film('Ilford HP5+', 400);
|
||||||
|
final Color _lightThemeColor = primaryColorsList[5];
|
||||||
|
final Color _darkThemeColor = primaryColorsList[3];
|
||||||
|
final ThemeData _themeLight = themeFrom(_lightThemeColor, Brightness.light);
|
||||||
|
final ThemeData _themeDark = themeFrom(_darkThemeColor, Brightness.dark);
|
||||||
|
|
||||||
/// Just a screenshot generator. No expectations here.
|
/// Just a screenshot generator. No expectations here.
|
||||||
void main() {
|
void main() {
|
||||||
final binding = IntegrationTestWidgetsFlutterBinding();
|
final binding = IntegrationTestWidgetsFlutterBinding();
|
||||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
final Color lightThemeColor = primaryColorsList[5];
|
|
||||||
final Color darkThemeColor = primaryColorsList[3];
|
|
||||||
|
|
||||||
void mockSharedPrefs(ThemeType theme, Color color) {
|
Future<void> mockSharedPrefs({
|
||||||
// ignore: invalid_use_of_visible_for_testing_member
|
int iso = 400,
|
||||||
|
int nd = 0,
|
||||||
|
double calibration = 0.0,
|
||||||
|
required ThemeType theme,
|
||||||
|
required Color color,
|
||||||
|
}) async {
|
||||||
SharedPreferences.setMockInitialValues({
|
SharedPreferences.setMockInitialValues({
|
||||||
/// Metering values
|
/// Metering values
|
||||||
UserPreferencesService.evSourceTypeKey: EvSourceType.camera.index,
|
UserPreferencesService.evSourceTypeKey: EvSourceType.camera.index,
|
||||||
UserPreferencesService.isoKey: 400,
|
UserPreferencesService.isoKey: iso,
|
||||||
UserPreferencesService.ndFilterKey: 0,
|
UserPreferencesService.ndFilterKey: nd,
|
||||||
|
|
||||||
/// Metering settings
|
/// Metering settings
|
||||||
UserPreferencesService.stopTypeKey: StopType.third.index,
|
UserPreferencesService.stopTypeKey: StopType.third.index,
|
||||||
UserPreferencesService.cameraEvCalibrationKey: 0.0,
|
UserPreferencesService.cameraEvCalibrationKey: calibration,
|
||||||
UserPreferencesService.lightSensorEvCalibrationKey: 0.0,
|
UserPreferencesService.lightSensorEvCalibrationKey: calibration,
|
||||||
UserPreferencesService.meteringScreenLayoutKey: json.encode(
|
UserPreferencesService.meteringScreenLayoutKey: json.encode(
|
||||||
{
|
{
|
||||||
MeteringScreenLayoutFeature.equipmentProfiles: true,
|
MeteringScreenLayoutFeature.equipmentProfiles: true,
|
||||||
|
@ -52,6 +69,7 @@ void main() {
|
||||||
),
|
),
|
||||||
|
|
||||||
/// General settings
|
/// General settings
|
||||||
|
UserPreferencesService.autostartTimerKey: false,
|
||||||
UserPreferencesService.caffeineKey: true,
|
UserPreferencesService.caffeineKey: true,
|
||||||
UserPreferencesService.hapticsKey: true,
|
UserPreferencesService.hapticsKey: true,
|
||||||
UserPreferencesService.volumeActionKey: VolumeAction.shutter.toString(),
|
UserPreferencesService.volumeActionKey: VolumeAction.shutter.toString(),
|
||||||
|
@ -61,6 +79,8 @@ void main() {
|
||||||
UserPreferencesService.themeTypeKey: theme.index,
|
UserPreferencesService.themeTypeKey: theme.index,
|
||||||
UserPreferencesService.primaryColorKey: color.value,
|
UserPreferencesService.primaryColorKey: color.value,
|
||||||
UserPreferencesService.dynamicColorKey: false,
|
UserPreferencesService.dynamicColorKey: false,
|
||||||
|
|
||||||
|
UserPreferencesService.seenChangelogVersionKey: await const PlatformUtils().version,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,7 +90,7 @@ void main() {
|
||||||
|
|
||||||
/// Generates several screenshots with the light theme
|
/// Generates several screenshots with the light theme
|
||||||
testWidgets('Generate light theme screenshots', (tester) async {
|
testWidgets('Generate light theme screenshots', (tester) async {
|
||||||
mockSharedPrefs(ThemeType.light, lightThemeColor);
|
await mockSharedPrefs(theme: ThemeType.light, color: _lightThemeColor);
|
||||||
await tester.pumpApplication(
|
await tester.pumpApplication(
|
||||||
availableFilms: [_mockFilm],
|
availableFilms: [_mockFilm],
|
||||||
filmsInUse: [_mockFilm],
|
filmsInUse: [_mockFilm],
|
||||||
|
@ -78,43 +98,39 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
await tester.takePhoto();
|
await tester.takePhoto();
|
||||||
await tester.takeScreenshot(binding, 'light-metering_reflected');
|
await tester.takeScreenshotLight(binding, 'metering-reflected');
|
||||||
|
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
await tester.tap(find.byTooltip(S.current.tooltipUseLightSensor));
|
await tester.tap(find.byTooltip(S.current.tooltipUseLightSensor));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.toggleIncidentMetering(7.3);
|
await tester.toggleIncidentMetering(7.3);
|
||||||
await tester.takeScreenshot(binding, 'light-metering_incident');
|
await tester.takeScreenshotLight(binding, 'metering-incident');
|
||||||
}
|
}
|
||||||
|
|
||||||
await tester.openAnimatedPicker<IsoValuePicker>();
|
await tester.openAnimatedPicker<IsoValuePicker>();
|
||||||
await tester.takeScreenshot(binding, 'light-metering_iso_picker');
|
await tester.takeScreenshotLight(binding, 'metering-iso-picker');
|
||||||
|
|
||||||
await tester.tapCancelButton();
|
await tester.tapCancelButton();
|
||||||
await tester.tap(find.byTooltip(S.current.tooltipOpenSettings));
|
await tester.tap(find.byTooltip(S.current.tooltipOpenSettings));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.takeScreenshot(binding, 'light-settings');
|
await tester.takeScreenshotLight(binding, 'settings');
|
||||||
|
|
||||||
await tester.tapDescendantTextOf<SettingsScreen>(S.current.meteringScreenLayout);
|
|
||||||
await tester.takeScreenshot(binding, 'light-settings_metering_screen_layout');
|
|
||||||
|
|
||||||
await tester.tapCancelButton();
|
|
||||||
await tester.tapDescendantTextOf<SettingsScreen>(S.current.equipmentProfiles);
|
await tester.tapDescendantTextOf<SettingsScreen>(S.current.equipmentProfiles);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.tapDescendantTextOf<EquipmentProfilesScreen>(mockEquipmentProfiles.first.name);
|
await tester.tapDescendantTextOf<EquipmentProfilesScreen>(mockEquipmentProfiles.first.name);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.takeScreenshot(binding, 'light-equipment_profiles');
|
await tester.takeScreenshotLight(binding, 'equipment-profiles');
|
||||||
|
|
||||||
await tester.tap(find.byIcon(Icons.iso).first);
|
await tester.tap(find.byIcon(Icons.iso_outlined).first);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.takeScreenshot(binding, 'light-equipment_profiles_iso_picker');
|
await tester.takeScreenshotLight(binding, 'equipment-profiles-iso-picker');
|
||||||
});
|
});
|
||||||
|
|
||||||
/// and the additionally the first one with the dark theme
|
/// and the additionally the first one with the dark theme
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'Generate dark theme screenshots',
|
'Generate dark theme screenshots',
|
||||||
(tester) async {
|
(tester) async {
|
||||||
mockSharedPrefs(ThemeType.dark, darkThemeColor);
|
await mockSharedPrefs(theme: ThemeType.dark, color: _darkThemeColor);
|
||||||
await tester.pumpApplication(
|
await tester.pumpApplication(
|
||||||
availableFilms: [_mockFilm],
|
availableFilms: [_mockFilm],
|
||||||
filmsInUse: [_mockFilm],
|
filmsInUse: [_mockFilm],
|
||||||
|
@ -122,14 +138,39 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
await tester.takePhoto();
|
await tester.takePhoto();
|
||||||
await tester.takeScreenshot(binding, 'dark-metering_reflected');
|
await tester.takeScreenshotDark(binding, 'metering-reflected');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (Platform.isAndroid) {
|
testWidgets(
|
||||||
await tester.tap(find.byTooltip(S.current.tooltipUseLightSensor));
|
'Generate timer screenshot',
|
||||||
await tester.pumpAndSettle();
|
(tester) async {
|
||||||
await tester.toggleIncidentMetering(7.3);
|
const timerExposurePair = ExposurePair(
|
||||||
await tester.takeScreenshot(binding, 'dark-metering_incident');
|
ApertureValue(16, StopType.full),
|
||||||
}
|
ShutterSpeedValue(8, false, StopType.full),
|
||||||
|
);
|
||||||
|
await mockSharedPrefs(
|
||||||
|
iso: 100,
|
||||||
|
nd: 8,
|
||||||
|
calibration: -0.3,
|
||||||
|
theme: ThemeType.light,
|
||||||
|
color: _lightThemeColor,
|
||||||
|
);
|
||||||
|
await tester.pumpApplication(
|
||||||
|
availableFilms: [_mockFilm],
|
||||||
|
filmsInUse: [_mockFilm],
|
||||||
|
selectedFilm: _mockFilm,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.takePhoto();
|
||||||
|
await tester.scrollToExposurePair(
|
||||||
|
ev: 5,
|
||||||
|
exposurePair: timerExposurePair,
|
||||||
|
);
|
||||||
|
await tester.tap(find.text(timerExposurePair.shutterSpeed.toString()));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.mockTimerResumedState(timerExposurePair.shutterSpeed);
|
||||||
|
await tester.takeScreenshotLight(binding, 'timer');
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -137,8 +178,56 @@ void main() {
|
||||||
final String _platformFolder = Platform.isAndroid ? 'android' : 'ios';
|
final String _platformFolder = Platform.isAndroid ? 'android' : 'ios';
|
||||||
|
|
||||||
extension on WidgetTester {
|
extension on WidgetTester {
|
||||||
Future<void> takeScreenshot(IntegrationTestWidgetsFlutterBinding binding, String name) async {
|
Future<void> takeScreenshotLight(IntegrationTestWidgetsFlutterBinding binding, String name) =>
|
||||||
await binding.takeScreenshot("$_platformFolder/${const String.fromEnvironment('deviceName')}/$name");
|
_takeScreenshot(binding, name, _themeLight);
|
||||||
|
Future<void> takeScreenshotDark(IntegrationTestWidgetsFlutterBinding binding, String name) =>
|
||||||
|
_takeScreenshot(binding, name, _themeDark);
|
||||||
|
|
||||||
|
Future<void> _takeScreenshot(IntegrationTestWidgetsFlutterBinding binding, String name, ThemeData theme) async {
|
||||||
|
final Color backgroundColor = theme.colorScheme.surface;
|
||||||
|
await binding.takeScreenshot(
|
||||||
|
ScreenshotArgs(
|
||||||
|
name: name,
|
||||||
|
deviceName: const String.fromEnvironment('deviceName'),
|
||||||
|
platformFolder: _platformFolder,
|
||||||
|
backgroundColor: backgroundColor.value.toRadixString(16),
|
||||||
|
isDark: theme.brightness == Brightness.dark,
|
||||||
|
).toString(),
|
||||||
|
);
|
||||||
await pumpAndSettle();
|
await pumpAndSettle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension on WidgetTester {
|
||||||
|
Future<void> scrollToExposurePair({
|
||||||
|
double ev = mockPhotoEv100,
|
||||||
|
EquipmentProfile equipmentProfile = defaultEquipmentProfile,
|
||||||
|
required ExposurePair exposurePair,
|
||||||
|
}) async {
|
||||||
|
final exposurePairs = MeteringContainerBuidler.buildExposureValues(
|
||||||
|
ev,
|
||||||
|
StopType.third,
|
||||||
|
equipmentProfile,
|
||||||
|
);
|
||||||
|
|
||||||
|
await scrollUntilVisible(
|
||||||
|
find.byWidgetPredicate((widget) => widget is Row && widget.key == ValueKey(exposurePairs.indexOf(exposurePair))),
|
||||||
|
56,
|
||||||
|
scrollable: find.descendant(of: find.byType(ExposurePairsList), matching: find.byType(Scrollable)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> mockTimerResumedState(ShutterSpeedValue shutterSpeedValue) async {
|
||||||
|
await tap(find.byType(AnimatedCircluarButton));
|
||||||
|
await pump(Dimens.durationS);
|
||||||
|
|
||||||
|
late final skipTimerDuration =
|
||||||
|
Duration(milliseconds: (shutterSpeedValue.value * 0.35 * Duration.millisecondsPerSecond).toInt());
|
||||||
|
await pump(skipTimerDuration);
|
||||||
|
|
||||||
|
final TimerScreenState state = this.state(find.byType(TimerScreen));
|
||||||
|
state.startStopIconController.stop();
|
||||||
|
state.timelineController.stop();
|
||||||
|
await pump();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
After Width: | Height: | Size: 492 KiB |
After Width: | Height: | Size: 221 KiB |
After Width: | Height: | Size: 221 KiB |
After Width: | Height: | Size: 235 KiB |
After Width: | Height: | Size: 220 KiB |
After Width: | Height: | Size: 500 KiB |
BIN
screenshots/generated/android/android/light_settings.png
Normal file
After Width: | Height: | Size: 235 KiB |
BIN
screenshots/generated/android/android/light_timer.png
Normal file
After Width: | Height: | Size: 219 KiB |
After Width: | Height: | Size: 346 KiB |
After Width: | Height: | Size: 159 KiB |
After Width: | Height: | Size: 163 KiB |
After Width: | Height: | Size: 177 KiB |
After Width: | Height: | Size: 345 KiB |
BIN
screenshots/generated/ios/iphone55inch/light_settings.png
Normal file
After Width: | Height: | Size: 167 KiB |
BIN
screenshots/generated/ios/iphone55inch/light_timer.png
Normal file
After Width: | Height: | Size: 148 KiB |
After Width: | Height: | Size: 507 KiB |
After Width: | Height: | Size: 223 KiB |
After Width: | Height: | Size: 227 KiB |
After Width: | Height: | Size: 248 KiB |
After Width: | Height: | Size: 501 KiB |
BIN
screenshots/generated/ios/iphone65inch/light_settings.png
Normal file
After Width: | Height: | Size: 235 KiB |
BIN
screenshots/generated/ios/iphone65inch/light_timer.png
Normal file
After Width: | Height: | Size: 206 KiB |
60
screenshots/models/screenshot_args.dart
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
class ScreenshotArgs {
|
||||||
|
final String name;
|
||||||
|
final String deviceName;
|
||||||
|
final String platformFolder;
|
||||||
|
final String backgroundColor;
|
||||||
|
final bool isDark;
|
||||||
|
|
||||||
|
static const _pathArgsDelimited = '_';
|
||||||
|
|
||||||
|
ScreenshotArgs({
|
||||||
|
required this.name,
|
||||||
|
required String deviceName,
|
||||||
|
required this.platformFolder,
|
||||||
|
required this.backgroundColor,
|
||||||
|
required this.isDark,
|
||||||
|
}) : deviceName = deviceName.replaceAll(' ', _pathArgsDelimited).replaceAll(RegExp('[(|)]'), '').toLowerCase();
|
||||||
|
|
||||||
|
ScreenshotArgs.fromRawName({
|
||||||
|
required String name,
|
||||||
|
required String deviceName,
|
||||||
|
required this.platformFolder,
|
||||||
|
}) : name = name.split(_pathArgsDelimited)[1],
|
||||||
|
deviceName = deviceName.replaceAll(' ', _pathArgsDelimited).replaceAll(RegExp('[(|)]'), '').toLowerCase(),
|
||||||
|
backgroundColor = name.split(_pathArgsDelimited)[2],
|
||||||
|
isDark = name.contains('dark');
|
||||||
|
|
||||||
|
static const _folderPrefix = 'screenshots/generated';
|
||||||
|
String get nameWithTheme => '${isDark ? 'dark' : 'light'}$_pathArgsDelimited$name';
|
||||||
|
|
||||||
|
String toPathRaw() =>
|
||||||
|
'$_folderPrefix/raw/$platformFolder/$deviceName/$nameWithTheme$_pathArgsDelimited$backgroundColor.png';
|
||||||
|
String toPath(String layoutName) => '$_folderPrefix/$platformFolder/$layoutName/$nameWithTheme.png';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => jsonEncode(_toJson());
|
||||||
|
|
||||||
|
factory ScreenshotArgs.fromString(String data) => ScreenshotArgs._fromJson(jsonDecode(data) as Map<String, dynamic>);
|
||||||
|
|
||||||
|
factory ScreenshotArgs._fromJson(Map<String, dynamic> data) {
|
||||||
|
return ScreenshotArgs(
|
||||||
|
name: data['name'] as String,
|
||||||
|
deviceName: data['deviceName'] as String,
|
||||||
|
platformFolder: data['platformFolder'] as String,
|
||||||
|
backgroundColor: data['backgroundColor'] as String,
|
||||||
|
isDark: data['isDark'] as bool,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _toJson() {
|
||||||
|
return {
|
||||||
|
"name": name,
|
||||||
|
"deviceName": deviceName,
|
||||||
|
"platformFolder": platformFolder,
|
||||||
|
"backgroundColor": backgroundColor,
|
||||||
|
"isDark": isDark,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
19
screenshots/models/screenshot_config.dart
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
class ScreenshotConfig {
|
||||||
|
final String title;
|
||||||
|
final String subtitle;
|
||||||
|
final String screenshotName;
|
||||||
|
|
||||||
|
const ScreenshotConfig({
|
||||||
|
required this.title,
|
||||||
|
required this.subtitle,
|
||||||
|
required this.screenshotName,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ScreenshotConfig.fromJson(Map<String, dynamic> data) {
|
||||||
|
return ScreenshotConfig(
|
||||||
|
title: data['title'] as String,
|
||||||
|
subtitle: data['subtitle'] as String,
|
||||||
|
screenshotName: data['screenshotName'] as String,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
49
screenshots/models/screenshot_device.dart
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
enum ScreenshotDevicePlatform { android, ios }
|
||||||
|
|
||||||
|
class ScreenshotDevice {
|
||||||
|
final String name;
|
||||||
|
final ScreenshotDevicePlatform platform;
|
||||||
|
final ({int dx, int dy}) screenshotFrameOffset;
|
||||||
|
|
||||||
|
const ScreenshotDevice({
|
||||||
|
required this.name,
|
||||||
|
required this.platform,
|
||||||
|
this.screenshotFrameOffset = (dx: 0, dy: 0),
|
||||||
|
});
|
||||||
|
|
||||||
|
ScreenshotDevice.fromDisplayName({
|
||||||
|
required String displayName,
|
||||||
|
required this.platform,
|
||||||
|
this.screenshotFrameOffset = (dx: 0, dy: 0),
|
||||||
|
}) : name = displayName.replaceAll(' ', '_').toLowerCase();
|
||||||
|
|
||||||
|
String get systemOverlayPathLight =>
|
||||||
|
'screenshots/assets/system_overlays/${platform.name}/${name}_system_overlay_light.png';
|
||||||
|
String get systemOverlayPathDark =>
|
||||||
|
'screenshots/assets/system_overlays/${platform.name}/${name}_system_overlay_dark.png';
|
||||||
|
String get deviceFramePath => 'screenshots/assets/frames/${platform.name}/${name}_frame.png';
|
||||||
|
}
|
||||||
|
|
||||||
|
final screenshotDevices = <String, ScreenshotDevice>{
|
||||||
|
for (final d in _screenshotDevicesAndroid + _screenshotDevicesIos) d.name: d,
|
||||||
|
};
|
||||||
|
|
||||||
|
final List<ScreenshotDevice> _screenshotDevicesAndroid = [
|
||||||
|
ScreenshotDevice.fromDisplayName(
|
||||||
|
displayName: 'Pixel 6',
|
||||||
|
platform: ScreenshotDevicePlatform.android,
|
||||||
|
screenshotFrameOffset: (dx: 67, dy: 66),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
final List<ScreenshotDevice> _screenshotDevicesIos = [
|
||||||
|
ScreenshotDevice.fromDisplayName(
|
||||||
|
displayName: 'iPhone 8 Plus',
|
||||||
|
platform: ScreenshotDevicePlatform.ios,
|
||||||
|
),
|
||||||
|
ScreenshotDevice.fromDisplayName(
|
||||||
|
displayName: 'iPhone 13 Pro',
|
||||||
|
platform: ScreenshotDevicePlatform.ios,
|
||||||
|
screenshotFrameOffset: (dx: 72, dy: 60),
|
||||||
|
),
|
||||||
|
];
|
35
screenshots/models/screenshot_layout.dart
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
enum ScreenshotLayout {
|
||||||
|
android(
|
||||||
|
size: (width: 1440, height: 2560),
|
||||||
|
contentPadding: (left: 144, top: 132, right: 144, bottom: 132),
|
||||||
|
titleFontPath: 'screenshots/assets/fonts/SF-Pro-Display-Bold.zip',
|
||||||
|
subtitleFontPath: 'screenshots/assets/fonts/SF-Pro-Display-Regular.zip',
|
||||||
|
),
|
||||||
|
iphone65inch(
|
||||||
|
size: (width: 1290, height: 2796),
|
||||||
|
contentPadding: (left: 144, top: 184, right: 144, bottom: 184),
|
||||||
|
titleFontPath: 'screenshots/assets/fonts/SF-Pro-Display-Bold.zip',
|
||||||
|
subtitleFontPath: 'screenshots/assets/fonts/SF-Pro-Display-Regular.zip',
|
||||||
|
),
|
||||||
|
iphone55inch(
|
||||||
|
size: (width: 1242, height: 2208),
|
||||||
|
contentPadding: (left: 144, top: 144, right: 144, bottom: 144),
|
||||||
|
titleFontPath: 'screenshots/assets/fonts/SF-Pro-Display-Bold.zip',
|
||||||
|
subtitleFontPath: 'screenshots/assets/fonts/SF-Pro-Display-Regular.zip',
|
||||||
|
);
|
||||||
|
|
||||||
|
final ({int height, int width}) size;
|
||||||
|
final ({int left, int top, int right, int bottom}) contentPadding;
|
||||||
|
final String titleFontPath;
|
||||||
|
final String subtitleFontPath;
|
||||||
|
|
||||||
|
String get titleFontDarkPath => '${titleFontPath.split('.').first}-dark.zip';
|
||||||
|
String get subtitleFontDarkPath => '${subtitleFontPath.split('.').first}-dark.zip';
|
||||||
|
|
||||||
|
const ScreenshotLayout({
|
||||||
|
required this.size,
|
||||||
|
required this.contentPadding,
|
||||||
|
required this.titleFontPath,
|
||||||
|
required this.subtitleFontPath,
|
||||||
|
});
|
||||||
|
}
|
4
screenshots/scripts/convert_to_store_screenshots.sh
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
dart run screenshots/convert_to_store_screenshots.dart -p android -d pixel_6 -l android
|
||||||
|
dart run screenshots/convert_to_store_screenshots.dart -p ios -d iphone_13_pro -l iphone55inch
|
||||||
|
dart run screenshots/convert_to_store_screenshots.dart -p ios -d iphone_13_pro -l iphone65inch
|
||||||
|
#dart run screenshots/convert_to_store_screenshots.dart -p ios -d ipad_pro_12.9-inch_6th_generation -l ipad13inch
|
1
screenshots/scripts/generate_android_screenshots.sh
Normal file
|
@ -0,0 +1 @@
|
||||||
|
sh screenshots/scripts/generate_screenshots.sh "Pixel 6"
|
|
@ -1,13 +1,8 @@
|
||||||
devices_array=("iPhone 8 Plus" "iPhone 13 Pro" "iPhone 13 Pro Max" "iPhone 15 Pro" "iPhone 15 Pro Max" "iPad Pro (12.9-inch) (6th generation)")
|
simulators_array=("iPhone 13 Pro" "iPad Pro (12.9-inch) (6th generation)")
|
||||||
|
|
||||||
open -a Simulator
|
open -a Simulator
|
||||||
|
for i in "${simulators_array[@]}"; do # https://www.baeldung.com/linux/shell-script-iterate-over-string-list#2-understanding--and--special-indices
|
||||||
for i in "${devices_array[@]}"; do # https://www.baeldung.com/linux/shell-script-iterate-over-string-list#2-understanding--and--special-indices
|
|
||||||
echo "$i"
|
echo "$i"
|
||||||
xcrun simctl boot "$i"
|
xcrun simctl boot "$i"
|
||||||
#uid=$(echo "$(fvm flutter devices)" | sed -n -r "s/$i \(mobile\) • (.*) • .* • .*\(simulator\)/\1/p")
|
|
||||||
#echo $uid
|
|
||||||
sh screenshots/scripts/generate_screenshots.sh "$i"
|
sh screenshots/scripts/generate_screenshots.sh "$i"
|
||||||
done
|
done
|
||||||
|
|
||||||
killall 'Simulator'
|
killall 'Simulator'
|
||||||
|
|
14
screenshots/utils/parse_configs.dart
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import '../models/screenshot_config.dart';
|
||||||
|
|
||||||
|
Map<String, ScreenshotConfig> parseScreenshotConfigs([String locale = 'en']) {
|
||||||
|
final configPath = 'screenshots/assets/content/screenshot_titles_$locale.json';
|
||||||
|
final data = jsonDecode(File(configPath).readAsStringSync()) as Map<String, dynamic>;
|
||||||
|
final entries = (data['screenshots'] as List).map((value) {
|
||||||
|
final config = ScreenshotConfig.fromJson(value as Map<String, dynamic>);
|
||||||
|
return MapEntry(config.screenshotName, config);
|
||||||
|
});
|
||||||
|
return Map.fromEntries(entries);
|
||||||
|
}
|
|
@ -91,6 +91,7 @@ class _GoldenTestApplicationMockState extends State<GoldenTestApplicationMock> {
|
||||||
IAPProduct(
|
IAPProduct(
|
||||||
storeId: IAPProductType.paidFeatures.storeId,
|
storeId: IAPProductType.paidFeatures.storeId,
|
||||||
status: widget.productStatus,
|
status: widget.productStatus,
|
||||||
|
price: '0.0\$',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: ApplicationWrapper(
|
child: ApplicationWrapper(
|
||||||
|
|
|
@ -26,6 +26,7 @@ void main() {
|
||||||
IAPProduct(
|
IAPProduct(
|
||||||
storeId: IAPProductType.paidFeatures.storeId,
|
storeId: IAPProductType.paidFeatures.storeId,
|
||||||
status: productStatus,
|
status: productStatus,
|
||||||
|
price: '0.0\$',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: EquipmentProfileProvider(
|
child: EquipmentProfileProvider(
|
||||||
|
|
|
@ -26,6 +26,7 @@ void main() {
|
||||||
IAPProduct(
|
IAPProduct(
|
||||||
storeId: IAPProductType.paidFeatures.storeId,
|
storeId: IAPProductType.paidFeatures.storeId,
|
||||||
status: productStatus,
|
status: productStatus,
|
||||||
|
price: '0.0\$',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: FilmsProvider(
|
child: FilmsProvider(
|
||||||
|
|
BIN
test/screens/lightmeter_pro/goldens/lightmeter_pro_screen.png
Normal file
After Width: | Height: | Size: 301 KiB |
|
@ -0,0 +1,49 @@
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:golden_toolkit/golden_toolkit.dart';
|
||||||
|
import 'package:lightmeter/data/models/theme_type.dart';
|
||||||
|
import 'package:lightmeter/screens/lightmeter_pro/screen_lightmeter_pro.dart';
|
||||||
|
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import '../../application_mock.dart';
|
||||||
|
import '../../utils/golden_test_set_theme.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
setUpAll(() {
|
||||||
|
SharedPreferences.setMockInitialValues({});
|
||||||
|
});
|
||||||
|
|
||||||
|
testGoldens(
|
||||||
|
'LightmeterProScreen golden test',
|
||||||
|
(tester) async {
|
||||||
|
final builder = DeviceBuilder();
|
||||||
|
builder.addScenario(
|
||||||
|
name: 'Get Pro',
|
||||||
|
widget: const _MockLightmeterProFlow(),
|
||||||
|
onCreate: (scenarioWidgetKey) async {
|
||||||
|
if (scenarioWidgetKey.toString().contains('Dark')) {
|
||||||
|
await setTheme<LightmeterProScreen>(tester, scenarioWidgetKey, ThemeType.dark);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await tester.pumpDeviceBuilder(builder);
|
||||||
|
await screenMatchesGolden(
|
||||||
|
tester,
|
||||||
|
'lightmeter_pro_screen',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MockLightmeterProFlow extends StatelessWidget {
|
||||||
|
const _MockLightmeterProFlow();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GoldenTestApplicationMock(
|
||||||
|
productStatus: IAPProductStatus.purchasable,
|
||||||
|
child: LightmeterProScreen(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,6 +28,7 @@ void main() {
|
||||||
IAPProduct(
|
IAPProduct(
|
||||||
storeId: IAPProductType.paidFeatures.storeId,
|
storeId: IAPProductType.paidFeatures.storeId,
|
||||||
status: IAPProductStatus.purchased,
|
status: IAPProductStatus.purchased,
|
||||||
|
price: '0.0\$',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: EquipmentProfileProvider(
|
child: EquipmentProfileProvider(
|
||||||
|
|
|
@ -37,7 +37,7 @@ void main() {
|
||||||
expect(pickerFinder, findsOneWidget);
|
expect(pickerFinder, findsOneWidget);
|
||||||
expect(find.descendant(of: pickerFinder, matching: find.text(S.current.fastestExposurePair)), findsOneWidget);
|
expect(find.descendant(of: pickerFinder, matching: find.text(S.current.fastestExposurePair)), findsOneWidget);
|
||||||
expect(find.descendant(of: pickerFinder, matching: find.text(S.current.slowestExposurePair)), findsOneWidget);
|
expect(find.descendant(of: pickerFinder, matching: find.text(S.current.slowestExposurePair)), findsOneWidget);
|
||||||
expect(find.descendant(of: pickerFinder, matching: find.text('f/1.0 - 1/2000')), findsOneWidget);
|
expect(find.descendant(of: pickerFinder, matching: find.text('f/1.0 - 1/4000')), findsOneWidget);
|
||||||
expect(find.descendant(of: pickerFinder, matching: find.text('f/45 - 1"')), findsOneWidget);
|
expect(find.descendant(of: pickerFinder, matching: find.text('f/45 - 1"')), findsOneWidget);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -27,6 +27,7 @@ void main() {
|
||||||
IAPProduct(
|
IAPProduct(
|
||||||
storeId: IAPProductType.paidFeatures.storeId,
|
storeId: IAPProductType.paidFeatures.storeId,
|
||||||
status: IAPProductStatus.purchased,
|
status: IAPProductStatus.purchased,
|
||||||
|
price: '0.0\$',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: FilmsProvider(
|
child: FilmsProvider(
|
||||||
|
|
|
@ -79,7 +79,7 @@ extension WidgetTesterActions on WidgetTester {
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: AnimatedDialogPicker<int>(
|
child: AnimatedDialogPicker<int>(
|
||||||
icon: Icons.iso,
|
icon: Icons.iso_outlined,
|
||||||
title: '',
|
title: '',
|
||||||
subtitle: '',
|
subtitle: '',
|
||||||
selectedValue: 0,
|
selectedValue: 0,
|
||||||
|
|
|
@ -51,7 +51,7 @@ extension WidgetTesterActions on WidgetTester {
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: AnimatedDialogPicker<int>(
|
child: AnimatedDialogPicker<int>(
|
||||||
icon: Icons.iso,
|
icon: Icons.iso_outlined,
|
||||||
title: '',
|
title: '',
|
||||||
subtitle: '',
|
subtitle: '',
|
||||||
selectedValue: 0,
|
selectedValue: 0,
|
||||||
|
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.6 MiB |
|
@ -15,6 +15,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import '../../../integration_test/utils/finder_actions.dart';
|
import '../../../integration_test/utils/finder_actions.dart';
|
||||||
import '../../../integration_test/utils/platform_channel_mock.dart';
|
import '../../../integration_test/utils/platform_channel_mock.dart';
|
||||||
import '../../application_mock.dart';
|
import '../../application_mock.dart';
|
||||||
|
import '../../utils/golden_test_set_theme.dart';
|
||||||
|
|
||||||
class _MeteringScreenConfig {
|
class _MeteringScreenConfig {
|
||||||
final IAPProductStatus iapProductStatus;
|
final IAPProductStatus iapProductStatus;
|
||||||
|
@ -54,16 +55,6 @@ void main() {
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setTheme(WidgetTester tester, Key scenarioWidgetKey, ThemeType themeType) async {
|
|
||||||
final flow = find.descendant(
|
|
||||||
of: find.byKey(scenarioWidgetKey),
|
|
||||||
matching: find.byType(MeteringFlow),
|
|
||||||
);
|
|
||||||
final BuildContext context = tester.element(flow);
|
|
||||||
UserPreferencesProvider.of(context).setThemeType(themeType);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> takePhoto(WidgetTester tester, Key scenarioWidgetKey) async {
|
Future<void> takePhoto(WidgetTester tester, Key scenarioWidgetKey) async {
|
||||||
final button = find.descendant(
|
final button = find.descendant(
|
||||||
of: find.byKey(scenarioWidgetKey),
|
of: find.byKey(scenarioWidgetKey),
|
||||||
|
@ -110,7 +101,7 @@ void main() {
|
||||||
onCreate: (scenarioWidgetKey) async {
|
onCreate: (scenarioWidgetKey) async {
|
||||||
await setEvSource(tester, scenarioWidgetKey, scenario.evSourceType);
|
await setEvSource(tester, scenarioWidgetKey, scenario.evSourceType);
|
||||||
if (scenarioWidgetKey.toString().contains('Dark')) {
|
if (scenarioWidgetKey.toString().contains('Dark')) {
|
||||||
await setTheme(tester, scenarioWidgetKey, ThemeType.dark);
|
await setTheme<MeteringFlow>(tester, scenarioWidgetKey, ThemeType.dark);
|
||||||
}
|
}
|
||||||
if (scenario.evSourceType == EvSourceType.camera) {
|
if (scenario.evSourceType == EvSourceType.camera) {
|
||||||
await takePhoto(tester, scenarioWidgetKey);
|
await takePhoto(tester, scenarioWidgetKey);
|
||||||
|
|