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.
This commit is contained in:
naturecodevoid
2023-05-20 11:31:00 -07:00
parent 093e21799f
commit 3a7cd29b22
246 changed files with 10176 additions and 142 deletions

View File

@@ -0,0 +1,454 @@
//
// AppDetailView.swift
// SideStore
//
// Created by Fabian Thies on 18.11.22.
// Copyright © 2022 Fabian Thies. All rights reserved.
//
import SwiftUI
import AsyncImage
import ExpandableText
import SFSafeSymbols
import AltStoreCore
struct AppDetailView: View {
let storeApp: StoreApp
let byteCountFormatter: ByteCountFormatter = {
let formatter = ByteCountFormatter()
return formatter
}()
@State var scrollOffset: CGFloat = .zero
let maxContentCornerRadius: CGFloat = 24
let headerViewHeight: CGFloat = 140
let permissionColumns = 4
var headerBlurRadius: CGFloat {
min(20, max(0, 20 - (scrollOffset / -150) * 20))
}
var isHeaderViewVisible: Bool {
scrollOffset < headerViewHeight + 12
}
var contentCornerRadius: CGFloat {
max(CGFloat.zero, min(maxContentCornerRadius, maxContentCornerRadius * (1 - self.scrollOffset / self.headerViewHeight)))
}
var canRateApp: Bool {
self.storeApp.installedApp != nil
}
var body: some View {
ObservableScrollView(scrollOffset: $scrollOffset) { proxy in
LazyVStack {
headerView
.frame(height: headerViewHeight)
contentView
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
AppPillButton(app: storeApp)
.disabled(isHeaderViewVisible)
.offset(y: isHeaderViewVisible ? 12 : 0)
.opacity(isHeaderViewVisible ? 0 : 1)
.animation(.easeInOut(duration: 0.2), value: isHeaderViewVisible)
}
ToolbarItemGroup(placement: .principal) {
HStack {
Spacer()
AppIconView(iconUrl: storeApp.iconURL, isSideStore: storeApp.isSideStore, size: 24)
Text(storeApp.name)
.bold()
Spacer()
}
.offset(y: isHeaderViewVisible ? 12 : 0)
.opacity(isHeaderViewVisible ? 0 : 1)
.animation(.easeInOut(duration: 0.2), value: isHeaderViewVisible)
}
}
}
var headerView: some View {
ZStack(alignment: .center) {
GeometryReader { proxy in
AppIconView(iconUrl: storeApp.iconURL, isSideStore: storeApp.isSideStore, size: proxy.frame(in: .global).width)
.blur(radius: headerBlurRadius)
.offset(y: min(0, scrollOffset))
}
.padding()
AppRowView(app: storeApp)
.padding(.horizontal)
}
}
var contentView: some View {
VStack(alignment: .leading, spacing: 24) {
VStack(alignment: .leading, spacing: 32) {
if storeApp.isFromOfficialSource {
officialAppBadge
} else if storeApp.isFromTrustedSource {
trustedAppBadge
}
if let subtitle = storeApp.subtitle {
VStack {
if #available(iOS 15.0, *) {
Image(systemSymbol: .quoteOpening)
.foregroundColor(.secondary.opacity(0.5))
.imageScale(.large)
.transformEffect(CGAffineTransform(a: 1, b: 0, c: -0.3, d: 1, tx: 0, ty: 0))
.frame(maxWidth: .infinity, alignment: .leading)
.offset(x: 30)
}
Text(subtitle)
.bold()
.italic()
.multilineTextAlignment(.center)
.frame(maxWidth: .infinity)
if #available(iOS 15.0, *) {
Image(systemSymbol: .quoteClosing)
.foregroundColor(.secondary.opacity(0.5))
.imageScale(.large)
.transformEffect(CGAffineTransform(a: 1, b: 0, c: -0.3, d: 1, tx: 0, ty: 0))
.frame(maxWidth: .infinity, alignment: .trailing)
.offset(x: -30)
}
}
.padding(.horizontal)
}
if !storeApp.screenshotURLs.isEmpty {
// Equatable: Only reload the view if the screenshots change.
// This prevents unnecessary redraws on scroll.
AppScreenshotsScrollView(urls: storeApp.screenshotURLs)
.equatable()
} else {
VStack() {
Text(L10n.AppDetailView.noScreenshots)
.italic()
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity)
.padding(.horizontal)
}
ExpandableText(text: storeApp.localizedDescription)
.lineLimit(6)
.expandButton(TextSet(text: L10n.AppDetailView.more, font: .callout, color: .accentColor))
.padding(.horizontal)
}
VStack(spacing: 24) {
Divider()
currentVersionView
Divider()
ratingsView
Divider()
permissionsView
Divider()
informationView
if !(storeApp.isFromOfficialSource || storeApp.isFromTrustedSource) {
Divider()
reportButton
}
}
.padding(.horizontal)
}
.padding(.vertical)
.background(
RoundedRectangle(cornerRadius: contentCornerRadius)
.foregroundColor(Color(UIColor.systemBackground))
.shadow(radius: isHeaderViewVisible ? 12 : 0)
)
}
var officialAppBadge: some View {
HintView(backgroundColor: Color(UIColor.secondarySystemBackground)) {
HStack {
Spacer()
Image(systemSymbol: .checkmarkSealFill)
Text(L10n.AppDetailView.Badge.official)
Spacer()
}
.foregroundColor(.accentColor)
}
.padding(.horizontal)
}
var trustedAppBadge: some View {
HintView(backgroundColor: Color(UIColor.secondarySystemBackground)) {
HStack {
Spacer()
Image(systemSymbol: .shieldLefthalfFill)
Text(L10n.AppDetailView.Badge.trusted)
Spacer()
}
.foregroundColor(.accentColor)
}
.padding(.horizontal)
}
var currentVersionView: some View {
VStack(alignment: .leading, spacing: 8) {
VStack {
HStack(alignment: .firstTextBaseline) {
Text(L10n.AppDetailView.whatsNew)
.bold()
.font(.title3)
Spacer()
NavigationLink {
AppVersionHistoryView(storeApp: self.storeApp)
} label: {
Text(L10n.AppDetailView.WhatsNew.versionHistory)
}
}
if let latestVersion = storeApp.latestVersion {
HStack {
Text(L10n.AppDetailView.version(latestVersion.version))
Spacer()
Text(DateFormatterHelper.string(forRelativeDate: latestVersion.date))
}
.font(.callout)
.foregroundColor(.secondary)
}
}
if let versionDescription = storeApp.versionDescription {
ExpandableText(text: versionDescription)
.lineLimit(5)
.expandButton(TextSet(text: L10n.AppDetailView.more, font: .callout, color: .accentColor))
} else {
Text(L10n.AppDetailView.noVersionInformation)
.foregroundColor(.secondary)
}
if true {
SwiftUI.Button {
UIApplication.shared.open(URL(string: "https://github.com/SideStore/SideStore")!) { _ in }
} label: {
HStack {
Text(L10n.AppDetailView.WhatsNew.showOnGithub)
Image(systemSymbol: .arrowUpForwardSquare)
}
}
}
}
}
var ratingsView: some View {
VStack(alignment: .leading, spacing: 8) {
HStack(alignment: .firstTextBaseline) {
Text(L10n.AppDetailView.whatsNew)
.bold()
.font(.title3)
Spacer()
NavigationLink {
AppVersionHistoryView(storeApp: self.storeApp)
} label: {
Text(L10n.AppDetailView.Reviews.seeAll)
}
}
HStack(spacing: 40) {
VStack {
Text("3.0")
.font(.system(size: 48, weight: .bold, design: .rounded))
.opacity(0.8)
Text(L10n.AppDetailView.Reviews.outOf(5))
.bold()
.font(.callout)
.foregroundColor(.secondary)
}
VStack(alignment: .trailing) {
LazyVGrid(columns: [GridItem(.fixed(48), alignment: .trailing), GridItem(.flexible())], spacing: 2) {
ForEach(Array(1...5).reversed(), id: \.self) { rating in
HStack(spacing: 2) {
ForEach(0..<rating) { _ in
Image(systemSymbol: .starFill)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 8)
}
}
ProgressView(value: 0.5)
.frame(maxWidth: .infinity)
.progressViewStyle(LinearProgressViewStyle(tint: .secondary))
}
}
.foregroundColor(.secondary)
.frame(maxWidth: .infinity)
Text(L10n.AppDetailView.Reviews.ratings(5))
.font(.callout)
.foregroundColor(.secondary)
}
}
TabView {
ForEach(0..<5) { i in
HintView(backgroundColor: Color(UIColor.secondarySystemBackground)) {
VStack(alignment: .leading) {
VStack(alignment: .leading, spacing: 4) {
HStack {
Text("Review \(i + 1)")
.bold()
.lineLimit(1)
Spacer()
Text(DateFormatterHelper.string(forRelativeDate: Date().addingTimeInterval(-60*60)))
.foregroundColor(.secondary)
}
RatingStars(rating: 5 - i)
.frame(height: 12)
.foregroundColor(.yellow)
}
ExpandableText(text: "Long review text content here.\nMultiple lines.\nAt least three are shown.\nBut are there more?")
.lineLimit(3)
.expandButton(TextSet(text: L10n.AppDetailView.more, font: .callout, color: .accentColor))
}
.frame(maxWidth: .infinity)
}
.tag(i)
.padding(.horizontal, 16)
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.frame(height: 150)
.padding(.horizontal, -16)
if self.canRateApp {
ModalNavigationLink {
NavigationView {
WriteAppReviewView(storeApp: self.storeApp)
}
} label: {
Label("Write a Review", systemSymbol: .squareAndPencil)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
var permissionsView: some View {
VStack(alignment: .leading, spacing: 8) {
Text(L10n.AppDetailView.permissions)
.bold()
.font(.title3)
if storeApp.permissions.isEmpty {
Text(L10n.AppDetailView.noPermissions)
.font(.callout)
.foregroundColor(.secondary)
} else {
AppPermissionsGrid(permissions: storeApp.permissions)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
var informationData: [(title: String, content: String)] {
var data: [(title: String, content: String)] = [
(L10n.AppDetailView.Information.source, self.storeApp.source?.name ?? ""),
(L10n.AppDetailView.Information.developer, self.storeApp.developerName),
// ("Category", self.storeApp.category),
]
if let latestVersion = self.storeApp.latestVersion {
data += [
(L10n.AppDetailView.Information.size, self.byteCountFormatter.string(fromByteCount: latestVersion.size)),
(L10n.AppDetailView.Information.latestVersion, self.storeApp.latestVersion?.version ?? ""),
]
let iOSVersion = ProcessInfo.processInfo.operatingSystemVersion
let hasCompatibilityInfo = [latestVersion.minOSVersion, latestVersion.maxOSVersion].compactMap({ $0 }).isEmpty == false
var compatibility: String = hasCompatibilityInfo ?
L10n.AppDetailView.Information.compatibilityCompatible :
L10n.AppDetailView.Information.compatibilityUnknown
if let minOSVersion = latestVersion.minOSVersion, ProcessInfo.processInfo.isOperatingSystemAtLeast(minOSVersion) == false {
compatibility = L10n.AppDetailView.Information.compatibilityAtLeast(minOSVersion.stringValue)
}
if let maxOSVersion = latestVersion.maxOSVersion,
(!ProcessInfo.processInfo.isOperatingSystemAtLeast(maxOSVersion) || maxOSVersion.stringValue.compare(iOSVersion.stringValue, options: .numeric) == .orderedSame) {
compatibility = L10n.AppDetailView.Information.compatibilityOrLower(maxOSVersion.stringValue)
}
data.append((L10n.AppDetailView.Information.compatibility, compatibility))
}
return data
}
var informationView: some View {
VStack(alignment: .leading) {
Text(L10n.AppDetailView.information)
.bold()
.font(.title3)
LazyVGrid(columns: [GridItem(.flexible(), alignment: .leading), GridItem(.flexible(), alignment: .trailing)], spacing: 8) {
ForEach(informationData, id: \.title) { title, content in
Text(title)
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
Text(content)
.multilineTextAlignment(.trailing)
}
}
}
}
var reportButton: some View {
SwiftUI.Button {
} label: {
Label("Report this App", systemSymbol: .exclamationmarkBubble)
}
}
}
struct AppDetailView_Previews: PreviewProvider {
static let context = DatabaseManager.shared.viewContext
static let app = StoreApp.makeAltStoreApp(in: context)
static var previews: some View {
NavigationView {
AppDetailView(storeApp: app)
}
}
}

View File

@@ -0,0 +1,56 @@
//
// AppPermissionsGrid.swift
// SideStore
//
// Created by Fabian Thies on 27.11.22.
// Copyright © 2022 SideStore. All rights reserved.
//
import SwiftUI
import SFSafeSymbols
import AltStoreCore
struct AppPermissionsGrid: View {
let permissions: [AppPermission]
let columns = Array(repeating: GridItem(.flexible()), count: 3)
var body: some View {
LazyVGrid(columns: columns) {
ForEach(permissions, id: \.type) { permission in
AppPermissionGridItemView(permission: permission)
}
}
}
}
struct AppPermissionGridItemView: View {
let permission: AppPermission
@State var isPopoverPresented = false
var body: some View {
SwiftUI.Button {
self.isPopoverPresented = true
} label: {
VStack {
Image(uiImage: permission.type.icon?.withRenderingMode(.alwaysTemplate) ?? UIImage(systemSymbol: .questionmark))
.foregroundColor(.primary)
.padding()
.background(Circle().foregroundColor(Color(.secondarySystemBackground)))
Text(permission.type.localizedShortName ?? permission.type.localizedName ?? "")
}
.foregroundColor(.primary)
}
.alert(isPresented: self.$isPopoverPresented) {
Alert(title: Text(L10n.AppPermissionGrid.usageDescription), message: Text(permission.usageDescription))
}
}
}
//struct AppPermissionsGrid_Previews: PreviewProvider {
// static var previews: some View {
// AppPermissionsGrid()
// }
//}

View File

@@ -0,0 +1,71 @@
//
// AppScreenshotsPreview.swift
// SideStore
//
// Created by Fabian Thies on 23.12.22.
// Copyright © 2022 SideStore. All rights reserved.
//
import SwiftUI
import AsyncImage
import AltStoreCore
struct AppScreenshotsPreview: View {
@Environment(\.dismiss)
private var dismiss
let urls: [URL]
let aspectRatio: CGFloat
@State var index: Int
init(urls: [URL], aspectRatio: CGFloat = 9/16, initialIndex: Int = 0) {
self.urls = urls
self.aspectRatio = aspectRatio
self._index = State(initialValue: initialIndex)
}
var body: some View {
TabView(selection: $index) {
ForEach(Array(urls.enumerated()), id: \.offset) { (i, url) in
AppScreenshot(url: url, aspectRatio: aspectRatio)
.padding()
.tag(i)
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.navigationTitle("\(index + 1) of \(self.urls.count)")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
SwiftUI.Button {
self.dismiss()
} label: {
Text(L10n.Action.close)
}
}
}
}
}
extension AppScreenshotsPreview: Equatable {
/// Prevent re-rendering of the view if the parameters didn't change
static func == (lhs: AppScreenshotsPreview, rhs: AppScreenshotsPreview) -> Bool {
lhs.urls == rhs.urls
}
}
struct AppScreenshotsPreview_Previews: PreviewProvider {
static let context = DatabaseManager.shared.viewContext
static let app = StoreApp.makeAltStoreApp(in: context)
static var previews: some View {
Color.clear
.sheet(isPresented: .constant(true)) {
NavigationView {
AppScreenshotsPreview(urls: app.screenshotURLs)
}
}
}
}

View File

@@ -0,0 +1,71 @@
//
// AppScreenshotsScrollView.swift
// SideStore
//
// Created by Fabian Thies on 27.11.22.
// Copyright © 2022 SideStore. All rights reserved.
//
import SwiftUI
import AsyncImage
/// Horizontal ScrollView with an asynchronously loaded image for each screenshot URL
///
/// The struct inherits the `Equatable` protocol and implements the respective comparisation function to prevent the view from being constantly re-rendered when a `@State` change in the parent view occurs.
/// This way, the `AppScreenshotsScrollView` will only be reloaded when the parameters change.
struct AppScreenshotsScrollView: View {
let urls: [URL]
var aspectRatio: CGFloat = 9/16
var height: CGFloat = 400
@State var selectedScreenshotIndex: Int?
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(Array(urls.enumerated()), id: \.offset) { i, url in
SwiftUI.Button {
self.selectedScreenshotIndex = i
} label: {
AppScreenshot(url: url)
}
}
}
.padding(.horizontal)
}
.frame(height: height)
.shadow(radius: 12)
.sheet(item: self.$selectedScreenshotIndex) { index in
NavigationView {
AppScreenshotsPreview(urls: urls, aspectRatio: aspectRatio, initialIndex: index)
}
}
}
}
extension AppScreenshotsScrollView: Equatable {
/// Prevent re-rendering of the view if the parameters didn't change
static func == (lhs: AppScreenshotsScrollView, rhs: AppScreenshotsScrollView) -> Bool {
lhs.urls == rhs.urls && lhs.aspectRatio == rhs.aspectRatio && lhs.height == rhs.height
}
}
extension Int: Identifiable {
public var id: Int {
self
}
}
import AltStoreCore
struct AppScreenshotsScrollView_Previews: PreviewProvider {
static let context = DatabaseManager.shared.viewContext
static let app = StoreApp.makeAltStoreApp(in: context)
static var previews: some View {
AppScreenshotsScrollView(urls: app.screenshotURLs)
}
}

View File

@@ -0,0 +1,55 @@
//
// AppVersionHistoryView.swift
// SideStore
//
// Created by Fabian Thies on 28.01.23.
// Copyright © 2023 SideStore. All rights reserved.
//
import SwiftUI
import AltStoreCore
import ExpandableText
struct AppVersionHistoryView: View {
let storeApp: StoreApp
var body: some View {
List {
ForEach(storeApp.versions.sorted(by: { $0.date < $1.date }), id: \.version) { version in
VStack(spacing: 8) {
HStack {
Text(version.version).bold()
Spacer()
Text(DateFormatterHelper.string(forRelativeDate: version.date))
.foregroundColor(.secondary)
}
if let versionDescription = version.localizedDescription {
ExpandableText(text: versionDescription)
.lineLimit(3)
.expandButton(TextSet(text: L10n.AppDetailView.more, font: .callout, color: .accentColor))
.buttonStyle(.plain)
} else {
Text("No version desciption available")
.italic()
.foregroundColor(.secondary)
}
}
}
}
.listStyle(PlainListStyle())
.navigationTitle("Version History")
}
}
struct AppVersionHistoryView_Previews: PreviewProvider {
static let context = DatabaseManager.shared.viewContext
static let app = StoreApp.makeAltStoreApp(in: context)
static var previews: some View {
NavigationView {
AppVersionHistoryView(storeApp: app)
}
}
}

View File

@@ -0,0 +1,102 @@
//
// WriteAppReviewView.swift
// SideStore
//
// Created by Fabian Thies on 19.02.23.
// Copyright © 2023 SideStore. All rights reserved.
//
import SwiftUI
import AltStoreCore
struct WriteAppReviewView: View {
@Environment(\.dismiss) var dismiss
let storeApp: StoreApp
@State var currentRating = 0
@State var reviewText = ""
var canSendReview: Bool {
// Only allow the user to send the review if a rating has been set and
// the review text is either empty or doesn't contain only whitespaces.
self.currentRating > 0 && (
self.reviewText.isEmpty || !self.reviewText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
)
}
var body: some View {
List {
// App Information
HStack {
AppIconView(iconUrl: storeApp.iconURL, isSideStore: storeApp.isSideStore, size: 50)
VStack(alignment: .leading) {
Text(storeApp.name)
.bold()
Text(storeApp.developerName)
.font(.callout)
.foregroundColor(.secondary)
}
}
// Rating
Section {
HStack {
Spacer()
ForEach(1...5) { rating in
SwiftUI.Button {
self.currentRating = rating
} label: {
Image(systemSymbol: rating > self.currentRating ? .star : .starFill)
.resizable()
.aspectRatio(contentMode: .fit)
}
.buttonStyle(PlainButtonStyle())
.frame(maxHeight: 40)
}
Spacer()
}
.foregroundColor(.yellow)
} header: {
Text("Rate the App")
}
// Review
Section {
TextEditor(text: self.$reviewText)
.frame(minHeight: 100, maxHeight: 250)
} header: {
Text("Leave a Review (optional)")
}
}
.navigationTitle("Write a Review")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
SwiftUI.Button("Cancel", action: self.dismiss)
}
ToolbarItem(placement: .confirmationAction) {
SwiftUI.Button("Send", action: self.sendReview)
.disabled(!self.canSendReview)
}
}
}
private func sendReview() {
NotificationManager.shared.showNotification(title: "Feature not Implemented")
self.dismiss()
}
}
struct WriteAppReviewView_Previews: PreviewProvider {
static let context = DatabaseManager.shared.viewContext
static let app = StoreApp.makeAltStoreApp(in: context)
static var previews: some View {
NavigationView {
WriteAppReviewView(storeApp: app)
}
}
}

View File

@@ -0,0 +1,56 @@
//
// AddSourceView.swift
// SideStore
//
// Created by Fabian Thies on 20.11.22.
// Copyright © 2022 Fabian Thies. All rights reserved.
//
import SwiftUI
import SFSafeSymbols
struct AddSourceView: View {
@State var sourceUrlText: String = ""
var continueHandler: (_ urlText: String) -> ()
var body: some View {
List {
Section {
TextField("https://connect.altstore.ml", text: $sourceUrlText)
.keyboardType(.URL)
.autocapitalization(.none)
.autocorrectionDisabled()
} header: {
Text(L10n.AddSourceView.sourceURL)
} footer: {
VStack(alignment: .leading, spacing: 4) {
Text(L10n.AddSourceView.sourceWarning)
HStack(alignment: .top) {
Image(systemSymbol: .exclamationmarkTriangleFill)
Text(L10n.AddSourceView.sourceWarningContinued)
}
}
}
SwiftUI.Button {
self.continueHandler(self.sourceUrlText)
} label: {
Text(L10n.AddSourceView.continue)
}
.disabled(URL(string: self.sourceUrlText)?.host == nil)
}
.listStyle(InsetGroupedListStyle())
.navigationTitle(L10n.AddSourceView.title)
.navigationBarTitleDisplayMode(.inline)
}
}
struct AddSourceView_Previews: PreviewProvider {
static var previews: some View {
AddSourceView(continueHandler: { _ in })
}
}

View File

@@ -0,0 +1,42 @@
//
// BrowseAppPreviewView.swift
// SideStore
//
// Created by Fabian Thies on 20.11.22.
// Copyright © 2022 Fabian Thies. All rights reserved.
//
import SwiftUI
import AsyncImage
import AltStoreCore
struct BrowseAppPreviewView: View {
let storeApp: StoreApp
var body: some View {
VStack(spacing: 16) {
AppRowView(app: storeApp)
if let subtitle = storeApp.subtitle {
Text(subtitle)
.multilineTextAlignment(.center)
}
if !storeApp.screenshotURLs.isEmpty {
HStack {
ForEach(storeApp.screenshotURLs.prefix(2)) { url in
AppScreenshot(url: url)
}
}
.frame(height: 300)
.shadow(radius: 8)
}
}
}
}
//struct BrowseAppPreviewView_Previews: PreviewProvider {
// static var previews: some View {
// BrowseAppPreviewView()
// }
//}

View File

@@ -0,0 +1,165 @@
//
// BrowseView.swift
// SideStoreUI
//
// Created by Fabian Thies on 18.11.22.
// Copyright © 2022 Fabian Thies. All rights reserved.
//
import SwiftUI
import SFSafeSymbols
import AltStoreCore
struct BrowseView: View {
@SwiftUI.FetchRequest(sortDescriptors: [
NSSortDescriptor(keyPath: \StoreApp.sourceIdentifier, ascending: true),
NSSortDescriptor(keyPath: \StoreApp.sortIndex, ascending: true),
NSSortDescriptor(keyPath: \StoreApp.name, ascending: true),
NSSortDescriptor(keyPath: \StoreApp.bundleIdentifier, ascending: true)
], predicate: NSPredicate(format: "%K != %@", #keyPath(StoreApp.bundleIdentifier), StoreApp.altstoreAppID))
var apps: FetchedResults<StoreApp>
var filteredApps: [StoreApp] {
apps.items(matching: self.searchText)
}
@State
var selectedStoreApp: StoreApp?
@State var searchText = ""
@State var isShowingSourcesView = false
var body: some View {
ScrollView {
VStack(alignment: .leading) {
if searchText.isEmpty {
VStack(alignment: .leading, spacing: 32) {
promotedCategoriesView
Text(L10n.BrowseView.Section.AllApps.title)
.font(.title2)
.bold()
}
}
if searchText.isEmpty, filteredApps.count == 0 {
HintView {
Text(L10n.BrowseView.Hints.NoApps.title)
.bold()
Text(L10n.BrowseView.Hints.NoApps.text)
.font(.callout)
.foregroundColor(.secondary)
SwiftUI.Button {
self.isShowingSourcesView = true
} label: {
Label(L10n.BrowseView.Hints.NoApps.addSource, systemSymbol: .plus)
}
.buttonStyle(FilledButtonStyle())
.padding(.top, 8)
}
} else {
LazyVStack(spacing: 32) {
ForEach(filteredApps, id: \.bundleIdentifier) { app in
NavigationLink {
AppDetailView(storeApp: app)
} label: {
BrowseAppPreviewView(storeApp: app)
}
.buttonStyle(PlainButtonStyle())
}
}
}
}
.padding()
.searchable(text: self.$searchText, placeholder: L10n.BrowseView.search)
}
.background(Color(UIColor.systemGroupedBackground).ignoresSafeArea())
.navigationTitle(L10n.BrowseView.title)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
SwiftUI.Button {
self.isShowingSourcesView = true
} label: {
Text(L10n.BrowseView.Actions.sources)
}
.sheet(isPresented: self.$isShowingSourcesView) {
NavigationView {
SourcesView()
}
}
}
ToolbarItem(placement: .navigationBarTrailing) {
SwiftUI.Button {
} label: {
Image(systemSymbol: .lineHorizontal3DecreaseCircle)
.imageScale(.large)
}
}
}
}
var promotedCategoriesView: some View {
VStack {
HStack {
Text(L10n.BrowseView.Section.PromotedCategories.title)
.font(.title2)
.bold()
Spacer()
SwiftUI.Button(action: {}, label: {
Text(L10n.BrowseView.Section.PromotedCategories.showAll)
})
.font(.callout)
}
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())]) {
PromotedCategoryView()
.shadow(color: .black.opacity(0.1), radius: 8, y: 5)
PromotedCategoryView()
.shadow(color: .black.opacity(0.1), radius: 8, y: 5)
}
}
}
}
struct BrowseView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
BrowseView()
}
}
}
struct PromotedCategoryView: View {
var body: some View {
ZStack {
GeometryReader { proxy in
RadialGradient(colors: [
Color(UIColor(hexString: "477E84")!),
Color(UIColor.secondarySystemBackground),
Color(UIColor.secondarySystemBackground),
Color(UIColor(hexString: "C38FF5")!)
], center: .bottomLeading, startRadius: 0, endRadius: proxy.size.width)
}
HStack {
Image(systemSymbol: .dpadRightFill)
Text(L10n.BrowseView.Categories.gamesAndEmulators)
.multilineTextAlignment(.leading)
}
.foregroundColor(.accentColor)
.padding()
}
.aspectRatio(21/9, contentMode: .fill)
.frame(maxWidth: .infinity)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}

View File

@@ -0,0 +1,105 @@
//
// ConfirmAddSourceView.swift
// SideStore
//
// Created by Fabian Thies on 18.11.22.
// Copyright © 2022 Fabian Thies. All rights reserved.
//
import SwiftUI
import SFSafeSymbols
import AltStoreCore
struct ConfirmAddSourceView: View {
let fetchedSource: FetchedSource
var source: Source {
fetchedSource.source
}
var confirmationHandler: (_ source: FetchedSource) -> ()
var cancellationHandler: () -> ()
var body: some View {
VStack(alignment: .leading) {
List {
Section {
VStack(alignment: .leading) {
Text("\(source.apps.count) \(L10n.ConfirmAddSourceView.apps)")
Text(source.apps.map { $0.name }.joined(separator: ", "))
.font(.callout)
.lineLimit(1)
.foregroundColor(.secondary)
}
VStack() {
Text("\(source.newsItems.count) \(L10n.ConfirmAddSourceView.newsItems)")
}
} header: {
Text(L10n.ConfirmAddSourceView.sourceContents)
}
Section {
VStack(alignment: .leading) {
Text(L10n.ConfirmAddSourceView.sourceIdentifier)
Text(source.identifier)
.font(.callout)
.foregroundColor(.secondary)
}
VStack(alignment: .leading) {
Text(L10n.ConfirmAddSourceView.sourceURL)
Text(source.sourceURL.absoluteString)
.font(.callout)
.foregroundColor(.secondary)
}
} header: {
Text(L10n.ConfirmAddSourceView.sourceInfo)
}
}
.listStyle(InsetGroupedListStyle())
Spacer()
SwiftUI.Button {
confirmationHandler(fetchedSource)
} label: {
Label(L10n.ConfirmAddSourceView.addSource, systemSymbol: .plus)
}
.buttonStyle(FilledButtonStyle())
.padding()
}
.background(Color(UIColor.systemGroupedBackground).ignoresSafeArea())
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
SwiftUI.Button {
} label: {
Image(systemSymbol: .xmarkCircleFill)
.foregroundColor(.secondary)
}
}
ToolbarItemGroup(placement: .navigation) {
VStack(alignment: .leading) {
Text(source.name)
.font(.title3)
.bold()
Text(source.identifier)
.font(.callout)
.foregroundColor(.secondary)
}
}
}
.navigationBarTitleDisplayMode(.inline)
}
}
struct ConfirmAddSourceView_Previews: PreviewProvider {
static var previews: some View {
AddSourceView(continueHandler: { _ in })
}
}

