Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e340b6d6a | ||
|
|
ec12492ea7 | ||
|
|
13ccbb3866 | ||
|
|
20d6e221a4 | ||
|
|
2bfddf255a | ||
|
|
0e8903d233 | ||
|
|
78f385d732 | ||
|
|
bd306ba07f | ||
|
|
ececc03d46 | ||
|
|
6202a53153 | ||
|
|
84b6b22f75 | ||
|
|
f8388658d8 | ||
|
|
3c36964b5c | ||
|
|
089515443d | ||
|
|
e63e5f2120 | ||
|
|
5a5238afee | ||
|
|
e00e2f8000 | ||
|
|
8795a9f173 | ||
|
|
5bd938abc5 | ||
|
|
514dbb98e7 | ||
|
|
106b1548d4 | ||
|
|
cacf2b1951 | ||
|
|
f1a4a519c3 | ||
|
|
4f0be34947 | ||
|
|
9da1121cdb | ||
|
|
0e92af13eb | ||
|
|
cdd613cc63 | ||
|
|
58535c9997 | ||
|
|
48647d4d89 | ||
|
|
16c54ac966 | ||
|
|
4214afd2b7 | ||
|
|
d28aa43319 | ||
|
|
4983aecb4c | ||
|
|
5ba566ceee | ||
|
|
b0fd0222df | ||
|
|
7b7bb3837c | ||
|
|
6879bf6379 | ||
|
|
fc2b1689d0 | ||
|
|
9c9ef4a13f | ||
|
|
a92010f5b1 | ||
|
|
813005694a | ||
|
|
11dd542cb3 | ||
|
|
949a0e0bfb | ||
|
|
ed8a246bad | ||
|
|
cfb8946ac3 | ||
|
|
660a015f8b | ||
|
|
54ac7f6522 | ||
|
|
a01af9ee21 | ||
|
|
71b7634069 | ||
|
|
32718071e0 | ||
|
|
30d602af29 | ||
|
|
f0043a763b | ||
|
|
516648cf34 | ||
|
|
5bd9c6bd98 | ||
|
|
3d82009d4a | ||
|
|
51c13faa0c | ||
|
|
e0aff4b532 | ||
|
|
b0c16922a2 | ||
|
|
ef5d839f5e | ||
|
|
4a3ca7774e | ||
|
|
8f93cce257 | ||
|
|
9a634da3ed | ||
|
|
6460036661 | ||
|
|
c968a1df2a | ||
|
|
54c106e01e | ||
|
|
41a5855211 | ||
|
|
2652c2a99b | ||
|
|
e987965765 | ||
|
|
a7d7a7975f | ||
|
|
c79929f37e | ||
|
|
d286c8b073 | ||
|
|
2cef75fb32 | ||
|
|
ca17c1cc66 | ||
|
|
98e8305c94 | ||
|
|
45ccfa0c7f | ||
|
|
7d7b909c66 | ||
|
|
d6c1059e99 | ||
|
|
9c43e39ffb | ||
|
|
adb5091dc2 | ||
|
|
7d3c2d93ac | ||
|
|
f4b161ed06 | ||
|
|
d57334265b | ||
|
|
f7efa48ae8 | ||
|
|
deb335efc3 | ||
|
|
f039f5af29 | ||
|
|
286b5e94e6 | ||
|
|
1a719e11e6 | ||
|
|
becfc596c1 | ||
|
|
1b76d0e0a6 | ||
|
|
53209ce89e | ||
|
|
384724ff27 | ||
|
|
e2eccd7968 | ||
|
|
f762ecc3b2 | ||
|
|
fa0b864933 | ||
|
|
e0f3921a75 | ||
|
|
f698ad2fb6 | ||
|
|
a0179cc6cc | ||
|
|
511e7c0e4b | ||
|
|
e734973be1 | ||
|
|
9979408b1a | ||
|
|
a5388bb60d | ||
|
|
4c32ec59d6 | ||
|
|
af1f0e75c7 | ||
|
|
3aa7a90a2f | ||
|
|
ec956218fd | ||
|
|
02fcdcecea | ||
|
|
3575e84165 | ||
|
|
90116e7ff6 | ||
|
|
ffe48a5382 | ||
|
|
c4ddc0c4fc | ||
|
|
23299ad73d | ||
|
|
accd07f808 | ||
|
|
ed39f85508 | ||
|
|
e8349b2a6f | ||
|
|
c36051fffc | ||
|
|
5f4fe6a040 | ||
|
|
843570dbca | ||
|
|
bb44b2067e | ||
|
|
2c2088e4b1 | ||
|
|
a7088c70e5 | ||
|
|
091fd828ab | ||
|
|
d168ffbd5a | ||
|
|
0af4c70e16 | ||
|
|
e402c17fa6 | ||
|
|
ab9b2509ff | ||
|
|
535d6009ad | ||
|
|
09981df37e | ||
|
|
f7eae4cee9 | ||
|
|
ab14e18b4a | ||
|
|
1630f7987a | ||
|
|
5a9bcab368 | ||
|
|
f7b118b671 | ||
|
|
b49f4a6891 | ||
|
|
3fd0c1b397 | ||
|
|
9282c3f404 | ||
|
|
ef3fca754f | ||
|
|
c08bb232cd | ||
|
|
2d1a1573e1 | ||
|
|
92b65b4a17 | ||
|
|
f72e0f6292 | ||
|
|
57a8c22a3f | ||
|
|
4195f97d56 | ||
|
|
c734030e87 | ||
|
|
96d997d41e | ||
|
|
023cc0b583 | ||
|
|
57dac128e2 | ||
|
|
77e0048c57 | ||
|
|
fe6954cb9c | ||
|
|
237f03a501 | ||
|
|
94f61ebf0c | ||
|
|
c3a0004c68 | ||
|
|
d7aa5cc9fc | ||
|
|
26c1d068a8 | ||
|
|
35ceebfe0d | ||
|
|
5daf158908 | ||
|
|
8f12a4f598 | ||
|
|
233a23c6c8 | ||
|
|
cf48d4d7c3 | ||
|
|
5bf8eeb873 | ||
|
|
8f5d58a25c | ||
|
|
363b91b244 | ||
|
|
efc34ae1fc | ||
|
|
9e858520c8 | ||
|
|
20aba200b3 | ||
|
|
cb235d9255 | ||
|
|
b0224d502a | ||
|
|
ce67aefc3b | ||
|
|
e8d864ff66 | ||
|
|
9e2ef6cc0d | ||
|
|
9d2df484d1 | ||
|
|
d28573dcd4 | ||
|
|
c3cb31d443 | ||
|
|
b258c58d59 | ||
|
|
d8e83d1704 | ||
|
|
e78a351c28 | ||
|
|
5ee443337c | ||
|
|
4dc96ba189 | ||
|
|
ecacb1afa7 | ||
|
|
7e21292a62 | ||
|
|
9104a7b7b9 | ||
|
|
ecc7a79a8c | ||
|
|
a1a70fcca2 | ||
|
|
d769c98e22 | ||
|
|
ce59a9eb05 | ||
|
|
b2984e0af5 | ||
|
|
bed2232aff | ||
|
|
e87f0f1aa0 | ||
|
|
9807a77d9b | ||
|
|
3e1423ff46 | ||
|
|
a4f805db26 | ||
|
|
0195aeb765 | ||
|
|
fcfe83572f | ||
|
|
d61942caec | ||
|
|
515360166c | ||
|
|
5c5507b02f | ||
|
|
7e0d2af5b7 | ||
|
|
780a0e7ab8 | ||
|
|
9359333b15 | ||
|
|
819aa436e8 | ||
|
|
5beee0ffe4 | ||
|
|
30aa9a6b8d | ||
|
|
7e088acd49 | ||
|
|
f702f726b7 | ||
|
|
46834a8a8b | ||
|
|
ca69fd62d9 | ||
|
|
8fd43d47dd | ||
|
|
732ec9a9cd | ||
|
|
a0d1982a14 | ||
|
|
bb63183a51 | ||
|
|
707beddcae | ||
|
|
c4c896e43e | ||
|
|
e2acf5ac4b | ||
|
|
fcecae3a87 | ||
|
|
55a2bb7576 | ||
|
|
6b085982f1 | ||
|
|
94f1c625be | ||
|
|
b0842ea4dc | ||
|
|
49431b5eba | ||
|
|
68ee5d879b | ||
|
|
984a1acd48 | ||
|
|
00b06b7b70 | ||
|
|
84adf3ac95 | ||
|
|
9cd72271ca | ||
|
|
e71ce58d9b | ||
|
|
eb03b055ea | ||
|
|
5af39e8fb6 | ||
|
|
b12cb96416 | ||
|
|
7f4108c4e3 | ||
|
|
be48a48704 | ||
|
|
88bc5c7551 | ||
|
|
7cd857a38b | ||
|
|
60e92c458e | ||
|
|
605f276a71 | ||
|
|
210ca4190a | ||
|
|
bd2d9c05bc | ||
|
|
ae526ecddf | ||
|
|
37d7949304 | ||
|
|
6092839004 | ||
|
|
31dc13b87d | ||
|
|
dbe91f5b07 | ||
|
|
d092b93a29 | ||
|
|
eff35450f6 | ||
|
|
6204feefc0 | ||
|
|
924374bccd | ||
|
|
eac8d15240 | ||
|
|
a55d840bce | ||
|
|
22aa0a5249 | ||
|
|
c3358efa9d | ||
|
|
e59f3ba1ab | ||
|
|
42f29074fa | ||
|
|
6299401286 | ||
|
|
deba65857d | ||
|
|
bd9dccd834 | ||
|
|
2410222c19 | ||
|
|
2c787a8d77 | ||
|
|
92fca9bca1 | ||
|
|
4dd1aad123 | ||
|
|
d8ac06ddca |
@@ -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/"
|
||||
@@ -4,6 +4,7 @@
|
||||
.DS_Store
|
||||
Build
|
||||
DerivedData
|
||||
build-home/
|
||||
*.xccheckout
|
||||
.github
|
||||
README.md
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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, *)) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 |
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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];
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]]);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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){
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#import "RelativeTouchHandler.h"
|
||||
#import "DataManager.h"
|
||||
#import "VoidLink-Swift.h"
|
||||
|
||||
#include <Limelight.h>
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||