Author SHA1 Message Date
hermes-agent 6e340b6d6a Read app version from built .ipa instead of hardcoding
Build VoidLink IPA / Build .ipa and publish source (push) Successful in 4m3s
The built app reports CFBundleShortVersionString 0.0.1 (MARKETING_VERSION),
but metadata hardcoded 1.0, so SideStore rejected the version mismatch.
Now read the version from the built app's Info.plist so it always matches.
2026-08-28 22:58:03 +00:00
hermes-agent ec12492ea7 Build Release config so .ipa bundle id matches source metadata
Build VoidLink IPA / Build .ipa and publish source (push) Successful in 4m1s
Debug config produces com.voidlink.tf.debug10.iOS, which mismatches the
com.voidlink.tf.iOS declared in VoidLink.json, so SideStore rejects the
install. Release produces the matching com.voidlink.tf.iOS.
2026-08-28 17:09:19 +00:00
hermes-agent 13ccbb3866 Add auto-deploy CI: build unsigned .ipa and publish to SideStore source
Build VoidLink IPA / Build .ipa and publish source (push) Successful in 2m58s
Builds VoidLink via xcodebuild (no signing) on the macOS runner, packages
the .app into an .ipa, and uploads .ipa + metadata + icon to perlis so it
can be installed from the SideStore source at apps.bueso.eu.
2026-08-28 11:06:46 +00:00
True Zhuanjia 20d6e221a4 minor fixes 2026-08-26 00:25:56 +08:00
True Zhuanjia 2bfddf255a widget interaction fixes 2026-08-25 23:20:24 +08:00
True Zhuanjia 0e8903d233 add tip for saving changes before exiting widget layout editor 2026-08-25 18:20:48 +08:00
True Zhuanjia 78f385d732 settingsVC ui fix 2026-08-25 16:10:19 +08:00
True Zhuanjia bd306ba07f gameSirG8MFi rumbler ready 2026-08-25 00:30:31 +08:00
TrueZhuanJia ececc03d46 Merge pull request #214 from chrisnch
Priority: rumble support
2026-08-24 22:25:06 +08:00
chrisnch 6202a53153 Fix GameSir rumble session teardown threading 2026-08-22 08:13:56 +01:00
chrisnch 84b6b22f75 Add native rumble support for GameSir G8+ MFi 2026-08-22 07:33:00 +01:00
True Zhuanjia f8388658d8 controller navigation fixes 2026-08-22 01:08:44 +08:00
True Zhuanjia 3c36964b5c controller Navigation improvements 2026-08-21 22:08:41 +08:00
True Zhuanjia 089515443d dualSense emulation beta ready (works with Sunshine-Foundation) 2026-08-21 18:01:39 +08:00
True Zhuanjia e63e5f2120 nest qiin2333's haptic convertor 2026-08-18 15:52:31 +08:00
True Zhuanjia 5a5238afee dualSense haptic beta 2026-08-17 22:29:21 +08:00
True Zhuanjia e00e2f8000 Merge branch 'Integration' into ds5 2026-08-17 20:42:03 +08:00
True Zhuanjia 8795a9f173 onscreen widget fix
motionHandler fix
2026-08-17 20:41:02 +08:00
True Zhuanjia 5bd938abc5 Merge branch 'Integration' into ds5
emulated gyro fix
2026-08-17 17:43:23 +08:00
True Zhuanjia 514dbb98e7 fixed: emulated gyro data not enabled 2026-08-17 17:13:27 +08:00
True Zhuanjia 106b1548d4 update audio-haptic lib 2026-08-17 15:30:08 +08:00
True Zhuanjia cacf2b1951 EQ tmp fixes 2026-08-17 04:39:45 +08:00
True Zhuanjia f1a4a519c3 adaptive trigger fixes 2026-08-16 19:28:39 +08:00
True Zhuanjia 4f0be34947 dualSense haptic rumbling by qin2333's PCM convertor 2026-08-16 18:03:50 +08:00
True Zhuanjia 9da1121cdb dual sense adaptive trigger implementation working with Sunshine-Foundation 2026-08-16 15:29:49 +08:00
True Zhuanjia 0e92af13eb update c-module 2026-08-16 13:21:00 +08:00
True Zhuanjia cdd613cc63 fix build error for simulator 2026-08-13 08:33:55 +08:00
True Zhuanjia 58535c9997 minor ui fix 2026-08-12 19:41:07 +08:00
True Zhuanjia 48647d4d89 - update tip strings 2026-08-12 18:28:04 +08:00
True Zhuanjia 16c54ac966 ui & tip string fixes 2026-08-11 23:48:03 +08:00
True Zhuanjia 4214afd2b7 statsOverlay for frame interpolation 2026-08-11 13:32:24 +08:00
True Zhuanjia d28aa43319 resolution persistence fix 2026-08-10 15:08:40 +08:00
True Zhuanjia 4983aecb4c ui fixes 2026-08-10 02:59:11 +08:00
True Zhuanjia 5ba566ceee menu ui fixes 2026-08-09 21:42:19 +08:00
True Zhuanjia b0fd0222df Merge commit '6879bf63791c3422fd88095237bee508524b6959' into frameInterpolation
# Conflicts:
#	VoidLink/Localization/Localizable.xcstrings
#	VoidLink/Stream/VideoDecoderRenderer.m
2026-08-09 20:54:04 +08:00
True Zhuanjia 7b7bb3837c frameInterpolation beta ready 2026-08-09 20:34:39 +08:00
AcakiandClaude Fable 5 6879bf6379 Allow switching rendering backend without restarting the app
The renderer setting previously forced an app quit (exit(0)) because
switching backends between sessions crashed or leaked. Fix the
underlying lifecycle bugs and remove the restart requirement:

- FrameQueue: really release frames in clear(), clear on session
  start/stop, and guard start/stop with an owner so a stale session's
  async cleanup can't pause a new session's queue
- MetalView: weak delegate (breaks retain cycle leaking the whole Metal
  stack), never start two render threads, bound the shutdown wait to
  avoid deadlocking against the render thread's dispatch_sync to main
- MetalViewController: explicit idempotent shutdown called from stream
  teardown (viewDidDisappear isn't guaranteed); snapshot the renderer
  in render-thread callbacks to avoid use-after-free during shutdown
- MetalVideoRenderer: make the shared colorspace name thread-safe and
  ARC-managed instead of a CFStringRef over-released in dealloc; guard
  NULL format description extensions
- Connection: drop the static renderer reference in DrCleanup
- VideoDecoderRenderer: cache the application background state via
  notifications instead of calling UIKit from the decode queue
  (Main Thread Checker violation)
- Frame: don't CFRetain(NULL) when a format desc has no extensions
- StreamFrameViewController: fix _streamView cleanup ordering
- Settings: apply the backend change in place (with the Metal caveats
  alert) and update localized tips; takes effect on next stream

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 17:14:01 +08:00
True Zhuanjia fc2b1689d0 frame interpolation Demo1 2026-08-08 03:45:50 +08:00
True Zhuanjia 9c9ef4a13f make pixelBuffer available to metalRenderer 2026-08-07 16:39:46 +08:00
True Zhuanjia a92010f5b1 in-flight bitrate request fix 2026-08-07 00:26:56 +08:00
True Zhuanjia 813005694a frame interpolation preparations 2026-08-06 19:57:24 +08:00
True Zhuanjia 11dd542cb3 controller navigation fixes 2026-08-04 15:14:21 +08:00
True Zhuanjia 949a0e0bfb revert hud visual feedback demo 2026-08-04 15:14:08 +08:00
True Zhuanjia ed8a246bad hud visual feedback demo 2026-08-04 09:30:39 +08:00
True Zhuanjia cfb8946ac3 widget label fix 2026-08-03 14:20:12 +08:00
True Zhuanjia 660a015f8b minor tweak and fixes 2026-08-03 11:10:30 +08:00
True Zhuanjia 54ac7f6522 minor fixes for controller navigation 2026-08-02 17:54:28 +08:00
True Zhuanjia a01af9ee21 refresh partial settings update flag 2026-08-02 00:23:51 +08:00
True Zhuanjia 71b7634069 refresh profileSelector tip update flag 2026-08-02 00:00:58 +08:00
True Zhuanjia 32718071e0 set widget profile update flag 2026-08-01 22:51:46 +08:00
True Zhuanjia 30d602af29 update ui string 2026-08-01 22:36:17 +08:00
True Zhuanjia f0043a763b new Tips for users 2026-08-01 21:09:02 +08:00
True Zhuanjia 516648cf34 on screen widget sublabel 2026-08-01 16:04:30 +08:00
True Zhuanjia 5bd9c6bd98 generic improvements & fixes 2026-07-31 22:13:13 +08:00
True Zhuanjia 3d82009d4a minor change 2026-07-31 00:05:40 +08:00
True Zhuanjia 51c13faa0c pencil tip offset 2026-07-30 23:22:50 +08:00
True Zhuanjia e0aff4b532 - genshin touch control enhancement preview for settings menu
- controller navigation & first connection tips
- enhancedTouchPreview bug fix
2026-07-30 14:14:33 +08:00
True Zhuanjia b0c16922a2 controller navigation improvements and fixes 2026-07-30 01:05:51 +08:00
True Zhuanjia ef5d839f5e refactor profileSelector view 2026-07-29 18:01:33 +08:00
True Zhuanjia 4a3ca7774e refactor toolboxView 2026-07-28 17:56:12 +08:00
True Zhuanjia 8f93cce257 change default settings for controller mouse mode 2026-07-27 21:42:29 +08:00
True Zhuanjia 9a634da3ed - feat: global controller navigation
- shorten initial host detection
2026-07-26 16:07:03 +08:00
True Zhuanjia 6460036661 update aboutView 2026-07-07 16:35:58 +08:00
True Zhuanjia c968a1df2a - controller navigation preparations
- disable tilt button for pencil
- widget picker UI improvement & fixes
- allow triggers to be controller mouse buttons
2026-07-07 16:13:46 +08:00
True Zhuanjia 54c106e01e radial menu demo 2026-07-01 15:20:57 +08:00
True Zhuanjia 41a5855211 minor fixes 2026-06-23 12:19:14 +08:00
True Zhuanjia 2652c2a99b set default dark theme for iOS 13 2026-06-23 11:00:09 +08:00
True Zhuanjia e987965765 tweak widget templates 2026-06-23 00:48:36 +08:00
True Zhuanjia a7d7a7975f fixed pencil touch for absoluteTouch 2026-06-22 22:12:15 +08:00
True Zhuanjia c79929f37e iap minor fix 2026-06-22 17:34:56 +08:00
True Zhuanjia d286c8b073 update widget templates 2026-06-22 14:32:37 +08:00
True Zhuanjia 2cef75fb32 localization fixes 2026-06-22 12:35:07 +08:00
True Zhuanjia ca17c1cc66 - handle iap purchase on app launch
- localization fix
- iap workflow fixes
2026-06-22 12:30:17 +08:00
True Zhuanjia 98e8305c94 update ui string 2026-06-22 01:04:05 +08:00
True Zhuanjia 45ccfa0c7f Merge branch 'IAPImrovements' into Integration
# Conflicts:
#	VoidLink/Localization/Localizable.xcstrings
xiap button for aboutView
2026-06-22 00:57:49 +08:00
True Zhuanjia 7d7b909c66 iap button for aboutView 2026-06-22 00:56:28 +08:00
True Zhuanjia d6c1059e99 improved iap workflow 2026-06-22 00:53:01 +08:00
True Zhuanjia 9c43e39ffb UI bug fix for game profile selector 2026-06-21 15:49:51 +08:00
True Zhuanjia adb5091dc2 improved motion control 2026-06-21 15:29:40 +08:00
True Zhuanjia 7d3c2d93ac add support for purchase intents 2026-06-20 22:56:04 +08:00
True Zhuanjia f4b161ed06 Set correct default mouse cursor mode 2026-06-20 16:03:14 +08:00
True Zhuanjia d57334265b improved motion control 2026-06-19 16:32:34 +08:00
True Zhuanjia f7efa48ae8 fixed: folder expansion compatibility issue on low versions of iOS 2026-06-17 00:58:21 +08:00
True Zhuanjia deb335efc3 auto restore for docked folder when app becomes active 2026-06-14 18:33:17 +08:00
True Zhuanjia f039f5af29 update aboutView 2026-06-14 14:17:07 +08:00
True Zhuanjia 286b5e94e6 fixed: undoRelocation not working for widget just collected by folder button 2026-06-14 13:00:29 +08:00
True Zhuanjia 1a719e11e6 - tweak widget templates
- minor improvements & fixes
2026-06-08 01:16:00 +08:00
True Zhuanjia becfc596c1 minor improvement & fixes 2026-06-07 13:24:09 +08:00
True Zhuanjia 1b76d0e0a6 - update tip message strings
- folder widget collection fixes
2026-06-06 22:09:48 +08:00
True Zhuanjia 53209ce89e - update default widget template with profile name localized
- improve widget import
2026-06-06 20:07:01 +08:00
True Zhuanjia 384724ff27 refactor oscprofilemanager to swift 2026-06-05 21:48:57 +08:00
True Zhuanjia e2eccd7968 - tweak widget templates
- multiple on-screen widget tweaks and improvements
2026-06-05 16:58:51 +08:00
True Zhuanjia f762ecc3b2 detailed definitions for connection termination errors 2026-06-05 16:53:40 +08:00
True Zhuanjia fa0b864933 - layout tool improvement: handles subfolder visibility in edit tool
- tweak iPad widget template
2026-05-31 14:00:44 +08:00
True Zhuanjia e0f3921a75 - template file update
- minor fixes
2026-05-31 02:11:30 +08:00
True Zhuanjia f698ad2fb6 default template update flag 2026-05-30 21:36:14 +08:00
True Zhuanjia a0179cc6cc new widget template demo 2026-05-30 19:28:37 +08:00
True Zhuanjia 511e7c0e4b folder widget & autodock improvement and fixes 2026-05-30 17:42:15 +08:00
True Zhuanjia e734973be1 widget layout tool fixes 2026-05-30 11:05:30 +08:00
True Zhuanjia 9979408b1a folder behavior minor fixes 2026-05-28 21:04:43 +08:00
True Zhuanjia a5388bb60d revert default mouse cursor mode to "captured" 2026-05-28 16:44:51 +08:00
True Zhuanjia 4c32ec59d6 tweak button sliding 2026-05-28 16:33:50 +08:00
True Zhuanjia af1f0e75c7 more preparations for new widget template 2026-05-28 14:21:02 +08:00
True Zhuanjia 3aa7a90a2f string localization for new default widget template 2026-05-28 11:59:40 +08:00
True Zhuanjia ec956218fd add entries to shortcut lib 2026-05-27 17:46:12 +08:00
True Zhuanjia 02fcdcecea shortcut library for widget picker 2026-05-27 13:01:12 +08:00
True Zhuanjia 3575e84165 tweak functional tab of widget picker view 2026-05-27 10:59:11 +08:00
True Zhuanjia 90116e7ff6 folder behavior improvement 2026-05-27 10:17:39 +08:00
True Zhuanjia ffe48a5382 deprecate NativeTouchOnly mode 2026-05-26 16:17:52 +08:00
True Zhuanjia c4ddc0c4fc minor fixes for absolute touch mode 2026-05-26 15:05:10 +08:00
True Zhuanjia 23299ad73d folder widget fixes 2026-05-26 13:20:30 +08:00
True Zhuanjia accd07f808 minor fix for pencil handler 2026-05-25 12:13:22 +08:00
True Zhuanjia ed39f85508 - c-module update
- minor fixes
2026-05-22 02:35:34 +08:00
True Zhuanjia e8349b2a6f update ui string 2026-05-21 20:37:17 +08:00
True Zhuanjia c36051fffc improve widget alignment 2026-05-21 20:31:54 +08:00
True Zhuanjia 5f4fe6a040 widget layering fixes 2026-05-21 19:44:28 +08:00
True Zhuanjia 843570dbca feat: on-screen button auto tap repeats 2026-05-21 17:54:29 +08:00
True Zhuanjia bb44b2067e 4*16 widget pool grid for button macro 2026-05-21 16:05:57 +08:00
True Zhuanjia 2c2088e4b1 - button widget skill combo enhanced to macro
- widget picker view UI fixes
2026-05-21 14:18:50 +08:00
True Zhuanjia a7088c70e5 app launch background color 2026-05-20 10:13:36 +08:00
True Zhuanjia 091fd828ab widget layout tool layering fix 2026-05-20 02:14:43 +08:00
True Zhuanjia d168ffbd5a minor fix 2026-05-20 01:15:05 +08:00
True Zhuanjia 0af4c70e16 - walk-sprint sign for direction pad
- session launch UI & animation fixes
2026-05-20 00:59:34 +08:00
True Zhuanjia e402c17fa6 adopted qiin2333's network related common-c commits 2026-05-19 15:39:23 +08:00
True Zhuanjia ab9b2509ff allow reversed input for motion control channels 2026-05-19 13:35:12 +08:00
True Zhuanjia 535d6009ad controller report issue fixes 2026-05-19 11:23:30 +08:00
True Zhuanjia 09981df37e - mic audio encryption for sunshine-foundation
- merge c-module updates from upstream
2026-05-18 22:12:26 +08:00
True Zhuanjia f7eae4cee9 controller report issue 2026-05-18 11:19:10 +08:00
True Zhuanjia ab14e18b4a folder expansion fix 2026-05-17 21:40:24 +08:00
True Zhuanjia 1630f7987a home indicator interaction fix 2026-05-16 22:45:45 +08:00
True Zhuanjia 5a9bcab368 Merge branch 'fix/resolution_change_stretch' into Integration 2026-05-14 14:58:08 +08:00
True Zhuanjia f7b118b671 tip string fixes 2026-05-14 00:24:48 +08:00
True Zhuanjia b49f4a6891 update default widget profiles 2026-05-13 22:52:11 +08:00
True Zhuanjia 3fd0c1b397 update ui string 2026-05-13 20:41:43 +08:00
True Zhuanjia 9282c3f404 touch mode change tip 2026-05-13 20:16:00 +08:00
True Zhuanjia ef3fca754f minor fixes 2026-05-13 17:25:35 +08:00
True Zhuanjia c08bb232cd layout tool batch edit improvement and tips 2026-05-13 17:04:19 +08:00
True Zhuanjia 2d1a1573e1 folder alpha tweak bug fixes 2026-05-13 16:23:43 +08:00
True Zhuanjia 92b65b4a17 update ui tip string 2026-05-13 15:56:35 +08:00
True Zhuanjia f72e0f6292 - valid & invalid button mode tips
- widget panel tips
2026-05-13 15:45:25 +08:00
True Zhuanjia 57a8c22a3f minor improvements 2026-05-12 22:09:03 +08:00
True Zhuanjia 4195f97d56 widget layout tool minor improvements 2026-05-12 20:46:28 +08:00
True Zhuanjia c734030e87 widget resize improvement 2026-05-12 19:09:44 +08:00
True Zhuanjia 96d997d41e button widget batch resize 2026-05-12 17:09:14 +08:00
True Zhuanjia 023cc0b583 highlight touchpad widget border during resizing 2026-05-12 16:20:53 +08:00
True Zhuanjia 57dac128e2 globe as escape switch 2026-05-12 11:56:15 +08:00
True Zhuanjia 77e0048c57 more tips for on screen widgets 2026-05-11 22:57:47 +08:00
True Zhuanjia fe6954cb9c tips for first keyboard or mouse connection on iOS 2026-05-11 16:32:17 +08:00
True Zhuanjia 237f03a501 streamview bug fixes 2026-05-11 12:43:45 +08:00
True Zhuanjia 94f61ebf0c shortcut button triggering fix 2026-05-09 19:42:25 +08:00
True Zhuanjia c3a0004c68 add lsvpad 2026-05-08 21:49:09 +08:00
True Zhuanjia d7aa5cc9fc improved displacement based stick pads 2026-05-08 21:05:03 +08:00
True Zhuanjia 26c1d068a8 inertial scrolling for displacement based stickpads 2026-05-05 23:54:54 +08:00
True Zhuanjia 35ceebfe0d - update default widget template
- change default button action for mousepad
- minor fixes
2026-05-01 00:21:55 +08:00
True Zhuanjia 5daf158908 update widget localization strings 2026-04-30 01:24:15 +08:00
True Zhuanjia 8f12a4f598 minor tweak 2026-04-29 22:17:44 +08:00
True Zhuanjia 233a23c6c8 refactor touchMode to game profile 2026-04-29 22:01:18 +08:00
True Zhuanjia cf48d4d7c3 new widget, magnifier 2026-04-29 18:43:06 +08:00
True Zhuanjia 5bf8eeb873 widget picker view ui string update 2026-04-29 10:48:11 +08:00
True Zhuanjia 8f5d58a25c minor fix 2026-04-29 00:07:44 +08:00
True Zhuanjia 363b91b244 folder import fixes 2026-04-28 23:52:14 +08:00
True Zhuanjia efc34ae1fc mac desktop experience fixes & tips 2026-04-28 17:09:33 +08:00
True Zhuanjia 9e858520c8 tweak touchHandler 2026-04-28 15:04:54 +08:00
True Zhuanjia 20aba200b3 new touchpad widget: absolute mousepad 2026-04-28 14:25:25 +08:00
True Zhuanjia cb235d9255 tweak inputAccessoryBar 2026-04-28 10:21:06 +08:00
True Zhuanjia b0224d502a button-combo available for folder 2026-04-27 23:15:08 +08:00
True Zhuanjia ce67aefc3b motion control for external gamepad 2026-04-27 16:23:45 +08:00
True Zhuanjia e8d864ff66 soft keyboard fixes 2026-04-26 21:40:24 +08:00
True Zhuanjia 9e2ef6cc0d audio resume fix 2026-04-26 19:38:34 +08:00
True Zhuanjia 9d2df484d1 make inputAccessoryBar delete button easier to tap 2026-04-26 17:36:35 +08:00
True Zhuanjia d28573dcd4 widgetPickerView logic fix 2026-04-26 17:20:22 +08:00
True Zhuanjia c3cb31d443 widgetPicker view compatibility fixes for low iOS versions 2026-04-26 16:42:15 +08:00
True Zhuanjia b258c58d59 soft keyboard activation fix 2026-04-24 23:37:34 +08:00
True Zhuanjia d8e83d1704 Merge branch 'Integration' into inputAccessoryBar
# Conflicts:
#	VoidLink/Utility/GenericUtils.swift
2026-04-24 23:27:21 +08:00
True Zhuanjia e78a351c28 inputAccessoryBar for soft keyboard 2026-04-24 22:59:37 +08:00
True Zhuanjia 5ee443337c autoDock and gesture interaction fixes 2026-04-23 17:09:53 +08:00
True Zhuanjia 4dc96ba189 widget alpha tweak fix 2026-04-23 15:14:33 +08:00
True Zhuanjia ecacb1afa7 lower iOS compatibility fixes 2026-04-22 23:53:19 +08:00
True Zhuanjia 7e21292a62 game profile system & selector 2026-04-22 12:46:51 +08:00
True Zhuanjia 9104a7b7b9 GUI command picker for toolbox menu 2026-04-16 17:31:49 +08:00
True Zhuanjia ecc7a79a8c layout profile table improvements 2026-04-16 16:03:52 +08:00
True Zhuanjia a1a70fcca2 - tool box menu improvements
- autoDock improvements
- live gamepad overlay close button
2026-04-16 14:31:21 +08:00
True Zhuanjia d769c98e22 layout tool ui fixes 2026-04-15 14:15:39 +08:00
True Zhuanjia ce59a9eb05 auto dock fixes 2026-04-14 17:16:11 +08:00
True Zhuanjia b2984e0af5 Stop tracking .DS_Store files 2026-04-14 15:56:52 +08:00
True Zhuanjia bed2232aff widget template update 2026-04-14 15:46:31 +08:00
True Zhuanjia e87f0f1aa0 system audio playback fix 2026-04-14 14:46:18 +08:00
True Zhuanjia 9807a77d9b gamepad overlay crash fix 2026-04-14 11:08:59 +08:00
True Zhuanjia 3e1423ff46 multi gamepad input fixes 2026-04-14 10:45:50 +08:00
True Zhuanjia a4f805db26 audio playback fixes 2026-04-14 09:48:11 +08:00
True Zhuanjia 0195aeb765 Revert "audio playback improvement"
This reverts commit 46834a8a8b.
2026-04-14 08:43:13 +08:00
True Zhuanjia fcfe83572f autoDock fixes 2026-04-13 18:16:37 +08:00
True Zhuanjia d61942caec TF widget template fix 2026-04-13 12:51:46 +08:00
True Zhuanjia 515360166c ui string fixes 2026-04-13 12:44:22 +08:00
True Zhuanjia 5c5507b02f update ui string 2026-04-13 11:05:35 +08:00
True Zhuanjia 7e0d2af5b7 - update widget templates
- minor fixes
2026-04-13 09:44:55 +08:00
True Zhuanjia 780a0e7ab8 - update widget tempates
- minor fixes
2026-04-13 01:58:57 +08:00
True Zhuanjia 9359333b15 widget batch edit fix 2026-04-12 22:40:21 +08:00
True Zhuanjia 819aa436e8 iOS 12 app crash fixes 2026-04-12 22:03:47 +08:00
True Zhuanjia 5beee0ffe4 update UI string 2026-04-12 20:48:03 +08:00
True Zhuanjia 30aa9a6b8d - update UI string
- minor improvements
2026-04-12 20:03:06 +08:00
True Zhuanjia 7e088acd49 dpad improvements 2026-04-12 19:23:47 +08:00
True Zhuanjia f702f726b7 - functional button fixes
- tweak auto dock animation
- input fixes
2026-04-11 14:34:54 +08:00
True Zhuanjia 46834a8a8b audio playback improvement 2026-04-10 23:35:21 +08:00
True Zhuanjia ca69fd62d9 persist pickerView tab selection 2026-04-10 23:25:46 +08:00
True Zhuanjia 8fd43d47dd more functional buttons 2026-04-10 21:34:14 +08:00
True Zhuanjia 732ec9a9cd widget picker fix 2026-04-10 19:19:22 +08:00
True Zhuanjia a0d1982a14 autoDock & toolbox gesture fixes 2026-04-10 17:05:04 +08:00
True Zhuanjia bb63183a51 update copyrights 2026-04-10 10:13:19 +08:00
True Zhuanjia 707beddcae refactor target and scheme names 2026-04-09 16:06:15 +08:00
True Zhuanjia c4c896e43e live gamepad overlay tip fix 2026-04-09 11:48:35 +08:00
True Zhuanjia e2acf5ac4b disable toolbox menu gesture 2026-04-08 20:03:27 +08:00
True Zhuanjia fcecae3a87 - update default Templates
- make RsWheel available
2026-04-08 11:56:05 +08:00
True Zhuanjia 55a2bb7576 disable soft keyboard tap gesture by default 2026-04-08 02:38:10 +08:00
True Zhuanjia 6b085982f1 cancel tap gesture for widget layout tool 2026-04-08 02:25:57 +08:00
True Zhuanjia 94f1c625be update default widget templates 2026-04-08 02:15:07 +08:00
True Zhuanjia b0842ea4dc autoDock fixes 2026-04-08 00:21:55 +08:00
True Zhuanjia 49431b5eba - widget template editing code
- widgetPickerView fixes
2026-04-08 00:07:13 +08:00
True Zhuanjia 68ee5d879b minor changes 2026-04-07 21:43:14 +08:00
True Zhuanjia 984a1acd48 turn on live feedback overlay on controller arrival 2026-04-07 19:49:07 +08:00
True Zhuanjia 00b06b7b70 - update ui string
- improve autodock
2026-04-07 15:09:30 +08:00
True Zhuanjia 84adf3ac95 command picker for drawing toolkit 2026-04-07 14:35:06 +08:00
True Zhuanjia 9cd72271ca minor fixes 2026-04-06 23:58:49 +08:00
True Zhuanjia e71ce58d9b enable on-screen widget by default 2026-04-06 23:22:07 +08:00
True Zhuanjia eb03b055ea folder button auto dock 2026-04-06 23:07:43 +08:00
True Zhuanjia 5af39e8fb6 gamepad live feedback overlay 2026-04-06 01:19:44 +08:00
Acaki b12cb96416 fix: video streatched when changing resolution in stream 2026-04-04 17:02:52 +08:00
True Zhuanjia 7f4108c4e3 minor fix 2026-04-04 03:25:35 +08:00
True Zhuanjia be48a48704 bulk edit stacks fix 2026-04-04 02:53:06 +08:00
True Zhuanjia 88bc5c7551 minor fixes 2026-04-04 01:19:18 +08:00
True Zhuanjia 7cd857a38b highlight color fixes 2026-04-04 00:43:38 +08:00
True Zhuanjia 60e92c458e - widget bulk edit, clear, import
- refactor coding
2026-04-03 22:14:05 +08:00
True Zhuanjia 605f276a71 widget picker bug fixes 2026-04-02 10:33:28 +08:00
True Zhuanjia 210ca4190a refactor coding 2026-04-02 10:23:16 +08:00
True Zhuanjia bd2d9c05bc gui widgetPicker 2026-04-02 06:51:33 +08:00
True Zhuanjia ae526ecddf fixed: soft keyboard brought up by iOS26 system-level action 2026-03-23 12:22:56 +08:00
True Zhuanjia 37d7949304 widget profile template update 2026-03-22 15:29:22 +08:00
True Zhuanjia 6092839004 folder button bulk move fix 2026-03-22 15:02:53 +08:00
True Zhuanjia 31dc13b87d minor change 2026-03-22 14:10:42 +08:00
True Zhuanjia dbe91f5b07 widget layout tool improvements 2026-03-22 14:01:09 +08:00
True Zhuanjia d092b93a29 - allow wider size range for on-screen widgets
- minor improvements
2026-03-22 11:21:51 +08:00
True Zhuanjia eff35450f6 feat: folder button bulk move 2026-03-22 00:32:48 +08:00
True Zhuanjia 6204feefc0 feat: on-screen button "DisableTouch" 2026-03-21 22:06:40 +08:00
True Zhuanjia 924374bccd Merge branch 'keyboardHeight' into Integration 2026-03-21 19:45:35 +08:00
True Zhuanjia eac8d15240 feat: designate soft keyboard height manually 2026-03-21 19:45:21 +08:00
True Zhuanjia a55d840bce minor tweak for pencilHandler 2026-03-21 17:06:45 +08:00
True Zhuanjia 22aa0a5249 tweak curve view UI 2026-03-21 13:27:45 +08:00
True Zhuanjia c3358efa9d undo last move button for pressure curve tool 2026-03-21 13:13:39 +08:00
True Zhuanjia e59f3ba1ab stroke start equalization and filtering 2026-03-21 12:43:16 +08:00
True Zhuanjia 42f29074fa block keyboard action initiated by physical gamepad for iOS 26 2026-03-16 16:25:46 +08:00
True Zhuanjia 6299401286 enable liquid glass 2026-03-15 11:44:16 +08:00
True Zhuanjia deba65857d Merge branch 'iOS26UI' into Integration 2026-03-15 11:43:36 +08:00
True Zhuanjia 691aa9c899 liquid glass for soft keyboard toolbar 2026-03-15 11:43:20 +08:00
True Zhuanjia bd9dccd834 Merge branch 'iOS26UI' into Integration 2026-03-14 14:15:28 +08:00
True Zhuanjia 0dd419a5a3 tweak toolbox view 2026-03-14 14:15:16 +08:00
True Zhuanjia 2410222c19 Merge branch 'iOS26UI' into Integration 2026-03-14 11:50:48 +08:00
True Zhuanjia 1269c46f56 UI improvements: widget profile selector table 2026-03-14 11:50:07 +08:00
True Zhuanjia 2c787a8d77 Merge branch 'iOS26UI' into Integration 2026-03-12 19:46:26 +08:00
True Zhuanjia 92fca9bca1 try to fix app crash caused by host discovery 2026-03-12 19:44:13 +08:00
True Zhuanjia f8badd51c5 deal with osw profile selector table navigation bar 2026-03-12 18:10:39 +08:00
True Zhuanjia ed8bf5af9e deal with separator color 2026-03-12 16:45:50 +08:00
True Zhuanjia b2521de84e refactor themeManager 2026-03-12 16:15:52 +08:00
True Zhuanjia 4dd1aad123 Merge branch 'iOS26UI' into Integration 2026-03-12 10:19:47 +08:00
True Zhuanjia 165b5e6638 tweak settings menu navigation bar height 2026-03-12 10:19:08 +08:00
True Zhuanjia d8ac06ddca dummy commit 2026-03-12 09:56:39 +08:00
True Zhuanjia e742466409 app view and app view navigation bar 2026-03-12 09:54:11 +08:00
True Zhuanjia 201bf5b447 liquid glass: settings menu navigation bar 2026-03-11 22:26:50 +08:00
True Zhuanjia 7bed913a1e Merge branch 'Integration' into iOS26UI 2026-03-11 15:36:44 +08:00
True Zhuanjia e067fd8fa2 tweak menu section header 2026-03-09 12:09:06 +08:00
True Zhuanjia ba7d5feb81 enable liquid glass 2026-03-07 10:12:14 +08:00
True Zhuanjia 4a9e7ad2a1 iOS26 UI adaption: host view navigation bar 2026-03-07 10:08:17 +08:00
True Zhuanjia 4b56a44a47 UI improvements 2026-03-06 17:29:38 +08:00
True Zhuanjia bc98b43de2 secure folder unfolding fix at the end of animation 2026-03-05 19:39:28 +08:00
True Zhuanjia 16b3df5279 fixed: unexpected triggering of buttons collected by folder 2026-03-05 15:11:15 +08:00
True Zhuanjia d202f2f6e3 - remove auto codec option
- set default codec to HEVC
2026-03-05 11:10:46 +08:00
True Zhuanjia 142474eac8 - fixed: widget layout broken by windowing, entering background, orientationChange+PiP 2026-03-04 22:58:20 +08:00
True Zhuanjia acc89dba39 enable track point for onscreen widget edit mode 2026-03-04 13:06:22 +08:00
True Zhuanjia 8cd366d2cd update UI string 2026-02-28 18:44:24 +08:00
True Zhuanjia 2d67c1b22b update UI string 2026-02-28 12:30:32 +08:00
True Zhuanjia 77b0ef55c9 UI string update 2026-02-28 10:44:49 +08:00
True Zhuanjia d76f404a4c UI tip update 2026-02-27 00:48:46 +08:00
True Zhuanjia d0b45a019f minor fixes 2026-02-27 00:08:28 +08:00
True Zhuanjia 0728e999eb - add necessary UI tips
- restore "view all apps" for host card
- minor fixes
2026-02-26 22:32:37 +08:00
True Zhuanjia f325e9a206 default setting update 2026-02-26 18:39:16 +08:00
True Zhuanjia 49b03e65b2 feat: hover mode selector 2026-02-26 17:18:21 +08:00
True Zhuanjia 2bc18e2f02 add disable hovering switch 2026-02-26 00:29:57 +08:00
True Zhuanjia dadb292676 handle hovering automatically 2026-02-25 22:06:09 +08:00
True Zhuanjia 2c9902ce77 feat: auto hover 2026-02-25 00:38:24 +08:00
True Zhuanjia 6d36219e88 - fixed: unable to setup updated pressure curve mapping after switching widget profile during streaming 2026-02-24 19:47:20 +08:00
True Zhuanjia 470862dbe0 - fixed: unable to setup updated pressure curve mapping during streaming
- flexible pressure curve start point
- larger pressure curve view
2026-02-24 16:49:31 +08:00
True Zhuanjia f6017824ee default settings update 2026-02-24 00:32:35 +08:00
True Zhuanjia adeb77cf65 fixed cursor flickering on host 2026-02-23 22:57:46 +08:00
True Zhuanjia 9bc080815a update copyright 2026-02-23 20:48:25 +08:00
True Zhuanjia d03eebb5ba default settings update 2026-02-23 20:24:17 +08:00
True Zhuanjia ca453d8059 - tweak setting stack sequence
- auto set fullColorRangeSwitch
2026-02-23 19:37:17 +08:00
True Zhuanjia cb8126d201 UI fixes 2026-02-19 14:51:10 +08:00
True Zhuanjia 1626f30bb6 lowest latency option of queueSize slider 2026-02-19 13:23:30 +08:00
True Zhuanjia 47b5ec2678 default setting update flag 2026-02-19 01:39:18 +08:00
True Zhuanjia ccd032834d - restore asyncDequeue switch
- tweak frame buffering
2026-02-19 01:31:55 +08:00
True Zhuanjia c7ffd51cec pencil pro widget template import changes 2026-02-18 13:01:08 +08:00
True Zhuanjia 38f8c50d7f native touch track point fix 2026-02-18 08:44:56 +08:00
True Zhuanjia fd653b87af allow sdr workaround on av1 2026-02-18 07:59:52 +08:00
True Zhuanjia 9bf02e477b UI string update 2026-02-14 19:59:55 +08:00
True Zhuanjia 4440c668f1 revert pencil force tweak 2026-02-14 13:55:53 +08:00
True Zhuanjia 63d5417a2c widget profile accessibility 2026-02-14 12:43:31 +08:00
True Zhuanjia 28d9a0faf2 update aboutView 2026-02-14 02:07:28 +08:00
True Zhuanjia 04845df09c enable onscreen widget for pencil pro pack 2026-02-14 01:01:48 +08:00
True Zhuanjia 85df511a23 pencil pro widget profile 2026-02-14 00:14:25 +08:00
True Zhuanjia 4ecb8bfede - fixed: onscreen widget level
- flush frames when entering background
2026-02-13 23:51:31 +08:00
True Zhuanjia 6008865a41 functional button rule update 2026-02-13 23:19:10 +08:00
True Zhuanjia 02926b2c6b widget profile localization 2026-02-13 22:55:00 +08:00
True Zhuanjia 49c5fd308e button sliding fix 2026-02-13 21:31:05 +08:00
True Zhuanjia 7eeff3bc35 hide queueSize stack during streaming 2026-02-13 14:30:25 +08:00
True Zhuanjia 6e99889681 update profile template binaries 2026-02-13 14:19:47 +08:00
True Zhuanjia 8cbd09fe9a update default profile localization 2026-02-12 23:42:36 +08:00
True Zhuanjia edc703c534 allow default profile overwritten by stream view changes 2026-02-12 23:24:35 +08:00
True Zhuanjia f85d545c17 slid&Hold folder stream view guideline 2026-02-12 23:12:23 +08:00
True Zhuanjia 0830173e59 - double tap to make slid&Hold folder movable
- folder state restore improvements
- default profile location ready
2026-02-12 22:54:20 +08:00
True Zhuanjia 0e21e3a9ed Merge commit 'a9ede7a904b1fed1984c28803cf179b41ed257f6' into Integration 2026-02-12 16:03:27 +08:00
True Zhuanjia 6356427454 enable track point for all on screen widgets 2026-02-12 15:51:02 +08:00
True Zhuanjia 4dc6220f28 touch track point improvements 2026-02-12 15:33:43 +08:00
True Zhuanjia 03fcfaff28 sdr performance workaround fix 2026-02-12 14:04:42 +08:00
True Zhuanjia d5c96f77cb highlight touchpad widget while being folded & unfolded 2026-02-11 23:24:17 +08:00
True Zhuanjia bb6f2f0c0d feat: track point for native touch 2026-02-11 22:56:50 +08:00
True Zhuanjia 8d54ed9316 feat: on screen widget touch track point 2026-02-11 21:43:33 +08:00
True Zhuanjia 44123c74d9 crash fixes 2026-02-11 18:54:45 +08:00
True Zhuanjia fdf2548fe3 remove simple & complete legacy on screen controls 2026-02-11 17:04:00 +08:00
True Zhuanjia bee72bd854 - better widget profile update strategy
- feat: restore widget templates
2026-02-11 16:41:46 +08:00
True Zhuanjia 8ec8f32935 crash fixes 2026-02-11 15:00:23 +08:00
True Zhuanjia 1d33feec8b sdr performance workaround fix 2026-02-11 11:43:00 +08:00
True Zhuanjia 80af6a9636 movable button stream view guideline displayed without dragging & moving 2026-02-11 11:26:02 +08:00
True Zhuanjia 94eb4dd8a0 try to fix again:
fixed: squeeze shortcut keys can't be held down
2026-02-11 10:51:47 +08:00
True Zhuanjia d3ce55cbd6 hide sdr performance workaround stack during streaming 2026-02-10 20:16:59 +08:00
True Zhuanjia 0b942caa98 fixed: squeeze shortcut keys can't be held down 2026-02-10 20:12:15 +08:00
True Zhuanjia afc6289a56 - disable sdr performance workaround for av1
- UI string changes
2026-02-10 19:54:53 +08:00
True Zhuanjia 79684598ab learn more action for rendering backend tip 2026-02-10 17:17:35 +08:00
True Zhuanjia b901f2f506 UI string updates 2026-02-10 16:52:35 +08:00
True Zhuanjia 14294009f0 feat: pencil squeeze shortcut 2026-02-10 15:55:36 +08:00
True Zhuanjia adbebd23c8 temporarily disable pencil in-app-purchase for iOS14 and lower OS 2026-02-09 22:43:02 +08:00
True Zhuanjia 3973e13706 feat: disablePencilSlideGestureSwitch 2026-02-09 20:59:41 +08:00
True Zhuanjia e56e68dd6f external display mode settings menu UI string 2026-02-09 17:28:56 +08:00
True Zhuanjia c3fcff8d29 functional button fixes 2026-02-09 16:41:48 +08:00
True Zhuanjia 0152f48886 add mouseButton support for pencil side tap & toolbox menu 2026-02-09 16:19:42 +08:00
True Zhuanjia bbd2e37067 - rename:
balanced mode to standard,
performance mode to metal.

- renderer message text update & menu action fix
2026-02-09 14:50:59 +08:00
True Zhuanjia 8377b5062a default settings update 2026-02-09 09:50:59 +08:00
True Zhuanjia 086f1af974 runtime default setting update changes 2026-02-09 00:16:39 +08:00
True Zhuanjia 652e989927 frame queue tweaks 2026-02-08 23:52:07 +08:00
True Zhuanjia 5480e15595 resume video rendering when app returns from background in legacy pacing mode 2026-02-07 18:05:03 +08:00
True Zhuanjia 375670beb4 sdr performance workaround 2026-02-07 17:00:25 +08:00
True Zhuanjia fa164f0e02 - reduce pressure curve tool tips
- fixed crash in pressure curve tool drawing stage
- initial pencil move force tweak
- frame queue tweaks demo
2026-02-06 15:27:29 +08:00
True Zhuanjia a9ede7a904 fixed: touchPad double tap button not released 2026-02-03 16:13:00 +08:00
True Zhuanjia fa32d157ca load pencil section again 2026-02-02 15:52:48 +08:00
True Zhuanjia 10839f88d8 hide pencil section for 3.2.9 2026-02-02 15:06:51 +08:00
True Zhuanjia 8fa909dea8 tell apart regular & pencil TF 2026-02-02 12:54:59 +08:00
True Zhuanjia 94250624a8 fix renamed var 2026-02-02 11:59:16 +08:00
True Zhuanjia 6f264441ab Merge branch 'Integration' into stylus3
# Conflicts:
#	VoidLink/ViewControllers/StreamFrameViewController.m
2026-02-02 11:56:47 +08:00
True Zhuanjia 03cf463182 dummy event fix 2026-02-02 11:53:50 +08:00
True Zhuanjia 3e29501b23 safetimer for oc 2026-02-02 10:38:38 +08:00
True Zhuanjia 600b882929 restoring fold/unfold states 2026-02-01 16:02:57 +08:00
True Zhuanjia be69af925a folder button minor improvements 2026-02-01 11:26:39 +08:00
True Zhuanjia d14ee640c5 - exclusive folder fix
- other minor fixes
2026-02-01 10:20:12 +08:00
True Zhuanjia ac6c688054 feat: "folder" onscreen button 2026-01-31 20:59:24 +08:00
True Zhuanjia 8f8e86ab4f - on screen button “folder” prototype
- overlapped buttons triggered at touch began
- memory management fixes
- reduce toolbox view & widget layout tool animation
- fast widget profile loading
- toolbox & widget profile selector quick dismiss
2026-01-29 10:06:04 +08:00
True Zhuanjia ab1d6bfa90 storekit2 in app purchase implementation 2026-01-14 20:47:29 +08:00
True Zhuanjia e038601b37 host info updated tip 2026-01-14 10:41:20 +08:00
TrueZhuanjia 008beb9444 Merge branch 'Integration' into stylus3 2026-01-09 11:21:25 +08:00
TrueZhuanjia 835d9c9ef6 fixed: unable to display ipv6 address correctly by long pressing host card view 2026-01-09 11:19:32 +08:00
TrueZhuanjia e29e5bd650 fixed: app crash caused by detecting hosts with alternative IPs 2026-01-09 10:48:24 +08:00
TrueZhuanjia fecdbec31c fixed: unable to save pressure curve to oscProfile in certain cases 2026-01-08 20:28:18 +08:00
TrueZhuanjia 66fdb607d8 Merge branch 'Integration' into stylus3 2026-01-08 18:26:21 +08:00
TrueZhuanjia 6d4f192cbe pressure curve implementation 2026-01-08 16:51:41 +08:00
TrueZhuanjia 4a59af7c20 fixed: app crash caused by controller button string utility 2026-01-07 10:50:12 +08:00
TrueZhuanjia 156b363049 Merge branch 'Integration' into stylus3 2025-12-31 10:55:51 +08:00
TrueZhuanjia c68540c169 allow touchpad widget border width to be zero 2025-12-31 10:08:32 +08:00
TrueZhuanjia f89372e265 Merge branch 'Integration' into stylus3 2025-12-29 17:26:59 +08:00
TrueZhuanjia c134dffec2 turn off unnecessary logs 2025-12-29 16:43:28 +08:00
TrueZhuanjia c2d4527d7e separate X & Y decelerationRate slider 2025-12-29 16:41:07 +08:00
TrueZhuanjia 2071b89d18 new preset on-screen widget profiles 20251229 2025-12-29 16:34:29 +08:00
TrueZhuanjia 46902afdc4 tweak wasdPad center icon 2025-12-29 14:48:33 +08:00
TrueZhuanjia d23e640977 minor fixes 2025-12-28 22:30:40 +08:00
TrueZhuanjia 9c18ab4142 prewarm soft keyboard on app launch 2025-12-28 22:19:30 +08:00
TrueZhuanjia 22e270c4df improve on-screen touchPad stroke 2025-12-28 22:09:15 +08:00
TrueZhuanjia 16bd288e70 preset profile editing 2025-12-28 20:50:21 +08:00
TrueZhuanjia 39643153df Merge branch 'Integration' into stylus3 2025-12-27 20:44:19 +08:00
TrueZhuanjia 2cf9fdfd40 fixed: unable to send touchPad widget to bottom in streamview 2025-12-27 20:43:10 +08:00
TrueZhuanjia f5226562ae Merge branch 'Integration' into stylus3 2025-12-27 14:52:19 +08:00
TrueZhuanjia 32ad5d2f8a highlight border when moving on-screen widgets in layout tool 2025-12-27 14:41:23 +08:00
TrueZhuanjia 245f47f660 always send touchPad widgets to bottom 2025-12-27 14:32:41 +08:00
TrueZhuanjia 17e3fd07f9 quick double tap button for lsWheel/rsWheel 2025-12-27 14:15:51 +08:00
TrueZhuanjia cdbe480863 - directionPad improvements
- feat: highlight alpha tweak
2025-12-27 12:54:46 +08:00
TrueZhuanjia 189ad76f38 - change stroke style, use rounded font style for on-screen widget label 2025-12-26 20:33:21 +08:00
TrueZhuanjia d8528fdc81 - feat: new on-screen widget lsWheel/rsWheel, direction wheel for RPG games.
- feat: direct walkmode test for lsWheel/rsWheel
- feat: direction minimum offset test for all on-screen widgets, gyro->stick mapping & physical controller.
2025-12-26 16:17:55 +08:00
TrueZhuanjia c257a8cab0 Merge branch 'Integration' into stylus3 2025-12-23 01:35:46 +08:00
TrueZhuanjia 2912a3a5cb feat: multi-IP storage for host (tested with zeroTier) 2025-12-23 01:15:37 +08:00
TrueZhuanjia 75f1d4d392 restore content offset after settings menu being loaded for the first time 2025-12-22 15:35:46 +08:00
TrueZhuanjia a6159cc785 Merge branch 'Integration' into stylus3 2025-12-22 10:53:14 +08:00
TrueZhuanjia 38affb4981 add stereo(SDL) audio option 2025-12-22 10:51:33 +08:00
TrueZhuanjia e64090d88f feat: ability to resume mic redirection after a phone call 2025-12-20 20:19:08 +08:00
TrueZhuanjia ef60aeeead fixed: in stereo mode, audio playback can't be resumed after a phone call is ended 2025-12-19 22:27:05 +08:00
TrueZhuanjia 25112b2c27 Merge branch 'Integration' into stylus3 2025-12-13 20:40:57 +08:00
TrueZhuanjia c0b1326dbd fixed: unable to turn on-screen widget on/off during streaming 2025-12-13 20:39:43 +08:00
TrueZhuanjia f77e533dd6 Merge branch 'Integration' into stylus3 2025-12-12 23:47:47 +08:00
TrueZhuanjia 4b001f9366 vertical decelerationRate for inertial touchPads (trackball/LsvPad/RsvPad) 2025-12-12 23:28:19 +08:00
TrueZhuanjia 41eadce89a synthesize rsvPad inertia + gyro input convergently 2025-12-12 21:31:12 +08:00
TrueZhuanjia 46194a5aa0 fixed: tap to toggle button not release after being cleared
reload on-screen widgets only when necessary
2025-12-11 16:40:47 +08:00
TrueZhuanjia 3630880454 - fixed: mic redirection was enabled by default
- fixed: micHandler timer memory leak
- tweak touchPad scroll inertia
2025-12-10 10:38:42 +08:00
TrueZhuanjia a59ffacd3a Merge branch 'Integration' into stylus3 2025-12-09 22:50:45 +08:00
TrueZhuanjia 03817bcfd7 inertial scroll for touchpad gesture 2025-12-09 22:49:34 +08:00
TrueZhuanjia 2c4751bef9 Merge branch 'Integration' into stylus3 2025-12-09 16:40:58 +08:00
TrueZhuanjia 7caa778ff8 - fixed memory leaks caused by on-screen button auto-tap timer
- feat: Inertial RsvPad/LsvPad
2025-12-09 16:38:28 +08:00
TrueZhuanjia 3977d342ba disable pencil tick based on timestamp 2025-12-07 10:13:25 +08:00
TrueZhuanjia 4e83421104 on-screen button manual hover for low-end iPads 2025-12-06 11:22:19 +08:00
TrueZhuanjia adafacbf81 - feat: on-screen button "oscHome" 2025-12-05 10:28:08 +08:00
TrueZhuanjia 053893005b rename varialbles 2025-12-05 10:15:53 +08:00
TrueZhuanjia 838af88d64 minor change 2025-12-04 17:56:14 +08:00
TrueZhuanjia 573ed07b4f Merge branch 'Integration' into stylus3
full color range switch
2025-12-03 13:24:22 +08:00
TrueZhuanjia aad8673ff0 UI tweaks for full color range switch 2025-12-03 13:21:55 +08:00
TrueZhuanJia b6a58ea8ab Merge pull request #160 from Acaki/fix/Metal_AV1_SDR
Fix Metal AV1 SDR color range issue
2025-12-03 08:29:24 +08:00
TrueZhuanjia 8c7bf18b65 tweak touchUp delay 2025-12-02 22:09:00 +08:00
Acaki 3823c95582 feat: add toggle for full range and disable toggles for unsupported codecs 2025-12-02 21:26:09 +08:00
TrueZhuanjia ad0f3dca30 TF bundle ID 2025-12-02 20:36:21 +08:00
TrueZhuanjia 5248bbe9c3 default tickInterval to 1500 2025-12-02 17:39:52 +08:00
TrueZhuanjia 1262a7b41f advancedPencilSupport-demo1 2025-12-02 17:15:56 +08:00
Acaki 85611bcab5 refactor: request limited range for connection 2025-12-02 15:04:15 +08:00
Acaki 46b5d9522d refactor: force limited range for AV1 only 2025-12-02 02:40:03 +08:00
TrueZhuanjia 6dc9ee569b improve widget relocation 2025-12-01 19:16:25 +08:00
TrueZhuanjia 9916f64065 fixed: unable to move mouse cursor under touchpad mode or on mousepad widget when finger moves slowly 2025-12-01 18:51:58 +08:00
Acaki 0ad6b13fae feat: add keyboard shortcut to disconnect & quit app 2025-12-01 17:12:41 +08:00
Acaki 5bbdb76d21 fix: workaround for Metal AV1 darker image 2025-12-01 16:38:47 +08:00
TrueZhuanjia cbf42b1e52 - rounded shape stick boost compensation for physical, gyro mapped & onscreen stick inputs
- add setting stack tips
2025-11-27 22:35:58 +08:00
TrueZhuanjia 72867d855d - minimum offset slider for stick touchpads (rspad rsvpad lspad lsvpad)
- prevents edit button to update onscreen widgets to a different widget type (e.g. touchpad -> button)
2025-11-27 15:06:41 +08:00
TrueZhuanjia 7d5b984157 controller mouse stack info tag crash fix 2025-11-27 10:38:25 +08:00
TrueZhuanjia a3d5771b14 - unlcok motion control section
- controller gyro button stack refactored to motion control section, saved with osc (widget) profile
- feat: reverse gyro-hold button switch
- DS4 gyro activated by default if controller gyro button is disabled
-  synth physical stick input switch
2025-11-27 00:26:53 +08:00
TrueZhuanjia 1338b4fb85 synthesize physical stick input switch for built-in gyro motion control 2025-11-26 16:41:08 +08:00
TrueZhuanjia 379d965769 - feat: controller gyro button (toggle & hold)
- feat: synthesize physical stick input with gyro
2025-11-26 11:48:34 +08:00
TrueZhuanjia d7bc73250d localization fix 2025-11-24 13:07:35 +08:00
TrueZhuanjia 349daef47f UITouch vector util 2025-11-24 08:57:07 +08:00
TrueZhuanjia 138658c635 feat: map controller to mouse with self-defined sticks, buttons, curve expo, pointer velocity. 2025-11-23 13:57:52 +08:00
TrueZhuanjia e058839faf separated controller section in settings menu 2025-11-19 14:50:39 +08:00
TrueZhuanjia d4d5146f66 controller->mouse demo 2025-11-19 14:35:56 +08:00
TrueZhuanjia 85f4838067 fixed: gyro minimum stick offset slider doesn't work on iPad 2025-11-17 00:20:43 +08:00
TrueZhuanjia f23108a3ba fixed some app theme issues 2025-11-15 19:30:53 +08:00
TrueZhuanjia 1623fc34cf fixed: settings menu motion control section some times can't be expanded 2025-11-09 11:18:24 +08:00
TrueZhuanjia d244c9bd1f minor tweak for abs touch 2025-11-08 14:58:42 +08:00
TrueZhuanjia e53e997cd4 tweak favorite setting parentStack spacing 2025-11-08 12:04:30 +08:00
TrueZhuanjia 5c646ef3d2 - touchpad gesture tweaks and improvements
- cancel realtime switch for single point touch gesture passthrough
- cancel remembering settings menu content offset
2025-11-08 11:34:54 +08:00
TrueZhuanjia f01d36d4a0 minor changes 2025-11-08 03:18:09 +08:00
TrueZhuanjia 37fc7e2cb4 fix setting stack long press crash 2025-11-08 03:01:42 +08:00
TrueZhuanjia ea68353328 - optimize touch pad gesture
- relative touch minor fix
2025-11-08 00:42:26 +08:00
TrueZhuanjia da4bfb851c remove test code 2025-11-07 23:45:20 +08:00
TrueZhuanjia 6a1f85975f tweak touch pad gesture sensitivity 2025-11-07 23:41:50 +08:00
TrueZhuanjia 700552bd75 - fixes some UI memory leaks
- clear some obsolete codes
2025-11-07 23:21:53 +08:00
TrueZhuanjia 5750b99af3 - fixed: memory leak and UI stuck caused by MenuSenctionView
- feat: memorize content offset for settings menu
- optimize gesture passthrough for relative touch & absolute touch
- feat: passthroughGesturesSwitch, ctrlDownForPinchSwitch, leftClickDelaySlider, touchModeSelector2 in experimental section
2025-11-07 17:34:19 +08:00
TrueZhuanjia cc35e6b3a4 touchmode indicator for exprerimental section 2025-11-06 21:05:34 +08:00
TrueZhuanjia 3b4800eb78 - touchPad mode optimization 2025-11-06 20:36:09 +08:00
TrueZhuanjia a2855ef522 feat: touchPad gesture sensitivities 2025-11-06 20:19:06 +08:00
TrueZhuanjia 935406b75b - slide distance threshold slider for relative touch & "mousePad", that fixes mouse cursor jump 2025-11-06 16:36:48 +08:00
TrueZhuanjia e933c5de60 - enhanced relative touch (touchpad mode & "mousePad" widget) pointer accuracy
- touchpad gesture simulation for touchPad & single point mode
2025-11-06 11:52:13 +08:00
TrueZhuanjia a23a5491ae feat: functional button "AbsTchDrag" 2025-11-05 11:51:29 +08:00
TrueZhuanjia 1ad8f51b7a feat: widget "ABSMOUSE" 2025-11-05 08:59:39 +08:00
TrueZhuanjia 4d1619346d update c module for "ABSMOUSE" widget 2025-11-04 21:56:24 +08:00
TrueZhuanjia df8c273cb9 - feed integral for roll->left stick motion control
- feat: roll->left stick motion control multi-level sensitivity
2025-11-04 16:01:21 +08:00
TrueZhuanjia 6eca97b4aa fixed: default on-screen widget borderAlpha offset 2025-11-04 11:03:36 +08:00
TrueZhuanjia 39f8398b10 feat: on screen widget "discreteWheel" 2025-11-04 10:48:20 +08:00
TrueZhuanjia e32a356cb5 feat: optimized single point (absolute touch) for OSRS & regular use cases 2025-11-03 23:58:49 +08:00
TrueZhuanjia e0c556da10 feat: mute in background switch 2025-11-03 16:18:21 +08:00
TrueZhuanjia a3f9e07cf7 feat: duck other apps switch in audio section 2025-11-03 11:25:57 +08:00
TrueZhuanjia ff40e55cd1 feat: append mouse left click after touch up as an option for OSRS (Old School RuneScape) or similar games that requires double tap on touchscreen 2025-10-31 15:45:55 +08:00
TrueZhuanjia e548bda73a stereo to system audio engine, multi-channel audio to SDL 2025-10-30 21:35:27 +08:00
TrueZhuanjia cf11b06f3e multi-audioChannel test 2025-10-30 15:41:10 +08:00
TrueZhuanjia a799c39aff set "useBuiltinMic" to true by default 2025-10-29 09:59:06 +08:00
TrueZhuanjia ff230b3433 refactor audio engine init to Connection.m 2025-10-29 09:36:16 +08:00
TrueZhuanjia a9f4db52b5 audio session config test 2025-10-29 00:16:24 +08:00
TrueZhuanjia 071e2a764a feat: audio engine selector 2025-10-28 23:33:42 +08:00
TrueZhuanjia e050c914bb Merge branch 'systemAudioEngineTest' into Integration 2025-10-28 21:55:57 +08:00
TrueZhuanjia cd9ef74f3f add multi-channel audio support 2025-10-28 21:54:16 +08:00
TrueZhuanjia 644687a50c optimize direction pad haptic feedback 2025-10-28 20:24:48 +08:00
TrueZhuanjia e7132e3371 - feat: highlight size tweak
- fixed: legacy onscreen controls unavailable in touch disabled mode
2025-10-28 16:41:39 +08:00
TrueZhuanjia 7223ab3be5 cancel haptic feedback for legacy on-screen control direction pad 2025-10-28 08:47:04 +08:00
TrueZhuanjia 99952281cb fixed: legacy on-screen control layers pass through native touch events 2025-10-28 08:37:26 +08:00
TrueZhuanjia 9c518dfeb6 fixed: direction pad on-screen widgets losing haptic feedback on iPhone 2025-10-28 08:18:31 +08:00
TrueZhuanjia 28e2f13e87 test: replace SDL with system audio engine to support audio playing when screen is dimmed out 2025-10-27 17:12:45 +08:00
TrueZhuanjia 4a9e109de5 stop video render when app enters background without pip enabled for balanced mode 2025-10-25 13:57:36 +08:00
TrueZhuanjia f5b56ca67f Fixed: widget profile, widget tool entry doesn't work for single point touch 2025-10-22 11:43:26 +08:00
TrueZhuanjia b7ad4d3562 Fixed: on-screen widget default border alpha issue 2025-10-20 19:45:33 +08:00
TrueZhuanjia 94a8caa5df fixed: unable to typing non-ascii characters by default iOS soft keyboards 2025-10-20 15:56:45 +08:00
TrueZhuanjia b650ac0ca3 show soft keyboard toolbar by default 2025-10-20 11:50:05 +08:00
TrueZhuanjia 9dda594519 feat: on-screen widget border alpha tweak 2025-10-19 11:04:56 +08:00
TrueZhuanjia 0ea6248773 minor fix for settings menu 2025-10-19 00:14:45 +08:00
TrueZhuanjia fee003b824 feat: app theme selector 2025-10-18 23:30:40 +08:00
TrueZhuanjia cd415cc639 feat: setup autotap timer by text input instead of slider. 2025-10-18 19:14:21 +08:00
TrueZhuanjia 10ceac2ce3 feat: label alpha tweak for on-screen widgets 2025-10-17 20:39:06 +08:00
TrueZhuanjia 86a0ff46ee - dynamic update theme fix for safeAreaPadding on iPhone 2025-10-17 16:27:55 +08:00
TrueZhuanjia 3887ad0c4c - iOS 14 UI minor fix for settings menu 2025-10-17 15:02:02 +08:00
TrueZhuanjia 58f534fcc8 minor fix 2025-10-15 22:15:07 +08:00
TrueZhuanjia f8e14006c1 try to reduce on-screen widget performance overheads 2025-10-15 21:43:55 +08:00
TrueZhuanjia a959a346b0 fixed: motion control configs not loaded correctly from oscProfile 2025-10-15 09:53:28 +08:00
TrueZhuanjia 65459b3f4e update preset widget profiles 2025-10-15 09:35:40 +08:00
TrueZhuanjia 2216899204 minor tweak 2025-10-14 23:10:25 +08:00
TrueZhuanjia 5fb4887c67 fixed: settings menu safeArea adaption some times not working on iPhone 2025-10-14 23:05:25 +08:00
TrueZhuanjia 6ff2add9dd - feat: edge sliding sensitivity for edge sliding gestures
- update flag for new preset widget profiles
2025-10-14 22:34:28 +08:00
TrueZhuanjia c963c1254d update preset widget profiles 2025-10-14 16:43:36 +08:00
TrueZhuanjia 8afce40e81 - feat: functional buttons "toolbox" "widgetTool" "widgetProfiles" "softKeyboard" 2025-10-14 15:49:06 +08:00
TrueZhuanjia e44504bc48 - feat: hybrid form for mousePad & LT/RT Pad 2025-10-14 14:22:44 +08:00
TrueZhuanjia 11b79dda5b - feat: yaw & pitch factor for each on-screen "gyro" button for seamlessly toggling among different level of gyro sensitivities. 2025-10-14 01:37:22 +08:00
TrueZhuanjia 8cee8b679c new preset on-screen widget templates with gyro & settings button 2025-10-12 23:21:54 +08:00
TrueZhuanjia 1cb9360d83 refactor "gyroOff" to "gyroPause" 2025-10-12 22:01:38 +08:00
TrueZhuanjia 297f5ad7e6 fixed: direction pad on-screen widget (Wasdpad, dPad ...) not responsive on first touch move 2025-10-12 21:28:21 +08:00
TrueZhuanjia 3f2a011d7b - feat: gyro-off on-screen button, the opposite of "gyro" button
- fixed: auto tap slider disappeared for regular on-screen buttons
2025-10-12 20:41:33 +08:00
TrueZhuanjia dd0c36bb20 feat: haptic engine selector 2025-10-12 16:49:10 +08:00
TrueZhuanjia aff1bdfe89 - feat: on screen widget "LTAPD" & "RTPAD" (left trigger pad & right trigger pad)
- try to fix single-tap not responsive on some iPhone by "single tap sensitivity option" in settings menu
2025-10-11 15:31:41 +08:00
TrueZhuanJia 00533f5d3b Merge pull request #132 from twilightgod/iphone-haptics
Fall back to phone haptics engine if controller doesn't support it
2025-10-09 17:12:19 +08:00
TrueZhuanjia eff682bc05 feat: minimum stick offset for gyro -> stick mapping 2025-10-09 09:58:57 +08:00
TrueZhuanjia 16a210efa2 feat: motion control - direct mapping from gyro to mouse or controller sticks, with on-screen button "gyro" for toggling 2025-10-07 23:09:06 +08:00
TrueZhuanjia 99a2a4ca1a minor fixes 2025-10-04 17:22:32 +08:00
TrueZhuanjia 67af68fcf1 - motion control demo1
- feat: movable on-screen functional buttons
2025-10-04 16:44:43 +08:00
TrueZhuanjia b96e2d1bdf fixed: built-in gyro mapping doesn't work on iOS12 & iOS13 2025-10-02 12:25:31 +08:00
TrueZhuanjia 2bb0ce086f - feat: tap to toggle on-screen button triggering
- fixed: special overlay button relocation
2025-09-29 17:27:49 +08:00
TrueZhuanjia 9bc518dc68 set button-type widget default properties 2025-09-29 10:44:26 +08:00
TrueZhuanjia f60433ff3f try to fix: built-in gyro doesn't work on iOS12 & iOS13 2025-09-28 10:59:01 +08:00
TrueZhuanjia b4493707ca Merge branch 'menuSectionFoldStateMemorize' into Integration 2025-09-28 10:35:34 +08:00
TrueZhuanjia 0fe10d8654 feat: remember menu section fold state 2025-09-28 10:34:40 +08:00
Rex Xiong 87d9590f3b Update HapticContext.m 2025-09-27 13:35:19 -07:00
Rex Xiong eeff9b74da Fallback to phone haptics engine if controller doesn't support it 2025-09-27 13:13:26 -07:00
TrueZhuanjia ceebd19c0e Merge branch 'aliasKeyboardType' into Integration 2025-09-26 09:18:00 +08:00
TrueZhuanjia 00e0566dd4 story board UI string fix 2025-09-25 20:00:21 +08:00
TrueZhuanjia acd4a04076 fixed: settings menu accidentally resized on iPad 2025-09-25 16:22:27 +08:00
TrueZhuanjia ac0aa29578 feat: mic volume 2025-09-25 13:30:30 +08:00
TrueZhuanjia 9a053d9ed8 fixed: mic redirection not working on iOS 12 2025-09-24 22:43:14 +08:00
TrueZhuanjia 9124a1fee5 use Deque for mic audio ringBuffering 2025-09-24 16:50:14 +08:00
TrueZhuanjia 858e7c7440 feat: use built-in mic 2025-09-24 12:49:25 +08:00
TrueZhuanjia e5669e2a30 feat: local volume 2025-09-23 23:51:21 +08:00
TrueZhuanjia 431b32b680 try to fix broken native touch & broken on screen widget layout when orientation is locked on iPhone iOS 16/17 control center 2025-09-23 20:55:15 +08:00
TrueZhuanjia 7dce11b4c4 add libupos-sim.a file reference 2025-09-22 23:04:58 +08:00
TrueZhuanjia 9fca3a5984 storyboard & settings menu UI fixes 2025-09-22 21:42:02 +08:00
TrueZhuanjia 733d5c38f3 - fixed: non-AWDL stuttering fix doesn't work when settings menu expands during streaming 2025-09-22 15:49:40 +08:00
HsunLu 3834b5a604 Fix: allow default keyboard for alias input (support Zhuyin) 2025-09-22 14:40:11 +08:00
TrueZhuanjia 07e36029d0 tweak relative touch handler 2025-09-22 13:18:25 +08:00
TrueZhuanjia 689221f10e send dummy event to prevent stuttering 2025-09-21 18:35:56 +08:00
TrueZhuanjia 60dacdf05f feat: dynamic theme update 2025-09-18 16:53:17 +08:00
TrueZhuanjia 1614bb0609 Merge branch 'dynamicTheme' into Integration
# Conflicts:
#	VoidLink/ViewControllers/SettingsViewController.m
2025-09-18 11:02:15 +08:00
TrueZhuanjia a386867dd0 fixed: stream view interrupted by mic stream 2025-09-16 08:44:38 +08:00
TrueZhuanjia e177d6eaea perfGraph tips 2025-09-15 13:15:04 +08:00
TrueZhuanjia 464492529e fixed possible crash in micHandler 2025-09-15 11:37:50 +08:00
TrueZhuanjia b950882846 tweak stats overlay 2025-09-15 10:54:53 +08:00
TrueZhuanjia 63808c9e9c use safeTimer for micHandler 2025-09-15 10:39:20 +08:00
TrueZhuanjia 76d138d64d minor change on storyboard 2025-09-15 09:33:47 +08:00
TrueZhuanjia 1232644268 minor fixes 2025-09-14 10:07:50 +08:00
TrueZhuanjia 2e776baec9 fixed potential crash in mousePad widget 2025-09-14 03:09:11 +08:00
TrueZhuanjia 8fb4829aef further fix widget layout with orientation change issue 2025-09-13 22:57:20 +08:00
TrueZhuanjia fcbc844587 switch to default profile after importing widget templates 2025-09-13 22:33:34 +08:00
TrueZhuanjia aecb1163e3 minor UI fixes & tweaks 2025-09-13 21:55:02 +08:00
TrueZhuanjia 0e51a49898 tweak statsOverlay 2025-09-13 21:21:57 +08:00
TrueZhuanjia adce543c45 hostView settings menu gesture improvement 2025-09-13 18:34:49 +08:00
TrueZhuanjia 83a32f96e6 update preset on screen widget templates 2025-09-13 18:01:18 +08:00
TrueZhuanjia 6c72f0331b feat: implements button visual feedback switch for legacy osc 2025-09-13 17:33:40 +08:00
TrueZhuanjia 92f1fbf33b fixed: unpredictable on screen widget size after orientation change in layout tool. now widget size stays consistent. 2025-09-13 17:13:11 +08:00
TrueZhuanjia 424fe2c2f0 fixed: allow widget profiles with identical names and shows the checkmark at the correct profile 2025-09-12 22:47:10 +08:00
TrueZhuanjia f5d629c62c replace widget templates 2025-09-12 22:16:19 +08:00
TrueZhuanjia 19fa975b6d - fixed: widget height adjusting bug 2025-09-12 21:58:25 +08:00
TrueZhuanjia a4951309d3 widget layout tool minor fixes 2025-09-12 17:03:40 +08:00
TrueZhuanjia 3ec53c29d4 - minor UI fix for touch mode selector 2025-09-12 13:14:38 +08:00
TrueZhuanjia 3fcaafc42b iOS 14 settings menu UI bug fix 2025-09-12 12:58:06 +08:00
TrueZhuanjia 5318acc427 minor fix for setting menu loading speed 2025-09-12 10:13:48 +08:00
TrueZhuanjia daa4fc80eb fixed: broken touch input caused by Absolute Touch 2025-09-11 23:14:58 +08:00
TrueZhuanjia c094d74d8e - feat: allow touchPad label to be display if it's different from touchPad cmdString 2025-09-11 21:39:55 +08:00
TrueZhuanjia 5418936816 - feat: mouseWheel on-screen widget 2025-09-11 21:33:15 +08:00
TrueZhuanjia c3b7679268 - fixed: widget height changes unexpectedly after being tapped 2025-09-11 19:49:36 +08:00
TrueZhuanjia d12b83d1ef - Fixed: mousepad unable to perform 2-finger right click in hovering mode
- Minor changes & improvements for mousePad widget
2025-09-11 17:42:17 +08:00
TrueZhuanjia 32b78d1a4c - move performance graph setting stack to experimental section 2025-09-11 15:02:55 +08:00
TrueZhuanjia 71f66fe8c0 - UI: highlighting emerging setting stack 2025-09-11 14:51:39 +08:00
TrueZhuanjia d054bbe35f - feat: on-screen button visual feedback switch
- feat: touch input disabled mode (while allowing on-screen widget input)
- fixed: swapAbxy stack dynamic UI
2025-09-11 11:37:14 +08:00
TrueZhuanjia bd1c4e3cc9 - update mic permission tip 2025-09-10 21:27:23 +08:00
TrueZhuanjia 928b765700 compatibility fix from c-module 2025-09-10 20:47:35 +08:00
TrueZhuanjia 45acc28384 Tap gesture fix 2025-09-10 12:39:26 +08:00
TrueZhuanjia 4e65abf6b1 - feat: auto tap for on-screen widget.
- fixed: on-screen widget button stuck.
2025-09-10 03:02:28 +08:00
TrueZhuanjia d818f8a7ce allows on-screen widget vibration during mic redirection 2025-09-08 20:24:11 +08:00
TrueZhuanjia c0850ce9d2 Merge branch 'micDemo' into Integration 2025-09-08 16:52:45 +08:00
TrueZhuanjia c3941df7e9 - feat: mic redirection (with Sunshine-Foundation) 2025-09-08 16:47:27 +08:00
TrueZhuanjia b1b46b3ec0 Merge commit 'metal renderer crash fix' into Integration 2025-09-02 12:36:50 +08:00
TrueZhuanjia dff6679a90 lib opus file reference 2025-09-02 08:54:53 +08:00
Acaki 4ab21aed0e fix: crash on background timer expired with Metal renderer 2025-09-01 22:51:41 +08:00
TrueZhuanjia d67a40f155 c-module source file reference update 2025-09-01 22:04:55 +08:00
TrueZhuanjia b506917382 update c-module for mic implementation 2025-09-01 10:41:51 +08:00
TrueZhuanjia 944f7422b3 set maximum bitrate to 800Mbps 2025-08-31 11:12:48 +08:00
TrueZhuanjia 4843c046d7 fixed: bitrate slider disabled when sunshine server does not support dynamic bitrate 2025-08-31 09:14:52 +08:00
TrueZhuanjia 61df4e7e61 a few ui string tweaks 2025-08-30 19:22:09 +08:00
TrueZhuanjia 48558316d1 Merge branch 'perfMode' into Integration 2025-08-30 18:57:59 +08:00
TrueZhuanjia 5903b358ee - unify pixel format to MTLPixelFormatRGBA16Float
- move renderingBackEnd to experimental section, add a few tips for performance mode
2025-08-30 18:54:30 +08:00
TrueZhuanjia 6881098ca2 Fixed: failed to launch app on iPhone when there're specific number of hosts presented no the host view. 2025-08-30 11:34:29 +08:00
TrueZhuanjia 4d783649f5 cancel iOS13 availability check for metal renderer 2025-08-28 23:34:26 +08:00
Acaki 2b40224b2b refactor: replace MTLPixelFormatBGR10A2Unorm with MTLPixelFormatBGRA10_XR for older devices 2025-08-28 21:31:14 +08:00
Acaki e4d6be3bb3 Revert "fix: crash when host is on hdr but client is not on Metal lower iOS versions"
This reverts commit 6c0b1550fd.
2025-08-28 21:29:51 +08:00
Acaki 6c0b1550fd fix: crash when host is on hdr but client is not on Metal lower iOS versions 2025-08-28 13:28:28 +08:00
TrueZhuanjia 967fbe8072 feat: no click option for mousePad on-screen widget 2025-08-28 13:23:35 +08:00
Acaki c7dd7a2c21 fix: metal renderer packed format crash on emulator and lower iOS version 2025-08-28 00:07:56 +08:00
TrueZhuanjia 49ed0e43e9 reduce on-screen widget visual effect performance cost 2025-08-27 22:35:47 +08:00
TrueZhuanjia 6bae985e2c ui & string tweaks 2025-08-27 20:20:48 +08:00
Acaki 39d7d9e213 feat: add back lowest latency mode for AVSampleBuffer 2025-08-27 15:53:00 +08:00
Acaki eb3501e090 Revert "feat: bring back CAMetalDisplayLink"
This reverts commit b6c029df
2025-08-27 15:16:24 +08:00
TrueZhuanjia 83566ad4e1 try to reduce directionPad on-screen widget performance cost 2025-08-27 14:04:45 +08:00
TrueZhuanjia 19e78d5eb6 Merge branch 'dynamicBitrate' into Integration 2025-08-26 14:45:04 +08:00
TrueZhuanjia 31ab333f5b realtime bitrate adjustment with Sunshine-Foundation (experimental) 2025-08-25 10:07:38 +08:00
xzzpig ff8cb20cae add support for dynamic theme update 2025-07-30 01:20:36 +08:00
198 changed files with 77208 additions and 9517 deletions
Vendored
BIN
View File
Binary file not shown.
+91
View File
@@ -0,0 +1,91 @@
name: Build VoidLink IPA
on:
push:
branches:
- Integration
workflow_dispatch:
jobs:
build:
name: Build .ipa and publish source
runs-on: macos
env:
# Per-app: change APP_NAME for each app repo. Used everywhere so the
# perlis dir name and file names always match (no hardcoded drift).
APP_NAME: VoidLink
# Point xcodebuild at full Xcode without sudo (sudo hangs in CI).
# DEVELOPER_DIR overrides the active developer directory for all steps.
DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Checkout submodules
run: git submodule update --init --recursive
- name: Select Xcode
run: xcodebuild -version
- name: Build unsigned .ipa
run: |
# Build the app (no signing — SideStore signs on-device).
# VoidLink's scheme is auto-generated from the VoidLink target.
# Release config → bundle id com.voidlink.tf.iOS (Debug uses
# com.voidlink.tf.debug10.iOS), so the .ipa matches the metadata.
xcodebuild \
-scheme VoidLink \
-sdk iphoneos \
-configuration Release \
archive \
-archivePath $PWD/build/VoidLink.xcarchive \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED="NO" \
CODE_SIGN_ENTITLEMENTS="" \
CODE_SIGNING_ALLOWED="NO"
# Package the .app bundle into an .ipa (Payload/App.app structure)
mkdir -p build/IPA/Payload
cp -R build/VoidLink.xcarchive/Products/Applications/VoidLink.app build/IPA/Payload/VoidLink.app
cd build/IPA
zip -r ../../$APP_NAME.ipa Payload
cd ../..
ls -la $APP_NAME.ipa
- name: Generate app metadata
run: |
# Read the version from the built app so it always matches the .ipa
# (no hardcoded drift). CFBundleShortVersionString = MARKETING_VERSION.
VERSION=$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" \
build/VoidLink.xcarchive/Products/Applications/VoidLink.app/Info.plist)
SIZE=$(stat -f%z $APP_NAME.ipa)
DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
# Per-app metadata file. The perlis aggregator folds this into the
# root source.json at https://apps.bueso.eu/source.json.
cat > $APP_NAME.json <<EOF
{
"name": "VoidLink",
"bundleIdentifier": "com.voidlink.tf.iOS",
"developerName": "Luis",
"localizedDescription": "VoidLink (Moonlight fork) - stream from your PC. Built via Gitea Actions.",
"iconURL": "https://apps.bueso.eu/$APP_NAME/icon.png",
"downloadURL": "https://apps.bueso.eu/$APP_NAME/$APP_NAME.ipa",
"version": "$VERSION",
"versionDate": "$DATE",
"size": $SIZE,
"permissions": []
}
EOF
cat $APP_NAME.json
- name: Upload to source host (perlis)
run: |
# Upload .ipa + per-app metadata + icon to perlis via SSH.
# PERLIS_SSH_HOST is the Tailscale MagicDNS name (perlis.bueso.loc),
# reachable from the iMac runner over the Tailscale mesh.
ssh -o StrictHostKeyChecking=no \
"${{ secrets.PERLIS_SSH_USER }}@${{ secrets.PERLIS_SSH_HOST }}" \
"mkdir -p /mnt/appdata/ios-apps/$APP_NAME"
scp -o StrictHostKeyChecking=no \
$APP_NAME.ipa $APP_NAME.json icon.png \
"${{ secrets.PERLIS_SSH_USER }}@${{ secrets.PERLIS_SSH_HOST }}:/mnt/appdata/ios-apps/$APP_NAME/"
+2 -1
View File
@@ -4,6 +4,7 @@
.DS_Store
Build
DerivedData
build-home/
*.xccheckout
.github
README.md
README.md
+3
View File
@@ -7,3 +7,6 @@ url=https://github.com/TrueZhuangJia/voidlink-c.git
[submodule "third_party/imgui"]
path = third_party/imgui
url = https://github.com/ocornut/imgui.git
[submodule "third_party/moonlight-audio-haptics"]
path = third_party/moonlight-audio-haptics
url = https://github.com/TrueZhuangJia/moonlight-audio-haptics.git
+9 -3
View File
@@ -1,9 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="24506" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina6_1" orientation="landscape" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15509"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24504"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@@ -12,8 +13,13 @@
<view contentMode="scaleToFill" id="VWy-YH-qYj">
<rect key="frame" x="0.0" y="0.0" width="896" height="414"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="backgroundColor" white="0.33333333329999998" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<point key="canvasLocation" x="33" y="187"/>
</view>
</objects>
<resources>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>
+1 -1
View File
@@ -985,7 +985,7 @@ ICAgICAgICAgICAgICA
<!--Profiles Table View Controller-->
<scene sceneID="W0E-wz-n2b">
<objects>
<viewController storyboardIdentifier="OSCProfilesTableViewController" id="Atv-Nj-0qv" customClass="OSCProfilesTableViewController" sceneMemberID="viewController">
<viewController storyboardIdentifier="OSCProfilesTableViewController" id="Atv-Nj-0qv" customClass="OSCProfilesTableViewController" customModule="VoidLink" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="osA-Uy-SE1"/>
<viewControllerLayoutGuide type="bottom" id="8ja-wQ-jbJ"/>
+1 -1
View File
@@ -1005,7 +1005,7 @@ ICAgICAgICAgICAgIA
<!--Profiles Table View Controller-->
<scene sceneID="C7c-yg-gQK">
<objects>
<viewController storyboardIdentifier="OSCProfilesTableViewController" id="0K8-kQ-xlN" customClass="OSCProfilesTableViewController" sceneMemberID="viewController">
<viewController storyboardIdentifier="OSCProfilesTableViewController" id="0K8-kQ-xlN" customClass="OSCProfilesTableViewController" customModule="VoidLink" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Nat-X1-41w"/>
<viewControllerLayoutGuide type="bottom" id="9SV-fx-82v"/>
+430 -74
View File
@@ -3,29 +3,64 @@
archiveVersion = 1;
classes = {
};
objectVersion = 60;
objectVersion = 70;
objects = {
/* Begin PBXBuildFile section */
A1D500022FFC000100000001 /* authored_haptics_engine.cpp in Sources */ = {isa = PBXBuildFile; fileRef = A1D500012FFC000100000001 /* authored_haptics_engine.cpp */; settings = {COMPILER_FLAGS = "-std=c++17"; }; };
180345E12E819877004AF36F /* libopus-sim.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 180345E02E819877004AF36F /* libopus-sim.a */; };
180E6E372E93BFDB0067C73A /* AlertControllerUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 180E6E362E93BFDB0067C73A /* AlertControllerUtil.swift */; };
1817B61E2E6FD04D000D938D /* SafeTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1817B61D2E6FD04D000D938D /* SafeTimer.swift */; };
182760F72E83E2EA00EC1C5C /* DequeModule in Frameworks */ = {isa = PBXBuildFile; productRef = 182760F62E83E2EA00EC1C5C /* DequeModule */; };
182760FA2E83E57D00EC1C5C /* DequeModule in Frameworks */ = {isa = PBXBuildFile; productRef = 182760F92E83E57D00EC1C5C /* DequeModule */; };
182760FD2E83E68B00EC1C5C /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = 182760FC2E83E68B00EC1C5C /* Collections */; };
1829CE3F2E2154DF0074A99A /* SceneDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 1829CE3E2E2154DF0074A99A /* SceneDelegate.m */; };
18341A042DEB662C0094E5F2 /* FixedTintImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 18341A032DEB662C0094E5F2 /* FixedTintImageView.m */; };
18416EA42EBB3DCE00AE0124 /* UITouchUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18416EA32EBB3DCE00AE0124 /* UITouchUtil.swift */; };
1850C82A2E667E4000845308 /* MicHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1850C8292E667E4000845308 /* MicHandler.swift */; };
18729EDE2E8BD68800101194 /* UUIDHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18729EDD2E8BD68800101194 /* UUIDHelper.swift */; };
18780CC72F10D66000B90483 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 18780CC62F10D66000B90483 /* StoreKit.framework */; };
A76B00032FFCC00100000003 /* GameSirG8MFiRumbler.swift in Sources */ = {isa = PBXBuildFile; fileRef = A76B00022FFCC00100000002 /* GameSirG8MFiRumbler.swift */; };
A76B00052FFCC00100000005 /* ExternalAccessory.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A76B00042FFCC00100000004 /* ExternalAccessory.framework */; };
18780CCA2F10D6CC00B90483 /* IAPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18780CC92F10D6CC00B90483 /* IAPManager.swift */; };
187F4B282E04658A00EECC59 /* WidgetPanelStackView.m in Sources */ = {isa = PBXBuildFile; fileRef = 187F4B272E04658A00EECC59 /* WidgetPanelStackView.m */; };
1888C1922E24C33100108730 /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1888C1912E24C33100108730 /* AboutViewController.swift */; };
1888C1932E24C41000108730 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1888C1902E24C2C800108730 /* AboutView.swift */; };
188973D32E386A670025E48C /* widgetTemplatesIPhone.bin in Resources */ = {isa = PBXBuildFile; fileRef = 188973D22E386A670025E48C /* widgetTemplatesIPhone.bin */; };
189AD29A2DE7446000E24B88 /* HostCollectionViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 189AD2992DE7446000E24B88 /* HostCollectionViewController.m */; };
18951D1D2EBB6D2300A86E12 /* TouchPadGestureHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18951D1C2EBB6D2300A86E12 /* TouchPadGestureHandler.swift */; };
189AD29A2DE7446000E24B88 /* HostCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 189AD2992DE7446000E24B88 /* HostCollectionViewController.swift */; };
18A0EE252EFB9B5A00BA853A /* SVGKit in Frameworks */ = {isa = PBXBuildFile; productRef = 18A0EE242EFB9B5A00BA853A /* SVGKit */; };
18A0EE272EFB9B5A00BA853A /* SVGKitSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 18A0EE262EFB9B5A00BA853A /* SVGKitSwift */; };
18A0EE2C2EFBCCFD00BA853A /* GraphicUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A0EE2B2EFBCCFD00BA853A /* GraphicUtils.swift */; };
18A765BD2ECD51FB004784AA /* ControllerUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A765BC2ECD51FB004784AA /* ControllerUtil.swift */; };
18B317032EE67A3A009B62C2 /* InertialScroller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B317022EE67A39009B62C2 /* InertialScroller.swift */; };
18D43D7F2F050240008C326F /* PressureCurve.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18D43D7E2F050240008C326F /* PressureCurve.swift */; };
18D43D812F050240008C326F /* PencilTipOffsetCalibrationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18D43D802F050240008C326F /* PencilTipOffsetCalibrationViewController.swift */; };
18E4BCEA2E891895000A8657 /* MotionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18E4BCE92E891895000A8657 /* MotionHandler.swift */; };
18FCADAF2ECAC23200787A96 /* PencilHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FCADAE2ECAC23200787A96 /* PencilHandler.swift */; };
2A263D4A2FF3B3F800154DEA /* PublicExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A263D492FF3B3F800154DEA /* PublicExtensions.swift */; };
2A263D4C2FF3B49600154DEA /* PublicExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A263D492FF3B3F800154DEA /* PublicExtensions.swift */; };
2A263D4F2FF3BA2800154DEA /* PublicUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A263D4E2FF3BA2800154DEA /* PublicUtils.swift */; };
2A263D502FF3BA2800154DEA /* PublicUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A263D4E2FF3BA2800154DEA /* PublicUtils.swift */; };
2A7113FD2F5A6C65001D852D /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A7113FC2F5A6C65001D852D /* ThemeManager.swift */; };
2A7113FF2F5A6C65001D852D /* UIAppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A7113FE2F5A6C65001D852D /* UIAppView.swift */; };
2A7114012F5A6C65001D852D /* GamepadNavigationIllustrationHud.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A7114002F5A6C65001D852D /* GamepadNavigationIllustrationHud.swift */; };
2AAA4E822F7666C5001E20DC /* ControllerUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A765BC2ECD51FB004784AA /* ControllerUtil.swift */; };
2AAA4E842F7666EE001E20DC /* LocalizationHelperExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E1CDF92C4FE83400547904 /* LocalizationHelperExtension.swift */; };
2AAA4E872F985F12001E20DC /* LocalizationHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 84FD0B502C31338D006B5BE5 /* LocalizationHelper.m */; };
2AC1E3832F507D80004F76C0 /* GenericUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AC1E3822F507D80004F76C0 /* GenericUtils.swift */; };
2AC1EAFB2F92260F004EDA80 /* UIStackExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AC1EAFA2F922602004EDA80 /* UIStackExtension.swift */; };
2AC1EAFC2F922889004EDA80 /* UIStackExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AC1EAFA2F922602004EDA80 /* UIStackExtension.swift */; };
2B7A10022FFB000100000001 /* SettingsTouchVelocityPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7A10012FFB000100000001 /* SettingsTouchVelocityPreview.swift */; };
3A4A3A112F0C000100000001 /* ProfileSelectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4A3A102F0C000100000001 /* ProfileSelectorViewController.swift */; };
3A4A3A132F0C000100000003 /* LayoutOnScreenControlsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4A3A122F0C000100000002 /* LayoutOnScreenControlsViewController.swift */; };
840751B32BF49C050096B657 /* CustomTapGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 840751B22BF49C050096B657 /* CustomTapGestureRecognizer.m */; };
841BAC922DE35D6B00DA639E /* ThemeManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 841BAC912DE35D6B00DA639E /* ThemeManager.m */; };
84203F232C5F24C00032EEED /* OnScreenWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84203F222C5F24C00032EEED /* OnScreenWidgetView.swift */; };
84351B0E2C29ACCB00A77989 /* LayoutOnScreenControls.m in Sources */ = {isa = PBXBuildFile; fileRef = 84351B072C29ACCA00A77989 /* LayoutOnScreenControls.m */; };
84351B0F2C29ACCB00A77989 /* OnScreenButtonState.m in Sources */ = {isa = PBXBuildFile; fileRef = 84351B092C29ACCA00A77989 /* OnScreenButtonState.m */; };
84351B102C29ACCB00A77989 /* OSCProfile.m in Sources */ = {isa = PBXBuildFile; fileRef = 84351B0A2C29ACCA00A77989 /* OSCProfile.m */; };
84351B112C29ACCB00A77989 /* OSCProfilesManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 84351B0B2C29ACCB00A77989 /* OSCProfilesManager.m */; };
84351B2B2C29AD6100A77989 /* LayoutOnScreenControlsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 84351B252C29AD6000A77989 /* LayoutOnScreenControlsViewController.m */; };
84351B2C2C29AD6100A77989 /* OSCProfilesTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 84351B272C29AD6000A77989 /* OSCProfilesTableViewController.m */; };
84351B112C29ACCB00A77989 /* OSCProfilesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1D4E0A22FBBBBCC00112233 /* OSCProfilesManager.swift */; };
84351B2D2C29AD6100A77989 /* ToolBarContainerView.m in Sources */ = {isa = PBXBuildFile; fileRef = 84351B292C29AD6000A77989 /* ToolBarContainerView.m */; };
84351B5C2C29ADBE00A77989 /* ProfileTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 84351B592C29ADBE00A77989 /* ProfileTableViewCell.m */; };
84351B5D2C29ADBE00A77989 /* ProfileTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 84351B5B2C29ADBE00A77989 /* ProfileTableViewCell.xib */; };
843667F02D9D7173004A7D7F /* widgetTemplates.bin in Resources */ = {isa = PBXBuildFile; fileRef = 843667EF2D9D7173004A7D7F /* widgetTemplates.bin */; };
843CC7F02C1E757D00BBC475 /* NativeTouchPointer.m in Sources */ = {isa = PBXBuildFile; fileRef = 843CC7EF2C1E757D00BBC475 /* NativeTouchPointer.m */; };
843CC7F62C1E7A1700BBC475 /* NativeTouchHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 843CC7F52C1E7A1700BBC475 /* NativeTouchHandler.m */; };
@@ -34,11 +69,11 @@
8454AC662C31907F00F0A302 /* iPad.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8454AC682C31907F00F0A302 /* iPad.storyboard */; };
8454AC6A2C319BDA00F0A302 /* iPhone.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8454AC6C2C319BDA00F0A302 /* iPhone.storyboard */; };
84591A512BE1625E006AAB10 /* CustomEdgeSlideGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 84591A502BE1625E006AAB10 /* CustomEdgeSlideGestureRecognizer.m */; };
847D2FDB2DDA015F00937A62 /* MenuSectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 847D2FDA2DDA015F00937A62 /* MenuSectionView.m */; };
848F456A2DDCD12500581E36 /* HostCardView.m in Sources */ = {isa = PBXBuildFile; fileRef = 848F45692DDCD12500581E36 /* HostCardView.m */; };
847D2FDB2DDA015F00937A62 /* MenuSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847D2FDA2DDA015F00937A62 /* MenuSectionView.swift */; };
848F456A2DDCD12500581E36 /* HostCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848F45692DDCD12500581E36 /* HostCardView.swift */; };
84E1CDEC2C4F67D300547904 /* CommandManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E1CDEB2C4F67D300547904 /* CommandManager.swift */; };
84E1CDEF2C4F688E00547904 /* ToolboxViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E1CDEE2C4F688E00547904 /* ToolboxViewController.swift */; };
84E1CDFA2C4FE83400547904 /* SwiftLocalizationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E1CDF92C4FE83400547904 /* SwiftLocalizationHelper.swift */; };
84E1CDFA2C4FE83400547904 /* LocalizationHelperExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E1CDF92C4FE83400547904 /* LocalizationHelperExtension.swift */; };
84FD0B512C31338D006B5BE5 /* LocalizationHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 84FD0B502C31338D006B5BE5 /* LocalizationHelper.m */; };
84FD0B542C3134A6006B5BE5 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 84FD0B532C3134A6006B5BE5 /* Localizable.xcstrings */; };
9777AAF32E2CF7A400D7FBAF /* Shaders-Linear.metal in Sources */ = {isa = PBXBuildFile; fileRef = 9777AAF22E2CF7A400D7FBAF /* Shaders-Linear.metal */; };
@@ -76,6 +111,10 @@
98CFB82F1CAD481B0048EF74 /* libmoonlight-common.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 98AB2E841CAD46840089BB98 /* libmoonlight-common.a */; };
98D5856D1C0EA79600F6CC00 /* TemporaryHost.m in Sources */ = {isa = PBXBuildFile; fileRef = 98D5856C1C0EA79600F6CC00 /* TemporaryHost.m */; };
98D585701C0ED0E800F6CC00 /* TemporarySettings.m in Sources */ = {isa = PBXBuildFile; fileRef = 98D5856F1C0ED0E800F6CC00 /* TemporarySettings.m */; };
A1900E012F9A100100BA853A /* InputAccessoryBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1900E002F9A100100BA853A /* InputAccessoryBar.swift */; };
A1B2C3D42F90000100ABCDEF /* AbstractGamepadOverlayWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D32F90000100ABCDEF /* AbstractGamepadOverlayWidgetView.swift */; };
A1C0DE012FBC000100000001 /* ControllerNavigator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C0DE002FBC000100000001 /* ControllerNavigator.swift */; };
A1C0DE032FBC000100000001 /* RadialMenuOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C0DE022FBC000100000001 /* RadialMenuOverlayView.swift */; };
DC1F5A07206436B20037755F /* ConnectionHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = DC1F5A06206436B20037755F /* ConnectionHelper.m */; };
F74BEF9C2C1A705600224667 /* OpenSSL in Frameworks */ = {isa = PBXBuildFile; productRef = F74BEF9B2C1A705600224667 /* OpenSSL */; };
FB1A674D2131E65900507771 /* KeyboardSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = FB1A674C2131E65900507771 /* KeyboardSupport.m */; };
@@ -107,6 +146,7 @@
FB89463019F646E200339C8A /* StreamConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89461919F646E200339C8A /* StreamConfiguration.m */; };
FB89463119F646E200339C8A /* StreamManager.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89461B19F646E200339C8A /* StreamManager.m */; };
FB89463219F646E200339C8A /* VideoDecoderRenderer.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89461D19F646E200339C8A /* VideoDecoderRenderer.m */; };
A1F100012FFB000100000001 /* FrameInterpolator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F100002FFB000100000001 /* FrameInterpolator.swift */; };
FB89463419F646E200339C8A /* Utils.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89462219F646E200339C8A /* Utils.m */; };
FB89463519F646E200339C8A /* MainFrameViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89462519F646E200339C8A /* MainFrameViewController.m */; };
FB89463619F646E200339C8A /* StreamFrameViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89462719F646E200339C8A /* StreamFrameViewController.m */; };
@@ -123,7 +163,6 @@
FBD3495019FF2174002D2A60 /* SettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FBD3494F19FF2174002D2A60 /* SettingsViewController.m */; };
FBD3495319FF36FB002D2A60 /* SWRevealViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FBD3495219FF36FB002D2A60 /* SWRevealViewController.m */; };
FBD349621A0089F6002D2A60 /* DataManager.m in Sources */ = {isa = PBXBuildFile; fileRef = FBD349611A0089F6002D2A60 /* DataManager.m */; };
FBDE86E619F82297001C18A8 /* UIAppView.m in Sources */ = {isa = PBXBuildFile; fileRef = FBDE86E519F82297001C18A8 /* UIAppView.m */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -151,41 +190,59 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
A1D500012FFC000100000001 /* authored_haptics_engine.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = authored_haptics_engine.cpp; path = "third_party/moonlight-audio-haptics/src/core/authored_haptics_engine.cpp"; sourceTree = SOURCE_ROOT; };
180345E02E819877004AF36F /* libopus-sim.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libopus-sim.a"; sourceTree = "<group>"; };
180E6E362E93BFDB0067C73A /* AlertControllerUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertControllerUtil.swift; sourceTree = "<group>"; };
1817B61D2E6FD04D000D938D /* SafeTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafeTimer.swift; sourceTree = "<group>"; };
1829CE3D2E2154DF0074A99A /* SceneDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SceneDelegate.h; sourceTree = "<group>"; };
1829CE3E2E2154DF0074A99A /* SceneDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SceneDelegate.m; sourceTree = "<group>"; };
18341A022DEB65AB0094E5F2 /* FixedTintImageView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FixedTintImageView.h; sourceTree = "<group>"; };
18341A032DEB662C0094E5F2 /* FixedTintImageView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FixedTintImageView.m; sourceTree = "<group>"; };
18416EA32EBB3DCE00AE0124 /* UITouchUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITouchUtil.swift; sourceTree = "<group>"; };
1850C8292E667E4000845308 /* MicHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MicHandler.swift; sourceTree = "<group>"; };
18729EDD2E8BD68800101194 /* UUIDHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UUIDHelper.swift; sourceTree = "<group>"; };
18780CC62F10D66000B90483 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
A76B00022FFCC00100000002 /* GameSirG8MFiRumbler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameSirG8MFiRumbler.swift; sourceTree = "<group>"; };
A76B00042FFCC00100000004 /* ExternalAccessory.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ExternalAccessory.framework; path = System/Library/Frameworks/ExternalAccessory.framework; sourceTree = SDKROOT; };
18780CC92F10D6CC00B90483 /* IAPManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IAPManager.swift; sourceTree = "<group>"; };
187F4B232E04633100EECC59 /* WidgetPanelStackView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WidgetPanelStackView.h; sourceTree = "<group>"; };
187F4B272E04658A00EECC59 /* WidgetPanelStackView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = WidgetPanelStackView.m; sourceTree = "<group>"; };
1888C1902E24C2C800108730 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
1888C1912E24C33100108730 /* AboutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = "<group>"; };
188973D22E386A670025E48C /* widgetTemplatesIPhone.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; name = widgetTemplatesIPhone.bin; path = PresetConfigs/widgetTemplatesIPhone.bin; sourceTree = "<group>"; };
189AD2962DE743B700E24B88 /* HostCollectionViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HostCollectionViewController.h; sourceTree = "<group>"; };
189AD2992DE7446000E24B88 /* HostCollectionViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HostCollectionViewController.m; sourceTree = "<group>"; };
18951D1C2EBB6D2300A86E12 /* TouchPadGestureHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchPadGestureHandler.swift; sourceTree = "<group>"; };
189AD2992DE7446000E24B88 /* HostCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostCollectionViewController.swift; sourceTree = "<group>"; };
18A0EE2B2EFBCCFD00BA853A /* GraphicUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphicUtils.swift; sourceTree = "<group>"; };
18A765BC2ECD51FB004784AA /* ControllerUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControllerUtil.swift; sourceTree = "<group>"; };
18B317022EE67A39009B62C2 /* InertialScroller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InertialScroller.swift; sourceTree = "<group>"; };
18BF141D2E0D7A00009DEE4F /* VoidLink v1.0.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "VoidLink v1.0.xcdatamodel"; sourceTree = "<group>"; };
18D43D7E2F050240008C326F /* PressureCurve.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PressureCurve.swift; sourceTree = "<group>"; };
18D43D802F050240008C326F /* PencilTipOffsetCalibrationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PencilTipOffsetCalibrationViewController.swift; sourceTree = "<group>"; };
18E4BCE92E891895000A8657 /* MotionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MotionHandler.swift; sourceTree = "<group>"; };
18FCADAE2ECAC23200787A96 /* PencilHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PencilHandler.swift; sourceTree = "<group>"; };
2A039E562F72892B00546768 /* VoidLinkPreview.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VoidLinkPreview.app; sourceTree = BUILT_PRODUCTS_DIR; };
2A263D492FF3B3F800154DEA /* PublicExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicExtensions.swift; sourceTree = "<group>"; };
2A263D4E2FF3BA2800154DEA /* PublicUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicUtils.swift; sourceTree = "<group>"; };
2A7113FC2F5A6C65001D852D /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = "<group>"; };
2A7113FE2F5A6C65001D852D /* UIAppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIAppView.swift; sourceTree = "<group>"; };
2A7114002F5A6C65001D852D /* GamepadNavigationIllustrationHud.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GamepadNavigationIllustrationHud.swift; sourceTree = "<group>"; };
2AC1E3822F507D80004F76C0 /* GenericUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericUtils.swift; sourceTree = "<group>"; };
2AC1EAFA2F922602004EDA80 /* UIStackExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIStackExtension.swift; sourceTree = "<group>"; };
2B7A10012FFB000100000001 /* SettingsTouchVelocityPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTouchVelocityPreview.swift; sourceTree = "<group>"; };
3A4A3A102F0C000100000001 /* ProfileSelectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSelectorViewController.swift; sourceTree = "<group>"; };
3A4A3A122F0C000100000002 /* LayoutOnScreenControlsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutOnScreenControlsViewController.swift; sourceTree = "<group>"; };
693B3A9A218638CD00982F7B /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; };
840751B12BF49B5A0096B657 /* CustomTapGestureRecognizer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CustomTapGestureRecognizer.h; sourceTree = "<group>"; };
840751B22BF49C050096B657 /* CustomTapGestureRecognizer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CustomTapGestureRecognizer.m; sourceTree = "<group>"; };
841BAC8F2DE35BEC00DA639E /* ThemeManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ThemeManager.h; sourceTree = "<group>"; };
841BAC912DE35D6B00DA639E /* ThemeManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ThemeManager.m; sourceTree = "<group>"; };
84203F222C5F24C00032EEED /* OnScreenWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnScreenWidgetView.swift; sourceTree = "<group>"; };
84351B062C29ACCA00A77989 /* OnScreenButtonState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OnScreenButtonState.h; sourceTree = "<group>"; };
84351B072C29ACCA00A77989 /* LayoutOnScreenControls.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LayoutOnScreenControls.m; sourceTree = "<group>"; };
84351B082C29ACCA00A77989 /* OSCProfilesManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OSCProfilesManager.h; sourceTree = "<group>"; };
84351B092C29ACCA00A77989 /* OnScreenButtonState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OnScreenButtonState.m; sourceTree = "<group>"; };
84351B0A2C29ACCA00A77989 /* OSCProfile.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OSCProfile.m; sourceTree = "<group>"; };
84351B0B2C29ACCB00A77989 /* OSCProfilesManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OSCProfilesManager.m; sourceTree = "<group>"; };
84351B0C2C29ACCB00A77989 /* LayoutOnScreenControls.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LayoutOnScreenControls.h; sourceTree = "<group>"; };
84351B0D2C29ACCB00A77989 /* OSCProfile.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OSCProfile.h; sourceTree = "<group>"; };
84351B252C29AD6000A77989 /* LayoutOnScreenControlsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LayoutOnScreenControlsViewController.m; sourceTree = "<group>"; };
84351B262C29AD6000A77989 /* OSCProfilesTableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OSCProfilesTableViewController.h; sourceTree = "<group>"; };
84351B272C29AD6000A77989 /* OSCProfilesTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OSCProfilesTableViewController.m; sourceTree = "<group>"; };
84351B282C29AD6000A77989 /* LayoutOnScreenControlsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LayoutOnScreenControlsViewController.h; sourceTree = "<group>"; };
84351B292C29AD6000A77989 /* ToolBarContainerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ToolBarContainerView.m; sourceTree = "<group>"; };
84351B2A2C29AD6000A77989 /* ToolBarContainerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ToolBarContainerView.h; sourceTree = "<group>"; };
84351B592C29ADBE00A77989 /* ProfileTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ProfileTableViewCell.m; sourceTree = "<group>"; };
84351B5A2C29ADBE00A77989 /* ProfileTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ProfileTableViewCell.h; sourceTree = "<group>"; };
84351B5B2C29ADBE00A77989 /* ProfileTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ProfileTableViewCell.xib; sourceTree = "<group>"; };
843667EF2D9D7173004A7D7F /* widgetTemplates.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; name = widgetTemplates.bin; path = PresetConfigs/widgetTemplates.bin; sourceTree = "<group>"; };
843CC7EE2C1E757D00BBC475 /* NativeTouchPointer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NativeTouchPointer.h; sourceTree = "<group>"; };
843CC7EF2C1E757D00BBC475 /* NativeTouchPointer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NativeTouchPointer.m; sourceTree = "<group>"; };
@@ -200,14 +257,12 @@
8454AC6D2C319BDA00F0A302 /* mul */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = mul; path = mul.lproj/iPhone.xcstrings; sourceTree = "<group>"; };
84591A4F2BE16240006AAB10 /* CustomEdgeSlideGestureRecognizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CustomEdgeSlideGestureRecognizer.h; sourceTree = "<group>"; };
84591A502BE1625E006AAB10 /* CustomEdgeSlideGestureRecognizer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CustomEdgeSlideGestureRecognizer.m; sourceTree = "<group>"; };
847D2FD82DDA00C800937A62 /* MenuSectionView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MenuSectionView.h; sourceTree = "<group>"; };
847D2FDA2DDA015F00937A62 /* MenuSectionView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MenuSectionView.m; sourceTree = "<group>"; };
848F45682DDCD0D600581E36 /* HostCardView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HostCardView.h; sourceTree = "<group>"; };
848F45692DDCD12500581E36 /* HostCardView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HostCardView.m; sourceTree = "<group>"; };
847D2FDA2DDA015F00937A62 /* MenuSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuSectionView.swift; sourceTree = "<group>"; };
848F45692DDCD12500581E36 /* HostCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostCardView.swift; sourceTree = "<group>"; };
84D8EB932C5146AD00FC10A9 /* VoidLink-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "VoidLink-Bridging-Header.h"; sourceTree = "<group>"; };
84E1CDEB2C4F67D300547904 /* CommandManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandManager.swift; sourceTree = "<group>"; };
84E1CDEE2C4F688E00547904 /* ToolboxViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolboxViewController.swift; sourceTree = "<group>"; };
84E1CDF92C4FE83400547904 /* SwiftLocalizationHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftLocalizationHelper.swift; sourceTree = "<group>"; };
84E1CDF92C4FE83400547904 /* LocalizationHelperExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationHelperExtension.swift; sourceTree = "<group>"; };
84FD0B4F2C313343006B5BE5 /* LocalizationHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LocalizationHelper.h; sourceTree = "<group>"; };
84FD0B502C31338D006B5BE5 /* LocalizationHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LocalizationHelper.m; sourceTree = "<group>"; };
84FD0B532C3134A6006B5BE5 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
@@ -280,6 +335,11 @@
98D5856C1C0EA79600F6CC00 /* TemporaryHost.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TemporaryHost.m; path = Database/TemporaryHost.m; sourceTree = "<group>"; };
98D5856E1C0ED0E800F6CC00 /* TemporarySettings.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TemporarySettings.h; path = Database/TemporarySettings.h; sourceTree = "<group>"; };
98D5856F1C0ED0E800F6CC00 /* TemporarySettings.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TemporarySettings.m; path = Database/TemporarySettings.m; sourceTree = "<group>"; };
A1900E002F9A100100BA853A /* InputAccessoryBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputAccessoryBar.swift; sourceTree = "<group>"; };
A1B2C3D32F90000100ABCDEF /* AbstractGamepadOverlayWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbstractGamepadOverlayWidgetView.swift; sourceTree = "<group>"; };
A1C0DE002FBC000100000001 /* ControllerNavigator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControllerNavigator.swift; sourceTree = "<group>"; };
A1C0DE022FBC000100000001 /* RadialMenuOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadialMenuOverlayView.swift; sourceTree = "<group>"; };
A1D4E0A22FBBBBCC00112233 /* OSCProfilesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSCProfilesManager.swift; sourceTree = "<group>"; };
DC1F5A05206436B10037755F /* ConnectionHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ConnectionHelper.h; sourceTree = "<group>"; };
DC1F5A06206436B20037755F /* ConnectionHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ConnectionHelper.m; sourceTree = "<group>"; };
FB1A674B2131E65900507771 /* KeyboardSupport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyboardSupport.h; sourceTree = "<group>"; };
@@ -340,6 +400,7 @@
FB89461B19F646E200339C8A /* StreamManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = StreamManager.m; sourceTree = "<group>"; };
FB89461C19F646E200339C8A /* VideoDecoderRenderer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VideoDecoderRenderer.h; sourceTree = "<group>"; };
FB89461D19F646E200339C8A /* VideoDecoderRenderer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VideoDecoderRenderer.m; sourceTree = "<group>"; };
A1F100002FFB000100000001 /* FrameInterpolator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FrameInterpolator.swift; sourceTree = "<group>"; };
FB89462119F646E200339C8A /* Utils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Utils.h; sourceTree = "<group>"; };
FB89462219F646E200339C8A /* Utils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Utils.m; sourceTree = "<group>"; };
FB89462419F646E200339C8A /* MainFrameViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MainFrameViewController.h; sourceTree = "<group>"; };
@@ -374,27 +435,64 @@
FBD3495219FF36FB002D2A60 /* SWRevealViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SWRevealViewController.m; sourceTree = "<group>"; };
FBD349601A0089F6002D2A60 /* DataManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DataManager.h; path = Database/DataManager.h; sourceTree = "<group>"; };
FBD349611A0089F6002D2A60 /* DataManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DataManager.m; path = Database/DataManager.m; sourceTree = "<group>"; };
FBDE86E419F82297001C18A8 /* UIAppView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIAppView.h; sourceTree = "<group>"; };
FBDE86E519F82297001C18A8 /* UIAppView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIAppView.m; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
2A93D5662F7D016C00C2207F /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
WidgetPickerViewController.swift,
);
target = 2A039E552F72892B00546768 /* VoidLinkPreview */;
};
2AAA4E862F76BDC6001E20DC /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
AbstractGamepadView.swift,
RadialMenuView.swift,
VirtualKeyboardView.swift,
WidgetPickerView.swift,
WidgetPickerViewController.swift,
);
target = FB290CED19B2C406004C83CF /* VoidLink */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
18A0EE282EFBCBFB00BA853A /* SVGs */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = SVGs; sourceTree = "<group>"; };
2A039E572F72892B00546768 /* VoidLinkPreview */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (2AAA4E862F76BDC6001E20DC /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 2A93D5662F7D016C00C2207F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = VoidLinkPreview; sourceTree = "<group>"; };
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
2A039E532F72892B00546768 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
FB290CEB19B2C406004C83CF /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
A76B00052FFCC00100000005 /* ExternalAccessory.framework in Frameworks */,
18A0EE272EFB9B5A00BA853A /* SVGKitSwift in Frameworks */,
18A0EE252EFB9B5A00BA853A /* SVGKit in Frameworks */,
9890CF6B203B7EE1006C4B06 /* libxml2.tbd in Frameworks */,
98CFB82F1CAD481B0048EF74 /* libmoonlight-common.a in Frameworks */,
FB8946ED19F6AFE800339C8A /* libopus.a in Frameworks */,
98882A082AF60F7400C5A11C /* libavutil.a in Frameworks */,
182760F72E83E2EA00EC1C5C /* DequeModule in Frameworks */,
182760FD2E83E68B00EC1C5C /* Collections in Frameworks */,
FB290CF419B2C406004C83CF /* CoreGraphics.framework in Frameworks */,
FB290CF819B2C406004C83CF /* CoreData.framework in Frameworks */,
98181BED2791281100E43572 /* CoreMotion.framework in Frameworks */,
F74BEF9C2C1A705600224667 /* OpenSSL in Frameworks */,
98882A072AF60F7200C5A11C /* libavformat.a in Frameworks */,
FB290CF619B2C406004C83CF /* UIKit.framework in Frameworks */,
18780CC72F10D66000B90483 /* StoreKit.framework in Frameworks */,
FB290CF219B2C406004C83CF /* Foundation.framework in Frameworks */,
FB4679011A57048000377732 /* CoreFoundation.framework in Frameworks */,
182760FA2E83E57D00EC1C5C /* DequeModule in Frameworks */,
98181BE92791277400E43572 /* libSDL2.a in Frameworks */,
98882A062AF60F7000C5A11C /* libavcodec.a in Frameworks */,
);
@@ -412,8 +510,7 @@
84351B092C29ACCA00A77989 /* OnScreenButtonState.m */,
84351B0D2C29ACCB00A77989 /* OSCProfile.h */,
84351B0A2C29ACCA00A77989 /* OSCProfile.m */,
84351B082C29ACCA00A77989 /* OSCProfilesManager.h */,
84351B0B2C29ACCB00A77989 /* OSCProfilesManager.m */,
A1D4E0A22FBBBBCC00112233 /* OSCProfilesManager.swift */,
);
path = CustomOSC;
sourceTree = "<group>";
@@ -421,27 +518,22 @@
84351B242C29AD2E00A77989 /* CustomOSCViewControl */ = {
isa = PBXGroup;
children = (
84351B2E2C29AD6600A77989 /* Cells */,
84351B282C29AD6000A77989 /* LayoutOnScreenControlsViewController.h */,
84351B252C29AD6000A77989 /* LayoutOnScreenControlsViewController.m */,
84351B2E2C29AD6600A77989 /* Table */,
3A4A3A122F0C000100000002 /* LayoutOnScreenControlsViewController.swift */,
187F4B232E04633100EECC59 /* WidgetPanelStackView.h */,
187F4B272E04658A00EECC59 /* WidgetPanelStackView.m */,
84351B262C29AD6000A77989 /* OSCProfilesTableViewController.h */,
84351B272C29AD6000A77989 /* OSCProfilesTableViewController.m */,
84351B2A2C29AD6000A77989 /* ToolBarContainerView.h */,
84351B292C29AD6000A77989 /* ToolBarContainerView.m */,
);
path = CustomOSCViewControl;
sourceTree = "<group>";
};
84351B2E2C29AD6600A77989 /* Cells */ = {
84351B2E2C29AD6600A77989 /* Table */ = {
isa = PBXGroup;
children = (
84351B5A2C29ADBE00A77989 /* ProfileTableViewCell.h */,
84351B592C29ADBE00A77989 /* ProfileTableViewCell.m */,
84351B5B2C29ADBE00A77989 /* ProfileTableViewCell.xib */,
3A4A3A102F0C000100000001 /* ProfileSelectorViewController.swift */,
);
path = Cells;
path = Table;
sourceTree = "<group>";
};
843667ED2D9D70A3004A7D7F /* PresetConfigs */ = {
@@ -458,8 +550,8 @@
children = (
84FD0B4F2C313343006B5BE5 /* LocalizationHelper.h */,
84FD0B502C31338D006B5BE5 /* LocalizationHelper.m */,
84E1CDF92C4FE83400547904 /* LocalizationHelperExtension.swift */,
84FD0B532C3134A6006B5BE5 /* Localizable.xcstrings */,
84E1CDF92C4FE83400547904 /* SwiftLocalizationHelper.swift */,
);
path = Localization;
sourceTree = "<group>";
@@ -543,6 +635,7 @@
98AB2E7F1CAD46830089BB98 /* moonlight-common.xcodeproj */,
FB290CF919B2C406004C83CF /* VoidLink */,
FB1A67542132419700507771 /* Moonlight TV */,
2A039E572F72892B00546768 /* VoidLinkPreview */,
FB290CF019B2C406004C83CF /* Frameworks */,
FB290CEF19B2C406004C83CF /* Products */,
);
@@ -552,6 +645,7 @@
isa = PBXGroup;
children = (
FB290CEE19B2C406004C83CF /* VoidLink.app */,
2A039E562F72892B00546768 /* VoidLinkPreview.app */,
);
name = Products;
sourceTree = "<group>";
@@ -559,6 +653,8 @@
FB290CF019B2C406004C83CF /* Frameworks */ = {
isa = PBXGroup;
children = (
A76B00042FFCC00100000004 /* ExternalAccessory.framework */,
18780CC62F10D66000B90483 /* StoreKit.framework */,
98882A032AF60F5300C5A11C /* libavcodec.a */,
98882A052AF60F5300C5A11C /* libavformat.a */,
98882A0A2AF60F9000C5A11C /* libavcodec.a */,
@@ -610,18 +706,17 @@
18341A022DEB65AB0094E5F2 /* FixedTintImageView.h */,
18341A032DEB662C0094E5F2 /* FixedTintImageView.m */,
FB290D0319B2C406004C83CF /* AppDelegate.m */,
848F45682DDCD0D600581E36 /* HostCardView.h */,
848F45692DDCD12500581E36 /* HostCardView.m */,
841BAC8F2DE35BEC00DA639E /* ThemeManager.h */,
841BAC912DE35D6B00DA639E /* ThemeManager.m */,
847D2FD82DDA00C800937A62 /* MenuSectionView.h */,
847D2FDA2DDA015F00937A62 /* MenuSectionView.m */,
FBDE86E419F82297001C18A8 /* UIAppView.h */,
FBDE86E519F82297001C18A8 /* UIAppView.m */,
848F45692DDCD12500581E36 /* HostCardView.swift */,
2A7113FC2F5A6C65001D852D /* ThemeManager.swift */,
847D2FDA2DDA015F00937A62 /* MenuSectionView.swift */,
2A7113FE2F5A6C65001D852D /* UIAppView.swift */,
2A7114002F5A6C65001D852D /* GamepadNavigationIllustrationHud.swift */,
180E6E362E93BFDB0067C73A /* AlertControllerUtil.swift */,
1888C1902E24C2C800108730 /* AboutView.swift */,
1888C1912E24C33100108730 /* AboutViewController.swift */,
FB89463719F6473800339C8A /* Launch Screen.xib */,
FB290D0819B2C406004C83CF /* Images.xcassets */,
18A0EE282EFBCBFB00BA853A /* SVGs */,
FB290D0519B2C406004C83CF /* Limelight.xcdatamodeld */,
FB1D59981BBCCD7E00F482CA /* AppCollectionView.h */,
FB1D59991BBCCD7E00F482CA /* AppCollectionView.m */,
@@ -661,6 +756,7 @@
isa = PBXGroup;
children = (
84351B052C29ACAF00A77989 /* CustomOSC */,
A76B00022FFCC00100000002 /* GameSirG8MFiRumbler.swift */,
FB89460A19F646E200339C8A /* ControllerSupport.h */,
FB89460B19F646E200339C8A /* ControllerSupport.m */,
FB89460C19F646E200339C8A /* StreamView.h */,
@@ -677,18 +773,32 @@
9827E7A7251436EA00F25707 /* HapticContext.h */,
840751B12BF49B5A0096B657 /* CustomTapGestureRecognizer.h */,
840751B22BF49C050096B657 /* CustomTapGestureRecognizer.m */,
9819CC12254F107A008A7C8E /* RelativeTouchHandler.h */,
843CC7F42C1E778000BBC475 /* NativeTouchHandler.h */,
844CD0E82C7F847E004861A7 /* PureNativeTouchHandler.h */,
844CD0EA2C7F8500004861A7 /* PureNativeTouchHandler.m */,
843CC7F42C1E778000BBC475 /* NativeTouchHandler.h */,
843CC7F52C1E7A1700BBC475 /* NativeTouchHandler.m */,
9819CC12254F107A008A7C8E /* RelativeTouchHandler.h */,
9819CC13254F107A008A7C8E /* RelativeTouchHandler.m */,
9819CC20254F180F008A7C8E /* AbsoluteTouchHandler.h */,
9819CC21254F180F008A7C8E /* AbsoluteTouchHandler.m */,
988FCD3F293B091B003050E2 /* KeyboardInputField.h */,
988FCD40293B091B003050E2 /* KeyboardInputField.m */,
18D43D7E2F050240008C326F /* PressureCurve.swift */,
18D43D802F050240008C326F /* PencilTipOffsetCalibrationViewController.swift */,
18FCADAE2ECAC23200787A96 /* PencilHandler.swift */,
18416EA32EBB3DCE00AE0124 /* UITouchUtil.swift */,
18B317022EE67A39009B62C2 /* InertialScroller.swift */,
18951D1C2EBB6D2300A86E12 /* TouchPadGestureHandler.swift */,
18A765BC2ECD51FB004784AA /* ControllerUtil.swift */,
A1C0DE002FBC000100000001 /* ControllerNavigator.swift */,
A1C0DE022FBC000100000001 /* RadialMenuOverlayView.swift */,
1850C8292E667E4000845308 /* MicHandler.swift */,
18E4BCE92E891895000A8657 /* MotionHandler.swift */,
84E1CDEB2C4F67D300547904 /* CommandManager.swift */,
84203F222C5F24C00032EEED /* OnScreenWidgetView.swift */,
A1900E002F9A100100BA853A /* InputAccessoryBar.swift */,
A1B2C3D32F90000100ABCDEF /* AbstractGamepadOverlayWidgetView.swift */,
1817B61D2E6FD04D000D938D /* SafeTimer.swift */,
);
path = Input;
sourceTree = "<group>";
@@ -720,6 +830,7 @@
FB89461B19F646E200339C8A /* StreamManager.m */,
FB89461C19F646E200339C8A /* VideoDecoderRenderer.h */,
FB89461D19F646E200339C8A /* VideoDecoderRenderer.m */,
A1F100002FFB000100000001 /* FrameInterpolator.swift */,
9803CCAB254F9EAF00EE185E /* ConnectionCallbacks.h */,
);
path = Stream;
@@ -728,6 +839,12 @@
FB89461E19F646E200339C8A /* Utility */ = {
isa = PBXGroup;
children = (
18729EDD2E8BD68800101194 /* UUIDHelper.swift */,
2AC1E3822F507D80004F76C0 /* GenericUtils.swift */,
2A263D4E2FF3BA2800154DEA /* PublicUtils.swift */,
2A263D492FF3B3F800154DEA /* PublicExtensions.swift */,
18A0EE2B2EFBCCFD00BA853A /* GraphicUtils.swift */,
18780CC92F10D6CC00B90483 /* IAPManager.swift */,
97E20DD42E2164A9000D5DCA /* BandwidthTracker.h */,
97E20DD52E2164A9000D5DCA /* BandwidthTracker.m */,
97E20DD62E2164A9000D5DCA /* FloatBuffer.h */,
@@ -757,12 +874,13 @@
FBD3495219FF36FB002D2A60 /* SWRevealViewController.m */,
FB89462419F646E200339C8A /* MainFrameViewController.h */,
FB89462519F646E200339C8A /* MainFrameViewController.m */,
189AD2962DE743B700E24B88 /* HostCollectionViewController.h */,
189AD2992DE7446000E24B88 /* HostCollectionViewController.m */,
189AD2992DE7446000E24B88 /* HostCollectionViewController.swift */,
FB89462619F646E200339C8A /* StreamFrameViewController.h */,
FB89462719F646E200339C8A /* StreamFrameViewController.m */,
FBD3494E19FF2174002D2A60 /* SettingsViewController.h */,
FBD3494F19FF2174002D2A60 /* SettingsViewController.m */,
2B7A10012FFB000100000001 /* SettingsTouchVelocityPreview.swift */,
2AC1EAFA2F922602004EDA80 /* UIStackExtension.swift */,
FB4A23B61A9D3637004D2EF2 /* LoadingFrameViewController.h */,
FB4A23B71A9D3637004D2EF2 /* LoadingFrameViewController.m */,
84E1CDEE2C4F688E00547904 /* ToolboxViewController.swift */,
@@ -809,6 +927,7 @@
FB8946E919F6AFB800339C8A /* lib */ = {
isa = PBXGroup;
children = (
180345E02E819877004AF36F /* libopus-sim.a */,
FB8946EA19F6AFB800339C8A /* libopus.a */,
);
path = lib;
@@ -875,6 +994,28 @@
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
2A039E552F72892B00546768 /* VoidLinkPreview */ = {
isa = PBXNativeTarget;
buildConfigurationList = 2A039E5E2F72892C00546768 /* Build configuration list for PBXNativeTarget "VoidLinkPreview" */;
buildPhases = (
2A039E522F72892B00546768 /* Sources */,
2A039E532F72892B00546768 /* Frameworks */,
2A039E542F72892B00546768 /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
2A039E572F72892B00546768 /* VoidLinkPreview */,
);
name = VoidLinkPreview;
packageProductDependencies = (
);
productName = VoidLinkSwiftUI;
productReference = 2A039E562F72892B00546768 /* VoidLinkPreview.app */;
productType = "com.apple.product-type.application";
};
FB290CED19B2C406004C83CF /* VoidLink */ = {
isa = PBXNativeTarget;
buildConfigurationList = FB290D2019B2C406004C83CF /* Build configuration list for PBXNativeTarget "VoidLink" */;
@@ -888,6 +1029,9 @@
dependencies = (
98AB2E861CAD468B0089BB98 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
18A0EE282EFBCBFB00BA853A /* SVGs */,
);
name = VoidLink;
productName = Limelight;
productReference = FB290CEE19B2C406004C83CF /* VoidLink.app */;
@@ -899,10 +1043,13 @@
FB290CE619B2C406004C83CF /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0730;
LastSwiftUpdateCheck = 2620;
LastUpgradeCheck = 1400;
ORGANIZATIONNAME = "Moonlight Game Streaming Project";
TargetAttributes = {
2A039E552F72892B00546768 = {
CreatedOnToolsVersion = 26.2;
};
FB290CED19B2C406004C83CF = {
LastSwiftMigration = 1140;
};
@@ -922,6 +1069,8 @@
mainGroup = FB290CE519B2C406004C83CF;
packageReferences = (
F74BEF9A2C1A705600224667 /* XCRemoteSwiftPackageReference "OpenSSL-Package" */,
182760F52E83E2EA00EC1C5C /* XCRemoteSwiftPackageReference "swift-collections" */,
18A0EE232EFB9B5A00BA853A /* XCRemoteSwiftPackageReference "SVGKit" */,
);
productRefGroup = FB290CEF19B2C406004C83CF /* Products */;
projectDirPath = "";
@@ -934,6 +1083,7 @@
projectRoot = "";
targets = (
FB290CED19B2C406004C83CF /* VoidLink */,
2A039E552F72892B00546768 /* VoidLinkPreview */,
);
};
/* End PBXProject section */
@@ -956,6 +1106,13 @@
/* End PBXReferenceProxy section */
/* Begin PBXResourcesBuildPhase section */
2A039E542F72892B00546768 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
FB290CEC19B2C406004C83CF /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@@ -968,60 +1125,90 @@
8454AC6A2C319BDA00F0A302 /* iPhone.storyboard in Resources */,
843667F02D9D7173004A7D7F /* widgetTemplates.bin in Resources */,
84FD0B542C3134A6006B5BE5 /* Localizable.xcstrings in Resources */,
84351B5D2C29ADBE00A77989 /* ProfileTableViewCell.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
2A039E522F72892B00546768 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
2AAA4E842F7666EE001E20DC /* LocalizationHelperExtension.swift in Sources */,
2AAA4E872F985F12001E20DC /* LocalizationHelper.m in Sources */,
2AAA4E822F7666C5001E20DC /* ControllerUtil.swift in Sources */,
2A263D4C2FF3B49600154DEA /* PublicExtensions.swift in Sources */,
2AC1EAFB2F92260F004EDA80 /* UIStackExtension.swift in Sources */,
2A263D502FF3BA2800154DEA /* PublicUtils.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
FB290CEA19B2C406004C83CF /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
A1D500022FFC000100000001 /* authored_haptics_engine.cpp in Sources */,
FB290D0719B2C406004C83CF /* Limelight.xcdatamodeld in Sources */,
9819CC22254F180F008A7C8E /* AbsoluteTouchHandler.m in Sources */,
9819CC14254F107A008A7C8E /* RelativeTouchHandler.m in Sources */,
FB1A674D2131E65900507771 /* KeyboardSupport.m in Sources */,
97E20DDF2E2164A9000D5DCA /* Frame.m in Sources */,
97E20DE02E2164A9000D5DCA /* FrameQueue.m in Sources */,
18951D1D2EBB6D2300A86E12 /* TouchPadGestureHandler.swift in Sources */,
97E20DE12E2164A9000D5DCA /* FloatBuffer.m in Sources */,
18FCADAF2ECAC23200787A96 /* PencilHandler.swift in Sources */,
97E20DE22E2164A9000D5DCA /* PaddedLabel.m in Sources */,
97E20DE32E2164A9000D5DCA /* BandwidthTracker.m in Sources */,
84591A512BE1625E006AAB10 /* CustomEdgeSlideGestureRecognizer.m in Sources */,
843CC7F62C1E7A1700BBC475 /* NativeTouchHandler.m in Sources */,
97E20DEE2E2165A5000D5DCA /* ImGuiPlots.mm in Sources */,
FB89463219F646E200339C8A /* VideoDecoderRenderer.m in Sources */,
A1F100012FFB000100000001 /* FrameInterpolator.swift in Sources */,
2A263D4F2FF3BA2800154DEA /* PublicUtils.swift in Sources */,
FB290D0419B2C406004C83CF /* AppDelegate.m in Sources */,
FB9AFD401A7E127D00872C98 /* AppListResponse.m in Sources */,
FB89463419F646E200339C8A /* Utils.m in Sources */,
FBDE86E619F82297001C18A8 /* UIAppView.m in Sources */,
2A7113FF2F5A6C65001D852D /* UIAppView.swift in Sources */,
2A7114012F5A6C65001D852D /* GamepadNavigationIllustrationHud.swift in Sources */,
FB89462F19F646E200339C8A /* Connection.m in Sources */,
FB9AFD321A7D867C00872C98 /* AppAssetRetriever.m in Sources */,
843E93BE2C41528700B1651F /* OrientationHelper.swift in Sources */,
FB4678FF1A565DAC00377732 /* WakeOnLanManager.m in Sources */,
97E21BBF2E21882D000D5DCA /* imgui_tables.cpp in Sources */,
97E21BC02E21882D000D5DCA /* imgui_demo.cpp in Sources */,
2A263D4A2FF3B3F800154DEA /* PublicExtensions.swift in Sources */,
97E21BC12E21882D000D5DCA /* imgui_widgets.cpp in Sources */,
97E21BC22E21882D000D5DCA /* imgui.cpp in Sources */,
97E21BC32E21882D000D5DCA /* imgui_draw.cpp in Sources */,
18A765BD2ECD51FB004784AA /* ControllerUtil.swift in Sources */,
A1C0DE012FBC000100000001 /* ControllerNavigator.swift in Sources */,
A1C0DE032FBC000100000001 /* RadialMenuOverlayView.swift in Sources */,
84E1CDEC2C4F67D300547904 /* CommandManager.swift in Sources */,
847D2FDB2DDA015F00937A62 /* MenuSectionView.m in Sources */,
847D2FDB2DDA015F00937A62 /* MenuSectionView.swift in Sources */,
18A0EE2C2EFBCCFD00BA853A /* GraphicUtils.swift in Sources */,
FB53E1431BE5DC4400CD6ECE /* IdManager.m in Sources */,
189AD29A2DE7446000E24B88 /* HostCollectionViewController.m in Sources */,
189AD29A2DE7446000E24B88 /* HostCollectionViewController.swift in Sources */,
9897B6A1221260EF00966419 /* VoidController.m in Sources */,
84351B112C29ACCB00A77989 /* OSCProfilesManager.m in Sources */,
84351B112C29ACCB00A77989 /* OSCProfilesManager.swift in Sources */,
FB89462919F646E200339C8A /* mkcert.c in Sources */,
FB9AFD281A7C84ED00872C98 /* HttpResponse.m in Sources */,
FB89463019F646E200339C8A /* StreamConfiguration.m in Sources */,
1850C82A2E667E4000845308 /* MicHandler.swift in Sources */,
FBD3495319FF36FB002D2A60 /* SWRevealViewController.m in Sources */,
84203F232C5F24C00032EEED /* OnScreenWidgetView.swift in Sources */,
A1900E012F9A100100BA853A /* InputAccessoryBar.swift in Sources */,
A1B2C3D42F90000100ABCDEF /* AbstractGamepadOverlayWidgetView.swift in Sources */,
2AC1E3832F507D80004F76C0 /* GenericUtils.swift in Sources */,
97E21BB52E2186A3000D5DCA /* imgui_impl_metal.mm in Sources */,
9777AAF32E2CF7A400D7FBAF /* Shaders-Linear.metal in Sources */,
84FD0B512C31338D006B5BE5 /* LocalizationHelper.m in Sources */,
97E20DEA2E216532000D5DCA /* SceneDelegate.m in Sources */,
1817B61E2E6FD04D000D938D /* SafeTimer.swift in Sources */,
FBD3495019FF2174002D2A60 /* SettingsViewController.m in Sources */,
2B7A10022FFB000100000001 /* SettingsTouchVelocityPreview.swift in Sources */,
843CC7F02C1E757D00BBC475 /* NativeTouchPointer.m in Sources */,
2A7113FD2F5A6C65001D852D /* ThemeManager.swift in Sources */,
FB89462C19F646E200339C8A /* HttpManager.m in Sources */,
FB89462D19F646E200339C8A /* MDNSManager.m in Sources */,
84351B0F2C29ACCB00A77989 /* OnScreenButtonState.m in Sources */,
@@ -1029,12 +1216,14 @@
840751B32BF49C050096B657 /* CustomTapGestureRecognizer.m in Sources */,
187F4B282E04658A00EECC59 /* WidgetPanelStackView.m in Sources */,
FB4678FA1A55FFAD00377732 /* DiscoveryManager.m in Sources */,
841BAC922DE35D6B00DA639E /* ThemeManager.m in Sources */,
18D43D7F2F050240008C326F /* PressureCurve.swift in Sources */,
18D43D812F050240008C326F /* PencilTipOffsetCalibrationViewController.swift in Sources */,
180E6E372E93BFDB0067C73A /* AlertControllerUtil.swift in Sources */,
98D5856D1C0EA79600F6CC00 /* TemporaryHost.m in Sources */,
FB89463519F646E200339C8A /* MainFrameViewController.m in Sources */,
FBD1C8E21A8AD71400C6703C /* Logger.m in Sources */,
FB1D599A1BBCCD7E00F482CA /* AppCollectionView.m in Sources */,
848F456A2DDCD12500581E36 /* HostCardView.m in Sources */,
848F456A2DDCD12500581E36 /* HostCardView.swift in Sources */,
FB89463619F646E200339C8A /* StreamFrameViewController.m in Sources */,
9827E7A32514366900F25707 /* HapticContext.m in Sources */,
9832D1361BBCD5C50036EF48 /* TemporaryApp.m in Sources */,
@@ -1045,33 +1234,39 @@
97E20DD02E21647A000D5DCA /* MetalViewController.m in Sources */,
97E20DD12E21647A000D5DCA /* MetalView.m in Sources */,
97E20DD22E21647A000D5DCA /* MetalVideoRenderer.m in Sources */,
84351B2C2C29AD6100A77989 /* OSCProfilesTableViewController.m in Sources */,
3A4A3A112F0C000100000001 /* ProfileSelectorViewController.swift in Sources */,
98D585701C0ED0E800F6CC00 /* TemporarySettings.m in Sources */,
1829CE3F2E2154DF0074A99A /* SceneDelegate.m in Sources */,
18729EDE2E8BD68800101194 /* UUIDHelper.swift in Sources */,
FB89462819F646E200339C8A /* CryptoManager.m in Sources */,
DC1F5A07206436B20037755F /* ConnectionHelper.m in Sources */,
FB89462E19F646E200339C8A /* PairManager.m in Sources */,
FB9AFD371A7E02DB00872C98 /* HttpRequest.m in Sources */,
1888C1932E24C41000108730 /* AboutView.swift in Sources */,
FB4678ED1A50C40900377732 /* OnScreenControls.m in Sources */,
84E1CDFA2C4FE83400547904 /* SwiftLocalizationHelper.swift in Sources */,
84E1CDFA2C4FE83400547904 /* LocalizationHelperExtension.swift in Sources */,
FB290D0019B2C406004C83CF /* main.m in Sources */,
9896219723D15C7000211983 /* X1Kit.swift in Sources */,
FBD3494319FC9C04002D2A60 /* AppAssetManager.m in Sources */,
FB6549561A57907E001C8F39 /* DiscoveryWorker.m in Sources */,
84351B0E2C29ACCB00A77989 /* LayoutOnScreenControls.m in Sources */,
A76B00032FFCC00100000003 /* GameSirG8MFiRumbler.swift in Sources */,
FB89462A19F646E200339C8A /* ControllerSupport.m in Sources */,
FB9AFD3D1A7E111600872C98 /* AppAssetResponse.m in Sources */,
1888C1922E24C33100108730 /* AboutViewController.swift in Sources */,
FBD349621A0089F6002D2A60 /* DataManager.m in Sources */,
97E20DE72E216510000D5DCA /* ImGuiRenderer.mm in Sources */,
18B317032EE67A3A009B62C2 /* InertialScroller.swift in Sources */,
2AC1EAFC2F922889004EDA80 /* UIStackExtension.swift in Sources */,
FB4A23B81A9D3637004D2EF2 /* LoadingFrameViewController.m in Sources */,
FB9AFD3A1A7E05CE00872C98 /* ServerInfoResponse.m in Sources */,
84351B2B2C29AD6100A77989 /* LayoutOnScreenControlsViewController.m in Sources */,
84351B5C2C29ADBE00A77989 /* ProfileTableViewCell.m in Sources */,
3A4A3A132F0C000100000003 /* LayoutOnScreenControlsViewController.swift in Sources */,
18341A042DEB662C0094E5F2 /* FixedTintImageView.m in Sources */,
18416EA42EBB3DCE00AE0124 /* UITouchUtil.swift in Sources */,
FB89463119F646E200339C8A /* StreamManager.m in Sources */,
18780CCA2F10D6CC00B90483 /* IAPManager.swift in Sources */,
84351B2D2C29AD6100A77989 /* ToolBarContainerView.m in Sources */,
18E4BCEA2E891895000A8657 /* MotionHandler.swift in Sources */,
988FCD41293B091B003050E2 /* KeyboardInputField.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1124,6 +1319,105 @@
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
2A039E5F2F72892C00546768 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = FRL23UV4N9;
ENABLE_PREVIEWS = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.trzj.VoidLinkPreview;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG VOIDLINK_PREVIEW $(inherited)";
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "VoidLinkPreview/VoidLinkPreview-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
2A039E602F72892C00546768 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = FRL23UV4N9;
ENABLE_PREVIEWS = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.trzj.VoidLinkPreview;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "VOIDLINK_PREVIEW $(inherited)";
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "VoidLinkPreview/VoidLinkPreview-Bridging-Header.h";
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
FB290D1E19B2C406004C83CF /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@@ -1247,10 +1541,13 @@
GCC_PREFIX_HEADER = "VoidLink/Limelight-Prefix.pch";
HEADER_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/third_party/moonlight-audio-haptics/include",
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
"$(PROJECT_DIR)/moonlight-common/moonlight-common-c/src",
"$(SDKROOT)/usr/include/libxml2/**",
"$(PROJECT_DIR)/libs/**",
"$(PROJECT_DIR)/libs/FFmpeg/include",
"$(PROJECT_DIR)/libs/SDL2/include",
"$(PROJECT_DIR)/libs/opus/include/opus",
"$(PROJECT_DIR)/Limelight/Input/CustomOSC",
"$(PROJECT_DIR)/Limelight/ViewControllers/CustomOSCViewControl",
"$(PROJECT_DIR)/Limelight/ViewControllers/CustomOSCViewControl/Cells",
@@ -1260,7 +1557,7 @@
"$(PROJECT_DIR)/Limelight/Database",
);
INFOPLIST_FILE = "VoidLink/Limelight-Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "VoidLink - beta";
INFOPLIST_KEY_CFBundleDisplayName = VoidLink;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
@@ -1271,6 +1568,7 @@
"$(inherited)",
"$(PROJECT_DIR)/third_party/imgui/examples/libs/glfw/lib-vc2010-32",
"$(PROJECT_DIR)/third_party/imgui/examples/libs/glfw/lib-vc2010-64",
"$(PROJECT_DIR)/libs/opus/lib",
);
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = (
"$(inherited)",
@@ -1285,6 +1583,8 @@
"$(PROJECT_DIR)/libs/FFmpeg/lib/iOS-Sim",
);
MARKETING_VERSION = 0.0.1;
"OTHER_LDFLAGS[sdk=iphoneos*]" = "$(PROJECT_DIR)/libs/opus/lib/iOS/libopus.a";
"OTHER_LDFLAGS[sdk=iphonesimulator*]" = "$(PROJECT_DIR)/libs/opus/lib/iOS-Sim/libopus.a";
PRODUCT_BUNDLE_IDENTIFIER = com.voidlink.tf.debug10.iOS;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE = "";
@@ -1312,10 +1612,13 @@
GCC_PREFIX_HEADER = "VoidLink/Limelight-Prefix.pch";
HEADER_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/third_party/moonlight-audio-haptics/include",
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
"$(PROJECT_DIR)/moonlight-common/moonlight-common-c/src",
"$(SDKROOT)/usr/include/libxml2/**",
"$(PROJECT_DIR)/libs/**",
"$(PROJECT_DIR)/libs/FFmpeg/include",
"$(PROJECT_DIR)/libs/SDL2/include",
"$(PROJECT_DIR)/libs/opus/include/opus",
"$(PROJECT_DIR)/Limelight/Input/CustomOSC",
"$(PROJECT_DIR)/Limelight/ViewControllers/CustomOSCViewControl",
"$(PROJECT_DIR)/Limelight/ViewControllers/CustomOSCViewControl/Cells",
@@ -1325,7 +1628,7 @@
"$(PROJECT_DIR)/Limelight/Database",
);
INFOPLIST_FILE = "VoidLink/Limelight-Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "VoidLink - beta";
INFOPLIST_KEY_CFBundleDisplayName = VoidLink;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
@@ -1336,6 +1639,7 @@
"$(inherited)",
"$(PROJECT_DIR)/third_party/imgui/examples/libs/glfw/lib-vc2010-32",
"$(PROJECT_DIR)/third_party/imgui/examples/libs/glfw/lib-vc2010-64",
"$(PROJECT_DIR)/libs/opus/lib",
);
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = (
"$(inherited)",
@@ -1350,6 +1654,8 @@
"$(PROJECT_DIR)/libs/FFmpeg/lib/iOS-Sim",
);
MARKETING_VERSION = 0.0.1;
"OTHER_LDFLAGS[sdk=iphoneos*]" = "$(PROJECT_DIR)/libs/opus/lib/iOS/libopus.a";
"OTHER_LDFLAGS[sdk=iphonesimulator*]" = "$(PROJECT_DIR)/libs/opus/lib/iOS-Sim/libopus.a";
PRODUCT_BUNDLE_IDENTIFIER = com.voidlink.tf.iOS;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE = "";
@@ -1367,6 +1673,15 @@
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
2A039E5E2F72892C00546768 /* Build configuration list for PBXNativeTarget "VoidLinkPreview" */ = {
isa = XCConfigurationList;
buildConfigurations = (
2A039E5F2F72892C00546768 /* Debug */,
2A039E602F72892C00546768 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
FB290CE919B2C406004C83CF /* Build configuration list for PBXProject "VoidLink" */ = {
isa = XCConfigurationList;
buildConfigurations = (
@@ -1388,6 +1703,22 @@
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
182760F52E83E2EA00EC1C5C /* XCRemoteSwiftPackageReference "swift-collections" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/apple/swift-collections.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.1.4;
};
};
18A0EE232EFB9B5A00BA853A /* XCRemoteSwiftPackageReference "SVGKit" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/SVGKit/SVGKit.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 3.0.0;
};
};
F74BEF9A2C1A705600224667 /* XCRemoteSwiftPackageReference "OpenSSL-Package" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/krzyzanowskim/OpenSSL-Package.git";
@@ -1399,6 +1730,31 @@
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
182760F62E83E2EA00EC1C5C /* DequeModule */ = {
isa = XCSwiftPackageProductDependency;
package = 182760F52E83E2EA00EC1C5C /* XCRemoteSwiftPackageReference "swift-collections" */;
productName = DequeModule;
};
182760F92E83E57D00EC1C5C /* DequeModule */ = {
isa = XCSwiftPackageProductDependency;
package = 182760F52E83E2EA00EC1C5C /* XCRemoteSwiftPackageReference "swift-collections" */;
productName = DequeModule;
};
182760FC2E83E68B00EC1C5C /* Collections */ = {
isa = XCSwiftPackageProductDependency;
package = 182760F52E83E2EA00EC1C5C /* XCRemoteSwiftPackageReference "swift-collections" */;
productName = Collections;
};
18A0EE242EFB9B5A00BA853A /* SVGKit */ = {
isa = XCSwiftPackageProductDependency;
package = 18A0EE232EFB9B5A00BA853A /* XCRemoteSwiftPackageReference "SVGKit" */;
productName = SVGKit;
};
18A0EE262EFB9B5A00BA853A /* SVGKitSwift */ = {
isa = XCSwiftPackageProductDependency;
package = 18A0EE232EFB9B5A00BA853A /* XCRemoteSwiftPackageReference "SVGKit" */;
productName = SVGKitSwift;
};
F74BEF9B2C1A705600224667 /* OpenSSL */ = {
isa = XCSwiftPackageProductDependency;
package = F74BEF9A2C1A705600224667 /* XCRemoteSwiftPackageReference "OpenSSL-Package" */;
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded</key>
<true/>
</dict>
</plist>
@@ -1,6 +1,15 @@
{
"originHash" : "f17bf6f1131c7255a75ffd6a989b69573cb49c332e612dffb429905d8d7afe72",
"originHash" : "d1fc50a28d75017e32553df2b233f8a7e2cf1542da32728ce6722b200f745608",
"pins" : [
{
"identity" : "cocoalumberjack",
"kind" : "remoteSourceControl",
"location" : "https://github.com/CocoaLumberjack/CocoaLumberjack.git",
"state" : {
"revision" : "a9ed4b6f9bdedce7d77046f43adfb8ce1fd54114",
"version" : "3.9.0"
}
},
{
"identity" : "openssl-package",
"kind" : "remoteSourceControl",
@@ -9,6 +18,33 @@
"revision" : "b9eb055fdf73e595cb4b9f665d13dc85975bf80a",
"version" : "3.3.2000"
}
},
{
"identity" : "svgkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SVGKit/SVGKit.git",
"state" : {
"revision" : "58152b9f7c85eab239160b36ffdfd364aa43d666",
"version" : "3.0.0"
}
},
{
"identity" : "swift-collections",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections.git",
"state" : {
"revision" : "c1805596154bb3a265fd91b8ac0c4433b4348fb0",
"version" : "1.2.0"
}
},
{
"identity" : "swift-log",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-log",
"state" : {
"revision" : "bc386b95f2a16ccd0150a8235e7c69eab2b866ca",
"version" : "1.8.0"
}
}
],
"version" : 3
BIN
View File
Binary file not shown.
+87 -27
View File
@@ -16,12 +16,12 @@ public struct AboutView: View {
public var aboutVC:UIViewController
public var body: some View {
VStack(spacing: 10) {
VStack(spacing: 7) {
// App 图标
Image(uiImage: UIImage(named: "AppIconMedium") ?? UIImage())
// .resizable()
.resizable(capInsets:EdgeInsets(top: 3.5, leading: 3.5, bottom: 3.5, trailing: 3.5))
.frame(width: 100, height: 100)
.frame(width: 90, height: 90)
.clipShape(RoundedRectangle(cornerRadius: 28))
// App 名称
@@ -35,47 +35,107 @@ public struct AboutView: View {
.font(.subheadline)
.foregroundColor(.gray)
}
// 说明文字
Text(SwiftLocalizationHelper.localizedString(forKey: "From the player community, to the player community."))
.multilineTextAlignment(.center)
.lineLimit(nil)
.frame(maxWidth: 570) // ✅ 避免 Text 被拉得太宽无法换行
if Locale.preferredLanguages.first!.hasPrefix("zh-Hans") {
if #available(iOS 14.0, *) {
Text("闽ICP备17012590号-3A\n主办单位:福州创图信息技术有限公司").font(.caption2)
Text("闽ICP备17012590号-3A 主办单位:福州创图信息技术有限公司").font(.caption2)
.multilineTextAlignment(.center)
.lineLimit(nil)
.frame(maxWidth: 570) // ✅ 避免 Text 被拉得太宽无法换行
.padding()
//.padding()
} else {
Text("闽ICP备17012590号-3A\n主办单位:福州创图信息技术有限公司").font(.caption)
Text("闽ICP备17012590号-3A 主办单位:福州创图信息技术有限公司").font(.caption)
.multilineTextAlignment(.center)
.lineLimit(nil)
.frame(maxWidth: 570) // ✅ 避免 Text 被拉得太宽无法换行
.padding()
//.padding()
}
}
// 说明文字
Text(LocalizationHelper.localizedString(forKey: "From the player community, to the player community."))
.multilineTextAlignment(.center)
.font(Font.caption.italic())
.lineLimit(nil)
.frame(maxWidth: 570) // ✅ 避免 Text 被拉得太宽无法换行
.padding()
Text(LocalizationHelper.localizedString(forKey: "VoidLink delivers better performance now!"))
.multilineTextAlignment(.center)
.font(Font.callout.bold())
.lineLimit(nil)
.frame(maxWidth: 570) // ✅ 避免 Text 被拉得太宽无法换行
// 链接按钮
if #available(iOS 14.0, *) {
Link(SwiftLocalizationHelper.localizedString(forKey: "Join us"), destination: URL(string: SwiftLocalizationHelper.localizedString(forKey: "supportLink"))!)
.padding(.top, 10)
Spacer()
// OK 按钮
Button(SwiftLocalizationHelper.localizedString(forKey: "OK")) {
aboutVC.dismiss(animated:true)
HStack(spacing: 20) {
Link(LocalizationHelper.localizedString(forKey: "Learn more"), destination: URL(string: LocalizationHelper.localizedString(forKey: "supportLink"))!)
if #available(iOS 16, *) {
let languageCode = Locale.current.language.languageCode?.identifier
if languageCode == "zh" {
Link(LocalizationHelper.localizedString(forKey: "加入QQ群"), destination: URL(string: LocalizationHelper.localizedString(forKey: "https://qm.qq.com/q/uM51CYWLS2"))!)
}
}
if PublicUtils.isIPhone {
Link(LocalizationHelper.localizedString(forKey: "joinCommunity"), destination: URL(string: LocalizationHelper.localizedString(forKey: "communityLink"))!)
}
}
.padding()
.background(Color.blue)
.foregroundColor(.white)
.frame(height: 46)
.cornerRadius(12)
.padding(.top, 10)
if !PublicUtils.isIPhone {
Link(LocalizationHelper.localizedString(forKey: "joinCommunity"), destination: URL(string: LocalizationHelper.localizedString(forKey: "communityLink"))!)
}
HStack(spacing: 15) {
if #available(iOS 15.0, *) {
if PublicUtils.isIPad, PublicUtils.isIAPAddonAvailable {
Button(LocalizationHelper.localizedString(forKey: "I'm an artist")) {
IAPManager.checkPurchaseInfo(.PencilProPack) { info in
if !info.valid {
IAPManager.inAppPurchaseAction(viewController: self.aboutVC, product: .PencilProPack)
}
else {
AlertControllerUtil.showAlert(
in: self.aboutVC,
title: "",
message: LocalizationHelper.localizedString(forKey:"Drawing Toolkit already purchased"),
withCancel: false,
buttonTitle: LocalizationHelper.localizedString(forKey: "OK"),
countdown: 0)
}
}
}
.padding()
.background(Color.blue.opacity(1))
.foregroundColor(.white)
.frame(height: 46)
.cornerRadius(12)
.padding(.top, 10)
}
}
// OK 按钮
Button(LocalizationHelper.localizedString(forKey: "OK")) {
aboutVC.dismiss(animated:true)
}
.padding()
.background(Color.blue)
.foregroundColor(.white)
.frame(height: 46)
.cornerRadius(12)
.padding(.top, 10)
}
} else {
HStack(spacing: 20) {
Button(SwiftLocalizationHelper.localizedString(forKey: "Join us")) {
Button(LocalizationHelper.localizedString(forKey: "Join us")) {
// 打开链接
if let url = URL(string: SwiftLocalizationHelper.localizedString(forKey: "supportLink")) {
if let url = URL(string: LocalizationHelper.localizedString(forKey: "supportLink")) {
UIApplication.shared.open(url)
}
}
@@ -86,7 +146,7 @@ public struct AboutView: View {
.frame(height: 33)
.frame(minWidth: 100)
/*
Button(SwiftLocalizationHelper.localizedString(forKey: "OK")) {
Button(LocalizationHelper.localizedString(forKey: "OK")) {
presentationMode.wrappedValue.dismiss()
}
.padding()
@@ -95,7 +155,7 @@ public struct AboutView: View {
.cornerRadius(12)
.frame(height: 33)
.frame(minWidth: 100)*/
Button(SwiftLocalizationHelper.localizedString(forKey: "OK")) {
Button(LocalizationHelper.localizedString(forKey: "OK")) {
aboutVC.dismiss(animated:true)
}
.padding()
+29 -4
View File
@@ -10,7 +10,26 @@
import SwiftUI
@available(iOS 13.0, *)
@objc class AboutViewController: UIViewController {
@objc class AboutViewController: UIViewController, ControllerUINavigationDelegate {
func getNavigationElements() -> [ControllerNavigationElement] {
var elements: [ControllerNavigationElement] = []
elements.append(ControllerNavigationElement(control:ControllerNavigator.radialMenuButtonPosition == .left ? .dpadRight : .a, action: "ok"))
return elements
}
func navigateByController(forward: Bool) {}
func navigateByController(downward: Bool) {}
func persistControllerNavigationHighlight() {}
func restoreControllerNavigationHighlight() {}
func restoreControllerNavigationHighlightAfterSettingsModeSwitch() {}
func uiWidgetActionForControllerNavigator(forward: Bool, from navigation: ControllerNavigationElement) {}
func uiButtonActionForControllerNavigator(pressed: Bool, from navigation: ControllerNavigationElement) {
if pressed, navigation.action == "ok" {
self.dismiss(animated: true)
}
}
override func viewDidLoad() {
super.viewDidLoad()
@@ -28,12 +47,18 @@ import SwiftUI
hostingVC.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
])
// 🎯 设置弹窗大小
preferredContentSize = CGSize(width: 530, height: 450)
preferredContentSize = CGSize(width: 530, height: 430)
// ⚠️ 一定要设置以下样式才会生效 preferredContentSize
modalPresentationStyle = .formSheet
}
override func viewDidAppear(_ animated: Bool) {
ControllerNavigator.setUINavigationDelegate(self)
}
override func viewDidDisappear(_ animated: Bool) {
ControllerNavigator.restorePreviousUINavigationDelegate(ifCurrentDelegateIs: self)
}
}
func isIPhone() -> Bool {
+199
View File
@@ -0,0 +1,199 @@
//
// AlertControllerUtil.swift
// VoidLink
//
// Created by Weimin on 2025/10/6.
// Copyright © 2025 True砖家 @ Bilibili. All rights reserved.
//
import UIKit
@objc class AlertControllerUtil: NSObject {
private enum AlertTextRole {
case title
case message
}
private static func makeCountdownMessage(baseMessage: String?, remainingSeconds: Int) -> String {
let countdownText = "\(remainingSeconds)"
guard let baseMessage, !baseMessage.isEmpty else {
return countdownText
}
return "\(baseMessage)\n\n\(countdownText)"
}
@available(iOS 13.0, *)
private static func makeLeftAlignedAttributedText(_ text: String, role: AlertTextRole) -> NSAttributedString {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .left
paragraphStyle.lineBreakMode = .byWordWrapping
let font: UIFont
let color: UIColor
switch role {
case .title:
font = .preferredFont(forTextStyle: .headline)
color = .label
case .message:
font = .preferredFont(forTextStyle: .footnote)
color = .label
}
return NSAttributedString(
string: text,
attributes: [
.paragraphStyle: paragraphStyle,
.font: font,
.foregroundColor: color
]
)
}
private static func applyLeftAlignment(title: String?, message: String?) {
if #available(iOS 26.0, *) {
return
}
if #available(iOS 13.0, *) {
if let title, !title.isEmpty {
alertController.setValue(makeLeftAlignedAttributedText(title, role: .title), forKey: "attributedTitle")
}
if let message, !message.isEmpty {
alertController.setValue(makeLeftAlignedAttributedText(message, role: .message), forKey: "attributedMessage")
}
}
}
/// 类方法,直接在任何 UIViewController 上显示倒计时弹窗
/// - Parameters:
/// - viewController: 要显示弹窗的控制器
/// - title: 弹窗标题
/// - message: 弹窗内容
/// - buttonTitle: 按钮初始文字(倒计时结束后显示)
/// - countdown: 倒计时秒数
/// - completion: 点击确认或倒计时结束的回调
@objc public static var actionCancelled:Bool = false
@objc public static var alertController:UIAlertController = UIAlertController()
@objc public static var cancelButtonString:String = "Cancel"
@objc static var completion: (() -> Void)? = nil
@objc public static var autoCompletion:Bool = false
@objc public static var isCountingDown:Bool = false
@objc class func showAlert(
in viewController: UIViewController? = nil,
title: String?,
message: String?,
withCancel: Bool,
buttonTitle: String,
countdown: Int,
action: (() -> Void)? = nil,
completion: (() -> Void)? = nil
) {
var remainingSeconds = countdown
let originalMessage = message
var isAlertDismissed = false
AlertControllerUtil.completion = completion
guard let viewController = viewController else {return}
alertController = UIAlertController(
title: title,
message: (countdown > 0 && !autoCompletion) ? makeCountdownMessage(baseMessage: originalMessage, remainingSeconds: remainingSeconds) : message,
preferredStyle: .alert
)
applyLeftAlignment(
title: title,
message: (countdown > 0 && !autoCompletion) ? makeCountdownMessage(baseMessage: originalMessage, remainingSeconds: remainingSeconds) : message
)
let confirmAction = UIAlertAction(title: buttonTitle, style: .default) { _ in
isAlertDismissed = true
actionCancelled = false
completion?()
cancelButtonString = "Cancel"
}
let cancelAction = UIAlertAction(title: LocalizationHelper.localizedString(forKey: cancelButtonString), style: .cancel) { _ in
isAlertDismissed = true
actionCancelled = true
completion?()
cancelButtonString = "Cancel"
}
confirmAction.isEnabled = false
if withCancel {alertController.addAction(cancelAction)}
if buttonTitle != "" && !autoCompletion {alertController.addAction(confirmAction)}
if(countdown == 0) {
confirmAction.isEnabled = buttonTitle != ""
}
viewController.present(alertController, animated: true, completion: action)
isCountingDown = !autoCompletion && countdown > 0
if(countdown == 0) {return}
// 倒计时
let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.main)
timer.schedule(deadline: .now() + (autoCompletion ? 0.5 : 1.0), repeating: autoCompletion ? 0.1: 1.0, leeway: .milliseconds(50))
timer.setEventHandler {
guard !isAlertDismissed else {
timer.cancel()
return
}
remainingSeconds -= 1
if remainingSeconds <= 0 {
isCountingDown = false
isAlertDismissed = true
timer.cancel()
if autoCompletion {
alertController.message = originalMessage
applyLeftAlignment(title: title, message: originalMessage)
autoCompletion = false
completion?()
AlertControllerUtil.completion = nil
alertController.dismiss(animated: false)
return
}
if PublicUtils.isRunningOnMacAsiPadApp && buttonTitle != "" {
AlertControllerUtil.completion = nil
alertController.dismiss(animated: false) {
showAlert(
in: viewController,
title: title,
message: originalMessage,
withCancel: withCancel,
buttonTitle: buttonTitle,
countdown: 0,
action: nil,
completion: completion
)
}
return
}
confirmAction.isEnabled = true
alertController.message = originalMessage
applyLeftAlignment(title: title, message: originalMessage)
if #available(iOS 13.0, *) {
ControllerNavigator.setUINavigationDelegate(alertController)
}
} else {
if !autoCompletion {
let countdownMessage = makeCountdownMessage(baseMessage: originalMessage, remainingSeconds: remainingSeconds)
alertController.message = countdownMessage
applyLeftAlignment(title: title, message: countdownMessage)
}
}
}
timer.resume()
}
}
+2
View File
@@ -60,6 +60,8 @@ static NSString* DB_NAME = @"Limelight_iOS.sqlite";
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for command tool customization after application launch (works only when user default is nil)
[CommandManager presetDefaultCommands];
[GenericUtils installSegmentedControlPreviousSelectionTracking];
[IAPManager shared];
// For iOS 12 and below, we need to manually create the window
if (@available(iOS 13.0, *)) {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+75 -11
View File
@@ -16,15 +16,17 @@
@interface DataManager : NSObject
typedef NS_ENUM(NSUInteger, UINavigationBarHeight) {
UINavigationBarHeightIPad = 50,
UINavigationBarHeightIPhone = 44
typedef NS_ENUM(NSInteger, ControllerGyroSwitchMode) {
ControllerGyroSwitchDisabled,
ControllerGyroSwitchPressToToggle,
ControllerGyroSwitchHoldDown
};
typedef NS_ENUM(NSInteger, TouchMode) {
RelativeTouch,
NativeTouch,
AbsoluteTouch,
TouchDisabled,
NativeTouchOnly
};
@@ -42,9 +44,23 @@ typedef NS_ENUM(NSInteger, GyroMode) {
AlwaysController
};
typedef NS_ENUM(NSInteger, HapticEnginePreference) {
HapticEngineAuto,
RumbleDevice,
LeftRightSwapped,
RumbleOff
};
typedef NS_ENUM(NSInteger, ControllerMouseStick) {
LeftStickToMouse,
RightStickToMouse,
};
typedef NS_ENUM(NSInteger, FramePacingMode) {
FramePacingModeOff,
FramePacingModeLegacy,
FramePacingModeQueue
FramePacingModeQueue,
FramePacingModeInterpolation
};
typedef NS_ENUM(NSInteger, SettingsMenuMode) {
@@ -53,7 +69,18 @@ typedef NS_ENUM(NSInteger, SettingsMenuMode) {
RemoveSettingItem,
};
- (void) saveSettingsWithBitrate:(NSInteger)bitrate
typedef NS_ENUM(NSInteger, WidgetSizeTransition) {
keepWidgetSize,
transitionWithOrientation,
};
typedef NS_ENUM(NSInteger, PencilTickMode) {
PencilTickDisabled,
ManualTick
};
- (void) saveSettings:(Settings*)settings
withBitrate:(NSInteger)bitrate
framerate:(NSInteger)framerate
height:(NSInteger)height
width:(NSInteger)width
@@ -69,25 +96,32 @@ typedef NS_ENUM(NSInteger, SettingsMenuMode) {
touchPointerVelocityFactor:(CGFloat)touchPointerVelocityFactor
mousePointerVelocityFactor:(CGFloat)mousePointerVelocityFactor
gyroSensitivity:(CGFloat)gyroSensitivity
localVolume:(CGFloat)localVolume
micVolume:(CGFloat)micVolume
touchMoveEventInterval:(NSInteger)touchMoveEventInterval
reverseMouseWheelDirection:(BOOL)reverseMouseWheelDirection
asyncNativeTouchPriority:(NSInteger)asyncNativeTouchPriority
asyncNativeTouchPriority:(NSInteger)asyncNativeTouchPriority
liftStreamViewForKeyboard:(BOOL)liftStreamViewForKeyboard
showKeyboardToolbar:(BOOL)showKeyboardToolbar
optimizeGames:(BOOL)optimizeGames
multiController:(BOOL)multiController
buttonVisualFeedback:(BOOL)buttonVisualFeedback
touchPointTracking:(BOOL)touchPointTracking
swapABXYButtons:(BOOL)swapABXYButtons
audioOnPC:(BOOL)audioOnPC
redirectMic:(BOOL)redirectMic
useBuiltinMic:(BOOL)useBuiltinMic
preferredCodec:(uint32_t)preferredCodec
enableYUV444:(BOOL)enableYUV444
enableYUV444:(BOOL)enableYUV444
enablePIP:(BOOL)enablePIP
fullColorRange:(BOOL)fullColorRange
enableHdr:(BOOL)enableHdr
btMouseSupport:(BOOL)btMouseSupport
// absoluteTouchMode:(BOOL)absoluteTouchMode
touchMode:(NSInteger)touchMode
statsOverlayLevel:(NSInteger)statsOverlayLevel
statsOverlayEnabled:(BOOL)statsOverlayEnabled
unlockDisplayOrientation:(BOOL)unlockDisplayOrientation
statsOverlayEnabled:(BOOL)statsOverlayEnabled
unlockDisplayOrientation:(BOOL)unlockDisplayOrientation
resolutionSelected:(NSInteger)resolutionSelected
externalDisplayMode:(NSInteger)externalDisplayMode
localMousePointerMode:(NSInteger)localMousePointerMode
@@ -95,8 +129,38 @@ typedef NS_ENUM(NSInteger, SettingsMenuMode) {
enableGraphs:(BOOL)enableGraphs
graphOpacity:(NSInteger)graphOpacity
renderingBackend:(NSInteger)renderingBackend
framePacingMode:(NSInteger)framePacingMode
backgroundSessionTimer:(NSInteger)backgroundSessionTimer;
framePacingMode:(NSInteger)framePacingMode
interpolationMaximumDimension:(NSInteger)interpolationMaximumDimension
interpolationMaximumPixelCount:(NSInteger)interpolationMaximumPixelCount
streamDimensionScale:(CGFloat)streamDimensionScale
sendDummyEvent:(BOOL)sendDummyEvent
rememberFoldState:(BOOL)rememberFoldState
singleTapSensitivy:(CGFloat)singleTapSensitivy
hapticEngine:(NSInteger)hapticEngine
edgeSlidingSensitivity:(CGFloat)edgeSlidingSensitivity
audioEngine:(NSInteger)audioEngine
delayLeftClick:(BOOL)delayLeftClick
duckOtherApps:(BOOL)duckOtherApps
muteInBackground:(BOOL)muteInBackground
relativeTouchSlideThreshold:(CGFloat)relativeTouchSlideThreshold
enablePinch:(BOOL)enablePinch
scrollSensitivity:(CGFloat)scrollSensitivity
pinchSensitivity:(CGFloat)pinchSensitivity
ctrlDownForPinch:(BOOL)ctrlDownForPinch
leftClickDelayMs:(CGFloat)leftClickDelayMs
settingsMenuOffset:(CGFloat)settingsMenuOffset
passthroughGestures:(BOOL)passthroughGestures
enableControllerNavigation:(BOOL)mapControllerToMouse
controllerMousePointerVelocity:(CGFloat)controllerMousePointerVelocity
controllerMouseExpo:(CGFloat)controllerMouseExpo
controllerGyroSwitchMode:(NSInteger)controllerGyroSwitchMode
enableFrameTimebase:(BOOL)enableFrameTimebase
asyncFrameDequeue:(BOOL)asyncFrameDequeue
sdrPerformanceWorkaround:(BOOL)sdrPerformanceWorkaround
softKeyboardHeight:(CGFloat)softKeyboardHeight
globeAsEscape:(BOOL)globeAsEscape
streamingRadialMenuDelay:(CGFloat)streamingRadialMenuDelay
backgroundSessionTimer:(NSInteger)backgroundSessionTimer;
- (NSArray*) getHosts;
- (void) updateHost:(TemporaryHost*)host;
+83 -9
View File
@@ -55,7 +55,8 @@
return uid;
}
- (void) saveSettingsWithBitrate:(NSInteger)bitrate
- (void) saveSettings:(Settings*)settings
withBitrate:(NSInteger)bitrate
framerate:(NSInteger)framerate
height:(NSInteger)height
width:(NSInteger)width
@@ -71,25 +72,32 @@
touchPointerVelocityFactor:(CGFloat)touchPointerVelocityFactor
mousePointerVelocityFactor:(CGFloat)mousePointerVelocityFactor
gyroSensitivity:(CGFloat)gyroSensitivity
localVolume:(CGFloat)localVolume
micVolume:(CGFloat)micVolume
touchMoveEventInterval:(NSInteger)touchMoveEventInterval
reverseMouseWheelDirection:(BOOL)reverseMouseWheelDirection
asyncNativeTouchPriority:(NSInteger)asyncNativeTouchPriority
asyncNativeTouchPriority:(NSInteger)asyncNativeTouchPriority
liftStreamViewForKeyboard:(BOOL)liftStreamViewForKeyboard
showKeyboardToolbar:(BOOL)showKeyboardToolbar
optimizeGames:(BOOL)optimizeGames
multiController:(BOOL)multiController
buttonVisualFeedback:(BOOL)buttonVisualFeedback
touchPointTracking:(BOOL)touchPointTracking
swapABXYButtons:(BOOL)swapABXYButtons
audioOnPC:(BOOL)audioOnPC
redirectMic:(BOOL)redirectMic
useBuiltinMic:(BOOL)useBuiltinMic
preferredCodec:(uint32_t)preferredCodec
enableYUV444:(BOOL)enableYUV444
enableYUV444:(BOOL)enableYUV444
enablePIP:(BOOL)enablePIP
fullColorRange:(BOOL)fullColorRange
enableHdr:(BOOL)enableHdr
btMouseSupport:(BOOL)btMouseSupport
// absoluteTouchMode:(BOOL)absoluteTouchMode
touchMode:(NSInteger)touchMode
statsOverlayLevel:(NSInteger)statsOverlayLevel
statsOverlayEnabled:(BOOL)statsOverlayEnabled
unlockDisplayOrientation:(BOOL)unlockDisplayOrientation
statsOverlayEnabled:(BOOL)statsOverlayEnabled
unlockDisplayOrientation:(BOOL)unlockDisplayOrientation
resolutionSelected:(NSInteger)resolutionSelected
externalDisplayMode:(NSInteger)externalDisplayMode
localMousePointerMode:(NSInteger)localMousePointerMode
@@ -97,11 +105,42 @@
enableGraphs:(BOOL)enableGraphs
graphOpacity:(NSInteger)graphOpacity
renderingBackend:(NSInteger)renderingBackend
framePacingMode:(NSInteger)framePacingMode
framePacingMode:(NSInteger)framePacingMode
interpolationMaximumDimension:(NSInteger)interpolationMaximumDimension
interpolationMaximumPixelCount:(NSInteger)interpolationMaximumPixelCount
streamDimensionScale:(CGFloat)streamDimensionScale
sendDummyEvent:(BOOL)sendDummyEvent
rememberFoldState:(BOOL)rememberFoldState
singleTapSensitivy:(CGFloat)singleTapSensitivy
hapticEngine:(NSInteger)hapticEngine
edgeSlidingSensitivity:(CGFloat)edgeSlidingSensitivity
audioEngine:(NSInteger)audioEngine
delayLeftClick:(BOOL)delayLeftClick
duckOtherApps:(BOOL)duckOtherApps
muteInBackground:(BOOL)muteInBackground
relativeTouchSlideThreshold:(CGFloat)relativeTouchSlideThreshold
enablePinch:(BOOL)enablePinch
scrollSensitivity:(CGFloat)scrollSensitivity
pinchSensitivity:(CGFloat)pinchSensitivity
ctrlDownForPinch:(BOOL)ctrlDownForPinch
leftClickDelayMs:(CGFloat)leftClickDelayMs
settingsMenuOffset:(CGFloat)settingsMenuOffset
passthroughGestures:(BOOL)passthroughGestures
enableControllerNavigation:(BOOL)enableControllerNavigation
controllerMousePointerVelocity:(CGFloat)controllerMousePointerVelocity
controllerMouseExpo:(CGFloat)controllerMouseExpo
controllerGyroSwitchMode:(NSInteger)controllerGyroSwitchMode
enableFrameTimebase:(BOOL)enableFrameTimebase
asyncFrameDequeue:(BOOL)asyncFrameDequeue
sdrPerformanceWorkaround:(BOOL)sdrPerformanceWorkaround
softKeyboardHeight:(CGFloat)softKeyboardHeight
globeAsEscape:(BOOL)globeAsEscape
streamingRadialMenuDelay:(CGFloat)streamingRadialMenuDelay
backgroundSessionTimer:(NSInteger)backgroundSessionTimer{
__block Settings* settingsToSave = settings;
[_managedObjectContext performBlockAndWait:^{
Settings* settingsToSave = [self retrieveSettings];
if(!settingsToSave) settingsToSave = [self retrieveSettings];
settingsToSave.framerate = [NSNumber numberWithInteger:framerate];
settingsToSave.bitrate = [NSNumber numberWithInteger:bitrate];
settingsToSave.height = [NSNumber numberWithInteger:height];
@@ -118,6 +157,8 @@
settingsToSave.touchPointerVelocityFactor = [NSNumber numberWithFloat:touchPointerVelocityFactor];
settingsToSave.mousePointerVelocityFactor = [NSNumber numberWithFloat:mousePointerVelocityFactor];
settingsToSave.gyroSensitivity = [NSNumber numberWithFloat:gyroSensitivity];
settingsToSave.localVolume = [NSNumber numberWithFloat:localVolume];
settingsToSave.micVolume = [NSNumber numberWithFloat:micVolume];
settingsToSave.touchMoveEventInterval = [NSNumber numberWithInteger:touchMoveEventInterval];
settingsToSave.reverseMouseWheelDirection = reverseMouseWheelDirection;
settingsToSave.asyncNativeTouchPriority = [NSNumber numberWithInteger:asyncNativeTouchPriority];
@@ -125,11 +166,17 @@
settingsToSave.showKeyboardToolbar = showKeyboardToolbar;
settingsToSave.optimizeGames = optimizeGames;
settingsToSave.multiController = multiController;
settingsToSave.buttonVisualFeedback = buttonVisualFeedback;
settingsToSave.touchPointTracking = touchPointTracking;
settingsToSave.swapABXYButtons = swapABXYButtons;
settingsToSave.playAudioOnPC = audioOnPC;
settingsToSave.redirectMic = redirectMic;
settingsToSave.useBuiltinMic = useBuiltinMic;
settingsToSave.preferredCodec = preferredCodec;
settingsToSave.enableYUV444 = enableYUV444;
settingsToSave.sdrPerformanceWorkaround = sdrPerformanceWorkaround;
settingsToSave.enablePIP = enablePIP;
settingsToSave.fullColorRange = fullColorRange;
settingsToSave.enableHdr = enableHdr;
settingsToSave.btMouseSupport = btMouseSupport;
// settingsToSave.absoluteTouchMode = absoluteTouchMode;
@@ -143,10 +190,38 @@
settingsToSave.backroundSessionTimer = [NSNumber numberWithInteger:backgroundSessionTimer];
settingsToSave.frameQueueSize = [NSNumber numberWithInteger:frameQueueSize];
settingsToSave.enableFrameTimebase = enableFrameTimebase;
settingsToSave.asyncFrameDequeue = asyncFrameDequeue;
settingsToSave.enableGraphs = enableGraphs;
settingsToSave.graphOpacity = [NSNumber numberWithInteger:graphOpacity];
settingsToSave.renderingBackend = [NSNumber numberWithInteger:renderingBackend];
settingsToSave.framePacingMode = [NSNumber numberWithInteger:framePacingMode];
settingsToSave.interpolationMaximumDimension = [NSNumber numberWithInteger:interpolationMaximumDimension];
settingsToSave.interpolationMaximumPixelCount = [NSNumber numberWithInteger:interpolationMaximumPixelCount];
settingsToSave.streamDimensionScale = [NSNumber numberWithDouble:streamDimensionScale];
settingsToSave.sendDummyEvent = sendDummyEvent;
settingsToSave.singleTapSensitivity = [NSNumber numberWithDouble:singleTapSensitivy];
settingsToSave.hapticEngine = [NSNumber numberWithInteger:hapticEngine];
settingsToSave.edgeSlidingSensitivity = [NSNumber numberWithFloat:edgeSlidingSensitivity];
settingsToSave.audioEngine = [NSNumber numberWithInteger:audioEngine];
settingsToSave.delayLeftClick = delayLeftClick;
settingsToSave.duckOtherApps = duckOtherApps;
settingsToSave.muteInBackground = muteInBackground;
settingsToSave.relativeTouchSlideThreshold = [NSNumber numberWithFloat:relativeTouchSlideThreshold];
settingsToSave.enablePinch = enablePinch;
settingsToSave.scrollSensitivity = [NSNumber numberWithFloat:scrollSensitivity];
settingsToSave.pinchSensitivity = [NSNumber numberWithFloat:pinchSensitivity];
settingsToSave.ctrlDownForPinch = ctrlDownForPinch;
settingsToSave.leftClickDelayMs = [NSNumber numberWithFloat:leftClickDelayMs];
settingsToSave.settingsMenuOffset = [NSNumber numberWithFloat:settingsMenuOffset];
settingsToSave.passthroughGestures = passthroughGestures;
settingsToSave.enableControllerNavigation = enableControllerNavigation;
settingsToSave.controllerMousePointerVelocity = [NSNumber numberWithFloat:controllerMousePointerVelocity];
settingsToSave.controllerMouseExpo = [NSNumber numberWithFloat:controllerMouseExpo];
settingsToSave.softKeyboardHeight = softKeyboardHeight;
settingsToSave.globeAsEscape = globeAsEscape;
settingsToSave.streamingRadialMenuDelay = @(streamingRadialMenuDelay);
settingsToSave.rememberFoldState = rememberFoldState;
[self saveData];
}];
}
@@ -301,4 +376,3 @@
}
@end
+2
View File
@@ -24,6 +24,7 @@
@property (atomic, nullable, retain) NSString *localAddress;
@property (atomic, nullable, retain) NSString *ipv6Address;
@property (atomic, nullable, retain) NSString *mac;
@property (atomic, nullable, retain) NSString *controllerNavigationHighlightedAppID;
@property (atomic) int serverCodecModeSupport;
NS_ASSUME_NONNULL_BEGIN
@@ -31,6 +32,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (atomic, retain) NSString *name;
@property (atomic, retain) NSString *uuid;
@property (atomic, retain) NSSet *appList;
@property (atomic, retain) NSMutableSet * activeAddressPool;
- (id) initFromHost:(Host*)host;
+23 -1
View File
@@ -28,12 +28,24 @@
self = [self init];
self.address = host.address;
NSSet *classes = [NSSet setWithObjects: [NSMutableSet class], [NSString class], nil];
NSError *error = nil;
self.activeAddressPool = [NSKeyedUnarchiver unarchivedObjectOfClasses:classes fromData:host.activeAddressPool error:&error];
if(self.activeAddressPool){
for(NSString* address in _activeAddressPool){
NSLog(@"%@, persisted addr: %@", host.name, address);
}
}
else self.activeAddressPool = [[NSMutableSet alloc] init];
self.externalAddress = host.externalAddress;
self.localAddress = host.localAddress;
self.ipv6Address = host.ipv6Address;
if(!(host.mac == nil || [host.mac isEqualToString:@"00:00:00:00:00:00"])) self.mac = host.mac; // try to fix invalid mac happens in some cases
self.name = host.name;
self.uuid = host.uuid;
self.controllerNavigationHighlightedAppID = host.controllerNavigationHighlightedAppID;
self.serverCodecModeSupport = host.serverCodecModeSupport;
self.serverCert = host.serverCert;
@@ -57,7 +69,7 @@
return self;
}
- (void) propagateChangesToParent:(Host*)parentHost {
- (void) propagateChangesToParent:(Host*)parentHost { // update persisted host data
// Avoid overwriting existing data with nil if
// we don't have everything populated in the temporary
// host.
@@ -73,6 +85,15 @@
if (self.ipv6Address != nil) {
parentHost.ipv6Address = self.ipv6Address;
}
if(self.activeAddressPool.count>0){
NSError *error;
parentHost.activeAddressPool = [NSKeyedArchiver archivedDataWithRootObject:self.activeAddressPool requiringSecureCoding:YES error:&error];
}
else if(self.activeAddress) [self.activeAddressPool addObject:self.activeAddress];
// NSLog(@"Persisting activeAddressPool, pool count %lu ... %f, host: %@", (unsigned long)self.activeAddressPool.count, CACurrentMediaTime(), self.name);
// try to fix invalid mac happens in some cases
// 添加主机crash问题重点关注
if (!(self.mac == nil || [self.mac isEqualToString:@"00:00:00:00:00:00"])) parentHost.mac = self.mac;
@@ -81,6 +102,7 @@
}
parentHost.name = self.name;
parentHost.uuid = self.uuid;
parentHost.controllerNavigationHighlightedAppID = self.controllerNavigationHighlightedAppID;
parentHost.serverCodecModeSupport = self.serverCodecModeSupport;
parentHost.pairState = [NSNumber numberWithInt:self.pairState];
}
+55
View File
@@ -33,6 +33,8 @@
@property (nonatomic, retain) NSNumber * touchMoveEventInterval;
@property (nonatomic, retain) NSNumber * touchPointerVelocityFactor;
@property (nonatomic, retain) NSNumber * gyroSensitivity;
@property (nonatomic, retain) NSNumber * localVolume;
@property (nonatomic, retain) NSNumber * micVolume;
@property (nonatomic, retain) NSNumber * emulatedControllerType;
@property (nonatomic, retain) NSNumber * mousePointerVelocityFactor;
@property (nonatomic, retain) NSNumber * pointerVelocityModeDivider;
@@ -40,6 +42,12 @@
@property (nonatomic, retain) NSNumber * resolutionSelected;
@property (nonatomic, retain) NSNumber * externalDisplayMode;
@property (nonatomic, retain) NSNumber * localMousePointerMode;
@property (nonatomic, retain) NSNumber * gyroBiasX;
@property (nonatomic, retain) NSNumber * gyroBiasY;
@property (nonatomic, retain) NSNumber * gyroBiasZ;
@property (nonatomic, retain) NSNumber * controllerGyroBiasX;
@property (nonatomic, retain) NSNumber * controllerGyroBiasY;
@property (nonatomic, retain) NSNumber * controllerGyroBiasZ;
@property (nonatomic, retain) NSNumber * backgroundSessionTimer;
@property (nonatomic) enum {
CODEC_PREF_AUTO,
@@ -48,12 +56,18 @@
CODEC_PREF_AV1,
} preferredCodec;
@property (nonatomic) BOOL enableYUV444;
@property (nonatomic) BOOL sdrPerformanceWorkaround;
@property (nonatomic) BOOL enablePIP;
@property (nonatomic) BOOL fullColorRange;
@property (nonatomic) BOOL reverseMouseWheelDirection;
@property (nonatomic, retain) NSNumber * asyncNativeTouchPriority;
@property (nonatomic) BOOL multiController;
@property (nonatomic) BOOL buttonVisualFeedback;
@property (nonatomic) BOOL touchPointTracking;
@property (nonatomic) BOOL swapABXYButtons;
@property (nonatomic) BOOL playAudioOnPC;
@property (nonatomic) BOOL redirectMic;
@property (nonatomic) BOOL useBuiltinMic;
@property (nonatomic) BOOL optimizeGames;
@property (nonatomic) BOOL enableHdr;
@property (nonatomic) BOOL btMouseSupport;
@@ -63,12 +77,53 @@
@property (nonatomic) BOOL statsOverlayEnabled;
@property (nonatomic) BOOL liftStreamViewForKeyboard;
@property (nonatomic) BOOL showKeyboardToolbar;
@property (nonatomic) CGFloat softKeyboardHeight;
@property (nonatomic) BOOL unlockDisplayOrientation;
@property (nonatomic) BOOL enableGraphs;
@property (nonatomic) BOOL enableFrameTimebase;
@property (nonatomic) BOOL asyncFrameDequeue;
@property (nonatomic, retain) NSNumber * frameQueueSize;
@property (nonatomic, retain) NSNumber * graphOpacity;
@property (nonatomic, retain) NSNumber * renderingBackend;
@property (nonatomic) BOOL sendDummyEvent;
@property (nonatomic) BOOL rememberFoldState;
@property (nonatomic, retain) NSNumber * singleTapSensitivity;
@property (nonatomic, retain) NSNumber * hapticEngine;
@property (nonatomic, retain) NSNumber * appTheme;
@property (nonatomic, retain) NSNumber * edgeSlidingSensitivity;
@property (nonatomic, retain) NSNumber * audioEngine;
@property (nonatomic) BOOL delayLeftClick;
@property (nonatomic) BOOL duckOtherApps;
@property (nonatomic) BOOL muteInBackground;
@property (nonatomic) BOOL enablePinch;
@property (nonatomic) BOOL ctrlDownForPinch;
@property (nonatomic) BOOL passthroughGestures;
@property (nonatomic, retain) NSNumber * framePacingMode;
@property (nonatomic, retain) NSNumber * interpolationMaximumDimension;
@property (nonatomic, retain) NSNumber * interpolationMaximumPixelCount;
@property (nonatomic, retain) NSNumber * streamDimensionScale;
@property (nonatomic, retain) NSNumber * scrollSensitivity;
@property (nonatomic, retain) NSNumber * pinchSensitivity;
@property (nonatomic, retain) NSNumber * relativeTouchSlideThreshold;
@property (nonatomic, retain) NSNumber * leftClickDelayMs;
@property (nonatomic, retain) NSNumber * settingsMenuOffset;
@property (nonatomic, retain) NSNumber * controllerMouseLeftButton;
@property (nonatomic, retain) NSNumber * controllerMouseRightButton;
@property (nonatomic, retain) NSNumber * controllerMouseStick;
@property (nonatomic, retain) NSNumber * localRadialMenuButton;
@property (nonatomic, retain) NSNumber * streamingRadialMenuButton;
@property (nonatomic) BOOL enableControllerNavigation;
@property (nonatomic, retain) NSNumber * controllerMousePointerVelocity;
@property (nonatomic, retain) NSNumber * controllerMouseExpo;
@property (nonatomic, retain) NSNumber * streamingRadialMenuDelay;
@property (nonatomic) BOOL globeAsEscape;
// Pencil settings:
@property (nonatomic, retain) NSNumber * pencilTickMode;
@property (nonatomic, retain) NSNumber * pencilTickIntervalUs;
@property (nonatomic, retain) NSNumber * pencilTipOffsetX;
@property (nonatomic, retain) NSNumber * pencilTipOffsetY;
- (id) initFromSettings:(Settings*)settings;
+61 -10
View File
@@ -42,6 +42,7 @@
self.preferredCodec = (typeof(self.preferredCodec))[[NSUserDefaults standardUserDefaults] integerForKey:@"preferredCodec"];
self.enableYUV444 = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableYUV444"];
self.enablePIP = [[NSUserDefaults standardUserDefaults] boolForKey:@"enablePIP"];
self.fullRange = [[NSUserDefaults standardUserDefaults] boolForKey:@"fullRange"];
self.frameQueueSize = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"frameQueueSize"]];
self.playAudioOnPC = [[NSUserDefaults standardUserDefaults] boolForKey:@"audioOnPC"];
self.enableHdr = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableHdr"];
@@ -52,16 +53,11 @@
self.statsOverlay = [[NSUserDefaults standardUserDefaults] boolForKey:@"statsOverlay"];
self.enableGraphs = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableGraphs"];
self.graphOpacity = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"graphOpacity"]];
// Set rendering backend based on iOS version and user preference
NSInteger savedBackend = [[NSUserDefaults standardUserDefaults] integerForKey:@"renderingBackend"];
if (@available(iOS 17.0, *)) {
// iOS 17+ can use Performance mode (Metal renderer)
self.renderingBackend = [NSNumber numberWithInteger:savedBackend];
} else {
// iOS < 17 must use Balanced mode (AVSB renderer)
self.renderingBackend = [NSNumber numberWithInteger:RENDER_AVSB];
}
self.renderingBackend = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"renderingBackend"]];
self.framePacingMode = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"framePacingMode"]];
self.interpolationMaximumDimension = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"interpolationMaximumDimension"]];
self.interpolationMaximumPixelCount = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"interpolationMaximumPixelCount"]];
self.streamDimensionScale = [NSNumber numberWithDouble:[[NSUserDefaults standardUserDefaults] doubleForKey:@"streamDimensionScale"]];
NSInteger _screenSize = [[NSUserDefaults standardUserDefaults] integerForKey:@"streamResolution"];
switch (_screenSize) {
@@ -95,12 +91,20 @@
self.audioConfig = settings.audioConfig;
self.preferredCodec = settings.preferredCodec;
self.enableYUV444 = settings.enableYUV444;
self.sdrPerformanceWorkaround = settings.sdrPerformanceWorkaround;
self.enablePIP = settings.enablePIP;
self.fullColorRange = settings.fullColorRange;
self.frameQueueSize = settings.frameQueueSize;
self.enableFrameTimebase = settings.enableFrameTimebase;
self.asyncFrameDequeue = settings.asyncFrameDequeue;
self.playAudioOnPC = settings.playAudioOnPC;
self.redirectMic = settings.redirectMic;
self.useBuiltinMic = settings.useBuiltinMic;
self.enableHdr = settings.enableHdr;
self.optimizeGames = settings.optimizeGames;
self.multiController = settings.multiController;
self.buttonVisualFeedback = settings.buttonVisualFeedback;
self.touchPointTracking = settings.touchPointTracking;
self.swapABXYButtons = settings.swapABXYButtons;
self.onscreenControls = settings.onscreenControls;
self.gyroMode = settings.gyroMode;
@@ -118,10 +122,13 @@
self.slideToSettingsDistance = settings.slideToSettingsDistance;
self.liftStreamViewForKeyboard = settings.liftStreamViewForKeyboard;
self.showKeyboardToolbar = settings.showKeyboardToolbar;
self.softKeyboardHeight = settings.softKeyboardHeight;
self.touchMoveEventInterval = settings.touchMoveEventInterval;
self.touchPointerVelocityFactor = settings.touchPointerVelocityFactor;
self.mousePointerVelocityFactor = settings.mousePointerVelocityFactor;
self.gyroSensitivity = settings.gyroSensitivity;
self.localVolume = settings.localVolume;
self.micVolume = settings.micVolume;
self.pointerVelocityModeDivider = settings.pointerVelocityModeDivider;
self.unlockDisplayOrientation = settings.unlockDisplayOrientation;
self.resolutionSelected = settings.resolutionSelected;
@@ -131,9 +138,53 @@
self.graphOpacity = settings.graphOpacity;
self.renderingBackend = settings.renderingBackend;
self.framePacingMode = settings.framePacingMode;
self.interpolationMaximumDimension = settings.interpolationMaximumDimension ?: @(0);
self.interpolationMaximumPixelCount = settings.interpolationMaximumPixelCount ?: @(0);
self.streamDimensionScale = settings.streamDimensionScale ?: @(1);
self.sendDummyEvent = settings.sendDummyEvent;
self.rememberFoldState = settings.rememberFoldState;
self.gyroBiasX = settings.gyroBiasX;
self.gyroBiasY = settings.gyroBiasY;
self.gyroBiasZ = settings.gyroBiasZ;
self.controllerGyroBiasX = settings.controllerGyroBiasX;
self.controllerGyroBiasY = settings.controllerGyroBiasY;
self.controllerGyroBiasZ = settings.controllerGyroBiasZ;
self.singleTapSensitivity = settings.singleTapSensitivity;
self.backgroundSessionTimer = settings.backroundSessionTimer;
#endif
self.edgeSlidingSensitivity = settings.edgeSlidingSensitivity;
self.appTheme = settings.appTheme;
self.hapticEngine = settings.hapticEngine;
self.uniqueId = settings.uniqueId;
self.audioEngine = settings.audioEngine;
self.delayLeftClick = settings.delayLeftClick;
self.duckOtherApps = settings.duckOtherApps;
self.muteInBackground = settings.muteInBackground;
self.relativeTouchSlideThreshold = settings.relativeTouchSlideThreshold;
self.enablePinch = settings.enablePinch;
self.scrollSensitivity = settings.scrollSensitivity;
self.pinchSensitivity = settings.pinchSensitivity;
self.leftClickDelayMs = settings.leftClickDelayMs;
self.ctrlDownForPinch = settings.ctrlDownForPinch;
self.settingsMenuOffset = settings.settingsMenuOffset;
self.passthroughGestures = settings.passthroughGestures;
self.enableControllerNavigation = settings.enableControllerNavigation;
self.controllerMouseLeftButton = settings.controllerMouseLeftButton;
self.controllerMouseRightButton = settings.controllerMouseRightButton;
self.localRadialMenuButton = settings.localRadialMenuButton;
self.streamingRadialMenuButton = settings.streamingRadialMenuButton;
self.controllerMouseStick = settings.controllerMouseStick;
self.controllerMousePointerVelocity = settings.controllerMousePointerVelocity;
self.controllerMouseExpo = settings.controllerMouseExpo;
self.streamingRadialMenuDelay = settings.streamingRadialMenuDelay;
self.globeAsEscape = settings.globeAsEscape;
// Pencil settings:
self.pencilTickMode = settings.pencilTickMode;
self.pencilTickIntervalUs = settings.pencilTickIntervalUs;
self.pencilTipOffsetX = settings.pencilTipOffsetX;
self.pencilTipOffsetY = settings.pencilTipOffsetY;
#endif
return self;
}
@@ -0,0 +1,565 @@
//
// GamepadNavigationIllustrationHud.swift
// VoidLink
//
// Created by True Zhuanjia on 2026/7/14.
// Copyright 2026 True Zhuanjia @ Bilibili. All rights reserved.
//
import UIKit
@available(iOS 13.0, *)
@objcMembers
final class GamepadNavigationIllustrationHud: UIView {
private enum KeyContent {
case text(String)
case symbol(String)
}
private struct Hint {
let control: ControllerElement
let keyContent: KeyContent
let titles: [String]
let isInAction: Bool
}
private let containerView = UIView()
private let contentStackView = UIStackView()
private var themeObserver: NSObjectProtocol?
private var windowConstraints: [NSLayoutConstraint] = []
private var trailingConstraint: NSLayoutConstraint?
private static let keyBorderViewTag = 4101
private static let keyContentViewTag = 4102
private static var currentHud: GamepadNavigationIllustrationHud?
private static var activeControls = Set<ControllerElement>()
private static var actionStateMinimumEndTimes: [ControllerElement: CFTimeInterval] = [:]
private static var pendingActionStateWorkItems: [ControllerElement: DispatchWorkItem] = [:]
private static var pendingClearHudWorkItem: DispatchWorkItem?
private static let minimumActionStateDuration: CFTimeInterval = 0.09
private static let hudScale: CGFloat = PublicUtils.isIPhone ? 0.74 : 1
private static let hudWidth: CGFloat = 235
private static let edgeMargin: CGFloat = PublicUtils.isIPhone ? 5 : 24
private var hints: [Hint] = []
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
observeThemeChanges()
updateTheme()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupView()
observeThemeChanges()
updateTheme()
}
deinit {
NSLayoutConstraint.deactivate(windowConstraints)
containerView.removeFromSuperview()
if let themeObserver {
NotificationCenter.default.removeObserver(themeObserver)
}
}
override func safeAreaInsetsDidChange() {
super.safeAreaInsetsDidChange()
if let window {
updateTrailingOffset(for: window)
}
}
@discardableResult
static func showInKeyWindow() -> GamepadNavigationIllustrationHud? {
guard let window = keyWindow(), ControllerUtil.primaryGCController != nil else {
return nil
}
let hud = currentHud ?? GamepadNavigationIllustrationHud()
currentHud = hud
hud.attach(to: window)
if let mainFrameVC = ControllerNavigator.radialMenuDelegate as? MainFrameViewController, ControllerNavigator.radialMenuView == nil {
DispatchQueue.main.asyncAfter(deadline: .now()+3) {
let streamFrameVC = StreamFrameViewController.sharedInstance()
if ControllerNavigator.radialMenuView == nil, mainFrameVC.isStreaming(), !mainFrameVC.settingsExpandedInStreamView, streamFrameVC?.hasNoPresentedVC == true, !ControllerNavigator.controllerMouseEnabled {
requestHudDetachKeepingMinimumActionDuration()
}
}
}
return hud
}
static func updateCurrentTheme() {
currentHud?.updateTheme()
}
@objc static func updateNavigationElements(_ elements: [ControllerNavigationElement], forceDisplay: Bool = false) {
if !forceDisplay {
guard ControllerNavigator.enabled, ControllerUtil.primaryGCController != nil else {return}
guard !ControllerNavigator.stickReleasedInRadialMenu else {return}
}
PublicUtils.runOnMain {
let hud = showInKeyWindow()
hud?.updateHints(with: elements)
}
}
@objc static func updateHud(forceDisplay: Bool = false) {
let elements = ControllerNavigator.uiNavigationDelegate?.getNavigationElements() ?? []
/*
for element in elements {
print("element \(element.action)")
} */
updateNavigationElements(elements, forceDisplay: forceDisplay)
}
static func updateActionState(for control: ControllerElement, isInAction: Bool) {
PublicUtils.runOnMain {
if isInAction {
pendingClearHudWorkItem?.cancel()
pendingClearHudWorkItem = nil
pendingActionStateWorkItems[control]?.cancel()
pendingActionStateWorkItems[control] = nil
actionStateMinimumEndTimes[control] = CACurrentMediaTime() + minimumActionStateDuration
let didChange = activeControls.insert(control).inserted
guard didChange else { return }
currentHud?.refreshActionStates()
} else {
requestActionStateEnd(for: control)
}
}
}
static func resetActionStates() {
PublicUtils.runOnMain {
guard !activeControls.isEmpty else { return }
Array(activeControls).forEach { requestActionStateEnd(for: $0) }
}
}
static func clearHud() {
PublicUtils.runOnMain {
if ControllerNavigator.controllerMouseEnabled {
finishHudDetach()
return
}
requestHudDetachKeepingMinimumActionDuration()
}
}
func updateTheme() {
let isDark = ThemeManager.userInterfaceStyle() == .dark
backgroundColor = isDark
? UIColor(red: 28.0 / 255.0, green: 28.0 / 255.0, blue: 30.0 / 255.0, alpha: 0.62)
: (PublicUtils.isIPhone ? UIColor.white.withAlphaComponent(0.75) : UIColor.white.withAlphaComponent(0.6))
layer.borderColor = (isDark ? UIColor.white.withAlphaComponent(0.10) : UIColor.black.withAlphaComponent(0.06)).cgColor
layer.shadowColor = UIColor.black.cgColor
layer.shadowOpacity = isDark ? 0.34 : 0.12
layer.shadowRadius = isDark ? 18 : 16
layer.shadowOffset = CGSize(width: 0, height: 8)
for (index, row) in contentStackView.arrangedSubviews.enumerated() {
guard hints.indices.contains(index),
let rowStack = row as? UIStackView,
let keyView = rowStack.arrangedSubviews.first,
let titleView = rowStack.arrangedSubviews.last else {
continue
}
let isInAction = hints[index].isInAction
let keyBorderView = keyView.viewWithTag(Self.keyBorderViewTag) ?? keyView
let keyContentView = keyView.viewWithTag(Self.keyContentViewTag) ?? keyView
let keyBackgroundColor = isInAction
? ThemeManager.appPrimaryColor.withAlphaComponent(isDark ? 0.2 : 0.13)
: ThemeManager.appPrimaryColor.withAlphaComponent(isDark ? 0.09 : 0.05)
keyView.backgroundColor = .clear
keyBorderView.layer.borderColor = ThemeManager.appPrimaryColor.withAlphaComponent(isDark ? 0.69 : 0.6).cgColor
keyContentView.backgroundColor = keyBackgroundColor
keyView.transform = .identity
for subview in keyContentView.subviews {
if let keyLabel = subview as? UILabel {
keyLabel.textColor = isInAction ? ThemeManager.appPrimaryColor.withAlphaComponent(isDark ? 1 : 1) : ThemeManager.appPrimaryColor.withAlphaComponent(isDark ? 0.83 : 0.83)
} else if let imageView = subview as? UIImageView {
imageView.tintColor = isInAction ? ThemeManager.appPrimaryColor.withAlphaComponent(isDark ? 1 : 1) : ThemeManager.appPrimaryColor.withAlphaComponent(isDark ? 0.83 : 0.83)
}
}
updateTitleColors(in: titleView, color: ThemeManager.textColor.withAlphaComponent(isDark ? 0.69 : 0.6))
}
}
private func setupView() {
translatesAutoresizingMaskIntoConstraints = false
isUserInteractionEnabled = false
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.isUserInteractionEnabled = false
transform = CGAffineTransform(scaleX: Self.hudScale, y: Self.hudScale)
layer.cornerRadius = 18
layer.cornerCurve = .continuous
layer.borderWidth = 1
contentStackView.axis = .vertical
contentStackView.alignment = .leading
contentStackView.spacing = 6
contentStackView.translatesAutoresizingMaskIntoConstraints = false
addSubview(contentStackView)
NSLayoutConstraint.activate([
contentStackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12),
contentStackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12),
contentStackView.topAnchor.constraint(equalTo: topAnchor, constant: 10),
contentStackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -10)
])
reloadHintRows()
}
private func updateHints(with elements: [ControllerNavigationElement]) {
let nextHints = hints(for: elements)
hints = nextHints.isEmpty ? [] : nextHints
guard !hints.isEmpty else {return}
reloadHintRows()
updateTheme()
isHidden = hints.isEmpty
}
private func refreshActionStates() {
hints = hints.map { hint in
Hint(
control: hint.control,
keyContent: hint.keyContent,
titles: hint.titles,
isInAction: Self.isControlActive(hint.control)
)
}
updateTheme()
}
private func reloadHintRows() {
contentStackView.arrangedSubviews.forEach { row in
contentStackView.removeArrangedSubview(row)
row.removeFromSuperview()
}
hints.forEach { contentStackView.addArrangedSubview(makeRow(for: $0)) }
}
private func hints(for navigationElements: [ControllerNavigationElement]) -> [Hint] {
var groupedElements: [ControllerElement: [ControllerNavigationElement]] = [:]
var orderedControls: [ControllerElement] = []
for navigationElement in navigationElements {
if groupedElements[navigationElement.control] == nil {
orderedControls.append(navigationElement.control)
groupedElements[navigationElement.control] = []
}
groupedElements[navigationElement.control]?.append(navigationElement)
}
return orderedControls.flatMap { control -> [Hint] in
guard let elements = groupedElements[control] else { return [] }
if elements.count == 2 {
return [hint(for: elements)]
}
return elements.map { hint(for: [$0]) }
}
}
private func hint(for navigationElements: [ControllerNavigationElement]) -> Hint {
let control = navigationElements.first?.control ?? .null
return Hint(
control: control,
keyContent: keyContent(for: control),
titles: navigationElements.map { $0.action.localized },
isInAction: navigationElements.contains(where: { $0.isInAction }) || Self.isControlActive(control)
)
}
private func keyContent(for control: ControllerElement) -> KeyContent {
let symbol = control.symbol
return symbol.isEmpty ? .text(control.displayName) : .symbol(symbol)
}
private func attach(to window: UIWindow) {
if containerView.superview !== window {
NSLayoutConstraint.deactivate(windowConstraints)
windowConstraints.removeAll()
removeFromSuperview()
containerView.removeFromSuperview()
window.addSubview(containerView)
containerView.addSubview(self)
let trailingConstraint = containerView.trailingAnchor.constraint(equalTo: window.safeAreaLayoutGuide.trailingAnchor)
self.trailingConstraint = trailingConstraint
windowConstraints = [
containerView.widthAnchor.constraint(equalToConstant: Self.hudWidth * Self.hudScale),
containerView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: Self.hudScale),
trailingConstraint,
containerView.bottomAnchor.constraint(equalTo: window.safeAreaLayoutGuide.bottomAnchor, constant: -Self.edgeMargin),
widthAnchor.constraint(equalToConstant: Self.hudWidth),
centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
centerYAnchor.constraint(equalTo: containerView.centerYAnchor)
]
NSLayoutConstraint.activate(windowConstraints)
}
updateTrailingOffset(for: window)
window.bringSubviewToFront(containerView)
}
private func detachFromWindow() {
NSLayoutConstraint.deactivate(windowConstraints)
windowConstraints.removeAll()
trailingConstraint = nil
removeFromSuperview()
containerView.removeFromSuperview()
}
private func updateTrailingOffset(for window: UIWindow) {
let horizontalMargin = Self.edgeMargin + (PublicUtils.isIPhone ? 0 : window.safeAreaInsets.bottom)
trailingConstraint?.constant = -horizontalMargin
}
private func makeRow(for hint: Hint) -> UIStackView {
let keyView = makeKeyView(for: hint.keyContent)
let titleView = makeTitleView(for: hint.titles)
let rowStackView = UIStackView(arrangedSubviews: [keyView, titleView])
rowStackView.axis = .horizontal
rowStackView.alignment = .center
rowStackView.spacing = 8
return rowStackView
}
private func makeTitleView(for titles: [String]) -> UIView {
if titles.count == 2 {
let stackView = UIStackView(arrangedSubviews: titles.map { makeTitleLabel(text: $0, isMergedTitle: true) })
stackView.axis = .vertical
stackView.alignment = .leading
stackView.spacing = 1
stackView.setContentCompressionResistancePriority(.required, for: .horizontal)
return stackView
}
return makeTitleLabel(text: titles.first ?? "", isMergedTitle: false)
}
private func makeTitleLabel(text: String, isMergedTitle: Bool) -> UILabel {
let titleLabel = UILabel()
titleLabel.text = text
titleLabel.font = UIFont.roundedSystemFont(ofSize: isMergedTitle ? 12.5 : 14, weight: .medium)
titleLabel.adjustsFontSizeToFitWidth = true
titleLabel.minimumScaleFactor = isMergedTitle ? 0.55 : 0.2
titleLabel.numberOfLines = 1
titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
return titleLabel
}
private func updateTitleColors(in view: UIView, color: UIColor) {
if let label = view as? UILabel {
label.textColor = color
return
}
view.subviews.forEach { updateTitleColors(in: $0, color: color) }
}
private func makeKeyView(for content: KeyContent) -> UIView {
let keyContainerView = UIView()
keyContainerView.translatesAutoresizingMaskIntoConstraints = false
keyContainerView.backgroundColor = .clear
NSLayoutConstraint.activate([
keyContainerView.widthAnchor.constraint(equalToConstant: 50),
keyContainerView.heightAnchor.constraint(equalToConstant: PublicUtils.isIPhone ? 38 : 42)
])
let borderView = UIView()
borderView.tag = Self.keyBorderViewTag
borderView.translatesAutoresizingMaskIntoConstraints = false
borderView.isUserInteractionEnabled = false
borderView.backgroundColor = .clear
borderView.layer.cornerRadius = 13
borderView.layer.cornerCurve = .continuous
borderView.layer.borderWidth = 1
borderView.layer.masksToBounds = true
keyContainerView.addSubview(borderView)
let keyView = UIView()
keyView.tag = Self.keyContentViewTag
keyView.translatesAutoresizingMaskIntoConstraints = false
keyView.layer.cornerRadius = 12
keyView.layer.cornerCurve = .continuous
keyView.layer.masksToBounds = true
keyContainerView.addSubview(keyView)
NSLayoutConstraint.activate([
borderView.leadingAnchor.constraint(equalTo: keyContainerView.leadingAnchor),
borderView.trailingAnchor.constraint(equalTo: keyContainerView.trailingAnchor),
borderView.topAnchor.constraint(equalTo: keyContainerView.topAnchor),
borderView.bottomAnchor.constraint(equalTo: keyContainerView.bottomAnchor),
keyView.leadingAnchor.constraint(equalTo: keyContainerView.leadingAnchor, constant: 1),
keyView.trailingAnchor.constraint(equalTo: keyContainerView.trailingAnchor, constant: -1),
keyView.topAnchor.constraint(equalTo: keyContainerView.topAnchor, constant: 1),
keyView.bottomAnchor.constraint(equalTo: keyContainerView.bottomAnchor, constant: -1)
])
switch content {
case .text(let text):
let keyLabel = InsetLabel()
keyLabel.contentInsets = UIEdgeInsets(top: 0, left: 6, bottom: 0, right: 6)
keyLabel.translatesAutoresizingMaskIntoConstraints = false
keyLabel.text = text
keyLabel.textAlignment = .center
keyLabel.font = UIFont.roundedSystemFont(ofSize: 12, weight: PublicUtils.isIPhone ? .semibold : .medium)
keyLabel.adjustsFontSizeToFitWidth = true
keyLabel.minimumScaleFactor = 0.2
keyLabel.numberOfLines = 1
keyLabel.lineBreakMode = .byClipping
keyView.addSubview(keyLabel)
NSLayoutConstraint.activate([
keyLabel.leadingAnchor.constraint(equalTo: keyView.leadingAnchor),
keyLabel.trailingAnchor.constraint(equalTo: keyView.trailingAnchor),
keyLabel.topAnchor.constraint(equalTo: keyView.topAnchor),
keyLabel.bottomAnchor.constraint(equalTo: keyView.bottomAnchor)
])
case .symbol(let symbolName):
let imageView = UIImageView(image: UIImage(systemName: symbolName))
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
imageView.preferredSymbolConfiguration = UIImage.SymbolConfiguration(pointSize: 18, weight: .medium)
keyView.addSubview(imageView)
NSLayoutConstraint.activate([
imageView.centerXAnchor.constraint(equalTo: keyView.centerXAnchor),
imageView.centerYAnchor.constraint(equalTo: keyView.centerYAnchor),
imageView.widthAnchor.constraint(lessThanOrEqualTo: keyView.widthAnchor, multiplier: 0.58),
imageView.heightAnchor.constraint(lessThanOrEqualTo: keyView.heightAnchor, multiplier: 0.58)
])
}
return keyContainerView
}
private func observeThemeChanges() {
themeObserver = NotificationCenter.default.addObserver(
forName: Notification.Name(ThemeManager.ThemeDidChangeNotification),
object: nil,
queue: .main
) { [weak self] _ in
self?.updateTheme()
}
}
private static func keyWindow() -> UIWindow? {
let sceneWindows = UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
.flatMap { $0.windows }
return sceneWindows.first(where: { $0.isKeyWindow })
?? sceneWindows.first
?? UIApplication.shared.windows.first(where: { $0.isKeyWindow })
?? UIApplication.shared.windows.first
}
private static func isControlActive(_ control: ControllerElement) -> Bool {
!activeControls(forHintControl: control).isDisjoint(with: activeControls)
}
private static func requestActionStateEnd(for control: ControllerElement) {
pendingActionStateWorkItems[control]?.cancel()
let now = CACurrentMediaTime()
let remainingDuration = max((actionStateMinimumEndTimes[control] ?? now) - now, 0)
guard remainingDuration > 0 else {
finishActionStateEnd(for: control)
return
}
let workItem = DispatchWorkItem {
finishActionStateEnd(for: control)
}
pendingActionStateWorkItems[control] = workItem
DispatchQueue.main.asyncAfter(deadline: .now() + remainingDuration, execute: workItem)
}
private static func finishActionStateEnd(for control: ControllerElement) {
pendingActionStateWorkItems[control]?.cancel()
pendingActionStateWorkItems[control] = nil
actionStateMinimumEndTimes[control] = nil
guard activeControls.remove(control) != nil else { return }
currentHud?.refreshActionStates()
}
private static func requestHudDetachKeepingMinimumActionDuration() {
pendingClearHudWorkItem?.cancel()
let remainingDuration = maximumRemainingActionStateDuration()
guard remainingDuration > 0 else {
finishHudDetach()
return
}
let workItem = DispatchWorkItem {
finishHudDetach()
}
pendingClearHudWorkItem = workItem
DispatchQueue.main.asyncAfter(deadline: .now() + remainingDuration, execute: workItem)
}
private static func finishHudDetach() {
pendingClearHudWorkItem?.cancel()
pendingClearHudWorkItem = nil
pendingActionStateWorkItems.values.forEach { $0.cancel() }
pendingActionStateWorkItems.removeAll()
actionStateMinimumEndTimes.removeAll()
activeControls.removeAll()
currentHud?.detachFromWindow()
currentHud = nil
}
private static func maximumRemainingActionStateDuration() -> CFTimeInterval {
let now = CACurrentMediaTime()
return activeControls.reduce(0) { partialResult, control in
max(partialResult, max((actionStateMinimumEndTimes[control] ?? now) - now, 0))
}
}
private static func activeControls(forHintControl control: ControllerElement) -> Set<ControllerElement> {
switch control {
case .leftStick, .leftStickX, .leftStickY:
return [.leftStick, .leftStickX, .leftStickY]
case .rightStick, .rightStickX, .rightStickY:
return [.rightStick, .rightStickX, .rightStickY]
case .dpad:
return [.dpad, .dpadUp, .dpadDown, .dpadLeft, .dpadRight]
case .abxy:
return [.abxy, .a, .b, .x, .y]
default:
return [control]
}
}
}
final class InsetLabel: UILabel {
var contentInsets = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 4)
override func drawText(in rect: CGRect) {
super.drawText(in: rect.inset(by: contentInsets))
}
override var intrinsicContentSize: CGSize {
let size = super.intrinsicContentSize
return CGSize(
width: size.width + contentInsets.left + contentInsets.right,
height: size.height + contentInsets.top + contentInsets.bottom
)
}
}
-33
View File
@@ -1,33 +0,0 @@
//
// HostCardView.h
// VoidLink
//
// Created by True砖家 on 5/18/25.
// Copyright © 2025 True砖家 @ Bilibili. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "TemporaryHost.h"
@protocol HostCardActionDelegate <NSObject>
- (void)appButtonTappedForHost:(TemporaryHost *)host;
- (void)launchButtonTappedForHost:(TemporaryHost *)host;
- (void)wakeupButtonTappedForHost:(TemporaryHost *)host;
- (void)pairButtonTappedForHost:(TemporaryHost *)host;
- (void)hostCardLongPressed:(TemporaryHost *)host view:(UIView *)view;
- (bool)isStreaming;
@end
@interface HostCardView : UIView
@property (nonatomic, assign) CGFloat sizeFactor;
@property (nonatomic, readonly) CGSize size;
@property (nonatomic, weak) id<HostCardActionDelegate> delegate; // Delegate property
- (void)resizeBySizeFactor:(CGFloat)factor;
- (id) initWithHost:(TemporaryHost*)host;
- (id) initWithHost:(TemporaryHost*)host andSizeFactor:(CGFloat)sizeFactor;
@end
-640
View File
@@ -1,640 +0,0 @@
//
// HostCardView.m
// VoidLink
//
// Created by True砖家 on 5/18/25.
// Copyright © 2025 True砖家 @ Bilibili. All rights reserved.
//
#import "HostCardView.h"
#import "LocalizationHelper.h"
#import "ThemeManager.h"
#import "FixedTintImageView.h"
@interface HostCardView ()
@property (nonatomic, strong) UIView *iconBackgroundView;
@property (nonatomic, strong) UIImageView *hostIconView;
@property (nonatomic, strong) UILabel *hostNameLabel;
@property (nonatomic, strong) UIImageView *statusIcon;
@property (nonatomic, strong) UILabel *statusLabel;
@property (nonatomic, strong) UIButton *appButton;
@property (nonatomic, strong) UIButton *launchButton;
@property (nonatomic, strong) UIButton *pairButton;
@property (nonatomic, strong) UIButton *wakeupButton;
@property (nonatomic, strong) UIButton *transparentButton;
@property (nonatomic, strong) NSLayoutConstraint *widthConstraint;
@property (nonatomic, strong) NSLayoutConstraint *heightConstraint;
@property (nonatomic, strong) UIView *separatorLine;
@property (nonatomic, assign) CGFloat cardContentpadding;
@property (nonatomic, assign) CGSize size;
@end
@implementation HostCardView {
TemporaryHost* _host;
UIActivityIndicatorView* _hostSpinner;
UIImageView* lockIconView;
CGFloat computerIconMonitorCenterYOffset;
CGFloat buttonHeight;
CGFloat buttonLabelFontSize;
CGFloat iconAndButtonSpacing;
UIColor *defaultBlue;
UIColor *defaultGreen;
CAGradientLayer *backgroundLayer;
bool longPressFired;
}
static const float REFRESH_CYCLE = 2.0f;
- (BOOL)isIPhone{
return ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone);
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
buttonLabelFontSize = 15*_sizeFactor;
longPressFired = false;
computerIconMonitorCenterYOffset = [self isIPhone] ? -2.75*_sizeFactor : -3.2*_sizeFactor;
iconAndButtonSpacing = 37*_sizeFactor;
buttonHeight = 39*_sizeFactor;
defaultBlue = [ThemeManager appPrimaryColor];
defaultGreen = [UIColor colorWithRed:52.0/255.0 green:199.0/255.0 blue:89.0/255.0 alpha:1.0];
// self.userIterfaceStyle = UIUserInterfaceStyleLight
UILongPressGestureRecognizer* longPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(hostCardLongPressed:)];
[self addGestureRecognizer:longPressRecognizer];
[self createBackgroundLayer];
[self setupUI];
}
return self;
}
- (id) initWithHost:(TemporaryHost*)host {
self.sizeFactor = 1.0;
self = [self init];
_host = host;
// Use UIContextMenuInteraction on iOS 13.0+ and a standard UILongPressGestureRecognizer
// for tvOS devices and iOS prior to 13.0.
return self;
}
- (id) initWithHost:(TemporaryHost*)host andSizeFactor:(CGFloat)sizeFactor {
_sizeFactor = sizeFactor;
self = [self init];
_host = host;
// Use UIContextMenuInteraction on iOS 13.0+ and a standard UILongPressGestureRecognizer
// for tvOS devices and iOS prior to 13.0.
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
[self setupUI];
}
return self;
}
- (void)appButtonTapped{
if(longPressFired){
longPressFired = false;
return;
}
if ([self.delegate respondsToSelector:@selector(appButtonTappedForHost:)]) {
[self.delegate appButtonTappedForHost:_host];
} else NSLog(@"Delegate not set or does not respond to appButtonTappedForHost:");
}
- (void)launchButtonTapped{
if(longPressFired){
longPressFired = false;
return;
}
if ([self.delegate respondsToSelector:@selector(launchButtonTappedForHost:)]) {
[self.delegate launchButtonTappedForHost:_host];
} else NSLog(@"Delegate not set or does not respond to launchButtonTappedForHost:");
}
- (void)wakeupButtonTapped{
if(longPressFired){
longPressFired = false;
return;
}
if ([self.delegate respondsToSelector:@selector(wakeupButtonTappedForHost:)]) {
[self.delegate wakeupButtonTappedForHost:_host];
} else NSLog(@"Delegate not set or does not respond to launchButtonTappedForHost:");
}
-(void)pairButtonTapped{
if(longPressFired){
longPressFired = false;
return;
}
if ([self.delegate respondsToSelector:@selector(pairButtonTappedForHost:)]) {
[self.delegate pairButtonTappedForHost:_host];
} else NSLog(@"Delegate not set or does not respond to pairButtonTappedForHost:");
}
- (void)hostCardLongPressed:(UILongPressGestureRecognizer* )gesture{
if(gesture.state != UIGestureRecognizerStateBegan) return;
longPressFired = true;
if ([self.delegate respondsToSelector:@selector(hostCardLongPressed:view:)]) {
[self.delegate hostCardLongPressed:_host view:self];
} else NSLog(@"Delegate not set or does not respond");
}
- (void)resizeBySizeFactor:(CGFloat)factor{
[self.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
self.sizeFactor = factor;
[self setupUI];
}
- (void)setupUI {
self.userInteractionEnabled = YES;
self.backgroundColor = [ThemeManager widgetBackgroundColor]; // theme
self.cardContentpadding = 13 * _sizeFactor;
self.clipsToBounds = YES;
self.translatesAutoresizingMaskIntoConstraints = NO;
CGFloat appButtonWidth = 100*_sizeFactor;
CGFloat launchButtonWidth = 120*_sizeFactor;
CGFloat cardWidth = _cardContentpadding*2 + appButtonWidth + 15*_sizeFactor + launchButtonWidth;
self.layer.cornerRadius = 2*(uint16_t)(cardWidth*0.0603/2) ;
/*
NSLayoutConstraint *topAnchorConstraint;
topAnchorConstraint = [self.topAnchor constraintEqualToAnchor:self.superview.topAnchor constant:500];
topAnchorConstraint.active = YES;
*/
_heightConstraint = [self.heightAnchor constraintEqualToConstant:300];
_heightConstraint.active = YES;
_widthConstraint = [self.widthAnchor constraintEqualToConstant:300];
_widthConstraint.active = YES;
// 图标背景
self.iconBackgroundView = [[UIView alloc] initWithFrame:CGRectMake(_cardContentpadding, _cardContentpadding, 80*_sizeFactor, 80*_sizeFactor)];
self.iconBackgroundView.backgroundColor = defaultBlue;
self.iconBackgroundView.layer.cornerRadius = 2*(uint16_t)(20*_sizeFactor/2);
[self addSubview:self.iconBackgroundView];
// 图标图片
self.hostIconView = [[UIImageView alloc] init];
self.hostIconView.translatesAutoresizingMaskIntoConstraints = NO;
self.hostIconView.contentMode = UIViewContentModeScaleAspectFit;
if (@available(iOS 13.0, *)) {
self.hostIconView.image = [[UIImage imageNamed:@"display"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
} else {
self.hostIconView.image = [UIImage imageNamed:@"Computer"];
[NSLayoutConstraint activateConstraints:@[
[self.hostIconView.heightAnchor constraintEqualToConstant:57*_sizeFactor],
[self.hostIconView.widthAnchor constraintEqualToConstant:57*_sizeFactor],
]];
}
self.hostIconView.tintColor = [UIColor whiteColor];
[self.iconBackgroundView addSubview:self.hostIconView];
[NSLayoutConstraint activateConstraints:@[
[self.hostIconView.centerXAnchor constraintEqualToAnchor:self.iconBackgroundView.centerXAnchor constant:0],
[self.hostIconView.centerYAnchor constraintEqualToAnchor:self.iconBackgroundView.centerYAnchor constant:0],
[self.hostIconView.heightAnchor constraintEqualToConstant:53.9*_sizeFactor],
[self.hostIconView.widthAnchor constraintEqualToConstant:53.9*_sizeFactor],
]];
// [self.iconBackgroundView layoutIfNeeded];
// [self.iconImageView layoutIfNeeded];
//spinner
bool oldiOS = false;
if (@available(iOS 13.0, *)){}
else oldiOS = true;
_hostSpinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];
_hostSpinner.userInteractionEnabled = NO;
_hostSpinner.translatesAutoresizingMaskIntoConstraints = NO;
_hostSpinner.hidesWhenStopped = YES;
[self.iconBackgroundView addSubview:_hostSpinner];
[NSLayoutConstraint activateConstraints:@[
[_hostSpinner.centerXAnchor constraintEqualToAnchor:self.iconBackgroundView.centerXAnchor constant:0],
[_hostSpinner.centerYAnchor constraintEqualToAnchor:self.iconBackgroundView.centerYAnchor constant:oldiOS ? -5.5*_sizeFactor : computerIconMonitorCenterYOffset],
]];
_hostSpinner.transform = CGAffineTransformMakeScale(_sizeFactor, _sizeFactor);
[_hostSpinner stopAnimating];
// [_hostSpinner startAnimating];
// lockIcon
lockIconView =[[UIImageView alloc] init];
lockIconView.translatesAutoresizingMaskIntoConstraints = NO;
lockIconView.contentMode = UIViewContentModeScaleAspectFit;
if (@available(iOS 13.0, *)) {
lockIconView.image = [[UIImage imageNamed:@"lock.fill"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
} else {
lockIconView.image = [UIImage imageNamed:@"LockedOverlayIcon"];
computerIconMonitorCenterYOffset = -5 * _sizeFactor;
}
lockIconView.tintColor = [UIColor whiteColor];
[self.iconBackgroundView insertSubview:lockIconView aboveSubview:_iconBackgroundView];
[NSLayoutConstraint activateConstraints:@[
[lockIconView.centerXAnchor constraintEqualToAnchor:_iconBackgroundView.centerXAnchor constant:0],
[lockIconView.centerYAnchor constraintEqualToAnchor:_iconBackgroundView.centerYAnchor constant:computerIconMonitorCenterYOffset],
[lockIconView.widthAnchor constraintEqualToConstant:17.05*_sizeFactor],
[lockIconView.heightAnchor constraintEqualToConstant:17.05*_sizeFactor]
]];
lockIconView.hidden = true;
// 设备名
CGFloat hostNameLabelCoordX = _cardContentpadding+_iconBackgroundView.frame.size.width+16*_sizeFactor;
self.hostNameLabel = [[UILabel alloc] initWithFrame:CGRectMake(hostNameLabelCoordX, _cardContentpadding+_iconBackgroundView.frame.size.height*0.02, cardWidth-hostNameLabelCoordX-_cardContentpadding, 30*_sizeFactor)];
self.hostNameLabel.numberOfLines = 1;
self.hostNameLabel.adjustsFontSizeToFitWidth = YES;
self.hostNameLabel.minimumScaleFactor = 0.8; // 最小字体缩放比例
self.hostNameLabel.lineBreakMode = NSLineBreakByTruncatingTail;
self.hostIconView.translatesAutoresizingMaskIntoConstraints = NO;
self.hostNameLabel.text = @"RazerBlade 16";
self.hostNameLabel.textColor = [UIColor whiteColor]; //theme
self.hostNameLabel.font = [UIFont boldSystemFontOfSize:18*_sizeFactor];
[self addSubview:self.hostNameLabel];
// 在线文字
self.statusLabel = [[UILabel alloc] initWithFrame:CGRectMake(205, 68, 100, 24)];
self.statusLabel.translatesAutoresizingMaskIntoConstraints = NO;
self.statusLabel.text = [LocalizationHelper localizedStringForKey:@"Online"];
self.statusLabel.font = [UIFont systemFontOfSize:14*_sizeFactor weight:UIFontWeightMedium];
self.statusLabel.textColor = defaultGreen;
// self.statusLabel.font = [UIFont systemFontOfSize:16*_sizeFactor];
// 在线状态图标
self.statusIcon = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 16*_sizeFactor, 16*_sizeFactor)];
self.statusIcon.contentMode = UIViewContentModeScaleAspectFit;
self.statusIcon.translatesAutoresizingMaskIntoConstraints = NO;
self.statusIcon.image = [[UIImage imageNamed:@"wifi_green"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
// self.statusIcon.tintColor = defaultGreen;
[self addSubview:self.statusIcon];
[self addSubview:self.statusLabel];
[NSLayoutConstraint activateConstraints:@[
[self.statusLabel.leadingAnchor constraintEqualToAnchor:self.statusIcon.trailingAnchor constant:3*_sizeFactor],
[self.statusLabel.topAnchor constraintEqualToAnchor:self.hostNameLabel.bottomAnchor constant:0*_sizeFactor],
[self.statusIcon.centerYAnchor constraintEqualToAnchor:self.statusLabel.centerYAnchor constant:0],
[self.statusIcon.leadingAnchor constraintEqualToAnchor:self.hostNameLabel.leadingAnchor],
[self.statusIcon.widthAnchor constraintEqualToConstant:16*_sizeFactor]
]];
// 启动应用按钮
self.appButton = [UIButton buttonWithType:UIButtonTypeSystem];
self.appButton.translatesAutoresizingMaskIntoConstraints = NO;
self.appButton.frame = CGRectMake(20, 200, 150, buttonHeight);
[self.appButton setTitle:[LocalizationHelper localizedStringForKey:@"Applications"] forState:UIControlStateNormal];
[self.appButton setTitleColor:[ThemeManager textColorGray] forState:UIControlStateNormal]; //theme
self.appButton.titleLabel.font = [UIFont systemFontOfSize:buttonLabelFontSize];
[self.appButton addTarget:self action:@selector(appButtonTapped) forControlEvents:UIControlEventPrimaryActionTriggered];
[self addSubview:self.appButton];
[NSLayoutConstraint activateConstraints:@[
[self.appButton.leadingAnchor constraintEqualToAnchor:self.iconBackgroundView.leadingAnchor constant:0],
[self.appButton.topAnchor constraintEqualToAnchor:self.iconBackgroundView.bottomAnchor constant:iconAndButtonSpacing],
[self.appButton.widthAnchor constraintEqualToConstant:appButtonWidth],
[self.appButton.heightAnchor constraintEqualToConstant:buttonHeight],
// [self.launchButton.titleLabel.leadingAnchor constraintEqualToAnchor:self.launchButton.imageView.trailingAnchor constant:3*_sizeFactor]
]];
// 开始串流按钮
self.launchButton = [UIButton buttonWithType:UIButtonTypeSystem];
self.launchButton.translatesAutoresizingMaskIntoConstraints = NO;
self.launchButton.frame = CGRectMake(0, 0, 150, 50);
self.launchButton.backgroundColor = defaultBlue;
self.launchButton.layer.cornerRadius = 2*(uint16_t)(cardWidth*0.0377/2);
[self.launchButton setTitle:[LocalizationHelper localizedStringForKey:@" Launch"] forState:UIControlStateNormal];
[self.launchButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; // theme
self.launchButton.titleLabel.font = [UIFont boldSystemFontOfSize:buttonLabelFontSize];
self.launchButton.tintColor = [UIColor whiteColor];
[self.launchButton addTarget:self action:@selector(launchButtonTapped) forControlEvents:UIControlEventPrimaryActionTriggered];
[self addSubview:self.launchButton];
if (@available(iOS 13.0, *)) {
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:buttonHeight*0.263];
[self.launchButton setImage:[UIImage systemImageNamed:@"play.fill" withConfiguration:config] forState:UIControlStateNormal];
} else {
// Fallback on earlier versions
}
[NSLayoutConstraint activateConstraints:@[
[self.launchButton.leadingAnchor constraintEqualToAnchor:self.appButton.trailingAnchor constant:15*_sizeFactor],
[self.launchButton.centerYAnchor constraintEqualToAnchor:self.appButton.centerYAnchor constant:0],
[self.launchButton.widthAnchor constraintEqualToConstant:launchButtonWidth],
[self.launchButton.heightAnchor constraintEqualToConstant:buttonHeight],
// [self.launchButton.titleLabel.leadingAnchor constraintEqualToAnchor:self.launchButton.imageView.trailingAnchor constant:3*_sizeFactor]
]];
// 配对按钮
self.pairButton = [UIButton buttonWithType:UIButtonTypeSystem];
self.pairButton.translatesAutoresizingMaskIntoConstraints = NO;
self.pairButton.frame = CGRectMake(0, 0, 150, 50);
self.pairButton.backgroundColor = [ThemeManager textTintColorWithAlpha];
self.pairButton.layer.cornerRadius = 2*(uint16_t)(cardWidth*0.0377/2);
[self.pairButton setTitle:[LocalizationHelper localizedStringForKey:@" Pair with PIN"] forState:UIControlStateNormal];
[self.pairButton setTitleColor:defaultBlue forState:UIControlStateNormal]; // theme
self.pairButton.titleLabel.font = [UIFont boldSystemFontOfSize:buttonLabelFontSize];
if (@available(iOS 13.0, *)) {
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:buttonHeight/3.4 weight:UIImageSymbolWeightBold];
UIImage *templateImage = [UIImage systemImageNamed:@"lock.open.fill" withConfiguration:config];
UIImage *coloredImage = [templateImage imageWithTintColor:defaultBlue renderingMode:UIImageRenderingModeAlwaysOriginal];
[self.pairButton setImage:coloredImage forState:UIControlStateNormal];
} else {
// Fallback on earlier versions
}
self.pairButton.backgroundColor = [ThemeManager textTintColorWithAlpha];
[self.pairButton setTitleColor:defaultBlue forState:UIControlStateNormal]; // theme
[self.pairButton addTarget:self action:@selector(pairButtonTapped) forControlEvents:UIControlEventPrimaryActionTriggered];
[self addSubview:self.pairButton];
[NSLayoutConstraint activateConstraints:@[
[self.pairButton.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:_cardContentpadding],
[self.pairButton.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-_cardContentpadding],
[self.pairButton.topAnchor constraintEqualToAnchor:self.iconBackgroundView.bottomAnchor constant:iconAndButtonSpacing],
// [self.pairButton.widthAnchor constraintEqualToConstant:launchButtonWidth],
[self.pairButton.heightAnchor constraintEqualToConstant:buttonHeight],
]];
// 唤醒按钮
self.wakeupButton = [UIButton buttonWithType:UIButtonTypeSystem];
self.wakeupButton.translatesAutoresizingMaskIntoConstraints = NO;
self.wakeupButton.frame = CGRectMake(0, 0, 150, 50);
self.wakeupButton.backgroundColor = [ThemeManager textTintColorWithAlpha];
self.wakeupButton.layer.cornerRadius = 2*(uint16_t)(cardWidth*0.0377/2);
[self.wakeupButton setTitle:[LocalizationHelper localizedStringForKey:@" Wake-on-LAN"] forState:UIControlStateNormal];
[self.wakeupButton setTitleColor:defaultBlue forState:UIControlStateNormal]; // theme
self.wakeupButton.titleLabel.font = [UIFont boldSystemFontOfSize:buttonLabelFontSize];
[self.wakeupButton addTarget:self action:@selector(wakeupButtonTapped) forControlEvents:UIControlEventPrimaryActionTriggered];
[self addSubview:self.wakeupButton];
[NSLayoutConstraint activateConstraints:@[
[self.wakeupButton.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:_cardContentpadding],
[self.wakeupButton.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-_cardContentpadding],
[self.wakeupButton.topAnchor constraintEqualToAnchor:self.iconBackgroundView.bottomAnchor constant:iconAndButtonSpacing],
// [self.pairButton.widthAnchor constraintEqualToConstant:launchButtonWidth],
[self.wakeupButton.heightAnchor constraintEqualToConstant:buttonHeight],
]];
//分隔线
_separatorLine = [[UIView alloc] init];
_separatorLine.backgroundColor = [UIColor colorWithWhite:0.3 alpha:5.0];
_separatorLine.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:_separatorLine];
[NSLayoutConstraint activateConstraints:@[
[_separatorLine.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:_cardContentpadding],
[_separatorLine.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-_cardContentpadding],
[_separatorLine.centerYAnchor constraintEqualToAnchor:_iconBackgroundView.bottomAnchor constant: iconAndButtonSpacing/2],
[_separatorLine.heightAnchor constraintEqualToConstant:1.0/UIScreen.mainScreen.scale]
]];
// 透明按钮
self.transparentButton = [UIButton buttonWithType:UIButtonTypeCustom];
// self.transparentButton.frame = CGRectMake(50, 100, 100, 100); // 设置点击区域
self.transparentButton.translatesAutoresizingMaskIntoConstraints = NO;
// 设置透明背景和无内容
self.transparentButton.backgroundColor = [UIColor clearColor];
[self.transparentButton setTitle:@"" forState:UIControlStateNormal];
[self.transparentButton setImage:nil forState:UIControlStateNormal];
[self.transparentButton addTarget:self action:@selector(appButtonTapped) forControlEvents:UIControlEventPrimaryActionTriggered];
[self addSubview:self.transparentButton];
[NSLayoutConstraint activateConstraints:@[
[self.transparentButton.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:0],
[self.transparentButton.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:0],
[self.transparentButton.topAnchor constraintEqualToAnchor:self.topAnchor constant:0],
[self.transparentButton.bottomAnchor constraintEqualToAnchor:self.separatorLine.topAnchor],
]];
_widthConstraint.constant = _cardContentpadding*2 + appButtonWidth + 15*_sizeFactor + launchButtonWidth;
_heightConstraint.constant = _cardContentpadding*2 + _iconBackgroundView.frame.size.height + iconAndButtonSpacing + buttonHeight + 1;
_size = CGSizeMake(_widthConstraint.constant, _heightConstraint.constant);
[self updateTheme:ThemeManager.userInterfaceStyle];
}
- (void)createBackgroundLayer{
backgroundLayer = [CAGradientLayer layer];
UIColor *gradientColorDark = [UIColor colorWithRed:0.0 green:0.319 blue:0.64 alpha:1.0];
UIColor *gradientColorLight = [gradientColorDark colorWithAlphaComponent:0.52];
CGColorRef gradientColorRef = ThemeManager.userInterfaceStyle == UIUserInterfaceStyleDark ? gradientColorDark.CGColor : gradientColorLight.CGColor;
backgroundLayer.colors = @[
(__bridge id)[UIColor clearColor].CGColor,
(__bridge id)[UIColor clearColor].CGColor,
(__bridge id)[UIColor clearColor].CGColor,
(__bridge id)gradientColorRef
];
backgroundLayer.locations = @[@0, @0.18, @0.5, @1];
backgroundLayer.startPoint = CGPointMake(0.25, 0.5);
backgroundLayer.endPoint = CGPointMake(0.75, 0.5);
CGAffineTransform transform = CGAffineTransformMake(-1.01, -1, 1, -3.67, 0.5, 2.83);
backgroundLayer.transform = CATransform3DMakeAffineTransform(transform);
[self.layer insertSublayer:backgroundLayer atIndex:0];
}
- (void)layoutSubviews {
[super layoutSubviews];
backgroundLayer.bounds = CGRectInset(self.bounds, -0.5 * self.bounds.size.width, -0.5 * self.bounds.size.height);
backgroundLayer.position = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
[self updateContentsForHost:_host]; // quick update before looping
}
- (void)tintAdjustmentModeDidChange {
// [super tintAdjustmentModeDidChange];
NSLog(@"tintChanged........");
self.tintAdjustmentMode = UIViewTintAdjustmentModeNormal;
}
- (void)updateTheme:(UIUserInterfaceStyle)userIterfaceStyle{
self.backgroundColor = [ThemeManager widgetBackgroundColor];
_hostNameLabel.textColor = [ThemeManager textColor];
[_appButton setTitleColor:[ThemeManager appPrimaryColor] forState:UIControlStateNormal];
_separatorLine.backgroundColor = [ThemeManager separatorColor];
backgroundLayer.hidden = NO;
[self updateContentsForHost:_host];
}
- (void)didMoveToSuperview {
// Start our update loop when we are added to our cell
if (self.superview != nil && _host != nil) {
NSLog(@"start update loop");
[self updateLoop];
}
}
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
/*
[super traitCollectionDidChange:previousTraitCollection];
if (@available(iOS 13.0, *)) {
if ([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) {
[self updateTheme:self.traitCollection.userInterfaceStyle];
}
} else {
[self updateTheme:UIUserInterfaceStyleDark];
}*/
}
- (bool)isDarkTheme{
return self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark;
}
- (void) updateLoop {
// Stop immediately if the view has been detached
if (self.superview == nil || [self.delegate isStreaming]) {
return;
}
NSLog(@"hostCard update loop %f", CACurrentMediaTime());
[self updateContentsForHost:_host];
// Queue the next refresh cycle
[self performSelector:@selector(updateLoop) withObject:self afterDelay:REFRESH_CYCLE];
}
- (void) updateContentsForHost:(TemporaryHost*)host {
_hostNameLabel.text = host.name;
backgroundLayer.hidden = !(host.state == StateOnline && host.pairState == PairStatePaired);
BOOL hostPaired = host.pairState == PairStatePaired;
bool hasValidMac = host.mac != nil && ![host.mac isEqualToString:@"00:00:00:00:00:00"];
switch (host.state) {
case StateOnline:
[_hostSpinner stopAnimating];
_statusLabel.textColor = defaultGreen;
_statusLabel.text = [LocalizationHelper localizedStringForKey:@"Online"];
self.statusIcon.image = [[UIImage imageNamed:@"wifi_green"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
self.statusIcon.hidden = NO;
_appButton.titleLabel.font = [UIFont systemFontOfSize:buttonLabelFontSize];
if(host.pairState == PairStatePaired){
_hostIconView.tintColor = [UIColor whiteColor];
_iconBackgroundView.backgroundColor = defaultBlue;
lockIconView.hidden = YES;
[_appButton setTitle:[LocalizationHelper localizedStringForKey:@"Applications"] forState:UIControlStateNormal];
[_launchButton setTitle:[LocalizationHelper localizedStringForKey:@" Launch"] forState:UIControlStateNormal];
[_appButton setEnabled:YES];
[_launchButton setEnabled:YES];
_appButton.hidden = NO;
_launchButton.hidden = NO;
_pairButton.hidden = YES;
_wakeupButton.hidden = YES;
[_appButton setTitleColor:defaultBlue forState:UIControlStateNormal]; // theme
if (@available(iOS 13.0, *)) {
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:buttonHeight*0.263];
[self.launchButton setImage:[UIImage systemImageNamed:@"play.fill" withConfiguration:config] forState:UIControlStateNormal];
} else {
}
_launchButton.backgroundColor = defaultBlue;
[_launchButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; // theme
}
else {
_iconBackgroundView.backgroundColor = [ThemeManager appPrimaryColorWithAlpha];
UIColor* iconColor = [[UIColor whiteColor] colorWithAlphaComponent:[self isDarkTheme] ? 0.63 : 1];
_hostIconView.tintColor = iconColor;
lockIconView.tintColor = iconColor;
lockIconView.hidden = NO;
_appButton.hidden = YES;
_launchButton.hidden = YES;
_pairButton.hidden = NO;
_wakeupButton.hidden = YES;
}
break;
case StateOffline:
[_hostSpinner stopAnimating];
// _iconBackgroundView.backgroundColor = _userIterfaceStyle == UIUserInterfaceStyleDark ? [UIColor appBackgroundColorDark] : [UIColor appBackgroundColorLight];
_iconBackgroundView.backgroundColor = [ThemeManager appBackgroundColor];
_statusLabel.textColor = [ThemeManager textColorGray];
_statusLabel.text = [LocalizationHelper localizedStringForKey: @"Offline"];
_statusIcon.tintColor = [ThemeManager textColorGray];
if (@available(iOS 13.0, *)) {
_statusIcon.image = [UIImage systemImageNamed:@"exclamationmark.triangle.fill"];
} else {
self.statusIcon.hidden = YES;
}
_hostIconView.tintColor = [ThemeManager lowProfileGray];
lockIconView.tintColor = [ThemeManager lowProfileGray];
lockIconView.hidden = hostPaired;
_appButton.hidden = YES;
_launchButton.hidden = YES;
_pairButton.hidden = YES;
_wakeupButton.hidden = NO;
if (@available(iOS 13.0, *)) {
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:buttonHeight/3.45 weight:UIImageSymbolWeightBold];
UIImage *templateImage = [UIImage systemImageNamed:@"power" withConfiguration:config];
UIImage *coloredImage = [templateImage imageWithTintColor:hasValidMac ? defaultBlue : [ThemeManager textColorGray] renderingMode:UIImageRenderingModeAlwaysOriginal];
[self.wakeupButton setImage:coloredImage forState:UIControlStateNormal];
} else {
// Fallback on earlier versions
}
self.wakeupButton.backgroundColor = hasValidMac ? [ThemeManager textTintColorWithAlpha] : [[ThemeManager textColorGray] colorWithAlphaComponent:0.2];
[self.wakeupButton setTitleColor: hasValidMac ? defaultBlue : [ThemeManager textColorGray] forState:UIControlStateNormal];
break;
case StateUnknown:
_hostSpinner.color = [UIColor whiteColor];
[_hostSpinner startAnimating];
// _iconBackgroundView.backgroundColor = _userIterfaceStyle == UIUserInterfaceStyleDark ? [UIColor appBackgroundColorDark] : [UIColor appBackgroundColorLight];
_iconBackgroundView.backgroundColor = [ThemeManager appPrimaryColorWithAlpha];
_statusLabel.textColor = [ThemeManager textColorGray];
_statusLabel.text = [LocalizationHelper localizedStringForKey: @"Detecting..."];
_statusIcon.tintColor = [ThemeManager textColorGray];
if (@available(iOS 13.0, *)) {
_statusIcon.image = [UIImage systemImageNamed:@"antenna.radiowaves.left.and.right"];
} else {
self.statusIcon.hidden = YES;
}
// _hostIconView.tintColor = [UIColor lowProfileGray];
_hostIconView.tintColor = defaultBlue;
lockIconView.hidden = YES;
_appButton.hidden = YES;
_launchButton.hidden = YES;
_pairButton.hidden = YES;
_wakeupButton.hidden = NO;
if (@available(iOS 13.0, *)) {
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:buttonHeight/3.45 weight:UIImageSymbolWeightBold];
UIImage *templateImage = [UIImage systemImageNamed:@"power" withConfiguration:config];
UIImage *coloredImage = [templateImage imageWithTintColor:hasValidMac ? defaultBlue : [ThemeManager textColorGray] renderingMode:UIImageRenderingModeAlwaysOriginal];
[self.wakeupButton setImage:coloredImage forState:UIControlStateNormal];
} else {
// Fallback on earlier versions
}
self.wakeupButton.backgroundColor = hasValidMac ? [ThemeManager textTintColorWithAlpha] : [[ThemeManager textColorGray] colorWithAlphaComponent:0.2];
[self.wakeupButton setTitleColor: hasValidMac ? defaultBlue : [ThemeManager textColorGray] forState:UIControlStateNormal];
break;
default:break;
}
}
@end
+599
View File
@@ -0,0 +1,599 @@
//
// HostCardView.m -> HostCardView.swift
// VoidLink
//
// Created by True砖家 on 2025.5.18
// Copyright © 2025 True砖家 @ Bilibili. All rights reserved.
//
import UIKit
@objc
protocol HostCardActionDelegate: NSObjectProtocol {
@objc(appButtonTappedForHost:)
func appButtonTapped(for host: TemporaryHost)
@objc(launchButtonTappedForHost:)
func launchButtonTapped(for host: TemporaryHost)
@objc(wakeupButtonTappedForHost:)
func wakeupButtonTapped(for host: TemporaryHost)
@objc(pairButtonTappedForHost:)
func pairButtonTapped(for host: TemporaryHost)
@objc(hostCardLongPressed:view:)
func hostCardLongPressed(_ host: TemporaryHost, view: UIView)
@objc(isStreaming)
func isStreaming() -> Bool
}
@objcMembers
class HostCardView: UIView {
var sizeFactor: CGFloat = 1
private(set) var size: CGSize = .zero
weak var delegate: HostCardActionDelegate?
private var iconBackgroundView: UIView!
private var hostIconView: UIImageView!
private var hostNameLabel: UILabel!
private var statusIcon: UIImageView!
private var statusLabel: UILabel!
private var appButton: UIButton!
private var launchButton: UIButton!
private var pairButton: UIButton!
private var wakeupButton: UIButton!
private var transparentButton: UIButton!
private var widthConstraint: NSLayoutConstraint!
private var heightConstraint: NSLayoutConstraint!
private var separatorLine: UIView!
private var cardContentpadding: CGFloat = 0
private var host: TemporaryHost?
private var hostSpinner: UIActivityIndicatorView!
private var lockIconView: UIImageView!
private var computerIconMonitorCenterYOffset: CGFloat = 0
private var buttonHeight: CGFloat = 0
private var buttonLabelFontSize: CGFloat = 0
private var iconAndButtonSpacing: CGFloat = 0
private var defaultBlue: UIColor = ThemeManager.appPrimaryColor
private var defaultGreen: UIColor = UIColor(red: 52 / 255, green: 199 / 255, blue: 89 / 255, alpha: 1)
private var backgroundLayer: CAGradientLayer!
private var longPressFired = false
private static let refreshCycle: TimeInterval = 2
private let stateUnknown = State(rawValue: 0)!
private let stateOffline = State(rawValue: 1)!
private let stateOnline = State(rawValue: 2)!
private let pairStatePaired = PairState(rawValue: 2)!
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupUI()
}
@objc(initWithHost:)
convenience init(host: TemporaryHost) {
self.init(host: host, andSizeFactor: 1)
}
@objc(initWithHost:andSizeFactor:)
init(host: TemporaryHost, andSizeFactor sizeFactor: CGFloat) {
self.sizeFactor = sizeFactor
self.host = host
super.init(frame: .zero)
commonInit()
}
private func commonInit() {
buttonLabelFontSize = 15 * sizeFactor
longPressFired = false
computerIconMonitorCenterYOffset = isIPhone() ? -2.75 * sizeFactor : -3.2 * sizeFactor
iconAndButtonSpacing = 37 * sizeFactor
buttonHeight = 39 * sizeFactor
defaultBlue = ThemeManager.appPrimaryColor
defaultGreen = UIColor(red: 52 / 255, green: 199 / 255, blue: 89 / 255, alpha: 1)
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(hostCardLongPressed(_:)))
addGestureRecognizer(longPressRecognizer)
createBackgroundLayer()
setupUI()
}
private func isIPhone() -> Bool {
UIDevice.current.userInterfaceIdiom == .phone
}
@objc func appButtonTapped() {
guard host?.pairState == pairStatePaired, host?.state == .online, self.parentViewController?.hasNoPresentedVC == true else { return }
if longPressFired {
longPressFired = false
return
}
guard let host else { return }
if let delegate {
delegate.appButtonTapped(for: host)
} else {
NSLog("Delegate not set or does not respond to appButtonTappedForHost:")
}
}
@objc func launchButtonTapped() {
guard host?.pairState == pairStatePaired, host?.state == .online, self.parentViewController?.hasNoPresentedVC == true else { return }
if longPressFired {
longPressFired = false
return
}
guard let host else { return }
if let delegate {
delegate.launchButtonTapped(for: host)
} else {
NSLog("Delegate not set or does not respond to launchButtonTappedForHost:")
}
}
@objc func wakeupButtonTapped() {
guard host?.state != .online, self.parentViewController?.hasNoPresentedVC == true else { return }
if longPressFired {
longPressFired = false
return
}
guard let host else { return }
if let delegate {
delegate.wakeupButtonTapped(for: host)
} else {
NSLog("Delegate not set or does not respond to launchButtonTappedForHost:")
}
}
@objc func pairButtonTapped() {
guard !pairButton.isHidden, self.parentViewController?.hasNoPresentedVC == true else { return }
if longPressFired {
longPressFired = false
return
}
guard let host else { return }
if let delegate {
delegate.pairButtonTapped(for: host)
} else {
NSLog("Delegate not set or does not respond to pairButtonTappedForHost:")
}
}
@objc func hostCardLongPressed(_ gesture: UILongPressGestureRecognizer) {
guard gesture.state == .began, let host else { return }
longPressFired = true
if let delegate {
delegate.hostCardLongPressed(host, view: self)
} else {
NSLog("Delegate not set or does not respond")
}
}
@objc(resizeBySizeFactor:)
func resizeBySizeFactor(_ factor: CGFloat) {
subviews.forEach { $0.removeFromSuperview() }
sizeFactor = factor
setupUI()
}
private func setupUI() {
isUserInteractionEnabled = true
backgroundColor = ThemeManager.widgetBackgroundColor
cardContentpadding = 13 * sizeFactor
clipsToBounds = true
translatesAutoresizingMaskIntoConstraints = false
let appButtonWidth = 100 * sizeFactor
let launchButtonWidth = 120 * sizeFactor
let cardWidth = cardContentpadding * 2 + appButtonWidth + 15 * sizeFactor + launchButtonWidth
layer.cornerRadius = 2 * CGFloat(UInt16(cardWidth * 0.0603 / 2))
if #available(iOS 13.0, *) {
layer.cornerCurve = .continuous
}
heightConstraint = heightAnchor.constraint(equalToConstant: 300)
heightConstraint.isActive = true
widthConstraint = widthAnchor.constraint(equalToConstant: 300)
widthConstraint.isActive = true
iconBackgroundView = UIView(frame: CGRect(x: cardContentpadding, y: cardContentpadding, width: 80 * sizeFactor, height: 80 * sizeFactor))
iconBackgroundView.backgroundColor = defaultBlue
iconBackgroundView.layer.cornerRadius = 2 * CGFloat(UInt16(20 * sizeFactor / 2))
if #available(iOS 13.0, *) {
iconBackgroundView.layer.cornerCurve = .continuous
}
addSubview(iconBackgroundView)
hostIconView = UIImageView()
hostIconView.translatesAutoresizingMaskIntoConstraints = false
hostIconView.contentMode = .scaleAspectFit
if #available(iOS 13.0, *) {
hostIconView.image = UIImage(named: "display")?.withRenderingMode(.alwaysTemplate)
} else {
hostIconView.image = UIImage(named: "Computer")
NSLayoutConstraint.activate([
hostIconView.heightAnchor.constraint(equalToConstant: 57 * sizeFactor),
hostIconView.widthAnchor.constraint(equalToConstant: 57 * sizeFactor),
])
}
hostIconView.tintColor = .white
iconBackgroundView.addSubview(hostIconView)
NSLayoutConstraint.activate([
hostIconView.centerXAnchor.constraint(equalTo: iconBackgroundView.centerXAnchor),
hostIconView.centerYAnchor.constraint(equalTo: iconBackgroundView.centerYAnchor),
hostIconView.heightAnchor.constraint(equalToConstant: 53.9 * sizeFactor),
hostIconView.widthAnchor.constraint(equalToConstant: 53.9 * sizeFactor),
])
let oldIOS: Bool
if #available(iOS 13.0, *) {
oldIOS = false
} else {
oldIOS = true
}
hostSpinner = UIActivityIndicatorView(style: .white)
hostSpinner.isUserInteractionEnabled = false
hostSpinner.translatesAutoresizingMaskIntoConstraints = false
hostSpinner.hidesWhenStopped = true
iconBackgroundView.addSubview(hostSpinner)
NSLayoutConstraint.activate([
hostSpinner.centerXAnchor.constraint(equalTo: iconBackgroundView.centerXAnchor),
hostSpinner.centerYAnchor.constraint(equalTo: iconBackgroundView.centerYAnchor, constant: oldIOS ? -5.5 * sizeFactor : computerIconMonitorCenterYOffset),
])
hostSpinner.transform = CGAffineTransform(scaleX: sizeFactor, y: sizeFactor)
hostSpinner.stopAnimating()
lockIconView = UIImageView()
lockIconView.translatesAutoresizingMaskIntoConstraints = false
lockIconView.contentMode = .scaleAspectFit
if #available(iOS 13.0, *) {
lockIconView.image = UIImage(named: "lock.fill")?.withRenderingMode(.alwaysTemplate)
} else {
lockIconView.image = UIImage(named: "LockedOverlayIcon")
computerIconMonitorCenterYOffset = -5 * sizeFactor
}
lockIconView.tintColor = .white
iconBackgroundView.addSubview(lockIconView)
NSLayoutConstraint.activate([
lockIconView.centerXAnchor.constraint(equalTo: iconBackgroundView.centerXAnchor),
lockIconView.centerYAnchor.constraint(equalTo: iconBackgroundView.centerYAnchor, constant: computerIconMonitorCenterYOffset),
lockIconView.widthAnchor.constraint(equalToConstant: 17.05 * sizeFactor),
lockIconView.heightAnchor.constraint(equalToConstant: 17.05 * sizeFactor),
])
lockIconView.isHidden = true
let hostNameLabelCoordX = cardContentpadding + iconBackgroundView.frame.size.width + 16 * sizeFactor
hostNameLabel = UILabel(frame: CGRect(x: hostNameLabelCoordX, y: cardContentpadding + iconBackgroundView.frame.size.height * 0.02, width: cardWidth - hostNameLabelCoordX - cardContentpadding, height: 30 * sizeFactor))
hostNameLabel.numberOfLines = 1
hostNameLabel.adjustsFontSizeToFitWidth = true
hostNameLabel.minimumScaleFactor = 0.8
hostNameLabel.lineBreakMode = .byTruncatingTail
hostNameLabel.text = "RazerBlade 16"
hostNameLabel.textColor = .white
hostNameLabel.font = .boldSystemFont(ofSize: 18 * sizeFactor)
addSubview(hostNameLabel)
statusLabel = UILabel(frame: CGRect(x: 205, y: 68, width: 100, height: 24))
statusLabel.translatesAutoresizingMaskIntoConstraints = false
statusLabel.text = LocalizationHelper.localizedString(forKey: "Online")
statusLabel.font = .systemFont(ofSize: 14 * sizeFactor, weight: .medium)
statusLabel.textColor = defaultGreen
statusIcon = UIImageView(frame: CGRect(x: 0, y: 0, width: 16 * sizeFactor, height: 16 * sizeFactor))
statusIcon.contentMode = .scaleAspectFit
statusIcon.translatesAutoresizingMaskIntoConstraints = false
statusIcon.image = UIImage(named: "wifi_green")?.withRenderingMode(.alwaysOriginal)
addSubview(statusIcon)
addSubview(statusLabel)
NSLayoutConstraint.activate([
statusLabel.leadingAnchor.constraint(equalTo: statusIcon.trailingAnchor, constant: 3 * sizeFactor),
statusLabel.topAnchor.constraint(equalTo: hostNameLabel.bottomAnchor),
statusIcon.centerYAnchor.constraint(equalTo: statusLabel.centerYAnchor),
statusIcon.leadingAnchor.constraint(equalTo: hostNameLabel.leadingAnchor),
statusIcon.widthAnchor.constraint(equalToConstant: 16 * sizeFactor),
])
appButton = UIButton(type: .system)
appButton.translatesAutoresizingMaskIntoConstraints = false
appButton.frame = CGRect(x: 20, y: 200, width: 150, height: buttonHeight)
appButton.setTitle(LocalizationHelper.localizedString(forKey: "Applications"), for: .normal)
appButton.setTitleColor(ThemeManager.textColorGray, for: .normal)
appButton.titleLabel?.font = .systemFont(ofSize: buttonLabelFontSize)
appButton.addTarget(self, action: #selector(appButtonTapped), for: .primaryActionTriggered)
addSubview(appButton)
NSLayoutConstraint.activate([
appButton.leadingAnchor.constraint(equalTo: iconBackgroundView.leadingAnchor),
appButton.topAnchor.constraint(equalTo: iconBackgroundView.bottomAnchor, constant: iconAndButtonSpacing),
appButton.widthAnchor.constraint(equalToConstant: appButtonWidth),
appButton.heightAnchor.constraint(equalToConstant: buttonHeight),
])
launchButton = UIButton(type: .system)
launchButton.translatesAutoresizingMaskIntoConstraints = false
launchButton.frame = CGRect(x: 0, y: 0, width: 150, height: 50)
launchButton.backgroundColor = defaultBlue
launchButton.layer.cornerRadius = 2 * CGFloat(UInt16(cardWidth * 0.0377 / 2))
if #available(iOS 13.0, *) {
launchButton.layer.cornerCurve = .continuous
}
launchButton.setTitle(LocalizationHelper.localizedString(forKey: " Launch"), for: .normal)
launchButton.setTitleColor(.white, for: .normal)
launchButton.titleLabel?.font = .boldSystemFont(ofSize: buttonLabelFontSize)
launchButton.tintColor = .white
launchButton.addTarget(self, action: #selector(launchButtonTapped), for: .primaryActionTriggered)
addSubview(launchButton)
if #available(iOS 13.0, *) {
let config = UIImage.SymbolConfiguration(pointSize: buttonHeight * 0.263)
launchButton.setImage(UIImage(systemName: "play.fill", withConfiguration: config), for: .normal)
}
NSLayoutConstraint.activate([
launchButton.leadingAnchor.constraint(equalTo: appButton.trailingAnchor, constant: 15 * sizeFactor),
launchButton.centerYAnchor.constraint(equalTo: appButton.centerYAnchor),
launchButton.widthAnchor.constraint(equalToConstant: launchButtonWidth),
launchButton.heightAnchor.constraint(equalToConstant: buttonHeight),
])
pairButton = UIButton(type: .system)
pairButton.translatesAutoresizingMaskIntoConstraints = false
pairButton.frame = CGRect(x: 0, y: 0, width: 150, height: 50)
pairButton.backgroundColor = ThemeManager.textTintColorWithAlpha
pairButton.layer.cornerRadius = 2 * CGFloat(UInt16(cardWidth * 0.0377 / 2))
if #available(iOS 13.0, *) {
pairButton.layer.cornerCurve = .continuous
}
pairButton.setTitle(LocalizationHelper.localizedString(forKey: " Pair with PIN"), for: .normal)
pairButton.setTitleColor(defaultBlue, for: .normal)
pairButton.titleLabel?.font = .boldSystemFont(ofSize: buttonLabelFontSize)
if #available(iOS 13.0, *) {
let config = UIImage.SymbolConfiguration(pointSize: buttonHeight / 3.4, weight: .bold)
let templateImage = UIImage(systemName: "lock.open.fill", withConfiguration: config)
let coloredImage = templateImage?.withTintColor(defaultBlue, renderingMode: .alwaysOriginal)
pairButton.setImage(coloredImage, for: .normal)
}
pairButton.addTarget(self, action: #selector(pairButtonTapped), for: .primaryActionTriggered)
addSubview(pairButton)
NSLayoutConstraint.activate([
pairButton.leadingAnchor.constraint(equalTo: leadingAnchor, constant: cardContentpadding),
pairButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -cardContentpadding),
pairButton.topAnchor.constraint(equalTo: iconBackgroundView.bottomAnchor, constant: iconAndButtonSpacing),
pairButton.heightAnchor.constraint(equalToConstant: buttonHeight),
])
wakeupButton = UIButton(type: .system)
wakeupButton.translatesAutoresizingMaskIntoConstraints = false
wakeupButton.frame = CGRect(x: 0, y: 0, width: 150, height: 50)
wakeupButton.backgroundColor = ThemeManager.textTintColorWithAlpha
wakeupButton.layer.cornerRadius = 2 * CGFloat(UInt16(cardWidth * 0.0377 / 2))
if #available(iOS 13.0, *) {
wakeupButton.layer.cornerCurve = .continuous
}
wakeupButton.setTitle(LocalizationHelper.localizedString(forKey: " Wake-on-LAN"), for: .normal)
wakeupButton.setTitleColor(defaultBlue, for: .normal)
wakeupButton.titleLabel?.font = .boldSystemFont(ofSize: buttonLabelFontSize)
wakeupButton.addTarget(self, action: #selector(wakeupButtonTapped), for: .primaryActionTriggered)
addSubview(wakeupButton)
NSLayoutConstraint.activate([
wakeupButton.leadingAnchor.constraint(equalTo: leadingAnchor, constant: cardContentpadding),
wakeupButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -cardContentpadding),
wakeupButton.topAnchor.constraint(equalTo: iconBackgroundView.bottomAnchor, constant: iconAndButtonSpacing),
wakeupButton.heightAnchor.constraint(equalToConstant: buttonHeight),
])
separatorLine = UIView()
separatorLine.backgroundColor = UIColor(white: 0.3, alpha: 5)
separatorLine.translatesAutoresizingMaskIntoConstraints = false
addSubview(separatorLine)
NSLayoutConstraint.activate([
separatorLine.leadingAnchor.constraint(equalTo: leadingAnchor, constant: cardContentpadding),
separatorLine.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -cardContentpadding),
separatorLine.centerYAnchor.constraint(equalTo: iconBackgroundView.bottomAnchor, constant: iconAndButtonSpacing / 2),
separatorLine.heightAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale),
])
transparentButton = UIButton(type: .custom)
transparentButton.translatesAutoresizingMaskIntoConstraints = false
transparentButton.backgroundColor = .clear
transparentButton.setTitle("", for: .normal)
transparentButton.setImage(nil, for: .normal)
transparentButton.addTarget(self, action: #selector(appButtonTapped), for: .primaryActionTriggered)
addSubview(transparentButton)
NSLayoutConstraint.activate([
transparentButton.leadingAnchor.constraint(equalTo: leadingAnchor),
transparentButton.trailingAnchor.constraint(equalTo: trailingAnchor),
transparentButton.topAnchor.constraint(equalTo: topAnchor),
transparentButton.bottomAnchor.constraint(equalTo: separatorLine.topAnchor),
])
widthConstraint.constant = cardContentpadding * 2 + appButtonWidth + 15 * sizeFactor + launchButtonWidth
heightConstraint.constant = cardContentpadding * 2 + iconBackgroundView.frame.size.height + iconAndButtonSpacing + buttonHeight + 1
size = CGSize(width: widthConstraint.constant, height: heightConstraint.constant)
updateTheme()
}
private func updateBackgroundLayerTheme() {
let gradientColorDark = UIColor(red: 0, green: 0.319, blue: 0.64, alpha: 1)
let gradientColorLight = gradientColorDark.withAlphaComponent(0.52)
let gradientColor = ThemeManager.userInterfaceStyle() == .dark ? gradientColorDark.cgColor : gradientColorLight.cgColor
backgroundLayer.colors = [
UIColor.clear.cgColor,
UIColor.clear.cgColor,
UIColor.clear.cgColor,
gradientColor,
]
}
private func createBackgroundLayer() {
backgroundLayer = CAGradientLayer()
updateBackgroundLayerTheme()
backgroundLayer.locations = [0, 0.18, 0.5, 1]
backgroundLayer.startPoint = CGPoint(x: 0.25, y: 0.5)
backgroundLayer.endPoint = CGPoint(x: 0.75, y: 0.5)
let transform = CGAffineTransform(a: -1.01, b: -1, c: 1, d: -3.67, tx: 0.5, ty: 2.83)
backgroundLayer.transform = CATransform3DMakeAffineTransform(transform)
layer.insertSublayer(backgroundLayer, at: 0)
}
override func layoutSubviews() {
super.layoutSubviews()
backgroundLayer.bounds = bounds.insetBy(dx: -0.5 * bounds.size.width, dy: -0.5 * bounds.size.height)
backgroundLayer.position = CGPoint(x: bounds.midX, y: bounds.midY)
updateContents(for: host)
}
func tintAdjustmentModeDidChange() {
tintAdjustmentMode = .normal
}
func updateTheme() {
backgroundColor = ThemeManager.widgetBackgroundColor
hostNameLabel?.textColor = ThemeManager.textColor
appButton?.setTitleColor(ThemeManager.appPrimaryColor, for: .normal)
separatorLine?.backgroundColor = ThemeManager.hostCardSeparatorColor
backgroundLayer?.isHidden = false
updateBackgroundLayerTheme()
updateContents(for: host)
}
override func didMoveToSuperview() {
if superview != nil && host != nil {
NSLog("start update loop")
updateLoop()
}
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
}
private func isDarkTheme() -> Bool {
ThemeManager.userInterfaceStyle() == .dark
}
@objc func updateLoop() {
if superview == nil || delegate?.isStreaming() == true {
return
}
DispatchQueue.main.asyncAfter(deadline: .now()+0.36) { [weak self] in
self?.updateContents(for: self?.host)
}
perform(#selector(updateLoop), with: self, afterDelay: Self.refreshCycle)
}
private func updateContents(for host: TemporaryHost?) {
guard let host else { return }
hostNameLabel.text = host.name
backgroundLayer.isHidden = !(host.state == stateOnline && host.pairState == pairStatePaired)
let hostPaired = host.pairState == pairStatePaired
let hasValidMac = host.mac != nil && host.mac != "00:00:00:00:00:00"
switch host.state {
case stateOnline:
hostSpinner.stopAnimating()
statusLabel.textColor = defaultGreen
statusLabel.text = LocalizationHelper.localizedString(forKey: "Online")
statusIcon.image = UIImage(named: "wifi_green")?.withRenderingMode(.alwaysOriginal)
statusIcon.isHidden = false
appButton.titleLabel?.font = .systemFont(ofSize: buttonLabelFontSize)
if host.pairState == pairStatePaired {
hostIconView.tintColor = .white
iconBackgroundView.backgroundColor = defaultBlue
lockIconView.isHidden = true
appButton.setTitle(LocalizationHelper.localizedString(forKey: "Applications"), for: .normal)
launchButton.setTitle(LocalizationHelper.localizedString(forKey: " Launch"), for: .normal)
appButton.isEnabled = true
launchButton.isEnabled = true
appButton.isHidden = false
launchButton.isHidden = false
pairButton.isHidden = true
wakeupButton.isHidden = true
appButton.setTitleColor(defaultBlue, for: .normal)
if #available(iOS 13.0, *) {
let config = UIImage.SymbolConfiguration(pointSize: buttonHeight * 0.263)
launchButton.setImage(UIImage(systemName: "play.fill", withConfiguration: config), for: .normal)
}
launchButton.backgroundColor = defaultBlue
launchButton.setTitleColor(.white, for: .normal)
} else {
iconBackgroundView.backgroundColor = ThemeManager.appPrimaryColorWithAlpha
let iconColor = UIColor.white.withAlphaComponent(isDarkTheme() ? 0.63 : 1)
hostIconView.tintColor = iconColor
lockIconView.tintColor = iconColor
lockIconView.isHidden = false
appButton.isHidden = true
launchButton.isHidden = true
pairButton.isHidden = false
wakeupButton.isHidden = true
}
case stateOffline:
hostSpinner.stopAnimating()
iconBackgroundView.backgroundColor = ThemeManager.offlineHostIconBackgroundColor
statusLabel.textColor = ThemeManager.textColorGray
statusLabel.text = LocalizationHelper.localizedString(forKey: "Offline")
statusIcon.tintColor = ThemeManager.textColorGray
if #available(iOS 13.0, *) {
statusIcon.image = UIImage(systemName: "exclamationmark.triangle.fill")
} else {
statusIcon.isHidden = true
}
hostIconView.tintColor = ThemeManager.lowProfileGray
lockIconView.tintColor = ThemeManager.lowProfileGray
lockIconView.isHidden = hostPaired
appButton.isHidden = true
launchButton.isHidden = true
pairButton.isHidden = true
wakeupButton.isHidden = false
if #available(iOS 13.0, *) {
let config = UIImage.SymbolConfiguration(pointSize: buttonHeight / 3.45, weight: .bold)
let templateImage = UIImage(systemName: "power", withConfiguration: config)
let color = hasValidMac ? defaultBlue : ThemeManager.textColorGray
wakeupButton.setImage(templateImage?.withTintColor(color, renderingMode: .alwaysOriginal), for: .normal)
}
wakeupButton.backgroundColor = hasValidMac ? ThemeManager.textTintColorWithAlpha : ThemeManager.textColorGray.withAlphaComponent(0.2)
wakeupButton.setTitleColor(hasValidMac ? defaultBlue : ThemeManager.textColorGray, for: .normal)
case stateUnknown:
hostSpinner.color = .white
hostSpinner.startAnimating()
iconBackgroundView.backgroundColor = ThemeManager.appPrimaryColorWithAlpha
statusLabel.textColor = ThemeManager.textColorGray
statusLabel.text = LocalizationHelper.localizedString(forKey: "Detecting...")
statusIcon.tintColor = ThemeManager.textColorGray
if #available(iOS 13.0, *) {
statusIcon.image = UIImage(systemName: "antenna.radiowaves.left.and.right")
} else {
statusIcon.isHidden = true
}
hostIconView.tintColor = defaultBlue
lockIconView.isHidden = true
appButton.isHidden = true
launchButton.isHidden = true
pairButton.isHidden = true
wakeupButton.isHidden = false
if #available(iOS 13.0, *) {
let config = UIImage.SymbolConfiguration(pointSize: buttonHeight / 3.45, weight: .bold)
let templateImage = UIImage(systemName: "power", withConfiguration: config)
let color = hasValidMac ? defaultBlue : ThemeManager.textColorGray
wakeupButton.setImage(templateImage?.withTintColor(color, renderingMode: .alwaysOriginal), for: .normal)
}
wakeupButton.backgroundColor = hasValidMac ? ThemeManager.textTintColorWithAlpha : ThemeManager.textColorGray.withAlphaComponent(0.2)
wakeupButton.setTitleColor(hasValidMac ? defaultBlue : ThemeManager.textColorGray, for: .normal)
default:
break
}
}
}
@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "StickWheel.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,34 @@
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" fill="none" viewBox="0 0 120 120">
<g clip-path="url(#a)">
<g filter="url(#b)">
<path fill="#fff" fill-opacity="1"
d="M67.25 10.437c.099.075.215.123.338.142a50 50 0 0 1 27.233 13.54.74.74 0 0 1 .004 1.056l-.776.775a.76.76 0 0 1-1.065.004 47.405 47.405 0 0 0-65.97 0 .76.76 0 0 1-1.064-.004l-.776-.775a.74.74 0 0 1 .004-1.057A50 50 0 0 1 52.41 10.58a.76.76 0 0 0 .339-.142l6.8-5.1a.75.75 0 0 1 .9 0z"/>
</g>
<g filter="url(#c)">
<path fill="#fff" fill-opacity="1"
d="M61.955 6.804c2.487 2.295 5.657 3.663 8.96 4.402a50 50 0 0 1 23.51 12.533c.52.493.52 1.316.013 1.823a1.326 1.326 0 0 1-1.849.013 47.403 47.403 0 0 0-65.179 0 1.326 1.326 0 0 1-1.848-.013 1.27 1.27 0 0 1 .012-1.823 50 50 0 0 1 23.512-12.533c3.302-.739 6.47-2.106 8.958-4.401L60 5z"
/>
</g>
</g>
<defs>
<!-- defs stay the same -->
<filter id="b" width="82.089" height="32.983" x="18.955" y="-.813" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur result="effect1_foregroundBlur_211_1847" stdDeviation="3"/>
</filter>
<filter id="c" width="69.634" height="22.444" x="25.183" y="5" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="1.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.08 0"/>
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_211_1847"/>
<feBlend in="SourceGraphic" in2="effect1_dropShadow_211_1847" result="shape"/>
</filter>
<clipPath id="a">
<path fill="#fff" d="M0 0h120v120H0z"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

@@ -0,0 +1,24 @@
{
"images" : [
{
"filename" : "apple.terminal.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--Generator: Apple Native CoreSVG 341-->
<!DOCTYPE svg
PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 30.5252 23.9534">
<g>
<rect height="23.9534" opacity="0" width="30.5252" x="0" y="0"/>
<path d="M4.30122 23.9534L25.8685 23.9534C28.6839 23.9534 30.1697 22.4762 30.1697 19.6955L30.1697 4.27466C30.1697 1.48987 28.6839 0.00807405 25.8685 0.00807405L4.30122 0.00807405C1.49449 0.00807405 0 1.48987 0 4.27466L0 19.6955C0 22.4762 1.49449 23.9534 4.30122 23.9534ZM4.53653 21.1263C3.42444 21.1263 2.82713 20.5624 2.82713 19.3955L2.82713 4.57058C2.82713 3.40367 3.42444 2.8352 4.53653 2.8352L25.6332 2.8352C26.7366 2.8352 27.3426 3.40367 27.3426 4.57058L27.3426 19.3955C27.3426 20.5624 26.7366 21.1263 25.6332 21.1263Z" fill="white" fill-opacity="0.85"/>
<path d="M5.97169 10.1889C4.79979 10.8986 5.79916 12.6879 7.08069 11.8853L10.6199 9.66475C11.4444 9.14716 11.4104 7.92273 10.6199 7.42649L7.08069 5.214C5.80319 4.40735 4.79113 6.19202 5.96765 6.90232L8.69215 8.54533ZM11.2928 11.7555C11.2928 12.2887 11.7209 12.7295 12.2708 12.7295L16.9785 12.7295C17.5244 12.7295 17.9485 12.2887 17.9485 11.7555C17.9485 11.2057 17.5244 10.7729 16.9785 10.7729L12.2708 10.7729C11.7249 10.7729 11.2928 11.2057 11.2928 11.7555Z" fill="white" fill-opacity="0.85"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,24 @@
{
"images" : [
{
"filename" : "circle.lefthalf.filled.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--Generator: Apple Native CoreSVG 341-->
<!DOCTYPE svg
PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 26.5852 26.2402">
<g>
<rect height="26.2402" opacity="0" width="26.5852" x="0" y="0"/>
<path d="M13.1137 24.8338L13.1137 1.38328C6.44134 1.38328 1.38556 6.44134 1.38556 13.1137C1.38556 19.7861 6.44134 24.8338 13.1137 24.8338ZM13.1137 26.2275C20.3549 26.2275 26.2298 20.3526 26.2298 13.1137C26.2298 5.87488 20.3549 0 13.1137 0C5.87488 0 0 5.87488 0 13.1137C0 20.3526 5.87488 26.2275 13.1137 26.2275ZM13.1137 23.6443C7.29091 23.6443 2.58322 18.9366 2.58322 13.1137C2.58322 7.29091 7.29091 2.58322 13.1137 2.58322C18.9366 2.58322 23.6465 7.29091 23.6465 13.1137C23.6465 18.9366 18.9366 23.6443 13.1137 23.6443Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 940 B

@@ -0,0 +1,24 @@
{
"images" : [
{
"filename" : "controllerMouse.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--Generator: Apple Native CoreSVG 341-->
<!DOCTYPE svg
PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 17.225 22.7356">
<g>
<rect height="22.7356" opacity="0" width="17.225" x="0" y="0"/>
<path d="M7.98991 16.743L14.7319 16.743C16.6897 16.743 17.5002 14.9583 16.3368 13.7776L3.19844 0.64154C1.94161-0.624432 0 0.0799009 0 1.90751L0 20.5374C0 22.1976 1.83421 22.8801 3.21901 21.5058Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 612 B

@@ -0,0 +1,24 @@
{
"images" : [
{
"filename" : "disableControllerMouse.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--Generator: Apple Native CoreSVG 341-->
<!DOCTYPE svg
PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 30.0974 26.0826">
<g>
<rect height="26.0826" opacity="0" width="30.0974" x="0" y="0"/>
<path d="M14.0091 18.5296L9.61512 22.9119C8.37098 24.156 6.72059 23.5467 6.72059 22.0486L6.72059 11.264ZM9.62782 2.61209L22.5136 15.5105C23.5673 16.5769 22.8436 18.1765 21.079 18.1765L20.638 18.1765L6.72059 4.29423L6.72059 3.75467C6.72059 2.09158 8.47254 1.46951 9.62782 2.61209Z" fill="white"/>
<path d="M1.75672 4.2117L21.9169 24.3084C22.2977 24.7019 22.9325 24.7019 23.3007 24.3084C23.6688 23.9275 23.6815 23.3181 23.3007 22.9246L3.15321 2.82791C2.77235 2.44705 2.15028 2.44705 1.75672 2.82791C1.38856 3.20877 1.38856 3.84354 1.75672 4.2117Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 966 B

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "gamecontroller.circle.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--Generator: Apple Native CoreSVG 341-->
<!DOCTYPE svg
PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 30.249 29.8975">
<g>
<rect height="29.8975" opacity="0" width="30.249" x="0" y="0"/>
<path d="M14.9414 29.8828C23.1885 29.8828 29.8828 23.1885 29.8828 14.9414C29.8828 6.69434 23.1885 0 14.9414 0C6.69434 0 0 6.69434 0 14.9414C0 23.1885 6.69434 29.8828 14.9414 29.8828ZM14.9414 27.3926C8.05664 27.3926 2.49023 21.8262 2.49023 14.9414C2.49023 8.05664 8.05664 2.49023 14.9414 2.49023C21.8262 2.49023 27.3926 8.05664 27.3926 14.9414C27.3926 21.8262 21.8262 27.3926 14.9414 27.3926Z" fill="white"/>
<path d="M7.9834 20.127C8.8623 20.127 9.47754 19.79 10.0342 19.1309L11.2207 17.6807C11.3818 17.4756 11.5723 17.373 11.7773 17.373L18.0762 17.373C18.2812 17.373 18.4717 17.4756 18.6328 17.6807L19.834 19.1309C20.376 19.79 20.9912 20.127 21.8701 20.127C23.3643 20.127 24.3604 19.1309 24.3604 17.6074C24.3604 16.9482 24.2139 16.2158 23.9648 15.3516C23.5547 14.0332 22.8516 12.2021 22.1777 10.7812C21.6211 9.60938 21.3428 9.06738 19.9658 8.75977C18.7207 8.48145 17.0215 8.29102 14.9268 8.29102C12.832 8.29102 11.1328 8.48145 9.8877 8.75977C8.51074 9.06738 8.23242 9.60938 7.67578 10.7812C7.00195 12.2021 6.31348 14.0332 5.90332 15.3516C5.6543 16.2158 5.49316 16.9482 5.49316 17.6074C5.49316 19.1309 6.48926 20.127 7.9834 20.127ZM9.44824 12.8906C9.44824 12.5391 9.66797 12.3047 10.0342 12.3047L11.2207 12.3047L11.2207 11.1621C11.2207 10.7959 11.4258 10.5615 11.7773 10.5615C12.1289 10.5615 12.334 10.7959 12.334 11.1621L12.334 12.3047L13.4619 12.3047C13.8574 12.3047 14.0918 12.5391 14.0918 12.8906C14.0918 13.2568 13.8574 13.4766 13.4619 13.4766L12.334 13.4766L12.334 14.6484C12.334 15 12.1289 15.2344 11.7773 15.2344C11.4258 15.2344 11.2207 15 11.2207 14.6484L11.2207 13.4766L10.0342 13.4766C9.66797 13.4766 9.44824 13.2568 9.44824 12.8906ZM19.1162 12.8174C18.5889 12.8174 18.1348 12.3926 18.1348 11.8359C18.1348 11.3086 18.5889 10.8691 19.1162 10.8691C19.6582 10.8691 20.0977 11.3086 20.0977 11.8359C20.0977 12.3926 19.6582 12.8174 19.1162 12.8174ZM17.1387 14.8242C16.5967 14.8242 16.1572 14.3848 16.1572 13.8428C16.1572 13.3008 16.5967 12.876 17.1387 12.876C17.6807 12.876 18.1201 13.3008 18.1201 13.8428C18.1201 14.3848 17.6807 14.8242 17.1387 14.8242Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "gamecontroller.circle.tag.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--Generator: Apple Native CoreSVG 341-->
<!DOCTYPE svg
PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 19.2876 18.9351">
<g>
<rect height="18.9351" opacity="0" width="19.2876" x="0" y="0"/>
<path d="M9.46289 18.9258C14.686 18.9258 18.9258 14.686 18.9258 9.46289C18.9258 4.23975 14.686 0 9.46289 0C4.23975 0 0 4.23975 0 9.46289C0 14.686 4.23975 18.9258 9.46289 18.9258ZM9.46289 17.3486C5.10254 17.3486 1.57715 13.8232 1.57715 9.46289C1.57715 5.10254 5.10254 1.57715 9.46289 1.57715C13.8232 1.57715 17.3486 5.10254 17.3486 9.46289C17.3486 13.8232 13.8232 17.3486 9.46289 17.3486Z" fill="white"/>
<path d="M5.05615 12.7471C5.61279 12.7471 6.00244 12.5337 6.35498 12.1162L7.10645 11.1978C7.2085 11.0679 7.3291 11.0029 7.45898 11.0029L11.4482 11.0029C11.5781 11.0029 11.6987 11.0679 11.8008 11.1978L12.5615 12.1162C12.9048 12.5337 13.2944 12.7471 13.8511 12.7471C14.7974 12.7471 15.4282 12.1162 15.4282 11.1514C15.4282 10.7339 15.3354 10.27 15.1777 9.72266C14.918 8.8877 14.4727 7.72803 14.0459 6.82812C13.6934 6.08594 13.5171 5.74268 12.645 5.54785C11.8564 5.37158 10.7803 5.25098 9.45361 5.25098C8.12695 5.25098 7.05078 5.37158 6.26221 5.54785C5.39014 5.74268 5.21387 6.08594 4.86133 6.82812C4.43457 7.72803 3.99854 8.8877 3.73877 9.72266C3.58105 10.27 3.479 10.7339 3.479 11.1514C3.479 12.1162 4.10986 12.7471 5.05615 12.7471ZM5.98389 8.16406C5.98389 7.94141 6.12305 7.79297 6.35498 7.79297L7.10645 7.79297L7.10645 7.06934C7.10645 6.8374 7.23633 6.68896 7.45898 6.68896C7.68164 6.68896 7.81152 6.8374 7.81152 7.06934L7.81152 7.79297L8.52588 7.79297C8.77637 7.79297 8.9248 7.94141 8.9248 8.16406C8.9248 8.396 8.77637 8.53516 8.52588 8.53516L7.81152 8.53516L7.81152 9.27734C7.81152 9.5 7.68164 9.64844 7.45898 9.64844C7.23633 9.64844 7.10645 9.5 7.10645 9.27734L7.10645 8.53516L6.35498 8.53516C6.12305 8.53516 5.98389 8.396 5.98389 8.16406ZM12.1069 8.11768C11.7729 8.11768 11.4854 7.84863 11.4854 7.49609C11.4854 7.16211 11.7729 6.88379 12.1069 6.88379C12.4502 6.88379 12.7285 7.16211 12.7285 7.49609C12.7285 7.84863 12.4502 8.11768 12.1069 8.11768ZM10.8545 9.38867C10.5112 9.38867 10.2329 9.11035 10.2329 8.76709C10.2329 8.42383 10.5112 8.15479 10.8545 8.15479C11.1978 8.15479 11.4761 8.42383 11.4761 8.76709C11.4761 9.11035 11.1978 9.38867 10.8545 9.38867Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "gyroscope.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--Generator: Apple Native CoreSVG 341-->
<!DOCTYPE svg
PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 22.7531 22.4167">
<g>
<rect height="22.4167" opacity="0" width="22.7531" x="0" y="0"/>
<path d="M4.66382 17.7636C6.51148 19.6044 10.4016 18.2265 14.3104 14.3211C18.2231 10.4016 19.6044 6.51148 17.7568 4.67065C15.8832 2.7976 12.0151 4.17898 8.08824 8.09898C4.17557 12.0224 2.79027 15.8832 4.66382 17.7636ZM5.99819 16.4151C4.94393 15.3574 6.33263 12.5482 9.42944 9.44409C12.5521 6.3214 15.3647 4.94734 16.4151 6.0016C17.4694 7.04854 16.0841 9.85382 12.9687 12.9833C9.86457 16.0841 7.04854 17.4728 5.99819 16.4151ZM17.7456 17.7602C19.6118 15.8832 18.2304 12.0258 14.3104 8.09507C10.4016 4.17898 6.52613 2.7976 4.64917 4.66724C2.80493 6.51148 4.19022 10.4016 8.1024 14.3138C12.0151 18.2265 15.8979 19.5971 17.7456 17.7602ZM16.4044 16.4151C15.3608 17.4728 12.5482 16.0841 9.42944 12.9687C6.31798 9.85724 4.94393 7.04854 5.99086 6.0016C7.04121 4.94734 9.85724 6.3214 12.9687 9.44018C16.0767 12.5516 17.4694 15.3608 16.4044 16.4151ZM19.0764 4.59024L21.8772 1.7895C22.2092 1.45357 22.2092 0.882298 21.8772 0.550279C21.5271 0.203607 20.97 0.210933 20.6272 0.550279L17.7762 3.39741ZM3.61603 17.5507L0.550279 20.6272C0.200191 20.9665 0.210933 21.5237 0.550279 21.863C0.878882 22.2058 1.43941 22.2097 1.7895 21.8737L5.03247 18.6342ZM11.203 22.406C17.3909 22.406 22.4094 17.3875 22.4094 11.203C22.4094 5.01849 17.3909 0 11.203 0C5.01849 0 0 5.01849 0 11.203C0 17.3875 5.01849 22.406 11.203 22.406ZM11.203 19.9444C6.36936 19.9444 2.46154 16.0366 2.46154 11.203C2.46154 6.36936 6.36936 2.46154 11.203 2.46154C16.0366 2.46154 19.9478 6.36936 19.9478 11.203C19.9478 16.0366 16.0366 19.9444 11.203 19.9444Z" fill="white" fill-opacity="0.85"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,24 @@
{
"images" : [
{
"filename" : "macwindow.and.pointer.arrow.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--Generator: Apple Native CoreSVG 341-->
<!DOCTYPE svg
PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 36.2556 34.0064">
<g>
<rect height="34.0064" opacity="0" width="36.2556" x="0" y="0"/>
<path d="M32.9778 10.6393L32.9778 21.2524L30.4905 18.7118L30.4905 10.7627C30.4905 8.79238 29.3426 7.64447 27.3723 7.64447L8.51517 7.64447C6.54486 7.64447 5.39694 8.79238 5.39694 10.7627L5.39694 23.2564C5.39694 25.229 6.54486 26.3665 8.51517 26.3665L24.569 26.3665L24.5407 28.4391C24.5386 28.5831 24.5481 28.722 24.5715 28.8538L8.38137 28.8538C4.91606 28.8538 2.9097 26.8578 2.9097 23.3798L2.9097 10.6393C2.9097 7.1613 4.91606 5.15722 8.38137 5.15722L27.7709 5.15722C31.057 5.15722 32.9778 7.1613 32.9778 10.6393ZM10.0795 11.0857C10.0795 11.7166 9.55111 12.2658 8.90161 12.2658C8.25212 12.2658 7.72146 11.7166 7.72146 11.0857C7.72146 10.4489 8.25212 9.89739 8.90161 9.89739C9.55111 9.89739 10.0795 10.4489 10.0795 11.0857ZM13.8213 11.0857C13.8213 11.7166 13.2929 12.2658 12.6434 12.2658C11.9917 12.2658 11.4633 11.7166 11.4633 11.0857C11.4633 10.4489 11.9917 9.89739 12.6434 9.89739C13.2929 9.89739 13.8213 10.4489 13.8213 11.0857ZM17.5631 11.0857C17.5631 11.7166 17.0324 12.2658 16.3852 12.2658C15.7335 12.2658 15.2051 11.7166 15.2051 11.0857C15.2051 10.4489 15.7335 9.89739 16.3852 9.89739C17.0324 9.89739 17.5631 10.4489 17.5631 11.0857Z" fill="white"/>
<path d="M32.5208 32.1175C33.1251 31.8661 33.4145 31.1857 33.1527 30.5836L31.2545 26.174L33.8561 26.0963C34.3956 26.0847 34.6538 25.5545 34.2699 25.1498L27.3886 18.1123C27.0219 17.7456 26.4847 17.9589 26.472 18.4685L26.3463 28.4749C26.3313 29.0318 26.9747 29.2161 27.3355 28.8462L29.1464 26.9975L30.9661 31.4878C31.2002 32.0852 31.9083 32.3747 32.5208 32.1175Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,24 @@
{
"images" : [
{
"filename" : "rectangle.portrait.and.arrow.right.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--Generator: Apple Native CoreSVG 341-->
<!DOCTYPE svg
PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 34.2565 27.6836">
<g>
<rect height="27.6836" opacity="0" width="34.2565" x="0" y="0"/>
<path d="M23.7582 5.55752L23.7582 10.1349L20.9311 10.1349L20.9311 5.6619C20.9311 3.85035 19.9125 2.82713 18.0963 2.82713L7.33593 2.82713C5.52438 2.82713 4.49712 3.85035 4.49712 5.6619L4.49712 22.0171C4.49712 23.8373 5.52438 24.8518 7.33593 24.8518L18.0963 24.8518C19.9125 24.8518 20.9311 23.8373 20.9311 22.0171L20.9311 17.5359L23.7582 17.5359L23.7582 22.1301C23.7582 25.6945 21.7778 27.679 18.2134 27.679L7.22751 27.679C3.6631 27.679 1.66999 25.6945 1.66999 22.1301L1.66999 5.55752C1.66999 1.99311 3.6631 0 7.22751 0L18.2134 0C21.7778 0 23.7582 1.99311 23.7582 5.55752Z" fill="white"/>
<path d="M13.1283 13.8354C13.1283 14.5549 13.714 15.1331 14.4006 15.1331L27.0257 15.1331L30.8363 14.8838C31.4191 14.8423 31.8888 14.4142 31.8888 13.8354C31.8888 13.2527 31.4191 12.8245 30.8363 12.783L27.0257 12.5292L14.4006 12.5292C13.714 12.5292 13.1283 13.116 13.1283 13.8354ZM25.8536 9.43889C25.8536 9.7568 25.9875 10.0845 26.231 10.3067L28.0471 12.0034L30.4369 13.8354L28.0471 15.6588L26.231 17.3642C25.9875 17.5864 25.8536 17.9014 25.8536 18.2193C25.8536 18.8327 26.3152 19.3681 26.9649 19.3681C27.2961 19.3681 27.5396 19.2337 27.7704 19.0029L31.7792 14.8423C32.1139 14.5036 32.2351 14.1782 32.2351 13.8354C32.2351 13.4841 32.1139 13.1633 31.7792 12.8286L27.7704 8.65533C27.5396 8.42453 27.2961 8.30279 26.9649 8.30279C26.3152 8.30279 25.8536 8.81687 25.8536 9.43889Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
View File
Binary file not shown.
+4 -1
View File
@@ -7,12 +7,15 @@
//
#import "StreamView.h"
#import "TemporarySettings.h"
NS_ASSUME_NONNULL_BEGIN
@interface AbsoluteTouchHandler : UIResponder
@property (class, nonatomic, assign) int mouseButtonForCursorMove;
-(id)initWithView:(StreamView*)view;
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings;
- (void)pauseLeftButtonDrag;
@end
+178 -30
View File
@@ -10,6 +10,7 @@
//
#import "AbsoluteTouchHandler.h"
#import "VoidLink-Swift.h"
#include <Limelight.h>
@@ -17,7 +18,7 @@
#define LONG_PRESS_ACTIVATION_DELAY 0.650f
// How far the finger can move before it cancels a right click
#define LONG_PRESS_ACTIVATION_DELTA 0.01f
#define LONG_PRESS_ACTIVATION_DELTA 0.02f
// How long the double tap deadzone stays in effect between touch up and touch down
#define DOUBLE_TAP_DEAD_ZONE_DELAY 0.250f
@@ -25,8 +26,13 @@
// How far the finger can move before it can override the double tap deadzone
#define DOUBLE_TAP_DEAD_ZONE_DELTA 0.025f
static int mouseButtonForCursorMove = BUTTON_LEFT;
@implementation AbsoluteTouchHandler {
StreamView* streamView;
__weak StreamView* streamView;
bool multiTouchesDetected;
bool passthroughGestures;
NSTimer* longPressTimer;
UITouch* lastTouchDown;
@@ -34,37 +40,83 @@
UITouch* lastTouchUp;
CGPoint lastTouchUpLocation;
UITouch* capturedTouch;
CGPoint touchBeganLocation;
CGPoint movingTouchLocation;
NSTimeInterval touchBeganTimeStamp;
NSTimeInterval leftClickTimeThreshold;
// upper screen edge check
bool touchPointSpawnedAtUpperScreenEdge;
CGFloat slideGestureVerticalThreshold;
CGFloat screenWidthWithThreshold;
CGFloat EDGE_TOLERANCE;
CGFloat _edgeTolerance;
bool _delayMouseLeftClick;
NSTimeInterval leftClickDelay;
bool dragButtonDown;
UInt8 currentTouchesCount;
bool rightButtonClicked;
}
- (id)initWithView:(StreamView*)view {
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings {
self = [self init];
self->streamView = view;
multiTouchesDetected = false;
passthroughGestures = settings.passthroughGestures;
_delayMouseLeftClick = settings.delayLeftClick;
// _delayMouseLeftClick = true;
dragButtonDown = false;
leftClickTimeThreshold = 0.1;
// upper screen check
EDGE_TOLERANCE = 10.0;
_edgeTolerance = settings.edgeSlidingSensitivity.floatValue;
slideGestureVerticalThreshold = CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.4;
screenWidthWithThreshold = CGRectGetWidth([[UIScreen mainScreen] bounds]) - EDGE_TOLERANCE;
screenWidthWithThreshold = CGRectGetWidth([[UIScreen mainScreen] bounds]) - _edgeTolerance;
self->touchPointSpawnedAtUpperScreenEdge = false;
leftClickDelay = ((CGFloat)settings.leftClickDelayMs.intValue)/1000;
return self;
}
- (void)onLongPressStart:(NSTimer*)timer {
// Raise the left click and start a right click
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
if(multiTouchesDetected) return;
if([self touchDidntMoveOnScreen:movingTouchLocation]){
if(_delayMouseLeftClick){
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
if(mouseButtonForCursorMove!=BUTTON_LEFT) LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, mouseButtonForCursorMove);
}
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
dispatch_time_t delayShort = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC));
dispatch_after(delayShort, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
self->rightButtonClicked = true;
});
}
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
rightButtonClicked = false;
if([UITouchUtil touchesIn:streamView from:event].count>=2){
multiTouchesDetected = true;
[longPressTimer invalidate];
longPressTimer = nil;
return;
}
CGPoint initialPoint = [[touches anyObject] locationInView:streamView];
if(initialPoint.y < slideGestureVerticalThreshold && (initialPoint.x < EDGE_TOLERANCE || initialPoint.x > screenWidthWithThreshold)) {
if(initialPoint.y < slideGestureVerticalThreshold && (initialPoint.x < _edgeTolerance || initialPoint.x > screenWidthWithThreshold)) {
self->touchPointSpawnedAtUpperScreenEdge = true;
return; // we're done here. this touch event will not be sent to the remote PC.
}
@@ -72,22 +124,27 @@
touchPointSpawnedAtUpperScreenEdge = false; // reset this flag immediately if we get a touch event passing the check above, this fixes irresponsive touch after closing the command tool menu.
// Ignore touch down events with more than one finger
/*
if ([[event allTouches] count] > 1) {
return;
}
}*/
UITouch* touch = [touches anyObject];
CGPoint touchLocation = [touch locationInView:streamView];
capturedTouch = [touches anyObject];
CGPoint touchLocation = [capturedTouch locationInView:streamView];
touchBeganTimeStamp = capturedTouch.timestamp;
// Don't reposition for finger down events within the deadzone. This makes double-clicking easier.
if (touch.timestamp - lastTouchUp.timestamp > DOUBLE_TAP_DEAD_ZONE_DELAY ||
if (capturedTouch.timestamp - lastTouchUp.timestamp > DOUBLE_TAP_DEAD_ZONE_DELAY ||
sqrt(pow((touchLocation.x / streamView.bounds.size.width) - (lastTouchUpLocation.x / streamView.bounds.size.width), 2) +
pow((touchLocation.y / streamView.bounds.size.height) - (lastTouchUpLocation.y / streamView.bounds.size.height), 2)) > DOUBLE_TAP_DEAD_ZONE_DELTA) {
[streamView updateCursorLocation:touchLocation isMouse:NO];
if(!multiTouchesDetected) [streamView updateCursorLocation:touchLocation isMouse:NO];
}
// Press the left button down
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
if(!_delayMouseLeftClick){
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT); //deprecated
}
// Start the long press timer
longPressTimer = [NSTimer scheduledTimerWithTimeInterval:LONG_PRESS_ACTIVATION_DELAY
@@ -96,8 +153,21 @@
userInfo:nil
repeats:NO];
lastTouchDown = touch;
lastTouchDown = capturedTouch;
lastTouchDownLocation = touchLocation;
movingTouchLocation = touchLocation;
touchBeganLocation = touchLocation;
currentTouchesCount = [UITouchUtil touchesIn:streamView from:event].count;
}
- (void)pauseLeftButtonDrag{
if(dragButtonDown){
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC));
dispatch_after(delay, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, mouseButtonForCursorMove);
});
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
@@ -105,42 +175,90 @@
if(touchPointSpawnedAtUpperScreenEdge) return; // we're done here. this touch event will not be sent to the remote PC.
// Ignore touch move events with more than one finger
/*
if ([[event allTouches] count] > 1) {
return;
}*/
NSSet* currentTouches = [UITouchUtil touchesIn:streamView from:event];
currentTouchesCount = currentTouches.count;
if(currentTouchesCount == 2){
if(mouseButtonForCursorMove!=BUTTON_LEFT) LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, mouseButtonForCursorMove);
if(passthroughGestures) [TouchPadGestureHandler handleGestureIn:streamView with:event];
}
if(currentTouchesCount > 1) return;
UITouch* touch = [touches anyObject];
CGPoint touchLocation = [touch locationInView:streamView];
if(![currentTouches containsObject:capturedTouch]) return;
if (sqrt(pow((touchLocation.x / streamView.bounds.size.width) - (lastTouchDownLocation.x / streamView.bounds.size.width), 2) +
pow((touchLocation.y / streamView.bounds.size.height) - (lastTouchDownLocation.y / streamView.bounds.size.height), 2)) > LONG_PRESS_ACTIVATION_DELTA) {
movingTouchLocation = [capturedTouch locationInView:streamView];
if (sqrt(pow((movingTouchLocation.x / streamView.bounds.size.width) - (lastTouchDownLocation.x / streamView.bounds.size.width), 2) +
pow((movingTouchLocation.y / streamView.bounds.size.height) - (lastTouchDownLocation.y / streamView.bounds.size.height), 2)) > LONG_PRESS_ACTIVATION_DELTA) {
// Moved too far since touch down. Cancel the long press timer.
[longPressTimer invalidate];
longPressTimer = nil;
NSTimeInterval dragDelay = mouseButtonForCursorMove == BUTTON_LEFT ? leftClickTimeThreshold : 0;
if(_delayMouseLeftClick && (CACurrentMediaTime()-touchBeganTimeStamp>dragDelay) && !dragButtonDown){
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, mouseButtonForCursorMove);
dragButtonDown = true;
}
}
[streamView updateCursorLocation:[[touches anyObject] locationInView:streamView] isMouse:NO];
if(!rightButtonClicked) [streamView updateCursorLocation:movingTouchLocation isMouse:NO];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[longPressTimer invalidate];
longPressTimer = nil;
if(TouchPadGestureHandler.ctrlDown) LiSendKeyboardEvent(CommandManager.keyboardButtonMappings[@"CTRL"].shortValue,KEY_ACTION_UP,0);
if(touchPointSpawnedAtUpperScreenEdge) return; // we're done here. this touch event will not be sent to the remote PC.
if(multiTouchesDetected) {
if([UITouchUtil touchesIn:streamView from:event].count == touches.count) multiTouchesDetected = false;
return;
};
if([UITouchUtil touchesIn:streamView from:event].count == touches.count) multiTouchesDetected = false;
// Only fire this logic if all touches have ended
if ([[event allTouches] count] == [touches count]) {
if ([touches containsObject:capturedTouch]) {
// Cancel the long press timer
[longPressTimer invalidate];
longPressTimer = nil;
// Left button up on finger up
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
// Raise right button too in case we triggered a long press gesture
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
// Remember this last touch for touch-down deadzoning
CGPoint touchEndLocation = [capturedTouch locationInView:streamView];
if(_delayMouseLeftClick){
if(CACurrentMediaTime()-touchBeganTimeStamp<leftClickTimeThreshold) {
if(CACurrentMediaTime()-lastTouchUp.timestamp<0.15
&& ![self isAdjacentPoints:touchEndLocation from:lastTouchUpLocation tolerance:30]) [streamView updateCursorLocation:touchEndLocation isMouse:NO];
if([self touchDidntMoveOnScreen:touchEndLocation] && !rightButtonClicked) [self sendShortMouseLeftButtonClickEvent];
}
else if(!rightButtonClicked){
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
if(mouseButtonForCursorMove!=BUTTON_LEFT) LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, mouseButtonForCursorMove);
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
}
}
else{
// Left button up on finger up
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
// Raise right button too in case we triggered a long press gesture
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
}
lastTouchUp = [touches anyObject];
lastTouchUpLocation = [lastTouchUp locationInView:streamView];
dragButtonDown = false;
}
}
@@ -149,4 +267,34 @@
[self touchesEnded:touches withEvent:event];
}
- (void)sendShortMouseLeftButtonClickEvent{
dispatch_time_t delayShort = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(leftClickDelay * NSEC_PER_SEC));
dispatch_time_t delayLong = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.03 * NSEC_PER_SEC));
dispatch_after(delayShort, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
dispatch_after(delayLong, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
});
});
}
- (bool)touchDidntMoveOnScreen:(CGPoint)touchLocation{
return [self isAdjacentPoints:touchLocation from:touchBeganLocation tolerance:3];
}
- (BOOL)isAdjacentPoints:(CGPoint)currentPoint from:(CGPoint)originalPoint tolerance:(CGFloat)tolerance {
bool isAdjacent = hypotf(originalPoint.x - currentPoint.x, originalPoint.y - currentPoint.y) <= hypot(tolerance, tolerance);
return isAdjacent;
}
+ (int)mouseButtonForCursorMove {
return mouseButtonForCursorMove;
}
+ (void)setMouseButtonForCursorMove:(int)button {
mouseButtonForCursorMove = button;
}
@end
@@ -0,0 +1,545 @@
//
// AbstractGamepadOverlayWidgetView.swift
// VoidLink
//
// Created by True砖家 on 2024/4/6.
// Copyright © 2026 True砖家 on Bilibili. All rights reserved.
//
import UIKit
import SwiftUI
@objc
protocol AbstractGamepadOverlayCloseButtonDelegate: AnyObject {
@objc(toggleGamepadOverlayWithOverlayEnabled:)
func toggleGamepadOverlay(withOverlayEnabled overlayEnabled: Bool)
}
private enum AbstractGamepadOverlayPersistence {
private static let defaults = UserDefaults.standard
private enum Key {
static let overlayCenterXRatio = "AbstractGamepadOverlay.overlay.centerXRatio"
static let overlayCenterYRatio = "AbstractGamepadOverlay.overlay.centerYRatio"
static let overlayWidth = "AbstractGamepadOverlay.overlay.width"
}
struct PersistedState {
let centerRatio: CGPoint
let size: CGSize
}
static func loadOverlayState() -> PersistedState? {
loadState(
centerXKey: Key.overlayCenterXRatio,
centerYKey: Key.overlayCenterYRatio,
widthKey: Key.overlayWidth
)
}
static func saveOverlayState(centerRatio: CGPoint, size: CGSize) {
saveState(
centerRatio: centerRatio,
size: size,
centerXKey: Key.overlayCenterXRatio,
centerYKey: Key.overlayCenterYRatio,
widthKey: Key.overlayWidth
)
}
static func centerRatio(for view: UIView, in superview: UIView) -> CGPoint {
let safeWidth = max(superview.bounds.width, 1)
let safeHeight = max(superview.bounds.height, 1)
return CGPoint(x: view.center.x / safeWidth, y: view.center.y / safeHeight)
}
static func center(in superview: UIView, from ratio: CGPoint) -> CGPoint {
CGPoint(
x: min(max(ratio.x, 0), 1) * superview.bounds.width,
y: min(max(ratio.y, 0), 1) * superview.bounds.height
)
}
private static func loadState(
centerXKey: String,
centerYKey: String,
widthKey: String
) -> PersistedState? {
guard defaults.object(forKey: centerXKey) != nil,
defaults.object(forKey: centerYKey) != nil,
defaults.object(forKey: widthKey) != nil else {
return nil
}
let width = defaults.double(forKey: widthKey)
guard width > 0 else { return nil }
return PersistedState(
centerRatio: CGPoint(
x: defaults.double(forKey: centerXKey),
y: defaults.double(forKey: centerYKey)
),
size: CGSize(width: width, height: 0)
)
}
private static func saveState(
centerRatio: CGPoint,
size: CGSize,
centerXKey: String,
centerYKey: String,
widthKey: String
) {
defaults.set(centerRatio.x, forKey: centerXKey)
defaults.set(centerRatio.y, forKey: centerYKey)
defaults.set(size.width, forKey: widthKey)
}
}
@available(iOS 13.0, *)
private struct AbstractGamepadOverlayBridgeView: View {
let usesPlayStationFaceButtons: Bool
private static let overlayClusterGapAdjustmentRatio: CGFloat = 0.13
private static let overlayUpperPrimaryClusterVerticalAdjustmentRatio: CGFloat = -0.05
private static let overlayCenterCompressionRatio: CGFloat = 0
private static let overlayPanelHorizontalInsetRatio: CGFloat = 0.083
private static let overlayTriggerStubSpacingAdjustmentRatio: CGFloat = 0.17
private static let overlayRightShoulderHorizontalOffsetRatio: CGFloat = 0.006
private static let overlayThumbPurpleStrength: CGFloat = 1.15
private static let overlayTriggerPurpleStrength: CGFloat = 2.0
@ObservedObject var stateCenter = GamepadOverlayStateCenter.shared
var body: some View {
AbstractGamepadView(
gamepadType: usesPlayStationFaceButtons ? GamepadType.ps : GamepadType.xbox,
metricsProfile: GamepadMetricsProfile.overlay,
clusterGapAdjustmentRatio: Self.overlayClusterGapAdjustmentRatio,
upperPrimaryClusterVerticalAdjustmentRatio: Self.overlayUpperPrimaryClusterVerticalAdjustmentRatio,
centerCompressionRatio: Self.overlayCenterCompressionRatio,
panelHorizontalInsetRatio: Self.overlayPanelHorizontalInsetRatio,
triggerStubSpacingAdjustmentRatio: Self.overlayTriggerStubSpacingAdjustmentRatio,
rightShoulderHorizontalOffsetRatio: Self.overlayRightShoulderHorizontalOffsetRatio,
thumbPurpleStrength: Self.overlayThumbPurpleStrength,
triggerPurpleStrength: Self.overlayTriggerPurpleStrength,
liveSnapshot: stateCenter.snapshot
)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.allowsHitTesting(false)
.background(Color.clear)
}
}
@available(iOS 13.0, *)
@objc
final class AbstractGamepadOverlayView: UIView {
private enum CloseButtonMetrics {
static let size: CGFloat = 40
static let topInset: CGFloat = -10
static let trailingInset: CGFloat = 0
static let symbolPointSize: CGFloat = 16
static let hideDelay: TimeInterval = 3
}
private enum OverlayMaskMetrics {
static let widthRatio: CGFloat = 0.85
static let heightRatio: CGFloat = 1.03
static let cornerRadiusRatio: CGFloat = 0.326
static let verticalOffsetRatio: CGFloat = -0.006
static let topRectWidthRatio: CGFloat = 0.6
static let topRectHeightRatio: CGFloat = 0.15
static let topRectCornerRadiusRatio: CGFloat = 0.22
static let topRectVerticalOffsetRatio: CGFloat = -0.5
}
private var hostingControllerBox: AnyObject?
private weak var hostedView: UIView?
private let visualMaskLayer = CAShapeLayer()
private let usesPlayStationFaceButtons: Bool
private let baseRenderWidth: CGFloat = 420
private let baseAspectRatio: CGFloat = 1.82
private let initialDisplayWidth: CGFloat
private var pinchBaseWidth: CGFloat = 0
private var panStartCenter: CGPoint = .zero
private var activeGestureCount = 0
private var didRestorePersistedState = false
private var hideCloseButtonWorkItem: DispatchWorkItem?
private let minWidth: CGFloat = 70
private let maxWidth: CGFloat = 520
private let doubleTapWidths: [CGFloat] = [PublicUtils.isIPhone ? 90 : 130, PublicUtils.isIPhone ? 165 : 200]
private lazy var closeButton: UIButton = makeCloseButton()
@objc weak var closeButtonDelegate: AbstractGamepadOverlayCloseButtonDelegate?
@objc init(frame: CGRect, usesPlayStationFaceButtons: Bool) {
self.usesPlayStationFaceButtons = usesPlayStationFaceButtons
self.initialDisplayWidth = frame.width > 0 ? frame.width : baseRenderWidth
super.init(frame: CGRect(origin: frame.origin, size: CGSize(width: baseRenderWidth, height: baseRenderWidth / baseAspectRatio)))
setupHostedViewIfNeeded()
}
override init(frame: CGRect) {
self.usesPlayStationFaceButtons = false
self.initialDisplayWidth = frame.width > 0 ? frame.width : baseRenderWidth
super.init(frame: CGRect(origin: frame.origin, size: CGSize(width: baseRenderWidth, height: baseRenderWidth / baseAspectRatio)))
setupHostedViewIfNeeded()
}
required init?(coder: NSCoder) {
self.usesPlayStationFaceButtons = false
self.initialDisplayWidth = baseRenderWidth
super.init(coder: coder)
setupHostedViewIfNeeded()
}
private func setupHostedViewIfNeeded() {
guard hostedView == nil else { return }
let hostingController = UIHostingController(rootView: AbstractGamepadOverlayBridgeView(usesPlayStationFaceButtons: usesPlayStationFaceButtons))
hostingController.view.backgroundColor = .clear
hostingController.view.isUserInteractionEnabled = false
hostingController.view.translatesAutoresizingMaskIntoConstraints = true
hostingController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
hostingController.view.frame = bounds
hostingControllerBox = hostingController
hostedView = hostingController.view
backgroundColor = .clear
clipsToBounds = false
isMultipleTouchEnabled = true
applyScale(forTargetWidth: initialDisplayWidth)
addSubview(hostingController.view)
addSubview(closeButton)
installGesturesIfNeeded()
}
override func layoutSubviews() {
super.layoutSubviews()
hostedView?.frame = bounds
layoutCloseButton()
updateVisualMask()
restorePersistedStateIfNeeded()
}
override func didMoveToSuperview() {
super.didMoveToSuperview()
didRestorePersistedState = false
restorePersistedStateIfNeeded()
}
private func installGesturesIfNeeded() {
guard gestureRecognizers?.isEmpty != false else { return }
let singleTap = UITapGestureRecognizer(target: self, action: #selector(handleSingleTap(_:)))
singleTap.numberOfTapsRequired = 1
singleTap.cancelsTouchesInView = true
let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
pan.minimumNumberOfTouches = 1
pan.maximumNumberOfTouches = 1
pan.cancelsTouchesInView = true
let pinch = UIPinchGestureRecognizer(target: self, action: #selector(handlePinch(_:)))
pinch.cancelsTouchesInView = true
let doubleTap = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap(_:)))
doubleTap.numberOfTapsRequired = 2
doubleTap.cancelsTouchesInView = true
singleTap.delegate = self
pan.delegate = self
pinch.delegate = self
doubleTap.delegate = self
pan.require(toFail: doubleTap)
singleTap.require(toFail: pan)
addGestureRecognizer(pan)
addGestureRecognizer(singleTap)
addGestureRecognizer(pinch)
addGestureRecognizer(doubleTap)
}
@objc private func handleSingleTap(_ recognizer: UITapGestureRecognizer) {
guard recognizer.state == .ended else { return }
registerUserInteraction()
presentFirstTouchTipIfNeeded()
scheduleCloseButtonHideIfNeeded()
}
@objc private func handlePan(_ recognizer: UIPanGestureRecognizer) {
guard let superview else { return }
switch recognizer.state {
case .began:
registerUserInteraction()
presentFirstTouchTipIfNeeded()
beginGestureSession()
panStartCenter = center
case .changed, .ended:
registerUserInteraction()
let translation = recognizer.translation(in: superview)
center = CGPoint(x: panStartCenter.x + translation.x, y: panStartCenter.y + translation.y)
if recognizer.state == .ended {
endGestureSession()
}
case .cancelled, .failed:
endGestureSession()
default:
break
}
}
@objc private func handlePinch(_ recognizer: UIPinchGestureRecognizer) {
switch recognizer.state {
case .began:
registerUserInteraction()
presentFirstTouchTipIfNeeded()
beginGestureSession()
pinchBaseWidth = frame.width
case .changed, .ended:
registerUserInteraction()
let nextWidth = max(minWidth, min(maxWidth, pinchBaseWidth * recognizer.scale))
applyScale(forTargetWidth: nextWidth)
if recognizer.state == .ended {
endGestureSession()
}
case .cancelled, .failed:
endGestureSession()
default:
break
}
}
@objc private func handleDoubleTap(_ recognizer: UITapGestureRecognizer) {
guard recognizer.state == .ended else { return }
registerUserInteraction()
presentFirstTouchTipIfNeeded()
applyScale(forTargetWidth: nextDoubleTapWidth())
persistState()
scheduleCloseButtonHideIfNeeded()
}
private func updateVisualMask() {
guard bounds.width > 1, bounds.height > 1 else {
visualMaskLayer.path = nil
hostedView?.layer.mask = nil
return
}
let maskWidth = bounds.width * OverlayMaskMetrics.widthRatio
let maskHeight = bounds.height * OverlayMaskMetrics.heightRatio
let maskRect = CGRect(
x: (bounds.width - maskWidth) * 0.5,
y: (bounds.height - maskHeight) * 0.5 + bounds.height * OverlayMaskMetrics.verticalOffsetRatio,
width: maskWidth,
height: maskHeight
)
let maskCornerRadius = min(maskRect.width, maskRect.height) * OverlayMaskMetrics.cornerRadiusRatio
let topRectWidth = bounds.width * OverlayMaskMetrics.topRectWidthRatio
let topRectHeight = bounds.height * OverlayMaskMetrics.topRectHeightRatio
let topRect = CGRect(
x: (bounds.width - topRectWidth) * 0.5,
y: (bounds.height - topRectHeight) * 0.5 + bounds.height * OverlayMaskMetrics.topRectVerticalOffsetRatio,
width: topRectWidth,
height: topRectHeight
)
let topRectCornerRadius = min(topRect.width, topRect.height) * OverlayMaskMetrics.topRectCornerRadiusRatio
visualMaskLayer.frame = bounds
let path = UIBezierPath(
roundedRect: maskRect,
byRoundingCorners: .allCorners,
cornerRadii: CGSize(width: maskCornerRadius, height: maskCornerRadius)
)
path.append(
UIBezierPath(
roundedRect: topRect,
byRoundingCorners: .allCorners,
cornerRadii: CGSize(width: topRectCornerRadius, height: topRectCornerRadius)
)
)
visualMaskLayer.path = path.cgPath
hostedView?.layer.mask = visualMaskLayer
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
bounds.contains(point)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
registerUserInteraction()
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
registerUserInteraction()
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
scheduleCloseButtonHideIfNeeded()
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesCancelled(touches, with: event)
scheduleCloseButtonHideIfNeeded()
}
private func restorePersistedStateIfNeeded() {
guard !didRestorePersistedState,
let superview,
bounds.width > 0,
bounds.height > 0,
superview.bounds.width > 0,
superview.bounds.height > 0 else { return }
defer { didRestorePersistedState = true }
guard let state = AbstractGamepadOverlayPersistence.loadOverlayState() else { return }
applyScale(forTargetWidth: state.size.width)
center = AbstractGamepadOverlayPersistence.center(in: superview, from: state.centerRatio)
}
private func beginGestureSession() {
activeGestureCount += 1
}
@objc func registerUserInteraction() {
showCloseButton()
cancelPendingCloseButtonHide()
}
private func presentFirstTouchTipIfNeeded() {
guard GenericUtils.isFirstLaunchGamepadOverlayFeature() else { return }
guard let presentingViewController = nearestViewController() else { return }
AlertControllerUtil.showAlert(
in: presentingViewController,
title: GenericUtils.gamepadOverlayFeatureTipTitle(),
message: GenericUtils.gamepadOverlayFeatureTipMessage(),
withCancel: false,
buttonTitle: LocalizationHelper.localizedString(forKey: "This tip won't be shown again"),
countdown: 6
)
}
private func nearestViewController() -> UIViewController? {
var responder: UIResponder? = self
while let current = responder {
if let viewController = current as? UIViewController {
return viewController
}
responder = current.next
}
return nil
}
private func endGestureSession() {
activeGestureCount = max(0, activeGestureCount - 1)
guard activeGestureCount == 0 else { return }
persistState()
scheduleCloseButtonHideIfNeeded()
}
private func persistState() {
guard let superview,
superview.bounds.width > 0,
superview.bounds.height > 0 else { return }
AbstractGamepadOverlayPersistence.saveOverlayState(
centerRatio: AbstractGamepadOverlayPersistence.centerRatio(for: self, in: superview),
size: frame.size
)
}
private func nextDoubleTapWidth() -> CGFloat {
let currentWidth = frame.width
return abs(currentWidth - doubleTapWidths[0]) <= abs(currentWidth - doubleTapWidths[1])
? doubleTapWidths[1]
: doubleTapWidths[0]
}
private func applyScale(forTargetWidth targetWidth: CGFloat) {
let clampedWidth = max(minWidth, min(maxWidth, targetWidth))
let targetScale = clampedWidth / baseRenderWidth
transform = CGAffineTransform(scaleX: targetScale, y: targetScale)
}
private func makeCloseButton() -> UIButton {
let button = UIButton(type: .system)
button.tintColor = UIColor.black.withAlphaComponent(0.66)
button.backgroundColor = UIColor.white.withAlphaComponent(0.78)
button.layer.borderWidth = 1
button.layer.borderColor = UIColor.white.withAlphaComponent(0.9).cgColor
button.layer.shadowColor = UIColor.black.withAlphaComponent(0.05).cgColor
button.layer.shadowOpacity = 1
button.layer.shadowRadius = 4
button.layer.shadowOffset = CGSize(width: 0, height: 2)
button.alpha = 0
button.isHidden = true
button.adjustsImageWhenHighlighted = false
button.addTarget(self, action: #selector(handleCloseButtonTap), for: .touchUpInside)
let configuration = UIImage.SymbolConfiguration(pointSize: CloseButtonMetrics.symbolPointSize, weight: .bold)
button.setImage(UIImage(systemName: "xmark", withConfiguration: configuration), for: .normal)
return button
}
private func layoutCloseButton() {
let size = CGFloat(Int(CloseButtonMetrics.size/2))*2
closeButton.frame = CGRect(
x: bounds.width - CloseButtonMetrics.trailingInset - size,
y: CloseButtonMetrics.topInset,
width: size,
height: size
)
closeButton.layer.cornerRadius = size * 0.5
}
private func showCloseButton() {
closeButton.isHidden = false
guard closeButton.alpha < 1 else { return }
UIView.animate(withDuration: 0.18) {
self.closeButton.alpha = 1
}
}
private func hideCloseButton() {
guard !closeButton.isHidden else { return }
UIView.animate(
withDuration: 0.18,
animations: {
self.closeButton.alpha = 0
},
completion: { _ in
self.closeButton.isHidden = true
}
)
}
private func cancelPendingCloseButtonHide() {
hideCloseButtonWorkItem?.cancel()
hideCloseButtonWorkItem = nil
}
@objc func scheduleCloseButtonHideIfNeeded() {
guard activeGestureCount == 0 else { return }
cancelPendingCloseButtonHide()
let workItem = DispatchWorkItem { [weak self] in
self?.hideCloseButton()
self?.hideCloseButtonWorkItem = nil
}
hideCloseButtonWorkItem = workItem
DispatchQueue.main.asyncAfter(deadline: .now() + CloseButtonMetrics.hideDelay, execute: workItem)
}
@objc private func handleCloseButtonTap() {
cancelPendingCloseButtonHide()
closeButtonDelegate?.toggleGamepadOverlay(withOverlayEnabled: false)
}
}
@available(iOS 13.0, *)
extension AbstractGamepadOverlayView: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
(gestureRecognizer is UIPanGestureRecognizer && otherGestureRecognizer is UIPinchGestureRecognizer)
|| (gestureRecognizer is UIPinchGestureRecognizer && otherGestureRecognizer is UIPanGestureRecognizer)
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
guard let touchedView = touch.view else { return true }
return touchedView !== closeButton && !touchedView.isDescendant(of: closeButton)
}
}
+373 -186
View File
@@ -1,5 +1,5 @@
//
// KeyManager.swift
// CommandManager.swift
// VoidLink
//
// Created by True砖家 on 2024/7/23.
@@ -19,12 +19,14 @@ import UIKit
// MARK: - Properties
@objc public private(set) var identifier: String
@objc var cmdString: String
@objc var alias: String
// MARK: - Initialization
init(cmdString: String, alias: String) {
self.identifier = UUID().uuidString
self.cmdString = cmdString
self.alias = alias
}
@@ -36,11 +38,13 @@ import UIKit
let alias = coder.decodeObject(of: NSString.self, forKey: "alias") as String? else {
return nil
}
self.identifier = coder.decodeObject(of: NSString.self, forKey: "identifier") as String? ?? UUID().uuidString
self.cmdString = cmdString
self.alias = alias
}
public func encode(with coder: NSCoder) {
coder.encode(identifier, forKey: "identifier")
coder.encode(cmdString, forKey: "keyboardCmdString")
coder.encode(alias, forKey: "alias")
}
@@ -50,6 +54,29 @@ import UIKit
// Define the CommandManager class
@objc public class CommandManager: NSObject {
@objc public static let shared = CommandManager()
private static let defaultCommandAliasMigrationMap: [String: String] = [
"任务管理器(Task Manager)": "=taskManager",
"Task Manager": "=taskManager",
"N卡截图(Nvidia Screenshot)": "=nvidiaShot",
"Nvidia Screenshot": "=nvidiaShot",
"N卡录屏(Nvidia Screen Recording)": "=nvidiaRec",
"Nvidia Screen Recording": "=nvidiaRec",
"关闭窗口(ALT+F4)": "=closeWindow",
"Close Window": "=closeWindow",
"全选(Select All)": "=selectAll",
"Select All": "=selectAll",
"复制(Copy)": "=copy",
"Copy": "=copy",
"粘贴(Paste)": "=paste",
"Paste": "=paste",
"切换桌面(Switch to Desktop)": "=switchDesktop",
"Switch to Desktop": "=switchDesktop",
"多显模式(Project)": "=projectDisplay",
"Project": "=projectDisplay",
"Xbox Game Bar": "=xboxGameBar",
"Steam Overlay": "=steamOverlay",
]
@objc public static let mouseButtonMappings: [String: Int32] = [
"M_LEFT" : BUTTON_LEFT,
@@ -61,7 +88,9 @@ import UIKit
"M_X1" : BUTTON_X1,
"MX1" : BUTTON_X1,
"M_X2" : BUTTON_X2,
"MX2" : BUTTON_X2
"MX2" : BUTTON_X2,
"WHEELUP" : 0xFF,
"WHEELDOWN" : -0xFF,
]
@@ -96,6 +125,7 @@ import UIKit
"PADDLE3" : PADDLE3_FLAG,
"PADDLE4" : PADDLE4_FLAG,
"MISC" : MISC_FLAG,
"OSCHOME" : SPECIAL_FLAG,
"OSCL2" : 0,
"L2" : 0,
"LT" : 0,
@@ -115,19 +145,74 @@ import UIKit
"OSCBACK"
]
@objc public static let touchPadCmds: [String] = ["LSVPAD", "RSVPAD", "LSPAD", "RSPAD", "DS4TOUCH", "MOUSEPAD", "DPAD", "TRACKBALL", "WASDPAD", "ARROWPAD"]
@objc public static let touchPadCmds: [String] = ["LSVPAD", "RSVPAD", "LSPAD", "RSPAD", "LSWHEEL", "RSWHEEL", "LTPAD", "RTPAD", "DS4TOUCH", "MOUSEPAD", "ABSMOUSEPAD", "ABSMOUSE", "MOUSEWHEEL", "WHEEL", "DISCRETEWHEEL", "DSWHEEL", "DPAD", "TRACKBALL", "WASDPAD", "ARROWPAD", "MAGNIFIER", "DUMMYPAD"]
@objc public static let mousePadWithButtonActions: [String] = ["MOUSEPAD", "ABSMOUSE", "ABSMOUSEPAD"]
@objc public static let mousePads: [String] = ["MOUSEPAD", "ABSMOUSE", "TRACKBALL", "ABSMOUSEPAD"]
@objc public static let directionPads: [String] = ["DPAD", "WASDPAD", "ARROWPAD"]
@objc public static let stickTouchPads: [String] = ["LSVPAD", "RSVPAD", "LSPAD", "RSPAD"]
@objc public static let nonVectorStickPads: [String] = ["LSPAD", "RSPAD"]
@objc public static let specialOverlayButtonCmds: [String] = ["SETTINGS"]
@objc public static let displacementBasedStickPads: [String] = ["LSPAD", "RSPAD"]
@objc public static let stickWheels: [String] = ["LSWHEEL", "RSWHEEL"]
@objc public static let velocityBasedTouchPads: [String] = ["LSVPAD", "RSVPAD", "MOUSEPAD", "TRACKBALL", "LTPAD", "RTPAD", "MOUSEWHEEL", "WHEEL"]
@objc public static let inertialTouchPads: [String] = ["LSVPAD", "RSVPAD", "TRACKBALL", "LSPAD", "RSPAD"]
@objc public static let verticalTouchPads: [String] = ["LTPAD", "RTPAD", "DISCRETEWHEEL", "DSWHEEL"]
@objc public static let singleDimensionActiveTouchpads: [String] = ["LTPAD", "RTPAD", "MOUSEWHEEL", "WHEEL", "DISCRETEWHEEL", "DSWHEEL"]
@objc public static let functionalButtonCmds: [String] = [
"SETTINGS",
"TOOLBOX",
"DISCONNECT",
"QUITAPP",
"PIP",
"WIDGETTOOL",
"ABSGAMEPAD",
"WIDGETPROFILES",
"PROFILES",
"PICKPROFILE",
"PICKPRFL",
"SOFTKEYBOARD",
"ABSTCHDRAG",
"FOLDER",
"LABELEDFOLDER",
"PRESSURECURVE",
"PENCILHOVER",
"BRUSH",
"ERASER",
"NOSINGLETOUCH",
"DISABLETILT",
"DISABLETOUCH",
"GAMEPADOVERLAY",
]
@objc public static let functionalTouchPadCmds: [String] = [
"MAGNIFIER"
]
@objc public static let pencilProButtonCmds: [String] = [
"PENCILHOVER",
"BRUSH",
"ERASER",
"NOSINGLETOUCH",
"DISABLETILT"
]
@objc public static let motionControlButtonCmds: [String] = ["GYRO","GYROPAUSE","ACCEL","MOTION"]
// @objc public static let specialGameWidgets: [String] = ["YSRSV", "YSLT", "YSRT", "YSRB", "YSB", "YSRT2", "YSRB2", "YSB2", "YSEM", "YSML", "YSMR", "YSWASD"]
static let keyboardButtonMappings: [String: Int16] = [
@objc public static let shortcutAllowedFunctionalButtonMappings: [String: Int16] = [
"BRUSH": 0xFF,
"ERASER": 0xFF,
"FOLDER": 0xFF,
"LABELEDFOLDER": 0xFF,
]
@objc public static let keyboardButtonMappings: [String: Int16] = [
// Windows Key Codes
"NULL": 0xFF,
"CTRL": 0x11, // VK_CONTROL
"RCTRL": 0xA3, // VK_RCONTROL
"SHIFT": 0x10, // VK_SHIFT
"RSHIFT": 0xA1, // VK_RSHIFT
"ALT": 0x12, // VK_MENU
"RALT": 0xA5, // VK_MENU
"F1": 0x70, // VK_F1
"F2": 0x71, // VK_F2
"F3": 0x72, // VK_F3
@@ -263,111 +348,131 @@ import UIKit
"LAUNCHAPP2": 0xB7, // VK_LAUNCH_APP2
"WIN": 0x5B,
"LEFT_WIN": 0x5B, // VK_LWIN
"LEFTWIN": 0x5B, // VK_LWIN
"LWIN": 0x5B, // VK_LWIN
"RIGHT_WIN": 0x5C, // VK_RWIN
"RIGHTWIN": 0x5C, // VK_RWIN
"RWIN": 0x5C, // VK_RWIN
"APPS": 0x5D, // VK_APPS
// macOS Key Codes
"CMD": 0x37, // ⌘ Command
"OPT": 0x3A, // ⌥ Option
"CONTROL": 0x3B, // ⌃ Control
"FUNCTION": 0x3F, // fn
"SHIFTMAC": 0x38, // ⇧ Shift
"DELETEMAC": 0x75, // Forward Delete
"RETURNMAC": 0x24, // Return
"ENTERMAC": 0x4C, // Enter
"ESCAPEMAC": 0x35, // Escape
"TABMAC": 0x30, // Tab
"SPACEMAC": 0x31, // Space
"UPARRMAC": 0x7E, // Up Arrow
"DOWNARRMAC": 0x7D, // Down Arrow
"LEFTARRMAC": 0x7B, // Left Arrow
"RIGHTARRMAC": 0x7C, // Right Arrow
"F1MAC": 0x7A, // F1
"F2MAC": 0x78, // F2
"F3MAC": 0x63, // F3
"F4MAC": 0x76, // F4
"F5MAC": 0x60, // F5
"F6MAC": 0x61, // F6
"F7MAC": 0x62, // F7
"F8MAC": 0x64, // F8
"F9MAC": 0x65, // F9
"F10MAC": 0x6D, // F10
"F11MAC": 0x67, // F11
"F12MAC": 0x6F, // F12
"0MAC": 0x52, // 0
"1MAC": 0x53, // 1
"2MAC": 0x54, // 2
"3MAC": 0x55, // 3
"4MAC": 0x56, // 4
"5MAC": 0x57, // 5
"6MAC": 0x58, // 6
"7MAC": 0x59, // 7
"8MAC": 0x5A, // 8
"9MAC": 0x5B, // 9
"NUMPAD0MAC": 0x4F, // Numpad 0
"NUMPAD1MAC": 0x50, // Numpad 1
"NUMPAD2MAC": 0x51, // Numpad 2
"NUMPAD3MAC": 0x52, // Numpad 3
"NUMPAD4MAC": 0x53, // Numpad 4
"NUMPAD5MAC": 0x54, // Numpad 5
"NUMPAD6MAC": 0x55, // Numpad 6
"NUMPAD7MAC": 0x56, // Numpad 7
"NUMPAD8MAC": 0x57, // Numpad 8
"NUMPAD9MAC": 0x58, // Numpad 9
"NUMPADADDMAC": 0x45, // Numpad Add
"NUMPADSUBTRACTMAC": 0x4A, // Numpad Subtract
"NUMPADMULTIPLYMAC": 0x43, // Numpad Multiply
"NUMPADDIVIDEMAC": 0x4B, // Numpad Divide
"NUMPADDECIMALMAC": 0x41, // Numpad Decimal
"SHIFT_MAC": 0x38, // ⇧ Shift
"DELETE_MAC": 0x75, // Forward Delete
"RETURN_MAC": 0x24, // Return
"ENTER_MAC": 0x4C, // Enter
"ESCAPE_MAC": 0x35, // Escape
"TAB_MAC": 0x30, // Tab
"SPACE_MAC": 0x31, // Space
"UP_ARROW_MAC": 0x7E, // Up Arrow
"DOWN_ARROW_MAC": 0x7D, // Down Arrow
"LEFT_ARROW_MAC": 0x7B, // Left Arrow
"RIGHT_ARROW_MAC": 0x7C, // Right Arrow
"F1_MAC": 0x7A, // F1
"F2_MAC": 0x78, // F2
"F3_MAC": 0x63, // F3
"F4_MAC": 0x76, // F4
"F5_MAC": 0x60, // F5
"F6_MAC": 0x61, // F6
"F7_MAC": 0x62, // F7
"F8_MAC": 0x64, // F8
"F9_MAC": 0x65, // F9
"F10_MAC": 0x6D, // F10
"F11_MAC": 0x67, // F11
"F12_MAC": 0x6F, // F12
"0_MAC": 0x52, // 0
"1_MAC": 0x53, // 1
"2_MAC": 0x54, // 2
"3_MAC": 0x55, // 3
"4_MAC": 0x56, // 4
"5_MAC": 0x57, // 5
"6_MAC": 0x58, // 6
"7_MAC": 0x59, // 7
"8_MAC": 0x5A, // 8
"9_MAC": 0x5B, // 9
"NUMPAD0_MAC": 0x4F, // Numpad 0
"NUMPAD1_MAC": 0x50, // Numpad 1
"NUMPAD2_MAC": 0x51, // Numpad 2
"NUMPAD3_MAC": 0x52, // Numpad 3
"NUMPAD4_MAC": 0x53, // Numpad 4
"NUMPAD5_MAC": 0x54, // Numpad 5
"NUMPAD6_MAC": 0x55, // Numpad 6
"NUMPAD7_MAC": 0x56, // Numpad 7
"NUMPAD8_MAC": 0x57, // Numpad 8
"NUMPAD9_MAC": 0x58, // Numpad 9
"NUMPAD_ADD_MAC": 0x45, // Numpad Add
"NUMPAD_SUBTRACT_MAC": 0x4A, // Numpad Subtract
"NUMPAD_MULTIPLY_MAC": 0x43, // Numpad Multiply
"NUMPAD_DIVIDE_MAC": 0x4B, // Numpad Divide
"NUMPAD_DECIMAL_MAC": 0x41, // Numpad Decimal
"ESCAPEMAC": 0x35,
"TABMAC": 0x30,
"RETURNMAC": 0x24,
"BACKSPACEMAC": 0x33,
"SPACEMAC": 0x31,
"F1MAC": 0x7A,
"F2MAC": 0x78,
"F3MAC": 0x63,
"F4MAC": 0x76,
"F5MAC": 0x60,
"F6MAC": 0x61,
"F7MAC": 0x62,
"F8MAC": 0x64,
"F9MAC": 0x65,
"F10MAC": 0x6D,
"F11MAC": 0x67,
"F12MAC": 0x6F,
"F13MAC": 0x69,
"F14MAC": 0x6B,
"F15MAC": 0x71,
"GRAVEACCENTMAC": 0x32,
"1MAC": 0x12,
"2MAC": 0x13,
"3MAC": 0x14,
"4MAC": 0x15,
"5MAC": 0x17,
"6MAC": 0x16,
"7MAC": 0x1A,
"8MAC": 0x1C,
"9MAC": 0x19,
"0MAC": 0x1D,
"MINUSMAC": 0x1B,
"EQUALSMAC": 0x18,
"QMAC": 0x0C,
"WMAC": 0x0D,
"EMAC": 0x0E,
"RMAC": 0x0F,
"TMAC": 0x11,
"YMAC": 0x10,
"UMAC": 0x20,
"IMAC": 0x22,
"OMAC": 0x1F,
"PMAC": 0x23,
"OPENBRACKETMAC": 0x21,
"CLOSEBRACKETMAC": 0x1E,
"BACKSLASHMAC": 0x2A,
"CAPSLOCKMAC": 0x39,
"AMAC": 0x00,
"SMAC": 0x01,
"DMAC": 0x02,
"FMAC": 0x03,
"GMAC": 0x05,
"HMAC": 0x04,
"JMAC": 0x26,
"KMAC": 0x28,
"LMAC": 0x25,
"SEMICOLONMAC": 0x29,
"SINGLEQUOTEMAC": 0x27,
"SHIFTMAC": 0x38,
"ZMAC": 0x06,
"XMAC": 0x07,
"CMAC": 0x08,
"VMAC": 0x09,
"BMAC": 0x0B,
"NMAC": 0x2D,
"MMAC": 0x2E,
"COMMAMAC": 0x2B,
"PERIODMAC": 0x2F,
"FORWARDSLASHMAC": 0x2C,
"CONTROL": 0x3B,
"RIGHTCONTROL": 0x3E,
"OPT": 0x3A,
"RIGHTOPT": 0x3D,
"CMD": 0x37,
"RIGHTCMD": 0x36,
"FUNCTION": 0x3F,
"HOMEMAC": 0x73,
"ENDMAC": 0x77,
"PGUPMAC": 0x74,
"PGDNMAC": 0x79,
"LEFTARRMAC": 0x7B,
"RIGHTARRMAC": 0x7C,
"DOWNARRMAC": 0x7D,
"UPARRMAC": 0x7E,
"DELMAC": 0x75,
"CONTEXTMAC": 0x72,
"NUMPADCLEARMAC": 0x47,
"NUMPADDIVIDEMAC": 0x4B,
"NUMPADMULTIPLYMAC": 0x43,
"NUMPADSUBTRACTMAC": 0x4E,
"NUMPADADDMAC": 0x45,
"NUMPAD0MAC": 0x4F,
"NUMPAD1MAC": 0x50,
"NUMPAD2MAC": 0x51,
"NUMPAD3MAC": 0x52,
"NUMPAD4MAC": 0x53,
"NUMPAD5MAC": 0x54,
"NUMPAD6MAC": 0x55,
"NUMPAD7MAC": 0x56,
"NUMPAD8MAC": 0x57,
"NUMPAD9MAC": 0x58,
"NUMPADDECIMALMAC": 0x41,
"NUMPADEQUALMAC": 0x51,
"ENTERMAC": 0x4C,
]
private var commands: [RemoteCommand] = []
@@ -383,21 +488,21 @@ import UIKit
let defaults = UserDefaults.standard
//if true { // save default entries if the data is empty.
if defaults.data(forKey: "savedCommands") == nil { // save default entries if the data is empty.
var defaultCommands: [RemoteCommand] = [
let defaultCommands: [RemoteCommand] = [
RemoteCommand(cmdString: "WIN", alias: "WIN"),
RemoteCommand(cmdString: "F11", alias: "F11"),
RemoteCommand(cmdString: "ESC", alias: "ESC"),
RemoteCommand(cmdString: "CTRL+SHIFT+ESC", alias: "任务管理器(Task Manager)"),
RemoteCommand(cmdString: "ALT+F1", alias: "N卡截图(Nvidia Screenshot)"),
RemoteCommand(cmdString: "ALT+F9", alias: "N卡录屏(Nvidia Screen Recording)"),
RemoteCommand(cmdString: "ALT+F4", alias: "关闭窗口(ALT+F4)"),
RemoteCommand(cmdString: "CTRL+A", alias: "全选(Select All)"),
RemoteCommand(cmdString: "CTRL+C", alias: "复制(Copy)"),
RemoteCommand(cmdString: "CTRL+V", alias: "粘贴(Paste)"),
RemoteCommand(cmdString: "WIN+D", alias: "切换桌面(Switch to Desktop)"),
RemoteCommand(cmdString: "WIN+P", alias: "多显模式(Project)"),
RemoteCommand(cmdString: "WIN+G", alias: "Xbox Game Bar"),
RemoteCommand(cmdString: "SHIFT+TAB", alias: "Steam Overlay"),
RemoteCommand(cmdString: "CTRL+SHIFT+ESC", alias: "=taskManager"),
RemoteCommand(cmdString: "ALT+F1", alias: "=nvidiaShot"),
RemoteCommand(cmdString: "ALT+F9", alias: "=nvidiaRec"),
RemoteCommand(cmdString: "ALT+F4", alias: "=closeWindow"),
RemoteCommand(cmdString: "CTRL+A", alias: "=selectAll"),
RemoteCommand(cmdString: "CTRL+C", alias: "=copy"),
RemoteCommand(cmdString: "CTRL+V", alias: "=paste"),
RemoteCommand(cmdString: "WIN+D", alias: "=switchDesktop"),
RemoteCommand(cmdString: "WIN+P", alias: "=projectDisplay"),
RemoteCommand(cmdString: "WIN+G", alias: "=xboxGameBar"),
RemoteCommand(cmdString: "SHIFT+TAB", alias: "=steamOverlay"),
]
let data = try? NSKeyedArchiver.archivedData(withRootObject: defaultCommands, requiringSecureCoding: false)
@@ -410,22 +515,26 @@ import UIKit
}
// extractKeyStrings from keyboardCMDString
@objc public func extractKeyStringsFromComboCommand(from input: String) -> [String]? {
let keys = CommandManager.keyboardButtonMappings.keys.joined(separator: "|")
@objc public func extractAutoReleaseButtonStrings(from cmd: String) -> [String]? {
let cmd = cmd.uppercased()
let mergedKeys = (Set(CommandManager.keyboardButtonMappings.keys)
.union(Set(CommandManager.mouseButtonMappings.keys))
.union(Set(CommandManager.shortcutAllowedFunctionalButtonMappings.keys)))
let keys = mergedKeys.joined(separator: "|")
let pattern = "^(?:(\(keys))(?:\\+(\(keys))*)*)$"
guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else {
print("Failed to create regex")
return nil
}
let range = NSRange(location: 0, length: input.utf16.count)
guard let match = regex.firstMatch(in: input, options: [], range: range) else {
print("No match found for input: \(input)")
let range = NSRange(location: 0, length: cmd.utf16.count)
guard let match = regex.firstMatch(in: cmd, options: [], range: range) else {
print("No match found for input: \(cmd)")
return nil
}
// print("Regex matched for input: \(input)")
let matchedString = (input as NSString).substring(with: match.range(at: 0))
let matchedString = (cmd as NSString).substring(with: match.range(at: 0))
let keyStrings = matchedString.split(separator: "+").map { String($0) }
guard !keyStrings.isEmpty else {
@@ -436,7 +545,10 @@ import UIKit
var validKeyStrings: [String] = []
for key in keyStrings {
if CommandManager.keyboardButtonMappings.keys.contains(key) {
if (CommandManager.keyboardButtonMappings.keys.contains(key)
|| CommandManager.mouseButtonMappings.keys.contains(key)
|| CommandManager.shortcutAllowedFunctionalButtonMappings.keys.contains(key)
) {
validKeyStrings.append(key)
} else {
print(" '\(key)' is not defined in key mappings")
@@ -450,7 +562,7 @@ import UIKit
}
for (index, key) in validKeyStrings.enumerated() {
print("Valid Key \(index): \(key)")
// print("Valid Key \(index): \(key)")
}
return validKeyStrings
@@ -458,61 +570,80 @@ import UIKit
}
//super combo key button strings
@objc public func extractSinglCmdStringsFromComboKeys(from input: String) -> [String]? {
let combinedStrings = [CommandManager.keyboardButtonMappings.keys.map { $0 as String },
CommandManager.oscButtonMappings.keys.map { $0 as String },
CommandManager.mouseButtonMappings.keys.map { $0 as String },
CommandManager.touchPadCmds.map { $0 as String }
]
.lazy
.flatMap { $0 } // 三维展开
.map(String.init(describing:)) // 安全类型转换
let keys = combinedStrings.joined(separator: "|")
let pattern = "^(?:\(keys))(?:-(?:\(keys)))*(?:-\\d+MS)?$"
guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else {
print("Failed to create regex")
return nil
}
let range = NSRange(location: 0, length: input.utf16.count)
guard let match = regex.firstMatch(in: input, options: [], range: range) else {
print("No match found for input: \(input)")
return nil
}
// print("Regex matched for input: \(input)")
let matchedString = (input as NSString).substring(with: match.range(at: 0))
let cmdStrings = matchedString.split(separator: "-").map { String($0) }
guard !cmdStrings.isEmpty else {
print("No key strings found in the matched string")
return nil
}
@objc public func extractCmdStrings(from input: String) -> [String]? {
var validCmdStrings: [String] = []
for key in cmdStrings {
validCmdStrings.append(key)
if PublicUtils.isGUIWidgetPickerAvailable {
let cmdStrings = input
.split(separator: "-")
.map { String($0) }
guard !cmdStrings.isEmpty else {
print("No key strings found in the input string")
return nil
}
for key in cmdStrings {
validCmdStrings.append(key)
}
}
if validCmdStrings.isEmpty {
print("No valid key strings found in the matched string")
return nil
else {
let input = input.uppercased()
let combinedStrings = [CommandManager.keyboardButtonMappings.keys.map { $0 as String },
CommandManager.oscButtonMappings.keys.map { $0 as String },
CommandManager.mouseButtonMappings.keys.map { $0 as String },
CommandManager.functionalButtonCmds.map { $0 as String },
CommandManager.motionControlButtonCmds.map { $0 as String },
CommandManager.touchPadCmds.map { $0 as String }
]
.lazy
.flatMap { $0 } // 三维展开
.map(String.init(describing:)) // 安全类型转换
let keys = combinedStrings.joined(separator: "|")
let pattern = "^(?:\(keys))(?:-(?:\(keys)))*(?:-\\d+MS)?$"
guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else {
print("Failed to create regex")
return nil
}
let range = NSRange(location: 0, length: input.utf16.count)
guard let match = regex.firstMatch(in: input, options: [], range: range) else {
print("No match found for input: \(input)")
return nil
}
// print("Regex matched for input: \(input)")
let matchedString = (input as NSString).substring(with: match.range(at: 0))
let cmdStrings = matchedString.split(separator: "-").map { String($0) }
guard !cmdStrings.isEmpty else {
print("No key strings found in the matched string")
return nil
}
for key in cmdStrings {
validCmdStrings.append(key)
}
if validCmdStrings.isEmpty {
print("No valid key strings found in the matched string")
return nil
}
}
for (index, key) in validCmdStrings.enumerated() {
print("Valid Key \(index): \(key)")
}
return validCmdStrings
}
@objc public func addCommand(_ command: RemoteCommand) -> Bool {
command.cmdString = command.cmdString.uppercased() // convert all letters to upper case
if(command.alias.trimmingCharacters(in: .whitespacesAndNewlines).count == 0) {command.alias = command.cmdString} // copy cmd string as alias when alias is empty
let keyStrings = extractKeyStringsFromComboCommand(from: command.cmdString)
let keyStrings = extractAutoReleaseButtonStrings(from: command.cmdString)
if (keyStrings == nil) {return false} // in case of non-keyboard command strings, return false
commands.append(command)
saveCommands()
@@ -531,6 +662,29 @@ import UIKit
@objc public func getAllCommands() -> [RemoteCommand] {
return commands
}
public func reorderCommands(withIdentifiers identifiers: [String]) {
guard !identifiers.isEmpty else { return }
var remainingCommandsByID = Dictionary(grouping: commands, by: { $0.identifier })
var reorderedCommands: [RemoteCommand] = []
for identifier in identifiers {
guard var commandGroup = remainingCommandsByID[identifier],
!commandGroup.isEmpty else {
continue
}
reorderedCommands.append(commandGroup.removeFirst())
remainingCommandsByID[identifier] = commandGroup
}
for command in commands where !reorderedCommands.contains(where: { $0 === command }) {
reorderedCommands.append(command)
}
commands = reorderedCommands
saveCommands()
}
private func loadCommands() {
if let savedCommandsData = UserDefaults.standard.data(forKey: "savedCommands") {
@@ -540,6 +694,7 @@ import UIKit
// Assign the unarchived commands to your property
print(" Assign the unarchived commands to your property ")
commands = savedCommands
migrateDefaultCommandAliasesIfNeeded()
} else {
// Handle the case where the data could not be unarchived into the expected type
print("Data could not be unarchived into [RemoteCommand]")
@@ -551,38 +706,72 @@ import UIKit
}
}
private func saveCommands() {
if let data = try? NSKeyedArchiver.archivedData(withRootObject: commands, requiringSecureCoding: false) {
UserDefaults.standard.set(data, forKey: "savedCommands")
}
}
private func migrateDefaultCommandAliasesIfNeeded() {
var didMigrate = false
for command in commands {
guard let migratedAlias = Self.defaultCommandAliasMigrationMap[command.alias] else {
continue
}
command.alias = migratedAlias
didMigrate = true
}
if didMigrate {
saveCommands()
}
}
@objc public func sendKeyComboCommand(keyboardCmdStrings: [String], delay: TimeInterval = 0.2, index: Int = 0) { // we need a large delay for WAN streaming
@objc public func sendAutoReleaseComboCommand(cmdStrings: [String]?, delay: TimeInterval = 0.2, index: Int = 0, pressOnly: Bool = false, releaseOnly:Bool = false) { // we need a large delay for WAN streaming
// 如果已处理完所有按键,则开始释放按键
guard index < keyboardCmdStrings.count else {
// 释放按键
for keyStr in keyboardCmdStrings.reversed() { // 从后往前释放按键
guard var cmdStrings = cmdStrings else { return }
cmdStrings = cmdStrings.filter { !CommandManager.shortcutAllowedFunctionalButtonMappings.keys.contains($0)}
if releaseOnly {
for keyStr in cmdStrings.reversed() {
if let keyCode = CommandManager.keyboardButtonMappings[keyStr] {
LiSendKeyboardEvent(keyCode, Int8(KEY_ACTION_UP), 0) // 释放按键
}
if let mouseButtonCode = CommandManager.mouseButtonMappings[keyStr]{
LiSendMouseButtonEvent(CChar(BUTTON_ACTION_RELEASE), mouseButtonCode)
}
}
return
}
guard index < cmdStrings.count else {
// 释放按键
DispatchQueue.main.asyncAfter(deadline: .now()) {
if pressOnly {return}
for keyStr in cmdStrings.reversed() { // 从后往前释放按键
if let keyCode = CommandManager.keyboardButtonMappings[keyStr] {
LiSendKeyboardEvent(keyCode, Int8(KEY_ACTION_UP), 0) // 释放按键
}
if let mouseButtonCode = CommandManager.mouseButtonMappings[keyStr]{
LiSendMouseButtonEvent(CChar(BUTTON_ACTION_RELEASE), mouseButtonCode)
}
}
}
return
}
// 获取当前按键的映射值
if let keyCode = CommandManager.keyboardButtonMappings[keyboardCmdStrings[index]] {
if let keyCode = CommandManager.keyboardButtonMappings[cmdStrings[index]] {
// 发送当前按键的按下事件
LiSendKeyboardEvent(keyCode, Int8(KEY_ACTION_DOWN), 0)
// 延迟后递归处理下一个按键
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
self.sendKeyComboCommand(keyboardCmdStrings: keyboardCmdStrings, delay: delay, index: index + 1)
}
} else if let mouseButtonCode = CommandManager.mouseButtonMappings[cmdStrings[index]]{
LiSendMouseButtonEvent(CChar(BUTTON_ACTION_PRESS), mouseButtonCode)
} else {
print("No mapping found for \(keyboardCmdStrings[index])")
// 如果当前按键没有映射,跳过当前按键并继续下一个
self.sendKeyComboCommand(keyboardCmdStrings: keyboardCmdStrings, delay: delay, index: index + 1)
print("No mapping found for \(cmdStrings[index])")
}
// 延迟后递归处理下一个按键
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
self.sendAutoReleaseComboCommand(cmdStrings: cmdStrings, delay: delay, index: index + 1, pressOnly: pressOnly, releaseOnly: releaseOnly)
}
}
@@ -602,6 +791,4 @@ import UIKit
}
return
}
}
File diff suppressed because it is too large Load Diff
+30
View File
@@ -15,7 +15,10 @@
- (void)gamepadPresenceChanged;
- (void)mousePresenceChanged;
- (void)mouseConnected;
- (void)keyboardConnected;
- (void)streamExitRequested;
- (void)controllerArrivalWithPlayerIndex:(int8_t)index;
@end
@@ -23,6 +26,8 @@
@property (readonly) bool shallDisableGyroHotSwitch;
+(nullable ControllerSupport*) sharedInstance;
-(id) initWithConfig:(StreamConfiguration*)streamConfig delegate:(id<ControllerSupportDelegate>)delegate;
-(void) connectionEstablished;
@@ -47,6 +52,28 @@
-(void) rumbleTriggers:(uint16_t)controllerNumber leftTrigger:(uint16_t)leftTrigger rightTrigger:(uint16_t)rightTrigger;
-(void) setMotionEventState:(uint16_t)controllerNumber motionType:(uint8_t)motionType reportRateHz:(uint16_t)reportRateHz;
-(void) setControllerLed:(uint16_t)controllerNumber r:(uint8_t)r g:(uint8_t)g b:(uint8_t)b;
-(void) setAdaptiveTriggers:(uint16_t)controllerNumber eventFlags:(uint8_t)eventFlags
typeLeft:(uint8_t)typeLeft typeRight:(uint8_t)typeRight
left:(const uint8_t*)left right:(const uint8_t*)right;
-(void) renderDualSenseHaptics:(uint16_t)controllerNumber
leftAmplitude:(float)leftAmplitude
leftSharpness:(float)leftSharpness
leftTransient:(float)leftTransient
rightAmplitude:(float)rightAmplitude
rightSharpness:(float)rightSharpness
rightTransient:(float)rightTransient
delaySeconds:(double)delaySeconds;
-(BOOL) hasDualSenseController:(uint16_t)controllerNumber;
-(void) renderDeviceDualSenseHaptics:(uint16_t)controllerNumber
leftAmplitude:(float)leftAmplitude
leftSharpness:(float)leftSharpness
leftTransient:(float)leftTransient
rightAmplitude:(float)rightAmplitude
rightSharpness:(float)rightSharpness
rightTransient:(float)rightTransient
delaySeconds:(double)delaySeconds;
-(void) cancelScheduledDualSenseHaptics;
-(void) updateTimerStateForOsc;
-(uint16_t) getActiveGamepadMask;
@@ -56,4 +83,7 @@
-(void)updateControllerSupport:(StreamConfiguration*)streamConfig delegate:(id<ControllerSupportDelegate>)delegate;
-(void) reinitiatePrimaryController;
-(void)sendNavigationButtonPress;
@end
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -11,7 +11,8 @@
#import <UIKit/UIKit.h>
#import "OnScreenControls.h"
#import "VoidLink-Swift.h"
@class OnScreenWidgetView;
NS_ASSUME_NONNULL_BEGIN
/**
@@ -22,13 +23,13 @@ NS_ASSUME_NONNULL_BEGIN
@property UIView* _view;
@property NSMutableArray *layoutChanges;
@property CALayer *layerBeingDragged;
@property UIViewController *layoutToolVC;
@property (nullable) CALayer *layerBeingDragged;
@property (nonatomic, weak) UIViewController *layoutToolVC;
- (id) initWithView:(UIView*)view controllerSup:(ControllerSupport*)controllerSupport
streamConfig:(StreamConfiguration*)streamConfig oscLevel:(int)oscLevel;
- (id) initWithView:(UIView*)view controllerSup:(nullable ControllerSupport*)controllerSupport
streamConfig:(nullable StreamConfiguration*)streamConfig oscLevel:(int)oscLevel;
- (CALayer*) controllerLayerFromName:(NSString*)name;
- (nullable CALayer*) controllerLayerFromName:(NSString*)name;
- (BOOL) isLayer:(CALayer*)layer hoveringOverButton:(UIButton*)button;
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
@@ -10,9 +10,8 @@
//
#import "LayoutOnScreenControls.h"
#import "OSCProfilesTableViewController.h"
#import "OnScreenButtonState.h"
#import "OSCProfilesManager.h"
#import "VoidLink-Swift.h"
@interface LayoutOnScreenControls ()
@@ -44,7 +43,7 @@
layoutChanges = [[NSMutableArray alloc] init]; // will contain OSC button layout changes the user has made for this profile
[self drawButtons];
// [self drawButtons];
[self drawGuidelines]; // add the blue guidelines that appear when button is being tapped and dragged
return self;
@@ -55,35 +54,44 @@
/**
* This method overrides the superclass's drawButtons method. The purpose of this method is to create a dPad parent layer, and add the four dPad buttons to it so that the user can drag the entire dPad around, directional buttons and all, as one unit as is the expected behavior. Note that we do not want the four dPad buttons to be child layers of a CALayer parent layer on the game stream view since the touch logic implemented for the four dPad buttons on the game stream view is written assuming the dPad buttons are not children of another parent CALayer
*/
- (void) drawButtons {
[super setDPadCenter]; // Set custom position for D-Pad here
[super setAnalogStickPositions]; // Set custom position for analog sticks here
- (void) drawButtons:(OSCProfile* )profile {
// [super regenLayerPool];
[super setDPadCenter:profile]; // Set custom position for D-Pad here
[super setAnalogStickPositions:profile]; // Set custom position for analog sticks here
[super drawBumpers];
[super drawButtons];
[super drawButtons:profile];
/*
UIImage* downButtonImage = [UIImage imageNamed:@"DownButton"];
UIImage* rightButtonImage = [UIImage imageNamed:@"RightButton"];
UIImage* upButtonImage = [UIImage imageNamed:@"UpButton"];
UIImage* leftButtonImage = [UIImage imageNamed:@"LeftButton"];
*/
UIImage* downButtonImage = [UIImage imageNamed:@"DownButton"];
UIImage* rightButtonImage = [UIImage imageNamed:@"RightButton"];
UIImage* upButtonImage = [UIImage imageNamed:@"UpButton"];
UIImage* leftButtonImage = [UIImage imageNamed:@"LeftButton"];
// create dPad background layer
self._dPadBackground = [CALayer layer];
self._dPadBackground.name = @"dPad";
self._dPadBackground.frame = CGRectMake(self.D_PAD_CENTER_X,
self.D_PAD_CENTER_Y,
self._leftButton.frame.size.width * 2 + BUTTON_DIST,
self._leftButton.frame.size.width * 2 + BUTTON_DIST);
self._dPadBackground.position = CGPointMake(self.D_PAD_CENTER_X, self.D_PAD_CENTER_Y); // since dPadBackground's dimensions have change after settings its width and height you need to reset its position again here
[self.OSCButtonLayers addObject:self._dPadBackground];
[_view.layer addSublayer:self._dPadBackground];
// add dPad buttons to parent layer
[self._dPadBackground addSublayer:self._downButton];
[self._dPadBackground addSublayer:self._rightButton];
[self._dPadBackground addSublayer:self._upButton];
[self._dPadBackground addSublayer:self._leftButton];
if([self.OSCButtonLayerPool containsObject:self._upButton]){
[self.OSCButtonLayerPool addObject:self._dPadBackground];
self._dPadBackground.name = @"dPad";
self._dPadBackground.frame = CGRectMake(self.D_PAD_CENTER_X,
self.D_PAD_CENTER_Y,
self._leftButton.frame.size.width * 2 + BUTTON_DIST,
self._leftButton.frame.size.width * 2 + BUTTON_DIST);
self._dPadBackground.position = CGPointMake(self.D_PAD_CENTER_X, self.D_PAD_CENTER_Y); // since dPadBackground's dimensions have change after settings its width and height you need to reset its position again here
[self.OSCButtonLayerPool addObject:self._dPadBackground];
[_view.layer addSublayer:self._dPadBackground];
// add dPad buttons to parent layer
[self._dPadBackground addSublayer:self._downButton];
[self._dPadBackground addSublayer:self._rightButton];
[self._dPadBackground addSublayer:self._upButton];
[self._dPadBackground addSublayer:self._leftButton];
}
for(CALayer* buttonLayer in self.OSCButtonLayerPool){
// NSLog(@"layer name %@", buttonLayer.name);
}
[self resizeControllerLayerWith:self._dPadBackground and:_dPadSizeFactor];
/*
@@ -128,46 +136,59 @@
*/
horizontalGuideline.backgroundColor = [UIColor blueColor]; // change horizontal guideline back to blue if it doesn't line up with one of the on screen buttons
for (CALayer *button in self.OSCButtonLayers) { // horizontal guideline position check
OnScreenWidgetView.isHorizontallyAligned = false;
for (CALayer *button in self.OSCButtonLayerPool) { // horizontal guideline position check
if ((layerBeingDragged != button) && !button.isHidden) {
if ((horizontalGuideline.center.y < button.position.y + 1) &&
(horizontalGuideline.center.y > button.position.y - 1)) {
horizontalGuideline.backgroundColor = [UIColor yellowColor];
OnScreenWidgetView.isHorizontallyAligned = true;
OnScreenWidgetView.alignedY = button.position.y;
break;
}
}
}
for(UIView* view in self.layoutToolVC.view.subviews){
if ([view isKindOfClass:[OnScreenWidgetView class]] && view != widget) {
if ((horizontalGuideline.center.y < view.center.y + 1) && (horizontalGuideline.center.y > view.center.y - 1)) {
OnScreenWidgetView* otherWidget = (OnScreenWidgetView *) view;
if(widget.isHidden) continue;
if(widget.isFolder && widget.bulkMoveEnabled && [widget.sequenceSet containsObject:@(otherWidget.sequence)]) break;
if((horizontalGuideline.center.y < view.center.y + 1) && (horizontalGuideline.center.y > view.center.y - 1)) {
horizontalGuideline.backgroundColor = [UIColor yellowColor];
OnScreenWidgetView.isHorizontallyAligned = true;
OnScreenWidgetView.alignedY = view.center.y;
break;
}
}
}
verticalGuideline.backgroundColor = [UIColor blueColor]; // change vertical guideline back to blue if it doesn't line up with one of the on screen buttons
for (CALayer *button in self.OSCButtonLayers) { // vertical guideline position check
OnScreenWidgetView.isVerticallyAligned = false;
for (CALayer *button in self.OSCButtonLayerPool) { // vertical guideline position check
if ((layerBeingDragged != button) && !button.isHidden) {
if ((verticalGuideline.center.x < button.position.x + 1) &&
(verticalGuideline.center.x > button.position.x - 1)) {
verticalGuideline.backgroundColor = [UIColor yellowColor];
OnScreenWidgetView.isVerticallyAligned = true;
OnScreenWidgetView.alignedX = button.position.x;
break;
}
}
}
for(UIView* view in self.layoutToolVC.view.subviews){
if ([view isKindOfClass:[OnScreenWidgetView class]] && view != widget) {
if(view.isHidden) continue;
if ((verticalGuideline.center.x < view.center.x + 1) && (verticalGuideline.center.x > view.center.x - 1)) {
verticalGuideline.backgroundColor = [UIColor yellowColor];
OnScreenWidgetView.isVerticallyAligned = true;
OnScreenWidgetView.alignedX = view.center.x;
break;
}
}
}
}
- (void) updateGuidelinesForLegacyOscButton{
- (void) updateGuidelinesForLegacyOnScreenControls{
/* have guidelines follow wherever the user is touching on the screen */
horizontalGuideline.center = layerBeingDragged.position;
verticalGuideline.center = layerBeingDragged.position;
@@ -177,7 +198,7 @@
-Change vertical guideline color to yellow if its x-position is almost equal to that of one of the buttons on screen.
*/
horizontalGuideline.backgroundColor = [UIColor blueColor]; // change horizontal guideline back to blue if it doesn't line up with one of the on screen buttons
for (CALayer *button in self.OSCButtonLayers) { // horizontal guideline position check
for (CALayer *button in self.OSCButtonLayerPool) { // horizontal guideline position check
if ((layerBeingDragged != button) && !button.isHidden) {
if ((horizontalGuideline.center.y < button.position.y + 1) &&
(horizontalGuideline.center.y > button.position.y - 1)) {
@@ -196,7 +217,7 @@
}
}
verticalGuideline.backgroundColor = [UIColor blueColor]; // change vertical guideline back to blue if it doesn't line up with one of the on screen buttons
for (CALayer *button in self.OSCButtonLayers) { // vertical guideline position check
for (CALayer *button in self.OSCButtonLayerPool) { // vertical guideline position check
if ((layerBeingDragged != button) && !button.isHidden) {
if ((verticalGuideline.center.x < button.position.x + 1) &&
(verticalGuideline.center.x > button.position.x - 1)) {
@@ -233,7 +254,7 @@
/* returns reference to button layer object given the button's name*/
- (CALayer*)controllerLayerFromName: (NSString*)name {
for (CALayer *buttonLayer in self.OSCButtonLayers) {
for (CALayer *buttonLayer in self.OSCButtonLayerPool) {
if ([buttonLayer.name isEqualToString:name]) {
return buttonLayer;
@@ -297,7 +318,7 @@
[[NSNotificationCenter defaultCenter] postNotificationName:@"LegacyOscCALayerSelectedNotification" object:layerBeingDragged]; // inform that a CALayer of onScreen controller has being selected
/* save the name, position, and visibility of button being touched in array in case user wants to undo the change later */
OnScreenButtonState *onScreenButtonState = [[OnScreenButtonState alloc] initWithButtonName:layerBeingDragged.name buttonType:LegacyOscButton andPosition:layerBeingDragged.position];
OnScreenButtonState *onScreenButtonState = [[OnScreenButtonState alloc] initWithButtonName:layerBeingDragged.name buttonType:LegacyOnScreenControls andPosition:layerBeingDragged.position];
// add hidden attr here
onScreenButtonState.isHidden = layerBeingDragged.isHidden;
@@ -307,7 +328,7 @@
/* make guide lines visible and position them over the button the user is touching */
horizontalGuideline.hidden = NO;
verticalGuideline.hidden = NO;
[self updateGuidelinesForLegacyOscButton];
[self updateGuidelinesForLegacyOnScreenControls];
}
}
@@ -317,7 +338,7 @@
[CATransaction begin];
[CATransaction setDisableActions:YES];
layerBeingDragged.position = touchLocation; // move object to touch location
[self updateGuidelinesForLegacyOscButton];
[self updateGuidelinesForLegacyOnScreenControls];
[CATransaction commit];
}
+64 -3
View File
@@ -14,17 +14,78 @@
NS_ASSUME_NONNULL_BEGIN
#define DEFAULT_TEMPLATE_NAME1 @"控件模板 / Widget templates"
#define DEFAULT_TEMPLATE_NAME2 @"旧版屏显控件 / Legacy on-screen controls"
#define DEFAULT_TEMPLATE_NAME2 @"旧版控件 / Legacy widgets"
typedef NS_ENUM(NSInteger, MapGyroTo) {
mapGyroToMouse,
mapGyroToControllerStick,
driftCorrection
};
typedef NS_ENUM(NSInteger, PencilAndHoverMode) {
hoverDisabled,
pencilOnly,
pencilToMouse,
pencilToTouch
};
/**
This object contains information pertaining to any of the user created, custom on screen controller layout configurations, or 'profiles.' The object contains a 'name' property for easy reference, as well as an 'isSelected' property which is used to determine whether this particular custom OSC layout should show on screen during game stream view. Only one 'OSCProfile' is set to 'isSelected' at any given time. The object also contains an array of 'OnScreenButtonStates' which provides information that allows us to move and hide/unhide each of the 19 on screen buttons. Note that the 'buttonStates' property should contain an NSMutableArray of ENCODED 'OnScreenButtonState' objects. This allows us to save the 'OSCProfile' object to NSUserDefaults.
Additionally the 'OSCProfile' object adopts encoding and decoding protocols so that we can encode the object before saving it to NSUserDefaults. By saving this object to NSUserDefaults we allow the user to save and load their custom on screen controller button layouts between app launches
*/
@interface OSCProfile : NSObject <NSCoding, NSSecureCoding>
@interface OSCProfile : NSObject <NSCoding, NSSecureCoding, NSMutableCopying>
@property NSString *name;
@property NSMutableArray <OnScreenButtonState *> *buttonStates;
@property NSMutableArray <NSData *> *buttonStatesEncoded;
@property (nonatomic, assign) int16_t unfoldedExclusiveFolderSequence;
@property NSSet<NSNumber *> *postExclusiveUnfoldedSequences;
@property BOOL isSelected;
@property (nonatomic, assign) bool useBuiltinGyro;
@property (nonatomic, assign) bool swapYawAndRoll;
@property (nonatomic, assign) MapGyroTo mapGyroTo;
@property (nonatomic, assign) bool yawPitchToRightStick;
@property (nonatomic, assign) bool rollToLeftStick;
@property (nonatomic, assign) bool synthesizePhysicalStick;
@property (nonatomic, assign) CGFloat gyroSensitivityYaw;
@property (nonatomic, assign) CGFloat gyroSensitivityPitch;
@property (nonatomic, assign) CGFloat gyroSensitivityRoll;
@property (nonatomic, assign) CGFloat accelSensitivityX;
@property (nonatomic, assign) CGFloat accelSensitivityY;
@property (nonatomic, assign) CGFloat accelSensitivityZ;
@property (nonatomic, assign) double gyroToStickMinOffset;
@property (nonatomic, assign) double physicalLeftStickMinOffset;
@property (nonatomic, assign) double physicalRightStickMinOffset;
@property (nonatomic, assign) int controllerGyroSwitchMode;
@property (nonatomic, assign) bool reverseGyroHoldButton;
@property (nonatomic, assign) int controllerGyroSwitchHold;
@property (nonatomic, assign) int controllerGyroSwitchToggle;
@property (nonatomic, assign) int touchMode;
@property (nonatomic, assign) CGFloat pointerVelocityModeDivider;
@property (nonatomic, assign) CGFloat touchPointerVelocityFactor;
@property (nonatomic, assign) CGPoint normalizedStreamViewOffset;
@property (nonatomic, assign) CGFloat streamViewScale;
@property (nonatomic, assign) CGFloat dualSenseTransient;
@property (nonatomic, assign) bool gamepadOverlayEnabled;
@property NSArray<NSNumber *> *pressureCurvePoints;
// @property NSArray<NSNumber *> *initialTouchPressureCurvePoints;
@property (nonatomic, assign) int phase1StrokeSampleIndexEnd;
@property (nonatomic, assign) int phase2StrokeSampleIndexEnd;
@property (nonatomic, assign) CGFloat strokeEqualizationStrength;
@property (nonatomic, assign) bool pressureCurveEnabled;
@property (nonatomic, assign) bool doubleTapShorcutEnabled;
@property NSString *brushShortcut;
@property NSString *eraserShortcut;
@property (nonatomic, assign) bool squeezeShorcutEnabled;
@property NSString *squeezeStartShortcut;
@property NSString *squeezeEndShortcut;
@property (nonatomic, assign) bool pencilPausesNativeTouch;
@property (nonatomic, assign) bool disablePencilSlideGestures;
@property (nonatomic, assign) PencilAndHoverMode pencilAndHoverMode;
- (id) initWithName:(NSString*)name buttonStates:(NSMutableArray*)buttonStates isSelected:(BOOL)isSelected;
+220 -4
View File
@@ -10,13 +10,14 @@
//
#import "OSCProfile.h"
#import "VoidLink-Swift.h"
@implementation OSCProfile
- (id) initWithName:(NSString*)name buttonStates:(NSMutableArray*)buttonStates isSelected:(BOOL)isSelected {
if ((self = [self init])) {
self.name = name;
self.buttonStates = buttonStates;
self.buttonStatesEncoded = buttonStates;
self.isSelected = isSelected;
}
@@ -29,18 +30,233 @@
- (void) encodeWithCoder:(NSCoder*)encoder {
[encoder encodeObject:self.name forKey:@"name"];
[encoder encodeObject:self.buttonStates forKey:@"buttonStates"];
[encoder encodeObject:self.buttonStatesEncoded forKey:@"buttonStates"];
[encoder encodeInt32:self.unfoldedExclusiveFolderSequence forKey:@"unfoldedExclusiveFolderSequence"];
[encoder encodeObject:self.postExclusiveUnfoldedSequences forKey:@"postExclusiveUnfoldedSequences"];
[encoder encodeBool:self.isSelected forKey:@"isSelected"];
[encoder encodeBool:self.useBuiltinGyro forKey:@"useBuiltinGyro"];
[encoder encodeBool:self.swapYawAndRoll forKey:@"swapYawAndRoll"];
[encoder encodeInt64:self.mapGyroTo forKey:@"mapGyroTo"];
[encoder encodeBool:self.yawPitchToRightStick forKey:@"yawPitchToRightStick"];
[encoder encodeBool:self.rollToLeftStick forKey:@"rollToLeftStick"];
[encoder encodeBool:self.synthesizePhysicalStick forKey:@"synthesizePhysicalStick"];
[encoder encodeFloat:self.gyroSensitivityYaw forKey:@"gyroSensitivityYaw"];
[encoder encodeFloat:self.gyroSensitivityPitch forKey:@"gyroSensitivityPitch"];
[encoder encodeFloat:self.gyroSensitivityRoll forKey:@"gyroSensitivityRoll"];
[encoder encodeFloat:self.accelSensitivityX forKey:@"accelSensitivityX"];
[encoder encodeFloat:self.accelSensitivityY forKey:@"accelSensitivityY"];
[encoder encodeFloat:self.accelSensitivityZ forKey:@"accelSensitivityZ"];
[encoder encodeDouble:self.gyroToStickMinOffset forKey:@"gyroToStickMinOffset"];
[encoder encodeDouble:self.physicalLeftStickMinOffset forKey:@"physicalLeftStickMinOffset"];
[encoder encodeDouble:self.physicalRightStickMinOffset forKey:@"physicalRightStickMinOffset"];
[encoder encodeInt:self.controllerGyroSwitchMode forKey:@"controllerGyroSwitchMode"];
[encoder encodeBool:self.reverseGyroHoldButton forKey:@"reverseGyroHoldButton"];
[encoder encodeInt:self.controllerGyroSwitchHold forKey:@"controllerGyroSwitchHold"];
[encoder encodeInt:self.controllerGyroSwitchToggle forKey:@"controllerGyroSwitchToggle"];
[encoder encodeFloat:self.pointerVelocityModeDivider forKey:@"pointerVelocityModeDivider"];
[encoder encodeInt:self.touchMode forKey:@"touchMode"];
[encoder encodeFloat:self.touchPointerVelocityFactor forKey:@"touchPointerVelocityFactor"];
[encoder encodeCGPoint:self.normalizedStreamViewOffset forKey:@"normalizedStreamViewOffset"];
[encoder encodeFloat:self.streamViewScale forKey:@"streamViewScale"];
[encoder encodeFloat:self.dualSenseTransient forKey:@"dualSenseTransient"];
[encoder encodeBool:self.gamepadOverlayEnabled forKey:@"gamepadOverlayEnabled"];
[encoder encodeObject:self.pressureCurvePoints forKey:@"pressureCurvePoints"];
// [encoder encodeObject:self.initialTouchPressureCurvePoints forKey:@"initialTouchPressureCurvePoints"];
[encoder encodeInt:self.phase1StrokeSampleIndexEnd forKey:@"phase1StrokeSampleIndexEnd"];
[encoder encodeInt:self.phase2StrokeSampleIndexEnd forKey:@"phase2StrokeSampleIndexEnd"];
[encoder encodeDouble:self.strokeEqualizationStrength forKey:@"strokeEqualizationStrength"];
[encoder encodeBool:self.pressureCurveEnabled forKey:@"pressureCurveEnabled"];
[encoder encodeBool:self.doubleTapShorcutEnabled forKey:@"doubleTapShorcutEnabled"];
[encoder encodeObject:self.brushShortcut forKey:@"brushShortcut"];
[encoder encodeObject:self.eraserShortcut forKey:@"eraserShortcut"];
[encoder encodeBool:self.squeezeShorcutEnabled forKey:@"squeezeShorcutEnabled"];
[encoder encodeObject:self.squeezeStartShortcut forKey:@"squeezeStartShortcut"];
[encoder encodeObject:self.squeezeEndShortcut forKey:@"squeezeEndShortcut"];
[encoder encodeBool:self.pencilPausesNativeTouch forKey:@"pencilPausesNativeTouch"];
[encoder encodeBool:self.disablePencilSlideGestures forKey:@"disablePencilSlideGestures"];
[encoder encodeInt64:self.pencilAndHoverMode forKey:@"pencilHoverMode"];
}
- (id) initWithCoder:(NSCoder*)decoder {
if (self = [super init]) {
self.name = [decoder decodeObjectForKey:@"name"];
self.buttonStates = [decoder decodeObjectForKey:@"buttonStates"];
self.name =
[decoder decodeObjectOfClasses:
[NSSet setWithObjects:
[NSString class],
nil]
forKey:@"name"];
self.buttonStatesEncoded =
[decoder decodeObjectOfClasses:
[NSSet setWithObjects:
[NSMutableArray class],
[NSArray class],
[NSData class],
nil]
forKey:@"buttonStates"];
self.unfoldedExclusiveFolderSequence = [decoder containsValueForKey:@"unfoldedExclusiveFolderSequence"] ? [decoder decodeInt32ForKey:@"unfoldedExclusiveFolderSequence"] : -1;
self.postExclusiveUnfoldedSequences = [decoder containsValueForKey:@"postExclusiveUnfoldedSequences"] ?
[decoder decodeObjectOfClasses:
[NSSet setWithObjects:
[NSMutableSet class],
[NSSet class],
[NSNumber class],
nil]
forKey:@"postExclusiveUnfoldedSequences"] :
[NSSet set];
self.isSelected = [decoder decodeBoolForKey:@"isSelected"];
self.useBuiltinGyro = [decoder containsValueForKey:@"useBuiltinGyro"] ? [decoder decodeBoolForKey:@"useBuiltinGyro"] : true;
self.swapYawAndRoll = [decoder containsValueForKey:@"swapYawAndRoll"] ? [decoder decodeBoolForKey:@"swapYawAndRoll"] : false;
self.mapGyroTo = [decoder containsValueForKey:@"mapGyroTo"] ? [decoder decodeInt64ForKey:@"mapGyroTo"] : mapGyroToMouse;
self.yawPitchToRightStick = [decoder containsValueForKey:@"yawPitchToRightStick"] ? [decoder decodeBoolForKey:@"yawPitchToRightStick"] : true;
self.rollToLeftStick = [decoder containsValueForKey:@"rollToLeftStick"] ? [decoder decodeBoolForKey:@"rollToLeftStick"] : false;
self.synthesizePhysicalStick = [decoder containsValueForKey:@"synthesizePhysicalStick"] ? [decoder decodeBoolForKey:@"synthesizePhysicalStick"] : true;
self.gyroSensitivityYaw = [decoder containsValueForKey:@"gyroSensitivityYaw"] ? [decoder decodeFloatForKey:@"gyroSensitivityYaw"] : 1.0;
self.gyroSensitivityPitch = [decoder containsValueForKey:@"gyroSensitivityPitch"] ? [decoder decodeFloatForKey:@"gyroSensitivityPitch"] : 1.0;
self.gyroSensitivityRoll = [decoder containsValueForKey:@"gyroSensitivityRoll"] ? [decoder decodeFloatForKey:@"gyroSensitivityRoll"] : 1.0;
self.accelSensitivityX = [decoder containsValueForKey:@"accelSensitivityX"] ? [decoder decodeFloatForKey:@"accelSensitivityX"] : 1.0;
self.accelSensitivityY = [decoder containsValueForKey:@"accelSensitivityY"] ? [decoder decodeFloatForKey:@"accelSensitivityY"] : 1.0;
self.accelSensitivityZ = [decoder containsValueForKey:@"accelSensitivityZ"] ? [decoder decodeFloatForKey:@"accelSensitivityZ"] : 1.0;
self.gyroToStickMinOffset = [decoder containsValueForKey:@"gyroToStickMinOffset"] ? [decoder decodeDoubleForKey:@"gyroToStickMinOffset"] : 0;
self.physicalLeftStickMinOffset = [decoder containsValueForKey:@"physicalLeftStickMinOffset"] ? [decoder decodeDoubleForKey:@"physicalLeftStickMinOffset"] : 0;
self.physicalRightStickMinOffset = [decoder containsValueForKey:@"physicalRightStickMinOffset"] ? [decoder decodeDoubleForKey:@"physicalRightStickMinOffset"] : 0;
self.controllerGyroSwitchMode = [decoder containsValueForKey:@"controllerGyroSwitchMode"] ? [decoder decodeIntForKey:@"controllerGyroSwitchMode"] : ControllerGyroSwitchDisabled;
self.reverseGyroHoldButton = [decoder containsValueForKey:@"reverseGyroHoldButton"] ? [decoder decodeBoolForKey:@"reverseGyroHoldButton"] : false;
self.controllerGyroSwitchHold = [decoder containsValueForKey:@"controllerGyroSwitchHold"] ? [decoder decodeIntForKey:@"controllerGyroSwitchHold"] : ControllerElementNull;
self.controllerGyroSwitchToggle = [decoder containsValueForKey:@"controllerGyroSwitchToggle"] ? [decoder decodeIntForKey:@"controllerGyroSwitchToggle"] : ControllerElementNull;
self.touchMode = [decoder containsValueForKey:@"touchMode"] ? [decoder decodeIntForKey:@"touchMode"] : NativeTouch;
if(self.touchMode == NativeTouchOnly) self.touchMode = NativeTouch;
self.pointerVelocityModeDivider = [decoder containsValueForKey:@"pointerVelocityModeDivider"] ? [decoder decodeFloatForKey:@"pointerVelocityModeDivider"] : 0.5;
self.touchPointerVelocityFactor = [decoder containsValueForKey:@"touchPointerVelocityFactor"] ? [decoder decodeFloatForKey:@"touchPointerVelocityFactor"] : 1.0;
self.normalizedStreamViewOffset = [decoder containsValueForKey:@"normalizedStreamViewOffset"] ? [decoder decodeCGPointForKey:@"normalizedStreamViewOffset"] : CGPointZero;
self.streamViewScale = [decoder containsValueForKey:@"streamViewScale"] ? [decoder decodeFloatForKey:@"streamViewScale"] : 1.0;
self.dualSenseTransient = [decoder containsValueForKey:@"dualSenseTransient"] ? [decoder decodeFloatForKey:@"dualSenseTransient"] : 1.0;
self.gamepadOverlayEnabled = [decoder containsValueForKey:@"gamepadOverlayEnabled"] ? [decoder decodeBoolForKey:@"gamepadOverlayEnabled"] : false;
self.pressureCurvePoints =
[decoder containsValueForKey:@"pressureCurvePoints"]
? [decoder decodeObjectOfClasses:
[NSSet setWithObjects:
[NSArray class],
[NSNumber class],
nil]
forKey:@"pressureCurvePoints"]
: @[@0.0, @0.0, @1.0, @1.0];
/* self.initialTouchPressureCurvePoints =
[decoder containsValueForKey:@"initialTouchPressureCurvePoints"]
? [decoder decodeObjectOfClasses:
[NSSet setWithObjects:
[NSArray class],
[NSNumber class],
nil]
forKey:@"initialTouchPressureCurvePoints"]
: @[@0.0, @0.0, @1.0, @1.0]; */
self.phase1StrokeSampleIndexEnd = [decoder containsValueForKey:@"phase1StrokeSampleIndexEnd"] ? [decoder decodeIntForKey:@"phase1StrokeSampleIndexEnd"] : 10;
self.phase2StrokeSampleIndexEnd = [decoder containsValueForKey:@"phase2StrokeSampleIndexEnd"] ? [decoder decodeIntForKey:@"phase2StrokeSampleIndexEnd"] : 24;
self.strokeEqualizationStrength = [decoder containsValueForKey:@"strokeEqualizationStrength"] ? [decoder decodeFloatForKey:@"strokeEqualizationStrength"] : 1.0;
self.pressureCurveEnabled = [decoder containsValueForKey:@"pressureCurveEnabled"] ? [decoder decodeBoolForKey:@"pressureCurveEnabled"] : false;
self.doubleTapShorcutEnabled = [decoder containsValueForKey:@"doubleTapShorcutEnabled"] ? [decoder decodeBoolForKey:@"doubleTapShorcutEnabled"] : false;
self.brushShortcut =
[decoder containsValueForKey:@"brushShortcut"]
? [decoder decodeObjectOfClasses:
[NSSet setWithObjects:
[NSString class],
nil]
forKey:@"brushShortcut"]
: @"";
self.eraserShortcut =
[decoder containsValueForKey:@"eraserShortcut"]
? [decoder decodeObjectOfClasses:
[NSSet setWithObjects:
[NSString class],
nil]
forKey:@"eraserShortcut"]
: @"";
self.squeezeShorcutEnabled = [decoder containsValueForKey:@"squeezeShorcutEnabled"] ? [decoder decodeBoolForKey:@"squeezeShorcutEnabled"] : false;
self.squeezeStartShortcut =
[decoder containsValueForKey:@"squeezeStartShortcut"]
? [decoder decodeObjectOfClasses:
[NSSet setWithObjects:
[NSString class],
nil]
forKey:@"squeezeStartShortcut"]
: @"";
self.squeezeEndShortcut =
[decoder containsValueForKey:@"squeezeEndShortcut"]
? [decoder decodeObjectOfClasses:
[NSSet setWithObjects:
[NSString class],
nil]
forKey:@"squeezeEndShortcut"]
: @"";
self.pencilPausesNativeTouch = [decoder containsValueForKey:@"pencilPausesNativeTouch"] ? [decoder decodeBoolForKey:@"pencilPausesNativeTouch"] : false;
self.disablePencilSlideGestures = [decoder containsValueForKey:@"disablePencilSlideGestures"] ? [decoder decodeBoolForKey:@"disablePencilSlideGestures"] : false;
self.pencilAndHoverMode = [decoder containsValueForKey:@"pencilHoverMode"] ? [decoder decodeInt64ForKey:@"pencilHoverMode"] : pencilOnly;
}
return self;
}
- (nonnull id)mutableCopyWithZone:(nullable NSZone *)zone {
OSCProfile *copy = [[[self class] allocWithZone:zone] init];
copy.name = [self.name mutableCopy]; // NSString → NSMutableString
copy.buttonStatesEncoded = [[NSMutableArray alloc] initWithArray:self.buttonStatesEncoded copyItems:YES];
copy.unfoldedExclusiveFolderSequence = self.unfoldedExclusiveFolderSequence;
copy.postExclusiveUnfoldedSequences = [self.postExclusiveUnfoldedSequences copy];
copy.isSelected = self.isSelected;
copy.useBuiltinGyro = self.useBuiltinGyro;
copy.swapYawAndRoll = self.swapYawAndRoll;
copy.mapGyroTo = self.mapGyroTo;
copy.yawPitchToRightStick = self.yawPitchToRightStick;
copy.rollToLeftStick = self.rollToLeftStick;
copy.synthesizePhysicalStick = self.synthesizePhysicalStick;
copy.gyroSensitivityYaw = self.gyroSensitivityYaw;
copy.gyroSensitivityPitch = self.gyroSensitivityPitch;
copy.gyroSensitivityRoll = self.gyroSensitivityRoll;
copy.accelSensitivityX = self.accelSensitivityX;
copy.accelSensitivityY = self.accelSensitivityY;
copy.accelSensitivityZ = self.accelSensitivityZ;
copy.gyroToStickMinOffset = self.gyroToStickMinOffset;
copy.physicalLeftStickMinOffset = self.physicalLeftStickMinOffset;
copy.physicalRightStickMinOffset = self.physicalRightStickMinOffset;
copy.controllerGyroSwitchMode = self.controllerGyroSwitchMode;
copy.reverseGyroHoldButton = self.reverseGyroHoldButton;
copy.controllerGyroSwitchHold = self.controllerGyroSwitchHold;
copy.controllerGyroSwitchToggle = self.controllerGyroSwitchToggle;
copy.touchMode = self.touchMode;
copy.pointerVelocityModeDivider = self.pointerVelocityModeDivider;
copy.touchPointerVelocityFactor = self.touchPointerVelocityFactor;
copy.normalizedStreamViewOffset = self.normalizedStreamViewOffset;
copy.streamViewScale = self.streamViewScale;
copy.gamepadOverlayEnabled = self.gamepadOverlayEnabled;
copy.pressureCurvePoints = [[NSMutableArray alloc] initWithArray:self.pressureCurvePoints copyItems:YES];
// copy.initialTouchPressureCurvePoints = [[NSMutableArray alloc] initWithArray:self.initialTouchPressureCurvePoints copyItems:YES];
copy.phase1StrokeSampleIndexEnd = self.phase1StrokeSampleIndexEnd;
copy.phase2StrokeSampleIndexEnd = self.phase2StrokeSampleIndexEnd;
copy.strokeEqualizationStrength = self.strokeEqualizationStrength;
copy.pressureCurveEnabled = self.pressureCurveEnabled;
copy.doubleTapShorcutEnabled = self.doubleTapShorcutEnabled;
copy.brushShortcut = [self.brushShortcut mutableCopy]; // NSString → NSMutableString
copy.eraserShortcut = [self.eraserShortcut mutableCopy]; // NSString → NSMutableString
copy.squeezeShorcutEnabled = self.squeezeShorcutEnabled;
copy.squeezeStartShortcut = [self.squeezeStartShortcut mutableCopy]; // NSString → NSMutableString
copy.squeezeEndShortcut = [self.squeezeEndShortcut mutableCopy]; // NSString → NSMutableString
copy.pencilPausesNativeTouch = self.pencilPausesNativeTouch;
copy.disablePencilSlideGestures = self.disablePencilSlideGestures;
copy.pencilAndHoverMode = self.pencilAndHoverMode;
return copy;
}
@end
@@ -1,77 +0,0 @@
//
// OSCProfilesManager.h
// Moonlight
//
// Created by Long Le on 1/1/23.
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
//
// Modified by True砖家 since 2024.6.24
// Copyright © 2024 True砖家 @ Bilibili. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "OSCProfile.h"
NS_ASSUME_NONNULL_BEGIN
/**
This singleton object can be accessed from any class and provides methods to get and set on screen controller profile related data.
Note that the implementation file contains a number of 'Helper' methods. These helper methods are only used in this class's implementation file and help to reduce re-writing large blocks of code that are called multiple times throughout the file
*/
@interface OSCProfilesManager : NSObject
@property NSMutableArray <OSCProfile *> *currentProfiles;
+ (OSCProfilesManager *)sharedManager:(CGRect)viewBounds;
+ (void)setOnScreenWidgetViewsSet:(NSMutableSet* )set;
+ (void)setLayoutViewBounds:(CGRect)bounds;
#pragma mark - Getters
/**
* Returns an array of decoded profile objects
*/
- (NSMutableArray *) getAllProfiles;
/**
* Returns the OSC Profile that is currently selected to be displayed on screen during game streaming
*/
- (OSCProfile *) getSelectedProfile;
/**
* Returns the index of the 'selected' profile within the array it's in
*/
- (NSInteger) getIndexOfSelectedProfile;
- (NSMutableArray *) getEncodedProfiles;
- (void) importEncodedProfiles:(NSMutableArray* )profilesEncoded;
- (OnScreenButtonState *)unarchiveButtonStateEncoded:(NSData *)data;
- (void) importDefaultTemplates;
#pragma mark - Setters
/**
* Sets the profile object with the particular 'name' as the selected profile to be displayed on screen during game streaming
*/
- (void) setProfileToSelected:(NSString *)name;
/**
* Saves a profile object with a particular 'name' and an array of button layers (the CALayer button layers are the objects currently visible on screen) to persistent storage
*/
- (void) saveProfileWithName:(NSString*)name andButtonLayers:(NSMutableArray *)buttonLayers;
- (bool) updateSelectedProfile:(NSMutableArray *) oscButtonLayers;
/**
* Delete current selected profile.
*/
- (void) deleteCurrentSelectedProfile;
#pragma mark - Queries
/**
* Lets the caller of this method know whether a profile with a given name already exists in persistent storage
*/
- (BOOL) profileNameAlreadyExist:(NSString*)name;
- (OSCProfile *) findProfileByName:(NSString*) name inProfileArray:(NSMutableArray*)profiles;
@end
NS_ASSUME_NONNULL_END
@@ -1,441 +0,0 @@
//
// OSCProfilesManager.m
// Moonlight
//
// Created by Long Le on 1/1/23.
// Copyright © 2023 Moonlight Game Streaming Project. All rights reserved.
//
// Modified by True砖家 since 2024.6.24
// Copyright © 2024 True砖家 @ Bilibili. All rights reserved.
//
#import "OSCProfilesManager.h"
#import "OnScreenButtonState.h"
#import "VoidLink-Swift.h"
#import "LayoutOnScreenControlsViewController.h"
#import "OnScreenControls.h"
@implementation OSCProfilesManager
static NSMutableSet *OnScreenWidgetViews;
static CGRect layoutViewBounds;
#pragma mark - Initializer
+ (OSCProfilesManager *) sharedManager:(CGRect)viewBounds {
static OSCProfilesManager *_sharedManager = nil;
static dispatch_once_t onceToken;
layoutViewBounds = viewBounds;
// NSLog(@"bounds width: %f, height: %f", layoutViewBounds.size.width, layoutViewBounds.size.height);
dispatch_once(&onceToken, ^{
_sharedManager = [[self alloc] init];
});
// _sharedManager.currentProfiles = [_sharedManager getAllProfiles];
return _sharedManager;
}
#pragma mark - Class Helper Methods
+ (void)setOnScreenWidgetViewsSet:(NSMutableSet* )set{
OnScreenWidgetViews = set;
}
+ (void)setLayoutViewBounds:(CGRect)bounds{
layoutViewBounds = bounds;
}
/**
* Returns the profile whose 'name' property has a value that is equal to the 'name' passed into the method
*/
- (OSCProfile *) OSCProfileWithName:(NSString*)name {
NSMutableArray *profiles = [self getAllProfiles];
for (OSCProfile *profile in profiles) {
if ([profile.name isEqualToString:name]) {
return profile;
}
}
return nil;
}
/**
* Returns an array of encoded 'OSCProfile' objects from the array of decoded 'OSCProfile' objects passed into this method
*/
- (NSMutableArray *) encodedProfilesFromArray:(NSMutableArray *)profiles {
NSMutableArray *profilesEncoded = [[NSMutableArray alloc] init];
/* encode each profile and add them back into an array */
for (OSCProfile *profile in profiles) { //
NSData *profileEncoded = [NSKeyedArchiver archivedDataWithRootObject:profile requiringSecureCoding:YES error:nil];
[profilesEncoded addObject:profileEncoded];
}
return profilesEncoded;
}
/**
* Delete an 'OSCProfile' object of specific index for another in the 'OSCProfile' objects array stored in persistent storage
*/
-(void)deleteCurrentSelectedProfile{
NSMutableArray *profiles = [self getAllProfiles];
NSInteger selectedIndex = [self getIndexOfSelectedProfile];
/* Remove the selected profile */
if(selectedIndex != 0) [profiles removeObjectAtIndex:selectedIndex]; //deleting default profile (index 0) is not allowed
else {
NSLog(@"Default profile!"); // to be updated here.
}
selectedIndex--;
if(selectedIndex < 0) selectedIndex = 0;
OSCProfile *newSelectedProfile = [profiles objectAtIndex:selectedIndex];
newSelectedProfile.isSelected = YES;
NSMutableArray *profilesEncoded = [self encodedProfilesFromArray:profiles]; // encode each 'profile' object in the array and add them to a new array
/* Encode the array itself, NOT the objects in the array, which are already encoded. Save array to persistent storage */
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:profilesEncoded requiringSecureCoding:YES error:nil];
[[NSUserDefaults standardUserDefaults] setObject:data forKey:@"OSCProfiles"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
/**
* Replaces one 'OSCProfile' object for another in the 'OSCProfile' objects array stored in persistent storage
*/
- (void) replaceProfile:(OSCProfile*)oldProfile withProfile:(OSCProfile*)newProfile {
NSMutableArray *profiles = [self getAllProfiles];
for (OSCProfile *profile in profiles) { // set all profiles' 'isSelected' property to NO since the new profile we're saving over the old profile with will be the 'selected' profile
profile.isSelected = NO;
}
/* Set the new profile as the selected one. The reasoning behind this is that this method is currently being used when the user saves over an existing profile with another profile that has the same name. The expected behavior is that the newly saved profile becomes the selected profile which will show on screen when they launch the game stream view */
newProfile.isSelected = YES;
/* Remove the old profile from the array and insert the new profile into its place */
int index = 0;
for (int i = 0; i < profiles.count; i++) {
if ([[profiles[i] name] isEqualToString: oldProfile.name]) {
index = i;
}
}
[profiles removeObjectAtIndex:index];
[profiles insertObject:newProfile atIndex:index];
NSMutableArray *profilesEncoded = [self encodedProfilesFromArray:profiles]; // encode each 'profile' object in the array and add them to a new array
/* Encode the array itself, NOT the objects in the array, which are already encoded. Save array to persistent storage */
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:profilesEncoded requiringSecureCoding:YES error:nil];
[[NSUserDefaults standardUserDefaults] setObject:data forKey:@"OSCProfiles"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (NSMutableArray *) getEncodedProfiles {
NSMutableArray *profiles = [self getAllProfiles];
NSMutableArray *profilesEncoded = [self encodedProfilesFromArray:profiles]; // encode each 'profile' object in the array and add them to a new array
return profilesEncoded;
}
- (void) importEncodedProfiles:(NSMutableArray* )profilesEncoded {
NSMutableArray* targetProfiles = [_currentProfiles mutableCopy]; //_currentProfiles is availabled as long as getAllProfiles was called before calling this method (_currentProfiles avoids frequent accessing persisted data)
OSCProfile* profile;
profile = [self findProfileByName:DEFAULT_TEMPLATE_NAME1 inProfileArray:targetProfiles];
if(profile && targetProfiles.count > 1) [targetProfiles removeObject:profile];
profile = [self findProfileByName:DEFAULT_TEMPLATE_NAME2 inProfileArray:targetProfiles];
if(profile && targetProfiles.count > 1) [targetProfiles removeObject:profile];
if(targetProfiles.count > 0) [targetProfiles removeObjectAtIndex:0];
NSMutableArray* localEncodedPofiles = [self encodedProfilesFromArray:targetProfiles];
[profilesEncoded addObjectsFromArray:localEncodedPofiles];
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:profilesEncoded requiringSecureCoding:YES error:nil];
[[NSUserDefaults standardUserDefaults] setObject:data forKey:@"OSCProfiles"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (BOOL)isIPhone{
return ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone);
}
- (void) importDefaultTemplates{
NSString *filePath = [[NSBundle mainBundle] pathForResource: [self isIPhone] ? @"widgetTemplatesIPhone": @"widgetTemplates" ofType:@"bin"];
if (filePath) {
// 2. 读取二进制数据
NSError *error;
NSData *fileData = [NSData dataWithContentsOfFile:filePath options:NSDataReadingMappedIfSafe error:&error];
NSMutableArray *profilesEncoded;
if (fileData && !error) {
// 3. 成功读取
NSSet *classes = [NSSet setWithObjects: [NSMutableData class], [NSMutableArray class], nil];
profilesEncoded = [NSKeyedUnarchiver unarchivedObjectOfClasses:classes fromData:fileData error:&error];
[self importEncodedProfiles:profilesEncoded];
}
}
}
#pragma mark - Globally Accessible Methods
#pragma mark - Getters
- (NSMutableArray *) getAllProfiles {
NSData *profilesArrayEncoded = [[NSUserDefaults standardUserDefaults] objectForKey: @"OSCProfiles"]; // Get the encoded array of encoded OSC profiles from persistent storage
NSSet *classes = [NSSet setWithObjects:[NSString class], [NSMutableData class], [NSMutableArray class], [OSCProfile class], [OnScreenButtonState class], nil];
NSMutableArray *profilesEncoded = [NSKeyedUnarchiver unarchivedObjectOfClasses:classes fromData:profilesArrayEncoded error:nil]; // Decode the encoded array itself, NOT the objects contained in the array
/* Decode each of the encoded profiles, place them into an array, then return the array */
NSMutableArray *profilesDecoded = [[NSMutableArray alloc] init];
OSCProfile *profileDecoded;
for (NSData *profileEncoded in profilesEncoded) {
profileDecoded = [NSKeyedUnarchiver unarchivedObjectOfClasses: classes fromData:profileEncoded error: nil];
[profilesDecoded addObject: profileDecoded];
}
_currentProfiles = profilesDecoded;
return profilesDecoded;
}
- (OSCProfile *) getSelectedProfile {
NSMutableArray *profiles = [self getAllProfiles];
for (OSCProfile *profile in profiles) {
if (profile.isSelected) {
return profile;
}
}
return nil;
}
- (NSInteger) getIndexOfSelectedProfile {
NSMutableArray *profiles = [self getAllProfiles];
for (OSCProfile *profile in profiles) {
if (profile.isSelected == YES) {
return [profiles indexOfObject:profile];
}
}
return 0; // if none of the profiles in the array have their 'isSelected' property set to YES (which should not be possible) return the 'Default' profile as the 'selected' profile
}
#pragma mark - Setters
- (void) setProfileToSelected:(NSString *)name {
NSMutableArray *profiles = [self getAllProfiles];
/* Iterate through each profile. If its name equals the value of the 'name' parameter passed into this method then set the profile's 'isSelected' property to YES, otherwise set the value to NO */
for (OSCProfile *profile in profiles) {
if ([profile.name isEqualToString:name]) {
profile.isSelected = YES;
}
else {
profile.isSelected = NO;
}
}
NSMutableArray *profilesEncoded = [self encodedProfilesFromArray:profiles]; // encode each 'profile' object in the array and add them to a new array
/* Encode the array itself, NOT the objects inside the array, which have already been encoded by this point */
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:profilesEncoded requiringSecureCoding:YES error:nil];
[[NSUserDefaults standardUserDefaults] setObject:data forKey:@"OSCProfiles"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (bool) updateSelectedProfile:(NSMutableArray *) oscButtonLayers {
NSMutableArray* buttonStatesEncoded = [self convertOnScreenControllerAndWidgetsToButtonStates:oscButtonLayers];
if([self getIndexOfSelectedProfile]==0) return false;
OSCProfile *newProfile = [[OSCProfile alloc] initWithName:[self getSelectedProfile].name
buttonStates:buttonStatesEncoded isSelected:YES]; // create a new 'OSCProfile'. Set the array of encoded button states created above to the 'buttonStates' property of the new profile, along with a 'name'. Set 'isSelected' argument to YES which will set this saved profile as the one that will show up in the game stream view
/* set all saved OSCProfiles 'isSelected' property to NO since the new profile you're adding will be set as the selected profile */
NSMutableArray *profiles = [self getAllProfiles];
for (OSCProfile *profile in profiles) {
profile.isSelected = NO;
}
[self replaceProfile:[self getSelectedProfile] withProfile:newProfile];
return true;
}
- (void) saveProfileWithName:(NSString*)name andButtonLayers:(NSMutableArray *)oscButtonLayers {
NSMutableArray* buttonStatesEncoded = [self convertOnScreenControllerAndWidgetsToButtonStates:oscButtonLayers];
OSCProfile *newProfile = [[OSCProfile alloc] initWithName:name
buttonStates:buttonStatesEncoded isSelected:YES]; // create a new 'OSCProfile'. Set the array of encoded button states created above to the 'buttonStates' property of the new profile, along with a 'name'. Set 'isSelected' argument to YES which will set this saved profile as the one that will show up in the game stream view
/* set all saved OSCProfiles 'isSelected' property to NO since the new profile you're adding will be set as the selected profile */
NSMutableArray *profiles = [self getAllProfiles];
for (OSCProfile *profile in profiles) {
profile.isSelected = NO;
}
if ([self profileNameAlreadyExist:name]) { // if a saved profile with the same 'name' already exists in persistent storage then overwrite it and save the change to persistent storage
[self replaceProfile:[self OSCProfileWithName:name] withProfile:newProfile];
}
else { // otherwise encode then add the new profile to the end of the OSCProfiles array
NSData *newProfileEncoded = [NSKeyedArchiver archivedDataWithRootObject:newProfile requiringSecureCoding:YES error:nil];
NSMutableArray *profilesEncoded = [self encodedProfilesFromArray:profiles];
[profilesEncoded addObject:newProfileEncoded];
/* Encode the 'profilesEncoded' array itself, NOT the objects in the 'profilesEncoded' array, all of which are already encoded by this point */
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:profilesEncoded requiringSecureCoding:YES error:nil];
[[NSUserDefaults standardUserDefaults] setObject:data forKey:@"OSCProfiles"];
[[NSUserDefaults standardUserDefaults] synchronize];
// saving test:
/*
NSString *documentsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
NSString *path = [documentsPath stringByAppendingPathComponent:@"newDefault.bin"];
[profiles writeToFile:path atomically:YES];
NSLog(@"默认 Profile 数据已保存到: %@", path);
*/
}
}
- (CGPoint)normalizeWidgetPosition:(CGPoint)position {
CGPoint newPosition = position;
if(position.x > 1.0 && position.y >1.0){
position.x = position.x / layoutViewBounds.size.width;
position.y = position.y / layoutViewBounds.size.height;
}
// asdfsda;
NSLog(@"sef.view bounds: %f, %f", layoutViewBounds.size.width, layoutViewBounds.size.height);
NSLog(@"position: %f, %f, denormalized position: %f, %f", position.x, position.y, newPosition.x, newPosition.y);
return position;
}
- (CGPoint)denormalizeWidgetPosition:(CGPoint)position {
if(position.x < 1.0 && position.y < 1.0){
position.x = position.x * layoutViewBounds.size.width;
position.y = position.y * layoutViewBounds.size.height;
}
return position;
}
- (NSMutableArray* ) convertOnScreenControllerAndWidgetsToButtonStates:(NSMutableArray *) oscButtonLayers {
/* iterate through each OSC button the user sees on screen, create an 'OnScreenButtonState' object from each button, encode each object, and then add each object to an array */
/*
NSSet *validPositionButtonNames = [NSSet setWithObjects:
@"l2Button",
@"l1Button",
@"dPad",
@"selectButton",
@"leftStickBackground",
@"rightStickBackground",
@"r2Button",
@"r1Button",
@"aButton",
@"bButton",
@"xButton",
@"yButton",
@"startButton",
nil]; */
NSMutableArray *buttonStatesEncoded = [[NSMutableArray alloc] init];
// save on-screen game controller buttons & sticks as buttonstate:
for (CALayer *oscButtonLayer in oscButtonLayers) {
CGPoint normalizedPosition = [self normalizeWidgetPosition:oscButtonLayer.position];
OnScreenButtonState *buttonState = [[OnScreenButtonState alloc] initWithButtonName:oscButtonLayer.name buttonType:LegacyOscButton andPosition:normalizedPosition];
// add hidden attr here
buttonState.isHidden = oscButtonLayer.isHidden;
buttonState.oscLayerSizeFactor = [OnScreenControls getControllerLayerSizeFactor:oscButtonLayer];
buttonState.backgroundAlpha = oscButtonLayer.opacity;
NSNumber *style = [OnScreenControls.layerVibrationStyleDic objectForKey:oscButtonLayer.name];
if ([style isKindOfClass:[NSNumber class]]) {
buttonState.vibrationStyle = [style unsignedCharValue];
// 使用 val
}
else buttonState.vibrationStyle = UIImpactFeedbackStyleLight;
// NSLog(@"oscLayerName: %@, opacity: %f, ", oscButtonLayer.name, buttonState.backgroundAlpha);
// buttonState.oscLayerSizeFactor = oscButtonLayer.bounds;
NSData *buttonStateEncoded = [NSKeyedArchiver archivedDataWithRootObject:buttonState requiringSecureCoding:YES error:nil];
[buttonStatesEncoded addObject: buttonStateEncoded];
}
// save on-screen widget views (keyboard & mouse command) as buttonstate:
for(OnScreenWidgetView* widgetView in OnScreenWidgetViews){
CGPoint normalizedPosition = [self normalizeWidgetPosition:widgetView.center];
OnScreenButtonState *buttonState = [[OnScreenButtonState alloc] initWithButtonName:widgetView.cmdString buttonType:CustomOnScreenWidget andPosition:normalizedPosition];
buttonState.alias = widgetView.buttonLabel;
buttonState.widthFactor = [self normalizeSizeWidthFactor:widgetView];
NSLog(@"logging widthFactor %f", buttonState.widthFactor);
buttonState.heightFactor = [self normalizeSizeHeightFactor:widgetView];
buttonState.backgroundAlpha = widgetView.backgroundAlpha;
buttonState.borderWidth = widgetView.borderWidth;
buttonState.vibrationStyle = widgetView.vibrationStyle;
buttonState.mouseButtonAction = widgetView.mouseButtonAction;
buttonState.sensitivityFactorX = widgetView.sensitivityFactorX;
buttonState.sensitivityFactorY = widgetView.sensitivityFactorY;
buttonState.decelerationRate = widgetView.trackballDecelerationRate;
buttonState.stickIndicatorOffset = widgetView.stickIndicatorOffset;
buttonState.widgetShape = widgetView.shape;
buttonState.minStickOffset = widgetView.minStickOffset;
buttonState.slideMode = widgetView.slideMode;
NSData *buttonStateEncoded = [NSKeyedArchiver archivedDataWithRootObject:buttonState requiringSecureCoding:YES error:nil];
[buttonStatesEncoded addObject: buttonStateEncoded];
}
return buttonStatesEncoded;
}
- (CGFloat)normalizeSizeWidthFactor:(OnScreenWidgetView* )widget{
CGFloat screenWidthInPoints = CGRectGetWidth([[UIScreen mainScreen] bounds]);
return widget.bounds.size.width/screenWidthInPoints * 10000;
}
- (CGFloat)normalizeSizeHeightFactor:(OnScreenWidgetView* )widget{
CGFloat screenWidthInPoints = CGRectGetWidth([[UIScreen mainScreen] bounds]);
return widget.bounds.size.height/screenWidthInPoints * 10000;
}
#pragma mark - Queries
- (BOOL) profileNameAlreadyExist:(NSString*)name {
NSMutableArray *profiles = [self getAllProfiles];
/* Iterate through the decoded profiles and return 'YES' if one of the profiles' 'name' properties equals the 'name' passed into this method */
for (OSCProfile *profile in profiles) {
if ([profile.name isEqualToString:name]) {
return YES;
}
}
return NO;
}
- (OSCProfile *) findProfileByName:(NSString*) name inProfileArray:(NSMutableArray*)profiles {
/* Iterate through the decoded profiles and return 'YES' if one of the profiles' 'name' properties equals the 'name' passed into this method */
for (OSCProfile *profile in profiles) {
if ([profile.name isEqualToString:name]) {
return profile;
}
}
return nil;
}
- (OnScreenButtonState *)unarchiveButtonStateEncoded:(NSData *)data {
OnScreenButtonState* buttonState;
buttonState = [NSKeyedUnarchiver unarchivedObjectOfClasses:[NSSet setWithObjects:[NSString class], [OnScreenButtonState class], nil]
fromData:data
error:nil];
return buttonState;
}
@end
@@ -0,0 +1,584 @@
//
// OSCProfilesManager.m -> OSCProfilesManager.swift
// Moonlight
//
// Created by Long Le on 1/1/23.
// Copyright © 2023 Moonlight Game Streaming Project. All rights reserved.
//
// Modified by True砖家 since 2024.6.24
// Copyright © 2024 True砖家 @ Bilibili. All rights reserved.
//
import UIKit
@objcMembers
class OSCProfilesManager: NSObject {
private static let profilesDefaultsKey = "OSCProfiles"
// private static let widgetProfileUpdatedKey = "widgetProfileUpdated-20260606"
private static let widgetProfileUpdatedKey = "widgetProfileUpdated-20260801"
private static var sharedInstance: OSCProfilesManager?
private static var onScreenWidgetViews: NSMutableSet?
private static var layoutViewBounds: CGRect = .zero
var currentProfiles: NSMutableArray = []
var widgetSizeTransition: WidgetSizeTransition = .keepWidgetSize
@objc(sharedManager:)
class func sharedManager(_ viewBounds: CGRect) -> OSCProfilesManager {
if viewBounds != .zero {
layoutViewBounds = viewBounds
}
if let sharedInstance {
return sharedInstance
}
let manager = OSCProfilesManager()
sharedInstance = manager
return manager
}
@objc(setOnScreenWidgetViewsSet:)
class func setOnScreenWidgetViewsSet(_ set: NSMutableSet) {
onScreenWidgetViews = set
}
@objc(setLayoutViewBounds:)
class func setLayoutViewBounds(_ bounds: CGRect) {
layoutViewBounds = bounds
}
private static let profileCodingClasses: [AnyClass] = [
NSString.self,
NSMutableData.self,
NSMutableArray.self,
OSCProfile.self,
OnScreenButtonState.self,
]
private static let buttonStateCodingClasses: [AnyClass] = [
NSString.self,
OnScreenButtonState.self,
NSSet.self,
NSNumber.self,
]
private func oscProfile(withName name: String) -> OSCProfile? {
let profiles = getAllProfiles()
for case let profile as OSCProfile in profiles where profile.name == name {
return profile
}
return nil
}
private func encodedProfiles(from profiles: NSMutableArray) -> NSMutableArray {
let profilesEncoded = NSMutableArray()
for case let profile as OSCProfile in profiles {
if let profileEncoded = try? NSKeyedArchiver.archivedData(withRootObject: profile, requiringSecureCoding: true) {
profilesEncoded.add(profileEncoded)
}
}
return profilesEncoded
}
private func persistProfiles(_ profiles: NSMutableArray) {
let profilesEncoded = encodedProfiles(from: profiles)
persistEncodedProfiles(profilesEncoded)
}
func persistProfileOrder(_ orderedProfiles: NSMutableArray) {
persistProfiles(orderedProfiles)
}
private func persistEncodedProfiles(_ profilesEncoded: NSMutableArray) {
guard let data = try? NSKeyedArchiver.archivedData(withRootObject: profilesEncoded, requiringSecureCoding: true) else {
return
}
let defaults = UserDefaults.standard
defaults.set(data, forKey: Self.profilesDefaultsKey)
defaults.synchronize()
}
func deleteCurrentSelectedProfile() {
let profiles = getAllProfiles()
var selectedIndex = getIndexOfSelectedProfile()
if selectedIndex != 0 {
profiles.removeObject(at: selectedIndex)
} else {
NSLog("Default profile!")
}
selectedIndex -= 1
if selectedIndex < 0 {
selectedIndex = 0
}
if let newSelectedProfile = profiles[safe: selectedIndex] as? OSCProfile {
newSelectedProfile.isSelected = true
}
persistProfiles(profiles)
}
func replaceSelectedProfile(with newProfile: OSCProfile, overwriteDefault: Bool) {
let profiles = getAllProfiles()
var index = 0
for case let profile as OSCProfile in profiles where profile.isSelected {
index = profiles.index(of: profile)
}
if index > 0 || overwriteDefault {
profiles.replaceObject(at: index, with: newProfile)
}
persistProfiles(profiles)
}
@objc(replaceProfile:withProfile:)
func replaceProfile(_ oldProfile: OSCProfile, withProfile newProfile: OSCProfile) {
let profiles = getAllProfiles()
for case let profile as OSCProfile in profiles {
profile.isSelected = false
}
newProfile.isSelected = true
var index = 0
for i in 0 ..< profiles.count {
guard let profile = profiles[i] as? OSCProfile else { continue }
if profile.name == oldProfile.name {
index = i
}
}
profiles.removeObject(at: index)
profiles.insert(newProfile, at: index)
persistProfiles(profiles)
}
func replace(_ oldProfile: OSCProfile, with newProfile: OSCProfile) {
replaceProfile(oldProfile, withProfile: newProfile)
}
func getEncodedProfiles() -> NSMutableArray {
guard
let profilesArrayEncoded = UserDefaults.standard.object(forKey: Self.profilesDefaultsKey) as? Data,
let profilesEncoded = try? NSKeyedUnarchiver.unarchivedObject(ofClasses: Self.profileCodingClasses, from: profilesArrayEncoded) as? NSMutableArray
else {
return NSMutableArray()
}
return profilesEncoded
}
func importEncodedProfiles(_ profilesEncoded: NSMutableArray) {
let localProfiles = currentProfiles.mutableCopy() as? NSMutableArray ?? NSMutableArray()
let profilesDecoded = decodeProfiles(from: profilesEncoded)
let importedIncludesDefault = (profilesDecoded.firstObject as? OSCProfile)?.name == "Default"
let isSingleProfile = profilesEncoded.count == 1
if importedIncludesDefault && localProfiles.count > 0 {
localProfiles.removeObject(at: 0)
} else if !importedIncludesDefault,
let localDefaultProfile = localProfiles.firstObject as? OSCProfile,
localDefaultProfile.name == "Default" {
profilesDecoded.insert(localDefaultProfile, at: 0)
localProfiles.removeObject(at: 0)
}
let localProfileNames = Set(
localProfiles.compactMap { ($0 as? OSCProfile)?.name }
)
for case let profile as OSCProfile in profilesDecoded {
if localProfileNames.contains(profile.name) {
// profile.name = "\(profile.name)-\(LocalizationHelper.localizedString(forKey:"Restored"))"
profile.name = "\(profile.name) - Restored"
}
}
let importedProfileNames = Set(
profilesDecoded.compactMap { ($0 as? OSCProfile)?.name }
)
var localProfilesToRemove: [OSCProfile] = []
for case let profile as OSCProfile in localProfiles {
if importedProfileNames.contains(profile.name) {localProfilesToRemove.append(profile)}
}
for case let profile as Any in localProfilesToRemove {
localProfiles.remove(profile)
}
var indexOffset = importedIncludesDefault ? 0: 0
for profile in localProfiles {
profilesDecoded.insert(profile, at: 1+indexOffset)
indexOffset += 1
}
if isSingleProfile, !importedIncludesDefault {
for profile in profilesDecoded {
if let profile = profile as? OSCProfile {
profile.isSelected = false
}
}
if let profile = profilesDecoded[profilesDecoded.count-1] as? OSCProfile {
profile.isSelected = true
}
}
persistEncodedProfiles(encodedProfiles(from: profilesDecoded))
}
private func isIPhone() -> Bool {
UIDevice.current.userInterfaceIdiom == .phone
}
func importDefaultTemplates(selectedIndex: UInt32? = nil) {
guard let filePath = Bundle.main.path(forResource: isIPhone() ? "widgetTemplatesIPhone" : "widgetTemplates", ofType: "bin") else {
return
}
do {
let fileData = try Data(contentsOf: URL(fileURLWithPath: filePath), options: .mappedIfSafe)
let profilePayloadData = try ProfileFileContainer.unpackedData(from: fileData)
if let profilesEncoded = try NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSMutableData.self, NSMutableArray.self], from: profilePayloadData) as? NSMutableArray {
importEncodedProfiles(profilesEncoded)
if let selectedIndex = selectedIndex {
setProfileToSelected(selectedIndex)
}
}
} catch {
return
}
}
/*
func updateDefaultTemplates() {
guard let filePath = Bundle.main.path(forResource: isIPhone() ? "widgetTemplatesIPhone" : "widgetTemplates", ofType: "bin") else {
return
}
do {
let fileData = try Data(contentsOf: URL(fileURLWithPath: filePath), options: .mappedIfSafe)
guard let defaultProfilesEncoded = try NSKeyedUnarchiver.unarchivedObject(ofClasses: Self.profileCodingClasses, from: fileData) as? NSMutableArray else {
return
}
let targetProfilesDecoded = getAllProfiles()
var newPencilProProfile: OSCProfile?
var pencilProProfileExisted = false
for case let profileEncoded as Data in defaultProfilesEncoded {
guard let defaultProfileDecoded = try NSKeyedUnarchiver.unarchivedObject(ofClasses: Self.profileCodingClasses, from: profileEncoded) as? OSCProfile else {
continue
}
let targetProfile = findProfile(byName: defaultProfileDecoded.name, inProfileArray: targetProfilesDecoded)
if defaultProfileDecoded.name == "Pencil Pro" {
newPencilProProfile = defaultProfileDecoded.mutableCopy() as? OSCProfile
}
if let targetProfile {
if targetProfile.name == "Pencil Pro" {
pencilProProfileExisted = true
}
let targetIndex = targetProfilesDecoded.index(of: targetProfile)
targetProfilesDecoded.replaceObject(at: targetIndex, with: defaultProfileDecoded)
}
}
if let newPencilProProfile, !pencilProProfileExisted {
targetProfilesDecoded.insert(newPencilProProfile, at: 1)
}
persistProfiles(targetProfilesDecoded)
setProfileToSelected(0)
} catch {
return
}
}
*/
func getAllProfiles() -> NSMutableArray {
guard
let profilesArrayEncoded = UserDefaults.standard.object(forKey: Self.profilesDefaultsKey) as? Data,
let profilesEncoded = try? NSKeyedUnarchiver.unarchivedObject(ofClasses: Self.profileCodingClasses, from: profilesArrayEncoded) as? NSMutableArray
else {
currentProfiles = NSMutableArray()
return currentProfiles
}
let profilesDecoded = decodeProfiles(from: profilesEncoded)
currentProfiles = profilesDecoded
return profilesDecoded
}
private func decodeProfiles(from profilesEncoded: NSMutableArray) -> NSMutableArray {
let profilesDecoded = NSMutableArray()
for case let profileEncoded as Data in profilesEncoded {
guard let profileDecoded = try? NSKeyedUnarchiver.unarchivedObject(ofClasses: Self.profileCodingClasses, from: profileEncoded) as? OSCProfile else {
continue
}
profilesDecoded.add(profileDecoded)
}
return profilesDecoded
}
func getSelectedProfile() -> OSCProfile {
var profiles = getAllProfiles()
let defaults = UserDefaults.standard
let needImportDefaultTemplates = defaults.object(forKey: Self.widgetProfileUpdatedKey) == nil
var selectedIndex: UInt32 = 0
for case let profile as OSCProfile in profiles where profile.isSelected {
selectedIndex = UInt32(profiles.index(of: profile))
}
let currentProfileCount = profiles.count
if (currentProfileCount == 0 || needImportDefaultTemplates) && !GenericUtils.pencilProPurchaseProcessedWithImportingWidgetTemplates {
selectedIndex = currentProfileCount == 0 ? 0 : selectedIndex
importDefaultTemplates(selectedIndex: selectedIndex)
defaults.set(true, forKey: Self.widgetProfileUpdatedKey)
defaults.synchronize()
profiles = getAllProfiles()
}
return profiles[Int(selectedIndex)] as! OSCProfile
// return (profiles.firstObject as? OSCProfile) ?? OSCProfile(name: "", buttonStates: NSMutableArray(), isSelected: false)
}
func getIndexOfSelectedProfile() -> Int {
let profiles = getAllProfiles()
for case let profile as OSCProfile in profiles where profile.isSelected {
return profiles.index(of: profile)
}
return 0
}
func getIndexOfLastProfile() -> UInt32 {
let profiles = getAllProfiles()
return profiles.count > 0 ? UInt32(profiles.count - 1) : 0
}
func setProfileToSelected(_ tableIndex: UInt32) {
let profiles = getAllProfiles()
for (idx, element) in profiles.enumerated() {
guard let profile = element as? OSCProfile else { continue }
profile.isSelected = idx == Int(tableIndex)
}
persistProfiles(profiles)
}
func updateSelectedProfile(_ oscButtonLayers: NSMutableSet) -> Bool {
let buttonStatesEncoded = convertOnScreenControllerAndWidgetsToButtonStates(oscButtonLayers)
if getIndexOfSelectedProfile() == 0 {
return false
}
let selectedProfile = getSelectedProfile()
selectedProfile.buttonStatesEncoded = buttonStatesEncoded
selectedProfile.unfoldedExclusiveFolderSequence = OnScreenWidgetView.unfoldedExclusiveFolderSequence
selectedProfile.postExclusiveUnfoldedSequences = Set(OnScreenWidgetView.postExclusiveUnfoldedSequences.map { NSNumber(value: $0) })
replaceSelectedProfile(with: selectedProfile, overwriteDefault: false)
return true
}
func duplicateSelectedProfile(withName name: String) {
if profileNameAlreadyExist(name) {
return
}
let profiles = getAllProfiles()
var newProfile: OSCProfile?
for case let profile as OSCProfile in profiles where profile.isSelected {
profile.isSelected = false
newProfile = profile.mutableCopy() as? OSCProfile
newProfile?.isSelected = true
newProfile?.name = name
}
guard let newProfile, let newProfileEncoded = try? NSKeyedArchiver.archivedData(withRootObject: newProfile, requiringSecureCoding: true) else {
return
}
let profilesEncoded = encodedProfiles(from: profiles)
profilesEncoded.add(newProfileEncoded)
persistEncodedProfiles(profilesEncoded)
}
func normalizeWidgetPosition(_ position: CGPoint) -> CGPoint {
var normalizedPosition = position
// let originalPosition = position
if abs(position.x) > 1.0, abs(position.y) > 1.0 {
normalizedPosition.x = position.x / Self.layoutViewBounds.size.width
normalizedPosition.y = position.y / Self.layoutViewBounds.size.height
}
// NSLog("layoutToolView bounds: %f, %f", Self.layoutViewBounds.size.width, Self.layoutViewBounds.size.height)
// NSLog("position: %f, %f, denormalized position: %f, %f", normalizedPosition.x, normalizedPosition.y, originalPosition.x, originalPosition.y)
return normalizedPosition
}
private func denormalizeWidgetPosition(_ position: CGPoint) -> CGPoint {
var denormalizedPosition = position
if abs(position.x) < 2.01, abs(position.y) < 2.01 {
denormalizedPosition.x = position.x * Self.layoutViewBounds.size.width
denormalizedPosition.y = position.y * Self.layoutViewBounds.size.height
}
return denormalizedPosition
}
private func convertOnScreenControllerAndWidgetsToButtonStates(_ oscButtonLayers: NSMutableSet) -> NSMutableArray {
let buttonStatesEncoded = NSMutableArray()
for case let oscButtonLayer as CALayer in oscButtonLayers {
if oscButtonLayer.isHidden {
continue
}
let normalizedPosition = normalizeWidgetPosition(oscButtonLayer.position)
let layerName = oscButtonLayer.name ?? ""
let buttonState = OnScreenButtonState(buttonName: layerName, buttonType: 0, andPosition: normalizedPosition)
buttonState.isHidden = oscButtonLayer.isHidden
buttonState.oscLayerSizeFactor = OnScreenControls.getControllerLayerSizeFactor(oscButtonLayer)
buttonState.backgroundAlpha = CGFloat(oscButtonLayer.opacity)
if let style = OnScreenControls.layerVibrationStyleDic()?.object(forKey: layerName) as? NSNumber {
buttonState.vibrationStyle = style.uint8Value
} else {
buttonState.vibrationStyle = UInt8(UIImpactFeedbackGenerator.FeedbackStyle.light.rawValue)
}
if let buttonStateEncoded = try? NSKeyedArchiver.archivedData(withRootObject: buttonState, requiringSecureCoding: true) {
buttonStatesEncoded.add(buttonStateEncoded)
}
}
widgetSizeTransition = .keepWidgetSize
for widgetView in OnScreenWidgetView.mapping.values {
let normalizedPosition = normalizeWidgetPosition(widgetView.storedCenter)
let buttonState = OnScreenButtonState(buttonName: widgetView.cmdString, buttonType: 1, andPosition: normalizedPosition)
buttonState.alias = widgetView.widgetLabel
buttonState.sequence = widgetView.sequence
buttonState.sequenceSet = Set(widgetView.sequenceSet.map { NSNumber(value: $0) })
buttonState.parentSequence = widgetView.parentSequence
buttonState.autoDockTimer = Int16(widgetView.autoDockIdleDuration)
buttonState.dockedAlpha = widgetView.autoDockSettledAlpha
buttonState.folded = widgetView.folded
buttonState.revealMode = UInt8(widgetView.revealMode.rawValue)
buttonState.bulkMoveEnabled = widgetView.bulkMoveEnabled
buttonState.widthFactor = normalizeSizeWidthFactor(with: widgetView)
buttonState.heightFactor = normalizeSizeHeightFactor(with: widgetView)
buttonState.backgroundAlpha = widgetView.originalBackgroundAlpha
buttonState.labelAlpha = widgetView.originalLabelAlpha
buttonState.borderAlpha = widgetView.borderAlpha
buttonState.highlightAlpha = widgetView.highlightAlpha
buttonState.borderWidth = widgetView.borderWidth
buttonState.highlightSizeFactor = widgetView.highlightSizeFactor
buttonState.autoTapInterval = UInt16(widgetView.autoTapInterval)
buttonState.autoTapRepeats = widgetView.autoTapRepeats
buttonState.vibrationStyle = UInt8(widgetView.vibrationStyle)
buttonState.mouseButtonAction = UInt8(widgetView.mouseButtonAction.rawValue)
buttonState.animatesTransition = widgetView.animatesTransition
buttonState.sensitivityFactorY = widgetView.sensitivityFactorY
buttonState.slideThreshold = widgetView.slideThreshold
buttonState.yawFactor = widgetView.yawFactor
buttonState.pitchFactor = widgetView.pitchFactor
buttonState.rollFactor = widgetView.rollFactor
buttonState.decelerationRateX = widgetView.decelerationRateX
buttonState.decelerationRateY = widgetView.decelerationRateY
buttonState.widgetShape = widgetView.shape
buttonState.walkModeThreshold = widgetView.dWheelWalkModeThreshold
buttonState.minStickOffset = widgetView.minStickOffset
buttonState.buttonMode = UInt8(widgetView.buttonMode.rawValue)
buttonState.sprintKeyActionType = widgetView.sprintKeyActionType.rawValue
buttonState.sprintKeyThreshold = widgetView.sprintKeyThreshold
buttonState.walkKeyActionType = widgetView.walkKeyActionType.rawValue
buttonState.walkKeyThreshold = widgetView.walkKeyThreshold
buttonState.sensitivityFactorX = widgetView.sensitivityFactorX
buttonState.componentSizeFactor = normalizeComponentSizeFactor(with: widgetView)
buttonState.touchPointAnchored = widgetView.touchPointAnchored
buttonState.stickIndicatorOffset = widgetView.stickIndicatorOffset
if let buttonStateEncoded = try? NSKeyedArchiver.archivedData(withRootObject: buttonState, requiringSecureCoding: true) {
buttonStatesEncoded.add(buttonStateEncoded)
}
}
return buttonStatesEncoded
}
private func getReferenceLen() -> CGFloat {
let screenBounds = UIScreen.main.bounds
let screenWidthInPoints = screenBounds.width
let screenHeightInPoints = screenBounds.height
let longSideLen = max(screenWidthInPoints, screenHeightInPoints)
if widgetSizeTransition == .transitionWithOrientation {
return screenWidthInPoints
}
return longSideLen
}
private func normalizeSizeWidthFactor(with widget: OnScreenWidgetView) -> CGFloat {
widget.bounds.size.width / getReferenceLen() * 10000
}
private func normalizeSizeHeightFactor(with widget: OnScreenWidgetView) -> CGFloat {
widget.bounds.size.height / getReferenceLen() * 10000
}
private func normalizeComponentSizeFactor(with widget: OnScreenWidgetView) -> CGFloat {
if widget.isStickWheel || widget.isDisplacementBasedStickPad {
return widget.denormalizedComponentSizeFactor * widget.baselineDiameter / getReferenceLen() * 10000
}
return 1
}
func profileNameAlreadyExist(_ name: String) -> Bool {
let profiles = getAllProfiles()
for case let profile as OSCProfile in profiles where profile.name == name {
return true
}
return false
}
@objc(findProfileByName:inProfileArray:)
func findProfile(byName name: String, inProfileArray profiles: NSMutableArray) -> OSCProfile? {
for case let profile as OSCProfile in profiles where profile.name == name {
return profile
}
return nil
}
func getIndex(byName name: String) -> UInt32? {
let profiles = getAllProfiles()
for case let profile as OSCProfile in profiles where profile.name == name {
return UInt32(profiles.index(of: profile))
}
return nil
}
func unarchiveButtonStateEncoded(_ data: Data) -> OnScreenButtonState {
if let buttonState = try? NSKeyedUnarchiver.unarchivedObject(ofClasses: Self.buttonStateCodingClasses, from: data) as? OnScreenButtonState {
return buttonState
}
return OnScreenButtonState(buttonName: "", buttonType: 0, andPosition: .zero)
}
}
private extension NSMutableArray {
subscript(safe index: Int) -> Any? {
guard index >= 0, index < count else { return nil }
return self[index]
}
}
+55 -12
View File
@@ -24,46 +24,89 @@ NS_ASSUME_NONNULL_BEGIN
@property NSString *name;
@property NSString *alias;
@property (nonatomic, assign) BOOL folded;
@property (nonatomic, assign) uint8_t revealMode;
@property (nonatomic, assign) BOOL bulkMoveEnabled;
@property (nonatomic, assign) int16_t sequence;
@property (nonatomic, assign) int16_t parentSequence;
@property NSSet *sequenceSet;
@property (nonatomic, assign) int16_t autoDockTimer;
@property (nonatomic, assign) CGFloat dockedAlpha;
@property CGPoint position;
@property (nonatomic, assign) BOOL isHidden;
@property (nonatomic, assign) uint8_t slideMode;
@property (nonatomic, assign) uint8_t buttonType;
@property (nonatomic, assign) uint8_t buttonMode;
@property (nonatomic, assign) uint8_t widgetType;
@property (nonatomic, assign) uint8_t sizeReference;
@property (nonatomic, assign) CGFloat widthFactor; // for OnScreenWidgetView
@property (nonatomic, assign) CGFloat heightFactor; // for OnScreenWidgetView
@property (nonatomic, assign) CGFloat componentSizeFactor; // for OnScreenWidgetView
@property (nonatomic, assign) CGFloat borderWidth; // for OnScreenWidgetView
@property (nonatomic, assign) CGFloat highlightSizeFactor; // for OnScreenWidgetView
@property (nonatomic, assign) CGFloat sensitivityFactorX; // for OnScreenWidgetView
@property (nonatomic, assign) CGFloat sensitivityFactorY; // for OnScreenWidgetView
@property (nonatomic, assign) CGFloat decelerationRate; // for OnScreenWidgetView
@property (nonatomic, assign) CGFloat slideThreshold; // for OnScreenWidgetView
@property (nonatomic, assign) CGFloat yawFactor; // for OnScreenWidgetView
@property (nonatomic, assign) CGFloat pitchFactor; // for OnScreenWidgetView
@property (nonatomic, assign) CGFloat rollFactor; // for OnScreenWidgetView
@property (nonatomic, assign) CGFloat decelerationRateX; // for OnScreenWidgetView
@property (nonatomic, assign) CGFloat decelerationRateY; // for OnScreenWidgetView
@property (nonatomic, assign) bool touchPointAnchored;
@property (nonatomic, assign) CGFloat stickIndicatorOffset; // for OnScreenWidgetView
@property (nonatomic, assign) CGFloat walkModeThreshold; // for OnScreenWidgetView
@property (nonatomic, assign) CGFloat minStickOffset; // for OnScreenWidgetView
@property NSString* widgetShape; // for OnScreenWidgetView
@property (nonatomic, assign) CGFloat oscLayerSizeFactor; // for OnScreenController CALayer
@property (nonatomic, assign) CGFloat backgroundAlpha; // for OnScreenController CALayer
@property (nonatomic, assign) uint8_t vibrationStyle; // for OnScreenController CALayer
@property (nonatomic, assign) uint8_t mouseButtonAction; // for OnScreenController CALayer
@property (nonatomic, assign) CGFloat backgroundAlpha;
@property (nonatomic, assign) CGFloat labelAlpha;
@property (nonatomic, assign) CGFloat borderAlpha;
@property (nonatomic, assign) CGFloat highlightAlpha;
@property (nonatomic, assign) uint8_t vibrationStyle;
@property (nonatomic, assign) uint8_t mouseButtonAction;
@property (nonatomic, assign) bool animatesTransition;
@property (nonatomic, assign) uint16_t autoTapInterval;
@property (nonatomic, assign) uint32_t autoTapRepeats;
@property (nonatomic, assign) uint8_t walkKeyActionType;
@property (nonatomic, assign) CGFloat walkKeyThreshold;
@property (nonatomic, assign) uint8_t sprintKeyActionType;
@property (nonatomic, assign) CGFloat sprintKeyThreshold;
// @property (nonatomic, assign) BOOL hasValidPosition;
typedef NS_ENUM(NSInteger, OnScreenButtonType) {
LegacyOscButton,
typedef NS_ENUM(NSInteger, OnScreenWidgetType) {
LegacyOnScreenControls,
CustomOnScreenWidget
};
typedef NS_ENUM(NSInteger, WidgetSizeReference) {
longSide,
shortSide
};
typedef NS_ENUM(NSInteger, MouseButtonAction) {
hovering,
leftButtonDown,
middleButtonDown,
rightButtonDown,
noClick
};
typedef NS_ENUM(NSInteger, ButtonSlideMode) {
toggle,
typedef NS_ENUM(NSInteger, ButtonMode) {
slideToToggle,
slideAndHold,
disabled
regular,
tapToToggle,
movable
};
typedef NS_ENUM(NSInteger, RevealMode) {
coexist,
exclusive
};
- (id) initWithButtonName:(NSString*)name buttonType:(uint8_t)buttonType andPosition:(CGPoint)position;
+72 -13
View File
@@ -10,6 +10,7 @@
//
#import "OnScreenButtonState.h"
#import "VoidLink-Swift.h"
@implementation OnScreenButtonState
@@ -19,7 +20,7 @@
self.name = name;
//self.isHidden = isHidden;
self.position = position;
self.buttonType = buttonType;
self.widgetType = buttonType;
}
return self;
@@ -31,49 +32,107 @@
- (void) encodeWithCoder:(NSCoder*)encoder {
[encoder encodeObject:self.name forKey:@"name"];
[encoder encodeBool:self.folded forKey:@"folded"];
[encoder encodeInt32:self.revealMode forKey:@"revealMode"];
[encoder encodeBool:self.bulkMoveEnabled forKey:@"bulkMoveEnabled"];
[encoder encodeInt32:self.sequence forKey:@"sequence"];
[encoder encodeInt32:self.parentSequence forKey:@"parentSequence"];
[encoder encodeObject:self.sequenceSet forKey:@"sequenceSet"];
[encoder encodeInt32:self.autoDockTimer forKey:@"autoDockTimer"];
[encoder encodeFloat:self.dockedAlpha forKey:@"dockedAlpha"];
[encoder encodeObject:self.alias forKey:@"alias"];
[encoder encodeInt:self.buttonType forKey:@"buttonType"];
[encoder encodeInt:self.widgetType forKey:@"buttonType"]; // keep original key
[encoder encodeInt:self.sizeReference forKey:@"sizeReference"];
[encoder encodeInt:self.vibrationStyle forKey:@"vibrationStyle"];
[encoder encodeInt:self.mouseButtonAction forKey:@"mouseButtonAction"];
[encoder encodeInt:self.slideMode forKey:@"slideMode"];
[encoder encodeBool:self.animatesTransition forKey:@"animatesTransition"];
[encoder encodeInt:self.buttonMode forKey:@"slideMode"]; // buttonMode: previously slideMode, keep it for consistency
[encoder encodeInt:self.autoTapInterval forKey:@"autoTapInterval"];
[encoder encodeInt:self.autoTapRepeats forKey:@"autoTapRepeats"];
[encoder encodeCGPoint:self.position forKey:@"position"];
[encoder encodeBool:self.isHidden forKey:@"isHidden"];
[encoder encodeFloat:self.widthFactor forKey:@"widthFactor"];
[encoder encodeFloat:self.heightFactor forKey:@"heightFactor"];
[encoder encodeFloat:self.componentSizeFactor forKey:@"componentSizeFactor"];
[encoder encodeFloat:self.sensitivityFactorX forKey:@"sensitivityFactorX"];
[encoder encodeFloat:self.sensitivityFactorY forKey:@"sensitivityFactorY"];
[encoder encodeFloat:self.decelerationRate forKey:@"decelerationRate"];
[encoder encodeFloat:self.slideThreshold forKey:@"slideThreshold"];
[encoder encodeFloat:self.yawFactor forKey:@"yawFactor"];
[encoder encodeFloat:self.pitchFactor forKey:@"pitchFactor"];
[encoder encodeFloat:self.rollFactor forKey:@"rollFactor"];
[encoder encodeFloat:self.decelerationRateX forKey:@"decelerationRateX"];
[encoder encodeFloat:self.decelerationRateY forKey:@"decelerationRateY"];
[encoder encodeBool:self.touchPointAnchored forKey:@"touchPointAnchored"];
[encoder encodeFloat:self.stickIndicatorOffset forKey:@"stickIndicatorOffset"];
[encoder encodeFloat:self.oscLayerSizeFactor forKey:@"oscLayerSizeFactor"];
[encoder encodeFloat:self.backgroundAlpha forKey:@"backgroundAlpha"];
[encoder encodeFloat:self.labelAlpha forKey:@"labelAlpha"];
[encoder encodeFloat:self.borderAlpha forKey:@"borderAlpha"];
[encoder encodeFloat:self.highlightAlpha forKey:@"highlightAlpha"];
[encoder encodeFloat:self.borderWidth forKey:@"borderWidth"];
[encoder encodeFloat:self.highlightSizeFactor forKey:@"highlightSizeFactor"];
[encoder encodeObject:self.widgetShape forKey:@"widgetShape"];
[encoder encodeFloat:self.walkModeThreshold forKey:@"walkModeThreshold"];
[encoder encodeFloat:self.minStickOffset forKey:@"minStickOffset"];
[encoder encodeInt:self.sprintKeyActionType forKey:@"sprintKeyActionType"];
[encoder encodeFloat:self.sprintKeyThreshold forKey:@"sprintKeyThreshold"];
[encoder encodeInt:self.walkKeyActionType forKey:@"walkKeyActionType"];
[encoder encodeFloat:self.walkKeyThreshold forKey:@"walkKeyThreshold"];
}
- (id) initWithCoder:(NSCoder*)decoder {
if (self = [super init]) {
self.name = [decoder decodeObjectForKey:@"name"];
self.folded = [decoder containsValueForKey:@"folded"] ? [decoder decodeBoolForKey:@"folded"] : false;
self.revealMode = [decoder containsValueForKey:@"revealMode"] ? [decoder decodeInt32ForKey:@"revealMode"] : coexist;
self.bulkMoveEnabled = [decoder containsValueForKey:@"bulkMoveEnabled"] ? [decoder decodeBoolForKey:@"bulkMoveEnabled"] : true;
self.sequence = [decoder containsValueForKey:@"sequence"] ? [decoder decodeInt32ForKey:@"sequence"] : -1;
self.parentSequence = [decoder containsValueForKey:@"parentSequence"] ? [decoder decodeInt32ForKey:@"parentSequence"] : -1;
self.sequenceSet = [decoder containsValueForKey:@"sequenceSet"] ? [decoder decodeObjectForKey:@"sequenceSet"] : [NSSet set];
self.autoDockTimer = [decoder containsValueForKey:@"autoDockTimer"] ? [decoder decodeInt32ForKey:@"autoDockTimer"] : 0;
self.dockedAlpha = [decoder containsValueForKey:@"dockedAlpha"] ? [decoder decodeFloatForKey:@"dockedAlpha"] : 0.2;
self.alias = [decoder decodeObjectForKey:@"alias"];
self.buttonType = [decoder decodeIntForKey:@"buttonType"];
self.vibrationStyle = [decoder decodeIntForKey:@"vibrationStyle"];
self.widgetType = [decoder decodeIntForKey:@"buttonType"];
self.sizeReference = [decoder containsValueForKey:@"sizeReference"] ? [decoder decodeIntForKey:@"sizeReference"] : longSide;
self.vibrationStyle = [decoder containsValueForKey:@"vibrationStyle"] ? [decoder decodeIntForKey:@"vibrationStyle"] : UIImpactFeedbackStyleLight;
self.mouseButtonAction = [decoder decodeIntForKey:@"mouseButtonAction"];
self.slideMode = [decoder containsValueForKey:@"slideMode"] ? [decoder decodeIntForKey:@"slideMode"] : 0;
self.animatesTransition = [decoder containsValueForKey:@"animatesTransition"] ? [decoder decodeBoolForKey:@"animatesTransition"] : true;
self.buttonMode = [decoder containsValueForKey:@"slideMode"] ? [decoder decodeIntForKey:@"slideMode"] : 0;
self.autoTapInterval = [decoder containsValueForKey:@"autoTapInterval"] ? [decoder decodeIntForKey:@"autoTapInterval"] : 45;
self.autoTapRepeats = [decoder containsValueForKey:@"autoTapRepeats"] ? [decoder decodeIntForKey:@"autoTapRepeats"] : 0;
self.position = [decoder decodeCGPointForKey:@"position"];
self.isHidden = [decoder decodeBoolForKey:@"isHidden"];
self.widthFactor = [decoder decodeFloatForKey:@"widthFactor"];
self.heightFactor = [decoder decodeFloatForKey:@"heightFactor"];
self.sensitivityFactorX = [decoder decodeFloatForKey:@"sensitivityFactorX"];
self.sensitivityFactorX = self.sensitivityFactorX == 0 ? 1.0 : self.sensitivityFactorX;
self.sensitivityFactorY = [decoder decodeFloatForKey:@"sensitivityFactorY"];
self.sensitivityFactorY = self.sensitivityFactorY == 0 ? 1.0 : self.sensitivityFactorY;
self.decelerationRate = [decoder decodeFloatForKey:@"decelerationRate"];
self.componentSizeFactor = [decoder containsValueForKey:@"componentSizeFactor"] ? [decoder decodeFloatForKey:@"componentSizeFactor"] : 1.0;
self.sensitivityFactorX = [decoder containsValueForKey:@"sensitivityFactorX"] ? [decoder decodeFloatForKey:@"sensitivityFactorX"] : 1.0;
self.sensitivityFactorY = [decoder containsValueForKey:@"sensitivityFactorY"] ? [decoder decodeFloatForKey:@"sensitivityFactorY"] : 1.0;
self.slideThreshold = [decoder containsValueForKey:@"slideThreshold"] ? [decoder decodeFloatForKey:@"slideThreshold"] : 6.0;
self.yawFactor = [decoder containsValueForKey:@"yawFactor"] ? [decoder decodeFloatForKey:@"yawFactor"] : 1.0;
self.pitchFactor = [decoder containsValueForKey:@"pitchFactor"] ? [decoder decodeFloatForKey:@"pitchFactor"] : 1.0;
self.rollFactor = [decoder containsValueForKey:@"rollFactor"] ? [decoder decodeFloatForKey:@"rollFactor"] : 1.0;
self.decelerationRateX = [decoder containsValueForKey:@"decelerationRateX"] ? [decoder decodeFloatForKey:@"decelerationRateX"] : 0.5;
self.decelerationRateY = [decoder containsValueForKey:@"decelerationRateY"] ? [decoder decodeFloatForKey:@"decelerationRateY"] : 0.5;
self.touchPointAnchored = [decoder containsValueForKey:@"touchPointAnchored"] ? [decoder decodeBoolForKey:@"touchPointAnchored"] : false;
self.stickIndicatorOffset = [decoder decodeFloatForKey:@"stickIndicatorOffset"];
self.oscLayerSizeFactor = [decoder decodeFloatForKey:@"oscLayerSizeFactor"];
self.backgroundAlpha = [decoder decodeFloatForKey:@"backgroundAlpha"];
self.backgroundAlpha = [decoder containsValueForKey:@"backgroundAlpha"] ? [decoder decodeFloatForKey:@"backgroundAlpha"] : 0.5;
self.labelAlpha = [decoder containsValueForKey:@"labelAlpha"] ? [decoder decodeFloatForKey:@"labelAlpha"] : 0.82;
self.borderAlpha = [decoder containsValueForKey:@"borderAlpha"] ? [decoder decodeFloatForKey:@"borderAlpha"] : 0.19;
self.highlightAlpha = [decoder containsValueForKey:@"highlightAlpha"] ? [decoder decodeFloatForKey:@"highlightAlpha"] : 0.77;
self.borderWidth = [decoder decodeFloatForKey:@"borderWidth"];
self.highlightSizeFactor = [decoder containsValueForKey:@"highlightSizeFactor"] ? [decoder decodeFloatForKey:@"highlightSizeFactor"] : 1.0;
self.widgetShape = [decoder decodeObjectForKey:@"widgetShape"];
self.walkModeThreshold = [decoder containsValueForKey:@"walkModeThreshold"] ? [decoder decodeFloatForKey:@"walkModeThreshold"] : 16383;
self.minStickOffset = [decoder decodeFloatForKey:@"minStickOffset"];
self.sprintKeyActionType = [decoder containsValueForKey:@"sprintKeyActionType"] ? [decoder decodeIntForKey:@"sprintKeyActionType"] : WalkSprintKeyActionTypeHold;
self.sprintKeyThreshold = [decoder containsValueForKey:@"sprintKeyThreshold"] ? [decoder decodeFloatForKey:@"sprintKeyThreshold"] : 0.6;
self.walkKeyActionType = [decoder containsValueForKey:@"walkKeyActionType"] ? [decoder decodeIntForKey:@"walkKeyActionType"] : WalkSprintKeyActionTypeHold;
self.walkKeyThreshold = [decoder containsValueForKey:@"walkKeyThreshold"] ? [decoder decodeFloatForKey:@"walkKeyThreshold"] : 0.08;
}
return self;
}
+1 -1
View File
@@ -14,7 +14,7 @@
CGFloat lowestTouchPointYCoord;
}
@property (nonatomic, weak) UIView* touchCapturingView;
@property (nonatomic, assign) uint8_t numberOfTouchesRequired;
@property (nonatomic, assign) bool immediateTriggering; // if enabled, trigger the signal on touchesBegan stage.
@property (nonatomic, assign) double tapDownTimeThreshold; // tap down threshold in seconds.
+29 -35
View File
@@ -39,13 +39,21 @@ static CGFloat screenWidthInPoints;
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// Check if the number of touches and taps meets the required criteria
if ([[event allTouches] count] == _numberOfTouchesRequired) {
_gestureCapturedTime = CACurrentMediaTime();
_gestureCaptured = true;
for(UITouch *touch in [event allTouches]){
if(lowestTouchPointYCoord < [touch locationInView:self.view].y) lowestTouchPointYCoord = [touch locationInView:self.view].y;
NSSet* allTouches = [event allTouches];
if (allTouches.count == _numberOfTouchesRequired) {
if([UITouchUtil touchesIn:_touchCapturingView from:event].count != allTouches.count){
_gestureCaptured = false;
self.state = UIGestureRecognizerStateFailed;
return;
}
_gestureCapturedTime = CACurrentMediaTime();
_gestureCaptured = true;
lowestTouchPointYCoord = 0;
for(UITouch *touch in allTouches){
if(lowestTouchPointYCoord < [touch locationInView:self.view].y) lowestTouchPointYCoord = [touch locationInView:self.view].y;
// NSLog(@"lowest point coord: %f, %f", lowestTouchPointYCoord, CACurrentMediaTime());
}
// this mechanism is deprecated.
/* if(_numberOfTouchesRequired == 2){
NSArray *twoTouches = [[event allTouches] allObjects];
@@ -55,13 +63,13 @@ static CGFloat screenWidthInPoints;
_lowestTouchPointHeight = screenHeightInPoints - lowestTouchPointYCoord;
// NSLog(@"gen _lowestTouchPointHeight %f markmark", _lowestTouchPointHeight);
if(_immediateTriggering){
lowestTouchPointYCoord = 0.0; //reset for next recoginition
// lowestTouchPointYCoord = 0.0; //reset for next recoginition
self.state = UIGestureRecognizerStateRecognized;
return;
}
self.state = UIGestureRecognizerStatePossible;
}
if ([[event allTouches] count] > _numberOfTouchesRequired) {
if (allTouches.count > _numberOfTouchesRequired) {
_gestureCaptured = false;
self.state = UIGestureRecognizerStateFailed;
}
@@ -76,40 +84,26 @@ static CGFloat screenWidthInPoints;
_gestureCaptured = false;
self.state = UIGestureRecognizerStateFailed;
}
else if(_gestureCaptured && allTouchesCount == [touches count] && !_isOnScreenControllerBeingPressed && ![self isOnScreenWidgetViewBeingPressed]){ //must exclude virtual controller & onscreen button taps here to prevent stucked button, _areVirtualControllerTaps flag is set by onscreencontrols, containOnScreenButtonsTaps will be returned by iterating all widget views in streamframeview
else if(_gestureCaptured
&& allTouchesCount == [touches count]
&& !_isOnScreenControllerBeingPressed){ //must exclude virtual controller & onscreen button taps here to prevent stucked button, _areVirtualControllerTaps flag is set by onscreencontrols, containOnScreenButtonsTaps will be returned by iterating all widget views in streamframeview
_gestureCaptured = false; //reset for next recognition
if((CACurrentMediaTime() - _gestureCapturedTime) < _tapDownTimeThreshold){
lowestTouchPointYCoord = 0.0; //reset for next recognition
self.state = UIGestureRecognizerStateRecognized;
// lowestTouchPointYCoord = 0.0; //reset for next recognition
for(UITouch* touch in touches){
CGVector vector = [UITouchUtil vectorOf:touch in:self.view];
if(hypot(vector.dx, vector.dy)>2){
self.state = UIGestureRecognizerStateFailed;
break;
}
}
if(self.state != UIGestureRecognizerStateFailed) self.state = UIGestureRecognizerStateRecognized;
// NSLog(@"gen _lowestTouchPointHeight %f markmark touchesEnd", _lowestTouchPointHeight);
}
}
if (allTouchesCount == [touches count]) _isOnScreenControllerBeingPressed = false; // need to reset this flag anyway, when all fingers are lefting
}
// it was a not a perfect choice to code OnScreenWidgetView in Swift...
// we're unable to import this class to swift codebase by the bridging header,and have to exlucde the onscreen button taps here
// by iterating every widget view instances. but it's basically ok since the number of widgetViews are always limited.
// and this method will only be called when the recognizer is active & and the taps passes all the checks and is about to ge triggered.
- (bool)isOnScreenWidgetViewBeingPressed {
NSTimeInterval allFingersTapDownTime = CACurrentMediaTime() - _gestureCapturedTime; //RIGHTCLICK_TAP_DOWN_TIME_THRESHOLD_S has been passed to this recognizer as _tapDownTimeShreshold, we'll decide how to deal with pressed flag of on-screen widget views based on tapDownTime
bool gotOneWidgetPressed = false;
// the tap gestures are all added to the streamFrameTopLayerView, where all the OnScreenWidgetViews are added. so we can iterate them in this way:
for(UIView* view in self.view.subviews){
if ([view isKindOfClass:[OnScreenWidgetView class]]) {
OnScreenWidgetView* widgetView = (OnScreenWidgetView*) view;
if(gotOneWidgetPressed && allFingersTapDownTime <= _tapDownTimeThreshold){ // once we have just 1 button pressed already,& the tapDownTime has not exceeded the threshold, we'll reset pressed flag for the all the widgetViews.
widgetView.pressed = false; //reset the flag for widgetView
continue;
}
if(widgetView.pressed){
gotOneWidgetPressed = true; //got one button pressed
if(allFingersTapDownTime <= _tapDownTimeThreshold) widgetView.pressed = false; // reset the flag for current widgetView if tapDowntime is still within the threshold, if not, leave the flag for mouse scroller gesture in relative touch mode.
}
}
}
return gotOneWidgetPressed;
}
@end
+456
View File
@@ -0,0 +1,456 @@
//
// GameSirG8MFiRumble.m -> GameSirG8MFiRumbler.swift
// VoidLink
//
// Created by chrisnch on 2026/08/22
// Copyright © 2026 chrisnch@github. All rights reserved.
//
// Modified by True砖家 on 2026/08/25
// Copyright © 2026 True砖家 on Bilibili. All rights reserved.
//
import ExternalAccessory
import Foundation
import GameController
import UIKit
@objcMembers
final class GameSirG8MFiRumbler: NSObject, StreamDelegate {
private static let gameSirG8MFiProtocol = "com.xiaoji.M2boot"
private static let gameSirManufacturer = "GameSir"
private static let gameSirG8MFiModel = "G8+ MFi"
static var repeatInterval: TimeInterval = 0.167253
private var session: EASession?
private var inFlightPacket: Data?
private var inFlightOffset = 0
private var queuedPacket: Data?
private var lastSentPacket: Data?
private var repeatTimer: Timer?
private var repeatPacket: Data?
private var appActive: Bool
private var invalidated = false
override init() {
appActive = UIApplication.shared.applicationState == .active
super.init()
NotificationCenter.default.addObserver(
self,
selector: #selector(applicationWillResignActive(_:)),
name: UIApplication.willResignActiveNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(applicationDidBecomeActive(_:)),
name: UIApplication.didBecomeActiveNotification,
object: nil
)
}
deinit {
NotificationCenter.default.removeObserver(self)
assert(Thread.isMainThread || session == nil, "Active GameSir sessions must be closed on the main thread before deallocation")
if Thread.isMainThread {
stopAndCloseOnMainThread()
}
}
func canHandleController(_ controller: GCController) -> Bool {
return isTargetController(controller) && connectedAccessory() != nil
}
func isTargetController(_ controller: GCController) -> Bool {
guard let vendorName = controller.vendorName, !vendorName.isEmpty else {
return false
}
return vendorName.range(of: Self.gameSirManufacturer, options: [.caseInsensitive, .diacriticInsensitive]) != nil &&
vendorName.range(of: "G8+", options: [.caseInsensitive, .diacriticInsensitive]) != nil &&
vendorName.range(of: "MFi", options: [.caseInsensitive, .diacriticInsensitive]) != nil
}
@objc(setLowFrequencyMotor:highFrequencyMotor:)
func setLowFrequencyMotor(_ lowFrequencyMotor: UInt16, highFrequencyMotor: UInt16) {
objc_sync_enter(self)
let isInvalidated = invalidated
objc_sync_exit(self)
if isInvalidated {
return
}
let packet = packet(lowFrequencyMotor: lowFrequencyMotor, highFrequencyMotor: highFrequencyMotor)
// NSLog("[G8Rumble] queue request low=%hu high=%hu packet=%@", lowFrequencyMotor, highFrequencyMotor, packet as NSData)
let shouldRepeat = lowFrequencyMotor != 0 || highFrequencyMotor != 0
DispatchQueue.main.async { [weak self] in
guard let self else {
return
}
objc_sync_enter(self)
let isInvalidated = self.invalidated
objc_sync_exit(self)
if isInvalidated {
// NSLog("[G8Rumble] drop packet because rumble handler is invalidated")
return
}
if !self.appActive {
// NSLog("[G8Rumble] drop packet because app is inactive packet=%@", packet as NSData)
return
}
if shouldRepeat {
self.stopRepeatTimer(clearPacket: false)
self.repeatPacket = packet
self.startRepeatTimer()
} else {
self.stopRepeatTimer()
}
self.queuePacket(packet)
}
}
func stopAndClose() {
if Thread.isMainThread {
stopAndCloseOnMainThread()
return
}
DispatchQueue.main.async { [weak self] in
self?.stopAndCloseOnMainThread()
}
}
func invalidate() {
objc_sync_enter(self)
invalidated = true
objc_sync_exit(self)
stopAndClose()
}
@objc private func applicationWillResignActive(_ notification: Notification) {
appActive = false
stopAndCloseOnMainThread()
}
@objc private func applicationDidBecomeActive(_ notification: Notification) {
appActive = true
}
private func connectedAccessory() -> EAAccessory? {
var sawAccessory = false
for accessory in EAAccessoryManager.shared().connectedAccessories {
sawAccessory = true
/*
NSLog(
"[G8Rumble] EA accessory name=%@ manufacturer=%@ model=%@ protocols=%@ connected=%d",
accessory.name,
accessory.manufacturer,
accessory.modelNumber,
accessory.protocolStrings,
accessory.isConnected
) */
guard accessory.isConnected,
accessory.protocolStrings.contains(Self.gameSirG8MFiProtocol),
!accessory.manufacturer.isEmpty,
!accessory.modelNumber.isEmpty,
accessory.manufacturer.caseInsensitiveCompare(Self.gameSirManufacturer) == .orderedSame,
accessory.modelNumber.caseInsensitiveCompare(Self.gameSirG8MFiModel) == .orderedSame else {
continue
}
NSLog("[G8Rumble] matched GameSir G8+ MFi accessory name=%@", accessory.name)
return accessory
}
if sawAccessory {
NSLog("[G8Rumble] no matching GameSir G8+ MFi accessory")
} else {
NSLog("[G8Rumble] no connected EA accessories")
}
return nil
}
private static func convertMotorAmplitude(_ amplitude: UInt16) -> UInt8 {
return UInt8((UInt32(amplitude) * 255 + 32767) / 65535)
}
private func packet(lowFrequencyMotor: UInt16, highFrequencyMotor: UInt16) -> Data {
return Data([
0x04,
Self.convertMotorAmplitude(lowFrequencyMotor),
0x01,
Self.convertMotorAmplitude(highFrequencyMotor),
0x01,
0x00,
0x00,
0x00,
0x00,
])
}
private func openSessionIfNeeded() -> Bool {
if session != nil {
return true
}
guard let accessory = connectedAccessory() else {
NSLog("[G8Rumble] cannot open session: accessory not found")
return false
}
NSLog("[G8Rumble] opening EASession protocol=%@ accessory=%@", Self.gameSirG8MFiProtocol, accessory.name)
guard let newSession = EASession(accessory: accessory, forProtocol: Self.gameSirG8MFiProtocol),
let inputStream = newSession.inputStream,
let outputStream = newSession.outputStream else {
NSLog("[G8Rumble] failed to create EASession")
return false
}
session = newSession
inputStream.delegate = self
outputStream.delegate = self
inputStream.schedule(in: .main, forMode: .default)
outputStream.schedule(in: .main, forMode: .default)
inputStream.open()
outputStream.open()
/*
NSLog(
"[G8Rumble] EASession opened inputStatus=%ld outputStatus=%ld",
inputStream.streamStatus.rawValue,
outputStream.streamStatus.rawValue
) */
return true
}
private func startRepeatTimer() {
let timer = Timer(timeInterval: Self.repeatInterval, repeats: true) { [weak self] _ in
self?.repeatCurrentPacket()
}
repeatTimer = timer
RunLoop.main.add(timer, forMode: .common)
// NSLog("[G8Rumble] repeat timer started interval=%.3f", Self.repeatInterval)
}
private func stopRepeatTimer(clearPacket: Bool = true) {
guard let timer = repeatTimer else {
if clearPacket {
repeatPacket = nil
}
return
}
timer.invalidate()
repeatTimer = nil
if clearPacket {
repeatPacket = nil
}
// NSLog("[G8Rumble] repeat timer stopped")
}
private func repeatCurrentPacket() {
objc_sync_enter(self)
let isInvalidated = invalidated
objc_sync_exit(self)
if isInvalidated || !appActive {
stopRepeatTimer()
return
}
guard let packet = repeatPacket else {
stopRepeatTimer()
return
}
queuePacket(packet)
}
private func queuePacket(_ packet: Data) {
queuedPacket = packet
// NSLog("[G8Rumble] queued packet=%@", packet as NSData)
if openSessionIfNeeded() {
flushOutput()
} else {
// NSLog("[G8Rumble] packet queued but session is not open packet=%@", packet as NSData)
}
}
private func flushOutput() {
guard let outputStream = session?.outputStream else {
return
}
guard outputStream.streamStatus == .open, outputStream.hasSpaceAvailable else {
/*
NSLog(
"[G8Rumble] flush skipped status=%ld hasSpace=%d queued=%@ inFlight=%@",
outputStream.streamStatus.rawValue,
outputStream.hasSpaceAvailable,
(queuedPacket as NSData?) ?? NSNull(),
(inFlightPacket as NSData?) ?? NSNull()
) */
return
}
while outputStream.hasSpaceAvailable {
if inFlightPacket == nil {
guard let packet = queuedPacket else {
return
}
inFlightPacket = packet
queuedPacket = nil
inFlightOffset = 0
}
guard let packet = inFlightPacket else {
return
}
let remainingLength = packet.count - inFlightOffset
let written = packet.withUnsafeBytes { rawBuffer -> Int in
guard let baseAddress = rawBuffer.bindMemory(to: UInt8.self).baseAddress else {
return -1
}
return outputStream.write(baseAddress.advanced(by: inFlightOffset), maxLength: remainingLength)
}
if written <= 0 {
NSLog(
"[G8Rumble] write failed packet=%@ offset=%lu remaining=%lu written=%ld status=%ld error=%@",
packet as NSData,
UInt(inFlightOffset),
UInt(remainingLength),
written,
outputStream.streamStatus.rawValue,
outputStream.streamError as NSError? ?? NSNull()
)
return
}
/*
NSLog(
"[G8Rumble] write packet=%@ offset=%lu remaining=%lu written=%ld status=%ld",
packet as NSData,
UInt(inFlightOffset),
UInt(remainingLength),
written,
outputStream.streamStatus.rawValue
) */
inFlightOffset += written
if inFlightOffset == packet.count {
// NSLog("[G8Rumble] packet sent %@", packet as NSData)
lastSentPacket = packet
inFlightPacket = nil
inFlightOffset = 0
}
}
}
private func writeStopPacketBestEffort() {
guard let outputStream = session?.outputStream else {
return
}
guard outputStream.streamStatus == .open, outputStream.hasSpaceAvailable else {
return
}
queuedPacket = nil
flushOutput()
if inFlightPacket != nil {
return
}
let stopPacket = packet(lowFrequencyMotor: 0, highFrequencyMotor: 0)
var offset = 0
while offset < stopPacket.count && outputStream.hasSpaceAvailable {
let written = stopPacket.withUnsafeBytes { rawBuffer -> Int in
guard let baseAddress = rawBuffer.bindMemory(to: UInt8.self).baseAddress else {
return -1
}
return outputStream.write(baseAddress.advanced(by: offset), maxLength: stopPacket.count - offset)
}
if written <= 0 {
NSLog(
"[G8Rumble] stop packet write failed offset=%lu written=%ld status=%ld error=%@",
UInt(offset),
written,
outputStream.streamStatus.rawValue,
outputStream.streamError as NSError? ?? NSNull()
)
break
}
offset += written
}
if offset == stopPacket.count {
lastSentPacket = stopPacket
}
NSLog("[G8Rumble] stop packet written=%lu/%lu packet=%@", UInt(offset), UInt(stopPacket.count), stopPacket as NSData)
}
private func closeSession() {
assert(Thread.isMainThread, "GameSir sessions must be closed on the main thread")
stopRepeatTimer()
if let session,
let inputStream = session.inputStream,
let outputStream = session.outputStream {
NSLog(
"[G8Rumble] closing EASession inputStatus=%ld outputStatus=%ld",
inputStream.streamStatus.rawValue,
outputStream.streamStatus.rawValue
)
inputStream.close()
outputStream.close()
inputStream.remove(from: .main, forMode: .default)
outputStream.remove(from: .main, forMode: .default)
inputStream.delegate = nil
outputStream.delegate = nil
}
session = nil
inFlightPacket = nil
inFlightOffset = 0
queuedPacket = nil
lastSentPacket = nil
}
private func stopAndCloseOnMainThread() {
assert(Thread.isMainThread, "GameSir sessions must be stopped on the main thread")
writeStopPacketBestEffort()
closeSession()
}
func stream(_ stream: Stream, handle eventCode: Stream.Event) {
guard let session,
let inputStream = session.inputStream,
let outputStream = session.outputStream else {
return
}
guard stream === inputStream || stream === outputStream else {
return
}
switch eventCode {
case .hasBytesAvailable:
var buffer = [UInt8](repeating: 0, count: 64)
while inputStream.hasBytesAvailable {
if inputStream.read(&buffer, maxLength: buffer.count) <= 0 {
break
}
}
case .openCompleted, .hasSpaceAvailable:
if stream === outputStream {
flushOutput()
}
case .errorOccurred, .endEncountered:
writeStopPacketBestEffort()
closeSession()
default:
break
}
}
}
+3
View File
@@ -12,6 +12,9 @@
@interface HapticContext : NSObject
-(void)setMotorAmplitude:(unsigned short)amplitude;
-(void)setAuthoredAmplitude:(float)amplitude
sharpness:(float)sharpness
transientStrength:(float)transientStrength;
-(void)cleanup;
+(HapticContext*) createContextForHighFreqMotor:(GCController*)gamepad;
+197 -29
View File
@@ -5,8 +5,12 @@
// Created by Cameron Gutman on 9/17/20.
// Copyright © 2020 Moonlight Game Streaming Project. All rights reserved.
//
// Modified by True砖家 on 2025.10.12
// Copyright © 2025 True砖家 @ Bilibili. All rights reserved.
//
#import "HapticContext.h"
#import "DataManager.h"
@import CoreHaptics;
@import GameController;
@@ -14,14 +18,20 @@
@implementation HapticContext {
GCControllerPlayerIndex _playerIndex;
CHHapticEngine* _hapticEngine API_AVAILABLE(ios(13.0), tvos(14.0));
id<CHHapticPatternPlayer> _hapticPlayer API_AVAILABLE(ios(13.0), tvos(14.0));
BOOL _playing;
id<CHHapticPatternPlayer> _motorHapticPlayer API_AVAILABLE(ios(13.0), tvos(14.0));
id<CHHapticPatternPlayer> _authoredHapticPlayer API_AVAILABLE(ios(13.0), tvos(14.0));
BOOL _motorPlaying;
BOOL _authoredPlaying;
}
-(void)cleanup API_AVAILABLE(ios(14.0), tvos(14.0)) {
if (_hapticPlayer != nil) {
[_hapticPlayer cancelAndReturnError:nil];
_hapticPlayer = nil;
if (_motorHapticPlayer != nil) {
[_motorHapticPlayer cancelAndReturnError:nil];
_motorHapticPlayer = nil;
}
if (_authoredHapticPlayer != nil) {
[_authoredHapticPlayer cancelAndReturnError:nil];
_authoredHapticPlayer = nil;
}
if (_hapticEngine != nil) {
[_hapticEngine stopWithCompletionHandler:nil];
@@ -39,15 +49,15 @@
// Stop the effect entirely if the amplitude is 0
if (amplitude == 0) {
if (_playing) {
[_hapticPlayer stopAtTime:0 error:&error];
_playing = NO;
if (_motorPlaying) {
[_motorHapticPlayer stopAtTime:0 error:&error];
_motorPlaying = NO;
}
return;
}
if (_hapticPlayer == nil) {
if (_motorHapticPlayer == nil) {
// We must initialize the intensity to 1.0f because the dynamic parameters are multiplied by this value before being applied
CHHapticEventParameter* intensityParameter = [[CHHapticEventParameter alloc] initWithParameterID:CHHapticEventParameterIDHapticIntensity value:1.0f];
CHHapticEvent* hapticEvent = [[CHHapticEvent alloc] initWithEventType:CHHapticEventTypeHapticContinuous parameters:[NSArray arrayWithObject:intensityParameter] relativeTime:0 duration:GCHapticDurationInfinite];
@@ -57,7 +67,7 @@
return;
}
_hapticPlayer = [_hapticEngine createPlayerWithPattern:hapticPattern error:&error];
_motorHapticPlayer = [_hapticEngine createPlayerWithPattern:hapticPattern error:&error];
if (error != nil) {
Log(LOG_W, @"Controller %d: Haptic player creation failed: %@", _playerIndex, error);
return;
@@ -65,37 +75,183 @@
}
CHHapticDynamicParameter* intensityParameter = [[CHHapticDynamicParameter alloc] initWithParameterID:CHHapticDynamicParameterIDHapticIntensityControl value:amplitude / 65535.0f relativeTime:0];
[_hapticPlayer sendParameters:[NSArray arrayWithObject:intensityParameter] atTime:CHHapticTimeImmediate error:&error];
[_motorHapticPlayer sendParameters:[NSArray arrayWithObject:intensityParameter] atTime:CHHapticTimeImmediate error:&error];
if (error != nil) {
Log(LOG_W, @"Controller %d: Haptic player parameter update failed: %@", _playerIndex, error);
return;
}
if (!_playing) {
[_hapticPlayer startAtTime:0 error:&error];
if (!_motorPlaying) {
[_motorHapticPlayer startAtTime:0 error:&error];
if (error != nil) {
_hapticPlayer = nil;
_motorHapticPlayer = nil;
Log(LOG_W, @"Controller %d: Haptic playback start failed: %@", _playerIndex, error);
return;
}
_playing = YES;
_motorPlaying = YES;
}
}
-(id) initWithGamepad:(GCController*)gamepad locality:(GCHapticsLocality)locality API_AVAILABLE(ios(14.0), tvos(14.0)) {
if (gamepad.haptics == nil) {
Log(LOG_W, @"Controller %d does not support haptics", gamepad.playerIndex);
-(void)setAuthoredAmplitude:(float)amplitude
sharpness:(float)sharpness
transientStrength:(float)transientStrength API_AVAILABLE(ios(14.0), tvos(14.0)) {
if (_hapticEngine == nil) {
return;
}
amplitude = fmaxf(0.0f, fminf(1.0f, amplitude));
sharpness = fmaxf(0.0f, fminf(1.0f, sharpness));
transientStrength = fmaxf(0.0f, fminf(1.0f, transientStrength));
NSError* error = nil;
if (amplitude == 0.0f) {
if (_authoredPlaying) {
[_authoredHapticPlayer stopAtTime:CHHapticTimeImmediate error:&error];
_authoredPlaying = NO;
}
}
else {
if (_authoredHapticPlayer == nil) {
CHHapticEventParameter* intensity = [[CHHapticEventParameter alloc]
initWithParameterID:CHHapticEventParameterIDHapticIntensity value:1.0f];
CHHapticEventParameter* eventSharpness = [[CHHapticEventParameter alloc]
initWithParameterID:CHHapticEventParameterIDHapticSharpness value:0.5f];
CHHapticEvent* event = [[CHHapticEvent alloc]
initWithEventType:CHHapticEventTypeHapticContinuous
parameters:@[intensity, eventSharpness]
relativeTime:0
duration:GCHapticDurationInfinite];
CHHapticPattern* pattern = [[CHHapticPattern alloc]
initWithEvents:@[event] parameters:@[] error:&error];
if (pattern == nil || error != nil) {
Log(LOG_W, @"Controller %d: Authored haptic pattern creation failed: %@", _playerIndex, error);
return;
}
_authoredHapticPlayer = [_hapticEngine createPlayerWithPattern:pattern error:&error];
if (_authoredHapticPlayer == nil || error != nil) {
Log(LOG_W, @"Controller %d: Authored haptic player creation failed: %@", _playerIndex, error);
return;
}
}
NSArray* parameters = @[
[[CHHapticDynamicParameter alloc]
initWithParameterID:CHHapticDynamicParameterIDHapticIntensityControl
value:amplitude relativeTime:0],
[[CHHapticDynamicParameter alloc]
initWithParameterID:CHHapticDynamicParameterIDHapticSharpnessControl
value:sharpness relativeTime:0]
];
[_authoredHapticPlayer sendParameters:parameters atTime:CHHapticTimeImmediate error:&error];
if (error != nil) {
Log(LOG_W, @"Controller %d: Authored haptic parameter update failed: %@", _playerIndex, error);
return;
}
if (!_authoredPlaying) {
[_authoredHapticPlayer startAtTime:CHHapticTimeImmediate error:&error];
if (error != nil) {
_authoredHapticPlayer = nil;
Log(LOG_W, @"Controller %d: Authored haptic playback start failed: %@", _playerIndex, error);
return;
}
_authoredPlaying = YES;
}
}
// Emit one-shot attacks only when the analyzer found a meaningful transient.
if (transientStrength >= 0.015f) {
CHHapticEventParameter* intensity = [[CHHapticEventParameter alloc]
initWithParameterID:CHHapticEventParameterIDHapticIntensity value:transientStrength];
CHHapticEventParameter* eventSharpness = [[CHHapticEventParameter alloc]
initWithParameterID:CHHapticEventParameterIDHapticSharpness value:sharpness];
CHHapticEvent* event = [[CHHapticEvent alloc]
initWithEventType:CHHapticEventTypeHapticTransient
parameters:@[intensity, eventSharpness]
relativeTime:0];
CHHapticPattern* pattern = [[CHHapticPattern alloc]
initWithEvents:@[event] parameters:@[] error:&error];
id<CHHapticPatternPlayer> transientPlayer = pattern == nil ? nil :
[_hapticEngine createPlayerWithPattern:pattern error:&error];
[transientPlayer startAtTime:CHHapticTimeImmediate error:&error];
if (error != nil) {
Log(LOG_W, @"Controller %d: Authored haptic transient failed: %@", _playerIndex, error);
}
}
}
-(id) initDeviceEngineContextWithGamepad:(GCController*)gamepad API_AVAILABLE(ios(13.0), tvos(13.0)) {
NSError *error = nil;
_hapticEngine = [[CHHapticEngine alloc] initAndReturnError:&error];
if (error != nil) {
Log(LOG_W, @"Controller %d: iPhone Haptic engine failed to start: %@", gamepad.playerIndex, error);
return nil;
}
_playerIndex = gamepad.playerIndex;
if (![[gamepad.haptics supportedLocalities] containsObject:locality]) {
Log(LOG_W, @"Controller %d does not support haptic locality: %@", gamepad.playerIndex, locality);
[_hapticEngine startAndReturnError:&error];
if (error != nil) {
Log(LOG_W, @"Controller %d: Haptic engine failed to start: %@", gamepad.playerIndex, error);
return nil;
}
__weak typeof(self) weakSelf = self;
_hapticEngine.stoppedHandler = ^(CHHapticEngineStoppedReason stoppedReason) {
HapticContext* me = weakSelf;
if (me == nil) {
return;
}
Log(LOG_W, @"Controller %d: Haptic engine stopped: %p", me->_playerIndex, stoppedReason);
me->_motorHapticPlayer = nil;
me->_authoredHapticPlayer = nil;
me->_hapticEngine = nil;
me->_motorPlaying = NO;
me->_authoredPlaying = NO;
};
_hapticEngine.resetHandler = ^{
HapticContext* me = weakSelf;
if (me == nil) {
return;
}
Log(LOG_W, @"Controller %d: Haptic engine reset", me->_playerIndex);
me->_motorHapticPlayer = nil;
me->_authoredHapticPlayer = nil;
me->_motorPlaying = NO;
me->_authoredPlaying = NO;
[me->_hapticEngine startAndReturnError:nil];
};
return self;
}
-(id) initWithGamepad:(GCController*)gamepad locality:(GCHapticsLocality)locality API_AVAILABLE(ios(14.0), tvos(14.0)) {
bool fallBackToPhoneHaptics = false;
if (gamepad.haptics == nil) {
Log(LOG_W, @"Controller %d does not support haptics", gamepad.playerIndex);
fallBackToPhoneHaptics = true;
}
else if (![[gamepad.haptics supportedLocalities] containsObject:locality]) {
Log(LOG_W, @"Controller %d does not support haptic locality: %@", gamepad.playerIndex, locality);
fallBackToPhoneHaptics = true;
}
if (fallBackToPhoneHaptics) {
Log(LOG_W, @"Controller %d falls back to use iPhone Haptics for locality: %@", gamepad.playerIndex, locality);
NSError *error = nil;
_hapticEngine = [[CHHapticEngine alloc] initAndReturnError:&error];
if (error != nil) {
Log(LOG_W, @"Controller %d: iPhone Haptic engine failed to start: %@", gamepad.playerIndex, error);
return nil;
}
}
else {
_hapticEngine = [gamepad.haptics createEngineWithLocality:locality];
}
_playerIndex = gamepad.playerIndex;
_hapticEngine = [gamepad.haptics createEngineWithLocality:locality];
NSError* error;
[_hapticEngine startAndReturnError:&error];
@@ -112,9 +268,11 @@
}
Log(LOG_W, @"Controller %d: Haptic engine stopped: %p", me->_playerIndex, stoppedReason);
me->_hapticPlayer = nil;
me->_motorHapticPlayer = nil;
me->_authoredHapticPlayer = nil;
me->_hapticEngine = nil;
me->_playing = NO;
me->_motorPlaying = NO;
me->_authoredPlaying = NO;
};
_hapticEngine.resetHandler = ^{
HapticContext* me = weakSelf;
@@ -123,8 +281,10 @@
}
Log(LOG_W, @"Controller %d: Haptic engine reset", me->_playerIndex);
me->_hapticPlayer = nil;
me->_playing = NO;
me->_motorHapticPlayer = nil;
me->_authoredHapticPlayer = nil;
me->_motorPlaying = NO;
me->_authoredPlaying = NO;
[me->_hapticEngine startAndReturnError:nil];
};
@@ -136,7 +296,9 @@
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityRightHandle];
}
else {
return nil;
if (@available(iOS 13.0, *)) {
return [[HapticContext alloc] initDeviceEngineContextWithGamepad:gamepad];
} else return nil;
}
}
@@ -145,7 +307,9 @@
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityLeftHandle];
}
else {
return nil;
if (@available(iOS 13.0, *)) {
return [[HapticContext alloc] initDeviceEngineContextWithGamepad:gamepad];
} else return nil;
}
}
@@ -154,7 +318,9 @@
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityLeftTrigger];
}
else {
return nil;
if (@available(iOS 13.0, *)) {
return [[HapticContext alloc] initDeviceEngineContextWithGamepad:gamepad];
} else return nil;
}
}
@@ -163,7 +329,9 @@
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityRightTrigger];
}
else {
return nil;
if (@available(iOS 13.0, *)) {
return [[HapticContext alloc] initDeviceEngineContextWithGamepad:gamepad];
} else return nil;
}
}
+36
View File
@@ -0,0 +1,36 @@
//
// InertialScroller.swift
// VoidLink
//
// Created by True砖家 on 2025/12/5.
// Copyright © 2025 True砖家 on Bilibili. All rights reserved.
//
class InertialScroller {
public var decelerationRateX: CGFloat
public var decelerationRateY: CGFloat
public lazy var timer: SafeTimer? = {
SafeTimer(interval: 1/displayLinkRate) { [weak self] in
guard let self = self, handler != nil else {return}
self.vector.dx = self.vector.dx * decelerationRateX
self.vector.dy = self.vector.dy * decelerationRateY
if abs(self.vector.dx) < self.timerSuspendThreshold && abs(self.vector.dy) < self.timerSuspendThreshold {
self.timer?.pause()
}
(self.handler ?? {})()
}
}()
public var vector: CGVector = .zero
public var displayLinkRate: CGFloat
public var timerSuspendThreshold: CGFloat = 0.06
public var handler: (() -> Void)?
init(decelerationRate: CGFloat = 0.93, displayLinkRate: CGFloat = 60, handler: (() -> Void)? = nil) {
self.decelerationRateX = decelerationRate
self.decelerationRateY = decelerationRate
self.displayLinkRate = displayLinkRate
self.handler = handler
}
}
File diff suppressed because it is too large Load Diff
+9 -5
View File
@@ -8,6 +8,7 @@
#import "KeyboardSupport.h"
#include <Limelight.h>
#include "VoidLink-Swift.h"
@implementation KeyboardSupport
@@ -17,7 +18,6 @@
}
else {
short keyCode;
switch (press.type) {
case UIPressTypeUpArrow:
keyCode = 0x26;
@@ -36,9 +36,13 @@
return NO;
}
LiSendKeyboardEvent(0x8000 | keyCode,
down ? KEY_ACTION_DOWN : KEY_ACTION_UP,
0);
bool physicalGamepadConnected = ControllerUtil.activeStreamingGCControllers.count>0;
// dispatch_after(dispatch_time(DISPATCH_TIME_NOW, physicalGamepadConnected ? 0.005*NSEC_PER_SEC : 0), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if(!physicalGamepadConnected)
LiSendKeyboardEvent(0x8000 | keyCode,
down ? KEY_ACTION_DOWN : KEY_ACTION_UP,
0);
// });
return YES;
}
@@ -252,7 +256,7 @@
NSLog(@"Undefined key pressed");
return false;
case 669: // This value corresponds to the "Globe" or "Language" key on most Apple branded iPad keyboards.
keyCode = 0x1B; // This value corresponds to "Escape", which is missing from most Apple branded iPad keyboards.
keyCode = GenericUtils.globeAsEscape ? 0x1B : 0XFF; // This value corresponds to "Escape", which is missing from most Apple branded iPad keyboards.
break;
default:
NSLog(@"Unhandled HID usage: %lu", (unsigned long)key.keyCode);
+545
View File
@@ -0,0 +1,545 @@
//
// MicHandler.swift
// VoidLink
//
// Created by True砖家 on 2025/9/2.
// Copyright © 2025 True砖家 on Bilibili. All rights reserved.
import AVFoundation
import Collections
@objc public protocol MicHandlerDelegate: AnyObject {
@objc optional func micHandlerDidFinishPlayback(_ handler: MicHandler)
@objc optional func micHandler(_ handler: MicHandler, didFailWithError error: NSError)
}
@objcMembers
public class MicHandler: NSObject {
private var notificationTokens = [NSObjectProtocol]()
private let engine = AVAudioEngine()
private let playerNode = AVAudioPlayerNode()
private var audioSink: Any?
private var micInputFormat: AVAudioFormat!
private var isRecording = false
private var useBuiltinMic = false
private var pcm16BufferDeque = Deque<Int16>()
private var pcm16BufferArray: [Int16] = []
private let bufferQueue = DispatchQueue(label: "pcm.buffer.queue")
private var timer: SafeTimer?
private static var volume: Float = 1.0
/*
private var recordedBuffers: [AVAudioPCMBuffer] = []
private var recordedPCM16: [Data] = []
private var recordedOpusPackets: [Data] = []
*/
private var opusEncoder: OpaquePointer?
private var opusDecoder: OpaquePointer?
private var sequenceNumber: UInt16 = 0
private let ssrc: UInt32 = 0x12345678
private var globalTimestamp: TimeInterval = 0
public weak var delegate: MicHandlerDelegate?
@objc public init(useBuiltinMic:Bool) {
super.init()
let token = NotificationCenter.default.addObserver(
forName: AVAudioSession.interruptionNotification,
object: nil,
queue: .main
) { [weak self] notification in
self?.handleInterruption(notification)
}
notificationTokens.append(token)
self.useBuiltinMic = useBuiltinMic
do {
try configureSession()
try configureEngine()
} catch {
notify(error)
}
}
/* ----------- Mic permission -------------*/
/// 请求麦克风权限
/// - Parameter completion: 可选 block,如果为 nil 且未授权,会弹窗提示跳转系统设置
@objc static func requestPermission(_ completion: ((Bool) -> Void)? = nil) {
let permission = AVAudioSession.sharedInstance().recordPermission
switch permission {
case .granted:
completion?(true)
case .denied:
if let callback = completion {
callback(false)
} else {
showSettingsAlert()
}
case .undetermined:
AVAudioSession.sharedInstance().requestRecordPermission { granted in
DispatchQueue.main.async {
if granted {
completion?(true)
} else {
if let callback = completion {
callback(false)
} else {
showSettingsAlert()
}
}
}
}
@unknown default:
if let callback = completion {
callback(false)
} else {
showSettingsAlert()
}
}
}
/// 弹窗提示用户跳转系统设置(英文版)
private static func showSettingsAlert() {
guard let topVC = topViewController() else { return }
let alert = UIAlertController(
title: LocalizationHelper.localizedString(forKey: "Microphone Permission") ,
message: LocalizationHelper.localizedString(forKey: "micPermissionTip"),
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: LocalizationHelper.localizedString(forKey: "Cancel"), style: .cancel, handler: nil))
alert.addAction(UIAlertAction(title: LocalizationHelper.localizedString(forKey: "Go to Settings") , style: .default, handler: { _ in
openSettings()
}))
topVC.present(alert, animated: true, completion: nil)
}
/// 打开系统设置
@objc static func openSettings() {
guard let url = URL(string: UIApplication.openSettingsURLString),
UIApplication.shared.canOpenURL(url) else { return }
UIApplication.shared.open(url)
}
/// 检查麦克风权限状态(返回 Int,OC 可用)
@objc static func permissionGranted() -> Bool {
return AVAudioSession.sharedInstance().recordPermission == AVAudioSession.RecordPermission.granted
}
/// 获取最顶层 UIViewController
private static func topViewController(base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return topViewController(base: nav.visibleViewController)
}
if let tab = base as? UITabBarController, let selected = tab.selectedViewController {
return topViewController(base: selected)
}
if let presented = base?.presentedViewController {
return topViewController(base: presented)
}
return base
}
/* ----------------------------------------*/
/* ----------- Audio Session -------------*/
private func handleInterruption(_ notification: Notification) {
guard let info = notification.userInfo,
let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSession.InterruptionType(rawValue: typeValue)
else { return }
switch type {
case .began:
self.stopTapping(stopEngine: true)
case .ended:
do {
try configureEngine()
} catch {
notify(error)
}
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
self.startTapping()
}
@unknown default:
break
}
}
private func configureOpus(sampleRate: Int32, channels: Int) throws {
var err: Int32 = 0
guard let enc = opus_encoder_create(sampleRate, Int32(channels), OPUS_APPLICATION_VOIP, &err), err == OPUS_OK else {
throw NSError(domain: "Opus", code: Int(err), userInfo: nil)
}
opusEncoder = enc
guard let dec = opus_decoder_create(sampleRate, Int32(channels), &err), err == OPUS_OK else {
throw NSError(domain: "Opus", code: Int(err), userInfo: nil)
}
opusDecoder = dec
// Optional: But defaults are fine. Only change when needed:
opus_encoder_ctl_wrapper(enc, Int32(OPUS_SET_BITRATE_REQUEST), opus_int32(64000)) // Set bitrate
opus_encoder_ctl_wrapper(enc, Int32(OPUS_SET_COMPLEXITY_REQUEST), opus_int32(5)) // Set complexity
opus_encoder_ctl_wrapper(enc, Int32(OPUS_SET_SIGNAL_REQUEST), OPUS_SIGNAL_VOICE) // Set signal type
}
@objc public func startTapping() {
// recordedBuffers.removeAll()
isRecording = true
self.timer?.start()
/*
DispatchQueue.main.asyncAfter(deadline: .now() + seconds) { [weak self] in
guard let self = self else { return }
self.isRecording = false
self.stop()
self.playbackRecorded()
self.playbackOpus()
}*/
}
@objc public func stopTapping(stopEngine:Bool) {
isRecording = false
self.timer?.pause()
// engine.inputNode.removeTap(onBus: 0)
if(stopEngine){
playerNode.stop()
engine.stop()
}
}
private func configureSession() throws {
let session = AVAudioSession.sharedInstance()
/*
let bluetoothAudioOption = self.useBuiltinMic ? AVAudioSession.CategoryOptions.allowBluetoothA2DP : AVAudioSession.CategoryOptions.allowBluetooth
try session.setCategory(.playAndRecord, mode: .default, options: [.mixWithOthers, .defaultToSpeaker, bluetoothAudioOption])
if #available(iOS 13.0, *) {
try session.setAllowHapticsAndSystemSoundsDuringRecording(true)
} */
// AVAudioSession Initiailized in Connection.m -> ArInit
// 列出所有可用输入
if self.useBuiltinMic, let inputs = session.availableInputs {
for port in inputs {
if port.portType == .builtInMic {
try session.setPreferredInput(port)
print("Set preferred input to built-in mic")
break
}
}
}
// try session.setActive(true)
}
private func sendOpusFrameFromDequeBuffer() {
bufferQueue.sync {
if pcm16BufferDeque.count >= 960 {
let chunk = Array(pcm16BufferDeque.prefix(960))
var packet = [UInt8](repeating: 0, count: 4000)
guard let enc = self.opusEncoder else {return}
let outBytes = opus_encode(enc, chunk, 960, &packet, Int32(packet.count))
sendMicrophoneOpusData(packet, outBytes)
let removeCount = min(960, pcm16BufferDeque.count)
if removeCount > 0 {
pcm16BufferDeque.removeFirst(removeCount)
}
}
}
}
private func sendOpusFrameFromArrayBuffer() {
bufferQueue.sync {
if pcm16BufferArray.count >= 960 {
let chunk = Array(pcm16BufferArray.prefix(960))
var packet = [UInt8](repeating: 0, count: 4000)
guard let enc = self.opusEncoder else {return}
let outBytes = opus_encode(enc, chunk, 960, &packet, Int32(packet.count))
sendMicrophoneOpusData(packet, outBytes)
let removeCount = min(960, pcm16BufferArray.count)
if removeCount > 0 {
pcm16BufferArray.removeFirst(removeCount)
}
}
}
}
@objc public static func setVolume(_ linearVolume: Float) {
let clamped = max(0.0, min(1.5, linearVolume))
let exponent: Float = 1.7
MicHandler.volume = powf(clamped, exponent)
}
private func configureEngine() throws {
let input = engine.inputNode
micInputFormat = input.inputFormat(forBus: 0)
try self.configureOpus(sampleRate: Int32(micInputFormat.sampleRate), channels: Int(micInputFormat.channelCount))
if #available(iOS 13.0, tvOS 13.0, *) {
try AVAudioSession.sharedInstance().setPreferredIOBufferDuration(0.02)
let sinkNode = AVAudioSinkNode { timestamp, frameCount, audioBufferList -> OSStatus in
guard self.isRecording else { return noErr}
let abl = audioBufferList.pointee.mBuffers
guard let data = abl.mData?.assumingMemoryBound(to: Float.self) else { return noErr }
let samples = UnsafeBufferPointer(start: data, count: Int(frameCount))
// 转 float32 -> int16
var chunk = [Int16](repeating: 0, count: samples.count)
for i in 0..<samples.count {
let clamped = max(min(samples[i] * MicHandler.volume, 1.0), -1.0)
chunk[i] = Int16(clamped * Float(Int16.max))
}
// 追加到缓冲区
self.bufferQueue.sync {
self.pcm16BufferDeque.append(contentsOf: chunk)
}
return noErr
}
audioSink = sinkNode
engine.attach(audioSink! as! AVAudioNode)
engine.connect(engine.inputNode, to: audioSink as! AVAudioNode, format: micInputFormat)
// 开定时器,每 20ms 触发一次
self.timer = SafeTimer(interval:0.02, delay: 0.05) {
self.sendOpusFrameFromDequeBuffer()
}
}
else{
engine.attach(playerNode)
engine.connect(playerNode, to: engine.mainMixerNode, format: micInputFormat)
input.installTap(onBus: 0, bufferSize: 5760, format: micInputFormat) { [weak self] buffer, _ in
guard let self = self, self.isRecording else { return }
// ===============================
// 把 buffer 转成 PCM16 并保存
let frameLength = Int(buffer.frameLength)
let channels = Int(buffer.format.channelCount)
var pcm16InterleavedBuffer = [Int16](repeating: 0, count: frameLength * channels)
if let floatPtrs = buffer.floatChannelData {
for ch in 0..<channels {
let floatPtr = floatPtrs[ch]
for i in 0..<frameLength {
let f = floatPtr[i] * MicHandler.volume
// 把 float 转到 Int16 范围:假设 float 在 -1…+1 之间
// 乘以 Int16.max (32767),再做裁剪
let scaled = f * Float(Int16.max)
let clipped: Float
if scaled > Float(Int16.max) {
clipped = Float(Int16.max)
} else if scaled < Float(Int16.min) {
clipped = Float(Int16.min)
} else {
clipped = scaled
}
pcm16InterleavedBuffer[i * channels + ch] = Int16(clipped)
}
}
}
// 现在 pcm16InterleavedBuffer 里就是转换后的 Int16 数据
self.bufferQueue.sync {
self.pcm16BufferArray.append(contentsOf: pcm16InterleavedBuffer)
}
}
// 开定时器,每 20ms 触发一次
self.timer = SafeTimer(interval:0.02, delay: 0.05) {
self.sendOpusFrameFromArrayBuffer()
}
}
engine.prepare()
try engine.start()
}
@objc public func clean() {
for token in notificationTokens {
NotificationCenter.default.removeObserver(token)
}
notificationTokens.removeAll()
self.timer?.clean()
}
private func notify(_ error: Error) {
delegate?.micHandler?(self, didFailWithError: error as NSError)
}
// ===============================
// 🔹 新增播放 Opus 数据方法
/*
private func playbackOpus() {
guard let dec = opusDecoder else { return }
let channels = Int(micInputFormat.channelCount)
for (index, packet) in recordedOpusPackets.enumerated() {
let maxFrames = 5760 // 最大 120ms
let pcmBuf = UnsafeMutablePointer<Int16>.allocate(capacity: maxFrames * channels)
defer { pcmBuf.deallocate() }
let frameCount = opus_decode(dec,
[UInt8](packet),
Int32(packet.count),
pcmBuf,
Int32(maxFrames),
0)
if frameCount < 0 {
print("Opus decode error: \(frameCount)")
continue
}
// 构造源格式 AVAudioPCMBuffer (PCM16)
let sourceFormat = AVAudioFormat(commonFormat: .pcmFormatInt16,
sampleRate: micInputFormat.sampleRate,
channels: micInputFormat.channelCount,
interleaved: true)!
guard let sourceBuffer = AVAudioPCMBuffer(pcmFormat: sourceFormat,
frameCapacity: AVAudioFrameCount(frameCount)) else { continue }
sourceBuffer.frameLength = AVAudioFrameCount(frameCount)
// 填充 PCM16 数据到 sourceBuffer
let srcPointer = sourceBuffer.int16ChannelData![0]
for i in 0..<Int(frameCount * Int32(channels)) {
srcPointer[i] = pcmBuf[i]
}
// 准备目标 buffer (Float32)
guard let floatBuffer = AVAudioPCMBuffer(pcmFormat: micInputFormat,
frameCapacity: AVAudioFrameCount(frameCount)) else { continue }
// 🔹 使用 AVAudioConverter 转换
let converter = AVAudioConverter(from: sourceFormat, to: micInputFormat)!
var error: NSError? = nil
let inputBlock: AVAudioConverterInputBlock = { inNumPackets, outStatus in
outStatus.pointee = .haveData
return sourceBuffer
}
converter.convert(to: floatBuffer, error: &error, withInputFrom: inputBlock)
if let error = error {
print("AVAudioConverter error: \(error)")
continue
}
// 播放
if index == recordedOpusPackets.count - 1 {
playerNode.scheduleBuffer(floatBuffer, at: nil, options: []) { [weak self] in
guard let self = self else { return }
DispatchQueue.main.async {
self.playerNode.stop()
self.engine.stop()
}
}
} else {
playerNode.scheduleBuffer(floatBuffer, at: nil, options: [], completionHandler: nil)
}
}
// 启动 engine
if !engine.isRunning {
do { try engine.start() } catch { print(error) }
}
if !playerNode.isPlaying {
playerNode.play()
}
}
// ===============================
// 🔹 改动 2:播放 PCM16 数据
private func playbackRecorded() {
do {
try configureEngine()
} catch {
notify(error)
}
let channels = Int(micInputFormat.channelCount)
for (index, pcmData) in recordedPCM16.enumerated() {
let frameCount = pcmData.count / (MemoryLayout<Int16>.size * channels)
guard let buf = AVAudioPCMBuffer(pcmFormat: micInputFormat, frameCapacity: AVAudioFrameCount(frameCount)) else { continue }
buf.frameLength = buf.frameCapacity
// PCM16 -> Float32
pcmData.withUnsafeBytes { rawBuf in
let pcmPtr = rawBuf.bindMemory(to: Int16.self).baseAddress!
for i in 0..<frameCount {
for ch in 0..<channels {
buf.floatChannelData?[ch][i] = Float(pcmPtr[i * channels + ch]) / Float(Int16.max)
}
}
}
// 只在最后一个 buffer 设置 completionHandler
if index == recordedPCM16.count - 1 {
playerNode.scheduleBuffer(buf, at: nil, options: []) { [weak self] in
guard let self = self else { return }
// 🔹 回到主线程安全停止
DispatchQueue.main.async {
self.playerNode.stop()
self.engine.stop()
}
}
} else {
playerNode.scheduleBuffer(buf, at: nil, options: [], completionHandler: nil)
}
}
playerNode.play()
}
private func deepCopy(_ buffer: AVAudioPCMBuffer, format: AVAudioFormat) -> AVAudioPCMBuffer? {
guard let out = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: buffer.frameLength) else { return nil }
out.frameLength = buffer.frameLength
let channels = Int(format.channelCount)
for ch in 0..<channels {
if let src = buffer.floatChannelData?[ch], let dst = out.floatChannelData?[ch] {
dst.update(from: src, count: Int(buffer.frameLength))
}
}
return out
}
private func merge(buffers: [AVAudioPCMBuffer], format: AVAudioFormat) -> AVAudioPCMBuffer? {
let total = buffers.reduce(0) { $0 + $1.frameLength }
guard let out = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: total) else { return nil }
out.frameLength = total
var writePos: AVAudioFrameCount = 0
let channels = Int(format.channelCount)
for b in buffers {
let frames = Int(b.frameLength)
for ch in 0..<channels {
if let src = b.floatChannelData?[ch], let dst = out.floatChannelData?[ch] {
dst.advanced(by: Int(writePos)).update(from: src, count: frames)
}
}
writePos += b.frameLength
}
return out
}
*/
}
/* -------------------------------------------------------*/
+525
View File
@@ -0,0 +1,525 @@
//
// MicHandler.swift
// VoidLink
//
// Created by True砖家 on 2025/9/28.
// Copyright © 2025 True砖家 on Bilibili. All rights reserved.
//
import Foundation
import CoreMotion
@objc public protocol OnScreenWidgetStickMixedInputDelegate: AnyObject {
func mixOnScreenRightStickAndGyroInput(x: CGFloat, y: CGFloat)
func mixOnScreenLeftStickAndGyroInput(x: CGFloat, y: CGFloat)
func gyroMixInputStarted() -> Bool
}
@objc class MotionHandler: NSObject, OnScreenWidgetStickMixedInputDelegate{
public func mixOnScreenRightStickAndGyroInput(x: CGFloat, y: CGFloat) {
rightStickTouchInputX = x
rightStickTouchInputY = y
}
public func mixOnScreenLeftStickAndGyroInput(x: CGFloat, y: CGFloat) {
// rollIntegral = 0
leftStickTouchInputX = x
leftStickTouchInputY = y
}
@objc public func mixPhysicalRightStickAndGyroInput(x: CGFloat, y: CGFloat) {
rightStickPhysicalInputX = synthesizePhysicalStick ? x : 0
rightStickPhysicalInputY = synthesizePhysicalStick ? y : 0
}
@objc public func mixPhysicalLeftStickAndGyroInput(x: CGFloat, y: CGFloat) {
// rollIntegral = 0
leftStickPhysicalInputX = synthesizePhysicalStick ? x : 0
leftStickPhysicalInputY = synthesizePhysicalStick ? y : 0
}
public func gyroMixInputStarted() -> Bool {
return motionIsWorking
}
private static let sharedInstance = MotionHandler()
@objc class func shared(profile: OSCProfile?) -> MotionHandler {
let sharedInstance = MotionHandler.sharedInstance
guard let profile = profile else { return sharedInstance }
sharedInstance.useBuiltinGyro = profile.useBuiltinGyro
sharedInstance.swapYawAndRoll = profile.swapYawAndRoll
sharedInstance.mapGyroTo = profile.mapGyroTo
sharedInstance.synthesizePhysicalStick = profile.synthesizePhysicalStick
sharedInstance.rollToLeftStick = profile.rollToLeftStick
sharedInstance.yawPitchToRightStick = profile.yawPitchToRightStick
sharedInstance.sensitvityYaw = profile.gyroSensitivityYaw
sharedInstance.sensitvityPitch = profile.gyroSensitivityPitch
sharedInstance.sensitvityRoll = profile.gyroSensitivityRoll
sharedInstance.gyroToStickMinOffset = profile.gyroToStickMinOffset
sharedInstance.onScreenControls = OnScreenControls.shared()
return sharedInstance
}
@objc public var motionControlStarted: Bool = false
@objc var useBuiltinGyro: Bool = true
@objc var swapYawAndRoll: Bool = false
private var motionIsWorking: Bool = false
private var accelControlStarted: Bool = false
private let motionManager = CMMotionManager()
private weak var activeGCController:GCController?
private var synthesizePhysicalStick:Bool = false
private var mapGyroTo:MapGyroTo = .mapGyroToControllerStick
private var rollToLeftStick:Bool = false
private var gravityYAngleRef:Double?
private var previousGravityYAngle:Double?
private var gravityYUnwrappedAngle:Double = 0
private var yawPitchToRightStick:Bool = false
public var sensitvityYaw:CGFloat = 1.0
public var sensitvityPitch:CGFloat = 1.0
public var sensitvityRoll:CGFloat = 1.0
@objc public var widgetYawFactor:CGFloat = 1.0
@objc public var widgetPitchFactor:CGFloat = 1.0
@objc public var widgetRollFactor:CGFloat = 1.0
@objc public var previousWidgetYawFactor:CGFloat = 1.0
@objc public var previousWidgetPitchFactor:CGFloat = 1.0
@objc public var previousWidgetRollFactor:CGFloat = 1.0
public var motionStarter:Any?
private var windowScene: Any?
@objc public var onScreenControls: OnScreenControls?
public let stickMaxOffset: CGFloat = 0x7FFE
private var stickInputScale: CGFloat = 35
private var yaw:Double = 0
private var pitch:Double = 0
private var roll:Double = 0
private var leftStickMotion:Double = 0
private var isCalibrating: Bool = false
private var sumX: Double = 0
private var sumY: Double = 0
private var sumZ: Double = 0
@objc public var gyroBiasX: Double = 0
@objc public var gyroBiasY: Double = 0
@objc public var gyroBiasZ: Double = 0
@objc public var controllerGyroBiasX: Double = 0
@objc public var controllerGyroBiasY: Double = 0
@objc public var controllerGyroBiasZ: Double = 0
@objc public var gyroToStickMinOffset:Double = 0
private var yawBias:Double = 0
private var pitchBias:Double = 0
private var rollBias:Double = 0
private var rightStickTouchInputX:Double = 0
private var rightStickTouchInputY:Double = 0
private var leftStickTouchInputX:Double = 0
private var leftStickTouchInputY:Double = 0
private var rightStickPhysicalInputX:Double = 0
private var rightStickPhysicalInputY:Double = 0
public var gyroToStickOffset:CGVector = CGVector(dx: 0, dy: 0)
private var leftStickPhysicalInputX:Double = 0
private var leftStickPhysicalInputY:Double = 0
var updateInterval: TimeInterval = 1.0 / 120.0 {
didSet {
motionManager.accelerometerUpdateInterval = updateInterval
motionManager.gyroUpdateInterval = updateInterval
}
}
@objc public override init() {
// _ = OSCProfilesManager.sharedManager(CGRectZero)
super.init()
if #available(iOS 13.0, *) {
let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene
self.windowScene = scene
} else {
// Fallback on earlier versions
}
motionManager.accelerometerUpdateInterval = updateInterval
motionManager.gyroUpdateInterval = updateInterval
}
public func startMotionControlByOnScreenButton(_ sender: OnScreenWidgetView, yawFactor:CGFloat, pitchFactor:CGFloat, rollFactor:CGFloat){
self.previousWidgetYawFactor = self.widgetYawFactor
self.previousWidgetPitchFactor = self.widgetPitchFactor
self.previousWidgetRollFactor = self.widgetRollFactor
self.widgetYawFactor = yawFactor
self.widgetPitchFactor = pitchFactor
self.widgetRollFactor = rollFactor
if self.motionStarter == nil {
self.motionStarter = sender
if sender.motionControlButtonString != "GYROPAUSE" {self.startMotionUpdate()}
}
else if sender.motionControlButtonString == "GYROPAUSE" {
self.startMotionUpdate()
}
}
@objc public func startMotionControlByControllerButton(){
self.previousWidgetYawFactor = self.widgetYawFactor
self.previousWidgetPitchFactor = self.widgetPitchFactor
self.previousWidgetRollFactor = self.widgetRollFactor
self.widgetYawFactor = 1
self.widgetPitchFactor = 1
self.widgetRollFactor = 1
self.startMotionUpdate()
}
@objc public func startMotionUpdate() {
if !motionControlStarted {
motionControlStarted = true
resetGravityYOffsetTracking()
// print("startGyroUpdate useBuiltinGyro \(useBuiltinGyro) \(CACurrentMediaTime())")
if useBuiltinGyro {
if motionManager.isGyroAvailable {
if rollToLeftStick {
leftStickMotion = leftStickTouchInputX
}
motionManager.startDeviceMotionUpdates(to: .main) { [weak self] motionData, _ in
guard let self = self, let data = motionData else { return }
self.handleMotionData(deviceMotion: data)
}
}
}
else {
if #available(iOS 14.0, *) {
if activeGCController == nil {
if let controllers = ControllerUtil.activeStreamingGCControllers as? Set<GCController> {
for controller in controllers {
if controller.playerIndex == .index1 {
activeGCController = controller
break
}
}
}
}
if let motion = activeGCController?.motion {
if motion.sensorsRequireManualActivation {
motion.sensorsActive = true
}
if rollToLeftStick {
leftStickMotion = 0
}
if motion.sensorsActive {
motion.valueChangedHandler = { [weak self] motion in
self?.handleMotionData(gcMotion: motion)
}
}
}
}
}
}
else {return}
}
@objc public func startAccelUpdate() {
accelControlStarted = true
if motionManager.isAccelerometerAvailable {
motionManager.startAccelerometerUpdates(to: .main) { [weak self] accelData, _ in
guard let self = self, let data = accelData else { return }
self.handleAccelerometerData(x: data.acceleration.x,
y: data.acceleration.y,
z: data.acceleration.z)
}
}
}
/// 停止更新
// private var interruptNoneGyroInput:Bool = false
@objc public func stopMotionUpdate(interruptNoneGyroInput:Bool=false) {
motionControlStarted = false
motionIsWorking = false
resetGravityYOffsetTracking()
if motionManager.isDeviceMotionActive{
motionManager.stopDeviceMotionUpdates()
}
if #available(iOS 14.0, *) {
activeGCController?.motion?.sensorsActive = false
}
self.clearGyroInput(interruptNonGyroInput:interruptNoneGyroInput)
}
@objc public func stopAccelUpdate() {
accelControlStarted = false
if motionManager.isAccelerometerActive {
motionManager.stopAccelerometerUpdates()
}
// self.onScreenControls.clearLeftStickTouchPadFlag()
// self.onScreenControls.clearRightStickTouchPadFlag()
}
// MARK: - 私有方法处理数据
private func handleAccelerometerData(x: Double, y: Double, z: Double) {
// 在这里处理加速度数据,比如计算方向、存储或驱动逻辑
}
private func handleMotionData(deviceMotion: CMDeviceMotion? = nil, gcMotion:GCMotion? = nil) {
var x:Double = 0
var y:Double = 0
var z:Double = 0
// var attitudeRoll:Double = 0
var gravityYOffset:Double = 0
if let deviceMotion = deviceMotion {
x = deviceMotion.rotationRate.x
y = deviceMotion.rotationRate.y
z = deviceMotion.rotationRate.z
// attitudeRoll = deviceMotion.attitude.yaw
gravityYOffset = updateGravityYOffset(
gravityX: deviceMotion.gravity.x,
gravityY: deviceMotion.gravity.y,
)
}
if let gcMotion = gcMotion {
x = gcMotion.rotationRate.x
y = gcMotion.rotationRate.y
z = gcMotion.rotationRate.z
// attitudeRoll = gcMotion.attitude.z
}
var yawSource:Double = 0
var pitchSource:Double = 0
var rollSource:Double = 0
/*
let correctedX:Double = x - gyroBiasX
let correctedY:Double = y - gyroBiasY
let correctedZ:Double = z - gyroBiasZ */
if useBuiltinGyro {
if #available(iOS 13.0, *) {
let orientation = (windowScene as! UIWindowScene).interfaceOrientation
switch orientation {
case .landscapeLeft:
yawBias = gyroBiasX
pitchBias = -gyroBiasY
rollBias = -gyroBiasZ
yawSource = x-yawBias
pitchSource = -y-pitchBias
rollSource = -z-rollBias
case .landscapeRight:
yawBias = -gyroBiasX
pitchBias = gyroBiasY
rollBias = -gyroBiasZ
yawSource = -x-yawBias
pitchSource = y-pitchBias
rollSource = -z-rollBias
case .portrait:
yawBias = -gyroBiasY
pitchBias = -gyroBiasX
rollBias = -gyroBiasZ
yawSource = -y-yawBias
pitchSource = -x-pitchBias
rollSource = -z-rollBias
case .portraitUpsideDown:
yawBias = gyroBiasY
pitchBias = gyroBiasX
rollBias = -gyroBiasZ
yawSource = y-yawBias
pitchSource = x-pitchBias
rollSource = -z-rollBias
default:
yawBias = gyroBiasX
pitchBias = -gyroBiasY
rollBias = -gyroBiasZ
yawSource = x-yawBias
pitchSource = -y-pitchBias
rollSource = -z-rollBias
}
} else {
let orientation = UIApplication.shared.statusBarOrientation
if orientation.isLandscape {
yawBias = gyroBiasX
pitchBias = -gyroBiasY
rollBias = -gyroBiasZ
yawSource = x-yawBias
pitchSource = -y-pitchBias
rollSource = -z-rollBias
} else if orientation.isPortrait {
yawBias = -gyroBiasY
pitchBias = -gyroBiasX
rollBias = -gyroBiasZ
yawSource = -y-yawBias
pitchSource = -x-pitchBias
rollSource = -z-rollBias
}
}
}
else {
yawBias = swapYawAndRoll ? controllerGyroBiasY : -controllerGyroBiasZ
pitchBias = -controllerGyroBiasX
rollBias = swapYawAndRoll ? -controllerGyroBiasZ : controllerGyroBiasY
yawSource = (swapYawAndRoll ? y : -z) - yawBias
pitchSource = -x-pitchBias
rollSource = (swapYawAndRoll ? -z : y) - rollBias
}
if !motionControlStarted {
self.clearGyroInput(interruptNonGyroInput: false)
// print("Gyro: stopped")
return
}
motionIsWorking = true
if mapGyroTo == MapGyroTo.mapGyroToMouse {
yaw = yawSource*sensitvityYaw*widgetYawFactor*30
pitch = pitchSource*sensitvityPitch*widgetPitchFactor*30
LiSendMouseMoveEvent(Int16(yaw),Int16(pitch))
}
//guard let onScreenControls = onScreenControls else { return }
if mapGyroTo == MapGyroTo.mapGyroToControllerStick {
if yawPitchToRightStick {
gyroToStickOffset.dx = gyroInputToStickInput(input:yawSource*sensitvityYaw*widgetYawFactor*10)
yaw = rightStickTouchInputX + rightStickPhysicalInputX + gyroToStickOffset.dx
yaw = self.clampStickInput(input: yaw)
gyroToStickOffset.dy = -gyroInputToStickInput(input:pitchSource*sensitvityPitch*widgetPitchFactor*10)
pitch = rightStickTouchInputY + rightStickPhysicalInputY + gyroToStickOffset.dy
pitch = self.clampStickInput(input: pitch)
let offsetVector = ControllerUtil.compensated(offsetVector: CGVector(dx: yaw, dy: pitch), minOffset: gyroToStickMinOffset)
onScreenControls?.sendRightStickTouchPadEvent(offsetVector.dx, offsetVector.dy)
}
if rollToLeftStick {
roll = gyroInputToStickInput(input:rollSource*sensitvityRoll*widgetRollFactor*0.2)
leftStickMotion = self.clampStickInput(input: useBuiltinGyro ? -stickMaxOffset*(gravityYOffset/Double.pi)*3*sensitvityRoll*widgetRollFactor : leftStickMotion + roll)
let mixedLeftStickOffsetX = self.clampStickInput(input: leftStickMotion+leftStickTouchInputX+leftStickPhysicalInputX)
let mixedLeftStickOffsetY = self.clampStickInput(input: leftStickTouchInputY+leftStickPhysicalInputY)
let offsetVector = ControllerUtil.compensated(offsetVector: CGVector(dx: mixedLeftStickOffsetX, dy: mixedLeftStickOffsetY), minOffset: gyroToStickMinOffset, circulate: true)
onScreenControls?.sendLeftStickTouchPadEvent(offsetVector.dx, offsetVector.dy)
}
}
}
private func clearGyroInput(interruptNonGyroInput:Bool){
guard ControllerUtil.gamepadArrivalReported else { return }
if yawPitchToRightStick{
let clearX = rightStickPhysicalInputX+rightStickTouchInputX-yawBias
let clearY = rightStickPhysicalInputY+rightStickTouchInputY-pitchBias
let clearVector = ControllerUtil.circulated(offsetVector: CGVector(dx: clearX, dy: clearY))
onScreenControls?.sendRightStickTouchPadEvent(clearVector.dx, clearVector.dy)
}
if rollToLeftStick{
leftStickMotion = leftStickTouchInputX
let clearX = leftStickPhysicalInputX+leftStickTouchInputX-rollBias
let clearY = leftStickPhysicalInputY+leftStickTouchInputY
let clearVector = ControllerUtil.circulated(offsetVector: CGVector(dx: clearX, dy: clearY))
onScreenControls?.sendLeftStickTouchPadEvent(clearVector.dx,clearVector.dy)
}
if(interruptNonGyroInput){
onScreenControls?.clearLeftStickTouchPadFlag()
onScreenControls?.clearRightStickTouchPadFlag()
}
}
private func resetGravityYOffsetTracking() {
gravityYAngleRef = nil
previousGravityYAngle = nil
gravityYUnwrappedAngle = 0
}
private func updateGravityYOffset(gravityX: Double, gravityY: Double) -> Double {
let gravityYAngle = atan2(gravityX, -gravityY)
guard let previousGravityYAngle = previousGravityYAngle else {
gravityYAngleRef = gravityYAngle
previousGravityYAngle = gravityYAngle
gravityYUnwrappedAngle = gravityYAngle
return 0
}
var delta = gravityYAngle - previousGravityYAngle
if delta > Double.pi {
delta -= 2 * Double.pi
} else if delta < -Double.pi {
delta += 2 * Double.pi
}
gravityYUnwrappedAngle += delta
self.previousGravityYAngle = gravityYAngle
return -(gravityYUnwrappedAngle - (gravityYAngleRef ?? gravityYUnwrappedAngle))
}
private func clampStickInput(input: CGFloat) -> CGFloat{
return fmax(fmin(input, stickMaxOffset),-stickMaxOffset)
}
private func gyroInputToStickInput(input: CGFloat) -> CGFloat{
return self.clampStickInput(input: stickMaxOffset * input / 8)
}
@objc public func calibrateGyroBias(duration: TimeInterval = 5.0, completion: @escaping () -> Void) {
guard motionManager.isGyroAvailable else { return }
isCalibrating = true
var sumX = 0.0, sumY = 0.0, sumZ = 0.0
var sampleCount = 0
if useBuiltinGyro {
motionManager.startDeviceMotionUpdates(to: .main) { [weak self] motionData, _ in
guard let self = self, self.isCalibrating, let data = motionData else { return }
sumX += data.rotationRate.x
sumY += data.rotationRate.y
sumZ += data.rotationRate.z
sampleCount += 1
}
}
else if #available(iOS 14.0, *) {
if let motion = activeGCController?.motion {
if motion.sensorsRequireManualActivation {
motion.sensorsActive = true
}
if motion.sensorsActive {
motion.valueChangedHandler = { [weak self] motion in
guard let self = self, self.isCalibrating else { return }
sumX += motion.rotationRate.x
sumY += motion.rotationRate.y
sumZ += motion.rotationRate.z
sampleCount += 1
}
}
}
}
// 5秒后计算平均值
DispatchQueue.main.asyncAfter(deadline: .now() + duration) { [weak self] in
guard let self = self else { return }
self.motionManager.stopDeviceMotionUpdates()
if sampleCount > 0 {
if useBuiltinGyro {
gyroBiasX = sumX / Double(sampleCount)
gyroBiasY = sumY / Double(sampleCount)
gyroBiasZ = sumZ / Double(sampleCount)
}
else {
controllerGyroBiasX = sumX / Double(sampleCount)
controllerGyroBiasY = sumY / Double(sampleCount)
controllerGyroBiasZ = sumZ / Double(sampleCount)
}
}
self.isCalibrating = false
completion()
}
}
}
+3 -3
View File
@@ -14,11 +14,11 @@
NS_ASSUME_NONNULL_BEGIN
@interface NativeTouchHandler : UIResponder
@property (nonatomic, assign) NSMutableSet<UITouch *>* touchesCapturedByOnScreenButtons;
- (id)initWithView:(StreamView* )view andSettings:(TemporarySettings*)settings;
- (id)initWithView:(StreamView*)view settings:(TemporarySettings*)settings profile:(OSCProfile* )profile;
- (void)setAllowSingleTouchEnabled:(BOOL)enabled;
@end
+106 -61
View File
@@ -11,16 +11,15 @@
#import "NativeTouchPointer.h"
#import "OnScreenControls.h"
#import "StreamView.h"
#import "VoidLink-Swift.h"
#include <Limelight.h>
@implementation NativeTouchHandler {
StreamView* streamView;
TemporarySettings* currentSettings;
__weak StreamView* streamView;
bool activateCoordSelector;
bool singleTouchDisabled;
CGFloat pointerVelocityDividerLocationByPoints;
uint16_t touchMoveEventIntervalUs;
bool asyncNativeTouch;
unsigned int touchDownQos;
@@ -35,29 +34,37 @@
NSMutableSet<NSNumber *> *pointerIdPool; //pre-defined pool of pointerIds.
NSMutableSet<NSNumber *> *unassignedPointerIds;
NSMutableSet *blacklistedTouches;
bool trackPointEnabled;
NSMutableArray<CAShapeLayer *> * trackPointPool; //pre-defined pool of pointerIds.
NSMutableDictionary *pointerObjDict;
CGFloat slideGestureVerticalThreshold;
CGFloat screenWidthWithThreshold;
CGFloat EDGE_TOLERANCE;
CGFloat _edgeTolerance;
CGRect streamViewBounds;
int64_t moveEventIntervalNSec;
}
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings{
- (id)initWithView:(StreamView*)view settings:(TemporarySettings*)settings profile:(OSCProfile* )profile{
self = [super init];
self->streamView = view;
self->currentSettings = settings;
self->activateCoordSelector = currentSettings.pointerVelocityModeDivider.floatValue != 1.0;
self->touchMoveEventIntervalUs = currentSettings.touchMoveEventInterval.intValue;
self->activateCoordSelector = profile.pointerVelocityModeDivider != 1.0;
self->moveEventIntervalNSec = (int64_t)(settings.touchMoveEventInterval.intValue * 1000);;
self->streamViewBounds = view.bounds;
self->pointerIdDict = [NSMutableDictionary dictionary];
self->pointerIdPool = [NSMutableSet set];
self->trackPointEnabled = settings.touchPointTracking;
self->trackPointPool = [NSMutableArray array];
for (uint8_t i = 0; i <= 10; i++) { //ipadOS supports upto 11 finger touches
[self->pointerIdPool addObject:@(i)];
[self->trackPointPool addObject:[GraphicUtils makeTouchTrackpointIn:streamView]];
}
self->activePointerIds = [NSMutableSet set];
self->blacklistedTouches = [NSMutableSet set];
@@ -85,13 +92,13 @@
self->pointerObjDict = [NSMutableDictionary dictionary];
EDGE_TOLERANCE = 10.0;
_edgeTolerance = settings.edgeSlidingSensitivity.floatValue;
slideGestureVerticalThreshold = CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.4;
screenWidthWithThreshold = CGRectGetWidth([[UIScreen mainScreen] bounds]) - EDGE_TOLERANCE;
screenWidthWithThreshold = CGRectGetWidth([[UIScreen mainScreen] bounds]) - _edgeTolerance;
self->pointerVelocityDividerLocationByPoints = self->streamView.bounds.size.width * settings.pointerVelocityModeDivider.floatValue;
self->pointerVelocityDividerLocationByPoints = self->streamView.bounds.size.width * profile.pointerVelocityModeDivider;
[NativeTouchPointer initContextWithView:self->streamView andSettings:settings];
[NativeTouchPointer initContextWithView:self->streamView profile:profile];
//_touchesCapturedByOnScreenButtons = [[NSMutableSet alloc] init];
return self;
}
@@ -164,7 +171,7 @@
//check if touch point is spawned on the left or right upper half screen edges, event to remote PC. this is for better handling in-stream slide gesture
CGPoint initialPoint = [touch locationInView:self->streamView];
if(initialPoint.y < slideGestureVerticalThreshold && (initialPoint.x < EDGE_TOLERANCE || initialPoint.x > screenWidthWithThreshold)) {
if(initialPoint.y < slideGestureVerticalThreshold && (initialPoint.x < _edgeTolerance || initialPoint.x > screenWidthWithThreshold)) {
[blacklistedTouches addObject:touchAddrObj];
}
}
@@ -187,10 +194,20 @@
}
- (void)sendTouchEvent:(UITouch*)touch withTouchtype:(uint8_t)touchType{
- (void)sendTouchEvent:(UITouch*)touch withTouchtype:(uint8_t)touchType withEvent:(UIEvent *)event{
//if(touchPointSpawnedAtUpperScreenEdge && touchType != LI_TOUCH_EVENT_UP) return; // we're done here. this touch event will not be sent to the remote PC. and this must be checked after coord selector finishes populating new relative coords, or the app will crash
if(!touch) return;
if([blacklistedTouches containsObject:@((uintptr_t)touch)]) return;
if(singleTouchDisabled
&& [UITouchUtil touchesIn:streamView from:event].count != 2
&& touchType != LI_TOUCH_EVENT_UP) return;
if(PencilHandler.pencilPausesNativeTouch
&& PencilHandler.isDrawing
&& touchType != LI_TOUCH_EVENT_UP) return;
CGPoint targetCoords;
//NSLog(@"selecting coords: %d", touch.phase == UITouchPhaseMoved);
// NSLog(@"excluded count: %d", (uint32_t)[excludedPointerIds count]);
@@ -203,85 +220,95 @@
CGFloat normalizedY = location.y / videoSize.height;
uint8_t pointerId = [self retrievePointerIdFromDict:touch];
if([self getPointerObjFromDict:touch].needResetCoords){ // access whether the current pointer has reached the boundary, and need a coord reset.
NativeTouchPointer *pointer = [self getPointerObjFromDict:touch];
if(pointer != nil && pointer.needResetCoords){ // access whether the current pointer has reached the boundary, and need a coord reset.
LiSendTouchEvent(LI_TOUCH_EVENT_UP, pointerId, normalizedX, normalizedY, 0, 0, 0, 0); //event must sent from the lowest level directy by LiSendTouchEvent to simulate continous dragging to another point on screen
LiSendTouchEvent(LI_TOUCH_EVENT_DOWN, pointerId, 0.3, 0.4, 0, 0, 0, 0);
}else LiSendTouchEvent(touchType, pointerId, normalizedX, normalizedY,(touch.force / touch.maximumPossibleForce) / sin(touch.altitudeAngle),0.0f, 0.0f,[self getRotationFromAzimuthAngle:[touch azimuthAngleInView:streamView]]);
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
if (asyncNativeTouch) dispatch_async(dispatch_get_global_queue(touchDownQos, 0), ^{
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
for (UITouch* touch in touches){
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]) continue;
if([OnScreenControls.touchesCapturedByOnScreenControls containsObject:touch]) continue;
[self handleTouchDown:touch]; //generate & populate pointerId
if(self->activateCoordSelector) [self populatePointerObjIntoDict:touch];
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_DOWN];
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_DOWN withEvent:event];
}
if(self->trackPointEnabled){
dispatch_async(dispatch_get_main_queue(), ^{
[CATransaction begin];
[CATransaction setDisableActions:YES];
for (UITouch* touch in touches){
uint8_t pointerId = [self retrievePointerIdFromDict:touch];
CAShapeLayer* trackPoint = self->trackPointPool[pointerId];
trackPoint.position = [touch locationInView:self->streamView];
trackPoint.hidden = false;
}
[CATransaction commit];
});
}
});
else{
for (UITouch* touch in touches){
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]) continue;
[self handleTouchDown:touch]; //generate & populate pointerId
if(self->activateCoordSelector) [self populatePointerObjIntoDict:touch];
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_DOWN];
}
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
if (asyncNativeTouch) dispatch_async(dispatch_get_global_queue(touchMoveQos, 0), ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, moveEventIntervalNSec), dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
for (UITouch* touch in touches){
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]) continue;
if([OnScreenControls.touchesCapturedByOnScreenControls containsObject:touch]) continue;
if(self->activateCoordSelector) [self updatePointerObjInDict:touch];
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_MOVE];
[[self getPointerObjFromDict:touch] doesNeedResetCoords]; // execute the judging of doesReachBoundary for current pointer instance. (happens after the event is sent to Sunshine service)
usleep(self->touchMoveEventIntervalUs);
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_MOVE withEvent:event];
[[self getPointerObjFromDict:touch] doesNeedResetCoords];
}
});
else {
for (UITouch* touch in touches){
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]) continue;
if(self->activateCoordSelector) [self updatePointerObjInDict:touch];
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_MOVE];
[[self getPointerObjFromDict:touch] doesNeedResetCoords]; // execute the judging of doesReachBoundary for current pointer instance. (happens after the event is sent to Sunshine service)
// usleep(self->touchMoveEventIntervalUs);
}
if(self->trackPointEnabled){
dispatch_async(dispatch_get_main_queue(), ^{
[CATransaction begin];
[CATransaction setDisableActions:YES];
for (UITouch* touch in touches){
uint8_t pointerId = [self retrievePointerIdFromDict:touch];
CAShapeLayer* trackPoint = self->trackPointPool[pointerId];
trackPoint.position = [touch locationInView:self->streamView];
}
[CATransaction commit];
});
}
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
if(asyncNativeTouch) dispatch_async(dispatch_get_global_queue(touchEndQos, 0), ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, moveEventIntervalNSec), dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
for (UITouch* touch in touches){
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]) continue;
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_UP]; //send touch event before remove pointerId
[self removePointerId:touch]; //then remove pointerId
if([OnScreenControls.touchesCapturedByOnScreenControls containsObject:touch]){
[OnScreenControls.touchesCapturedByOnScreenControls removeObject:touch];
continue;
}
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_UP withEvent:event]; //send touch event before remove pointerId
if(self->trackPointEnabled){
dispatch_async(dispatch_get_main_queue(), ^{
uint8_t pointerId = [self retrievePointerIdFromDict:touch];
CAShapeLayer* trackPoint = self->trackPointPool[pointerId];
[CATransaction begin];
[CATransaction setDisableActions:YES];
trackPoint.hidden = true;
[CATransaction commit];
[self removePointerId:touch];
});
}
else [self removePointerId:touch];
if(self->activateCoordSelector) [self removePointerObjFromDict:touch];
[self->blacklistedTouches removeObject:@((uintptr_t)touch)];
}
//if(self->touchPointSpawnedAtUpperScreenEdge && [[event allTouches] count] == [touches count])
});
else{
for (UITouch* touch in touches){
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]) continue;
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_UP]; //send touch event before remove pointerId
[self removePointerId:touch]; //then remove pointerId
if(self->activateCoordSelector) [self removePointerObjFromDict:touch];
}
}
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
[self touchesEnded:touches withEvent:event];
}
- (void)populatePointerObjIntoDict:(UITouch*)touch{
NativeTouchPointer* pointer = [[NativeTouchPointer alloc] initWithTouch:touch];
pointer.useRelativeCoords = pointer.initialPoint.x > pointerVelocityDividerLocationByPoints;
@@ -310,4 +337,22 @@
return pointer.useRelativeCoords ? pointer.latestRelativePoint : pointer.latestPoint;
}
- (void)setAllowSingleTouchEnabled:(BOOL)enabled{
singleTouchDisabled = !enabled;
}
- (void)dealloc{
NSLog(@"dealloc nativeTouchHanlder %f", CACurrentMediaTime());
pointerIdDict = nil;
activePointerIds = nil;
pointerIdPool= nil;
unassignedPointerIds = nil;
blacklistedTouches = nil;
for(CAShapeLayer* layer in trackPointPool){
[layer removeFromSuperlayer];
}
trackPointPool = nil;
pointerObjDict = nil;
}
@end
+2 -1
View File
@@ -21,7 +21,8 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, assign) bool useRelativeCoords;
+ (void)initContextWithView:(StreamView*)view andSettings:(TemporarySettings*)settings;
+ (void)initContextWithView:(StreamView*)view profile:(OSCProfile*)profile;
+ (void)cleanUpContext;
- (bool)doesNeedResetCoords;
- (void)updatePointerCoords:(UITouch *)touch;
+6 -2
View File
@@ -53,14 +53,18 @@ static StreamView* streamView;
_latestRelativePoint.y = _previousRelativePoint.y + pointerVelocityFactor * (_latestPoint.y - _previousPoint.y);
}
+ (void)initContextWithView:(StreamView*)view andSettings:(TemporarySettings*)settings {
+ (void)initContextWithView:(StreamView*)view profile:(OSCProfile*)profile {
streamView = view;
streamViewWidth = streamView.frame.size.width;
fixedResetCoordX = streamViewWidth * 0.3;
streamViewHeight = streamView.frame.size.height;
fixedResetCoordY = streamViewHeight * 0.4;
pointerVelocityFactor = settings.touchPointerVelocityFactor.floatValue;
pointerVelocityFactor = profile.touchPointerVelocityFactor;
NSLog(@"stream wdith %f, stream height %f", streamViewWidth, streamViewHeight);
}
+ (void)cleanUpContext {
streamView = nil;
}
@end
+19 -25
View File
@@ -17,12 +17,7 @@
@class ControllerSupport;
@class StreamConfiguration;
// sending self as an instance to OnScreenWidgetView or other related classes
@protocol InstanceProviderDelegate <NSObject>
- (void)getOnScreenControlsInstance:(id)sender; // Renamed delegate method
@end
@class LayoutOnScreenControls;
static const float D_PAD_DIST = 10;
static const float BUTTON_DIST = 20;
@@ -36,13 +31,13 @@ static const float BUTTON_DIST = 20;
CGFloat _dPadSizeFactor;
}
+ (NSMutableDictionary *)layerVibrationStyleDic;
+ (NSMutableDictionary *_Nullable)layerVibrationStyleDic;
typedef NS_ENUM(NSInteger, OnScreenControlsLevel) {
OnScreenControlsLevelOff,
OnScreenControlsLevelCustom,
OnScreenControlsLevelSimple,
OnScreenControlsLevelFull,
OnScreenControlsLevelCustom,
OnScreenControlsLevelAuto, // move it here instead of delete , to ensure integrity of the codes
// Internal levels selected by ControllerSupport
@@ -52,11 +47,6 @@ typedef NS_ENUM(NSInteger, OnScreenControlsLevel) {
};
@property (nonatomic, weak) id<InstanceProviderDelegate> delegate; // Delegate property
- (void)sendInstance;
// @property (nonatomic, assign) CustomTapGestureRecognizer* mouseRightClickTapRecognizer; // this object will be passed to onscreencontrols class for areVirtualControllerTaps flag setting
@property (nonatomic, assign) bool isLayingOut;
//@property (nonatomic) NSMutableSet<UITouch* >* touchesCapturedByOnScreenButtons;
@@ -105,12 +95,15 @@ typedef NS_ENUM(NSInteger, OnScreenControlsLevel) {
@property OnScreenControlsLevel _level;
@property NSMutableArray *OSCButtonLayers;
@property NSMutableSet *OSCButtonLayerPool;
@property NSMutableDictionary* layerVibrationStyleDic;
+ (NSMutableSet* )touchAddrsCapturedByOnScreenControls;
+ (NSMutableSet* )touchesCapturedByOnScreenControls;
+ (OnScreenControls* )shared;
+ (void)setShared:(OnScreenControls*)instance;
- (void) sendRightStickTouchPadEvent:(CGFloat) stickX : (CGFloat) stickY;
- (void) clearRightStickTouchPadFlag;
- (void) sendLeftStickTouchPadEvent:(CGFloat) stickX : (CGFloat) stickY;
@@ -125,18 +118,19 @@ typedef NS_ENUM(NSInteger, OnScreenControlsLevel) {
- (BOOL) handleTouchUpEvent:(NSSet*)touches;
- (BOOL) handleTouchMovedEvent:(NSSet*)touches;
- (void) setLevel:(OnScreenControlsLevel)level;
- (void) show;
- (void) showLegacyWidgetsWith:(OSCProfile* )profile;
- (void) regenLayerPool;
- (void) setupComplexControls;
- (void) drawButtons;
- (void) drawButtons:(OSCProfile* _Nullable)profile;
- (void) drawBumpers;
- (void) updateControls;
- (void) updateLegacyWidgetsWith:(OSCProfile* _Nullable )profile;
- (OnScreenControlsLevel) getLevel;
- (void) setDPadCenter;
- (void) setAnalogStickPositions;
- (void) positionAndResizeSingleControllerLayers;
- (void) resizeControllerLayerWith:(CALayer*)layer and:(CGFloat)sizeFactor;
- (void) adjustControllerLayerOpacityWith:(CALayer*)layer and:(CGFloat)alpha;
+ (CGFloat) getControllerLayerSizeFactor:(CALayer*)layer;
- (CGFloat) getControllerLayerOpacity:(CALayer*)layer;
- (void) setDPadCenter:(OSCProfile *_Nonnull)profile;;
- (void) setAnalogStickPositions:(OSCProfile *_Nonnull)profile;
- (void) positionAndResizeSingleControllerLayers:(OSCProfile *_Nonnull)profile;
- (void) resizeControllerLayerWith:(CALayer*_Nonnull)layer and:(CGFloat)sizeFactor;
- (void) adjustControllerLayerOpacityWith:(CALayer*_Nonnull)layer and:(CGFloat)alpha;
+ (CGFloat) getControllerLayerSizeFactor:(CALayer*_Nonnull)layer;
- (CGFloat) getControllerLayerOpacity:(CALayer*_Nonnull)layer;
@end
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+731
View File
@@ -0,0 +1,731 @@
//
// PencilHandler.swift
// VoidLink
//
// Created by True砖家 on 2025/11/17.
// Copyright © 2025 True砖家 on Bilibili. All rights reserved.
//
import UIKit
@objc class PencilHandler: UIResponder, UIPencilInteractionDelegate {
@objc static var shared: PencilHandler?
/// 用来处理所有 touch 事件的宿主视图
weak var streamView: UIView?
// private var tickTimer: SafeTimer
private var pencilInteraction: Any?
@objc var streamAspectRatio: Float
private var tickInterval: TimeInterval
private var manualTick: Bool
private var pencilTickEnabled: Bool
private var pressureCurveEnabled: Bool = false
@objc private(set) var pencilTipOffset: CGPoint = .zero
private var manualHoverFlag: Bool = false
@objc static var hoverSupported: Bool = false
// @objc static private(set) var autoHoverTermination: Bool = false
@objc static private(set) var pencilAndHoverMode: PencilAndHoverMode = .pencilOnly
private(set) var pencilProEnabled: Bool = false
private var isFirstMove: Bool = false
private var strokeSampleIndex: Int32 = 0
private var initialMoveEventIndexLimit: Int64
private var touchBeganForce: Float = 0
@objc static private(set) var isDrawing: Bool = false
@objc static private(set) var pencilPausesNativeTouch: Bool = false
@objc var disableTilt: Bool = false
// static private let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero)
static private var selectedProfile:OSCProfile?
private var strokeLUT = PressureCurveLUT(curve: PressureCurve())
// private var initialTouchLUT = PressureCurveLUT(curve: PressureCurve())
private var phase1StrokeSampleIndexEnd:Int32 = 0
private var phase2StrokeSampleIndexEnd:Int32 = 0
private var phase2EqualizationStrength:Float = 0
enum StrokePhase: UInt8, CaseIterable {
case phase1
case phase2
case phase3
}
private func getStrokePhase(sampleIndex:Int32) -> StrokePhase {
if sampleIndex > phase2StrokeSampleIndexEnd {
return .phase3
}
if sampleIndex > phase1StrokeSampleIndexEnd {
return .phase2
}
return .phase1
}
@objc init(streamView: UIView, settings: TemporarySettings) {
self.streamView = streamView
streamAspectRatio = settings.width.floatValue/settings.height.floatValue
tickInterval = TimeInterval(settings.pencilTickIntervalUs.floatValue/1000000)
manualTick = settings.pencilTickMode.intValue == PencilTickMode.ManualTick.rawValue
pencilTickEnabled = settings.pencilTickMode.intValue != PencilTickMode.PencilTickDisabled.rawValue
pencilTipOffset = CGPoint(x: CGFloat(settings.pencilTipOffsetX?.floatValue ?? 0),
y: CGFloat(settings.pencilTipOffsetY?.floatValue ?? 0))
// initialMoveEventIndexLimit = UIScreen.main.maximumFramesPerSecond > 60 ? 4 : 2
initialMoveEventIndexLimit = 0
super.init()
setupPressureLUT()
PencilHandler.shared = self
}
@objc public func setupPressureLUT(profile:OSCProfile? = nil){
var selectedProfile:OSCProfile
if profile == nil {
let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero)
selectedProfile = oscProfileMan.getSelectedProfile()
}
else{
selectedProfile = profile!
}
doubleTapShorcuts.removeAll()
if selectedProfile.doubleTapShorcutEnabled {
if selectedProfile.eraserShortcut != "" {
doubleTapShorcuts.append(selectedProfile.eraserShortcut)
}
if selectedProfile.brushShortcut != "" {
doubleTapShorcuts.append(selectedProfile.brushShortcut)
}
}
PencilHandler.squeezeStartShortcut = ""
PencilHandler.squeezeEndShortcut = ""
if selectedProfile.squeezeShorcutEnabled {
PencilHandler.squeezeStartShortcut = selectedProfile.squeezeStartShortcut
PencilHandler.squeezeEndShortcut = selectedProfile.squeezeEndShortcut
print("squeezeShorcutEnabled \(CACurrentMediaTime())")
}
// PencilHandler.autoHoverTermination = selectedProfile.autoPencilHoverTermination
PencilHandler.pencilAndHoverMode = selectedProfile.pencilAndHoverMode
pressureCurveEnabled = selectedProfile.pressureCurveEnabled
if #available(iOS 15.0, *) {
IAPManager.checkPurchaseInfo(.PencilProPack) { info in
self.pressureCurveEnabled = self.pressureCurveEnabled && info.valid
self.pencilTickEnabled = self.pencilTickEnabled && info.valid
self.pencilTipOffset = info.valid ? self.pencilTipOffset : .zero
self.pencilProEnabled = info.valid
PencilHandler.pencilPausesNativeTouch = selectedProfile.pencilPausesNativeTouch && info.valid
if info.valid {
self.setupPencilInteraction(view: self.streamView)
}
}
}
let strokePressureCurvePoints = PressureCurve.importCurvePoints(selectedProfile.pressureCurvePoints)
let strokePressureCurve = PressureCurve()
strokePressureCurve.polylinePoints = strokePressureCurvePoints
strokePressureCurve.buildCurveSegments()
// let initialTouchPressureCurvePoints = PressureCurve.importCurvePoints(selectedProfile.initialTouchPressureCurvePoints)
// let initialTouchPressureCurve = PressureCurve()
// initialTouchPressureCurve.polylinePoints = initialTouchPressureCurvePoints
// initialTouchPressureCurve.buildCurveSegments()
phase1StrokeSampleIndexEnd = selectedProfile.phase1StrokeSampleIndexEnd
phase2StrokeSampleIndexEnd = selectedProfile.phase2StrokeSampleIndexEnd
phase2EqualizationStrength = Float(selectedProfile.strokeEqualizationStrength)
DispatchQueue.main.asyncAfter(deadline: .now()) {
self.strokeLUT = PressureCurveLUT(curve: strokePressureCurve)
// self.initialTouchLUT = PressureCurveLUT(curve: initialTouchPressureCurve)
}
}
// MARK: - Touch Events
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let event = event else { return }
previousForce = 0
previousTargetForce = 0
PencilHandler.isDrawing = true
// isFirstMove = true
strokeSampleIndex = 0
guard PencilHandler.pencilAndHoverMode == .pencilOnly || PencilHandler.pencilAndHoverMode == .hoverDisabled else {
handleNonPencilModes(touches)
return
}
for touch in touches {
let coalesced = event.coalescedTouches(for: touch) ?? []
_ = self.sendStylusEvent(touchBatch: pencilTickEnabled ? coalesced : [touch])
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let event = event else { return }
guard PencilHandler.pencilAndHoverMode == .pencilOnly || PencilHandler.pencilAndHoverMode == .hoverDisabled else {
handleNonPencilModes(touches)
return
}
for touch in touches {
let coalesced = event.coalescedTouches(for: touch) ?? []
_ = self.sendStylusEvent(touchBatch: pencilTickEnabled ? coalesced : [touch])
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let event = event else { return }
guard PencilHandler.pencilAndHoverMode == .pencilOnly || PencilHandler.pencilAndHoverMode == .hoverDisabled else {
handleNonPencilModes(touches)
return
}
for touch in touches {
let coalesced = event.coalescedTouches(for: touch) ?? []
_ = self.sendStylusEvent(touchBatch: pencilTickEnabled ? coalesced : [touch])
}
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
self.touchesEnded(touches, with: event)
}
private func handleNonPencilModes(_ touches: Set<UITouch>) {
guard let touch = touches.first else { return }
let location = touch.location(in: streamView)
let normalizedLocation = self.getNormalizedLocation(point: location)
if let streamView = streamView as? StreamView {
switch touch.phase {
case .began where PencilHandler.pencilAndHoverMode == .pencilToMouse:
streamView.updateCursorLocation(location, isMouse: false)
LiSendMouseButtonEvent(CChar(BUTTON_ACTION_PRESS), BUTTON_LEFT)
case .began where PencilHandler.pencilAndHoverMode == .pencilToTouch:
LiSendTouchEvent(UInt8(LI_TOUCH_EVENT_DOWN), 666666, Float(normalizedLocation.x), Float(normalizedLocation.y), Float((touch.force/touch.maximumPossibleForce)/sin(touch.altitudeAngle)), 0, 0, getRotation(fromAzimuthAngle: Float(touch.azimuthAngle(in: streamView))))
case .moved where PencilHandler.pencilAndHoverMode == .pencilToMouse:
streamView.updateCursorLocation(location, isMouse: false)
case .moved where PencilHandler.pencilAndHoverMode == .pencilToTouch:
LiSendTouchEvent(UInt8(LI_TOUCH_EVENT_MOVE), 666666, Float(normalizedLocation.x), Float(normalizedLocation.y), Float((touch.force/touch.maximumPossibleForce)/sin(touch.altitudeAngle)), 0, 0, getRotation(fromAzimuthAngle: Float(touch.azimuthAngle(in: streamView))))
case .ended where PencilHandler.pencilAndHoverMode == .pencilToMouse,
.cancelled where PencilHandler.pencilAndHoverMode == .pencilToMouse:
streamView.updateCursorLocation(location, isMouse: false)
LiSendMouseButtonEvent(CChar(BUTTON_ACTION_RELEASE), BUTTON_LEFT)
case .ended where PencilHandler.pencilAndHoverMode == .pencilToTouch,
.cancelled where PencilHandler.pencilAndHoverMode == .pencilToTouch:
LiSendTouchEvent(UInt8(LI_TOUCH_EVENT_UP), 666666, Float(normalizedLocation.x), Float(normalizedLocation.y), 0, 0, 0, 0)
default:
break
}
}
}
// MARK: - Core Logic
func getVideoAreaSize()-> CGSize {
guard let streamView = streamView else { return CGSize(width: 0, height: 0) }
if streamView.bounds.size.width > streamView.bounds.size.height * CGFloat(streamAspectRatio) {
return CGSizeMake(streamView.bounds.size.height * CGFloat(streamAspectRatio), streamView.bounds.size.height);
} else {
return CGSizeMake(streamView.bounds.size.width, streamView.bounds.size.width / CGFloat(streamAspectRatio));
}
}
func adjustCoordinatesForVideoArea(point: CGPoint) -> CGPoint {
guard let streamView = streamView else { return CGPoint(x: 0, y: 0) }
var x = point.x - streamView.bounds.origin.x
var y = point.y - streamView.bounds.origin.y
if x < streamView.bounds.width / 2 {
x -= 1
} else {
x += 1
}
if y < streamView.bounds.height / 2 {
y -= 1
} else {
y += 1
}
let videoSize = getVideoAreaSize() // 返回 CGSize
let videoOrigin = CGPoint(
x: streamView.bounds.width / 2 - videoSize.width / 2,
y: streamView.bounds.height / 2 - videoSize.height / 2
)
let confinedX = min(max(x, videoOrigin.x), videoOrigin.x + videoSize.width) - videoOrigin.x
let confinedY = min(max(y, videoOrigin.y), videoOrigin.y + videoSize.height) - videoOrigin.y
return CGPoint(x: confinedX, y: confinedY)
}
func getRotation(fromAzimuthAngle azimuthAngle: Float) -> UInt16 {
var rotationAngle = (azimuthAngle - .pi / 2) * (180.0 / .pi) // 弧度转角度
if rotationAngle < 0 {
rotationAngle += 360
}
return UInt16(rotationAngle)
}
func getTilt(fromAltitudeAngle altitudeAngle: Float) -> UInt8 {
let altitudeDegs = abs(Int16(altitudeAngle * (180.0 / .pi)))
let tilt = disableTilt ? 0 : UInt8(90 - min(90, Int(altitudeDegs)))
return tilt
}
private func getNormalizedLocation(point: CGPoint) -> CGPoint {
let location = self.adjustCoordinatesForVideoArea(point: point)
let videoSize = self.getVideoAreaSize()
let normalizedLocation = CGPoint(x: location.x/videoSize.width, y: location.y/videoSize.height)
return normalizedLocation
}
var previousForce: Float = 0
var previousTargetForce: Float = 0
var phase2IndexCount: Int32 = 0
private func sendStylusEvent(touchBatch: [UITouch], with event: UIEvent? = nil) -> DispatchTime {
guard let streamView = streamView, touchBatch.count>0 else { return .now() }
var tickMoment: TimeInterval = 0
var previousTimeStamp: TimeInterval = 0
var delay:TimeInterval = 0
var dispatchMoment: DispatchTime = .now()
phase2IndexCount = phase2StrokeSampleIndexEnd - phase1StrokeSampleIndexEnd
for touch in touchBatch {
let point = touch.preciseLocation(in: streamView)
.applying(CGAffineTransform(translationX: pencilTipOffset.x, y: pencilTipOffset.y))
let azimuth = touch.azimuthAngle(in: streamView)
let altitude = touch.altitudeAngle
let normalizedLocation = self.getNormalizedLocation(point: point)
var force = Float(touch.force/touch.maximumPossibleForce)/sin(Float(altitude))
force = (self.pencilTickEnabled && force == 0) ? previousForce : force
self.strokeSampleIndex += 1
var targetForce:Float
var equalizationStep:Float = 0
var equalizedForce:Float = 0
if(phase2IndexCount>0){
equalizationStep = (phase2EqualizationStrength-1)/Float(phase2IndexCount-1)
}
let strokePhase = getStrokePhase(sampleIndex: self.strokeSampleIndex)
let forceMapping = pressureCurveEnabled ? self.strokeLUT.value(at: force) : force
switch strokePhase {
case .phase1:
targetForce = pencilTickEnabled ? 0 : forceMapping
case .phase2:
targetForce = forceMapping
targetForce = max(targetForce,previousTargetForce)
equalizedForce = targetForce*(phase2EqualizationStrength - equalizationStep*Float(strokeSampleIndex-phase1StrokeSampleIndexEnd-1))
case .phase3:
targetForce = forceMapping
}
targetForce = pencilTickEnabled ? targetForce : force
previousForce = force
previousTargetForce = targetForce
let eventType:UInt8
switch touch.phase {
case .began:
eventType = UInt8(manualHoverFlag ? LI_TOUCH_EVENT_HOVER : LI_TOUCH_EVENT_DOWN)
case .moved:
eventType = UInt8(manualHoverFlag ? LI_TOUCH_EVENT_HOVER : LI_TOUCH_EVENT_MOVE)
case .ended:
eventType = UInt8(manualHoverFlag ? LI_TOUCH_EVENT_HOVER_LEAVE : LI_TOUCH_EVENT_UP)
targetForce = 0
previousForce = 0
previousTargetForce = 0
case .cancelled:
eventType = UInt8(manualHoverFlag ? LI_TOUCH_EVENT_HOVER_LEAVE : LI_TOUCH_EVENT_UP)
targetForce = 0
previousForce = 0
previousTargetForce = 0
default:
eventType = UInt8(LI_TOUCH_EVENT_HOVER)
}
delay = manualTick ? 0.0086 : 0.0086
delay = eventType == UInt8(LI_TOUCH_EVENT_UP) ? delay : 0
if previousTimeStamp == 0 {
previousTimeStamp = touch.timestamp
tickMoment = 0
dispatchMoment = .now()
}
else {
tickMoment += (manualTick ? tickInterval : touch.timestamp - previousTimeStamp)
}
let sendableForce = strokePhase == .phase2 ? equalizedForce : targetForce
DispatchQueue.global().asyncAfter(deadline: dispatchMoment + tickMoment + delay) {
/*
if PencilHandler.autoHoverEnabled, eventType == UInt8(LI_TOUCH_EVENT_DOWN) {
LiSendPenEvent(UInt8(LI_TOUCH_EVENT_HOVER_LEAVE), UInt8(LI_TOOL_TYPE_PEN), 0, Float(normalizedLocation.x), Float(normalizedLocation.y), 0, 0, 0, self.getRotation(fromAzimuthAngle: Float(azimuth)), self.getTilt(fromAltitudeAngle: Float(altitude)))
}
*/
if strokePhase != .phase1 || !self.pencilTickEnabled {LiSendPenEvent(eventType, UInt8(LI_TOOL_TYPE_PEN), 0, Float(normalizedLocation.x), Float(normalizedLocation.y), sendableForce, 0, 0, self.getRotation(fromAzimuthAngle: Float(azimuth)), self.getTilt(fromAltitudeAngle: Float(altitude)))}
else {
LiSendPenEvent(UInt8(LI_TOUCH_EVENT_HOVER), UInt8(LI_TOOL_TYPE_PEN), 0, Float(normalizedLocation.x), Float(normalizedLocation.y), 0, 0, 0, self.getRotation(fromAzimuthAngle: Float(azimuth)), self.getTilt(fromAltitudeAngle: Float(altitude)))
}
if eventType == UInt8(LI_TOUCH_EVENT_UP) {
PencilHandler.isDrawing = false
if PencilHandler.pencilAndHoverMode == .hoverDisabled || !PencilHandler.hoverSupported {
LiSendPenEvent(UInt8(LI_TOUCH_EVENT_HOVER), UInt8(LI_TOOL_TYPE_PEN), 0, Float(normalizedLocation.x), Float(normalizedLocation.y), 0, 0, 0, self.getRotation(fromAzimuthAngle: Float(azimuth)), self.getTilt(fromAltitudeAngle: Float(altitude)))
DispatchQueue.global().asyncAfter(deadline: .now() + 0.0086){
if !PencilHandler.isDrawing {
LiSendPenEvent(UInt8(LI_TOUCH_EVENT_HOVER_LEAVE), UInt8(LI_TOOL_TYPE_PEN), 0, Float(normalizedLocation.x), Float(normalizedLocation.y), 0, 0, 0, self.getRotation(fromAzimuthAngle: Float(azimuth)), self.getTilt(fromAltitudeAngle: Float(altitude)))
}
}
}
}
}
}
return dispatchMoment + tickMoment + delay
}
@objc public func switchPencilHover(){
manualHoverFlag = !manualHoverFlag
}
@objc public func updatePencilTipOffset(x: CGFloat, y: CGFloat) {
pencilTipOffset = CGPoint(x: x, y: y)
}
@objc public func enablePencilHover(){
manualHoverFlag = true
}
@objc public func disablePencilHover(){
manualHoverFlag = false
}
@available(iOS 12.1, *)
private func setupPencilInteraction(view:UIView?) {
let interaction = UIPencilInteraction()
guard let view else { return }
if pencilInteraction != nil {return}
interaction.delegate = self
view.addInteraction(interaction)
pencilInteraction = interaction
}
@available(iOS 12.1, *)
func pencilInteractionDidTap(_ interaction: UIPencilInteraction) {
if !pencilProEnabled {return}
if doubleTapShorcuts.isEmpty {return}
let keyStrings = CommandManager.shared.extractAutoReleaseButtonStrings(from: doubleTapShorcuts[shortcutIndex])
print("pencilInteractionDidTap \(CACurrentMediaTime())")
CommandManager.shared.sendAutoReleaseComboCommand(cmdStrings: keyStrings, delay: 0.1)
shortcutIndex = (shortcutIndex + 1) % doubleTapShorcuts.count
}
@available(iOS 17.5, *)
func pencilInteraction(
_ interaction: UIPencilInteraction,
didReceiveSqueeze squeeze: UIPencilInteraction.Squeeze
) {
if !pencilProEnabled {return}
if PencilHandler.squeezeStartShortcut == "", PencilHandler.squeezeEndShortcut == "" {return}
let keepPressedUntilRelease = PencilHandler.squeezeEndShortcut.uppercased() == "NULL"
let squeezePressKeyStrings = CommandManager.shared.extractAutoReleaseButtonStrings(from: PencilHandler.squeezeStartShortcut)
let squeezeReleaseKeyStrings = CommandManager.shared.extractAutoReleaseButtonStrings(from: PencilHandler.squeezeEndShortcut)
guard let squeezePressKeyStrings = squeezePressKeyStrings else {return}
guard squeezePressKeyStrings.count>0 else {return}
switch squeeze.phase {
case .began:
if keepPressedUntilRelease {CommandManager.shared.sendAutoReleaseComboCommand(cmdStrings: squeezePressKeyStrings, delay: 0.1, pressOnly: true)}
else {CommandManager.shared.sendAutoReleaseComboCommand(cmdStrings: squeezePressKeyStrings, delay: 0.1)}
break
case .ended, .cancelled:
if keepPressedUntilRelease {CommandManager.shared.sendAutoReleaseComboCommand(cmdStrings: squeezePressKeyStrings, releaseOnly: true)}
else {CommandManager.shared.sendAutoReleaseComboCommand(cmdStrings: squeezeReleaseKeyStrings, delay: 0.1)}
break
default:
break
}
}
@objc static private(set) var eraserShortcut:String = ""
@objc static private(set) var brushShortcut:String = ""
private var doubleTapShorcuts: Array<String> = []
private var shortcutIndex: Int = 0
@objc func replaceBrush(with shortcut:String){
if let i = doubleTapShorcuts.indices.last {
doubleTapShorcuts[i] = shortcut
}
}
@objc func replaceEraser(with shortcut:String){
if let i = doubleTapShorcuts.indices.first {
doubleTapShorcuts[i] = shortcut
}
}
@available(iOS 13.0, *)
func widgetPickerViewController(_ controller: WidgetPickerViewController, didCreateWidget payload: NSDictionary) {
let params = payload.mutableCopy() as? NSMutableDictionary ?? NSMutableDictionary()
let pickerAction = ((params["pickerAction"] as? String) ?? "").lowercased()
params.removeObject(forKey: "pickerAction")
if pickerAction == "create" {
}
}
@objc static public func enterDoubleTapShortcuts(in viewController: UIViewController){
if #available(iOS 13.0, *) {
let pickerViewController = WidgetPickerViewController()
pickerViewController.delegate = (viewController as! any WidgetPickerViewControllerDelegate)
pickerViewController.keyboardPickerMode = .shortcutPicker
pickerViewController.tabIdentifiers = ["keyboard", "shortcuts"]
pickerViewController.initialTabIdentifier = "keyboard"
pickerViewController.shortcutIdentifier = "eraser"
pickerViewController.shortcutPickerTipText = LocalizationHelper.localizedString(forKey: "Select eraser shortcut keys")
pickerViewController.presentOverFullScreen(from: viewController)
return
}
/*
let alert = UIAlertController(title: LocalizationHelper.localizedString(forKey: "Eraser Shortcut"),
message: LocalizationHelper.localizedString(forKey: "Enter eraser keyboard shortcut:"),
preferredStyle: .alert)
alert.addTextField { textField in
textField.placeholder = LocalizationHelper.localizedString(forKey:"Example: e, ctrl+e, alt+e ...")
textField.keyboardType = .asciiCapable
textField.autocorrectionType = .no
textField.spellCheckingType = .no
textField.text = selectedProfile.eraserShortcut
}
let okAction = UIAlertAction(title: LocalizationHelper.localizedString(forKey: "OK"), style: .default) { _ in
let comboButtons = alert.textFields?[0].text ?? ""
let keyStrings = CommandManager.shared.extractAutoReleaseButtonStrings(from: comboButtons)
if keyStrings?.count ?? 0 > 0 || comboButtons == "" {
eraserShortcut = comboButtons
}
enterBrushShortcut(in: viewController)
}
let learnMoreAction = UIAlertAction(title: LocalizationHelper.localizedString(forKey: "Learn More"), style: .default) { _ in
if let url = URL(string: LocalizationHelper.localizedString(forKey: "pencilKeyboardCmdURL")) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
alert.addAction(learnMoreAction)
alert.addAction(okAction)
viewController.present(alert, animated: true, completion: {
})
*/
}
@available(iOS 13.0, *)
@objc static public func enterBrushShortcut(in viewController: UIViewController){
let pickerViewController = WidgetPickerViewController()
pickerViewController.delegate = (viewController as! any WidgetPickerViewControllerDelegate)
pickerViewController.keyboardPickerMode = .shortcutPicker
pickerViewController.tabIdentifiers = ["keyboard", "shortcuts"]
pickerViewController.initialTabIdentifier = "keyboard"
pickerViewController.shortcutIdentifier = "brush"
pickerViewController.shortcutPickerTipText = LocalizationHelper.localizedString(forKey: "Select brush shortcut keys")
pickerViewController.presentOverFullScreen(from: viewController)
return
/*
let alert = UIAlertController(title: LocalizationHelper.localizedString(forKey: "Brush Shortcut"),
message: LocalizationHelper.localizedString(forKey: "Enter brush keyboard shortcut:"),
preferredStyle: .alert)
alert.addTextField { textField in
textField.placeholder = LocalizationHelper.localizedString(forKey:"Example: b, ctrl+b, alt+b ...")
textField.keyboardType = .asciiCapable
textField.autocorrectionType = .no
textField.spellCheckingType = .no
textField.text = selectedProfile?.brushShortcut
}
let okAction = UIAlertAction(title: LocalizationHelper.localizedString(forKey: "OK"), style: .default) { _ in
let comboButtons = alert.textFields?[0].text ?? ""
let keyStrings = CommandManager.shared.extractAutoReleaseButtonStrings(from: comboButtons)
if keyStrings?.count ?? 0 > 0 || comboButtons == "" {
brushShortcut = comboButtons
}
if selectedProfile?.brushShortcut != brushShortcut
|| selectedProfile?.eraserShortcut != eraserShortcut {
guard let selectedProfile = selectedProfile else {return}
let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero)
selectedProfile.brushShortcut = brushShortcut
selectedProfile.eraserShortcut = eraserShortcut
oscProfileMan.replaceSelectedProfile(with: selectedProfile, overwriteDefault: true)
}
}
let learnMoreAction = UIAlertAction(title: LocalizationHelper.localizedString(forKey: "Learn More"), style: .default) { _ in
if let url = URL(string: LocalizationHelper.localizedString(forKey: "pencilKeyboardCmdURL")) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
alert.addAction(learnMoreAction)
alert.addAction(okAction)
viewController.present(alert, animated: true, completion: {
})
*/
}
@objc static private(set) var squeezeStartShortcut:String = ""
@objc static private(set) var squeezeEndShortcut:String = ""
@objc static public func enterSqueezeShortcuts(in viewController: UIViewController){
if #available(iOS 13.0, *) {
let pickerViewController = WidgetPickerViewController()
pickerViewController.delegate = (viewController as! any WidgetPickerViewControllerDelegate)
pickerViewController.keyboardPickerMode = .shortcutPicker
pickerViewController.tabIdentifiers = ["keyboard", "shortcuts"]
pickerViewController.initialTabIdentifier = "keyboard"
pickerViewController.shortcutIdentifier = "squeezePress"
pickerViewController.shortcutPickerTipText = LocalizationHelper.localizedString(forKey: "squeezePressShortcutPickerTip")
pickerViewController.presentOverFullScreen(from: viewController)
}
/*
let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero)
selectedProfile = oscProfileMan.getSelectedProfile()
guard let selectedProfile = selectedProfile else {return}
let alert = UIAlertController(title: LocalizationHelper.localizedString(forKey: "Squeeze Shortcut"),
message: LocalizationHelper.localizedString(forKey: "enterSqueezePressShort"),
preferredStyle: .alert)
alert.addTextField { textField in
textField.placeholder = LocalizationHelper.localizedString(forKey:"Example: e, ctrl+e, alt+e ...")
textField.keyboardType = .asciiCapable
textField.autocorrectionType = .no
textField.spellCheckingType = .no
textField.text = selectedProfile.squeezeStartShortcut
}
let okAction = UIAlertAction(title: LocalizationHelper.localizedString(forKey: "OK"), style: .default) { _ in
let comboButtons = alert.textFields?[0].text ?? ""
let keyStrings = CommandManager.shared.extractAutoReleaseButtonStrings(from: comboButtons)
if keyStrings?.count ?? 0 > 0 || comboButtons == "" {
squeezeStartShortcut = comboButtons
}
enterSqueezeEndShortcut(in: viewController)
}
let learnMoreAction = UIAlertAction(title: LocalizationHelper.localizedString(forKey: "Learn More"), style: .default) { _ in
if let url = URL(string: LocalizationHelper.localizedString(forKey: "pencilKeyboardCmdURL")) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
alert.addAction(learnMoreAction)
alert.addAction(okAction)
viewController.present(alert, animated: true, completion: {
})
*/
}
@objc static public func enterSqueezeEndShortcut(in viewController: UIViewController){
if #available(iOS 13.0, *) {
let pickerViewController = WidgetPickerViewController()
pickerViewController.delegate = (viewController as! any WidgetPickerViewControllerDelegate)
pickerViewController.keyboardPickerMode = .shortcutPicker
pickerViewController.tabIdentifiers = ["keyboard", "shortcuts"]
pickerViewController.initialTabIdentifier = "keyboard"
pickerViewController.shortcutIdentifier = "squeezeRelease"
pickerViewController.shortcutPickerTipText = LocalizationHelper.localizedString(forKey: "squeezeReleaseShortcutPickerTip")
pickerViewController.presentOverFullScreen(from: viewController)
}
/*
let alert = UIAlertController(title: LocalizationHelper.localizedString(forKey: "Squeeze Shortcut"),
message: LocalizationHelper.localizedString(forKey: "enterSqueezeReleaseShort"),
preferredStyle: .alert)
alert.addTextField { textField in
textField.placeholder = LocalizationHelper.localizedString(forKey:"Example: b, ctrl+b, alt+b ...")
textField.keyboardType = .asciiCapable
textField.autocorrectionType = .no
textField.spellCheckingType = .no
textField.text = selectedProfile?.squeezeEndShortcut
}
let okAction = UIAlertAction(title: LocalizationHelper.localizedString(forKey: "OK"), style: .default) { _ in
let comboButtons = alert.textFields?[0].text ?? ""
let keyStrings = CommandManager.shared.extractAutoReleaseButtonStrings(from: comboButtons)
if keyStrings?.count ?? 0 > 0 || comboButtons == "" {
squeezeEndShortcut = comboButtons
}
if selectedProfile?.squeezeEndShortcut != squeezeEndShortcut
|| selectedProfile?.squeezeStartShortcut != squeezeStartShortcut {
guard let selectedProfile = selectedProfile else {return}
let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero)
selectedProfile.squeezeEndShortcut = squeezeEndShortcut
selectedProfile.squeezeStartShortcut = squeezeStartShortcut
oscProfileMan.replaceSelectedProfile(with: selectedProfile, overwriteDefault: true)
}
}
let learnMoreAction = UIAlertAction(title: LocalizationHelper.localizedString(forKey: "Learn More"), style: .default) { _ in
if let url = URL(string: LocalizationHelper.localizedString(forKey: "pencilKeyboardCmdURL")) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
alert.addAction(learnMoreAction)
alert.addAction(okAction)
viewController.present(alert, animated: true, completion: {
})
*/
}
private func attachHoverLeave(normalizedLocation:CGPoint){
DispatchQueue.global().asyncAfter(deadline: .now()+0.0086) {
LiSendPenEvent(UInt8(LI_TOUCH_EVENT_HOVER), UInt8(LI_TOOL_TYPE_PEN), 0, Float(normalizedLocation.x), Float(normalizedLocation.y), 0, 0, 0, 0, 0)
DispatchQueue.global().asyncAfter(deadline: .now()+0.0086) {
// LiSendPenEvent(UInt8(LI_TOUCH_EVENT_HOVER_LEAVE), UInt8(LI_TOOL_TYPE_PEN), 0, Float(normalizedLocation.x), Float(normalizedLocation.y), 0, 0, 0, 0, 0)
}
}
}
private func preTouchHoverActionAt(normalizedLocation:CGPoint){
DispatchQueue.global().asyncAfter(deadline: .now()+0.0086) {
LiSendPenEvent(UInt8(LI_TOUCH_EVENT_HOVER), UInt8(LI_TOOL_TYPE_PEN), 0, Float(normalizedLocation.x), Float(normalizedLocation.y), 0, 0, 0, 0, 0)
DispatchQueue.global().asyncAfter(deadline: .now()+0.0086) {
LiSendPenEvent(UInt8(LI_TOUCH_EVENT_HOVER_LEAVE), UInt8(LI_TOOL_TYPE_PEN), 0, Float(normalizedLocation.x), Float(normalizedLocation.y), 0, 0, 0, 0, 0)
}
}
}
}
@@ -0,0 +1,361 @@
//
// PencilTipOffsetCalibrationViewController.swift
// VoidLink
//
// Created by True砖家 on 2026/7/30.
// Copyright © 2026 True砖家 on Bilibili. All rights reserved.
//
import UIKit
private enum PencilTipOffsetCalibrationLayout {
static let containerWidth: CGFloat = 430
static let containerHeight: CGFloat = 430
static let navBarHeight: CGFloat = 50
static let canvasTop: CGFloat = 66
static let canvasWidth: CGFloat = 320
static let canvasHeight: CGFloat = 260
static let sliderTopSpacing: CGFloat = 18
static let horizontalInset: CGFloat = 30
}
private final class PencilTipOffsetCanvasView: UIView {
var offset: CGPoint = .zero {
didSet { setNeedsDisplay() }
}
private var rawTouchPoint: CGPoint? {
didSet { setNeedsDisplay() }
}
override init(frame: CGRect) {
super.init(frame: frame)
isMultipleTouchEnabled = false
backgroundColor = UIColor(white: 0.97, alpha: 1)
layer.borderColor = UIColor.black.withAlphaComponent(0.12).cgColor
layer.borderWidth = 0.5
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
let center = CGPoint(x: bounds.midX, y: bounds.midY)
context.setLineWidth(1)
context.setStrokeColor(UIColor.black.withAlphaComponent(0.14).cgColor)
context.move(to: CGPoint(x: 0, y: center.y))
context.addLine(to: CGPoint(x: bounds.maxX, y: center.y))
context.move(to: CGPoint(x: center.x, y: 0))
context.addLine(to: CGPoint(x: center.x, y: bounds.maxY))
context.strokePath()
context.setStrokeColor(UIColor.systemBlue.withAlphaComponent(0.45).cgColor)
context.setLineWidth(2)
context.move(to: center)
context.addLine(to: CGPoint(x: center.x + offset.x, y: center.y + offset.y))
context.strokePath()
if let rawTouchPoint {
drawLandingPreview(in: context, rawPoint: rawTouchPoint)
}
context.setFillColor(UIColor.black.withAlphaComponent(0.22).cgColor)
context.fillEllipse(in: CGRect(x: center.x - 5, y: center.y - 5, width: 10, height: 10))
let tipCenter = CGPoint(x: center.x + offset.x, y: center.y + offset.y)
context.setFillColor(UIColor.systemBlue.cgColor)
context.fillEllipse(in: CGRect(x: tipCenter.x - 5, y: tipCenter.y - 5, width: 10, height: 10))
}
private func drawLandingPreview(in context: CGContext, rawPoint: CGPoint) {
let correctedPoint = CGPoint(x: rawPoint.x + offset.x, y: rawPoint.y + offset.y)
context.setFillColor(UIColor.black.withAlphaComponent(0.28).cgColor)
context.fillEllipse(in: CGRect(x: rawPoint.x - 5, y: rawPoint.y - 5, width: 10, height: 10))
context.setFillColor(UIColor.systemBlue.cgColor)
context.fillEllipse(in: CGRect(x: correctedPoint.x - 5, y: correctedPoint.y - 5, width: 10, height: 10))
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
updateTouchPoint(from: touches)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
updateTouchPoint(from: touches)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
updateTouchPoint(from: touches)
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
rawTouchPoint = nil
}
private func updateTouchPoint(from touches: Set<UITouch>) {
guard let touch = touches.first(where: { $0.type == .pencil }) else { return }
rawTouchPoint = touch.preciseLocation(in: self)
}
}
@objc class PencilTipOffsetCalibrationViewController: UIViewController, UIGestureRecognizerDelegate {
private let container = UIView()
private let canvasView = PencilTipOffsetCanvasView()
private let xSlider = UISlider()
private let ySlider = UISlider()
private let xLabel = UILabel()
private let yLabel = UILabel()
private var saveButton = UIBarButtonItem()
private var persistedOffset: CGPoint = .zero
private var currentOffset: CGPoint = .zero
override func viewDidLoad() {
super.viewDidLoad()
view.isOpaque = false
view.backgroundColor = UIColor.clear
if #available(iOS 13.0, *) {
overrideUserInterfaceStyle = .light
}
loadPersistedOffset()
setupContainer()
setupNavigationBar()
setupCanvas()
setupSliders()
applyOffset(currentOffset)
NotificationCenter.default.addObserver(
self,
selector: #selector(pencilProPurchaseAborted(_:)),
name: AddOnProduct.PencilProPack.purchaseAbortedNotification(),
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(persistOffset),
name: AddOnProduct.PencilProPack.purchaseSucceededNotification(),
object: nil
)
let tap = UITapGestureRecognizer(target: self, action: #selector(dismissSelf))
tap.delegate = self
view.addGestureRecognizer(tap)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
NotificationCenter.default.removeObserver(self)
}
private func loadPersistedOffset() {
let dataMan = DataManager()
let settings = dataMan.retrieveSettings()
persistedOffset = CGPoint(
x: CGFloat(settings?.pencilTipOffsetX?.floatValue ?? 0),
y: CGFloat(settings?.pencilTipOffsetY?.floatValue ?? 0)
)
currentOffset = persistedOffset
}
private func setupContainer() {
container.translatesAutoresizingMaskIntoConstraints = false
container.backgroundColor = .white
container.layer.cornerRadius = 12
container.layer.masksToBounds = true
container.layer.borderWidth = 0.5
container.layer.borderColor = UIColor.black.withAlphaComponent(0.15).cgColor
view.addSubview(container)
NSLayoutConstraint.activate([
container.centerXAnchor.constraint(equalTo: view.centerXAnchor),
container.centerYAnchor.constraint(equalTo: view.centerYAnchor),
container.widthAnchor.constraint(equalToConstant: PencilTipOffsetCalibrationLayout.containerWidth),
container.heightAnchor.constraint(equalToConstant: PencilTipOffsetCalibrationLayout.containerHeight)
])
}
private func setupNavigationBar() {
let navBar = UINavigationBar()
navBar.translatesAutoresizingMaskIntoConstraints = false
container.addSubview(navBar)
if #available(iOS 13.0, *) {
let appearance = UINavigationBarAppearance()
appearance.shadowColor = nil
appearance.backgroundColor = .white
navBar.standardAppearance = appearance
navBar.scrollEdgeAppearance = appearance
} else {
navBar.setBackgroundImage(UIImage(), for: .default)
navBar.shadowImage = UIImage()
navBar.isTranslucent = true
navBar.backgroundColor = .clear
}
saveButton = UIBarButtonItem(title: LocalizationHelper.localizedString(forKey: "Save"), style: .plain, target: self, action: #selector(saveTapped))
let resetButton = UIBarButtonItem(title: LocalizationHelper.localizedString(forKey: "Reset"), style: .plain, target: self, action: #selector(resetTapped))
let navItem = UINavigationItem(title: LocalizationHelper.localizedString(forKey: "Pencil Tip Offset"))
navItem.leftBarButtonItems = [resetButton]
navItem.rightBarButtonItems = [saveButton]
if #available(iOS 26.0, *) {
resetButton.hidesSharedBackground = true
saveButton.hidesSharedBackground = true
resetButton.tintColor = .tintColor
saveButton.tintColor = .tintColor
}
navBar.items = [navItem]
NSLayoutConstraint.activate([
navBar.topAnchor.constraint(equalTo: container.safeAreaLayoutGuide.topAnchor),
navBar.leadingAnchor.constraint(equalTo: container.leadingAnchor),
navBar.trailingAnchor.constraint(equalTo: container.trailingAnchor),
navBar.heightAnchor.constraint(equalToConstant: PencilTipOffsetCalibrationLayout.navBarHeight)
])
}
private func setupCanvas() {
canvasView.translatesAutoresizingMaskIntoConstraints = false
container.addSubview(canvasView)
NSLayoutConstraint.activate([
canvasView.topAnchor.constraint(equalTo: container.topAnchor, constant: PencilTipOffsetCalibrationLayout.canvasTop),
canvasView.centerXAnchor.constraint(equalTo: container.centerXAnchor),
canvasView.widthAnchor.constraint(equalToConstant: PencilTipOffsetCalibrationLayout.canvasWidth),
canvasView.heightAnchor.constraint(equalToConstant: PencilTipOffsetCalibrationLayout.canvasHeight)
])
}
private func setupSliders() {
let sliderStack = UIStackView()
sliderStack.axis = .vertical
sliderStack.spacing = 10
sliderStack.translatesAutoresizingMaskIntoConstraints = false
container.addSubview(sliderStack)
configureSlider(xSlider, action: #selector(sliderMoved(_:)))
configureSlider(ySlider, action: #selector(sliderMoved(_:)))
configureLabel(xLabel)
configureLabel(yLabel)
sliderStack.addArrangedSubview(makeSliderRow(label: xLabel, slider: xSlider))
sliderStack.addArrangedSubview(makeSliderRow(label: yLabel, slider: ySlider))
NSLayoutConstraint.activate([
sliderStack.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: PencilTipOffsetCalibrationLayout.horizontalInset),
sliderStack.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -PencilTipOffsetCalibrationLayout.horizontalInset),
sliderStack.topAnchor.constraint(equalTo: canvasView.bottomAnchor, constant: PencilTipOffsetCalibrationLayout.sliderTopSpacing)
])
xSlider.value = Float(currentOffset.x)
ySlider.value = Float(currentOffset.y)
updateLabels()
}
private func configureSlider(_ slider: UISlider, action: Selector) {
slider.minimumValue = -15
slider.maximumValue = 15
slider.addTarget(self, action: action, for: .valueChanged)
if PublicUtils.liquidGlassEnabled, #available(iOS 13.0, *) {
slider.maximumTrackTintColor = .tertiarySystemFill
}
}
private func configureLabel(_ label: UILabel) {
label.font = UIFont.systemFont(ofSize: 14)
label.textColor = .black
label.widthAnchor.constraint(equalToConstant: 110).isActive = true
}
private func makeSliderRow(label: UILabel, slider: UISlider) -> UIStackView {
let row = UIStackView(arrangedSubviews: [label, slider])
row.axis = .horizontal
row.alignment = .center
row.spacing = 12
return row
}
@objc private func sliderMoved(_ sender: UISlider) {
currentOffset = CGPoint(x: CGFloat(xSlider.value), y: CGFloat(ySlider.value))
applyOffset(currentOffset)
}
private func applyOffset(_ offset: CGPoint) {
canvasView.offset = offset
updateLabels()
}
private func updateLabels() {
xLabel.text = "\(LocalizationHelper.localizedString(forKey: "X Offset")): \(String(format: "%.1f", currentOffset.x))"
yLabel.text = "\(LocalizationHelper.localizedString(forKey: "Y Offset")): \(String(format: "%.1f", currentOffset.y))"
}
@objc private func resetTapped() {
xSlider.value = 0
ySlider.value = 0
currentOffset = .zero
applyOffset(currentOffset)
}
@objc private func persistOffset() {
let dataMan = DataManager()
guard let settings = dataMan.retrieveSettings() else { return }
settings.pencilTipOffsetX = NSNumber(value: Float(currentOffset.x))
settings.pencilTipOffsetY = NSNumber(value: Float(currentOffset.y))
dataMan.saveData()
persistedOffset = currentOffset
PencilHandler.shared?.updatePencilTipOffset(x: currentOffset.x, y: currentOffset.y)
DispatchQueue.main.async {
AlertControllerUtil.autoCompletion = true
AlertControllerUtil.showAlert(
in: self,
title: "",
message: LocalizationHelper.localizedString(forKey: "Pencil tip offset saved"),
withCancel: false,
buttonTitle: "",
countdown: 1
)
}
}
@objc private func saveTapped() {
saveButton.isEnabled = false
IAPManager.checkPurchaseInfo(.PencilProPack) { info in
self.saveButton.isEnabled = true
if info.valid {
self.persistOffset()
} else {
IAPManager.inAppPurchaseAction(viewController: self, product: .PencilProPack)
}
}
}
@objc private func pencilProPurchaseAborted(_ notification: Notification) {
guard let interruption = notification.object as? PurchaseInterruption else { return }
if interruption == .lowOSVersion {
AlertControllerUtil.showAlert(
in: self,
title: "",
message: LocalizationHelper.localizedString(forKey:"PencilProPackLowOSVersionTip"),
withCancel: false,
buttonTitle: LocalizationHelper.localizedString(forKey: "OK"),
countdown: 0
)
}
}
@objc private func dismissSelf() {
dismiss(animated: true)
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
return touch.view == view
}
}
+964
View File
@@ -0,0 +1,964 @@
//
// PressureCurve.swift
// VoidLink
//
// Created by True砖家 on 2026/1/5.
// Copyright © 2025 True砖家 on Bilibili. All rights reserved.
//
import UIKit
// MARK: - PressureCurve
final class PressureCurve {
var polylinePoints: [CGPoint] = [
CGPoint(x: 0, y: 0.0),
CGPoint(x: 1.0, y: 1.0)
]
private var factor: CGFloat = 0.39
private var cachedSegments: [(p0: CGPoint, c1: CGPoint, c2: CGPoint, p1: CGPoint)] = []
private var curveCutPoints: [CGPoint] {
guard polylinePoints.count >= 2 else { return polylinePoints }
var cuts: [CGPoint] = []
cuts.append(polylinePoints[0]) // 起点
for i in 0..<(polylinePoints.count - 2) {
let mid = CGPoint(
x: (polylinePoints[i+1].x + polylinePoints[i+2].x)/2,
y: (polylinePoints[i+1].y + polylinePoints[i+2].y)/2
)
if i < polylinePoints.count - 3 {
cuts.append(mid)
}
}
cuts.append(polylinePoints.last!) // 终点
return cuts
}
private func curveFactor(for middlePointCount: Int) -> CGFloat {
guard middlePointCount > 0 else { return 2.0 / 3.0 }
let n = CGFloat(middlePointCount)
return 0.30538319 * pow(n, -2.1518917) + 0.36128348
}
public func buildCurveSegments() {
let cuts = curveCutPoints
var segments: [(CGPoint, CGPoint, CGPoint, CGPoint)] = []
factor = curveFactor(for: polylinePoints.count-2)
for i in 0..<(cuts.count - 1) {
let p0 = cuts[i]
let p1 = cuts[i+1]
// segment index
let startIndex = i
let endIndex = i+1
let tangentStart: CGPoint
if startIndex < polylinePoints.count - 1 {
tangentStart = CGPoint(
x: (polylinePoints[startIndex + 1].x - polylinePoints[startIndex].x) * factor,
y: (polylinePoints[startIndex + 1].y - polylinePoints[startIndex].y) * factor
)
} else {
tangentStart = .zero
}
let tangentEnd: CGPoint
if endIndex < polylinePoints.count - 1 {
tangentEnd = CGPoint(
x: (polylinePoints[endIndex + 1].x - polylinePoints[endIndex].x) * factor,
y: (polylinePoints[endIndex + 1].y - polylinePoints[endIndex].y) * factor
)
} else {
tangentEnd = .zero
}
let c1 = CGPoint(x: p0.x + tangentStart.x, y: p0.y + tangentStart.y)
let c2 = CGPoint(x: p1.x - tangentEnd.x, y: p1.y - tangentEnd.y)
segments.append((p0: p0, c1: c1, c2: c2, p1: p1))
}
cachedSegments = segments
}
/// 采样 曲线
func sampleCurve(stepsPerSegment: Int = 50) -> [CGPoint] {
var points: [CGPoint] = []
buildCurveSegments()
for seg in cachedSegments {
let sampled = sampleCurve(p0: seg.p0, c1: seg.c1, c2: seg.c2, p1: seg.p1, steps: stepsPerSegment)
if !points.isEmpty { points.removeLast() } // 避免重复点
points.append(contentsOf: sampled)
}
return points
}
private func sampleCurve(p0: CGPoint, c1: CGPoint, c2: CGPoint, p1: CGPoint, steps: Int) -> [CGPoint] {
var result: [CGPoint] = []
for i in 0...steps {
let t = CGFloat(i)/CGFloat(steps)
let mt = 1-t
let x = mt*mt*mt*p0.x + 3*mt*mt*t*c1.x + 3*mt*t*t*c2.x + t*t*t*p1.x
let y = mt*mt*mt*p0.y + 3*mt*mt*t*c1.y + 3*mt*t*t*c2.y + t*t*t*p1.y
result.append(CGPoint(x:x, y:y))
}
return result
}
var tangentPoints: [CGPoint] { polylinePoints }
func value(at x: CGFloat) -> CGFloat {
guard let firstPoint = polylinePoints.first else { return 0 }
if x < firstPoint.x {return 0}
let xClamped = min(max(x, 0), 1)
let segments = cachedSegments
for seg in segments {
if xClamped >= seg.p0.x && xClamped <= seg.p1.x {
let t = solveT(forX: xClamped,
p0: seg.p0.x,
c1: seg.c1.x,
c2: seg.c2.x,
p1: seg.p1.x)
return cubic(p0: seg.p0.y,
c1: seg.c1.y,
c2: seg.c2.y,
p1: seg.p1.y,
t: t)
}
}
return segments.last?.p1.y ?? 0
}
private func solveT(
forX x: CGFloat,
p0: CGFloat,
c1: CGFloat,
c2: CGFloat,
p1: CGFloat,
iterations: Int = 18
) -> CGFloat {
var low: CGFloat = 0
var high: CGFloat = 1
var t: CGFloat = 0.5
for _ in 0..<iterations {
t = (low + high) * 0.5
let xt = cubic(p0: p0, c1: c1, c2: c2, p1: p1, t: t)
if xt < x {
low = t
} else {
high = t
}
}
return t
}
@inline(__always)
private func cubic(
p0: CGFloat,
c1: CGFloat,
c2: CGFloat,
p1: CGFloat,
t: CGFloat
) -> CGFloat {
let mt = 1 - t
return mt*mt*mt*p0
+ 3*mt*mt*t*c1
+ 3*mt*t*t*c2
+ t*t*t*p1
}
static func exportCurvePoints(_ curve: PressureCurve) -> [NSNumber] {
var arr: [NSNumber] = []
for p in curve.polylinePoints {
arr.append(NSNumber(value: Float(p.x)))
arr.append(NSNumber(value: Float(p.y)))
}
return arr
}
static func importCurvePoints(_ numbers: [NSNumber]) -> [CGPoint] {
var pts: [CGPoint] = []
let count = numbers.count / 2
for i in 0..<count {
let x = CGFloat(numbers[i*2].floatValue)
let y = CGFloat(numbers[i*2 + 1].floatValue)
pts.append(CGPoint(x: x, y: y))
}
return pts
}
static func generateLinearCurvePoints(from pressures: [CGFloat]) -> [CGPoint] {
guard !pressures.isEmpty else { return [] }
let samples = pressures.map { min(max($0, 0), 1) }
let maxPressure = samples.max() ?? 0
if maxPressure < 1 {
let platformX = maxPressure
return [
CGPoint(x: 0, y: 0),
CGPoint(x: platformX, y: 1),
CGPoint(x: platformX, y: 1),
CGPoint(x: 1, y: 1)
]
} else {
return [
CGPoint(x: 0, y: 0),
CGPoint(x: 1, y: 1)
]
}
}
}
// MARK: - PressureCurveView
final class PressureCurveView: UIView {
let curve = PressureCurve()
private var draggingIndex: Int? = nil
private var latestDraggingIndex: Int? = nil
private var newPointGenerated: Bool = false
private var draggingPointPreviousLocation: CGPoint? = nil
private let hitRadius: CGFloat = 35
enum PressureTestStage: UInt8 {
case drawingStage
case curveStage
}
public var testStage: PressureTestStage = .curveStage
private var pressures:[CGFloat] = []
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .black
}
required init?(coder: NSCoder) { fatalError() }
public var showGraph: Bool = false {
didSet { setNeedsDisplay() } // 改变开关立即刷新
}
override func draw(_ rect: CGRect) {
guard let ctx = UIGraphicsGetCurrentContext() else { return }
ctx.saveGState()
// 缩放和平移
let scale: CGFloat = 0.95
let dx = (bounds.width - bounds.width * scale) / 2
let dy = (bounds.height - bounds.height * scale) / 2
ctx.translateBy(x: dx, y: dy)
ctx.scaleBy(x: scale, y: scale)
if showGraph {
drawGrid(ctx)
drawCurve(ctx)
drawTangents(ctx)
}
ctx.restoreGState()
}
private func drawGrid(_ ctx: CGContext) {
ctx.setStrokeColor(UIColor.darkGray.cgColor)
ctx.setLineWidth(1)
for i in 0...4 {
let t = CGFloat(i)/4
let x = bounds.width*t
let y = bounds.height*t
ctx.move(to: CGPoint(x:x, y:0))
ctx.addLine(to: CGPoint(x:x, y:bounds.height))
ctx.move(to: CGPoint(x:0, y:y))
ctx.addLine(to: CGPoint(x:bounds.width, y:y))
}
ctx.strokePath()
}
private func drawCurve(_ ctx: CGContext) {
ctx.setStrokeColor(UIColor.systemBlue.cgColor)
ctx.setLineWidth(6)
let points = curve.sampleCurve(stepsPerSegment: 60)
for (i,p) in points.enumerated() {
let mapped = map(p)
if i==0 { ctx.move(to:mapped) } else { ctx.addLine(to:mapped) }
}
ctx.strokePath()
}
private func drawTangents(_ ctx: CGContext) {
let pts = curve.tangentPoints
ctx.setStrokeColor(UIColor.systemPurple.cgColor)
ctx.setLineWidth(2)
for i in 0..<pts.count {
let p = map(pts[i])
if i==0 { ctx.move(to:p) } else { ctx.addLine(to:p) }
}
ctx.strokePath()
for p in pts {
let c = map(p)
let r: CGFloat = 16
ctx.setFillColor(UIColor.systemPurple.cgColor)
ctx.fill(CGRect(x:c.x-r/2, y:c.y-r/2, width:r, height:r))
}
}
private func map(_ p: CGPoint) -> CGPoint {
CGPoint(x:p.x*bounds.width, y:(1-p.y)*bounds.height)
}
private func unmap(_ p: CGPoint) -> CGPoint {
CGPoint(
x: p.x / bounds.width,
y: 1 - (p.y / bounds.height)
)
}
private func hitTestPoint(_ location: CGPoint) -> Int? {
let pts = curve.tangentPoints
for (i, p) in pts.enumerated() {
let c = map(p)
let dx = c.x - location.x
let dy = c.y - location.y
if dx*dx + dy*dy <= hitRadius*hitRadius {
return i
}
}
return nil
}
func undoLastMove() {
if newPointGenerated {
guard let latestDraggingIndex = latestDraggingIndex else { return }
self.latestDraggingIndex = nil
curve.polylinePoints.remove(at: latestDraggingIndex )
setNeedsDisplay()
}
else {
guard let latestDraggingIndex = latestDraggingIndex else { return }
self.latestDraggingIndex = nil
guard let draggingPointPreviousLocation = draggingPointPreviousLocation else { return }
curve.polylinePoints[latestDraggingIndex] = draggingPointPreviousLocation
setNeedsDisplay()
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: self)
if testStage == .drawingStage {
return
}
draggingIndex = hitTestPoint(location)
if draggingIndex == nil {
guard
let touch = touches.first
else { return }
newPointGenerated = true
let location = touch.location(in: self)
var point = unmap(location)
point.x = min(max(point.x, 0), 1)
point.y = min(max(point.y, 0), 1)
var insertIdx:Int = -1
for (idx, polylinePoint) in curve.polylinePoints.enumerated() {
if point.x <= polylinePoint.x {
insertIdx = idx
break
}
}
if insertIdx >= 0 {
curve.polylinePoints.insert(point, at: insertIdx)
draggingIndex = insertIdx
}
latestDraggingIndex = draggingIndex
setNeedsDisplay()
}
else {
guard let draggingIndex = draggingIndex else { return }
latestDraggingIndex = draggingIndex
draggingPointPreviousLocation = curve.polylinePoints[draggingIndex]
newPointGenerated = false
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if testStage == .drawingStage {
let altitude = touches.first!.altitudeAngle
let force = (touches.first!.force/touches.first!.maximumPossibleForce)/sin(altitude)
pressures.append(force)
return
}
guard
let touch = touches.first,
let idx = draggingIndex
else { return }
let location = touch.location(in: self)
var point = unmap(location)
if draggingIndex == 0 {
let nextPoint = curve.polylinePoints[idx+1]
point.x = min(max(point.x, 0), nextPoint.x)
// point.x = 0
point.y = min(max(point.y, 0), 1)
}
else if draggingIndex == curve.polylinePoints.count-1 {
point.x = 1
point.y = min(max(point.y, 0), 1)
}
else {
let nextPoint = curve.polylinePoints[idx+1]
let previousPoint = curve.polylinePoints[idx-1]
point.x = min(max(point.x, previousPoint.x), nextPoint.x)
point.y = min(max(point.y, 0), 1)
}
// print("currenPoint \(point)")
curve.polylinePoints[idx] = point
setNeedsDisplay()
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if testStage == .drawingStage {
if pressures.isEmpty { return }
curve.polylinePoints = PressureCurve.generateLinearCurvePoints(from: pressures)
testStage = .curveStage
showGraph = true
let parentVC = parentViewController as! PressureCurveViewController
parentVC.setupNavigationBar()
// parentVC.showCurveStageTips()
}
draggingIndex = nil
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
draggingIndex = nil
}
}
final class PressureCurveLUT {
static let levels = 2048
public let table: [Float]
init(curve: PressureCurve) {
curve.buildCurveSegments()
var t = [Float](repeating: 0, count: Self.levels)
let maxIndex = Float(Self.levels - 1)
for i in 0..<Self.levels {
let x = CGFloat(Float(i) / maxIndex)
let y = curve.value(at: x)
t[i] = Float(y)
}
self.table = t
}
@inline(__always)
func value(at x: Float) -> Float {
let clamped = min(max(x, 0), 1)
let index = Int((clamped * Float(Self.levels - 1)).rounded())
return table[index]
}
}
// MARK: - ViewController
class PressureCurveViewController: UIViewController, UIGestureRecognizerDelegate {
private var navBar = UINavigationBar()
private var saveButton: UIBarButtonItem = UIBarButtonItem()
private var container = UIView()
private var curveView = PressureCurveView()
// private var initialTouchCurvePoints: [CGPoint] = []
private var strokePressureCurvePoints: [CGPoint] = []
private var phase1StrokeSampleIndexEnd: Int32 = 0
private var phase2StrokeSampleIndexEnd: Int32 = 0
private var minPressure: CGFloat = 0
private var maxPressure: CGFloat = 1
private var isFirstLaunch: Bool = GenericUtils.isFirstLaunchPressureCurveTool()
// private var curvePhaseButton: UIBarButtonItem = UIBarButtonItem()
private let hookFilterLabel = UILabel()
private let hookFilterSlider = UISlider()
private let strokeEqualizationDepthLabel = UILabel()
private let strokeEqualizationDepthSlider = UISlider()
private let strokeEqualizationStrengthLabel = UILabel()
private let strokeEqualizationStrengthSlider = UISlider()
enum PressureCurvePhase: UInt8, CaseIterable {
case initialTouch
case stroke
func next() -> PressureCurvePhase {
let all = Self.allCases
let index = all.firstIndex(of: self)!
let nextIndex = (index + 1) % all.count
return all[nextIndex]
}
}
private(set) var curvePhase: PressureCurvePhase = .initialTouch
override func viewDidLoad() {
super.viewDidLoad()
self.view.isOpaque = false
self.view.backgroundColor = UIColor.clear // 半透明遮罩
let curveWidth: CGFloat = 530
let curveHeight: CGFloat = 530
curveView = PressureCurveView(frame: CGRect(x: 0, y: 0, width: curveWidth, height: curveHeight))
curveView.center = CGPoint(x: self.view.center.x, y: self.view.center.y)
curveView.backgroundColor = .white
curveView.layer.cornerRadius = 0
curveView.layer.masksToBounds = true
container = UIView(frame: CGRect(x: 0, y: 0, width: curveWidth+10, height: curveHeight+152))
container.center = CGPoint(x: self.view.bounds.midX, y: self.view.bounds.midY+6.5)
container.backgroundColor = .white
container.layer.cornerRadius = 12
container.layer.masksToBounds = true
container.layer.borderWidth = 0.5
container.layer.borderColor = UIColor.black.withAlphaComponent(0.15).cgColor
container.addSubview(curveView)
self.setupNavigationBar()
self.setupBottomSliders()
self.loadPersistedCurve()
self.curveView.showGraph = true
self.view.addSubview(container)
curveView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
curveView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
curveView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor,constant: -25),
curveView.widthAnchor.constraint(equalToConstant: curveWidth),
curveView.heightAnchor.constraint(equalToConstant: curveHeight)
])
self.displayCurve()
NotificationCenter.default.addObserver(
self,
selector: #selector(pencilProPurchaseAborted(_:)),
name: AddOnProduct.PencilProPack.purchaseAbortedNotification(),
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(persistCurve),
name: AddOnProduct.PencilProPack.purchaseSucceededNotification(),
object: nil
)
let tap = UITapGestureRecognizer(target: self, action: #selector(dismissSelf))
tap.delegate = self
view.addGestureRecognizer(tap)
}
override func viewDidAppear(_ animated: Bool) {
/*
AlertControllerUtil.cancelButtonString = LocalizationHelper.localizedString(forKey: "No")
AlertControllerUtil.showAlert(
in: self,
title: LocalizationHelper.localizedString(forKey: "Pen Pressure Curve"),
message: LocalizationHelper.localizedString(forKey:"Reset the pen pressure curve by starting from the pen pressure test?"),
withCancel: true,
buttonTitle: LocalizationHelper.localizedString(forKey: "Yes"),
countdown: 0,
action: {},
completion: {
if AlertControllerUtil.actionCancelled {
self.curveView.showGraph = true
self.curveView.testStage = .curveStage
self.curveView.setNeedsDisplay()
self.setupNavigationBar()
self.showCurveStageTips()
}
else {
self.pressureRangeTest()
}
}
) */
if isFirstLaunch {
showCurveStageTips()
}
}
override func viewWillDisappear(_ animated: Bool) {
NotificationCenter.default.removeObserver(self)
}
private func loadPersistedCurve() {
let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero)
let oscProfile = oscProfileMan.getSelectedProfile()
// initialTouchCurvePoints = PressureCurve.importCurvePoints(oscProfile.initialTouchPressureCurvePoints)
strokePressureCurvePoints = PressureCurve.importCurvePoints(oscProfile.pressureCurvePoints)
displayCurve()
hookFilterSlider.value = Float(oscProfile.phase1StrokeSampleIndexEnd)
strokeEqualizationDepthSlider.value = Float(oscProfile.phase2StrokeSampleIndexEnd)
strokeEqualizationStrengthSlider.value = Float(oscProfile.strokeEqualizationStrength)
strokeEqualizationDepthSliderMoved(strokeEqualizationDepthSlider)
hookFilterDepthSliderMoved(hookFilterSlider)
strokeEqualizationStrengthSliderMoved(strokeEqualizationStrengthSlider)
}
private func displayCurve() {
switch curvePhase {
case .initialTouch:
break
// curveView.curve.polylinePoints = initialTouchCurvePoints
case .stroke:
curveView.curve.polylinePoints = strokePressureCurvePoints
}
curveView.setNeedsDisplay()
}
private func setupBottomSliders() {
hookFilterLabel.translatesAutoresizingMaskIntoConstraints = false
hookFilterSlider.translatesAutoresizingMaskIntoConstraints = false
strokeEqualizationDepthLabel.translatesAutoresizingMaskIntoConstraints = false
strokeEqualizationDepthSlider.translatesAutoresizingMaskIntoConstraints = false
strokeEqualizationStrengthLabel.translatesAutoresizingMaskIntoConstraints = false
strokeEqualizationStrengthSlider.translatesAutoresizingMaskIntoConstraints = false
let traits = UITraitCollection(userInterfaceStyle: .light)
hookFilterLabel.font = UIFont.systemFont(ofSize: 14)
hookFilterLabel.textColor = .black
if PublicUtils.liquidGlassEnabled {
if #available(iOS 13.0, *) {hookFilterSlider.maximumTrackTintColor = .tertiarySystemFill.resolvedColor(with: traits)}
}
hookFilterSlider.minimumValue = 1
hookFilterSlider.maximumValue = 20
hookFilterSlider.addTarget(self, action: #selector(hookFilterDepthSliderMoved(_:)), for: .valueChanged)
strokeEqualizationDepthLabel.font = UIFont.systemFont(ofSize: 14)
strokeEqualizationDepthLabel.textColor = .black
if PublicUtils.liquidGlassEnabled {
if #available(iOS 13.0, *) {strokeEqualizationDepthSlider.maximumTrackTintColor = .tertiarySystemFill.resolvedColor(with: traits)}
}
strokeEqualizationDepthSlider.minimumValue = 1
strokeEqualizationDepthSlider.maximumValue = 80
strokeEqualizationDepthSlider.addTarget(self, action: #selector(strokeEqualizationDepthSliderMoved(_:)), for: .valueChanged)
strokeEqualizationStrengthLabel.font = UIFont.systemFont(ofSize: 14)
strokeEqualizationStrengthLabel.textColor = .black
if PublicUtils.liquidGlassEnabled {
if #available(iOS 13.0, *) {strokeEqualizationStrengthSlider.maximumTrackTintColor = .tertiarySystemFill.resolvedColor(with: traits)}
}
strokeEqualizationStrengthSlider.minimumValue = 0.1
strokeEqualizationStrengthSlider.maximumValue = 3.5
strokeEqualizationStrengthSlider.addTarget(self, action: #selector(strokeEqualizationStrengthSliderMoved(_:)), for: .valueChanged)
container.addSubview(hookFilterLabel)
container.addSubview(hookFilterSlider)
container.addSubview(strokeEqualizationDepthLabel)
container.addSubview(strokeEqualizationDepthSlider)
container.addSubview(strokeEqualizationStrengthLabel)
container.addSubview(strokeEqualizationStrengthSlider)
NSLayoutConstraint.activate([
// Slider
hookFilterLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 16),
hookFilterLabel.widthAnchor.constraint(equalToConstant: 260),
hookFilterSlider.leadingAnchor.constraint(equalTo: hookFilterLabel.trailingAnchor, constant: 12),
hookFilterSlider.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -16),
hookFilterSlider.topAnchor.constraint(equalTo: curveView.bottomAnchor, constant: 0),
hookFilterLabel.centerYAnchor.constraint(equalTo: hookFilterSlider.centerYAnchor, constant:0),
strokeEqualizationDepthLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 16),
strokeEqualizationDepthLabel.widthAnchor.constraint(equalToConstant: 260),
strokeEqualizationDepthSlider.leadingAnchor.constraint(equalTo: strokeEqualizationDepthLabel.trailingAnchor, constant: 12),
strokeEqualizationDepthSlider.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -16),
strokeEqualizationDepthSlider.topAnchor.constraint(equalTo: hookFilterSlider.bottomAnchor, constant: 0),
strokeEqualizationDepthLabel.centerYAnchor.constraint(equalTo: strokeEqualizationDepthSlider.centerYAnchor, constant:0),
strokeEqualizationStrengthLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 16),
strokeEqualizationStrengthLabel.widthAnchor.constraint(equalToConstant: 260),
strokeEqualizationStrengthSlider.leadingAnchor.constraint(equalTo: strokeEqualizationStrengthLabel.trailingAnchor, constant: 12),
strokeEqualizationStrengthSlider.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -16),
strokeEqualizationStrengthSlider.topAnchor.constraint(equalTo: strokeEqualizationDepthSlider.bottomAnchor, constant: 0),
strokeEqualizationStrengthLabel.centerYAnchor.constraint(equalTo: strokeEqualizationStrengthSlider.centerYAnchor, constant:0),
])
}
@objc func hookFilterDepthSliderMoved(_ sender: UISlider) {
phase1StrokeSampleIndexEnd = Int32(sender.value.rounded())
hookFilterLabel.text = "\(LocalizationHelper.localizedString(forKey: "Stroke start hook filtering depth: "))\(String(format: "%d", Int32(sender.value)))";
if sender.value > strokeEqualizationDepthSlider.value {
strokeEqualizationDepthSlider.value = sender.value
strokeEqualizationDepthSliderMoved(strokeEqualizationDepthSlider)
}
}
@objc func strokeEqualizationDepthSliderMoved(_ sender: UISlider) {
sender.value = max(sender.value, hookFilterSlider.value)
phase2StrokeSampleIndexEnd = Int32(sender.value.rounded())
strokeEqualizationDepthLabel.text = "\(LocalizationHelper.localizedString(forKey: "Stroke start equalization depth: "))\(String(format: "%d", Int32(sender.value)))";
}
@objc func strokeEqualizationStrengthSliderMoved(_ sender: UISlider) {
// sender.value = sender.value
strokeEqualizationStrengthLabel.text = "\(LocalizationHelper.localizedString(forKey: "Stroke start force scaling: "))\(String(format: "%.2f", sender.value))";
}
public func setupNavigationBar() {
// 1. 创建 UINavigationBar
navBar.removeFromSuperview()
navBar = UINavigationBar()
navBar.translatesAutoresizingMaskIntoConstraints = false
container.addSubview(navBar)
var navItem = UINavigationItem()
if #available(iOS 13.0, *) {
let appearance = UINavigationBarAppearance()
// appearance.configureWithTransparentBackground() // 透明背景
appearance.shadowColor = nil // 去掉底部细线
appearance.backgroundColor = .white // 可以额外设置完全透明
navBar.standardAppearance = appearance
navBar.scrollEdgeAppearance = appearance
} else {
navBar.setBackgroundImage(UIImage(), for: .default)
navBar.shadowImage = UIImage() // 去掉底部线
navBar.isTranslucent = true
navBar.backgroundColor = .clear
}
// 2. 添加约束 (顶部,左右)
NSLayoutConstraint.activate([
navBar.topAnchor.constraint(equalTo: container.safeAreaLayoutGuide.topAnchor),
navBar.leadingAnchor.constraint(equalTo: container.leadingAnchor),
navBar.trailingAnchor.constraint(equalTo: container.trailingAnchor),
navBar.heightAnchor.constraint(equalToConstant: 50) // 导航栏标准高度
])
if curveView.testStage == .curveStage {
let resetButton = UIBarButtonItem(title: LocalizationHelper.localizedString(forKey: "Reset"), style: .plain, target: self, action: #selector(resetTapped))
let undoButton = UIBarButtonItem(title: LocalizationHelper.localizedString(forKey: "Undo"), style: .plain, target: self, action: #selector(undoTapped))
let rangeTestButton = UIBarButtonItem(title: LocalizationHelper.localizedString(forKey: "Pressure-range-test"), style: .plain, target: self, action: #selector(pressureRangeTest))
// curvePhaseButton = UIBarButtonItem(title: LocalizationHelper.localizedString(forKey: "Initial-touch-curve"), style: .plain, target: self, action: #selector(curvePhaseButtonTapped))
curvePhase = .stroke
saveButton = UIBarButtonItem(title: LocalizationHelper.localizedString(forKey: "Save"), style: .plain, target: self, action: #selector(saveTapped))
// let quitButton = UIBarButtonItem(title: LocalizationHelper.localizedString(forKey: "Exit"), style: .plain, target: self, action: #selector(exitTapped))
navItem.leftBarButtonItems = [resetButton, undoButton, rangeTestButton,]
navItem.rightBarButtonItems = [saveButton]
}
if curveView.testStage == .drawingStage {
navItem = UINavigationItem(title: LocalizationHelper.localizedString(forKey: "Pressure Range Test"))
}
if #available(iOS 26.0, *) {
if let buttons = navItem.leftBarButtonItems {
for button in buttons {
button.hidesSharedBackground = true
button.tintColor = .tintColor
}
}
if let buttons = navItem.rightBarButtonItems {
for button in buttons {
button.hidesSharedBackground = true
button.tintColor = .tintColor
}
}
}
navBar.items = [navItem]
}
public func showCurveStageTips() {
AlertControllerUtil.showAlert(
in: self,
title: LocalizationHelper.localizedString(forKey: "Pen Pressure Curve"),
message: LocalizationHelper.localizedString(forKey:"Adjust the pressure curve by adding or dragging the purple squares."),
withCancel: false,
buttonTitle: LocalizationHelper.localizedString(forKey: "This tip won't be shown again"),
countdown: 6
)
}
@objc private func resetTapped() {
curveView.curve.polylinePoints = [CGPoint(x: 0, y: 0), CGPoint(x: 1, y: 1)]
DispatchQueue.main.async {
self.curveView.setNeedsDisplay()
}
}
@objc private func undoTapped() {
DispatchQueue.main.async {
self.curveView.undoLastMove()
}
}
@objc private func pencilProPurchaseAborted(_ notification: Notification) {
guard let interruption = notification.object as? PurchaseInterruption else { return }
if interruption != .learnMore {
resetTapped()
}
if interruption == .lowOSVersion {
AlertControllerUtil.showAlert(
in: self,
title: "",
message: LocalizationHelper.localizedString(forKey:"PencilProPackLowOSVersionTip"),
withCancel: false,
buttonTitle: LocalizationHelper.localizedString(forKey: "OK"),
countdown: 0)
}
}
@objc private func pressureRangeTest() {
AlertControllerUtil.showAlert(
in: self,
title: LocalizationHelper.localizedString(forKey: "Pressure Range Test"),
message: LocalizationHelper.localizedString(forKey:"pressureRangeTestTip"),
withCancel: true,
buttonTitle: LocalizationHelper.localizedString(forKey: "OK"),
countdown: 0,
completion:{
if AlertControllerUtil.actionCancelled { return }
self.resetTapped()
self.curveView.showGraph = false
self.curveView.testStage = .drawingStage
self.curveView.setNeedsDisplay()
self.setupNavigationBar()
})
}
/*
@objc private func curvePhaseButtonTapped() {
switch curvePhase {
case .initialTouch:
// initialTouchCurvePoints = curveView.curve.polylinePoints
curvePhaseButton.title = LocalizationHelper.localizedString(forKey:"Initial-touch-curve")
case .stroke:
strokeCurvePoints = curveView.curve.polylinePoints
curvePhaseButton.title = LocalizationHelper.localizedString(forKey:"Stroke-curve")
}
curvePhase = curvePhase.next()
displayCurve()
} */
@objc private func persistCurve() {
let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero)
let selectedProfile = oscProfileMan.getSelectedProfile()
switch curvePhase {
case .initialTouch:
break
// initialTouchCurvePoints = curveView.curve.polylinePoints
case .stroke:
strokePressureCurvePoints = curveView.curve.polylinePoints
}
curveView.curve.polylinePoints = strokePressureCurvePoints
selectedProfile.pressureCurvePoints = PressureCurve.exportCurvePoints(curveView.curve)
// curveView.curve.polylinePoints = initialTouchCurvePoints
// selectedProfile.initialTouchPressureCurvePoints = PressureCurve.exportCurvePoints(curveView.curve)
displayCurve()
selectedProfile.phase1StrokeSampleIndexEnd = Int32(hookFilterSlider.value)
selectedProfile.phase2StrokeSampleIndexEnd = Int32(strokeEqualizationDepthSlider.value)
selectedProfile.strokeEqualizationStrength = CGFloat(strokeEqualizationStrengthSlider.value)
oscProfileMan.replaceSelectedProfile(with: selectedProfile, overwriteDefault: true)
DispatchQueue.main.async {
if self.isFirstLaunch {
AlertControllerUtil.showAlert(
in: self,
title: LocalizationHelper.localizedString(forKey: "Pen Pressure Curve"),
message: LocalizationHelper.localizedString(forKey:"firstPressureCurvePersistTip"),
withCancel: false,
buttonTitle: LocalizationHelper.localizedString(forKey: "This tip won't be shown again"),
countdown: 11,
completion: {
PencilHandler.shared?.setupPressureLUT()
})
self.isFirstLaunch = false
}
else{
AlertControllerUtil.autoCompletion = true
AlertControllerUtil.showAlert(
in: self,
title: LocalizationHelper.localizedString(forKey: ""),
message: LocalizationHelper.localizedString(forKey:"Pressure curve saved"),
withCancel: false,
buttonTitle: "",
countdown: 1,
completion: {
PencilHandler.shared?.setupPressureLUT()
})
}
}
}
@objc private func saveTapped() {
saveButton.isEnabled = false
IAPManager.checkPurchaseInfo(.PencilProPack) { info in
self.saveButton.isEnabled = true
if info.valid {
self.persistCurve()
}
else{
IAPManager.inAppPurchaseAction(viewController: self, product: .PencilProPack)
}
}
}
@objc private func exitTapped() {
self.dismiss(animated: true)
}
public func gestureRecognizer(
_ gestureRecognizer: UIGestureRecognizer,
shouldReceive touch: UITouch
) -> Bool {
return touch.view == view
}
@objc private func dismissSelf(){
dismiss(animated: true, completion: nil)
}
}
+3 -1
View File
@@ -14,7 +14,9 @@
NS_ASSUME_NONNULL_BEGIN
@interface PureNativeTouchHandler : UIResponder
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings;
- (id)initWithView:(StreamView*)view settings:(TemporarySettings*)settings profile:(OSCProfile *)profile;
@end
+84 -51
View File
@@ -11,15 +11,14 @@
#import "NativeTouchPointer.h"
#import "OnScreenControls.h"
#import "StreamView.h"
#import "VoidLink-Swift.h"
#include <Limelight.h>
@implementation PureNativeTouchHandler {
StreamView* streamView;
TemporarySettings* currentSettings;
__weak StreamView* streamView;
bool activateCoordSelector;
CGFloat pointerVelocityDividerLocationByPoints;
uint16_t touchMoveEventIntervalUs;
bool asyncNativeTouch;
unsigned int touchDownQos;
@@ -34,29 +33,38 @@
NSMutableSet<NSNumber *> *pointerIdPool; //pre-defined pool of pointerIds.
NSMutableSet<NSNumber *> *unassignedPointerIds;
NSMutableSet *blacklistedTouches;
bool trackPointEnabled;
NSMutableArray<CAShapeLayer *> * trackPointPool;
NSMutableDictionary *pointerObjDict;
CGFloat slideGestureVerticalThreshold;
CGFloat screenWidthWithThreshold;
CGFloat EDGE_TOLERANCE;
CGFloat _edgeTolerance;
CGRect streamViewBounds;
int64_t moveEventIntervalNSec;
}
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings{
- (id)initWithView:(StreamView*)view settings:(TemporarySettings*)settings profile:(OSCProfile *)profile{
self = [super init];
self->streamView = view;
self->currentSettings = settings;
self->activateCoordSelector = currentSettings.pointerVelocityModeDivider.floatValue != 1.0;
self->touchMoveEventIntervalUs = currentSettings.touchMoveEventInterval.intValue;
self->activateCoordSelector = profile.pointerVelocityModeDivider != 1.0;
self->moveEventIntervalNSec = (int64_t)(settings.touchMoveEventInterval.intValue * 1000);;
self->streamViewBounds = view.bounds;
self->pointerIdDict = [NSMutableDictionary dictionary];
self->pointerIdPool = [NSMutableSet set];
self->trackPointEnabled = settings.touchPointTracking;
self->trackPointPool = [NSMutableArray array];
for (uint8_t i = 0; i <= 10; i++) { //ipadOS supports upto 11 finger touches
[self->pointerIdPool addObject:@(i)];
[self->trackPointPool addObject:[GraphicUtils makeTouchTrackpointIn:streamView]];
}
self->activePointerIds = [NSMutableSet set];
self->blacklistedTouches = [NSMutableSet set];
@@ -81,16 +89,15 @@
default: break;
}
self->pointerObjDict = [NSMutableDictionary dictionary];
EDGE_TOLERANCE = 10.0;
_edgeTolerance = settings.edgeSlidingSensitivity.floatValue;
slideGestureVerticalThreshold = CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.4;
screenWidthWithThreshold = CGRectGetWidth([[UIScreen mainScreen] bounds]) - EDGE_TOLERANCE;
screenWidthWithThreshold = CGRectGetWidth([[UIScreen mainScreen] bounds]) - _edgeTolerance;
self->pointerVelocityDividerLocationByPoints = self->streamView.bounds.size.width * settings.pointerVelocityModeDivider.floatValue;
self->pointerVelocityDividerLocationByPoints = self->streamView.bounds.size.width * profile.pointerVelocityModeDivider;
[NativeTouchPointer initContextWithView:self->streamView andSettings:settings];
[NativeTouchPointer initContextWithView:self->streamView profile:profile];
//_touchesCapturedByOnScreenButtons = [[NSMutableSet alloc] init];
return self;
}
@@ -163,7 +170,7 @@
//check if touch point is spawned on the left or right upper half screen edges, event to remote PC. this is for better handling in-stream slide gesture
CGPoint initialPoint = [touch locationInView:self->streamView];
if(initialPoint.y < slideGestureVerticalThreshold && (initialPoint.x < EDGE_TOLERANCE || initialPoint.x > screenWidthWithThreshold)) {
if(initialPoint.y < slideGestureVerticalThreshold && (initialPoint.x < _edgeTolerance || initialPoint.x > screenWidthWithThreshold)) {
[blacklistedTouches addObject:touchAddrObj];
}
}
@@ -187,7 +194,9 @@
- (void)sendTouchEvent:(UITouch*)touch withTouchtype:(uint8_t)touchType{
//if(touchPointSpawnedAtUpperScreenEdge && touchType != LI_TOUCH_EVENT_UP) return; // we're done here. this touch event will not be sent to the remote PC. and this must be checked after coord selector finishes populating new relative coords, or the app will crash
//if(touchPointSpawnedAtUpperScreenEdge && touchType != LI_TOUCH_EVENT_UP) return; // we're done here. this touch event will not be sent to the remote PC. and this must be checked after coord selector finishes populating new relative coords, or the app will crash
if(!touch) return;
if([blacklistedTouches containsObject:@((uintptr_t)touch)]) return;
CGPoint targetCoords;
@@ -202,72 +211,83 @@
CGFloat normalizedY = location.y / videoSize.height;
uint8_t pointerId = [self retrievePointerIdFromDict:touch];
if([self getPointerObjFromDict:touch].needResetCoords){ // access whether the current pointer has reached the boundary, and need a coord reset.
NativeTouchPointer *pointer = [self getPointerObjFromDict:touch];
if(pointer != nil && pointer.needResetCoords){ // access whether the current pointer has reached the boundary, and need a coord reset.
LiSendTouchEvent(LI_TOUCH_EVENT_UP, pointerId, normalizedX, normalizedY, 0, 0, 0, 0); //event must sent from the lowest level directy by LiSendTouchEvent to simulate continous dragging to another point on screen
LiSendTouchEvent(LI_TOUCH_EVENT_DOWN, pointerId, 0.3, 0.4, 0, 0, 0, 0);
}else LiSendTouchEvent(touchType, pointerId, normalizedX, normalizedY,(touch.force / touch.maximumPossibleForce) / sin(touch.altitudeAngle),0.0f, 0.0f,[self getRotationFromAzimuthAngle:[touch azimuthAngleInView:streamView]]);
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
if (asyncNativeTouch) dispatch_async(dispatch_get_global_queue(touchDownQos, 0), ^{
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
for (UITouch* touch in touches){
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
[self handleTouchDown:touch]; //generate & populate pointerId
if(self->activateCoordSelector) [self populatePointerObjIntoDict:touch];
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_DOWN];
}
if(self->trackPointEnabled){
dispatch_async(dispatch_get_main_queue(), ^{
[CATransaction begin];
[CATransaction setDisableActions:YES];
for (UITouch* touch in touches){
uint8_t pointerId = [self retrievePointerIdFromDict:touch];
CAShapeLayer* trackPoint = self->trackPointPool[pointerId];
trackPoint.position = [touch locationInView:self->streamView];
trackPoint.hidden = false;
}
[CATransaction commit];
});
}
});
else{
for (UITouch* touch in touches){
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
[self handleTouchDown:touch]; //generate & populate pointerId
if(self->activateCoordSelector) [self populatePointerObjIntoDict:touch];
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_DOWN];
}
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
if (asyncNativeTouch) dispatch_async(dispatch_get_global_queue(touchMoveQos, 0), ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, moveEventIntervalNSec), dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
for (UITouch* touch in touches){
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
if(self->activateCoordSelector) [self updatePointerObjInDict:touch];
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_MOVE];
[[self getPointerObjFromDict:touch] doesNeedResetCoords]; // execute the judging of doesReachBoundary for current pointer instance. (happens after the event is sent to Sunshine service)
usleep(self->touchMoveEventIntervalUs);
[[self getPointerObjFromDict:touch] doesNeedResetCoords];
}
});
else {
for (UITouch* touch in touches){
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
if(self->activateCoordSelector) [self updatePointerObjInDict:touch];
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_MOVE];
[[self getPointerObjFromDict:touch] doesNeedResetCoords]; // execute the judging of doesReachBoundary for current pointer instance. (happens after the event is sent to Sunshine service)
// usleep(self->touchMoveEventIntervalUs);
}
if(self->trackPointEnabled){
dispatch_async(dispatch_get_main_queue(), ^{
[CATransaction begin];
[CATransaction setDisableActions:YES];
for (UITouch* touch in touches){
uint8_t pointerId = [self retrievePointerIdFromDict:touch];
CAShapeLayer* trackPoint = self->trackPointPool[pointerId];
trackPoint.position = [touch locationInView:self->streamView];
}
[CATransaction commit];
});
}
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
if(asyncNativeTouch) dispatch_async(dispatch_get_global_queue(touchEndQos, 0), ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, moveEventIntervalNSec), dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
for (UITouch* touch in touches){
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_UP]; //send touch event before remove pointerId
[self removePointerId:touch]; //then remove pointerId
if(self->trackPointEnabled){
dispatch_async(dispatch_get_main_queue(), ^{
uint8_t pointerId = [self retrievePointerIdFromDict:touch];
CAShapeLayer* trackPoint = self->trackPointPool[pointerId];
[CATransaction begin];
[CATransaction setDisableActions:YES];
trackPoint.hidden = true;
[CATransaction commit];
[self removePointerId:touch];
});
}
else [self removePointerId:touch];
if(self->activateCoordSelector) [self removePointerObjFromDict:touch];
[self->blacklistedTouches removeObject:@((uintptr_t)touch)];
}
//if(self->touchPointSpawnedAtUpperScreenEdge && [[event allTouches] count] == [touches count])
});
else{
for (UITouch* touch in touches){
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_UP]; //send touch event before remove pointerId
[self removePointerId:touch]; //then remove pointerId
if(self->activateCoordSelector) [self removePointerObjFromDict:touch];
}
}
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
@@ -303,5 +323,18 @@
return pointer.useRelativeCoords ? pointer.latestRelativePoint : pointer.latestPoint;
}
@end
- (void)dealloc{
NSLog(@"dealloc purePativeTouchHanlder %f", CACurrentMediaTime());
pointerIdDict = nil;
activePointerIds = nil;
pointerIdPool= nil;
unassignedPointerIds = nil;
blacklistedTouches = nil;
for(CAShapeLayer* layer in trackPointPool){
[layer removeFromSuperlayer];
}
trackPointPool = nil;
pointerObjDict = nil;
}
@end
+203
View File
@@ -0,0 +1,203 @@
//
// RadialMenuOverlayView.swift
// VoidLink
//
// Created by True砖家 on 2026/7/1.
// Copyright © 2026 True砖家 on Bilibili. All rights reserved.
//
import SwiftUI
import UIKit
@available(iOS 13.0, *)
final class RadialMenuOverlayView: UIView {
func getNavigationElements() -> [ControllerNavigationElement] {
var elements: [ControllerNavigationElement] = []
elements.append(ControllerNavigationElement(control: ControllerNavigator.radialMenuButtonPosition == .right ? .leftStick : .rightStick, action: "focusNavigation"))
return elements
}
private enum Metrics {
static let diameter: CGFloat = PublicUtils.isIPhone ? 200 : 260
static let releaseDistanceEpsilon: CGFloat = 0.015
}
private let selectionState = RadialMenuSelectionState()
private var hostingController: UIHostingController<RadialMenuView>?
private var themeObserver: NSObjectProtocol?
private var previousJoystickDistance: CGFloat?
private var isJoystickReturningToCenter = false
private var releaseLatchedSelectedIndex: Int?
static var menuSectors: [RadialMenuSector] = []
init() {
super.init(frame: .zero)
setupView()
observeThemeChanges()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupView()
observeThemeChanges()
}
deinit {
removeThemeObserver()
}
static func presentInKeyWindow() -> RadialMenuOverlayView? {
guard let window = keyWindow() else { return nil }
let overlayView = RadialMenuOverlayView()
overlayView.translatesAutoresizingMaskIntoConstraints = false
CATransaction.begin()
CATransaction.setDisableActions(true)
window.addSubview(overlayView)
NSLayoutConstraint.activate([
overlayView.widthAnchor.constraint(equalToConstant: Metrics.diameter),
overlayView.heightAnchor.constraint(equalToConstant: Metrics.diameter),
overlayView.centerXAnchor.constraint(equalTo: window.centerXAnchor),
overlayView.centerYAnchor.constraint(equalTo: window.centerYAnchor)
])
CATransaction.commit()
return overlayView
}
func updateSelection(xOffset: CGFloat, yOffset: CGFloat) -> RadialMenuItem? {
guard Thread.isMainThread else {
return DispatchQueue.main.sync {
self.updateSelection(xOffset: xOffset, yOffset: yOffset)
}
}
let previousSelectedIndex = selectionState.selectedIndex
latchSelectionIfNeeded(
distance: Self.normalizedDistance(xOffset: xOffset, yOffset: yOffset),
selectedIndex: previousSelectedIndex
)
selectionState.updateSelection(xOffset: xOffset, yOffset: yOffset)
guard previousSelectedIndex != nil, selectionState.selectedIndex == nil else {
return nil
}
let actionSelectedIndex = releaseLatchedSelectedIndex ?? previousSelectedIndex
resetReleaseLatch()
guard let actionSelectedIndex,
Self.menuSectors.indices.contains(actionSelectedIndex) else {
return nil
}
return Self.menuSectors[actionSelectedIndex].item
}
func dismiss() {
removeThemeObserver()
hostingController?.willMove(toParent: nil)
hostingController?.view.removeFromSuperview()
hostingController?.removeFromParent()
hostingController = nil
removeFromSuperview()
}
private func setupView() {
backgroundColor = .clear
isUserInteractionEnabled = false
clipsToBounds = false
let radialMenuView = makeRadialMenuView()
let hostingController = UIHostingController(rootView: radialMenuView)
hostingController.view.backgroundColor = .clear
hostingController.view.isUserInteractionEnabled = false
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
self.hostingController = hostingController
addSubview(hostingController.view)
NSLayoutConstraint.activate([
hostingController.view.topAnchor.constraint(equalTo: topAnchor),
hostingController.view.bottomAnchor.constraint(equalTo: bottomAnchor),
hostingController.view.leadingAnchor.constraint(equalTo: leadingAnchor),
hostingController.view.trailingAnchor.constraint(equalTo: trailingAnchor)
])
GamepadNavigationIllustrationHud.updateNavigationElements(self.getNavigationElements())
}
private func observeThemeChanges() {
themeObserver = NotificationCenter.default.addObserver(
forName: Notification.Name(ThemeManager.ThemeDidChangeNotification),
object: nil,
queue: .main
) { [weak self] _ in
self?.refreshTheme()
}
}
private func removeThemeObserver() {
if let themeObserver {
NotificationCenter.default.removeObserver(themeObserver)
self.themeObserver = nil
}
}
private func refreshTheme() {
hostingController?.rootView = makeRadialMenuView()
}
private func latchSelectionIfNeeded(distance: CGFloat, selectedIndex: Int?) {
defer { previousJoystickDistance = distance }
guard let previousJoystickDistance else {
return
}
if distance + Metrics.releaseDistanceEpsilon < previousJoystickDistance {
if !isJoystickReturningToCenter {
releaseLatchedSelectedIndex = selectedIndex
isJoystickReturningToCenter = true
}
} else if distance > previousJoystickDistance + Metrics.releaseDistanceEpsilon {
resetReleaseLatch(keepingPreviousDistance: true)
}
}
private func resetReleaseLatch(keepingPreviousDistance: Bool = false) {
if !keepingPreviousDistance {
previousJoystickDistance = nil
}
isJoystickReturningToCenter = false
releaseLatchedSelectedIndex = nil
}
private func makeRadialMenuView() -> RadialMenuView {
RadialMenuView(
sectors: Self.menuSectors,
isTouchSelectionEnabled: false,
style: RadialMenuStyle.themed(
for: ThemeManager.userInterfaceStyle(),
accentColor: ThemeManager.appPrimaryColor
),
selectionState: selectionState
)
}
private static func keyWindow() -> UIWindow? {
UIApplication.shared.windows.first(where: { $0.isKeyWindow }) ?? UIApplication.shared.windows.first
}
private static func normalizedDistance(xOffset: CGFloat, yOffset: CGFloat) -> CGFloat {
let x = max(min(xOffset, 1), -1)
let y = max(min(yOffset, 1), -1)
return hypot(x, y)
}
func navigateByController(forward: Bool) {}
func navigateByController(downward: Bool) {}
func uiWidgetActionForControllerNavigator(forward: Bool, from navigation: ControllerNavigationElement) {}
func uiButtonActionForControllerNavigator(pressed: Bool, from navigation: ControllerNavigationElement) {}
}
+2 -2
View File
@@ -13,13 +13,13 @@
#import "DataManager.h"
#import "CustomTapGestureRecognizer.h"
#define RIGHTCLICK_TAP_DOWN_TIME_THRESHOLD_S 0.15
#define RIGHTCLICK_TAP_DOWN_TIME_THRESHOLD_S 0.1
NS_ASSUME_NONNULL_BEGIN
@interface RelativeTouchHandler : UIResponder
@property (nonatomic, readonly) CustomTapGestureRecognizer* mouseRightClickTapRecognizer; // this object will be passed to onscreencontrol class for areVirtualControllerTaps flag setting
@property (nonatomic, readonly) CustomTapGestureRecognizer* mouseRightClickTapRecognizer; // this object will be passed to onscreencontrol class for areVirtualControllerTaps flag setting
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings;
+91 -100
View File
@@ -1,6 +1,6 @@
//
// RelativeTouchHandler.m
// Moonlight
// VoidLink
//
// Completely refactored by True砖家 on 2024.9.13
// Copyright © 2024 True砖家 on Bilibili. All rights reserved.
@@ -8,6 +8,7 @@
#import "RelativeTouchHandler.h"
#import "DataManager.h"
#import "VoidLink-Swift.h"
#include <Limelight.h>
@@ -23,22 +24,26 @@ static const float QUICK_TAP_TIME_INTERVAL = 0.2;
BOOL firstTouchMoved;
BOOL mousePointerMoved;
BOOL quickTapDetected;
BOOL isInMouseWheelScrollingMode;
// upper screen edge check
bool touchPointSpawnedAtUpperScreenEdge;
CGFloat slideGestureVerticalThreshold;
CGFloat screenWidthWithThreshold;
CGFloat EDGE_TOLERANCE;
CGFloat _edgeTolerance;
UITouch* touchLockedForMouseMove;
UITouch* quickTapTouch;
bool multiTouchesDetected;
CADisplayLink *displayLink;
#if TARGET_OS_TV
UIGestureRecognizer* remotePressRecognizer;
UIGestureRecognizer* remoteLongPressRecognizer;
#endif
StreamView* streamView;
__weak StreamView* streamView;
}
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings {
@@ -53,18 +58,21 @@ static const float QUICK_TAP_TIME_INTERVAL = 0.2;
_mouseRightClickTapRecognizer.delaysTouchesBegan = NO;
_mouseRightClickTapRecognizer.delaysTouchesEnded = NO;
[self->streamView.streamFrameTopLayerView addGestureRecognizer:_mouseRightClickTapRecognizer]; // add all additional gestures to the streamFrameTopLayerView instead of the streamview.
_mouseRightClickTapRecognizer.touchCapturingView = streamView;
isInMouseWheelScrollingMode = false;
firstTouchMoved = false;
mousePointerMoved = false;
mousePointerTimestamp = 0;
// upper screen check
EDGE_TOLERANCE = 10.0;
_edgeTolerance = settings.edgeSlidingSensitivity.floatValue;
slideGestureVerticalThreshold = CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.4;
screenWidthWithThreshold = CGRectGetWidth([[UIScreen mainScreen] bounds]) - EDGE_TOLERANCE;
screenWidthWithThreshold = CGRectGetWidth([[UIScreen mainScreen] bounds]) - _edgeTolerance;
self->touchPointSpawnedAtUpperScreenEdge = false;
// self->displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkCallback:)];
// [self->displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
#if TARGET_OS_TV
remotePressRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(remoteButtonPressed:)];
remotePressRecognizer.allowedPressTypes = @[@(UIPressTypeSelect)];
@@ -81,42 +89,21 @@ static const float QUICK_TAP_TIME_INTERVAL = 0.2;
- (bool)isOnScreenControllerBeingPressed:(NSSet* )touches{
for(UITouch* touch in touches){
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]) return true;
if([OnScreenControls.touchesCapturedByOnScreenControls containsObject:touch]) return true;
}
return false;
}
- (bool)isOnScreenWidgetViewBeingPressed {
bool gotOneButtonPressed = false;
for(UIView* view in self->streamView.superview.subviews){ // iterates all on-screen widget views in StreamFrameView
if ([view isKindOfClass:[OnScreenWidgetView class]]) {
OnScreenWidgetView* widgetView = (OnScreenWidgetView*) view;
if(widgetView.pressed){
gotOneButtonPressed = true; //got one button pressed
}
}
}
return gotOneButtonPressed;
}
- (void)resetAllPressedFlagsForOnScreenWidgetViews {
for(UIView* view in self->streamView.superview.subviews){ // iterates all on-screen widget views in StreamFrameView
if ([view isKindOfClass:[OnScreenWidgetView class]]) {
OnScreenWidgetView* widgetView = (OnScreenWidgetView*) view;
widgetView.pressed = false;
}
}
}
- (void)mouseRightClick {
multiTouchesDetected = false;
dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC));
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
Log(LOG_D, @"Sending right mouse button press");
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
Log(LOG_D, @"Sending right mouse button press");
// Wait 100 ms to simulate a real button press
usleep(100 * 1000);
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
dispatch_after(delay, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
});
});
}
@@ -130,35 +117,34 @@ static const float QUICK_TAP_TIME_INTERVAL = 0.2;
return hypotf(originalPoint.x - currentPoint.x, originalPoint.y - currentPoint.y) <= 300;
}
- (BOOL)isAdjacentPoints:(CGPoint)currentPoint from:(CGPoint)originalPoint tolerance:(CGFloat)tolerance {
bool isAdjacent = hypotf(originalPoint.x - currentPoint.x, originalPoint.y - currentPoint.y) <= hypot(tolerance, tolerance);
return isAdjacent;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
firstTouchMoved = false;
//check if touch point is spawned on the left or right upper half screen edges, this is the highest priority
CGPoint initialPoint = [[touches anyObject] locationInView:streamView];
if(initialPoint.y < slideGestureVerticalThreshold && (initialPoint.x < EDGE_TOLERANCE || initialPoint.x > screenWidthWithThreshold)) {
if(initialPoint.y < slideGestureVerticalThreshold && (initialPoint.x < _edgeTolerance || initialPoint.x > screenWidthWithThreshold)) {
self->touchPointSpawnedAtUpperScreenEdge = true;
return;
}
firstTouchMoved = false;
touchPointSpawnedAtUpperScreenEdge = false; // reset this flag immediately if we get a touch event passing the check above, this fixes irresponsive touch after closing the command tool menu.
if([[event allTouches] count] == 2 && ![self isOnScreenWidgetViewBeingPressed] && ![self isOnScreenControllerBeingPressed:[event allTouches]]){
NSLog(@"get in scrolling mode");
isInMouseWheelScrollingMode = true;
return; // if we got 2 touches on the blank area, it's gonna be a mouse scroll touch, and must prevent UITtouch object for mouse pointer being captured & locked
if([UITouchUtil touchesIn:streamView from:event].count>=2){
multiTouchesDetected = true;
return;
}
// NSLog(@"touches count in began stage: %llu", (uint64_t)[touches count]);
UITouch* candidateTouch = nil;
// the onscreen controllers are implmented by CALayer, which can not intercept UITouch event, touch will penetrate to the streamView level and captured in the touches callback of this touchHandler class.
// the onscreen button are UIViews, they intercept UITouch events, so we don't need to worry about them.
for(UITouch* touch in touches){
// NSLog(@"candidate touch test: %llu", (uint64_t)touch);
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]){
// NSLog(@"%f controller tap detected", CACurrentMediaTime());
if([OnScreenControls.touchesCapturedByOnScreenControls containsObject:touch]){
continue;
}
else candidateTouch = touch;
@@ -166,15 +152,18 @@ static const float QUICK_TAP_TIME_INTERVAL = 0.2;
// quick double tap detection for dragging. simulates a real notebook computer touchpad
CGPoint currentTouchLocation = [candidateTouch locationInView:streamView];
NSTimeInterval tapInterval = CACurrentMediaTime() - mousePointerTimestamp;
if(tapInterval < QUICK_TAP_TIME_INTERVAL && [self isAdjacentTouches:currentTouchLocation from:initialMousePointerLocation] ) {
// NSLog(@"quick click detected");
quickTapDetected = true;
NSLog(@"quick Tap Detected");
// LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT); // do not press down mouse button here, or it wiil easily turn to double click on remote PC
if([UITouchUtil touchesIn:streamView from:event].count == 1){
NSTimeInterval tapInterval = CACurrentMediaTime() - mousePointerTimestamp;
if(tapInterval < QUICK_TAP_TIME_INTERVAL
&& [self isAdjacentTouches:currentTouchLocation from:initialMousePointerLocation]) {
quickTapDetected = true;
NSLog(@"quick Tap Detected");
}
quickTapTouch = touches.anyObject;
}
// we must use [event allTouches] to check if touchLockedForMouseMove is captured, because the UITouch object could be captured by upper layer of UIView(in cases like tap gestures), not passed to the touches callbacks in this class, but still available in [event allTouches]
// use [event allTouches] to check if touchLockedForMouseMove is captured, if already captured, don't update touchLockedForMouseMove
if(candidateTouch != nil && ![[event allTouches] containsObject:touchLockedForMouseMove]){
touchLockedForMouseMove = candidateTouch;
// NSLog(@"Candidate touch for mouse movement locked");
@@ -184,37 +173,37 @@ static const float QUICK_TAP_TIME_INTERVAL = 0.2;
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
//NSLog(@"%f, touchesMoved callback, is scrolling: %d, touches count: %d", CACurrentMediaTime(), isInMouseWheelScrollingMode, (uint32_t)[touches count]);
if(isInMouseWheelScrollingMode && [[event allTouches] count] == 2 && ![self isOnScreenWidgetViewBeingPressed] && ![self isOnScreenControllerBeingPressed:touches]){
NSSet* twoTouches = [event allTouches];
CGPoint firstLocation = [[[twoTouches allObjects] objectAtIndex:0] locationInView:streamView];
CGPoint secondLocation = [[[twoTouches allObjects] objectAtIndex:1] locationInView:streamView];
CGPoint avgLocation = CGPointMake((firstLocation.x + secondLocation.x) / 2, (firstLocation.y + secondLocation.y) / 2);
if ((CACurrentMediaTime() - _mouseRightClickTapRecognizer.gestureCapturedTime > RIGHTCLICK_TAP_DOWN_TIME_THRESHOLD_S) && twoFingerTouchLocation.y != avgLocation.y) { //prevent sending scrollevent while right click gesture is being recognized. The time threshold is only 150ms, resulting in a barely noticeable delay before the scroll event is activated.
// and we must exclude onscreen button taps & on-screen controller taps
LiSendHighResScrollEvent((avgLocation.y - twoFingerTouchLocation.y) * 10);
}
twoFingerTouchLocation = avgLocation;
return;
}
// NSLog(@"%f touchesMoved callback, locked touch: %llu", CACurrentMediaTime(), (uintptr_t)touchLockedForMouseMove);
NSSet* currentTouches = [UITouchUtil touchesIn:streamView from:event];
if(![self isOnScreenControllerBeingPressed:currentTouches]) [TouchPadGestureHandler handleGestureIn:streamView with:event];
if(multiTouchesDetected) return;
if([touches containsObject:touchLockedForMouseMove]){
mousePointerMoved = true;
[self sendMouseMoveEvent:touchLockedForMouseMove];
CGPoint currentLocation = [touchLockedForMouseMove locationInView:streamView];
bool isAdjacentPoints = [self isAdjacentPoints:initialMousePointerLocation from:currentLocation tolerance:currentSettings.singleTapSensitivity.doubleValue];
if(!mousePointerMoved && !isAdjacentPoints){
mousePointerMoved = true;
}
[self sendMouseMoveEvent:currentLocation];
}
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
if(isInMouseWheelScrollingMode){
isInMouseWheelScrollingMode = false;
if(TouchPadGestureHandler.ctrlDown) LiSendKeyboardEvent(CommandManager.keyboardButtonMappings[@"CTRL"].shortValue,KEY_ACTION_UP,0);
[TouchPadGestureHandler startInertialScroll];
if(multiTouchesDetected){
if([UITouchUtil touchesIn:streamView from:event].count == touches.count){
multiTouchesDetected = false;
}
return;
}
if([UITouchUtil touchesIn:streamView from:event].count == touches.count) multiTouchesDetected = false;
if([touches containsObject:touchLockedForMouseMove]){
// dealing with a single first tap, whether the button will be released, is going to be decided in sendLongMouseLeftButtonClickEvent
if(!mousePointerMoved && !self->quickTapDetected) [self sendLongMouseLeftButtonClickEvent];
@@ -230,24 +219,23 @@ static const float QUICK_TAP_TIME_INTERVAL = 0.2;
mousePointerMoved = false;
}
if([[event allTouches] count] == [touches count]){
isInMouseWheelScrollingMode = false;
for(UITouch* touch in touches){
[OnScreenControls.touchesCapturedByOnScreenControls removeObject:touch];
}
if([UITouchUtil touchesIn:streamView from:event].count == [touches count]){
touchLockedForMouseMove = nil;
mousePointerMoved = false; // need to reset this anyway
[self resetAllPressedFlagsForOnScreenWidgetViews]; // reset all pressed flag for on-screen widget views after all fingers lifted from screen.
}
touchPointSpawnedAtUpperScreenEdge = false;
}
- (void)sendMouseMoveEvent:(UITouch* )touch{
- (void)sendMouseMoveEvent:(CGPoint)currentLocation{
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
if(self->touchPointSpawnedAtUpperScreenEdge) return; // we're done here. this touch event will not be sent to the remote PC.
CGPoint currentLocation = [touch locationInView:self->streamView];
if (!self->firstTouchMoved) {
bool isAdjacentPoints = [self isAdjacentPoints:self->initialMousePointerLocation from:currentLocation tolerance:self->currentSettings.relativeTouchSlideThreshold.floatValue];
if (!self->firstTouchMoved && !isAdjacentPoints) {
self->latestMousePointerLocation = currentLocation;
self->firstTouchMoved = true;
}
@@ -259,37 +247,40 @@ static const float QUICK_TAP_TIME_INTERVAL = 0.2;
int deltaY = (currentLocation.y - self->latestMousePointerLocation.y) * 1.35 * self->currentSettings.mousePointerVelocityFactor.floatValue;
if (deltaX != 0 || deltaY != 0) {
LiSendMouseMoveEvent(deltaX, deltaY);
self->latestMousePointerLocation = currentLocation;
if(self->touchPointSpawnedAtUpperScreenEdge) return; // we're done here. this touch event will not be sent to the remote PC.
if(self->firstTouchMoved) LiSendMouseMoveEvent(deltaX, deltaY);
}
}
});
}
// this will turn into a dragging anytime...
- (void)sendLongMouseLeftButtonClickEvent{
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
// if (!self->isDragging){
Log(LOG_D, @"Sending left mouse button press");
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
// Wait 100 ms to simulate a real button press
usleep(QUICK_TAP_TIME_INTERVAL * 1000000);
if(!self->quickTapDetected){
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
}
// else NSLog(@"Left mouse button release cancelled, keep pressing down, turning into dragging...");
dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(QUICK_TAP_TIME_INTERVAL * NSEC_PER_SEC));
dispatch_after(delay, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if(!self->quickTapDetected){
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
}
// else NSLog(@"Left mouse button release cancelled, keep pressing down, turning into dragging...");
});
// do not release the button if we're still dragging, this will prevent the dragging from being interrupted.
});
}
- (void)sendShortMouseLeftButtonClickEvent{
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
usleep(50 * 1000);
dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC));
dispatch_after(delay, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
usleep(50 * 1000);
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
dispatch_after(delay, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
});
});
}
+147
View File
@@ -0,0 +1,147 @@
//
// SafeTimer.swift
// VoidLink
//
// Created by True砖家 on 2025/9/9.
// Copyright © 2025 True砖家@Bilibili. All rights reserved.
//
import Foundation
import QuartzCore
@objc(SafeTimer)
final class SafeTimer: NSObject {
private var timer: DispatchSourceTimer?
private let timerQueue: DispatchQueue // 私有串行队列
private let timerQueueKey = DispatchSpecificKey<UInt8>()
private let userHandler: () -> Void
private let interval: TimeInterval
private let delay: TimeInterval
private var shouldRunHandler: Bool = false // 控制逻辑上的暂停/恢复
private var isCleaned: Bool = false // 是否已经 clean
@objc private(set) var remainingMinimumRunCount: Int = 0
private var shouldCleanAfterMinimumRuns: Bool = false
@objc init(interval: TimeInterval = 1.0,
delay: TimeInterval = 0,
queueLabel: String = "com.example.safetimer",
handler: @escaping () -> Void) {
self.interval = interval
self.delay = delay
self.userHandler = handler
self.timerQueue = DispatchQueue(label: queueLabel)
self.timerQueue.setSpecific(key: timerQueueKey, value: 1)
super.init()
let t = DispatchSource.makeTimerSource(queue: timerQueue)
t.schedule(deadline: .now() + delay, repeating: interval)
t.setEventHandler { [weak self] in
guard let self = self, !self.isCleaned else { return }
if !self.shouldRunHandler { return }
self.userHandler()
if self.remainingMinimumRunCount > 0 {
self.remainingMinimumRunCount -= 1
}
if self.shouldCleanAfterMinimumRuns && self.remainingMinimumRunCount == 0 {
self.cleanOnTimerQueue(force: true)
}
}
t.resume() // timer 一开始就 resume,避免 suspend/resume 的计数问题
self.timer = t
}
deinit {
cleanForDeinit()
}
/// 开始逻辑上的计时
@objc func start() {
timerQueue.async { [weak self] in
guard let self = self, !self.isCleaned else { return }
self.shouldRunHandler = true
}
}
/// 暂停逻辑上的计时
@objc func pause() {
timerQueue.async { [weak self] in
guard let self = self, !self.isCleaned else { return }
self.shouldRunHandler = false
}
}
/// 重置下一次触发时间,并开始执行 handler
@objc func restart() {
restart(minimumRunCount: 1)
}
/// 重置下一次触发时间,并保证 clean 前至少执行 minimumRunCount 次 handler
@objc(restartWithMinimumRunCount:)
func restart(minimumRunCount: Int = 1) {
timerQueue.async { [weak self] in
guard let self = self, let t = self.timer, !self.isCleaned else { return }
self.remainingMinimumRunCount = max(0, minimumRunCount)
self.shouldCleanAfterMinimumRuns = false
t.schedule(deadline: .now() + self.delay, repeating: self.interval)
self.shouldRunHandler = true
}
}
@objc func isRunning() -> Bool {
if DispatchQueue.getSpecific(key: timerQueueKey) != nil {
return shouldRunHandler
}
return timerQueue.sync {
shouldRunHandler && !isCleaned
}
}
/// 彻底清理 timer;如果 restart(minimumRunCount:) 尚未跑够次数,则延后到最低次数满足后清理
@objc func clean() {
if DispatchQueue.getSpecific(key: timerQueueKey) != nil {
cleanOnTimerQueue()
return
}
// 同步在 timerQueue 执行,保证已排队事件全部处理完(或跳过)
timerQueue.sync {
cleanOnTimerQueue()
}
}
private func cleanForDeinit() {
if DispatchQueue.getSpecific(key: timerQueueKey) != nil {
cleanOnTimerQueue(force: true)
return
}
timerQueue.sync {
cleanOnTimerQueue(force: true)
}
}
private func cleanOnTimerQueue(force: Bool = false) {
guard !isCleaned else { return }
if !force && shouldRunHandler && remainingMinimumRunCount > 0 {
shouldCleanAfterMinimumRuns = true
return
}
isCleaned = true
shouldRunHandler = false
remainingMinimumRunCount = 0
shouldCleanAfterMinimumRuns = false
if let t = timer {
t.setEventHandler {} // 清空 handler
t.cancel() // 取消 timer
timer = nil
}
}
}
+34 -17
View File
@@ -13,9 +13,10 @@
#import "ControllerSupport.h"
#import "OnScreenControls.h"
#import "VoidLink-Swift.h"
#import "StreamConfiguration.h"
@class PencilHandler;
@protocol UserInteractionDelegate <NSObject>
- (void)userInteractionBegan;
@@ -24,35 +25,41 @@
- (void)toggleStatsOverlay;
- (void)toggleMouseCapture;
- (void)toggleMouseVisible;
- (void)expandSettingsView;
- (void)disconnectAndQuitApp;
@end
#if TARGET_OS_TV
@interface StreamView : UIView <X1KitMouseDelegate, UITextFieldDelegate>
#else
@interface StreamView : UIView <X1KitMouseDelegate, UITextFieldDelegate, UIPointerInteractionDelegate>
#endif
@interface StreamView : UIView
@property (assign, nonatomic) UIView* streamFrameTopLayerView;
@property (weak, nonatomic) UIView* streamFrameTopLayerView;
@property (weak, nonatomic) id<UserInteractionDelegate> interactionDelegate;
@property (assign, nonatomic) CGFloat streamAspectRatio;
@property (assign, nonatomic) CGRect originalFrame;
@property (assign, nonatomic) bool widgetToolOpened;
@property (strong, nonatomic) OnScreenControls* onScreenControls;
@property (weak, nonatomic) PencilHandler* pencilHandler;
- (void) setupStreamView:(ControllerSupport*)controllerSupport
interactionDelegate:(id<UserInteractionDelegate>)interactionDelegate
config:(StreamConfiguration*)streamConfig
streamFrameTopLayerView:(UIView* )topLayerView
- (void) setupStreamViewWithControllerSupport:(ControllerSupport*)controllerSupport
interactionDelegate:(id<UserInteractionDelegate>)interactionDelegate
streamConfig:(StreamConfiguration*)streamConfig
gameProfile:(OSCProfile* )profile
streamFrameTopLayerView:(UIView* )topLayerView
;
- (void) showOnScreenControls;
- (void)cleanUp;
- (void) reloadLegacyWidgets:(OSCProfile* )profile;
- (void) setOnScreenControls;
- (void) disableOnScreenControls;
- (void) reloadOnScreenControlsRealtimeWith:(ControllerSupport*)controllerSupport
andConfig:(StreamConfiguration*)streamConfig;
- (void) reloadOnScreenControlsRealtimeWithControllerSupport:(ControllerSupport*)controllerSupport
streamConfig:(StreamConfiguration*)streamConfig;
- (void) reloadOnScreenControlsWith:(ControllerSupport*)controllerSupport
andConfig:(StreamConfiguration*)streamConfig;
- (void) clearOnScreenWidgets;
- (void) reloadOnScreenWidgetViews;
- (void) reloadGameProfile:(OSCProfile* )profile reloadWidgets:(bool)reloadWidgets;
- (void) saveStreamingGameProfileChanges;
- (bool) isOnScreenWidgetEnabled;
- (CGSize) getVideoAreaSize;
- (CGPoint) adjustCoordinatesForVideoArea:(CGPoint)point;
@@ -60,11 +67,21 @@
- (OnScreenControlsLevel) getCurrentOscState;
-(void)readyToBringUpSoftKeyboardByToolbox;
- (void)readyToBringUpSoftKeyboardByToolbox;
- (void)keyboardWillShow:(NSNotification *)notification;
- (void)keyboardWillHide;
- (void)handleNonStandardKeyboard:(NSNotification *)notification;
- (void)liftMetalVideoViewIfNeeded:(CGFloat)liftHeight;
- (void)alterAbsTouchDragWith:(int32_t)mouseButton;
- (void)enablePencilHover;
- (void)disablePencilHover;
- (void)setAllowSingleTouchEnabled:(BOOL)enabled;
- (void)toggleTouchDisabled:(bool)disabled;
- (void)startInteractionTimer;
#if !TARGET_OS_TV
- (void) updateCursorLocation:(CGPoint)location isMouse:(BOOL)isMouse;
#endif
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,72 @@
//
// TouchPadGestureHandler.swift
// VoidLink
//
// Created by True砖家 on 2025/11/5.
// Copyright © 2025 True砖家 on Bilibili. All rights reserved.
//
import UIKit
@objc class TouchPadGestureHandler: NSObject {
@objc public static var ctrlDown:Bool = false
@objc public static var enablePinch:Bool = true
@objc public static var ctrlDownForPinch:Bool = true
@objc public static var enableHorizontalScroll:Bool = true
@objc public static var scrollSensitivity:CGFloat = 1.0
@objc public static var pinchSensitivity:CGFloat = 1.0
@objc public static var displayLinkRate:CGFloat = 60
private static var inertialScroller: InertialScroller = InertialScroller(decelerationRate: displayLinkRate > 110 ? 0.96 : 0.9, displayLinkRate: displayLinkRate) {
if ctrlDown {return}
LiSendHighResScrollEvent(Int16(inertialScroller.vector.dy*7*scrollSensitivity))
if TouchPadGestureHandler.enableHorizontalScroll {LiSendHighResHScrollEvent(Int16(-inertialScroller.vector.dx*7*scrollSensitivity))}
}
@objc public static func startInertialScroll(){
inertialScroller.timer?.restart()
}
@objc public static func handleGesture(in view: UIView, with event: UIEvent) {
inertialScroller.timer?.pause()
let currentTouches = UITouchUtil.touches(in: view, from: event)
guard currentTouches.count == 2 else { return }
LiSendMouseButtonEvent(CChar(BUTTON_ACTION_RELEASE), BUTTON_LEFT)
LiSendMouseButtonEvent(CChar(BUTTON_ACTION_RELEASE), BUTTON_RIGHT)
guard let touch1 = currentTouches.first else { return }
var mutable = Array(currentTouches)
mutable.removeAll { $0 == touch1 }
guard let touch2 = mutable.first else { return }
let currentDistance = UITouchUtil.distance(between: touch1, and: touch2, in: view)
let previousDistance = UITouchUtil.previousDistance(between: touch1, and: touch2, in: view)
let midPointVector = UITouchUtil.midPointVector(between: touch1, and: touch2, in: view)
let midPointDeltaX = midPointVector.dx
let midPointDeltaY = midPointVector.dy
let sendHorizontalScroll = abs(midPointDeltaX) > 1.2*abs(midPointDeltaY)
inertialScroller.vector = CGVector(dx: sendHorizontalScroll ? midPointDeltaX : 0, dy: midPointDeltaY)
let originalPinchDelta = currentDistance-previousDistance;
let pinchDelta = enablePinch ? originalPinchDelta*7*pinchSensitivity : 0;
LiSendHighResScrollEvent(Int16(pinchDelta + midPointDeltaY*7*scrollSensitivity))
if enableHorizontalScroll, sendHorizontalScroll {LiSendHighResHScrollEvent(Int16(-midPointDeltaX*7*scrollSensitivity))}
if enablePinch, ctrlDownForPinch {
let midPointDelta = hypot(midPointDeltaX, midPointDeltaY)
if abs(originalPinchDelta) > midPointDelta*1.3, midPointDelta < 2 {
LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["CTRL"]!, CChar(KEY_ACTION_DOWN), 0)
ctrlDown = true
} else {
LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["CTRL"]!, CChar(KEY_ACTION_UP), 0)
ctrlDown = false
}
}
}
}
+51
View File
@@ -0,0 +1,51 @@
//
// UITouchUtil.swift
// VoidLink
//
// Created by True砖家 on 2025/11/5.
// Copyright © 2025 True砖家 on Bilibili. All rights reserved.
//
import UIKit
@objc class UITouchUtil: NSObject {
@objc public static func touches(in view: UIView, from event: UIEvent?) -> Set<UITouch> {
guard let touches = event?.allTouches else { return [] }
return touches.filter {
$0.view === view && ($0.type == .direct || $0.type == .pencil)
}
}
@objc public static func distance(between touch1: UITouch?, and touch2: UITouch?, in view: UIView?) -> CGFloat {
guard let view = view, let touch1 = touch1, let touch2 = touch2 else { return 0 }
let p1 = touch1.location(in: view)
let p2 = touch2.location(in: view)
return hypot(p1.x - p2.x, p1.y - p2.y)
}
@objc public static func previousDistance(between touch1: UITouch?, and touch2: UITouch?, in view: UIView?) -> CGFloat {
guard let view = view, let touch1 = touch1, let touch2 = touch2 else { return 0 }
let p1 = touch1.previousLocation(in: view)
let p2 = touch2.previousLocation(in: view)
return hypot(p1.x - p2.x, p1.y - p2.y)
}
@objc public static func vector(of touch: UITouch?, in view: UIView?) -> CGVector {
guard let view = view, let touch = touch else { return CGVectorMake(0,0) }
let p2 = touch.location(in: view)
let p1 = touch.previousLocation(in: view)
return CGVectorMake(p2.x - p1.x, p2.y - p1.y)
}
@objc public static func midPointVector(between touch1: UITouch?, and touch2: UITouch?, in view: UIView?) -> CGVector {
guard let view = view, let touch1 = touch1, let touch2 = touch2 else { return CGVector(dx: 0, dy: 0)}
let vector1 = vector(of: touch1, in: view)
let vector2 = vector(of: touch2, in: view)
let deltaX = (vector1.dx+vector2.dx)/2
let deltaY = (vector1.dy+vector2.dy)/2
return CGVector(dx: deltaX, dy: deltaY)
}
}
+4
View File
@@ -7,6 +7,7 @@
//
#import "HapticContext.h"
#include <stdint.h>
@import GameController;
@import CoreHaptics;
@@ -14,6 +15,8 @@
@import CoreMotion;
#endif
typedef NS_ENUM(uint8_t, ControllerHardware);
@interface VoidController : NSObject
typedef struct {
@@ -69,6 +72,7 @@ typedef NS_ENUM(NSInteger, ControllerDeviceBatteryState) {
@property(nonatomic, strong) NSMutableSet* _Nullable motionTypes;
@property(nonatomic, assign) uint16_t reportRateHz;
@property(nonatomic, assign) uint16_t controllerNumber;
@property(nonatomic, assign) ControllerHardware hardware;
@property(nonatomic, assign) BOOL hasAccelerometer;
@property(nonatomic, assign) BOOL hasGyroscope;
+23 -15
View File
@@ -2,8 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>GCSupportsGameMode</key>
<true/>
<key>CADisableMinimumFrameDuration</key>
<true/>
<key>CADisableMinimumFrameDurationOnPhone</key>
@@ -63,22 +61,19 @@
<string>_nvstream._tcp</string>
</array>
<key>NSLocalNetworkUsageDescription</key>
<string>Moonlight uses the local network to connect to your gaming PC for streaming.</string>
<string>VoidLink uses the local network to connect to your gaming PC for streaming.</string>
<key>NSMicrophoneUsageDescription</key>
<string>麦克风访问权限允许VoidLink 实现麦克风重定向。麦克风重定向仅支持Sunshine基地版。
VoidLink uses microphone to stream your voice to PC host. Microphone redirection works with Sunshine-Foundation (a fork of Sunshine) only.</string>
<key>NSMotionUsageDescription</key>
<string>Moonlight uses the device Gyro and Acceleration data to emulate motion controls when streaming.</string>
<string>VoidLink uses the device Gyro and Acceleration data to emulate motion controls when streaming.</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleExternalDisplay</key>
<array>
<dict>
<key>UISceneDelegateClassName</key>
<string>SceneDelegate</string>
<key>UISceneConfigurationName</key>
<string>External Display Configuration</string>
</dict>
</array>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
@@ -88,9 +83,16 @@
<string>SceneDelegate</string>
</dict>
</array>
<key>UIWindowSceneSessionRoleExternalDisplay</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>External Display Configuration</string>
<key>UISceneDelegateClassName</key>
<string>SceneDelegate</string>
</dict>
</array>
</dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
@@ -98,6 +100,8 @@
<array>
<string>audio</string>
</array>
<key>UIDesignRequiresCompatibility</key>
<false/>
<key>UILaunchStoryboardName</key>
<string>Launch Screen</string>
<key>UIMainStoryboardFile</key>
@@ -112,6 +116,10 @@
<false/>
<key>UIStatusBarHidden</key>
<true/>
<key>UISupportedExternalAccessoryProtocols</key>
<array>
<string>com.xiaoji.M2boot</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationLandscapeLeft</string>
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23788.4" systemVersion="24F74" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="24512" systemVersion="24G419" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
<entity name="App" representedClassName="App" syncable="YES" codeGenerationType="class">
<attribute name="hdrSupported" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="hidden" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
@@ -8,7 +8,9 @@
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
</entity>
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
<attribute name="activeAddressPool" optional="YES" attributeType="Binary" syncable="YES"/>
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="controllerNavigationHighlightedAppID" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="ipv6Address" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
@@ -20,53 +22,112 @@
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
</entity>
<entity name="InputAccessoryBarButton" representedClassName="InputAccessoryBarButton" syncable="YES" codeGenerationType="class">
<attribute name="buttonLabel" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="cmdString" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="index" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
</entity>
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
<attribute name="appTheme" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="asyncFrameDequeue" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<attribute name="asyncNativeTouchPriority" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="audioConfig" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="NO" syncable="YES"/>
<attribute name="audioEngine" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="backroundSessionTimer" optional="YES" attributeType="Integer 32" defaultValueString="32767" usesScalarValueType="NO" syncable="YES"/>
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
<attribute name="btMouseSupport" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="emulatedControllerType" optional="YES" attributeType="Integer 16" defaultValueString="2" usesScalarValueType="NO" syncable="YES"/>
<attribute name="buttonVisualFeedback" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<attribute name="controllerGyroBiasX" optional="YES" attributeType="Double" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="controllerGyroBiasY" optional="YES" attributeType="Double" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="controllerGyroBiasZ" optional="YES" attributeType="Double" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="controllerMouseExpo" optional="YES" attributeType="Float" defaultValueString="1.8" usesScalarValueType="NO" syncable="YES"/>
<attribute name="controllerMouseLeftButton" attributeType="Integer 32" defaultValueString="8" usesScalarValueType="NO" syncable="YES"/>
<attribute name="controllerMousePointerVelocity" optional="YES" attributeType="Float" defaultValueString="15" usesScalarValueType="NO" syncable="YES"/>
<attribute name="controllerMouseRightButton" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
<attribute name="controllerMouseStick" attributeType="Integer 32" defaultValueString="8388640" usesScalarValueType="NO" syncable="YES"/>
<attribute name="ctrlDownForPinch" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<attribute name="customLocalRadialMenuButtonPosition" attributeType="Integer 32" defaultValueString="16777215" usesScalarValueType="NO" syncable="YES"/>
<attribute name="customStreamingRadialMenuButtonPosition" attributeType="Integer 32" defaultValueString="16777215" usesScalarValueType="NO" syncable="YES"/>
<attribute name="delayLeftClick" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<attribute name="deviceGyroMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="duckOtherApps" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="edgeSlidingSensitivity" optional="YES" attributeType="Float" defaultValueString="22" usesScalarValueType="NO" syncable="YES"/>
<attribute name="emulatedControllerType" optional="YES" attributeType="Integer 16" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
<attribute name="enableControllerNavigation" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<attribute name="enableFrameTimebase" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="enableGraphs" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="enablePinch" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<attribute name="enablePIP" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<attribute name="enableYUV444" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="externalDisplayMode" optional="YES" attributeType="Integer 16" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
<attribute name="frameQueueSize" optional="YES" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="NO" syncable="YES"/>
<attribute name="framePacingMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="framePacingMode" optional="YES" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
<attribute name="frameQueueSize" optional="YES" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
<attribute name="fullColorRange" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<attribute name="globeAsEscape" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<attribute name="graphOpacity" optional="YES" attributeType="Integer 32" defaultValueString="50" usesScalarValueType="NO" syncable="YES"/>
<attribute name="gyroBiasX" optional="YES" attributeType="Double" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="gyroBiasY" optional="YES" attributeType="Double" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="gyroBiasZ" optional="YES" attributeType="Double" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="gyroMode" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="gyroSensitivity" optional="YES" attributeType="Float" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
<attribute name="graphOpacity" optional="YES" attributeType="Integer 32" defaultValueString="50" usesScalarValueType="NO" syncable="YES"/>
<attribute name="hapticEngine" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
<attribute name="keyboardToggleFingers" optional="YES" attributeType="Integer 16" defaultValueString="3" usesScalarValueType="NO" syncable="YES"/>
<attribute name="interpolationMaximumDimension" optional="YES" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
<attribute name="interpolationMaximumPixelCount" optional="YES" attributeType="Integer 32" defaultValueString="921600" usesScalarValueType="NO" syncable="YES"/>
<attribute name="keyboardToggleFingers" optional="YES" attributeType="Integer 16" defaultValueString="99" usesScalarValueType="NO" syncable="YES"/>
<attribute name="leftClickDelayMs" optional="YES" attributeType="Float" defaultValueString="15" usesScalarValueType="NO" syncable="YES"/>
<attribute name="liftStreamViewForKeyboard" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<attribute name="localMousePointerMode" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="localRadialMenuButton" attributeType="Integer 32" defaultValueString="8388608" usesScalarValueType="NO" syncable="YES"/>
<attribute name="localVolume" optional="YES" attributeType="Float" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
<attribute name="micVolume" optional="YES" attributeType="Float" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
<attribute name="mousePointerVelocityFactor" optional="YES" attributeType="Float" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
<attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="muteInBackground" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
<attribute name="optimizeGames" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<attribute name="oscLayoutToolFingers" optional="YES" attributeType="Integer 16" defaultValueString="4" usesScalarValueType="NO" syncable="YES"/>
<attribute name="oscLayoutToolFingers" optional="YES" attributeType="Integer 16" defaultValueString="99" usesScalarValueType="NO" syncable="YES"/>
<attribute name="passthroughGestures" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="pencilTickIntervalUs" optional="YES" attributeType="Float" defaultValueString="2000" usesScalarValueType="NO" syncable="YES"/>
<attribute name="pencilTickMode" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="pencilTipOffsetX" optional="YES" attributeType="Float" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="pencilTipOffsetY" optional="YES" attributeType="Float" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="pinchSensitivity" optional="YES" attributeType="Float" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
<attribute name="playAudioOnPC" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="pointerVelocityModeDivider" optional="YES" attributeType="Float" defaultValueString="0.5" usesScalarValueType="NO" syncable="YES"/>
<attribute name="preferredCodec" attributeType="Integer 32" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<attribute name="preferredCodec" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="YES" syncable="YES"/>
<attribute name="redirectMic" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="relativeTouchSlideThreshold" optional="YES" attributeType="Float" defaultValueString="6" usesScalarValueType="NO" syncable="YES"/>
<attribute name="rememberFoldState" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<attribute name="renderingBackend" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="resolutionSelected" optional="YES" attributeType="Integer 16" defaultValueString="4" usesScalarValueType="NO" syncable="YES"/>
<attribute name="reverseMouseWheelDirection" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<attribute name="scrollSensitivity" optional="YES" attributeType="Float" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
<attribute name="sdrPerformanceWorkaround" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="sendDummyEvent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<attribute name="settingsMenuMode" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="settingsMenuOffset" optional="YES" attributeType="Float" defaultValueString="22" usesScalarValueType="NO" syncable="YES"/>
<attribute name="settingsMenuWidth" attributeType="Float" defaultValueString="370" usesScalarValueType="NO" syncable="YES"/>
<attribute name="showKeyboardToolbar" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="showKeyboardToolbar" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<attribute name="singleTapSensitivity" optional="YES" attributeType="Double" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
<attribute name="slideToSettingsDistance" optional="YES" attributeType="Float" defaultValueString="0.2" usesScalarValueType="NO" syncable="YES"/>
<attribute name="slideToSettingsScreenEdge" optional="YES" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="NO" syncable="YES"/>
<attribute name="softKeyboardHeight" attributeType="Float" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="statsOverlayEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="statsOverlayLevel" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="renderingBackend" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="streamDimensionScale" optional="YES" attributeType="Double" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
<attribute name="streamingRadialMenuButton" attributeType="Integer 32" defaultValueString="16" usesScalarValueType="NO" syncable="YES"/>
<attribute name="streamingRadialMenuDelay" optional="YES" attributeType="Float" defaultValueString="0.5" usesScalarValueType="NO" syncable="YES"/>
<attribute name="swapABXYButtons" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="touchMode" optional="YES" attributeType="Integer 16" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
<attribute name="touchMoveEventInterval" optional="YES" attributeType="Integer 16" defaultValueString="320" usesScalarValueType="NO" syncable="YES"/>
<attribute name="touchMoveEventInterval" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="touchPointerVelocityFactor" optional="YES" attributeType="Float" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
<attribute name="touchPointTracking" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
<attribute name="unlockDisplayOrientation" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="useBuiltinMic" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
</entity>
</model>
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More