View File

@@ -0,0 +1,299 @@
//
// SourcesView.swift
// SideStore
//
// Created by Fabian Thies on 20.11.22.
// Copyright © 2022 Fabian Thies. All rights reserved.
//
import SwiftUI
import SFSafeSymbols
import AltStoreCore
import CoreData
struct SourcesView: View {
@Environment(\.dismiss)
private var dismiss
@Environment(\.managedObjectContext)
var managedObjectContext
@SwiftUI.FetchRequest(sortDescriptors: [
NSSortDescriptor(keyPath: \Source.name, ascending: true),
NSSortDescriptor(keyPath: \Source.sourceURL, ascending: true),
NSSortDescriptor(keyPath: \Source.identifier, ascending: true)
])
var installedSources: FetchedResults<Source>
@State var trustedSources: [Source] = []
@State private var isLoadingTrustedSources: Bool = false
@State private var sourcesFetchContext: NSManagedObjectContext?
@State var isShowingAddSourceAlert = false
@State var sourceToConfirm: FetchedSource?
var body: some View {
ScrollView {
LazyVStack(alignment: .leading, spacing: 24) {
// Installed Sources
LazyVStack(alignment: .leading, spacing: 12) {
Text(L10n.SourcesView.sourcesDescription)
.font(.callout)
.foregroundColor(.secondary)
ForEach(installedSources, id: \.identifier) { source in
VStack(alignment: .leading) {
Text(source.name)
.bold()
Text(source.sourceURL.absoluteString)
.font(.callout)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.tintedBackground(.accentColor)
.clipShape(RoundedRectangle(cornerRadius: 24, style: .circular))
.if(source.identifier != Source.altStoreIdentifier) { view in
view.contextMenu(ContextMenu(menuItems: {
SwiftUI.Button {
self.removeSource(source)
} label: {
Label(L10n.SourcesView.remove, systemSymbol: .trash)
}
}))
}
}
}
// Trusted Sources
LazyVStack(alignment: .leading, spacing: 16) {
HStack(spacing: 4) {
Text(L10n.SourcesView.trustedSources)
.font(.title3)
.bold()
Image(systemSymbol: .shieldLefthalfFill)
.foregroundColor(.accentColor)
}
Text(L10n.SourcesView.reviewedText)
.font(.callout)
.foregroundColor(.secondary)
if self.isLoadingTrustedSources {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
.frame(maxWidth: .infinity)
} else {
ForEach(self.trustedSources, id: \.sourceURL) { source in
HStack {
VStack(alignment: .leading) {
Text(source.name)
.bold()
Text(source.sourceURL.absoluteString)
.font(.callout)
.foregroundColor(.secondary)
}
Spacer()
if self.installedSources.contains(where: { $0.sourceURL == source.sourceURL }) {
Image(systemSymbol: .checkmarkCircle)
.foregroundColor(.accentColor)
} else {
SwiftUI.Button {
self.fetchSource(with: source.sourceURL.absoluteString)
} label: {
Text("ADD")
.bold()
}
.buttonStyle(PillButtonStyle(tintColor: Asset.accentColor.color, progress: nil))
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.tintedBackground(.accentColor)
.clipShape(RoundedRectangle(cornerRadius: 24, style: .circular))
}
}
}
}
.padding()
}
.navigationTitle(L10n.SourcesView.sources)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
SwiftUI.Button {
self.isShowingAddSourceAlert = true
} label: {
Image(systemSymbol: .plus)
}
.sheet(isPresented: self.$isShowingAddSourceAlert) {
NavigationView {
AddSourceView(continueHandler: fetchSource(with:))
}
}
.sheet(item: self.$sourceToConfirm) { source in
if #available(iOS 16.0, *) {
NavigationView {
ConfirmAddSourceView(fetchedSource: source, confirmationHandler: addSource(_:)) {
self.sourceToConfirm = nil
}
}
.presentationDetents([.medium])
}
}
}
ToolbarItem(placement: .navigationBarTrailing) {
SwiftUI.Button(action: self.dismiss) {
Text(L10n.SourcesView.done).bold()
}
}
}
.onAppear(perform: self.fetchTrustedSources)
}
func fetchSource(with urlText: String) {
self.isShowingAddSourceAlert = false
guard let url = URL(string: urlText) else {
return
}
let context = DatabaseManager.shared.persistentContainer.newBackgroundSavingViewContext()
AppManager.shared.fetchSource(sourceURL: url, managedObjectContext: context) { result in
switch result {
case let .success(source):
self.sourceToConfirm = FetchedSource(source: source, context: context)
case let .failure(error):
print(error)
}
}
}
func addSource(_ source: FetchedSource) {
source.context.perform {
do {
try source.context.save()
} catch {
print(error)
NotificationManager.shared.reportError(error: error)
}
}
self.sourceToConfirm = nil
}
func removeSource(_ source: Source) {
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
let source = context.object(with: source.objectID) as! Source
context.delete(source)
do {
try context.save()
} catch {
print(error)
}
}
}
func fetchTrustedSources() {
self.isLoadingTrustedSources = true
AppManager.shared.fetchTrustedSources { result in
switch result {
case .success(let trustedSources):
// Cache trusted source IDs.
UserDefaults.shared.trustedSourceIDs = trustedSources.map { $0.identifier }
// Don't show sources without a sourceURL.
let featuredSourceURLs = trustedSources.compactMap { $0.sourceURL }
// This context is never saved, but keeps the managed sources alive.
let context = DatabaseManager.shared.persistentContainer.newBackgroundSavingViewContext()
self.sourcesFetchContext = context
let dispatchGroup = DispatchGroup()
var sourcesByURL = [URL: Source]()
var errors: [(error: Error, sourceURL: URL)] = []
for sourceURL in featuredSourceURLs {
dispatchGroup.enter()
AppManager.shared.fetchSource(sourceURL: sourceURL, managedObjectContext: context) { result in
defer {
dispatchGroup.leave()
}
// Serialize access to sourcesByURL.
context.performAndWait {
switch result
{
case .failure(let error): errors.append((error, sourceURL))
case .success(let source): sourcesByURL[source.sourceURL] = source
}
}
}
}
dispatchGroup.notify(queue: .main) {
if let (error, _) = errors.first {
NotificationManager.shared.reportError(error: error)
}
let sources = featuredSourceURLs.compactMap { sourcesByURL[$0] }
self.trustedSources = sources
self.isLoadingTrustedSources = false
}
case .failure(let error):
NotificationManager.shared.reportError(error: error)
self.isLoadingTrustedSources = false
}
}
}
}
struct SourcesView_Previews: PreviewProvider {
static var previews: some View {
Color.clear
.sheet(isPresented: .constant(true)) {
NavigationView {
SourcesView()
}
}
}
}
extension Source: Identifiable {
public var id: String {
self.identifier
}
}
struct FetchedSource: Identifiable {
let source: Source
let context: NSManagedObjectContext
var id: String {
source.identifier
}
init(source: Source, context: NSManagedObjectContext) {
self.source = source
self.context = context
}
}

