Compare commits

..

53 Commits

Author SHA1 Message Date
naturecodevoid
8cb5b3d47d ci: trigger build 2023-06-19 13:44:14 -07:00
Spidy123222
3d9c5ad890 Add onboarding issue number
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-06-14 19:02:15 -07:00
naturecodevoid
6f14b6b046 improve: move Reset Image Cache to Dev Mode 2023-06-14 18:54:18 -07:00
naturecodevoid
c3f5d9f218 fix(MDC): use free app limit for messages instead of hardcoding 3
it might be better to specify "with MDC"
2023-06-14 18:51:34 -07:00
naturecodevoid
91d3a528a0 improve: enable dev mode by default on simulator 2023-06-14 17:49:48 -07:00
naturecodevoid
0fc8f3d72e improve: fakeUndo3AppLimitPatch button wording 2023-06-14 17:48:39 -07:00
naturecodevoid
a959dd73bb feat(dev mode): add button to force 10 app limit 2023-06-14 17:47:30 -07:00
naturecodevoid
3c0995b5fa improve: lock more things behind UNSTABLE compile time flag 2023-06-13 20:40:35 -07:00
naturecodevoid
34bbe93b3d improve: move onboarding into a separate unstable feature 2023-06-13 20:37:26 -07:00
Spidy123222
ff24ea81c9 Add proper GitHub issues
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-06-12 23:27:27 -07:00
Spidy123222
18d251c364 Merge branch 'develop' into feature/unstable-features 2023-06-12 23:09:30 -07:00
naturecodevoid
2ff637f62e [skip ci] refactor: rename CowExploits to MDC 2023-06-04 08:08:48 -07:00
naturecodevoid
373a73c158 fix(MDC): make Info.plist valid again and actually use the info.plist preprocessor 2023-06-04 08:01:45 -07:00
naturecodevoid
95e98a17bb fix: add NSAppleMusicUsageDescription to Info.plist for MDC 2023-06-04 07:31:58 -07:00
naturecodevoid
8bd8ec8723 fix(MDC): revert f1shy's MDC changes and renames 2023-06-04 07:19:42 -07:00
naturecodevoid
e7f766095a fix(SwiftUI onboarding): make pairing file text wrap, only show full onboarding if SwiftUI unstable feature is enabled
also update anisette servers
2023-06-03 12:40:47 -07:00
naturecodevoid
7e9aafe86e ci: fix early build exit 2023-06-03 11:09:05 -07:00
naturecodevoid
51f900a5bb style: remove whitespace from README 2023-06-03 10:52:54 -07:00
naturecodevoid
02e63f2303 Merge remote-tracking branch 'origin/develop' into feature/unstable-features 2023-06-03 10:52:15 -07:00
naturecodevoid
b45108e519 ci: trigger build 2023-06-03 10:45:02 -07:00
naturecodevoid
28ecca5ed0 ci: make MDC ipa 2023-06-03 10:40:04 -07:00
naturecodevoid
742feed356 fix: compile error when not making an MDC build 2023-06-03 07:17:18 -07:00
naturecodevoid
b8c12a1041 fix: compile error 2023-06-02 22:06:05 -07:00
naturecodevoid
a6349198cf improve: use guard instead of if 2023-06-01 07:39:36 -07:00
naturecodevoid
465c87d442 feat: MDC (and update generated localizations and project file) 2023-06-01 07:38:26 -07:00
naturecodevoid
40c6d60138 improve: add more capabilities to FilledButtonStyle 2023-06-01 07:37:07 -07:00
naturecodevoid
7bb1c1cf05 refactor: Reduce duplicate code with Error.message()
also add some things I forgot in previous commits
2023-06-01 07:36:40 -07:00
naturecodevoid
175b5bec95 refactor: Reduce duplication code with UIApplication.keyWindow and .topController and improve alert function 2023-06-01 07:35:07 -07:00
naturecodevoid
f69ad9830a improve: put dev mode in better sections 2023-06-01 07:26:30 -07:00
naturecodevoid
3ee53e8c2b fix(SwiftUI): improve chevronRight colors for credit links 2023-05-29 20:33:30 -07:00
naturecodevoid
93ae81159e move UnstableFeaturesView to Unstable Features folder 2023-05-29 18:56:48 -07:00
naturecodevoid
6a942a3971 feat: enable unstable features on nightly and PR builds 2023-05-29 18:08:47 -07:00
naturecodevoid
5853aaa778 update comment with URL of example of onEnable and onDisable hooks to use a more permanent URL
Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-05-27 22:21:36 -07:00
naturecodevoid
54703ddca3 feat: allow changing SideStore app icon from within My Apps 2023-05-27 22:10:26 -07:00
naturecodevoid
ce90ae4195 Remove Settings.bundle in favor of in-app advanced settings 2023-05-27 21:57:30 -07:00
naturecodevoid
026392dbc7 More improvements and fixes (see commit description)
- put SwiftUI in an unstable feature
- Add Reset adi.pb to SwiftUI settings
- Add localizations to more things such as Error Log and Refresh Attempts
- Move debug logging into Advanced Settings
- Add padding to version text at the bottom of SwiftUI settings
- Add some things to Unstable Features such as nesting the Feature enum in UnstableFeatures and allowing on enable/disable hooks
- Don't use ObservableObject for UnstableFeatures as it's not needed
- fix a bug with unstable features where the toggle would be reverted if you go into another tab and then back
- Use SwiftUI advanced settings in UIKit
2023-05-27 21:53:04 -07:00
naturecodevoid
d2c15b5acd project: strip all symbols in an attempt to exclude swiftui from non-unstable builds and reduce binary size 2023-05-24 21:02:23 -07:00
naturecodevoid
2219035cd0 More improvements to unstable features and advanced settings
- added description of what they are and notice if there are none available
- move them to advanced settings
- add alert for unstable features in dev mode if the build does not have them enabled
- move stuff out of the danger zone and into anisette section in advanced settings
2023-05-24 21:01:11 -07:00
naturecodevoid
a8917f095e fix: remove jkcoxson anisette servers from SwiftUI advanced settings 2023-05-24 07:39:58 -07:00
naturecodevoid
3cab2e5d15 fix: add https to ani.sidestore.io in SwiftUI advanced settings 2023-05-24 07:38:39 -07:00
naturecodevoid
e2c5267d3f fix: rename allowDevModeOnlyFeatures to inDevMode and simplify dummy filtering 2023-05-20 14:35:59 -07:00
naturecodevoid
5709229fdf fix: remove weird character
Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-05-20 14:28:33 -07:00
naturecodevoid
e1607d2f61 fix: Hide ratings in app detail 2023-05-20 14:23:45 -07:00
naturecodevoid
637a0354c5 feat: view to enable/disable Unstable Features 2023-05-20 14:23:25 -07:00
Fabian Thies
9c3461b0c6 Fix disabling horizontal scroll on onboarding screens and made showing only certain steps more reusable 2023-05-20 22:14:50 +02:00
naturecodevoid
e3103b3034 Move TabBarController.swift into UIKit folder 2023-05-20 12:42:19 -07:00
naturecodevoid
2db073d2c5 Reorganize AltStore project into UIKit and SwiftUI folders 2023-05-20 12:35:53 -07:00
naturecodevoid
e06cca8224 Fixes (see commit description)
- Fix issue caused by merge
- Improve icons in onboarding
- Use onboarding's pairing file step properly
2023-05-20 12:25:07 -07:00
naturecodevoid
3a7cd29b22 Merge SwiftUI (#221) + SwiftUI improvements (#265)
commit 22f1ff7cd7d4d4750eeda2067d23846900239b83
Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
Date:   Sat May 20 11:29:01 2023 -0700

    fix: actually disable LocalConsole's character limit

commit 4b51915da7bc0637ccf819ac45c7d727d450ae12
Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
Date:   Sat May 20 11:27:12 2023 -0700

    Merge SwiftUI improvements (#265)

    commit 7f73f2adef
    Merge: 72f34dd2 38a1c7ee
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Sat May 20 11:23:07 2023 -0700

        Merge remote-tracking branch 'origin/fabianthdev/feature/SwiftUI' into naturecodevoid/swiftui-improvements

    commit 72f34dd286
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Wed Apr 12 18:21:49 2023 -0700

        feat: default to Storm icon for PR builds

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

    commit 060c37c423
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Sun Apr 9 19:40:53 2023 -0700

        fix(icons): sky appears correctly in light mode

    commit 8c2968aeb3
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Sun Apr 9 14:29:03 2023 -0700

        fix: build errors

    commit 4f512b6318
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Sun Apr 9 13:54:01 2023 -0700

        project(minimuxer): fix actions build error

    commit 5b752cf26e
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Sun Apr 9 13:51:54 2023 -0700

        fix: remove duplicate isSideStore checks with a StoreApp extension

    commit 62a478277e
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Sun Apr 9 13:41:58 2023 -0700

        fix(AsyncFallibleButton): try to use failureReason and then fallback to localizedDescription

    commit 994b2318a9
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Sun Apr 9 13:38:44 2023 -0700

        feat(dev mode): add AFC file explorer and dump profiles

    commit 423ac28ba3
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Sun Apr 9 13:35:14 2023 -0700

        project(AltStore): xcode wants to move these around I guess

    commit af2cdd48d6
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Sun Apr 9 13:34:57 2023 -0700

        feat: add debug logging toggle

    commit 44fe0c5686
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Sun Apr 9 13:33:11 2023 -0700

        project(minimuxer): Add libminimuxer as an input file for build step

    commit 3d46a3069a
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Sun Apr 9 13:32:22 2023 -0700

        fix: handle source conflict in merge policy

    commit 82e8fb7389
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Sun Apr 9 13:31:39 2023 -0700

        docs: include info on Developer Mode

    commit 1dd0cd7d90
    Merge: 92a9650c 566841a9
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Thu Apr 6 21:07:33 2023 -0700

        Merge branch 'fabianthdev/feature/SwiftUI' into naturecodevoid/swiftui-improvements

    commit 566841a9a6
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Thu Apr 6 21:06:07 2023 -0700

        Fix not being able to open the project

    commit 92a9650c0c
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Thu Apr 6 20:49:49 2023 -0700

        Apply DevModeView suggestion

    commit df94e79472
    Merge: d3cfc4ba cd2c5ad7
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Thu Apr 6 20:48:52 2023 -0700

        Merge branch 'fabianthdev/feature/SwiftUI' into naturecodevoid/swiftui-improvements

    commit cd2c5ad7b4
    Merge: 3466870d 6146f1bd
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Thu Apr 6 20:43:10 2023 -0700

        Merge remote-tracking branch 'origin/develop' into fabianthdev/feature/SwiftUI

    commit d3cfc4bab9
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Wed Feb 22 13:05:11 2023 -0800

        FileExplorer: Replace file when inserting

    commit df62461d4a
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Wed Feb 22 13:04:52 2023 -0800

        Settings: Add Export Logs and commit xcodeproj changes

    commit 817d2de5e0
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Wed Feb 22 12:19:07 2023 -0800

        Rename View+SideStore

    commit 3ea478ad05
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Wed Feb 22 12:18:42 2023 -0800

        DevMode: Add password

    commit 13f9a9d1bf
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Wed Feb 22 11:43:13 2023 -0800

        AdvancedSettingsView: improve anisette URL by using a label instead of a placeholder

    commit 3821a6034d
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Tue Feb 21 17:34:56 2023 -0800

        project: attempt to fix crashing on launch

    commit 3e8d7da0c3
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Sun Feb 19 13:49:22 2023 -0800

        AdvancedSettingsView: Remove autocomplete from anisette URL text field

    commit a42c1a705f
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Sun Feb 19 13:25:59 2023 -0800

        SettingsView: Adjust ordering a little bit and remove accent color

    commit 30efc6f210
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Sun Feb 19 13:19:26 2023 -0800

        LaunchViewController: Revert changes

    commit 60412721ee
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Sun Feb 19 13:04:42 2023 -0800

        Fix build errors

        hopefully this doesn't have any unintended side effects

    commit cba00a3b9d
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Sun Feb 19 12:03:22 2023 -0800

        Add Advanced Settings in-app

    commit 2aa880d10e
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Sun Feb 19 10:56:01 2023 -0800

        Fix build errors after merge

    commit 47848ddd18
    Merge: deac960e 3466870d
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Sun Feb 19 09:56:21 2023 -0800

        Merge branch 'fabianthdev/feature/SwiftUI' into naturecodevoid/swiftui-improvements

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

    commit deac960e10
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Sun Feb 19 09:54:56 2023 -0800

        Revert OutputCapturer changes since Fabian already added the fix

    commit 9f05123e42
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Sun Feb 19 09:16:49 2023 -0800

        AppIconView: Make isSideStore required

    commit d9a4b07095
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Sun Feb 19 09:16:07 2023 -0800

        Fix changing SideStore app icon not displaying My Apps

    commit 839699ee03
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Sun Feb 19 09:00:19 2023 -0800

        Icons: add Vista by Swifticul

    commit 81409227d6
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Sun Feb 19 08:06:33 2023 -0800

        Add developer mode

    commit 49b9be160f
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Sun Feb 19 07:57:29 2023 -0800

        AppRowView: Disable ratings if there aren't any ratings

    commit 3466870d8f
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Sun Feb 19 14:31:01 2023 +0100

        [ADD] UI for writing an app review and submit an app rating

    commit ffe8a92a4e
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Sun Feb 19 14:30:21 2023 +0100

        [CHANGE] UI fixes and SwiftUI previews for easier development

    commit bc2cae46a8
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Sun Feb 19 14:25:13 2023 +0100

        [ADD] Refresh all apps functionality in MyAppsView

    commit a95d8a502c
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Sun Feb 19 11:40:26 2023 +0100

        [FIX] STDOUT output not visible in Xcode console

    commit 19e66112dd
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Sat Feb 18 20:27:06 2023 -0800

        SourcesView: Fix 1 trusted source causing an error making all trusted sources fail to load

    commit 0d3cb843ea
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Sat Feb 18 20:26:32 2023 -0800

        SourcesViewController: Fix 1 trusted source causing an error making all trusted sources fail to load

    commit df1a662acc
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Sat Feb 18 20:25:58 2023 -0800

        FetchTrustedSourcesOperation: Remove redundant if statement

    commit 684c9e08eb
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Sat Feb 18 10:48:05 2023 -0800

        Fix HMR

    commit c585c57965
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Fri Feb 17 18:51:06 2023 -0800

        Revert fixes since it didn't actually fix the problem

    commit 3605ca6422
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Fri Feb 17 18:20:56 2023 -0800

        Fix HMR again

    commit 40f4c94f4d
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Fri Feb 17 18:11:25 2023 -0800

        Fix HMR crashing the app

    commit 986465d8f4
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Fri Feb 17 17:44:56 2023 -0800

        Project: Add HMR

        https://github.com/krzysztofzablocki/Inject#individual-developer-setup-once-per-machine

    commit 09db1ba9fc
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Thu Feb 16 18:13:32 2023 -0800

        SettingsView: Move App Icon to a new, general settings section

    commit 8874480b8c
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Thu Feb 16 17:57:51 2023 -0800

        Icons: invert Sky

    commit f0cc4613da
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Thu Feb 16 17:57:19 2023 -0800

        AppIconsView: Add artists

    commit bec78322a4
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Wed Feb 15 21:00:28 2023 -0800

        actions: Add build step that changes default icon

    commit 03777fd2e7
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Wed Feb 15 20:49:07 2023 -0800

        Icons: add Sky, Honeydew, Midnight

    commit 96ae60a9f2
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Wed Feb 15 19:36:10 2023 -0800

        AppIconsView: improve the way primary icons are handled

    commit c7ad6b10a1
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Wed Feb 15 19:35:57 2023 -0800

        Icons: reduce image sizes

    commit 8b8e471c97
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Wed Feb 15 18:52:42 2023 -0800

        Add App Icon changer

    commit 38c0a8a9a3
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Tue Feb 14 08:24:49 2023 -0800

        Fix ConnectAppleIDView being shoved into a sidebar on iPad

    commit e7ff6496c1
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Tue Feb 14 08:20:16 2023 -0800

        AuthenticationOperation: fix 2FA code not being displayed

        Bandaid fix, it would be better to have the alert in ConnectAppleIDView

    commit c2e89b09ea
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Mon Feb 13 21:44:48 2023 -0800

        RootView: Fix UI being shoved into sidebar on iPad (closes #264, thanks @Swifticul!)

    commit ec4dbb6679
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Mon Feb 13 21:06:59 2023 -0800

        OutputCapturer: fix logging disappearing from Xcode/idevicedebug run

    commit d80c9ba2a8
    Author: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
    Date:   Mon Feb 13 21:06:17 2023 -0800

        remove unused apps.json files

    commit b2f81bf7c6
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Mon Feb 13 18:56:34 2023 +0100

        [ADD] LocalConsole showing STDOUT and STDERR

    commit 2fffa6e122
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Sat Feb 4 14:35:58 2023 +0100

        [FIX] App compatibility info

    commit 723c8e9539
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Sat Feb 4 14:29:02 2023 +0100

        [ADD] Debug entries for refresh attempts, sending feedback, advanced settings, and resetting the pairing file

    commit 07159b0ea6
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Sat Feb 4 13:07:04 2023 +0100

        [ADD] Error log view

    commit e0bd54389c
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Sat Feb 4 12:55:25 2023 +0100

        [FIX] Various UI issues

    commit 57213fbf0c
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Sat Feb 4 12:46:43 2023 +0100

        [ADD] App report button and trusted source badge in app detail view

    commit 0239dfcd6d
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Fri Feb 3 18:19:07 2023 +0100

        [FIX] AppIDsView and authentication workflow

    commit 5af6f825ee
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Fri Feb 3 18:16:48 2023 +0100

        [FIX] Full screen app screenshot previews

    commit b4859512ab
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Fri Feb 3 14:58:06 2023 +0100

        [FIX] Accent color

    commit 3d0f385af7
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Tue Jan 31 22:38:42 2023 +0100

        [CHANGE] Overhaul of the AppDetailView with version history, reviews & ratings, and app information

    commit f3e58e1485
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Tue Jan 31 22:37:37 2023 +0100

        [UPDATE] AppPillButton dimensions and expiration text

    commit d3e04c1db7
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Tue Jan 31 22:35:09 2023 +0100

        [FIX] Show App IDs button only if user is logged in with their Apple ID

    commit ed1970245a
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Tue Jan 31 22:32:11 2023 +0100

        [ADD] Load and show trusted sources with option to add them to the app

    commit 15dd885a1b
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Tue Jan 31 22:30:21 2023 +0100

        [ADD] Credits section in SettingsView

    commit 4663c01700
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Mon Jan 16 21:23:16 2023 +0100

        [CHANGE] Extracted all strings into the Localizable.strings

    commit e733601c66
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Mon Jan 16 19:03:33 2023 +0100

        [FIX] Text alignment in SettingsView

    commit fc974a8079
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Mon Jan 16 19:02:58 2023 +0100

        [ADD] Hint for new users who don't have any sources

    commit 6aaadc79e5
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Mon Jan 16 18:59:39 2023 +0100

        [ADD] AppScreenshot view with ImageProcessor to automatically rotate landscape screenshots

    commit b9177e89c6
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Fri Jan 13 13:37:38 2023 +0100

        [FIX] Issues introduced by changes to the AltSource specification.

    commit 1531c0a77f
    Author: Fabian Thies <github@fabian-thies.de>
    Date:   Fri Jan 13 12:48:27 2023 +0100

        [UPDATE] Translations (#7)

        This PR merges all the new translations made on the SideStore weblate instance (https://translate.sidestore.io/projects/sidestore/app).

        New translations:
        - French
        - Korean

        Updated translations:
        - Spanish

        Co-authored-by: bogotesr <bogotesr@gmail.com>
        Co-authored-by: GABO1423 <35014183+GABO1423@users.noreply.github.com>
        Co-authored-by: Joss Laymon <71040782+bogotesr@users.noreply.github.com>
        Co-authored-by: mindfreakdev <shost212@gmail.com>
        Co-authored-by: Python <rjp2030@proton.me>
        Co-authored-by: Testi Cules <ervd516@gmail.com>

    commit 1dde36face
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Fri Jan 13 12:25:50 2023 +0100

        [FIX] Changes made by Xcode 14 after building the app

    commit c3c3783ba4
    Author: Upal <shost212@gmail.com>
    Date:   Mon Dec 26 19:18:33 2022 +0530

        Added Hindi Language (#5)

        * Added Hindi Language

    commit 8400af3423
    Author: mindfreakdev <shost212@gmail.com>
    Date:   Sun Dec 25 16:52:01 2022 +0530

        Added Dutch Language

    commit 243c7efc09
    Author: mindfreakdev <shost212@gmail.com>
    Date:   Sun Dec 25 12:30:42 2022 +0530

        Added Ukrainian Language

    commit 0298a0235b
    Author: mindfreakdev <shost212@gmail.com>
    Date:   Sun Dec 25 12:28:00 2022 +0530

        Added Ukrainian Language

    commit e5b2496b09
    Author: Gabriel Morazán <35014183+GABO1423@users.noreply.github.com>
    Date:   Sun Dec 25 01:08:47 2022 -0400

        Screen Crunch sucks

        Signed-off-by: Gabriel Morazán <35014183+GABO1423@users.noreply.github.com>

    commit 75c52a3af2
    Author: GABO1423 <35014183+GABO1423@users.noreply.github.com>
    Date:   Sun Dec 25 00:58:22 2022 -0400

        Spanish Translation Tweaks

    commit 2c07009b04
    Author: bogotesr <bogotesr@gmail.com>
    Date:   Sat Dec 24 21:06:28 2022 -0700

        Add es-419 and finish adding support for the translations

        Added Latin American Spanish (probably not the best translation)

        Made everything reference the swiftgen stuff rather than having strings

    commit 6257fdcd61
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Thu Dec 22 10:29:57 2022 +0100

        [CHANGE] Extracted some example strings and replaced them by generated localized strings

    commit e23956d4ed
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Thu Dec 22 10:21:57 2022 +0100

        [ADD] SwiftGen configuration and generated files

    commit 1341de8315
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Thu Dec 22 10:10:58 2022 +0100

        [ADD] Empty Localizable.strings

    commit 77f5844e4d
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Fri Jan 13 12:04:10 2023 +0100

        [WIP] AppScreenshot view with ImageProcessor to automatically rotate landscape images. Possible through my fork of the AsyncImage framework.

    commit b3c4819e8d
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Fri Jan 13 12:02:56 2023 +0100

        [WIP] Fetch trusted sources in SourcesView

    commit a6ca73f8fc
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Fri Jan 13 12:02:06 2023 +0100

        [WIP] AppIDs view in My Apps section

    commit f17d00c0bc
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Fri Jan 13 12:00:00 2023 +0100

        [ADD] Badge in AppDetailView for apps from the official source and (WIP) trusted sources

    commit 875453533b
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Fri Jan 13 11:58:25 2023 +0100

        [ADD] Hint view in MyAppsView telling the user about where to find updates in the future if no updates are available

    commit 9a7a39a58e
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Fri Jan 13 11:54:44 2023 +0100

        [FIX] App permission icon color

    commit 65db392388
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Fri Jan 13 11:51:06 2023 +0100

        [ADD] Show source name and external url domain in NewsItemView

    commit 6a6fc22995
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Fri Dec 23 16:02:57 2022 +0100

        [ADD] Full-screen app screenshot preview

    commit 5697c4c063
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Fri Dec 23 15:21:16 2022 +0100

        [CHANGE] Replace system image name strings with SFSymbols

    commit bcd54067d3
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Fri Dec 23 13:12:39 2022 +0100

        [ADD] Dependency: SFSafeSymbols

    commit c7ce32a562
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Wed Dec 21 17:49:49 2022 +0100

        [ADD] WIP: Promoted category cards and app list filter button in BrowseView

    commit 5a1496a3cd
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Wed Dec 21 17:48:45 2022 +0100

        [FIX] AccentColor in dark mode

    commit 497c048240
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Wed Dec 21 17:48:23 2022 +0100

        [ADD] Carousel for SideStore-specific announcements in NewsView

    commit 02e48a207f
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Wed Dec 21 17:45:44 2022 +0100

        [ADD] WIP: Add My Apps view with support for sideloading new apps, refreshing installed apps and much more

    commit a0eb30f98e
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Mon Dec 12 19:20:54 2022 +0100

        [CHANGE] Fixed the AppRowView background blur effect

    commit 378631e976
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Mon Dec 12 19:20:10 2022 +0100

        [ADD] Backported dismiss() environment variable to let views dismiss themselves

    commit 0e7083539d
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Mon Dec 12 19:18:57 2022 +0100

        [ADD] Search bar for BrowseView on iOS 15

    commit 0c034b61d9
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Mon Dec 12 19:16:36 2022 +0100

        [CHANGE] Fetch news when NewsView appears

    commit 89dea75b84
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Mon Dec 12 19:15:16 2022 +0100

        Improved app detail view

    commit 81ea791b63
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Mon Dec 12 19:12:38 2022 +0100

        [ADD] Authentication view for connecting SideStore to an Apple ID

    commit c81f716427
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Sun Nov 27 16:41:30 2022 +0100

        [WIP] Fixed the app permissions grid in AppDetailView

    commit eb151d74dd
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Sun Nov 27 16:17:08 2022 +0100

        [ADD] Expandable app and version description texts

    commit 0dc7af5e51
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Sun Nov 27 00:26:15 2022 +0100

        [ADD] iOS 13 compatible AsyncImage implementation with cache

    commit d3e8473f45
    Author: Fabian Thies <git@fabian-thies.de>
    Date:   Wed Nov 23 22:34:02 2022 +0100

        [ADD] News, Browse and Settings views ported to SwiftUI

        This commit contains WIP SwiftUI versions of most of the views in SideStore.

commit 38a1c7eef6
Author: Fabian Thies <git@fabian-thies.de>
Date:   Sat May 20 20:05:36 2023 +0200

    Fix rebase issues

commit f6252c3a8b
Author: Fabian Thies <git@fabian-thies.de>
Date:   Sat May 20 19:10:51 2023 +0200

    Fix trusted sources being enabled in onboarding process regardless of user choice

commit 653d80b88e
Author: Fabian Thies <git@fabian-thies.de>
Date:   Fri May 19 13:14:15 2023 +0200

    Add onboarding screens for an easy setup of SideStore

commit 89609ad35c
Author: Fabian Thies <git@fabian-thies.de>
Date:   Sun Feb 19 14:31:01 2023 +0100

    [ADD] UI for writing an app review and submit an app rating

commit 2211013e57
Author: Fabian Thies <git@fabian-thies.de>
Date:   Sun Feb 19 14:30:21 2023 +0100

    [CHANGE] UI fixes and SwiftUI previews for easier development

commit f206ee1406
Author: Fabian Thies <git@fabian-thies.de>
Date:   Sun Feb 19 14:25:13 2023 +0100

    [ADD] Refresh all apps functionality in MyAppsView

commit 00dc9b36af
Author: Fabian Thies <git@fabian-thies.de>
Date:   Sun Feb 19 11:40:26 2023 +0100

    [FIX] STDOUT output not visible in Xcode console

commit 24146cef90
Author: Fabian Thies <git@fabian-thies.de>
Date:   Mon Feb 13 18:56:34 2023 +0100

    [ADD] LocalConsole showing STDOUT and STDERR

commit c46a50ec58
Author: Fabian Thies <git@fabian-thies.de>
Date:   Sat Feb 4 14:35:58 2023 +0100

    [FIX] App compatibility info

commit de7e909c01
Author: Fabian Thies <git@fabian-thies.de>
Date:   Sat Feb 4 14:29:02 2023 +0100

    [ADD] Debug entries for refresh attempts, sending feedback, advanced settings, and resetting the pairing file

commit fbc754d8b7
Author: Fabian Thies <git@fabian-thies.de>
Date:   Sat Feb 4 13:07:04 2023 +0100

    [ADD] Error log view

commit 767d878051
Author: Fabian Thies <git@fabian-thies.de>
Date:   Sat Feb 4 12:55:25 2023 +0100

    [FIX] Various UI issues

commit 132b140af2
Author: Fabian Thies <git@fabian-thies.de>
Date:   Sat Feb 4 12:46:43 2023 +0100

    [ADD] App report button and trusted source badge in app detail view

commit df7d8871ff
Author: Fabian Thies <git@fabian-thies.de>
Date:   Fri Feb 3 18:19:07 2023 +0100

    [FIX] AppIDsView and authentication workflow

commit ca2398e4c7
Author: Fabian Thies <git@fabian-thies.de>
Date:   Fri Feb 3 18:16:48 2023 +0100

    [FIX] Full screen app screenshot previews

commit b8f02d2152
Author: Fabian Thies <git@fabian-thies.de>
Date:   Fri Feb 3 14:58:06 2023 +0100

    [FIX] Accent color

commit e85876cd24
Author: Fabian Thies <git@fabian-thies.de>
Date:   Tue Jan 31 22:38:42 2023 +0100

    [CHANGE] Overhaul of the AppDetailView with version history, reviews & ratings, and app information

commit 3f06a53058
Author: Fabian Thies <git@fabian-thies.de>
Date:   Tue Jan 31 22:37:37 2023 +0100

    [UPDATE] AppPillButton dimensions and expiration text

commit 4ee053a4f9
Author: Fabian Thies <git@fabian-thies.de>
Date:   Tue Jan 31 22:35:09 2023 +0100

    [FIX] Show App IDs button only if user is logged in with their Apple ID

commit e5369524ce
Author: Fabian Thies <git@fabian-thies.de>
Date:   Tue Jan 31 22:32:11 2023 +0100

    [ADD] Load and show trusted sources with option to add them to the app

commit 77465cebd0
Author: Fabian Thies <git@fabian-thies.de>
Date:   Tue Jan 31 22:30:21 2023 +0100

    [ADD] Credits section in SettingsView

commit f90bf3bfcf
Author: Fabian Thies <git@fabian-thies.de>
Date:   Mon Jan 16 21:23:16 2023 +0100

    [CHANGE] Extracted all strings into the Localizable.strings

commit 0000610b9d
Author: Fabian Thies <git@fabian-thies.de>
Date:   Mon Jan 16 19:03:33 2023 +0100

    [FIX] Text alignment in SettingsView

commit c7e095583d
Author: Fabian Thies <git@fabian-thies.de>
Date:   Mon Jan 16 19:02:58 2023 +0100

    [ADD] Hint for new users who don't have any sources

commit a725f3e9cc
Author: Fabian Thies <git@fabian-thies.de>
Date:   Mon Jan 16 18:59:39 2023 +0100

    [ADD] AppScreenshot view with ImageProcessor to automatically rotate landscape screenshots

commit b5dea18073
Author: Fabian Thies <git@fabian-thies.de>
Date:   Fri Jan 13 13:37:38 2023 +0100

    [FIX] Issues introduced by changes to the AltSource specification.

commit b9b309e603
Author: Fabian Thies <github@fabian-thies.de>
Date:   Fri Jan 13 12:48:27 2023 +0100

    [UPDATE] Translations (#7)

    This PR merges all the new translations made on the SideStore weblate instance (https://translate.sidestore.io/projects/sidestore/app).

    New translations:
    - French
    - Korean

    Updated translations:
    - Spanish

    Co-authored-by: bogotesr <bogotesr@gmail.com>
    Co-authored-by: GABO1423 <35014183+GABO1423@users.noreply.github.com>
    Co-authored-by: Joss Laymon <71040782+bogotesr@users.noreply.github.com>
    Co-authored-by: mindfreakdev <shost212@gmail.com>
    Co-authored-by: Python <rjp2030@proton.me>
    Co-authored-by: Testi Cules <ervd516@gmail.com>

commit 15f1be0aa8
Author: Fabian Thies <git@fabian-thies.de>
Date:   Fri Jan 13 12:25:50 2023 +0100

    [FIX] Changes made by Xcode 14 after building the app

commit ffd80ce0b4
Author: Upal <shost212@gmail.com>
Date:   Mon Dec 26 19:18:33 2022 +0530

    Added Hindi Language (#5)

    * Added Hindi Language

commit 350891ee2a
Author: mindfreakdev <shost212@gmail.com>
Date:   Sun Dec 25 16:52:01 2022 +0530

    Added Dutch Language

commit 5dec1cd561
Author: mindfreakdev <shost212@gmail.com>
Date:   Sun Dec 25 12:30:42 2022 +0530

    Added Ukrainian Language

commit c4d235d742
Author: mindfreakdev <shost212@gmail.com>
Date:   Sun Dec 25 12:28:00 2022 +0530

    Added Ukrainian Language

commit cdc6675dd5
Author: Gabriel Morazán <35014183+GABO1423@users.noreply.github.com>
Date:   Sun Dec 25 01:08:47 2022 -0400

    Screen Crunch sucks

    Signed-off-by: Gabriel Morazán <35014183+GABO1423@users.noreply.github.com>

commit 85635bb26e
Author: GABO1423 <35014183+GABO1423@users.noreply.github.com>
Date:   Sun Dec 25 00:58:22 2022 -0400

    Spanish Translation Tweaks

commit 3be0a4a89c
Author: bogotesr <bogotesr@gmail.com>
Date:   Sat Dec 24 21:06:28 2022 -0700

    Add es-419 and finish adding support for the translations

    Added Latin American Spanish (probably not the best translation)

    Made everything reference the swiftgen stuff rather than having strings

commit 47e47fb3cf
Author: Fabian Thies <git@fabian-thies.de>
Date:   Thu Dec 22 10:29:57 2022 +0100

    [CHANGE] Extracted some example strings and replaced them by generated localized strings

commit 48903034b6
Author: Fabian Thies <git@fabian-thies.de>
Date:   Thu Dec 22 10:21:57 2022 +0100

    [ADD] SwiftGen configuration and generated files

commit 6952218ee7
Author: Fabian Thies <git@fabian-thies.de>
Date:   Thu Dec 22 10:10:58 2022 +0100

    [ADD] Empty Localizable.strings

commit 80146c1e03
Author: Fabian Thies <git@fabian-thies.de>
Date:   Fri Jan 13 12:04:10 2023 +0100

    [WIP] AppScreenshot view with ImageProcessor to automatically rotate landscape images. Possible through my fork of the AsyncImage framework.

commit 642ae996c9
Author: Fabian Thies <git@fabian-thies.de>
Date:   Fri Jan 13 12:02:56 2023 +0100

    [WIP] Fetch trusted sources in SourcesView

commit 8040636aa5
Author: Fabian Thies <git@fabian-thies.de>
Date:   Fri Jan 13 12:02:06 2023 +0100

    [WIP] AppIDs view in My Apps section

commit 731fcfaca7
Author: Fabian Thies <git@fabian-thies.de>
Date:   Fri Jan 13 12:00:00 2023 +0100

    [ADD] Badge in AppDetailView for apps from the official source and (WIP) trusted sources

commit 708fb3fccd
Author: Fabian Thies <git@fabian-thies.de>
Date:   Fri Jan 13 11:58:25 2023 +0100

    [ADD] Hint view in MyAppsView telling the user about where to find updates in the future if no updates are available

commit 9f429fb068
Author: Fabian Thies <git@fabian-thies.de>
Date:   Fri Jan 13 11:54:44 2023 +0100

    [FIX] App permission icon color

commit 29fc693f4d
Author: Fabian Thies <git@fabian-thies.de>
Date:   Fri Jan 13 11:51:06 2023 +0100

    [ADD] Show source name and external url domain in NewsItemView

commit 6f373ad305
Author: Fabian Thies <git@fabian-thies.de>
Date:   Fri Dec 23 16:02:57 2022 +0100

    [ADD] Full-screen app screenshot preview

commit c069d779d9
Author: Fabian Thies <git@fabian-thies.de>
Date:   Fri Dec 23 15:21:16 2022 +0100

    [CHANGE] Replace system image name strings with SFSymbols

commit cd88970a22
Author: Fabian Thies <git@fabian-thies.de>
Date:   Fri Dec 23 13:12:39 2022 +0100

    [ADD] Dependency: SFSafeSymbols

commit 6b6708e43c
Author: Fabian Thies <git@fabian-thies.de>
Date:   Wed Dec 21 17:49:49 2022 +0100

    [ADD] WIP: Promoted category cards and app list filter button in BrowseView

commit 9206eeb9e3
Author: Fabian Thies <git@fabian-thies.de>
Date:   Wed Dec 21 17:48:45 2022 +0100

    [FIX] AccentColor in dark mode

commit 080bbb3c51
Author: Fabian Thies <git@fabian-thies.de>
Date:   Wed Dec 21 17:48:23 2022 +0100

    [ADD] Carousel for SideStore-specific announcements in NewsView

commit ea2c862900
Author: Fabian Thies <git@fabian-thies.de>
Date:   Wed Dec 21 17:45:44 2022 +0100

    [ADD] WIP: Add My Apps view with support for sideloading new apps, refreshing installed apps and much more

commit 4fe72ea113
Author: Fabian Thies <git@fabian-thies.de>
Date:   Mon Dec 12 19:20:54 2022 +0100

    [CHANGE] Fixed the AppRowView background blur effect

commit c486a62b50
Author: Fabian Thies <git@fabian-thies.de>
Date:   Mon Dec 12 19:20:10 2022 +0100

    [ADD] Backported dismiss() environment variable to let views dismiss themselves

commit 3ce4451da4
Author: Fabian Thies <git@fabian-thies.de>
Date:   Mon Dec 12 19:18:57 2022 +0100

    [ADD] Search bar for BrowseView on iOS 15

commit 294ba12391
Author: Fabian Thies <git@fabian-thies.de>
Date:   Mon Dec 12 19:16:36 2022 +0100

    [CHANGE] Fetch news when NewsView appears

commit 4a3343fe61
Author: Fabian Thies <git@fabian-thies.de>
Date:   Mon Dec 12 19:15:16 2022 +0100

    Improved app detail view

commit d1e6ddd435
Author: Fabian Thies <git@fabian-thies.de>
Date:   Mon Dec 12 19:12:38 2022 +0100

    [ADD] Authentication view for connecting SideStore to an Apple ID

commit 3e0379dc70
Author: Fabian Thies <git@fabian-thies.de>
Date:   Sun Nov 27 16:41:30 2022 +0100

    [WIP] Fixed the app permissions grid in AppDetailView

commit d99674f8bd
Author: Fabian Thies <git@fabian-thies.de>
Date:   Sun Nov 27 16:17:08 2022 +0100

    [ADD] Expandable app and version description texts

commit ca7acc17da
Author: Fabian Thies <git@fabian-thies.de>
Date:   Sun Nov 27 00:26:15 2022 +0100

    [ADD] iOS 13 compatible AsyncImage implementation with cache

commit 16a8bce102
Author: Fabian Thies <git@fabian-thies.de>
Date:   Wed Nov 23 22:34:02 2022 +0100

    [ADD] News, Browse and Settings views ported to SwiftUI

    This commit contains WIP SwiftUI versions of most of the views in SideStore.
2023-05-20 11:31:25 -07:00
naturecodevoid
093e21799f fix: crash if save is called on non-unstable build 2023-05-20 10:48:52 -07:00
naturecodevoid
ad98ce43a9 fix warning 2023-05-20 10:47:55 -07:00
naturecodevoid
7f39d010b2 feat: merge #282 as an unstable feature 2023-05-20 09:51:45 -07:00
naturecodevoid
b6c9797104 Unstable Features groundwork 2023-05-20 09:24:09 -07:00
686 changed files with 21738 additions and 43561 deletions

2
.github/CODEOWNERS vendored
View File

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

View File

@@ -2,15 +2,15 @@ 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: |
## Please note that the issue tracker is not for support
Thanks for taking the time to fill out this bug report! Before you continue filling out the report, please **[search in GitHub Issues](https://github.com/SideStore/SideStore/issues?q=is%3Aissue+is%3Aopen) for the bug you are experiencing** in case it has already been reported. Thanks for taking the time to fill out this bug report! Before you continue filling out the report, please **[search in GitHub Issues](https://github.com/SideStore/SideStore/issues?q=is%3Aissue+is%3Aopen) for the bug you are experiencing** in case it has already been reported.
**Please use [Discord](https://discord.gg/sidestore-949183273383395328) or [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) for support.** **Please use [Discord](https://discord.gg/RgpFBX3Q3k) 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/sidestore-949183273383395328 url: https://discord.gg/RgpFBX3Q3k
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,14 +2,15 @@ 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/sidestore-949183273383395328) or [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) for support.** **Please use [Discord](https://discord.gg/RgpFBX3Q3k) or [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) for support.**
- type: textarea - type: textarea
id: description id: description
attributes: attributes:

View File

@@ -1,63 +0,0 @@
import requests
import sys
import os
# Your GitHub Personal Access Token
GITHUB_TOKEN = os.getenv("GITHUB_TOKEN")
# Repository details
REPO_OWNER = "SideStore"
REPO_NAME = "SideStore"
API_URL = f"https://api.github.com/repos/{REPO_OWNER}/{REPO_NAME}/actions/caches"
# Common headers for GitHub API calls
HEADERS = {
"Accept": "application/vnd.github+json",
"Authorization": f"Bearer {GITHUB_TOKEN}"
}
def list_caches():
response = requests.get(API_URL, headers=HEADERS)
if response.status_code != 200:
print(f"Failed to list caches. HTTP {response.status_code}")
print("Response:", response.text)
sys.exit(1)
data = response.json()
return data.get("actions_caches", [])
def delete_cache(cache_id):
delete_url = f"{API_URL}/{cache_id}"
response = requests.delete(delete_url, headers=HEADERS)
return response.status_code
def main():
caches = list_caches()
if not caches:
print("No caches found.")
return
print("Found caches:")
for cache in caches:
print(f"ID: {cache.get('id')}, Key: {cache.get('key')}")
print("\nDeleting caches...")
for cache in caches:
cache_id = cache.get("id")
status = delete_cache(cache_id)
if status == 204:
print(f"Successfully deleted cache with ID: {cache_id}")
else:
print(f"Failed to delete cache with ID: {cache_id}. HTTP status code: {status}")
print("All caches processed.")
if __name__ == "__main__":
main()
### How to use
'''
just export the GITHUB_TOKEN and then run this script via `python3 cache.py' to delete the caches
'''

View File

@@ -10,3 +10,6 @@
<!-- Example: --> <!-- Example: -->
- [x] Finish UI changes - [x] Finish UI changes
- [ ] Test - [ ] Test
<!-- If your PR doesn't close an issue, you can remove the next line. -->
Closes #1234

View File

@@ -1,28 +0,0 @@
name: Alpha SideStore build
on:
push:
branches:
- develop-alpha
# cancel duplicate run if from same branch
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
jobs:
Reusable-build:
uses: ./.github/workflows/reusable-sidestore-build.yml
with:
# bundle_id: "com.SideStore.SideStore.Alpha"
bundle_id: "com.SideStore.SideStore"
# bundle_id_suffix: ".Alpha"
is_beta: true
publish: ${{ vars.PUBLISH_ALPHA_UPDATES == 'true' }}
is_shared_build_num: false
release_tag: "alpha"
release_name: "Alpha"
upstream_tag: "nightly"
upstream_name: "Nightly"
secrets:
CROSS_REPO_PUSH_KEY: ${{ secrets.CROSS_REPO_PUSH_KEY }}
BUILD_LOG_ZIP_PASSWORD: ${{ secrets.BUILD_LOG_ZIP_PASSWORD }}

View File

@@ -20,58 +20,3 @@ jobs:
format: name format: name
addTo: pull addTo: pull
# addTo: pullandissues # addTo: pullandissues
nightly-link-comment:
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v6
with:
# This snippet is public-domain, taken from
# https://github.com/oprypin/nightly.link/blob/master/.github/workflows/pr-comment.yml
script: |
async function upsertComment(owner, repo, issue_number, purpose, body) {
const {data: comments} = await github.rest.issues.listComments(
{owner, repo, issue_number});
const marker = `<!-- bot: ${purpose} -->`;
body = marker + "\n" + body;
const existing = comments.filter((c) => c.body.includes(marker));
if (existing.length > 0) {
const last = existing[existing.length - 1];
core.info(`Updating comment ${last.id}`);
await github.rest.issues.updateComment({
owner, repo,
body,
comment_id: last.id,
});
} else {
core.info(`Creating a comment in issue / PR #${issue_number}`);
await github.rest.issues.createComment({issue_number, body, owner, repo});
}
}
const {owner, repo} = context.repo;
const run_id = ${{github.event.workflow_run.id}};
const pull_requests = ${{ toJSON(github.event.workflow_run.pull_requests) }};
if (!pull_requests.length) {
return core.error("This workflow doesn't match any pull requests!");
}
const artifacts = await github.paginate(
github.rest.actions.listWorkflowRunArtifacts, {owner, repo, run_id});
if (!artifacts.length) {
return core.error(`No artifacts found`);
}
let body = `Download the artifacts for this pull request (nightly.link):\n`;
for (const art of artifacts) {
body += `\n* [${art.name}.zip](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`;
}
core.info("Review thread message body:", body);
for (const pr of pull_requests) {
await upsertComment(owner, repo, pr.number,
"nightly-link", body);
}

View File

@@ -11,13 +11,13 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- os: 'macos-14' - os: 'macos-12'
version: '15.4' version: '14.2'
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v2
with: with:
submodules: recursive submodules: recursive
@@ -27,6 +27,9 @@ jobs:
- 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: Change default icon to beta icon
run: sed -e 's/= Neon/= Starburst/' -i '' ./AltStore.xcodeproj/project.pbxproj
- name: Get version - name: Get version
id: version id: version
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
@@ -35,25 +38,25 @@ jobs:
run: echo "${{ steps.version.outputs.version }}" run: echo "${{ steps.version.outputs.version }}"
- name: Setup Xcode - name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1 uses: maxim-lobanov/setup-xcode@v1.4.1
with: with:
xcode-version: ${{ matrix.version }} xcode-version: ${{ matrix.version }}
- name: "[Normal] Build SideStore, fakesign app and convert to IPA"
- name: Cache Build run: |
uses: irgaly/xcode-cache@v1 make build | xcpretty
with: make fakesign
key: xcode-cache-deriveddata-${{ github.sha }} make ipa
restore-keys: xcode-cache-deriveddata
- name: Build SideStore - name: Enable MDC
run: make build | xcpretty && exit ${PIPESTATUS[0]} run: make enable_mdc
- name: Fakesign app - name: "[MDC] Build SideStore, fakesign app and convert to IPA"
run: make fakesign run: |
make clean
- name: Convert to IPA make build DSYM_FOLDER=./MDC-dSYM | xcpretty
run: make ipa make fakesign
make ipa IPA_NAME=SideStore-MDC.ipa
- name: Get current date - name: Get current date
id: date id: date
@@ -71,7 +74,9 @@ jobs:
tag_name: ${{ github.ref_name }} tag_name: ${{ github.ref_name }}
draft: true draft: true
prerelease: true prerelease: true
files: SideStore.ipa files: |
SideStore.ipa
SideStore-MDC.ipa
body: | 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. --> <!-- 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!** 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!**
@@ -90,14 +95,29 @@ jobs:
- name: Add version to IPA file name - name: Add version to IPA file name
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
- name: Add version to MDC IPA file name
run: mv SideStore-MDC.ipa SideStore-MDC-${{ steps.version.outputs.version }}.ipa
- name: Upload SideStore.ipa Artifact - name: Upload SideStore.ipa Artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3.1.0
with: with:
name: SideStore-${{ steps.version.outputs.version }}.ipa name: SideStore-${{ steps.version.outputs.version }}.ipa
path: SideStore-${{ steps.version.outputs.version }}.ipa path: SideStore-${{ steps.version.outputs.version }}.ipa
- name: Upload *.dSYM Artifact - name: Upload SideStore-MDC.ipa Artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-MDC-${{ steps.version.outputs.version }}.ipa
path: SideStore-MDC-${{ steps.version.outputs.version }}.ipa
- name: Upload dSYM Artifact
uses: actions/upload-artifact@v3.1.0
with: with:
name: SideStore-${{ steps.version.outputs.version }}-dSYM name: SideStore-${{ steps.version.outputs.version }}-dSYM
path: ./*.dSYM/ path: ./dSYM/*
- name: Upload MDC-dSYM Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-MDC-${{ steps.version.outputs.version }}-dSYM
path: ./MDC-dSYM/*

View File

@@ -1,34 +0,0 @@
#!/usr/bin/env bash
# Ensure we are in root directory
cd "$(dirname "$0")/../.."
DATE=`date -u +'%Y.%m.%d'`
BUILD_NUM=1
# Use RELEASE_CHANNEL from the environment variable or default to "beta"
RELEASE_CHANNEL=${RELEASE_CHANNEL:-"beta"}
write() {
sed -e "/MARKETING_VERSION = .*/s/$/-$RELEASE_CHANNEL.$DATE.$BUILD_NUM+$(git rev-parse --short HEAD)/" -i '' Build.xcconfig
echo "$DATE,$BUILD_NUM" > build_number.txt
}
if [ ! -f "build_number.txt" ]; then
write
exit 0
fi
LAST_DATE=`cat build_number.txt | perl -n -e '/([^,]*),([^ ]*)$/ && print $1'`
LAST_BUILD_NUM=`cat build_number.txt | perl -n -e '/([^,]*),([^ ]*)$/ && print $2'`
# if [[ "$DATE" != "$LAST_DATE" ]]; then
# write
# else
# BUILD_NUM=`expr $LAST_BUILD_NUM + 1`
# write
# fi
# Build number is always incremental
BUILD_NUM=`expr $LAST_BUILD_NUM + 1`
write

View File

@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# Ensure we are in root directory
cd "$(dirname "$0")/../.."
DATE=`date -u +'%Y.%m.%d'`
BUILD_NUM=1
write() {
sed -e "/MARKETING_VERSION = .*/s/$/-nightly.$DATE.$BUILD_NUM+$(git rev-parse --short HEAD)/" -i '' Build.xcconfig
echo "$DATE,$BUILD_NUM" > .nightly-build-num
}
if [ ! -f ".nightly-build-num" ]; then
write
exit 0
fi
LAST_DATE=`cat .nightly-build-num | perl -n -e '/([^,]*),([^ ]*)$/ && print $1'`
LAST_BUILD_NUM=`cat .nightly-build-num | perl -n -e '/([^,]*),([^ ]*)$/ && print $2'`
if [[ "$DATE" != "$LAST_DATE" ]]; then
write
else
BUILD_NUM=`expr $LAST_BUILD_NUM + 1`
write
fi

View File

@@ -1,82 +1,136 @@
name: Nightly SideStore Build name: Nightly SideStore build
on: on:
push: push:
branches: branches:
- develop - develop
schedule:
- cron: '0 0 * * *' # Runs every night at midnight UTC
workflow_dispatch: # Allows manual trigger
# cancel duplicate run if from same branch
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
check-changes: build:
if: github.event_name == 'schedule' name: Build and upload SideStore Nightly
runs-on: ubuntu-latest concurrency:
outputs: group: ${{ github.ref }}
has_changes: ${{ steps.check.outputs.has_changes }} cancel-in-progress: true
strategy:
fail-fast: false
matrix:
include:
- os: 'macos-12'
version: '14.2'
runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout repository - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v2
with: with:
fetch-depth: 0 # Ensure full history submodules: recursive
- name: Get last successful workflow run - name: Install dependencies
id: get_last_success run: brew install ldid
- name: Cache .nightly-build-num
uses: actions/cache@v3
with:
path: .nightly-build-num
key: nightly-build-num
- name: Increase nightly build number and set as version
run: bash .github/workflows/increase-nightly-build-num.sh
- name: Change default icon to nightly icon
run: sed -e 's/= Neon/= Steel/' -i '' ./AltStore.xcodeproj/project.pbxproj
- name: Enable unstable features
run: make enable_unstable
- name: Get version
id: version
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
- name: Echo version
run: echo "${{ steps.version.outputs.version }}"
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.4.1
with:
xcode-version: ${{ matrix.version }}
- name: "[Normal] Build SideStore, fakesign app and convert to IPA"
run: | run: |
LAST_SUCCESS=$(gh run list --workflow "Nightly SideStore Build" --json createdAt,conclusion \ make build | xcpretty
--jq '[.[] | select(.conclusion=="success")][0].createdAt' || echo "") make fakesign
echo "Last successful run: $LAST_SUCCESS" make ipa
echo "last_success=$LAST_SUCCESS" >> $GITHUB_ENV
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Check for new commits since last successful build - name: Enable MDC
id: check run: make enable_mdc
- name: "[MDC] Build SideStore, fakesign app and convert to IPA"
run: | run: |
if [ -n "$LAST_SUCCESS" ]; then make clean
NEW_COMMITS=$(git rev-list --count --since="$LAST_SUCCESS" origin/develop) make build DSYM_FOLDER=./MDC-dSYM | xcpretty
COMMIT_LOG=$(git log --since="$LAST_SUCCESS" --pretty=format:"%h %s" origin/develop) make fakesign
else make ipa IPA_NAME=SideStore-MDC.ipa
NEW_COMMITS=1
COMMIT_LOG=$(git log -n 10 --pretty=format:"%h %s" origin/develop) # Show last 10 commits if no history
fi
echo "Has changes: $NEW_COMMITS"
echo "New commits since last successful build:"
echo "$COMMIT_LOG"
if [ "$NEW_COMMITS" -gt 0 ]; then
echo "has_changes=true" >> $GITHUB_OUTPUT
else
echo "has_changes=false" >> $GITHUB_OUTPUT
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
LAST_SUCCESS: ${{ env.last_success }}
Reusable-build: - name: Get current date
if: | id: date
always() && run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
(github.event_name == 'push' ||
(github.event_name == 'schedule' && needs.check-changes.result == 'success' && needs.check-changes.outputs.has_changes == 'true'))
needs: check-changes
uses: ./.github/workflows/reusable-sidestore-build.yml
with:
# bundle_id: "com.SideStore.SideStore.Nightly"
bundle_id: "com.SideStore.SideStore"
# bundle_id_suffix: ".Nightly"
is_beta: true
publish: ${{ vars.PUBLISH_NIGHTLY_UPDATES == 'true' }}
is_shared_build_num: false
release_tag: "nightly"
release_name: "Nightly"
upstream_tag: "0.5.10"
upstream_name: "Stable"
secrets:
CROSS_REPO_PUSH_KEY: ${{ secrets.CROSS_REPO_PUSH_KEY }}
BUILD_LOG_ZIP_PASSWORD: ${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
- 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
SideStore-MDC.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: Add version to MDC IPA file name
run: mv SideStore-MDC.ipa SideStore-MDC-${{ steps.version.outputs.version }}.ipa
- name: Upload SideStore.ipa Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-${{ steps.version.outputs.version }}.ipa
path: SideStore-${{ steps.version.outputs.version }}.ipa
- name: Upload SideStore-MDC.ipa Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-MDC-${{ steps.version.outputs.version }}.ipa
path: SideStore-MDC-${{ steps.version.outputs.version }}.ipa
- name: Upload dSYM Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-${{ steps.version.outputs.version }}-dSYM
path: ./dSYM/*
- name: Upload MDC-dSYM Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-MDC-${{ steps.version.outputs.version }}-dSYM
path: ./MDC-dSYM/*
- name: Reset cache for apps.sidestore.io/nightly
run: sleep 10 && curl https://apps.sidestore.io/reset-cache/nightly/${{ secrets.SIDESOURCE_KEY }}

View File

@@ -1,38 +1,38 @@
name: Pull Request SideStore build name: Pull Request SideStore build
on: on:
pull_request: pull_request:
# types: [opened, synchronize, reopened, ready_for_review, converted_to_draft]
types: [opened, synchronize, reopened, ready_for_review]
jobs: jobs:
build: build:
name: Build and upload SideStore name: Build and upload SideStore
if: ${{ github.event.pull_request.draft == false }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- os: 'macos-14' - os: 'macos-12'
version: '16.1' version: '14.2'
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 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: Install xcbeautify
run: brew install xcbeautify
- name: Add PR suffix to version - name: Add PR suffix to version
run: sed -e "/MARKETING_VERSION = .*/s/\$/-pr.${{ github.event.pull_request.number }}+$(git rev-parse --short ${COMMIT:-HEAD})/" -i '' Build.xcconfig run: sed -e "/MARKETING_VERSION = .*/s/\$/-pr.${{ github.event.pull_request.number }}+$(git rev-parse --short ${COMMIT:-HEAD})/" -i '' Build.xcconfig
env: env:
COMMIT: ${{ github.event.pull_request.head.sha }} COMMIT: ${{ github.event.pull_request.head.sha }}
- name: Change default icon to alpha icon
run: sed -e 's/= Neon/= Storm/' -i '' ./AltStore.xcodeproj/project.pbxproj
- name: Enable unstable features
run: make enable_unstable
- name: Get version - name: Get version
id: version id: version
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
@@ -41,58 +41,52 @@ jobs:
run: echo "${{ steps.version.outputs.version }}" run: echo "${{ steps.version.outputs.version }}"
- name: Setup Xcode - name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.6.0 uses: maxim-lobanov/setup-xcode@v1.4.1
with: with:
xcode-version: ${{ matrix.version }} xcode-version: ${{ matrix.version }}
- name: Cache Build - name: "[Normal] Build SideStore, fakesign app and convert to IPA"
uses: irgaly/xcode-cache@v1
with:
key: xcode-cache-deriveddata-${{ github.sha }}
restore-keys: xcode-cache-deriveddata-
swiftpm-cache-key: xcode-cache-sourcedata-${{ github.sha }}
swiftpm-cache-restore-keys: |
xcode-cache-sourcedata-
- name: List Files and derived data
run: | run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<" make build | xcpretty
ls -la . make fakesign
echo "" make ipa
echo ">>>>>>>>> SideStore <<<<<<<<<<"
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Dependencies <<<<<<<<<<"
find Dependencies -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
echo ""
- name: Enable MDC
- name: Build SideStore run: make enable_mdc
run: NSUnbufferedIO=YES make build 2>&1 | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
- name: Fakesign app - name: "[MDC] Build SideStore, fakesign app and convert to IPA"
run: make fakesign run: |
make clean
- name: Convert to IPA make build DSYM_FOLDER=./MDC-dSYM | xcpretty
run: make ipa make fakesign
make ipa IPA_NAME=SideStore-MDC.ipa
- name: Add version to IPA file name - name: Add version to IPA file name
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
- name: Add version to MDC IPA file name
run: mv SideStore-MDC.ipa SideStore-MDC-${{ steps.version.outputs.version }}.ipa
- name: Upload SideStore.ipa Artifact - name: Upload SideStore.ipa Artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3.1.0
with: with:
name: SideStore-${{ steps.version.outputs.version }}.ipa name: SideStore-${{ steps.version.outputs.version }}.ipa
path: SideStore-${{ steps.version.outputs.version }}.ipa path: SideStore-${{ steps.version.outputs.version }}.ipa
- name: Upload *.dSYM Artifact - name: Upload SideStore-MDC.ipa Artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-MDC-${{ steps.version.outputs.version }}.ipa
path: SideStore-MDC-${{ steps.version.outputs.version }}.ipa
- name: Upload dSYM Artifact
uses: actions/upload-artifact@v3.1.0
with: with:
name: SideStore-${{ steps.version.outputs.version }}-dSYM name: SideStore-${{ steps.version.outputs.version }}-dSYM
path: ./SideStore.xcarchive/dSYMs/* path: ./dSYM/*
- name: Upload MDC-dSYM Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-MDC-${{ steps.version.outputs.version }}-dSYM
path: ./MDC-dSYM/*

View File

@@ -1,104 +0,0 @@
name: Reusable SideStore Build
on:
workflow_call:
inputs:
is_beta:
required: false
default: false
type: boolean
publish:
required: false
default: false
type: boolean
is_shared_build_num:
required: false
default: true
type: boolean
release_name:
required: true
type: string
release_tag:
required: true
type: string
upstream_tag:
required: true
type: string
upstream_name:
required: true
type: string
bundle_id:
default: com.SideStore.SideStore
required: true
type: string
bundle_id_suffix:
default: ''
required: false
type: string
secrets:
# GITHUB_TOKEN:
# required: true
CROSS_REPO_PUSH_KEY:
required: true
BUILD_LOG_ZIP_PASSWORD:
required: false
# since build cache, test-build cache, test-run cache are involved, out of order exec if serialization is on individual jobs will wreak all sorts of havoc
# so we serialize on the entire workflow
concurrency:
group: serialize-workflow
jobs:
shared:
uses: ./.github/workflows/sidestore-shared.yml
secrets: inherit
build:
needs: shared
uses: ./.github/workflows/sidestore-build.yml
with:
is_beta: ${{ inputs.is_beta }}
is_shared_build_num: ${{ inputs.is_shared_build_num }}
release_tag: ${{ inputs.release_tag }}
short_commit: ${{ needs.shared.outputs.short-commit }}
bundle_id: ${{ inputs.bundle_id }}
bundle_id_suffix: ${{ inputs.bundle_id_suffix }}
secrets: inherit
tests-build:
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_BUILD == '1' }}
needs: shared
uses: ./.github/workflows/sidestore-tests-build.yml
with:
release_tag: ${{ inputs.release_tag }}
short_commit: ${{ needs.shared.outputs.short-commit }}
secrets: inherit
tests-run:
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
needs: [shared, tests-build]
uses: ./.github/workflows/sidestore-tests-run.yml
with:
release_tag: ${{ inputs.release_tag }}
short_commit: ${{ needs.shared.outputs.short-commit }}
secrets: inherit
deploy:
needs: [shared, build, tests-build, tests-run] # Keep tests-run in needs
if: ${{ always() && (needs.tests-run.result == 'skipped' || needs.tests-run.result == 'success') }}
uses: ./.github/workflows/sidestore-deploy.yml
with:
is_beta: ${{ inputs.is_beta }}
publish: ${{ inputs.publish }}
release_name: ${{ inputs.release_name }}
release_tag: ${{ inputs.release_tag }}
upstream_tag: ${{ inputs.upstream_tag }}
upstream_name: ${{ inputs.upstream_name }}
version: ${{ needs.build.outputs.version }}
short_commit: ${{ needs.shared.outputs.short-commit }}
release_channel: ${{ needs.build.outputs.release-channel }}
marketing_version: ${{ needs.build.outputs.marketing-version }}
bundle_id: ${{ inputs.bundle_id }}
secrets: inherit

View File

@@ -1,358 +0,0 @@
name: SideStore Build
on:
workflow_call:
inputs:
is_beta:
type: boolean
is_shared_build_num:
type: boolean
release_tag:
type: string
bundle_id:
type: string
bundle_id_suffix:
type: string
short_commit:
type: string
secrets:
CROSS_REPO_PUSH_KEY:
required: true
BUILD_LOG_ZIP_PASSWORD:
required: false
outputs:
version:
value: ${{ jobs.build.outputs.version }}
marketing-version:
value: ${{ jobs.build.outputs.marketing-version }}
release-channel:
value: ${{ jobs.build.outputs.release-channel }}
jobs:
build:
name: Build SideStore - ${{ inputs.release_tag }}
strategy:
fail-fast: false
matrix:
include:
- os: 'macos-26'
version: '26.0'
runs-on: ${{ matrix.os }}
outputs:
version: ${{ steps.version.outputs.version }}
marketing-version: ${{ steps.marketing-version.outputs.MARKETING_VERSION }}
release-channel: ${{ steps.release-channel.outputs.RELEASE_CHANNEL }}
steps:
- name: Set beta status
run: echo "IS_BETA=${{ inputs.is_beta }}" >> $GITHUB_ENV
shell: bash
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
- name: Install dependencies - ldid & xcbeautify
run: |
brew install ldid xcbeautify
- name: Set ref based on is_shared_build_num
if: ${{ inputs.is_beta }}
id: set_ref
run: |
if [ "${{ inputs.is_shared_build_num }}" == "true" ]; then
echo "ref=main" >> $GITHUB_ENV
else
echo "ref=${{ inputs.release_tag }}" >> $GITHUB_ENV
fi
shell: bash
- name: Checkout SideStore/beta-build-num repo
if: ${{ inputs.is_beta }}
uses: actions/checkout@v4
with:
repository: 'SideStore/beta-build-num'
ref: ${{ env.ref }}
token: ${{ secrets.CROSS_REPO_PUSH_KEY }}
path: 'SideStore/beta-build-num'
- name: Copy build_number.txt to repo root
if: ${{ inputs.is_beta }}
run: |
cp SideStore/beta-build-num/build_number.txt .
echo "cat build_number.txt"
cat build_number.txt
shell: bash
- name: Echo Build.xcconfig
run: |
echo "cat Build.xcconfig"
cat Build.xcconfig
shell: bash
- name: Set Release Channel info for build number bumper
id: release-channel
run: |
RELEASE_CHANNEL="${{ inputs.release_tag }}"
echo "RELEASE_CHANNEL=${RELEASE_CHANNEL}" >> $GITHUB_ENV
echo "RELEASE_CHANNEL=${RELEASE_CHANNEL}" >> $GITHUB_OUTPUT
echo "RELEASE_CHANNEL=${RELEASE_CHANNEL}"
shell: bash
- name: Increase build number for beta builds
if: ${{ inputs.is_beta }}
run: |
bash .github/workflows/increase-beta-build-num.sh
shell: bash
- name: Extract MARKETING_VERSION from Build.xcconfig
id: version
run: |
version=$(grep MARKETING_VERSION Build.xcconfig | sed -e 's/MARKETING_VERSION = //g')
echo "version=$version" >> $GITHUB_OUTPUT
echo "version=$version"
shell: bash
- name: Set MARKETING_VERSION
if: ${{ inputs.is_beta }}
id: marketing-version
run: |
# Extract version number (e.g., "0.6.0")
version=$(echo "${{ steps.version.outputs.version }}" | sed -E 's/^[^0-9]*([0-9]+\.[0-9]+\.[0-9]+).*/\1/')
# Extract date (YYYYMMDD) (e.g., "20250205")
date=$(echo "${{ steps.version.outputs.version }}" | sed -E 's/.*\.([0-9]{4})\.([0-9]{2})\.([0-9]{2})\..*/\1\2\3/')
# Extract build number (e.g., "2")
build_num=$(echo "${{ steps.version.outputs.version }}" | sed -E 's/.*\.([0-9]+)\+.*/\1/')
# Combine them into the final output
MARKETING_VERSION="${version}-${date}.${build_num}+${{ inputs.short_commit }}"
echo "MARKETING_VERSION=$MARKETING_VERSION" >> $GITHUB_ENV
echo "MARKETING_VERSION=$MARKETING_VERSION" >> $GITHUB_OUTPUT
echo "MARKETING_VERSION=$MARKETING_VERSION"
shell: bash
- name: Echo Updated Build.xcconfig, build_number.txt
if: ${{ inputs.is_beta }}
run: |
cat Build.xcconfig
cat build_number.txt
shell: bash
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: ${{ matrix.version }}
- name: (Build) Restore Xcode & SwiftPM Cache (Exact match)
id: xcode-cache-restore
uses: actions/cache/restore@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-build-${{ github.ref_name }}-${{ github.sha }}
- name: (Build) Restore Xcode & SwiftPM Cache (Last Available)
id: xcode-cache-restore-recent
uses: actions/cache/restore@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-build-${{ github.ref_name }}-
# - name: (Build) Cache Build
# uses: irgaly/xcode-cache@v1.8.1
# with:
# key: xcode-cache-deriveddata-build-${{ github.ref_name }}-${{ github.sha }}
# restore-keys: xcode-cache-deriveddata-build-${{ github.ref_name }}-
# swiftpm-cache-key: xcode-cache-sourcedata-build-${{ github.ref_name }}-${{ github.sha }}
# swiftpm-cache-restore-keys: |
# xcode-cache-sourcedata-build-${{ github.ref_name }}-
- name: (Build) Clean previous build artifacts
# using 'tee' to intercept stdout and log for detailed build-log
run: |
make clean
mkdir -p build/logs
shell: bash
- name: (Build) List Files and derived data
if: always()
shell: bash
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
ls -la .
echo ""
echo ">>>>>>>>> SideStore <<<<<<<<<<"
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Dependencies <<<<<<<<<<"
find Dependencies -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
echo ""
- name: Set BundleID Suffix for Sidestore build
run: |
echo "BUNDLE_ID_SUFFIX=${{ inputs.bundle_id_suffix }}" >> $GITHUB_ENV
shell: bash
- name: Build SideStore.xcarchive
# using 'tee' to intercept stdout and log for detailed build-log
run: |
NSUnbufferedIO=YES make -B build 2>&1 | tee -a build/logs/build.log | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
shell: bash
- name: Fakesign app
run: make fakesign | tee -a build/logs/build.log
shell: bash
- name: Convert to IPA
run: make ipa | tee -a build/logs/build.log
shell: bash
- name: (Build) Save Xcode & SwiftPM Cache
id: cache-save
if: ${{ steps.xcode-cache-restore.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-build-${{ github.ref_name }}-${{ github.sha }}
- name: (Build) List Files and Build artifacts
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
ls -la .
echo ""
echo ">>>>>>>>> Build <<<<<<<<<<"
find build -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> SideStore <<<<<<<<<<"
find SideStore -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> SideStore.xcarchive <<<<<<<<<<"
find SideStore.xcarchive -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
echo ""
shell: bash
- name: Encrypt build-logs for upload
id: encrypt-build-log
run: |
DEFAULT_BUILD_LOG_PASSWORD=12345
BUILD_LOG_ZIP_PASSWORD=${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
BUILD_LOG_ZIP_PASSWORD=${BUILD_LOG_ZIP_PASSWORD:-$DEFAULT_BUILD_LOG_PASSWORD}
if [ "$BUILD_LOG_ZIP_PASSWORD" == "$DEFAULT_BUILD_LOG_PASSWORD" ]; then
echo "Warning: BUILD_LOG_ZIP_PASSWORD is not set. Defaulting to '${DEFAULT_BUILD_LOG_PASSWORD}'."
fi
pushd build/logs && zip -e -P "$BUILD_LOG_ZIP_PASSWORD" ../../encrypted-build-logs.zip * || popd
echo "::set-output name=encrypted::true"
shell: bash
- name: Upload encrypted-build-logs.zip
id: attach-encrypted-build-log
if: ${{ always() && steps.encrypt-build-log.outputs.encrypted == 'true' }}
uses: actions/upload-artifact@v4
with:
name: encrypted-build-logs-${{ steps.version.outputs.version }}.zip
path: encrypted-build-logs.zip
- name: Upload SideStore.ipa Artifact
uses: actions/upload-artifact@v4
with:
name: SideStore-${{ steps.version.outputs.version }}.ipa
path: SideStore.ipa
- name: Zip dSYMs
run: zip -r -9 ./SideStore.dSYMs.zip ./SideStore.xcarchive/dSYMs
shell: bash
- name: Upload *.dSYM Artifact
uses: actions/upload-artifact@v4
with:
name: SideStore-${{ steps.version.outputs.version }}-dSYMs.zip
path: SideStore.dSYMs.zip
- name: Keep rolling the build numbers for each successful build
if: ${{ inputs.is_beta }}
run: |
pushd SideStore/beta-build-num/
echo "Configure Git user (committer details)"
git config user.name "GitHub Actions"
git config user.email "github-actions@github.com"
echo "Adding files to commit"
git add --verbose build_number.txt
git commit -m " - updated for ${{ inputs.release_tag }} - ${{ inputs.short_commit }} deployment" || echo "No changes to commit"
echo "Pushing to remote repo"
git push --verbose
popd
shell: bash
- name: Get last successful commit
id: get_last_commit
run: |
# Try to get the last successful workflow run commit
LAST_SUCCESS_SHA=$(gh run list --branch "${{ github.ref_name }}" --status success --json headSha --jq '.[0].headSha')
echo "LAST_SUCCESS_SHA=$LAST_SUCCESS_SHA" >> $GITHUB_OUTPUT
echo "LAST_SUCCESS_SHA=$LAST_SUCCESS_SHA" >> $GITHUB_ENV
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
- name: Create release notes
run: |
LAST_SUCCESS_SHA=${{ steps.get_last_commit.outputs.LAST_SUCCESS_SHA}}
echo "Last successful commit SHA: $LAST_SUCCESS_SHA"
FROM_COMMIT=$LAST_SUCCESS_SHA
# Check if we got a valid SHA
if [ -z "$LAST_SUCCESS_SHA" ] || [ "$LAST_SUCCESS_SHA" = "null" ]; then
echo "No successful run found, using initial commit of branch"
# Get the first commit of the branch (initial commit)
FROM_COMMIT=$(git rev-list --max-parents=0 HEAD)
fi
python3 update_release_notes.py $FROM_COMMIT ${{ inputs.release_tag }} ${{ github.ref_name }}
# cat release-notes.md
shell: bash
- name: Upload release-notes.md
uses: actions/upload-artifact@v4
with:
name: release-notes-${{ inputs.short_commit }}.md
path: release-notes.md
- name: Upload update_release_notes.py
uses: actions/upload-artifact@v4
with:
name: update_release_notes-${{ inputs.short_commit }}.py
path: update_release_notes.py
- name: Upload update_apps.py
uses: actions/upload-artifact@v4
with:
name: update_apps-${{ inputs.short_commit }}.py
path: update_apps.py

View File

@@ -1,264 +0,0 @@
name: SideStore Deploy
on:
workflow_call:
inputs:
is_beta:
type: boolean
publish:
type: boolean
release_name:
type: string
release_tag:
type: string
upstream_tag:
type: string
upstream_name:
type: string
version:
type: string
short_commit:
type: string
marketing_version:
type: string
release_channel:
type: string
bundle_id:
type: string
secrets:
CROSS_REPO_PUSH_KEY:
required: true
# GITHUB_TOKEN:
# required: true
jobs:
deploy:
name: Deploy SideStore - ${{ inputs.release_tag }}
runs-on: macos-15
steps:
- name: Download IPA artifact
uses: actions/download-artifact@v4
with:
name: SideStore-${{ inputs.version }}.ipa
- name: Download dSYM artifact
uses: actions/download-artifact@v4
with:
name: SideStore-${{ inputs.version }}-dSYMs.zip
- name: Download encrypted-build-logs artifact
uses: actions/download-artifact@v4
with:
name: encrypted-build-logs-${{ inputs.version }}.zip
- name: Download encrypted-tests-build-logs artifact
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_BUILD == '1' }}
uses: actions/download-artifact@v4
with:
name: encrypted-tests-build-logs-${{ inputs.short_commit }}.zip
- name: Download encrypted-tests-run-logs artifact
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
uses: actions/download-artifact@v4
with:
name: encrypted-tests-run-logs-${{ inputs.short_commit }}.zip
- name: Download tests-recording artifact
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
uses: actions/download-artifact@v4
with:
name: tests-recording-${{ inputs.short_commit }}.mp4
- name: Download test-results artifact
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
uses: actions/download-artifact@v4
with:
name: test-results-${{ inputs.short_commit }}.zip
- name: Download release-notes.md
uses: actions/download-artifact@v4
with:
name: release-notes-${{ inputs.short_commit }}.md
- name: Download update_release_notes.py
uses: actions/download-artifact@v4
with:
name: update_release_notes-${{ inputs.short_commit }}.py
- name: Download update_apps.py
uses: actions/download-artifact@v4
with:
name: update_apps-${{ inputs.short_commit }}.py
- name: Read release notes
id: release_notes
run: |
CONTENT=$(python3 update_release_notes.py --retrieve ${{ inputs.release_tag }})
echo "content<<EOF" >> $GITHUB_OUTPUT
echo "$CONTENT" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
shell: bash
- name: List files before upload
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
find . -maxdepth 4 -exec ls -ld {} + || true # List contents if directory exists
echo ""
shell: bash
- name: Get current date
id: date
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
shell: bash
- name: Get current date in AltStore date form
id: date_altstore
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
shell: bash
- name: List files to upload
id: list_uploads
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
find . -maxdepth 4 -exec ls -ld {} + || true # List contents if directory exists
echo ""
FILES="SideStore.ipa SideStore.dSYMs.zip encrypted-build-logs.zip"
if [[ "${{ vars.ENABLE_TESTS }}" == "1" && "${{ vars.ENABLE_TESTS_BUILD }}" == "1" ]]; then
FILES="$FILES encrypted-tests-build-logs.zip"
fi
if [[ "${{ vars.ENABLE_TESTS }}" == "1" && "${{ vars.ENABLE_TESTS_RUN }}" == "1" ]]; then
FILES="$FILES encrypted-tests-run-logs.zip test-results.zip tests-recording.mp4"
fi
echo "Final upload list:"
for f in $FILES; do
if [[ -f "$f" ]]; then
echo " ✓ $f"
else
echo " - $f (missing)"
fi
done
echo "files=$FILES" >> $GITHUB_OUTPUT
- name: Upload to releases
uses: IsaacShelton/update-existing-release@v1.3.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
release: ${{ inputs.release_name }}
tag: ${{ inputs.release_tag }}
prerelease: ${{ inputs.is_beta }}
files: ${{ steps.list_uploads.outputs.files }}
body: |
This is an ⚠️ **EXPERIMENTAL** ⚠️ ${{ inputs.release_name }} build for commit [${{ github.sha }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }}).
${{ inputs.release_name }} builds are **extremely experimental builds only meant to be used by developers and beta testers. They often contain bugs and experimental features. Use at your own risk!**
If you want to try out new features early but want a lower chance of bugs, you can look at [SideStore ${{ inputs.upstream_name }}](https://github.com/${{ github.repository }}/releases?q=${{ inputs.upstream_tag }}).
## Build Info
Built at (UTC): `${{ steps.date.outputs.date }}`
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
Commit SHA: `${{ github.sha }}`
Version: `${{ inputs.version }}`
${{ steps.release_notes.outputs.content }}
- name: Get formatted date
run: |
FORMATTED_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
echo "Formatted date: $FORMATTED_DATE"
echo "FORMATTED_DATE=$FORMATTED_DATE" >> $GITHUB_ENV
shell: bash
- name: Get size of IPA in bytes (macOS/Linux)
run: |
if [[ "$(uname)" == "Darwin" ]]; then
# macOS
IPA_SIZE=$(stat -f %z SideStore.ipa)
else
# Linux
IPA_SIZE=$(stat -c %s SideStore.ipa)
fi
echo "IPA size in bytes: $IPA_SIZE"
echo "IPA_SIZE=$IPA_SIZE" >> $GITHUB_ENV
shell: bash
- name: Compute SHA-256 of IPA
run: |
SHA256_HASH=$(shasum -a 256 SideStore.ipa | awk '{ print $1 }')
echo "SHA-256 Hash: $SHA256_HASH"
echo "SHA256_HASH=$SHA256_HASH" >> $GITHUB_ENV
shell: bash
- name: Set Release Info variables
run: |
# Format localized description
LOCALIZED_DESCRIPTION=$(cat <<EOF
This is release for:
- version: "${{ inputs.version }}"
- revision: "${{ inputs.short_commit }}"
- timestamp: "${{ steps.date.outputs.date }}"
Release Notes:
${{ steps.release_notes.outputs.content }}
EOF
)
echo "IS_BETA=${{ inputs.is_beta }}" >> $GITHUB_ENV
echo "BUNDLE_IDENTIFIER=${{ inputs.bundle_id }}" >> $GITHUB_ENV
echo "VERSION_IPA=${{ inputs.marketing_version }}" >> $GITHUB_ENV
echo "VERSION_DATE=$FORMATTED_DATE" >> $GITHUB_ENV
echo "RELEASE_CHANNEL=${{ inputs.release_channel }}" >> $GITHUB_ENV
echo "SIZE=$IPA_SIZE" >> $GITHUB_ENV
echo "SHA256=$SHA256_HASH" >> $GITHUB_ENV
echo "DOWNLOAD_URL=https://github.com/SideStore/SideStore/releases/download/${{ inputs.release_tag }}/SideStore.ipa" >> $GITHUB_ENV
# multiline strings
echo "LOCALIZED_DESCRIPTION<<EOF" >> $GITHUB_ENV
echo "$LOCALIZED_DESCRIPTION" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
shell: bash
- name: Check if Publish updates is set
id: check_publish
run: |
echo "Publish updates to source.json = ${{ inputs.publish }}"
shell: bash
- name: Checkout SideStore/apps-v2.json
if: ${{ inputs.is_beta && inputs.publish }}
uses: actions/checkout@v4
with:
repository: 'SideStore/apps-v2.json'
ref: 'main' # this branch is shared by all beta builds, so beta build workflows are serialized
token: ${{ secrets.CROSS_REPO_PUSH_KEY }}
path: 'SideStore/apps-v2.json'
# for stable builds, let the user manually edit the source.json
- name: Publish to SideStore/apps-v2.json
if: ${{ inputs.is_beta && inputs.publish }}
id: publish-release
shell: bash
run: |
# Copy and execute the update script
pushd SideStore/apps-v2.json/
# Configure Git user (committer details)
git config user.name "GitHub Actions"
git config user.email "github-actions@github.com"
# update the source.json
python3 ../../update_apps.py "./_includes/source.json"
# Commit changes and push using SSH
git add --verbose ./_includes/source.json
git commit -m " - updated for ${{ inputs.short_commit }} deployment" || echo "No changes to commit"
git push --verbose
popd

View File

@@ -1,24 +0,0 @@
name: SideStore Shared
on:
workflow_call:
outputs:
short-commit:
value: ${{ jobs.shared.outputs.short-commit }}
jobs:
shared:
name: Shared Steps
strategy:
fail-fast: false
runs-on: 'macos-15'
steps:
- name: Set short commit hash
id: commit-id
run: |
# SHORT_COMMIT="${{ github.sha }}"
SHORT_COMMIT=${GITHUB_SHA:0:7}
echo "Short commit hash: $SHORT_COMMIT"
echo "SHORT_COMMIT=$SHORT_COMMIT" >> $GITHUB_OUTPUT
outputs:
short-commit: ${{ steps.commit-id.outputs.SHORT_COMMIT }}

View File

@@ -1,165 +0,0 @@
name: SideStore Tests Build
on:
workflow_call:
inputs:
release_tag:
type: string
short_commit:
type: string
secrets:
BUILD_LOG_ZIP_PASSWORD:
required: false
jobs:
tests-build:
name: Tests-Build SideStore - ${{ inputs.release_tag }}
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_BUILD == '1' }}
strategy:
fail-fast: false
matrix:
include:
- os: 'macos-26'
version: '26.0'
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install dependencies - xcbeautify
run: |
brew install xcbeautify
shell: bash
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: '26.0'
# - name: (Tests-Build) Cache Build
# uses: irgaly/xcode-cache@v1.8.1
# with:
# key: xcode-cache-deriveddata-test-${{ github.ref_name }}-${{ github.sha }}
# # tests shouldn't restore cache unless it is same build
# # restore-keys: xcode-cache-deriveddata-test-${{ github.ref_name }}-
# swiftpm-cache-key: xcode-cache-sourcedata-test-${{ github.ref_name }}-${{ github.sha }}
# swiftpm-cache-restore-keys: |
# xcode-cache-sourcedata-test-${{ github.ref_name }}-
# delete-used-deriveddata-cache: true
- name: (Tests-Build) Restore Xcode & SwiftPM Cache (Exact match)
id: xcode-cache-restore
uses: actions/cache/restore@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-tests-${{ github.ref_name }}-${{ github.sha }}
- name: (Tests-Build) Restore Xcode & SwiftPM Cache (Last Available)
id: xcode-cache-restore-recent
uses: actions/cache/restore@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-tests-${{ github.ref_name }}-
- name: Clean Derived Data (if required)
if: ${{ vars.PERFORM_CLEAN_TESTS_BUILD == '1' }}
run: |
rm -rf ~/Library/Developer/Xcode/DerivedData/
make clean
xcodebuild clean
shell: bash
- name: (Tests-Build) Clean previous build artifacts
run: |
make clean
mkdir -p build/logs
shell: bash
- name: (Tests-Build) List Files and derived data
shell: bash
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
ls -la .
echo ""
echo ">>>>>>>>> SideStore <<<<<<<<<<"
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Dependencies <<<<<<<<<<"
find Dependencies -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
echo ""
- name: Build SideStore Tests
# using 'tee' to intercept stdout and log for detailed build-log
shell: bash
run: |
NSUnbufferedIO=YES make -B build-tests 2>&1 | tee -a build/logs/tests-build.log | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
- name: (Tests-Build) Save Xcode & SwiftPM Cache
id: cache-save
if: ${{ steps.xcode-cache-restore.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-tests-${{ github.ref_name }}-${{ github.sha }}
- name: (Tests-Build) List Files and Build artifacts
if: always()
shell: bash
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
ls -la .
echo ""
echo ">>>>>>>>> Build <<<<<<<<<<"
find build -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
find ~/Library/Developer/Xcode/DerivedData -maxdepth 8 -exec ls -ld {} + | grep "Build/Products" >> tests-build-deriveddata.txt || true
echo ""
- uses: actions/upload-artifact@v4
if: always()
with:
name: tests-build-deriveddata-${{ inputs.short_commit }}.txt
path: tests-build-deriveddata.txt
- name: Encrypt tests-build-logs for upload
id: encrypt-test-log
if: always()
shell: bash
run: |
DEFAULT_BUILD_LOG_PASSWORD=12345
BUILD_LOG_ZIP_PASSWORD=${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
BUILD_LOG_ZIP_PASSWORD=${BUILD_LOG_ZIP_PASSWORD:-$DEFAULT_BUILD_LOG_PASSWORD}
if [ "$BUILD_LOG_ZIP_PASSWORD" == "$DEFAULT_BUILD_LOG_PASSWORD" ]; then
echo "Warning: BUILD_LOG_ZIP_PASSWORD is not set. Defaulting to '${DEFAULT_BUILD_LOG_PASSWORD}'."
fi
pushd build/logs && zip -e -P "$BUILD_LOG_ZIP_PASSWORD" ../../encrypted-tests-build-logs.zip * || popd
echo "::set-output name=encrypted::true"
- name: Upload encrypted-tests-build-logs.zip
id: attach-encrypted-test-log
if: always() && steps.encrypt-test-log.outputs.encrypted == 'true'
uses: actions/upload-artifact@v4
with:
name: encrypted-tests-build-logs-${{ inputs.short_commit }}.zip
path: encrypted-tests-build-logs.zip

View File

@@ -1,196 +0,0 @@
name: SideStore Tests Run
on:
workflow_call:
inputs:
release_tag:
type: string
short_commit:
type: string
secrets:
BUILD_LOG_ZIP_PASSWORD:
required: false
jobs:
tests-run:
name: Tests-Run SideStore - ${{ inputs.release_tag }}
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
strategy:
fail-fast: false
matrix:
include:
- os: 'macos-26'
version: '26.0'
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Boot Simulator async(nohup) for testing
run: |
mkdir -p build/logs
nohup make -B boot-sim-async </dev/null >> build/logs/tests-run.log 2>&1 &
shell: bash
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: '26.0'
# - name: (Tests-Run) Cache Build
# uses: irgaly/xcode-cache@v1.8.1
# with:
# # This comes from
# key: xcode-cache-deriveddata-test-${{ github.ref_name }}-${{ github.sha }}
# swiftpm-cache-key: xcode-cache-sourcedata-test-${{ github.ref_name }}-${{ github.sha }}
- name: (Tests-Build) Restore Xcode & SwiftPM Cache (Exact match) [from tests-build job]
id: xcode-cache-restore
uses: actions/cache/restore@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-tests-${{ github.ref_name }}-${{ github.sha }}
- name: (Tests-Run) Clean previous build artifacts
run: |
make clean
mkdir -p build/logs
shell: bash
- name: (Tests-Run) List Files and derived data
shell: bash
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
ls -la .
echo ""
echo ">>>>>>>>> SideStore <<<<<<<<<<"
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Dependencies <<<<<<<<<<"
find Dependencies -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
find ~/Library/Developer/Xcode/DerivedData -maxdepth 8 -exec ls -ld {} + | grep "Build/Products" >> tests-run-deriveddata.txt || true
echo ""
- uses: actions/upload-artifact@v4
if: always()
with:
name: tests-run-deriveddata-${{ inputs.short_commit }}.txt
path: tests-run-deriveddata.txt
# we expect simulator to have been booted by now, so exit otherwise
- name: Simulator Boot Check
run: |
mkdir -p build/logs
make -B sim-boot-check | tee -a build/logs/tests-run.log
exit ${PIPESTATUS[0]}
shell: bash
- name: Start Recording UI tests (if DEBUG_RECORD_TESTS is set to 1)
if: ${{ vars.DEBUG_RECORD_TESTS == '1' }}
run: |
nohup xcrun simctl io booted recordVideo -f tests-recording.mp4 --codec h264 </dev/null > tests-recording.log 2>&1 &
RECORD_PID=$!
echo "RECORD_PID=$RECORD_PID" >> $GITHUB_ENV
shell: bash
- name: Run SideStore Tests
# using 'tee' to intercept stdout and log for detailed build-log
run: |
make run-tests 2>&1 | tee -a build/logs/tests-run.log && exit ${PIPESTATUS[0]}
# NSUnbufferedIO=YES make -B run-tests 2>&1 | tee build/logs/tests-run.log | xcpretty -r junit --output ./build/tests/test-results.xml && exit ${PIPESTATUS[0]}
shell: bash
- name: Stop Recording tests
if: ${{ always() && env.RECORD_PID != '' }}
run: |
kill -INT ${{ env.RECORD_PID }}
shell: bash
- name: (Tests-Run) List Files and Build artifacts
if: always()
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
ls -la .
echo ""
echo ">>>>>>>>> Build <<<<<<<<<<"
find build -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
echo ""
shell: bash
- name: Encrypt tests-run-logs for upload
id: encrypt-test-log
if: always()
run: |
DEFAULT_BUILD_LOG_PASSWORD=12345
BUILD_LOG_ZIP_PASSWORD=${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
BUILD_LOG_ZIP_PASSWORD=${BUILD_LOG_ZIP_PASSWORD:-$DEFAULT_BUILD_LOG_PASSWORD}
if [ "$BUILD_LOG_ZIP_PASSWORD" == "$DEFAULT_BUILD_LOG_PASSWORD" ]; then
echo "Warning: BUILD_LOG_ZIP_PASSWORD is not set. Defaulting to '${DEFAULT_BUILD_LOG_PASSWORD}'."
fi
pushd build/logs && zip -e -P "$BUILD_LOG_ZIP_PASSWORD" ../../encrypted-tests-run-logs.zip * || popd
echo "::set-output name=encrypted::true"
shell: bash
- name: Upload encrypted-tests-run-logs.zip
id: attach-encrypted-test-log
if: always() && steps.encrypt-test-log.outputs.encrypted == 'true'
uses: actions/upload-artifact@v4
with:
name: encrypted-tests-run-logs-${{ inputs.short_commit }}.zip
path: encrypted-tests-run-logs.zip
- name: Print tests-recording.log contents (if exists)
if: ${{ always() && env.RECORD_PID != '' }}
run: |
if [ -f tests-recording.log ]; then
echo "tests-recording.log found. Its contents:"
cat tests-recording.log
else
echo "tests-recording.log not found."
fi
shell: bash
- name: Check for tests-recording.mp4 presence
id: check-recording
if: ${{ always() && env.RECORD_PID != '' }}
run: |
if [ -f tests-recording.mp4 ]; then
echo "::set-output name=found::true"
echo "tests-recording.mp4 found."
else
echo "tests-recording.mp4 not found, skipping upload."
echo "::set-output name=found::false"
fi
shell: bash
- name: Upload tests-recording.mp4
id: upload-recording
if: ${{ always() && steps.check-recording.outputs.found == 'true' }}
uses: actions/upload-artifact@v4
with:
name: tests-recording-${{ inputs.short_commit }}.mp4
path: tests-recording.mp4
- name: Zip test-results
run: zip -r -9 ./test-results.zip ./build/tests
shell: bash
- name: Upload Test Artifacts
uses: actions/upload-artifact@v4
with:
name: test-results-${{ inputs.short_commit }}.zip
path: test-results.zip

View File

@@ -3,240 +3,115 @@ 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
workflow_dispatch:
jobs: jobs:
build: build:
name: Build SideStore - stable (on tag push) name: Build and upload SideStore
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- os: 'macos-26' - os: 'macos-12'
version: '26.0' version: '14.2'
runs-on: ${{ matrix.os }}
runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v2
with: with:
submodules: recursive submodules: recursive
- name: Echo Build.xcconfig - name: Install dependencies
run: | run: brew install ldid
echo "cat Build.xcconfig"
cat Build.xcconfig
shell: bash
# - name: Change MARKETING_VERSION to the pushed tag that triggered this build - 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: Echo Updated Build.xcconfig - name: Get version
run: |
cat Build.xcconfig
shell: bash
- name: Extract MARKETING_VERSION from Build.xcconfig
id: version id: version
run: | run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
version=$(grep MARKETING_VERSION Build.xcconfig | sed -e 's/MARKETING_VERSION = //g')
echo "version=$version" >> $GITHUB_OUTPUT
echo "version=$version"
echo "MARKETING_VERSION=$version" >> $GITHUB_ENV - name: Echo version
echo "MARKETING_VERSION=$version" >> $GITHUB_OUTPUT run: echo "${{ steps.version.outputs.version }}"
echo "MARKETING_VERSION=$version"
shell: bash
- name: Fail the build if pushed tag and embedded MARKETING_VERSION in Build.xcconfig are mismatching
run: |
if [ "$MARKETING_VERSION" != "${{ github.ref_name }}" ]; then
echo 'Version mismatch: $tag != $marketing_version ... '
echo " expected-tag : $MARKETING_VERSION"
echo " pushed-tag : ${{ github.ref_name }}"
exit 1
fi
echo 'Version matches: $tag == $marketing_version ... '
echo " expected-tag : $MARKETING_VERSION"
echo " pushed-tag : ${{ github.ref_name }}"
shell: bash
- name: Install dependencies - ldid & xcbeautify
run: |
brew install ldid xcbeautify
- name: Setup Xcode - name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.6.0 uses: maxim-lobanov/setup-xcode@v1.4.1
with: with:
xcode-version: ${{ matrix.version }} xcode-version: ${{ matrix.version }}
- name: (Build) Restore Xcode & SwiftPM Cache (Exact match) - name: "[Normal] Build SideStore, fakesign app and convert to IPA"
id: xcode-cache-restore run: |
uses: actions/cache/restore@v3 make build | xcpretty
with: make fakesign
path: | make ipa
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-build-stable-${{ github.sha }}
- name: (Build) Restore Xcode & SwiftPM Cache (Last Available) - name: Enable MDC
id: xcode-cache-restore-recent run: make enable_mdc
uses: actions/cache/restore@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-build-stable-
- name: (Build) Clean previous build artifacts - name: "[MDC] Build SideStore, fakesign app and convert to IPA"
run: | run: |
make clean make clean
mkdir -p build/logs make build DSYM_FOLDER=./MDC-dSYM | xcpretty
shell: bash make fakesign
make ipa IPA_NAME=SideStore-MDC.ipa
- name: (Build) List Files and derived data
if: always()
shell: bash
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
ls -la .
echo ""
echo ">>>>>>>>> SideStore <<<<<<<<<<"
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Dependencies <<<<<<<<<<"
find Dependencies -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
echo ""
- name: Build SideStore.xcarchive
# using 'tee' to intercept stdout and log for detailed build-log
run: |
NSUnbufferedIO=YES make -B build 2>&1 | tee -a build/logs/build.log | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
shell: bash
- name: Fakesign app
run: make fakesign | tee -a build/logs/build.log
shell: bash
- name: Convert to IPA
run: make ipa | tee -a build/logs/build.log
shell: bash
- name: (Build) Save Xcode & SwiftPM Cache
id: cache-save
if: ${{ steps.xcode-cache-restore.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-build-stable-${{ github.sha }}
- name: (Build) List Files and Build artifacts
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
ls -la .
echo ""
echo ">>>>>>>>> Build <<<<<<<<<<"
find build -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> SideStore <<<<<<<<<<"
find SideStore -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> SideStore.xcarchive <<<<<<<<<<"
find SideStore.xcarchive -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
echo ""
shell: bash
- name: Encrypt build-logs for upload
id: encrypt-build-log
run: |
DEFAULT_BUILD_LOG_PASSWORD=12345
BUILD_LOG_ZIP_PASSWORD=${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
BUILD_LOG_ZIP_PASSWORD=${BUILD_LOG_ZIP_PASSWORD:-$DEFAULT_BUILD_LOG_PASSWORD}
if [ "$BUILD_LOG_ZIP_PASSWORD" == "$DEFAULT_BUILD_LOG_PASSWORD" ]; then
echo "Warning: BUILD_LOG_ZIP_PASSWORD is not set. Defaulting to '${DEFAULT_BUILD_LOG_PASSWORD}'."
fi
pushd build/logs && zip -e -P "$BUILD_LOG_ZIP_PASSWORD" ../../encrypted-build-logs.zip * || popd
echo "::set-output name=encrypted::true"
shell: bash
- name: Upload encrypted-build-logs.zip
id: attach-encrypted-build-log
if: ${{ always() && steps.encrypt-build-log.outputs.encrypted == 'true' }}
uses: actions/upload-artifact@v4
with:
name: encrypted-build-logs-${{ steps.version.outputs.version }}.zip
path: encrypted-build-logs.zip
- name: Upload SideStore.ipa Artifact
uses: actions/upload-artifact@v4
with:
name: SideStore-${{ steps.version.outputs.version }}.ipa
path: SideStore.ipa
- name: Zip dSYMs
run: zip -r -9 ./SideStore.dSYMs.zip ./SideStore.xcarchive/dSYMs
shell: bash
- name: Upload *.dSYM Artifact
uses: actions/upload-artifact@v4
with:
name: SideStore-${{ steps.version.outputs.version }}-dSYMs.zip
path: SideStore.dSYMs.zip
- name: Get current date - name: Get current date
id: date id: date
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
shell: bash
- name: Get current date in AltStore date form - name: Get current date in AltStore date form
id: date_altstore id: date_altstore
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
shell: bash
- name: Upload to releases - name: Upload to new stable release
uses: IsaacShelton/update-existing-release@v1.3.1 uses: softprops/action-gh-release@v1
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
name: ${{ steps.version.outputs.version }}
tag_name: ${{ github.ref_name }}
draft: true draft: true
release: ${{ github.ref_name }} # name files: |
tag: ${{ github.ref_name }} SideStore.ipa
# stick with what the user pushed, do not use latest commit or anything, SideStore-MDC.ipa
# ex: if we want to go back to previous release due to hot issue, dev can create a new tag pointing to that older working tag/commit so as to keep it as an update (to revert major issue)
# in this case we do not want the tag to be auto-updated to latest
updateTag: false
prerelease: false
files: >
SideStore.ipa
SideStore.dSYMs.zip
encrypted-build-logs.zip
body: | 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. --> <!-- 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 ## Changelog
- TODO - TODO
## Build Info ## 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 }}`
Built at (UTC): `${{ steps.date.outputs.date }}` - name: Add version to IPA file name
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}` run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
Commit SHA: `${{ github.sha }}`
Version: `${{ steps.version.outputs.version }}` - name: Add version to MDC IPA file name
run: mv SideStore-MDC.ipa SideStore-MDC-${{ steps.version.outputs.version }}.ipa
- name: Upload SideStore.ipa Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-${{ steps.version.outputs.version }}.ipa
path: SideStore-${{ steps.version.outputs.version }}.ipa
- name: Upload SideStore-MDC.ipa Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-MDC-${{ steps.version.outputs.version }}.ipa
path: SideStore-MDC-${{ steps.version.outputs.version }}.ipa
- name: Upload dSYM Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-${{ steps.version.outputs.version }}-dSYM
path: ./dSYM/*
- name: Upload MDC-dSYM Artifact
uses: actions/upload-artifact@v3.1.0
with:
name: SideStore-MDC-${{ steps.version.outputs.version }}-dSYM
path: ./MDC-dSYM/*

38
.gitignore vendored
View File

@@ -1,18 +1,14 @@
# macOS # macOS
# #
**/*.DS_Store *.DS_Store
# Xcode # Xcode
# #
## CocoaPods
Pods/
## Build generated ## Build generated
build/ build/
DerivedData DerivedData
archive.xcarchive
SideStore.xcarchive
## Various settings ## Various settings
*.pbxuser *.pbxuser
!default.pbxuser !default.pbxuser
@@ -40,33 +36,11 @@ xcuserdata
.idea/ .idea/
Payload/ Payload/
**/SideStore.ipa SideStore*.ipa
**/AltBackup.ipa *dSYM
**/*.dSYM
Dependencies/.*-prebuilt-fetch-* Dependencies/.*-prebuilt-fetch-*
SideStore/minimuxer/* Dependencies/minimuxer/*
SideStore/em_proxy/* Dependencies/em_proxy/*
!Dependencies/**/.gitkeep !Dependencies/**/.gitkeep
.nightly-build-num .nightly-build-num
## em_proxy and minimuxer biaries
**/.last-prebuilt-fetch-em_proxy
**/.last-prebuilt-fetch-minimuxer
# misc
**/output.txt
SideStore/.skip-prebuilt-fetch-minimuxer
SideStore/.skip-prebuilt-fetch-em_proxy
.git.bkp/
# Never check-in this package.resolved file
# coz SPM then resolves packages using the stale entries in this file
*.xcodeproj/**/Package.resolved
*.xcworkspace/**/Package.resolved
# some more commandline build artifacts
test-recording.mp4
test-recording.log
altstore-sources.md
local-build.sh

77
.gitmodules vendored
View File

@@ -1,68 +1,21 @@
#-------------------------------
# When changing url/branch in this .gitmodules file,
# Always ensure you run:
# 1. `git rm --cached <submodule_relative_path>` # this removes the submodule entry from general git tracking
# 2. `rm -rf .git/modules/<submodule_relative_path>` # this removes the stale name entries in submodule tracker
# 3. `rm -rf <submodule_relative_path>` # removes the submodule completely
# 4. `git submodule --deinit <submodule_relative_path>` # make sure that the submodule is de-inited too (ignore errors at this point)
# 5. `git submodule add [-b <branch_name>] <repo_url> <submodule_relative_path>` # This adds the submodule back into general git tracking and also adds to the submodule tracker
# 6. Step 5 creates an entry in the .gitmodules when a submodule is added,
# So if you already had one entry, try to remove duplicates at this point
# 7. `git submodule sync --recursive` # this now sets/updates the submodule repo url tracker into git config
# 8. `git submodule update --init --recursive` # this now clones the updated repo set by .gitmodules
# But this will always fetch the latest commit sepecified by the custom(if set)/default branch
# 9. If you do want to have a specific commit in that submodule branch and not latest, you need to perform normal detached head checkout and check-in as follows:
# `pushd <submodule_relative_path>` # switch to the submodule repo
# `git checkout <commit-id>` # this creates a detached head state
# `popd` # get back to parent repo
# `git add <submodule_relative_path>` # check-in the changes in parent for this submodule link (tracker)
# `git commit -m <commit-message>` # commit it to parent repo
# `git push` # push to parent repo to preserve this entire change in the submodule repo/link file
#
# NOTES:
# 1. updating just this .gitmodules file is NOT ENOUGH when changing repo url and performing a simple `git submodule update --init --recursive`, need to do all the above listed steps for proper tracking
# 2. updating the branch in this .gitmodules for same repo is okay as long as `git submodule update --init --recursive` is also performed followed by it
# 3. Ensure there is no stale entries or duplicate entries in this .gitmodules file coz, `git submodule add ...` creates an entry here.
#-------------------------------
[submodule "Dependencies/Roxas"] [submodule "Dependencies/Roxas"]
path = Dependencies/Roxas path = Dependencies/Roxas
url = https://github.com/rileytestut/Roxas.git url = https://github.com/rileytestut/Roxas.git
[submodule "Dependencies/libimobiledevice"] [submodule "Dependencies/libimobiledevice"]
path = Dependencies/libimobiledevice path = Dependencies/libimobiledevice
url = https://github.com/SideStore/libimobiledevice url = https://github.com/libimobiledevice/libimobiledevice
[submodule "Dependencies/libusbmuxd"] [submodule "Dependencies/libusbmuxd"]
path = Dependencies/libusbmuxd path = Dependencies/libusbmuxd
url = https://github.com/libimobiledevice/libusbmuxd.git url = https://github.com/libimobiledevice/libusbmuxd.git
[submodule "Dependencies/libplist"] [submodule "Dependencies/libplist"]
path = Dependencies/libplist path = Dependencies/libplist
url = https://github.com/SideStore/libplist.git url = https://github.com/libimobiledevice/libplist.git
[submodule "Dependencies/MarkdownAttributedString"] [submodule "Dependencies/MarkdownAttributedString"]
path = Dependencies/MarkdownAttributedString path = Dependencies/MarkdownAttributedString
url = https://github.com/chockenberry/MarkdownAttributedString.git url = https://github.com/chockenberry/MarkdownAttributedString.git
[submodule "Dependencies/libimobiledevice-glue"] [submodule "Dependencies/libimobiledevice-glue"]
path = Dependencies/libimobiledevice-glue path = Dependencies/libimobiledevice-glue
url = https://github.com/libimobiledevice/libimobiledevice-glue url = https://github.com/libimobiledevice/libimobiledevice-glue
[submodule "Dependencies/libfragmentzip"]
path = Dependencies/libfragmentzip
#sidestore dependencies url = https://github.com/SideStore/libfragmentzip.git
[submodule "SideStore/minimuxer"]
path = SideStore/minimuxer
url = https://github.com/SideStore/minimuxer
branch = master
[submodule "SideStore/em_proxy"]
path = SideStore/em_proxy
url = https://github.com/SideStore/em_proxy
branch = master
[submodule "SideStore/libfragmentzip"]
path = SideStore/libfragmentzip
url = https://github.com/SideStore/libfragmentzip
branch = master
[submodule "SideStore/apps-v2.json"]
path = SideStore/apps-v2.json
url = https://github.com/SideStore/apps-v2.json
branch = main
[submodule "SideStore/AltSign"]
path = SideStore/AltSign
url = https://github.com/SideStore/AltSign
branch = master

3
AltBackup.xcconfig Normal file
View File

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

View File

@@ -4,7 +4,7 @@
<dict> <dict>
<key>com.apple.security.application-groups</key> <key>com.apple.security.application-groups</key>
<array> <array>
<string>group.$(GROUP_ID)</string> <string>group.$(APP_GROUP_IDENTIFIER)</string>
</array> </array>
</dict> </dict>
</plist> </plist>

View File

@@ -10,10 +10,10 @@ import UIKit
extension AppDelegate extension AppDelegate
{ {
static let startBackupNotification = Notification.Name("io.sidestore.StartBackup") static let startBackupNotification = Notification.Name("io.altstore.StartBackup")
static let startRestoreNotification = Notification.Name("io.sidestore.StartRestore") static let startRestoreNotification = Notification.Name("io.altstore.StartRestore")
static let operationDidFinishNotification = Notification.Name("io.sidestore.BackupOperationFinished") static let operationDidFinishNotification = Notification.Name("io.altstore.BackupOperationFinished")
static let operationResultKey = "result" static let operationResultKey = "result"
} }
@@ -88,25 +88,14 @@ private extension AppDelegate
@objc func operationDidFinish(_ notification: Notification) @objc func operationDidFinish(_ notification: Notification)
{ {
defer { defer { self.currentBackupReturnURL = nil }
self.currentBackupReturnURL = nil
}
// TODO: @mahee96: This doesn't account cases where backup is too long and user switched to other apps
// The check for self.currentBackupReturnURL when backup/restore was still in progress but app switched
// between FG/BG is improper, since it will ignore(eat up) the response(success/failure) to parent
//
// This leaves the backup/restore to show dummy animation forever
guard guard
let returnURL = self.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 { else { return }
return // This is bad (Needs fixing - never eat up response like this unless there is no context to post response to!)
}
guard var components = URLComponents(url: returnURL, resolvingAgainstBaseURL: false) else { guard var components = URLComponents(url: returnURL, resolvingAgainstBaseURL: false) else { return }
return // This is ASSERTION Failure, ie RETURN URL needs to be valid. So ignoring (eating up) response is not the solution
}
switch result switch result
{ {
@@ -123,7 +112,6 @@ private extension AppDelegate
guard let responseURL = components.url else { return } guard let responseURL = components.url else { return }
DispatchQueue.main.async { DispatchQueue.main.async {
// Response to the caller/parent app is posted here (url is provided by caller in incoming query params)
UIApplication.shared.open(responseURL, options: [:]) { (success) in UIApplication.shared.open(responseURL, options: [:]) { (success) in
print("Sent response to app with success:", success) print("Sent response to app with success:", success)
} }

View File

@@ -1,151 +1,91 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "40.png",
"idiom" : "iphone", "idiom" : "iphone",
"scale" : "2x", "scale" : "2x",
"size" : "20x20" "size" : "20x20"
}, },
{ {
"filename" : "60.png",
"idiom" : "iphone", "idiom" : "iphone",
"scale" : "3x", "scale" : "3x",
"size" : "20x20" "size" : "20x20"
}, },
{ {
"filename" : "29.png",
"idiom" : "iphone",
"scale" : "1x",
"size" : "29x29"
},
{
"filename" : "58.png",
"idiom" : "iphone", "idiom" : "iphone",
"scale" : "2x", "scale" : "2x",
"size" : "29x29" "size" : "29x29"
}, },
{ {
"filename" : "87.png",
"idiom" : "iphone", "idiom" : "iphone",
"scale" : "3x", "scale" : "3x",
"size" : "29x29" "size" : "29x29"
}, },
{ {
"filename" : "80.png",
"idiom" : "iphone", "idiom" : "iphone",
"scale" : "2x", "scale" : "2x",
"size" : "40x40" "size" : "40x40"
}, },
{ {
"filename" : "120.png",
"idiom" : "iphone", "idiom" : "iphone",
"scale" : "3x", "scale" : "3x",
"size" : "40x40" "size" : "40x40"
}, },
{ {
"filename" : "57.png",
"idiom" : "iphone",
"scale" : "1x",
"size" : "57x57"
},
{
"filename" : "114.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "57x57"
},
{
"filename" : "120.png",
"idiom" : "iphone", "idiom" : "iphone",
"scale" : "2x", "scale" : "2x",
"size" : "60x60" "size" : "60x60"
}, },
{ {
"filename" : "180.png",
"idiom" : "iphone", "idiom" : "iphone",
"scale" : "3x", "scale" : "3x",
"size" : "60x60" "size" : "60x60"
}, },
{ {
"filename" : "20.png",
"idiom" : "ipad", "idiom" : "ipad",
"scale" : "1x", "scale" : "1x",
"size" : "20x20" "size" : "20x20"
}, },
{ {
"filename" : "40.png",
"idiom" : "ipad", "idiom" : "ipad",
"scale" : "2x", "scale" : "2x",
"size" : "20x20" "size" : "20x20"
}, },
{ {
"filename" : "29.png",
"idiom" : "ipad", "idiom" : "ipad",
"scale" : "1x", "scale" : "1x",
"size" : "29x29" "size" : "29x29"
}, },
{ {
"filename" : "58.png",
"idiom" : "ipad", "idiom" : "ipad",
"scale" : "2x", "scale" : "2x",
"size" : "29x29" "size" : "29x29"
}, },
{ {
"filename" : "40.png",
"idiom" : "ipad", "idiom" : "ipad",
"scale" : "1x", "scale" : "1x",
"size" : "40x40" "size" : "40x40"
}, },
{ {
"filename" : "80.png",
"idiom" : "ipad", "idiom" : "ipad",
"scale" : "2x", "scale" : "2x",
"size" : "40x40" "size" : "40x40"
}, },
{ {
"filename" : "50.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "50x50"
},
{
"filename" : "100.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "50x50"
},
{
"filename" : "72.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "72x72"
},
{
"filename" : "144.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "72x72"
},
{
"filename" : "76.png",
"idiom" : "ipad", "idiom" : "ipad",
"scale" : "1x", "scale" : "1x",
"size" : "76x76" "size" : "76x76"
}, },
{ {
"filename" : "152.png",
"idiom" : "ipad", "idiom" : "ipad",
"scale" : "2x", "scale" : "2x",
"size" : "76x76" "size" : "76x76"
}, },
{ {
"filename" : "167.png",
"idiom" : "ipad", "idiom" : "ipad",
"scale" : "2x", "scale" : "2x",
"size" : "83.5x83.5" "size" : "83.5x83.5"
}, },
{ {
"filename" : "1024.png",
"idiom" : "ios-marketing", "idiom" : "ios-marketing",
"scale" : "1x", "scale" : "1x",
"size" : "1024x1024" "size" : "1024x1024"

57
AltBackup/BackupController.swift Executable file → Normal file
View File

@@ -26,60 +26,20 @@ extension Error
struct BackupError: ALTLocalizedError struct BackupError: ALTLocalizedError
{ {
enum Code: ALTErrorEnum, RawRepresentable enum Code
{ {
case invalidBundleID case invalidBundleID
case appGroupNotFound(String?) case appGroupNotFound(String?)
case randomError // Used for debugging. case randomError // Used for debugging.
// Provide failure reason for each error code
var errorFailureReason: String {
switch self {
case .invalidBundleID:
return NSLocalizedString("The bundle identifier is invalid.", comment: "")
case .appGroupNotFound(let appGroup):
if let appGroup = appGroup {
return String(format: NSLocalizedString("The app group “%@” could not be found.", comment: ""), appGroup)
} else {
return NSLocalizedString("The AltStore app group could not be found.", comment: "")
}
case .randomError:
return NSLocalizedString("A random error occurred.", comment: "")
}
}
static var errorDomain: String {
return "com.sidestore.BackupError"
}
// Add a raw value for RawRepresentable conformance
var rawValue: Int {
switch self {
case .invalidBundleID: return 0
case .appGroupNotFound: return 1
case .randomError: return 2
}
}
// Initializer for RawRepresentable
init?(rawValue: Int) {
switch rawValue {
case 0: self = .invalidBundleID
case 1: self = .appGroupNotFound(nil)
case 2: self = .randomError
default: return nil
}
}
} }
let code: Code let code: Code
let sourceFile: String let sourceFile: String
let sourceFileLine: Int let sourceFileLine: Int
var failure: String? var failure: String?
var errorTitle: String?
var errorFailure: String?
var failureReason: String? { var failureReason: String? {
switch self.code switch self.code
{ {
@@ -106,19 +66,12 @@ struct BackupError: ALTLocalizedError
return userInfo.compactMapValues { $0 } return userInfo.compactMapValues { $0 }
} }
// Implement description for CustomStringConvertible
var description: String {
return "\(errorTitle ?? "Unknown Error"): \(failureReason ?? "No reason available")"
}
init(_ code: Code, description: String? = nil, file: String = #file, line: Int = #line) init(_ code: Code, description: String? = nil, file: String = #file, line: Int = #line)
{ {
self.code = code self.code = code
self.failure = description self.failure = description
self.sourceFile = file self.sourceFile = file
self.sourceFileLine = line self.sourceFileLine = line
self.errorTitle = NSLocalizedString("Backup Error", comment: "")
self.errorFailure = description
} }
} }
@@ -143,9 +96,7 @@ class BackupController: NSObject
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 { else { throw BackupError(.appGroupNotFound(nil), description: NSLocalizedString("Unable to create backup directory.", comment: "")) }
throw BackupError(.appGroupNotFound(nil), description: NSLocalizedString("Unable to create backup directory.", comment: ""))
}
let backupsDirectory = sharedDirectoryURL.appendingPathComponent("Backups") let backupsDirectory = sharedDirectoryURL.appendingPathComponent("Backups")

View File

@@ -5,6 +5,7 @@
<key>ALTAppGroups</key> <key>ALTAppGroups</key>
<array> <array>
<string>group.$(APP_GROUP_IDENTIFIER)</string> <string>group.$(APP_GROUP_IDENTIFIER)</string>
<string>group.com.SideStore.SideStore</string>
</array> </array>
<key>ALTBundleIdentifier</key> <key>ALTBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
@@ -28,15 +29,15 @@
<key>CFBundleTypeRole</key> <key>CFBundleTypeRole</key>
<string>Editor</string> <string>Editor</string>
<key>CFBundleURLName</key> <key>CFBundleURLName</key>
<string>SideBackup General</string> <string>AltBackup General</string>
<key>CFBundleURLSchemes</key> <key>CFBundleURLSchemes</key>
<array> <array>
<string>sidebackup</string> <string>altbackup</string>
</array> </array>
</dict> </dict>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string> <string>1</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>

View File

@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>application-identifier</key>
<string>XYZ0123456.com.SideStore.SideStore.AltBackup</string>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.team-identifier</key>
<string>XYZ0123456</string>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.SideStore.SideStore</string>
</array>
<key>get-task-allow</key>
<true/>
</dict>
</plist>

View File

@@ -82,25 +82,23 @@ class ViewController: UIViewController
self.activityIndicatorView.color = .altstoreText self.activityIndicatorView.color = .altstoreText
self.activityIndicatorView.startAnimating() self.activityIndicatorView.startAnimating()
// TODO: @mahee96: Disabled these backup/restore buttons in altbackup.app screen which were present for debugging purpose. #if DEBUG
// Can find something useful for these later, but these are not required by this backup/restore app let button1 = UIButton(type: .system)
// #if DEBUG button1.setTitle("Backup", for: .normal)
// let button1 = UIButton(type: .system) button1.setTitleColor(.white, for: .normal)
// button1.setTitle("Backup", for: .normal) button1.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
// button1.setTitleColor(.white, for: .normal) button1.addTarget(self, action: #selector(ViewController.backup), for: .primaryActionTriggered)
// 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)
// let button2 = UIButton(type: .system) button2.setTitleColor(.white, for: .normal)
// button2.setTitle("Restore", for: .normal) button2.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
// button2.setTitleColor(.white, for: .normal) button2.addTarget(self, action: #selector(ViewController.restore), for: .primaryActionTriggered)
// 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!, button1, button2]
// #else
let arrangedSubviews = [self.textLabel!, self.detailTextLabel!, self.activityIndicatorView!] let arrangedSubviews = [self.textLabel!, self.detailTextLabel!, self.activityIndicatorView!]
// #endif #endif
let stackView = UIStackView(arrangedSubviews: arrangedSubviews) let stackView = UIStackView(arrangedSubviews: arrangedSubviews)
stackView.translatesAutoresizingMaskIntoConstraints = false stackView.translatesAutoresizingMaskIntoConstraints = false
@@ -157,8 +155,7 @@ private extension ViewController
self.textLabel.text = NSLocalizedString("Restoring app data…", comment: "") self.textLabel.text = NSLocalizedString("Restoring app data…", comment: "")
self.detailTextLabel.isHidden = true self.detailTextLabel.isHidden = true
self.activityIndicatorView.startAnimating() self.activityIndicatorView.startAnimating()
// TODO: @mahee96: This is pointless since, app going in bg/fg should still report its last operation properly
case .none: case .none:
self.textLabel.text = String(format: NSLocalizedString("%@ is inactive.", comment: ""), self.textLabel.text = String(format: NSLocalizedString("%@ is inactive.", comment: ""),
Bundle.main.appName ?? NSLocalizedString("App", comment: "")) Bundle.main.appName ?? NSLocalizedString("App", comment: ""))
@@ -201,9 +198,6 @@ private extension ViewController
} }
} }
// TODO: @mahee96: This doesn't account cases where backup is too long and user switched to other apps
// Now the user has lost his progress since current operation was cancelled due to switch between FG and BG
// if this just the reset for enum such that UI stops showing progress circle, then this is fine!
@objc func didEnterBackground(_ notification: Notification) @objc func didEnterBackground(_ notification: Notification)
{ {
// Reset UI once we've left app (but not before). // Reset UI once we've left app (but not before).

View File

@@ -0,0 +1,59 @@
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//
#import <Foundation/Foundation.h>
// Shared
#import "ALTConstants.h"
#import "ALTConnection.h"
#import "NSError+ALTServerError.h"
#import "CFNotificationName+AltStore.h"
// libproc
int proc_pidpath(int pid, void * buffer, uint32_t buffersize);
// Security.framework
CF_ENUM(uint32_t) {
kSecCSInternalInformation = 1 << 0,
kSecCSSigningInformation = 1 << 1,
kSecCSRequirementInformation = 1 << 2,
kSecCSDynamicInformation = 1 << 3,
kSecCSContentInformation = 1 << 4,
kSecCSSkipResourceDirectory = 1 << 5,
kSecCSCalculateCMSDigest = 1 << 6,
};
OSStatus SecStaticCodeCreateWithPath(CFURLRef path, uint32_t flags, void ** __nonnull CF_RETURNS_RETAINED staticCode);
OSStatus SecCodeCopySigningInformation(void *code, uint32_t flags, CFDictionaryRef * __nonnull CF_RETURNS_RETAINED information);
NS_ASSUME_NONNULL_BEGIN
@interface AKDevice : NSObject
@property (class, readonly) AKDevice *currentDevice;
@property (strong, readonly) NSString *serialNumber;
@property (strong, readonly) NSString *uniqueDeviceIdentifier;
@property (strong, readonly) NSString *serverFriendlyDescription;
@end
@interface AKAppleIDSession : NSObject
- (instancetype)initWithIdentifier:(NSString *)identifier;
- (NSDictionary<NSString *, NSString *> *)appleIDHeadersForRequest:(NSURLRequest *)request;
@end
@interface LSApplicationWorkspace : NSObject
@property (class, readonly) LSApplicationWorkspace *defaultWorkspace;
- (BOOL)installApplication:(NSURL *)fileURL withOptions:(nullable NSDictionary<NSString *, id> *)options error:(NSError *_Nullable *)error;
- (BOOL)uninstallApplication:(NSString *)bundleIdentifier withOptions:(nullable NSDictionary *)options;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>application-identifier</key>
<string>$(DEVELOPMENT_TEAM).$(ORG_IDENTIFIER).AltDaemon</string>
<key>get-task-allow</key>
<true/>
<key>platform-application</key>
<true/>
<key>com.apple.authkit.client.private</key>
<true/>
<key>com.apple.private.mobileinstall.allowedSPI</key>
<array>
<string>Install</string>
<string>Uninstall</string>
<string>InstallForLaunchServices</string>
<string>UninstallForLaunchServices</string>
<string>InstallLocalProvisioned</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,65 @@
//
// AnisetteDataManager.swift
// AltDaemon
//
// Created by Riley Testut on 6/1/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import AltSign
private extension UserDefaults
{
@objc var localUserID: String? {
get { return self.string(forKey: #keyPath(UserDefaults.localUserID)) }
set { self.set(newValue, forKey: #keyPath(UserDefaults.localUserID)) }
}
}
struct AnisetteDataManager
{
static let shared = AnisetteDataManager()
private let dateFormatter = ISO8601DateFormatter()
private init()
{
dlopen("/System/Library/PrivateFrameworks/AuthKit.framework/AuthKit", RTLD_NOW);
}
func requestAnisetteData() throws -> ALTAnisetteData
{
var request = URLRequest(url: URL(string: "https://developerservices2.apple.com/services/QH65B2/listTeams.action?clientId=XABBG36SBA")!)
request.httpMethod = "POST"
let akAppleIDSession = unsafeBitCast(NSClassFromString("AKAppleIDSession")!, to: AKAppleIDSession.Type.self)
let akDevice = unsafeBitCast(NSClassFromString("AKDevice")!, to: AKDevice.Type.self)
let session = akAppleIDSession.init(identifier: "com.apple.gs.xcode.auth")
let headers = session.appleIDHeaders(for: request)
let device = akDevice.current
let date = self.dateFormatter.date(from: headers["X-Apple-I-Client-Time"] ?? "") ?? Date()
var localUserID = UserDefaults.standard.localUserID
if localUserID == nil
{
localUserID = UUID().uuidString
UserDefaults.standard.localUserID = localUserID
}
let anisetteData = ALTAnisetteData(machineID: headers["X-Apple-I-MD-M"] ?? "",
oneTimePassword: headers["X-Apple-I-MD"] ?? "",
localUserID: headers["X-Apple-I-MD-LU"] ?? localUserID ?? "",
routingInfo: UInt64(headers["X-Apple-I-MD-RINFO"] ?? "") ?? 0,
deviceUniqueIdentifier: device.uniqueDeviceIdentifier,
deviceSerialNumber: device.serialNumber,
deviceDescription: "<MacBookPro15,1> <Mac OS X;10.15.2;19C57> <com.apple.AuthKit/1 (com.apple.dt.Xcode/3594.4.19)>",
date: date,
locale: .current,
timeZone: .current)
return anisetteData
}
}

138
AltDaemon/AppManager.swift Normal file
View File

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

View File

@@ -0,0 +1,123 @@
//
// DaemonRequestHandler.swift
// AltDaemon
//
// Created by Riley Testut on 6/1/20.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
typealias DaemonConnectionManager = ConnectionManager<DaemonRequestHandler>
private let connectionManager = ConnectionManager(requestHandler: DaemonRequestHandler(),
connectionHandlers: [XPCConnectionHandler()])
extension DaemonConnectionManager
{
static var shared: ConnectionManager {
return connectionManager
}
}
struct DaemonRequestHandler: RequestHandler
{
func handleAnisetteDataRequest(_ request: AnisetteDataRequest, for connection: Connection, completionHandler: @escaping (Result<AnisetteDataResponse, Error>) -> Void)
{
do
{
let anisetteData = try AnisetteDataManager.shared.requestAnisetteData()
let response = AnisetteDataResponse(anisetteData: anisetteData)
completionHandler(.success(response))
}
catch
{
completionHandler(.failure(error))
}
}
func handlePrepareAppRequest(_ request: PrepareAppRequest, for connection: Connection, completionHandler: @escaping (Result<InstallationProgressResponse, Error>) -> Void)
{
guard let fileURL = request.fileURL else { return completionHandler(.failure(ALTServerError(.invalidRequest))) }
print("Awaiting begin installation request...")
connection.receiveRequest() { (result) in
print("Received begin installation request with result:", result)
do
{
guard case .beginInstallation(let request) = try result.get() else { throw ALTServerError(.unknownRequest) }
guard let bundleIdentifier = request.bundleIdentifier else { throw ALTServerError(.invalidRequest) }
AppManager.shared.installApp(at: fileURL, bundleIdentifier: bundleIdentifier, activeProfiles: request.activeProfiles) { (result) in
let result = result.map { InstallationProgressResponse(progress: 1.0) }
print("Installed app with result:", result)
completionHandler(result)
}
}
catch
{
completionHandler(.failure(error))
}
}
}
func handleInstallProvisioningProfilesRequest(_ request: InstallProvisioningProfilesRequest, for connection: Connection,
completionHandler: @escaping (Result<InstallProvisioningProfilesResponse, Error>) -> Void)
{
AppManager.shared.install(request.provisioningProfiles, activeProfiles: request.activeProfiles) { (result) in
switch result
{
case .failure(let error):
print("Failed to install profiles \(request.provisioningProfiles.map { $0.bundleIdentifier }):", error)
completionHandler(.failure(error))
case .success:
print("Installed profiles:", request.provisioningProfiles.map { $0.bundleIdentifier })
let response = InstallProvisioningProfilesResponse()
completionHandler(.success(response))
}
}
}
func handleRemoveProvisioningProfilesRequest(_ request: RemoveProvisioningProfilesRequest, for connection: Connection,
completionHandler: @escaping (Result<RemoveProvisioningProfilesResponse, Error>) -> Void)
{
AppManager.shared.removeProvisioningProfiles(forBundleIdentifiers: request.bundleIdentifiers) { (result) in
switch result
{
case .failure(let error):
print("Failed to remove profiles \(request.bundleIdentifiers):", error)
completionHandler(.failure(error))
case .success:
print("Removed profiles:", request.bundleIdentifiers)
let response = RemoveProvisioningProfilesResponse()
completionHandler(.success(response))
}
}
}
func handleRemoveAppRequest(_ request: RemoveAppRequest, for connection: Connection, completionHandler: @escaping (Result<RemoveAppResponse, Error>) -> Void)
{
AppManager.shared.removeApp(forBundleIdentifier: request.bundleIdentifier) { (result) in
switch result
{
case .failure(let error):
print("Failed to remove app \(request.bundleIdentifier):", error)
completionHandler(.failure(error))
case .success:
print("Removed app:", request.bundleIdentifier)
let response = RemoveAppResponse()
completionHandler(.success(response))
}
}
}
}

View File

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

14
AltDaemon/main.swift Normal file
View File

@@ -0,0 +1,14 @@
//
// main.swift
// AltDaemon
//
// Created by Riley Testut on 6/2/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
autoreleasepool {
DaemonConnectionManager.shared.start()
RunLoop.current.run()
}

View File

@@ -0,0 +1,10 @@
Package: com.rileytestut.altdaemon
Name: AltDaemon
Depends:
Version: 1.0
Architecture: iphoneos-arm
Description: AltDaemon allows AltStore to install and refresh apps without a computer.
Maintainer: Riley Testut
Author: Riley Testut
Homepage: https://altstore.io
Section: System

View File

@@ -0,0 +1,2 @@
#!/bin/sh
launchctl load /Library/LaunchDaemons/com.rileytestut.altdaemon.plist

View File

@@ -0,0 +1,2 @@
#!/bin/sh
launchctl unload /Library/LaunchDaemons/com.rileytestut.altdaemon.plist >> /dev/null 2>&1

2
AltDaemon/package/DEBIAN/prerm Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/sh
launchctl unload /Library/LaunchDaemons/com.rileytestut.altdaemon.plist

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.rileytestut.altdaemon</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/env</string>
<string>_MSSafeMode=1</string>
<string>_SafeMode=1</string>
<string>/usr/bin/AltDaemon</string>
</array>
<key>UserName</key>
<string>mobile</string>
<key>KeepAlive</key>
<false/>
<key>RunAtLoad</key>
<false/>
<key>MachServices</key>
<dict>
<key>cy:io.altstore.altdaemon</key>
<true/>
<key>lh:io.altstore.altdaemon</key>
<true/>
</dict>
</dict>
</plist>

Binary file not shown.

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,167 @@
{
"pins" : [
{
"identity" : "altsign",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SideStore/AltSign",
"state" : {
"branch" : "master",
"revision" : "7e0e7edcf8fbc44ac1e35da3e9030a297aa18b84"
}
},
{
"identity" : "appcenter-sdk-apple",
"kind" : "remoteSourceControl",
"location" : "https://github.com/microsoft/appcenter-sdk-apple.git",
"state" : {
"revision" : "8354a50fe01a7e54e196d3b5493b5ab53dd5866a",
"version" : "4.4.2"
}
},
{
"identity" : "asyncimage",
"kind" : "remoteSourceControl",
"location" : "https://github.com/fabianthdev/AsyncImage",
"state" : {
"branch" : "main",
"revision" : "018a4fffea025066d795ebb025c2769183f3fffb"
}
},
{
"identity" : "expandabletext",
"kind" : "remoteSourceControl",
"location" : "https://github.com/fabianthdev/ExpandableText",
"state" : {
"branch" : "main",
"revision" : "a375f5b8c73f0af69aa7add890378fdf404a29bc"
}
},
{
"identity" : "inject",
"kind" : "remoteSourceControl",
"location" : "https://github.com/krzysztofzablocki/Inject.git",
"state" : {
"revision" : "abcc4b091fd384cfd09b149a60298b75dc87c5b9",
"version" : "1.2.3"
}
},
{
"identity" : "keychainaccess",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kishikawakatsumi/KeychainAccess.git",
"state" : {
"revision" : "84e546727d66f1adc5439debad16270d0fdd04e7",
"version" : "4.2.2"
}
},
{
"identity" : "launchatlogin",
"kind" : "remoteSourceControl",
"location" : "https://github.com/sindresorhus/LaunchAtLogin.git",
"state" : {
"revision" : "e8171b3e38a2816f579f58f3dac1522aa39efe41",
"version" : "4.2.0"
}
},
{
"identity" : "localconsole",
"kind" : "remoteSourceControl",
"location" : "https://github.com/naturecodevoid/LocalConsole.git",
"state" : {
"branch" : "main",
"revision" : "4ead9c3e565190172caac62b5179347e02999365"
}
},
{
"identity" : "nuke",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kean/Nuke.git",
"state" : {
"revision" : "9318d02a8a6d20af56505c9673261c1fd3b3aebe",
"version" : "7.6.3"
}
},
{
"identity" : "openssl",
"kind" : "remoteSourceControl",
"location" : "https://github.com/krzyzanowskim/OpenSSL",
"state" : {
"revision" : "033fcb41dac96b1b6effa945ca1f9ade002370b2",
"version" : "1.1.1501"
}
},
{
"identity" : "plcrashreporter",
"kind" : "remoteSourceControl",
"location" : "https://github.com/microsoft/PLCrashReporter.git",
"state" : {
"revision" : "6b27393cad517c067dceea85fadf050e70c4ceaa",
"version" : "1.10.1"
}
},
{
"identity" : "reachability.swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ashleymills/Reachability.swift",
"state" : {
"branch" : "master",
"revision" : "a81b7367f2c46875f29577e03a60c39cdfad0c8d"
}
},
{
"identity" : "semanticversion",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SwiftPackageIndex/SemanticVersion.git",
"state" : {
"revision" : "fc670910dc0903cc269b3d0b776cda5703979c4e",
"version" : "0.3.5"
}
},
{
"identity" : "sfsafesymbols",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SFSafeSymbols/SFSafeSymbols",
"state" : {
"revision" : "50bc33264e6c0972f905b61af656201cf6091de8",
"version" : "4.0.0"
}
},
{
"identity" : "sparkle",
"kind" : "remoteSourceControl",
"location" : "https://github.com/sparkle-project/Sparkle.git",
"state" : {
"revision" : "286edd1fa22505a9e54d170e9fd07d775ea233f2",
"version" : "2.1.0"
}
},
{
"identity" : "starscream",
"kind" : "remoteSourceControl",
"location" : "https://github.com/daltoniam/Starscream.git",
"state" : {
"revision" : "df8d82047f6654d8e4b655d1b1525c64e1059d21",
"version" : "4.0.4"
}
},
{
"identity" : "stprivilegedtask",
"kind" : "remoteSourceControl",
"location" : "https://github.com/JoeMatt/STPrivilegedTask.git",
"state" : {
"branch" : "master",
"revision" : "10a9150ef32d444af326beba76356ae9af95a3e7"
}
},
{
"identity" : "zipfoundation",
"kind" : "remoteSourceControl",
"location" : "https://github.com/weichsel/ZIPFoundation.git",
"state" : {
"revision" : "43ec568034b3731101dbf7670765d671c30f54f3",
"version" : "0.9.16"
}
}
],
"version" : 2
}

View File

@@ -0,0 +1,111 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1150"
version = "1.3">
<BuildAction
parallelizeBuildables = "NO"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A7A6DC28A6D60809855FE404C6A3EA29"
BuildableName = "libPods-AltDaemon.a"
BlueprintName = "Pods-AltDaemon"
ReferencedContainer = "container:Pods/Pods.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BF1E314F22A0616100370A3C"
BuildableName = "libAltKit.a"
BlueprintName = "AltKit"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BF18BFE624857D7900DD5981"
BuildableName = "AltDaemon"
BlueprintName = "AltDaemon"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Release"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BF18BFE624857D7900DD5981"
BuildableName = "AltDaemon"
BlueprintName = "AltDaemon"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</MacroExpansion>
<EnvironmentVariables>
<EnvironmentVariable
key = "THEOS"
value = "~/theos"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BF18BFE624857D7900DD5981"
BuildableName = "AltDaemon"
BlueprintName = "AltDaemon"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -1,11 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "2610" LastUpgradeVersion = "1120"
version = "1.7"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES" buildImplicitDependencies = "YES">
buildArchitectures = "Automatic">
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry <BuildActionEntry
buildForTesting = "YES" buildForTesting = "YES"
@@ -15,10 +14,10 @@
buildForAnalyzing = "YES"> buildForAnalyzing = "YES">
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "CA60C44C93D7A30E3695DD59" BlueprintIdentifier = "BF5C5FC4237DF5AE00EDD0C6"
BuildableName = "libem_proxy_static.a" BuildableName = "AltPlugin.mailbundle"
BlueprintName = "em_proxy-staticlib" BlueprintName = "AltPlugin"
ReferencedContainer = "container:em_proxy.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildActionEntry> </BuildActionEntry>
</BuildActionEntries> </BuildActionEntries>
@@ -27,14 +26,15 @@
buildConfiguration = "Debug" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES" shouldUseLaunchSchemeArgsEnv = "YES">
shouldAutocreateTestPlan = "YES"> <Testables>
</Testables>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0" launchStyle = "1"
useCustomWorkingDirectory = "NO" useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO" ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES" debugDocumentVersioning = "YES"
@@ -50,10 +50,10 @@
<MacroExpansion> <MacroExpansion>
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "CA60C44C93D7A30E3695DD59" BlueprintIdentifier = "BF5C5FC4237DF5AE00EDD0C6"
BuildableName = "libem_proxy_static.a" BuildableName = "AltPlugin.mailbundle"
BlueprintName = "em_proxy-staticlib" BlueprintName = "AltPlugin"
ReferencedContainer = "container:em_proxy.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</MacroExpansion> </MacroExpansion>
</ProfileAction> </ProfileAction>

View File

@@ -1,11 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1610" LastUpgradeVersion = "1020"
version = "1.7"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES" buildImplicitDependencies = "YES">
buildArchitectures = "Automatic">
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry <BuildActionEntry
buildForTesting = "YES" buildForTesting = "YES"
@@ -15,9 +14,9 @@
buildForAnalyzing = "YES"> buildForAnalyzing = "YES">
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "BF58047A246A28F7008AE704" BlueprintIdentifier = "BF45868C229872EA00BD7491"
BuildableName = "AltBackup.app" BuildableName = "AltServer.app"
BlueprintName = "AltBackup" BlueprintName = "AltServer"
ReferencedContainer = "container:AltStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildActionEntry> </BuildActionEntry>
@@ -27,8 +26,20 @@
buildConfiguration = "Debug" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES" shouldUseLaunchSchemeArgsEnv = "YES">
shouldAutocreateTestPlan = "YES"> <Testables>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BF45868C229872EA00BD7491"
BuildableName = "AltServer.app"
BlueprintName = "AltServer"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Debug"
@@ -44,42 +55,14 @@
runnableDebuggingMode = "0"> runnableDebuggingMode = "0">
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "BF58047A246A28F7008AE704" BlueprintIdentifier = "BF45868C229872EA00BD7491"
BuildableName = "AltBackup.app" BuildableName = "AltServer.app"
BlueprintName = "AltBackup" BlueprintName = "AltServer"
ReferencedContainer = "container:AltStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<CommandLineArguments> <AdditionalOptions>
<CommandLineArgument </AdditionalOptions>
argument = "-com.apple.CoreData.ConcurrencyDebug 1"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "-com.apple.CoreData.MigrationDebug 1"
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
argument = "-com.apple.CoreData.SQLiteIntegrityCheck 1"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "-com.apple.CoreData.SQLDebug 1"
isEnabled = "NO">
</CommandLineArgument>
</CommandLineArguments>
<EnvironmentVariables>
<EnvironmentVariable
key = "OS_ACTIVITY_MODE"
value = "$(DEBUG_ACTIVITY_MODE)"
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable
key = "OBJC_DEBUG_DUPLICATE_CLASSES"
value = "$(DEBUG_DUPLICATE_CLASSES)"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release"
@@ -91,9 +74,9 @@
runnableDebuggingMode = "0"> runnableDebuggingMode = "0">
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "BF58047A246A28F7008AE704" BlueprintIdentifier = "BF45868C229872EA00BD7491"
BuildableName = "AltBackup.app" BuildableName = "AltServer.app"
BlueprintName = "AltBackup" BlueprintName = "AltServer"
ReferencedContainer = "container:AltStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1020" LastUpgradeVersion = "1020"
version = "1.7"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"> buildImplicitDependencies = "YES">
@@ -26,8 +26,9 @@
buildConfiguration = "Release" buildConfiguration = "Release"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES" shouldUseLaunchSchemeArgsEnv = "YES">
shouldAutocreateTestPlan = "YES"> <Testables>
</Testables>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Release" buildConfiguration = "Release"
@@ -52,33 +53,9 @@
<CommandLineArguments> <CommandLineArguments>
<CommandLineArgument <CommandLineArgument
argument = "-com.apple.CoreData.ConcurrencyDebug 1" argument = "-com.apple.CoreData.ConcurrencyDebug 1"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "-com.apple.CoreData.MigrationDebug 1"
isEnabled = "YES"> isEnabled = "YES">
</CommandLineArgument> </CommandLineArgument>
<CommandLineArgument
argument = "-com.apple.CoreData.SQLiteIntegrityCheck 1"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "-com.apple.CoreData.SQLDebug 1"
isEnabled = "NO">
</CommandLineArgument>
</CommandLineArguments> </CommandLineArguments>
<EnvironmentVariables>
<EnvironmentVariable
key = "OS_ACTIVITY_MODE"
value = "$(DEBUG_ACTIVITY_MODE)"
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable
key = "OBJC_DEBUG_DUPLICATE_CLASSES"
value = "$(DEBUG_DUPLICATE_CLASSES)"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release"

View File

@@ -1,11 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1610" LastUpgradeVersion = "1020"
version = "1.7"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES" buildImplicitDependencies = "YES">
buildArchitectures = "Automatic">
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry <BuildActionEntry
buildForTesting = "YES" buildForTesting = "YES"
@@ -28,28 +27,7 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"> shouldUseLaunchSchemeArgsEnv = "YES">
<TestPlans>
<TestPlanReference
reference = "container:SideStore/Tests/SideStoreTests.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
<Testables> <Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A8E2DB202D684CBD009E5D31"
BuildableName = "UITests.xctest"
BlueprintName = "UITests"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
<SelectedTests>
<Test
Identifier = "UITests/testExample()">
</Test>
</SelectedTests>
</TestableReference>
</Testables> </Testables>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
@@ -75,33 +53,9 @@
<CommandLineArguments> <CommandLineArguments>
<CommandLineArgument <CommandLineArgument
argument = "-com.apple.CoreData.ConcurrencyDebug 1" argument = "-com.apple.CoreData.ConcurrencyDebug 1"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "-com.apple.CoreData.MigrationDebug 1"
isEnabled = "YES"> isEnabled = "YES">
</CommandLineArgument> </CommandLineArgument>
<CommandLineArgument
argument = "-com.apple.CoreData.SQLiteIntegrityCheck 1"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "-com.apple.CoreData.SQLDebug 1"
isEnabled = "NO">
</CommandLineArgument>
</CommandLineArguments> </CommandLineArguments>
<EnvironmentVariables>
<EnvironmentVariable
key = "OS_ACTIVITY_MODE"
value = "$(DEBUG_ACTIVITY_MODE)"
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable
key = "OBJC_DEBUG_DUPLICATE_CLASSES"
value = "$(DEBUG_DUPLICATE_CLASSES)"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release"

View File

@@ -1,11 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "2610" LastUpgradeVersion = "1230"
version = "1.7"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES" buildImplicitDependencies = "YES">
buildArchitectures = "Automatic">
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry <BuildActionEntry
buildForTesting = "YES" buildForTesting = "YES"
@@ -15,10 +14,10 @@
buildForAnalyzing = "YES"> buildForAnalyzing = "YES">
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "CA609C732349A560B9642892" BlueprintIdentifier = "BFF7C903257844C900E55F36"
BuildableName = "libminimuxer_static.a" BuildableName = "AltXPC.xpc"
BlueprintName = "minimuxer-staticlib" BlueprintName = "AltXPC"
ReferencedContainer = "container:minimuxer.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildActionEntry> </BuildActionEntry>
</BuildActionEntries> </BuildActionEntries>
@@ -27,8 +26,9 @@
buildConfiguration = "Debug" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES" shouldUseLaunchSchemeArgsEnv = "YES">
shouldAutocreateTestPlan = "YES"> <Testables>
</Testables>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Debug"
@@ -40,6 +40,16 @@
debugDocumentVersioning = "YES" debugDocumentVersioning = "YES"
debugServiceExtension = "internal" debugServiceExtension = "internal"
allowLocationSimulation = "YES"> allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BF45868C229872EA00BD7491"
BuildableName = "AltServer.app"
BlueprintName = "AltServer"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release"
@@ -50,10 +60,10 @@
<MacroExpansion> <MacroExpansion>
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "CA609C732349A560B9642892" BlueprintIdentifier = "BFF7C903257844C900E55F36"
BuildableName = "libminimuxer_static.a" BuildableName = "AltXPC.xpc"
BlueprintName = "minimuxer-staticlib" BlueprintName = "AltXPC"
ReferencedContainer = "container:minimuxer.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</MacroExpansion> </MacroExpansion>
</ProfileAction> </ProfileAction>

View File

@@ -1,60 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1620"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<TestPlans>
<TestPlanReference
reference = "container:SideStore/Tests/DataStructureTests.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A81A8CC42D68BA610086C96F"
BuildableName = "DataStructureTests.xctest"
BlueprintName = "DataStructureTests"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:AltStore.xcodeproj">
</FileRef>
<FileRef
location = "group:SideStore/AltSign">
</FileRef>
<FileRef
location = "group:Dependencies/Roxas/Roxas.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -6,3 +6,7 @@
#import "ALTAppPatcher.h" #import "ALTAppPatcher.h"
#include "fragmentzip.h" #include "fragmentzip.h"
#ifdef MDC
#import "grant_full_disk_access.h"
#endif /* MDC */

View File

@@ -4,11 +4,7 @@
<dict> <dict>
<key>aps-environment</key> <key>aps-environment</key>
<string>development</string> <string>development</string>
<key>com.apple.developer.kernel.extended-virtual-addressing</key> <key>com.apple.developer.siri</key>
<true/>
<key>com.apple.developer.kernel.increased-debugging-memory-limit</key>
<true/>
<key>com.apple.developer.kernel.increased-memory-limit</key>
<true/> <true/>
<key>com.apple.security.application-groups</key> <key>com.apple.security.application-groups</key>
<array> <array>

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.$(APP_GROUP_IDENTIFIER)</string>
</array>
</dict>
</plist>

View File

@@ -14,13 +14,7 @@ import AppCenter
import AppCenterAnalytics import AppCenterAnalytics
import AppCenterCrashes import AppCenterCrashes
#if DEBUG
private let appCenterAppSecret = "73532d3e-e573-4693-99a4-9f85840bbb44" private let appCenterAppSecret = "73532d3e-e573-4693-99a4-9f85840bbb44"
#elseif RELEASE
private let appCenterAppSecret = "73532d3e-e573-4693-99a4-9f85840bbb44"
#else
private let appCenterAppSecret = "73532d3e-e573-4693-99a4-9f85840bbb44"
#endif
extension AnalyticsManager extension AnalyticsManager
{ {
@@ -30,14 +24,10 @@ extension AnalyticsManager
case bundleIdentifier case bundleIdentifier
case developerName case developerName
case version case version
case buildVersion
case size case size
case tintColor case tintColor
case sourceIdentifier case sourceIdentifier
case sourceURL case sourceURL
case patreonURL
case pledgeAmount
case pledgeCurrency
} }
enum Event enum Event
@@ -69,14 +59,10 @@ extension AnalyticsManager
.bundleIdentifier: app.bundleIdentifier, .bundleIdentifier: app.bundleIdentifier,
.developerName: app.storeApp?.developerName, .developerName: app.storeApp?.developerName,
.version: app.version, .version: app.version,
.buildVersion: app.buildVersion,
.size: appBundleSize?.description, .size: appBundleSize?.description,
.tintColor: app.storeApp?.tintColor?.hexString, .tintColor: app.storeApp?.tintColor?.hexString,
.sourceIdentifier: app.storeApp?.sourceIdentifier, .sourceIdentifier: app.storeApp?.sourceIdentifier,
.sourceURL: app.storeApp?.source?.sourceURL.absoluteString, .sourceURL: app.storeApp?.source?.sourceURL.absoluteString
.patreonURL: app.storeApp?.source?.patreonURL?.absoluteString,
.pledgeAmount: app.storeApp?.pledgeAmount?.description,
.pledgeCurrency: app.storeApp?.pledgeCurrency
] ]
} }

View File

@@ -1,203 +0,0 @@
//
// AppContentViewController.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
extension AppContentViewController
{
private enum Row: Int, CaseIterable
{
case subtitle
case screenshots
case description
case versionDescription
case permissions
}
}
final class AppContentViewController: UITableViewController
{
var app: StoreApp!
// private lazy var screenshotsDataSource = self.makeScreenshotsDataSource()
private lazy var dateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium
dateFormatter.timeStyle = .none
return dateFormatter
}()
private lazy var byteCountFormatter: ByteCountFormatter = {
let formatter = ByteCountFormatter()
return formatter
}()
@IBOutlet private var subtitleLabel: UILabel!
// @IBOutlet private var descriptionTextView: CollapsingTextView!
@IBOutlet private var descriptionTextView: CollapsingMarkdownView!
// @IBOutlet private var versionDescriptionTextView: CollapsingTextView!
@IBOutlet private var versionDescriptionTextView: CollapsingMarkdownView!
@IBOutlet private var versionLabel: UILabel!
@IBOutlet private var versionDateLabel: UILabel!
@IBOutlet private var sizeLabel: UILabel!
@IBOutlet private(set) var appScreenshotsViewController: AppScreenshotsViewController!
@IBOutlet private var appScreenshotsHeightConstraint: NSLayoutConstraint!
@IBOutlet private(set) var appDetailCollectionViewController: AppDetailCollectionViewController!
@IBOutlet private var appDetailCollectionViewHeightConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.contentInset.bottom = 20
self.subtitleLabel.text = self.app.subtitle
let desc = self.app.localizedDescription
self.descriptionTextView.text = desc
if let version = self.app.latestAvailableVersion {
self.versionDescriptionTextView.text = version.localizedDescription ?? "nil"
self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), version.localizedVersion)
self.versionDateLabel.text = Date().relativeDateString(since: version.date)
self.sizeLabel.text = ByteCountFormatter.string(fromByteCount: version.size, countStyle: .file)
} else {
self.versionDescriptionTextView.text = "nil"
self.versionLabel.text = nil
self.versionDateLabel.text = nil
self.sizeLabel.text = ByteCountFormatter.string(fromByteCount: 0, countStyle: .file)
}
self.descriptionTextView.maximumNumberOfLines = 5
self.versionDescriptionTextView.maximumNumberOfLines = 5
self.descriptionTextView.toggleButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
self.versionDescriptionTextView.toggleButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
}
override func viewDidLayoutSubviews()
{
super.viewDidLayoutSubviews()
var needsTableViewUpdate = false
let screenshotsHeight = self.appScreenshotsViewController.collectionView.contentSize.height
if self.appScreenshotsHeightConstraint.constant != screenshotsHeight && screenshotsHeight > 0
{
self.appScreenshotsHeightConstraint.constant = screenshotsHeight
needsTableViewUpdate = true
}
let permissionsHeight = self.appDetailCollectionViewController.collectionView.contentSize.height
if self.appDetailCollectionViewHeightConstraint.constant != permissionsHeight && permissionsHeight > 0
{
self.appDetailCollectionViewHeightConstraint.constant = permissionsHeight
needsTableViewUpdate = true
}
if needsTableViewUpdate
{
UIView.performWithoutAnimation {
// Update row height without animation.
self.tableView.beginUpdates()
self.tableView.endUpdates()
}
}
}
}
private extension AppContentViewController
{
@IBSegueAction
func makeAppScreenshotsViewController(_ coder: NSCoder, sender: Any?) -> UIViewController?
{
let appScreenshotsViewController = AppScreenshotsViewController(app: self.app, coder: coder)
self.appScreenshotsViewController = appScreenshotsViewController
return appScreenshotsViewController
}
func makePermissionsDataSource() -> RSTArrayCollectionViewDataSource<AppPermission>
{
let dataSource = RSTArrayCollectionViewDataSource(items: Array(self.app.permissions))
dataSource.cellConfigurationHandler = { (cell, permission, indexPath) in
let cell = cell as! PermissionCollectionViewCell
// cell.button.setImage(permission.type.icon, for: .normal)
// cell.button.tintColor = .label
// cell.textLabel.text = permission.type.localizedShortName ?? permission.type.localizedName
let icon = UIImage(systemName: permission.symbolName ?? "lock")
cell.button.setImage(icon, for: .normal)
cell.textLabel.text = permission.localizedDisplayName
}
return dataSource
}
@IBSegueAction
func makeAppDetailCollectionViewController(_ coder: NSCoder, sender: Any?) -> UIViewController?
{
let appDetailViewController = AppDetailCollectionViewController(app: self.app, coder: coder)
self.appDetailCollectionViewController = appDetailViewController
return appDetailViewController
}
}
private extension AppContentViewController
{
@objc func toggleCollapsingSection(_ sender: UIButton)
{
let indexPath: IndexPath
switch sender
{
case self.descriptionTextView.toggleButton:
indexPath = IndexPath(row: Row.description.rawValue, section: 0)
case self.versionDescriptionTextView.toggleButton:
indexPath = IndexPath(row: Row.versionDescription.rawValue, section: 0)
default: return
}
// Disable animations to prevent some potentially strange ones.
UIView.performWithoutAnimation {
self.tableView.reloadRows(at: [indexPath], with: .none)
}
}
}
extension AppContentViewController
{
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
{
switch Row.allCases[indexPath.row]
{
case .screenshots:
guard !self.app.allScreenshots.isEmpty else { return 0.0 }
return UITableView.automaticDimension
case .permissions:
guard !self.app.permissions.isEmpty else { return 0.0 }
return UITableView.automaticDimension
default:
return super.tableView(tableView, heightForRowAt: indexPath)
}
}
}

View File

@@ -1,300 +0,0 @@
//
// AppDetailCollectionViewController.swift
// AltStore
//
// Created by Riley Testut on 5/5/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import UIKit
import SwiftUI
import AltStoreCore
import Roxas
extension AppDetailCollectionViewController
{
private enum Section: Int
{
case privacy
case knownEntitlements
case unknownEntitlements
}
private enum ElementKind: String
{
case title
case button
}
@objc(SafeAreaIgnoringCollectionView)
private class SafeAreaIgnoringCollectionView: UICollectionView
{
override var safeAreaInsets: UIEdgeInsets {
get {
// Fixes incorrect layout if collection view height is taller than safe area height.
return .zero
}
set {
// There MUST be a setter for this to work, even if it does nothing ¯\_()_/¯
}
}
}
}
class AppDetailCollectionViewController: UICollectionViewController
{
let app: StoreApp
private let privacyPermissions: [AppPermission]
private let knownEntitlementPermissions: [AppPermission]
private let unknownEntitlementPermissions: [AppPermission]
private lazy var dataSource = self.makeDataSource()
private lazy var privacyDataSource = self.makePrivacyDataSource()
private lazy var entitlementsDataSource = self.makeEntitlementsDataSource()
private var headerRegistration: UICollectionView.SupplementaryRegistration<UICollectionViewListCell>!
override var collectionViewLayout: UICollectionViewCompositionalLayout {
return self.collectionView.collectionViewLayout as! UICollectionViewCompositionalLayout
}
init?(app: StoreApp, coder: NSCoder)
{
self.app = app
let comparator: (AppPermission, AppPermission) -> Bool = { (permissionA, permissionB) -> Bool in
switch (permissionA.localizedName, permissionB.localizedName)
{
case (let nameA?, let nameB?):
// Sort by localizedName, if both have one.
return nameA.localizedStandardCompare(nameB) == .orderedAscending
case (nil, nil):
// Sort by raw permission value as fallback.
return permissionA.permission.rawValue < permissionB.permission.rawValue
// Sort "known" permissions before "unknown" ones.
case (_?, nil): return true
case (nil, _?): return false
}
}
self.privacyPermissions = app.permissions.filter { $0.type == .privacy }.sorted(by: comparator)
let entitlementPermissions = app.permissions.lazy.filter { $0.type == .entitlement }
self.knownEntitlementPermissions = entitlementPermissions.filter { $0.isKnown }.sorted(by: comparator)
self.unknownEntitlementPermissions = entitlementPermissions.filter { !$0.isKnown }.sorted(by: comparator)
super.init(coder: coder)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad()
{
super.viewDidLoad()
// Allow parent background color to show through.
self.collectionView.backgroundColor = nil
// Match the parent table view margins.
self.collectionView.directionalLayoutMargins.leading = 20
self.collectionView.directionalLayoutMargins.trailing = 20
let collectionViewLayout = self.makeLayout()
self.collectionView.collectionViewLayout = collectionViewLayout
self.collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "PrivacyCell")
self.collectionView.register(UICollectionViewListCell.self, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
self.headerRegistration = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) { [weak self] (headerView, elementKind, indexPath) in
var configuration = UIListContentConfiguration.plainHeader()
// Match parent table view section headers.
configuration.textProperties.font = UIFont.systemFont(ofSize: 22, weight: .bold) // .boldSystemFont(ofSize:) returns *semi-bold* color smh.
configuration.textProperties.color = .label
switch Section(rawValue: indexPath.section)!
{
case .privacy: break
case .knownEntitlements:
configuration.text = nil
configuration.secondaryTextProperties.font = UIFont.preferredFont(forTextStyle: .callout)
configuration.textToSecondaryTextVerticalPadding = 8
configuration.secondaryText = NSLocalizedString("Entitlements are additional permissions that grant access to certain system services, including potentially sensitive information.", comment: "")
case .unknownEntitlements:
configuration.text = NSLocalizedString("Other Entitlements", comment: "")
let action = UIAction(image: UIImage(systemName: "questionmark.circle")) { _ in
self?.showUnknownEntitlementsAlert()
}
let helpButton = UIButton(primaryAction: action)
let customAccessory = UICellAccessory.customView(configuration: .init(customView: helpButton, placement: .trailing(), tintColor: self?.app.tintColor ?? .altPrimary))
headerView.accessories = [customAccessory]
}
headerView.contentConfiguration = configuration
headerView.backgroundConfiguration = UIBackgroundConfiguration.clear()
}
self.dataSource.proxy = self
self.collectionView.dataSource = self.dataSource
self.collectionView.delegate = self
}
}
private extension AppDetailCollectionViewController
{
func makeLayout() -> UICollectionViewCompositionalLayout
{
let layoutConfig = UICollectionViewCompositionalLayoutConfiguration()
layoutConfig.contentInsetsReference = .layoutMargins
let layout = UICollectionViewCompositionalLayout(sectionProvider: { [privacyPermissions, knownEntitlementPermissions, unknownEntitlementPermissions] (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
guard let section = Section(rawValue: sectionIndex) else { return nil }
switch section
{
case .privacy:
guard !privacyPermissions.isEmpty, #available(iOS 16, *) else { return nil } // Hide section pre-iOS 16.
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(50)) // Underestimate height to prevent jumping size abruptly.
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(50))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let layoutSection = NSCollectionLayoutSection(group: group)
layoutSection.interGroupSpacing = 10
return layoutSection
case .knownEntitlements where !knownEntitlementPermissions.isEmpty: fallthrough
case .unknownEntitlements where !unknownEntitlementPermissions.isEmpty:
var configuration = UICollectionLayoutListConfiguration(appearance: .plain)
configuration.headerMode = .supplementary
configuration.showsSeparators = false
configuration.backgroundColor = .altBackground
let layoutSection = NSCollectionLayoutSection.list(using: configuration, layoutEnvironment: layoutEnvironment)
layoutSection.contentInsets.top = 4
return layoutSection
case .knownEntitlements, .unknownEntitlements: return nil
}
}, configuration: layoutConfig)
return layout
}
func makeDataSource() -> RSTCompositeCollectionViewDataSource<AppPermission>
{
let dataSource = RSTCompositeCollectionViewDataSource(dataSources: [self.privacyDataSource, self.entitlementsDataSource])
return dataSource
}
func makePrivacyDataSource() -> RSTDynamicCollectionViewDataSource<AppPermission>
{
let dataSource = RSTDynamicCollectionViewDataSource<AppPermission>()
dataSource.cellIdentifierHandler = { _ in "PrivacyCell" }
dataSource.numberOfSectionsHandler = { 1 }
dataSource.cellConfigurationHandler = { [weak self] (cell, _, indexPath) in
guard let self, #available(iOS 16, *) else { return }
cell.contentConfiguration = UIHostingConfiguration {
AppPermissionsCard(title: "Privacy",
description: "\(self.app.name) may request access to the following:",
tintColor: Color(uiColor: self.app.tintColor ?? .altPrimary),
permissions: self.privacyPermissions)
}
.margins(.horizontal, 0)
}
if #available(iOS 16, *)
{
dataSource.numberOfItemsHandler = { [privacyPermissions] _ in !privacyPermissions.isEmpty ? 1 : 0 }
}
else
{
dataSource.numberOfItemsHandler = { _ in 0 }
}
return dataSource
}
func makeEntitlementsDataSource() -> RSTCompositeCollectionViewDataSource<AppPermission>
{
let knownEntitlementsDataSource = RSTArrayCollectionViewDataSource(items: self.knownEntitlementPermissions)
let unknownEntitlementsDataSource = RSTArrayCollectionViewDataSource(items: self.unknownEntitlementPermissions)
let dataSource = RSTCompositeCollectionViewDataSource(dataSources: [knownEntitlementsDataSource, unknownEntitlementsDataSource])
dataSource.cellConfigurationHandler = { [weak self] (cell, appPermission, _) in
let cell = cell as! UICollectionViewListCell
let tintColor = self?.app.tintColor ?? .altPrimary
var content = cell.defaultContentConfiguration()
content.text = appPermission.localizedDisplayName
content.secondaryText = appPermission.permission.rawValue
content.secondaryTextProperties.color = .secondaryLabel
if appPermission.isKnown
{
content.image = UIImage(systemName: appPermission.effectiveSymbolName)
content.imageProperties.tintColor = tintColor
if #available(iOS 15.4, *) /*, let self */ // Capturing self leads to strong-reference cycle.
{
let detailAccessory = UICellAccessory.detail(options: .init(tintColor: tintColor)) {
self?.showPermissionAlert(for: appPermission)
}
cell.accessories = [detailAccessory]
}
}
cell.contentConfiguration = content
cell.backgroundConfiguration = UIBackgroundConfiguration.clear()
}
return dataSource
}
}
private extension AppDetailCollectionViewController
{
func showPermissionAlert(for permission: AppPermission)
{
let alertController = UIAlertController(title: permission.localizedDisplayName, message: permission.localizedDescription, preferredStyle: .alert)
alertController.addAction(.ok)
self.present(alertController, animated: true)
}
func showUnknownEntitlementsAlert()
{
let alertController = UIAlertController(title: NSLocalizedString("Other Entitlements", comment: ""), message: NSLocalizedString("SideStore does not have detailed information for these entitlements.", comment: ""), preferredStyle: .alert)
alertController.addAction(.ok)
self.present(alertController, animated: true)
}
}
extension AppDetailCollectionViewController
{
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView
{
let headerView = self.collectionView.dequeueConfiguredReusableSupplementary(using: self.headerRegistration, for: indexPath)
return headerView
}
override func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool
{
return false
}
override func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool
{
return false
}
}

View File

@@ -1,276 +0,0 @@
//
// AppPermissionsCard.swift
// AltStore
//
// Created by Riley Testut on 5/4/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import SwiftUI
import AltStoreCore
@available(iOS 16, *)
extension AppPermissionsCard
{
private struct TransitionKey: Hashable
{
static func name(_ permission: Permission) -> TransitionKey {
TransitionKey(key: "name", permission: permission)
}
static func icon(_ permission: Permission) -> TransitionKey {
TransitionKey(key: "icon", permission: permission)
}
let key: String
let permission: Permission
private init(key: String, permission: Permission)
{
self.key = key
self.permission = permission
}
}
}
@available(iOS 16, *)
struct AppPermissionsCard<Permission: AppPermissionProtocol>: View
{
let title: LocalizedStringKey
let description: LocalizedStringKey
let tintColor: Color
let permissions: [Permission]
@State
private var selectedPermission: Permission?
@Namespace
private var animation
private var isTitleVisible: Bool {
if selectedPermission == nil
{
// Title should always be visible when showing all permissions.
return true
}
// If showing permission details, only show title if there
// are more than 2 permissions total to save vertical space.
let isTitleVisible = permissions.count > 2
return isTitleVisible
}
var body: some View {
let title = Text(title)
.font(.title3)
.bold()
.minimumScaleFactor(0.1) // Avoid clipping during matchedGeometryEffect animation.
VStack(spacing: 8) {
if isTitleVisible
{
// If title is visible, place _outside_ `content`
// to avoid being covered by permissionDetailView.
title
}
let content = VStack(spacing: 8) {
if !isTitleVisible
{
// Place title inside `content` when not visible
// so it's covered by permissionDetailView.
title
}
VStack(spacing: 20) {
Text(description)
.font(.subheadline)
.fixedSize(horizontal: false, vertical: true)
Grid(verticalSpacing: 15) {
ForEach(permissions, id: \.self) { permission in
permissionRow(for: permission)
}
}
Text("Tap a permission to learn more.")
.font(.subheadline)
.fixedSize(horizontal: false, vertical: true)
}
}
if let selectedPermission
{
// Hide content with overlay to preserve existing size.
content.hidden().overlay {
permissionDetailView(for: selectedPermission)
}
}
else
{
content
}
}
.overlay(alignment: .topTrailing) {
if selectedPermission != nil
{
Image(systemName: "xmark.circle.fill")
.imageScale(.medium)
}
}
.multilineTextAlignment(.center)
.frame(maxWidth: .infinity)
.padding(20)
.overlay {
if selectedPermission != nil
{
// Make entire view tappable when overlay is visible.
SwiftUI.Button(action: hidePermission) {
VStack {}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}
.foregroundColor(.secondary) // Vibrancy
.background(.regularMaterial) // Blur background for auto-legibility correction.
.background(tintColor, in: RoundedRectangle(cornerRadius: 30, style: .continuous))
}
@ViewBuilder
private func permissionRow(for permission: Permission) -> some View
{
GridRow {
SwiftUI.Button(action: { show(permission) }) {
HStack {
let text = Text(permission.localizedDisplayName)
.font(.body)
.bold()
.minimumScaleFactor(0.33)
.lineLimit(.max) // Setting lineLimit to anything fixes text wrapping at large text sizes.
let image = Image(systemName: permission.effectiveSymbolName)
.gridColumnAlignment(.center)
if selectedPermission != nil
{
Label(title: { text }, icon: { image })
.hidden()
}
else
{
Label {
text.matchedGeometryEffect(id: TransitionKey.name(permission), in: animation)
} icon: {
image.matchedGeometryEffect(id: TransitionKey.icon(permission), in: animation)
}
}
Spacer()
Image(systemName: "info.circle")
.imageScale(.large)
}
.contentShape(Rectangle()) // Make entire HStack tappable.
}
}
.frame(minHeight: 30) // Make row tall enough to tap.
}
@ViewBuilder
private func permissionDetailView(for permission: Permission) -> some View
{
VStack(spacing: 15) {
Image(systemName: permission.effectiveSymbolName)
.font(.largeTitle)
.fixedSize(horizontal: false, vertical: true)
.matchedGeometryEffect(id: TransitionKey.icon(permission), in: animation)
Text(permission.localizedDisplayName)
.font(.title2)
.bold()
.minimumScaleFactor(0.1) // Avoid clipping during matchedGeometryEffect animation.
.matchedGeometryEffect(id: TransitionKey.name(permission), in: animation)
if let usageDescription = permission.usageDescription
{
Text(usageDescription)
.font(.subheadline)
.minimumScaleFactor(0.75)
}
}
.multilineTextAlignment(.leading)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
init(title: LocalizedStringKey, description: LocalizedStringKey, tintColor: Color, permissions: [Permission])
{
self.init(title: title, description: description, tintColor: tintColor, permissions: permissions, selectedPermission: nil)
}
fileprivate init(title: LocalizedStringKey, description: LocalizedStringKey, tintColor: Color, permissions: [Permission], selectedPermission: Permission? = nil)
{
self.title = title
self.description = description
self.tintColor = tintColor
self.permissions = permissions
// Set _selectedPermission directly or else the preview won't detect it.
self._selectedPermission = State(initialValue: selectedPermission)
}
}
@available(iOS 16, *)
private extension AppPermissionsCard
{
func show(_ permission: Permission)
{
withAnimation {
self.selectedPermission = permission
}
}
func hidePermission()
{
withAnimation {
self.selectedPermission = nil
}
}
}
@available(iOS 16, *)
struct AppPermissionsCard_Previews: PreviewProvider
{
static var previews: some View {
let appPermissions = [
PreviewAppPermission(permission: ALTAppPrivacyPermission.localNetwork),
PreviewAppPermission(permission: ALTAppPrivacyPermission.microphone),
PreviewAppPermission(permission: ALTAppPrivacyPermission.photos),
PreviewAppPermission(permission: ALTAppPrivacyPermission.camera),
PreviewAppPermission(permission: ALTAppPrivacyPermission.faceID),
PreviewAppPermission(permission: ALTAppPrivacyPermission.appleMusic),
PreviewAppPermission(permission: ALTAppPrivacyPermission.bluetooth),
PreviewAppPermission(permission: ALTAppPrivacyPermission.calendars),
]
let tintColor = Color(uiColor: .deltaPrimary!)
return ForEach(1...8, id: \.self) { index in
AppPermissionsCard(title: "Privacy",
description: "Delta may request access to the following:",
tintColor: tintColor,
permissions: Array(appPermissions.prefix(index)))
.frame(width: 350)
.previewLayout(.sizeThatFits)
AppPermissionsCard(title: "Privacy",
description: "Delta may request access to the following:",
tintColor: tintColor,
permissions: Array(appPermissions.prefix(index)),
selectedPermission: appPermissions.first)
.frame(width: 350)
.previewLayout(.sizeThatFits)
}
}
}

View File

@@ -1,154 +0,0 @@
//
// AppScreenshotCollectionViewCell.swift
// AltStore
//
// Created by Riley Testut on 10/11/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import UIKit
import AltStoreCore
extension AppScreenshotCollectionViewCell
{
private class ImageView: UIImageView
{
override func layoutSubviews()
{
super.layoutSubviews()
// Explicitly layout cell to ensure rounded corners are accurate.
self.superview?.superview?.setNeedsLayout()
}
}
}
class AppScreenshotCollectionViewCell: UICollectionViewCell
{
let imageView: UIImageView
var aspectRatio: CGSize = AppScreenshot.defaultAspectRatio {
didSet {
self.updateAspectRatio()
}
}
private var isRounded: Bool = false {
didSet {
self.setNeedsLayout()
self.layoutIfNeeded()
}
}
private var aspectRatioConstraint: NSLayoutConstraint?
override init(frame: CGRect)
{
self.imageView = ImageView(frame: .zero)
self.imageView.clipsToBounds = true
self.imageView.layer.cornerCurve = .continuous
self.imageView.layer.borderColor = UIColor.tertiaryLabel.cgColor
super.init(frame: frame)
self.imageView.translatesAutoresizingMaskIntoConstraints = false
self.contentView.addSubview(self.imageView)
let widthConstraint = self.imageView.widthAnchor.constraint(equalTo: self.contentView.widthAnchor)
widthConstraint.priority = .defaultHigh
let heightConstraint = self.imageView.heightAnchor.constraint(equalTo: self.contentView.heightAnchor)
heightConstraint.priority = .defaultHigh
NSLayoutConstraint.activate([
widthConstraint,
heightConstraint,
self.imageView.widthAnchor.constraint(lessThanOrEqualTo: self.contentView.widthAnchor),
self.imageView.heightAnchor.constraint(lessThanOrEqualTo: self.contentView.heightAnchor),
self.imageView.centerXAnchor.constraint(equalTo: self.contentView.centerXAnchor),
self.imageView.centerYAnchor.constraint(equalTo: self.contentView.centerYAnchor)
])
self.updateAspectRatio()
self.updateTraits()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?)
{
super.traitCollectionDidChange(previousTraitCollection)
self.updateTraits()
}
override func layoutSubviews()
{
super.layoutSubviews()
if self.isRounded
{
let cornerRadius = self.imageView.bounds.width / 9.0 // Based on iPhone 15
self.imageView.layer.cornerRadius = cornerRadius
}
else
{
self.imageView.layer.cornerRadius = 5
}
}
}
extension AppScreenshotCollectionViewCell
{
func setImage(_ image: UIImage?)
{
guard var image, let cgImage = image.cgImage else {
self.imageView.image = image
return
}
if image.size.width > image.size.height && self.aspectRatio.width < self.aspectRatio.height
{
// Image is landscape, but cell has portrait aspect ratio, so rotate image to match.
image = UIImage(cgImage: cgImage, scale: image.scale, orientation: .right)
}
self.imageView.image = image
}
}
private extension AppScreenshotCollectionViewCell
{
func updateAspectRatio()
{
self.aspectRatioConstraint?.isActive = false
self.aspectRatioConstraint = self.imageView.widthAnchor.constraint(equalTo: self.imageView.heightAnchor, multiplier: self.aspectRatio.width / self.aspectRatio.height)
self.aspectRatioConstraint?.isActive = true
let aspectRatio: Double
if self.aspectRatio.width > self.aspectRatio.height
{
aspectRatio = self.aspectRatio.height / self.aspectRatio.width
}
else
{
aspectRatio = self.aspectRatio.width / self.aspectRatio.height
}
let tolerance = 0.001 as Double
let modernAspectRatio = AppScreenshot.defaultAspectRatio.width / AppScreenshot.defaultAspectRatio.height
let isRounded = (aspectRatio >= modernAspectRatio - tolerance) && (aspectRatio <= modernAspectRatio + tolerance)
self.isRounded = isRounded
}
func updateTraits()
{
let displayScale = (self.traitCollection.displayScale == 0.0) ? 1.0 : self.traitCollection.displayScale
self.imageView.layer.borderWidth = 1.0 / displayScale
}
}

View File

@@ -1,186 +0,0 @@
//
// AppScreenshotsViewController.swift
// AltStore
//
// Created by Riley Testut on 9/18/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import UIKit
import AltStoreCore
import Roxas
import Nuke
class AppScreenshotsViewController: UICollectionViewController
{
let app: StoreApp
private lazy var dataSource = self.makeDataSource()
init?(app: StoreApp, coder: NSCoder)
{
self.app = app
super.init(coder: coder)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad()
{
super.viewDidLoad()
self.collectionView.showsHorizontalScrollIndicator = false
// Allow parent background color to show through.
self.collectionView.backgroundColor = nil
// Match the parent table view margins.
self.collectionView.directionalLayoutMargins.top = 0
self.collectionView.directionalLayoutMargins.bottom = 0
self.collectionView.directionalLayoutMargins.leading = 20
self.collectionView.directionalLayoutMargins.trailing = 20
let collectionViewLayout = self.makeLayout()
self.collectionView.collectionViewLayout = collectionViewLayout
self.collectionView.register(AppScreenshotCollectionViewCell.self, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
self.collectionView.dataSource = self.dataSource
self.collectionView.prefetchDataSource = self.dataSource
}
}
private extension AppScreenshotsViewController
{
func makeLayout() -> UICollectionViewCompositionalLayout
{
let layoutConfig = UICollectionViewCompositionalLayoutConfiguration()
layoutConfig.contentInsetsReference = .layoutMargins
let preferredHeight = 400.0
let estimatedWidth = preferredHeight * (AppScreenshot.defaultAspectRatio.width / AppScreenshot.defaultAspectRatio.height)
let layout = UICollectionViewCompositionalLayout(sectionProvider: { [dataSource] (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
let screenshotWidths = dataSource.items.map { screenshot in
var aspectRatio = screenshot.size ?? AppScreenshot.defaultAspectRatio
if aspectRatio.width > aspectRatio.height
{
aspectRatio = CGSize(width: aspectRatio.height, height: aspectRatio.width)
}
let screenshotWidth = (preferredHeight * (aspectRatio.width / aspectRatio.height)).rounded()
return screenshotWidth
}
let smallestWidth = screenshotWidths.sorted().first
let itemWidth = smallestWidth ?? estimatedWidth // Use smallestWidth to ensure we never overshoot an item when paging.
let itemSize = NSCollectionLayoutSize(widthDimension: .estimated(itemWidth), heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .estimated(itemWidth), heightDimension: .absolute(preferredHeight))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let layoutSection = NSCollectionLayoutSection(group: group)
layoutSection.interGroupSpacing = 10
layoutSection.orthogonalScrollingBehavior = .groupPaging
return layoutSection
}, configuration: layoutConfig)
return layout
}
func makeDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<AppScreenshot, UIImage>
{
let screenshots = self.app.preferredScreenshots()
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<AppScreenshot, UIImage>(items: screenshots)
dataSource.cellConfigurationHandler = { [weak self] (cell, screenshot, indexPath) in
let cell = cell as! AppScreenshotCollectionViewCell
cell.imageView.isIndicatingActivity = true
cell.setImage(nil)
var aspectRatio = screenshot.size ?? AppScreenshot.defaultAspectRatio
if aspectRatio.width > aspectRatio.height
{
switch screenshot.deviceType
{
case .iphone:
// Always rotate landscape iPhone screenshots regardless of horizontal size class.
aspectRatio = CGSize(width: aspectRatio.height, height: aspectRatio.width)
case .ipad where self?.traitCollection.horizontalSizeClass == .compact:
// Only rotate landscape iPad screenshots if we're in horizontally compact environment.
aspectRatio = CGSize(width: aspectRatio.height, height: aspectRatio.width)
default: break
}
}
cell.aspectRatio = aspectRatio
}
dataSource.prefetchHandler = { (screenshot, indexPath, completionHandler) in
let imageURL = screenshot.imageURL
return RSTAsyncBlockOperation() { (operation) in
let request = ImageRequest(url: imageURL)
ImagePipeline.shared.loadImage(with: request, progress: nil) { result in
guard !operation.isCancelled else { return operation.finish() }
switch result
{
case .success(let response): completionHandler(response.image, nil)
case .failure(let error): completionHandler(nil, error)
}
}
}
}
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
let cell = cell as! AppScreenshotCollectionViewCell
cell.imageView.isIndicatingActivity = false
cell.setImage(image)
if let error = error
{
print("Error loading image:", error)
}
}
return dataSource
}
}
extension AppScreenshotsViewController
{
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
{
let screenshot = self.dataSource.item(at: indexPath)
let previewViewController = PreviewAppScreenshotsViewController(app: self.app)
previewViewController.currentScreenshot = screenshot
let navigationController = UINavigationController(rootViewController: previewViewController)
navigationController.modalPresentationStyle = .fullScreen
self.present(navigationController, animated: true)
}
}
@available(iOS 17, *)
#Preview(traits: .portrait) {
DatabaseManager.shared.startForPreview()
let fetchRequest = StoreApp.fetchRequest()
let storeApp = try! DatabaseManager.shared.viewContext.fetch(fetchRequest).first!
let storyboard = UIStoryboard(name: "Main", bundle: .main)
let appViewConttroller = storyboard.instantiateViewController(withIdentifier: "appViewController") as! AppViewController
appViewConttroller.app = storeApp
let navigationController = UINavigationController(rootViewController: appViewConttroller)
return navigationController
}

View File

@@ -1,189 +0,0 @@
//
// PreviewAppScreenshotsViewController.swift
// AltStore
//
// Created by Riley Testut on 9/19/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import UIKit
import AltStoreCore
import Roxas
import Nuke
class PreviewAppScreenshotsViewController: UICollectionViewController
{
let app: StoreApp
var currentScreenshot: AppScreenshot?
private lazy var dataSource = self.makeDataSource()
init(app: StoreApp)
{
self.app = app
super.init(collectionViewLayout: UICollectionViewFlowLayout())
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad()
{
super.viewDidLoad()
let tintColor = self.app.tintColor ?? .altPrimary
self.navigationController?.view.tintColor = tintColor
self.view.backgroundColor = .systemBackground
self.collectionView.backgroundColor = nil
let collectionViewLayout = self.makeLayout()
self.collectionView.collectionViewLayout = collectionViewLayout
self.collectionView.directionalLayoutMargins.leading = 20
self.collectionView.directionalLayoutMargins.trailing = 20
self.collectionView.preservesSuperviewLayoutMargins = true
self.collectionView.insetsLayoutMarginsFromSafeArea = true
self.collectionView.alwaysBounceVertical = false
self.collectionView.register(AppScreenshotCollectionViewCell.self, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
self.collectionView.dataSource = self.dataSource
self.collectionView.prefetchDataSource = self.dataSource
let doneButton = UIBarButtonItem(systemItem: .done, primaryAction: UIAction { [weak self] _ in
self?.dismissPreview()
})
self.navigationItem.rightBarButtonItem = doneButton
let swipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(PreviewAppScreenshotsViewController.dismissPreview))
swipeGestureRecognizer.direction = .down
self.view.addGestureRecognizer(swipeGestureRecognizer)
}
override func viewIsAppearing(_ animated: Bool)
{
super.viewIsAppearing(animated)
if let screenshot = self.currentScreenshot, let index = self.dataSource.items.firstIndex(of: screenshot)
{
let indexPath = IndexPath(item: index, section: 0)
self.collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: false)
}
}
}
private extension PreviewAppScreenshotsViewController
{
func makeLayout() -> UICollectionViewCompositionalLayout
{
let layoutConfig = UICollectionViewCompositionalLayoutConfiguration()
layoutConfig.contentInsetsReference = .none
let layout = UICollectionViewCompositionalLayout(sectionProvider: { [weak self] (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
guard let self else { return nil }
let contentInsets = self.collectionView.directionalLayoutMargins
let groupWidth = layoutEnvironment.container.contentSize.width - (contentInsets.leading + contentInsets.trailing)
let groupHeight = layoutEnvironment.container.contentSize.height - (contentInsets.top + contentInsets.bottom)
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(groupWidth), heightDimension: .absolute(groupHeight))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let layoutSection = NSCollectionLayoutSection(group: group)
layoutSection.orthogonalScrollingBehavior = .groupPagingCentered
layoutSection.interGroupSpacing = 10
return layoutSection
}, configuration: layoutConfig)
return layout
}
func makeDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<AppScreenshot, UIImage>
{
let screenshots = self.app.preferredScreenshots()
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<AppScreenshot, UIImage>(items: screenshots)
dataSource.cellConfigurationHandler = { [weak self] (cell, screenshot, indexPath) in
let cell = cell as! AppScreenshotCollectionViewCell
cell.imageView.isIndicatingActivity = true
cell.setImage(nil)
var aspectRatio = screenshot.size ?? AppScreenshot.defaultAspectRatio
if aspectRatio.width > aspectRatio.height
{
switch screenshot.deviceType
{
case .iphone:
// Always rotate landscape iPhone screenshots regardless of horizontal size class.
aspectRatio = CGSize(width: aspectRatio.height, height: aspectRatio.width)
case .ipad where self?.traitCollection.horizontalSizeClass == .compact:
// Only rotate landscape iPad screenshots if we're in horizontally compact environment.
aspectRatio = CGSize(width: aspectRatio.height, height: aspectRatio.width)
default: break
}
}
cell.aspectRatio = aspectRatio
}
dataSource.prefetchHandler = { (screenshot, indexPath, completionHandler) in
let imageURL = screenshot.imageURL
return RSTAsyncBlockOperation() { (operation) in
let request = ImageRequest(url: imageURL)
ImagePipeline.shared.loadImage(with: request, progress: nil) { result in
guard !operation.isCancelled else { return operation.finish() }
switch result
{
case .success(let response): completionHandler(response.image, nil)
case .failure(let error): completionHandler(nil, error)
}
}
}
}
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
let cell = cell as! AppScreenshotCollectionViewCell
cell.imageView.isIndicatingActivity = false
cell.setImage(image)
if let error = error
{
print("Error loading image:", error)
}
}
return dataSource
}
}
private extension PreviewAppScreenshotsViewController
{
@objc func dismissPreview()
{
self.dismiss(animated: true)
}
}
@available(iOS 17, *)
#Preview(traits: .portrait) {
DatabaseManager.shared.startForPreview()
let fetchRequest = StoreApp.fetchRequest()
let storeApp = try! DatabaseManager.shared.viewContext.fetch(fetchRequest).first!
let previewViewController = PreviewAppScreenshotsViewController(app: storeApp)
let navigationController = UINavigationController(rootViewController: previewViewController)
return navigationController
}

View File

@@ -10,16 +10,13 @@ import UIKit
import UserNotifications import UserNotifications
import AVFoundation import AVFoundation
import Intents import Intents
import LocalConsole
import AltStoreCore import AltStoreCore
import AltSign import AltSign
import Roxas import Roxas
import EmotionalDamage import EmotionalDamage
import Nuke
extension UIApplication: LegacyBackgroundFetching {}
extension AppDelegate extension AppDelegate
{ {
static let openPatreonSettingsDeepLinkNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".OpenPatreonSettingsDeepLinkNotification") static let openPatreonSettingsDeepLinkNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".OpenPatreonSettingsDeepLinkNotification")
@@ -27,12 +24,10 @@ extension AppDelegate
static let addSourceDeepLinkNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".AddSourceDeepLinkNotification") static let addSourceDeepLinkNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".AddSourceDeepLinkNotification")
static let appBackupDidFinish = Notification.Name(Bundle.Info.appbundleIdentifier + ".AppBackupDidFinish") static let appBackupDidFinish = Notification.Name(Bundle.Info.appbundleIdentifier + ".AppBackupDidFinish")
static let exportCertificateNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".ExportCertificateNotification")
static let importAppDeepLinkURLKey = "fileURL" static let importAppDeepLinkURLKey = "fileURL"
static let appBackupResultKey = "result" static let appBackupResultKey = "result"
static let addSourceDeepLinkURLKey = "sourceURL" static let addSourceDeepLinkURLKey = "sourceURL"
static let exportCertificateCallbackTemplateKey = "callback"
} }
@UIApplicationMain @UIApplicationMain
@@ -40,47 +35,42 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow? var window: UIWindow?
private let intentHandler = IntentHandler() @available(iOS 14, *)
private let viewAppIntentHandler = ViewAppIntentHandler() private var intentHandler: IntentHandler {
get { _intentHandler as! IntentHandler }
set { _intentHandler = newValue }
}
public let consoleLog = ConsoleLog() @available(iOS 14, *)
private var viewAppIntentHandler: ViewAppIntentHandler {
get { _viewAppIntentHandler as! ViewAppIntentHandler }
set { _viewAppIntentHandler = newValue }
}
private lazy var _intentHandler: Any = {
guard #available(iOS 14, *) else { fatalError() }
return IntentHandler()
}()
private lazy var _viewAppIntentHandler: Any = {
guard #available(iOS 14, *) else { fatalError() }
return ViewAppIntentHandler()
}()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
{ {
// navigation bar buttons spacing is too much (so hack it to use minimal spacing) // Copy STDOUT and STDERR to the logging console
// this is swift-5 specific behavior and might change _ = OutputCapturer.shared
// https://stackoverflow.com/a/64988363/11971304
//
// Warning: this affects all screens through out the app, and basically overrides storyboard
let stackViewAppearance = UIStackView.appearance(whenContainedInInstancesOf: [UINavigationBar.self])
stackViewAppearance.spacing = -8 // adjust as needed
consoleLog.startCapturing()
print("===================================================")
print("| App is Starting up |")
print("===================================================")
print("| Console Logger started capturing output streams |")
print("===================================================")
print("\n ")
// Override point for customization after application launch.
// UserDefaults.standard.setValue(true, forKey: "com.apple.CoreData.MigrationDebug")
// UserDefaults.standard.setValue(true, forKey: "com.apple.CoreData.SQLDebug")
// Register default settings before doing anything else. // Register default settings before doing anything else.
UserDefaults.registerDefaults() UserDefaults.registerDefaults()
#if UNSTABLE
UnstableFeatures.load()
#endif
// Recreate Database if requested LCManager.shared.isVisible = UserDefaults.standard.isConsoleEnabled
// NOTE: Userdefaults are local to the SideStore.app sandbox and are not shared LCManager.shared.isCharacterLimitDisabled = true // we want all logs exported
if UserDefaults.standard.recreateDatabaseOnNextStart{
// reset the state
UserDefaults.standard.recreateDatabaseOnNextStart = false
// re-create database
DatabaseManager.recreateDatabase()
}
DatabaseManager.shared.start { (error) in DatabaseManager.shared.start { (error) in
if let error = error if let error = error
@@ -96,13 +86,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
AnalyticsManager.shared.start() AnalyticsManager.shared.start()
self.setTintColor() self.setTintColor()
self.prepareImageCache()
// TODO: @mahee96: find if we need to start em_proxy as in altstore?
if UserDefaults.standard.enableEMPforWireguard {
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
}
SecureValueTransformer.register() SecureValueTransformer.register()
if UserDefaults.standard.firstLaunch == nil if UserDefaults.standard.firstLaunch == nil
@@ -113,7 +97,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
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 && targetEnvironment(simulator) #if DEBUG || BETA
UserDefaults.standard.isDebugModeEnabled = true UserDefaults.standard.isDebugModeEnabled = true
#endif #endif
@@ -125,10 +109,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
func applicationDidEnterBackground(_ application: UIApplication) func applicationDidEnterBackground(_ application: UIApplication)
{ {
// Make sure to update SceneDelegate.sceneDidEnterBackground() as well. // Make sure to update SceneDelegate.sceneDidEnterBackground() as well.
// TODO: @mahee96: find if we need to stop em_proxy as in altstore?
if UserDefaults.standard.enableEMPforWireguard {
stop_em_proxy()
}
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)
@@ -145,11 +126,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
func applicationWillEnterForeground(_ application: UIApplication) func applicationWillEnterForeground(_ application: UIApplication)
{ {
AppManager.shared.update() AppManager.shared.update()
if UserDefaults.standard.enableEMPforWireguard { start_em_proxy(bind_addr: Consts.Proxy.serverURL)
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
}
PatreonAPI.shared.refreshPatreonAccount()
} }
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool
@@ -159,6 +136,8 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, handlerFor intent: INIntent) -> Any? func application(_ application: UIApplication, handlerFor intent: INIntent) -> Any?
{ {
guard #available(iOS 14, *) else { return nil }
switch intent switch intent
{ {
case is RefreshAllIntent: return self.intentHandler case is RefreshAllIntent: return self.intentHandler
@@ -166,19 +145,9 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
default: return nil default: return nil
} }
} }
func applicationWillTerminate(_ application: UIApplication) {
// Stop console logging and clean up resources
print("\n ")
print("===================================================")
print("| Console Logger stopped capturing output streams |")
print("===================================================")
print("| App is being terminated |")
print("===================================================")
consoleLog.stopCapturing()
}
} }
@available(iOS 13, *)
extension AppDelegate extension AppDelegate
{ {
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration
@@ -203,33 +172,6 @@ private extension AppDelegate
self.window?.tintColor = .altPrimary self.window?.tintColor = .altPrimary
} }
func prepareImageCache()
{
// Avoid caching responses twice.
DataLoader.sharedUrlCache.diskCapacity = 0
let pipeline = ImagePipeline { configuration in
do
{
let dataCache = try DataCache(name: "io.sidestore.Nuke")
dataCache.sizeLimit = 512 * 1024 * 1024 // 512MB
configuration.dataCache = dataCache
}
catch
{
Logger.main.error("Failed to create image disk cache. Falling back to URL cache. \(error.localizedDescription, privacy: .public)")
}
}
ImagePipeline.shared = pipeline
if let dataCache = ImagePipeline.shared.configuration.dataCache as? DataCache, #available(iOS 15, *)
{
Logger.main.info("Current image cache size: \(dataCache.totalSize.formatted(.byteCount(style: .file)), privacy: .public)")
}
}
func open(_ url: URL) -> Bool func open(_ url: URL) -> Bool
{ {
if url.isFileURL if url.isFileURL
@@ -300,26 +242,6 @@ private extension AppDelegate
return true return true
case "pairing":
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:]
guard let callbackTemplate = queryItems["urlName"]?.removingPercentEncoding else { return false }
DispatchQueue.main.async {
exportPairingFile(callbackTemplate)
}
return true
case "certificate":
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:]
guard let callbackTemplate = queryItems["callback_template"]?.removingPercentEncoding else { return false }
DispatchQueue.main.async {
NotificationCenter.default.post(name: AppDelegate.exportCertificateNotification, object: nil, userInfo: [AppDelegate.exportCertificateCallbackTemplateKey: callbackTemplate])
}
return true
default: return false default: return false
} }
} }
@@ -331,12 +253,12 @@ 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 as LegacyBackgroundFetching).setMinimumBackgroundFetchInterval(1 * 60 * 60) UIApplication.shared.setMinimumBackgroundFetchInterval(1 * 60 * 60)
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { (success, error) in UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { (success, error) in
} }
#if DEBUG && targetEnvironment(simulator) #if DEBUG
UIApplication.shared.registerForRemoteNotifications() UIApplication.shared.registerForRemoteNotifications()
#endif #endif
} }
@@ -445,12 +367,10 @@ private extension AppDelegate
{ {
let (sources, context) = try result.get() let (sources, context) = try result.get()
let previousUpdatesFetchRequest = InstalledApp.supportedUpdatesFetchRequest() 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)]
#keyPath(InstalledApp.storeApp.latestSupportedVersion.version),
#keyPath(InstalledApp.storeApp.latestSupportedVersion._buildVersion)]
let previousNewsItemsFetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NSFetchRequestResult> let previousNewsItemsFetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NSFetchRequestResult>
previousNewsItemsFetchRequest.includesPendingChanges = false previousNewsItemsFetchRequest.includesPendingChanges = false
@@ -462,9 +382,7 @@ private extension AppDelegate
try context.save() try context.save()
let updatesFetchRequest = InstalledApp.updatesFetchRequest()
let updatesFetchRequest = InstalledApp.supportedUpdatesFetchRequest()
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)
@@ -472,23 +390,12 @@ private extension AppDelegate
for update in updates for update in updates
{ {
guard let storeApp = update.storeApp, let latestSupportedVersion = storeApp.latestSupportedVersion, latestSupportedVersion.isSupported 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 }
if let previousUpdate = previousUpdates.first(where: { $0[#keyPath(InstalledApp.bundleIdentifier)] == update.bundleIdentifier })
{
// An update for this app was already available, so check whether the version or build version is different.
guard let previousVersion = previousUpdate[#keyPath(InstalledApp.storeApp.latestSupportedVersion.version)] else { continue }
// previousUpdate might not contain buildVersion, but if it does then map empty string to nil to match AppVersion.
let previousBuildVersion = previousUpdate[#keyPath(InstalledApp.storeApp.latestSupportedVersion._buildVersion)].map { $0.isEmpty ? nil : "" }
// Only show notification if previous latestSupportedVersion does not _exactly_ match current latestSupportedVersion.
guard previousVersion != latestSupportedVersion.version || previousBuildVersion != latestSupportedVersion.buildVersion 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, latestSupportedVersion.localizedVersion) 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)

View File

@@ -1,13 +1,12 @@
<?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="23504" 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="23506"/> <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"/>
<capability name="collection view cell content view" 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>
<scenes> <scenes>
@@ -37,11 +36,11 @@
</tabBar> </tabBar>
<connections> <connections>
<segue destination="kjR-gi-fgT" kind="relationship" relationship="viewControllers" id="eWy-uk-nwG"/> <segue destination="kjR-gi-fgT" kind="relationship" relationship="viewControllers" id="eWy-uk-nwG"/>
<segue destination="faz-B4-Sub" kind="relationship" relationship="viewControllers" id="dXz-Tu-hW8"/>
<segue destination="3Ew-ox-i4n" kind="relationship" relationship="viewControllers" id="zii-dF-qEt"/>
<segue destination="p3d-dP-Swg" kind="relationship" relationship="viewControllers" id="4Nf-rL-P4c"/>
<segue destination="Qo4-72-Hmr" kind="presentation" identifier="presentSources" id="Qd6-ba-dIo"/>
<segue destination="bTL-bY-9Yq" kind="presentation" identifier="finishJailbreak" id="cIc-Ta-uNk"/> <segue destination="bTL-bY-9Yq" kind="presentation" identifier="finishJailbreak" id="cIc-Ta-uNk"/>
<segue destination="HCK-G6-KdY" kind="relationship" relationship="viewControllers" id="X0t-T6-JeA"/>
<segue destination="faz-B4-Sub" kind="relationship" relationship="viewControllers" id="OLu-kM-z1J"/>
<segue destination="3Ew-ox-i4n" kind="relationship" relationship="viewControllers" id="phQ-Pc-pqw"/>
<segue destination="p3d-dP-Swg" kind="relationship" relationship="viewControllers" id="cQE-Az-fdo"/>
</connections> </connections>
</tabBarController> </tabBarController>
<placeholder placeholderIdentifier="IBFirstResponder" id="HuB-VB-40B" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="HuB-VB-40B" sceneMemberID="firstResponder"/>
@@ -51,7 +50,7 @@
<!--Browse--> <!--Browse-->
<scene sceneID="rXq-UR-qQp"> <scene sceneID="rXq-UR-qQp">
<objects> <objects>
<collectionViewController storyboardIdentifier="browseViewController" id="e3L-BF-iXp" customClass="BrowseViewController" customModule="SideStore" customModuleProvider="target" 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"/>
@@ -68,11 +67,20 @@
<outlet property="delegate" destination="e3L-BF-iXp" id="Mdp-x4-hZe"/> <outlet property="delegate" destination="e3L-BF-iXp" id="Mdp-x4-hZe"/>
</connections> </connections>
</collectionView> </collectionView>
<navigationItem key="navigationItem" title="Browse" id="pUx-6k-rtr"/> <navigationItem key="navigationItem" title="Browse" id="pUx-6k-rtr">
<barButtonItem key="rightBarButtonItem" title="Sources" id="6Ul-JW-TMT">
<connections>
<segue destination="Qo4-72-Hmr" kind="presentation" id="de9-NH-aec"/>
</connections>
</barButtonItem>
</navigationItem>
<connections>
<outlet property="sourcesBarButtonItem" destination="6Ul-JW-TMT" id="99s-O4-OpX"/>
</connections>
</collectionViewController> </collectionViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="cMN-i4-Bxk" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="cMN-i4-Bxk" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="2730" y="-373"/> <point key="canvasLocation" x="1730" y="-17"/>
</scene> </scene>
<!--App View Controller--> <!--App View Controller-->
<scene sceneID="TgT-LO-3Er"> <scene sceneID="TgT-LO-3Er">
@@ -216,7 +224,7 @@
</viewController> </viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="C9o-C3-sMK" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="C9o-C3-sMK" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="2730" y="439"/> <point key="canvasLocation" x="2525.5999999999999" y="-17.541229385307346"/>
</scene> </scene>
<!--App--> <!--App-->
<scene sceneID="CgX-7h-sRI"> <scene sceneID="CgX-7h-sRI">
@@ -254,40 +262,51 @@
<inset key="separatorInset" minX="15" minY="0.0" maxX="15" maxY="0.0"/> <inset key="separatorInset" minX="15" minY="0.0" maxX="15" maxY="0.0"/>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="nI6-wC-H2d"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="nI6-wC-H2d">
<rect key="frame" x="0.0" y="107" width="375" height="300"/> <rect key="frame" x="0.0" y="107" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="nI6-wC-H2d" id="Z4y-vb-Z4Q"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="nI6-wC-H2d" id="Z4y-vb-Z4Q">
<rect key="frame" x="0.0" y="0.0" width="375" height="300"/> <rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<containerView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="5yj-Nb-f5H"> <collectionView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" dataMode="prototypes" translatesAutoresizingMaskIntoConstraints="NO" id="ppk-lL-at8">
<rect key="frame" x="0.0" y="0.0" width="375" height="300"/> <rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
<constraints> <color key="backgroundColor" name="Background"/>
<constraint firstAttribute="height" priority="999" constant="300" id="dpf-ba-NNr"/> <collectionViewFlowLayout key="collectionViewLayout" scrollDirection="horizontal" minimumLineSpacing="10" minimumInteritemSpacing="15" id="ace-Ns-Jd2">
</constraints> <size key="itemSize" width="189" height="406"/>
<connections> <size key="headerReferenceSize" width="0.0" height="0.0"/>
<segue destination="nX2-hQ-qjX" kind="embed" destinationCreationSelector="makeAppScreenshotsViewController:sender:" id="VxG-Pu-Kf1"/> <size key="footerReferenceSize" width="0.0" height="0.0"/>
</connections> <inset key="sectionInset" minX="15" minY="0.0" maxX="15" maxY="0.0"/>
</containerView> </collectionViewFlowLayout>
<cells>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="Cell" id="2U6-d3-e4r" customClass="ScreenshotCollectionViewCell">
<rect key="frame" x="15" y="-181" width="189" height="406"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
<rect key="frame" x="0.0" y="0.0" width="189" height="406"/>
<autoresizingMask key="autoresizingMask"/>
</view>
</collectionViewCell>
</cells>
</collectionView>
</subviews> </subviews>
<constraints> <constraints>
<constraint firstAttribute="trailing" secondItem="5yj-Nb-f5H" secondAttribute="trailing" id="2DI-44-pC1"/> <constraint firstAttribute="trailing" secondItem="ppk-lL-at8" secondAttribute="trailing" id="3QR-Y2-v26"/>
<constraint firstItem="5yj-Nb-f5H" firstAttribute="top" secondItem="Z4y-vb-Z4Q" secondAttribute="top" id="URh-5T-73x"/> <constraint firstAttribute="bottom" secondItem="ppk-lL-at8" secondAttribute="bottom" id="EgJ-Uw-5ta"/>
<constraint firstAttribute="bottom" secondItem="5yj-Nb-f5H" secondAttribute="bottom" id="Yb6-aZ-qNF"/> <constraint firstItem="ppk-lL-at8" firstAttribute="leading" secondItem="Z4y-vb-Z4Q" secondAttribute="leading" id="wHf-S9-gMV"/>
<constraint firstItem="5yj-Nb-f5H" firstAttribute="leading" secondItem="Z4y-vb-Z4Q" secondAttribute="leading" id="rpG-Ip-qZU"/> <constraint firstItem="ppk-lL-at8" firstAttribute="top" secondItem="Z4y-vb-Z4Q" secondAttribute="top" id="xY5-w8-roA"/>
</constraints> </constraints>
</tableViewCellContentView> </tableViewCellContentView>
<color key="backgroundColor" name="Background"/> <color key="backgroundColor" name="Background"/>
<inset key="separatorInset" minX="15" minY="0.0" maxX="15" maxY="0.0"/> <inset key="separatorInset" minX="15" minY="0.0" maxX="15" maxY="0.0"/>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="EL5-UC-RIw" customClass="AppContentTableViewCell" customModule="SideStore" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="EL5-UC-RIw" customClass="AppContentTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="407" width="375" height="98"/> <rect key="frame" x="0.0" y="151" width="375" height="98"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="EL5-UC-RIw" id="D1G-nK-G0Z"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="EL5-UC-RIw" id="D1G-nK-G0Z">
<rect key="frame" x="0.0" y="0.0" width="375" height="98"/> <rect key="frame" x="0.0" y="0.0" width="375" height="98"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" text="Hello Me" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="Pyt-8D-BZA" customClass="CollapsingMarkdownView" customModule="SideStore" customModuleProvider="target"> <textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" text="Hello Me" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="Pyt-8D-BZA" customClass="CollapsingTextView" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="20" y="20" width="335" height="34"/> <rect key="frame" x="20" y="20" width="335" height="34"/>
<color key="backgroundColor" name="Background"/> <color key="backgroundColor" name="Background"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/> <fontDescription key="fontDescription" type="system" pointSize="15"/>
@@ -305,7 +324,7 @@
<inset key="separatorInset" minX="1000" minY="0.0" maxX="0.0" maxY="0.0"/> <inset key="separatorInset" minX="1000" minY="0.0" maxX="0.0" maxY="0.0"/>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="47M-El-a4G" customClass="AppContentTableViewCell" customModule="SideStore" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="47M-El-a4G" customClass="AppContentTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="505" width="375" height="137.5"/> <rect key="frame" x="0.0" y="249" width="375" height="137.5"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="47M-El-a4G" id="f9D-OR-oGE"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="47M-El-a4G" id="f9D-OR-oGE">
<rect key="frame" x="0.0" y="0.0" width="375" height="137.5"/> <rect key="frame" x="0.0" y="0.0" width="375" height="137.5"/>
@@ -353,7 +372,7 @@
</stackView> </stackView>
</subviews> </subviews>
</stackView> </stackView>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="wQF-WY-Gdz" customClass="CollapsingMarkdownView" customModule="SideStore" customModuleProvider="target"> <textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="wQF-WY-Gdz" customClass="CollapsingTextView" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="20" y="59.5" width="335" height="34"/> <rect key="frame" x="20" y="59.5" width="335" height="34"/>
<color key="backgroundColor" name="Background"/> <color key="backgroundColor" name="Background"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/> <fontDescription key="fontDescription" type="system" pointSize="15"/>
@@ -373,35 +392,83 @@
<color key="backgroundColor" name="Background"/> <color key="backgroundColor" name="Background"/>
<inset key="separatorInset" minX="1000" minY="0.0" maxX="0.0" maxY="0.0"/> <inset key="separatorInset" minX="1000" minY="0.0" maxX="0.0" maxY="0.0"/>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" rowHeight="300" id="nM7-vJ-W8b"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" rowHeight="149" id="nM7-vJ-W8b">
<rect key="frame" x="0.0" y="642.5" width="375" height="300"/> <rect key="frame" x="0.0" y="386.5" width="375" height="149"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="nM7-vJ-W8b" id="cQ2-Jd-pRK"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="nM7-vJ-W8b" id="cQ2-Jd-pRK">
<rect key="frame" x="0.0" y="0.0" width="375" height="300"/> <rect key="frame" x="0.0" y="0.0" width="375" height="149"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" axis="vertical" distribution="equalSpacing" alignment="center" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="Jvb-r8-XrY"> <stackView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" axis="vertical" distribution="equalSpacing" alignment="center" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="Jvb-r8-XrY">
<rect key="frame" x="0.0" y="0.0" width="375" height="300"/> <rect key="frame" x="0.0" y="0.0" width="375" height="149"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="Permissions" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="dj7-G8-GFv"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="Permissions" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="dj7-G8-GFv">
<rect key="frame" x="20" y="0.0" width="335" height="26.5"/> <rect key="frame" x="20" y="0.0" width="335" height="26"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="22"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="22"/>
<nil key="textColor"/> <nil key="textColor"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<containerView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="wus-dU-ZqZ"> <collectionView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" dataMode="prototypes" translatesAutoresizingMaskIntoConstraints="NO" id="r8T-dj-wQX">
<rect key="frame" x="0.0" y="80" width="375" height="200"/> <rect key="frame" x="0.0" y="41" width="375" height="88"/>
<color key="backgroundColor" name="Background"/>
<constraints> <constraints>
<constraint firstAttribute="height" constant="200" id="HFx-PP-dAt"/> <constraint firstAttribute="height" priority="999" constant="88" id="6Lk-OO-MsA"/>
</constraints> </constraints>
<connections> <collectionViewFlowLayout key="collectionViewLayout" scrollDirection="horizontal" minimumLineSpacing="10" minimumInteritemSpacing="10" id="2HF-4d-3Im">
<segue destination="OYP-I1-A3i" kind="embed" destinationCreationSelector="makeAppDetailCollectionViewController:sender:" id="Uxh-GM-nzb"/> <size key="itemSize" width="60" height="88"/>
</connections> <size key="headerReferenceSize" width="0.0" height="0.0"/>
</containerView> <size key="footerReferenceSize" width="0.0" height="0.0"/>
<inset key="sectionInset" minX="20" minY="0.0" maxX="20" maxY="0.0"/>
</collectionViewFlowLayout>
<cells>
<collectionViewCell opaque="NO" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="Cell" id="WYy-bZ-h3T" customClass="PermissionCollectionViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="20" y="0.0" width="60" height="88"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
<rect key="frame" x="0.0" y="0.0" width="60" height="88"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="center" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="fSx-We-L4W">
<rect key="frame" x="0.0" y="0.0" width="60" height="87.5"/>
<subviews>
<button opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="79g-9q-mE2">
<rect key="frame" x="5" y="0.0" width="50" height="50"/>
<constraints>
<constraint firstAttribute="width" constant="50" id="0LZ-4n-COH"/>
<constraint firstAttribute="height" constant="50" id="keD-mf-Rga"/>
</constraints>
</button>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" textAlignment="center" lineBreakMode="wordWrap" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="pQi-FD-18P">
<rect key="frame" x="12.5" y="56" width="35.5" height="31.5"/>
<string key="text">Hello
World</string>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</stackView>
</subviews>
</view>
<constraints>
<constraint firstAttribute="trailing" secondItem="fSx-We-L4W" secondAttribute="trailing" id="IyD-vD-tA4"/>
<constraint firstItem="fSx-We-L4W" firstAttribute="leading" secondItem="WYy-bZ-h3T" secondAttribute="leading" id="bTq-op-ivD"/>
<constraint firstItem="fSx-We-L4W" firstAttribute="top" secondItem="WYy-bZ-h3T" secondAttribute="top" id="sMw-NS-jtY"/>
</constraints>
<connections>
<outlet property="button" destination="79g-9q-mE2" id="G5V-SS-vaA"/>
<outlet property="textLabel" destination="pQi-FD-18P" id="D5d-20-cm3"/>
<segue destination="Ojq-DN-xcF" kind="popoverPresentation" identifier="showPermission" popoverAnchorView="r8T-dj-wQX" id="ftM-H7-Q7G">
<popoverArrowDirection key="popoverArrowDirection" down="YES"/>
</segue>
</connections>
</collectionViewCell>
</cells>
</collectionView>
</subviews> </subviews>
<constraints> <constraints>
<constraint firstItem="dj7-G8-GFv" firstAttribute="leading" secondItem="Jvb-r8-XrY" secondAttribute="leading" constant="20" id="9pB-Md-91A"/> <constraint firstItem="dj7-G8-GFv" firstAttribute="leading" secondItem="Jvb-r8-XrY" secondAttribute="leading" constant="20" id="9pB-Md-91A"/>
<constraint firstItem="wus-dU-ZqZ" firstAttribute="width" secondItem="Jvb-r8-XrY" secondAttribute="width" id="coR-wZ-TkD"/> <constraint firstItem="r8T-dj-wQX" firstAttribute="width" secondItem="Jvb-r8-XrY" secondAttribute="width" id="QJH-2y-DSh"/>
</constraints> </constraints>
<edgeInsets key="layoutMargins" top="0.0" left="0.0" bottom="20" right="0.0"/> <edgeInsets key="layoutMargins" top="0.0" left="0.0" bottom="20" right="0.0"/>
</stackView> </stackView>
@@ -427,9 +494,9 @@
<navigationItem key="navigationItem" title="App" largeTitleDisplayMode="never" id="sWo-Y8-aF6"/> <navigationItem key="navigationItem" title="App" largeTitleDisplayMode="never" id="sWo-Y8-aF6"/>
<size key="freeformSize" width="375" height="667"/> <size key="freeformSize" width="375" height="667"/>
<connections> <connections>
<outlet property="appDetailCollectionViewHeightConstraint" destination="HFx-PP-dAt" id="ti3-q6-ku1"/>
<outlet property="appScreenshotsHeightConstraint" destination="dpf-ba-NNr" id="shO-Kq-Y90"/>
<outlet property="descriptionTextView" destination="Pyt-8D-BZA" id="cgV-Hg-LrH"/> <outlet property="descriptionTextView" destination="Pyt-8D-BZA" id="cgV-Hg-LrH"/>
<outlet property="permissionsCollectionView" destination="r8T-dj-wQX" id="Xud-5X-w2E"/>
<outlet property="screenshotsCollectionView" destination="ppk-lL-at8" id="YoQ-Z6-WTP"/>
<outlet property="sizeLabel" destination="DgM-bD-bBY" id="Oky-ax-u20"/> <outlet property="sizeLabel" destination="DgM-bD-bBY" id="Oky-ax-u20"/>
<outlet property="subtitleLabel" destination="BsL-O2-UjD" id="cfe-cf-4a9"/> <outlet property="subtitleLabel" destination="BsL-O2-UjD" id="cfe-cf-4a9"/>
<outlet property="versionDateLabel" destination="wGD-mS-8fO" id="icB-lC-g9x"/> <outlet property="versionDateLabel" destination="wGD-mS-8fO" id="icB-lC-g9x"/>
@@ -439,52 +506,52 @@
</tableViewController> </tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dhh-ZN-LoG" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="dhh-ZN-LoG" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="3506" y="437"/> <point key="canvasLocation" x="3302" y="-18"/>
</scene> </scene>
<!--App Screenshots View Controller--> <!--Permission Popover View Controller-->
<scene sceneID="E6k-TI-c4N"> <scene sceneID="24j-EJ-G4e">
<objects> <objects>
<collectionViewController storyboardIdentifier="appScreenshotsViewController" id="nX2-hQ-qjX" customClass="AppScreenshotsViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController"> <viewController id="Ojq-DN-xcF" customClass="PermissionPopoverViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" contentInsetAdjustmentBehavior="never" dataMode="prototypes" id="zXl-if-KtH"> <view key="view" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="IgU-aM-YrX">
<rect key="frame" x="0.0" y="0.0" width="375" height="300"/> <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"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/> <subviews>
<collectionViewFlowLayout key="collectionViewLayout" scrollDirection="horizontal" automaticEstimatedItemSize="YES" minimumLineSpacing="10" minimumInteritemSpacing="10" id="MGS-YY-5g9"> <stackView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="xnC-tS-ZdV">
<size key="itemSize" width="150" height="300"/> <rect key="frame" x="20" y="10" width="335" height="197"/>
<size key="headerReferenceSize" width="0.0" height="0.0"/> <subviews>
<size key="footerReferenceSize" width="0.0" height="0.0"/> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="500" insetsLayoutMarginsFromSafeArea="NO" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="4fh-lO-rAn">
<inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/> <rect key="frame" x="0.0" y="0.0" width="335" height="17"/>
</collectionViewFlowLayout> <fontDescription key="fontDescription" type="boldSystem" pointSize="14"/>
<cells/> <nil key="textColor"/>
<connections> <nil key="highlightedColor"/>
<outlet property="dataSource" destination="nX2-hQ-qjX" id="QRj-01-ddR"/> </label>
<outlet property="delegate" destination="nX2-hQ-qjX" id="Ha5-Xa-Q6e"/> <label opaque="NO" userInteractionEnabled="NO" contentMode="TopLeft" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="500" insetsLayoutMarginsFromSafeArea="NO" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" preferredMaxLayoutWidth="300" translatesAutoresizingMaskIntoConstraints="NO" id="ErG-8A-uqY">
</connections> <rect key="frame" x="0.0" y="21" width="335" height="176"/>
</collectionView> <fontDescription key="fontDescription" type="system" pointSize="13"/>
</collectionViewController> <nil key="textColor"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="np0-Hj-vy7" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/> <nil key="highlightedColor"/>
</label>
</subviews>
<edgeInsets key="layoutMargins" top="0.0" left="0.0" bottom="0.0" right="0.0"/>
</stackView>
</subviews>
<viewLayoutGuide key="safeArea" id="c7x-ee-3HH"/>
<color key="backgroundColor" systemColor="tertiarySystemBackgroundColor"/>
<constraints>
<constraint firstItem="xnC-tS-ZdV" firstAttribute="leading" secondItem="c7x-ee-3HH" secondAttribute="leading" constant="20" id="LO8-Au-SYF"/>
<constraint firstItem="c7x-ee-3HH" firstAttribute="bottom" secondItem="xnC-tS-ZdV" secondAttribute="bottom" constant="10" id="NZ9-iG-E10"/>
<constraint firstItem="c7x-ee-3HH" firstAttribute="trailing" secondItem="xnC-tS-ZdV" secondAttribute="trailing" constant="20" id="ZkD-tb-mBf"/>
<constraint firstItem="xnC-tS-ZdV" firstAttribute="top" secondItem="c7x-ee-3HH" secondAttribute="top" constant="10" id="oKq-9e-DtW"/>
</constraints>
</view>
<connections>
<outlet property="descriptionLabel" destination="ErG-8A-uqY" id="iuN-kE-IEm"/>
<outlet property="nameLabel" destination="4fh-lO-rAn" id="GWh-7k-yWw"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="7Tu-x9-xBb" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="4302" y="20"/> <point key="canvasLocation" x="4257" y="-412"/>
</scene>
<!--App Detail Collection View Controller-->
<scene sceneID="Pcn-h5-5fk">
<objects>
<collectionViewController id="OYP-I1-A3i" customClass="AppDetailCollectionViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" dataMode="prototypes" id="y1V-56-IqS" customClass="SafeAreaIgnoringCollectionView">
<rect key="frame" x="0.0" y="0.0" width="375" height="200"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<collectionViewLayout key="collectionViewLayout" id="KQE-PB-FbG"/>
<cells/>
<connections>
<outlet property="dataSource" destination="OYP-I1-A3i" id="YDU-V6-g0R"/>
<outlet property="delegate" destination="OYP-I1-A3i" id="faX-I5-qJ2"/>
</connections>
</collectionView>
</collectionViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="fxm-bB-W29" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="4298" y="434"/>
</scene> </scene>
<!--Settings--> <!--Settings-->
<scene sceneID="KlD-j0-ROn"> <scene sceneID="KlD-j0-ROn">
@@ -494,7 +561,7 @@
</viewControllerPlaceholder> </viewControllerPlaceholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="HgE-PD-dC2" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="HgE-PD-dC2" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="233" y="550"/> <point key="canvasLocation" x="962" y="1197"/>
</scene> </scene>
<!--News--> <!--News-->
<scene sceneID="bqw-wB-hyB"> <scene sceneID="bqw-wB-hyB">
@@ -529,14 +596,13 @@
<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">
<rect key="frame" x="0.0" y="20" width="375" height="96"/> <rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<color key="tintColor" name="Primary"/> <color key="tintColor" name="Primary"/>
<edgeInsets key="layoutMargins" top="8" left="20" bottom="8" right="8"/>
</navigationBar> </navigationBar>
<nil name="viewControllers"/> <nil name="viewControllers"/>
<connections> <connections>
<segue destination="KKu-kI-2kg" kind="relationship" relationship="rootViewController" id="2Dm-Oy-wu0"/> <segue destination="e3L-BF-iXp" kind="relationship" relationship="rootViewController" id="EVp-fA-PvU"/>
</connections> </connections>
</navigationController> </navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="OkH-49-O0J" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="OkH-49-O0J" userLabel="First Responder" sceneMemberID="firstResponder"/>
@@ -549,7 +615,7 @@
<viewControllerPlaceholder storyboardName="PatchApp" id="bTL-bY-9Yq" sceneMemberID="viewController"/> <viewControllerPlaceholder storyboardName="PatchApp" id="bTL-bY-9Yq" sceneMemberID="viewController"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="NyZ-z6-R2q" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="NyZ-z6-R2q" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="-228" y="551"/> <point key="canvasLocation" x="-1" y="545"/>
</scene> </scene>
<!--My Apps--> <!--My Apps-->
<scene sceneID="nhh-BJ-XiT"> <scene sceneID="nhh-BJ-XiT">
@@ -560,9 +626,8 @@
</tabBarItem> </tabBarItem>
<toolbarItems/> <toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="CzO-Kt-BlZ" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target"> <navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="CzO-Kt-BlZ" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="20" width="375" height="96"/> <rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<edgeInsets key="layoutMargins" top="8" left="20" bottom="8" right="8"/>
</navigationBar> </navigationBar>
<nil name="viewControllers"/> <nil name="viewControllers"/>
<connections> <connections>
@@ -639,19 +704,12 @@
<color key="textColor" name="Primary"/> <color key="textColor" name="Primary"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Who-nd-jyt">
<rect key="frame" x="313" y="13" width="38" height="34.5"/>
<state key="normal" title="Button"/>
<buttonConfiguration key="configuration" style="plain" title="..."/>
</button>
</subviews> </subviews>
<constraints> <constraints>
<constraint firstItem="Who-nd-jyt" firstAttribute="trailing" secondItem="F8U-ab-fOM" secondAttribute="trailingMargin" id="0Fe-FJ-P3p"/>
<constraint firstItem="z04-yg-x1t" firstAttribute="centerY" secondItem="F8U-ab-fOM" secondAttribute="centerY" id="9w9-Z0-jZl"/> <constraint firstItem="z04-yg-x1t" firstAttribute="centerY" secondItem="F8U-ab-fOM" secondAttribute="centerY" id="9w9-Z0-jZl"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="z04-yg-x1t" secondAttribute="bottom" constant="10" id="IWL-Ei-QC2"/> <constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="z04-yg-x1t" secondAttribute="bottom" constant="10" id="IWL-Ei-QC2"/>
<constraint firstItem="z04-yg-x1t" firstAttribute="top" relation="greaterThanOrEqual" secondItem="F8U-ab-fOM" secondAttribute="top" constant="10" id="fLp-au-PLf"/> <constraint firstItem="z04-yg-x1t" firstAttribute="top" relation="greaterThanOrEqual" secondItem="F8U-ab-fOM" secondAttribute="top" constant="10" id="fLp-au-PLf"/>
<constraint firstItem="z04-yg-x1t" firstAttribute="centerX" secondItem="F8U-ab-fOM" secondAttribute="centerX" id="fiy-Zt-GmB"/> <constraint firstItem="z04-yg-x1t" firstAttribute="centerX" secondItem="F8U-ab-fOM" secondAttribute="centerX" id="fiy-Zt-GmB"/>
<constraint firstItem="Who-nd-jyt" firstAttribute="centerY" secondItem="F8U-ab-fOM" secondAttribute="centerY" id="tV3-4W-6Ha"/>
</constraints> </constraints>
</view> </view>
<vibrancyEffect style="secondaryLabel"> <vibrancyEffect style="secondaryLabel">
@@ -679,8 +737,6 @@
</constraints> </constraints>
<connections> <connections>
<outlet property="blurView" destination="7iO-O4-Mr9" id="kQ4-9N-nnv"/> <outlet property="blurView" destination="7iO-O4-Mr9" id="kQ4-9N-nnv"/>
<outlet property="button" destination="Who-nd-jyt" id="EA8-Jn-NJs"/>
<outlet property="textLabel" destination="z04-yg-x1t" id="njE-fn-vxd"/>
</connections> </connections>
</collectionViewCell> </collectionViewCell>
</cells> </cells>
@@ -709,7 +765,7 @@
</stackView> </stackView>
</subviews> </subviews>
<constraints> <constraints>
<constraint firstAttribute="bottom" secondItem="GFQ-Wy-Qhy" secondAttribute="bottom" priority="999" constant="8" id="HGl-P6-G2v"/> <constraint firstAttribute="bottom" secondItem="GFQ-Wy-Qhy" secondAttribute="bottom" constant="8" id="HGl-P6-G2v"/>
<constraint firstItem="GFQ-Wy-Qhy" firstAttribute="top" secondItem="HYs-co-nJZ" secondAttribute="top" id="gg9-XU-2ej"/> <constraint firstItem="GFQ-Wy-Qhy" firstAttribute="top" secondItem="HYs-co-nJZ" secondAttribute="top" id="gg9-XU-2ej"/>
<constraint firstItem="GFQ-Wy-Qhy" firstAttribute="centerX" secondItem="HYs-co-nJZ" secondAttribute="centerX" id="vyo-h4-yD9"/> <constraint firstItem="GFQ-Wy-Qhy" firstAttribute="centerX" secondItem="HYs-co-nJZ" secondAttribute="centerX" id="vyo-h4-yD9"/>
</constraints> </constraints>
@@ -736,56 +792,7 @@
</collectionViewController> </collectionViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="kiO-UO-esV" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="kiO-UO-esV" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="1729" y="716"/> <point key="canvasLocation" x="1728.8" y="716.49175412293857"/>
</scene>
<!--Featured View Controller-->
<scene sceneID="1eF-L7-aZz">
<objects>
<collectionViewController storyboardIdentifier="featuredViewController" id="KKu-kI-2kg" customClass="FeaturedViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" id="2HL-eH-weG">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<collectionViewFlowLayout key="collectionViewLayout" automaticEstimatedItemSize="YES" minimumLineSpacing="10" minimumInteritemSpacing="10" id="PI1-YC-d4l">
<size key="itemSize" width="128" height="128"/>
<size key="headerReferenceSize" width="0.0" height="0.0"/>
<size key="footerReferenceSize" width="0.0" height="0.0"/>
<inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>
</collectionViewFlowLayout>
<cells>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="Cell" id="Eo1-84-9m0">
<rect key="frame" x="0.0" y="0.0" width="128" height="128"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<collectionViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="4ra-vw-qNw">
<rect key="frame" x="0.0" y="0.0" width="128" height="128"/>
<autoresizingMask key="autoresizingMask"/>
</collectionViewCellContentView>
</collectionViewCell>
</cells>
<connections>
<outlet property="dataSource" destination="KKu-kI-2kg" id="tXR-fi-SxU"/>
<outlet property="delegate" destination="KKu-kI-2kg" id="XC4-MP-Zdr"/>
</connections>
</collectionView>
<navigationItem key="navigationItem" id="zft-Mo-I7C"/>
<connections>
<segue destination="e3L-BF-iXp" kind="show" identifier="showBrowseViewController" destinationCreationSelector="makeBrowseViewController:sender:" id="qDq-A7-sdW"/>
<segue destination="177-gr-dJU" kind="show" identifier="showSourceDetails" destinationCreationSelector="makeSourceDetailViewController:sender:" id="dmC-aP-9Hg"/>
</connections>
</collectionViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Hwb-Di-x8C" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1729" y="-19"/>
</scene>
<!--sourceDetailViewController-->
<scene sceneID="nDc-kS-RDF">
<objects>
<viewControllerPlaceholder storyboardName="Sources" referencedIdentifier="sourceDetailViewController" id="177-gr-dJU" sceneMemberID="viewController">
<navigationItem key="navigationItem" id="7hT-A6-bBi"/>
</viewControllerPlaceholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="bhw-oh-Eeq" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2730" y="-21"/>
</scene> </scene>
<!--App IDs--> <!--App IDs-->
<scene sceneID="kvf-US-rRe"> <scene sceneID="kvf-US-rRe">
@@ -802,22 +809,30 @@
<inset key="sectionInset" minX="0.0" minY="10" maxX="0.0" maxY="20"/> <inset key="sectionInset" minX="0.0" minY="10" maxX="0.0" maxY="20"/>
</collectionViewFlowLayout> </collectionViewFlowLayout>
<cells> <cells>
<collectionViewCell opaque="NO" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" reuseIdentifier="Cell" id="XWu-DU-xbh" customClass="AppBannerCollectionViewCell" customModule="SideStore" customModuleProvider="target"> <collectionViewCell opaque="NO" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" reuseIdentifier="Cell" id="XWu-DU-xbh" customClass="BannerCollectionViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="70" width="375" height="80"/> <rect key="frame" x="0.0" y="70" width="375" height="80"/>
<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="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" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="1w8-fI-98T" customClass="AppBannerView" customModule="SideStore" customModuleProvider="target"> <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"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<accessibility key="accessibilityConfiguration"> <accessibility key="accessibilityConfiguration">
<bool key="isElement" value="YES"/> <bool key="isElement" value="YES"/>
</accessibility> </accessibility>
</view> </view>
</subviews> </subviews>
</view> </view>
<constraints>
<constraint firstAttribute="trailingMargin" secondItem="1w8-fI-98T" secondAttribute="trailing" id="0bS-49-dqo"/>
<constraint firstAttribute="bottom" secondItem="1w8-fI-98T" secondAttribute="bottom" id="Bif-xB-0gt"/>
<constraint firstItem="1w8-fI-98T" firstAttribute="top" secondItem="XWu-DU-xbh" secondAttribute="top" id="aEf-KK-MHU"/>
<constraint firstItem="1w8-fI-98T" firstAttribute="leading" secondItem="XWu-DU-xbh" secondAttribute="leadingMargin" id="mFW-ti-cVB"/>
</constraints>
<connections>
<outlet property="bannerView" destination="1w8-fI-98T" id="OH8-L9-TZn"/>
</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="SideStore" customModuleProvider="target"> <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">
@@ -866,9 +881,9 @@
</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" 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>
@@ -885,7 +900,7 @@
<placeholder placeholderIdentifier="IBFirstResponder" id="Lvd-jC-AZO" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="Lvd-jC-AZO" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
<exit id="eS1-sQ-VUA" userLabel="Exit" sceneMemberID="exit"/> <exit id="eS1-sQ-VUA" userLabel="Exit" sceneMemberID="exit"/>
</objects> </objects>
<point key="canvasLocation" x="3506" y="1121"/> <point key="canvasLocation" x="3301.5999999999999" y="715.59220389805103"/>
</scene> </scene>
<!--News--> <!--News-->
<scene sceneID="BV8-6J-nIv"> <scene sceneID="BV8-6J-nIv">
@@ -894,7 +909,7 @@
<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">
<rect key="frame" x="0.0" y="20" width="375" height="96"/> <rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<edgeInsets key="layoutMargins" top="8" left="20" bottom="8" right="8"/> <edgeInsets key="layoutMargins" top="8" left="20" bottom="8" right="8"/>
</navigationBar> </navigationBar>
@@ -913,9 +928,8 @@
<navigationController automaticallyAdjustsScrollViewInsets="NO" 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"/>
<edgeInsets key="layoutMargins" top="8" left="20" bottom="8" right="8"/>
</navigationBar> </navigationBar>
<nil name="viewControllers"/> <nil name="viewControllers"/>
<connections> <connections>
@@ -924,40 +938,176 @@
</navigationController> </navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="3LN-mt-qWn" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="3LN-mt-qWn" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="2730" y="1120"/> <point key="canvasLocation" x="2526" y="731"/>
</scene> </scene>
<!--Sources--> <!--Sources-->
<scene sceneID="Vzf-tb-LIH"> <scene sceneID="0S1-zn-9KZ">
<objects> <objects>
<viewControllerPlaceholder storyboardName="Sources" id="HCK-G6-KdY" sceneMemberID="viewController"> <collectionViewController title="Sources" id="cHC-TX-KzQ" customClass="SourcesViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<tabBarItem key="tabBarItem" title="Item" id="Q7y-bi-ncT"/> <collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" alwaysBounceVertical="YES" dataMode="prototypes" id="S36-hD-vu2">
</viewControllerPlaceholder> <rect key="frame" x="0.0" y="0.0" width="375" height="647"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="VTd-he-VYb" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="15" minimumInteritemSpacing="10" id="X20-b5-XEP">
<size key="itemSize" width="375" height="80"/>
<size key="headerReferenceSize" width="50" height="200"/>
<size key="footerReferenceSize" width="50" height="50"/>
<inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>
</collectionViewFlowLayout>
<cells>
<collectionViewCell opaque="NO" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" reuseIdentifier="Cell" id="XcN-o4-9qm" customClass="BannerCollectionViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="200" width="375" height="80"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<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="80"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<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"/>
<accessibility key="accessibilityConfiguration">
<bool key="isElement" value="YES"/>
</accessibility>
</view>
</subviews>
</view>
<constraints>
<constraint firstAttribute="bottom" secondItem="LW1-CC-bWu" secondAttribute="bottom" id="Pkr-zO-0wx"/>
<constraint firstItem="LW1-CC-bWu" firstAttribute="leading" secondItem="XcN-o4-9qm" secondAttribute="leadingMargin" id="egJ-X3-yEz"/>
<constraint firstItem="LW1-CC-bWu" firstAttribute="top" secondItem="XcN-o4-9qm" secondAttribute="top" id="glF-aM-4xQ"/>
<constraint firstAttribute="trailingMargin" secondItem="LW1-CC-bWu" secondAttribute="trailing" id="tQx-yV-LTq"/>
</constraints>
<connections>
<outlet property="bannerView" destination="LW1-CC-bWu" id="mwO-Ne-L1L"/>
</connections>
</collectionViewCell>
</cells>
<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"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Manage sources to control which apps are available to download through SideStore." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="TZv-TM-uJj">
<rect key="frame" x="8" y="14" width="359" height="171"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCallout"/>
<color key="textColor" white="0.5" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstItem="TZv-TM-uJj" firstAttribute="top" secondItem="8N7-JY-mcA" secondAttribute="top" constant="14" id="2zE-UV-24S"/>
<constraint firstAttribute="bottom" secondItem="TZv-TM-uJj" secondAttribute="bottom" constant="15" id="Aml-PC-dko"/>
<constraint firstAttribute="trailingMargin" secondItem="TZv-TM-uJj" secondAttribute="trailing" id="V0U-al-5eb"/>
<constraint firstAttribute="leadingMargin" secondItem="TZv-TM-uJj" secondAttribute="leading" id="aS5-6Y-rMd"/>
</constraints>
<connections>
<outlet property="bottomLayoutConstraint" destination="Aml-PC-dko" id="I1s-ae-C8A"/>
<outlet property="leadingLayoutConstraint" destination="aS5-6Y-rMd" id="An8-KN-xfb"/>
<outlet property="textLabel" destination="TZv-TM-uJj" id="kWV-Wv-5gz"/>
<outlet property="topLayoutConstraint" destination="2zE-UV-24S" id="mjq-yH-v8J"/>
<outlet property="trailingLayoutConstraint" destination="V0U-al-5eb" id="z8b-2G-SgY"/>
</connections>
</collectionReusableView>
<collectionReusableView key="sectionFooterView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Footer" id="X5B-Kp-w1p" customClass="SourcesFooterView">
<rect key="frame" x="0.0" y="280" width="375" height="50"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="j0O-xE-gyd">
<rect key="frame" x="8" y="0.0" width="359" height="50"/>
<subviews>
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" animating="YES" style="medium" translatesAutoresizingMaskIntoConstraints="NO" id="PNx-uR-y2F">
<rect key="frame" x="0.0" y="0.0" width="359" height="0.0"/>
</activityIndicatorView>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" verticalCompressionResistancePriority="800" scrollEnabled="NO" contentInsetAdjustmentBehavior="never" editable="NO" text="Test" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="66c-H8-KJx">
<rect key="frame" x="0.0" y="15" width="359" height="35"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<color key="textColor" white="0.5" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
</subviews>
</stackView>
</subviews>
<constraints>
<constraint firstAttribute="bottom" secondItem="j0O-xE-gyd" secondAttribute="bottom" id="BQ5-11-BzK"/>
<constraint firstItem="j0O-xE-gyd" firstAttribute="top" secondItem="X5B-Kp-w1p" secondAttribute="top" id="KZg-fd-8Cp" propertyAccessControl="none"/>
<constraint firstItem="j0O-xE-gyd" firstAttribute="leading" secondItem="X5B-Kp-w1p" secondAttribute="leadingMargin" id="R2x-Io-bXD"/>
<constraint firstAttribute="trailingMargin" secondItem="j0O-xE-gyd" secondAttribute="trailing" id="aBK-Bq-P9O"/>
</constraints>
<connections>
<outlet property="activityIndicatorView" destination="PNx-uR-y2F" id="7Le-VW-GYK"/>
<outlet property="bottomLayoutConstraint" destination="BQ5-11-BzK" id="iJR-4o-u9l"/>
<outlet property="leadingLayoutConstraint" destination="R2x-Io-bXD" id="plZ-Yj-zTc"/>
<outlet property="textView" destination="66c-H8-KJx" id="kwc-OH-U6i"/>
<outlet property="topLayoutConstraint" destination="KZg-fd-8Cp" id="zNM-UU-feF"/>
<outlet property="trailingLayoutConstraint" destination="aBK-Bq-P9O" id="L2r-VL-ruT"/>
</connections>
</collectionReusableView>
<connections>
<outlet property="dataSource" destination="cHC-TX-KzQ" id="VHQ-ls-gde"/>
<outlet property="delegate" destination="cHC-TX-KzQ" id="MWr-Xg-N2k"/>
</connections>
</collectionView>
<navigationItem key="navigationItem" title="Sources" id="QTB-W7-6BG">
<barButtonItem key="leftBarButtonItem" systemItem="add" id="kBB-5c-8gw">
<connections>
<action selector="addSource" destination="cHC-TX-KzQ" id="WiB-Jg-NzT"/>
</connections>
</barButtonItem>
<barButtonItem key="rightBarButtonItem" style="done" systemItem="done" id="NQF-u2-PZv">
<connections>
<segue destination="zjS-Nr-VTw" kind="unwind" unwindAction="unwindFromSourcesViewController:" id="la1-dJ-UhL"/>
</connections>
</barButtonItem>
</navigationItem>
</collectionViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="TrV-p3-ZAt" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
<exit id="zjS-Nr-VTw" userLabel="Exit" sceneMemberID="exit"/>
</objects> </objects>
<point key="canvasLocation" x="-2" y="550"/> <point key="canvasLocation" x="3302" y="1430"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="6NV-LQ-gKB">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="Qo4-72-Hmr" sceneMemberID="viewController">
<toolbarItems/>
<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="96"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
<segue destination="cHC-TX-KzQ" kind="relationship" relationship="rootViewController" id="BC5-Fs-dCj"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="4mO-93-4qk" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2526" y="1445"/>
</scene> </scene>
</scenes> </scenes>
<inferredMetricsTieBreakers> <inferredMetricsTieBreakers>
<segue reference="Qd6-ba-dIo"/>
<segue reference="cnd-KK-o60"/> <segue reference="cnd-KK-o60"/>
</inferredMetricsTieBreakers> </inferredMetricsTieBreakers>
<color key="tintColor" name="Primary"/> <color key="tintColor" name="Primary"/>
<resources> <resources>
<image name="Back" width="18" height="18"/> <image name="Back" width="18" height="18"/>
<image name="Browse" width="128" height="128"/> <image name="Browse" width="20" height="20"/>
<image name="MyApps" width="20" height="20"/> <image name="MyApps" width="20" height="20"/>
<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"/>
</systemColor> </systemColor>
<systemColor name="tertiarySystemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources> </resources>
</document> </document>

View File

@@ -1,692 +0,0 @@
//
// BrowseViewController.swift
// AltStore
//
// Created by Riley Testut on 7/15/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
import Combine
import minimuxer
import AltStoreCore
import Roxas
import Nuke
class BrowseViewController: UICollectionViewController, PeekPopPreviewing
{
// Nil == Show apps from all sources.
let source: Source?
private(set) var category: StoreCategory? {
didSet {
self.updateDataSource()
self.update()
}
}
var searchPredicate: NSPredicate? {
didSet {
self.updateDataSource()
}
}
private lazy var dataSource = self.makeDataSource()
private lazy var placeholderView = RSTPlaceholderView(frame: .zero)
private let prototypeCell = AppCardCollectionViewCell(frame: .zero)
private var sortButton: UIBarButtonItem?
private var preferredAppSorting: AppSorting = UserDefaults.shared.preferredAppSorting
private var cancellables = Set<AnyCancellable>()
private var titleStackView: UIStackView!
private var titleSourceIconView: AppIconImageView!
private var titleCategoryIconView: UIImageView!
private var titleLabel: UILabel!
init?(source: Source?, coder: NSCoder)
{
self.source = source
self.category = nil
super.init(coder: coder)
}
init?(category: StoreCategory?, coder: NSCoder)
{
self.source = nil
self.category = category
super.init(coder: coder)
}
required init?(coder: NSCoder)
{
self.source = nil
self.category = nil
super.init(coder: coder)
}
private var cachedItemSizes = [String: CGSize]()
@IBOutlet private var sourcesBarButtonItem: UIBarButtonItem!
override func viewDidLoad()
{
super.viewDidLoad()
self.collectionView.backgroundColor = .altBackground
self.collectionView.alwaysBounceVertical = true
self.dataSource.searchController.searchableKeyPaths = [#keyPath(StoreApp.name),
#keyPath(StoreApp.subtitle),
#keyPath(StoreApp.developerName),
#keyPath(StoreApp.bundleIdentifier)]
self.navigationItem.searchController = self.dataSource.searchController
self.prototypeCell.contentView.translatesAutoresizingMaskIntoConstraints = false
self.collectionView.register(AppCardCollectionViewCell.self, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
self.collectionView.dataSource = self.dataSource
self.collectionView.prefetchDataSource = self.dataSource
let collectionViewLayout = self.collectionViewLayout as! UICollectionViewFlowLayout
collectionViewLayout.minimumLineSpacing = 30
(self as PeekPopPreviewing).registerForPreviewing(with: self, sourceView: self.collectionView)
let refreshControl = UIRefreshControl(frame: .zero, primaryAction: UIAction { [weak self] _ in
self?.updateSources()
})
self.collectionView.refreshControl = refreshControl
if self.category != nil, #available(iOS 16, *)
{
let categoriesMenu = UIMenu(children: [
UIDeferredMenuElement.uncached { [weak self] completion in
let actions = self?.makeCategoryActions() ?? []
completion(actions)
}
])
self.navigationItem.titleMenuProvider = { _ in categoriesMenu }
}
self.titleSourceIconView = AppIconImageView(style: .circular)
self.titleCategoryIconView = UIImageView(frame: .zero)
self.titleCategoryIconView.contentMode = .scaleAspectFit
self.titleLabel = UILabel()
self.titleLabel.font = UIFont.preferredFont(forTextStyle: .headline)
self.titleStackView = UIStackView(arrangedSubviews: [self.titleSourceIconView, self.titleCategoryIconView, self.titleLabel])
self.titleStackView.spacing = 4
self.titleStackView.translatesAutoresizingMaskIntoConstraints = false
self.navigationItem.largeTitleDisplayMode = .never
if #available(iOS 16, *)
{
self.navigationItem.preferredSearchBarPlacement = .automatic
}
if #available(iOS 15, *)
{
self.prepareAppSorting()
}
self.preparePipeline()
NSLayoutConstraint.activate([
// Source icon = equal width and height
self.titleSourceIconView.heightAnchor.constraint(equalToConstant: 26),
self.titleSourceIconView.widthAnchor.constraint(equalTo: self.titleSourceIconView.heightAnchor),
// Category icon = constant height, variable widths
self.titleCategoryIconView.heightAnchor.constraint(equalToConstant: 26)
])
self.updateDataSource()
self.update()
}
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
self.update()
}
override func viewDidDisappear(_ animated: Bool)
{
super.viewDidDisappear(animated)
self.navigationController?.navigationBar.tintColor = nil
}
}
private extension BrowseViewController
{
func preparePipeline()
{
AppManager.shared.$updateSourcesResult
.receive(on: RunLoop.main) // Delay to next run loop so we receive _current_ value (not previous value).
.sink { [weak self] result in
self?.update()
}
.store(in: &self.cancellables)
}
func makeFetchRequest() -> NSFetchRequest<StoreApp>
{
let fetchRequest = StoreApp.fetchRequest() as NSFetchRequest<StoreApp>
fetchRequest.returnsObjectsAsFaults = false
let predicate = StoreApp.visibleAppsPredicate
if let source = self.source
{
let filterPredicate = NSPredicate(format: "%K == %@", #keyPath(StoreApp._source), source)
fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [filterPredicate, predicate])
}
else if let category = self.category
{
let categoryPredicate = switch category {
case .other: StoreApp.otherCategoryPredicate
default: NSPredicate(format: "%K == %@", #keyPath(StoreApp._category), category.rawValue)
}
fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [categoryPredicate, predicate])
}
else
{
fetchRequest.predicate = predicate
}
var sortDescriptors = [NSSortDescriptor(keyPath: \StoreApp.name, ascending: true),
NSSortDescriptor(keyPath: \StoreApp.bundleIdentifier, ascending: true),
NSSortDescriptor(keyPath: \StoreApp.sourceIdentifier, ascending: true)]
switch self.preferredAppSorting
{
case .default:
let descriptor = NSSortDescriptor(keyPath: \StoreApp.sortIndex, ascending: self.preferredAppSorting.isAscending)
sortDescriptors.insert(descriptor, at: 0)
case .name:
// Already sorting by name, no need to prepend additional sort descriptor.
break
case .developer:
let descriptor = NSSortDescriptor(keyPath: \StoreApp.developerName, ascending: self.preferredAppSorting.isAscending)
sortDescriptors.insert(descriptor, at: 0)
case .lastUpdated:
let descriptor = NSSortDescriptor(keyPath: \StoreApp.latestSupportedVersion?.date, ascending: self.preferredAppSorting.isAscending)
sortDescriptors.insert(descriptor, at: 0)
}
fetchRequest.sortDescriptors = sortDescriptors
return fetchRequest
}
func makeDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>
{
let fetchRequest = self.makeFetchRequest()
let context = self.source?.managedObjectContext ?? DatabaseManager.shared.viewContext
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>(fetchRequest: fetchRequest, managedObjectContext: context)
dataSource.placeholderView = self.placeholderView
dataSource.cellConfigurationHandler = { [weak self] (cell, app, indexPath) in
guard let self else { return }
let cell = cell as! AppCardCollectionViewCell
cell.layoutMargins.left = self.view.layoutMargins.left
cell.layoutMargins.right = self.view.layoutMargins.right
let showSourceIcon = (self.source == nil) // Hide source icon if redundant
cell.configure(for: app, showSourceIcon: showSourceIcon)
cell.bannerView.iconImageView.image = nil
cell.bannerView.iconImageView.isIndicatingActivity = true
cell.bannerView.button.addTarget(self, action: #selector(BrowseViewController.performAppAction(_:)), for: .primaryActionTriggered)
cell.bannerView.button.activityIndicatorView.style = .medium
cell.bannerView.button.activityIndicatorView.color = .white
let tintColor = app.tintColor ?? .altPrimary
cell.tintColor = tintColor
}
dataSource.prefetchHandler = { (storeApp, indexPath, completionHandler) -> Foundation.Operation? in
let iconURL = storeApp.iconURL
return RSTAsyncBlockOperation() { (operation) in
ImagePipeline.shared.loadImage(with: iconURL, progress: nil) { result in
guard !operation.isCancelled else { return operation.finish() }
switch result
{
case .success(let response): completionHandler(response.image, nil)
case .failure(let error): completionHandler(nil, error)
}
}
}
}
dataSource.prefetchCompletionHandler = { [weak dataSource] (cell, image, indexPath, error) in
let cell = cell as! AppCardCollectionViewCell
cell.bannerView.iconImageView.isIndicatingActivity = false
cell.bannerView.iconImageView.image = image
if let error = error, let dataSource
{
let app = dataSource.item(at: indexPath)
Logger.main.debug("Failed to load app icon from \(app.iconURL, privacy: .public). \(error.localizedDescription, privacy: .public)")
}
}
return dataSource
}
func updateDataSource()
{
let fetchRequest = self.makeFetchRequest()
let context = self.source?.managedObjectContext ?? DatabaseManager.shared.viewContext
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
self.dataSource.fetchedResultsController = fetchedResultsController
self.dataSource.predicate = self.searchPredicate
}
func updateSources()
{
AppManager.shared.updateAllSources { result in
self.collectionView.refreshControl?.endRefreshing()
guard case .failure(let error) = result else { return }
if self.dataSource.itemCount > 0
{
let toastView = ToastView(error: error)
toastView.addTarget(nil, action: #selector(TabBarController.presentSources), for: .touchUpInside)
toastView.show(in: self)
}
}
}
func update()
{
if self.searchPredicate != nil
{
self.placeholderView.textLabel.text = NSLocalizedString("No Apps", comment: "")
self.placeholderView.textLabel.isHidden = false
self.placeholderView.detailTextLabel.text = NSLocalizedString("Please make sure your spelling is correct, or try searching for another app.", comment: "")
self.placeholderView.detailTextLabel.isHidden = false
self.placeholderView.activityIndicatorView.stopAnimating()
}
else
{
switch AppManager.shared.updateSourcesResult
{
case nil:
self.placeholderView.textLabel.isHidden = true
self.placeholderView.detailTextLabel.isHidden = false
self.placeholderView.detailTextLabel.text = NSLocalizedString("Loading...", comment: "")
self.placeholderView.activityIndicatorView.startAnimating()
case .failure(let error):
self.placeholderView.textLabel.isHidden = false
self.placeholderView.detailTextLabel.isHidden = false
self.placeholderView.textLabel.text = NSLocalizedString("Unable to Fetch Apps", comment: "")
self.placeholderView.detailTextLabel.text = error.localizedDescription
self.placeholderView.activityIndicatorView.stopAnimating()
case .success:
self.placeholderView.textLabel.text = NSLocalizedString("No Apps", comment: "")
self.placeholderView.textLabel.isHidden = false
self.placeholderView.detailTextLabel.isHidden = true
self.placeholderView.activityIndicatorView.stopAnimating()
}
}
let tintColor: UIColor
if let source = self.source
{
tintColor = source.effectiveTintColor?.adjustedForDisplay ?? .altPrimary
self.title = source.name
self.titleSourceIconView.backgroundColor = tintColor
self.titleSourceIconView.isHidden = false
self.titleCategoryIconView.isHidden = true
if let iconURL = source.effectiveIconURL
{
Nuke.loadImage(with: iconURL, into: self.titleSourceIconView) { result in
switch result
{
case .failure(let error): Logger.main.error("Failed to fetch source icon at \(iconURL, privacy: .public). \(error.localizedDescription, privacy: .public)")
case .success: self.titleSourceIconView.backgroundColor = .white
}
}
}
}
else if let category = self.category
{
tintColor = category.tintColor
self.title = category.localizedName
let image = UIImage(systemName: category.filledSymbolName)?.withTintColor(tintColor, renderingMode: .alwaysOriginal)
self.titleCategoryIconView.image = image
self.titleCategoryIconView.isHidden = false
self.titleSourceIconView.isHidden = true
}
else
{
tintColor = .altPrimary
self.title = NSLocalizedString("Browse", comment: "")
self.titleSourceIconView.isHidden = true
self.titleCategoryIconView.isHidden = true
}
self.titleLabel.text = self.title
self.titleStackView.sizeToFit()
self.navigationItem.titleView = self.titleStackView
self.view.tintColor = tintColor
let appearance = NavigationBarAppearance()
appearance.configureWithTintColor(tintColor)
appearance.configureWithDefaultBackground()
let edgeAppearance = appearance.copy()
edgeAppearance.configureWithTransparentBackground()
self.navigationItem.standardAppearance = appearance
self.navigationItem.scrollEdgeAppearance = edgeAppearance
// Necessary to tint UISearchController's inline bar button.
self.navigationController?.navigationBar.tintColor = tintColor
if let sortButton
{
sortButton.image = sortButton.image?.withTintColor(tintColor, renderingMode: .alwaysOriginal)
}
}
func makeCategoryActions() -> [UIAction]
{
let handler = { [weak self] (category: StoreCategory) in
self?.category = category
}
let fetchRequest = NSFetchRequest(entityName: StoreApp.entity().name!) as NSFetchRequest<NSDictionary>
fetchRequest.resultType = .dictionaryResultType
fetchRequest.returnsDistinctResults = true
fetchRequest.propertiesToFetch = [#keyPath(StoreApp._category)]
fetchRequest.predicate = StoreApp.visibleAppsPredicate
do
{
let dictionaries = try DatabaseManager.shared.viewContext.fetch(fetchRequest)
// Keep nil values
let categories = dictionaries.map { $0[#keyPath(StoreApp._category)] as? String? ?? nil }.map { rawCategory -> StoreCategory in
guard let rawCategory else { return .other }
return StoreCategory(rawValue: rawCategory) ?? .other
}
var sortedCategories = Set(categories).sorted(by: { $0.localizedName.localizedStandardCompare($1.localizedName) == .orderedAscending })
if let otherIndex = sortedCategories.firstIndex(of: .other)
{
// Ensure "Other" is always last
sortedCategories.move(fromOffsets: [otherIndex], toOffset: sortedCategories.count)
}
let actions = sortedCategories.map { category in
let state: UIAction.State = (category == self.category) ? .on : .off
let image = UIImage(systemName: category.filledSymbolName)?.withTintColor(category.tintColor, renderingMode: .alwaysOriginal)
return UIAction(title: category.localizedName, image: image, state: state) { _ in
handler(category)
}
}
return actions
}
catch
{
Logger.main.error("Failed to fetch categories. \(error.localizedDescription, privacy: .public)")
return []
}
}
@available(iOS 15, *)
func prepareAppSorting()
{
if self.preferredAppSorting == .default && self.source == nil
{
// Only allow `default` sorting if source is non-nil.
// Otherwise, fall back to `lastUpdated` sorting.
self.preferredAppSorting = .lastUpdated
// Don't update UserDefaults unless explicitly changed by user.
// UserDefaults.shared.preferredAppSorting = .lastUpdated
}
let children = UIDeferredMenuElement.uncached { [weak self] completion in
guard let self else { return completion([]) }
var sortingOptions = AppSorting.allCases
if self.source == nil
{
// Only allow `default` sorting when source is non-nil.
sortingOptions = sortingOptions.filter { $0 != .default }
}
let actions = sortingOptions.map { sorting in
let state: UIMenuElement.State = (sorting == self.preferredAppSorting) ? .on : .off
let action = UIAction(title: sorting.localizedName, image: nil, state: state) { action in
self.preferredAppSorting = sorting
UserDefaults.shared.preferredAppSorting = sorting // Update separately to save change.
self.updateDataSource()
}
return action
}
completion(actions)
}
let sortMenu = UIMenu(title: NSLocalizedString("Sort by…", comment: ""), options: [.singleSelection], children: [children])
let sortIcon = UIImage(systemName: "arrow.up.arrow.down")
let sortButton = UIBarButtonItem(title: NSLocalizedString("Sort by…", comment: ""), image: sortIcon, primaryAction: nil, menu: sortMenu)
self.sortButton = sortButton
self.navigationItem.rightBarButtonItems = [sortButton]
}
}
private extension BrowseViewController
{
@IBAction func performAppAction(_ sender: PillButton)
{
let point = self.collectionView.convert(sender.center, from: sender.superview)
guard let indexPath = self.collectionView.indexPathForItem(at: point) else { return }
let app = self.dataSource.item(at: indexPath)
// if let installedApp = app.installedApp, !installedApp.isUpdateAvailable
if let installedApp = app.installedApp, !installedApp.hasUpdate
{
self.open(installedApp)
}
else
{
self.install(app, at: indexPath)
}
}
func install(_ app: StoreApp, at indexPath: IndexPath)
{
let previousProgress = AppManager.shared.installationProgress(for: app)
guard previousProgress == nil else {
previousProgress?.cancel()
return
}
if !minimuxer.ready() {
let toastView = ToastView(error: MinimuxerError.NoConnection)
toastView.show(in: self)
return
}
Task<Void, Never>(priority: .userInitiated) { @MainActor in
// if let installedApp = app.installedApp, installedApp.isUpdateAvailable
if let installedApp = app.installedApp, installedApp.hasUpdate
{
AppManager.shared.update(installedApp, presentingViewController: self, completionHandler: finish(_:))
}
else
{
await AppManager.shared.installAsync(app, presentingViewController: self, completionHandler: finish(_:))
}
UIView.performWithoutAnimation {
self.collectionView.reloadItems(at: [indexPath])
}
}
@MainActor
func finish(_ result: Result<InstalledApp, Error>)
{
DispatchQueue.main.async {
switch result
{
case .failure(OperationError.cancelled): break // Ignore
case .failure(let error):
let toastView = ToastView(error: error, opensLog: true)
toastView.show(in: self)
case .success: print("Installed app:", app.bundleIdentifier)
}
UIView.performWithoutAnimation {
if let indexPath = self.dataSource.fetchedResultsController.indexPath(forObject: app)
{
self.collectionView.reloadItems(at: [indexPath])
}
else
{
self.collectionView.reloadSections(IndexSet(integer: indexPath.section))
}
}
}
}
}
func open(_ installedApp: InstalledApp)
{
UIApplication.shared.open(installedApp.openAppURL)
}
}
extension BrowseViewController: UICollectionViewDelegateFlowLayout
{
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
let item = self.dataSource.item(at: indexPath)
let itemID = item.globallyUniqueID ?? item.bundleIdentifier
if let previousSize = self.cachedItemSizes[itemID]
{
return previousSize
}
self.dataSource.cellConfigurationHandler(self.prototypeCell, item, indexPath)
let insets = (self.view.layoutMargins.left + self.view.layoutMargins.right)
let widthConstraint = self.prototypeCell.contentView.widthAnchor.constraint(equalToConstant: collectionView.bounds.width - insets)
widthConstraint.isActive = true
defer { widthConstraint.isActive = false }
// Manually update cell width & layout so we can accurately calculate screenshot sizes.
self.prototypeCell.frame.size.width = widthConstraint.constant
self.prototypeCell.layoutIfNeeded()
let itemSize = self.prototypeCell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
self.cachedItemSizes[itemID] = itemSize
return itemSize
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
{
let app = self.dataSource.item(at: indexPath)
let appViewController = AppViewController.makeAppViewController(app: app)
// Fall back to presentingViewController.navigationController in case we're being used for search results.
let navigationController = self.navigationController ?? self.presentingViewController?.navigationController
navigationController?.pushViewController(appViewController, animated: true)
}
}
extension BrowseViewController: UIViewControllerPreviewingDelegate
{
@available(iOS, deprecated: 13.0)
func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController?
{
guard
let indexPath = self.collectionView.indexPathForItem(at: location),
let cell = self.collectionView.cellForItem(at: indexPath)
else { return nil }
previewingContext.sourceRect = cell.frame
let app = self.dataSource.item(at: indexPath)
let appViewController = AppViewController.makeAppViewController(app: app)
return appViewController
}
@available(iOS, deprecated: 13.0)
func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController)
{
self.navigationController?.pushViewController(viewControllerToCommit, animated: true)
}
}
@available(iOS 17, *)
#Preview(traits: .portrait) {
DatabaseManager.shared.startForPreview()
let storyboard = UIStoryboard(name: "Main", bundle: .main)
let browseViewController = storyboard.instantiateViewController(identifier: "browseViewController") { coder in
BrowseViewController(source: nil, coder: coder)
}
let navigationController = UINavigationController(rootViewController: browseViewController)
return navigationController
}

View File

@@ -1,100 +0,0 @@
//
// FeaturedComponents.swift
// AltStore
//
// Created by Riley Testut on 12/4/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import UIKit
class LargeIconCollectionViewCell: UICollectionViewCell
{
let textLabel = UILabel(frame: .zero)
let imageView = UIImageView(frame: .zero)
override init(frame: CGRect)
{
self.textLabel.translatesAutoresizingMaskIntoConstraints = false
self.textLabel.textColor = .white
self.textLabel.font = .preferredFont(forTextStyle: .headline)
self.imageView.translatesAutoresizingMaskIntoConstraints = false
self.imageView.contentMode = .center
self.imageView.tintColor = .white
self.imageView.alpha = 0.4
self.imageView.preferredSymbolConfiguration = .init(pointSize: 80)
super.init(frame: frame)
self.contentView.clipsToBounds = true
self.contentView.layer.cornerRadius = 16
self.contentView.layer.cornerCurve = .continuous
self.contentView.addSubview(self.textLabel)
self.contentView.addSubview(self.imageView)
NSLayoutConstraint.activate([
self.textLabel.leadingAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.leadingAnchor, constant: 4),
self.textLabel.bottomAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.bottomAnchor, constant: -4),
self.imageView.centerXAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: -30),
self.imageView.centerYAnchor.constraint(equalTo: self.contentView.centerYAnchor, constant: 0),
self.imageView.heightAnchor.constraint(equalTo: self.contentView.heightAnchor, constant: 0),
self.imageView.widthAnchor.constraint(equalTo: self.imageView.heightAnchor)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class IconButtonCollectionReusableView: UICollectionReusableView
{
let iconButton: UIButton
let titleButton: UIButton
private let stackView: UIStackView
override init(frame: CGRect)
{
let iconHeight = 26.0
self.iconButton = UIButton(type: .custom)
self.iconButton.translatesAutoresizingMaskIntoConstraints = false
self.iconButton.clipsToBounds = true
self.iconButton.layer.cornerRadius = iconHeight / 2
let content = UIListContentConfiguration.plainHeader()
self.titleButton = UIButton(type: .system)
self.titleButton.translatesAutoresizingMaskIntoConstraints = false
self.titleButton.titleLabel?.font = content.textProperties.font
self.titleButton.setTitleColor(content.textProperties.color, for: .normal)
self.stackView = UIStackView(arrangedSubviews: [self.iconButton, self.titleButton])
self.stackView.translatesAutoresizingMaskIntoConstraints = false
self.stackView.axis = .horizontal
self.stackView.alignment = .center
self.stackView.spacing = UIStackView.spacingUseSystem
self.stackView.isLayoutMarginsRelativeArrangement = false
super.init(frame: frame)
self.addSubview(self.stackView)
NSLayoutConstraint.activate([
self.iconButton.heightAnchor.constraint(equalToConstant: iconHeight),
self.iconButton.widthAnchor.constraint(equalTo: self.iconButton.heightAnchor),
self.stackView.topAnchor.constraint(equalTo: self.topAnchor),
self.stackView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
self.stackView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
self.stackView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@@ -1,747 +0,0 @@
//
// FeaturedViewController.swift
// AltStore
//
// Created by Riley Testut on 11/8/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import UIKit
import AltStoreCore
import Roxas
import Nuke
extension UIAction.Identifier
{
fileprivate static let showAllApps = Self("io.sidestore.ShowAllApps")
fileprivate static let showSourceDetails = Self("io.sidestore.ShowSourceDetails")
}
extension FeaturedViewController
{
// Open-ended because each Source is its own section
private struct Section: RawRepresentable, Equatable
{
static let recentlyUpdated = Section(rawValue: 0)
static let categories = Section(rawValue: 1)
static let featuredHeader = Section(rawValue: 2)
let rawValue: Int
var isFeaturedAppsSection: Bool {
return self.rawValue > Section.featuredHeader.rawValue
}
init(rawValue: Int)
{
self.rawValue = rawValue
}
}
private enum ReuseID: String
{
case recent = "RecentCell"
case category = "CategoryCell"
case featuredApp = "FeaturedAppCell"
}
private enum ElementKind: String
{
case sectionHeader
case sourceHeader
case button
}
}
class FeaturedViewController: UICollectionViewController
{
private lazy var dataSource = self.makeDataSource()
private lazy var recentlyUpdatedDataSource = self.makeRecentlyUpdatedDataSource()
private lazy var categoriesDataSource = self.makeCategoriesDataSource()
private lazy var featuredAppsDataSource = self.makeFeaturedAppsDataSource()
private var searchController: RSTSearchController!
private var searchBrowseViewController: BrowseViewController!
override func viewDidLoad()
{
super.viewDidLoad()
self.title = NSLocalizedString("Browse", comment: "")
let layout = Self.makeLayout()
self.collectionView.collectionViewLayout = layout
self.dataSource.proxy = self
self.collectionView.dataSource = self.dataSource
self.collectionView.prefetchDataSource = self.dataSource
self.collectionView.register(AppBannerCollectionViewCell.self, forCellWithReuseIdentifier: ReuseID.recent.rawValue)
self.collectionView.register(LargeIconCollectionViewCell.self, forCellWithReuseIdentifier: ReuseID.category.rawValue)
self.collectionView.register(AppCardCollectionViewCell.self, forCellWithReuseIdentifier: ReuseID.featuredApp.rawValue)
self.collectionView.register(UICollectionViewListCell.self, forSupplementaryViewOfKind: ElementKind.sectionHeader.rawValue, withReuseIdentifier: ElementKind.sectionHeader.rawValue)
self.collectionView.register(IconButtonCollectionReusableView.self, forSupplementaryViewOfKind: ElementKind.sourceHeader.rawValue, withReuseIdentifier: ElementKind.sourceHeader.rawValue)
self.collectionView.register(ButtonCollectionReusableView.self, forSupplementaryViewOfKind: ElementKind.button.rawValue, withReuseIdentifier: ElementKind.button.rawValue)
self.collectionView.backgroundColor = .altBackground
self.collectionView.directionalLayoutMargins.leading = 20
self.collectionView.directionalLayoutMargins.trailing = 20
let storyboard = UIStoryboard(name: "Main", bundle: nil)
self.searchBrowseViewController = storyboard.instantiateViewController(identifier: "browseViewController") { coder in
let browseViewController = BrowseViewController(coder: coder)
return browseViewController
}
self.searchController = RSTSearchController(searchResultsController: self.searchBrowseViewController)
self.searchController.searchableKeyPaths = [#keyPath(StoreApp.name),
#keyPath(StoreApp.developerName),
#keyPath(StoreApp.subtitle),
#keyPath(StoreApp.bundleIdentifier)]
self.searchController.searchHandler = { [weak searchBrowseViewController] (searchValue, _) in
searchBrowseViewController?.searchPredicate = searchValue.predicate
return nil
}
self.navigationItem.searchController = self.searchController
self.navigationItem.hidesSearchBarWhenScrolling = true
self.navigationItem.largeTitleDisplayMode = .always
}
override func viewDidAppear(_ animated: Bool)
{
super.viewDidAppear(animated)
self.navigationController?.navigationBar.tintColor = .altPrimary
}
}
private extension FeaturedViewController
{
class func makeLayout() -> UICollectionViewCompositionalLayout
{
let config = UICollectionViewCompositionalLayoutConfiguration()
config.interSectionSpacing = 0 // Must be 0 for Section.featuredHeader
config.contentInsetsReference = .layoutMargins
let layout = UICollectionViewCompositionalLayout(sectionProvider: { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
let section = Section(rawValue: sectionIndex)
let spacing = 10.0
let interSectionSpacing = 30.0
let titleSize = NSCollectionLayoutSize(widthDimension: .estimated(100), heightDimension: .estimated(30))
switch section
{
case .recentlyUpdated:
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(AppBannerView.standardHeight))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(AppBannerView.standardHeight * 2 + spacing))
let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item, item]) // 2 items per group
group.interItemSpacing = .fixed(spacing)
let layoutSection = NSCollectionLayoutSection(group: group)
layoutSection.interGroupSpacing = spacing
layoutSection.orthogonalScrollingBehavior = .groupPagingCentered
layoutSection.contentInsets.bottom = interSectionSpacing
layoutSection.boundarySupplementaryItems = [
NSCollectionLayoutBoundarySupplementaryItem(layoutSize: titleSize, elementKind: ElementKind.sectionHeader.rawValue, alignment: .topLeading)
]
return layoutSection
case .categories:
let itemWidth = (layoutEnvironment.container.effectiveContentSize.width - spacing) / 2
let itemHeight = 90.0
let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(itemWidth), heightDimension: .absolute(itemHeight))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(itemHeight))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item, item]) // 2 items per group
group.interItemSpacing = .fixed(spacing)
let layoutSection = NSCollectionLayoutSection(group: group)
layoutSection.interGroupSpacing = spacing
layoutSection.orthogonalScrollingBehavior = .none
layoutSection.contentInsets.bottom = interSectionSpacing
layoutSection.boundarySupplementaryItems = [
NSCollectionLayoutBoundarySupplementaryItem(layoutSize: titleSize, elementKind: ElementKind.sectionHeader.rawValue, alignment: .topLeading)
]
return layoutSection
case .featuredHeader:
// We don't want to show any items, so set height to 1.0
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let group = NSCollectionLayoutGroup.vertical(layoutSize: itemSize, subitems: [item])
let layoutSection = NSCollectionLayoutSection(group: group)
layoutSection.contentInsets.top = 0
layoutSection.contentInsets.bottom = 0
layoutSection.boundarySupplementaryItems = [
NSCollectionLayoutBoundarySupplementaryItem(layoutSize: titleSize, elementKind: ElementKind.sectionHeader.rawValue, alignment: .topLeading)
]
return layoutSection
case _ where section.isFeaturedAppsSection:
let itemHeight: NSCollectionLayoutDimension = if #available(iOS 17, *) { .uniformAcrossSiblings(estimate: 350) } else { .estimated(350) }
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: itemHeight)
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: itemHeight)
let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])
group.interItemSpacing = .fixed(spacing)
let titleHeader = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: titleSize, elementKind: ElementKind.sourceHeader.rawValue, alignment: .topLeading)
let buttonSize = NSCollectionLayoutSize(widthDimension: .estimated(44), heightDimension: .estimated(20))
let buttonHeader = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: buttonSize, elementKind: ElementKind.button.rawValue, alignment: .topTrailing)
let layoutSection = NSCollectionLayoutSection(group: group)
layoutSection.interGroupSpacing = spacing
layoutSection.orthogonalScrollingBehavior = .groupPagingCentered
layoutSection.contentInsets.top = 8
layoutSection.contentInsets.bottom = interSectionSpacing
layoutSection.boundarySupplementaryItems = [titleHeader, buttonHeader]
return layoutSection
default: return nil
}
}, configuration: config)
return layout
}
func makeDataSource() -> RSTCompositeCollectionViewPrefetchingDataSource<StoreApp, UIImage>
{
let featuredHeaderDataSource = RSTDynamicCollectionViewDataSource<StoreApp>()
featuredHeaderDataSource.numberOfSectionsHandler = { 1 }
featuredHeaderDataSource.numberOfItemsHandler = { _ in 0 }
let dataSource = RSTCompositeCollectionViewPrefetchingDataSource<StoreApp, UIImage>(dataSources: [self.recentlyUpdatedDataSource, self.categoriesDataSource, featuredHeaderDataSource, self.featuredAppsDataSource])
dataSource.predicate = StoreApp.visibleAppsPredicate // Ensure we never accidentally show hidden apps
return dataSource
}
func makeRecentlyUpdatedDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>
{
let fetchRequest = StoreApp.fetchRequest() as NSFetchRequest<StoreApp>
fetchRequest.returnsObjectsAsFaults = false
fetchRequest.sortDescriptors = [
NSSortDescriptor(keyPath: \StoreApp.latestSupportedVersion?.date, ascending: false),
NSSortDescriptor(keyPath: \StoreApp.name, ascending: true),
NSSortDescriptor(keyPath: \StoreApp.bundleIdentifier, ascending: true),
NSSortDescriptor(keyPath: \StoreApp.sourceIdentifier, ascending: true),
]
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
dataSource.cellIdentifierHandler = { _ in ReuseID.recent.rawValue }
dataSource.liveFetchLimit = 10 // Show 10 most recently updated apps
dataSource.cellConfigurationHandler = { cell, storeApp, indexPath in
let cell = cell as! AppBannerCollectionViewCell
cell.tintColor = storeApp.tintColor
cell.contentView.preservesSuperviewLayoutMargins = false
cell.contentView.layoutMargins = .zero
cell.bannerView.button.isIndicatingActivity = false
cell.bannerView.configure(for: storeApp)
if let versionDate = storeApp.latestSupportedVersion?.date
{
cell.bannerView.subtitleLabel.text = Date().relativeDateString(since: versionDate, dateFormatter: Date.mediumDateFormatter)
}
cell.bannerView.button.addTarget(self, action: #selector(FeaturedViewController.performAppAction), for: .primaryActionTriggered)
cell.bannerView.iconImageView.image = nil
cell.bannerView.iconImageView.isIndicatingActivity = true
}
dataSource.prefetchHandler = { (storeApp, indexPath, completion) -> Foundation.Operation? in
return RSTAsyncBlockOperation { (operation) in
storeApp.managedObjectContext?.perform {
ImagePipeline.shared.loadImage(with: storeApp.iconURL, progress: nil) { result in
guard !operation.isCancelled else { return operation.finish() }
switch result
{
case .success(let response): completion(response.image, nil)
case .failure(let error): completion(nil, error)
}
}
}
}
}
dataSource.prefetchCompletionHandler = { [weak dataSource] (cell, image, indexPath, error) in
let cell = cell as! AppBannerCollectionViewCell
cell.bannerView.iconImageView.image = image
cell.bannerView.iconImageView.isIndicatingActivity = false
if let error, let dataSource
{
let app = dataSource.item(at: indexPath)
Logger.main.debug("Failed to app icon from \(app.iconURL, privacy: .public). \(error.localizedDescription, privacy: .public)")
}
}
return dataSource
}
func makeCategoriesDataSource() -> RSTCompositeCollectionViewDataSource<StoreApp>
{
let knownCategories = StoreCategory.allCases.filter { $0 != .other }.map { $0.rawValue }
let knownFetchRequest = StoreApp.fetchRequest()
knownFetchRequest.predicate = NSPredicate(format: "%K IN %@", #keyPath(StoreApp._category), knownCategories)
knownFetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \StoreApp._category, ascending: true),
NSSortDescriptor(keyPath: \StoreApp.bundleIdentifier, ascending: true),
NSSortDescriptor(keyPath: \StoreApp.sourceIdentifier, ascending: true)]
let unknownFetchRequest = StoreApp.fetchRequest()
unknownFetchRequest.predicate = StoreApp.otherCategoryPredicate
unknownFetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \StoreApp._category, ascending: true),
NSSortDescriptor(keyPath: \StoreApp.bundleIdentifier, ascending: true),
NSSortDescriptor(keyPath: \StoreApp.sourceIdentifier, ascending: true)]
let knownController = NSFetchedResultsController(fetchRequest: knownFetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: #keyPath(StoreApp._category), cacheName: nil)
let knownDataSource = RSTFetchedResultsCollectionViewDataSource<StoreApp>(fetchedResultsController: knownController)
knownDataSource.liveFetchLimit = 1 // One app per category
let unknownController = NSFetchedResultsController(fetchRequest: unknownFetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: nil, cacheName: nil)
let unknownDataSource = RSTFetchedResultsCollectionViewDataSource<StoreApp>(fetchedResultsController: unknownController)
unknownDataSource.liveFetchLimit = 1
// Use composite data source to ensure "Other" category is always last.
let dataSource = RSTCompositeCollectionViewDataSource<StoreApp>(dataSources: [knownDataSource, unknownDataSource])
dataSource.shouldFlattenSections = true // Combine into single section, with one StoreApp per category.
dataSource.cellIdentifierHandler = { _ in ReuseID.category.rawValue }
dataSource.cellConfigurationHandler = { cell, storeApp, indexPath in
let category = storeApp.category ?? .other
let cell = cell as! LargeIconCollectionViewCell
cell.textLabel.text = category.localizedName
cell.imageView.image = UIImage(systemName: category.symbolName)
var background = UIBackgroundConfiguration.clear()
background.backgroundColor = category.tintColor
background.cornerRadius = 16
cell.backgroundConfiguration = background
}
return dataSource
}
func makeFeaturedAppsDataSource() -> RSTCompositeCollectionViewPrefetchingDataSource<StoreApp, UIImage>
{
let fetchRequest = StoreApp.fetchRequest() as NSFetchRequest<StoreApp>
fetchRequest.returnsObjectsAsFaults = false
fetchRequest.sortDescriptors = [
// Sort by Source first to group into sections.
NSSortDescriptor(keyPath: \StoreApp._source?.featuredSortID, ascending: true),
// Show uninstalled apps first.
// Sorting by StoreApp.installedApp crashes because InstalledApp does not respond to compare:
// Instead, sort by StoreApp.installedApp.storeApp.source.sourceIdentifier, which will be either nil OR source ID.
NSSortDescriptor(keyPath: \StoreApp.installedApp?.storeApp?.sourceIdentifier, ascending: true),
// Show featured apps first.
// Sorting by StoreApp.featuringSource crashes because Source does not respond to compare:
// Instead, sort by StoreApp.featuringSource.identifier, which will be either nil OR source ID.
NSSortDescriptor(keyPath: \StoreApp.featuringSource?.identifier, ascending: false),
// Randomize order within sections.
NSSortDescriptor(keyPath: \StoreApp.featuredSortID, ascending: true),
// Sanity check to ensure stable ordering
NSSortDescriptor(keyPath: \StoreApp.bundleIdentifier, ascending: true)
]
let sourceHasRemainingAppsPredicate = NSPredicate(format:
"""
SUBQUERY(%K, $app,
($app.%K != %@) AND ($app.%K == nil) AND (($app.%K == NO) OR ($app.%K == NO) OR ($app.%K == YES))
).@count > 0
""",
#keyPath(StoreApp._source._apps),
#keyPath(StoreApp.bundleIdentifier), StoreApp.altstoreAppID,
#keyPath(StoreApp.installedApp),
#keyPath(StoreApp.isPledgeRequired), #keyPath(StoreApp.isHiddenWithoutPledge), #keyPath(StoreApp.isPledged)
)
let primaryFetchRequest = fetchRequest.copy() as! NSFetchRequest<StoreApp>
primaryFetchRequest.predicate = sourceHasRemainingAppsPredicate
let primaryController = NSFetchedResultsController(fetchRequest: primaryFetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: #keyPath(StoreApp._source.featuredSortID), cacheName: nil)
let primaryDataSource = RSTFetchedResultsCollectionViewDataSource<StoreApp>(fetchedResultsController: primaryController)
primaryDataSource.liveFetchLimit = 5
let secondaryFetchRequest = fetchRequest.copy() as! NSFetchRequest<StoreApp>
secondaryFetchRequest.predicate = NSCompoundPredicate(notPredicateWithSubpredicate: sourceHasRemainingAppsPredicate)
let secondaryController = NSFetchedResultsController(fetchRequest: secondaryFetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: #keyPath(StoreApp._source.featuredSortID), cacheName: nil)
let secondaryDataSource = RSTFetchedResultsCollectionViewDataSource<StoreApp>(fetchedResultsController: secondaryController)
secondaryDataSource.liveFetchLimit = 5
// Ensure sources with no remaining apps always come last.
let dataSource = RSTCompositeCollectionViewPrefetchingDataSource<StoreApp, UIImage>(dataSources: [primaryDataSource, secondaryDataSource])
dataSource.cellIdentifierHandler = { _ in ReuseID.featuredApp.rawValue }
dataSource.cellConfigurationHandler = { cell, storeApp, indexPath in
let cell = cell as! AppCardCollectionViewCell
cell.configure(for: storeApp)
cell.prefersPagingScreenshots = false
cell.bannerView.button.addTarget(self, action: #selector(FeaturedViewController.performAppAction), for: .primaryActionTriggered)
cell.bannerView.sourceIconImageView.isHidden = true
cell.bannerView.iconImageView.image = nil
cell.bannerView.iconImageView.isIndicatingActivity = true
}
dataSource.prefetchHandler = { (storeApp, indexPath, completion) -> Foundation.Operation? in
return RSTAsyncBlockOperation { (operation) in
storeApp.managedObjectContext?.perform {
ImagePipeline.shared.loadImage(with: storeApp.iconURL, progress: nil) { result in
guard !operation.isCancelled else { return operation.finish() }
switch result
{
case .success(let response): completion(response.image, nil)
case .failure(let error): completion(nil, error)
}
}
}
}
}
dataSource.prefetchCompletionHandler = { [weak dataSource] (cell, image, indexPath, error) in
let cell = cell as! AppCardCollectionViewCell
cell.bannerView.iconImageView.image = image
cell.bannerView.iconImageView.isIndicatingActivity = false
if let error = error, let dataSource
{
let app = dataSource.item(at: indexPath)
Logger.main.debug("Failed to app icon from \(app.iconURL, privacy: .public). \(error.localizedDescription, privacy: .public)")
}
}
return dataSource
}
}
private extension FeaturedViewController
{
@IBSegueAction
func makeBrowseViewController(_ coder: NSCoder, sender: Any) -> UIViewController?
{
if let category = sender as? StoreCategory
{
let browseViewController = BrowseViewController(category: category, coder: coder)
return browseViewController
}
else if let source = sender as? Source
{
let browseViewController = BrowseViewController(source: source, coder: coder)
return browseViewController
}
else
{
let browseViewController = BrowseViewController(coder: coder)
return browseViewController
}
}
@IBSegueAction
func makeSourceDetailViewController(_ coder: NSCoder, sender: Any?) -> UIViewController?
{
guard let source = sender as? Source else { return nil }
let sourceDetailViewController = SourceDetailViewController(source: source, coder: coder)
return sourceDetailViewController
}
func showAllApps(for source: Source)
{
self.performSegue(withIdentifier: "showBrowseViewController", sender: source)
}
func showSourceDetails(for source: Source)
{
self.performSegue(withIdentifier: "showSourceDetails", sender: source)
}
}
private extension FeaturedViewController
{
@objc func performAppAction(_ sender: PillButton)
{
let point = self.collectionView.convert(sender.center, from: sender.superview)
guard let indexPath = self.collectionView.indexPathForItem(at: point) else { return }
let storeApp = self.dataSource.item(at: indexPath)
// if let installedApp = storeApp.installedApp, !installedApp.isUpdateAvailable
if let installedApp = storeApp.installedApp, !installedApp.hasUpdate
{
self.open(installedApp)
}
else
{
self.install(storeApp, at: indexPath)
}
}
@objc func install(_ storeApp: StoreApp, at indexPath: IndexPath)
{
let previousProgress = AppManager.shared.installationProgress(for: storeApp)
guard previousProgress == nil else {
previousProgress?.cancel()
return
}
// if let installedApp = storeApp.installedApp, installedApp.isUpdateAvailable
if let installedApp = storeApp.installedApp, installedApp.hasUpdate
{
AppManager.shared.update(installedApp, presentingViewController: self, completionHandler: finish(_:))
}
else
{
AppManager.shared.install(storeApp, presentingViewController: self, completionHandler: finish(_:))
}
UIView.performWithoutAnimation {
self.collectionView.reloadItems(at: [indexPath])
}
func finish(_ result: Result<InstalledApp, Error>)
{
DispatchQueue.main.async {
switch result
{
case .failure(OperationError.cancelled): break // Ignore
case .failure(let error):
let toastView = ToastView(error: error)
toastView.opensErrorLog = true
toastView.show(in: self)
case .success:
Logger.main.info("Installed app \(storeApp.bundleIdentifier, privacy: .public) from FeaturedViewController.")
}
for indexPath in self.collectionView.indexPathsForVisibleItems
{
// Only need to reload if it's still visible.
let item = self.dataSource.item(at: indexPath)
guard item == storeApp else { continue }
UIView.performWithoutAnimation {
self.collectionView.reloadItems(at: [indexPath])
}
}
}
}
}
func open(_ installedApp: InstalledApp)
{
UIApplication.shared.open(installedApp.openAppURL)
}
}
extension FeaturedViewController
{
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView
{
let section = Section(rawValue: indexPath.section)
switch kind
{
case ElementKind.sourceHeader.rawValue:
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: kind, for: indexPath) as! IconButtonCollectionReusableView
let indexPath = IndexPath(item: 0, section: indexPath.section)
let storeApp = self.dataSource.item(at: indexPath)
var content = UIListContentConfiguration.plainHeader()
content.text = storeApp.source?.name ?? NSLocalizedString("Unknown Source", comment: "")
content.textProperties.numberOfLines = 1
content.directionalLayoutMargins.leading = 0
content.imageToTextPadding = 8
content.imageProperties.reservedLayoutSize = CGSize(width: 26, height: 26)
content.imageProperties.maximumSize = CGSize(width: 26, height: 26)
content.imageProperties.cornerRadius = 13
UIView.performWithoutAnimation {
headerView.titleButton.setTitle(content.text, for: .normal)
headerView.titleButton.layoutIfNeeded()
}
headerView.iconButton.backgroundColor = storeApp.source?.effectiveTintColor?.adjustedForDisplay
headerView.iconButton.setImage(nil, for: .normal)
if let iconURL = storeApp.source?.effectiveIconURL
{
ImagePipeline.shared.loadImage(with: iconURL) { result in
guard case .success(let image) = result else { return }
headerView.iconButton.backgroundColor = .white
headerView.iconButton.setImage(image.image, for: .normal)
}
}
let buttons = [headerView.iconButton, headerView.titleButton]
for button in buttons
{
button.removeAction(identifiedBy: .showSourceDetails, for: .primaryActionTriggered)
if let source = storeApp.source
{
let action = UIAction(identifier: .showSourceDetails) { [weak self] _ in
self?.showSourceDetails(for: source)
}
button.addAction(action, for: .primaryActionTriggered)
}
}
return headerView
case ElementKind.sectionHeader.rawValue:
// Regular section header
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: kind, for: indexPath) as! UICollectionViewListCell
var content: UIListContentConfiguration = if #available(iOS 15, *) {
.prominentInsetGroupedHeader()
}
else {
.groupedHeader()
}
switch section
{
case .recentlyUpdated: content.text = NSLocalizedString("New & Updated", comment: "")
case .categories: content.text = NSLocalizedString("Categories", comment: "")
case .featuredHeader: content.text = NSLocalizedString("Featured", comment: "")
default: break
}
content.directionalLayoutMargins.leading = .zero
content.directionalLayoutMargins.trailing = .zero
headerView.contentConfiguration = content
return headerView
case ElementKind.button.rawValue where section.isFeaturedAppsSection:
let buttonView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: kind, for: indexPath) as! ButtonCollectionReusableView
let indexPath = IndexPath(item: 0, section: indexPath.section)
let storeApp = self.dataSource.item(at: indexPath)
buttonView.tintColor = storeApp.source?.effectiveTintColor?.adjustedForDisplay ?? .altPrimary
buttonView.button.setTitle(NSLocalizedString("See All", comment: ""), for: .normal)
buttonView.button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
buttonView.button.contentEdgeInsets.bottom = 8
buttonView.button.removeAction(identifiedBy: .showAllApps, for: .primaryActionTriggered)
if let source = storeApp.source
{
let action = UIAction(identifier: .showAllApps) { [weak self] _ in
self?.showAllApps(for: source)
}
buttonView.button.addAction(action, for: .primaryActionTriggered)
}
return buttonView
default: return UICollectionReusableView(frame: .zero)
}
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
{
let storeApp = self.dataSource.item(at: indexPath)
let section = Section(rawValue: indexPath.section)
switch section
{
case _ where section.isFeaturedAppsSection: fallthrough
case .recentlyUpdated:
let appViewController = AppViewController.makeAppViewController(app: storeApp)
self.navigationController?.pushViewController(appViewController, animated: true)
case .categories:
let category = storeApp.category ?? .other
self.performSegue(withIdentifier: "showBrowseViewController", sender: category)
default: break
}
}
}
@available(iOS 17, *)
#Preview(traits: .portrait) {
DatabaseManager.shared.startForPreview()
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let featuredViewController = storyboard.instantiateViewController(identifier: "featuredViewController")
let navigationController = UINavigationController(rootViewController: featuredViewController)
navigationController.navigationBar.prefersLargeTitles = true
navigationController.modalPresentationStyle = .fullScreen
let viewController = UIViewController()
AppManager.shared.fetchSources() { (result) in
do
{
let (_, context) = try result.get()
try context.save()
}
catch let error as NSError
{
Logger.main.error("Failed to fetch sources for preview. \(error.localizedDescription, privacy: .public)")
}
}
AppManager.shared.updateKnownSources { result in
Task {
do
{
let knownSources = try result.get()
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
await withThrowingTaskGroup(of: Void.self) { taskGroup in
for source in knownSources.0
{
guard let sourceURL = source.sourceURL else { continue }
taskGroup.addTask {
_ = try await AppManager.shared.fetchSource(sourceURL: sourceURL, managedObjectContext: context)
}
}
}
await context.performAsync {
try! context.save()
}
await MainActor.run {
viewController.present(navigationController, animated: true)
}
}
catch
{
Logger.main.error("Failed to fetch known sources for preview. \(error.localizedDescription, privacy: .public)")
}
}
}
return viewController
}

View File

@@ -1,52 +0,0 @@
//
// AppBannerCollectionViewCell.swift
// AltStore
//
// Created by Riley Testut on 3/23/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import UIKit
class AppBannerCollectionViewCell: UICollectionViewListCell
{
let bannerView = AppBannerView(frame: .zero)
override init(frame: CGRect)
{
super.init(frame: frame)
self.initialize()
}
required init?(coder: NSCoder)
{
super.init(coder: coder)
self.initialize()
}
private func initialize()
{
// Prevent content "squishing" when scrolling offscreen.
self.insetsLayoutMarginsFromSafeArea = false
self.contentView.insetsLayoutMarginsFromSafeArea = false
self.bannerView.insetsLayoutMarginsFromSafeArea = false
self.backgroundView = UIView() // Clear background
self.selectedBackgroundView = UIView() // Disable selection highlighting.
self.contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.contentView.preservesSuperviewLayoutMargins = true
self.bannerView.translatesAutoresizingMaskIntoConstraints = false
self.contentView.addSubview(self.bannerView)
NSLayoutConstraint.activate([
self.bannerView.topAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.topAnchor),
self.bannerView.bottomAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.bottomAnchor),
self.bannerView.leadingAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.leadingAnchor),
self.bannerView.trailingAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.trailingAnchor),
])
}
}

View File

@@ -1,428 +0,0 @@
//
// AppBannerView.swift
// AltStore
//
// Created by Riley Testut on 8/29/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
import AltStoreCore
import Roxas
import Nuke
extension AppBannerView
{
static let standardHeight = 88.0
enum Style
{
case app
case source
}
enum AppAction
{
case install
case open
case update
case custom(String)
}
}
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 }
}
var style: Style = .app
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 sourceIconImageView: AppIconImageView!
@IBOutlet var backgroundEffectView: UIVisualEffectView!
@IBOutlet private var vibrancyView: UIVisualEffectView!
@IBOutlet private var stackView: UIStackView!
@IBOutlet private var accessibilityView: UIView!
@IBOutlet private var iconImageViewHeightConstraint: NSLayoutConstraint!
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
self.sourceIconImageView.style = .circular
self.sourceIconImageView.isHidden = true
self.layoutMargins = self.stackView.layoutMargins
self.insetsLayoutMarginsFromSafeArea = false
self.stackView.isLayoutMarginsRelativeArrangement = true
self.stackView.preservesSuperviewLayoutMargins = true
}
override func tintColorDidChange()
{
super.tintColorDidChange()
if self.tintAdjustmentMode != .dimmed
{
self.originalTintColor = self.tintColor
}
self.update()
}
}
extension AppBannerView
{
func configure(for app: AppProtocol, action: AppAction? = nil, showSourceIcon: Bool = true)
{
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 let track = storeApp.latestSupportedVersion?.channel,
ReleaseTracks.betaTracks.contains(track)
{
self.name = String(format: NSLocalizedString("%@ beta", comment: ""), app.name)
self.isBeta = true
}
}
}
self.style = .app
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
}
if let storeApp = app.storeApp, storeApp.isPledgeRequired
{
// Always show button label for Patreon apps.
self.buttonLabel.isHidden = false
if storeApp.isPledged
{
self.buttonLabel.text = NSLocalizedString("Pledged", comment: "")
}
else if storeApp.installedApp != nil
{
self.buttonLabel.text = NSLocalizedString("Pledge Expired", comment: "")
}
else
{
self.buttonLabel.text = NSLocalizedString("Join Patreon", comment: "")
}
}
else
{
self.buttonLabel.isHidden = true
}
if let source = app.storeApp?.source, showSourceIcon
{
self.sourceIconImageView.isHidden = false
self.sourceIconImageView.backgroundColor = source.effectiveTintColor?.adjustedForDisplay ?? .altPrimary
if let iconURL = source.effectiveIconURL
{
if let image = ImageCache.shared[iconURL]
{
self.sourceIconImageView.backgroundColor = .white
self.sourceIconImageView.image = image.image
}
else
{
self.sourceIconImageView.image = nil
Nuke.loadImage(with: iconURL, into: self.sourceIconImageView) { result in
switch result
{
case .failure(let error): Logger.main.error("Failed to fetch source icon from \(iconURL, privacy: .public). \(error.localizedDescription, privacy: .public)")
case .success: self.sourceIconImageView.backgroundColor = .white // In case icon has transparent background.
}
}
}
}
}
else
{
self.sourceIconImageView.isHidden = true
}
let buttonAction: AppAction
if let action
{
buttonAction = action
}
else if let storeApp = app.storeApp
{
if let installedApp = storeApp.installedApp
{
// App is installed
// if installedApp.isUpdateAvailable
if installedApp.hasUpdate
{
buttonAction = .update
}
else
{
buttonAction = .open
}
}
else
{
// App is not installed
buttonAction = .install
}
}
else
{
// App is not from a source, fall back to .open
buttonAction = .open
}
UIView.performWithoutAnimation {
switch buttonAction
{
case .open:
let buttonTitle = NSLocalizedString("Open", comment: "")
self.button.setTitle(buttonTitle.uppercased(), for: .normal)
self.button.accessibilityLabel = String(format: NSLocalizedString("Open %@", comment: ""), values.name)
self.button.accessibilityValue = buttonTitle
self.button.countdownDate = nil
case .update:
let buttonTitle = NSLocalizedString("Update", comment: "")
self.button.setTitle(buttonTitle.uppercased(), for: .normal)
self.button.accessibilityLabel = String(format: NSLocalizedString("Update %@", comment: ""), values.name)
self.button.accessibilityValue = buttonTitle
self.button.countdownDate = nil
case .custom(let buttonTitle):
self.button.setTitle(buttonTitle, for: .normal)
self.button.accessibilityLabel = buttonTitle
self.button.accessibilityValue = buttonTitle
self.button.countdownDate = nil
case .install:
if let storeApp = app.storeApp, storeApp.isPledgeRequired
{
// Pledge required
if storeApp.isPledged
{
let buttonTitle = NSLocalizedString("Install", comment: "")
self.button.setTitle(buttonTitle.uppercased(), for: .normal)
self.button.accessibilityLabel = String(format: NSLocalizedString("Install %@", comment: ""), app.name)
self.button.accessibilityValue = buttonTitle
}
else if let amount = storeApp.pledgeAmount, let currencyCode = storeApp.pledgeCurrency, !storeApp.prefersCustomPledge, #available(iOS 15, *)
{
let price = amount.formatted(.currency(code: currencyCode).presentation(.narrow).precision(.fractionLength(0...2)))
let buttonTitle = String(format: NSLocalizedString("%@/mo", comment: ""), price)
self.button.setTitle(buttonTitle, for: .normal)
self.button.accessibilityLabel = String(format: NSLocalizedString("Pledge %@ a month", comment: ""), price)
self.button.accessibilityValue = String(format: NSLocalizedString("%@ a month", comment: ""), price)
}
else
{
let buttonTitle = NSLocalizedString("Pledge", comment: "")
self.button.setTitle(buttonTitle.uppercased(), for: .normal)
self.button.accessibilityLabel = buttonTitle
self.button.accessibilityValue = buttonTitle
}
}
else
{
// Free app
let buttonTitle = NSLocalizedString("Free", comment: "")
self.button.setTitle(buttonTitle.uppercased(), for: .normal)
self.button.accessibilityLabel = String(format: NSLocalizedString("Download %@", comment: ""), app.name)
self.button.accessibilityValue = buttonTitle
}
if let versionDate = app.storeApp?.latestSupportedVersion?.date, versionDate > Date()
{
self.button.countdownDate = versionDate
}
else
{
self.button.countdownDate = nil
}
}
// Ensure PillButton is correct size before assigning progress.
self.layoutIfNeeded()
}
if let progress = AppManager.shared.installationProgress(for: app), progress.fractionCompleted < 1.0
{
self.button.progress = progress
}
else
{
self.button.progress = nil
}
}
func configure(for source: Source)
{
self.style = .source
let subtitle: String
if let text = source.subtitle
{
subtitle = text
}
else if let scheme = source.sourceURL.scheme
{
subtitle = source.sourceURL.absoluteString.replacingOccurrences(of: scheme + "://", with: "")
}
else
{
subtitle = source.sourceURL.absoluteString
}
self.titleLabel.text = source.name
self.subtitleLabel.text = subtitle
let tintColor = source.effectiveTintColor ?? .altPrimary
self.tintColor = tintColor
let accessibilityLabel = source.name + "\n" + subtitle
self.accessibilityLabel = accessibilityLabel
}
}
private extension AppBannerView
{
func update()
{
self.clipsToBounds = true
self.layer.cornerRadius = 22
let tintColor = self.originalTintColor ?? self.tintColor
self.subtitleLabel.textColor = tintColor
switch self.style
{
case .app:
self.directionalLayoutMargins.trailing = self.stackView.directionalLayoutMargins.trailing
self.iconImageViewHeightConstraint.constant = 60
self.iconImageView.style = .icon
self.titleLabel.textColor = .label
self.button.style = .pill
self.backgroundEffectView.contentView.backgroundColor = UIColor(resource: .blurTint)
self.backgroundEffectView.backgroundColor = tintColor
case .source:
self.directionalLayoutMargins.trailing = 20
self.iconImageViewHeightConstraint.constant = 44
self.iconImageView.style = .circular
self.titleLabel.textColor = .white
self.button.style = .custom
self.backgroundEffectView.contentView.backgroundColor = tintColor?.adjustedForDisplay
self.backgroundEffectView.backgroundColor = nil
if let tintColor, tintColor.isTooBright
{
let textVibrancyEffect = UIVibrancyEffect(blurEffect: .init(style: .systemChromeMaterialLight), style: .fill)
self.vibrancyView.effect = textVibrancyEffect
}
else
{
// Thinner == more dull
let textVibrancyEffect = UIVibrancyEffect(blurEffect: .init(style: .systemThinMaterialDark), style: .secondaryLabel)
self.vibrancyView.effect = textVibrancyEffect
}
}
}
}

View File

@@ -1,388 +0,0 @@
//
// AppCardCollectionViewCell.swift
// AltStore
//
// Created by Riley Testut on 10/13/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import UIKit
import AltStoreCore
import Roxas
import Nuke
private let minimumItemSpacing = 8.0
class AppCardCollectionViewCell: UICollectionViewCell
{
let bannerView: AppBannerView
let captionLabel: UILabel
var prefersPagingScreenshots = true
private let screenshotsCollectionView: UICollectionView
private let stackView: UIStackView
private let topAreaPanGestureRecognizer: UIPanGestureRecognizer
private lazy var dataSource = self.makeDataSource()
private var screenshots: [AppScreenshot] = [] {
didSet {
self.dataSource.items = self.screenshots
if self.screenshots.isEmpty
{
// No screenshots, so hide collection view.
self.collectionViewAspectRatioConstraint.isActive = false
self.stackView.layoutMargins.bottom = 0
}
else
{
// At least one screenshot, so show collection view.
self.collectionViewAspectRatioConstraint.isActive = true
self.stackView.layoutMargins.bottom = self.screenshotsCollectionView.directionalLayoutMargins.leading
}
}
}
private let collectionViewAspectRatioConstraint: NSLayoutConstraint
override init(frame: CGRect)
{
self.bannerView = AppBannerView(frame: .zero)
self.bannerView.layoutMargins.bottom = 0
let vibrancyEffect = UIVibrancyEffect(blurEffect: UIBlurEffect(style: .systemChromeMaterial), style: .secondaryLabel)
let captionVibrancyView = UIVisualEffectView(effect: vibrancyEffect)
self.captionLabel = UILabel(frame: .zero)
self.captionLabel.font = UIFont(descriptor: UIFontDescriptor.preferredFontDescriptor(withTextStyle: .footnote).bolded(), size: 0)
self.captionLabel.textAlignment = .center
self.captionLabel.numberOfLines = 2
self.captionLabel.minimumScaleFactor = 0.8
captionVibrancyView.contentView.addSubview(self.captionLabel, pinningEdgesWith: .zero)
self.screenshotsCollectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
self.screenshotsCollectionView.backgroundColor = nil
self.screenshotsCollectionView.alwaysBounceVertical = false
self.screenshotsCollectionView.alwaysBounceHorizontal = true
self.screenshotsCollectionView.showsHorizontalScrollIndicator = false
self.screenshotsCollectionView.showsVerticalScrollIndicator = false
self.stackView = UIStackView(arrangedSubviews: [self.bannerView, captionVibrancyView, self.screenshotsCollectionView])
self.stackView.translatesAutoresizingMaskIntoConstraints = false
self.stackView.spacing = 12
self.stackView.axis = .vertical
self.stackView.alignment = .fill
self.stackView.distribution = .equalSpacing
// Aspect ratio constraint to fit exactly 3 modern portrait iPhone screenshots side-by-side (with spacing).
let inset = self.bannerView.layoutMargins.left
let multiplier = (AppScreenshot.defaultAspectRatio.width * 3) / AppScreenshot.defaultAspectRatio.height
let spacing = (inset * 2) + (minimumItemSpacing * 2)
self.collectionViewAspectRatioConstraint = self.screenshotsCollectionView.widthAnchor.constraint(equalTo: self.screenshotsCollectionView.heightAnchor, multiplier: multiplier, constant: spacing)
// Allows us to ignore swipes in top portion of screenshotsCollectionView.
self.topAreaPanGestureRecognizer = UIPanGestureRecognizer(target: nil, action: nil)
self.topAreaPanGestureRecognizer.cancelsTouchesInView = false
self.topAreaPanGestureRecognizer.delaysTouchesBegan = false
self.topAreaPanGestureRecognizer.delaysTouchesEnded = false
super.init(frame: frame)
self.contentView.clipsToBounds = true
self.contentView.layer.cornerCurve = .continuous
self.contentView.addSubview(self.bannerView.backgroundEffectView, pinningEdgesWith: .zero)
self.contentView.addSubview(self.stackView, pinningEdgesWith: .zero)
self.screenshotsCollectionView.collectionViewLayout = self.makeLayout()
self.screenshotsCollectionView.dataSource = self.dataSource
self.screenshotsCollectionView.prefetchDataSource = self.dataSource
// Adding screenshotsCollectionView's gesture recognizers to self.contentView breaks paging,
// so instead we intercept taps and pass them onto delegate.
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(AppCardCollectionViewCell.handleTapGesture(_:)))
tapGestureRecognizer.cancelsTouchesInView = false
tapGestureRecognizer.delaysTouchesBegan = false
tapGestureRecognizer.delaysTouchesEnded = false
self.screenshotsCollectionView.addGestureRecognizer(tapGestureRecognizer)
self.topAreaPanGestureRecognizer.delegate = self
self.screenshotsCollectionView.panGestureRecognizer.require(toFail: self.topAreaPanGestureRecognizer)
self.screenshotsCollectionView.addGestureRecognizer(self.topAreaPanGestureRecognizer)
self.screenshotsCollectionView.register(AppScreenshotCollectionViewCell.self, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
self.stackView.isLayoutMarginsRelativeArrangement = true
self.stackView.layoutMargins.bottom = inset
self.contentView.preservesSuperviewLayoutMargins = true
self.screenshotsCollectionView.directionalLayoutMargins = NSDirectionalEdgeInsets(top: 0, leading: inset, bottom: 0, trailing: inset)
NSLayoutConstraint.activate([
self.bannerView.heightAnchor.constraint(equalToConstant: AppBannerView.standardHeight - inset)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews()
{
super.layoutSubviews()
self.contentView.layer.cornerRadius = self.bannerView.layer.cornerRadius
}
}
private extension AppCardCollectionViewCell
{
func makeLayout() -> UICollectionViewCompositionalLayout
{
let layoutConfig = UICollectionViewCompositionalLayoutConfiguration()
layoutConfig.contentInsetsReference = .layoutMargins
let layout = UICollectionViewCompositionalLayout(sectionProvider: { [weak self] (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
guard let self else { return nil }
var contentWidth = 0.0
var numberOfVisibleScreenshots = 0
for screenshot in self.screenshots
{
var aspectRatio = screenshot.aspectRatio
if aspectRatio.width > aspectRatio.height
{
switch screenshot.deviceType
{
case .iphone:
// Always rotate landscape iPhone screenshots
aspectRatio = CGSize(width: aspectRatio.height, height: aspectRatio.width)
case .ipad:
// Never rotate iPad screenshots
break
default: break
}
}
let screenshotWidth = (layoutEnvironment.container.effectiveContentSize.height * (aspectRatio.width / aspectRatio.height)).rounded(.up) // Round to ensure we over-estimate contentWidth.
let totalContentWidth = contentWidth + (screenshotWidth + minimumItemSpacing)
if totalContentWidth > layoutEnvironment.container.effectiveContentSize.width
{
// totalContentWidth is larger than visible width.
break
}
contentWidth = totalContentWidth
numberOfVisibleScreenshots += 1
}
// Use .estimated(1) to ensure we don't over-estimate widths, which can cause incorrect layouts for the last group.
let itemSize = NSCollectionLayoutSize(widthDimension: .estimated(1), heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
if numberOfVisibleScreenshots == 1
{
// If there's only one screenshot visible initially, we'll (reluctantly) opt-in to flexible spacing on both sides.
// This ensures the items are always centered, but may result in larger spacings between items than we'd prefer.
item.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: .flexible(0), top: nil, trailing: .flexible(0), bottom: nil)
}
else
{
// Otherwise, only have flexible spacing on the leading edge, which will be balanced by trailingGroup's flexible trailing spacing.
item.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: .flexible(0), top: nil, trailing: nil, bottom: nil)
}
let groupItem = NSCollectionLayoutItem(layoutSize: itemSize)
let trailingGroup = NSCollectionLayoutGroup.horizontal(layoutSize: itemSize, subitems: [groupItem])
trailingGroup.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: nil, top: nil, trailing: .flexible(0), bottom: nil)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item, trailingGroup])
group.interItemSpacing = .fixed(minimumItemSpacing)
if numberOfVisibleScreenshots < self.screenshots.count
{
// There are more screenshots than what is displayed, so no need to manually center them.
}
else
{
// We're showing all screenshots initially, so make sure they're centered.
let insetWidth = (layoutEnvironment.container.effectiveContentSize.width - contentWidth) / 2.0
group.contentInsets.leading = (insetWidth - 1).rounded(.down) // Subtract 1 to avoid overflowing/clipping
}
let layoutSection = NSCollectionLayoutSection(group: group)
layoutSection.orthogonalScrollingBehavior = .groupPagingCentered
layoutSection.interGroupSpacing = self.screenshotsCollectionView.directionalLayoutMargins.leading + self.screenshotsCollectionView.directionalLayoutMargins.trailing
return layoutSection
}, configuration: layoutConfig)
return layout
}
func makeDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<AppScreenshot, UIImage>
{
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<AppScreenshot, UIImage>(items: [])
dataSource.cellConfigurationHandler = { (cell, screenshot, indexPath) in
let cell = cell as! AppScreenshotCollectionViewCell
cell.imageView.image = nil
cell.imageView.isIndicatingActivity = true
var aspectRatio = screenshot.aspectRatio
if aspectRatio.width > aspectRatio.height
{
switch screenshot.deviceType
{
case .iphone:
// Always rotate landscape iPhone screenshots
aspectRatio = CGSize(width: aspectRatio.height, height: aspectRatio.width)
case .ipad:
// Never rotate iPad screenshots
break
default: break
}
}
cell.aspectRatio = aspectRatio
}
dataSource.prefetchHandler = { (screenshot, indexPath, completionHandler) in
let imageURL = screenshot.imageURL
return RSTAsyncBlockOperation() { (operation) in
let request = ImageRequest(url: imageURL)
ImagePipeline.shared.loadImage(with: request, progress: nil) { result in
guard !operation.isCancelled else { return operation.finish() }
switch result
{
case .success(let response): completionHandler(response.image, nil)
case .failure(let error): completionHandler(nil, error)
}
}
}
}
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
let cell = cell as! AppScreenshotCollectionViewCell
cell.imageView.isIndicatingActivity = false
cell.setImage(image)
if let error = error
{
print("Error loading image:", error)
}
}
return dataSource
}
@objc func handleTapGesture(_ tapGesture: UITapGestureRecognizer)
{
var superview: UIView? = self.superview
var collectionView: UICollectionView? = nil
while case let view? = superview
{
if let cv = view as? UICollectionView
{
collectionView = cv
break
}
superview = view.superview
}
if let collectionView, let indexPath = collectionView.indexPath(for: self)
{
collectionView.delegate?.collectionView?(collectionView, didSelectItemAt: indexPath)
}
}
}
extension AppCardCollectionViewCell
{
func configure(for storeApp: StoreApp, showSourceIcon: Bool = true)
{
self.screenshots = storeApp.preferredScreenshots()
// Explicitly set to false to ensure we're starting from a non-activity indicating state.
// Otherwise, cell reuse can mess up some cached values.
self.bannerView.button.isIndicatingActivity = false
self.bannerView.tintColor = storeApp.tintColor
self.bannerView.configure(for: storeApp, showSourceIcon: showSourceIcon)
self.bannerView.subtitleLabel.numberOfLines = 1
self.bannerView.subtitleLabel.lineBreakMode = .byTruncatingTail
self.bannerView.subtitleLabel.minimumScaleFactor = 0.8
self.bannerView.subtitleLabel.text = storeApp.developerName
if let subtitle = storeApp.subtitle, !subtitle.isEmpty
{
self.captionLabel.text = subtitle
self.captionLabel.isHidden = false
}
else
{
self.captionLabel.isHidden = true
}
}
}
extension AppCardCollectionViewCell: UIGestureRecognizerDelegate
{
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool
{
// Never recognize topAreaPanGestureRecognizer unless prefersPagingScreenshots is false.
guard !self.prefersPagingScreenshots else { return false }
let point = gestureRecognizer.location(in: self.screenshotsCollectionView)
// Top area = Top 3/4
let isTopArea = point.y < (self.screenshotsCollectionView.bounds.height / 4) * 3
return isTopArea
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool
{
guard let panGestureRecognizer = otherGestureRecognizer as? UIPanGestureRecognizer, let view = panGestureRecognizer.view else { return false }
if view.isDescendant(of: self.screenshotsCollectionView)
{
// Only allow nested gesture recognizers if topAreaPanGestureRecognizer fails.
return true
}
else
{
// Always allow parent gesture recognizers.
return false
}
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool
{
guard let panGestureRecognizer = otherGestureRecognizer as? UIPanGestureRecognizer, let view = panGestureRecognizer.view else { return true }
if view.isDescendant(of: self.screenshotsCollectionView)
{
// Don't recognize topAreaPanGestureRecognizer alongside nested gesture recognizers.
return false
}
else
{
// Allow recognizing simultaneously with parent gesture recognizers.
// This fixes accidentally breaking scrolling in parent.
return true
}
}
}

View File

@@ -1,69 +0,0 @@
//
// AppIconImageView.swift
// AltStore
//
// Created by Riley Testut on 5/9/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
extension AppIconImageView
{
enum Style
{
case icon
case circular
}
}
class AppIconImageView: UIImageView
{
var style: Style = .icon {
didSet {
self.setNeedsLayout()
}
}
init(style: Style)
{
self.style = style
super.init(image: nil)
self.initialize()
}
required init?(coder: NSCoder)
{
super.init(coder: coder)
self.initialize()
}
private func initialize()
{
self.contentMode = .scaleAspectFill
self.clipsToBounds = true
self.backgroundColor = .white
self.layer.cornerCurve = .continuous
}
override func layoutSubviews()
{
super.layoutSubviews()
switch self.style
{
case .icon:
// Based off of 60pt icon having 12pt radius.
let radius = self.bounds.height / 5
self.layer.cornerRadius = radius
case .circular:
let radius = self.bounds.height / 2
self.layer.cornerRadius = radius
}
}
}

View File

@@ -1,649 +0,0 @@
//
// HeaderContentViewController.swift
// AltStore
//
// Created by Riley Testut on 3/10/23.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
import AltStoreCore
import Roxas
import Nuke
protocol ScrollableContentViewController: UIViewController
{
var scrollView: UIScrollView { get }
}
class HeaderContentViewController<Header: UIView, Content: ScrollableContentViewController> : UIViewController,
UIAdaptivePresentationControllerDelegate,
UIScrollViewDelegate,
UIGestureRecognizerDelegate
{
var tintColor: UIColor? {
didSet {
guard self.isViewLoaded else { return }
self.view.tintColor = self.tintColor?.adjustedForDisplay
self.update()
}
}
private(set) var headerView: Header!
private(set) var contentViewController: Content!
private(set) var backButton: VibrantButton!
private(set) var backgroundImageView: UIImageView!
private(set) var navigationBarNameLabel: UILabel!
private(set) var navigationBarIconView: UIImageView!
private(set) var navigationBarTitleView: UIStackView!
private(set) var navigationBarButton: PillButton!
private var scrollView: UIScrollView!
private var headerScrollView: UIScrollView!
private var headerContainerView: UIView!
private var backgroundBlurView: UIVisualEffectView!
private var contentViewControllerShadowView: UIView!
private var ignoreBackGestureRecognizer: UIPanGestureRecognizer!
private var blurAnimator: UIViewPropertyAnimator?
private var navigationBarAnimator: UIViewPropertyAnimator?
private var contentSizeObservation: NSKeyValueObservation?
private var _shouldResetLayout = false
private var _backgroundBlurEffect: UIBlurEffect?
private var _backgroundBlurTintColor: UIColor?
private var isViewingHeader: Bool {
let isViewingHeader = (self.headerScrollView.contentOffset.x != self.headerScrollView.contentInset.left)
return isViewingHeader
}
override var preferredStatusBarStyle: UIStatusBarStyle {
if #available(iOS 17, *)
{
// On iOS 17+, .default will update the status bar automatically.
return .default
}
else
{
return _preferredStatusBarStyle
}
}
private var _preferredStatusBarStyle: UIStatusBarStyle = .default
init()
{
super.init(nibName: nil, bundle: nil)
}
deinit
{
self.blurAnimator?.stopAnimation(true)
self.navigationBarAnimator?.stopAnimation(true)
}
required init?(coder: NSCoder)
{
super.init(coder: coder)
}
func makeContentViewController() -> Content
{
fatalError()
}
func makeHeaderView() -> Header
{
fatalError()
}
override func viewDidLoad()
{
super.viewDidLoad()
self.view.backgroundColor = .white
self.view.clipsToBounds = true
self.navigationItem.largeTitleDisplayMode = .never
self.navigationController?.presentationController?.delegate = self
// Background
self.backgroundImageView = UIImageView(frame: .zero)
self.backgroundImageView.contentMode = .scaleAspectFill
self.view.addSubview(self.backgroundImageView)
let blurEffect = UIBlurEffect(style: .regular)
self.backgroundBlurView = UIVisualEffectView(effect: blurEffect)
self.view.addSubview(self.backgroundBlurView, pinningEdgesWith: .zero)
// Header View
self.headerContainerView = UIView(frame: .zero)
self.view.addSubview(self.headerContainerView, pinningEdgesWith: .zero)
self.ignoreBackGestureRecognizer = UIPanGestureRecognizer(target: self, action: nil)
self.ignoreBackGestureRecognizer.delegate = self
self.headerContainerView.addGestureRecognizer(self.ignoreBackGestureRecognizer)
self.navigationController?.interactivePopGestureRecognizer?.require(toFail: self.ignoreBackGestureRecognizer) // So we can disable back gesture when viewing header.
self.headerScrollView = UIScrollView(frame: .zero)
self.headerScrollView.delegate = self
self.headerScrollView.isPagingEnabled = true
self.headerScrollView.clipsToBounds = false
self.headerScrollView.indicatorStyle = .white
self.headerScrollView.showsVerticalScrollIndicator = false
self.headerContainerView.addSubview(self.headerScrollView)
self.headerContainerView.addGestureRecognizer(self.headerScrollView.panGestureRecognizer) // Allow panning outside headerScrollView bounds.
self.headerView = self.makeHeaderView()
self.headerScrollView.addSubview(self.headerView)
let imageConfiguration = UIImage.SymbolConfiguration(weight: .semibold)
let image = UIImage(systemName: "chevron.backward", withConfiguration: imageConfiguration)
self.backButton = VibrantButton(type: .system)
self.backButton.image = image
self.backButton.tintColor = self.tintColor
self.backButton.sizeToFit()
self.backButton.addTarget(self.navigationController, action: #selector(UINavigationController.popViewController(animated:)), for: .primaryActionTriggered)
self.view.addSubview(self.backButton)
// Content View Controller
self.contentViewController = self.makeContentViewController()
self.contentViewController.view.frame = self.view.bounds
self.contentViewController.view.layer.cornerRadius = 38
self.contentViewController.view.layer.masksToBounds = true
self.addChild(self.contentViewController)
self.view.addSubview(self.contentViewController.view)
self.contentViewController.didMove(toParent: self)
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.view.insertSubview(self.contentViewControllerShadowView, belowSubview: self.contentViewController.view)
// Add scrollView to front so the scroll indicators are visible, but disable user interaction.
self.scrollView = UIScrollView(frame: CGRect(origin: .zero, size: self.view.bounds.size))
self.scrollView.delegate = self
self.scrollView.isUserInteractionEnabled = false
self.scrollView.contentInsetAdjustmentBehavior = .never
self.view.addSubview(self.scrollView, pinningEdgesWith: .zero)
self.view.addGestureRecognizer(self.scrollView.panGestureRecognizer)
self.contentViewController.scrollView.panGestureRecognizer.require(toFail: self.scrollView.panGestureRecognizer)
self.contentViewController.scrollView.showsVerticalScrollIndicator = false
self.contentViewController.scrollView.contentInsetAdjustmentBehavior = .never
// Navigation Bar Title View
self.navigationBarNameLabel = UILabel(frame: .zero)
self.navigationBarNameLabel.font = UIFont.boldSystemFont(ofSize: 17) // We want semibold, which this (apparently) returns.
self.navigationBarNameLabel.text = self.title
self.navigationBarNameLabel.sizeToFit()
self.navigationBarIconView = UIImageView(frame: .zero)
self.navigationBarIconView.clipsToBounds = true
self.navigationBarTitleView = UIStackView(arrangedSubviews: [self.navigationBarIconView, self.navigationBarNameLabel])
self.navigationBarTitleView.axis = .horizontal
self.navigationBarTitleView.spacing = 8
self.navigationBarButton = PillButton(type: .system)
self.navigationBarButton.setContentCompressionResistancePriority(UILayoutPriority(rawValue: 9000), for: .horizontal) // Prioritize over title length.
// Embed navigationBarButton in container view with Auto Layout to ensure it can automatically update its size.
let buttonContainerView = UIView()
buttonContainerView.addSubview(self.navigationBarButton, pinningEdgesWith: .zero)
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: buttonContainerView)
NSLayoutConstraint.activate([
self.navigationBarIconView.widthAnchor.constraint(equalToConstant: 35),
self.navigationBarIconView.heightAnchor.constraint(equalTo: self.navigationBarIconView.widthAnchor)
])
let size = self.navigationBarTitleView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
self.navigationBarTitleView.bounds.size = size
self.navigationItem.titleView = self.navigationBarTitleView
self._backgroundBlurEffect = self.backgroundBlurView.effect as? UIBlurEffect
self._backgroundBlurTintColor = self.backgroundBlurView.contentView.backgroundColor
self.contentSizeObservation = self.contentViewController.scrollView.observe(\.contentSize, options: [.new, .old]) { [weak self] (scrollView, change) in
guard let size = change.newValue, let previousSize = change.oldValue, size != previousSize else { return }
self?.view.setNeedsLayout()
self?.view.layoutIfNeeded()
}
// Don't call update() before subclasses have finished viewDidLoad().
// self.update()
NotificationCenter.default.addObserver(self, selector: #selector(HeaderContentViewController.willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(HeaderContentViewController.didBecomeActive(_:)), name: UIApplication.didBecomeActiveNotification, object: nil)
if #available(iOS 15, *)
{
// Fix navigation bar + tab bar appearance on iOS 15.
self.setContentScrollView(self.scrollView)
}
// Start with navigation bar hidden.
self.hideNavigationBar()
self.view.tintColor = self.tintColor?.adjustedForDisplay
}
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
self.prepareBlur()
// Update blur immediately.
self.view.setNeedsLayout()
self.view.layoutIfNeeded()
self.headerScrollView.flashScrollIndicators()
self.update()
}
override func viewIsAppearing(_ animated: Bool)
{
super.viewIsAppearing(animated)
// Ensure header view has correct layout dimensions.
self.headerView.setNeedsLayout()
}
override func viewDidAppear(_ animated: Bool)
{
super.viewDidAppear(animated)
self._shouldResetLayout = true
self.view.setNeedsLayout()
self.view.layoutIfNeeded()
}
override func viewDidLayoutSubviews()
{
super.viewDidLayoutSubviews()
if self._shouldResetLayout
{
// Various events can cause UI to mess up, so reset affected components now.
self.prepareBlur()
// Reset navigation bar animation, and create a new one later in this method if necessary.
self.resetNavigationBarAnimation()
self._shouldResetLayout = false
}
let statusBarHeight: Double
if let navigationController, navigationController.presentingViewController != nil, navigationController.modalPresentationStyle != .fullScreen
{
statusBarHeight = 20
}
else if let statusBarManager = (self.view.window ?? self.presentedViewController?.view.window)?.windowScene?.statusBarManager
{
statusBarHeight = statusBarManager.statusBarFrame.height
}
else
{
statusBarHeight = 0
}
let cornerRadius = self.contentViewControllerShadowView.layer.cornerRadius
let inset = 15 as CGFloat
let padding = 20 as CGFloat
let backButtonSize = self.backButton.sizeThatFits(CGSize(width: Double.infinity, height: .infinity))
let largestBackButtonDimension = max(backButtonSize.width, backButtonSize.height) // Enforce 1:1 aspect ratio.
var backButtonFrame = CGRect(x: inset, y: statusBarHeight, width: largestBackButtonDimension, height: largestBackButtonDimension)
var headerFrame = CGRect(x: inset, y: 0, width: self.view.bounds.width - inset * 2, height: self.headerView.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 backButtonPadding = 8.0
let minimumHeaderY = backButtonFrame.maxY + backButtonPadding
let minimumContentHeight = minimumHeaderY + headerFrame.height + padding // Minimum height for header + back button + spacing.
let maximumContentY = max(self.view.bounds.width * 0.667, minimumContentHeight) // Initial Y-value of content view.
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
// Update blur.
self.updateBlur()
// Animate navigation bar.
let showNavigationBarThreshold = (maximumContentY - minimumContentHeight) + backButtonFrame.origin.y
if self.scrollView.contentOffset.y > showNavigationBarThreshold
{
if self.navigationBarAnimator == nil
{
self.prepareNavigationBarAnimation()
}
let difference = self.scrollView.contentOffset.y - showNavigationBarThreshold
let range: Double
if self.presentingViewController == nil && self.parent?.presentingViewController == nil
{
// Not presented modally, so rely on safe area + navigation bar height.
range = (headerFrame.height + padding) - (self.navigationController?.navigationBar.bounds.height ?? self.view.safeAreaInsets.top)
}
else
{
// Presented modally, so rely on maximumContentY.
range = maximumContentY - (maximumContentY - padding - headerFrame.height) - inset
}
let fractionComplete = min(difference, range) / range
self.navigationBarAnimator?.fractionComplete = fractionComplete
}
else
{
self.navigationBarAnimator?.fractionComplete = 0.0
self.resetNavigationBarAnimation()
}
let beginMovingBackButtonThreshold = (maximumContentY - minimumContentHeight)
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.scrollView.contentOffset.y = -self.contentViewController.scrollView.contentInset.top + difference
}
else
{
// Keep content table view's content offset at the top.
self.contentViewController.scrollView.contentOffset.y = -self.contentViewController.scrollView.contentInset.top
}
// 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.frame = contentFrame
self.contentViewControllerShadowView.frame = contentFrame
self.backgroundImageView.frame = backgroundIconFrame
self.backButton.frame = backButtonFrame
self.backButton.layer.cornerRadius = backButtonFrame.height / 2
// Adjust header scroll view content size for paging
self.headerView.frame = CGRect(origin: .zero, size: headerFrame.size)
self.headerScrollView.frame = headerFrame
self.headerScrollView.contentSize = CGSize(width: headerFrame.width * 2, height: headerFrame.height)
self.scrollView.verticalScrollIndicatorInsets.top = statusBarHeight
self.headerScrollView.horizontalScrollIndicatorInsets.bottom = -12
// Adjust content offset + size.
let contentOffset = self.scrollView.contentOffset
var contentSize = self.contentViewController.scrollView.contentSize
contentSize.height += self.contentViewController.scrollView.contentInset.top + self.contentViewController.scrollView.contentInset.bottom
contentSize.height += maximumContentY
contentSize.height = max(contentSize.height, self.view.bounds.height + maximumContentY - (self.navigationController?.navigationBar.bounds.height ?? 0))
self.scrollView.contentSize = contentSize
self.scrollView.contentOffset = contentOffset
}
func update()
{
// Overridden by subclasses.
}
/// Cannot add @objc functions in extensions of generic types, so include them in main definition instead.
//MARK: Notifications
@objc private func willEnterForeground(_ notification: Notification)
{
guard let navigationController = self.navigationController, navigationController.topViewController == self else { return }
self._shouldResetLayout = true
self.view.setNeedsLayout()
}
@objc private func didBecomeActive(_ notification: Notification)
{
guard let navigationController = self.navigationController, navigationController.topViewController == self else { return }
// Fixes incorrect blur after app becomes inactive -> active again.
self._shouldResetLayout = true
self.view.setNeedsLayout()
}
//MARK: UIAdaptivePresentationControllerDelegate
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool
{
return false
}
//MARK: UIScrollViewDelegate
func scrollViewDidScroll(_ scrollView: UIScrollView)
{
switch scrollView
{
case self.scrollView: self.view.setNeedsLayout()
case self.headerScrollView:
// Do NOT call setNeedsLayout(), or else it will mess with scrolling.
self.headerScrollView.showsHorizontalScrollIndicator = false
self.updateBlur()
default: break
}
}
//MARK: UIGestureRecognizerDelegate
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool
{
// Ignore interactive back gesture when viewing header, which means returning `true` to enable ignoreBackGestureRecognizer.
let disableBackGesture = self.isViewingHeader
return disableBackGesture
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool
{
return true
}
}
private extension HeaderContentViewController
{
func showNavigationBar()
{
self.navigationBarIconView.alpha = 1.0
self.navigationBarNameLabel.alpha = 1.0
self.navigationBarButton.alpha = 1.0
self.updateNavigationBarAppearance(isHidden: false)
if self.traitCollection.userInterfaceStyle == .dark
{
self._preferredStatusBarStyle = .lightContent
}
else
{
self._preferredStatusBarStyle = .default
}
if #unavailable(iOS 17)
{
self.navigationController?.setNeedsStatusBarAppearanceUpdate()
}
}
func hideNavigationBar()
{
self.navigationBarIconView.alpha = 0.0
self.navigationBarNameLabel.alpha = 0.0
self.navigationBarButton.alpha = 0.0
self.updateNavigationBarAppearance(isHidden: true)
self._preferredStatusBarStyle = .lightContent
if #unavailable(iOS 17)
{
self.navigationController?.setNeedsStatusBarAppearanceUpdate()
}
}
func updateNavigationBarAppearance(isHidden: Bool)
{
let barAppearance = self.navigationItem.standardAppearance as? NavigationBarAppearance ?? NavigationBarAppearance()
if isHidden
{
barAppearance.configureWithTransparentBackground()
barAppearance.ignoresUserInteraction = true
}
else
{
barAppearance.configureWithDefaultBackground()
barAppearance.ignoresUserInteraction = false
}
barAppearance.titleTextAttributes = [.foregroundColor: UIColor.clear]
let dynamicColor = UIColor { traitCollection in
var tintColor = self.tintColor ?? .altPrimary
if traitCollection.userInterfaceStyle == .dark && tintColor.isTooDark
{
tintColor = .white
}
else
{
tintColor = tintColor.adjustedForDisplay
}
return tintColor
}
let tintColor = isHidden ? UIColor.clear : dynamicColor
barAppearance.configureWithTintColor(tintColor)
self.navigationItem.standardAppearance = barAppearance
self.navigationItem.scrollEdgeAppearance = barAppearance
}
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 updateBlur()
{
// A full blur is too much for header, so we reduce the visible blur by 0.3, resulting in 70% blur.
let minimumBlurFraction = 0.3 as CGFloat
if self.isViewingHeader
{
let maximumX = self.headerScrollView.bounds.width
let fraction = self.headerScrollView.contentOffset.x / maximumX
let fractionComplete = (fraction * (1.0 - minimumBlurFraction)) + minimumBlurFraction
self.blurAnimator?.fractionComplete = fractionComplete
}
else if self.scrollView.contentOffset.y < 0
{
// 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
}
}
func prepareNavigationBarAnimation()
{
self.resetNavigationBarAnimation()
self.navigationBarAnimator = UIViewPropertyAnimator(duration: 1.0, curve: .linear) { [weak self] in
self?.showNavigationBar()
// Must call layoutIfNeeded() to animate appearance change.
self?.navigationController?.navigationBar.layoutIfNeeded()
self?.contentViewController.view.layer.cornerRadius = 0
}
self.navigationBarAnimator?.startAnimation()
self.navigationBarAnimator?.pauseAnimation()
self.update()
}
func resetNavigationBarAnimation()
{
guard self.navigationBarAnimator != nil else { return }
self.navigationBarAnimator?.stopAnimation(true)
self.navigationBarAnimator = nil
self.hideNavigationBar()
self.contentViewController.view.layer.cornerRadius = self.contentViewControllerShadowView.layer.cornerRadius
}
}

View File

@@ -1,101 +0,0 @@
//
// NavigationBar.swift
// AltStore
//
// Created by Riley Testut on 7/15/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
import Roxas
class NavigationBarAppearance: UINavigationBarAppearance
{
// We sometimes need to ignore user interaction so
// we can tap items underneath the navigation bar.
var ignoresUserInteraction: Bool = false
override func copy(with zone: NSZone? = nil) -> Any
{
let copy = super.copy(with: zone) as! NavigationBarAppearance
copy.ignoresUserInteraction = self.ignoresUserInteraction
return copy
}
}
class NavigationBar: UINavigationBar
{
@IBInspectable var automaticallyAdjustsItemPositions: Bool = true
override init(frame: CGRect)
{
super.init(frame: frame)
self.initialize()
}
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
self.initialize()
}
private func initialize()
{
let standardAppearance = UINavigationBarAppearance()
standardAppearance.configureWithDefaultBackground()
standardAppearance.shadowColor = nil
let edgeAppearance = UINavigationBarAppearance()
edgeAppearance.configureWithOpaqueBackground()
edgeAppearance.backgroundColor = self.barTintColor
edgeAppearance.shadowColor = nil
if let tintColor = self.barTintColor
{
let textAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
standardAppearance.backgroundColor = tintColor
standardAppearance.titleTextAttributes = textAttributes
standardAppearance.largeTitleTextAttributes = textAttributes
edgeAppearance.titleTextAttributes = textAttributes
edgeAppearance.largeTitleTextAttributes = textAttributes
}
else
{
standardAppearance.backgroundColor = nil
}
self.scrollEdgeAppearance = edgeAppearance
self.standardAppearance = standardAppearance
}
override func layoutSubviews()
{
super.layoutSubviews()
if self.automaticallyAdjustsItemPositions
{
// We can't easily shift just the back button up, so we shift the entire content view slightly.
for contentView in self.subviews
{
guard NSStringFromClass(type(of: contentView)).contains("ContentView") else { continue }
contentView.center.y -= 2
}
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
{
if let appearance = self.topItem?.standardAppearance as? NavigationBarAppearance, appearance.ignoresUserInteraction
{
// Ignore touches.
return nil
}
return super.hitTest(point, with: event)
}
}

View File

@@ -1,150 +0,0 @@
//
// VibrantButton.swift
// AltStore
//
// Created by Riley Testut on 3/22/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import UIKit
private let preferredFont = UIFont.boldSystemFont(ofSize: 14)
class VibrantButton: UIButton
{
var title: String? {
didSet {
if #available(iOS 15, *)
{
self.configuration?.title = self.title
}
else
{
self.setTitle(self.title, for: .normal)
}
}
}
var image: UIImage? {
didSet {
if #available(iOS 15, *)
{
self.configuration?.image = self.image
}
else
{
self.setImage(self.image, for: .normal)
}
}
}
var contentInsets: NSDirectionalEdgeInsets = .zero {
didSet {
if #available(iOS 15, *)
{
self.configuration?.contentInsets = self.contentInsets
}
else
{
self.contentEdgeInsets = UIEdgeInsets(top: self.contentInsets.top, left: self.contentInsets.leading, bottom: self.contentInsets.bottom, right: self.contentInsets.trailing)
}
}
}
override var isIndicatingActivity: Bool {
didSet {
guard #available(iOS 15, *) else { return }
self.updateConfiguration()
}
}
private let vibrancyView = UIVisualEffectView(effect: nil)
override init(frame: CGRect)
{
super.init(frame: frame)
self.initialize()
}
required init?(coder: NSCoder)
{
super.init(coder: coder)
self.initialize()
}
private func initialize()
{
let blurEffect = UIBlurEffect(style: .systemThinMaterial)
let vibrancyEffect = UIVibrancyEffect(blurEffect: blurEffect, style: .fill) // .fill is more vibrant than .secondaryLabel
if #available(iOS 15, *)
{
var backgroundConfig = UIBackgroundConfiguration.clear()
backgroundConfig.visualEffect = blurEffect
var config = UIButton.Configuration.plain()
config.cornerStyle = .capsule
config.background = backgroundConfig
config.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { [weak self] (attributes) in
var attributes = attributes
attributes.font = preferredFont
if let self, self.isIndicatingActivity
{
// Hide title when indicating activity, but without changing intrinsicContentSize.
attributes.foregroundColor = UIColor.clear
}
return attributes
}
self.configuration = config
}
else
{
self.clipsToBounds = true
self.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8) // Add padding.
let blurView = UIVisualEffectView(effect: blurEffect)
blurView.isUserInteractionEnabled = false
self.addSubview(blurView, pinningEdgesWith: .zero)
self.insertSubview(blurView, at: 0)
}
self.vibrancyView.effect = vibrancyEffect
self.vibrancyView.isUserInteractionEnabled = false
self.addSubview(self.vibrancyView, pinningEdgesWith: .zero)
}
override func layoutSubviews()
{
super.layoutSubviews()
self.layer.cornerRadius = self.bounds.midY
// Make sure content subviews are inside self.vibrancyView.contentView.
if let titleLabel = self.titleLabel, titleLabel.superview != self.vibrancyView.contentView
{
self.vibrancyView.contentView.addSubview(titleLabel)
}
if let imageView = self.imageView, imageView.superview != self.vibrancyView.contentView
{
self.vibrancyView.contentView.addSubview(imageView)
}
if self.activityIndicatorView.superview != self.vibrancyView.contentView
{
self.vibrancyView.contentView.addSubview(self.activityIndicatorView)
}
if #unavailable(iOS 15)
{
// Update font after init because the original titleLabel is replaced.
self.titleLabel?.font = preferredFont
}
}
}

View File

@@ -0,0 +1,13 @@
//
// Error+Message.swift
// SideStore
//
// Created by naturecodevoid on 5/30/23.
// Copyright © 2023 SideStore. All rights reserved.
//
extension Error {
func message() -> String {
(self as? LocalizedError)?.failureReason ?? self.localizedDescription
}
}

View File

@@ -8,6 +8,8 @@
import Intents import Intents
// Requires iOS 14 in-app intent handling.
@available(iOS 14, *)
extension INInteraction extension INInteraction
{ {
static func refreshAllApps() -> INInteraction static func refreshAllApps() -> INInteraction

View File

@@ -0,0 +1,19 @@
//
// Source+Trusted.swift
// SideStore
//
// Created by Fabian Thies on 04.02.23.
// Copyright © 2023 SideStore. All rights reserved.
//
import AltStoreCore
extension Source {
var isOfficial: Bool {
self.identifier == Source.altStoreIdentifier
}
var isTrusted: Bool {
UserDefaults.shared.trustedSourceIDs?.contains(self.identifier) ?? false
}
}

View File

@@ -0,0 +1,17 @@
//
// StoreApp+Searchable.swift
// SideStore
//
// Created by Fabian Thies on 01.12.22.
// Copyright © 2022 SideStore. All rights reserved.
//
import AltStoreCore
extension StoreApp: Filterable {
func matches(_ searchText: String) -> Bool {
searchText.isEmpty ||
self.name.lowercased().contains(searchText.lowercased()) ||
self.developerName.lowercased().contains(searchText.lowercased())
}
}

View File

@@ -0,0 +1,15 @@
//
// StoreApp+SideStore.swift
// SideStore
//
// Created by naturecodevoid on 4/9/23.
// Copyright © 2023 SideStore. All rights reserved.
//
import AltStoreCore
extension StoreApp {
var isSideStore: Bool {
self.bundleIdentifier == Bundle.Info.appbundleIdentifier
}
}

View File

@@ -0,0 +1,19 @@
//
// StoreApp+Trusted.swift
// SideStore
//
// Created by Fabian Thies on 04.02.23.
// Copyright © 2023 SideStore. All rights reserved.
//
import AltStoreCore
extension StoreApp {
var isFromOfficialSource: Bool {
self.source?.isOfficial ?? false
}
var isFromTrustedSource: Bool {
self.source?.isTrusted ?? false
}
}

View File

@@ -0,0 +1,45 @@
//
// UIApplication+SideStore.swift
// SideStore
//
// Created by naturecodevoid on 5/20/23.
// Copyright © 2023 SideStore. All rights reserved.
//
extension UIApplication {
static var keyWindow: UIWindow? {
UIApplication.shared.windows.filter { $0.isKeyWindow }.first
}
static var topController: UIViewController? {
guard var topController = keyWindow?.rootViewController else { return nil }
while let presentedViewController = topController.presentedViewController {
topController = presentedViewController
}
return topController
}
static func alert(
title: String? = nil,
message: String? = nil,
leftButton: (text: String, action: ((UIAlertAction) -> Void)?)? = nil,
rightButton: (text: String, action: ((UIAlertAction) -> Void)?)? = nil,
leftButtonStyle: UIAlertAction.Style = .default,
rightButtonStyle: UIAlertAction.Style = .default
) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
if let leftButton = leftButton {
alert.addAction(UIAlertAction(title: leftButton.text, style: leftButtonStyle, handler: leftButton.action))
}
if let rightButton = rightButton {
alert.addAction(UIAlertAction(title: rightButton.text, style: rightButtonStyle, handler: rightButton.action))
}
if rightButton == nil && leftButton == nil {
alert.addAction(UIAlertAction(title: NSLocalizedString("Ok", comment: ""), style: .default))
}
DispatchQueue.main.async {
topController?.present(alert, animated: true)
}
}
}

View File

@@ -1,62 +0,0 @@
//
// UIColor+AltStore.swift
// AltStore
//
// Created by Riley Testut on 5/23/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import UIKit
extension UIColor
{
static let altBackground = UIColor(named: "Background")!
}
extension UIColor
{
private static let brightnessMaxThreshold = 0.85
private static let brightnessMinThreshold = 0.35
private static let saturationBrightnessThreshold = 0.5
var adjustedForDisplay: UIColor {
guard self.isTooBright || self.isTooDark else { return self }
return UIColor { traits in
var hue: CGFloat = 0
var saturation: CGFloat = 0
var brightness: CGFloat = 0
guard self.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: nil) else { return self }
brightness = min(brightness, UIColor.brightnessMaxThreshold)
if traits.userInterfaceStyle == .dark
{
// Only raise brightness when in dark mode.
brightness = max(brightness, UIColor.brightnessMinThreshold)
}
let color = UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: 1.0)
return color
}
}
var isTooBright: Bool {
var saturation: CGFloat = 0
var brightness: CGFloat = 0
guard self.getHue(nil, saturation: &saturation, brightness: &brightness, alpha: nil) else { return false }
let isTooBright = (brightness >= UIColor.brightnessMaxThreshold && saturation <= UIColor.saturationBrightnessThreshold)
return isTooBright
}
var isTooDark: Bool {
var brightness: CGFloat = 0
guard self.getHue(nil, saturation: nil, brightness: &brightness, alpha: nil) else { return false }
let isTooDark = brightness <= UIColor.brightnessMinThreshold
return isTooDark
}
}

View File

@@ -29,6 +29,7 @@ extension UIDevice
} }
} }
@available(iOS 14, *)
var supportsFugu14: Bool { var supportsFugu14: Bool {
#if targetEnvironment(simulator) #if targetEnvironment(simulator)
return true return true
@@ -39,6 +40,7 @@ extension UIDevice
#endif #endif
} }
@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)

View File

@@ -16,6 +16,7 @@ private extension SystemSoundID
static let tryAgain = SystemSoundID(1102) static let tryAgain = SystemSoundID(1102)
} }
@available(iOS 13, *)
extension UIDevice extension UIDevice
{ {
enum VibrationPattern enum VibrationPattern
@@ -25,6 +26,7 @@ extension UIDevice
} }
} }
@available(iOS 13, *)
extension UIDevice extension UIDevice
{ {
var isVibrationSupported: Bool { var isVibrationSupported: Bool {

View File

@@ -1,18 +0,0 @@
//
// UIFontDescriptor+Bold.swift
// AltStore
//
// Created by Riley Testut on 10/16/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import UIKit
extension UIFontDescriptor
{
func bolded() -> UIFontDescriptor
{
guard let descriptor = self.withSymbolicTraits(.traitBold) else { return self }
return descriptor
}
}

View File

@@ -1,22 +0,0 @@
//
// UINavigationBarAppearance+TintColor.swift
// AltStore
//
// Created by Riley Testut on 4/4/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import UIKit
extension UINavigationBarAppearance
{
func configureWithTintColor(_ tintColor: UIColor)
{
let buttonAppearance = UIBarButtonItemAppearance(style: .plain)
buttonAppearance.normal.titleTextAttributes = [.foregroundColor: tintColor]
self.buttonAppearance = buttonAppearance
let backButtonImage = UIImage(systemName: "chevron.backward")?.withTintColor(tintColor, renderingMode: .alwaysOriginal)
self.setBackIndicatorImage(backButtonImage, transitionMaskImage: backButtonImage)
}
}

View File

@@ -1,14 +0,0 @@
//
// UTType+AltStore.swift
// AltStore
//
// Created by Riley Testut on 11/3/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import UniformTypeIdentifiers
extension UTType
{
static let ipa = UTType(importedAs: "com.apple.itunes.ipa")
}

View File

@@ -0,0 +1,22 @@
//
// View+Hidden.swift
// SideStore
//
// Created by naturecodevoid on 2/18/23.
// Copyright © 2023 SideStore. All rights reserved.
//
import SwiftUI
// https://stackoverflow.com/a/59228385 (modified)
extension View {
@ViewBuilder func isHidden(_ hidden: Binding<Bool>, remove: Bool = false) -> some View {
if hidden.wrappedValue {
if !remove {
self.hidden()
}
} else {
self
}
}
}

View File

@@ -2,18 +2,25 @@
<!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>
<string>group.com.SideStore.SideStore</string>
</array> </array>
<key>ALTDeviceID</key> <key>ALTDeviceID</key>
<string>00008120-001270DA119B401E</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>
<!--
for some reason, when we use the Info.plist preprocessor, 2 slashes in a row
removes the rest of the line and makes the plist invalid. to get around this,
we add a variable expansion ( $() ) in between the slashes that will ultimately
evaluate to nothing, keeping the original URL while keeping the plist valid.
-->
<string>http:/$()/ani.sidestore.io:6969</string>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDocumentTypes</key> <key>CFBundleDocumentTypes</key>
@@ -35,17 +42,6 @@
</array> </array>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIcons</key>
<dict>
<key>CFBundlePrimaryIcon</key>
<dict>
<key>NSAppIconComplementingColorNames</key>
<array>
<string>GradientTop</string>
<string>GradientBottom</string>
</array>
</dict>
</dict>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
@@ -54,6 +50,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>
@@ -62,9 +60,10 @@
<key>CFBundleTypeRole</key> <key>CFBundleTypeRole</key>
<string>Editor</string> <string>Editor</string>
<key>CFBundleURLName</key> <key>CFBundleURLName</key>
<string>SideStore General</string> <string>AltStore General</string>
<key>CFBundleURLSchemes</key> <key>CFBundleURLSchemes</key>
<array> <array>
<string>altstore</string>
<string>sidestore</string> <string>sidestore</string>
</array> </array>
</dict> </dict>
@@ -72,15 +71,16 @@
<key>CFBundleTypeRole</key> <key>CFBundleTypeRole</key>
<string>Editor</string> <string>Editor</string>
<key>CFBundleURLName</key> <key>CFBundleURLName</key>
<string>SideStore Backup</string> <string>AltStore Backup</string>
<key>CFBundleURLSchemes</key> <key>CFBundleURLSchemes</key>
<array> <array>
<string>altstore-com.rileytestut.AltStore</string>
<string>sidestore-com.SideStore.SideStore</string> <string>sidestore-com.SideStore.SideStore</string>
</array> </array>
</dict> </dict>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string> <string>1</string>
<key>INIntentsSupported</key> <key>INIntentsSupported</key>
<array> <array>
<string>RefreshAllIntent</string> <string>RefreshAllIntent</string>
@@ -88,18 +88,17 @@
</array> </array>
<key>LSApplicationQueriesSchemes</key> <key>LSApplicationQueriesSchemes</key>
<array> <array>
<string>sidestore-com.SideStore.SideStore</string> <string>altstore-com.rileytestut.AltStore</string>
<string>sidestore-com.SideStore.SideStore.Beta</string> <string>altstore-com.rileytestut.AltStore.Beta</string>
<string>altstore-com.rileytestut.Delta</string>
<string>altstore-com.rileytestut.Delta.Beta</string>
<string>altstore-com.rileytestut.Delta.Lite</string>
<string>altstore-com.rileytestut.Delta.Lite.Beta</string>
<string>altstore-com.rileytestut.Clip</string>
<string>altstore-com.rileytestut.Clip.Beta</string>
</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>
@@ -111,42 +110,6 @@
<string>RefreshAllIntent</string> <string>RefreshAllIntent</string>
<string>ViewAppIntent</string> <string>ViewAppIntent</string>
</array> </array>
<key>OSLogPreferences</key>
<dict>
<key>com.SideStore.SideStore</key>
<dict>
<key>AltJIT</key>
<dict>
<key>Level</key>
<dict>
<key>Enable</key>
<string>Info</string>
<key>Persist</key>
<string>Info</string>
</dict>
</dict>
<key>Main</key>
<dict>
<key>Level</key>
<dict>
<key>Enable</key>
<string>Info</string>
<key>Persist</key>
<string>Info</string>
</dict>
</dict>
<key>Sideload</key>
<dict>
<key>Level</key>
<dict>
<key>Enable</key>
<string>Info</string>
<key>Persist</key>
<string>Info</string>
</dict>
</dict>
</dict>
</dict>
<key>UIApplicationSceneManifest</key> <key>UIApplicationSceneManifest</key>
<dict> <dict>
<key>UIApplicationSupportsMultipleScenes</key> <key>UIApplicationSupportsMultipleScenes</key>
@@ -174,10 +137,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>
@@ -222,8 +188,6 @@
<dict> <dict>
<key>public.filename-extension</key> <key>public.filename-extension</key>
<string>ipa</string> <string>ipa</string>
<key>public.mime-type</key>
<string>application/x-ios-app</string>
</dict> </dict>
</dict> </dict>
<dict> <dict>
@@ -246,5 +210,17 @@
</dict> </dict>
</dict> </dict>
</array> </array>
<key>UISupportsDocumentBrowser</key>
<true/>
<key>UIFileSharingEnabled</key>
<true/>
<!--
#if MDC
-->
<key>NSAppleMusicUsageDescription</key>
<string>Full access to files on your device is required to apply the installd patch to remove the 3 app limit that free developer accounts have.</string>
<!--
#endif
-->
</dict> </dict>
</plist> </plist>

View File

@@ -1,29 +0,0 @@
//
// AppShortcuts.swift
// AltStore
//
// Created by Riley Testut on 8/23/22.
// Copyright © 2022 Riley Testut. All rights reserved.
//
import AppIntents
@available(iOS 17, *)
public struct ShortcutsProvider: AppShortcutsProvider
{
public static var appShortcuts: [AppShortcut] {
AppShortcut(intent: RefreshAllAppsIntent(),
phrases: [
"Refresh \(.applicationName)",
"Refresh \(.applicationName) apps",
"Refresh my \(.applicationName) apps",
"Refresh apps with \(.applicationName)",
],
shortTitle: "Refresh All Apps",
systemImageName: "arrow.triangle.2.circlepath")
}
public static var shortcutTileColor: ShortcutTileColor {
return .teal
}
}

View File

@@ -1,194 +0,0 @@
//
// RefreshAllAppsIntent.swift
// AltStore
//
// Created by Riley Testut on 8/18/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import AppIntents
import WidgetKit
import AltStoreCore
// Shouldn't conform types we don't own to protocols we don't own, so make custom
// NSError subclass that conforms to CustomLocalizedStringResourceConvertible instead.
//
// Would prefer to just conform ALTLocalizedError to CustomLocalizedStringResourceConvertible,
// but that can't be done without raising minimum version for ALTLocalizedError to iOS 16 :/
@available(iOS 16, *)
class IntentError: NSError, CustomLocalizedStringResourceConvertible
{
var localizedStringResource: LocalizedStringResource {
return "\(self.localizedDescription)"
}
init(_ error: some Error)
{
let serializedError = (error as NSError).sanitizedForSerialization()
super.init(domain: serializedError.domain, code: serializedError.code, userInfo: serializedError.userInfo)
}
required init?(coder: NSCoder)
{
super.init(coder: coder)
}
}
@available(iOS 17.0, *)
extension RefreshAllAppsIntent
{
private actor OperationActor
{
private(set) var operation: BackgroundRefreshAppsOperation?
func set(_ operation: BackgroundRefreshAppsOperation?)
{
self.operation = operation
}
}
}
@available(iOS 17.0, *)
struct RefreshAllAppsIntent: AppIntent, CustomIntentMigratedAppIntent, PredictableIntent, ProgressReportingIntent, ForegroundContinuableIntent
{
static let intentClassName = "RefreshAllIntent"
static var title: LocalizedStringResource = "Refresh All Apps"
static var description = IntentDescription("Refreshes your sideloaded apps to prevent them from expiring.")
static var parameterSummary: some ParameterSummary {
Summary("Refresh All Apps")
}
static var predictionConfiguration: some IntentPredictionConfiguration {
IntentPrediction {
DisplayRepresentation(
title: "Refresh All Apps",
subtitle: ""
)
}
}
let presentsNotifications: Bool
private let operationActor = OperationActor()
init(presentsNotifications: Bool)
{
self.presentsNotifications = presentsNotifications
self.progress.completedUnitCount = 0
self.progress.totalUnitCount = 1
}
init()
{
self.init(presentsNotifications: false)
}
func perform() async throws -> some IntentResult & ProvidesDialog
{
do
{
// Request foreground execution at ~27 seconds to gracefully handle timeout.
let deadline: ContinuousClock.Instant = .now + .seconds(27)
try await withThrowingTaskGroup(of: Void.self) { taskGroup in
taskGroup.addTask {
try await self.refreshAllApps()
}
taskGroup.addTask {
try await Task.sleep(until: deadline)
throw OperationError.timedOut
}
do
{
for try await _ in taskGroup.prefix(1)
{
// We only care about the first child task to complete.
taskGroup.cancelAll()
break
}
}
catch OperationError.timedOut
{
// We took too long to finish and return the final result,
// so we'll now present a normal notification when finished.
let operation = await self.operationActor.operation
operation?.presentsFinishedNotification = true
try await self.requestToContinueInForeground()
}
}
return .result(dialog: "All apps have been refreshed.")
}
catch
{
let intentError = IntentError(error)
throw intentError
}
}
}
@available(iOS 17.0, *)
private extension RefreshAllAppsIntent
{
func refreshAllApps() async throws
{
if !DatabaseManager.shared.isStarted
{
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
DatabaseManager.shared.start { error in
if let error
{
continuation.resume(throwing: error)
}
else
{
continuation.resume()
}
}
}
}
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
let installedApps = await context.perform { InstalledApp.fetchAppsForRefreshingAll(in: context) }
try await withCheckedThrowingContinuation { continuation in
let operation = AppManager.shared.backgroundRefresh(installedApps, presentsNotifications: self.presentsNotifications) { (result) in
do
{
let results = try result.get()
for (_, result) in results
{
guard case let .failure(error) = result else { continue }
throw error
}
continuation.resume()
}
catch ~RefreshErrorCode.noInstalledApps
{
continuation.resume()
}
catch
{
continuation.resume(throwing: error)
}
}
operation.ignoresServerNotFoundError = false
self.progress.addChild(operation.progress, withPendingUnitCount: 1)
Task {
await self.operationActor.set(operation)
}
}
}
}

View File

@@ -1,45 +0,0 @@
//
// RefreshAllAppsWidgetIntent.swift
// AltStore
//
// Created by Riley Testut on 8/18/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import AppIntents
@available(iOS 17, *)
struct RefreshAllAppsWidgetIntent: AppIntent, ProgressReportingIntent
{
static var title: LocalizedStringResource { "Refresh Apps via Widget" }
static var isDiscoverable: Bool { false } // Don't show in Shortcuts or Spotlight.
#if !WIDGET_EXTENSION
private let intent = RefreshAllAppsIntent(presentsNotifications: true)
#endif
func perform() async throws -> some IntentResult
{
#if !WIDGET_EXTENSION
do
{
_ = try await self.intent.perform()
}
catch
{
print("Failed to refresh apps via widget.", error)
}
#endif
return .result()
}
}
// To ensure this intent is handled by the app itself (and not widget extension)
// we need to conform to either `ForegroundContinuableIntent` or `AudioPlaybackIntent`.
// https://mastodon.social/@mgorbach/110812347476671807
//
// Unfortunately `ForegroundContinuableIntent` is marked as unavailable in app extensions,
// so we "conform" RefreshAllAppsWidgetIntent to it in an `unavailable` extension ¯\_()_/¯
@available(iOS, unavailable)
extension RefreshAllAppsWidgetIntent: ForegroundContinuableIntent {}

View File

@@ -8,13 +8,12 @@
import Foundation import Foundation
import minimuxer
import AltStoreCore import AltStoreCore
@available(iOS 14, *) @available(iOS 14, *)
final class IntentHandler: NSObject, RefreshAllIntentHandling final class IntentHandler: NSObject, RefreshAllIntentHandling
{ {
private let queue = DispatchQueue(label: "io.sidestore.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]()
@@ -40,12 +39,8 @@ final class IntentHandler: NSObject, RefreshAllIntentHandling
// 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.
self.queue.asyncAfter(deadline: .now() + 8.0) { self.queue.asyncAfter(deadline: .now() + 9.0) {
if minimuxer.ready() { self.finish(intent, response: RefreshAllIntentResponse(code: .ready, userActivity: nil))
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
} else {
self.finish(intent, response: RefreshAllIntentResponse(code: .failure, userActivity: nil))
}
} }
if !DatabaseManager.shared.isStarted if !DatabaseManager.shared.isStarted
@@ -57,14 +52,12 @@ final class IntentHandler: NSObject, RefreshAllIntentHandling
} }
else else
{ {
self.finish(intent, response: RefreshAllIntentResponse(code: .ready, userActivity: nil))
self.refreshApps(intent: intent) self.refreshApps(intent: intent)
} }
} }
} }
else else
{ {
self.finish(intent, response: RefreshAllIntentResponse(code: .ready, userActivity: nil))
self.refreshApps(intent: intent) self.refreshApps(intent: intent)
} }
} }
@@ -90,11 +83,6 @@ final class IntentHandler: NSObject, RefreshAllIntentHandling
// 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
if minimuxer.ready() {
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
} else {
self.finish(intent, response: RefreshAllIntentResponse(code: .failure, userActivity: nil))
}
} }
self.finish(intent, response: RefreshAllIntentResponse(code: .inProgress, userActivity: nil)) self.finish(intent, response: RefreshAllIntentResponse(code: .inProgress, userActivity: nil))
@@ -103,6 +91,7 @@ final class IntentHandler: NSObject, RefreshAllIntentHandling
} }
} }
@available(iOS 14, *)
private extension IntentHandler private extension IntentHandler
{ {
func finish(_ intent: RefreshAllIntent, response: RefreshAllIntentResponse) func finish(_ intent: RefreshAllIntent, response: RefreshAllIntentResponse)
@@ -117,9 +106,6 @@ private extension IntentHandler
{ {
// 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
DispatchQueue.main.async {
UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
}
} }
} }
} }
@@ -127,7 +113,7 @@ private extension IntentHandler
func refreshApps(intent: RefreshAllIntent) func refreshApps(intent: RefreshAllIntent)
{ {
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
let installedApps = InstalledApp.fetchAppsForRefreshingAll(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
{ {
@@ -140,12 +126,10 @@ private extension IntentHandler
} }
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil)) self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
} }
catch ~RefreshErrorCode.noInstalledApps catch RefreshError.noInstalledApps
{ {
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil)) self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
} }
catch let error as NSError catch let error as NSError
{ {

View File

@@ -7,416 +7,216 @@
// //
import UIKit import UIKit
import SwiftUI
import Roxas import Roxas
import EmotionalDamage import EmotionalDamage
import minimuxer import minimuxer
import WidgetKit
import AltSign
import AltStoreCore import AltStoreCore
import UniformTypeIdentifiers import UniformTypeIdentifiers
let pairingFileName = "ALTPairingFile.mobiledevicepairing" let pairingFileName = "ALTPairingFile.mobiledevicepairing"
final class LaunchViewController: UIViewController, UIDocumentPickerDelegate { final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
{
private var didFinishLaunching = false private var didFinishLaunching = false
private var retries = 0
private var maxRetries = 3 private var destinationViewController: UIViewController!
private var splashView: SplashView!
private var destinationViewController: TabBarController? override var launchConditions: [RSTLaunchCondition] {
private var startTime: Date! let isDatabaseStarted = RSTLaunchCondition(condition: { DatabaseManager.shared.isStarted }) { (completionHandler) in
DatabaseManager.shared.start(completionHandler: completionHandler)
override func viewDidLoad() {
super.viewDidLoad()
splashView = SplashView(frame: view.bounds, appName: "SideStore")
destinationViewController = storyboard!.instantiateViewController(withIdentifier: "tabBarController") as? TabBarController
view.addSubview(splashView)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
guard !didFinishLaunching else { return }
Task {
startTime = Date()
await runLaunchSequence()
doPostLaunch()
} }
return [isDatabaseStarted]
} }
private func runLaunchSequence() async { override var childForStatusBarStyle: UIViewController? {
guard retries < maxRetries else { return } return self.children.first
retries += 1 }
await Task.detached { override var childForStatusBarHidden: UIViewController? {
if !DatabaseManager.shared.isStarted { return self.children.first
await withCheckedContinuation { continuation in }
DatabaseManager.shared.start { error in
if let error { override func viewDidLoad()
Task { await self.handleLaunchError(error, retryCallback: self.runLaunchSequence) } {
} else { defer {
Task { await self.finishLaunching() } if UnstableFeatures.enabled(.swiftUI) {
} let rootView = RootView()
continuation.resume(returning: ()) .environment(\.managedObjectContext, DatabaseManager.shared.viewContext)
} self.destinationViewController = UIHostingController(rootView: rootView)
}
} else { } else {
await self.finishLaunching() // Create destinationViewController now so view controllers can register for receiving Notifications.
self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as! TabBarController
} }
}.value }
super.viewDidLoad()
} }
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
private func doPostLaunch() { #if MDC
SideJITManager.shared.checkAndPromptIfNeeded(presentingVC: self) MDC.alertIfNotPatched()
if #available(iOS 17, *), UserDefaults.standard.sidejitenable { #endif
DispatchQueue.global().async { SideJITManager.shared.askForNetwork() }
print("SideJITServer Enabled")
}
#if !targetEnvironment(simulator) #if !targetEnvironment(simulator)
if UnstableFeatures.enabled(.onboarding) && !UserDefaults.standard.onboardingComplete {
detectAndImportAccountFile() self.showOnboarding()
return
if UserDefaults.standard.enableEMPforWireguard {
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.") self.showOnboarding(enabledSteps: [.pairing])
return return
} }
start_minimuxer_threads(pf) start_minimuxer_threads(pf)
#endif #endif
} }
func start_minimuxer_threads(_ pairing_file: String) { func showOnboarding(enabledSteps: [OnboardingStep] = OnboardingStep.allCases) {
target_minimuxer_address() let onboardingView = OnboardingView(onDismiss: { self.dismiss(animated: true) }, enabledSteps: enabledSteps)
let documentsDirectory = FileManager.default.documentsDirectory.absoluteString .environment(\.managedObjectContext, DatabaseManager.shared.viewContext)
do { let navigationController = UINavigationController(rootViewController: UIHostingController(rootView: onboardingView))
let loggingEnabled = UserDefaults.standard.isMinimuxerConsoleLoggingEnabled navigationController.isNavigationBarHidden = true
try minimuxer.startWithLogger(pairing_file, documentsDirectory, loggingEnabled) navigationController.isModalInPresentation = true
} catch { self.present(navigationController, animated: true)
try! FileManager.default.removeItem(at: FileManager.default.documentsDirectory.appendingPathComponent(pairingFileName)) }
displayError("minimuxer failed to start, please restart SideStore. \((error as? LocalizedError)?.failureReason ?? "UNKNOWN ERROR")")
} func fetchPairingFile() -> String? {
start_auto_mounter(documentsDirectory) let filename = "ALTPairingFile.mobiledevicepairing"
let fm = FileManager.default
let documentsPath = fm.documentsDirectory.appendingPathComponent("/\(filename)")
if fm.fileExists(atPath: documentsPath.path), let contents = try? String(contentsOf: documentsPath), !contents.isEmpty {
print("Loaded ALTPairingFile from \(documentsPath.path)")
return contents
} else if
let appResourcePath = Bundle.main.url(forResource: "ALTPairingFile", withExtension: "mobiledevicepairing"),
fm.fileExists(atPath: appResourcePath.path),
let data = fm.contents(atPath: appResourcePath.path),
let contents = String(data: data, encoding: .utf8),
!contents.isEmpty {
print("Loaded ALTPairingFile from \(appResourcePath.path)")
return contents
} 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")
return plistString
}
return nil
} }
func fetchPairingFile() -> String? { PairingFileManager.shared.fetchPairingFile(presentingVC: self) }
func displayError(_ msg: String) { func displayError(_ msg: String) {
print(msg) print(msg)
let alert = UIAlertController(title: "Error launching SideStore", message: msg, preferredStyle: .alert) // Create a new alert
self.present(alert, animated: true) let dialogMessage = UIAlertController(title: "Error launching SideStore", message: msg, preferredStyle: .alert)
}
// Present alert to user
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
defer {
if (isSecuredURL) {
url.stopAccessingSecurityScopedResource()
}
}
do { do {
let data = try Data(contentsOf: url) // Read to a string
guard let pairingString = String(data: data, encoding: .utf8) else { let data1 = try Data(contentsOf: urls[0])
let pairing_string = String(bytes: data1, encoding: .utf8)
if pairing_string == nil {
displayError("Unable to read pairing file") displayError("Unable to read pairing file")
return
} }
try pairingString.write(to: FileManager.default.documentsDirectory.appendingPathComponent(pairingFileName), atomically: true, encoding: .utf8)
start_minimuxer_threads(pairingString) // Save to a file for next launch
let pairingFile = FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)")
try pairing_string?.write(to: pairingFile, atomically: true, encoding: String.Encoding.utf8)
// Start minimuxer now that we have a file
start_minimuxer_threads(pairing_string!)
} catch { } catch {
displayError("Unable to read pairing file") displayError("Unable to read pairing file")
} }
if (isSecuredURL) {
url.stopAccessingSecurityScopedResource()
}
controller.dismiss(animated: true, completion: nil) controller.dismiss(animated: true, completion: nil)
} }
func documentPickerWasCancelled(_ controller: 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 importAccountAtFile(_ file: URL, remove: Bool = false) { func start_minimuxer_threads(_ pairing_file: String) {
_ = file.startAccessingSecurityScopedResource() target_minimuxer_address()
defer { file.stopAccessingSecurityScopedResource() } let documentsDirectory = FileManager.default.documentsDirectory.absoluteString
guard let accountD = try? Data(contentsOf: file) else { do {
let toastView = ToastView(text: NSLocalizedString("Could not read data from file!", comment: ""), detailText: "\(file)") try start(pairing_file, documentsDirectory)
return toastView.show(in: self) } catch {
try! FileManager.default.removeItem(at: FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)"))
displayError("minimuxer failed to start, please restart SideStore. \(error.message())")
} }
guard let account = try? Foundation.JSONDecoder().decode(ImportedAccount.self, from: accountD) else { set_debug(UserDefaults.shared.isDebugLoggingEnabled)
let toastView = ToastView(text: NSLocalizedString("Could not parse data from file!", comment: ""), detailText: "\(file)") start_auto_mounter(documentsDirectory)
return toastView.show(in: self) }
}
extension LaunchViewController
{
override func handleLaunchError(_ error: Error)
{
do
{
throw error
} }
print("We want to import this account probably: \(account)") catch let error as NSError
if remove { {
try? FileManager.default.removeItem(at: file) let title = error.userInfo[NSLocalizedFailureErrorKey] as? String ?? NSLocalizedString("Unable to Launch SideStore", comment: "")
}
Keychain.shared.appleIDEmailAddress = account.email let errorDescription: String
Keychain.shared.appleIDPassword = account.password
Keychain.shared.adiPb = account.adiPB if #available(iOS 14.5, *)
Keychain.shared.identifier = account.local_user {
if let altCert = ALTCertificate(p12Data: account.cert, password: account.certpass) { let errorMessages = [error.debugDescription] + error.underlyingErrors.map { ($0 as NSError).debugDescription }
Keychain.shared.signingCertificate = altCert.encryptedP12Data(withPassword: "")! errorDescription = errorMessages.joined(separator: "\n\n")
Keychain.shared.signingCertificatePassword = account.certpass }
let toastView = ToastView(text: NSLocalizedString("Successfully imported '\(account.email)'!", comment: ""), detailText: "SideStore should be fully operational!") else
return toastView.show(in: self) {
} else { errorDescription = error.debugDescription
let toastView = ToastView(text: NSLocalizedString("Failed to import account certificate!", comment: ""), detailText: "Failed to create ALTCertificate. Check if the password is correct. Still imported account/adi.pb details!") }
return toastView.show(in: self)
let alertController = UIAlertController(title: title, message: errorDescription, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: NSLocalizedString("Retry", comment: ""), style: .default, handler: { (action) in
self.handleLaunchConditions()
}))
self.present(alertController, animated: true, completion: nil)
} }
} }
func detectAndImportAccountFile() { override func finishLaunching()
let accountFileURL = FileManager.default.documentsDirectory.appendingPathComponent("Account.sideconf") {
#if !DEBUG super.finishLaunching()
importAccountAtFile(accountFileURL, remove: true)
#else guard !self.didFinishLaunching else { return }
importAccountAtFile(accountFileURL)
#endif
}
}
extension LaunchViewController {
@MainActor
func handleLaunchError(_ error: Error, retryCallback: (() async -> Void)? = nil) {
do { throw error } catch let error as NSError {
let title = error.userInfo[NSLocalizedFailureErrorKey] as? String ?? NSLocalizedString("Unable to Launch SideStore", comment: "")
let desc: String
if #available(iOS 14.5, *) {
desc = ([error.debugDescription] + error.underlyingErrors.map { ($0 as NSError).debugDescription }).joined(separator: "\n\n")
} else {
desc = error.debugDescription
}
let alert = UIAlertController(title: title, message: desc, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("Retry", comment: ""), style: .default) { _ in
Task { await retryCallback?() }
})
present(alert, animated: true)
}
}
@MainActor
func finishLaunching() async {
guard !didFinishLaunching else { return }
didFinishLaunching = true
AppManager.shared.update() AppManager.shared.update()
AppManager.shared.updatePatronsIfNeeded() AppManager.shared.updatePatronsIfNeeded()
PatreonAPI.shared.refreshPatreonAccount() PatreonAPI.shared.refreshPatreonAccount()
AppManager.shared.updateAllSources { result in
guard case .failure(let error) = result else { return } // Add view controller as child (rather than presenting modally)
Logger.main.error("Failed to update sources on launch. \(error.localizedDescription, privacy: .public)") // so tint adjustment + card presentations works correctly.
self.destinationViewController.view.frame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.height)
self.destinationViewController.view.alpha = 0.0
let errorDesc = ErrorProcessing(.fullError).getDescription(error: error as NSError) self.addChild(self.destinationViewController)
print("Failed to update sources on launch. \(errorDesc)") self.view.addSubview(self.destinationViewController.view, pinningEdgesWith: .zero)
self.destinationViewController.didMove(toParent: self)
var mode: ToastView.InfoMode = .fullError
if String(describing: error).contains("The Internet connection appears to be offline"){ UIView.animate(withDuration: 0.2) {
mode = .localizedDescription // dont make noise! self.destinationViewController.view.alpha = 1.0
}
let toastView = ToastView(error: error, mode: mode)
toastView.addTarget(self.destinationViewController, action: #selector(TabBarController.presentSources), for: .touchUpInside)
toastView.show(in: self.destinationViewController!.selectedViewController ?? self.destinationViewController!)
} }
updateKnownSources()
WidgetCenter.shared.reloadAllTimelines()
didFinishLaunching = true
let destinationVC = destinationViewController! self.didFinishLaunching = true
let elapsed = abs(startTime.timeIntervalSinceNow)
let remaining = elapsed >= 1 ? 0 : 1 - elapsed
try? await Task.sleep(nanoseconds: UInt64(remaining * 1_000_000_000))
destinationVC.loadViewIfNeeded()
addChild(destinationVC)
destinationVC.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(destinationVC.view)
destinationVC.didMove(toParent: self)
// Pin edges BEFORE animation
NSLayoutConstraint.activate([
destinationVC.view.topAnchor.constraint(equalTo: view.topAnchor),
destinationVC.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
destinationVC.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
destinationVC.view.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
// Set initial alpha for fade-in
destinationVC.view.alpha = 0
UIView.transition(with: view, duration: 0.3, options: .transitionCrossDissolve) { [self] in
self.splashView.alpha = 0
destinationVC.view.alpha = 1
} completion: { _ in
self.splashView.removeFromSuperview()
self.destinationViewController = destinationVC
}
}
func updateKnownSources() {
AppManager.shared.updateKnownSources { result in
switch result {
case .failure(let error): print("[ALTLog] Failed to update known sources:", error)
case .success((_, let blockedSources)):
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in
let blockedSourceIDs = Set(blockedSources.lazy.map { $0.identifier })
let blockedSourceURLs = Set(blockedSources.lazy.compactMap { $0.sourceURL })
let predicate = NSPredicate(format: "%K IN %@ OR %K IN %@", #keyPath(Source.identifier), blockedSourceIDs, #keyPath(Source.sourceURL), blockedSourceURLs)
let sourceErrors = Source.all(satisfying: predicate, in: context).map { source in
let blocked = blockedSources.first { $0.identifier == source.identifier }
return SourceError.blocked(source, bundleIDs: blocked?.bundleIDs, existingSource: source)
}
guard !sourceErrors.isEmpty else { return }
Task {
for error in sourceErrors {
let title = String(format: NSLocalizedString("“%@” Blocked", comment: ""), error.$source.name)
let message = [error.localizedDescription, error.recoverySuggestion].compactMap { $0 }.joined(separator: "\n\n")
await self.presentAlert(title: title, message: message)
}
}
}
}
}
}
}
// MARK: - SplashView
final class SplashView: UIView {
let iconView = UIImageView()
let titleLabel = UILabel()
init(frame: CGRect, appName: String) {
super.init(frame: frame)
backgroundColor = .systemBackground
setupIcon()
setupTitle(appName: appName)
}
required init?(coder: NSCoder) { fatalError() }
private func setupIcon() {
let container = UIView()
container.translatesAutoresizingMaskIntoConstraints = false
container.layer.shadowColor = UIColor.black.cgColor
container.layer.shadowOpacity = 0.25
container.layer.shadowOffset = CGSize(width: 0, height: 4)
container.layer.shadowRadius = 8
addSubview(container)
iconView.image = UIImage(named: "AppIcon") ?? UIImage(named: "AppIcon60x60") ?? UIImage(systemName: "app.fill")
iconView.contentMode = .scaleAspectFit
iconView.translatesAutoresizingMaskIntoConstraints = false
iconView.layer.cornerRadius = 24
iconView.clipsToBounds = true
container.addSubview(iconView)
NSLayoutConstraint.activate([
container.centerXAnchor.constraint(equalTo: centerXAnchor),
container.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -20),
container.widthAnchor.constraint(equalToConstant: 120),
container.heightAnchor.constraint(equalToConstant: 120),
iconView.topAnchor.constraint(equalTo: container.topAnchor),
iconView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
iconView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
iconView.trailingAnchor.constraint(equalTo: container.trailingAnchor)
])
}
private func setupTitle(appName: String) {
titleLabel.text = appName
titleLabel.font = .systemFont(ofSize: 24, weight: .bold)
titleLabel.textColor = .label
titleLabel.textAlignment = .center
titleLabel.translatesAutoresizingMaskIntoConstraints = false
addSubview(titleLabel)
NSLayoutConstraint.activate([
titleLabel.topAnchor.constraint(equalTo: iconView.bottomAnchor, constant: 12),
titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor)
])
}
}
// MARK: - PairingFileManager
final class PairingFileManager {
static let shared = PairingFileManager()
func fetchPairingFile(presentingVC: UIViewController) -> String? {
let fm = FileManager.default
let filename = pairingFileName
let documentsPath = fm.documentsDirectory.appendingPathComponent("/\(filename)")
if fm.fileExists(atPath: documentsPath.path),
let contents = try? String(contentsOf: documentsPath), !contents.isEmpty {
return contents
}
if let url = Bundle.main.url(forResource: "ALTPairingFile", withExtension: "mobiledevicepairing"),
fm.fileExists(atPath: url.path),
let data = fm.contents(atPath: url.path),
let contents = String(data: data, encoding: .utf8),
!contents.isEmpty, !UserDefaults.standard.isPairingReset { return contents }
if let plistString = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String,
!plistString.isEmpty, !plistString.contains("insert pairing file here"), !UserDefaults.standard.isPairingReset { return plistString }
presentPairingFileAlert(on: presentingVC)
return nil
}
private func presentPairingFileAlert(on vc: UIViewController) {
let alert = UIAlertController(title: "Pairing File", message: "Select the pairing file or select \"Help\" for help.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Help", style: .default) { _ in
if let url = URL(string: "https://docs.sidestore.io/docs/advanced/pairing-file") { UIApplication.shared.open(url) }
sleep(2); exit(0)
})
alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in
var types = UTType.types(tag: "plist", tagClass: .filenameExtension, conformingTo: nil)
types.append(contentsOf: UTType.types(tag: "mobiledevicepairing", tagClass: .filenameExtension, conformingTo: .data))
types.append(.xml)
let picker = UIDocumentPickerViewController(forOpeningContentTypes: types)
picker.delegate = vc as? UIDocumentPickerDelegate
picker.shouldShowFileExtensions = true
vc.present(picker, animated: true)
UserDefaults.standard.isPairingReset = false
})
vc.present(alert, animated: true)
}
}
// MARK: - SideJITManager
final class SideJITManager {
static let shared = SideJITManager()
func checkAndPromptIfNeeded(presentingVC: UIViewController) {
guard #available(iOS 17, *), !UserDefaults.standard.sidejitenable else { return }
DispatchQueue.global().async {
self.isSideJITServerDetected { result in
DispatchQueue.main.async {
switch result {
case .success():
let alert = UIAlertController(title: "SideJITServer Detected", message: "Would you like to enable SideJITServer", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in UserDefaults.standard.sidejitenable = true })
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
presentingVC.present(alert, animated: true)
case .failure(_): print("Cannot find sideJITServer")
}
}
}
}
}
func askForNetwork() {
let address = UserDefaults.standard.textInputSideJITServerurl ?? ""
let SJSURL = address.isEmpty ? "http://sidejitserver._http._tcp.local:8080" : address
URLSession.shared.dataTask(with: URL(string: "\(SJSURL)/re/")!) { data, resp, err in
print("data: \(String(describing: data)), response: \(String(describing: resp)), error: \(String(describing: err))")
}.resume()
}
func isSideJITServerDetected(completion: @escaping (Result<Void, Error>) -> Void) {
let address = UserDefaults.standard.textInputSideJITServerurl ?? ""
let SJSURL = address.isEmpty ? "http://sidejitserver._http._tcp.local:8080" : address
guard let url = URL(string: SJSURL) else { return }
URLSession.shared.dataTask(with: url) { _, _, error in
if let error = error { completion(.failure(error)); return }
completion(.success(()))
}.resume()
} }
} }

View File

@@ -0,0 +1,177 @@
// Extension of MDC+AltStoreCore for the functionality AltStore uses
// The only reason we can't have it all in AltStore is because AltStoreCore requires one variable of MDC to determine the free app limit
import Foundation
import AltStoreCore
extension MDC {
#if MDC
enum PatchError: LocalizedError {
case NoFDA(error: String)
case FailedPatchd
var failureReason: String? {
switch (self) {
case .NoFDA(let error): return L10n.Remove3AppLimitView.Errors.noFDA(error)
case .FailedPatchd: return L10n.Remove3AppLimitView.Errors.failedPatchd
}
}
}
static func patch3AppLimit() async throws {
#if !targetEnvironment(simulator)
let res: PatchError? = await withCheckedContinuation { continuation in
grant_full_disk_access { error in
if let error = error {
continuation.resume(returning: PatchError.NoFDA(error: error.message()))
} else if !patch_installd() {
continuation.resume(returning: PatchError.FailedPatchd)
} else {
continuation.resume(returning: nil)
}
}
}
if let error = res {
throw error
}
#else
print("The patch would be running right now if you weren't using a simulator. It will stop \"running\" in 3 seconds.")
try await Task.sleep(nanoseconds: UInt64(3 * Double(NSEC_PER_SEC)))
// throw MDC.PatchError.NoFDA(error: "This is a test error")
#endif
UserDefaults.shared.lastInstalldPatchBootTime = bootTime()
UserDefaults.shared.hasPatchedInstalldEver = true
}
static func alertIfNotPatched() {
guard UserDefaults.shared.hasPatchedInstalldEver && !installdHasBeenPatched && isSupported else { return }
UIApplication.alert(
title: L10n.Remove3AppLimitView.title,
message: L10n.Remove3AppLimitView.NotAppliedAlert.message,
leftButton: (text: L10n.Remove3AppLimitView.NotAppliedAlert.apply, action: { _ in
Task {
do {
try await MDC.patch3AppLimit()
await UIApplication.alert(
title: L10n.Remove3AppLimitView.success
)
} catch {
await UIApplication.alert(
title: L10n.AsyncFallibleButton.error,
message: error.message()
)
}
}
}),
rightButton: (text: L10n.Remove3AppLimitView.NotAppliedAlert.continueWithout, action: nil)
)
}
#endif
private static let ios15 = OperatingSystemVersion(majorVersion: 15, minorVersion: 0, patchVersion: 0) // supported
private static let ios15_7_2 = OperatingSystemVersion(majorVersion: 15, minorVersion: 7, patchVersion: 2) // fixed
private static let ios16 = OperatingSystemVersion(majorVersion: 16, minorVersion: 0, patchVersion: 0) // supported
private static let ios16_2 = OperatingSystemVersion(majorVersion: 16, minorVersion: 2, patchVersion: 0) // fixed
static var isSupported: Bool {
#if targetEnvironment(simulator)
true
#else
(ProcessInfo.processInfo.isOperatingSystemAtLeast(ios15) && !ProcessInfo.processInfo.isOperatingSystemAtLeast(ios15_7_2)) ||
(ProcessInfo.processInfo.isOperatingSystemAtLeast(ios16) && !ProcessInfo.processInfo.isOperatingSystemAtLeast(ios16_2))
#endif
}
}
#if MDC
// enum WhitelistPatchResult {
// case success, failure
// }
//
// let blankplist = "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NUWVBFIHBsaXN0IFBVQkxJQyAiLS8vQXBwbGUvL0RURCBQTElTVCAxLjAvL0VOIiAiaHR0cDovL3d3dy5hcHBsZS5jb20vRFREcy9Qcm9wZXJ0eUxpc3QtMS4wLmR0ZCI+CjxwbGlzdCB2ZXJzaW9uPSIxLjAiPgo8ZGljdC8+CjwvcGxpc3Q+Cg=="
//
// func patchWhiteList() {
// overwriteFileData(originPath: "/private/var/db/MobileIdentityData/AuthListBannedUpps.plist", replacementData: try! Data(base64Encoded: blankplist)!)
// overwriteFileData(originPath: "/private/var/db/MobileIdentityData/AuthListBannedCdHashes.plist", replacementData: try! Data(base64Encoded: blankplist)!)
// overwriteFileData(originPath: "/private/var/db/MobileIdentityData/Rejections.plist", replacementData: try! Data(base64Encoded: blankplist)!)
// }
//
// func overwriteFileData(originPath: String, replacementData: Data) -> Bool {
// #if false
// let documentDirectory = FileManager.default.urls(
// for: .documentDirectory,
// in: .userDomainMask
// )[0].path
//
// let pathToRealTarget = originPath
// let originPath = documentDirectory + originPath
// let origData = try! Data(contentsOf: URL(fileURLWithPath: pathToRealTarget))
// try! origData.write(to: URL(fileURLWithPath: originPath))
// #endif
//
// // open and map original font
// let fd = open(originPath, O_RDONLY | O_CLOEXEC)
// if fd == -1 {
// print("Could not open target file")
// return false
// }
// defer { close(fd) }
// // check size of font
// let originalFileSize = lseek(fd, 0, SEEK_END)
// guard originalFileSize >= replacementData.count else {
// print("Original file: \(originalFileSize)")
// print("Replacement file: \(replacementData.count)")
// print("File too big!")
// return false
// }
// lseek(fd, 0, SEEK_SET)
//
// // Map the font we want to overwrite so we can mlock it
// let fileMap = mmap(nil, replacementData.count, PROT_READ, MAP_SHARED, fd, 0)
// if fileMap == MAP_FAILED {
// print("Failed to map")
// return false
// }
// // mlock so the file gets cached in memory
// guard mlock(fileMap, replacementData.count) == 0 else {
// print("Failed to mlock")
// return true
// }
//
// // for every 16k chunk, rewrite
// print(Date())
// for chunkOff in stride(from: 0, to: replacementData.count, by: 0x4000) {
// print(String(format: "%lx", chunkOff))
// let dataChunk = replacementData[chunkOff..<min(replacementData.count, chunkOff + 0x4000)]
// var overwroteOne = false
// for _ in 0..<2 {
// let overwriteSucceeded = dataChunk.withUnsafeBytes { dataChunkBytes in
// unalign_csr(
// fd, Int64(chunkOff), dataChunkBytes.baseAddress, dataChunkBytes.count
// )
// }
// if overwriteSucceeded {
// overwroteOne = true
// print("Successfully overwrote!")
// break
// }
// print("try again?!")
// }
// guard overwroteOne else {
// print("Failed to overwrite")
// return false
// }
// }
// print(Date())
// print("Successfully overwrote!")
// return true
// }
//
// func readFile(path: String) -> String? {
// return (try? String?(String(contentsOfFile: path)) ?? "ERROR: Could not read from file! Are you running in the simulator or not unsandboxed?")
// }
#endif

View File

@@ -0,0 +1,33 @@
//
// MDC+AltStoreCore.swift
// AltStoreCore
//
// Created by naturecodevoid on 5/31/23.
// Copyright © 2023 SideStore. All rights reserved.
//
import Foundation
// Parts of MDC we need in AltStoreCore
// TODO: destroy AltStoreCore
public class MDC {
#if MDC
public static var installdHasBeenPatched: Bool {
guard let lastInstalldPatchBootTime = UserDefaults.shared.lastInstalldPatchBootTime else { return false }
return lastInstalldPatchBootTime == bootTime()
}
#endif
}
#if MDC
public func bootTime() -> Date? {
var tv = timeval()
var tvSize = MemoryLayout<timeval>.size
let err = sysctlbyname("kern.boottime", &tv, &tvSize, nil, 0)
guard err == 0, tvSize == MemoryLayout<timeval>.size else {
return nil
}
return Date(timeIntervalSince1970: Double(tv.tv_sec) + Double(tv.tv_usec) / 1_000_000.0)
}
#endif

View File

@@ -0,0 +1,99 @@
//
// Remove3AppLimitView.swift
// SideStore
//
// Created by naturecodevoid on 5/29/23.
// Copyright © 2023 SideStore. All rights reserved.
//
#if MDC
import SwiftUI
import AltStoreCore
fileprivate extension View {
func common() -> some View {
self
.padding()
.transition(.opacity.animation(.linear))
}
}
struct Remove3AppLimitView: View {
@ObservedObject private var iO = Inject.observer
@State var runningPatch = false
@State private var showErrorAlert = false
@State private var errorAlertMessage = ""
@State private var showSuccessAlert = false
@ViewBuilder
private var notSupported: some View {
Text(L10n.Remove3AppLimitView.notSupported)
}
@ViewBuilder
private var installdHasBeenPatched: some View {
Text(L10n.Remove3AppLimitView.alreadyPatched)
Text(L10n.Remove3AppLimitView.tenAppsInfo)
}
@ViewBuilder
private var applyPatch: some View {
Text(L10n.Remove3AppLimitView.patchInfo)
Text(L10n.Remove3AppLimitView.tenAppsInfo)
}
var body: some View {
VStack {
if !MDC.isSupported {
notSupported.common()
} else {
if MDC.installdHasBeenPatched {
installdHasBeenPatched.common()
} else {
applyPatch.common()
SwiftUI.Button(action: {
Task {
do {
guard !runningPatch else { return }
runningPatch = true
try await MDC.patch3AppLimit()
showSuccessAlert = true
} catch {
errorAlertMessage = error.message()
showErrorAlert = true
}
runningPatch = false
}
}) { Text(L10n.Remove3AppLimitView.applyPatch) }
.buttonStyle(FilledButtonStyle(isLoading: runningPatch, hideLabelOnLoading: false))
.padding()
}
}
Spacer()
}
.alert(isPresented: $showErrorAlert) {
Alert(
title: Text(L10n.AsyncFallibleButton.error),
message: Text(errorAlertMessage)
)
}
.alert(isPresented: $showSuccessAlert) {
Alert(
title: Text(L10n.Action.success),
message: Text(L10n.Remove3AppLimitView.success)
)
}
.navigationTitle(L10n.Remove3AppLimitView.title)
.enableInjection()
}
}
struct Remove3AppLimitView_Previews: PreviewProvider {
static var previews: some View {
Remove3AppLimitView()
}
}
#endif

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