Compare commits

..

164 Commits

Author SHA1 Message Date
Spidy123222
94ee08c718 Enable building for connect branch.
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2024-01-12 19:00:06 -08:00
Spidy123222
fd7dc94975 Swap to Connect Source
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2024-01-12 18:57:34 -08:00
naturecodevoid
b78707808d [skip ci] Remove me from feature report assignees
Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-12-12 18:32:21 -08:00
naturecodevoid
d41518581a [skip ci] Remove me from bug report assignees
Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-12-12 18:32:05 -08:00
Stern
4abbfe6142 [skip ci] ci: Update stable.yml
This removes workflow_dispatch as it was making the builds show as SideStore `develop`

Signed-off-by: Stern <xsternent@gmail.com>
2023-11-28 05:50:44 -05:00
Stern
dae813d80c Merge pull request #550 from SideStore/Add-description-for-idle
Add description on what disable idle timeout toggle + change add to Siri text for accuracy
2023-11-28 03:12:22 -05:00
Spidy123222
af89b178ad Change button text for adding Siri to refresh apps
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-11-27 23:39:20 -08:00
Spidy123222
8c269207fd add description on what disable idle timeout toggle
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-11-27 23:18:36 -08:00
junepark678
42ecd38517 bugfix(NoIdle): Fix slider not being set to correct value on load 2023-11-28 12:00:20 +09:00
June Park
9f7d4dee49 [skip ci] bump nightly to 0.5.5
Signed-off-by: June Park <rjp2030@outlook.com>
2023-11-28 03:02:04 +09:00
June P
458b8e491e bugfix: fix removal of attributes 2023-11-28 02:34:15 +09:00
Stern
495e621e69 Merge pull request #539 from junepark678/NoIdle
feat(Operations): don't idle timeout during installations
2023-11-27 12:12:10 -05:00
June P
c986512b5f bugfix: fix appending to a list that is nil 2023-11-28 02:04:14 +09:00
June Park
d277754ae5 Merge branch 'develop' into NoIdle
Signed-off-by: June Park <rjp2030@outlook.com>
2023-11-28 00:46:48 +09:00
junepark678
2ef2e2f26b bugfix: make it able to be toggled, fix bug in crash on installation 2023-11-28 00:44:47 +09:00
nythepegasus
23a53034fa [skip ci] Bump nightly to 0.5.4
Signed-off-by: nythepegasus <nythepegasus84@gmail.com>
2023-11-27 10:39:41 -05:00
Stern
ce57d72a78 [skip ci] ci: add workflow_dispatch
Signed-off-by: Stern <xsternent@gmail.com>
2023-11-27 10:18:06 -05:00
nythepegasus
502b89d890 [skip ci] Add other project maintainers to CODEOWNERS
Signed-off-by: nythepegasus <nythepegasus84@gmail.com>
2023-11-27 09:37:25 -05:00
junepark678
5f0015fad0 chore(Clear Cache): do proper error handling 2023-11-27 09:31:36 -05:00
June Park
c81236957b bugfix(settings): fix rounding issues on clear cache button (#536) 2023-11-27 09:31:36 -05:00
Spidy123222
970ab38b27 move debug row 2023-11-27 09:31:36 -05:00
Spidy123222
8a5c31b81d make button function again 2023-11-27 09:31:36 -05:00
Spidy123222
8508fe79b5 change order of settings debug section 2023-11-27 09:31:36 -05:00
Spidy123222
3859e98801 Add button to storyboard 🙄 2023-11-27 09:31:36 -05:00
Spidy123222
a759c7be9e please fix to show button 2023-11-27 09:31:36 -05:00
Spidy123222
12fc6cf6e2 attempt fix settings 2023-11-27 09:31:36 -05:00
Spidy123222
580db6530e fix the mighty error 2023-11-27 09:31:36 -05:00
Spidy123222
9c67c237ee get rest of batcherror 2023-11-27 09:31:36 -05:00
Spidy123222
357d85a72e please o riley build 2023-11-27 09:31:36 -05:00
Spidy123222
88ad828ce0 hopefully fix error code build error 2023-11-27 09:31:36 -05:00
Riley Testut
a95625a34a Adds “Clear Cache” description to Techy Things section footer
(cherry picked from commit 913db5131b)
2023-11-27 09:31:36 -05:00
Riley Testut
95e00d81f5 Adds “Clear Cache” button to remove temporary files and uninstalled app backups
(cherry picked from commit 3adfc9db6d)
2023-11-27 09:31:36 -05:00
junepark678
c2e386a5c5 chore(App IDs, My Apps): change back to full 2023-11-27 09:22:19 -05:00
junepark678
a76aade4ff chore(App IDs, My Apps): change to use DateComponentsFormatter.UnitsStyle.abbreviated 2023-11-27 09:22:19 -05:00
junepark678
65c9986103 bugfix(App IDs, My Apps): fix date display 2023-11-27 09:22:19 -05:00
junepark678
9e2b9b6639 bugfix(App IDs, My Apps): display only necessary information 2023-11-27 09:22:19 -05:00
junepark678
cf373634d7 bugfix(App IDs, My Apps): calculate in correct direction in time (we aren't time travelers) 2023-11-27 09:22:19 -05:00
junepark678
b3d5d976b4 feat(My Apps): make expiration dates more specific 2023-11-27 09:22:19 -05:00
junepark678
c3c31995ce chore(App IDs): localize Unknown string 2023-11-27 09:22:19 -05:00
junepark678
7e92e17429 feat(App IDs): make expiration dates more specific 2023-11-27 09:22:19 -05:00
junepark678
88ab8fa8d7 feat: remove reliance on Info.plist for getting udid 2023-11-27 09:21:54 -05:00
junepark678
ebe78932bf feat(Operations): don't idle timeout during installations 2023-11-26 10:51:33 +09:00
nythepegasus
2e613e6d15 [skip ci] Update Ignited source URL 2023-11-06 09:02:49 -05:00
Spidy123222
35ee92db12 Change pairing file link with new wiki link
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-10-23 13:12:23 -07:00
nythepegasus
04d9f760ad Fix AltWidget App Group issue 2023-10-23 10:12:09 -04:00
naturecodevoid
4f52743be8 Revert most of xcodeproj changes 2023-10-21 20:44:49 -07:00
naturecodevoid
32cae7a5b2 Revert "Attempt to fix submodule dependencies for GH runner"
This reverts commit 5cff914ff3.
2023-10-21 20:30:32 -07:00
naturecodevoid
c2c0e3b790 Use forked libplist 2023-10-21 20:30:23 -07:00
nythepegasus
6d36a30787 [skip ci] Bump nightly to 0.5.3
Signed-off-by: nythepegasus <nythepegasus84@gmail.com>
2023-10-21 21:02:41 -04:00
nythepegasus
48a86ec6de Update Roxas 2023-10-21 20:35:32 -04:00
nythepegasus
5cff914ff3 Attempt to fix submodule dependencies for GH runner 2023-10-21 20:34:44 -04:00
nythepegasus
70ea725ce3 Update limd/libplist submodules 2023-10-20 22:04:47 -04:00
nythepegasus
78f12e45f9 Add iOS 17 JIT error notice with other errors 2023-10-20 21:51:24 -04:00
nythepegasus
e5061acc20 Hardcode SideStore's URL scheme for now 2023-10-20 21:51:24 -04:00
nythepegasus
2d7bc51d30 Revert this change 2023-10-20 21:51:24 -04:00
nythepegasus
9128b67ee8 Add newly compiled AltBackup 2023-10-20 21:51:24 -04:00
Bogdan Seniuc
551c004476 Use provisioning profile details instead of guessing active app limit 2023-10-20 21:50:50 -04:00
Spidy123222
ed6a8d1379 [skip ci] Remove emuthreeds from trusted sources 2023-10-18 21:44:55 -07:00
nythepegasus
766fb89e0b Change this to be hardcoded SideStore search 2023-10-17 17:38:09 -04:00
nythepegasus
c5b8cb4459 Remove buggy retry code finally 2023-10-17 17:37:29 -04:00
naturecodevoid
0deae92829 Bump version to 0.5.2 so nightly builds have a higher SemVer version than stable 2023-09-18 16:18:05 -07:00
naturecodevoid
cc5d2f1813 Build nightly with latest minimuxer changes to attempt to fix plist_from_memory crash 2023-09-18 16:17:14 -07:00
naturecodevoid
41151d0d49 0.5.1 2023-09-17 14:01:13 -07:00
Spidy123222
52702264a3 Change version to 0.5.2 2023-09-17 12:36:44 -07:00
naturecodevoid
6e297e1278 Update Swift Packages and submodules (#469)
* Update Swift Packages

* Update submodules

* make sure it builds

---------

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-09-17 10:45:55 -07:00
naturecodevoid
e3bb9b425f [skip ci] Add more information to staging errors (#468)
* Point to my forks and attempt to add more information to staging errors

* Improve error message a bit

* Revert fetch-prebuilt.sh changes

* Undo some whitespace changes

* missed one

* oops

* [skip ci]

* [skip ci]

* [skip ci] remove staging directory from install app error

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>

---------

Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
Co-authored-by: Dadoum <dadoum@protonmail.com>
2023-09-17 10:37:49 -07:00
Spidy123222
79255be79c Update Altsign (August 6) (#467)
This changes the git commit or be our latest altsign version.
2023-09-16 01:51:35 -07:00
nythepegasus
7c836f5ba1 Update emuplace source
Signed-off-by: nythepegasus <nythepegasus84@gmail.com>
2023-08-13 11:54:00 -04:00
Spidy123222
938bcd14ad Add ignited source
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-08-06 14:15:51 -07:00
Joelle Stickney
229d79fc05 Removed Quantum Source
Signed-off-by: Joelle Stickney <joellestickney+commit@gmail.com>
2023-08-06 16:26:24 -04:00
Joelle Stickney
2d3dac2e1d Added Quantum Source to trusted sources
Signed-off-by: Joelle Stickney <joellestickney+commit@gmail.com>
2023-08-06 15:30:21 -04:00
nythepegasus
e23f5e7894 [skip ci] Change Discord custom invite to static website invite
Signed-off-by: nythepegasus <nythepegasus84@gmail.com>
2023-08-04 13:17:02 -04:00
Spidy123222
571d27c814 Fix message and put in proper spot.
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-07-27 23:42:32 -07:00
Spidy123222
dde6bd4fe3 Make Notification explanation smaller for refresh
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-07-27 23:17:58 -07:00
Nythepegasus
6e6dbd9329 Bump version to 0.5.0 2023-07-27 06:56:51 -04:00
Nythepegasus
258268f5ef Update default anisette server 2023-07-27 06:55:31 -04:00
Nythepegasus
9ae49977fb Fix going to the home screen 2023-07-27 06:23:26 -04:00
Nythepegasus
d61c54fa60 Reintroduce notification/pop-up 2023-07-27 06:23:26 -04:00
Nythepegasus
980699af6f Remove extra returns and make sure to decrement 2023-07-27 06:23:26 -04:00
Nythepegasus
cc5c280882 Finally fix retries for minimuxer calls 2023-07-27 06:23:26 -04:00
nythepegasus
090456bba1 [skip ci] Merge pull request #419 from Nythepegasus/chore/up-fixes
[Chore] Pull upstream changes from AltStore
2023-07-27 06:22:54 -04:00
nythepegasus
5354d4eb76 Merge pull request #414 from SideStore/patch/fix-widget
[Patch] Change Widget entitlements to search hardcoded group first
2023-07-27 06:11:48 -04:00
Riley Testut
b986fae611 Enforces 77x31 minimum size for PillButton 2023-07-27 04:47:44 -04:00
Riley Testut
cfcfc3e928 Fixes incorrect cell height for some News items
We need to take layoutMargins into account when calculating the height of the prototype cell.
2023-07-27 04:47:44 -04:00
Riley Testut
f97548fc3a Fixes UIDocumentPickerViewController deprecation warnings 2023-07-27 04:47:44 -04:00
Riley Testut
36913b425c Fixes “variable mutated after capture by sendable closure” warning 2023-07-27 04:47:44 -04:00
Riley Testut
822ea08d89 Fixes AppViewController deprecation warnings 2023-07-27 04:47:43 -04:00
Riley Testut
98dd6f3fe7 Fixes CollapsingTextView “TextKit 1 compatibility mode” runtime warning 2023-07-27 04:47:38 -04:00
Nythepegasus
b3f0dbb155 Change circle and logo to just be circle for now 2023-07-27 02:59:30 -04:00
nythepegasus
6904d931c3 Change the lock screen icon to be legible
Signed-off-by: nythepegasus <me@nythepegas.us>
2023-07-27 02:25:09 -04:00
Nythepegasus
529466a9f7 Merge branch 'develop' into patch/fix-widget 2023-07-24 09:29:03 -04:00
Nythepegasus
77dc695ba1 Revert the retries here as these seem buggier 2023-07-24 09:22:34 -04:00
Nythepegasus
e17776f651 Remove unused Riley and Shane images from bundle 2023-07-24 07:54:20 -04:00
Nythepegasus
0d2f355a74 Update AltWidget assets 2023-07-24 07:18:42 -04:00
Nythepegasus
2ce1576016 Move the first app group to be known shared group 2023-07-24 05:49:36 -04:00
Nythepegasus
0f3be3c494 Update AltWidget/ReleaseEntitlements.plist
Signed-off-by: Nythepegasus <me@nythepegas.us>
2023-07-24 05:40:51 -04:00
Nythepegasus
8c1ca8503a Fix Actions builds for AltWidget 2023-07-24 05:28:26 -04:00
Wes Bryie
32a59c17f4 [skip ci] em_proxy link update (#412)
Signed-off-by: Wes Bryie <wes@wesbryie.tech>
2023-07-21 20:04:44 -07:00
Riley Testut
b4b4ceab0b Fixes updating apps with manually-removed app extensions (e.g. uYou+) 2023-07-17 12:15:04 -04:00
Nythepegasus
be1f27bb9e Revert "Fix building on Xcode 15"
This reverts commit 3de24dcfce.
2023-07-11 02:00:49 -04:00
Nythepegasus
ed10ddb1cb Go to home screen instead of Safari/notif combo 2023-07-11 01:45:14 -04:00
Nythepegasus
dbdb4b0f32 Add various retries to the minimuxer calls 2023-07-11 01:44:11 -04:00
Nythepegasus
59e537362e Xcode 15 keeps adding imobiledevice.swift 2023-07-10 14:33:53 -04:00
Nythepegasus
6d96bf414f These vars don't change, let's use let keyword 2023-07-10 14:32:41 -04:00
Nythepegasus
e7ba778a5f Update AltSign and OpenSSL packages 2023-07-09 14:00:02 -04:00
Nythepegasus
933d349cd5 Fix libfragmentzip's search path 2023-07-09 14:00:02 -04:00
Nythepegasus
3de24dcfce Fix building on Xcode 15 2023-07-09 13:59:55 -04:00
Spidy123222
3275d16b8b [skip ci] use odyssey team unified source
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-07-08 23:53:53 -07:00
Spidy123222
5bb4cd1dad [skip ci] Add Taurine
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-07-08 23:32:41 -07:00
Nythepegasus
16b14441fa Update Macley omnisette server IP
Signed-off-by: Nythepegasus <nythepegasus84@gmail.com>
2023-07-03 10:47:54 -04:00
Joshua Laymon
93a6272d30 Merge pull request #387 from bogotesr/develop
Fix up patreon screen and add socials
2023-06-21 21:58:04 -07:00
Spidy123222
0dc526f778 Replace old servers with macley v3 ansiette 2023-06-14 13:56:12 -07:00
bogotesr
183e185812 Change Patreon screen again
Refresh to "support us" and include social media.
2023-06-13 23:14:01 -07:00
Stern
e02453598c Update pr.yml
Signed-off-by: Stern <xsternent@gmail.com>
2023-06-13 20:31:37 -04:00
Stern
24af1b5b5f Update pr.yml
Signed-off-by: Stern <xsternent@gmail.com>
2023-06-13 20:30:41 -04:00
Spidy123222
5864c283f6 Add EmuPalace Source 2023-06-12 20:35:36 -07:00
Spidy123222
be78fa4b91 Upload config (#383)
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-06-12 09:39:20 -07:00
Spidy123222
b3abf69a02 Change label to 0.4.0
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-05-29 19:33:38 -07:00
Spidy123222
c530dc11ae Change default anisette to v3 anisette server (#367) 2023-05-29 19:15:15 -07:00
Joelle Stickney
d368ddbd11 Merge pull request #365 from lonkelle/develop
Co-authored-by: Joe Mattiello <mail@joemattiello.com>
2023-05-24 23:11:22 -04:00
Joelle Stickney
e5c6521a15 Co-authoring all the things
Co-authored-by: Joe Mattiello <mail@joemattiello.com>
2023-05-24 23:08:33 -04:00
Joelle Stickney
898a59768e Update README.md
Co-authored-by: Joe Mattiello <mail@joemattiello.com>
2023-05-24 23:04:45 -04:00
Joelle Stickney
a85bc93142 Update README.md
Co-authored-by: Joe Mattiello <mail@joemattiello.com>

Signed-off-by: Joelle Stickney <joellestickney+commit@gmail.com>
2023-05-24 03:56:58 -04:00
Joelle Stickney
c6c1f9faa0 Update README.md
Co-authored-by: JoeMatt <mail@joemattiello.com>

Signed-off-by: Joelle Stickney <joellestickney+commit@gmail.com>
2023-05-24 03:55:28 -04:00
Joelle Stickney
0eea19c9cc Update README.md
Signed-off-by: Joelle Stickney <joellestickney+commit@gmail.com>
2023-05-24 03:53:51 -04:00
naturecodevoid
ed2270ff46 Anisette V3 (#324)
* initial anisette V3 implementation

* update V3 urls and log version

* fix crash where FetchAnisetteDataOperation.clientInfo would be nil when getting anisette V3 without provisioning first

* move adi.pb reset to its own button instead of doing it on sign out

* fallback to V1 if client_info fails

* make sure to unwrap optional strings

* feat(anisette): update v3 usage, improve error messages and names, report v3 errors to the user

* refactor(anisette): reduce duplicate JSON to anisette code

* fixes(anisette v3): improve errors, fix v3 server check, fix some edge cases where SideStore could crash and instead return an error, retry on -45061
2023-05-18 01:30:18 -07:00
SoY0ung
45b6c3b338 Fix 'The name for this app is invalid' error(#361)
Fix 'The name for this app is invalid' error when sideloading with non-ascii name ipa
2023-05-15 12:38:26 +08:00
SoY0ung
84e2284f56 Optimizing function calls
Thanks for @ktprograms advice
2023-05-14 19:06:22 +08:00
SoY0ung
1c0d0be622 Fix 'The name for this app is invalid' error
This error is related to App ID creation failure.
App ID name must be an ascii text. It is not allowed to create an App ID with non-ascii text like Chinese, Japanese.
If the name is NOT an ascii text, using bundleID instead.
2023-05-14 02:55:36 +08:00
naturecodevoid
a9ce0f487d fix: open Safari instead of force closing and provide a fallback for users with notifications disabled 2023-05-06 19:25:37 -07:00
naturecodevoid
07533e0365 fix: ensure minimuxer is started when refreshing in the background 2023-04-16 10:07:04 -07:00
naturecodevoid
ee5ddd4264 fix: use a notification instead of an alert for force close 2023-04-16 09:29:12 -07:00
naturecodevoid
f519d22d81 fix: removing _CodeSignature folder before resigning 2023-04-13 21:21:51 -07:00
naturecodevoid
51ed87086a [skip ci] ci: fully rename SideStore.ipa, even after extracting the artifact zip 2023-04-13 07:30:20 -07:00
naturecodevoid
1ca3aa3cdb fix: force close SideStore after 3 seconds if still reinstalling 2023-04-13 07:20:36 -07:00
naturecodevoid
0178c63f6a fix: hopefully reduce ApplicationVerificationFailed errors by removing _CodeSignature folders since those may cause a problem 2023-04-12 19:53:27 -07:00
naturecodevoid
8a97c409fa fix: add .AltWidget to app group ID when modifying for SideStore 2023-04-12 07:46:14 -07:00
naturecodevoid
3dd0735305 fix: always reinstall when refreshing ourselves 2023-04-11 21:50:15 -07:00
naturecodevoid
536f775baa Revert "Don't reinstall on first SideStore refresh"
This reverts commit 40e1225b87.
2023-04-11 21:12:01 -07:00
naturecodevoid
00f7a684a3 [skip ci] chore: rename tempEnt.plist to ReleaseEntitlements.plist to reduce future confusion 2023-04-11 21:09:32 -07:00
naturecodevoid
d79b166a6a chore: Remove old apps.json/app.json files 2023-04-11 21:05:53 -07:00
naturecodevoid
b3d827f56a refactor: remove minimuxerToOperationError in favor of extending MinimuxerError to be a LocalizedError and remove unused cases from OperationError 2023-04-11 21:04:07 -07:00
naturecodevoid
40bcef1dcb Use XYZ0123456 team ID for tempEnt.plist
Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-04-06 20:45:18 -07:00
naturecodevoid
6146f1bdaa Update tempEnt.plist 2023-04-06 12:42:37 -07:00
f1shy-dev
f5d82d9ef0 Update tempEnt.plist to change AltStore to SideStore
Signed-off-by: f1shy-dev <56125930+f1shy-dev@users.noreply.github.com>
2023-04-06 20:23:27 +01:00
naturecodevoid
b2a29ae606 [skip ci] Include commit SHA in nightly version 2023-04-02 15:08:48 -07:00
naturecodevoid
98ccba53a2 [skip ci] Add version to artifact name
we can't do this for releases because some download URLs might rely on it being named SideStore.ipa

Build Info will also have version anyways
2023-04-02 08:01:57 -07:00
naturecodevoid
9bfda36647 [skip ci] Log version 2023-04-02 08:00:11 -07:00
naturecodevoid
5710cdf19c [skip ci] Fix PR commit suffix 2023-04-02 07:58:38 -07:00
naturecodevoid
20cf54bfcd [skip ci] Rename and move the first application groups log 2023-04-02 07:34:48 -07:00
naturecodevoid
2ce639e750 Remove app groups that contain AltStore 2023-04-01 20:03:15 -07:00
naturecodevoid
b1ed413c4f Revert Joelle's fix 2023-04-01 16:15:04 -07:00
naturecodevoid
b8c3060037 Log provisioning profile application groups 2023-04-01 16:10:40 -07:00
naturecodevoid
c3ea4940d7 Reduce duplicate consts 2023-04-01 16:10:05 -07:00
naturecodevoid
40e1225b87 Don't reinstall on first SideStore refresh 2023-04-01 16:09:28 -07:00
naturecodevoid
0c171122b2 refactor minimuxer to use swift-bridge (#321)
also add team ID to the end of the bundle ID for Debug builds to mirror SideServer
2023-04-01 16:02:12 -07:00
Joelle Stickney
6d0f4bb3da Fixes widget refreshing and is more thorough matching store ID 2023-03-28 23:48:24 -04:00
Joelle Stickney
5e2cc6e20c Update store check to check for AltServer or SideServer installation 2023-03-28 01:33:55 -04:00
naturecodevoid
99cb43bbea [skip ci] include commit SHA in PR builds
Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-03-24 08:56:30 -07:00
Riley Testut
ca7d8277f7 Fixes “no provisioning profile with the requested identifier…” error
As of March 20, 2023, deleting an app’s auto-generated free provisioning profile is no longer supported. However, fetching the provisioning profile now re-generates is every time, so there’s no need to delete it first.

As a workaround, we now simply use the first profile we fetched if we receive an error when deleting it. This approach should continue to work even if Apple later reverses this change.
2023-03-21 18:52:56 -04:00
Joe Mattiello
337d26333e Update README.md
Signed-off-by: Joe Mattiello <mail@joemattiello.com>
2023-03-20 00:02:11 -04:00
Joe Mattiello
ebb64d255b Update README.md
Signed-off-by: Joe Mattiello <mail@joemattiello.com>
2023-03-20 00:00:56 -04:00
Joe Mattiello
7dcb199f68 Update README.md
Add repobeats svg

Signed-off-by: Joe Mattiello <mail@joemattiello.com>
2023-03-19 23:28:32 -04:00
naturecodevoid
4334e887de [skip ci] use bundle ID from Build.xcconfig in AltStore.xcconfig 2023-03-12 16:38:59 -07:00
500 changed files with 18119 additions and 20294 deletions

View File

@@ -1,21 +0,0 @@
# https://docs.codecov.io/docs/codecov-yaml
codecov:
require_ci_to_pass: true
coverage:
precision: 2
round: down
range: "70...100"
ignore:
- Dependencies
status:
patch:
default:
if_no_uploads: error
changes: true
project:
default:
target: auto
if_no_uploads: error
comment: false

View File

@@ -1,35 +1,39 @@
# http://editorconfig.org # EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true root = true
# Unix-style newlines with a newline ending every file
[*] [*]
indent_style = space
charset = utf-8
indent_size = 4
end_of_line = lf end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true insert_final_newline = true
[*.{md,markdown}] # Matches multiple files with brace expansion notation
trim_trailing_whitespace = false # Set default charset
[*.{js,py}]
charset = utf-8# 4 space indentation
[*.{c,h,m,mm}] # Swift files
trim_trailing_whitespace = true [*.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_style = space
indent_size = 2 indent_size = 2
[*.js] # Matches the exact files either package.json or .travis.yml
indent_size = 2 [{package.json,.travis.yml}]
indent_style = space
[*.{swift}]
trim_trailing_whitespace = true
indent_style = tab
indent_size = 4
[Makefile]
trim_trailing_whitespace = true
indent_style = tab
indent_size = 8
[*.{yaml|yml}]
indent_size = 2 indent_size = 2

0
.env
View File

2
.github/CODEOWNERS vendored
View File

@@ -1 +1 @@
* @JoeMatt @lonkelle * @JoeMatt @lonkelle @nythepegasus @Spidy123222 @SternXD

View File

@@ -2,15 +2,14 @@ name: Bug Report
description: Report a bug description: Report a bug
title: "[BUG] " title: "[BUG] "
labels: ["bug"] labels: ["bug"]
assignees: assignees: []
- naturecodevoid
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
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. 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/RgpFBX3Q3k) or [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) for support.** **Please use [Discord](https://discord.gg/sidestore-949183273383395328) or [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) for support.**
- type: textarea - type: textarea
id: description id: description
attributes: attributes:

View File

@@ -3,7 +3,7 @@ blank_issues_enabled: false
contact_links: contact_links:
- name: Discord - name: Discord
url: https://discord.gg/RgpFBX3Q3k url: https://discord.gg/sidestore-949183273383395328
about: If you need support, please go here first instead of making an issue! about: If you need support, please go here first instead of making an issue!
- name: GitHub Discussions - name: GitHub Discussions
url: https://github.com/SideStore/SideStore/discussions url: https://github.com/SideStore/SideStore/discussions

View File

@@ -2,15 +2,14 @@ name: Feature Request
description: Suggest a feature description: Suggest a feature
title: "[FEATURE REQUEST] " title: "[FEATURE REQUEST] "
labels: ["enhancement"] labels: ["enhancement"]
assignees: assignees: []
- naturecodevoid
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: | 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. 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/RgpFBX3Q3k) or [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) for support.** **Please use [Discord](https://discord.gg/sidestore-949183273383395328) or [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) for support.**
- type: textarea - type: textarea
id: description id: description
attributes: attributes:

View File

@@ -1,20 +0,0 @@
# .github/workflows/sidestore-project.yml
name: SideStore Project
on:
push:
branches:
- main
pull_request:
jobs:
build:
name: Build
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- uses: tuist/tuist-action@0.13.0
with:
command: 'build'
arguments: ''

View File

@@ -2,7 +2,7 @@ name: Beta SideStore build
on: on:
push: push:
tags: tags:
- '[0-9]+.[0-9]+.[0-9]+-beta.[0-9]+' # example: 1.0.0-beta.1 - '[0-9]+.[0-9]+.[0-9]+-beta.[0-9]+' # example: 1.0.0-beta.1
jobs: jobs:
build: build:
@@ -11,80 +11,86 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- os: 'macos-12' - os: 'macos-12'
version: '14.2' version: '14.2'
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v2
with: with:
submodules: recursive submodules: recursive
- name: Install dependencies - name: Install dependencies
run: brew install ldid run: brew install ldid
- name: Change version to tag - name: Change version to tag
run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
- name: Setup Xcode - name: Get version
uses: maxim-lobanov/setup-xcode@v1.4.1 id: version
with: run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
xcode-version: ${{ matrix.version }}
- name: Build SideStore - name: Echo version
run: make build | xcpretty && exit ${PIPESTATUS[0]} run: echo "${{ steps.version.outputs.version }}"
- name: Fakesign app - name: Setup Xcode
run: make fakesign uses: maxim-lobanov/setup-xcode@v1.4.1
with:
xcode-version: ${{ matrix.version }}
- name: Convert to IPA - name: Build SideStore
run: make ipa 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 - name: Upload SideStore.ipa Artifact
uses: actions/upload-artifact@v3.1.0 uses: actions/upload-artifact@v3.1.0
with: with:
name: SideStore.ipa name: SideStore-${{ steps.version.outputs.version }}.ipa
path: SideStore.ipa path: SideStore-${{ steps.version.outputs.version }}.ipa
- name: Upload *.dSYM Artifact - name: Upload *.dSYM Artifact
uses: actions/upload-artifact@v3.1.0 uses: actions/upload-artifact@v3.1.0
with: with:
name: SideStore-dSYM name: SideStore-${{ steps.version.outputs.version }}-dSYM
path: ./*.dSYM/ path: ./*.dSYM/
- name: Get version
id: version
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
- name: Get current date
id: date
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
- name: Get current date in SideStore date form
id: date_sidestore
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_sidestore.outputs.date }}`
Commit SHA: `${{ github.sha }}`
Version: `${{ steps.version.outputs.version }}`

View File

@@ -7,7 +7,7 @@ DATE=`date -u +'%Y.%m.%d'`
BUILD_NUM=1 BUILD_NUM=1
write() { write() {
sed -e "/MARKETING_VERSION = .*/s/$/-nightly.$DATE.$BUILD_NUM/" -i '' Build.xcconfig sed -e "/MARKETING_VERSION = .*/s/$/-nightly.$DATE.$BUILD_NUM+$(git rev-parse --short HEAD)/" -i '' Build.xcconfig
echo "$DATE,$BUILD_NUM" > .nightly-build-num echo "$DATE,$BUILD_NUM" > .nightly-build-num
} }

View File

@@ -2,7 +2,7 @@ name: Nightly SideStore build
on: on:
push: push:
branches: branches:
- develop - connect-develop
jobs: jobs:
build: build:
@@ -14,87 +14,93 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- os: 'macos-12' - os: 'macos-12'
version: '14.2' version: '14.2'
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v2
with: with:
submodules: recursive submodules: recursive
- name: Install dependencies - name: Install dependencies
run: brew install ldid run: brew install ldid
- name: Cache .nightly-build-num - name: Cache .nightly-build-num
uses: actions/cache@v3 uses: actions/cache@v3
with: with:
path: .nightly-build-num path: .nightly-build-num
key: nightly-build-num key: nightly-build-num
- name: Increase nightly build number and set as version - name: Increase nightly build number and set as version
run: bash .github/workflows/increase-nightly-build-num.sh run: bash .github/workflows/increase-nightly-build-num.sh
- name: Setup Xcode - name: Get version
uses: maxim-lobanov/setup-xcode@v1.4.1 id: version
with: run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
xcode-version: ${{ matrix.version }}
- name: Build SideStore - name: Echo version
run: make build | xcpretty && exit ${PIPESTATUS[0]} run: echo "${{ steps.version.outputs.version }}"
- name: Fakesign app - name: Setup Xcode
run: make fakesign uses: maxim-lobanov/setup-xcode@v1.4.1
with:
xcode-version: ${{ matrix.version }}
- name: Convert to IPA - name: Build SideStore
run: make ipa 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 nightly release
uses: IsaacShelton/update-existing-release@v1.3.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
release: "Nightly"
tag: "nightly"
prerelease: true
files: SideStore.ipa
body: |
This is an ⚠️ **EXPERIMENTAL** ⚠️ nightly build for commit [${{ github.sha }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }}).
Nightly builds are **extremely experimental builds only meant to be used by developers and alpha 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 Beta](https://github.com/${{ github.repository }}/releases?q=beta).
## 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 - name: Upload SideStore.ipa Artifact
uses: actions/upload-artifact@v3.1.0 uses: actions/upload-artifact@v3.1.0
with: with:
name: SideStore.ipa name: SideStore-${{ steps.version.outputs.version }}.ipa
path: SideStore.ipa path: SideStore-${{ steps.version.outputs.version }}.ipa
- name: Upload *.dSYM Artifact - name: Upload *.dSYM Artifact
uses: actions/upload-artifact@v3.1.0 uses: actions/upload-artifact@v3.1.0
with: with:
name: SideStore-dSYM name: SideStore-${{ steps.version.outputs.version }}-dSYM
path: ./*.dSYM/ path: ./*.dSYM/
- name: Get version - name: Reset cache for apps.sidestore.io/nightly
id: version run: sleep 10 && curl https://apps.sidestore.io/reset-cache/nightly/${{ secrets.SIDESOURCE_KEY }}
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
- name: Get current date
id: date
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
- name: Get current date in SideStore date form
id: date_sidestore
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Upload to nightly release
uses: IsaacShelton/update-existing-release@v1.3.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
release: "Nightly"
tag: "nightly"
prerelease: true
files: SideStore.ipa
body: |
This is an ⚠️ **EXPERIMENTAL** ⚠️ nightly build for commit [${{ github.sha }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }}).
Nightly builds are **extremely experimental builds only meant to be used by developers and alpha 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 Beta](https://github.com/${{ github.repository }}/releases?q=beta).
## Build Info
Built at (UTC): `${{ steps.date.outputs.date }}`
Built at (UTC date): `${{ steps.date_sidestore.outputs.date }}`
Commit SHA: `${{ github.sha }}`
Version: `${{ steps.version.outputs.version }}`
- name: Reset cache for apps.sidestore.io/nightly
run: sleep 10 && curl https://apps.sidestore.io/reset-cache/nightly/${{ secrets.SIDESOURCE_KEY }}

View File

@@ -23,7 +23,16 @@ jobs:
run: brew install ldid run: brew install ldid
- name: Add PR suffix to version - name: Add PR suffix to version
run: sed -e '/MARKETING_VERSION = .*/s/$/-pr.${{ github.event.pull_request.number }}/' -i '' Build.xcconfig 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 - name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.4.1 uses: maxim-lobanov/setup-xcode@v1.4.1
@@ -39,14 +48,17 @@ jobs:
- name: Convert to IPA - name: Convert to IPA
run: make 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 - name: Upload SideStore.ipa Artifact
uses: actions/upload-artifact@v3.1.0 uses: actions/upload-artifact@v3.1.0
with: with:
name: SideStore.ipa name: SideStore-${{ steps.version.outputs.version }}.ipa
path: SideStore.ipa path: SideStore-${{ steps.version.outputs.version }}.ipa
- name: Upload *.dSYM Artifact - name: Upload *.dSYM Artifact
uses: actions/upload-artifact@v3.1.0 uses: actions/upload-artifact@v3.1.0
with: with:
name: SideStore-dSYM name: SideStore-${{ steps.version.outputs.version }}-dSYM
path: ./*.dSYM/ path: ./*.dSYM/

View File

@@ -2,7 +2,7 @@ name: Stable SideStore build
on: on:
push: push:
tags: tags:
- '[0-9]+.[0-9]+.[0-9]+' # example: 1.0.0 - '[0-9]+.[0-9]+.[0-9]+' # example: 1.0.0
jobs: jobs:
build: build:
@@ -11,77 +11,83 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- os: 'macos-12' - os: 'macos-12'
version: '14.2' version: '14.2'
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v2
with: with:
submodules: recursive submodules: recursive
- name: Install dependencies - name: Install dependencies
run: brew install ldid run: brew install ldid
- name: Change version to tag - name: Change version to tag
run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
- name: Setup Xcode - name: Get version
uses: maxim-lobanov/setup-xcode@v1.4.1 id: version
with: run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
xcode-version: ${{ matrix.version }}
- name: Build SideStore - name: Echo version
run: make build | xcpretty && exit ${PIPESTATUS[0]} run: echo "${{ steps.version.outputs.version }}"
- name: Fakesign app - name: Setup Xcode
run: make fakesign uses: maxim-lobanov/setup-xcode@v1.4.1
with:
xcode-version: ${{ matrix.version }}
- name: Convert to IPA - name: Build SideStore
run: make ipa 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 stable release
uses: softprops/action-gh-release@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
name: ${{ steps.version.outputs.version }}
tag_name: ${{ github.ref_name }}
draft: 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. -->
## 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 - name: Upload SideStore.ipa Artifact
uses: actions/upload-artifact@v3.1.0 uses: actions/upload-artifact@v3.1.0
with: with:
name: SideStore.ipa name: SideStore-${{ steps.version.outputs.version }}.ipa
path: SideStore.ipa path: SideStore-${{ steps.version.outputs.version }}.ipa
- name: Upload *.dSYM Artifact - name: Upload *.dSYM Artifact
uses: actions/upload-artifact@v3.1.0 uses: actions/upload-artifact@v3.1.0
with: with:
name: SideStore-dSYM name: SideStore-${{ steps.version.outputs.version }}-dSYM
path: ./*.dSYM/ path: ./*.dSYM/
- name: Get version
id: version
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
- name: Get current date
id: date
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
- name: Get current date in SideStore date form
id: date_sidestore
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Upload to new stable release
uses: softprops/action-gh-release@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
name: ${{ steps.version.outputs.version }}
tag_name: ${{ github.ref_name }}
draft: 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. -->
## Changelog
- TODO
## Build Info
Built at (UTC): `${{ steps.date.outputs.date }}`
Built at (UTC date): `${{ steps.date_sidestore.outputs.date }}`
Commit SHA: `${{ github.sha }}`
Version: `${{ steps.version.outputs.version }}`

27
.gitmodules vendored
View File

@@ -1,6 +1,21 @@
[submodule "Dependencies/em_proxy"] [submodule "Dependencies/Roxas"]
path = SideStoreApp/Dependencies/em_proxy path = Dependencies/Roxas
url = https://github.com/SideStore/em_proxy.git url = https://github.com/rileytestut/Roxas.git
[submodule "Dependencies/minimuxer"] [submodule "Dependencies/libimobiledevice"]
path = SideStoreApp/Dependencies/minimuxer path = Dependencies/libimobiledevice
url = https://github.com/SideStore/minimuxer.git url = https://github.com/libimobiledevice/libimobiledevice
[submodule "Dependencies/libusbmuxd"]
path = Dependencies/libusbmuxd
url = https://github.com/libimobiledevice/libusbmuxd.git
[submodule "Dependencies/libplist"]
path = Dependencies/libplist
url = https://github.com/SideStore/libplist.git
[submodule "Dependencies/MarkdownAttributedString"]
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
[submodule "Dependencies/libfragmentzip"]
path = Dependencies/libfragmentzip
url = https://github.com/SideStore/libfragmentzip.git

View File

@@ -1,28 +0,0 @@
# ---- About ----
module: SideStore
module_version: 1.0,0
author: SideStore
readme: README.md
copyright: 'See [license](https://github.com/SideStore/SideStore/blob/develop/LICENSE) for more details.'
# ---- URLs ----
author_url: https://sidestore.io
dash_url: https://sidestore.io/docsets/SideStore.xml
github_url: https://github.com/SideStore/SideStore/
github_file_prefix: https://github.com/SideStore/SideStore/tree/1.0.2/
# ---- Sources ----
source_directory: Sources
documentation: .build/x86_64-apple-macosx/debug/SideStore.docc
# ---- Generation ----
clean: true
output: docs
min_acl: public
hide_documentation_coverage: false
skip_undocumented: false
objc: false
swift_version: 5.1.0
# ---- Formatting ----
theme: fullwidth

View File

@@ -1,42 +0,0 @@
# .swiftformat
## file options
--exclude .build,.github,.swiftpm,.vscode,Configurations,Dependencies
## format options
--allman false
--binarygrouping 4,8
--commas always
--comments indent
--decimalgrouping 3,6
--elseposition same-line
--empty void
--exponentcase lowercase
--exponentgrouping disabled
--fractiongrouping disabled
--header ignore
--hexgrouping 4,8
--hexliteralcase uppercase
--ifdef indent
--importgrouping testable-bottom
--indent 4
--indentcase false
--linebreaks lf
--maxwidth none
--octalgrouping 4,8
--operatorfunc spaced
--patternlet hoist
--ranges spaced
--self remove
--semicolons inline
--stripunusedargs always
--swiftversion 5.1
--trimwhitespace always
--wraparguments preserve
--wrapcollections preserve
## rules
--enable isEmpty,andOperator,assertionFailures

View File

@@ -1,76 +0,0 @@
disabled_rules:
- block_based_kvo
- colon
- control_statement
- cyclomatic_complexity
- discarded_notification_center_observer
- file_length
- function_parameter_count
- generic_type_name
- identifier_name
- multiple_closures_with_trailing_closure
- nesting
- switch_case_alignment
- todo
- type_name
- type_body_length
- function_body_length
- unused_closure_parameter
# parameterized rules can be customized from this configuration file
line_length: 200
# parameterized rules are first parameterized as a warning level, then error level.
type_body_length:
- 300 # warning
- 600 # error
# parameterized rules are first parameterized as a warning level, then error level.
# identifier_name_max_length:
# - 40 # warning
# - 60 # error
# # parameterized rules are first parameterized as a warning level, then error level.
# identifier_name_min_length:
# - 3 # warning
# - 2 # error
function_body_length:
- 200 # warning
- 500 # error
large_tuple:
- 4 # warning
- 6 # error
opt_in_rules:
- empty_count
- force_unwrapping
excluded: # paths to ignore during linting. overridden byincluded.
- .build
- .github
- .swiftpm
- .vscode
- Dependencies
analyzer_rules: # Rules run by `swiftlint analyze` (experimental)
- explicit_self
# Override these rules to be warnings for now
force_cast: warning
force_try: warning
empty_count: warning
reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, junit)
custom_rules:
placeholders_in_comments:
included: ".*\\.swift"
name: "No Placeholders in Comments"
regex: "<#([^#]+)#>"
match_kinds:
- comment
- doccomment
message: "Placeholder left in comment."
tiles_deprecated:
included: ".*\\.swift"
name: "Tiles are deprecated in favor of Frame"
regex: "([T,t]ile$|^[T,t]il[e,es])"
message: "Tiles are deprecated in favor of Frame"
severity: warning

3
AltBackup.xcconfig Normal file
View File

@@ -0,0 +1,3 @@
#include "Build.xcconfig"
PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER).AltBackup

View File

@@ -7,109 +7,115 @@
// //
import UIKit import UIKit
import OSLog
#if canImport(Logging)
import Logging
#endif
extension AppDelegate { extension AppDelegate
{
static let startBackupNotification = Notification.Name("io.altstore.StartBackup") static let startBackupNotification = Notification.Name("io.altstore.StartBackup")
static let startRestoreNotification = Notification.Name("io.altstore.StartRestore") static let startRestoreNotification = Notification.Name("io.altstore.StartRestore")
static let operationDidFinishNotification = Notification.Name("io.altstore.BackupOperationFinished") static let operationDidFinishNotification = Notification.Name("io.altstore.BackupOperationFinished")
static let operationResultKey = "result" static let operationResultKey = "result"
} }
@UIApplicationMain @UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate { class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow? var window: UIWindow?
private var currentBackupReturnURL: URL? private var currentBackupReturnURL: URL?
func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
{
// Override point for customization after application launch. // Override point for customization after application launch.
NotificationCenter.default.addObserver(self, selector: #selector(AppDelegate.operationDidFinish(_:)), name: AppDelegate.operationDidFinishNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(AppDelegate.operationDidFinish(_:)), name: AppDelegate.operationDidFinishNotification, object: nil)
let viewController = ViewController() let viewController = ViewController()
window = UIWindow(frame: UIScreen.main.bounds) self.window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = viewController self.window?.rootViewController = viewController
window?.makeKeyAndVisible() self.window?.makeKeyAndVisible()
return true return true
} }
func applicationWillResignActive(_: UIApplication) { func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
} }
func applicationDidBecomeActive(_: UIApplication) { func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
} }
func applicationWillTerminate(_: UIApplication) { func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
} }
func application(_: UIApplication, open url: URL, options _: [UIApplication.OpenURLOptionsKey: Any]) -> Bool { func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool
open(url) {
return self.open(url)
} }
} }
private extension AppDelegate { private extension AppDelegate
func open(_ url: URL) -> Bool { {
func open(_ url: URL) -> Bool
{
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false } guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false }
guard let command = components.host?.lowercased() else { return false } guard let command = components.host?.lowercased() else { return false }
switch command { switch command
{
case "backup": case "backup":
guard let returnString = components.queryItems?.first(where: { $0.name == "returnURL" })?.value, let returnURL = URL(string: returnString) else { return false } guard let returnString = components.queryItems?.first(where: { $0.name == "returnURL" })?.value, let returnURL = URL(string: returnString) else { return false }
currentBackupReturnURL = returnURL self.currentBackupReturnURL = returnURL
NotificationCenter.default.post(name: AppDelegate.startBackupNotification, object: nil) NotificationCenter.default.post(name: AppDelegate.startBackupNotification, object: nil)
return true return true
case "restore": case "restore":
guard let returnString = components.queryItems?.first(where: { $0.name == "returnURL" })?.value, let returnURL = URL(string: returnString) else { return false } guard let returnString = components.queryItems?.first(where: { $0.name == "returnURL" })?.value, let returnURL = URL(string: returnString) else { return false }
currentBackupReturnURL = returnURL self.currentBackupReturnURL = returnURL
NotificationCenter.default.post(name: AppDelegate.startRestoreNotification, object: nil) NotificationCenter.default.post(name: AppDelegate.startRestoreNotification, object: nil)
return true return true
default: return false default: return false
} }
} }
@objc func operationDidFinish(_ notification: Notification) { @objc func operationDidFinish(_ notification: Notification)
{
defer { self.currentBackupReturnURL = nil } defer { self.currentBackupReturnURL = nil }
guard guard
let returnURL = currentBackupReturnURL, let returnURL = self.currentBackupReturnURL,
let result = notification.userInfo?[AppDelegate.operationResultKey] as? Result<Void, Error> let result = notification.userInfo?[AppDelegate.operationResultKey] as? Result<Void, Error>
else { return } else { return }
guard var components = URLComponents(url: returnURL, resolvingAgainstBaseURL: false) else { return } guard var components = URLComponents(url: returnURL, resolvingAgainstBaseURL: false) else { return }
switch result { switch result
{
case .success: case .success:
components.path = "/success" components.path = "/success"
case let .failure(error as NSError): case .failure(let error as NSError):
components.path = "/failure" components.path = "/failure"
components.queryItems = ["errorDomain": error.domain, components.queryItems = ["errorDomain": error.domain,
"errorCode": String(error.code), "errorCode": String(error.code),
"errorDescription": error.localizedDescription].map { URLQueryItem(name: $0, value: $1) } "errorDescription": error.localizedDescription].map { URLQueryItem(name: $0, value: $1) }
} }
guard let responseURL = components.url else { return } guard let responseURL = components.url else { return }
DispatchQueue.main.async { DispatchQueue.main.async {
UIApplication.shared.open(responseURL, options: [:]) { success in UIApplication.shared.open(responseURL, options: [:]) { (success) in
os_log("Sent response to app with success: %@", type: .info , success) print("Sent response to app with success:", success)
} }
} }
} }
} }

View File

@@ -7,21 +7,15 @@
// //
import Foundation import Foundation
import OSLog
#if canImport(Logging)
import Logging
#endif
import AltSign extension ErrorUserInfoKey
import Roxas {
import protocol SideStoreCore.ALTLocalizedError
extension ErrorUserInfoKey {
static let sourceFile: String = "alt_sourceFile" static let sourceFile: String = "alt_sourceFile"
static let sourceFileLine: String = "alt_sourceFileLine" static let sourceFileLine: String = "alt_sourceFileLine"
} }
extension Error { extension Error
{
var sourceDescription: String? { var sourceDescription: String? {
guard let sourceFile = (self as NSError).userInfo[ErrorUserInfoKey.sourceFile] as? String, let sourceFileLine = (self as NSError).userInfo[ErrorUserInfoKey.sourceFileLine] else { guard let sourceFile = (self as NSError).userInfo[ErrorUserInfoKey.sourceFile] as? String, let sourceFileLine = (self as NSError).userInfo[ErrorUserInfoKey.sourceFileLine] else {
return nil return nil
@@ -30,225 +24,267 @@ extension Error {
} }
} }
struct BackupError: ALTLocalizedError { struct BackupError: ALTLocalizedError
enum Code { {
enum Code
{
case invalidBundleID case invalidBundleID
case appGroupNotFound(String?) case appGroupNotFound(String?)
case randomError // Used for debugging. case randomError // Used for debugging.
} }
let code: Code let code: Code
let sourceFile: String let sourceFile: String
let sourceFileLine: Int let sourceFileLine: Int
var failure: String? var failure: String?
var failureReason: String? { var failureReason: String? {
switch code { switch self.code
{
case .invalidBundleID: return NSLocalizedString("The bundle identifier is invalid.", comment: "") case .invalidBundleID: return NSLocalizedString("The bundle identifier is invalid.", comment: "")
case let .appGroupNotFound(appGroup): case .appGroupNotFound(let appGroup):
if let appGroup = appGroup { if let appGroup = appGroup
{
return String(format: NSLocalizedString("The app group “%@” could not be found.", comment: ""), appGroup) return String(format: NSLocalizedString("The app group “%@” could not be found.", comment: ""), appGroup)
} else { }
else
{
return NSLocalizedString("The AltStore app group could not be found.", comment: "") return NSLocalizedString("The AltStore app group could not be found.", comment: "")
} }
case .randomError: return NSLocalizedString("A random error occured.", comment: "") case .randomError: return NSLocalizedString("A random error occured.", comment: "")
} }
} }
var errorUserInfo: [String: Any] { var errorUserInfo: [String : Any] {
let userInfo: [String: Any?] = [NSLocalizedDescriptionKey: errorDescription, let userInfo: [String: Any?] = [NSLocalizedDescriptionKey: self.errorDescription,
NSLocalizedFailureReasonErrorKey: failureReason, NSLocalizedFailureReasonErrorKey: self.failureReason,
NSLocalizedFailureErrorKey: failure, NSLocalizedFailureErrorKey: self.failure,
ErrorUserInfoKey.sourceFile: sourceFile, ErrorUserInfoKey.sourceFile: self.sourceFile,
ErrorUserInfoKey.sourceFileLine: sourceFileLine] ErrorUserInfoKey.sourceFileLine: self.sourceFileLine]
return userInfo.compactMapValues { $0 } return userInfo.compactMapValues { $0 }
} }
init(_ code: Code, description: String? = nil, file: String = #file, line: Int = #line) { init(_ code: Code, description: String? = nil, file: String = #file, line: Int = #line)
{
self.code = code self.code = code
failure = description self.failure = description
sourceFile = file self.sourceFile = file
sourceFileLine = line self.sourceFileLine = line
} }
} }
class BackupController: NSObject { class BackupController: NSObject
{
private let fileCoordinator = NSFileCoordinator(filePresenter: nil) private let fileCoordinator = NSFileCoordinator(filePresenter: nil)
private let operationQueue = OperationQueue() private let operationQueue = OperationQueue()
override init() { override init()
operationQueue.name = "AltBackup-BackupQueue" {
self.operationQueue.name = "AltBackup-BackupQueue"
} }
func performBackup(completionHandler: @escaping (Result<Void, Error>) -> Void) { func performBackup(completionHandler: @escaping (Result<Void, Error>) -> Void)
do { {
do
{
guard let bundleIdentifier = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.altBundleID) as? String else { guard let bundleIdentifier = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.altBundleID) as? String else {
throw BackupError(.invalidBundleID, description: NSLocalizedString("Unable to create backup directory.", comment: "")) throw BackupError(.invalidBundleID, description: NSLocalizedString("Unable to create backup directory.", comment: ""))
} }
guard guard
let altstoreAppGroup = Bundle.main.altstoreAppGroup, let altstoreAppGroup = Bundle.main.altstoreAppGroup,
let sharedDirectoryURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: 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") let backupsDirectory = sharedDirectoryURL.appendingPathComponent("Backups")
// Use temporary directory to prevent messing up successful backup with incomplete one. // Use temporary directory to prevent messing up successful backup with incomplete one.
let temporaryAppBackupDirectory = backupsDirectory.appendingPathComponent("Temp", isDirectory: true).appendingPathComponent(UUID().uuidString) let temporaryAppBackupDirectory = backupsDirectory.appendingPathComponent("Temp", isDirectory: true).appendingPathComponent(UUID().uuidString)
let appBackupDirectory = backupsDirectory.appendingPathComponent(bundleIdentifier) let appBackupDirectory = backupsDirectory.appendingPathComponent(bundleIdentifier)
let writingIntent = NSFileAccessIntent.writingIntent(with: temporaryAppBackupDirectory, options: []) let writingIntent = NSFileAccessIntent.writingIntent(with: temporaryAppBackupDirectory, options: [])
let replacementIntent = NSFileAccessIntent.writingIntent(with: appBackupDirectory, options: [.forReplacing]) let replacementIntent = NSFileAccessIntent.writingIntent(with: appBackupDirectory, options: [.forReplacing])
fileCoordinator.coordinate(with: [writingIntent, replacementIntent], queue: operationQueue) { error in self.fileCoordinator.coordinate(with: [writingIntent, replacementIntent], queue: self.operationQueue) { (error) in
do { do
if let error = error { {
if let error = error
{
throw error throw error
} }
do { do
{
let mainGroupBackupDirectory = temporaryAppBackupDirectory.appendingPathComponent("App") let mainGroupBackupDirectory = temporaryAppBackupDirectory.appendingPathComponent("App")
try FileManager.default.createDirectory(at: mainGroupBackupDirectory, withIntermediateDirectories: true, attributes: nil) try FileManager.default.createDirectory(at: mainGroupBackupDirectory, withIntermediateDirectories: true, attributes: nil)
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let backupDocumentsDirectory = mainGroupBackupDirectory.appendingPathComponent(documentsDirectory.lastPathComponent) let backupDocumentsDirectory = mainGroupBackupDirectory.appendingPathComponent(documentsDirectory.lastPathComponent)
if FileManager.default.fileExists(atPath: backupDocumentsDirectory.path) { if FileManager.default.fileExists(atPath: backupDocumentsDirectory.path)
{
try FileManager.default.removeItem(at: backupDocumentsDirectory) try FileManager.default.removeItem(at: backupDocumentsDirectory)
} }
if FileManager.default.fileExists(atPath: documentsDirectory.path) { if FileManager.default.fileExists(atPath: documentsDirectory.path)
{
try FileManager.default.copyItem(at: documentsDirectory, to: backupDocumentsDirectory) try FileManager.default.copyItem(at: documentsDirectory, to: backupDocumentsDirectory)
} }
print("Copied Documents directory from \(documentsDirectory) to \(backupDocumentsDirectory)") print("Copied Documents directory from \(documentsDirectory) to \(backupDocumentsDirectory)")
let libraryDirectory = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0] let libraryDirectory = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0]
let backupLibraryDirectory = mainGroupBackupDirectory.appendingPathComponent(libraryDirectory.lastPathComponent) let backupLibraryDirectory = mainGroupBackupDirectory.appendingPathComponent(libraryDirectory.lastPathComponent)
if FileManager.default.fileExists(atPath: backupLibraryDirectory.path) { if FileManager.default.fileExists(atPath: backupLibraryDirectory.path)
{
try FileManager.default.removeItem(at: backupLibraryDirectory) try FileManager.default.removeItem(at: backupLibraryDirectory)
} }
if FileManager.default.fileExists(atPath: libraryDirectory.path) { if FileManager.default.fileExists(atPath: libraryDirectory.path)
{
try FileManager.default.copyItem(at: libraryDirectory, to: backupLibraryDirectory) try FileManager.default.copyItem(at: libraryDirectory, to: backupLibraryDirectory)
} }
print("Copied Library directory from \(libraryDirectory) to \(backupLibraryDirectory)") print("Copied Library directory from \(libraryDirectory) to \(backupLibraryDirectory)")
} }
for appGroup in Bundle.main.appGroups where appGroup != altstoreAppGroup { for appGroup in Bundle.main.appGroups where appGroup != altstoreAppGroup
{
guard let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else { guard let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
throw BackupError(.appGroupNotFound(appGroup), description: NSLocalizedString("Unable to create app group backup directory.", comment: "")) throw BackupError(.appGroupNotFound(appGroup), description: NSLocalizedString("Unable to create app group backup directory.", comment: ""))
} }
let backupAppGroupURL = temporaryAppBackupDirectory.appendingPathComponent(appGroup) let backupAppGroupURL = temporaryAppBackupDirectory.appendingPathComponent(appGroup)
// There are several system hidden files that we don't have permission to read, so we just skip all hidden files in app group directories. // There are several system hidden files that we don't have permission to read, so we just skip all hidden files in app group directories.
try self.copyDirectoryContents(at: appGroupURL, to: backupAppGroupURL, options: [.skipsHiddenFiles]) try self.copyDirectoryContents(at: appGroupURL, to: backupAppGroupURL, options: [.skipsHiddenFiles])
} }
// Replace previous backup with new backup. // Replace previous backup with new backup.
_ = try FileManager.default.replaceItemAt(appBackupDirectory, withItemAt: temporaryAppBackupDirectory) _ = try FileManager.default.replaceItemAt(appBackupDirectory, withItemAt: temporaryAppBackupDirectory)
os_log("Replaced previous backup with new backup: %@", type: .info, temporaryAppBackupDirectory.absoluteString) print("Replaced previous backup with new backup:", temporaryAppBackupDirectory)
completionHandler(.success(())) completionHandler(.success(()))
} catch { }
do { try FileManager.default.removeItem(at: temporaryAppBackupDirectory) } catch { os_log("Failed to remove temporary directory. %@", type: .error , error.localizedDescription) } catch
{
do { try FileManager.default.removeItem(at: temporaryAppBackupDirectory) }
catch { print("Failed to remove temporary directory.", error) }
completionHandler(.failure(error)) completionHandler(.failure(error))
} }
} }
} catch { }
catch
{
completionHandler(.failure(error)) completionHandler(.failure(error))
} }
} }
func restoreBackup(completionHandler: @escaping (Result<Void, Error>) -> Void) { func restoreBackup(completionHandler: @escaping (Result<Void, Error>) -> Void)
do { {
do
{
guard let bundleIdentifier = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.altBundleID) as? String else { guard let bundleIdentifier = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.altBundleID) as? String else {
throw BackupError(.invalidBundleID, description: NSLocalizedString("Unable to access backup.", comment: "")) throw BackupError(.invalidBundleID, description: NSLocalizedString("Unable to access backup.", comment: ""))
} }
guard guard
let altstoreAppGroup = Bundle.main.altstoreAppGroup, let altstoreAppGroup = Bundle.main.altstoreAppGroup,
let sharedDirectoryURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: altstoreAppGroup) let sharedDirectoryURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: altstoreAppGroup)
else { throw BackupError(.appGroupNotFound(nil), description: NSLocalizedString("Unable to access backup.", comment: "")) } else { throw BackupError(.appGroupNotFound(nil), description: NSLocalizedString("Unable to access backup.", comment: "")) }
let backupsDirectory = sharedDirectoryURL.appendingPathComponent("Backups") let backupsDirectory = sharedDirectoryURL.appendingPathComponent("Backups")
let appBackupDirectory = backupsDirectory.appendingPathComponent(bundleIdentifier) let appBackupDirectory = backupsDirectory.appendingPathComponent(bundleIdentifier)
let readingIntent = NSFileAccessIntent.readingIntent(with: appBackupDirectory, options: []) let readingIntent = NSFileAccessIntent.readingIntent(with: appBackupDirectory, options: [])
fileCoordinator.coordinate(with: [readingIntent], queue: operationQueue) { error in self.fileCoordinator.coordinate(with: [readingIntent], queue: self.operationQueue) { (error) in
do { do
if let error = error { {
if let error = error
{
throw error throw error
} }
let mainGroupBackupDirectory = appBackupDirectory.appendingPathComponent("App") let mainGroupBackupDirectory = appBackupDirectory.appendingPathComponent("App")
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let backupDocumentsDirectory = mainGroupBackupDirectory.appendingPathComponent(documentsDirectory.lastPathComponent) let backupDocumentsDirectory = mainGroupBackupDirectory.appendingPathComponent(documentsDirectory.lastPathComponent)
let libraryDirectory = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0] let libraryDirectory = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0]
let backupLibraryDirectory = mainGroupBackupDirectory.appendingPathComponent(libraryDirectory.lastPathComponent) let backupLibraryDirectory = mainGroupBackupDirectory.appendingPathComponent(libraryDirectory.lastPathComponent)
try self.copyDirectoryContents(at: backupDocumentsDirectory, to: documentsDirectory) try self.copyDirectoryContents(at: backupDocumentsDirectory, to: documentsDirectory)
try self.copyDirectoryContents(at: backupLibraryDirectory, to: libraryDirectory) try self.copyDirectoryContents(at: backupLibraryDirectory, to: libraryDirectory)
for appGroup in Bundle.main.appGroups where appGroup != altstoreAppGroup { for appGroup in Bundle.main.appGroups where appGroup != altstoreAppGroup
{
guard let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else { guard let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
throw BackupError(.appGroupNotFound(appGroup), description: NSLocalizedString("Unable to read app group backup.", comment: "")) throw BackupError(.appGroupNotFound(appGroup), description: NSLocalizedString("Unable to read app group backup.", comment: ""))
} }
let backupAppGroupURL = appBackupDirectory.appendingPathComponent(appGroup) let backupAppGroupURL = appBackupDirectory.appendingPathComponent(appGroup)
try self.copyDirectoryContents(at: backupAppGroupURL, to: appGroupURL) try self.copyDirectoryContents(at: backupAppGroupURL, to: appGroupURL)
} }
completionHandler(.success(())) completionHandler(.success(()))
} catch { }
catch
{
completionHandler(.failure(error)) completionHandler(.failure(error))
} }
} }
} catch { }
catch
{
completionHandler(.failure(error)) completionHandler(.failure(error))
} }
} }
} }
private extension BackupController { private extension BackupController
func copyDirectoryContents(at sourceDirectoryURL: URL, to destinationDirectoryURL: URL, options: FileManager.DirectoryEnumerationOptions = []) throws { {
func copyDirectoryContents(at sourceDirectoryURL: URL, to destinationDirectoryURL: URL, options: FileManager.DirectoryEnumerationOptions = []) throws
{
guard FileManager.default.fileExists(atPath: sourceDirectoryURL.path) else { return } guard FileManager.default.fileExists(atPath: sourceDirectoryURL.path) else { return }
if !FileManager.default.fileExists(atPath: destinationDirectoryURL.path) { if !FileManager.default.fileExists(atPath: destinationDirectoryURL.path)
{
try FileManager.default.createDirectory(at: destinationDirectoryURL, withIntermediateDirectories: true, attributes: nil) try FileManager.default.createDirectory(at: destinationDirectoryURL, withIntermediateDirectories: true, attributes: nil)
} }
for fileURL in try FileManager.default.contentsOfDirectory(at: sourceDirectoryURL, includingPropertiesForKeys: [.isDirectoryKey], options: options) { for fileURL in try FileManager.default.contentsOfDirectory(at: sourceDirectoryURL, includingPropertiesForKeys: [.isDirectoryKey], options: options)
{
let isDirectory = try fileURL.resourceValues(forKeys: [.isDirectoryKey]).isDirectory ?? false let isDirectory = try fileURL.resourceValues(forKeys: [.isDirectoryKey]).isDirectory ?? false
let destinationURL = destinationDirectoryURL.appendingPathComponent(fileURL.lastPathComponent) let destinationURL = destinationDirectoryURL.appendingPathComponent(fileURL.lastPathComponent)
if FileManager.default.fileExists(atPath: destinationURL.path) { if FileManager.default.fileExists(atPath: destinationURL.path)
{
do { do {
try FileManager.default.removeItem(at: destinationURL) try FileManager.default.removeItem(at: destinationURL)
} catch CocoaError.fileWriteNoPermission where isDirectory { }
try copyDirectoryContents(at: fileURL, to: destinationURL, options: options) catch CocoaError.fileWriteNoPermission where isDirectory {
try self.copyDirectoryContents(at: fileURL, to: destinationURL, options: options)
continue continue
} catch { }
catch {
print(error) print(error)
throw error throw error
} }
} }
do { do {
try FileManager.default.copyItem(at: fileURL, to: destinationURL) try FileManager.default.copyItem(at: fileURL, to: destinationURL)
print("Copied item from \(fileURL) to \(destinationURL)") print("Copied item from \(fileURL) to \(destinationURL)")
} catch let error where fileURL.lastPathComponent == "Inbox" && fileURL.deletingLastPathComponent().lastPathComponent == "Documents" { }
catch let error where fileURL.lastPathComponent == "Inbox" && fileURL.deletingLastPathComponent().lastPathComponent == "Documents" {
// Ignore errors for /Documents/Inbox // Ignore errors for /Documents/Inbox
os_log("Failed to copy Inbox directory: %@", type: .error , error.localizedDescription) print("Failed to copy Inbox directory:", error)
} catch { }
catch {
print(error) print(error)
throw error throw error
} }

View File

@@ -35,16 +35,6 @@
<string>altbackup</string> <string>altbackup</string>
</array> </array>
</dict> </dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>SideBackup General</string>
<key>CFBundleURLSchemes</key>
<array>
<string>sidebackup</string>
</array>
</dict>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1</string> <string>1</string>

View File

@@ -8,7 +8,8 @@
import UIKit import UIKit
extension UIColor { extension UIColor
{
static let altstoreBackground = UIColor(named: "Background")! static let altstoreBackground = UIColor(named: "Background")!
static let altstoreText = UIColor(named: "Text")! static let altstoreText = UIColor(named: "Text")!
} }

View File

@@ -0,0 +1,206 @@
//
// ViewController.swift
// AltBackup
//
// Created by Riley Testut on 5/11/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import UIKit
extension Bundle
{
var appName: String? {
let appName =
Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String ??
Bundle.main.object(forInfoDictionaryKey: kCFBundleNameKey as String) as? String
return appName
}
}
extension ViewController
{
enum BackupOperation
{
case backup
case restore
}
}
class ViewController: UIViewController
{
private let backupController = BackupController()
private var currentOperation: BackupOperation? {
didSet {
DispatchQueue.main.async {
self.update()
}
}
}
private var textLabel: UILabel!
private var detailTextLabel: UILabel!
private var activityIndicatorView: UIActivityIndicatorView!
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?)
{
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.backup), name: AppDelegate.startBackupNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.restore), name: AppDelegate.startRestoreNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.didEnterBackground(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil)
}
required init?(coder: NSCoder) {
fatalError()
}
override func viewDidLoad()
{
super.viewDidLoad()
self.view.backgroundColor = .altstoreBackground
self.textLabel = UILabel(frame: .zero)
self.textLabel.font = UIFont.preferredFont(forTextStyle: .title2)
self.textLabel.textColor = .altstoreText
self.textLabel.textAlignment = .center
self.textLabel.numberOfLines = 0
self.detailTextLabel = UILabel(frame: .zero)
self.detailTextLabel.font = UIFont.preferredFont(forTextStyle: .body)
self.detailTextLabel.textColor = .altstoreText
self.detailTextLabel.textAlignment = .center
self.detailTextLabel.numberOfLines = 0
self.activityIndicatorView = UIActivityIndicatorView(style: .whiteLarge)
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
let arrangedSubviews = [self.textLabel!, self.detailTextLabel!, self.activityIndicatorView!]
#endif
let stackView = UIStackView(arrangedSubviews: arrangedSubviews)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.spacing = 22
stackView.axis = .vertical
stackView.alignment = .center
self.view.addSubview(stackView)
NSLayoutConstraint.activate([stackView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
stackView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
stackView.leadingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: self.view.safeAreaLayoutGuide.leadingAnchor, multiplier: 1.0),
self.view.safeAreaLayoutGuide.trailingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: stackView.trailingAnchor, multiplier: 1.0)])
self.update()
}
}
private extension ViewController
{
@objc func backup()
{
self.currentOperation = .backup
self.backupController.performBackup { (result) in
let appName = Bundle.main.appName ?? NSLocalizedString("App", comment: "")
let title = String(format: NSLocalizedString("%@ could not be backed up.", comment: ""), appName)
self.process(result, errorTitle: title)
}
}
@objc func restore()
{
self.currentOperation = .restore
self.backupController.restoreBackup { (result) in
let appName = Bundle.main.appName ?? NSLocalizedString("App", comment: "")
let title = String(format: NSLocalizedString("%@ could not be restored.", comment: ""), appName)
self.process(result, errorTitle: title)
}
}
func update()
{
switch self.currentOperation
{
case .backup:
self.textLabel.text = NSLocalizedString("Backing up app data…", comment: "")
self.detailTextLabel.isHidden = true
self.activityIndicatorView.startAnimating()
case .restore:
self.textLabel.text = NSLocalizedString("Restoring app data…", comment: "")
self.detailTextLabel.isHidden = true
self.activityIndicatorView.startAnimating()
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 SideStore to continue using it.", comment: ""),
Bundle.main.appName ?? NSLocalizedString("this app", comment: ""))
self.detailTextLabel.isHidden = false
self.activityIndicatorView.stopAnimating()
}
}
}
private extension ViewController
{
func process(_ result: Result<Void, Error>, errorTitle: String)
{
DispatchQueue.main.async {
switch result
{
case .success: break
case .failure(let error as NSError):
let message: String
if let sourceDescription = error.sourceDescription
{
message = error.localizedDescription + "\n\n" + sourceDescription
}
else
{
message = error.localizedDescription
}
let alertController = UIAlertController(title: errorTitle, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
self.present(alertController, animated: true, completion: nil)
}
NotificationCenter.default.post(name: AppDelegate.operationDidFinishNotification, object: nil, userInfo: [AppDelegate.operationResultKey: result])
}
}
@objc func didEnterBackground(_ notification: Notification)
{
// Reset UI once we've left app (but not before).
self.currentOperation = nil
}
}

View File

@@ -10,41 +10,46 @@ import Foundation
import AltSign import AltSign
private extension UserDefaults { private extension UserDefaults
{
@objc var localUserID: String? { @objc var localUserID: String? {
get { string(forKey: #keyPath(UserDefaults.localUserID)) } get { return self.string(forKey: #keyPath(UserDefaults.localUserID)) }
set { set(newValue, forKey: #keyPath(UserDefaults.localUserID)) } set { self.set(newValue, forKey: #keyPath(UserDefaults.localUserID)) }
} }
} }
struct AnisetteDataManager { struct AnisetteDataManager
{
static let shared = AnisetteDataManager() static let shared = AnisetteDataManager()
private let dateFormatter = ISO8601DateFormatter() private let dateFormatter = ISO8601DateFormatter()
private init() { private init()
dlopen("/System/Library/PrivateFrameworks/AuthKit.framework/AuthKit", RTLD_NOW) {
dlopen("/System/Library/PrivateFrameworks/AuthKit.framework/AuthKit", RTLD_NOW);
} }
func requestAnisetteData() throws -> ALTAnisetteData { func requestAnisetteData() throws -> ALTAnisetteData
var request = URLRequest(url: URL(string: "https://developerservices2.apple.com/services/QH65B2/listTeams.action?clientId=XABBG36SBA")!) {
var request = URLRequest(url: URL(string: "https://developerservices2.apple.com/services/QH65B2/listTeams.action?clientId=XABBG36SBA")!)
request.httpMethod = "POST" request.httpMethod = "POST"
let akAppleIDSession = unsafeBitCast(NSClassFromString("AKAppleIDSession")!, to: AKAppleIDSession.Type.self) let akAppleIDSession = unsafeBitCast(NSClassFromString("AKAppleIDSession")!, to: AKAppleIDSession.Type.self)
let akDevice = unsafeBitCast(NSClassFromString("AKDevice")!, to: AKDevice.Type.self) let akDevice = unsafeBitCast(NSClassFromString("AKDevice")!, to: AKDevice.Type.self)
let session = akAppleIDSession.init(identifier: "com.apple.gs.xcode.auth") let session = akAppleIDSession.init(identifier: "com.apple.gs.xcode.auth")
let headers = session.appleIDHeaders(for: request) let headers = session.appleIDHeaders(for: request)
let device = akDevice.current let device = akDevice.current
let date = dateFormatter.date(from: headers["X-Apple-I-Client-Time"] ?? "") ?? Date() let date = self.dateFormatter.date(from: headers["X-Apple-I-Client-Time"] ?? "") ?? Date()
var localUserID = UserDefaults.standard.localUserID var localUserID = UserDefaults.standard.localUserID
if localUserID == nil { if localUserID == nil
{
localUserID = UUID().uuidString localUserID = UUID().uuidString
UserDefaults.standard.localUserID = localUserID UserDefaults.standard.localUserID = localUserID
} }
let anisetteData = ALTAnisetteData(machineID: headers["X-Apple-I-MD-M"] ?? "", let anisetteData = ALTAnisetteData(machineID: headers["X-Apple-I-MD-M"] ?? "",
oneTimePassword: headers["X-Apple-I-MD"] ?? "", oneTimePassword: headers["X-Apple-I-MD"] ?? "",
localUserID: headers["X-Apple-I-MD-LU"] ?? localUserID ?? "", localUserID: headers["X-Apple-I-MD-LU"] ?? localUserID ?? "",

View File

@@ -10,105 +10,127 @@ import Foundation
import AltSign import AltSign
private extension URL { private extension URL
{
static let profilesDirectoryURL = URL(fileURLWithPath: "/var/MobileDevice/ProvisioningProfiles", isDirectory: true) static let profilesDirectoryURL = URL(fileURLWithPath: "/var/MobileDevice/ProvisioningProfiles", isDirectory: true)
} }
private extension CFNotificationName { private extension CFNotificationName
{
static let updatedProvisioningProfiles = CFNotificationName("MISProvisioningProfileRemoved" as CFString) static let updatedProvisioningProfiles = CFNotificationName("MISProvisioningProfileRemoved" as CFString)
} }
struct AppManager { struct AppManager
{
static let shared = AppManager() static let shared = AppManager()
private let appQueue = DispatchQueue(label: "com.rileytestut.AltDaemon.appQueue", qos: .userInitiated) private let appQueue = DispatchQueue(label: "com.rileytestut.AltDaemon.appQueue", qos: .userInitiated)
private let profilesQueue = OperationQueue() private let profilesQueue = OperationQueue()
private let fileCoordinator = NSFileCoordinator() private let fileCoordinator = NSFileCoordinator()
private init() { private init()
profilesQueue.name = "com.rileytestut.AltDaemon.profilesQueue" {
profilesQueue.qualityOfService = .userInitiated 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) { func installApp(at fileURL: URL, bundleIdentifier: String, activeProfiles: Set<String>?, completionHandler: @escaping (Result<Void, Error>) -> Void)
appQueue.async { {
self.appQueue.async {
let lsApplicationWorkspace = unsafeBitCast(NSClassFromString("LSApplicationWorkspace")!, to: LSApplicationWorkspace.Type.self) let lsApplicationWorkspace = unsafeBitCast(NSClassFromString("LSApplicationWorkspace")!, to: LSApplicationWorkspace.Type.self)
let options = ["CFBundleIdentifier": bundleIdentifier, "AllowInstallLocalProvisioned": NSNumber(value: true)] as [String: Any] let options = ["CFBundleIdentifier": bundleIdentifier, "AllowInstallLocalProvisioned": NSNumber(value: true)] as [String : Any]
let result = Result { try lsApplicationWorkspace.default.installApplication(fileURL, withOptions: options) } let result = Result { try lsApplicationWorkspace.default.installApplication(fileURL, withOptions: options) }
completionHandler(result) completionHandler(result)
} }
} }
func removeApp(forBundleIdentifier bundleIdentifier: String, completionHandler: @escaping (Result<Void, Error>) -> Void) { func removeApp(forBundleIdentifier bundleIdentifier: String, completionHandler: @escaping (Result<Void, Error>) -> Void)
appQueue.async { {
self.appQueue.async {
let lsApplicationWorkspace = unsafeBitCast(NSClassFromString("LSApplicationWorkspace")!, to: LSApplicationWorkspace.Type.self) let lsApplicationWorkspace = unsafeBitCast(NSClassFromString("LSApplicationWorkspace")!, to: LSApplicationWorkspace.Type.self)
lsApplicationWorkspace.default.uninstallApplication(bundleIdentifier, withOptions: nil) lsApplicationWorkspace.default.uninstallApplication(bundleIdentifier, withOptions: nil)
completionHandler(.success(())) completionHandler(.success(()))
} }
} }
func install(_ profiles: Set<ALTProvisioningProfile>, activeProfiles: Set<String>?, completionHandler: @escaping (Result<Void, Error>) -> Void) { func install(_ profiles: Set<ALTProvisioningProfile>, activeProfiles: Set<String>?, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
let intent = NSFileAccessIntent.writingIntent(with: .profilesDirectoryURL, options: []) let intent = NSFileAccessIntent.writingIntent(with: .profilesDirectoryURL, options: [])
fileCoordinator.coordinate(with: [intent], queue: profilesQueue) { error in self.fileCoordinator.coordinate(with: [intent], queue: self.profilesQueue) { (error) in
do { do
if let error = error { {
if let error = error
{
throw error throw error
} }
let installingBundleIDs = Set(profiles.map(\.bundleIdentifier)) let installingBundleIDs = Set(profiles.map(\.bundleIdentifier))
let profileURLs = try FileManager.default.contentsOfDirectory(at: intent.url, includingPropertiesForKeys: nil, options: []) 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. // Remove all inactive profiles (if active profiles are provided), and the previous profiles.
for fileURL in profileURLs { for fileURL in profileURLs
{
// Use memory mapping to reduce peak memory usage and stay within limit. // Use memory mapping to reduce peak memory usage and stay within limit.
guard let profile = try? ALTProvisioningProfile(url: fileURL, options: [.mappedIfSafe]) else { continue } guard let profile = try? ALTProvisioningProfile(url: fileURL, options: [.mappedIfSafe]) else { continue }
if installingBundleIDs.contains(profile.bundleIdentifier) || (activeProfiles?.contains(profile.bundleIdentifier) == false && profile.isFreeProvisioningProfile) { if installingBundleIDs.contains(profile.bundleIdentifier) || (activeProfiles?.contains(profile.bundleIdentifier) == false && profile.isFreeProvisioningProfile)
{
try FileManager.default.removeItem(at: fileURL) try FileManager.default.removeItem(at: fileURL)
} else { }
os_log("Ignoring: %@ %@", type: .info , profile.bundleIdentifier, profile.uuid) else
{
print("Ignoring:", profile.bundleIdentifier, profile.uuid)
} }
} }
for profile in profiles { for profile in profiles
{
let destinationURL = URL.profilesDirectoryURL.appendingPathComponent(profile.uuid.uuidString.lowercased()) let destinationURL = URL.profilesDirectoryURL.appendingPathComponent(profile.uuid.uuidString.lowercased())
try profile.data.write(to: destinationURL, options: .atomic) try profile.data.write(to: destinationURL, options: .atomic)
} }
completionHandler(.success(())) completionHandler(.success(()))
} catch { }
catch
{
completionHandler(.failure(error)) completionHandler(.failure(error))
} }
// Notify system to prevent accidentally untrusting developer certificate. // Notify system to prevent accidentally untrusting developer certificate.
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), .updatedProvisioningProfiles, nil, nil, true) CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), .updatedProvisioningProfiles, nil, nil, true)
} }
} }
func removeProvisioningProfiles(forBundleIdentifiers bundleIdentifiers: Set<String>, completionHandler: @escaping (Result<Void, Error>) -> Void) { func removeProvisioningProfiles(forBundleIdentifiers bundleIdentifiers: Set<String>, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
let intent = NSFileAccessIntent.writingIntent(with: .profilesDirectoryURL, options: []) let intent = NSFileAccessIntent.writingIntent(with: .profilesDirectoryURL, options: [])
fileCoordinator.coordinate(with: [intent], queue: profilesQueue) { error in self.fileCoordinator.coordinate(with: [intent], queue: self.profilesQueue) { (error) in
do { do
{
let profileURLs = try FileManager.default.contentsOfDirectory(at: intent.url, includingPropertiesForKeys: nil, options: []) let profileURLs = try FileManager.default.contentsOfDirectory(at: intent.url, includingPropertiesForKeys: nil, options: [])
for fileURL in profileURLs { for fileURL in profileURLs
{
guard let profile = ALTProvisioningProfile(url: fileURL) else { continue } guard let profile = ALTProvisioningProfile(url: fileURL) else { continue }
if bundleIdentifiers.contains(profile.bundleIdentifier) { if bundleIdentifiers.contains(profile.bundleIdentifier)
{
try FileManager.default.removeItem(at: fileURL) try FileManager.default.removeItem(at: fileURL)
} }
} }
completionHandler(.success(())) completionHandler(.success(()))
} catch { }
catch
{
completionHandler(.failure(error)) completionHandler(.failure(error))
} }
// Notify system to prevent accidentally untrusting developer certificate. // Notify system to prevent accidentally untrusting developer certificate.
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), .updatedProvisioningProfiles, nil, nil, true) CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), .updatedProvisioningProfiles, nil, nil, true)
} }

View File

@@ -8,103 +8,113 @@
import Foundation import Foundation
import SideKit
import OSLog
#if canImport(Logging)
import Logging
#endif
typealias DaemonConnectionManager = ConnectionManager<DaemonRequestHandler> typealias DaemonConnectionManager = ConnectionManager<DaemonRequestHandler>
private let connectionManager = ConnectionManager(requestHandler: DaemonRequestHandler(), private let connectionManager = ConnectionManager(requestHandler: DaemonRequestHandler(),
connectionHandlers: [XPCConnectionHandler()]) connectionHandlers: [XPCConnectionHandler()])
extension DaemonConnectionManager { extension DaemonConnectionManager
{
static var shared: ConnectionManager { static var shared: ConnectionManager {
connectionManager return connectionManager
} }
} }
struct DaemonRequestHandler: RequestHandler { struct DaemonRequestHandler: RequestHandler
func handleAnisetteDataRequest(_: AnisetteDataRequest, for _: Connection, completionHandler: @escaping (Result<AnisetteDataResponse, Error>) -> Void) { {
do { func handleAnisetteDataRequest(_ request: AnisetteDataRequest, for connection: Connection, completionHandler: @escaping (Result<AnisetteDataResponse, Error>) -> Void)
{
do
{
let anisetteData = try AnisetteDataManager.shared.requestAnisetteData() let anisetteData = try AnisetteDataManager.shared.requestAnisetteData()
let response = AnisetteDataResponse(anisetteData: anisetteData) let response = AnisetteDataResponse(anisetteData: anisetteData)
completionHandler(.success(response)) completionHandler(.success(response))
} catch { }
catch
{
completionHandler(.failure(error)) completionHandler(.failure(error))
} }
} }
func handlePrepareAppRequest(_ request: PrepareAppRequest, for connection: Connection, completionHandler: @escaping (Result<InstallationProgressResponse, Error>) -> Void) { func handlePrepareAppRequest(_ request: PrepareAppRequest, for connection: Connection, completionHandler: @escaping (Result<InstallationProgressResponse, Error>) -> Void)
{
guard let fileURL = request.fileURL else { return completionHandler(.failure(ALTServerError(.invalidRequest))) } guard let fileURL = request.fileURL else { return completionHandler(.failure(ALTServerError(.invalidRequest))) }
print("Awaiting begin installation request...") print("Awaiting begin installation request...")
connection.receiveRequest { result in connection.receiveRequest() { (result) in
os_log("Received begin installation request with result: %@", type: .info , String(describing: result)) print("Received begin installation request with result:", result)
do { do
guard case let .beginInstallation(request) = try result.get() else { throw ALTServerError(.unknownRequest) } {
guard case .beginInstallation(let request) = try result.get() else { throw ALTServerError(.unknownRequest) }
guard let bundleIdentifier = request.bundleIdentifier else { throw ALTServerError(.invalidRequest) } guard let bundleIdentifier = request.bundleIdentifier else { throw ALTServerError(.invalidRequest) }
AppManager.shared.installApp(at: fileURL, bundleIdentifier: bundleIdentifier, activeProfiles: request.activeProfiles) { result in AppManager.shared.installApp(at: fileURL, bundleIdentifier: bundleIdentifier, activeProfiles: request.activeProfiles) { (result) in
let result = result.map { InstallationProgressResponse(progress: 1.0) } let result = result.map { InstallationProgressResponse(progress: 1.0) }
os_log("Installed app with result: %@", type: .info, String(describing: result)) print("Installed app with result:", result)
completionHandler(result) completionHandler(result)
} }
} catch { }
catch
{
completionHandler(.failure(error)) completionHandler(.failure(error))
} }
} }
} }
func handleInstallProvisioningProfilesRequest(_ request: InstallProvisioningProfilesRequest, for _: Connection, func handleInstallProvisioningProfilesRequest(_ request: InstallProvisioningProfilesRequest, for connection: Connection,
completionHandler: @escaping (Result<InstallProvisioningProfilesResponse, Error>) -> Void) { completionHandler: @escaping (Result<InstallProvisioningProfilesResponse, Error>) -> Void)
AppManager.shared.install(request.provisioningProfiles, activeProfiles: request.activeProfiles) { result in {
switch result { AppManager.shared.install(request.provisioningProfiles, activeProfiles: request.activeProfiles) { (result) in
case let .failure(error): switch result
os_log("Failed to install profiles %@ : %@", type: .error , request.provisioningProfiles.map { $0.bundleIdentifier }.joined(separator: "\n"), error.localizedDescription) {
case .failure(let error):
print("Failed to install profiles \(request.provisioningProfiles.map { $0.bundleIdentifier }):", error)
completionHandler(.failure(error)) completionHandler(.failure(error))
case .success: case .success:
os_log("Installed profiles: %@", type: .info , request.provisioningProfiles.map { $0.bundleIdentifier }.joined(separator: "\n")) print("Installed profiles:", request.provisioningProfiles.map { $0.bundleIdentifier })
let response = InstallProvisioningProfilesResponse() let response = InstallProvisioningProfilesResponse()
completionHandler(.success(response)) completionHandler(.success(response))
} }
} }
} }
func handleRemoveProvisioningProfilesRequest(_ request: RemoveProvisioningProfilesRequest, for _: Connection, func handleRemoveProvisioningProfilesRequest(_ request: RemoveProvisioningProfilesRequest, for connection: Connection,
completionHandler: @escaping (Result<RemoveProvisioningProfilesResponse, Error>) -> Void) { completionHandler: @escaping (Result<RemoveProvisioningProfilesResponse, Error>) -> Void)
AppManager.shared.removeProvisioningProfiles(forBundleIdentifiers: request.bundleIdentifiers) { result in {
switch result { AppManager.shared.removeProvisioningProfiles(forBundleIdentifiers: request.bundleIdentifiers) { (result) in
case let .failure(error): switch result
os_log("Failed to remove profiles %@ : %@", type: .error, request.bundleIdentifiers, error.localizedDescription) {
case .failure(let error):
print("Failed to remove profiles \(request.bundleIdentifiers):", error)
completionHandler(.failure(error)) completionHandler(.failure(error))
case .success: case .success:
os_log("Removed profiles: %@", type: .info , request.bundleIdentifiers) print("Removed profiles:", request.bundleIdentifiers)
let response = RemoveProvisioningProfilesResponse() let response = RemoveProvisioningProfilesResponse()
completionHandler(.success(response)) completionHandler(.success(response))
} }
} }
} }
func handleRemoveAppRequest(_ request: RemoveAppRequest, for _: Connection, completionHandler: @escaping (Result<RemoveAppResponse, Error>) -> Void) { func handleRemoveAppRequest(_ request: RemoveAppRequest, for connection: Connection, completionHandler: @escaping (Result<RemoveAppResponse, Error>) -> Void)
AppManager.shared.removeApp(forBundleIdentifier: request.bundleIdentifier) { result in {
switch result { AppManager.shared.removeApp(forBundleIdentifier: request.bundleIdentifier) { (result) in
case let .failure(error): switch result
os_log("Failed to remove app %@ : %@", type: .error , request.bundleIdentifier, error.localizedDescription) {
case .failure(let error):
print("Failed to remove app \(request.bundleIdentifier):", error)
completionHandler(.failure(error)) completionHandler(.failure(error))
case .success: case .success:
os_log("Removed app: %@", type: .info , request.bundleIdentifier) print("Removed app:", request.bundleIdentifier)
let response = RemoveAppResponse() let response = RemoveAppResponse()
completionHandler(.success(response)) completionHandler(.success(response))
} }

View File

@@ -8,78 +8,86 @@
import Foundation import Foundation
import Security import Security
import SideStoreCore
class XPCConnectionHandler: NSObject, ConnectionHandler { class XPCConnectionHandler: NSObject, ConnectionHandler
{
var connectionHandler: ((Connection) -> Void)? var connectionHandler: ((Connection) -> Void)?
var disconnectionHandler: ((Connection) -> Void)? var disconnectionHandler: ((Connection) -> Void)?
private let dispatchQueue = DispatchQueue(label: "io.altstore.XPCConnectionListener", qos: .utility) private let dispatchQueue = DispatchQueue(label: "io.altstore.XPCConnectionListener", qos: .utility)
private let listeners = XPCConnection.machServiceNames.map { NSXPCListener.makeListener(machServiceName: $0) } private let listeners = XPCConnection.machServiceNames.map { NSXPCListener.makeListener(machServiceName: $0) }
deinit { deinit
{
self.stopListening() self.stopListening()
} }
func startListening() { func startListening()
for listener in listeners { {
for listener in self.listeners
{
listener.delegate = self listener.delegate = self
listener.resume() listener.resume()
} }
} }
func stopListening() { func stopListening()
listeners.forEach { $0.suspend() } {
self.listeners.forEach { $0.suspend() }
} }
} }
private extension XPCConnectionHandler { private extension XPCConnectionHandler
func disconnect(_ connection: Connection) { {
func disconnect(_ connection: Connection)
{
connection.disconnect() connection.disconnect()
disconnectionHandler?(connection) self.disconnectionHandler?(connection)
} }
} }
extension XPCConnectionHandler: NSXPCListenerDelegate { extension XPCConnectionHandler: NSXPCListenerDelegate
func listener(_: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool { {
func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool
{
let maximumPathLength = 4 * UInt32(MAXPATHLEN) let maximumPathLength = 4 * UInt32(MAXPATHLEN)
let pathBuffer = UnsafeMutablePointer<CChar>.allocate(capacity: Int(maximumPathLength)) let pathBuffer = UnsafeMutablePointer<CChar>.allocate(capacity: Int(maximumPathLength))
defer { pathBuffer.deallocate() } defer { pathBuffer.deallocate() }
proc_pidpath(newConnection.processIdentifier, pathBuffer, maximumPathLength) proc_pidpath(newConnection.processIdentifier, pathBuffer, maximumPathLength)
let path = String(cString: pathBuffer) let path = String(cString: pathBuffer)
let fileURL = URL(fileURLWithPath: path) let fileURL = URL(fileURLWithPath: path)
var code: UnsafeMutableRawPointer? var code: UnsafeMutableRawPointer?
defer { code.map { Unmanaged<AnyObject>.fromOpaque($0).release() } } defer { code.map { Unmanaged<AnyObject>.fromOpaque($0).release() } }
var status = SecStaticCodeCreateWithPath(fileURL as CFURL, 0, &code) var status = SecStaticCodeCreateWithPath(fileURL as CFURL, 0, &code)
guard status == 0 else { return false } guard status == 0 else { return false }
var signingInfo: CFDictionary? var signingInfo: CFDictionary?
defer { signingInfo.map { Unmanaged<AnyObject>.passUnretained($0).release() } } defer { signingInfo.map { Unmanaged<AnyObject>.passUnretained($0).release() } }
status = SecCodeCopySigningInformation(code, kSecCSInternalInformation | kSecCSSigningInformation, &signingInfo) status = SecCodeCopySigningInformation(code, kSecCSInternalInformation | kSecCSSigningInformation, &signingInfo)
guard status == 0 else { return false } guard status == 0 else { return false }
// Only accept connections from AltStore. // Only accept connections from AltStore.
guard guard
let codeSigningInfo = signingInfo as? [String: Any], let codeSigningInfo = signingInfo as? [String: Any],
let bundleIdentifier = codeSigningInfo["identifier"] as? String, let bundleIdentifier = codeSigningInfo["identifier"] as? String,
bundleIdentifier.contains(Bundle.Info.appbundleIdentifier) bundleIdentifier.contains(Bundle.Info.appbundleIdentifier)
else { return false } else { return false }
let connection = XPCConnection(newConnection) let connection = XPCConnection(newConnection)
newConnection.invalidationHandler = { [weak self, weak connection] in newConnection.invalidationHandler = { [weak self, weak connection] in
guard let self = self, let connection = connection else { return } guard let self = self, let connection = connection else { return }
self.disconnect(connection) self.disconnect(connection)
} }
connectionHandler?(connection) self.connectionHandler?(connection)
return true return true
} }
} }

3
AltStore.xcconfig Normal file
View File

@@ -0,0 +1,3 @@
#include "Build.xcconfig"
PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER)

View File

@@ -6,7 +6,7 @@
"location" : "https://github.com/SideStore/AltSign", "location" : "https://github.com/SideStore/AltSign",
"state" : { "state" : {
"branch" : "master", "branch" : "master",
"revision" : "7e0e7edcf8fbc44ac1e35da3e9030a297aa18b84" "revision" : "cc6189f0f7cd8e5bd24943af9322e0ff9420e9f4"
} }
}, },
{ {
@@ -14,8 +14,17 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/microsoft/appcenter-sdk-apple.git", "location" : "https://github.com/microsoft/appcenter-sdk-apple.git",
"state" : { "state" : {
"revision" : "8354a50fe01a7e54e196d3b5493b5ab53dd5866a", "revision" : "b2dc99cfedead0bad4e6573d86c5228c89cff332",
"version" : "4.4.2" "version" : "4.4.3"
}
},
{
"identity" : "imobiledevice.swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SideStore/iMobileDevice.swift",
"state" : {
"revision" : "74e481106dd155c0cd21bca6795fd9fe5f751654",
"version" : "1.0.5"
} }
}, },
{ {
@@ -50,8 +59,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/krzyzanowskim/OpenSSL", "location" : "https://github.com/krzyzanowskim/OpenSSL",
"state" : { "state" : {
"revision" : "033fcb41dac96b1b6effa945ca1f9ade002370b2", "revision" : "0faf71a188bcfdf0245cab42886b9b240ca71c52",
"version" : "1.1.1501" "version" : "1.1.2200"
} }
}, },
{ {
@@ -59,17 +68,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/microsoft/PLCrashReporter.git", "location" : "https://github.com/microsoft/PLCrashReporter.git",
"state" : { "state" : {
"revision" : "6b27393cad517c067dceea85fadf050e70c4ceaa", "revision" : "81cdec2b3827feb03286cb297f4c501a8eb98df1",
"version" : "1.10.1" "version" : "1.10.2"
}
},
{
"identity" : "roxas",
"kind" : "remoteSourceControl",
"location" : "https://github.com/JoeMatt/Roxas.git",
"state" : {
"revision" : "17338c09ec0ffeea4c68135f17c1f801a3d6d10d",
"version" : "1.2.2"
} }
}, },
{ {
@@ -77,17 +77,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/SwiftPackageIndex/SemanticVersion.git", "location" : "https://github.com/SwiftPackageIndex/SemanticVersion.git",
"state" : { "state" : {
"revision" : "fc670910dc0903cc269b3d0b776cda5703979c4e", "revision" : "a70840d5fca686ae3bd2fcf8aecc5ded0bd4f125",
"version" : "0.3.5" "version" : "0.3.6"
}
},
{
"identity" : "sidekit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SideStore/SideKit.git",
"state" : {
"revision" : "7ea34a09b52c104077dea8e0b90f8dc55d43b36b",
"version" : "0.1.0"
} }
}, },
{ {
@@ -95,8 +86,17 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/sparkle-project/Sparkle.git", "location" : "https://github.com/sparkle-project/Sparkle.git",
"state" : { "state" : {
"revision" : "286edd1fa22505a9e54d170e9fd07d775ea233f2", "revision" : "f0ceaf5cc9f3f23daa0ccb6dcebd79fc96ccc7d9",
"version" : "2.1.0" "version" : "2.5.0"
}
},
{
"identity" : "starscream",
"kind" : "remoteSourceControl",
"location" : "https://github.com/daltoniam/Starscream.git",
"state" : {
"revision" : "ac6c0fc9da221873e01bd1a0d4818498a71eef33",
"version" : "4.0.6"
} }
}, },
{ {

View File

@@ -31,7 +31,7 @@
BlueprintIdentifier = "BF1E314F22A0616100370A3C" BlueprintIdentifier = "BF1E314F22A0616100370A3C"
BuildableName = "libAltKit.a" BuildableName = "libAltKit.a"
BlueprintName = "AltKit" BlueprintName = "AltKit"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildActionEntry> </BuildActionEntry>
<BuildActionEntry <BuildActionEntry
@@ -43,9 +43,9 @@
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "BF18BFE624857D7900DD5981" BlueprintIdentifier = "BF18BFE624857D7900DD5981"
BuildableName = "SideDaemon" BuildableName = "AltDaemon"
BlueprintName = "SideDaemon" BlueprintName = "AltDaemon"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildActionEntry> </BuildActionEntry>
</BuildActionEntries> </BuildActionEntries>
@@ -72,9 +72,9 @@
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "BF18BFE624857D7900DD5981" BlueprintIdentifier = "BF18BFE624857D7900DD5981"
BuildableName = "SideDaemon" BuildableName = "AltDaemon"
BlueprintName = "SideDaemon" BlueprintName = "AltDaemon"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</MacroExpansion> </MacroExpansion>
<EnvironmentVariables> <EnvironmentVariables>
@@ -95,9 +95,9 @@
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "BF18BFE624857D7900DD5981" BlueprintIdentifier = "BF18BFE624857D7900DD5981"
BuildableName = "SideDaemon" BuildableName = "AltDaemon"
BlueprintName = "SideDaemon" BlueprintName = "AltDaemon"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</MacroExpansion> </MacroExpansion>
</ProfileAction> </ProfileAction>

View File

@@ -17,7 +17,7 @@
BlueprintIdentifier = "BF5C5FC4237DF5AE00EDD0C6" BlueprintIdentifier = "BF5C5FC4237DF5AE00EDD0C6"
BuildableName = "AltPlugin.mailbundle" BuildableName = "AltPlugin.mailbundle"
BlueprintName = "AltPlugin" BlueprintName = "AltPlugin"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildActionEntry> </BuildActionEntry>
</BuildActionEntries> </BuildActionEntries>
@@ -53,7 +53,7 @@
BlueprintIdentifier = "BF5C5FC4237DF5AE00EDD0C6" BlueprintIdentifier = "BF5C5FC4237DF5AE00EDD0C6"
BuildableName = "AltPlugin.mailbundle" BuildableName = "AltPlugin.mailbundle"
BlueprintName = "AltPlugin" BlueprintName = "AltPlugin"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</MacroExpansion> </MacroExpansion>
</ProfileAction> </ProfileAction>

View File

@@ -17,7 +17,7 @@
BlueprintIdentifier = "BF45868C229872EA00BD7491" BlueprintIdentifier = "BF45868C229872EA00BD7491"
BuildableName = "AltServer.app" BuildableName = "AltServer.app"
BlueprintName = "AltServer" BlueprintName = "AltServer"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildActionEntry> </BuildActionEntry>
</BuildActionEntries> </BuildActionEntries>
@@ -27,17 +27,19 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"> shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion> <MacroExpansion>
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "BF45868C229872EA00BD7491" BlueprintIdentifier = "BF45868C229872EA00BD7491"
BuildableName = "AltServer.app" BuildableName = "AltServer.app"
BlueprintName = "AltServer" BlueprintName = "AltServer"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</MacroExpansion> </MacroExpansion>
<Testables> <AdditionalOptions>
</Testables> </AdditionalOptions>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Debug"
@@ -56,9 +58,11 @@
BlueprintIdentifier = "BF45868C229872EA00BD7491" BlueprintIdentifier = "BF45868C229872EA00BD7491"
BuildableName = "AltServer.app" BuildableName = "AltServer.app"
BlueprintName = "AltServer" BlueprintName = "AltServer"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release"
@@ -73,7 +77,7 @@
BlueprintIdentifier = "BF45868C229872EA00BD7491" BlueprintIdentifier = "BF45868C229872EA00BD7491"
BuildableName = "AltServer.app" BuildableName = "AltServer.app"
BlueprintName = "AltServer" BlueprintName = "AltServer"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
</ProfileAction> </ProfileAction>

View File

@@ -17,7 +17,7 @@
BlueprintIdentifier = "BFD247692284B9A500981D42" BlueprintIdentifier = "BFD247692284B9A500981D42"
BuildableName = "SideStore.app" BuildableName = "SideStore.app"
BlueprintName = "SideStore" BlueprintName = "SideStore"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildActionEntry> </BuildActionEntry>
</BuildActionEntries> </BuildActionEntries>
@@ -47,7 +47,7 @@
BlueprintIdentifier = "BFD247692284B9A500981D42" BlueprintIdentifier = "BFD247692284B9A500981D42"
BuildableName = "SideStore.app" BuildableName = "SideStore.app"
BlueprintName = "SideStore" BlueprintName = "SideStore"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<CommandLineArguments> <CommandLineArguments>
@@ -70,7 +70,7 @@
BlueprintIdentifier = "BFD247692284B9A500981D42" BlueprintIdentifier = "BFD247692284B9A500981D42"
BuildableName = "SideStore.app" BuildableName = "SideStore.app"
BlueprintName = "SideStore" BlueprintName = "SideStore"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
</ProfileAction> </ProfileAction>

View File

@@ -17,7 +17,7 @@
BlueprintIdentifier = "BFD247692284B9A500981D42" BlueprintIdentifier = "BFD247692284B9A500981D42"
BuildableName = "SideStore.app" BuildableName = "SideStore.app"
BlueprintName = "SideStore" BlueprintName = "SideStore"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildActionEntry> </BuildActionEntry>
</BuildActionEntries> </BuildActionEntries>
@@ -47,7 +47,7 @@
BlueprintIdentifier = "BFD247692284B9A500981D42" BlueprintIdentifier = "BFD247692284B9A500981D42"
BuildableName = "SideStore.app" BuildableName = "SideStore.app"
BlueprintName = "SideStore" BlueprintName = "SideStore"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<CommandLineArguments> <CommandLineArguments>
@@ -70,7 +70,7 @@
BlueprintIdentifier = "BFD247692284B9A500981D42" BlueprintIdentifier = "BFD247692284B9A500981D42"
BuildableName = "SideStore.app" BuildableName = "SideStore.app"
BlueprintName = "SideStore" BlueprintName = "SideStore"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
</ProfileAction> </ProfileAction>

View File

@@ -17,7 +17,7 @@
BlueprintIdentifier = "BFF7C903257844C900E55F36" BlueprintIdentifier = "BFF7C903257844C900E55F36"
BuildableName = "AltXPC.xpc" BuildableName = "AltXPC.xpc"
BlueprintName = "AltXPC" BlueprintName = "AltXPC"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildActionEntry> </BuildActionEntry>
</BuildActionEntries> </BuildActionEntries>
@@ -47,7 +47,7 @@
BlueprintIdentifier = "BF45868C229872EA00BD7491" BlueprintIdentifier = "BF45868C229872EA00BD7491"
BuildableName = "AltServer.app" BuildableName = "AltServer.app"
BlueprintName = "AltServer" BlueprintName = "AltServer"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
</LaunchAction> </LaunchAction>
@@ -63,7 +63,7 @@
BlueprintIdentifier = "BFF7C903257844C900E55F36" BlueprintIdentifier = "BFF7C903257844C900E55F36"
BuildableName = "AltXPC.xpc" BuildableName = "AltXPC.xpc"
BlueprintName = "AltXPC" BlueprintName = "AltXPC"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</MacroExpansion> </MacroExpansion>
</ProfileAction> </ProfileAction>

View File

@@ -0,0 +1,8 @@
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//
#import "NSAttributedString+Markdown.h"
#import "ALTAppPatcher.h"
#include "fragmentzip.h"

View File

@@ -4,8 +4,6 @@
<dict> <dict>
<key>aps-environment</key> <key>aps-environment</key>
<string>development</string> <string>development</string>
<key>com.apple.developer.kernel.increased-memory-limit</key>
<true/>
<key>com.apple.developer.siri</key> <key>com.apple.developer.siri</key>
<true/> <true/>
<key>com.apple.security.application-groups</key> <key>com.apple.security.application-groups</key>

View File

@@ -8,7 +8,7 @@
import Foundation import Foundation
import SideStoreCore import AltStoreCore
import AppCenter import AppCenter
import AppCenterAnalytics import AppCenterAnalytics
@@ -16,8 +16,10 @@ import AppCenterCrashes
private let appCenterAppSecret = "73532d3e-e573-4693-99a4-9f85840bbb44" private let appCenterAppSecret = "73532d3e-e573-4693-99a4-9f85840bbb44"
public extension AnalyticsManager { extension AnalyticsManager
enum EventProperty: String { {
enum EventProperty: String
{
case name case name
case bundleIdentifier case bundleIdentifier
case developerName case developerName
@@ -27,28 +29,31 @@ public extension AnalyticsManager {
case sourceIdentifier case sourceIdentifier
case sourceURL case sourceURL
} }
enum Event { enum Event
{
case installedApp(InstalledApp) case installedApp(InstalledApp)
case updatedApp(InstalledApp) case updatedApp(InstalledApp)
case refreshedApp(InstalledApp) case refreshedApp(InstalledApp)
public var name: String { var name: String {
switch self { switch self
{
case .installedApp: return "installed_app" case .installedApp: return "installed_app"
case .updatedApp: return "updated_app" case .updatedApp: return "updated_app"
case .refreshedApp: return "refreshed_app" case .refreshedApp: return "refreshed_app"
} }
} }
public var properties: [EventProperty: String] { var properties: [EventProperty: String] {
let properties: [EventProperty: String?] let properties: [EventProperty: String?]
switch self { switch self
case let .installedApp(app), let .updatedApp(app), let .refreshedApp(app): {
case .installedApp(let app), .updatedApp(let app), .refreshedApp(let app):
let appBundleURL = InstalledApp.fileURL(for: app) let appBundleURL = InstalledApp.fileURL(for: app)
let appBundleSize = FileManager.default.directorySize(at: appBundleURL) let appBundleSize = FileManager.default.directorySize(at: appBundleURL)
properties = [ properties = [
.name: app.name, .name: app.name,
.bundleIdentifier: app.bundleIdentifier, .bundleIdentifier: app.bundleIdentifier,
@@ -60,31 +65,37 @@ public extension AnalyticsManager {
.sourceURL: app.storeApp?.source?.sourceURL.absoluteString .sourceURL: app.storeApp?.source?.sourceURL.absoluteString
] ]
} }
return properties.compactMapValues { $0 } return properties.compactMapValues { $0 }
} }
} }
} }
public final class AnalyticsManager { final class AnalyticsManager
public static let shared = AnalyticsManager() {
static let shared = AnalyticsManager()
private init() {}
private init()
{
}
} }
public extension AnalyticsManager { extension AnalyticsManager
func start() { {
func start()
{
AppCenter.start(withAppSecret: appCenterAppSecret, services: [ AppCenter.start(withAppSecret: appCenterAppSecret, services: [
Analytics.self, Analytics.self,
Crashes.self Crashes.self
]) ])
} }
func trackEvent(_ event: Event) { func trackEvent(_ event: Event)
let properties = event.properties.reduce(into: [:]) { properties, item in {
let properties = event.properties.reduce(into: [:]) { (properties, item) in
properties[item.key.rawValue] = item.value properties[item.key.rawValue] = item.value
} }
Analytics.trackEvent(event.name, withProperties: properties) Analytics.trackEvent(event.name, withProperties: properties)
} }
} }

View File

@@ -8,17 +8,15 @@
import UIKit import UIKit
import SideStoreCore import AltStoreCore
import RoxasUIKit import Roxas
import OSLog
#if canImport(Logging)
import Logging
#endif
import Nuke import Nuke
extension AppContentViewController { extension AppContentViewController
private enum Row: Int, CaseIterable { {
private enum Row: Int, CaseIterable
{
case subtitle case subtitle
case screenshots case screenshots
case description case description
@@ -27,169 +25,186 @@ extension AppContentViewController {
} }
} }
final class AppContentViewController: UITableViewController { final class AppContentViewController: UITableViewController
{
var app: StoreApp! var app: StoreApp!
private lazy var screenshotsDataSource = self.makeScreenshotsDataSource() private lazy var screenshotsDataSource = self.makeScreenshotsDataSource()
private lazy var permissionsDataSource = self.makePermissionsDataSource() private lazy var permissionsDataSource = self.makePermissionsDataSource()
private lazy var dateFormatter: DateFormatter = { private lazy var dateFormatter: DateFormatter = {
let dateFormatter = DateFormatter() let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium dateFormatter.dateStyle = .medium
dateFormatter.timeStyle = .none dateFormatter.timeStyle = .none
return dateFormatter return dateFormatter
}() }()
private lazy var byteCountFormatter: ByteCountFormatter = { private lazy var byteCountFormatter: ByteCountFormatter = {
let formatter = ByteCountFormatter() let formatter = ByteCountFormatter()
return formatter return formatter
}() }()
@IBOutlet private var subtitleLabel: UILabel! @IBOutlet private var subtitleLabel: UILabel!
@IBOutlet private var descriptionTextView: CollapsingTextView! @IBOutlet private var descriptionTextView: CollapsingTextView!
@IBOutlet private var versionDescriptionTextView: CollapsingTextView! @IBOutlet private var versionDescriptionTextView: CollapsingTextView!
@IBOutlet private var versionLabel: UILabel! @IBOutlet private var versionLabel: UILabel!
@IBOutlet private var versionDateLabel: UILabel! @IBOutlet private var versionDateLabel: UILabel!
@IBOutlet private var sizeLabel: UILabel! @IBOutlet private var sizeLabel: UILabel!
@IBOutlet private var screenshotsCollectionView: UICollectionView! @IBOutlet private var screenshotsCollectionView: UICollectionView!
@IBOutlet private var permissionsCollectionView: UICollectionView! @IBOutlet private var permissionsCollectionView: UICollectionView!
var preferredScreenshotSize: CGSize? { var preferredScreenshotSize: CGSize? {
let layout = self.screenshotsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout let layout = self.screenshotsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
let aspectRatio: CGFloat = 16.0 / 9.0 // Hardcoded for now. let aspectRatio: CGFloat = 16.0 / 9.0 // Hardcoded for now.
let width = self.screenshotsCollectionView.bounds.width - (layout.minimumInteritemSpacing * 2) let width = self.screenshotsCollectionView.bounds.width - (layout.minimumInteritemSpacing * 2)
let itemWidth = width / 1.5 let itemWidth = width / 1.5
let itemHeight = itemWidth * aspectRatio let itemHeight = itemWidth * aspectRatio
return CGSize(width: itemWidth, height: itemHeight) return CGSize(width: itemWidth, height: itemHeight)
} }
override func viewDidLoad() { override func viewDidLoad()
{
super.viewDidLoad() super.viewDidLoad()
tableView.contentInset.bottom = 20 self.tableView.contentInset.bottom = 20
screenshotsCollectionView.dataSource = screenshotsDataSource self.screenshotsCollectionView.dataSource = self.screenshotsDataSource
screenshotsCollectionView.prefetchDataSource = screenshotsDataSource self.screenshotsCollectionView.prefetchDataSource = self.screenshotsDataSource
permissionsCollectionView.dataSource = permissionsDataSource self.permissionsCollectionView.dataSource = self.permissionsDataSource
subtitleLabel.text = app.subtitle self.subtitleLabel.text = self.app.subtitle
descriptionTextView.text = app.localizedDescription self.descriptionTextView.text = self.app.localizedDescription
if let version = app.latestVersion { if let version = self.app.latestVersion
versionDescriptionTextView.text = version.localizedDescription {
versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), version.version) self.versionDescriptionTextView.text = version.localizedDescription
versionDateLabel.text = Date().relativeDateString(since: version.date, dateFormatter: dateFormatter) self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), version.version)
sizeLabel.text = byteCountFormatter.string(fromByteCount: version.size) self.versionDateLabel.text = Date().relativeDateString(since: version.date, dateFormatter: self.dateFormatter)
} else { self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: version.size)
versionDescriptionTextView.text = nil
versionLabel.text = nil
versionDateLabel.text = nil
sizeLabel.text = byteCountFormatter.string(fromByteCount: 0)
} }
else
descriptionTextView.maximumNumberOfLines = 5 {
descriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered) self.versionDescriptionTextView.text = nil
self.versionLabel.text = nil
versionDescriptionTextView.maximumNumberOfLines = 3 self.versionDateLabel.text = nil
versionDescriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered) self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: 0)
}
self.descriptionTextView.maximumNumberOfLines = 5
self.descriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
self.versionDescriptionTextView.maximumNumberOfLines = 3
self.versionDescriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
} }
override func viewDidLayoutSubviews() { override func viewDidLayoutSubviews()
{
super.viewDidLayoutSubviews() super.viewDidLayoutSubviews()
guard var size = preferredScreenshotSize else { return } guard var size = self.preferredScreenshotSize else { return }
size.height = min(size.height, screenshotsCollectionView.bounds.height) // Silence temporary "item too tall" warning. size.height = min(size.height, self.screenshotsCollectionView.bounds.height) // Silence temporary "item too tall" warning.
let layout = screenshotsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout let layout = self.screenshotsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
layout.itemSize = size layout.itemSize = size
} }
override func prepare(for segue: UIStoryboardSegue, sender: Any?) { override func prepare(for segue: UIStoryboardSegue, sender: Any?)
{
guard segue.identifier == "showPermission" else { return } guard segue.identifier == "showPermission" else { return }
guard let cell = sender as? UICollectionViewCell, let indexPath = permissionsCollectionView.indexPath(for: cell) else { return } guard let cell = sender as? UICollectionViewCell, let indexPath = self.permissionsCollectionView.indexPath(for: cell) else { return }
let permission = permissionsDataSource.item(at: indexPath) let permission = self.permissionsDataSource.item(at: indexPath)
let maximumWidth = view.bounds.width - 20 let maximumWidth = self.view.bounds.width - 20
let permissionPopoverViewController = segue.destination as! PermissionPopoverViewController let permissionPopoverViewController = segue.destination as! PermissionPopoverViewController
permissionPopoverViewController.permission = permission permissionPopoverViewController.permission = permission
permissionPopoverViewController.view.widthAnchor.constraint(lessThanOrEqualToConstant: maximumWidth).isActive = true permissionPopoverViewController.view.widthAnchor.constraint(lessThanOrEqualToConstant: maximumWidth).isActive = true
let size = permissionPopoverViewController.view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) let size = permissionPopoverViewController.view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
permissionPopoverViewController.preferredContentSize = size permissionPopoverViewController.preferredContentSize = size
permissionPopoverViewController.popoverPresentationController?.delegate = self permissionPopoverViewController.popoverPresentationController?.delegate = self
permissionPopoverViewController.popoverPresentationController?.sourceRect = cell.frame permissionPopoverViewController.popoverPresentationController?.sourceRect = cell.frame
permissionPopoverViewController.popoverPresentationController?.sourceView = permissionsCollectionView permissionPopoverViewController.popoverPresentationController?.sourceView = self.permissionsCollectionView
} }
} }
private extension AppContentViewController { private extension AppContentViewController
func makeScreenshotsDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage> { {
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>(items: app.screenshotURLs as [NSURL]) func makeScreenshotsDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>
dataSource.cellConfigurationHandler = { cell, _, _ in {
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>(items: self.app.screenshotURLs as [NSURL])
dataSource.cellConfigurationHandler = { (cell, screenshot, indexPath) in
let cell = cell as! ScreenshotCollectionViewCell let cell = cell as! ScreenshotCollectionViewCell
cell.imageView.image = nil cell.imageView.image = nil
cell.imageView.isIndicatingActivity = true cell.imageView.isIndicatingActivity = true
} }
dataSource.prefetchHandler = { imageURL, _, completionHandler in dataSource.prefetchHandler = { (imageURL, indexPath, completionHandler) in
RSTAsyncBlockOperation { operation in return RSTAsyncBlockOperation() { (operation) in
let request = ImageRequest(url: imageURL as URL, processor: .screenshot) let request = ImageRequest(url: imageURL as URL, processor: .screenshot)
ImagePipeline.shared.loadImage(with: request, progress: nil, completion: { response, error in ImagePipeline.shared.loadImage(with: request, progress: nil, completion: { (response, error) in
guard !operation.isCancelled else { return operation.finish() } guard !operation.isCancelled else { return operation.finish() }
if let image = response?.image { if let image = response?.image
{
completionHandler(image, nil) completionHandler(image, nil)
} else { }
else
{
completionHandler(nil, error) completionHandler(nil, error)
} }
}) })
} }
} }
dataSource.prefetchCompletionHandler = { cell, image, _, error in dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
let cell = cell as! ScreenshotCollectionViewCell let cell = cell as! ScreenshotCollectionViewCell
cell.imageView.isIndicatingActivity = false cell.imageView.isIndicatingActivity = false
cell.imageView.image = image cell.imageView.image = image
if let error = error { if let error = error
os_log("Error loading image: %@", type: .error, error.localizedDescription) {
print("Error loading image:", error)
} }
} }
return dataSource return dataSource
} }
func makePermissionsDataSource() -> RSTArrayCollectionViewDataSource<AppPermission> { func makePermissionsDataSource() -> RSTArrayCollectionViewDataSource<AppPermission>
let dataSource = RSTArrayCollectionViewDataSource(items: app.permissions) {
dataSource.cellConfigurationHandler = { cell, permission, _ in let dataSource = RSTArrayCollectionViewDataSource(items: self.app.permissions)
dataSource.cellConfigurationHandler = { (cell, permission, indexPath) in
let cell = cell as! PermissionCollectionViewCell let cell = cell as! PermissionCollectionViewCell
cell.button.setImage(permission.type.icon, for: .normal) cell.button.setImage(permission.type.icon, for: .normal)
cell.button.tintColor = .label cell.button.tintColor = .label
cell.textLabel.text = permission.type.localizedShortName ?? permission.type.localizedName cell.textLabel.text = permission.type.localizedShortName ?? permission.type.localizedName
} }
return dataSource return dataSource
} }
} }
private extension AppContentViewController { private extension AppContentViewController
@objc func toggleCollapsingSection(_ sender: UIButton) { {
@objc func toggleCollapsingSection(_ sender: UIButton)
{
let indexPath: IndexPath let indexPath: IndexPath
switch sender { switch sender
case descriptionTextView.moreButton: indexPath = IndexPath(row: Row.description.rawValue, section: 0) {
case versionDescriptionTextView.moreButton: indexPath = IndexPath(row: Row.versionDescription.rawValue, section: 0) case self.descriptionTextView.moreButton: indexPath = IndexPath(row: Row.description.rawValue, section: 0)
case self.versionDescriptionTextView.moreButton: indexPath = IndexPath(row: Row.versionDescription.rawValue, section: 0)
default: return default: return
} }
// Disable animations to prevent some potentially strange ones. // Disable animations to prevent some potentially strange ones.
UIView.performWithoutAnimation { UIView.performWithoutAnimation {
self.tableView.reloadRows(at: [indexPath], with: .none) self.tableView.reloadRows(at: [indexPath], with: .none)
@@ -197,29 +212,35 @@ private extension AppContentViewController {
} }
} }
extension AppContentViewController { extension AppContentViewController
override func tableView(_: UITableView, willDisplay cell: UITableViewCell, forRowAt _: IndexPath) { {
cell.tintColor = app.tintColor override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath)
{
cell.tintColor = self.app.tintColor
} }
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
switch Row.allCases[indexPath.row] { {
switch Row.allCases[indexPath.row]
{
case .screenshots: case .screenshots:
guard let size = preferredScreenshotSize else { return 0.0 } guard let size = self.preferredScreenshotSize else { return 0.0 }
return size.height return size.height
case .permissions: case .permissions:
guard !app.permissions.isEmpty else { return 0.0 } guard !self.app.permissions.isEmpty else { return 0.0 }
return super.tableView(tableView, heightForRowAt: indexPath) return super.tableView(tableView, heightForRowAt: indexPath)
default: default:
return super.tableView(tableView, heightForRowAt: indexPath) return super.tableView(tableView, heightForRowAt: indexPath)
} }
} }
} }
extension AppContentViewController: UIPopoverPresentationControllerDelegate { extension AppContentViewController: UIPopoverPresentationControllerDelegate
func adaptivePresentationStyle(for _: UIPresentationController, traitCollection _: UITraitCollection) -> UIModalPresentationStyle { {
.none func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle
{
return .none
} }
} }

View File

@@ -8,33 +8,36 @@
import UIKit import UIKit
@objc final class PermissionCollectionViewCell: UICollectionViewCell
final class PermissionCollectionViewCell: UICollectionViewCell { {
@IBOutlet var button: UIButton! @IBOutlet var button: UIButton!
@IBOutlet var textLabel: UILabel! @IBOutlet var textLabel: UILabel!
override func layoutSubviews() { override func layoutSubviews()
{
super.layoutSubviews() super.layoutSubviews()
button.layer.cornerRadius = button.bounds.midY self.button.layer.cornerRadius = self.button.bounds.midY
} }
override func tintColorDidChange() { override func tintColorDidChange()
{
super.tintColorDidChange() super.tintColorDidChange()
button.backgroundColor = tintColor.withAlphaComponent(0.15) self.button.backgroundColor = self.tintColor.withAlphaComponent(0.15)
textLabel.textColor = tintColor self.textLabel.textColor = self.tintColor
} }
} }
@objc final class AppContentTableViewCell: UITableViewCell
final class AppContentTableViewCell: UITableViewCell { {
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize { override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize
{
// Ensure cell is laid out so it will report correct size. // Ensure cell is laid out so it will report correct size.
layoutIfNeeded() self.layoutIfNeeded()
let size = super.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: horizontalFittingPriority, verticalFittingPriority: verticalFittingPriority) let size = super.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: horizontalFittingPriority, verticalFittingPriority: verticalFittingPriority)
return size return size
} }
} }

View File

@@ -0,0 +1,570 @@
//
// AppViewController.swift
// AltStore
//
// Created by Riley Testut on 7/22/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
import AltStoreCore
import Roxas
import Nuke
final class AppViewController: UIViewController
{
var app: StoreApp!
private var contentViewController: AppContentViewController!
private var contentViewControllerShadowView: UIView!
private var blurAnimator: UIViewPropertyAnimator?
private var navigationBarAnimator: UIViewPropertyAnimator?
private var contentSizeObservation: NSKeyValueObservation?
@IBOutlet private var scrollView: UIScrollView!
@IBOutlet private var contentView: UIView!
@IBOutlet private var bannerView: AppBannerView!
@IBOutlet private var backButton: UIButton!
@IBOutlet private var backButtonContainerView: UIVisualEffectView!
@IBOutlet private var backgroundAppIconImageView: UIImageView!
@IBOutlet private var backgroundBlurView: UIVisualEffectView!
@IBOutlet private var navigationBarTitleView: UIView!
@IBOutlet private var navigationBarDownloadButton: PillButton!
@IBOutlet private var navigationBarAppIconImageView: UIImageView!
@IBOutlet private var navigationBarAppNameLabel: UILabel!
private var _shouldResetLayout = false
private var _backgroundBlurEffect: UIBlurEffect?
private var _backgroundBlurTintColor: UIColor?
private var _preferredStatusBarStyle: UIStatusBarStyle = .default
override var preferredStatusBarStyle: UIStatusBarStyle {
return _preferredStatusBarStyle
}
override func viewDidLoad()
{
super.viewDidLoad()
self.navigationBarTitleView.sizeToFit()
self.navigationItem.titleView = self.navigationBarTitleView
self.contentViewControllerShadowView = UIView()
self.contentViewControllerShadowView.backgroundColor = .white
self.contentViewControllerShadowView.layer.cornerRadius = 38
self.contentViewControllerShadowView.layer.shadowColor = UIColor.black.cgColor
self.contentViewControllerShadowView.layer.shadowOffset = CGSize(width: 0, height: -1)
self.contentViewControllerShadowView.layer.shadowRadius = 10
self.contentViewControllerShadowView.layer.shadowOpacity = 0.3
self.contentViewController.view.superview?.insertSubview(self.contentViewControllerShadowView, at: 0)
self.contentView.addGestureRecognizer(self.scrollView.panGestureRecognizer)
self.contentViewController.view.layer.cornerRadius = 38
self.contentViewController.view.layer.masksToBounds = true
self.contentViewController.tableView.panGestureRecognizer.require(toFail: self.scrollView.panGestureRecognizer)
self.contentViewController.tableView.showsVerticalScrollIndicator = false
// Bring to front so the scroll indicators are visible.
self.view.bringSubviewToFront(self.scrollView)
self.scrollView.isUserInteractionEnabled = false
self.bannerView.frame = CGRect(x: 0, y: 0, width: 300, height: 93)
self.bannerView.backgroundEffectView.effect = UIBlurEffect(style: .regular)
self.bannerView.backgroundEffectView.backgroundColor = .clear
self.bannerView.iconImageView.image = nil
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
self.contentSizeObservation = self.contentViewController.tableView.observe(\.contentSize) { [weak self] (tableView, change) in
self?.view.setNeedsLayout()
self?.view.layoutIfNeeded()
}
self.update()
NotificationCenter.default.addObserver(self, selector: #selector(AppViewController.didChangeApp(_:)), name: .NSManagedObjectContextObjectsDidChange, object: DatabaseManager.shared.viewContext)
NotificationCenter.default.addObserver(self, selector: #selector(AppViewController.willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(AppViewController.didBecomeActive(_:)), name: UIApplication.didBecomeActiveNotification, object: nil)
self._backgroundBlurEffect = self.backgroundBlurView.effect as? UIBlurEffect
self._backgroundBlurTintColor = self.backgroundBlurView.contentView.backgroundColor
// Load Images
for imageView in [self.bannerView.iconImageView!, self.backgroundAppIconImageView!, self.navigationBarAppIconImageView!]
{
imageView.isIndicatingActivity = true
Nuke.loadImage(with: self.app.iconURL, options: .shared, into: imageView, progress: nil) { [weak imageView] (response, error) in
if response?.image != nil
{
imageView?.isIndicatingActivity = false
}
}
}
}
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
self.prepareBlur()
// Update blur immediately.
self.view.setNeedsLayout()
self.view.layoutIfNeeded()
self.transitionCoordinator?.animate(alongsideTransition: { (context) in
self.hideNavigationBar()
}, completion: nil)
}
override func viewDidAppear(_ animated: Bool)
{
super.viewDidAppear(animated)
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)
if self.navigationController == nil
{
self.resetNavigationBarAnimation()
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
{
guard segue.identifier == "embedAppContentViewController" else { return }
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)
self.navigationItem.scrollEdgeAppearance = self.navigationController?.navigationBar.standardAppearance
}
}
override func viewDidLayoutSubviews()
{
super.viewDidLayoutSubviews()
if self._shouldResetLayout
{
// 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.
self.resetNavigationBarAnimation()
self._shouldResetLayout = false
}
let statusBarHeight = self.view.window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0
let cornerRadius = self.contentViewControllerShadowView.layer.cornerRadius
let inset = 12 as CGFloat
let padding = 20 as CGFloat
let backButtonSize = self.backButton.sizeThatFits(CGSize(width: 1000, height: 1000))
var backButtonFrame = CGRect(x: inset, y: statusBarHeight,
width: backButtonSize.width + 20, height: backButtonSize.height + 20)
var headerFrame = CGRect(x: inset, y: 0, width: self.view.bounds.width - inset * 2, height: self.bannerView.bounds.height)
var contentFrame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.height)
var backgroundIconFrame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.width)
let minimumHeaderY = backButtonFrame.maxY + 8
let minimumContentY = minimumHeaderY + headerFrame.height + padding
let maximumContentY = self.view.bounds.width * 0.667
// A full blur is too much, so we reduce the visible blur by 0.3, resulting in 70% blur.
let minimumBlurFraction = 0.3 as CGFloat
contentFrame.origin.y = maximumContentY - self.scrollView.contentOffset.y
headerFrame.origin.y = contentFrame.origin.y - padding - headerFrame.height
// Stretch the app icon image to fill additional vertical space if necessary.
let height = max(contentFrame.origin.y + cornerRadius * 2, backgroundIconFrame.height)
backgroundIconFrame.size.height = height
let blurThreshold = 0 as CGFloat
if self.scrollView.contentOffset.y < blurThreshold
{
// Determine how much to lessen blur by.
let range = 75 as CGFloat
let difference = -self.scrollView.contentOffset.y
let fraction = min(difference, range) / range
let fractionComplete = (fraction * (1.0 - minimumBlurFraction)) + minimumBlurFraction
self.blurAnimator?.fractionComplete = fractionComplete
}
else
{
// Set blur to default.
self.blurAnimator?.fractionComplete = minimumBlurFraction
}
// Animate navigation bar.
let showNavigationBarThreshold = (maximumContentY - minimumContentY) + backButtonFrame.origin.y
if self.scrollView.contentOffset.y > showNavigationBarThreshold
{
if self.navigationBarAnimator == nil
{
self.prepareNavigationBarAnimation()
}
let difference = self.scrollView.contentOffset.y - showNavigationBarThreshold
let range = (headerFrame.height + padding) - (self.navigationController?.navigationBar.bounds.height ?? self.view.safeAreaInsets.top)
let fractionComplete = min(difference, range) / range
self.navigationBarAnimator?.fractionComplete = fractionComplete
}
else
{
self.resetNavigationBarAnimation()
}
let beginMovingBackButtonThreshold = (maximumContentY - minimumContentY)
if self.scrollView.contentOffset.y > beginMovingBackButtonThreshold
{
let difference = self.scrollView.contentOffset.y - beginMovingBackButtonThreshold
backButtonFrame.origin.y -= difference
}
let pinContentToTopThreshold = maximumContentY
if self.scrollView.contentOffset.y > pinContentToTopThreshold
{
contentFrame.origin.y = 0
backgroundIconFrame.origin.y = 0
let difference = self.scrollView.contentOffset.y - pinContentToTopThreshold
self.contentViewController.tableView.contentOffset.y = difference
}
else
{
// Keep content table view's content offset at the top.
self.contentViewController.tableView.contentOffset.y = 0
}
// Keep background app icon centered in gap between top of content and top of screen.
backgroundIconFrame.origin.y = (contentFrame.origin.y / 2) - backgroundIconFrame.height / 2
// Set frames.
self.contentViewController.view.superview?.frame = contentFrame
self.bannerView.frame = headerFrame
self.backgroundAppIconImageView.frame = backgroundIconFrame
self.backgroundBlurView.frame = backgroundIconFrame
self.backButtonContainerView.frame = backButtonFrame
self.contentViewControllerShadowView.frame = self.contentViewController.view.frame
self.backButtonContainerView.layer.cornerRadius = self.backButtonContainerView.bounds.midY
self.scrollView.verticalScrollIndicatorInsets.top = statusBarHeight
// Adjust content offset + size.
let contentOffset = self.scrollView.contentOffset
var contentSize = self.contentViewController.tableView.contentSize
contentSize.height += maximumContentY
self.scrollView.contentSize = contentSize
self.scrollView.contentOffset = contentOffset
self.bannerView.backgroundEffectView.backgroundColor = .clear
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?)
{
super.traitCollectionDidChange(previousTraitCollection)
self._shouldResetLayout = true
}
deinit
{
self.blurAnimator?.stopAnimation(true)
self.navigationBarAnimator?.stopAnimation(true)
}
}
extension AppViewController
{
final class func makeAppViewController(app: StoreApp) -> AppViewController
{
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let appViewController = storyboard.instantiateViewController(withIdentifier: "appViewController") as! AppViewController
appViewController.app = app
return appViewController
}
}
private extension AppViewController
{
func 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 let versionDate = self.app.latestVersion?.date, versionDate > Date()
{
self.bannerView.button.countdownDate = versionDate
self.navigationBarDownloadButton.countdownDate = versionDate
}
else
{
self.bannerView.button.countdownDate = nil
self.navigationBarDownloadButton.countdownDate = nil
}
let barButtonItem = self.navigationItem.rightBarButtonItem
self.navigationItem.rightBarButtonItem = nil
self.navigationItem.rightBarButtonItem = barButtonItem
}
func showNavigationBar(for navigationController: UINavigationController? = nil)
{
let navigationController = navigationController ?? self.navigationController
navigationController?.navigationBar.alpha = 1.0
navigationController?.navigationBar.tintColor = .altPrimary
navigationController?.navigationBar.setNeedsLayout()
if self.traitCollection.userInterfaceStyle == .dark
{
self._preferredStatusBarStyle = .lightContent
}
else
{
self._preferredStatusBarStyle = .default
}
navigationController?.setNeedsStatusBarAppearanceUpdate()
}
func hideNavigationBar(for navigationController: UINavigationController? = nil)
{
let navigationController = navigationController ?? self.navigationController
navigationController?.navigationBar.alpha = 0.0
self._preferredStatusBarStyle = .lightContent
navigationController?.setNeedsStatusBarAppearanceUpdate()
}
func prepareBlur()
{
if let animator = self.blurAnimator
{
animator.stopAnimation(true)
}
self.backgroundBlurView.effect = self._backgroundBlurEffect
self.backgroundBlurView.contentView.backgroundColor = self._backgroundBlurTintColor
self.blurAnimator = UIViewPropertyAnimator(duration: 1.0, curve: .linear) { [weak self] in
self?.backgroundBlurView.effect = nil
self?.backgroundBlurView.contentView.backgroundColor = .clear
}
self.blurAnimator?.startAnimation()
self.blurAnimator?.pauseAnimation()
}
func prepareNavigationBarAnimation()
{
self.resetNavigationBarAnimation()
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
self?.contentViewController.view.layer.cornerRadius = 0
}
self.navigationBarAnimator?.startAnimation()
self.navigationBarAnimator?.pauseAnimation()
self.update()
}
func resetNavigationBarAnimation()
{
self.navigationBarAnimator?.stopAnimation(true)
self.navigationBarAnimator = nil
self.hideNavigationBar()
self.contentViewController.view.layer.cornerRadius = self.contentViewControllerShadowView.layer.cornerRadius
}
}
extension AppViewController
{
@IBAction func popViewController(_ sender: UIButton)
{
self.navigationController?.popViewController(animated: true)
}
@IBAction func performAppAction(_ sender: PillButton)
{
if let installedApp = self.app.installedApp
{
self.open(installedApp)
}
else
{
self.downloadApp()
}
}
func downloadApp()
{
guard self.app.installedApp == nil else { return }
let group = AppManager.shared.install(self.app, presentingViewController: self) { (result) in
do
{
_ = try result.get()
}
catch OperationError.cancelled
{
// Ignore
}
catch
{
DispatchQueue.main.async {
let toastView = ToastView(error: error)
toastView.show(in: self)
}
}
DispatchQueue.main.async {
self.bannerView.button.progress = nil
self.navigationBarDownloadButton.progress = nil
self.update()
}
}
self.bannerView.button.progress = group.progress
self.navigationBarDownloadButton.progress = group.progress
}
func open(_ installedApp: InstalledApp)
{
UIApplication.shared.open(installedApp.openAppURL)
}
}
private extension AppViewController
{
@objc func didChangeApp(_ notification: Notification)
{
// Async so that AppManager.installationProgress(for:) is nil when we update.
DispatchQueue.main.async {
self.update()
}
}
@objc func willEnterForeground(_ notification: Notification)
{
guard let navigationController = self.navigationController, navigationController.topViewController == self else { return }
self._shouldResetLayout = true
self.view.setNeedsLayout()
}
@objc func didBecomeActive(_ notification: Notification)
{
guard let navigationController = self.navigationController, navigationController.topViewController == self else { return }
// Fixes Navigation Bar appearing after app becomes inactive -> active again.
self._shouldResetLayout = true
self.view.setNeedsLayout()
}
}
extension AppViewController: UIScrollViewDelegate
{
func scrollViewDidScroll(_ scrollView: UIScrollView)
{
self.view.setNeedsLayout()
self.view.layoutIfNeeded()
}
}

View File

@@ -8,18 +8,20 @@
import UIKit import UIKit
import SideStoreCore import AltStoreCore
final class PermissionPopoverViewController: UIViewController { final class PermissionPopoverViewController: UIViewController
{
var permission: AppPermission! var permission: AppPermission!
@IBOutlet private var nameLabel: UILabel! @IBOutlet private var nameLabel: UILabel!
@IBOutlet private var descriptionLabel: UILabel! @IBOutlet private var descriptionLabel: UILabel!
override func viewDidLoad() { override func viewDidLoad()
{
super.viewDidLoad() super.viewDidLoad()
nameLabel.text = permission.type.localizedName self.nameLabel.text = self.permission.type.localizedName
descriptionLabel.text = permission.usageDescription self.descriptionLabel.text = self.permission.usageDescription
} }
} }

View File

@@ -0,0 +1,248 @@
//
// AppIDsViewController.swift
// AltStore
//
// Created by Riley Testut on 1/27/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import UIKit
import AltStoreCore
import Roxas
final class AppIDsViewController: UICollectionViewController
{
private lazy var dataSource = self.makeDataSource()
private var didInitialFetch = false
private var isLoading = false {
didSet {
self.update()
}
}
@IBOutlet var activityIndicatorBarButtonItem: UIBarButtonItem!
override func viewDidLoad()
{
super.viewDidLoad()
self.collectionView.dataSource = self.dataSource
self.activityIndicatorBarButtonItem.isIndicatingActivity = true
let refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: #selector(AppIDsViewController.fetchAppIDs), for: .primaryActionTriggered)
self.collectionView.refreshControl = refreshControl
}
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
if !self.didInitialFetch
{
self.fetchAppIDs()
}
}
}
private extension AppIDsViewController
{
func makeDataSource() -> RSTFetchedResultsCollectionViewDataSource<AppID>
{
let fetchRequest = AppID.fetchRequest() as NSFetchRequest<AppID>
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \AppID.name, ascending: true),
NSSortDescriptor(keyPath: \AppID.bundleIdentifier, ascending: true),
NSSortDescriptor(keyPath: \AppID.expirationDate, ascending: true)]
fetchRequest.returnsObjectsAsFaults = false
if let team = DatabaseManager.shared.activeTeam()
{
fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(AppID.team), team)
}
else
{
fetchRequest.predicate = NSPredicate(value: false)
}
let dataSource = RSTFetchedResultsCollectionViewDataSource<AppID>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
dataSource.proxy = self
dataSource.cellConfigurationHandler = { (cell, appID, indexPath) in
let tintColor = UIColor.altPrimary
let cell = cell as! BannerCollectionViewCell
cell.layoutMargins.left = self.view.layoutMargins.left
cell.layoutMargins.right = self.view.layoutMargins.right
cell.tintColor = tintColor
cell.bannerView.iconImageView.isHidden = true
cell.bannerView.button.isIndicatingActivity = false
cell.bannerView.buttonLabel.text = NSLocalizedString("Expires in", comment: "")
let attributedAccessibilityLabel = NSMutableAttributedString(string: appID.name + ". ")
if let expirationDate = appID.expirationDate
{
cell.bannerView.button.isHidden = false
cell.bannerView.button.isUserInteractionEnabled = false
cell.bannerView.buttonLabel.isHidden = false
let currentDate = Date()
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .full
formatter.includesApproximationPhrase = false
formatter.includesTimeRemainingPhrase = false
formatter.allowedUnits = [.minute, .hour, .day]
formatter.maximumUnitCount = 1
cell.bannerView.button.setTitle((formatter.string(from: currentDate, to: expirationDate) ?? NSLocalizedString("Unknown", comment: "")).uppercased(), for: .normal)
// formatter.includesTimeRemainingPhrase = true
// attributedAccessibilityLabel.mutableString.append((formatter.string(from: currentDate, to: expirationDate) ?? NSLocalizedString("Unknown", comment: "")) + " ")
}
else
{
cell.bannerView.button.isHidden = true
cell.bannerView.button.isUserInteractionEnabled = true
cell.bannerView.buttonLabel.isHidden = true
}
cell.bannerView.titleLabel.text = appID.name
cell.bannerView.subtitleLabel.text = appID.bundleIdentifier
cell.bannerView.subtitleLabel.numberOfLines = 2
let attributedBundleIdentifier = NSMutableAttributedString(string: appID.bundleIdentifier.lowercased(), attributes: [.accessibilitySpeechPunctuation: true])
if let team = appID.team, let range = attributedBundleIdentifier.string.range(of: team.identifier.lowercased()), #available(iOS 13, *)
{
// Prefer to speak the team ID one character at a time.
let nsRange = NSRange(range, in: attributedBundleIdentifier.string)
attributedBundleIdentifier.addAttributes([.accessibilitySpeechSpellOut: true], range: nsRange)
}
attributedAccessibilityLabel.append(attributedBundleIdentifier)
cell.bannerView.accessibilityAttributedLabel = attributedAccessibilityLabel
// Make sure refresh button is correct size.
cell.layoutIfNeeded()
}
return dataSource
}
@objc func fetchAppIDs()
{
guard !self.isLoading else { return }
self.isLoading = true
AppManager.shared.fetchAppIDs { (result) in
do
{
let (_, context) = try result.get()
try context.save()
}
catch
{
DispatchQueue.main.async {
let toastView = ToastView(error: error)
toastView.show(in: self)
}
}
DispatchQueue.main.async {
self.isLoading = false
}
}
}
func update()
{
if !self.isLoading
{
self.collectionView.refreshControl?.endRefreshing()
self.activityIndicatorBarButtonItem.isIndicatingActivity = false
}
}
}
extension AppIDsViewController: UICollectionViewDelegateFlowLayout
{
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
return CGSize(width: collectionView.bounds.width, height: 80)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize
{
let indexPath = IndexPath(row: 0, section: section)
let headerView = self.collectionView(collectionView, viewForSupplementaryElementOfKind: UICollectionView.elementKindSectionHeader, at: indexPath)
// Use this view to calculate the optimal size based on the collection view's width
let size = headerView.systemLayoutSizeFitting(CGSize(width: collectionView.frame.width, height: UIView.layoutFittingExpandedSize.height),
withHorizontalFittingPriority: .required, // Width is fixed
verticalFittingPriority: .fittingSizeLevel) // Height can be as large as needed
return size
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize
{
return CGSize(width: collectionView.bounds.width, height: 50)
}
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView
{
switch kind
{
case UICollectionView.elementKindSectionHeader:
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Header", for: indexPath) as! TextCollectionReusableView
headerView.layoutMargins.left = self.view.layoutMargins.left
headerView.layoutMargins.right = self.view.layoutMargins.right
if let activeTeam = DatabaseManager.shared.activeTeam(), activeTeam.type == .free
{
let text = NSLocalizedString("""
Each app and app extension installed with SideStore must register an App ID with Apple. Apple limits non-developer Apple IDs to 10 App IDs at a time.
**App IDs can't be deleted**, but they do expire after one week. SideStore will automatically renew App IDs for all active apps once they've expired.
""", comment: "")
let attributedText = NSAttributedString(markdownRepresentation: text, attributes: [.font: headerView.textLabel.font as Any])
headerView.textLabel.attributedText = attributedText
}
else
{
headerView.textLabel.text = NSLocalizedString("""
Each app and app extension installed with SideStore must register an App ID with Apple.
App IDs for paid developer accounts never expire, and there is no limit to how many you can create.
""", comment: "")
}
return headerView
case UICollectionView.elementKindSectionFooter:
let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Footer", for: indexPath) as! TextCollectionReusableView
let count = self.dataSource.itemCount
if count == 1
{
footerView.textLabel.text = NSLocalizedString("1 App ID", comment: "")
}
else
{
footerView.textLabel.text = String(format: NSLocalizedString("%@ App IDs", comment: ""), NSNumber(value: count))
}
return footerView
default: fatalError()
}
}
}

View File

@@ -6,162 +6,192 @@
// Copyright © 2019 Riley Testut. All rights reserved. // Copyright © 2019 Riley Testut. All rights reserved.
// //
import AVFoundation
import Intents
import UIKit import UIKit
import UserNotifications import UserNotifications
import OSLog import AVFoundation
#if canImport(Logging) import Intents
import Logging
#endif
import AltStoreCore
import AltSign import AltSign
import SideStoreCore import Roxas
import SideStoreAppKit
import EmotionalDamage import EmotionalDamage
import RoxasUIKit
extension AppDelegate
{
static let openPatreonSettingsDeepLinkNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".OpenPatreonSettingsDeepLinkNotification")
static let importAppDeepLinkNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".ImportAppDeepLinkNotification")
static let addSourceDeepLinkNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".AddSourceDeepLinkNotification")
static let appBackupDidFinish = Notification.Name(Bundle.Info.appbundleIdentifier + ".AppBackupDidFinish")
static let importAppDeepLinkURLKey = "fileURL"
static let appBackupResultKey = "result"
static let addSourceDeepLinkURLKey = "sourceURL"
}
@UIApplicationMain @UIApplicationMain
final class AppDelegate: SideStoreAppDelegate { final class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var window: UIWindow?
@available(iOS 14, *) @available(iOS 14, *)
private var intentHandler: IntentHandler { private var intentHandler: IntentHandler {
get { _intentHandler as! IntentHandler } get { _intentHandler as! IntentHandler }
set { _intentHandler = newValue } set { _intentHandler = newValue }
} }
@available(iOS 14, *) @available(iOS 14, *)
private var viewAppIntentHandler: ViewAppIntentHandler { private var viewAppIntentHandler: ViewAppIntentHandler {
get { _viewAppIntentHandler as! ViewAppIntentHandler } get { _viewAppIntentHandler as! ViewAppIntentHandler }
set { _viewAppIntentHandler = newValue } set { _viewAppIntentHandler = newValue }
} }
private lazy var _intentHandler: Any = { private lazy var _intentHandler: Any = {
guard #available(iOS 14, *) else { fatalError() } guard #available(iOS 14, *) else { fatalError() }
return IntentHandler() return IntentHandler()
}() }()
private lazy var _viewAppIntentHandler: Any = { private lazy var _viewAppIntentHandler: Any = {
guard #available(iOS 14, *) else { fatalError() } guard #available(iOS 14, *) else { fatalError() }
return ViewAppIntentHandler() return ViewAppIntentHandler()
}() }()
func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
{
// Register default settings before doing anything else. // Register default settings before doing anything else.
UserDefaults.registerDefaults() UserDefaults.registerDefaults()
DatabaseManager.shared.start { error in DatabaseManager.shared.start { (error) in
if let error = error { if let error = error
os_log("Failed to start DatabaseManager. Error: %@", type: .error , error.localizedDescription) {
} else { print("Failed to start DatabaseManager. Error:", error as Any)
os_log("Started DatabaseManager.", type: .info) }
let transformer = ALTAppPermissionTypeTransformer() else
ValueTransformer.setValueTransformer(transformer, forName: NSValueTransformerName(rawValue: "ALTAppPermissionTypeTransformer")) {
print("Started DatabaseManager.")
} }
} }
AnalyticsManager.shared.start() AnalyticsManager.shared.start()
setTintColor() self.setTintColor()
SecureValueTransformer.register() SecureValueTransformer.register()
if UserDefaults.standard.firstLaunch == nil { if UserDefaults.standard.firstLaunch == nil
{
Keychain.shared.reset() Keychain.shared.reset()
UserDefaults.standard.firstLaunch = Date() UserDefaults.standard.firstLaunch = Date()
} }
UserDefaults.standard.preferredServerID = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.serverID) as? String UserDefaults.standard.preferredServerID = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.serverID) as? String
#if DEBUG || BETA #if DEBUG || BETA
UserDefaults.standard.isDebugModeEnabled = true UserDefaults.standard.isDebugModeEnabled = true
#endif #endif
prepareForBackgroundFetch() self.prepareForBackgroundFetch()
return true return true
} }
func applicationDidEnterBackground(_: UIApplication) { func applicationDidEnterBackground(_ application: UIApplication)
{
// Make sure to update SceneDelegate.sceneDidEnterBackground() as well. // Make sure to update SceneDelegate.sceneDidEnterBackground() as well.
guard let oneMonthAgo = Calendar.current.date(byAdding: .month, value: -1, to: Date()) else { return } guard let oneMonthAgo = Calendar.current.date(byAdding: .month, value: -1, to: Date()) else { return }
let midnightOneMonthAgo = Calendar.current.startOfDay(for: oneMonthAgo) let midnightOneMonthAgo = Calendar.current.startOfDay(for: oneMonthAgo)
DatabaseManager.shared.purgeLoggedErrors(before: midnightOneMonthAgo) { result in DatabaseManager.shared.purgeLoggedErrors(before: midnightOneMonthAgo) { result in
switch result { switch result
{
case .success: break case .success: break
case let .failure(error): os_log("[ALTLog] Failed to purge logged errors before %@. %@", type: .error , midnightOneMonthAgo.debugDescription, error.localizedDescription) case .failure(let error): print("[ALTLog] Failed to purge logged errors before \(midnightOneMonthAgo).", error)
} }
} }
} }
func applicationWillEnterForeground(_: UIApplication) { func applicationWillEnterForeground(_ application: UIApplication)
{
AppManager.shared.update() AppManager.shared.update()
start_em_proxy(bind_addr: Consts.Proxy.serverURL) start_em_proxy(bind_addr: Consts.Proxy.serverURL)
} }
func application(_: UIApplication, open url: URL, options _: [UIApplication.OpenURLOptionsKey: Any]) -> Bool { func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool
open(url) {
return self.open(url)
} }
func application(_: UIApplication, handlerFor intent: INIntent) -> Any? { func application(_ application: UIApplication, handlerFor intent: INIntent) -> Any?
{
guard #available(iOS 14, *) else { return nil } guard #available(iOS 14, *) else { return nil }
switch intent { switch intent
case is RefreshAllIntent: return intentHandler {
case is ViewAppIntent: return viewAppIntentHandler case is RefreshAllIntent: return self.intentHandler
case is ViewAppIntent: return self.viewAppIntentHandler
default: return nil default: return nil
} }
} }
} }
@available(iOS 13, *) @available(iOS 13, *)
extension AppDelegate { extension AppDelegate
func application(_: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options _: UIScene.ConnectionOptions) -> UISceneConfiguration { {
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration
{
// Called when a new scene session is being created. // Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with. // Use this method to select a configuration to create the new scene with.
UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
} }
func application(_: UIApplication, didDiscardSceneSessions _: Set<UISceneSession>) { func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>)
{
// Called when the user discards a scene session. // Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return. // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
} }
} }
private extension AppDelegate { private extension AppDelegate
func setTintColor() { {
window?.tintColor = .altPrimary func setTintColor()
{
self.window?.tintColor = .altPrimary
} }
func open(_ url: URL) -> Bool { func open(_ url: URL) -> Bool
if url.isFileURL { {
if url.isFileURL
{
guard url.pathExtension.lowercased() == "ipa" else { return false } guard url.pathExtension.lowercased() == "ipa" else { return false }
DispatchQueue.main.async { DispatchQueue.main.async {
NotificationCenter.default.post(name: AppDelegate.importAppDeepLinkNotification, object: nil, userInfo: [AppDelegate.importAppDeepLinkURLKey: url]) NotificationCenter.default.post(name: AppDelegate.importAppDeepLinkNotification, object: nil, userInfo: [AppDelegate.importAppDeepLinkURLKey: url])
} }
return true return true
} else { }
else
{
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false } guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false }
guard let host = components.host?.lowercased() else { return false } guard let host = components.host?.lowercased() else { return false }
switch host { switch host
{
case "patreon": case "patreon":
DispatchQueue.main.async { DispatchQueue.main.async {
NotificationCenter.default.post(name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil) NotificationCenter.default.post(name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
} }
return true return true
case "appbackupresponse": case "appbackupresponse":
let result: Result<Void, Error> let result: Result<Void, Error>
switch url.path.lowercased() { switch url.path.lowercased()
{
case "/success": result = .success(()) case "/success": result = .success(())
case "/failure": case "/failure":
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name] = $1.value } ?? [:] let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name] = $1.value } ?? [:]
@@ -170,193 +200,216 @@ private extension AppDelegate {
let errorCodeString = queryItems["errorCode"], let errorCode = Int(errorCodeString), let errorCodeString = queryItems["errorCode"], let errorCode = Int(errorCodeString),
let errorDescription = queryItems["errorDescription"] let errorDescription = queryItems["errorDescription"]
else { return false } else { return false }
let error = NSError(domain: errorDomain, code: errorCode, userInfo: [NSLocalizedDescriptionKey: errorDescription]) let error = NSError(domain: errorDomain, code: errorCode, userInfo: [NSLocalizedDescriptionKey: errorDescription])
result = .failure(error) result = .failure(error)
default: return false default: return false
} }
NotificationCenter.default.post(name: AppDelegate.appBackupDidFinish, object: nil, userInfo: [AppDelegate.appBackupResultKey: result]) NotificationCenter.default.post(name: AppDelegate.appBackupDidFinish, object: nil, userInfo: [AppDelegate.appBackupResultKey: result])
return true return true
case "install": case "install":
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:] let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:]
guard let downloadURLString = queryItems["url"], let downloadURL = URL(string: downloadURLString) else { return false } guard let downloadURLString = queryItems["url"], let downloadURL = URL(string: downloadURLString) else { return false }
DispatchQueue.main.async { DispatchQueue.main.async {
NotificationCenter.default.post(name: AppDelegate.importAppDeepLinkNotification, object: nil, userInfo: [AppDelegate.importAppDeepLinkURLKey: downloadURL]) NotificationCenter.default.post(name: AppDelegate.importAppDeepLinkNotification, object: nil, userInfo: [AppDelegate.importAppDeepLinkURLKey: downloadURL])
} }
return true return true
case "source": case "source":
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:] let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:]
guard let sourceURLString = queryItems["url"], let sourceURL = URL(string: sourceURLString) else { return false } guard let sourceURLString = queryItems["url"], let sourceURL = URL(string: sourceURLString) else { return false }
DispatchQueue.main.async { DispatchQueue.main.async {
NotificationCenter.default.post(name: AppDelegate.addSourceDeepLinkNotification, object: nil, userInfo: [AppDelegate.addSourceDeepLinkURLKey: sourceURL]) NotificationCenter.default.post(name: AppDelegate.addSourceDeepLinkNotification, object: nil, userInfo: [AppDelegate.addSourceDeepLinkURLKey: sourceURL])
} }
return true return true
default: return false default: return false
} }
} }
} }
} }
extension AppDelegate { extension AppDelegate
private func prepareForBackgroundFetch() { {
private func prepareForBackgroundFetch()
{
// "Fetch" every hour, but then refresh only those that need to be refreshed (so we don't drain the battery). // "Fetch" every hour, but then refresh only those that need to be refreshed (so we don't drain the battery).
UIApplication.shared.setMinimumBackgroundFetchInterval(1 * 60 * 60) UIApplication.shared.setMinimumBackgroundFetchInterval(1 * 60 * 60)
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { _, _ in UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { (success, error) in
} }
#if DEBUG #if DEBUG
UIApplication.shared.registerForRemoteNotifications() UIApplication.shared.registerForRemoteNotifications()
#endif #endif
} }
func application(_: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data)
{ {
let tokenParts = deviceToken.map { data -> String in let tokenParts = deviceToken.map { data -> String in
String(format: "%02.2hhx", data) return String(format: "%02.2hhx", data)
} }
let token = tokenParts.joined() let token = tokenParts.joined()
os_log("Push Token: %@", type: .debug , token) print("Push Token:", token)
} }
func application(_ application: UIApplication, didReceiveRemoteNotification _: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void)
{
self.application(application, performFetchWithCompletionHandler: completionHandler) self.application(application, performFetchWithCompletionHandler: completionHandler)
} }
func application(_: UIApplication, performFetchWithCompletionHandler backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void) { func application(_ application: UIApplication, performFetchWithCompletionHandler backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void)
if UserDefaults.standard.isBackgroundRefreshEnabled && !UserDefaults.standard.presentedLaunchReminderNotification { {
if UserDefaults.standard.isBackgroundRefreshEnabled && !UserDefaults.standard.presentedLaunchReminderNotification
{
let threeHours: TimeInterval = 3 * 60 * 60 let threeHours: TimeInterval = 3 * 60 * 60
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: threeHours, repeats: false) let trigger = UNTimeIntervalNotificationTrigger(timeInterval: threeHours, repeats: false)
let content = UNMutableNotificationContent() let content = UNMutableNotificationContent()
content.title = NSLocalizedString("App Refresh Tip", comment: "") content.title = NSLocalizedString("App Refresh Tip", comment: "")
content.body = NSLocalizedString("The more you open SideStore, the more chances it's given to refresh apps in the background.", comment: "") content.body = NSLocalizedString("The more you open SideStore, the more chances it's given to refresh apps in the background.", comment: "")
let request = UNNotificationRequest(identifier: "background-refresh-reminder5", content: content, trigger: trigger) let request = UNNotificationRequest(identifier: "background-refresh-reminder5", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request) UNUserNotificationCenter.current().add(request)
UserDefaults.standard.presentedLaunchReminderNotification = true UserDefaults.standard.presentedLaunchReminderNotification = true
} }
BackgroundTaskManager.shared.performExtendedBackgroundTask { taskResult, taskCompletionHandler in BackgroundTaskManager.shared.performExtendedBackgroundTask { (taskResult, taskCompletionHandler) in
if let error = taskResult.error { if let error = taskResult.error
os_log("Error starting extended background task. Aborting. %@", type: .error, error.localizedDescription) {
print("Error starting extended background task. Aborting.", error)
backgroundFetchCompletionHandler(.failed) backgroundFetchCompletionHandler(.failed)
taskCompletionHandler() taskCompletionHandler()
return return
} }
if !DatabaseManager.shared.isStarted { if !DatabaseManager.shared.isStarted
DatabaseManager.shared.start { error in {
if error != nil { DatabaseManager.shared.start() { (error) in
if error != nil
{
backgroundFetchCompletionHandler(.failed) backgroundFetchCompletionHandler(.failed)
taskCompletionHandler() taskCompletionHandler()
} else { }
self.performBackgroundFetch { backgroundFetchResult in else
{
self.performBackgroundFetch { (backgroundFetchResult) in
backgroundFetchCompletionHandler(backgroundFetchResult) backgroundFetchCompletionHandler(backgroundFetchResult)
} refreshAppsCompletionHandler: { _ in } refreshAppsCompletionHandler: { (refreshAppsResult) in
taskCompletionHandler() taskCompletionHandler()
} }
} }
} }
} else { }
self.performBackgroundFetch { backgroundFetchResult in else
{
self.performBackgroundFetch { (backgroundFetchResult) in
backgroundFetchCompletionHandler(backgroundFetchResult) backgroundFetchCompletionHandler(backgroundFetchResult)
} refreshAppsCompletionHandler: { _ in } refreshAppsCompletionHandler: { (refreshAppsResult) in
taskCompletionHandler() taskCompletionHandler()
} }
} }
} }
} }
func performBackgroundFetch(backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void, func performBackgroundFetch(backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void,
refreshAppsCompletionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void) { refreshAppsCompletionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void)
fetchSources { result in {
switch result { self.fetchSources { (result) in
switch result
{
case .failure: backgroundFetchCompletionHandler(.failed) case .failure: backgroundFetchCompletionHandler(.failed)
case .success: backgroundFetchCompletionHandler(.newData) case .success: backgroundFetchCompletionHandler(.newData)
} }
if !UserDefaults.standard.isBackgroundRefreshEnabled { if !UserDefaults.standard.isBackgroundRefreshEnabled
{
refreshAppsCompletionHandler(.success([:])) refreshAppsCompletionHandler(.success([:]))
} }
} }
guard UserDefaults.standard.isBackgroundRefreshEnabled else { return } guard UserDefaults.standard.isBackgroundRefreshEnabled else { return }
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
let installedApps = InstalledApp.fetchAppsForBackgroundRefresh(in: context) let installedApps = InstalledApp.fetchAppsForBackgroundRefresh(in: context)
AppManager.shared.backgroundRefresh(installedApps, completionHandler: refreshAppsCompletionHandler) AppManager.shared.backgroundRefresh(installedApps, completionHandler: refreshAppsCompletionHandler)
} }
} }
} }
private extension AppDelegate { private extension AppDelegate
func fetchSources(completionHandler: @escaping (Result<Set<Source>, Error>) -> Void) { {
AppManager.shared.fetchSources { result in func fetchSources(completionHandler: @escaping (Result<Set<Source>, Error>) -> Void)
do { {
AppManager.shared.fetchSources() { (result) in
do
{
let (sources, context) = try result.get() let (sources, context) = try result.get()
let previousUpdatesFetchRequest = InstalledApp.updatesFetchRequest() as! NSFetchRequest<NSFetchRequestResult> let previousUpdatesFetchRequest = InstalledApp.updatesFetchRequest() as! NSFetchRequest<NSFetchRequestResult>
previousUpdatesFetchRequest.includesPendingChanges = false previousUpdatesFetchRequest.includesPendingChanges = false
previousUpdatesFetchRequest.resultType = .dictionaryResultType previousUpdatesFetchRequest.resultType = .dictionaryResultType
previousUpdatesFetchRequest.propertiesToFetch = [#keyPath(InstalledApp.bundleIdentifier)] previousUpdatesFetchRequest.propertiesToFetch = [#keyPath(InstalledApp.bundleIdentifier)]
let previousNewsItemsFetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NSFetchRequestResult> let previousNewsItemsFetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NSFetchRequestResult>
previousNewsItemsFetchRequest.includesPendingChanges = false previousNewsItemsFetchRequest.includesPendingChanges = false
previousNewsItemsFetchRequest.resultType = .dictionaryResultType previousNewsItemsFetchRequest.resultType = .dictionaryResultType
previousNewsItemsFetchRequest.propertiesToFetch = [#keyPath(NewsItem.identifier)] previousNewsItemsFetchRequest.propertiesToFetch = [#keyPath(NewsItem.identifier)]
let previousUpdates = try context.fetch(previousUpdatesFetchRequest) as! [[String: String]] let previousUpdates = try context.fetch(previousUpdatesFetchRequest) as! [[String: String]]
let previousNewsItems = try context.fetch(previousNewsItemsFetchRequest) as! [[String: String]] let previousNewsItems = try context.fetch(previousNewsItemsFetchRequest) as! [[String: String]]
try context.save() try context.save()
let updatesFetchRequest = InstalledApp.updatesFetchRequest() let updatesFetchRequest = InstalledApp.updatesFetchRequest()
let newsItemsFetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NewsItem> let newsItemsFetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NewsItem>
let updates = try context.fetch(updatesFetchRequest) let updates = try context.fetch(updatesFetchRequest)
let newsItems = try context.fetch(newsItemsFetchRequest) let newsItems = try context.fetch(newsItemsFetchRequest)
for update in updates { for update in updates
{
guard !previousUpdates.contains(where: { $0[#keyPath(InstalledApp.bundleIdentifier)] == update.bundleIdentifier }) else { continue } guard !previousUpdates.contains(where: { $0[#keyPath(InstalledApp.bundleIdentifier)] == update.bundleIdentifier }) else { continue }
guard let storeApp = update.storeApp, let version = storeApp.version else { continue } guard let storeApp = update.storeApp, let version = storeApp.version else { continue }
let content = UNMutableNotificationContent() let content = UNMutableNotificationContent()
content.title = NSLocalizedString("New Update Available", comment: "") content.title = NSLocalizedString("New Update Available", comment: "")
content.body = String(format: NSLocalizedString("%@ %@ is now available for download.", comment: ""), update.name, version) content.body = String(format: NSLocalizedString("%@ %@ is now available for download.", comment: ""), update.name, version)
content.sound = .default content.sound = .default
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
UNUserNotificationCenter.current().add(request) UNUserNotificationCenter.current().add(request)
} }
for newsItem in newsItems { for newsItem in newsItems
{
guard !previousNewsItems.contains(where: { $0[#keyPath(NewsItem.identifier)] == newsItem.identifier }) else { continue } guard !previousNewsItems.contains(where: { $0[#keyPath(NewsItem.identifier)] == newsItem.identifier }) else { continue }
guard !newsItem.isSilent else { continue } guard !newsItem.isSilent else { continue }
let content = UNMutableNotificationContent() let content = UNMutableNotificationContent()
if let app = newsItem.storeApp { if let app = newsItem.storeApp
{
content.title = String(format: NSLocalizedString("%@ News", comment: ""), app.name) content.title = String(format: NSLocalizedString("%@ News", comment: ""), app.name)
} else { }
else
{
content.title = NSLocalizedString("SideStore News", comment: "") content.title = NSLocalizedString("SideStore News", comment: "")
} }
content.body = newsItem.title content.body = newsItem.title
content.sound = .default content.sound = .default
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
UNUserNotificationCenter.current().add(request) UNUserNotificationCenter.current().add(request)
} }
@@ -364,10 +417,12 @@ private extension AppDelegate {
DispatchQueue.main.async { DispatchQueue.main.async {
UIApplication.shared.applicationIconBadgeNumber = updates.count UIApplication.shared.applicationIconBadgeNumber = updates.count
} }
completionHandler(.success(sources)) completionHandler(.success(sources))
} catch { }
os_log("Error fetching apps: %@", type: .error, error.localizedDescription) catch
{
print("Error fetching apps:", error)
completionHandler(.failure(error)) completionHandler(.failure(error))
} }
} }

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_12" orientation="portrait" appearance="light"/> <device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
<capability name="Named colors" minToolsVersion="9.0"/> <capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@@ -12,9 +12,9 @@
<!--Navigation Controller--> <!--Navigation Controller-->
<scene sceneID="lNR-II-WoW"> <scene sceneID="lNR-II-WoW">
<objects> <objects>
<navigationController storyboardIdentifier="navigationController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="ZTo-53-dSL" sceneMemberID="viewController"> <navigationController storyboardIdentifier="navigationController" id="ZTo-53-dSL" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" barStyle="black" largeTitles="YES" id="Aej-RF-PfV" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target"> <navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" barStyle="black" largeTitles="YES" id="Aej-RF-PfV" customClass="NavigationBar" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="59" width="393" height="96"/> <rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="barTintColor" name="SettingsBackground"/> <color key="barTintColor" name="SettingsBackground"/>
@@ -36,34 +36,34 @@
<!--Authentication View Controller--> <!--Authentication View Controller-->
<scene sceneID="OCd-xc-Ms7"> <scene sceneID="OCd-xc-Ms7">
<objects> <objects>
<viewController storyboardIdentifier="authenticationViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="yO1-iT-7NP" customClass="AuthenticationViewController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <viewController storyboardIdentifier="authenticationViewController" id="yO1-iT-7NP" customClass="AuthenticationViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="mjy-4S-hyH"> <view key="view" contentMode="scaleToFill" id="mjy-4S-hyH">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<view hidden="YES" userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="oyW-Fd-ojD" userLabel="Sizing View"> <view hidden="YES" userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="oyW-Fd-ojD" userLabel="Sizing View">
<rect key="frame" x="0.0" y="103" width="393" height="715"/> <rect key="frame" x="0.0" y="44" width="375" height="623"/>
</view> </view>
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" alwaysBounceVertical="YES" indicatorStyle="white" keyboardDismissMode="onDrag" translatesAutoresizingMaskIntoConstraints="NO" id="WXx-hX-AXv"> <scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" alwaysBounceVertical="YES" indicatorStyle="white" keyboardDismissMode="onDrag" translatesAutoresizingMaskIntoConstraints="NO" id="WXx-hX-AXv">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<subviews> <subviews>
<view contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2wp-qG-f0Z"> <view contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2wp-qG-f0Z">
<rect key="frame" x="0.0" y="0.0" width="393" height="715"/> <rect key="frame" x="0.0" y="0.0" width="375" height="623"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" spacing="50" translatesAutoresizingMaskIntoConstraints="NO" id="YmX-7v-pxh"> <stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" spacing="50" translatesAutoresizingMaskIntoConstraints="NO" id="YmX-7v-pxh">
<rect key="frame" x="16" y="6" width="361" height="359"/> <rect key="frame" x="16" y="6" width="343" height="359.5"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="Yfu-hI-0B7" userLabel="Welcome"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="Yfu-hI-0B7" userLabel="Welcome">
<rect key="frame" x="0.0" y="0.0" width="361" height="67"/> <rect key="frame" x="0.0" y="0.0" width="343" height="67.5"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Welcome to SideStore." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="EI2-V3-zQZ"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Welcome to SideStore." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="EI2-V3-zQZ">
<rect key="frame" x="0.0" y="0.0" width="357.33333333333331" height="40.666666666666664"/> <rect key="frame" x="0.0" y="0.0" width="332" height="41"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="34"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="34"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Sign in with your Apple ID to get started." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="SNU-tv-8Au"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Sign in with your Apple ID to get started." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="SNU-tv-8Au">
<rect key="frame" x="0.0" y="46.666666666666664" width="306.33333333333331" height="20.333333333333336"/> <rect key="frame" x="0.0" y="47" width="306.5" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/> <fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@@ -71,19 +71,19 @@
</subviews> </subviews>
</stackView> </stackView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="32" translatesAutoresizingMaskIntoConstraints="NO" id="Aqh-MD-HFf"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="32" translatesAutoresizingMaskIntoConstraints="NO" id="Aqh-MD-HFf">
<rect key="frame" x="0.0" y="117" width="361" height="242"/> <rect key="frame" x="0.0" y="117.5" width="343" height="242"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="Oy6-xr-cZ7"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="Oy6-xr-cZ7">
<rect key="frame" x="0.0" y="0.0" width="361" height="159"/> <rect key="frame" x="0.0" y="0.0" width="343" height="159"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="H95-7V-Kk8" userLabel="Apple ID"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="H95-7V-Kk8" userLabel="Apple ID">
<rect key="frame" x="0.0" y="0.0" width="361" height="72"/> <rect key="frame" x="0.0" y="0.0" width="343" height="72"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="KN1-Kp-M1q"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="KN1-Kp-M1q">
<rect key="frame" x="0.0" y="0.0" width="361" height="17"/> <rect key="frame" x="0.0" y="0.0" width="343" height="17"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="APPLE ID" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="59N-O1-6bM"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="APPLE ID" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="59N-O1-6bM">
<rect key="frame" x="14" y="0.0" width="347" height="17"/> <rect key="frame" x="14" y="0.0" width="329" height="17"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/> <fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" white="1" alpha="0.75" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="0.75" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@@ -92,10 +92,10 @@
<edgeInsets key="layoutMargins" top="0.0" left="14" bottom="0.0" right="0.0"/> <edgeInsets key="layoutMargins" top="0.0" left="14" bottom="0.0" right="0.0"/>
</stackView> </stackView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="gNe-dC-oI1"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="gNe-dC-oI1">
<rect key="frame" x="0.0" y="21" width="361" height="51"/> <rect key="frame" x="0.0" y="21" width="343" height="51"/>
<subviews> <subviews>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="name@email.com" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="DBu-vt-hlo"> <textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="name@email.com" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="DBu-vt-hlo">
<rect key="frame" x="14" y="0.0" width="333" height="51"/> <rect key="frame" x="14" y="0.0" width="315" height="51"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="19"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="19"/>
@@ -118,13 +118,13 @@
</subviews> </subviews>
</stackView> </stackView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="hd5-yc-rcq" userLabel="Password"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="hd5-yc-rcq" userLabel="Password">
<rect key="frame" x="0.0" y="87" width="361" height="72"/> <rect key="frame" x="0.0" y="87" width="343" height="72"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="lvX-im-C95"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="lvX-im-C95">
<rect key="frame" x="0.0" y="0.0" width="361" height="17"/> <rect key="frame" x="0.0" y="0.0" width="343" height="17"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="PASSWORD" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ava-XY-7vs"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="PASSWORD" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ava-XY-7vs">
<rect key="frame" x="14" y="0.0" width="347" height="17"/> <rect key="frame" x="14" y="0.0" width="329" height="17"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/> <fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" white="1" alpha="0.75" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="0.75" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@@ -133,10 +133,10 @@
<edgeInsets key="layoutMargins" top="0.0" left="14" bottom="0.0" right="0.0"/> <edgeInsets key="layoutMargins" top="0.0" left="14" bottom="0.0" right="0.0"/>
</stackView> </stackView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="cLc-iA-yq5"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="cLc-iA-yq5">
<rect key="frame" x="0.0" y="21" width="361" height="51"/> <rect key="frame" x="0.0" y="21" width="343" height="51"/>
<subviews> <subviews>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="••••••••" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="R77-TQ-lVT"> <textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="••••••••" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="R77-TQ-lVT">
<rect key="frame" x="14" y="0.0" width="333" height="51"/> <rect key="frame" x="14" y="0.0" width="315" height="51"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="19"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="19"/>
@@ -161,7 +161,7 @@
</subviews> </subviews>
</stackView> </stackView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="2N5-zd-fUj"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="2N5-zd-fUj">
<rect key="frame" x="0.0" y="191" width="361" height="51"/> <rect key="frame" x="0.0" y="191" width="343" height="51"/>
<color key="backgroundColor" name="SettingsHighlighted"/> <color key="backgroundColor" name="SettingsHighlighted"/>
<constraints> <constraints>
<constraint firstAttribute="height" constant="51" id="4BK-Un-5pl"/> <constraint firstAttribute="height" constant="51" id="4BK-Un-5pl"/>
@@ -179,16 +179,16 @@
</subviews> </subviews>
</stackView> </stackView>
<stackView opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="250" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="DBk-rT-ZE8"> <stackView opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="250" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="DBk-rT-ZE8">
<rect key="frame" x="16" y="611" width="361" height="96"/> <rect key="frame" x="16" y="518.5" width="343" height="96.5"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Why do we need this?" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="p9U-0q-Kn8"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Why do we need this?" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="p9U-0q-Kn8">
<rect key="frame" x="0.0" y="0.0" width="361" height="20.333333333333332"/> <rect key="frame" x="0.0" y="0.0" width="343" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="249" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" minimumScaleFactor="0.25" translatesAutoresizingMaskIntoConstraints="NO" id="on2-62-waY"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="249" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" minimumScaleFactor="0.25" translatesAutoresizingMaskIntoConstraints="NO" id="on2-62-waY">
<rect key="frame" x="0.0" y="24.333333333333371" width="361" height="71.666666666666671"/> <rect key="frame" x="0.0" y="24.5" width="343" height="72"/>
<string key="text">Your Apple ID is used to configure apps so they can be installed on this device. Your credentials will be stored securely in this device's Keychain and sent only to Apple for authentication.</string> <string key="text">Your Apple ID is used to configure apps so they can be installed on this device. Your credentials will be stored securely in this device's Keychain and sent only to Apple for authentication.</string>
<fontDescription key="fontDescription" type="system" pointSize="15"/> <fontDescription key="fontDescription" type="system" pointSize="15"/>
<color key="textColor" white="1" alpha="0.75" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="0.75" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
@@ -258,19 +258,19 @@
<!--How it works--> <!--How it works-->
<scene sceneID="dMt-EA-SGy"> <scene sceneID="dMt-EA-SGy">
<objects> <objects>
<viewController storyboardIdentifier="instructionsViewController" hidesBottomBarWhenPushed="YES" useStoryboardIdentifierAsRestorationIdentifier="YES" id="aFi-fb-W0B" customClass="InstructionsViewController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <viewController storyboardIdentifier="instructionsViewController" hidesBottomBarWhenPushed="YES" id="aFi-fb-W0B" customClass="InstructionsViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" id="Otz-hn-WGS"> <view key="view" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" id="Otz-hn-WGS">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" distribution="equalSpacing" translatesAutoresizingMaskIntoConstraints="NO" id="bp6-55-IG2"> <stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" distribution="equalSpacing" translatesAutoresizingMaskIntoConstraints="NO" id="bp6-55-IG2">
<rect key="frame" x="0.0" y="103" width="393" height="656"/> <rect key="frame" x="0.0" y="44" width="375" height="564"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="FjP-tm-w7K"> <stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="FjP-tm-w7K">
<rect key="frame" x="16" y="35.000000000000007" width="361" height="95.666666666666686"/> <rect key="frame" x="16" y="35" width="343" height="95.5"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="1" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="i9V-3h-B8f"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="1" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="i9V-3h-B8f">
<rect key="frame" x="0.0" y="0.0" width="59" height="95.666666666666671"/> <rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
<constraints> <constraints>
<constraint firstAttribute="width" constant="59" id="ILg-0e-PW8"/> <constraint firstAttribute="width" constant="59" id="ILg-0e-PW8"/>
</constraints> </constraints>
@@ -279,16 +279,16 @@
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="Q20-ml-9D0"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="Q20-ml-9D0">
<rect key="frame" x="79" y="15.999999999999996" width="282" height="63.666666666666657"/> <rect key="frame" x="79" y="16" width="264" height="64"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Launch SideStore" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="XKD-XH-eB0"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Launch SideStore" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="XKD-XH-eB0">
<rect key="frame" x="0.0" y="0.0" width="282" height="20.333333333333332"/> <rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Leave SideStore running in the background on your idevice." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="6HP-Xh-sAH"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Leave SideStore running in the background on your idevice." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="6HP-Xh-sAH">
<rect key="frame" x="0.0" y="25.333333333333346" width="282" height="38.333333333333343"/> <rect key="frame" x="0.0" y="25.5" width="264" height="38.5"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/> <fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@@ -298,10 +298,10 @@
</subviews> </subviews>
</stackView> </stackView>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="LpI-Jt-SzX"> <stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="LpI-Jt-SzX">
<rect key="frame" x="16" y="198.33333333333331" width="361" height="95.666666666666686"/> <rect key="frame" x="16" y="168" width="343" height="95.5"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="2" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="0LW-eE-qHa"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="2" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="0LW-eE-qHa">
<rect key="frame" x="0.0" y="0.0" width="59" height="95.666666666666671"/> <rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
<constraints> <constraints>
<constraint firstAttribute="width" constant="59" id="HzE-AA-eE5"/> <constraint firstAttribute="width" constant="59" id="HzE-AA-eE5"/>
</constraints> </constraints>
@@ -310,16 +310,16 @@
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="dMu-eg-gIO"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="dMu-eg-gIO">
<rect key="frame" x="79" y="16.333333333333371" width="282" height="63"/> <rect key="frame" x="79" y="17" width="264" height="61.5"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Connect to Wi-Fi and VPN" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="esj-pD-D4A"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Connect to Wi-Fi and VPN" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="esj-pD-D4A">
<rect key="frame" x="0.0" y="0.0" width="282" height="20.333333333333332"/> <rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Enable SideStore VPN in Wireguard and be able to use Sidestore on the go." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="4rk-ge-FSj"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Enable SideStore VPN in Wireguard and be able to use Sidestore on the go." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="4rk-ge-FSj">
<rect key="frame" x="0.0" y="25.333333333333311" width="282" height="37.666666666666657"/> <rect key="frame" x="0.0" y="25.5" width="264" height="36"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/> <fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@@ -329,10 +329,10 @@
</subviews> </subviews>
</stackView> </stackView>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="tfb-ja-9UC"> <stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="tfb-ja-9UC">
<rect key="frame" x="16" y="362" width="361" height="95.666666666666686"/> <rect key="frame" x="16" y="300.5" width="343" height="95.5"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="3" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="nVr-El-Csi"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="3" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="nVr-El-Csi">
<rect key="frame" x="0.0" y="0.0" width="59" height="95.666666666666671"/> <rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
<constraints> <constraints>
<constraint firstAttribute="width" constant="59" id="fRj-b4-VTe"/> <constraint firstAttribute="width" constant="59" id="fRj-b4-VTe"/>
</constraints> </constraints>
@@ -341,16 +341,16 @@
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="z6Y-zi-teL"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="z6Y-zi-teL">
<rect key="frame" x="79" y="15.999999999999996" width="282" height="63.666666666666657"/> <rect key="frame" x="79" y="16" width="264" height="64"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Download Apps" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="JeJ-bk-UCA"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Download Apps" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="JeJ-bk-UCA">
<rect key="frame" x="0.0" y="0.0" width="282" height="20.333333333333332"/> <rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Browse and download apps directly from SideStore." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="M7T-9j-uyt"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Browse and download apps directly from SideStore." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="M7T-9j-uyt">
<rect key="frame" x="0.0" y="25.333333333333318" width="282" height="38.333333333333343"/> <rect key="frame" x="0.0" y="25.5" width="264" height="38.5"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/> <fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@@ -360,10 +360,10 @@
</subviews> </subviews>
</stackView> </stackView>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="X3r-G1-vf2"> <stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="X3r-G1-vf2">
<rect key="frame" x="16" y="525.33333333333337" width="361" height="95.666666666666629"/> <rect key="frame" x="16" y="433.5" width="343" height="95.5"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="4" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="i2U-NL-plG"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="4" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="i2U-NL-plG">
<rect key="frame" x="0.0" y="0.0" width="59" height="95.666666666666671"/> <rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
<constraints> <constraints>
<constraint firstAttribute="width" constant="59" id="4Qg-s9-p7s"/> <constraint firstAttribute="width" constant="59" id="4Qg-s9-p7s"/>
</constraints> </constraints>
@@ -372,16 +372,16 @@
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="Xs6-pJ-PUz"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="Xs6-pJ-PUz">
<rect key="frame" x="79" y="15.999999999999996" width="282" height="63.666666666666657"/> <rect key="frame" x="79" y="17" width="264" height="62"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Apps Refresh Automatically" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="nvb-Aq-sYa"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Apps Refresh Automatically" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="nvb-Aq-sYa">
<rect key="frame" x="0.0" y="0.0" width="282" height="20.333333333333332"/> <rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Apps are refreshed in the background while you are on SideStore VPN!" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="HU5-Hv-E3d"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Apps are refreshed in the background while you are on SideStore VPN!" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="HU5-Hv-E3d">
<rect key="frame" x="0.0" y="25.333333333333261" width="282" height="38.333333333333343"/> <rect key="frame" x="0.0" y="25.5" width="264" height="36.5"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/> <fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@@ -394,7 +394,7 @@
<edgeInsets key="layoutMargins" top="35" left="0.0" bottom="35" right="0.0"/> <edgeInsets key="layoutMargins" top="35" left="0.0" bottom="35" right="0.0"/>
</stackView> </stackView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="qZ9-AR-2zK"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="qZ9-AR-2zK">
<rect key="frame" x="16" y="759" width="361" height="51"/> <rect key="frame" x="16" y="608" width="343" height="51"/>
<color key="backgroundColor" name="SettingsHighlighted"/> <color key="backgroundColor" name="SettingsHighlighted"/>
<constraints> <constraints>
<constraint firstAttribute="height" constant="51" id="LQz-qG-ZJK"/> <constraint firstAttribute="height" constant="51" id="LQz-qG-ZJK"/>
@@ -431,22 +431,22 @@
</objects> </objects>
<point key="canvasLocation" x="1353" y="736"/> <point key="canvasLocation" x="1353" y="736"/>
</scene> </scene>
<!--Refresh SideStore--> <!--Refresh AltStore-->
<scene sceneID="9Vh-dM-OqX"> <scene sceneID="9Vh-dM-OqX">
<objects> <objects>
<viewController storyboardIdentifier="refreshAltStoreViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="aoK-yE-UVT" customClass="RefreshAltStoreViewController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <viewController storyboardIdentifier="refreshAltStoreViewController" id="aoK-yE-UVT" customClass="RefreshAltStoreViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" id="R83-kV-365"> <view key="view" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" id="R83-kV-365">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="fpO-Bf-gFY" customClass="RSTPlaceholderView"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="fpO-Bf-gFY" customClass="RSTPlaceholderView">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
</view> </view>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="tDQ-ao-1Jg"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="tDQ-ao-1Jg">
<rect key="frame" x="16" y="721" width="361" height="89"/> <rect key="frame" x="16" y="570" width="343" height="89"/>
<subviews> <subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="xcg-hT-tDe" customClass="PillButton" customModule="SideStore" customModuleProvider="target"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="xcg-hT-tDe" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="361" height="51"/> <rect key="frame" x="0.0" y="0.0" width="343" height="51"/>
<color key="backgroundColor" name="SettingsHighlighted"/> <color key="backgroundColor" name="SettingsHighlighted"/>
<constraints> <constraints>
<constraint firstAttribute="height" constant="51" id="SJA-N9-Z6u"/> <constraint firstAttribute="height" constant="51" id="SJA-N9-Z6u"/>
@@ -461,7 +461,7 @@
</connections> </connections>
</button> </button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="qua-VA-asJ"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="qua-VA-asJ">
<rect key="frame" x="0.0" y="59" width="361" height="30"/> <rect key="frame" x="0.0" y="59" width="343" height="30"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="15"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="15"/>
<state key="normal" title="Refresh Later"> <state key="normal" title="Refresh Later">
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
@@ -485,7 +485,7 @@
<constraint firstItem="tDQ-ao-1Jg" firstAttribute="leading" secondItem="R83-kV-365" secondAttribute="leadingMargin" id="zEt-Xr-kJx"/> <constraint firstItem="tDQ-ao-1Jg" firstAttribute="leading" secondItem="R83-kV-365" secondAttribute="leadingMargin" id="zEt-Xr-kJx"/>
</constraints> </constraints>
</view> </view>
<navigationItem key="navigationItem" title="Refresh SideStore" largeTitleDisplayMode="always" id="5nk-NR-jtV"/> <navigationItem key="navigationItem" title="Refresh AltStore" largeTitleDisplayMode="always" id="5nk-NR-jtV"/>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/> <simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
<connections> <connections>
<outlet property="placeholderView" destination="fpO-Bf-gFY" id="q7d-au-d94"/> <outlet property="placeholderView" destination="fpO-Bf-gFY" id="q7d-au-d94"/>
@@ -498,30 +498,30 @@
<!--Select a Team--> <!--Select a Team-->
<scene sceneID="ioQ-WB-CLJ"> <scene sceneID="ioQ-WB-CLJ">
<objects> <objects>
<viewController storyboardIdentifier="selectTeamViewController" hidesBottomBarWhenPushed="YES" useStoryboardIdentifierAsRestorationIdentifier="YES" id="kOD-4P-a6L" customClass="SelectTeamViewController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <viewController storyboardIdentifier="selectTeamViewController" hidesBottomBarWhenPushed="YES" id="kOD-4P-a6L" customClass="SelectTeamViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" indicatorStyle="white" dataMode="prototypes" style="grouped" separatorStyle="none" rowHeight="60" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="fWW-kX-ifH"> <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" indicatorStyle="white" dataMode="prototypes" style="grouped" separatorStyle="none" rowHeight="60" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="fWW-kX-ifH">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" name="SettingsBackground"/> <color key="backgroundColor" name="SettingsBackground"/>
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="separatorColor" white="1" alpha="0.25" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="separatorColor" white="1" alpha="0.25" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<prototypes> <prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="TeamCell" textLabel="6ip-34-gmM" detailTextLabel="knk-Wf-PKf" style="IBUITableViewCellStyleSubtitle" id="qeQ-eb-2SC" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="TeamCell" textLabel="6ip-34-gmM" detailTextLabel="knk-Wf-PKf" style="IBUITableViewCellStyleSubtitle" id="qeQ-eb-2SC" customClass="InsetGroupTableViewCell" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="55.333332061767578" width="393" height="60"/> <rect key="frame" x="0.0" y="55.5" width="375" height="60"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="qeQ-eb-2SC" id="bT4-Fc-u6I"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="qeQ-eb-2SC" id="bT4-Fc-u6I">
<rect key="frame" x="0.0" y="0.0" width="352.66666666666669" height="60"/> <rect key="frame" x="0.0" y="0.0" width="334" height="60"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Team 1" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="6ip-34-gmM"> <label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Team 1" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="6ip-34-gmM">
<rect key="frame" x="30.000000000000004" y="9.9999999999999982" width="56.333333333333336" height="20.333333333333332"/> <rect key="frame" x="30" y="10" width="56.5" height="20.5"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Description" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="knk-Wf-PKf"> <label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Description" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="knk-Wf-PKf">
<rect key="frame" x="30" y="33.333333333333329" width="70" height="14.333333333333334"/> <rect key="frame" x="30" y="33.5" width="70" height="14.5"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="12"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="12"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
@@ -551,18 +551,19 @@
<placeholder placeholderIdentifier="IBFirstResponder" id="yH5-jU-aez" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="yH5-jU-aez" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="1401" y="734"/> <point key="canvasLocation" x="1401" y="734"/>
</scene> </scene>
</scenes> </scenes>
<color key="tintColor" name="Primary"/> <color key="tintColor" name="Primary"/>
<resources> <resources>
<namedColor name="Primary"> <namedColor name="Primary">
<color red="0.64313725490196083" green="0.019607843137254902" blue="0.98039215686274506" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color red="0.0040000001899898052" green="0.50199997425079346" blue="0.51800000667572021" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor> </namedColor>
<namedColor name="SettingsBackground"> <namedColor name="SettingsBackground">
<color red="0.45098039215686275" green="0.015686274509803921" blue="0.68627450980392157" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color red="0.0039215686274509803" green="0.50196078431372548" blue="0.51764705882352946" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor> </namedColor>
<namedColor name="SettingsHighlighted"> <namedColor name="SettingsHighlighted">
<color red="0.38823529411764707" green="0.011764705882352941" blue="0.58823529411764708" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color red="0.0080000003799796104" green="0.32199999690055847" blue="0.40400001406669617" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor> </namedColor>
</resources> </resources>
</document> </document>

View File

@@ -0,0 +1,171 @@
//
// AuthenticationViewController.swift
// AltStore
//
// Created by Riley Testut on 9/5/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
import AltSign
final class AuthenticationViewController: UIViewController
{
var authenticationHandler: ((String, String, @escaping (Result<(ALTAccount, ALTAppleAPISession), Error>) -> Void) -> Void)?
var completionHandler: (((ALTAccount, ALTAppleAPISession, String)?) -> Void)?
private weak var toastView: ToastView?
@IBOutlet private var appleIDTextField: UITextField!
@IBOutlet private var passwordTextField: UITextField!
@IBOutlet private var signInButton: UIButton!
@IBOutlet private var appleIDBackgroundView: UIView!
@IBOutlet private var passwordBackgroundView: UIView!
@IBOutlet private var scrollView: UIScrollView!
@IBOutlet private var contentStackView: UIStackView!
override func viewDidLoad()
{
super.viewDidLoad()
self.signInButton.activityIndicatorView.style = .medium
for view in [self.appleIDBackgroundView!, self.passwordBackgroundView!, self.signInButton!]
{
view.clipsToBounds = true
view.layer.cornerRadius = 16
}
if UIScreen.main.isExtraCompactHeight
{
self.contentStackView.spacing = 20
}
NotificationCenter.default.addObserver(self, selector: #selector(AuthenticationViewController.textFieldDidChangeText(_:)), name: UITextField.textDidChangeNotification, object: self.appleIDTextField)
NotificationCenter.default.addObserver(self, selector: #selector(AuthenticationViewController.textFieldDidChangeText(_:)), name: UITextField.textDidChangeNotification, object: self.passwordTextField)
self.update()
}
override func viewDidDisappear(_ animated: Bool)
{
super.viewDidDisappear(animated)
self.signInButton.isIndicatingActivity = false
self.toastView?.dismiss()
}
}
private extension AuthenticationViewController
{
func update()
{
if let _ = self.validate()
{
self.signInButton.isEnabled = true
self.signInButton.alpha = 1.0
}
else
{
self.signInButton.isEnabled = false
self.signInButton.alpha = 0.6
}
}
func validate() -> (String, String)?
{
guard
let emailAddress = self.appleIDTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines), !emailAddress.isEmpty,
let password = self.passwordTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines), !password.isEmpty
else { return nil }
return (emailAddress, password)
}
}
private extension AuthenticationViewController
{
@IBAction func authenticate()
{
guard let (emailAddress, password) = self.validate() else { return }
self.appleIDTextField.resignFirstResponder()
self.passwordTextField.resignFirstResponder()
self.signInButton.isIndicatingActivity = true
self.authenticationHandler?(emailAddress, password) { (result) in
switch result
{
case .failure(ALTAppleAPIError.requiresTwoFactorAuthentication):
// Ignore
DispatchQueue.main.async {
self.signInButton.isIndicatingActivity = false
}
case .failure(let error as NSError):
DispatchQueue.main.async {
let error = error.withLocalizedFailure(NSLocalizedString("Failed to Log In", comment: ""))
let toastView = ToastView(error: error)
toastView.textLabel.textColor = .altPink
toastView.detailTextLabel.textColor = .altPink
toastView.show(in: self)
self.toastView = toastView
self.signInButton.isIndicatingActivity = false
}
case .success((let account, let session)):
self.completionHandler?((account, session, password))
}
DispatchQueue.main.async {
self.scrollView.setContentOffset(CGPoint(x: 0, y: -self.view.safeAreaInsets.top), animated: true)
}
}
}
@IBAction func cancel(_ sender: UIBarButtonItem)
{
self.completionHandler?(nil)
}
}
extension AuthenticationViewController: UITextFieldDelegate
{
func textFieldShouldReturn(_ textField: UITextField) -> Bool
{
switch textField
{
case self.appleIDTextField: self.passwordTextField.becomeFirstResponder()
case self.passwordTextField: self.authenticate()
default: break
}
self.update()
return false
}
func textFieldDidBeginEditing(_ textField: UITextField)
{
guard UIScreen.main.isExtraCompactHeight else { return }
// Position all the controls within visible frame.
var contentOffset = self.scrollView.contentOffset
contentOffset.y = 44
self.scrollView.setContentOffset(contentOffset, animated: true)
}
}
extension AuthenticationViewController
{
@objc func textFieldDidChangeText(_ notification: Notification)
{
self.update()
}
}

View File

@@ -0,0 +1,54 @@
//
// InstructionsViewController.swift
// AltStore
//
// Created by Riley Testut on 9/6/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
final class InstructionsViewController: UIViewController
{
var completionHandler: (() -> Void)?
var showsBottomButton: Bool = false
@IBOutlet private var contentStackView: UIStackView!
@IBOutlet private var dismissButton: UIButton!
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
override func viewDidLoad()
{
super.viewDidLoad()
if UIScreen.main.isExtraCompactHeight
{
self.contentStackView.layoutMargins.top = 0
self.contentStackView.layoutMargins.bottom = self.contentStackView.layoutMargins.left
}
self.dismissButton.clipsToBounds = true
self.dismissButton.layer.cornerRadius = 16
if self.showsBottomButton
{
self.navigationItem.hidesBackButton = true
}
else
{
self.dismissButton.isHidden = true
}
}
}
private extension InstructionsViewController
{
@IBAction func dismiss()
{
self.completionHandler?()
}
}

View File

@@ -8,69 +8,77 @@
import UIKit import UIKit
import AltStoreCore
import AltSign import AltSign
import SideStoreCore import Roxas
import RoxasUIKit
final class RefreshAltStoreViewController: UIViewController { final class RefreshAltStoreViewController: UIViewController
{
var context: AuthenticatedOperationContext! var context: AuthenticatedOperationContext!
var completionHandler: ((Result<Void, Error>) -> Void)? var completionHandler: ((Result<Void, Error>) -> Void)?
@IBOutlet private var placeholderView: RSTPlaceholderView! @IBOutlet private var placeholderView: RSTPlaceholderView!
override func viewDidLoad() { override func viewDidLoad()
{
super.viewDidLoad() super.viewDidLoad()
placeholderView.textLabel.isHidden = true self.placeholderView.textLabel.isHidden = true
placeholderView.detailTextLabel.textAlignment = .left self.placeholderView.detailTextLabel.textAlignment = .left
placeholderView.detailTextLabel.textColor = UIColor.white.withAlphaComponent(0.6) self.placeholderView.detailTextLabel.textColor = UIColor.white.withAlphaComponent(0.6)
placeholderView.detailTextLabel.text = NSLocalizedString("SideStore was unable to use an existing signing certificate, so it had to create a new one. This will cause any apps installed with an existing certificate to expire — including SideStore.\n\nTo prevent SideStore from expiring early, please refresh the app now. SideStore will quit once refreshing is complete.", comment: "") self.placeholderView.detailTextLabel.text = NSLocalizedString("SideStore was unable to use an existing signing certificate, so it had to create a new one. This will cause any apps installed with an existing certificate to expire — including SideStore.\n\nTo prevent SideStore from expiring early, please refresh the app now. SideStore will quit once refreshing is complete.", comment: "")
} }
} }
private extension RefreshAltStoreViewController { private extension RefreshAltStoreViewController
@IBAction func refreshAltStore(_ sender: PillButton) { {
@IBAction func refreshAltStore(_ sender: PillButton)
{
guard let altStore = InstalledApp.fetchAltStore(in: DatabaseManager.shared.viewContext) else { return } guard let altStore = InstalledApp.fetchAltStore(in: DatabaseManager.shared.viewContext) else { return }
func refresh() { func refresh()
{
sender.isIndicatingActivity = true sender.isIndicatingActivity = true
if let progress = AppManager.shared.installationProgress(for: altStore) { if let progress = AppManager.shared.installationProgress(for: altStore)
{
// Cancel pending AltStore installation so we can start a new one. // Cancel pending AltStore installation so we can start a new one.
progress.cancel() progress.cancel()
} }
// Install, _not_ refresh, to ensure we are installing with a non-revoked certificate. // Install, _not_ refresh, to ensure we are installing with a non-revoked certificate.
let group = AppManager.shared.install(altStore, presentingViewController: self, context: context) { result in let group = AppManager.shared.install(altStore, presentingViewController: self, context: self.context) { (result) in
switch result { switch result
{
case .success: self.completionHandler?(.success(())) case .success: self.completionHandler?(.success(()))
case let .failure(error as NSError): case .failure(let error as NSError):
DispatchQueue.main.async { DispatchQueue.main.async {
sender.progress = nil sender.progress = nil
sender.isIndicatingActivity = false sender.isIndicatingActivity = false
let alertController = UIAlertController(title: NSLocalizedString("Failed to Refresh SideStore", comment: ""), message: error.localizedFailureReason ?? error.localizedDescription, preferredStyle: .alert) let alertController = UIAlertController(title: NSLocalizedString("Failed to Refresh SideStore", comment: ""), message: error.localizedFailureReason ?? error.localizedDescription, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: NSLocalizedString("Try Again", comment: ""), style: .default, handler: { _ in alertController.addAction(UIAlertAction(title: NSLocalizedString("Try Again", comment: ""), style: .default, handler: { (action) in
refresh() refresh()
})) }))
alertController.addAction(UIAlertAction(title: NSLocalizedString("Refresh Later", comment: ""), style: .cancel, handler: { _ in alertController.addAction(UIAlertAction(title: NSLocalizedString("Refresh Later", comment: ""), style: .cancel, handler: { (action) in
self.completionHandler?(.failure(error)) self.completionHandler?(.failure(error))
})) }))
self.present(alertController, animated: true, completion: nil) self.present(alertController, animated: true, completion: nil)
} }
} }
} }
sender.progress = group.progress sender.progress = group.progress
} }
refresh() refresh()
} }
@IBAction func cancel(_: UIButton) { @IBAction func cancel(_ sender: UIButton)
completionHandler?(.failure(OperationError.cancelled)) {
self.completionHandler?(.failure(OperationError.cancelled))
} }
} }

View File

@@ -0,0 +1,61 @@
//
// SelectTeamViewController.swift
// AltStore
//
// Created by Megarushing on 4/26/21.
// Copyright © 2021 Riley Testut. All rights reserved.
//
import UIKit
import SafariServices
import MessageUI
import Intents
import IntentsUI
import AltSign
final class SelectTeamViewController: UITableViewController
{
public var teams: [ALTTeam]?
public var completionHandler: ((Result<ALTTeam, Swift.Error>) -> Void)?
private var prototypeHeaderFooterView: SettingsHeaderFooterView!
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return teams?.count ?? 0
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
return self.completionHandler!(.success((self.teams?[indexPath.row])!))
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TeamCell", for: indexPath) as! InsetGroupTableViewCell
cell.textLabel?.text = self.teams?[indexPath.row].name
cell.detailTextLabel?.text = self.teams?[indexPath.row].type.localizedDescription
if indexPath.row == 0
{
cell.style = InsetGroupTableViewCell.Style.top
} else if indexPath.row == self.tableView(self.tableView, numberOfRowsInSection: indexPath.section) - 1 {
cell.style = InsetGroupTableViewCell.Style.bottom
} else {
cell.style = InsetGroupTableViewCell.Style.middle
}
return cell
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
"Teams"
}
}

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="wKh-xq-NuP"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21223" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="wKh-xq-NuP">
<device id="retina4_7" orientation="portrait" appearance="light"/> <device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21204"/>
<capability name="Named colors" minToolsVersion="9.0"/> <capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/> <capability name="System colors in document resources" minToolsVersion="11.0"/>
@@ -13,7 +13,7 @@
<!--Launch View Controller--> <!--Launch View Controller-->
<scene sceneID="q24-yd-v7v"> <scene sceneID="q24-yd-v7v">
<objects> <objects>
<viewController storyboardIdentifier="launchViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="wKh-xq-NuP" customClass="LaunchViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController"> <viewController id="wKh-xq-NuP" customClass="LaunchViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="G9E-Qs-gFM"> <view key="view" contentMode="scaleToFill" id="G9E-Qs-gFM">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -28,7 +28,7 @@
<!--Tab Bar Controller--> <!--Tab Bar Controller-->
<scene sceneID="yl2-sM-qoP"> <scene sceneID="yl2-sM-qoP">
<objects> <objects>
<tabBarController storyboardIdentifier="tabBarController" modalPresentationStyle="fullScreen" useStoryboardIdentifierAsRestorationIdentifier="YES" id="49e-Tb-3d3" customClass="TabBarController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <tabBarController storyboardIdentifier="tabBarController" modalPresentationStyle="fullScreen" id="49e-Tb-3d3" customClass="TabBarController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<tabBar key="tabBar" contentMode="scaleToFill" id="W28-zg-YXA"> <tabBar key="tabBar" contentMode="scaleToFill" id="W28-zg-YXA">
<rect key="frame" x="0.0" y="975" width="768" height="49"/> <rect key="frame" x="0.0" y="975" width="768" height="49"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
@@ -50,7 +50,7 @@
<!--Browse--> <!--Browse-->
<scene sceneID="rXq-UR-qQp"> <scene sceneID="rXq-UR-qQp">
<objects> <objects>
<collectionViewController storyboardIdentifier="browseViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="e3L-BF-iXp" customClass="BrowseViewController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <collectionViewController id="e3L-BF-iXp" customClass="BrowseViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" id="CaT-1q-qnx"> <collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" id="CaT-1q-qnx">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -85,7 +85,7 @@
<!--App View Controller--> <!--App View Controller-->
<scene sceneID="TgT-LO-3Er"> <scene sceneID="TgT-LO-3Er">
<objects> <objects>
<viewController storyboardIdentifier="appViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="0V6-N4-hTO" customClass="AppViewController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <viewController storyboardIdentifier="appViewController" id="0V6-N4-hTO" customClass="AppViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="0cR-li-tCB"> <view key="view" contentMode="scaleToFill" id="0cR-li-tCB">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -131,7 +131,7 @@
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Qlg-m3-lXg"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Qlg-m3-lXg">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<subviews> <subviews>
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="NEy-yr-cLS" customClass="AppBannerView" customModule="SideStoreAppKit"> <view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="NEy-yr-cLS" customClass="AppBannerView" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="37" y="287" width="300" height="93"/> <rect key="frame" x="37" y="287" width="300" height="93"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</view> </view>
@@ -191,7 +191,7 @@
</constraints> </constraints>
</view> </view>
<navigationItem key="navigationItem" largeTitleDisplayMode="never" id="maq-gT-QcS"> <navigationItem key="navigationItem" largeTitleDisplayMode="never" id="maq-gT-QcS">
<barButtonItem key="rightBarButtonItem" style="done" id="FLf-DS-F77"> <barButtonItem key="rightBarButtonItem" style="plain" id="FLf-DS-F77">
<button key="customView" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" id="grk-xM-YWA" customClass="PillButton" customModule="SideStore" customModuleProvider="target"> <button key="customView" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" id="grk-xM-YWA" customClass="PillButton" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="287" y="6.5" width="72" height="31"/> <rect key="frame" x="287" y="6.5" width="72" height="31"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
@@ -229,7 +229,7 @@
<!--App--> <!--App-->
<scene sceneID="CgX-7h-sRI"> <scene sceneID="CgX-7h-sRI">
<objects> <objects>
<tableViewController storyboardIdentifier="appContentViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="kBq-V8-3XC" customClass="AppContentViewController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <tableViewController id="kBq-V8-3XC" customClass="AppContentViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" contentInsetAdjustmentBehavior="never" dataMode="static" style="plain" separatorStyle="none" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" contentViewInsetsToSafeArea="NO" id="w5c-Q3-FcU"> <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" contentInsetAdjustmentBehavior="never" dataMode="static" style="plain" separatorStyle="none" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" contentViewInsetsToSafeArea="NO" id="w5c-Q3-FcU">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -511,7 +511,7 @@ World</string>
<!--Permission Popover View Controller--> <!--Permission Popover View Controller-->
<scene sceneID="24j-EJ-G4e"> <scene sceneID="24j-EJ-G4e">
<objects> <objects>
<viewController storyboardIdentifier="permissionPopoverViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="Ojq-DN-xcF" customClass="PermissionPopoverViewController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <viewController id="Ojq-DN-xcF" customClass="PermissionPopoverViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="IgU-aM-YrX"> <view key="view" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="IgU-aM-YrX">
<rect key="frame" x="0.0" y="0.0" width="375" height="217"/> <rect key="frame" x="0.0" y="0.0" width="375" height="217"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -566,7 +566,7 @@ World</string>
<!--News--> <!--News-->
<scene sceneID="bqw-wB-hyB"> <scene sceneID="bqw-wB-hyB">
<objects> <objects>
<collectionViewController storyboardIdentifier="newsViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="3sa-FZ-PTg" customClass="NewsViewController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <collectionViewController id="3sa-FZ-PTg" customClass="NewsViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" id="736-lq-Aef"> <collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" id="736-lq-Aef">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -592,7 +592,7 @@ World</string>
<!--Browse--> <!--Browse-->
<scene sceneID="VHa-uP-bFU"> <scene sceneID="VHa-uP-bFU">
<objects> <objects>
<navigationController storyboardIdentifier="forwardingNavigationControllerBrowse" automaticallyAdjustsScrollViewInsets="NO" useStoryboardIdentifierAsRestorationIdentifier="YES" id="faz-B4-Sub" customClass="ForwardingNavigationController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <navigationController automaticallyAdjustsScrollViewInsets="NO" id="faz-B4-Sub" customClass="ForwardingNavigationController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<tabBarItem key="tabBarItem" title="Browse" image="Browse" id="Uwh-Bg-Ymq"/> <tabBarItem key="tabBarItem" title="Browse" image="Browse" id="Uwh-Bg-Ymq"/>
<toolbarItems/> <toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="dIv-qd-9L5" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target"> <navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="dIv-qd-9L5" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
@@ -620,7 +620,7 @@ World</string>
<!--My Apps--> <!--My Apps-->
<scene sceneID="nhh-BJ-XiT"> <scene sceneID="nhh-BJ-XiT">
<objects> <objects>
<navigationController storyboardIdentifier="forwardingNavigationControllerNyApps" automaticallyAdjustsScrollViewInsets="NO" useStoryboardIdentifierAsRestorationIdentifier="YES" id="3Ew-ox-i4n" customClass="ForwardingNavigationController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <navigationController automaticallyAdjustsScrollViewInsets="NO" id="3Ew-ox-i4n" customClass="ForwardingNavigationController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<tabBarItem key="tabBarItem" title="My Apps" image="MyApps" id="4gT-9u-k7y"> <tabBarItem key="tabBarItem" title="My Apps" image="MyApps" id="4gT-9u-k7y">
<color key="badgeColor" name="Primary"/> <color key="badgeColor" name="Primary"/>
</tabBarItem> </tabBarItem>
@@ -641,7 +641,7 @@ World</string>
<!--My Apps--> <!--My Apps-->
<scene sceneID="EC8-Sf-AF9"> <scene sceneID="EC8-Sf-AF9">
<objects> <objects>
<collectionViewController storyboardIdentifier="myAppsViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="hv7-Ar-voT" customClass="MyAppsViewController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <collectionViewController id="hv7-Ar-voT" customClass="MyAppsViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" id="Jrp-gi-4Df"> <collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" id="Jrp-gi-4Df">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -660,7 +660,7 @@ World</string>
<rect key="frame" x="0.0" y="0.0" width="375" height="60"/> <rect key="frame" x="0.0" y="0.0" width="375" height="60"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="mos-e4-dQ7" customClass="AppBannerView" customModule="SideStoreAppKit"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="mos-e4-dQ7" customClass="AppBannerView" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="8" y="0.0" width="359" height="60"/> <rect key="frame" x="8" y="0.0" width="359" height="60"/>
</view> </view>
</subviews> </subviews>
@@ -797,7 +797,7 @@ World</string>
<!--App IDs--> <!--App IDs-->
<scene sceneID="kvf-US-rRe"> <scene sceneID="kvf-US-rRe">
<objects> <objects>
<collectionViewController storyboardIdentifier="appIDsViewController" title="App IDs" useStoryboardIdentifierAsRestorationIdentifier="YES" id="y1A-Nm-mw7" customClass="AppIDsViewController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <collectionViewController title="App IDs" id="y1A-Nm-mw7" customClass="AppIDsViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" dataMode="prototypes" id="v1r-C8-h6h"> <collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" dataMode="prototypes" id="v1r-C8-h6h">
<rect key="frame" x="0.0" y="0.0" width="375" height="647"/> <rect key="frame" x="0.0" y="0.0" width="375" height="647"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -816,7 +816,7 @@ World</string>
<rect key="frame" x="0.0" y="0.0" width="375" height="80"/> <rect key="frame" x="0.0" y="0.0" width="375" height="80"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="1w8-fI-98T" customClass="AppBannerView" customModule="SideStoreAppKit"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="1w8-fI-98T" customClass="AppBannerView" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="8" y="0.0" width="359" height="80"/> <rect key="frame" x="8" y="0.0" width="359" height="80"/>
<accessibility key="accessibilityConfiguration"> <accessibility key="accessibilityConfiguration">
<bool key="isElement" value="YES"/> <bool key="isElement" value="YES"/>
@@ -835,7 +835,7 @@ World</string>
</connections> </connections>
</collectionViewCell> </collectionViewCell>
</cells> </cells>
<collectionReusableView key="sectionHeaderView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Header" id="th0-G6-bRt" customClass="TextCollectionReusableView" customModule="SideStoreAppKit"> <collectionReusableView key="sectionHeaderView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Header" id="th0-G6-bRt" customClass="TextCollectionReusableView" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="60"/> <rect key="frame" x="0.0" y="0.0" width="375" height="60"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
@@ -856,7 +856,7 @@ World</string>
<outlet property="textLabel" destination="83Z-Ih-nOW" id="xxM-HD-iJS"/> <outlet property="textLabel" destination="83Z-Ih-nOW" id="xxM-HD-iJS"/>
</connections> </connections>
</collectionReusableView> </collectionReusableView>
<collectionReusableView key="sectionFooterView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Footer" id="xMh-lD-r6C" customClass="TextCollectionReusableView" customModule="SideStoreAppKit"> <collectionReusableView key="sectionFooterView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Footer" id="xMh-lD-r6C" customClass="TextCollectionReusableView" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="170" width="375" height="50"/> <rect key="frame" x="0.0" y="170" width="375" height="50"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
@@ -881,9 +881,9 @@ World</string>
</connections> </connections>
</collectionView> </collectionView>
<navigationItem key="navigationItem" title="App IDs" id="3Co-uv-Fhb"> <navigationItem key="navigationItem" title="App IDs" id="3Co-uv-Fhb">
<barButtonItem key="leftBarButtonItem" style="done" id="Aqs-QK-Ups"> <barButtonItem key="leftBarButtonItem" style="plain" id="Aqs-QK-Ups">
<view key="customView" contentMode="scaleToFill" id="p0q-Fg-3Ba"> <view key="customView" contentMode="scaleToFill" id="p0q-Fg-3Ba">
<rect key="frame" x="16" y="7" width="83" height="42"/> <rect key="frame" x="16" y="1" width="83" height="42"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</view> </view>
</barButtonItem> </barButtonItem>
@@ -905,7 +905,7 @@ World</string>
<!--News--> <!--News-->
<scene sceneID="BV8-6J-nIv"> <scene sceneID="BV8-6J-nIv">
<objects> <objects>
<navigationController storyboardIdentifier="forwardingNavigationControllerNews" automaticallyAdjustsScrollViewInsets="NO" useStoryboardIdentifierAsRestorationIdentifier="YES" id="kjR-gi-fgT" customClass="ForwardingNavigationController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <navigationController automaticallyAdjustsScrollViewInsets="NO" id="kjR-gi-fgT" customClass="ForwardingNavigationController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<tabBarItem key="tabBarItem" title="News" image="News" id="fVN-ed-uO1"/> <tabBarItem key="tabBarItem" title="News" image="News" id="fVN-ed-uO1"/>
<toolbarItems/> <toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="525-jF-uDK" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target"> <navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="525-jF-uDK" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
@@ -925,10 +925,10 @@ World</string>
<!--Navigation Controller--> <!--Navigation Controller-->
<scene sceneID="1Gj-mS-BaN"> <scene sceneID="1Gj-mS-BaN">
<objects> <objects>
<navigationController storyboardIdentifier="nav1" automaticallyAdjustsScrollViewInsets="NO" useStoryboardIdentifierAsRestorationIdentifier="YES" id="IXk-qg-mFJ" sceneMemberID="viewController"> <navigationController automaticallyAdjustsScrollViewInsets="NO" id="IXk-qg-mFJ" sceneMemberID="viewController">
<toolbarItems/> <toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="9sB-f3-Fnk"> <navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="9sB-f3-Fnk">
<rect key="frame" x="0.0" y="0.0" width="375" height="108"/> <rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
</navigationBar> </navigationBar>
<nil name="viewControllers"/> <nil name="viewControllers"/>
@@ -943,7 +943,7 @@ World</string>
<!--Sources--> <!--Sources-->
<scene sceneID="0S1-zn-9KZ"> <scene sceneID="0S1-zn-9KZ">
<objects> <objects>
<collectionViewController storyboardIdentifier="sourcesViewController" title="Sources" useStoryboardIdentifierAsRestorationIdentifier="YES" id="cHC-TX-KzQ" customClass="SourcesViewController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <collectionViewController title="Sources" id="cHC-TX-KzQ" customClass="SourcesViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" alwaysBounceVertical="YES" dataMode="prototypes" id="S36-hD-vu2"> <collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" alwaysBounceVertical="YES" dataMode="prototypes" id="S36-hD-vu2">
<rect key="frame" x="0.0" y="0.0" width="375" height="647"/> <rect key="frame" x="0.0" y="0.0" width="375" height="647"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -962,7 +962,7 @@ World</string>
<rect key="frame" x="0.0" y="0.0" width="375" height="80"/> <rect key="frame" x="0.0" y="0.0" width="375" height="80"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="LW1-CC-bWu" customClass="AppBannerView" customModule="SideStoreAppKit"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="LW1-CC-bWu" customClass="AppBannerView" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="8" y="0.0" width="359" height="80"/> <rect key="frame" x="8" y="0.0" width="359" height="80"/>
<accessibility key="accessibilityConfiguration"> <accessibility key="accessibilityConfiguration">
<bool key="isElement" value="YES"/> <bool key="isElement" value="YES"/>
@@ -981,7 +981,7 @@ World</string>
</connections> </connections>
</collectionViewCell> </collectionViewCell>
</cells> </cells>
<collectionReusableView key="sectionHeaderView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Header" id="8N7-JY-mcA" customClass="TextCollectionReusableView" customModule="SideStoreAppKit"> <collectionReusableView key="sectionHeaderView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Header" id="8N7-JY-mcA" customClass="TextCollectionReusableView" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="200"/> <rect key="frame" x="0.0" y="0.0" width="375" height="200"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
@@ -1067,10 +1067,10 @@ World</string>
<!--Navigation Controller--> <!--Navigation Controller-->
<scene sceneID="6NV-LQ-gKB"> <scene sceneID="6NV-LQ-gKB">
<objects> <objects>
<navigationController storyboardIdentifier="nav2" automaticallyAdjustsScrollViewInsets="NO" useStoryboardIdentifierAsRestorationIdentifier="YES" id="Qo4-72-Hmr" sceneMemberID="viewController"> <navigationController automaticallyAdjustsScrollViewInsets="NO" id="Qo4-72-Hmr" sceneMemberID="viewController">
<toolbarItems/> <toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="mcx-oR-qPe"> <navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="mcx-oR-qPe">
<rect key="frame" x="0.0" y="0.0" width="375" height="108"/> <rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
</navigationBar> </navigationBar>
<nil name="viewControllers"/> <nil name="viewControllers"/>
@@ -1095,13 +1095,13 @@ World</string>
<image name="News" width="19" height="20"/> <image name="News" width="19" height="20"/>
<image name="Settings" width="20" height="20"/> <image name="Settings" width="20" height="20"/>
<namedColor name="Background"> <namedColor name="Background">
<color red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color red="0.6431" green="0.0196" blue="0.9804" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor> </namedColor>
<namedColor name="BlurTint"> <namedColor name="BlurTint">
<color red="1" green="1" blue="1" alpha="0.30000001192092896" colorSpace="custom" customColorSpace="sRGB"/> <color red="1" green="1" blue="1" alpha="0.3" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor> </namedColor>
<namedColor name="Primary"> <namedColor name="Primary">
<color red="0.64313725490196083" green="0.019607843137254902" blue="0.98039215686274506" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color red="0.6431" green="0.0196" blue="0.9804" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor> </namedColor>
<systemColor name="systemBackgroundColor"> <systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>

View File

@@ -8,80 +8,85 @@
import UIKit import UIKit
import RoxasUIKit import Roxas
import OSLog
#if canImport(Logging)
import Logging
#endif
import Nuke import Nuke
@objc final class BrowseCollectionViewCell: UICollectionViewCell { @objc final class BrowseCollectionViewCell: UICollectionViewCell
{
var imageURLs: [URL] = [] { var imageURLs: [URL] = [] {
didSet { didSet {
dataSource.items = imageURLs as [NSURL] self.dataSource.items = self.imageURLs as [NSURL]
} }
} }
private lazy var dataSource = self.makeDataSource() private lazy var dataSource = self.makeDataSource()
@IBOutlet var bannerView: AppBannerView! @IBOutlet var bannerView: AppBannerView!
@IBOutlet var subtitleLabel: UILabel! @IBOutlet var subtitleLabel: UILabel!
@IBOutlet private(set) var screenshotsCollectionView: UICollectionView! @IBOutlet private(set) var screenshotsCollectionView: UICollectionView!
override func awakeFromNib() { override func awakeFromNib()
{
super.awakeFromNib() super.awakeFromNib()
contentView.preservesSuperviewLayoutMargins = true self.contentView.preservesSuperviewLayoutMargins = true
// Must be registered programmatically, not in BrowseCollectionViewCell.xib, or else it'll throw an exception 🤷. // Must be registered programmatically, not in BrowseCollectionViewCell.xib, or else it'll throw an exception 🤷.
screenshotsCollectionView.register(ScreenshotCollectionViewCell.self, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier) self.screenshotsCollectionView.register(ScreenshotCollectionViewCell.self, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
screenshotsCollectionView.delegate = self self.screenshotsCollectionView.delegate = self
screenshotsCollectionView.dataSource = dataSource self.screenshotsCollectionView.dataSource = self.dataSource
screenshotsCollectionView.prefetchDataSource = dataSource self.screenshotsCollectionView.prefetchDataSource = self.dataSource
} }
} }
private extension BrowseCollectionViewCell { private extension BrowseCollectionViewCell
func makeDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage> { {
func makeDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>
{
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>(items: []) let dataSource = RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>(items: [])
dataSource.cellConfigurationHandler = { cell, _, _ in dataSource.cellConfigurationHandler = { (cell, screenshot, indexPath) in
let cell = cell as! ScreenshotCollectionViewCell let cell = cell as! ScreenshotCollectionViewCell
cell.imageView.image = nil cell.imageView.image = nil
cell.imageView.isIndicatingActivity = true cell.imageView.isIndicatingActivity = true
} }
dataSource.prefetchHandler = { imageURL, _, completionHandler in dataSource.prefetchHandler = { (imageURL, indexPath, completionHandler) in
RSTAsyncBlockOperation { operation in return RSTAsyncBlockOperation() { (operation) in
let request = ImageRequest(url: imageURL as URL, processor: .screenshot) let request = ImageRequest(url: imageURL as URL, processor: .screenshot)
ImagePipeline.shared.loadImage(with: request, progress: nil, completion: { response, error in ImagePipeline.shared.loadImage(with: request, progress: nil, completion: { (response, error) in
guard !operation.isCancelled else { return operation.finish() } guard !operation.isCancelled else { return operation.finish() }
if let image = response?.image { if let image = response?.image
{
completionHandler(image, nil) completionHandler(image, nil)
} else { }
else
{
completionHandler(nil, error) completionHandler(nil, error)
} }
}) })
} }
} }
dataSource.prefetchCompletionHandler = { cell, image, _, error in dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
let cell = cell as! ScreenshotCollectionViewCell let cell = cell as! ScreenshotCollectionViewCell
cell.imageView.isIndicatingActivity = false cell.imageView.isIndicatingActivity = false
cell.imageView.image = image cell.imageView.image = image
if let error = error { if let error = error
os_log("Error loading image: %@", type: .error , error.localizedDescription) {
print("Error loading image:", error)
} }
} }
return dataSource return dataSource
} }
} }
extension BrowseCollectionViewCell: UICollectionViewDelegateFlowLayout { extension BrowseCollectionViewCell: UICollectionViewDelegateFlowLayout
func collectionView(_ collectionView: UICollectionView, layout _: UICollectionViewLayout, sizeForItemAt _: IndexPath) -> CGSize { {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
// Assuming 9.0 / 16.0 ratio for now. // Assuming 9.0 / 16.0 ratio for now.
let aspectRatio: CGFloat = 9.0 / 16.0 let aspectRatio: CGFloat = 9.0 / 16.0

View File

@@ -1,28 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15400" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/> <device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15404"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<objects> <objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/> <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" restorationIdentifier="browseCollectionViewCell" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Cell" id="ln4-pC-7KY" customClass="BrowseCollectionViewCell" customModule="SideStoreAppKit"> <collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Cell" id="ln4-pC-7KY" customClass="BrowseCollectionViewCell" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="369"/> <rect key="frame" x="0.0" y="0.0" width="375" height="369"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO"> <view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
<rect key="frame" x="0.0" y="0.0" width="375" height="369"/> <rect key="frame" x="0.0" y="0.0" width="375" height="369"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="5gU-g3-Fsy"> <stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="5gU-g3-Fsy">
<rect key="frame" x="16" y="0.0" width="343" height="369"/> <rect key="frame" x="16" y="0.0" width="343" height="369"/>
<subviews> <subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ziA-mP-AY2" customClass="AppBannerView" customModule="SideStoreAppKit"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ziA-mP-AY2" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="343" height="88"/> <rect key="frame" x="0.0" y="0.0" width="343" height="88"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/> <color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints> <constraints>
<constraint firstAttribute="height" constant="88" id="z3D-cI-jhp"/> <constraint firstAttribute="height" constant="88" id="z3D-cI-jhp"/>
</constraints> </constraints>
@@ -62,9 +61,4 @@
<point key="canvasLocation" x="136.95652173913044" y="152.67857142857142"/> <point key="canvasLocation" x="136.95652173913044" y="152.67857142857142"/>
</collectionViewCell> </collectionViewCell>
</objects> </objects>
<resources>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document> </document>

View File

@@ -8,67 +8,70 @@
import UIKit import UIKit
import SideStoreCore import minimuxer
import RoxasUIKit import AltStoreCore
import OSLog import Roxas
#if canImport(Logging)
import Logging
#endif
import Nuke import Nuke
class BrowseViewController: UICollectionViewController { class BrowseViewController: UICollectionViewController
{
private lazy var dataSource = self.makeDataSource() private lazy var dataSource = self.makeDataSource()
private lazy var placeholderView = RSTPlaceholderView(frame: .zero) private lazy var placeholderView = RSTPlaceholderView(frame: .zero)
private let prototypeCell = BrowseCollectionViewCell.instantiate(with: BrowseCollectionViewCell.nib!)! private let prototypeCell = BrowseCollectionViewCell.instantiate(with: BrowseCollectionViewCell.nib!)!
private var loadingState: LoadingState = .loading { private var loadingState: LoadingState = .loading {
didSet { didSet {
update() self.update()
} }
} }
private var cachedItemSizes = [String: CGSize]() private var cachedItemSizes = [String: CGSize]()
@IBOutlet private var sourcesBarButtonItem: UIBarButtonItem! @IBOutlet private var sourcesBarButtonItem: UIBarButtonItem!
override func viewDidLoad() { override func viewDidLoad()
{
super.viewDidLoad() super.viewDidLoad()
#if BETA #if BETA
dataSource.searchController.searchableKeyPaths = [#keyPath(InstalledApp.name)] self.dataSource.searchController.searchableKeyPaths = [#keyPath(InstalledApp.name)]
navigationItem.searchController = dataSource.searchController self.navigationItem.searchController = self.dataSource.searchController
#endif #endif
prototypeCell.contentView.translatesAutoresizingMaskIntoConstraints = false self.prototypeCell.contentView.translatesAutoresizingMaskIntoConstraints = false
collectionView.register(BrowseCollectionViewCell.nib, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier) self.collectionView.register(BrowseCollectionViewCell.nib, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
collectionView.dataSource = dataSource self.collectionView.dataSource = self.dataSource
collectionView.prefetchDataSource = dataSource self.collectionView.prefetchDataSource = self.dataSource
registerForPreviewing(with: self, sourceView: collectionView) self.registerForPreviewing(with: self, sourceView: self.collectionView)
update() self.update()
} }
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated) super.viewWillAppear(animated)
fetchSource() self.fetchSource()
updateDataSource() self.updateDataSource()
update() self.update()
} }
@IBAction private func unwindFromSourcesViewController(_: UIStoryboardSegue) { @IBAction private func unwindFromSourcesViewController(_ segue: UIStoryboardSegue)
fetchSource() {
self.fetchSource()
} }
} }
private extension BrowseViewController { private extension BrowseViewController
func makeDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage> { {
func makeDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>
{
let fetchRequest = StoreApp.fetchRequest() as NSFetchRequest<StoreApp> let fetchRequest = StoreApp.fetchRequest() as NSFetchRequest<StoreApp>
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \StoreApp.sourceIdentifier, ascending: true), fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \StoreApp.sourceIdentifier, ascending: true),
NSSortDescriptor(keyPath: \StoreApp.sortIndex, ascending: true), NSSortDescriptor(keyPath: \StoreApp.sortIndex, ascending: true),
@@ -76,46 +79,52 @@ private extension BrowseViewController {
NSSortDescriptor(keyPath: \StoreApp.bundleIdentifier, ascending: true)] NSSortDescriptor(keyPath: \StoreApp.bundleIdentifier, ascending: true)]
fetchRequest.returnsObjectsAsFaults = false fetchRequest.returnsObjectsAsFaults = false
fetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(StoreApp.bundleIdentifier), StoreApp.altstoreAppID) fetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(StoreApp.bundleIdentifier), StoreApp.altstoreAppID)
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext) let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
dataSource.cellConfigurationHandler = { cell, app, _ in dataSource.cellConfigurationHandler = { (cell, app, indexPath) in
let cell = cell as! BrowseCollectionViewCell let cell = cell as! BrowseCollectionViewCell
cell.layoutMargins.left = self.view.layoutMargins.left cell.layoutMargins.left = self.view.layoutMargins.left
cell.layoutMargins.right = self.view.layoutMargins.right cell.layoutMargins.right = self.view.layoutMargins.right
cell.subtitleLabel.text = app.subtitle cell.subtitleLabel.text = app.subtitle
cell.imageURLs = Array(app.screenshotURLs.prefix(2)) cell.imageURLs = Array(app.screenshotURLs.prefix(2))
cell.bannerView.configure(for: app) cell.bannerView.configure(for: app)
cell.bannerView.iconImageView.image = nil cell.bannerView.iconImageView.image = nil
cell.bannerView.iconImageView.isIndicatingActivity = true cell.bannerView.iconImageView.isIndicatingActivity = true
cell.bannerView.button.addTarget(self, action: #selector(BrowseViewController.performAppAction(_:)), for: .primaryActionTriggered) cell.bannerView.button.addTarget(self, action: #selector(BrowseViewController.performAppAction(_:)), for: .primaryActionTriggered)
cell.bannerView.button.activityIndicatorView.style = .medium cell.bannerView.button.activityIndicatorView.style = .medium
// Explicitly set to false to ensure we're starting from a non-activity indicating state. // Explicitly set to false to ensure we're starting from a non-activity indicating state.
// Otherwise, cell reuse can mess up some cached values. // Otherwise, cell reuse can mess up some cached values.
cell.bannerView.button.isIndicatingActivity = false cell.bannerView.button.isIndicatingActivity = false
let tintColor = app.tintColor ?? .altPrimary let tintColor = app.tintColor ?? .altPrimary
cell.tintColor = tintColor cell.tintColor = tintColor
if app.installedApp == nil { if app.installedApp == nil
{
let buttonTitle = NSLocalizedString("Free", comment: "") let buttonTitle = NSLocalizedString("Free", comment: "")
cell.bannerView.button.setTitle(buttonTitle.uppercased(), for: .normal) cell.bannerView.button.setTitle(buttonTitle.uppercased(), for: .normal)
cell.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Download %@", comment: ""), app.name) cell.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Download %@", comment: ""), app.name)
cell.bannerView.button.accessibilityValue = buttonTitle cell.bannerView.button.accessibilityValue = buttonTitle
let progress = AppManager.shared.installationProgress(for: app) let progress = AppManager.shared.installationProgress(for: app)
cell.bannerView.button.progress = progress cell.bannerView.button.progress = progress
if let versionDate = app.latestVersion?.date, versionDate > Date() { if let versionDate = app.latestVersion?.date, versionDate > Date()
{
cell.bannerView.button.countdownDate = app.versionDate cell.bannerView.button.countdownDate = app.versionDate
} else { }
else
{
cell.bannerView.button.countdownDate = nil cell.bannerView.button.countdownDate = nil
} }
} else { }
else
{
cell.bannerView.button.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal) cell.bannerView.button.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal)
cell.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Open %@", comment: ""), app.name) cell.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Open %@", comment: ""), app.name)
cell.bannerView.button.accessibilityValue = nil cell.bannerView.button.accessibilityValue = nil
@@ -123,204 +132,240 @@ private extension BrowseViewController {
cell.bannerView.button.countdownDate = nil cell.bannerView.button.countdownDate = nil
} }
} }
dataSource.prefetchHandler = { storeApp, _, completionHandler -> Foundation.Operation? in dataSource.prefetchHandler = { (storeApp, indexPath, completionHandler) -> Foundation.Operation? in
let iconURL = storeApp.iconURL let iconURL = storeApp.iconURL
return RSTAsyncBlockOperation { operation in return RSTAsyncBlockOperation() { (operation) in
ImagePipeline.shared.loadImage(with: iconURL, progress: nil, completion: { response, error in ImagePipeline.shared.loadImage(with: iconURL, progress: nil, completion: { (response, error) in
guard !operation.isCancelled else { return operation.finish() } guard !operation.isCancelled else { return operation.finish() }
if let image = response?.image { if let image = response?.image
{
completionHandler(image, nil) completionHandler(image, nil)
} else { }
else
{
completionHandler(nil, error) completionHandler(nil, error)
} }
}) })
} }
} }
dataSource.prefetchCompletionHandler = { cell, image, _, error in dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
let cell = cell as! BrowseCollectionViewCell let cell = cell as! BrowseCollectionViewCell
cell.bannerView.iconImageView.isIndicatingActivity = false cell.bannerView.iconImageView.isIndicatingActivity = false
cell.bannerView.iconImageView.image = image cell.bannerView.iconImageView.image = image
if let error = error { if let error = error
os_log("Error loading image: %@", type: .error , error.localizedDescription) {
print("Error loading image:", error)
} }
} }
dataSource.placeholderView = placeholderView dataSource.placeholderView = self.placeholderView
return dataSource return dataSource
} }
func updateDataSource() { func updateDataSource()
dataSource.predicate = nil {
self.dataSource.predicate = nil
} }
func fetchSource() { func fetchSource()
loadingState = .loading {
self.loadingState = .loading
AppManager.shared.fetchSources { result in
do { AppManager.shared.fetchSources() { (result) in
do { do
{
do
{
let (_, context) = try result.get() let (_, context) = try result.get()
try context.save() try context.save()
DispatchQueue.main.async { DispatchQueue.main.async {
self.loadingState = .finished(.success(())) self.loadingState = .finished(.success(()))
} }
} catch let error as AppManager.FetchSourcesError { }
catch let error as AppManager.FetchSourcesError
{
try error.managedObjectContext?.save() try error.managedObjectContext?.save()
throw error throw error
} }
} catch { }
catch
{
DispatchQueue.main.async { DispatchQueue.main.async {
if self.dataSource.itemCount > 0 { if self.dataSource.itemCount > 0
{
let toastView = ToastView(error: error) let toastView = ToastView(error: error)
toastView.addTarget(nil, action: #selector(TabBarController.presentSources), for: .touchUpInside) toastView.addTarget(nil, action: #selector(TabBarController.presentSources), for: .touchUpInside)
toastView.show(in: self) toastView.show(in: self)
} }
self.loadingState = .finished(.failure(error)) self.loadingState = .finished(.failure(error))
} }
} }
} }
} }
func update() { func update()
switch loadingState { {
switch self.loadingState
{
case .loading: case .loading:
placeholderView.textLabel.isHidden = true self.placeholderView.textLabel.isHidden = true
placeholderView.detailTextLabel.isHidden = false self.placeholderView.detailTextLabel.isHidden = false
placeholderView.detailTextLabel.text = NSLocalizedString("Loading...", comment: "") self.placeholderView.detailTextLabel.text = NSLocalizedString("Loading...", comment: "")
placeholderView.activityIndicatorView.startAnimating() self.placeholderView.activityIndicatorView.startAnimating()
case let .finished(.failure(error)): case .finished(.failure(let error)):
placeholderView.textLabel.isHidden = false self.placeholderView.textLabel.isHidden = false
placeholderView.detailTextLabel.isHidden = false self.placeholderView.detailTextLabel.isHidden = false
placeholderView.textLabel.text = NSLocalizedString("Unable to Fetch Apps", comment: "") self.placeholderView.textLabel.text = NSLocalizedString("Unable to Fetch Apps", comment: "")
placeholderView.detailTextLabel.text = error.localizedDescription self.placeholderView.detailTextLabel.text = error.localizedDescription
placeholderView.activityIndicatorView.stopAnimating() self.placeholderView.activityIndicatorView.stopAnimating()
case .finished(.success): case .finished(.success):
placeholderView.textLabel.isHidden = true self.placeholderView.textLabel.isHidden = true
placeholderView.detailTextLabel.isHidden = true self.placeholderView.detailTextLabel.isHidden = true
placeholderView.activityIndicatorView.stopAnimating() self.placeholderView.activityIndicatorView.stopAnimating()
} }
} }
} }
private extension BrowseViewController { private extension BrowseViewController
@IBAction func performAppAction(_ sender: PillButton) { {
let point = collectionView.convert(sender.center, from: sender.superview) @IBAction func performAppAction(_ sender: PillButton)
guard let indexPath = collectionView.indexPathForItem(at: point) else { return } {
let point = self.collectionView.convert(sender.center, from: sender.superview)
let app = dataSource.item(at: indexPath) guard let indexPath = self.collectionView.indexPathForItem(at: point) else { return }
if let installedApp = app.installedApp { let app = self.dataSource.item(at: indexPath)
open(installedApp)
} else { if let installedApp = app.installedApp
install(app, at: indexPath) {
self.open(installedApp)
}
else
{
self.install(app, at: indexPath)
} }
} }
func install(_ app: StoreApp, at indexPath: IndexPath) { func install(_ app: StoreApp, at indexPath: IndexPath)
{
let previousProgress = AppManager.shared.installationProgress(for: app) let previousProgress = AppManager.shared.installationProgress(for: app)
guard previousProgress == nil else { guard previousProgress == nil else {
previousProgress?.cancel() previousProgress?.cancel()
return return
} }
_ = AppManager.shared.install(app, presentingViewController: self) { result in if !minimuxer.ready() {
let toastView = ToastView(error: MinimuxerError.NoConnection)
toastView.show(in: self)
return
}
_ = AppManager.shared.install(app, presentingViewController: self) { (result) in
DispatchQueue.main.async { DispatchQueue.main.async {
switch result { switch result
{
case .failure(OperationError.cancelled): break // Ignore case .failure(OperationError.cancelled): break // Ignore
case let .failure(error): case .failure(let error):
let toastView = ToastView(error: error) let toastView = ToastView(error: error)
toastView.show(in: self) toastView.show(in: self)
case .success: os_log("Installed app: %@", type: .info , app.bundleIdentifier) case .success: print("Installed app:", app.bundleIdentifier)
} }
self.collectionView.reloadItems(at: [indexPath]) self.collectionView.reloadItems(at: [indexPath])
} }
} }
collectionView.reloadItems(at: [indexPath]) self.collectionView.reloadItems(at: [indexPath])
} }
func open(_ installedApp: InstalledApp) { func open(_ installedApp: InstalledApp)
{
UIApplication.shared.open(installedApp.openAppURL) UIApplication.shared.open(installedApp.openAppURL)
} }
} }
extension BrowseViewController: UICollectionViewDelegateFlowLayout { extension BrowseViewController: UICollectionViewDelegateFlowLayout
func collectionView(_ collectionView: UICollectionView, layout _: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { {
let item = dataSource.item(at: indexPath) func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
let item = self.dataSource.item(at: indexPath)
if let previousSize = cachedItemSizes[item.bundleIdentifier] { if let previousSize = self.cachedItemSizes[item.bundleIdentifier]
{
return previousSize return previousSize
} }
let maxVisibleScreenshots = 2 as CGFloat let maxVisibleScreenshots = 2 as CGFloat
let aspectRatio: CGFloat = 16.0 / 9.0 let aspectRatio: CGFloat = 16.0 / 9.0
let layout = prototypeCell.screenshotsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout let layout = self.prototypeCell.screenshotsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
let padding = (layout.minimumInteritemSpacing * (maxVisibleScreenshots - 1)) + layout.sectionInset.left + layout.sectionInset.right let padding = (layout.minimumInteritemSpacing * (maxVisibleScreenshots - 1)) + layout.sectionInset.left + layout.sectionInset.right
dataSource.cellConfigurationHandler(prototypeCell, item, indexPath) self.dataSource.cellConfigurationHandler(self.prototypeCell, item, indexPath)
let widthConstraint = prototypeCell.contentView.widthAnchor.constraint(equalToConstant: collectionView.bounds.width) let widthConstraint = self.prototypeCell.contentView.widthAnchor.constraint(equalToConstant: collectionView.bounds.width)
widthConstraint.isActive = true widthConstraint.isActive = true
defer { widthConstraint.isActive = false } defer { widthConstraint.isActive = false }
// Manually update cell width & layout so we can accurately calculate screenshot sizes. // Manually update cell width & layout so we can accurately calculate screenshot sizes.
prototypeCell.frame.size.width = widthConstraint.constant self.prototypeCell.frame.size.width = widthConstraint.constant
prototypeCell.layoutIfNeeded() self.prototypeCell.layoutIfNeeded()
let collectionViewWidth = prototypeCell.screenshotsCollectionView.bounds.width let collectionViewWidth = self.prototypeCell.screenshotsCollectionView.bounds.width
let screenshotWidth = ((collectionViewWidth - padding) / maxVisibleScreenshots).rounded(.down) let screenshotWidth = ((collectionViewWidth - padding) / maxVisibleScreenshots).rounded(.down)
let screenshotHeight = screenshotWidth * aspectRatio let screenshotHeight = screenshotWidth * aspectRatio
let heightConstraint = prototypeCell.screenshotsCollectionView.heightAnchor.constraint(equalToConstant: screenshotHeight) let heightConstraint = self.prototypeCell.screenshotsCollectionView.heightAnchor.constraint(equalToConstant: screenshotHeight)
heightConstraint.priority = .defaultHigh // Prevent temporary unsatisfiable constraints error. heightConstraint.priority = .defaultHigh // Prevent temporary unsatisfiable constraints error.
heightConstraint.isActive = true heightConstraint.isActive = true
defer { heightConstraint.isActive = false } defer { heightConstraint.isActive = false }
let itemSize = prototypeCell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) let itemSize = self.prototypeCell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
cachedItemSizes[item.bundleIdentifier] = itemSize self.cachedItemSizes[item.bundleIdentifier] = itemSize
return itemSize return itemSize
} }
override func collectionView(_: UICollectionView, didSelectItemAt indexPath: IndexPath) { override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
let app = dataSource.item(at: indexPath) {
let app = self.dataSource.item(at: indexPath)
let appViewController = AppViewController.makeAppViewController(app: app) let appViewController = AppViewController.makeAppViewController(app: app)
navigationController?.pushViewController(appViewController, animated: true) self.navigationController?.pushViewController(appViewController, animated: true)
} }
} }
extension BrowseViewController: UIViewControllerPreviewingDelegate { extension BrowseViewController: UIViewControllerPreviewingDelegate
func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? { {
func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController?
{
guard guard
let indexPath = collectionView.indexPathForItem(at: location), let indexPath = self.collectionView.indexPathForItem(at: location),
let cell = collectionView.cellForItem(at: indexPath) let cell = self.collectionView.cellForItem(at: indexPath)
else { return nil } else { return nil }
previewingContext.sourceRect = cell.frame previewingContext.sourceRect = cell.frame
let app = dataSource.item(at: indexPath) let app = self.dataSource.item(at: indexPath)
let appViewController = AppViewController.makeAppViewController(app: app) let appViewController = AppViewController.makeAppViewController(app: app)
return appViewController return appViewController
} }
func previewingContext(_: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController) { func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController)
navigationController?.pushViewController(viewControllerToCommit, animated: true) {
self.navigationController?.pushViewController(viewControllerToCommit, animated: true)
} }
} }

View File

@@ -0,0 +1,44 @@
//
// ScreenshotCollectionViewCell.swift
// AltStore
//
// Created by Riley Testut on 7/15/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
import Roxas
@objc(ScreenshotCollectionViewCell)
class ScreenshotCollectionViewCell: UICollectionViewCell
{
let imageView = UIImageView(image: nil)
override init(frame: CGRect)
{
super.init(frame: frame)
self.initialize()
}
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
self.initialize()
}
private func initialize()
{
self.imageView.layer.masksToBounds = true
self.addSubview(self.imageView, pinningEdgesWith: .zero)
}
override func layoutSubviews()
{
super.layoutSubviews()
self.imageView.layer.cornerRadius = 4
}
}

View File

@@ -0,0 +1,144 @@
//
// AppBannerView.swift
// AltStore
//
// Created by Riley Testut on 8/29/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
import AltStoreCore
import Roxas
class AppBannerView: RSTNibView
{
override var accessibilityLabel: String? {
get { return self.accessibilityView?.accessibilityLabel }
set { self.accessibilityView?.accessibilityLabel = newValue }
}
override open var accessibilityAttributedLabel: NSAttributedString? {
get { return self.accessibilityView?.accessibilityAttributedLabel }
set { self.accessibilityView?.accessibilityAttributedLabel = newValue }
}
override var accessibilityValue: String? {
get { return self.accessibilityView?.accessibilityValue }
set { self.accessibilityView?.accessibilityValue = newValue }
}
override open var accessibilityAttributedValue: NSAttributedString? {
get { return self.accessibilityView?.accessibilityAttributedValue }
set { self.accessibilityView?.accessibilityAttributedValue = newValue }
}
override open var accessibilityTraits: UIAccessibilityTraits {
get { return self.accessibilityView?.accessibilityTraits ?? [] }
set { self.accessibilityView?.accessibilityTraits = newValue }
}
private var originalTintColor: UIColor?
@IBOutlet var titleLabel: UILabel!
@IBOutlet var subtitleLabel: UILabel!
@IBOutlet var iconImageView: AppIconImageView!
@IBOutlet var button: PillButton!
@IBOutlet var buttonLabel: UILabel!
@IBOutlet var betaBadgeView: UIView!
@IBOutlet var backgroundEffectView: UIVisualEffectView!
@IBOutlet private var vibrancyView: UIVisualEffectView!
@IBOutlet private var accessibilityView: UIView!
override init(frame: CGRect)
{
super.init(frame: frame)
self.initialize()
}
required init?(coder: NSCoder)
{
super.init(coder: coder)
self.initialize()
}
private func initialize()
{
self.accessibilityView.accessibilityTraits.formUnion(.button)
self.isAccessibilityElement = false
self.accessibilityElements = [self.accessibilityView, self.button].compactMap { $0 }
self.betaBadgeView.isHidden = true
}
override func tintColorDidChange()
{
super.tintColorDidChange()
if self.tintAdjustmentMode != .dimmed
{
self.originalTintColor = self.tintColor
}
self.update()
}
}
extension AppBannerView
{
func configure(for app: AppProtocol)
{
struct AppValues
{
var name: String
var developerName: String? = nil
var isBeta: Bool = false
init(app: AppProtocol)
{
self.name = app.name
guard let storeApp = (app as? StoreApp) ?? (app as? InstalledApp)?.storeApp else { return }
self.developerName = storeApp.developerName
if storeApp.isBeta
{
self.name = String(format: NSLocalizedString("%@ beta", comment: ""), app.name)
self.isBeta = true
}
}
}
let values = AppValues(app: app)
self.titleLabel.text = app.name // Don't use values.name since that already includes "beta".
self.betaBadgeView.isHidden = !values.isBeta
if let developerName = values.developerName
{
self.subtitleLabel.text = developerName
self.accessibilityLabel = String(format: NSLocalizedString("%@ by %@", comment: ""), values.name, developerName)
}
else
{
self.subtitleLabel.text = NSLocalizedString("Sideloaded", comment: "")
self.accessibilityLabel = values.name
}
}
}
private extension AppBannerView
{
func update()
{
self.clipsToBounds = true
self.layer.cornerRadius = 22
self.subtitleLabel.textColor = self.originalTintColor ?? self.tintColor
self.backgroundEffectView.backgroundColor = self.originalTintColor ?? self.tintColor
}
}

View File

@@ -1,15 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17154" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/> <device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17124"/>
<capability name="Named colors" minToolsVersion="9.0"/> <capability name="Named colors" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/> <capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<objects> <objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="AppBannerView" customModule="SideStoreAppKit"> <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
<connections> <connections>
<outlet property="accessibilityView" destination="bJL-Yw-i4u" id="PWe-tw-jDA"/> <outlet property="accessibilityView" destination="bJL-Yw-i4u" id="PWe-tw-jDA"/>
<outlet property="backgroundEffectView" destination="rZk-be-tiI" id="fzU-VT-JeW"/> <outlet property="backgroundEffectView" destination="rZk-be-tiI" id="fzU-VT-JeW"/>
@@ -23,7 +23,7 @@
</connections> </connections>
</placeholder> </placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" restorationIdentifier="appBannerView" insetsLayoutMarginsFromSafeArea="NO" id="FxI-Fh-ll5"> <view opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="FxI-Fh-ll5">
<rect key="frame" x="0.0" y="0.0" width="375" height="88"/> <rect key="frame" x="0.0" y="0.0" width="375" height="88"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
@@ -46,7 +46,7 @@
<stackView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" alignment="center" spacing="11" translatesAutoresizingMaskIntoConstraints="NO" id="d1T-UD-gWG" userLabel="App Info"> <stackView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" alignment="center" spacing="11" translatesAutoresizingMaskIntoConstraints="NO" id="d1T-UD-gWG" userLabel="App Info">
<rect key="frame" x="0.0" y="0.0" width="375" height="88"/> <rect key="frame" x="0.0" y="0.0" width="375" height="88"/>
<subviews> <subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="avS-dx-4iy" customClass="AppIconImageView" customModule="SideStore" customModuleProvider="target"> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="avS-dx-4iy" customClass="AppIconImageView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="14" y="14" width="60" height="60"/> <rect key="frame" x="14" y="14" width="60" height="60"/>
<constraints> <constraints>
<constraint firstAttribute="height" constant="60" id="6lU-H8-nEw"/> <constraint firstAttribute="height" constant="60" id="6lU-H8-nEw"/>
@@ -110,7 +110,7 @@
<color key="textColor" systemColor="secondaryLabelColor"/> <color key="textColor" systemColor="secondaryLabelColor"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="900" horizontalCompressionResistancePriority="900" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="tVx-3G-dcu" customClass="PillButton" customModule="SideStore" customModuleProvider="target"> <button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="900" horizontalCompressionResistancePriority="900" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="tVx-3G-dcu" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="77" height="31"/> <rect key="frame" x="0.0" y="0.0" width="77" height="31"/>
<color key="backgroundColor" red="0.22352941179999999" green="0.4941176471" blue="0.39607843139999999" alpha="0.10000000000000001" colorSpace="custom" customColorSpace="sRGB"/> <color key="backgroundColor" red="0.22352941179999999" green="0.4941176471" blue="0.39607843139999999" alpha="0.10000000000000001" colorSpace="custom" customColorSpace="sRGB"/>
<constraints> <constraints>

View File

@@ -0,0 +1,43 @@
//
// AppIconImageView.swift
// AltStore
//
// Created by Riley Testut on 5/9/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
final class AppIconImageView: UIImageView
{
override func awakeFromNib()
{
super.awakeFromNib()
self.contentMode = .scaleAspectFill
self.clipsToBounds = true
self.backgroundColor = .white
if #available(iOS 13, *)
{
self.layer.cornerCurve = .continuous
}
else
{
if self.layer.responds(to: Selector(("continuousCorners")))
{
self.layer.setValue(true, forKey: "continuousCorners")
}
}
}
override func layoutSubviews()
{
super.layoutSubviews()
// Based off of 60pt icon having 12pt radius.
let radius = self.bounds.height / 5
self.layer.cornerRadius = radius
}
}

View File

@@ -8,67 +8,78 @@
import AVFoundation import AVFoundation
public final class BackgroundTaskManager { final class BackgroundTaskManager
public static let shared = BackgroundTaskManager() {
static let shared = BackgroundTaskManager()
private var isPlaying = false private var isPlaying = false
private let audioEngine: AVAudioEngine private let audioEngine: AVAudioEngine
private let player: AVAudioPlayerNode private let player: AVAudioPlayerNode
private let audioFile: AVAudioFile private let audioFile: AVAudioFile
private let audioEngineQueue: DispatchQueue private let audioEngineQueue: DispatchQueue
private init() { private init()
audioEngine = AVAudioEngine() {
audioEngine.mainMixerNode.outputVolume = 0.0 self.audioEngine = AVAudioEngine()
self.audioEngine.mainMixerNode.outputVolume = 0.0
player = AVAudioPlayerNode()
audioEngine.attach(player) self.player = AVAudioPlayerNode()
self.audioEngine.attach(self.player)
do {
do
{
let audioFileURL = Bundle.main.url(forResource: "Silence", withExtension: "m4a")! let audioFileURL = Bundle.main.url(forResource: "Silence", withExtension: "m4a")!
audioFile = try AVAudioFile(forReading: audioFileURL) self.audioFile = try AVAudioFile(forReading: audioFileURL)
audioEngine.connect(player, to: audioEngine.mainMixerNode, format: audioFile.processingFormat) self.audioEngine.connect(self.player, to: self.audioEngine.mainMixerNode, format: self.audioFile.processingFormat)
} catch { }
catch
{
fatalError("Error. \(error)") fatalError("Error. \(error)")
} }
audioEngineQueue = DispatchQueue(label: "com.altstore.BackgroundTaskManager") self.audioEngineQueue = DispatchQueue(label: "com.altstore.BackgroundTaskManager")
} }
} }
public extension BackgroundTaskManager { extension BackgroundTaskManager
func performExtendedBackgroundTask(taskHandler: @escaping ((Result<Void, Error>, @escaping () -> Void) -> Void)) { {
func finish() { func performExtendedBackgroundTask(taskHandler: @escaping ((Result<Void, Error>, @escaping () -> Void) -> Void))
player.stop() {
audioEngine.stop() func finish()
{
isPlaying = false self.player.stop()
self.audioEngine.stop()
self.isPlaying = false
} }
audioEngineQueue.sync { self.audioEngineQueue.sync {
do { do
{
try AVAudioSession.sharedInstance().setCategory(.playback, options: .mixWithOthers) try AVAudioSession.sharedInstance().setCategory(.playback, options: .mixWithOthers)
try AVAudioSession.sharedInstance().setActive(true) try AVAudioSession.sharedInstance().setActive(true)
// Schedule audio file buffers. // Schedule audio file buffers.
self.scheduleAudioFile() self.scheduleAudioFile()
self.scheduleAudioFile() self.scheduleAudioFile()
let outputFormat = self.audioEngine.outputNode.outputFormat(forBus: 0) let outputFormat = self.audioEngine.outputNode.outputFormat(forBus: 0)
self.audioEngine.connect(self.audioEngine.mainMixerNode, to: self.audioEngine.outputNode, format: outputFormat) self.audioEngine.connect(self.audioEngine.mainMixerNode, to: self.audioEngine.outputNode, format: outputFormat)
try self.audioEngine.start() try self.audioEngine.start()
self.player.play() self.player.play()
self.isPlaying = true self.isPlaying = true
taskHandler(.success(())) { taskHandler(.success(())) {
finish() finish()
} }
} catch { }
catch
{
taskHandler(.failure(error)) { taskHandler(.failure(error)) {
finish() finish()
} }
@@ -77,9 +88,11 @@ public extension BackgroundTaskManager {
} }
} }
private extension BackgroundTaskManager { private extension BackgroundTaskManager
func scheduleAudioFile() { {
player.scheduleFile(audioFile, at: nil) { func scheduleAudioFile()
{
self.player.scheduleFile(self.audioFile, at: nil) {
self.audioEngineQueue.async { self.audioEngineQueue.async {
guard self.isPlaying else { return } guard self.isPlaying else { return }
self.scheduleAudioFile() self.scheduleAudioFile()

View File

@@ -8,44 +8,46 @@
import UIKit import UIKit
@objc final class BannerCollectionViewCell: UICollectionViewCell
final class BannerCollectionViewCell: UICollectionViewCell { {
private(set) var errorBadge: UIView? private(set) var errorBadge: UIView?
@IBOutlet private(set) var bannerView: AppBannerView! @IBOutlet private(set) var bannerView: AppBannerView!
override func awakeFromNib() { override func awakeFromNib()
{
super.awakeFromNib() super.awakeFromNib()
contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight] self.contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
contentView.preservesSuperviewLayoutMargins = true self.contentView.preservesSuperviewLayoutMargins = true
if #available(iOS 13.0, *) { if #available(iOS 13.0, *)
{
let errorBadge = UIView() let errorBadge = UIView()
errorBadge.translatesAutoresizingMaskIntoConstraints = false errorBadge.translatesAutoresizingMaskIntoConstraints = false
errorBadge.isHidden = true errorBadge.isHidden = true
self.addSubview(errorBadge) self.addSubview(errorBadge)
// Solid background to make the X opaque white. // Solid background to make the X opaque white.
let backgroundView = UIView() let backgroundView = UIView()
backgroundView.translatesAutoresizingMaskIntoConstraints = false backgroundView.translatesAutoresizingMaskIntoConstraints = false
backgroundView.backgroundColor = .white backgroundView.backgroundColor = .white
errorBadge.addSubview(backgroundView) errorBadge.addSubview(backgroundView)
let badgeView = UIImageView(image: UIImage(systemName: "exclamationmark.circle.fill")) let badgeView = UIImageView(image: UIImage(systemName: "exclamationmark.circle.fill"))
badgeView.preferredSymbolConfiguration = UIImage.SymbolConfiguration(scale: .large) badgeView.preferredSymbolConfiguration = UIImage.SymbolConfiguration(scale: .large)
badgeView.tintColor = .systemRed badgeView.tintColor = .systemRed
errorBadge.addSubview(badgeView, pinningEdgesWith: .zero) errorBadge.addSubview(badgeView, pinningEdgesWith: .zero)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
errorBadge.centerXAnchor.constraint(equalTo: self.bannerView.trailingAnchor, constant: -5), errorBadge.centerXAnchor.constraint(equalTo: self.bannerView.trailingAnchor, constant: -5),
errorBadge.centerYAnchor.constraint(equalTo: self.bannerView.topAnchor, constant: 5), errorBadge.centerYAnchor.constraint(equalTo: self.bannerView.topAnchor, constant: 5),
backgroundView.centerXAnchor.constraint(equalTo: badgeView.centerXAnchor), backgroundView.centerXAnchor.constraint(equalTo: badgeView.centerXAnchor),
backgroundView.centerYAnchor.constraint(equalTo: badgeView.centerYAnchor), backgroundView.centerYAnchor.constraint(equalTo: badgeView.centerYAnchor),
backgroundView.widthAnchor.constraint(equalTo: badgeView.widthAnchor, multiplier: 0.5), backgroundView.widthAnchor.constraint(equalTo: badgeView.widthAnchor, multiplier: 0.5),
backgroundView.heightAnchor.constraint(equalTo: badgeView.heightAnchor, multiplier: 0.5) backgroundView.heightAnchor.constraint(equalTo: badgeView.heightAnchor, multiplier: 0.5)
]) ])
self.errorBadge = errorBadge self.errorBadge = errorBadge
} }
} }

View File

@@ -8,50 +8,58 @@
import UIKit import UIKit
final class Button: UIButton { final class Button: UIButton
{
override var intrinsicContentSize: CGSize { override var intrinsicContentSize: CGSize {
var size = super.intrinsicContentSize var size = super.intrinsicContentSize
size.width += 20 size.width += 20
size.height += 10 size.height += 10
return size return size
} }
override func awakeFromNib() { override func awakeFromNib()
{
super.awakeFromNib() super.awakeFromNib()
setTitleColor(.white, for: .normal) self.setTitleColor(.white, for: .normal)
layer.masksToBounds = true self.layer.masksToBounds = true
layer.cornerRadius = 8 self.layer.cornerRadius = 8
update() self.update()
} }
override func tintColorDidChange() { override func tintColorDidChange()
{
super.tintColorDidChange() super.tintColorDidChange()
update() self.update()
} }
override var isHighlighted: Bool { override var isHighlighted: Bool {
didSet { didSet {
self.update() self.update()
} }
} }
override var isEnabled: Bool { override var isEnabled: Bool {
didSet { didSet {
update() self.update()
} }
} }
} }
private extension Button { private extension Button
func update() { {
if isEnabled { func update()
backgroundColor = tintColor {
} else { if self.isEnabled
backgroundColor = .lightGray {
self.backgroundColor = self.tintColor
}
else
{
self.backgroundColor = .lightGray
} }
} }
} }

View File

@@ -0,0 +1,150 @@
//
// CollapsingTextView.swift
// AltStore
//
// Created by Riley Testut on 7/23/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
final class CollapsingTextView: UITextView
{
var isCollapsed = true {
didSet {
self.setNeedsLayout()
}
}
var maximumNumberOfLines = 2 {
didSet {
self.setNeedsLayout()
}
}
var lineSpacing: Double = 2 {
didSet {
self.setNeedsLayout()
}
}
let moreButton = UIButton(type: .system)
override func awakeFromNib()
{
super.awakeFromNib()
self.initialize()
}
private func initialize()
{
if #available(iOS 16, *)
{
self.updateText()
}
else
{
self.layoutManager.delegate = self
}
self.textContainerInset = .zero
self.textContainer.lineFragmentPadding = 0
self.textContainer.lineBreakMode = .byTruncatingTail
self.textContainer.heightTracksTextView = true
self.textContainer.widthTracksTextView = true
self.moreButton.setTitle(NSLocalizedString("More", comment: ""), for: .normal)
self.moreButton.addTarget(self, action: #selector(CollapsingTextView.toggleCollapsed(_:)), for: .primaryActionTriggered)
self.addSubview(self.moreButton)
self.setNeedsLayout()
}
override func layoutSubviews()
{
super.layoutSubviews()
guard let font = self.font else { return }
let buttonFont = UIFont.systemFont(ofSize: font.pointSize, weight: .medium)
self.moreButton.titleLabel?.font = buttonFont
let buttonY = (font.lineHeight + self.lineSpacing) * CGFloat(self.maximumNumberOfLines - 1)
let size = self.moreButton.sizeThatFits(CGSize(width: 1000, height: 1000))
let moreButtonFrame = CGRect(x: self.bounds.width - self.moreButton.bounds.width,
y: buttonY,
width: size.width,
height: font.lineHeight)
self.moreButton.frame = moreButtonFrame
if self.isCollapsed
{
self.textContainer.maximumNumberOfLines = self.maximumNumberOfLines
let boundingSize = self.attributedText.boundingRect(with: CGSize(width: self.textContainer.size.width, height: .infinity), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
let maximumCollapsedHeight = font.lineHeight * Double(self.maximumNumberOfLines)
if boundingSize.height.rounded() > maximumCollapsedHeight.rounded()
{
var exclusionFrame = moreButtonFrame
exclusionFrame.origin.y += self.moreButton.bounds.midY
exclusionFrame.size.width = self.bounds.width // Extra wide to make sure it wraps to next line.
self.textContainer.exclusionPaths = [UIBezierPath(rect: exclusionFrame)]
self.moreButton.isHidden = false
}
else
{
self.textContainer.exclusionPaths = []
self.moreButton.isHidden = true
}
}
else
{
self.textContainer.maximumNumberOfLines = 0
self.textContainer.exclusionPaths = []
self.moreButton.isHidden = true
}
self.invalidateIntrinsicContentSize()
}
}
private extension CollapsingTextView
{
@objc func toggleCollapsed(_ sender: UIButton)
{
self.isCollapsed.toggle()
}
@available(iOS 16, *)
func updateText()
{
do
{
let style = NSMutableParagraphStyle()
style.lineSpacing = self.lineSpacing
var attributedText = try AttributedString(self.attributedText, including: \.uiKit)
attributedText[AttributeScopes.UIKitAttributes.ParagraphStyleAttribute.self] = style
self.attributedText = NSAttributedString(attributedText)
}
catch
{
print("[ALTLog] Failed to update CollapsingTextView line spacing:", error)
}
}
}
extension CollapsingTextView: NSLayoutManagerDelegate
{
func layoutManager(_ layoutManager: NSLayoutManager, lineSpacingAfterGlyphAt glyphIndex: Int, withProposedLineFragmentRect rect: CGRect) -> CGFloat
{
return self.lineSpacing
}
}

View File

@@ -8,12 +8,13 @@
import UIKit import UIKit
final class ForwardingNavigationController: UINavigationController { final class ForwardingNavigationController: UINavigationController
{
override var childForStatusBarStyle: UIViewController? { override var childForStatusBarStyle: UIViewController? {
self.topViewController return self.topViewController
} }
override var childForStatusBarHidden: UIViewController? { override var childForStatusBarHidden: UIViewController? {
topViewController return self.topViewController
} }
} }

View File

@@ -8,78 +8,93 @@
import UIKit import UIKit
import RoxasUIKit import Roxas
@objc final class NavigationBar: UINavigationBar
final class NavigationBar: UINavigationBar { {
@objc
@IBInspectable var automaticallyAdjustsItemPositions: Bool = true @IBInspectable var automaticallyAdjustsItemPositions: Bool = true
private let backgroundColorView = UIView() private let backgroundColorView = UIView()
override init(frame: CGRect) { override init(frame: CGRect)
{
super.init(frame: frame) super.init(frame: frame)
initialize() self.initialize()
} }
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder) super.init(coder: aDecoder)
initialize() self.initialize()
} }
private func initialize() { private func initialize()
if #available(iOS 13, *) { {
if #available(iOS 13, *)
{
let standardAppearance = UINavigationBarAppearance() let standardAppearance = UINavigationBarAppearance()
standardAppearance.configureWithDefaultBackground() standardAppearance.configureWithDefaultBackground()
standardAppearance.shadowColor = nil standardAppearance.shadowColor = nil
let edgeAppearance = UINavigationBarAppearance() let edgeAppearance = UINavigationBarAppearance()
edgeAppearance.configureWithOpaqueBackground() edgeAppearance.configureWithOpaqueBackground()
edgeAppearance.backgroundColor = self.barTintColor edgeAppearance.backgroundColor = self.barTintColor
edgeAppearance.shadowColor = nil edgeAppearance.shadowColor = nil
if let tintColor = self.barTintColor { if let tintColor = self.barTintColor
{
let textAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white] let textAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
standardAppearance.backgroundColor = tintColor standardAppearance.backgroundColor = tintColor
standardAppearance.titleTextAttributes = textAttributes standardAppearance.titleTextAttributes = textAttributes
standardAppearance.largeTitleTextAttributes = textAttributes standardAppearance.largeTitleTextAttributes = textAttributes
edgeAppearance.titleTextAttributes = textAttributes edgeAppearance.titleTextAttributes = textAttributes
edgeAppearance.largeTitleTextAttributes = textAttributes edgeAppearance.largeTitleTextAttributes = textAttributes
} else { }
else
{
standardAppearance.backgroundColor = nil standardAppearance.backgroundColor = nil
} }
self.scrollEdgeAppearance = edgeAppearance self.scrollEdgeAppearance = edgeAppearance
self.standardAppearance = standardAppearance self.standardAppearance = standardAppearance
} else { }
shadowImage = UIImage() else
{
if let tintColor = barTintColor { self.shadowImage = UIImage()
backgroundColorView.backgroundColor = tintColor
if let tintColor = self.barTintColor
{
self.backgroundColorView.backgroundColor = tintColor
// Top = -50 to cover status bar area above navigation bar on any device. // Top = -50 to cover status bar area above navigation bar on any device.
// Bottom = -1 to prevent a flickering gray line from appearing. // Bottom = -1 to prevent a flickering gray line from appearing.
addSubview(backgroundColorView, pinningEdgesWith: UIEdgeInsets(top: -50, left: 0, bottom: -1, right: 0)) self.addSubview(self.backgroundColorView, pinningEdgesWith: UIEdgeInsets(top: -50, left: 0, bottom: -1, right: 0))
} else { }
barTintColor = .white else
{
self.barTintColor = .white
} }
} }
} }
override func layoutSubviews() { override func layoutSubviews()
{
super.layoutSubviews() super.layoutSubviews()
if backgroundColorView.superview != nil { if self.backgroundColorView.superview != nil
insertSubview(backgroundColorView, at: 1) {
self.insertSubview(self.backgroundColorView, at: 1)
} }
if automaticallyAdjustsItemPositions { if self.automaticallyAdjustsItemPositions
{
// We can't easily shift just the back button up, so we shift the entire content view slightly. // We can't easily shift just the back button up, so we shift the entire content view slightly.
for contentView in subviews { for contentView in self.subviews
{
guard NSStringFromClass(type(of: contentView)).contains("ContentView") else { continue } guard NSStringFromClass(type(of: contentView)).contains("ContentView") else { continue }
contentView.center.y -= 2 contentView.center.y -= 2
} }

View File

@@ -0,0 +1,207 @@
//
// PillButton.swift
// AltStore
//
// Created by Riley Testut on 7/15/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
extension PillButton
{
static let minimumSize = CGSize(width: 77, height: 31)
static let contentInsets = NSDirectionalEdgeInsets(top: 7, leading: 13, bottom: 7, trailing: 13)
}
final class PillButton: UIButton
{
override var accessibilityValue: String? {
get {
guard self.progress != nil else { return super.accessibilityValue }
return self.progressView.accessibilityValue
}
set { super.accessibilityValue = newValue }
}
var progress: Progress? {
didSet {
self.progressView.progress = Float(self.progress?.fractionCompleted ?? 0)
self.progressView.observedProgress = self.progress
let isUserInteractionEnabled = self.isUserInteractionEnabled
self.isIndicatingActivity = (self.progress != nil)
self.isUserInteractionEnabled = isUserInteractionEnabled
self.update()
}
}
var progressTintColor: UIColor? {
get {
return self.progressView.progressTintColor
}
set {
self.progressView.progressTintColor = newValue
}
}
var countdownDate: Date? {
didSet {
self.isEnabled = (self.countdownDate == nil)
self.displayLink.isPaused = (self.countdownDate == nil)
if self.countdownDate == nil
{
self.setTitle(nil, for: .disabled)
}
}
}
private let progressView = UIProgressView(progressViewStyle: .default)
private lazy var displayLink: CADisplayLink = {
let displayLink = CADisplayLink(target: self, selector: #selector(PillButton.updateCountdown))
displayLink.preferredFramesPerSecond = 15
displayLink.isPaused = true
displayLink.add(to: .main, forMode: .common)
return displayLink
}()
private let dateComponentsFormatter: DateComponentsFormatter = {
let dateComponentsFormatter = DateComponentsFormatter()
dateComponentsFormatter.zeroFormattingBehavior = [.pad]
dateComponentsFormatter.collapsesLargestUnit = false
return dateComponentsFormatter
}()
override var intrinsicContentSize: CGSize {
let size = self.sizeThatFits(CGSize(width: Double.infinity, height: .infinity))
return size
}
deinit
{
self.displayLink.remove(from: .main, forMode: RunLoop.Mode.default)
}
override func awakeFromNib()
{
super.awakeFromNib()
self.layer.masksToBounds = true
self.accessibilityTraits.formUnion([.updatesFrequently, .button])
self.contentEdgeInsets = UIEdgeInsets(top: Self.contentInsets.top, left: Self.contentInsets.leading, bottom: Self.contentInsets.bottom, right: Self.contentInsets.trailing)
self.activityIndicatorView.style = .medium
self.activityIndicatorView.isUserInteractionEnabled = false
self.progressView.progress = 0
self.progressView.trackImage = UIImage()
self.progressView.isUserInteractionEnabled = false
self.addSubview(self.progressView)
self.update()
}
override func layoutSubviews()
{
super.layoutSubviews()
self.progressView.bounds.size.width = self.bounds.width
let scale = self.bounds.height / self.progressView.bounds.height
self.progressView.transform = CGAffineTransform.identity.scaledBy(x: 1, y: scale)
self.progressView.center = CGPoint(x: self.bounds.midX, y: self.bounds.midY)
self.layer.cornerRadius = self.bounds.midY
}
override func tintColorDidChange()
{
super.tintColorDidChange()
self.update()
}
override func sizeThatFits(_ size: CGSize) -> CGSize
{
var size = super.sizeThatFits(size)
size.width = max(size.width, PillButton.minimumSize.width)
size.height = max(size.height, PillButton.minimumSize.height)
return size
}
}
private extension PillButton
{
func update()
{
if self.progress == nil
{
self.setTitleColor(.white, for: .normal)
self.backgroundColor = self.tintColor
}
else
{
self.setTitleColor(self.tintColor, for: .normal)
self.backgroundColor = self.tintColor.withAlphaComponent(0.15)
}
self.progressView.progressTintColor = self.tintColor
}
@objc func updateCountdown()
{
guard let endDate = self.countdownDate else { return }
let startDate = Date()
let interval = endDate.timeIntervalSince(startDate)
guard interval > 0 else {
self.isEnabled = true
return
}
let text: String?
if interval < (1 * 60 * 60)
{
self.dateComponentsFormatter.unitsStyle = .positional
self.dateComponentsFormatter.allowedUnits = [.minute, .second]
text = self.dateComponentsFormatter.string(from: startDate, to: endDate)
}
else if interval < (2 * 24 * 60 * 60)
{
self.dateComponentsFormatter.unitsStyle = .positional
self.dateComponentsFormatter.allowedUnits = [.hour, .minute, .second]
text = self.dateComponentsFormatter.string(from: startDate, to: endDate)
}
else
{
self.dateComponentsFormatter.unitsStyle = .full
self.dateComponentsFormatter.allowedUnits = [.day]
let numberOfDays = endDate.numberOfCalendarDays(since: startDate)
text = String(format: NSLocalizedString("%@ DAYS", comment: ""), NSNumber(value: numberOfDays))
}
if let text = text
{
UIView.performWithoutAnimation {
self.isEnabled = false
self.setTitle(text, for: .disabled)
self.layoutIfNeeded()
}
}
else
{
self.isEnabled = true
}
}
}

View File

@@ -8,10 +8,10 @@
import UIKit import UIKit
@objc class TextCollectionReusableView: UICollectionReusableView
public class TextCollectionReusableView: UICollectionReusableView { {
@IBOutlet var textLabel: UILabel! @IBOutlet var textLabel: UILabel!
@IBOutlet var topLayoutConstraint: NSLayoutConstraint! @IBOutlet var topLayoutConstraint: NSLayoutConstraint!
@IBOutlet var bottomLayoutConstraint: NSLayoutConstraint! @IBOutlet var bottomLayoutConstraint: NSLayoutConstraint!
@IBOutlet var leadingLayoutConstraint: NSLayoutConstraint! @IBOutlet var leadingLayoutConstraint: NSLayoutConstraint!

View File

@@ -6,110 +6,125 @@
// Copyright © 2019 Riley Testut. All rights reserved. // Copyright © 2019 Riley Testut. All rights reserved.
// //
import RoxasUIKit import Roxas
import SideStoreCore import AltStoreCore
import SideKit
import AltSign
extension TimeInterval { extension TimeInterval
{
static let shortToastViewDuration = 4.0 static let shortToastViewDuration = 4.0
static let longToastViewDuration = 8.0 static let longToastViewDuration = 8.0
} }
final class ToastView: RSTToastView { final class ToastView: RSTToastView
{
var preferredDuration: TimeInterval var preferredDuration: TimeInterval
override init(text: String, detailText detailedText: String?) { override init(text: String, detailText detailedText: String?)
if detailedText == nil { {
preferredDuration = .shortToastViewDuration if detailedText == nil
} else { {
preferredDuration = .longToastViewDuration self.preferredDuration = .shortToastViewDuration
} }
else
{
self.preferredDuration = .longToastViewDuration
}
super.init(text: text, detailText: detailedText) super.init(text: text, detailText: detailedText)
isAccessibilityElement = true self.isAccessibilityElement = true
layoutMargins = UIEdgeInsets(top: 8, left: 16, bottom: 10, right: 16) self.layoutMargins = UIEdgeInsets(top: 8, left: 16, bottom: 10, right: 16)
setNeedsLayout() self.setNeedsLayout()
if let stackView = textLabel.superview as? UIStackView { if let stackView = self.textLabel.superview as? UIStackView
{
// RSTToastView does not expose stack view containing labels, // RSTToastView does not expose stack view containing labels,
// so we access it indirectly as the labels' superview. // so we access it indirectly as the labels' superview.
stackView.spacing = (detailedText != nil) ? 4.0 : 0.0 stackView.spacing = (detailedText != nil) ? 4.0 : 0.0
} }
} }
convenience init(error: Error) { convenience init(error: Error)
{
var error = error as NSError var error = error as NSError
var underlyingError = error.underlyingError var underlyingError = error.underlyingError
var preferredDuration: TimeInterval? var preferredDuration: TimeInterval?
if if
let unwrappedUnderlyingError = underlyingError, let unwrappedUnderlyingError = underlyingError,
error.domain == AltServerErrorDomain && error.code == -1 //ALTServerError.underlyingError().rawValue error.domain == AltServerErrorDomain && error.code == ALTServerError.Code.underlyingError.rawValue
{ {
// Treat underlyingError as the primary error. // Treat underlyingError as the primary error.
error = unwrappedUnderlyingError as NSError error = unwrappedUnderlyingError as NSError
underlyingError = nil underlyingError = nil
preferredDuration = .longToastViewDuration preferredDuration = .longToastViewDuration
} }
let text: String let text: String
let detailText: String? let detailText: String?
if let failure = error.localizedFailure { if let failure = error.localizedFailure
{
text = failure text = failure
detailText = error.localizedFailureReason ?? error.localizedRecoverySuggestion ?? underlyingError?.localizedDescription ?? error.localizedDescription detailText = error.localizedFailureReason ?? error.localizedRecoverySuggestion ?? underlyingError?.localizedDescription ?? error.localizedDescription
} else if let reason = error.localizedFailureReason { }
else if let reason = error.localizedFailureReason
{
text = reason text = reason
detailText = error.localizedRecoverySuggestion ?? underlyingError?.localizedDescription detailText = error.localizedRecoverySuggestion ?? underlyingError?.localizedDescription
} else { }
else
{
text = error.localizedDescription text = error.localizedDescription
detailText = underlyingError?.localizedDescription ?? error.localizedRecoverySuggestion detailText = underlyingError?.localizedDescription ?? error.localizedRecoverySuggestion
} }
self.init(text: text, detailText: detailText) self.init(text: text, detailText: detailText)
if let preferredDuration = preferredDuration { if let preferredDuration = preferredDuration
{
self.preferredDuration = preferredDuration self.preferredDuration = preferredDuration
} }
} }
@available(*, unavailable) required init(coder aDecoder: NSCoder) {
required init(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
override func layoutSubviews() { override func layoutSubviews()
{
super.layoutSubviews() super.layoutSubviews()
// Rough calculation to determine height of ToastView with one-line textLabel. // Rough calculation to determine height of ToastView with one-line textLabel.
let minimumHeight = textLabel.font.lineHeight.rounded() + 18 let minimumHeight = self.textLabel.font.lineHeight.rounded() + 18
layer.cornerRadius = minimumHeight / 2 self.layer.cornerRadius = minimumHeight/2
} }
func show(in viewController: UIViewController) { func show(in viewController: UIViewController)
show(in: viewController.navigationController?.view ?? viewController.view, duration: preferredDuration) {
self.show(in: viewController.navigationController?.view ?? viewController.view, duration: self.preferredDuration)
} }
override func show(in view: UIView, duration: TimeInterval) { override func show(in view: UIView, duration: TimeInterval)
{
super.show(in: view, duration: duration) super.show(in: view, duration: duration)
let announcement = (textLabel.text ?? "") + ". " + (detailTextLabel.text ?? "") let announcement = (self.textLabel.text ?? "") + ". " + (self.detailTextLabel.text ?? "")
accessibilityLabel = announcement self.accessibilityLabel = announcement
// Minimum 0.75 delay to prevent announcement being cut off by VoiceOver. // Minimum 0.75 delay to prevent announcement being cut off by VoiceOver.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.75) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.75) {
UIAccessibility.post(notification: .announcement, argument: announcement) UIAccessibility.post(notification: .announcement, argument: announcement)
} }
} }
override func show(in view: UIView) { override func show(in view: UIView)
show(in: view, duration: preferredDuration) {
self.show(in: view, duration: self.preferredDuration)
} }
} }

View File

@@ -0,0 +1,17 @@
//
// Proxy.swift
// SideStore
//
// Created by Joseph Mattiello on 11/7/22.
// Copyright © 2022 Riley Testut. All rights reserved.
//
import Foundation
public extension Consts {
enum Proxy {
static let address = "127.0.0.1"
static let port = "51820"
static let serverURL = "\(address):\(port)"
}
}

View File

@@ -8,4 +8,6 @@
import Foundation import Foundation
public enum Consts {} public enum Consts {
}

View File

@@ -7,28 +7,30 @@
// //
import Foundation import Foundation
import OSLog
#if canImport(Logging)
import Logging
#endif
extension FileManager { extension FileManager
func directorySize(at directoryURL: URL) -> Int? { {
func directorySize(at directoryURL: URL) -> Int?
{
guard let enumerator = FileManager.default.enumerator(at: directoryURL, includingPropertiesForKeys: [.fileSizeKey]) else { return nil } guard let enumerator = FileManager.default.enumerator(at: directoryURL, includingPropertiesForKeys: [.fileSizeKey]) else { return nil }
var total = 0 var total: Int = 0
for case let fileURL as URL in enumerator { for case let fileURL as URL in enumerator
do { {
do
{
let resourceValues = try fileURL.resourceValues(forKeys: [.fileSizeKey]) let resourceValues = try fileURL.resourceValues(forKeys: [.fileSizeKey])
guard let fileSize = resourceValues.fileSize else { continue } guard let fileSize = resourceValues.fileSize else { continue }
total += fileSize total += fileSize
} catch { }
os_log("Failed to read file size for item: %@. %@", type: .error, fileURL.absoluteString, error.localizedDescription) catch
{
print("Failed to read file size for item: \(fileURL).", error)
} }
} }
return total return total
} }
} }

View File

@@ -10,11 +10,13 @@ import Intents
// Requires iOS 14 in-app intent handling. // Requires iOS 14 in-app intent handling.
@available(iOS 14, *) @available(iOS 14, *)
extension INInteraction { extension INInteraction
static func refreshAllApps() -> INInteraction { {
static func refreshAllApps() -> INInteraction
{
let refreshAllIntent = RefreshAllIntent() let refreshAllIntent = RefreshAllIntent()
refreshAllIntent.suggestedInvocationPhrase = NSString.deferredLocalizedIntentsString(with: "Refresh my apps") as String refreshAllIntent.suggestedInvocationPhrase = NSString.deferredLocalizedIntentsString(with: "Refresh my apps") as String
let interaction = INInteraction(intent: refreshAllIntent, response: nil) let interaction = INInteraction(intent: refreshAllIntent, response: nil)
return interaction return interaction
} }

View File

@@ -10,7 +10,8 @@ import Foundation
import OSLog import OSLog
public let customLog = OSLog(subsystem: "org.sidestore.sidestore", public let customLog = OSLog(subsystem: "org.sidestore.sidestore",
category: "ios") category: "ios")
public extension OSLog { public extension OSLog {
/// Error logger extension /// Error logger extension
@@ -21,7 +22,7 @@ public extension OSLog {
static func error(_ message: StaticString, _ args: CVarArg...) { static func error(_ message: StaticString, _ args: CVarArg...) {
os_log(message, log: customLog, type: .error, args) os_log(message, log: customLog, type: .error, args)
} }
/// Info logger extension /// Info logger extension
/// - Parameters: /// - Parameters:
/// - message: String or format string /// - message: String or format string
@@ -30,7 +31,7 @@ public extension OSLog {
static func info(_ message: StaticString, _ args: CVarArg...) { static func info(_ message: StaticString, _ args: CVarArg...) {
os_log(message, log: customLog, type: .info, args) os_log(message, log: customLog, type: .info, args)
} }
/// Debug logger extension /// Debug logger extension
/// - Parameters: /// - Parameters:
/// - message: String or format string /// - message: String or format string
@@ -48,7 +49,7 @@ public extension OSLog {
/// - message: String or format string /// - message: String or format string
/// - args: optional args for format string /// - args: optional args for format string
@inlinable @inlinable
public func ELOG(_ message: StaticString, file _: StaticString = #file, function _: StaticString = #function, line _: UInt = #line, _ args: CVarArg...) { public func ELOG(_ message: StaticString, file: StaticString = #file, function: StaticString = #function, line: UInt = #line, _ args: CVarArg...) {
OSLog.error(message, args) OSLog.error(message, args)
} }
@@ -57,7 +58,7 @@ public func ELOG(_ message: StaticString, file _: StaticString = #file, function
/// - message: String or format string /// - message: String or format string
/// - args: optional args for format string /// - args: optional args for format string
@inlinable @inlinable
public func ILOG(_ message: StaticString, file _: StaticString = #file, function _: StaticString = #function, line _: UInt = #line, _ args: CVarArg...) { public func ILOG(_ message: StaticString, file: StaticString = #file, function: StaticString = #function, line: UInt = #line, _ args: CVarArg...) {
OSLog.info(message, args) OSLog.info(message, args)
} }
@@ -66,8 +67,8 @@ public func ILOG(_ message: StaticString, file _: StaticString = #file, function
/// - message: String or format string /// - message: String or format string
/// - args: optional args for format string /// - args: optional args for format string
@inlinable @inlinable
public func DLOG(_ message: StaticString, file _: StaticString = #file, function _: StaticString = #function, line _: UInt = #line, _ args: CVarArg...) { public func DLOG(_ message: StaticString, file: StaticString = #file, function: StaticString = #function, line: UInt = #line, _ args: CVarArg...) {
OSLog.debug(message, args) OSLog.debug(message, args)
} }
// MARK: Helpers // mark: Helpers

View File

@@ -6,10 +6,11 @@
// Copyright © 2020 Riley Testut. All rights reserved. // Copyright © 2020 Riley Testut. All rights reserved.
// //
import ARKit
import UIKit import UIKit
import ARKit
extension UIDevice { extension UIDevice
{
var isJailbroken: Bool { var isJailbroken: Bool {
if if
FileManager.default.fileExists(atPath: "/Applications/Cydia.app") || FileManager.default.fileExists(atPath: "/Applications/Cydia.app") ||
@@ -18,29 +19,31 @@ extension UIDevice {
FileManager.default.fileExists(atPath: "/usr/sbin/sshd") || FileManager.default.fileExists(atPath: "/usr/sbin/sshd") ||
FileManager.default.fileExists(atPath: "/etc/apt") || FileManager.default.fileExists(atPath: "/etc/apt") ||
FileManager.default.fileExists(atPath: "/private/var/lib/apt/") || FileManager.default.fileExists(atPath: "/private/var/lib/apt/") ||
UIApplication.shared.canOpenURL(URL(string: "cydia://")!) UIApplication.shared.canOpenURL(URL(string:"cydia://")!)
{ {
return true return true
} else { }
else
{
return false return false
} }
} }
@available(iOS 14, *) @available(iOS 14, *)
var supportsFugu14: Bool { var supportsFugu14: Bool {
#if targetEnvironment(simulator) #if targetEnvironment(simulator)
return true return true
#else #else
// Fugu14 is supported on devices with an A12 processor or better. // Fugu14 is supported on devices with an A12 processor or better.
// ARKit 3 is only supported by devices with an A12 processor or better, according to the documentation. // ARKit 3 is only supported by devices with an A12 processor or better, according to the documentation.
return ARBodyTrackingConfiguration.isSupported return ARBodyTrackingConfiguration.isSupported
#endif #endif
} }
@available(iOS 14, *) @available(iOS 14, *)
var isUntetheredJailbreakRequired: Bool { var isUntetheredJailbreakRequired: Bool {
let ios14_4 = OperatingSystemVersion(majorVersion: 14, minorVersion: 4, patchVersion: 0) let ios14_4 = OperatingSystemVersion(majorVersion: 14, minorVersion: 4, patchVersion: 0)
let isUntetheredJailbreakRequired = ProcessInfo.processInfo.isOperatingSystemAtLeast(ios14_4) let isUntetheredJailbreakRequired = ProcessInfo.processInfo.isOperatingSystemAtLeast(ios14_4)
return isUntetheredJailbreakRequired return isUntetheredJailbreakRequired
} }

View File

@@ -8,32 +8,37 @@
import AudioToolbox import AudioToolbox
import CoreHaptics import CoreHaptics
import UIKit
private extension SystemSoundID { private extension SystemSoundID
{
static let pop = SystemSoundID(1520) static let pop = SystemSoundID(1520)
static let cancelled = SystemSoundID(1521) static let cancelled = SystemSoundID(1521)
static let tryAgain = SystemSoundID(1102) static let tryAgain = SystemSoundID(1102)
} }
@available(iOS 13, *) @available(iOS 13, *)
extension UIDevice { extension UIDevice
enum VibrationPattern { {
enum VibrationPattern
{
case success case success
case error case error
} }
} }
@available(iOS 13, *) @available(iOS 13, *)
extension UIDevice { extension UIDevice
{
var isVibrationSupported: Bool { var isVibrationSupported: Bool {
CHHapticEngine.capabilitiesForHardware().supportsHaptics return CHHapticEngine.capabilitiesForHardware().supportsHaptics
} }
func vibrate(pattern: VibrationPattern) { func vibrate(pattern: VibrationPattern)
guard isVibrationSupported else { return } {
guard self.isVibrationSupported else { return }
switch pattern {
switch pattern
{
case .success: AudioServicesPlaySystemSound(.tryAgain) case .success: AudioServicesPlaySystemSound(.tryAgain)
case .error: AudioServicesPlaySystemSound(.cancelled) case .error: AudioServicesPlaySystemSound(.cancelled)
} }

View File

@@ -8,8 +8,9 @@
import UIKit import UIKit
extension UIScreen { extension UIScreen
{
var isExtraCompactHeight: Bool { var isExtraCompactHeight: Bool {
fixedCoordinateSpace.bounds.height < 600 return self.fixedCoordinateSpace.bounds.height < 600
} }
} }

View File

@@ -2,8 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>ALTAnisetteURL</key>
<string>https://ani.sidestore.io</string>
<key>ALTAppGroups</key> <key>ALTAppGroups</key>
<array> <array>
<string>group.$(APP_GROUP_IDENTIFIER)</string> <string>group.$(APP_GROUP_IDENTIFIER)</string>
@@ -11,10 +9,12 @@
</array> </array>
<key>ALTDeviceID</key> <key>ALTDeviceID</key>
<string>00008101-000129D63698001E</string> <string>00008101-000129D63698001E</string>
<key>ALTPairingFile</key>
<string>&lt;insert pairing file here&gt;</string>
<key>ALTServerID</key> <key>ALTServerID</key>
<string>1F7D5B55-79CE-4546-A029-D4DDC4AF3B6D</string> <string>1F7D5B55-79CE-4546-A029-D4DDC4AF3B6D</string>
<key>ALTPairingFile</key>
<string>&lt;insert pairing file here&gt;</string>
<key>ALTAnisetteURL</key>
<string>https://ani.sidestore.io</string>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDocumentTypes</key> <key>CFBundleDocumentTypes</key>
@@ -44,6 +44,8 @@
<string>$(PRODUCT_NAME)</string> <string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string> <string>$(MARKETING_VERSION)</string>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>
@@ -91,13 +93,6 @@
</array> </array>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSBonjourServices</key> <key>NSBonjourServices</key>
<array> <array>
<string>_altserver._tcp</string> <string>_altserver._tcp</string>
@@ -136,10 +131,13 @@
<string>fetch</string> <string>fetch</string>
<string>remote-notification</string> <string>remote-notification</string>
</array> </array>
<key>UIFileSharingEnabled</key>
<true/>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>
<string>Main</string> <string>Main</string>
<key>UIRequiredDeviceCapabilities</key> <key>UIRequiredDeviceCapabilities</key>
@@ -206,5 +204,7 @@
</dict> </dict>
</dict> </dict>
</array> </array>
<key>UIFileSharingEnabled</key>
<true/>
</dict> </dict>
</plist> </plist>

View File

@@ -7,74 +7,84 @@
// //
import Foundation import Foundation
import SideStoreCore
import Intents import AltStoreCore
import OSLog
#if canImport(Logging)
import Logging
#endif
@available(iOS 14, *) @available(iOS 14, *)
public final class IntentHandler: NSObject, RefreshAllIntentHandling { final class IntentHandler: NSObject, RefreshAllIntentHandling
{
private let queue = DispatchQueue(label: "io.altstore.IntentHandler") private let queue = DispatchQueue(label: "io.altstore.IntentHandler")
private var completionHandlers = [RefreshAllIntent: (RefreshAllIntentResponse) -> Void]() private var completionHandlers = [RefreshAllIntent: (RefreshAllIntentResponse) -> Void]()
private var queuedResponses = [RefreshAllIntent: RefreshAllIntentResponse]() private var queuedResponses = [RefreshAllIntent: RefreshAllIntentResponse]()
private var operations = [RefreshAllIntent: BackgroundRefreshAppsOperation]() private var operations = [RefreshAllIntent: BackgroundRefreshAppsOperation]()
public func confirm(intent: RefreshAllIntent, completion: @escaping (RefreshAllIntentResponse) -> Void) { func confirm(intent: RefreshAllIntent, completion: @escaping (RefreshAllIntentResponse) -> Void)
{
// Refreshing apps usually, but not always, completes within alotted time. // Refreshing apps usually, but not always, completes within alotted time.
// As a workaround, we'll start refreshing apps in confirm() so we can // As a workaround, we'll start refreshing apps in confirm() so we can
// take advantage of some extra time before starting handle() timeout timer. // take advantage of some extra time before starting handle() timeout timer.
completionHandlers[intent] = { response in self.completionHandlers[intent] = { (response) in
if response.code != .ready { if response.code != .ready
{
// Operation finished before confirmation "timeout". // Operation finished before confirmation "timeout".
// Cache response to return it when handle() is called. // Cache response to return it when handle() is called.
self.queuedResponses[intent] = response self.queuedResponses[intent] = response
} }
completion(RefreshAllIntentResponse(code: .ready, userActivity: nil)) completion(RefreshAllIntentResponse(code: .ready, userActivity: nil))
} }
// Give ourselves 9 extra seconds before starting handle() timeout timer. // Give ourselves 9 extra seconds before starting handle() timeout timer.
// 10 seconds or longer results in timeout regardless. // 10 seconds or longer results in timeout regardless.
queue.asyncAfter(deadline: .now() + 9.0) { self.queue.asyncAfter(deadline: .now() + 9.0) {
self.finish(intent, response: RefreshAllIntentResponse(code: .ready, userActivity: nil)) self.finish(intent, response: RefreshAllIntentResponse(code: .ready, userActivity: nil))
} }
if !DatabaseManager.shared.isStarted { if !DatabaseManager.shared.isStarted
DatabaseManager.shared.start { error in {
if let error = error { DatabaseManager.shared.start() { (error) in
if let error = error
{
self.finish(intent, response: RefreshAllIntentResponse.failure(localizedDescription: error.localizedDescription)) self.finish(intent, response: RefreshAllIntentResponse.failure(localizedDescription: error.localizedDescription))
} else { }
else
{
self.refreshApps(intent: intent) self.refreshApps(intent: intent)
} }
} }
} else { }
refreshApps(intent: intent) else
{
self.refreshApps(intent: intent)
} }
} }
public func handle(intent: RefreshAllIntent, completion: @escaping (RefreshAllIntentResponse) -> Void) { func handle(intent: RefreshAllIntent, completion: @escaping (RefreshAllIntentResponse) -> Void)
completionHandlers[intent] = { response in {
self.completionHandlers[intent] = { (response) in
// Ignore .ready response from confirm() timeout. // Ignore .ready response from confirm() timeout.
guard response.code != .ready else { return } guard response.code != .ready else { return }
completion(response) completion(response)
} }
if let response = queuedResponses[intent] { if let response = self.queuedResponses[intent]
queuedResponses[intent] = nil {
finish(intent, response: response) self.queuedResponses[intent] = nil
} else { self.finish(intent, response: response)
queue.asyncAfter(deadline: .now() + 7.0) { }
if let operation = self.operations[intent] { else
{
self.queue.asyncAfter(deadline: .now() + 7.0) {
if let operation = self.operations[intent]
{
// We took too long to finish and return the final result, // We took too long to finish and return the final result,
// so we'll now present a normal notification when finished. // so we'll now present a normal notification when finished.
operation.presentsFinishedNotification = true operation.presentsFinishedNotification = true
} }
self.finish(intent, response: RefreshAllIntentResponse(code: .inProgress, userActivity: nil)) self.finish(intent, response: RefreshAllIntentResponse(code: .inProgress, userActivity: nil))
} }
} }
@@ -82,42 +92,54 @@ public final class IntentHandler: NSObject, RefreshAllIntentHandling {
} }
@available(iOS 14, *) @available(iOS 14, *)
private extension IntentHandler { private extension IntentHandler
func finish(_ intent: RefreshAllIntent, response: RefreshAllIntentResponse) { {
queue.async { func finish(_ intent: RefreshAllIntent, response: RefreshAllIntentResponse)
if let completionHandler = self.completionHandlers[intent] { {
self.queue.async {
if let completionHandler = self.completionHandlers[intent]
{
self.completionHandlers[intent] = nil self.completionHandlers[intent] = nil
completionHandler(response) completionHandler(response)
} else if response.code != .ready && response.code != .inProgress { }
else if response.code != .ready && response.code != .inProgress
{
// Queue response in case refreshing finishes after confirm() but before handle(). // Queue response in case refreshing finishes after confirm() but before handle().
self.queuedResponses[intent] = response self.queuedResponses[intent] = response
} }
} }
} }
func refreshApps(intent: RefreshAllIntent) { func refreshApps(intent: RefreshAllIntent)
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in {
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
let installedApps = InstalledApp.fetchActiveApps(in: context) let installedApps = InstalledApp.fetchActiveApps(in: context)
let operation = AppManager.shared.backgroundRefresh(installedApps, presentsNotifications: false) { result in let operation = AppManager.shared.backgroundRefresh(installedApps, presentsNotifications: false) { (result) in
do { do
{
let results = try result.get() let results = try result.get()
for (_, result) in results { for (_, result) in results
{
guard case let .failure(error) = result else { continue } guard case let .failure(error) = result else { continue }
throw error throw error
} }
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil)) self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
} catch RefreshError.noInstalledApps { }
catch RefreshError.noInstalledApps
{
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil)) self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
} catch let error as NSError { }
os_log("Failed to refresh apps in background. %@", type: .error , error.localizedDescription) catch let error as NSError
{
print("Failed to refresh apps in background.", error)
self.finish(intent, response: RefreshAllIntentResponse.failure(localizedDescription: error.localizedFailureReason ?? error.localizedDescription)) self.finish(intent, response: RefreshAllIntentResponse.failure(localizedDescription: error.localizedFailureReason ?? error.localizedDescription))
} }
self.operations[intent] = nil self.operations[intent] = nil
} }
self.operations[intent] = operation self.operations[intent] = operation
} }
} }

View File

@@ -6,58 +6,60 @@
// Copyright © 2019 Riley Testut. All rights reserved. // Copyright © 2019 Riley Testut. All rights reserved.
// //
import UIKit
import Roxas
import EmotionalDamage import EmotionalDamage
import minimuxer import minimuxer
import MiniMuxer
import SideStoreAppKit
import RoxasUIKit
import UIKit
import SideStoreCore import AltStoreCore
import UniformTypeIdentifiers import UniformTypeIdentifiers
final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate { let pairingFileName = "ALTPairingFile.mobiledevicepairing"
final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
{
private var didFinishLaunching = false private var didFinishLaunching = false
private var destinationViewController: UIViewController! private var destinationViewController: UIViewController!
override var launchConditions: [RSTLaunchCondition] { override var launchConditions: [RSTLaunchCondition] {
let isDatabaseStarted = RSTLaunchCondition(condition: { DatabaseManager.shared.isStarted }) { completionHandler in let isDatabaseStarted = RSTLaunchCondition(condition: { DatabaseManager.shared.isStarted }) { (completionHandler) in
DatabaseManager.shared.start(completionHandler: completionHandler) DatabaseManager.shared.start(completionHandler: completionHandler)
} }
return [isDatabaseStarted] return [isDatabaseStarted]
} }
override var childForStatusBarStyle: UIViewController? { override var childForStatusBarStyle: UIViewController? {
self.children.first return self.children.first
} }
override var childForStatusBarHidden: UIViewController? { override var childForStatusBarHidden: UIViewController? {
self.children.first return self.children.first
} }
override func viewDidLoad() { override func viewDidLoad()
{
defer { defer {
// Create destinationViewController now so view controllers can register for receiving Notifications. // Create destinationViewController now so view controllers can register for receiving Notifications.
self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as! TabBarController self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as! TabBarController
} }
super.viewDidLoad() super.viewDidLoad()
} }
override func viewDidAppear(_: Bool) { override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true) super.viewDidAppear(true)
#if !targetEnvironment(simulator) #if !targetEnvironment(simulator)
start_em_proxy(bind_addr: Consts.Proxy.serverURL) start_em_proxy(bind_addr: Consts.Proxy.serverURL)
guard let pf = fetchPairingFile() else { guard let pf = fetchPairingFile() else {
displayError("Device pairing file not found.") displayError("Device pairing file not found.")
return return
} }
start_minimuxer_threads(pf) start_minimuxer_threads(pf)
#endif #endif
} }
func fetchPairingFile() -> String? { func fetchPairingFile() -> String? {
let filename = "ALTPairingFile.mobiledevicepairing" let filename = "ALTPairingFile.mobiledevicepairing"
let fm = FileManager.default let fm = FileManager.default
@@ -70,20 +72,19 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
fm.fileExists(atPath: appResourcePath.path), fm.fileExists(atPath: appResourcePath.path),
let data = fm.contents(atPath: appResourcePath.path), let data = fm.contents(atPath: appResourcePath.path),
let contents = String(data: data, encoding: .utf8), let contents = String(data: data, encoding: .utf8),
!contents.isEmpty !contents.isEmpty {
{
print("Loaded ALTPairingFile from \(appResourcePath.path)") print("Loaded ALTPairingFile from \(appResourcePath.path)")
return contents return contents
} else if let plistString = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String, !plistString.isEmpty, !plistString.contains("insert pairing file here") { } else if let plistString = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String, !plistString.isEmpty, !plistString.contains("insert pairing file here"){
print("Loaded ALTPairingFile from Info.plist") print("Loaded ALTPairingFile from Info.plist")
return plistString return plistString
} else { } else {
// Show an alert explaining the pairing file // Show an alert explaining the pairing file
// Create new Alert // Create new Alert
let dialogMessage = UIAlertController(title: "Pairing File", message: "Select the pairing file for your device. For more information, go to https://wiki.sidestore.io/guides/install#pairing-process", preferredStyle: .alert) let dialogMessage = UIAlertController(title: "Pairing File", message: "Select the pairing file for your device. For more information, go to https://wiki.sidestore.io/guides/getting-started/#pairing-file", preferredStyle: .alert)
// Create OK button with action handler // Create OK button with action handler
let ok = UIAlertAction(title: "OK", style: .default, handler: { _ in let ok = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in
// Try to load it from a file picker // Try to load it from a file picker
var types = UTType.types(tag: "plist", tagClass: UTTagClass.filenameExtension, conformingTo: nil) var types = UTType.types(tag: "plist", tagClass: UTTagClass.filenameExtension, conformingTo: nil)
types.append(contentsOf: UTType.types(tag: "mobiledevicepairing", tagClass: UTTagClass.filenameExtension, conformingTo: UTType.data)) types.append(contentsOf: UTType.types(tag: "mobiledevicepairing", tagClass: UTTagClass.filenameExtension, conformingTo: UTType.data))
@@ -92,13 +93,13 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
documentPickerController.shouldShowFileExtensions = true documentPickerController.shouldShowFileExtensions = true
documentPickerController.delegate = self documentPickerController.delegate = self
self.present(documentPickerController, animated: true, completion: nil) self.present(documentPickerController, animated: true, completion: nil)
}) })
// Add OK button to a dialog message //Add OK button to a dialog message
dialogMessage.addAction(ok) dialogMessage.addAction(ok)
// Present Alert to // Present Alert to
present(dialogMessage, animated: true, completion: nil) self.present(dialogMessage, animated: true, completion: nil)
return nil return nil
} }
@@ -110,9 +111,9 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
let dialogMessage = UIAlertController(title: "Error launching SideStore", message: msg, preferredStyle: .alert) let dialogMessage = UIAlertController(title: "Error launching SideStore", message: msg, preferredStyle: .alert)
// Present alert to user // Present alert to user
present(dialogMessage, animated: true, completion: nil) self.present(dialogMessage, animated: true, completion: nil)
} }
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
let url = urls[0] let url = urls[0]
let isSecuredURL = url.startAccessingSecurityScopedResource() == true let isSecuredURL = url.startAccessingSecurityScopedResource() == true
@@ -124,95 +125,99 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
if pairing_string == nil { if pairing_string == nil {
displayError("Unable to read pairing file") displayError("Unable to read pairing file")
} }
// Save to a file for next launch // Save to a file for next launch
let filename = "ALTPairingFile.mobiledevicepairing" let pairingFile = FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)")
let fm = FileManager.default try pairing_string?.write(to: pairingFile, atomically: true, encoding: String.Encoding.utf8)
let documentsPath = fm.documentsDirectory.appendingPathComponent("/\(filename)")
try pairing_string?.write(to: documentsPath, atomically: true, encoding: String.Encoding.utf8)
// Start minimuxer now that we have a file // Start minimuxer now that we have a file
start_minimuxer_threads(pairing_string!) start_minimuxer_threads(pairing_string!)
} catch { } catch {
displayError("Unable to read pairing file") displayError("Unable to read pairing file")
} }
if isSecuredURL { if (isSecuredURL) {
url.stopAccessingSecurityScopedResource() url.stopAccessingSecurityScopedResource()
} }
controller.dismiss(animated: true, completion: nil) controller.dismiss(animated: true, completion: nil)
} }
func documentPickerWasCancelled(_: UIDocumentPickerViewController) { func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
displayError("Choosing a pairing file was cancelled. Please re-open the app and try again.") displayError("Choosing a pairing file was cancelled. Please re-open the app and try again.")
} }
func start_minimuxer_threads(_ pairing_file: String) { func start_minimuxer_threads(_ pairing_file: String) {
set_usbmuxd_socket() target_minimuxer_address()
#if false // Retries let documentsDirectory = FileManager.default.documentsDirectory.absoluteString
var res = start_minimuxer(pairing_file: pairing_file) do {
var attempts = 10 try start(pairing_file, documentsDirectory)
while attempts != 0, res != 0 { } catch {
print("start_minimuxer `res` != 0, retry #\(attempts)") try! FileManager.default.removeItem(at: FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)"))
res = start_minimuxer(pairing_file: pairing_file) displayError("minimuxer failed to start, please restart SideStore. \((error as? LocalizedError)?.failureReason ?? "UNKNOWN ERROR!!!!!! REPORT TO GITHUB ISSUES!")")
attempts -= 1 }
} if #available(iOS 17, *) {
#else // TODO: iOS 17 and above have a new JIT implementation that is completely broken in SideStore :(
let res = start_minimuxer(pairing_file: pairing_file) }
#endif else {
if res != 0 { start_auto_mounter(documentsDirectory)
displayError("minimuxer failed to start. Incorrect arguments were passed.")
} }
auto_mount_dev_image()
} }
} }
extension LaunchViewController { extension LaunchViewController
override func handleLaunchError(_ error: Error) { {
do { override func handleLaunchError(_ error: Error)
{
do
{
throw error throw error
} catch let error as NSError { }
catch let error as NSError
{
let title = error.userInfo[NSLocalizedFailureErrorKey] as? String ?? NSLocalizedString("Unable to Launch SideStore", comment: "") let title = error.userInfo[NSLocalizedFailureErrorKey] as? String ?? NSLocalizedString("Unable to Launch SideStore", comment: "")
let errorDescription: String let errorDescription: String
if #available(iOS 14.5, *) { if #available(iOS 14.5, *)
{
let errorMessages = [error.debugDescription] + error.underlyingErrors.map { ($0 as NSError).debugDescription } let errorMessages = [error.debugDescription] + error.underlyingErrors.map { ($0 as NSError).debugDescription }
errorDescription = errorMessages.joined(separator: "\n\n") errorDescription = errorMessages.joined(separator: "\n\n")
} else { }
else
{
errorDescription = error.debugDescription errorDescription = error.debugDescription
} }
let alertController = UIAlertController(title: title, message: errorDescription, preferredStyle: .alert) let alertController = UIAlertController(title: title, message: errorDescription, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: NSLocalizedString("Retry", comment: ""), style: .default, handler: { _ in alertController.addAction(UIAlertAction(title: NSLocalizedString("Retry", comment: ""), style: .default, handler: { (action) in
self.handleLaunchConditions() self.handleLaunchConditions()
})) }))
self.present(alertController, animated: true, completion: nil) self.present(alertController, animated: true, completion: nil)
} }
} }
override func finishLaunching() { override func finishLaunching()
{
super.finishLaunching() super.finishLaunching()
guard !didFinishLaunching else { return } guard !self.didFinishLaunching else { return }
AppManager.shared.update() AppManager.shared.update()
AppManager.shared.updatePatronsIfNeeded() AppManager.shared.updatePatronsIfNeeded()
PatreonAPI.shared.refreshPatreonAccount() PatreonAPI.shared.refreshPatreonAccount()
// Add view controller as child (rather than presenting modally) // Add view controller as child (rather than presenting modally)
// so tint adjustment + card presentations works correctly. // so tint adjustment + card presentations works correctly.
destinationViewController.view.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height: view.bounds.height) self.destinationViewController.view.frame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.height)
destinationViewController.view.alpha = 0.0 self.destinationViewController.view.alpha = 0.0
addChild(destinationViewController) self.addChild(self.destinationViewController)
view.addSubview(destinationViewController.view, pinningEdgesWith: .zero) self.view.addSubview(self.destinationViewController.view, pinningEdgesWith: .zero)
destinationViewController.didMove(toParent: self) self.destinationViewController.didMove(toParent: self)
UIView.animate(withDuration: 0.2) { UIView.animate(withDuration: 0.2) {
self.destinationViewController.view.alpha = 1.0 self.destinationViewController.view.alpha = 1.0
} }
didFinishLaunching = true self.didFinishLaunching = true
} }
} }

View File

@@ -0,0 +1,86 @@
//
// AppManagerErrors.swift
// AltStore
//
// Created by Riley Testut on 8/27/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import CoreData
import AltStoreCore
extension AppManager
{
struct FetchSourcesError: LocalizedError, CustomNSError
{
var primaryError: Error?
var sources: Set<Source>?
var errors = [Source: Error]()
var managedObjectContext: NSManagedObjectContext?
var errorDescription: String? {
if let error = self.primaryError
{
return error.localizedDescription
}
else
{
var localizedDescription: String?
self.managedObjectContext?.performAndWait {
if self.sources?.count == 1
{
localizedDescription = NSLocalizedString("Could not refresh store.", comment: "")
}
else if self.errors.count == 1
{
guard let source = self.errors.keys.first else { return }
localizedDescription = String(format: NSLocalizedString("Could not refresh source “%@”.", comment: ""), source.name)
}
else
{
localizedDescription = String(format: NSLocalizedString("Could not refresh %@ sources.", comment: ""), NSNumber(value: self.errors.count))
}
}
return localizedDescription
}
}
var recoverySuggestion: String? {
if let error = self.primaryError as NSError?
{
return error.localizedRecoverySuggestion
}
else if self.errors.count == 1
{
return nil
}
else
{
return NSLocalizedString("Tap to view source errors.", comment: "")
}
}
var errorUserInfo: [String : Any] {
guard let error = self.errors.values.first, self.errors.count == 1 else { return [:] }
return [NSUnderlyingErrorKey: error]
}
init(_ error: Error)
{
self.primaryError = error
}
init(sources: Set<Source>, errors: [Source: Error], context: NSManagedObjectContext)
{
self.sources = sources
self.errors = errors
self.managedObjectContext = context
}
}
}

View File

@@ -0,0 +1,44 @@
//
// InstalledAppsCollectionHeaderView.swift
// AltStore
//
// Created by Riley Testut on 3/9/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import UIKit
final class InstalledAppsCollectionHeaderView: UICollectionReusableView
{
let textLabel: UILabel
let button: UIButton
override init(frame: CGRect)
{
self.textLabel = UILabel()
self.textLabel.translatesAutoresizingMaskIntoConstraints = false
self.textLabel.font = UIFont.systemFont(ofSize: 24, weight: .bold)
self.textLabel.accessibilityTraits.insert(.header)
self.button = UIButton(type: .system)
self.button.translatesAutoresizingMaskIntoConstraints = false
self.button.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .medium)
super.init(frame: frame)
self.addSubview(self.textLabel)
self.addSubview(self.button)
NSLayoutConstraint.activate([self.textLabel.leadingAnchor.constraint(equalTo: self.layoutMarginsGuide.leadingAnchor),
self.textLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor)])
NSLayoutConstraint.activate([self.button.trailingAnchor.constraint(equalTo: self.layoutMarginsGuide.trailingAnchor),
self.button.firstBaselineAnchor.constraint(equalTo: self.textLabel.firstBaselineAnchor)])
self.preservesSuperviewLayoutMargins = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

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