View File

@@ -0,0 +1,54 @@
//
// AppAction.swift
// SideStore
//
// Created by Fabian Thies on 20.12.22.
// Copyright © 2022 SideStore. All rights reserved.
//
import Foundation
import SFSafeSymbols
enum AppAction: Int, CaseIterable {
case install, open, refresh
case activate, deactivate
case remove
case enableJIT
case backup, exportBackup, restoreBackup
case chooseCustomIcon, resetCustomIcon
var title: String {
switch self {
case .install: return L10n.AppAction.install
case .open: return L10n.AppAction.open
case .refresh: return L10n.AppAction.refresh
case .activate: return L10n.AppAction.activate
case .deactivate: return L10n.AppAction.deactivate
case .remove: return L10n.AppAction.remove
case .enableJIT: return L10n.AppAction.enableJIT
case .backup: return L10n.AppAction.backup
case .exportBackup: return L10n.AppAction.exportBackup
case .restoreBackup: return L10n.AppAction.restoreBackup
case .chooseCustomIcon: return L10n.AppAction.chooseCustomIcon
case .resetCustomIcon: return L10n.AppAction.resetIcon
}
}
var symbol: SFSymbol {
switch self {
case .install: return .squareAndArrowDown
case .open: return .arrowUpForwardApp
case .refresh: return .arrowClockwise
case .activate: return .checkmarkCircle
case .deactivate: return .xmarkCircle
case .remove: return .trash
case .enableJIT: return .bolt
case .backup: return .docOnDoc
case .exportBackup: return .arrowUpDoc
case .restoreBackup: return .arrowDownDoc
case .chooseCustomIcon: return .photo
case .resetCustomIcon: return .arrowUturnLeft
}
}
}

View File

@@ -0,0 +1,119 @@
//
// AppIDsView.swift
// SideStore
//
// Created by Fabian Thies on 23.12.22.
// Copyright © 2022 SideStore. All rights reserved.
//
import SwiftUI
import AltStoreCore
struct AppIDsView: View {
@Environment(\.dismiss) var dismiss
@SwiftUI.FetchRequest(sortDescriptors: [
NSSortDescriptor(keyPath: \AppID.name, ascending: true),
NSSortDescriptor(keyPath: \AppID.bundleIdentifier, ascending: true),
NSSortDescriptor(keyPath: \AppID.expirationDate, ascending: true)
], predicate: NSPredicate(format: "%K == %@", #keyPath(AppID.team), DatabaseManager.shared.activeTeam() ?? Team()))
var appIDs: FetchedResults<AppID>
@State var isLoading: Bool = false
var body: some View {
ScrollView {
LazyVStack(alignment: .leading) {
Text(L10n.AppIDsView.description)
.foregroundColor(.secondary)
ForEach(appIDs, id: \.identifier) { appId in
HStack {
VStack(alignment: .leading) {
Text(appId.name)
.bold()
Text(appId.bundleIdentifier)
.font(.footnote)
.foregroundColor(.secondary)
}
Spacer()
if let expirationDate = appId.expirationDate {
VStack(spacing: 4) {
Text("Expires in")
.font(.caption)
.foregroundColor(.accentColor)
SwiftUI.Button {
} label: {
Text(DateFormatterHelper.string(forExpirationDate: expirationDate).uppercased())
.bold()
}
.buttonStyle(PillButtonStyle(tintColor: .altPrimary))
.disabled(true)
}
}
}
.padding()
.tintedBackground(.accentColor)
.clipShape(RoundedRectangle(cornerRadius: 24, style: .circular))
}
}
.padding()
}
.navigationTitle(L10n.AppIDsView.title)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
if self.isLoading {
ProgressView()
.progressViewStyle(.circular)
}
}
ToolbarItem(placement: .confirmationAction) {
SwiftUI.Button(L10n.Action.done, action: self.dismiss)
}
}
.onAppear(performAsync: self.updateAppIDs)
}
func updateAppIDs() async {
self.isLoading = true
defer { self.isLoading = false }
await withCheckedContinuation { continuation in
AppManager.shared.fetchAppIDs { result in
do {
let (_, context) = try result.get()
try context.save()
} catch {
print(error)
NotificationManager.shared.reportError(error: error)
}
continuation.resume()
}
}
}
}
extension View {
func onAppear(performAsync task: @escaping () async -> Void) -> some View {
self.onAppear(perform: { Task { await task() } })
}
}
struct AppIDsView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
AppIDsView()
}
}
}

View File

