screenshot work

This commit is contained in:
Jonas Bark
2025-12-06 13:42:18 +01:00
parent 6b5c202e93
commit c8c449d2ef
12 changed files with 150 additions and 90 deletions

View File

@@ -173,7 +173,7 @@ jobs:
- name: Generate screenshots for App Stores
if: inputs.build_github
run: |
flutter test test/screenshot_test.dart
flutter test --update-goldens
zip -r BikeControl.storeassets.zip screenshots
echo "Screenshots generated successfully"
@@ -264,7 +264,9 @@ jobs:
overwrite: true
name: Releases
path: |
screenshots/device-GitHub-600x900.png
screenshots/device-noFrame-1100x2390.png
screenshots/trainer-noFrame-1100x2390.png
screenshots/customization-noFrame-1100x2390.png
build/BikeControl.screenshots.zip
#10 Extract Version
@@ -280,7 +282,7 @@ jobs:
if: inputs.build_github
uses: ncipollo/release-action@v1
with:
artifacts: "build/app/outputs/flutter-apk/BikeControl.android.apk,build/macos/Build/Products/Release/BikeControl.macos.zip,build/BikeControl.screenshots.zip,screenshots/device-GitHub-600x900.png"
artifacts: "build/app/outputs/flutter-apk/BikeControl.android.apk,build/macos/Build/Products/Release/BikeControl.macos.zip,build/BikeControl.screenshots.zip,screenshots/device-noFrame-1100x2390.png,screenshots/trainer-noFrame-1100x2390.png,screenshots/customization-noFrame-1100x2390.png"
allowUpdates: true
prerelease: true
bodyFile: /tmp/release_body.md

View File

