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 | ||
|
|
691aa9c899 | ||
|
|
bd9dccd834 | ||
|
|
0dd419a5a3 | ||
|
|
2410222c19 | ||
|
|
1269c46f56 | ||
|
|
2c787a8d77 | ||
|
|
92fca9bca1 | ||
|
|
f8badd51c5 | ||
|
|
ed8bf5af9e | ||
|
|
b2521de84e | ||
|
|
4dd1aad123 | ||
|
|
165b5e6638 | ||
|
|
d8ac06ddca | ||
|
|
e742466409 | ||
|
|
201bf5b447 | ||
|
|
7bed913a1e | ||
|
|
e067fd8fa2 | ||
|
|
ba7d5feb81 | ||
|
|
4a9e7ad2a1 | ||
|
|
4b56a44a47 | ||
|
|
bc98b43de2 | ||
|
|
16b3df5279 | ||
|
|
d202f2f6e3 | ||
|
|
142474eac8 | ||
|
|
acc89dba39 | ||
|
|
8cd366d2cd | ||
|
|
2d67c1b22b | ||
|
|
77b0ef55c9 | ||
|
|
d76f404a4c | ||
|
|
d0b45a019f | ||
|
|
0728e999eb | ||
|
|
f325e9a206 | ||
|
|
49b03e65b2 | ||
|
|
2bc18e2f02 | ||
|
|
dadb292676 | ||
|
|
2c9902ce77 | ||
|
|
6d36219e88 | ||
|
|
470862dbe0 | ||
|
|
f6017824ee | ||
|
|
adeb77cf65 | ||
|
|
9bc080815a | ||
|
|
d03eebb5ba | ||
|
|
ca453d8059 | ||
|
|
cb8126d201 | ||
|
|
1626f30bb6 | ||
|
|
47b5ec2678 | ||
|
|
ccd032834d | ||
|
|
c7ffd51cec | ||
|
|
38f8c50d7f | ||
|
|
fd653b87af | ||
|
|
9bf02e477b | ||
|
|
4440c668f1 | ||
|
|
63d5417a2c | ||
|
|
28d9a0faf2 | ||
|
|
04845df09c | ||
|
|
85df511a23 | ||
|
|
4ecb8bfede | ||
|
|
6008865a41 | ||
|
|
02926b2c6b | ||
|
|
49c5fd308e | ||
|
|
7eeff3bc35 | ||
|
|
6e99889681 | ||
|
|
8cbd09fe9a | ||
|
|
edc703c534 | ||
|
|
f85d545c17 | ||
|
|
0830173e59 | ||
|
|
0e21e3a9ed | ||
|
|
6356427454 | ||
|
|
4dc6220f28 | ||
|
|
03fcfaff28 | ||
|
|
d5c96f77cb | ||
|
|
bb6f2f0c0d | ||
|
|
8d54ed9316 | ||
|
|
44123c74d9 | ||
|
|
fdf2548fe3 | ||
|
|
bee72bd854 | ||
|
|
8ec8f32935 | ||
|
|
1d33feec8b | ||
|
|
80af6a9636 | ||
|
|
94eb4dd8a0 | ||
|
|
d3ce55cbd6 | ||
|
|
0b942caa98 | ||
|
|
afc6289a56 | ||
|
|
79684598ab | ||
|
|
b901f2f506 | ||
|
|
14294009f0 | ||
|
|
adbebd23c8 | ||
|
|
3973e13706 | ||
|
|
e56e68dd6f | ||
|
|
c3fcff8d29 | ||
|
|
0152f48886 | ||
|
|
bbd2e37067 | ||
|
|
8377b5062a | ||
|
|
086f1af974 | ||
|
|
652e989927 | ||
|
|
5480e15595 | ||
|
|
375670beb4 | ||
|
|
fa164f0e02 | ||
|
|
a9ede7a904 | ||
|
|
fa32d157ca | ||
|
|
10839f88d8 | ||
|
|
8fa909dea8 | ||
|
|
94250624a8 | ||
|
|
6f264441ab | ||
|
|
03cf463182 | ||
|
|
3e29501b23 | ||
|
|
600b882929 | ||
|
|
be69af925a | ||
|
|
d14ee640c5 | ||
|
|
ac6c688054 | ||
|
|
8f8e86ab4f | ||
|
|
ab1d6bfa90 | ||
|
|
e038601b37 | ||
|
|
008beb9444 | ||
|
|
835d9c9ef6 | ||
|
|
e29e5bd650 | ||
|
|
fecdbec31c | ||
|
|
66fdb607d8 | ||
|
|
6d4f192cbe | ||
|
|
4a59af7c20 | ||
|
|
156b363049 | ||
|
|
c68540c169 | ||
|
|
f89372e265 | ||
|
|
c134dffec2 | ||
|
|
c2d4527d7e | ||
|
|
2071b89d18 | ||
|
|
46902afdc4 | ||
|
|
d23e640977 | ||
|
|
9c18ab4142 | ||
|
|
22e270c4df | ||
|
|
16bd288e70 | ||
|
|
39643153df | ||
|
|
2cf9fdfd40 | ||
|
|
f5226562ae | ||
|
|
32ad5d2f8a | ||
|
|
245f47f660 | ||
|
|
17e3fd07f9 | ||
|
|
cdbe480863 | ||
|
|
189ad76f38 | ||
|
|
d8528fdc81 | ||
|
|
c257a8cab0 | ||
|
|
2912a3a5cb | ||
|
|
75f1d4d392 | ||
|
|
a6159cc785 | ||
|
|
38affb4981 | ||
|
|
e64090d88f | ||
|
|
ef60aeeead | ||
|
|
25112b2c27 | ||
|
|
c0b1326dbd | ||
|
|
f77e533dd6 | ||
|
|
4b001f9366 | ||
|
|
41eadce89a | ||
|
|
46194a5aa0 | ||
|
|
3630880454 | ||
|
|
a59ffacd3a | ||
|
|
03817bcfd7 | ||
|
|
2c4751bef9 | ||
|
|
7caa778ff8 | ||
|
|
3977d342ba | ||
|
|
4e83421104 | ||
|
|
adafacbf81 | ||
|
|
053893005b | ||
|
|
838af88d64 | ||
|
|
573ed07b4f | ||
|
|
aad8673ff0 | ||
|
|
b6a58ea8ab | ||
|
|
8c7bf18b65 | ||
|
|
3823c95582 | ||
|
|
ad0f3dca30 | ||
|
|
5248bbe9c3 | ||
|
|
1262a7b41f | ||
|
|
85611bcab5 | ||
|
|
46b5d9522d | ||
|
|
6dc9ee569b | ||
|
|
9916f64065 | ||
|
|
0ad6b13fae | ||
|
|
5bbdb76d21 | ||
|
|
cbf42b1e52 | ||
|
|
72867d855d | ||
|
|
7d5b984157 | ||
|
|
a3d5771b14 | ||
|
|
1338b4fb85 | ||
|
|
379d965769 | ||
|
|
d7bc73250d | ||
|
|
349daef47f | ||
|
|
138658c635 | ||
|
|
e058839faf | ||
|
|
d4d5146f66 | ||
|
|
85f4838067 | ||
|
|
f23108a3ba | ||
|
|
1623fc34cf | ||
|
|
d244c9bd1f | ||
|
|
e53e997cd4 | ||
|
|
5c646ef3d2 | ||
|
|
f01d36d4a0 | ||
|
|
37fc7e2cb4 | ||
|
|
ea68353328 | ||
|
|
da4bfb851c | ||
|
|
6a1f85975f | ||
|
|
700552bd75 | ||
|
|
5750b99af3 | ||
|
|
cc35e6b3a4 | ||
|
|
3b4800eb78 | ||
|
|
a2855ef522 | ||
|
|
935406b75b | ||
|
|
e933c5de60 | ||
|
|
a23a5491ae | ||
|
|
1ad8f51b7a | ||
|
|
4d1619346d | ||
|
|
df8c273cb9 | ||
|
|
6eca97b4aa | ||
|
|
39f8398b10 | ||
|
|
e32a356cb5 | ||
|
|
e0c556da10 | ||
|
|
a3f9e07cf7 | ||
|
|
ff40e55cd1 | ||
|
|
e548bda73a | ||
|
|
cf11b06f3e | ||
|
|
a799c39aff | ||
|
|
ff230b3433 | ||
|
|
a9f4db52b5 | ||
|
|
071e2a764a | ||
|
|
e050c914bb | ||
|
|
cd9ef74f3f | ||
|
|
644687a50c | ||
|
|
e7132e3371 | ||
|
|
7223ab3be5 | ||
|
|
99952281cb | ||
|
|
9c518dfeb6 | ||
|
|
28e2f13e87 | ||
|
|
4a9e109de5 | ||
|
|
f5b56ca67f | ||
|
|
b7ad4d3562 | ||
|
|
94a8caa5df | ||
|
|
b650ac0ca3 | ||
|
|
9dda594519 | ||
|
|
0ea6248773 | ||
|
|
fee003b824 | ||
|
|
cd415cc639 | ||
|
|
10ceac2ce3 | ||
|
|
86a0ff46ee | ||
|
|
3887ad0c4c | ||
|
|
58f534fcc8 | ||
|
|
f8e14006c1 | ||
|
|
a959a346b0 | ||
|
|
65459b3f4e | ||
|
|
2216899204 | ||
|
|
5fb4887c67 | ||
|
|
6ff2add9dd | ||
|
|
c963c1254d | ||
|
|
8afce40e81 | ||
|
|
e44504bc48 | ||
|
|
11b79dda5b | ||
|
|
8cee8b679c | ||
|
|
1cb9360d83 | ||
|
|
297f5ad7e6 | ||
|
|
3f2a011d7b | ||
|
|
dd0c36bb20 | ||
|
|
aff1bdfe89 | ||
|
|
00533f5d3b | ||
|
|
eff682bc05 | ||
|
|
16a210efa2 | ||
|
|
99a2a4ca1a | ||
|
|
67af68fcf1 | ||
|
|
b96e2d1bdf | ||
|
|
2bb0ce086f | ||
|
|
9bc518dc68 | ||
|
|
f60433ff3f | ||
|
|
b4493707ca | ||
|
|
0fe10d8654 | ||
|
|
87d9590f3b | ||
|
|
eeff9b74da | ||
|
|
ceebd19c0e | ||
|
|
00e0566dd4 | ||
|
|
acd4a04076 | ||
|
|
ac0aa29578 | ||
|
|
9a053d9ed8 | ||
|
|
9124a1fee5 | ||
|
|
858e7c7440 | ||
|
|
e5669e2a30 | ||
|
|
431b32b680 | ||
|
|
7dce11b4c4 | ||
|
|
9fca3a5984 | ||
|
|
733d5c38f3 | ||
|
|
3834b5a604 | ||
|
|
07e36029d0 | ||
|
|
689221f10e | ||
|
|
60dacdf05f | ||
|
|
1614bb0609 | ||
|
|
a386867dd0 | ||
|
|
e177d6eaea | ||
|
|
464492529e | ||
|
|
b950882846 | ||
|
|
63808c9e9c | ||
|
|
76d138d64d | ||
|
|
1232644268 | ||
|
|
2e776baec9 | ||
|
|
8fb4829aef | ||
|
|
fcbc844587 | ||
|
|
aecb1163e3 | ||
|
|
0e51a49898 | ||
|
|
adce543c45 | ||
|
|
83a32f96e6 | ||
|
|
6c72f0331b | ||
|
|
92f1fbf33b | ||
|
|
424fe2c2f0 | ||
|
|
f5d629c62c | ||
|
|
19fa975b6d | ||
|
|
a4951309d3 | ||
|
|
3ec53c29d4 | ||
|
|
3fcaafc42b | ||
|
|
5318acc427 | ||
|
|
daa4fc80eb | ||
|
|
c094d74d8e | ||
|
|
5418936816 | ||
|
|
c3b7679268 | ||
|
|
d12b83d1ef | ||
|
|
32b78d1a4c | ||
|
|
71f66fe8c0 | ||
|
|
d054bbe35f | ||
|
|
bd1c4e3cc9 | ||
|
|
928b765700 | ||
|
|
45acc28384 | ||
|
|
4e65abf6b1 | ||
|
|
d818f8a7ce | ||
|
|
c0850ce9d2 | ||
|
|
c3941df7e9 | ||
|
|
b1b46b3ec0 | ||
|
|
dff6679a90 | ||
|
|
4ab21aed0e | ||
|
|
d67a40f155 | ||
|
|
b506917382 | ||
|
|
944f7422b3 | ||
|
|
4843c046d7 | ||
|
|
61df4e7e61 | ||
|
|
48558316d1 | ||
|
|
5903b358ee | ||
|
|
6881098ca2 | ||
|
|
4d783649f5 | ||
|
|
2b40224b2b | ||
|
|
e4d6be3bb3 | ||
|
|
6c0b1550fd | ||
|
|
967fbe8072 | ||
|
|
c7dd7a2c21 | ||
|
|
49ed0e43e9 | ||
|
|
6bae985e2c | ||
|
|
39d7d9e213 | ||
|
|
eb3501e090 | ||
|
|
83566ad4e1 | ||
|
|
19e78d5eb6 | ||
|
|
31ab333f5b | ||
|
|
ff8cb20cae |
@@ -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
|
.DS_Store
|
||||||
Build
|
Build
|
||||||
DerivedData
|
DerivedData
|
||||||
|
build-home/
|
||||||
*.xccheckout
|
*.xccheckout
|
||||||
.github
|
.github
|
||||||
README.md
|
README.md
|
||||||
|
|||||||
@@ -7,3 +7,6 @@ url=https://github.com/TrueZhuangJia/voidlink-c.git
|
|||||||
[submodule "third_party/imgui"]
|
[submodule "third_party/imgui"]
|
||||||
path = third_party/imgui
|
path = third_party/imgui
|
||||||
url = https://github.com/ocornut/imgui.git
|
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"?>
|
<?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"/>
|
<device id="retina6_1" orientation="landscape" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<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"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<objects>
|
<objects>
|
||||||
@@ -12,8 +13,13 @@
|
|||||||
<view contentMode="scaleToFill" id="VWy-YH-qYj">
|
<view contentMode="scaleToFill" id="VWy-YH-qYj">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="896" height="414"/>
|
<rect key="frame" x="0.0" y="0.0" width="896" height="414"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
<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"/>
|
<point key="canvasLocation" x="33" y="187"/>
|
||||||
</view>
|
</view>
|
||||||
</objects>
|
</objects>
|
||||||
|
<resources>
|
||||||
|
<systemColor name="systemBackgroundColor">
|
||||||
|
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
</systemColor>
|
||||||
|
</resources>
|
||||||
</document>
|
</document>
|
||||||
|
|||||||
@@ -985,7 +985,7 @@ ICAgICAgICAgICAgICA
|
|||||||
<!--Profiles Table View Controller-->
|
<!--Profiles Table View Controller-->
|
||||||
<scene sceneID="W0E-wz-n2b">
|
<scene sceneID="W0E-wz-n2b">
|
||||||
<objects>
|
<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>
|
<layoutGuides>
|
||||||
<viewControllerLayoutGuide type="top" id="osA-Uy-SE1"/>
|
<viewControllerLayoutGuide type="top" id="osA-Uy-SE1"/>
|
||||||
<viewControllerLayoutGuide type="bottom" id="8ja-wQ-jbJ"/>
|
<viewControllerLayoutGuide type="bottom" id="8ja-wQ-jbJ"/>
|
||||||
|
|||||||
@@ -1005,7 +1005,7 @@ ICAgICAgICAgICAgIA
|
|||||||
<!--Profiles Table View Controller-->
|
<!--Profiles Table View Controller-->
|
||||||
<scene sceneID="C7c-yg-gQK">
|
<scene sceneID="C7c-yg-gQK">
|
||||||
<objects>
|
<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>
|
<layoutGuides>
|
||||||
<viewControllerLayoutGuide type="top" id="Nat-X1-41w"/>
|
<viewControllerLayoutGuide type="top" id="Nat-X1-41w"/>
|
||||||
<viewControllerLayoutGuide type="bottom" id="9SV-fx-82v"/>
|
<viewControllerLayoutGuide type="bottom" id="9SV-fx-82v"/>
|
||||||
|
|||||||
@@ -3,29 +3,64 @@
|
|||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 60;
|
objectVersion = 70;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
|
A1D500022FFC000100000001 /* authored_haptics_engine.cpp in Sources */ = {isa = PBXBuildFile; fileRef = A1D500012FFC000100000001 /* authored_haptics_engine.cpp */; settings = {COMPILER_FLAGS = "-std=c++17"; }; };
|
||||||
|
180345E12E819877004AF36F /* libopus-sim.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 180345E02E819877004AF36F /* libopus-sim.a */; };
|
||||||
|
180E6E372E93BFDB0067C73A /* AlertControllerUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 180E6E362E93BFDB0067C73A /* AlertControllerUtil.swift */; };
|
||||||
|
1817B61E2E6FD04D000D938D /* SafeTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1817B61D2E6FD04D000D938D /* SafeTimer.swift */; };
|
||||||
|
182760F72E83E2EA00EC1C5C /* DequeModule in Frameworks */ = {isa = PBXBuildFile; productRef = 182760F62E83E2EA00EC1C5C /* DequeModule */; };
|
||||||
|
182760FA2E83E57D00EC1C5C /* DequeModule in Frameworks */ = {isa = PBXBuildFile; productRef = 182760F92E83E57D00EC1C5C /* DequeModule */; };
|
||||||
|
182760FD2E83E68B00EC1C5C /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = 182760FC2E83E68B00EC1C5C /* Collections */; };
|
||||||
1829CE3F2E2154DF0074A99A /* SceneDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 1829CE3E2E2154DF0074A99A /* SceneDelegate.m */; };
|
1829CE3F2E2154DF0074A99A /* SceneDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 1829CE3E2E2154DF0074A99A /* SceneDelegate.m */; };
|
||||||
18341A042DEB662C0094E5F2 /* FixedTintImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 18341A032DEB662C0094E5F2 /* FixedTintImageView.m */; };
|
18341A042DEB662C0094E5F2 /* FixedTintImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 18341A032DEB662C0094E5F2 /* FixedTintImageView.m */; };
|
||||||
|
18416EA42EBB3DCE00AE0124 /* UITouchUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18416EA32EBB3DCE00AE0124 /* UITouchUtil.swift */; };
|
||||||
|
1850C82A2E667E4000845308 /* MicHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1850C8292E667E4000845308 /* MicHandler.swift */; };
|
||||||
|
18729EDE2E8BD68800101194 /* UUIDHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18729EDD2E8BD68800101194 /* UUIDHelper.swift */; };
|
||||||
|
18780CC72F10D66000B90483 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 18780CC62F10D66000B90483 /* StoreKit.framework */; };
|
||||||
|
A76B00032FFCC00100000003 /* GameSirG8MFiRumbler.swift in Sources */ = {isa = PBXBuildFile; fileRef = A76B00022FFCC00100000002 /* GameSirG8MFiRumbler.swift */; };
|
||||||
|
A76B00052FFCC00100000005 /* ExternalAccessory.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A76B00042FFCC00100000004 /* ExternalAccessory.framework */; };
|
||||||
|
18780CCA2F10D6CC00B90483 /* IAPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18780CC92F10D6CC00B90483 /* IAPManager.swift */; };
|
||||||
187F4B282E04658A00EECC59 /* WidgetPanelStackView.m in Sources */ = {isa = PBXBuildFile; fileRef = 187F4B272E04658A00EECC59 /* WidgetPanelStackView.m */; };
|
187F4B282E04658A00EECC59 /* WidgetPanelStackView.m in Sources */ = {isa = PBXBuildFile; fileRef = 187F4B272E04658A00EECC59 /* WidgetPanelStackView.m */; };
|
||||||
1888C1922E24C33100108730 /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1888C1912E24C33100108730 /* AboutViewController.swift */; };
|
1888C1922E24C33100108730 /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1888C1912E24C33100108730 /* AboutViewController.swift */; };
|
||||||
1888C1932E24C41000108730 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1888C1902E24C2C800108730 /* AboutView.swift */; };
|
1888C1932E24C41000108730 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1888C1902E24C2C800108730 /* AboutView.swift */; };
|
||||||
188973D32E386A670025E48C /* widgetTemplatesIPhone.bin in Resources */ = {isa = PBXBuildFile; fileRef = 188973D22E386A670025E48C /* widgetTemplatesIPhone.bin */; };
|
188973D32E386A670025E48C /* widgetTemplatesIPhone.bin in Resources */ = {isa = PBXBuildFile; fileRef = 188973D22E386A670025E48C /* widgetTemplatesIPhone.bin */; };
|
||||||
189AD29A2DE7446000E24B88 /* HostCollectionViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 189AD2992DE7446000E24B88 /* HostCollectionViewController.m */; };
|
18951D1D2EBB6D2300A86E12 /* TouchPadGestureHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18951D1C2EBB6D2300A86E12 /* TouchPadGestureHandler.swift */; };
|
||||||
|
189AD29A2DE7446000E24B88 /* HostCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 189AD2992DE7446000E24B88 /* HostCollectionViewController.swift */; };
|
||||||
|
18A0EE252EFB9B5A00BA853A /* SVGKit in Frameworks */ = {isa = PBXBuildFile; productRef = 18A0EE242EFB9B5A00BA853A /* SVGKit */; };
|
||||||
|
18A0EE272EFB9B5A00BA853A /* SVGKitSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 18A0EE262EFB9B5A00BA853A /* SVGKitSwift */; };
|
||||||
|
18A0EE2C2EFBCCFD00BA853A /* GraphicUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A0EE2B2EFBCCFD00BA853A /* GraphicUtils.swift */; };
|
||||||
|
18A765BD2ECD51FB004784AA /* ControllerUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A765BC2ECD51FB004784AA /* ControllerUtil.swift */; };
|
||||||
|
18B317032EE67A3A009B62C2 /* InertialScroller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B317022EE67A39009B62C2 /* InertialScroller.swift */; };
|
||||||
|
18D43D7F2F050240008C326F /* PressureCurve.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18D43D7E2F050240008C326F /* PressureCurve.swift */; };
|
||||||
|
18D43D812F050240008C326F /* PencilTipOffsetCalibrationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18D43D802F050240008C326F /* PencilTipOffsetCalibrationViewController.swift */; };
|
||||||
|
18E4BCEA2E891895000A8657 /* MotionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18E4BCE92E891895000A8657 /* MotionHandler.swift */; };
|
||||||
|
18FCADAF2ECAC23200787A96 /* PencilHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FCADAE2ECAC23200787A96 /* PencilHandler.swift */; };
|
||||||
|
2A263D4A2FF3B3F800154DEA /* PublicExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A263D492FF3B3F800154DEA /* PublicExtensions.swift */; };
|
||||||
|
2A263D4C2FF3B49600154DEA /* PublicExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A263D492FF3B3F800154DEA /* PublicExtensions.swift */; };
|
||||||
|
2A263D4F2FF3BA2800154DEA /* PublicUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A263D4E2FF3BA2800154DEA /* PublicUtils.swift */; };
|
||||||
|
2A263D502FF3BA2800154DEA /* PublicUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A263D4E2FF3BA2800154DEA /* PublicUtils.swift */; };
|
||||||
|
2A7113FD2F5A6C65001D852D /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A7113FC2F5A6C65001D852D /* ThemeManager.swift */; };
|
||||||
|
2A7113FF2F5A6C65001D852D /* UIAppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A7113FE2F5A6C65001D852D /* UIAppView.swift */; };
|
||||||
|
2A7114012F5A6C65001D852D /* GamepadNavigationIllustrationHud.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A7114002F5A6C65001D852D /* GamepadNavigationIllustrationHud.swift */; };
|
||||||
|
2AAA4E822F7666C5001E20DC /* ControllerUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A765BC2ECD51FB004784AA /* ControllerUtil.swift */; };
|
||||||
|
2AAA4E842F7666EE001E20DC /* LocalizationHelperExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E1CDF92C4FE83400547904 /* LocalizationHelperExtension.swift */; };
|
||||||
|
2AAA4E872F985F12001E20DC /* LocalizationHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 84FD0B502C31338D006B5BE5 /* LocalizationHelper.m */; };
|
||||||
|
2AC1E3832F507D80004F76C0 /* GenericUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AC1E3822F507D80004F76C0 /* GenericUtils.swift */; };
|
||||||
|
2AC1EAFB2F92260F004EDA80 /* UIStackExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AC1EAFA2F922602004EDA80 /* UIStackExtension.swift */; };
|
||||||
|
2AC1EAFC2F922889004EDA80 /* UIStackExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AC1EAFA2F922602004EDA80 /* UIStackExtension.swift */; };
|
||||||
|
2B7A10022FFB000100000001 /* SettingsTouchVelocityPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7A10012FFB000100000001 /* SettingsTouchVelocityPreview.swift */; };
|
||||||
|
3A4A3A112F0C000100000001 /* ProfileSelectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4A3A102F0C000100000001 /* ProfileSelectorViewController.swift */; };
|
||||||
|
3A4A3A132F0C000100000003 /* LayoutOnScreenControlsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4A3A122F0C000100000002 /* LayoutOnScreenControlsViewController.swift */; };
|
||||||
840751B32BF49C050096B657 /* CustomTapGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 840751B22BF49C050096B657 /* CustomTapGestureRecognizer.m */; };
|
840751B32BF49C050096B657 /* CustomTapGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 840751B22BF49C050096B657 /* CustomTapGestureRecognizer.m */; };
|
||||||
841BAC922DE35D6B00DA639E /* ThemeManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 841BAC912DE35D6B00DA639E /* ThemeManager.m */; };
|
|
||||||
84203F232C5F24C00032EEED /* OnScreenWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84203F222C5F24C00032EEED /* OnScreenWidgetView.swift */; };
|
84203F232C5F24C00032EEED /* OnScreenWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84203F222C5F24C00032EEED /* OnScreenWidgetView.swift */; };
|
||||||
84351B0E2C29ACCB00A77989 /* LayoutOnScreenControls.m in Sources */ = {isa = PBXBuildFile; fileRef = 84351B072C29ACCA00A77989 /* LayoutOnScreenControls.m */; };
|
84351B0E2C29ACCB00A77989 /* LayoutOnScreenControls.m in Sources */ = {isa = PBXBuildFile; fileRef = 84351B072C29ACCA00A77989 /* LayoutOnScreenControls.m */; };
|
||||||
84351B0F2C29ACCB00A77989 /* OnScreenButtonState.m in Sources */ = {isa = PBXBuildFile; fileRef = 84351B092C29ACCA00A77989 /* OnScreenButtonState.m */; };
|
84351B0F2C29ACCB00A77989 /* OnScreenButtonState.m in Sources */ = {isa = PBXBuildFile; fileRef = 84351B092C29ACCA00A77989 /* OnScreenButtonState.m */; };
|
||||||
84351B102C29ACCB00A77989 /* OSCProfile.m in Sources */ = {isa = PBXBuildFile; fileRef = 84351B0A2C29ACCA00A77989 /* OSCProfile.m */; };
|
84351B102C29ACCB00A77989 /* OSCProfile.m in Sources */ = {isa = PBXBuildFile; fileRef = 84351B0A2C29ACCA00A77989 /* OSCProfile.m */; };
|
||||||
84351B112C29ACCB00A77989 /* OSCProfilesManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 84351B0B2C29ACCB00A77989 /* OSCProfilesManager.m */; };
|
84351B112C29ACCB00A77989 /* OSCProfilesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1D4E0A22FBBBBCC00112233 /* OSCProfilesManager.swift */; };
|
||||||
84351B2B2C29AD6100A77989 /* LayoutOnScreenControlsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 84351B252C29AD6000A77989 /* LayoutOnScreenControlsViewController.m */; };
|
|
||||||
84351B2C2C29AD6100A77989 /* OSCProfilesTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 84351B272C29AD6000A77989 /* OSCProfilesTableViewController.m */; };
|
|
||||||
84351B2D2C29AD6100A77989 /* ToolBarContainerView.m in Sources */ = {isa = PBXBuildFile; fileRef = 84351B292C29AD6000A77989 /* ToolBarContainerView.m */; };
|
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 */; };
|
843667F02D9D7173004A7D7F /* widgetTemplates.bin in Resources */ = {isa = PBXBuildFile; fileRef = 843667EF2D9D7173004A7D7F /* widgetTemplates.bin */; };
|
||||||
843CC7F02C1E757D00BBC475 /* NativeTouchPointer.m in Sources */ = {isa = PBXBuildFile; fileRef = 843CC7EF2C1E757D00BBC475 /* NativeTouchPointer.m */; };
|
843CC7F02C1E757D00BBC475 /* NativeTouchPointer.m in Sources */ = {isa = PBXBuildFile; fileRef = 843CC7EF2C1E757D00BBC475 /* NativeTouchPointer.m */; };
|
||||||
843CC7F62C1E7A1700BBC475 /* NativeTouchHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 843CC7F52C1E7A1700BBC475 /* NativeTouchHandler.m */; };
|
843CC7F62C1E7A1700BBC475 /* NativeTouchHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 843CC7F52C1E7A1700BBC475 /* NativeTouchHandler.m */; };
|
||||||
@@ -34,11 +69,11 @@
|
|||||||
8454AC662C31907F00F0A302 /* iPad.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8454AC682C31907F00F0A302 /* iPad.storyboard */; };
|
8454AC662C31907F00F0A302 /* iPad.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8454AC682C31907F00F0A302 /* iPad.storyboard */; };
|
||||||
8454AC6A2C319BDA00F0A302 /* iPhone.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8454AC6C2C319BDA00F0A302 /* iPhone.storyboard */; };
|
8454AC6A2C319BDA00F0A302 /* iPhone.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8454AC6C2C319BDA00F0A302 /* iPhone.storyboard */; };
|
||||||
84591A512BE1625E006AAB10 /* CustomEdgeSlideGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 84591A502BE1625E006AAB10 /* CustomEdgeSlideGestureRecognizer.m */; };
|
84591A512BE1625E006AAB10 /* CustomEdgeSlideGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 84591A502BE1625E006AAB10 /* CustomEdgeSlideGestureRecognizer.m */; };
|
||||||
847D2FDB2DDA015F00937A62 /* MenuSectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 847D2FDA2DDA015F00937A62 /* MenuSectionView.m */; };
|
847D2FDB2DDA015F00937A62 /* MenuSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847D2FDA2DDA015F00937A62 /* MenuSectionView.swift */; };
|
||||||
848F456A2DDCD12500581E36 /* HostCardView.m in Sources */ = {isa = PBXBuildFile; fileRef = 848F45692DDCD12500581E36 /* HostCardView.m */; };
|
848F456A2DDCD12500581E36 /* HostCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848F45692DDCD12500581E36 /* HostCardView.swift */; };
|
||||||
84E1CDEC2C4F67D300547904 /* CommandManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E1CDEB2C4F67D300547904 /* CommandManager.swift */; };
|
84E1CDEC2C4F67D300547904 /* CommandManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E1CDEB2C4F67D300547904 /* CommandManager.swift */; };
|
||||||
84E1CDEF2C4F688E00547904 /* ToolboxViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E1CDEE2C4F688E00547904 /* ToolboxViewController.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 */; };
|
84FD0B512C31338D006B5BE5 /* LocalizationHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 84FD0B502C31338D006B5BE5 /* LocalizationHelper.m */; };
|
||||||
84FD0B542C3134A6006B5BE5 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 84FD0B532C3134A6006B5BE5 /* Localizable.xcstrings */; };
|
84FD0B542C3134A6006B5BE5 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 84FD0B532C3134A6006B5BE5 /* Localizable.xcstrings */; };
|
||||||
9777AAF32E2CF7A400D7FBAF /* Shaders-Linear.metal in Sources */ = {isa = PBXBuildFile; fileRef = 9777AAF22E2CF7A400D7FBAF /* Shaders-Linear.metal */; };
|
9777AAF32E2CF7A400D7FBAF /* Shaders-Linear.metal in Sources */ = {isa = PBXBuildFile; fileRef = 9777AAF22E2CF7A400D7FBAF /* Shaders-Linear.metal */; };
|
||||||
@@ -76,6 +111,10 @@
|
|||||||
98CFB82F1CAD481B0048EF74 /* libmoonlight-common.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 98AB2E841CAD46840089BB98 /* libmoonlight-common.a */; };
|
98CFB82F1CAD481B0048EF74 /* libmoonlight-common.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 98AB2E841CAD46840089BB98 /* libmoonlight-common.a */; };
|
||||||
98D5856D1C0EA79600F6CC00 /* TemporaryHost.m in Sources */ = {isa = PBXBuildFile; fileRef = 98D5856C1C0EA79600F6CC00 /* TemporaryHost.m */; };
|
98D5856D1C0EA79600F6CC00 /* TemporaryHost.m in Sources */ = {isa = PBXBuildFile; fileRef = 98D5856C1C0EA79600F6CC00 /* TemporaryHost.m */; };
|
||||||
98D585701C0ED0E800F6CC00 /* TemporarySettings.m in Sources */ = {isa = PBXBuildFile; fileRef = 98D5856F1C0ED0E800F6CC00 /* TemporarySettings.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 */; };
|
DC1F5A07206436B20037755F /* ConnectionHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = DC1F5A06206436B20037755F /* ConnectionHelper.m */; };
|
||||||
F74BEF9C2C1A705600224667 /* OpenSSL in Frameworks */ = {isa = PBXBuildFile; productRef = F74BEF9B2C1A705600224667 /* OpenSSL */; };
|
F74BEF9C2C1A705600224667 /* OpenSSL in Frameworks */ = {isa = PBXBuildFile; productRef = F74BEF9B2C1A705600224667 /* OpenSSL */; };
|
||||||
FB1A674D2131E65900507771 /* KeyboardSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = FB1A674C2131E65900507771 /* KeyboardSupport.m */; };
|
FB1A674D2131E65900507771 /* KeyboardSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = FB1A674C2131E65900507771 /* KeyboardSupport.m */; };
|
||||||
@@ -107,6 +146,7 @@
|
|||||||
FB89463019F646E200339C8A /* StreamConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89461919F646E200339C8A /* StreamConfiguration.m */; };
|
FB89463019F646E200339C8A /* StreamConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89461919F646E200339C8A /* StreamConfiguration.m */; };
|
||||||
FB89463119F646E200339C8A /* StreamManager.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89461B19F646E200339C8A /* StreamManager.m */; };
|
FB89463119F646E200339C8A /* StreamManager.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89461B19F646E200339C8A /* StreamManager.m */; };
|
||||||
FB89463219F646E200339C8A /* VideoDecoderRenderer.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89461D19F646E200339C8A /* VideoDecoderRenderer.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 */; };
|
FB89463419F646E200339C8A /* Utils.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89462219F646E200339C8A /* Utils.m */; };
|
||||||
FB89463519F646E200339C8A /* MainFrameViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89462519F646E200339C8A /* MainFrameViewController.m */; };
|
FB89463519F646E200339C8A /* MainFrameViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89462519F646E200339C8A /* MainFrameViewController.m */; };
|
||||||
FB89463619F646E200339C8A /* StreamFrameViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89462719F646E200339C8A /* StreamFrameViewController.m */; };
|
FB89463619F646E200339C8A /* StreamFrameViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89462719F646E200339C8A /* StreamFrameViewController.m */; };
|
||||||
@@ -123,7 +163,6 @@
|
|||||||
FBD3495019FF2174002D2A60 /* SettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FBD3494F19FF2174002D2A60 /* SettingsViewController.m */; };
|
FBD3495019FF2174002D2A60 /* SettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FBD3494F19FF2174002D2A60 /* SettingsViewController.m */; };
|
||||||
FBD3495319FF36FB002D2A60 /* SWRevealViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FBD3495219FF36FB002D2A60 /* SWRevealViewController.m */; };
|
FBD3495319FF36FB002D2A60 /* SWRevealViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FBD3495219FF36FB002D2A60 /* SWRevealViewController.m */; };
|
||||||
FBD349621A0089F6002D2A60 /* DataManager.m in Sources */ = {isa = PBXBuildFile; fileRef = FBD349611A0089F6002D2A60 /* DataManager.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 */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -151,41 +190,59 @@
|
|||||||
/* End PBXContainerItemProxy section */
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
A1D500012FFC000100000001 /* authored_haptics_engine.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = authored_haptics_engine.cpp; path = "third_party/moonlight-audio-haptics/src/core/authored_haptics_engine.cpp"; sourceTree = SOURCE_ROOT; };
|
||||||
|
180345E02E819877004AF36F /* libopus-sim.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libopus-sim.a"; sourceTree = "<group>"; };
|
||||||
|
180E6E362E93BFDB0067C73A /* AlertControllerUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertControllerUtil.swift; sourceTree = "<group>"; };
|
||||||
|
1817B61D2E6FD04D000D938D /* SafeTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafeTimer.swift; sourceTree = "<group>"; };
|
||||||
1829CE3D2E2154DF0074A99A /* SceneDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SceneDelegate.h; sourceTree = "<group>"; };
|
1829CE3D2E2154DF0074A99A /* SceneDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SceneDelegate.h; sourceTree = "<group>"; };
|
||||||
1829CE3E2E2154DF0074A99A /* SceneDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SceneDelegate.m; sourceTree = "<group>"; };
|
1829CE3E2E2154DF0074A99A /* SceneDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SceneDelegate.m; sourceTree = "<group>"; };
|
||||||
18341A022DEB65AB0094E5F2 /* FixedTintImageView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FixedTintImageView.h; sourceTree = "<group>"; };
|
18341A022DEB65AB0094E5F2 /* FixedTintImageView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FixedTintImageView.h; sourceTree = "<group>"; };
|
||||||
18341A032DEB662C0094E5F2 /* FixedTintImageView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FixedTintImageView.m; sourceTree = "<group>"; };
|
18341A032DEB662C0094E5F2 /* FixedTintImageView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FixedTintImageView.m; sourceTree = "<group>"; };
|
||||||
|
18416EA32EBB3DCE00AE0124 /* UITouchUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITouchUtil.swift; sourceTree = "<group>"; };
|
||||||
|
1850C8292E667E4000845308 /* MicHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MicHandler.swift; sourceTree = "<group>"; };
|
||||||
|
18729EDD2E8BD68800101194 /* UUIDHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UUIDHelper.swift; sourceTree = "<group>"; };
|
||||||
|
18780CC62F10D66000B90483 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
|
||||||
|
A76B00022FFCC00100000002 /* GameSirG8MFiRumbler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameSirG8MFiRumbler.swift; sourceTree = "<group>"; };
|
||||||
|
A76B00042FFCC00100000004 /* ExternalAccessory.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ExternalAccessory.framework; path = System/Library/Frameworks/ExternalAccessory.framework; sourceTree = SDKROOT; };
|
||||||
|
18780CC92F10D6CC00B90483 /* IAPManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IAPManager.swift; sourceTree = "<group>"; };
|
||||||
187F4B232E04633100EECC59 /* WidgetPanelStackView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WidgetPanelStackView.h; sourceTree = "<group>"; };
|
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>"; };
|
187F4B272E04658A00EECC59 /* WidgetPanelStackView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = WidgetPanelStackView.m; sourceTree = "<group>"; };
|
||||||
1888C1902E24C2C800108730 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
|
1888C1902E24C2C800108730 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
|
||||||
1888C1912E24C33100108730 /* AboutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = "<group>"; };
|
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>"; };
|
188973D22E386A670025E48C /* widgetTemplatesIPhone.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; name = widgetTemplatesIPhone.bin; path = PresetConfigs/widgetTemplatesIPhone.bin; sourceTree = "<group>"; };
|
||||||
189AD2962DE743B700E24B88 /* HostCollectionViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HostCollectionViewController.h; sourceTree = "<group>"; };
|
18951D1C2EBB6D2300A86E12 /* TouchPadGestureHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchPadGestureHandler.swift; 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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
840751B22BF49C050096B657 /* CustomTapGestureRecognizer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CustomTapGestureRecognizer.m; sourceTree = "<group>"; };
|
||||||
841BAC8F2DE35BEC00DA639E /* ThemeManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ThemeManager.h; sourceTree = "<group>"; };
|
|
||||||
841BAC912DE35D6B00DA639E /* ThemeManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ThemeManager.m; sourceTree = "<group>"; };
|
|
||||||
84203F222C5F24C00032EEED /* OnScreenWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnScreenWidgetView.swift; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
843CC7EF2C1E757D00BBC475 /* NativeTouchPointer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NativeTouchPointer.m; sourceTree = "<group>"; };
|
||||||
@@ -200,14 +257,12 @@
|
|||||||
8454AC6D2C319BDA00F0A302 /* mul */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = mul; path = mul.lproj/iPhone.xcstrings; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
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.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuSectionView.swift; sourceTree = "<group>"; };
|
||||||
847D2FDA2DDA015F00937A62 /* MenuSectionView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MenuSectionView.m; sourceTree = "<group>"; };
|
848F45692DDCD12500581E36 /* HostCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostCardView.swift; 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>"; };
|
|
||||||
84D8EB932C5146AD00FC10A9 /* VoidLink-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "VoidLink-Bridging-Header.h"; 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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
84FD0B532C3134A6006B5BE5 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
|
||||||
@@ -280,6 +335,11 @@
|
|||||||
98D5856C1C0EA79600F6CC00 /* TemporaryHost.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TemporaryHost.m; path = Database/TemporaryHost.m; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
FB1A674B2131E65900507771 /* KeyboardSupport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyboardSupport.h; sourceTree = "<group>"; };
|
||||||
@@ -340,6 +400,7 @@
|
|||||||
FB89461B19F646E200339C8A /* StreamManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = StreamManager.m; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
FB89462419F646E200339C8A /* MainFrameViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MainFrameViewController.h; sourceTree = "<group>"; };
|
||||||
@@ -374,27 +435,64 @@
|
|||||||
FBD3495219FF36FB002D2A60 /* SWRevealViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SWRevealViewController.m; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
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 */
|
/* 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 */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
2A039E532F72892B00546768 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
FB290CEB19B2C406004C83CF /* Frameworks */ = {
|
FB290CEB19B2C406004C83CF /* Frameworks */ = {
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
A76B00052FFCC00100000005 /* ExternalAccessory.framework in Frameworks */,
|
||||||
|
18A0EE272EFB9B5A00BA853A /* SVGKitSwift in Frameworks */,
|
||||||
|
18A0EE252EFB9B5A00BA853A /* SVGKit in Frameworks */,
|
||||||
9890CF6B203B7EE1006C4B06 /* libxml2.tbd in Frameworks */,
|
9890CF6B203B7EE1006C4B06 /* libxml2.tbd in Frameworks */,
|
||||||
98CFB82F1CAD481B0048EF74 /* libmoonlight-common.a in Frameworks */,
|
98CFB82F1CAD481B0048EF74 /* libmoonlight-common.a in Frameworks */,
|
||||||
FB8946ED19F6AFE800339C8A /* libopus.a in Frameworks */,
|
|
||||||
98882A082AF60F7400C5A11C /* libavutil.a in Frameworks */,
|
98882A082AF60F7400C5A11C /* libavutil.a in Frameworks */,
|
||||||
|
182760F72E83E2EA00EC1C5C /* DequeModule in Frameworks */,
|
||||||
|
182760FD2E83E68B00EC1C5C /* Collections in Frameworks */,
|
||||||
FB290CF419B2C406004C83CF /* CoreGraphics.framework in Frameworks */,
|
FB290CF419B2C406004C83CF /* CoreGraphics.framework in Frameworks */,
|
||||||
FB290CF819B2C406004C83CF /* CoreData.framework in Frameworks */,
|
FB290CF819B2C406004C83CF /* CoreData.framework in Frameworks */,
|
||||||
98181BED2791281100E43572 /* CoreMotion.framework in Frameworks */,
|
98181BED2791281100E43572 /* CoreMotion.framework in Frameworks */,
|
||||||
F74BEF9C2C1A705600224667 /* OpenSSL in Frameworks */,
|
F74BEF9C2C1A705600224667 /* OpenSSL in Frameworks */,
|
||||||
98882A072AF60F7200C5A11C /* libavformat.a in Frameworks */,
|
98882A072AF60F7200C5A11C /* libavformat.a in Frameworks */,
|
||||||
FB290CF619B2C406004C83CF /* UIKit.framework in Frameworks */,
|
FB290CF619B2C406004C83CF /* UIKit.framework in Frameworks */,
|
||||||
|
18780CC72F10D66000B90483 /* StoreKit.framework in Frameworks */,
|
||||||
FB290CF219B2C406004C83CF /* Foundation.framework in Frameworks */,
|
FB290CF219B2C406004C83CF /* Foundation.framework in Frameworks */,
|
||||||
FB4679011A57048000377732 /* CoreFoundation.framework in Frameworks */,
|
FB4679011A57048000377732 /* CoreFoundation.framework in Frameworks */,
|
||||||
|
182760FA2E83E57D00EC1C5C /* DequeModule in Frameworks */,
|
||||||
98181BE92791277400E43572 /* libSDL2.a in Frameworks */,
|
98181BE92791277400E43572 /* libSDL2.a in Frameworks */,
|
||||||
98882A062AF60F7000C5A11C /* libavcodec.a in Frameworks */,
|
98882A062AF60F7000C5A11C /* libavcodec.a in Frameworks */,
|
||||||
);
|
);
|
||||||
@@ -412,8 +510,7 @@
|
|||||||
84351B092C29ACCA00A77989 /* OnScreenButtonState.m */,
|
84351B092C29ACCA00A77989 /* OnScreenButtonState.m */,
|
||||||
84351B0D2C29ACCB00A77989 /* OSCProfile.h */,
|
84351B0D2C29ACCB00A77989 /* OSCProfile.h */,
|
||||||
84351B0A2C29ACCA00A77989 /* OSCProfile.m */,
|
84351B0A2C29ACCA00A77989 /* OSCProfile.m */,
|
||||||
84351B082C29ACCA00A77989 /* OSCProfilesManager.h */,
|
A1D4E0A22FBBBBCC00112233 /* OSCProfilesManager.swift */,
|
||||||
84351B0B2C29ACCB00A77989 /* OSCProfilesManager.m */,
|
|
||||||
);
|
);
|
||||||
path = CustomOSC;
|
path = CustomOSC;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -421,27 +518,22 @@
|
|||||||
84351B242C29AD2E00A77989 /* CustomOSCViewControl */ = {
|
84351B242C29AD2E00A77989 /* CustomOSCViewControl */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
84351B2E2C29AD6600A77989 /* Cells */,
|
84351B2E2C29AD6600A77989 /* Table */,
|
||||||
84351B282C29AD6000A77989 /* LayoutOnScreenControlsViewController.h */,
|
3A4A3A122F0C000100000002 /* LayoutOnScreenControlsViewController.swift */,
|
||||||
84351B252C29AD6000A77989 /* LayoutOnScreenControlsViewController.m */,
|
|
||||||
187F4B232E04633100EECC59 /* WidgetPanelStackView.h */,
|
187F4B232E04633100EECC59 /* WidgetPanelStackView.h */,
|
||||||
187F4B272E04658A00EECC59 /* WidgetPanelStackView.m */,
|
187F4B272E04658A00EECC59 /* WidgetPanelStackView.m */,
|
||||||
84351B262C29AD6000A77989 /* OSCProfilesTableViewController.h */,
|
|
||||||
84351B272C29AD6000A77989 /* OSCProfilesTableViewController.m */,
|
|
||||||
84351B2A2C29AD6000A77989 /* ToolBarContainerView.h */,
|
84351B2A2C29AD6000A77989 /* ToolBarContainerView.h */,
|
||||||
84351B292C29AD6000A77989 /* ToolBarContainerView.m */,
|
84351B292C29AD6000A77989 /* ToolBarContainerView.m */,
|
||||||
);
|
);
|
||||||
path = CustomOSCViewControl;
|
path = CustomOSCViewControl;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
84351B2E2C29AD6600A77989 /* Cells */ = {
|
84351B2E2C29AD6600A77989 /* Table */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
84351B5A2C29ADBE00A77989 /* ProfileTableViewCell.h */,
|
3A4A3A102F0C000100000001 /* ProfileSelectorViewController.swift */,
|
||||||
84351B592C29ADBE00A77989 /* ProfileTableViewCell.m */,
|
|
||||||
84351B5B2C29ADBE00A77989 /* ProfileTableViewCell.xib */,
|
|
||||||
);
|
);
|
||||||
path = Cells;
|
path = Table;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
843667ED2D9D70A3004A7D7F /* PresetConfigs */ = {
|
843667ED2D9D70A3004A7D7F /* PresetConfigs */ = {
|
||||||
@@ -458,8 +550,8 @@
|
|||||||
children = (
|
children = (
|
||||||
84FD0B4F2C313343006B5BE5 /* LocalizationHelper.h */,
|
84FD0B4F2C313343006B5BE5 /* LocalizationHelper.h */,
|
||||||
84FD0B502C31338D006B5BE5 /* LocalizationHelper.m */,
|
84FD0B502C31338D006B5BE5 /* LocalizationHelper.m */,
|
||||||
|
84E1CDF92C4FE83400547904 /* LocalizationHelperExtension.swift */,
|
||||||
84FD0B532C3134A6006B5BE5 /* Localizable.xcstrings */,
|
84FD0B532C3134A6006B5BE5 /* Localizable.xcstrings */,
|
||||||
84E1CDF92C4FE83400547904 /* SwiftLocalizationHelper.swift */,
|
|
||||||
);
|
);
|
||||||
path = Localization;
|
path = Localization;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -543,6 +635,7 @@
|
|||||||
98AB2E7F1CAD46830089BB98 /* moonlight-common.xcodeproj */,
|
98AB2E7F1CAD46830089BB98 /* moonlight-common.xcodeproj */,
|
||||||
FB290CF919B2C406004C83CF /* VoidLink */,
|
FB290CF919B2C406004C83CF /* VoidLink */,
|
||||||
FB1A67542132419700507771 /* Moonlight TV */,
|
FB1A67542132419700507771 /* Moonlight TV */,
|
||||||
|
2A039E572F72892B00546768 /* VoidLinkPreview */,
|
||||||
FB290CF019B2C406004C83CF /* Frameworks */,
|
FB290CF019B2C406004C83CF /* Frameworks */,
|
||||||
FB290CEF19B2C406004C83CF /* Products */,
|
FB290CEF19B2C406004C83CF /* Products */,
|
||||||
);
|
);
|
||||||
@@ -552,6 +645,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
FB290CEE19B2C406004C83CF /* VoidLink.app */,
|
FB290CEE19B2C406004C83CF /* VoidLink.app */,
|
||||||
|
2A039E562F72892B00546768 /* VoidLinkPreview.app */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -559,6 +653,8 @@
|
|||||||
FB290CF019B2C406004C83CF /* Frameworks */ = {
|
FB290CF019B2C406004C83CF /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
A76B00042FFCC00100000004 /* ExternalAccessory.framework */,
|
||||||
|
18780CC62F10D66000B90483 /* StoreKit.framework */,
|
||||||
98882A032AF60F5300C5A11C /* libavcodec.a */,
|
98882A032AF60F5300C5A11C /* libavcodec.a */,
|
||||||
98882A052AF60F5300C5A11C /* libavformat.a */,
|
98882A052AF60F5300C5A11C /* libavformat.a */,
|
||||||
98882A0A2AF60F9000C5A11C /* libavcodec.a */,
|
98882A0A2AF60F9000C5A11C /* libavcodec.a */,
|
||||||
@@ -610,18 +706,17 @@
|
|||||||
18341A022DEB65AB0094E5F2 /* FixedTintImageView.h */,
|
18341A022DEB65AB0094E5F2 /* FixedTintImageView.h */,
|
||||||
18341A032DEB662C0094E5F2 /* FixedTintImageView.m */,
|
18341A032DEB662C0094E5F2 /* FixedTintImageView.m */,
|
||||||
FB290D0319B2C406004C83CF /* AppDelegate.m */,
|
FB290D0319B2C406004C83CF /* AppDelegate.m */,
|
||||||
848F45682DDCD0D600581E36 /* HostCardView.h */,
|
848F45692DDCD12500581E36 /* HostCardView.swift */,
|
||||||
848F45692DDCD12500581E36 /* HostCardView.m */,
|
2A7113FC2F5A6C65001D852D /* ThemeManager.swift */,
|
||||||
841BAC8F2DE35BEC00DA639E /* ThemeManager.h */,
|
847D2FDA2DDA015F00937A62 /* MenuSectionView.swift */,
|
||||||
841BAC912DE35D6B00DA639E /* ThemeManager.m */,
|
2A7113FE2F5A6C65001D852D /* UIAppView.swift */,
|
||||||
847D2FD82DDA00C800937A62 /* MenuSectionView.h */,
|
2A7114002F5A6C65001D852D /* GamepadNavigationIllustrationHud.swift */,
|
||||||
847D2FDA2DDA015F00937A62 /* MenuSectionView.m */,
|
180E6E362E93BFDB0067C73A /* AlertControllerUtil.swift */,
|
||||||
FBDE86E419F82297001C18A8 /* UIAppView.h */,
|
|
||||||
FBDE86E519F82297001C18A8 /* UIAppView.m */,
|
|
||||||
1888C1902E24C2C800108730 /* AboutView.swift */,
|
1888C1902E24C2C800108730 /* AboutView.swift */,
|
||||||
1888C1912E24C33100108730 /* AboutViewController.swift */,
|
1888C1912E24C33100108730 /* AboutViewController.swift */,
|
||||||
FB89463719F6473800339C8A /* Launch Screen.xib */,
|
FB89463719F6473800339C8A /* Launch Screen.xib */,
|
||||||
FB290D0819B2C406004C83CF /* Images.xcassets */,
|
FB290D0819B2C406004C83CF /* Images.xcassets */,
|
||||||
|
18A0EE282EFBCBFB00BA853A /* SVGs */,
|
||||||
FB290D0519B2C406004C83CF /* Limelight.xcdatamodeld */,
|
FB290D0519B2C406004C83CF /* Limelight.xcdatamodeld */,
|
||||||
FB1D59981BBCCD7E00F482CA /* AppCollectionView.h */,
|
FB1D59981BBCCD7E00F482CA /* AppCollectionView.h */,
|
||||||
FB1D59991BBCCD7E00F482CA /* AppCollectionView.m */,
|
FB1D59991BBCCD7E00F482CA /* AppCollectionView.m */,
|
||||||
@@ -661,6 +756,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
84351B052C29ACAF00A77989 /* CustomOSC */,
|
84351B052C29ACAF00A77989 /* CustomOSC */,
|
||||||
|
A76B00022FFCC00100000002 /* GameSirG8MFiRumbler.swift */,
|
||||||
FB89460A19F646E200339C8A /* ControllerSupport.h */,
|
FB89460A19F646E200339C8A /* ControllerSupport.h */,
|
||||||
FB89460B19F646E200339C8A /* ControllerSupport.m */,
|
FB89460B19F646E200339C8A /* ControllerSupport.m */,
|
||||||
FB89460C19F646E200339C8A /* StreamView.h */,
|
FB89460C19F646E200339C8A /* StreamView.h */,
|
||||||
@@ -677,18 +773,32 @@
|
|||||||
9827E7A7251436EA00F25707 /* HapticContext.h */,
|
9827E7A7251436EA00F25707 /* HapticContext.h */,
|
||||||
840751B12BF49B5A0096B657 /* CustomTapGestureRecognizer.h */,
|
840751B12BF49B5A0096B657 /* CustomTapGestureRecognizer.h */,
|
||||||
840751B22BF49C050096B657 /* CustomTapGestureRecognizer.m */,
|
840751B22BF49C050096B657 /* CustomTapGestureRecognizer.m */,
|
||||||
9819CC12254F107A008A7C8E /* RelativeTouchHandler.h */,
|
|
||||||
843CC7F42C1E778000BBC475 /* NativeTouchHandler.h */,
|
|
||||||
844CD0E82C7F847E004861A7 /* PureNativeTouchHandler.h */,
|
844CD0E82C7F847E004861A7 /* PureNativeTouchHandler.h */,
|
||||||
844CD0EA2C7F8500004861A7 /* PureNativeTouchHandler.m */,
|
844CD0EA2C7F8500004861A7 /* PureNativeTouchHandler.m */,
|
||||||
|
843CC7F42C1E778000BBC475 /* NativeTouchHandler.h */,
|
||||||
843CC7F52C1E7A1700BBC475 /* NativeTouchHandler.m */,
|
843CC7F52C1E7A1700BBC475 /* NativeTouchHandler.m */,
|
||||||
|
9819CC12254F107A008A7C8E /* RelativeTouchHandler.h */,
|
||||||
9819CC13254F107A008A7C8E /* RelativeTouchHandler.m */,
|
9819CC13254F107A008A7C8E /* RelativeTouchHandler.m */,
|
||||||
9819CC20254F180F008A7C8E /* AbsoluteTouchHandler.h */,
|
9819CC20254F180F008A7C8E /* AbsoluteTouchHandler.h */,
|
||||||
9819CC21254F180F008A7C8E /* AbsoluteTouchHandler.m */,
|
9819CC21254F180F008A7C8E /* AbsoluteTouchHandler.m */,
|
||||||
988FCD3F293B091B003050E2 /* KeyboardInputField.h */,
|
988FCD3F293B091B003050E2 /* KeyboardInputField.h */,
|
||||||
988FCD40293B091B003050E2 /* KeyboardInputField.m */,
|
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 */,
|
84E1CDEB2C4F67D300547904 /* CommandManager.swift */,
|
||||||
84203F222C5F24C00032EEED /* OnScreenWidgetView.swift */,
|
84203F222C5F24C00032EEED /* OnScreenWidgetView.swift */,
|
||||||
|
A1900E002F9A100100BA853A /* InputAccessoryBar.swift */,
|
||||||
|
A1B2C3D32F90000100ABCDEF /* AbstractGamepadOverlayWidgetView.swift */,
|
||||||
|
1817B61D2E6FD04D000D938D /* SafeTimer.swift */,
|
||||||
);
|
);
|
||||||
path = Input;
|
path = Input;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -720,6 +830,7 @@
|
|||||||
FB89461B19F646E200339C8A /* StreamManager.m */,
|
FB89461B19F646E200339C8A /* StreamManager.m */,
|
||||||
FB89461C19F646E200339C8A /* VideoDecoderRenderer.h */,
|
FB89461C19F646E200339C8A /* VideoDecoderRenderer.h */,
|
||||||
FB89461D19F646E200339C8A /* VideoDecoderRenderer.m */,
|
FB89461D19F646E200339C8A /* VideoDecoderRenderer.m */,
|
||||||
|
A1F100002FFB000100000001 /* FrameInterpolator.swift */,
|
||||||
9803CCAB254F9EAF00EE185E /* ConnectionCallbacks.h */,
|
9803CCAB254F9EAF00EE185E /* ConnectionCallbacks.h */,
|
||||||
);
|
);
|
||||||
path = Stream;
|
path = Stream;
|
||||||
@@ -728,6 +839,12 @@
|
|||||||
FB89461E19F646E200339C8A /* Utility */ = {
|
FB89461E19F646E200339C8A /* Utility */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
18729EDD2E8BD68800101194 /* UUIDHelper.swift */,
|
||||||
|
2AC1E3822F507D80004F76C0 /* GenericUtils.swift */,
|
||||||
|
2A263D4E2FF3BA2800154DEA /* PublicUtils.swift */,
|
||||||
|
2A263D492FF3B3F800154DEA /* PublicExtensions.swift */,
|
||||||
|
18A0EE2B2EFBCCFD00BA853A /* GraphicUtils.swift */,
|
||||||
|
18780CC92F10D6CC00B90483 /* IAPManager.swift */,
|
||||||
97E20DD42E2164A9000D5DCA /* BandwidthTracker.h */,
|
97E20DD42E2164A9000D5DCA /* BandwidthTracker.h */,
|
||||||
97E20DD52E2164A9000D5DCA /* BandwidthTracker.m */,
|
97E20DD52E2164A9000D5DCA /* BandwidthTracker.m */,
|
||||||
97E20DD62E2164A9000D5DCA /* FloatBuffer.h */,
|
97E20DD62E2164A9000D5DCA /* FloatBuffer.h */,
|
||||||
@@ -757,12 +874,13 @@
|
|||||||
FBD3495219FF36FB002D2A60 /* SWRevealViewController.m */,
|
FBD3495219FF36FB002D2A60 /* SWRevealViewController.m */,
|
||||||
FB89462419F646E200339C8A /* MainFrameViewController.h */,
|
FB89462419F646E200339C8A /* MainFrameViewController.h */,
|
||||||
FB89462519F646E200339C8A /* MainFrameViewController.m */,
|
FB89462519F646E200339C8A /* MainFrameViewController.m */,
|
||||||
189AD2962DE743B700E24B88 /* HostCollectionViewController.h */,
|
189AD2992DE7446000E24B88 /* HostCollectionViewController.swift */,
|
||||||
189AD2992DE7446000E24B88 /* HostCollectionViewController.m */,
|
|
||||||
FB89462619F646E200339C8A /* StreamFrameViewController.h */,
|
FB89462619F646E200339C8A /* StreamFrameViewController.h */,
|
||||||
FB89462719F646E200339C8A /* StreamFrameViewController.m */,
|
FB89462719F646E200339C8A /* StreamFrameViewController.m */,
|
||||||
FBD3494E19FF2174002D2A60 /* SettingsViewController.h */,
|
FBD3494E19FF2174002D2A60 /* SettingsViewController.h */,
|
||||||
FBD3494F19FF2174002D2A60 /* SettingsViewController.m */,
|
FBD3494F19FF2174002D2A60 /* SettingsViewController.m */,
|
||||||
|
2B7A10012FFB000100000001 /* SettingsTouchVelocityPreview.swift */,
|
||||||
|
2AC1EAFA2F922602004EDA80 /* UIStackExtension.swift */,
|
||||||
FB4A23B61A9D3637004D2EF2 /* LoadingFrameViewController.h */,
|
FB4A23B61A9D3637004D2EF2 /* LoadingFrameViewController.h */,
|
||||||
FB4A23B71A9D3637004D2EF2 /* LoadingFrameViewController.m */,
|
FB4A23B71A9D3637004D2EF2 /* LoadingFrameViewController.m */,
|
||||||
84E1CDEE2C4F688E00547904 /* ToolboxViewController.swift */,
|
84E1CDEE2C4F688E00547904 /* ToolboxViewController.swift */,
|
||||||
@@ -809,6 +927,7 @@
|
|||||||
FB8946E919F6AFB800339C8A /* lib */ = {
|
FB8946E919F6AFB800339C8A /* lib */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
180345E02E819877004AF36F /* libopus-sim.a */,
|
||||||
FB8946EA19F6AFB800339C8A /* libopus.a */,
|
FB8946EA19F6AFB800339C8A /* libopus.a */,
|
||||||
);
|
);
|
||||||
path = lib;
|
path = lib;
|
||||||
@@ -875,6 +994,28 @@
|
|||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget 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 */ = {
|
FB290CED19B2C406004C83CF /* VoidLink */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = FB290D2019B2C406004C83CF /* Build configuration list for PBXNativeTarget "VoidLink" */;
|
buildConfigurationList = FB290D2019B2C406004C83CF /* Build configuration list for PBXNativeTarget "VoidLink" */;
|
||||||
@@ -888,6 +1029,9 @@
|
|||||||
dependencies = (
|
dependencies = (
|
||||||
98AB2E861CAD468B0089BB98 /* PBXTargetDependency */,
|
98AB2E861CAD468B0089BB98 /* PBXTargetDependency */,
|
||||||
);
|
);
|
||||||
|
fileSystemSynchronizedGroups = (
|
||||||
|
18A0EE282EFBCBFB00BA853A /* SVGs */,
|
||||||
|
);
|
||||||
name = VoidLink;
|
name = VoidLink;
|
||||||
productName = Limelight;
|
productName = Limelight;
|
||||||
productReference = FB290CEE19B2C406004C83CF /* VoidLink.app */;
|
productReference = FB290CEE19B2C406004C83CF /* VoidLink.app */;
|
||||||
@@ -899,10 +1043,13 @@
|
|||||||
FB290CE619B2C406004C83CF /* Project object */ = {
|
FB290CE619B2C406004C83CF /* Project object */ = {
|
||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
LastSwiftUpdateCheck = 0730;
|
LastSwiftUpdateCheck = 2620;
|
||||||
LastUpgradeCheck = 1400;
|
LastUpgradeCheck = 1400;
|
||||||
ORGANIZATIONNAME = "Moonlight Game Streaming Project";
|
ORGANIZATIONNAME = "Moonlight Game Streaming Project";
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
|
2A039E552F72892B00546768 = {
|
||||||
|
CreatedOnToolsVersion = 26.2;
|
||||||
|
};
|
||||||
FB290CED19B2C406004C83CF = {
|
FB290CED19B2C406004C83CF = {
|
||||||
LastSwiftMigration = 1140;
|
LastSwiftMigration = 1140;
|
||||||
};
|
};
|
||||||
@@ -922,6 +1069,8 @@
|
|||||||
mainGroup = FB290CE519B2C406004C83CF;
|
mainGroup = FB290CE519B2C406004C83CF;
|
||||||
packageReferences = (
|
packageReferences = (
|
||||||
F74BEF9A2C1A705600224667 /* XCRemoteSwiftPackageReference "OpenSSL-Package" */,
|
F74BEF9A2C1A705600224667 /* XCRemoteSwiftPackageReference "OpenSSL-Package" */,
|
||||||
|
182760F52E83E2EA00EC1C5C /* XCRemoteSwiftPackageReference "swift-collections" */,
|
||||||
|
18A0EE232EFB9B5A00BA853A /* XCRemoteSwiftPackageReference "SVGKit" */,
|
||||||
);
|
);
|
||||||
productRefGroup = FB290CEF19B2C406004C83CF /* Products */;
|
productRefGroup = FB290CEF19B2C406004C83CF /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
@@ -934,6 +1083,7 @@
|
|||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
targets = (
|
targets = (
|
||||||
FB290CED19B2C406004C83CF /* VoidLink */,
|
FB290CED19B2C406004C83CF /* VoidLink */,
|
||||||
|
2A039E552F72892B00546768 /* VoidLinkPreview */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
@@ -956,6 +1106,13 @@
|
|||||||
/* End PBXReferenceProxy section */
|
/* End PBXReferenceProxy section */
|
||||||
|
|
||||||
/* Begin PBXResourcesBuildPhase section */
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
2A039E542F72892B00546768 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
FB290CEC19B2C406004C83CF /* Resources */ = {
|
FB290CEC19B2C406004C83CF /* Resources */ = {
|
||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -968,60 +1125,90 @@
|
|||||||
8454AC6A2C319BDA00F0A302 /* iPhone.storyboard in Resources */,
|
8454AC6A2C319BDA00F0A302 /* iPhone.storyboard in Resources */,
|
||||||
843667F02D9D7173004A7D7F /* widgetTemplates.bin in Resources */,
|
843667F02D9D7173004A7D7F /* widgetTemplates.bin in Resources */,
|
||||||
84FD0B542C3134A6006B5BE5 /* Localizable.xcstrings in Resources */,
|
84FD0B542C3134A6006B5BE5 /* Localizable.xcstrings in Resources */,
|
||||||
84351B5D2C29ADBE00A77989 /* ProfileTableViewCell.xib in Resources */,
|
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase 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 */ = {
|
FB290CEA19B2C406004C83CF /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
A1D500022FFC000100000001 /* authored_haptics_engine.cpp in Sources */,
|
||||||
FB290D0719B2C406004C83CF /* Limelight.xcdatamodeld in Sources */,
|
FB290D0719B2C406004C83CF /* Limelight.xcdatamodeld in Sources */,
|
||||||
9819CC22254F180F008A7C8E /* AbsoluteTouchHandler.m in Sources */,
|
9819CC22254F180F008A7C8E /* AbsoluteTouchHandler.m in Sources */,
|
||||||
9819CC14254F107A008A7C8E /* RelativeTouchHandler.m in Sources */,
|
9819CC14254F107A008A7C8E /* RelativeTouchHandler.m in Sources */,
|
||||||
FB1A674D2131E65900507771 /* KeyboardSupport.m in Sources */,
|
FB1A674D2131E65900507771 /* KeyboardSupport.m in Sources */,
|
||||||
97E20DDF2E2164A9000D5DCA /* Frame.m in Sources */,
|
97E20DDF2E2164A9000D5DCA /* Frame.m in Sources */,
|
||||||
97E20DE02E2164A9000D5DCA /* FrameQueue.m in Sources */,
|
97E20DE02E2164A9000D5DCA /* FrameQueue.m in Sources */,
|
||||||
|
18951D1D2EBB6D2300A86E12 /* TouchPadGestureHandler.swift in Sources */,
|
||||||
97E20DE12E2164A9000D5DCA /* FloatBuffer.m in Sources */,
|
97E20DE12E2164A9000D5DCA /* FloatBuffer.m in Sources */,
|
||||||
|
18FCADAF2ECAC23200787A96 /* PencilHandler.swift in Sources */,
|
||||||
97E20DE22E2164A9000D5DCA /* PaddedLabel.m in Sources */,
|
97E20DE22E2164A9000D5DCA /* PaddedLabel.m in Sources */,
|
||||||
97E20DE32E2164A9000D5DCA /* BandwidthTracker.m in Sources */,
|
97E20DE32E2164A9000D5DCA /* BandwidthTracker.m in Sources */,
|
||||||
84591A512BE1625E006AAB10 /* CustomEdgeSlideGestureRecognizer.m in Sources */,
|
84591A512BE1625E006AAB10 /* CustomEdgeSlideGestureRecognizer.m in Sources */,
|
||||||
843CC7F62C1E7A1700BBC475 /* NativeTouchHandler.m in Sources */,
|
843CC7F62C1E7A1700BBC475 /* NativeTouchHandler.m in Sources */,
|
||||||
97E20DEE2E2165A5000D5DCA /* ImGuiPlots.mm in Sources */,
|
97E20DEE2E2165A5000D5DCA /* ImGuiPlots.mm in Sources */,
|
||||||
FB89463219F646E200339C8A /* VideoDecoderRenderer.m in Sources */,
|
FB89463219F646E200339C8A /* VideoDecoderRenderer.m in Sources */,
|
||||||
|
A1F100012FFB000100000001 /* FrameInterpolator.swift in Sources */,
|
||||||
|
2A263D4F2FF3BA2800154DEA /* PublicUtils.swift in Sources */,
|
||||||
FB290D0419B2C406004C83CF /* AppDelegate.m in Sources */,
|
FB290D0419B2C406004C83CF /* AppDelegate.m in Sources */,
|
||||||
FB9AFD401A7E127D00872C98 /* AppListResponse.m in Sources */,
|
FB9AFD401A7E127D00872C98 /* AppListResponse.m in Sources */,
|
||||||
FB89463419F646E200339C8A /* Utils.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 */,
|
FB89462F19F646E200339C8A /* Connection.m in Sources */,
|
||||||
FB9AFD321A7D867C00872C98 /* AppAssetRetriever.m in Sources */,
|
FB9AFD321A7D867C00872C98 /* AppAssetRetriever.m in Sources */,
|
||||||
843E93BE2C41528700B1651F /* OrientationHelper.swift in Sources */,
|
843E93BE2C41528700B1651F /* OrientationHelper.swift in Sources */,
|
||||||
FB4678FF1A565DAC00377732 /* WakeOnLanManager.m in Sources */,
|
FB4678FF1A565DAC00377732 /* WakeOnLanManager.m in Sources */,
|
||||||
97E21BBF2E21882D000D5DCA /* imgui_tables.cpp in Sources */,
|
97E21BBF2E21882D000D5DCA /* imgui_tables.cpp in Sources */,
|
||||||
97E21BC02E21882D000D5DCA /* imgui_demo.cpp in Sources */,
|
97E21BC02E21882D000D5DCA /* imgui_demo.cpp in Sources */,
|
||||||
|
2A263D4A2FF3B3F800154DEA /* PublicExtensions.swift in Sources */,
|
||||||
97E21BC12E21882D000D5DCA /* imgui_widgets.cpp in Sources */,
|
97E21BC12E21882D000D5DCA /* imgui_widgets.cpp in Sources */,
|
||||||
97E21BC22E21882D000D5DCA /* imgui.cpp in Sources */,
|
97E21BC22E21882D000D5DCA /* imgui.cpp in Sources */,
|
||||||
97E21BC32E21882D000D5DCA /* imgui_draw.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 */,
|
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 */,
|
FB53E1431BE5DC4400CD6ECE /* IdManager.m in Sources */,
|
||||||
189AD29A2DE7446000E24B88 /* HostCollectionViewController.m in Sources */,
|
189AD29A2DE7446000E24B88 /* HostCollectionViewController.swift in Sources */,
|
||||||
9897B6A1221260EF00966419 /* VoidController.m in Sources */,
|
9897B6A1221260EF00966419 /* VoidController.m in Sources */,
|
||||||
84351B112C29ACCB00A77989 /* OSCProfilesManager.m in Sources */,
|
84351B112C29ACCB00A77989 /* OSCProfilesManager.swift in Sources */,
|
||||||
FB89462919F646E200339C8A /* mkcert.c in Sources */,
|
FB89462919F646E200339C8A /* mkcert.c in Sources */,
|
||||||
FB9AFD281A7C84ED00872C98 /* HttpResponse.m in Sources */,
|
FB9AFD281A7C84ED00872C98 /* HttpResponse.m in Sources */,
|
||||||
FB89463019F646E200339C8A /* StreamConfiguration.m in Sources */,
|
FB89463019F646E200339C8A /* StreamConfiguration.m in Sources */,
|
||||||
|
1850C82A2E667E4000845308 /* MicHandler.swift in Sources */,
|
||||||
FBD3495319FF36FB002D2A60 /* SWRevealViewController.m in Sources */,
|
FBD3495319FF36FB002D2A60 /* SWRevealViewController.m in Sources */,
|
||||||
84203F232C5F24C00032EEED /* OnScreenWidgetView.swift 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 */,
|
97E21BB52E2186A3000D5DCA /* imgui_impl_metal.mm in Sources */,
|
||||||
9777AAF32E2CF7A400D7FBAF /* Shaders-Linear.metal in Sources */,
|
9777AAF32E2CF7A400D7FBAF /* Shaders-Linear.metal in Sources */,
|
||||||
84FD0B512C31338D006B5BE5 /* LocalizationHelper.m in Sources */,
|
84FD0B512C31338D006B5BE5 /* LocalizationHelper.m in Sources */,
|
||||||
97E20DEA2E216532000D5DCA /* SceneDelegate.m in Sources */,
|
97E20DEA2E216532000D5DCA /* SceneDelegate.m in Sources */,
|
||||||
|
1817B61E2E6FD04D000D938D /* SafeTimer.swift in Sources */,
|
||||||
FBD3495019FF2174002D2A60 /* SettingsViewController.m in Sources */,
|
FBD3495019FF2174002D2A60 /* SettingsViewController.m in Sources */,
|
||||||
|
2B7A10022FFB000100000001 /* SettingsTouchVelocityPreview.swift in Sources */,
|
||||||
843CC7F02C1E757D00BBC475 /* NativeTouchPointer.m in Sources */,
|
843CC7F02C1E757D00BBC475 /* NativeTouchPointer.m in Sources */,
|
||||||
|
2A7113FD2F5A6C65001D852D /* ThemeManager.swift in Sources */,
|
||||||
FB89462C19F646E200339C8A /* HttpManager.m in Sources */,
|
FB89462C19F646E200339C8A /* HttpManager.m in Sources */,
|
||||||
FB89462D19F646E200339C8A /* MDNSManager.m in Sources */,
|
FB89462D19F646E200339C8A /* MDNSManager.m in Sources */,
|
||||||
84351B0F2C29ACCB00A77989 /* OnScreenButtonState.m in Sources */,
|
84351B0F2C29ACCB00A77989 /* OnScreenButtonState.m in Sources */,
|
||||||
@@ -1029,12 +1216,14 @@
|
|||||||
840751B32BF49C050096B657 /* CustomTapGestureRecognizer.m in Sources */,
|
840751B32BF49C050096B657 /* CustomTapGestureRecognizer.m in Sources */,
|
||||||
187F4B282E04658A00EECC59 /* WidgetPanelStackView.m in Sources */,
|
187F4B282E04658A00EECC59 /* WidgetPanelStackView.m in Sources */,
|
||||||
FB4678FA1A55FFAD00377732 /* DiscoveryManager.m in Sources */,
|
FB4678FA1A55FFAD00377732 /* DiscoveryManager.m in Sources */,
|
||||||
841BAC922DE35D6B00DA639E /* ThemeManager.m in Sources */,
|
18D43D7F2F050240008C326F /* PressureCurve.swift in Sources */,
|
||||||
|
18D43D812F050240008C326F /* PencilTipOffsetCalibrationViewController.swift in Sources */,
|
||||||
|
180E6E372E93BFDB0067C73A /* AlertControllerUtil.swift in Sources */,
|
||||||
98D5856D1C0EA79600F6CC00 /* TemporaryHost.m in Sources */,
|
98D5856D1C0EA79600F6CC00 /* TemporaryHost.m in Sources */,
|
||||||
FB89463519F646E200339C8A /* MainFrameViewController.m in Sources */,
|
FB89463519F646E200339C8A /* MainFrameViewController.m in Sources */,
|
||||||
FBD1C8E21A8AD71400C6703C /* Logger.m in Sources */,
|
FBD1C8E21A8AD71400C6703C /* Logger.m in Sources */,
|
||||||
FB1D599A1BBCCD7E00F482CA /* AppCollectionView.m in Sources */,
|
FB1D599A1BBCCD7E00F482CA /* AppCollectionView.m in Sources */,
|
||||||
848F456A2DDCD12500581E36 /* HostCardView.m in Sources */,
|
848F456A2DDCD12500581E36 /* HostCardView.swift in Sources */,
|
||||||
FB89463619F646E200339C8A /* StreamFrameViewController.m in Sources */,
|
FB89463619F646E200339C8A /* StreamFrameViewController.m in Sources */,
|
||||||
9827E7A32514366900F25707 /* HapticContext.m in Sources */,
|
9827E7A32514366900F25707 /* HapticContext.m in Sources */,
|
||||||
9832D1361BBCD5C50036EF48 /* TemporaryApp.m in Sources */,
|
9832D1361BBCD5C50036EF48 /* TemporaryApp.m in Sources */,
|
||||||
@@ -1045,33 +1234,39 @@
|
|||||||
97E20DD02E21647A000D5DCA /* MetalViewController.m in Sources */,
|
97E20DD02E21647A000D5DCA /* MetalViewController.m in Sources */,
|
||||||
97E20DD12E21647A000D5DCA /* MetalView.m in Sources */,
|
97E20DD12E21647A000D5DCA /* MetalView.m in Sources */,
|
||||||
97E20DD22E21647A000D5DCA /* MetalVideoRenderer.m in Sources */,
|
97E20DD22E21647A000D5DCA /* MetalVideoRenderer.m in Sources */,
|
||||||
84351B2C2C29AD6100A77989 /* OSCProfilesTableViewController.m in Sources */,
|
3A4A3A112F0C000100000001 /* ProfileSelectorViewController.swift in Sources */,
|
||||||
98D585701C0ED0E800F6CC00 /* TemporarySettings.m in Sources */,
|
98D585701C0ED0E800F6CC00 /* TemporarySettings.m in Sources */,
|
||||||
1829CE3F2E2154DF0074A99A /* SceneDelegate.m in Sources */,
|
1829CE3F2E2154DF0074A99A /* SceneDelegate.m in Sources */,
|
||||||
|
18729EDE2E8BD68800101194 /* UUIDHelper.swift in Sources */,
|
||||||
FB89462819F646E200339C8A /* CryptoManager.m in Sources */,
|
FB89462819F646E200339C8A /* CryptoManager.m in Sources */,
|
||||||
DC1F5A07206436B20037755F /* ConnectionHelper.m in Sources */,
|
DC1F5A07206436B20037755F /* ConnectionHelper.m in Sources */,
|
||||||
FB89462E19F646E200339C8A /* PairManager.m in Sources */,
|
FB89462E19F646E200339C8A /* PairManager.m in Sources */,
|
||||||
FB9AFD371A7E02DB00872C98 /* HttpRequest.m in Sources */,
|
FB9AFD371A7E02DB00872C98 /* HttpRequest.m in Sources */,
|
||||||
1888C1932E24C41000108730 /* AboutView.swift in Sources */,
|
1888C1932E24C41000108730 /* AboutView.swift in Sources */,
|
||||||
FB4678ED1A50C40900377732 /* OnScreenControls.m in Sources */,
|
FB4678ED1A50C40900377732 /* OnScreenControls.m in Sources */,
|
||||||
84E1CDFA2C4FE83400547904 /* SwiftLocalizationHelper.swift in Sources */,
|
84E1CDFA2C4FE83400547904 /* LocalizationHelperExtension.swift in Sources */,
|
||||||
FB290D0019B2C406004C83CF /* main.m in Sources */,
|
FB290D0019B2C406004C83CF /* main.m in Sources */,
|
||||||
9896219723D15C7000211983 /* X1Kit.swift in Sources */,
|
9896219723D15C7000211983 /* X1Kit.swift in Sources */,
|
||||||
FBD3494319FC9C04002D2A60 /* AppAssetManager.m in Sources */,
|
FBD3494319FC9C04002D2A60 /* AppAssetManager.m in Sources */,
|
||||||
FB6549561A57907E001C8F39 /* DiscoveryWorker.m in Sources */,
|
FB6549561A57907E001C8F39 /* DiscoveryWorker.m in Sources */,
|
||||||
84351B0E2C29ACCB00A77989 /* LayoutOnScreenControls.m in Sources */,
|
84351B0E2C29ACCB00A77989 /* LayoutOnScreenControls.m in Sources */,
|
||||||
|
A76B00032FFCC00100000003 /* GameSirG8MFiRumbler.swift in Sources */,
|
||||||
FB89462A19F646E200339C8A /* ControllerSupport.m in Sources */,
|
FB89462A19F646E200339C8A /* ControllerSupport.m in Sources */,
|
||||||
FB9AFD3D1A7E111600872C98 /* AppAssetResponse.m in Sources */,
|
FB9AFD3D1A7E111600872C98 /* AppAssetResponse.m in Sources */,
|
||||||
1888C1922E24C33100108730 /* AboutViewController.swift in Sources */,
|
1888C1922E24C33100108730 /* AboutViewController.swift in Sources */,
|
||||||
FBD349621A0089F6002D2A60 /* DataManager.m in Sources */,
|
FBD349621A0089F6002D2A60 /* DataManager.m in Sources */,
|
||||||
97E20DE72E216510000D5DCA /* ImGuiRenderer.mm in Sources */,
|
97E20DE72E216510000D5DCA /* ImGuiRenderer.mm in Sources */,
|
||||||
|
18B317032EE67A3A009B62C2 /* InertialScroller.swift in Sources */,
|
||||||
|
2AC1EAFC2F922889004EDA80 /* UIStackExtension.swift in Sources */,
|
||||||
FB4A23B81A9D3637004D2EF2 /* LoadingFrameViewController.m in Sources */,
|
FB4A23B81A9D3637004D2EF2 /* LoadingFrameViewController.m in Sources */,
|
||||||
FB9AFD3A1A7E05CE00872C98 /* ServerInfoResponse.m in Sources */,
|
FB9AFD3A1A7E05CE00872C98 /* ServerInfoResponse.m in Sources */,
|
||||||
84351B2B2C29AD6100A77989 /* LayoutOnScreenControlsViewController.m in Sources */,
|
3A4A3A132F0C000100000003 /* LayoutOnScreenControlsViewController.swift in Sources */,
|
||||||
84351B5C2C29ADBE00A77989 /* ProfileTableViewCell.m in Sources */,
|
|
||||||
18341A042DEB662C0094E5F2 /* FixedTintImageView.m in Sources */,
|
18341A042DEB662C0094E5F2 /* FixedTintImageView.m in Sources */,
|
||||||
|
18416EA42EBB3DCE00AE0124 /* UITouchUtil.swift in Sources */,
|
||||||
FB89463119F646E200339C8A /* StreamManager.m in Sources */,
|
FB89463119F646E200339C8A /* StreamManager.m in Sources */,
|
||||||
|
18780CCA2F10D6CC00B90483 /* IAPManager.swift in Sources */,
|
||||||
84351B2D2C29AD6100A77989 /* ToolBarContainerView.m in Sources */,
|
84351B2D2C29AD6100A77989 /* ToolBarContainerView.m in Sources */,
|
||||||
|
18E4BCEA2E891895000A8657 /* MotionHandler.swift in Sources */,
|
||||||
988FCD41293B091B003050E2 /* KeyboardInputField.m in Sources */,
|
988FCD41293B091B003050E2 /* KeyboardInputField.m in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@@ -1124,6 +1319,105 @@
|
|||||||
/* End PBXVariantGroup section */
|
/* End PBXVariantGroup section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration 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 */ = {
|
FB290D1E19B2C406004C83CF /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
@@ -1247,10 +1541,13 @@
|
|||||||
GCC_PREFIX_HEADER = "VoidLink/Limelight-Prefix.pch";
|
GCC_PREFIX_HEADER = "VoidLink/Limelight-Prefix.pch";
|
||||||
HEADER_SEARCH_PATHS = (
|
HEADER_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
|
"$(PROJECT_DIR)/third_party/moonlight-audio-haptics/include",
|
||||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
|
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
|
||||||
"$(PROJECT_DIR)/moonlight-common/moonlight-common-c/src",
|
"$(PROJECT_DIR)/moonlight-common/moonlight-common-c/src",
|
||||||
"$(SDKROOT)/usr/include/libxml2/**",
|
"$(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/Input/CustomOSC",
|
||||||
"$(PROJECT_DIR)/Limelight/ViewControllers/CustomOSCViewControl",
|
"$(PROJECT_DIR)/Limelight/ViewControllers/CustomOSCViewControl",
|
||||||
"$(PROJECT_DIR)/Limelight/ViewControllers/CustomOSCViewControl/Cells",
|
"$(PROJECT_DIR)/Limelight/ViewControllers/CustomOSCViewControl/Cells",
|
||||||
@@ -1260,7 +1557,7 @@
|
|||||||
"$(PROJECT_DIR)/Limelight/Database",
|
"$(PROJECT_DIR)/Limelight/Database",
|
||||||
);
|
);
|
||||||
INFOPLIST_FILE = "VoidLink/Limelight-Info.plist";
|
INFOPLIST_FILE = "VoidLink/Limelight-Info.plist";
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "VoidLink - beta";
|
INFOPLIST_KEY_CFBundleDisplayName = VoidLink;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@@ -1271,6 +1568,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"$(PROJECT_DIR)/third_party/imgui/examples/libs/glfw/lib-vc2010-32",
|
"$(PROJECT_DIR)/third_party/imgui/examples/libs/glfw/lib-vc2010-32",
|
||||||
"$(PROJECT_DIR)/third_party/imgui/examples/libs/glfw/lib-vc2010-64",
|
"$(PROJECT_DIR)/third_party/imgui/examples/libs/glfw/lib-vc2010-64",
|
||||||
|
"$(PROJECT_DIR)/libs/opus/lib",
|
||||||
);
|
);
|
||||||
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = (
|
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@@ -1285,6 +1583,8 @@
|
|||||||
"$(PROJECT_DIR)/libs/FFmpeg/lib/iOS-Sim",
|
"$(PROJECT_DIR)/libs/FFmpeg/lib/iOS-Sim",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.0.1;
|
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_BUNDLE_IDENTIFIER = com.voidlink.tf.debug10.iOS;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE = "";
|
PROVISIONING_PROFILE = "";
|
||||||
@@ -1312,10 +1612,13 @@
|
|||||||
GCC_PREFIX_HEADER = "VoidLink/Limelight-Prefix.pch";
|
GCC_PREFIX_HEADER = "VoidLink/Limelight-Prefix.pch";
|
||||||
HEADER_SEARCH_PATHS = (
|
HEADER_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
|
"$(PROJECT_DIR)/third_party/moonlight-audio-haptics/include",
|
||||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
|
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
|
||||||
"$(PROJECT_DIR)/moonlight-common/moonlight-common-c/src",
|
"$(PROJECT_DIR)/moonlight-common/moonlight-common-c/src",
|
||||||
"$(SDKROOT)/usr/include/libxml2/**",
|
"$(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/Input/CustomOSC",
|
||||||
"$(PROJECT_DIR)/Limelight/ViewControllers/CustomOSCViewControl",
|
"$(PROJECT_DIR)/Limelight/ViewControllers/CustomOSCViewControl",
|
||||||
"$(PROJECT_DIR)/Limelight/ViewControllers/CustomOSCViewControl/Cells",
|
"$(PROJECT_DIR)/Limelight/ViewControllers/CustomOSCViewControl/Cells",
|
||||||
@@ -1325,7 +1628,7 @@
|
|||||||
"$(PROJECT_DIR)/Limelight/Database",
|
"$(PROJECT_DIR)/Limelight/Database",
|
||||||
);
|
);
|
||||||
INFOPLIST_FILE = "VoidLink/Limelight-Info.plist";
|
INFOPLIST_FILE = "VoidLink/Limelight-Info.plist";
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "VoidLink - beta";
|
INFOPLIST_KEY_CFBundleDisplayName = VoidLink;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@@ -1336,6 +1639,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"$(PROJECT_DIR)/third_party/imgui/examples/libs/glfw/lib-vc2010-32",
|
"$(PROJECT_DIR)/third_party/imgui/examples/libs/glfw/lib-vc2010-32",
|
||||||
"$(PROJECT_DIR)/third_party/imgui/examples/libs/glfw/lib-vc2010-64",
|
"$(PROJECT_DIR)/third_party/imgui/examples/libs/glfw/lib-vc2010-64",
|
||||||
|
"$(PROJECT_DIR)/libs/opus/lib",
|
||||||
);
|
);
|
||||||
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = (
|
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@@ -1350,6 +1654,8 @@
|
|||||||
"$(PROJECT_DIR)/libs/FFmpeg/lib/iOS-Sim",
|
"$(PROJECT_DIR)/libs/FFmpeg/lib/iOS-Sim",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.0.1;
|
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_BUNDLE_IDENTIFIER = com.voidlink.tf.iOS;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE = "";
|
PROVISIONING_PROFILE = "";
|
||||||
@@ -1367,6 +1673,15 @@
|
|||||||
/* End XCBuildConfiguration section */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList 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" */ = {
|
FB290CE919B2C406004C83CF /* Build configuration list for PBXProject "VoidLink" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
@@ -1388,6 +1703,22 @@
|
|||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
/* Begin XCRemoteSwiftPackageReference section */
|
/* Begin XCRemoteSwiftPackageReference section */
|
||||||
|
182760F52E83E2EA00EC1C5C /* XCRemoteSwiftPackageReference "swift-collections" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/apple/swift-collections.git";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 1.1.4;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
18A0EE232EFB9B5A00BA853A /* XCRemoteSwiftPackageReference "SVGKit" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/SVGKit/SVGKit.git";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 3.0.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
F74BEF9A2C1A705600224667 /* XCRemoteSwiftPackageReference "OpenSSL-Package" */ = {
|
F74BEF9A2C1A705600224667 /* XCRemoteSwiftPackageReference "OpenSSL-Package" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/krzyzanowskim/OpenSSL-Package.git";
|
repositoryURL = "https://github.com/krzyzanowskim/OpenSSL-Package.git";
|
||||||
@@ -1399,6 +1730,31 @@
|
|||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
182760F62E83E2EA00EC1C5C /* DequeModule */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 182760F52E83E2EA00EC1C5C /* XCRemoteSwiftPackageReference "swift-collections" */;
|
||||||
|
productName = DequeModule;
|
||||||
|
};
|
||||||
|
182760F92E83E57D00EC1C5C /* DequeModule */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 182760F52E83E2EA00EC1C5C /* XCRemoteSwiftPackageReference "swift-collections" */;
|
||||||
|
productName = DequeModule;
|
||||||
|
};
|
||||||
|
182760FC2E83E68B00EC1C5C /* Collections */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 182760F52E83E2EA00EC1C5C /* XCRemoteSwiftPackageReference "swift-collections" */;
|
||||||
|
productName = Collections;
|
||||||
|
};
|
||||||
|
18A0EE242EFB9B5A00BA853A /* SVGKit */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 18A0EE232EFB9B5A00BA853A /* XCRemoteSwiftPackageReference "SVGKit" */;
|
||||||
|
productName = SVGKit;
|
||||||
|
};
|
||||||
|
18A0EE262EFB9B5A00BA853A /* SVGKitSwift */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 18A0EE232EFB9B5A00BA853A /* XCRemoteSwiftPackageReference "SVGKit" */;
|
||||||
|
productName = SVGKitSwift;
|
||||||
|
};
|
||||||
F74BEF9B2C1A705600224667 /* OpenSSL */ = {
|
F74BEF9B2C1A705600224667 /* OpenSSL */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = F74BEF9A2C1A705600224667 /* XCRemoteSwiftPackageReference "OpenSSL-Package" */;
|
package = F74BEF9A2C1A705600224667 /* XCRemoteSwiftPackageReference "OpenSSL-Package" */;
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -1,6 +1,15 @@
|
|||||||
{
|
{
|
||||||
"originHash" : "f17bf6f1131c7255a75ffd6a989b69573cb49c332e612dffb429905d8d7afe72",
|
"originHash" : "d1fc50a28d75017e32553df2b233f8a7e2cf1542da32728ce6722b200f745608",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
|
{
|
||||||
|
"identity" : "cocoalumberjack",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/CocoaLumberjack/CocoaLumberjack.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "a9ed4b6f9bdedce7d77046f43adfb8ce1fd54114",
|
||||||
|
"version" : "3.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "openssl-package",
|
"identity" : "openssl-package",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
@@ -9,6 +18,33 @@
|
|||||||
"revision" : "b9eb055fdf73e595cb4b9f665d13dc85975bf80a",
|
"revision" : "b9eb055fdf73e595cb4b9f665d13dc85975bf80a",
|
||||||
"version" : "3.3.2000"
|
"version" : "3.3.2000"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "svgkit",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/SVGKit/SVGKit.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "58152b9f7c85eab239160b36ffdfd364aa43d666",
|
||||||
|
"version" : "3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-collections",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-collections.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "c1805596154bb3a265fd91b8ac0c4433b4348fb0",
|
||||||
|
"version" : "1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-log",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-log",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "bc386b95f2a16ccd0150a8235e7c69eab2b866ca",
|
||||||
|
"version" : "1.8.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"version" : 3
|
"version" : 3
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ public struct AboutView: View {
|
|||||||
public var aboutVC:UIViewController
|
public var aboutVC:UIViewController
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
VStack(spacing: 10) {
|
VStack(spacing: 7) {
|
||||||
// App 图标
|
// App 图标
|
||||||
Image(uiImage: UIImage(named: "AppIconMedium") ?? UIImage())
|
Image(uiImage: UIImage(named: "AppIconMedium") ?? UIImage())
|
||||||
// .resizable()
|
// .resizable()
|
||||||
.resizable(capInsets:EdgeInsets(top: 3.5, leading: 3.5, bottom: 3.5, trailing: 3.5))
|
.resizable(capInsets:EdgeInsets(top: 3.5, leading: 3.5, bottom: 3.5, trailing: 3.5))
|
||||||
.frame(width: 100, height: 100)
|
.frame(width: 90, height: 90)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 28))
|
.clipShape(RoundedRectangle(cornerRadius: 28))
|
||||||
|
|
||||||
// App 名称
|
// App 名称
|
||||||
@@ -35,47 +35,107 @@ public struct AboutView: View {
|
|||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 说明文字
|
|
||||||
Text(SwiftLocalizationHelper.localizedString(forKey: "From the player community, to the player community."))
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.lineLimit(nil)
|
|
||||||
.frame(maxWidth: 570) // ✅ 避免 Text 被拉得太宽无法换行
|
|
||||||
|
|
||||||
if Locale.preferredLanguages.first!.hasPrefix("zh-Hans") {
|
if Locale.preferredLanguages.first!.hasPrefix("zh-Hans") {
|
||||||
if #available(iOS 14.0, *) {
|
if #available(iOS 14.0, *) {
|
||||||
Text("闽ICP备17012590号-3A\n主办单位:福州创图信息技术有限公司").font(.caption2)
|
Text("闽ICP备17012590号-3A 主办单位:福州创图信息技术有限公司").font(.caption2)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.lineLimit(nil)
|
.lineLimit(nil)
|
||||||
.frame(maxWidth: 570) // ✅ 避免 Text 被拉得太宽无法换行
|
.frame(maxWidth: 570) // ✅ 避免 Text 被拉得太宽无法换行
|
||||||
.padding()
|
//.padding()
|
||||||
} else {
|
} else {
|
||||||
Text("闽ICP备17012590号-3A\n主办单位:福州创图信息技术有限公司").font(.caption)
|
Text("闽ICP备17012590号-3A 主办单位:福州创图信息技术有限公司").font(.caption)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.lineLimit(nil)
|
.lineLimit(nil)
|
||||||
.frame(maxWidth: 570) // ✅ 避免 Text 被拉得太宽无法换行
|
.frame(maxWidth: 570) // ✅ 避免 Text 被拉得太宽无法换行
|
||||||
.padding()
|
//.padding()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 说明文字
|
||||||
|
Text(LocalizationHelper.localizedString(forKey: "From the player community, to the player community."))
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.font(Font.caption.italic())
|
||||||
|
.lineLimit(nil)
|
||||||
|
.frame(maxWidth: 570) // ✅ 避免 Text 被拉得太宽无法换行
|
||||||
|
.padding()
|
||||||
|
|
||||||
|
|
||||||
|
Text(LocalizationHelper.localizedString(forKey: "VoidLink delivers better performance now!"))
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.font(Font.callout.bold())
|
||||||
|
.lineLimit(nil)
|
||||||
|
.frame(maxWidth: 570) // ✅ 避免 Text 被拉得太宽无法换行
|
||||||
|
|
||||||
|
|
||||||
// 链接按钮
|
// 链接按钮
|
||||||
if #available(iOS 14.0, *) {
|
if #available(iOS 14.0, *) {
|
||||||
Link(SwiftLocalizationHelper.localizedString(forKey: "Join us"), destination: URL(string: SwiftLocalizationHelper.localizedString(forKey: "supportLink"))!)
|
|
||||||
.padding(.top, 10)
|
HStack(spacing: 20) {
|
||||||
Spacer()
|
Link(LocalizationHelper.localizedString(forKey: "Learn more"), destination: URL(string: LocalizationHelper.localizedString(forKey: "supportLink"))!)
|
||||||
// OK 按钮
|
|
||||||
Button(SwiftLocalizationHelper.localizedString(forKey: "OK")) {
|
if #available(iOS 16, *) {
|
||||||
aboutVC.dismiss(animated:true)
|
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()
|
.padding(.top, 10)
|
||||||
.background(Color.blue)
|
|
||||||
.foregroundColor(.white)
|
if !PublicUtils.isIPhone {
|
||||||
.frame(height: 46)
|
Link(LocalizationHelper.localizedString(forKey: "joinCommunity"), destination: URL(string: LocalizationHelper.localizedString(forKey: "communityLink"))!)
|
||||||
.cornerRadius(12)
|
}
|
||||||
|
|
||||||
|
HStack(spacing: 15) {
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
if PublicUtils.isIPad, PublicUtils.isIAPAddonAvailable {
|
||||||
|
Button(LocalizationHelper.localizedString(forKey: "I'm an artist")) {
|
||||||
|
IAPManager.checkPurchaseInfo(.PencilProPack) { info in
|
||||||
|
if !info.valid {
|
||||||
|
IAPManager.inAppPurchaseAction(viewController: self.aboutVC, product: .PencilProPack)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
AlertControllerUtil.showAlert(
|
||||||
|
in: self.aboutVC,
|
||||||
|
title: "",
|
||||||
|
message: LocalizationHelper.localizedString(forKey:"Drawing Toolkit already purchased"),
|
||||||
|
withCancel: false,
|
||||||
|
buttonTitle: LocalizationHelper.localizedString(forKey: "OK"),
|
||||||
|
countdown: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color.blue.opacity(1))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.frame(height: 46)
|
||||||
|
.cornerRadius(12)
|
||||||
|
.padding(.top, 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OK 按钮
|
||||||
|
Button(LocalizationHelper.localizedString(forKey: "OK")) {
|
||||||
|
aboutVC.dismiss(animated:true)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color.blue)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.frame(height: 46)
|
||||||
|
.cornerRadius(12)
|
||||||
|
.padding(.top, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
HStack(spacing: 20) {
|
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)
|
UIApplication.shared.open(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,7 +146,7 @@ public struct AboutView: View {
|
|||||||
.frame(height: 33)
|
.frame(height: 33)
|
||||||
.frame(minWidth: 100)
|
.frame(minWidth: 100)
|
||||||
/*
|
/*
|
||||||
Button(SwiftLocalizationHelper.localizedString(forKey: "OK")) {
|
Button(LocalizationHelper.localizedString(forKey: "OK")) {
|
||||||
presentationMode.wrappedValue.dismiss()
|
presentationMode.wrappedValue.dismiss()
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
@@ -95,7 +155,7 @@ public struct AboutView: View {
|
|||||||
.cornerRadius(12)
|
.cornerRadius(12)
|
||||||
.frame(height: 33)
|
.frame(height: 33)
|
||||||
.frame(minWidth: 100)*/
|
.frame(minWidth: 100)*/
|
||||||
Button(SwiftLocalizationHelper.localizedString(forKey: "OK")) {
|
Button(LocalizationHelper.localizedString(forKey: "OK")) {
|
||||||
aboutVC.dismiss(animated:true)
|
aboutVC.dismiss(animated:true)
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
|
|||||||
@@ -10,7 +10,26 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
@available(iOS 13.0, *)
|
@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() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
@@ -28,12 +47,18 @@ import SwiftUI
|
|||||||
hostingVC.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
hostingVC.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
])
|
])
|
||||||
|
|
||||||
// 🎯 设置弹窗大小
|
preferredContentSize = CGSize(width: 530, height: 430)
|
||||||
preferredContentSize = CGSize(width: 530, height: 450)
|
|
||||||
|
|
||||||
// ⚠️ 一定要设置以下样式才会生效 preferredContentSize
|
|
||||||
modalPresentationStyle = .formSheet
|
modalPresentationStyle = .formSheet
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
ControllerNavigator.setUINavigationDelegate(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidDisappear(_ animated: Bool) {
|
||||||
|
ControllerNavigator.restorePreviousUINavigationDelegate(ifCurrentDelegateIs: self)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func isIPhone() -> Bool {
|
func isIPhone() -> Bool {
|
||||||
|
|||||||
@@ -0,0 +1,199 @@
|
|||||||
|
//
|
||||||
|
// AlertControllerUtil.swift
|
||||||
|
// VoidLink
|
||||||
|
//
|
||||||
|
// Created by Weimin on 2025/10/6.
|
||||||
|
// Copyright © 2025 True砖家 @ Bilibili. All rights reserved.
|
||||||
|
//
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
@objc class AlertControllerUtil: NSObject {
|
||||||
|
private enum AlertTextRole {
|
||||||
|
case title
|
||||||
|
case message
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func makeCountdownMessage(baseMessage: String?, remainingSeconds: Int) -> String {
|
||||||
|
let countdownText = "\(remainingSeconds)"
|
||||||
|
guard let baseMessage, !baseMessage.isEmpty else {
|
||||||
|
return countdownText
|
||||||
|
}
|
||||||
|
return "\(baseMessage)\n\n\(countdownText)"
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 13.0, *)
|
||||||
|
private static func makeLeftAlignedAttributedText(_ text: String, role: AlertTextRole) -> NSAttributedString {
|
||||||
|
let paragraphStyle = NSMutableParagraphStyle()
|
||||||
|
paragraphStyle.alignment = .left
|
||||||
|
paragraphStyle.lineBreakMode = .byWordWrapping
|
||||||
|
|
||||||
|
let font: UIFont
|
||||||
|
let color: UIColor
|
||||||
|
|
||||||
|
switch role {
|
||||||
|
case .title:
|
||||||
|
font = .preferredFont(forTextStyle: .headline)
|
||||||
|
color = .label
|
||||||
|
case .message:
|
||||||
|
font = .preferredFont(forTextStyle: .footnote)
|
||||||
|
color = .label
|
||||||
|
}
|
||||||
|
|
||||||
|
return NSAttributedString(
|
||||||
|
string: text,
|
||||||
|
attributes: [
|
||||||
|
.paragraphStyle: paragraphStyle,
|
||||||
|
.font: font,
|
||||||
|
.foregroundColor: color
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func applyLeftAlignment(title: String?, message: String?) {
|
||||||
|
if #available(iOS 26.0, *) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
if let title, !title.isEmpty {
|
||||||
|
alertController.setValue(makeLeftAlignedAttributedText(title, role: .title), forKey: "attributedTitle")
|
||||||
|
}
|
||||||
|
|
||||||
|
if let message, !message.isEmpty {
|
||||||
|
alertController.setValue(makeLeftAlignedAttributedText(message, role: .message), forKey: "attributedMessage")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 类方法,直接在任何 UIViewController 上显示倒计时弹窗
|
||||||
|
/// - Parameters:
|
||||||
|
/// - viewController: 要显示弹窗的控制器
|
||||||
|
/// - title: 弹窗标题
|
||||||
|
/// - message: 弹窗内容
|
||||||
|
/// - buttonTitle: 按钮初始文字(倒计时结束后显示)
|
||||||
|
/// - countdown: 倒计时秒数
|
||||||
|
/// - completion: 点击确认或倒计时结束的回调
|
||||||
|
|
||||||
|
@objc public static var actionCancelled:Bool = false
|
||||||
|
@objc public static var alertController:UIAlertController = UIAlertController()
|
||||||
|
@objc public static var cancelButtonString:String = "Cancel"
|
||||||
|
@objc static var completion: (() -> Void)? = nil
|
||||||
|
@objc public static var autoCompletion:Bool = false
|
||||||
|
@objc public static var isCountingDown:Bool = false
|
||||||
|
|
||||||
|
@objc class func showAlert(
|
||||||
|
in viewController: UIViewController? = nil,
|
||||||
|
title: String?,
|
||||||
|
message: String?,
|
||||||
|
withCancel: Bool,
|
||||||
|
buttonTitle: String,
|
||||||
|
countdown: Int,
|
||||||
|
action: (() -> Void)? = nil,
|
||||||
|
completion: (() -> Void)? = nil
|
||||||
|
) {
|
||||||
|
var remainingSeconds = countdown
|
||||||
|
let originalMessage = message
|
||||||
|
var isAlertDismissed = false
|
||||||
|
|
||||||
|
AlertControllerUtil.completion = completion
|
||||||
|
|
||||||
|
guard let viewController = viewController else {return}
|
||||||
|
|
||||||
|
alertController = UIAlertController(
|
||||||
|
title: title,
|
||||||
|
message: (countdown > 0 && !autoCompletion) ? makeCountdownMessage(baseMessage: originalMessage, remainingSeconds: remainingSeconds) : message,
|
||||||
|
preferredStyle: .alert
|
||||||
|
)
|
||||||
|
applyLeftAlignment(
|
||||||
|
title: title,
|
||||||
|
message: (countdown > 0 && !autoCompletion) ? makeCountdownMessage(baseMessage: originalMessage, remainingSeconds: remainingSeconds) : message
|
||||||
|
)
|
||||||
|
|
||||||
|
let confirmAction = UIAlertAction(title: buttonTitle, style: .default) { _ in
|
||||||
|
isAlertDismissed = true
|
||||||
|
actionCancelled = false
|
||||||
|
completion?()
|
||||||
|
cancelButtonString = "Cancel"
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelAction = UIAlertAction(title: LocalizationHelper.localizedString(forKey: cancelButtonString), style: .cancel) { _ in
|
||||||
|
isAlertDismissed = true
|
||||||
|
actionCancelled = true
|
||||||
|
completion?()
|
||||||
|
cancelButtonString = "Cancel"
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmAction.isEnabled = false
|
||||||
|
|
||||||
|
if withCancel {alertController.addAction(cancelAction)}
|
||||||
|
if buttonTitle != "" && !autoCompletion {alertController.addAction(confirmAction)}
|
||||||
|
|
||||||
|
if(countdown == 0) {
|
||||||
|
confirmAction.isEnabled = buttonTitle != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
viewController.present(alertController, animated: true, completion: action)
|
||||||
|
isCountingDown = !autoCompletion && countdown > 0
|
||||||
|
|
||||||
|
if(countdown == 0) {return}
|
||||||
|
|
||||||
|
// 倒计时
|
||||||
|
let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.main)
|
||||||
|
timer.schedule(deadline: .now() + (autoCompletion ? 0.5 : 1.0), repeating: autoCompletion ? 0.1: 1.0, leeway: .milliseconds(50))
|
||||||
|
timer.setEventHandler {
|
||||||
|
guard !isAlertDismissed else {
|
||||||
|
timer.cancel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
remainingSeconds -= 1
|
||||||
|
|
||||||
|
if remainingSeconds <= 0 {
|
||||||
|
isCountingDown = false
|
||||||
|
isAlertDismissed = true
|
||||||
|
timer.cancel()
|
||||||
|
|
||||||
|
if autoCompletion {
|
||||||
|
alertController.message = originalMessage
|
||||||
|
applyLeftAlignment(title: title, message: originalMessage)
|
||||||
|
autoCompletion = false
|
||||||
|
completion?()
|
||||||
|
AlertControllerUtil.completion = nil
|
||||||
|
alertController.dismiss(animated: false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if PublicUtils.isRunningOnMacAsiPadApp && buttonTitle != "" {
|
||||||
|
AlertControllerUtil.completion = nil
|
||||||
|
alertController.dismiss(animated: false) {
|
||||||
|
showAlert(
|
||||||
|
in: viewController,
|
||||||
|
title: title,
|
||||||
|
message: originalMessage,
|
||||||
|
withCancel: withCancel,
|
||||||
|
buttonTitle: buttonTitle,
|
||||||
|
countdown: 0,
|
||||||
|
action: nil,
|
||||||
|
completion: completion
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmAction.isEnabled = true
|
||||||
|
alertController.message = originalMessage
|
||||||
|
applyLeftAlignment(title: title, message: originalMessage)
|
||||||
|
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
ControllerNavigator.setUINavigationDelegate(alertController)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !autoCompletion {
|
||||||
|
let countdownMessage = makeCountdownMessage(baseMessage: originalMessage, remainingSeconds: remainingSeconds)
|
||||||
|
alertController.message = countdownMessage
|
||||||
|
applyLeftAlignment(title: title, message: countdownMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
timer.resume()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -60,6 +60,8 @@ static NSString* DB_NAME = @"Limelight_iOS.sqlite";
|
|||||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
|
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
|
||||||
// Override point for command tool customization after application launch (works only when user default is nil)
|
// Override point for command tool customization after application launch (works only when user default is nil)
|
||||||
[CommandManager presetDefaultCommands];
|
[CommandManager presetDefaultCommands];
|
||||||
|
[GenericUtils installSegmentedControlPreviousSelectionTracking];
|
||||||
|
[IAPManager shared];
|
||||||
|
|
||||||
// For iOS 12 and below, we need to manually create the window
|
// For iOS 12 and below, we need to manually create the window
|
||||||
if (@available(iOS 13.0, *)) {
|
if (@available(iOS 13.0, *)) {
|
||||||
|
|||||||
@@ -16,15 +16,17 @@
|
|||||||
|
|
||||||
@interface DataManager : NSObject
|
@interface DataManager : NSObject
|
||||||
|
|
||||||
typedef NS_ENUM(NSUInteger, UINavigationBarHeight) {
|
typedef NS_ENUM(NSInteger, ControllerGyroSwitchMode) {
|
||||||
UINavigationBarHeightIPad = 50,
|
ControllerGyroSwitchDisabled,
|
||||||
UINavigationBarHeightIPhone = 44
|
ControllerGyroSwitchPressToToggle,
|
||||||
|
ControllerGyroSwitchHoldDown
|
||||||
};
|
};
|
||||||
|
|
||||||
typedef NS_ENUM(NSInteger, TouchMode) {
|
typedef NS_ENUM(NSInteger, TouchMode) {
|
||||||
RelativeTouch,
|
RelativeTouch,
|
||||||
NativeTouch,
|
NativeTouch,
|
||||||
AbsoluteTouch,
|
AbsoluteTouch,
|
||||||
|
TouchDisabled,
|
||||||
NativeTouchOnly
|
NativeTouchOnly
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -42,9 +44,23 @@ typedef NS_ENUM(NSInteger, GyroMode) {
|
|||||||
AlwaysController
|
AlwaysController
|
||||||
};
|
};
|
||||||
|
|
||||||
|
typedef NS_ENUM(NSInteger, HapticEnginePreference) {
|
||||||
|
HapticEngineAuto,
|
||||||
|
RumbleDevice,
|
||||||
|
LeftRightSwapped,
|
||||||
|
RumbleOff
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef NS_ENUM(NSInteger, ControllerMouseStick) {
|
||||||
|
LeftStickToMouse,
|
||||||
|
RightStickToMouse,
|
||||||
|
};
|
||||||
|
|
||||||
typedef NS_ENUM(NSInteger, FramePacingMode) {
|
typedef NS_ENUM(NSInteger, FramePacingMode) {
|
||||||
|
FramePacingModeOff,
|
||||||
FramePacingModeLegacy,
|
FramePacingModeLegacy,
|
||||||
FramePacingModeQueue
|
FramePacingModeQueue,
|
||||||
|
FramePacingModeInterpolation
|
||||||
};
|
};
|
||||||
|
|
||||||
typedef NS_ENUM(NSInteger, SettingsMenuMode) {
|
typedef NS_ENUM(NSInteger, SettingsMenuMode) {
|
||||||
@@ -53,7 +69,18 @@ typedef NS_ENUM(NSInteger, SettingsMenuMode) {
|
|||||||
RemoveSettingItem,
|
RemoveSettingItem,
|
||||||
};
|
};
|
||||||
|
|
||||||
- (void) saveSettingsWithBitrate:(NSInteger)bitrate
|
typedef NS_ENUM(NSInteger, WidgetSizeTransition) {
|
||||||
|
keepWidgetSize,
|
||||||
|
transitionWithOrientation,
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef NS_ENUM(NSInteger, PencilTickMode) {
|
||||||
|
PencilTickDisabled,
|
||||||
|
ManualTick
|
||||||
|
};
|
||||||
|
|
||||||
|
- (void) saveSettings:(Settings*)settings
|
||||||
|
withBitrate:(NSInteger)bitrate
|
||||||
framerate:(NSInteger)framerate
|
framerate:(NSInteger)framerate
|
||||||
height:(NSInteger)height
|
height:(NSInteger)height
|
||||||
width:(NSInteger)width
|
width:(NSInteger)width
|
||||||
@@ -69,25 +96,32 @@ typedef NS_ENUM(NSInteger, SettingsMenuMode) {
|
|||||||
touchPointerVelocityFactor:(CGFloat)touchPointerVelocityFactor
|
touchPointerVelocityFactor:(CGFloat)touchPointerVelocityFactor
|
||||||
mousePointerVelocityFactor:(CGFloat)mousePointerVelocityFactor
|
mousePointerVelocityFactor:(CGFloat)mousePointerVelocityFactor
|
||||||
gyroSensitivity:(CGFloat)gyroSensitivity
|
gyroSensitivity:(CGFloat)gyroSensitivity
|
||||||
|
localVolume:(CGFloat)localVolume
|
||||||
|
micVolume:(CGFloat)micVolume
|
||||||
touchMoveEventInterval:(NSInteger)touchMoveEventInterval
|
touchMoveEventInterval:(NSInteger)touchMoveEventInterval
|
||||||
reverseMouseWheelDirection:(BOOL)reverseMouseWheelDirection
|
reverseMouseWheelDirection:(BOOL)reverseMouseWheelDirection
|
||||||
asyncNativeTouchPriority:(NSInteger)asyncNativeTouchPriority
|
asyncNativeTouchPriority:(NSInteger)asyncNativeTouchPriority
|
||||||
liftStreamViewForKeyboard:(BOOL)liftStreamViewForKeyboard
|
liftStreamViewForKeyboard:(BOOL)liftStreamViewForKeyboard
|
||||||
showKeyboardToolbar:(BOOL)showKeyboardToolbar
|
showKeyboardToolbar:(BOOL)showKeyboardToolbar
|
||||||
optimizeGames:(BOOL)optimizeGames
|
optimizeGames:(BOOL)optimizeGames
|
||||||
multiController:(BOOL)multiController
|
multiController:(BOOL)multiController
|
||||||
|
buttonVisualFeedback:(BOOL)buttonVisualFeedback
|
||||||
|
touchPointTracking:(BOOL)touchPointTracking
|
||||||
swapABXYButtons:(BOOL)swapABXYButtons
|
swapABXYButtons:(BOOL)swapABXYButtons
|
||||||
audioOnPC:(BOOL)audioOnPC
|
audioOnPC:(BOOL)audioOnPC
|
||||||
|
redirectMic:(BOOL)redirectMic
|
||||||
|
useBuiltinMic:(BOOL)useBuiltinMic
|
||||||
preferredCodec:(uint32_t)preferredCodec
|
preferredCodec:(uint32_t)preferredCodec
|
||||||
enableYUV444:(BOOL)enableYUV444
|
enableYUV444:(BOOL)enableYUV444
|
||||||
enablePIP:(BOOL)enablePIP
|
enablePIP:(BOOL)enablePIP
|
||||||
|
fullColorRange:(BOOL)fullColorRange
|
||||||
enableHdr:(BOOL)enableHdr
|
enableHdr:(BOOL)enableHdr
|
||||||
btMouseSupport:(BOOL)btMouseSupport
|
btMouseSupport:(BOOL)btMouseSupport
|
||||||
// absoluteTouchMode:(BOOL)absoluteTouchMode
|
// absoluteTouchMode:(BOOL)absoluteTouchMode
|
||||||
touchMode:(NSInteger)touchMode
|
touchMode:(NSInteger)touchMode
|
||||||
statsOverlayLevel:(NSInteger)statsOverlayLevel
|
statsOverlayLevel:(NSInteger)statsOverlayLevel
|
||||||
statsOverlayEnabled:(BOOL)statsOverlayEnabled
|
statsOverlayEnabled:(BOOL)statsOverlayEnabled
|
||||||
unlockDisplayOrientation:(BOOL)unlockDisplayOrientation
|
unlockDisplayOrientation:(BOOL)unlockDisplayOrientation
|
||||||
resolutionSelected:(NSInteger)resolutionSelected
|
resolutionSelected:(NSInteger)resolutionSelected
|
||||||
externalDisplayMode:(NSInteger)externalDisplayMode
|
externalDisplayMode:(NSInteger)externalDisplayMode
|
||||||
localMousePointerMode:(NSInteger)localMousePointerMode
|
localMousePointerMode:(NSInteger)localMousePointerMode
|
||||||
@@ -95,8 +129,38 @@ typedef NS_ENUM(NSInteger, SettingsMenuMode) {
|
|||||||
enableGraphs:(BOOL)enableGraphs
|
enableGraphs:(BOOL)enableGraphs
|
||||||
graphOpacity:(NSInteger)graphOpacity
|
graphOpacity:(NSInteger)graphOpacity
|
||||||
renderingBackend:(NSInteger)renderingBackend
|
renderingBackend:(NSInteger)renderingBackend
|
||||||
framePacingMode:(NSInteger)framePacingMode
|
framePacingMode:(NSInteger)framePacingMode
|
||||||
backgroundSessionTimer:(NSInteger)backgroundSessionTimer;
|
interpolationMaximumDimension:(NSInteger)interpolationMaximumDimension
|
||||||
|
interpolationMaximumPixelCount:(NSInteger)interpolationMaximumPixelCount
|
||||||
|
streamDimensionScale:(CGFloat)streamDimensionScale
|
||||||
|
sendDummyEvent:(BOOL)sendDummyEvent
|
||||||
|
rememberFoldState:(BOOL)rememberFoldState
|
||||||
|
singleTapSensitivy:(CGFloat)singleTapSensitivy
|
||||||
|
hapticEngine:(NSInteger)hapticEngine
|
||||||
|
edgeSlidingSensitivity:(CGFloat)edgeSlidingSensitivity
|
||||||
|
audioEngine:(NSInteger)audioEngine
|
||||||
|
delayLeftClick:(BOOL)delayLeftClick
|
||||||
|
duckOtherApps:(BOOL)duckOtherApps
|
||||||
|
muteInBackground:(BOOL)muteInBackground
|
||||||
|
relativeTouchSlideThreshold:(CGFloat)relativeTouchSlideThreshold
|
||||||
|
enablePinch:(BOOL)enablePinch
|
||||||
|
scrollSensitivity:(CGFloat)scrollSensitivity
|
||||||
|
pinchSensitivity:(CGFloat)pinchSensitivity
|
||||||
|
ctrlDownForPinch:(BOOL)ctrlDownForPinch
|
||||||
|
leftClickDelayMs:(CGFloat)leftClickDelayMs
|
||||||
|
settingsMenuOffset:(CGFloat)settingsMenuOffset
|
||||||
|
passthroughGestures:(BOOL)passthroughGestures
|
||||||
|
enableControllerNavigation:(BOOL)mapControllerToMouse
|
||||||
|
controllerMousePointerVelocity:(CGFloat)controllerMousePointerVelocity
|
||||||
|
controllerMouseExpo:(CGFloat)controllerMouseExpo
|
||||||
|
controllerGyroSwitchMode:(NSInteger)controllerGyroSwitchMode
|
||||||
|
enableFrameTimebase:(BOOL)enableFrameTimebase
|
||||||
|
asyncFrameDequeue:(BOOL)asyncFrameDequeue
|
||||||
|
sdrPerformanceWorkaround:(BOOL)sdrPerformanceWorkaround
|
||||||
|
softKeyboardHeight:(CGFloat)softKeyboardHeight
|
||||||
|
globeAsEscape:(BOOL)globeAsEscape
|
||||||
|
streamingRadialMenuDelay:(CGFloat)streamingRadialMenuDelay
|
||||||
|
backgroundSessionTimer:(NSInteger)backgroundSessionTimer;
|
||||||
|
|
||||||
- (NSArray*) getHosts;
|
- (NSArray*) getHosts;
|
||||||
- (void) updateHost:(TemporaryHost*)host;
|
- (void) updateHost:(TemporaryHost*)host;
|
||||||
|
|||||||
@@ -55,7 +55,8 @@
|
|||||||
return uid;
|
return uid;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) saveSettingsWithBitrate:(NSInteger)bitrate
|
- (void) saveSettings:(Settings*)settings
|
||||||
|
withBitrate:(NSInteger)bitrate
|
||||||
framerate:(NSInteger)framerate
|
framerate:(NSInteger)framerate
|
||||||
height:(NSInteger)height
|
height:(NSInteger)height
|
||||||
width:(NSInteger)width
|
width:(NSInteger)width
|
||||||
@@ -71,25 +72,32 @@
|
|||||||
touchPointerVelocityFactor:(CGFloat)touchPointerVelocityFactor
|
touchPointerVelocityFactor:(CGFloat)touchPointerVelocityFactor
|
||||||
mousePointerVelocityFactor:(CGFloat)mousePointerVelocityFactor
|
mousePointerVelocityFactor:(CGFloat)mousePointerVelocityFactor
|
||||||
gyroSensitivity:(CGFloat)gyroSensitivity
|
gyroSensitivity:(CGFloat)gyroSensitivity
|
||||||
|
localVolume:(CGFloat)localVolume
|
||||||
|
micVolume:(CGFloat)micVolume
|
||||||
touchMoveEventInterval:(NSInteger)touchMoveEventInterval
|
touchMoveEventInterval:(NSInteger)touchMoveEventInterval
|
||||||
reverseMouseWheelDirection:(BOOL)reverseMouseWheelDirection
|
reverseMouseWheelDirection:(BOOL)reverseMouseWheelDirection
|
||||||
asyncNativeTouchPriority:(NSInteger)asyncNativeTouchPriority
|
asyncNativeTouchPriority:(NSInteger)asyncNativeTouchPriority
|
||||||
liftStreamViewForKeyboard:(BOOL)liftStreamViewForKeyboard
|
liftStreamViewForKeyboard:(BOOL)liftStreamViewForKeyboard
|
||||||
showKeyboardToolbar:(BOOL)showKeyboardToolbar
|
showKeyboardToolbar:(BOOL)showKeyboardToolbar
|
||||||
optimizeGames:(BOOL)optimizeGames
|
optimizeGames:(BOOL)optimizeGames
|
||||||
multiController:(BOOL)multiController
|
multiController:(BOOL)multiController
|
||||||
|
buttonVisualFeedback:(BOOL)buttonVisualFeedback
|
||||||
|
touchPointTracking:(BOOL)touchPointTracking
|
||||||
swapABXYButtons:(BOOL)swapABXYButtons
|
swapABXYButtons:(BOOL)swapABXYButtons
|
||||||
audioOnPC:(BOOL)audioOnPC
|
audioOnPC:(BOOL)audioOnPC
|
||||||
|
redirectMic:(BOOL)redirectMic
|
||||||
|
useBuiltinMic:(BOOL)useBuiltinMic
|
||||||
preferredCodec:(uint32_t)preferredCodec
|
preferredCodec:(uint32_t)preferredCodec
|
||||||
enableYUV444:(BOOL)enableYUV444
|
enableYUV444:(BOOL)enableYUV444
|
||||||
enablePIP:(BOOL)enablePIP
|
enablePIP:(BOOL)enablePIP
|
||||||
|
fullColorRange:(BOOL)fullColorRange
|
||||||
enableHdr:(BOOL)enableHdr
|
enableHdr:(BOOL)enableHdr
|
||||||
btMouseSupport:(BOOL)btMouseSupport
|
btMouseSupport:(BOOL)btMouseSupport
|
||||||
// absoluteTouchMode:(BOOL)absoluteTouchMode
|
// absoluteTouchMode:(BOOL)absoluteTouchMode
|
||||||
touchMode:(NSInteger)touchMode
|
touchMode:(NSInteger)touchMode
|
||||||
statsOverlayLevel:(NSInteger)statsOverlayLevel
|
statsOverlayLevel:(NSInteger)statsOverlayLevel
|
||||||
statsOverlayEnabled:(BOOL)statsOverlayEnabled
|
statsOverlayEnabled:(BOOL)statsOverlayEnabled
|
||||||
unlockDisplayOrientation:(BOOL)unlockDisplayOrientation
|
unlockDisplayOrientation:(BOOL)unlockDisplayOrientation
|
||||||
resolutionSelected:(NSInteger)resolutionSelected
|
resolutionSelected:(NSInteger)resolutionSelected
|
||||||
externalDisplayMode:(NSInteger)externalDisplayMode
|
externalDisplayMode:(NSInteger)externalDisplayMode
|
||||||
localMousePointerMode:(NSInteger)localMousePointerMode
|
localMousePointerMode:(NSInteger)localMousePointerMode
|
||||||
@@ -97,11 +105,42 @@
|
|||||||
enableGraphs:(BOOL)enableGraphs
|
enableGraphs:(BOOL)enableGraphs
|
||||||
graphOpacity:(NSInteger)graphOpacity
|
graphOpacity:(NSInteger)graphOpacity
|
||||||
renderingBackend:(NSInteger)renderingBackend
|
renderingBackend:(NSInteger)renderingBackend
|
||||||
framePacingMode:(NSInteger)framePacingMode
|
framePacingMode:(NSInteger)framePacingMode
|
||||||
|
interpolationMaximumDimension:(NSInteger)interpolationMaximumDimension
|
||||||
|
interpolationMaximumPixelCount:(NSInteger)interpolationMaximumPixelCount
|
||||||
|
streamDimensionScale:(CGFloat)streamDimensionScale
|
||||||
|
sendDummyEvent:(BOOL)sendDummyEvent
|
||||||
|
rememberFoldState:(BOOL)rememberFoldState
|
||||||
|
singleTapSensitivy:(CGFloat)singleTapSensitivy
|
||||||
|
hapticEngine:(NSInteger)hapticEngine
|
||||||
|
edgeSlidingSensitivity:(CGFloat)edgeSlidingSensitivity
|
||||||
|
audioEngine:(NSInteger)audioEngine
|
||||||
|
delayLeftClick:(BOOL)delayLeftClick
|
||||||
|
duckOtherApps:(BOOL)duckOtherApps
|
||||||
|
muteInBackground:(BOOL)muteInBackground
|
||||||
|
relativeTouchSlideThreshold:(CGFloat)relativeTouchSlideThreshold
|
||||||
|
enablePinch:(BOOL)enablePinch
|
||||||
|
scrollSensitivity:(CGFloat)scrollSensitivity
|
||||||
|
pinchSensitivity:(CGFloat)pinchSensitivity
|
||||||
|
ctrlDownForPinch:(BOOL)ctrlDownForPinch
|
||||||
|
leftClickDelayMs:(CGFloat)leftClickDelayMs
|
||||||
|
settingsMenuOffset:(CGFloat)settingsMenuOffset
|
||||||
|
passthroughGestures:(BOOL)passthroughGestures
|
||||||
|
enableControllerNavigation:(BOOL)enableControllerNavigation
|
||||||
|
controllerMousePointerVelocity:(CGFloat)controllerMousePointerVelocity
|
||||||
|
controllerMouseExpo:(CGFloat)controllerMouseExpo
|
||||||
|
controllerGyroSwitchMode:(NSInteger)controllerGyroSwitchMode
|
||||||
|
enableFrameTimebase:(BOOL)enableFrameTimebase
|
||||||
|
asyncFrameDequeue:(BOOL)asyncFrameDequeue
|
||||||
|
sdrPerformanceWorkaround:(BOOL)sdrPerformanceWorkaround
|
||||||
|
softKeyboardHeight:(CGFloat)softKeyboardHeight
|
||||||
|
globeAsEscape:(BOOL)globeAsEscape
|
||||||
|
streamingRadialMenuDelay:(CGFloat)streamingRadialMenuDelay
|
||||||
backgroundSessionTimer:(NSInteger)backgroundSessionTimer{
|
backgroundSessionTimer:(NSInteger)backgroundSessionTimer{
|
||||||
|
|
||||||
|
__block Settings* settingsToSave = settings;
|
||||||
[_managedObjectContext performBlockAndWait:^{
|
[_managedObjectContext performBlockAndWait:^{
|
||||||
Settings* settingsToSave = [self retrieveSettings];
|
if(!settingsToSave) settingsToSave = [self retrieveSettings];
|
||||||
settingsToSave.framerate = [NSNumber numberWithInteger:framerate];
|
settingsToSave.framerate = [NSNumber numberWithInteger:framerate];
|
||||||
settingsToSave.bitrate = [NSNumber numberWithInteger:bitrate];
|
settingsToSave.bitrate = [NSNumber numberWithInteger:bitrate];
|
||||||
settingsToSave.height = [NSNumber numberWithInteger:height];
|
settingsToSave.height = [NSNumber numberWithInteger:height];
|
||||||
@@ -118,6 +157,8 @@
|
|||||||
settingsToSave.touchPointerVelocityFactor = [NSNumber numberWithFloat:touchPointerVelocityFactor];
|
settingsToSave.touchPointerVelocityFactor = [NSNumber numberWithFloat:touchPointerVelocityFactor];
|
||||||
settingsToSave.mousePointerVelocityFactor = [NSNumber numberWithFloat:mousePointerVelocityFactor];
|
settingsToSave.mousePointerVelocityFactor = [NSNumber numberWithFloat:mousePointerVelocityFactor];
|
||||||
settingsToSave.gyroSensitivity = [NSNumber numberWithFloat:gyroSensitivity];
|
settingsToSave.gyroSensitivity = [NSNumber numberWithFloat:gyroSensitivity];
|
||||||
|
settingsToSave.localVolume = [NSNumber numberWithFloat:localVolume];
|
||||||
|
settingsToSave.micVolume = [NSNumber numberWithFloat:micVolume];
|
||||||
settingsToSave.touchMoveEventInterval = [NSNumber numberWithInteger:touchMoveEventInterval];
|
settingsToSave.touchMoveEventInterval = [NSNumber numberWithInteger:touchMoveEventInterval];
|
||||||
settingsToSave.reverseMouseWheelDirection = reverseMouseWheelDirection;
|
settingsToSave.reverseMouseWheelDirection = reverseMouseWheelDirection;
|
||||||
settingsToSave.asyncNativeTouchPriority = [NSNumber numberWithInteger:asyncNativeTouchPriority];
|
settingsToSave.asyncNativeTouchPriority = [NSNumber numberWithInteger:asyncNativeTouchPriority];
|
||||||
@@ -125,11 +166,17 @@
|
|||||||
settingsToSave.showKeyboardToolbar = showKeyboardToolbar;
|
settingsToSave.showKeyboardToolbar = showKeyboardToolbar;
|
||||||
settingsToSave.optimizeGames = optimizeGames;
|
settingsToSave.optimizeGames = optimizeGames;
|
||||||
settingsToSave.multiController = multiController;
|
settingsToSave.multiController = multiController;
|
||||||
|
settingsToSave.buttonVisualFeedback = buttonVisualFeedback;
|
||||||
|
settingsToSave.touchPointTracking = touchPointTracking;
|
||||||
settingsToSave.swapABXYButtons = swapABXYButtons;
|
settingsToSave.swapABXYButtons = swapABXYButtons;
|
||||||
settingsToSave.playAudioOnPC = audioOnPC;
|
settingsToSave.playAudioOnPC = audioOnPC;
|
||||||
|
settingsToSave.redirectMic = redirectMic;
|
||||||
|
settingsToSave.useBuiltinMic = useBuiltinMic;
|
||||||
settingsToSave.preferredCodec = preferredCodec;
|
settingsToSave.preferredCodec = preferredCodec;
|
||||||
settingsToSave.enableYUV444 = enableYUV444;
|
settingsToSave.enableYUV444 = enableYUV444;
|
||||||
|
settingsToSave.sdrPerformanceWorkaround = sdrPerformanceWorkaround;
|
||||||
settingsToSave.enablePIP = enablePIP;
|
settingsToSave.enablePIP = enablePIP;
|
||||||
|
settingsToSave.fullColorRange = fullColorRange;
|
||||||
settingsToSave.enableHdr = enableHdr;
|
settingsToSave.enableHdr = enableHdr;
|
||||||
settingsToSave.btMouseSupport = btMouseSupport;
|
settingsToSave.btMouseSupport = btMouseSupport;
|
||||||
// settingsToSave.absoluteTouchMode = absoluteTouchMode;
|
// settingsToSave.absoluteTouchMode = absoluteTouchMode;
|
||||||
@@ -143,10 +190,38 @@
|
|||||||
settingsToSave.backroundSessionTimer = [NSNumber numberWithInteger:backgroundSessionTimer];
|
settingsToSave.backroundSessionTimer = [NSNumber numberWithInteger:backgroundSessionTimer];
|
||||||
|
|
||||||
settingsToSave.frameQueueSize = [NSNumber numberWithInteger:frameQueueSize];
|
settingsToSave.frameQueueSize = [NSNumber numberWithInteger:frameQueueSize];
|
||||||
|
settingsToSave.enableFrameTimebase = enableFrameTimebase;
|
||||||
|
settingsToSave.asyncFrameDequeue = asyncFrameDequeue;
|
||||||
settingsToSave.enableGraphs = enableGraphs;
|
settingsToSave.enableGraphs = enableGraphs;
|
||||||
settingsToSave.graphOpacity = [NSNumber numberWithInteger:graphOpacity];
|
settingsToSave.graphOpacity = [NSNumber numberWithInteger:graphOpacity];
|
||||||
settingsToSave.renderingBackend = [NSNumber numberWithInteger:renderingBackend];
|
settingsToSave.renderingBackend = [NSNumber numberWithInteger:renderingBackend];
|
||||||
settingsToSave.framePacingMode = [NSNumber numberWithInteger:framePacingMode];
|
settingsToSave.framePacingMode = [NSNumber numberWithInteger:framePacingMode];
|
||||||
|
settingsToSave.interpolationMaximumDimension = [NSNumber numberWithInteger:interpolationMaximumDimension];
|
||||||
|
settingsToSave.interpolationMaximumPixelCount = [NSNumber numberWithInteger:interpolationMaximumPixelCount];
|
||||||
|
settingsToSave.streamDimensionScale = [NSNumber numberWithDouble:streamDimensionScale];
|
||||||
|
settingsToSave.sendDummyEvent = sendDummyEvent;
|
||||||
|
settingsToSave.singleTapSensitivity = [NSNumber numberWithDouble:singleTapSensitivy];
|
||||||
|
settingsToSave.hapticEngine = [NSNumber numberWithInteger:hapticEngine];
|
||||||
|
settingsToSave.edgeSlidingSensitivity = [NSNumber numberWithFloat:edgeSlidingSensitivity];
|
||||||
|
settingsToSave.audioEngine = [NSNumber numberWithInteger:audioEngine];
|
||||||
|
settingsToSave.delayLeftClick = delayLeftClick;
|
||||||
|
settingsToSave.duckOtherApps = duckOtherApps;
|
||||||
|
settingsToSave.muteInBackground = muteInBackground;
|
||||||
|
settingsToSave.relativeTouchSlideThreshold = [NSNumber numberWithFloat:relativeTouchSlideThreshold];
|
||||||
|
settingsToSave.enablePinch = enablePinch;
|
||||||
|
settingsToSave.scrollSensitivity = [NSNumber numberWithFloat:scrollSensitivity];
|
||||||
|
settingsToSave.pinchSensitivity = [NSNumber numberWithFloat:pinchSensitivity];
|
||||||
|
settingsToSave.ctrlDownForPinch = ctrlDownForPinch;
|
||||||
|
settingsToSave.leftClickDelayMs = [NSNumber numberWithFloat:leftClickDelayMs];
|
||||||
|
settingsToSave.settingsMenuOffset = [NSNumber numberWithFloat:settingsMenuOffset];
|
||||||
|
settingsToSave.passthroughGestures = passthroughGestures;
|
||||||
|
settingsToSave.enableControllerNavigation = enableControllerNavigation;
|
||||||
|
settingsToSave.controllerMousePointerVelocity = [NSNumber numberWithFloat:controllerMousePointerVelocity];
|
||||||
|
settingsToSave.controllerMouseExpo = [NSNumber numberWithFloat:controllerMouseExpo];
|
||||||
|
settingsToSave.softKeyboardHeight = softKeyboardHeight;
|
||||||
|
settingsToSave.globeAsEscape = globeAsEscape;
|
||||||
|
settingsToSave.streamingRadialMenuDelay = @(streamingRadialMenuDelay);
|
||||||
|
settingsToSave.rememberFoldState = rememberFoldState;
|
||||||
[self saveData];
|
[self saveData];
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
@@ -301,4 +376,3 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
@property (atomic, nullable, retain) NSString *localAddress;
|
@property (atomic, nullable, retain) NSString *localAddress;
|
||||||
@property (atomic, nullable, retain) NSString *ipv6Address;
|
@property (atomic, nullable, retain) NSString *ipv6Address;
|
||||||
@property (atomic, nullable, retain) NSString *mac;
|
@property (atomic, nullable, retain) NSString *mac;
|
||||||
|
@property (atomic, nullable, retain) NSString *controllerNavigationHighlightedAppID;
|
||||||
@property (atomic) int serverCodecModeSupport;
|
@property (atomic) int serverCodecModeSupport;
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
@@ -31,6 +32,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
@property (atomic, retain) NSString *name;
|
@property (atomic, retain) NSString *name;
|
||||||
@property (atomic, retain) NSString *uuid;
|
@property (atomic, retain) NSString *uuid;
|
||||||
@property (atomic, retain) NSSet *appList;
|
@property (atomic, retain) NSSet *appList;
|
||||||
|
@property (atomic, retain) NSMutableSet * activeAddressPool;
|
||||||
|
|
||||||
- (id) initFromHost:(Host*)host;
|
- (id) initFromHost:(Host*)host;
|
||||||
|
|
||||||
|
|||||||
@@ -28,12 +28,24 @@
|
|||||||
self = [self init];
|
self = [self init];
|
||||||
|
|
||||||
self.address = host.address;
|
self.address = host.address;
|
||||||
|
|
||||||
|
NSSet *classes = [NSSet setWithObjects: [NSMutableSet class], [NSString class], nil];
|
||||||
|
NSError *error = nil;
|
||||||
|
self.activeAddressPool = [NSKeyedUnarchiver unarchivedObjectOfClasses:classes fromData:host.activeAddressPool error:&error];
|
||||||
|
if(self.activeAddressPool){
|
||||||
|
for(NSString* address in _activeAddressPool){
|
||||||
|
NSLog(@"%@, persisted addr: %@", host.name, address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else self.activeAddressPool = [[NSMutableSet alloc] init];
|
||||||
|
|
||||||
self.externalAddress = host.externalAddress;
|
self.externalAddress = host.externalAddress;
|
||||||
self.localAddress = host.localAddress;
|
self.localAddress = host.localAddress;
|
||||||
self.ipv6Address = host.ipv6Address;
|
self.ipv6Address = host.ipv6Address;
|
||||||
if(!(host.mac == nil || [host.mac isEqualToString:@"00:00:00:00:00:00"])) self.mac = host.mac; // try to fix invalid mac happens in some cases
|
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.name = host.name;
|
||||||
self.uuid = host.uuid;
|
self.uuid = host.uuid;
|
||||||
|
self.controllerNavigationHighlightedAppID = host.controllerNavigationHighlightedAppID;
|
||||||
self.serverCodecModeSupport = host.serverCodecModeSupport;
|
self.serverCodecModeSupport = host.serverCodecModeSupport;
|
||||||
self.serverCert = host.serverCert;
|
self.serverCert = host.serverCert;
|
||||||
|
|
||||||
@@ -57,7 +69,7 @@
|
|||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) propagateChangesToParent:(Host*)parentHost {
|
- (void) propagateChangesToParent:(Host*)parentHost { // update persisted host data
|
||||||
// Avoid overwriting existing data with nil if
|
// Avoid overwriting existing data with nil if
|
||||||
// we don't have everything populated in the temporary
|
// we don't have everything populated in the temporary
|
||||||
// host.
|
// host.
|
||||||
@@ -73,6 +85,15 @@
|
|||||||
if (self.ipv6Address != nil) {
|
if (self.ipv6Address != nil) {
|
||||||
parentHost.ipv6Address = self.ipv6Address;
|
parentHost.ipv6Address = self.ipv6Address;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(self.activeAddressPool.count>0){
|
||||||
|
NSError *error;
|
||||||
|
parentHost.activeAddressPool = [NSKeyedArchiver archivedDataWithRootObject:self.activeAddressPool requiringSecureCoding:YES error:&error];
|
||||||
|
}
|
||||||
|
else if(self.activeAddress) [self.activeAddressPool addObject:self.activeAddress];
|
||||||
|
|
||||||
|
// NSLog(@"Persisting activeAddressPool, pool count %lu ... %f, host: %@", (unsigned long)self.activeAddressPool.count, CACurrentMediaTime(), self.name);
|
||||||
|
|
||||||
// try to fix invalid mac happens in some cases
|
// try to fix invalid mac happens in some cases
|
||||||
// 添加主机crash问题重点关注
|
// 添加主机crash问题重点关注
|
||||||
if (!(self.mac == nil || [self.mac isEqualToString:@"00:00:00:00:00:00"])) parentHost.mac = self.mac;
|
if (!(self.mac == nil || [self.mac isEqualToString:@"00:00:00:00:00:00"])) parentHost.mac = self.mac;
|
||||||
@@ -81,6 +102,7 @@
|
|||||||
}
|
}
|
||||||
parentHost.name = self.name;
|
parentHost.name = self.name;
|
||||||
parentHost.uuid = self.uuid;
|
parentHost.uuid = self.uuid;
|
||||||
|
parentHost.controllerNavigationHighlightedAppID = self.controllerNavigationHighlightedAppID;
|
||||||
parentHost.serverCodecModeSupport = self.serverCodecModeSupport;
|
parentHost.serverCodecModeSupport = self.serverCodecModeSupport;
|
||||||
parentHost.pairState = [NSNumber numberWithInt:self.pairState];
|
parentHost.pairState = [NSNumber numberWithInt:self.pairState];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,8 @@
|
|||||||
@property (nonatomic, retain) NSNumber * touchMoveEventInterval;
|
@property (nonatomic, retain) NSNumber * touchMoveEventInterval;
|
||||||
@property (nonatomic, retain) NSNumber * touchPointerVelocityFactor;
|
@property (nonatomic, retain) NSNumber * touchPointerVelocityFactor;
|
||||||
@property (nonatomic, retain) NSNumber * gyroSensitivity;
|
@property (nonatomic, retain) NSNumber * gyroSensitivity;
|
||||||
|
@property (nonatomic, retain) NSNumber * localVolume;
|
||||||
|
@property (nonatomic, retain) NSNumber * micVolume;
|
||||||
@property (nonatomic, retain) NSNumber * emulatedControllerType;
|
@property (nonatomic, retain) NSNumber * emulatedControllerType;
|
||||||
@property (nonatomic, retain) NSNumber * mousePointerVelocityFactor;
|
@property (nonatomic, retain) NSNumber * mousePointerVelocityFactor;
|
||||||
@property (nonatomic, retain) NSNumber * pointerVelocityModeDivider;
|
@property (nonatomic, retain) NSNumber * pointerVelocityModeDivider;
|
||||||
@@ -40,6 +42,12 @@
|
|||||||
@property (nonatomic, retain) NSNumber * resolutionSelected;
|
@property (nonatomic, retain) NSNumber * resolutionSelected;
|
||||||
@property (nonatomic, retain) NSNumber * externalDisplayMode;
|
@property (nonatomic, retain) NSNumber * externalDisplayMode;
|
||||||
@property (nonatomic, retain) NSNumber * localMousePointerMode;
|
@property (nonatomic, retain) NSNumber * localMousePointerMode;
|
||||||
|
@property (nonatomic, retain) NSNumber * gyroBiasX;
|
||||||
|
@property (nonatomic, retain) NSNumber * gyroBiasY;
|
||||||
|
@property (nonatomic, retain) NSNumber * gyroBiasZ;
|
||||||
|
@property (nonatomic, retain) NSNumber * controllerGyroBiasX;
|
||||||
|
@property (nonatomic, retain) NSNumber * controllerGyroBiasY;
|
||||||
|
@property (nonatomic, retain) NSNumber * controllerGyroBiasZ;
|
||||||
@property (nonatomic, retain) NSNumber * backgroundSessionTimer;
|
@property (nonatomic, retain) NSNumber * backgroundSessionTimer;
|
||||||
@property (nonatomic) enum {
|
@property (nonatomic) enum {
|
||||||
CODEC_PREF_AUTO,
|
CODEC_PREF_AUTO,
|
||||||
@@ -48,12 +56,18 @@
|
|||||||
CODEC_PREF_AV1,
|
CODEC_PREF_AV1,
|
||||||
} preferredCodec;
|
} preferredCodec;
|
||||||
@property (nonatomic) BOOL enableYUV444;
|
@property (nonatomic) BOOL enableYUV444;
|
||||||
|
@property (nonatomic) BOOL sdrPerformanceWorkaround;
|
||||||
@property (nonatomic) BOOL enablePIP;
|
@property (nonatomic) BOOL enablePIP;
|
||||||
|
@property (nonatomic) BOOL fullColorRange;
|
||||||
@property (nonatomic) BOOL reverseMouseWheelDirection;
|
@property (nonatomic) BOOL reverseMouseWheelDirection;
|
||||||
@property (nonatomic, retain) NSNumber * asyncNativeTouchPriority;
|
@property (nonatomic, retain) NSNumber * asyncNativeTouchPriority;
|
||||||
@property (nonatomic) BOOL multiController;
|
@property (nonatomic) BOOL multiController;
|
||||||
|
@property (nonatomic) BOOL buttonVisualFeedback;
|
||||||
|
@property (nonatomic) BOOL touchPointTracking;
|
||||||
@property (nonatomic) BOOL swapABXYButtons;
|
@property (nonatomic) BOOL swapABXYButtons;
|
||||||
@property (nonatomic) BOOL playAudioOnPC;
|
@property (nonatomic) BOOL playAudioOnPC;
|
||||||
|
@property (nonatomic) BOOL redirectMic;
|
||||||
|
@property (nonatomic) BOOL useBuiltinMic;
|
||||||
@property (nonatomic) BOOL optimizeGames;
|
@property (nonatomic) BOOL optimizeGames;
|
||||||
@property (nonatomic) BOOL enableHdr;
|
@property (nonatomic) BOOL enableHdr;
|
||||||
@property (nonatomic) BOOL btMouseSupport;
|
@property (nonatomic) BOOL btMouseSupport;
|
||||||
@@ -63,12 +77,53 @@
|
|||||||
@property (nonatomic) BOOL statsOverlayEnabled;
|
@property (nonatomic) BOOL statsOverlayEnabled;
|
||||||
@property (nonatomic) BOOL liftStreamViewForKeyboard;
|
@property (nonatomic) BOOL liftStreamViewForKeyboard;
|
||||||
@property (nonatomic) BOOL showKeyboardToolbar;
|
@property (nonatomic) BOOL showKeyboardToolbar;
|
||||||
|
@property (nonatomic) CGFloat softKeyboardHeight;
|
||||||
@property (nonatomic) BOOL unlockDisplayOrientation;
|
@property (nonatomic) BOOL unlockDisplayOrientation;
|
||||||
@property (nonatomic) BOOL enableGraphs;
|
@property (nonatomic) BOOL enableGraphs;
|
||||||
|
@property (nonatomic) BOOL enableFrameTimebase;
|
||||||
|
@property (nonatomic) BOOL asyncFrameDequeue;
|
||||||
@property (nonatomic, retain) NSNumber * frameQueueSize;
|
@property (nonatomic, retain) NSNumber * frameQueueSize;
|
||||||
@property (nonatomic, retain) NSNumber * graphOpacity;
|
@property (nonatomic, retain) NSNumber * graphOpacity;
|
||||||
@property (nonatomic, retain) NSNumber * renderingBackend;
|
@property (nonatomic, retain) NSNumber * renderingBackend;
|
||||||
|
@property (nonatomic) BOOL sendDummyEvent;
|
||||||
|
@property (nonatomic) BOOL rememberFoldState;
|
||||||
|
@property (nonatomic, retain) NSNumber * singleTapSensitivity;
|
||||||
|
@property (nonatomic, retain) NSNumber * hapticEngine;
|
||||||
|
@property (nonatomic, retain) NSNumber * appTheme;
|
||||||
|
@property (nonatomic, retain) NSNumber * edgeSlidingSensitivity;
|
||||||
|
@property (nonatomic, retain) NSNumber * audioEngine;
|
||||||
|
@property (nonatomic) BOOL delayLeftClick;
|
||||||
|
@property (nonatomic) BOOL duckOtherApps;
|
||||||
|
@property (nonatomic) BOOL muteInBackground;
|
||||||
|
@property (nonatomic) BOOL enablePinch;
|
||||||
|
@property (nonatomic) BOOL ctrlDownForPinch;
|
||||||
|
@property (nonatomic) BOOL passthroughGestures;
|
||||||
@property (nonatomic, retain) NSNumber * framePacingMode;
|
@property (nonatomic, retain) NSNumber * framePacingMode;
|
||||||
|
@property (nonatomic, retain) NSNumber * interpolationMaximumDimension;
|
||||||
|
@property (nonatomic, retain) NSNumber * interpolationMaximumPixelCount;
|
||||||
|
@property (nonatomic, retain) NSNumber * streamDimensionScale;
|
||||||
|
@property (nonatomic, retain) NSNumber * scrollSensitivity;
|
||||||
|
@property (nonatomic, retain) NSNumber * pinchSensitivity;
|
||||||
|
@property (nonatomic, retain) NSNumber * relativeTouchSlideThreshold;
|
||||||
|
@property (nonatomic, retain) NSNumber * leftClickDelayMs;
|
||||||
|
@property (nonatomic, retain) NSNumber * settingsMenuOffset;
|
||||||
|
@property (nonatomic, retain) NSNumber * controllerMouseLeftButton;
|
||||||
|
@property (nonatomic, retain) NSNumber * controllerMouseRightButton;
|
||||||
|
@property (nonatomic, retain) NSNumber * controllerMouseStick;
|
||||||
|
@property (nonatomic, retain) NSNumber * localRadialMenuButton;
|
||||||
|
@property (nonatomic, retain) NSNumber * streamingRadialMenuButton;
|
||||||
|
@property (nonatomic) BOOL enableControllerNavigation;
|
||||||
|
@property (nonatomic, retain) NSNumber * controllerMousePointerVelocity;
|
||||||
|
@property (nonatomic, retain) NSNumber * controllerMouseExpo;
|
||||||
|
@property (nonatomic, retain) NSNumber * streamingRadialMenuDelay;
|
||||||
|
@property (nonatomic) BOOL globeAsEscape;
|
||||||
|
|
||||||
|
// Pencil settings:
|
||||||
|
@property (nonatomic, retain) NSNumber * pencilTickMode;
|
||||||
|
@property (nonatomic, retain) NSNumber * pencilTickIntervalUs;
|
||||||
|
@property (nonatomic, retain) NSNumber * pencilTipOffsetX;
|
||||||
|
@property (nonatomic, retain) NSNumber * pencilTipOffsetY;
|
||||||
|
|
||||||
|
|
||||||
- (id) initFromSettings:(Settings*)settings;
|
- (id) initFromSettings:(Settings*)settings;
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
self.preferredCodec = (typeof(self.preferredCodec))[[NSUserDefaults standardUserDefaults] integerForKey:@"preferredCodec"];
|
self.preferredCodec = (typeof(self.preferredCodec))[[NSUserDefaults standardUserDefaults] integerForKey:@"preferredCodec"];
|
||||||
self.enableYUV444 = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableYUV444"];
|
self.enableYUV444 = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableYUV444"];
|
||||||
self.enablePIP = [[NSUserDefaults standardUserDefaults] boolForKey:@"enablePIP"];
|
self.enablePIP = [[NSUserDefaults standardUserDefaults] boolForKey:@"enablePIP"];
|
||||||
|
self.fullRange = [[NSUserDefaults standardUserDefaults] boolForKey:@"fullRange"];
|
||||||
self.frameQueueSize = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"frameQueueSize"]];
|
self.frameQueueSize = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"frameQueueSize"]];
|
||||||
self.playAudioOnPC = [[NSUserDefaults standardUserDefaults] boolForKey:@"audioOnPC"];
|
self.playAudioOnPC = [[NSUserDefaults standardUserDefaults] boolForKey:@"audioOnPC"];
|
||||||
self.enableHdr = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableHdr"];
|
self.enableHdr = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableHdr"];
|
||||||
@@ -52,16 +53,11 @@
|
|||||||
self.statsOverlay = [[NSUserDefaults standardUserDefaults] boolForKey:@"statsOverlay"];
|
self.statsOverlay = [[NSUserDefaults standardUserDefaults] boolForKey:@"statsOverlay"];
|
||||||
self.enableGraphs = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableGraphs"];
|
self.enableGraphs = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableGraphs"];
|
||||||
self.graphOpacity = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"graphOpacity"]];
|
self.graphOpacity = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"graphOpacity"]];
|
||||||
// Set rendering backend based on iOS version and user preference
|
self.renderingBackend = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"renderingBackend"]];
|
||||||
NSInteger savedBackend = [[NSUserDefaults standardUserDefaults] integerForKey:@"renderingBackend"];
|
|
||||||
if (@available(iOS 17.0, *)) {
|
|
||||||
// iOS 17+ can use Performance mode (Metal renderer)
|
|
||||||
self.renderingBackend = [NSNumber numberWithInteger:savedBackend];
|
|
||||||
} else {
|
|
||||||
// iOS < 17 must use Balanced mode (AVSB renderer)
|
|
||||||
self.renderingBackend = [NSNumber numberWithInteger:RENDER_AVSB];
|
|
||||||
}
|
|
||||||
self.framePacingMode = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"framePacingMode"]];
|
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"];
|
NSInteger _screenSize = [[NSUserDefaults standardUserDefaults] integerForKey:@"streamResolution"];
|
||||||
switch (_screenSize) {
|
switch (_screenSize) {
|
||||||
@@ -95,12 +91,20 @@
|
|||||||
self.audioConfig = settings.audioConfig;
|
self.audioConfig = settings.audioConfig;
|
||||||
self.preferredCodec = settings.preferredCodec;
|
self.preferredCodec = settings.preferredCodec;
|
||||||
self.enableYUV444 = settings.enableYUV444;
|
self.enableYUV444 = settings.enableYUV444;
|
||||||
|
self.sdrPerformanceWorkaround = settings.sdrPerformanceWorkaround;
|
||||||
self.enablePIP = settings.enablePIP;
|
self.enablePIP = settings.enablePIP;
|
||||||
|
self.fullColorRange = settings.fullColorRange;
|
||||||
self.frameQueueSize = settings.frameQueueSize;
|
self.frameQueueSize = settings.frameQueueSize;
|
||||||
|
self.enableFrameTimebase = settings.enableFrameTimebase;
|
||||||
|
self.asyncFrameDequeue = settings.asyncFrameDequeue;
|
||||||
self.playAudioOnPC = settings.playAudioOnPC;
|
self.playAudioOnPC = settings.playAudioOnPC;
|
||||||
|
self.redirectMic = settings.redirectMic;
|
||||||
|
self.useBuiltinMic = settings.useBuiltinMic;
|
||||||
self.enableHdr = settings.enableHdr;
|
self.enableHdr = settings.enableHdr;
|
||||||
self.optimizeGames = settings.optimizeGames;
|
self.optimizeGames = settings.optimizeGames;
|
||||||
self.multiController = settings.multiController;
|
self.multiController = settings.multiController;
|
||||||
|
self.buttonVisualFeedback = settings.buttonVisualFeedback;
|
||||||
|
self.touchPointTracking = settings.touchPointTracking;
|
||||||
self.swapABXYButtons = settings.swapABXYButtons;
|
self.swapABXYButtons = settings.swapABXYButtons;
|
||||||
self.onscreenControls = settings.onscreenControls;
|
self.onscreenControls = settings.onscreenControls;
|
||||||
self.gyroMode = settings.gyroMode;
|
self.gyroMode = settings.gyroMode;
|
||||||
@@ -118,10 +122,13 @@
|
|||||||
self.slideToSettingsDistance = settings.slideToSettingsDistance;
|
self.slideToSettingsDistance = settings.slideToSettingsDistance;
|
||||||
self.liftStreamViewForKeyboard = settings.liftStreamViewForKeyboard;
|
self.liftStreamViewForKeyboard = settings.liftStreamViewForKeyboard;
|
||||||
self.showKeyboardToolbar = settings.showKeyboardToolbar;
|
self.showKeyboardToolbar = settings.showKeyboardToolbar;
|
||||||
|
self.softKeyboardHeight = settings.softKeyboardHeight;
|
||||||
self.touchMoveEventInterval = settings.touchMoveEventInterval;
|
self.touchMoveEventInterval = settings.touchMoveEventInterval;
|
||||||
self.touchPointerVelocityFactor = settings.touchPointerVelocityFactor;
|
self.touchPointerVelocityFactor = settings.touchPointerVelocityFactor;
|
||||||
self.mousePointerVelocityFactor = settings.mousePointerVelocityFactor;
|
self.mousePointerVelocityFactor = settings.mousePointerVelocityFactor;
|
||||||
self.gyroSensitivity = settings.gyroSensitivity;
|
self.gyroSensitivity = settings.gyroSensitivity;
|
||||||
|
self.localVolume = settings.localVolume;
|
||||||
|
self.micVolume = settings.micVolume;
|
||||||
self.pointerVelocityModeDivider = settings.pointerVelocityModeDivider;
|
self.pointerVelocityModeDivider = settings.pointerVelocityModeDivider;
|
||||||
self.unlockDisplayOrientation = settings.unlockDisplayOrientation;
|
self.unlockDisplayOrientation = settings.unlockDisplayOrientation;
|
||||||
self.resolutionSelected = settings.resolutionSelected;
|
self.resolutionSelected = settings.resolutionSelected;
|
||||||
@@ -131,9 +138,53 @@
|
|||||||
self.graphOpacity = settings.graphOpacity;
|
self.graphOpacity = settings.graphOpacity;
|
||||||
self.renderingBackend = settings.renderingBackend;
|
self.renderingBackend = settings.renderingBackend;
|
||||||
self.framePacingMode = settings.framePacingMode;
|
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.backgroundSessionTimer = settings.backroundSessionTimer;
|
||||||
#endif
|
self.edgeSlidingSensitivity = settings.edgeSlidingSensitivity;
|
||||||
|
self.appTheme = settings.appTheme;
|
||||||
|
self.hapticEngine = settings.hapticEngine;
|
||||||
self.uniqueId = settings.uniqueId;
|
self.uniqueId = settings.uniqueId;
|
||||||
|
self.audioEngine = settings.audioEngine;
|
||||||
|
self.delayLeftClick = settings.delayLeftClick;
|
||||||
|
self.duckOtherApps = settings.duckOtherApps;
|
||||||
|
self.muteInBackground = settings.muteInBackground;
|
||||||
|
self.relativeTouchSlideThreshold = settings.relativeTouchSlideThreshold;
|
||||||
|
self.enablePinch = settings.enablePinch;
|
||||||
|
self.scrollSensitivity = settings.scrollSensitivity;
|
||||||
|
self.pinchSensitivity = settings.pinchSensitivity;
|
||||||
|
self.leftClickDelayMs = settings.leftClickDelayMs;
|
||||||
|
self.ctrlDownForPinch = settings.ctrlDownForPinch;
|
||||||
|
self.settingsMenuOffset = settings.settingsMenuOffset;
|
||||||
|
self.passthroughGestures = settings.passthroughGestures;
|
||||||
|
self.enableControllerNavigation = settings.enableControllerNavigation;
|
||||||
|
self.controllerMouseLeftButton = settings.controllerMouseLeftButton;
|
||||||
|
self.controllerMouseRightButton = settings.controllerMouseRightButton;
|
||||||
|
self.localRadialMenuButton = settings.localRadialMenuButton;
|
||||||
|
self.streamingRadialMenuButton = settings.streamingRadialMenuButton;
|
||||||
|
self.controllerMouseStick = settings.controllerMouseStick;
|
||||||
|
self.controllerMousePointerVelocity = settings.controllerMousePointerVelocity;
|
||||||
|
self.controllerMouseExpo = settings.controllerMouseExpo;
|
||||||
|
self.streamingRadialMenuDelay = settings.streamingRadialMenuDelay;
|
||||||
|
self.globeAsEscape = settings.globeAsEscape;
|
||||||
|
|
||||||
|
// Pencil settings:
|
||||||
|
self.pencilTickMode = settings.pencilTickMode;
|
||||||
|
self.pencilTickIntervalUs = settings.pencilTickIntervalUs;
|
||||||
|
self.pencilTipOffsetX = settings.pencilTipOffsetX;
|
||||||
|
self.pencilTipOffsetY = settings.pencilTipOffsetY;
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,565 @@
|
|||||||
|
//
|
||||||
|
// GamepadNavigationIllustrationHud.swift
|
||||||
|
// VoidLink
|
||||||
|
//
|
||||||
|
// Created by True Zhuanjia on 2026/7/14.
|
||||||
|
// Copyright 2026 True Zhuanjia @ Bilibili. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
@available(iOS 13.0, *)
|
||||||
|
@objcMembers
|
||||||
|
final class GamepadNavigationIllustrationHud: UIView {
|
||||||
|
private enum KeyContent {
|
||||||
|
case text(String)
|
||||||
|
case symbol(String)
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct Hint {
|
||||||
|
let control: ControllerElement
|
||||||
|
let keyContent: KeyContent
|
||||||
|
let titles: [String]
|
||||||
|
let isInAction: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
private let containerView = UIView()
|
||||||
|
private let contentStackView = UIStackView()
|
||||||
|
private var themeObserver: NSObjectProtocol?
|
||||||
|
private var windowConstraints: [NSLayoutConstraint] = []
|
||||||
|
private var trailingConstraint: NSLayoutConstraint?
|
||||||
|
private static let keyBorderViewTag = 4101
|
||||||
|
private static let keyContentViewTag = 4102
|
||||||
|
private static var currentHud: GamepadNavigationIllustrationHud?
|
||||||
|
private static var activeControls = Set<ControllerElement>()
|
||||||
|
private static var actionStateMinimumEndTimes: [ControllerElement: CFTimeInterval] = [:]
|
||||||
|
private static var pendingActionStateWorkItems: [ControllerElement: DispatchWorkItem] = [:]
|
||||||
|
private static var pendingClearHudWorkItem: DispatchWorkItem?
|
||||||
|
private static let minimumActionStateDuration: CFTimeInterval = 0.09
|
||||||
|
private static let hudScale: CGFloat = PublicUtils.isIPhone ? 0.74 : 1
|
||||||
|
private static let hudWidth: CGFloat = 235
|
||||||
|
private static let edgeMargin: CGFloat = PublicUtils.isIPhone ? 5 : 24
|
||||||
|
private var hints: [Hint] = []
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
setupView()
|
||||||
|
observeThemeChanges()
|
||||||
|
updateTheme()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
setupView()
|
||||||
|
observeThemeChanges()
|
||||||
|
updateTheme()
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
NSLayoutConstraint.deactivate(windowConstraints)
|
||||||
|
containerView.removeFromSuperview()
|
||||||
|
if let themeObserver {
|
||||||
|
NotificationCenter.default.removeObserver(themeObserver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func safeAreaInsetsDidChange() {
|
||||||
|
super.safeAreaInsetsDidChange()
|
||||||
|
if let window {
|
||||||
|
updateTrailingOffset(for: window)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
static func showInKeyWindow() -> GamepadNavigationIllustrationHud? {
|
||||||
|
guard let window = keyWindow(), ControllerUtil.primaryGCController != nil else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let hud = currentHud ?? GamepadNavigationIllustrationHud()
|
||||||
|
currentHud = hud
|
||||||
|
hud.attach(to: window)
|
||||||
|
|
||||||
|
if let mainFrameVC = ControllerNavigator.radialMenuDelegate as? MainFrameViewController, ControllerNavigator.radialMenuView == nil {
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now()+3) {
|
||||||
|
let streamFrameVC = StreamFrameViewController.sharedInstance()
|
||||||
|
if ControllerNavigator.radialMenuView == nil, mainFrameVC.isStreaming(), !mainFrameVC.settingsExpandedInStreamView, streamFrameVC?.hasNoPresentedVC == true, !ControllerNavigator.controllerMouseEnabled {
|
||||||
|
requestHudDetachKeepingMinimumActionDuration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hud
|
||||||
|
}
|
||||||
|
|
||||||
|
static func updateCurrentTheme() {
|
||||||
|
currentHud?.updateTheme()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@objc static func updateNavigationElements(_ elements: [ControllerNavigationElement], forceDisplay: Bool = false) {
|
||||||
|
if !forceDisplay {
|
||||||
|
guard ControllerNavigator.enabled, ControllerUtil.primaryGCController != nil else {return}
|
||||||
|
guard !ControllerNavigator.stickReleasedInRadialMenu else {return}
|
||||||
|
}
|
||||||
|
PublicUtils.runOnMain {
|
||||||
|
let hud = showInKeyWindow()
|
||||||
|
hud?.updateHints(with: elements)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc static func updateHud(forceDisplay: Bool = false) {
|
||||||
|
let elements = ControllerNavigator.uiNavigationDelegate?.getNavigationElements() ?? []
|
||||||
|
/*
|
||||||
|
for element in elements {
|
||||||
|
print("element \(element.action)")
|
||||||
|
} */
|
||||||
|
updateNavigationElements(elements, forceDisplay: forceDisplay)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func updateActionState(for control: ControllerElement, isInAction: Bool) {
|
||||||
|
PublicUtils.runOnMain {
|
||||||
|
if isInAction {
|
||||||
|
pendingClearHudWorkItem?.cancel()
|
||||||
|
pendingClearHudWorkItem = nil
|
||||||
|
pendingActionStateWorkItems[control]?.cancel()
|
||||||
|
pendingActionStateWorkItems[control] = nil
|
||||||
|
actionStateMinimumEndTimes[control] = CACurrentMediaTime() + minimumActionStateDuration
|
||||||
|
|
||||||
|
let didChange = activeControls.insert(control).inserted
|
||||||
|
guard didChange else { return }
|
||||||
|
currentHud?.refreshActionStates()
|
||||||
|
} else {
|
||||||
|
requestActionStateEnd(for: control)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func resetActionStates() {
|
||||||
|
PublicUtils.runOnMain {
|
||||||
|
guard !activeControls.isEmpty else { return }
|
||||||
|
Array(activeControls).forEach { requestActionStateEnd(for: $0) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func clearHud() {
|
||||||
|
PublicUtils.runOnMain {
|
||||||
|
if ControllerNavigator.controllerMouseEnabled {
|
||||||
|
finishHudDetach()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
requestHudDetachKeepingMinimumActionDuration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateTheme() {
|
||||||
|
let isDark = ThemeManager.userInterfaceStyle() == .dark
|
||||||
|
|
||||||
|
backgroundColor = isDark
|
||||||
|
? UIColor(red: 28.0 / 255.0, green: 28.0 / 255.0, blue: 30.0 / 255.0, alpha: 0.62)
|
||||||
|
: (PublicUtils.isIPhone ? UIColor.white.withAlphaComponent(0.75) : UIColor.white.withAlphaComponent(0.6))
|
||||||
|
layer.borderColor = (isDark ? UIColor.white.withAlphaComponent(0.10) : UIColor.black.withAlphaComponent(0.06)).cgColor
|
||||||
|
layer.shadowColor = UIColor.black.cgColor
|
||||||
|
layer.shadowOpacity = isDark ? 0.34 : 0.12
|
||||||
|
layer.shadowRadius = isDark ? 18 : 16
|
||||||
|
layer.shadowOffset = CGSize(width: 0, height: 8)
|
||||||
|
|
||||||
|
for (index, row) in contentStackView.arrangedSubviews.enumerated() {
|
||||||
|
guard hints.indices.contains(index),
|
||||||
|
let rowStack = row as? UIStackView,
|
||||||
|
let keyView = rowStack.arrangedSubviews.first,
|
||||||
|
let titleView = rowStack.arrangedSubviews.last else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let isInAction = hints[index].isInAction
|
||||||
|
let keyBorderView = keyView.viewWithTag(Self.keyBorderViewTag) ?? keyView
|
||||||
|
let keyContentView = keyView.viewWithTag(Self.keyContentViewTag) ?? keyView
|
||||||
|
let keyBackgroundColor = isInAction
|
||||||
|
? ThemeManager.appPrimaryColor.withAlphaComponent(isDark ? 0.2 : 0.13)
|
||||||
|
: ThemeManager.appPrimaryColor.withAlphaComponent(isDark ? 0.09 : 0.05)
|
||||||
|
keyView.backgroundColor = .clear
|
||||||
|
keyBorderView.layer.borderColor = ThemeManager.appPrimaryColor.withAlphaComponent(isDark ? 0.69 : 0.6).cgColor
|
||||||
|
keyContentView.backgroundColor = keyBackgroundColor
|
||||||
|
keyView.transform = .identity
|
||||||
|
for subview in keyContentView.subviews {
|
||||||
|
if let keyLabel = subview as? UILabel {
|
||||||
|
keyLabel.textColor = isInAction ? ThemeManager.appPrimaryColor.withAlphaComponent(isDark ? 1 : 1) : ThemeManager.appPrimaryColor.withAlphaComponent(isDark ? 0.83 : 0.83)
|
||||||
|
} else if let imageView = subview as? UIImageView {
|
||||||
|
imageView.tintColor = isInAction ? ThemeManager.appPrimaryColor.withAlphaComponent(isDark ? 1 : 1) : ThemeManager.appPrimaryColor.withAlphaComponent(isDark ? 0.83 : 0.83)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateTitleColors(in: titleView, color: ThemeManager.textColor.withAlphaComponent(isDark ? 0.69 : 0.6))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupView() {
|
||||||
|
translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
isUserInteractionEnabled = false
|
||||||
|
containerView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
containerView.isUserInteractionEnabled = false
|
||||||
|
transform = CGAffineTransform(scaleX: Self.hudScale, y: Self.hudScale)
|
||||||
|
layer.cornerRadius = 18
|
||||||
|
layer.cornerCurve = .continuous
|
||||||
|
layer.borderWidth = 1
|
||||||
|
|
||||||
|
contentStackView.axis = .vertical
|
||||||
|
contentStackView.alignment = .leading
|
||||||
|
contentStackView.spacing = 6
|
||||||
|
contentStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(contentStackView)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
contentStackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12),
|
||||||
|
contentStackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12),
|
||||||
|
contentStackView.topAnchor.constraint(equalTo: topAnchor, constant: 10),
|
||||||
|
contentStackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -10)
|
||||||
|
])
|
||||||
|
|
||||||
|
reloadHintRows()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateHints(with elements: [ControllerNavigationElement]) {
|
||||||
|
let nextHints = hints(for: elements)
|
||||||
|
hints = nextHints.isEmpty ? [] : nextHints
|
||||||
|
guard !hints.isEmpty else {return}
|
||||||
|
reloadHintRows()
|
||||||
|
updateTheme()
|
||||||
|
isHidden = hints.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
private func refreshActionStates() {
|
||||||
|
hints = hints.map { hint in
|
||||||
|
Hint(
|
||||||
|
control: hint.control,
|
||||||
|
keyContent: hint.keyContent,
|
||||||
|
titles: hint.titles,
|
||||||
|
isInAction: Self.isControlActive(hint.control)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
updateTheme()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func reloadHintRows() {
|
||||||
|
contentStackView.arrangedSubviews.forEach { row in
|
||||||
|
contentStackView.removeArrangedSubview(row)
|
||||||
|
row.removeFromSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
hints.forEach { contentStackView.addArrangedSubview(makeRow(for: $0)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func hints(for navigationElements: [ControllerNavigationElement]) -> [Hint] {
|
||||||
|
var groupedElements: [ControllerElement: [ControllerNavigationElement]] = [:]
|
||||||
|
var orderedControls: [ControllerElement] = []
|
||||||
|
|
||||||
|
for navigationElement in navigationElements {
|
||||||
|
if groupedElements[navigationElement.control] == nil {
|
||||||
|
orderedControls.append(navigationElement.control)
|
||||||
|
groupedElements[navigationElement.control] = []
|
||||||
|
}
|
||||||
|
groupedElements[navigationElement.control]?.append(navigationElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
return orderedControls.flatMap { control -> [Hint] in
|
||||||
|
guard let elements = groupedElements[control] else { return [] }
|
||||||
|
if elements.count == 2 {
|
||||||
|
return [hint(for: elements)]
|
||||||
|
}
|
||||||
|
|
||||||
|
return elements.map { hint(for: [$0]) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func hint(for navigationElements: [ControllerNavigationElement]) -> Hint {
|
||||||
|
let control = navigationElements.first?.control ?? .null
|
||||||
|
return Hint(
|
||||||
|
control: control,
|
||||||
|
keyContent: keyContent(for: control),
|
||||||
|
titles: navigationElements.map { $0.action.localized },
|
||||||
|
isInAction: navigationElements.contains(where: { $0.isInAction }) || Self.isControlActive(control)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func keyContent(for control: ControllerElement) -> KeyContent {
|
||||||
|
let symbol = control.symbol
|
||||||
|
return symbol.isEmpty ? .text(control.displayName) : .symbol(symbol)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func attach(to window: UIWindow) {
|
||||||
|
if containerView.superview !== window {
|
||||||
|
NSLayoutConstraint.deactivate(windowConstraints)
|
||||||
|
windowConstraints.removeAll()
|
||||||
|
removeFromSuperview()
|
||||||
|
containerView.removeFromSuperview()
|
||||||
|
window.addSubview(containerView)
|
||||||
|
containerView.addSubview(self)
|
||||||
|
|
||||||
|
let trailingConstraint = containerView.trailingAnchor.constraint(equalTo: window.safeAreaLayoutGuide.trailingAnchor)
|
||||||
|
self.trailingConstraint = trailingConstraint
|
||||||
|
windowConstraints = [
|
||||||
|
containerView.widthAnchor.constraint(equalToConstant: Self.hudWidth * Self.hudScale),
|
||||||
|
containerView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: Self.hudScale),
|
||||||
|
trailingConstraint,
|
||||||
|
containerView.bottomAnchor.constraint(equalTo: window.safeAreaLayoutGuide.bottomAnchor, constant: -Self.edgeMargin),
|
||||||
|
|
||||||
|
widthAnchor.constraint(equalToConstant: Self.hudWidth),
|
||||||
|
centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
|
||||||
|
centerYAnchor.constraint(equalTo: containerView.centerYAnchor)
|
||||||
|
]
|
||||||
|
NSLayoutConstraint.activate(windowConstraints)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTrailingOffset(for: window)
|
||||||
|
window.bringSubviewToFront(containerView)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func detachFromWindow() {
|
||||||
|
NSLayoutConstraint.deactivate(windowConstraints)
|
||||||
|
windowConstraints.removeAll()
|
||||||
|
trailingConstraint = nil
|
||||||
|
removeFromSuperview()
|
||||||
|
containerView.removeFromSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateTrailingOffset(for window: UIWindow) {
|
||||||
|
let horizontalMargin = Self.edgeMargin + (PublicUtils.isIPhone ? 0 : window.safeAreaInsets.bottom)
|
||||||
|
trailingConstraint?.constant = -horizontalMargin
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeRow(for hint: Hint) -> UIStackView {
|
||||||
|
let keyView = makeKeyView(for: hint.keyContent)
|
||||||
|
let titleView = makeTitleView(for: hint.titles)
|
||||||
|
|
||||||
|
let rowStackView = UIStackView(arrangedSubviews: [keyView, titleView])
|
||||||
|
rowStackView.axis = .horizontal
|
||||||
|
rowStackView.alignment = .center
|
||||||
|
rowStackView.spacing = 8
|
||||||
|
return rowStackView
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeTitleView(for titles: [String]) -> UIView {
|
||||||
|
if titles.count == 2 {
|
||||||
|
let stackView = UIStackView(arrangedSubviews: titles.map { makeTitleLabel(text: $0, isMergedTitle: true) })
|
||||||
|
stackView.axis = .vertical
|
||||||
|
stackView.alignment = .leading
|
||||||
|
stackView.spacing = 1
|
||||||
|
stackView.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||||
|
return stackView
|
||||||
|
}
|
||||||
|
|
||||||
|
return makeTitleLabel(text: titles.first ?? "", isMergedTitle: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeTitleLabel(text: String, isMergedTitle: Bool) -> UILabel {
|
||||||
|
let titleLabel = UILabel()
|
||||||
|
titleLabel.text = text
|
||||||
|
titleLabel.font = UIFont.roundedSystemFont(ofSize: isMergedTitle ? 12.5 : 14, weight: .medium)
|
||||||
|
titleLabel.adjustsFontSizeToFitWidth = true
|
||||||
|
titleLabel.minimumScaleFactor = isMergedTitle ? 0.55 : 0.2
|
||||||
|
titleLabel.numberOfLines = 1
|
||||||
|
titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||||
|
return titleLabel
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateTitleColors(in view: UIView, color: UIColor) {
|
||||||
|
if let label = view as? UILabel {
|
||||||
|
label.textColor = color
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
view.subviews.forEach { updateTitleColors(in: $0, color: color) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeKeyView(for content: KeyContent) -> UIView {
|
||||||
|
let keyContainerView = UIView()
|
||||||
|
keyContainerView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
keyContainerView.backgroundColor = .clear
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
keyContainerView.widthAnchor.constraint(equalToConstant: 50),
|
||||||
|
keyContainerView.heightAnchor.constraint(equalToConstant: PublicUtils.isIPhone ? 38 : 42)
|
||||||
|
])
|
||||||
|
|
||||||
|
let borderView = UIView()
|
||||||
|
borderView.tag = Self.keyBorderViewTag
|
||||||
|
borderView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
borderView.isUserInteractionEnabled = false
|
||||||
|
borderView.backgroundColor = .clear
|
||||||
|
borderView.layer.cornerRadius = 13
|
||||||
|
borderView.layer.cornerCurve = .continuous
|
||||||
|
borderView.layer.borderWidth = 1
|
||||||
|
borderView.layer.masksToBounds = true
|
||||||
|
keyContainerView.addSubview(borderView)
|
||||||
|
|
||||||
|
let keyView = UIView()
|
||||||
|
keyView.tag = Self.keyContentViewTag
|
||||||
|
keyView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
keyView.layer.cornerRadius = 12
|
||||||
|
keyView.layer.cornerCurve = .continuous
|
||||||
|
keyView.layer.masksToBounds = true
|
||||||
|
keyContainerView.addSubview(keyView)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
borderView.leadingAnchor.constraint(equalTo: keyContainerView.leadingAnchor),
|
||||||
|
borderView.trailingAnchor.constraint(equalTo: keyContainerView.trailingAnchor),
|
||||||
|
borderView.topAnchor.constraint(equalTo: keyContainerView.topAnchor),
|
||||||
|
borderView.bottomAnchor.constraint(equalTo: keyContainerView.bottomAnchor),
|
||||||
|
|
||||||
|
keyView.leadingAnchor.constraint(equalTo: keyContainerView.leadingAnchor, constant: 1),
|
||||||
|
keyView.trailingAnchor.constraint(equalTo: keyContainerView.trailingAnchor, constant: -1),
|
||||||
|
keyView.topAnchor.constraint(equalTo: keyContainerView.topAnchor, constant: 1),
|
||||||
|
keyView.bottomAnchor.constraint(equalTo: keyContainerView.bottomAnchor, constant: -1)
|
||||||
|
])
|
||||||
|
|
||||||
|
switch content {
|
||||||
|
case .text(let text):
|
||||||
|
let keyLabel = InsetLabel()
|
||||||
|
keyLabel.contentInsets = UIEdgeInsets(top: 0, left: 6, bottom: 0, right: 6)
|
||||||
|
keyLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
keyLabel.text = text
|
||||||
|
keyLabel.textAlignment = .center
|
||||||
|
keyLabel.font = UIFont.roundedSystemFont(ofSize: 12, weight: PublicUtils.isIPhone ? .semibold : .medium)
|
||||||
|
keyLabel.adjustsFontSizeToFitWidth = true
|
||||||
|
keyLabel.minimumScaleFactor = 0.2
|
||||||
|
keyLabel.numberOfLines = 1
|
||||||
|
keyLabel.lineBreakMode = .byClipping
|
||||||
|
keyView.addSubview(keyLabel)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
keyLabel.leadingAnchor.constraint(equalTo: keyView.leadingAnchor),
|
||||||
|
keyLabel.trailingAnchor.constraint(equalTo: keyView.trailingAnchor),
|
||||||
|
keyLabel.topAnchor.constraint(equalTo: keyView.topAnchor),
|
||||||
|
keyLabel.bottomAnchor.constraint(equalTo: keyView.bottomAnchor)
|
||||||
|
])
|
||||||
|
case .symbol(let symbolName):
|
||||||
|
let imageView = UIImageView(image: UIImage(systemName: symbolName))
|
||||||
|
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
imageView.contentMode = .scaleAspectFit
|
||||||
|
imageView.preferredSymbolConfiguration = UIImage.SymbolConfiguration(pointSize: 18, weight: .medium)
|
||||||
|
keyView.addSubview(imageView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
imageView.centerXAnchor.constraint(equalTo: keyView.centerXAnchor),
|
||||||
|
imageView.centerYAnchor.constraint(equalTo: keyView.centerYAnchor),
|
||||||
|
imageView.widthAnchor.constraint(lessThanOrEqualTo: keyView.widthAnchor, multiplier: 0.58),
|
||||||
|
imageView.heightAnchor.constraint(lessThanOrEqualTo: keyView.heightAnchor, multiplier: 0.58)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyContainerView
|
||||||
|
}
|
||||||
|
|
||||||
|
private func observeThemeChanges() {
|
||||||
|
themeObserver = NotificationCenter.default.addObserver(
|
||||||
|
forName: Notification.Name(ThemeManager.ThemeDidChangeNotification),
|
||||||
|
object: nil,
|
||||||
|
queue: .main
|
||||||
|
) { [weak self] _ in
|
||||||
|
self?.updateTheme()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func keyWindow() -> UIWindow? {
|
||||||
|
let sceneWindows = UIApplication.shared.connectedScenes
|
||||||
|
.compactMap { $0 as? UIWindowScene }
|
||||||
|
.flatMap { $0.windows }
|
||||||
|
return sceneWindows.first(where: { $0.isKeyWindow })
|
||||||
|
?? sceneWindows.first
|
||||||
|
?? UIApplication.shared.windows.first(where: { $0.isKeyWindow })
|
||||||
|
?? UIApplication.shared.windows.first
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func isControlActive(_ control: ControllerElement) -> Bool {
|
||||||
|
!activeControls(forHintControl: control).isDisjoint(with: activeControls)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func requestActionStateEnd(for control: ControllerElement) {
|
||||||
|
pendingActionStateWorkItems[control]?.cancel()
|
||||||
|
|
||||||
|
let now = CACurrentMediaTime()
|
||||||
|
let remainingDuration = max((actionStateMinimumEndTimes[control] ?? now) - now, 0)
|
||||||
|
guard remainingDuration > 0 else {
|
||||||
|
finishActionStateEnd(for: control)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let workItem = DispatchWorkItem {
|
||||||
|
finishActionStateEnd(for: control)
|
||||||
|
}
|
||||||
|
pendingActionStateWorkItems[control] = workItem
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + remainingDuration, execute: workItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func finishActionStateEnd(for control: ControllerElement) {
|
||||||
|
pendingActionStateWorkItems[control]?.cancel()
|
||||||
|
pendingActionStateWorkItems[control] = nil
|
||||||
|
actionStateMinimumEndTimes[control] = nil
|
||||||
|
|
||||||
|
guard activeControls.remove(control) != nil else { return }
|
||||||
|
currentHud?.refreshActionStates()
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func requestHudDetachKeepingMinimumActionDuration() {
|
||||||
|
pendingClearHudWorkItem?.cancel()
|
||||||
|
|
||||||
|
let remainingDuration = maximumRemainingActionStateDuration()
|
||||||
|
guard remainingDuration > 0 else {
|
||||||
|
finishHudDetach()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let workItem = DispatchWorkItem {
|
||||||
|
finishHudDetach()
|
||||||
|
}
|
||||||
|
pendingClearHudWorkItem = workItem
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + remainingDuration, execute: workItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func finishHudDetach() {
|
||||||
|
pendingClearHudWorkItem?.cancel()
|
||||||
|
pendingClearHudWorkItem = nil
|
||||||
|
pendingActionStateWorkItems.values.forEach { $0.cancel() }
|
||||||
|
pendingActionStateWorkItems.removeAll()
|
||||||
|
actionStateMinimumEndTimes.removeAll()
|
||||||
|
activeControls.removeAll()
|
||||||
|
currentHud?.detachFromWindow()
|
||||||
|
currentHud = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func maximumRemainingActionStateDuration() -> CFTimeInterval {
|
||||||
|
let now = CACurrentMediaTime()
|
||||||
|
return activeControls.reduce(0) { partialResult, control in
|
||||||
|
max(partialResult, max((actionStateMinimumEndTimes[control] ?? now) - now, 0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func activeControls(forHintControl control: ControllerElement) -> Set<ControllerElement> {
|
||||||
|
switch control {
|
||||||
|
case .leftStick, .leftStickX, .leftStickY:
|
||||||
|
return [.leftStick, .leftStickX, .leftStickY]
|
||||||
|
case .rightStick, .rightStickX, .rightStickY:
|
||||||
|
return [.rightStick, .rightStickX, .rightStickY]
|
||||||
|
case .dpad:
|
||||||
|
return [.dpad, .dpadUp, .dpadDown, .dpadLeft, .dpadRight]
|
||||||
|
case .abxy:
|
||||||
|
return [.abxy, .a, .b, .x, .y]
|
||||||
|
default:
|
||||||
|
return [control]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class InsetLabel: UILabel {
|
||||||
|
var contentInsets = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 4)
|
||||||
|
|
||||||
|
override func drawText(in rect: CGRect) {
|
||||||
|
super.drawText(in: rect.inset(by: contentInsets))
|
||||||
|
}
|
||||||
|
|
||||||
|
override var intrinsicContentSize: CGSize {
|
||||||
|
let size = super.intrinsicContentSize
|
||||||
|
return CGSize(
|
||||||
|
width: size.width + contentInsets.left + contentInsets.right,
|
||||||
|
height: size.height + contentInsets.top + contentInsets.bottom
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
//
|
|
||||||
// HostCardView.h
|
|
||||||
// VoidLink
|
|
||||||
//
|
|
||||||
// Created by True砖家 on 5/18/25.
|
|
||||||
// Copyright © 2025 True砖家 @ Bilibili. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
|
|
||||||
#import <UIKit/UIKit.h>
|
|
||||||
#import "TemporaryHost.h"
|
|
||||||
|
|
||||||
@protocol HostCardActionDelegate <NSObject>
|
|
||||||
- (void)appButtonTappedForHost:(TemporaryHost *)host;
|
|
||||||
- (void)launchButtonTappedForHost:(TemporaryHost *)host;
|
|
||||||
- (void)wakeupButtonTappedForHost:(TemporaryHost *)host;
|
|
||||||
- (void)pairButtonTappedForHost:(TemporaryHost *)host;
|
|
||||||
- (void)hostCardLongPressed:(TemporaryHost *)host view:(UIView *)view;
|
|
||||||
- (bool)isStreaming;
|
|
||||||
@end
|
|
||||||
|
|
||||||
|
|
||||||
@interface HostCardView : UIView
|
|
||||||
@property (nonatomic, assign) CGFloat sizeFactor;
|
|
||||||
@property (nonatomic, readonly) CGSize size;
|
|
||||||
@property (nonatomic, weak) id<HostCardActionDelegate> delegate; // Delegate property
|
|
||||||
|
|
||||||
- (void)resizeBySizeFactor:(CGFloat)factor;
|
|
||||||
- (id) initWithHost:(TemporaryHost*)host;
|
|
||||||
- (id) initWithHost:(TemporaryHost*)host andSizeFactor:(CGFloat)sizeFactor;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
@@ -1,640 +0,0 @@
|
|||||||
//
|
|
||||||
// HostCardView.m
|
|
||||||
// VoidLink
|
|
||||||
//
|
|
||||||
// Created by True砖家 on 5/18/25.
|
|
||||||
// Copyright © 2025 True砖家 @ Bilibili. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "HostCardView.h"
|
|
||||||
#import "LocalizationHelper.h"
|
|
||||||
#import "ThemeManager.h"
|
|
||||||
#import "FixedTintImageView.h"
|
|
||||||
|
|
||||||
|
|
||||||
@interface HostCardView ()
|
|
||||||
|
|
||||||
@property (nonatomic, strong) UIView *iconBackgroundView;
|
|
||||||
@property (nonatomic, strong) UIImageView *hostIconView;
|
|
||||||
@property (nonatomic, strong) UILabel *hostNameLabel;
|
|
||||||
@property (nonatomic, strong) UIImageView *statusIcon;
|
|
||||||
@property (nonatomic, strong) UILabel *statusLabel;
|
|
||||||
@property (nonatomic, strong) UIButton *appButton;
|
|
||||||
@property (nonatomic, strong) UIButton *launchButton;
|
|
||||||
@property (nonatomic, strong) UIButton *pairButton;
|
|
||||||
@property (nonatomic, strong) UIButton *wakeupButton;
|
|
||||||
@property (nonatomic, strong) UIButton *transparentButton;
|
|
||||||
@property (nonatomic, strong) NSLayoutConstraint *widthConstraint;
|
|
||||||
@property (nonatomic, strong) NSLayoutConstraint *heightConstraint;
|
|
||||||
@property (nonatomic, strong) UIView *separatorLine;
|
|
||||||
@property (nonatomic, assign) CGFloat cardContentpadding;
|
|
||||||
@property (nonatomic, assign) CGSize size;
|
|
||||||
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
|
|
||||||
@implementation HostCardView {
|
|
||||||
TemporaryHost* _host;
|
|
||||||
UIActivityIndicatorView* _hostSpinner;
|
|
||||||
UIImageView* lockIconView;
|
|
||||||
CGFloat computerIconMonitorCenterYOffset;
|
|
||||||
CGFloat buttonHeight;
|
|
||||||
CGFloat buttonLabelFontSize;
|
|
||||||
CGFloat iconAndButtonSpacing;
|
|
||||||
UIColor *defaultBlue;
|
|
||||||
UIColor *defaultGreen;
|
|
||||||
CAGradientLayer *backgroundLayer;
|
|
||||||
bool longPressFired;
|
|
||||||
}
|
|
||||||
|
|
||||||
static const float REFRESH_CYCLE = 2.0f;
|
|
||||||
|
|
||||||
- (BOOL)isIPhone{
|
|
||||||
return ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone);
|
|
||||||
}
|
|
||||||
|
|
||||||
- (instancetype)initWithFrame:(CGRect)frame {
|
|
||||||
self = [super initWithFrame:frame];
|
|
||||||
if (self) {
|
|
||||||
buttonLabelFontSize = 15*_sizeFactor;
|
|
||||||
longPressFired = false;
|
|
||||||
computerIconMonitorCenterYOffset = [self isIPhone] ? -2.75*_sizeFactor : -3.2*_sizeFactor;
|
|
||||||
iconAndButtonSpacing = 37*_sizeFactor;
|
|
||||||
buttonHeight = 39*_sizeFactor;
|
|
||||||
defaultBlue = [ThemeManager appPrimaryColor];
|
|
||||||
defaultGreen = [UIColor colorWithRed:52.0/255.0 green:199.0/255.0 blue:89.0/255.0 alpha:1.0];
|
|
||||||
// self.userIterfaceStyle = UIUserInterfaceStyleLight
|
|
||||||
|
|
||||||
UILongPressGestureRecognizer* longPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(hostCardLongPressed:)];
|
|
||||||
[self addGestureRecognizer:longPressRecognizer];
|
|
||||||
|
|
||||||
[self createBackgroundLayer];
|
|
||||||
[self setupUI];
|
|
||||||
}
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (id) initWithHost:(TemporaryHost*)host {
|
|
||||||
self.sizeFactor = 1.0;
|
|
||||||
self = [self init];
|
|
||||||
_host = host;
|
|
||||||
|
|
||||||
// Use UIContextMenuInteraction on iOS 13.0+ and a standard UILongPressGestureRecognizer
|
|
||||||
// for tvOS devices and iOS prior to 13.0.
|
|
||||||
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (id) initWithHost:(TemporaryHost*)host andSizeFactor:(CGFloat)sizeFactor {
|
|
||||||
_sizeFactor = sizeFactor;
|
|
||||||
self = [self init];
|
|
||||||
_host = host;
|
|
||||||
|
|
||||||
// Use UIContextMenuInteraction on iOS 13.0+ and a standard UILongPressGestureRecognizer
|
|
||||||
// for tvOS devices and iOS prior to 13.0.
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
- (instancetype)initWithCoder:(NSCoder *)coder {
|
|
||||||
self = [super initWithCoder:coder];
|
|
||||||
if (self) {
|
|
||||||
[self setupUI];
|
|
||||||
}
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)appButtonTapped{
|
|
||||||
if(longPressFired){
|
|
||||||
longPressFired = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if ([self.delegate respondsToSelector:@selector(appButtonTappedForHost:)]) {
|
|
||||||
[self.delegate appButtonTappedForHost:_host];
|
|
||||||
} else NSLog(@"Delegate not set or does not respond to appButtonTappedForHost:");
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)launchButtonTapped{
|
|
||||||
if(longPressFired){
|
|
||||||
longPressFired = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if ([self.delegate respondsToSelector:@selector(launchButtonTappedForHost:)]) {
|
|
||||||
[self.delegate launchButtonTappedForHost:_host];
|
|
||||||
} else NSLog(@"Delegate not set or does not respond to launchButtonTappedForHost:");
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)wakeupButtonTapped{
|
|
||||||
if(longPressFired){
|
|
||||||
longPressFired = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if ([self.delegate respondsToSelector:@selector(wakeupButtonTappedForHost:)]) {
|
|
||||||
[self.delegate wakeupButtonTappedForHost:_host];
|
|
||||||
} else NSLog(@"Delegate not set or does not respond to launchButtonTappedForHost:");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
-(void)pairButtonTapped{
|
|
||||||
if(longPressFired){
|
|
||||||
longPressFired = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if ([self.delegate respondsToSelector:@selector(pairButtonTappedForHost:)]) {
|
|
||||||
[self.delegate pairButtonTappedForHost:_host];
|
|
||||||
} else NSLog(@"Delegate not set or does not respond to pairButtonTappedForHost:");
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)hostCardLongPressed:(UILongPressGestureRecognizer* )gesture{
|
|
||||||
if(gesture.state != UIGestureRecognizerStateBegan) return;
|
|
||||||
longPressFired = true;
|
|
||||||
if ([self.delegate respondsToSelector:@selector(hostCardLongPressed:view:)]) {
|
|
||||||
[self.delegate hostCardLongPressed:_host view:self];
|
|
||||||
} else NSLog(@"Delegate not set or does not respond");
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)resizeBySizeFactor:(CGFloat)factor{
|
|
||||||
[self.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
|
|
||||||
self.sizeFactor = factor;
|
|
||||||
[self setupUI];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)setupUI {
|
|
||||||
self.userInteractionEnabled = YES;
|
|
||||||
self.backgroundColor = [ThemeManager widgetBackgroundColor]; // theme
|
|
||||||
self.cardContentpadding = 13 * _sizeFactor;
|
|
||||||
self.clipsToBounds = YES;
|
|
||||||
self.translatesAutoresizingMaskIntoConstraints = NO;
|
|
||||||
CGFloat appButtonWidth = 100*_sizeFactor;
|
|
||||||
CGFloat launchButtonWidth = 120*_sizeFactor;
|
|
||||||
CGFloat cardWidth = _cardContentpadding*2 + appButtonWidth + 15*_sizeFactor + launchButtonWidth;
|
|
||||||
self.layer.cornerRadius = 2*(uint16_t)(cardWidth*0.0603/2) ;
|
|
||||||
/*
|
|
||||||
NSLayoutConstraint *topAnchorConstraint;
|
|
||||||
topAnchorConstraint = [self.topAnchor constraintEqualToAnchor:self.superview.topAnchor constant:500];
|
|
||||||
topAnchorConstraint.active = YES;
|
|
||||||
*/
|
|
||||||
_heightConstraint = [self.heightAnchor constraintEqualToConstant:300];
|
|
||||||
_heightConstraint.active = YES;
|
|
||||||
_widthConstraint = [self.widthAnchor constraintEqualToConstant:300];
|
|
||||||
_widthConstraint.active = YES;
|
|
||||||
|
|
||||||
|
|
||||||
// 图标背景
|
|
||||||
self.iconBackgroundView = [[UIView alloc] initWithFrame:CGRectMake(_cardContentpadding, _cardContentpadding, 80*_sizeFactor, 80*_sizeFactor)];
|
|
||||||
self.iconBackgroundView.backgroundColor = defaultBlue;
|
|
||||||
self.iconBackgroundView.layer.cornerRadius = 2*(uint16_t)(20*_sizeFactor/2);
|
|
||||||
[self addSubview:self.iconBackgroundView];
|
|
||||||
|
|
||||||
// 图标图片
|
|
||||||
self.hostIconView = [[UIImageView alloc] init];
|
|
||||||
self.hostIconView.translatesAutoresizingMaskIntoConstraints = NO;
|
|
||||||
self.hostIconView.contentMode = UIViewContentModeScaleAspectFit;
|
|
||||||
if (@available(iOS 13.0, *)) {
|
|
||||||
self.hostIconView.image = [[UIImage imageNamed:@"display"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
|
||||||
} else {
|
|
||||||
self.hostIconView.image = [UIImage imageNamed:@"Computer"];
|
|
||||||
[NSLayoutConstraint activateConstraints:@[
|
|
||||||
[self.hostIconView.heightAnchor constraintEqualToConstant:57*_sizeFactor],
|
|
||||||
[self.hostIconView.widthAnchor constraintEqualToConstant:57*_sizeFactor],
|
|
||||||
]];
|
|
||||||
}
|
|
||||||
self.hostIconView.tintColor = [UIColor whiteColor];
|
|
||||||
[self.iconBackgroundView addSubview:self.hostIconView];
|
|
||||||
[NSLayoutConstraint activateConstraints:@[
|
|
||||||
[self.hostIconView.centerXAnchor constraintEqualToAnchor:self.iconBackgroundView.centerXAnchor constant:0],
|
|
||||||
[self.hostIconView.centerYAnchor constraintEqualToAnchor:self.iconBackgroundView.centerYAnchor constant:0],
|
|
||||||
[self.hostIconView.heightAnchor constraintEqualToConstant:53.9*_sizeFactor],
|
|
||||||
[self.hostIconView.widthAnchor constraintEqualToConstant:53.9*_sizeFactor],
|
|
||||||
]];
|
|
||||||
// [self.iconBackgroundView layoutIfNeeded];
|
|
||||||
// [self.iconImageView layoutIfNeeded];
|
|
||||||
|
|
||||||
//spinner
|
|
||||||
bool oldiOS = false;
|
|
||||||
if (@available(iOS 13.0, *)){}
|
|
||||||
else oldiOS = true;
|
|
||||||
|
|
||||||
_hostSpinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];
|
|
||||||
_hostSpinner.userInteractionEnabled = NO;
|
|
||||||
_hostSpinner.translatesAutoresizingMaskIntoConstraints = NO;
|
|
||||||
_hostSpinner.hidesWhenStopped = YES;
|
|
||||||
[self.iconBackgroundView addSubview:_hostSpinner];
|
|
||||||
[NSLayoutConstraint activateConstraints:@[
|
|
||||||
[_hostSpinner.centerXAnchor constraintEqualToAnchor:self.iconBackgroundView.centerXAnchor constant:0],
|
|
||||||
[_hostSpinner.centerYAnchor constraintEqualToAnchor:self.iconBackgroundView.centerYAnchor constant:oldiOS ? -5.5*_sizeFactor : computerIconMonitorCenterYOffset],
|
|
||||||
]];
|
|
||||||
_hostSpinner.transform = CGAffineTransformMakeScale(_sizeFactor, _sizeFactor);
|
|
||||||
[_hostSpinner stopAnimating];
|
|
||||||
// [_hostSpinner startAnimating];
|
|
||||||
|
|
||||||
|
|
||||||
// lockIcon
|
|
||||||
lockIconView =[[UIImageView alloc] init];
|
|
||||||
lockIconView.translatesAutoresizingMaskIntoConstraints = NO;
|
|
||||||
lockIconView.contentMode = UIViewContentModeScaleAspectFit;
|
|
||||||
if (@available(iOS 13.0, *)) {
|
|
||||||
lockIconView.image = [[UIImage imageNamed:@"lock.fill"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
|
||||||
} else {
|
|
||||||
lockIconView.image = [UIImage imageNamed:@"LockedOverlayIcon"];
|
|
||||||
computerIconMonitorCenterYOffset = -5 * _sizeFactor;
|
|
||||||
}
|
|
||||||
lockIconView.tintColor = [UIColor whiteColor];
|
|
||||||
[self.iconBackgroundView insertSubview:lockIconView aboveSubview:_iconBackgroundView];
|
|
||||||
[NSLayoutConstraint activateConstraints:@[
|
|
||||||
[lockIconView.centerXAnchor constraintEqualToAnchor:_iconBackgroundView.centerXAnchor constant:0],
|
|
||||||
[lockIconView.centerYAnchor constraintEqualToAnchor:_iconBackgroundView.centerYAnchor constant:computerIconMonitorCenterYOffset],
|
|
||||||
[lockIconView.widthAnchor constraintEqualToConstant:17.05*_sizeFactor],
|
|
||||||
[lockIconView.heightAnchor constraintEqualToConstant:17.05*_sizeFactor]
|
|
||||||
]];
|
|
||||||
lockIconView.hidden = true;
|
|
||||||
|
|
||||||
// 设备名
|
|
||||||
CGFloat hostNameLabelCoordX = _cardContentpadding+_iconBackgroundView.frame.size.width+16*_sizeFactor;
|
|
||||||
self.hostNameLabel = [[UILabel alloc] initWithFrame:CGRectMake(hostNameLabelCoordX, _cardContentpadding+_iconBackgroundView.frame.size.height*0.02, cardWidth-hostNameLabelCoordX-_cardContentpadding, 30*_sizeFactor)];
|
|
||||||
self.hostNameLabel.numberOfLines = 1;
|
|
||||||
self.hostNameLabel.adjustsFontSizeToFitWidth = YES;
|
|
||||||
self.hostNameLabel.minimumScaleFactor = 0.8; // 最小字体缩放比例
|
|
||||||
self.hostNameLabel.lineBreakMode = NSLineBreakByTruncatingTail;
|
|
||||||
|
|
||||||
self.hostIconView.translatesAutoresizingMaskIntoConstraints = NO;
|
|
||||||
self.hostNameLabel.text = @"RazerBlade 16";
|
|
||||||
self.hostNameLabel.textColor = [UIColor whiteColor]; //theme
|
|
||||||
self.hostNameLabel.font = [UIFont boldSystemFontOfSize:18*_sizeFactor];
|
|
||||||
[self addSubview:self.hostNameLabel];
|
|
||||||
|
|
||||||
// 在线文字
|
|
||||||
self.statusLabel = [[UILabel alloc] initWithFrame:CGRectMake(205, 68, 100, 24)];
|
|
||||||
self.statusLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
|
||||||
self.statusLabel.text = [LocalizationHelper localizedStringForKey:@"Online"];
|
|
||||||
self.statusLabel.font = [UIFont systemFontOfSize:14*_sizeFactor weight:UIFontWeightMedium];
|
|
||||||
self.statusLabel.textColor = defaultGreen;
|
|
||||||
// self.statusLabel.font = [UIFont systemFontOfSize:16*_sizeFactor];
|
|
||||||
// 在线状态图标
|
|
||||||
self.statusIcon = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 16*_sizeFactor, 16*_sizeFactor)];
|
|
||||||
self.statusIcon.contentMode = UIViewContentModeScaleAspectFit;
|
|
||||||
self.statusIcon.translatesAutoresizingMaskIntoConstraints = NO;
|
|
||||||
self.statusIcon.image = [[UIImage imageNamed:@"wifi_green"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
|
|
||||||
// self.statusIcon.tintColor = defaultGreen;
|
|
||||||
[self addSubview:self.statusIcon];
|
|
||||||
[self addSubview:self.statusLabel];
|
|
||||||
[NSLayoutConstraint activateConstraints:@[
|
|
||||||
[self.statusLabel.leadingAnchor constraintEqualToAnchor:self.statusIcon.trailingAnchor constant:3*_sizeFactor],
|
|
||||||
[self.statusLabel.topAnchor constraintEqualToAnchor:self.hostNameLabel.bottomAnchor constant:0*_sizeFactor],
|
|
||||||
|
|
||||||
[self.statusIcon.centerYAnchor constraintEqualToAnchor:self.statusLabel.centerYAnchor constant:0],
|
|
||||||
[self.statusIcon.leadingAnchor constraintEqualToAnchor:self.hostNameLabel.leadingAnchor],
|
|
||||||
[self.statusIcon.widthAnchor constraintEqualToConstant:16*_sizeFactor]
|
|
||||||
]];
|
|
||||||
|
|
||||||
// 启动应用按钮
|
|
||||||
self.appButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
|
||||||
self.appButton.translatesAutoresizingMaskIntoConstraints = NO;
|
|
||||||
self.appButton.frame = CGRectMake(20, 200, 150, buttonHeight);
|
|
||||||
[self.appButton setTitle:[LocalizationHelper localizedStringForKey:@"Applications"] forState:UIControlStateNormal];
|
|
||||||
[self.appButton setTitleColor:[ThemeManager textColorGray] forState:UIControlStateNormal]; //theme
|
|
||||||
self.appButton.titleLabel.font = [UIFont systemFontOfSize:buttonLabelFontSize];
|
|
||||||
[self.appButton addTarget:self action:@selector(appButtonTapped) forControlEvents:UIControlEventPrimaryActionTriggered];
|
|
||||||
[self addSubview:self.appButton];
|
|
||||||
[NSLayoutConstraint activateConstraints:@[
|
|
||||||
[self.appButton.leadingAnchor constraintEqualToAnchor:self.iconBackgroundView.leadingAnchor constant:0],
|
|
||||||
[self.appButton.topAnchor constraintEqualToAnchor:self.iconBackgroundView.bottomAnchor constant:iconAndButtonSpacing],
|
|
||||||
[self.appButton.widthAnchor constraintEqualToConstant:appButtonWidth],
|
|
||||||
[self.appButton.heightAnchor constraintEqualToConstant:buttonHeight],
|
|
||||||
// [self.launchButton.titleLabel.leadingAnchor constraintEqualToAnchor:self.launchButton.imageView.trailingAnchor constant:3*_sizeFactor]
|
|
||||||
]];
|
|
||||||
|
|
||||||
|
|
||||||
// 开始串流按钮
|
|
||||||
self.launchButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
|
||||||
self.launchButton.translatesAutoresizingMaskIntoConstraints = NO;
|
|
||||||
self.launchButton.frame = CGRectMake(0, 0, 150, 50);
|
|
||||||
self.launchButton.backgroundColor = defaultBlue;
|
|
||||||
|
|
||||||
self.launchButton.layer.cornerRadius = 2*(uint16_t)(cardWidth*0.0377/2);
|
|
||||||
[self.launchButton setTitle:[LocalizationHelper localizedStringForKey:@" Launch"] forState:UIControlStateNormal];
|
|
||||||
[self.launchButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; // theme
|
|
||||||
self.launchButton.titleLabel.font = [UIFont boldSystemFontOfSize:buttonLabelFontSize];
|
|
||||||
self.launchButton.tintColor = [UIColor whiteColor];
|
|
||||||
[self.launchButton addTarget:self action:@selector(launchButtonTapped) forControlEvents:UIControlEventPrimaryActionTriggered];
|
|
||||||
[self addSubview:self.launchButton];
|
|
||||||
if (@available(iOS 13.0, *)) {
|
|
||||||
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:buttonHeight*0.263];
|
|
||||||
[self.launchButton setImage:[UIImage systemImageNamed:@"play.fill" withConfiguration:config] forState:UIControlStateNormal];
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// Fallback on earlier versions
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
[NSLayoutConstraint activateConstraints:@[
|
|
||||||
[self.launchButton.leadingAnchor constraintEqualToAnchor:self.appButton.trailingAnchor constant:15*_sizeFactor],
|
|
||||||
[self.launchButton.centerYAnchor constraintEqualToAnchor:self.appButton.centerYAnchor constant:0],
|
|
||||||
[self.launchButton.widthAnchor constraintEqualToConstant:launchButtonWidth],
|
|
||||||
[self.launchButton.heightAnchor constraintEqualToConstant:buttonHeight],
|
|
||||||
// [self.launchButton.titleLabel.leadingAnchor constraintEqualToAnchor:self.launchButton.imageView.trailingAnchor constant:3*_sizeFactor]
|
|
||||||
]];
|
|
||||||
|
|
||||||
// 配对按钮
|
|
||||||
self.pairButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
|
||||||
self.pairButton.translatesAutoresizingMaskIntoConstraints = NO;
|
|
||||||
self.pairButton.frame = CGRectMake(0, 0, 150, 50);
|
|
||||||
self.pairButton.backgroundColor = [ThemeManager textTintColorWithAlpha];
|
|
||||||
self.pairButton.layer.cornerRadius = 2*(uint16_t)(cardWidth*0.0377/2);
|
|
||||||
[self.pairButton setTitle:[LocalizationHelper localizedStringForKey:@" Pair with PIN"] forState:UIControlStateNormal];
|
|
||||||
[self.pairButton setTitleColor:defaultBlue forState:UIControlStateNormal]; // theme
|
|
||||||
self.pairButton.titleLabel.font = [UIFont boldSystemFontOfSize:buttonLabelFontSize];
|
|
||||||
if (@available(iOS 13.0, *)) {
|
|
||||||
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:buttonHeight/3.4 weight:UIImageSymbolWeightBold];
|
|
||||||
UIImage *templateImage = [UIImage systemImageNamed:@"lock.open.fill" withConfiguration:config];
|
|
||||||
UIImage *coloredImage = [templateImage imageWithTintColor:defaultBlue renderingMode:UIImageRenderingModeAlwaysOriginal];
|
|
||||||
[self.pairButton setImage:coloredImage forState:UIControlStateNormal];
|
|
||||||
} else {
|
|
||||||
// Fallback on earlier versions
|
|
||||||
}
|
|
||||||
self.pairButton.backgroundColor = [ThemeManager textTintColorWithAlpha];
|
|
||||||
[self.pairButton setTitleColor:defaultBlue forState:UIControlStateNormal]; // theme
|
|
||||||
|
|
||||||
[self.pairButton addTarget:self action:@selector(pairButtonTapped) forControlEvents:UIControlEventPrimaryActionTriggered];
|
|
||||||
[self addSubview:self.pairButton];
|
|
||||||
|
|
||||||
[NSLayoutConstraint activateConstraints:@[
|
|
||||||
[self.pairButton.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:_cardContentpadding],
|
|
||||||
[self.pairButton.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-_cardContentpadding],
|
|
||||||
[self.pairButton.topAnchor constraintEqualToAnchor:self.iconBackgroundView.bottomAnchor constant:iconAndButtonSpacing],
|
|
||||||
// [self.pairButton.widthAnchor constraintEqualToConstant:launchButtonWidth],
|
|
||||||
[self.pairButton.heightAnchor constraintEqualToConstant:buttonHeight],
|
|
||||||
]];
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 唤醒按钮
|
|
||||||
self.wakeupButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
|
||||||
self.wakeupButton.translatesAutoresizingMaskIntoConstraints = NO;
|
|
||||||
self.wakeupButton.frame = CGRectMake(0, 0, 150, 50);
|
|
||||||
self.wakeupButton.backgroundColor = [ThemeManager textTintColorWithAlpha];
|
|
||||||
self.wakeupButton.layer.cornerRadius = 2*(uint16_t)(cardWidth*0.0377/2);
|
|
||||||
[self.wakeupButton setTitle:[LocalizationHelper localizedStringForKey:@" Wake-on-LAN"] forState:UIControlStateNormal];
|
|
||||||
[self.wakeupButton setTitleColor:defaultBlue forState:UIControlStateNormal]; // theme
|
|
||||||
self.wakeupButton.titleLabel.font = [UIFont boldSystemFontOfSize:buttonLabelFontSize];
|
|
||||||
[self.wakeupButton addTarget:self action:@selector(wakeupButtonTapped) forControlEvents:UIControlEventPrimaryActionTriggered];
|
|
||||||
[self addSubview:self.wakeupButton];
|
|
||||||
|
|
||||||
[NSLayoutConstraint activateConstraints:@[
|
|
||||||
[self.wakeupButton.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:_cardContentpadding],
|
|
||||||
[self.wakeupButton.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-_cardContentpadding],
|
|
||||||
[self.wakeupButton.topAnchor constraintEqualToAnchor:self.iconBackgroundView.bottomAnchor constant:iconAndButtonSpacing],
|
|
||||||
// [self.pairButton.widthAnchor constraintEqualToConstant:launchButtonWidth],
|
|
||||||
[self.wakeupButton.heightAnchor constraintEqualToConstant:buttonHeight],
|
|
||||||
]];
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//分隔线
|
|
||||||
_separatorLine = [[UIView alloc] init];
|
|
||||||
_separatorLine.backgroundColor = [UIColor colorWithWhite:0.3 alpha:5.0];
|
|
||||||
_separatorLine.translatesAutoresizingMaskIntoConstraints = NO;
|
|
||||||
[self addSubview:_separatorLine];
|
|
||||||
[NSLayoutConstraint activateConstraints:@[
|
|
||||||
[_separatorLine.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:_cardContentpadding],
|
|
||||||
[_separatorLine.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-_cardContentpadding],
|
|
||||||
[_separatorLine.centerYAnchor constraintEqualToAnchor:_iconBackgroundView.bottomAnchor constant: iconAndButtonSpacing/2],
|
|
||||||
[_separatorLine.heightAnchor constraintEqualToConstant:1.0/UIScreen.mainScreen.scale]
|
|
||||||
]];
|
|
||||||
|
|
||||||
// 透明按钮
|
|
||||||
self.transparentButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
|
||||||
// self.transparentButton.frame = CGRectMake(50, 100, 100, 100); // 设置点击区域
|
|
||||||
self.transparentButton.translatesAutoresizingMaskIntoConstraints = NO;
|
|
||||||
// 设置透明背景和无内容
|
|
||||||
self.transparentButton.backgroundColor = [UIColor clearColor];
|
|
||||||
[self.transparentButton setTitle:@"" forState:UIControlStateNormal];
|
|
||||||
[self.transparentButton setImage:nil forState:UIControlStateNormal];
|
|
||||||
[self.transparentButton addTarget:self action:@selector(appButtonTapped) forControlEvents:UIControlEventPrimaryActionTriggered];
|
|
||||||
|
|
||||||
[self addSubview:self.transparentButton];
|
|
||||||
[NSLayoutConstraint activateConstraints:@[
|
|
||||||
[self.transparentButton.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:0],
|
|
||||||
[self.transparentButton.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:0],
|
|
||||||
[self.transparentButton.topAnchor constraintEqualToAnchor:self.topAnchor constant:0],
|
|
||||||
[self.transparentButton.bottomAnchor constraintEqualToAnchor:self.separatorLine.topAnchor],
|
|
||||||
]];
|
|
||||||
|
|
||||||
|
|
||||||
_widthConstraint.constant = _cardContentpadding*2 + appButtonWidth + 15*_sizeFactor + launchButtonWidth;
|
|
||||||
_heightConstraint.constant = _cardContentpadding*2 + _iconBackgroundView.frame.size.height + iconAndButtonSpacing + buttonHeight + 1;
|
|
||||||
|
|
||||||
_size = CGSizeMake(_widthConstraint.constant, _heightConstraint.constant);
|
|
||||||
|
|
||||||
[self updateTheme:ThemeManager.userInterfaceStyle];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)createBackgroundLayer{
|
|
||||||
backgroundLayer = [CAGradientLayer layer];
|
|
||||||
UIColor *gradientColorDark = [UIColor colorWithRed:0.0 green:0.319 blue:0.64 alpha:1.0];
|
|
||||||
UIColor *gradientColorLight = [gradientColorDark colorWithAlphaComponent:0.52];
|
|
||||||
CGColorRef gradientColorRef = ThemeManager.userInterfaceStyle == UIUserInterfaceStyleDark ? gradientColorDark.CGColor : gradientColorLight.CGColor;
|
|
||||||
backgroundLayer.colors = @[
|
|
||||||
(__bridge id)[UIColor clearColor].CGColor,
|
|
||||||
(__bridge id)[UIColor clearColor].CGColor,
|
|
||||||
(__bridge id)[UIColor clearColor].CGColor,
|
|
||||||
(__bridge id)gradientColorRef
|
|
||||||
];
|
|
||||||
|
|
||||||
backgroundLayer.locations = @[@0, @0.18, @0.5, @1];
|
|
||||||
backgroundLayer.startPoint = CGPointMake(0.25, 0.5);
|
|
||||||
backgroundLayer.endPoint = CGPointMake(0.75, 0.5);
|
|
||||||
|
|
||||||
CGAffineTransform transform = CGAffineTransformMake(-1.01, -1, 1, -3.67, 0.5, 2.83);
|
|
||||||
backgroundLayer.transform = CATransform3DMakeAffineTransform(transform);
|
|
||||||
[self.layer insertSublayer:backgroundLayer atIndex:0];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)layoutSubviews {
|
|
||||||
[super layoutSubviews];
|
|
||||||
backgroundLayer.bounds = CGRectInset(self.bounds, -0.5 * self.bounds.size.width, -0.5 * self.bounds.size.height);
|
|
||||||
backgroundLayer.position = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
|
|
||||||
[self updateContentsForHost:_host]; // quick update before looping
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)tintAdjustmentModeDidChange {
|
|
||||||
// [super tintAdjustmentModeDidChange];
|
|
||||||
NSLog(@"tintChanged........");
|
|
||||||
self.tintAdjustmentMode = UIViewTintAdjustmentModeNormal;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
- (void)updateTheme:(UIUserInterfaceStyle)userIterfaceStyle{
|
|
||||||
self.backgroundColor = [ThemeManager widgetBackgroundColor];
|
|
||||||
_hostNameLabel.textColor = [ThemeManager textColor];
|
|
||||||
[_appButton setTitleColor:[ThemeManager appPrimaryColor] forState:UIControlStateNormal];
|
|
||||||
_separatorLine.backgroundColor = [ThemeManager separatorColor];
|
|
||||||
backgroundLayer.hidden = NO;
|
|
||||||
[self updateContentsForHost:_host];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)didMoveToSuperview {
|
|
||||||
// Start our update loop when we are added to our cell
|
|
||||||
if (self.superview != nil && _host != nil) {
|
|
||||||
NSLog(@"start update loop");
|
|
||||||
[self updateLoop];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
|
|
||||||
/*
|
|
||||||
[super traitCollectionDidChange:previousTraitCollection];
|
|
||||||
|
|
||||||
if (@available(iOS 13.0, *)) {
|
|
||||||
if ([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) {
|
|
||||||
[self updateTheme:self.traitCollection.userInterfaceStyle];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
[self updateTheme:UIUserInterfaceStyleDark];
|
|
||||||
}*/
|
|
||||||
}
|
|
||||||
|
|
||||||
- (bool)isDarkTheme{
|
|
||||||
return self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void) updateLoop {
|
|
||||||
// Stop immediately if the view has been detached
|
|
||||||
if (self.superview == nil || [self.delegate isStreaming]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSLog(@"hostCard update loop %f", CACurrentMediaTime());
|
|
||||||
|
|
||||||
[self updateContentsForHost:_host];
|
|
||||||
|
|
||||||
// Queue the next refresh cycle
|
|
||||||
[self performSelector:@selector(updateLoop) withObject:self afterDelay:REFRESH_CYCLE];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void) updateContentsForHost:(TemporaryHost*)host {
|
|
||||||
_hostNameLabel.text = host.name;
|
|
||||||
backgroundLayer.hidden = !(host.state == StateOnline && host.pairState == PairStatePaired);
|
|
||||||
|
|
||||||
BOOL hostPaired = host.pairState == PairStatePaired;
|
|
||||||
bool hasValidMac = host.mac != nil && ![host.mac isEqualToString:@"00:00:00:00:00:00"];
|
|
||||||
|
|
||||||
switch (host.state) {
|
|
||||||
case StateOnline:
|
|
||||||
[_hostSpinner stopAnimating];
|
|
||||||
_statusLabel.textColor = defaultGreen;
|
|
||||||
_statusLabel.text = [LocalizationHelper localizedStringForKey:@"Online"];
|
|
||||||
self.statusIcon.image = [[UIImage imageNamed:@"wifi_green"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
|
|
||||||
self.statusIcon.hidden = NO;
|
|
||||||
_appButton.titleLabel.font = [UIFont systemFontOfSize:buttonLabelFontSize];
|
|
||||||
if(host.pairState == PairStatePaired){
|
|
||||||
_hostIconView.tintColor = [UIColor whiteColor];
|
|
||||||
_iconBackgroundView.backgroundColor = defaultBlue;
|
|
||||||
lockIconView.hidden = YES;
|
|
||||||
[_appButton setTitle:[LocalizationHelper localizedStringForKey:@"Applications"] forState:UIControlStateNormal];
|
|
||||||
[_launchButton setTitle:[LocalizationHelper localizedStringForKey:@" Launch"] forState:UIControlStateNormal];
|
|
||||||
[_appButton setEnabled:YES];
|
|
||||||
[_launchButton setEnabled:YES];
|
|
||||||
_appButton.hidden = NO;
|
|
||||||
_launchButton.hidden = NO;
|
|
||||||
_pairButton.hidden = YES;
|
|
||||||
_wakeupButton.hidden = YES;
|
|
||||||
|
|
||||||
[_appButton setTitleColor:defaultBlue forState:UIControlStateNormal]; // theme
|
|
||||||
if (@available(iOS 13.0, *)) {
|
|
||||||
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:buttonHeight*0.263];
|
|
||||||
[self.launchButton setImage:[UIImage systemImageNamed:@"play.fill" withConfiguration:config] forState:UIControlStateNormal];
|
|
||||||
} else {
|
|
||||||
}
|
|
||||||
_launchButton.backgroundColor = defaultBlue;
|
|
||||||
[_launchButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; // theme
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
_iconBackgroundView.backgroundColor = [ThemeManager appPrimaryColorWithAlpha];
|
|
||||||
UIColor* iconColor = [[UIColor whiteColor] colorWithAlphaComponent:[self isDarkTheme] ? 0.63 : 1];
|
|
||||||
_hostIconView.tintColor = iconColor;
|
|
||||||
lockIconView.tintColor = iconColor;
|
|
||||||
lockIconView.hidden = NO;
|
|
||||||
_appButton.hidden = YES;
|
|
||||||
_launchButton.hidden = YES;
|
|
||||||
_pairButton.hidden = NO;
|
|
||||||
_wakeupButton.hidden = YES;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case StateOffline:
|
|
||||||
[_hostSpinner stopAnimating];
|
|
||||||
// _iconBackgroundView.backgroundColor = _userIterfaceStyle == UIUserInterfaceStyleDark ? [UIColor appBackgroundColorDark] : [UIColor appBackgroundColorLight];
|
|
||||||
_iconBackgroundView.backgroundColor = [ThemeManager appBackgroundColor];
|
|
||||||
_statusLabel.textColor = [ThemeManager textColorGray];
|
|
||||||
_statusLabel.text = [LocalizationHelper localizedStringForKey: @"Offline"];
|
|
||||||
_statusIcon.tintColor = [ThemeManager textColorGray];
|
|
||||||
if (@available(iOS 13.0, *)) {
|
|
||||||
_statusIcon.image = [UIImage systemImageNamed:@"exclamationmark.triangle.fill"];
|
|
||||||
} else {
|
|
||||||
self.statusIcon.hidden = YES;
|
|
||||||
}
|
|
||||||
|
|
||||||
_hostIconView.tintColor = [ThemeManager lowProfileGray];
|
|
||||||
lockIconView.tintColor = [ThemeManager lowProfileGray];
|
|
||||||
lockIconView.hidden = hostPaired;
|
|
||||||
_appButton.hidden = YES;
|
|
||||||
_launchButton.hidden = YES;
|
|
||||||
_pairButton.hidden = YES;
|
|
||||||
_wakeupButton.hidden = NO;
|
|
||||||
|
|
||||||
if (@available(iOS 13.0, *)) {
|
|
||||||
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:buttonHeight/3.45 weight:UIImageSymbolWeightBold];
|
|
||||||
UIImage *templateImage = [UIImage systemImageNamed:@"power" withConfiguration:config];
|
|
||||||
UIImage *coloredImage = [templateImage imageWithTintColor:hasValidMac ? defaultBlue : [ThemeManager textColorGray] renderingMode:UIImageRenderingModeAlwaysOriginal];
|
|
||||||
[self.wakeupButton setImage:coloredImage forState:UIControlStateNormal];
|
|
||||||
} else {
|
|
||||||
// Fallback on earlier versions
|
|
||||||
}
|
|
||||||
|
|
||||||
self.wakeupButton.backgroundColor = hasValidMac ? [ThemeManager textTintColorWithAlpha] : [[ThemeManager textColorGray] colorWithAlphaComponent:0.2];
|
|
||||||
[self.wakeupButton setTitleColor: hasValidMac ? defaultBlue : [ThemeManager textColorGray] forState:UIControlStateNormal];
|
|
||||||
|
|
||||||
break;
|
|
||||||
case StateUnknown:
|
|
||||||
_hostSpinner.color = [UIColor whiteColor];
|
|
||||||
[_hostSpinner startAnimating];
|
|
||||||
// _iconBackgroundView.backgroundColor = _userIterfaceStyle == UIUserInterfaceStyleDark ? [UIColor appBackgroundColorDark] : [UIColor appBackgroundColorLight];
|
|
||||||
_iconBackgroundView.backgroundColor = [ThemeManager appPrimaryColorWithAlpha];
|
|
||||||
_statusLabel.textColor = [ThemeManager textColorGray];
|
|
||||||
_statusLabel.text = [LocalizationHelper localizedStringForKey: @"Detecting..."];
|
|
||||||
_statusIcon.tintColor = [ThemeManager textColorGray];
|
|
||||||
if (@available(iOS 13.0, *)) {
|
|
||||||
_statusIcon.image = [UIImage systemImageNamed:@"antenna.radiowaves.left.and.right"];
|
|
||||||
} else {
|
|
||||||
self.statusIcon.hidden = YES;
|
|
||||||
}
|
|
||||||
// _hostIconView.tintColor = [UIColor lowProfileGray];
|
|
||||||
_hostIconView.tintColor = defaultBlue;
|
|
||||||
lockIconView.hidden = YES;
|
|
||||||
_appButton.hidden = YES;
|
|
||||||
_launchButton.hidden = YES;
|
|
||||||
_pairButton.hidden = YES;
|
|
||||||
_wakeupButton.hidden = NO;
|
|
||||||
|
|
||||||
if (@available(iOS 13.0, *)) {
|
|
||||||
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:buttonHeight/3.45 weight:UIImageSymbolWeightBold];
|
|
||||||
UIImage *templateImage = [UIImage systemImageNamed:@"power" withConfiguration:config];
|
|
||||||
UIImage *coloredImage = [templateImage imageWithTintColor:hasValidMac ? defaultBlue : [ThemeManager textColorGray] renderingMode:UIImageRenderingModeAlwaysOriginal];
|
|
||||||
[self.wakeupButton setImage:coloredImage forState:UIControlStateNormal];
|
|
||||||
} else {
|
|
||||||
// Fallback on earlier versions
|
|
||||||
}
|
|
||||||
self.wakeupButton.backgroundColor = hasValidMac ? [ThemeManager textTintColorWithAlpha] : [[ThemeManager textColorGray] colorWithAlphaComponent:0.2];
|
|
||||||
[self.wakeupButton setTitleColor: hasValidMac ? defaultBlue : [ThemeManager textColorGray] forState:UIControlStateNormal];
|
|
||||||
break;
|
|
||||||
default:break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -0,0 +1,599 @@
|
|||||||
|
//
|
||||||
|
// HostCardView.m -> HostCardView.swift
|
||||||
|
// VoidLink
|
||||||
|
//
|
||||||
|
// Created by True砖家 on 2025.5.18
|
||||||
|
// Copyright © 2025 True砖家 @ Bilibili. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
@objc
|
||||||
|
protocol HostCardActionDelegate: NSObjectProtocol {
|
||||||
|
@objc(appButtonTappedForHost:)
|
||||||
|
func appButtonTapped(for host: TemporaryHost)
|
||||||
|
@objc(launchButtonTappedForHost:)
|
||||||
|
func launchButtonTapped(for host: TemporaryHost)
|
||||||
|
@objc(wakeupButtonTappedForHost:)
|
||||||
|
func wakeupButtonTapped(for host: TemporaryHost)
|
||||||
|
@objc(pairButtonTappedForHost:)
|
||||||
|
func pairButtonTapped(for host: TemporaryHost)
|
||||||
|
@objc(hostCardLongPressed:view:)
|
||||||
|
func hostCardLongPressed(_ host: TemporaryHost, view: UIView)
|
||||||
|
@objc(isStreaming)
|
||||||
|
func isStreaming() -> Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
@objcMembers
|
||||||
|
class HostCardView: UIView {
|
||||||
|
var sizeFactor: CGFloat = 1
|
||||||
|
private(set) var size: CGSize = .zero
|
||||||
|
weak var delegate: HostCardActionDelegate?
|
||||||
|
|
||||||
|
private var iconBackgroundView: UIView!
|
||||||
|
private var hostIconView: UIImageView!
|
||||||
|
private var hostNameLabel: UILabel!
|
||||||
|
private var statusIcon: UIImageView!
|
||||||
|
private var statusLabel: UILabel!
|
||||||
|
private var appButton: UIButton!
|
||||||
|
private var launchButton: UIButton!
|
||||||
|
private var pairButton: UIButton!
|
||||||
|
private var wakeupButton: UIButton!
|
||||||
|
private var transparentButton: UIButton!
|
||||||
|
private var widthConstraint: NSLayoutConstraint!
|
||||||
|
private var heightConstraint: NSLayoutConstraint!
|
||||||
|
private var separatorLine: UIView!
|
||||||
|
private var cardContentpadding: CGFloat = 0
|
||||||
|
|
||||||
|
private var host: TemporaryHost?
|
||||||
|
private var hostSpinner: UIActivityIndicatorView!
|
||||||
|
private var lockIconView: UIImageView!
|
||||||
|
private var computerIconMonitorCenterYOffset: CGFloat = 0
|
||||||
|
private var buttonHeight: CGFloat = 0
|
||||||
|
private var buttonLabelFontSize: CGFloat = 0
|
||||||
|
private var iconAndButtonSpacing: CGFloat = 0
|
||||||
|
private var defaultBlue: UIColor = ThemeManager.appPrimaryColor
|
||||||
|
private var defaultGreen: UIColor = UIColor(red: 52 / 255, green: 199 / 255, blue: 89 / 255, alpha: 1)
|
||||||
|
private var backgroundLayer: CAGradientLayer!
|
||||||
|
private var longPressFired = false
|
||||||
|
|
||||||
|
private static let refreshCycle: TimeInterval = 2
|
||||||
|
private let stateUnknown = State(rawValue: 0)!
|
||||||
|
private let stateOffline = State(rawValue: 1)!
|
||||||
|
private let stateOnline = State(rawValue: 2)!
|
||||||
|
private let pairStatePaired = PairState(rawValue: 2)!
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
commonInit()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
setupUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(initWithHost:)
|
||||||
|
convenience init(host: TemporaryHost) {
|
||||||
|
self.init(host: host, andSizeFactor: 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(initWithHost:andSizeFactor:)
|
||||||
|
init(host: TemporaryHost, andSizeFactor sizeFactor: CGFloat) {
|
||||||
|
self.sizeFactor = sizeFactor
|
||||||
|
self.host = host
|
||||||
|
super.init(frame: .zero)
|
||||||
|
commonInit()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func commonInit() {
|
||||||
|
buttonLabelFontSize = 15 * sizeFactor
|
||||||
|
longPressFired = false
|
||||||
|
computerIconMonitorCenterYOffset = isIPhone() ? -2.75 * sizeFactor : -3.2 * sizeFactor
|
||||||
|
iconAndButtonSpacing = 37 * sizeFactor
|
||||||
|
buttonHeight = 39 * sizeFactor
|
||||||
|
defaultBlue = ThemeManager.appPrimaryColor
|
||||||
|
defaultGreen = UIColor(red: 52 / 255, green: 199 / 255, blue: 89 / 255, alpha: 1)
|
||||||
|
|
||||||
|
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(hostCardLongPressed(_:)))
|
||||||
|
addGestureRecognizer(longPressRecognizer)
|
||||||
|
|
||||||
|
createBackgroundLayer()
|
||||||
|
setupUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isIPhone() -> Bool {
|
||||||
|
UIDevice.current.userInterfaceIdiom == .phone
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func appButtonTapped() {
|
||||||
|
guard host?.pairState == pairStatePaired, host?.state == .online, self.parentViewController?.hasNoPresentedVC == true else { return }
|
||||||
|
if longPressFired {
|
||||||
|
longPressFired = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let host else { return }
|
||||||
|
if let delegate {
|
||||||
|
delegate.appButtonTapped(for: host)
|
||||||
|
} else {
|
||||||
|
NSLog("Delegate not set or does not respond to appButtonTappedForHost:")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func launchButtonTapped() {
|
||||||
|
guard host?.pairState == pairStatePaired, host?.state == .online, self.parentViewController?.hasNoPresentedVC == true else { return }
|
||||||
|
if longPressFired {
|
||||||
|
longPressFired = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let host else { return }
|
||||||
|
if let delegate {
|
||||||
|
delegate.launchButtonTapped(for: host)
|
||||||
|
} else {
|
||||||
|
NSLog("Delegate not set or does not respond to launchButtonTappedForHost:")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func wakeupButtonTapped() {
|
||||||
|
guard host?.state != .online, self.parentViewController?.hasNoPresentedVC == true else { return }
|
||||||
|
if longPressFired {
|
||||||
|
longPressFired = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let host else { return }
|
||||||
|
if let delegate {
|
||||||
|
delegate.wakeupButtonTapped(for: host)
|
||||||
|
} else {
|
||||||
|
NSLog("Delegate not set or does not respond to launchButtonTappedForHost:")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func pairButtonTapped() {
|
||||||
|
guard !pairButton.isHidden, self.parentViewController?.hasNoPresentedVC == true else { return }
|
||||||
|
if longPressFired {
|
||||||
|
longPressFired = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let host else { return }
|
||||||
|
if let delegate {
|
||||||
|
delegate.pairButtonTapped(for: host)
|
||||||
|
} else {
|
||||||
|
NSLog("Delegate not set or does not respond to pairButtonTappedForHost:")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func hostCardLongPressed(_ gesture: UILongPressGestureRecognizer) {
|
||||||
|
guard gesture.state == .began, let host else { return }
|
||||||
|
longPressFired = true
|
||||||
|
if let delegate {
|
||||||
|
delegate.hostCardLongPressed(host, view: self)
|
||||||
|
} else {
|
||||||
|
NSLog("Delegate not set or does not respond")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(resizeBySizeFactor:)
|
||||||
|
func resizeBySizeFactor(_ factor: CGFloat) {
|
||||||
|
subviews.forEach { $0.removeFromSuperview() }
|
||||||
|
sizeFactor = factor
|
||||||
|
setupUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupUI() {
|
||||||
|
isUserInteractionEnabled = true
|
||||||
|
backgroundColor = ThemeManager.widgetBackgroundColor
|
||||||
|
cardContentpadding = 13 * sizeFactor
|
||||||
|
clipsToBounds = true
|
||||||
|
translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
let appButtonWidth = 100 * sizeFactor
|
||||||
|
let launchButtonWidth = 120 * sizeFactor
|
||||||
|
let cardWidth = cardContentpadding * 2 + appButtonWidth + 15 * sizeFactor + launchButtonWidth
|
||||||
|
layer.cornerRadius = 2 * CGFloat(UInt16(cardWidth * 0.0603 / 2))
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
layer.cornerCurve = .continuous
|
||||||
|
}
|
||||||
|
|
||||||
|
heightConstraint = heightAnchor.constraint(equalToConstant: 300)
|
||||||
|
heightConstraint.isActive = true
|
||||||
|
widthConstraint = widthAnchor.constraint(equalToConstant: 300)
|
||||||
|
widthConstraint.isActive = true
|
||||||
|
|
||||||
|
iconBackgroundView = UIView(frame: CGRect(x: cardContentpadding, y: cardContentpadding, width: 80 * sizeFactor, height: 80 * sizeFactor))
|
||||||
|
iconBackgroundView.backgroundColor = defaultBlue
|
||||||
|
iconBackgroundView.layer.cornerRadius = 2 * CGFloat(UInt16(20 * sizeFactor / 2))
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
iconBackgroundView.layer.cornerCurve = .continuous
|
||||||
|
}
|
||||||
|
addSubview(iconBackgroundView)
|
||||||
|
|
||||||
|
hostIconView = UIImageView()
|
||||||
|
hostIconView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
hostIconView.contentMode = .scaleAspectFit
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
hostIconView.image = UIImage(named: "display")?.withRenderingMode(.alwaysTemplate)
|
||||||
|
} else {
|
||||||
|
hostIconView.image = UIImage(named: "Computer")
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
hostIconView.heightAnchor.constraint(equalToConstant: 57 * sizeFactor),
|
||||||
|
hostIconView.widthAnchor.constraint(equalToConstant: 57 * sizeFactor),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
hostIconView.tintColor = .white
|
||||||
|
iconBackgroundView.addSubview(hostIconView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
hostIconView.centerXAnchor.constraint(equalTo: iconBackgroundView.centerXAnchor),
|
||||||
|
hostIconView.centerYAnchor.constraint(equalTo: iconBackgroundView.centerYAnchor),
|
||||||
|
hostIconView.heightAnchor.constraint(equalToConstant: 53.9 * sizeFactor),
|
||||||
|
hostIconView.widthAnchor.constraint(equalToConstant: 53.9 * sizeFactor),
|
||||||
|
])
|
||||||
|
|
||||||
|
let oldIOS: Bool
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
oldIOS = false
|
||||||
|
} else {
|
||||||
|
oldIOS = true
|
||||||
|
}
|
||||||
|
|
||||||
|
hostSpinner = UIActivityIndicatorView(style: .white)
|
||||||
|
hostSpinner.isUserInteractionEnabled = false
|
||||||
|
hostSpinner.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
hostSpinner.hidesWhenStopped = true
|
||||||
|
iconBackgroundView.addSubview(hostSpinner)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
hostSpinner.centerXAnchor.constraint(equalTo: iconBackgroundView.centerXAnchor),
|
||||||
|
hostSpinner.centerYAnchor.constraint(equalTo: iconBackgroundView.centerYAnchor, constant: oldIOS ? -5.5 * sizeFactor : computerIconMonitorCenterYOffset),
|
||||||
|
])
|
||||||
|
hostSpinner.transform = CGAffineTransform(scaleX: sizeFactor, y: sizeFactor)
|
||||||
|
hostSpinner.stopAnimating()
|
||||||
|
|
||||||
|
lockIconView = UIImageView()
|
||||||
|
lockIconView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
lockIconView.contentMode = .scaleAspectFit
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
lockIconView.image = UIImage(named: "lock.fill")?.withRenderingMode(.alwaysTemplate)
|
||||||
|
} else {
|
||||||
|
lockIconView.image = UIImage(named: "LockedOverlayIcon")
|
||||||
|
computerIconMonitorCenterYOffset = -5 * sizeFactor
|
||||||
|
}
|
||||||
|
lockIconView.tintColor = .white
|
||||||
|
iconBackgroundView.addSubview(lockIconView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
lockIconView.centerXAnchor.constraint(equalTo: iconBackgroundView.centerXAnchor),
|
||||||
|
lockIconView.centerYAnchor.constraint(equalTo: iconBackgroundView.centerYAnchor, constant: computerIconMonitorCenterYOffset),
|
||||||
|
lockIconView.widthAnchor.constraint(equalToConstant: 17.05 * sizeFactor),
|
||||||
|
lockIconView.heightAnchor.constraint(equalToConstant: 17.05 * sizeFactor),
|
||||||
|
])
|
||||||
|
lockIconView.isHidden = true
|
||||||
|
|
||||||
|
let hostNameLabelCoordX = cardContentpadding + iconBackgroundView.frame.size.width + 16 * sizeFactor
|
||||||
|
hostNameLabel = UILabel(frame: CGRect(x: hostNameLabelCoordX, y: cardContentpadding + iconBackgroundView.frame.size.height * 0.02, width: cardWidth - hostNameLabelCoordX - cardContentpadding, height: 30 * sizeFactor))
|
||||||
|
hostNameLabel.numberOfLines = 1
|
||||||
|
hostNameLabel.adjustsFontSizeToFitWidth = true
|
||||||
|
hostNameLabel.minimumScaleFactor = 0.8
|
||||||
|
hostNameLabel.lineBreakMode = .byTruncatingTail
|
||||||
|
hostNameLabel.text = "RazerBlade 16"
|
||||||
|
hostNameLabel.textColor = .white
|
||||||
|
hostNameLabel.font = .boldSystemFont(ofSize: 18 * sizeFactor)
|
||||||
|
addSubview(hostNameLabel)
|
||||||
|
|
||||||
|
statusLabel = UILabel(frame: CGRect(x: 205, y: 68, width: 100, height: 24))
|
||||||
|
statusLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
statusLabel.text = LocalizationHelper.localizedString(forKey: "Online")
|
||||||
|
statusLabel.font = .systemFont(ofSize: 14 * sizeFactor, weight: .medium)
|
||||||
|
statusLabel.textColor = defaultGreen
|
||||||
|
|
||||||
|
statusIcon = UIImageView(frame: CGRect(x: 0, y: 0, width: 16 * sizeFactor, height: 16 * sizeFactor))
|
||||||
|
statusIcon.contentMode = .scaleAspectFit
|
||||||
|
statusIcon.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
statusIcon.image = UIImage(named: "wifi_green")?.withRenderingMode(.alwaysOriginal)
|
||||||
|
addSubview(statusIcon)
|
||||||
|
addSubview(statusLabel)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
statusLabel.leadingAnchor.constraint(equalTo: statusIcon.trailingAnchor, constant: 3 * sizeFactor),
|
||||||
|
statusLabel.topAnchor.constraint(equalTo: hostNameLabel.bottomAnchor),
|
||||||
|
statusIcon.centerYAnchor.constraint(equalTo: statusLabel.centerYAnchor),
|
||||||
|
statusIcon.leadingAnchor.constraint(equalTo: hostNameLabel.leadingAnchor),
|
||||||
|
statusIcon.widthAnchor.constraint(equalToConstant: 16 * sizeFactor),
|
||||||
|
])
|
||||||
|
|
||||||
|
appButton = UIButton(type: .system)
|
||||||
|
appButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
appButton.frame = CGRect(x: 20, y: 200, width: 150, height: buttonHeight)
|
||||||
|
appButton.setTitle(LocalizationHelper.localizedString(forKey: "Applications"), for: .normal)
|
||||||
|
appButton.setTitleColor(ThemeManager.textColorGray, for: .normal)
|
||||||
|
appButton.titleLabel?.font = .systemFont(ofSize: buttonLabelFontSize)
|
||||||
|
appButton.addTarget(self, action: #selector(appButtonTapped), for: .primaryActionTriggered)
|
||||||
|
addSubview(appButton)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
appButton.leadingAnchor.constraint(equalTo: iconBackgroundView.leadingAnchor),
|
||||||
|
appButton.topAnchor.constraint(equalTo: iconBackgroundView.bottomAnchor, constant: iconAndButtonSpacing),
|
||||||
|
appButton.widthAnchor.constraint(equalToConstant: appButtonWidth),
|
||||||
|
appButton.heightAnchor.constraint(equalToConstant: buttonHeight),
|
||||||
|
])
|
||||||
|
|
||||||
|
launchButton = UIButton(type: .system)
|
||||||
|
launchButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
launchButton.frame = CGRect(x: 0, y: 0, width: 150, height: 50)
|
||||||
|
launchButton.backgroundColor = defaultBlue
|
||||||
|
launchButton.layer.cornerRadius = 2 * CGFloat(UInt16(cardWidth * 0.0377 / 2))
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
launchButton.layer.cornerCurve = .continuous
|
||||||
|
}
|
||||||
|
launchButton.setTitle(LocalizationHelper.localizedString(forKey: " Launch"), for: .normal)
|
||||||
|
launchButton.setTitleColor(.white, for: .normal)
|
||||||
|
launchButton.titleLabel?.font = .boldSystemFont(ofSize: buttonLabelFontSize)
|
||||||
|
launchButton.tintColor = .white
|
||||||
|
launchButton.addTarget(self, action: #selector(launchButtonTapped), for: .primaryActionTriggered)
|
||||||
|
addSubview(launchButton)
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
let config = UIImage.SymbolConfiguration(pointSize: buttonHeight * 0.263)
|
||||||
|
launchButton.setImage(UIImage(systemName: "play.fill", withConfiguration: config), for: .normal)
|
||||||
|
}
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
launchButton.leadingAnchor.constraint(equalTo: appButton.trailingAnchor, constant: 15 * sizeFactor),
|
||||||
|
launchButton.centerYAnchor.constraint(equalTo: appButton.centerYAnchor),
|
||||||
|
launchButton.widthAnchor.constraint(equalToConstant: launchButtonWidth),
|
||||||
|
launchButton.heightAnchor.constraint(equalToConstant: buttonHeight),
|
||||||
|
])
|
||||||
|
|
||||||
|
pairButton = UIButton(type: .system)
|
||||||
|
pairButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
pairButton.frame = CGRect(x: 0, y: 0, width: 150, height: 50)
|
||||||
|
pairButton.backgroundColor = ThemeManager.textTintColorWithAlpha
|
||||||
|
pairButton.layer.cornerRadius = 2 * CGFloat(UInt16(cardWidth * 0.0377 / 2))
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
pairButton.layer.cornerCurve = .continuous
|
||||||
|
}
|
||||||
|
pairButton.setTitle(LocalizationHelper.localizedString(forKey: " Pair with PIN"), for: .normal)
|
||||||
|
pairButton.setTitleColor(defaultBlue, for: .normal)
|
||||||
|
pairButton.titleLabel?.font = .boldSystemFont(ofSize: buttonLabelFontSize)
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
let config = UIImage.SymbolConfiguration(pointSize: buttonHeight / 3.4, weight: .bold)
|
||||||
|
let templateImage = UIImage(systemName: "lock.open.fill", withConfiguration: config)
|
||||||
|
let coloredImage = templateImage?.withTintColor(defaultBlue, renderingMode: .alwaysOriginal)
|
||||||
|
pairButton.setImage(coloredImage, for: .normal)
|
||||||
|
}
|
||||||
|
pairButton.addTarget(self, action: #selector(pairButtonTapped), for: .primaryActionTriggered)
|
||||||
|
addSubview(pairButton)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
pairButton.leadingAnchor.constraint(equalTo: leadingAnchor, constant: cardContentpadding),
|
||||||
|
pairButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -cardContentpadding),
|
||||||
|
pairButton.topAnchor.constraint(equalTo: iconBackgroundView.bottomAnchor, constant: iconAndButtonSpacing),
|
||||||
|
pairButton.heightAnchor.constraint(equalToConstant: buttonHeight),
|
||||||
|
])
|
||||||
|
|
||||||
|
wakeupButton = UIButton(type: .system)
|
||||||
|
wakeupButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
wakeupButton.frame = CGRect(x: 0, y: 0, width: 150, height: 50)
|
||||||
|
wakeupButton.backgroundColor = ThemeManager.textTintColorWithAlpha
|
||||||
|
wakeupButton.layer.cornerRadius = 2 * CGFloat(UInt16(cardWidth * 0.0377 / 2))
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
wakeupButton.layer.cornerCurve = .continuous
|
||||||
|
}
|
||||||
|
wakeupButton.setTitle(LocalizationHelper.localizedString(forKey: " Wake-on-LAN"), for: .normal)
|
||||||
|
wakeupButton.setTitleColor(defaultBlue, for: .normal)
|
||||||
|
wakeupButton.titleLabel?.font = .boldSystemFont(ofSize: buttonLabelFontSize)
|
||||||
|
wakeupButton.addTarget(self, action: #selector(wakeupButtonTapped), for: .primaryActionTriggered)
|
||||||
|
addSubview(wakeupButton)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
wakeupButton.leadingAnchor.constraint(equalTo: leadingAnchor, constant: cardContentpadding),
|
||||||
|
wakeupButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -cardContentpadding),
|
||||||
|
wakeupButton.topAnchor.constraint(equalTo: iconBackgroundView.bottomAnchor, constant: iconAndButtonSpacing),
|
||||||
|
wakeupButton.heightAnchor.constraint(equalToConstant: buttonHeight),
|
||||||
|
])
|
||||||
|
|
||||||
|
separatorLine = UIView()
|
||||||
|
separatorLine.backgroundColor = UIColor(white: 0.3, alpha: 5)
|
||||||
|
separatorLine.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(separatorLine)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
separatorLine.leadingAnchor.constraint(equalTo: leadingAnchor, constant: cardContentpadding),
|
||||||
|
separatorLine.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -cardContentpadding),
|
||||||
|
separatorLine.centerYAnchor.constraint(equalTo: iconBackgroundView.bottomAnchor, constant: iconAndButtonSpacing / 2),
|
||||||
|
separatorLine.heightAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale),
|
||||||
|
])
|
||||||
|
|
||||||
|
transparentButton = UIButton(type: .custom)
|
||||||
|
transparentButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
transparentButton.backgroundColor = .clear
|
||||||
|
transparentButton.setTitle("", for: .normal)
|
||||||
|
transparentButton.setImage(nil, for: .normal)
|
||||||
|
transparentButton.addTarget(self, action: #selector(appButtonTapped), for: .primaryActionTriggered)
|
||||||
|
addSubview(transparentButton)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
transparentButton.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
transparentButton.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
transparentButton.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
transparentButton.bottomAnchor.constraint(equalTo: separatorLine.topAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
widthConstraint.constant = cardContentpadding * 2 + appButtonWidth + 15 * sizeFactor + launchButtonWidth
|
||||||
|
heightConstraint.constant = cardContentpadding * 2 + iconBackgroundView.frame.size.height + iconAndButtonSpacing + buttonHeight + 1
|
||||||
|
size = CGSize(width: widthConstraint.constant, height: heightConstraint.constant)
|
||||||
|
|
||||||
|
updateTheme()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateBackgroundLayerTheme() {
|
||||||
|
let gradientColorDark = UIColor(red: 0, green: 0.319, blue: 0.64, alpha: 1)
|
||||||
|
let gradientColorLight = gradientColorDark.withAlphaComponent(0.52)
|
||||||
|
let gradientColor = ThemeManager.userInterfaceStyle() == .dark ? gradientColorDark.cgColor : gradientColorLight.cgColor
|
||||||
|
backgroundLayer.colors = [
|
||||||
|
UIColor.clear.cgColor,
|
||||||
|
UIColor.clear.cgColor,
|
||||||
|
UIColor.clear.cgColor,
|
||||||
|
gradientColor,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private func createBackgroundLayer() {
|
||||||
|
backgroundLayer = CAGradientLayer()
|
||||||
|
updateBackgroundLayerTheme()
|
||||||
|
backgroundLayer.locations = [0, 0.18, 0.5, 1]
|
||||||
|
backgroundLayer.startPoint = CGPoint(x: 0.25, y: 0.5)
|
||||||
|
backgroundLayer.endPoint = CGPoint(x: 0.75, y: 0.5)
|
||||||
|
let transform = CGAffineTransform(a: -1.01, b: -1, c: 1, d: -3.67, tx: 0.5, ty: 2.83)
|
||||||
|
backgroundLayer.transform = CATransform3DMakeAffineTransform(transform)
|
||||||
|
layer.insertSublayer(backgroundLayer, at: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
backgroundLayer.bounds = bounds.insetBy(dx: -0.5 * bounds.size.width, dy: -0.5 * bounds.size.height)
|
||||||
|
backgroundLayer.position = CGPoint(x: bounds.midX, y: bounds.midY)
|
||||||
|
updateContents(for: host)
|
||||||
|
}
|
||||||
|
|
||||||
|
func tintAdjustmentModeDidChange() {
|
||||||
|
tintAdjustmentMode = .normal
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateTheme() {
|
||||||
|
backgroundColor = ThemeManager.widgetBackgroundColor
|
||||||
|
hostNameLabel?.textColor = ThemeManager.textColor
|
||||||
|
appButton?.setTitleColor(ThemeManager.appPrimaryColor, for: .normal)
|
||||||
|
separatorLine?.backgroundColor = ThemeManager.hostCardSeparatorColor
|
||||||
|
|
||||||
|
backgroundLayer?.isHidden = false
|
||||||
|
updateBackgroundLayerTheme()
|
||||||
|
updateContents(for: host)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didMoveToSuperview() {
|
||||||
|
if superview != nil && host != nil {
|
||||||
|
NSLog("start update loop")
|
||||||
|
updateLoop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isDarkTheme() -> Bool {
|
||||||
|
ThemeManager.userInterfaceStyle() == .dark
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func updateLoop() {
|
||||||
|
if superview == nil || delegate?.isStreaming() == true {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now()+0.36) { [weak self] in
|
||||||
|
self?.updateContents(for: self?.host)
|
||||||
|
}
|
||||||
|
|
||||||
|
perform(#selector(updateLoop), with: self, afterDelay: Self.refreshCycle)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateContents(for host: TemporaryHost?) {
|
||||||
|
guard let host else { return }
|
||||||
|
|
||||||
|
hostNameLabel.text = host.name
|
||||||
|
backgroundLayer.isHidden = !(host.state == stateOnline && host.pairState == pairStatePaired)
|
||||||
|
|
||||||
|
let hostPaired = host.pairState == pairStatePaired
|
||||||
|
let hasValidMac = host.mac != nil && host.mac != "00:00:00:00:00:00"
|
||||||
|
|
||||||
|
switch host.state {
|
||||||
|
case stateOnline:
|
||||||
|
hostSpinner.stopAnimating()
|
||||||
|
statusLabel.textColor = defaultGreen
|
||||||
|
statusLabel.text = LocalizationHelper.localizedString(forKey: "Online")
|
||||||
|
statusIcon.image = UIImage(named: "wifi_green")?.withRenderingMode(.alwaysOriginal)
|
||||||
|
statusIcon.isHidden = false
|
||||||
|
appButton.titleLabel?.font = .systemFont(ofSize: buttonLabelFontSize)
|
||||||
|
if host.pairState == pairStatePaired {
|
||||||
|
hostIconView.tintColor = .white
|
||||||
|
iconBackgroundView.backgroundColor = defaultBlue
|
||||||
|
lockIconView.isHidden = true
|
||||||
|
appButton.setTitle(LocalizationHelper.localizedString(forKey: "Applications"), for: .normal)
|
||||||
|
launchButton.setTitle(LocalizationHelper.localizedString(forKey: " Launch"), for: .normal)
|
||||||
|
appButton.isEnabled = true
|
||||||
|
launchButton.isEnabled = true
|
||||||
|
appButton.isHidden = false
|
||||||
|
launchButton.isHidden = false
|
||||||
|
pairButton.isHidden = true
|
||||||
|
wakeupButton.isHidden = true
|
||||||
|
appButton.setTitleColor(defaultBlue, for: .normal)
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
let config = UIImage.SymbolConfiguration(pointSize: buttonHeight * 0.263)
|
||||||
|
launchButton.setImage(UIImage(systemName: "play.fill", withConfiguration: config), for: .normal)
|
||||||
|
}
|
||||||
|
launchButton.backgroundColor = defaultBlue
|
||||||
|
launchButton.setTitleColor(.white, for: .normal)
|
||||||
|
} else {
|
||||||
|
iconBackgroundView.backgroundColor = ThemeManager.appPrimaryColorWithAlpha
|
||||||
|
let iconColor = UIColor.white.withAlphaComponent(isDarkTheme() ? 0.63 : 1)
|
||||||
|
hostIconView.tintColor = iconColor
|
||||||
|
lockIconView.tintColor = iconColor
|
||||||
|
lockIconView.isHidden = false
|
||||||
|
appButton.isHidden = true
|
||||||
|
launchButton.isHidden = true
|
||||||
|
pairButton.isHidden = false
|
||||||
|
wakeupButton.isHidden = true
|
||||||
|
}
|
||||||
|
|
||||||
|
case stateOffline:
|
||||||
|
hostSpinner.stopAnimating()
|
||||||
|
iconBackgroundView.backgroundColor = ThemeManager.offlineHostIconBackgroundColor
|
||||||
|
statusLabel.textColor = ThemeManager.textColorGray
|
||||||
|
statusLabel.text = LocalizationHelper.localizedString(forKey: "Offline")
|
||||||
|
statusIcon.tintColor = ThemeManager.textColorGray
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
statusIcon.image = UIImage(systemName: "exclamationmark.triangle.fill")
|
||||||
|
} else {
|
||||||
|
statusIcon.isHidden = true
|
||||||
|
}
|
||||||
|
|
||||||
|
hostIconView.tintColor = ThemeManager.lowProfileGray
|
||||||
|
lockIconView.tintColor = ThemeManager.lowProfileGray
|
||||||
|
lockIconView.isHidden = hostPaired
|
||||||
|
appButton.isHidden = true
|
||||||
|
launchButton.isHidden = true
|
||||||
|
pairButton.isHidden = true
|
||||||
|
wakeupButton.isHidden = false
|
||||||
|
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
let config = UIImage.SymbolConfiguration(pointSize: buttonHeight / 3.45, weight: .bold)
|
||||||
|
let templateImage = UIImage(systemName: "power", withConfiguration: config)
|
||||||
|
let color = hasValidMac ? defaultBlue : ThemeManager.textColorGray
|
||||||
|
wakeupButton.setImage(templateImage?.withTintColor(color, renderingMode: .alwaysOriginal), for: .normal)
|
||||||
|
}
|
||||||
|
|
||||||
|
wakeupButton.backgroundColor = hasValidMac ? ThemeManager.textTintColorWithAlpha : ThemeManager.textColorGray.withAlphaComponent(0.2)
|
||||||
|
wakeupButton.setTitleColor(hasValidMac ? defaultBlue : ThemeManager.textColorGray, for: .normal)
|
||||||
|
|
||||||
|
case stateUnknown:
|
||||||
|
hostSpinner.color = .white
|
||||||
|
hostSpinner.startAnimating()
|
||||||
|
iconBackgroundView.backgroundColor = ThemeManager.appPrimaryColorWithAlpha
|
||||||
|
statusLabel.textColor = ThemeManager.textColorGray
|
||||||
|
statusLabel.text = LocalizationHelper.localizedString(forKey: "Detecting...")
|
||||||
|
statusIcon.tintColor = ThemeManager.textColorGray
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
statusIcon.image = UIImage(systemName: "antenna.radiowaves.left.and.right")
|
||||||
|
} else {
|
||||||
|
statusIcon.isHidden = true
|
||||||
|
}
|
||||||
|
hostIconView.tintColor = defaultBlue
|
||||||
|
lockIconView.isHidden = true
|
||||||
|
appButton.isHidden = true
|
||||||
|
launchButton.isHidden = true
|
||||||
|
pairButton.isHidden = true
|
||||||
|
wakeupButton.isHidden = false
|
||||||
|
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
let config = UIImage.SymbolConfiguration(pointSize: buttonHeight / 3.45, weight: .bold)
|
||||||
|
let templateImage = UIImage(systemName: "power", withConfiguration: config)
|
||||||
|
let color = hasValidMac ? defaultBlue : ThemeManager.textColorGray
|
||||||
|
wakeupButton.setImage(templateImage?.withTintColor(color, renderingMode: .alwaysOriginal), for: .normal)
|
||||||
|
}
|
||||||
|
wakeupButton.backgroundColor = hasValidMac ? ThemeManager.textTintColorWithAlpha : ThemeManager.textColorGray.withAlphaComponent(0.2)
|
||||||
|
wakeupButton.setTitleColor(hasValidMac ? defaultBlue : ThemeManager.textColorGray, for: .normal)
|
||||||
|
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "StickWheel.svg",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" fill="none" viewBox="0 0 120 120">
|
||||||
|
<g clip-path="url(#a)">
|
||||||
|
<g filter="url(#b)">
|
||||||
|
<path fill="#fff" fill-opacity="1"
|
||||||
|
d="M67.25 10.437c.099.075.215.123.338.142a50 50 0 0 1 27.233 13.54.74.74 0 0 1 .004 1.056l-.776.775a.76.76 0 0 1-1.065.004 47.405 47.405 0 0 0-65.97 0 .76.76 0 0 1-1.064-.004l-.776-.775a.74.74 0 0 1 .004-1.057A50 50 0 0 1 52.41 10.58a.76.76 0 0 0 .339-.142l6.8-5.1a.75.75 0 0 1 .9 0z"/>
|
||||||
|
</g>
|
||||||
|
<g filter="url(#c)">
|
||||||
|
<path fill="#fff" fill-opacity="1"
|
||||||
|
d="M61.955 6.804c2.487 2.295 5.657 3.663 8.96 4.402a50 50 0 0 1 23.51 12.533c.52.493.52 1.316.013 1.823a1.326 1.326 0 0 1-1.849.013 47.403 47.403 0 0 0-65.179 0 1.326 1.326 0 0 1-1.848-.013 1.27 1.27 0 0 1 .012-1.823 50 50 0 0 1 23.512-12.533c3.302-.739 6.47-2.106 8.958-4.401L60 5z"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<!-- defs stay the same -->
|
||||||
|
<filter id="b" width="82.089" height="32.983" x="18.955" y="-.813" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||||
|
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||||
|
<feGaussianBlur result="effect1_foregroundBlur_211_1847" stdDeviation="3"/>
|
||||||
|
</filter>
|
||||||
|
<filter id="c" width="69.634" height="22.444" x="25.183" y="5" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||||
|
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||||
|
<feOffset dy="1.5"/>
|
||||||
|
<feComposite in2="hardAlpha" operator="out"/>
|
||||||
|
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.08 0"/>
|
||||||
|
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_211_1847"/>
|
||||||
|
<feBlend in="SourceGraphic" in2="effect1_dropShadow_211_1847" result="shape"/>
|
||||||
|
</filter>
|
||||||
|
<clipPath id="a">
|
||||||
|
<path fill="#fff" d="M0 0h120v120H0z"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "apple.terminal.svg",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"preserves-vector-representation" : true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--Generator: Apple Native CoreSVG 341-->
|
||||||
|
<!DOCTYPE svg
|
||||||
|
PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||||
|
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 30.5252 23.9534">
|
||||||
|
<g>
|
||||||
|
<rect height="23.9534" opacity="0" width="30.5252" x="0" y="0"/>
|
||||||
|
<path d="M4.30122 23.9534L25.8685 23.9534C28.6839 23.9534 30.1697 22.4762 30.1697 19.6955L30.1697 4.27466C30.1697 1.48987 28.6839 0.00807405 25.8685 0.00807405L4.30122 0.00807405C1.49449 0.00807405 0 1.48987 0 4.27466L0 19.6955C0 22.4762 1.49449 23.9534 4.30122 23.9534ZM4.53653 21.1263C3.42444 21.1263 2.82713 20.5624 2.82713 19.3955L2.82713 4.57058C2.82713 3.40367 3.42444 2.8352 4.53653 2.8352L25.6332 2.8352C26.7366 2.8352 27.3426 3.40367 27.3426 4.57058L27.3426 19.3955C27.3426 20.5624 26.7366 21.1263 25.6332 21.1263Z" fill="white" fill-opacity="0.85"/>
|
||||||
|
<path d="M5.97169 10.1889C4.79979 10.8986 5.79916 12.6879 7.08069 11.8853L10.6199 9.66475C11.4444 9.14716 11.4104 7.92273 10.6199 7.42649L7.08069 5.214C5.80319 4.40735 4.79113 6.19202 5.96765 6.90232L8.69215 8.54533ZM11.2928 11.7555C11.2928 12.2887 11.7209 12.7295 12.2708 12.7295L16.9785 12.7295C17.5244 12.7295 17.9485 12.2887 17.9485 11.7555C17.9485 11.2057 17.5244 10.7729 16.9785 10.7729L12.2708 10.7729C11.7249 10.7729 11.2928 11.2057 11.2928 11.7555Z" fill="white" fill-opacity="0.85"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "circle.lefthalf.filled.svg",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"preserves-vector-representation" : true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--Generator: Apple Native CoreSVG 341-->
|
||||||
|
<!DOCTYPE svg
|
||||||
|
PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||||
|
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 26.5852 26.2402">
|
||||||
|
<g>
|
||||||
|
<rect height="26.2402" opacity="0" width="26.5852" x="0" y="0"/>
|
||||||
|
<path d="M13.1137 24.8338L13.1137 1.38328C6.44134 1.38328 1.38556 6.44134 1.38556 13.1137C1.38556 19.7861 6.44134 24.8338 13.1137 24.8338ZM13.1137 26.2275C20.3549 26.2275 26.2298 20.3526 26.2298 13.1137C26.2298 5.87488 20.3549 0 13.1137 0C5.87488 0 0 5.87488 0 13.1137C0 20.3526 5.87488 26.2275 13.1137 26.2275ZM13.1137 23.6443C7.29091 23.6443 2.58322 18.9366 2.58322 13.1137C2.58322 7.29091 7.29091 2.58322 13.1137 2.58322C18.9366 2.58322 23.6465 7.29091 23.6465 13.1137C23.6465 18.9366 18.9366 23.6443 13.1137 23.6443Z" fill="white"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 940 B |
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "controllerMouse.svg",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"preserves-vector-representation" : true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--Generator: Apple Native CoreSVG 341-->
|
||||||
|
<!DOCTYPE svg
|
||||||
|
PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||||
|
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 17.225 22.7356">
|
||||||
|
<g>
|
||||||
|
<rect height="22.7356" opacity="0" width="17.225" x="0" y="0"/>
|
||||||
|
<path d="M7.98991 16.743L14.7319 16.743C16.6897 16.743 17.5002 14.9583 16.3368 13.7776L3.19844 0.64154C1.94161-0.624432 0 0.0799009 0 1.90751L0 20.5374C0 22.1976 1.83421 22.8801 3.21901 21.5058Z" fill="white"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 612 B |
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "disableControllerMouse.svg",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"preserves-vector-representation" : true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--Generator: Apple Native CoreSVG 341-->
|
||||||
|
<!DOCTYPE svg
|
||||||
|
PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||||
|
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 30.0974 26.0826">
|
||||||
|
<g>
|
||||||
|
<rect height="26.0826" opacity="0" width="30.0974" x="0" y="0"/>
|
||||||
|
<path d="M14.0091 18.5296L9.61512 22.9119C8.37098 24.156 6.72059 23.5467 6.72059 22.0486L6.72059 11.264ZM9.62782 2.61209L22.5136 15.5105C23.5673 16.5769 22.8436 18.1765 21.079 18.1765L20.638 18.1765L6.72059 4.29423L6.72059 3.75467C6.72059 2.09158 8.47254 1.46951 9.62782 2.61209Z" fill="white"/>
|
||||||
|
<path d="M1.75672 4.2117L21.9169 24.3084C22.2977 24.7019 22.9325 24.7019 23.3007 24.3084C23.6688 23.9275 23.6815 23.3181 23.3007 22.9246L3.15321 2.82791C2.77235 2.44705 2.15028 2.44705 1.75672 2.82791C1.38856 3.20877 1.38856 3.84354 1.75672 4.2117Z" fill="white"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 966 B |
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "gamecontroller.circle.svg",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--Generator: Apple Native CoreSVG 341-->
|
||||||
|
<!DOCTYPE svg
|
||||||
|
PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||||
|
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 30.249 29.8975">
|
||||||
|
<g>
|
||||||
|
<rect height="29.8975" opacity="0" width="30.249" x="0" y="0"/>
|
||||||
|
<path d="M14.9414 29.8828C23.1885 29.8828 29.8828 23.1885 29.8828 14.9414C29.8828 6.69434 23.1885 0 14.9414 0C6.69434 0 0 6.69434 0 14.9414C0 23.1885 6.69434 29.8828 14.9414 29.8828ZM14.9414 27.3926C8.05664 27.3926 2.49023 21.8262 2.49023 14.9414C2.49023 8.05664 8.05664 2.49023 14.9414 2.49023C21.8262 2.49023 27.3926 8.05664 27.3926 14.9414C27.3926 21.8262 21.8262 27.3926 14.9414 27.3926Z" fill="white"/>
|
||||||
|
<path d="M7.9834 20.127C8.8623 20.127 9.47754 19.79 10.0342 19.1309L11.2207 17.6807C11.3818 17.4756 11.5723 17.373 11.7773 17.373L18.0762 17.373C18.2812 17.373 18.4717 17.4756 18.6328 17.6807L19.834 19.1309C20.376 19.79 20.9912 20.127 21.8701 20.127C23.3643 20.127 24.3604 19.1309 24.3604 17.6074C24.3604 16.9482 24.2139 16.2158 23.9648 15.3516C23.5547 14.0332 22.8516 12.2021 22.1777 10.7812C21.6211 9.60938 21.3428 9.06738 19.9658 8.75977C18.7207 8.48145 17.0215 8.29102 14.9268 8.29102C12.832 8.29102 11.1328 8.48145 9.8877 8.75977C8.51074 9.06738 8.23242 9.60938 7.67578 10.7812C7.00195 12.2021 6.31348 14.0332 5.90332 15.3516C5.6543 16.2158 5.49316 16.9482 5.49316 17.6074C5.49316 19.1309 6.48926 20.127 7.9834 20.127ZM9.44824 12.8906C9.44824 12.5391 9.66797 12.3047 10.0342 12.3047L11.2207 12.3047L11.2207 11.1621C11.2207 10.7959 11.4258 10.5615 11.7773 10.5615C12.1289 10.5615 12.334 10.7959 12.334 11.1621L12.334 12.3047L13.4619 12.3047C13.8574 12.3047 14.0918 12.5391 14.0918 12.8906C14.0918 13.2568 13.8574 13.4766 13.4619 13.4766L12.334 13.4766L12.334 14.6484C12.334 15 12.1289 15.2344 11.7773 15.2344C11.4258 15.2344 11.2207 15 11.2207 14.6484L11.2207 13.4766L10.0342 13.4766C9.66797 13.4766 9.44824 13.2568 9.44824 12.8906ZM19.1162 12.8174C18.5889 12.8174 18.1348 12.3926 18.1348 11.8359C18.1348 11.3086 18.5889 10.8691 19.1162 10.8691C19.6582 10.8691 20.0977 11.3086 20.0977 11.8359C20.0977 12.3926 19.6582 12.8174 19.1162 12.8174ZM17.1387 14.8242C16.5967 14.8242 16.1572 14.3848 16.1572 13.8428C16.1572 13.3008 16.5967 12.876 17.1387 12.876C17.6807 12.876 18.1201 13.3008 18.1201 13.8428C18.1201 14.3848 17.6807 14.8242 17.1387 14.8242Z" fill="white"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "gamecontroller.circle.tag.svg",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--Generator: Apple Native CoreSVG 341-->
|
||||||
|
<!DOCTYPE svg
|
||||||
|
PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||||
|
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 19.2876 18.9351">
|
||||||
|
<g>
|
||||||
|
<rect height="18.9351" opacity="0" width="19.2876" x="0" y="0"/>
|
||||||
|
<path d="M9.46289 18.9258C14.686 18.9258 18.9258 14.686 18.9258 9.46289C18.9258 4.23975 14.686 0 9.46289 0C4.23975 0 0 4.23975 0 9.46289C0 14.686 4.23975 18.9258 9.46289 18.9258ZM9.46289 17.3486C5.10254 17.3486 1.57715 13.8232 1.57715 9.46289C1.57715 5.10254 5.10254 1.57715 9.46289 1.57715C13.8232 1.57715 17.3486 5.10254 17.3486 9.46289C17.3486 13.8232 13.8232 17.3486 9.46289 17.3486Z" fill="white"/>
|
||||||
|
<path d="M5.05615 12.7471C5.61279 12.7471 6.00244 12.5337 6.35498 12.1162L7.10645 11.1978C7.2085 11.0679 7.3291 11.0029 7.45898 11.0029L11.4482 11.0029C11.5781 11.0029 11.6987 11.0679 11.8008 11.1978L12.5615 12.1162C12.9048 12.5337 13.2944 12.7471 13.8511 12.7471C14.7974 12.7471 15.4282 12.1162 15.4282 11.1514C15.4282 10.7339 15.3354 10.27 15.1777 9.72266C14.918 8.8877 14.4727 7.72803 14.0459 6.82812C13.6934 6.08594 13.5171 5.74268 12.645 5.54785C11.8564 5.37158 10.7803 5.25098 9.45361 5.25098C8.12695 5.25098 7.05078 5.37158 6.26221 5.54785C5.39014 5.74268 5.21387 6.08594 4.86133 6.82812C4.43457 7.72803 3.99854 8.8877 3.73877 9.72266C3.58105 10.27 3.479 10.7339 3.479 11.1514C3.479 12.1162 4.10986 12.7471 5.05615 12.7471ZM5.98389 8.16406C5.98389 7.94141 6.12305 7.79297 6.35498 7.79297L7.10645 7.79297L7.10645 7.06934C7.10645 6.8374 7.23633 6.68896 7.45898 6.68896C7.68164 6.68896 7.81152 6.8374 7.81152 7.06934L7.81152 7.79297L8.52588 7.79297C8.77637 7.79297 8.9248 7.94141 8.9248 8.16406C8.9248 8.396 8.77637 8.53516 8.52588 8.53516L7.81152 8.53516L7.81152 9.27734C7.81152 9.5 7.68164 9.64844 7.45898 9.64844C7.23633 9.64844 7.10645 9.5 7.10645 9.27734L7.10645 8.53516L6.35498 8.53516C6.12305 8.53516 5.98389 8.396 5.98389 8.16406ZM12.1069 8.11768C11.7729 8.11768 11.4854 7.84863 11.4854 7.49609C11.4854 7.16211 11.7729 6.88379 12.1069 6.88379C12.4502 6.88379 12.7285 7.16211 12.7285 7.49609C12.7285 7.84863 12.4502 8.11768 12.1069 8.11768ZM10.8545 9.38867C10.5112 9.38867 10.2329 9.11035 10.2329 8.76709C10.2329 8.42383 10.5112 8.15479 10.8545 8.15479C11.1978 8.15479 11.4761 8.42383 11.4761 8.76709C11.4761 9.11035 11.1978 9.38867 10.8545 9.38867Z" fill="white"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "gyroscope.svg",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--Generator: Apple Native CoreSVG 341-->
|
||||||
|
<!DOCTYPE svg
|
||||||
|
PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||||
|
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 22.7531 22.4167">
|
||||||
|
<g>
|
||||||
|
<rect height="22.4167" opacity="0" width="22.7531" x="0" y="0"/>
|
||||||
|
<path d="M4.66382 17.7636C6.51148 19.6044 10.4016 18.2265 14.3104 14.3211C18.2231 10.4016 19.6044 6.51148 17.7568 4.67065C15.8832 2.7976 12.0151 4.17898 8.08824 8.09898C4.17557 12.0224 2.79027 15.8832 4.66382 17.7636ZM5.99819 16.4151C4.94393 15.3574 6.33263 12.5482 9.42944 9.44409C12.5521 6.3214 15.3647 4.94734 16.4151 6.0016C17.4694 7.04854 16.0841 9.85382 12.9687 12.9833C9.86457 16.0841 7.04854 17.4728 5.99819 16.4151ZM17.7456 17.7602C19.6118 15.8832 18.2304 12.0258 14.3104 8.09507C10.4016 4.17898 6.52613 2.7976 4.64917 4.66724C2.80493 6.51148 4.19022 10.4016 8.1024 14.3138C12.0151 18.2265 15.8979 19.5971 17.7456 17.7602ZM16.4044 16.4151C15.3608 17.4728 12.5482 16.0841 9.42944 12.9687C6.31798 9.85724 4.94393 7.04854 5.99086 6.0016C7.04121 4.94734 9.85724 6.3214 12.9687 9.44018C16.0767 12.5516 17.4694 15.3608 16.4044 16.4151ZM19.0764 4.59024L21.8772 1.7895C22.2092 1.45357 22.2092 0.882298 21.8772 0.550279C21.5271 0.203607 20.97 0.210933 20.6272 0.550279L17.7762 3.39741ZM3.61603 17.5507L0.550279 20.6272C0.200191 20.9665 0.210933 21.5237 0.550279 21.863C0.878882 22.2058 1.43941 22.2097 1.7895 21.8737L5.03247 18.6342ZM11.203 22.406C17.3909 22.406 22.4094 17.3875 22.4094 11.203C22.4094 5.01849 17.3909 0 11.203 0C5.01849 0 0 5.01849 0 11.203C0 17.3875 5.01849 22.406 11.203 22.406ZM11.203 19.9444C6.36936 19.9444 2.46154 16.0366 2.46154 11.203C2.46154 6.36936 6.36936 2.46154 11.203 2.46154C16.0366 2.46154 19.9478 6.36936 19.9478 11.203C19.9478 16.0366 16.0366 19.9444 11.203 19.9444Z" fill="white" fill-opacity="0.85"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "macwindow.and.pointer.arrow.svg",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"preserves-vector-representation" : true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--Generator: Apple Native CoreSVG 341-->
|
||||||
|
<!DOCTYPE svg
|
||||||
|
PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||||
|
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 36.2556 34.0064">
|
||||||
|
<g>
|
||||||
|
<rect height="34.0064" opacity="0" width="36.2556" x="0" y="0"/>
|
||||||
|
<path d="M32.9778 10.6393L32.9778 21.2524L30.4905 18.7118L30.4905 10.7627C30.4905 8.79238 29.3426 7.64447 27.3723 7.64447L8.51517 7.64447C6.54486 7.64447 5.39694 8.79238 5.39694 10.7627L5.39694 23.2564C5.39694 25.229 6.54486 26.3665 8.51517 26.3665L24.569 26.3665L24.5407 28.4391C24.5386 28.5831 24.5481 28.722 24.5715 28.8538L8.38137 28.8538C4.91606 28.8538 2.9097 26.8578 2.9097 23.3798L2.9097 10.6393C2.9097 7.1613 4.91606 5.15722 8.38137 5.15722L27.7709 5.15722C31.057 5.15722 32.9778 7.1613 32.9778 10.6393ZM10.0795 11.0857C10.0795 11.7166 9.55111 12.2658 8.90161 12.2658C8.25212 12.2658 7.72146 11.7166 7.72146 11.0857C7.72146 10.4489 8.25212 9.89739 8.90161 9.89739C9.55111 9.89739 10.0795 10.4489 10.0795 11.0857ZM13.8213 11.0857C13.8213 11.7166 13.2929 12.2658 12.6434 12.2658C11.9917 12.2658 11.4633 11.7166 11.4633 11.0857C11.4633 10.4489 11.9917 9.89739 12.6434 9.89739C13.2929 9.89739 13.8213 10.4489 13.8213 11.0857ZM17.5631 11.0857C17.5631 11.7166 17.0324 12.2658 16.3852 12.2658C15.7335 12.2658 15.2051 11.7166 15.2051 11.0857C15.2051 10.4489 15.7335 9.89739 16.3852 9.89739C17.0324 9.89739 17.5631 10.4489 17.5631 11.0857Z" fill="white"/>
|
||||||
|
<path d="M32.5208 32.1175C33.1251 31.8661 33.4145 31.1857 33.1527 30.5836L31.2545 26.174L33.8561 26.0963C34.3956 26.0847 34.6538 25.5545 34.2699 25.1498L27.3886 18.1123C27.0219 17.7456 26.4847 17.9589 26.472 18.4685L26.3463 28.4749C26.3313 29.0318 26.9747 29.2161 27.3355 28.8462L29.1464 26.9975L30.9661 31.4878C31.2002 32.0852 31.9083 32.3747 32.5208 32.1175Z" fill="white"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "rectangle.portrait.and.arrow.right.svg",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"preserves-vector-representation" : true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--Generator: Apple Native CoreSVG 341-->
|
||||||
|
<!DOCTYPE svg
|
||||||
|
PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||||
|
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 34.2565 27.6836">
|
||||||
|
<g>
|
||||||
|
<rect height="27.6836" opacity="0" width="34.2565" x="0" y="0"/>
|
||||||
|
<path d="M23.7582 5.55752L23.7582 10.1349L20.9311 10.1349L20.9311 5.6619C20.9311 3.85035 19.9125 2.82713 18.0963 2.82713L7.33593 2.82713C5.52438 2.82713 4.49712 3.85035 4.49712 5.6619L4.49712 22.0171C4.49712 23.8373 5.52438 24.8518 7.33593 24.8518L18.0963 24.8518C19.9125 24.8518 20.9311 23.8373 20.9311 22.0171L20.9311 17.5359L23.7582 17.5359L23.7582 22.1301C23.7582 25.6945 21.7778 27.679 18.2134 27.679L7.22751 27.679C3.6631 27.679 1.66999 25.6945 1.66999 22.1301L1.66999 5.55752C1.66999 1.99311 3.6631 0 7.22751 0L18.2134 0C21.7778 0 23.7582 1.99311 23.7582 5.55752Z" fill="white"/>
|
||||||
|
<path d="M13.1283 13.8354C13.1283 14.5549 13.714 15.1331 14.4006 15.1331L27.0257 15.1331L30.8363 14.8838C31.4191 14.8423 31.8888 14.4142 31.8888 13.8354C31.8888 13.2527 31.4191 12.8245 30.8363 12.783L27.0257 12.5292L14.4006 12.5292C13.714 12.5292 13.1283 13.116 13.1283 13.8354ZM25.8536 9.43889C25.8536 9.7568 25.9875 10.0845 26.231 10.3067L28.0471 12.0034L30.4369 13.8354L28.0471 15.6588L26.231 17.3642C25.9875 17.5864 25.8536 17.9014 25.8536 18.2193C25.8536 18.8327 26.3152 19.3681 26.9649 19.3681C27.2961 19.3681 27.5396 19.2337 27.7704 19.0029L31.7792 14.8423C32.1139 14.5036 32.2351 14.1782 32.2351 13.8354C32.2351 13.4841 32.1139 13.1633 31.7792 12.8286L27.7704 8.65533C27.5396 8.42453 27.2961 8.30279 26.9649 8.30279C26.3152 8.30279 25.8536 8.81687 25.8536 9.43889Z" fill="white"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -7,12 +7,15 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#import "StreamView.h"
|
#import "StreamView.h"
|
||||||
|
#import "TemporarySettings.h"
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
@interface AbsoluteTouchHandler : UIResponder
|
@interface AbsoluteTouchHandler : UIResponder
|
||||||
|
@property (class, nonatomic, assign) int mouseButtonForCursorMove;
|
||||||
|
|
||||||
-(id)initWithView:(StreamView*)view;
|
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings;
|
||||||
|
- (void)pauseLeftButtonDrag;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#import "AbsoluteTouchHandler.h"
|
#import "AbsoluteTouchHandler.h"
|
||||||
|
#import "VoidLink-Swift.h"
|
||||||
|
|
||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@
|
|||||||
#define LONG_PRESS_ACTIVATION_DELAY 0.650f
|
#define LONG_PRESS_ACTIVATION_DELAY 0.650f
|
||||||
|
|
||||||
// How far the finger can move before it cancels a right click
|
// How far the finger can move before it cancels a right click
|
||||||
#define LONG_PRESS_ACTIVATION_DELTA 0.01f
|
#define LONG_PRESS_ACTIVATION_DELTA 0.02f
|
||||||
|
|
||||||
// How long the double tap deadzone stays in effect between touch up and touch down
|
// How long the double tap deadzone stays in effect between touch up and touch down
|
||||||
#define DOUBLE_TAP_DEAD_ZONE_DELAY 0.250f
|
#define DOUBLE_TAP_DEAD_ZONE_DELAY 0.250f
|
||||||
@@ -25,8 +26,13 @@
|
|||||||
// How far the finger can move before it can override the double tap deadzone
|
// How far the finger can move before it can override the double tap deadzone
|
||||||
#define DOUBLE_TAP_DEAD_ZONE_DELTA 0.025f
|
#define DOUBLE_TAP_DEAD_ZONE_DELTA 0.025f
|
||||||
|
|
||||||
|
static int mouseButtonForCursorMove = BUTTON_LEFT;
|
||||||
|
|
||||||
@implementation AbsoluteTouchHandler {
|
@implementation AbsoluteTouchHandler {
|
||||||
StreamView* streamView;
|
__weak StreamView* streamView;
|
||||||
|
|
||||||
|
bool multiTouchesDetected;
|
||||||
|
bool passthroughGestures;
|
||||||
|
|
||||||
NSTimer* longPressTimer;
|
NSTimer* longPressTimer;
|
||||||
UITouch* lastTouchDown;
|
UITouch* lastTouchDown;
|
||||||
@@ -34,37 +40,83 @@
|
|||||||
UITouch* lastTouchUp;
|
UITouch* lastTouchUp;
|
||||||
CGPoint lastTouchUpLocation;
|
CGPoint lastTouchUpLocation;
|
||||||
|
|
||||||
|
UITouch* capturedTouch;
|
||||||
|
|
||||||
|
CGPoint touchBeganLocation;
|
||||||
|
CGPoint movingTouchLocation;
|
||||||
|
|
||||||
|
NSTimeInterval touchBeganTimeStamp;
|
||||||
|
NSTimeInterval leftClickTimeThreshold;
|
||||||
|
|
||||||
// upper screen edge check
|
// upper screen edge check
|
||||||
bool touchPointSpawnedAtUpperScreenEdge;
|
bool touchPointSpawnedAtUpperScreenEdge;
|
||||||
CGFloat slideGestureVerticalThreshold;
|
CGFloat slideGestureVerticalThreshold;
|
||||||
CGFloat screenWidthWithThreshold;
|
CGFloat screenWidthWithThreshold;
|
||||||
CGFloat EDGE_TOLERANCE;
|
CGFloat _edgeTolerance;
|
||||||
|
|
||||||
|
bool _delayMouseLeftClick;
|
||||||
|
NSTimeInterval leftClickDelay;
|
||||||
|
|
||||||
|
bool dragButtonDown;
|
||||||
|
UInt8 currentTouchesCount;
|
||||||
|
|
||||||
|
bool rightButtonClicked;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (id)initWithView:(StreamView*)view {
|
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings {
|
||||||
self = [self init];
|
self = [self init];
|
||||||
self->streamView = view;
|
self->streamView = view;
|
||||||
|
|
||||||
|
multiTouchesDetected = false;
|
||||||
|
passthroughGestures = settings.passthroughGestures;
|
||||||
|
|
||||||
|
_delayMouseLeftClick = settings.delayLeftClick;
|
||||||
|
// _delayMouseLeftClick = true;
|
||||||
|
dragButtonDown = false;
|
||||||
|
|
||||||
|
leftClickTimeThreshold = 0.1;
|
||||||
|
|
||||||
// upper screen check
|
// upper screen check
|
||||||
EDGE_TOLERANCE = 10.0;
|
_edgeTolerance = settings.edgeSlidingSensitivity.floatValue;
|
||||||
slideGestureVerticalThreshold = CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.4;
|
slideGestureVerticalThreshold = CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.4;
|
||||||
screenWidthWithThreshold = CGRectGetWidth([[UIScreen mainScreen] bounds]) - EDGE_TOLERANCE;
|
screenWidthWithThreshold = CGRectGetWidth([[UIScreen mainScreen] bounds]) - _edgeTolerance;
|
||||||
self->touchPointSpawnedAtUpperScreenEdge = false;
|
self->touchPointSpawnedAtUpperScreenEdge = false;
|
||||||
|
|
||||||
|
leftClickDelay = ((CGFloat)settings.leftClickDelayMs.intValue)/1000;
|
||||||
|
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)onLongPressStart:(NSTimer*)timer {
|
- (void)onLongPressStart:(NSTimer*)timer {
|
||||||
// Raise the left click and start a right click
|
// Raise the left click and start a right click
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
if(multiTouchesDetected) return;
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
|
|
||||||
|
if([self touchDidntMoveOnScreen:movingTouchLocation]){
|
||||||
|
if(_delayMouseLeftClick){
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||||
|
if(mouseButtonForCursorMove!=BUTTON_LEFT) LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, mouseButtonForCursorMove);
|
||||||
|
}
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
|
||||||
|
dispatch_time_t delayShort = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC));
|
||||||
|
dispatch_after(delayShort, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
|
||||||
|
self->rightButtonClicked = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
|
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||||
|
rightButtonClicked = false;
|
||||||
|
|
||||||
|
if([UITouchUtil touchesIn:streamView from:event].count>=2){
|
||||||
|
multiTouchesDetected = true;
|
||||||
|
[longPressTimer invalidate];
|
||||||
|
longPressTimer = nil;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
CGPoint initialPoint = [[touches anyObject] locationInView:streamView];
|
CGPoint initialPoint = [[touches anyObject] locationInView:streamView];
|
||||||
if(initialPoint.y < slideGestureVerticalThreshold && (initialPoint.x < EDGE_TOLERANCE || initialPoint.x > screenWidthWithThreshold)) {
|
if(initialPoint.y < slideGestureVerticalThreshold && (initialPoint.x < _edgeTolerance || initialPoint.x > screenWidthWithThreshold)) {
|
||||||
self->touchPointSpawnedAtUpperScreenEdge = true;
|
self->touchPointSpawnedAtUpperScreenEdge = true;
|
||||||
return; // we're done here. this touch event will not be sent to the remote PC.
|
return; // we're done here. this touch event will not be sent to the remote PC.
|
||||||
}
|
}
|
||||||
@@ -72,22 +124,27 @@
|
|||||||
touchPointSpawnedAtUpperScreenEdge = false; // reset this flag immediately if we get a touch event passing the check above, this fixes irresponsive touch after closing the command tool menu.
|
touchPointSpawnedAtUpperScreenEdge = false; // reset this flag immediately if we get a touch event passing the check above, this fixes irresponsive touch after closing the command tool menu.
|
||||||
|
|
||||||
// Ignore touch down events with more than one finger
|
// Ignore touch down events with more than one finger
|
||||||
|
/*
|
||||||
if ([[event allTouches] count] > 1) {
|
if ([[event allTouches] count] > 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}*/
|
||||||
|
|
||||||
UITouch* touch = [touches anyObject];
|
capturedTouch = [touches anyObject];
|
||||||
CGPoint touchLocation = [touch locationInView:streamView];
|
CGPoint touchLocation = [capturedTouch locationInView:streamView];
|
||||||
|
|
||||||
|
touchBeganTimeStamp = capturedTouch.timestamp;
|
||||||
|
|
||||||
// Don't reposition for finger down events within the deadzone. This makes double-clicking easier.
|
// Don't reposition for finger down events within the deadzone. This makes double-clicking easier.
|
||||||
if (touch.timestamp - lastTouchUp.timestamp > DOUBLE_TAP_DEAD_ZONE_DELAY ||
|
if (capturedTouch.timestamp - lastTouchUp.timestamp > DOUBLE_TAP_DEAD_ZONE_DELAY ||
|
||||||
sqrt(pow((touchLocation.x / streamView.bounds.size.width) - (lastTouchUpLocation.x / streamView.bounds.size.width), 2) +
|
sqrt(pow((touchLocation.x / streamView.bounds.size.width) - (lastTouchUpLocation.x / streamView.bounds.size.width), 2) +
|
||||||
pow((touchLocation.y / streamView.bounds.size.height) - (lastTouchUpLocation.y / streamView.bounds.size.height), 2)) > DOUBLE_TAP_DEAD_ZONE_DELTA) {
|
pow((touchLocation.y / streamView.bounds.size.height) - (lastTouchUpLocation.y / streamView.bounds.size.height), 2)) > DOUBLE_TAP_DEAD_ZONE_DELTA) {
|
||||||
[streamView updateCursorLocation:touchLocation isMouse:NO];
|
if(!multiTouchesDetected) [streamView updateCursorLocation:touchLocation isMouse:NO];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Press the left button down
|
// Press the left button down
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
if(!_delayMouseLeftClick){
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT); //deprecated
|
||||||
|
}
|
||||||
|
|
||||||
// Start the long press timer
|
// Start the long press timer
|
||||||
longPressTimer = [NSTimer scheduledTimerWithTimeInterval:LONG_PRESS_ACTIVATION_DELAY
|
longPressTimer = [NSTimer scheduledTimerWithTimeInterval:LONG_PRESS_ACTIVATION_DELAY
|
||||||
@@ -96,8 +153,21 @@
|
|||||||
userInfo:nil
|
userInfo:nil
|
||||||
repeats:NO];
|
repeats:NO];
|
||||||
|
|
||||||
lastTouchDown = touch;
|
lastTouchDown = capturedTouch;
|
||||||
lastTouchDownLocation = touchLocation;
|
lastTouchDownLocation = touchLocation;
|
||||||
|
movingTouchLocation = touchLocation;
|
||||||
|
touchBeganLocation = touchLocation;
|
||||||
|
currentTouchesCount = [UITouchUtil touchesIn:streamView from:event].count;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)pauseLeftButtonDrag{
|
||||||
|
if(dragButtonDown){
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||||
|
dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC));
|
||||||
|
dispatch_after(delay, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, mouseButtonForCursorMove);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
|
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||||
@@ -105,42 +175,90 @@
|
|||||||
if(touchPointSpawnedAtUpperScreenEdge) return; // we're done here. this touch event will not be sent to the remote PC.
|
if(touchPointSpawnedAtUpperScreenEdge) return; // we're done here. this touch event will not be sent to the remote PC.
|
||||||
|
|
||||||
// Ignore touch move events with more than one finger
|
// Ignore touch move events with more than one finger
|
||||||
|
/*
|
||||||
if ([[event allTouches] count] > 1) {
|
if ([[event allTouches] count] > 1) {
|
||||||
return;
|
return;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
NSSet* currentTouches = [UITouchUtil touchesIn:streamView from:event];
|
||||||
|
currentTouchesCount = currentTouches.count;
|
||||||
|
|
||||||
|
if(currentTouchesCount == 2){
|
||||||
|
if(mouseButtonForCursorMove!=BUTTON_LEFT) LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, mouseButtonForCursorMove);
|
||||||
|
if(passthroughGestures) [TouchPadGestureHandler handleGestureIn:streamView with:event];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(currentTouchesCount > 1) return;
|
||||||
|
|
||||||
UITouch* touch = [touches anyObject];
|
if(![currentTouches containsObject:capturedTouch]) return;
|
||||||
CGPoint touchLocation = [touch locationInView:streamView];
|
|
||||||
|
|
||||||
if (sqrt(pow((touchLocation.x / streamView.bounds.size.width) - (lastTouchDownLocation.x / streamView.bounds.size.width), 2) +
|
movingTouchLocation = [capturedTouch locationInView:streamView];
|
||||||
pow((touchLocation.y / streamView.bounds.size.height) - (lastTouchDownLocation.y / streamView.bounds.size.height), 2)) > LONG_PRESS_ACTIVATION_DELTA) {
|
|
||||||
|
if (sqrt(pow((movingTouchLocation.x / streamView.bounds.size.width) - (lastTouchDownLocation.x / streamView.bounds.size.width), 2) +
|
||||||
|
pow((movingTouchLocation.y / streamView.bounds.size.height) - (lastTouchDownLocation.y / streamView.bounds.size.height), 2)) > LONG_PRESS_ACTIVATION_DELTA) {
|
||||||
// Moved too far since touch down. Cancel the long press timer.
|
// Moved too far since touch down. Cancel the long press timer.
|
||||||
[longPressTimer invalidate];
|
[longPressTimer invalidate];
|
||||||
longPressTimer = nil;
|
longPressTimer = nil;
|
||||||
|
|
||||||
|
NSTimeInterval dragDelay = mouseButtonForCursorMove == BUTTON_LEFT ? leftClickTimeThreshold : 0;
|
||||||
|
|
||||||
|
if(_delayMouseLeftClick && (CACurrentMediaTime()-touchBeganTimeStamp>dragDelay) && !dragButtonDown){
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, mouseButtonForCursorMove);
|
||||||
|
dragButtonDown = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[streamView updateCursorLocation:[[touches anyObject] locationInView:streamView] isMouse:NO];
|
if(!rightButtonClicked) [streamView updateCursorLocation:movingTouchLocation isMouse:NO];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
|
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||||
|
[longPressTimer invalidate];
|
||||||
|
longPressTimer = nil;
|
||||||
|
|
||||||
|
if(TouchPadGestureHandler.ctrlDown) LiSendKeyboardEvent(CommandManager.keyboardButtonMappings[@"CTRL"].shortValue,KEY_ACTION_UP,0);
|
||||||
|
|
||||||
if(touchPointSpawnedAtUpperScreenEdge) return; // we're done here. this touch event will not be sent to the remote PC.
|
if(touchPointSpawnedAtUpperScreenEdge) return; // we're done here. this touch event will not be sent to the remote PC.
|
||||||
|
|
||||||
|
if(multiTouchesDetected) {
|
||||||
|
if([UITouchUtil touchesIn:streamView from:event].count == touches.count) multiTouchesDetected = false;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if([UITouchUtil touchesIn:streamView from:event].count == touches.count) multiTouchesDetected = false;
|
||||||
|
|
||||||
// Only fire this logic if all touches have ended
|
// Only fire this logic if all touches have ended
|
||||||
if ([[event allTouches] count] == [touches count]) {
|
if ([touches containsObject:capturedTouch]) {
|
||||||
// Cancel the long press timer
|
// Cancel the long press timer
|
||||||
[longPressTimer invalidate];
|
[longPressTimer invalidate];
|
||||||
longPressTimer = nil;
|
longPressTimer = nil;
|
||||||
|
|
||||||
// Left button up on finger up
|
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
|
||||||
|
|
||||||
// Raise right button too in case we triggered a long press gesture
|
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
|
|
||||||
|
|
||||||
// Remember this last touch for touch-down deadzoning
|
// Remember this last touch for touch-down deadzoning
|
||||||
|
CGPoint touchEndLocation = [capturedTouch locationInView:streamView];
|
||||||
|
|
||||||
|
if(_delayMouseLeftClick){
|
||||||
|
if(CACurrentMediaTime()-touchBeganTimeStamp<leftClickTimeThreshold) {
|
||||||
|
if(CACurrentMediaTime()-lastTouchUp.timestamp<0.15
|
||||||
|
&& ![self isAdjacentPoints:touchEndLocation from:lastTouchUpLocation tolerance:30]) [streamView updateCursorLocation:touchEndLocation isMouse:NO];
|
||||||
|
if([self touchDidntMoveOnScreen:touchEndLocation] && !rightButtonClicked) [self sendShortMouseLeftButtonClickEvent];
|
||||||
|
}
|
||||||
|
else if(!rightButtonClicked){
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||||
|
if(mouseButtonForCursorMove!=BUTTON_LEFT) LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, mouseButtonForCursorMove);
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
// Left button up on finger up
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||||
|
|
||||||
|
// Raise right button too in case we triggered a long press gesture
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
lastTouchUp = [touches anyObject];
|
lastTouchUp = [touches anyObject];
|
||||||
lastTouchUpLocation = [lastTouchUp locationInView:streamView];
|
lastTouchUpLocation = [lastTouchUp locationInView:streamView];
|
||||||
|
|
||||||
|
dragButtonDown = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,4 +267,34 @@
|
|||||||
[self touchesEnded:touches withEvent:event];
|
[self touchesEnded:touches withEvent:event];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)sendShortMouseLeftButtonClickEvent{
|
||||||
|
dispatch_time_t delayShort = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(leftClickDelay * NSEC_PER_SEC));
|
||||||
|
dispatch_time_t delayLong = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.03 * NSEC_PER_SEC));
|
||||||
|
dispatch_after(delayShort, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
||||||
|
dispatch_after(delayLong, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
- (bool)touchDidntMoveOnScreen:(CGPoint)touchLocation{
|
||||||
|
return [self isAdjacentPoints:touchLocation from:touchBeganLocation tolerance:3];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)isAdjacentPoints:(CGPoint)currentPoint from:(CGPoint)originalPoint tolerance:(CGFloat)tolerance {
|
||||||
|
bool isAdjacent = hypotf(originalPoint.x - currentPoint.x, originalPoint.y - currentPoint.y) <= hypot(tolerance, tolerance);
|
||||||
|
return isAdjacent;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (int)mouseButtonForCursorMove {
|
||||||
|
return mouseButtonForCursorMove;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (void)setMouseButtonForCursorMove:(int)button {
|
||||||
|
mouseButtonForCursorMove = button;
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,545 @@
|
|||||||
|
//
|
||||||
|
// AbstractGamepadOverlayWidgetView.swift
|
||||||
|
// VoidLink
|
||||||
|
//
|
||||||
|
// Created by True砖家 on 2024/4/6.
|
||||||
|
// Copyright © 2026 True砖家 on Bilibili. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@objc
|
||||||
|
protocol AbstractGamepadOverlayCloseButtonDelegate: AnyObject {
|
||||||
|
@objc(toggleGamepadOverlayWithOverlayEnabled:)
|
||||||
|
func toggleGamepadOverlay(withOverlayEnabled overlayEnabled: Bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum AbstractGamepadOverlayPersistence {
|
||||||
|
private static let defaults = UserDefaults.standard
|
||||||
|
|
||||||
|
private enum Key {
|
||||||
|
static let overlayCenterXRatio = "AbstractGamepadOverlay.overlay.centerXRatio"
|
||||||
|
static let overlayCenterYRatio = "AbstractGamepadOverlay.overlay.centerYRatio"
|
||||||
|
static let overlayWidth = "AbstractGamepadOverlay.overlay.width"
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PersistedState {
|
||||||
|
let centerRatio: CGPoint
|
||||||
|
let size: CGSize
|
||||||
|
}
|
||||||
|
|
||||||
|
static func loadOverlayState() -> PersistedState? {
|
||||||
|
loadState(
|
||||||
|
centerXKey: Key.overlayCenterXRatio,
|
||||||
|
centerYKey: Key.overlayCenterYRatio,
|
||||||
|
widthKey: Key.overlayWidth
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func saveOverlayState(centerRatio: CGPoint, size: CGSize) {
|
||||||
|
saveState(
|
||||||
|
centerRatio: centerRatio,
|
||||||
|
size: size,
|
||||||
|
centerXKey: Key.overlayCenterXRatio,
|
||||||
|
centerYKey: Key.overlayCenterYRatio,
|
||||||
|
widthKey: Key.overlayWidth
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func centerRatio(for view: UIView, in superview: UIView) -> CGPoint {
|
||||||
|
let safeWidth = max(superview.bounds.width, 1)
|
||||||
|
let safeHeight = max(superview.bounds.height, 1)
|
||||||
|
return CGPoint(x: view.center.x / safeWidth, y: view.center.y / safeHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func center(in superview: UIView, from ratio: CGPoint) -> CGPoint {
|
||||||
|
CGPoint(
|
||||||
|
x: min(max(ratio.x, 0), 1) * superview.bounds.width,
|
||||||
|
y: min(max(ratio.y, 0), 1) * superview.bounds.height
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func loadState(
|
||||||
|
centerXKey: String,
|
||||||
|
centerYKey: String,
|
||||||
|
widthKey: String
|
||||||
|
) -> PersistedState? {
|
||||||
|
guard defaults.object(forKey: centerXKey) != nil,
|
||||||
|
defaults.object(forKey: centerYKey) != nil,
|
||||||
|
defaults.object(forKey: widthKey) != nil else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let width = defaults.double(forKey: widthKey)
|
||||||
|
guard width > 0 else { return nil }
|
||||||
|
|
||||||
|
return PersistedState(
|
||||||
|
centerRatio: CGPoint(
|
||||||
|
x: defaults.double(forKey: centerXKey),
|
||||||
|
y: defaults.double(forKey: centerYKey)
|
||||||
|
),
|
||||||
|
size: CGSize(width: width, height: 0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func saveState(
|
||||||
|
centerRatio: CGPoint,
|
||||||
|
size: CGSize,
|
||||||
|
centerXKey: String,
|
||||||
|
centerYKey: String,
|
||||||
|
widthKey: String
|
||||||
|
) {
|
||||||
|
defaults.set(centerRatio.x, forKey: centerXKey)
|
||||||
|
defaults.set(centerRatio.y, forKey: centerYKey)
|
||||||
|
defaults.set(size.width, forKey: widthKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 13.0, *)
|
||||||
|
private struct AbstractGamepadOverlayBridgeView: View {
|
||||||
|
let usesPlayStationFaceButtons: Bool
|
||||||
|
private static let overlayClusterGapAdjustmentRatio: CGFloat = 0.13
|
||||||
|
private static let overlayUpperPrimaryClusterVerticalAdjustmentRatio: CGFloat = -0.05
|
||||||
|
private static let overlayCenterCompressionRatio: CGFloat = 0
|
||||||
|
private static let overlayPanelHorizontalInsetRatio: CGFloat = 0.083
|
||||||
|
private static let overlayTriggerStubSpacingAdjustmentRatio: CGFloat = 0.17
|
||||||
|
private static let overlayRightShoulderHorizontalOffsetRatio: CGFloat = 0.006
|
||||||
|
private static let overlayThumbPurpleStrength: CGFloat = 1.15
|
||||||
|
private static let overlayTriggerPurpleStrength: CGFloat = 2.0
|
||||||
|
@ObservedObject var stateCenter = GamepadOverlayStateCenter.shared
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
AbstractGamepadView(
|
||||||
|
gamepadType: usesPlayStationFaceButtons ? GamepadType.ps : GamepadType.xbox,
|
||||||
|
metricsProfile: GamepadMetricsProfile.overlay,
|
||||||
|
clusterGapAdjustmentRatio: Self.overlayClusterGapAdjustmentRatio,
|
||||||
|
upperPrimaryClusterVerticalAdjustmentRatio: Self.overlayUpperPrimaryClusterVerticalAdjustmentRatio,
|
||||||
|
centerCompressionRatio: Self.overlayCenterCompressionRatio,
|
||||||
|
panelHorizontalInsetRatio: Self.overlayPanelHorizontalInsetRatio,
|
||||||
|
triggerStubSpacingAdjustmentRatio: Self.overlayTriggerStubSpacingAdjustmentRatio,
|
||||||
|
rightShoulderHorizontalOffsetRatio: Self.overlayRightShoulderHorizontalOffsetRatio,
|
||||||
|
thumbPurpleStrength: Self.overlayThumbPurpleStrength,
|
||||||
|
triggerPurpleStrength: Self.overlayTriggerPurpleStrength,
|
||||||
|
liveSnapshot: stateCenter.snapshot
|
||||||
|
)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
|
||||||
|
.allowsHitTesting(false)
|
||||||
|
.background(Color.clear)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 13.0, *)
|
||||||
|
@objc
|
||||||
|
final class AbstractGamepadOverlayView: UIView {
|
||||||
|
private enum CloseButtonMetrics {
|
||||||
|
static let size: CGFloat = 40
|
||||||
|
static let topInset: CGFloat = -10
|
||||||
|
static let trailingInset: CGFloat = 0
|
||||||
|
static let symbolPointSize: CGFloat = 16
|
||||||
|
static let hideDelay: TimeInterval = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum OverlayMaskMetrics {
|
||||||
|
static let widthRatio: CGFloat = 0.85
|
||||||
|
static let heightRatio: CGFloat = 1.03
|
||||||
|
static let cornerRadiusRatio: CGFloat = 0.326
|
||||||
|
static let verticalOffsetRatio: CGFloat = -0.006
|
||||||
|
|
||||||
|
static let topRectWidthRatio: CGFloat = 0.6
|
||||||
|
static let topRectHeightRatio: CGFloat = 0.15
|
||||||
|
static let topRectCornerRadiusRatio: CGFloat = 0.22
|
||||||
|
static let topRectVerticalOffsetRatio: CGFloat = -0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
private var hostingControllerBox: AnyObject?
|
||||||
|
private weak var hostedView: UIView?
|
||||||
|
private let visualMaskLayer = CAShapeLayer()
|
||||||
|
private let usesPlayStationFaceButtons: Bool
|
||||||
|
private let baseRenderWidth: CGFloat = 420
|
||||||
|
private let baseAspectRatio: CGFloat = 1.82
|
||||||
|
private let initialDisplayWidth: CGFloat
|
||||||
|
private var pinchBaseWidth: CGFloat = 0
|
||||||
|
private var panStartCenter: CGPoint = .zero
|
||||||
|
private var activeGestureCount = 0
|
||||||
|
private var didRestorePersistedState = false
|
||||||
|
private var hideCloseButtonWorkItem: DispatchWorkItem?
|
||||||
|
private let minWidth: CGFloat = 70
|
||||||
|
private let maxWidth: CGFloat = 520
|
||||||
|
private let doubleTapWidths: [CGFloat] = [PublicUtils.isIPhone ? 90 : 130, PublicUtils.isIPhone ? 165 : 200]
|
||||||
|
private lazy var closeButton: UIButton = makeCloseButton()
|
||||||
|
@objc weak var closeButtonDelegate: AbstractGamepadOverlayCloseButtonDelegate?
|
||||||
|
|
||||||
|
@objc init(frame: CGRect, usesPlayStationFaceButtons: Bool) {
|
||||||
|
self.usesPlayStationFaceButtons = usesPlayStationFaceButtons
|
||||||
|
self.initialDisplayWidth = frame.width > 0 ? frame.width : baseRenderWidth
|
||||||
|
super.init(frame: CGRect(origin: frame.origin, size: CGSize(width: baseRenderWidth, height: baseRenderWidth / baseAspectRatio)))
|
||||||
|
setupHostedViewIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
self.usesPlayStationFaceButtons = false
|
||||||
|
self.initialDisplayWidth = frame.width > 0 ? frame.width : baseRenderWidth
|
||||||
|
super.init(frame: CGRect(origin: frame.origin, size: CGSize(width: baseRenderWidth, height: baseRenderWidth / baseAspectRatio)))
|
||||||
|
setupHostedViewIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
self.usesPlayStationFaceButtons = false
|
||||||
|
self.initialDisplayWidth = baseRenderWidth
|
||||||
|
super.init(coder: coder)
|
||||||
|
setupHostedViewIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupHostedViewIfNeeded() {
|
||||||
|
guard hostedView == nil else { return }
|
||||||
|
|
||||||
|
let hostingController = UIHostingController(rootView: AbstractGamepadOverlayBridgeView(usesPlayStationFaceButtons: usesPlayStationFaceButtons))
|
||||||
|
hostingController.view.backgroundColor = .clear
|
||||||
|
hostingController.view.isUserInteractionEnabled = false
|
||||||
|
hostingController.view.translatesAutoresizingMaskIntoConstraints = true
|
||||||
|
hostingController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||||
|
hostingController.view.frame = bounds
|
||||||
|
|
||||||
|
hostingControllerBox = hostingController
|
||||||
|
hostedView = hostingController.view
|
||||||
|
backgroundColor = .clear
|
||||||
|
clipsToBounds = false
|
||||||
|
isMultipleTouchEnabled = true
|
||||||
|
applyScale(forTargetWidth: initialDisplayWidth)
|
||||||
|
|
||||||
|
addSubview(hostingController.view)
|
||||||
|
addSubview(closeButton)
|
||||||
|
installGesturesIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
hostedView?.frame = bounds
|
||||||
|
layoutCloseButton()
|
||||||
|
updateVisualMask()
|
||||||
|
restorePersistedStateIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didMoveToSuperview() {
|
||||||
|
super.didMoveToSuperview()
|
||||||
|
didRestorePersistedState = false
|
||||||
|
restorePersistedStateIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func installGesturesIfNeeded() {
|
||||||
|
guard gestureRecognizers?.isEmpty != false else { return }
|
||||||
|
let singleTap = UITapGestureRecognizer(target: self, action: #selector(handleSingleTap(_:)))
|
||||||
|
singleTap.numberOfTapsRequired = 1
|
||||||
|
singleTap.cancelsTouchesInView = true
|
||||||
|
let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
|
||||||
|
pan.minimumNumberOfTouches = 1
|
||||||
|
pan.maximumNumberOfTouches = 1
|
||||||
|
pan.cancelsTouchesInView = true
|
||||||
|
let pinch = UIPinchGestureRecognizer(target: self, action: #selector(handlePinch(_:)))
|
||||||
|
pinch.cancelsTouchesInView = true
|
||||||
|
let doubleTap = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap(_:)))
|
||||||
|
doubleTap.numberOfTapsRequired = 2
|
||||||
|
doubleTap.cancelsTouchesInView = true
|
||||||
|
singleTap.delegate = self
|
||||||
|
pan.delegate = self
|
||||||
|
pinch.delegate = self
|
||||||
|
doubleTap.delegate = self
|
||||||
|
pan.require(toFail: doubleTap)
|
||||||
|
singleTap.require(toFail: pan)
|
||||||
|
addGestureRecognizer(pan)
|
||||||
|
addGestureRecognizer(singleTap)
|
||||||
|
addGestureRecognizer(pinch)
|
||||||
|
addGestureRecognizer(doubleTap)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func handleSingleTap(_ recognizer: UITapGestureRecognizer) {
|
||||||
|
guard recognizer.state == .ended else { return }
|
||||||
|
registerUserInteraction()
|
||||||
|
presentFirstTouchTipIfNeeded()
|
||||||
|
scheduleCloseButtonHideIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func handlePan(_ recognizer: UIPanGestureRecognizer) {
|
||||||
|
guard let superview else { return }
|
||||||
|
switch recognizer.state {
|
||||||
|
case .began:
|
||||||
|
registerUserInteraction()
|
||||||
|
presentFirstTouchTipIfNeeded()
|
||||||
|
beginGestureSession()
|
||||||
|
panStartCenter = center
|
||||||
|
case .changed, .ended:
|
||||||
|
registerUserInteraction()
|
||||||
|
let translation = recognizer.translation(in: superview)
|
||||||
|
center = CGPoint(x: panStartCenter.x + translation.x, y: panStartCenter.y + translation.y)
|
||||||
|
if recognizer.state == .ended {
|
||||||
|
endGestureSession()
|
||||||
|
}
|
||||||
|
case .cancelled, .failed:
|
||||||
|
endGestureSession()
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func handlePinch(_ recognizer: UIPinchGestureRecognizer) {
|
||||||
|
switch recognizer.state {
|
||||||
|
case .began:
|
||||||
|
registerUserInteraction()
|
||||||
|
presentFirstTouchTipIfNeeded()
|
||||||
|
beginGestureSession()
|
||||||
|
pinchBaseWidth = frame.width
|
||||||
|
case .changed, .ended:
|
||||||
|
registerUserInteraction()
|
||||||
|
let nextWidth = max(minWidth, min(maxWidth, pinchBaseWidth * recognizer.scale))
|
||||||
|
applyScale(forTargetWidth: nextWidth)
|
||||||
|
if recognizer.state == .ended {
|
||||||
|
endGestureSession()
|
||||||
|
}
|
||||||
|
case .cancelled, .failed:
|
||||||
|
endGestureSession()
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func handleDoubleTap(_ recognizer: UITapGestureRecognizer) {
|
||||||
|
guard recognizer.state == .ended else { return }
|
||||||
|
registerUserInteraction()
|
||||||
|
presentFirstTouchTipIfNeeded()
|
||||||
|
applyScale(forTargetWidth: nextDoubleTapWidth())
|
||||||
|
persistState()
|
||||||
|
scheduleCloseButtonHideIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateVisualMask() {
|
||||||
|
guard bounds.width > 1, bounds.height > 1 else {
|
||||||
|
visualMaskLayer.path = nil
|
||||||
|
hostedView?.layer.mask = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let maskWidth = bounds.width * OverlayMaskMetrics.widthRatio
|
||||||
|
let maskHeight = bounds.height * OverlayMaskMetrics.heightRatio
|
||||||
|
let maskRect = CGRect(
|
||||||
|
x: (bounds.width - maskWidth) * 0.5,
|
||||||
|
y: (bounds.height - maskHeight) * 0.5 + bounds.height * OverlayMaskMetrics.verticalOffsetRatio,
|
||||||
|
width: maskWidth,
|
||||||
|
height: maskHeight
|
||||||
|
)
|
||||||
|
let maskCornerRadius = min(maskRect.width, maskRect.height) * OverlayMaskMetrics.cornerRadiusRatio
|
||||||
|
let topRectWidth = bounds.width * OverlayMaskMetrics.topRectWidthRatio
|
||||||
|
let topRectHeight = bounds.height * OverlayMaskMetrics.topRectHeightRatio
|
||||||
|
let topRect = CGRect(
|
||||||
|
x: (bounds.width - topRectWidth) * 0.5,
|
||||||
|
y: (bounds.height - topRectHeight) * 0.5 + bounds.height * OverlayMaskMetrics.topRectVerticalOffsetRatio,
|
||||||
|
width: topRectWidth,
|
||||||
|
height: topRectHeight
|
||||||
|
)
|
||||||
|
let topRectCornerRadius = min(topRect.width, topRect.height) * OverlayMaskMetrics.topRectCornerRadiusRatio
|
||||||
|
|
||||||
|
visualMaskLayer.frame = bounds
|
||||||
|
let path = UIBezierPath(
|
||||||
|
roundedRect: maskRect,
|
||||||
|
byRoundingCorners: .allCorners,
|
||||||
|
cornerRadii: CGSize(width: maskCornerRadius, height: maskCornerRadius)
|
||||||
|
)
|
||||||
|
path.append(
|
||||||
|
UIBezierPath(
|
||||||
|
roundedRect: topRect,
|
||||||
|
byRoundingCorners: .allCorners,
|
||||||
|
cornerRadii: CGSize(width: topRectCornerRadius, height: topRectCornerRadius)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
visualMaskLayer.path = path.cgPath
|
||||||
|
hostedView?.layer.mask = visualMaskLayer
|
||||||
|
}
|
||||||
|
|
||||||
|
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||||
|
bounds.contains(point)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
|
super.touchesBegan(touches, with: event)
|
||||||
|
registerUserInteraction()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
|
super.touchesMoved(touches, with: event)
|
||||||
|
registerUserInteraction()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
|
super.touchesEnded(touches, with: event)
|
||||||
|
scheduleCloseButtonHideIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
|
super.touchesCancelled(touches, with: event)
|
||||||
|
scheduleCloseButtonHideIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func restorePersistedStateIfNeeded() {
|
||||||
|
guard !didRestorePersistedState,
|
||||||
|
let superview,
|
||||||
|
bounds.width > 0,
|
||||||
|
bounds.height > 0,
|
||||||
|
superview.bounds.width > 0,
|
||||||
|
superview.bounds.height > 0 else { return }
|
||||||
|
defer { didRestorePersistedState = true }
|
||||||
|
|
||||||
|
guard let state = AbstractGamepadOverlayPersistence.loadOverlayState() else { return }
|
||||||
|
applyScale(forTargetWidth: state.size.width)
|
||||||
|
center = AbstractGamepadOverlayPersistence.center(in: superview, from: state.centerRatio)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func beginGestureSession() {
|
||||||
|
activeGestureCount += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func registerUserInteraction() {
|
||||||
|
showCloseButton()
|
||||||
|
cancelPendingCloseButtonHide()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func presentFirstTouchTipIfNeeded() {
|
||||||
|
guard GenericUtils.isFirstLaunchGamepadOverlayFeature() else { return }
|
||||||
|
guard let presentingViewController = nearestViewController() else { return }
|
||||||
|
|
||||||
|
AlertControllerUtil.showAlert(
|
||||||
|
in: presentingViewController,
|
||||||
|
title: GenericUtils.gamepadOverlayFeatureTipTitle(),
|
||||||
|
message: GenericUtils.gamepadOverlayFeatureTipMessage(),
|
||||||
|
withCancel: false,
|
||||||
|
buttonTitle: LocalizationHelper.localizedString(forKey: "This tip won't be shown again"),
|
||||||
|
countdown: 6
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func nearestViewController() -> UIViewController? {
|
||||||
|
var responder: UIResponder? = self
|
||||||
|
while let current = responder {
|
||||||
|
if let viewController = current as? UIViewController {
|
||||||
|
return viewController
|
||||||
|
}
|
||||||
|
responder = current.next
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func endGestureSession() {
|
||||||
|
activeGestureCount = max(0, activeGestureCount - 1)
|
||||||
|
guard activeGestureCount == 0 else { return }
|
||||||
|
persistState()
|
||||||
|
scheduleCloseButtonHideIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func persistState() {
|
||||||
|
guard let superview,
|
||||||
|
superview.bounds.width > 0,
|
||||||
|
superview.bounds.height > 0 else { return }
|
||||||
|
AbstractGamepadOverlayPersistence.saveOverlayState(
|
||||||
|
centerRatio: AbstractGamepadOverlayPersistence.centerRatio(for: self, in: superview),
|
||||||
|
size: frame.size
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func nextDoubleTapWidth() -> CGFloat {
|
||||||
|
let currentWidth = frame.width
|
||||||
|
return abs(currentWidth - doubleTapWidths[0]) <= abs(currentWidth - doubleTapWidths[1])
|
||||||
|
? doubleTapWidths[1]
|
||||||
|
: doubleTapWidths[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyScale(forTargetWidth targetWidth: CGFloat) {
|
||||||
|
let clampedWidth = max(minWidth, min(maxWidth, targetWidth))
|
||||||
|
let targetScale = clampedWidth / baseRenderWidth
|
||||||
|
transform = CGAffineTransform(scaleX: targetScale, y: targetScale)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeCloseButton() -> UIButton {
|
||||||
|
let button = UIButton(type: .system)
|
||||||
|
button.tintColor = UIColor.black.withAlphaComponent(0.66)
|
||||||
|
button.backgroundColor = UIColor.white.withAlphaComponent(0.78)
|
||||||
|
button.layer.borderWidth = 1
|
||||||
|
button.layer.borderColor = UIColor.white.withAlphaComponent(0.9).cgColor
|
||||||
|
button.layer.shadowColor = UIColor.black.withAlphaComponent(0.05).cgColor
|
||||||
|
button.layer.shadowOpacity = 1
|
||||||
|
button.layer.shadowRadius = 4
|
||||||
|
button.layer.shadowOffset = CGSize(width: 0, height: 2)
|
||||||
|
button.alpha = 0
|
||||||
|
button.isHidden = true
|
||||||
|
button.adjustsImageWhenHighlighted = false
|
||||||
|
button.addTarget(self, action: #selector(handleCloseButtonTap), for: .touchUpInside)
|
||||||
|
|
||||||
|
let configuration = UIImage.SymbolConfiguration(pointSize: CloseButtonMetrics.symbolPointSize, weight: .bold)
|
||||||
|
button.setImage(UIImage(systemName: "xmark", withConfiguration: configuration), for: .normal)
|
||||||
|
return button
|
||||||
|
}
|
||||||
|
|
||||||
|
private func layoutCloseButton() {
|
||||||
|
let size = CGFloat(Int(CloseButtonMetrics.size/2))*2
|
||||||
|
closeButton.frame = CGRect(
|
||||||
|
x: bounds.width - CloseButtonMetrics.trailingInset - size,
|
||||||
|
y: CloseButtonMetrics.topInset,
|
||||||
|
width: size,
|
||||||
|
height: size
|
||||||
|
)
|
||||||
|
closeButton.layer.cornerRadius = size * 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showCloseButton() {
|
||||||
|
closeButton.isHidden = false
|
||||||
|
guard closeButton.alpha < 1 else { return }
|
||||||
|
UIView.animate(withDuration: 0.18) {
|
||||||
|
self.closeButton.alpha = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func hideCloseButton() {
|
||||||
|
guard !closeButton.isHidden else { return }
|
||||||
|
UIView.animate(
|
||||||
|
withDuration: 0.18,
|
||||||
|
animations: {
|
||||||
|
self.closeButton.alpha = 0
|
||||||
|
},
|
||||||
|
completion: { _ in
|
||||||
|
self.closeButton.isHidden = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func cancelPendingCloseButtonHide() {
|
||||||
|
hideCloseButtonWorkItem?.cancel()
|
||||||
|
hideCloseButtonWorkItem = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func scheduleCloseButtonHideIfNeeded() {
|
||||||
|
guard activeGestureCount == 0 else { return }
|
||||||
|
cancelPendingCloseButtonHide()
|
||||||
|
let workItem = DispatchWorkItem { [weak self] in
|
||||||
|
self?.hideCloseButton()
|
||||||
|
self?.hideCloseButtonWorkItem = nil
|
||||||
|
}
|
||||||
|
hideCloseButtonWorkItem = workItem
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + CloseButtonMetrics.hideDelay, execute: workItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func handleCloseButtonTap() {
|
||||||
|
cancelPendingCloseButtonHide()
|
||||||
|
closeButtonDelegate?.toggleGamepadOverlay(withOverlayEnabled: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 13.0, *)
|
||||||
|
extension AbstractGamepadOverlayView: UIGestureRecognizerDelegate {
|
||||||
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
|
(gestureRecognizer is UIPanGestureRecognizer && otherGestureRecognizer is UIPinchGestureRecognizer)
|
||||||
|
|| (gestureRecognizer is UIPinchGestureRecognizer && otherGestureRecognizer is UIPanGestureRecognizer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
||||||
|
guard let touchedView = touch.view else { return true }
|
||||||
|
return touchedView !== closeButton && !touchedView.isDescendant(of: closeButton)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
//
|
//
|
||||||
// KeyManager.swift
|
// CommandManager.swift
|
||||||
// VoidLink
|
// VoidLink
|
||||||
//
|
//
|
||||||
// Created by True砖家 on 2024/7/23.
|
// Created by True砖家 on 2024/7/23.
|
||||||
@@ -19,12 +19,14 @@ import UIKit
|
|||||||
|
|
||||||
// MARK: - Properties
|
// MARK: - Properties
|
||||||
|
|
||||||
|
@objc public private(set) var identifier: String
|
||||||
@objc var cmdString: String
|
@objc var cmdString: String
|
||||||
@objc var alias: String
|
@objc var alias: String
|
||||||
|
|
||||||
// MARK: - Initialization
|
// MARK: - Initialization
|
||||||
|
|
||||||
init(cmdString: String, alias: String) {
|
init(cmdString: String, alias: String) {
|
||||||
|
self.identifier = UUID().uuidString
|
||||||
self.cmdString = cmdString
|
self.cmdString = cmdString
|
||||||
self.alias = alias
|
self.alias = alias
|
||||||
}
|
}
|
||||||
@@ -36,11 +38,13 @@ import UIKit
|
|||||||
let alias = coder.decodeObject(of: NSString.self, forKey: "alias") as String? else {
|
let alias = coder.decodeObject(of: NSString.self, forKey: "alias") as String? else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
self.identifier = coder.decodeObject(of: NSString.self, forKey: "identifier") as String? ?? UUID().uuidString
|
||||||
self.cmdString = cmdString
|
self.cmdString = cmdString
|
||||||
self.alias = alias
|
self.alias = alias
|
||||||
}
|
}
|
||||||
|
|
||||||
public func encode(with coder: NSCoder) {
|
public func encode(with coder: NSCoder) {
|
||||||
|
coder.encode(identifier, forKey: "identifier")
|
||||||
coder.encode(cmdString, forKey: "keyboardCmdString")
|
coder.encode(cmdString, forKey: "keyboardCmdString")
|
||||||
coder.encode(alias, forKey: "alias")
|
coder.encode(alias, forKey: "alias")
|
||||||
}
|
}
|
||||||
@@ -50,6 +54,29 @@ import UIKit
|
|||||||
// Define the CommandManager class
|
// Define the CommandManager class
|
||||||
@objc public class CommandManager: NSObject {
|
@objc public class CommandManager: NSObject {
|
||||||
@objc public static let shared = CommandManager()
|
@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] = [
|
@objc public static let mouseButtonMappings: [String: Int32] = [
|
||||||
"M_LEFT" : BUTTON_LEFT,
|
"M_LEFT" : BUTTON_LEFT,
|
||||||
@@ -61,7 +88,9 @@ import UIKit
|
|||||||
"M_X1" : BUTTON_X1,
|
"M_X1" : BUTTON_X1,
|
||||||
"MX1" : BUTTON_X1,
|
"MX1" : BUTTON_X1,
|
||||||
"M_X2" : BUTTON_X2,
|
"M_X2" : BUTTON_X2,
|
||||||
"MX2" : BUTTON_X2
|
"MX2" : BUTTON_X2,
|
||||||
|
"WHEELUP" : 0xFF,
|
||||||
|
"WHEELDOWN" : -0xFF,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -96,6 +125,7 @@ import UIKit
|
|||||||
"PADDLE3" : PADDLE3_FLAG,
|
"PADDLE3" : PADDLE3_FLAG,
|
||||||
"PADDLE4" : PADDLE4_FLAG,
|
"PADDLE4" : PADDLE4_FLAG,
|
||||||
"MISC" : MISC_FLAG,
|
"MISC" : MISC_FLAG,
|
||||||
|
"OSCHOME" : SPECIAL_FLAG,
|
||||||
"OSCL2" : 0,
|
"OSCL2" : 0,
|
||||||
"L2" : 0,
|
"L2" : 0,
|
||||||
"LT" : 0,
|
"LT" : 0,
|
||||||
@@ -115,19 +145,74 @@ import UIKit
|
|||||||
"OSCBACK"
|
"OSCBACK"
|
||||||
]
|
]
|
||||||
|
|
||||||
@objc public static let touchPadCmds: [String] = ["LSVPAD", "RSVPAD", "LSPAD", "RSPAD", "DS4TOUCH", "MOUSEPAD", "DPAD", "TRACKBALL", "WASDPAD", "ARROWPAD"]
|
@objc public static let touchPadCmds: [String] = ["LSVPAD", "RSVPAD", "LSPAD", "RSPAD", "LSWHEEL", "RSWHEEL", "LTPAD", "RTPAD", "DS4TOUCH", "MOUSEPAD", "ABSMOUSEPAD", "ABSMOUSE", "MOUSEWHEEL", "WHEEL", "DISCRETEWHEEL", "DSWHEEL", "DPAD", "TRACKBALL", "WASDPAD", "ARROWPAD", "MAGNIFIER", "DUMMYPAD"]
|
||||||
|
@objc public static let mousePadWithButtonActions: [String] = ["MOUSEPAD", "ABSMOUSE", "ABSMOUSEPAD"]
|
||||||
|
@objc public static let mousePads: [String] = ["MOUSEPAD", "ABSMOUSE", "TRACKBALL", "ABSMOUSEPAD"]
|
||||||
|
@objc public static let directionPads: [String] = ["DPAD", "WASDPAD", "ARROWPAD"]
|
||||||
@objc public static let stickTouchPads: [String] = ["LSVPAD", "RSVPAD", "LSPAD", "RSPAD"]
|
@objc public static let 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 specialOverlayButtonCmds: [String] = ["SETTINGS"]
|
@objc public static let stickWheels: [String] = ["LSWHEEL", "RSWHEEL"]
|
||||||
|
@objc public static let velocityBasedTouchPads: [String] = ["LSVPAD", "RSVPAD", "MOUSEPAD", "TRACKBALL", "LTPAD", "RTPAD", "MOUSEWHEEL", "WHEEL"]
|
||||||
|
@objc public static let inertialTouchPads: [String] = ["LSVPAD", "RSVPAD", "TRACKBALL", "LSPAD", "RSPAD"]
|
||||||
|
@objc public static let verticalTouchPads: [String] = ["LTPAD", "RTPAD", "DISCRETEWHEEL", "DSWHEEL"]
|
||||||
|
@objc public static let singleDimensionActiveTouchpads: [String] = ["LTPAD", "RTPAD", "MOUSEWHEEL", "WHEEL", "DISCRETEWHEEL", "DSWHEEL"]
|
||||||
|
@objc public static let functionalButtonCmds: [String] = [
|
||||||
|
"SETTINGS",
|
||||||
|
"TOOLBOX",
|
||||||
|
"DISCONNECT",
|
||||||
|
"QUITAPP",
|
||||||
|
"PIP",
|
||||||
|
"WIDGETTOOL",
|
||||||
|
"ABSGAMEPAD",
|
||||||
|
"WIDGETPROFILES",
|
||||||
|
"PROFILES",
|
||||||
|
"PICKPROFILE",
|
||||||
|
"PICKPRFL",
|
||||||
|
"SOFTKEYBOARD",
|
||||||
|
"ABSTCHDRAG",
|
||||||
|
"FOLDER",
|
||||||
|
"LABELEDFOLDER",
|
||||||
|
"PRESSURECURVE",
|
||||||
|
"PENCILHOVER",
|
||||||
|
"BRUSH",
|
||||||
|
"ERASER",
|
||||||
|
"NOSINGLETOUCH",
|
||||||
|
"DISABLETILT",
|
||||||
|
"DISABLETOUCH",
|
||||||
|
"GAMEPADOVERLAY",
|
||||||
|
]
|
||||||
|
|
||||||
|
@objc public static let functionalTouchPadCmds: [String] = [
|
||||||
|
"MAGNIFIER"
|
||||||
|
]
|
||||||
|
|
||||||
|
@objc public static let pencilProButtonCmds: [String] = [
|
||||||
|
"PENCILHOVER",
|
||||||
|
"BRUSH",
|
||||||
|
"ERASER",
|
||||||
|
"NOSINGLETOUCH",
|
||||||
|
"DISABLETILT"
|
||||||
|
]
|
||||||
|
@objc public static let motionControlButtonCmds: [String] = ["GYRO","GYROPAUSE","ACCEL","MOTION"]
|
||||||
|
|
||||||
// @objc public static let specialGameWidgets: [String] = ["YSRSV", "YSLT", "YSRT", "YSRB", "YSB", "YSRT2", "YSRB2", "YSB2", "YSEM", "YSML", "YSMR", "YSWASD"]
|
// @objc public static let specialGameWidgets: [String] = ["YSRSV", "YSLT", "YSRT", "YSRB", "YSB", "YSRT2", "YSRB2", "YSB2", "YSEM", "YSML", "YSMR", "YSWASD"]
|
||||||
|
|
||||||
static let keyboardButtonMappings: [String: Int16] = [
|
@objc public static let shortcutAllowedFunctionalButtonMappings: [String: Int16] = [
|
||||||
|
"BRUSH": 0xFF,
|
||||||
|
"ERASER": 0xFF,
|
||||||
|
"FOLDER": 0xFF,
|
||||||
|
"LABELEDFOLDER": 0xFF,
|
||||||
|
]
|
||||||
|
|
||||||
|
@objc public static let keyboardButtonMappings: [String: Int16] = [
|
||||||
// Windows Key Codes
|
// Windows Key Codes
|
||||||
"NULL": 0xFF,
|
"NULL": 0xFF,
|
||||||
"CTRL": 0x11, // VK_CONTROL
|
"CTRL": 0x11, // VK_CONTROL
|
||||||
|
"RCTRL": 0xA3, // VK_RCONTROL
|
||||||
"SHIFT": 0x10, // VK_SHIFT
|
"SHIFT": 0x10, // VK_SHIFT
|
||||||
|
"RSHIFT": 0xA1, // VK_RSHIFT
|
||||||
"ALT": 0x12, // VK_MENU
|
"ALT": 0x12, // VK_MENU
|
||||||
|
"RALT": 0xA5, // VK_MENU
|
||||||
"F1": 0x70, // VK_F1
|
"F1": 0x70, // VK_F1
|
||||||
"F2": 0x71, // VK_F2
|
"F2": 0x71, // VK_F2
|
||||||
"F3": 0x72, // VK_F3
|
"F3": 0x72, // VK_F3
|
||||||
@@ -263,111 +348,131 @@ import UIKit
|
|||||||
"LAUNCHAPP2": 0xB7, // VK_LAUNCH_APP2
|
"LAUNCHAPP2": 0xB7, // VK_LAUNCH_APP2
|
||||||
"WIN": 0x5B,
|
"WIN": 0x5B,
|
||||||
"LEFT_WIN": 0x5B, // VK_LWIN
|
"LEFT_WIN": 0x5B, // VK_LWIN
|
||||||
|
"LEFTWIN": 0x5B, // VK_LWIN
|
||||||
|
"LWIN": 0x5B, // VK_LWIN
|
||||||
"RIGHT_WIN": 0x5C, // VK_RWIN
|
"RIGHT_WIN": 0x5C, // VK_RWIN
|
||||||
"RIGHTWIN": 0x5C, // VK_RWIN
|
"RIGHTWIN": 0x5C, // VK_RWIN
|
||||||
|
"RWIN": 0x5C, // VK_RWIN
|
||||||
"APPS": 0x5D, // VK_APPS
|
"APPS": 0x5D, // VK_APPS
|
||||||
|
|
||||||
// macOS Key Codes
|
// macOS Key Codes
|
||||||
"CMD": 0x37, // ⌘ Command
|
"ESCAPEMAC": 0x35,
|
||||||
"OPT": 0x3A, // ⌥ Option
|
"TABMAC": 0x30,
|
||||||
"CONTROL": 0x3B, // ⌃ Control
|
"RETURNMAC": 0x24,
|
||||||
"FUNCTION": 0x3F, // fn
|
"BACKSPACEMAC": 0x33,
|
||||||
"SHIFTMAC": 0x38, // ⇧ Shift
|
"SPACEMAC": 0x31,
|
||||||
"DELETEMAC": 0x75, // Forward Delete
|
|
||||||
"RETURNMAC": 0x24, // Return
|
"F1MAC": 0x7A,
|
||||||
"ENTERMAC": 0x4C, // Enter
|
"F2MAC": 0x78,
|
||||||
"ESCAPEMAC": 0x35, // Escape
|
"F3MAC": 0x63,
|
||||||
"TABMAC": 0x30, // Tab
|
"F4MAC": 0x76,
|
||||||
"SPACEMAC": 0x31, // Space
|
"F5MAC": 0x60,
|
||||||
"UPARRMAC": 0x7E, // Up Arrow
|
"F6MAC": 0x61,
|
||||||
"DOWNARRMAC": 0x7D, // Down Arrow
|
"F7MAC": 0x62,
|
||||||
"LEFTARRMAC": 0x7B, // Left Arrow
|
"F8MAC": 0x64,
|
||||||
"RIGHTARRMAC": 0x7C, // Right Arrow
|
"F9MAC": 0x65,
|
||||||
"F1MAC": 0x7A, // F1
|
"F10MAC": 0x6D,
|
||||||
"F2MAC": 0x78, // F2
|
"F11MAC": 0x67,
|
||||||
"F3MAC": 0x63, // F3
|
"F12MAC": 0x6F,
|
||||||
"F4MAC": 0x76, // F4
|
"F13MAC": 0x69,
|
||||||
"F5MAC": 0x60, // F5
|
"F14MAC": 0x6B,
|
||||||
"F6MAC": 0x61, // F6
|
"F15MAC": 0x71,
|
||||||
"F7MAC": 0x62, // F7
|
|
||||||
"F8MAC": 0x64, // F8
|
"GRAVEACCENTMAC": 0x32,
|
||||||
"F9MAC": 0x65, // F9
|
"1MAC": 0x12,
|
||||||
"F10MAC": 0x6D, // F10
|
"2MAC": 0x13,
|
||||||
"F11MAC": 0x67, // F11
|
"3MAC": 0x14,
|
||||||
"F12MAC": 0x6F, // F12
|
"4MAC": 0x15,
|
||||||
"0MAC": 0x52, // 0
|
"5MAC": 0x17,
|
||||||
"1MAC": 0x53, // 1
|
"6MAC": 0x16,
|
||||||
"2MAC": 0x54, // 2
|
"7MAC": 0x1A,
|
||||||
"3MAC": 0x55, // 3
|
"8MAC": 0x1C,
|
||||||
"4MAC": 0x56, // 4
|
"9MAC": 0x19,
|
||||||
"5MAC": 0x57, // 5
|
"0MAC": 0x1D,
|
||||||
"6MAC": 0x58, // 6
|
"MINUSMAC": 0x1B,
|
||||||
"7MAC": 0x59, // 7
|
"EQUALSMAC": 0x18,
|
||||||
"8MAC": 0x5A, // 8
|
|
||||||
"9MAC": 0x5B, // 9
|
"QMAC": 0x0C,
|
||||||
"NUMPAD0MAC": 0x4F, // Numpad 0
|
"WMAC": 0x0D,
|
||||||
"NUMPAD1MAC": 0x50, // Numpad 1
|
"EMAC": 0x0E,
|
||||||
"NUMPAD2MAC": 0x51, // Numpad 2
|
"RMAC": 0x0F,
|
||||||
"NUMPAD3MAC": 0x52, // Numpad 3
|
"TMAC": 0x11,
|
||||||
"NUMPAD4MAC": 0x53, // Numpad 4
|
"YMAC": 0x10,
|
||||||
"NUMPAD5MAC": 0x54, // Numpad 5
|
"UMAC": 0x20,
|
||||||
"NUMPAD6MAC": 0x55, // Numpad 6
|
"IMAC": 0x22,
|
||||||
"NUMPAD7MAC": 0x56, // Numpad 7
|
"OMAC": 0x1F,
|
||||||
"NUMPAD8MAC": 0x57, // Numpad 8
|
"PMAC": 0x23,
|
||||||
"NUMPAD9MAC": 0x58, // Numpad 9
|
"OPENBRACKETMAC": 0x21,
|
||||||
"NUMPADADDMAC": 0x45, // Numpad Add
|
"CLOSEBRACKETMAC": 0x1E,
|
||||||
"NUMPADSUBTRACTMAC": 0x4A, // Numpad Subtract
|
"BACKSLASHMAC": 0x2A,
|
||||||
"NUMPADMULTIPLYMAC": 0x43, // Numpad Multiply
|
|
||||||
"NUMPADDIVIDEMAC": 0x4B, // Numpad Divide
|
"CAPSLOCKMAC": 0x39,
|
||||||
"NUMPADDECIMALMAC": 0x41, // Numpad Decimal
|
"AMAC": 0x00,
|
||||||
"SHIFT_MAC": 0x38, // ⇧ Shift
|
"SMAC": 0x01,
|
||||||
"DELETE_MAC": 0x75, // Forward Delete
|
"DMAC": 0x02,
|
||||||
"RETURN_MAC": 0x24, // Return
|
"FMAC": 0x03,
|
||||||
"ENTER_MAC": 0x4C, // Enter
|
"GMAC": 0x05,
|
||||||
"ESCAPE_MAC": 0x35, // Escape
|
"HMAC": 0x04,
|
||||||
"TAB_MAC": 0x30, // Tab
|
"JMAC": 0x26,
|
||||||
"SPACE_MAC": 0x31, // Space
|
"KMAC": 0x28,
|
||||||
"UP_ARROW_MAC": 0x7E, // Up Arrow
|
"LMAC": 0x25,
|
||||||
"DOWN_ARROW_MAC": 0x7D, // Down Arrow
|
"SEMICOLONMAC": 0x29,
|
||||||
"LEFT_ARROW_MAC": 0x7B, // Left Arrow
|
"SINGLEQUOTEMAC": 0x27,
|
||||||
"RIGHT_ARROW_MAC": 0x7C, // Right Arrow
|
|
||||||
"F1_MAC": 0x7A, // F1
|
"SHIFTMAC": 0x38,
|
||||||
"F2_MAC": 0x78, // F2
|
"ZMAC": 0x06,
|
||||||
"F3_MAC": 0x63, // F3
|
"XMAC": 0x07,
|
||||||
"F4_MAC": 0x76, // F4
|
"CMAC": 0x08,
|
||||||
"F5_MAC": 0x60, // F5
|
"VMAC": 0x09,
|
||||||
"F6_MAC": 0x61, // F6
|
"BMAC": 0x0B,
|
||||||
"F7_MAC": 0x62, // F7
|
"NMAC": 0x2D,
|
||||||
"F8_MAC": 0x64, // F8
|
"MMAC": 0x2E,
|
||||||
"F9_MAC": 0x65, // F9
|
"COMMAMAC": 0x2B,
|
||||||
"F10_MAC": 0x6D, // F10
|
"PERIODMAC": 0x2F,
|
||||||
"F11_MAC": 0x67, // F11
|
"FORWARDSLASHMAC": 0x2C,
|
||||||
"F12_MAC": 0x6F, // F12
|
|
||||||
"0_MAC": 0x52, // 0
|
"CONTROL": 0x3B,
|
||||||
"1_MAC": 0x53, // 1
|
"RIGHTCONTROL": 0x3E,
|
||||||
"2_MAC": 0x54, // 2
|
"OPT": 0x3A,
|
||||||
"3_MAC": 0x55, // 3
|
"RIGHTOPT": 0x3D,
|
||||||
"4_MAC": 0x56, // 4
|
"CMD": 0x37,
|
||||||
"5_MAC": 0x57, // 5
|
"RIGHTCMD": 0x36,
|
||||||
"6_MAC": 0x58, // 6
|
"FUNCTION": 0x3F,
|
||||||
"7_MAC": 0x59, // 7
|
|
||||||
"8_MAC": 0x5A, // 8
|
"HOMEMAC": 0x73,
|
||||||
"9_MAC": 0x5B, // 9
|
"ENDMAC": 0x77,
|
||||||
"NUMPAD0_MAC": 0x4F, // Numpad 0
|
"PGUPMAC": 0x74,
|
||||||
"NUMPAD1_MAC": 0x50, // Numpad 1
|
"PGDNMAC": 0x79,
|
||||||
"NUMPAD2_MAC": 0x51, // Numpad 2
|
|
||||||
"NUMPAD3_MAC": 0x52, // Numpad 3
|
"LEFTARRMAC": 0x7B,
|
||||||
"NUMPAD4_MAC": 0x53, // Numpad 4
|
"RIGHTARRMAC": 0x7C,
|
||||||
"NUMPAD5_MAC": 0x54, // Numpad 5
|
"DOWNARRMAC": 0x7D,
|
||||||
"NUMPAD6_MAC": 0x55, // Numpad 6
|
"UPARRMAC": 0x7E,
|
||||||
"NUMPAD7_MAC": 0x56, // Numpad 7
|
|
||||||
"NUMPAD8_MAC": 0x57, // Numpad 8
|
"DELMAC": 0x75,
|
||||||
"NUMPAD9_MAC": 0x58, // Numpad 9
|
|
||||||
"NUMPAD_ADD_MAC": 0x45, // Numpad Add
|
"CONTEXTMAC": 0x72,
|
||||||
"NUMPAD_SUBTRACT_MAC": 0x4A, // Numpad Subtract
|
|
||||||
"NUMPAD_MULTIPLY_MAC": 0x43, // Numpad Multiply
|
"NUMPADCLEARMAC": 0x47,
|
||||||
"NUMPAD_DIVIDE_MAC": 0x4B, // Numpad Divide
|
"NUMPADDIVIDEMAC": 0x4B,
|
||||||
"NUMPAD_DECIMAL_MAC": 0x41, // Numpad Decimal
|
"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] = []
|
private var commands: [RemoteCommand] = []
|
||||||
@@ -383,21 +488,21 @@ import UIKit
|
|||||||
let defaults = UserDefaults.standard
|
let defaults = UserDefaults.standard
|
||||||
//if true { // save default entries if the data is empty.
|
//if true { // save default entries if the data is empty.
|
||||||
if defaults.data(forKey: "savedCommands") == nil { // save default entries if the data is empty.
|
if defaults.data(forKey: "savedCommands") == nil { // save default entries if the data is empty.
|
||||||
var defaultCommands: [RemoteCommand] = [
|
let defaultCommands: [RemoteCommand] = [
|
||||||
RemoteCommand(cmdString: "WIN", alias: "WIN"),
|
RemoteCommand(cmdString: "WIN", alias: "WIN"),
|
||||||
RemoteCommand(cmdString: "F11", alias: "F11"),
|
RemoteCommand(cmdString: "F11", alias: "F11"),
|
||||||
RemoteCommand(cmdString: "ESC", alias: "ESC"),
|
RemoteCommand(cmdString: "ESC", alias: "ESC"),
|
||||||
RemoteCommand(cmdString: "CTRL+SHIFT+ESC", alias: "任务管理器(Task Manager)"),
|
RemoteCommand(cmdString: "CTRL+SHIFT+ESC", alias: "=taskManager"),
|
||||||
RemoteCommand(cmdString: "ALT+F1", alias: "N卡截图(Nvidia Screenshot)"),
|
RemoteCommand(cmdString: "ALT+F1", alias: "=nvidiaShot"),
|
||||||
RemoteCommand(cmdString: "ALT+F9", alias: "N卡录屏(Nvidia Screen Recording)"),
|
RemoteCommand(cmdString: "ALT+F9", alias: "=nvidiaRec"),
|
||||||
RemoteCommand(cmdString: "ALT+F4", alias: "关闭窗口(ALT+F4)"),
|
RemoteCommand(cmdString: "ALT+F4", alias: "=closeWindow"),
|
||||||
RemoteCommand(cmdString: "CTRL+A", alias: "全选(Select All)"),
|
RemoteCommand(cmdString: "CTRL+A", alias: "=selectAll"),
|
||||||
RemoteCommand(cmdString: "CTRL+C", alias: "复制(Copy)"),
|
RemoteCommand(cmdString: "CTRL+C", alias: "=copy"),
|
||||||
RemoteCommand(cmdString: "CTRL+V", alias: "粘贴(Paste)"),
|
RemoteCommand(cmdString: "CTRL+V", alias: "=paste"),
|
||||||
RemoteCommand(cmdString: "WIN+D", alias: "切换桌面(Switch to Desktop)"),
|
RemoteCommand(cmdString: "WIN+D", alias: "=switchDesktop"),
|
||||||
RemoteCommand(cmdString: "WIN+P", alias: "多显模式(Project)"),
|
RemoteCommand(cmdString: "WIN+P", alias: "=projectDisplay"),
|
||||||
RemoteCommand(cmdString: "WIN+G", alias: "Xbox Game Bar"),
|
RemoteCommand(cmdString: "WIN+G", alias: "=xboxGameBar"),
|
||||||
RemoteCommand(cmdString: "SHIFT+TAB", alias: "Steam Overlay"),
|
RemoteCommand(cmdString: "SHIFT+TAB", alias: "=steamOverlay"),
|
||||||
]
|
]
|
||||||
|
|
||||||
let data = try? NSKeyedArchiver.archivedData(withRootObject: defaultCommands, requiringSecureCoding: false)
|
let data = try? NSKeyedArchiver.archivedData(withRootObject: defaultCommands, requiringSecureCoding: false)
|
||||||
@@ -410,22 +515,26 @@ import UIKit
|
|||||||
}
|
}
|
||||||
|
|
||||||
// extractKeyStrings from keyboardCMDString
|
// extractKeyStrings from keyboardCMDString
|
||||||
@objc public func extractKeyStringsFromComboCommand(from input: String) -> [String]? {
|
@objc public func extractAutoReleaseButtonStrings(from cmd: String) -> [String]? {
|
||||||
let keys = CommandManager.keyboardButtonMappings.keys.joined(separator: "|")
|
let cmd = cmd.uppercased()
|
||||||
|
let mergedKeys = (Set(CommandManager.keyboardButtonMappings.keys)
|
||||||
|
.union(Set(CommandManager.mouseButtonMappings.keys))
|
||||||
|
.union(Set(CommandManager.shortcutAllowedFunctionalButtonMappings.keys)))
|
||||||
|
let keys = mergedKeys.joined(separator: "|")
|
||||||
let pattern = "^(?:(\(keys))(?:\\+(\(keys))*)*)$"
|
let pattern = "^(?:(\(keys))(?:\\+(\(keys))*)*)$"
|
||||||
|
|
||||||
guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else {
|
guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else {
|
||||||
print("Failed to create regex")
|
print("Failed to create regex")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
let range = NSRange(location: 0, length: input.utf16.count)
|
let range = NSRange(location: 0, length: cmd.utf16.count)
|
||||||
guard let match = regex.firstMatch(in: input, options: [], range: range) else {
|
guard let match = regex.firstMatch(in: cmd, options: [], range: range) else {
|
||||||
print("No match found for input: \(input)")
|
print("No match found for input: \(cmd)")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// print("Regex matched for input: \(input)")
|
// print("Regex matched for input: \(input)")
|
||||||
|
|
||||||
let matchedString = (input as NSString).substring(with: match.range(at: 0))
|
let matchedString = (cmd as NSString).substring(with: match.range(at: 0))
|
||||||
let keyStrings = matchedString.split(separator: "+").map { String($0) }
|
let keyStrings = matchedString.split(separator: "+").map { String($0) }
|
||||||
|
|
||||||
guard !keyStrings.isEmpty else {
|
guard !keyStrings.isEmpty else {
|
||||||
@@ -436,7 +545,10 @@ import UIKit
|
|||||||
var validKeyStrings: [String] = []
|
var validKeyStrings: [String] = []
|
||||||
|
|
||||||
for key in keyStrings {
|
for key in keyStrings {
|
||||||
if CommandManager.keyboardButtonMappings.keys.contains(key) {
|
if (CommandManager.keyboardButtonMappings.keys.contains(key)
|
||||||
|
|| CommandManager.mouseButtonMappings.keys.contains(key)
|
||||||
|
|| CommandManager.shortcutAllowedFunctionalButtonMappings.keys.contains(key)
|
||||||
|
) {
|
||||||
validKeyStrings.append(key)
|
validKeyStrings.append(key)
|
||||||
} else {
|
} else {
|
||||||
print(" '\(key)' is not defined in key mappings")
|
print(" '\(key)' is not defined in key mappings")
|
||||||
@@ -450,7 +562,7 @@ import UIKit
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (index, key) in validKeyStrings.enumerated() {
|
for (index, key) in validKeyStrings.enumerated() {
|
||||||
print("Valid Key \(index): \(key)")
|
// print("Valid Key \(index): \(key)")
|
||||||
}
|
}
|
||||||
|
|
||||||
return validKeyStrings
|
return validKeyStrings
|
||||||
@@ -458,61 +570,80 @@ import UIKit
|
|||||||
}
|
}
|
||||||
|
|
||||||
//super combo key button strings
|
//super combo key button strings
|
||||||
@objc public func extractSinglCmdStringsFromComboKeys(from input: String) -> [String]? {
|
@objc public func extractCmdStrings(from input: String) -> [String]? {
|
||||||
let combinedStrings = [CommandManager.keyboardButtonMappings.keys.map { $0 as String },
|
|
||||||
CommandManager.oscButtonMappings.keys.map { $0 as String },
|
|
||||||
CommandManager.mouseButtonMappings.keys.map { $0 as String },
|
|
||||||
CommandManager.touchPadCmds.map { $0 as String }
|
|
||||||
]
|
|
||||||
.lazy
|
|
||||||
.flatMap { $0 } // 三维展开
|
|
||||||
.map(String.init(describing:)) // 安全类型转换
|
|
||||||
|
|
||||||
let keys = combinedStrings.joined(separator: "|")
|
|
||||||
let pattern = "^(?:\(keys))(?:-(?:\(keys)))*(?:-\\d+MS)?$"
|
|
||||||
|
|
||||||
|
|
||||||
guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else {
|
|
||||||
print("Failed to create regex")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
let range = NSRange(location: 0, length: input.utf16.count)
|
|
||||||
guard let match = regex.firstMatch(in: input, options: [], range: range) else {
|
|
||||||
print("No match found for input: \(input)")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// print("Regex matched for input: \(input)")
|
|
||||||
|
|
||||||
let matchedString = (input as NSString).substring(with: match.range(at: 0))
|
|
||||||
let cmdStrings = matchedString.split(separator: "-").map { String($0) }
|
|
||||||
|
|
||||||
guard !cmdStrings.isEmpty else {
|
|
||||||
print("No key strings found in the matched string")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var validCmdStrings: [String] = []
|
var validCmdStrings: [String] = []
|
||||||
|
|
||||||
for key in cmdStrings {
|
if PublicUtils.isGUIWidgetPickerAvailable {
|
||||||
validCmdStrings.append(key)
|
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 {
|
||||||
if validCmdStrings.isEmpty {
|
let input = input.uppercased()
|
||||||
print("No valid key strings found in the matched string")
|
let combinedStrings = [CommandManager.keyboardButtonMappings.keys.map { $0 as String },
|
||||||
return nil
|
CommandManager.oscButtonMappings.keys.map { $0 as String },
|
||||||
|
CommandManager.mouseButtonMappings.keys.map { $0 as String },
|
||||||
|
CommandManager.functionalButtonCmds.map { $0 as String },
|
||||||
|
CommandManager.motionControlButtonCmds.map { $0 as String },
|
||||||
|
CommandManager.touchPadCmds.map { $0 as String }
|
||||||
|
]
|
||||||
|
.lazy
|
||||||
|
.flatMap { $0 } // 三维展开
|
||||||
|
.map(String.init(describing:)) // 安全类型转换
|
||||||
|
|
||||||
|
let keys = combinedStrings.joined(separator: "|")
|
||||||
|
let pattern = "^(?:\(keys))(?:-(?:\(keys)))*(?:-\\d+MS)?$"
|
||||||
|
|
||||||
|
|
||||||
|
guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else {
|
||||||
|
print("Failed to create regex")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let range = NSRange(location: 0, length: input.utf16.count)
|
||||||
|
guard let match = regex.firstMatch(in: input, options: [], range: range) else {
|
||||||
|
print("No match found for input: \(input)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// print("Regex matched for input: \(input)")
|
||||||
|
|
||||||
|
let matchedString = (input as NSString).substring(with: match.range(at: 0))
|
||||||
|
let cmdStrings = matchedString.split(separator: "-").map { String($0) }
|
||||||
|
|
||||||
|
guard !cmdStrings.isEmpty else {
|
||||||
|
print("No key strings found in the matched string")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for key in cmdStrings {
|
||||||
|
validCmdStrings.append(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
if validCmdStrings.isEmpty {
|
||||||
|
print("No valid key strings found in the matched string")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (index, key) in validCmdStrings.enumerated() {
|
for (index, key) in validCmdStrings.enumerated() {
|
||||||
print("Valid Key \(index): \(key)")
|
print("Valid Key \(index): \(key)")
|
||||||
}
|
}
|
||||||
|
|
||||||
return validCmdStrings
|
return validCmdStrings
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc public func addCommand(_ command: RemoteCommand) -> Bool {
|
@objc public func addCommand(_ command: RemoteCommand) -> Bool {
|
||||||
command.cmdString = command.cmdString.uppercased() // convert all letters to upper case
|
command.cmdString = command.cmdString.uppercased() // convert all letters to upper case
|
||||||
if(command.alias.trimmingCharacters(in: .whitespacesAndNewlines).count == 0) {command.alias = command.cmdString} // copy cmd string as alias when alias is empty
|
if(command.alias.trimmingCharacters(in: .whitespacesAndNewlines).count == 0) {command.alias = command.cmdString} // copy cmd string as alias when alias is empty
|
||||||
let keyStrings = extractKeyStringsFromComboCommand(from: command.cmdString)
|
let keyStrings = extractAutoReleaseButtonStrings(from: command.cmdString)
|
||||||
if (keyStrings == nil) {return false} // in case of non-keyboard command strings, return false
|
if (keyStrings == nil) {return false} // in case of non-keyboard command strings, return false
|
||||||
commands.append(command)
|
commands.append(command)
|
||||||
saveCommands()
|
saveCommands()
|
||||||
@@ -531,6 +662,29 @@ import UIKit
|
|||||||
@objc public func getAllCommands() -> [RemoteCommand] {
|
@objc public func getAllCommands() -> [RemoteCommand] {
|
||||||
return commands
|
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() {
|
private func loadCommands() {
|
||||||
if let savedCommandsData = UserDefaults.standard.data(forKey: "savedCommands") {
|
if let savedCommandsData = UserDefaults.standard.data(forKey: "savedCommands") {
|
||||||
@@ -540,6 +694,7 @@ import UIKit
|
|||||||
// Assign the unarchived commands to your property
|
// Assign the unarchived commands to your property
|
||||||
print(" Assign the unarchived commands to your property ")
|
print(" Assign the unarchived commands to your property ")
|
||||||
commands = savedCommands
|
commands = savedCommands
|
||||||
|
migrateDefaultCommandAliasesIfNeeded()
|
||||||
} else {
|
} else {
|
||||||
// Handle the case where the data could not be unarchived into the expected type
|
// Handle the case where the data could not be unarchived into the expected type
|
||||||
print("Data could not be unarchived into [RemoteCommand]")
|
print("Data could not be unarchived into [RemoteCommand]")
|
||||||
@@ -551,38 +706,72 @@ import UIKit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private func saveCommands() {
|
private func saveCommands() {
|
||||||
if let data = try? NSKeyedArchiver.archivedData(withRootObject: commands, requiringSecureCoding: false) {
|
if let data = try? NSKeyedArchiver.archivedData(withRootObject: commands, requiringSecureCoding: false) {
|
||||||
UserDefaults.standard.set(data, forKey: "savedCommands")
|
UserDefaults.standard.set(data, forKey: "savedCommands")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func migrateDefaultCommandAliasesIfNeeded() {
|
||||||
|
var didMigrate = false
|
||||||
|
for command in commands {
|
||||||
|
guard let migratedAlias = Self.defaultCommandAliasMigrationMap[command.alias] else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
command.alias = migratedAlias
|
||||||
|
didMigrate = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if didMigrate {
|
||||||
|
saveCommands()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@objc public func sendKeyComboCommand(keyboardCmdStrings: [String], delay: TimeInterval = 0.2, index: Int = 0) { // we need a large delay for WAN streaming
|
@objc public func sendAutoReleaseComboCommand(cmdStrings: [String]?, delay: TimeInterval = 0.2, index: Int = 0, pressOnly: Bool = false, releaseOnly:Bool = false) { // we need a large delay for WAN streaming
|
||||||
// 如果已处理完所有按键,则开始释放按键
|
// 如果已处理完所有按键,则开始释放按键
|
||||||
guard index < keyboardCmdStrings.count else {
|
guard var cmdStrings = cmdStrings else { return }
|
||||||
// 释放按键
|
|
||||||
for keyStr in keyboardCmdStrings.reversed() { // 从后往前释放按键
|
cmdStrings = cmdStrings.filter { !CommandManager.shortcutAllowedFunctionalButtonMappings.keys.contains($0)}
|
||||||
|
|
||||||
|
if releaseOnly {
|
||||||
|
for keyStr in cmdStrings.reversed() {
|
||||||
if let keyCode = CommandManager.keyboardButtonMappings[keyStr] {
|
if let keyCode = CommandManager.keyboardButtonMappings[keyStr] {
|
||||||
LiSendKeyboardEvent(keyCode, Int8(KEY_ACTION_UP), 0) // 释放按键
|
LiSendKeyboardEvent(keyCode, Int8(KEY_ACTION_UP), 0) // 释放按键
|
||||||
}
|
}
|
||||||
|
if let mouseButtonCode = CommandManager.mouseButtonMappings[keyStr]{
|
||||||
|
LiSendMouseButtonEvent(CChar(BUTTON_ACTION_RELEASE), mouseButtonCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard index < cmdStrings.count else {
|
||||||
|
// 释放按键
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now()) {
|
||||||
|
if pressOnly {return}
|
||||||
|
for keyStr in cmdStrings.reversed() { // 从后往前释放按键
|
||||||
|
if let keyCode = CommandManager.keyboardButtonMappings[keyStr] {
|
||||||
|
LiSendKeyboardEvent(keyCode, Int8(KEY_ACTION_UP), 0) // 释放按键
|
||||||
|
}
|
||||||
|
if let mouseButtonCode = CommandManager.mouseButtonMappings[keyStr]{
|
||||||
|
LiSendMouseButtonEvent(CChar(BUTTON_ACTION_RELEASE), mouseButtonCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取当前按键的映射值
|
// 获取当前按键的映射值
|
||||||
if let keyCode = CommandManager.keyboardButtonMappings[keyboardCmdStrings[index]] {
|
if let keyCode = CommandManager.keyboardButtonMappings[cmdStrings[index]] {
|
||||||
// 发送当前按键的按下事件
|
// 发送当前按键的按下事件
|
||||||
LiSendKeyboardEvent(keyCode, Int8(KEY_ACTION_DOWN), 0)
|
LiSendKeyboardEvent(keyCode, Int8(KEY_ACTION_DOWN), 0)
|
||||||
|
} else if let mouseButtonCode = CommandManager.mouseButtonMappings[cmdStrings[index]]{
|
||||||
// 延迟后递归处理下一个按键
|
LiSendMouseButtonEvent(CChar(BUTTON_ACTION_PRESS), mouseButtonCode)
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
|
|
||||||
self.sendKeyComboCommand(keyboardCmdStrings: keyboardCmdStrings, delay: delay, index: index + 1)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
print("No mapping found for \(keyboardCmdStrings[index])")
|
print("No mapping found for \(cmdStrings[index])")
|
||||||
// 如果当前按键没有映射,跳过当前按键并继续下一个
|
}
|
||||||
self.sendKeyComboCommand(keyboardCmdStrings: keyboardCmdStrings, delay: delay, index: index + 1)
|
// 延迟后递归处理下一个按键
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
|
||||||
|
self.sendAutoReleaseComboCommand(cmdStrings: cmdStrings, delay: delay, index: index + 1, pressOnly: pressOnly, releaseOnly: releaseOnly)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -602,6 +791,4 @@ import UIKit
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,10 @@
|
|||||||
|
|
||||||
- (void)gamepadPresenceChanged;
|
- (void)gamepadPresenceChanged;
|
||||||
- (void)mousePresenceChanged;
|
- (void)mousePresenceChanged;
|
||||||
|
- (void)mouseConnected;
|
||||||
|
- (void)keyboardConnected;
|
||||||
- (void)streamExitRequested;
|
- (void)streamExitRequested;
|
||||||
|
- (void)controllerArrivalWithPlayerIndex:(int8_t)index;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@@ -23,6 +26,8 @@
|
|||||||
|
|
||||||
@property (readonly) bool shallDisableGyroHotSwitch;
|
@property (readonly) bool shallDisableGyroHotSwitch;
|
||||||
|
|
||||||
|
+(nullable ControllerSupport*) sharedInstance;
|
||||||
|
|
||||||
-(id) initWithConfig:(StreamConfiguration*)streamConfig delegate:(id<ControllerSupportDelegate>)delegate;
|
-(id) initWithConfig:(StreamConfiguration*)streamConfig delegate:(id<ControllerSupportDelegate>)delegate;
|
||||||
-(void) connectionEstablished;
|
-(void) connectionEstablished;
|
||||||
|
|
||||||
@@ -47,6 +52,28 @@
|
|||||||
-(void) rumbleTriggers:(uint16_t)controllerNumber leftTrigger:(uint16_t)leftTrigger rightTrigger:(uint16_t)rightTrigger;
|
-(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) 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) 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;
|
-(uint16_t) getActiveGamepadMask;
|
||||||
|
|
||||||
@@ -56,4 +83,7 @@
|
|||||||
|
|
||||||
-(void)updateControllerSupport:(StreamConfiguration*)streamConfig delegate:(id<ControllerSupportDelegate>)delegate;
|
-(void)updateControllerSupport:(StreamConfiguration*)streamConfig delegate:(id<ControllerSupportDelegate>)delegate;
|
||||||
|
|
||||||
|
-(void) reinitiatePrimaryController;
|
||||||
|
-(void)sendNavigationButtonPress;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -11,7 +11,8 @@
|
|||||||
|
|
||||||
#import <UIKit/UIKit.h>
|
#import <UIKit/UIKit.h>
|
||||||
#import "OnScreenControls.h"
|
#import "OnScreenControls.h"
|
||||||
#import "VoidLink-Swift.h"
|
|
||||||
|
@class OnScreenWidgetView;
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -22,13 +23,13 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
|
|
||||||
@property UIView* _view;
|
@property UIView* _view;
|
||||||
@property NSMutableArray *layoutChanges;
|
@property NSMutableArray *layoutChanges;
|
||||||
@property CALayer *layerBeingDragged;
|
@property (nullable) CALayer *layerBeingDragged;
|
||||||
@property UIViewController *layoutToolVC;
|
@property (nonatomic, weak) UIViewController *layoutToolVC;
|
||||||
|
|
||||||
- (id) initWithView:(UIView*)view controllerSup:(ControllerSupport*)controllerSupport
|
- (id) initWithView:(UIView*)view controllerSup:(nullable ControllerSupport*)controllerSupport
|
||||||
streamConfig:(StreamConfiguration*)streamConfig oscLevel:(int)oscLevel;
|
streamConfig:(nullable StreamConfiguration*)streamConfig oscLevel:(int)oscLevel;
|
||||||
|
|
||||||
- (CALayer*) controllerLayerFromName:(NSString*)name;
|
- (nullable CALayer*) controllerLayerFromName:(NSString*)name;
|
||||||
- (BOOL) isLayer:(CALayer*)layer hoveringOverButton:(UIButton*)button;
|
- (BOOL) isLayer:(CALayer*)layer hoveringOverButton:(UIButton*)button;
|
||||||
|
|
||||||
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
|
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
|
||||||
|
|||||||
@@ -10,9 +10,8 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#import "LayoutOnScreenControls.h"
|
#import "LayoutOnScreenControls.h"
|
||||||
#import "OSCProfilesTableViewController.h"
|
|
||||||
#import "OnScreenButtonState.h"
|
#import "OnScreenButtonState.h"
|
||||||
#import "OSCProfilesManager.h"
|
#import "VoidLink-Swift.h"
|
||||||
|
|
||||||
|
|
||||||
@interface LayoutOnScreenControls ()
|
@interface LayoutOnScreenControls ()
|
||||||
@@ -44,7 +43,7 @@
|
|||||||
|
|
||||||
layoutChanges = [[NSMutableArray alloc] init]; // will contain OSC button layout changes the user has made for this profile
|
layoutChanges = [[NSMutableArray alloc] init]; // will contain OSC button layout changes the user has made for this profile
|
||||||
|
|
||||||
[self drawButtons];
|
// [self drawButtons];
|
||||||
[self drawGuidelines]; // add the blue guidelines that appear when button is being tapped and dragged
|
[self drawGuidelines]; // add the blue guidelines that appear when button is being tapped and dragged
|
||||||
|
|
||||||
return self;
|
return self;
|
||||||
@@ -55,35 +54,44 @@
|
|||||||
/**
|
/**
|
||||||
* This method overrides the superclass's drawButtons method. The purpose of this method is to create a dPad parent layer, and add the four dPad buttons to it so that the user can drag the entire dPad around, directional buttons and all, as one unit as is the expected behavior. Note that we do not want the four dPad buttons to be child layers of a CALayer parent layer on the game stream view since the touch logic implemented for the four dPad buttons on the game stream view is written assuming the dPad buttons are not children of another parent CALayer
|
* This method overrides the superclass's drawButtons method. The purpose of this method is to create a dPad parent layer, and add the four dPad buttons to it so that the user can drag the entire dPad around, directional buttons and all, as one unit as is the expected behavior. Note that we do not want the four dPad buttons to be child layers of a CALayer parent layer on the game stream view since the touch logic implemented for the four dPad buttons on the game stream view is written assuming the dPad buttons are not children of another parent CALayer
|
||||||
*/
|
*/
|
||||||
- (void) drawButtons {
|
- (void) drawButtons:(OSCProfile* )profile {
|
||||||
[super setDPadCenter]; // Set custom position for D-Pad here
|
// [super regenLayerPool];
|
||||||
[super setAnalogStickPositions]; // Set custom position for analog sticks here
|
[super setDPadCenter:profile]; // Set custom position for D-Pad here
|
||||||
|
[super setAnalogStickPositions:profile]; // Set custom position for analog sticks here
|
||||||
[super drawBumpers];
|
[super drawBumpers];
|
||||||
[super drawButtons];
|
[super drawButtons:profile];
|
||||||
|
|
||||||
|
/*
|
||||||
|
UIImage* downButtonImage = [UIImage imageNamed:@"DownButton"];
|
||||||
|
UIImage* rightButtonImage = [UIImage imageNamed:@"RightButton"];
|
||||||
|
UIImage* upButtonImage = [UIImage imageNamed:@"UpButton"];
|
||||||
|
UIImage* leftButtonImage = [UIImage imageNamed:@"LeftButton"];
|
||||||
|
*/
|
||||||
|
|
||||||
UIImage* downButtonImage = [UIImage imageNamed:@"DownButton"];
|
|
||||||
UIImage* rightButtonImage = [UIImage imageNamed:@"RightButton"];
|
|
||||||
UIImage* upButtonImage = [UIImage imageNamed:@"UpButton"];
|
|
||||||
UIImage* leftButtonImage = [UIImage imageNamed:@"LeftButton"];
|
|
||||||
|
|
||||||
// create dPad background layer
|
// create dPad background layer
|
||||||
self._dPadBackground = [CALayer layer];
|
if([self.OSCButtonLayerPool containsObject:self._upButton]){
|
||||||
self._dPadBackground.name = @"dPad";
|
[self.OSCButtonLayerPool addObject:self._dPadBackground];
|
||||||
self._dPadBackground.frame = CGRectMake(self.D_PAD_CENTER_X,
|
self._dPadBackground.name = @"dPad";
|
||||||
self.D_PAD_CENTER_Y,
|
self._dPadBackground.frame = CGRectMake(self.D_PAD_CENTER_X,
|
||||||
self._leftButton.frame.size.width * 2 + BUTTON_DIST,
|
self.D_PAD_CENTER_Y,
|
||||||
self._leftButton.frame.size.width * 2 + BUTTON_DIST);
|
self._leftButton.frame.size.width * 2 + BUTTON_DIST,
|
||||||
self._dPadBackground.position = CGPointMake(self.D_PAD_CENTER_X, self.D_PAD_CENTER_Y); // since dPadBackground's dimensions have change after settings its width and height you need to reset its position again here
|
self._leftButton.frame.size.width * 2 + BUTTON_DIST);
|
||||||
[self.OSCButtonLayers addObject:self._dPadBackground];
|
self._dPadBackground.position = CGPointMake(self.D_PAD_CENTER_X, self.D_PAD_CENTER_Y); // since dPadBackground's dimensions have change after settings its width and height you need to reset its position again here
|
||||||
[_view.layer addSublayer:self._dPadBackground];
|
[self.OSCButtonLayerPool addObject:self._dPadBackground];
|
||||||
|
[_view.layer addSublayer:self._dPadBackground];
|
||||||
// add dPad buttons to parent layer
|
|
||||||
[self._dPadBackground addSublayer:self._downButton];
|
// add dPad buttons to parent layer
|
||||||
[self._dPadBackground addSublayer:self._rightButton];
|
[self._dPadBackground addSublayer:self._downButton];
|
||||||
[self._dPadBackground addSublayer:self._upButton];
|
[self._dPadBackground addSublayer:self._rightButton];
|
||||||
[self._dPadBackground addSublayer:self._leftButton];
|
[self._dPadBackground addSublayer:self._upButton];
|
||||||
|
[self._dPadBackground addSublayer:self._leftButton];
|
||||||
|
}
|
||||||
|
|
||||||
|
for(CALayer* buttonLayer in self.OSCButtonLayerPool){
|
||||||
|
// NSLog(@"layer name %@", buttonLayer.name);
|
||||||
|
}
|
||||||
|
|
||||||
[self resizeControllerLayerWith:self._dPadBackground and:_dPadSizeFactor];
|
[self resizeControllerLayerWith:self._dPadBackground and:_dPadSizeFactor];
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -128,46 +136,59 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
horizontalGuideline.backgroundColor = [UIColor blueColor]; // change horizontal guideline back to blue if it doesn't line up with one of the on screen buttons
|
horizontalGuideline.backgroundColor = [UIColor blueColor]; // change horizontal guideline back to blue if it doesn't line up with one of the on screen buttons
|
||||||
for (CALayer *button in self.OSCButtonLayers) { // horizontal guideline position check
|
OnScreenWidgetView.isHorizontallyAligned = false;
|
||||||
|
for (CALayer *button in self.OSCButtonLayerPool) { // horizontal guideline position check
|
||||||
if ((layerBeingDragged != button) && !button.isHidden) {
|
if ((layerBeingDragged != button) && !button.isHidden) {
|
||||||
if ((horizontalGuideline.center.y < button.position.y + 1) &&
|
if ((horizontalGuideline.center.y < button.position.y + 1) &&
|
||||||
(horizontalGuideline.center.y > button.position.y - 1)) {
|
(horizontalGuideline.center.y > button.position.y - 1)) {
|
||||||
horizontalGuideline.backgroundColor = [UIColor yellowColor];
|
horizontalGuideline.backgroundColor = [UIColor yellowColor];
|
||||||
|
OnScreenWidgetView.isHorizontallyAligned = true;
|
||||||
|
OnScreenWidgetView.alignedY = button.position.y;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for(UIView* view in self.layoutToolVC.view.subviews){
|
for(UIView* view in self.layoutToolVC.view.subviews){
|
||||||
if ([view isKindOfClass:[OnScreenWidgetView class]] && view != widget) {
|
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];
|
horizontalGuideline.backgroundColor = [UIColor yellowColor];
|
||||||
|
OnScreenWidgetView.isHorizontallyAligned = true;
|
||||||
|
OnScreenWidgetView.alignedY = view.center.y;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
verticalGuideline.backgroundColor = [UIColor blueColor]; // change vertical guideline back to blue if it doesn't line up with one of the on screen buttons
|
verticalGuideline.backgroundColor = [UIColor blueColor]; // change vertical guideline back to blue if it doesn't line up with one of the on screen buttons
|
||||||
for (CALayer *button in self.OSCButtonLayers) { // vertical guideline position check
|
OnScreenWidgetView.isVerticallyAligned = false;
|
||||||
|
for (CALayer *button in self.OSCButtonLayerPool) { // vertical guideline position check
|
||||||
if ((layerBeingDragged != button) && !button.isHidden) {
|
if ((layerBeingDragged != button) && !button.isHidden) {
|
||||||
if ((verticalGuideline.center.x < button.position.x + 1) &&
|
if ((verticalGuideline.center.x < button.position.x + 1) &&
|
||||||
(verticalGuideline.center.x > button.position.x - 1)) {
|
(verticalGuideline.center.x > button.position.x - 1)) {
|
||||||
verticalGuideline.backgroundColor = [UIColor yellowColor];
|
verticalGuideline.backgroundColor = [UIColor yellowColor];
|
||||||
|
OnScreenWidgetView.isVerticallyAligned = true;
|
||||||
|
OnScreenWidgetView.alignedX = button.position.x;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for(UIView* view in self.layoutToolVC.view.subviews){
|
for(UIView* view in self.layoutToolVC.view.subviews){
|
||||||
if ([view isKindOfClass:[OnScreenWidgetView class]] && view != widget) {
|
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)) {
|
if ((verticalGuideline.center.x < view.center.x + 1) && (verticalGuideline.center.x > view.center.x - 1)) {
|
||||||
verticalGuideline.backgroundColor = [UIColor yellowColor];
|
verticalGuideline.backgroundColor = [UIColor yellowColor];
|
||||||
|
OnScreenWidgetView.isVerticallyAligned = true;
|
||||||
|
OnScreenWidgetView.alignedX = view.center.x;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) updateGuidelinesForLegacyOscButton{
|
- (void) updateGuidelinesForLegacyOnScreenControls{
|
||||||
/* have guidelines follow wherever the user is touching on the screen */
|
/* have guidelines follow wherever the user is touching on the screen */
|
||||||
horizontalGuideline.center = layerBeingDragged.position;
|
horizontalGuideline.center = layerBeingDragged.position;
|
||||||
verticalGuideline.center = layerBeingDragged.position;
|
verticalGuideline.center = layerBeingDragged.position;
|
||||||
@@ -177,7 +198,7 @@
|
|||||||
-Change vertical guideline color to yellow if its x-position is almost equal to that of one of the buttons on screen.
|
-Change vertical guideline color to yellow if its x-position is almost equal to that of one of the buttons on screen.
|
||||||
*/
|
*/
|
||||||
horizontalGuideline.backgroundColor = [UIColor blueColor]; // change horizontal guideline back to blue if it doesn't line up with one of the on screen buttons
|
horizontalGuideline.backgroundColor = [UIColor blueColor]; // change horizontal guideline back to blue if it doesn't line up with one of the on screen buttons
|
||||||
for (CALayer *button in self.OSCButtonLayers) { // horizontal guideline position check
|
for (CALayer *button in self.OSCButtonLayerPool) { // horizontal guideline position check
|
||||||
if ((layerBeingDragged != button) && !button.isHidden) {
|
if ((layerBeingDragged != button) && !button.isHidden) {
|
||||||
if ((horizontalGuideline.center.y < button.position.y + 1) &&
|
if ((horizontalGuideline.center.y < button.position.y + 1) &&
|
||||||
(horizontalGuideline.center.y > button.position.y - 1)) {
|
(horizontalGuideline.center.y > button.position.y - 1)) {
|
||||||
@@ -196,7 +217,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
verticalGuideline.backgroundColor = [UIColor blueColor]; // change vertical guideline back to blue if it doesn't line up with one of the on screen buttons
|
verticalGuideline.backgroundColor = [UIColor blueColor]; // change vertical guideline back to blue if it doesn't line up with one of the on screen buttons
|
||||||
for (CALayer *button in self.OSCButtonLayers) { // vertical guideline position check
|
for (CALayer *button in self.OSCButtonLayerPool) { // vertical guideline position check
|
||||||
if ((layerBeingDragged != button) && !button.isHidden) {
|
if ((layerBeingDragged != button) && !button.isHidden) {
|
||||||
if ((verticalGuideline.center.x < button.position.x + 1) &&
|
if ((verticalGuideline.center.x < button.position.x + 1) &&
|
||||||
(verticalGuideline.center.x > button.position.x - 1)) {
|
(verticalGuideline.center.x > button.position.x - 1)) {
|
||||||
@@ -233,7 +254,7 @@
|
|||||||
|
|
||||||
/* returns reference to button layer object given the button's name*/
|
/* returns reference to button layer object given the button's name*/
|
||||||
- (CALayer*)controllerLayerFromName: (NSString*)name {
|
- (CALayer*)controllerLayerFromName: (NSString*)name {
|
||||||
for (CALayer *buttonLayer in self.OSCButtonLayers) {
|
for (CALayer *buttonLayer in self.OSCButtonLayerPool) {
|
||||||
|
|
||||||
if ([buttonLayer.name isEqualToString:name]) {
|
if ([buttonLayer.name isEqualToString:name]) {
|
||||||
return buttonLayer;
|
return buttonLayer;
|
||||||
@@ -297,7 +318,7 @@
|
|||||||
[[NSNotificationCenter defaultCenter] postNotificationName:@"LegacyOscCALayerSelectedNotification" object:layerBeingDragged]; // inform that a CALayer of onScreen controller has being selected
|
[[NSNotificationCenter defaultCenter] postNotificationName:@"LegacyOscCALayerSelectedNotification" object:layerBeingDragged]; // inform that a CALayer of onScreen controller has being selected
|
||||||
|
|
||||||
/* save the name, position, and visibility of button being touched in array in case user wants to undo the change later */
|
/* save the name, position, and visibility of button being touched in array in case user wants to undo the change later */
|
||||||
OnScreenButtonState *onScreenButtonState = [[OnScreenButtonState alloc] initWithButtonName:layerBeingDragged.name buttonType:LegacyOscButton andPosition:layerBeingDragged.position];
|
OnScreenButtonState *onScreenButtonState = [[OnScreenButtonState alloc] initWithButtonName:layerBeingDragged.name buttonType:LegacyOnScreenControls andPosition:layerBeingDragged.position];
|
||||||
// add hidden attr here
|
// add hidden attr here
|
||||||
onScreenButtonState.isHidden = layerBeingDragged.isHidden;
|
onScreenButtonState.isHidden = layerBeingDragged.isHidden;
|
||||||
|
|
||||||
@@ -307,7 +328,7 @@
|
|||||||
/* make guide lines visible and position them over the button the user is touching */
|
/* make guide lines visible and position them over the button the user is touching */
|
||||||
horizontalGuideline.hidden = NO;
|
horizontalGuideline.hidden = NO;
|
||||||
verticalGuideline.hidden = NO;
|
verticalGuideline.hidden = NO;
|
||||||
[self updateGuidelinesForLegacyOscButton];
|
[self updateGuidelinesForLegacyOnScreenControls];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,7 +338,7 @@
|
|||||||
[CATransaction begin];
|
[CATransaction begin];
|
||||||
[CATransaction setDisableActions:YES];
|
[CATransaction setDisableActions:YES];
|
||||||
layerBeingDragged.position = touchLocation; // move object to touch location
|
layerBeingDragged.position = touchLocation; // move object to touch location
|
||||||
[self updateGuidelinesForLegacyOscButton];
|
[self updateGuidelinesForLegacyOnScreenControls];
|
||||||
[CATransaction commit];
|
[CATransaction commit];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,17 +14,78 @@
|
|||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
#define DEFAULT_TEMPLATE_NAME1 @"控件模板 / Widget templates"
|
#define DEFAULT_TEMPLATE_NAME1 @"控件模板 / Widget templates"
|
||||||
#define DEFAULT_TEMPLATE_NAME2 @"旧版屏显控件 / Legacy on-screen controls"
|
#define DEFAULT_TEMPLATE_NAME2 @"旧版控件 / Legacy widgets"
|
||||||
|
|
||||||
|
typedef NS_ENUM(NSInteger, MapGyroTo) {
|
||||||
|
mapGyroToMouse,
|
||||||
|
mapGyroToControllerStick,
|
||||||
|
driftCorrection
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef NS_ENUM(NSInteger, PencilAndHoverMode) {
|
||||||
|
hoverDisabled,
|
||||||
|
pencilOnly,
|
||||||
|
pencilToMouse,
|
||||||
|
pencilToTouch
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
This object contains information pertaining to any of the user created, custom on screen controller layout configurations, or 'profiles.' The object contains a 'name' property for easy reference, as well as an 'isSelected' property which is used to determine whether this particular custom OSC layout should show on screen during game stream view. Only one 'OSCProfile' is set to 'isSelected' at any given time. The object also contains an array of 'OnScreenButtonStates' which provides information that allows us to move and hide/unhide each of the 19 on screen buttons. Note that the 'buttonStates' property should contain an NSMutableArray of ENCODED 'OnScreenButtonState' objects. This allows us to save the 'OSCProfile' object to NSUserDefaults.
|
This object contains information pertaining to any of the user created, custom on screen controller layout configurations, or 'profiles.' The object contains a 'name' property for easy reference, as well as an 'isSelected' property which is used to determine whether this particular custom OSC layout should show on screen during game stream view. Only one 'OSCProfile' is set to 'isSelected' at any given time. The object also contains an array of 'OnScreenButtonStates' which provides information that allows us to move and hide/unhide each of the 19 on screen buttons. Note that the 'buttonStates' property should contain an NSMutableArray of ENCODED 'OnScreenButtonState' objects. This allows us to save the 'OSCProfile' object to NSUserDefaults.
|
||||||
Additionally the 'OSCProfile' object adopts encoding and decoding protocols so that we can encode the object before saving it to NSUserDefaults. By saving this object to NSUserDefaults we allow the user to save and load their custom on screen controller button layouts between app launches
|
Additionally the 'OSCProfile' object adopts encoding and decoding protocols so that we can encode the object before saving it to NSUserDefaults. By saving this object to NSUserDefaults we allow the user to save and load their custom on screen controller button layouts between app launches
|
||||||
*/
|
*/
|
||||||
@interface OSCProfile : NSObject <NSCoding, NSSecureCoding>
|
@interface OSCProfile : NSObject <NSCoding, NSSecureCoding, NSMutableCopying>
|
||||||
|
|
||||||
@property NSString *name;
|
@property NSString *name;
|
||||||
@property NSMutableArray <OnScreenButtonState *> *buttonStates;
|
@property NSMutableArray <NSData *> *buttonStatesEncoded;
|
||||||
|
@property (nonatomic, assign) int16_t unfoldedExclusiveFolderSequence;
|
||||||
|
@property NSSet<NSNumber *> *postExclusiveUnfoldedSequences;
|
||||||
@property BOOL isSelected;
|
@property BOOL isSelected;
|
||||||
|
@property (nonatomic, assign) bool useBuiltinGyro;
|
||||||
|
@property (nonatomic, assign) bool swapYawAndRoll;
|
||||||
|
@property (nonatomic, assign) MapGyroTo mapGyroTo;
|
||||||
|
@property (nonatomic, assign) bool yawPitchToRightStick;
|
||||||
|
@property (nonatomic, assign) bool rollToLeftStick;
|
||||||
|
@property (nonatomic, assign) bool synthesizePhysicalStick;
|
||||||
|
@property (nonatomic, assign) CGFloat gyroSensitivityYaw;
|
||||||
|
@property (nonatomic, assign) CGFloat gyroSensitivityPitch;
|
||||||
|
@property (nonatomic, assign) CGFloat gyroSensitivityRoll;
|
||||||
|
@property (nonatomic, assign) CGFloat accelSensitivityX;
|
||||||
|
@property (nonatomic, assign) CGFloat accelSensitivityY;
|
||||||
|
@property (nonatomic, assign) CGFloat accelSensitivityZ;
|
||||||
|
@property (nonatomic, assign) double gyroToStickMinOffset;
|
||||||
|
@property (nonatomic, assign) double physicalLeftStickMinOffset;
|
||||||
|
@property (nonatomic, assign) double physicalRightStickMinOffset;
|
||||||
|
@property (nonatomic, assign) int controllerGyroSwitchMode;
|
||||||
|
@property (nonatomic, assign) bool reverseGyroHoldButton;
|
||||||
|
@property (nonatomic, assign) int controllerGyroSwitchHold;
|
||||||
|
@property (nonatomic, assign) int controllerGyroSwitchToggle;
|
||||||
|
@property (nonatomic, assign) int touchMode;
|
||||||
|
@property (nonatomic, assign) CGFloat pointerVelocityModeDivider;
|
||||||
|
@property (nonatomic, assign) CGFloat touchPointerVelocityFactor;
|
||||||
|
@property (nonatomic, assign) CGPoint normalizedStreamViewOffset;
|
||||||
|
@property (nonatomic, assign) CGFloat streamViewScale;
|
||||||
|
@property (nonatomic, assign) CGFloat dualSenseTransient;
|
||||||
|
|
||||||
|
@property (nonatomic, assign) bool gamepadOverlayEnabled;
|
||||||
|
|
||||||
|
@property NSArray<NSNumber *> *pressureCurvePoints;
|
||||||
|
// @property NSArray<NSNumber *> *initialTouchPressureCurvePoints;
|
||||||
|
@property (nonatomic, assign) int phase1StrokeSampleIndexEnd;
|
||||||
|
@property (nonatomic, assign) int phase2StrokeSampleIndexEnd;
|
||||||
|
@property (nonatomic, assign) CGFloat strokeEqualizationStrength;
|
||||||
|
@property (nonatomic, assign) bool pressureCurveEnabled;
|
||||||
|
@property (nonatomic, assign) bool doubleTapShorcutEnabled;
|
||||||
|
@property NSString *brushShortcut;
|
||||||
|
@property NSString *eraserShortcut;
|
||||||
|
@property (nonatomic, assign) bool squeezeShorcutEnabled;
|
||||||
|
@property NSString *squeezeStartShortcut;
|
||||||
|
@property NSString *squeezeEndShortcut;
|
||||||
|
@property (nonatomic, assign) bool pencilPausesNativeTouch;
|
||||||
|
@property (nonatomic, assign) bool disablePencilSlideGestures;
|
||||||
|
@property (nonatomic, assign) PencilAndHoverMode pencilAndHoverMode;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
- (id) initWithName:(NSString*)name buttonStates:(NSMutableArray*)buttonStates isSelected:(BOOL)isSelected;
|
- (id) initWithName:(NSString*)name buttonStates:(NSMutableArray*)buttonStates isSelected:(BOOL)isSelected;
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,14 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#import "OSCProfile.h"
|
#import "OSCProfile.h"
|
||||||
|
#import "VoidLink-Swift.h"
|
||||||
|
|
||||||
@implementation OSCProfile
|
@implementation OSCProfile
|
||||||
|
|
||||||
- (id) initWithName:(NSString*)name buttonStates:(NSMutableArray*)buttonStates isSelected:(BOOL)isSelected {
|
- (id) initWithName:(NSString*)name buttonStates:(NSMutableArray*)buttonStates isSelected:(BOOL)isSelected {
|
||||||
if ((self = [self init])) {
|
if ((self = [self init])) {
|
||||||
self.name = name;
|
self.name = name;
|
||||||
self.buttonStates = buttonStates;
|
self.buttonStatesEncoded = buttonStates;
|
||||||
self.isSelected = isSelected;
|
self.isSelected = isSelected;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,18 +30,233 @@
|
|||||||
|
|
||||||
- (void) encodeWithCoder:(NSCoder*)encoder {
|
- (void) encodeWithCoder:(NSCoder*)encoder {
|
||||||
[encoder encodeObject:self.name forKey:@"name"];
|
[encoder encodeObject:self.name forKey:@"name"];
|
||||||
[encoder encodeObject:self.buttonStates forKey:@"buttonStates"];
|
[encoder encodeObject:self.buttonStatesEncoded forKey:@"buttonStates"];
|
||||||
|
[encoder encodeInt32:self.unfoldedExclusiveFolderSequence forKey:@"unfoldedExclusiveFolderSequence"];
|
||||||
|
[encoder encodeObject:self.postExclusiveUnfoldedSequences forKey:@"postExclusiveUnfoldedSequences"];
|
||||||
[encoder encodeBool:self.isSelected forKey:@"isSelected"];
|
[encoder encodeBool:self.isSelected forKey:@"isSelected"];
|
||||||
|
[encoder encodeBool:self.useBuiltinGyro forKey:@"useBuiltinGyro"];
|
||||||
|
[encoder encodeBool:self.swapYawAndRoll forKey:@"swapYawAndRoll"];
|
||||||
|
[encoder encodeInt64:self.mapGyroTo forKey:@"mapGyroTo"];
|
||||||
|
[encoder encodeBool:self.yawPitchToRightStick forKey:@"yawPitchToRightStick"];
|
||||||
|
[encoder encodeBool:self.rollToLeftStick forKey:@"rollToLeftStick"];
|
||||||
|
[encoder encodeBool:self.synthesizePhysicalStick forKey:@"synthesizePhysicalStick"];
|
||||||
|
[encoder encodeFloat:self.gyroSensitivityYaw forKey:@"gyroSensitivityYaw"];
|
||||||
|
[encoder encodeFloat:self.gyroSensitivityPitch forKey:@"gyroSensitivityPitch"];
|
||||||
|
[encoder encodeFloat:self.gyroSensitivityRoll forKey:@"gyroSensitivityRoll"];
|
||||||
|
[encoder encodeFloat:self.accelSensitivityX forKey:@"accelSensitivityX"];
|
||||||
|
[encoder encodeFloat:self.accelSensitivityY forKey:@"accelSensitivityY"];
|
||||||
|
[encoder encodeFloat:self.accelSensitivityZ forKey:@"accelSensitivityZ"];
|
||||||
|
[encoder encodeDouble:self.gyroToStickMinOffset forKey:@"gyroToStickMinOffset"];
|
||||||
|
[encoder encodeDouble:self.physicalLeftStickMinOffset forKey:@"physicalLeftStickMinOffset"];
|
||||||
|
[encoder encodeDouble:self.physicalRightStickMinOffset forKey:@"physicalRightStickMinOffset"];
|
||||||
|
[encoder encodeInt:self.controllerGyroSwitchMode forKey:@"controllerGyroSwitchMode"];
|
||||||
|
[encoder encodeBool:self.reverseGyroHoldButton forKey:@"reverseGyroHoldButton"];
|
||||||
|
[encoder encodeInt:self.controllerGyroSwitchHold forKey:@"controllerGyroSwitchHold"];
|
||||||
|
[encoder encodeInt:self.controllerGyroSwitchToggle forKey:@"controllerGyroSwitchToggle"];
|
||||||
|
[encoder encodeFloat:self.pointerVelocityModeDivider forKey:@"pointerVelocityModeDivider"];
|
||||||
|
[encoder encodeInt:self.touchMode forKey:@"touchMode"];
|
||||||
|
[encoder encodeFloat:self.touchPointerVelocityFactor forKey:@"touchPointerVelocityFactor"];
|
||||||
|
[encoder encodeCGPoint:self.normalizedStreamViewOffset forKey:@"normalizedStreamViewOffset"];
|
||||||
|
[encoder encodeFloat:self.streamViewScale forKey:@"streamViewScale"];
|
||||||
|
[encoder encodeFloat:self.dualSenseTransient forKey:@"dualSenseTransient"];
|
||||||
|
[encoder encodeBool:self.gamepadOverlayEnabled forKey:@"gamepadOverlayEnabled"];
|
||||||
|
|
||||||
|
[encoder encodeObject:self.pressureCurvePoints forKey:@"pressureCurvePoints"];
|
||||||
|
// [encoder encodeObject:self.initialTouchPressureCurvePoints forKey:@"initialTouchPressureCurvePoints"];
|
||||||
|
[encoder encodeInt:self.phase1StrokeSampleIndexEnd forKey:@"phase1StrokeSampleIndexEnd"];
|
||||||
|
[encoder encodeInt:self.phase2StrokeSampleIndexEnd forKey:@"phase2StrokeSampleIndexEnd"];
|
||||||
|
[encoder encodeDouble:self.strokeEqualizationStrength forKey:@"strokeEqualizationStrength"];
|
||||||
|
[encoder encodeBool:self.pressureCurveEnabled forKey:@"pressureCurveEnabled"];
|
||||||
|
[encoder encodeBool:self.doubleTapShorcutEnabled forKey:@"doubleTapShorcutEnabled"];
|
||||||
|
[encoder encodeObject:self.brushShortcut forKey:@"brushShortcut"];
|
||||||
|
[encoder encodeObject:self.eraserShortcut forKey:@"eraserShortcut"];
|
||||||
|
[encoder encodeBool:self.squeezeShorcutEnabled forKey:@"squeezeShorcutEnabled"];
|
||||||
|
[encoder encodeObject:self.squeezeStartShortcut forKey:@"squeezeStartShortcut"];
|
||||||
|
[encoder encodeObject:self.squeezeEndShortcut forKey:@"squeezeEndShortcut"];
|
||||||
|
[encoder encodeBool:self.pencilPausesNativeTouch forKey:@"pencilPausesNativeTouch"];
|
||||||
|
[encoder encodeBool:self.disablePencilSlideGestures forKey:@"disablePencilSlideGestures"];
|
||||||
|
[encoder encodeInt64:self.pencilAndHoverMode forKey:@"pencilHoverMode"];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (id) initWithCoder:(NSCoder*)decoder {
|
- (id) initWithCoder:(NSCoder*)decoder {
|
||||||
if (self = [super init]) {
|
if (self = [super init]) {
|
||||||
self.name = [decoder decodeObjectForKey:@"name"];
|
self.name =
|
||||||
self.buttonStates = [decoder decodeObjectForKey:@"buttonStates"];
|
[decoder decodeObjectOfClasses:
|
||||||
|
[NSSet setWithObjects:
|
||||||
|
[NSString class],
|
||||||
|
nil]
|
||||||
|
forKey:@"name"];
|
||||||
|
self.buttonStatesEncoded =
|
||||||
|
[decoder decodeObjectOfClasses:
|
||||||
|
[NSSet setWithObjects:
|
||||||
|
[NSMutableArray class],
|
||||||
|
[NSArray class],
|
||||||
|
[NSData class],
|
||||||
|
nil]
|
||||||
|
forKey:@"buttonStates"];
|
||||||
|
self.unfoldedExclusiveFolderSequence = [decoder containsValueForKey:@"unfoldedExclusiveFolderSequence"] ? [decoder decodeInt32ForKey:@"unfoldedExclusiveFolderSequence"] : -1;
|
||||||
|
self.postExclusiveUnfoldedSequences = [decoder containsValueForKey:@"postExclusiveUnfoldedSequences"] ?
|
||||||
|
[decoder decodeObjectOfClasses:
|
||||||
|
[NSSet setWithObjects:
|
||||||
|
[NSMutableSet class],
|
||||||
|
[NSSet class],
|
||||||
|
[NSNumber class],
|
||||||
|
nil]
|
||||||
|
forKey:@"postExclusiveUnfoldedSequences"] :
|
||||||
|
[NSSet set];
|
||||||
self.isSelected = [decoder decodeBoolForKey:@"isSelected"];
|
self.isSelected = [decoder decodeBoolForKey:@"isSelected"];
|
||||||
|
self.useBuiltinGyro = [decoder containsValueForKey:@"useBuiltinGyro"] ? [decoder decodeBoolForKey:@"useBuiltinGyro"] : true;
|
||||||
|
self.swapYawAndRoll = [decoder containsValueForKey:@"swapYawAndRoll"] ? [decoder decodeBoolForKey:@"swapYawAndRoll"] : false;
|
||||||
|
self.mapGyroTo = [decoder containsValueForKey:@"mapGyroTo"] ? [decoder decodeInt64ForKey:@"mapGyroTo"] : mapGyroToMouse;
|
||||||
|
self.yawPitchToRightStick = [decoder containsValueForKey:@"yawPitchToRightStick"] ? [decoder decodeBoolForKey:@"yawPitchToRightStick"] : true;
|
||||||
|
self.rollToLeftStick = [decoder containsValueForKey:@"rollToLeftStick"] ? [decoder decodeBoolForKey:@"rollToLeftStick"] : false;
|
||||||
|
self.synthesizePhysicalStick = [decoder containsValueForKey:@"synthesizePhysicalStick"] ? [decoder decodeBoolForKey:@"synthesizePhysicalStick"] : true;
|
||||||
|
self.gyroSensitivityYaw = [decoder containsValueForKey:@"gyroSensitivityYaw"] ? [decoder decodeFloatForKey:@"gyroSensitivityYaw"] : 1.0;
|
||||||
|
self.gyroSensitivityPitch = [decoder containsValueForKey:@"gyroSensitivityPitch"] ? [decoder decodeFloatForKey:@"gyroSensitivityPitch"] : 1.0;
|
||||||
|
self.gyroSensitivityRoll = [decoder containsValueForKey:@"gyroSensitivityRoll"] ? [decoder decodeFloatForKey:@"gyroSensitivityRoll"] : 1.0;
|
||||||
|
self.accelSensitivityX = [decoder containsValueForKey:@"accelSensitivityX"] ? [decoder decodeFloatForKey:@"accelSensitivityX"] : 1.0;
|
||||||
|
self.accelSensitivityY = [decoder containsValueForKey:@"accelSensitivityY"] ? [decoder decodeFloatForKey:@"accelSensitivityY"] : 1.0;
|
||||||
|
self.accelSensitivityZ = [decoder containsValueForKey:@"accelSensitivityZ"] ? [decoder decodeFloatForKey:@"accelSensitivityZ"] : 1.0;
|
||||||
|
self.gyroToStickMinOffset = [decoder containsValueForKey:@"gyroToStickMinOffset"] ? [decoder decodeDoubleForKey:@"gyroToStickMinOffset"] : 0;
|
||||||
|
self.physicalLeftStickMinOffset = [decoder containsValueForKey:@"physicalLeftStickMinOffset"] ? [decoder decodeDoubleForKey:@"physicalLeftStickMinOffset"] : 0;
|
||||||
|
self.physicalRightStickMinOffset = [decoder containsValueForKey:@"physicalRightStickMinOffset"] ? [decoder decodeDoubleForKey:@"physicalRightStickMinOffset"] : 0;
|
||||||
|
self.controllerGyroSwitchMode = [decoder containsValueForKey:@"controllerGyroSwitchMode"] ? [decoder decodeIntForKey:@"controllerGyroSwitchMode"] : ControllerGyroSwitchDisabled;
|
||||||
|
self.reverseGyroHoldButton = [decoder containsValueForKey:@"reverseGyroHoldButton"] ? [decoder decodeBoolForKey:@"reverseGyroHoldButton"] : false;
|
||||||
|
self.controllerGyroSwitchHold = [decoder containsValueForKey:@"controllerGyroSwitchHold"] ? [decoder decodeIntForKey:@"controllerGyroSwitchHold"] : ControllerElementNull;
|
||||||
|
self.controllerGyroSwitchToggle = [decoder containsValueForKey:@"controllerGyroSwitchToggle"] ? [decoder decodeIntForKey:@"controllerGyroSwitchToggle"] : ControllerElementNull;
|
||||||
|
|
||||||
|
self.touchMode = [decoder containsValueForKey:@"touchMode"] ? [decoder decodeIntForKey:@"touchMode"] : NativeTouch;
|
||||||
|
if(self.touchMode == NativeTouchOnly) self.touchMode = NativeTouch;
|
||||||
|
|
||||||
|
self.pointerVelocityModeDivider = [decoder containsValueForKey:@"pointerVelocityModeDivider"] ? [decoder decodeFloatForKey:@"pointerVelocityModeDivider"] : 0.5;
|
||||||
|
self.touchPointerVelocityFactor = [decoder containsValueForKey:@"touchPointerVelocityFactor"] ? [decoder decodeFloatForKey:@"touchPointerVelocityFactor"] : 1.0;
|
||||||
|
|
||||||
|
self.normalizedStreamViewOffset = [decoder containsValueForKey:@"normalizedStreamViewOffset"] ? [decoder decodeCGPointForKey:@"normalizedStreamViewOffset"] : CGPointZero;
|
||||||
|
self.streamViewScale = [decoder containsValueForKey:@"streamViewScale"] ? [decoder decodeFloatForKey:@"streamViewScale"] : 1.0;
|
||||||
|
self.dualSenseTransient = [decoder containsValueForKey:@"dualSenseTransient"] ? [decoder decodeFloatForKey:@"dualSenseTransient"] : 1.0;
|
||||||
|
|
||||||
|
self.gamepadOverlayEnabled = [decoder containsValueForKey:@"gamepadOverlayEnabled"] ? [decoder decodeBoolForKey:@"gamepadOverlayEnabled"] : false;
|
||||||
|
|
||||||
|
self.pressureCurvePoints =
|
||||||
|
[decoder containsValueForKey:@"pressureCurvePoints"]
|
||||||
|
? [decoder decodeObjectOfClasses:
|
||||||
|
[NSSet setWithObjects:
|
||||||
|
[NSArray class],
|
||||||
|
[NSNumber class],
|
||||||
|
nil]
|
||||||
|
forKey:@"pressureCurvePoints"]
|
||||||
|
: @[@0.0, @0.0, @1.0, @1.0];
|
||||||
|
|
||||||
|
/* self.initialTouchPressureCurvePoints =
|
||||||
|
[decoder containsValueForKey:@"initialTouchPressureCurvePoints"]
|
||||||
|
? [decoder decodeObjectOfClasses:
|
||||||
|
[NSSet setWithObjects:
|
||||||
|
[NSArray class],
|
||||||
|
[NSNumber class],
|
||||||
|
nil]
|
||||||
|
forKey:@"initialTouchPressureCurvePoints"]
|
||||||
|
: @[@0.0, @0.0, @1.0, @1.0]; */
|
||||||
|
|
||||||
|
self.phase1StrokeSampleIndexEnd = [decoder containsValueForKey:@"phase1StrokeSampleIndexEnd"] ? [decoder decodeIntForKey:@"phase1StrokeSampleIndexEnd"] : 10;
|
||||||
|
self.phase2StrokeSampleIndexEnd = [decoder containsValueForKey:@"phase2StrokeSampleIndexEnd"] ? [decoder decodeIntForKey:@"phase2StrokeSampleIndexEnd"] : 24;
|
||||||
|
self.strokeEqualizationStrength = [decoder containsValueForKey:@"strokeEqualizationStrength"] ? [decoder decodeFloatForKey:@"strokeEqualizationStrength"] : 1.0;
|
||||||
|
|
||||||
|
self.pressureCurveEnabled = [decoder containsValueForKey:@"pressureCurveEnabled"] ? [decoder decodeBoolForKey:@"pressureCurveEnabled"] : false;
|
||||||
|
|
||||||
|
self.doubleTapShorcutEnabled = [decoder containsValueForKey:@"doubleTapShorcutEnabled"] ? [decoder decodeBoolForKey:@"doubleTapShorcutEnabled"] : false;
|
||||||
|
self.brushShortcut =
|
||||||
|
[decoder containsValueForKey:@"brushShortcut"]
|
||||||
|
? [decoder decodeObjectOfClasses:
|
||||||
|
[NSSet setWithObjects:
|
||||||
|
[NSString class],
|
||||||
|
nil]
|
||||||
|
forKey:@"brushShortcut"]
|
||||||
|
: @"";
|
||||||
|
|
||||||
|
self.eraserShortcut =
|
||||||
|
[decoder containsValueForKey:@"eraserShortcut"]
|
||||||
|
? [decoder decodeObjectOfClasses:
|
||||||
|
[NSSet setWithObjects:
|
||||||
|
[NSString class],
|
||||||
|
nil]
|
||||||
|
forKey:@"eraserShortcut"]
|
||||||
|
: @"";
|
||||||
|
|
||||||
|
self.squeezeShorcutEnabled = [decoder containsValueForKey:@"squeezeShorcutEnabled"] ? [decoder decodeBoolForKey:@"squeezeShorcutEnabled"] : false;
|
||||||
|
self.squeezeStartShortcut =
|
||||||
|
[decoder containsValueForKey:@"squeezeStartShortcut"]
|
||||||
|
? [decoder decodeObjectOfClasses:
|
||||||
|
[NSSet setWithObjects:
|
||||||
|
[NSString class],
|
||||||
|
nil]
|
||||||
|
forKey:@"squeezeStartShortcut"]
|
||||||
|
: @"";
|
||||||
|
|
||||||
|
self.squeezeEndShortcut =
|
||||||
|
[decoder containsValueForKey:@"squeezeEndShortcut"]
|
||||||
|
? [decoder decodeObjectOfClasses:
|
||||||
|
[NSSet setWithObjects:
|
||||||
|
[NSString class],
|
||||||
|
nil]
|
||||||
|
forKey:@"squeezeEndShortcut"]
|
||||||
|
: @"";
|
||||||
|
|
||||||
|
self.pencilPausesNativeTouch = [decoder containsValueForKey:@"pencilPausesNativeTouch"] ? [decoder decodeBoolForKey:@"pencilPausesNativeTouch"] : false;
|
||||||
|
self.disablePencilSlideGestures = [decoder containsValueForKey:@"disablePencilSlideGestures"] ? [decoder decodeBoolForKey:@"disablePencilSlideGestures"] : false;
|
||||||
|
self.pencilAndHoverMode = [decoder containsValueForKey:@"pencilHoverMode"] ? [decoder decodeInt64ForKey:@"pencilHoverMode"] : pencilOnly;
|
||||||
}
|
}
|
||||||
|
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (nonnull id)mutableCopyWithZone:(nullable NSZone *)zone {
|
||||||
|
OSCProfile *copy = [[[self class] allocWithZone:zone] init];
|
||||||
|
copy.name = [self.name mutableCopy]; // NSString → NSMutableString
|
||||||
|
copy.buttonStatesEncoded = [[NSMutableArray alloc] initWithArray:self.buttonStatesEncoded copyItems:YES];
|
||||||
|
copy.unfoldedExclusiveFolderSequence = self.unfoldedExclusiveFolderSequence;
|
||||||
|
copy.postExclusiveUnfoldedSequences = [self.postExclusiveUnfoldedSequences copy];
|
||||||
|
copy.isSelected = self.isSelected;
|
||||||
|
copy.useBuiltinGyro = self.useBuiltinGyro;
|
||||||
|
copy.swapYawAndRoll = self.swapYawAndRoll;
|
||||||
|
copy.mapGyroTo = self.mapGyroTo;
|
||||||
|
copy.yawPitchToRightStick = self.yawPitchToRightStick;
|
||||||
|
copy.rollToLeftStick = self.rollToLeftStick;
|
||||||
|
copy.synthesizePhysicalStick = self.synthesizePhysicalStick;
|
||||||
|
copy.gyroSensitivityYaw = self.gyroSensitivityYaw;
|
||||||
|
copy.gyroSensitivityPitch = self.gyroSensitivityPitch;
|
||||||
|
copy.gyroSensitivityRoll = self.gyroSensitivityRoll;
|
||||||
|
copy.accelSensitivityX = self.accelSensitivityX;
|
||||||
|
copy.accelSensitivityY = self.accelSensitivityY;
|
||||||
|
copy.accelSensitivityZ = self.accelSensitivityZ;
|
||||||
|
copy.gyroToStickMinOffset = self.gyroToStickMinOffset;
|
||||||
|
copy.physicalLeftStickMinOffset = self.physicalLeftStickMinOffset;
|
||||||
|
copy.physicalRightStickMinOffset = self.physicalRightStickMinOffset;
|
||||||
|
copy.controllerGyroSwitchMode = self.controllerGyroSwitchMode;
|
||||||
|
copy.reverseGyroHoldButton = self.reverseGyroHoldButton;
|
||||||
|
copy.controllerGyroSwitchHold = self.controllerGyroSwitchHold;
|
||||||
|
copy.controllerGyroSwitchToggle = self.controllerGyroSwitchToggle;
|
||||||
|
copy.touchMode = self.touchMode;
|
||||||
|
copy.pointerVelocityModeDivider = self.pointerVelocityModeDivider;
|
||||||
|
copy.touchPointerVelocityFactor = self.touchPointerVelocityFactor;
|
||||||
|
copy.normalizedStreamViewOffset = self.normalizedStreamViewOffset;
|
||||||
|
copy.streamViewScale = self.streamViewScale;
|
||||||
|
copy.gamepadOverlayEnabled = self.gamepadOverlayEnabled;
|
||||||
|
copy.pressureCurvePoints = [[NSMutableArray alloc] initWithArray:self.pressureCurvePoints copyItems:YES];
|
||||||
|
// copy.initialTouchPressureCurvePoints = [[NSMutableArray alloc] initWithArray:self.initialTouchPressureCurvePoints copyItems:YES];
|
||||||
|
copy.phase1StrokeSampleIndexEnd = self.phase1StrokeSampleIndexEnd;
|
||||||
|
copy.phase2StrokeSampleIndexEnd = self.phase2StrokeSampleIndexEnd;
|
||||||
|
copy.strokeEqualizationStrength = self.strokeEqualizationStrength;
|
||||||
|
copy.pressureCurveEnabled = self.pressureCurveEnabled;
|
||||||
|
copy.doubleTapShorcutEnabled = self.doubleTapShorcutEnabled;
|
||||||
|
copy.brushShortcut = [self.brushShortcut mutableCopy]; // NSString → NSMutableString
|
||||||
|
copy.eraserShortcut = [self.eraserShortcut mutableCopy]; // NSString → NSMutableString
|
||||||
|
copy.squeezeShorcutEnabled = self.squeezeShorcutEnabled;
|
||||||
|
copy.squeezeStartShortcut = [self.squeezeStartShortcut mutableCopy]; // NSString → NSMutableString
|
||||||
|
copy.squeezeEndShortcut = [self.squeezeEndShortcut mutableCopy]; // NSString → NSMutableString
|
||||||
|
copy.pencilPausesNativeTouch = self.pencilPausesNativeTouch;
|
||||||
|
copy.disablePencilSlideGestures = self.disablePencilSlideGestures;
|
||||||
|
copy.pencilAndHoverMode = self.pencilAndHoverMode;
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
//
|
|
||||||
// OSCProfilesManager.h
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Long Le on 1/1/23.
|
|
||||||
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
|
|
||||||
//
|
|
||||||
// Modified by True砖家 since 2024.6.24
|
|
||||||
// Copyright © 2024 True砖家 @ Bilibili. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#import "OSCProfile.h"
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
|
||||||
|
|
||||||
/**
|
|
||||||
This singleton object can be accessed from any class and provides methods to get and set on screen controller profile related data.
|
|
||||||
Note that the implementation file contains a number of 'Helper' methods. These helper methods are only used in this class's implementation file and help to reduce re-writing large blocks of code that are called multiple times throughout the file
|
|
||||||
*/
|
|
||||||
@interface OSCProfilesManager : NSObject
|
|
||||||
@property NSMutableArray <OSCProfile *> *currentProfiles;
|
|
||||||
|
|
||||||
|
|
||||||
+ (OSCProfilesManager *)sharedManager:(CGRect)viewBounds;
|
|
||||||
+ (void)setOnScreenWidgetViewsSet:(NSMutableSet* )set;
|
|
||||||
+ (void)setLayoutViewBounds:(CGRect)bounds;
|
|
||||||
|
|
||||||
|
|
||||||
#pragma mark - Getters
|
|
||||||
/**
|
|
||||||
* Returns an array of decoded profile objects
|
|
||||||
*/
|
|
||||||
- (NSMutableArray *) getAllProfiles;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the OSC Profile that is currently selected to be displayed on screen during game streaming
|
|
||||||
*/
|
|
||||||
- (OSCProfile *) getSelectedProfile;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the index of the 'selected' profile within the array it's in
|
|
||||||
*/
|
|
||||||
- (NSInteger) getIndexOfSelectedProfile;
|
|
||||||
|
|
||||||
- (NSMutableArray *) getEncodedProfiles;
|
|
||||||
- (void) importEncodedProfiles:(NSMutableArray* )profilesEncoded;
|
|
||||||
- (OnScreenButtonState *)unarchiveButtonStateEncoded:(NSData *)data;
|
|
||||||
- (void) importDefaultTemplates;
|
|
||||||
|
|
||||||
#pragma mark - Setters
|
|
||||||
/**
|
|
||||||
* Sets the profile object with the particular 'name' as the selected profile to be displayed on screen during game streaming
|
|
||||||
*/
|
|
||||||
- (void) setProfileToSelected:(NSString *)name;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves a profile object with a particular 'name' and an array of button layers (the CALayer button layers are the objects currently visible on screen) to persistent storage
|
|
||||||
*/
|
|
||||||
- (void) saveProfileWithName:(NSString*)name andButtonLayers:(NSMutableArray *)buttonLayers;
|
|
||||||
|
|
||||||
- (bool) updateSelectedProfile:(NSMutableArray *) oscButtonLayers;
|
|
||||||
/**
|
|
||||||
* Delete current selected profile.
|
|
||||||
*/
|
|
||||||
- (void) deleteCurrentSelectedProfile;
|
|
||||||
|
|
||||||
#pragma mark - Queries
|
|
||||||
/**
|
|
||||||
* Lets the caller of this method know whether a profile with a given name already exists in persistent storage
|
|
||||||
*/
|
|
||||||
- (BOOL) profileNameAlreadyExist:(NSString*)name;
|
|
||||||
- (OSCProfile *) findProfileByName:(NSString*) name inProfileArray:(NSMutableArray*)profiles;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
|
||||||
@@ -1,441 +0,0 @@
|
|||||||
//
|
|
||||||
// OSCProfilesManager.m
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Long Le on 1/1/23.
|
|
||||||
// Copyright © 2023 Moonlight Game Streaming Project. All rights reserved.
|
|
||||||
//
|
|
||||||
// Modified by True砖家 since 2024.6.24
|
|
||||||
// Copyright © 2024 True砖家 @ Bilibili. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "OSCProfilesManager.h"
|
|
||||||
#import "OnScreenButtonState.h"
|
|
||||||
#import "VoidLink-Swift.h"
|
|
||||||
#import "LayoutOnScreenControlsViewController.h"
|
|
||||||
#import "OnScreenControls.h"
|
|
||||||
|
|
||||||
@implementation OSCProfilesManager
|
|
||||||
|
|
||||||
static NSMutableSet *OnScreenWidgetViews;
|
|
||||||
static CGRect layoutViewBounds;
|
|
||||||
|
|
||||||
#pragma mark - Initializer
|
|
||||||
|
|
||||||
+ (OSCProfilesManager *) sharedManager:(CGRect)viewBounds {
|
|
||||||
static OSCProfilesManager *_sharedManager = nil;
|
|
||||||
static dispatch_once_t onceToken;
|
|
||||||
layoutViewBounds = viewBounds;
|
|
||||||
// NSLog(@"bounds width: %f, height: %f", layoutViewBounds.size.width, layoutViewBounds.size.height);
|
|
||||||
dispatch_once(&onceToken, ^{
|
|
||||||
_sharedManager = [[self alloc] init];
|
|
||||||
});
|
|
||||||
// _sharedManager.currentProfiles = [_sharedManager getAllProfiles];
|
|
||||||
return _sharedManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#pragma mark - Class Helper Methods
|
|
||||||
|
|
||||||
|
|
||||||
+ (void)setOnScreenWidgetViewsSet:(NSMutableSet* )set{
|
|
||||||
OnScreenWidgetViews = set;
|
|
||||||
}
|
|
||||||
|
|
||||||
+ (void)setLayoutViewBounds:(CGRect)bounds{
|
|
||||||
layoutViewBounds = bounds;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the profile whose 'name' property has a value that is equal to the 'name' passed into the method
|
|
||||||
*/
|
|
||||||
- (OSCProfile *) OSCProfileWithName:(NSString*)name {
|
|
||||||
NSMutableArray *profiles = [self getAllProfiles];
|
|
||||||
|
|
||||||
for (OSCProfile *profile in profiles) {
|
|
||||||
|
|
||||||
if ([profile.name isEqualToString:name]) {
|
|
||||||
return profile;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an array of encoded 'OSCProfile' objects from the array of decoded 'OSCProfile' objects passed into this method
|
|
||||||
*/
|
|
||||||
- (NSMutableArray *) encodedProfilesFromArray:(NSMutableArray *)profiles {
|
|
||||||
|
|
||||||
NSMutableArray *profilesEncoded = [[NSMutableArray alloc] init];
|
|
||||||
/* encode each profile and add them back into an array */
|
|
||||||
for (OSCProfile *profile in profiles) { //
|
|
||||||
NSData *profileEncoded = [NSKeyedArchiver archivedDataWithRootObject:profile requiringSecureCoding:YES error:nil];
|
|
||||||
[profilesEncoded addObject:profileEncoded];
|
|
||||||
}
|
|
||||||
|
|
||||||
return profilesEncoded;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete an 'OSCProfile' object of specific index for another in the 'OSCProfile' objects array stored in persistent storage
|
|
||||||
*/
|
|
||||||
-(void)deleteCurrentSelectedProfile{
|
|
||||||
NSMutableArray *profiles = [self getAllProfiles];
|
|
||||||
NSInteger selectedIndex = [self getIndexOfSelectedProfile];
|
|
||||||
|
|
||||||
/* Remove the selected profile */
|
|
||||||
if(selectedIndex != 0) [profiles removeObjectAtIndex:selectedIndex]; //deleting default profile (index 0) is not allowed
|
|
||||||
else {
|
|
||||||
NSLog(@"Default profile!"); // to be updated here.
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedIndex--;
|
|
||||||
if(selectedIndex < 0) selectedIndex = 0;
|
|
||||||
OSCProfile *newSelectedProfile = [profiles objectAtIndex:selectedIndex];
|
|
||||||
newSelectedProfile.isSelected = YES;
|
|
||||||
|
|
||||||
NSMutableArray *profilesEncoded = [self encodedProfilesFromArray:profiles]; // encode each 'profile' object in the array and add them to a new array
|
|
||||||
|
|
||||||
/* Encode the array itself, NOT the objects in the array, which are already encoded. Save array to persistent storage */
|
|
||||||
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:profilesEncoded requiringSecureCoding:YES error:nil];
|
|
||||||
[[NSUserDefaults standardUserDefaults] setObject:data forKey:@"OSCProfiles"];
|
|
||||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replaces one 'OSCProfile' object for another in the 'OSCProfile' objects array stored in persistent storage
|
|
||||||
*/
|
|
||||||
- (void) replaceProfile:(OSCProfile*)oldProfile withProfile:(OSCProfile*)newProfile {
|
|
||||||
NSMutableArray *profiles = [self getAllProfiles];
|
|
||||||
|
|
||||||
for (OSCProfile *profile in profiles) { // set all profiles' 'isSelected' property to NO since the new profile we're saving over the old profile with will be the 'selected' profile
|
|
||||||
profile.isSelected = NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Set the new profile as the selected one. The reasoning behind this is that this method is currently being used when the user saves over an existing profile with another profile that has the same name. The expected behavior is that the newly saved profile becomes the selected profile which will show on screen when they launch the game stream view */
|
|
||||||
newProfile.isSelected = YES;
|
|
||||||
|
|
||||||
/* Remove the old profile from the array and insert the new profile into its place */
|
|
||||||
int index = 0;
|
|
||||||
for (int i = 0; i < profiles.count; i++) {
|
|
||||||
|
|
||||||
if ([[profiles[i] name] isEqualToString: oldProfile.name]) {
|
|
||||||
index = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
[profiles removeObjectAtIndex:index];
|
|
||||||
[profiles insertObject:newProfile atIndex:index];
|
|
||||||
|
|
||||||
NSMutableArray *profilesEncoded = [self encodedProfilesFromArray:profiles]; // encode each 'profile' object in the array and add them to a new array
|
|
||||||
|
|
||||||
/* Encode the array itself, NOT the objects in the array, which are already encoded. Save array to persistent storage */
|
|
||||||
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:profilesEncoded requiringSecureCoding:YES error:nil];
|
|
||||||
[[NSUserDefaults standardUserDefaults] setObject:data forKey:@"OSCProfiles"];
|
|
||||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSMutableArray *) getEncodedProfiles {
|
|
||||||
NSMutableArray *profiles = [self getAllProfiles];
|
|
||||||
NSMutableArray *profilesEncoded = [self encodedProfilesFromArray:profiles]; // encode each 'profile' object in the array and add them to a new array
|
|
||||||
return profilesEncoded;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void) importEncodedProfiles:(NSMutableArray* )profilesEncoded {
|
|
||||||
NSMutableArray* targetProfiles = [_currentProfiles mutableCopy]; //_currentProfiles is availabled as long as getAllProfiles was called before calling this method (_currentProfiles avoids frequent accessing persisted data)
|
|
||||||
OSCProfile* profile;
|
|
||||||
profile = [self findProfileByName:DEFAULT_TEMPLATE_NAME1 inProfileArray:targetProfiles];
|
|
||||||
if(profile && targetProfiles.count > 1) [targetProfiles removeObject:profile];
|
|
||||||
profile = [self findProfileByName:DEFAULT_TEMPLATE_NAME2 inProfileArray:targetProfiles];
|
|
||||||
if(profile && targetProfiles.count > 1) [targetProfiles removeObject:profile];
|
|
||||||
if(targetProfiles.count > 0) [targetProfiles removeObjectAtIndex:0];
|
|
||||||
NSMutableArray* localEncodedPofiles = [self encodedProfilesFromArray:targetProfiles];
|
|
||||||
[profilesEncoded addObjectsFromArray:localEncodedPofiles];
|
|
||||||
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:profilesEncoded requiringSecureCoding:YES error:nil];
|
|
||||||
[[NSUserDefaults standardUserDefaults] setObject:data forKey:@"OSCProfiles"];
|
|
||||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
- (BOOL)isIPhone{
|
|
||||||
return ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
- (void) importDefaultTemplates{
|
|
||||||
NSString *filePath = [[NSBundle mainBundle] pathForResource: [self isIPhone] ? @"widgetTemplatesIPhone": @"widgetTemplates" ofType:@"bin"];
|
|
||||||
if (filePath) {
|
|
||||||
// 2. 读取二进制数据
|
|
||||||
NSError *error;
|
|
||||||
NSData *fileData = [NSData dataWithContentsOfFile:filePath options:NSDataReadingMappedIfSafe error:&error];
|
|
||||||
NSMutableArray *profilesEncoded;
|
|
||||||
if (fileData && !error) {
|
|
||||||
// 3. 成功读取
|
|
||||||
NSSet *classes = [NSSet setWithObjects: [NSMutableData class], [NSMutableArray class], nil];
|
|
||||||
profilesEncoded = [NSKeyedUnarchiver unarchivedObjectOfClasses:classes fromData:fileData error:&error];
|
|
||||||
[self importEncodedProfiles:profilesEncoded];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#pragma mark - Globally Accessible Methods
|
|
||||||
|
|
||||||
#pragma mark - Getters
|
|
||||||
|
|
||||||
- (NSMutableArray *) getAllProfiles {
|
|
||||||
|
|
||||||
NSData *profilesArrayEncoded = [[NSUserDefaults standardUserDefaults] objectForKey: @"OSCProfiles"]; // Get the encoded array of encoded OSC profiles from persistent storage
|
|
||||||
NSSet *classes = [NSSet setWithObjects:[NSString class], [NSMutableData class], [NSMutableArray class], [OSCProfile class], [OnScreenButtonState class], nil];
|
|
||||||
|
|
||||||
NSMutableArray *profilesEncoded = [NSKeyedUnarchiver unarchivedObjectOfClasses:classes fromData:profilesArrayEncoded error:nil]; // Decode the encoded array itself, NOT the objects contained in the array
|
|
||||||
|
|
||||||
/* Decode each of the encoded profiles, place them into an array, then return the array */
|
|
||||||
NSMutableArray *profilesDecoded = [[NSMutableArray alloc] init];
|
|
||||||
OSCProfile *profileDecoded;
|
|
||||||
for (NSData *profileEncoded in profilesEncoded) {
|
|
||||||
|
|
||||||
profileDecoded = [NSKeyedUnarchiver unarchivedObjectOfClasses: classes fromData:profileEncoded error: nil];
|
|
||||||
[profilesDecoded addObject: profileDecoded];
|
|
||||||
}
|
|
||||||
_currentProfiles = profilesDecoded;
|
|
||||||
return profilesDecoded;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (OSCProfile *) getSelectedProfile {
|
|
||||||
NSMutableArray *profiles = [self getAllProfiles];
|
|
||||||
|
|
||||||
for (OSCProfile *profile in profiles) {
|
|
||||||
|
|
||||||
if (profile.isSelected) {
|
|
||||||
return profile;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSInteger) getIndexOfSelectedProfile {
|
|
||||||
NSMutableArray *profiles = [self getAllProfiles];
|
|
||||||
for (OSCProfile *profile in profiles) {
|
|
||||||
|
|
||||||
if (profile.isSelected == YES) {
|
|
||||||
return [profiles indexOfObject:profile];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0; // if none of the profiles in the array have their 'isSelected' property set to YES (which should not be possible) return the 'Default' profile as the 'selected' profile
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#pragma mark - Setters
|
|
||||||
|
|
||||||
- (void) setProfileToSelected:(NSString *)name {
|
|
||||||
NSMutableArray *profiles = [self getAllProfiles];
|
|
||||||
|
|
||||||
/* Iterate through each profile. If its name equals the value of the 'name' parameter passed into this method then set the profile's 'isSelected' property to YES, otherwise set the value to NO */
|
|
||||||
for (OSCProfile *profile in profiles) {
|
|
||||||
|
|
||||||
if ([profile.name isEqualToString:name]) {
|
|
||||||
profile.isSelected = YES;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
profile.isSelected = NO;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NSMutableArray *profilesEncoded = [self encodedProfilesFromArray:profiles]; // encode each 'profile' object in the array and add them to a new array
|
|
||||||
|
|
||||||
/* Encode the array itself, NOT the objects inside the array, which have already been encoded by this point */
|
|
||||||
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:profilesEncoded requiringSecureCoding:YES error:nil];
|
|
||||||
[[NSUserDefaults standardUserDefaults] setObject:data forKey:@"OSCProfiles"];
|
|
||||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
- (bool) updateSelectedProfile:(NSMutableArray *) oscButtonLayers {
|
|
||||||
NSMutableArray* buttonStatesEncoded = [self convertOnScreenControllerAndWidgetsToButtonStates:oscButtonLayers];
|
|
||||||
if([self getIndexOfSelectedProfile]==0) return false;
|
|
||||||
OSCProfile *newProfile = [[OSCProfile alloc] initWithName:[self getSelectedProfile].name
|
|
||||||
buttonStates:buttonStatesEncoded isSelected:YES]; // create a new 'OSCProfile'. Set the array of encoded button states created above to the 'buttonStates' property of the new profile, along with a 'name'. Set 'isSelected' argument to YES which will set this saved profile as the one that will show up in the game stream view
|
|
||||||
|
|
||||||
|
|
||||||
/* set all saved OSCProfiles 'isSelected' property to NO since the new profile you're adding will be set as the selected profile */
|
|
||||||
NSMutableArray *profiles = [self getAllProfiles];
|
|
||||||
for (OSCProfile *profile in profiles) {
|
|
||||||
profile.isSelected = NO;
|
|
||||||
}
|
|
||||||
[self replaceProfile:[self getSelectedProfile] withProfile:newProfile];
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
- (void) saveProfileWithName:(NSString*)name andButtonLayers:(NSMutableArray *)oscButtonLayers {
|
|
||||||
NSMutableArray* buttonStatesEncoded = [self convertOnScreenControllerAndWidgetsToButtonStates:oscButtonLayers];
|
|
||||||
OSCProfile *newProfile = [[OSCProfile alloc] initWithName:name
|
|
||||||
buttonStates:buttonStatesEncoded isSelected:YES]; // create a new 'OSCProfile'. Set the array of encoded button states created above to the 'buttonStates' property of the new profile, along with a 'name'. Set 'isSelected' argument to YES which will set this saved profile as the one that will show up in the game stream view
|
|
||||||
/* set all saved OSCProfiles 'isSelected' property to NO since the new profile you're adding will be set as the selected profile */
|
|
||||||
NSMutableArray *profiles = [self getAllProfiles];
|
|
||||||
for (OSCProfile *profile in profiles) {
|
|
||||||
|
|
||||||
profile.isSelected = NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ([self profileNameAlreadyExist:name]) { // if a saved profile with the same 'name' already exists in persistent storage then overwrite it and save the change to persistent storage
|
|
||||||
[self replaceProfile:[self OSCProfileWithName:name] withProfile:newProfile];
|
|
||||||
}
|
|
||||||
else { // otherwise encode then add the new profile to the end of the OSCProfiles array
|
|
||||||
NSData *newProfileEncoded = [NSKeyedArchiver archivedDataWithRootObject:newProfile requiringSecureCoding:YES error:nil];
|
|
||||||
NSMutableArray *profilesEncoded = [self encodedProfilesFromArray:profiles];
|
|
||||||
[profilesEncoded addObject:newProfileEncoded];
|
|
||||||
|
|
||||||
/* Encode the 'profilesEncoded' array itself, NOT the objects in the 'profilesEncoded' array, all of which are already encoded by this point */
|
|
||||||
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:profilesEncoded requiringSecureCoding:YES error:nil];
|
|
||||||
[[NSUserDefaults standardUserDefaults] setObject:data forKey:@"OSCProfiles"];
|
|
||||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
||||||
|
|
||||||
// saving test:
|
|
||||||
/*
|
|
||||||
NSString *documentsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
|
|
||||||
NSString *path = [documentsPath stringByAppendingPathComponent:@"newDefault.bin"];
|
|
||||||
|
|
||||||
[profiles writeToFile:path atomically:YES];
|
|
||||||
NSLog(@"默认 Profile 数据已保存到: %@", path);
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (CGPoint)normalizeWidgetPosition:(CGPoint)position {
|
|
||||||
CGPoint newPosition = position;
|
|
||||||
if(position.x > 1.0 && position.y >1.0){
|
|
||||||
position.x = position.x / layoutViewBounds.size.width;
|
|
||||||
position.y = position.y / layoutViewBounds.size.height;
|
|
||||||
}
|
|
||||||
// asdfsda;
|
|
||||||
NSLog(@"sef.view bounds: %f, %f", layoutViewBounds.size.width, layoutViewBounds.size.height);
|
|
||||||
NSLog(@"position: %f, %f, denormalized position: %f, %f", position.x, position.y, newPosition.x, newPosition.y);
|
|
||||||
return position;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (CGPoint)denormalizeWidgetPosition:(CGPoint)position {
|
|
||||||
if(position.x < 1.0 && position.y < 1.0){
|
|
||||||
position.x = position.x * layoutViewBounds.size.width;
|
|
||||||
position.y = position.y * layoutViewBounds.size.height;
|
|
||||||
}
|
|
||||||
return position;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSMutableArray* ) convertOnScreenControllerAndWidgetsToButtonStates:(NSMutableArray *) oscButtonLayers {
|
|
||||||
/* iterate through each OSC button the user sees on screen, create an 'OnScreenButtonState' object from each button, encode each object, and then add each object to an array */
|
|
||||||
/*
|
|
||||||
NSSet *validPositionButtonNames = [NSSet setWithObjects:
|
|
||||||
@"l2Button",
|
|
||||||
@"l1Button",
|
|
||||||
@"dPad",
|
|
||||||
@"selectButton",
|
|
||||||
@"leftStickBackground",
|
|
||||||
@"rightStickBackground",
|
|
||||||
@"r2Button",
|
|
||||||
@"r1Button",
|
|
||||||
@"aButton",
|
|
||||||
@"bButton",
|
|
||||||
@"xButton",
|
|
||||||
@"yButton",
|
|
||||||
@"startButton",
|
|
||||||
nil]; */
|
|
||||||
NSMutableArray *buttonStatesEncoded = [[NSMutableArray alloc] init];
|
|
||||||
|
|
||||||
// save on-screen game controller buttons & sticks as buttonstate:
|
|
||||||
for (CALayer *oscButtonLayer in oscButtonLayers) {
|
|
||||||
CGPoint normalizedPosition = [self normalizeWidgetPosition:oscButtonLayer.position];
|
|
||||||
OnScreenButtonState *buttonState = [[OnScreenButtonState alloc] initWithButtonName:oscButtonLayer.name buttonType:LegacyOscButton andPosition:normalizedPosition];
|
|
||||||
// add hidden attr here
|
|
||||||
buttonState.isHidden = oscButtonLayer.isHidden;
|
|
||||||
buttonState.oscLayerSizeFactor = [OnScreenControls getControllerLayerSizeFactor:oscButtonLayer];
|
|
||||||
buttonState.backgroundAlpha = oscButtonLayer.opacity;
|
|
||||||
|
|
||||||
NSNumber *style = [OnScreenControls.layerVibrationStyleDic objectForKey:oscButtonLayer.name];
|
|
||||||
if ([style isKindOfClass:[NSNumber class]]) {
|
|
||||||
buttonState.vibrationStyle = [style unsignedCharValue];
|
|
||||||
// 使用 val
|
|
||||||
}
|
|
||||||
else buttonState.vibrationStyle = UIImpactFeedbackStyleLight;
|
|
||||||
|
|
||||||
// NSLog(@"oscLayerName: %@, opacity: %f, ", oscButtonLayer.name, buttonState.backgroundAlpha);
|
|
||||||
// buttonState.oscLayerSizeFactor = oscButtonLayer.bounds;
|
|
||||||
|
|
||||||
NSData *buttonStateEncoded = [NSKeyedArchiver archivedDataWithRootObject:buttonState requiringSecureCoding:YES error:nil];
|
|
||||||
[buttonStatesEncoded addObject: buttonStateEncoded];
|
|
||||||
}
|
|
||||||
|
|
||||||
// save on-screen widget views (keyboard & mouse command) as buttonstate:
|
|
||||||
for(OnScreenWidgetView* widgetView in OnScreenWidgetViews){
|
|
||||||
CGPoint normalizedPosition = [self normalizeWidgetPosition:widgetView.center];
|
|
||||||
OnScreenButtonState *buttonState = [[OnScreenButtonState alloc] initWithButtonName:widgetView.cmdString buttonType:CustomOnScreenWidget andPosition:normalizedPosition];
|
|
||||||
buttonState.alias = widgetView.buttonLabel;
|
|
||||||
buttonState.widthFactor = [self normalizeSizeWidthFactor:widgetView];
|
|
||||||
NSLog(@"logging widthFactor %f", buttonState.widthFactor);
|
|
||||||
buttonState.heightFactor = [self normalizeSizeHeightFactor:widgetView];
|
|
||||||
buttonState.backgroundAlpha = widgetView.backgroundAlpha;
|
|
||||||
buttonState.borderWidth = widgetView.borderWidth;
|
|
||||||
buttonState.vibrationStyle = widgetView.vibrationStyle;
|
|
||||||
buttonState.mouseButtonAction = widgetView.mouseButtonAction;
|
|
||||||
buttonState.sensitivityFactorX = widgetView.sensitivityFactorX;
|
|
||||||
buttonState.sensitivityFactorY = widgetView.sensitivityFactorY;
|
|
||||||
buttonState.decelerationRate = widgetView.trackballDecelerationRate;
|
|
||||||
buttonState.stickIndicatorOffset = widgetView.stickIndicatorOffset;
|
|
||||||
buttonState.widgetShape = widgetView.shape;
|
|
||||||
buttonState.minStickOffset = widgetView.minStickOffset;
|
|
||||||
buttonState.slideMode = widgetView.slideMode;
|
|
||||||
|
|
||||||
NSData *buttonStateEncoded = [NSKeyedArchiver archivedDataWithRootObject:buttonState requiringSecureCoding:YES error:nil];
|
|
||||||
[buttonStatesEncoded addObject: buttonStateEncoded];
|
|
||||||
|
|
||||||
}
|
|
||||||
return buttonStatesEncoded;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (CGFloat)normalizeSizeWidthFactor:(OnScreenWidgetView* )widget{
|
|
||||||
CGFloat screenWidthInPoints = CGRectGetWidth([[UIScreen mainScreen] bounds]);
|
|
||||||
return widget.bounds.size.width/screenWidthInPoints * 10000;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (CGFloat)normalizeSizeHeightFactor:(OnScreenWidgetView* )widget{
|
|
||||||
CGFloat screenWidthInPoints = CGRectGetWidth([[UIScreen mainScreen] bounds]);
|
|
||||||
return widget.bounds.size.height/screenWidthInPoints * 10000;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#pragma mark - Queries
|
|
||||||
|
|
||||||
- (BOOL) profileNameAlreadyExist:(NSString*)name {
|
|
||||||
NSMutableArray *profiles = [self getAllProfiles];
|
|
||||||
/* Iterate through the decoded profiles and return 'YES' if one of the profiles' 'name' properties equals the 'name' passed into this method */
|
|
||||||
for (OSCProfile *profile in profiles) {
|
|
||||||
if ([profile.name isEqualToString:name]) {
|
|
||||||
return YES;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (OSCProfile *) findProfileByName:(NSString*) name inProfileArray:(NSMutableArray*)profiles {
|
|
||||||
/* Iterate through the decoded profiles and return 'YES' if one of the profiles' 'name' properties equals the 'name' passed into this method */
|
|
||||||
for (OSCProfile *profile in profiles) {
|
|
||||||
if ([profile.name isEqualToString:name]) {
|
|
||||||
return profile;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (OnScreenButtonState *)unarchiveButtonStateEncoded:(NSData *)data {
|
|
||||||
OnScreenButtonState* buttonState;
|
|
||||||
buttonState = [NSKeyedUnarchiver unarchivedObjectOfClasses:[NSSet setWithObjects:[NSString class], [OnScreenButtonState class], nil]
|
|
||||||
fromData:data
|
|
||||||
error:nil];
|
|
||||||
return buttonState;
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -0,0 +1,584 @@
|
|||||||
|
//
|
||||||
|
// OSCProfilesManager.m -> OSCProfilesManager.swift
|
||||||
|
// Moonlight
|
||||||
|
//
|
||||||
|
// Created by Long Le on 1/1/23.
|
||||||
|
// Copyright © 2023 Moonlight Game Streaming Project. All rights reserved.
|
||||||
|
//
|
||||||
|
// Modified by True砖家 since 2024.6.24
|
||||||
|
// Copyright © 2024 True砖家 @ Bilibili. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
@objcMembers
|
||||||
|
class OSCProfilesManager: NSObject {
|
||||||
|
private static let profilesDefaultsKey = "OSCProfiles"
|
||||||
|
// private static let widgetProfileUpdatedKey = "widgetProfileUpdated-20260606"
|
||||||
|
private static let widgetProfileUpdatedKey = "widgetProfileUpdated-20260801"
|
||||||
|
|
||||||
|
private static var sharedInstance: OSCProfilesManager?
|
||||||
|
private static var onScreenWidgetViews: NSMutableSet?
|
||||||
|
private static var layoutViewBounds: CGRect = .zero
|
||||||
|
|
||||||
|
var currentProfiles: NSMutableArray = []
|
||||||
|
var widgetSizeTransition: WidgetSizeTransition = .keepWidgetSize
|
||||||
|
|
||||||
|
@objc(sharedManager:)
|
||||||
|
class func sharedManager(_ viewBounds: CGRect) -> OSCProfilesManager {
|
||||||
|
if viewBounds != .zero {
|
||||||
|
layoutViewBounds = viewBounds
|
||||||
|
}
|
||||||
|
if let sharedInstance {
|
||||||
|
return sharedInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
let manager = OSCProfilesManager()
|
||||||
|
sharedInstance = manager
|
||||||
|
return manager
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(setOnScreenWidgetViewsSet:)
|
||||||
|
class func setOnScreenWidgetViewsSet(_ set: NSMutableSet) {
|
||||||
|
onScreenWidgetViews = set
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(setLayoutViewBounds:)
|
||||||
|
class func setLayoutViewBounds(_ bounds: CGRect) {
|
||||||
|
layoutViewBounds = bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
private static let profileCodingClasses: [AnyClass] = [
|
||||||
|
NSString.self,
|
||||||
|
NSMutableData.self,
|
||||||
|
NSMutableArray.self,
|
||||||
|
OSCProfile.self,
|
||||||
|
OnScreenButtonState.self,
|
||||||
|
]
|
||||||
|
|
||||||
|
private static let buttonStateCodingClasses: [AnyClass] = [
|
||||||
|
NSString.self,
|
||||||
|
OnScreenButtonState.self,
|
||||||
|
NSSet.self,
|
||||||
|
NSNumber.self,
|
||||||
|
]
|
||||||
|
|
||||||
|
private func oscProfile(withName name: String) -> OSCProfile? {
|
||||||
|
let profiles = getAllProfiles()
|
||||||
|
for case let profile as OSCProfile in profiles where profile.name == name {
|
||||||
|
return profile
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func encodedProfiles(from profiles: NSMutableArray) -> NSMutableArray {
|
||||||
|
let profilesEncoded = NSMutableArray()
|
||||||
|
for case let profile as OSCProfile in profiles {
|
||||||
|
if let profileEncoded = try? NSKeyedArchiver.archivedData(withRootObject: profile, requiringSecureCoding: true) {
|
||||||
|
profilesEncoded.add(profileEncoded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return profilesEncoded
|
||||||
|
}
|
||||||
|
|
||||||
|
private func persistProfiles(_ profiles: NSMutableArray) {
|
||||||
|
let profilesEncoded = encodedProfiles(from: profiles)
|
||||||
|
persistEncodedProfiles(profilesEncoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
func persistProfileOrder(_ orderedProfiles: NSMutableArray) {
|
||||||
|
persistProfiles(orderedProfiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func persistEncodedProfiles(_ profilesEncoded: NSMutableArray) {
|
||||||
|
guard let data = try? NSKeyedArchiver.archivedData(withRootObject: profilesEncoded, requiringSecureCoding: true) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let defaults = UserDefaults.standard
|
||||||
|
defaults.set(data, forKey: Self.profilesDefaultsKey)
|
||||||
|
defaults.synchronize()
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteCurrentSelectedProfile() {
|
||||||
|
let profiles = getAllProfiles()
|
||||||
|
var selectedIndex = getIndexOfSelectedProfile()
|
||||||
|
|
||||||
|
if selectedIndex != 0 {
|
||||||
|
profiles.removeObject(at: selectedIndex)
|
||||||
|
} else {
|
||||||
|
NSLog("Default profile!")
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedIndex -= 1
|
||||||
|
if selectedIndex < 0 {
|
||||||
|
selectedIndex = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if let newSelectedProfile = profiles[safe: selectedIndex] as? OSCProfile {
|
||||||
|
newSelectedProfile.isSelected = true
|
||||||
|
}
|
||||||
|
|
||||||
|
persistProfiles(profiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
func replaceSelectedProfile(with newProfile: OSCProfile, overwriteDefault: Bool) {
|
||||||
|
let profiles = getAllProfiles()
|
||||||
|
var index = 0
|
||||||
|
|
||||||
|
for case let profile as OSCProfile in profiles where profile.isSelected {
|
||||||
|
index = profiles.index(of: profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
if index > 0 || overwriteDefault {
|
||||||
|
profiles.replaceObject(at: index, with: newProfile)
|
||||||
|
}
|
||||||
|
|
||||||
|
persistProfiles(profiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(replaceProfile:withProfile:)
|
||||||
|
func replaceProfile(_ oldProfile: OSCProfile, withProfile newProfile: OSCProfile) {
|
||||||
|
let profiles = getAllProfiles()
|
||||||
|
|
||||||
|
for case let profile as OSCProfile in profiles {
|
||||||
|
profile.isSelected = false
|
||||||
|
}
|
||||||
|
|
||||||
|
newProfile.isSelected = true
|
||||||
|
|
||||||
|
var index = 0
|
||||||
|
for i in 0 ..< profiles.count {
|
||||||
|
guard let profile = profiles[i] as? OSCProfile else { continue }
|
||||||
|
if profile.name == oldProfile.name {
|
||||||
|
index = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
profiles.removeObject(at: index)
|
||||||
|
profiles.insert(newProfile, at: index)
|
||||||
|
persistProfiles(profiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
func replace(_ oldProfile: OSCProfile, with newProfile: OSCProfile) {
|
||||||
|
replaceProfile(oldProfile, withProfile: newProfile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEncodedProfiles() -> NSMutableArray {
|
||||||
|
guard
|
||||||
|
let profilesArrayEncoded = UserDefaults.standard.object(forKey: Self.profilesDefaultsKey) as? Data,
|
||||||
|
let profilesEncoded = try? NSKeyedUnarchiver.unarchivedObject(ofClasses: Self.profileCodingClasses, from: profilesArrayEncoded) as? NSMutableArray
|
||||||
|
else {
|
||||||
|
return NSMutableArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
return profilesEncoded
|
||||||
|
}
|
||||||
|
|
||||||
|
func importEncodedProfiles(_ profilesEncoded: NSMutableArray) {
|
||||||
|
let localProfiles = currentProfiles.mutableCopy() as? NSMutableArray ?? NSMutableArray()
|
||||||
|
let profilesDecoded = decodeProfiles(from: profilesEncoded)
|
||||||
|
let importedIncludesDefault = (profilesDecoded.firstObject as? OSCProfile)?.name == "Default"
|
||||||
|
let isSingleProfile = profilesEncoded.count == 1
|
||||||
|
|
||||||
|
if importedIncludesDefault && localProfiles.count > 0 {
|
||||||
|
localProfiles.removeObject(at: 0)
|
||||||
|
} else if !importedIncludesDefault,
|
||||||
|
let localDefaultProfile = localProfiles.firstObject as? OSCProfile,
|
||||||
|
localDefaultProfile.name == "Default" {
|
||||||
|
profilesDecoded.insert(localDefaultProfile, at: 0)
|
||||||
|
localProfiles.removeObject(at: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
let localProfileNames = Set(
|
||||||
|
localProfiles.compactMap { ($0 as? OSCProfile)?.name }
|
||||||
|
)
|
||||||
|
|
||||||
|
for case let profile as OSCProfile in profilesDecoded {
|
||||||
|
if localProfileNames.contains(profile.name) {
|
||||||
|
// profile.name = "\(profile.name)-\(LocalizationHelper.localizedString(forKey:"Restored"))"
|
||||||
|
profile.name = "\(profile.name) - Restored"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let importedProfileNames = Set(
|
||||||
|
profilesDecoded.compactMap { ($0 as? OSCProfile)?.name }
|
||||||
|
)
|
||||||
|
|
||||||
|
var localProfilesToRemove: [OSCProfile] = []
|
||||||
|
for case let profile as OSCProfile in localProfiles {
|
||||||
|
if importedProfileNames.contains(profile.name) {localProfilesToRemove.append(profile)}
|
||||||
|
}
|
||||||
|
|
||||||
|
for case let profile as Any in localProfilesToRemove {
|
||||||
|
localProfiles.remove(profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
var indexOffset = importedIncludesDefault ? 0: 0
|
||||||
|
for profile in localProfiles {
|
||||||
|
profilesDecoded.insert(profile, at: 1+indexOffset)
|
||||||
|
indexOffset += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if isSingleProfile, !importedIncludesDefault {
|
||||||
|
for profile in profilesDecoded {
|
||||||
|
if let profile = profile as? OSCProfile {
|
||||||
|
profile.isSelected = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let profile = profilesDecoded[profilesDecoded.count-1] as? OSCProfile {
|
||||||
|
profile.isSelected = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
persistEncodedProfiles(encodedProfiles(from: profilesDecoded))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isIPhone() -> Bool {
|
||||||
|
UIDevice.current.userInterfaceIdiom == .phone
|
||||||
|
}
|
||||||
|
|
||||||
|
func importDefaultTemplates(selectedIndex: UInt32? = nil) {
|
||||||
|
guard let filePath = Bundle.main.path(forResource: isIPhone() ? "widgetTemplatesIPhone" : "widgetTemplates", ofType: "bin") else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let fileData = try Data(contentsOf: URL(fileURLWithPath: filePath), options: .mappedIfSafe)
|
||||||
|
let profilePayloadData = try ProfileFileContainer.unpackedData(from: fileData)
|
||||||
|
if let profilesEncoded = try NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSMutableData.self, NSMutableArray.self], from: profilePayloadData) as? NSMutableArray {
|
||||||
|
importEncodedProfiles(profilesEncoded)
|
||||||
|
if let selectedIndex = selectedIndex {
|
||||||
|
setProfileToSelected(selectedIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
func updateDefaultTemplates() {
|
||||||
|
guard let filePath = Bundle.main.path(forResource: isIPhone() ? "widgetTemplatesIPhone" : "widgetTemplates", ofType: "bin") else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let fileData = try Data(contentsOf: URL(fileURLWithPath: filePath), options: .mappedIfSafe)
|
||||||
|
guard let defaultProfilesEncoded = try NSKeyedUnarchiver.unarchivedObject(ofClasses: Self.profileCodingClasses, from: fileData) as? NSMutableArray else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let targetProfilesDecoded = getAllProfiles()
|
||||||
|
var newPencilProProfile: OSCProfile?
|
||||||
|
var pencilProProfileExisted = false
|
||||||
|
|
||||||
|
for case let profileEncoded as Data in defaultProfilesEncoded {
|
||||||
|
guard let defaultProfileDecoded = try NSKeyedUnarchiver.unarchivedObject(ofClasses: Self.profileCodingClasses, from: profileEncoded) as? OSCProfile else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let targetProfile = findProfile(byName: defaultProfileDecoded.name, inProfileArray: targetProfilesDecoded)
|
||||||
|
if defaultProfileDecoded.name == "Pencil Pro" {
|
||||||
|
newPencilProProfile = defaultProfileDecoded.mutableCopy() as? OSCProfile
|
||||||
|
}
|
||||||
|
|
||||||
|
if let targetProfile {
|
||||||
|
if targetProfile.name == "Pencil Pro" {
|
||||||
|
pencilProProfileExisted = true
|
||||||
|
}
|
||||||
|
let targetIndex = targetProfilesDecoded.index(of: targetProfile)
|
||||||
|
targetProfilesDecoded.replaceObject(at: targetIndex, with: defaultProfileDecoded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let newPencilProProfile, !pencilProProfileExisted {
|
||||||
|
targetProfilesDecoded.insert(newPencilProProfile, at: 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
persistProfiles(targetProfilesDecoded)
|
||||||
|
setProfileToSelected(0)
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
func getAllProfiles() -> NSMutableArray {
|
||||||
|
guard
|
||||||
|
let profilesArrayEncoded = UserDefaults.standard.object(forKey: Self.profilesDefaultsKey) as? Data,
|
||||||
|
let profilesEncoded = try? NSKeyedUnarchiver.unarchivedObject(ofClasses: Self.profileCodingClasses, from: profilesArrayEncoded) as? NSMutableArray
|
||||||
|
else {
|
||||||
|
currentProfiles = NSMutableArray()
|
||||||
|
return currentProfiles
|
||||||
|
}
|
||||||
|
|
||||||
|
let profilesDecoded = decodeProfiles(from: profilesEncoded)
|
||||||
|
currentProfiles = profilesDecoded
|
||||||
|
return profilesDecoded
|
||||||
|
}
|
||||||
|
|
||||||
|
private func decodeProfiles(from profilesEncoded: NSMutableArray) -> NSMutableArray {
|
||||||
|
let profilesDecoded = NSMutableArray()
|
||||||
|
for case let profileEncoded as Data in profilesEncoded {
|
||||||
|
guard let profileDecoded = try? NSKeyedUnarchiver.unarchivedObject(ofClasses: Self.profileCodingClasses, from: profileEncoded) as? OSCProfile else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
profilesDecoded.add(profileDecoded)
|
||||||
|
}
|
||||||
|
return profilesDecoded
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSelectedProfile() -> OSCProfile {
|
||||||
|
var profiles = getAllProfiles()
|
||||||
|
let defaults = UserDefaults.standard
|
||||||
|
let needImportDefaultTemplates = defaults.object(forKey: Self.widgetProfileUpdatedKey) == nil
|
||||||
|
|
||||||
|
var selectedIndex: UInt32 = 0
|
||||||
|
for case let profile as OSCProfile in profiles where profile.isSelected {
|
||||||
|
selectedIndex = UInt32(profiles.index(of: profile))
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentProfileCount = profiles.count
|
||||||
|
|
||||||
|
if (currentProfileCount == 0 || needImportDefaultTemplates) && !GenericUtils.pencilProPurchaseProcessedWithImportingWidgetTemplates {
|
||||||
|
selectedIndex = currentProfileCount == 0 ? 0 : selectedIndex
|
||||||
|
importDefaultTemplates(selectedIndex: selectedIndex)
|
||||||
|
defaults.set(true, forKey: Self.widgetProfileUpdatedKey)
|
||||||
|
defaults.synchronize()
|
||||||
|
profiles = getAllProfiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return profiles[Int(selectedIndex)] as! OSCProfile
|
||||||
|
|
||||||
|
// return (profiles.firstObject as? OSCProfile) ?? OSCProfile(name: "", buttonStates: NSMutableArray(), isSelected: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getIndexOfSelectedProfile() -> Int {
|
||||||
|
let profiles = getAllProfiles()
|
||||||
|
for case let profile as OSCProfile in profiles where profile.isSelected {
|
||||||
|
return profiles.index(of: profile)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func getIndexOfLastProfile() -> UInt32 {
|
||||||
|
let profiles = getAllProfiles()
|
||||||
|
return profiles.count > 0 ? UInt32(profiles.count - 1) : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func setProfileToSelected(_ tableIndex: UInt32) {
|
||||||
|
let profiles = getAllProfiles()
|
||||||
|
|
||||||
|
for (idx, element) in profiles.enumerated() {
|
||||||
|
guard let profile = element as? OSCProfile else { continue }
|
||||||
|
profile.isSelected = idx == Int(tableIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
persistProfiles(profiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateSelectedProfile(_ oscButtonLayers: NSMutableSet) -> Bool {
|
||||||
|
let buttonStatesEncoded = convertOnScreenControllerAndWidgetsToButtonStates(oscButtonLayers)
|
||||||
|
if getIndexOfSelectedProfile() == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let selectedProfile = getSelectedProfile()
|
||||||
|
selectedProfile.buttonStatesEncoded = buttonStatesEncoded
|
||||||
|
selectedProfile.unfoldedExclusiveFolderSequence = OnScreenWidgetView.unfoldedExclusiveFolderSequence
|
||||||
|
selectedProfile.postExclusiveUnfoldedSequences = Set(OnScreenWidgetView.postExclusiveUnfoldedSequences.map { NSNumber(value: $0) })
|
||||||
|
replaceSelectedProfile(with: selectedProfile, overwriteDefault: false)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func duplicateSelectedProfile(withName name: String) {
|
||||||
|
if profileNameAlreadyExist(name) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let profiles = getAllProfiles()
|
||||||
|
var newProfile: OSCProfile?
|
||||||
|
|
||||||
|
for case let profile as OSCProfile in profiles where profile.isSelected {
|
||||||
|
profile.isSelected = false
|
||||||
|
newProfile = profile.mutableCopy() as? OSCProfile
|
||||||
|
newProfile?.isSelected = true
|
||||||
|
newProfile?.name = name
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let newProfile, let newProfileEncoded = try? NSKeyedArchiver.archivedData(withRootObject: newProfile, requiringSecureCoding: true) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let profilesEncoded = encodedProfiles(from: profiles)
|
||||||
|
profilesEncoded.add(newProfileEncoded)
|
||||||
|
persistEncodedProfiles(profilesEncoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeWidgetPosition(_ position: CGPoint) -> CGPoint {
|
||||||
|
var normalizedPosition = position
|
||||||
|
// let originalPosition = position
|
||||||
|
if abs(position.x) > 1.0, abs(position.y) > 1.0 {
|
||||||
|
normalizedPosition.x = position.x / Self.layoutViewBounds.size.width
|
||||||
|
normalizedPosition.y = position.y / Self.layoutViewBounds.size.height
|
||||||
|
}
|
||||||
|
// NSLog("layoutToolView bounds: %f, %f", Self.layoutViewBounds.size.width, Self.layoutViewBounds.size.height)
|
||||||
|
// NSLog("position: %f, %f, denormalized position: %f, %f", normalizedPosition.x, normalizedPosition.y, originalPosition.x, originalPosition.y)
|
||||||
|
return normalizedPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
private func denormalizeWidgetPosition(_ position: CGPoint) -> CGPoint {
|
||||||
|
var denormalizedPosition = position
|
||||||
|
if abs(position.x) < 2.01, abs(position.y) < 2.01 {
|
||||||
|
denormalizedPosition.x = position.x * Self.layoutViewBounds.size.width
|
||||||
|
denormalizedPosition.y = position.y * Self.layoutViewBounds.size.height
|
||||||
|
}
|
||||||
|
return denormalizedPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
private func convertOnScreenControllerAndWidgetsToButtonStates(_ oscButtonLayers: NSMutableSet) -> NSMutableArray {
|
||||||
|
let buttonStatesEncoded = NSMutableArray()
|
||||||
|
|
||||||
|
for case let oscButtonLayer as CALayer in oscButtonLayers {
|
||||||
|
if oscButtonLayer.isHidden {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let normalizedPosition = normalizeWidgetPosition(oscButtonLayer.position)
|
||||||
|
let layerName = oscButtonLayer.name ?? ""
|
||||||
|
let buttonState = OnScreenButtonState(buttonName: layerName, buttonType: 0, andPosition: normalizedPosition)
|
||||||
|
buttonState.isHidden = oscButtonLayer.isHidden
|
||||||
|
buttonState.oscLayerSizeFactor = OnScreenControls.getControllerLayerSizeFactor(oscButtonLayer)
|
||||||
|
buttonState.backgroundAlpha = CGFloat(oscButtonLayer.opacity)
|
||||||
|
|
||||||
|
if let style = OnScreenControls.layerVibrationStyleDic()?.object(forKey: layerName) as? NSNumber {
|
||||||
|
buttonState.vibrationStyle = style.uint8Value
|
||||||
|
} else {
|
||||||
|
buttonState.vibrationStyle = UInt8(UIImpactFeedbackGenerator.FeedbackStyle.light.rawValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let buttonStateEncoded = try? NSKeyedArchiver.archivedData(withRootObject: buttonState, requiringSecureCoding: true) {
|
||||||
|
buttonStatesEncoded.add(buttonStateEncoded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
widgetSizeTransition = .keepWidgetSize
|
||||||
|
for widgetView in OnScreenWidgetView.mapping.values {
|
||||||
|
let normalizedPosition = normalizeWidgetPosition(widgetView.storedCenter)
|
||||||
|
let buttonState = OnScreenButtonState(buttonName: widgetView.cmdString, buttonType: 1, andPosition: normalizedPosition)
|
||||||
|
buttonState.alias = widgetView.widgetLabel
|
||||||
|
buttonState.sequence = widgetView.sequence
|
||||||
|
buttonState.sequenceSet = Set(widgetView.sequenceSet.map { NSNumber(value: $0) })
|
||||||
|
buttonState.parentSequence = widgetView.parentSequence
|
||||||
|
buttonState.autoDockTimer = Int16(widgetView.autoDockIdleDuration)
|
||||||
|
buttonState.dockedAlpha = widgetView.autoDockSettledAlpha
|
||||||
|
buttonState.folded = widgetView.folded
|
||||||
|
buttonState.revealMode = UInt8(widgetView.revealMode.rawValue)
|
||||||
|
buttonState.bulkMoveEnabled = widgetView.bulkMoveEnabled
|
||||||
|
buttonState.widthFactor = normalizeSizeWidthFactor(with: widgetView)
|
||||||
|
buttonState.heightFactor = normalizeSizeHeightFactor(with: widgetView)
|
||||||
|
buttonState.backgroundAlpha = widgetView.originalBackgroundAlpha
|
||||||
|
buttonState.labelAlpha = widgetView.originalLabelAlpha
|
||||||
|
buttonState.borderAlpha = widgetView.borderAlpha
|
||||||
|
buttonState.highlightAlpha = widgetView.highlightAlpha
|
||||||
|
buttonState.borderWidth = widgetView.borderWidth
|
||||||
|
buttonState.highlightSizeFactor = widgetView.highlightSizeFactor
|
||||||
|
buttonState.autoTapInterval = UInt16(widgetView.autoTapInterval)
|
||||||
|
buttonState.autoTapRepeats = widgetView.autoTapRepeats
|
||||||
|
buttonState.vibrationStyle = UInt8(widgetView.vibrationStyle)
|
||||||
|
buttonState.mouseButtonAction = UInt8(widgetView.mouseButtonAction.rawValue)
|
||||||
|
buttonState.animatesTransition = widgetView.animatesTransition
|
||||||
|
buttonState.sensitivityFactorY = widgetView.sensitivityFactorY
|
||||||
|
buttonState.slideThreshold = widgetView.slideThreshold
|
||||||
|
buttonState.yawFactor = widgetView.yawFactor
|
||||||
|
buttonState.pitchFactor = widgetView.pitchFactor
|
||||||
|
buttonState.rollFactor = widgetView.rollFactor
|
||||||
|
buttonState.decelerationRateX = widgetView.decelerationRateX
|
||||||
|
buttonState.decelerationRateY = widgetView.decelerationRateY
|
||||||
|
buttonState.widgetShape = widgetView.shape
|
||||||
|
buttonState.walkModeThreshold = widgetView.dWheelWalkModeThreshold
|
||||||
|
buttonState.minStickOffset = widgetView.minStickOffset
|
||||||
|
buttonState.buttonMode = UInt8(widgetView.buttonMode.rawValue)
|
||||||
|
buttonState.sprintKeyActionType = widgetView.sprintKeyActionType.rawValue
|
||||||
|
buttonState.sprintKeyThreshold = widgetView.sprintKeyThreshold
|
||||||
|
buttonState.walkKeyActionType = widgetView.walkKeyActionType.rawValue
|
||||||
|
buttonState.walkKeyThreshold = widgetView.walkKeyThreshold
|
||||||
|
buttonState.sensitivityFactorX = widgetView.sensitivityFactorX
|
||||||
|
buttonState.componentSizeFactor = normalizeComponentSizeFactor(with: widgetView)
|
||||||
|
buttonState.touchPointAnchored = widgetView.touchPointAnchored
|
||||||
|
buttonState.stickIndicatorOffset = widgetView.stickIndicatorOffset
|
||||||
|
|
||||||
|
if let buttonStateEncoded = try? NSKeyedArchiver.archivedData(withRootObject: buttonState, requiringSecureCoding: true) {
|
||||||
|
buttonStatesEncoded.add(buttonStateEncoded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buttonStatesEncoded
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getReferenceLen() -> CGFloat {
|
||||||
|
let screenBounds = UIScreen.main.bounds
|
||||||
|
let screenWidthInPoints = screenBounds.width
|
||||||
|
let screenHeightInPoints = screenBounds.height
|
||||||
|
let longSideLen = max(screenWidthInPoints, screenHeightInPoints)
|
||||||
|
|
||||||
|
if widgetSizeTransition == .transitionWithOrientation {
|
||||||
|
return screenWidthInPoints
|
||||||
|
}
|
||||||
|
return longSideLen
|
||||||
|
}
|
||||||
|
|
||||||
|
private func normalizeSizeWidthFactor(with widget: OnScreenWidgetView) -> CGFloat {
|
||||||
|
widget.bounds.size.width / getReferenceLen() * 10000
|
||||||
|
}
|
||||||
|
|
||||||
|
private func normalizeSizeHeightFactor(with widget: OnScreenWidgetView) -> CGFloat {
|
||||||
|
widget.bounds.size.height / getReferenceLen() * 10000
|
||||||
|
}
|
||||||
|
|
||||||
|
private func normalizeComponentSizeFactor(with widget: OnScreenWidgetView) -> CGFloat {
|
||||||
|
if widget.isStickWheel || widget.isDisplacementBasedStickPad {
|
||||||
|
return widget.denormalizedComponentSizeFactor * widget.baselineDiameter / getReferenceLen() * 10000
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func profileNameAlreadyExist(_ name: String) -> Bool {
|
||||||
|
let profiles = getAllProfiles()
|
||||||
|
for case let profile as OSCProfile in profiles where profile.name == name {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(findProfileByName:inProfileArray:)
|
||||||
|
func findProfile(byName name: String, inProfileArray profiles: NSMutableArray) -> OSCProfile? {
|
||||||
|
for case let profile as OSCProfile in profiles where profile.name == name {
|
||||||
|
return profile
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getIndex(byName name: String) -> UInt32? {
|
||||||
|
let profiles = getAllProfiles()
|
||||||
|
for case let profile as OSCProfile in profiles where profile.name == name {
|
||||||
|
return UInt32(profiles.index(of: profile))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func unarchiveButtonStateEncoded(_ data: Data) -> OnScreenButtonState {
|
||||||
|
if let buttonState = try? NSKeyedUnarchiver.unarchivedObject(ofClasses: Self.buttonStateCodingClasses, from: data) as? OnScreenButtonState {
|
||||||
|
return buttonState
|
||||||
|
}
|
||||||
|
return OnScreenButtonState(buttonName: "", buttonType: 0, andPosition: .zero)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension NSMutableArray {
|
||||||
|
subscript(safe index: Int) -> Any? {
|
||||||
|
guard index >= 0, index < count else { return nil }
|
||||||
|
return self[index]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,46 +24,89 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
|
|
||||||
@property NSString *name;
|
@property NSString *name;
|
||||||
@property NSString *alias;
|
@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 CGPoint position;
|
||||||
@property (nonatomic, assign) BOOL isHidden;
|
@property (nonatomic, assign) BOOL isHidden;
|
||||||
@property (nonatomic, assign) uint8_t slideMode;
|
@property (nonatomic, assign) uint8_t buttonMode;
|
||||||
@property (nonatomic, assign) uint8_t buttonType;
|
@property (nonatomic, assign) uint8_t widgetType;
|
||||||
|
@property (nonatomic, assign) uint8_t sizeReference;
|
||||||
@property (nonatomic, assign) CGFloat widthFactor; // for OnScreenWidgetView
|
@property (nonatomic, assign) CGFloat widthFactor; // for OnScreenWidgetView
|
||||||
@property (nonatomic, assign) CGFloat heightFactor; // for OnScreenWidgetView
|
@property (nonatomic, assign) CGFloat heightFactor; // for OnScreenWidgetView
|
||||||
|
@property (nonatomic, assign) CGFloat componentSizeFactor; // for OnScreenWidgetView
|
||||||
@property (nonatomic, assign) CGFloat borderWidth; // for OnScreenWidgetView
|
@property (nonatomic, assign) CGFloat borderWidth; // for OnScreenWidgetView
|
||||||
|
@property (nonatomic, assign) CGFloat highlightSizeFactor; // for OnScreenWidgetView
|
||||||
@property (nonatomic, assign) CGFloat sensitivityFactorX; // for OnScreenWidgetView
|
@property (nonatomic, assign) CGFloat sensitivityFactorX; // for OnScreenWidgetView
|
||||||
@property (nonatomic, assign) CGFloat sensitivityFactorY; // for OnScreenWidgetView
|
@property (nonatomic, assign) CGFloat sensitivityFactorY; // for OnScreenWidgetView
|
||||||
@property (nonatomic, assign) CGFloat decelerationRate; // for OnScreenWidgetView
|
@property (nonatomic, assign) CGFloat slideThreshold; // for OnScreenWidgetView
|
||||||
|
@property (nonatomic, assign) CGFloat yawFactor; // for OnScreenWidgetView
|
||||||
|
@property (nonatomic, assign) CGFloat pitchFactor; // for OnScreenWidgetView
|
||||||
|
@property (nonatomic, assign) CGFloat rollFactor; // for OnScreenWidgetView
|
||||||
|
@property (nonatomic, assign) CGFloat decelerationRateX; // for OnScreenWidgetView
|
||||||
|
@property (nonatomic, assign) CGFloat decelerationRateY; // for OnScreenWidgetView
|
||||||
|
@property (nonatomic, assign) bool touchPointAnchored;
|
||||||
@property (nonatomic, assign) CGFloat stickIndicatorOffset; // for OnScreenWidgetView
|
@property (nonatomic, assign) CGFloat stickIndicatorOffset; // for OnScreenWidgetView
|
||||||
|
@property (nonatomic, assign) CGFloat walkModeThreshold; // for OnScreenWidgetView
|
||||||
@property (nonatomic, assign) CGFloat minStickOffset; // for OnScreenWidgetView
|
@property (nonatomic, assign) CGFloat minStickOffset; // for OnScreenWidgetView
|
||||||
@property NSString* widgetShape; // for OnScreenWidgetView
|
@property NSString* widgetShape; // for OnScreenWidgetView
|
||||||
|
|
||||||
@property (nonatomic, assign) CGFloat oscLayerSizeFactor; // for OnScreenController CALayer
|
@property (nonatomic, assign) CGFloat oscLayerSizeFactor; // for OnScreenController CALayer
|
||||||
@property (nonatomic, assign) CGFloat backgroundAlpha; // for OnScreenController CALayer
|
@property (nonatomic, assign) CGFloat backgroundAlpha;
|
||||||
@property (nonatomic, assign) uint8_t vibrationStyle; // for OnScreenController CALayer
|
@property (nonatomic, assign) CGFloat labelAlpha;
|
||||||
@property (nonatomic, assign) uint8_t mouseButtonAction; // for OnScreenController CALayer
|
@property (nonatomic, assign) CGFloat borderAlpha;
|
||||||
|
@property (nonatomic, assign) CGFloat highlightAlpha;
|
||||||
|
@property (nonatomic, assign) uint8_t vibrationStyle;
|
||||||
|
@property (nonatomic, assign) uint8_t mouseButtonAction;
|
||||||
|
@property (nonatomic, assign) bool animatesTransition;
|
||||||
|
@property (nonatomic, assign) uint16_t autoTapInterval;
|
||||||
|
@property (nonatomic, assign) uint32_t autoTapRepeats;
|
||||||
|
|
||||||
|
@property (nonatomic, assign) uint8_t walkKeyActionType;
|
||||||
|
@property (nonatomic, assign) CGFloat walkKeyThreshold;
|
||||||
|
@property (nonatomic, assign) uint8_t sprintKeyActionType;
|
||||||
|
@property (nonatomic, assign) CGFloat sprintKeyThreshold;
|
||||||
|
|
||||||
|
|
||||||
// @property (nonatomic, assign) BOOL hasValidPosition;
|
// @property (nonatomic, assign) BOOL hasValidPosition;
|
||||||
|
|
||||||
typedef NS_ENUM(NSInteger, OnScreenButtonType) {
|
typedef NS_ENUM(NSInteger, OnScreenWidgetType) {
|
||||||
LegacyOscButton,
|
LegacyOnScreenControls,
|
||||||
CustomOnScreenWidget
|
CustomOnScreenWidget
|
||||||
};
|
};
|
||||||
|
|
||||||
|
typedef NS_ENUM(NSInteger, WidgetSizeReference) {
|
||||||
|
longSide,
|
||||||
|
shortSide
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
typedef NS_ENUM(NSInteger, MouseButtonAction) {
|
typedef NS_ENUM(NSInteger, MouseButtonAction) {
|
||||||
hovering,
|
hovering,
|
||||||
leftButtonDown,
|
leftButtonDown,
|
||||||
middleButtonDown,
|
middleButtonDown,
|
||||||
rightButtonDown,
|
rightButtonDown,
|
||||||
|
noClick
|
||||||
};
|
};
|
||||||
|
|
||||||
typedef NS_ENUM(NSInteger, ButtonSlideMode) {
|
typedef NS_ENUM(NSInteger, ButtonMode) {
|
||||||
toggle,
|
slideToToggle,
|
||||||
slideAndHold,
|
slideAndHold,
|
||||||
disabled
|
regular,
|
||||||
|
tapToToggle,
|
||||||
|
movable
|
||||||
};
|
};
|
||||||
|
|
||||||
|
typedef NS_ENUM(NSInteger, RevealMode) {
|
||||||
|
coexist,
|
||||||
|
exclusive
|
||||||
|
};
|
||||||
|
|
||||||
- (id) initWithButtonName:(NSString*)name buttonType:(uint8_t)buttonType andPosition:(CGPoint)position;
|
- (id) initWithButtonName:(NSString*)name buttonType:(uint8_t)buttonType andPosition:(CGPoint)position;
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#import "OnScreenButtonState.h"
|
#import "OnScreenButtonState.h"
|
||||||
|
#import "VoidLink-Swift.h"
|
||||||
|
|
||||||
@implementation OnScreenButtonState
|
@implementation OnScreenButtonState
|
||||||
|
|
||||||
@@ -19,7 +20,7 @@
|
|||||||
self.name = name;
|
self.name = name;
|
||||||
//self.isHidden = isHidden;
|
//self.isHidden = isHidden;
|
||||||
self.position = position;
|
self.position = position;
|
||||||
self.buttonType = buttonType;
|
self.widgetType = buttonType;
|
||||||
}
|
}
|
||||||
|
|
||||||
return self;
|
return self;
|
||||||
@@ -31,49 +32,107 @@
|
|||||||
|
|
||||||
- (void) encodeWithCoder:(NSCoder*)encoder {
|
- (void) encodeWithCoder:(NSCoder*)encoder {
|
||||||
[encoder encodeObject:self.name forKey:@"name"];
|
[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 encodeObject:self.alias forKey:@"alias"];
|
||||||
[encoder encodeInt:self.buttonType forKey:@"buttonType"];
|
[encoder encodeInt:self.widgetType forKey:@"buttonType"]; // keep original key
|
||||||
|
[encoder encodeInt:self.sizeReference forKey:@"sizeReference"];
|
||||||
[encoder encodeInt:self.vibrationStyle forKey:@"vibrationStyle"];
|
[encoder encodeInt:self.vibrationStyle forKey:@"vibrationStyle"];
|
||||||
[encoder encodeInt:self.mouseButtonAction forKey:@"mouseButtonAction"];
|
[encoder encodeInt:self.mouseButtonAction forKey:@"mouseButtonAction"];
|
||||||
[encoder encodeInt:self.slideMode forKey:@"slideMode"];
|
[encoder encodeBool:self.animatesTransition forKey:@"animatesTransition"];
|
||||||
|
[encoder encodeInt:self.buttonMode forKey:@"slideMode"]; // buttonMode: previously slideMode, keep it for consistency
|
||||||
|
[encoder encodeInt:self.autoTapInterval forKey:@"autoTapInterval"];
|
||||||
|
[encoder encodeInt:self.autoTapRepeats forKey:@"autoTapRepeats"];
|
||||||
[encoder encodeCGPoint:self.position forKey:@"position"];
|
[encoder encodeCGPoint:self.position forKey:@"position"];
|
||||||
[encoder encodeBool:self.isHidden forKey:@"isHidden"];
|
[encoder encodeBool:self.isHidden forKey:@"isHidden"];
|
||||||
[encoder encodeFloat:self.widthFactor forKey:@"widthFactor"];
|
[encoder encodeFloat:self.widthFactor forKey:@"widthFactor"];
|
||||||
[encoder encodeFloat:self.heightFactor forKey:@"heightFactor"];
|
[encoder encodeFloat:self.heightFactor forKey:@"heightFactor"];
|
||||||
|
[encoder encodeFloat:self.componentSizeFactor forKey:@"componentSizeFactor"];
|
||||||
[encoder encodeFloat:self.sensitivityFactorX forKey:@"sensitivityFactorX"];
|
[encoder encodeFloat:self.sensitivityFactorX forKey:@"sensitivityFactorX"];
|
||||||
[encoder encodeFloat:self.sensitivityFactorY forKey:@"sensitivityFactorY"];
|
[encoder encodeFloat:self.sensitivityFactorY forKey:@"sensitivityFactorY"];
|
||||||
[encoder encodeFloat:self.decelerationRate forKey:@"decelerationRate"];
|
[encoder encodeFloat:self.slideThreshold forKey:@"slideThreshold"];
|
||||||
|
[encoder encodeFloat:self.yawFactor forKey:@"yawFactor"];
|
||||||
|
[encoder encodeFloat:self.pitchFactor forKey:@"pitchFactor"];
|
||||||
|
[encoder encodeFloat:self.rollFactor forKey:@"rollFactor"];
|
||||||
|
[encoder encodeFloat:self.decelerationRateX forKey:@"decelerationRateX"];
|
||||||
|
[encoder encodeFloat:self.decelerationRateY forKey:@"decelerationRateY"];
|
||||||
|
[encoder encodeBool:self.touchPointAnchored forKey:@"touchPointAnchored"];
|
||||||
[encoder encodeFloat:self.stickIndicatorOffset forKey:@"stickIndicatorOffset"];
|
[encoder encodeFloat:self.stickIndicatorOffset forKey:@"stickIndicatorOffset"];
|
||||||
[encoder encodeFloat:self.oscLayerSizeFactor forKey:@"oscLayerSizeFactor"];
|
[encoder encodeFloat:self.oscLayerSizeFactor forKey:@"oscLayerSizeFactor"];
|
||||||
[encoder encodeFloat:self.backgroundAlpha forKey:@"backgroundAlpha"];
|
[encoder encodeFloat:self.backgroundAlpha forKey:@"backgroundAlpha"];
|
||||||
|
[encoder encodeFloat:self.labelAlpha forKey:@"labelAlpha"];
|
||||||
|
[encoder encodeFloat:self.borderAlpha forKey:@"borderAlpha"];
|
||||||
|
[encoder encodeFloat:self.highlightAlpha forKey:@"highlightAlpha"];
|
||||||
[encoder encodeFloat:self.borderWidth forKey:@"borderWidth"];
|
[encoder encodeFloat:self.borderWidth forKey:@"borderWidth"];
|
||||||
|
[encoder encodeFloat:self.highlightSizeFactor forKey:@"highlightSizeFactor"];
|
||||||
[encoder encodeObject:self.widgetShape forKey:@"widgetShape"];
|
[encoder encodeObject:self.widgetShape forKey:@"widgetShape"];
|
||||||
|
[encoder encodeFloat:self.walkModeThreshold forKey:@"walkModeThreshold"];
|
||||||
[encoder encodeFloat:self.minStickOffset forKey:@"minStickOffset"];
|
[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 {
|
- (id) initWithCoder:(NSCoder*)decoder {
|
||||||
if (self = [super init]) {
|
if (self = [super init]) {
|
||||||
self.name = [decoder decodeObjectForKey:@"name"];
|
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.alias = [decoder decodeObjectForKey:@"alias"];
|
||||||
self.buttonType = [decoder decodeIntForKey:@"buttonType"];
|
self.widgetType = [decoder decodeIntForKey:@"buttonType"];
|
||||||
self.vibrationStyle = [decoder decodeIntForKey:@"vibrationStyle"];
|
self.sizeReference = [decoder containsValueForKey:@"sizeReference"] ? [decoder decodeIntForKey:@"sizeReference"] : longSide;
|
||||||
|
self.vibrationStyle = [decoder containsValueForKey:@"vibrationStyle"] ? [decoder decodeIntForKey:@"vibrationStyle"] : UIImpactFeedbackStyleLight;
|
||||||
self.mouseButtonAction = [decoder decodeIntForKey:@"mouseButtonAction"];
|
self.mouseButtonAction = [decoder decodeIntForKey:@"mouseButtonAction"];
|
||||||
self.slideMode = [decoder containsValueForKey:@"slideMode"] ? [decoder decodeIntForKey:@"slideMode"] : 0;
|
self.animatesTransition = [decoder containsValueForKey:@"animatesTransition"] ? [decoder decodeBoolForKey:@"animatesTransition"] : true;
|
||||||
|
self.buttonMode = [decoder containsValueForKey:@"slideMode"] ? [decoder decodeIntForKey:@"slideMode"] : 0;
|
||||||
|
self.autoTapInterval = [decoder containsValueForKey:@"autoTapInterval"] ? [decoder decodeIntForKey:@"autoTapInterval"] : 45;
|
||||||
|
self.autoTapRepeats = [decoder containsValueForKey:@"autoTapRepeats"] ? [decoder decodeIntForKey:@"autoTapRepeats"] : 0;
|
||||||
self.position = [decoder decodeCGPointForKey:@"position"];
|
self.position = [decoder decodeCGPointForKey:@"position"];
|
||||||
self.isHidden = [decoder decodeBoolForKey:@"isHidden"];
|
self.isHidden = [decoder decodeBoolForKey:@"isHidden"];
|
||||||
self.widthFactor = [decoder decodeFloatForKey:@"widthFactor"];
|
self.widthFactor = [decoder decodeFloatForKey:@"widthFactor"];
|
||||||
self.heightFactor = [decoder decodeFloatForKey:@"heightFactor"];
|
self.heightFactor = [decoder decodeFloatForKey:@"heightFactor"];
|
||||||
self.sensitivityFactorX = [decoder decodeFloatForKey:@"sensitivityFactorX"];
|
self.componentSizeFactor = [decoder containsValueForKey:@"componentSizeFactor"] ? [decoder decodeFloatForKey:@"componentSizeFactor"] : 1.0;
|
||||||
self.sensitivityFactorX = self.sensitivityFactorX == 0 ? 1.0 : self.sensitivityFactorX;
|
self.sensitivityFactorX = [decoder containsValueForKey:@"sensitivityFactorX"] ? [decoder decodeFloatForKey:@"sensitivityFactorX"] : 1.0;
|
||||||
self.sensitivityFactorY = [decoder decodeFloatForKey:@"sensitivityFactorY"];
|
self.sensitivityFactorY = [decoder containsValueForKey:@"sensitivityFactorY"] ? [decoder decodeFloatForKey:@"sensitivityFactorY"] : 1.0;
|
||||||
self.sensitivityFactorY = self.sensitivityFactorY == 0 ? 1.0 : self.sensitivityFactorY;
|
self.slideThreshold = [decoder containsValueForKey:@"slideThreshold"] ? [decoder decodeFloatForKey:@"slideThreshold"] : 6.0;
|
||||||
self.decelerationRate = [decoder decodeFloatForKey:@"decelerationRate"];
|
self.yawFactor = [decoder containsValueForKey:@"yawFactor"] ? [decoder decodeFloatForKey:@"yawFactor"] : 1.0;
|
||||||
|
self.pitchFactor = [decoder containsValueForKey:@"pitchFactor"] ? [decoder decodeFloatForKey:@"pitchFactor"] : 1.0;
|
||||||
|
self.rollFactor = [decoder containsValueForKey:@"rollFactor"] ? [decoder decodeFloatForKey:@"rollFactor"] : 1.0;
|
||||||
|
self.decelerationRateX = [decoder containsValueForKey:@"decelerationRateX"] ? [decoder decodeFloatForKey:@"decelerationRateX"] : 0.5;
|
||||||
|
self.decelerationRateY = [decoder containsValueForKey:@"decelerationRateY"] ? [decoder decodeFloatForKey:@"decelerationRateY"] : 0.5;
|
||||||
|
self.touchPointAnchored = [decoder containsValueForKey:@"touchPointAnchored"] ? [decoder decodeBoolForKey:@"touchPointAnchored"] : false;
|
||||||
self.stickIndicatorOffset = [decoder decodeFloatForKey:@"stickIndicatorOffset"];
|
self.stickIndicatorOffset = [decoder decodeFloatForKey:@"stickIndicatorOffset"];
|
||||||
self.oscLayerSizeFactor = [decoder decodeFloatForKey:@"oscLayerSizeFactor"];
|
self.oscLayerSizeFactor = [decoder decodeFloatForKey:@"oscLayerSizeFactor"];
|
||||||
self.backgroundAlpha = [decoder decodeFloatForKey:@"backgroundAlpha"];
|
self.backgroundAlpha = [decoder containsValueForKey:@"backgroundAlpha"] ? [decoder decodeFloatForKey:@"backgroundAlpha"] : 0.5;
|
||||||
|
self.labelAlpha = [decoder containsValueForKey:@"labelAlpha"] ? [decoder decodeFloatForKey:@"labelAlpha"] : 0.82;
|
||||||
|
self.borderAlpha = [decoder containsValueForKey:@"borderAlpha"] ? [decoder decodeFloatForKey:@"borderAlpha"] : 0.19;
|
||||||
|
self.highlightAlpha = [decoder containsValueForKey:@"highlightAlpha"] ? [decoder decodeFloatForKey:@"highlightAlpha"] : 0.77;
|
||||||
self.borderWidth = [decoder decodeFloatForKey:@"borderWidth"];
|
self.borderWidth = [decoder decodeFloatForKey:@"borderWidth"];
|
||||||
|
self.highlightSizeFactor = [decoder containsValueForKey:@"highlightSizeFactor"] ? [decoder decodeFloatForKey:@"highlightSizeFactor"] : 1.0;
|
||||||
self.widgetShape = [decoder decodeObjectForKey:@"widgetShape"];
|
self.widgetShape = [decoder decodeObjectForKey:@"widgetShape"];
|
||||||
|
self.walkModeThreshold = [decoder containsValueForKey:@"walkModeThreshold"] ? [decoder decodeFloatForKey:@"walkModeThreshold"] : 16383;
|
||||||
self.minStickOffset = [decoder decodeFloatForKey:@"minStickOffset"];
|
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;
|
return self;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
CGFloat lowestTouchPointYCoord;
|
CGFloat lowestTouchPointYCoord;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@property (nonatomic, weak) UIView* touchCapturingView;
|
||||||
@property (nonatomic, assign) uint8_t numberOfTouchesRequired;
|
@property (nonatomic, assign) uint8_t numberOfTouchesRequired;
|
||||||
@property (nonatomic, assign) bool immediateTriggering; // if enabled, trigger the signal on touchesBegan stage.
|
@property (nonatomic, assign) bool immediateTriggering; // if enabled, trigger the signal on touchesBegan stage.
|
||||||
@property (nonatomic, assign) double tapDownTimeThreshold; // tap down threshold in seconds.
|
@property (nonatomic, assign) double tapDownTimeThreshold; // tap down threshold in seconds.
|
||||||
|
|||||||
@@ -39,13 +39,21 @@ static CGFloat screenWidthInPoints;
|
|||||||
|
|
||||||
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
||||||
// Check if the number of touches and taps meets the required criteria
|
// Check if the number of touches and taps meets the required criteria
|
||||||
if ([[event allTouches] count] == _numberOfTouchesRequired) {
|
NSSet* allTouches = [event allTouches];
|
||||||
_gestureCapturedTime = CACurrentMediaTime();
|
if (allTouches.count == _numberOfTouchesRequired) {
|
||||||
_gestureCaptured = true;
|
if([UITouchUtil touchesIn:_touchCapturingView from:event].count != allTouches.count){
|
||||||
for(UITouch *touch in [event allTouches]){
|
_gestureCaptured = false;
|
||||||
if(lowestTouchPointYCoord < [touch locationInView:self.view].y) lowestTouchPointYCoord = [touch locationInView:self.view].y;
|
self.state = UIGestureRecognizerStateFailed;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_gestureCapturedTime = CACurrentMediaTime();
|
||||||
|
_gestureCaptured = true;
|
||||||
|
lowestTouchPointYCoord = 0;
|
||||||
|
for(UITouch *touch in allTouches){
|
||||||
|
if(lowestTouchPointYCoord < [touch locationInView:self.view].y) lowestTouchPointYCoord = [touch locationInView:self.view].y;
|
||||||
|
// NSLog(@"lowest point coord: %f, %f", lowestTouchPointYCoord, CACurrentMediaTime());
|
||||||
|
}
|
||||||
// this mechanism is deprecated.
|
// this mechanism is deprecated.
|
||||||
/* if(_numberOfTouchesRequired == 2){
|
/* if(_numberOfTouchesRequired == 2){
|
||||||
NSArray *twoTouches = [[event allTouches] allObjects];
|
NSArray *twoTouches = [[event allTouches] allObjects];
|
||||||
@@ -55,13 +63,13 @@ static CGFloat screenWidthInPoints;
|
|||||||
_lowestTouchPointHeight = screenHeightInPoints - lowestTouchPointYCoord;
|
_lowestTouchPointHeight = screenHeightInPoints - lowestTouchPointYCoord;
|
||||||
// NSLog(@"gen _lowestTouchPointHeight %f markmark", _lowestTouchPointHeight);
|
// NSLog(@"gen _lowestTouchPointHeight %f markmark", _lowestTouchPointHeight);
|
||||||
if(_immediateTriggering){
|
if(_immediateTriggering){
|
||||||
lowestTouchPointYCoord = 0.0; //reset for next recoginition
|
// lowestTouchPointYCoord = 0.0; //reset for next recoginition
|
||||||
self.state = UIGestureRecognizerStateRecognized;
|
self.state = UIGestureRecognizerStateRecognized;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.state = UIGestureRecognizerStatePossible;
|
self.state = UIGestureRecognizerStatePossible;
|
||||||
}
|
}
|
||||||
if ([[event allTouches] count] > _numberOfTouchesRequired) {
|
if (allTouches.count > _numberOfTouchesRequired) {
|
||||||
_gestureCaptured = false;
|
_gestureCaptured = false;
|
||||||
self.state = UIGestureRecognizerStateFailed;
|
self.state = UIGestureRecognizerStateFailed;
|
||||||
}
|
}
|
||||||
@@ -76,40 +84,26 @@ static CGFloat screenWidthInPoints;
|
|||||||
_gestureCaptured = false;
|
_gestureCaptured = false;
|
||||||
self.state = UIGestureRecognizerStateFailed;
|
self.state = UIGestureRecognizerStateFailed;
|
||||||
}
|
}
|
||||||
else if(_gestureCaptured && allTouchesCount == [touches count] && !_isOnScreenControllerBeingPressed && ![self isOnScreenWidgetViewBeingPressed]){ //must exclude virtual controller & onscreen button taps here to prevent stucked button, _areVirtualControllerTaps flag is set by onscreencontrols, containOnScreenButtonsTaps will be returned by iterating all widget views in streamframeview
|
else if(_gestureCaptured
|
||||||
|
&& allTouchesCount == [touches count]
|
||||||
|
&& !_isOnScreenControllerBeingPressed){ //must exclude virtual controller & onscreen button taps here to prevent stucked button, _areVirtualControllerTaps flag is set by onscreencontrols, containOnScreenButtonsTaps will be returned by iterating all widget views in streamframeview
|
||||||
_gestureCaptured = false; //reset for next recognition
|
_gestureCaptured = false; //reset for next recognition
|
||||||
if((CACurrentMediaTime() - _gestureCapturedTime) < _tapDownTimeThreshold){
|
if((CACurrentMediaTime() - _gestureCapturedTime) < _tapDownTimeThreshold){
|
||||||
lowestTouchPointYCoord = 0.0; //reset for next recognition
|
// lowestTouchPointYCoord = 0.0; //reset for next recognition
|
||||||
self.state = UIGestureRecognizerStateRecognized;
|
|
||||||
|
for(UITouch* touch in touches){
|
||||||
|
CGVector vector = [UITouchUtil vectorOf:touch in:self.view];
|
||||||
|
if(hypot(vector.dx, vector.dy)>2){
|
||||||
|
self.state = UIGestureRecognizerStateFailed;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(self.state != UIGestureRecognizerStateFailed) self.state = UIGestureRecognizerStateRecognized;
|
||||||
// NSLog(@"gen _lowestTouchPointHeight %f markmark touchesEnd", _lowestTouchPointHeight);
|
// NSLog(@"gen _lowestTouchPointHeight %f markmark touchesEnd", _lowestTouchPointHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (allTouchesCount == [touches count]) _isOnScreenControllerBeingPressed = false; // need to reset this flag anyway, when all fingers are lefting
|
if (allTouchesCount == [touches count]) _isOnScreenControllerBeingPressed = false; // need to reset this flag anyway, when all fingers are lefting
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// it was a not a perfect choice to code OnScreenWidgetView in Swift...
|
|
||||||
// we're unable to import this class to swift codebase by the bridging header,and have to exlucde the onscreen button taps here
|
|
||||||
// by iterating every widget view instances. but it's basically ok since the number of widgetViews are always limited.
|
|
||||||
// and this method will only be called when the recognizer is active & and the taps passes all the checks and is about to ge triggered.
|
|
||||||
- (bool)isOnScreenWidgetViewBeingPressed {
|
|
||||||
NSTimeInterval allFingersTapDownTime = CACurrentMediaTime() - _gestureCapturedTime; //RIGHTCLICK_TAP_DOWN_TIME_THRESHOLD_S has been passed to this recognizer as _tapDownTimeShreshold, we'll decide how to deal with pressed flag of on-screen widget views based on tapDownTime
|
|
||||||
bool gotOneWidgetPressed = false;
|
|
||||||
// the tap gestures are all added to the streamFrameTopLayerView, where all the OnScreenWidgetViews are added. so we can iterate them in this way:
|
|
||||||
for(UIView* view in self.view.subviews){
|
|
||||||
if ([view isKindOfClass:[OnScreenWidgetView class]]) {
|
|
||||||
OnScreenWidgetView* widgetView = (OnScreenWidgetView*) view;
|
|
||||||
if(gotOneWidgetPressed && allFingersTapDownTime <= _tapDownTimeThreshold){ // once we have just 1 button pressed already,& the tapDownTime has not exceeded the threshold, we'll reset pressed flag for the all the widgetViews.
|
|
||||||
widgetView.pressed = false; //reset the flag for widgetView
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if(widgetView.pressed){
|
|
||||||
gotOneWidgetPressed = true; //got one button pressed
|
|
||||||
if(allFingersTapDownTime <= _tapDownTimeThreshold) widgetView.pressed = false; // reset the flag for current widgetView if tapDowntime is still within the threshold, if not, leave the flag for mouse scroller gesture in relative touch mode.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return gotOneWidgetPressed;
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -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
|
@interface HapticContext : NSObject
|
||||||
|
|
||||||
-(void)setMotorAmplitude:(unsigned short)amplitude;
|
-(void)setMotorAmplitude:(unsigned short)amplitude;
|
||||||
|
-(void)setAuthoredAmplitude:(float)amplitude
|
||||||
|
sharpness:(float)sharpness
|
||||||
|
transientStrength:(float)transientStrength;
|
||||||
-(void)cleanup;
|
-(void)cleanup;
|
||||||
|
|
||||||
+(HapticContext*) createContextForHighFreqMotor:(GCController*)gamepad;
|
+(HapticContext*) createContextForHighFreqMotor:(GCController*)gamepad;
|
||||||
|
|||||||
@@ -5,8 +5,12 @@
|
|||||||
// Created by Cameron Gutman on 9/17/20.
|
// Created by Cameron Gutman on 9/17/20.
|
||||||
// Copyright © 2020 Moonlight Game Streaming Project. All rights reserved.
|
// Copyright © 2020 Moonlight Game Streaming Project. All rights reserved.
|
||||||
//
|
//
|
||||||
|
// Modified by True砖家 on 2025.10.12
|
||||||
|
// Copyright © 2025 True砖家 @ Bilibili. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
#import "HapticContext.h"
|
#import "HapticContext.h"
|
||||||
|
#import "DataManager.h"
|
||||||
|
|
||||||
@import CoreHaptics;
|
@import CoreHaptics;
|
||||||
@import GameController;
|
@import GameController;
|
||||||
@@ -14,14 +18,20 @@
|
|||||||
@implementation HapticContext {
|
@implementation HapticContext {
|
||||||
GCControllerPlayerIndex _playerIndex;
|
GCControllerPlayerIndex _playerIndex;
|
||||||
CHHapticEngine* _hapticEngine API_AVAILABLE(ios(13.0), tvos(14.0));
|
CHHapticEngine* _hapticEngine API_AVAILABLE(ios(13.0), tvos(14.0));
|
||||||
id<CHHapticPatternPlayer> _hapticPlayer API_AVAILABLE(ios(13.0), tvos(14.0));
|
id<CHHapticPatternPlayer> _motorHapticPlayer API_AVAILABLE(ios(13.0), tvos(14.0));
|
||||||
BOOL _playing;
|
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)) {
|
-(void)cleanup API_AVAILABLE(ios(14.0), tvos(14.0)) {
|
||||||
if (_hapticPlayer != nil) {
|
if (_motorHapticPlayer != nil) {
|
||||||
[_hapticPlayer cancelAndReturnError:nil];
|
[_motorHapticPlayer cancelAndReturnError:nil];
|
||||||
_hapticPlayer = nil;
|
_motorHapticPlayer = nil;
|
||||||
|
}
|
||||||
|
if (_authoredHapticPlayer != nil) {
|
||||||
|
[_authoredHapticPlayer cancelAndReturnError:nil];
|
||||||
|
_authoredHapticPlayer = nil;
|
||||||
}
|
}
|
||||||
if (_hapticEngine != nil) {
|
if (_hapticEngine != nil) {
|
||||||
[_hapticEngine stopWithCompletionHandler:nil];
|
[_hapticEngine stopWithCompletionHandler:nil];
|
||||||
@@ -39,15 +49,15 @@
|
|||||||
|
|
||||||
// Stop the effect entirely if the amplitude is 0
|
// Stop the effect entirely if the amplitude is 0
|
||||||
if (amplitude == 0) {
|
if (amplitude == 0) {
|
||||||
if (_playing) {
|
if (_motorPlaying) {
|
||||||
[_hapticPlayer stopAtTime:0 error:&error];
|
[_motorHapticPlayer stopAtTime:0 error:&error];
|
||||||
_playing = NO;
|
_motorPlaying = NO;
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
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
|
// 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];
|
CHHapticEventParameter* intensityParameter = [[CHHapticEventParameter alloc] initWithParameterID:CHHapticEventParameterIDHapticIntensity value:1.0f];
|
||||||
CHHapticEvent* hapticEvent = [[CHHapticEvent alloc] initWithEventType:CHHapticEventTypeHapticContinuous parameters:[NSArray arrayWithObject:intensityParameter] relativeTime:0 duration:GCHapticDurationInfinite];
|
CHHapticEvent* hapticEvent = [[CHHapticEvent alloc] initWithEventType:CHHapticEventTypeHapticContinuous parameters:[NSArray arrayWithObject:intensityParameter] relativeTime:0 duration:GCHapticDurationInfinite];
|
||||||
@@ -57,7 +67,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_hapticPlayer = [_hapticEngine createPlayerWithPattern:hapticPattern error:&error];
|
_motorHapticPlayer = [_hapticEngine createPlayerWithPattern:hapticPattern error:&error];
|
||||||
if (error != nil) {
|
if (error != nil) {
|
||||||
Log(LOG_W, @"Controller %d: Haptic player creation failed: %@", _playerIndex, error);
|
Log(LOG_W, @"Controller %d: Haptic player creation failed: %@", _playerIndex, error);
|
||||||
return;
|
return;
|
||||||
@@ -65,37 +75,183 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
CHHapticDynamicParameter* intensityParameter = [[CHHapticDynamicParameter alloc] initWithParameterID:CHHapticDynamicParameterIDHapticIntensityControl value:amplitude / 65535.0f relativeTime:0];
|
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) {
|
if (error != nil) {
|
||||||
Log(LOG_W, @"Controller %d: Haptic player parameter update failed: %@", _playerIndex, error);
|
Log(LOG_W, @"Controller %d: Haptic player parameter update failed: %@", _playerIndex, error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_playing) {
|
if (!_motorPlaying) {
|
||||||
[_hapticPlayer startAtTime:0 error:&error];
|
[_motorHapticPlayer startAtTime:0 error:&error];
|
||||||
if (error != nil) {
|
if (error != nil) {
|
||||||
_hapticPlayer = nil;
|
_motorHapticPlayer = nil;
|
||||||
Log(LOG_W, @"Controller %d: Haptic playback start failed: %@", _playerIndex, error);
|
Log(LOG_W, @"Controller %d: Haptic playback start failed: %@", _playerIndex, error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_playing = YES;
|
_motorPlaying = YES;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
-(id) initWithGamepad:(GCController*)gamepad locality:(GCHapticsLocality)locality API_AVAILABLE(ios(14.0), tvos(14.0)) {
|
-(void)setAuthoredAmplitude:(float)amplitude
|
||||||
if (gamepad.haptics == nil) {
|
sharpness:(float)sharpness
|
||||||
Log(LOG_W, @"Controller %d does not support haptics", gamepad.playerIndex);
|
transientStrength:(float)transientStrength API_AVAILABLE(ios(14.0), tvos(14.0)) {
|
||||||
|
if (_hapticEngine == nil) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
amplitude = fmaxf(0.0f, fminf(1.0f, amplitude));
|
||||||
|
sharpness = fmaxf(0.0f, fminf(1.0f, sharpness));
|
||||||
|
transientStrength = fmaxf(0.0f, fminf(1.0f, transientStrength));
|
||||||
|
|
||||||
|
NSError* error = nil;
|
||||||
|
if (amplitude == 0.0f) {
|
||||||
|
if (_authoredPlaying) {
|
||||||
|
[_authoredHapticPlayer stopAtTime:CHHapticTimeImmediate error:&error];
|
||||||
|
_authoredPlaying = NO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (_authoredHapticPlayer == nil) {
|
||||||
|
CHHapticEventParameter* intensity = [[CHHapticEventParameter alloc]
|
||||||
|
initWithParameterID:CHHapticEventParameterIDHapticIntensity value:1.0f];
|
||||||
|
CHHapticEventParameter* eventSharpness = [[CHHapticEventParameter alloc]
|
||||||
|
initWithParameterID:CHHapticEventParameterIDHapticSharpness value:0.5f];
|
||||||
|
CHHapticEvent* event = [[CHHapticEvent alloc]
|
||||||
|
initWithEventType:CHHapticEventTypeHapticContinuous
|
||||||
|
parameters:@[intensity, eventSharpness]
|
||||||
|
relativeTime:0
|
||||||
|
duration:GCHapticDurationInfinite];
|
||||||
|
CHHapticPattern* pattern = [[CHHapticPattern alloc]
|
||||||
|
initWithEvents:@[event] parameters:@[] error:&error];
|
||||||
|
if (pattern == nil || error != nil) {
|
||||||
|
Log(LOG_W, @"Controller %d: Authored haptic pattern creation failed: %@", _playerIndex, error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_authoredHapticPlayer = [_hapticEngine createPlayerWithPattern:pattern error:&error];
|
||||||
|
if (_authoredHapticPlayer == nil || error != nil) {
|
||||||
|
Log(LOG_W, @"Controller %d: Authored haptic player creation failed: %@", _playerIndex, error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NSArray* parameters = @[
|
||||||
|
[[CHHapticDynamicParameter alloc]
|
||||||
|
initWithParameterID:CHHapticDynamicParameterIDHapticIntensityControl
|
||||||
|
value:amplitude relativeTime:0],
|
||||||
|
[[CHHapticDynamicParameter alloc]
|
||||||
|
initWithParameterID:CHHapticDynamicParameterIDHapticSharpnessControl
|
||||||
|
value:sharpness relativeTime:0]
|
||||||
|
];
|
||||||
|
[_authoredHapticPlayer sendParameters:parameters atTime:CHHapticTimeImmediate error:&error];
|
||||||
|
if (error != nil) {
|
||||||
|
Log(LOG_W, @"Controller %d: Authored haptic parameter update failed: %@", _playerIndex, error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!_authoredPlaying) {
|
||||||
|
[_authoredHapticPlayer startAtTime:CHHapticTimeImmediate error:&error];
|
||||||
|
if (error != nil) {
|
||||||
|
_authoredHapticPlayer = nil;
|
||||||
|
Log(LOG_W, @"Controller %d: Authored haptic playback start failed: %@", _playerIndex, error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_authoredPlaying = YES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit one-shot attacks only when the analyzer found a meaningful transient.
|
||||||
|
if (transientStrength >= 0.015f) {
|
||||||
|
CHHapticEventParameter* intensity = [[CHHapticEventParameter alloc]
|
||||||
|
initWithParameterID:CHHapticEventParameterIDHapticIntensity value:transientStrength];
|
||||||
|
CHHapticEventParameter* eventSharpness = [[CHHapticEventParameter alloc]
|
||||||
|
initWithParameterID:CHHapticEventParameterIDHapticSharpness value:sharpness];
|
||||||
|
CHHapticEvent* event = [[CHHapticEvent alloc]
|
||||||
|
initWithEventType:CHHapticEventTypeHapticTransient
|
||||||
|
parameters:@[intensity, eventSharpness]
|
||||||
|
relativeTime:0];
|
||||||
|
CHHapticPattern* pattern = [[CHHapticPattern alloc]
|
||||||
|
initWithEvents:@[event] parameters:@[] error:&error];
|
||||||
|
id<CHHapticPatternPlayer> transientPlayer = pattern == nil ? nil :
|
||||||
|
[_hapticEngine createPlayerWithPattern:pattern error:&error];
|
||||||
|
[transientPlayer startAtTime:CHHapticTimeImmediate error:&error];
|
||||||
|
if (error != nil) {
|
||||||
|
Log(LOG_W, @"Controller %d: Authored haptic transient failed: %@", _playerIndex, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
-(id) initDeviceEngineContextWithGamepad:(GCController*)gamepad API_AVAILABLE(ios(13.0), tvos(13.0)) {
|
||||||
|
NSError *error = nil;
|
||||||
|
_hapticEngine = [[CHHapticEngine alloc] initAndReturnError:&error];
|
||||||
|
if (error != nil) {
|
||||||
|
Log(LOG_W, @"Controller %d: iPhone Haptic engine failed to start: %@", gamepad.playerIndex, error);
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
_playerIndex = gamepad.playerIndex;
|
||||||
|
|
||||||
if (![[gamepad.haptics supportedLocalities] containsObject:locality]) {
|
[_hapticEngine startAndReturnError:&error];
|
||||||
Log(LOG_W, @"Controller %d does not support haptic locality: %@", gamepad.playerIndex, locality);
|
if (error != nil) {
|
||||||
|
Log(LOG_W, @"Controller %d: Haptic engine failed to start: %@", gamepad.playerIndex, error);
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
__weak typeof(self) weakSelf = self;
|
||||||
|
_hapticEngine.stoppedHandler = ^(CHHapticEngineStoppedReason stoppedReason) {
|
||||||
|
HapticContext* me = weakSelf;
|
||||||
|
if (me == nil) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log(LOG_W, @"Controller %d: Haptic engine stopped: %p", me->_playerIndex, stoppedReason);
|
||||||
|
me->_motorHapticPlayer = nil;
|
||||||
|
me->_authoredHapticPlayer = nil;
|
||||||
|
me->_hapticEngine = nil;
|
||||||
|
me->_motorPlaying = NO;
|
||||||
|
me->_authoredPlaying = NO;
|
||||||
|
};
|
||||||
|
_hapticEngine.resetHandler = ^{
|
||||||
|
HapticContext* me = weakSelf;
|
||||||
|
if (me == nil) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log(LOG_W, @"Controller %d: Haptic engine reset", me->_playerIndex);
|
||||||
|
me->_motorHapticPlayer = nil;
|
||||||
|
me->_authoredHapticPlayer = nil;
|
||||||
|
me->_motorPlaying = NO;
|
||||||
|
me->_authoredPlaying = NO;
|
||||||
|
[me->_hapticEngine startAndReturnError:nil];
|
||||||
|
};
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
-(id) initWithGamepad:(GCController*)gamepad locality:(GCHapticsLocality)locality API_AVAILABLE(ios(14.0), tvos(14.0)) {
|
||||||
|
bool fallBackToPhoneHaptics = false;
|
||||||
|
|
||||||
|
if (gamepad.haptics == nil) {
|
||||||
|
Log(LOG_W, @"Controller %d does not support haptics", gamepad.playerIndex);
|
||||||
|
fallBackToPhoneHaptics = true;
|
||||||
|
}
|
||||||
|
else if (![[gamepad.haptics supportedLocalities] containsObject:locality]) {
|
||||||
|
Log(LOG_W, @"Controller %d does not support haptic locality: %@", gamepad.playerIndex, locality);
|
||||||
|
fallBackToPhoneHaptics = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fallBackToPhoneHaptics) {
|
||||||
|
Log(LOG_W, @"Controller %d falls back to use iPhone Haptics for locality: %@", gamepad.playerIndex, locality);
|
||||||
|
NSError *error = nil;
|
||||||
|
_hapticEngine = [[CHHapticEngine alloc] initAndReturnError:&error];
|
||||||
|
if (error != nil) {
|
||||||
|
Log(LOG_W, @"Controller %d: iPhone Haptic engine failed to start: %@", gamepad.playerIndex, error);
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_hapticEngine = [gamepad.haptics createEngineWithLocality:locality];
|
||||||
|
}
|
||||||
|
|
||||||
_playerIndex = gamepad.playerIndex;
|
_playerIndex = gamepad.playerIndex;
|
||||||
_hapticEngine = [gamepad.haptics createEngineWithLocality:locality];
|
|
||||||
|
|
||||||
NSError* error;
|
NSError* error;
|
||||||
[_hapticEngine startAndReturnError:&error];
|
[_hapticEngine startAndReturnError:&error];
|
||||||
@@ -112,9 +268,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
Log(LOG_W, @"Controller %d: Haptic engine stopped: %p", me->_playerIndex, stoppedReason);
|
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->_hapticEngine = nil;
|
||||||
me->_playing = NO;
|
me->_motorPlaying = NO;
|
||||||
|
me->_authoredPlaying = NO;
|
||||||
};
|
};
|
||||||
_hapticEngine.resetHandler = ^{
|
_hapticEngine.resetHandler = ^{
|
||||||
HapticContext* me = weakSelf;
|
HapticContext* me = weakSelf;
|
||||||
@@ -123,8 +281,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
Log(LOG_W, @"Controller %d: Haptic engine reset", me->_playerIndex);
|
Log(LOG_W, @"Controller %d: Haptic engine reset", me->_playerIndex);
|
||||||
me->_hapticPlayer = nil;
|
me->_motorHapticPlayer = nil;
|
||||||
me->_playing = NO;
|
me->_authoredHapticPlayer = nil;
|
||||||
|
me->_motorPlaying = NO;
|
||||||
|
me->_authoredPlaying = NO;
|
||||||
[me->_hapticEngine startAndReturnError:nil];
|
[me->_hapticEngine startAndReturnError:nil];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -136,7 +296,9 @@
|
|||||||
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityRightHandle];
|
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityRightHandle];
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return nil;
|
if (@available(iOS 13.0, *)) {
|
||||||
|
return [[HapticContext alloc] initDeviceEngineContextWithGamepad:gamepad];
|
||||||
|
} else return nil;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +307,9 @@
|
|||||||
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityLeftHandle];
|
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityLeftHandle];
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return nil;
|
if (@available(iOS 13.0, *)) {
|
||||||
|
return [[HapticContext alloc] initDeviceEngineContextWithGamepad:gamepad];
|
||||||
|
} else return nil;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,7 +318,9 @@
|
|||||||
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityLeftTrigger];
|
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityLeftTrigger];
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return nil;
|
if (@available(iOS 13.0, *)) {
|
||||||
|
return [[HapticContext alloc] initDeviceEngineContextWithGamepad:gamepad];
|
||||||
|
} else return nil;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +329,9 @@
|
|||||||
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityRightTrigger];
|
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityRightTrigger];
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return nil;
|
if (@available(iOS 13.0, *)) {
|
||||||
|
return [[HapticContext alloc] initDeviceEngineContextWithGamepad:gamepad];
|
||||||
|
} else return nil;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
//
|
||||||
|
// InertialScroller.swift
|
||||||
|
// VoidLink
|
||||||
|
//
|
||||||
|
// Created by True砖家 on 2025/12/5.
|
||||||
|
// Copyright © 2025 True砖家 on Bilibili. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
class InertialScroller {
|
||||||
|
public var decelerationRateX: CGFloat
|
||||||
|
public var decelerationRateY: CGFloat
|
||||||
|
|
||||||
|
public lazy var timer: SafeTimer? = {
|
||||||
|
SafeTimer(interval: 1/displayLinkRate) { [weak self] in
|
||||||
|
guard let self = self, handler != nil else {return}
|
||||||
|
self.vector.dx = self.vector.dx * decelerationRateX
|
||||||
|
self.vector.dy = self.vector.dy * decelerationRateY
|
||||||
|
if abs(self.vector.dx) < self.timerSuspendThreshold && abs(self.vector.dy) < self.timerSuspendThreshold {
|
||||||
|
self.timer?.pause()
|
||||||
|
}
|
||||||
|
(self.handler ?? {})()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
public var vector: CGVector = .zero
|
||||||
|
public var displayLinkRate: CGFloat
|
||||||
|
public var timerSuspendThreshold: CGFloat = 0.06
|
||||||
|
public var handler: (() -> Void)?
|
||||||
|
|
||||||
|
init(decelerationRate: CGFloat = 0.93, displayLinkRate: CGFloat = 60, handler: (() -> Void)? = nil) {
|
||||||
|
self.decelerationRateX = decelerationRate
|
||||||
|
self.decelerationRateY = decelerationRate
|
||||||
|
self.displayLinkRate = displayLinkRate
|
||||||
|
self.handler = handler
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#import "KeyboardSupport.h"
|
#import "KeyboardSupport.h"
|
||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
|
#include "VoidLink-Swift.h"
|
||||||
|
|
||||||
@implementation KeyboardSupport
|
@implementation KeyboardSupport
|
||||||
|
|
||||||
@@ -17,7 +18,6 @@
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
short keyCode;
|
short keyCode;
|
||||||
|
|
||||||
switch (press.type) {
|
switch (press.type) {
|
||||||
case UIPressTypeUpArrow:
|
case UIPressTypeUpArrow:
|
||||||
keyCode = 0x26;
|
keyCode = 0x26;
|
||||||
@@ -36,9 +36,13 @@
|
|||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
|
|
||||||
LiSendKeyboardEvent(0x8000 | keyCode,
|
bool physicalGamepadConnected = ControllerUtil.activeStreamingGCControllers.count>0;
|
||||||
down ? KEY_ACTION_DOWN : KEY_ACTION_UP,
|
// dispatch_after(dispatch_time(DISPATCH_TIME_NOW, physicalGamepadConnected ? 0.005*NSEC_PER_SEC : 0), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||||
0);
|
if(!physicalGamepadConnected)
|
||||||
|
LiSendKeyboardEvent(0x8000 | keyCode,
|
||||||
|
down ? KEY_ACTION_DOWN : KEY_ACTION_UP,
|
||||||
|
0);
|
||||||
|
// });
|
||||||
|
|
||||||
return YES;
|
return YES;
|
||||||
}
|
}
|
||||||
@@ -252,7 +256,7 @@
|
|||||||
NSLog(@"Undefined key pressed");
|
NSLog(@"Undefined key pressed");
|
||||||
return false;
|
return false;
|
||||||
case 669: // This value corresponds to the "Globe" or "Language" key on most Apple branded iPad keyboards.
|
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;
|
break;
|
||||||
default:
|
default:
|
||||||
NSLog(@"Unhandled HID usage: %lu", (unsigned long)key.keyCode);
|
NSLog(@"Unhandled HID usage: %lu", (unsigned long)key.keyCode);
|
||||||
|
|||||||
@@ -0,0 +1,545 @@
|
|||||||
|
//
|
||||||
|
// MicHandler.swift
|
||||||
|
// VoidLink
|
||||||
|
//
|
||||||
|
// Created by True砖家 on 2025/9/2.
|
||||||
|
// Copyright © 2025 True砖家 on Bilibili. All rights reserved.
|
||||||
|
|
||||||
|
import AVFoundation
|
||||||
|
import Collections
|
||||||
|
|
||||||
|
@objc public protocol MicHandlerDelegate: AnyObject {
|
||||||
|
@objc optional func micHandlerDidFinishPlayback(_ handler: MicHandler)
|
||||||
|
@objc optional func micHandler(_ handler: MicHandler, didFailWithError error: NSError)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objcMembers
|
||||||
|
public class MicHandler: NSObject {
|
||||||
|
|
||||||
|
private var notificationTokens = [NSObjectProtocol]()
|
||||||
|
private let engine = AVAudioEngine()
|
||||||
|
private let playerNode = AVAudioPlayerNode()
|
||||||
|
private var audioSink: Any?
|
||||||
|
private var micInputFormat: AVAudioFormat!
|
||||||
|
private var isRecording = false
|
||||||
|
private var useBuiltinMic = false
|
||||||
|
|
||||||
|
private var pcm16BufferDeque = Deque<Int16>()
|
||||||
|
private var pcm16BufferArray: [Int16] = []
|
||||||
|
private let bufferQueue = DispatchQueue(label: "pcm.buffer.queue")
|
||||||
|
private var timer: SafeTimer?
|
||||||
|
private static var volume: Float = 1.0
|
||||||
|
|
||||||
|
/*
|
||||||
|
private var recordedBuffers: [AVAudioPCMBuffer] = []
|
||||||
|
private var recordedPCM16: [Data] = []
|
||||||
|
private var recordedOpusPackets: [Data] = []
|
||||||
|
*/
|
||||||
|
|
||||||
|
private var opusEncoder: OpaquePointer?
|
||||||
|
private var opusDecoder: OpaquePointer?
|
||||||
|
|
||||||
|
private var sequenceNumber: UInt16 = 0
|
||||||
|
private let ssrc: UInt32 = 0x12345678
|
||||||
|
|
||||||
|
private var globalTimestamp: TimeInterval = 0
|
||||||
|
|
||||||
|
|
||||||
|
public weak var delegate: MicHandlerDelegate?
|
||||||
|
|
||||||
|
@objc public init(useBuiltinMic:Bool) {
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
let token = NotificationCenter.default.addObserver(
|
||||||
|
forName: AVAudioSession.interruptionNotification,
|
||||||
|
object: nil,
|
||||||
|
queue: .main
|
||||||
|
) { [weak self] notification in
|
||||||
|
self?.handleInterruption(notification)
|
||||||
|
}
|
||||||
|
notificationTokens.append(token)
|
||||||
|
|
||||||
|
self.useBuiltinMic = useBuiltinMic
|
||||||
|
do {
|
||||||
|
try configureSession()
|
||||||
|
try configureEngine()
|
||||||
|
} catch {
|
||||||
|
notify(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ----------- Mic permission -------------*/
|
||||||
|
/// 请求麦克风权限
|
||||||
|
/// - Parameter completion: 可选 block,如果为 nil 且未授权,会弹窗提示跳转系统设置
|
||||||
|
@objc static func requestPermission(_ completion: ((Bool) -> Void)? = nil) {
|
||||||
|
let permission = AVAudioSession.sharedInstance().recordPermission
|
||||||
|
switch permission {
|
||||||
|
case .granted:
|
||||||
|
completion?(true)
|
||||||
|
case .denied:
|
||||||
|
if let callback = completion {
|
||||||
|
callback(false)
|
||||||
|
} else {
|
||||||
|
showSettingsAlert()
|
||||||
|
}
|
||||||
|
case .undetermined:
|
||||||
|
AVAudioSession.sharedInstance().requestRecordPermission { granted in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if granted {
|
||||||
|
completion?(true)
|
||||||
|
} else {
|
||||||
|
if let callback = completion {
|
||||||
|
callback(false)
|
||||||
|
} else {
|
||||||
|
showSettingsAlert()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@unknown default:
|
||||||
|
if let callback = completion {
|
||||||
|
callback(false)
|
||||||
|
} else {
|
||||||
|
showSettingsAlert()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 弹窗提示用户跳转系统设置(英文版)
|
||||||
|
private static func showSettingsAlert() {
|
||||||
|
guard let topVC = topViewController() else { return }
|
||||||
|
let alert = UIAlertController(
|
||||||
|
title: LocalizationHelper.localizedString(forKey: "Microphone Permission") ,
|
||||||
|
message: LocalizationHelper.localizedString(forKey: "micPermissionTip"),
|
||||||
|
preferredStyle: .alert
|
||||||
|
)
|
||||||
|
alert.addAction(UIAlertAction(title: LocalizationHelper.localizedString(forKey: "Cancel"), style: .cancel, handler: nil))
|
||||||
|
alert.addAction(UIAlertAction(title: LocalizationHelper.localizedString(forKey: "Go to Settings") , style: .default, handler: { _ in
|
||||||
|
openSettings()
|
||||||
|
}))
|
||||||
|
topVC.present(alert, animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 打开系统设置
|
||||||
|
@objc static func openSettings() {
|
||||||
|
guard let url = URL(string: UIApplication.openSettingsURLString),
|
||||||
|
UIApplication.shared.canOpenURL(url) else { return }
|
||||||
|
UIApplication.shared.open(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 检查麦克风权限状态(返回 Int,OC 可用)
|
||||||
|
@objc static func permissionGranted() -> Bool {
|
||||||
|
return AVAudioSession.sharedInstance().recordPermission == AVAudioSession.RecordPermission.granted
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取最顶层 UIViewController
|
||||||
|
private static func topViewController(base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
|
||||||
|
if let nav = base as? UINavigationController {
|
||||||
|
return topViewController(base: nav.visibleViewController)
|
||||||
|
}
|
||||||
|
if let tab = base as? UITabBarController, let selected = tab.selectedViewController {
|
||||||
|
return topViewController(base: selected)
|
||||||
|
}
|
||||||
|
if let presented = base?.presentedViewController {
|
||||||
|
return topViewController(base: presented)
|
||||||
|
}
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
/* ----------------------------------------*/
|
||||||
|
|
||||||
|
|
||||||
|
/* ----------- Audio Session -------------*/
|
||||||
|
|
||||||
|
private func handleInterruption(_ notification: Notification) {
|
||||||
|
guard let info = notification.userInfo,
|
||||||
|
let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
|
||||||
|
let type = AVAudioSession.InterruptionType(rawValue: typeValue)
|
||||||
|
else { return }
|
||||||
|
|
||||||
|
switch type {
|
||||||
|
case .began:
|
||||||
|
self.stopTapping(stopEngine: true)
|
||||||
|
case .ended:
|
||||||
|
do {
|
||||||
|
try configureEngine()
|
||||||
|
} catch {
|
||||||
|
notify(error)
|
||||||
|
}
|
||||||
|
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
|
||||||
|
self.startTapping()
|
||||||
|
}
|
||||||
|
@unknown default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureOpus(sampleRate: Int32, channels: Int) throws {
|
||||||
|
var err: Int32 = 0
|
||||||
|
guard let enc = opus_encoder_create(sampleRate, Int32(channels), OPUS_APPLICATION_VOIP, &err), err == OPUS_OK else {
|
||||||
|
throw NSError(domain: "Opus", code: Int(err), userInfo: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
opusEncoder = enc
|
||||||
|
|
||||||
|
guard let dec = opus_decoder_create(sampleRate, Int32(channels), &err), err == OPUS_OK else {
|
||||||
|
throw NSError(domain: "Opus", code: Int(err), userInfo: nil)
|
||||||
|
}
|
||||||
|
opusDecoder = dec
|
||||||
|
|
||||||
|
// Optional: But defaults are fine. Only change when needed:
|
||||||
|
opus_encoder_ctl_wrapper(enc, Int32(OPUS_SET_BITRATE_REQUEST), opus_int32(64000)) // Set bitrate
|
||||||
|
opus_encoder_ctl_wrapper(enc, Int32(OPUS_SET_COMPLEXITY_REQUEST), opus_int32(5)) // Set complexity
|
||||||
|
opus_encoder_ctl_wrapper(enc, Int32(OPUS_SET_SIGNAL_REQUEST), OPUS_SIGNAL_VOICE) // Set signal type
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public func startTapping() {
|
||||||
|
// recordedBuffers.removeAll()
|
||||||
|
isRecording = true
|
||||||
|
self.timer?.start()
|
||||||
|
/*
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + seconds) { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.isRecording = false
|
||||||
|
self.stop()
|
||||||
|
self.playbackRecorded()
|
||||||
|
self.playbackOpus()
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public func stopTapping(stopEngine:Bool) {
|
||||||
|
isRecording = false
|
||||||
|
self.timer?.pause()
|
||||||
|
// engine.inputNode.removeTap(onBus: 0)
|
||||||
|
if(stopEngine){
|
||||||
|
playerNode.stop()
|
||||||
|
engine.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureSession() throws {
|
||||||
|
let session = AVAudioSession.sharedInstance()
|
||||||
|
/*
|
||||||
|
let bluetoothAudioOption = self.useBuiltinMic ? AVAudioSession.CategoryOptions.allowBluetoothA2DP : AVAudioSession.CategoryOptions.allowBluetooth
|
||||||
|
try session.setCategory(.playAndRecord, mode: .default, options: [.mixWithOthers, .defaultToSpeaker, bluetoothAudioOption])
|
||||||
|
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
try session.setAllowHapticsAndSystemSoundsDuringRecording(true)
|
||||||
|
} */
|
||||||
|
// AVAudioSession Initiailized in Connection.m -> ArInit
|
||||||
|
|
||||||
|
// 列出所有可用输入
|
||||||
|
if self.useBuiltinMic, let inputs = session.availableInputs {
|
||||||
|
for port in inputs {
|
||||||
|
if port.portType == .builtInMic {
|
||||||
|
try session.setPreferredInput(port)
|
||||||
|
print("Set preferred input to built-in mic")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// try session.setActive(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func sendOpusFrameFromDequeBuffer() {
|
||||||
|
bufferQueue.sync {
|
||||||
|
if pcm16BufferDeque.count >= 960 {
|
||||||
|
let chunk = Array(pcm16BufferDeque.prefix(960))
|
||||||
|
var packet = [UInt8](repeating: 0, count: 4000)
|
||||||
|
guard let enc = self.opusEncoder else {return}
|
||||||
|
let outBytes = opus_encode(enc, chunk, 960, &packet, Int32(packet.count))
|
||||||
|
sendMicrophoneOpusData(packet, outBytes)
|
||||||
|
let removeCount = min(960, pcm16BufferDeque.count)
|
||||||
|
if removeCount > 0 {
|
||||||
|
pcm16BufferDeque.removeFirst(removeCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func sendOpusFrameFromArrayBuffer() {
|
||||||
|
bufferQueue.sync {
|
||||||
|
if pcm16BufferArray.count >= 960 {
|
||||||
|
let chunk = Array(pcm16BufferArray.prefix(960))
|
||||||
|
var packet = [UInt8](repeating: 0, count: 4000)
|
||||||
|
guard let enc = self.opusEncoder else {return}
|
||||||
|
let outBytes = opus_encode(enc, chunk, 960, &packet, Int32(packet.count))
|
||||||
|
sendMicrophoneOpusData(packet, outBytes)
|
||||||
|
let removeCount = min(960, pcm16BufferArray.count)
|
||||||
|
if removeCount > 0 {
|
||||||
|
pcm16BufferArray.removeFirst(removeCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public static func setVolume(_ linearVolume: Float) {
|
||||||
|
let clamped = max(0.0, min(1.5, linearVolume))
|
||||||
|
let exponent: Float = 1.7
|
||||||
|
MicHandler.volume = powf(clamped, exponent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureEngine() throws {
|
||||||
|
let input = engine.inputNode
|
||||||
|
micInputFormat = input.inputFormat(forBus: 0)
|
||||||
|
|
||||||
|
try self.configureOpus(sampleRate: Int32(micInputFormat.sampleRate), channels: Int(micInputFormat.channelCount))
|
||||||
|
|
||||||
|
if #available(iOS 13.0, tvOS 13.0, *) {
|
||||||
|
try AVAudioSession.sharedInstance().setPreferredIOBufferDuration(0.02)
|
||||||
|
|
||||||
|
let sinkNode = AVAudioSinkNode { timestamp, frameCount, audioBufferList -> OSStatus in
|
||||||
|
|
||||||
|
guard self.isRecording else { return noErr}
|
||||||
|
|
||||||
|
let abl = audioBufferList.pointee.mBuffers
|
||||||
|
guard let data = abl.mData?.assumingMemoryBound(to: Float.self) else { return noErr }
|
||||||
|
let samples = UnsafeBufferPointer(start: data, count: Int(frameCount))
|
||||||
|
|
||||||
|
// 转 float32 -> int16
|
||||||
|
var chunk = [Int16](repeating: 0, count: samples.count)
|
||||||
|
for i in 0..<samples.count {
|
||||||
|
let clamped = max(min(samples[i] * MicHandler.volume, 1.0), -1.0)
|
||||||
|
chunk[i] = Int16(clamped * Float(Int16.max))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 追加到缓冲区
|
||||||
|
self.bufferQueue.sync {
|
||||||
|
self.pcm16BufferDeque.append(contentsOf: chunk)
|
||||||
|
}
|
||||||
|
|
||||||
|
return noErr
|
||||||
|
}
|
||||||
|
|
||||||
|
audioSink = sinkNode
|
||||||
|
engine.attach(audioSink! as! AVAudioNode)
|
||||||
|
engine.connect(engine.inputNode, to: audioSink as! AVAudioNode, format: micInputFormat)
|
||||||
|
|
||||||
|
// 开定时器,每 20ms 触发一次
|
||||||
|
self.timer = SafeTimer(interval:0.02, delay: 0.05) {
|
||||||
|
self.sendOpusFrameFromDequeBuffer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
engine.attach(playerNode)
|
||||||
|
engine.connect(playerNode, to: engine.mainMixerNode, format: micInputFormat)
|
||||||
|
input.installTap(onBus: 0, bufferSize: 5760, format: micInputFormat) { [weak self] buffer, _ in
|
||||||
|
guard let self = self, self.isRecording else { return }
|
||||||
|
|
||||||
|
// ===============================
|
||||||
|
// 把 buffer 转成 PCM16 并保存
|
||||||
|
let frameLength = Int(buffer.frameLength)
|
||||||
|
let channels = Int(buffer.format.channelCount)
|
||||||
|
var pcm16InterleavedBuffer = [Int16](repeating: 0, count: frameLength * channels)
|
||||||
|
|
||||||
|
if let floatPtrs = buffer.floatChannelData {
|
||||||
|
for ch in 0..<channels {
|
||||||
|
let floatPtr = floatPtrs[ch]
|
||||||
|
for i in 0..<frameLength {
|
||||||
|
let f = floatPtr[i] * MicHandler.volume
|
||||||
|
// 把 float 转到 Int16 范围:假设 float 在 -1…+1 之间
|
||||||
|
// 乘以 Int16.max (32767),再做裁剪
|
||||||
|
let scaled = f * Float(Int16.max)
|
||||||
|
let clipped: Float
|
||||||
|
if scaled > Float(Int16.max) {
|
||||||
|
clipped = Float(Int16.max)
|
||||||
|
} else if scaled < Float(Int16.min) {
|
||||||
|
clipped = Float(Int16.min)
|
||||||
|
} else {
|
||||||
|
clipped = scaled
|
||||||
|
}
|
||||||
|
pcm16InterleavedBuffer[i * channels + ch] = Int16(clipped)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 现在 pcm16InterleavedBuffer 里就是转换后的 Int16 数据
|
||||||
|
self.bufferQueue.sync {
|
||||||
|
self.pcm16BufferArray.append(contentsOf: pcm16InterleavedBuffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开定时器,每 20ms 触发一次
|
||||||
|
self.timer = SafeTimer(interval:0.02, delay: 0.05) {
|
||||||
|
self.sendOpusFrameFromArrayBuffer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
engine.prepare()
|
||||||
|
try engine.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public func clean() {
|
||||||
|
for token in notificationTokens {
|
||||||
|
NotificationCenter.default.removeObserver(token)
|
||||||
|
}
|
||||||
|
notificationTokens.removeAll()
|
||||||
|
self.timer?.clean()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func notify(_ error: Error) {
|
||||||
|
delegate?.micHandler?(self, didFailWithError: error as NSError)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===============================
|
||||||
|
// 🔹 新增播放 Opus 数据方法
|
||||||
|
/*
|
||||||
|
private func playbackOpus() {
|
||||||
|
guard let dec = opusDecoder else { return }
|
||||||
|
|
||||||
|
let channels = Int(micInputFormat.channelCount)
|
||||||
|
|
||||||
|
for (index, packet) in recordedOpusPackets.enumerated() {
|
||||||
|
let maxFrames = 5760 // 最大 120ms
|
||||||
|
let pcmBuf = UnsafeMutablePointer<Int16>.allocate(capacity: maxFrames * channels)
|
||||||
|
defer { pcmBuf.deallocate() }
|
||||||
|
|
||||||
|
let frameCount = opus_decode(dec,
|
||||||
|
[UInt8](packet),
|
||||||
|
Int32(packet.count),
|
||||||
|
pcmBuf,
|
||||||
|
Int32(maxFrames),
|
||||||
|
0)
|
||||||
|
if frameCount < 0 {
|
||||||
|
print("Opus decode error: \(frameCount)")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构造源格式 AVAudioPCMBuffer (PCM16)
|
||||||
|
let sourceFormat = AVAudioFormat(commonFormat: .pcmFormatInt16,
|
||||||
|
sampleRate: micInputFormat.sampleRate,
|
||||||
|
channels: micInputFormat.channelCount,
|
||||||
|
interleaved: true)!
|
||||||
|
|
||||||
|
guard let sourceBuffer = AVAudioPCMBuffer(pcmFormat: sourceFormat,
|
||||||
|
frameCapacity: AVAudioFrameCount(frameCount)) else { continue }
|
||||||
|
sourceBuffer.frameLength = AVAudioFrameCount(frameCount)
|
||||||
|
|
||||||
|
// 填充 PCM16 数据到 sourceBuffer
|
||||||
|
let srcPointer = sourceBuffer.int16ChannelData![0]
|
||||||
|
for i in 0..<Int(frameCount * Int32(channels)) {
|
||||||
|
srcPointer[i] = pcmBuf[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 准备目标 buffer (Float32)
|
||||||
|
guard let floatBuffer = AVAudioPCMBuffer(pcmFormat: micInputFormat,
|
||||||
|
frameCapacity: AVAudioFrameCount(frameCount)) else { continue }
|
||||||
|
|
||||||
|
// 🔹 使用 AVAudioConverter 转换
|
||||||
|
let converter = AVAudioConverter(from: sourceFormat, to: micInputFormat)!
|
||||||
|
var error: NSError? = nil
|
||||||
|
let inputBlock: AVAudioConverterInputBlock = { inNumPackets, outStatus in
|
||||||
|
outStatus.pointee = .haveData
|
||||||
|
return sourceBuffer
|
||||||
|
}
|
||||||
|
|
||||||
|
converter.convert(to: floatBuffer, error: &error, withInputFrom: inputBlock)
|
||||||
|
if let error = error {
|
||||||
|
print("AVAudioConverter error: \(error)")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 播放
|
||||||
|
if index == recordedOpusPackets.count - 1 {
|
||||||
|
playerNode.scheduleBuffer(floatBuffer, at: nil, options: []) { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.playerNode.stop()
|
||||||
|
self.engine.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
playerNode.scheduleBuffer(floatBuffer, at: nil, options: [], completionHandler: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动 engine
|
||||||
|
if !engine.isRunning {
|
||||||
|
do { try engine.start() } catch { print(error) }
|
||||||
|
}
|
||||||
|
|
||||||
|
if !playerNode.isPlaying {
|
||||||
|
playerNode.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===============================
|
||||||
|
// 🔹 改动 2:播放 PCM16 数据
|
||||||
|
private func playbackRecorded() {
|
||||||
|
do {
|
||||||
|
try configureEngine()
|
||||||
|
} catch {
|
||||||
|
notify(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
let channels = Int(micInputFormat.channelCount)
|
||||||
|
|
||||||
|
for (index, pcmData) in recordedPCM16.enumerated() {
|
||||||
|
let frameCount = pcmData.count / (MemoryLayout<Int16>.size * channels)
|
||||||
|
guard let buf = AVAudioPCMBuffer(pcmFormat: micInputFormat, frameCapacity: AVAudioFrameCount(frameCount)) else { continue }
|
||||||
|
buf.frameLength = buf.frameCapacity
|
||||||
|
|
||||||
|
// PCM16 -> Float32
|
||||||
|
pcmData.withUnsafeBytes { rawBuf in
|
||||||
|
let pcmPtr = rawBuf.bindMemory(to: Int16.self).baseAddress!
|
||||||
|
for i in 0..<frameCount {
|
||||||
|
for ch in 0..<channels {
|
||||||
|
buf.floatChannelData?[ch][i] = Float(pcmPtr[i * channels + ch]) / Float(Int16.max)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只在最后一个 buffer 设置 completionHandler
|
||||||
|
if index == recordedPCM16.count - 1 {
|
||||||
|
playerNode.scheduleBuffer(buf, at: nil, options: []) { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
// 🔹 回到主线程安全停止
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.playerNode.stop()
|
||||||
|
self.engine.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
playerNode.scheduleBuffer(buf, at: nil, options: [], completionHandler: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
playerNode.play()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func deepCopy(_ buffer: AVAudioPCMBuffer, format: AVAudioFormat) -> AVAudioPCMBuffer? {
|
||||||
|
guard let out = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: buffer.frameLength) else { return nil }
|
||||||
|
out.frameLength = buffer.frameLength
|
||||||
|
let channels = Int(format.channelCount)
|
||||||
|
for ch in 0..<channels {
|
||||||
|
if let src = buffer.floatChannelData?[ch], let dst = out.floatChannelData?[ch] {
|
||||||
|
dst.update(from: src, count: Int(buffer.frameLength))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
private func merge(buffers: [AVAudioPCMBuffer], format: AVAudioFormat) -> AVAudioPCMBuffer? {
|
||||||
|
let total = buffers.reduce(0) { $0 + $1.frameLength }
|
||||||
|
guard let out = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: total) else { return nil }
|
||||||
|
out.frameLength = total
|
||||||
|
|
||||||
|
var writePos: AVAudioFrameCount = 0
|
||||||
|
let channels = Int(format.channelCount)
|
||||||
|
|
||||||
|
for b in buffers {
|
||||||
|
let frames = Int(b.frameLength)
|
||||||
|
for ch in 0..<channels {
|
||||||
|
if let src = b.floatChannelData?[ch], let dst = out.floatChannelData?[ch] {
|
||||||
|
dst.advanced(by: Int(writePos)).update(from: src, count: frames)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writePos += b.frameLength
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------------------*/
|
||||||
@@ -0,0 +1,525 @@
|
|||||||
|
//
|
||||||
|
// MicHandler.swift
|
||||||
|
// VoidLink
|
||||||
|
//
|
||||||
|
// Created by True砖家 on 2025/9/28.
|
||||||
|
// Copyright © 2025 True砖家 on Bilibili. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreMotion
|
||||||
|
|
||||||
|
@objc public protocol OnScreenWidgetStickMixedInputDelegate: AnyObject {
|
||||||
|
func mixOnScreenRightStickAndGyroInput(x: CGFloat, y: CGFloat)
|
||||||
|
func mixOnScreenLeftStickAndGyroInput(x: CGFloat, y: CGFloat)
|
||||||
|
func gyroMixInputStarted() -> Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc class MotionHandler: NSObject, OnScreenWidgetStickMixedInputDelegate{
|
||||||
|
public func mixOnScreenRightStickAndGyroInput(x: CGFloat, y: CGFloat) {
|
||||||
|
rightStickTouchInputX = x
|
||||||
|
rightStickTouchInputY = y
|
||||||
|
}
|
||||||
|
|
||||||
|
public func mixOnScreenLeftStickAndGyroInput(x: CGFloat, y: CGFloat) {
|
||||||
|
// rollIntegral = 0
|
||||||
|
leftStickTouchInputX = x
|
||||||
|
leftStickTouchInputY = y
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public func mixPhysicalRightStickAndGyroInput(x: CGFloat, y: CGFloat) {
|
||||||
|
rightStickPhysicalInputX = synthesizePhysicalStick ? x : 0
|
||||||
|
rightStickPhysicalInputY = synthesizePhysicalStick ? y : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public func mixPhysicalLeftStickAndGyroInput(x: CGFloat, y: CGFloat) {
|
||||||
|
// rollIntegral = 0
|
||||||
|
leftStickPhysicalInputX = synthesizePhysicalStick ? x : 0
|
||||||
|
leftStickPhysicalInputY = synthesizePhysicalStick ? y : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
public func gyroMixInputStarted() -> Bool {
|
||||||
|
return motionIsWorking
|
||||||
|
}
|
||||||
|
|
||||||
|
private static let sharedInstance = MotionHandler()
|
||||||
|
|
||||||
|
@objc class func shared(profile: OSCProfile?) -> MotionHandler {
|
||||||
|
let sharedInstance = MotionHandler.sharedInstance
|
||||||
|
guard let profile = profile else { return sharedInstance }
|
||||||
|
sharedInstance.useBuiltinGyro = profile.useBuiltinGyro
|
||||||
|
sharedInstance.swapYawAndRoll = profile.swapYawAndRoll
|
||||||
|
sharedInstance.mapGyroTo = profile.mapGyroTo
|
||||||
|
sharedInstance.synthesizePhysicalStick = profile.synthesizePhysicalStick
|
||||||
|
sharedInstance.rollToLeftStick = profile.rollToLeftStick
|
||||||
|
sharedInstance.yawPitchToRightStick = profile.yawPitchToRightStick
|
||||||
|
sharedInstance.sensitvityYaw = profile.gyroSensitivityYaw
|
||||||
|
sharedInstance.sensitvityPitch = profile.gyroSensitivityPitch
|
||||||
|
sharedInstance.sensitvityRoll = profile.gyroSensitivityRoll
|
||||||
|
sharedInstance.gyroToStickMinOffset = profile.gyroToStickMinOffset
|
||||||
|
sharedInstance.onScreenControls = OnScreenControls.shared()
|
||||||
|
return sharedInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public var motionControlStarted: Bool = false
|
||||||
|
@objc var useBuiltinGyro: Bool = true
|
||||||
|
@objc var swapYawAndRoll: Bool = false
|
||||||
|
private var motionIsWorking: Bool = false
|
||||||
|
private var accelControlStarted: Bool = false
|
||||||
|
private let motionManager = CMMotionManager()
|
||||||
|
private weak var activeGCController:GCController?
|
||||||
|
private var synthesizePhysicalStick:Bool = false
|
||||||
|
private var mapGyroTo:MapGyroTo = .mapGyroToControllerStick
|
||||||
|
private var rollToLeftStick:Bool = false
|
||||||
|
private var gravityYAngleRef:Double?
|
||||||
|
private var previousGravityYAngle:Double?
|
||||||
|
private var gravityYUnwrappedAngle:Double = 0
|
||||||
|
private var yawPitchToRightStick:Bool = false
|
||||||
|
public var sensitvityYaw:CGFloat = 1.0
|
||||||
|
public var sensitvityPitch:CGFloat = 1.0
|
||||||
|
public var sensitvityRoll:CGFloat = 1.0
|
||||||
|
@objc public var widgetYawFactor:CGFloat = 1.0
|
||||||
|
@objc public var widgetPitchFactor:CGFloat = 1.0
|
||||||
|
@objc public var widgetRollFactor:CGFloat = 1.0
|
||||||
|
@objc public var previousWidgetYawFactor:CGFloat = 1.0
|
||||||
|
@objc public var previousWidgetPitchFactor:CGFloat = 1.0
|
||||||
|
@objc public var previousWidgetRollFactor:CGFloat = 1.0
|
||||||
|
public var motionStarter:Any?
|
||||||
|
private var windowScene: Any?
|
||||||
|
|
||||||
|
@objc public var onScreenControls: OnScreenControls?
|
||||||
|
public let stickMaxOffset: CGFloat = 0x7FFE
|
||||||
|
private var stickInputScale: CGFloat = 35
|
||||||
|
|
||||||
|
private var yaw:Double = 0
|
||||||
|
private var pitch:Double = 0
|
||||||
|
private var roll:Double = 0
|
||||||
|
private var leftStickMotion:Double = 0
|
||||||
|
|
||||||
|
private var isCalibrating: Bool = false
|
||||||
|
private var sumX: Double = 0
|
||||||
|
private var sumY: Double = 0
|
||||||
|
private var sumZ: Double = 0
|
||||||
|
@objc public var gyroBiasX: Double = 0
|
||||||
|
@objc public var gyroBiasY: Double = 0
|
||||||
|
@objc public var gyroBiasZ: Double = 0
|
||||||
|
@objc public var controllerGyroBiasX: Double = 0
|
||||||
|
@objc public var controllerGyroBiasY: Double = 0
|
||||||
|
@objc public var controllerGyroBiasZ: Double = 0
|
||||||
|
@objc public var gyroToStickMinOffset:Double = 0
|
||||||
|
private var yawBias:Double = 0
|
||||||
|
private var pitchBias:Double = 0
|
||||||
|
private var rollBias:Double = 0
|
||||||
|
|
||||||
|
private var rightStickTouchInputX:Double = 0
|
||||||
|
private var rightStickTouchInputY:Double = 0
|
||||||
|
|
||||||
|
private var leftStickTouchInputX:Double = 0
|
||||||
|
private var leftStickTouchInputY:Double = 0
|
||||||
|
|
||||||
|
private var rightStickPhysicalInputX:Double = 0
|
||||||
|
private var rightStickPhysicalInputY:Double = 0
|
||||||
|
public var gyroToStickOffset:CGVector = CGVector(dx: 0, dy: 0)
|
||||||
|
|
||||||
|
private var leftStickPhysicalInputX:Double = 0
|
||||||
|
private var leftStickPhysicalInputY:Double = 0
|
||||||
|
|
||||||
|
var updateInterval: TimeInterval = 1.0 / 120.0 {
|
||||||
|
didSet {
|
||||||
|
motionManager.accelerometerUpdateInterval = updateInterval
|
||||||
|
motionManager.gyroUpdateInterval = updateInterval
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public override init() {
|
||||||
|
// _ = OSCProfilesManager.sharedManager(CGRectZero)
|
||||||
|
super.init()
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene
|
||||||
|
self.windowScene = scene
|
||||||
|
} else {
|
||||||
|
// Fallback on earlier versions
|
||||||
|
}
|
||||||
|
motionManager.accelerometerUpdateInterval = updateInterval
|
||||||
|
motionManager.gyroUpdateInterval = updateInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
public func startMotionControlByOnScreenButton(_ sender: OnScreenWidgetView, yawFactor:CGFloat, pitchFactor:CGFloat, rollFactor:CGFloat){
|
||||||
|
self.previousWidgetYawFactor = self.widgetYawFactor
|
||||||
|
self.previousWidgetPitchFactor = self.widgetPitchFactor
|
||||||
|
self.previousWidgetRollFactor = self.widgetRollFactor
|
||||||
|
self.widgetYawFactor = yawFactor
|
||||||
|
self.widgetPitchFactor = pitchFactor
|
||||||
|
self.widgetRollFactor = rollFactor
|
||||||
|
if self.motionStarter == nil {
|
||||||
|
self.motionStarter = sender
|
||||||
|
if sender.motionControlButtonString != "GYROPAUSE" {self.startMotionUpdate()}
|
||||||
|
}
|
||||||
|
else if sender.motionControlButtonString == "GYROPAUSE" {
|
||||||
|
self.startMotionUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public func startMotionControlByControllerButton(){
|
||||||
|
self.previousWidgetYawFactor = self.widgetYawFactor
|
||||||
|
self.previousWidgetPitchFactor = self.widgetPitchFactor
|
||||||
|
self.previousWidgetRollFactor = self.widgetRollFactor
|
||||||
|
self.widgetYawFactor = 1
|
||||||
|
self.widgetPitchFactor = 1
|
||||||
|
self.widgetRollFactor = 1
|
||||||
|
self.startMotionUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public func startMotionUpdate() {
|
||||||
|
if !motionControlStarted {
|
||||||
|
motionControlStarted = true
|
||||||
|
resetGravityYOffsetTracking()
|
||||||
|
|
||||||
|
// print("startGyroUpdate useBuiltinGyro \(useBuiltinGyro) \(CACurrentMediaTime())")
|
||||||
|
|
||||||
|
if useBuiltinGyro {
|
||||||
|
if motionManager.isGyroAvailable {
|
||||||
|
if rollToLeftStick {
|
||||||
|
leftStickMotion = leftStickTouchInputX
|
||||||
|
}
|
||||||
|
motionManager.startDeviceMotionUpdates(to: .main) { [weak self] motionData, _ in
|
||||||
|
guard let self = self, let data = motionData else { return }
|
||||||
|
self.handleMotionData(deviceMotion: data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if #available(iOS 14.0, *) {
|
||||||
|
if activeGCController == nil {
|
||||||
|
if let controllers = ControllerUtil.activeStreamingGCControllers as? Set<GCController> {
|
||||||
|
for controller in controllers {
|
||||||
|
if controller.playerIndex == .index1 {
|
||||||
|
activeGCController = controller
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let motion = activeGCController?.motion {
|
||||||
|
if motion.sensorsRequireManualActivation {
|
||||||
|
motion.sensorsActive = true
|
||||||
|
}
|
||||||
|
if rollToLeftStick {
|
||||||
|
leftStickMotion = 0
|
||||||
|
}
|
||||||
|
if motion.sensorsActive {
|
||||||
|
motion.valueChangedHandler = { [weak self] motion in
|
||||||
|
self?.handleMotionData(gcMotion: motion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {return}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public func startAccelUpdate() {
|
||||||
|
accelControlStarted = true
|
||||||
|
if motionManager.isAccelerometerAvailable {
|
||||||
|
motionManager.startAccelerometerUpdates(to: .main) { [weak self] accelData, _ in
|
||||||
|
guard let self = self, let data = accelData else { return }
|
||||||
|
self.handleAccelerometerData(x: data.acceleration.x,
|
||||||
|
y: data.acceleration.y,
|
||||||
|
z: data.acceleration.z)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 停止更新
|
||||||
|
// private var interruptNoneGyroInput:Bool = false
|
||||||
|
|
||||||
|
@objc public func stopMotionUpdate(interruptNoneGyroInput:Bool=false) {
|
||||||
|
motionControlStarted = false
|
||||||
|
motionIsWorking = false
|
||||||
|
resetGravityYOffsetTracking()
|
||||||
|
if motionManager.isDeviceMotionActive{
|
||||||
|
motionManager.stopDeviceMotionUpdates()
|
||||||
|
}
|
||||||
|
if #available(iOS 14.0, *) {
|
||||||
|
activeGCController?.motion?.sensorsActive = false
|
||||||
|
}
|
||||||
|
self.clearGyroInput(interruptNonGyroInput:interruptNoneGyroInput)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public func stopAccelUpdate() {
|
||||||
|
accelControlStarted = false
|
||||||
|
if motionManager.isAccelerometerActive {
|
||||||
|
motionManager.stopAccelerometerUpdates()
|
||||||
|
}
|
||||||
|
// self.onScreenControls.clearLeftStickTouchPadFlag()
|
||||||
|
// self.onScreenControls.clearRightStickTouchPadFlag()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - 私有方法处理数据
|
||||||
|
private func handleAccelerometerData(x: Double, y: Double, z: Double) {
|
||||||
|
// 在这里处理加速度数据,比如计算方向、存储或驱动逻辑
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleMotionData(deviceMotion: CMDeviceMotion? = nil, gcMotion:GCMotion? = nil) {
|
||||||
|
var x:Double = 0
|
||||||
|
var y:Double = 0
|
||||||
|
var z:Double = 0
|
||||||
|
// var attitudeRoll:Double = 0
|
||||||
|
var gravityYOffset:Double = 0
|
||||||
|
|
||||||
|
if let deviceMotion = deviceMotion {
|
||||||
|
x = deviceMotion.rotationRate.x
|
||||||
|
y = deviceMotion.rotationRate.y
|
||||||
|
z = deviceMotion.rotationRate.z
|
||||||
|
// attitudeRoll = deviceMotion.attitude.yaw
|
||||||
|
gravityYOffset = updateGravityYOffset(
|
||||||
|
gravityX: deviceMotion.gravity.x,
|
||||||
|
gravityY: deviceMotion.gravity.y,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let gcMotion = gcMotion {
|
||||||
|
x = gcMotion.rotationRate.x
|
||||||
|
y = gcMotion.rotationRate.y
|
||||||
|
z = gcMotion.rotationRate.z
|
||||||
|
// attitudeRoll = gcMotion.attitude.z
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var yawSource:Double = 0
|
||||||
|
var pitchSource:Double = 0
|
||||||
|
var rollSource:Double = 0
|
||||||
|
|
||||||
|
/*
|
||||||
|
let correctedX:Double = x - gyroBiasX
|
||||||
|
let correctedY:Double = y - gyroBiasY
|
||||||
|
let correctedZ:Double = z - gyroBiasZ */
|
||||||
|
|
||||||
|
if useBuiltinGyro {
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
let orientation = (windowScene as! UIWindowScene).interfaceOrientation
|
||||||
|
switch orientation {
|
||||||
|
case .landscapeLeft:
|
||||||
|
yawBias = gyroBiasX
|
||||||
|
pitchBias = -gyroBiasY
|
||||||
|
rollBias = -gyroBiasZ
|
||||||
|
yawSource = x-yawBias
|
||||||
|
pitchSource = -y-pitchBias
|
||||||
|
rollSource = -z-rollBias
|
||||||
|
case .landscapeRight:
|
||||||
|
yawBias = -gyroBiasX
|
||||||
|
pitchBias = gyroBiasY
|
||||||
|
rollBias = -gyroBiasZ
|
||||||
|
yawSource = -x-yawBias
|
||||||
|
pitchSource = y-pitchBias
|
||||||
|
rollSource = -z-rollBias
|
||||||
|
case .portrait:
|
||||||
|
yawBias = -gyroBiasY
|
||||||
|
pitchBias = -gyroBiasX
|
||||||
|
rollBias = -gyroBiasZ
|
||||||
|
yawSource = -y-yawBias
|
||||||
|
pitchSource = -x-pitchBias
|
||||||
|
rollSource = -z-rollBias
|
||||||
|
case .portraitUpsideDown:
|
||||||
|
yawBias = gyroBiasY
|
||||||
|
pitchBias = gyroBiasX
|
||||||
|
rollBias = -gyroBiasZ
|
||||||
|
yawSource = y-yawBias
|
||||||
|
pitchSource = x-pitchBias
|
||||||
|
rollSource = -z-rollBias
|
||||||
|
default:
|
||||||
|
yawBias = gyroBiasX
|
||||||
|
pitchBias = -gyroBiasY
|
||||||
|
rollBias = -gyroBiasZ
|
||||||
|
yawSource = x-yawBias
|
||||||
|
pitchSource = -y-pitchBias
|
||||||
|
rollSource = -z-rollBias
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let orientation = UIApplication.shared.statusBarOrientation
|
||||||
|
if orientation.isLandscape {
|
||||||
|
yawBias = gyroBiasX
|
||||||
|
pitchBias = -gyroBiasY
|
||||||
|
rollBias = -gyroBiasZ
|
||||||
|
yawSource = x-yawBias
|
||||||
|
pitchSource = -y-pitchBias
|
||||||
|
rollSource = -z-rollBias
|
||||||
|
} else if orientation.isPortrait {
|
||||||
|
yawBias = -gyroBiasY
|
||||||
|
pitchBias = -gyroBiasX
|
||||||
|
rollBias = -gyroBiasZ
|
||||||
|
yawSource = -y-yawBias
|
||||||
|
pitchSource = -x-pitchBias
|
||||||
|
rollSource = -z-rollBias
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
yawBias = swapYawAndRoll ? controllerGyroBiasY : -controllerGyroBiasZ
|
||||||
|
pitchBias = -controllerGyroBiasX
|
||||||
|
rollBias = swapYawAndRoll ? -controllerGyroBiasZ : controllerGyroBiasY
|
||||||
|
yawSource = (swapYawAndRoll ? y : -z) - yawBias
|
||||||
|
pitchSource = -x-pitchBias
|
||||||
|
rollSource = (swapYawAndRoll ? -z : y) - rollBias
|
||||||
|
}
|
||||||
|
|
||||||
|
if !motionControlStarted {
|
||||||
|
self.clearGyroInput(interruptNonGyroInput: false)
|
||||||
|
// print("Gyro: stopped")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
motionIsWorking = true
|
||||||
|
|
||||||
|
if mapGyroTo == MapGyroTo.mapGyroToMouse {
|
||||||
|
yaw = yawSource*sensitvityYaw*widgetYawFactor*30
|
||||||
|
pitch = pitchSource*sensitvityPitch*widgetPitchFactor*30
|
||||||
|
LiSendMouseMoveEvent(Int16(yaw),Int16(pitch))
|
||||||
|
}
|
||||||
|
|
||||||
|
//guard let onScreenControls = onScreenControls else { return }
|
||||||
|
|
||||||
|
if mapGyroTo == MapGyroTo.mapGyroToControllerStick {
|
||||||
|
if yawPitchToRightStick {
|
||||||
|
gyroToStickOffset.dx = gyroInputToStickInput(input:yawSource*sensitvityYaw*widgetYawFactor*10)
|
||||||
|
yaw = rightStickTouchInputX + rightStickPhysicalInputX + gyroToStickOffset.dx
|
||||||
|
yaw = self.clampStickInput(input: yaw)
|
||||||
|
|
||||||
|
gyroToStickOffset.dy = -gyroInputToStickInput(input:pitchSource*sensitvityPitch*widgetPitchFactor*10)
|
||||||
|
pitch = rightStickTouchInputY + rightStickPhysicalInputY + gyroToStickOffset.dy
|
||||||
|
pitch = self.clampStickInput(input: pitch)
|
||||||
|
|
||||||
|
let offsetVector = ControllerUtil.compensated(offsetVector: CGVector(dx: yaw, dy: pitch), minOffset: gyroToStickMinOffset)
|
||||||
|
|
||||||
|
onScreenControls?.sendRightStickTouchPadEvent(offsetVector.dx, offsetVector.dy)
|
||||||
|
}
|
||||||
|
if rollToLeftStick {
|
||||||
|
roll = gyroInputToStickInput(input:rollSource*sensitvityRoll*widgetRollFactor*0.2)
|
||||||
|
|
||||||
|
leftStickMotion = self.clampStickInput(input: useBuiltinGyro ? -stickMaxOffset*(gravityYOffset/Double.pi)*3*sensitvityRoll*widgetRollFactor : leftStickMotion + roll)
|
||||||
|
|
||||||
|
let mixedLeftStickOffsetX = self.clampStickInput(input: leftStickMotion+leftStickTouchInputX+leftStickPhysicalInputX)
|
||||||
|
let mixedLeftStickOffsetY = self.clampStickInput(input: leftStickTouchInputY+leftStickPhysicalInputY)
|
||||||
|
|
||||||
|
let offsetVector = ControllerUtil.compensated(offsetVector: CGVector(dx: mixedLeftStickOffsetX, dy: mixedLeftStickOffsetY), minOffset: gyroToStickMinOffset, circulate: true)
|
||||||
|
|
||||||
|
onScreenControls?.sendLeftStickTouchPadEvent(offsetVector.dx, offsetVector.dy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func clearGyroInput(interruptNonGyroInput:Bool){
|
||||||
|
guard ControllerUtil.gamepadArrivalReported else { return }
|
||||||
|
|
||||||
|
if yawPitchToRightStick{
|
||||||
|
let clearX = rightStickPhysicalInputX+rightStickTouchInputX-yawBias
|
||||||
|
let clearY = rightStickPhysicalInputY+rightStickTouchInputY-pitchBias
|
||||||
|
let clearVector = ControllerUtil.circulated(offsetVector: CGVector(dx: clearX, dy: clearY))
|
||||||
|
|
||||||
|
onScreenControls?.sendRightStickTouchPadEvent(clearVector.dx, clearVector.dy)
|
||||||
|
}
|
||||||
|
if rollToLeftStick{
|
||||||
|
leftStickMotion = leftStickTouchInputX
|
||||||
|
let clearX = leftStickPhysicalInputX+leftStickTouchInputX-rollBias
|
||||||
|
let clearY = leftStickPhysicalInputY+leftStickTouchInputY
|
||||||
|
let clearVector = ControllerUtil.circulated(offsetVector: CGVector(dx: clearX, dy: clearY))
|
||||||
|
|
||||||
|
onScreenControls?.sendLeftStickTouchPadEvent(clearVector.dx,clearVector.dy)
|
||||||
|
}
|
||||||
|
if(interruptNonGyroInput){
|
||||||
|
onScreenControls?.clearLeftStickTouchPadFlag()
|
||||||
|
onScreenControls?.clearRightStickTouchPadFlag()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func resetGravityYOffsetTracking() {
|
||||||
|
gravityYAngleRef = nil
|
||||||
|
previousGravityYAngle = nil
|
||||||
|
gravityYUnwrappedAngle = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateGravityYOffset(gravityX: Double, gravityY: Double) -> Double {
|
||||||
|
let gravityYAngle = atan2(gravityX, -gravityY)
|
||||||
|
|
||||||
|
guard let previousGravityYAngle = previousGravityYAngle else {
|
||||||
|
gravityYAngleRef = gravityYAngle
|
||||||
|
previousGravityYAngle = gravityYAngle
|
||||||
|
gravityYUnwrappedAngle = gravityYAngle
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var delta = gravityYAngle - previousGravityYAngle
|
||||||
|
if delta > Double.pi {
|
||||||
|
delta -= 2 * Double.pi
|
||||||
|
} else if delta < -Double.pi {
|
||||||
|
delta += 2 * Double.pi
|
||||||
|
}
|
||||||
|
|
||||||
|
gravityYUnwrappedAngle += delta
|
||||||
|
self.previousGravityYAngle = gravityYAngle
|
||||||
|
return -(gravityYUnwrappedAngle - (gravityYAngleRef ?? gravityYUnwrappedAngle))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func clampStickInput(input: CGFloat) -> CGFloat{
|
||||||
|
return fmax(fmin(input, stickMaxOffset),-stickMaxOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func gyroInputToStickInput(input: CGFloat) -> CGFloat{
|
||||||
|
return self.clampStickInput(input: stickMaxOffset * input / 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public func calibrateGyroBias(duration: TimeInterval = 5.0, completion: @escaping () -> Void) {
|
||||||
|
guard motionManager.isGyroAvailable else { return }
|
||||||
|
|
||||||
|
isCalibrating = true
|
||||||
|
var sumX = 0.0, sumY = 0.0, sumZ = 0.0
|
||||||
|
var sampleCount = 0
|
||||||
|
|
||||||
|
if useBuiltinGyro {
|
||||||
|
motionManager.startDeviceMotionUpdates(to: .main) { [weak self] motionData, _ in
|
||||||
|
guard let self = self, self.isCalibrating, let data = motionData else { return }
|
||||||
|
sumX += data.rotationRate.x
|
||||||
|
sumY += data.rotationRate.y
|
||||||
|
sumZ += data.rotationRate.z
|
||||||
|
sampleCount += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if #available(iOS 14.0, *) {
|
||||||
|
if let motion = activeGCController?.motion {
|
||||||
|
if motion.sensorsRequireManualActivation {
|
||||||
|
motion.sensorsActive = true
|
||||||
|
}
|
||||||
|
if motion.sensorsActive {
|
||||||
|
motion.valueChangedHandler = { [weak self] motion in
|
||||||
|
guard let self = self, self.isCalibrating else { return }
|
||||||
|
sumX += motion.rotationRate.x
|
||||||
|
sumY += motion.rotationRate.y
|
||||||
|
sumZ += motion.rotationRate.z
|
||||||
|
sampleCount += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5秒后计算平均值
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + duration) { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.motionManager.stopDeviceMotionUpdates()
|
||||||
|
if sampleCount > 0 {
|
||||||
|
if useBuiltinGyro {
|
||||||
|
gyroBiasX = sumX / Double(sampleCount)
|
||||||
|
gyroBiasY = sumY / Double(sampleCount)
|
||||||
|
gyroBiasZ = sumZ / Double(sampleCount)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
controllerGyroBiasX = sumX / Double(sampleCount)
|
||||||
|
controllerGyroBiasY = sumY / Double(sampleCount)
|
||||||
|
controllerGyroBiasZ = sumZ / Double(sampleCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.isCalibrating = false
|
||||||
|
completion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,11 +14,11 @@
|
|||||||
NS_ASSUME_NONNULL_BEGIN
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
@interface NativeTouchHandler : UIResponder
|
@interface NativeTouchHandler : UIResponder
|
||||||
|
|
||||||
@property (nonatomic, assign) NSMutableSet<UITouch *>* touchesCapturedByOnScreenButtons;
|
@property (nonatomic, assign) NSMutableSet<UITouch *>* touchesCapturedByOnScreenButtons;
|
||||||
|
|
||||||
|
- (id)initWithView:(StreamView*)view settings:(TemporarySettings*)settings profile:(OSCProfile* )profile;
|
||||||
- (id)initWithView:(StreamView* )view andSettings:(TemporarySettings*)settings;
|
- (void)setAllowSingleTouchEnabled:(BOOL)enabled;
|
||||||
|
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
|||||||
@@ -11,16 +11,15 @@
|
|||||||
#import "NativeTouchPointer.h"
|
#import "NativeTouchPointer.h"
|
||||||
#import "OnScreenControls.h"
|
#import "OnScreenControls.h"
|
||||||
#import "StreamView.h"
|
#import "StreamView.h"
|
||||||
|
#import "VoidLink-Swift.h"
|
||||||
|
|
||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
|
|
||||||
|
|
||||||
@implementation NativeTouchHandler {
|
@implementation NativeTouchHandler {
|
||||||
StreamView* streamView;
|
__weak StreamView* streamView;
|
||||||
TemporarySettings* currentSettings;
|
|
||||||
bool activateCoordSelector;
|
bool activateCoordSelector;
|
||||||
|
bool singleTouchDisabled;
|
||||||
CGFloat pointerVelocityDividerLocationByPoints;
|
CGFloat pointerVelocityDividerLocationByPoints;
|
||||||
uint16_t touchMoveEventIntervalUs;
|
|
||||||
|
|
||||||
bool asyncNativeTouch;
|
bool asyncNativeTouch;
|
||||||
unsigned int touchDownQos;
|
unsigned int touchDownQos;
|
||||||
@@ -35,29 +34,37 @@
|
|||||||
NSMutableSet<NSNumber *> *pointerIdPool; //pre-defined pool of pointerIds.
|
NSMutableSet<NSNumber *> *pointerIdPool; //pre-defined pool of pointerIds.
|
||||||
NSMutableSet<NSNumber *> *unassignedPointerIds;
|
NSMutableSet<NSNumber *> *unassignedPointerIds;
|
||||||
NSMutableSet *blacklistedTouches;
|
NSMutableSet *blacklistedTouches;
|
||||||
|
|
||||||
|
bool trackPointEnabled;
|
||||||
|
NSMutableArray<CAShapeLayer *> * trackPointPool; //pre-defined pool of pointerIds.
|
||||||
|
|
||||||
NSMutableDictionary *pointerObjDict;
|
NSMutableDictionary *pointerObjDict;
|
||||||
|
|
||||||
CGFloat slideGestureVerticalThreshold;
|
CGFloat slideGestureVerticalThreshold;
|
||||||
CGFloat screenWidthWithThreshold;
|
CGFloat screenWidthWithThreshold;
|
||||||
CGFloat EDGE_TOLERANCE;
|
CGFloat _edgeTolerance;
|
||||||
|
|
||||||
CGRect streamViewBounds;
|
CGRect streamViewBounds;
|
||||||
|
|
||||||
|
int64_t moveEventIntervalNSec;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings{
|
- (id)initWithView:(StreamView*)view settings:(TemporarySettings*)settings profile:(OSCProfile* )profile{
|
||||||
self = [super init];
|
self = [super init];
|
||||||
self->streamView = view;
|
self->streamView = view;
|
||||||
self->currentSettings = settings;
|
self->activateCoordSelector = profile.pointerVelocityModeDivider != 1.0;
|
||||||
self->activateCoordSelector = currentSettings.pointerVelocityModeDivider.floatValue != 1.0;
|
self->moveEventIntervalNSec = (int64_t)(settings.touchMoveEventInterval.intValue * 1000);;
|
||||||
self->touchMoveEventIntervalUs = currentSettings.touchMoveEventInterval.intValue;
|
|
||||||
self->streamViewBounds = view.bounds;
|
self->streamViewBounds = view.bounds;
|
||||||
|
|
||||||
self->pointerIdDict = [NSMutableDictionary dictionary];
|
self->pointerIdDict = [NSMutableDictionary dictionary];
|
||||||
self->pointerIdPool = [NSMutableSet set];
|
self->pointerIdPool = [NSMutableSet set];
|
||||||
|
self->trackPointEnabled = settings.touchPointTracking;
|
||||||
|
self->trackPointPool = [NSMutableArray array];
|
||||||
for (uint8_t i = 0; i <= 10; i++) { //ipadOS supports upto 11 finger touches
|
for (uint8_t i = 0; i <= 10; i++) { //ipadOS supports upto 11 finger touches
|
||||||
[self->pointerIdPool addObject:@(i)];
|
[self->pointerIdPool addObject:@(i)];
|
||||||
|
[self->trackPointPool addObject:[GraphicUtils makeTouchTrackpointIn:streamView]];
|
||||||
}
|
}
|
||||||
|
|
||||||
self->activePointerIds = [NSMutableSet set];
|
self->activePointerIds = [NSMutableSet set];
|
||||||
self->blacklistedTouches = [NSMutableSet set];
|
self->blacklistedTouches = [NSMutableSet set];
|
||||||
|
|
||||||
@@ -85,13 +92,13 @@
|
|||||||
|
|
||||||
self->pointerObjDict = [NSMutableDictionary dictionary];
|
self->pointerObjDict = [NSMutableDictionary dictionary];
|
||||||
|
|
||||||
EDGE_TOLERANCE = 10.0;
|
_edgeTolerance = settings.edgeSlidingSensitivity.floatValue;
|
||||||
slideGestureVerticalThreshold = CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.4;
|
slideGestureVerticalThreshold = CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.4;
|
||||||
screenWidthWithThreshold = CGRectGetWidth([[UIScreen mainScreen] bounds]) - EDGE_TOLERANCE;
|
screenWidthWithThreshold = CGRectGetWidth([[UIScreen mainScreen] bounds]) - _edgeTolerance;
|
||||||
|
|
||||||
self->pointerVelocityDividerLocationByPoints = self->streamView.bounds.size.width * settings.pointerVelocityModeDivider.floatValue;
|
self->pointerVelocityDividerLocationByPoints = self->streamView.bounds.size.width * profile.pointerVelocityModeDivider;
|
||||||
|
|
||||||
[NativeTouchPointer initContextWithView:self->streamView andSettings:settings];
|
[NativeTouchPointer initContextWithView:self->streamView profile:profile];
|
||||||
//_touchesCapturedByOnScreenButtons = [[NSMutableSet alloc] init];
|
//_touchesCapturedByOnScreenButtons = [[NSMutableSet alloc] init];
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
@@ -164,7 +171,7 @@
|
|||||||
|
|
||||||
//check if touch point is spawned on the left or right upper half screen edges, event to remote PC. this is for better handling in-stream slide gesture
|
//check if touch point is spawned on the left or right upper half screen edges, event to remote PC. this is for better handling in-stream slide gesture
|
||||||
CGPoint initialPoint = [touch locationInView:self->streamView];
|
CGPoint initialPoint = [touch locationInView:self->streamView];
|
||||||
if(initialPoint.y < slideGestureVerticalThreshold && (initialPoint.x < EDGE_TOLERANCE || initialPoint.x > screenWidthWithThreshold)) {
|
if(initialPoint.y < slideGestureVerticalThreshold && (initialPoint.x < _edgeTolerance || initialPoint.x > screenWidthWithThreshold)) {
|
||||||
[blacklistedTouches addObject:touchAddrObj];
|
[blacklistedTouches addObject:touchAddrObj];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -187,10 +194,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
- (void)sendTouchEvent:(UITouch*)touch withTouchtype:(uint8_t)touchType{
|
- (void)sendTouchEvent:(UITouch*)touch withTouchtype:(uint8_t)touchType withEvent:(UIEvent *)event{
|
||||||
//if(touchPointSpawnedAtUpperScreenEdge && touchType != LI_TOUCH_EVENT_UP) return; // we're done here. this touch event will not be sent to the remote PC. and this must be checked after coord selector finishes populating new relative coords, or the app will crash
|
//if(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([blacklistedTouches containsObject:@((uintptr_t)touch)]) return;
|
||||||
|
|
||||||
|
if(singleTouchDisabled
|
||||||
|
&& [UITouchUtil touchesIn:streamView from:event].count != 2
|
||||||
|
&& touchType != LI_TOUCH_EVENT_UP) return;
|
||||||
|
|
||||||
|
if(PencilHandler.pencilPausesNativeTouch
|
||||||
|
&& PencilHandler.isDrawing
|
||||||
|
&& touchType != LI_TOUCH_EVENT_UP) return;
|
||||||
|
|
||||||
CGPoint targetCoords;
|
CGPoint targetCoords;
|
||||||
//NSLog(@"selecting coords: %d", touch.phase == UITouchPhaseMoved);
|
//NSLog(@"selecting coords: %d", touch.phase == UITouchPhaseMoved);
|
||||||
// NSLog(@"excluded count: %d", (uint32_t)[excludedPointerIds count]);
|
// NSLog(@"excluded count: %d", (uint32_t)[excludedPointerIds count]);
|
||||||
@@ -203,85 +220,95 @@
|
|||||||
CGFloat normalizedY = location.y / videoSize.height;
|
CGFloat normalizedY = location.y / videoSize.height;
|
||||||
uint8_t pointerId = [self retrievePointerIdFromDict:touch];
|
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_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);
|
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]]);
|
}else LiSendTouchEvent(touchType, pointerId, normalizedX, normalizedY,(touch.force / touch.maximumPossibleForce) / sin(touch.altitudeAngle),0.0f, 0.0f,[self getRotationFromAzimuthAngle:[touch azimuthAngleInView:streamView]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
|
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||||
if (asyncNativeTouch) dispatch_async(dispatch_get_global_queue(touchDownQos, 0), ^{
|
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
||||||
for (UITouch* touch in touches){
|
for (UITouch* touch in touches){
|
||||||
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
|
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
|
||||||
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]) continue;
|
if([OnScreenControls.touchesCapturedByOnScreenControls containsObject:touch]) continue;
|
||||||
[self handleTouchDown:touch]; //generate & populate pointerId
|
[self handleTouchDown:touch]; //generate & populate pointerId
|
||||||
if(self->activateCoordSelector) [self populatePointerObjIntoDict:touch];
|
if(self->activateCoordSelector) [self populatePointerObjIntoDict:touch];
|
||||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_DOWN];
|
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_DOWN withEvent:event];
|
||||||
|
}
|
||||||
|
|
||||||
|
if(self->trackPointEnabled){
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
[CATransaction begin];
|
||||||
|
[CATransaction setDisableActions:YES];
|
||||||
|
for (UITouch* touch in touches){
|
||||||
|
uint8_t pointerId = [self retrievePointerIdFromDict:touch];
|
||||||
|
CAShapeLayer* trackPoint = self->trackPointPool[pointerId];
|
||||||
|
trackPoint.position = [touch locationInView:self->streamView];
|
||||||
|
trackPoint.hidden = false;
|
||||||
|
}
|
||||||
|
[CATransaction commit];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
else{
|
|
||||||
for (UITouch* touch in touches){
|
|
||||||
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
|
|
||||||
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]) continue;
|
|
||||||
[self handleTouchDown:touch]; //generate & populate pointerId
|
|
||||||
if(self->activateCoordSelector) [self populatePointerObjIntoDict:touch];
|
|
||||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_DOWN];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
|
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||||
if (asyncNativeTouch) dispatch_async(dispatch_get_global_queue(touchMoveQos, 0), ^{
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, moveEventIntervalNSec), dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
||||||
for (UITouch* touch in touches){
|
for (UITouch* touch in touches){
|
||||||
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
|
if([OnScreenControls.touchesCapturedByOnScreenControls containsObject:touch]) continue;
|
||||||
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]) continue;
|
|
||||||
if(self->activateCoordSelector) [self updatePointerObjInDict:touch];
|
if(self->activateCoordSelector) [self updatePointerObjInDict:touch];
|
||||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_MOVE];
|
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_MOVE withEvent:event];
|
||||||
[[self getPointerObjFromDict:touch] doesNeedResetCoords]; // execute the judging of doesReachBoundary for current pointer instance. (happens after the event is sent to Sunshine service)
|
[[self getPointerObjFromDict:touch] doesNeedResetCoords];
|
||||||
usleep(self->touchMoveEventIntervalUs);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
else {
|
|
||||||
for (UITouch* touch in touches){
|
if(self->trackPointEnabled){
|
||||||
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]) continue;
|
[CATransaction begin];
|
||||||
if(self->activateCoordSelector) [self updatePointerObjInDict:touch];
|
[CATransaction setDisableActions:YES];
|
||||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_MOVE];
|
for (UITouch* touch in touches){
|
||||||
[[self getPointerObjFromDict:touch] doesNeedResetCoords]; // execute the judging of doesReachBoundary for current pointer instance. (happens after the event is sent to Sunshine service)
|
uint8_t pointerId = [self retrievePointerIdFromDict:touch];
|
||||||
// usleep(self->touchMoveEventIntervalUs);
|
CAShapeLayer* trackPoint = self->trackPointPool[pointerId];
|
||||||
}
|
trackPoint.position = [touch locationInView:self->streamView];
|
||||||
|
}
|
||||||
|
[CATransaction commit];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
|
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||||
if(asyncNativeTouch) dispatch_async(dispatch_get_global_queue(touchEndQos, 0), ^{
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, moveEventIntervalNSec), dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
||||||
for (UITouch* touch in touches){
|
for (UITouch* touch in touches){
|
||||||
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
|
if([OnScreenControls.touchesCapturedByOnScreenControls containsObject:touch]){
|
||||||
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]) continue;
|
[OnScreenControls.touchesCapturedByOnScreenControls removeObject:touch];
|
||||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_UP]; //send touch event before remove pointerId
|
continue;
|
||||||
[self removePointerId:touch]; //then remove pointerId
|
}
|
||||||
|
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_UP withEvent:event]; //send touch event before remove pointerId
|
||||||
|
|
||||||
|
if(self->trackPointEnabled){
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
uint8_t pointerId = [self retrievePointerIdFromDict:touch];
|
||||||
|
CAShapeLayer* trackPoint = self->trackPointPool[pointerId];
|
||||||
|
[CATransaction begin];
|
||||||
|
[CATransaction setDisableActions:YES];
|
||||||
|
trackPoint.hidden = true;
|
||||||
|
[CATransaction commit];
|
||||||
|
[self removePointerId:touch];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else [self removePointerId:touch];
|
||||||
|
|
||||||
if(self->activateCoordSelector) [self removePointerObjFromDict:touch];
|
if(self->activateCoordSelector) [self removePointerObjFromDict:touch];
|
||||||
[self->blacklistedTouches removeObject:@((uintptr_t)touch)];
|
[self->blacklistedTouches removeObject:@((uintptr_t)touch)];
|
||||||
}
|
}
|
||||||
//if(self->touchPointSpawnedAtUpperScreenEdge && [[event allTouches] count] == [touches count])
|
|
||||||
});
|
});
|
||||||
else{
|
|
||||||
for (UITouch* touch in touches){
|
|
||||||
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
|
|
||||||
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]) continue;
|
|
||||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_UP]; //send touch event before remove pointerId
|
|
||||||
[self removePointerId:touch]; //then remove pointerId
|
|
||||||
if(self->activateCoordSelector) [self removePointerObjFromDict:touch];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
|
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||||
[self touchesEnded:touches withEvent:event];
|
[self touchesEnded:touches withEvent:event];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
- (void)populatePointerObjIntoDict:(UITouch*)touch{
|
- (void)populatePointerObjIntoDict:(UITouch*)touch{
|
||||||
NativeTouchPointer* pointer = [[NativeTouchPointer alloc] initWithTouch:touch];
|
NativeTouchPointer* pointer = [[NativeTouchPointer alloc] initWithTouch:touch];
|
||||||
pointer.useRelativeCoords = pointer.initialPoint.x > pointerVelocityDividerLocationByPoints;
|
pointer.useRelativeCoords = pointer.initialPoint.x > pointerVelocityDividerLocationByPoints;
|
||||||
@@ -310,4 +337,22 @@
|
|||||||
return pointer.useRelativeCoords ? pointer.latestRelativePoint : pointer.latestPoint;
|
return pointer.useRelativeCoords ? pointer.latestRelativePoint : pointer.latestPoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)setAllowSingleTouchEnabled:(BOOL)enabled{
|
||||||
|
singleTouchDisabled = !enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)dealloc{
|
||||||
|
NSLog(@"dealloc nativeTouchHanlder %f", CACurrentMediaTime());
|
||||||
|
pointerIdDict = nil;
|
||||||
|
activePointerIds = nil;
|
||||||
|
pointerIdPool= nil;
|
||||||
|
unassignedPointerIds = nil;
|
||||||
|
blacklistedTouches = nil;
|
||||||
|
for(CAShapeLayer* layer in trackPointPool){
|
||||||
|
[layer removeFromSuperlayer];
|
||||||
|
}
|
||||||
|
trackPointPool = nil;
|
||||||
|
pointerObjDict = nil;
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
@property (nonatomic, assign) bool useRelativeCoords;
|
@property (nonatomic, assign) bool useRelativeCoords;
|
||||||
|
|
||||||
|
|
||||||
+ (void)initContextWithView:(StreamView*)view andSettings:(TemporarySettings*)settings;
|
+ (void)initContextWithView:(StreamView*)view profile:(OSCProfile*)profile;
|
||||||
|
+ (void)cleanUpContext;
|
||||||
|
|
||||||
- (bool)doesNeedResetCoords;
|
- (bool)doesNeedResetCoords;
|
||||||
- (void)updatePointerCoords:(UITouch *)touch;
|
- (void)updatePointerCoords:(UITouch *)touch;
|
||||||
|
|||||||
@@ -53,14 +53,18 @@ static StreamView* streamView;
|
|||||||
_latestRelativePoint.y = _previousRelativePoint.y + pointerVelocityFactor * (_latestPoint.y - _previousPoint.y);
|
_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;
|
streamView = view;
|
||||||
streamViewWidth = streamView.frame.size.width;
|
streamViewWidth = streamView.frame.size.width;
|
||||||
fixedResetCoordX = streamViewWidth * 0.3;
|
fixedResetCoordX = streamViewWidth * 0.3;
|
||||||
streamViewHeight = streamView.frame.size.height;
|
streamViewHeight = streamView.frame.size.height;
|
||||||
fixedResetCoordY = streamViewHeight * 0.4;
|
fixedResetCoordY = streamViewHeight * 0.4;
|
||||||
pointerVelocityFactor = settings.touchPointerVelocityFactor.floatValue;
|
pointerVelocityFactor = profile.touchPointerVelocityFactor;
|
||||||
NSLog(@"stream wdith %f, stream height %f", streamViewWidth, streamViewHeight);
|
NSLog(@"stream wdith %f, stream height %f", streamViewWidth, streamViewHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
+ (void)cleanUpContext {
|
||||||
|
streamView = nil;
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -17,12 +17,7 @@
|
|||||||
|
|
||||||
@class ControllerSupport;
|
@class ControllerSupport;
|
||||||
@class StreamConfiguration;
|
@class StreamConfiguration;
|
||||||
|
@class LayoutOnScreenControls;
|
||||||
// sending self as an instance to OnScreenWidgetView or other related classes
|
|
||||||
@protocol InstanceProviderDelegate <NSObject>
|
|
||||||
- (void)getOnScreenControlsInstance:(id)sender; // Renamed delegate method
|
|
||||||
@end
|
|
||||||
|
|
||||||
|
|
||||||
static const float D_PAD_DIST = 10;
|
static const float D_PAD_DIST = 10;
|
||||||
static const float BUTTON_DIST = 20;
|
static const float BUTTON_DIST = 20;
|
||||||
@@ -36,13 +31,13 @@ static const float BUTTON_DIST = 20;
|
|||||||
CGFloat _dPadSizeFactor;
|
CGFloat _dPadSizeFactor;
|
||||||
}
|
}
|
||||||
|
|
||||||
+ (NSMutableDictionary *)layerVibrationStyleDic;
|
+ (NSMutableDictionary *_Nullable)layerVibrationStyleDic;
|
||||||
|
|
||||||
typedef NS_ENUM(NSInteger, OnScreenControlsLevel) {
|
typedef NS_ENUM(NSInteger, OnScreenControlsLevel) {
|
||||||
OnScreenControlsLevelOff,
|
OnScreenControlsLevelOff,
|
||||||
|
OnScreenControlsLevelCustom,
|
||||||
OnScreenControlsLevelSimple,
|
OnScreenControlsLevelSimple,
|
||||||
OnScreenControlsLevelFull,
|
OnScreenControlsLevelFull,
|
||||||
OnScreenControlsLevelCustom,
|
|
||||||
OnScreenControlsLevelAuto, // move it here instead of delete , to ensure integrity of the codes
|
OnScreenControlsLevelAuto, // move it here instead of delete , to ensure integrity of the codes
|
||||||
|
|
||||||
// Internal levels selected by ControllerSupport
|
// Internal levels selected by ControllerSupport
|
||||||
@@ -52,11 +47,6 @@ typedef NS_ENUM(NSInteger, OnScreenControlsLevel) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@property (nonatomic, weak) id<InstanceProviderDelegate> delegate; // Delegate property
|
|
||||||
- (void)sendInstance;
|
|
||||||
|
|
||||||
// @property (nonatomic, assign) CustomTapGestureRecognizer* mouseRightClickTapRecognizer; // this object will be passed to onscreencontrols class for areVirtualControllerTaps flag setting
|
// @property (nonatomic, assign) CustomTapGestureRecognizer* mouseRightClickTapRecognizer; // this object will be passed to onscreencontrols class for areVirtualControllerTaps flag setting
|
||||||
@property (nonatomic, assign) bool isLayingOut;
|
@property (nonatomic, assign) bool isLayingOut;
|
||||||
//@property (nonatomic) NSMutableSet<UITouch* >* touchesCapturedByOnScreenButtons;
|
//@property (nonatomic) NSMutableSet<UITouch* >* touchesCapturedByOnScreenButtons;
|
||||||
@@ -105,12 +95,15 @@ typedef NS_ENUM(NSInteger, OnScreenControlsLevel) {
|
|||||||
|
|
||||||
@property OnScreenControlsLevel _level;
|
@property OnScreenControlsLevel _level;
|
||||||
|
|
||||||
@property NSMutableArray *OSCButtonLayers;
|
@property NSMutableSet *OSCButtonLayerPool;
|
||||||
@property NSMutableDictionary* layerVibrationStyleDic;
|
@property NSMutableDictionary* layerVibrationStyleDic;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
+ (NSMutableSet* )touchAddrsCapturedByOnScreenControls;
|
+ (NSMutableSet* )touchesCapturedByOnScreenControls;
|
||||||
|
+ (OnScreenControls* )shared;
|
||||||
|
+ (void)setShared:(OnScreenControls*)instance;
|
||||||
|
|
||||||
- (void) sendRightStickTouchPadEvent:(CGFloat) stickX : (CGFloat) stickY;
|
- (void) sendRightStickTouchPadEvent:(CGFloat) stickX : (CGFloat) stickY;
|
||||||
- (void) clearRightStickTouchPadFlag;
|
- (void) clearRightStickTouchPadFlag;
|
||||||
- (void) sendLeftStickTouchPadEvent:(CGFloat) stickX : (CGFloat) stickY;
|
- (void) sendLeftStickTouchPadEvent:(CGFloat) stickX : (CGFloat) stickY;
|
||||||
@@ -125,18 +118,19 @@ typedef NS_ENUM(NSInteger, OnScreenControlsLevel) {
|
|||||||
- (BOOL) handleTouchUpEvent:(NSSet*)touches;
|
- (BOOL) handleTouchUpEvent:(NSSet*)touches;
|
||||||
- (BOOL) handleTouchMovedEvent:(NSSet*)touches;
|
- (BOOL) handleTouchMovedEvent:(NSSet*)touches;
|
||||||
- (void) setLevel:(OnScreenControlsLevel)level;
|
- (void) setLevel:(OnScreenControlsLevel)level;
|
||||||
- (void) show;
|
- (void) showLegacyWidgetsWith:(OSCProfile* )profile;
|
||||||
|
- (void) regenLayerPool;
|
||||||
- (void) setupComplexControls;
|
- (void) setupComplexControls;
|
||||||
- (void) drawButtons;
|
- (void) drawButtons:(OSCProfile* _Nullable)profile;
|
||||||
- (void) drawBumpers;
|
- (void) drawBumpers;
|
||||||
- (void) updateControls;
|
- (void) updateLegacyWidgetsWith:(OSCProfile* _Nullable )profile;
|
||||||
- (OnScreenControlsLevel) getLevel;
|
- (OnScreenControlsLevel) getLevel;
|
||||||
- (void) setDPadCenter;
|
- (void) setDPadCenter:(OSCProfile *_Nonnull)profile;;
|
||||||
- (void) setAnalogStickPositions;
|
- (void) setAnalogStickPositions:(OSCProfile *_Nonnull)profile;
|
||||||
- (void) positionAndResizeSingleControllerLayers;
|
- (void) positionAndResizeSingleControllerLayers:(OSCProfile *_Nonnull)profile;
|
||||||
- (void) resizeControllerLayerWith:(CALayer*)layer and:(CGFloat)sizeFactor;
|
- (void) resizeControllerLayerWith:(CALayer*_Nonnull)layer and:(CGFloat)sizeFactor;
|
||||||
- (void) adjustControllerLayerOpacityWith:(CALayer*)layer and:(CGFloat)alpha;
|
- (void) adjustControllerLayerOpacityWith:(CALayer*_Nonnull)layer and:(CGFloat)alpha;
|
||||||
+ (CGFloat) getControllerLayerSizeFactor:(CALayer*)layer;
|
+ (CGFloat) getControllerLayerSizeFactor:(CALayer*_Nonnull)layer;
|
||||||
- (CGFloat) getControllerLayerOpacity:(CALayer*)layer;
|
- (CGFloat) getControllerLayerOpacity:(CALayer*_Nonnull)layer;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -0,0 +1,731 @@
|
|||||||
|
//
|
||||||
|
// PencilHandler.swift
|
||||||
|
// VoidLink
|
||||||
|
//
|
||||||
|
// Created by True砖家 on 2025/11/17.
|
||||||
|
// Copyright © 2025 True砖家 on Bilibili. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
@objc class PencilHandler: UIResponder, UIPencilInteractionDelegate {
|
||||||
|
@objc static var shared: PencilHandler?
|
||||||
|
|
||||||
|
/// 用来处理所有 touch 事件的宿主视图
|
||||||
|
weak var streamView: UIView?
|
||||||
|
// private var tickTimer: SafeTimer
|
||||||
|
private var pencilInteraction: Any?
|
||||||
|
@objc var streamAspectRatio: Float
|
||||||
|
private var tickInterval: TimeInterval
|
||||||
|
private var manualTick: Bool
|
||||||
|
private var pencilTickEnabled: Bool
|
||||||
|
private var pressureCurveEnabled: Bool = false
|
||||||
|
@objc private(set) var pencilTipOffset: CGPoint = .zero
|
||||||
|
private var manualHoverFlag: Bool = false
|
||||||
|
@objc static var hoverSupported: Bool = false
|
||||||
|
// @objc static private(set) var autoHoverTermination: Bool = false
|
||||||
|
@objc static private(set) var pencilAndHoverMode: PencilAndHoverMode = .pencilOnly
|
||||||
|
private(set) var pencilProEnabled: Bool = false
|
||||||
|
private var isFirstMove: Bool = false
|
||||||
|
private var strokeSampleIndex: Int32 = 0
|
||||||
|
private var initialMoveEventIndexLimit: Int64
|
||||||
|
private var touchBeganForce: Float = 0
|
||||||
|
@objc static private(set) var isDrawing: Bool = false
|
||||||
|
@objc static private(set) var pencilPausesNativeTouch: Bool = false
|
||||||
|
|
||||||
|
@objc var disableTilt: Bool = false
|
||||||
|
|
||||||
|
// static private let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero)
|
||||||
|
static private var selectedProfile:OSCProfile?
|
||||||
|
|
||||||
|
private var strokeLUT = PressureCurveLUT(curve: PressureCurve())
|
||||||
|
// private var initialTouchLUT = PressureCurveLUT(curve: PressureCurve())
|
||||||
|
private var phase1StrokeSampleIndexEnd:Int32 = 0
|
||||||
|
private var phase2StrokeSampleIndexEnd:Int32 = 0
|
||||||
|
private var phase2EqualizationStrength:Float = 0
|
||||||
|
|
||||||
|
enum StrokePhase: UInt8, CaseIterable {
|
||||||
|
case phase1
|
||||||
|
case phase2
|
||||||
|
case phase3
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getStrokePhase(sampleIndex:Int32) -> StrokePhase {
|
||||||
|
if sampleIndex > phase2StrokeSampleIndexEnd {
|
||||||
|
return .phase3
|
||||||
|
}
|
||||||
|
if sampleIndex > phase1StrokeSampleIndexEnd {
|
||||||
|
return .phase2
|
||||||
|
}
|
||||||
|
return .phase1
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc init(streamView: UIView, settings: TemporarySettings) {
|
||||||
|
self.streamView = streamView
|
||||||
|
streamAspectRatio = settings.width.floatValue/settings.height.floatValue
|
||||||
|
tickInterval = TimeInterval(settings.pencilTickIntervalUs.floatValue/1000000)
|
||||||
|
manualTick = settings.pencilTickMode.intValue == PencilTickMode.ManualTick.rawValue
|
||||||
|
pencilTickEnabled = settings.pencilTickMode.intValue != PencilTickMode.PencilTickDisabled.rawValue
|
||||||
|
pencilTipOffset = CGPoint(x: CGFloat(settings.pencilTipOffsetX?.floatValue ?? 0),
|
||||||
|
y: CGFloat(settings.pencilTipOffsetY?.floatValue ?? 0))
|
||||||
|
// initialMoveEventIndexLimit = UIScreen.main.maximumFramesPerSecond > 60 ? 4 : 2
|
||||||
|
initialMoveEventIndexLimit = 0
|
||||||
|
super.init()
|
||||||
|
setupPressureLUT()
|
||||||
|
PencilHandler.shared = self
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public func setupPressureLUT(profile:OSCProfile? = nil){
|
||||||
|
var selectedProfile:OSCProfile
|
||||||
|
if profile == nil {
|
||||||
|
let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero)
|
||||||
|
selectedProfile = oscProfileMan.getSelectedProfile()
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
selectedProfile = profile!
|
||||||
|
}
|
||||||
|
|
||||||
|
doubleTapShorcuts.removeAll()
|
||||||
|
if selectedProfile.doubleTapShorcutEnabled {
|
||||||
|
if selectedProfile.eraserShortcut != "" {
|
||||||
|
doubleTapShorcuts.append(selectedProfile.eraserShortcut)
|
||||||
|
}
|
||||||
|
if selectedProfile.brushShortcut != "" {
|
||||||
|
doubleTapShorcuts.append(selectedProfile.brushShortcut)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PencilHandler.squeezeStartShortcut = ""
|
||||||
|
PencilHandler.squeezeEndShortcut = ""
|
||||||
|
if selectedProfile.squeezeShorcutEnabled {
|
||||||
|
PencilHandler.squeezeStartShortcut = selectedProfile.squeezeStartShortcut
|
||||||
|
PencilHandler.squeezeEndShortcut = selectedProfile.squeezeEndShortcut
|
||||||
|
print("squeezeShorcutEnabled \(CACurrentMediaTime())")
|
||||||
|
}
|
||||||
|
|
||||||
|
// PencilHandler.autoHoverTermination = selectedProfile.autoPencilHoverTermination
|
||||||
|
PencilHandler.pencilAndHoverMode = selectedProfile.pencilAndHoverMode
|
||||||
|
|
||||||
|
pressureCurveEnabled = selectedProfile.pressureCurveEnabled
|
||||||
|
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
IAPManager.checkPurchaseInfo(.PencilProPack) { info in
|
||||||
|
self.pressureCurveEnabled = self.pressureCurveEnabled && info.valid
|
||||||
|
self.pencilTickEnabled = self.pencilTickEnabled && info.valid
|
||||||
|
self.pencilTipOffset = info.valid ? self.pencilTipOffset : .zero
|
||||||
|
self.pencilProEnabled = info.valid
|
||||||
|
PencilHandler.pencilPausesNativeTouch = selectedProfile.pencilPausesNativeTouch && info.valid
|
||||||
|
if info.valid {
|
||||||
|
self.setupPencilInteraction(view: self.streamView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let strokePressureCurvePoints = PressureCurve.importCurvePoints(selectedProfile.pressureCurvePoints)
|
||||||
|
let strokePressureCurve = PressureCurve()
|
||||||
|
strokePressureCurve.polylinePoints = strokePressureCurvePoints
|
||||||
|
strokePressureCurve.buildCurveSegments()
|
||||||
|
|
||||||
|
// let initialTouchPressureCurvePoints = PressureCurve.importCurvePoints(selectedProfile.initialTouchPressureCurvePoints)
|
||||||
|
// let initialTouchPressureCurve = PressureCurve()
|
||||||
|
// initialTouchPressureCurve.polylinePoints = initialTouchPressureCurvePoints
|
||||||
|
// initialTouchPressureCurve.buildCurveSegments()
|
||||||
|
|
||||||
|
phase1StrokeSampleIndexEnd = selectedProfile.phase1StrokeSampleIndexEnd
|
||||||
|
phase2StrokeSampleIndexEnd = selectedProfile.phase2StrokeSampleIndexEnd
|
||||||
|
phase2EqualizationStrength = Float(selectedProfile.strokeEqualizationStrength)
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now()) {
|
||||||
|
self.strokeLUT = PressureCurveLUT(curve: strokePressureCurve)
|
||||||
|
// self.initialTouchLUT = PressureCurveLUT(curve: initialTouchPressureCurve)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Touch Events
|
||||||
|
|
||||||
|
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
|
guard let event = event else { return }
|
||||||
|
previousForce = 0
|
||||||
|
previousTargetForce = 0
|
||||||
|
PencilHandler.isDrawing = true
|
||||||
|
// isFirstMove = true
|
||||||
|
strokeSampleIndex = 0
|
||||||
|
|
||||||
|
guard PencilHandler.pencilAndHoverMode == .pencilOnly || PencilHandler.pencilAndHoverMode == .hoverDisabled else {
|
||||||
|
handleNonPencilModes(touches)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for touch in touches {
|
||||||
|
let coalesced = event.coalescedTouches(for: touch) ?? []
|
||||||
|
_ = self.sendStylusEvent(touchBatch: pencilTickEnabled ? coalesced : [touch])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
|
guard let event = event else { return }
|
||||||
|
|
||||||
|
guard PencilHandler.pencilAndHoverMode == .pencilOnly || PencilHandler.pencilAndHoverMode == .hoverDisabled else {
|
||||||
|
handleNonPencilModes(touches)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for touch in touches {
|
||||||
|
let coalesced = event.coalescedTouches(for: touch) ?? []
|
||||||
|
_ = self.sendStylusEvent(touchBatch: pencilTickEnabled ? coalesced : [touch])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
|
guard let event = event else { return }
|
||||||
|
|
||||||
|
guard PencilHandler.pencilAndHoverMode == .pencilOnly || PencilHandler.pencilAndHoverMode == .hoverDisabled else {
|
||||||
|
handleNonPencilModes(touches)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for touch in touches {
|
||||||
|
let coalesced = event.coalescedTouches(for: touch) ?? []
|
||||||
|
_ = self.sendStylusEvent(touchBatch: pencilTickEnabled ? coalesced : [touch])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
|
self.touchesEnded(touches, with: event)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleNonPencilModes(_ touches: Set<UITouch>) {
|
||||||
|
guard let touch = touches.first else { return }
|
||||||
|
let location = touch.location(in: streamView)
|
||||||
|
let normalizedLocation = self.getNormalizedLocation(point: location)
|
||||||
|
if let streamView = streamView as? StreamView {
|
||||||
|
switch touch.phase {
|
||||||
|
case .began where PencilHandler.pencilAndHoverMode == .pencilToMouse:
|
||||||
|
streamView.updateCursorLocation(location, isMouse: false)
|
||||||
|
LiSendMouseButtonEvent(CChar(BUTTON_ACTION_PRESS), BUTTON_LEFT)
|
||||||
|
case .began where PencilHandler.pencilAndHoverMode == .pencilToTouch:
|
||||||
|
LiSendTouchEvent(UInt8(LI_TOUCH_EVENT_DOWN), 666666, Float(normalizedLocation.x), Float(normalizedLocation.y), Float((touch.force/touch.maximumPossibleForce)/sin(touch.altitudeAngle)), 0, 0, getRotation(fromAzimuthAngle: Float(touch.azimuthAngle(in: streamView))))
|
||||||
|
case .moved where PencilHandler.pencilAndHoverMode == .pencilToMouse:
|
||||||
|
streamView.updateCursorLocation(location, isMouse: false)
|
||||||
|
case .moved where PencilHandler.pencilAndHoverMode == .pencilToTouch:
|
||||||
|
LiSendTouchEvent(UInt8(LI_TOUCH_EVENT_MOVE), 666666, Float(normalizedLocation.x), Float(normalizedLocation.y), Float((touch.force/touch.maximumPossibleForce)/sin(touch.altitudeAngle)), 0, 0, getRotation(fromAzimuthAngle: Float(touch.azimuthAngle(in: streamView))))
|
||||||
|
case .ended where PencilHandler.pencilAndHoverMode == .pencilToMouse,
|
||||||
|
.cancelled where PencilHandler.pencilAndHoverMode == .pencilToMouse:
|
||||||
|
streamView.updateCursorLocation(location, isMouse: false)
|
||||||
|
LiSendMouseButtonEvent(CChar(BUTTON_ACTION_RELEASE), BUTTON_LEFT)
|
||||||
|
case .ended where PencilHandler.pencilAndHoverMode == .pencilToTouch,
|
||||||
|
.cancelled where PencilHandler.pencilAndHoverMode == .pencilToTouch:
|
||||||
|
LiSendTouchEvent(UInt8(LI_TOUCH_EVENT_UP), 666666, Float(normalizedLocation.x), Float(normalizedLocation.y), 0, 0, 0, 0)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Core Logic
|
||||||
|
|
||||||
|
func getVideoAreaSize()-> CGSize {
|
||||||
|
guard let streamView = streamView else { return CGSize(width: 0, height: 0) }
|
||||||
|
if streamView.bounds.size.width > streamView.bounds.size.height * CGFloat(streamAspectRatio) {
|
||||||
|
return CGSizeMake(streamView.bounds.size.height * CGFloat(streamAspectRatio), streamView.bounds.size.height);
|
||||||
|
} else {
|
||||||
|
return CGSizeMake(streamView.bounds.size.width, streamView.bounds.size.width / CGFloat(streamAspectRatio));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func adjustCoordinatesForVideoArea(point: CGPoint) -> CGPoint {
|
||||||
|
guard let streamView = streamView else { return CGPoint(x: 0, y: 0) }
|
||||||
|
|
||||||
|
var x = point.x - streamView.bounds.origin.x
|
||||||
|
var y = point.y - streamView.bounds.origin.y
|
||||||
|
|
||||||
|
if x < streamView.bounds.width / 2 {
|
||||||
|
x -= 1
|
||||||
|
} else {
|
||||||
|
x += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if y < streamView.bounds.height / 2 {
|
||||||
|
y -= 1
|
||||||
|
} else {
|
||||||
|
y += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
let videoSize = getVideoAreaSize() // 返回 CGSize
|
||||||
|
let videoOrigin = CGPoint(
|
||||||
|
x: streamView.bounds.width / 2 - videoSize.width / 2,
|
||||||
|
y: streamView.bounds.height / 2 - videoSize.height / 2
|
||||||
|
)
|
||||||
|
|
||||||
|
let confinedX = min(max(x, videoOrigin.x), videoOrigin.x + videoSize.width) - videoOrigin.x
|
||||||
|
let confinedY = min(max(y, videoOrigin.y), videoOrigin.y + videoSize.height) - videoOrigin.y
|
||||||
|
|
||||||
|
return CGPoint(x: confinedX, y: confinedY)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func getRotation(fromAzimuthAngle azimuthAngle: Float) -> UInt16 {
|
||||||
|
var rotationAngle = (azimuthAngle - .pi / 2) * (180.0 / .pi) // 弧度转角度
|
||||||
|
if rotationAngle < 0 {
|
||||||
|
rotationAngle += 360
|
||||||
|
}
|
||||||
|
return UInt16(rotationAngle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTilt(fromAltitudeAngle altitudeAngle: Float) -> UInt8 {
|
||||||
|
let altitudeDegs = abs(Int16(altitudeAngle * (180.0 / .pi)))
|
||||||
|
let tilt = disableTilt ? 0 : UInt8(90 - min(90, Int(altitudeDegs)))
|
||||||
|
return tilt
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getNormalizedLocation(point: CGPoint) -> CGPoint {
|
||||||
|
let location = self.adjustCoordinatesForVideoArea(point: point)
|
||||||
|
let videoSize = self.getVideoAreaSize()
|
||||||
|
let normalizedLocation = CGPoint(x: location.x/videoSize.width, y: location.y/videoSize.height)
|
||||||
|
return normalizedLocation
|
||||||
|
}
|
||||||
|
|
||||||
|
var previousForce: Float = 0
|
||||||
|
var previousTargetForce: Float = 0
|
||||||
|
var phase2IndexCount: Int32 = 0
|
||||||
|
private func sendStylusEvent(touchBatch: [UITouch], with event: UIEvent? = nil) -> DispatchTime {
|
||||||
|
guard let streamView = streamView, touchBatch.count>0 else { return .now() }
|
||||||
|
|
||||||
|
var tickMoment: TimeInterval = 0
|
||||||
|
var previousTimeStamp: TimeInterval = 0
|
||||||
|
var delay:TimeInterval = 0
|
||||||
|
var dispatchMoment: DispatchTime = .now()
|
||||||
|
|
||||||
|
phase2IndexCount = phase2StrokeSampleIndexEnd - phase1StrokeSampleIndexEnd
|
||||||
|
|
||||||
|
for touch in touchBatch {
|
||||||
|
let point = touch.preciseLocation(in: streamView)
|
||||||
|
.applying(CGAffineTransform(translationX: pencilTipOffset.x, y: pencilTipOffset.y))
|
||||||
|
let azimuth = touch.azimuthAngle(in: streamView)
|
||||||
|
let altitude = touch.altitudeAngle
|
||||||
|
let normalizedLocation = self.getNormalizedLocation(point: point)
|
||||||
|
var force = Float(touch.force/touch.maximumPossibleForce)/sin(Float(altitude))
|
||||||
|
force = (self.pencilTickEnabled && force == 0) ? previousForce : force
|
||||||
|
|
||||||
|
self.strokeSampleIndex += 1
|
||||||
|
|
||||||
|
var targetForce:Float
|
||||||
|
var equalizationStep:Float = 0
|
||||||
|
var equalizedForce:Float = 0
|
||||||
|
if(phase2IndexCount>0){
|
||||||
|
equalizationStep = (phase2EqualizationStrength-1)/Float(phase2IndexCount-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let strokePhase = getStrokePhase(sampleIndex: self.strokeSampleIndex)
|
||||||
|
let forceMapping = pressureCurveEnabled ? self.strokeLUT.value(at: force) : force
|
||||||
|
switch strokePhase {
|
||||||
|
case .phase1:
|
||||||
|
targetForce = pencilTickEnabled ? 0 : forceMapping
|
||||||
|
case .phase2:
|
||||||
|
targetForce = forceMapping
|
||||||
|
targetForce = max(targetForce,previousTargetForce)
|
||||||
|
equalizedForce = targetForce*(phase2EqualizationStrength - equalizationStep*Float(strokeSampleIndex-phase1StrokeSampleIndexEnd-1))
|
||||||
|
case .phase3:
|
||||||
|
targetForce = forceMapping
|
||||||
|
}
|
||||||
|
targetForce = pencilTickEnabled ? targetForce : force
|
||||||
|
|
||||||
|
previousForce = force
|
||||||
|
previousTargetForce = targetForce
|
||||||
|
|
||||||
|
let eventType:UInt8
|
||||||
|
|
||||||
|
switch touch.phase {
|
||||||
|
case .began:
|
||||||
|
eventType = UInt8(manualHoverFlag ? LI_TOUCH_EVENT_HOVER : LI_TOUCH_EVENT_DOWN)
|
||||||
|
case .moved:
|
||||||
|
eventType = UInt8(manualHoverFlag ? LI_TOUCH_EVENT_HOVER : LI_TOUCH_EVENT_MOVE)
|
||||||
|
case .ended:
|
||||||
|
eventType = UInt8(manualHoverFlag ? LI_TOUCH_EVENT_HOVER_LEAVE : LI_TOUCH_EVENT_UP)
|
||||||
|
targetForce = 0
|
||||||
|
previousForce = 0
|
||||||
|
previousTargetForce = 0
|
||||||
|
case .cancelled:
|
||||||
|
eventType = UInt8(manualHoverFlag ? LI_TOUCH_EVENT_HOVER_LEAVE : LI_TOUCH_EVENT_UP)
|
||||||
|
targetForce = 0
|
||||||
|
previousForce = 0
|
||||||
|
previousTargetForce = 0
|
||||||
|
default:
|
||||||
|
eventType = UInt8(LI_TOUCH_EVENT_HOVER)
|
||||||
|
}
|
||||||
|
|
||||||
|
delay = manualTick ? 0.0086 : 0.0086
|
||||||
|
delay = eventType == UInt8(LI_TOUCH_EVENT_UP) ? delay : 0
|
||||||
|
|
||||||
|
if previousTimeStamp == 0 {
|
||||||
|
previousTimeStamp = touch.timestamp
|
||||||
|
tickMoment = 0
|
||||||
|
dispatchMoment = .now()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
tickMoment += (manualTick ? tickInterval : touch.timestamp - previousTimeStamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
let sendableForce = strokePhase == .phase2 ? equalizedForce : targetForce
|
||||||
|
|
||||||
|
DispatchQueue.global().asyncAfter(deadline: dispatchMoment + tickMoment + delay) {
|
||||||
|
/*
|
||||||
|
if PencilHandler.autoHoverEnabled, eventType == UInt8(LI_TOUCH_EVENT_DOWN) {
|
||||||
|
LiSendPenEvent(UInt8(LI_TOUCH_EVENT_HOVER_LEAVE), UInt8(LI_TOOL_TYPE_PEN), 0, Float(normalizedLocation.x), Float(normalizedLocation.y), 0, 0, 0, self.getRotation(fromAzimuthAngle: Float(azimuth)), self.getTilt(fromAltitudeAngle: Float(altitude)))
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
if strokePhase != .phase1 || !self.pencilTickEnabled {LiSendPenEvent(eventType, UInt8(LI_TOOL_TYPE_PEN), 0, Float(normalizedLocation.x), Float(normalizedLocation.y), sendableForce, 0, 0, self.getRotation(fromAzimuthAngle: Float(azimuth)), self.getTilt(fromAltitudeAngle: Float(altitude)))}
|
||||||
|
else {
|
||||||
|
LiSendPenEvent(UInt8(LI_TOUCH_EVENT_HOVER), UInt8(LI_TOOL_TYPE_PEN), 0, Float(normalizedLocation.x), Float(normalizedLocation.y), 0, 0, 0, self.getRotation(fromAzimuthAngle: Float(azimuth)), self.getTilt(fromAltitudeAngle: Float(altitude)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if eventType == UInt8(LI_TOUCH_EVENT_UP) {
|
||||||
|
PencilHandler.isDrawing = false
|
||||||
|
if PencilHandler.pencilAndHoverMode == .hoverDisabled || !PencilHandler.hoverSupported {
|
||||||
|
LiSendPenEvent(UInt8(LI_TOUCH_EVENT_HOVER), UInt8(LI_TOOL_TYPE_PEN), 0, Float(normalizedLocation.x), Float(normalizedLocation.y), 0, 0, 0, self.getRotation(fromAzimuthAngle: Float(azimuth)), self.getTilt(fromAltitudeAngle: Float(altitude)))
|
||||||
|
DispatchQueue.global().asyncAfter(deadline: .now() + 0.0086){
|
||||||
|
if !PencilHandler.isDrawing {
|
||||||
|
LiSendPenEvent(UInt8(LI_TOUCH_EVENT_HOVER_LEAVE), UInt8(LI_TOOL_TYPE_PEN), 0, Float(normalizedLocation.x), Float(normalizedLocation.y), 0, 0, 0, self.getRotation(fromAzimuthAngle: Float(azimuth)), self.getTilt(fromAltitudeAngle: Float(altitude)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dispatchMoment + tickMoment + delay
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public func switchPencilHover(){
|
||||||
|
manualHoverFlag = !manualHoverFlag
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public func updatePencilTipOffset(x: CGFloat, y: CGFloat) {
|
||||||
|
pencilTipOffset = CGPoint(x: x, y: y)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public func enablePencilHover(){
|
||||||
|
manualHoverFlag = true
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public func disablePencilHover(){
|
||||||
|
manualHoverFlag = false
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 12.1, *)
|
||||||
|
private func setupPencilInteraction(view:UIView?) {
|
||||||
|
let interaction = UIPencilInteraction()
|
||||||
|
guard let view else { return }
|
||||||
|
if pencilInteraction != nil {return}
|
||||||
|
interaction.delegate = self
|
||||||
|
view.addInteraction(interaction)
|
||||||
|
pencilInteraction = interaction
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 12.1, *)
|
||||||
|
func pencilInteractionDidTap(_ interaction: UIPencilInteraction) {
|
||||||
|
if !pencilProEnabled {return}
|
||||||
|
if doubleTapShorcuts.isEmpty {return}
|
||||||
|
let keyStrings = CommandManager.shared.extractAutoReleaseButtonStrings(from: doubleTapShorcuts[shortcutIndex])
|
||||||
|
print("pencilInteractionDidTap \(CACurrentMediaTime())")
|
||||||
|
CommandManager.shared.sendAutoReleaseComboCommand(cmdStrings: keyStrings, delay: 0.1)
|
||||||
|
shortcutIndex = (shortcutIndex + 1) % doubleTapShorcuts.count
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 17.5, *)
|
||||||
|
func pencilInteraction(
|
||||||
|
_ interaction: UIPencilInteraction,
|
||||||
|
didReceiveSqueeze squeeze: UIPencilInteraction.Squeeze
|
||||||
|
) {
|
||||||
|
if !pencilProEnabled {return}
|
||||||
|
if PencilHandler.squeezeStartShortcut == "", PencilHandler.squeezeEndShortcut == "" {return}
|
||||||
|
|
||||||
|
let keepPressedUntilRelease = PencilHandler.squeezeEndShortcut.uppercased() == "NULL"
|
||||||
|
let squeezePressKeyStrings = CommandManager.shared.extractAutoReleaseButtonStrings(from: PencilHandler.squeezeStartShortcut)
|
||||||
|
let squeezeReleaseKeyStrings = CommandManager.shared.extractAutoReleaseButtonStrings(from: PencilHandler.squeezeEndShortcut)
|
||||||
|
|
||||||
|
guard let squeezePressKeyStrings = squeezePressKeyStrings else {return}
|
||||||
|
guard squeezePressKeyStrings.count>0 else {return}
|
||||||
|
|
||||||
|
switch squeeze.phase {
|
||||||
|
case .began:
|
||||||
|
if keepPressedUntilRelease {CommandManager.shared.sendAutoReleaseComboCommand(cmdStrings: squeezePressKeyStrings, delay: 0.1, pressOnly: true)}
|
||||||
|
else {CommandManager.shared.sendAutoReleaseComboCommand(cmdStrings: squeezePressKeyStrings, delay: 0.1)}
|
||||||
|
break
|
||||||
|
case .ended, .cancelled:
|
||||||
|
if keepPressedUntilRelease {CommandManager.shared.sendAutoReleaseComboCommand(cmdStrings: squeezePressKeyStrings, releaseOnly: true)}
|
||||||
|
else {CommandManager.shared.sendAutoReleaseComboCommand(cmdStrings: squeezeReleaseKeyStrings, delay: 0.1)}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc static private(set) var eraserShortcut:String = ""
|
||||||
|
@objc static private(set) var brushShortcut:String = ""
|
||||||
|
private var doubleTapShorcuts: Array<String> = []
|
||||||
|
private var shortcutIndex: Int = 0
|
||||||
|
|
||||||
|
@objc func replaceBrush(with shortcut:String){
|
||||||
|
if let i = doubleTapShorcuts.indices.last {
|
||||||
|
doubleTapShorcuts[i] = shortcut
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func replaceEraser(with shortcut:String){
|
||||||
|
if let i = doubleTapShorcuts.indices.first {
|
||||||
|
doubleTapShorcuts[i] = shortcut
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 13.0, *)
|
||||||
|
func widgetPickerViewController(_ controller: WidgetPickerViewController, didCreateWidget payload: NSDictionary) {
|
||||||
|
let params = payload.mutableCopy() as? NSMutableDictionary ?? NSMutableDictionary()
|
||||||
|
let pickerAction = ((params["pickerAction"] as? String) ?? "").lowercased()
|
||||||
|
params.removeObject(forKey: "pickerAction")
|
||||||
|
|
||||||
|
if pickerAction == "create" {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc static public func enterDoubleTapShortcuts(in viewController: UIViewController){
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
let pickerViewController = WidgetPickerViewController()
|
||||||
|
pickerViewController.delegate = (viewController as! any WidgetPickerViewControllerDelegate)
|
||||||
|
pickerViewController.keyboardPickerMode = .shortcutPicker
|
||||||
|
pickerViewController.tabIdentifiers = ["keyboard", "shortcuts"]
|
||||||
|
pickerViewController.initialTabIdentifier = "keyboard"
|
||||||
|
pickerViewController.shortcutIdentifier = "eraser"
|
||||||
|
pickerViewController.shortcutPickerTipText = LocalizationHelper.localizedString(forKey: "Select eraser shortcut keys")
|
||||||
|
pickerViewController.presentOverFullScreen(from: viewController)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
let alert = UIAlertController(title: LocalizationHelper.localizedString(forKey: "Eraser Shortcut"),
|
||||||
|
message: LocalizationHelper.localizedString(forKey: "Enter eraser keyboard shortcut:"),
|
||||||
|
preferredStyle: .alert)
|
||||||
|
|
||||||
|
alert.addTextField { textField in
|
||||||
|
textField.placeholder = LocalizationHelper.localizedString(forKey:"Example: e, ctrl+e, alt+e ...")
|
||||||
|
textField.keyboardType = .asciiCapable
|
||||||
|
textField.autocorrectionType = .no
|
||||||
|
textField.spellCheckingType = .no
|
||||||
|
textField.text = selectedProfile.eraserShortcut
|
||||||
|
}
|
||||||
|
|
||||||
|
let okAction = UIAlertAction(title: LocalizationHelper.localizedString(forKey: "OK"), style: .default) { _ in
|
||||||
|
let comboButtons = alert.textFields?[0].text ?? ""
|
||||||
|
let keyStrings = CommandManager.shared.extractAutoReleaseButtonStrings(from: comboButtons)
|
||||||
|
if keyStrings?.count ?? 0 > 0 || comboButtons == "" {
|
||||||
|
eraserShortcut = comboButtons
|
||||||
|
}
|
||||||
|
enterBrushShortcut(in: viewController)
|
||||||
|
}
|
||||||
|
|
||||||
|
let learnMoreAction = UIAlertAction(title: LocalizationHelper.localizedString(forKey: "Learn More"), style: .default) { _ in
|
||||||
|
if let url = URL(string: LocalizationHelper.localizedString(forKey: "pencilKeyboardCmdURL")) {
|
||||||
|
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
alert.addAction(learnMoreAction)
|
||||||
|
alert.addAction(okAction)
|
||||||
|
|
||||||
|
viewController.present(alert, animated: true, completion: {
|
||||||
|
})
|
||||||
|
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 13.0, *)
|
||||||
|
@objc static public func enterBrushShortcut(in viewController: UIViewController){
|
||||||
|
let pickerViewController = WidgetPickerViewController()
|
||||||
|
pickerViewController.delegate = (viewController as! any WidgetPickerViewControllerDelegate)
|
||||||
|
pickerViewController.keyboardPickerMode = .shortcutPicker
|
||||||
|
pickerViewController.tabIdentifiers = ["keyboard", "shortcuts"]
|
||||||
|
pickerViewController.initialTabIdentifier = "keyboard"
|
||||||
|
pickerViewController.shortcutIdentifier = "brush"
|
||||||
|
pickerViewController.shortcutPickerTipText = LocalizationHelper.localizedString(forKey: "Select brush shortcut keys")
|
||||||
|
pickerViewController.presentOverFullScreen(from: viewController)
|
||||||
|
return
|
||||||
|
|
||||||
|
/*
|
||||||
|
let alert = UIAlertController(title: LocalizationHelper.localizedString(forKey: "Brush Shortcut"),
|
||||||
|
message: LocalizationHelper.localizedString(forKey: "Enter brush keyboard shortcut:"),
|
||||||
|
preferredStyle: .alert)
|
||||||
|
|
||||||
|
alert.addTextField { textField in
|
||||||
|
textField.placeholder = LocalizationHelper.localizedString(forKey:"Example: b, ctrl+b, alt+b ...")
|
||||||
|
textField.keyboardType = .asciiCapable
|
||||||
|
textField.autocorrectionType = .no
|
||||||
|
textField.spellCheckingType = .no
|
||||||
|
textField.text = selectedProfile?.brushShortcut
|
||||||
|
}
|
||||||
|
|
||||||
|
let okAction = UIAlertAction(title: LocalizationHelper.localizedString(forKey: "OK"), style: .default) { _ in
|
||||||
|
let comboButtons = alert.textFields?[0].text ?? ""
|
||||||
|
let keyStrings = CommandManager.shared.extractAutoReleaseButtonStrings(from: comboButtons)
|
||||||
|
if keyStrings?.count ?? 0 > 0 || comboButtons == "" {
|
||||||
|
brushShortcut = comboButtons
|
||||||
|
}
|
||||||
|
|
||||||
|
if selectedProfile?.brushShortcut != brushShortcut
|
||||||
|
|| selectedProfile?.eraserShortcut != eraserShortcut {
|
||||||
|
guard let selectedProfile = selectedProfile else {return}
|
||||||
|
let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero)
|
||||||
|
selectedProfile.brushShortcut = brushShortcut
|
||||||
|
selectedProfile.eraserShortcut = eraserShortcut
|
||||||
|
oscProfileMan.replaceSelectedProfile(with: selectedProfile, overwriteDefault: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let learnMoreAction = UIAlertAction(title: LocalizationHelper.localizedString(forKey: "Learn More"), style: .default) { _ in
|
||||||
|
if let url = URL(string: LocalizationHelper.localizedString(forKey: "pencilKeyboardCmdURL")) {
|
||||||
|
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
alert.addAction(learnMoreAction)
|
||||||
|
alert.addAction(okAction)
|
||||||
|
|
||||||
|
viewController.present(alert, animated: true, completion: {
|
||||||
|
|
||||||
|
})
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc static private(set) var squeezeStartShortcut:String = ""
|
||||||
|
@objc static private(set) var squeezeEndShortcut:String = ""
|
||||||
|
|
||||||
|
@objc static public func enterSqueezeShortcuts(in viewController: UIViewController){
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
let pickerViewController = WidgetPickerViewController()
|
||||||
|
pickerViewController.delegate = (viewController as! any WidgetPickerViewControllerDelegate)
|
||||||
|
pickerViewController.keyboardPickerMode = .shortcutPicker
|
||||||
|
pickerViewController.tabIdentifiers = ["keyboard", "shortcuts"]
|
||||||
|
pickerViewController.initialTabIdentifier = "keyboard"
|
||||||
|
pickerViewController.shortcutIdentifier = "squeezePress"
|
||||||
|
pickerViewController.shortcutPickerTipText = LocalizationHelper.localizedString(forKey: "squeezePressShortcutPickerTip")
|
||||||
|
pickerViewController.presentOverFullScreen(from: viewController)
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero)
|
||||||
|
selectedProfile = oscProfileMan.getSelectedProfile()
|
||||||
|
guard let selectedProfile = selectedProfile else {return}
|
||||||
|
|
||||||
|
let alert = UIAlertController(title: LocalizationHelper.localizedString(forKey: "Squeeze Shortcut"),
|
||||||
|
message: LocalizationHelper.localizedString(forKey: "enterSqueezePressShort"),
|
||||||
|
preferredStyle: .alert)
|
||||||
|
|
||||||
|
alert.addTextField { textField in
|
||||||
|
textField.placeholder = LocalizationHelper.localizedString(forKey:"Example: e, ctrl+e, alt+e ...")
|
||||||
|
textField.keyboardType = .asciiCapable
|
||||||
|
textField.autocorrectionType = .no
|
||||||
|
textField.spellCheckingType = .no
|
||||||
|
textField.text = selectedProfile.squeezeStartShortcut
|
||||||
|
}
|
||||||
|
|
||||||
|
let okAction = UIAlertAction(title: LocalizationHelper.localizedString(forKey: "OK"), style: .default) { _ in
|
||||||
|
let comboButtons = alert.textFields?[0].text ?? ""
|
||||||
|
let keyStrings = CommandManager.shared.extractAutoReleaseButtonStrings(from: comboButtons)
|
||||||
|
if keyStrings?.count ?? 0 > 0 || comboButtons == "" {
|
||||||
|
squeezeStartShortcut = comboButtons
|
||||||
|
}
|
||||||
|
enterSqueezeEndShortcut(in: viewController)
|
||||||
|
}
|
||||||
|
|
||||||
|
let learnMoreAction = UIAlertAction(title: LocalizationHelper.localizedString(forKey: "Learn More"), style: .default) { _ in
|
||||||
|
if let url = URL(string: LocalizationHelper.localizedString(forKey: "pencilKeyboardCmdURL")) {
|
||||||
|
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
alert.addAction(learnMoreAction)
|
||||||
|
alert.addAction(okAction)
|
||||||
|
|
||||||
|
viewController.present(alert, animated: true, completion: {
|
||||||
|
})
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc static public func enterSqueezeEndShortcut(in viewController: UIViewController){
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
let pickerViewController = WidgetPickerViewController()
|
||||||
|
pickerViewController.delegate = (viewController as! any WidgetPickerViewControllerDelegate)
|
||||||
|
pickerViewController.keyboardPickerMode = .shortcutPicker
|
||||||
|
pickerViewController.tabIdentifiers = ["keyboard", "shortcuts"]
|
||||||
|
pickerViewController.initialTabIdentifier = "keyboard"
|
||||||
|
pickerViewController.shortcutIdentifier = "squeezeRelease"
|
||||||
|
pickerViewController.shortcutPickerTipText = LocalizationHelper.localizedString(forKey: "squeezeReleaseShortcutPickerTip")
|
||||||
|
pickerViewController.presentOverFullScreen(from: viewController)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
let alert = UIAlertController(title: LocalizationHelper.localizedString(forKey: "Squeeze Shortcut"),
|
||||||
|
message: LocalizationHelper.localizedString(forKey: "enterSqueezeReleaseShort"),
|
||||||
|
preferredStyle: .alert)
|
||||||
|
|
||||||
|
alert.addTextField { textField in
|
||||||
|
textField.placeholder = LocalizationHelper.localizedString(forKey:"Example: b, ctrl+b, alt+b ...")
|
||||||
|
textField.keyboardType = .asciiCapable
|
||||||
|
textField.autocorrectionType = .no
|
||||||
|
textField.spellCheckingType = .no
|
||||||
|
textField.text = selectedProfile?.squeezeEndShortcut
|
||||||
|
}
|
||||||
|
|
||||||
|
let okAction = UIAlertAction(title: LocalizationHelper.localizedString(forKey: "OK"), style: .default) { _ in
|
||||||
|
let comboButtons = alert.textFields?[0].text ?? ""
|
||||||
|
let keyStrings = CommandManager.shared.extractAutoReleaseButtonStrings(from: comboButtons)
|
||||||
|
if keyStrings?.count ?? 0 > 0 || comboButtons == "" {
|
||||||
|
squeezeEndShortcut = comboButtons
|
||||||
|
}
|
||||||
|
|
||||||
|
if selectedProfile?.squeezeEndShortcut != squeezeEndShortcut
|
||||||
|
|| selectedProfile?.squeezeStartShortcut != squeezeStartShortcut {
|
||||||
|
guard let selectedProfile = selectedProfile else {return}
|
||||||
|
let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero)
|
||||||
|
selectedProfile.squeezeEndShortcut = squeezeEndShortcut
|
||||||
|
selectedProfile.squeezeStartShortcut = squeezeStartShortcut
|
||||||
|
oscProfileMan.replaceSelectedProfile(with: selectedProfile, overwriteDefault: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let learnMoreAction = UIAlertAction(title: LocalizationHelper.localizedString(forKey: "Learn More"), style: .default) { _ in
|
||||||
|
if let url = URL(string: LocalizationHelper.localizedString(forKey: "pencilKeyboardCmdURL")) {
|
||||||
|
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
alert.addAction(learnMoreAction)
|
||||||
|
alert.addAction(okAction)
|
||||||
|
|
||||||
|
viewController.present(alert, animated: true, completion: {
|
||||||
|
|
||||||
|
})
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
private func attachHoverLeave(normalizedLocation:CGPoint){
|
||||||
|
DispatchQueue.global().asyncAfter(deadline: .now()+0.0086) {
|
||||||
|
LiSendPenEvent(UInt8(LI_TOUCH_EVENT_HOVER), UInt8(LI_TOOL_TYPE_PEN), 0, Float(normalizedLocation.x), Float(normalizedLocation.y), 0, 0, 0, 0, 0)
|
||||||
|
DispatchQueue.global().asyncAfter(deadline: .now()+0.0086) {
|
||||||
|
// LiSendPenEvent(UInt8(LI_TOUCH_EVENT_HOVER_LEAVE), UInt8(LI_TOOL_TYPE_PEN), 0, Float(normalizedLocation.x), Float(normalizedLocation.y), 0, 0, 0, 0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func preTouchHoverActionAt(normalizedLocation:CGPoint){
|
||||||
|
DispatchQueue.global().asyncAfter(deadline: .now()+0.0086) {
|
||||||
|
LiSendPenEvent(UInt8(LI_TOUCH_EVENT_HOVER), UInt8(LI_TOOL_TYPE_PEN), 0, Float(normalizedLocation.x), Float(normalizedLocation.y), 0, 0, 0, 0, 0)
|
||||||
|
DispatchQueue.global().asyncAfter(deadline: .now()+0.0086) {
|
||||||
|
LiSendPenEvent(UInt8(LI_TOUCH_EVENT_HOVER_LEAVE), UInt8(LI_TOOL_TYPE_PEN), 0, Float(normalizedLocation.x), Float(normalizedLocation.y), 0, 0, 0, 0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,361 @@
|
|||||||
|
//
|
||||||
|
// PencilTipOffsetCalibrationViewController.swift
|
||||||
|
// VoidLink
|
||||||
|
//
|
||||||
|
// Created by True砖家 on 2026/7/30.
|
||||||
|
// Copyright © 2026 True砖家 on Bilibili. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
private enum PencilTipOffsetCalibrationLayout {
|
||||||
|
static let containerWidth: CGFloat = 430
|
||||||
|
static let containerHeight: CGFloat = 430
|
||||||
|
static let navBarHeight: CGFloat = 50
|
||||||
|
static let canvasTop: CGFloat = 66
|
||||||
|
static let canvasWidth: CGFloat = 320
|
||||||
|
static let canvasHeight: CGFloat = 260
|
||||||
|
static let sliderTopSpacing: CGFloat = 18
|
||||||
|
static let horizontalInset: CGFloat = 30
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class PencilTipOffsetCanvasView: UIView {
|
||||||
|
var offset: CGPoint = .zero {
|
||||||
|
didSet { setNeedsDisplay() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private var rawTouchPoint: CGPoint? {
|
||||||
|
didSet { setNeedsDisplay() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
isMultipleTouchEnabled = false
|
||||||
|
backgroundColor = UIColor(white: 0.97, alpha: 1)
|
||||||
|
layer.borderColor = UIColor.black.withAlphaComponent(0.12).cgColor
|
||||||
|
layer.borderWidth = 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func draw(_ rect: CGRect) {
|
||||||
|
guard let context = UIGraphicsGetCurrentContext() else { return }
|
||||||
|
|
||||||
|
let center = CGPoint(x: bounds.midX, y: bounds.midY)
|
||||||
|
context.setLineWidth(1)
|
||||||
|
context.setStrokeColor(UIColor.black.withAlphaComponent(0.14).cgColor)
|
||||||
|
context.move(to: CGPoint(x: 0, y: center.y))
|
||||||
|
context.addLine(to: CGPoint(x: bounds.maxX, y: center.y))
|
||||||
|
context.move(to: CGPoint(x: center.x, y: 0))
|
||||||
|
context.addLine(to: CGPoint(x: center.x, y: bounds.maxY))
|
||||||
|
context.strokePath()
|
||||||
|
|
||||||
|
context.setStrokeColor(UIColor.systemBlue.withAlphaComponent(0.45).cgColor)
|
||||||
|
context.setLineWidth(2)
|
||||||
|
context.move(to: center)
|
||||||
|
context.addLine(to: CGPoint(x: center.x + offset.x, y: center.y + offset.y))
|
||||||
|
context.strokePath()
|
||||||
|
|
||||||
|
if let rawTouchPoint {
|
||||||
|
drawLandingPreview(in: context, rawPoint: rawTouchPoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.setFillColor(UIColor.black.withAlphaComponent(0.22).cgColor)
|
||||||
|
context.fillEllipse(in: CGRect(x: center.x - 5, y: center.y - 5, width: 10, height: 10))
|
||||||
|
|
||||||
|
let tipCenter = CGPoint(x: center.x + offset.x, y: center.y + offset.y)
|
||||||
|
context.setFillColor(UIColor.systemBlue.cgColor)
|
||||||
|
context.fillEllipse(in: CGRect(x: tipCenter.x - 5, y: tipCenter.y - 5, width: 10, height: 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func drawLandingPreview(in context: CGContext, rawPoint: CGPoint) {
|
||||||
|
let correctedPoint = CGPoint(x: rawPoint.x + offset.x, y: rawPoint.y + offset.y)
|
||||||
|
|
||||||
|
context.setFillColor(UIColor.black.withAlphaComponent(0.28).cgColor)
|
||||||
|
context.fillEllipse(in: CGRect(x: rawPoint.x - 5, y: rawPoint.y - 5, width: 10, height: 10))
|
||||||
|
|
||||||
|
context.setFillColor(UIColor.systemBlue.cgColor)
|
||||||
|
context.fillEllipse(in: CGRect(x: correctedPoint.x - 5, y: correctedPoint.y - 5, width: 10, height: 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
|
updateTouchPoint(from: touches)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
|
updateTouchPoint(from: touches)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
|
updateTouchPoint(from: touches)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
|
rawTouchPoint = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateTouchPoint(from touches: Set<UITouch>) {
|
||||||
|
guard let touch = touches.first(where: { $0.type == .pencil }) else { return }
|
||||||
|
rawTouchPoint = touch.preciseLocation(in: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc class PencilTipOffsetCalibrationViewController: UIViewController, UIGestureRecognizerDelegate {
|
||||||
|
private let container = UIView()
|
||||||
|
private let canvasView = PencilTipOffsetCanvasView()
|
||||||
|
private let xSlider = UISlider()
|
||||||
|
private let ySlider = UISlider()
|
||||||
|
private let xLabel = UILabel()
|
||||||
|
private let yLabel = UILabel()
|
||||||
|
private var saveButton = UIBarButtonItem()
|
||||||
|
private var persistedOffset: CGPoint = .zero
|
||||||
|
private var currentOffset: CGPoint = .zero
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
view.isOpaque = false
|
||||||
|
view.backgroundColor = UIColor.clear
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
overrideUserInterfaceStyle = .light
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPersistedOffset()
|
||||||
|
setupContainer()
|
||||||
|
setupNavigationBar()
|
||||||
|
setupCanvas()
|
||||||
|
setupSliders()
|
||||||
|
applyOffset(currentOffset)
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(pencilProPurchaseAborted(_:)),
|
||||||
|
name: AddOnProduct.PencilProPack.purchaseAbortedNotification(),
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(persistOffset),
|
||||||
|
name: AddOnProduct.PencilProPack.purchaseSucceededNotification(),
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
let tap = UITapGestureRecognizer(target: self, action: #selector(dismissSelf))
|
||||||
|
tap.delegate = self
|
||||||
|
view.addGestureRecognizer(tap)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillDisappear(_ animated: Bool) {
|
||||||
|
super.viewWillDisappear(animated)
|
||||||
|
NotificationCenter.default.removeObserver(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadPersistedOffset() {
|
||||||
|
let dataMan = DataManager()
|
||||||
|
let settings = dataMan.retrieveSettings()
|
||||||
|
persistedOffset = CGPoint(
|
||||||
|
x: CGFloat(settings?.pencilTipOffsetX?.floatValue ?? 0),
|
||||||
|
y: CGFloat(settings?.pencilTipOffsetY?.floatValue ?? 0)
|
||||||
|
)
|
||||||
|
currentOffset = persistedOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupContainer() {
|
||||||
|
container.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
container.backgroundColor = .white
|
||||||
|
container.layer.cornerRadius = 12
|
||||||
|
container.layer.masksToBounds = true
|
||||||
|
container.layer.borderWidth = 0.5
|
||||||
|
container.layer.borderColor = UIColor.black.withAlphaComponent(0.15).cgColor
|
||||||
|
view.addSubview(container)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
container.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
||||||
|
container.centerYAnchor.constraint(equalTo: view.centerYAnchor),
|
||||||
|
container.widthAnchor.constraint(equalToConstant: PencilTipOffsetCalibrationLayout.containerWidth),
|
||||||
|
container.heightAnchor.constraint(equalToConstant: PencilTipOffsetCalibrationLayout.containerHeight)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupNavigationBar() {
|
||||||
|
let navBar = UINavigationBar()
|
||||||
|
navBar.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
container.addSubview(navBar)
|
||||||
|
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
let appearance = UINavigationBarAppearance()
|
||||||
|
appearance.shadowColor = nil
|
||||||
|
appearance.backgroundColor = .white
|
||||||
|
navBar.standardAppearance = appearance
|
||||||
|
navBar.scrollEdgeAppearance = appearance
|
||||||
|
} else {
|
||||||
|
navBar.setBackgroundImage(UIImage(), for: .default)
|
||||||
|
navBar.shadowImage = UIImage()
|
||||||
|
navBar.isTranslucent = true
|
||||||
|
navBar.backgroundColor = .clear
|
||||||
|
}
|
||||||
|
|
||||||
|
saveButton = UIBarButtonItem(title: LocalizationHelper.localizedString(forKey: "Save"), style: .plain, target: self, action: #selector(saveTapped))
|
||||||
|
let resetButton = UIBarButtonItem(title: LocalizationHelper.localizedString(forKey: "Reset"), style: .plain, target: self, action: #selector(resetTapped))
|
||||||
|
let navItem = UINavigationItem(title: LocalizationHelper.localizedString(forKey: "Pencil Tip Offset"))
|
||||||
|
navItem.leftBarButtonItems = [resetButton]
|
||||||
|
navItem.rightBarButtonItems = [saveButton]
|
||||||
|
|
||||||
|
if #available(iOS 26.0, *) {
|
||||||
|
resetButton.hidesSharedBackground = true
|
||||||
|
saveButton.hidesSharedBackground = true
|
||||||
|
resetButton.tintColor = .tintColor
|
||||||
|
saveButton.tintColor = .tintColor
|
||||||
|
}
|
||||||
|
|
||||||
|
navBar.items = [navItem]
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
navBar.topAnchor.constraint(equalTo: container.safeAreaLayoutGuide.topAnchor),
|
||||||
|
navBar.leadingAnchor.constraint(equalTo: container.leadingAnchor),
|
||||||
|
navBar.trailingAnchor.constraint(equalTo: container.trailingAnchor),
|
||||||
|
navBar.heightAnchor.constraint(equalToConstant: PencilTipOffsetCalibrationLayout.navBarHeight)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupCanvas() {
|
||||||
|
canvasView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
container.addSubview(canvasView)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
canvasView.topAnchor.constraint(equalTo: container.topAnchor, constant: PencilTipOffsetCalibrationLayout.canvasTop),
|
||||||
|
canvasView.centerXAnchor.constraint(equalTo: container.centerXAnchor),
|
||||||
|
canvasView.widthAnchor.constraint(equalToConstant: PencilTipOffsetCalibrationLayout.canvasWidth),
|
||||||
|
canvasView.heightAnchor.constraint(equalToConstant: PencilTipOffsetCalibrationLayout.canvasHeight)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupSliders() {
|
||||||
|
let sliderStack = UIStackView()
|
||||||
|
sliderStack.axis = .vertical
|
||||||
|
sliderStack.spacing = 10
|
||||||
|
sliderStack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
container.addSubview(sliderStack)
|
||||||
|
|
||||||
|
configureSlider(xSlider, action: #selector(sliderMoved(_:)))
|
||||||
|
configureSlider(ySlider, action: #selector(sliderMoved(_:)))
|
||||||
|
configureLabel(xLabel)
|
||||||
|
configureLabel(yLabel)
|
||||||
|
|
||||||
|
sliderStack.addArrangedSubview(makeSliderRow(label: xLabel, slider: xSlider))
|
||||||
|
sliderStack.addArrangedSubview(makeSliderRow(label: yLabel, slider: ySlider))
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
sliderStack.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: PencilTipOffsetCalibrationLayout.horizontalInset),
|
||||||
|
sliderStack.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -PencilTipOffsetCalibrationLayout.horizontalInset),
|
||||||
|
sliderStack.topAnchor.constraint(equalTo: canvasView.bottomAnchor, constant: PencilTipOffsetCalibrationLayout.sliderTopSpacing)
|
||||||
|
])
|
||||||
|
|
||||||
|
xSlider.value = Float(currentOffset.x)
|
||||||
|
ySlider.value = Float(currentOffset.y)
|
||||||
|
updateLabels()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureSlider(_ slider: UISlider, action: Selector) {
|
||||||
|
slider.minimumValue = -15
|
||||||
|
slider.maximumValue = 15
|
||||||
|
slider.addTarget(self, action: action, for: .valueChanged)
|
||||||
|
if PublicUtils.liquidGlassEnabled, #available(iOS 13.0, *) {
|
||||||
|
slider.maximumTrackTintColor = .tertiarySystemFill
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureLabel(_ label: UILabel) {
|
||||||
|
label.font = UIFont.systemFont(ofSize: 14)
|
||||||
|
label.textColor = .black
|
||||||
|
label.widthAnchor.constraint(equalToConstant: 110).isActive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeSliderRow(label: UILabel, slider: UISlider) -> UIStackView {
|
||||||
|
let row = UIStackView(arrangedSubviews: [label, slider])
|
||||||
|
row.axis = .horizontal
|
||||||
|
row.alignment = .center
|
||||||
|
row.spacing = 12
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func sliderMoved(_ sender: UISlider) {
|
||||||
|
currentOffset = CGPoint(x: CGFloat(xSlider.value), y: CGFloat(ySlider.value))
|
||||||
|
applyOffset(currentOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyOffset(_ offset: CGPoint) {
|
||||||
|
canvasView.offset = offset
|
||||||
|
updateLabels()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateLabels() {
|
||||||
|
xLabel.text = "\(LocalizationHelper.localizedString(forKey: "X Offset")): \(String(format: "%.1f", currentOffset.x))"
|
||||||
|
yLabel.text = "\(LocalizationHelper.localizedString(forKey: "Y Offset")): \(String(format: "%.1f", currentOffset.y))"
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func resetTapped() {
|
||||||
|
xSlider.value = 0
|
||||||
|
ySlider.value = 0
|
||||||
|
currentOffset = .zero
|
||||||
|
applyOffset(currentOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func persistOffset() {
|
||||||
|
let dataMan = DataManager()
|
||||||
|
guard let settings = dataMan.retrieveSettings() else { return }
|
||||||
|
settings.pencilTipOffsetX = NSNumber(value: Float(currentOffset.x))
|
||||||
|
settings.pencilTipOffsetY = NSNumber(value: Float(currentOffset.y))
|
||||||
|
dataMan.saveData()
|
||||||
|
persistedOffset = currentOffset
|
||||||
|
PencilHandler.shared?.updatePencilTipOffset(x: currentOffset.x, y: currentOffset.y)
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
AlertControllerUtil.autoCompletion = true
|
||||||
|
AlertControllerUtil.showAlert(
|
||||||
|
in: self,
|
||||||
|
title: "",
|
||||||
|
message: LocalizationHelper.localizedString(forKey: "Pencil tip offset saved"),
|
||||||
|
withCancel: false,
|
||||||
|
buttonTitle: "",
|
||||||
|
countdown: 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func saveTapped() {
|
||||||
|
saveButton.isEnabled = false
|
||||||
|
IAPManager.checkPurchaseInfo(.PencilProPack) { info in
|
||||||
|
self.saveButton.isEnabled = true
|
||||||
|
if info.valid {
|
||||||
|
self.persistOffset()
|
||||||
|
} else {
|
||||||
|
IAPManager.inAppPurchaseAction(viewController: self, product: .PencilProPack)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func pencilProPurchaseAborted(_ notification: Notification) {
|
||||||
|
guard let interruption = notification.object as? PurchaseInterruption else { return }
|
||||||
|
if interruption == .lowOSVersion {
|
||||||
|
AlertControllerUtil.showAlert(
|
||||||
|
in: self,
|
||||||
|
title: "",
|
||||||
|
message: LocalizationHelper.localizedString(forKey:"PencilProPackLowOSVersionTip"),
|
||||||
|
withCancel: false,
|
||||||
|
buttonTitle: LocalizationHelper.localizedString(forKey: "OK"),
|
||||||
|
countdown: 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func dismissSelf() {
|
||||||
|
dismiss(animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
||||||
|
return touch.view == view
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,964 @@
|
|||||||
|
//
|
||||||
|
// PressureCurve.swift
|
||||||
|
// VoidLink
|
||||||
|
//
|
||||||
|
// Created by True砖家 on 2026/1/5.
|
||||||
|
// Copyright © 2025 True砖家 on Bilibili. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
// MARK: - PressureCurve
|
||||||
|
final class PressureCurve {
|
||||||
|
|
||||||
|
var polylinePoints: [CGPoint] = [
|
||||||
|
CGPoint(x: 0, y: 0.0),
|
||||||
|
CGPoint(x: 1.0, y: 1.0)
|
||||||
|
]
|
||||||
|
|
||||||
|
private var factor: CGFloat = 0.39
|
||||||
|
|
||||||
|
private var cachedSegments: [(p0: CGPoint, c1: CGPoint, c2: CGPoint, p1: CGPoint)] = []
|
||||||
|
|
||||||
|
private var curveCutPoints: [CGPoint] {
|
||||||
|
guard polylinePoints.count >= 2 else { return polylinePoints }
|
||||||
|
var cuts: [CGPoint] = []
|
||||||
|
cuts.append(polylinePoints[0]) // 起点
|
||||||
|
for i in 0..<(polylinePoints.count - 2) {
|
||||||
|
let mid = CGPoint(
|
||||||
|
x: (polylinePoints[i+1].x + polylinePoints[i+2].x)/2,
|
||||||
|
y: (polylinePoints[i+1].y + polylinePoints[i+2].y)/2
|
||||||
|
)
|
||||||
|
if i < polylinePoints.count - 3 {
|
||||||
|
cuts.append(mid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cuts.append(polylinePoints.last!) // 终点
|
||||||
|
return cuts
|
||||||
|
}
|
||||||
|
|
||||||
|
private func curveFactor(for middlePointCount: Int) -> CGFloat {
|
||||||
|
guard middlePointCount > 0 else { return 2.0 / 3.0 }
|
||||||
|
let n = CGFloat(middlePointCount)
|
||||||
|
return 0.30538319 * pow(n, -2.1518917) + 0.36128348
|
||||||
|
}
|
||||||
|
|
||||||
|
public func buildCurveSegments() {
|
||||||
|
let cuts = curveCutPoints
|
||||||
|
var segments: [(CGPoint, CGPoint, CGPoint, CGPoint)] = []
|
||||||
|
|
||||||
|
factor = curveFactor(for: polylinePoints.count-2)
|
||||||
|
|
||||||
|
for i in 0..<(cuts.count - 1) {
|
||||||
|
let p0 = cuts[i]
|
||||||
|
let p1 = cuts[i+1]
|
||||||
|
|
||||||
|
// segment index
|
||||||
|
let startIndex = i
|
||||||
|
let endIndex = i+1
|
||||||
|
|
||||||
|
let tangentStart: CGPoint
|
||||||
|
if startIndex < polylinePoints.count - 1 {
|
||||||
|
tangentStart = CGPoint(
|
||||||
|
x: (polylinePoints[startIndex + 1].x - polylinePoints[startIndex].x) * factor,
|
||||||
|
y: (polylinePoints[startIndex + 1].y - polylinePoints[startIndex].y) * factor
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
tangentStart = .zero
|
||||||
|
}
|
||||||
|
|
||||||
|
let tangentEnd: CGPoint
|
||||||
|
if endIndex < polylinePoints.count - 1 {
|
||||||
|
tangentEnd = CGPoint(
|
||||||
|
x: (polylinePoints[endIndex + 1].x - polylinePoints[endIndex].x) * factor,
|
||||||
|
y: (polylinePoints[endIndex + 1].y - polylinePoints[endIndex].y) * factor
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
tangentEnd = .zero
|
||||||
|
}
|
||||||
|
|
||||||
|
let c1 = CGPoint(x: p0.x + tangentStart.x, y: p0.y + tangentStart.y)
|
||||||
|
let c2 = CGPoint(x: p1.x - tangentEnd.x, y: p1.y - tangentEnd.y)
|
||||||
|
|
||||||
|
segments.append((p0: p0, c1: c1, c2: c2, p1: p1))
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedSegments = segments
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 采样 曲线
|
||||||
|
func sampleCurve(stepsPerSegment: Int = 50) -> [CGPoint] {
|
||||||
|
var points: [CGPoint] = []
|
||||||
|
buildCurveSegments()
|
||||||
|
for seg in cachedSegments {
|
||||||
|
let sampled = sampleCurve(p0: seg.p0, c1: seg.c1, c2: seg.c2, p1: seg.p1, steps: stepsPerSegment)
|
||||||
|
if !points.isEmpty { points.removeLast() } // 避免重复点
|
||||||
|
points.append(contentsOf: sampled)
|
||||||
|
}
|
||||||
|
return points
|
||||||
|
}
|
||||||
|
|
||||||
|
private func sampleCurve(p0: CGPoint, c1: CGPoint, c2: CGPoint, p1: CGPoint, steps: Int) -> [CGPoint] {
|
||||||
|
var result: [CGPoint] = []
|
||||||
|
for i in 0...steps {
|
||||||
|
let t = CGFloat(i)/CGFloat(steps)
|
||||||
|
let mt = 1-t
|
||||||
|
let x = mt*mt*mt*p0.x + 3*mt*mt*t*c1.x + 3*mt*t*t*c2.x + t*t*t*p1.x
|
||||||
|
let y = mt*mt*mt*p0.y + 3*mt*mt*t*c1.y + 3*mt*t*t*c2.y + t*t*t*p1.y
|
||||||
|
result.append(CGPoint(x:x, y:y))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
var tangentPoints: [CGPoint] { polylinePoints }
|
||||||
|
|
||||||
|
func value(at x: CGFloat) -> CGFloat {
|
||||||
|
|
||||||
|
guard let firstPoint = polylinePoints.first else { return 0 }
|
||||||
|
if x < firstPoint.x {return 0}
|
||||||
|
|
||||||
|
let xClamped = min(max(x, 0), 1)
|
||||||
|
let segments = cachedSegments
|
||||||
|
|
||||||
|
for seg in segments {
|
||||||
|
if xClamped >= seg.p0.x && xClamped <= seg.p1.x {
|
||||||
|
let t = solveT(forX: xClamped,
|
||||||
|
p0: seg.p0.x,
|
||||||
|
c1: seg.c1.x,
|
||||||
|
c2: seg.c2.x,
|
||||||
|
p1: seg.p1.x)
|
||||||
|
return cubic(p0: seg.p0.y,
|
||||||
|
c1: seg.c1.y,
|
||||||
|
c2: seg.c2.y,
|
||||||
|
p1: seg.p1.y,
|
||||||
|
t: t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return segments.last?.p1.y ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private func solveT(
|
||||||
|
forX x: CGFloat,
|
||||||
|
p0: CGFloat,
|
||||||
|
c1: CGFloat,
|
||||||
|
c2: CGFloat,
|
||||||
|
p1: CGFloat,
|
||||||
|
iterations: Int = 18
|
||||||
|
) -> CGFloat {
|
||||||
|
|
||||||
|
var low: CGFloat = 0
|
||||||
|
var high: CGFloat = 1
|
||||||
|
var t: CGFloat = 0.5
|
||||||
|
|
||||||
|
for _ in 0..<iterations {
|
||||||
|
t = (low + high) * 0.5
|
||||||
|
let xt = cubic(p0: p0, c1: c1, c2: c2, p1: p1, t: t)
|
||||||
|
|
||||||
|
if xt < x {
|
||||||
|
low = t
|
||||||
|
} else {
|
||||||
|
high = t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
@inline(__always)
|
||||||
|
private func cubic(
|
||||||
|
p0: CGFloat,
|
||||||
|
c1: CGFloat,
|
||||||
|
c2: CGFloat,
|
||||||
|
p1: CGFloat,
|
||||||
|
t: CGFloat
|
||||||
|
) -> CGFloat {
|
||||||
|
|
||||||
|
let mt = 1 - t
|
||||||
|
return mt*mt*mt*p0
|
||||||
|
+ 3*mt*mt*t*c1
|
||||||
|
+ 3*mt*t*t*c2
|
||||||
|
+ t*t*t*p1
|
||||||
|
}
|
||||||
|
|
||||||
|
static func exportCurvePoints(_ curve: PressureCurve) -> [NSNumber] {
|
||||||
|
var arr: [NSNumber] = []
|
||||||
|
for p in curve.polylinePoints {
|
||||||
|
arr.append(NSNumber(value: Float(p.x)))
|
||||||
|
arr.append(NSNumber(value: Float(p.y)))
|
||||||
|
}
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
|
||||||
|
static func importCurvePoints(_ numbers: [NSNumber]) -> [CGPoint] {
|
||||||
|
var pts: [CGPoint] = []
|
||||||
|
let count = numbers.count / 2
|
||||||
|
|
||||||
|
for i in 0..<count {
|
||||||
|
let x = CGFloat(numbers[i*2].floatValue)
|
||||||
|
let y = CGFloat(numbers[i*2 + 1].floatValue)
|
||||||
|
pts.append(CGPoint(x: x, y: y))
|
||||||
|
}
|
||||||
|
return pts
|
||||||
|
}
|
||||||
|
|
||||||
|
static func generateLinearCurvePoints(from pressures: [CGFloat]) -> [CGPoint] {
|
||||||
|
guard !pressures.isEmpty else { return [] }
|
||||||
|
|
||||||
|
let samples = pressures.map { min(max($0, 0), 1) }
|
||||||
|
let maxPressure = samples.max() ?? 0
|
||||||
|
|
||||||
|
if maxPressure < 1 {
|
||||||
|
let platformX = maxPressure
|
||||||
|
|
||||||
|
return [
|
||||||
|
CGPoint(x: 0, y: 0),
|
||||||
|
CGPoint(x: platformX, y: 1),
|
||||||
|
CGPoint(x: platformX, y: 1),
|
||||||
|
CGPoint(x: 1, y: 1)
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
return [
|
||||||
|
CGPoint(x: 0, y: 0),
|
||||||
|
CGPoint(x: 1, y: 1)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - PressureCurveView
|
||||||
|
final class PressureCurveView: UIView {
|
||||||
|
|
||||||
|
let curve = PressureCurve()
|
||||||
|
|
||||||
|
private var draggingIndex: Int? = nil
|
||||||
|
private var latestDraggingIndex: Int? = nil
|
||||||
|
private var newPointGenerated: Bool = false
|
||||||
|
private var draggingPointPreviousLocation: CGPoint? = nil
|
||||||
|
private let hitRadius: CGFloat = 35
|
||||||
|
|
||||||
|
enum PressureTestStage: UInt8 {
|
||||||
|
case drawingStage
|
||||||
|
case curveStage
|
||||||
|
}
|
||||||
|
public var testStage: PressureTestStage = .curveStage
|
||||||
|
private var pressures:[CGFloat] = []
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
backgroundColor = .black
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) { fatalError() }
|
||||||
|
|
||||||
|
public var showGraph: Bool = false {
|
||||||
|
didSet { setNeedsDisplay() } // 改变开关立即刷新
|
||||||
|
}
|
||||||
|
|
||||||
|
override func draw(_ rect: CGRect) {
|
||||||
|
guard let ctx = UIGraphicsGetCurrentContext() else { return }
|
||||||
|
|
||||||
|
ctx.saveGState()
|
||||||
|
|
||||||
|
// 缩放和平移
|
||||||
|
let scale: CGFloat = 0.95
|
||||||
|
let dx = (bounds.width - bounds.width * scale) / 2
|
||||||
|
let dy = (bounds.height - bounds.height * scale) / 2
|
||||||
|
ctx.translateBy(x: dx, y: dy)
|
||||||
|
ctx.scaleBy(x: scale, y: scale)
|
||||||
|
|
||||||
|
if showGraph {
|
||||||
|
drawGrid(ctx)
|
||||||
|
drawCurve(ctx)
|
||||||
|
drawTangents(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.restoreGState()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func drawGrid(_ ctx: CGContext) {
|
||||||
|
ctx.setStrokeColor(UIColor.darkGray.cgColor)
|
||||||
|
ctx.setLineWidth(1)
|
||||||
|
for i in 0...4 {
|
||||||
|
let t = CGFloat(i)/4
|
||||||
|
let x = bounds.width*t
|
||||||
|
let y = bounds.height*t
|
||||||
|
ctx.move(to: CGPoint(x:x, y:0))
|
||||||
|
ctx.addLine(to: CGPoint(x:x, y:bounds.height))
|
||||||
|
ctx.move(to: CGPoint(x:0, y:y))
|
||||||
|
ctx.addLine(to: CGPoint(x:bounds.width, y:y))
|
||||||
|
}
|
||||||
|
ctx.strokePath()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func drawCurve(_ ctx: CGContext) {
|
||||||
|
ctx.setStrokeColor(UIColor.systemBlue.cgColor)
|
||||||
|
ctx.setLineWidth(6)
|
||||||
|
let points = curve.sampleCurve(stepsPerSegment: 60)
|
||||||
|
for (i,p) in points.enumerated() {
|
||||||
|
let mapped = map(p)
|
||||||
|
if i==0 { ctx.move(to:mapped) } else { ctx.addLine(to:mapped) }
|
||||||
|
}
|
||||||
|
ctx.strokePath()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func drawTangents(_ ctx: CGContext) {
|
||||||
|
let pts = curve.tangentPoints
|
||||||
|
ctx.setStrokeColor(UIColor.systemPurple.cgColor)
|
||||||
|
ctx.setLineWidth(2)
|
||||||
|
for i in 0..<pts.count {
|
||||||
|
let p = map(pts[i])
|
||||||
|
if i==0 { ctx.move(to:p) } else { ctx.addLine(to:p) }
|
||||||
|
}
|
||||||
|
ctx.strokePath()
|
||||||
|
|
||||||
|
for p in pts {
|
||||||
|
let c = map(p)
|
||||||
|
let r: CGFloat = 16
|
||||||
|
ctx.setFillColor(UIColor.systemPurple.cgColor)
|
||||||
|
ctx.fill(CGRect(x:c.x-r/2, y:c.y-r/2, width:r, height:r))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func map(_ p: CGPoint) -> CGPoint {
|
||||||
|
CGPoint(x:p.x*bounds.width, y:(1-p.y)*bounds.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func unmap(_ p: CGPoint) -> CGPoint {
|
||||||
|
CGPoint(
|
||||||
|
x: p.x / bounds.width,
|
||||||
|
y: 1 - (p.y / bounds.height)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func hitTestPoint(_ location: CGPoint) -> Int? {
|
||||||
|
let pts = curve.tangentPoints
|
||||||
|
for (i, p) in pts.enumerated() {
|
||||||
|
let c = map(p)
|
||||||
|
let dx = c.x - location.x
|
||||||
|
let dy = c.y - location.y
|
||||||
|
if dx*dx + dy*dy <= hitRadius*hitRadius {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func undoLastMove() {
|
||||||
|
if newPointGenerated {
|
||||||
|
guard let latestDraggingIndex = latestDraggingIndex else { return }
|
||||||
|
self.latestDraggingIndex = nil
|
||||||
|
curve.polylinePoints.remove(at: latestDraggingIndex )
|
||||||
|
setNeedsDisplay()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
guard let latestDraggingIndex = latestDraggingIndex else { return }
|
||||||
|
self.latestDraggingIndex = nil
|
||||||
|
guard let draggingPointPreviousLocation = draggingPointPreviousLocation else { return }
|
||||||
|
curve.polylinePoints[latestDraggingIndex] = draggingPointPreviousLocation
|
||||||
|
setNeedsDisplay()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
|
guard let touch = touches.first else { return }
|
||||||
|
let location = touch.location(in: self)
|
||||||
|
|
||||||
|
if testStage == .drawingStage {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
draggingIndex = hitTestPoint(location)
|
||||||
|
if draggingIndex == nil {
|
||||||
|
guard
|
||||||
|
let touch = touches.first
|
||||||
|
else { return }
|
||||||
|
newPointGenerated = true
|
||||||
|
let location = touch.location(in: self)
|
||||||
|
var point = unmap(location)
|
||||||
|
|
||||||
|
point.x = min(max(point.x, 0), 1)
|
||||||
|
point.y = min(max(point.y, 0), 1)
|
||||||
|
var insertIdx:Int = -1
|
||||||
|
|
||||||
|
for (idx, polylinePoint) in curve.polylinePoints.enumerated() {
|
||||||
|
if point.x <= polylinePoint.x {
|
||||||
|
insertIdx = idx
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if insertIdx >= 0 {
|
||||||
|
curve.polylinePoints.insert(point, at: insertIdx)
|
||||||
|
draggingIndex = insertIdx
|
||||||
|
}
|
||||||
|
latestDraggingIndex = draggingIndex
|
||||||
|
setNeedsDisplay()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
guard let draggingIndex = draggingIndex else { return }
|
||||||
|
latestDraggingIndex = draggingIndex
|
||||||
|
draggingPointPreviousLocation = curve.polylinePoints[draggingIndex]
|
||||||
|
newPointGenerated = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
|
if testStage == .drawingStage {
|
||||||
|
let altitude = touches.first!.altitudeAngle
|
||||||
|
let force = (touches.first!.force/touches.first!.maximumPossibleForce)/sin(altitude)
|
||||||
|
pressures.append(force)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard
|
||||||
|
let touch = touches.first,
|
||||||
|
let idx = draggingIndex
|
||||||
|
else { return }
|
||||||
|
|
||||||
|
let location = touch.location(in: self)
|
||||||
|
var point = unmap(location)
|
||||||
|
|
||||||
|
if draggingIndex == 0 {
|
||||||
|
let nextPoint = curve.polylinePoints[idx+1]
|
||||||
|
point.x = min(max(point.x, 0), nextPoint.x)
|
||||||
|
// point.x = 0
|
||||||
|
point.y = min(max(point.y, 0), 1)
|
||||||
|
}
|
||||||
|
else if draggingIndex == curve.polylinePoints.count-1 {
|
||||||
|
point.x = 1
|
||||||
|
point.y = min(max(point.y, 0), 1)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let nextPoint = curve.polylinePoints[idx+1]
|
||||||
|
let previousPoint = curve.polylinePoints[idx-1]
|
||||||
|
point.x = min(max(point.x, previousPoint.x), nextPoint.x)
|
||||||
|
point.y = min(max(point.y, 0), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// print("currenPoint \(point)")
|
||||||
|
|
||||||
|
curve.polylinePoints[idx] = point
|
||||||
|
setNeedsDisplay()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
|
if testStage == .drawingStage {
|
||||||
|
if pressures.isEmpty { return }
|
||||||
|
curve.polylinePoints = PressureCurve.generateLinearCurvePoints(from: pressures)
|
||||||
|
testStage = .curveStage
|
||||||
|
showGraph = true
|
||||||
|
let parentVC = parentViewController as! PressureCurveViewController
|
||||||
|
parentVC.setupNavigationBar()
|
||||||
|
// parentVC.showCurveStageTips()
|
||||||
|
}
|
||||||
|
draggingIndex = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
|
draggingIndex = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
final class PressureCurveLUT {
|
||||||
|
static let levels = 2048
|
||||||
|
public let table: [Float]
|
||||||
|
|
||||||
|
init(curve: PressureCurve) {
|
||||||
|
curve.buildCurveSegments()
|
||||||
|
|
||||||
|
var t = [Float](repeating: 0, count: Self.levels)
|
||||||
|
|
||||||
|
let maxIndex = Float(Self.levels - 1)
|
||||||
|
|
||||||
|
for i in 0..<Self.levels {
|
||||||
|
let x = CGFloat(Float(i) / maxIndex)
|
||||||
|
let y = curve.value(at: x)
|
||||||
|
t[i] = Float(y)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.table = t
|
||||||
|
}
|
||||||
|
|
||||||
|
@inline(__always)
|
||||||
|
func value(at x: Float) -> Float {
|
||||||
|
let clamped = min(max(x, 0), 1)
|
||||||
|
let index = Int((clamped * Float(Self.levels - 1)).rounded())
|
||||||
|
return table[index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - ViewController
|
||||||
|
class PressureCurveViewController: UIViewController, UIGestureRecognizerDelegate {
|
||||||
|
|
||||||
|
private var navBar = UINavigationBar()
|
||||||
|
private var saveButton: UIBarButtonItem = UIBarButtonItem()
|
||||||
|
|
||||||
|
private var container = UIView()
|
||||||
|
private var curveView = PressureCurveView()
|
||||||
|
// private var initialTouchCurvePoints: [CGPoint] = []
|
||||||
|
private var strokePressureCurvePoints: [CGPoint] = []
|
||||||
|
|
||||||
|
private var phase1StrokeSampleIndexEnd: Int32 = 0
|
||||||
|
private var phase2StrokeSampleIndexEnd: Int32 = 0
|
||||||
|
|
||||||
|
private var minPressure: CGFloat = 0
|
||||||
|
private var maxPressure: CGFloat = 1
|
||||||
|
private var isFirstLaunch: Bool = GenericUtils.isFirstLaunchPressureCurveTool()
|
||||||
|
|
||||||
|
// private var curvePhaseButton: UIBarButtonItem = UIBarButtonItem()
|
||||||
|
private let hookFilterLabel = UILabel()
|
||||||
|
private let hookFilterSlider = UISlider()
|
||||||
|
private let strokeEqualizationDepthLabel = UILabel()
|
||||||
|
private let strokeEqualizationDepthSlider = UISlider()
|
||||||
|
private let strokeEqualizationStrengthLabel = UILabel()
|
||||||
|
private let strokeEqualizationStrengthSlider = UISlider()
|
||||||
|
|
||||||
|
enum PressureCurvePhase: UInt8, CaseIterable {
|
||||||
|
case initialTouch
|
||||||
|
case stroke
|
||||||
|
func next() -> PressureCurvePhase {
|
||||||
|
let all = Self.allCases
|
||||||
|
let index = all.firstIndex(of: self)!
|
||||||
|
let nextIndex = (index + 1) % all.count
|
||||||
|
return all[nextIndex]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private(set) var curvePhase: PressureCurvePhase = .initialTouch
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
self.view.isOpaque = false
|
||||||
|
self.view.backgroundColor = UIColor.clear // 半透明遮罩
|
||||||
|
|
||||||
|
let curveWidth: CGFloat = 530
|
||||||
|
let curveHeight: CGFloat = 530
|
||||||
|
curveView = PressureCurveView(frame: CGRect(x: 0, y: 0, width: curveWidth, height: curveHeight))
|
||||||
|
curveView.center = CGPoint(x: self.view.center.x, y: self.view.center.y)
|
||||||
|
curveView.backgroundColor = .white
|
||||||
|
curveView.layer.cornerRadius = 0
|
||||||
|
curveView.layer.masksToBounds = true
|
||||||
|
|
||||||
|
container = UIView(frame: CGRect(x: 0, y: 0, width: curveWidth+10, height: curveHeight+152))
|
||||||
|
container.center = CGPoint(x: self.view.bounds.midX, y: self.view.bounds.midY+6.5)
|
||||||
|
container.backgroundColor = .white
|
||||||
|
container.layer.cornerRadius = 12
|
||||||
|
container.layer.masksToBounds = true
|
||||||
|
container.layer.borderWidth = 0.5
|
||||||
|
container.layer.borderColor = UIColor.black.withAlphaComponent(0.15).cgColor
|
||||||
|
container.addSubview(curveView)
|
||||||
|
|
||||||
|
self.setupNavigationBar()
|
||||||
|
self.setupBottomSliders()
|
||||||
|
self.loadPersistedCurve()
|
||||||
|
self.curveView.showGraph = true
|
||||||
|
|
||||||
|
|
||||||
|
self.view.addSubview(container)
|
||||||
|
|
||||||
|
curveView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
curveView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
|
||||||
|
curveView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor,constant: -25),
|
||||||
|
curveView.widthAnchor.constraint(equalToConstant: curveWidth),
|
||||||
|
curveView.heightAnchor.constraint(equalToConstant: curveHeight)
|
||||||
|
])
|
||||||
|
|
||||||
|
self.displayCurve()
|
||||||
|
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(pencilProPurchaseAborted(_:)),
|
||||||
|
name: AddOnProduct.PencilProPack.purchaseAbortedNotification(),
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(persistCurve),
|
||||||
|
name: AddOnProduct.PencilProPack.purchaseSucceededNotification(),
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
let tap = UITapGestureRecognizer(target: self, action: #selector(dismissSelf))
|
||||||
|
tap.delegate = self
|
||||||
|
view.addGestureRecognizer(tap)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
/*
|
||||||
|
AlertControllerUtil.cancelButtonString = LocalizationHelper.localizedString(forKey: "No")
|
||||||
|
AlertControllerUtil.showAlert(
|
||||||
|
in: self,
|
||||||
|
title: LocalizationHelper.localizedString(forKey: "Pen Pressure Curve"),
|
||||||
|
message: LocalizationHelper.localizedString(forKey:"Reset the pen pressure curve by starting from the pen pressure test?"),
|
||||||
|
withCancel: true,
|
||||||
|
buttonTitle: LocalizationHelper.localizedString(forKey: "Yes"),
|
||||||
|
countdown: 0,
|
||||||
|
action: {},
|
||||||
|
completion: {
|
||||||
|
if AlertControllerUtil.actionCancelled {
|
||||||
|
self.curveView.showGraph = true
|
||||||
|
self.curveView.testStage = .curveStage
|
||||||
|
self.curveView.setNeedsDisplay()
|
||||||
|
self.setupNavigationBar()
|
||||||
|
self.showCurveStageTips()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
self.pressureRangeTest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) */
|
||||||
|
if isFirstLaunch {
|
||||||
|
showCurveStageTips()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillDisappear(_ animated: Bool) {
|
||||||
|
NotificationCenter.default.removeObserver(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadPersistedCurve() {
|
||||||
|
let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero)
|
||||||
|
let oscProfile = oscProfileMan.getSelectedProfile()
|
||||||
|
// initialTouchCurvePoints = PressureCurve.importCurvePoints(oscProfile.initialTouchPressureCurvePoints)
|
||||||
|
strokePressureCurvePoints = PressureCurve.importCurvePoints(oscProfile.pressureCurvePoints)
|
||||||
|
displayCurve()
|
||||||
|
|
||||||
|
hookFilterSlider.value = Float(oscProfile.phase1StrokeSampleIndexEnd)
|
||||||
|
strokeEqualizationDepthSlider.value = Float(oscProfile.phase2StrokeSampleIndexEnd)
|
||||||
|
strokeEqualizationStrengthSlider.value = Float(oscProfile.strokeEqualizationStrength)
|
||||||
|
|
||||||
|
strokeEqualizationDepthSliderMoved(strokeEqualizationDepthSlider)
|
||||||
|
hookFilterDepthSliderMoved(hookFilterSlider)
|
||||||
|
strokeEqualizationStrengthSliderMoved(strokeEqualizationStrengthSlider)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func displayCurve() {
|
||||||
|
switch curvePhase {
|
||||||
|
case .initialTouch:
|
||||||
|
break
|
||||||
|
// curveView.curve.polylinePoints = initialTouchCurvePoints
|
||||||
|
case .stroke:
|
||||||
|
curveView.curve.polylinePoints = strokePressureCurvePoints
|
||||||
|
}
|
||||||
|
curveView.setNeedsDisplay()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupBottomSliders() {
|
||||||
|
|
||||||
|
hookFilterLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
hookFilterSlider.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
strokeEqualizationDepthLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
strokeEqualizationDepthSlider.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
strokeEqualizationStrengthLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
strokeEqualizationStrengthSlider.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
let traits = UITraitCollection(userInterfaceStyle: .light)
|
||||||
|
|
||||||
|
hookFilterLabel.font = UIFont.systemFont(ofSize: 14)
|
||||||
|
hookFilterLabel.textColor = .black
|
||||||
|
if PublicUtils.liquidGlassEnabled {
|
||||||
|
if #available(iOS 13.0, *) {hookFilterSlider.maximumTrackTintColor = .tertiarySystemFill.resolvedColor(with: traits)}
|
||||||
|
}
|
||||||
|
hookFilterSlider.minimumValue = 1
|
||||||
|
hookFilterSlider.maximumValue = 20
|
||||||
|
hookFilterSlider.addTarget(self, action: #selector(hookFilterDepthSliderMoved(_:)), for: .valueChanged)
|
||||||
|
|
||||||
|
strokeEqualizationDepthLabel.font = UIFont.systemFont(ofSize: 14)
|
||||||
|
strokeEqualizationDepthLabel.textColor = .black
|
||||||
|
if PublicUtils.liquidGlassEnabled {
|
||||||
|
if #available(iOS 13.0, *) {strokeEqualizationDepthSlider.maximumTrackTintColor = .tertiarySystemFill.resolvedColor(with: traits)}
|
||||||
|
}
|
||||||
|
strokeEqualizationDepthSlider.minimumValue = 1
|
||||||
|
strokeEqualizationDepthSlider.maximumValue = 80
|
||||||
|
strokeEqualizationDepthSlider.addTarget(self, action: #selector(strokeEqualizationDepthSliderMoved(_:)), for: .valueChanged)
|
||||||
|
|
||||||
|
strokeEqualizationStrengthLabel.font = UIFont.systemFont(ofSize: 14)
|
||||||
|
strokeEqualizationStrengthLabel.textColor = .black
|
||||||
|
if PublicUtils.liquidGlassEnabled {
|
||||||
|
if #available(iOS 13.0, *) {strokeEqualizationStrengthSlider.maximumTrackTintColor = .tertiarySystemFill.resolvedColor(with: traits)}
|
||||||
|
}
|
||||||
|
strokeEqualizationStrengthSlider.minimumValue = 0.1
|
||||||
|
strokeEqualizationStrengthSlider.maximumValue = 3.5
|
||||||
|
strokeEqualizationStrengthSlider.addTarget(self, action: #selector(strokeEqualizationStrengthSliderMoved(_:)), for: .valueChanged)
|
||||||
|
|
||||||
|
container.addSubview(hookFilterLabel)
|
||||||
|
container.addSubview(hookFilterSlider)
|
||||||
|
container.addSubview(strokeEqualizationDepthLabel)
|
||||||
|
container.addSubview(strokeEqualizationDepthSlider)
|
||||||
|
container.addSubview(strokeEqualizationStrengthLabel)
|
||||||
|
container.addSubview(strokeEqualizationStrengthSlider)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
|
||||||
|
// Slider
|
||||||
|
hookFilterLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 16),
|
||||||
|
hookFilterLabel.widthAnchor.constraint(equalToConstant: 260),
|
||||||
|
hookFilterSlider.leadingAnchor.constraint(equalTo: hookFilterLabel.trailingAnchor, constant: 12),
|
||||||
|
hookFilterSlider.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -16),
|
||||||
|
hookFilterSlider.topAnchor.constraint(equalTo: curveView.bottomAnchor, constant: 0),
|
||||||
|
hookFilterLabel.centerYAnchor.constraint(equalTo: hookFilterSlider.centerYAnchor, constant:0),
|
||||||
|
|
||||||
|
strokeEqualizationDepthLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 16),
|
||||||
|
strokeEqualizationDepthLabel.widthAnchor.constraint(equalToConstant: 260),
|
||||||
|
strokeEqualizationDepthSlider.leadingAnchor.constraint(equalTo: strokeEqualizationDepthLabel.trailingAnchor, constant: 12),
|
||||||
|
strokeEqualizationDepthSlider.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -16),
|
||||||
|
strokeEqualizationDepthSlider.topAnchor.constraint(equalTo: hookFilterSlider.bottomAnchor, constant: 0),
|
||||||
|
strokeEqualizationDepthLabel.centerYAnchor.constraint(equalTo: strokeEqualizationDepthSlider.centerYAnchor, constant:0),
|
||||||
|
|
||||||
|
strokeEqualizationStrengthLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 16),
|
||||||
|
strokeEqualizationStrengthLabel.widthAnchor.constraint(equalToConstant: 260),
|
||||||
|
strokeEqualizationStrengthSlider.leadingAnchor.constraint(equalTo: strokeEqualizationStrengthLabel.trailingAnchor, constant: 12),
|
||||||
|
strokeEqualizationStrengthSlider.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -16),
|
||||||
|
strokeEqualizationStrengthSlider.topAnchor.constraint(equalTo: strokeEqualizationDepthSlider.bottomAnchor, constant: 0),
|
||||||
|
strokeEqualizationStrengthLabel.centerYAnchor.constraint(equalTo: strokeEqualizationStrengthSlider.centerYAnchor, constant:0),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func hookFilterDepthSliderMoved(_ sender: UISlider) {
|
||||||
|
phase1StrokeSampleIndexEnd = Int32(sender.value.rounded())
|
||||||
|
hookFilterLabel.text = "\(LocalizationHelper.localizedString(forKey: "Stroke start hook filtering depth: "))\(String(format: "%d", Int32(sender.value)))";
|
||||||
|
if sender.value > strokeEqualizationDepthSlider.value {
|
||||||
|
strokeEqualizationDepthSlider.value = sender.value
|
||||||
|
strokeEqualizationDepthSliderMoved(strokeEqualizationDepthSlider)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func strokeEqualizationDepthSliderMoved(_ sender: UISlider) {
|
||||||
|
sender.value = max(sender.value, hookFilterSlider.value)
|
||||||
|
phase2StrokeSampleIndexEnd = Int32(sender.value.rounded())
|
||||||
|
strokeEqualizationDepthLabel.text = "\(LocalizationHelper.localizedString(forKey: "Stroke start equalization depth: "))\(String(format: "%d", Int32(sender.value)))";
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func strokeEqualizationStrengthSliderMoved(_ sender: UISlider) {
|
||||||
|
// sender.value = sender.value
|
||||||
|
strokeEqualizationStrengthLabel.text = "\(LocalizationHelper.localizedString(forKey: "Stroke start force scaling: "))\(String(format: "%.2f", sender.value))";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public func setupNavigationBar() {
|
||||||
|
// 1. 创建 UINavigationBar
|
||||||
|
navBar.removeFromSuperview()
|
||||||
|
navBar = UINavigationBar()
|
||||||
|
navBar.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
container.addSubview(navBar)
|
||||||
|
|
||||||
|
var navItem = UINavigationItem()
|
||||||
|
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
let appearance = UINavigationBarAppearance()
|
||||||
|
// appearance.configureWithTransparentBackground() // 透明背景
|
||||||
|
appearance.shadowColor = nil // 去掉底部细线
|
||||||
|
appearance.backgroundColor = .white // 可以额外设置完全透明
|
||||||
|
|
||||||
|
navBar.standardAppearance = appearance
|
||||||
|
navBar.scrollEdgeAppearance = appearance
|
||||||
|
} else {
|
||||||
|
navBar.setBackgroundImage(UIImage(), for: .default)
|
||||||
|
navBar.shadowImage = UIImage() // 去掉底部线
|
||||||
|
navBar.isTranslucent = true
|
||||||
|
navBar.backgroundColor = .clear
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 添加约束 (顶部,左右)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
navBar.topAnchor.constraint(equalTo: container.safeAreaLayoutGuide.topAnchor),
|
||||||
|
navBar.leadingAnchor.constraint(equalTo: container.leadingAnchor),
|
||||||
|
navBar.trailingAnchor.constraint(equalTo: container.trailingAnchor),
|
||||||
|
navBar.heightAnchor.constraint(equalToConstant: 50) // 导航栏标准高度
|
||||||
|
])
|
||||||
|
|
||||||
|
if curveView.testStage == .curveStage {
|
||||||
|
let resetButton = UIBarButtonItem(title: LocalizationHelper.localizedString(forKey: "Reset"), style: .plain, target: self, action: #selector(resetTapped))
|
||||||
|
let undoButton = UIBarButtonItem(title: LocalizationHelper.localizedString(forKey: "Undo"), style: .plain, target: self, action: #selector(undoTapped))
|
||||||
|
let rangeTestButton = UIBarButtonItem(title: LocalizationHelper.localizedString(forKey: "Pressure-range-test"), style: .plain, target: self, action: #selector(pressureRangeTest))
|
||||||
|
// curvePhaseButton = UIBarButtonItem(title: LocalizationHelper.localizedString(forKey: "Initial-touch-curve"), style: .plain, target: self, action: #selector(curvePhaseButtonTapped))
|
||||||
|
curvePhase = .stroke
|
||||||
|
saveButton = UIBarButtonItem(title: LocalizationHelper.localizedString(forKey: "Save"), style: .plain, target: self, action: #selector(saveTapped))
|
||||||
|
// let quitButton = UIBarButtonItem(title: LocalizationHelper.localizedString(forKey: "Exit"), style: .plain, target: self, action: #selector(exitTapped))
|
||||||
|
navItem.leftBarButtonItems = [resetButton, undoButton, rangeTestButton,]
|
||||||
|
navItem.rightBarButtonItems = [saveButton]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if curveView.testStage == .drawingStage {
|
||||||
|
navItem = UINavigationItem(title: LocalizationHelper.localizedString(forKey: "Pressure Range Test"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if #available(iOS 26.0, *) {
|
||||||
|
if let buttons = navItem.leftBarButtonItems {
|
||||||
|
for button in buttons {
|
||||||
|
button.hidesSharedBackground = true
|
||||||
|
button.tintColor = .tintColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let buttons = navItem.rightBarButtonItems {
|
||||||
|
for button in buttons {
|
||||||
|
button.hidesSharedBackground = true
|
||||||
|
button.tintColor = .tintColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
navBar.items = [navItem]
|
||||||
|
}
|
||||||
|
|
||||||
|
public func showCurveStageTips() {
|
||||||
|
AlertControllerUtil.showAlert(
|
||||||
|
in: self,
|
||||||
|
title: LocalizationHelper.localizedString(forKey: "Pen Pressure Curve"),
|
||||||
|
message: LocalizationHelper.localizedString(forKey:"Adjust the pressure curve by adding or dragging the purple squares."),
|
||||||
|
withCancel: false,
|
||||||
|
buttonTitle: LocalizationHelper.localizedString(forKey: "This tip won't be shown again"),
|
||||||
|
countdown: 6
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func resetTapped() {
|
||||||
|
curveView.curve.polylinePoints = [CGPoint(x: 0, y: 0), CGPoint(x: 1, y: 1)]
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.curveView.setNeedsDisplay()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func undoTapped() {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.curveView.undoLastMove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func pencilProPurchaseAborted(_ notification: Notification) {
|
||||||
|
guard let interruption = notification.object as? PurchaseInterruption else { return }
|
||||||
|
if interruption != .learnMore {
|
||||||
|
resetTapped()
|
||||||
|
}
|
||||||
|
if interruption == .lowOSVersion {
|
||||||
|
AlertControllerUtil.showAlert(
|
||||||
|
in: self,
|
||||||
|
title: "",
|
||||||
|
message: LocalizationHelper.localizedString(forKey:"PencilProPackLowOSVersionTip"),
|
||||||
|
withCancel: false,
|
||||||
|
buttonTitle: LocalizationHelper.localizedString(forKey: "OK"),
|
||||||
|
countdown: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func pressureRangeTest() {
|
||||||
|
AlertControllerUtil.showAlert(
|
||||||
|
in: self,
|
||||||
|
title: LocalizationHelper.localizedString(forKey: "Pressure Range Test"),
|
||||||
|
message: LocalizationHelper.localizedString(forKey:"pressureRangeTestTip"),
|
||||||
|
withCancel: true,
|
||||||
|
buttonTitle: LocalizationHelper.localizedString(forKey: "OK"),
|
||||||
|
countdown: 0,
|
||||||
|
completion:{
|
||||||
|
if AlertControllerUtil.actionCancelled { return }
|
||||||
|
self.resetTapped()
|
||||||
|
self.curveView.showGraph = false
|
||||||
|
self.curveView.testStage = .drawingStage
|
||||||
|
self.curveView.setNeedsDisplay()
|
||||||
|
self.setupNavigationBar()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
@objc private func curvePhaseButtonTapped() {
|
||||||
|
switch curvePhase {
|
||||||
|
case .initialTouch:
|
||||||
|
// initialTouchCurvePoints = curveView.curve.polylinePoints
|
||||||
|
curvePhaseButton.title = LocalizationHelper.localizedString(forKey:"Initial-touch-curve")
|
||||||
|
case .stroke:
|
||||||
|
strokeCurvePoints = curveView.curve.polylinePoints
|
||||||
|
curvePhaseButton.title = LocalizationHelper.localizedString(forKey:"Stroke-curve")
|
||||||
|
}
|
||||||
|
curvePhase = curvePhase.next()
|
||||||
|
displayCurve()
|
||||||
|
} */
|
||||||
|
|
||||||
|
@objc private func persistCurve() {
|
||||||
|
let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero)
|
||||||
|
let selectedProfile = oscProfileMan.getSelectedProfile()
|
||||||
|
|
||||||
|
switch curvePhase {
|
||||||
|
case .initialTouch:
|
||||||
|
break
|
||||||
|
// initialTouchCurvePoints = curveView.curve.polylinePoints
|
||||||
|
case .stroke:
|
||||||
|
strokePressureCurvePoints = curveView.curve.polylinePoints
|
||||||
|
}
|
||||||
|
|
||||||
|
curveView.curve.polylinePoints = strokePressureCurvePoints
|
||||||
|
selectedProfile.pressureCurvePoints = PressureCurve.exportCurvePoints(curveView.curve)
|
||||||
|
// curveView.curve.polylinePoints = initialTouchCurvePoints
|
||||||
|
// selectedProfile.initialTouchPressureCurvePoints = PressureCurve.exportCurvePoints(curveView.curve)
|
||||||
|
|
||||||
|
displayCurve()
|
||||||
|
|
||||||
|
selectedProfile.phase1StrokeSampleIndexEnd = Int32(hookFilterSlider.value)
|
||||||
|
selectedProfile.phase2StrokeSampleIndexEnd = Int32(strokeEqualizationDepthSlider.value)
|
||||||
|
selectedProfile.strokeEqualizationStrength = CGFloat(strokeEqualizationStrengthSlider.value)
|
||||||
|
|
||||||
|
oscProfileMan.replaceSelectedProfile(with: selectedProfile, overwriteDefault: true)
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if self.isFirstLaunch {
|
||||||
|
AlertControllerUtil.showAlert(
|
||||||
|
in: self,
|
||||||
|
title: LocalizationHelper.localizedString(forKey: "Pen Pressure Curve"),
|
||||||
|
message: LocalizationHelper.localizedString(forKey:"firstPressureCurvePersistTip"),
|
||||||
|
withCancel: false,
|
||||||
|
buttonTitle: LocalizationHelper.localizedString(forKey: "This tip won't be shown again"),
|
||||||
|
countdown: 11,
|
||||||
|
completion: {
|
||||||
|
PencilHandler.shared?.setupPressureLUT()
|
||||||
|
})
|
||||||
|
self.isFirstLaunch = false
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
AlertControllerUtil.autoCompletion = true
|
||||||
|
AlertControllerUtil.showAlert(
|
||||||
|
in: self,
|
||||||
|
title: LocalizationHelper.localizedString(forKey: ""),
|
||||||
|
message: LocalizationHelper.localizedString(forKey:"Pressure curve saved"),
|
||||||
|
withCancel: false,
|
||||||
|
buttonTitle: "",
|
||||||
|
countdown: 1,
|
||||||
|
completion: {
|
||||||
|
PencilHandler.shared?.setupPressureLUT()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func saveTapped() {
|
||||||
|
saveButton.isEnabled = false
|
||||||
|
IAPManager.checkPurchaseInfo(.PencilProPack) { info in
|
||||||
|
self.saveButton.isEnabled = true
|
||||||
|
if info.valid {
|
||||||
|
self.persistCurve()
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
IAPManager.inAppPurchaseAction(viewController: self, product: .PencilProPack)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func exitTapped() {
|
||||||
|
self.dismiss(animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func gestureRecognizer(
|
||||||
|
_ gestureRecognizer: UIGestureRecognizer,
|
||||||
|
shouldReceive touch: UITouch
|
||||||
|
) -> Bool {
|
||||||
|
return touch.view == view
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func dismissSelf(){
|
||||||
|
dismiss(animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,9 @@
|
|||||||
NS_ASSUME_NONNULL_BEGIN
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
@interface PureNativeTouchHandler : UIResponder
|
@interface PureNativeTouchHandler : UIResponder
|
||||||
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings;
|
|
||||||
|
|
||||||
|
- (id)initWithView:(StreamView*)view settings:(TemporarySettings*)settings profile:(OSCProfile *)profile;
|
||||||
|
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -11,15 +11,14 @@
|
|||||||
#import "NativeTouchPointer.h"
|
#import "NativeTouchPointer.h"
|
||||||
#import "OnScreenControls.h"
|
#import "OnScreenControls.h"
|
||||||
#import "StreamView.h"
|
#import "StreamView.h"
|
||||||
|
#import "VoidLink-Swift.h"
|
||||||
|
|
||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
|
|
||||||
@implementation PureNativeTouchHandler {
|
@implementation PureNativeTouchHandler {
|
||||||
StreamView* streamView;
|
__weak StreamView* streamView;
|
||||||
TemporarySettings* currentSettings;
|
|
||||||
bool activateCoordSelector;
|
bool activateCoordSelector;
|
||||||
CGFloat pointerVelocityDividerLocationByPoints;
|
CGFloat pointerVelocityDividerLocationByPoints;
|
||||||
uint16_t touchMoveEventIntervalUs;
|
|
||||||
|
|
||||||
bool asyncNativeTouch;
|
bool asyncNativeTouch;
|
||||||
unsigned int touchDownQos;
|
unsigned int touchDownQos;
|
||||||
@@ -34,29 +33,38 @@
|
|||||||
NSMutableSet<NSNumber *> *pointerIdPool; //pre-defined pool of pointerIds.
|
NSMutableSet<NSNumber *> *pointerIdPool; //pre-defined pool of pointerIds.
|
||||||
NSMutableSet<NSNumber *> *unassignedPointerIds;
|
NSMutableSet<NSNumber *> *unassignedPointerIds;
|
||||||
NSMutableSet *blacklistedTouches;
|
NSMutableSet *blacklistedTouches;
|
||||||
|
|
||||||
|
bool trackPointEnabled;
|
||||||
|
NSMutableArray<CAShapeLayer *> * trackPointPool;
|
||||||
|
|
||||||
NSMutableDictionary *pointerObjDict;
|
NSMutableDictionary *pointerObjDict;
|
||||||
|
|
||||||
CGFloat slideGestureVerticalThreshold;
|
CGFloat slideGestureVerticalThreshold;
|
||||||
CGFloat screenWidthWithThreshold;
|
CGFloat screenWidthWithThreshold;
|
||||||
CGFloat EDGE_TOLERANCE;
|
CGFloat _edgeTolerance;
|
||||||
|
|
||||||
CGRect streamViewBounds;
|
CGRect streamViewBounds;
|
||||||
|
|
||||||
|
int64_t moveEventIntervalNSec;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings{
|
- (id)initWithView:(StreamView*)view settings:(TemporarySettings*)settings profile:(OSCProfile *)profile{
|
||||||
self = [super init];
|
self = [super init];
|
||||||
self->streamView = view;
|
self->streamView = view;
|
||||||
self->currentSettings = settings;
|
|
||||||
self->activateCoordSelector = currentSettings.pointerVelocityModeDivider.floatValue != 1.0;
|
self->activateCoordSelector = profile.pointerVelocityModeDivider != 1.0;
|
||||||
self->touchMoveEventIntervalUs = currentSettings.touchMoveEventInterval.intValue;
|
self->moveEventIntervalNSec = (int64_t)(settings.touchMoveEventInterval.intValue * 1000);;
|
||||||
self->streamViewBounds = view.bounds;
|
self->streamViewBounds = view.bounds;
|
||||||
|
|
||||||
self->pointerIdDict = [NSMutableDictionary dictionary];
|
self->pointerIdDict = [NSMutableDictionary dictionary];
|
||||||
self->pointerIdPool = [NSMutableSet set];
|
self->pointerIdPool = [NSMutableSet set];
|
||||||
|
self->trackPointEnabled = settings.touchPointTracking;
|
||||||
|
self->trackPointPool = [NSMutableArray array];
|
||||||
for (uint8_t i = 0; i <= 10; i++) { //ipadOS supports upto 11 finger touches
|
for (uint8_t i = 0; i <= 10; i++) { //ipadOS supports upto 11 finger touches
|
||||||
[self->pointerIdPool addObject:@(i)];
|
[self->pointerIdPool addObject:@(i)];
|
||||||
|
[self->trackPointPool addObject:[GraphicUtils makeTouchTrackpointIn:streamView]];
|
||||||
}
|
}
|
||||||
|
|
||||||
self->activePointerIds = [NSMutableSet set];
|
self->activePointerIds = [NSMutableSet set];
|
||||||
self->blacklistedTouches = [NSMutableSet set];
|
self->blacklistedTouches = [NSMutableSet set];
|
||||||
|
|
||||||
@@ -81,16 +89,15 @@
|
|||||||
default: break;
|
default: break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
self->pointerObjDict = [NSMutableDictionary dictionary];
|
self->pointerObjDict = [NSMutableDictionary dictionary];
|
||||||
|
|
||||||
EDGE_TOLERANCE = 10.0;
|
_edgeTolerance = settings.edgeSlidingSensitivity.floatValue;
|
||||||
slideGestureVerticalThreshold = CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.4;
|
slideGestureVerticalThreshold = CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.4;
|
||||||
screenWidthWithThreshold = CGRectGetWidth([[UIScreen mainScreen] bounds]) - EDGE_TOLERANCE;
|
screenWidthWithThreshold = CGRectGetWidth([[UIScreen mainScreen] bounds]) - _edgeTolerance;
|
||||||
|
|
||||||
self->pointerVelocityDividerLocationByPoints = self->streamView.bounds.size.width * settings.pointerVelocityModeDivider.floatValue;
|
self->pointerVelocityDividerLocationByPoints = self->streamView.bounds.size.width * profile.pointerVelocityModeDivider;
|
||||||
|
|
||||||
[NativeTouchPointer initContextWithView:self->streamView andSettings:settings];
|
[NativeTouchPointer initContextWithView:self->streamView profile:profile];
|
||||||
//_touchesCapturedByOnScreenButtons = [[NSMutableSet alloc] init];
|
//_touchesCapturedByOnScreenButtons = [[NSMutableSet alloc] init];
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
@@ -163,7 +170,7 @@
|
|||||||
|
|
||||||
//check if touch point is spawned on the left or right upper half screen edges, event to remote PC. this is for better handling in-stream slide gesture
|
//check if touch point is spawned on the left or right upper half screen edges, event to remote PC. this is for better handling in-stream slide gesture
|
||||||
CGPoint initialPoint = [touch locationInView:self->streamView];
|
CGPoint initialPoint = [touch locationInView:self->streamView];
|
||||||
if(initialPoint.y < slideGestureVerticalThreshold && (initialPoint.x < EDGE_TOLERANCE || initialPoint.x > screenWidthWithThreshold)) {
|
if(initialPoint.y < slideGestureVerticalThreshold && (initialPoint.x < _edgeTolerance || initialPoint.x > screenWidthWithThreshold)) {
|
||||||
[blacklistedTouches addObject:touchAddrObj];
|
[blacklistedTouches addObject:touchAddrObj];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -187,7 +194,9 @@
|
|||||||
|
|
||||||
|
|
||||||
- (void)sendTouchEvent:(UITouch*)touch withTouchtype:(uint8_t)touchType{
|
- (void)sendTouchEvent:(UITouch*)touch withTouchtype:(uint8_t)touchType{
|
||||||
//if(touchPointSpawnedAtUpperScreenEdge && touchType != LI_TOUCH_EVENT_UP) return; // we're done here. this touch event will not be sent to the remote PC. and this must be checked after coord selector finishes populating new relative coords, or the app will crash
|
//if(touchPointSpawnedAtUpperScreenEdge && touchType != LI_TOUCH_EVENT_UP) return; // we're done here. this touch event will not be sent to the remote PC. and this must be checked after coord selector finishes populating new relative coords, or the app will crash
|
||||||
|
if(!touch) return;
|
||||||
|
|
||||||
if([blacklistedTouches containsObject:@((uintptr_t)touch)]) return;
|
if([blacklistedTouches containsObject:@((uintptr_t)touch)]) return;
|
||||||
|
|
||||||
CGPoint targetCoords;
|
CGPoint targetCoords;
|
||||||
@@ -202,72 +211,83 @@
|
|||||||
CGFloat normalizedY = location.y / videoSize.height;
|
CGFloat normalizedY = location.y / videoSize.height;
|
||||||
uint8_t pointerId = [self retrievePointerIdFromDict:touch];
|
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_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);
|
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]]);
|
}else LiSendTouchEvent(touchType, pointerId, normalizedX, normalizedY,(touch.force / touch.maximumPossibleForce) / sin(touch.altitudeAngle),0.0f, 0.0f,[self getRotationFromAzimuthAngle:[touch azimuthAngleInView:streamView]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
|
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||||
if (asyncNativeTouch) dispatch_async(dispatch_get_global_queue(touchDownQos, 0), ^{
|
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
||||||
for (UITouch* touch in touches){
|
for (UITouch* touch in touches){
|
||||||
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
|
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
|
||||||
[self handleTouchDown:touch]; //generate & populate pointerId
|
[self handleTouchDown:touch]; //generate & populate pointerId
|
||||||
if(self->activateCoordSelector) [self populatePointerObjIntoDict:touch];
|
if(self->activateCoordSelector) [self populatePointerObjIntoDict:touch];
|
||||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_DOWN];
|
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_DOWN];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(self->trackPointEnabled){
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
[CATransaction begin];
|
||||||
|
[CATransaction setDisableActions:YES];
|
||||||
|
for (UITouch* touch in touches){
|
||||||
|
uint8_t pointerId = [self retrievePointerIdFromDict:touch];
|
||||||
|
CAShapeLayer* trackPoint = self->trackPointPool[pointerId];
|
||||||
|
trackPoint.position = [touch locationInView:self->streamView];
|
||||||
|
trackPoint.hidden = false;
|
||||||
|
}
|
||||||
|
[CATransaction commit];
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
else{
|
|
||||||
for (UITouch* touch in touches){
|
|
||||||
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
|
|
||||||
[self handleTouchDown:touch]; //generate & populate pointerId
|
|
||||||
if(self->activateCoordSelector) [self populatePointerObjIntoDict:touch];
|
|
||||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_DOWN];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
|
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||||
if (asyncNativeTouch) dispatch_async(dispatch_get_global_queue(touchMoveQos, 0), ^{
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, moveEventIntervalNSec), dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
||||||
for (UITouch* touch in touches){
|
for (UITouch* touch in touches){
|
||||||
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
|
|
||||||
if(self->activateCoordSelector) [self updatePointerObjInDict:touch];
|
if(self->activateCoordSelector) [self updatePointerObjInDict:touch];
|
||||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_MOVE];
|
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_MOVE];
|
||||||
[[self getPointerObjFromDict:touch] doesNeedResetCoords]; // execute the judging of doesReachBoundary for current pointer instance. (happens after the event is sent to Sunshine service)
|
[[self getPointerObjFromDict:touch] doesNeedResetCoords];
|
||||||
usleep(self->touchMoveEventIntervalUs);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
else {
|
|
||||||
for (UITouch* touch in touches){
|
if(self->trackPointEnabled){
|
||||||
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
if(self->activateCoordSelector) [self updatePointerObjInDict:touch];
|
[CATransaction begin];
|
||||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_MOVE];
|
[CATransaction setDisableActions:YES];
|
||||||
[[self getPointerObjFromDict:touch] doesNeedResetCoords]; // execute the judging of doesReachBoundary for current pointer instance. (happens after the event is sent to Sunshine service)
|
for (UITouch* touch in touches){
|
||||||
// usleep(self->touchMoveEventIntervalUs);
|
uint8_t pointerId = [self retrievePointerIdFromDict:touch];
|
||||||
}
|
CAShapeLayer* trackPoint = self->trackPointPool[pointerId];
|
||||||
|
trackPoint.position = [touch locationInView:self->streamView];
|
||||||
|
}
|
||||||
|
[CATransaction commit];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
|
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||||
if(asyncNativeTouch) dispatch_async(dispatch_get_global_queue(touchEndQos, 0), ^{
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, moveEventIntervalNSec), dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
||||||
for (UITouch* touch in touches){
|
for (UITouch* touch in touches){
|
||||||
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
|
|
||||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_UP]; //send touch event before remove pointerId
|
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_UP]; //send touch event before remove pointerId
|
||||||
[self removePointerId:touch]; //then remove pointerId
|
|
||||||
|
if(self->trackPointEnabled){
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
uint8_t pointerId = [self retrievePointerIdFromDict:touch];
|
||||||
|
CAShapeLayer* trackPoint = self->trackPointPool[pointerId];
|
||||||
|
[CATransaction begin];
|
||||||
|
[CATransaction setDisableActions:YES];
|
||||||
|
trackPoint.hidden = true;
|
||||||
|
[CATransaction commit];
|
||||||
|
[self removePointerId:touch];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else [self removePointerId:touch];
|
||||||
|
|
||||||
if(self->activateCoordSelector) [self removePointerObjFromDict:touch];
|
if(self->activateCoordSelector) [self removePointerObjFromDict:touch];
|
||||||
[self->blacklistedTouches removeObject:@((uintptr_t)touch)];
|
[self->blacklistedTouches removeObject:@((uintptr_t)touch)];
|
||||||
}
|
}
|
||||||
//if(self->touchPointSpawnedAtUpperScreenEdge && [[event allTouches] count] == [touches count])
|
|
||||||
});
|
});
|
||||||
else{
|
|
||||||
for (UITouch* touch in touches){
|
|
||||||
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
|
|
||||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_UP]; //send touch event before remove pointerId
|
|
||||||
[self removePointerId:touch]; //then remove pointerId
|
|
||||||
if(self->activateCoordSelector) [self removePointerObjFromDict:touch];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
|
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||||
@@ -303,5 +323,18 @@
|
|||||||
return pointer.useRelativeCoords ? pointer.latestRelativePoint : pointer.latestPoint;
|
return pointer.useRelativeCoords ? pointer.latestRelativePoint : pointer.latestPoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
- (void)dealloc{
|
||||||
|
NSLog(@"dealloc purePativeTouchHanlder %f", CACurrentMediaTime());
|
||||||
|
pointerIdDict = nil;
|
||||||
|
activePointerIds = nil;
|
||||||
|
pointerIdPool= nil;
|
||||||
|
unassignedPointerIds = nil;
|
||||||
|
blacklistedTouches = nil;
|
||||||
|
for(CAShapeLayer* layer in trackPointPool){
|
||||||
|
[layer removeFromSuperlayer];
|
||||||
|
}
|
||||||
|
trackPointPool = nil;
|
||||||
|
pointerObjDict = nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
|
|||||||
@@ -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) {}
|
||||||
|
}
|
||||||
@@ -13,13 +13,13 @@
|
|||||||
#import "DataManager.h"
|
#import "DataManager.h"
|
||||||
#import "CustomTapGestureRecognizer.h"
|
#import "CustomTapGestureRecognizer.h"
|
||||||
|
|
||||||
#define RIGHTCLICK_TAP_DOWN_TIME_THRESHOLD_S 0.15
|
#define RIGHTCLICK_TAP_DOWN_TIME_THRESHOLD_S 0.1
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
@interface RelativeTouchHandler : UIResponder
|
@interface RelativeTouchHandler : UIResponder
|
||||||
@property (nonatomic, readonly) CustomTapGestureRecognizer* mouseRightClickTapRecognizer; // this object will be passed to onscreencontrol class for areVirtualControllerTaps flag setting
|
|
||||||
|
|
||||||
|
@property (nonatomic, readonly) CustomTapGestureRecognizer* mouseRightClickTapRecognizer; // this object will be passed to onscreencontrol class for areVirtualControllerTaps flag setting
|
||||||
|
|
||||||
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings;
|
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//
|
//
|
||||||
// RelativeTouchHandler.m
|
// RelativeTouchHandler.m
|
||||||
// Moonlight
|
// VoidLink
|
||||||
//
|
//
|
||||||
// Completely refactored by True砖家 on 2024.9.13
|
// Completely refactored by True砖家 on 2024.9.13
|
||||||
// Copyright © 2024 True砖家 on Bilibili. All rights reserved.
|
// Copyright © 2024 True砖家 on Bilibili. All rights reserved.
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#import "RelativeTouchHandler.h"
|
#import "RelativeTouchHandler.h"
|
||||||
#import "DataManager.h"
|
#import "DataManager.h"
|
||||||
|
#import "VoidLink-Swift.h"
|
||||||
|
|
||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
|
|
||||||
@@ -23,22 +24,26 @@ static const float QUICK_TAP_TIME_INTERVAL = 0.2;
|
|||||||
BOOL firstTouchMoved;
|
BOOL firstTouchMoved;
|
||||||
BOOL mousePointerMoved;
|
BOOL mousePointerMoved;
|
||||||
BOOL quickTapDetected;
|
BOOL quickTapDetected;
|
||||||
BOOL isInMouseWheelScrollingMode;
|
|
||||||
|
|
||||||
// upper screen edge check
|
// upper screen edge check
|
||||||
bool touchPointSpawnedAtUpperScreenEdge;
|
bool touchPointSpawnedAtUpperScreenEdge;
|
||||||
CGFloat slideGestureVerticalThreshold;
|
CGFloat slideGestureVerticalThreshold;
|
||||||
CGFloat screenWidthWithThreshold;
|
CGFloat screenWidthWithThreshold;
|
||||||
CGFloat EDGE_TOLERANCE;
|
CGFloat _edgeTolerance;
|
||||||
|
|
||||||
UITouch* touchLockedForMouseMove;
|
UITouch* touchLockedForMouseMove;
|
||||||
|
UITouch* quickTapTouch;
|
||||||
|
|
||||||
|
bool multiTouchesDetected;
|
||||||
|
|
||||||
|
CADisplayLink *displayLink;
|
||||||
|
|
||||||
#if TARGET_OS_TV
|
#if TARGET_OS_TV
|
||||||
UIGestureRecognizer* remotePressRecognizer;
|
UIGestureRecognizer* remotePressRecognizer;
|
||||||
UIGestureRecognizer* remoteLongPressRecognizer;
|
UIGestureRecognizer* remoteLongPressRecognizer;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
StreamView* streamView;
|
__weak StreamView* streamView;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings {
|
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings {
|
||||||
@@ -53,18 +58,21 @@ static const float QUICK_TAP_TIME_INTERVAL = 0.2;
|
|||||||
_mouseRightClickTapRecognizer.delaysTouchesBegan = NO;
|
_mouseRightClickTapRecognizer.delaysTouchesBegan = NO;
|
||||||
_mouseRightClickTapRecognizer.delaysTouchesEnded = NO;
|
_mouseRightClickTapRecognizer.delaysTouchesEnded = NO;
|
||||||
[self->streamView.streamFrameTopLayerView addGestureRecognizer:_mouseRightClickTapRecognizer]; // add all additional gestures to the streamFrameTopLayerView instead of the streamview.
|
[self->streamView.streamFrameTopLayerView addGestureRecognizer:_mouseRightClickTapRecognizer]; // add all additional gestures to the streamFrameTopLayerView instead of the streamview.
|
||||||
|
_mouseRightClickTapRecognizer.touchCapturingView = streamView;
|
||||||
|
|
||||||
isInMouseWheelScrollingMode = false;
|
|
||||||
firstTouchMoved = false;
|
firstTouchMoved = false;
|
||||||
mousePointerMoved = false;
|
mousePointerMoved = false;
|
||||||
mousePointerTimestamp = 0;
|
mousePointerTimestamp = 0;
|
||||||
|
|
||||||
// upper screen check
|
// upper screen check
|
||||||
EDGE_TOLERANCE = 10.0;
|
_edgeTolerance = settings.edgeSlidingSensitivity.floatValue;
|
||||||
slideGestureVerticalThreshold = CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.4;
|
slideGestureVerticalThreshold = CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.4;
|
||||||
screenWidthWithThreshold = CGRectGetWidth([[UIScreen mainScreen] bounds]) - EDGE_TOLERANCE;
|
screenWidthWithThreshold = CGRectGetWidth([[UIScreen mainScreen] bounds]) - _edgeTolerance;
|
||||||
self->touchPointSpawnedAtUpperScreenEdge = false;
|
self->touchPointSpawnedAtUpperScreenEdge = false;
|
||||||
|
|
||||||
|
// self->displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkCallback:)];
|
||||||
|
// [self->displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
|
||||||
|
|
||||||
#if TARGET_OS_TV
|
#if TARGET_OS_TV
|
||||||
remotePressRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(remoteButtonPressed:)];
|
remotePressRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(remoteButtonPressed:)];
|
||||||
remotePressRecognizer.allowedPressTypes = @[@(UIPressTypeSelect)];
|
remotePressRecognizer.allowedPressTypes = @[@(UIPressTypeSelect)];
|
||||||
@@ -81,42 +89,21 @@ static const float QUICK_TAP_TIME_INTERVAL = 0.2;
|
|||||||
|
|
||||||
- (bool)isOnScreenControllerBeingPressed:(NSSet* )touches{
|
- (bool)isOnScreenControllerBeingPressed:(NSSet* )touches{
|
||||||
for(UITouch* touch in touches){
|
for(UITouch* touch in touches){
|
||||||
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]) return true;
|
if([OnScreenControls.touchesCapturedByOnScreenControls containsObject:touch]) return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
- (bool)isOnScreenWidgetViewBeingPressed {
|
|
||||||
bool gotOneButtonPressed = false;
|
|
||||||
for(UIView* view in self->streamView.superview.subviews){ // iterates all on-screen widget views in StreamFrameView
|
|
||||||
if ([view isKindOfClass:[OnScreenWidgetView class]]) {
|
|
||||||
OnScreenWidgetView* widgetView = (OnScreenWidgetView*) view;
|
|
||||||
if(widgetView.pressed){
|
|
||||||
gotOneButtonPressed = true; //got one button pressed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return gotOneButtonPressed;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)resetAllPressedFlagsForOnScreenWidgetViews {
|
|
||||||
for(UIView* view in self->streamView.superview.subviews){ // iterates all on-screen widget views in StreamFrameView
|
|
||||||
if ([view isKindOfClass:[OnScreenWidgetView class]]) {
|
|
||||||
OnScreenWidgetView* widgetView = (OnScreenWidgetView*) view;
|
|
||||||
widgetView.pressed = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
- (void)mouseRightClick {
|
- (void)mouseRightClick {
|
||||||
|
multiTouchesDetected = false;
|
||||||
|
dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC));
|
||||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
||||||
Log(LOG_D, @"Sending right mouse button press");
|
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
|
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
|
||||||
|
Log(LOG_D, @"Sending right mouse button press");
|
||||||
// Wait 100 ms to simulate a real button press
|
// Wait 100 ms to simulate a real button press
|
||||||
usleep(100 * 1000);
|
dispatch_after(delay, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
|
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,35 +117,34 @@ static const float QUICK_TAP_TIME_INTERVAL = 0.2;
|
|||||||
return hypotf(originalPoint.x - currentPoint.x, originalPoint.y - currentPoint.y) <= 300;
|
return hypotf(originalPoint.x - currentPoint.x, originalPoint.y - currentPoint.y) <= 300;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (BOOL)isAdjacentPoints:(CGPoint)currentPoint from:(CGPoint)originalPoint tolerance:(CGFloat)tolerance {
|
||||||
|
bool isAdjacent = hypotf(originalPoint.x - currentPoint.x, originalPoint.y - currentPoint.y) <= hypot(tolerance, tolerance);
|
||||||
|
return isAdjacent;
|
||||||
|
}
|
||||||
|
|
||||||
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
|
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||||
|
firstTouchMoved = false;
|
||||||
|
|
||||||
//check if touch point is spawned on the left or right upper half screen edges, this is the highest priority
|
//check if touch point is spawned on the left or right upper half screen edges, this is the highest priority
|
||||||
CGPoint initialPoint = [[touches anyObject] locationInView:streamView];
|
CGPoint initialPoint = [[touches anyObject] locationInView:streamView];
|
||||||
if(initialPoint.y < slideGestureVerticalThreshold && (initialPoint.x < EDGE_TOLERANCE || initialPoint.x > screenWidthWithThreshold)) {
|
if(initialPoint.y < slideGestureVerticalThreshold && (initialPoint.x < _edgeTolerance || initialPoint.x > screenWidthWithThreshold)) {
|
||||||
self->touchPointSpawnedAtUpperScreenEdge = true;
|
self->touchPointSpawnedAtUpperScreenEdge = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
firstTouchMoved = false;
|
|
||||||
touchPointSpawnedAtUpperScreenEdge = false; // reset this flag immediately if we get a touch event passing the check above, this fixes irresponsive touch after closing the command tool menu.
|
touchPointSpawnedAtUpperScreenEdge = false; // reset this flag immediately if we get a touch event passing the check above, this fixes irresponsive touch after closing the command tool menu.
|
||||||
|
|
||||||
if([[event allTouches] count] == 2 && ![self isOnScreenWidgetViewBeingPressed] && ![self isOnScreenControllerBeingPressed:[event allTouches]]){
|
if([UITouchUtil touchesIn:streamView from:event].count>=2){
|
||||||
NSLog(@"get in scrolling mode");
|
multiTouchesDetected = true;
|
||||||
isInMouseWheelScrollingMode = true;
|
return;
|
||||||
return; // if we got 2 touches on the blank area, it's gonna be a mouse scroll touch, and must prevent UITtouch object for mouse pointer being captured & locked
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NSLog(@"touches count in began stage: %llu", (uint64_t)[touches count]);
|
|
||||||
|
|
||||||
UITouch* candidateTouch = nil;
|
UITouch* candidateTouch = nil;
|
||||||
|
|
||||||
// the onscreen controllers are implmented by CALayer, which can not intercept UITouch event, touch will penetrate to the streamView level and captured in the touches callback of this touchHandler class.
|
// the onscreen controllers are implmented by CALayer, which can not intercept UITouch event, touch will penetrate to the streamView level and captured in the touches callback of this touchHandler class.
|
||||||
// the onscreen button are UIViews, they intercept UITouch events, so we don't need to worry about them.
|
// the onscreen button are UIViews, they intercept UITouch events, so we don't need to worry about them.
|
||||||
for(UITouch* touch in touches){
|
for(UITouch* touch in touches){
|
||||||
// NSLog(@"candidate touch test: %llu", (uint64_t)touch);
|
if([OnScreenControls.touchesCapturedByOnScreenControls containsObject:touch]){
|
||||||
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]){
|
|
||||||
// NSLog(@"%f controller tap detected", CACurrentMediaTime());
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
else candidateTouch = touch;
|
else candidateTouch = touch;
|
||||||
@@ -166,15 +152,18 @@ static const float QUICK_TAP_TIME_INTERVAL = 0.2;
|
|||||||
|
|
||||||
// quick double tap detection for dragging. simulates a real notebook computer touchpad
|
// quick double tap detection for dragging. simulates a real notebook computer touchpad
|
||||||
CGPoint currentTouchLocation = [candidateTouch locationInView:streamView];
|
CGPoint currentTouchLocation = [candidateTouch locationInView:streamView];
|
||||||
NSTimeInterval tapInterval = CACurrentMediaTime() - mousePointerTimestamp;
|
|
||||||
if(tapInterval < QUICK_TAP_TIME_INTERVAL && [self isAdjacentTouches:currentTouchLocation from:initialMousePointerLocation] ) {
|
if([UITouchUtil touchesIn:streamView from:event].count == 1){
|
||||||
// NSLog(@"quick click detected");
|
NSTimeInterval tapInterval = CACurrentMediaTime() - mousePointerTimestamp;
|
||||||
quickTapDetected = true;
|
if(tapInterval < QUICK_TAP_TIME_INTERVAL
|
||||||
NSLog(@"quick Tap Detected");
|
&& [self isAdjacentTouches:currentTouchLocation from:initialMousePointerLocation]) {
|
||||||
// LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT); // do not press down mouse button here, or it wiil easily turn to double click on remote PC
|
quickTapDetected = true;
|
||||||
|
NSLog(@"quick Tap Detected");
|
||||||
|
}
|
||||||
|
quickTapTouch = touches.anyObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
// we must use [event allTouches] to check if touchLockedForMouseMove is captured, because the UITouch object could be captured by upper layer of UIView(in cases like tap gestures), not passed to the touches callbacks in this class, but still available in [event allTouches]
|
// use [event allTouches] to check if touchLockedForMouseMove is captured, if already captured, don't update touchLockedForMouseMove
|
||||||
if(candidateTouch != nil && ![[event allTouches] containsObject:touchLockedForMouseMove]){
|
if(candidateTouch != nil && ![[event allTouches] containsObject:touchLockedForMouseMove]){
|
||||||
touchLockedForMouseMove = candidateTouch;
|
touchLockedForMouseMove = candidateTouch;
|
||||||
// NSLog(@"Candidate touch for mouse movement locked");
|
// NSLog(@"Candidate touch for mouse movement locked");
|
||||||
@@ -184,37 +173,37 @@ static const float QUICK_TAP_TIME_INTERVAL = 0.2;
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
|
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||||
|
|
||||||
//NSLog(@"%f, touchesMoved callback, is scrolling: %d, touches count: %d", CACurrentMediaTime(), isInMouseWheelScrollingMode, (uint32_t)[touches count]);
|
|
||||||
|
|
||||||
if(isInMouseWheelScrollingMode && [[event allTouches] count] == 2 && ![self isOnScreenWidgetViewBeingPressed] && ![self isOnScreenControllerBeingPressed:touches]){
|
|
||||||
NSSet* twoTouches = [event allTouches];
|
|
||||||
CGPoint firstLocation = [[[twoTouches allObjects] objectAtIndex:0] locationInView:streamView];
|
|
||||||
CGPoint secondLocation = [[[twoTouches allObjects] objectAtIndex:1] locationInView:streamView];
|
|
||||||
|
|
||||||
CGPoint avgLocation = CGPointMake((firstLocation.x + secondLocation.x) / 2, (firstLocation.y + secondLocation.y) / 2);
|
NSSet* currentTouches = [UITouchUtil touchesIn:streamView from:event];
|
||||||
if ((CACurrentMediaTime() - _mouseRightClickTapRecognizer.gestureCapturedTime > RIGHTCLICK_TAP_DOWN_TIME_THRESHOLD_S) && twoFingerTouchLocation.y != avgLocation.y) { //prevent sending scrollevent while right click gesture is being recognized. The time threshold is only 150ms, resulting in a barely noticeable delay before the scroll event is activated.
|
|
||||||
// and we must exclude onscreen button taps & on-screen controller taps
|
if(![self isOnScreenControllerBeingPressed:currentTouches]) [TouchPadGestureHandler handleGestureIn:streamView with:event];
|
||||||
LiSendHighResScrollEvent((avgLocation.y - twoFingerTouchLocation.y) * 10);
|
|
||||||
}
|
if(multiTouchesDetected) return;
|
||||||
twoFingerTouchLocation = avgLocation;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// NSLog(@"%f touchesMoved callback, locked touch: %llu", CACurrentMediaTime(), (uintptr_t)touchLockedForMouseMove);
|
|
||||||
|
|
||||||
if([touches containsObject:touchLockedForMouseMove]){
|
if([touches containsObject:touchLockedForMouseMove]){
|
||||||
mousePointerMoved = true;
|
CGPoint currentLocation = [touchLockedForMouseMove locationInView:streamView];
|
||||||
[self sendMouseMoveEvent:touchLockedForMouseMove];
|
bool isAdjacentPoints = [self isAdjacentPoints:initialMousePointerLocation from:currentLocation tolerance:currentSettings.singleTapSensitivity.doubleValue];
|
||||||
|
if(!mousePointerMoved && !isAdjacentPoints){
|
||||||
|
mousePointerMoved = true;
|
||||||
|
}
|
||||||
|
[self sendMouseMoveEvent:currentLocation];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
|
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||||
if(isInMouseWheelScrollingMode){
|
|
||||||
isInMouseWheelScrollingMode = false;
|
if(TouchPadGestureHandler.ctrlDown) LiSendKeyboardEvent(CommandManager.keyboardButtonMappings[@"CTRL"].shortValue,KEY_ACTION_UP,0);
|
||||||
|
|
||||||
|
[TouchPadGestureHandler startInertialScroll];
|
||||||
|
|
||||||
|
if(multiTouchesDetected){
|
||||||
|
if([UITouchUtil touchesIn:streamView from:event].count == touches.count){
|
||||||
|
multiTouchesDetected = false;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if([UITouchUtil touchesIn:streamView from:event].count == touches.count) multiTouchesDetected = false;
|
||||||
|
|
||||||
if([touches containsObject:touchLockedForMouseMove]){
|
if([touches containsObject:touchLockedForMouseMove]){
|
||||||
// dealing with a single first tap, whether the button will be released, is going to be decided in sendLongMouseLeftButtonClickEvent
|
// dealing with a single first tap, whether the button will be released, is going to be decided in sendLongMouseLeftButtonClickEvent
|
||||||
if(!mousePointerMoved && !self->quickTapDetected) [self sendLongMouseLeftButtonClickEvent];
|
if(!mousePointerMoved && !self->quickTapDetected) [self sendLongMouseLeftButtonClickEvent];
|
||||||
@@ -230,24 +219,23 @@ static const float QUICK_TAP_TIME_INTERVAL = 0.2;
|
|||||||
mousePointerMoved = false;
|
mousePointerMoved = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if([[event allTouches] count] == [touches count]){
|
for(UITouch* touch in touches){
|
||||||
isInMouseWheelScrollingMode = false;
|
[OnScreenControls.touchesCapturedByOnScreenControls removeObject:touch];
|
||||||
|
}
|
||||||
|
|
||||||
|
if([UITouchUtil touchesIn:streamView from:event].count == [touches count]){
|
||||||
touchLockedForMouseMove = nil;
|
touchLockedForMouseMove = nil;
|
||||||
mousePointerMoved = false; // need to reset this anyway
|
mousePointerMoved = false; // need to reset this anyway
|
||||||
[self resetAllPressedFlagsForOnScreenWidgetViews]; // reset all pressed flag for on-screen widget views after all fingers lifted from screen.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
touchPointSpawnedAtUpperScreenEdge = false;
|
touchPointSpawnedAtUpperScreenEdge = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)sendMouseMoveEvent:(CGPoint)currentLocation{
|
||||||
- (void)sendMouseMoveEvent:(UITouch* )touch{
|
|
||||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
||||||
if(self->touchPointSpawnedAtUpperScreenEdge) return; // we're done here. this touch event will not be sent to the remote PC.
|
bool isAdjacentPoints = [self isAdjacentPoints:self->initialMousePointerLocation from:currentLocation tolerance:self->currentSettings.relativeTouchSlideThreshold.floatValue];
|
||||||
|
|
||||||
CGPoint currentLocation = [touch locationInView:self->streamView];
|
if (!self->firstTouchMoved && !isAdjacentPoints) {
|
||||||
|
|
||||||
if (!self->firstTouchMoved) {
|
|
||||||
self->latestMousePointerLocation = currentLocation;
|
self->latestMousePointerLocation = currentLocation;
|
||||||
self->firstTouchMoved = true;
|
self->firstTouchMoved = true;
|
||||||
}
|
}
|
||||||
@@ -259,37 +247,40 @@ static const float QUICK_TAP_TIME_INTERVAL = 0.2;
|
|||||||
int deltaY = (currentLocation.y - self->latestMousePointerLocation.y) * 1.35 * self->currentSettings.mousePointerVelocityFactor.floatValue;
|
int deltaY = (currentLocation.y - self->latestMousePointerLocation.y) * 1.35 * self->currentSettings.mousePointerVelocityFactor.floatValue;
|
||||||
|
|
||||||
if (deltaX != 0 || deltaY != 0) {
|
if (deltaX != 0 || deltaY != 0) {
|
||||||
LiSendMouseMoveEvent(deltaX, deltaY);
|
|
||||||
self->latestMousePointerLocation = currentLocation;
|
self->latestMousePointerLocation = currentLocation;
|
||||||
|
if(self->touchPointSpawnedAtUpperScreenEdge) return; // we're done here. this touch event will not be sent to the remote PC.
|
||||||
|
if(self->firstTouchMoved) LiSendMouseMoveEvent(deltaX, deltaY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// this will turn into a dragging anytime...
|
// this will turn into a dragging anytime...
|
||||||
- (void)sendLongMouseLeftButtonClickEvent{
|
- (void)sendLongMouseLeftButtonClickEvent{
|
||||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
||||||
// if (!self->isDragging){
|
// if (!self->isDragging){
|
||||||
Log(LOG_D, @"Sending left mouse button press");
|
Log(LOG_D, @"Sending left mouse button press");
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
||||||
|
|
||||||
// Wait 100 ms to simulate a real button press
|
// Wait 100 ms to simulate a real button press
|
||||||
usleep(QUICK_TAP_TIME_INTERVAL * 1000000);
|
dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(QUICK_TAP_TIME_INTERVAL * NSEC_PER_SEC));
|
||||||
if(!self->quickTapDetected){
|
dispatch_after(delay, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
if(!self->quickTapDetected){
|
||||||
}
|
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||||
// else NSLog(@"Left mouse button release cancelled, keep pressing down, turning into dragging...");
|
}
|
||||||
|
// else NSLog(@"Left mouse button release cancelled, keep pressing down, turning into dragging...");
|
||||||
|
});
|
||||||
// do not release the button if we're still dragging, this will prevent the dragging from being interrupted.
|
// do not release the button if we're still dragging, this will prevent the dragging from being interrupted.
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)sendShortMouseLeftButtonClickEvent{
|
- (void)sendShortMouseLeftButtonClickEvent{
|
||||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC));
|
||||||
usleep(50 * 1000);
|
dispatch_after(delay, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
||||||
usleep(50 * 1000);
|
dispatch_after(delay, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
//
|
||||||
|
// SafeTimer.swift
|
||||||
|
// VoidLink
|
||||||
|
//
|
||||||
|
// Created by True砖家 on 2025/9/9.
|
||||||
|
// Copyright © 2025 True砖家@Bilibili. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import QuartzCore
|
||||||
|
|
||||||
|
@objc(SafeTimer)
|
||||||
|
final class SafeTimer: NSObject {
|
||||||
|
private var timer: DispatchSourceTimer?
|
||||||
|
private let timerQueue: DispatchQueue // 私有串行队列
|
||||||
|
private let timerQueueKey = DispatchSpecificKey<UInt8>()
|
||||||
|
private let userHandler: () -> Void
|
||||||
|
private let interval: TimeInterval
|
||||||
|
private let delay: TimeInterval
|
||||||
|
|
||||||
|
private var shouldRunHandler: Bool = false // 控制逻辑上的暂停/恢复
|
||||||
|
private var isCleaned: Bool = false // 是否已经 clean
|
||||||
|
@objc private(set) var remainingMinimumRunCount: Int = 0
|
||||||
|
private var shouldCleanAfterMinimumRuns: Bool = false
|
||||||
|
|
||||||
|
@objc init(interval: TimeInterval = 1.0,
|
||||||
|
delay: TimeInterval = 0,
|
||||||
|
queueLabel: String = "com.example.safetimer",
|
||||||
|
handler: @escaping () -> Void) {
|
||||||
|
|
||||||
|
self.interval = interval
|
||||||
|
self.delay = delay
|
||||||
|
self.userHandler = handler
|
||||||
|
self.timerQueue = DispatchQueue(label: queueLabel)
|
||||||
|
self.timerQueue.setSpecific(key: timerQueueKey, value: 1)
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
let t = DispatchSource.makeTimerSource(queue: timerQueue)
|
||||||
|
t.schedule(deadline: .now() + delay, repeating: interval)
|
||||||
|
|
||||||
|
t.setEventHandler { [weak self] in
|
||||||
|
guard let self = self, !self.isCleaned else { return }
|
||||||
|
if !self.shouldRunHandler { return }
|
||||||
|
self.userHandler()
|
||||||
|
if self.remainingMinimumRunCount > 0 {
|
||||||
|
self.remainingMinimumRunCount -= 1
|
||||||
|
}
|
||||||
|
if self.shouldCleanAfterMinimumRuns && self.remainingMinimumRunCount == 0 {
|
||||||
|
self.cleanOnTimerQueue(force: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.resume() // timer 一开始就 resume,避免 suspend/resume 的计数问题
|
||||||
|
self.timer = t
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
cleanForDeinit()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 开始逻辑上的计时
|
||||||
|
@objc func start() {
|
||||||
|
timerQueue.async { [weak self] in
|
||||||
|
guard let self = self, !self.isCleaned else { return }
|
||||||
|
self.shouldRunHandler = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 暂停逻辑上的计时
|
||||||
|
@objc func pause() {
|
||||||
|
timerQueue.async { [weak self] in
|
||||||
|
guard let self = self, !self.isCleaned else { return }
|
||||||
|
self.shouldRunHandler = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 重置下一次触发时间,并开始执行 handler
|
||||||
|
@objc func restart() {
|
||||||
|
restart(minimumRunCount: 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 重置下一次触发时间,并保证 clean 前至少执行 minimumRunCount 次 handler
|
||||||
|
@objc(restartWithMinimumRunCount:)
|
||||||
|
func restart(minimumRunCount: Int = 1) {
|
||||||
|
timerQueue.async { [weak self] in
|
||||||
|
guard let self = self, let t = self.timer, !self.isCleaned else { return }
|
||||||
|
self.remainingMinimumRunCount = max(0, minimumRunCount)
|
||||||
|
self.shouldCleanAfterMinimumRuns = false
|
||||||
|
t.schedule(deadline: .now() + self.delay, repeating: self.interval)
|
||||||
|
self.shouldRunHandler = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func isRunning() -> Bool {
|
||||||
|
if DispatchQueue.getSpecific(key: timerQueueKey) != nil {
|
||||||
|
return shouldRunHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
return timerQueue.sync {
|
||||||
|
shouldRunHandler && !isCleaned
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 彻底清理 timer;如果 restart(minimumRunCount:) 尚未跑够次数,则延后到最低次数满足后清理
|
||||||
|
@objc func clean() {
|
||||||
|
if DispatchQueue.getSpecific(key: timerQueueKey) != nil {
|
||||||
|
cleanOnTimerQueue()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同步在 timerQueue 执行,保证已排队事件全部处理完(或跳过)
|
||||||
|
timerQueue.sync {
|
||||||
|
cleanOnTimerQueue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func cleanForDeinit() {
|
||||||
|
if DispatchQueue.getSpecific(key: timerQueueKey) != nil {
|
||||||
|
cleanOnTimerQueue(force: true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
timerQueue.sync {
|
||||||
|
cleanOnTimerQueue(force: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func cleanOnTimerQueue(force: Bool = false) {
|
||||||
|
guard !isCleaned else { return }
|
||||||
|
if !force && shouldRunHandler && remainingMinimumRunCount > 0 {
|
||||||
|
shouldCleanAfterMinimumRuns = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isCleaned = true
|
||||||
|
shouldRunHandler = false
|
||||||
|
remainingMinimumRunCount = 0
|
||||||
|
shouldCleanAfterMinimumRuns = false
|
||||||
|
|
||||||
|
if let t = timer {
|
||||||
|
t.setEventHandler {} // 清空 handler
|
||||||
|
t.cancel() // 取消 timer
|
||||||
|
timer = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,9 +13,10 @@
|
|||||||
|
|
||||||
#import "ControllerSupport.h"
|
#import "ControllerSupport.h"
|
||||||
#import "OnScreenControls.h"
|
#import "OnScreenControls.h"
|
||||||
#import "VoidLink-Swift.h"
|
|
||||||
#import "StreamConfiguration.h"
|
#import "StreamConfiguration.h"
|
||||||
|
|
||||||
|
@class PencilHandler;
|
||||||
|
|
||||||
@protocol UserInteractionDelegate <NSObject>
|
@protocol UserInteractionDelegate <NSObject>
|
||||||
|
|
||||||
- (void)userInteractionBegan;
|
- (void)userInteractionBegan;
|
||||||
@@ -24,35 +25,41 @@
|
|||||||
- (void)toggleStatsOverlay;
|
- (void)toggleStatsOverlay;
|
||||||
- (void)toggleMouseCapture;
|
- (void)toggleMouseCapture;
|
||||||
- (void)toggleMouseVisible;
|
- (void)toggleMouseVisible;
|
||||||
|
- (void)expandSettingsView;
|
||||||
|
- (void)disconnectAndQuitApp;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
#if TARGET_OS_TV
|
@interface StreamView : UIView
|
||||||
@interface StreamView : UIView <X1KitMouseDelegate, UITextFieldDelegate>
|
|
||||||
#else
|
|
||||||
@interface StreamView : UIView <X1KitMouseDelegate, UITextFieldDelegate, UIPointerInteractionDelegate>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
@property (assign, nonatomic) UIView* streamFrameTopLayerView;
|
@property (weak, nonatomic) UIView* streamFrameTopLayerView;
|
||||||
|
@property (weak, nonatomic) id<UserInteractionDelegate> interactionDelegate;
|
||||||
@property (assign, nonatomic) CGFloat streamAspectRatio;
|
@property (assign, nonatomic) CGFloat streamAspectRatio;
|
||||||
@property (assign, nonatomic) CGRect originalFrame;
|
@property (assign, nonatomic) CGRect originalFrame;
|
||||||
@property (assign, nonatomic) bool widgetToolOpened;
|
@property (assign, nonatomic) bool widgetToolOpened;
|
||||||
|
@property (strong, nonatomic) OnScreenControls* onScreenControls;
|
||||||
|
@property (weak, nonatomic) PencilHandler* pencilHandler;
|
||||||
|
|
||||||
|
- (void) setupStreamViewWithControllerSupport:(ControllerSupport*)controllerSupport
|
||||||
- (void) setupStreamView:(ControllerSupport*)controllerSupport
|
interactionDelegate:(id<UserInteractionDelegate>)interactionDelegate
|
||||||
interactionDelegate:(id<UserInteractionDelegate>)interactionDelegate
|
streamConfig:(StreamConfiguration*)streamConfig
|
||||||
config:(StreamConfiguration*)streamConfig
|
gameProfile:(OSCProfile* )profile
|
||||||
streamFrameTopLayerView:(UIView* )topLayerView
|
streamFrameTopLayerView:(UIView* )topLayerView
|
||||||
;
|
;
|
||||||
- (void) showOnScreenControls;
|
|
||||||
|
- (void)cleanUp;
|
||||||
|
|
||||||
|
- (void) reloadLegacyWidgets:(OSCProfile* )profile;
|
||||||
- (void) setOnScreenControls;
|
- (void) setOnScreenControls;
|
||||||
- (void) disableOnScreenControls;
|
- (void) disableOnScreenControls;
|
||||||
- (void) reloadOnScreenControlsRealtimeWith:(ControllerSupport*)controllerSupport
|
- (void) reloadOnScreenControlsRealtimeWithControllerSupport:(ControllerSupport*)controllerSupport
|
||||||
andConfig:(StreamConfiguration*)streamConfig;
|
streamConfig:(StreamConfiguration*)streamConfig;
|
||||||
- (void) reloadOnScreenControlsWith:(ControllerSupport*)controllerSupport
|
- (void) reloadOnScreenControlsWith:(ControllerSupport*)controllerSupport
|
||||||
andConfig:(StreamConfiguration*)streamConfig;
|
andConfig:(StreamConfiguration*)streamConfig;
|
||||||
- (void) clearOnScreenWidgets;
|
- (void) clearOnScreenWidgets;
|
||||||
- (void) reloadOnScreenWidgetViews;
|
- (void) reloadGameProfile:(OSCProfile* )profile reloadWidgets:(bool)reloadWidgets;
|
||||||
|
- (void) saveStreamingGameProfileChanges;
|
||||||
|
- (bool) isOnScreenWidgetEnabled;
|
||||||
|
|
||||||
- (CGSize) getVideoAreaSize;
|
- (CGSize) getVideoAreaSize;
|
||||||
- (CGPoint) adjustCoordinatesForVideoArea:(CGPoint)point;
|
- (CGPoint) adjustCoordinatesForVideoArea:(CGPoint)point;
|
||||||
@@ -60,11 +67,21 @@
|
|||||||
|
|
||||||
- (OnScreenControlsLevel) getCurrentOscState;
|
- (OnScreenControlsLevel) getCurrentOscState;
|
||||||
|
|
||||||
-(void)readyToBringUpSoftKeyboardByToolbox;
|
- (void)readyToBringUpSoftKeyboardByToolbox;
|
||||||
- (void)keyboardWillShow:(NSNotification *)notification;
|
- (void)keyboardWillShow:(NSNotification *)notification;
|
||||||
- (void)keyboardWillHide;
|
- (void)keyboardWillHide;
|
||||||
|
- (void)handleNonStandardKeyboard:(NSNotification *)notification;
|
||||||
- (void)liftMetalVideoViewIfNeeded:(CGFloat)liftHeight;
|
- (void)liftMetalVideoViewIfNeeded:(CGFloat)liftHeight;
|
||||||
|
|
||||||
|
- (void)alterAbsTouchDragWith:(int32_t)mouseButton;
|
||||||
|
|
||||||
|
- (void)enablePencilHover;
|
||||||
|
- (void)disablePencilHover;
|
||||||
|
- (void)setAllowSingleTouchEnabled:(BOOL)enabled;
|
||||||
|
- (void)toggleTouchDisabled:(bool)disabled;
|
||||||
|
|
||||||
|
- (void)startInteractionTimer;
|
||||||
|
|
||||||
#if !TARGET_OS_TV
|
#if !TARGET_OS_TV
|
||||||
- (void) updateCursorLocation:(CGPoint)location isMouse:(BOOL)isMouse;
|
- (void) updateCursorLocation:(CGPoint)location isMouse:(BOOL)isMouse;
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
//
|
||||||
|
// TouchPadGestureHandler.swift
|
||||||
|
// VoidLink
|
||||||
|
//
|
||||||
|
// Created by True砖家 on 2025/11/5.
|
||||||
|
// Copyright © 2025 True砖家 on Bilibili. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
@objc class TouchPadGestureHandler: NSObject {
|
||||||
|
|
||||||
|
@objc public static var ctrlDown:Bool = false
|
||||||
|
@objc public static var enablePinch:Bool = true
|
||||||
|
@objc public static var ctrlDownForPinch:Bool = true
|
||||||
|
@objc public static var enableHorizontalScroll:Bool = true
|
||||||
|
@objc public static var scrollSensitivity:CGFloat = 1.0
|
||||||
|
@objc public static var pinchSensitivity:CGFloat = 1.0
|
||||||
|
@objc public static var displayLinkRate:CGFloat = 60
|
||||||
|
|
||||||
|
private static var inertialScroller: InertialScroller = InertialScroller(decelerationRate: displayLinkRate > 110 ? 0.96 : 0.9, displayLinkRate: displayLinkRate) {
|
||||||
|
if ctrlDown {return}
|
||||||
|
LiSendHighResScrollEvent(Int16(inertialScroller.vector.dy*7*scrollSensitivity))
|
||||||
|
if TouchPadGestureHandler.enableHorizontalScroll {LiSendHighResHScrollEvent(Int16(-inertialScroller.vector.dx*7*scrollSensitivity))}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public static func startInertialScroll(){
|
||||||
|
inertialScroller.timer?.restart()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public static func handleGesture(in view: UIView, with event: UIEvent) {
|
||||||
|
inertialScroller.timer?.pause()
|
||||||
|
|
||||||
|
let currentTouches = UITouchUtil.touches(in: view, from: event)
|
||||||
|
guard currentTouches.count == 2 else { return }
|
||||||
|
|
||||||
|
LiSendMouseButtonEvent(CChar(BUTTON_ACTION_RELEASE), BUTTON_LEFT)
|
||||||
|
LiSendMouseButtonEvent(CChar(BUTTON_ACTION_RELEASE), BUTTON_RIGHT)
|
||||||
|
|
||||||
|
guard let touch1 = currentTouches.first else { return }
|
||||||
|
var mutable = Array(currentTouches)
|
||||||
|
mutable.removeAll { $0 == touch1 }
|
||||||
|
guard let touch2 = mutable.first else { return }
|
||||||
|
|
||||||
|
let currentDistance = UITouchUtil.distance(between: touch1, and: touch2, in: view)
|
||||||
|
let previousDistance = UITouchUtil.previousDistance(between: touch1, and: touch2, in: view)
|
||||||
|
|
||||||
|
let midPointVector = UITouchUtil.midPointVector(between: touch1, and: touch2, in: view)
|
||||||
|
let midPointDeltaX = midPointVector.dx
|
||||||
|
let midPointDeltaY = midPointVector.dy
|
||||||
|
|
||||||
|
let sendHorizontalScroll = abs(midPointDeltaX) > 1.2*abs(midPointDeltaY)
|
||||||
|
|
||||||
|
inertialScroller.vector = CGVector(dx: sendHorizontalScroll ? midPointDeltaX : 0, dy: midPointDeltaY)
|
||||||
|
|
||||||
|
let originalPinchDelta = currentDistance-previousDistance;
|
||||||
|
let pinchDelta = enablePinch ? originalPinchDelta*7*pinchSensitivity : 0;
|
||||||
|
LiSendHighResScrollEvent(Int16(pinchDelta + midPointDeltaY*7*scrollSensitivity))
|
||||||
|
if enableHorizontalScroll, sendHorizontalScroll {LiSendHighResHScrollEvent(Int16(-midPointDeltaX*7*scrollSensitivity))}
|
||||||
|
|
||||||
|
if enablePinch, ctrlDownForPinch {
|
||||||
|
let midPointDelta = hypot(midPointDeltaX, midPointDeltaY)
|
||||||
|
if abs(originalPinchDelta) > midPointDelta*1.3, midPointDelta < 2 {
|
||||||
|
LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["CTRL"]!, CChar(KEY_ACTION_DOWN), 0)
|
||||||
|
ctrlDown = true
|
||||||
|
} else {
|
||||||
|
LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["CTRL"]!, CChar(KEY_ACTION_UP), 0)
|
||||||
|
ctrlDown = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
//
|
||||||
|
// UITouchUtil.swift
|
||||||
|
// VoidLink
|
||||||
|
//
|
||||||
|
// Created by True砖家 on 2025/11/5.
|
||||||
|
// Copyright © 2025 True砖家 on Bilibili. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
@objc class UITouchUtil: NSObject {
|
||||||
|
@objc public static func touches(in view: UIView, from event: UIEvent?) -> Set<UITouch> {
|
||||||
|
guard let touches = event?.allTouches else { return [] }
|
||||||
|
return touches.filter {
|
||||||
|
$0.view === view && ($0.type == .direct || $0.type == .pencil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public static func distance(between touch1: UITouch?, and touch2: UITouch?, in view: UIView?) -> CGFloat {
|
||||||
|
guard let view = view, let touch1 = touch1, let touch2 = touch2 else { return 0 }
|
||||||
|
let p1 = touch1.location(in: view)
|
||||||
|
let p2 = touch2.location(in: view)
|
||||||
|
return hypot(p1.x - p2.x, p1.y - p2.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public static func previousDistance(between touch1: UITouch?, and touch2: UITouch?, in view: UIView?) -> CGFloat {
|
||||||
|
guard let view = view, let touch1 = touch1, let touch2 = touch2 else { return 0 }
|
||||||
|
let p1 = touch1.previousLocation(in: view)
|
||||||
|
let p2 = touch2.previousLocation(in: view)
|
||||||
|
return hypot(p1.x - p2.x, p1.y - p2.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public static func vector(of touch: UITouch?, in view: UIView?) -> CGVector {
|
||||||
|
guard let view = view, let touch = touch else { return CGVectorMake(0,0) }
|
||||||
|
let p2 = touch.location(in: view)
|
||||||
|
let p1 = touch.previousLocation(in: view)
|
||||||
|
return CGVectorMake(p2.x - p1.x, p2.y - p1.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public static func midPointVector(between touch1: UITouch?, and touch2: UITouch?, in view: UIView?) -> CGVector {
|
||||||
|
guard let view = view, let touch1 = touch1, let touch2 = touch2 else { return CGVector(dx: 0, dy: 0)}
|
||||||
|
|
||||||
|
let vector1 = vector(of: touch1, in: view)
|
||||||
|
let vector2 = vector(of: touch2, in: view)
|
||||||
|
|
||||||
|
let deltaX = (vector1.dx+vector2.dx)/2
|
||||||
|
let deltaY = (vector1.dy+vector2.dy)/2
|
||||||
|
|
||||||
|
return CGVector(dx: deltaX, dy: deltaY)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#import "HapticContext.h"
|
#import "HapticContext.h"
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
@import GameController;
|
@import GameController;
|
||||||
@import CoreHaptics;
|
@import CoreHaptics;
|
||||||
@@ -14,6 +15,8 @@
|
|||||||
@import CoreMotion;
|
@import CoreMotion;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
typedef NS_ENUM(uint8_t, ControllerHardware);
|
||||||
|
|
||||||
@interface VoidController : NSObject
|
@interface VoidController : NSObject
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
@@ -69,6 +72,7 @@ typedef NS_ENUM(NSInteger, ControllerDeviceBatteryState) {
|
|||||||
@property(nonatomic, strong) NSMutableSet* _Nullable motionTypes;
|
@property(nonatomic, strong) NSMutableSet* _Nullable motionTypes;
|
||||||
@property(nonatomic, assign) uint16_t reportRateHz;
|
@property(nonatomic, assign) uint16_t reportRateHz;
|
||||||
@property(nonatomic, assign) uint16_t controllerNumber;
|
@property(nonatomic, assign) uint16_t controllerNumber;
|
||||||
|
@property(nonatomic, assign) ControllerHardware hardware;
|
||||||
@property(nonatomic, assign) BOOL hasAccelerometer;
|
@property(nonatomic, assign) BOOL hasAccelerometer;
|
||||||
@property(nonatomic, assign) BOOL hasGyroscope;
|
@property(nonatomic, assign) BOOL hasGyroscope;
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>GCSupportsGameMode</key>
|
|
||||||
<true/>
|
|
||||||
<key>CADisableMinimumFrameDuration</key>
|
<key>CADisableMinimumFrameDuration</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
@@ -63,22 +61,19 @@
|
|||||||
<string>_nvstream._tcp</string>
|
<string>_nvstream._tcp</string>
|
||||||
</array>
|
</array>
|
||||||
<key>NSLocalNetworkUsageDescription</key>
|
<key>NSLocalNetworkUsageDescription</key>
|
||||||
<string>Moonlight uses the local network to connect to your gaming PC for streaming.</string>
|
<string>VoidLink uses the local network to connect to your gaming PC for streaming.</string>
|
||||||
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
|
<string>麦克风访问权限允许VoidLink 实现麦克风重定向。麦克风重定向仅支持Sunshine基地版。
|
||||||
|
|
||||||
|
VoidLink uses microphone to stream your voice to PC host. Microphone redirection works with Sunshine-Foundation (a fork of Sunshine) only.</string>
|
||||||
<key>NSMotionUsageDescription</key>
|
<key>NSMotionUsageDescription</key>
|
||||||
<string>Moonlight uses the device Gyro and Acceleration data to emulate motion controls when streaming.</string>
|
<string>VoidLink uses the device Gyro and Acceleration data to emulate motion controls when streaming.</string>
|
||||||
<key>UIApplicationSceneManifest</key>
|
<key>UIApplicationSceneManifest</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>UIApplicationSupportsMultipleScenes</key>
|
||||||
|
<true/>
|
||||||
<key>UISceneConfigurations</key>
|
<key>UISceneConfigurations</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>UIWindowSceneSessionRoleExternalDisplay</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>UISceneDelegateClassName</key>
|
|
||||||
<string>SceneDelegate</string>
|
|
||||||
<key>UISceneConfigurationName</key>
|
|
||||||
<string>External Display Configuration</string>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
<key>UIWindowSceneSessionRoleApplication</key>
|
<key>UIWindowSceneSessionRoleApplication</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
@@ -88,9 +83,16 @@
|
|||||||
<string>SceneDelegate</string>
|
<string>SceneDelegate</string>
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
|
<key>UIWindowSceneSessionRoleExternalDisplay</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>UISceneConfigurationName</key>
|
||||||
|
<string>External Display Configuration</string>
|
||||||
|
<key>UISceneDelegateClassName</key>
|
||||||
|
<string>SceneDelegate</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
<key>UIApplicationSupportsMultipleScenes</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
</dict>
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<true/>
|
<true/>
|
||||||
@@ -98,6 +100,8 @@
|
|||||||
<array>
|
<array>
|
||||||
<string>audio</string>
|
<string>audio</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>UIDesignRequiresCompatibility</key>
|
||||||
|
<false/>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>Launch Screen</string>
|
<string>Launch Screen</string>
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
@@ -112,6 +116,10 @@
|
|||||||
<false/>
|
<false/>
|
||||||
<key>UIStatusBarHidden</key>
|
<key>UIStatusBarHidden</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>UISupportedExternalAccessoryProtocols</key>
|
||||||
|
<array>
|
||||||
|
<string>com.xiaoji.M2boot</string>
|
||||||
|
</array>
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23788.4" systemVersion="24F74" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
|
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="24512" systemVersion="24G419" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
|
||||||
<entity name="App" representedClassName="App" syncable="YES" codeGenerationType="class">
|
<entity name="App" representedClassName="App" syncable="YES" codeGenerationType="class">
|
||||||
<attribute name="hdrSupported" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
<attribute name="hdrSupported" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||||
<attribute name="hidden" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
<attribute name="hidden" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||||
@@ -8,7 +8,9 @@
|
|||||||
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
|
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
|
<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="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="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||||
<attribute name="ipv6Address" optional="YES" attributeType="String" syncable="YES"/>
|
<attribute name="ipv6Address" optional="YES" attributeType="String" syncable="YES"/>
|
||||||
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||||
@@ -20,53 +22,112 @@
|
|||||||
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
|
<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"/>
|
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
|
||||||
</entity>
|
</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">
|
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
|
||||||
|
<attribute name="appTheme" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||||
|
<attribute name="asyncFrameDequeue" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||||
<attribute name="asyncNativeTouchPriority" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
<attribute name="asyncNativeTouchPriority" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||||
<attribute name="audioConfig" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="NO" syncable="YES"/>
|
<attribute name="audioConfig" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="NO" syncable="YES"/>
|
||||||
|
<attribute name="audioEngine" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||||
<attribute name="backroundSessionTimer" optional="YES" attributeType="Integer 32" defaultValueString="32767" usesScalarValueType="NO" syncable="YES"/>
|
<attribute name="backroundSessionTimer" optional="YES" attributeType="Integer 32" defaultValueString="32767" usesScalarValueType="NO" syncable="YES"/>
|
||||||
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
|
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
|
||||||
<attribute name="btMouseSupport" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
<attribute name="btMouseSupport" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||||
<attribute name="emulatedControllerType" optional="YES" attributeType="Integer 16" defaultValueString="2" usesScalarValueType="NO" syncable="YES"/>
|
<attribute name="buttonVisualFeedback" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||||
|
<attribute name="controllerGyroBiasX" optional="YES" attributeType="Double" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||||
|
<attribute name="controllerGyroBiasY" optional="YES" attributeType="Double" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||||
|
<attribute name="controllerGyroBiasZ" optional="YES" attributeType="Double" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||||
|
<attribute name="controllerMouseExpo" optional="YES" attributeType="Float" defaultValueString="1.8" usesScalarValueType="NO" syncable="YES"/>
|
||||||
|
<attribute name="controllerMouseLeftButton" attributeType="Integer 32" defaultValueString="8" usesScalarValueType="NO" syncable="YES"/>
|
||||||
|
<attribute name="controllerMousePointerVelocity" optional="YES" attributeType="Float" defaultValueString="15" usesScalarValueType="NO" syncable="YES"/>
|
||||||
|
<attribute name="controllerMouseRightButton" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||||
|
<attribute name="controllerMouseStick" attributeType="Integer 32" defaultValueString="8388640" usesScalarValueType="NO" syncable="YES"/>
|
||||||
|
<attribute name="ctrlDownForPinch" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||||
|
<attribute name="customLocalRadialMenuButtonPosition" attributeType="Integer 32" defaultValueString="16777215" usesScalarValueType="NO" syncable="YES"/>
|
||||||
|
<attribute name="customStreamingRadialMenuButtonPosition" attributeType="Integer 32" defaultValueString="16777215" usesScalarValueType="NO" syncable="YES"/>
|
||||||
|
<attribute name="delayLeftClick" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||||
<attribute name="deviceGyroMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
<attribute name="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="enableGraphs" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||||
<attribute name="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
<attribute name="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||||
|
<attribute name="enablePinch" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||||
<attribute name="enablePIP" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
<attribute name="enablePIP" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||||
<attribute name="enableYUV444" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
<attribute name="enableYUV444" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||||
<attribute name="externalDisplayMode" optional="YES" attributeType="Integer 16" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
<attribute name="externalDisplayMode" optional="YES" attributeType="Integer 16" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||||
<attribute name="frameQueueSize" optional="YES" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="NO" syncable="YES"/>
|
<attribute name="framePacingMode" optional="YES" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||||
<attribute name="framePacingMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
<attribute name="frameQueueSize" optional="YES" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
|
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
|
||||||
|
<attribute name="fullColorRange" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||||
|
<attribute name="globeAsEscape" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||||
|
<attribute name="graphOpacity" optional="YES" attributeType="Integer 32" defaultValueString="50" usesScalarValueType="NO" syncable="YES"/>
|
||||||
|
<attribute name="gyroBiasX" optional="YES" attributeType="Double" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||||
|
<attribute name="gyroBiasY" optional="YES" attributeType="Double" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||||
|
<attribute name="gyroBiasZ" optional="YES" attributeType="Double" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||||
<attribute name="gyroMode" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
<attribute name="gyroMode" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||||
<attribute name="gyroSensitivity" optional="YES" attributeType="Float" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
<attribute name="gyroSensitivity" optional="YES" attributeType="Float" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||||
<attribute name="graphOpacity" optional="YES" attributeType="Integer 32" defaultValueString="50" usesScalarValueType="NO" syncable="YES"/>
|
<attribute name="hapticEngine" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
|
<attribute name="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="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="localMousePointerMode" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||||
|
<attribute name="localRadialMenuButton" attributeType="Integer 32" defaultValueString="8388608" usesScalarValueType="NO" syncable="YES"/>
|
||||||
|
<attribute name="localVolume" optional="YES" attributeType="Float" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||||
|
<attribute name="micVolume" optional="YES" attributeType="Float" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||||
<attribute name="mousePointerVelocityFactor" optional="YES" attributeType="Float" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
<attribute name="mousePointerVelocityFactor" optional="YES" attributeType="Float" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||||
<attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
<attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
<attribute name="muteInBackground" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||||
|
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||||
<attribute name="optimizeGames" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
<attribute name="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="playAudioOnPC" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||||
<attribute name="pointerVelocityModeDivider" optional="YES" attributeType="Float" defaultValueString="0.5" usesScalarValueType="NO" syncable="YES"/>
|
<attribute name="pointerVelocityModeDivider" optional="YES" attributeType="Float" defaultValueString="0.5" usesScalarValueType="NO" syncable="YES"/>
|
||||||
<attribute name="preferredCodec" attributeType="Integer 32" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
<attribute name="preferredCodec" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="YES" syncable="YES"/>
|
||||||
|
<attribute name="redirectMic" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||||
|
<attribute name="relativeTouchSlideThreshold" optional="YES" attributeType="Float" defaultValueString="6" usesScalarValueType="NO" syncable="YES"/>
|
||||||
|
<attribute name="rememberFoldState" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||||
|
<attribute name="renderingBackend" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||||
<attribute name="resolutionSelected" optional="YES" attributeType="Integer 16" defaultValueString="4" usesScalarValueType="NO" syncable="YES"/>
|
<attribute name="resolutionSelected" optional="YES" attributeType="Integer 16" defaultValueString="4" usesScalarValueType="NO" syncable="YES"/>
|
||||||
<attribute name="reverseMouseWheelDirection" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
<attribute name="reverseMouseWheelDirection" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||||
|
<attribute name="scrollSensitivity" optional="YES" attributeType="Float" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||||
|
<attribute name="sdrPerformanceWorkaround" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||||
|
<attribute name="sendDummyEvent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||||
<attribute name="settingsMenuMode" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
<attribute name="settingsMenuMode" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||||
|
<attribute name="settingsMenuOffset" optional="YES" attributeType="Float" defaultValueString="22" usesScalarValueType="NO" syncable="YES"/>
|
||||||
<attribute name="settingsMenuWidth" attributeType="Float" defaultValueString="370" usesScalarValueType="NO" syncable="YES"/>
|
<attribute name="settingsMenuWidth" attributeType="Float" defaultValueString="370" usesScalarValueType="NO" syncable="YES"/>
|
||||||
<attribute name="showKeyboardToolbar" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
<attribute name="showKeyboardToolbar" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||||
|
<attribute name="singleTapSensitivity" optional="YES" attributeType="Double" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||||
<attribute name="slideToSettingsDistance" optional="YES" attributeType="Float" defaultValueString="0.2" usesScalarValueType="NO" syncable="YES"/>
|
<attribute name="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="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="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="statsOverlayLevel" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||||
<attribute name="renderingBackend" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
<attribute name="streamDimensionScale" optional="YES" attributeType="Double" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||||
|
<attribute name="streamingRadialMenuButton" attributeType="Integer 32" defaultValueString="16" usesScalarValueType="NO" syncable="YES"/>
|
||||||
|
<attribute name="streamingRadialMenuDelay" optional="YES" attributeType="Float" defaultValueString="0.5" usesScalarValueType="NO" syncable="YES"/>
|
||||||
<attribute name="swapABXYButtons" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
<attribute name="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="touchMode" optional="YES" attributeType="Integer 16" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||||
<attribute name="touchMoveEventInterval" optional="YES" attributeType="Integer 16" defaultValueString="320" usesScalarValueType="NO" syncable="YES"/>
|
<attribute name="touchMoveEventInterval" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||||
<attribute name="touchPointerVelocityFactor" optional="YES" attributeType="Float" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
<attribute name="touchPointerVelocityFactor" optional="YES" attributeType="Float" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||||
|
<attribute name="touchPointTracking" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||||
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
|
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
|
||||||
<attribute name="unlockDisplayOrientation" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
<attribute name="unlockDisplayOrientation" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||||
|
<attribute name="useBuiltinMic" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
|
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
|
||||||
</entity>
|
</entity>
|
||||||
</model>
|
</model>
|
||||||