@@ -0,0 +1,437 @@
//
// MyAppsView.swift
// SideStoreUI
//
// Created by Fabian Thies on 18.11.22.
// Copyright © 2022 Fabian Thies. All rights reserved.
//
import SwiftUI
import SFSafeSymbols
import MobileCoreServices
import AltStoreCore
struct MyAppsView: View {
// TODO: Refactor
@SwiftUI.FetchRequest(sortDescriptors: [
NSSortDescriptor(keyPath: \InstalledApp.storeApp?.latestVersion?.date, ascending: true),
NSSortDescriptor(keyPath: \InstalledApp.name, ascending: true)
], predicate: NSPredicate(format: "%K == YES AND %K != nil AND %K != %K",
#keyPath(InstalledApp.isActive), #keyPath(InstalledApp.storeApp),
#keyPath(InstalledApp.version), #keyPath(InstalledApp.storeApp.latestVersion.version))
)
var updates: FetchedResults<InstalledApp>
@SwiftUI.FetchRequest(sortDescriptors: [
NSSortDescriptor(keyPath: \InstalledApp.expirationDate, ascending: true),
NSSortDescriptor(keyPath: \InstalledApp.refreshedDate, ascending: false),
NSSortDescriptor(keyPath: \InstalledApp.name, ascending: true)
], predicate: NSPredicate(format: "%K == YES", #keyPath(InstalledApp.isActive)))
var activeApps: FetchedResults<InstalledApp>
@AppStorage("shouldShowAppUpdateHint")
var shouldShowAppUpdateHint: Bool = true
@ObservedObject
var viewModel = MyAppsViewModel()
// TODO: Refactor
@State var isRefreshingAllApps: Bool = false
@State var selectedSideloadingIpaURL: URL?
var remainingAppIDs: Int {
guard let team = DatabaseManager.shared.activeTeam() else {
return 0
}
let maximumAppIDCount = 10
return max(maximumAppIDCount - team.appIDs.count, 0)
}
// TODO: Refactor
let sideloadFileTypes: [String] = {
if let types = UTTypeCreateAllIdentifiersForTag(kUTTagClassFilenameExtension, "ipa" as CFString, nil)?.takeRetainedValue()
{
return (types as NSArray).map { $0 as! String }
}
else
{
return ["com.apple.itunes.ipa"] // Declared by the system.
}
}()
var body: some View {
ScrollView {
LazyVStack(spacing: 16) {
if let progress = SideloadingManager.shared.progress {
VStack {
Text(L10n.MyAppsView.sideloading)
.padding()
ProgressView(progress)
.progressViewStyle(LinearProgressViewStyle())
}
.background(Color(UIColor.secondarySystemBackground))
}
if updates.isEmpty {
if shouldShowAppUpdateHint {
updatesSection
}
}
HStack {
Text(L10n.MyAppsView.active)
.font(.title2)
.bold()
Spacer()
if !self.isRefreshingAllApps {
SwiftUI.Button(L10n.MyAppsView.refreshAll, action: self.refreshAllApps)
} else {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
}
}
ForEach(activeApps, id: \.bundleIdentifier) { app in
if let storeApp = app.storeApp {
NavigationLink {
AppDetailView(storeApp: storeApp)
} label: {
self.rowView(for: app)
}
.buttonStyle(PlainButtonStyle())
} else {
self.rowView(for: app)
}
}
if let activeTeam = DatabaseManager.shared.activeTeam() {
VStack {
if activeTeam.type == .free {
Text("\(remainingAppIDs) \(L10n.MyAppsView.appIDsRemaining)")
.foregroundColor(.secondary)
}
ModalNavigationLink(L10n.MyAppsView.viewAppIDs) {
NavigationView {
AppIDsView()
}
}
}
}
}
.padding(.horizontal)
}
.background(Color(UIColor.systemGroupedBackground).ignoresSafeArea())
.navigationTitle(L10n.MyAppsView.myApps)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
ModalNavigationLink {
DocumentPicker(selectedUrl: $selectedSideloadingIpaURL, supportedTypes: sideloadFileTypes)
.ignoresSafeArea()
} label: {
Image(systemSymbol: .plus)
.imageScale(.large)
}
.onChange(of: self.selectedSideloadingIpaURL) { newValue in
guard let url = newValue else {
return
}
self.sideloadApp(at: url)
}
}
}
}
var updatesSection: some View {
HintView {
HStack(alignment: .center) {
Text(L10n.MyAppsView.Hints.NoUpdates.title)
.bold()
Spacer()
Menu {
SwiftUI.Button {
self.dismissUpdatesHint(forever: false)
} label: {
Label(L10n.MyAppsView.Hints.NoUpdates.dismissForNow, systemSymbol: .zzz)
}
SwiftUI.Button {
self.dismissUpdatesHint(forever: true)
} label: {
Label(L10n.MyAppsView.Hints.NoUpdates.dontShowAgain, systemSymbol: .xmark)
}
} label: {
Image(systemSymbol: .xmark)
}
.foregroundColor(.secondary)
}
Text(L10n.MyAppsView.Hints.NoUpdates.text)
.font(.callout)
.foregroundColor(.secondary)
}
}
@ViewBuilder
func rowView(for app: AppProtocol) -> some View {
AppRowView(app: app, showRemainingDays: true)
.contextMenu(ContextMenu(menuItems: {
ForEach(self.actions(for: app), id: \.self) { action in
SwiftUI.Button {
self.perform(action: action, for: app)
} label: {
Label(action.title, systemSymbol: action.symbol)
}
}
}))
}
func refreshAllApps() {
let installedApps = InstalledApp.fetchAppsForRefreshingAll(in: DatabaseManager.shared.viewContext)
self.isRefreshingAllApps = true
self.refresh(installedApps) { result in
self.isRefreshingAllApps = false
}
}
func dismissUpdatesHint(forever: Bool) {
withAnimation {
self.shouldShowAppUpdateHint = false
}
}
}
extension MyAppsView {
// TODO: Convert to async?
func refresh(_ apps: [InstalledApp], completionHandler: @escaping ([String : Result<InstalledApp, Error>]) -> Void) {
let group = AppManager.shared.refresh(apps, presentingViewController: nil, group: self.viewModel.refreshGroup)
group.completionHandler = { results in
DispatchQueue.main.async {
let failures = results.compactMapValues { result -> Error? in
switch result {
case .failure(OperationError.cancelled):
return nil
case .failure(let error):
return error
case .success:
return nil
}
}
guard !failures.isEmpty else { return }
if let failure = failures.first, results.count == 1 {
NotificationManager.shared.reportError(error: failure.value)
} else {
// TODO: Localize
let title = "\(L10n.MyAppsView.failedToRefresh) \(failures.count) \(L10n.MyAppsView.apps)"
let error = failures.first?.value as NSError?
let message = error?.localizedFailure ?? error?.localizedFailureReason ?? error?.localizedDescription
NotificationManager.shared.showNotification(title: title, detailText: message)
}
self.viewModel.refreshGroup = nil
completionHandler(results)
}
}
self.viewModel.refreshGroup = group
}
}
extension MyAppsView {
func actions(for app: AppProtocol) -> [AppAction] {
guard let installedApp = app as? InstalledApp else {
return []
}
guard installedApp.bundleIdentifier != StoreApp.altstoreAppID else {
return [.refresh]
}
var actions: [AppAction] = []
if installedApp.isActive {
actions.append(.open)
actions.append(.refresh)
actions.append(.enableJIT)
} else {
actions.append(.activate)
}
actions.append(.chooseCustomIcon)
if installedApp.hasAlternateIcon {
actions.append(.resetCustomIcon)
}
if installedApp.isActive {
actions.append(.backup)
} else if let _ = UTTypeCopyDeclaration(installedApp.installedAppUTI as CFString)?.takeRetainedValue() as NSDictionary?, !UserDefaults.standard.isLegacyDeactivationSupported {
// Allow backing up inactive apps if they are still installed,
// but on an iOS version that no longer supports legacy deactivation.
// This handles edge case where you can't install more apps until you
// delete some, but can't activate inactive apps again to back them up first.
actions.append(.backup)
}
if let backupDirectoryURL = FileManager.default.backupDirectoryURL(for: installedApp) {
// TODO: Refactor
var backupExists = false
var outError: NSError? = nil
let coordinator = NSFileCoordinator()
coordinator.coordinate(readingItemAt: backupDirectoryURL, options: [.withoutChanges], error: &outError) { (backupDirectoryURL) in
backupExists = FileManager.default.fileExists(atPath: backupDirectoryURL.path)
}
if backupExists {
actions.append(.exportBackup)
if installedApp.isActive {
actions.append(.restoreBackup)
}
} else if let error = outError {
print("Unable to check if backup exists:", error)
}
}
if installedApp.isActive {
actions.append(.deactivate)
}
if installedApp.bundleIdentifier != StoreApp.altstoreAppID {
actions.append(.remove)
}
return actions
}
func perform(action: AppAction, for app: AppProtocol) {
guard let installedApp = app as? InstalledApp else {
// Invalid state.
return
}
switch action {
case .install: break
case .open: self.open(installedApp)
case .refresh: self.refresh(installedApp)
case .activate: self.activate(installedApp)
case .deactivate: self.deactivate(installedApp)
case .remove: self.remove(installedApp)
case .enableJIT: self.enableJIT(for: installedApp)
case .backup: self.backup(installedApp)
case .exportBackup: self.exportBackup(installedApp)
case .restoreBackup: self.restoreBackup(installedApp)
case .chooseCustomIcon: self.chooseIcon(for: installedApp)
case .resetCustomIcon: self.resetIcon(for: installedApp)
}
}
func open(_ app: InstalledApp) {
UIApplication.shared.open(app.openAppURL) { success in
guard !success else { return }
NotificationManager.shared.reportError(error: OperationError.openAppFailed(name: app.name))
}
}
func refresh(_ app: InstalledApp) {
let previousProgress = AppManager.shared.refreshProgress(for: app)
guard previousProgress == nil else {
previousProgress?.cancel()
return
}
self.refresh([app]) { (results) in
print("Finished refreshing with results:", results.map { ($0, $1.error?.localizedDescription ?? "success") })
}
}
func activate(_ app: InstalledApp) {
}
func deactivate(_ app: InstalledApp) {
}
func remove(_ app: InstalledApp) {
}
func enableJIT(for app: InstalledApp) {
AppManager.shared.enableJIT(for: app) { result in
switch result {
case .success:
break
case .failure(let error):
NotificationManager.shared.reportError(error: error)
}
}
}
func backup(_ app: InstalledApp) {
}
func exportBackup(_ app: InstalledApp) {
}
func restoreBackup(_ app: InstalledApp) {
}
func chooseIcon(for app: InstalledApp) {
}
func resetIcon(for app: InstalledApp) {
}
func setIcon(for app: InstalledApp, to image: UIImage? = nil) {
}
func sideloadApp(at url: URL) {
SideloadingManager.shared.sideloadApp(at: url) { result in
switch result {
case .success:
print("App sideloaded successfully.")
case .failure(let error):
print("Failed to sideload app: \(error.localizedDescription)")
}
}
}
}
struct MyAppsView_Previews: PreviewProvider {
static let context = DatabaseManager.shared.viewContext
static let app = StoreApp.makeAltStoreApp(in: context)
static var previews: some View {
NavigationView {
MyAppsView()
}
}
}

View File

@@ -0,0 +1,16 @@
//
// MyAppsViewModel.swift
// SideStore
//
// Created by Fabian Thies on 13.12.22.
// Copyright © 2022 SideStore. All rights reserved.
//
import SwiftUI
import AltStoreCore
class MyAppsViewModel: ViewModel {
var refreshGroup: RefreshGroup?
}

View File

@@ -0,0 +1,129 @@
//
// NewsItemView.swift
// SideStoreUI
//
// Created by Fabian Thies on 18.11.22.
// Copyright © 2022 Fabian Thies. All rights reserved.
//
import SwiftUI
import AsyncImage
import AltStoreCore
struct NewsItemView: View {
typealias TapHandler<T> = (T) -> Void
let newsItem: NewsItem
private var newsSelectionHandler: TapHandler<NewsItem>? = nil
private var appSelectionHandler: TapHandler<StoreApp>? = nil
init(newsItem: NewsItem) {
self.newsItem = newsItem
}
var body: some View {
VStack(spacing: 12) {
newsContent
.onTapGesture {
newsSelectionHandler?(newsItem)
}
if let connectedApp = newsItem.storeApp {
NavigationLink {
AppDetailView(storeApp: connectedApp)
} label: {
AppRowView(app: connectedApp)
}
.buttonStyle(PlainButtonStyle())
}
}
}
var newsContent: some View {
VStack(alignment: .leading, spacing: 12) {
VStack(alignment: .leading, spacing: 12) {
VStack(alignment: .leading) {
Text(newsItem.title)
.font(.title2)
.bold()
.foregroundColor(.white)
HStack(spacing: 0) {
if let sourceName = newsItem.source?.name {
Text(sourceName)
.italic()
}
if let externalURL = newsItem.externalURL {
Text(" • ")
HStack(spacing: 0) {
Image(systemSymbol: .link)
Text(externalURL.host ?? "")
.italic()
}
}
}
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.7))
}
Text(newsItem.caption)
.foregroundColor(.white.opacity(0.7))
}
.padding(24)
if let imageUrl = newsItem.imageURL {
AsyncImage(url: imageUrl) { image in
image
.resizable()
.aspectRatio(contentMode: .fit)
} placeholder: {
Color.secondary
.frame(maxWidth: .infinity, maxHeight: 100)
}
}
}
.frame(
maxWidth: .infinity,
alignment: .topLeading
)
.background(Color(newsItem.tintColor))
.clipShape(RoundedRectangle(cornerRadius: 24, style: .circular))
}
func onNewsSelection(_ handler: @escaping TapHandler<NewsItem>) -> Self {
var newSelf = self
newSelf.newsSelectionHandler = handler
return newSelf
}
func onAppSelection(_ handler: @escaping TapHandler<StoreApp>) -> Self {
var newSelf = self
newSelf.appSelectionHandler = handler
return newSelf
}
}
extension URL: Identifiable {
public var id: String {
return self.absoluteString
}
}
//struct NewsItemView_Previews: PreviewProvider {
// static var previews: some View {
// NewsItemView()
// }
//}
extension NewsItemView: Equatable {
/// Prevent re-rendering of the view if the parameters didn't change
static func == (lhs: NewsItemView, rhs: NewsItemView) -> Bool {
lhs.newsItem.identifier == rhs.newsItem.identifier
}
}

View File

