diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8716e77..250b299 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 diff --git a/README.md b/README.md index 715435f..98b2603 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/lib/bluetooth/devices/bluetooth_device.dart b/lib/bluetooth/devices/bluetooth_device.dart index b4ffb9c..585c9fb 100644 --- a/lib/bluetooth/devices/bluetooth_device.dart +++ b/lib/bluetooth/devices/bluetooth_device.dart @@ -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), diff --git a/lib/pages/configuration.dart b/lib/pages/configuration.dart index baae10d..2425ec9 100644 --- a/lib/pages/configuration.dart +++ b/lib/pages/configuration.dart @@ -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 { children: [ Select( 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 { 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, diff --git a/lib/pages/navigation.dart b/lib/pages/navigation.dart index 303820c..7472873 100644 --- a/lib/pages/navigation.dart +++ b/lib/pages/navigation.dart @@ -127,7 +127,9 @@ class _NavigationState extends State { 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( diff --git a/lib/pages/touch_area.dart b/lib/pages/touch_area.dart index 2491a90..0e2fd02 100644 --- a/lib/pages/touch_area.dart +++ b/lib/pages/touch_area.dart @@ -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, ), diff --git a/lib/utils/core.dart b/lib/utils/core.dart index d5490b1..8d66939 100644 --- a/lib/utils/core.dart +++ b/lib/utils/core.dart @@ -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 && diff --git a/lib/utils/keymap/apps/my_whoosh.dart b/lib/utils/keymap/apps/my_whoosh.dart index 0780494..a9f1c84 100644 --- a/lib/utils/keymap/apps/my_whoosh.dart +++ b/lib/utils/keymap/apps/my_whoosh.dart @@ -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 diff --git a/lib/utils/keymap/apps/training_peaks.dart b/lib/utils/keymap/apps/training_peaks.dart index fb18824..a7f9fe9 100644 --- a/lib/utils/keymap/apps/training_peaks.dart +++ b/lib/utils/keymap/apps/training_peaks.dart @@ -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( diff --git a/lib/widgets/keymap_explanation.dart b/lib/widgets/keymap_explanation.dart index c8be488..f080d08 100644 --- a/lib/widgets/keymap_explanation.dart +++ b/lib/widgets/keymap_explanation.dart @@ -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, diff --git a/test/custom_frame.dart b/test/custom_frame.dart index 8300df6..d8ff6b8 100644 --- a/test/custom_frame.dart +++ b/test/custom_frame.dart @@ -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), - }, - ), + ], ), ), - ], - ), - ), - ); + ); } } diff --git a/test/screenshot_test.dart b/test/screenshot_test.dart index 24ef04d..c38323c 100644 --- a/test/screenshot_test.dart +++ b/test/screenshot_test.dart @@ -15,6 +15,15 @@ import 'package:universal_ble/universal_ble.dart'; import 'custom_frame.dart'; +enum DeviceType { + android, + androidTablet, + iPhone, + iPad, + desktop, + noFrame, +} + Future main() async { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); PackageInfo.setMockInitialValues( @@ -45,9 +54,13 @@ Future 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 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 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 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 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 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 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 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 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', ), ); }