diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6500fd6..dd8f7c9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -193,7 +193,6 @@ jobs: whatsNewDirectory: whatsnew - name: Build iOS app and release - if: github.ref == 'refs/heads/ios' && false env: APPSTORE_API_KEY: ${{ secrets.APPSTORE_API_KEY }} APPSTORE_API_ISSUER_ID: ${{ secrets.APPSTORE_API_ISSUER_ID }} diff --git a/CHANGELOG.md b/CHANGELOG.md index c1b36fd..fc07437 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - your iPad or other tablet runs e.g. MyWhoosh (does not need to have SwiftControl installed) - after pairing SwiftControl to your iPad / tablet via Bluetooth your phone will send the button presses to your iPad / tablet - Ride: analog paddles are now supported thanks to contributor @jmoro +- you can now zoom in and out in the Keymap customization screen ### 2.6.3 (2025-10-01) - fix a few issues with the new touch placement feature diff --git a/lib/pages/touch_area.dart b/lib/pages/touch_area.dart index 9329c79..d16be10 100644 --- a/lib/pages/touch_area.dart +++ b/lib/pages/touch_area.dart @@ -37,6 +37,7 @@ class _TouchAreaSetupPageState extends State { File? _backgroundImage; late StreamSubscription _actionSubscription; ZwiftButton? _pressedButton; + final TransformationController _transformationController = TransformationController(); Rect? _imageRect; @@ -50,9 +51,6 @@ class _TouchAreaSetupPageState extends State { // need to decode image to get its size so we can have a percentage mapping if (actionHandler is RemoteActions) { decodeImageFromList(_backgroundImage!.readAsBytesSync()).then((decodedImage) { - print(decodedImage.width); - print(decodedImage.height); - // calculate image rectangle in the current screen, given it's boxfit contain final screenSize = MediaQuery.sizeOf(context); final imageAspectRatio = decodedImage.width / decodedImage.height; @@ -70,7 +68,7 @@ class _TouchAreaSetupPageState extends State { final left = (screenSize.width - width) / 2; _imageRect = Rect.fromLTWH(left, 0, width, height); } - print('Image Rect: $_imageRect'); + setState(() {}); }); } }); @@ -134,7 +132,7 @@ class _TouchAreaSetupPageState extends State { keyPair = KeyPair( touchPosition: _imageRect != null - ? Offset(actionHandler.supportedApp!.keymap.keyPairs.length * 10, 10) + ? Offset((actionHandler.supportedApp!.keymap.keyPairs.length + 1) * 10, 10) : context.size! .center(Offset.zero) .translate(actionHandler.supportedApp!.keymap.keyPairs.length * 40, 0), @@ -179,7 +177,7 @@ class _TouchAreaSetupPageState extends State { print('Difference: $differenceInHeight'); } - final isOnTheRightEdge = position.dx > (MediaQuery.sizeOf(context).width - 250); + //final isOnTheRightEdge = position.dx > (MediaQuery.sizeOf(context).width - 250); final iconSize = 40.0; final draggable = [ @@ -324,13 +322,24 @@ class _TouchAreaSetupPageState extends State { child: Tooltip( message: 'Drag to reposition', child: Draggable( + dragAnchorStrategy: (widget, context, position) { + final scale = _transformationController.value.getMaxScaleOnAxis(); + final RenderBox renderObject = context.findRenderObject() as RenderBox; + return renderObject.globalToLocal(position).scale(scale, scale); + }, feedback: Material(color: Colors.transparent, child: icon), childWhenDragging: const SizedBox.shrink(), - onDraggableCanceled: (velo, offset) { + onDragEnd: (details) { // otherwise simulated touch will move it - if (velo.pixelsPerSecond.distance > 0) { - final fixedPosition = offset + Offset(iconSize / 2, differenceInHeight + iconSize / 2); - setState(() => onPositionChanged(fixedPosition)); + if (details.velocity.pixelsPerSecond.distance > 0) { + final matrix = Matrix4.inverted(_transformationController.value); + final height = 0; + final sceneY = details.offset.dy - height; + final viewportPoint = MatrixUtils.transformPoint( + matrix, + Offset(details.offset.dx, sceneY) + Offset(iconSize / 2, differenceInHeight + iconSize / 2), + ); + setState(() => onPositionChanged(viewportPoint)); } }, child: icon, @@ -344,110 +353,119 @@ class _TouchAreaSetupPageState extends State { final isDesktop = Platform.isWindows || Platform.isLinux || Platform.isMacOS; final devicePixelRatio = isDesktop ? 1.0 : MediaQuery.devicePixelRatioOf(context); return Scaffold( - body: Stack( - children: [ - if (_backgroundImage != null) - Positioned.fill(child: Opacity(opacity: 0.5, child: Image.file(_backgroundImage!, fit: BoxFit.contain))) - else - Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisSize: MainAxisSize.min, - spacing: 8, - children: [ - Text('''1. Create an in-game screenshot of your app (e.g. within MyWhoosh) in landscape orientation + body: InteractiveViewer( + transformationController: _transformationController, + child: Stack( + children: [ + if (_backgroundImage != null) + Positioned.fill(child: Opacity(opacity: 0.5, child: Image.file(_backgroundImage!, fit: BoxFit.contain))) + else + Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + spacing: 8, + children: [ + Text( + '''1. Create an in-game screenshot of your app (e.g. within MyWhoosh) in landscape orientation 2. Load the screenshot with the button below 3. The app is automatically set to landscape orientation for accurate mapping 4. Press a button on your Click device to create a touch area 5. Drag the touch areas to the desired position on the screenshot -6. Save and close this screen'''), - ElevatedButton( - onPressed: () { - _pickScreenshot(); - }, - child: Text('Load in-game screenshot for placement'), - ), - ], +6. Save and close this screen''', + ), + ElevatedButton( + onPressed: () { + _pickScreenshot(); + }, + child: Text('Load in-game screenshot for placement'), + ), + ], + ), ), ), - ), - // draw image rect for debugging - if (_imageRect != null && _backgroundImage != null) - Positioned.fromRect( - rect: _imageRect!, - child: Container(decoration: BoxDecoration(border: Border.all(color: Colors.green, width: 2))), - ), + // draw image rect for debugging + if (_imageRect != null && _backgroundImage != null) + Positioned.fromRect( + rect: _imageRect!, + child: Container(decoration: BoxDecoration(border: Border.all(color: Colors.green, width: 2))), + ), - if (actionHandler is! RemoteActions || _imageRect != null) - ...?actionHandler.supportedApp?.keymap.keyPairs.map((keyPair) { - final Offset offset; + if (actionHandler is! RemoteActions || _imageRect != null) + ...?actionHandler.supportedApp?.keymap.keyPairs.map((keyPair) { + final Offset offset; - if (_imageRect != null) { - // map the percentage position to the image rect - final relativeX = min(100.0, keyPair.touchPosition.dx) / 100.0; - final relativeY = min(100.0, keyPair.touchPosition.dy) / 100.0; - //print('Relative position: $relativeX, $relativeY'); - offset = Offset( - _imageRect!.left + relativeX * _imageRect!.width, - _imageRect!.top + relativeY * _imageRect!.height, + if (_imageRect != null) { + // map the percentage position to the image rect + final relativeX = min(100.0, keyPair.touchPosition.dx) / 100.0; + final relativeY = min(100.0, keyPair.touchPosition.dy) / 100.0; + //print('Relative position: $relativeX, $relativeY'); + offset = Offset( + _imageRect!.left + relativeX * _imageRect!.width, + _imageRect!.top + relativeY * _imageRect!.height, + ); + } else { + offset = Offset( + keyPair.touchPosition.dx / devicePixelRatio, + keyPair.touchPosition.dy / devicePixelRatio, + ); + } + + //print('Drawing at offset $offset for keypair with position ${keyPair.touchPosition}'); + + return _buildDraggableArea( + enableTouch: true, + position: offset, + keyPair: keyPair, + onPositionChanged: (newPos) { + if (_imageRect != null) { + // convert to percentage + final relativeX = ((newPos.dx - _imageRect!.left) / _imageRect!.width).clamp(0.0, 1.0); + final relativeY = ((newPos.dy - _imageRect!.top) / _imageRect!.height).clamp(0.0, 1.0); + keyPair.touchPosition = Offset(relativeX * 100.0, relativeY * 100.0); + } else { + final converted = newPos * devicePixelRatio; + keyPair.touchPosition = converted; + } + setState(() {}); + }, + color: Colors.red, ); - } else { - offset = Offset( - keyPair.touchPosition.dx / devicePixelRatio, - keyPair.touchPosition.dy / devicePixelRatio, - ); - } + }), - //print('Drawing at offset $offset for keypair with position ${keyPair.touchPosition}'); + Positioned.fill(child: Testbed()), - return _buildDraggableArea( - enableTouch: true, - position: offset, - keyPair: keyPair, - onPositionChanged: (newPos) { - if (_imageRect != null) { - // convert to percentage - final relativeX = ((newPos.dx - _imageRect!.left) / _imageRect!.width).clamp(0.0, 1.0); - final relativeY = ((newPos.dy - _imageRect!.top) / _imageRect!.height).clamp(0.0, 1.0); - keyPair.touchPosition = Offset(relativeX * 100.0, relativeY * 100.0); - } else { - final converted = newPos * devicePixelRatio; - keyPair.touchPosition = converted; - } - setState(() {}); - }, - color: Colors.red, - ); - }), - - Positioned.fill(child: Testbed()), - - Positioned( - top: 40, - right: 20, - child: Row( - spacing: 8, - children: [ - ElevatedButton.icon(onPressed: _saveAndClose, icon: const Icon(Icons.save), label: const Text("Save")), - PopupMenuButton( - itemBuilder: - (c) => [ - PopupMenuItem( - child: Text('Reset'), - onTap: () { - actionHandler.supportedApp?.keymap.reset(); - setState(() {}); - }, - ), - ], - icon: Icon(Icons.more_vert), - ), - if (kDebugMode) MenuButton(), - ], + Positioned( + top: 40, + right: 20, + child: Row( + spacing: 8, + children: [ + ElevatedButton.icon( + onPressed: _saveAndClose, + icon: const Icon(Icons.save), + label: const Text("Save"), + ), + PopupMenuButton( + itemBuilder: + (c) => [ + PopupMenuItem( + child: Text('Reset'), + onTap: () { + actionHandler.supportedApp?.keymap.reset(); + setState(() {}); + }, + ), + ], + icon: Icon(Icons.more_vert), + ), + if (kDebugMode) MenuButton(), + ], + ), ), - ), - ], + ], + ), ), ); } diff --git a/lib/utils/actions/remote.dart b/lib/utils/actions/remote.dart index 60166b9..ec76c2f 100644 --- a/lib/utils/actions/remote.dart +++ b/lib/utils/actions/remote.dart @@ -21,7 +21,7 @@ class RemoteActions extends BaseActions { return 'Keymap entry not found for action: ${action.toString().splitByUpperCase()}'; } - if (!(actionHandler as RemoteActions).isConnected && !screenshotMode) { + if (!(actionHandler as RemoteActions).isConnected) { return 'Not connected to a device'; } diff --git a/pubspec.yaml b/pubspec.yaml index e9bf353..cc5e6ae 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: swift_control description: "SwiftControl - Control your virtual riding" publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 3.0.1+17 +version: 3.0.1+18 environment: sdk: ^3.7.0