Compare commits
1475 Commits
1.4.3
...
spidy/cell
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
625389ab96 | ||
|
|
f7e34cbbe9 | ||
|
|
0fe8d7fed9 | ||
|
|
1a1aa42e02 | ||
|
|
7ff4b48223 | ||
|
|
4801f6e8f2 | ||
|
|
ff28f6fa8f | ||
|
|
2d141afbaf | ||
|
|
06e38aae00 | ||
|
|
d8783230a7 | ||
|
|
6c479bfede | ||
|
|
591913743e | ||
|
|
77d95fe278 | ||
|
|
0cd62d371a | ||
|
|
9771f6bb9a | ||
|
|
e553efbad5 | ||
|
|
a4dfd28a3c | ||
|
|
a7496e08e3 | ||
|
|
2f3be07b5d | ||
|
|
cbde3e6495 | ||
|
|
117f31e158 | ||
|
|
420efcbb11 | ||
|
|
ae8e9a3506 | ||
|
|
3785891923 | ||
|
|
e85db67ac7 | ||
|
|
39d0835f5b | ||
|
|
729fca9100 | ||
|
|
c6703d66c1 | ||
|
|
2197161d55 | ||
|
|
cfaf79f878 | ||
|
|
2bea980d1f | ||
|
|
f11e27c712 | ||
|
|
b316e84f0d | ||
|
|
4668f8499b | ||
|
|
f9aedaba04 | ||
|
|
8cb3de9ab5 | ||
|
|
ca57d58219 | ||
|
|
6a56fbd206 | ||
|
|
cec3825de0 | ||
|
|
b3e99d1ae3 | ||
|
|
7243d79646 | ||
|
|
e50da6603c | ||
|
|
136f07e4b9 | ||
|
|
f4d367b857 | ||
|
|
3e96583525 | ||
|
|
84bb1f7c08 | ||
|
|
a5aec978bb | ||
|
|
d677292bd3 | ||
|
|
722f67d3c7 | ||
|
|
07e0aea24f | ||
|
|
673f2ba693 | ||
|
|
0070519736 | ||
|
|
359b38609b | ||
|
|
348a24d885 | ||
|
|
ebdd0d4cb4 | ||
|
|
614ab4cd33 | ||
|
|
ca38008328 | ||
|
|
e5713fa3a9 | ||
|
|
35e3cf1e14 | ||
|
|
ca8c394ae0 | ||
|
|
5323fdadcf | ||
|
|
e43bff5f8f | ||
|
|
4659d617f8 | ||
|
|
87fe360927 | ||
|
|
71212130c5 | ||
|
|
6370105c85 | ||
|
|
15f4ae7b5a | ||
|
|
08e11eece4 | ||
|
|
1a43ad4aa3 | ||
|
|
a5ec12e3df | ||
|
|
c0400446bc | ||
|
|
13c3d0c1e9 | ||
|
|
92edd4b800 | ||
|
|
eb0e1326b9 | ||
|
|
a8fd1a3e83 | ||
|
|
533655c96b | ||
|
|
a322f9b5e9 | ||
|
|
4805ed8d3b | ||
|
|
caa38cfcae | ||
|
|
77833c6ffc | ||
|
|
61086a681a | ||
|
|
cf81d2876c | ||
|
|
e5144d112a | ||
|
|
bf766c1b84 | ||
|
|
e608211f32 | ||
|
|
ef9135d7ee | ||
|
|
8b2c92d94c | ||
|
|
ac486a4723 | ||
|
|
c3847276f7 | ||
|
|
e43e962bcc | ||
|
|
0245f6072a | ||
|
|
ac63314a91 | ||
|
|
803eb615cd | ||
|
|
b218437388 | ||
|
|
df2ffb1235 | ||
|
|
7ed8c20dfc | ||
|
|
203a7e6f11 | ||
|
|
c6f843ebc3 | ||
|
|
13d924abf6 | ||
|
|
1641f6e93f | ||
|
|
c0a81edf6b | ||
|
|
e9391b7a21 | ||
|
|
8935ba08b4 | ||
|
|
eb539cd7f6 | ||
|
|
172481fee5 | ||
|
|
28de1953c4 | ||
|
|
29ed2afb3d | ||
|
|
332f56324c | ||
|
|
341e498b3f | ||
|
|
1a0a7eb9d7 | ||
|
|
5c55f45c84 | ||
|
|
c62cc3dabd | ||
|
|
acb8af5645 | ||
|
|
0da743e9a6 | ||
|
|
abd3735ae4 | ||
|
|
1eba9b60cb | ||
|
|
3e74e4ae5d | ||
|
|
e8798499d3 | ||
|
|
dc4a543b3b | ||
|
|
1649bb73d9 | ||
|
|
337349c324 | ||
|
|
d81c59ecf9 | ||
|
|
e62d9023f8 | ||
|
|
c5def98e87 | ||
|
|
48d691204b | ||
|
|
e2836fcd70 | ||
|
|
a8395ebcdc | ||
|
|
55f4aa7deb | ||
|
|
61989e7d40 | ||
|
|
d13e469cf2 | ||
|
|
6ed5acdb40 | ||
|
|
ba825d4218 | ||
|
|
720a397dd4 | ||
|
|
e29d9f7904 | ||
|
|
f69b293004 | ||
|
|
4e10527f03 | ||
|
|
46871f63ed | ||
|
|
bb8a1b57cd | ||
|
|
9283ce3289 | ||
|
|
efbcafc7cc | ||
|
|
c721fa01f9 | ||
|
|
bd5b011a62 | ||
|
|
b3382df216 | ||
|
|
5db45565f3 | ||
|
|
4e71e5d879 | ||
|
|
a967a7aaad | ||
|
|
74749b6502 | ||
|
|
bf8a42d490 | ||
|
|
a2f37c8895 | ||
|
|
1ea2f0e5e6 | ||
|
|
ee03d9fa51 | ||
|
|
51f2588d3c | ||
|
|
5416deddbe | ||
|
|
3525fb4afa | ||
|
|
82c04cd423 | ||
|
|
1666c40bd8 | ||
|
|
7ae10c6022 | ||
|
|
a547d2bc8a | ||
|
|
fba5ca4e12 | ||
|
|
2e01116f1f | ||
|
|
2e247f1773 | ||
|
|
e21e116535 | ||
|
|
8a77090586 | ||
|
|
d1be3f1914 | ||
|
|
198fb45c9d | ||
|
|
066d3e11a2 | ||
|
|
68470b61c5 | ||
|
|
6c5cf2ef06 | ||
|
|
73c86661be | ||
|
|
ff4a000406 | ||
|
|
c69c8c69fa | ||
|
|
e165273554 | ||
|
|
e3d08ebf16 | ||
|
|
3f959f111c | ||
|
|
950e54a04b | ||
|
|
a22fb3fd2f | ||
|
|
7971a896c6 | ||
|
|
210d235345 | ||
|
|
b39840e344 | ||
|
|
8b9e473ace | ||
|
|
178145f57e | ||
|
|
7f18e5a678 | ||
|
|
0b43b6d152 | ||
|
|
397bd21ff6 | ||
|
|
a642553b43 | ||
|
|
5e753855a3 | ||
|
|
d6ae65420d | ||
|
|
2c29e3f902 | ||
|
|
e01e31f3d5 | ||
|
|
caf491aa00 | ||
|
|
b80e0757e8 | ||
|
|
1f1b7ff083 | ||
|
|
f2e3a31520 | ||
|
|
79794f7fd5 | ||
|
|
157bfed965 | ||
|
|
e3d0dac09a | ||
|
|
e1d8887907 | ||
|
|
7e45d4fa33 | ||
|
|
015863b4fc | ||
|
|
a7b84bbc20 | ||
|
|
dded866025 | ||
|
|
88519ff5e8 | ||
|
|
3f2f93d2b5 | ||
|
|
c1910f49eb | ||
|
|
571a65a46a | ||
|
|
2c07d14a00 | ||
|
|
a2a199d64e | ||
|
|
893c628e80 | ||
|
|
523c543690 | ||
|
|
ebf055dc7d | ||
|
|
daa5ba1a9f | ||
|
|
54a895c11a | ||
|
|
2eeaeca8f4 | ||
|
|
7af0992a2b | ||
|
|
cf0a2001f0 | ||
|
|
cfe2111844 | ||
|
|
09e39d1ead | ||
|
|
dd8d6d447f | ||
|
|
992b6b7262 | ||
|
|
1f9326b452 | ||
|
|
36f64f6c0a | ||
|
|
901b9ae337 | ||
|
|
a173455e2d | ||
|
|
adee94819a | ||
|
|
c41d25c19b | ||
|
|
1203c9f5f2 | ||
|
|
1b85f6532b | ||
|
|
f080379994 | ||
|
|
a9a698b704 | ||
|
|
f9f56c4d66 | ||
|
|
fd03b33e9b | ||
|
|
2737384147 | ||
|
|
e005846324 | ||
|
|
bfed227940 | ||
|
|
8cfbe309ef | ||
|
|
d0eeefbc34 | ||
|
|
d716d88d33 | ||
|
|
5b69eb7bef | ||
|
|
21ab603756 | ||
|
|
7af8f5c817 | ||
|
|
460389f9c1 | ||
|
|
9b50ed83d3 | ||
|
|
89ec42ca87 | ||
|
|
0ab47360ff | ||
|
|
93ca83528b | ||
|
|
e39b9fe309 | ||
|
|
4cb85f3d59 | ||
|
|
7dc37d82e3 | ||
|
|
b7b5f50e69 | ||
|
|
7642c2f948 | ||
|
|
6cf8d4b48a | ||
|
|
d96056762f | ||
|
|
8f20b5bb8d | ||
|
|
7d2a6a9189 | ||
|
|
f7efb6569e | ||
|
|
4bc76fe93b | ||
|
|
59a5495ec0 | ||
|
|
7353a1f28b | ||
|
|
b757410044 | ||
|
|
5058658b66 | ||
|
|
f542a52bda | ||
|
|
976f4e1041 | ||
|
|
77e223e541 | ||
|
|
82cb14df6c | ||
|
|
6c559325b9 | ||
|
|
e662ba64fa | ||
|
|
49705a71f9 | ||
|
|
78081947c4 | ||
|
|
182dfb3c75 | ||
|
|
90c6c64e71 | ||
|
|
6c60c2092c | ||
|
|
90a1f4dd83 | ||
|
|
9597c7deb6 | ||
|
|
d045c0ed4d | ||
|
|
3ea24fdfea | ||
|
|
c19c68a2cf | ||
|
|
46ccbe5aad | ||
|
|
7ac485def0 | ||
|
|
7f60048b0c | ||
|
|
1d030a9550 | ||
|
|
5b8ca13565 | ||
|
|
ad431fb4b7 | ||
|
|
26b6c8d034 | ||
|
|
8ebb0d0f35 | ||
|
|
17018ea20f | ||
|
|
67f3c3a561 | ||
|
|
a2812c0528 | ||
|
|
a96fb13372 | ||
|
|
76070ffaa1 | ||
|
|
cec00769a9 | ||
|
|
a36043840f | ||
|
|
c0de04183c | ||
|
|
da949ec85f | ||
|
|
5a369574cc | ||
|
|
6650d3b73f | ||
|
|
e50cce0d5e | ||
|
|
148250f29a | ||
|
|
d6fc92cf5f | ||
|
|
99b16b83b6 | ||
|
|
792ca96ff3 | ||
|
|
3494c5b33b | ||
|
|
49502ce1ef | ||
|
|
eafeb97fa6 | ||
|
|
259870c92e | ||
|
|
e1cbec3864 | ||
|
|
779a82f3d4 | ||
|
|
d9d9a9a156 | ||
|
|
a7b31ec7a2 | ||
|
|
63a3203e50 | ||
|
|
e27c5f0b87 | ||
|
|
e298b440e8 | ||
|
|
03f46515ef | ||
|
|
81542d253f | ||
|
|
8bf3aef06c | ||
|
|
8a21c66927 | ||
|
|
44ade05e53 | ||
|
|
fd402f924f | ||
|
|
1f83ea00d3 | ||
|
|
d07b3e6c3a | ||
|
|
c0e780cbbd | ||
|
|
c86e00413b | ||
|
|
d2ed5bff57 | ||
|
|
07ed25ab54 | ||
|
|
2568f41e20 | ||
|
|
8ba28d0cd4 | ||
|
|
aa655fc5a3 | ||
|
|
d3d609550e | ||
|
|
fc5355345e | ||
|
|
aa1ed04bce | ||
|
|
2899e3ea5f | ||
|
|
6ee90f6c2a | ||
|
|
2f603778d6 | ||
|
|
ac62612a18 | ||
|
|
b8b32d501c | ||
|
|
edcdf94383 | ||
|
|
bce824254b | ||
|
|
846285eb1f | ||
|
|
74a231242e | ||
|
|
1c02da8806 | ||
|
|
f85dcdcd4a | ||
|
|
f477115003 | ||
|
|
79fc75edbd | ||
|
|
6d7d06a85e | ||
|
|
afb393b80b | ||
|
|
cfdc1aa82c | ||
|
|
2466c4d5c9 | ||
|
|
673eff4a51 | ||
|
|
3c73418fc3 | ||
|
|
b72b46b864 | ||
|
|
69a01a3262 | ||
|
|
becc626027 | ||
|
|
90fbb28b54 | ||
|
|
4688e9b927 | ||
|
|
7bcd0ea748 | ||
|
|
6a520b3410 | ||
|
|
f2ab214f27 | ||
|
|
de601cfacb | ||
|
|
b7a04d59b4 | ||
|
|
fec02cd80a | ||
|
|
9b1d65b571 | ||
|
|
be640930ce | ||
|
|
31aeec6b38 | ||
|
|
d7aa3b405d | ||
|
|
5d27397f03 | ||
|
|
bea54fa748 | ||
|
|
3391058475 | ||
|
|
55aa893b21 | ||
|
|
2ebd234ec8 | ||
|
|
2b3b60819e | ||
|
|
b0e43b8b97 | ||
|
|
4514fe1c2c | ||
|
|
1fbec33719 | ||
|
|
74b6fb6ec0 | ||
|
|
703db062e6 | ||
|
|
29627504cc | ||
|
|
1992ecd3a2 | ||
|
|
36ac3af7dc | ||
|
|
786bf4ac63 | ||
|
|
86d7afb95d | ||
|
|
81af866268 | ||
|
|
abc7b8d933 | ||
|
|
9ac26a99a8 | ||
|
|
80030acb87 | ||
|
|
0c958dad19 | ||
|
|
9ea94912d4 | ||
|
|
36743c0cf4 | ||
|
|
870ef0c47f | ||
|
|
5c808ec59e | ||
|
|
20b424c97c | ||
|
|
1b8daa59c0 | ||
|
|
71eb77cfda | ||
|
|
5cb40de113 | ||
|
|
9716ee6152 | ||
|
|
cf09843538 | ||
|
|
d625b381d9 | ||
|
|
05332ca122 | ||
|
|
be31611cb7 | ||
|
|
3773a051ab | ||
|
|
142b9c6810 | ||
|
|
cccbe3a80b | ||
|
|
0ad9ceaa95 | ||
|
|
8946ab8a65 | ||
|
|
a981201016 | ||
|
|
5da80863b9 | ||
|
|
850b6890e2 | ||
|
|
e370034e0b | ||
|
|
8add1d0f4a | ||
|
|
ba94886ba9 | ||
|
|
dddb9c5ddb | ||
|
|
389af4d5e6 | ||
|
|
aa9fda7a97 | ||
|
|
9f7f73f835 | ||
|
|
947b31881f | ||
|
|
7e232cafbe | ||
|
|
91ea34110b | ||
|
|
47b69b40aa | ||
|
|
99a3746e1a | ||
|
|
6ba642335b | ||
|
|
869b2dc92a | ||
|
|
f692da047a | ||
|
|
f352aaf9c5 | ||
|
|
299b5ca04c | ||
|
|
d83891d794 | ||
|
|
1b20f17052 | ||
|
|
583de6c0ec | ||
|
|
140193c040 | ||
|
|
50076f6e96 | ||
|
|
a53d45b1dc | ||
|
|
65562602af | ||
|
|
c20ed78cec | ||
|
|
2fa9dbb859 | ||
|
|
edf3281eee | ||
|
|
b89d086e79 | ||
|
|
67271c479c | ||
|
|
7977267107 | ||
|
|
a49e16f591 | ||
|
|
57059967c6 | ||
|
|
c15459e313 | ||
|
|
86ec59d204 | ||
|
|
6fc9ad010d | ||
|
|
932e66deca | ||
|
|
59a72ad096 | ||
|
|
d7384cfae9 | ||
|
|
e33a40ecb1 | ||
|
|
21b2a869a1 | ||
|
|
34c503da4b | ||
|
|
cd42cc827f | ||
|
|
83d8d2e38a | ||
|
|
c3820136a6 | ||
|
|
89347ffffa | ||
|
|
98125e93aa | ||
|
|
2aebaf80e0 | ||
|
|
1d19a31a86 | ||
|
|
ac8f82c30a | ||
|
|
b03b7bfe68 | ||
|
|
f9911d285d | ||
|
|
20cf2326c6 | ||
|
|
bff9eef2dd | ||
|
|
45df1c10cb | ||
|
|
61b2a9bb82 | ||
|
|
df43561494 | ||
|
|
4551451b57 | ||
|
|
727ab0b554 | ||
|
|
d53e36633d | ||
|
|
9ddc27f6ca | ||
|
|
1ece687e37 | ||
|
|
2ccf01cf9c | ||
|
|
c19b541739 | ||
|
|
27ca2f285b | ||
|
|
93b6da4855 | ||
|
|
6adf55b4b6 | ||
|
|
bca5c4e9a4 | ||
|
|
5e48e36ce2 | ||
|
|
4da8316c12 | ||
|
|
81c3825c92 | ||
|
|
3df1297b1c | ||
|
|
d89a15f74b | ||
|
|
55ccb723e5 | ||
|
|
6c0cfdf99e | ||
|
|
9a6272f8e0 | ||
|
|
07b1750a9c | ||
|
|
9cf61bd4df | ||
|
|
e34f3ce201 | ||
|
|
5d5da9e910 | ||
|
|
a341a15a5e | ||
|
|
58b6c0d6ac | ||
|
|
297a71bf91 | ||
|
|
d657ffc8ca | ||
|
|
a1865b6725 | ||
|
|
fa01fa708e | ||
|
|
80fc8e7a1e | ||
|
|
891609b64e | ||
|
|
021b49c436 | ||
|
|
f39ebfb905 | ||
|
|
c4117c0ac9 | ||
|
|
00423bec08 | ||
|
|
46bd977371 | ||
|
|
4410775aec | ||
|
|
b760418252 | ||
|
|
a7b28d5027 | ||
|
|
f83303a6b7 | ||
|
|
76ef018638 | ||
|
|
2aaa7761fc | ||
|
|
ea2600aba9 | ||
|
|
3f6688523a | ||
|
|
641c716d57 | ||
|
|
693969dc28 | ||
|
|
fd8dd20c1b | ||
|
|
7747994c80 | ||
|
|
9b885085c9 | ||
|
|
e605399633 | ||
|
|
a7d52db453 | ||
|
|
c1bbca9ed7 | ||
|
|
be8bf44784 | ||
|
|
15b3cd5f2d | ||
|
|
f926f596aa | ||
|
|
74bccf4caf | ||
|
|
5161c506f0 | ||
|
|
f19ae2f422 | ||
|
|
68a87f55bf | ||
|
|
6d10933a83 | ||
|
|
fa2689454b | ||
|
|
e0222c5f7c | ||
|
|
b60f9f8e08 | ||
|
|
f5b63b52b4 | ||
|
|
e03813c19e | ||
|
|
86ae06e0c8 | ||
|
|
a38eba8449 | ||
|
|
f9bd65a1b5 | ||
|
|
7f9ee81150 | ||
|
|
641e7d5f2e | ||
|
|
f5c40ae571 | ||
|
|
4f6eaf1aac | ||
|
|
c83d486269 | ||
|
|
99db3dc086 | ||
|
|
038efd9f9e | ||
|
|
8c85290c74 | ||
|
|
0fa941e6ef | ||
|
|
26c173c479 | ||
|
|
8f9cf96f3d | ||
|
|
22f3c881a1 | ||
|
|
0d79a01d74 | ||
|
|
a3c373108d | ||
|
|
ea0564126e | ||
|
|
2afaf73fc5 | ||
|
|
e8f676b10b | ||
|
|
9bb6f7eac0 | ||
|
|
07bc34ae7a | ||
|
|
3aa041d2ad | ||
|
|
f7640e35d1 | ||
|
|
efce9a8579 | ||
|
|
5a2f32704c | ||
|
|
177d453491 | ||
|
|
bec6ca9eec | ||
|
|
254a9773ec | ||
|
|
b9dd6432a1 | ||
|
|
d89c0f3e36 | ||
|
|
fd89f35246 | ||
|
|
ee410605e8 | ||
|
|
f884d72a8b | ||
|
|
bd3beb5983 | ||
|
|
44e08b2d66 | ||
|
|
d560e14423 | ||
|
|
7dfbba9b00 | ||
|
|
7ad8db7bdc | ||
|
|
64a9281e6e | ||
|
|
16c71be7f9 | ||
|
|
7d380da5d1 | ||
|
|
a09f4bbd7a | ||
|
|
5b275d6811 | ||
|
|
ea506b904d | ||
|
|
6985d0f476 | ||
|
|
357211e917 | ||
|
|
000cf1ca22 | ||
|
|
a2553f4c3f | ||
|
|
4b9d81cd13 | ||
|
|
e5824ddd35 | ||
|
|
23b6623020 | ||
|
|
bc7311c159 | ||
|
|
39f7e60e8b | ||
|
|
b88044757f | ||
|
|
7f2bd494b5 | ||
|
|
b2dcdc02c7 | ||
|
|
9c7d222a9e | ||
|
|
82cacb1b51 | ||
|
|
f44c3c18a2 | ||
|
|
bfea606bee | ||
|
|
5dfb36ca48 | ||
|
|
654f73f4ee | ||
|
|
5145e355ce | ||
|
|
20cd6d98fc | ||
|
|
12ca34f40f | ||
|
|
fc99fb32a4 | ||
|
|
779887e582 | ||
|
|
6fa2fa16f7 | ||
|
|
bdb1d68b6b | ||
|
|
404bd1450b | ||
|
|
06d28ca663 | ||
|
|
ed1365281f | ||
|
|
824fc48e77 | ||
|
|
8695c412d7 | ||
|
|
e4dfe1125a | ||
|
|
589ece3860 | ||
|
|
a5b7abea0d | ||
|
|
0a58a1fdc3 | ||
|
|
aa2409178b | ||
|
|
960492f1d0 | ||
|
|
726ba873fc | ||
|
|
f1f3e49bc5 | ||
|
|
d00e6de8a2 | ||
|
|
f24f721845 | ||
|
|
b7f5acd332 | ||
|
|
65598e2cd5 | ||
|
|
806421f19f | ||
|
|
9df4026ed4 | ||
|
|
17abda66ba | ||
|
|
f16e9c75b4 | ||
|
|
f9c22ff617 | ||
|
|
2cfc307359 | ||
|
|
66a17bc27f | ||
|
|
5da3974795 | ||
|
|
a8f0d9da9b | ||
|
|
4a3dbc20d6 | ||
|
|
624c4086f1 | ||
|
|
d54b7aa3bf | ||
|
|
1646c7cb83 | ||
|
|
ec0c0df78c | ||
|
|
1d1be0a8f9 | ||
|
|
7afd11fdc6 | ||
|
|
8fcc5622e1 | ||
|
|
8759ed091f | ||
|
|
d2d90ab9da | ||
|
|
2e987647dc | ||
|
|
e96a5114e5 | ||
|
|
6c7223b991 | ||
|
|
b5bcf229ae | ||
|
|
b60536dded | ||
|
|
881091595c | ||
|
|
8f1a91df1b | ||
|
|
2a7926539f | ||
|
|
2017584da4 | ||
|
|
db57de28d6 | ||
|
|
1fcdb18477 | ||
|
|
35561336c6 | ||
|
|
2f9f3e6c72 | ||
|
|
3c02938bfd | ||
|
|
23386c88ea | ||
|
|
0965299e6f | ||
|
|
65485ecdf5 | ||
|
|
3d70271306 | ||
|
|
83d39666d2 | ||
|
|
7409c0ef4e | ||
|
|
b8030ed0a9 | ||
|
|
a537e70459 | ||
|
|
9f38601102 | ||
|
|
f82743af98 | ||
|
|
76f8fc6d9a | ||
|
|
9d5248e2e8 | ||
|
|
9217044b1d | ||
|
|
5d87650553 | ||
|
|
f8d3d4971f | ||
|
|
7e01972cd4 | ||
|
|
f294f1045a | ||
|
|
3086492cbc | ||
|
|
583226392c | ||
|
|
531f8b5a0d | ||
|
|
9f04b3a9f1 | ||
|
|
95a3bbf6b9 | ||
|
|
b458e75098 | ||
|
|
5c526dba82 | ||
|
|
4e84b9ea27 | ||
|
|
cea53ca158 | ||
|
|
40855063c9 | ||
|
|
41a68a1897 | ||
|
|
9c8c1b4311 | ||
|
|
5e383c2148 | ||
|
|
dd88e03b4c | ||
|
|
9a3cb2b5ec | ||
|
|
01084039df | ||
|
|
feb35d2b6e | ||
|
|
3e941cfb0d | ||
|
|
931a34cf7d | ||
|
|
43dc332329 | ||
|
|
c8ae28003f | ||
|
|
cf32f25457 | ||
|
|
fee5309b50 | ||
|
|
b8c7f51d94 | ||
|
|
d00ce2bc11 | ||
|
|
2402655e56 | ||
|
|
283f7a998a | ||
|
|
2b355dbf8c | ||
|
|
ead80e50f9 | ||
|
|
71c53d81c2 | ||
|
|
8498f3df94 | ||
|
|
faa1555294 | ||
|
|
110f70e34c | ||
|
|
cd3b4c46b4 | ||
|
|
60a0657721 | ||
|
|
62c655c927 | ||
|
|
84b0f134b0 | ||
|
|
09902a3f71 | ||
|
|
3db004c733 | ||
|
|
4a566d8823 | ||
|
|
d37e033d35 | ||
|
|
afca84a852 | ||
|
|
9e0c5dab1a | ||
|
|
77eaa2fb5a | ||
|
|
b81ba38d1c | ||
|
|
d5a9050464 | ||
|
|
3c99fe22d3 | ||
|
|
f5b47d0508 | ||
|
|
db84a38519 | ||
|
|
c466e7698a | ||
|
|
6a8283a163 | ||
|
|
1ee334ca68 | ||
|
|
a3a34eb9ef | ||
|
|
db633c90be | ||
|
|
5e0e4dbd4e | ||
|
|
090967c21d | ||
|
|
6c8e9b886d | ||
|
|
2bb2eea226 | ||
|
|
a2bb26a86e | ||
|
|
948a8eb90b | ||
|
|
e66e223189 | ||
|
|
2aee6ac57e | ||
|
|
66abac80d8 | ||
|
|
a1e0d5f834 | ||
|
|
6a1181b21f | ||
|
|
c25ae10873 | ||
|
|
2842c8f669 | ||
|
|
3161892585 | ||
|
|
489843f987 | ||
|
|
dc0b30ab67 | ||
|
|
c3235cc554 | ||
|
|
6568e5918a | ||
|
|
91fba6db99 | ||
|
|
6b7e9a66f1 | ||
|
|
3682b65a4a | ||
|
|
117412645b | ||
|
|
c784ff6925 | ||
|
|
cf477024fc | ||
|
|
d595b7037f | ||
|
|
0c5007c8d8 | ||
|
|
8a87445d1f | ||
|
|
76a693fae4 | ||
|
|
9c150d5f4a | ||
|
|
e5febcdc6c | ||
|
|
1e969a0888 | ||
|
|
72bb549ea3 | ||
|
|
3c7cfdd91f | ||
|
|
b21f80cdd7 | ||
|
|
867a9c77e6 | ||
|
|
bc2d2c18fc | ||
|
|
ab923d245d | ||
|
|
fcf1b9ae03 | ||
|
|
59896e4f89 | ||
|
|
2a9f88c810 | ||
|
|
e98b0a3758 | ||
|
|
0cb6da7be4 | ||
|
|
fc3ff41fc4 | ||
|
|
719ddc8263 | ||
|
|
9f6b1284bb | ||
|
|
bfb4a90fdd | ||
|
|
d1b6bedc30 | ||
|
|
b78c75d829 | ||
|
|
4518f07b5b | ||
|
|
b4e18c50d3 | ||
|
|
d18482a04a | ||
|
|
f3d9dd777d | ||
|
|
e117c4b9a3 | ||
|
|
95666178e5 | ||
|
|
56403466b9 | ||
|
|
c7344ef548 | ||
|
|
71c4abfce8 | ||
|
|
0613af2240 | ||
|
|
dd832ad6df | ||
|
|
8cf7bc9998 | ||
|
|
c1cf11c04c | ||
|
|
0058c40f46 | ||
|
|
1397389f95 | ||
|
|
6a2d3e1d22 | ||
|
|
46ac704013 | ||
|
|
0fdab2a5c5 | ||
|
|
8f4586bfef | ||
|
|
52d0c9861f | ||
|
|
feace61eb4 | ||
|
|
1f5cc8f283 | ||
|
|
60b8520237 | ||
|
|
6a67c5e9a2 | ||
|
|
bcc241518c | ||
|
|
6dfa8f1556 | ||
|
|
19dde692b2 | ||
|
|
14dc93b5d2 | ||
|
|
547620235e | ||
|
|
b43fd0a54b | ||
|
|
8b782c9416 | ||
|
|
aab4e62e24 | ||
|
|
1713fccfc4 | ||
|
|
83ece72ae1 | ||
|
|
d60bcc49e1 | ||
|
|
bc9c37adda | ||
|
|
2583c7f617 | ||
|
|
fea5229e02 | ||
|
|
68be615057 | ||
|
|
370cafcba0 | ||
|
|
f923c1602e | ||
|
|
50a85be872 | ||
|
|
aae4725a3c | ||
|
|
9d76ee9f19 | ||
|
|
34a101b796 | ||
|
|
49b1fd751c | ||
|
|
4c5bf7bb7d | ||
|
|
2d71631d93 | ||
|
|
fa0d933956 | ||
|
|
b5d6384a07 | ||
|
|
d39644a4c9 | ||
|
|
a2feb34dc1 | ||
|
|
7e5fe64153 | ||
|
|
44175d071c | ||
|
|
bae26de444 | ||
|
|
b78707808d | ||
|
|
d41518581a | ||
|
|
4abbfe6142 | ||
|
|
dae813d80c | ||
|
|
af89b178ad | ||
|
|
8c269207fd | ||
|
|
42ecd38517 | ||
|
|
9f7d4dee49 | ||
|
|
458b8e491e | ||
|
|
495e621e69 | ||
|
|
c986512b5f | ||
|
|
d277754ae5 | ||
|
|
2ef2e2f26b | ||
|
|
23a53034fa | ||
|
|
ce57d72a78 | ||
|
|
502b89d890 | ||
|
|
5f0015fad0 | ||
|
|
c81236957b | ||
|
|
970ab38b27 | ||
|
|
8a5c31b81d | ||
|
|
8508fe79b5 | ||
|
|
3859e98801 | ||
|
|
a759c7be9e | ||
|
|
12fc6cf6e2 | ||
|
|
580db6530e | ||
|
|
9c67c237ee | ||
|
|
357d85a72e | ||
|
|
88ad828ce0 | ||
|
|
a95625a34a | ||
|
|
95e00d81f5 | ||
|
|
c2e386a5c5 | ||
|
|
a76aade4ff | ||
|
|
65c9986103 | ||
|
|
9e2b9b6639 | ||
|
|
cf373634d7 | ||
|
|
b3d5d976b4 | ||
|
|
c3c31995ce | ||
|
|
7e92e17429 | ||
|
|
88ab8fa8d7 | ||
|
|
ebe78932bf | ||
|
|
2e613e6d15 | ||
|
|
35ee92db12 | ||
|
|
04d9f760ad | ||
|
|
4f52743be8 | ||
|
|
32cae7a5b2 | ||
|
|
c2c0e3b790 | ||
|
|
6d36a30787 | ||
|
|
48a86ec6de | ||
|
|
5cff914ff3 | ||
|
|
70ea725ce3 | ||
|
|
78f12e45f9 | ||
|
|
e5061acc20 | ||
|
|
2d7bc51d30 | ||
|
|
9128b67ee8 | ||
|
|
551c004476 | ||
|
|
ed6a8d1379 | ||
|
|
766fb89e0b | ||
|
|
c5b8cb4459 | ||
|
|
0deae92829 | ||
|
|
cc5d2f1813 | ||
|
|
41151d0d49 | ||
|
|
52702264a3 | ||
|
|
6e297e1278 | ||
|
|
e3bb9b425f | ||
|
|
79255be79c | ||
|
|
7c836f5ba1 | ||
|
|
938bcd14ad | ||
|
|
229d79fc05 | ||
|
|
2d3dac2e1d | ||
|
|
e23f5e7894 | ||
|
|
571d27c814 | ||
|
|
dde6bd4fe3 | ||
|
|
6e6dbd9329 | ||
|
|
258268f5ef | ||
|
|
9ae49977fb | ||
|
|
d61c54fa60 | ||
|
|
980699af6f | ||
|
|
cc5c280882 | ||
|
|
090456bba1 | ||
|
|
5354d4eb76 | ||
|
|
b986fae611 | ||
|
|
cfcfc3e928 | ||
|
|
f97548fc3a | ||
|
|
36913b425c | ||
|
|
822ea08d89 | ||
|
|
98dd6f3fe7 | ||
|
|
b3f0dbb155 | ||
|
|
6904d931c3 | ||
|
|
529466a9f7 | ||
|
|
77dc695ba1 | ||
|
|
e17776f651 | ||
|
|
0d2f355a74 | ||
|
|
2ce1576016 | ||
|
|
0f3be3c494 | ||
|
|
8c1ca8503a | ||
|
|
32a59c17f4 | ||
|
|
b4b4ceab0b | ||
|
|
be1f27bb9e | ||
|
|
ed10ddb1cb | ||
|
|
dbdb4b0f32 | ||
|
|
59e537362e | ||
|
|
6d96bf414f | ||
|
|
e7ba778a5f | ||
|
|
933d349cd5 | ||
|
|
3de24dcfce | ||
|
|
3275d16b8b | ||
|
|
5bb4cd1dad | ||
|
|
16b14441fa | ||
|
|
93a6272d30 | ||
|
|
0dc526f778 | ||
|
|
183e185812 | ||
|
|
e02453598c | ||
|
|
24af1b5b5f | ||
|
|
5864c283f6 | ||
|
|
be78fa4b91 | ||
|
|
b3abf69a02 | ||
|
|
c530dc11ae | ||
|
|
d368ddbd11 | ||
|
|
e5c6521a15 | ||
|
|
898a59768e | ||
|
|
a85bc93142 | ||
|
|
c6c1f9faa0 | ||
|
|
0eea19c9cc | ||
|
|
ed2270ff46 | ||
|
|
45b6c3b338 | ||
|
|
84e2284f56 | ||
|
|
1c0d0be622 | ||
|
|
a9ce0f487d | ||
|
|
07533e0365 | ||
|
|
ee5ddd4264 | ||
|
|
f519d22d81 | ||
|
|
51ed87086a | ||
|
|
1ca3aa3cdb | ||
|
|
0178c63f6a | ||
|
|
8a97c409fa | ||
|
|
3dd0735305 | ||
|
|
536f775baa | ||
|
|
00f7a684a3 | ||
|
|
d79b166a6a | ||
|
|
b3d827f56a | ||
|
|
40bcef1dcb | ||
|
|
6146f1bdaa | ||
|
|
f5d82d9ef0 | ||
|
|
b2a29ae606 | ||
|
|
98ccba53a2 | ||
|
|
9bfda36647 | ||
|
|
5710cdf19c | ||
|
|
20cf54bfcd | ||
|
|
2ce639e750 | ||
|
|
b1ed413c4f | ||
|
|
b8c3060037 | ||
|
|
c3ea4940d7 | ||
|
|
40e1225b87 | ||
|
|
0c171122b2 | ||
|
|
6d0f4bb3da | ||
|
|
5e2cc6e20c | ||
|
|
99cb43bbea | ||
|
|
ca7d8277f7 | ||
|
|
337d26333e | ||
|
|
ebb64d255b | ||
|
|
7dcb199f68 | ||
|
|
4334e887de | ||
|
|
4e84dc4cc8 | ||
|
|
1a1ed072bf | ||
|
|
ae457f07c4 | ||
|
|
00095942c3 | ||
|
|
d1caa5fc21 | ||
|
|
813e2f97ac | ||
|
|
bcb5a90f5e | ||
|
|
020a1a3149 | ||
|
|
c4d649ec58 | ||
|
|
c02cf2c284 | ||
|
|
c30afd042e | ||
|
|
17640fe6cf | ||
|
|
2e4f6ee420 | ||
|
|
a3768d9221 | ||
|
|
80c3390363 | ||
|
|
a5e3869d8f | ||
|
|
aa7d7c2d02 | ||
|
|
015f205569 | ||
|
|
e59fb15926 | ||
|
|
173c585f2d | ||
|
|
6f8c27793e | ||
|
|
332b81c803 | ||
|
|
4b343b500d | ||
|
|
e87c537642 | ||
|
|
2e6300cce2 | ||
|
|
09514d15a6 | ||
|
|
0de23dcba0 | ||
|
|
bacb153151 | ||
|
|
a01aa299d8 | ||
|
|
44edbddbd8 | ||
|
|
79d677cf3c | ||
|
|
be39b6512f | ||
|
|
fcfeea35da | ||
|
|
7d0eb8c61e | ||
|
|
4d8438a6b6 | ||
|
|
f611244e35 | ||
|
|
546a978d3b | ||
|
|
70b23fb073 | ||
|
|
a56ca597d6 | ||
|
|
679e0228a8 | ||
|
|
e153394323 | ||
|
|
5bd1fcfcfd | ||
|
|
2a392ddc44 | ||
|
|
b5cb8bc0d9 | ||
|
|
fa170bcf98 | ||
|
|
7939d46949 | ||
|
|
ab9df8201a | ||
|
|
4a670ec091 | ||
|
|
10e57e59c4 | ||
|
|
b9ec43ef34 | ||
|
|
42197cd375 | ||
|
|
704852973b | ||
|
|
056b4200df | ||
|
|
250a7d8627 | ||
|
|
1ba51e161e | ||
|
|
32e58af896 | ||
|
|
312fa6fe76 | ||
|
|
afbe0837ba | ||
|
|
36ad2a720f | ||
|
|
901e3b14bb | ||
|
|
588d209f7b | ||
|
|
554c54e6be | ||
|
|
b0fac34ffc | ||
|
|
5ede9f7c6b | ||
|
|
c7254fd23e | ||
|
|
55fcea04af | ||
|
|
c212c0a6b2 | ||
|
|
a31fd6709a | ||
|
|
e367fd2b73 | ||
|
|
1ca67d0241 | ||
|
|
8ffa952ff9 | ||
|
|
da246fa30b | ||
|
|
13f306742e | ||
|
|
f3815dc45e | ||
|
|
d086254012 | ||
|
|
bc4d5ba097 | ||
|
|
c556783fe3 | ||
|
|
5fba4c12aa | ||
|
|
7e0dde3ece | ||
|
|
fc03e83531 | ||
|
|
4c441077c7 | ||
|
|
4a5ca81e9a | ||
|
|
75eebe8f8c | ||
|
|
271a8cdac5 | ||
|
|
25103c1188 | ||
|
|
d81058e606 | ||
|
|
693df54b3b | ||
|
|
ae6ed99dc4 | ||
|
|
14bd58e741 | ||
|
|
6d35a7a4ba | ||
|
|
46b0d1ceac | ||
|
|
67a66d2fcd | ||
|
|
43e90b57ea | ||
|
|
c80740e590 | ||
|
|
54ccb9611e | ||
|
|
8fcb897800 | ||
|
|
699eda5d1b | ||
|
|
d7d0a83550 | ||
|
|
e3c331c911 | ||
|
|
eda4dd6aec | ||
|
|
8ad7be474d | ||
|
|
a64435f155 | ||
|
|
fa160124d2 | ||
|
|
5765cb8330 | ||
|
|
f472b227bb | ||
|
|
d2b419c42e | ||
|
|
09d4de660f | ||
|
|
728dcd8523 | ||
|
|
93cf9bf6a9 | ||
|
|
50841f5e24 | ||
|
|
fc6d92d1fc | ||
|
|
7162a029bb | ||
|
|
d797ddd668 | ||
|
|
989e8c3aa6 | ||
|
|
08b79af242 | ||
|
|
0d2f346a30 | ||
|
|
39f1d5f5fd | ||
|
|
05008bb7f8 | ||
|
|
be90d6fc45 | ||
|
|
a1bcdf9924 | ||
|
|
b0e001393c | ||
|
|
2d08941f6a | ||
|
|
d0fef1f312 | ||
|
|
68342cb0d4 | ||
|
|
2b419212a7 | ||
|
|
b2cbc7e34d | ||
|
|
61247e575b | ||
|
|
31e18266d1 | ||
|
|
df8a8de889 | ||
|
|
8a037d6b29 | ||
|
|
47b555b98c | ||
|
|
0c2dae475e | ||
|
|
dc676d04d8 | ||
|
|
15b54bff50 | ||
|
|
47bd4b4c0b | ||
|
|
3c8b36ddfe | ||
|
|
608df3fddd | ||
|
|
c092c285ee | ||
|
|
93b745e379 | ||
|
|
c18db77ade | ||
|
|
2c0b167e6b | ||
|
|
313254d0c8 | ||
|
|
6f519c97d3 | ||
|
|
17a3e16b1d | ||
|
|
8199358088 | ||
|
|
412928eeaa | ||
|
|
51e1b935bd | ||
|
|
742b51e5e2 | ||
|
|
fdb5e2eebb | ||
|
|
0192f64cd2 | ||
|
|
193298ac87 | ||
|
|
a81cb81799 | ||
|
|
ad8a7fdc9b | ||
|
|
5440afcebe | ||
|
|
715d7e664c | ||
|
|
aa182cfa68 | ||
|
|
f92dd7a872 | ||
|
|
b02b9197d0 | ||
|
|
86d02be70c | ||
|
|
cb990978ee | ||
|
|
a103202c92 | ||
|
|
9d7b133037 | ||
|
|
f727f2a1a9 | ||
|
|
03034768d9 | ||
|
|
aed3e20e08 | ||
|
|
74bac6d986 | ||
|
|
7ebecc353a | ||
|
|
f0302b0d1e | ||
|
|
0b004ad089 | ||
|
|
c9001f068b | ||
|
|
96e0554aae | ||
|
|
31b4aadaba | ||
|
|
f46fa5392a | ||
|
|
3b6a17f193 | ||
|
|
aea77d3b8c | ||
|
|
7cfbe077db | ||
|
|
7bb620f941 | ||
|
|
5b0341a733 | ||
|
|
d99225da1f | ||
|
|
f279180a37 | ||
|
|
9a22018477 | ||
|
|
197119e56d | ||
|
|
dd055ddc5d | ||
|
|
94c3277245 | ||
|
|
fdaf402472 | ||
|
|
bce7764f75 | ||
|
|
70a258aae2 | ||
|
|
77cf00e8e4 | ||
|
|
de05579d1f | ||
|
|
c7d4b722d0 | ||
|
|
02b837c54b | ||
|
|
50e0e88cc2 | ||
|
|
c34245ff21 | ||
|
|
f1a8334f59 | ||
|
|
c8fc4ea500 | ||
|
|
39805bc103 | ||
|
|
2c615682df | ||
|
|
1257e4efac | ||
|
|
5e4a21087e | ||
|
|
2aaef99a54 | ||
|
|
161d3a795d | ||
|
|
9b671cb1a9 | ||
|
|
07d9a9f2c3 | ||
|
|
efabe7f536 | ||
|
|
82aead976e | ||
|
|
17be52c7b6 | ||
|
|
d0a196ec40 | ||
|
|
d484de185d | ||
|
|
96e4e7a4e8 | ||
|
|
4adb34b959 | ||
|
|
819bc12a68 | ||
|
|
eb23e5365f | ||
|
|
84e2faf8a8 | ||
|
|
84f58efc17 | ||
|
|
42254ee4a1 | ||
|
|
7c564aed7a | ||
|
|
8cdcb29274 | ||
|
|
403a369df9 | ||
|
|
5527912cd1 | ||
|
|
c8531dfe37 | ||
|
|
ed8bb2e5a1 | ||
|
|
dd66355488 | ||
|
|
fc3f83231c | ||
|
|
e70c712020 | ||
|
|
1b34aeaec4 | ||
|
|
2566bfa2ed | ||
|
|
ba06f2bbc6 | ||
|
|
1e4fe1680f | ||
|
|
2effb199a1 | ||
|
|
23c139320a | ||
|
|
f65eba606e | ||
|
|
7a2825da9a | ||
|
|
2975eddfe9 | ||
|
|
7f28eae954 | ||
|
|
789be5e942 | ||
|
|
85b114cdfd | ||
|
|
53d063e994 | ||
|
|
0ab081ccbc | ||
|
|
00ce9d64dc | ||
|
|
be20c024aa | ||
|
|
992fb9839a | ||
|
|
96ae2ee7ac | ||
|
|
379cecb08f | ||
|
|
9089b271b3 | ||
|
|
c9d522fad5 | ||
|
|
be80aa1512 | ||
|
|
c1d64a8027 | ||
|
|
1bc2aa9d38 | ||
|
|
e167ee104b | ||
|
|
43b85da314 | ||
|
|
b6c21c9766 | ||
|
|
874da8c8d6 | ||
|
|
989580d196 | ||
|
|
03ef54c37b | ||
|
|
ab56dda275 | ||
|
|
3a91f958e3 | ||
|
|
79913a0c9c | ||
|
|
d0fe64ecfa | ||
|
|
4da69685a1 | ||
|
|
bf560dd10d | ||
|
|
7ce76ee28d | ||
|
|
540e9bad29 | ||
|
|
22c2e2c4e5 | ||
|
|
f67d9dcdfa | ||
|
|
edfcadcbdc | ||
|
|
156bcc7d54 | ||
|
|
63ff912d76 | ||
|
|
6cbfaac4f4 | ||
|
|
6ad6e0d8c0 | ||
|
|
7c38bb03b9 | ||
|
|
5574172d99 | ||
|
|
bc8081ebae | ||
|
|
6ed6132c54 | ||
|
|
76c02c98d8 | ||
|
|
012a7885ff | ||
|
|
2b3d41d982 | ||
|
|
a56a48145b | ||
|
|
8dc097e23c | ||
|
|
0323520389 | ||
|
|
e1e395023d | ||
|
|
850214b103 | ||
|
|
02e9805482 | ||
|
|
0feae8402e | ||
|
|
042da53b54 | ||
|
|
aa1b2bace7 | ||
|
|
6b1b4d6015 | ||
|
|
ebeac417e5 | ||
|
|
be005616ea | ||
|
|
ec3a9b0615 | ||
|
|
0b3e651c4b | ||
|
|
426bdd3aa1 | ||
|
|
75e29d61f8 | ||
|
|
dfab283154 | ||
|
|
986c0d7edc | ||
|
|
918c44bc89 | ||
|
|
b8f680d74a | ||
|
|
76fcf6d545 | ||
|
|
c51d25c58b | ||
|
|
1efdba096c | ||
|
|
c4505b7c42 | ||
|
|
d5235bd40b | ||
|
|
cc3feb4843 | ||
|
|
353d105c04 | ||
|
|
070cb6c873 | ||
|
|
a066dda0f9 | ||
|
|
ac929c2603 | ||
|
|
9102402a18 | ||
|
|
54a0fc21d8 | ||
|
|
e2b8b7369e | ||
|
|
bcfbe515a4 | ||
|
|
46834ab5ce | ||
|
|
646000920f | ||
|
|
5ea83ccea1 | ||
|
|
03c6473685 | ||
|
|
d37890fac4 | ||
|
|
d5057ea8ea | ||
|
|
2cbebbe9b7 | ||
|
|
71b1885f74 | ||
|
|
2a8e3887ad | ||
|
|
2f92ce6bda | ||
|
|
9c58755317 | ||
|
|
9c1fe4d63b | ||
|
|
994d3c74fd | ||
|
|
a413c24b45 | ||
|
|
dc276a6393 | ||
|
|
cf6448845f | ||
|
|
b45c859861 | ||
|
|
fd81092392 | ||
|
|
26ef3073ae | ||
|
|
72a684a22f | ||
|
|
14529030be | ||
|
|
9570b797fd | ||
|
|
cdb5fb34dd | ||
|
|
ddff6a24f3 | ||
|
|
ae3c0acfc0 | ||
|
|
eef23ae49d | ||
|
|
2262f04fb3 | ||
|
|
b7a99ed508 | ||
|
|
38f68de3ea | ||
|
|
6b6f016189 | ||
|
|
82faa89912 | ||
|
|
dfd49de8d1 | ||
|
|
aa8dd80e54 | ||
|
|
751d9419ff | ||
|
|
643d7bf6fa | ||
|
|
afb20f79a9 | ||
|
|
6c2a83964b | ||
|
|
07daff261a | ||
|
|
8ddeb7f9fb | ||
|
|
1f7c089c70 | ||
|
|
f1f6852ab4 | ||
|
|
e5d66defbc | ||
|
|
29913c5b09 | ||
|
|
21d807d0c3 | ||
|
|
a6e5c32166 | ||
|
|
0b3e94b974 | ||
|
|
6db5aec672 | ||
|
|
73ff5fe9dc | ||
|
|
77694aac8e | ||
|
|
a04a27c1e3 | ||
|
|
7a547c70e3 | ||
|
|
b0abf0e7a5 | ||
|
|
48c49c6ec7 | ||
|
|
16564500e2 | ||
|
|
6f6b17b211 | ||
|
|
f1618ad9df | ||
|
|
947a14d6a2 | ||
|
|
f030ecd66f | ||
|
|
ec86fb77b0 | ||
|
|
cfa246adc5 | ||
|
|
ebb236e47c | ||
|
|
37b00d670b | ||
|
|
23516d0466 | ||
|
|
2b4f1ce1c2 | ||
|
|
c786858f17 | ||
|
|
a149cb231b | ||
|
|
3c9ef728e1 | ||
|
|
4257f58f96 | ||
|
|
ddfab31781 | ||
|
|
5e3e8f2809 | ||
|
|
fefa8b174d | ||
|
|
fb3d732a62 | ||
|
|
0658e323ae | ||
|
|
35046b33ff | ||
|
|
8f4c70c9cc | ||
|
|
c9bc14ab7f | ||
|
|
0f023905c8 | ||
|
|
61dc02514a | ||
|
|
590998fbaa | ||
|
|
7a1f631113 | ||
|
|
706229640f | ||
|
|
0397db51f7 | ||
|
|
e53928cf1e | ||
|
|
17b8fd6e6f | ||
|
|
03338b589c | ||
|
|
ac8560afd3 | ||
|
|
ef0cae6953 | ||
|
|
ed396b400d | ||
|
|
e6ef288a69 | ||
|
|
619d16ddd3 | ||
|
|
943fe79d3c | ||
|
|
310d4619b4 | ||
|
|
35e9d8752b | ||
|
|
aa057918ee | ||
|
|
5428ebf129 | ||
|
|
1384037430 | ||
|
|
c2bda2241c | ||
|
|
337d432fdd | ||
|
|
45d104fd2c | ||
|
|
00b2e25b01 | ||
|
|
1b16193e21 | ||
|
|
203aec2854 | ||
|
|
5997ac5424 | ||
|
|
b4f97aadf1 | ||
|
|
b7caaeb788 | ||
|
|
c3ca4fa8f3 | ||
|
|
d5563aafba | ||
|
|
c89c244225 | ||
|
|
08e540e12f | ||
|
|
2849eebb28 | ||
|
|
683307b9af | ||
|
|
5231ea1c1e | ||
|
|
35ae81c76c | ||
|
|
a4d7d94301 | ||
|
|
44b0092b44 | ||
|
|
c6b8f69ef2 | ||
|
|
eac35ef8f4 | ||
|
|
e9eee50b3e | ||
|
|
f7c797e0b0 | ||
|
|
f9d66e0a78 | ||
|
|
a9d1d6edf5 | ||
|
|
babb2c0856 | ||
|
|
0570f2cd5b | ||
|
|
9c72b7ae8f | ||
|
|
e4b0b153e5 | ||
|
|
3edd8d5ebe | ||
|
|
bf68a284bb | ||
|
|
978544ed3f | ||
|
|
98135bc5fd | ||
|
|
626924bc34 | ||
|
|
a0fd2b6d16 | ||
|
|
44c431e9e0 | ||
|
|
6852f892f0 | ||
|
|
ec1eaf00eb | ||
|
|
ae0aa7dc65 | ||
|
|
29f78c7429 | ||
|
|
cd8834e368 | ||
|
|
f3fc967710 | ||
|
|
7d93c64b5b | ||
|
|
d4b957db23 | ||
|
|
d9678855a0 | ||
|
|
65c01e3f6e | ||
|
|
6821cee443 | ||
|
|
15a12da321 | ||
|
|
009d064576 | ||
|
|
3a4e2d9f9b | ||
|
|
e7afa235f7 | ||
|
|
edc5bd5d21 | ||
|
|
c06b09e00c | ||
|
|
3eeba27191 | ||
|
|
c6d1a040a1 | ||
|
|
558a3fc865 | ||
|
|
e0b50ac80c | ||
|
|
07ef7ae18f | ||
|
|
d07bd33e06 | ||
|
|
1616ca1c34 | ||
|
|
52fe74fbea | ||
|
|
8857ccbf86 | ||
|
|
279a290b60 | ||
|
|
128a3fe2f2 | ||
|
|
c97acfc76c | ||
|
|
bc2dae1b21 | ||
|
|
983b8ebe38 | ||
|
|
b6ba4640de | ||
|
|
5214aaafe7 | ||
|
|
39713f95ea | ||
|
|
b88f56e185 | ||
|
|
248444c04d | ||
|
|
dbd27e6113 | ||
|
|
3f09a79645 | ||
|
|
98b3746b25 | ||
|
|
07f8c38820 | ||
|
|
8393c07601 | ||
|
|
22d7595357 | ||
|
|
2157d95c56 | ||
|
|
729b2a1f0d | ||
|
|
cbcd5fbd2c | ||
|
|
a3318b1253 | ||
|
|
e59c7e1124 | ||
|
|
8dc108030d | ||
|
|
6e4feecff0 | ||
|
|
77c085ef1a | ||
|
|
acc202031c |
39
.editorconfig
Normal file
@@ -0,0 +1,39 @@
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
# Matches multiple files with brace expansion notation
|
||||
# Set default charset
|
||||
[*.{js,py}]
|
||||
charset = utf-8# 4 space indentation
|
||||
|
||||
# Swift files
|
||||
[*.swift]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
charset = utf-8# 4 space indentation
|
||||
|
||||
# 4 space indentation
|
||||
[*.py]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
# Tab indentation (no size specified)
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
||||
# Indentation override for all JS under lib directory
|
||||
[lib/**.js]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# Matches the exact files either package.json or .travis.yml
|
||||
[{package.json,.travis.yml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
1007
.github/.obsolete/reusable-build-workflow.yml
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* @JoeMatt @lonkelle @nythepegasus @Spidy123222 @SternXD
|
||||
40
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: Bug Report
|
||||
description: Report a bug
|
||||
title: "[BUG] "
|
||||
labels: ["bug"]
|
||||
assignees: []
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Please note that the issue tracker is not for support
|
||||
Thanks for taking the time to fill out this bug report! Before you continue filling out the report, please **[search in GitHub Issues](https://github.com/SideStore/SideStore/issues?q=is%3Aissue+is%3Aopen) for the bug you are experiencing** in case it has already been reported.
|
||||
|
||||
**Please use [Discord](https://discord.gg/sidestore-949183273383395328) or [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) for support.**
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: What is the bug and how did you discover it?
|
||||
placeholder: Please be clear and concise with your description.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: how-to-reproduce
|
||||
attributes:
|
||||
label: Instructions to reproduce
|
||||
description: Please include clear and consistent instructions for reproducing the bug to make it easier for us to fix it.
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: app-version
|
||||
attributes:
|
||||
label: What version of SideStore are you using?
|
||||
description: To retrieve this, go to `Settings` in the SideStore app and scroll down to the bottom.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: other-info
|
||||
attributes:
|
||||
label: Other info
|
||||
description: If you have any other comments, other info that might be useful, or if you found a workaround, please put it here.
|
||||
10
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# force issue template usage
|
||||
blank_issues_enabled: false
|
||||
|
||||
contact_links:
|
||||
- name: Discord
|
||||
url: https://discord.gg/sidestore-949183273383395328
|
||||
about: If you need support, please go here first instead of making an issue!
|
||||
- name: GitHub Discussions
|
||||
url: https://github.com/SideStore/SideStore/discussions
|
||||
about: As an alternative to Discord, you can also make a new GitHub discussion.
|
||||
32
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: Feature Request
|
||||
description: Suggest a feature
|
||||
title: "[FEATURE REQUEST] "
|
||||
labels: ["enhancement"]
|
||||
assignees: []
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this feature request! Before you continue filling out the form, please **[search in GitHub Issues](https://github.com/SideStore/SideStore/issues?q=is%3Aissue+is%3Aopen) for the feature you are suggestion** in case it has already been suggested.
|
||||
|
||||
**Please use [Discord](https://discord.gg/sidestore-949183273383395328) or [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) for support.**
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe the feature
|
||||
description: What is the feature? How would it work?
|
||||
placeholder: Please be clear and concise with your description.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: use-cases
|
||||
attributes:
|
||||
label: Use cases
|
||||
description: Please include multiple use cases where this feature would be useful.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Alternatives
|
||||
description: If you have alternative ideas of how this feature could work, you can put them here.
|
||||
63
.github/maintenance/cache.py
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
import requests
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Your GitHub Personal Access Token
|
||||
GITHUB_TOKEN = os.getenv("GITHUB_TOKEN")
|
||||
|
||||
# Repository details
|
||||
REPO_OWNER = "SideStore"
|
||||
REPO_NAME = "SideStore"
|
||||
|
||||
|
||||
API_URL = f"https://api.github.com/repos/{REPO_OWNER}/{REPO_NAME}/actions/caches"
|
||||
|
||||
# Common headers for GitHub API calls
|
||||
HEADERS = {
|
||||
"Accept": "application/vnd.github+json",
|
||||
"Authorization": f"Bearer {GITHUB_TOKEN}"
|
||||
}
|
||||
|
||||
def list_caches():
|
||||
response = requests.get(API_URL, headers=HEADERS)
|
||||
if response.status_code != 200:
|
||||
print(f"Failed to list caches. HTTP {response.status_code}")
|
||||
print("Response:", response.text)
|
||||
sys.exit(1)
|
||||
data = response.json()
|
||||
return data.get("actions_caches", [])
|
||||
|
||||
def delete_cache(cache_id):
|
||||
delete_url = f"{API_URL}/{cache_id}"
|
||||
response = requests.delete(delete_url, headers=HEADERS)
|
||||
return response.status_code
|
||||
|
||||
def main():
|
||||
caches = list_caches()
|
||||
if not caches:
|
||||
print("No caches found.")
|
||||
return
|
||||
|
||||
print("Found caches:")
|
||||
for cache in caches:
|
||||
print(f"ID: {cache.get('id')}, Key: {cache.get('key')}")
|
||||
|
||||
print("\nDeleting caches...")
|
||||
for cache in caches:
|
||||
cache_id = cache.get("id")
|
||||
status = delete_cache(cache_id)
|
||||
if status == 204:
|
||||
print(f"Successfully deleted cache with ID: {cache_id}")
|
||||
else:
|
||||
print(f"Failed to delete cache with ID: {cache_id}. HTTP status code: {status}")
|
||||
|
||||
print("All caches processed.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
### How to use
|
||||
'''
|
||||
just export the GITHUB_TOKEN and then run this script via `python3 cache.py' to delete the caches
|
||||
'''
|
||||
12
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
### Changes
|
||||
|
||||
<!-- Fill this list with what your PR changes. Example: -->
|
||||
- Fix bug
|
||||
- Change UI for QOL
|
||||
|
||||
<!-- If your PR is ready to be merged, you can remove this section. -->
|
||||
### Todo before merge
|
||||
|
||||
<!-- Example: -->
|
||||
- [x] Finish UI changes
|
||||
- [ ] Test
|
||||
28
.github/workflows/alpha.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: Alpha SideStore build
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop-alpha
|
||||
|
||||
# cancel duplicate run if from same branch
|
||||
concurrency:
|
||||
group: ${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
Reusable-build:
|
||||
uses: ./.github/workflows/reusable-sidestore-build.yml
|
||||
with:
|
||||
# bundle_id: "com.SideStore.SideStore.Alpha"
|
||||
bundle_id: "com.SideStore.SideStore"
|
||||
# bundle_id_suffix: ".Alpha"
|
||||
is_beta: true
|
||||
publish: ${{ vars.PUBLISH_ALPHA_UPDATES == 'true' }}
|
||||
is_shared_build_num: false
|
||||
release_tag: "alpha"
|
||||
release_name: "Alpha"
|
||||
upstream_tag: "nightly"
|
||||
upstream_name: "Nightly"
|
||||
secrets:
|
||||
CROSS_REPO_PUSH_KEY: ${{ secrets.CROSS_REPO_PUSH_KEY }}
|
||||
BUILD_LOG_ZIP_PASSWORD: ${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
|
||||
77
.github/workflows/attach_build_products.yml
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
name: Add artifact links to pull request and related issues
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [Pull Request SideStore build]
|
||||
types: [completed]
|
||||
|
||||
jobs:
|
||||
artifacts-url-comments:
|
||||
name: add artifact links to pull request and related issues job
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
steps:
|
||||
- name: add artifact links to pull request and related issues step
|
||||
uses: tonyhallett/artifacts-url-comments@v1.1.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
prefix: Builds for this Pull Request are available at
|
||||
suffix: Have a nice day.
|
||||
format: name
|
||||
addTo: pull
|
||||
# addTo: pullandissues
|
||||
nightly-link-comment:
|
||||
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
with:
|
||||
# This snippet is public-domain, taken from
|
||||
# https://github.com/oprypin/nightly.link/blob/master/.github/workflows/pr-comment.yml
|
||||
script: |
|
||||
async function upsertComment(owner, repo, issue_number, purpose, body) {
|
||||
const {data: comments} = await github.rest.issues.listComments(
|
||||
{owner, repo, issue_number});
|
||||
|
||||
const marker = `<!-- bot: ${purpose} -->`;
|
||||
body = marker + "\n" + body;
|
||||
|
||||
const existing = comments.filter((c) => c.body.includes(marker));
|
||||
if (existing.length > 0) {
|
||||
const last = existing[existing.length - 1];
|
||||
core.info(`Updating comment ${last.id}`);
|
||||
await github.rest.issues.updateComment({
|
||||
owner, repo,
|
||||
body,
|
||||
comment_id: last.id,
|
||||
});
|
||||
} else {
|
||||
core.info(`Creating a comment in issue / PR #${issue_number}`);
|
||||
await github.rest.issues.createComment({issue_number, body, owner, repo});
|
||||
}
|
||||
}
|
||||
|
||||
const {owner, repo} = context.repo;
|
||||
const run_id = ${{github.event.workflow_run.id}};
|
||||
|
||||
const pull_requests = ${{ toJSON(github.event.workflow_run.pull_requests) }};
|
||||
if (!pull_requests.length) {
|
||||
return core.error("This workflow doesn't match any pull requests!");
|
||||
}
|
||||
|
||||
const artifacts = await github.paginate(
|
||||
github.rest.actions.listWorkflowRunArtifacts, {owner, repo, run_id});
|
||||
if (!artifacts.length) {
|
||||
return core.error(`No artifacts found`);
|
||||
}
|
||||
let body = `Download the artifacts for this pull request (nightly.link):\n`;
|
||||
for (const art of artifacts) {
|
||||
body += `\n* [${art.name}.zip](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`;
|
||||
}
|
||||
|
||||
core.info("Review thread message body:", body);
|
||||
|
||||
for (const pr of pull_requests) {
|
||||
await upsertComment(owner, repo, pr.number,
|
||||
"nightly-link", body);
|
||||
}
|
||||
103
.github/workflows/beta.yml
vendored
Normal file
@@ -0,0 +1,103 @@
|
||||
name: Beta SideStore build
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '[0-9]+.[0-9]+.[0-9]+-beta.[0-9]+' # example: 1.0.0-beta.1
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and upload SideStore Beta
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: 'macos-14'
|
||||
version: '15.4'
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install dependencies
|
||||
run: brew install ldid
|
||||
|
||||
- name: Change version to tag
|
||||
run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Echo version
|
||||
run: echo "${{ steps.version.outputs.version }}"
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: ${{ matrix.version }}
|
||||
|
||||
|
||||
- name: Cache Build
|
||||
uses: irgaly/xcode-cache@v1
|
||||
with:
|
||||
key: xcode-cache-deriveddata-${{ github.sha }}
|
||||
restore-keys: xcode-cache-deriveddata
|
||||
|
||||
- name: Build SideStore
|
||||
run: make build | xcpretty && exit ${PIPESTATUS[0]}
|
||||
|
||||
- name: Fakesign app
|
||||
run: make fakesign
|
||||
|
||||
- name: Convert to IPA
|
||||
run: make ipa
|
||||
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get current date in AltStore date form
|
||||
id: date_altstore
|
||||
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload to new beta release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
name: ${{ steps.version.outputs.version }}
|
||||
tag_name: ${{ github.ref_name }}
|
||||
draft: true
|
||||
prerelease: true
|
||||
files: SideStore.ipa
|
||||
body: |
|
||||
<!-- NOTE: to reset SideSource cache, go to `https://apps.sidestore.io/reset-cache/nightly/<sidesource key>`. This is not included in the GitHub Action since it makes draft releases so they can be edited and have a changelog. -->
|
||||
Beta builds are hand-picked builds from development commits that will allow you to try out new features earlier than normal. However, **they might contain bugs and other issues. Use at your own risk!**
|
||||
|
||||
## Changelog
|
||||
|
||||
- TODO
|
||||
|
||||
## Build Info
|
||||
|
||||
Built at (UTC): `${{ steps.date.outputs.date }}`
|
||||
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
|
||||
Commit SHA: `${{ github.sha }}`
|
||||
Version: `${{ steps.version.outputs.version }}`
|
||||
|
||||
- name: Add version to IPA file name
|
||||
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Upload SideStore.ipa Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
path: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Upload *.dSYM Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
||||
path: ./*.dSYM/
|
||||
34
.github/workflows/increase-beta-build-num.sh
vendored
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Ensure we are in root directory
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
DATE=`date -u +'%Y.%m.%d'`
|
||||
BUILD_NUM=1
|
||||
|
||||
# Use RELEASE_CHANNEL from the environment variable or default to "beta"
|
||||
RELEASE_CHANNEL=${RELEASE_CHANNEL:-"beta"}
|
||||
|
||||
write() {
|
||||
sed -e "/MARKETING_VERSION = .*/s/$/-$RELEASE_CHANNEL.$DATE.$BUILD_NUM+$(git rev-parse --short HEAD)/" -i '' Build.xcconfig
|
||||
echo "$DATE,$BUILD_NUM" > build_number.txt
|
||||
}
|
||||
|
||||
if [ ! -f "build_number.txt" ]; then
|
||||
write
|
||||
exit 0
|
||||
fi
|
||||
|
||||
LAST_DATE=`cat build_number.txt | perl -n -e '/([^,]*),([^ ]*)$/ && print $1'`
|
||||
LAST_BUILD_NUM=`cat build_number.txt | perl -n -e '/([^,]*),([^ ]*)$/ && print $2'`
|
||||
|
||||
# if [[ "$DATE" != "$LAST_DATE" ]]; then
|
||||
# write
|
||||
# else
|
||||
# BUILD_NUM=`expr $LAST_BUILD_NUM + 1`
|
||||
# write
|
||||
# fi
|
||||
|
||||
# Build number is always incremental
|
||||
BUILD_NUM=`expr $LAST_BUILD_NUM + 1`
|
||||
write
|
||||
82
.github/workflows/nightly.yml
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
name: Nightly SideStore Build
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
schedule:
|
||||
- cron: '0 0 * * *' # Runs every night at midnight UTC
|
||||
workflow_dispatch: # Allows manual trigger
|
||||
|
||||
# cancel duplicate run if from same branch
|
||||
concurrency:
|
||||
group: ${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check-changes:
|
||||
if: github.event_name == 'schedule'
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
has_changes: ${{ steps.check.outputs.has_changes }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Ensure full history
|
||||
|
||||
- name: Get last successful workflow run
|
||||
id: get_last_success
|
||||
run: |
|
||||
LAST_SUCCESS=$(gh run list --workflow "Nightly SideStore Build" --json createdAt,conclusion \
|
||||
--jq '[.[] | select(.conclusion=="success")][0].createdAt' || echo "")
|
||||
echo "Last successful run: $LAST_SUCCESS"
|
||||
echo "last_success=$LAST_SUCCESS" >> $GITHUB_ENV
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Check for new commits since last successful build
|
||||
id: check
|
||||
run: |
|
||||
if [ -n "$LAST_SUCCESS" ]; then
|
||||
NEW_COMMITS=$(git rev-list --count --since="$LAST_SUCCESS" origin/develop)
|
||||
COMMIT_LOG=$(git log --since="$LAST_SUCCESS" --pretty=format:"%h %s" origin/develop)
|
||||
else
|
||||
NEW_COMMITS=1
|
||||
COMMIT_LOG=$(git log -n 10 --pretty=format:"%h %s" origin/develop) # Show last 10 commits if no history
|
||||
fi
|
||||
|
||||
echo "Has changes: $NEW_COMMITS"
|
||||
echo "New commits since last successful build:"
|
||||
echo "$COMMIT_LOG"
|
||||
|
||||
if [ "$NEW_COMMITS" -gt 0 ]; then
|
||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
LAST_SUCCESS: ${{ env.last_success }}
|
||||
|
||||
Reusable-build:
|
||||
if: |
|
||||
always() &&
|
||||
(github.event_name == 'push' ||
|
||||
(github.event_name == 'schedule' && needs.check-changes.result == 'success' && needs.check-changes.outputs.has_changes == 'true'))
|
||||
needs: check-changes
|
||||
uses: ./.github/workflows/reusable-sidestore-build.yml
|
||||
with:
|
||||
# bundle_id: "com.SideStore.SideStore.Nightly"
|
||||
bundle_id: "com.SideStore.SideStore"
|
||||
# bundle_id_suffix: ".Nightly"
|
||||
is_beta: true
|
||||
publish: ${{ vars.PUBLISH_NIGHTLY_UPDATES == 'true' }}
|
||||
is_shared_build_num: false
|
||||
release_tag: "nightly"
|
||||
release_name: "Nightly"
|
||||
upstream_tag: "0.5.10"
|
||||
upstream_name: "Stable"
|
||||
secrets:
|
||||
CROSS_REPO_PUSH_KEY: ${{ secrets.CROSS_REPO_PUSH_KEY }}
|
||||
BUILD_LOG_ZIP_PASSWORD: ${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
|
||||
|
||||
143
.github/workflows/pr.yml
vendored
Normal file
@@ -0,0 +1,143 @@
|
||||
name: Pull Request SideStore build
|
||||
on:
|
||||
pull_request:
|
||||
# types: [opened, synchronize, reopened, ready_for_review, converted_to_draft]
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and upload SideStore
|
||||
if: ${{ github.event.pull_request.draft == false }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: 'macos-14'
|
||||
version: '16.1'
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install dependencies
|
||||
run: brew install ldid
|
||||
|
||||
- name: Install xcbeautify
|
||||
run: brew install xcbeautify
|
||||
|
||||
- name: Add PR suffix to version
|
||||
run: sed -e "/MARKETING_VERSION = .*/s/\$/-pr.${{ github.event.pull_request.number }}+$(git rev-parse --short ${COMMIT:-HEAD})/" -i '' Build.xcconfig
|
||||
env:
|
||||
COMMIT: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Echo version
|
||||
run: echo "${{ steps.version.outputs.version }}"
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||
with:
|
||||
xcode-version: ${{ matrix.version }}
|
||||
|
||||
- name: Cache Build
|
||||
uses: irgaly/xcode-cache@v1
|
||||
with:
|
||||
key: xcode-cache-deriveddata-${{ github.sha }}
|
||||
restore-keys: xcode-cache-deriveddata-
|
||||
swiftpm-cache-key: xcode-cache-sourcedata-${{ github.sha }}
|
||||
swiftpm-cache-restore-keys: |
|
||||
xcode-cache-sourcedata-
|
||||
|
||||
|
||||
- name: Restore Pods from Cache (Exact match)
|
||||
id: pods-restore
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-${{ hashFiles('Podfile') }}
|
||||
# restore-keys: | # commented out to strictly check cache for this particular podfile
|
||||
# pods-cache-
|
||||
|
||||
- name: Restore Pods from Cache (Last Available)
|
||||
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
||||
id: pods-restore-recent
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-
|
||||
|
||||
- name: Install CocoaPods
|
||||
# if: ${{ steps.pods-restore.outputs.cache-hit != 'true'}}
|
||||
id: pods-install
|
||||
run: |
|
||||
pod install
|
||||
|
||||
- name: Save Pods to Cache
|
||||
id: save-pods
|
||||
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
||||
uses: actions/cache/save@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-${{ hashFiles('Podfile') }}
|
||||
|
||||
- name: List Files and derived data
|
||||
run: |
|
||||
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||
ls -la .
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Pods <<<<<<<<<<"
|
||||
find Pods -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> SideStore <<<<<<<<<<"
|
||||
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Dependencies <<<<<<<<<<"
|
||||
find Dependencies -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
|
||||
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
|
||||
- name: Build SideStore
|
||||
run: NSUnbufferedIO=YES make build 2>&1 | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
|
||||
|
||||
- name: Fakesign app
|
||||
run: make fakesign
|
||||
|
||||
- name: Convert to IPA
|
||||
run: make ipa
|
||||
|
||||
- name: Add version to IPA file name
|
||||
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Upload SideStore.ipa Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
path: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Upload *.dSYM Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
||||
path: ./SideStore.xcarchive/dSYMs/*
|
||||
104
.github/workflows/reusable-sidestore-build.yml
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
name: Reusable SideStore Build
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
is_beta:
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
publish:
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
is_shared_build_num:
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
release_name:
|
||||
required: true
|
||||
type: string
|
||||
release_tag:
|
||||
required: true
|
||||
type: string
|
||||
upstream_tag:
|
||||
required: true
|
||||
type: string
|
||||
upstream_name:
|
||||
required: true
|
||||
type: string
|
||||
bundle_id:
|
||||
default: com.SideStore.SideStore
|
||||
required: true
|
||||
type: string
|
||||
bundle_id_suffix:
|
||||
default: ''
|
||||
required: false
|
||||
type: string
|
||||
|
||||
secrets:
|
||||
# GITHUB_TOKEN:
|
||||
# required: true
|
||||
CROSS_REPO_PUSH_KEY:
|
||||
required: true
|
||||
BUILD_LOG_ZIP_PASSWORD:
|
||||
required: false
|
||||
|
||||
|
||||
# since build cache, test-build cache, test-run cache are involved, out of order exec if serialization is on individual jobs will wreak all sorts of havoc
|
||||
# so we serialize on the entire workflow
|
||||
concurrency:
|
||||
group: serialize-workflow
|
||||
|
||||
jobs:
|
||||
shared:
|
||||
uses: ./.github/workflows/sidestore-shared.yml
|
||||
secrets: inherit
|
||||
|
||||
build:
|
||||
needs: shared
|
||||
uses: ./.github/workflows/sidestore-build.yml
|
||||
with:
|
||||
is_beta: ${{ inputs.is_beta }}
|
||||
is_shared_build_num: ${{ inputs.is_shared_build_num }}
|
||||
release_tag: ${{ inputs.release_tag }}
|
||||
short_commit: ${{ needs.shared.outputs.short-commit }}
|
||||
bundle_id: ${{ inputs.bundle_id }}
|
||||
bundle_id_suffix: ${{ inputs.bundle_id_suffix }}
|
||||
secrets: inherit
|
||||
|
||||
tests-build:
|
||||
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_BUILD == '1' }}
|
||||
needs: shared
|
||||
uses: ./.github/workflows/sidestore-tests-build.yml
|
||||
with:
|
||||
release_tag: ${{ inputs.release_tag }}
|
||||
short_commit: ${{ needs.shared.outputs.short-commit }}
|
||||
secrets: inherit
|
||||
|
||||
tests-run:
|
||||
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
|
||||
needs: [shared, tests-build]
|
||||
uses: ./.github/workflows/sidestore-tests-run.yml
|
||||
with:
|
||||
release_tag: ${{ inputs.release_tag }}
|
||||
short_commit: ${{ needs.shared.outputs.short-commit }}
|
||||
secrets: inherit
|
||||
|
||||
deploy:
|
||||
needs: [shared, build, tests-build, tests-run] # Keep tests-run in needs
|
||||
if: ${{ always() && (needs.tests-run.result == 'skipped' || needs.tests-run.result == 'success') }}
|
||||
uses: ./.github/workflows/sidestore-deploy.yml
|
||||
with:
|
||||
is_beta: ${{ inputs.is_beta }}
|
||||
publish: ${{ inputs.publish }}
|
||||
release_name: ${{ inputs.release_name }}
|
||||
release_tag: ${{ inputs.release_tag }}
|
||||
upstream_tag: ${{ inputs.upstream_tag }}
|
||||
upstream_name: ${{ inputs.upstream_name }}
|
||||
version: ${{ needs.build.outputs.version }}
|
||||
short_commit: ${{ needs.shared.outputs.short-commit }}
|
||||
release_channel: ${{ needs.build.outputs.release-channel }}
|
||||
marketing_version: ${{ needs.build.outputs.marketing-version }}
|
||||
bundle_id: ${{ inputs.bundle_id }}
|
||||
secrets: inherit
|
||||
401
.github/workflows/sidestore-build.yml
vendored
Normal file
@@ -0,0 +1,401 @@
|
||||
name: SideStore Build
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
is_beta:
|
||||
type: boolean
|
||||
is_shared_build_num:
|
||||
type: boolean
|
||||
release_tag:
|
||||
type: string
|
||||
bundle_id:
|
||||
type: string
|
||||
bundle_id_suffix:
|
||||
type: string
|
||||
short_commit:
|
||||
type: string
|
||||
secrets:
|
||||
CROSS_REPO_PUSH_KEY:
|
||||
required: true
|
||||
BUILD_LOG_ZIP_PASSWORD:
|
||||
required: false
|
||||
outputs:
|
||||
version:
|
||||
value: ${{ jobs.build.outputs.version }}
|
||||
marketing-version:
|
||||
value: ${{ jobs.build.outputs.marketing-version }}
|
||||
release-channel:
|
||||
value: ${{ jobs.build.outputs.release-channel }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build SideStore - ${{ inputs.release_tag }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: 'macos-15'
|
||||
version: '16.2'
|
||||
runs-on: ${{ matrix.os }}
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
marketing-version: ${{ steps.marketing-version.outputs.MARKETING_VERSION }}
|
||||
release-channel: ${{ steps.release-channel.outputs.RELEASE_CHANNEL }}
|
||||
|
||||
steps:
|
||||
- name: Set beta status
|
||||
run: echo "IS_BETA=${{ inputs.is_beta }}" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install dependencies - ldid & xcbeautify
|
||||
run: |
|
||||
brew install ldid xcbeautify
|
||||
|
||||
- name: Set ref based on is_shared_build_num
|
||||
if: ${{ inputs.is_beta }}
|
||||
id: set_ref
|
||||
run: |
|
||||
if [ "${{ inputs.is_shared_build_num }}" == "true" ]; then
|
||||
echo "ref=main" >> $GITHUB_ENV
|
||||
else
|
||||
echo "ref=${{ inputs.release_tag }}" >> $GITHUB_ENV
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Checkout SideStore/beta-build-num repo
|
||||
if: ${{ inputs.is_beta }}
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: 'SideStore/beta-build-num'
|
||||
ref: ${{ env.ref }}
|
||||
token: ${{ secrets.CROSS_REPO_PUSH_KEY }}
|
||||
path: 'SideStore/beta-build-num'
|
||||
|
||||
- name: Copy build_number.txt to repo root
|
||||
if: ${{ inputs.is_beta }}
|
||||
run: |
|
||||
cp SideStore/beta-build-num/build_number.txt .
|
||||
echo "cat build_number.txt"
|
||||
cat build_number.txt
|
||||
shell: bash
|
||||
|
||||
- name: Echo Build.xcconfig
|
||||
run: |
|
||||
echo "cat Build.xcconfig"
|
||||
cat Build.xcconfig
|
||||
shell: bash
|
||||
|
||||
- name: Set Release Channel info for build number bumper
|
||||
id: release-channel
|
||||
run: |
|
||||
RELEASE_CHANNEL="${{ inputs.release_tag }}"
|
||||
echo "RELEASE_CHANNEL=${RELEASE_CHANNEL}" >> $GITHUB_ENV
|
||||
echo "RELEASE_CHANNEL=${RELEASE_CHANNEL}" >> $GITHUB_OUTPUT
|
||||
echo "RELEASE_CHANNEL=${RELEASE_CHANNEL}"
|
||||
shell: bash
|
||||
|
||||
- name: Increase build number for beta builds
|
||||
if: ${{ inputs.is_beta }}
|
||||
run: |
|
||||
bash .github/workflows/increase-beta-build-num.sh
|
||||
shell: bash
|
||||
|
||||
- name: Extract MARKETING_VERSION from Build.xcconfig
|
||||
id: version
|
||||
run: |
|
||||
version=$(grep MARKETING_VERSION Build.xcconfig | sed -e 's/MARKETING_VERSION = //g')
|
||||
echo "version=$version" >> $GITHUB_OUTPUT
|
||||
echo "version=$version"
|
||||
shell: bash
|
||||
|
||||
- name: Set MARKETING_VERSION
|
||||
if: ${{ inputs.is_beta }}
|
||||
id: marketing-version
|
||||
run: |
|
||||
# Extract version number (e.g., "0.6.0")
|
||||
version=$(echo "${{ steps.version.outputs.version }}" | sed -E 's/^[^0-9]*([0-9]+\.[0-9]+\.[0-9]+).*/\1/')
|
||||
# Extract date (YYYYMMDD) (e.g., "20250205")
|
||||
date=$(echo "${{ steps.version.outputs.version }}" | sed -E 's/.*\.([0-9]{4})\.([0-9]{2})\.([0-9]{2})\..*/\1\2\3/')
|
||||
# Extract build number (e.g., "2")
|
||||
build_num=$(echo "${{ steps.version.outputs.version }}" | sed -E 's/.*\.([0-9]+)\+.*/\1/')
|
||||
|
||||
# Combine them into the final output
|
||||
MARKETING_VERSION="${version}-${date}.${build_num}+${{ inputs.short_commit }}"
|
||||
|
||||
echo "MARKETING_VERSION=$MARKETING_VERSION" >> $GITHUB_ENV
|
||||
echo "MARKETING_VERSION=$MARKETING_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "MARKETING_VERSION=$MARKETING_VERSION"
|
||||
shell: bash
|
||||
|
||||
- name: Echo Updated Build.xcconfig, build_number.txt
|
||||
if: ${{ inputs.is_beta }}
|
||||
run: |
|
||||
cat Build.xcconfig
|
||||
cat build_number.txt
|
||||
shell: bash
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||
with:
|
||||
xcode-version: ${{ matrix.version }}
|
||||
|
||||
- name: (Build) Restore Xcode & SwiftPM Cache (Exact match)
|
||||
id: xcode-cache-restore
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
~/Library/Developer/Xcode/DerivedData
|
||||
~/Library/Caches/org.swift.swiftpm
|
||||
key: xcode-cache-build-${{ github.ref_name }}-${{ github.sha }}
|
||||
|
||||
- name: (Build) Restore Xcode & SwiftPM Cache (Last Available)
|
||||
id: xcode-cache-restore-recent
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
~/Library/Developer/Xcode/DerivedData
|
||||
~/Library/Caches/org.swift.swiftpm
|
||||
key: xcode-cache-build-${{ github.ref_name }}-
|
||||
|
||||
# - name: (Build) Cache Build
|
||||
# uses: irgaly/xcode-cache@v1.8.1
|
||||
# with:
|
||||
# key: xcode-cache-deriveddata-build-${{ github.ref_name }}-${{ github.sha }}
|
||||
# restore-keys: xcode-cache-deriveddata-build-${{ github.ref_name }}-
|
||||
# swiftpm-cache-key: xcode-cache-sourcedata-build-${{ github.ref_name }}-${{ github.sha }}
|
||||
# swiftpm-cache-restore-keys: |
|
||||
# xcode-cache-sourcedata-build-${{ github.ref_name }}-
|
||||
|
||||
- name: (Build) Restore Pods from Cache (Exact match)
|
||||
id: pods-restore
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-build-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
|
||||
# restore-keys: | # commented out to strictly check cache for this particular podfile
|
||||
# pods-cache-
|
||||
|
||||
- name: (Build) Restore Pods from Cache (Last Available)
|
||||
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
||||
id: pods-restore-recent
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-build-${{ github.ref_name }}-
|
||||
|
||||
|
||||
- name: (Build) Install CocoaPods
|
||||
run: pod install
|
||||
shell: bash
|
||||
|
||||
- name: (Build) Save Pods to Cache
|
||||
id: save-pods
|
||||
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
||||
uses: actions/cache/save@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-build-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
|
||||
|
||||
- name: (Build) Clean previous build artifacts
|
||||
# using 'tee' to intercept stdout and log for detailed build-log
|
||||
run: |
|
||||
make clean
|
||||
mkdir -p build/logs
|
||||
shell: bash
|
||||
|
||||
- name: (Build) List Files and derived data
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||
ls -la .
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Pods <<<<<<<<<<"
|
||||
find Pods -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> SideStore <<<<<<<<<<"
|
||||
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Dependencies <<<<<<<<<<"
|
||||
find Dependencies -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
|
||||
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
- name: Set BundleID Suffix for Sidestore build
|
||||
run: |
|
||||
echo "BUNDLE_ID_SUFFIX=${{ inputs.bundle_id_suffix }}" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
|
||||
- name: Build SideStore.xcarchive
|
||||
# using 'tee' to intercept stdout and log for detailed build-log
|
||||
run: |
|
||||
NSUnbufferedIO=YES make -B build 2>&1 | tee -a build/logs/build.log | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
|
||||
shell: bash
|
||||
|
||||
- name: Fakesign app
|
||||
run: make fakesign | tee -a build/logs/build.log
|
||||
shell: bash
|
||||
|
||||
- name: Convert to IPA
|
||||
run: make ipa | tee -a build/logs/build.log
|
||||
shell: bash
|
||||
|
||||
- name: (Build) Save Xcode & SwiftPM Cache
|
||||
id: cache-save
|
||||
if: ${{ steps.xcode-cache-restore.outputs.cache-hit != 'true' }}
|
||||
uses: actions/cache/save@v3
|
||||
with:
|
||||
path: |
|
||||
~/Library/Developer/Xcode/DerivedData
|
||||
~/Library/Caches/org.swift.swiftpm
|
||||
key: xcode-cache-build-${{ github.ref_name }}-${{ github.sha }}
|
||||
|
||||
- name: (Build) List Files and Build artifacts
|
||||
run: |
|
||||
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||
ls -la .
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Build <<<<<<<<<<"
|
||||
find build -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> SideStore <<<<<<<<<<"
|
||||
find SideStore -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> SideStore.xcarchive <<<<<<<<<<"
|
||||
find SideStore.xcarchive -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
|
||||
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
|
||||
echo ""
|
||||
shell: bash
|
||||
|
||||
- name: Encrypt build-logs for upload
|
||||
id: encrypt-build-log
|
||||
run: |
|
||||
DEFAULT_BUILD_LOG_PASSWORD=12345
|
||||
|
||||
BUILD_LOG_ZIP_PASSWORD=${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
|
||||
BUILD_LOG_ZIP_PASSWORD=${BUILD_LOG_ZIP_PASSWORD:-$DEFAULT_BUILD_LOG_PASSWORD}
|
||||
|
||||
if [ "$BUILD_LOG_ZIP_PASSWORD" == "$DEFAULT_BUILD_LOG_PASSWORD" ]; then
|
||||
echo "Warning: BUILD_LOG_ZIP_PASSWORD is not set. Defaulting to '${DEFAULT_BUILD_LOG_PASSWORD}'."
|
||||
fi
|
||||
|
||||
pushd build/logs && zip -e -P "$BUILD_LOG_ZIP_PASSWORD" ../../encrypted-build-logs.zip * || popd
|
||||
echo "::set-output name=encrypted::true"
|
||||
shell: bash
|
||||
|
||||
- name: Upload encrypted-build-logs.zip
|
||||
id: attach-encrypted-build-log
|
||||
if: ${{ always() && steps.encrypt-build-log.outputs.encrypted == 'true' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: encrypted-build-logs-${{ steps.version.outputs.version }}.zip
|
||||
path: encrypted-build-logs.zip
|
||||
|
||||
- name: Upload SideStore.ipa Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
path: SideStore.ipa
|
||||
|
||||
- name: Zip dSYMs
|
||||
run: zip -r -9 ./SideStore.dSYMs.zip ./SideStore.xcarchive/dSYMs
|
||||
shell: bash
|
||||
|
||||
- name: Upload *.dSYM Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}-dSYMs.zip
|
||||
path: SideStore.dSYMs.zip
|
||||
|
||||
- name: Keep rolling the build numbers for each successful build
|
||||
if: ${{ inputs.is_beta }}
|
||||
run: |
|
||||
pushd SideStore/beta-build-num/
|
||||
|
||||
echo "Configure Git user (committer details)"
|
||||
git config user.name "GitHub Actions"
|
||||
git config user.email "github-actions@github.com"
|
||||
|
||||
echo "Adding files to commit"
|
||||
git add --verbose build_number.txt
|
||||
git commit -m " - updated for ${{ inputs.release_tag }} - ${{ inputs.short_commit }} deployment" || echo "No changes to commit"
|
||||
|
||||
echo "Pushing to remote repo"
|
||||
git push --verbose
|
||||
popd
|
||||
shell: bash
|
||||
|
||||
- name: Get last successful commit
|
||||
id: get_last_commit
|
||||
run: |
|
||||
# Try to get the last successful workflow run commit
|
||||
LAST_SUCCESS_SHA=$(gh run list --branch "${{ github.ref_name }}" --status success --json headSha --jq '.[0].headSha')
|
||||
echo "LAST_SUCCESS_SHA=$LAST_SUCCESS_SHA" >> $GITHUB_OUTPUT
|
||||
echo "LAST_SUCCESS_SHA=$LAST_SUCCESS_SHA" >> $GITHUB_ENV
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
shell: bash
|
||||
|
||||
- name: Create release notes
|
||||
run: |
|
||||
LAST_SUCCESS_SHA=${{ steps.get_last_commit.outputs.LAST_SUCCESS_SHA}}
|
||||
echo "Last successful commit SHA: $LAST_SUCCESS_SHA"
|
||||
|
||||
FROM_COMMIT=$LAST_SUCCESS_SHA
|
||||
# Check if we got a valid SHA
|
||||
if [ -z "$LAST_SUCCESS_SHA" ] || [ "$LAST_SUCCESS_SHA" = "null" ]; then
|
||||
echo "No successful run found, using initial commit of branch"
|
||||
# Get the first commit of the branch (initial commit)
|
||||
FROM_COMMIT=$(git rev-list --max-parents=0 HEAD)
|
||||
fi
|
||||
|
||||
python3 update_release_notes.py $FROM_COMMIT ${{ inputs.release_tag }} ${{ github.ref_name }}
|
||||
# cat release-notes.md
|
||||
shell: bash
|
||||
|
||||
- name: Upload release-notes.md
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-notes-${{ inputs.short_commit }}.md
|
||||
path: release-notes.md
|
||||
|
||||
- name: Upload update_release_notes.py
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: update_release_notes-${{ inputs.short_commit }}.py
|
||||
path: update_release_notes.py
|
||||
|
||||
- name: Upload update_apps.py
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: update_apps-${{ inputs.short_commit }}.py
|
||||
path: update_apps.py
|
||||
235
.github/workflows/sidestore-deploy.yml
vendored
Normal file
@@ -0,0 +1,235 @@
|
||||
name: SideStore Deploy
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
is_beta:
|
||||
type: boolean
|
||||
publish:
|
||||
type: boolean
|
||||
release_name:
|
||||
type: string
|
||||
release_tag:
|
||||
type: string
|
||||
upstream_tag:
|
||||
type: string
|
||||
upstream_name:
|
||||
type: string
|
||||
version:
|
||||
type: string
|
||||
short_commit:
|
||||
type: string
|
||||
marketing_version:
|
||||
type: string
|
||||
release_channel:
|
||||
type: string
|
||||
bundle_id:
|
||||
type: string
|
||||
secrets:
|
||||
CROSS_REPO_PUSH_KEY:
|
||||
required: true
|
||||
# GITHUB_TOKEN:
|
||||
# required: true
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy SideStore - ${{ inputs.release_tag }}
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- name: Download IPA artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: SideStore-${{ inputs.version }}.ipa
|
||||
|
||||
- name: Download dSYM artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: SideStore-${{ inputs.version }}-dSYMs.zip
|
||||
|
||||
- name: Download encrypted-build-logs artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: encrypted-build-logs-${{ inputs.version }}.zip
|
||||
|
||||
- name: Download encrypted-tests-build-logs artifact
|
||||
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_BUILD == '1' }}
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: encrypted-tests-build-logs-${{ inputs.short_commit }}.zip
|
||||
|
||||
- name: Download encrypted-tests-run-logs artifact
|
||||
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: encrypted-tests-run-logs-${{ inputs.short_commit }}.zip
|
||||
|
||||
- name: Download tests-recording artifact
|
||||
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: tests-recording-${{ inputs.short_commit }}.mp4
|
||||
|
||||
- name: Download test-results artifact
|
||||
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: test-results-${{ inputs.short_commit }}.zip
|
||||
|
||||
- name: Download release-notes.md
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: release-notes-${{ inputs.short_commit }}.md
|
||||
|
||||
- name: Download update_release_notes.py
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: update_release_notes-${{ inputs.short_commit }}.py
|
||||
|
||||
- name: Download update_apps.py
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: update_apps-${{ inputs.short_commit }}.py
|
||||
|
||||
- name: Read release notes
|
||||
id: release_notes
|
||||
run: |
|
||||
CONTENT=$(python3 update_release_notes.py --retrieve ${{ inputs.release_tag }})
|
||||
echo "content<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$CONTENT" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- name: List files before upload
|
||||
run: |
|
||||
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||
find . -maxdepth 4 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
shell: bash
|
||||
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- name: Get current date in AltStore date form
|
||||
id: date_altstore
|
||||
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- name: Upload to releases
|
||||
uses: IsaacShelton/update-existing-release@v1.3.1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
release: ${{ inputs.release_name }}
|
||||
tag: ${{ inputs.release_tag }}
|
||||
prerelease: ${{ inputs.is_beta }}
|
||||
files: SideStore.ipa SideStore.dSYMs.zip encrypted-build-logs.zip encrypted-tests-build-logs.zip encrypted-tests-run-logs.zip test-results.zip tests-recording.mp4
|
||||
body: |
|
||||
This is an ⚠️ **EXPERIMENTAL** ⚠️ ${{ inputs.release_name }} build for commit [${{ github.sha }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }}).
|
||||
|
||||
${{ inputs.release_name }} builds are **extremely experimental builds only meant to be used by developers and beta testers. They often contain bugs and experimental features. Use at your own risk!**
|
||||
|
||||
If you want to try out new features early but want a lower chance of bugs, you can look at [SideStore ${{ inputs.upstream_name }}](https://github.com/${{ github.repository }}/releases?q=${{ inputs.upstream_tag }}).
|
||||
|
||||
## Build Info
|
||||
|
||||
Built at (UTC): `${{ steps.date.outputs.date }}`
|
||||
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
|
||||
Commit SHA: `${{ github.sha }}`
|
||||
Version: `${{ inputs.version }}`
|
||||
|
||||
${{ steps.release_notes.outputs.content }}
|
||||
|
||||
- name: Get formatted date
|
||||
run: |
|
||||
FORMATTED_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
echo "Formatted date: $FORMATTED_DATE"
|
||||
echo "FORMATTED_DATE=$FORMATTED_DATE" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- name: Get size of IPA in bytes (macOS/Linux)
|
||||
run: |
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
# macOS
|
||||
IPA_SIZE=$(stat -f %z SideStore.ipa)
|
||||
else
|
||||
# Linux
|
||||
IPA_SIZE=$(stat -c %s SideStore.ipa)
|
||||
fi
|
||||
echo "IPA size in bytes: $IPA_SIZE"
|
||||
echo "IPA_SIZE=$IPA_SIZE" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- name: Compute SHA-256 of IPA
|
||||
run: |
|
||||
SHA256_HASH=$(shasum -a 256 SideStore.ipa | awk '{ print $1 }')
|
||||
echo "SHA-256 Hash: $SHA256_HASH"
|
||||
echo "SHA256_HASH=$SHA256_HASH" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- name: Set Release Info variables
|
||||
run: |
|
||||
# Format localized description
|
||||
LOCALIZED_DESCRIPTION=$(cat <<EOF
|
||||
This is release for:
|
||||
- version: "${{ inputs.version }}"
|
||||
- revision: "${{ inputs.short_commit }}"
|
||||
- timestamp: "${{ steps.date.outputs.date }}"
|
||||
|
||||
Release Notes:
|
||||
${{ steps.release_notes.outputs.content }}
|
||||
EOF
|
||||
)
|
||||
|
||||
echo "IS_BETA=${{ inputs.is_beta }}" >> $GITHUB_ENV
|
||||
echo "BUNDLE_IDENTIFIER=${{ inputs.bundle_id }}" >> $GITHUB_ENV
|
||||
echo "VERSION_IPA=${{ inputs.marketing_version }}" >> $GITHUB_ENV
|
||||
echo "VERSION_DATE=$FORMATTED_DATE" >> $GITHUB_ENV
|
||||
echo "RELEASE_CHANNEL=${{ inputs.release_channel }}" >> $GITHUB_ENV
|
||||
echo "SIZE=$IPA_SIZE" >> $GITHUB_ENV
|
||||
echo "SHA256=$SHA256_HASH" >> $GITHUB_ENV
|
||||
echo "DOWNLOAD_URL=https://github.com/SideStore/SideStore/releases/download/${{ inputs.release_tag }}/SideStore.ipa" >> $GITHUB_ENV
|
||||
|
||||
# multiline strings
|
||||
echo "LOCALIZED_DESCRIPTION<<EOF" >> $GITHUB_ENV
|
||||
echo "$LOCALIZED_DESCRIPTION" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- name: Check if Publish updates is set
|
||||
id: check_publish
|
||||
run: |
|
||||
echo "Publish updates to source.json = ${{ inputs.publish }}"
|
||||
shell: bash
|
||||
|
||||
- name: Checkout SideStore/apps-v2.json
|
||||
if: ${{ inputs.is_beta && inputs.publish }}
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: 'SideStore/apps-v2.json'
|
||||
ref: 'main' # this branch is shared by all beta builds, so beta build workflows are serialized
|
||||
token: ${{ secrets.CROSS_REPO_PUSH_KEY }}
|
||||
path: 'SideStore/apps-v2.json'
|
||||
|
||||
# for stable builds, let the user manually edit the source.json
|
||||
- name: Publish to SideStore/apps-v2.json
|
||||
if: ${{ inputs.is_beta && inputs.publish }}
|
||||
id: publish-release
|
||||
shell: bash
|
||||
run: |
|
||||
# Copy and execute the update script
|
||||
pushd SideStore/apps-v2.json/
|
||||
|
||||
# Configure Git user (committer details)
|
||||
git config user.name "GitHub Actions"
|
||||
git config user.email "github-actions@github.com"
|
||||
|
||||
# update the source.json
|
||||
python3 ../../update_apps.py "./_includes/source.json"
|
||||
|
||||
# Commit changes and push using SSH
|
||||
git add --verbose ./_includes/source.json
|
||||
git commit -m " - updated for ${{ inputs.short_commit }} deployment" || echo "No changes to commit"
|
||||
|
||||
git push --verbose
|
||||
popd
|
||||
24
.github/workflows/sidestore-shared.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: SideStore Shared
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
outputs:
|
||||
short-commit:
|
||||
value: ${{ jobs.shared.outputs.short-commit }}
|
||||
|
||||
jobs:
|
||||
shared:
|
||||
name: Shared Steps
|
||||
strategy:
|
||||
fail-fast: false
|
||||
runs-on: 'macos-15'
|
||||
steps:
|
||||
- name: Set short commit hash
|
||||
id: commit-id
|
||||
run: |
|
||||
# SHORT_COMMIT="${{ github.sha }}"
|
||||
SHORT_COMMIT=${GITHUB_SHA:0:7}
|
||||
echo "Short commit hash: $SHORT_COMMIT"
|
||||
echo "SHORT_COMMIT=$SHORT_COMMIT" >> $GITHUB_OUTPUT
|
||||
outputs:
|
||||
short-commit: ${{ steps.commit-id.outputs.SHORT_COMMIT }}
|
||||
204
.github/workflows/sidestore-tests-build.yml
vendored
Normal file
@@ -0,0 +1,204 @@
|
||||
name: SideStore Tests Build
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
release_tag:
|
||||
type: string
|
||||
short_commit:
|
||||
type: string
|
||||
secrets:
|
||||
BUILD_LOG_ZIP_PASSWORD:
|
||||
required: false
|
||||
|
||||
jobs:
|
||||
tests-build:
|
||||
name: Tests-Build SideStore - ${{ inputs.release_tag }}
|
||||
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_BUILD == '1' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: 'macos-15'
|
||||
version: '16.2'
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install dependencies - xcbeautify
|
||||
run: |
|
||||
brew install xcbeautify
|
||||
shell: bash
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||
with:
|
||||
xcode-version: '16.2'
|
||||
|
||||
# - name: (Tests-Build) Cache Build
|
||||
# uses: irgaly/xcode-cache@v1.8.1
|
||||
# with:
|
||||
# key: xcode-cache-deriveddata-test-${{ github.ref_name }}-${{ github.sha }}
|
||||
# # tests shouldn't restore cache unless it is same build
|
||||
# # restore-keys: xcode-cache-deriveddata-test-${{ github.ref_name }}-
|
||||
# swiftpm-cache-key: xcode-cache-sourcedata-test-${{ github.ref_name }}-${{ github.sha }}
|
||||
# swiftpm-cache-restore-keys: |
|
||||
# xcode-cache-sourcedata-test-${{ github.ref_name }}-
|
||||
# delete-used-deriveddata-cache: true
|
||||
|
||||
- name: (Tests-Build) Restore Xcode & SwiftPM Cache (Exact match)
|
||||
id: xcode-cache-restore
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
~/Library/Developer/Xcode/DerivedData
|
||||
~/Library/Caches/org.swift.swiftpm
|
||||
key: xcode-cache-tests-${{ github.ref_name }}-${{ github.sha }}
|
||||
|
||||
- name: (Tests-Build) Restore Xcode & SwiftPM Cache (Last Available)
|
||||
id: xcode-cache-restore-recent
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
~/Library/Developer/Xcode/DerivedData
|
||||
~/Library/Caches/org.swift.swiftpm
|
||||
key: xcode-cache-tests-${{ github.ref_name }}-
|
||||
|
||||
- name: (Tests-Build) Restore Pods from Cache (Exact match)
|
||||
id: pods-restore
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-test-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
|
||||
|
||||
- name: (Tests-Build) Restore Pods from Cache (Last Available)
|
||||
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
||||
id: pods-restore-recent
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-test-${{ github.ref_name }}-
|
||||
|
||||
- name: (Tests-Build) Install CocoaPods
|
||||
run: pod install
|
||||
shell: bash
|
||||
|
||||
- name: (Tests-Build) Save Pods to Cache
|
||||
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
||||
uses: actions/cache/save@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-test-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
|
||||
|
||||
- name: Clean Derived Data (if required)
|
||||
if: ${{ vars.PERFORM_CLEAN_TESTS_BUILD == '1' }}
|
||||
run: |
|
||||
rm -rf ~/Library/Developer/Xcode/DerivedData/
|
||||
make clean
|
||||
xcodebuild clean
|
||||
shell: bash
|
||||
|
||||
- name: (Tests-Build) Clean previous build artifacts
|
||||
run: |
|
||||
make clean
|
||||
mkdir -p build/logs
|
||||
shell: bash
|
||||
|
||||
- name: (Tests-Build) List Files and derived data
|
||||
shell: bash
|
||||
run: |
|
||||
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||
ls -la .
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Pods <<<<<<<<<<"
|
||||
find Pods -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> SideStore <<<<<<<<<<"
|
||||
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Dependencies <<<<<<<<<<"
|
||||
find Dependencies -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
|
||||
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
- name: Build SideStore Tests
|
||||
# using 'tee' to intercept stdout and log for detailed build-log
|
||||
shell: bash
|
||||
run: |
|
||||
NSUnbufferedIO=YES make -B build-tests 2>&1 | tee -a build/logs/tests-build.log | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
|
||||
|
||||
- name: (Tests-Build) Save Xcode & SwiftPM Cache
|
||||
id: cache-save
|
||||
if: ${{ steps.xcode-cache-restore.outputs.cache-hit != 'true' }}
|
||||
uses: actions/cache/save@v3
|
||||
with:
|
||||
path: |
|
||||
~/Library/Developer/Xcode/DerivedData
|
||||
~/Library/Caches/org.swift.swiftpm
|
||||
key: xcode-cache-tests-${{ github.ref_name }}-${{ github.sha }}
|
||||
|
||||
- name: (Tests-Build) List Files and Build artifacts
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||
ls -la .
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Build <<<<<<<<<<"
|
||||
find build -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
|
||||
find ~/Library/Developer/Xcode/DerivedData -maxdepth 8 -exec ls -ld {} + | grep "Build/Products" >> tests-build-deriveddata.txt || true
|
||||
echo ""
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: tests-build-deriveddata-${{ inputs.short_commit }}.txt
|
||||
path: tests-build-deriveddata.txt
|
||||
|
||||
- name: Encrypt tests-build-logs for upload
|
||||
id: encrypt-test-log
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
DEFAULT_BUILD_LOG_PASSWORD=12345
|
||||
|
||||
BUILD_LOG_ZIP_PASSWORD=${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
|
||||
BUILD_LOG_ZIP_PASSWORD=${BUILD_LOG_ZIP_PASSWORD:-$DEFAULT_BUILD_LOG_PASSWORD}
|
||||
|
||||
if [ "$BUILD_LOG_ZIP_PASSWORD" == "$DEFAULT_BUILD_LOG_PASSWORD" ]; then
|
||||
echo "Warning: BUILD_LOG_ZIP_PASSWORD is not set. Defaulting to '${DEFAULT_BUILD_LOG_PASSWORD}'."
|
||||
fi
|
||||
|
||||
pushd build/logs && zip -e -P "$BUILD_LOG_ZIP_PASSWORD" ../../encrypted-tests-build-logs.zip * || popd
|
||||
echo "::set-output name=encrypted::true"
|
||||
|
||||
- name: Upload encrypted-tests-build-logs.zip
|
||||
id: attach-encrypted-test-log
|
||||
if: always() && steps.encrypt-test-log.outputs.encrypted == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: encrypted-tests-build-logs-${{ inputs.short_commit }}.zip
|
||||
path: encrypted-tests-build-logs.zip
|
||||
235
.github/workflows/sidestore-tests-run.yml
vendored
Normal file
@@ -0,0 +1,235 @@
|
||||
name: SideStore Tests Run
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
release_tag:
|
||||
type: string
|
||||
short_commit:
|
||||
type: string
|
||||
secrets:
|
||||
BUILD_LOG_ZIP_PASSWORD:
|
||||
required: false
|
||||
|
||||
jobs:
|
||||
tests-run:
|
||||
name: Tests-Run SideStore - ${{ inputs.release_tag }}
|
||||
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: 'macos-15'
|
||||
version: '16.2'
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Boot Simulator async(nohup) for testing
|
||||
run: |
|
||||
mkdir -p build/logs
|
||||
nohup make -B boot-sim-async </dev/null >> build/logs/tests-run.log 2>&1 &
|
||||
shell: bash
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||
with:
|
||||
xcode-version: '16.2'
|
||||
|
||||
# - name: (Tests-Run) Cache Build
|
||||
# uses: irgaly/xcode-cache@v1.8.1
|
||||
# with:
|
||||
# # This comes from
|
||||
# key: xcode-cache-deriveddata-test-${{ github.ref_name }}-${{ github.sha }}
|
||||
# swiftpm-cache-key: xcode-cache-sourcedata-test-${{ github.ref_name }}-${{ github.sha }}
|
||||
|
||||
- name: (Tests-Build) Restore Xcode & SwiftPM Cache (Exact match) [from tests-build job]
|
||||
id: xcode-cache-restore
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
~/Library/Developer/Xcode/DerivedData
|
||||
~/Library/Caches/org.swift.swiftpm
|
||||
key: xcode-cache-tests-${{ github.ref_name }}-${{ github.sha }}
|
||||
|
||||
- name: (Tests-Run) Restore Pods from Cache (Exact match)
|
||||
id: pods-restore
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-test-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
|
||||
|
||||
- name: (Tests-Run) Restore Pods from Cache (Last Available)
|
||||
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
||||
id: pods-restore-recent
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-test-${{ github.ref_name }}-
|
||||
|
||||
- name: (Tests-Run) Install CocoaPods
|
||||
run: pod install
|
||||
shell: bash
|
||||
|
||||
- name: (Tests-Run) Save Pods to Cache
|
||||
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
||||
uses: actions/cache/save@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-test-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
|
||||
|
||||
- name: (Tests-Run) Clean previous build artifacts
|
||||
run: |
|
||||
make clean
|
||||
mkdir -p build/logs
|
||||
shell: bash
|
||||
|
||||
- name: (Tests-Run) List Files and derived data
|
||||
shell: bash
|
||||
run: |
|
||||
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||
ls -la .
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Pods <<<<<<<<<<"
|
||||
find Pods -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> SideStore <<<<<<<<<<"
|
||||
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Dependencies <<<<<<<<<<"
|
||||
find Dependencies -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
|
||||
find ~/Library/Developer/Xcode/DerivedData -maxdepth 8 -exec ls -ld {} + | grep "Build/Products" >> tests-run-deriveddata.txt || true
|
||||
echo ""
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: tests-run-deriveddata-${{ inputs.short_commit }}.txt
|
||||
path: tests-run-deriveddata.txt
|
||||
|
||||
# we expect simulator to have been booted by now, so exit otherwise
|
||||
- name: Simulator Boot Check
|
||||
run: |
|
||||
mkdir -p build/logs
|
||||
make -B sim-boot-check | tee -a build/logs/tests-run.log
|
||||
exit ${PIPESTATUS[0]}
|
||||
shell: bash
|
||||
|
||||
- name: Start Recording UI tests (if DEBUG_RECORD_TESTS is set to 1)
|
||||
if: ${{ vars.DEBUG_RECORD_TESTS == '1' }}
|
||||
run: |
|
||||
nohup xcrun simctl io booted recordVideo -f tests-recording.mp4 --codec h264 </dev/null > tests-recording.log 2>&1 &
|
||||
RECORD_PID=$!
|
||||
echo "RECORD_PID=$RECORD_PID" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- name: Run SideStore Tests
|
||||
# using 'tee' to intercept stdout and log for detailed build-log
|
||||
run: |
|
||||
make run-tests 2>&1 | tee -a build/logs/tests-run.log && exit ${PIPESTATUS[0]}
|
||||
# NSUnbufferedIO=YES make -B run-tests 2>&1 | tee build/logs/tests-run.log | xcpretty -r junit --output ./build/tests/test-results.xml && exit ${PIPESTATUS[0]}
|
||||
shell: bash
|
||||
|
||||
- name: Stop Recording tests
|
||||
if: ${{ always() && env.RECORD_PID != '' }}
|
||||
run: |
|
||||
kill -INT ${{ env.RECORD_PID }}
|
||||
shell: bash
|
||||
|
||||
- name: (Tests-Run) List Files and Build artifacts
|
||||
if: always()
|
||||
run: |
|
||||
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||
ls -la .
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Build <<<<<<<<<<"
|
||||
find build -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
shell: bash
|
||||
|
||||
- name: Encrypt tests-run-logs for upload
|
||||
id: encrypt-test-log
|
||||
if: always()
|
||||
run: |
|
||||
DEFAULT_BUILD_LOG_PASSWORD=12345
|
||||
|
||||
BUILD_LOG_ZIP_PASSWORD=${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
|
||||
BUILD_LOG_ZIP_PASSWORD=${BUILD_LOG_ZIP_PASSWORD:-$DEFAULT_BUILD_LOG_PASSWORD}
|
||||
|
||||
if [ "$BUILD_LOG_ZIP_PASSWORD" == "$DEFAULT_BUILD_LOG_PASSWORD" ]; then
|
||||
echo "Warning: BUILD_LOG_ZIP_PASSWORD is not set. Defaulting to '${DEFAULT_BUILD_LOG_PASSWORD}'."
|
||||
fi
|
||||
|
||||
pushd build/logs && zip -e -P "$BUILD_LOG_ZIP_PASSWORD" ../../encrypted-tests-run-logs.zip * || popd
|
||||
echo "::set-output name=encrypted::true"
|
||||
shell: bash
|
||||
|
||||
- name: Upload encrypted-tests-run-logs.zip
|
||||
id: attach-encrypted-test-log
|
||||
if: always() && steps.encrypt-test-log.outputs.encrypted == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: encrypted-tests-run-logs-${{ inputs.short_commit }}.zip
|
||||
path: encrypted-tests-run-logs.zip
|
||||
|
||||
- name: Print tests-recording.log contents (if exists)
|
||||
if: ${{ always() && env.RECORD_PID != '' }}
|
||||
run: |
|
||||
if [ -f tests-recording.log ]; then
|
||||
echo "tests-recording.log found. Its contents:"
|
||||
cat tests-recording.log
|
||||
else
|
||||
echo "tests-recording.log not found."
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Check for tests-recording.mp4 presence
|
||||
id: check-recording
|
||||
if: ${{ always() && env.RECORD_PID != '' }}
|
||||
run: |
|
||||
if [ -f tests-recording.mp4 ]; then
|
||||
echo "::set-output name=found::true"
|
||||
echo "tests-recording.mp4 found."
|
||||
else
|
||||
echo "tests-recording.mp4 not found, skipping upload."
|
||||
echo "::set-output name=found::false"
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Upload tests-recording.mp4
|
||||
id: upload-recording
|
||||
if: ${{ always() && steps.check-recording.outputs.found == 'true' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: tests-recording-${{ inputs.short_commit }}.mp4
|
||||
path: tests-recording.mp4
|
||||
|
||||
- name: Zip test-results
|
||||
run: zip -r -9 ./test-results.zip ./build/tests
|
||||
shell: bash
|
||||
|
||||
- name: Upload Test Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-results-${{ inputs.short_commit }}.zip
|
||||
path: test-results.zip
|
||||
283
.github/workflows/stable.yml
vendored
Normal file
@@ -0,0 +1,283 @@
|
||||
name: Stable SideStore build
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '[0-9]+.[0-9]+.[0-9]+' # example: 1.0.0
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build SideStore - stable (on tag push)
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: 'macos-15'
|
||||
version: '16.2'
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Echo Build.xcconfig
|
||||
run: |
|
||||
echo "cat Build.xcconfig"
|
||||
cat Build.xcconfig
|
||||
shell: bash
|
||||
|
||||
# - name: Change MARKETING_VERSION to the pushed tag that triggered this build
|
||||
# run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
|
||||
|
||||
- name: Echo Updated Build.xcconfig
|
||||
run: |
|
||||
cat Build.xcconfig
|
||||
shell: bash
|
||||
|
||||
- name: Extract MARKETING_VERSION from Build.xcconfig
|
||||
id: version
|
||||
run: |
|
||||
version=$(grep MARKETING_VERSION Build.xcconfig | sed -e 's/MARKETING_VERSION = //g')
|
||||
echo "version=$version" >> $GITHUB_OUTPUT
|
||||
echo "version=$version"
|
||||
|
||||
echo "MARKETING_VERSION=$version" >> $GITHUB_ENV
|
||||
echo "MARKETING_VERSION=$version" >> $GITHUB_OUTPUT
|
||||
echo "MARKETING_VERSION=$version"
|
||||
|
||||
shell: bash
|
||||
|
||||
- name: Fail the build if pushed tag and embedded MARKETING_VERSION in Build.xcconfig are mismatching
|
||||
run: |
|
||||
if [ "$MARKETING_VERSION" != "${{ github.ref_name }}" ]; then
|
||||
echo 'Version mismatch: $tag != $marketing_version ... '
|
||||
echo " expected-tag : $MARKETING_VERSION"
|
||||
echo " pushed-tag : ${{ github.ref_name }}"
|
||||
exit 1
|
||||
fi
|
||||
echo 'Version matches: $tag == $marketing_version ... '
|
||||
echo " expected-tag : $MARKETING_VERSION"
|
||||
echo " pushed-tag : ${{ github.ref_name }}"
|
||||
shell: bash
|
||||
|
||||
- name: Install dependencies - ldid & xcbeautify
|
||||
run: |
|
||||
brew install ldid xcbeautify
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||
with:
|
||||
xcode-version: ${{ matrix.version }}
|
||||
|
||||
- name: (Build) Restore Xcode & SwiftPM Cache (Exact match)
|
||||
id: xcode-cache-restore
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
~/Library/Developer/Xcode/DerivedData
|
||||
~/Library/Caches/org.swift.swiftpm
|
||||
key: xcode-cache-build-stable-${{ github.sha }}
|
||||
|
||||
- name: (Build) Restore Xcode & SwiftPM Cache (Last Available)
|
||||
id: xcode-cache-restore-recent
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
~/Library/Developer/Xcode/DerivedData
|
||||
~/Library/Caches/org.swift.swiftpm
|
||||
key: xcode-cache-build-stable-
|
||||
|
||||
- name: (Build) Restore Pods from Cache (Exact match)
|
||||
id: pods-restore
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-build-stable-${{ hashFiles('Podfile') }}
|
||||
|
||||
- name: (Build) Restore Pods from Cache (Last Available)
|
||||
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
||||
id: pods-restore-recent
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-build-stable-
|
||||
|
||||
|
||||
- name: (Build) Install CocoaPods
|
||||
run: pod install
|
||||
shell: bash
|
||||
|
||||
- name: (Build) Save Pods to Cache
|
||||
id: save-pods
|
||||
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
||||
uses: actions/cache/save@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-build-stable-${{ hashFiles('Podfile') }}
|
||||
|
||||
- name: (Build) Clean previous build artifacts
|
||||
run: |
|
||||
make clean
|
||||
mkdir -p build/logs
|
||||
shell: bash
|
||||
|
||||
- name: (Build) List Files and derived data
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||
ls -la .
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Pods <<<<<<<<<<"
|
||||
find Pods -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> SideStore <<<<<<<<<<"
|
||||
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Dependencies <<<<<<<<<<"
|
||||
find Dependencies -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
|
||||
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
- name: Build SideStore.xcarchive
|
||||
# using 'tee' to intercept stdout and log for detailed build-log
|
||||
run: |
|
||||
NSUnbufferedIO=YES make -B build 2>&1 | tee -a build/logs/build.log | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
|
||||
shell: bash
|
||||
|
||||
- name: Fakesign app
|
||||
run: make fakesign | tee -a build/logs/build.log
|
||||
shell: bash
|
||||
|
||||
- name: Convert to IPA
|
||||
run: make ipa | tee -a build/logs/build.log
|
||||
shell: bash
|
||||
|
||||
- name: (Build) Save Xcode & SwiftPM Cache
|
||||
id: cache-save
|
||||
if: ${{ steps.xcode-cache-restore.outputs.cache-hit != 'true' }}
|
||||
uses: actions/cache/save@v3
|
||||
with:
|
||||
path: |
|
||||
~/Library/Developer/Xcode/DerivedData
|
||||
~/Library/Caches/org.swift.swiftpm
|
||||
key: xcode-cache-build-stable-${{ github.sha }}
|
||||
|
||||
- name: (Build) List Files and Build artifacts
|
||||
run: |
|
||||
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||
ls -la .
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Build <<<<<<<<<<"
|
||||
find build -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> SideStore <<<<<<<<<<"
|
||||
find SideStore -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> SideStore.xcarchive <<<<<<<<<<"
|
||||
find SideStore.xcarchive -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
|
||||
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
|
||||
echo ""
|
||||
shell: bash
|
||||
|
||||
- name: Encrypt build-logs for upload
|
||||
id: encrypt-build-log
|
||||
run: |
|
||||
DEFAULT_BUILD_LOG_PASSWORD=12345
|
||||
|
||||
BUILD_LOG_ZIP_PASSWORD=${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
|
||||
BUILD_LOG_ZIP_PASSWORD=${BUILD_LOG_ZIP_PASSWORD:-$DEFAULT_BUILD_LOG_PASSWORD}
|
||||
|
||||
if [ "$BUILD_LOG_ZIP_PASSWORD" == "$DEFAULT_BUILD_LOG_PASSWORD" ]; then
|
||||
echo "Warning: BUILD_LOG_ZIP_PASSWORD is not set. Defaulting to '${DEFAULT_BUILD_LOG_PASSWORD}'."
|
||||
fi
|
||||
|
||||
pushd build/logs && zip -e -P "$BUILD_LOG_ZIP_PASSWORD" ../../encrypted-build-logs.zip * || popd
|
||||
echo "::set-output name=encrypted::true"
|
||||
shell: bash
|
||||
|
||||
- name: Upload encrypted-build-logs.zip
|
||||
id: attach-encrypted-build-log
|
||||
if: ${{ always() && steps.encrypt-build-log.outputs.encrypted == 'true' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: encrypted-build-logs-${{ steps.version.outputs.version }}.zip
|
||||
path: encrypted-build-logs.zip
|
||||
|
||||
- name: Upload SideStore.ipa Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
path: SideStore.ipa
|
||||
|
||||
- name: Zip dSYMs
|
||||
run: zip -r -9 ./SideStore.dSYMs.zip ./SideStore.xcarchive/dSYMs
|
||||
shell: bash
|
||||
|
||||
- name: Upload *.dSYM Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}-dSYMs.zip
|
||||
path: SideStore.dSYMs.zip
|
||||
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- name: Get current date in AltStore date form
|
||||
id: date_altstore
|
||||
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- name: Upload to releases
|
||||
uses: IsaacShelton/update-existing-release@v1.3.1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
draft: true
|
||||
release: ${{ github.ref_name }} # name
|
||||
tag: ${{ github.ref_name }}
|
||||
# stick with what the user pushed, do not use latest commit or anything,
|
||||
# ex: if we want to go back to previous release due to hot issue, dev can create a new tag pointing to that older working tag/commit so as to keep it as an update (to revert major issue)
|
||||
# in this case we do not want the tag to be auto-updated to latest
|
||||
updateTag: false
|
||||
prerelease: false
|
||||
files: >
|
||||
SideStore.ipa
|
||||
SideStore.dSYMs.zip
|
||||
encrypted-build-logs.zip
|
||||
body: |
|
||||
<!-- NOTE: to reset SideSource cache, go to `https://apps.sidestore.io/reset-cache/nightly/<sidesource key>`. This is not included in the GitHub Action since it makes draft releases so they can be edited and have a changelog. -->
|
||||
## Changelog
|
||||
|
||||
- TODO
|
||||
|
||||
## Build Info
|
||||
|
||||
Built at (UTC): `${{ steps.date.outputs.date }}`
|
||||
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
|
||||
Commit SHA: `${{ github.sha }}`
|
||||
Version: `${{ steps.version.outputs.version }}`
|
||||
46
.gitignore
vendored
@@ -1,14 +1,18 @@
|
||||
# macOS
|
||||
#
|
||||
*.DS_Store
|
||||
**/*.DS_Store
|
||||
|
||||
# Xcode
|
||||
#
|
||||
|
||||
## CocoaPods
|
||||
Pods/
|
||||
|
||||
## Build generated
|
||||
build/
|
||||
DerivedData
|
||||
|
||||
SideStore.xcarchive
|
||||
## Various settings
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
@@ -27,4 +31,42 @@ xcuserdata
|
||||
*.xcscmblueprint
|
||||
|
||||
## Obj-C/Swift specific
|
||||
*.hmap
|
||||
*.hmap
|
||||
/newrelic_agent.log
|
||||
/CodeSigning.xcconfig
|
||||
/.vscode
|
||||
|
||||
## AppCode specific
|
||||
.idea/
|
||||
|
||||
Payload/
|
||||
**/SideStore.ipa
|
||||
**/AltBackup.ipa
|
||||
**/*.dSYM
|
||||
|
||||
Dependencies/.*-prebuilt-fetch-*
|
||||
SideStore/minimuxer/*
|
||||
SideStore/em_proxy/*
|
||||
!Dependencies/**/.gitkeep
|
||||
.nightly-build-num
|
||||
|
||||
## em_proxy and minimuxer biaries
|
||||
**/.last-prebuilt-fetch-em_proxy
|
||||
**/.last-prebuilt-fetch-minimuxer
|
||||
|
||||
# misc
|
||||
**/output.txt
|
||||
SideStore/.skip-prebuilt-fetch-minimuxer
|
||||
SideStore/.skip-prebuilt-fetch-em_proxy
|
||||
|
||||
.git.bkp/
|
||||
# Never check-in this package.resolved file
|
||||
# coz SPM then resolves packages using the stale entries in this file
|
||||
*.xcodeproj/**/Package.resolved
|
||||
*.xcworkspace/**/Package.resolved
|
||||
|
||||
# some more commandline build artifacts
|
||||
test-recording.mp4
|
||||
test-recording.log
|
||||
altstore-sources.md
|
||||
local-build.sh
|
||||
76
.gitmodules
vendored
@@ -1,18 +1,68 @@
|
||||
#-------------------------------
|
||||
# When changing url/branch in this .gitmodules file,
|
||||
# Always ensure you run:
|
||||
# 1. `git rm --cached <submodule_relative_path>` # this removes the submodule entry from general git tracking
|
||||
# 2. `rm -rf .git/modules/<submodule_relative_path>` # this removes the stale name entries in submodule tracker
|
||||
# 3. `rm -rf <submodule_relative_path>` # removes the submodule completely
|
||||
# 4. `git submodule --deinit <submodule_relative_path>` # make sure that the submodule is de-inited too (ignore errors at this point)
|
||||
# 5. `git submodule add [-b <branch_name>] <repo_url> <submodule_relative_path>` # This adds the submodule back into general git tracking and also adds to the submodule tracker
|
||||
# 6. Step 5 creates an entry in the .gitmodules when a submodule is added,
|
||||
# So if you already had one entry, try to remove duplicates at this point
|
||||
# 7. `git submodule sync --recursive` # this now sets/updates the submodule repo url tracker into git config
|
||||
# 8. `git submodule update --init --recursive` # this now clones the updated repo set by .gitmodules
|
||||
# But this will always fetch the latest commit sepecified by the custom(if set)/default branch
|
||||
# 9. If you do want to have a specific commit in that submodule branch and not latest, you need to perform normal detached head checkout and check-in as follows:
|
||||
# `pushd <submodule_relative_path>` # switch to the submodule repo
|
||||
# `git checkout <commit-id>` # this creates a detached head state
|
||||
# `popd` # get back to parent repo
|
||||
# `git add <submodule_relative_path>` # check-in the changes in parent for this submodule link (tracker)
|
||||
# `git commit -m <commit-message>` # commit it to parent repo
|
||||
# `git push` # push to parent repo to preserve this entire change in the submodule repo/link file
|
||||
#
|
||||
# NOTES:
|
||||
# 1. updating just this .gitmodules file is NOT ENOUGH when changing repo url and performing a simple `git submodule update --init --recursive`, need to do all the above listed steps for proper tracking
|
||||
# 2. updating the branch in this .gitmodules for same repo is okay as long as `git submodule update --init --recursive` is also performed followed by it
|
||||
# 3. Ensure there is no stale entries or duplicate entries in this .gitmodules file coz, `git submodule add ...` creates an entry here.
|
||||
#-------------------------------
|
||||
|
||||
[submodule "Dependencies/Roxas"]
|
||||
path = Dependencies/Roxas
|
||||
url = https://github.com/rileytestut/Roxas.git
|
||||
[submodule "Dependencies/AltSign"]
|
||||
path = Dependencies/AltSign
|
||||
url = https://github.com/rileytestut/AltSign.git
|
||||
path = Dependencies/Roxas
|
||||
url = https://github.com/rileytestut/Roxas.git
|
||||
[submodule "Dependencies/libimobiledevice"]
|
||||
path = Dependencies/libimobiledevice
|
||||
url = https://github.com/rileytestut/libimobiledevice.git
|
||||
path = Dependencies/libimobiledevice
|
||||
url = https://github.com/libimobiledevice/libimobiledevice
|
||||
[submodule "Dependencies/libusbmuxd"]
|
||||
path = Dependencies/libusbmuxd
|
||||
url = https://github.com/libimobiledevice/libusbmuxd.git
|
||||
path = Dependencies/libusbmuxd
|
||||
url = https://github.com/libimobiledevice/libusbmuxd.git
|
||||
[submodule "Dependencies/libplist"]
|
||||
path = Dependencies/libplist
|
||||
url = https://github.com/libimobiledevice/libplist.git
|
||||
path = Dependencies/libplist
|
||||
url = https://github.com/SideStore/libplist.git
|
||||
[submodule "Dependencies/MarkdownAttributedString"]
|
||||
path = Dependencies/MarkdownAttributedString
|
||||
url = https://github.com/chockenberry/MarkdownAttributedString.git
|
||||
path = Dependencies/MarkdownAttributedString
|
||||
url = https://github.com/chockenberry/MarkdownAttributedString.git
|
||||
[submodule "Dependencies/libimobiledevice-glue"]
|
||||
path = Dependencies/libimobiledevice-glue
|
||||
url = https://github.com/libimobiledevice/libimobiledevice-glue
|
||||
|
||||
|
||||
#sidestore dependencies
|
||||
[submodule "SideStore/minimuxer"]
|
||||
path = SideStore/minimuxer
|
||||
url = https://github.com/SideStore/minimuxer
|
||||
branch = master
|
||||
[submodule "SideStore/em_proxy"]
|
||||
path = SideStore/em_proxy
|
||||
url = https://github.com/SideStore/em_proxy
|
||||
branch = master
|
||||
[submodule "SideStore/libfragmentzip"]
|
||||
path = SideStore/libfragmentzip
|
||||
url = https://github.com/SideStore/libfragmentzip
|
||||
branch = master
|
||||
[submodule "SideStore/apps-v2.json"]
|
||||
path = SideStore/apps-v2.json
|
||||
url = https://github.com/SideStore/apps-v2.json
|
||||
branch = main
|
||||
[submodule "SideStore/AltSign"]
|
||||
path = SideStore/AltSign
|
||||
url = https://github.com/SideStore/AltSign
|
||||
branch = master
|
||||
@@ -4,7 +4,7 @@
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.rileytestut.AltStore</string>
|
||||
<string>group.$(APP_GROUP_IDENTIFIER)</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -10,10 +10,10 @@ import UIKit
|
||||
|
||||
extension AppDelegate
|
||||
{
|
||||
static let startBackupNotification = Notification.Name("io.altstore.StartBackup")
|
||||
static let startRestoreNotification = Notification.Name("io.altstore.StartRestore")
|
||||
static let startBackupNotification = Notification.Name("io.sidestore.StartBackup")
|
||||
static let startRestoreNotification = Notification.Name("io.sidestore.StartRestore")
|
||||
|
||||
static let operationDidFinishNotification = Notification.Name("io.altstore.BackupOperationFinished")
|
||||
static let operationDidFinishNotification = Notification.Name("io.sidestore.BackupOperationFinished")
|
||||
|
||||
static let operationResultKey = "result"
|
||||
}
|
||||
@@ -88,14 +88,25 @@ private extension AppDelegate
|
||||
|
||||
@objc func operationDidFinish(_ notification: Notification)
|
||||
{
|
||||
defer { self.currentBackupReturnURL = nil }
|
||||
defer {
|
||||
self.currentBackupReturnURL = nil
|
||||
}
|
||||
|
||||
// TODO: @mahee96: This doesn't account cases where backup is too long and user switched to other apps
|
||||
// The check for self.currentBackupReturnURL when backup/restore was still in progress but app switched
|
||||
// between FG/BG is improper, since it will ignore(eat up) the response(success/failure) to parent
|
||||
//
|
||||
// This leaves the backup/restore to show dummy animation forever
|
||||
guard
|
||||
let returnURL = self.currentBackupReturnURL,
|
||||
let result = notification.userInfo?[AppDelegate.operationResultKey] as? Result<Void, Error>
|
||||
else { return }
|
||||
else {
|
||||
return // This is bad (Needs fixing - never eat up response like this unless there is no context to post response to!)
|
||||
}
|
||||
|
||||
guard var components = URLComponents(url: returnURL, resolvingAgainstBaseURL: false) else { return }
|
||||
guard var components = URLComponents(url: returnURL, resolvingAgainstBaseURL: false) else {
|
||||
return // This is ASSERTION Failure, ie RETURN URL needs to be valid. So ignoring (eating up) response is not the solution
|
||||
}
|
||||
|
||||
switch result
|
||||
{
|
||||
@@ -112,6 +123,7 @@ private extension AppDelegate
|
||||
guard let responseURL = components.url else { return }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
// Response to the caller/parent app is posted here (url is provided by caller in incoming query params)
|
||||
UIApplication.shared.open(responseURL, options: [:]) { (success) in
|
||||
print("Sent response to app with success:", success)
|
||||
}
|
||||
|
||||
61
AltBackup/BackupController.swift
Normal file → Executable file
@@ -26,18 +26,58 @@ extension Error
|
||||
|
||||
struct BackupError: ALTLocalizedError
|
||||
{
|
||||
enum Code
|
||||
enum Code: ALTErrorEnum, RawRepresentable
|
||||
{
|
||||
case invalidBundleID
|
||||
case appGroupNotFound(String?)
|
||||
case randomError // Used for debugging.
|
||||
|
||||
// Provide failure reason for each error code
|
||||
var errorFailureReason: String {
|
||||
switch self {
|
||||
case .invalidBundleID:
|
||||
return NSLocalizedString("The bundle identifier is invalid.", comment: "")
|
||||
case .appGroupNotFound(let appGroup):
|
||||
if let appGroup = appGroup {
|
||||
return String(format: NSLocalizedString("The app group “%@” could not be found.", comment: ""), appGroup)
|
||||
} else {
|
||||
return NSLocalizedString("The AltStore app group could not be found.", comment: "")
|
||||
}
|
||||
case .randomError:
|
||||
return NSLocalizedString("A random error occurred.", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
static var errorDomain: String {
|
||||
return "com.sidestore.BackupError"
|
||||
}
|
||||
|
||||
// Add a raw value for RawRepresentable conformance
|
||||
var rawValue: Int {
|
||||
switch self {
|
||||
case .invalidBundleID: return 0
|
||||
case .appGroupNotFound: return 1
|
||||
case .randomError: return 2
|
||||
}
|
||||
}
|
||||
|
||||
// Initializer for RawRepresentable
|
||||
init?(rawValue: Int) {
|
||||
switch rawValue {
|
||||
case 0: self = .invalidBundleID
|
||||
case 1: self = .appGroupNotFound(nil)
|
||||
case 2: self = .randomError
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let code: Code
|
||||
|
||||
let sourceFile: String
|
||||
let sourceFileLine: Int
|
||||
|
||||
var failure: String?
|
||||
|
||||
var errorTitle: String?
|
||||
var errorFailure: String?
|
||||
|
||||
var failureReason: String? {
|
||||
@@ -60,18 +100,25 @@ struct BackupError: ALTLocalizedError
|
||||
var errorUserInfo: [String : Any] {
|
||||
let userInfo: [String: Any?] = [NSLocalizedDescriptionKey: self.errorDescription,
|
||||
NSLocalizedFailureReasonErrorKey: self.failureReason,
|
||||
NSLocalizedFailureErrorKey: self.errorFailure,
|
||||
NSLocalizedFailureErrorKey: self.failure,
|
||||
ErrorUserInfoKey.sourceFile: self.sourceFile,
|
||||
ErrorUserInfoKey.sourceFileLine: self.sourceFileLine]
|
||||
return userInfo.compactMapValues { $0 }
|
||||
}
|
||||
|
||||
// Implement description for CustomStringConvertible
|
||||
var description: String {
|
||||
return "\(errorTitle ?? "Unknown Error"): \(failureReason ?? "No reason available")"
|
||||
}
|
||||
|
||||
init(_ code: Code, description: String? = nil, file: String = #file, line: Int = #line)
|
||||
{
|
||||
self.code = code
|
||||
self.errorFailure = description
|
||||
self.failure = description
|
||||
self.sourceFile = file
|
||||
self.sourceFileLine = line
|
||||
self.errorTitle = NSLocalizedString("Backup Error", comment: "")
|
||||
self.errorFailure = description
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +143,9 @@ class BackupController: NSObject
|
||||
guard
|
||||
let altstoreAppGroup = Bundle.main.altstoreAppGroup,
|
||||
let sharedDirectoryURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: altstoreAppGroup)
|
||||
else { throw BackupError(.appGroupNotFound(nil), description: NSLocalizedString("Unable to create backup directory.", comment: "")) }
|
||||
else {
|
||||
throw BackupError(.appGroupNotFound(nil), description: NSLocalizedString("Unable to create backup directory.", comment: ""))
|
||||
}
|
||||
|
||||
let backupsDirectory = sharedDirectoryURL.appendingPathComponent("Backups")
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
<dict>
|
||||
<key>ALTAppGroups</key>
|
||||
<array>
|
||||
<string>group.com.rileytestut.AltStore</string>
|
||||
<string>group.$(APP_GROUP_IDENTIFIER)</string>
|
||||
</array>
|
||||
<key>ALTBundleIdentifier</key>
|
||||
<string>com.rileytestut.AltBackup</string>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
@@ -28,15 +28,15 @@
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>AltBackup General</string>
|
||||
<string>SideBackup General</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>altbackup</string>
|
||||
<string>sidebackup</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
|
||||
18
AltBackup/Resources/ReleaseEntitlements.plist
Executable file
@@ -0,0 +1,18 @@
|
||||
<?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>application-identifier</key>
|
||||
<string>XYZ0123456.com.SideStore.SideStore.AltBackup</string>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.team-identifier</key>
|
||||
<string>XYZ0123456</string>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.SideStore.SideStore</string>
|
||||
</array>
|
||||
<key>get-task-allow</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -82,23 +82,25 @@ class ViewController: UIViewController
|
||||
self.activityIndicatorView.color = .altstoreText
|
||||
self.activityIndicatorView.startAnimating()
|
||||
|
||||
#if DEBUG
|
||||
let button1 = UIButton(type: .system)
|
||||
button1.setTitle("Backup", for: .normal)
|
||||
button1.setTitleColor(.white, for: .normal)
|
||||
button1.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
button1.addTarget(self, action: #selector(ViewController.backup), for: .primaryActionTriggered)
|
||||
|
||||
let button2 = UIButton(type: .system)
|
||||
button2.setTitle("Restore", for: .normal)
|
||||
button2.setTitleColor(.white, for: .normal)
|
||||
button2.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
button2.addTarget(self, action: #selector(ViewController.restore), for: .primaryActionTriggered)
|
||||
|
||||
let arrangedSubviews = [self.textLabel!, self.detailTextLabel!, self.activityIndicatorView!, button1, button2]
|
||||
#else
|
||||
// TODO: @mahee96: Disabled these backup/restore buttons in altbackup.app screen which were present for debugging purpose.
|
||||
// Can find something useful for these later, but these are not required by this backup/restore app
|
||||
// #if DEBUG
|
||||
// let button1 = UIButton(type: .system)
|
||||
// button1.setTitle("Backup", for: .normal)
|
||||
// button1.setTitleColor(.white, for: .normal)
|
||||
// button1.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
// button1.addTarget(self, action: #selector(ViewController.backup), for: .primaryActionTriggered)
|
||||
//
|
||||
// let button2 = UIButton(type: .system)
|
||||
// button2.setTitle("Restore", for: .normal)
|
||||
// button2.setTitleColor(.white, for: .normal)
|
||||
// button2.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
// button2.addTarget(self, action: #selector(ViewController.restore), for: .primaryActionTriggered)
|
||||
//
|
||||
// let arrangedSubviews = [self.textLabel!, self.detailTextLabel!, self.activityIndicatorView!, button1, button2]
|
||||
// #else
|
||||
let arrangedSubviews = [self.textLabel!, self.detailTextLabel!, self.activityIndicatorView!]
|
||||
#endif
|
||||
// #endif
|
||||
|
||||
let stackView = UIStackView(arrangedSubviews: arrangedSubviews)
|
||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
@@ -155,12 +157,13 @@ private extension ViewController
|
||||
self.textLabel.text = NSLocalizedString("Restoring app data…", comment: "")
|
||||
self.detailTextLabel.isHidden = true
|
||||
self.activityIndicatorView.startAnimating()
|
||||
|
||||
|
||||
// TODO: @mahee96: This is pointless since, app going in bg/fg should still report its last operation properly
|
||||
case .none:
|
||||
self.textLabel.text = String(format: NSLocalizedString("%@ is inactive.", comment: ""),
|
||||
Bundle.main.appName ?? NSLocalizedString("App", comment: ""))
|
||||
|
||||
self.detailTextLabel.text = String(format: NSLocalizedString("Refresh %@ in AltStore to continue using it.", comment: ""),
|
||||
self.detailTextLabel.text = String(format: NSLocalizedString("Refresh %@ in SideStore to continue using it.", comment: ""),
|
||||
Bundle.main.appName ?? NSLocalizedString("this app", comment: ""))
|
||||
|
||||
self.detailTextLabel.isHidden = false
|
||||
@@ -198,6 +201,9 @@ private extension ViewController
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: @mahee96: This doesn't account cases where backup is too long and user switched to other apps
|
||||
// Now the user has lost his progress since current operation was cancelled due to switch between FG and BG
|
||||
// if this just the reset for enum such that UI stops showing progress circle, then this is fine!
|
||||
@objc func didEnterBackground(_ notification: Notification)
|
||||
{
|
||||
// Reset UI once we've left app (but not before).
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
//
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
// Shared
|
||||
#import "ALTConstants.h"
|
||||
#import "ALTConnection.h"
|
||||
#import "NSError+ALTServerError.h"
|
||||
#import "CFNotificationName+AltStore.h"
|
||||
|
||||
// libproc
|
||||
int proc_pidpath(int pid, void * buffer, uint32_t buffersize);
|
||||
|
||||
// Security.framework
|
||||
CF_ENUM(uint32_t) {
|
||||
kSecCSInternalInformation = 1 << 0,
|
||||
kSecCSSigningInformation = 1 << 1,
|
||||
kSecCSRequirementInformation = 1 << 2,
|
||||
kSecCSDynamicInformation = 1 << 3,
|
||||
kSecCSContentInformation = 1 << 4,
|
||||
kSecCSSkipResourceDirectory = 1 << 5,
|
||||
kSecCSCalculateCMSDigest = 1 << 6,
|
||||
};
|
||||
|
||||
OSStatus SecStaticCodeCreateWithPath(CFURLRef path, uint32_t flags, void ** __nonnull CF_RETURNS_RETAINED staticCode);
|
||||
OSStatus SecCodeCopySigningInformation(void *code, uint32_t flags, CFDictionaryRef * __nonnull CF_RETURNS_RETAINED information);
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface AKDevice : NSObject
|
||||
|
||||
@property (class, readonly) AKDevice *currentDevice;
|
||||
|
||||
@property (strong, readonly) NSString *serialNumber;
|
||||
@property (strong, readonly) NSString *uniqueDeviceIdentifier;
|
||||
@property (strong, readonly) NSString *serverFriendlyDescription;
|
||||
|
||||
@end
|
||||
|
||||
@interface AKAppleIDSession : NSObject
|
||||
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier;
|
||||
|
||||
- (NSDictionary<NSString *, NSString *> *)appleIDHeadersForRequest:(NSURLRequest *)request;
|
||||
|
||||
@end
|
||||
|
||||
@interface LSApplicationWorkspace : NSObject
|
||||
|
||||
@property (class, readonly) LSApplicationWorkspace *defaultWorkspace;
|
||||
|
||||
- (BOOL)installApplication:(NSURL *)fileURL withOptions:(nullable NSDictionary<NSString *, id> *)options error:(NSError *_Nullable *)error;
|
||||
- (BOOL)uninstallApplication:(NSString *)bundleIdentifier withOptions:(nullable NSDictionary *)options;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -1,22 +0,0 @@
|
||||
<?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>application-identifier</key>
|
||||
<string>6XVY5G3U44.com.rileytestut.AltDaemon</string>
|
||||
<key>get-task-allow</key>
|
||||
<true/>
|
||||
<key>platform-application</key>
|
||||
<true/>
|
||||
<key>com.apple.authkit.client.private</key>
|
||||
<true/>
|
||||
<key>com.apple.private.mobileinstall.allowedSPI</key>
|
||||
<array>
|
||||
<string>Install</string>
|
||||
<string>Uninstall</string>
|
||||
<string>InstallForLaunchServices</string>
|
||||
<string>UninstallForLaunchServices</string>
|
||||
<string>InstallLocalProvisioned</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,65 +0,0 @@
|
||||
//
|
||||
// AnisetteDataManager.swift
|
||||
// AltDaemon
|
||||
//
|
||||
// Created by Riley Testut on 6/1/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import AltSign
|
||||
|
||||
private extension UserDefaults
|
||||
{
|
||||
@objc var localUserID: String? {
|
||||
get { return self.string(forKey: #keyPath(UserDefaults.localUserID)) }
|
||||
set { self.set(newValue, forKey: #keyPath(UserDefaults.localUserID)) }
|
||||
}
|
||||
}
|
||||
|
||||
struct AnisetteDataManager
|
||||
{
|
||||
static let shared = AnisetteDataManager()
|
||||
|
||||
private let dateFormatter = ISO8601DateFormatter()
|
||||
|
||||
private init()
|
||||
{
|
||||
dlopen("/System/Library/PrivateFrameworks/AuthKit.framework/AuthKit", RTLD_NOW);
|
||||
}
|
||||
|
||||
func requestAnisetteData() throws -> ALTAnisetteData
|
||||
{
|
||||
var request = URLRequest(url: URL(string: "https://developerservices2.apple.com/services/QH65B2/listTeams.action?clientId=XABBG36SBA")!)
|
||||
request.httpMethod = "POST"
|
||||
|
||||
let akAppleIDSession = unsafeBitCast(NSClassFromString("AKAppleIDSession")!, to: AKAppleIDSession.Type.self)
|
||||
let akDevice = unsafeBitCast(NSClassFromString("AKDevice")!, to: AKDevice.Type.self)
|
||||
|
||||
let session = akAppleIDSession.init(identifier: "com.apple.gs.xcode.auth")
|
||||
let headers = session.appleIDHeaders(for: request)
|
||||
|
||||
let device = akDevice.current
|
||||
let date = self.dateFormatter.date(from: headers["X-Apple-I-Client-Time"] ?? "") ?? Date()
|
||||
|
||||
var localUserID = UserDefaults.standard.localUserID
|
||||
if localUserID == nil
|
||||
{
|
||||
localUserID = UUID().uuidString
|
||||
UserDefaults.standard.localUserID = localUserID
|
||||
}
|
||||
|
||||
let anisetteData = ALTAnisetteData(machineID: headers["X-Apple-I-MD-M"] ?? "",
|
||||
oneTimePassword: headers["X-Apple-I-MD"] ?? "",
|
||||
localUserID: headers["X-Apple-I-MD-LU"] ?? localUserID ?? "",
|
||||
routingInfo: UInt64(headers["X-Apple-I-MD-RINFO"] ?? "") ?? 0,
|
||||
deviceUniqueIdentifier: device.uniqueDeviceIdentifier,
|
||||
deviceSerialNumber: device.serialNumber,
|
||||
deviceDescription: "<MacBookPro15,1> <Mac OS X;10.15.2;19C57> <com.apple.AuthKit/1 (com.apple.dt.Xcode/3594.4.19)>",
|
||||
date: date,
|
||||
locale: .current,
|
||||
timeZone: .current)
|
||||
return anisetteData
|
||||
}
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
//
|
||||
// AppManager.swift
|
||||
// AltDaemon
|
||||
//
|
||||
// Created by Riley Testut on 6/1/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import AltSign
|
||||
|
||||
private extension URL
|
||||
{
|
||||
static let profilesDirectoryURL = URL(fileURLWithPath: "/var/MobileDevice/ProvisioningProfiles", isDirectory: true)
|
||||
}
|
||||
|
||||
private extension CFNotificationName
|
||||
{
|
||||
static let updatedProvisioningProfiles = CFNotificationName("MISProvisioningProfileRemoved" as CFString)
|
||||
}
|
||||
|
||||
struct AppManager
|
||||
{
|
||||
static let shared = AppManager()
|
||||
|
||||
private let appQueue = DispatchQueue(label: "com.rileytestut.AltDaemon.appQueue", qos: .userInitiated)
|
||||
private let profilesQueue = OperationQueue()
|
||||
|
||||
private let fileCoordinator = NSFileCoordinator()
|
||||
|
||||
private init()
|
||||
{
|
||||
self.profilesQueue.name = "com.rileytestut.AltDaemon.profilesQueue"
|
||||
self.profilesQueue.qualityOfService = .userInitiated
|
||||
}
|
||||
|
||||
func installApp(at fileURL: URL, bundleIdentifier: String, activeProfiles: Set<String>?, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
self.appQueue.async {
|
||||
let lsApplicationWorkspace = unsafeBitCast(NSClassFromString("LSApplicationWorkspace")!, to: LSApplicationWorkspace.Type.self)
|
||||
|
||||
let options = ["CFBundleIdentifier": bundleIdentifier, "AllowInstallLocalProvisioned": NSNumber(value: true)] as [String : Any]
|
||||
let result = Result { try lsApplicationWorkspace.default.installApplication(fileURL, withOptions: options) }
|
||||
|
||||
completionHandler(result)
|
||||
}
|
||||
}
|
||||
|
||||
func removeApp(forBundleIdentifier bundleIdentifier: String, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
self.appQueue.async {
|
||||
let lsApplicationWorkspace = unsafeBitCast(NSClassFromString("LSApplicationWorkspace")!, to: LSApplicationWorkspace.Type.self)
|
||||
lsApplicationWorkspace.default.uninstallApplication(bundleIdentifier, withOptions: nil)
|
||||
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func install(_ profiles: Set<ALTProvisioningProfile>, activeProfiles: Set<String>?, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
let intent = NSFileAccessIntent.writingIntent(with: .profilesDirectoryURL, options: [])
|
||||
self.fileCoordinator.coordinate(with: [intent], queue: self.profilesQueue) { (error) in
|
||||
do
|
||||
{
|
||||
if let error = error
|
||||
{
|
||||
throw error
|
||||
}
|
||||
|
||||
let installingBundleIDs = Set(profiles.map(\.bundleIdentifier))
|
||||
|
||||
let profileURLs = try FileManager.default.contentsOfDirectory(at: intent.url, includingPropertiesForKeys: nil, options: [])
|
||||
|
||||
// Remove all inactive profiles (if active profiles are provided), and the previous profiles.
|
||||
for fileURL in profileURLs
|
||||
{
|
||||
// Use memory mapping to reduce peak memory usage and stay within limit.
|
||||
guard let profile = try? ALTProvisioningProfile(url: fileURL, options: [.mappedIfSafe]) else { continue }
|
||||
|
||||
if installingBundleIDs.contains(profile.bundleIdentifier) || (activeProfiles?.contains(profile.bundleIdentifier) == false && profile.isFreeProvisioningProfile)
|
||||
{
|
||||
try FileManager.default.removeItem(at: fileURL)
|
||||
}
|
||||
else
|
||||
{
|
||||
print("Ignoring:", profile.bundleIdentifier, profile.uuid)
|
||||
}
|
||||
}
|
||||
|
||||
for profile in profiles
|
||||
{
|
||||
let destinationURL = URL.profilesDirectoryURL.appendingPathComponent(profile.uuid.uuidString.lowercased())
|
||||
try profile.data.write(to: destinationURL, options: .atomic)
|
||||
}
|
||||
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
|
||||
// Notify system to prevent accidentally untrusting developer certificate.
|
||||
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), .updatedProvisioningProfiles, nil, nil, true)
|
||||
}
|
||||
}
|
||||
|
||||
func removeProvisioningProfiles(forBundleIdentifiers bundleIdentifiers: Set<String>, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
let intent = NSFileAccessIntent.writingIntent(with: .profilesDirectoryURL, options: [])
|
||||
self.fileCoordinator.coordinate(with: [intent], queue: self.profilesQueue) { (error) in
|
||||
do
|
||||
{
|
||||
let profileURLs = try FileManager.default.contentsOfDirectory(at: intent.url, includingPropertiesForKeys: nil, options: [])
|
||||
|
||||
for fileURL in profileURLs
|
||||
{
|
||||
guard let profile = ALTProvisioningProfile(url: fileURL) else { continue }
|
||||
|
||||
if bundleIdentifiers.contains(profile.bundleIdentifier)
|
||||
{
|
||||
try FileManager.default.removeItem(at: fileURL)
|
||||
}
|
||||
}
|
||||
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
|
||||
// Notify system to prevent accidentally untrusting developer certificate.
|
||||
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), .updatedProvisioningProfiles, nil, nil, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
//
|
||||
// DaemonRequestHandler.swift
|
||||
// AltDaemon
|
||||
//
|
||||
// Created by Riley Testut on 6/1/20.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
typealias DaemonConnectionManager = ConnectionManager<DaemonRequestHandler>
|
||||
|
||||
private let connectionManager = ConnectionManager(requestHandler: DaemonRequestHandler(),
|
||||
connectionHandlers: [XPCConnectionHandler()])
|
||||
|
||||
extension DaemonConnectionManager
|
||||
{
|
||||
static var shared: ConnectionManager {
|
||||
return connectionManager
|
||||
}
|
||||
}
|
||||
|
||||
struct DaemonRequestHandler: RequestHandler
|
||||
{
|
||||
func handleAnisetteDataRequest(_ request: AnisetteDataRequest, for connection: Connection, completionHandler: @escaping (Result<AnisetteDataResponse, Error>) -> Void)
|
||||
{
|
||||
do
|
||||
{
|
||||
let anisetteData = try AnisetteDataManager.shared.requestAnisetteData()
|
||||
|
||||
let response = AnisetteDataResponse(anisetteData: anisetteData)
|
||||
completionHandler(.success(response))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
func handlePrepareAppRequest(_ request: PrepareAppRequest, for connection: Connection, completionHandler: @escaping (Result<InstallationProgressResponse, Error>) -> Void)
|
||||
{
|
||||
guard let fileURL = request.fileURL else { return completionHandler(.failure(ALTServerError(.invalidRequest))) }
|
||||
|
||||
print("Awaiting begin installation request...")
|
||||
|
||||
connection.receiveRequest() { (result) in
|
||||
print("Received begin installation request with result:", result)
|
||||
|
||||
do
|
||||
{
|
||||
guard case .beginInstallation(let request) = try result.get() else { throw ALTServerError(.unknownRequest) }
|
||||
guard let bundleIdentifier = request.bundleIdentifier else { throw ALTServerError(.invalidRequest) }
|
||||
|
||||
AppManager.shared.installApp(at: fileURL, bundleIdentifier: bundleIdentifier, activeProfiles: request.activeProfiles) { (result) in
|
||||
let result = result.map { InstallationProgressResponse(progress: 1.0) }
|
||||
print("Installed app with result:", result)
|
||||
|
||||
completionHandler(result)
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleInstallProvisioningProfilesRequest(_ request: InstallProvisioningProfilesRequest, for connection: Connection,
|
||||
completionHandler: @escaping (Result<InstallProvisioningProfilesResponse, Error>) -> Void)
|
||||
{
|
||||
AppManager.shared.install(request.provisioningProfiles, activeProfiles: request.activeProfiles) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error):
|
||||
print("Failed to install profiles \(request.provisioningProfiles.map { $0.bundleIdentifier }):", error)
|
||||
completionHandler(.failure(error))
|
||||
|
||||
case .success:
|
||||
print("Installed profiles:", request.provisioningProfiles.map { $0.bundleIdentifier })
|
||||
|
||||
let response = InstallProvisioningProfilesResponse()
|
||||
completionHandler(.success(response))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleRemoveProvisioningProfilesRequest(_ request: RemoveProvisioningProfilesRequest, for connection: Connection,
|
||||
completionHandler: @escaping (Result<RemoveProvisioningProfilesResponse, Error>) -> Void)
|
||||
{
|
||||
AppManager.shared.removeProvisioningProfiles(forBundleIdentifiers: request.bundleIdentifiers) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error):
|
||||
print("Failed to remove profiles \(request.bundleIdentifiers):", error)
|
||||
completionHandler(.failure(error))
|
||||
|
||||
case .success:
|
||||
print("Removed profiles:", request.bundleIdentifiers)
|
||||
|
||||
let response = RemoveProvisioningProfilesResponse()
|
||||
completionHandler(.success(response))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleRemoveAppRequest(_ request: RemoveAppRequest, for connection: Connection, completionHandler: @escaping (Result<RemoveAppResponse, Error>) -> Void)
|
||||
{
|
||||
AppManager.shared.removeApp(forBundleIdentifier: request.bundleIdentifier) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error):
|
||||
print("Failed to remove app \(request.bundleIdentifier):", error)
|
||||
completionHandler(.failure(error))
|
||||
|
||||
case .success:
|
||||
print("Removed app:", request.bundleIdentifier)
|
||||
|
||||
let response = RemoveAppResponse()
|
||||
completionHandler(.success(response))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
//
|
||||
// XPCConnectionHandler.swift
|
||||
// AltDaemon
|
||||
//
|
||||
// Created by Riley Testut on 9/14/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Security
|
||||
|
||||
class XPCConnectionHandler: NSObject, ConnectionHandler
|
||||
{
|
||||
var connectionHandler: ((Connection) -> Void)?
|
||||
var disconnectionHandler: ((Connection) -> Void)?
|
||||
|
||||
private let dispatchQueue = DispatchQueue(label: "io.altstore.XPCConnectionListener", qos: .utility)
|
||||
private let listeners = XPCConnection.machServiceNames.map { NSXPCListener.makeListener(machServiceName: $0) }
|
||||
|
||||
deinit
|
||||
{
|
||||
self.stopListening()
|
||||
}
|
||||
|
||||
func startListening()
|
||||
{
|
||||
for listener in self.listeners
|
||||
{
|
||||
listener.delegate = self
|
||||
listener.resume()
|
||||
}
|
||||
}
|
||||
|
||||
func stopListening()
|
||||
{
|
||||
self.listeners.forEach { $0.suspend() }
|
||||
}
|
||||
}
|
||||
|
||||
private extension XPCConnectionHandler
|
||||
{
|
||||
func disconnect(_ connection: Connection)
|
||||
{
|
||||
connection.disconnect()
|
||||
|
||||
self.disconnectionHandler?(connection)
|
||||
}
|
||||
}
|
||||
|
||||
extension XPCConnectionHandler: NSXPCListenerDelegate
|
||||
{
|
||||
func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool
|
||||
{
|
||||
let maximumPathLength = 4 * UInt32(MAXPATHLEN)
|
||||
|
||||
let pathBuffer = UnsafeMutablePointer<CChar>.allocate(capacity: Int(maximumPathLength))
|
||||
defer { pathBuffer.deallocate() }
|
||||
|
||||
proc_pidpath(newConnection.processIdentifier, pathBuffer, maximumPathLength)
|
||||
|
||||
let path = String(cString: pathBuffer)
|
||||
let fileURL = URL(fileURLWithPath: path)
|
||||
|
||||
var code: UnsafeMutableRawPointer?
|
||||
defer { code.map { Unmanaged<AnyObject>.fromOpaque($0).release() } }
|
||||
|
||||
var status = SecStaticCodeCreateWithPath(fileURL as CFURL, 0, &code)
|
||||
guard status == 0 else { return false }
|
||||
|
||||
var signingInfo: CFDictionary?
|
||||
defer { signingInfo.map { Unmanaged<AnyObject>.passUnretained($0).release() } }
|
||||
|
||||
status = SecCodeCopySigningInformation(code, kSecCSInternalInformation | kSecCSSigningInformation, &signingInfo)
|
||||
guard status == 0 else { return false }
|
||||
|
||||
// Only accept connections from AltStore.
|
||||
guard
|
||||
let codeSigningInfo = signingInfo as? [String: Any],
|
||||
let bundleIdentifier = codeSigningInfo["identifier"] as? String,
|
||||
bundleIdentifier.contains("com.rileytestut.AltStore")
|
||||
else { return false }
|
||||
|
||||
let connection = XPCConnection(newConnection)
|
||||
newConnection.invalidationHandler = { [weak self, weak connection] in
|
||||
guard let self = self, let connection = connection else { return }
|
||||
self.disconnect(connection)
|
||||
}
|
||||
|
||||
self.connectionHandler?(connection)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
//
|
||||
// main.swift
|
||||
// AltDaemon
|
||||
//
|
||||
// Created by Riley Testut on 6/2/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
autoreleasepool {
|
||||
DaemonConnectionManager.shared.start()
|
||||
RunLoop.current.run()
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
Package: com.rileytestut.altdaemon
|
||||
Name: AltDaemon
|
||||
Depends:
|
||||
Version: 1.0
|
||||
Architecture: iphoneos-arm
|
||||
Description: AltDaemon allows AltStore to install and refresh apps without a computer.
|
||||
Maintainer: Riley Testut
|
||||
Author: Riley Testut
|
||||
Homepage: https://altstore.io
|
||||
Section: System
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/bin/sh
|
||||
launchctl load /Library/LaunchDaemons/com.rileytestut.altdaemon.plist
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/bin/sh
|
||||
launchctl unload /Library/LaunchDaemons/com.rileytestut.altdaemon.plist >> /dev/null 2>&1
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/bin/sh
|
||||
launchctl unload /Library/LaunchDaemons/com.rileytestut.altdaemon.plist
|
||||
@@ -1,28 +0,0 @@
|
||||
<?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>Label</key>
|
||||
<string>com.rileytestut.altdaemon</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/usr/bin/env</string>
|
||||
<string>_MSSafeMode=1</string>
|
||||
<string>_SafeMode=1</string>
|
||||
<string>/usr/bin/AltDaemon</string>
|
||||
</array>
|
||||
<key>UserName</key>
|
||||
<string>mobile</string>
|
||||
<key>KeepAlive</key>
|
||||
<false/>
|
||||
<key>RunAtLoad</key>
|
||||
<false/>
|
||||
<key>MachServices</key>
|
||||
<dict>
|
||||
<key>cy:io.altstore.altdaemon</key>
|
||||
<true/>
|
||||
<key>lh:io.altstore.altdaemon</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,23 +0,0 @@
|
||||
//
|
||||
// ALTPluginService.h
|
||||
// AltPlugin
|
||||
//
|
||||
// Created by Riley Testut on 11/14/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class ALTAnisetteData;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface ALTPluginService : NSObject
|
||||
|
||||
@property (class, nonatomic, readonly) ALTPluginService *sharedService;
|
||||
|
||||
- (ALTAnisetteData *)requestAnisetteData;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -1,105 +0,0 @@
|
||||
//
|
||||
// ALTPluginService.m
|
||||
// AltPlugin
|
||||
//
|
||||
// Created by Riley Testut on 11/14/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ALTPluginService.h"
|
||||
|
||||
#import <dlfcn.h>
|
||||
|
||||
#import "ALTAnisetteData.h"
|
||||
|
||||
@import AppKit;
|
||||
|
||||
@interface AKAppleIDSession : NSObject
|
||||
- (id)appleIDHeadersForRequest:(id)arg1;
|
||||
@end
|
||||
|
||||
@interface AKDevice
|
||||
+ (AKDevice *)currentDevice;
|
||||
- (NSString *)uniqueDeviceIdentifier;
|
||||
- (NSString *)serialNumber;
|
||||
- (NSString *)serverFriendlyDescription;
|
||||
@end
|
||||
|
||||
@interface ALTPluginService ()
|
||||
|
||||
@property (nonatomic, readonly) NSISO8601DateFormatter *dateFormatter;
|
||||
|
||||
@end
|
||||
|
||||
@implementation ALTPluginService
|
||||
|
||||
+ (instancetype)sharedService
|
||||
{
|
||||
static ALTPluginService *_service = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
_service = [[self alloc] init];
|
||||
});
|
||||
|
||||
return _service;
|
||||
}
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
if (self)
|
||||
{
|
||||
_dateFormatter = [[NSISO8601DateFormatter alloc] init];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (void)initialize
|
||||
{
|
||||
[[ALTPluginService sharedService] start];
|
||||
}
|
||||
|
||||
- (void)start
|
||||
{
|
||||
dlopen("/System/Library/PrivateFrameworks/AuthKit.framework/AuthKit", RTLD_NOW);
|
||||
|
||||
[[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(receiveNotification:) name:@"com.rileytestut.AltServer.FetchAnisetteData" object:nil];
|
||||
}
|
||||
|
||||
- (ALTAnisetteData *)requestAnisetteData
|
||||
{
|
||||
NSMutableURLRequest* req = [[NSMutableURLRequest alloc] initWithURL:[[NSURL alloc] initWithString:@"https://developerservices2.apple.com/services/QH65B2/listTeams.action?clientId=XABBG36SBA"]];
|
||||
[req setHTTPMethod:@"POST"];
|
||||
|
||||
AKAppleIDSession *session = [[NSClassFromString(@"AKAppleIDSession") alloc] initWithIdentifier:@"com.apple.gs.xcode.auth"];
|
||||
NSDictionary *headers = [session appleIDHeadersForRequest:req];
|
||||
|
||||
AKDevice *device = [NSClassFromString(@"AKDevice") currentDevice];
|
||||
NSDate *date = [self.dateFormatter dateFromString:headers[@"X-Apple-I-Client-Time"]];
|
||||
|
||||
ALTAnisetteData *anisetteData = [[NSClassFromString(@"ALTAnisetteData") alloc] initWithMachineID:headers[@"X-Apple-I-MD-M"]
|
||||
oneTimePassword:headers[@"X-Apple-I-MD"]
|
||||
localUserID:headers[@"X-Apple-I-MD-LU"]
|
||||
routingInfo:[headers[@"X-Apple-I-MD-RINFO"] longLongValue]
|
||||
deviceUniqueIdentifier:device.uniqueDeviceIdentifier
|
||||
deviceSerialNumber:device.serialNumber
|
||||
deviceDescription:device.serverFriendlyDescription
|
||||
date:date
|
||||
locale:[NSLocale currentLocale]
|
||||
timeZone:[NSTimeZone localTimeZone]];
|
||||
|
||||
return anisetteData;
|
||||
}
|
||||
|
||||
- (void)receiveNotification:(NSNotification *)notification
|
||||
{
|
||||
NSString *requestUUID = notification.userInfo[@"requestUUID"];
|
||||
|
||||
ALTAnisetteData *anisetteData = [self requestAnisetteData];
|
||||
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:anisetteData requiringSecureCoding:YES error:nil];
|
||||
|
||||
[[NSDistributedNotificationCenter defaultCenter] postNotificationName:@"com.rileytestut.AltServer.AnisetteDataResponse" object:nil userInfo:@{@"requestUUID": requestUUID, @"anisetteData": data} deliverImmediately:YES];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,78 +0,0 @@
|
||||
<?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>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2019 Riley Testut. All rights reserved.</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>ALTPluginService</string>
|
||||
<key>Supported10.14PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string># UUIDs for versions from 10.12 to 99.99.99</string>
|
||||
<string># For mail version 10.0 (3226) on OS X Version 10.12 (build 16A319)</string>
|
||||
<string>36CCB8BB-2207-455E-89BC-B9D6E47ABB5B</string>
|
||||
<string># For mail version 10.1 (3251) on OS X Version 10.12.1 (build 16B2553a)</string>
|
||||
<string>9054AFD9-2607-489E-8E63-8B09A749BC61</string>
|
||||
<string># For mail version 10.2 (3259) on OS X Version 10.12.2 (build 16D12b)</string>
|
||||
<string>1CD3B36A-0E3B-4A26-8F7E-5BDF96AAC97E</string>
|
||||
<string># For mail version 10.3 (3273) on OS X Version 10.12.4 (build 16G1036)</string>
|
||||
<string>21560BD9-A3CC-482E-9B99-95B7BF61EDC1</string>
|
||||
<string># For mail version 11.0 (3441.0.1) on OS X Version 10.13 (build 17A315i)</string>
|
||||
<string>C86CD990-4660-4E36-8CDA-7454DEB2E199</string>
|
||||
<string># For mail version 12.0 (3445.100.39) on OS X Version 10.14.1 (build 18B45d)</string>
|
||||
<string>A4343FAF-AE18-40D0-8A16-DFAE481AF9C1</string>
|
||||
<string># For mail version 13.0 (3594.4.2) on OS X Version 10.15 (build 19A558d)</string>
|
||||
<string>6EEA38FB-1A0B-469B-BB35-4C2E0EEA9053</string>
|
||||
</array>
|
||||
<key>Supported10.15PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string># UUIDs for versions from 10.12 to 99.99.99</string>
|
||||
<string># For mail version 10.0 (3226) on OS X Version 10.12 (build 16A319)</string>
|
||||
<string>36CCB8BB-2207-455E-89BC-B9D6E47ABB5B</string>
|
||||
<string># For mail version 10.1 (3251) on OS X Version 10.12.1 (build 16B2553a)</string>
|
||||
<string>9054AFD9-2607-489E-8E63-8B09A749BC61</string>
|
||||
<string># For mail version 10.2 (3259) on OS X Version 10.12.2 (build 16D12b)</string>
|
||||
<string>1CD3B36A-0E3B-4A26-8F7E-5BDF96AAC97E</string>
|
||||
<string># For mail version 10.3 (3273) on OS X Version 10.12.4 (build 16G1036)</string>
|
||||
<string>21560BD9-A3CC-482E-9B99-95B7BF61EDC1</string>
|
||||
<string># For mail version 11.0 (3441.0.1) on OS X Version 10.13 (build 17A315i)</string>
|
||||
<string>C86CD990-4660-4E36-8CDA-7454DEB2E199</string>
|
||||
<string># For mail version 12.0 (3445.100.39) on OS X Version 10.14.1 (build 18B45d)</string>
|
||||
<string>A4343FAF-AE18-40D0-8A16-DFAE481AF9C1</string>
|
||||
<string># For mail version 13.0 (3594.4.2) on OS X Version 10.15 (build 19A558d)</string>
|
||||
<string>6EEA38FB-1A0B-469B-BB35-4C2E0EEA9053</string>
|
||||
</array>
|
||||
<key>Supported11.0PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
|
||||
</array>
|
||||
<key>Supported11.1PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
|
||||
</array>
|
||||
<key>Supported11.2PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
|
||||
</array>
|
||||
<key>Supported11.3PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,14 +0,0 @@
|
||||
//
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
//
|
||||
|
||||
#import "ALTDeviceManager.h"
|
||||
#import "ALTWiredConnection.h"
|
||||
#import "ALTNotificationConnection.h"
|
||||
|
||||
// Shared
|
||||
#import "ALTConstants.h"
|
||||
#import "ALTConnection.h"
|
||||
#import "AltXPCProtocol.h"
|
||||
#import "NSError+ALTServerError.h"
|
||||
#import "CFNotificationName+AltStore.h"
|
||||
@@ -1,5 +0,0 @@
|
||||
<?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/>
|
||||
</plist>
|
||||
@@ -1,146 +0,0 @@
|
||||
//
|
||||
// AnisetteDataManager.swift
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 11/16/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
private extension Bundle
|
||||
{
|
||||
struct ID
|
||||
{
|
||||
static let mail = "com.apple.mail"
|
||||
static let altXPC = "com.rileytestut.AltXPC"
|
||||
}
|
||||
}
|
||||
|
||||
private extension ALTAnisetteData
|
||||
{
|
||||
func sanitize(byReplacingBundleID bundleID: String)
|
||||
{
|
||||
guard let range = self.deviceDescription.lowercased().range(of: "(" + bundleID.lowercased()) else { return }
|
||||
|
||||
var adjustedDescription = self.deviceDescription[..<range.lowerBound]
|
||||
adjustedDescription += "(com.apple.dt.Xcode/3594.4.19)>"
|
||||
|
||||
self.deviceDescription = String(adjustedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
class AnisetteDataManager: NSObject
|
||||
{
|
||||
static let shared = AnisetteDataManager()
|
||||
|
||||
private var anisetteDataCompletionHandlers: [String: (Result<ALTAnisetteData, Error>) -> Void] = [:]
|
||||
private var anisetteDataTimers: [String: Timer] = [:]
|
||||
|
||||
private lazy var xpcConnection: NSXPCConnection = {
|
||||
let connection = NSXPCConnection(serviceName: Bundle.ID.altXPC)
|
||||
connection.remoteObjectInterface = NSXPCInterface(with: AltXPCProtocol.self)
|
||||
connection.resume()
|
||||
return connection
|
||||
}()
|
||||
|
||||
private override init()
|
||||
{
|
||||
super.init()
|
||||
|
||||
DistributedNotificationCenter.default().addObserver(self, selector: #selector(AnisetteDataManager.handleAnisetteDataResponse(_:)), name: Notification.Name("com.rileytestut.AltServer.AnisetteDataResponse"), object: nil)
|
||||
}
|
||||
|
||||
func requestAnisetteData(_ completion: @escaping (Result<ALTAnisetteData, Error>) -> Void)
|
||||
{
|
||||
self.requestAnisetteDataFromXPCService { (result) in
|
||||
do
|
||||
{
|
||||
let anisetteData = try result.get()
|
||||
completion(.success(anisetteData))
|
||||
}
|
||||
catch CocoaError.xpcConnectionInterrupted
|
||||
{
|
||||
// SIP and/or AMFI are not disabled, so fall back to Mail plug-in.
|
||||
self.requestAnisetteDataFromPlugin { (result) in
|
||||
completion(result)
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isXPCAvailable(completion: @escaping (Bool) -> Void)
|
||||
{
|
||||
guard let proxy = self.xpcConnection.remoteObjectProxyWithErrorHandler({ (error) in
|
||||
completion(false)
|
||||
}) as? AltXPCProtocol else { return }
|
||||
|
||||
proxy.ping {
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension AnisetteDataManager
|
||||
{
|
||||
func requestAnisetteDataFromXPCService(completion: @escaping (Result<ALTAnisetteData, Error>) -> Void)
|
||||
{
|
||||
guard let proxy = self.xpcConnection.remoteObjectProxyWithErrorHandler({ (error) in
|
||||
print("Anisette XPC Error:", error)
|
||||
completion(.failure(error))
|
||||
}) as? AltXPCProtocol else { return }
|
||||
|
||||
proxy.requestAnisetteData { (anisetteData, error) in
|
||||
anisetteData?.sanitize(byReplacingBundleID: Bundle.ID.altXPC)
|
||||
completion(Result(anisetteData, error))
|
||||
}
|
||||
}
|
||||
|
||||
func requestAnisetteDataFromPlugin(completion: @escaping (Result<ALTAnisetteData, Error>) -> Void)
|
||||
{
|
||||
let requestUUID = UUID().uuidString
|
||||
self.anisetteDataCompletionHandlers[requestUUID] = completion
|
||||
|
||||
let timer = Timer(timeInterval: 1.0, repeats: false) { (timer) in
|
||||
self.finishRequest(forUUID: requestUUID, result: .failure(ALTServerError(.pluginNotFound)))
|
||||
}
|
||||
self.anisetteDataTimers[requestUUID] = timer
|
||||
|
||||
RunLoop.main.add(timer, forMode: .default)
|
||||
|
||||
DistributedNotificationCenter.default().postNotificationName(Notification.Name("com.rileytestut.AltServer.FetchAnisetteData"), object: nil, userInfo: ["requestUUID": requestUUID], options: .deliverImmediately)
|
||||
}
|
||||
|
||||
@objc func handleAnisetteDataResponse(_ notification: Notification)
|
||||
{
|
||||
guard let userInfo = notification.userInfo, let requestUUID = userInfo["requestUUID"] as? String else { return }
|
||||
|
||||
if
|
||||
let archivedAnisetteData = userInfo["anisetteData"] as? Data,
|
||||
let anisetteData = try? NSKeyedUnarchiver.unarchivedObject(ofClass: ALTAnisetteData.self, from: archivedAnisetteData)
|
||||
{
|
||||
anisetteData.sanitize(byReplacingBundleID: Bundle.ID.mail)
|
||||
self.finishRequest(forUUID: requestUUID, result: .success(anisetteData))
|
||||
}
|
||||
else
|
||||
{
|
||||
self.finishRequest(forUUID: requestUUID, result: .failure(ALTServerError(.invalidAnisetteData)))
|
||||
}
|
||||
}
|
||||
|
||||
func finishRequest(forUUID requestUUID: String, result: Result<ALTAnisetteData, Error>)
|
||||
{
|
||||
let completionHandler = self.anisetteDataCompletionHandlers[requestUUID]
|
||||
self.anisetteDataCompletionHandlers[requestUUID] = nil
|
||||
|
||||
let timer = self.anisetteDataTimers[requestUUID]
|
||||
self.anisetteDataTimers[requestUUID] = nil
|
||||
|
||||
timer?.invalidate()
|
||||
completionHandler?(result)
|
||||
}
|
||||
}
|
||||
@@ -1,397 +0,0 @@
|
||||
//
|
||||
// AppDelegate.swift
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 5/24/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import UserNotifications
|
||||
|
||||
import AltSign
|
||||
|
||||
import LaunchAtLogin
|
||||
|
||||
#if STAGING
|
||||
private let altstoreAppURL = URL(string: "https://f000.backblazeb2.com/file/altstore-staging/altstore.ipa")!
|
||||
#elseif BETA
|
||||
private let altstoreAppURL = URL(string: "https://f000.backblazeb2.com/file/altstore/altstore-beta.ipa")!
|
||||
#else
|
||||
private let altstoreAppURL = URL(string: "https://f000.backblazeb2.com/file/altstore/altstore.ipa")!
|
||||
#endif
|
||||
|
||||
@NSApplicationMain
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
private let pluginManager = PluginManager()
|
||||
|
||||
private var statusItem: NSStatusItem?
|
||||
|
||||
private var connectedDevices = [ALTDevice]()
|
||||
|
||||
private weak var authenticationAlert: NSAlert?
|
||||
|
||||
@IBOutlet private var appMenu: NSMenu!
|
||||
@IBOutlet private var connectedDevicesMenu: NSMenu!
|
||||
@IBOutlet private var sideloadIPAConnectedDevicesMenu: NSMenu!
|
||||
@IBOutlet private var launchAtLoginMenuItem: NSMenuItem!
|
||||
@IBOutlet private var installMailPluginMenuItem: NSMenuItem!
|
||||
|
||||
private weak var authenticationAppleIDTextField: NSTextField?
|
||||
private weak var authenticationPasswordTextField: NSSecureTextField?
|
||||
|
||||
func applicationDidFinishLaunching(_ aNotification: Notification)
|
||||
{
|
||||
UserDefaults.standard.registerDefaults()
|
||||
|
||||
UNUserNotificationCenter.current().delegate = self
|
||||
|
||||
ServerConnectionManager.shared.start()
|
||||
ALTDeviceManager.shared.start()
|
||||
|
||||
let item = NSStatusBar.system.statusItem(withLength: -1)
|
||||
item.menu = self.appMenu
|
||||
item.button?.image = NSImage(named: "MenuBarIcon")
|
||||
self.statusItem = item
|
||||
|
||||
self.appMenu.delegate = self
|
||||
self.connectedDevicesMenu.delegate = self
|
||||
self.sideloadIPAConnectedDevicesMenu.delegate = self
|
||||
|
||||
UNUserNotificationCenter.current().requestAuthorization(options: [.alert]) { (success, error) in
|
||||
guard success else { return }
|
||||
|
||||
if !UserDefaults.standard.didPresentInitialNotification
|
||||
{
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = NSLocalizedString("AltServer Running", comment: "")
|
||||
content.body = NSLocalizedString("AltServer runs in the background as a menu bar app listening for AltStore.", comment: "")
|
||||
|
||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
||||
UNUserNotificationCenter.current().add(request)
|
||||
|
||||
UserDefaults.standard.didPresentInitialNotification = true
|
||||
}
|
||||
}
|
||||
|
||||
if self.pluginManager.isUpdateAvailable
|
||||
{
|
||||
self.installMailPlugin()
|
||||
}
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ aNotification: Notification)
|
||||
{
|
||||
// Insert code here to tear down your application
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppDelegate
|
||||
{
|
||||
@objc func installAltStore(_ item: NSMenuItem)
|
||||
{
|
||||
guard let index = item.menu?.index(of: item), index != -1 else { return }
|
||||
|
||||
let device = self.connectedDevices[index]
|
||||
self.installApplication(at: altstoreAppURL, to: device)
|
||||
}
|
||||
|
||||
@objc func sideloadIPA(_ item: NSMenuItem)
|
||||
{
|
||||
guard let index = item.menu?.index(of: item), index != -1 else { return }
|
||||
|
||||
let device = self.connectedDevices[index]
|
||||
|
||||
let openPanel = NSOpenPanel()
|
||||
openPanel.canChooseDirectories = false
|
||||
openPanel.allowsMultipleSelection = false
|
||||
openPanel.allowedFileTypes = ["ipa"]
|
||||
openPanel.begin { (response) in
|
||||
guard let fileURL = openPanel.url, response == .OK else { return }
|
||||
self.installApplication(at: fileURL, to: device)
|
||||
}
|
||||
}
|
||||
|
||||
func installApplication(at url: URL, to device: ALTDevice)
|
||||
{
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("Please enter your Apple ID and password.", comment: "")
|
||||
alert.informativeText = NSLocalizedString("Your Apple ID and password are not saved and are only sent to Apple for authentication.", comment: "")
|
||||
|
||||
let textFieldSize = NSSize(width: 300, height: 22)
|
||||
|
||||
let appleIDTextField = NSTextField(frame: NSRect(x: 0, y: 0, width: textFieldSize.width, height: textFieldSize.height))
|
||||
appleIDTextField.delegate = self
|
||||
appleIDTextField.translatesAutoresizingMaskIntoConstraints = false
|
||||
appleIDTextField.placeholderString = NSLocalizedString("Apple ID", comment: "")
|
||||
alert.window.initialFirstResponder = appleIDTextField
|
||||
self.authenticationAppleIDTextField = appleIDTextField
|
||||
|
||||
let passwordTextField = NSSecureTextField(frame: NSRect(x: 0, y: 0, width: textFieldSize.width, height: textFieldSize.height))
|
||||
passwordTextField.delegate = self
|
||||
passwordTextField.translatesAutoresizingMaskIntoConstraints = false
|
||||
passwordTextField.placeholderString = NSLocalizedString("Password", comment: "")
|
||||
self.authenticationPasswordTextField = passwordTextField
|
||||
|
||||
appleIDTextField.nextKeyView = passwordTextField
|
||||
|
||||
let stackView = NSStackView(frame: NSRect(x: 0, y: 0, width: textFieldSize.width, height: textFieldSize.height * 2))
|
||||
stackView.orientation = .vertical
|
||||
stackView.distribution = .equalSpacing
|
||||
stackView.spacing = 0
|
||||
stackView.addArrangedSubview(appleIDTextField)
|
||||
stackView.addArrangedSubview(passwordTextField)
|
||||
alert.accessoryView = stackView
|
||||
|
||||
alert.addButton(withTitle: NSLocalizedString("Install", comment: ""))
|
||||
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
|
||||
|
||||
self.authenticationAlert = alert
|
||||
self.validate()
|
||||
|
||||
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
|
||||
|
||||
let response = alert.runModal()
|
||||
guard response == .alertFirstButtonReturn else { return }
|
||||
|
||||
let username = appleIDTextField.stringValue
|
||||
let password = passwordTextField.stringValue
|
||||
|
||||
func install()
|
||||
{
|
||||
ALTDeviceManager.shared.installApplication(at: url, to: device, appleID: username, password: password) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .success(let application):
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = NSLocalizedString("Installation Succeeded", comment: "")
|
||||
content.body = String(format: NSLocalizedString("%@ was successfully installed on %@.", comment: ""), application.name, device.name)
|
||||
|
||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
||||
UNUserNotificationCenter.current().add(request)
|
||||
|
||||
case .failure(InstallError.cancelled), .failure(ALTAppleAPIError.requiresTwoFactorAuthentication):
|
||||
// Ignore
|
||||
break
|
||||
|
||||
case .failure(let error as NSError):
|
||||
|
||||
let alert = NSAlert()
|
||||
alert.alertStyle = .critical
|
||||
alert.messageText = NSLocalizedString("Installation Failed", comment: "")
|
||||
|
||||
if let underlyingError = error.userInfo[NSUnderlyingErrorKey] as? Error
|
||||
{
|
||||
alert.informativeText = underlyingError.localizedDescription
|
||||
}
|
||||
else if let recoverySuggestion = error.localizedRecoverySuggestion
|
||||
{
|
||||
alert.informativeText = error.localizedDescription + "\n\n" + recoverySuggestion
|
||||
}
|
||||
else
|
||||
{
|
||||
alert.informativeText = error.localizedDescription
|
||||
}
|
||||
|
||||
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
|
||||
|
||||
alert.runModal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !self.pluginManager.isMailPluginInstalled || self.pluginManager.isUpdateAvailable
|
||||
{
|
||||
AnisetteDataManager.shared.isXPCAvailable { (isAvailable) in
|
||||
if isAvailable
|
||||
{
|
||||
// XPC service is available, so we don't need to install/update Mail plug-in.
|
||||
// Users can still manually do so from the AltServer menu.
|
||||
install()
|
||||
}
|
||||
else
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
self.installMailPlugin { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure: break
|
||||
case .success: install()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
install()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func toggleLaunchAtLogin(_ item: NSMenuItem)
|
||||
{
|
||||
LaunchAtLogin.isEnabled.toggle()
|
||||
}
|
||||
|
||||
@objc func handleInstallMailPluginMenuItem(_ item: NSMenuItem)
|
||||
{
|
||||
if !self.pluginManager.isMailPluginInstalled || self.pluginManager.isUpdateAvailable
|
||||
{
|
||||
self.installMailPlugin()
|
||||
}
|
||||
else
|
||||
{
|
||||
self.uninstallMailPlugin()
|
||||
}
|
||||
}
|
||||
|
||||
private func installMailPlugin(completion: ((Result<Void, Error>) -> Void)? = nil)
|
||||
{
|
||||
self.pluginManager.installMailPlugin { (result) in
|
||||
DispatchQueue.main.async {
|
||||
switch result
|
||||
{
|
||||
case .failure(PluginError.cancelled): break
|
||||
case .failure(let error):
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("Failed to Install Mail Plug-in", comment: "")
|
||||
alert.informativeText = error.localizedDescription
|
||||
alert.runModal()
|
||||
|
||||
case .success:
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("Mail Plug-in Installed", comment: "")
|
||||
alert.informativeText = NSLocalizedString("Please restart Mail and enable AltPlugin in Mail's Preferences. Mail must be running when installing or refreshing apps with AltServer.", comment: "")
|
||||
alert.runModal()
|
||||
}
|
||||
|
||||
completion?(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func uninstallMailPlugin()
|
||||
{
|
||||
self.pluginManager.uninstallMailPlugin { (result) in
|
||||
DispatchQueue.main.async {
|
||||
switch result
|
||||
{
|
||||
case .failure(PluginError.cancelled): break
|
||||
case .failure(let error):
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("Failed to Uninstall Mail Plug-in", comment: "")
|
||||
alert.informativeText = error.localizedDescription
|
||||
alert.runModal()
|
||||
|
||||
case .success:
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("Mail Plug-in Uninstalled", comment: "")
|
||||
alert.informativeText = NSLocalizedString("Please restart Mail for changes to take effect. You will not be able to use AltServer until the plug-in is reinstalled.", comment: "")
|
||||
alert.runModal()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AppDelegate: NSMenuDelegate
|
||||
{
|
||||
func menuWillOpen(_ menu: NSMenu)
|
||||
{
|
||||
guard menu == self.appMenu else { return }
|
||||
|
||||
self.connectedDevices = ALTDeviceManager.shared.availableDevices
|
||||
|
||||
self.launchAtLoginMenuItem.target = self
|
||||
self.launchAtLoginMenuItem.action = #selector(AppDelegate.toggleLaunchAtLogin(_:))
|
||||
self.launchAtLoginMenuItem.state = LaunchAtLogin.isEnabled ? .on : .off
|
||||
|
||||
if self.pluginManager.isUpdateAvailable
|
||||
{
|
||||
self.installMailPluginMenuItem.title = NSLocalizedString("Update Mail Plug-in", comment: "")
|
||||
}
|
||||
else if self.pluginManager.isMailPluginInstalled
|
||||
{
|
||||
self.installMailPluginMenuItem.title = NSLocalizedString("Uninstall Mail Plug-in", comment: "")
|
||||
}
|
||||
else
|
||||
{
|
||||
self.installMailPluginMenuItem.title = NSLocalizedString("Install Mail Plug-in", comment: "")
|
||||
}
|
||||
self.installMailPluginMenuItem.target = self
|
||||
self.installMailPluginMenuItem.action = #selector(AppDelegate.handleInstallMailPluginMenuItem(_:))
|
||||
}
|
||||
|
||||
func numberOfItems(in menu: NSMenu) -> Int
|
||||
{
|
||||
guard menu == self.connectedDevicesMenu || menu == self.sideloadIPAConnectedDevicesMenu else { return -1 }
|
||||
|
||||
return self.connectedDevices.isEmpty ? 1 : self.connectedDevices.count
|
||||
}
|
||||
|
||||
func menu(_ menu: NSMenu, update item: NSMenuItem, at index: Int, shouldCancel: Bool) -> Bool
|
||||
{
|
||||
guard menu == self.connectedDevicesMenu || menu == self.sideloadIPAConnectedDevicesMenu else { return false }
|
||||
|
||||
if self.connectedDevices.isEmpty
|
||||
{
|
||||
item.title = NSLocalizedString("No Connected Devices", comment: "")
|
||||
item.isEnabled = false
|
||||
item.target = nil
|
||||
item.action = nil
|
||||
}
|
||||
else
|
||||
{
|
||||
let device = self.connectedDevices[index]
|
||||
item.title = device.name
|
||||
item.isEnabled = true
|
||||
item.target = self
|
||||
item.action = (menu == self.connectedDevicesMenu) ? #selector(AppDelegate.installAltStore(_:)) : #selector(AppDelegate.sideloadIPA(_:))
|
||||
item.tag = index
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension AppDelegate: NSTextFieldDelegate
|
||||
{
|
||||
func controlTextDidChange(_ obj: Notification)
|
||||
{
|
||||
self.validate()
|
||||
}
|
||||
|
||||
func controlTextDidEndEditing(_ obj: Notification)
|
||||
{
|
||||
self.validate()
|
||||
}
|
||||
|
||||
private func validate()
|
||||
{
|
||||
guard
|
||||
let appleID = self.authenticationAppleIDTextField?.stringValue.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
let password = self.authenticationPasswordTextField?.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
else { return }
|
||||
|
||||
if appleID.isEmpty || password.isEmpty
|
||||
{
|
||||
self.authenticationAlert?.buttons.first?.isEnabled = false
|
||||
}
|
||||
else
|
||||
{
|
||||
self.authenticationAlert?.buttons.first?.isEnabled = true
|
||||
}
|
||||
|
||||
self.authenticationAlert?.layout()
|
||||
}
|
||||
}
|
||||
|
||||
extension AppDelegate: UNUserNotificationCenterDelegate
|
||||
{
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void)
|
||||
{
|
||||
completionHandler([.alert, .sound, .badge])
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "16x16",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon@16.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "16x16",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon@32-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "32x32",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon@32.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "32x32",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon@64.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "128x128",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon@128.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "128x128",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon@256-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "256x256",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon@256.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "256x256",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon@512-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "512x512",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon@512.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "512x512",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon@1024.png",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 294 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "MenuBar@19.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "MenuBar@38.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
@@ -1,381 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="17503.1" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="17503.1"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Application-->
|
||||
<scene sceneID="JPo-4y-FX3">
|
||||
<objects>
|
||||
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
|
||||
<stackView distribution="fill" orientation="vertical" alignment="leading" spacing="4" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" id="urc-xw-Dhc">
|
||||
<rect key="frame" x="0.0" y="0.0" width="300" height="46"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<subviews>
|
||||
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="zLd-d8-ghZ">
|
||||
<rect key="frame" x="0.0" y="25" width="300" height="21"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Apple ID" drawsBackground="YES" id="BXa-Re-rs3">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="Voe-Tx-rLC" id="QtW-r2-Vuh"/>
|
||||
<outlet property="nextKeyView" destination="9rp-Vx-rvB" id="bQY-qj-Sej"/>
|
||||
</connections>
|
||||
</textField>
|
||||
<secureTextField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="9rp-Vx-rvB">
|
||||
<rect key="frame" x="0.0" y="0.0" width="300" height="21"/>
|
||||
<secureTextFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Password" drawsBackground="YES" usesSingleLineMode="YES" id="xqJ-wt-DlP">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
<allowedInputSourceLocales>
|
||||
<string>NSAllRomanInputSourcesLocaleIdentifier</string>
|
||||
</allowedInputSourceLocales>
|
||||
</secureTextFieldCell>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="Voe-Tx-rLC" id="qav-xj-izy"/>
|
||||
</connections>
|
||||
</secureTextField>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="9rp-Vx-rvB" firstAttribute="width" secondItem="urc-xw-Dhc" secondAttribute="width" id="Eht-pU-Gyh"/>
|
||||
<constraint firstItem="zLd-d8-ghZ" firstAttribute="width" secondItem="urc-xw-Dhc" secondAttribute="width" id="mg7-Kq-abL"/>
|
||||
<constraint firstAttribute="width" constant="300" id="zqf-x6-BET"/>
|
||||
</constraints>
|
||||
<visibilityPriorities>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
</visibilityPriorities>
|
||||
<customSpacing>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
</customSpacing>
|
||||
</stackView>
|
||||
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="AltServer" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="appMenu" destination="uQy-DD-JDr" id="7cY-Ov-AOW"/>
|
||||
<outlet property="authenticationAppleIDTextField" destination="zLd-d8-ghZ" id="wW5-0J-zdq"/>
|
||||
<outlet property="authenticationPasswordTextField" destination="9rp-Vx-rvB" id="ZoC-DI-jzQ"/>
|
||||
<outlet property="connectedDevicesMenu" destination="KJ9-WY-pW1" id="Mcv-64-iFU"/>
|
||||
<outlet property="installMailPluginMenuItem" destination="3CM-gV-X2G" id="lio-ha-z0S"/>
|
||||
<outlet property="launchAtLoginMenuItem" destination="IyR-FQ-upe" id="Fxn-EP-hwH"/>
|
||||
<outlet property="sideloadIPAConnectedDevicesMenu" destination="IuI-bV-fTY" id="QQw-St-HfG"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
<customObject id="Arf-IC-5eb" customClass="SUUpdater"/>
|
||||
<application id="hnw-xV-0zn" sceneMemberID="viewController">
|
||||
<menu key="mainMenu" title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
|
||||
<items>
|
||||
<menuItem title="AltServer" id="1Xt-HY-uBw">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="AltServer" systemMenu="apple" id="uQy-DD-JDr">
|
||||
<items>
|
||||
<menuItem title="About AltServer" id="5kV-Vb-QxS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="orderFrontStandardAboutPanel:" target="Ady-hI-5gd" id="Exp-CZ-Vem"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
|
||||
<menuItem title="Install AltStore" id="MJ8-Lt-SSV">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Install AltStore" systemMenu="recentDocuments" id="KJ9-WY-pW1">
|
||||
<items>
|
||||
<menuItem title="No Connected Devices" id="N5N-3K-XuR">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="clearRecentDocuments:" target="Ady-hI-5gd" id="DKG-yI-Ujv"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="Voe-Tx-rLC" id="VYb-BL-Zri"/>
|
||||
</connections>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Sideload .ipa" id="x0e-zI-0A2" userLabel="Install .ipa">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Sideload .ipa" systemMenu="recentDocuments" id="IuI-bV-fTY">
|
||||
<items>
|
||||
<menuItem title="No Connected Devices" id="in5-an-MD0">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="clearRecentDocuments:" target="Ady-hI-5gd" id="aUE-On-axK"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="Voe-Tx-rLC" id="N3K-su-XV6"/>
|
||||
</connections>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="1ZZ-BB-xHy"/>
|
||||
<menuItem title="Launch at Login" id="IyR-FQ-upe" userLabel="Launch At Login">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
</menuItem>
|
||||
<menuItem title="Install Mail Plug-in" id="3CM-gV-X2G" userLabel="Mail Plug-in">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="mVM-Nm-Zi9"/>
|
||||
<menuItem title="Check for Updates..." id="Tnq-gD-Eic" userLabel="Check for Updates">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="checkForUpdates:" target="Arf-IC-5eb" id="7JG-du-nr4"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="hmG-xg-qgm"/>
|
||||
<menuItem title="Quit AltServer" keyEquivalent="q" id="4sb-4s-VLi">
|
||||
<connections>
|
||||
<action selector="terminate:" target="Ady-hI-5gd" id="Te7-pn-YzF"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Edit" id="5QF-Oa-p0T">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Edit" id="W48-6f-4Dl">
|
||||
<items>
|
||||
<menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg">
|
||||
<connections>
|
||||
<action selector="undo:" target="Ady-hI-5gd" id="M6e-cu-g7V"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam">
|
||||
<connections>
|
||||
<action selector="redo:" target="Ady-hI-5gd" id="oIA-Rs-6OD"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
|
||||
<menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG">
|
||||
<connections>
|
||||
<action selector="cut:" target="Ady-hI-5gd" id="YJe-68-I9s"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU">
|
||||
<connections>
|
||||
<action selector="copy:" target="Ady-hI-5gd" id="G1f-GL-Joy"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
|
||||
<connections>
|
||||
<action selector="paste:" target="Ady-hI-5gd" id="UvS-8e-Qdg"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="pasteAsPlainText:" target="Ady-hI-5gd" id="cEh-KX-wJQ"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Delete" id="pa3-QI-u2k">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="delete:" target="Ady-hI-5gd" id="0Mk-Ml-PaM"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m">
|
||||
<connections>
|
||||
<action selector="selectAll:" target="Ady-hI-5gd" id="VNm-Mi-diN"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/>
|
||||
<menuItem title="Find" id="4EN-yA-p0u">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Find" id="1b7-l0-nxx">
|
||||
<items>
|
||||
<menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="cD7-Qs-BN4"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="WD3-Gg-5AJ"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="NDo-RZ-v9R"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="HOh-sY-3ay"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="U76-nv-p5D"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd">
|
||||
<connections>
|
||||
<action selector="centerSelectionInVisibleArea:" target="Ady-hI-5gd" id="IOG-6D-g5B"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Spelling and Grammar" id="Dv1-io-Yv7">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Spelling" id="3IN-sU-3Bg">
|
||||
<items>
|
||||
<menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI">
|
||||
<connections>
|
||||
<action selector="showGuessPanel:" target="Ady-hI-5gd" id="vFj-Ks-hy3"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7">
|
||||
<connections>
|
||||
<action selector="checkSpelling:" target="Ady-hI-5gd" id="fz7-VC-reM"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="bNw-od-mp5"/>
|
||||
<menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleContinuousSpellChecking:" target="Ady-hI-5gd" id="7w6-Qz-0kB"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleGrammarChecking:" target="Ady-hI-5gd" id="muD-Qn-j4w"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Correct Spelling Automatically" id="78Y-hA-62v">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticSpellingCorrection:" target="Ady-hI-5gd" id="2lM-Qi-WAP"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Substitutions" id="9ic-FL-obx">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Substitutions" id="FeM-D8-WVr">
|
||||
<items>
|
||||
<menuItem title="Show Substitutions" id="z6F-FW-3nz">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="orderFrontSubstitutionsPanel:" target="Ady-hI-5gd" id="oku-mr-iSq"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/>
|
||||
<menuItem title="Smart Copy/Paste" id="9yt-4B-nSM">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleSmartInsertDelete:" target="Ady-hI-5gd" id="3IJ-Se-DZD"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smart Quotes" id="hQb-2v-fYv">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticQuoteSubstitution:" target="Ady-hI-5gd" id="ptq-xd-QOA"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smart Dashes" id="rgM-f4-ycn">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticDashSubstitution:" target="Ady-hI-5gd" id="oCt-pO-9gS"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smart Links" id="cwL-P1-jid">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticLinkDetection:" target="Ady-hI-5gd" id="Gip-E3-Fov"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Data Detectors" id="tRr-pd-1PS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticDataDetection:" target="Ady-hI-5gd" id="R1I-Nq-Kbl"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Text Replacement" id="HFQ-gK-NFA">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticTextReplacement:" target="Ady-hI-5gd" id="DvP-Fe-Py6"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Transformations" id="2oI-Rn-ZJC">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Transformations" id="c8a-y6-VQd">
|
||||
<items>
|
||||
<menuItem title="Make Upper Case" id="vmV-6d-7jI">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="uppercaseWord:" target="Ady-hI-5gd" id="sPh-Tk-edu"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Make Lower Case" id="d9M-CD-aMd">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="lowercaseWord:" target="Ady-hI-5gd" id="iUZ-b5-hil"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Capitalize" id="UEZ-Bs-lqG">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="capitalizeWord:" target="Ady-hI-5gd" id="26H-TL-nsh"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Speech" id="xrE-MZ-jX0">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Speech" id="3rS-ZA-NoH">
|
||||
<items>
|
||||
<menuItem title="Start Speaking" id="Ynk-f8-cLZ">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="startSpeaking:" target="Ady-hI-5gd" id="654-Ng-kyl"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Stop Speaking" id="Oyz-dy-DGm">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="stopSpeaking:" target="Ady-hI-5gd" id="dX8-6p-jy9"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Help" id="wpr-3q-Mcd">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ">
|
||||
<items>
|
||||
<menuItem title="AltServer Help" keyEquivalent="?" id="FKE-Sm-Kum">
|
||||
<connections>
|
||||
<action selector="showHelp:" target="Ady-hI-5gd" id="y7X-2Q-9no"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="Voe-Tx-rLC" id="PrD-fu-P6m"/>
|
||||
</connections>
|
||||
</application>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="75" y="0.0"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
@@ -1,24 +0,0 @@
|
||||
//
|
||||
// ALTNotificationConnection+Private.h
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 1/10/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ALTNotificationConnection.h"
|
||||
|
||||
#include <libimobiledevice/libimobiledevice.h>
|
||||
#include <libimobiledevice/notification_proxy.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface ALTNotificationConnection ()
|
||||
|
||||
@property (nonatomic, readonly) np_client_t client;
|
||||
|
||||
- (instancetype)initWithDevice:(ALTDevice *)device client:(np_client_t)client;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -1,30 +0,0 @@
|
||||
//
|
||||
// ALTNotificationConnection.h
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 1/10/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import "AltSign.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
NS_SWIFT_NAME(NotificationConnection)
|
||||
@interface ALTNotificationConnection : NSObject
|
||||
|
||||
@property (nonatomic, copy, readonly) ALTDevice *device;
|
||||
|
||||
@property (nonatomic, copy, nullable) void (^receivedNotificationHandler)(CFNotificationName notification);
|
||||
|
||||
- (void)startListeningForNotifications:(NSArray<NSString *> *)notifications
|
||||
completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
|
||||
|
||||
- (void)sendNotification:(CFNotificationName)notification
|
||||
completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
|
||||
|
||||
- (void)disconnect;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -1,94 +0,0 @@
|
||||
//
|
||||
// ALTNotificationConnection.m
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 1/10/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ALTNotificationConnection+Private.h"
|
||||
|
||||
#import "NSError+ALTServerError.h"
|
||||
|
||||
void ALTDeviceReceivedNotification(const char *notification, void *user_data);
|
||||
|
||||
@implementation ALTNotificationConnection
|
||||
|
||||
- (instancetype)initWithDevice:(ALTDevice *)device client:(np_client_t)client
|
||||
{
|
||||
self = [super init];
|
||||
if (self)
|
||||
{
|
||||
_device = [device copy];
|
||||
_client = client;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[self disconnect];
|
||||
}
|
||||
|
||||
- (void)disconnect
|
||||
{
|
||||
np_client_free(self.client);
|
||||
_client = nil;
|
||||
}
|
||||
|
||||
- (void)startListeningForNotifications:(NSArray<NSString *> *)notifications completionHandler:(void (^)(BOOL, NSError * _Nullable))completionHandler
|
||||
{
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
|
||||
const char **notificationNames = (const char **)malloc((notifications.count + 1) * sizeof(char *));
|
||||
for (int i = 0; i < notifications.count; i++)
|
||||
{
|
||||
NSString *name = notifications[i];
|
||||
notificationNames[i] = name.UTF8String;
|
||||
}
|
||||
notificationNames[notifications.count] = NULL; // Must have terminating NULL entry.
|
||||
|
||||
np_error_t result = np_observe_notifications(self.client, notificationNames);
|
||||
if (result != NP_E_SUCCESS)
|
||||
{
|
||||
return completionHandler(NO, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorLostConnection userInfo:nil]);
|
||||
}
|
||||
|
||||
result = np_set_notify_callback(self.client, ALTDeviceReceivedNotification, (__bridge void *)self);
|
||||
if (result != NP_E_SUCCESS)
|
||||
{
|
||||
return completionHandler(NO, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorLostConnection userInfo:nil]);
|
||||
}
|
||||
|
||||
completionHandler(YES, nil);
|
||||
|
||||
free(notificationNames);
|
||||
});
|
||||
}
|
||||
|
||||
- (void)sendNotification:(CFNotificationName)notification completionHandler:(void (^)(BOOL, NSError * _Nullable))completionHandler
|
||||
{
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
|
||||
np_error_t result = np_post_notification(self.client, [(__bridge NSString *)notification UTF8String]);
|
||||
if (result == NP_E_SUCCESS)
|
||||
{
|
||||
completionHandler(YES, nil);
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(NO, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorLostConnection userInfo:nil]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
void ALTDeviceReceivedNotification(const char *notification, void *user_data)
|
||||
{
|
||||
ALTNotificationConnection *connection = (__bridge ALTNotificationConnection *)user_data;
|
||||
|
||||
if (connection.receivedNotificationHandler)
|
||||
{
|
||||
connection.receivedNotificationHandler((__bridge CFNotificationName)@(notification));
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
//
|
||||
// ALTWiredConnection+Private.h
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 1/10/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ALTWiredConnection.h"
|
||||
|
||||
#include <libimobiledevice/libimobiledevice.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface ALTWiredConnection ()
|
||||
|
||||
@property (nonatomic, readwrite, getter=isConnected) BOOL connected;
|
||||
|
||||
@property (nonatomic, readonly) idevice_connection_t connection;
|
||||
|
||||
- (instancetype)initWithDevice:(ALTDevice *)device connection:(idevice_connection_t)connection;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -1,29 +0,0 @@
|
||||
//
|
||||
// ALTWiredConnection.h
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 1/10/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import "AltSign.h"
|
||||
|
||||
#import "ALTConnection.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
NS_SWIFT_NAME(WiredConnection)
|
||||
@interface ALTWiredConnection : NSObject <ALTConnection>
|
||||
|
||||
@property (nonatomic, readonly, getter=isConnected) BOOL connected;
|
||||
|
||||
@property (nonatomic, copy, readonly) ALTDevice *device;
|
||||
|
||||
- (void)sendData:(NSData *)data completionHandler:(void (^)(BOOL, NSError * _Nullable))completionHandler;
|
||||
- (void)receiveDataWithExpectedSize:(NSInteger)expectedSize completionHandler:(void (^)(NSData * _Nullable, NSError * _Nullable))completionHandler;
|
||||
|
||||
- (void)disconnect;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -1,117 +0,0 @@
|
||||
//
|
||||
// ALTWiredConnection.m
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 1/10/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ALTWiredConnection+Private.h"
|
||||
|
||||
#import "ALTConnection.h"
|
||||
#import "NSError+ALTServerError.h"
|
||||
|
||||
@implementation ALTWiredConnection
|
||||
|
||||
- (instancetype)initWithDevice:(ALTDevice *)device connection:(idevice_connection_t)connection
|
||||
{
|
||||
self = [super init];
|
||||
if (self)
|
||||
{
|
||||
_device = [device copy];
|
||||
_connection = connection;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[self disconnect];
|
||||
}
|
||||
|
||||
- (void)disconnect
|
||||
{
|
||||
if (![self isConnected])
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
idevice_disconnect(self.connection);
|
||||
_connection = nil;
|
||||
|
||||
self.connected = NO;
|
||||
}
|
||||
|
||||
- (void)sendData:(NSData *)data completionHandler:(void (^)(BOOL, NSError * _Nullable))completionHandler
|
||||
{
|
||||
void (^finish)(NSError *error) = ^(NSError *error) {
|
||||
if (error != nil)
|
||||
{
|
||||
NSLog(@"Send Error: %@", error);
|
||||
completionHandler(NO, error);
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(YES, nil);
|
||||
}
|
||||
};
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
|
||||
NSMutableData *mutableData = [data mutableCopy];
|
||||
while (mutableData.length > 0)
|
||||
{
|
||||
uint32_t sentBytes = 0;
|
||||
if (idevice_connection_send(self.connection, (const char *)mutableData.bytes, (int32_t)mutableData.length, &sentBytes) != IDEVICE_E_SUCCESS)
|
||||
{
|
||||
return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorLostConnection userInfo:nil]);
|
||||
}
|
||||
|
||||
[mutableData replaceBytesInRange:NSMakeRange(0, sentBytes) withBytes:NULL length:0];
|
||||
}
|
||||
|
||||
finish(nil);
|
||||
});
|
||||
}
|
||||
|
||||
- (void)receiveDataWithExpectedSize:(NSInteger)expectedSize completionHandler:(void (^)(NSData * _Nullable, NSError * _Nullable))completionHandler
|
||||
{
|
||||
void (^finish)(NSData *data, NSError *error) = ^(NSData *data, NSError *error) {
|
||||
if (error != nil)
|
||||
{
|
||||
NSLog(@"Receive Data Error: %@", error);
|
||||
}
|
||||
|
||||
completionHandler(data, error);
|
||||
};
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
|
||||
char bytes[4096];
|
||||
NSMutableData *receivedData = [NSMutableData dataWithCapacity:expectedSize];
|
||||
|
||||
while (receivedData.length < expectedSize)
|
||||
{
|
||||
uint32_t size = MIN(4096, (uint32_t)expectedSize - (uint32_t)receivedData.length);
|
||||
|
||||
uint32_t receivedBytes = 0;
|
||||
if (idevice_connection_receive_timeout(self.connection, bytes, size, &receivedBytes, 10000) != IDEVICE_E_SUCCESS)
|
||||
{
|
||||
return finish(nil, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorLostConnection userInfo:nil]);
|
||||
}
|
||||
|
||||
NSData *data = [NSData dataWithBytesNoCopy:bytes length:receivedBytes freeWhenDone:NO];
|
||||
[receivedData appendData:data];
|
||||
}
|
||||
|
||||
finish(receivedData, nil);
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - NSObject -
|
||||
|
||||
- (NSString *)description
|
||||
{
|
||||
return [NSString stringWithFormat:@"%@ (Wired)", self.device.name];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,218 +0,0 @@
|
||||
//
|
||||
// RequestHandler.swift
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 5/23/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
typealias ServerConnectionManager = ConnectionManager<ServerRequestHandler>
|
||||
|
||||
private let connectionManager = ConnectionManager(requestHandler: ServerRequestHandler(),
|
||||
connectionHandlers: [WirelessConnectionHandler(), WiredConnectionHandler()])
|
||||
|
||||
extension ServerConnectionManager
|
||||
{
|
||||
static var shared: ConnectionManager {
|
||||
return connectionManager
|
||||
}
|
||||
}
|
||||
|
||||
struct ServerRequestHandler: RequestHandler
|
||||
{
|
||||
func handleAnisetteDataRequest(_ request: AnisetteDataRequest, for connection: Connection, completionHandler: @escaping (Result<AnisetteDataResponse, Error>) -> Void)
|
||||
{
|
||||
AnisetteDataManager.shared.requestAnisetteData { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let anisetteData):
|
||||
let response = AnisetteDataResponse(anisetteData: anisetteData)
|
||||
completionHandler(.success(response))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handlePrepareAppRequest(_ request: PrepareAppRequest, for connection: Connection, completionHandler: @escaping (Result<InstallationProgressResponse, Error>) -> Void)
|
||||
{
|
||||
var temporaryURL: URL?
|
||||
|
||||
func finish(_ result: Result<InstallationProgressResponse, Error>)
|
||||
{
|
||||
if let temporaryURL = temporaryURL
|
||||
{
|
||||
do { try FileManager.default.removeItem(at: temporaryURL) }
|
||||
catch { print("Failed to remove .ipa.", error) }
|
||||
}
|
||||
|
||||
completionHandler(result)
|
||||
}
|
||||
|
||||
self.receiveApp(for: request, from: connection) { (result) in
|
||||
print("Received app with result:", result)
|
||||
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(.failure(error))
|
||||
case .success(let fileURL):
|
||||
temporaryURL = fileURL
|
||||
|
||||
print("Awaiting begin installation request...")
|
||||
|
||||
connection.receiveRequest() { (result) in
|
||||
print("Received begin installation request with result:", result)
|
||||
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(.failure(error))
|
||||
case .success(.beginInstallation(let installRequest)):
|
||||
print("Installing app to device \(request.udid)...")
|
||||
|
||||
self.installApp(at: fileURL, toDeviceWithUDID: request.udid, activeProvisioningProfiles: installRequest.activeProfiles, connection: connection) { (result) in
|
||||
print("Installed app to device with result:", result)
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(.failure(error))
|
||||
case .success:
|
||||
let response = InstallationProgressResponse(progress: 1.0)
|
||||
finish(.success(response))
|
||||
}
|
||||
}
|
||||
|
||||
case .success: finish(.failure(ALTServerError(.unknownRequest)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleInstallProvisioningProfilesRequest(_ request: InstallProvisioningProfilesRequest, for connection: Connection,
|
||||
completionHandler: @escaping (Result<InstallProvisioningProfilesResponse, Error>) -> Void)
|
||||
{
|
||||
ALTDeviceManager.shared.installProvisioningProfiles(request.provisioningProfiles, toDeviceWithUDID: request.udid, activeProvisioningProfiles: request.activeProfiles) { (success, error) in
|
||||
if let error = error, !success
|
||||
{
|
||||
print("Failed to install profiles \(request.provisioningProfiles.map { $0.bundleIdentifier }):", error)
|
||||
completionHandler(.failure(ALTServerError(error)))
|
||||
}
|
||||
else
|
||||
{
|
||||
print("Installed profiles:", request.provisioningProfiles.map { $0.bundleIdentifier })
|
||||
|
||||
let response = InstallProvisioningProfilesResponse()
|
||||
completionHandler(.success(response))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleRemoveProvisioningProfilesRequest(_ request: RemoveProvisioningProfilesRequest, for connection: Connection,
|
||||
completionHandler: @escaping (Result<RemoveProvisioningProfilesResponse, Error>) -> Void)
|
||||
{
|
||||
ALTDeviceManager.shared.removeProvisioningProfiles(forBundleIdentifiers: request.bundleIdentifiers, fromDeviceWithUDID: request.udid) { (success, error) in
|
||||
if let error = error, !success
|
||||
{
|
||||
print("Failed to remove profiles \(request.bundleIdentifiers):", error)
|
||||
completionHandler(.failure(ALTServerError(error)))
|
||||
}
|
||||
else
|
||||
{
|
||||
print("Removed profiles:", request.bundleIdentifiers)
|
||||
|
||||
let response = RemoveProvisioningProfilesResponse()
|
||||
completionHandler(.success(response))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleRemoveAppRequest(_ request: RemoveAppRequest, for connection: Connection, completionHandler: @escaping (Result<RemoveAppResponse, Error>) -> Void)
|
||||
{
|
||||
ALTDeviceManager.shared.removeApp(forBundleIdentifier: request.bundleIdentifier, fromDeviceWithUDID: request.udid) { (success, error) in
|
||||
if let error = error, !success
|
||||
{
|
||||
print("Failed to remove app \(request.bundleIdentifier):", error)
|
||||
completionHandler(.failure(ALTServerError(error)))
|
||||
}
|
||||
else
|
||||
{
|
||||
print("Removed app:", request.bundleIdentifier)
|
||||
|
||||
let response = RemoveAppResponse()
|
||||
completionHandler(.success(response))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension RequestHandler
|
||||
{
|
||||
func receiveApp(for request: PrepareAppRequest, from connection: Connection, completionHandler: @escaping (Result<URL, ALTServerError>) -> Void)
|
||||
{
|
||||
connection.receiveData(expectedSize: request.contentSize) { (result) in
|
||||
do
|
||||
{
|
||||
print("Received app data!")
|
||||
|
||||
let data = try result.get()
|
||||
|
||||
guard ALTDeviceManager.shared.availableDevices.contains(where: { $0.identifier == request.udid }) else { throw ALTServerError(.deviceNotFound) }
|
||||
|
||||
print("Writing app data...")
|
||||
|
||||
let temporaryURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".ipa")
|
||||
try data.write(to: temporaryURL, options: .atomic)
|
||||
|
||||
print("Wrote app to URL:", temporaryURL)
|
||||
|
||||
completionHandler(.success(temporaryURL))
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Error processing app data:", error)
|
||||
|
||||
completionHandler(.failure(ALTServerError(error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func installApp(at fileURL: URL, toDeviceWithUDID udid: String, activeProvisioningProfiles: Set<String>?, connection: Connection, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void)
|
||||
{
|
||||
let serialQueue = DispatchQueue(label: "com.altstore.ConnectionManager.installQueue", qos: .default)
|
||||
var isSending = false
|
||||
|
||||
var observation: NSKeyValueObservation?
|
||||
|
||||
let progress = ALTDeviceManager.shared.installApp(at: fileURL, toDeviceWithUDID: udid, activeProvisioningProfiles: activeProvisioningProfiles) { (success, error) in
|
||||
print("Installed app with result:", error == nil ? "Success" : error!.localizedDescription)
|
||||
|
||||
if let error = error.map({ ALTServerError($0) })
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
|
||||
observation?.invalidate()
|
||||
observation = nil
|
||||
}
|
||||
|
||||
observation = progress.observe(\.fractionCompleted, changeHandler: { (progress, change) in
|
||||
serialQueue.async {
|
||||
guard !isSending else { return }
|
||||
isSending = true
|
||||
|
||||
print("Progress:", progress.fractionCompleted)
|
||||
let response = InstallationProgressResponse(progress: progress.fractionCompleted)
|
||||
|
||||
connection.send(response) { (result) in
|
||||
serialQueue.async {
|
||||
isSending = false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
//
|
||||
// WiredConnectionHandler.swift
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 6/1/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class WiredConnectionHandler: ConnectionHandler
|
||||
{
|
||||
var connectionHandler: ((Connection) -> Void)?
|
||||
var disconnectionHandler: ((Connection) -> Void)?
|
||||
|
||||
private var notificationConnections = [ALTDevice: NotificationConnection]()
|
||||
|
||||
func startListening()
|
||||
{
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(WiredConnectionHandler.deviceDidConnect(_:)), name: .deviceManagerDeviceDidConnect, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(WiredConnectionHandler.deviceDidDisconnect(_:)), name: .deviceManagerDeviceDidDisconnect, object: nil)
|
||||
}
|
||||
|
||||
func stopListening()
|
||||
{
|
||||
NotificationCenter.default.removeObserver(self, name: .deviceManagerDeviceDidConnect, object: nil)
|
||||
NotificationCenter.default.removeObserver(self, name: .deviceManagerDeviceDidDisconnect, object: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private extension WiredConnectionHandler
|
||||
{
|
||||
func startNotificationConnection(to device: ALTDevice)
|
||||
{
|
||||
ALTDeviceManager.shared.startNotificationConnection(to: device) { (connection, error) in
|
||||
guard let connection = connection else { return }
|
||||
|
||||
let notifications: [CFNotificationName] = [.wiredServerConnectionAvailableRequest, .wiredServerConnectionStartRequest]
|
||||
connection.startListening(forNotifications: notifications.map { String($0.rawValue) }) { (success, error) in
|
||||
guard success else { return }
|
||||
|
||||
connection.receivedNotificationHandler = { [weak self, weak connection] (notification) in
|
||||
guard let self = self, let connection = connection else { return }
|
||||
self.handle(notification, for: connection)
|
||||
}
|
||||
|
||||
self.notificationConnections[device] = connection
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stopNotificationConnection(to device: ALTDevice)
|
||||
{
|
||||
guard let connection = self.notificationConnections[device] else { return }
|
||||
connection.disconnect()
|
||||
|
||||
self.notificationConnections[device] = nil
|
||||
}
|
||||
|
||||
func handle(_ notification: CFNotificationName, for connection: NotificationConnection)
|
||||
{
|
||||
switch notification
|
||||
{
|
||||
case .wiredServerConnectionAvailableRequest:
|
||||
connection.sendNotification(.wiredServerConnectionAvailableResponse) { (success, error) in
|
||||
if let error = error, !success
|
||||
{
|
||||
print("Error sending wired server connection response.", error)
|
||||
}
|
||||
else
|
||||
{
|
||||
print("Sent wired server connection available response!")
|
||||
}
|
||||
}
|
||||
|
||||
case .wiredServerConnectionStartRequest:
|
||||
ALTDeviceManager.shared.startWiredConnection(to: connection.device) { (wiredConnection, error) in
|
||||
if let wiredConnection = wiredConnection
|
||||
{
|
||||
print("Started wired server connection!")
|
||||
self.connectionHandler?(wiredConnection)
|
||||
|
||||
var observation: NSKeyValueObservation?
|
||||
observation = wiredConnection.observe(\.isConnected) { [weak self] (connection, change) in
|
||||
guard !connection.isConnected else { return }
|
||||
self?.disconnectionHandler?(connection)
|
||||
|
||||
observation?.invalidate()
|
||||
}
|
||||
}
|
||||
else if let error = error
|
||||
{
|
||||
print("Error starting wired server connection.", error)
|
||||
}
|
||||
}
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension WiredConnectionHandler
|
||||
{
|
||||
@objc func deviceDidConnect(_ notification: Notification)
|
||||
{
|
||||
guard let device = notification.object as? ALTDevice else { return }
|
||||
self.startNotificationConnection(to: device)
|
||||
}
|
||||
|
||||
@objc func deviceDidDisconnect(_ notification: Notification)
|
||||
{
|
||||
guard let device = notification.object as? ALTDevice else { return }
|
||||
self.stopNotificationConnection(to: device)
|
||||
}
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
//
|
||||
// WirelessConnectionHandler.swift
|
||||
// AltKit
|
||||
//
|
||||
// Created by Riley Testut on 6/1/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Network
|
||||
|
||||
extension WirelessConnectionHandler
|
||||
{
|
||||
public enum State
|
||||
{
|
||||
case notRunning
|
||||
case connecting
|
||||
case running(NWListener.Service)
|
||||
case failed(Swift.Error)
|
||||
}
|
||||
}
|
||||
|
||||
public class WirelessConnectionHandler: ConnectionHandler
|
||||
{
|
||||
public var connectionHandler: ((Connection) -> Void)?
|
||||
public var disconnectionHandler: ((Connection) -> Void)?
|
||||
|
||||
public var stateUpdateHandler: ((State) -> Void)?
|
||||
|
||||
public private(set) var state: State = .notRunning {
|
||||
didSet {
|
||||
self.stateUpdateHandler?(self.state)
|
||||
}
|
||||
}
|
||||
|
||||
private lazy var listener = self.makeListener()
|
||||
private let dispatchQueue = DispatchQueue(label: "io.altstore.WirelessConnectionListener", qos: .utility)
|
||||
|
||||
public func startListening()
|
||||
{
|
||||
switch self.state
|
||||
{
|
||||
case .notRunning, .failed: self.listener.start(queue: self.dispatchQueue)
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
public func stopListening()
|
||||
{
|
||||
switch self.state
|
||||
{
|
||||
case .running: self.listener.cancel()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension WirelessConnectionHandler
|
||||
{
|
||||
func makeListener() -> NWListener
|
||||
{
|
||||
let listener = try! NWListener(using: .tcp)
|
||||
|
||||
let service: NWListener.Service
|
||||
|
||||
if let serverID = UserDefaults.standard.serverID?.data(using: .utf8)
|
||||
{
|
||||
let txtDictionary = ["serverID": serverID]
|
||||
let txtData = NetService.data(fromTXTRecord: txtDictionary)
|
||||
|
||||
service = NWListener.Service(name: nil, type: ALTServerServiceType, domain: nil, txtRecord: txtData)
|
||||
}
|
||||
else
|
||||
{
|
||||
service = NWListener.Service(type: ALTServerServiceType)
|
||||
}
|
||||
|
||||
listener.service = service
|
||||
|
||||
listener.serviceRegistrationUpdateHandler = { (serviceChange) in
|
||||
switch serviceChange
|
||||
{
|
||||
case .add(.service(let name, let type, let domain, _)):
|
||||
let service = NWListener.Service(name: name, type: type, domain: domain, txtRecord: nil)
|
||||
self.state = .running(service)
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
listener.stateUpdateHandler = { (state) in
|
||||
switch state
|
||||
{
|
||||
case .ready: break
|
||||
case .waiting, .setup: self.state = .connecting
|
||||
case .cancelled: self.state = .notRunning
|
||||
case .failed(let error): self.state = .failed(error)
|
||||
@unknown default: break
|
||||
}
|
||||
}
|
||||
|
||||
listener.newConnectionHandler = { [weak self] (connection) in
|
||||
self?.prepare(connection)
|
||||
}
|
||||
|
||||
return listener
|
||||
}
|
||||
|
||||
func prepare(_ nwConnection: NWConnection)
|
||||
{
|
||||
print("Preparing:", nwConnection)
|
||||
|
||||
// Use same instance for all callbacks.
|
||||
let connection = NetworkConnection(nwConnection)
|
||||
|
||||
nwConnection.stateUpdateHandler = { [weak self] (state) in
|
||||
switch state
|
||||
{
|
||||
case .setup, .preparing: break
|
||||
|
||||
case .ready:
|
||||
print("Connected to client:", connection)
|
||||
self?.connectionHandler?(connection)
|
||||
|
||||
case .waiting:
|
||||
print("Waiting for connection...")
|
||||
|
||||
case .failed(let error):
|
||||
print("Failed to connect to service \(nwConnection.endpoint).", error)
|
||||
self?.disconnect(connection)
|
||||
|
||||
case .cancelled:
|
||||
self?.disconnect(connection)
|
||||
|
||||
@unknown default: break
|
||||
}
|
||||
}
|
||||
|
||||
nwConnection.start(queue: self.dispatchQueue)
|
||||
}
|
||||
|
||||
func disconnect(_ connection: Connection)
|
||||
{
|
||||
connection.disconnect()
|
||||
|
||||
self.disconnectionHandler?(connection)
|
||||
}
|
||||
}
|
||||
@@ -1,846 +0,0 @@
|
||||
//
|
||||
// ALTDeviceManager+Installation.swift
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 7/1/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import UserNotifications
|
||||
import ObjectiveC
|
||||
|
||||
private let appGroupsLock = NSLock()
|
||||
|
||||
enum InstallError: LocalizedError
|
||||
{
|
||||
case cancelled
|
||||
case noTeam
|
||||
case missingPrivateKey
|
||||
case missingCertificate
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self
|
||||
{
|
||||
case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "")
|
||||
case .noTeam: return "You are not a member of any developer teams."
|
||||
case .missingPrivateKey: return "The developer certificate's private key could not be found."
|
||||
case .missingCertificate: return "The developer certificate could not be found."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ALTDeviceManager
|
||||
{
|
||||
func installApplication(at url: URL, to device: ALTDevice, appleID: String, password: String, completion: @escaping (Result<ALTApplication, Error>) -> Void)
|
||||
{
|
||||
let destinationDirectoryURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
||||
|
||||
func finish(_ result: Result<ALTApplication, Error>, title: String = "")
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
completion(result)
|
||||
}
|
||||
|
||||
try? FileManager.default.removeItem(at: destinationDirectoryURL)
|
||||
}
|
||||
|
||||
AnisetteDataManager.shared.requestAnisetteData { (result) in
|
||||
do
|
||||
{
|
||||
let anisetteData = try result.get()
|
||||
|
||||
self.authenticate(appleID: appleID, password: password, anisetteData: anisetteData) { (result) in
|
||||
do
|
||||
{
|
||||
let (account, session) = try result.get()
|
||||
|
||||
self.fetchTeam(for: account, session: session) { (result) in
|
||||
do
|
||||
{
|
||||
let team = try result.get()
|
||||
|
||||
self.register(device, team: team, session: session) { (result) in
|
||||
do
|
||||
{
|
||||
let device = try result.get()
|
||||
|
||||
self.fetchCertificate(for: team, session: session) { (result) in
|
||||
do
|
||||
{
|
||||
let certificate = try result.get()
|
||||
|
||||
if !url.isFileURL
|
||||
{
|
||||
// Show alert before downloading remote .ipa.
|
||||
self.showInstallationAlert(appName: NSLocalizedString("AltStore", comment: ""), deviceName: device.name)
|
||||
}
|
||||
|
||||
self.downloadApp(from: url) { (result) in
|
||||
do
|
||||
{
|
||||
let fileURL = try result.get()
|
||||
|
||||
try FileManager.default.createDirectory(at: destinationDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
let appBundleURL = try FileManager.default.unzipAppBundle(at: fileURL, toDirectory: destinationDirectoryURL)
|
||||
guard let application = ALTApplication(fileURL: appBundleURL) else { throw ALTError(.invalidApp) }
|
||||
|
||||
if url.isFileURL
|
||||
{
|
||||
// Show alert after "downloading" local .ipa.
|
||||
self.showInstallationAlert(appName: application.name, deviceName: device.name)
|
||||
}
|
||||
|
||||
// Refresh anisette data to prevent session timeouts.
|
||||
AnisetteDataManager.shared.requestAnisetteData { (result) in
|
||||
do
|
||||
{
|
||||
let anisetteData = try result.get()
|
||||
session.anisetteData = anisetteData
|
||||
|
||||
self.prepareAllProvisioningProfiles(for: application, device: device, team: team, session: session) { (result) in
|
||||
do
|
||||
{
|
||||
let profiles = try result.get()
|
||||
|
||||
self.install(application, to: device, team: team, certificate: certificate, profiles: profiles) { (result) in
|
||||
finish(result.map { application }, title: "Failed to Install AltStore")
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(.failure(error), title: "Failed to Fetch Provisioning Profiles")
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(.failure(error), title: "Failed to Refresh Anisette Data")
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(.failure(error), title: "Failed to Download AltStore")
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(.failure(error), title: "Failed to Fetch Certificate")
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(.failure(error), title: "Failed to Register Device")
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(.failure(error), title: "Failed to Fetch Team")
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(.failure(error), title: "Failed to Authenticate")
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(.failure(error), title: "Failed to Fetch Anisette Data")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension ALTDeviceManager
|
||||
{
|
||||
func downloadApp(from url: URL, completionHandler: @escaping (Result<URL, Error>) -> Void)
|
||||
{
|
||||
guard !url.isFileURL else { return completionHandler(.success(url)) }
|
||||
|
||||
let downloadTask = URLSession.shared.downloadTask(with: url) { (fileURL, response, error) in
|
||||
do
|
||||
{
|
||||
let (fileURL, _) = try Result((fileURL, response), error).get()
|
||||
completionHandler(.success(fileURL))
|
||||
|
||||
do { try FileManager.default.removeItem(at: fileURL) }
|
||||
catch { print("Failed to remove downloaded .ipa.", error) }
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
downloadTask.resume()
|
||||
}
|
||||
|
||||
func authenticate(appleID: String, password: String, anisetteData: ALTAnisetteData, completionHandler: @escaping (Result<(ALTAccount, ALTAppleAPISession), Error>) -> Void)
|
||||
{
|
||||
func handleVerificationCode(_ completionHandler: @escaping (String?) -> Void)
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("Two-Factor Authentication Enabled", comment: "")
|
||||
alert.informativeText = NSLocalizedString("Please enter the 6-digit verification code that was sent to your Apple devices.", comment: "")
|
||||
|
||||
let textField = NSTextField(frame: NSRect(x: 0, y: 0, width: 300, height: 22))
|
||||
textField.delegate = self
|
||||
textField.translatesAutoresizingMaskIntoConstraints = false
|
||||
textField.placeholderString = NSLocalizedString("123456", comment: "")
|
||||
alert.accessoryView = textField
|
||||
alert.window.initialFirstResponder = textField
|
||||
|
||||
alert.addButton(withTitle: NSLocalizedString("Continue", comment: ""))
|
||||
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
|
||||
|
||||
self.securityCodeAlert = alert
|
||||
self.securityCodeTextField = textField
|
||||
self.validate()
|
||||
|
||||
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
|
||||
|
||||
let response = alert.runModal()
|
||||
if response == .alertFirstButtonReturn
|
||||
{
|
||||
let code = textField.stringValue
|
||||
completionHandler(code)
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ALTAppleAPI.shared.authenticate(appleID: appleID, password: password, anisetteData: anisetteData, verificationHandler: handleVerificationCode) { (account, session, error) in
|
||||
if let account = account, let session = session
|
||||
{
|
||||
completionHandler(.success((account, session)))
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.failure(error ?? ALTAppleAPIError(.unknown)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchTeam(for account: ALTAccount, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTTeam, Error>) -> Void)
|
||||
{
|
||||
ALTAppleAPI.shared.fetchTeams(for: account, session: session) { (teams, error) in
|
||||
do
|
||||
{
|
||||
let teams = try Result(teams, error).get()
|
||||
|
||||
if let team = teams.first(where: { $0.type == .individual })
|
||||
{
|
||||
return completionHandler(.success(team))
|
||||
}
|
||||
else if let team = teams.first(where: { $0.type == .free })
|
||||
{
|
||||
return completionHandler(.success(team))
|
||||
}
|
||||
else if let team = teams.first
|
||||
{
|
||||
return completionHandler(.success(team))
|
||||
}
|
||||
else
|
||||
{
|
||||
throw InstallError.noTeam
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchCertificate(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTCertificate, Error>) -> Void)
|
||||
{
|
||||
ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
|
||||
do
|
||||
{
|
||||
let certificates = try Result(certificates, error).get()
|
||||
|
||||
let applicationSupportDirectoryURL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
|
||||
let altserverDirectoryURL = applicationSupportDirectoryURL.appendingPathComponent("com.rileytestut.AltServer")
|
||||
let certificatesDirectoryURL = altserverDirectoryURL.appendingPathComponent("Certificates")
|
||||
|
||||
try FileManager.default.createDirectory(at: certificatesDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
let certificateFileURL = certificatesDirectoryURL.appendingPathComponent(team.identifier + ".p12")
|
||||
|
||||
var isCancelled = false
|
||||
|
||||
// Check if there is another AltStore certificate, which means AltStore has been installed with this Apple ID before.
|
||||
if let previousCertificate = certificates.first(where: { $0.machineName?.starts(with: "AltStore") == true })
|
||||
{
|
||||
if FileManager.default.fileExists(atPath: certificateFileURL.path),
|
||||
let data = try? Data(contentsOf: certificateFileURL),
|
||||
let certificate = ALTCertificate(p12Data: data, password: previousCertificate.machineIdentifier)
|
||||
{
|
||||
// Manually set machineIdentifier so we can encrypt + embed certificate if needed.
|
||||
certificate.machineIdentifier = previousCertificate.machineIdentifier
|
||||
return completionHandler(.success(certificate))
|
||||
}
|
||||
|
||||
DispatchQueue.main.sync {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("Multiple AltServers Not Supported", comment: "")
|
||||
alert.informativeText = NSLocalizedString("Please use the same AltServer you previously used with this Apple ID, or else apps installed with other AltServers will stop working.\n\nAre you sure you want to continue?", comment: "")
|
||||
|
||||
alert.addButton(withTitle: NSLocalizedString("Continue", comment: ""))
|
||||
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
|
||||
|
||||
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
|
||||
|
||||
let buttonIndex = alert.runModal()
|
||||
if buttonIndex == NSApplication.ModalResponse.alertSecondButtonReturn
|
||||
{
|
||||
isCancelled = true
|
||||
}
|
||||
}
|
||||
|
||||
guard !isCancelled else { return completionHandler(.failure(InstallError.cancelled)) }
|
||||
}
|
||||
|
||||
if team.type != .free
|
||||
{
|
||||
DispatchQueue.main.sync {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("Installing this app will revoke your iOS development certificate.", comment: "")
|
||||
alert.informativeText = NSLocalizedString("""
|
||||
This will not affect apps you've submitted to the App Store, but may cause apps you've installed to your devices with Xcode to stop working until you reinstall them.
|
||||
|
||||
To prevent this from happening, feel free to try again with another Apple ID.
|
||||
""", comment: "")
|
||||
|
||||
alert.addButton(withTitle: NSLocalizedString("Continue", comment: ""))
|
||||
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
|
||||
|
||||
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
|
||||
|
||||
let buttonIndex = alert.runModal()
|
||||
if buttonIndex == NSApplication.ModalResponse.alertSecondButtonReturn
|
||||
{
|
||||
isCancelled = true
|
||||
}
|
||||
}
|
||||
|
||||
guard !isCancelled else { return completionHandler(.failure(InstallError.cancelled)) }
|
||||
}
|
||||
|
||||
if let certificate = certificates.first
|
||||
{
|
||||
ALTAppleAPI.shared.revoke(certificate, for: team, session: session) { (success, error) in
|
||||
do
|
||||
{
|
||||
try Result(success, error).get()
|
||||
self.fetchCertificate(for: team, session: session, completionHandler: completionHandler)
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ALTAppleAPI.shared.addCertificate(machineName: "AltStore", to: team, session: session) { (certificate, error) in
|
||||
do
|
||||
{
|
||||
let certificate = try Result(certificate, error).get()
|
||||
guard let privateKey = certificate.privateKey else { throw InstallError.missingPrivateKey }
|
||||
|
||||
ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
|
||||
do
|
||||
{
|
||||
let certificates = try Result(certificates, error).get()
|
||||
|
||||
guard let certificate = certificates.first(where: { $0.serialNumber == certificate.serialNumber }) else {
|
||||
throw InstallError.missingCertificate
|
||||
}
|
||||
|
||||
certificate.privateKey = privateKey
|
||||
|
||||
completionHandler(.success(certificate))
|
||||
|
||||
if let machineIdentifier = certificate.machineIdentifier,
|
||||
let encryptedData = certificate.encryptedP12Data(withPassword: machineIdentifier)
|
||||
{
|
||||
// Cache certificate.
|
||||
do { try encryptedData.write(to: certificateFileURL, options: .atomic) }
|
||||
catch { print("Failed to cache certificate:", error) }
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func prepareAllProvisioningProfiles(for application: ALTApplication, device: ALTDevice, team: ALTTeam, session: ALTAppleAPISession,
|
||||
completion: @escaping (Result<[String: ALTProvisioningProfile], Error>) -> Void)
|
||||
{
|
||||
self.prepareProvisioningProfile(for: application, parentApp: nil, device: device, team: team, session: session) { (result) in
|
||||
do
|
||||
{
|
||||
let profile = try result.get()
|
||||
|
||||
var profiles = [application.bundleIdentifier: profile]
|
||||
var error: Error?
|
||||
|
||||
let dispatchGroup = DispatchGroup()
|
||||
|
||||
for appExtension in application.appExtensions
|
||||
{
|
||||
dispatchGroup.enter()
|
||||
|
||||
self.prepareProvisioningProfile(for: appExtension, parentApp: application, device: device, team: team, session: session) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let e): error = e
|
||||
case .success(let profile): profiles[appExtension.bundleIdentifier] = profile
|
||||
}
|
||||
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.notify(queue: .global()) {
|
||||
if let error = error
|
||||
{
|
||||
completion(.failure(error))
|
||||
}
|
||||
else
|
||||
{
|
||||
completion(.success(profiles))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func prepareProvisioningProfile(for application: ALTApplication, parentApp: ALTApplication?, device: ALTDevice, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
|
||||
{
|
||||
let parentBundleID = parentApp?.bundleIdentifier ?? application.bundleIdentifier
|
||||
let updatedParentBundleID: String
|
||||
|
||||
if application.isAltStoreApp
|
||||
{
|
||||
// Use legacy bundle ID format for AltStore (and its extensions).
|
||||
updatedParentBundleID = "com.\(team.identifier).\(parentBundleID)"
|
||||
}
|
||||
else
|
||||
{
|
||||
updatedParentBundleID = parentBundleID + "." + team.identifier // Append just team identifier to make it harder to track.
|
||||
}
|
||||
|
||||
let bundleID = application.bundleIdentifier.replacingOccurrences(of: parentBundleID, with: updatedParentBundleID)
|
||||
|
||||
let preferredName: String
|
||||
|
||||
if let parentApp = parentApp
|
||||
{
|
||||
preferredName = parentApp.name + " " + application.name
|
||||
}
|
||||
else
|
||||
{
|
||||
preferredName = application.name
|
||||
}
|
||||
|
||||
self.registerAppID(name: preferredName, bundleID: bundleID, team: team, session: session) { (result) in
|
||||
do
|
||||
{
|
||||
let appID = try result.get()
|
||||
|
||||
self.updateFeatures(for: appID, app: application, team: team, session: session) { (result) in
|
||||
do
|
||||
{
|
||||
let appID = try result.get()
|
||||
|
||||
self.updateAppGroups(for: appID, app: application, team: team, session: session) { (result) in
|
||||
do
|
||||
{
|
||||
let appID = try result.get()
|
||||
|
||||
self.fetchProvisioningProfile(for: appID, device: device, team: team, session: session) { (result) in
|
||||
completionHandler(result)
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func registerAppID(name appName: String, bundleID: String, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
||||
{
|
||||
ALTAppleAPI.shared.fetchAppIDs(for: team, session: session) { (appIDs, error) in
|
||||
do
|
||||
{
|
||||
let appIDs = try Result(appIDs, error).get()
|
||||
|
||||
if let appID = appIDs.first(where: { $0.bundleIdentifier == bundleID })
|
||||
{
|
||||
completionHandler(.success(appID))
|
||||
}
|
||||
else
|
||||
{
|
||||
ALTAppleAPI.shared.addAppID(withName: appName, bundleIdentifier: bundleID, team: team, session: session) { (appID, error) in
|
||||
completionHandler(Result(appID, error))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateFeatures(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
||||
{
|
||||
let requiredFeatures = app.entitlements.compactMap { (entitlement, value) -> (ALTFeature, Any)? in
|
||||
guard let feature = ALTFeature(entitlement: entitlement) else { return nil }
|
||||
return (feature, value)
|
||||
}
|
||||
|
||||
var features = requiredFeatures.reduce(into: [ALTFeature: Any]()) { $0[$1.0] = $1.1 }
|
||||
|
||||
if let applicationGroups = app.entitlements[.appGroups] as? [String], !applicationGroups.isEmpty
|
||||
{
|
||||
features[.appGroups] = true
|
||||
}
|
||||
|
||||
var updateFeatures = false
|
||||
|
||||
// Determine whether the required features are already enabled for the AppID.
|
||||
for (feature, value) in features
|
||||
{
|
||||
if let appIDValue = appID.features[feature] as AnyObject?, (value as AnyObject).isEqual(appIDValue)
|
||||
{
|
||||
// AppID already has this feature enabled and the values are the same.
|
||||
continue
|
||||
}
|
||||
else
|
||||
{
|
||||
// AppID either doesn't have this feature enabled or the value has changed,
|
||||
// so we need to update it to reflect new values.
|
||||
updateFeatures = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if updateFeatures
|
||||
{
|
||||
let appID = appID.copy() as! ALTAppID
|
||||
appID.features = features
|
||||
|
||||
ALTAppleAPI.shared.update(appID, team: team, session: session) { (appID, error) in
|
||||
completionHandler(Result(appID, error))
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.success(appID))
|
||||
}
|
||||
}
|
||||
|
||||
func updateAppGroups(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
||||
{
|
||||
let applicationGroups = app.entitlements[.appGroups] as? [String] ?? []
|
||||
if applicationGroups.isEmpty
|
||||
{
|
||||
guard let isAppGroupsEnabled = appID.features[.appGroups] as? Bool, isAppGroupsEnabled else {
|
||||
// No app groups, and we also haven't enabled the feature, so don't continue.
|
||||
// For apps with no app groups but have had the feature enabled already
|
||||
// we'll continue and assign the app ID to an empty array
|
||||
// in case we need to explicitly remove them.
|
||||
return completionHandler(.success(appID))
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch onto global queue to prevent appGroupsLock deadlock.
|
||||
DispatchQueue.global().async {
|
||||
|
||||
// Ensure we're not concurrently fetching and updating app groups,
|
||||
// which can lead to race conditions such as adding an app group twice.
|
||||
appGroupsLock.lock()
|
||||
|
||||
func finish(_ result: Result<ALTAppID, Error>)
|
||||
{
|
||||
appGroupsLock.unlock()
|
||||
completionHandler(result)
|
||||
}
|
||||
|
||||
ALTAppleAPI.shared.fetchAppGroups(for: team, session: session) { (groups, error) in
|
||||
switch Result(groups, error)
|
||||
{
|
||||
case .failure(let error): finish(.failure(error))
|
||||
case .success(let fetchedGroups):
|
||||
let dispatchGroup = DispatchGroup()
|
||||
|
||||
var groups = [ALTAppGroup]()
|
||||
var errors = [Error]()
|
||||
|
||||
for groupIdentifier in applicationGroups
|
||||
{
|
||||
let adjustedGroupIdentifier = groupIdentifier + "." + team.identifier
|
||||
|
||||
if let group = fetchedGroups.first(where: { $0.groupIdentifier == adjustedGroupIdentifier })
|
||||
{
|
||||
groups.append(group)
|
||||
}
|
||||
else
|
||||
{
|
||||
dispatchGroup.enter()
|
||||
|
||||
// Not all characters are allowed in group names, so we replace periods with spaces (like Apple does).
|
||||
let name = "AltStore " + groupIdentifier.replacingOccurrences(of: ".", with: " ")
|
||||
|
||||
ALTAppleAPI.shared.addAppGroup(withName: name, groupIdentifier: adjustedGroupIdentifier, team: team, session: session) { (group, error) in
|
||||
switch Result(group, error)
|
||||
{
|
||||
case .success(let group): groups.append(group)
|
||||
case .failure(let error): errors.append(error)
|
||||
}
|
||||
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.notify(queue: .global()) {
|
||||
if let error = errors.first
|
||||
{
|
||||
finish(.failure(error))
|
||||
}
|
||||
else
|
||||
{
|
||||
ALTAppleAPI.shared.assign(appID, to: Array(groups), team: team, session: session) { (success, error) in
|
||||
let result = Result(success, error)
|
||||
finish(result.map { _ in appID })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func register(_ device: ALTDevice, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void)
|
||||
{
|
||||
ALTAppleAPI.shared.fetchDevices(for: team, types: device.type, session: session) { (devices, error) in
|
||||
do
|
||||
{
|
||||
let devices = try Result(devices, error).get()
|
||||
|
||||
if let device = devices.first(where: { $0.identifier == device.identifier })
|
||||
{
|
||||
completionHandler(.success(device))
|
||||
}
|
||||
else
|
||||
{
|
||||
ALTAppleAPI.shared.registerDevice(name: device.name, identifier: device.identifier, type: device.type, team: team, session: session) { (device, error) in
|
||||
completionHandler(Result(device, error))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchProvisioningProfile(for appID: ALTAppID, device: ALTDevice, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
|
||||
{
|
||||
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: device.type, team: team, session: session) { (profile, error) in
|
||||
completionHandler(Result(profile, error))
|
||||
}
|
||||
}
|
||||
|
||||
func install(_ application: ALTApplication, to device: ALTDevice, team: ALTTeam, certificate: ALTCertificate, profiles: [String: ALTProvisioningProfile], completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
func prepare(_ bundle: Bundle, additionalInfoDictionaryValues: [String: Any] = [:]) throws
|
||||
{
|
||||
guard let identifier = bundle.bundleIdentifier else { throw ALTError(.missingAppBundle) }
|
||||
guard let profile = profiles[identifier] else { throw ALTError(.missingProvisioningProfile) }
|
||||
guard var infoDictionary = bundle.completeInfoDictionary else { throw ALTError(.missingInfoPlist) }
|
||||
|
||||
infoDictionary[kCFBundleIdentifierKey as String] = profile.bundleIdentifier
|
||||
infoDictionary[Bundle.Info.altBundleID] = identifier
|
||||
|
||||
for (key, value) in additionalInfoDictionaryValues
|
||||
{
|
||||
infoDictionary[key] = value
|
||||
}
|
||||
|
||||
if let appGroups = profile.entitlements[.appGroups] as? [String]
|
||||
{
|
||||
infoDictionary[Bundle.Info.appGroups] = appGroups
|
||||
}
|
||||
|
||||
try (infoDictionary as NSDictionary).write(to: bundle.infoPlistURL)
|
||||
}
|
||||
|
||||
DispatchQueue.global().async {
|
||||
do
|
||||
{
|
||||
guard let appBundle = Bundle(url: application.fileURL) else { throw ALTError(.missingAppBundle) }
|
||||
guard let infoDictionary = appBundle.completeInfoDictionary else { throw ALTError(.missingInfoPlist) }
|
||||
|
||||
let openAppURL = URL(string: "altstore-" + application.bundleIdentifier + "://")!
|
||||
|
||||
var allURLSchemes = infoDictionary[Bundle.Info.urlTypes] as? [[String: Any]] ?? []
|
||||
|
||||
// Embed open URL so AltBackup can return to AltStore.
|
||||
let altstoreURLScheme = ["CFBundleTypeRole": "Editor",
|
||||
"CFBundleURLName": application.bundleIdentifier,
|
||||
"CFBundleURLSchemes": [openAppURL.scheme!]] as [String : Any]
|
||||
allURLSchemes.append(altstoreURLScheme)
|
||||
|
||||
var additionalValues: [String: Any] = [Bundle.Info.urlTypes: allURLSchemes]
|
||||
|
||||
if application.isAltStoreApp
|
||||
{
|
||||
additionalValues[Bundle.Info.deviceID] = device.identifier
|
||||
additionalValues[Bundle.Info.serverID] = UserDefaults.standard.serverID
|
||||
|
||||
if
|
||||
let machineIdentifier = certificate.machineIdentifier,
|
||||
let encryptedData = certificate.encryptedP12Data(withPassword: machineIdentifier)
|
||||
{
|
||||
additionalValues[Bundle.Info.certificateID] = certificate.serialNumber
|
||||
|
||||
let certificateURL = application.fileURL.appendingPathComponent("ALTCertificate.p12")
|
||||
try encryptedData.write(to: certificateURL, options: .atomic)
|
||||
}
|
||||
}
|
||||
|
||||
try prepare(appBundle, additionalInfoDictionaryValues: additionalValues)
|
||||
|
||||
for appExtension in application.appExtensions
|
||||
{
|
||||
guard let bundle = Bundle(url: appExtension.fileURL) else { throw ALTError(.missingAppBundle) }
|
||||
try prepare(bundle)
|
||||
}
|
||||
|
||||
let resigner = ALTSigner(team: team, certificate: certificate)
|
||||
resigner.signApp(at: application.fileURL, provisioningProfiles: Array(profiles.values)) { (success, error) in
|
||||
do
|
||||
{
|
||||
try Result(success, error).get()
|
||||
|
||||
let activeProfiles: Set<String>? = (team.type == .free && application.isAltStoreApp) ? Set(profiles.values.map(\.bundleIdentifier)) : nil
|
||||
ALTDeviceManager.shared.installApp(at: application.fileURL, toDeviceWithUDID: device.identifier, activeProvisioningProfiles: activeProfiles) { (success, error) in
|
||||
completionHandler(Result(success, error))
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to install app", error)
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to install AltStore", error)
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func showInstallationAlert(appName: String, deviceName: String)
|
||||
{
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = String(format: NSLocalizedString("Installing %@ to %@...", comment: ""), appName, deviceName)
|
||||
content.body = NSLocalizedString("This may take a few seconds.", comment: "")
|
||||
|
||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
||||
UNUserNotificationCenter.current().add(request)
|
||||
}
|
||||
}
|
||||
|
||||
private var securityCodeAlertKey = 0
|
||||
private var securityCodeTextFieldKey = 0
|
||||
|
||||
extension ALTDeviceManager: NSTextFieldDelegate
|
||||
{
|
||||
var securityCodeAlert: NSAlert? {
|
||||
get { return objc_getAssociatedObject(self, &securityCodeAlertKey) as? NSAlert }
|
||||
set { objc_setAssociatedObject(self, &securityCodeAlertKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
|
||||
}
|
||||
|
||||
var securityCodeTextField: NSTextField? {
|
||||
get { return objc_getAssociatedObject(self, &securityCodeTextFieldKey) as? NSTextField }
|
||||
set { objc_setAssociatedObject(self, &securityCodeTextFieldKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
|
||||
}
|
||||
|
||||
public func controlTextDidChange(_ obj: Notification)
|
||||
{
|
||||
self.validate()
|
||||
}
|
||||
|
||||
public func controlTextDidEndEditing(_ obj: Notification)
|
||||
{
|
||||
self.validate()
|
||||
}
|
||||
|
||||
private func validate()
|
||||
{
|
||||
guard let code = self.securityCodeTextField?.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) else { return }
|
||||
|
||||
if code.count == 6
|
||||
{
|
||||
self.securityCodeAlert?.buttons.first?.isEnabled = true
|
||||
}
|
||||
else
|
||||
{
|
||||
self.securityCodeAlert?.buttons.first?.isEnabled = false
|
||||
}
|
||||
|
||||
self.securityCodeAlert?.layout()
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
//
|
||||
// ALTDeviceManager.h
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 5/24/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "AltSign.h"
|
||||
|
||||
@class ALTWiredConnection;
|
||||
@class ALTNotificationConnection;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
extern NSNotificationName const ALTDeviceManagerDeviceDidConnectNotification NS_SWIFT_NAME(deviceManagerDeviceDidConnect);
|
||||
extern NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification NS_SWIFT_NAME(deviceManagerDeviceDidDisconnect);
|
||||
|
||||
@interface ALTDeviceManager : NSObject
|
||||
|
||||
@property (class, nonatomic, readonly) ALTDeviceManager *sharedManager;
|
||||
|
||||
@property (nonatomic, readonly) NSArray<ALTDevice *> *connectedDevices;
|
||||
@property (nonatomic, readonly) NSArray<ALTDevice *> *availableDevices;
|
||||
|
||||
- (void)start;
|
||||
|
||||
/* App Installation */
|
||||
- (NSProgress *)installAppAtURL:(NSURL *)fileURL toDeviceWithUDID:(NSString *)udid activeProvisioningProfiles:(nullable NSSet<NSString *> *)activeProvisioningProfiles completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
|
||||
- (void)removeAppForBundleIdentifier:(NSString *)bundleIdentifier fromDeviceWithUDID:(NSString *)udid completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
|
||||
|
||||
- (void)installProvisioningProfiles:(NSSet<ALTProvisioningProfile *> *)provisioningProfiles toDeviceWithUDID:(NSString *)udid activeProvisioningProfiles:(nullable NSSet<NSString *> *)activeProvisioningProfiles completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
|
||||
- (void)removeProvisioningProfilesForBundleIdentifiers:(NSSet<NSString *> *)bundleIdentifiers fromDeviceWithUDID:(NSString *)udid completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
|
||||
|
||||
/* Connections */
|
||||
- (void)startWiredConnectionToDevice:(ALTDevice *)device completionHandler:(void (^)(ALTWiredConnection *_Nullable connection, NSError *_Nullable error))completionHandler;
|
||||
- (void)startNotificationConnectionToDevice:(ALTDevice *)device completionHandler:(void (^)(ALTNotificationConnection *_Nullable connection, NSError *_Nullable error))completionHandler;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -1,38 +0,0 @@
|
||||
//
|
||||
// UserDefaults+AltServer.swift
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 7/31/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension UserDefaults
|
||||
{
|
||||
var serverID: String? {
|
||||
get {
|
||||
return self.string(forKey: "serverID")
|
||||
}
|
||||
set {
|
||||
self.set(newValue, forKey: "serverID")
|
||||
}
|
||||
}
|
||||
|
||||
var didPresentInitialNotification: Bool {
|
||||
get {
|
||||
return self.bool(forKey: "didPresentInitialNotification")
|
||||
}
|
||||
set {
|
||||
self.set(newValue, forKey: "didPresentInitialNotification")
|
||||
}
|
||||
}
|
||||
|
||||
func registerDefaults()
|
||||
{
|
||||
if self.serverID == nil
|
||||
{
|
||||
self.serverID = UUID().uuidString
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
<?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>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string></string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||
<key>LSUIElement</key>
|
||||
<true/>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2019 Riley Testut. All rights reserved.</string>
|
||||
<key>NSMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>SUFeedURL</key>
|
||||
<string>https://altstore.io/altserver/sparkle-macos.xml</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,310 +0,0 @@
|
||||
//
|
||||
// PluginManager.swift
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 9/16/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AppKit
|
||||
import CryptoKit
|
||||
|
||||
import STPrivilegedTask
|
||||
|
||||
private let pluginDirectoryURL = URL(fileURLWithPath: "/Library/Mail/Bundles", isDirectory: true)
|
||||
private let pluginURL = pluginDirectoryURL.appendingPathComponent("AltPlugin.mailbundle")
|
||||
|
||||
enum PluginError: LocalizedError
|
||||
{
|
||||
case cancelled
|
||||
case unknown
|
||||
case notFound
|
||||
case mismatchedHash(hash: String, expectedHash: String)
|
||||
case taskError(String)
|
||||
case taskErrorCode(Int)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self
|
||||
{
|
||||
case .cancelled: return NSLocalizedString("Mail plug-in installation was cancelled.", comment: "")
|
||||
case .unknown: return NSLocalizedString("Failed to install Mail plug-in.", comment: "")
|
||||
case .notFound: return NSLocalizedString("The Mail plug-in does not exist at the requested URL.", comment: "")
|
||||
case .mismatchedHash(let hash, let expectedHash): return String(format: NSLocalizedString("The hash of the downloaded Mail plug-in does not match the expected hash.\n\nHash:\n%@\n\nExpected Hash:\n%@", comment: ""), hash, expectedHash)
|
||||
case .taskError(let output): return output
|
||||
case .taskErrorCode(let errorCode): return String(format: NSLocalizedString("There was an error installing the Mail plug-in. (Error Code: %@)", comment: ""), NSNumber(value: errorCode))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PluginVersion
|
||||
{
|
||||
var url: URL
|
||||
var sha256Hash: String
|
||||
var version: String
|
||||
|
||||
static let v1_0 = PluginVersion(url: URL(string: "https://f000.backblazeb2.com/file/altstore/altserver/altplugin/1_0.zip")!,
|
||||
sha256Hash: "070e9b7e1f74e7a6474d36253ab5a3623ff93892acc9e1043c3581f2ded12200",
|
||||
version: "1.0")
|
||||
|
||||
static let v1_3 = PluginVersion(url: Bundle.main.url(forResource: "AltPlugin", withExtension: "zip")!,
|
||||
sha256Hash: "6c939d6601ea9793f149e4f6dd4a154e8229a9b9cf7f4bea4a1d6bca7d433512",
|
||||
version: "1.3")
|
||||
}
|
||||
|
||||
class PluginManager
|
||||
{
|
||||
var isMailPluginInstalled: Bool {
|
||||
let isMailPluginInstalled = FileManager.default.fileExists(atPath: pluginURL.path)
|
||||
return isMailPluginInstalled
|
||||
}
|
||||
|
||||
var isUpdateAvailable: Bool {
|
||||
guard let bundle = Bundle(url: pluginURL) else { return false }
|
||||
|
||||
// Load Info.plist from disk because Bundle.infoDictionary is cached by system.
|
||||
let infoDictionaryURL = bundle.bundleURL.appendingPathComponent("Contents/Info.plist")
|
||||
guard let infoDictionary = NSDictionary(contentsOf: infoDictionaryURL) as? [String: Any],
|
||||
let version = infoDictionary["CFBundleShortVersionString"] as? String
|
||||
else { return false }
|
||||
|
||||
let isUpdateAvailable = (version != self.preferredVersion.version)
|
||||
return isUpdateAvailable
|
||||
}
|
||||
|
||||
private var preferredVersion: PluginVersion {
|
||||
if #available(macOS 11, *)
|
||||
{
|
||||
return .v1_3
|
||||
}
|
||||
else
|
||||
{
|
||||
return .v1_0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PluginManager
|
||||
{
|
||||
func installMailPlugin(completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
do
|
||||
{
|
||||
let alert = NSAlert()
|
||||
|
||||
if self.isUpdateAvailable
|
||||
{
|
||||
alert.messageText = NSLocalizedString("Update Mail Plug-in", comment: "")
|
||||
alert.informativeText = NSLocalizedString("An update is available for AltServer's Mail plug-in. Please update the plug-in now in order to keep using AltStore.", comment: "")
|
||||
|
||||
alert.addButton(withTitle: NSLocalizedString("Update Plug-in", comment: ""))
|
||||
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
|
||||
}
|
||||
else
|
||||
{
|
||||
alert.messageText = NSLocalizedString("Install Mail Plug-in", comment: "")
|
||||
alert.informativeText = NSLocalizedString("AltServer requires a Mail plug-in in order to retrieve necessary information about your Apple ID. Would you like to install it now?", comment: "")
|
||||
|
||||
alert.addButton(withTitle: NSLocalizedString("Install Plug-in", comment: ""))
|
||||
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
|
||||
}
|
||||
|
||||
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
|
||||
|
||||
let response = alert.runModal()
|
||||
guard response == .alertFirstButtonReturn else { throw PluginError.cancelled }
|
||||
|
||||
self.downloadPlugin { (result) in
|
||||
do
|
||||
{
|
||||
let fileURL = try result.get()
|
||||
|
||||
// Ensure plug-in directory exists.
|
||||
let authorization = try self.runAndKeepAuthorization("mkdir", arguments: ["-p", pluginDirectoryURL.path])
|
||||
|
||||
// Create temporary directory.
|
||||
let temporaryDirectoryURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
||||
try FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
||||
defer { try? FileManager.default.removeItem(at: temporaryDirectoryURL) }
|
||||
|
||||
// Unzip AltPlugin to temporary directory.
|
||||
try self.runAndKeepAuthorization("unzip", arguments: ["-o", fileURL.path, "-d", temporaryDirectoryURL.path], authorization: authorization)
|
||||
|
||||
if FileManager.default.fileExists(atPath: pluginURL.path)
|
||||
{
|
||||
// Delete existing Mail plug-in.
|
||||
try self.runAndKeepAuthorization("rm", arguments: ["-rf", pluginURL.path], authorization: authorization)
|
||||
}
|
||||
|
||||
// Copy AltPlugin to Mail plug-ins directory.
|
||||
// Must be separate step than unzip to prevent macOS from considering plug-in corrupted.
|
||||
let unzippedPluginURL = temporaryDirectoryURL.appendingPathComponent(pluginURL.lastPathComponent)
|
||||
try self.runAndKeepAuthorization("cp", arguments: ["-R", unzippedPluginURL.path, pluginDirectoryURL.path], authorization: authorization)
|
||||
|
||||
guard self.isMailPluginInstalled else { throw PluginError.unknown }
|
||||
|
||||
// Enable Mail plug-in preferences.
|
||||
try self.run("defaults", arguments: ["write", "/Library/Preferences/com.apple.mail", "EnableBundles", "-bool", "YES"], authorization: authorization)
|
||||
|
||||
print("Finished installing Mail plug-in!")
|
||||
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(PluginError.cancelled))
|
||||
}
|
||||
}
|
||||
|
||||
func uninstallMailPlugin(completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("Uninstall Mail Plug-in", comment: "")
|
||||
alert.informativeText = NSLocalizedString("Are you sure you want to uninstall the AltServer Mail plug-in? You will no longer be able to install or refresh apps with AltStore.", comment: "")
|
||||
|
||||
alert.addButton(withTitle: NSLocalizedString("Uninstall Plug-in", comment: ""))
|
||||
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
|
||||
|
||||
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
|
||||
|
||||
let response = alert.runModal()
|
||||
guard response == .alertFirstButtonReturn else { return completionHandler(.failure(PluginError.cancelled)) }
|
||||
|
||||
DispatchQueue.global().async {
|
||||
do
|
||||
{
|
||||
if FileManager.default.fileExists(atPath: pluginURL.path)
|
||||
{
|
||||
// Delete Mail plug-in from privileged directory.
|
||||
try self.run("rm", arguments: ["-rf", pluginURL.path])
|
||||
}
|
||||
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension PluginManager
|
||||
{
|
||||
func downloadPlugin(completion: @escaping (Result<URL, Error>) -> Void)
|
||||
{
|
||||
let pluginVersion = self.preferredVersion
|
||||
|
||||
func finish(_ result: Result<URL, Error>)
|
||||
{
|
||||
do
|
||||
{
|
||||
let fileURL = try result.get()
|
||||
|
||||
if #available(OSX 10.15, *)
|
||||
{
|
||||
let data = try Data(contentsOf: fileURL)
|
||||
let sha256Hash = SHA256.hash(data: data)
|
||||
let hashString = sha256Hash.compactMap { String(format: "%02x", $0) }.joined()
|
||||
|
||||
print("Comparing Mail plug-in hash (\(hashString)) against expected hash (\(pluginVersion.sha256Hash))...")
|
||||
guard hashString == pluginVersion.sha256Hash else { throw PluginError.mismatchedHash(hash: hashString, expectedHash: pluginVersion.sha256Hash) }
|
||||
}
|
||||
|
||||
completion(.success(fileURL))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
if pluginVersion.url.isFileURL
|
||||
{
|
||||
finish(.success(pluginVersion.url))
|
||||
}
|
||||
else
|
||||
{
|
||||
let downloadTask = URLSession.shared.downloadTask(with: pluginVersion.url) { (fileURL, response, error) in
|
||||
if let response = response as? HTTPURLResponse
|
||||
{
|
||||
guard response.statusCode != 404 else { return finish(.failure(PluginError.notFound)) }
|
||||
}
|
||||
|
||||
let result = Result(fileURL, error)
|
||||
finish(result)
|
||||
|
||||
if let fileURL = fileURL
|
||||
{
|
||||
try? FileManager.default.removeItem(at: fileURL)
|
||||
}
|
||||
}
|
||||
|
||||
downloadTask.resume()
|
||||
}
|
||||
}
|
||||
|
||||
func run(_ program: String, arguments: [String], authorization: AuthorizationRef? = nil) throws
|
||||
{
|
||||
_ = try self._run(program, arguments: arguments, authorization: authorization, freeAuthorization: true)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func runAndKeepAuthorization(_ program: String, arguments: [String], authorization: AuthorizationRef? = nil) throws -> AuthorizationRef
|
||||
{
|
||||
return try self._run(program, arguments: arguments, authorization: authorization, freeAuthorization: false)
|
||||
}
|
||||
|
||||
func _run(_ program: String, arguments: [String], authorization: AuthorizationRef? = nil, freeAuthorization: Bool) throws -> AuthorizationRef
|
||||
{
|
||||
var launchPath = "/usr/bin/" + program
|
||||
if !FileManager.default.fileExists(atPath: launchPath)
|
||||
{
|
||||
launchPath = "/bin/" + program
|
||||
}
|
||||
|
||||
print("Running program:", launchPath)
|
||||
|
||||
let task = STPrivilegedTask()
|
||||
task.launchPath = launchPath
|
||||
task.arguments = arguments
|
||||
task.freeAuthorizationWhenDone = freeAuthorization
|
||||
|
||||
let errorCode: OSStatus
|
||||
|
||||
if let authorization = authorization
|
||||
{
|
||||
errorCode = task.launch(withAuthorization: authorization)
|
||||
}
|
||||
else
|
||||
{
|
||||
errorCode = task.launch()
|
||||
}
|
||||
|
||||
guard errorCode == 0 else { throw PluginError.taskErrorCode(Int(errorCode)) }
|
||||
|
||||
task.waitUntilExit()
|
||||
|
||||
print("Exit code:", task.terminationStatus)
|
||||
|
||||
guard task.terminationStatus == 0 else {
|
||||
let outputData = task.outputFileHandle.readDataToEndOfFile()
|
||||
|
||||
if let outputString = String(data: outputData, encoding: .utf8), !outputString.isEmpty
|
||||
{
|
||||
throw PluginError.taskError(outputString)
|
||||
}
|
||||
|
||||
throw PluginError.taskErrorCode(Int(task.terminationStatus))
|
||||
}
|
||||
|
||||
guard let authorization = task.authorization else { throw PluginError.unknown }
|
||||
return authorization
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,6 @@
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:AltStore.xcodeproj">
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1020"
|
||||
version = "1.3">
|
||||
LastUpgradeVersion = "1610"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
buildImplicitDependencies = "YES"
|
||||
buildArchitectures = "Automatic">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
@@ -14,9 +15,9 @@
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF45868C229872EA00BD7491"
|
||||
BuildableName = "AltServer.app"
|
||||
BlueprintName = "AltServer"
|
||||
BlueprintIdentifier = "BF58047A246A28F7008AE704"
|
||||
BuildableName = "AltBackup.app"
|
||||
BlueprintName = "AltBackup"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
@@ -26,20 +27,8 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF45868C229872EA00BD7491"
|
||||
BuildableName = "AltServer.app"
|
||||
BlueprintName = "AltServer"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
@@ -55,14 +44,42 @@
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF45868C229872EA00BD7491"
|
||||
BuildableName = "AltServer.app"
|
||||
BlueprintName = "AltServer"
|
||||
BlueprintIdentifier = "BF58047A246A28F7008AE704"
|
||||
BuildableName = "AltBackup.app"
|
||||
BlueprintName = "AltBackup"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
<CommandLineArguments>
|
||||
<CommandLineArgument
|
||||
argument = "-com.apple.CoreData.ConcurrencyDebug 1"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "-com.apple.CoreData.MigrationDebug 1"
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "-com.apple.CoreData.SQLiteIntegrityCheck 1"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "-com.apple.CoreData.SQLDebug 1"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
</CommandLineArguments>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "OS_ACTIVITY_MODE"
|
||||
value = "$(DEBUG_ACTIVITY_MODE)"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
<EnvironmentVariable
|
||||
key = "OBJC_DEBUG_DUPLICATE_CLASSES"
|
||||
value = "$(DEBUG_DUPLICATE_CLASSES)"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
@@ -74,9 +91,9 @@
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF45868C229872EA00BD7491"
|
||||
BuildableName = "AltServer.app"
|
||||
BlueprintName = "AltServer"
|
||||
BlueprintIdentifier = "BF58047A246A28F7008AE704"
|
||||
BuildableName = "AltBackup.app"
|
||||
BlueprintName = "AltBackup"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
@@ -1,111 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1150"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "NO"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "A7A6DC28A6D60809855FE404C6A3EA29"
|
||||
BuildableName = "libPods-AltDaemon.a"
|
||||
BlueprintName = "Pods-AltDaemon"
|
||||
ReferencedContainer = "container:Pods/Pods.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF1E314F22A0616100370A3C"
|
||||
BuildableName = "libAltKit.a"
|
||||
BlueprintName = "AltKit"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF18BFE624857D7900DD5981"
|
||||
BuildableName = "AltDaemon"
|
||||
BlueprintName = "AltDaemon"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Release"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF18BFE624857D7900DD5981"
|
||||
BuildableName = "AltDaemon"
|
||||
BlueprintName = "AltDaemon"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "THEOS"
|
||||
value = "~/theos"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF18BFE624857D7900DD5981"
|
||||
BuildableName = "AltDaemon"
|
||||
BlueprintName = "AltDaemon"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -1,77 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1230"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BFF7C903257844C900E55F36"
|
||||
BuildableName = "AltXPC.xpc"
|
||||
BlueprintName = "AltXPC"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF45868C229872EA00BD7491"
|
||||
BuildableName = "AltServer.app"
|
||||
BlueprintName = "AltServer"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BFF7C903257844C900E55F36"
|
||||
BuildableName = "AltXPC.xpc"
|
||||
BlueprintName = "AltXPC"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -1,40 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1120"
|
||||
version = "1.3">
|
||||
LastUpgradeVersion = "1620"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF5C5FC4237DF5AE00EDD0C6"
|
||||
BuildableName = "AltPlugin.mailbundle"
|
||||
BlueprintName = "AltPlugin"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
buildImplicitDependencies = "YES"
|
||||
buildArchitectures = "Automatic">
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<TestPlans>
|
||||
<TestPlanReference
|
||||
reference = "container:SideStore/Tests/DataStructureTests.xctestplan"
|
||||
default = "YES">
|
||||
</TestPlanReference>
|
||||
</TestPlans>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "A81A8CC42D68BA610086C96F"
|
||||
BuildableName = "DataStructureTests.xctest"
|
||||
BlueprintName = "DataStructureTests"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "1"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
@@ -47,15 +49,6 @@
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF5C5FC4237DF5AE00EDD0C6"
|
||||
BuildableName = "AltPlugin.mailbundle"
|
||||
BlueprintName = "AltPlugin"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1020"
|
||||
version = "1.3">
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
@@ -15,23 +15,22 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
||||
BuildableName = "AltStore.app"
|
||||
BlueprintName = "AltStore"
|
||||
BuildableName = "SideStore.app"
|
||||
BlueprintName = "SideStore"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
buildConfiguration = "Release"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
buildConfiguration = "Release"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
@@ -45,17 +44,41 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
||||
BuildableName = "AltStore.app"
|
||||
BlueprintName = "AltStore"
|
||||
BuildableName = "SideStore.app"
|
||||
BlueprintName = "SideStore"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<CommandLineArguments>
|
||||
<CommandLineArgument
|
||||
argument = "-com.apple.CoreData.ConcurrencyDebug 1"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "-com.apple.CoreData.MigrationDebug 1"
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "-com.apple.CoreData.SQLiteIntegrityCheck 1"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "-com.apple.CoreData.SQLDebug 1"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
</CommandLineArguments>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "OS_ACTIVITY_MODE"
|
||||
value = "$(DEBUG_ACTIVITY_MODE)"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
<EnvironmentVariable
|
||||
key = "OBJC_DEBUG_DUPLICATE_CLASSES"
|
||||
value = "$(DEBUG_DUPLICATE_CLASSES)"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
@@ -68,14 +91,14 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
||||
BuildableName = "AltStore.app"
|
||||
BlueprintName = "AltStore"
|
||||
BuildableName = "SideStore.app"
|
||||
BlueprintName = "SideStore"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
buildConfiguration = "Release">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
130
AltStore.xcodeproj/xcshareddata/xcschemes/SideStore.xcscheme
Normal file
@@ -0,0 +1,130 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1610"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES"
|
||||
buildArchitectures = "Automatic">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
||||
BuildableName = "SideStore.app"
|
||||
BlueprintName = "SideStore"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<TestPlans>
|
||||
<TestPlanReference
|
||||
reference = "container:SideStore/Tests/SideStoreTests.xctestplan"
|
||||
default = "YES">
|
||||
</TestPlanReference>
|
||||
</TestPlans>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "A8E2DB202D684CBD009E5D31"
|
||||
BuildableName = "UITests.xctest"
|
||||
BlueprintName = "UITests"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
<SelectedTests>
|
||||
<Test
|
||||
Identifier = "UITests/testExample()">
|
||||
</Test>
|
||||
</SelectedTests>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
||||
BuildableName = "SideStore.app"
|
||||
BlueprintName = "SideStore"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<CommandLineArguments>
|
||||
<CommandLineArgument
|
||||
argument = "-com.apple.CoreData.ConcurrencyDebug 1"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "-com.apple.CoreData.MigrationDebug 1"
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "-com.apple.CoreData.SQLiteIntegrityCheck 1"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "-com.apple.CoreData.SQLDebug 1"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
</CommandLineArguments>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "OS_ACTIVITY_MODE"
|
||||
value = "$(DEBUG_ACTIVITY_MODE)"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
<EnvironmentVariable
|
||||
key = "OBJC_DEBUG_DUPLICATE_CLASSES"
|
||||
value = "$(DEBUG_DUPLICATE_CLASSES)"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
||||
BuildableName = "SideStore.app"
|
||||
BlueprintName = "SideStore"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
4
AltStore.xcworkspace/contents.xcworkspacedata
generated
@@ -2,10 +2,10 @@
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "container:AltStore.xcodeproj">
|
||||
location = "group:AltStore.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Dependencies/AltSign">
|
||||
location = "group:SideStore/AltSign">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Dependencies/Roxas/Roxas.xcodeproj">
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
<?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>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -3,3 +3,6 @@
|
||||
//
|
||||
|
||||
#import "NSAttributedString+Markdown.h"
|
||||
#import "ALTAppPatcher.h"
|
||||
|
||||
#include "fragmentzip.h"
|
||||
|
||||
@@ -2,13 +2,27 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<!-- <key>com.apple.security.files.user-selected.read-write</key>
|
||||
<array>
|
||||
<string></string>
|
||||
</array>
|
||||
<key>com.apple.developer.applesignin</key>
|
||||
<array>
|
||||
<string></string>
|
||||
</array> -->
|
||||
<key>com.apple.developer.kernel.extended-virtual-addressing</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.kernel.increased-debugging-memory-limit</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.kernel.increased-memory-limit</key>
|
||||
<true/>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.siri</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.rileytestut.AltStore</string>
|
||||
<string>group.$(APP_GROUP_IDENTIFIER)</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -15,11 +15,11 @@ import AppCenterAnalytics
|
||||
import AppCenterCrashes
|
||||
|
||||
#if DEBUG
|
||||
private let appCenterAppSecret = "bb08e9bb-c126-408d-bf3f-324c8473fd40"
|
||||
private let appCenterAppSecret = "73532d3e-e573-4693-99a4-9f85840bbb44"
|
||||
#elseif RELEASE
|
||||
private let appCenterAppSecret = "b6718932-294a-432b-81f2-be1e17ff85c5"
|
||||
private let appCenterAppSecret = "73532d3e-e573-4693-99a4-9f85840bbb44"
|
||||
#else
|
||||
private let appCenterAppSecret = "e873f6ca-75eb-4685-818f-801e0e375d60"
|
||||
private let appCenterAppSecret = "73532d3e-e573-4693-99a4-9f85840bbb44"
|
||||
#endif
|
||||
|
||||
extension AnalyticsManager
|
||||
@@ -30,10 +30,14 @@ extension AnalyticsManager
|
||||
case bundleIdentifier
|
||||
case developerName
|
||||
case version
|
||||
case buildVersion
|
||||
case size
|
||||
case tintColor
|
||||
case sourceIdentifier
|
||||
case sourceURL
|
||||
case patreonURL
|
||||
case pledgeAmount
|
||||
case pledgeCurrency
|
||||
}
|
||||
|
||||
enum Event
|
||||
@@ -65,10 +69,14 @@ extension AnalyticsManager
|
||||
.bundleIdentifier: app.bundleIdentifier,
|
||||
.developerName: app.storeApp?.developerName,
|
||||
.version: app.version,
|
||||
.buildVersion: app.buildVersion,
|
||||
.size: appBundleSize?.description,
|
||||
.tintColor: app.storeApp?.tintColor?.hexString,
|
||||
.sourceIdentifier: app.storeApp?.sourceIdentifier,
|
||||
.sourceURL: app.storeApp?.source?.sourceURL.absoluteString
|
||||
.sourceURL: app.storeApp?.source?.sourceURL.absoluteString,
|
||||
.patreonURL: app.storeApp?.source?.patreonURL?.absoluteString,
|
||||
.pledgeAmount: app.storeApp?.pledgeAmount?.description,
|
||||
.pledgeCurrency: app.storeApp?.pledgeCurrency
|
||||
]
|
||||
}
|
||||
|
||||
@@ -77,7 +85,7 @@ extension AnalyticsManager
|
||||
}
|
||||
}
|
||||
|
||||
class AnalyticsManager
|
||||
final class AnalyticsManager
|
||||
{
|
||||
static let shared = AnalyticsManager()
|
||||
|
||||
@@ -90,9 +98,9 @@ extension AnalyticsManager
|
||||
{
|
||||
func start()
|
||||
{
|
||||
MSAppCenter.start(appCenterAppSecret, withServices:[
|
||||
MSAnalytics.self,
|
||||
MSCrashes.self
|
||||
AppCenter.start(withAppSecret: appCenterAppSecret, services: [
|
||||
Analytics.self,
|
||||
Crashes.self
|
||||
])
|
||||
}
|
||||
|
||||
@@ -102,6 +110,6 @@ extension AnalyticsManager
|
||||
properties[item.key.rawValue] = item.value
|
||||
}
|
||||
|
||||
MSAnalytics.trackEvent(event.name, withProperties: properties)
|
||||
Analytics.trackEvent(event.name, withProperties: properties)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,13 +25,11 @@ extension AppContentViewController
|
||||
}
|
||||
}
|
||||
|
||||
class AppContentViewController: UITableViewController
|
||||
final class AppContentViewController: UITableViewController
|
||||
{
|
||||
var app: StoreApp!
|
||||
|
||||
private lazy var screenshotsDataSource = self.makeScreenshotsDataSource()
|
||||
private lazy var permissionsDataSource = self.makePermissionsDataSource()
|
||||
|
||||
// private lazy var screenshotsDataSource = self.makeScreenshotsDataSource()
|
||||
private lazy var dateFormatter: DateFormatter = {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateStyle = .medium
|
||||
@@ -45,137 +43,113 @@ class AppContentViewController: UITableViewController
|
||||
}()
|
||||
|
||||
@IBOutlet private var subtitleLabel: UILabel!
|
||||
@IBOutlet private var descriptionTextView: CollapsingTextView!
|
||||
@IBOutlet private var versionDescriptionTextView: CollapsingTextView!
|
||||
// @IBOutlet private var descriptionTextView: CollapsingTextView!
|
||||
@IBOutlet private var descriptionTextView: CollapsingMarkdownView!
|
||||
// @IBOutlet private var versionDescriptionTextView: CollapsingTextView!
|
||||
@IBOutlet private var versionDescriptionTextView: CollapsingMarkdownView!
|
||||
@IBOutlet private var versionLabel: UILabel!
|
||||
@IBOutlet private var versionDateLabel: UILabel!
|
||||
@IBOutlet private var sizeLabel: UILabel!
|
||||
|
||||
@IBOutlet private var screenshotsCollectionView: UICollectionView!
|
||||
@IBOutlet private var permissionsCollectionView: UICollectionView!
|
||||
@IBOutlet private(set) var appScreenshotsViewController: AppScreenshotsViewController!
|
||||
@IBOutlet private var appScreenshotsHeightConstraint: NSLayoutConstraint!
|
||||
|
||||
var preferredScreenshotSize: CGSize? {
|
||||
let layout = self.screenshotsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
|
||||
|
||||
let aspectRatio: CGFloat = 16.0 / 9.0 // Hardcoded for now.
|
||||
|
||||
let width = self.screenshotsCollectionView.bounds.width - (layout.minimumInteritemSpacing * 2)
|
||||
|
||||
let itemWidth = width / 1.5
|
||||
let itemHeight = itemWidth * aspectRatio
|
||||
|
||||
return CGSize(width: itemWidth, height: itemHeight)
|
||||
}
|
||||
@IBOutlet private(set) var appDetailCollectionViewController: AppDetailCollectionViewController!
|
||||
@IBOutlet private var appDetailCollectionViewHeightConstraint: NSLayoutConstraint!
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
self.tableView.contentInset.bottom = 20
|
||||
|
||||
self.screenshotsCollectionView.dataSource = self.screenshotsDataSource
|
||||
self.screenshotsCollectionView.prefetchDataSource = self.screenshotsDataSource
|
||||
|
||||
self.permissionsCollectionView.dataSource = self.permissionsDataSource
|
||||
|
||||
self.subtitleLabel.text = self.app.subtitle
|
||||
self.descriptionTextView.text = self.app.localizedDescription
|
||||
self.versionDescriptionTextView.text = self.app.versionDescription
|
||||
self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), self.app.version)
|
||||
self.versionDateLabel.text = Date().relativeDateString(since: self.app.versionDate, dateFormatter: self.dateFormatter)
|
||||
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: Int64(self.app.size))
|
||||
let desc = self.app.localizedDescription
|
||||
self.descriptionTextView.text = desc
|
||||
|
||||
if let version = self.app.latestAvailableVersion {
|
||||
self.versionDescriptionTextView.text = version.localizedDescription ?? "nil"
|
||||
self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), version.localizedVersion)
|
||||
self.versionDateLabel.text = Date().relativeDateString(since: version.date)
|
||||
self.sizeLabel.text = ByteCountFormatter.string(fromByteCount: version.size, countStyle: .file)
|
||||
} else {
|
||||
self.versionDescriptionTextView.text = "nil"
|
||||
self.versionLabel.text = nil
|
||||
self.versionDateLabel.text = nil
|
||||
self.sizeLabel.text = ByteCountFormatter.string(fromByteCount: 0, countStyle: .file)
|
||||
}
|
||||
|
||||
self.descriptionTextView.maximumNumberOfLines = 5
|
||||
self.descriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
|
||||
self.versionDescriptionTextView.maximumNumberOfLines = 5
|
||||
|
||||
self.versionDescriptionTextView.maximumNumberOfLines = 3
|
||||
self.versionDescriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
|
||||
self.descriptionTextView.toggleButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
|
||||
self.versionDescriptionTextView.toggleButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews()
|
||||
{
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
guard var size = self.preferredScreenshotSize else { return }
|
||||
size.height = min(size.height, self.screenshotsCollectionView.bounds.height) // Silence temporary "item too tall" warning.
|
||||
var needsTableViewUpdate = false
|
||||
|
||||
let layout = self.screenshotsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
|
||||
layout.itemSize = size
|
||||
}
|
||||
|
||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
|
||||
{
|
||||
guard segue.identifier == "showPermission" else { return }
|
||||
let screenshotsHeight = self.appScreenshotsViewController.collectionView.contentSize.height
|
||||
if self.appScreenshotsHeightConstraint.constant != screenshotsHeight && screenshotsHeight > 0
|
||||
{
|
||||
self.appScreenshotsHeightConstraint.constant = screenshotsHeight
|
||||
needsTableViewUpdate = true
|
||||
}
|
||||
|
||||
guard let cell = sender as? UICollectionViewCell, let indexPath = self.permissionsCollectionView.indexPath(for: cell) else { return }
|
||||
let permissionsHeight = self.appDetailCollectionViewController.collectionView.contentSize.height
|
||||
if self.appDetailCollectionViewHeightConstraint.constant != permissionsHeight && permissionsHeight > 0
|
||||
{
|
||||
self.appDetailCollectionViewHeightConstraint.constant = permissionsHeight
|
||||
needsTableViewUpdate = true
|
||||
}
|
||||
|
||||
let permission = self.permissionsDataSource.item(at: indexPath)
|
||||
|
||||
let maximumWidth = self.view.bounds.width - 20
|
||||
|
||||
let permissionPopoverViewController = segue.destination as! PermissionPopoverViewController
|
||||
permissionPopoverViewController.permission = permission
|
||||
permissionPopoverViewController.view.widthAnchor.constraint(lessThanOrEqualToConstant: maximumWidth).isActive = true
|
||||
|
||||
let size = permissionPopoverViewController.view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
|
||||
permissionPopoverViewController.preferredContentSize = size
|
||||
|
||||
permissionPopoverViewController.popoverPresentationController?.delegate = self
|
||||
permissionPopoverViewController.popoverPresentationController?.sourceRect = cell.frame
|
||||
permissionPopoverViewController.popoverPresentationController?.sourceView = self.permissionsCollectionView
|
||||
if needsTableViewUpdate
|
||||
{
|
||||
UIView.performWithoutAnimation {
|
||||
// Update row height without animation.
|
||||
self.tableView.beginUpdates()
|
||||
self.tableView.endUpdates()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppContentViewController
|
||||
{
|
||||
func makeScreenshotsDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>
|
||||
@IBSegueAction
|
||||
func makeAppScreenshotsViewController(_ coder: NSCoder, sender: Any?) -> UIViewController?
|
||||
{
|
||||
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>(items: self.app.screenshotURLs as [NSURL])
|
||||
dataSource.cellConfigurationHandler = { (cell, screenshot, indexPath) in
|
||||
let cell = cell as! ScreenshotCollectionViewCell
|
||||
cell.imageView.image = nil
|
||||
cell.imageView.isIndicatingActivity = true
|
||||
}
|
||||
dataSource.prefetchHandler = { (imageURL, indexPath, completionHandler) in
|
||||
return RSTAsyncBlockOperation() { (operation) in
|
||||
ImagePipeline.shared.loadImage(with: imageURL as URL, progress: nil, completion: { (response, error) in
|
||||
guard !operation.isCancelled else { return operation.finish() }
|
||||
|
||||
if let image = response?.image
|
||||
{
|
||||
completionHandler(image, nil)
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||
let cell = cell as! ScreenshotCollectionViewCell
|
||||
cell.imageView.isIndicatingActivity = false
|
||||
cell.imageView.image = image
|
||||
let appScreenshotsViewController = AppScreenshotsViewController(app: self.app, coder: coder)
|
||||
self.appScreenshotsViewController = appScreenshotsViewController
|
||||
return appScreenshotsViewController
|
||||
}
|
||||
|
||||
func makePermissionsDataSource() -> RSTArrayCollectionViewDataSource<AppPermission>
|
||||
{
|
||||
let dataSource = RSTArrayCollectionViewDataSource(items: Array(self.app.permissions))
|
||||
dataSource.cellConfigurationHandler = { (cell, permission, indexPath) in
|
||||
let cell = cell as! PermissionCollectionViewCell
|
||||
// cell.button.setImage(permission.type.icon, for: .normal)
|
||||
// cell.button.tintColor = .label
|
||||
// cell.textLabel.text = permission.type.localizedShortName ?? permission.type.localizedName
|
||||
|
||||
if let error = error
|
||||
{
|
||||
print("Error loading image:", error)
|
||||
}
|
||||
let icon = UIImage(systemName: permission.symbolName ?? "lock")
|
||||
cell.button.setImage(icon, for: .normal)
|
||||
|
||||
cell.textLabel.text = permission.localizedDisplayName
|
||||
}
|
||||
|
||||
return dataSource
|
||||
}
|
||||
|
||||
func makePermissionsDataSource() -> RSTArrayCollectionViewDataSource<AppPermission>
|
||||
{
|
||||
let dataSource = RSTArrayCollectionViewDataSource(items: self.app.permissions)
|
||||
dataSource.cellConfigurationHandler = { (cell, permission, indexPath) in
|
||||
let cell = cell as! PermissionCollectionViewCell
|
||||
cell.button.setImage(permission.type.icon, for: .normal)
|
||||
cell.textLabel.text = permission.type.localizedShortName
|
||||
}
|
||||
|
||||
return dataSource
|
||||
@IBSegueAction
|
||||
func makeAppDetailCollectionViewController(_ coder: NSCoder, sender: Any?) -> UIViewController?
|
||||
{
|
||||
let appDetailViewController = AppDetailCollectionViewController(app: self.app, coder: coder)
|
||||
self.appDetailCollectionViewController = appDetailViewController
|
||||
return appDetailViewController
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,8 +161,12 @@ private extension AppContentViewController
|
||||
|
||||
switch sender
|
||||
{
|
||||
case self.descriptionTextView.moreButton: indexPath = IndexPath(row: Row.description.rawValue, section: 0)
|
||||
case self.versionDescriptionTextView.moreButton: indexPath = IndexPath(row: Row.versionDescription.rawValue, section: 0)
|
||||
case self.descriptionTextView.toggleButton:
|
||||
indexPath = IndexPath(row: Row.description.rawValue, section: 0)
|
||||
|
||||
case self.versionDescriptionTextView.toggleButton:
|
||||
indexPath = IndexPath(row: Row.versionDescription.rawValue, section: 0)
|
||||
|
||||
default: return
|
||||
}
|
||||
|
||||
@@ -208,17 +186,18 @@ extension AppContentViewController
|
||||
|
||||
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
|
||||
{
|
||||
guard indexPath.row == Row.screenshots.rawValue else { return super.tableView(tableView, heightForRowAt: indexPath) }
|
||||
|
||||
guard let size = self.preferredScreenshotSize else { return 0.0 }
|
||||
return size.height
|
||||
}
|
||||
}
|
||||
|
||||
extension AppContentViewController: UIPopoverPresentationControllerDelegate
|
||||
{
|
||||
func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle
|
||||
{
|
||||
return .none
|
||||
switch Row.allCases[indexPath.row]
|
||||
{
|
||||
case .screenshots:
|
||||
guard !self.app.allScreenshots.isEmpty else { return 0.0 }
|
||||
return UITableView.automaticDimension
|
||||
|
||||
case .permissions:
|
||||
guard !self.app.permissions.isEmpty else { return 0.0 }
|
||||
return UITableView.automaticDimension
|
||||
|
||||
default:
|
||||
return super.tableView(tableView, heightForRowAt: indexPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
class PermissionCollectionViewCell: UICollectionViewCell
|
||||
final class PermissionCollectionViewCell: UICollectionViewCell
|
||||
{
|
||||
@IBOutlet var button: UIButton!
|
||||
@IBOutlet var textLabel: UILabel!
|
||||
@@ -29,7 +29,7 @@ class PermissionCollectionViewCell: UICollectionViewCell
|
||||
}
|
||||
}
|
||||
|
||||
class AppContentTableViewCell: UITableViewCell
|
||||
final class AppContentTableViewCell: UITableViewCell
|
||||
{
|
||||
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize
|
||||
{
|
||||
|
||||
300
AltStore/App Detail/AppDetailCollectionViewController.swift
Normal file
@@ -0,0 +1,300 @@
|
||||
//
|
||||
// AppDetailCollectionViewController.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 5/5/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
import AltStoreCore
|
||||
import Roxas
|
||||
|
||||
extension AppDetailCollectionViewController
|
||||
{
|
||||
private enum Section: Int
|
||||
{
|
||||
case privacy
|
||||
case knownEntitlements
|
||||
case unknownEntitlements
|
||||
}
|
||||
|
||||
private enum ElementKind: String
|
||||
{
|
||||
case title
|
||||
case button
|
||||
}
|
||||
|
||||
@objc(SafeAreaIgnoringCollectionView)
|
||||
private class SafeAreaIgnoringCollectionView: UICollectionView
|
||||
{
|
||||
override var safeAreaInsets: UIEdgeInsets {
|
||||
get {
|
||||
// Fixes incorrect layout if collection view height is taller than safe area height.
|
||||
return .zero
|
||||
}
|
||||
set {
|
||||
// There MUST be a setter for this to work, even if it does nothing ¯\_(ツ)_/¯
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AppDetailCollectionViewController: UICollectionViewController
|
||||
{
|
||||
let app: StoreApp
|
||||
private let privacyPermissions: [AppPermission]
|
||||
private let knownEntitlementPermissions: [AppPermission]
|
||||
private let unknownEntitlementPermissions: [AppPermission]
|
||||
|
||||
private lazy var dataSource = self.makeDataSource()
|
||||
private lazy var privacyDataSource = self.makePrivacyDataSource()
|
||||
private lazy var entitlementsDataSource = self.makeEntitlementsDataSource()
|
||||
|
||||
private var headerRegistration: UICollectionView.SupplementaryRegistration<UICollectionViewListCell>!
|
||||
|
||||
override var collectionViewLayout: UICollectionViewCompositionalLayout {
|
||||
return self.collectionView.collectionViewLayout as! UICollectionViewCompositionalLayout
|
||||
}
|
||||
|
||||
init?(app: StoreApp, coder: NSCoder)
|
||||
{
|
||||
self.app = app
|
||||
|
||||
let comparator: (AppPermission, AppPermission) -> Bool = { (permissionA, permissionB) -> Bool in
|
||||
switch (permissionA.localizedName, permissionB.localizedName)
|
||||
{
|
||||
case (let nameA?, let nameB?):
|
||||
// Sort by localizedName, if both have one.
|
||||
return nameA.localizedStandardCompare(nameB) == .orderedAscending
|
||||
|
||||
case (nil, nil):
|
||||
// Sort by raw permission value as fallback.
|
||||
return permissionA.permission.rawValue < permissionB.permission.rawValue
|
||||
|
||||
// Sort "known" permissions before "unknown" ones.
|
||||
case (_?, nil): return true
|
||||
case (nil, _?): return false
|
||||
}
|
||||
}
|
||||
|
||||
self.privacyPermissions = app.permissions.filter { $0.type == .privacy }.sorted(by: comparator)
|
||||
|
||||
let entitlementPermissions = app.permissions.lazy.filter { $0.type == .entitlement }
|
||||
self.knownEntitlementPermissions = entitlementPermissions.filter { $0.isKnown }.sorted(by: comparator)
|
||||
self.unknownEntitlementPermissions = entitlementPermissions.filter { !$0.isKnown }.sorted(by: comparator)
|
||||
|
||||
super.init(coder: coder)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
// Allow parent background color to show through.
|
||||
self.collectionView.backgroundColor = nil
|
||||
|
||||
// Match the parent table view margins.
|
||||
self.collectionView.directionalLayoutMargins.leading = 20
|
||||
self.collectionView.directionalLayoutMargins.trailing = 20
|
||||
|
||||
let collectionViewLayout = self.makeLayout()
|
||||
self.collectionView.collectionViewLayout = collectionViewLayout
|
||||
|
||||
self.collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "PrivacyCell")
|
||||
self.collectionView.register(UICollectionViewListCell.self, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
|
||||
|
||||
self.headerRegistration = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) { [weak self] (headerView, elementKind, indexPath) in
|
||||
var configuration = UIListContentConfiguration.plainHeader()
|
||||
|
||||
// Match parent table view section headers.
|
||||
configuration.textProperties.font = UIFont.systemFont(ofSize: 22, weight: .bold) // .boldSystemFont(ofSize:) returns *semi-bold* color smh.
|
||||
configuration.textProperties.color = .label
|
||||
|
||||
switch Section(rawValue: indexPath.section)!
|
||||
{
|
||||
case .privacy: break
|
||||
case .knownEntitlements:
|
||||
configuration.text = nil
|
||||
|
||||
configuration.secondaryTextProperties.font = UIFont.preferredFont(forTextStyle: .callout)
|
||||
configuration.textToSecondaryTextVerticalPadding = 8
|
||||
configuration.secondaryText = NSLocalizedString("Entitlements are additional permissions that grant access to certain system services, including potentially sensitive information.", comment: "")
|
||||
|
||||
case .unknownEntitlements:
|
||||
configuration.text = NSLocalizedString("Other Entitlements", comment: "")
|
||||
|
||||
let action = UIAction(image: UIImage(systemName: "questionmark.circle")) { _ in
|
||||
self?.showUnknownEntitlementsAlert()
|
||||
}
|
||||
|
||||
let helpButton = UIButton(primaryAction: action)
|
||||
let customAccessory = UICellAccessory.customView(configuration: .init(customView: helpButton, placement: .trailing(), tintColor: self?.app.tintColor ?? .altPrimary))
|
||||
headerView.accessories = [customAccessory]
|
||||
}
|
||||
|
||||
headerView.contentConfiguration = configuration
|
||||
headerView.backgroundConfiguration = UIBackgroundConfiguration.clear()
|
||||
}
|
||||
|
||||
self.dataSource.proxy = self
|
||||
self.collectionView.dataSource = self.dataSource
|
||||
self.collectionView.delegate = self
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppDetailCollectionViewController
|
||||
{
|
||||
func makeLayout() -> UICollectionViewCompositionalLayout
|
||||
{
|
||||
let layoutConfig = UICollectionViewCompositionalLayoutConfiguration()
|
||||
layoutConfig.contentInsetsReference = .layoutMargins
|
||||
|
||||
let layout = UICollectionViewCompositionalLayout(sectionProvider: { [privacyPermissions, knownEntitlementPermissions, unknownEntitlementPermissions] (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
|
||||
guard let section = Section(rawValue: sectionIndex) else { return nil }
|
||||
switch section
|
||||
{
|
||||
case .privacy:
|
||||
guard !privacyPermissions.isEmpty, #available(iOS 16, *) else { return nil } // Hide section pre-iOS 16.
|
||||
|
||||
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(50)) // Underestimate height to prevent jumping size abruptly.
|
||||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||
|
||||
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(50))
|
||||
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
|
||||
|
||||
let layoutSection = NSCollectionLayoutSection(group: group)
|
||||
layoutSection.interGroupSpacing = 10
|
||||
return layoutSection
|
||||
|
||||
case .knownEntitlements where !knownEntitlementPermissions.isEmpty: fallthrough
|
||||
case .unknownEntitlements where !unknownEntitlementPermissions.isEmpty:
|
||||
var configuration = UICollectionLayoutListConfiguration(appearance: .plain)
|
||||
configuration.headerMode = .supplementary
|
||||
configuration.showsSeparators = false
|
||||
configuration.backgroundColor = .altBackground
|
||||
|
||||
let layoutSection = NSCollectionLayoutSection.list(using: configuration, layoutEnvironment: layoutEnvironment)
|
||||
layoutSection.contentInsets.top = 4
|
||||
return layoutSection
|
||||
|
||||
case .knownEntitlements, .unknownEntitlements: return nil
|
||||
}
|
||||
}, configuration: layoutConfig)
|
||||
|
||||
return layout
|
||||
}
|
||||
|
||||
func makeDataSource() -> RSTCompositeCollectionViewDataSource<AppPermission>
|
||||
{
|
||||
let dataSource = RSTCompositeCollectionViewDataSource(dataSources: [self.privacyDataSource, self.entitlementsDataSource])
|
||||
return dataSource
|
||||
}
|
||||
|
||||
func makePrivacyDataSource() -> RSTDynamicCollectionViewDataSource<AppPermission>
|
||||
{
|
||||
let dataSource = RSTDynamicCollectionViewDataSource<AppPermission>()
|
||||
dataSource.cellIdentifierHandler = { _ in "PrivacyCell" }
|
||||
dataSource.numberOfSectionsHandler = { 1 }
|
||||
dataSource.cellConfigurationHandler = { [weak self] (cell, _, indexPath) in
|
||||
guard let self, #available(iOS 16, *) else { return }
|
||||
|
||||
cell.contentConfiguration = UIHostingConfiguration {
|
||||
AppPermissionsCard(title: "Privacy",
|
||||
description: "\(self.app.name) may request access to the following:",
|
||||
tintColor: Color(uiColor: self.app.tintColor ?? .altPrimary),
|
||||
permissions: self.privacyPermissions)
|
||||
}
|
||||
.margins(.horizontal, 0)
|
||||
}
|
||||
|
||||
if #available(iOS 16, *)
|
||||
{
|
||||
dataSource.numberOfItemsHandler = { [privacyPermissions] _ in !privacyPermissions.isEmpty ? 1 : 0 }
|
||||
}
|
||||
else
|
||||
{
|
||||
dataSource.numberOfItemsHandler = { _ in 0 }
|
||||
}
|
||||
|
||||
return dataSource
|
||||
}
|
||||
|
||||
func makeEntitlementsDataSource() -> RSTCompositeCollectionViewDataSource<AppPermission>
|
||||
{
|
||||
let knownEntitlementsDataSource = RSTArrayCollectionViewDataSource(items: self.knownEntitlementPermissions)
|
||||
let unknownEntitlementsDataSource = RSTArrayCollectionViewDataSource(items: self.unknownEntitlementPermissions)
|
||||
|
||||
let dataSource = RSTCompositeCollectionViewDataSource(dataSources: [knownEntitlementsDataSource, unknownEntitlementsDataSource])
|
||||
dataSource.cellConfigurationHandler = { [weak self] (cell, appPermission, _) in
|
||||
let cell = cell as! UICollectionViewListCell
|
||||
let tintColor = self?.app.tintColor ?? .altPrimary
|
||||
|
||||
var content = cell.defaultContentConfiguration()
|
||||
content.text = appPermission.localizedDisplayName
|
||||
content.secondaryText = appPermission.permission.rawValue
|
||||
content.secondaryTextProperties.color = .secondaryLabel
|
||||
|
||||
if appPermission.isKnown
|
||||
{
|
||||
content.image = UIImage(systemName: appPermission.effectiveSymbolName)
|
||||
content.imageProperties.tintColor = tintColor
|
||||
|
||||
if #available(iOS 15.4, *) /*, let self */ // Capturing self leads to strong-reference cycle.
|
||||
{
|
||||
let detailAccessory = UICellAccessory.detail(options: .init(tintColor: tintColor)) {
|
||||
self?.showPermissionAlert(for: appPermission)
|
||||
}
|
||||
cell.accessories = [detailAccessory]
|
||||
}
|
||||
}
|
||||
|
||||
cell.contentConfiguration = content
|
||||
cell.backgroundConfiguration = UIBackgroundConfiguration.clear()
|
||||
}
|
||||
|
||||
return dataSource
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppDetailCollectionViewController
|
||||
{
|
||||
func showPermissionAlert(for permission: AppPermission)
|
||||
{
|
||||
let alertController = UIAlertController(title: permission.localizedDisplayName, message: permission.localizedDescription, preferredStyle: .alert)
|
||||
alertController.addAction(.ok)
|
||||
self.present(alertController, animated: true)
|
||||
}
|
||||
|
||||
func showUnknownEntitlementsAlert()
|
||||
{
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Other Entitlements", comment: ""), message: NSLocalizedString("SideStore does not have detailed information for these entitlements.", comment: ""), preferredStyle: .alert)
|
||||
alertController.addAction(.ok)
|
||||
self.present(alertController, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension AppDetailCollectionViewController
|
||||
{
|
||||
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView
|
||||
{
|
||||
let headerView = self.collectionView.dequeueConfiguredReusableSupplementary(using: self.headerRegistration, for: indexPath)
|
||||
return headerView
|
||||
}
|
||||
|
||||
override func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool
|
||||
{
|
||||
return false
|
||||
}
|
||||
|
||||
override func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool
|
||||
{
|
||||
return false
|
||||
}
|
||||
}
|
||||
276
AltStore/App Detail/AppPermissionsCard.swift
Normal file
@@ -0,0 +1,276 @@
|
||||
//
|
||||
// AppPermissionsCard.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 5/4/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
@available(iOS 16, *)
|
||||
extension AppPermissionsCard
|
||||
{
|
||||
private struct TransitionKey: Hashable
|
||||
{
|
||||
static func name(_ permission: Permission) -> TransitionKey {
|
||||
TransitionKey(key: "name", permission: permission)
|
||||
}
|
||||
|
||||
static func icon(_ permission: Permission) -> TransitionKey {
|
||||
TransitionKey(key: "icon", permission: permission)
|
||||
}
|
||||
|
||||
let key: String
|
||||
let permission: Permission
|
||||
|
||||
private init(key: String, permission: Permission)
|
||||
{
|
||||
self.key = key
|
||||
self.permission = permission
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 16, *)
|
||||
struct AppPermissionsCard<Permission: AppPermissionProtocol>: View
|
||||
{
|
||||
let title: LocalizedStringKey
|
||||
let description: LocalizedStringKey
|
||||
let tintColor: Color
|
||||
|
||||
let permissions: [Permission]
|
||||
|
||||
@State
|
||||
private var selectedPermission: Permission?
|
||||
|
||||
@Namespace
|
||||
private var animation
|
||||
|
||||
private var isTitleVisible: Bool {
|
||||
if selectedPermission == nil
|
||||
{
|
||||
// Title should always be visible when showing all permissions.
|
||||
return true
|
||||
}
|
||||
|
||||
// If showing permission details, only show title if there
|
||||
// are more than 2 permissions total to save vertical space.
|
||||
let isTitleVisible = permissions.count > 2
|
||||
return isTitleVisible
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let title = Text(title)
|
||||
.font(.title3)
|
||||
.bold()
|
||||
.minimumScaleFactor(0.1) // Avoid clipping during matchedGeometryEffect animation.
|
||||
|
||||
VStack(spacing: 8) {
|
||||
if isTitleVisible
|
||||
{
|
||||
// If title is visible, place _outside_ `content`
|
||||
// to avoid being covered by permissionDetailView.
|
||||
title
|
||||
}
|
||||
|
||||
let content = VStack(spacing: 8) {
|
||||
if !isTitleVisible
|
||||
{
|
||||
// Place title inside `content` when not visible
|
||||
// so it's covered by permissionDetailView.
|
||||
title
|
||||
}
|
||||
|
||||
VStack(spacing: 20) {
|
||||
Text(description)
|
||||
.font(.subheadline)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Grid(verticalSpacing: 15) {
|
||||
ForEach(permissions, id: \.self) { permission in
|
||||
permissionRow(for: permission)
|
||||
}
|
||||
}
|
||||
|
||||
Text("Tap a permission to learn more.")
|
||||
.font(.subheadline)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
|
||||
if let selectedPermission
|
||||
{
|
||||
// Hide content with overlay to preserve existing size.
|
||||
content.hidden().overlay {
|
||||
permissionDetailView(for: selectedPermission)
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
content
|
||||
}
|
||||
}
|
||||
.overlay(alignment: .topTrailing) {
|
||||
if selectedPermission != nil
|
||||
{
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.imageScale(.medium)
|
||||
}
|
||||
}
|
||||
.multilineTextAlignment(.center)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(20)
|
||||
.overlay {
|
||||
if selectedPermission != nil
|
||||
{
|
||||
// Make entire view tappable when overlay is visible.
|
||||
SwiftUI.Button(action: hidePermission) {
|
||||
VStack {}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
}
|
||||
}
|
||||
.foregroundColor(.secondary) // Vibrancy
|
||||
.background(.regularMaterial) // Blur background for auto-legibility correction.
|
||||
.background(tintColor, in: RoundedRectangle(cornerRadius: 30, style: .continuous))
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func permissionRow(for permission: Permission) -> some View
|
||||
{
|
||||
GridRow {
|
||||
SwiftUI.Button(action: { show(permission) }) {
|
||||
HStack {
|
||||
let text = Text(permission.localizedDisplayName)
|
||||
.font(.body)
|
||||
.bold()
|
||||
.minimumScaleFactor(0.33)
|
||||
.lineLimit(.max) // Setting lineLimit to anything fixes text wrapping at large text sizes.
|
||||
|
||||
let image = Image(systemName: permission.effectiveSymbolName)
|
||||
.gridColumnAlignment(.center)
|
||||
|
||||
if selectedPermission != nil
|
||||
{
|
||||
Label(title: { text }, icon: { image })
|
||||
.hidden()
|
||||
}
|
||||
else
|
||||
{
|
||||
Label {
|
||||
text.matchedGeometryEffect(id: TransitionKey.name(permission), in: animation)
|
||||
} icon: {
|
||||
image.matchedGeometryEffect(id: TransitionKey.icon(permission), in: animation)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "info.circle")
|
||||
.imageScale(.large)
|
||||
}
|
||||
.contentShape(Rectangle()) // Make entire HStack tappable.
|
||||
}
|
||||
}
|
||||
.frame(minHeight: 30) // Make row tall enough to tap.
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func permissionDetailView(for permission: Permission) -> some View
|
||||
{
|
||||
VStack(spacing: 15) {
|
||||
Image(systemName: permission.effectiveSymbolName)
|
||||
.font(.largeTitle)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.matchedGeometryEffect(id: TransitionKey.icon(permission), in: animation)
|
||||
|
||||
Text(permission.localizedDisplayName)
|
||||
.font(.title2)
|
||||
.bold()
|
||||
.minimumScaleFactor(0.1) // Avoid clipping during matchedGeometryEffect animation.
|
||||
.matchedGeometryEffect(id: TransitionKey.name(permission), in: animation)
|
||||
|
||||
if let usageDescription = permission.usageDescription
|
||||
{
|
||||
Text(usageDescription)
|
||||
.font(.subheadline)
|
||||
.minimumScaleFactor(0.75)
|
||||
}
|
||||
}
|
||||
.multilineTextAlignment(.leading)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
|
||||
init(title: LocalizedStringKey, description: LocalizedStringKey, tintColor: Color, permissions: [Permission])
|
||||
{
|
||||
self.init(title: title, description: description, tintColor: tintColor, permissions: permissions, selectedPermission: nil)
|
||||
}
|
||||
|
||||
fileprivate init(title: LocalizedStringKey, description: LocalizedStringKey, tintColor: Color, permissions: [Permission], selectedPermission: Permission? = nil)
|
||||
{
|
||||
self.title = title
|
||||
self.description = description
|
||||
self.tintColor = tintColor
|
||||
self.permissions = permissions
|
||||
|
||||
// Set _selectedPermission directly or else the preview won't detect it.
|
||||
self._selectedPermission = State(initialValue: selectedPermission)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 16, *)
|
||||
private extension AppPermissionsCard
|
||||
{
|
||||
func show(_ permission: Permission)
|
||||
{
|
||||
withAnimation {
|
||||
self.selectedPermission = permission
|
||||
}
|
||||
}
|
||||
|
||||
func hidePermission()
|
||||
{
|
||||
withAnimation {
|
||||
self.selectedPermission = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 16, *)
|
||||
struct AppPermissionsCard_Previews: PreviewProvider
|
||||
{
|
||||
static var previews: some View {
|
||||
let appPermissions = [
|
||||
PreviewAppPermission(permission: ALTAppPrivacyPermission.localNetwork),
|
||||
PreviewAppPermission(permission: ALTAppPrivacyPermission.microphone),
|
||||
PreviewAppPermission(permission: ALTAppPrivacyPermission.photos),
|
||||
PreviewAppPermission(permission: ALTAppPrivacyPermission.camera),
|
||||
PreviewAppPermission(permission: ALTAppPrivacyPermission.faceID),
|
||||
PreviewAppPermission(permission: ALTAppPrivacyPermission.appleMusic),
|
||||
PreviewAppPermission(permission: ALTAppPrivacyPermission.bluetooth),
|
||||
PreviewAppPermission(permission: ALTAppPrivacyPermission.calendars),
|
||||
]
|
||||
|
||||
let tintColor = Color(uiColor: .deltaPrimary!)
|
||||
|
||||
return ForEach(1...8, id: \.self) { index in
|
||||
AppPermissionsCard(title: "Privacy",
|
||||
description: "Delta may request access to the following:",
|
||||
tintColor: tintColor,
|
||||
permissions: Array(appPermissions.prefix(index)))
|
||||
.frame(width: 350)
|
||||
.previewLayout(.sizeThatFits)
|
||||
|
||||
AppPermissionsCard(title: "Privacy",
|
||||
description: "Delta may request access to the following:",
|
||||
tintColor: tintColor,
|
||||
permissions: Array(appPermissions.prefix(index)),
|
||||
selectedPermission: appPermissions.first)
|
||||
.frame(width: 350)
|
||||
.previewLayout(.sizeThatFits)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import Roxas
|
||||
|
||||
import Nuke
|
||||
|
||||
class AppViewController: UIViewController
|
||||
final class AppViewController: UIViewController
|
||||
{
|
||||
var app: StoreApp!
|
||||
|
||||
@@ -42,13 +42,22 @@ class AppViewController: UIViewController
|
||||
@IBOutlet private var navigationBarAppNameLabel: UILabel!
|
||||
|
||||
private var _shouldResetLayout = false
|
||||
private var _viewDidAppear = false
|
||||
private var _backgroundBlurEffect: UIBlurEffect?
|
||||
private var _backgroundBlurTintColor: UIColor?
|
||||
|
||||
private var _preferredStatusBarStyle: UIStatusBarStyle = .default
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
return _preferredStatusBarStyle
|
||||
if #available(iOS 17, *)
|
||||
{
|
||||
// On iOS 17+, .default will update the status bar automatically.
|
||||
return .default
|
||||
}
|
||||
else
|
||||
{
|
||||
return _preferredStatusBarStyle
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidLoad()
|
||||
@@ -58,6 +67,11 @@ class AppViewController: UIViewController
|
||||
self.navigationBarTitleView.sizeToFit()
|
||||
self.navigationItem.titleView = self.navigationBarTitleView
|
||||
|
||||
// spacing in storyboard wasn't working, so had to do programatically
|
||||
if let stackView = self.navigationBarTitleView as? UIStackView {
|
||||
stackView.spacing = 8
|
||||
}
|
||||
|
||||
self.contentViewControllerShadowView = UIView()
|
||||
self.contentViewControllerShadowView.backgroundColor = .white
|
||||
self.contentViewControllerShadowView.layer.cornerRadius = 38
|
||||
@@ -73,6 +87,7 @@ class AppViewController: UIViewController
|
||||
self.contentViewController.view.layer.masksToBounds = true
|
||||
|
||||
self.contentViewController.tableView.panGestureRecognizer.require(toFail: self.scrollView.panGestureRecognizer)
|
||||
self.contentViewController.appDetailCollectionViewController.collectionView.panGestureRecognizer.require(toFail: self.scrollView.panGestureRecognizer)
|
||||
self.contentViewController.tableView.showsVerticalScrollIndicator = false
|
||||
|
||||
// Bring to front so the scroll indicators are visible.
|
||||
@@ -86,15 +101,12 @@ class AppViewController: UIViewController
|
||||
self.bannerView.iconImageView.tintColor = self.app.tintColor
|
||||
self.bannerView.button.tintColor = self.app.tintColor
|
||||
self.bannerView.tintColor = self.app.tintColor
|
||||
|
||||
self.bannerView.configure(for: self.app)
|
||||
self.bannerView.accessibilityTraits.remove(.button)
|
||||
|
||||
self.bannerView.button.addTarget(self, action: #selector(AppViewController.performAppAction(_:)), for: .primaryActionTriggered)
|
||||
|
||||
self.backButtonContainerView.tintColor = self.app.tintColor
|
||||
|
||||
self.navigationController?.navigationBar.tintColor = self.app.tintColor
|
||||
self.navigationBarDownloadButton.tintColor = self.app.tintColor
|
||||
self.navigationBarAppNameLabel.text = self.app.name
|
||||
self.navigationBarAppIconImageView.tintColor = self.app.tintColor
|
||||
@@ -118,13 +130,17 @@ class AppViewController: UIViewController
|
||||
{
|
||||
imageView.isIndicatingActivity = true
|
||||
|
||||
Nuke.loadImage(with: self.app.iconURL, options: .shared, into: imageView, progress: nil) { [weak imageView] (response, error) in
|
||||
if response?.image != nil
|
||||
Nuke.loadImage(with: self.app.iconURL, options: .shared, into: imageView, progress: nil) { [weak imageView] (result) in
|
||||
switch result
|
||||
{
|
||||
imageView?.isIndicatingActivity = false
|
||||
case .success: imageView?.isIndicatingActivity = false
|
||||
case .failure(let error): print("[ALTLog] Failed to load app icons.", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start with navigation bar hidden.
|
||||
self.hideNavigationBar()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool)
|
||||
@@ -136,42 +152,26 @@ class AppViewController: UIViewController
|
||||
// Update blur immediately.
|
||||
self.view.setNeedsLayout()
|
||||
self.view.layoutIfNeeded()
|
||||
|
||||
self.transitionCoordinator?.animate(alongsideTransition: { (context) in
|
||||
self.hideNavigationBar()
|
||||
}, completion: nil)
|
||||
}
|
||||
|
||||
override func viewIsAppearing(_ animated: Bool)
|
||||
{
|
||||
super.viewIsAppearing(animated)
|
||||
|
||||
// Prevent banner temporarily flashing a color due to being added back to self.view.
|
||||
self.bannerView.backgroundEffectView.backgroundColor = .clear
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool)
|
||||
{
|
||||
super.viewDidAppear(animated)
|
||||
self._viewDidAppear = true
|
||||
|
||||
self._shouldResetLayout = true
|
||||
self.view.setNeedsLayout()
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool)
|
||||
{
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
// Guard against "dismissing" when presenting via 3D Touch pop.
|
||||
guard self.navigationController != nil else { return }
|
||||
|
||||
// Store reference since self.navigationController will be nil after disappearing.
|
||||
let navigationController = self.navigationController
|
||||
navigationController?.navigationBar.barStyle = .default // Don't animate, or else status bar might appear messed-up.
|
||||
|
||||
self.transitionCoordinator?.animate(alongsideTransition: { (context) in
|
||||
self.showNavigationBar(for: navigationController)
|
||||
}, completion: { (context) in
|
||||
if !context.isCancelled
|
||||
{
|
||||
self.showNavigationBar(for: navigationController)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool)
|
||||
{
|
||||
super.viewDidDisappear(animated)
|
||||
@@ -188,6 +188,12 @@ class AppViewController: UIViewController
|
||||
|
||||
self.contentViewController = segue.destination as? AppContentViewController
|
||||
self.contentViewController.app = self.app
|
||||
|
||||
if #available(iOS 15, *)
|
||||
{
|
||||
// Fix navigation bar + tab bar appearance on iOS 15.
|
||||
self.setContentScrollView(self.scrollView)
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews()
|
||||
@@ -198,11 +204,6 @@ class AppViewController: UIViewController
|
||||
{
|
||||
// Various events can cause UI to mess up, so reset affected components now.
|
||||
|
||||
if self.navigationController?.topViewController == self
|
||||
{
|
||||
self.hideNavigationBar()
|
||||
}
|
||||
|
||||
self.prepareBlur()
|
||||
|
||||
// Reset navigation bar animation, and create a new one later in this method if necessary.
|
||||
@@ -210,8 +211,22 @@ class AppViewController: UIViewController
|
||||
|
||||
self._shouldResetLayout = false
|
||||
}
|
||||
|
||||
let statusBarHeight = UIApplication.shared.statusBarFrame.height
|
||||
|
||||
let statusBarHeight: Double
|
||||
|
||||
if let navigationController, navigationController.presentingViewController != nil, navigationController.modalPresentationStyle != .fullScreen
|
||||
{
|
||||
statusBarHeight = 20
|
||||
}
|
||||
else if let statusBarManager = (self.view.window ?? self.presentedViewController?.view.window)?.windowScene?.statusBarManager
|
||||
{
|
||||
statusBarHeight = statusBarManager.statusBarFrame.height
|
||||
}
|
||||
else
|
||||
{
|
||||
statusBarHeight = 0
|
||||
}
|
||||
|
||||
let cornerRadius = self.contentViewControllerShadowView.layer.cornerRadius
|
||||
|
||||
let inset = 12 as CGFloat
|
||||
@@ -270,13 +285,25 @@ class AppViewController: UIViewController
|
||||
}
|
||||
|
||||
let difference = self.scrollView.contentOffset.y - showNavigationBarThreshold
|
||||
let range = (headerFrame.height + padding) - (self.navigationController?.navigationBar.bounds.height ?? self.view.safeAreaInsets.top)
|
||||
|
||||
let range: Double
|
||||
if self.presentingViewController == nil && self.parent?.presentingViewController == nil
|
||||
{
|
||||
// Not presented modally, so rely on safe area + navigation bar height.
|
||||
range = (headerFrame.height + padding) - (self.navigationController?.navigationBar.bounds.height ?? self.view.safeAreaInsets.top)
|
||||
}
|
||||
else
|
||||
{
|
||||
// Presented modally, so rely on maximumContentY.
|
||||
range = maximumContentY - (maximumContentY - padding - headerFrame.height) - inset
|
||||
}
|
||||
|
||||
let fractionComplete = min(difference, range) / range
|
||||
self.navigationBarAnimator?.fractionComplete = fractionComplete
|
||||
}
|
||||
else
|
||||
{
|
||||
self.navigationBarAnimator?.fractionComplete = 0.0
|
||||
self.resetNavigationBarAnimation()
|
||||
}
|
||||
|
||||
@@ -316,7 +343,7 @@ class AppViewController: UIViewController
|
||||
|
||||
self.backButtonContainerView.layer.cornerRadius = self.backButtonContainerView.bounds.midY
|
||||
|
||||
self.scrollView.scrollIndicatorInsets.top = statusBarHeight
|
||||
self.scrollView.verticalScrollIndicatorInsets.top = statusBarHeight
|
||||
|
||||
// Adjust content offset + size.
|
||||
let contentOffset = self.scrollView.contentOffset
|
||||
@@ -333,7 +360,11 @@ class AppViewController: UIViewController
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?)
|
||||
{
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
self._shouldResetLayout = true
|
||||
|
||||
if self._viewDidAppear
|
||||
{
|
||||
self._shouldResetLayout = true
|
||||
}
|
||||
}
|
||||
|
||||
deinit
|
||||
@@ -345,7 +376,7 @@ class AppViewController: UIViewController
|
||||
|
||||
extension AppViewController
|
||||
{
|
||||
class func makeAppViewController(app: StoreApp) -> AppViewController
|
||||
final class func makeAppViewController(app: StoreApp) -> AppViewController
|
||||
{
|
||||
let storyboard = UIStoryboard(name: "Main", bundle: nil)
|
||||
|
||||
@@ -359,46 +390,40 @@ private extension AppViewController
|
||||
{
|
||||
func update()
|
||||
{
|
||||
var buttonAction: AppBannerView.AppAction?
|
||||
|
||||
// if let installedApp = self.app.installedApp, let latestVersion = self.app.latestAvailableVersion, !installedApp.matches(latestVersion), !self.app.isPledgeRequired || self.app.isPledged
|
||||
if let installedApp = self.app.installedApp, installedApp.hasUpdate
|
||||
{
|
||||
// Explicitly set button action to .update if there is an update available, even if it's not supported.
|
||||
buttonAction = .update
|
||||
}
|
||||
|
||||
for button in [self.bannerView.button!, self.navigationBarDownloadButton!]
|
||||
{
|
||||
button.tintColor = self.app.tintColor
|
||||
button.isIndicatingActivity = false
|
||||
|
||||
if self.app.installedApp == nil
|
||||
{
|
||||
button.setTitle(NSLocalizedString("FREE", comment: ""), for: .normal)
|
||||
}
|
||||
else
|
||||
{
|
||||
button.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal)
|
||||
}
|
||||
|
||||
let progress = AppManager.shared.installationProgress(for: self.app)
|
||||
button.progress = progress
|
||||
}
|
||||
|
||||
if Date() < self.app.versionDate
|
||||
{
|
||||
self.bannerView.button.countdownDate = self.app.versionDate
|
||||
self.navigationBarDownloadButton.countdownDate = self.app.versionDate
|
||||
}
|
||||
else
|
||||
{
|
||||
self.bannerView.button.countdownDate = nil
|
||||
self.navigationBarDownloadButton.countdownDate = nil
|
||||
}
|
||||
self.bannerView.configure(for: self.app, action: buttonAction)
|
||||
|
||||
let title = self.bannerView.button.title(for: .normal)
|
||||
self.navigationBarDownloadButton.setTitle(title, for: .normal)
|
||||
self.navigationBarDownloadButton.progress = self.bannerView.button.progress
|
||||
self.navigationBarDownloadButton.countdownDate = self.bannerView.button.countdownDate
|
||||
|
||||
let barButtonItem = self.navigationItem.rightBarButtonItem
|
||||
self.navigationItem.rightBarButtonItem = nil
|
||||
self.navigationItem.rightBarButtonItem = barButtonItem
|
||||
}
|
||||
|
||||
func showNavigationBar(for navigationController: UINavigationController? = nil)
|
||||
func showNavigationBar()
|
||||
{
|
||||
let navigationController = navigationController ?? self.navigationController
|
||||
navigationController?.navigationBar.alpha = 1.0
|
||||
navigationController?.navigationBar.tintColor = .altPrimary
|
||||
navigationController?.navigationBar.setNeedsLayout()
|
||||
self.navigationBarAppIconImageView.alpha = 1.0
|
||||
self.navigationBarAppNameLabel.alpha = 1.0
|
||||
self.navigationBarDownloadButton.alpha = 1.0
|
||||
|
||||
self.updateNavigationBarAppearance(isHidden: false)
|
||||
|
||||
if self.traitCollection.userInterfaceStyle == .dark
|
||||
{
|
||||
@@ -409,16 +434,51 @@ private extension AppViewController
|
||||
self._preferredStatusBarStyle = .default
|
||||
}
|
||||
|
||||
navigationController?.setNeedsStatusBarAppearanceUpdate()
|
||||
if #unavailable(iOS 17)
|
||||
{
|
||||
self.navigationController?.setNeedsStatusBarAppearanceUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
func hideNavigationBar(for navigationController: UINavigationController? = nil)
|
||||
func hideNavigationBar()
|
||||
{
|
||||
let navigationController = navigationController ?? self.navigationController
|
||||
navigationController?.navigationBar.alpha = 0.0
|
||||
self.navigationBarAppIconImageView.alpha = 0.0
|
||||
self.navigationBarAppNameLabel.alpha = 0.0
|
||||
self.navigationBarDownloadButton.alpha = 0.0
|
||||
|
||||
self.updateNavigationBarAppearance(isHidden: true)
|
||||
|
||||
self._preferredStatusBarStyle = .lightContent
|
||||
navigationController?.setNeedsStatusBarAppearanceUpdate()
|
||||
|
||||
if #unavailable(iOS 17)
|
||||
{
|
||||
self.navigationController?.setNeedsStatusBarAppearanceUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
// Copied from HeaderContentViewController
|
||||
func updateNavigationBarAppearance(isHidden: Bool)
|
||||
{
|
||||
let barAppearance = self.navigationItem.standardAppearance as? NavigationBarAppearance ?? NavigationBarAppearance()
|
||||
|
||||
if isHidden
|
||||
{
|
||||
barAppearance.configureWithTransparentBackground()
|
||||
barAppearance.ignoresUserInteraction = true
|
||||
}
|
||||
else
|
||||
{
|
||||
barAppearance.configureWithDefaultBackground()
|
||||
barAppearance.ignoresUserInteraction = false
|
||||
}
|
||||
|
||||
barAppearance.titleTextAttributes = [.foregroundColor: UIColor.clear]
|
||||
|
||||
let tintColor = isHidden ? UIColor.clear : self.app.tintColor ?? .altPrimary
|
||||
barAppearance.configureWithTintColor(tintColor)
|
||||
|
||||
self.navigationItem.standardAppearance = barAppearance
|
||||
self.navigationItem.scrollEdgeAppearance = barAppearance
|
||||
}
|
||||
|
||||
func prepareBlur()
|
||||
@@ -446,8 +506,10 @@ private extension AppViewController
|
||||
|
||||
self.navigationBarAnimator = UIViewPropertyAnimator(duration: 1.0, curve: .linear) { [weak self] in
|
||||
self?.showNavigationBar()
|
||||
self?.navigationController?.navigationBar.tintColor = self?.app.tintColor
|
||||
self?.navigationController?.navigationBar.barTintColor = nil
|
||||
|
||||
// Must call layoutIfNeeded() to animate appearance change.
|
||||
self?.navigationController?.navigationBar.layoutIfNeeded()
|
||||
|
||||
self?.contentViewController.view.layer.cornerRadius = 0
|
||||
}
|
||||
|
||||
@@ -459,6 +521,8 @@ private extension AppViewController
|
||||
|
||||
func resetNavigationBarAnimation()
|
||||
{
|
||||
guard self.navigationBarAnimator != nil else { return }
|
||||
|
||||
self.navigationBarAnimator?.stopAnimation(true)
|
||||
self.navigationBarAnimator = nil
|
||||
|
||||
@@ -479,7 +543,15 @@ extension AppViewController
|
||||
{
|
||||
if let installedApp = self.app.installedApp
|
||||
{
|
||||
self.open(installedApp)
|
||||
// if let latestVersion = self.app.latestAvailableVersion, !installedApp.matches(latestVersion), !self.app.isPledgeRequired || self.app.isPledged
|
||||
if let latestVersion = self.app.latestAvailableVersion, installedApp.hasUpdate
|
||||
{
|
||||
self.updateApp(installedApp, to: latestVersion)
|
||||
}
|
||||
else
|
||||
{
|
||||
self.open(installedApp)
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -491,38 +563,72 @@ extension AppViewController
|
||||
{
|
||||
guard self.app.installedApp == nil else { return }
|
||||
|
||||
let progress = AppManager.shared.install(self.app, presentingViewController: self) { (result) in
|
||||
do
|
||||
{
|
||||
_ = try result.get()
|
||||
}
|
||||
catch OperationError.cancelled
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
catch
|
||||
{
|
||||
Task<Void, Never>(priority: .userInitiated) {
|
||||
let group = await AppManager.shared.installAsync(self.app, presentingViewController: self) { (result) in
|
||||
do
|
||||
{
|
||||
_ = try result.get()
|
||||
}
|
||||
catch OperationError.cancelled
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
catch
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
let toastView = ToastView(error: error)
|
||||
toastView.opensErrorLog = true
|
||||
toastView.show(in: self)
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let toastView = ToastView(error: error)
|
||||
toastView.show(in: self)
|
||||
self.bannerView.button.progress = nil
|
||||
self.navigationBarDownloadButton.progress = nil
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.bannerView.button.progress = nil
|
||||
self.navigationBarDownloadButton.progress = nil
|
||||
self.update()
|
||||
if !group.progress.isCancelled
|
||||
{
|
||||
self.bannerView.button.progress = group.progress
|
||||
self.navigationBarDownloadButton.progress = group.progress
|
||||
}
|
||||
}
|
||||
|
||||
self.bannerView.button.progress = progress
|
||||
self.navigationBarDownloadButton.progress = progress
|
||||
}
|
||||
|
||||
func open(_ installedApp: InstalledApp)
|
||||
{
|
||||
UIApplication.shared.open(installedApp.openAppURL)
|
||||
}
|
||||
|
||||
func updateApp(_ installedApp: InstalledApp, to version: AppVersion)
|
||||
{
|
||||
let previousProgress = AppManager.shared.installationProgress(for: installedApp)
|
||||
guard previousProgress == nil else {
|
||||
//TODO: Handle cancellation
|
||||
//previousProgress?.cancel()
|
||||
return
|
||||
}
|
||||
|
||||
AppManager.shared.update(installedApp, to: version, presentingViewController: self) { (result) in
|
||||
DispatchQueue.main.async {
|
||||
switch result
|
||||
{
|
||||
case .success: print("Updated app from AppViewController:", installedApp.bundleIdentifier)
|
||||
case .failure(OperationError.cancelled): break
|
||||
case .failure(let error):
|
||||
let toastView = ToastView(error: error)
|
||||
toastView.opensErrorLog = true
|
||||
toastView.show(in: self)
|
||||
}
|
||||
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppViewController
|
||||
|
||||
@@ -10,7 +10,7 @@ import UIKit
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
class PermissionPopoverViewController: UIViewController
|
||||
final class PermissionPopoverViewController: UIViewController
|
||||
{
|
||||
var permission: AppPermission!
|
||||
|
||||
@@ -21,7 +21,7 @@ class PermissionPopoverViewController: UIViewController
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
self.nameLabel.text = self.permission.type.localizedName
|
||||
self.nameLabel.text = self.permission.localizedName ?? self.permission.permission.rawValue
|
||||
self.descriptionLabel.text = self.permission.usageDescription
|
||||
}
|
||||
}
|
||||
|
||||