@@ -0,0 +1,97 @@
//
// NewsView.swift
// SideStoreUI
//
// Created by Fabian Thies on 18.11.22.
// Copyright © 2022 Fabian Thies. All rights reserved.
//
import SwiftUI
import AltStoreCore
struct NewsView: View {
@SwiftUI.FetchRequest(sortDescriptors: [
NSSortDescriptor(keyPath: \NewsItem.date, ascending: false),
NSSortDescriptor(keyPath: \NewsItem.sortIndex, ascending: true),
NSSortDescriptor(keyPath: \NewsItem.sourceIdentifier, ascending: true)
])
var news: FetchedResults<NewsItem>
@State
var activeExternalUrl: URL?
@State
var selectedStoreApp: StoreApp?
var body: some View {
ScrollView {
self.announcementsCarousel
VStack(alignment: .leading) {
Text(L10n.NewsView.Section.FromSources.title)
.font(.title2)
.bold()
LazyVStack(spacing: 24) {
ForEach(news, id: \.objectID) { newsItem in
NewsItemView(newsItem: newsItem)
.onNewsSelection { newsItem in
self.activeExternalUrl = newsItem.externalURL
}
.frame(
maxWidth: .infinity,
alignment: .topLeading
)
}
}
}
.padding()
}
.background(Color(UIColor.systemGroupedBackground).ignoresSafeArea())
.navigationTitle(L10n.NewsView.title)
.sheet(item: self.$activeExternalUrl) { url in
SafariView(url: url)
.ignoresSafeArea()
}
.onAppear(perform: fetchNews)
}
var announcementsCarousel: some View {
TabView {
ForEach(0..<5) { _ in
RoundedRectangle(cornerRadius: 10)
.foregroundColor(.secondary)
.shadow(radius: 5, y: 3)
.padding()
}
}
.tabViewStyle(PageTabViewStyle())
.frame(maxWidth: .infinity)
.aspectRatio(16/9, contentMode: .fit)
}
func fetchNews() {
AppManager.shared.fetchSources { result in
do {
do {
let (_, context) = try result.get()
try context.save()
} catch let error as AppManager.FetchSourcesError {
try error.managedObjectContext?.save()
throw error
}
} catch {
print(error)
NotificationManager.shared.reportError(error: error)
}
}
}
}
struct NewsView_Previews: PreviewProvider {
static var previews: some View {
NewsView()
}
}

View File

@@ -0,0 +1,22 @@
//
// NewsViewModel.swift
// SideStoreUI
//
// Created by Fabian Thies on 18.11.22.
// Copyright © 2022 Fabian Thies. All rights reserved.
//
import SwiftUI
import AltStoreCore
class NewsViewModel: ViewModel {
@SwiftUI.FetchRequest(sortDescriptors: [
NSSortDescriptor(keyPath: \NewsItem.date, ascending: false),
NSSortDescriptor(keyPath: \NewsItem.sortIndex, ascending: true),
NSSortDescriptor(keyPath: \NewsItem.sourceIdentifier, ascending: true)
])
var news: FetchedResults<NewsItem>
init() {}
}

View File

@@ -0,0 +1,89 @@
//
// AppIconsShowcase.swift
// SideStore
//
// Created by Fabian Thies on 25.02.23.
// Copyright © 2023 SideStore. All rights reserved.
//
import SwiftUI
struct AppIconsShowcase: View {
@State var animationProgress = 0.0
@State var animation2Progress = 0.0
var body: some View {
VStack {
GeometryReader { proxy in
ZStack(alignment: .bottom) {
Image(uiImage: UIImage(named: "AppIcon")!)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 0.2 * proxy.size.width)
.clipShape(RoundedRectangle(cornerRadius: 24, style: .circular))
.offset(x: -0.3*proxy.size.width * self.animationProgress, y: -30)
.rotationEffect(.degrees(-20 * self.animationProgress))
.shadow(radius: 8 * self.animationProgress)
Image(uiImage: UIImage(named: "AppIcon")!)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 0.25 * proxy.size.width)
.clipShape(RoundedRectangle(cornerRadius: 24, style: .circular))
.offset(x: -0.15*proxy.size.width * self.animationProgress, y: -10)
.rotationEffect(.degrees(-10 * self.animationProgress))
.shadow(radius: 12 * self.animationProgress)
Image(uiImage: UIImage(named: "AppIcon")!)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 0.2 * proxy.size.width)
.clipShape(RoundedRectangle(cornerRadius: 24, style: .circular))
.offset(x: self.animationProgress*0.3*proxy.size.width, y: -30)
.rotationEffect(.degrees(self.animationProgress*20))
.shadow(radius: 8 * self.animationProgress)
Image(uiImage: UIImage(named: "AppIcon")!)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 0.25 * proxy.size.width)
.clipShape(RoundedRectangle(cornerRadius: 24, style: .circular))
.offset(x: self.animationProgress * 0.15*proxy.size.width, y: -10)
.rotationEffect(.degrees(self.animationProgress * 10))
.shadow(radius: 12 * self.animationProgress)
Image(uiImage: UIImage(named: "AppIcon")!)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 0.3 * proxy.size.width)
.clipShape(RoundedRectangle(cornerRadius: 24, style: .circular))
.shadow(radius: 16 * self.animationProgress + 8 * self.animation2Progress)
.scaleEffect(1.0 + 0.05 * self.animation2Progress)
}
.frame(maxWidth: proxy.size.width)
}
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
withAnimation(.spring()) {
self.animationProgress = 1.0
self.animation2Progress = 1.0
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
withAnimation(.spring()) {
self.animation2Progress = 0.0
}
}
}
}
}
}
struct AppIconsShowcase_Previews: PreviewProvider {
static var previews: some View {
AppIconsShowcase()
.frame(height: 150)
}
}

View File

@@ -0,0 +1,86 @@
//
// OnboardingStepView.swift
// SideStore
//
// Created by Fabian Thies on 25.02.23.
// Copyright © 2023 SideStore. All rights reserved.
//
import SwiftUI
struct OnboardingStep<Title: View, Hero: View, Content: View, Action: View> {
@ViewBuilder
var title: Title
@ViewBuilder
var hero: Hero
@ViewBuilder
var content: Content
@ViewBuilder
var action: Action
}
struct OnboardingStepView<Title: View, Hero: View, Content: View, Action: View>: View {
@ViewBuilder
var title: Title
@ViewBuilder
var hero: Hero
@ViewBuilder
var content: Content
@ViewBuilder
var action: Action
var body: some View {
VStack(spacing: 64) {
self.title
.font(.largeTitle.weight(.bold))
.frame(maxWidth: .infinity, alignment: .leading)
self.hero
.frame(height: 150)
self.content
Spacer()
self.action
}
.frame(maxWidth: .infinity)
.padding()
}
}
struct OnboardingStepView_Previews: PreviewProvider {
static var previews: some View {
OnboardingStepView(title: {
VStack(alignment: .leading) {
Text("Welcome to")
Text("SideStore")
.foregroundColor(.accentColor)
}
}, hero: {
AppIconsShowcase()
}, content: {
VStack(spacing: 16) {
Text("Before you can start sideloading apps, there is some setup to do.")
Text("The following setup will guide you through the steps one by one.")
Text("You will need a computer (Windows, macOS, Linux) and your Apple ID.")
}
}, action: {
SwiftUI.Button("Continue") {
}
.buttonStyle(FilledButtonStyle())
})
}
}

View File