@@ -78,6 +78,8 @@ The app connects to your Controller devices (such as Zwift ones) automatically.
- Connect to the trainer app using Network
- available on Android, iOS, macOS, Windows
- supported by e.g. MyWhoosh, Rouvy and Zwift
- Connect to supported trainer app using the [OpenBikeControl](https://openbikecontrol.org) protocol
- available on Android, iOS, macOS, Windows
## Alternatives
- [qdomyos-zwift](https://www.qzfitness.com/) directly controls the trainer (as opposed to controlling the trainer app). This can be useful if your trainer app does not support virtual shifting.

View File

@@ -264,8 +264,8 @@ abstract class BluetoothDevice extends BaseDevice {
runSpacing: 12,
children: [
SizedBox(
width: screenshotMode ? 200 : null,
height: screenshotMode ? 100 : null,
width: screenshotMode ? 160 : null,
height: screenshotMode ? 70 : null,
child: Card(
filled: true,
fillColor: Theme.of(context).colorScheme.background,
@@ -286,8 +286,8 @@ abstract class BluetoothDevice extends BaseDevice {
),
if (batteryLevel != null)
SizedBox(
width: screenshotMode ? 200 : null,
height: screenshotMode ? 100 : null,
width: screenshotMode ? 160 : null,
height: screenshotMode ? 70 : null,
child: Card(
filled: true,
fillColor: Theme.of(context).colorScheme.background,
@@ -312,8 +312,8 @@ abstract class BluetoothDevice extends BaseDevice {
),
if (firmwareVersion != null)
SizedBox(
width: screenshotMode ? 200 : null,
height: screenshotMode ? 100 : null,
width: screenshotMode ? 160 : null,
height: screenshotMode ? 70 : null,
child: Card(
filled: true,
padding: EdgeInsets.all(12),
@@ -342,8 +342,8 @@ abstract class BluetoothDevice extends BaseDevice {
),
if (rssi != null)
SizedBox(
width: screenshotMode ? 200 : null,
height: screenshotMode ? 100 : null,
width: screenshotMode ? 160 : null,
height: screenshotMode ? 70 : null,
child: Card(
filled: true,
padding: EdgeInsets.all(12),

View File

@@ -1,4 +1,5 @@
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:swift_control/main.dart';
import 'package:swift_control/pages/button_edit.dart';
import 'package:swift_control/utils/core.dart';
import 'package:swift_control/utils/i18n_extension.dart';
@@ -57,7 +58,7 @@ class _ConfigurationPageState extends State<ConfigurationPage> {
children: [
Select<SupportedApp>(
constraints: BoxConstraints(maxWidth: 400, minWidth: 400),
itemBuilder: (c, app) => Text(app.name),
itemBuilder: (c, app) => Text(screenshotMode ? 'Trainer app' : app.name),
popup: SelectPopup(
items: SelectItemList(
children: SupportedApp.supportedApps.map((app) {
@@ -94,7 +95,9 @@ class _ConfigurationPageState extends State<ConfigurationPage> {
if (core.settings.getTrainerApp() != null) ...[
SizedBox(height: 8),
Text(
context.i18n.selectTargetWhereAppRuns(core.settings.getTrainerApp()?.name ?? 'the Trainer app'),
context.i18n.selectTargetWhereAppRuns(
screenshotMode ? 'Trainer app' : core.settings.getTrainerApp()?.name ?? 'the Trainer app',
),
).small,
Flex(
direction: isMobile ? Axis.vertical : Axis.horizontal,

View File

@@ -127,7 +127,9 @@ class _NavigationState extends State<Navigation> {
return Scaffold(
headers: [
AppBar(
padding: const EdgeInsets.only(top: 12, bottom: 8, left: 12, right: 12) * Theme.of(context).scaling,
padding:
const EdgeInsets.only(top: 12, bottom: 8, left: 12, right: 12) *
(screenshotMode ? 2 : Theme.of(context).scaling),
title: AppTitle(),
backgroundColor: Theme.of(context).colorScheme.background,
trailing: buildMenuButtons(

View File

@@ -9,6 +9,7 @@ import 'package:flutter/services.dart';
import 'package:image_picker/image_picker.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:swift_control/main.dart';
import 'package:swift_control/utils/core.dart';
import 'package:swift_control/utils/i18n_extension.dart';
import 'package:swift_control/widgets/keymap_explanation.dart';
@@ -455,7 +456,7 @@ class _KeyWidget extends StatelessWidget {
child: Text(
label.splitByUpperCase(),
style: TextStyle(
fontFamily: 'monospace',
fontFamily: screenshotMode ? null : 'monospace',
fontSize: 12,
color: Theme.of(context).colorScheme.primaryForeground,
),

View File

@@ -176,6 +176,7 @@ class CoreLogic {
AppInfo? get obpConnectedApp => core.obpMdnsEmulator.isConnected.value ?? core.obpBluetoothEmulator.isConnected.value;
bool get emulatorEnabled =>
screenshotMode ||
(core.settings.getMyWhooshLinkEnabled() && showMyWhooshLink) ||
(core.settings.getZwiftBleEmulatorEnabled() && showZwiftBleEmulator) ||
(core.settings.getZwiftMdnsEmulatorEnabled() && showZwiftMsdnEmulator) ||
@@ -195,6 +196,7 @@ class CoreLogic {
((showLocalControl && core.settings.getLocalEnabled()) || (isRemoteControlEnabled));
bool get hasNoConnectionMethod =>
!screenshotMode &&
!isZwiftBleEnabled &&
!isZwiftMdnsEnabled &&
!showObpActions &&

View File

@@ -1,5 +1,6 @@
import 'package:dartx/dartx.dart';
import 'package:flutter/services.dart';
import 'package:swift_control/main.dart';
import 'package:swift_control/utils/keymap/apps/supported_app.dart';
import 'package:swift_control/utils/requirements/multi.dart';
@@ -13,7 +14,7 @@ class MyWhoosh extends SupportedApp {
packageName: "com.mywhoosh.whooshgame",
compatibleTargets: Target.values,
supportsZwiftEmulation: false,
supportsOpenBikeProtocol: false,
supportsOpenBikeProtocol: screenshotMode,
keymap: Keymap(
keyPairs: [
...ControllerButton.values

View File

@@ -16,9 +16,7 @@ class TrainingPeaks extends SupportedApp {
: super(
name: 'TrainingPeaks Virtual / IndieVelo',
packageName: "com.indieVelo.client",
compatibleTargets: !kIsWeb && Platform.isIOS
? Target.values.filterNot((e) => e == Target.thisDevice).toList()
: Target.values,
compatibleTargets: !kIsWeb && Platform.isIOS ? [Target.otherDevice] : Target.values,
supportsZwiftEmulation: false,
supportsOpenBikeProtocol: false,
keymap: Keymap(

View File

@@ -163,10 +163,10 @@ class _ButtonEditor extends StatelessWidget {
currentProfile,
skipName: '$currentProfile (Copy)',
);
if (newName != null) {
if (newName != null && context.mounted) {
buildToast(context, title: context.i18n.createdNewCustomProfile(newName));
final selectedKeyPair = core.actionHandler.supportedApp!.keymap.keyPairs.firstWhere(
(e) => e == this.keyPair,
(e) => e == keyPair,
);
await openDrawer(
context: context,

View File

@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:golden_screenshot/golden_screenshot.dart';
import 'package:swift_control/widgets/ui/colors.dart';
import 'screenshot_test.dart';
class CustomFrame extends StatelessWidget {
const CustomFrame({
super.key,
@@ -12,7 +14,7 @@ class CustomFrame extends StatelessWidget {
required this.platform,
});
final TargetPlatform platform;
final DeviceType platform;
final String title;
final ScreenshotDevice device;
final ScreenshotFrameColors? frameColors;
@@ -20,61 +22,64 @@ class CustomFrame extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [BKColor.main, BKColor.mainEnd],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Stack(
fit: StackFit.expand,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 54, horizontal: 8),
child: Text(
title,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: 36,
fontWeight: FontWeight.bold,
final borderRadiusValue = 26.0;
return platform == DeviceType.noFrame
? Scaffold(body: child)
: Scaffold(
body: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [BKColor.main, BKColor.mainEnd],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
),
Positioned(
top: 170,
left: 8,
right: 8,
bottom: -30,
child: FittedBox(
child: Container(
width: device.resolution.width / device.pixelRatio,
height: device.resolution.height / device.pixelRatio,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(64),
child: Stack(
fit: StackFit.expand,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 54, horizontal: 8),
child: Text(
title,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: 36,
fontWeight: FontWeight.bold,
),
),
),
foregroundDecoration: BoxDecoration(
border: Border.all(width: 8),
borderRadius: BorderRadius.circular(64),
Positioned(
top: 170,
left: 8,
right: 8,
bottom: -30,
child: FittedBox(
child: Container(
width: device.resolution.width / device.pixelRatio,
height: device.resolution.height / device.pixelRatio,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(borderRadiusValue),
),
foregroundDecoration: BoxDecoration(
border: Border.all(width: 8),
borderRadius: BorderRadius.circular(borderRadiusValue),
),
clipBehavior: Clip.antiAlias,
child: switch (platform) {
DeviceType.android => ScreenshotFrame.androidPhone(device: device, child: child),
DeviceType.androidTablet => ScreenshotFrame.androidTablet(device: device, child: child),
DeviceType.iPhone => ScreenshotFrame.iphone(device: device, child: child),
DeviceType.iPad => ScreenshotFrame.ipad(device: device, child: child),
DeviceType.desktop => ScreenshotFrame.noFrame(device: device, child: child),
DeviceType.noFrame => throw UnimplementedError(),
},
),
),
),
clipBehavior: Clip.antiAlias,
child: switch (platform) {
TargetPlatform.android => ScreenshotFrame.androidPhone(device: device, child: child),
TargetPlatform.fuchsia => throw UnimplementedError(),
TargetPlatform.iOS => ScreenshotFrame.iphone(device: device, child: child),
TargetPlatform.linux => throw UnimplementedError(),
TargetPlatform.macOS => ScreenshotFrame.noFrame(device: device, child: child),
TargetPlatform.windows => ScreenshotFrame.noFrame(device: device, child: child),
},
),
],
),
),
],
),
),
);
);
}
}

View File

@@ -15,6 +15,15 @@ import 'package:universal_ble/universal_ble.dart';
import 'custom_frame.dart';
enum DeviceType {
android,
androidTablet,
iPhone,
iPad,
desktop,
noFrame,
}
Future<void> main() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
PackageInfo.setMockInitialValues(
@@ -45,9 +54,13 @@ Future<void> main() async {
..batteryLevel = 81,
]);
final List<(TargetPlatform type, Size size)> sizes = [
(TargetPlatform.android, Size(1320, 2868)),
(TargetPlatform.iOS, Size(1320, 2868)),
final List<({DeviceType type, TargetPlatform platform, Size size})> sizes = [
(type: DeviceType.android, platform: TargetPlatform.android, size: Size(1320, 2868)),
(type: DeviceType.androidTablet, platform: TargetPlatform.android, size: Size(3840, 2400)),
(type: DeviceType.iPhone, platform: TargetPlatform.iOS, size: Size(1242, 2688)),
(type: DeviceType.iPad, platform: TargetPlatform.iOS, size: Size(2752, 2064)),
(type: DeviceType.desktop, platform: TargetPlatform.windows, size: Size(2752, 2064)),
(type: DeviceType.noFrame, platform: TargetPlatform.windows, size: Size(1320, 2868) / 1.2),
/*('iPhone', Size(1242, 2688)),
('macOS', Size(1280, 800)),
('GitHub', Size(600, 900)),*/
@@ -56,15 +69,15 @@ Future<void> main() async {
debugDisableShadows = true;
screenshotMode = true;
testGoldens('Device', (WidgetTester tester) async {
testGoldens('Init', (WidgetTester tester) async {
screenshotMode = true;
await tester.loadAssets();
for (final size in sizes) {
await tester.pumpWidget(
ScreenshotApp(
device: ScreenshotDevice(
platform: size.$1,
resolution: size.$2,
platform: size.platform,
resolution: size.size,
pixelRatio: 3,
goldenSubFolder: 'iphoneScreenshots/',
frameBuilder:
@@ -73,7 +86,37 @@ Future<void> main() async {
required ScreenshotFrameColors? frameColors,
required Widget child,
}) => CustomFrame(
platform: size.$1,
platform: size.type,
title: 'BikeControl connects to your favorite controller',
device: device,
child: child,
),
),
home: BikeControlApp(
page: BCPage.devices,
),
),
);
await tester.pump();
}
});
testGoldens('Device', (WidgetTester tester) async {
for (final size in sizes) {
await tester.pumpWidget(
ScreenshotApp(
device: ScreenshotDevice(
platform: size.platform,
resolution: size.size,
pixelRatio: 3,
goldenSubFolder: 'iphoneScreenshots/',
frameBuilder:
({
required ScreenshotDevice device,
required ScreenshotFrameColors? frameColors,
required Widget child,
}) => CustomFrame(
platform: size.type,
title: 'BikeControl connects to your favorite controller',
device: device,
child: child,
@@ -89,19 +132,20 @@ Future<void> main() async {
await expectLater(
find.byType(ma.Scaffold),
matchesGoldenFile(
'../screenshots/device-${size.$1.name}-${size.$2.width.toInt()}-${size.$2.height.toInt()}.png',
'../screenshots/device-${size.type.name}-${size.size.width.toInt()}x${size.size.height.toInt()}.png',
),
);
}
});
testGoldens('Trainer', (WidgetTester tester) async {
screenshotMode = true;
for (final size in sizes) {
await tester.pumpWidget(
ScreenshotApp(
device: ScreenshotDevice(
platform: size.$1,
resolution: size.$2,
platform: size.platform,
resolution: size.size,
pixelRatio: 3,
goldenSubFolder: 'iphoneScreenshots/',
frameBuilder:
@@ -110,8 +154,8 @@ Future<void> main() async {
required ScreenshotFrameColors? frameColors,
required Widget child,
}) => CustomFrame(
platform: size.$1,
title: 'BikeControl connects to your favorite controller',
platform: size.type,
title: 'Choose how BikeControl connects to your trainer',
device: device,
child: child,
),
@@ -126,7 +170,7 @@ Future<void> main() async {
await expectLater(
find.byType(ma.Scaffold),
matchesGoldenFile(
'../screenshots/trainer-${size.$1.name}-${size.$2.width.toInt()}-${size.$2.height.toInt()}.png',
'../screenshots/trainer-${size.type.name}-${size.size.width.toInt()}x${size.size.height.toInt()}.png',
),
);
}
@@ -139,8 +183,8 @@ Future<void> main() async {
await tester.pumpWidget(
ScreenshotApp(
device: ScreenshotDevice(
platform: size.$1,
resolution: size.$2,
platform: size.platform,
resolution: size.size,
pixelRatio: 3,
goldenSubFolder: 'iphoneScreenshots/',
frameBuilder:
@@ -149,7 +193,7 @@ Future<void> main() async {
required ScreenshotFrameColors? frameColors,
required Widget child,
}) => CustomFrame(
platform: size.$1,
platform: size.type,
title: 'Customize every controller button',
device: device,
child: child,
@@ -165,7 +209,7 @@ Future<void> main() async {
await expectLater(
find.byType(ma.Scaffold),
matchesGoldenFile(
'../screenshots/customization-${size.$1.name}-${size.$2.width.toInt()}-${size.$2.height.toInt()}.png',
'../screenshots/customization-${size.type.name}-${size.size.width.toInt()}x${size.size.height.toInt()}.png',
),
);
}