258 Commits
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 bd9dccd834 Merge branch 'iOS26UI' into Integration 2026-03-14 14:15:28 +08:00
True Zhuanjia 2410222c19 Merge branch 'iOS26UI' into Integration 2026-03-14 11:50:48 +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 4dd1aad123 Merge branch 'iOS26UI' into Integration 2026-03-12 10:19:47 +08:00
True Zhuanjia d8ac06ddca dummy commit 2026-03-12 09:56:39 +08:00
168 changed files with 54045 additions and 8120 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/"
+1
View File
@@ -4,6 +4,7 @@
.DS_Store
Build
DerivedData
build-home/
*.xccheckout
.github
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"/>
+295 -64
View File
@@ -7,6 +7,7 @@
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 */; };
@@ -19,34 +20,47 @@
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 */; };
18951D1D2EBB6D2300A86E12 /* TouchPadGestureHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18951D1C2EBB6D2300A86E12 /* TouchPadGestureHandler.swift */; };
189AD29A2DE7446000E24B88 /* HostCollectionViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 189AD2992DE7446000E24B88 /* HostCollectionViewController.m */; };
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 */; };
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 */; };
@@ -55,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 */; };
@@ -97,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 */; };
@@ -128,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 */; };
@@ -144,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 */
@@ -172,6 +190,7 @@
/* 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>"; };
@@ -183,6 +202,8 @@
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>"; };
@@ -190,38 +211,38 @@
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>"; };
18951D1C2EBB6D2300A86E12 /* TouchPadGestureHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchPadGestureHandler.swift; 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>"; };
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>"; };
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>"; };
@@ -236,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>"; };
@@ -316,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>"; };
@@ -376,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>"; };
@@ -410,26 +435,52 @@
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 */,
180345E12E819877004AF36F /* libopus-sim.a in Frameworks */,
182760F72E83E2EA00EC1C5C /* DequeModule in Frameworks */,
182760FD2E83E68B00EC1C5C /* Collections in Frameworks */,
FB290CF419B2C406004C83CF /* CoreGraphics.framework in Frameworks */,
@@ -459,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>";
@@ -468,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 */ = {
@@ -505,8 +550,8 @@
children = (
84FD0B4F2C313343006B5BE5 /* LocalizationHelper.h */,
84FD0B502C31338D006B5BE5 /* LocalizationHelper.m */,
84E1CDF92C4FE83400547904 /* LocalizationHelperExtension.swift */,
84FD0B532C3134A6006B5BE5 /* Localizable.xcstrings */,
84E1CDF92C4FE83400547904 /* SwiftLocalizationHelper.swift */,
);
path = Localization;
sourceTree = "<group>";
@@ -590,6 +635,7 @@
98AB2E7F1CAD46830089BB98 /* moonlight-common.xcodeproj */,
FB290CF919B2C406004C83CF /* VoidLink */,
FB1A67542132419700507771 /* Moonlight TV */,
2A039E572F72892B00546768 /* VoidLinkPreview */,
FB290CF019B2C406004C83CF /* Frameworks */,
FB290CEF19B2C406004C83CF /* Products */,
);
@@ -599,6 +645,7 @@
isa = PBXGroup;
children = (
FB290CEE19B2C406004C83CF /* VoidLink.app */,
2A039E562F72892B00546768 /* VoidLinkPreview.app */,
);
name = Products;
sourceTree = "<group>";
@@ -606,6 +653,7 @@
FB290CF019B2C406004C83CF /* Frameworks */ = {
isa = PBXGroup;
children = (
A76B00042FFCC00100000004 /* ExternalAccessory.framework */,
18780CC62F10D66000B90483 /* StoreKit.framework */,
98882A032AF60F5300C5A11C /* libavcodec.a */,
98882A052AF60F5300C5A11C /* libavformat.a */,
@@ -658,13 +706,11 @@
18341A022DEB65AB0094E5F2 /* FixedTintImageView.h */,
18341A032DEB662C0094E5F2 /* FixedTintImageView.m */,
FB290D0319B2C406004C83CF /* AppDelegate.m */,
848F45682DDCD0D600581E36 /* HostCardView.h */,
848F45692DDCD12500581E36 /* HostCardView.m */,
848F45692DDCD12500581E36 /* HostCardView.swift */,
2A7113FC2F5A6C65001D852D /* ThemeManager.swift */,
847D2FD82DDA00C800937A62 /* MenuSectionView.h */,
847D2FDA2DDA015F00937A62 /* MenuSectionView.m */,
FBDE86E419F82297001C18A8 /* UIAppView.h */,
FBDE86E519F82297001C18A8 /* UIAppView.m */,
847D2FDA2DDA015F00937A62 /* MenuSectionView.swift */,
2A7113FE2F5A6C65001D852D /* UIAppView.swift */,
2A7114002F5A6C65001D852D /* GamepadNavigationIllustrationHud.swift */,
180E6E362E93BFDB0067C73A /* AlertControllerUtil.swift */,
1888C1902E24C2C800108730 /* AboutView.swift */,
1888C1912E24C33100108730 /* AboutViewController.swift */,
@@ -710,6 +756,7 @@
isa = PBXGroup;
children = (
84351B052C29ACAF00A77989 /* CustomOSC */,
A76B00022FFCC00100000002 /* GameSirG8MFiRumbler.swift */,
FB89460A19F646E200339C8A /* ControllerSupport.h */,
FB89460B19F646E200339C8A /* ControllerSupport.m */,
FB89460C19F646E200339C8A /* StreamView.h */,
@@ -737,15 +784,20 @@
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;
@@ -778,6 +830,7 @@
FB89461B19F646E200339C8A /* StreamManager.m */,
FB89461C19F646E200339C8A /* VideoDecoderRenderer.h */,
FB89461D19F646E200339C8A /* VideoDecoderRenderer.m */,
A1F100002FFB000100000001 /* FrameInterpolator.swift */,
9803CCAB254F9EAF00EE185E /* ConnectionCallbacks.h */,
);
path = Stream;
@@ -788,6 +841,8 @@
children = (
18729EDD2E8BD68800101194 /* UUIDHelper.swift */,
2AC1E3822F507D80004F76C0 /* GenericUtils.swift */,
2A263D4E2FF3BA2800154DEA /* PublicUtils.swift */,
2A263D492FF3B3F800154DEA /* PublicExtensions.swift */,
18A0EE2B2EFBCCFD00BA853A /* GraphicUtils.swift */,
18780CC92F10D6CC00B90483 /* IAPManager.swift */,
97E20DD42E2164A9000D5DCA /* BandwidthTracker.h */,
@@ -819,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 */,
@@ -938,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" */;
@@ -965,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;
};
@@ -1002,6 +1083,7 @@
projectRoot = "";
targets = (
FB290CED19B2C406004C83CF /* VoidLink */,
2A039E552F72892B00546768 /* VoidLinkPreview */,
);
};
/* End PBXProject section */
@@ -1024,6 +1106,13 @@
/* End PBXReferenceProxy section */
/* Begin PBXResourcesBuildPhase section */
2A039E542F72892B00546768 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
FB290CEC19B2C406004C83CF /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@@ -1036,17 +1125,30 @@
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 */,
@@ -1062,33 +1164,41 @@
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 */,
@@ -1096,6 +1206,7 @@
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 */,
@@ -1106,12 +1217,13 @@
187F4B282E04658A00EECC59 /* WidgetPanelStackView.m in Sources */,
FB4678FA1A55FFAD00377732 /* DiscoveryManager.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 */,
@@ -1122,7 +1234,7 @@
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 */,
@@ -1132,22 +1244,23 @@
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 */,
@@ -1206,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 = {
@@ -1329,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",
@@ -1368,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 = "";
@@ -1395,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",
@@ -1434,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 = "";
@@ -1451,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 = (
@@ -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>
BIN
View File
Binary file not shown.
+59 -9
View File
@@ -53,7 +53,7 @@ public struct AboutView: View {
}
// 说明文字
Text(SwiftLocalizationHelper.localizedString(forKey: "From the player community, to the player community."))
Text(LocalizationHelper.localizedString(forKey: "From the player community, to the player community."))
.multilineTextAlignment(.center)
.font(Font.caption.italic())
.lineLimit(nil)
@@ -61,19 +61,65 @@ public struct AboutView: View {
.padding()
Text(SwiftLocalizationHelper.localizedString(forKey: "VoidLink delivers better performance now!"))
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: "Learn more & join us"), destination: URL(string: SwiftLocalizationHelper.localizedString(forKey: "supportLink"))!)
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(.top, 10)
Spacer()
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(SwiftLocalizationHelper.localizedString(forKey: "OK")) {
Button(LocalizationHelper.localizedString(forKey: "OK")) {
aboutVC.dismiss(animated:true)
}
.padding()
@@ -81,11 +127,15 @@ public struct AboutView: View {
.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)
}
}
@@ -96,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()
@@ -105,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()
+28 -3
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: 430)
// ⚠️ 一定要设置以下样式才会生效 preferredContentSize
modalPresentationStyle = .formSheet
}
override func viewDidAppear(_ animated: Bool) {
ControllerNavigator.setUINavigationDelegate(self)
}
override func viewDidDisappear(_ animated: Bool) {
ControllerNavigator.restorePreviousUINavigationDelegate(ifCurrentDelegateIs: self)
}
}
func isIPhone() -> Bool {
+120 -9
View File
@@ -8,6 +8,61 @@
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:
@@ -21,10 +76,12 @@ import UIKit
@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,
in viewController: UIViewController? = nil,
title: String?,
message: String?,
withCancel: Bool,
@@ -34,16 +91,32 @@ import UIKit
completion: (() -> Void)? = nil
) {
var remainingSeconds = countdown
let originalMessage = message
var isAlertDismissed = false
alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
AlertControllerUtil.completion = completion
let confirmAction = UIAlertAction(title: "\(remainingSeconds)", style: .default) { _ in
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: SwiftLocalizationHelper.localizedString(forKey: cancelButtonString), style: .cancel) { _ in
let cancelAction = UIAlertAction(title: LocalizationHelper.localizedString(forKey: cancelButtonString), style: .cancel) { _ in
isAlertDismissed = true
actionCancelled = true
completion?()
cancelButtonString = "Cancel"
@@ -55,32 +128,70 @@ import UIKit
if buttonTitle != "" && !autoCompletion {alertController.addAction(confirmAction)}
if(countdown == 0) {
confirmAction.setValue(buttonTitle, forKey: "title")
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(), repeating: 1.0, leeway: .milliseconds(50))
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()
confirmAction.isEnabled = true
confirmAction.setValue(buttonTitle, forKey: "title")
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 {
confirmAction.setValue("\(remainingSeconds)", forKey: "title")
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.
+9 -2
View File
@@ -59,7 +59,8 @@ typedef NS_ENUM(NSInteger, ControllerMouseStick) {
typedef NS_ENUM(NSInteger, FramePacingMode) {
FramePacingModeOff,
FramePacingModeLegacy,
FramePacingModeQueue
FramePacingModeQueue,
FramePacingModeInterpolation
};
typedef NS_ENUM(NSInteger, SettingsMenuMode) {
@@ -129,6 +130,9 @@ typedef NS_ENUM(NSInteger, PencilTickMode) {
graphOpacity:(NSInteger)graphOpacity
renderingBackend:(NSInteger)renderingBackend
framePacingMode:(NSInteger)framePacingMode
interpolationMaximumDimension:(NSInteger)interpolationMaximumDimension
interpolationMaximumPixelCount:(NSInteger)interpolationMaximumPixelCount
streamDimensionScale:(CGFloat)streamDimensionScale
sendDummyEvent:(BOOL)sendDummyEvent
rememberFoldState:(BOOL)rememberFoldState
singleTapSensitivy:(CGFloat)singleTapSensitivy
@@ -146,13 +150,16 @@ typedef NS_ENUM(NSInteger, PencilTickMode) {
leftClickDelayMs:(CGFloat)leftClickDelayMs
settingsMenuOffset:(CGFloat)settingsMenuOffset
passthroughGestures:(BOOL)passthroughGestures
mapControllerToMouse:(BOOL)mapControllerToMouse
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;
+14 -3
View File
@@ -106,6 +106,9 @@
graphOpacity:(NSInteger)graphOpacity
renderingBackend:(NSInteger)renderingBackend
framePacingMode:(NSInteger)framePacingMode
interpolationMaximumDimension:(NSInteger)interpolationMaximumDimension
interpolationMaximumPixelCount:(NSInteger)interpolationMaximumPixelCount
streamDimensionScale:(CGFloat)streamDimensionScale
sendDummyEvent:(BOOL)sendDummyEvent
rememberFoldState:(BOOL)rememberFoldState
singleTapSensitivy:(CGFloat)singleTapSensitivy
@@ -123,13 +126,16 @@
leftClickDelayMs:(CGFloat)leftClickDelayMs
settingsMenuOffset:(CGFloat)settingsMenuOffset
passthroughGestures:(BOOL)passthroughGestures
mapControllerToMouse:(BOOL)mapControllerToMouse
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;
@@ -190,6 +196,9 @@
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];
@@ -206,9 +215,12 @@
settingsToSave.leftClickDelayMs = [NSNumber numberWithFloat:leftClickDelayMs];
settingsToSave.settingsMenuOffset = [NSNumber numberWithFloat:settingsMenuOffset];
settingsToSave.passthroughGestures = passthroughGestures;
settingsToSave.mapControllerToMouse = mapControllerToMouse;
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];
}];
@@ -364,4 +376,3 @@
}
@end
+1
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
+3 -1
View File
@@ -45,6 +45,7 @@
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;
@@ -89,7 +90,7 @@
NSError *error;
parentHost.activeAddressPool = [NSKeyedArchiver archivedDataWithRootObject:self.activeAddressPool requiringSecureCoding:YES error:&error];
}
else [self.activeAddressPool addObject:self.activeAddress];
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);
@@ -101,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];
}
+14 -3
View File
@@ -45,6 +45,9 @@
@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,
@@ -74,6 +77,7 @@
@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;
@@ -95,6 +99,9 @@
@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;
@@ -103,15 +110,19 @@
@property (nonatomic, retain) NSNumber * controllerMouseLeftButton;
@property (nonatomic, retain) NSNumber * controllerMouseRightButton;
@property (nonatomic, retain) NSNumber * controllerMouseStick;
@property (nonatomic, retain) NSNumber * controllerMouseSwitch;
@property (nonatomic) BOOL mapControllerToMouse;
@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;
+17 -3
View File
@@ -55,6 +55,9 @@
self.graphOpacity = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"graphOpacity"]];
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) {
@@ -119,6 +122,7 @@
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;
@@ -134,11 +138,17 @@
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;
self.edgeSlidingSensitivity = settings.edgeSlidingSensitivity;
@@ -157,18 +167,22 @@
self.ctrlDownForPinch = settings.ctrlDownForPinch;
self.settingsMenuOffset = settings.settingsMenuOffset;
self.passthroughGestures = settings.passthroughGestures;
self.mapControllerToMouse = settings.mapControllerToMouse;
self.enableControllerNavigation = settings.enableControllerNavigation;
self.controllerMouseLeftButton = settings.controllerMouseLeftButton;
self.controllerMouseRightButton = settings.controllerMouseRightButton;
self.controllerMouseSwitch = settings.controllerMouseSwitch;
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
@@ -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;
- (void)updateTheme;
@end
-644
View File
@@ -1,644 +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 "VoidLink-Swift.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];
}
- (void)updateBackgroundLayerTheme{
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
];
}
- (void)createBackgroundLayer{
backgroundLayer = [CAGradientLayer layer];
[self updateBackgroundLayerTheme];
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 {
self.backgroundColor = ThemeManager.widgetBackgroundColor;
_hostNameLabel.textColor = ThemeManager.textColor;
[_appButton setTitleColor:ThemeManager.appPrimaryColor forState:UIControlStateNormal];
_separatorLine.backgroundColor = ThemeManager.hostCardSeparatorColor;
backgroundLayer.hidden = NO;
[self updateBackgroundLayerTheme];
[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 ThemeManager.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.offlineHostIconBackgroundColor;
_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,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,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.
+1
View File
@@ -7,6 +7,7 @@
//
#import "StreamView.h"
#import "TemporarySettings.h"
NS_ASSUME_NONNULL_BEGIN
@@ -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)
}
}
+260 -131
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")
}
@@ -51,6 +55,29 @@ import UIKit
@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,
"MLEFT" : BUTTON_LEFT,
@@ -118,21 +145,25 @@ import UIKit
"OSCBACK"
]
@objc public static let touchPadCmds: [String] = ["LSVPAD", "RSVPAD", "LSPAD", "RSPAD", "LSWHEEL", "RSWHEEL", "LTPAD", "RTPAD", "DS4TOUCH", "MOUSEPAD", "ABSMOUSE", "MOUSEWHEEL", "WHEEL", "DISCRETEWHEEL", "DSWHEEL", "DPAD", "TRACKBALL", "WASDPAD", "ARROWPAD"]
@objc public static let mousePadWithButtonActions: [String] = ["MOUSEPAD", "ABSMOUSE"]
@objc public static let mousePad: [String] = ["MOUSEPAD", "ABSMOUSE", "TRACKBALL"]
@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 displacementBasedStickPads: [String] = ["LSPAD", "RSPAD"]
@objc public static let stickWheels: [String] = ["LSWHEEL", "RSWHEEL"]
@objc public static let vectorTouchPads: [String] = ["LSVPAD", "RSVPAD", "MOUSEPAD", "TRACKBALL"]
@objc public static let inertialTouchPads: [String] = ["LSVPAD", "RSVPAD", "TRACKBALL"]
@objc public static let verticalTouchPads: [String] = ["LTPAD", "RTPAD", "MOUSEWHEEL", "WHEEL", "DISCRETEWHEEL", "DSWHEEL"]
@objc public static let bidirectionalVerticalTouchPads: [String] = ["LTPAD", "RTPAD", "MOUSEWHEEL", "WHEEL", "DISCRETEWHEEL", "DSWHEEL"]
@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",
@@ -140,30 +171,48 @@ import UIKit
"SOFTKEYBOARD",
"ABSTCHDRAG",
"FOLDER",
"LABELEDFOLDER",
"PRESSURECURVE",
"PENCILHOVER",
"BRUSH",
"ERASER",
"NOSINGLETOUCH"
"NOSINGLETOUCH",
"DISABLETILT",
"DISABLETOUCH",
"GAMEPADOVERLAY",
]
@objc public static let functionalTouchPadCmds: [String] = [
"MAGNIFIER"
]
@objc public static let pencilProButtonCmds: [String] = [
"PENCILHOVER",
"BRUSH",
"ERASER",
"NOSINGLETOUCH"
"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"]
@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,
"BRUSH": 0xFF,
"ERASER": 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
@@ -299,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] = []
@@ -423,17 +492,17 @@ import UIKit
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)
@@ -449,7 +518,8 @@ import UIKit
@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.mouseButtonMappings.keys))
.union(Set(CommandManager.shortcutAllowedFunctionalButtonMappings.keys)))
let keys = mergedKeys.joined(separator: "|")
let pattern = "^(?:(\(keys))(?:\\+(\(keys))*)*)$"
@@ -476,7 +546,9 @@ import UIKit
for key in keyStrings {
if (CommandManager.keyboardButtonMappings.keys.contains(key)
|| CommandManager.mouseButtonMappings.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")
@@ -490,7 +562,7 @@ import UIKit
}
for (index, key) in validKeyStrings.enumerated() {
print("Valid Key \(index): \(key)")
// print("Valid Key \(index): \(key)")
}
return validKeyStrings
@@ -499,6 +571,23 @@ import UIKit
//super combo key button strings
@objc public func extractCmdStrings(from input: String) -> [String]? {
var validCmdStrings: [String] = []
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)
}
}
else {
let input = input.uppercased()
let combinedStrings = [CommandManager.keyboardButtonMappings.keys.map { $0 as String },
CommandManager.oscButtonMappings.keys.map { $0 as String },
@@ -534,8 +623,6 @@ import UIKit
return nil
}
var validCmdStrings: [String] = []
for key in cmdStrings {
validCmdStrings.append(key)
}
@@ -545,10 +632,11 @@ import UIKit
return nil
}
}
for (index, key) in validCmdStrings.enumerated() {
print("Valid Key \(index): \(key)")
}
return validCmdStrings
}
@@ -575,6 +663,29 @@ import UIKit
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") {
do {
@@ -583,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]")
@@ -600,9 +712,26 @@ import UIKit
}
}
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 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 let cmdStrings = cmdStrings else { return }
guard var cmdStrings = cmdStrings else { return }
cmdStrings = cmdStrings.filter { !CommandManager.shortcutAllowedFunctionalButtonMappings.keys.contains($0)}
if releaseOnly {
for keyStr in cmdStrings.reversed() {
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 (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 ()
@@ -89,9 +88,8 @@
[self._dPadBackground addSublayer:self._leftButton];
}
NSLog(@"layer name ttttttttttt============================");
for(CALayer* buttonLayer in self.OSCButtonLayerPool){
NSLog(@"layer name %@", buttonLayer.name);
// NSLog(@"layer name %@", buttonLayer.name);
}
[self resizeControllerLayerWith:self._dPadBackground and:_dPadSizeFactor];
@@ -138,43 +136,56 @@
*/
horizontalGuideline.backgroundColor = [UIColor blueColor]; // change horizontal guideline back to blue if it doesn't line up with one of the on screen buttons
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
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) updateGuidelinesForLegacyOnScreenControls{
+21 -6
View File
@@ -22,11 +22,11 @@ typedef NS_ENUM(NSInteger, MapGyroTo) {
driftCorrection
};
typedef NS_ENUM(NSInteger, PencilHoverMode) {
HoverDisabled,
HoverPencil,
HoverMouse,
HoverBoth
typedef NS_ENUM(NSInteger, PencilAndHoverMode) {
hoverDisabled,
pencilOnly,
pencilToMouse,
pencilToTouch
};
@@ -41,6 +41,8 @@ typedef NS_ENUM(NSInteger, PencilHoverMode) {
@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;
@@ -58,7 +60,20 @@ typedef NS_ENUM(NSInteger, PencilHoverMode) {
@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;
@@ -68,7 +83,7 @@ typedef NS_ENUM(NSInteger, PencilHoverMode) {
@property NSString *squeezeEndShortcut;
@property (nonatomic, assign) bool pencilPausesNativeTouch;
@property (nonatomic, assign) bool disablePencilSlideGestures;
@property (nonatomic, assign) PencilHoverMode pencilHoverMode;
@property (nonatomic, assign) PencilAndHoverMode pencilAndHoverMode;
+61 -5
View File
@@ -34,6 +34,8 @@
[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"];
@@ -51,7 +53,19 @@
[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"];
@@ -61,7 +75,7 @@
[encoder encodeObject:self.squeezeEndShortcut forKey:@"squeezeEndShortcut"];
[encoder encodeBool:self.pencilPausesNativeTouch forKey:@"pencilPausesNativeTouch"];
[encoder encodeBool:self.disablePencilSlideGestures forKey:@"disablePencilSlideGestures"];
[encoder encodeInt64:self.pencilHoverMode forKey:@"pencilHoverMode"];
[encoder encodeInt64:self.pencilAndHoverMode forKey:@"pencilHoverMode"];
}
- (id) initWithCoder:(NSCoder*)decoder {
@@ -91,6 +105,8 @@
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;
@@ -106,8 +122,21 @@
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"] : ControllerButtonNull;
self.controllerGyroSwitchToggle = [decoder containsValueForKey:@"controllerGyroSwitchToggle"] ? [decoder decodeIntForKey:@"controllerGyroSwitchToggle"] : ControllerButtonNull;
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:
@@ -117,6 +146,21 @@
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;
@@ -159,7 +203,7 @@
self.pencilPausesNativeTouch = [decoder containsValueForKey:@"pencilPausesNativeTouch"] ? [decoder decodeBoolForKey:@"pencilPausesNativeTouch"] : false;
self.disablePencilSlideGestures = [decoder containsValueForKey:@"disablePencilSlideGestures"] ? [decoder decodeBoolForKey:@"disablePencilSlideGestures"] : false;
self.pencilHoverMode = [decoder containsValueForKey:@"pencilHoverMode"] ? [decoder decodeInt64ForKey:@"pencilHoverMode"] : HoverPencil;
self.pencilAndHoverMode = [decoder containsValueForKey:@"pencilHoverMode"] ? [decoder decodeInt64ForKey:@"pencilHoverMode"] : pencilOnly;
}
return self;
@@ -172,6 +216,8 @@
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;
@@ -189,7 +235,17 @@
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
@@ -199,7 +255,7 @@
copy.squeezeEndShortcut = [self.squeezeEndShortcut mutableCopy]; // NSString → NSMutableString
copy.pencilPausesNativeTouch = self.pencilPausesNativeTouch;
copy.disablePencilSlideGestures = self.disablePencilSlideGestures;
copy.pencilHoverMode = self.pencilHoverMode;
copy.pencilAndHoverMode = self.pencilAndHoverMode;
return copy;
}
@@ -1,85 +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"
#import "DataManager.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;
@property (atomic, assign) WidgetSizeTransition widgetSizeTransition;
+ (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;
- (uint32_t) getIndexOfLastProfile;
/**
* Returns the index of the 'selected' profile within the array it's in
*/
- (NSInteger) getIndexOfSelectedProfile;
- (NSMutableArray *) getEncodedProfiles;
// - (NSData* )getSelectedEncodedProfile;
- (void) importEncodedProfiles:(NSMutableArray* )profilesEncoded;
- (OnScreenButtonState *)unarchiveButtonStateEncoded:(NSData *)data;
- (void) updateDefaultTemplates;
- (void) importDefaultTemplates;
#pragma mark - Setters
/**
* Sets the profile object with the particular index as the selected profile to be displayed on screen during game streaming
*/
- (void) setProfileToSelected:(uint32_t)index;
- (void) replaceSelectedProfileWith:(OSCProfile*)newProfile overwriteDefault:(bool)overwriteDefault;
- (void) replaceProfile:(OSCProfile*)oldProfile withProfile:(OSCProfile*)newProfile;
/**
* 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) duplicateSelectedProfileWithName:(NSString*)name;
- (bool) updateSelectedProfile:(NSMutableSet *) 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;
- (CGPoint)normalizeWidgetPosition:(CGPoint)position;
@end
NS_ASSUME_NONNULL_END
@@ -1,542 +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;
if(!CGRectEqualToRect(viewBounds, CGRectZero)) 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];
}
- (void)replaceSelectedProfileWith:(OSCProfile*)newProfile overwriteDefault:(bool)overwriteDefault{
NSInteger index = 0;
NSMutableArray *profiles = [self getAllProfiles];
for (OSCProfile *profile in profiles) {
if (profile.isSelected == YES) {
index = [profiles indexOfObject:profile];
}
}
if(index>0) profiles[index] = newProfile;
else if(overwriteDefault) profiles[index] = newProfile;
NSMutableArray *profilesEncoded = [self encodedProfilesFromArray:profiles];
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 {
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
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)
/*
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];
*/
NSString *bundleId = [[NSBundle mainBundle] bundleIdentifier];
bool importPencilProProfile = ([bundleId isEqualToString:@"com.voidlink.iOS"]
|| [bundleId isEqualToString:@"com.voidlinkextreme.iOS"]
|| [bundleId isEqualToString:@"com.voidlink.tf.debug10.iOS"]);
if(!importPencilProProfile && [GenericUtils isIPad]){
[profilesEncoded removeObjectAtIndex:1];
}
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];
[self setProfileToSelected:0];
}
}
}
- (void) updateDefaultTemplates{
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];
if (fileData && !error) {
NSSet *classes = [NSSet setWithObjects:[NSString class], [NSMutableData class], [NSMutableArray class], [OSCProfile class], [OnScreenButtonState class], nil];
NSMutableArray *defaultProfilesEncoded = [NSKeyedUnarchiver unarchivedObjectOfClasses:classes fromData:fileData error:nil]; // Decode the encoded array itself, NOT the objects contained in the array
OSCProfile *defaultProfileDecoded;
NSMutableArray *targetProfilesDecoded = [self getAllProfiles];
OSCProfile *newPencilProProfile;
bool pencilProProfileExisted = false;
for (NSData *profileEncoded in defaultProfilesEncoded) {
defaultProfileDecoded = [NSKeyedUnarchiver unarchivedObjectOfClasses: classes fromData:profileEncoded error: nil];
OSCProfile *targetProfile = [self findProfileByName:defaultProfileDecoded.name inProfileArray:targetProfilesDecoded];
if([defaultProfileDecoded.name isEqualToString:@"Pencil Pro"]){
newPencilProProfile = [defaultProfileDecoded mutableCopy];
}
if(targetProfile){
if([targetProfile.name isEqualToString:@"Pencil Pro"]){
pencilProProfileExisted = true;
}
uint64_t targetIndex = [targetProfilesDecoded indexOfObject:targetProfile];
targetProfilesDecoded[targetIndex] = defaultProfileDecoded;
}
}
if(newPencilProProfile && !pencilProProfileExisted){
if(newPencilProProfile) [targetProfilesDecoded insertObject:newPencilProProfile atIndex:1];
}
NSMutableArray* targetPofilesEncoded = [self encodedProfilesFromArray:targetProfilesDecoded];
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:targetPofilesEncoded requiringSecureCoding:YES error:nil];
[[NSUserDefaults standardUserDefaults] setObject:data forKey:@"OSCProfiles"];
[[NSUserDefaults standardUserDefaults] synchronize];
[self setProfileToSelected:0];
}
}
}
#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 {
NSLog(@"getAllProfiles test %f", CACurrentMediaTime());
NSMutableArray *profiles = [self getAllProfiles];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
// NSString* persistedKey = @"widgetProfileUpdated-20251015";
NSString* persistedKey = @"widgetProfileUpdated-20260213";
BOOL needImportDefaultTemplates = [defaults objectForKey:persistedKey] == nil;
if(profiles.count == 0 || needImportDefaultTemplates){
if(profiles.count == 0) [self importDefaultTemplates];
else [self updateDefaultTemplates];
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:persistedKey];
[[NSUserDefaults standardUserDefaults] synchronize];
profiles = [self getAllProfiles];
}
for (OSCProfile *profile in profiles) {
if (profile.isSelected) {
return profile;
}
}
return profiles[0];
}
- (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
}
- (uint32_t) getIndexOfLastProfile {
NSMutableArray *profiles = [self getAllProfiles];
return (uint32_t)profiles.count-1; // 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:(uint32_t)tableIndex {
NSMutableArray *profiles = [self getAllProfiles];
[profiles enumerateObjectsUsingBlock:^(OSCProfile* profile, NSUInteger idx, BOOL *stop) {
profile.isSelected = idx == tableIndex;
}];
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:(NSMutableSet *) 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
*/
OSCProfile *selectedProfile = [self getSelectedProfile];
selectedProfile.buttonStatesEncoded = buttonStatesEncoded;
selectedProfile.unfoldedExclusiveFolderSequence = OnScreenWidgetView.unfoldedExclusiveFolderSequence;
selectedProfile.postExclusiveUnfoldedSequences = OnScreenWidgetView.postExclusiveUnfoldedSequences;
[self replaceSelectedProfileWith:selectedProfile overwriteDefault:NO];
return true;
}
- (void) duplicateSelectedProfileWithName:(NSString*)name {
if ([self profileNameAlreadyExist:name]) return;
else { // otherwise encode then add the new profile to the end of the OSCProfiles array
NSMutableArray *profiles = [self getAllProfiles];
OSCProfile* newProfile = nil;
for(OSCProfile* profile in profiles){
if(profile.isSelected){
profile.isSelected = false;
newProfile = [profile mutableCopy];
newProfile.isSelected = true;
newProfile.name = name;
}
}
if(newProfile){
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];
}
}
}
- (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;
}
NSLog(@"layoutToolView 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:(NSMutableSet *) 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) {
if(oscButtonLayer.isHidden) continue;
CGPoint normalizedPosition = [self normalizeWidgetPosition:oscButtonLayer.position];
OnScreenButtonState *buttonState = [[OnScreenButtonState alloc] initWithButtonName:oscButtonLayer.name buttonType:LegacyOnScreenControls 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:
_widgetSizeTransition = keepWidgetSize;
for(OnScreenWidgetView* widgetView in OnScreenWidgetViews){
CGPoint normalizedPosition = [self normalizeWidgetPosition:widgetView.storedCenter];
OnScreenButtonState *buttonState = [[OnScreenButtonState alloc] initWithButtonName:widgetView.cmdString buttonType:CustomOnScreenWidget andPosition:normalizedPosition];
buttonState.alias = widgetView.widgetLabel;
buttonState.sequence = widgetView.sequence;
buttonState.sequenceSet = widgetView.sequenceSet;
buttonState.parentSequence = widgetView.parentSequence;
buttonState.folded = widgetView.folded;
buttonState.revealMode = widgetView.revealMode;
buttonState.widthFactor = [self normalizeSizeWidthFactorWith:widgetView];
buttonState.heightFactor = [self normalizeSizeHeightFactorWith:widgetView];
buttonState.componentSizeFactor = [self normalizeComponentSizeFactorWith:widgetView];
buttonState.backgroundAlpha = widgetView.backgroundAlpha;
buttonState.labelAlpha = widgetView.labelAlpha;
buttonState.borderAlpha = widgetView.borderAlpha;
buttonState.highlightAlpha = widgetView.highlightAlpha;
buttonState.borderWidth = widgetView.borderWidth;
buttonState.highlightSizeFactor = widgetView.highlightSizeFactor;
buttonState.autoTapInterval = widgetView.autoTapInterval;
buttonState.vibrationStyle = widgetView.vibrationStyle;
buttonState.mouseButtonAction = widgetView.mouseButtonAction;
buttonState.sensitivityFactorX = widgetView.sensitivityFactorX;
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.stickIndicatorOffset = widgetView.stickIndicatorOffset;
buttonState.widgetShape = widgetView.shape;
buttonState.walkModeThreshold = widgetView.dWheelWalkModeThreshold;
buttonState.minStickOffset = widgetView.minStickOffset;
buttonState.buttonMode = widgetView.buttonMode;
NSData *buttonStateEncoded = [NSKeyedArchiver archivedDataWithRootObject:buttonState requiringSecureCoding:YES error:nil];
[buttonStatesEncoded addObject: buttonStateEncoded];
}
return buttonStatesEncoded;
}
- (CGFloat)getReferenceLen{
CGFloat screenWidthInPoints = CGRectGetWidth([[UIScreen mainScreen] bounds]);
CGFloat screenHeightInPoints = CGRectGetHeight([[UIScreen mainScreen] bounds]);
CGFloat longSideLen = fmax(screenWidthInPoints,screenHeightInPoints);
CGFloat referenceLen = longSideLen;
if(_widgetSizeTransition == keepWidgetSize) referenceLen = longSideLen;
if(_widgetSizeTransition == transitionWithOrientation) referenceLen = screenWidthInPoints;
return referenceLen;
}
- (CGFloat)normalizeSizeWidthFactorWith:(OnScreenWidgetView* )widget{
return widget.bounds.size.width/[self getReferenceLen] * 10000;
}
- (CGFloat)normalizeSizeHeightFactorWith:(OnScreenWidgetView* )widget{
return widget.bounds.size.height/[self getReferenceLen] * 10000;
}
- (CGFloat)normalizeComponentSizeFactorWith:(OnScreenWidgetView* )widget{
if(widget.isStickWheel) {
return widget.denormalizedComponentSizeFactor*widget.baselineDiameter/[self getReferenceLen] * 10000;
}
return 1;
}
#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], [NSSet class], [NSNumber 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]
}
}
@@ -26,9 +26,13 @@ NS_ASSUME_NONNULL_BEGIN
@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 buttonMode;
@@ -47,6 +51,7 @@ NS_ASSUME_NONNULL_BEGIN
@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
@@ -59,7 +64,15 @@ NS_ASSUME_NONNULL_BEGIN
@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;
@@ -34,16 +34,22 @@
[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.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 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"];
@@ -57,6 +63,7 @@
[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"];
@@ -68,6 +75,10 @@
[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 {
@@ -75,16 +86,22 @@
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.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.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"];
@@ -98,6 +115,7 @@
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 containsValueForKey:@"backgroundAlpha"] ? [decoder decodeFloatForKey:@"backgroundAlpha"] : 0.5;
@@ -109,6 +127,12 @@
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;
}
+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;
+124 -23
View File
@@ -18,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];
@@ -43,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];
@@ -61,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;
@@ -69,21 +75,108 @@
}
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;
}
}
-(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);
}
}
}
@@ -110,9 +203,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;
@@ -121,8 +216,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];
};
@@ -171,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;
@@ -182,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];
};
File diff suppressed because it is too large Load Diff
+6 -2
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;
}
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);
+7 -7
View File
@@ -110,12 +110,12 @@ public class MicHandler: NSObject {
private static func showSettingsAlert() {
guard let topVC = topViewController() else { return }
let alert = UIAlertController(
title: SwiftLocalizationHelper.localizedString(forKey: "Microphone Permission") ,
message: SwiftLocalizationHelper.localizedString(forKey: "micPermissionTip"),
title: LocalizationHelper.localizedString(forKey: "Microphone Permission") ,
message: LocalizationHelper.localizedString(forKey: "micPermissionTip"),
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "Cancel"), style: .cancel, handler: nil))
alert.addAction(UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "Go to Settings") , style: .default, handler: { _ in
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)
@@ -190,7 +190,7 @@ public class MicHandler: NSObject {
// 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_MUSIC) // Set signal type
opus_encoder_ctl_wrapper(enc, Int32(OPUS_SET_SIGNAL_REQUEST), OPUS_SIGNAL_VOICE) // Set signal type
}
@objc public func startTapping() {
@@ -248,7 +248,7 @@ public class MicHandler: NSObject {
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))
sendMicrophoneData(packet, outBytes)
sendMicrophoneOpusData(packet, outBytes)
let removeCount = min(960, pcm16BufferDeque.count)
if removeCount > 0 {
pcm16BufferDeque.removeFirst(removeCount)
@@ -264,7 +264,7 @@ public class MicHandler: NSObject {
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))
sendMicrophoneData(packet, outBytes)
sendMicrophoneOpusData(packet, outBytes)
let removeCount = min(960, pcm16BufferArray.count)
if removeCount > 0 {
pcm16BufferArray.removeFirst(removeCount)
+189 -49
View File
@@ -39,7 +39,7 @@ import CoreMotion
}
public func gyroMixInputStarted() -> Bool {
return gyroIsWorking
return motionIsWorking
}
private static let sharedInstance = MotionHandler()
@@ -47,6 +47,8 @@ import CoreMotion
@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
@@ -59,14 +61,19 @@ import CoreMotion
return sharedInstance
}
@objc public var gyroControlStarted: Bool = false
private var gyroIsWorking: Bool = false
@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
@@ -77,7 +84,7 @@ import CoreMotion
@objc public var previousWidgetYawFactor:CGFloat = 1.0
@objc public var previousWidgetPitchFactor:CGFloat = 1.0
@objc public var previousWidgetRollFactor:CGFloat = 1.0
public var gyroStarter:Any?
public var motionStarter:Any?
private var windowScene: Any?
@objc public var onScreenControls: OnScreenControls?
@@ -87,7 +94,7 @@ import CoreMotion
private var yaw:Double = 0
private var pitch:Double = 0
private var roll:Double = 0
private var rollIntegral:Double = 0
private var leftStickMotion:Double = 0
private var isCalibrating: Bool = false
private var sumX: Double = 0
@@ -96,6 +103,9 @@ import CoreMotion
@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
@@ -122,7 +132,7 @@ import CoreMotion
}
@objc public override init() {
let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero)
// _ = OSCProfilesManager.sharedManager(CGRectZero)
super.init()
if #available(iOS 13.0, *) {
let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene
@@ -134,43 +144,75 @@ import CoreMotion
motionManager.gyroUpdateInterval = updateInterval
}
public func startGyroByOnScreenButton(_ sender: OnScreenWidgetView, yawFactor:CGFloat, pitchFactor:CGFloat, rollFactor:CGFloat){
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
print("self.widgetRollFactor \(self.widgetRollFactor)")
if self.gyroStarter == nil {
self.gyroStarter = sender
if sender.motionControlButtonString != "GYROPAUSE" {self.startGyroUpdate()}
if self.motionStarter == nil {
self.motionStarter = sender
if sender.motionControlButtonString != "GYROPAUSE" {self.startMotionUpdate()}
}
else if sender.motionControlButtonString == "GYROPAUSE" {
self.startGyroUpdate()
self.startMotionUpdate()
}
}
@objc public func startGyroByControllerButton(){
@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.startGyroUpdate()
self.startMotionUpdate()
}
/// 启动传感器数据更新
@objc public func startGyroUpdate() {
if !gyroControlStarted {
gyroControlStarted = true
@objc public func startMotionUpdate() {
if !motionControlStarted {
motionControlStarted = true
resetGravityYOffsetTracking()
// print("startGyroUpdate useBuiltinGyro \(useBuiltinGyro) \(CACurrentMediaTime())")
if useBuiltinGyro {
if motionManager.isGyroAvailable {
motionManager.startGyroUpdates(to: .main) { [weak self] gyroData, _ in
guard let self = self, let data = gyroData else { return }
self.handleGyroData(x: data.rotationRate.x,
y: data.rotationRate.y,
z: data.rotationRate.z)
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)
}
}
}
}
}
}
@@ -190,15 +232,19 @@ import CoreMotion
}
/// 停止更新
private var interruptNoneGyroInput:Bool = false
// private var interruptNoneGyroInput:Bool = false
@objc public func stopGyroUpdate(interruptNoneGyroInput:Bool=false, resetLeftStick:Bool=false) {
gyroControlStarted = false
gyroIsWorking = false
if motionManager.isGyroActive{
motionManager.stopGyroUpdates()
@objc public func stopMotionUpdate(interruptNoneGyroInput:Bool=false) {
motionControlStarted = false
motionIsWorking = false
resetGravityYOffsetTracking()
if motionManager.isDeviceMotionActive{
motionManager.stopDeviceMotionUpdates()
}
self.clearGyroInput(interruptNonGyroInput:interruptNoneGyroInput, resetLeftStick:resetLeftStick)
if #available(iOS 14.0, *) {
activeGCController?.motion?.sensorsActive = false
}
self.clearGyroInput(interruptNonGyroInput:interruptNoneGyroInput)
}
@objc public func stopAccelUpdate() {
@@ -215,7 +261,31 @@ import CoreMotion
// 在这里处理加速度数据,比如计算方向、存储或驱动逻辑
}
private func handleGyroData(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
@@ -226,6 +296,7 @@ import CoreMotion
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 {
@@ -283,14 +354,23 @@ import CoreMotion
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 !gyroControlStarted {
self.clearGyroInput(interruptNonGyroInput: interruptNoneGyroInput)
print("Gyro: stopped")
if !motionControlStarted {
self.clearGyroInput(interruptNonGyroInput: false)
// print("Gyro: stopped")
return
}
gyroIsWorking = true
motionIsWorking = true
if mapGyroTo == MapGyroTo.mapGyroToMouse {
yaw = yawSource*sensitvityYaw*widgetYawFactor*30
@@ -316,29 +396,36 @@ import CoreMotion
}
if rollToLeftStick {
roll = gyroInputToStickInput(input:rollSource*sensitvityRoll*widgetRollFactor*0.2)
rollIntegral = rollIntegral + roll
let mixedLeftStickOffsetX = self.clampStickInput(input: rollIntegral+leftStickTouchInputX+leftStickPhysicalInputX)
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)
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, resetLeftStick:Bool=false){
// guard let onScreenControls = onScreenControls else { return }
private func clearGyroInput(interruptNonGyroInput:Bool){
guard ControllerUtil.gamepadArrivalReported else { return }
if yawPitchToRightStick{
onScreenControls?.sendRightStickTouchPadEvent(rightStickPhysicalInputX+rightStickTouchInputX-yawBias, rightStickPhysicalInputY+rightStickTouchInputY-pitchBias)
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{
if resetLeftStick {
rollIntegral = 0
onScreenControls?.sendLeftStickTouchPadEvent(leftStickPhysicalInputX+leftStickTouchInputX-rollBias,leftStickPhysicalInputY+leftStickTouchInputY)
}
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()
@@ -346,6 +433,34 @@ import CoreMotion
}
}
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)
}
@@ -361,23 +476,48 @@ import CoreMotion
var sumX = 0.0, sumY = 0.0, sumZ = 0.0
var sampleCount = 0
motionManager.startGyroUpdates(to: .main) { [weak self] gyroData, _ in
guard let self = self, self.isCalibrating, let data = gyroData else { return }
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.stopGyroUpdates()
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()
}
+1 -1
View File
@@ -17,7 +17,7 @@ NS_ASSUME_NONNULL_BEGIN
@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
+9 -5
View File
@@ -11,6 +11,7 @@
#import "NativeTouchPointer.h"
#import "OnScreenControls.h"
#import "StreamView.h"
#import "VoidLink-Swift.h"
#include <Limelight.h>
@@ -48,10 +49,10 @@
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->activateCoordSelector = settings.pointerVelocityModeDivider.floatValue != 1.0;
self->activateCoordSelector = profile.pointerVelocityModeDivider != 1.0;
self->moveEventIntervalNSec = (int64_t)(settings.touchMoveEventInterval.intValue * 1000);;
self->streamViewBounds = view.bounds;
@@ -95,9 +96,9 @@
slideGestureVerticalThreshold = CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.4;
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;
}
@@ -195,6 +196,8 @@
- (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
@@ -217,7 +220,8 @@
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]]);
+1 -1
View File
@@ -21,7 +21,7 @@ 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;
+2 -2
View File
@@ -53,13 +53,13 @@ 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);
}
+10 -4
View File
@@ -18,7 +18,7 @@
#import <CoreMotion/CoreMotion.h>
#endif
#import "OnScreenButtonState.h"
#import "OSCProfilesManager.h"
#import "VoidLink-Swift.h"
#import "DataManager.h"
#define UPDATE_BUTTON(x, y) (buttonFlags = \
@@ -77,6 +77,7 @@ static NSSet *validPositionButtonNames;
BOOL _swapABXY;
BOOL _visualFeedbackEnabled;
BOOL _largerStickLR1;
BOOL _firstTapFromOsc;
CGFloat _oscTapExlusionAreaSizeFactor;
OSCProfilesManager *profilesManager;
NSMutableDictionary *_activeCustomOscButtonPositionDict;
@@ -187,6 +188,10 @@ static float L3_Y;
- (void) pressDownControllerButton: (int)flag{
[_controllerSupport setButtonFlag:_controller flags:flag];
[_controllerSupport updateFinished:_controller];
if(_firstTapFromOsc){
_firstTapFromOsc = false;
[_controllerSupport updateTimerStateForOsc];
}
}
- (void) releaseControllerButton: (int)flag{
@@ -300,6 +305,7 @@ static float L3_Y;
_activeCustomOscButtonPositionDict = [[NSMutableDictionary alloc] init];
touchesCapturedByOnScreenControls = [[NSMutableSet alloc] init];
_firstTapFromOsc = true;
if(![self isKindOfClass:[LayoutOnScreenControls class]]) OnScreenControls.shared = self;
@@ -354,7 +360,7 @@ static float L3_Y;
}
- (CGPoint) denormalizeWidgetPosition:(CGPoint)position {
if(position.x < 1.0 && position.y < 1.0) {
if(position.x < 1.01 && position.y < 1.01) {
position.x = position.x * _view.bounds.size.width;
position.y = position.y * _view.bounds.size.height;
// NSLog(@"denormalizing position: %f, %f", position.x, position.y);
@@ -913,7 +919,7 @@ static float L3_Y;
[buttonLayer.name isEqualToString:@"rightButton"] ||
[buttonLayer.name isEqualToString:@"downButton"] ||
[buttonLayer.name isEqualToString:@"leftButton"]){
// NSLog(@"layerName: %@, alpha: %f", buttonLayer.name, buttonStateDecoded.backgroundAlpha);
// NSLog(@"layerName: %@, opacity: %f", buttonLayer.name, buttonStateDecoded.backgroundAlpha);
[self adjustControllerLayerOpacityWith:buttonLayer and:buttonStateDecoded.backgroundAlpha];
}
if([buttonLayer.name isEqualToString:@"leftStickBackground"]){
@@ -2050,7 +2056,7 @@ static float L3_Y;
if (layer == self._rightStickBackground) {
self._rightStick.opacity = targetAlpha;
self._rightStickBackground.opacity = targetAlpha + 1.0f/6.0f;
NSLog(@"right stick init alpha: %f", targetAlpha);
NSLog(@"right stick init opacity: %f", targetAlpha);
}
if (layer == self._leftStickBackground){
File diff suppressed because it is too large Load Diff
+243 -53
View File
@@ -16,27 +16,50 @@ import UIKit
weak var streamView: UIView?
// private var tickTimer: SafeTimer
private var pencilInteraction: Any?
private var streamAspectRatio: Float
@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 hoverMode: PencilHoverMode = .HoverPencil
@objc static private(set) var pencilAndHoverMode: PencilAndHoverMode = .pencilOnly
private(set) var pencilProEnabled: Bool = false
private var isFirstMove: Bool = false
private var moveEventIndex: Int64 = 0
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 pressureLUT = PressureCurveLUT(curve: PressureCurve())
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
@@ -44,6 +67,8 @@ import UIKit
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()
@@ -80,7 +105,7 @@ import UIKit
}
// PencilHandler.autoHoverTermination = selectedProfile.autoPencilHoverTermination
PencilHandler.hoverMode = selectedProfile.pencilHoverMode
PencilHandler.pencilAndHoverMode = selectedProfile.pencilAndHoverMode
pressureCurveEnabled = selectedProfile.pressureCurveEnabled
@@ -88,6 +113,7 @@ import UIKit
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 {
@@ -96,12 +122,23 @@ import UIKit
}
}
let persistedCurvePoints = PressureCurve.importCurvePoints(selectedProfile.pressureCurvePoints)
let curve = PressureCurve()
curve.polylinePoints = persistedCurvePoints
curve.buildCurveSegments()
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.pressureLUT = PressureCurveLUT(curve: curve)
self.strokeLUT = PressureCurveLUT(curve: strokePressureCurve)
// self.initialTouchLUT = PressureCurveLUT(curve: initialTouchPressureCurve)
}
}
@@ -109,9 +146,17 @@ import UIKit
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let event = event else { return }
previousForce = 0
previousTargetForce = 0
PencilHandler.isDrawing = true
// isFirstMove = true
moveEventIndex = 0
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])
@@ -120,15 +165,26 @@ import UIKit
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])
}
moveEventIndex += 1
}
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])
@@ -139,6 +195,34 @@ import UIKit
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 {
@@ -191,9 +275,20 @@ import UIKit
func getTilt(fromAltitudeAngle altitudeAngle: Float) -> UInt8 {
let altitudeDegs = abs(Int16(altitudeAngle * (180.0 / .pi)))
return UInt8(90 - min(90, Int(altitudeDegs)))
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() }
@@ -202,29 +297,61 @@ import UIKit
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 location = self.adjustCoordinatesForVideoArea(point: point)
let videoSize = self.getVideoAreaSize()
let normalizedLocation = CGPoint(x: location.x/videoSize.width, y: location.y/videoSize.height)
let force = Float(touch.force/touch.maximumPossibleForce)/sin(Float(altitude))
var targetForce = pressureCurveEnabled ? self.pressureLUT.value(at: force) : force
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)
touchBeganForce = targetForce
case .moved:
eventType = UInt8(manualHoverFlag ? LI_TOUCH_EVENT_HOVER : LI_TOUCH_EVENT_MOVE)
targetForce = moveEventIndex < initialMoveEventIndexLimit ? touchBeganForce : targetForce
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)
}
@@ -241,20 +368,23 @@ import UIKit
tickMoment += (manualTick ? tickInterval : touch.timestamp - previousTimeStamp)
}
let sendableForce = targetForce
DispatchQueue.global().asyncAfter(deadline: dispatchMoment + tickMoment + delay) {
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)))
}
*/
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)))
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.hoverMode == .HoverDisabled || !PencilHandler.hoverSupported {
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 {
@@ -273,6 +403,10 @@ import UIKit
manualHoverFlag = !manualHoverFlag
}
@objc public func updatePencilTipOffset(x: CGFloat, y: CGFloat) {
pencilTipOffset = CGPoint(x: x, y: y)
}
@objc public func enablePencilHover(){
manualHoverFlag = true
}
@@ -347,24 +481,44 @@ import UIKit
}
}
@objc static public func enterDoubleTapShortcuts(in viewController: UIViewController){
let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero)
selectedProfile = oscProfileMan.getSelectedProfile()
guard let selectedProfile = selectedProfile else {return}
@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")
let alert = UIAlertController(title: SwiftLocalizationHelper.localizedString(forKey: "Eraser Shortcut"),
message: SwiftLocalizationHelper.localizedString(forKey: "Enter eraser keyboard shortcut:"),
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 = SwiftLocalizationHelper.localizedString(forKey:"Example: e, ctrl+e, alt+e ...")
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: SwiftLocalizationHelper.localizedString(forKey: "OK"), style: .default) { _ in
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 == "" {
@@ -373,8 +527,8 @@ import UIKit
enterBrushShortcut(in: viewController)
}
let learnMoreAction = UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "Learn More"), style: .default) { _ in
if let url = URL(string: SwiftLocalizationHelper.localizedString(forKey: "pencilKeyboardCmdURL")) {
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)
}
}
@@ -385,23 +539,35 @@ import UIKit
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: SwiftLocalizationHelper.localizedString(forKey: "Brush Shortcut"),
message: SwiftLocalizationHelper.localizedString(forKey: "Enter brush keyboard shortcut:"),
/*
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 = SwiftLocalizationHelper.localizedString(forKey:"Example: b, ctrl+b, alt+b ...")
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: SwiftLocalizationHelper.localizedString(forKey: "OK"), style: .default) { _ in
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 == "" {
@@ -418,8 +584,8 @@ import UIKit
}
}
let learnMoreAction = UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "Learn More"), style: .default) { _ in
if let url = URL(string: SwiftLocalizationHelper.localizedString(forKey: "pencilKeyboardCmdURL")) {
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)
}
}
@@ -430,30 +596,41 @@ import UIKit
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: SwiftLocalizationHelper.localizedString(forKey: "Squeeze Shortcut"),
message: SwiftLocalizationHelper.localizedString(forKey: "enterSqueezePressShort"),
let alert = UIAlertController(title: LocalizationHelper.localizedString(forKey: "Squeeze Shortcut"),
message: LocalizationHelper.localizedString(forKey: "enterSqueezePressShort"),
preferredStyle: .alert)
alert.addTextField { textField in
textField.placeholder = SwiftLocalizationHelper.localizedString(forKey:"Example: e, ctrl+e, alt+e ...")
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: SwiftLocalizationHelper.localizedString(forKey: "OK"), style: .default) { _ in
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 == "" {
@@ -462,8 +639,8 @@ import UIKit
enterSqueezeEndShortcut(in: viewController)
}
let learnMoreAction = UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "Learn More"), style: .default) { _ in
if let url = URL(string: SwiftLocalizationHelper.localizedString(forKey: "pencilKeyboardCmdURL")) {
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)
}
}
@@ -473,23 +650,35 @@ import UIKit
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: SwiftLocalizationHelper.localizedString(forKey: "Squeeze Shortcut"),
message: SwiftLocalizationHelper.localizedString(forKey: "enterSqueezeReleaseShort"),
/*
let alert = UIAlertController(title: LocalizationHelper.localizedString(forKey: "Squeeze Shortcut"),
message: LocalizationHelper.localizedString(forKey: "enterSqueezeReleaseShort"),
preferredStyle: .alert)
alert.addTextField { textField in
textField.placeholder = SwiftLocalizationHelper.localizedString(forKey:"Example: b, ctrl+b, alt+b ...")
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: SwiftLocalizationHelper.localizedString(forKey: "OK"), style: .default) { _ in
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 == "" {
@@ -506,8 +695,8 @@ import UIKit
}
}
let learnMoreAction = UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "Learn More"), style: .default) { _ in
if let url = URL(string: SwiftLocalizationHelper.localizedString(forKey: "pencilKeyboardCmdURL")) {
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)
}
}
@@ -518,6 +707,7 @@ import UIKit
viewController.present(alert, animated: true, completion: {
})
*/
}
private func attachHoverLeave(normalizedLocation:CGPoint){
@@ -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
}
}
+283 -53
View File
@@ -233,6 +233,9 @@ 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 {
@@ -342,6 +345,22 @@ final class PressureCurveView: UIView {
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)
@@ -355,6 +374,7 @@ final class PressureCurveView: UIView {
guard
let touch = touches.first
else { return }
newPointGenerated = true
let location = touch.location(in: self)
var point = unmap(location)
@@ -372,8 +392,15 @@ final class PressureCurveView: UIView {
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?) {
@@ -462,31 +489,45 @@ final class PressureCurveLUT {
}
}
extension UIView {
var parentViewController: UIViewController? {
var responder: UIResponder? = self
while let r = responder {
if let vc = r as? UIViewController {
return vc
}
responder = r.next
}
return nil
}
}
// MARK: - ViewController
class PressureCurveViewController: UIViewController {
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()
@@ -496,34 +537,39 @@ class PressureCurveViewController: UIViewController {
let curveWidth: CGFloat = 530
let curveHeight: CGFloat = 530
curveView = PressureCurveView(frame: CGRect(x: 0, y: 0, width: curveWidth, height: curveHeight))
curveView.center = self.view.center
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+50))
container.center = CGPoint(x: self.view.bounds.midX, y: self.view.bounds.midY-20)
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 = 2
container.layer.borderColor = ThemeManager.separatorColor.cgColor
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.setupNavigationBar()
self.view.addSubview(container)
curveView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
curveView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
curveView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
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(_:)),
@@ -536,17 +582,21 @@ class PressureCurveViewController: UIViewController {
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 = SwiftLocalizationHelper.localizedString(forKey: "No")
AlertControllerUtil.cancelButtonString = LocalizationHelper.localizedString(forKey: "No")
AlertControllerUtil.showAlert(
in: self,
title: SwiftLocalizationHelper.localizedString(forKey: "Pen Pressure Curve"),
message: SwiftLocalizationHelper.localizedString(forKey:"Reset the pen pressure curve by starting from the pen pressure test?"),
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: SwiftLocalizationHelper.localizedString(forKey: "Yes"),
buttonTitle: LocalizationHelper.localizedString(forKey: "Yes"),
countdown: 0,
action: {},
completion: {
@@ -573,11 +623,123 @@ class PressureCurveViewController: UIViewController {
private func loadPersistedCurve() {
let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero)
let persistedCurvePoints = PressureCurve.importCurvePoints(oscProfileMan.getSelectedProfile().pressureCurvePoints)
curveView.curve.polylinePoints = persistedCurvePoints
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()
@@ -589,9 +751,9 @@ class PressureCurveViewController: UIViewController {
if #available(iOS 13.0, *) {
let appearance = UINavigationBarAppearance()
appearance.configureWithTransparentBackground() // 透明背景
// appearance.configureWithTransparentBackground() // 透明背景
appearance.shadowColor = nil // 去掉底部细线
appearance.backgroundColor = .clear // 可以额外设置完全透明
appearance.backgroundColor = .white // 可以额外设置完全透明
navBar.standardAppearance = appearance
navBar.scrollEdgeAppearance = appearance
@@ -602,40 +764,56 @@ class PressureCurveViewController: UIViewController {
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: 42) // 导航栏标准高度
navBar.heightAnchor.constraint(equalToConstant: 50) // 导航栏标准高度
])
if curveView.testStage == .curveStage {
let resetButton = UIBarButtonItem(title: SwiftLocalizationHelper.localizedString(forKey: "Reset"), style: .plain, target: self, action: #selector(resetTapped))
let rangeTestButton = UIBarButtonItem(title: SwiftLocalizationHelper.localizedString(forKey: "Pressure-range-test"), style: .plain, target: self, action: #selector(pressureRangeTest))
let saveButton = UIBarButtonItem(title: SwiftLocalizationHelper.localizedString(forKey: "Save"), style: .plain, target: self, action: #selector(saveTapped))
let quitButton = UIBarButtonItem(title: SwiftLocalizationHelper.localizedString(forKey: "Exit"), style: .plain, target: self, action: #selector(exitTapped))
navItem.leftBarButtonItems = [resetButton, rangeTestButton]
navItem.rightBarButtonItems = [quitButton, saveButton]
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: SwiftLocalizationHelper.localizedString(forKey: "Pressure Range Test"))
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
}
}
}
// 4. 将 UINavigationItem 添加到导航栏
navBar.items = [navItem]
}
public func showCurveStageTips() {
AlertControllerUtil.showAlert(
in: self,
title: SwiftLocalizationHelper.localizedString(forKey: "Pen Pressure Curve"),
message: SwiftLocalizationHelper.localizedString(forKey:"Adjust the pressure curve by adding or dragging the purple squares."),
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: SwiftLocalizationHelper.localizedString(forKey: "This tip won't be shown again"),
buttonTitle: LocalizationHelper.localizedString(forKey: "This tip won't be shown again"),
countdown: 6
)
}
@@ -647,6 +825,12 @@ class PressureCurveViewController: UIViewController {
}
}
@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 {
@@ -656,9 +840,9 @@ class PressureCurveViewController: UIViewController {
AlertControllerUtil.showAlert(
in: self,
title: "",
message: SwiftLocalizationHelper.localizedString(forKey:"PencilProPackLowOSVersionTip"),
message: LocalizationHelper.localizedString(forKey:"PencilProPackLowOSVersionTip"),
withCancel: false,
buttonTitle: SwiftLocalizationHelper.localizedString(forKey: "OK"),
buttonTitle: LocalizationHelper.localizedString(forKey: "OK"),
countdown: 0)
}
}
@@ -666,10 +850,10 @@ class PressureCurveViewController: UIViewController {
@objc private func pressureRangeTest() {
AlertControllerUtil.showAlert(
in: self,
title: SwiftLocalizationHelper.localizedString(forKey: "Pressure Range Test"),
message: SwiftLocalizationHelper.localizedString(forKey:"pressureRangeTestTip"),
title: LocalizationHelper.localizedString(forKey: "Pressure Range Test"),
message: LocalizationHelper.localizedString(forKey:"pressureRangeTestTip"),
withCancel: true,
buttonTitle: SwiftLocalizationHelper.localizedString(forKey: "OK"),
buttonTitle: LocalizationHelper.localizedString(forKey: "OK"),
countdown: 0,
completion:{
if AlertControllerUtil.actionCancelled { return }
@@ -681,20 +865,53 @@ class PressureCurveViewController: UIViewController {
})
}
/*
@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: SwiftLocalizationHelper.localizedString(forKey: "Pen Pressure Curve"),
message: SwiftLocalizationHelper.localizedString(forKey:"firstPressureCurvePersistTip"),
title: LocalizationHelper.localizedString(forKey: "Pen Pressure Curve"),
message: LocalizationHelper.localizedString(forKey:"firstPressureCurvePersistTip"),
withCancel: false,
buttonTitle: SwiftLocalizationHelper.localizedString(forKey: "This tip won't be shown again"),
buttonTitle: LocalizationHelper.localizedString(forKey: "This tip won't be shown again"),
countdown: 11,
completion: {
PencilHandler.shared?.setupPressureLUT()
@@ -705,8 +922,8 @@ class PressureCurveViewController: UIViewController {
AlertControllerUtil.autoCompletion = true
AlertControllerUtil.showAlert(
in: self,
title: SwiftLocalizationHelper.localizedString(forKey: ""),
message: SwiftLocalizationHelper.localizedString(forKey:"Pressure curve saved"),
title: LocalizationHelper.localizedString(forKey: ""),
message: LocalizationHelper.localizedString(forKey:"Pressure curve saved"),
withCancel: false,
buttonTitle: "",
countdown: 1,
@@ -718,7 +935,9 @@ class PressureCurveViewController: UIViewController {
}
@objc private func saveTapped() {
saveButton.isEnabled = false
IAPManager.checkPurchaseInfo(.PencilProPack) { info in
self.saveButton.isEnabled = true
if info.valid {
self.persistCurve()
}
@@ -731,4 +950,15 @@ class PressureCurveViewController: UIViewController {
@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)
}
}
+1 -1
View File
@@ -16,7 +16,7 @@ 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
+9 -6
View File
@@ -11,6 +11,7 @@
#import "NativeTouchPointer.h"
#import "OnScreenControls.h"
#import "StreamView.h"
#import "VoidLink-Swift.h"
#include <Limelight.h>
@@ -47,11 +48,11 @@
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->activateCoordSelector = settings.pointerVelocityModeDivider.floatValue != 1.0;
self->activateCoordSelector = profile.pointerVelocityModeDivider != 1.0;
self->moveEventIntervalNSec = (int64_t)(settings.touchMoveEventInterval.intValue * 1000);;
self->streamViewBounds = view.bounds;
@@ -94,9 +95,9 @@
slideGestureVerticalThreshold = CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.4;
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;
}
@@ -194,6 +195,8 @@
- (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(!touch) return;
if([blacklistedTouches containsObject:@((uintptr_t)touch)]) return;
CGPoint targetCoords;
@@ -208,7 +211,8 @@
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]]);
@@ -334,4 +338,3 @@
}
@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) {}
}
+1
View File
@@ -8,6 +8,7 @@
#import "RelativeTouchHandler.h"
#import "DataManager.h"
#import "VoidLink-Swift.h"
#include <Limelight.h>
+60 -9
View File
@@ -13,12 +13,15 @@ import QuartzCore
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,
@@ -29,6 +32,7 @@ final class SafeTimer: NSObject {
self.delay = delay
self.userHandler = handler
self.timerQueue = DispatchQueue(label: queueLabel)
self.timerQueue.setSpecific(key: timerQueueKey, value: 1)
super.init()
@@ -39,6 +43,12 @@ final class SafeTimer: NSObject {
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 的计数问题
@@ -46,7 +56,7 @@ final class SafeTimer: NSObject {
}
deinit {
clean()
cleanForDeinit()
}
/// 开始逻辑上的计时
@@ -67,30 +77,71 @@ final class SafeTimer: NSObject {
/// 重置下一次触发时间,并开始执行 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 {
return shouldRunHandler;
if DispatchQueue.getSpecific(key: timerQueueKey) != nil {
return shouldRunHandler
}
/// 彻底清理 timer,返回后保证 handler 不再执行
return timerQueue.sync {
shouldRunHandler && !isCleaned
}
}
/// 彻底清理 timer;如果 restart(minimumRunCount:) 尚未跑够次数,则延后到最低次数满足后清理
@objc func clean() {
if isCleaned { return }
if DispatchQueue.getSpecific(key: timerQueueKey) != nil {
cleanOnTimerQueue()
return
}
// 同步在 timerQueue 执行,保证已排队事件全部处理完(或跳过)
timerQueue.sync {
self.isCleaned = true
self.shouldRunHandler = false
cleanOnTimerQueue()
}
}
if let t = self.timer {
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
self.timer = nil
}
timer = nil
}
}
}
+15 -13
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,15 +25,12 @@
- (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 (weak, nonatomic) UIView* streamFrameTopLayerView;
@property (weak, nonatomic) id<UserInteractionDelegate> interactionDelegate;
@@ -41,11 +39,11 @@
@property (assign, nonatomic) bool widgetToolOpened;
@property (strong, nonatomic) OnScreenControls* onScreenControls;
@property (weak, nonatomic) PencilHandler* pencilHandler;
@property (weak, nonatomic) UIViewController* streamFrameVC;
- (void) setupStreamView:(ControllerSupport*)controllerSupport
- (void) setupStreamViewWithControllerSupport:(ControllerSupport*)controllerSupport
interactionDelegate:(id<UserInteractionDelegate>)interactionDelegate
config:(StreamConfiguration*)streamConfig
streamConfig:(StreamConfiguration*)streamConfig
gameProfile:(OSCProfile* )profile
streamFrameTopLayerView:(UIView* )topLayerView
;
@@ -54,13 +52,13 @@
- (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:(bool)reload;
- (void) saveStreamViewWidgetChanges;
- (void) reloadGameProfile:(OSCProfile* )profile reloadWidgets:(bool)reloadWidgets;
- (void) saveStreamingGameProfileChanges;
- (bool) isOnScreenWidgetEnabled;
- (CGSize) getVideoAreaSize;
@@ -72,6 +70,7 @@
- (void)readyToBringUpSoftKeyboardByToolbox;
- (void)keyboardWillShow:(NSNotification *)notification;
- (void)keyboardWillHide;
- (void)handleNonStandardKeyboard:(NSNotification *)notification;
- (void)liftMetalVideoViewIfNeeded:(CGFloat)liftHeight;
- (void)alterAbsTouchDragWith:(int32_t)mouseButton;
@@ -79,6 +78,9 @@
- (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;
+330 -114
View File
@@ -17,7 +17,6 @@
#import "ControllerSupport.h"
#import "KeyboardSupport.h"
#import "VoidLink-Swift.h"
#import "OSCProfilesManager.h"
#import "NativeTouchPointer.h"
#import "NativeTouchHandler.h"
#import "PureNativeTouchHandler.h"
@@ -29,6 +28,15 @@
#import "StreamFrameViewController.h"
#if TARGET_OS_TV
@interface StreamView() <X1KitMouseDelegate, UITextFieldDelegate>
#else
@interface StreamView() <X1KitMouseDelegate, UITextFieldDelegate, UIPointerInteractionDelegate, InputAccessoryBarDelegate>
#endif
@property (weak, nonatomic) StreamFrameViewController* streamFrameVC;
@end
static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
/*
@@ -40,6 +48,8 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
KeyboardInputField* keyInputField;
BOOL isInputingText;
bool dockedKeyboardActionDetected;
bool isPencilHovering;
NSMutableSet* keysDown;
float streamAspectRatio;
@@ -57,7 +67,9 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
int localMousePointerMode;
TouchMode touchMode;
UIResponder* touchHandler;
UIResponder* sessionTouchHandler;
NSTimer* interactionTimer;
BOOL hasUserInteracted;
@@ -69,6 +81,8 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
#if defined(__IPHONE_16_1) || defined(__TVOS_16_1)
UIHoverGestureRecognizer *stylusHoverRecognizer;
#endif
CGFloat designatedSoftKeyboardHeight;
bool keyboardHeightDesignatedForLandscape;
CGFloat HeightViewLiftedTo;
UILabel* keyboardToggleTip;
@@ -78,10 +92,13 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
OSCProfilesManager* oscProfileMan;
}
- (void) setupStreamView:(ControllerSupport*)controllerSupport
- (void) setupStreamViewWithControllerSupport:(ControllerSupport*)controllerSupport
interactionDelegate:(id<UserInteractionDelegate>)interactionDelegate
config:(StreamConfiguration*)streamConfig
streamConfig:(StreamConfiguration*)streamConfig
gameProfile:(OSCProfile* )profile
streamFrameTopLayerView:(UIView* )topLayerView{
if(!self.streamFrameVC) self.streamFrameVC = (StreamFrameViewController* )[PublicUtils parentViewControllerForView:self];
self->comboKeyModifierFlags = (UIKeyModifierControl|UIKeyModifierAlternate|UIKeyModifierShift);
self->_streamFrameTopLayerView = topLayerView;
@@ -116,7 +133,10 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
keyboardToggleTip.layer.cornerRadius = 10;
keyboardToggleTip.clipsToBounds = true;
// if(settings.touchMode.intValue == NativeTouchOnly) [self addGestureRecognizer:keyboardToggleRecognizer]; //keep legacy approach in pure native mode
designatedSoftKeyboardHeight = settings.softKeyboardHeight * PublicUtils.screenHeight;
keyboardHeightDesignatedForLandscape = designatedSoftKeyboardHeight != 0;
// if(touchMode == NativeTouchOnly) [self addGestureRecognizer:keyboardToggleRecognizer]; //keep legacy approach in pure native mode
// else [self->streamFrameTopLayerView addGestureRecognizer:keyboardToggleRecognizer]; //add to the superview in other modes
// [self->streamFrameTopLayerView addGestureRecognizer:keyboardToggleRecognizer]; //add to the superview in other modes
@@ -130,45 +150,20 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
_pencilHandler = PencilHandler.shared;
// iOS uses touch Mode depending on user preference
switch (settings.touchMode.intValue) {
case NativeTouch:
keyboardToggleRecognizer.immediateTriggering = false;
self->touchHandler = [[NativeTouchHandler alloc] initWithView:self andSettings:settings];
break;
case NativeTouchOnly:
keyboardToggleRecognizer.immediateTriggering = false;
self->touchHandler = [[PureNativeTouchHandler alloc] initWithView:self andSettings:settings];break;
case RelativeTouch:
self->touchHandler = [[RelativeTouchHandler alloc] initWithView:self andSettings:settings];
keyboardToggleRecognizer.immediateTriggering = false;
// if(settings.onscreenControls.intValue == OnScreenControlsLevelCustom) keyboardToggleRecognizer.numberOfTouchesRequired = 3; //deprecated: fixing keyboard taps to 3, in order to invoke OSC rebase in stream view by 4-finger tap.
break;
case AbsoluteTouch:
self->touchHandler = [[AbsoluteTouchHandler alloc] initWithView:self andSettings:settings];
keyboardToggleRecognizer.immediateTriggering = true; //triggers signal in touchesBegan callback stage
break;
case TouchDisabled:
self->touchHandler = nil;
keyboardToggleRecognizer.immediateTriggering = false;
break;
default:
break;
}
[self updateTouchHandlerWithProfile:profile];
// we'll render on-screen controls on the toplayer too:
_onScreenControls = [[OnScreenControls alloc] initWithView:self->_streamFrameTopLayerView controllerSup:controllerSupport streamConfig:streamConfig]; // don't delete, this is mandatory
// OnScreenControls.shared = _onScreenControls;
/*
// here we pass the tap recognizer to the onscreencontrols obj
if (settings.touchMode.intValue == RelativeTouch){
if (touchMode == RelativeTouch){
RelativeTouchHandler* relativeTouchHandler = (RelativeTouchHandler*) touchHandler;
onScreenControls.mouseRightClickTapRecognizer = relativeTouchHandler.mouseRightClickTapRecognizer;
} */
OnScreenControlsLevel level = (OnScreenControlsLevel)[settings.onscreenControls integerValue];
if (settings.touchMode.intValue != RelativeTouch && settings.touchMode.intValue != NativeTouch ) {
if (touchMode != RelativeTouch && touchMode != NativeTouch ) {
Log(LOG_I, @"On-screen controls disabled in non-relative touch mode");
[_onScreenControls setLevel:OnScreenControlsLevelOff];
@@ -176,7 +171,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
[OnScreenControls.touchesCapturedByOnScreenControls removeAllObjects]; // reset the attribute to nil
/*
if(settings.touchMode.intValue == NativeTouch){
if(touchMode == NativeTouch){
NativeTouchHandler* nativeTouchHandler = (NativeTouchHandler* )touchHandler;
nativeTouchHandler.touchesCapturedByOnScreenButtons = onScreenControls.touchesCapturedByOnScreenButtons;
touchHandler = nativeTouchHandler;
@@ -235,6 +230,38 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
[self becomeFirstResponder];
}
- (void)updateTouchHandlerWithProfile:(OSCProfile* )profile{
touchMode = profile.touchMode;
switch (touchMode) {
case NativeTouch:
keyboardToggleRecognizer.immediateTriggering = false;
self->touchHandler = [[NativeTouchHandler alloc] initWithView:self settings:settings profile:profile];
[(NativeTouchHandler* )touchHandler setAllowSingleTouchEnabled:!_streamFrameVC.singleTouchDisabled];
break;
case NativeTouchOnly:
keyboardToggleRecognizer.immediateTriggering = false;
self->touchHandler = [[PureNativeTouchHandler alloc] initWithView:self settings:settings profile:profile];
break;
case RelativeTouch:
self->touchHandler = [[RelativeTouchHandler alloc] initWithView:self andSettings:settings];
keyboardToggleRecognizer.immediateTriggering = false;
// if(settings.onscreenControls.intValue == OnScreenControlsLevelCustom) keyboardToggleRecognizer.numberOfTouchesRequired = 3; //deprecated: fixing keyboard taps to 3, in order to invoke OSC rebase in stream view by 4-finger tap.
break;
case AbsoluteTouch:
self->touchHandler = [[AbsoluteTouchHandler alloc] initWithView:self andSettings:settings];
keyboardToggleRecognizer.immediateTriggering = true; //triggers signal in touchesBegan callback stage
break;
case TouchDisabled:
self->touchHandler = nil;
keyboardToggleRecognizer.immediateTriggering = false;
break;
default:
break;
}
sessionTouchHandler = touchHandler;
if(_streamFrameVC.touchDisabled) touchHandler = nil;
}
- (void)refreshKeyboardToggleRecognizer:(uint8_t)numberOfTouches{
[self->_streamFrameTopLayerView removeGestureRecognizer:keyboardToggleRecognizer];
keyboardToggleRecognizer = [[CustomTapGestureRecognizer alloc] initWithTarget:self action:@selector(toggleKeyboard)];
@@ -248,19 +275,22 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
- (void)keyboardWillShow:(NSNotification *)notification{
// NSLog(@"keyboard will show markmark %f", CACurrentMediaTime());
dockedKeyboardActionDetected = true;
NSLog(@"keyboard will show markmark %d", isInputingText);
if(settings.liftStreamViewForKeyboard && !isInputingText){
isInputingText = true;
NSDictionary *userInfo = notification.userInfo;
// Get the keyboard size from the notification
CGRect keyboardFrame = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
CGFloat screenHeight = CGRectGetHeight([[UIScreen mainScreen] bounds]);
CGFloat keyboardHeight = keyboardFrame.size.height;
CGFloat toolbarHeight = settings.showKeyboardToolbar ? GenericUtils.legacyToolbarHeight : 0;
CGFloat screenHeight = PublicUtils.screenHeight;
CGFloat totalKeyboardHeight = keyboardFrame.size.height;
CGFloat toolbarHeight = settings.showKeyboardToolbar ? GenericUtils.inputAccessoryBarHeight : 0;
if(keyboardHeight < screenHeight * 0.33333333333 + toolbarHeight
|| keyboardHeight > screenHeight*0.8 + toolbarHeight){
keyboardHeight = screenHeight*0.5 + toolbarHeight;
if(totalKeyboardHeight < screenHeight * 0.33333333333 + toolbarHeight
|| totalKeyboardHeight > screenHeight*0.8 + toolbarHeight){
totalKeyboardHeight = screenHeight*0.5 + toolbarHeight;
// [self toggleKeyboard];
/*
if(keyboardToggleRecognizer.lowestTouchPointHeight < screenHeight/2){
@@ -272,7 +302,17 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
}*/
}
HeightViewLiftedTo = keyboardHeight - keyboardToggleRecognizer.lowestTouchPointHeight + CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.15; // lift the StreamView to the height of lowest touch point of multi-finger tap gesture, while reserving the view of 1/10 screen height for remote typing.
bool useDesignatedKeyboardHeight = false;
if (@available(iOS 13.0, *)) {
useDesignatedKeyboardHeight = PublicUtils.isLandscape && keyboardHeightDesignatedForLandscape;
totalKeyboardHeight = useDesignatedKeyboardHeight ? designatedSoftKeyboardHeight+toolbarHeight : totalKeyboardHeight;
}
else {
useDesignatedKeyboardHeight = keyboardHeightDesignatedForLandscape;
totalKeyboardHeight = useDesignatedKeyboardHeight ? designatedSoftKeyboardHeight+toolbarHeight : totalKeyboardHeight;
}
HeightViewLiftedTo = totalKeyboardHeight - keyboardToggleRecognizer.lowestTouchPointHeight + PublicUtils.screenHeight * (useDesignatedKeyboardHeight ? 0.1 : 0.15); // lift the StreamView to the height of lowest touch point of multi-finger tap gesture, while reserving the view of 1/10 screen height for remote typing.
if(HeightViewLiftedTo < 0) HeightViewLiftedTo = 0; // set HeightViewLiftedTo to 0 if it is high enough and not going to be covered by keyboard.
CGRect liftedStreamFrame = self.frame;
liftedStreamFrame.origin.y -= HeightViewLiftedTo;
@@ -282,11 +322,49 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
[self liftMetalVideoViewIfNeeded:HeightViewLiftedTo];
[self refreshKeyboardToggleRecognizer:settings.keyboardToggleFingers.intValue];
if(keyboardToggleTip.superview && !keyboardToggleTip.hidden) [OnScreenWidgetView restoreFromTemporaryHideAll];
[keyboardToggleTip removeFromSuperview];
}
NSLog(@"keyboard will show %f", CACurrentMediaTime());
}
- (void)handleNonStandardKeyboard:(NSNotification *)notification{
dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC));
dispatch_after(delayTime, dispatch_get_main_queue(), ^{// Code to execute after the delay
if(!self->dockedKeyboardActionDetected){
if(self->keyboardToggleTip.superview && !self->keyboardToggleTip.hidden) [OnScreenWidgetView restoreFromTemporaryHideAll];
[self->keyboardToggleTip removeFromSuperview];
if(!self->isInputingText) [self keyboardWillHide];
[self refreshKeyboardToggleRecognizer:self->settings.keyboardToggleFingers.intValue];
self->dockedKeyboardActionDetected = false;
}
return;
});
/*
dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC));
dispatch_after(delayTime, dispatch_get_main_queue(), ^{// Code to execute after the delay
if(!self->dockedKeyboardActionDetected){
[self->keyboardToggleTip removeFromSuperview];
self->dockedKeyboardActionDetected = false;
self->isInputingText = !self->isInputingText;
[self->keyInputField removeFromSuperview];
AlertControllerUtil.autoCompletion = true;
[AlertControllerUtil showAlertIn:self->_streamFrameVC
title:@""
message:[LocalizationHelper localizedStringForKey:@"Floating keyboard not supported"]
withCancel:NO
buttonTitle:@""
countdown:1
action:^{}
completion:^{}];
}
});
*/
}
- (UIViewController *)parentViewController {
UIResponder *responder = self;
while (responder) {
@@ -300,6 +378,12 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
// this method also deals with recovering streamview when local keyboard is turned off
- (void)keyboardWillHide{
if (@available(iOS 13.0, *)) {
InputAccessoryBar* bar = (InputAccessoryBar* ) keyInputField.inputAccessoryView;
if(bar) [bar releasePressedKeys];
}
dockedKeyboardActionDetected = true;
// NSLog(@"keyboard will hide markmark %f", CACurrentMediaTime());
keyboardToggleRecognizer.numberOfTouchesRequired = settings.keyboardToggleFingers.intValue; // reset this number
@@ -310,6 +394,8 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
[self liftMetalVideoViewIfNeeded:0];
isInputingText = NO;
[keyInputField removeFromSuperview];
}
}
@@ -352,7 +438,12 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
NSLog(@"change num of fingers required");
[self refreshKeyboardToggleRecognizer:1];
keyboardToggleTip.translatesAutoresizingMaskIntoConstraints = NO;
NSLog(@"tip obj: %@", keyboardToggleTip);
dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC));
dispatch_after(delayTime, dispatch_get_main_queue(), ^{// Code to execute after the delay
[OnScreenWidgetView temporaryHideAll];
});
[self addSubview:keyboardToggleTip];
[NSLayoutConstraint activateConstraints:@[
[keyboardToggleTip.centerXAnchor constraintEqualToAnchor:self.centerXAnchor constant:0],
@@ -370,9 +461,12 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
if (isInputingText) {
Log(LOG_D, @"Closing the keyboard");
[keyInputField resignFirstResponder];
if(self->keyboardToggleTip.superview && !self->keyboardToggleTip.hidden) [OnScreenWidgetView restoreFromTemporaryHideAll];
[keyboardToggleTip removeFromSuperview];
[OnScreenWidgetView restoreFromTemporaryHideAll];
} else {
Log(LOG_D, @"Opening the keyboard");
[self addSubview:keyInputField];
// Prepare the textbox used to capture keyboard events.
keyInputField.delegate = self;
keyInputField.text = @"0";
@@ -390,7 +484,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
UIBarButtonItem *deleteBarButton = [self createButtonWithImageNamed:@"DeleteIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0x2E isToggleable:NO isDoneButton:false];
UIBarButtonItem *flexibleSpace = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
[customToolbarView setItems:[NSArray arrayWithObjects:doneBarButton, windowsBarButton, escapeBarButton, tabBarButton, shiftBarButton, controlBarButton, altBarButton, deleteBarButton, flexibleSpace, nil]];
if (GenericUtils.liquidGlassEnabled) {
if (PublicUtils.liquidGlassEnabled) {
if (@available(iOS 26.0, *)) {
for(UIBarButtonItem *button in customToolbarView.items){
button.hidesSharedBackground = true;
@@ -408,8 +502,17 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
[customToolbarView sendSubviewToBack:glassView];
}
}
if (@available(iOS 13.0, *)) {
InputAccessoryBar *barView = [[InputAccessoryBar alloc] initWithFrame:CGRectMake(0, 0, self.bounds.size.width, GenericUtils.inputAccessoryBarHeight)];
barView.delegate = self;
keyInputField.inputAccessoryView = barView;
}
else {
keyInputField.inputAccessoryView = customToolbarView;
}
}
#endif
[keyInputField becomeFirstResponder];
[keyInputField addTarget:self action:@selector(onKeyboardPressed:) forControlEvents:UIControlEventEditingChanged];
@@ -417,7 +520,13 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
[keyInputField.undoManager disableUndoRegistration];
//[keyboardToggleTip removeFromSuperview];
}
dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.025 * NSEC_PER_SEC));
dispatch_after(delayTime, dispatch_get_main_queue(), ^{// Code to execute after the delay
if(!self->dockedKeyboardActionDetected) self->isInputingText = !self->isInputingText;
});
}
- (void)startInteractionTimer {
@@ -468,19 +577,13 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
#endif
}
// we'll enable on screen buttons, and disable on screen controllers for absolute touch
- (bool) isOscEnabled{
return (settings.touchMode.intValue == RelativeTouch || settings.touchMode.intValue == NativeTouch || settings.touchMode.intValue == AbsoluteTouch || settings.touchMode.intValue == TouchDisabled) && settings.onscreenControls.intValue != OnScreenControlsLevelOff;
}
// we'll enable on screen buttons, and disable on screen controllers for absolute touch
- (bool) isOnScreenWidgetEnabled{
return [self isOscEnabled] && settings.onscreenControls.intValue == OnScreenControlsLevelCustom;
return settings.onscreenControls.intValue == OnScreenControlsLevelCustom;
}
- (void) reloadOnScreenControlsRealtimeWith:(ControllerSupport*)controllerSupport
andConfig:(StreamConfiguration*)streamConfig {
- (void) reloadOnScreenControlsRealtimeWithControllerSupport:(ControllerSupport*)controllerSupport
streamConfig:(StreamConfiguration*)streamConfig {
[self reloadOnScreenControlsWith:controllerSupport andConfig:streamConfig];
/*
StreamFrameViewController* vc = (StreamFrameViewController* )_streamFrameVC;
@@ -500,7 +603,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
RelativeTouchHandler* relativeTouchHandler = (RelativeTouchHandler *)touchHandler;
onScreenControls.mouseRightClickTapRecognizer = relativeTouchHandler.mouseRightClickTapRecognizer;
} */
if([self isOscEnabled]) [_onScreenControls setLevel:(OnScreenControlsLevel)settings.onscreenControls.intValue];
if([self isOnScreenWidgetEnabled]) [_onScreenControls setLevel:(OnScreenControlsLevel)settings.onscreenControls.intValue];
}
@@ -517,7 +620,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
}
- (CGPoint)denormalizeWidgetPosition:(CGPoint)position {
if(position.x < 1.0 && position.y < 1.0){
if(position.x < 2.01 && position.y < 2.01){
position.x = position.x * _streamFrameTopLayerView.bounds.size.width;
position.y = position.y * _streamFrameTopLayerView.bounds.size.height;
}
@@ -533,7 +636,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
else return longSide;
}
- (void)saveStreamViewWidgetChanges{
- (void)saveStreamingGameProfileChanges{
/*
NSMutableDictionary* relocatedWidgetDict = [NSMutableDictionary dictionary];
@@ -554,7 +657,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
if(!hasFolderStateChanged) hasFolderStateChanged = widget.folded != widget.persistedFolded;
}
if(relocatedWidgetSequences.count == 0 && !hasFolderStateChanged) return;
if(relocatedWidgetSequences.count == 0 && !hasFolderStateChanged && !OnScreenWidgetView.profileChangedDuringStreaming) return;
NSLog(@"relocatedWidgetSequences.count %lu, hasFolderStateChanged %d %F", relocatedWidgetSequences.count, hasFolderStateChanged, CACurrentMediaTime());
oscProfileMan = [OSCProfilesManager sharedManager:self->_streamFrameTopLayerView.bounds];
@@ -576,41 +679,63 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
newProfile.unfoldedExclusiveFolderSequence = OnScreenWidgetView.unfoldedExclusiveFolderSequence;
newProfile.postExclusiveUnfoldedSequences = OnScreenWidgetView.postExclusiveUnfoldedSequences;
newProfile.gamepadOverlayEnabled = _streamFrameVC.virtualGamepadOverlay != nil;
newProfile.normalizedStreamViewOffset = CGPointMake(_streamFrameVC.streamViewMagnifierContentOffset.x/self.bounds.size.width, _streamFrameVC.streamViewMagnifierContentOffset.y/self.bounds.size.height);
newProfile.streamViewScale = _streamFrameVC.streamViewMagnifierZoomScale;
[oscProfileMan replaceSelectedProfileWith:newProfile overwriteDefault:YES];
}
- (void) reloadOnScreenWidgetViews:(bool)reload{
NSLog(@"reloadOnScreenWidgets in streamview %f", CACurrentMediaTime());
- (void) reloadGameProfile:(OSCProfile* )profile reloadWidgets:(bool)reloadWidgets{
self->oscProfileMan = [OSCProfilesManager sharedManager:self->_streamFrameTopLayerView.bounds];
if(!profile){
profile = [self->oscProfileMan getSelectedProfile]; //returns the currently selected OSCProfile
}
NSLog(@"reloadOnScreenWidgets in streamview %d", reloadWidgets);
dispatch_async(dispatch_get_main_queue(), ^{
if(!self->oscProfileMan) self->oscProfileMan = [OSCProfilesManager sharedManager:self->_streamFrameTopLayerView.bounds];
OSCProfile *oscProfile = [self->oscProfileMan getSelectedProfile]; //returns the currently selected OSCProfile
MotionHandler* motionHandler = [MotionHandler sharedWithProfile:oscProfile];
MotionHandler* motionHandler = [MotionHandler sharedWithProfile:profile];
// get streamFrameVC
if(!self->_streamFrameVC) self->_streamFrameVC = [self parentViewController];
if(!self.streamFrameVC) self.streamFrameVC = (StreamFrameViewController* )[PublicUtils parentViewControllerForView:self];
/*
if (@available(iOS 13.0, *)) {
if(oscProfile.gamepadOverlayEnabled) [(StreamFrameViewController* )self->_streamFrameVC loadAbstractGamepadOverlayIfNeeded];
} */
// bool customOscEnabled = [self isOscEnabled] && settings.onscreenControls.intValue == OnScreenControlsLevelCustom;
if(![self isOnScreenWidgetEnabled]) return;
// if(![self isOnScreenWidgetEnabled]) return;
OnScreenWidgetView.buttonVisualFeedbackEnabled = self->settings.buttonVisualFeedback;
OnScreenWidgetView.gamepadOverlayFLag = profile.gamepadOverlayEnabled;
ControllerUtil.dualSenseHapticTransient = profile.dualSenseTransient;
bool hasLegacyWidget = false;
if(reload && !OnScreenWidgetView.editMode){
if(reloadWidgets && !OnScreenWidgetView.editMode){
// remove all keyboard widget views first
[self clearOnScreenWidgets];
self->_streamFrameVC.touchDisabled = false;
self->_streamFrameVC.singleTouchDisabled = false;
PencilHandler* pencilHandler = [PencilHandler shared];
if(pencilHandler) pencilHandler.disableTilt = false;
bool sequenceGenerated = false;
bool hasMovableWidget = false;
for (NSInteger i = 0; i < oscProfile.buttonStatesEncoded.count; i++) {
NSData* buttonStateEncoded = oscProfile.buttonStatesEncoded[i];
for (NSInteger i = 0; i < profile.buttonStatesEncoded.count; i++) {
NSData* buttonStateEncoded = profile.buttonStatesEncoded[i];
OnScreenButtonState* buttonState = [self->oscProfileMan unarchiveButtonStateEncoded:buttonStateEncoded];
if(buttonState.widgetType == CustomOnScreenWidget){
OnScreenWidgetView* widgetView = [[OnScreenWidgetView alloc] initWithCmdString:buttonState.name buttonLabel:buttonState.alias shape:buttonState.widgetShape profile:oscProfile]; //reconstruct widgetView
OnScreenWidgetView* widgetView = [OnScreenWidgetView widgetWithCmdString:buttonState.name buttonLabel:buttonState.alias shape:buttonState.widgetShape profile:profile]; //reconstruct widgetView
widgetView.functionalButtonDelegate = (id<OnScreenFunctionalButtonDelegate>)self->_streamFrameVC;
widgetView.functionalWidgetDelegate = (id<OnScreenFunctionalWidgetDelegate>)self->_streamFrameVC;
widgetView.motionHandler = motionHandler;
widgetView.sequence = buttonState.sequence == -1 ? i : buttonState.sequence;
@@ -619,9 +744,13 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
[OnScreenWidgetView setWithWidget:widgetView for:widgetView.sequence];
widgetView.sequenceSet = buttonState.sequenceSet;
widgetView.parentSequence = buttonState.parentSequence;
widgetView.autoDockIdleDuration = buttonState.autoDockTimer;
widgetView.storedAutoDockIdleDuration = widgetView.autoDockIdleDuration;
widgetView.autoDockSettledAlpha = buttonState.dockedAlpha;
widgetView.folded = buttonState.folded;
widgetView.persistedFolded = buttonState.folded;
widgetView.revealMode = buttonState.revealMode;
widgetView.bulkMoveEnabled = buttonState.bulkMoveEnabled;
widgetView.translatesAutoresizingMaskIntoConstraints = NO; // weird but this is mandatory, or you will find no key views added to the right place
widgetView.widthFactor = buttonState.widthFactor;
@@ -630,8 +759,10 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
widgetView.borderWidth = buttonState.borderWidth;
widgetView.highlightSizeFactor = buttonState.highlightSizeFactor;
widgetView.autoTapInterval = buttonState.autoTapInterval;
widgetView.autoTapRepeats = buttonState.autoTapRepeats;
[widgetView setVibrationWithStyle:buttonState.vibrationStyle];
widgetView.mouseButtonAction = buttonState.mouseButtonAction;
widgetView.animatesTransition = buttonState.animatesTransition;
widgetView.sensitivityFactorX = buttonState.sensitivityFactorX;
widgetView.sensitivityFactorY = buttonState.sensitivityFactorY;
widgetView.slideThreshold = buttonState.slideThreshold;
@@ -644,6 +775,13 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
widgetView.minStickOffset = buttonState.minStickOffset;
widgetView.dWheelWalkModeThreshold = buttonState.walkModeThreshold;
widgetView.buttonMode = buttonState.buttonMode;
if([widgetView.cmdString isEqualToString:@"NOSINGLETOUCH"]) widgetView.buttonMode = movable;
widgetView.sprintKeyActionType = buttonState.sprintKeyActionType;
widgetView.sprintKeyThreshold = buttonState.sprintKeyThreshold;
widgetView.walkKeyActionType = buttonState.walkKeyActionType;
widgetView.walkKeyThreshold = buttonState.walkKeyThreshold;
widgetView.hidden = !OnScreenWidgetView.enableFolderAnimation && widgetView.parentSequence != -1;
// Add the widgetView to the view controller's view
[self->_streamFrameTopLayerView addSubview:widgetView]; // add keyboard button to the stream frame view. must add it to the target view before setting location.
buttonState.position = [self denormalizeWidgetPosition:buttonState.position];
@@ -659,36 +797,40 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
[widgetView tweakHighlightAlphaWithAlpha:buttonState.highlightAlpha];
[widgetView setupAutoTapTimer];
[widgetView setupInertialScrollerWithFps:self->settings.framerate.intValue];
///
widgetView.sensitivityFactorX = buttonState.sensitivityFactorX;
widgetView.componentSizeFactor = buttonState.componentSizeFactor;
widgetView.touchPointAnchored = buttonState.touchPointAnchored;
widgetView.stickIndicatorOffset = buttonState.stickIndicatorOffset;
if(widgetView.isFolder && widgetView.parentSequence<0 && widgetView.autoDockIdleDuration>0) {
[widgetView setAutoDockWithEnabled:true];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
if(!AlertControllerUtil.isCountingDown) [widgetView restartAutoDockCountdown];
});
}
if(sequenceGenerated){
// NSLog(@"widgetView.sequence %d %f", widgetView.sequence, CACurrentMediaTime());
buttonState.sequence = widgetView.sequence;
oscProfile.buttonStatesEncoded[i] = [NSKeyedArchiver archivedDataWithRootObject:buttonState requiringSecureCoding:YES error:nil];
profile.buttonStatesEncoded[i] = [NSKeyedArchiver archivedDataWithRootObject:buttonState requiringSecureCoding:YES error:nil];
}
[widgetView accessWidgetAttributes];
[widgetView setupAtrributedText]; // rely on "labeledFolder cmd"
}
else if(buttonState.widgetType == LegacyOnScreenControls) hasLegacyWidget = true;
}
if(sequenceGenerated && hasMovableWidget){
[self->oscProfileMan replaceSelectedProfileWith:oscProfile overwriteDefault:YES];
[self->oscProfileMan replaceSelectedProfileWith:profile overwriteDefault:YES];
}
uint64_t buttonIndex = 9999999;
UIView* deepestButton;
for (UIView *subview in self->_streamFrameTopLayerView.subviews) {
if ([subview isKindOfClass:[OnScreenWidgetView class]]) {
OnScreenWidgetView* widget = (OnScreenWidgetView* ) subview;
if(widget.widgetType == WidgetTypeEnumButton){
uint64_t index = [self->_streamFrameTopLayerView.subviews indexOfObject:subview];
if (index<buttonIndex){
buttonIndex = index;
deepestButton = subview;
}
}
}
}
OnScreenWidgetView.deepestButton = nil;
UIView* deepestButton = [OnScreenWidgetView getDeepestButton];
NSMutableSet* motionControlButtons = [NSMutableSet new];
if(deepestButton){
for (UIView *subview in self->_streamFrameTopLayerView.subviews) {
if ([subview isKindOfClass:[OnScreenWidgetView class]]) {
@@ -696,25 +838,33 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
if(widget.widgetType == WidgetTypeEnumTouchPad){
[self->_streamFrameTopLayerView insertSubview:subview belowSubview:deepestButton];
}
if(widget.isMotionControlButton) [motionControlButtons addObject:widget];
}
}
for(OnScreenWidgetView* button in motionControlButtons) {
[self->_streamFrameTopLayerView insertSubview:button belowSubview:deepestButton];
}
}
}
OnScreenWidgetView.unfoldedExclusiveFolderSequence = oscProfile.unfoldedExclusiveFolderSequence;
[OnScreenWidgetView setPostExclusiveUnfoldeds:oscProfile.postExclusiveUnfoldedSequences];
OnScreenWidgetView.unfoldedExclusiveFolderSequence = profile.unfoldedExclusiveFolderSequence;
[OnScreenWidgetView setPostExclusiveUnfoldeds:profile.postExclusiveUnfoldedSequences];
[OnScreenWidgetView restoreFoldedStates];
NSLog(@"hasLegacyWidget %d %f", hasLegacyWidget, CACurrentMediaTime());
// legacy widgets
if(!OnScreenWidgetView.editMode && (hasLegacyWidget || !reload)){
if([self isOscEnabled]) [self reloadLegacyWidgets:oscProfile];
if(!OnScreenWidgetView.editMode && (hasLegacyWidget || !reloadWidgets)){
if([self isOnScreenWidgetEnabled]) [self reloadLegacyWidgets:profile];
else [self disableOnScreenControls];
}
if(reload && !OnScreenWidgetView.editMode){
[PencilHandler.shared setupPressureLUTWithProfile:oscProfile];
if(reloadWidgets && !OnScreenWidgetView.editMode){
[PencilHandler.shared setupPressureLUTWithProfile:profile];
}
[self.streamFrameVC restorePersistedStreamViewOffsetAndScaleWithProfile:profile];
[self updateTouchHandlerWithProfile:profile];
OnScreenWidgetView.profileChangedDuringStreaming = false;
});
}
@@ -858,7 +1008,12 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
});
}
CGPoint location = [self adjustCoordinatesForVideoArea:[gesture locationInView:self]];
CGPoint originalLocation = [gesture locationInView:self];
CGPoint location = [self adjustCoordinatesForVideoArea:originalLocation];
NSLog(@" location %f, %f", location.x, location.y);
PencilHandler* handler = PencilHandler.shared;
location = CGPointApplyAffineTransform(location, CGAffineTransformMakeTranslation(handler.pencilTipOffset.x, handler.pencilTipOffset.y));
CGSize videoSize = [self getVideoAreaSize];
float distance = 0.0f;
@@ -879,19 +1034,20 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
dispatch_after(0, dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE,0), ^{// Code to execute after the delay
if(PencilHandler.isDrawing) return;
switch (PencilHandler.hoverMode) {
case HoverPencil:
if(PencilHandler.isDrawing && PencilHandler.pencilAndHoverMode == pencilOnly && PencilHandler.pencilAndHoverMode == hoverDisabled) return;
switch (PencilHandler.pencilAndHoverMode) {
case pencilOnly:
LiSendPenEvent(type, LI_TOOL_TYPE_PEN, 0, location.x / videoSize.width, location.y / videoSize.height, distance, 0.0f, 0.0f, rotationAngle, tiltAngle);
break;
case HoverMouse:
[self updateCursorLocation:location isMouse:YES];
case pencilToMouse:
if(gesture.state != UIGestureRecognizerStateEnded) [self updateCursorLocation:originalLocation isMouse:NO];
break;
case HoverDisabled:
case hoverDisabled:
break;
case HoverBoth:
case pencilToTouch:
/*
LiSendPenEvent(type, LI_TOOL_TYPE_PEN, 0, location.x / videoSize.width, location.y / videoSize.height, distance, 0.0f, 0.0f, rotationAngle, tiltAngle);
[self updateCursorLocation:location isMouse:YES];
[self updateCursorLocation:location isMouse:YES]; */
break;
default:
break;
@@ -905,10 +1061,12 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
#if !TARGET_OS_TV
// if (@available(iOS 13.4, *)) {
// cancel restriction of native touch for iOS13.3 & lower
if (settings.touchMode.intValue == NativeTouchOnly) {
if (touchMode == NativeTouchOnly) {
[touchHandler touchesBegan:touches withEvent:event];
return; //This is a native touch oriented fork, in pure native touch mode, this call back method deals with native touch only.
}
// else if(touchMode == RelativeTouch && keyboardToggleRecognizer.numberOfTouchesRequired == 1) return;
NSMutableSet* pencilTouches = [NSMutableSet set];
for (UITouch* touch in touches) {
if (touch.type == UITouchTypePencil) {
@@ -918,6 +1076,8 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
NSMutableSet* nonPencilTouches;
if(pencilTouches.count>0){
[_pencilHandler touchesBegan:pencilTouches withEvent:event];
GenericUtils.pencilInStreaming = true;
if(touchMode == AbsoluteTouch) [_streamFrameVC setMagnifierViewportInteractionEnabled:false];
nonPencilTouches = [touches mutableCopy];
[nonPencilTouches minusSet:pencilTouches];
}
@@ -936,8 +1096,8 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
[self startInteractionTimer];
NSSet* targetTouches = nonPencilTouches ? nonPencilTouches : touches;
if(settings.touchMode.intValue == NativeTouch || settings.touchMode.intValue == RelativeTouch){
[self->_onScreenControls handleTouchDownEvent:targetTouches];
if(touchMode != AbsoluteTouch){
if([self isOnScreenWidgetEnabled]) [self->_onScreenControls handleTouchDownEvent:targetTouches];
[self->touchHandler touchesBegan:targetTouches withEvent:event];
}
else if(![_onScreenControls handleTouchDownEvent:targetTouches]) [touchHandler touchesBegan:targetTouches withEvent:event];
@@ -951,11 +1111,11 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
// [button setTitle:@"666" forState:UIControlStateNormal];
[button setImage:image forState:UIControlStateNormal];
button.frame = GenericUtils.liquidGlassEnabled ? CGRectMake(0, 0, 30, 30) : CGRectMake(0, 0, 30, 30);
button.frame = PublicUtils.liquidGlassEnabled ? CGRectMake(0, 0, 30, 30) : CGRectMake(0, 0, 30, 30);
button.imageView.contentMode = UIViewContentModeScaleAspectFit;
button.imageView.backgroundColor = backgroundColor;
button.imageView.layer.cornerRadius = 10.0;
button.imageEdgeInsets = (GenericUtils.liquidGlassEnabled
button.imageEdgeInsets = (PublicUtils.liquidGlassEnabled
? (isDoneButton ? UIEdgeInsetsMake(16, 16, 16, 16) : UIEdgeInsetsMake(27.5, 27.5, 27.5, 27.5))
: UIEdgeInsetsMake(6, 6, 6, 6));
[button addTarget:target action:action forControlEvents:UIControlEventTouchUpInside];
@@ -973,7 +1133,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
isOn = !isOn;
// Update the button's appearance based on its new state
if (isOn) {
sender.imageView.backgroundColor = GenericUtils.liquidGlassEnabled ? [UIColor.systemGrayColor colorWithAlphaComponent:0.5] : [UIColor lightGrayColor];
sender.imageView.backgroundColor = PublicUtils.liquidGlassEnabled ? [UIColor.systemGrayColor colorWithAlphaComponent:0.5] : [UIColor lightGrayColor];
} else {
sender.imageView.backgroundColor = [UIColor blackColor];
}
@@ -1072,7 +1232,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
#if !TARGET_OS_TV
if (settings.touchMode.intValue == NativeTouchOnly) {
if (touchMode == NativeTouchOnly) {
[touchHandler touchesMoved:touches withEvent:event];
return; //This is a native touch oriented fork, in pure native touch mode, this call back method deals with native touch only.
}
@@ -1113,9 +1273,9 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
hasUserInteracted = YES;
NSSet* targetTouches = nonPencilTouches ? nonPencilTouches : touches;
if(self->settings.touchMode.intValue == NativeTouch || self->settings.touchMode.intValue == RelativeTouch){
if(self->touchMode != AbsoluteTouch){
[self->touchHandler touchesMoved:targetTouches withEvent:event];
[self->_onScreenControls handleTouchMovedEvent:targetTouches];
if([self isOnScreenWidgetEnabled]) [self->_onScreenControls handleTouchMovedEvent:targetTouches];
}
else if(![self->_onScreenControls handleTouchMovedEvent:targetTouches]) [self->touchHandler touchesMoved:targetTouches withEvent:event];
}
@@ -1125,19 +1285,22 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
return;
}
switch (press.key.keyCode) {
case UIKeyboardHIDUsageKeyboardQ:
case UIKeyboardHIDUsageKeyboardD:
[_interactionDelegate streamExitRequested];
break;
case UIKeyboardHIDUsageKeyboardS:
case UIKeyboardHIDUsageKeyboardO:
[_interactionDelegate toggleStatsOverlay];
break;
case UIKeyboardHIDUsageKeyboardS:
[_interactionDelegate expandSettingsView];
break;
case UIKeyboardHIDUsageKeyboardM:
[_interactionDelegate toggleMouseCapture];
break;
case UIKeyboardHIDUsageKeyboardC:
[_interactionDelegate toggleMouseVisible];
break;
case UIKeyboardHIDUsageKeyboardD:
case UIKeyboardHIDUsageKeyboardQ:
[_interactionDelegate disconnectAndQuitApp];
break;
default:
@@ -1145,7 +1308,36 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
}
}
- (BOOL)isOptionVerticalArrowPress:(UIPress *)press {
if (@available(iOS 13.4, tvOS 13.4, *)) {
UIKey *key = press.key;
if (key == nil) {
return NO;
}
return key.modifierFlags == UIKeyModifierAlternate &&
(key.keyCode == UIKeyboardHIDUsageKeyboardUpArrow ||
key.keyCode == UIKeyboardHIDUsageKeyboardDownArrow);
}
return NO;
}
- (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event {
if (@available(iOS 17.0, *)) nil;
else {
BOOL shouldBypassMagnifierInset = NO;
for (UIPress *press in presses) {
if ([self isOptionVerticalArrowPress:press]) {
shouldBypassMagnifierInset = YES;
break;
}
}
if (shouldBypassMagnifierInset) {
_streamFrameVC.scrollView.contentInset = UIEdgeInsetsZero;
}
}
BOOL handled = NO;
if (@available(iOS 13.4, tvOS 13.4, *)) {
@@ -1166,6 +1358,21 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
}
- (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event {
if (@available(iOS 17.0, *)) nil;
else {
BOOL shouldRestoreMagnifierMetrics = NO;
for (UIPress *press in presses) {
if ([self isOptionVerticalArrowPress:press]) {
shouldRestoreMagnifierMetrics = YES;
break;
}
}
if (shouldRestoreMagnifierMetrics) {
[_streamFrameVC updateMagnifierViewportMetrics];
[_streamFrameVC resetMagnifierStreamViewWithAnimated:NO];
}
}
BOOL handled = NO;
if (@available(iOS 13.4, tvOS 13.4, *)) {
@@ -1187,7 +1394,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
#if !TARGET_OS_TV
if (settings.touchMode.intValue == NativeTouchOnly) {
if (touchMode == NativeTouchOnly) {
[touchHandler touchesEnded:touches withEvent:event];
return; //This is a native touch oriented fork, in pure native touch mode, this call back method deals with native touch only.
}
@@ -1218,9 +1425,9 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
hasUserInteracted = YES;
NSSet* targetTouches = nonPencilTouches ? nonPencilTouches : touches;
if(settings.touchMode.intValue == NativeTouch || settings.touchMode.intValue == RelativeTouch){
if(touchMode != AbsoluteTouch){
[self->touchHandler touchesEnded:targetTouches withEvent:event]; // when touches ended, must call the native touchhandler before onScreenControls, since the NSSet of touches captured by on screen button shall be updated later
[self->_onScreenControls handleTouchUpEvent:targetTouches];
if([self isOnScreenWidgetEnabled]) [self->_onScreenControls handleTouchUpEvent:targetTouches];
}
else if(![_onScreenControls handleTouchUpEvent:targetTouches]) [touchHandler touchesEnded:targetTouches withEvent:event];
}
@@ -1228,7 +1435,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
[touchHandler touchesCancelled:touches withEvent:event];
#if !TARGET_OS_TV
if (settings.touchMode.intValue == NativeTouchOnly) return; //This is a native touch oriented fork, in pure native touch mode, this call back method deals with native touch only.
if (touchMode == NativeTouchOnly) return; //This is a native touch oriented fork, in pure native touch mode, this call back method deals with native touch only.
for (UITouch* touch in touches) {
if (touch.type == UITouchTypePencil) {
[self touchesEnded:touches withEvent:event];
@@ -1404,6 +1611,11 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
[keysDown removeAllObjects];
}
- (void)inputAccessoryBarDidTapClose:(InputAccessoryBar *)bar API_AVAILABLE(ios(13.0)){
[keyInputField resignFirstResponder];
isInputingText = false;
}
- (void)onKeyboardPressed:(UITextField *)textField {
if (textField.markedTextRange) return;
@@ -1598,6 +1810,10 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
}
}
- (void)toggleTouchDisabled:(bool)disabled{
touchHandler = disabled ? nil : sessionTouchHandler;
}
- (void)cleanUp{
[keyInputField resignFirstResponder];
keyInputField.delegate = nil;
+4 -2
View File
@@ -49,12 +49,14 @@ import UIKit
let midPointDeltaX = midPointVector.dx
let midPointDeltaY = midPointVector.dy
inertialScroller.vector = CGVector(dx: midPointDeltaX, dy: midPointDeltaY)
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 {LiSendHighResHScrollEvent(Int16(-midPointDeltaX*7*scrollSensitivity))}
if enableHorizontalScroll, sendHorizontalScroll {LiSendHighResHScrollEvent(Int16(-midPointDeltaX*7*scrollSensitivity))}
if enablePinch, ctrlDownForPinch {
let midPointDelta = hypot(midPointDeltaX, midPointDeltaY)
+4 -2
View File
@@ -10,8 +10,10 @@ import UIKit
@objc class UITouchUtil: NSObject {
@objc public static func touches(in view: UIView, from event: UIEvent?) -> Set<UITouch> {
guard (event?.allTouches) != nil else { return [] }
return event?.allTouches?.filter({ $0.view == view && $0.type == .direct}) ?? []
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 {
+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;
+6 -2
View File
@@ -63,9 +63,9 @@
<key>NSLocalNetworkUsageDescription</key>
<string>VoidLink uses the local network to connect to your gaming PC for streaming.</string>
<key>NSMicrophoneUsageDescription</key>
<string>VoidLink需要麦克风访问权限。现阶段,麦克风重定向仅支持Sunshine基地版,且麦克风音频流【未经加密】。
<string>麦克风访问权限允许VoidLink 实现麦克风重定向。麦克风重定向仅支持Sunshine基地版。
VoidLink uses microphone to stream your voice to gaming PC. Microphone redirection currently works with Sunshine-Foundation (a fork of Sunshine) only, and the mic audio stream is NOT ENCRYPTED at this stage.</string>
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>VoidLink uses the device Gyro and Acceleration data to emulate motion controls when streaming.</string>
<key>UIApplicationSceneManifest</key>
@@ -116,6 +116,10 @@ VoidLink uses microphone to stream your voice to gaming PC. Microphone redirecti
<false/>
<key>UIStatusBarHidden</key>
<true/>
<key>UISupportedExternalAccessoryProtocols</key>
<array>
<string>com.xiaoji.M2boot</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationLandscapeLeft</string>
@@ -10,6 +10,7 @@
<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"/>
@@ -21,6 +22,11 @@
<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"/>
@@ -31,18 +37,23 @@
<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="buttonVisualFeedback" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<attribute name="controllerMouseExpo" optional="YES" attributeType="Float" defaultValueString="1.6" usesScalarValueType="NO" syncable="YES"/>
<attribute name="controllerMouseLeftButton" attributeType="Integer 32" defaultValueString="16777215" usesScalarValueType="NO" syncable="YES"/>
<attribute name="controllerMousePointerVelocity" optional="YES" attributeType="Float" defaultValueString="12" usesScalarValueType="NO" syncable="YES"/>
<attribute name="controllerMouseRightButton" attributeType="Integer 32" defaultValueString="16777215" usesScalarValueType="NO" syncable="YES"/>
<attribute name="controllerMouseStick" attributeType="Integer 16" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
<attribute name="controllerMouseSwitch" attributeType="Integer 32" defaultValueString="16777215" usesScalarValueType="NO" 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"/>
@@ -54,6 +65,7 @@
<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"/>
@@ -62,22 +74,26 @@
<attribute name="gyroSensitivity" optional="YES" attributeType="Float" defaultValueString="1" 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="mapControllerToMouse" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" 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="muteInBackground" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" 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"/>
@@ -98,8 +114,12 @@
<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="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="0" usesScalarValueType="NO" syncable="YES"/>
File diff suppressed because it is too large Load Diff
@@ -1,15 +1,12 @@
//
// SwiftLocalizationHelper.swift
// LocalizationHelper.swift
// VoidLink
//
// Created by True砖家 on 2024/7/23.
// Copyright © True砖家 on Bilibili. All rights reserved.
//
import Foundation
class SwiftLocalizationHelper {
extension LocalizationHelper {
static func localizedString(forKey key: String, _ args: CVarArg...) -> String {
let format = NSLocalizedString(key, tableName: "Localizable", bundle: .main, value: "", comment: "")
return String(format: format, arguments: args)
-54
View File
@@ -1,54 +0,0 @@
//
// MenuSectionView.h
// VoidLink
//
// Created by True砖家 on 5/18/25.
// Copyright © 2025 True砖家 @ Bilibili. All rights reserved.
//
// MenuSectionView.h
#import <UIKit/UIKit.h>
#import "DataManager.h"
@protocol MenuSectionDelegate <NSObject>
- (void)hideOverlappedDynamicLabels;
- (SettingsMenuMode)getSettingsMenuMode;
@end
@interface MenuSectionView : UIView
@property (class, nonatomic, assign) BOOL overridePersistedFoldState;
// 外部可访问属性
@property (nonatomic, strong) UIStackView *rootStackView;
@property (nonatomic, assign) CGFloat leadingTrailingPadding;
@property (nonatomic, assign) CGFloat separatorLinePadding;
@property (nonatomic, strong) UILabel *titleLabel;
@property (nonatomic, copy) NSString *sectionTitle;
@property (nonatomic, copy) NSString *identifier;
@property (nonatomic, assign) BOOL isExpanded;
@property (nonatomic, assign) BOOL expandable;
@property (nonatomic, copy) void (^lockedSectionHandler)(void);
@property (nonatomic, strong) UIColor *backgroundColor;
@property (nonatomic, assign) CGFloat rootStackViewSpacing;
@property (nonatomic, assign) CGFloat headerViewHeight;
@property (nonatomic, assign) CGFloat headerViewVerticalSpacing;
@property (nonatomic, strong) NSMutableArray<UIStackView *> *subStackViews;
@property (nonatomic, strong) UIImageView *iconImageView;
@property (nonatomic, strong) UIView *separatorLine;
@property (nonatomic, weak) id<MenuSectionDelegate> delegate; // Delegate property
// 方法
- (void)setSectionWithIcon:(UIImage *)icon size:(CGFloat)size sizeConstraint:(CGFloat)constant;
- (void)setSectionWithIcon:(UIImage *)icon size:(CGFloat)size weight:(UIImageSymbolWeight)weight sizeConstraint:(CGFloat)constant API_AVAILABLE(ios(13.0));
- (void)addSubStackView:(UIStackView *)stackView;
- (void)addToParentStack:(UIStackView *)parentStack;
- (void)removeSubStackView:(UIStackView *)stackView;
- (void)updateLayout;
- (void)updateViewForFoldState;
- (void)setExpanded:(BOOL)overridePersistedState;
@end
-425
View File
@@ -1,425 +0,0 @@
//
// MenuSectionView.h
// VoidLink
//
// Created by True砖家 on 5/18/25.
// Copyright © 2025 True砖家 @ Bilibili. All rights reserved.
//
// MenuSectionView.m
#import "MenuSectionView.h"
#import "VoidLink-Swift.h"
@interface MenuSectionView ()
@property (nonatomic, strong) UIButton *toggleButton;
@property (nonatomic, strong) UIButton *toggleArea;
@property (nonatomic, strong) NSLayoutConstraint *heightConstraint;
@property (nonatomic, strong) UIView *headerView;
@property (assign) CGFloat titleOffset;
@end
@implementation MenuSectionView
static BOOL overridePersistedFoldState = YES;
+ (BOOL)overridePersistedFoldState {
return overridePersistedFoldState;
}
+ (void)setOverridePersistedFoldState:(BOOL)val {
overridePersistedFoldState = val;
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self commonInit];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
[self commonInit];
}
return self;
}
- (void)commonInit {
// 默认值
_leadingTrailingPadding = 5;
_separatorLinePadding = 40;
_sectionTitle = @"Section";
_isExpanded = YES;
_expandable = true;
_backgroundColor = [UIColor clearColor];
_rootStackViewSpacing = [self isIPhone] ? 10 : 12;
_subStackViews = [NSMutableArray array];
_headerViewHeight = 37;
_headerViewVerticalSpacing = 25;
_titleOffset = 41.5;
// 设置视图
self.layer.cornerRadius = 10.0;
self.layer.masksToBounds = YES;
self.backgroundColor = _backgroundColor;
// 头部视图(包含图标和标题)
_headerView = [[UIView alloc] init];
_headerView.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:_headerView];
// 图标视图
_iconImageView = [[UIImageView alloc] init];
_iconImageView.contentMode = UIViewContentModeScaleAspectFit;
_iconImageView.translatesAutoresizingMaskIntoConstraints = NO;
_iconImageView.tintColor = ThemeManager.textColor;
[_headerView addSubview:_iconImageView];
// 标题标签
_titleLabel = [[UILabel alloc] init];
_titleLabel.text = _sectionTitle;
_titleLabel.accessibilityIdentifier = @"menuSectionTitleLabel";
_titleLabel.font = [UIFont systemFontOfSize:19.5 weight:UIFontWeightMedium];
_titleLabel.textColor = ThemeManager.textColor;
_titleLabel.textAlignment = NSTextAlignmentLeft;
_titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
[_headerView addSubview:_titleLabel];
// 切换按钮
_toggleButton = [UIButton buttonWithType:UIButtonTypeSystem];
_toggleArea = [UIButton buttonWithType:UIButtonTypeSystem];
if (@available(iOS 13.0, *)) {
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:_headerViewHeight*0.31 weight:UIImageSymbolWeightBold];
[_toggleButton setImage:[UIImage systemImageNamed:@"chevron.right" withConfiguration:config] forState:UIControlStateNormal];
} else {
// [_toggleButton setTitle:@"<" forState:UIControlStateNormal];
}
[_toggleButton addTarget:self action:@selector(toggleFold) forControlEvents:UIControlEventTouchUpInside];
[_toggleArea addTarget:self action:@selector(toggleFold) forControlEvents:UIControlEventTouchUpInside];
_toggleButton.translatesAutoresizingMaskIntoConstraints = NO;
_toggleArea.translatesAutoresizingMaskIntoConstraints = NO;
_toggleButton.contentEdgeInsets = UIEdgeInsetsMake(-10,-10,-10,-10); // 上下左右各扩展 20pt
[_headerView addSubview:_toggleButton];
[_headerView addSubview:_toggleArea];
// 根stackView
_rootStackView = [[UIStackView alloc] init];
_rootStackView.axis = UILayoutConstraintAxisVertical;
_rootStackView.spacing = _rootStackViewSpacing;
_rootStackView.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:_rootStackView];
// 分隔线
_separatorLine = [[UIView alloc] init];
_separatorLine.backgroundColor = ThemeManager.separatorColor;
_separatorLine.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:_separatorLine];
// 高度约束
_heightConstraint = [self.heightAnchor constraintEqualToConstant:44];
_heightConstraint.active = YES;
// 布局
[self setupConstraints];
[self updateViewForFoldState];
self.lockedSectionHandler = ^{
NSLog(@"Null execution of lockedSectionHandler");
};
}
- (void)setupConstraints {
// 头部视图约束
[NSLayoutConstraint activateConstraints:@[
[_headerView.topAnchor constraintEqualToAnchor:self.topAnchor],
[_headerView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
[_headerView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
[_headerView.heightAnchor constraintEqualToConstant:_headerViewHeight]
]];
// 标题约束
if(@available(iOS 13.0, *)){
[NSLayoutConstraint activateConstraints:@[
[_titleLabel.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:_titleOffset],
[_titleLabel.centerYAnchor constraintEqualToAnchor:_headerView.centerYAnchor],
[_titleLabel.trailingAnchor constraintEqualToAnchor:_toggleButton.leadingAnchor constant:-8]
]];
}
else [NSLayoutConstraint activateConstraints:@[
[_titleLabel.leadingAnchor constraintEqualToAnchor:_headerView.leadingAnchor constant:0],
[_titleLabel.centerYAnchor constraintEqualToAnchor:_headerView.centerYAnchor],
[_titleLabel.trailingAnchor constraintEqualToAnchor:_toggleButton.leadingAnchor constant:-8]
]];
// 切换按钮约束
[NSLayoutConstraint activateConstraints:@[
[_toggleButton.trailingAnchor constraintEqualToAnchor:_headerView.trailingAnchor constant:-_leadingTrailingPadding],
[_toggleButton.centerYAnchor constraintEqualToAnchor:_headerView.centerYAnchor],
[_toggleButton.widthAnchor constraintEqualToConstant:_headerViewHeight*0.8],
[_toggleButton.heightAnchor constraintEqualToConstant:_headerViewHeight*0.8]
]];
[NSLayoutConstraint activateConstraints:@[
[_toggleArea.leadingAnchor constraintEqualToAnchor:_headerView.leadingAnchor],
[_toggleArea.centerYAnchor constraintEqualToAnchor:_headerView.centerYAnchor],
[_toggleArea.trailingAnchor constraintEqualToAnchor:_toggleButton.leadingAnchor],
//[_toggleArea.widthAnchor constraintEqualToConstant:_headerViewHeight],
[_toggleArea.heightAnchor constraintEqualToConstant:_headerViewHeight+25]
]];
[self layoutIfNeeded];
// 根stackView约束
[NSLayoutConstraint activateConstraints:@[
[_rootStackView.topAnchor constraintEqualToAnchor:self.topAnchor constant:_headerViewHeight+_headerViewVerticalSpacing],
[_rootStackView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:_leadingTrailingPadding],
[_rootStackView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-_leadingTrailingPadding],
[_rootStackView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor constant:-1] // 为分隔线留出空间
]];
// 分隔线约束
self.clipsToBounds = false;//mark: settingMenuLayout
[NSLayoutConstraint activateConstraints:@[
//[_separatorLine.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
//[_separatorLine.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
[_separatorLine.centerXAnchor constraintEqualToAnchor:self.centerXAnchor],
[_separatorLine.widthAnchor constraintEqualToAnchor:self.widthAnchor constant:-5], // //mark: settingMenuLayout
[_separatorLine.bottomAnchor constraintEqualToAnchor:self.bottomAnchor],
[_separatorLine.heightAnchor constraintEqualToConstant:GenericUtils.menuSectionSeparatorWidth]
]];
[NSLayoutConstraint activateConstraints:@[
[self.headerView.topAnchor constraintEqualToAnchor:self.topAnchor constant:0],
[self.rootStackView.topAnchor constraintEqualToAnchor:self.headerView.bottomAnchor constant:0],
]];
}
#pragma mark - Public Methods
- (void)setSectionWithIcon:(UIImage *)icon size:(CGFloat)size sizeConstraint:(CGFloat)constant{
if (@available(iOS 13.0, *)) {
UIImageSymbolConfiguration *config;
if(icon.isSymbolImage) config = [UIImageSymbolConfiguration configurationWithPointSize:size weight:UIImageSymbolWeightBold];
else{
config = [UIImageSymbolConfiguration configurationWithPointSize:size];
icon = [icon imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
}
_iconImageView.image = [icon imageWithConfiguration:config];
} else {
_iconImageView.image = icon;
}
_iconImageView.tintColor = ThemeManager.textColor;
_iconImageView.hidden = (icon == nil);
// 图标约束
[NSLayoutConstraint activateConstraints:@[
[_iconImageView.centerXAnchor constraintEqualToAnchor:_headerView.leadingAnchor constant:_leadingTrailingPadding+_headerViewHeight/2-3],
[_iconImageView.centerYAnchor constraintEqualToAnchor:_headerView.centerYAnchor constant:-0],
[_iconImageView.widthAnchor constraintEqualToConstant:_headerViewHeight+constant],
[_iconImageView.heightAnchor constraintEqualToConstant:_headerViewHeight+constant]
]];
[self updateLayout];
}
- (void)setSectionWithIcon:(UIImage *)icon size:(CGFloat)size weight:(UIImageSymbolWeight)weight sizeConstraint:(CGFloat)constant API_AVAILABLE(ios(13.0)){
if (@available(iOS 13.0, *)) {
UIImageSymbolConfiguration *config;
if(icon.isSymbolImage) config = [UIImageSymbolConfiguration configurationWithPointSize:size weight:weight];
else{
config = [UIImageSymbolConfiguration configurationWithPointSize:size];
icon = [icon imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
}
_iconImageView.image = [icon imageWithConfiguration:config];
} else {
_iconImageView.image = icon;
}
_iconImageView.tintColor = ThemeManager.textColor;
_iconImageView.hidden = (icon == nil);
// 图标约束
[NSLayoutConstraint activateConstraints:@[
[_iconImageView.centerXAnchor constraintEqualToAnchor:_headerView.leadingAnchor constant:_leadingTrailingPadding+_headerViewHeight/2-3],
[_iconImageView.centerYAnchor constraintEqualToAnchor:_headerView.centerYAnchor],
// [_iconImageView.topAnchor constraintEqualToAnchor:_headerView.topAnchor],
[_iconImageView.widthAnchor constraintEqualToConstant:_headerViewHeight+constant],
[_iconImageView.heightAnchor constraintEqualToConstant:_headerViewHeight+constant]
]];
[self updateLayout];
}
- (void)setLeadingTrailingPadding:(CGFloat)leadingTrailingPadding {
_leadingTrailingPadding = leadingTrailingPadding;
[self updateLayout];
}
- (void)setSectionTitle:(NSString *)sectionTitle {
_sectionTitle = sectionTitle;
_titleLabel.text = sectionTitle;
}
- (void)setExpanded:(BOOL)isExpanded {
self.isExpanded = !_expandable ? false : isExpanded;
[self updateViewForFoldState];
[[NSUserDefaults standardUserDefaults] setBool:self.isExpanded forKey:self.identifier];
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (void)setBackgroundColor:(UIColor *)backgroundColor {
[super setBackgroundColor:backgroundColor];
_backgroundColor = backgroundColor;
}
- (void)setRootStackViewSpacing:(CGFloat)rootStackViewSpacing {
_rootStackViewSpacing = rootStackViewSpacing;
_rootStackView.spacing = rootStackViewSpacing;
[self updateLayout];
}
- (void)addSubStackView:(UIStackView *)stackView {
[CATransaction begin];
[CATransaction setDisableActions:YES];
[UIView performWithoutAnimation:^{
if (![_subStackViews containsObject:stackView]) {
[_subStackViews addObject:stackView];
[_rootStackView addArrangedSubview:stackView];
//[_rootStackView layoutIfNeeded];
//[self updateLayout];
}
}];
[CATransaction commit];
// NSLog(@"rootStack height2: %f", _rootStackView.frame.size.height);
}
- (void)addToParentStack:(UIStackView *)parentStack {
[parentStack insertArrangedSubview:self atIndex:parentStack.arrangedSubviews.count];
self.translatesAutoresizingMaskIntoConstraints = NO;
[NSLayoutConstraint activateConstraints:@[
[self.leadingAnchor constraintEqualToAnchor:parentStack.leadingAnchor constant:0],
[self.trailingAnchor constraintEqualToAnchor:parentStack.trailingAnchor constant:0],
]];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
BOOL persistedFoldState = [defaults objectForKey:self.identifier] ? [defaults boolForKey:self.identifier] : YES;
[self setExpanded: MenuSectionView.overridePersistedFoldState ? _expandable : persistedFoldState];
}
- (void)removeSubStackView:(UIStackView *)stackView {
[CATransaction begin];
[CATransaction setDisableActions:YES];
if ([_subStackViews containsObject:stackView]) {
[_subStackViews removeObject:stackView];
[_rootStackView removeArrangedSubview:stackView];
[stackView removeFromSuperview];
[self updateLayout];
}
[CATransaction commit];
}
- (void)updateLayout {
// 更新边距约束
for (NSLayoutConstraint *constraint in self.constraints) {
if (constraint.firstItem == _rootStackView && constraint.firstAttribute == NSLayoutAttributeLeading) {
constraint.constant = _leadingTrailingPadding;
} else if (constraint.firstItem == _rootStackView && constraint.firstAttribute == NSLayoutAttributeTrailing) {
constraint.constant = -_leadingTrailingPadding;
}
}
for (NSLayoutConstraint *constraint in _headerView.constraints) {
if (constraint.firstItem == _iconImageView && constraint.firstAttribute == NSLayoutAttributeLeading) {
constraint.constant = _leadingTrailingPadding;
} else if (constraint.firstItem == _toggleButton && constraint.firstAttribute == NSLayoutAttributeTrailing) {
constraint.constant = -_leadingTrailingPadding;
}
}
[self setNeedsLayout];
[self layoutIfNeeded];
}
#pragma mark - Private Methods
- (BOOL)isIPhone {
return [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone;
}
- (void)toggleFold {
self.isExpanded = !_expandable ? false : !self.isExpanded;
if(!_expandable && self.lockedSectionHandler) self.lockedSectionHandler();
[self updateViewForFoldState];
[[NSUserDefaults standardUserDefaults] setBool:self.isExpanded forKey:self.identifier];
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (SettingsMenuMode)getSettingsMenuMode {
if ([self.delegate respondsToSelector:@selector(getSettingsMenuMode)]) {
return [self.delegate getSettingsMenuMode];
} else return AllSettings;
}
- (void)updateViewForFoldState {
// [self setupConstraints];
// don't run this repeatedly
NSInteger visibleCount = 0;
for (UIView *subview in _rootStackView.arrangedSubviews) {
if (!subview.isHidden) {
visibleCount++;
}
}
self.hidden = visibleCount == 0;
if (_isExpanded) {
_rootStackView.hidden = NO;
_separatorLine.hidden = NO;
CGSize fittingSize = [self.rootStackView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
//CGSize fittingSize = [self.rootStackView systemLayoutSizeFittingSize:UILayoutFittingExpandedSize];
CGFloat rootStackViewHeight = fittingSize.height;
fittingSize = [self.headerView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
CGFloat headerHeight = fittingSize.height;
self.heightConstraint.constant = headerHeight + _headerViewVerticalSpacing + rootStackViewHeight + _separatorLinePadding + 1;
[UIView animateWithDuration:0 animations:^{
self.toggleButton.transform = CGAffineTransformMakeRotation(M_PI/2);
[NSLayoutConstraint activateConstraints:@[
[self.rootStackView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor constant:-self.separatorLinePadding],
// [self.rootStackView.topAnchor constraintEqualToAnchor:self.headerView.bottomAnchor constant:0],
]];
[self layoutIfNeeded];
}];
} else {
// 折叠状态
CGSize fittingSize = [self.headerView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
_rootStackView.hidden = YES;
_separatorLine.hidden = NO;
_heightConstraint.constant = fittingSize.height + _headerViewVerticalSpacing;
// [self updateLayout];
[UIView animateWithDuration:0 animations:^{
self.toggleButton.transform = CGAffineTransformIdentity;
[NSLayoutConstraint activateConstraints:@[
[self.headerView.topAnchor constraintEqualToAnchor:self.topAnchor constant:0],
// [self.rootStackView.topAnchor constraintEqualToAnchor:self.headerView.bottomAnchor constant:0],
]];
[self layoutIfNeeded];
}];
}
if ([self.delegate respondsToSelector:@selector(hideOverlappedDynamicLabels)]) {
[self.delegate hideOverlappedDynamicLabels];
}
}
@end
+374
View File
@@ -0,0 +1,374 @@
//
// MenuSectionView.m -> MenuSectionView.swift
// VoidLink
//
// Created by True砖家 on 2025.5.18
// Copyright © 2025 True砖家 @ Bilibili. All rights reserved.
//
import UIKit
@objc
protocol MenuSectionDelegate: NSObjectProtocol {
func hideOverlappedDynamicLabels()
func getSettingsMenuMode() -> SettingsMenuMode
}
@objcMembers
class MenuSectionView: UIView {
static var overridePersistedFoldState = true
var rootStackView: UIStackView!
var leadingTrailingPadding: CGFloat = 5 {
didSet {
updateLayout()
}
}
var separatorLinePadding: CGFloat = 40
var titleLabel: UILabel!
var sectionTitle: String = "Section" {
didSet {
titleLabel?.text = sectionTitle
}
}
var identifier: String?
var isExpanded = true
var expandable = true
var lockedSectionHandler: (() -> Void)?
var rootStackViewSpacing: CGFloat = UIDevice.current.userInterfaceIdiom == .phone ? 10 : 12 {
didSet {
rootStackView?.spacing = rootStackViewSpacing
updateLayout()
}
}
var headerViewHeight: CGFloat = 37
var headerViewVerticalSpacing: CGFloat = 25
var subStackViews = NSMutableArray()
var iconImageView: UIImageView!
var separatorLine: UIView!
weak var delegate: MenuSectionDelegate?
private var toggleButton: UIButton!
private var toggleArea: UIButton!
private var heightConstraint: NSLayoutConstraint!
var headerView: UIView!
private var titleOffset: CGFloat = 41.5
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
layer.cornerRadius = 10
layer.masksToBounds = true
backgroundColor = .clear
headerView = UIButton()
headerView.translatesAutoresizingMaskIntoConstraints = false
headerView.accessibilityIdentifier = "sectionHeader"
addSubview(headerView)
iconImageView = UIImageView()
iconImageView.contentMode = .scaleAspectFit
iconImageView.translatesAutoresizingMaskIntoConstraints = false
iconImageView.tintColor = ThemeManager.textColor
headerView.addSubview(iconImageView)
titleLabel = UILabel()
titleLabel.text = sectionTitle
titleLabel.accessibilityIdentifier = "menuSectionTitleLabel"
titleLabel.font = .systemFont(ofSize: 19.5, weight: .medium)
titleLabel.textColor = ThemeManager.textColor
titleLabel.textAlignment = .left
titleLabel.translatesAutoresizingMaskIntoConstraints = false
headerView.addSubview(titleLabel)
toggleButton = UIButton(type: .system)
toggleArea = UIButton(type: .system)
if #available(iOS 13.0, *) {
let config = UIImage.SymbolConfiguration(pointSize: headerViewHeight * 0.31, weight: .bold)
toggleButton.setImage(UIImage(systemName: "chevron.right", withConfiguration: config), for: .normal)
}
toggleButton.addTarget(self, action: #selector(toggleFold), for: .touchUpInside)
toggleArea.addTarget(self, action: #selector(toggleFold), for: .touchUpInside)
toggleButton.translatesAutoresizingMaskIntoConstraints = false
toggleArea.translatesAutoresizingMaskIntoConstraints = false
toggleButton.contentEdgeInsets = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10)
headerView.addSubview(toggleButton)
headerView.addSubview(toggleArea)
rootStackView = UIStackView()
rootStackView.axis = .vertical
rootStackView.spacing = rootStackViewSpacing
rootStackView.translatesAutoresizingMaskIntoConstraints = false
addSubview(rootStackView)
separatorLine = UIView()
separatorLine.backgroundColor = ThemeManager.separatorColor
separatorLine.translatesAutoresizingMaskIntoConstraints = false
addSubview(separatorLine)
heightConstraint = heightAnchor.constraint(equalToConstant: 44)
heightConstraint.isActive = true
setupConstraints()
updateViewForFoldState()
lockedSectionHandler = {
NSLog("Null execution of lockedSectionHandler")
}
}
private func setupConstraints() {
NSLayoutConstraint.activate([
headerView.topAnchor.constraint(equalTo: topAnchor),
headerView.leadingAnchor.constraint(equalTo: leadingAnchor),
headerView.trailingAnchor.constraint(equalTo: trailingAnchor),
headerView.heightAnchor.constraint(equalToConstant: headerViewHeight),
])
if #available(iOS 13.0, *) {
NSLayoutConstraint.activate([
titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: titleOffset),
titleLabel.centerYAnchor.constraint(equalTo: headerView.centerYAnchor),
titleLabel.trailingAnchor.constraint(equalTo: toggleButton.leadingAnchor, constant: -8),
])
} else {
NSLayoutConstraint.activate([
titleLabel.leadingAnchor.constraint(equalTo: headerView.leadingAnchor),
titleLabel.centerYAnchor.constraint(equalTo: headerView.centerYAnchor),
titleLabel.trailingAnchor.constraint(equalTo: toggleButton.leadingAnchor, constant: -8),
])
}
NSLayoutConstraint.activate([
toggleButton.trailingAnchor.constraint(equalTo: headerView.trailingAnchor, constant: -leadingTrailingPadding),
toggleButton.centerYAnchor.constraint(equalTo: headerView.centerYAnchor),
toggleButton.widthAnchor.constraint(equalToConstant: headerViewHeight * 0.8),
toggleButton.heightAnchor.constraint(equalToConstant: headerViewHeight * 0.8),
])
NSLayoutConstraint.activate([
toggleArea.leadingAnchor.constraint(equalTo: headerView.leadingAnchor),
toggleArea.centerYAnchor.constraint(equalTo: headerView.centerYAnchor),
toggleArea.trailingAnchor.constraint(equalTo: toggleButton.leadingAnchor),
toggleArea.heightAnchor.constraint(equalToConstant: headerViewHeight + 25),
])
layoutIfNeeded()
NSLayoutConstraint.activate([
rootStackView.topAnchor.constraint(equalTo: topAnchor, constant: headerViewHeight + headerViewVerticalSpacing),
rootStackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: leadingTrailingPadding),
rootStackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -leadingTrailingPadding),
rootStackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -1),
])
clipsToBounds = false
NSLayoutConstraint.activate([
separatorLine.centerXAnchor.constraint(equalTo: centerXAnchor),
separatorLine.widthAnchor.constraint(equalTo: widthAnchor, constant: -5),
separatorLine.bottomAnchor.constraint(equalTo: bottomAnchor),
separatorLine.heightAnchor.constraint(equalToConstant: GenericUtils.menuSectionSeparatorWidth),
])
NSLayoutConstraint.activate([
headerView.topAnchor.constraint(equalTo: topAnchor),
rootStackView.topAnchor.constraint(equalTo: headerView.bottomAnchor),
])
}
@objc(setSectionWithIcon:size:sizeConstraint:)
func setSection(withIcon icon: UIImage?, size: CGFloat, sizeConstraint constant: CGFloat) {
var displayIcon = icon
if #available(iOS 13.0, *) {
let config: UIImage.SymbolConfiguration
if displayIcon?.isSymbolImage == true {
config = UIImage.SymbolConfiguration(pointSize: size, weight: .bold)
} else {
config = UIImage.SymbolConfiguration(pointSize: size)
displayIcon = displayIcon?.withRenderingMode(.alwaysTemplate)
}
iconImageView.image = displayIcon?.withConfiguration(config)
} else {
iconImageView.image = displayIcon
}
iconImageView.tintColor = ThemeManager.textColor
iconImageView.isHidden = displayIcon == nil
NSLayoutConstraint.activate([
iconImageView.centerXAnchor.constraint(equalTo: headerView.leadingAnchor, constant: leadingTrailingPadding + headerViewHeight / 2 - 3),
iconImageView.centerYAnchor.constraint(equalTo: headerView.centerYAnchor),
iconImageView.widthAnchor.constraint(equalToConstant: headerViewHeight + constant),
iconImageView.heightAnchor.constraint(equalToConstant: headerViewHeight + constant),
])
updateLayout()
}
@available(iOS 13.0, *)
@objc(setSectionWithIcon:size:weight:sizeConstraint:)
func setSection(withIcon icon: UIImage?, size: CGFloat, weight: UIImage.SymbolWeight, sizeConstraint constant: CGFloat) {
var displayIcon = icon
let config: UIImage.SymbolConfiguration
if displayIcon?.isSymbolImage == true {
config = UIImage.SymbolConfiguration(pointSize: size, weight: weight)
} else {
config = UIImage.SymbolConfiguration(pointSize: size)
displayIcon = displayIcon?.withRenderingMode(.alwaysTemplate)
}
iconImageView.image = displayIcon?.withConfiguration(config)
iconImageView.tintColor = ThemeManager.textColor
iconImageView.isHidden = displayIcon == nil
NSLayoutConstraint.activate([
iconImageView.centerXAnchor.constraint(equalTo: headerView.leadingAnchor, constant: leadingTrailingPadding + headerViewHeight / 2 - 3),
iconImageView.centerYAnchor.constraint(equalTo: headerView.centerYAnchor),
iconImageView.widthAnchor.constraint(equalToConstant: headerViewHeight + constant),
iconImageView.heightAnchor.constraint(equalToConstant: headerViewHeight + constant),
])
updateLayout()
}
@objc(setExpanded:)
func setExpanded(_ isExpanded: Bool) {
self.isExpanded = !expandable ? false : isExpanded
updateViewForFoldState()
if let identifier {
UserDefaults.standard.set(self.isExpanded, forKey: identifier)
UserDefaults.standard.synchronize()
}
}
func addSubStackView(_ stackView: UIStackView) {
CATransaction.begin()
CATransaction.setDisableActions(true)
UIView.performWithoutAnimation {
if !subStackViews.contains(stackView) {
subStackViews.add(stackView)
rootStackView.addArrangedSubview(stackView)
}
}
CATransaction.commit()
}
func add(toParentStack parentStack: UIStackView) {
parentStack.insertArrangedSubview(self, at: parentStack.arrangedSubviews.count)
translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
leadingAnchor.constraint(equalTo: parentStack.leadingAnchor),
trailingAnchor.constraint(equalTo: parentStack.trailingAnchor),
])
let persistedFoldState: Bool
if let identifier, UserDefaults.standard.object(forKey: identifier) != nil {
persistedFoldState = UserDefaults.standard.bool(forKey: identifier)
} else {
persistedFoldState = true
}
setExpanded(MenuSectionView.overridePersistedFoldState ? expandable : persistedFoldState)
headerView.accessibilityIdentifier = "\(self.headerView.accessibilityIdentifier ?? "sectionHeader")-\(self.identifier ?? "")"
}
func removeSubStackView(_ stackView: UIStackView) {
CATransaction.begin()
CATransaction.setDisableActions(true)
if subStackViews.contains(stackView) {
subStackViews.remove(stackView)
rootStackView.removeArrangedSubview(stackView)
stackView.removeFromSuperview()
updateLayout()
}
CATransaction.commit()
}
func updateLayout() {
for constraint in constraints {
if constraint.firstItem === rootStackView && constraint.firstAttribute == .leading {
constraint.constant = leadingTrailingPadding
} else if constraint.firstItem === rootStackView && constraint.firstAttribute == .trailing {
constraint.constant = -leadingTrailingPadding
}
}
for constraint in headerView.constraints {
if constraint.firstItem === iconImageView && constraint.firstAttribute == .leading {
constraint.constant = leadingTrailingPadding
} else if constraint.firstItem === toggleButton && constraint.firstAttribute == .trailing {
constraint.constant = -leadingTrailingPadding
}
}
setNeedsLayout()
layoutIfNeeded()
}
@objc func toggleFold() {
isExpanded = !expandable ? false : !isExpanded
if !expandable {
lockedSectionHandler?()
}
updateViewForFoldState()
if let identifier {
UserDefaults.standard.set(isExpanded, forKey: identifier)
UserDefaults.standard.synchronize()
}
}
func getSettingsMenuMode() -> SettingsMenuMode {
let allSettings = SettingsMenuMode(rawValue: 0)!
if delegate?.responds(to: #selector(MenuSectionDelegate.getSettingsMenuMode)) == true {
return delegate?.getSettingsMenuMode() ?? allSettings
}
return allSettings
}
func updateViewForFoldState() {
let visibleCount = rootStackView.arrangedSubviews.filter { !$0.isHidden }.count
isHidden = visibleCount == 0
if isExpanded {
rootStackView.isHidden = false
separatorLine.isHidden = false
var fittingSize = rootStackView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
let rootStackViewHeight = fittingSize.height
fittingSize = headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
let headerHeight = fittingSize.height
heightConstraint.constant = headerHeight + headerViewVerticalSpacing + rootStackViewHeight + separatorLinePadding + 1
UIView.animate(withDuration: 0) {
self.toggleButton.transform = CGAffineTransform(rotationAngle: .pi / 2)
NSLayoutConstraint.activate([
self.rootStackView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -self.separatorLinePadding),
])
self.layoutIfNeeded()
}
} else {
let fittingSize = headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
rootStackView.isHidden = true
separatorLine.isHidden = false
heightConstraint.constant = fittingSize.height + headerViewVerticalSpacing
UIView.animate(withDuration: 0) {
self.toggleButton.transform = .identity
NSLayoutConstraint.activate([
self.headerView.topAnchor.constraint(equalTo: self.topAnchor),
])
self.layoutIfNeeded()
}
}
if delegate?.responds(to: #selector(MenuSectionDelegate.hideOverlappedDynamicLabels)) == true {
delegate?.hideOverlappedDynamicLabels()
}
}
}
+45 -38
View File
@@ -106,7 +106,17 @@ static const NSUInteger MaxFramesInFlight = 3;
// best to tone-map Windows content
static BOOL useEDR = YES;
CFStringRef __currentColorSpace;
// Name of the colorspace most recently applied to the layer, for the stats overlay.
// Class-level because the stats path only has access to the class; guarded by a lock
// since it is written on the render thread and read from other threads, and multiple
// renderer instances can briefly overlap during a session switch.
static NSString *__currentColorSpace;
static void setCurrentColorSpaceName(NSString *name) {
@synchronized ([MetalVideoRenderer class]) {
__currentColorSpace = name;
}
}
@implementation MetalVideoRenderer {
dispatch_queue_t _sq;
@@ -226,10 +236,8 @@ CFStringRef __currentColorSpace;
_renderPassDescriptor = nil;
}
if (__currentColorSpace) {
CFRelease(__currentColorSpace);
__currentColorSpace = NULL;
}
// Do NOT clear __currentColorSpace here: a new renderer instance may already be
// live (session switch) and have set its own value.
}
#if !TARGET_OS_TV
@@ -303,9 +311,10 @@ CFStringRef __currentColorSpace;
// CAEDRMetadata is only available on iOS 16.0+
if (@available(iOS 16.0, tvOS 16.0, *)) {
if (frame.pixelBuffer) {
CFDataRef masteringDisplayColorVolume = CVBufferCopyAttachment(frame.pixelBuffer, kCVImageBufferMasteringDisplayColorVolumeKey, nil);
CFDataRef contentLightLevel = CVBufferCopyAttachment(frame.pixelBuffer, kCVImageBufferContentLightLevelInfoKey, nil);
CVPixelBufferRef imageBuffer = frame.imageBuffer;
if (imageBuffer) {
CFDataRef masteringDisplayColorVolume = CVBufferCopyAttachment(imageBuffer, kCVImageBufferMasteringDisplayColorVolumeKey, nil);
CFDataRef contentLightLevel = CVBufferCopyAttachment(imageBuffer, kCVImageBufferContentLightLevelInfoKey, nil);
// Configure optical output scale (100.0f is standard for HDR10)
const float opticalOutputScale = 100.0f;
@@ -404,9 +413,11 @@ CFStringRef __currentColorSpace;
paramBuffer.cscParams = (fullRange ? k_CscParams_Bt709Full : k_CscParams_Bt709Lim);
break;
case COLORSPACE_REC_2020: {
// Frames without a format description (e.g. produced for the AVSB
// backend) must not crash us here
CFDictionaryRef ext = [frame getFormatDescExtensions];
CFStringRef frame_trc = CFDictionaryGetValue(ext, kCVImageBufferTransferFunctionKey);
if (CFEqual(frame_trc, kCVImageBufferTransferFunction_SMPTE_ST_2084_PQ)) {
CFStringRef frame_trc = ext ? CFDictionaryGetValue(ext, kCVImageBufferTransferFunctionKey) : NULL;
if (frame_trc && CFEqual(frame_trc, kCVImageBufferTransferFunction_SMPTE_ST_2084_PQ)) {
isHDR = YES;
newColorSpace = CGColorSpaceCreateWithName(_nonFullHdrColorSpace);
newPixelFormat = _nonFullHdrPixelFormat;
@@ -477,11 +488,7 @@ CFStringRef __currentColorSpace;
});
}
#endif
if (__currentColorSpace) {
CFRelease(__currentColorSpace);
__currentColorSpace = NULL;
}
__currentColorSpace = CGColorSpaceCopyName(newColorSpace);
setCurrentColorSpaceName(newColorSpace ? CFBridgingRelease(CGColorSpaceCopyName(newColorSpace)) : nil);
CGColorSpaceRelease(newColorSpace);
}
@@ -592,15 +599,16 @@ CFStringRef __currentColorSpace;
FQLog(LOG_I, @"[%d / %.3f ms] Metal frame rendering", frame.frameNumber, frame.pts);
if (!frame.pixelBuffer) {
Log(LOG_W, @"Frame pixelBuffer is NULL, skipping render");
CVPixelBufferRef imageBuffer = frame.imageBuffer;
if (!imageBuffer) {
Log(LOG_W, @"Frame imageBuffer is NULL, skipping render");
return;
}
size_t planes = CVPixelBufferGetPlaneCount(frame.pixelBuffer);
size_t planes = CVPixelBufferGetPlaneCount(imageBuffer);
// For packed formats like BGRA, plane count is 0 but we treat it as 1 plane
OSType pixelFormatType = CVPixelBufferGetPixelFormatType(frame.pixelBuffer);
OSType pixelFormatType = CVPixelBufferGetPixelFormatType(imageBuffer);
BOOL isPackedFormat = (pixelFormatType == kCVPixelFormatType_32BGRA ||
pixelFormatType == kCVPixelFormatType_32ARGB);
if (isPackedFormat) {
@@ -695,7 +703,7 @@ CFStringRef __currentColorSpace;
if (isPackedFormat) {
// Handle packed BGRA format - iOS/macOS uses BGRA internally
// Check if the pixel buffer has IOSurface backing
CFTypeRef ioSurface = CVPixelBufferGetIOSurface(frame.pixelBuffer);
CFTypeRef ioSurface = CVPixelBufferGetIOSurface(imageBuffer);
if (!ioSurface) {
Log(LOG_E, @"CVPixelBuffer does not have IOSurface backing - cannot create Metal texture");
return;
@@ -703,35 +711,35 @@ CFStringRef __currentColorSpace;
CVReturn err = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
_textureCache,
frame.pixelBuffer,
imageBuffer,
NULL,
MTLPixelFormatBGRA8Unorm,
CVPixelBufferGetWidth(frame.pixelBuffer),
CVPixelBufferGetHeight(frame.pixelBuffer),
CVPixelBufferGetWidth(imageBuffer),
CVPixelBufferGetHeight(imageBuffer),
0, // planeIndex must be 0 for non-planar
&_cvMetalTextures[0]);
if (err != kCVReturnSuccess) {
Log(LOG_E, @"CVMetalTextureCacheCreateTextureFromImage() failed for BGRA: %d", err);
Log(LOG_E, @"PixelBuffer info - format: 0x%X, width: %zu, height: %zu, IOSurface: %p",
pixelFormatType,
CVPixelBufferGetWidth(frame.pixelBuffer),
CVPixelBufferGetHeight(frame.pixelBuffer),
CVPixelBufferGetWidth(imageBuffer),
CVPixelBufferGetHeight(imageBuffer),
ioSurface);
return;
} else {
id<MTLTexture> texture = CVMetalTextureGetTexture(_cvMetalTextures[0]);
Log(LOG_I, @"DEBUG: Created BGRA texture: format=%lu, width=%zu, height=%zu",
(unsigned long)MTLPixelFormatBGRA8Unorm,
CVPixelBufferGetWidth(frame.pixelBuffer),
CVPixelBufferGetHeight(frame.pixelBuffer));
CVPixelBufferGetWidth(imageBuffer),
CVPixelBufferGetHeight(imageBuffer));
}
} else {
// Handle planar YUV formats
size_t actualPlanes = CVPixelBufferGetPlaneCount(frame.pixelBuffer);
size_t actualPlanes = CVPixelBufferGetPlaneCount(imageBuffer);
for (size_t i = 0; i < actualPlanes; i++) {
MTLPixelFormat fmt;
switch (CVPixelBufferGetPixelFormatType(frame.pixelBuffer)) {
switch (CVPixelBufferGetPixelFormatType(imageBuffer)) {
case kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange:
case kCVPixelFormatType_444YpCbCr8BiPlanarVideoRange:
case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange:
@@ -747,17 +755,17 @@ CFStringRef __currentColorSpace;
break;
default:
Log(LOG_E, @"Unknown pixel format: %@", CVPixelBufferGetPixelFormatType(frame.pixelBuffer));
Log(LOG_E, @"Unknown pixel format: %@", CVPixelBufferGetPixelFormatType(imageBuffer));
return;
}
CVReturn err = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
_textureCache,
frame.pixelBuffer,
imageBuffer,
NULL,
fmt,
CVPixelBufferGetWidthOfPlane(frame.pixelBuffer, i),
CVPixelBufferGetHeightOfPlane(frame.pixelBuffer, i),
CVPixelBufferGetWidthOfPlane(imageBuffer, i),
CVPixelBufferGetHeightOfPlane(imageBuffer, i),
i,
&_cvMetalTextures[i]);
if (err != kCVReturnSuccess) {
@@ -779,7 +787,7 @@ CFStringRef __currentColorSpace;
[renderEncoder setFragmentTexture:CVMetalTextureGetTexture(_cvMetalTextures[0]) atIndex:0];
} else {
// For planar formats, set multiple textures
size_t actualPlanes = CVPixelBufferGetPlaneCount(frame.pixelBuffer);
size_t actualPlanes = CVPixelBufferGetPlaneCount(imageBuffer);
for (size_t i = 0; i < actualPlanes; i++) {
[renderEncoder setFragmentTexture:CVMetalTextureGetTexture(_cvMetalTextures[i]) atIndex:i];
}
@@ -813,7 +821,7 @@ CFStringRef __currentColorSpace;
// signal semaphore, compute GPU time average, and clear textures
__block dispatch_semaphore_t block_semaphore = _inFlightSemaphore;
__block size_t texturesToClean = isPackedFormat ? 1 : CVPixelBufferGetPlaneCount(frame.pixelBuffer);
__block size_t texturesToClean = isPackedFormat ? 1 : CVPixelBufferGetPlaneCount(imageBuffer);
[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> cb) {
dispatch_semaphore_signal(block_semaphore);
@@ -902,10 +910,9 @@ CFStringRef __currentColorSpace;
}
+ (NSString *)currentColorSpace {
if (__currentColorSpace) {
return (__bridge NSString *)__currentColorSpace;
@synchronized ([MetalVideoRenderer class]) {
return __currentColorSpace;
}
return nil;
}
@end
+3 -1
View File
@@ -24,7 +24,9 @@
@interface MetalView : UIView <CALayerDelegate>
@property (nonatomic, nonnull, readonly) CAMetalLayer *metalLayer;
@property (nonatomic, nullable) id<MetalViewDelegate> delegate;
// weak: the delegate (MetalViewController) strongly owns this view; a strong
// reference here creates a retain cycle that leaks the whole Metal stack.
@property (nonatomic, weak, nullable) id<MetalViewDelegate> delegate;
@property (nonatomic) float framerate;
- (void)initCommon;
+18 -1
View File
@@ -45,10 +45,20 @@
Log(LOG_I, @"[MetalView] sending renderThread a cancel message");
[_renderThread cancel];
Log(LOG_I, @"[MetalView] waiting on renderThread to finish");
while (!_renderThread.isFinished) {
// Bounded wait only: shutdown typically runs on the main thread, and the render
// thread may be blocked in a dispatch_sync onto the main queue (layer colorspace
// changes). Spinning here forever would deadlock; if we time out, the thread
// exits on its own once the main queue is serviced again (it retains the view
// via its block, so this is safe).
CFTimeInterval deadline = CACurrentMediaTime() + 1.0;
while (!_renderThread.isFinished && CACurrentMediaTime() < deadline) {
usleep(100);
}
if (_renderThread.isFinished) {
Log(LOG_I, @"[MetalView] renderThread has finished");
} else {
Log(LOG_W, @"[MetalView] renderThread still busy after 1s, letting it exit asynchronously");
}
_renderThread = nil;
}
}
@@ -68,6 +78,13 @@
return;
}
// Never run two render loops at once (the view can move between windows,
// e.g. external display or re-parenting)
if (_renderThread) {
Log(LOG_W, @"[MetalView] movedToWindow with a live renderThread, restarting it");
[self shutdown];
}
// Render on a new thread
_renderThread = [[NSThread alloc] initWithBlock:^{
while (![NSThread currentThread].isCancelled) {
+4
View File
@@ -24,4 +24,8 @@
- (void)pauseRendering;
- (void)resumeRendering;
// Idempotent teardown: stops the render thread, display link and renderer.
// Must be called when the stream session ends; also runs from viewDidDisappear.
- (void)shutdown;
@end
+25 -8
View File
@@ -109,13 +109,18 @@
}
- (void)waitToRenderTo:(nonnull CAMetalLayer *)layer {
// Skip waiting when renderer is paused
if (_renderer.isStopping) {
// Snapshot into a strong local: shutdown (main thread) can nil _renderer while
// the render thread is inside this method.
MetalVideoRenderer *renderer = _renderer;
// Skip waiting when renderer is paused or gone
if (!renderer || renderer.isStopping) {
_frameSlotAcquired = NO;
return;
}
if (@available(iOS 13.0, *)) {
_frameSlotAcquired = [_renderer waitToRenderTo:layer];
_frameSlotAcquired = [renderer waitToRenderTo:layer];
}
if (_frameSlotAcquired) {
@@ -128,22 +133,27 @@
if (!_frameSlotAcquired) {
return;
}
CFTimeInterval timeout = (1.0f / _framerate) - _renderer.averageGPUTime;
// Snapshot for the same reason as in waitToRenderTo
MetalVideoRenderer *renderer = _renderer;
if (!renderer) {
return;
}
CFTimeInterval timeout = (1.0f / _framerate) - renderer.averageGPUTime;
Frame *frame = [_frameQueue dequeueWithTimeoutSync:timeout];
if (!_renderer.isStopping) {
if (!renderer.isStopping) {
// Only render if not paused
if (frame) {
//if (@available(iOS 13.0, *)) {
[_renderer renderFrame:frame toLayer:layer];
[renderer renderFrame:frame toLayer:layer];
//}
} else {
dispatch_semaphore_signal([_renderer inFlightSemaphore]);
dispatch_semaphore_signal([renderer inFlightSemaphore]);
}
} else {
// When paused, we still dequeue frames to prevent accumulation
// but don't render them. Also sleep a bit to reduce CPU usage
dispatch_semaphore_signal([_renderer inFlightSemaphore]);
dispatch_semaphore_signal([renderer inFlightSemaphore]);
usleep(100000);
}
}
@@ -177,11 +187,18 @@
Log(LOG_I, @"[MetalViewController] viewDidDisappear");
[self shutdown];
}
- (void)shutdown {
if (_displayLink) {
[_displayLink invalidate];
_displayLink = nil;
}
// Stop the renderer before the render thread: isStopping makes the thread's
// renderFrame return early, so it can't enter the dispatch_sync-to-main path
// while we wait for it below.
if (_renderer) {
[_renderer shutdown];
_renderer = nil;
+2 -2
View File
@@ -142,7 +142,7 @@
host = [[TemporaryHost alloc] init];
host.activeAddress = host.address = hostAddress;
if(!host.activeAddressPool) host.activeAddressPool = [[NSMutableSet alloc] init];
[host.activeAddressPool addObject:host.activeAddress];
if(host.activeAddress) [host.activeAddressPool addObject:host.activeAddress];
host.state = StateOnline;
[serverInfoResponse populateHost:host];
@@ -288,7 +288,7 @@
existingHost.externalAddress = host.externalAddress;
}
existingHost.activeAddress = host.activeAddress;
[existingHost.activeAddressPool addObject:host.activeAddress];
if(host.activeAddress) [existingHost.activeAddressPool addObject:host.activeAddress];
existingHost.state = host.state;
return NO; // returning a "NO" here means Updating addresses of existing host
}
+1 -1
View File
@@ -117,7 +117,7 @@ static const float POLL_RATE = 2.0f; // Poll every 2 seconds
if (receivedResponse) {
_host.activeAddress = address;
if(!_host.activeAddressPool) _host.activeAddressPool = [[NSMutableSet alloc] init];
[_host.activeAddressPool addObject:address];
if(address) [_host.activeAddressPool addObject:address];
[serverInfoResp populateHost:_host];
// Update the database using the response

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