@@ -0,0 +1,443 @@
//
// OnboardingView.swift
// SideStore
//
// Created by Fabian Thies on 25.02.23.
// Copyright © 2023 SideStore. All rights reserved.
//
import SwiftUI
import CoreData
import AltStoreCore
import minimuxer
import Reachability
import UniformTypeIdentifiers
struct OnboardingView: View {
enum OnboardingStep: Int, CaseIterable {
case welcome, pairing, wireguard, wireguardConfig, addSources, finish
}
@Environment(\.dismiss) var dismiss
// Temporary workaround for UIKit compatibility
var onDismiss: (() -> Void)? = nil
@State var currentStep: OnboardingStep = .wireguard //.welcome
@State private var pairingFileURL: URL? = nil
@State private var isWireGuardAppStorePageVisible: Bool = false
@State private var isDownloadingWireGuardProfile: Bool = false
@State private var wireGuardProfileFileURL: URL? = nil
@State private var reachabilityNotifier: Reachability? = nil
@State private var isWireGuardTunnelReachable: Bool = false
@State private var areTrustedSourcesEnabled: Bool = false
@State private var isLoadingTrustedSources: Bool = false
let pairingFileTypes = UTType.types(tag: "plist", tagClass: UTTagClass.filenameExtension, conformingTo: nil) + UTType.types(tag: "mobiledevicepairing", tagClass: UTTagClass.filenameExtension, conformingTo: UTType.data) + [.xml]
var body: some View {
TabView(selection: self.$currentStep) {
welcomeStep
.tag(OnboardingStep.welcome)
.highPriorityGesture(DragGesture())
pairingView
.tag(OnboardingStep.pairing)
.highPriorityGesture(DragGesture())
wireguardView
.tag(OnboardingStep.wireguard)
.highPriorityGesture(DragGesture())
wireguardConfigView
.tag(OnboardingStep.wireguardConfig)
.highPriorityGesture(DragGesture())
addSourcesView
.tag(OnboardingStep.addSources)
.highPriorityGesture(DragGesture())
finishView
.tag(OnboardingStep.finish)
.highPriorityGesture(DragGesture())
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.edgesIgnoringSafeArea(.bottom)
.background(Color.accentColor.opacity(0.1).edgesIgnoringSafeArea(.all))
.onChange(of: self.currentStep) { step in
switch step {
case .wireguardConfig:
self.startPingingWireGuardTunnel()
default:
self.stopPingingWireGuardTunnel()
}
}
}
func showNextStep() {
withAnimation {
self.currentStep = OnboardingStep(rawValue: self.currentStep.rawValue + 1) ?? self.currentStep
}
}
var welcomeStep: some View {
OnboardingStepView {
VStack(alignment: .leading) {
Text("Welcome to")
Text("SideStore")
.foregroundColor(.accentColor)
}
} hero: {
AppIconsShowcase()
} content: {
VStack(alignment: .leading, spacing: 16) {
Text("Before you can start sideloading apps, there is some setup to do.")
Text("The following setup will guide you through the steps one by one.")
Text("You will need a computer (Windows, macOS, Linux) and your Apple ID.")
}
} action: {
SwiftUI.Button("Continue") {
self.showNextStep()
}
.buttonStyle(FilledButtonStyle())
}
}
var pairingView: some View {
OnboardingStepView(title: {
VStack(alignment: .leading) {
Text("Pair your Device")
}
}, hero: {
Image(systemSymbol: .link)
.resizable()
.aspectRatio(contentMode: .fit)
.foregroundColor(.accentColor)
.shadow(color: .accentColor.opacity(0.8), radius: 12)
}, content: {
VStack(alignment: .leading, spacing: 16) {
Text("SideStore supports sideloading even on non-jailbroken devices.")
Text("For it to work, you have to generate a pairing file as described [here in our documentation](https://wiki.sidestore.io/guides/install#pairing-process).")
Text("Once you have the `<UUID>.mobiledevicepairing`, import it using the button below.")
}
}, action: {
ModalNavigationLink("Select Pairing File") {
DocumentPicker(selectedUrl: self.$pairingFileURL,
supportedTypes: self.pairingFileTypes.map { $0.identifier })
}
.buttonStyle(FilledButtonStyle())
.onChange(of: self.pairingFileURL) { newValue in
guard let url = newValue else {
return
}
self.importPairingFile(url: url)
}
})
}
var wireguardView: some View {
OnboardingStepView(title: {
VStack(alignment: .leading) {
Text("Download WireGuard")
}
}, hero: {
Image(systemSymbol: .icloudAndArrowDown)
.resizable()
.aspectRatio(contentMode: .fit)
.foregroundColor(.accentColor)
.shadow(color: .accentColor.opacity(0.8), radius: 12)
}, content: {
VStack(alignment: .leading, spacing: 16) {
Text("To sideload and sign app on-device without the need of a computer program like SideServer, a local WireGuard connection is required.")
Text("This connection is strictly local-only and does not connect to a server on the internet.")
Text("First, download WireGuard from the App Store (free).")
}
}, action: {
AppStoreView(isVisible: self.$isWireGuardAppStorePageVisible, itunesItemId: 1441195209)
.frame(width: .zero, height: .zero)
VStack {
SwiftUI.Button("Show in App Store") {
self.isWireGuardAppStorePageVisible = true
}
.buttonStyle(FilledButtonStyle())
SwiftUI.Button("Continue") {
self.showNextStep()
}
.buttonStyle(FilledButtonStyle())
}
})
}
var wireguardConfigView: some View {
OnboardingStepView(title: {
VStack(alignment: .leading) {
Text("Enable the WireGuard Tunnel")
}
}, hero: {
Image(systemSymbol: .network)
.resizable()
.aspectRatio(contentMode: .fit)
.foregroundColor(.accentColor)
.shadow(color: .accentColor.opacity(0.8), radius: 12)
}, content: {
VStack(alignment: .leading, spacing: 16) {
Text("Once WireGuard is installed, a configuration file has to be installed in the WireGuard app.")
Text("Tap the button below and open the downloaded file in the WireGuard app.")
Text("Then, activate the VPN tunnel to continue.")
}
}, action: {
VStack {
SwiftUI.Button("Download and Install Configuration File") {
self.downloadWireGuardProfile()
}
.buttonStyle(FilledButtonStyle(isLoading: self.isDownloadingWireGuardProfile))
.sheet(item: self.$wireGuardProfileFileURL) { fileURL in
ActivityView(items: [fileURL])
}
SwiftUI.Button(self.isWireGuardTunnelReachable ? "Continue" : "Waiting for connection...",
action: self.showNextStep)
.buttonStyle(FilledButtonStyle())
.disabled(!self.isWireGuardTunnelReachable)
}
})
}
var addSourcesView: some View {
OnboardingStepView(title: {
VStack(alignment: .leading) {
Text("Add Sources")
}
}, hero: {
Image(systemSymbol: .booksVertical)
.resizable()
.aspectRatio(contentMode: .fit)
.foregroundColor(.accentColor)
.shadow(color: .accentColor.opacity(0.8), radius: 12)
}, content: {
VStack(alignment: .leading, spacing: 16) {
Text("All apps are provided through sources, which anyone can create and share with the world.")
Text("We have compiled a list of trusted sources for SideStore which you can enable to start sideloading your favorite apps.")
Text("By default, only the source containing SideStore itself is enabled.")
Toggle("Enable Trusted Sources", isOn: $areTrustedSourcesEnabled)
}
}, action: {
SwiftUI.Button("Continue") {
self.setupTrustedSources()
}
.buttonStyle(FilledButtonStyle(isLoading: self.isLoadingTrustedSources))
.disabled(self.isLoadingTrustedSources)
})
}
var finishView: some View {
OnboardingStepView(title: {
VStack(alignment: .leading) {
Text("Setup Completed")
}
}, hero: {
Image(systemSymbol: .checkmark)
.resizable()
.aspectRatio(contentMode: .fit)
.foregroundColor(.accentColor)
.shadow(color: .accentColor.opacity(0.8), radius: 12)
}, content: {
VStack(alignment: .leading, spacing: 16) {
Text("Congratulations, you did it! 🎉")
Text("You can start your sideloading journey.")
}
}, action: {
SwiftUI.Button("Let's Go") {
self.finishOnboarding()
}
.buttonStyle(FilledButtonStyle())
})
}
}
extension OnboardingView {
func importPairingFile(url: URL) {
let isSecuredURL = url.startAccessingSecurityScopedResource() == true
do {
// Read to a string
let data = try Data(contentsOf: url)
let pairing_string = String(bytes: data, encoding: .utf8)
if pairing_string == nil {
// TODO: Show error message
debugPrint("Unable to read pairing file")
// displayError("Unable to read pairing file")
}
// Save to a file for next launch
let filename = "ALTPairingFile.mobiledevicepairing"
let fm = FileManager.default
let documentsPath = fm.documentsDirectory.appendingPathComponent("/\(filename)")
try pairing_string?.write(to: documentsPath, atomically: true, encoding: String.Encoding.utf8)
// Start minimuxer now that we have a file
start_minimuxer_threads(pairing_string!)
// Show the next onboarding step
self.showNextStep()
} catch {
NotificationManager.shared.reportError(error: error)
}
if (isSecuredURL) {
url.stopAccessingSecurityScopedResource()
}
}
func start_minimuxer_threads(_ pairing_file: String) {
target_minimuxer_address()
let documentsDirectory = FileManager.default.documentsDirectory.absoluteString
do {
try start(pairing_file, documentsDirectory)
} catch {
try! FileManager.default.removeItem(at: FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)"))
NotificationManager.shared.reportError(error: error)
debugPrint("minimuxer failed to start, please restart SideStore.", error)
// displayError("minimuxer failed to start, please restart SideStore. \((error as? LocalizedError)?.failureReason ?? "UNKNOWN ERROR!!!!!! REPORT TO GITHUB ISSUES!")")
}
start_auto_mounter(documentsDirectory)
}
}
extension OnboardingView {
func downloadWireGuardProfile() {
let profileDownloadUrl = "https://github.com/SideStore/SideStore/releases/download/0.3.1/SideStore.conf"
let destinationUrl = FileManager.default.temporaryDirectory.appendingPathComponent("SideStore.conf")
self.isDownloadingWireGuardProfile = true
URLSession.shared.dataTask(with: URLRequest(url: URL(string: profileDownloadUrl)!)) { data, response, error in
defer { self.isDownloadingWireGuardProfile = false }
if let error {
NotificationManager.shared.reportError(error: error)
return
}
guard let response = response as? HTTPURLResponse, 200..<300 ~= response.statusCode, let data else {
// TODO: Show error message
return
}
do {
try data.write(to: destinationUrl)
self.wireGuardProfileFileURL = destinationUrl
} catch {
NotificationManager.shared.reportError(error: error)
return
}
}.resume()
}
func startPingingWireGuardTunnel() {
do {
self.reachabilityNotifier = try Reachability(hostname: "10.7.0.1")
self.reachabilityNotifier?.whenReachable = { _ in
self.isWireGuardTunnelReachable = true
}
self.reachabilityNotifier?.whenUnreachable = { _ in
self.isWireGuardTunnelReachable = false
}
try self.reachabilityNotifier?.startNotifier()
} catch {
// TODO: Show error message
debugPrint(error)
NotificationManager.shared.reportError(error: error)
}
}
func stopPingingWireGuardTunnel() {
self.reachabilityNotifier?.stopNotifier()
}
}
extension OnboardingView {
func setupTrustedSources() {
guard self.areTrustedSourcesEnabled else {
return self.showNextStep()
}
self.isLoadingTrustedSources = true
AppManager.shared.fetchTrustedSources { result in
switch result {
case .success(let trustedSources):
// Cache trusted source IDs.
UserDefaults.shared.trustedSourceIDs = trustedSources.map { $0.identifier }
// Don't show sources without a sourceURL.
let featuredSourceURLs = trustedSources.compactMap { $0.sourceURL }
// This context is never saved, but keeps the managed sources alive.
let context = DatabaseManager.shared.persistentContainer.newBackgroundSavingViewContext()
let dispatchGroup = DispatchGroup()
for sourceURL in featuredSourceURLs {
dispatchGroup.enter()
AppManager.shared.fetchSource(sourceURL: sourceURL, managedObjectContext: context) { result in
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .main) {
self.isLoadingTrustedSources = false
// Save the fetched trusted sources
do {
try context.save()
} catch {
NotificationManager.shared.reportError(error: error)
}
self.showNextStep()
}
case .failure(let error):
NotificationManager.shared.reportError(error: error)
self.isLoadingTrustedSources = false
}
}
}
}
extension OnboardingView {
func finishOnboarding() {
// Set the onboarding complete flag
UserDefaults.standard.onboardingComplete = true
if let onDismiss {
onDismiss()
} else {
self.dismiss()
}
}
}
struct OnboardingView_Previews: PreviewProvider {
static var previews: some View {
ForEach(OnboardingView.OnboardingStep.allCases, id: \.self) { step in
Color.red
.ignoresSafeArea()
.sheet(isPresented: .constant(true)) {
OnboardingView(currentStep: step)
}
}
}
}

View File

@@ -0,0 +1,114 @@
//
// RootView.swift
// SideStoreUI
//
// Created by Fabian Thies on 18.11.22.
// Copyright © 2022 Fabian Thies. All rights reserved.
//
import SwiftUI
import SFSafeSymbols
@_exported import Inject
struct RootView: View {
@State var selectedTab: Tab = .defaultTab
var body: some View {
TabView(selection: self.$selectedTab) {
ForEach(Tab.allCases) { tab in
NavigationView {
content(for: tab)
}
.navigationViewStyle(StackNavigationViewStyle())
.tag(tab)
.tabItem {
tab.label
}
}
}
.overlay(self.notificationsOverlay)
}
@ViewBuilder
func content(for tab: Tab) -> some View {
switch tab {
case .news:
NewsView()
case .browse:
BrowseView()
case .myApps:
MyAppsView()
case .settings:
SettingsView()
}
}
@ObservedObject
var notificationManager = NotificationManager.shared
var notificationsOverlay: some View {
VStack {
Spacer()
ForEach(Array(notificationManager.notifications.values)) { notification in
VStack(alignment: .leading) {
Text(notification.title)
.bold()
if let message = notification.message {
Text(message)
.font(.callout)
}
}
.padding()
.frame(maxWidth: .infinity)
.foregroundColor(.white)
.background(Color.accentColor)
.clipShape(RoundedRectangle(cornerRadius: 16))
.shadow(radius: 15)
}
Spacer()
.frame(height: 50)
}
.padding()
.animation(.easeInOut)
}
}
extension RootView {
enum Tab: Int, NavigationTab {
case news, browse, myApps, settings
static var defaultTab: RootView.Tab = .news
var displaySymbol: SFSymbol {
switch self {
case .news: return .newspaper
case .browse: return .booksVertical
case .myApps: return .squareStack
case .settings: return .gearshape
}
}
var displayName: String {
switch self {
case .news: return L10n.RootView.news
case .browse: return L10n.RootView.browse
case .myApps: return L10n.RootView.myApps
case .settings: return L10n.RootView.settings
}
}
var label: some View {
Label(self.displayName, systemSymbol: self.displaySymbol)
}
}
}
struct RootView_Previews: PreviewProvider {
static var previews: some View {
RootView()
}
}

View File

@@ -0,0 +1,76 @@
//
// AdvancedSettingsView.swift
// SideStore
//
// Created by naturecodevoid on 2/19/23.
// Copyright © 2023 SideStore. All rights reserved.
//
import SwiftUI
private struct Server: Identifiable {
var id: String { value }
var display: String
var value: String
}
struct AdvancedSettingsView: View {
@ObservedObject private var iO = Inject.observer
private let anisetteServers = [
Server(display: "SideStore", value: "http://ani.sidestore.io"),
Server(display: "Macley (US)", value: "http://us1.sternserv.tech"),
Server(display: "Macley (DE)", value: "http://de1.sternserv.tech"),
Server(display: "DrPudding", value: "https://sign.rheaa.xyz"),
Server(display: "jkcoxson (AltServer)", value: "http://jkcoxson.com:2095"),
Server(display: "jkcoxson (Provision)", value: "http://jkcoxson.com:2052"),
Server(display: "Sideloadly", value: "https://sideloadly.io/anisette/irGb3Quww8zrhgqnzmrx"),
Server(display: "Nick", value: "http://45.33.29.114"),
Server(display: "Jawshoeadan", value: "https://anisette.jawshoeadan.me"),
Server(display: "crystall1nedev", value: "https://anisette.crystall1ne.software/"),
]
@AppStorage("textServer")
var usePreferred: Bool = true
@AppStorage("textInputAnisetteURL")
var anisetteURL: String = ""
@AppStorage("customAnisetteURL")
var selectedAnisetteServer: String = ""
var body: some View {
List {
Section {
Picker(L10n.AdvancedSettingsView.anisette, selection: $selectedAnisetteServer) {
ForEach(anisetteServers) { server in
Text(server.display)
}
}
}
Section {
Toggle(L10n.AdvancedSettingsView.DangerZone.usePreferred, isOn: $usePreferred)
HStack {
Text(L10n.AdvancedSettingsView.DangerZone.anisetteURL)
TextField("", text: $anisetteURL)
.autocapitalization(.none)
.autocorrectionDisabled(true)
}
} header: {
Text(L10n.AdvancedSettingsView.dangerZone)
} footer: {
Text(L10n.AdvancedSettingsView.dangerZoneInfo)
}
}
.navigationTitle(L10n.AdvancedSettingsView.title)
.enableInjection()
}
}
struct AdvancedSettingsView_Previews: PreviewProvider {
static var previews: some View {
AdvancedSettingsView()
}
}

View File

@@ -0,0 +1,135 @@
//
// AppIconsView.swift
// SideStore
//
// Created by naturecodevoid on 2/14/23.
// Copyright © 2023 SideStore. All rights reserved.
//
import SwiftUI
import SFSafeSymbols
struct Icon: Identifiable {
var id: String { assetName }
var displayName: String
let assetName: String
}
private struct SpecialIcon {
let assetName: String
let suffix: String?
let forceIndex: Int?
}
class AppIconsData: ObservableObject {
static let shared = AppIconsData()
private static let specialIcons = [
SpecialIcon(assetName: "Neon", suffix: "(Stable)", forceIndex: 0),
SpecialIcon(assetName: "Starburst", suffix: "(Beta)", forceIndex: 1),
SpecialIcon(assetName: "Steel", suffix: "(Nightly)", forceIndex: 2),
]
@Published var icons: [Icon] = []
@Published var primaryIcon: Icon?
@Published var selectedIconName: String?
private init() {
let bundleIcons = Bundle.main.object(forInfoDictionaryKey: "CFBundleIcons") as! [String: Any]
let primaryIconData = bundleIcons["CFBundlePrimaryIcon"] as! [String: Any]
let primaryIconName = primaryIconData["CFBundleIconName"] as! String
primaryIcon = Icon(displayName: primaryIconName, assetName: primaryIconName)
icons.append(primaryIcon!)
for (key, _) in bundleIcons["CFBundleAlternateIcons"] as! [String: Any] {
icons.append(Icon(displayName: key, assetName: key))
}
// sort alphabetically
icons.sort { $0.assetName < $1.assetName }
for specialIcon in AppIconsData.specialIcons {
guard let icon = icons.enumerated().first(where: { $0.element.assetName == specialIcon.assetName }) else { continue }
if let suffix = specialIcon.suffix {
icons[icon.offset].displayName += " " + suffix
}
if let forceIndex = specialIcon.forceIndex {
let e = icons.remove(at: icon.offset)
icons.insert(e, at: forceIndex)
}
}
if let alternateIconName = UIApplication.shared.alternateIconName {
selectedIconName = icons.first { $0.assetName == alternateIconName }?.assetName ?? primaryIcon!.assetName
} else {
selectedIconName = primaryIcon!.assetName
}
}
}
struct AppIconsView: View {
@ObservedObject private var iO = Inject.observer
@ObservedObject private var data = AppIconsData.shared
private let artists = [
"Chris (LitRitt)": ["Neon", "Starburst", "Steel", "Storm"],
"naturecodevoid": ["Honeydew", "Midnight", "Sky"],
"Swifticul": ["Vista"],
]
@State private var selectedIcon: String? = "" // this is just so the list row background changes when selecting a value, I couldn't get it to keep the selected icon name (for some reason it was always "", even when I set it to the selected icon asset name)
private let size: CGFloat = 72
private var cornerRadius: CGFloat {
size * 0.234
}
var body: some View {
List(data.icons, selection: $selectedIcon) { icon in
SwiftUI.Button(action: {
data.selectedIconName = icon.assetName
// Pass nil for primary icon
UIApplication.shared.setAlternateIconName(icon.assetName == data.primaryIcon!.assetName ? nil : icon.assetName, completionHandler: { error in
if let error = error {
print("error when setting alternate app icon to \(icon.assetName): \(error.localizedDescription)")
} else {
print("successfully changed app icon to \(icon.assetName)")
}
})
}) {
HStack(spacing: 20) {
// if we don't have an additional image asset for each icon, it will have low resolution
Image(uiImage: UIImage(named: icon.assetName + "-image") ?? UIImage())
.resizable()
.renderingMode(.original)
.cornerRadius(cornerRadius)
.frame(width: size, height: size)
VStack(alignment: .leading) {
Text(icon.displayName)
if let artist = artists.first(where: { $0.value.contains(icon.assetName) }) {
Text("By " + artist.key)
.foregroundColor(.gray)
}
}
Spacer()
if data.selectedIconName == icon.assetName {
Image(systemSymbol: .checkmark)
.foregroundColor(Color.blue)
}
}
}.foregroundColor(.primary)
}
.navigationTitle(L10n.AppIconsView.title)
.enableInjection()
}
}
struct AppIconsView_Previews: PreviewProvider {
static var previews: some View {
AppIconsView()
}
}

View File

@@ -0,0 +1,109 @@
//
// ConnectAppleIDView.swift
// SideStore
//
// Created by Fabian Thies on 29.11.22.
// Copyright © 2022 SideStore. All rights reserved.
//
import SwiftUI
import AltSign
struct ConnectAppleIDView: View {
typealias AuthenticationHandler = (String, String, @escaping (Result<(ALTAccount, ALTAppleAPISession), Error>) -> Void) -> Void
typealias CompletionHandler = ((ALTAccount, ALTAppleAPISession, String)?) -> Void
@Environment(\.dismiss)
private var dismiss
var authenticationHandler: AuthenticationHandler?
var completionHandler: CompletionHandler?
@State var email: String = ""
@State var password: String = ""
@State var isLoading: Bool = false
var isFormValid: Bool {
!email.isEmpty && !password.isEmpty
}
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 32) {
Text(L10n.ConnectAppleIDView.startWithSignIn)
VStack(spacing: 16) {
RoundedTextField(title: L10n.ConnectAppleIDView.appleID, placeholder: "user@sidestore.io", text: $email)
RoundedTextField(title: L10n.ConnectAppleIDView.password, placeholder: "••••••", text: $password, isSecure: true)
}
SwiftUI.Button(action: signIn) {
Text(L10n.ConnectAppleIDView.signIn)
.bold()
}
.buttonStyle(FilledButtonStyle(isLoading: isLoading))
.disabled(!isFormValid)
Spacer()
VStack(alignment: .leading) {
Text(L10n.ConnectAppleIDView.whyDoWeNeedThis)
.bold()
Text(L10n.ConnectAppleIDView.footer)
}
.padding()
.background(
RoundedRectangle(cornerRadius: 12)
.foregroundColor(Color(.secondarySystemBackground))
)
}
.padding(.horizontal)
}
.frame(maxWidth: .infinity)
.navigationTitle(L10n.ConnectAppleIDView.connectYourAppleID)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
SwiftUI.Button(action: self.cancel) {
Text(L10n.ConnectAppleIDView.cancel)
}
}
}
}
func signIn() {
self.isLoading = true
self.authenticationHandler?(email, password) { (result) in
defer {
self.isLoading = false
}
switch result
{
case .failure(ALTAppleAPIError.requiresTwoFactorAuthentication):
// Ignore
break
case .failure(let error as NSError):
let error = error.withLocalizedFailure(NSLocalizedString(L10n.ConnectAppleIDView.failedToSignIn, comment: ""))
print(error)
case .success((let account, let session)):
self.completionHandler?((account, session, password))
}
}
}
func cancel() {
self.completionHandler?(nil)
// self.dismiss()
}
}
struct ConnectAppleIDView_Previews: PreviewProvider {
static var previews: some View {
ConnectAppleIDView()
}
}

View File

@@ -0,0 +1,190 @@
//
// DevModeView.swift
// SideStore
//
// Created by naturecodevoid on 2/16/23.
// Copyright © 2023 SideStore. All rights reserved.
//
import SwiftUI
import LocalConsole
import minimuxer
// Yes, we know the password is right here. It's not supposed to be a secret, just something to hopefully prevent people breaking SideStore with dev mode and then complaining to us.
let DEV_MODE_PASSWORD = "devmode"
struct DevModePrompt: View {
@Binding var isShowingDevModePrompt: Bool
@Binding var isShowingDevModeMenu: Bool
@State var countdown = 0
@State var isShowingPasswordAlert = false
@State var isShowingIncorrectPasswordAlert = false
@State var password = ""
var button: some View {
SwiftUI.Button(action: {
if #available(iOS 16.0, *) {
isShowingPasswordAlert = true
} else {
// iOS 14 doesn't support .alert, so just go straight to dev mode without asking for a password
// iOS 15 also doesn't seem to support TextField in an alert (the text field was nonexistent)
enableDevMode()
}
}) {
Text(countdown <= 0 ? L10n.Action.enable + " " + L10n.DevModeView.title : L10n.DevModeView.read + " (\(countdown))")
.foregroundColor(.red)
}
.buttonStyle(FilledButtonStyle()) // TODO: set tintColor so text is more readable
.disabled(countdown > 0)
}
@ViewBuilder
var text: some View {
if #available(iOS 15.0, *),
let string = try? AttributedString(markdown: L10n.DevModeView.prompt, options: AttributedString.MarkdownParsingOptions(interpretedSyntax: .inlineOnlyPreservingWhitespace)) {
Text(string)
} else {
Text(L10n.DevModeView.prompt)
}
}
var view: some View {
ScrollView {
VStack {
text
.foregroundColor(.primary)
.padding(.bottom)
button
}
.padding(.horizontal)
}
.frame(maxWidth: .infinity)
.navigationTitle(L10n.DevModeView.title)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
SwiftUI.Button(action: { isShowingDevModePrompt = false }) {
Text(L10n.Action.close)
}
}
}
.onAppear {
countdown = 20
tickCountdown()
}
}
var body: some View {
NavigationView {
if #available(iOS 15.0, *) {
view
.alert(L10n.DevModeView.password, isPresented: $isShowingPasswordAlert) {
TextField(L10n.DevModeView.password, text: $password)
.autocapitalization(.none)
.autocorrectionDisabled(true)
SwiftUI.Button(L10n.Action.submit, action: {
if password == DEV_MODE_PASSWORD {
enableDevMode()
} else {
isShowingIncorrectPasswordAlert = true
}
})
}
.alert(L10n.DevModeView.incorrectPassword, isPresented: $isShowingIncorrectPasswordAlert) {
SwiftUI.Button(L10n.Action.tryAgain, action: {
isShowingIncorrectPasswordAlert = false
isShowingPasswordAlert = true
})
SwiftUI.Button(L10n.Action.cancel, action: {
isShowingIncorrectPasswordAlert = false
isShowingDevModePrompt = false
})
}
} else {
view
}
}
}
func enableDevMode() {
UserDefaults.standard.isDevModeEnabled = true
isShowingDevModePrompt = false
isShowingDevModeMenu = true
}
func tickCountdown() {
if countdown <= 0 { return }
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
countdown -= 1
tickCountdown()
}
}
}
struct DevModeMenu: View {
@ObservedObject private var iO = Inject.observer
@AppStorage("isConsoleEnabled")
var isConsoleEnabled: Bool = false
var body: some View {
List {
Section {
Toggle(L10n.DevModeView.console, isOn: self.$isConsoleEnabled)
.onChange(of: self.isConsoleEnabled) { value in
LCManager.shared.isVisible = value
}
NavigationLink(L10n.DevModeView.dataExplorer) {
FileExplorer.normal(url: FileManager.default.altstoreSharedDirectory)
.navigationTitle(L10n.DevModeView.dataExplorer)
}.foregroundColor(.red)
NavigationLink(L10n.DevModeView.tmpExplorer) {
FileExplorer.normal(url: FileManager.default.temporaryDirectory)
.navigationTitle(L10n.DevModeView.tmpExplorer)
}.foregroundColor(.red)
Toggle(L10n.DevModeView.skipResign, isOn: ResignAppOperation.skipResignBinding)
.foregroundColor(.red)
} footer: {
Text(L10n.DevModeView.footer)
}
Section {
AsyncFallibleButton(action: {
let dir = try dump_profiles(FileManager.default.documentsDirectory.absoluteString)
DispatchQueue.main.async {
UIApplication.shared.open(URL(string: "shareddocuments://" + dir.toString())!, options: [:], completionHandler: nil)
}
}) { execute in
Text(L10n.DevModeView.Minimuxer.dumpProfiles)
}
NavigationLink(L10n.DevModeView.Minimuxer.afcExplorer) {
FileExplorer.afc()
.navigationTitle(L10n.DevModeView.Minimuxer.afcExplorer)
}.foregroundColor(.red)
} header: {
Text(L10n.DevModeView.minimuxer)
} footer: {
Text(L10n.DevModeView.Minimuxer.footer)
}
}
.navigationTitle(L10n.DevModeView.title)
.enableInjection()
}
}
struct DevModeView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
List {
NavigationLink("DevModeMenu") {
DevModeMenu()
}
}
}
}
}

View File

@@ -0,0 +1,169 @@
//
// ErrorLogView.swift
// SideStore
//
// Created by Fabian Thies on 03.02.23.
// Copyright © 2023 SideStore. All rights reserved.
//
import SwiftUI
import AltStoreCore
import ExpandableText
struct ErrorLogView: View {
@Environment(\.dismiss) var dismiss
@SwiftUI.FetchRequest(sortDescriptors: [
NSSortDescriptor(keyPath: \LoggedError.date, ascending: false)
])
var loggedErrors: FetchedResults<LoggedError>
var groupedLoggedErrors: [Date: [LoggedError]] {
Dictionary(grouping: loggedErrors, by: { Calendar.current.startOfDay(for: $0.date) })
}
@State var currentFaqUrl: URL?
@State var isShowingMinimuxerLog: Bool = false
@State var isShowingDeleteConfirmation: Bool = false
var body: some View {
List {
ForEach(groupedLoggedErrors.keys.sorted(by: { $0 > $1 }), id: \.self) { date in
Section {
let errors = groupedLoggedErrors[date] ?? []
ForEach(errors, id: \.date) { error in
VStack(spacing: 8) {
HStack(alignment: .top) {
Group {
if let storeApp = error.storeApp {
AppIconView(iconUrl: storeApp.iconURL, isSideStore: storeApp.isSideStore, size: 50)
} else {
ZStack {
RoundedRectangle(cornerRadius: 50*0.234, style: .continuous)
.foregroundColor(Color(UIColor.secondarySystemFill))
Image(systemSymbol: .exclamationmarkCircle)
.imageScale(.large)
.foregroundColor(.red)
}
.frame(width: 50, height: 50)
}
}
VStack(alignment: .leading) {
Text(error.localizedFailure ?? "Operation Failed")
.bold()
Group {
switch error.domain {
case AltServerErrorDomain: Text("SideServer Error \(error.code)")
case OperationError.domain: Text("SideStore Error \(error.code)")
default: Text(error.error.localizedErrorCode)
}
}
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
Text(DateFormatterHelper.timeString(for: error.date))
.font(.caption)
.foregroundColor(.secondary)
}
let nsError = error.error as NSError
let errorDescription = [nsError.localizedDescription, nsError.localizedRecoverySuggestion].compactMap { $0 }.joined(separator: "\n\n")
Menu {
SwiftUI.Button {
UIPasteboard.general.string = errorDescription
} label: {
Label("Copy Error Message", systemSymbol: .docOnDoc)
}
SwiftUI.Button {
UIPasteboard.general.string = error.error.localizedErrorCode
} label: {
Label("Copy Error Code", systemSymbol: .docOnDoc)
}
SwiftUI.Button {
self.searchFAQ(for: error)
} label: {
Label("Search FAQ", systemSymbol: .magnifyingglass)
}
} label: {
Text(errorDescription)
.multilineTextAlignment(.leading)
.foregroundColor(.primary)
}
}
}
} header: {
Text(DateFormatterHelper.string(for: date))
}
}
}
.navigationBarTitle("Error Log")
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
ModalNavigationLink {
FilePreviewView(urls: [
FileManager.default.documentsDirectory.appendingPathComponent("minimuxer.log")
])
.ignoresSafeArea()
} label: {
Image(systemSymbol: .ladybug)
}
SwiftUI.Button {
self.isShowingDeleteConfirmation = true
} label: {
Image(systemSymbol: .trash)
}
.actionSheet(isPresented: self.$isShowingDeleteConfirmation) {
ActionSheet(
title: Text("Are you sure you want to clear the error log?"),
buttons: [
.destructive(Text("Clear Error Log"), action: self.clearLoggedErrors),
.cancel()
]
)
}
}
}
.sheet(item: self.$currentFaqUrl) { url in
SafariView(url: url)
}
}
func searchFAQ(for error: LoggedError) {
let baseURL = URL(string: "https://faq.altstore.io/getting-started/troubleshooting-guide")!
var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)!
let query = [error.domain, "\(error.code)"].joined(separator: "+")
components.queryItems = [URLQueryItem(name: "q", value: query)]
self.currentFaqUrl = components.url ?? baseURL
}
func clearLoggedErrors() {
DatabaseManager.shared.purgeLoggedErrors { result in
if case let .failure(error) = result {
NotificationManager.shared.reportError(error: error)
}
}
}
}
struct ErrorLogView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
ErrorLogView()
}
}
}

View File

@@ -0,0 +1,112 @@
//
// LicensesView.swift
// SideStore
//
// Created by Fabian Thies on 21.01.23.
// Copyright © 2023 SideStore. All rights reserved.
//
import SwiftUI
struct LicensesView: View {
let licenses = """
Jay Freeman (ldid)
Copyright (C) 2007-2012 Jay Freeman (saurik)
libimobiledevice
© 2007-2015 by the contributors of libimobiledevice - All rights reserved.
Gilles Vollant (minizip)
Copyright (C) 1998-2005 Gilles Vollant
Kishikawa Katsumi (KeychainAccess)
Copyright (c) 2014 kishikawa katsumi
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Alexander Grebenyuk (Nuke)
Copyright (c) 2015-2019 Alexander Grebenyuk
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Craig Hockenberry (MarkdownAttributedString)
Copyright (c) 2020 The Iconfactory, Inc. https://iconfactory.com
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
The OpenSSL Project (OpenSSL)
Copyright (c) 1998-2019 The OpenSSL Project. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. All advertising materials mentioning features or use of this software must display the following acknowledgment: "This product includes software developed by the OpenSSL Project for use in the OpenSSL Toolkit. (http://www.openssl.org/)"
4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to endorse or promote products derived from this software without prior written permission. For written permission, please contact openssl-core@openssl.org.
5. Products derived from this software may not be called "OpenSSL" nor may "OpenSSL" appear in their names without prior written permission of the OpenSSL Project.
6. Redistributions of any form whatsoever must retain the following acknowledgment:
"This product includes software developed by the OpenSSL Project for use in the OpenSSL Toolkit (http://www.openssl.org/)"
THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
This product includes cryptographic software written by Eric Young (eay@cryptsoft.com). This product includes software written by Tim Hudson (tjh@cryptsoft.com).
Eric Young (SSLeay)
/* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com)
All rights reserved.
This package is an SSL implementation written by Eric Young (eay@cryptsoft.com).
The implementation was written so as to conform with Netscapes SSL. This library is free for commercial and non-commercial use as long as the following conditions are aheared to. The following conditions apply to all code found in this distribution, be it the RC4, RSA, lhash, DES, etc., code; not just the SSL code. The SSL documentation included with this distribution is covered by the same copyright terms except that the holder is Tim Hudson (tjh@cryptsoft.com).
Copyright remains Eric Young's, and as such any Copyright notices in the code are not to be removed. If this package is used in a product, Eric Young should be given attribution as the author of the parts of the library used. This can be in the form of a textual message at program startup or in documentation (online or textual) provided with the package.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. All advertising materials mentioning features or use of this software must display the following acknowledgement:
"This product includes cryptographic software written by Eric Young (eay@cryptsoft.com)"
The word 'cryptographic' can be left out if the rouines from the library being used are not cryptographic related :-).
4. If you include any Windows specific code (or a derivative thereof) from the apps directory (application code) you must include an acknowledgement:
"This product includes software written by Tim Hudson (tjh@cryptsoft.com)" THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
The licence and distribution terms for any publically available version or derivative of this code cannot be changed. i.e. this code cannot simply be copied and put under another distribution licence [including the GNU Public Licence.]
Toni Ronkko (dirent)
Copyright (c) 1998-2019 Toni Ronkko
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Microsoft Corporation (C++ REST SDK)
Copyright (c) Microsoft Corporation
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Kutuzov Viktor (mman-win32)
Copyright (c) Kutuzov Viktor
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
ICONS
Settings by i cons from the Noun Project
"""
var body: some View {
ScrollView {
Text(licenses)
.padding()
}
.navigationTitle("Software Licenses")
}
}
struct LicensesView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
LicensesView()
}
}
}

View File

@@ -0,0 +1,90 @@
//
// RefreshAttemptsView.swift
// SideStore
//
// Created by Fabian Thies on 04.02.23.
// Copyright © 2023 SideStore. All rights reserved.
//
import SwiftUI
import AltStoreCore
struct RefreshAttemptsView: View {
@SwiftUI.FetchRequest(sortDescriptors: [
NSSortDescriptor(keyPath: \RefreshAttempt.date, ascending: false)
])
var refreshAttempts: FetchedResults<RefreshAttempt>
var groupedRefreshAttempts: [Date: [RefreshAttempt]] {
Dictionary(grouping: refreshAttempts, by: { Calendar.current.startOfDay(for: $0.date) })
}
var body: some View {
List {
ForEach(groupedRefreshAttempts.keys.sorted(by: { $0 > $1 }), id: \.self) { date in
Section {
let attempts = groupedRefreshAttempts[date] ?? []
ForEach(attempts, id: \.date) { attempt in
VStack(alignment: .leading, spacing: 8) {
HStack {
if attempt.isSuccess {
Text("Success")
.bold()
.foregroundColor(.green)
} else {
Text("Failure")
.bold()
.foregroundColor(.red)
}
Spacer()
Text(DateFormatterHelper.timeString(for: attempt.date))
.font(.caption)
.foregroundColor(.secondary)
}
if let description = attempt.errorDescription {
Text(description)
}
}
}
} header: {
Text(DateFormatterHelper.string(for: date))
}
}
}
.background(self.listBackground)
.navigationTitle("Refresh Attempts")
}
@ViewBuilder
var listBackground: some View {
if self.refreshAttempts.isEmpty {
VStack(spacing: 8) {
Spacer()
Text("No Refresh Attempts")
.font(.title)
Text("The more you use SideStore, the more often iOS will allow it to refresh apps in the background.")
Spacer()
}
.multilineTextAlignment(.center)
.foregroundColor(.secondary)
.padding()
} else {
Color.clear
}
}
}
struct RefreshAttemptsView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
RefreshAttemptsView()
}
}
}

View File

@@ -0,0 +1,374 @@
//
// SettingsView.swift
// SideStoreUI
//
// Created by Fabian Thies on 18.11.22.
// Copyright © 2022 Fabian Thies. All rights reserved.
//
import SwiftUI
import AsyncImage
import SFSafeSymbols
import LocalConsole
import AltStoreCore
import Intents
import minimuxer
struct SettingsView: View {
@ObservedObject private var iO = Inject.observer
var connectedAppleID: Team? {
DatabaseManager.shared.activeTeam()
}
@SwiftUI.FetchRequest(sortDescriptors: [], predicate: NSPredicate(format: "%K == YES", #keyPath(Team.isActiveTeam)))
var connectedTeams: FetchedResults<Team>
@AppStorage("isBackgroundRefreshEnabled")
var isBackgroundRefreshEnabled: Bool = true
@AppStorage("isDevModeEnabled")
var isDevModeEnabled: Bool = false
@AppStorage("isDebugLoggingEnabled")
var isDebugLoggingEnabled: Bool = false
@State var isShowingConnectAppleIDView = false
@State var isShowingResetPairingFileConfirmation = false
@State var isShowingDevModePrompt = false
@State var isShowingDevModeMenu = false
@State var externalURLToShow: URL?
@State var quickLookURL: URL?
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown Version"
var body: some View {
List {
Section {
if let connectedAppleID = connectedTeams.first {
HStack {
Text(L10n.SettingsView.ConnectedAppleID.name)
.foregroundColor(.secondary)
Spacer()
Text(connectedAppleID.name)
}
HStack {
Text(L10n.SettingsView.ConnectedAppleID.eMail)
.foregroundColor(.secondary)
Spacer()
Text(connectedAppleID.account.appleID)
}
HStack {
Text(L10n.SettingsView.ConnectedAppleID.type)
.foregroundColor(.secondary)
Spacer()
Text(connectedAppleID.type.localizedDescription)
}
} else {
SwiftUI.Button {
self.connectAppleID()
} label: {
Text(L10n.SettingsView.connectAppleID)
}
}
} header: {
if !connectedTeams.isEmpty {
HStack {
Text(L10n.SettingsView.ConnectedAppleID.text)
Spacer()
SwiftUI.Button {
self.disconnectAppleID()
} label: {
Text(L10n.SettingsView.ConnectedAppleID.signOut)
.font(.callout)
.bold()
}
}
}
} footer: {
VStack(alignment: .leading, spacing: 4) {
Text(L10n.SettingsView.ConnectedAppleID.Footer.p1)
Text(L10n.SettingsView.ConnectedAppleID.Footer.p2)
}
}
Section {
NavigationLink(L10n.AppIconsView.title) {
AppIconsView()
}
}
Section {
Toggle(isOn: self.$isBackgroundRefreshEnabled, label: {
Text(L10n.SettingsView.backgroundRefresh)
})
ModalNavigationLink(L10n.SettingsView.addToSiri) {
if let shortcut = INShortcut(intent: INInteraction.refreshAllApps().intent) {
SiriShortcutSetupView(shortcut: shortcut)
}
}
} header: {
Text(L10n.SettingsView.refreshingApps)
} footer: {
Text(L10n.SettingsView.refreshingAppsFooter)
}
Section {
SwiftUI.Button {
self.externalURLToShow = URL(string: "https://sidestore.io")!
} label: {
HStack {
Text("Developers")
.foregroundColor(.secondary)
Spacer()
Text("SideStore Team")
Image(systemSymbol: .chevronRight)
.foregroundColor(.secondary)
}
}
.foregroundColor(.primary)
SwiftUI.Button {
self.externalURLToShow = URL(string: "https://fabian-thies.de")!
} label: {
HStack {
Text(L10n.SettingsView.swiftUIRedesign)
.foregroundColor(.secondary)
Spacer()
Text("fabianthdev")
Image(systemSymbol: .chevronRight)
.foregroundColor(.secondary)
}
}
.foregroundColor(.primary)
NavigationLink {
LicensesView()
} label: {
Text("Licenses")
}
} header: {
Text(L10n.SettingsView.credits)
}
Section {
NavigationLink("Show Error Log") {
ErrorLogView()
}
NavigationLink("Show Refresh Attempts") {
RefreshAttemptsView()
}
NavigationLink(L10n.AdvancedSettingsView.title) {
AdvancedSettingsView()
}
Toggle(L10n.SettingsView.debugLogging, isOn: self.$isDebugLoggingEnabled)
.onChange(of: self.isDebugLoggingEnabled) { value in
UserDefaults.shared.isDebugLoggingEnabled = value
set_debug(value)
}
AsyncFallibleButton(action: self.exportLogs, label: { execute in Text(L10n.SettingsView.exportLogs) })
if MailComposeView.canSendMail {
ModalNavigationLink("Send Feedback") {
MailComposeView(recipients: ["support@sidestore.io"],
subject: "SideStore Beta \(appVersion) Feedback") {
NotificationManager.shared.showNotification(title: "Thank you for your feedback!")
} onError: { error in
NotificationManager.shared.reportError(error: error)
}
.ignoresSafeArea()
}
}
SwiftUI.Button(L10n.SettingsView.switchToUIKit, action: self.switchToUIKit)
SwiftUI.Button(L10n.SettingsView.resetImageCache, action: self.resetImageCache)
.foregroundColor(.red)
SwiftUI.Button("Reset Pairing File") {
self.isShowingResetPairingFileConfirmation = true
}
.foregroundColor(.red)
.actionSheet(isPresented: self.$isShowingResetPairingFileConfirmation) {
ActionSheet(title: Text("Are you sure to reset the pairing file?"), message: Text("You can reset the pairing file when you cannot sideload apps or enable JIT. SideStore will close when the file has been deleted."), buttons: [
.destructive(Text("Delete and Reset"), action: self.resetPairingFile),
.cancel()
])
}
if isDevModeEnabled {
NavigationLink(L10n.DevModeView.title, isActive: self.$isShowingDevModeMenu) {
DevModeMenu()
}.foregroundColor(.red)
} else {
SwiftUI.Button(L10n.DevModeView.title) {
self.isShowingDevModePrompt = true
}
.foregroundColor(.red)
.sheet(isPresented: self.$isShowingDevModePrompt) {
DevModePrompt(isShowingDevModePrompt: self.$isShowingDevModePrompt, isShowingDevModeMenu: self.$isShowingDevModeMenu)
}
}
} header: {
Text(L10n.SettingsView.debug)
}
Section {
} footer: {
Text("SideStore \(appVersion)")
.multilineTextAlignment(.center)
.frame(maxWidth: .infinity)
}
}
.listStyle(InsetGroupedListStyle())
.navigationTitle(L10n.SettingsView.title)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
SwiftUI.Button {
} label: {
Image(systemSymbol: .personCropCircle)
.imageScale(.large)
}
}
}
.sheet(item: $externalURLToShow) { url in
SafariView(url: url)
}
.quickLookPreview($quickLookURL)
.enableInjection()
}
// var appleIDSection: some View {
//
// }
func connectAppleID() {
guard let rootViewController = UIApplication.shared.keyWindow?.rootViewController else {
return
}
AppManager.shared.authenticate(presentingViewController: rootViewController) { (result) in
DispatchQueue.main.async {
switch result
{
case .failure(OperationError.cancelled):
// Ignore
break
case .failure(let error):
NotificationManager.shared.reportError(error: error)
case .success: break
}
}
}
}
func disconnectAppleID() {
DatabaseManager.shared.signOut { (error) in
DispatchQueue.main.async {
if let error = error
{
NotificationManager.shared.reportError(error: error)
}
}
}
}
func switchToUIKit() {
let storyboard = UIStoryboard(name: "Main", bundle: .main)
let rootVC = storyboard.instantiateViewController(withIdentifier: "tabBarController") as! TabBarController
UIApplication.shared.keyWindow?.rootViewController = rootVC
}
func resetImageCache() {
do {
let url = try FileManager.default.url(
for: .cachesDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: true)
try FileManager.default.removeItem(at: url.appendingPathComponent("com.zeu.cache", isDirectory: true))
} catch let error {
fatalError("\(error)")
}
}
func resetPairingFile() {
let filename = "ALTPairingFile.mobiledevicepairing"
let fileURL = FileManager.default.documentsDirectory.appendingPathComponent(filename)
// Delete the pairing file if it exists
if FileManager.default.fileExists(atPath: fileURL.path) {
do {
try FileManager.default.removeItem(at: fileURL)
print("Pairing file deleted successfully.")
} catch {
print("Failed to delete pairing file:", error)
}
}
// Close and exit SideStore
UIApplication.shared.perform(#selector(URLSessionTask.suspend))
DispatchQueue.main.asyncAfter(deadline: .now().advanced(by: .milliseconds(500))) {
exit(0)
}
}
func exportLogs() throws {
let path = FileManager.default.documentsDirectory.appendingPathComponent("sidestore.log")
var text = LCManager.shared.currentText
// TODO: add more potentially sensitive info to this array
var remove = [String]()
if let connectedAppleID = connectedTeams.first {
remove.append(connectedAppleID.name)
remove.append(connectedAppleID.account.appleID)
remove.append(connectedAppleID.account.firstName)
remove.append(connectedAppleID.account.lastName)
remove.append(connectedAppleID.account.localizedName)
remove.append(connectedAppleID.account.identifier)
remove.append(connectedAppleID.identifier)
}
if let udid = fetch_udid() {
remove.append(udid.toString())
}
for toRemove in remove {
text = text.replacingOccurrences(of: toRemove, with: "[removed]")
}
guard let data = text.data(using: .utf8) else { throw NSError(domain: "Failed to get data.", code: 2) }
try data.write(to: path)
quickLookURL = path
}
}
struct SettingsView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
SettingsView()
}
}
}