diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index efa1a067..5cc9af4f 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -84,6 +84,7 @@ B3C395F7284F362400DA9E2F /* AppCenterAnalytics in Frameworks */ = {isa = PBXBuildFile; productRef = B3C395F6284F362400DA9E2F /* AppCenterAnalytics */; }; B3C395F9284F362400DA9E2F /* AppCenterCrashes in Frameworks */ = {isa = PBXBuildFile; productRef = B3C395F8284F362400DA9E2F /* AppCenterCrashes */; }; B3EE16B62925E27D00B3B1F5 /* AnisetteManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3EE16B52925E27D00B3B1F5 /* AnisetteManager.swift */; }; + BD4513AB2C6FA98C0052BCC0 /* AppExtensionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4513AA2C6FA98C0052BCC0 /* AppExtensionView.swift */; }; BF02419622F2199300129732 /* RefreshAttemptsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF02419522F2199300129732 /* RefreshAttemptsViewController.swift */; }; BF08858322DE795100DE9F1E /* MyAppsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF08858222DE795100DE9F1E /* MyAppsViewController.swift */; }; BF08858522DE7EC800DE9F1E /* UpdateCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF08858422DE7EC800DE9F1E /* UpdateCollectionViewCell.swift */; }; @@ -584,6 +585,7 @@ B3C3960E284F4F9100DA9E2F /* AltStoreCore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltStoreCore.xcconfig; sourceTree = ""; }; B3C3960F284F53E900DA9E2F /* AltBackup.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltBackup.xcconfig; sourceTree = ""; }; B3EE16B52925E27D00B3B1F5 /* AnisetteManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnisetteManager.swift; sourceTree = ""; }; + BD4513AA2C6FA98C0052BCC0 /* AppExtensionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppExtensionView.swift; sourceTree = ""; }; BF02419522F2199300129732 /* RefreshAttemptsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshAttemptsViewController.swift; sourceTree = ""; }; BF08858222DE795100DE9F1E /* MyAppsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAppsViewController.swift; sourceTree = ""; }; BF08858422DE7EC800DE9F1E /* UpdateCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateCollectionViewCell.swift; sourceTree = ""; }; @@ -1623,6 +1625,7 @@ children = ( BF0C4EBC22A1BD8B009A2DD7 /* AppManager.swift */, BF88F97124F8727D00BB75DF /* AppManagerErrors.swift */, + BD4513AA2C6FA98C0052BCC0 /* AppExtensionView.swift */, ); path = "Managing Apps"; sourceTree = ""; @@ -2532,6 +2535,7 @@ BF6C8FAE2429597900125131 /* BannerCollectionViewCell.swift in Sources */, BF6F439223644C6E00A0B879 /* RefreshAltStoreViewController.swift in Sources */, BFE60742231B07E6002B0E8E /* SettingsHeaderFooterView.swift in Sources */, + BD4513AB2C6FA98C0052BCC0 /* AppExtensionView.swift in Sources */, BFE338E822F10E56002E24B9 /* LaunchViewController.swift in Sources */, BFA8172B23C5633D001B5953 /* FetchAnisetteDataOperation.swift in Sources */, BF9ABA4722DD0638008935CF /* BrowseCollectionViewCell.swift in Sources */, diff --git a/AltStore/Managing Apps/AppExtensionView.swift b/AltStore/Managing Apps/AppExtensionView.swift new file mode 100644 index 00000000..018c83fc --- /dev/null +++ b/AltStore/Managing Apps/AppExtensionView.swift @@ -0,0 +1,81 @@ +// +// AppExtensionView.swift +// SideStore +// +// Created by June P on 8/17/24. +// Copyright © 2024 SideStore. All rights reserved. +// + +import SwiftUI +import CAltSign + +extension ALTApplication: Identifiable {} + +struct AppExtensionView: View { + var extensions: Set + @State var selection: [ALTApplication] = [] + + var completion: (_ selection: [ALTApplication]) -> Any? + + var body: some View { + NavigationView { + List { + ForEach(self.extensions.sorted { + $0.bundleIdentifier < $1.bundleIdentifier + }, id: \.self) { item in + MultipleSelectionRow(title: item.bundleIdentifier, isSelected: !selection.contains(item)) { + if self.selection.contains(item) { + self.selection.removeAll(where: { $0 == item }) + } + else { + self.selection.append(item) + } + } + } + } + .navigationTitle("App Extensions") + .onDisappear { + _ = completion(selection) + } + } + } +} + +struct MultipleSelectionRow: View { + var title: String + var isSelected: Bool + var action: () -> Void + + var body: some View { + SwiftUI.Button(action: self.action) { + HStack { + Text(self.title) + if self.isSelected { + Spacer() + Image(systemName: "checkmark") + } + } + } + } +} + +class AppExtensionViewHostingController: UIHostingController { + + + var completion: Optional<(_ selection: [ALTApplication]) -> Any?> = nil + + required init(extensions: Set, completion: @escaping (_ selection: [ALTApplication]) -> Any?) { + self.completion = completion + super.init(rootView: AppExtensionView(extensions: extensions, completion: completion)) + } + + @MainActor required dynamic init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } +} + +extension AppExtensionViewHostingController: UIPopoverPresentationControllerDelegate { + func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle { + return .none + } +} diff --git a/AltStore/Managing Apps/AppManager.swift b/AltStore/Managing Apps/AppManager.swift index 6cff5816..9dd5104b 100644 --- a/AltStore/Managing Apps/AppManager.swift +++ b/AltStore/Managing Apps/AppManager.swift @@ -8,6 +8,7 @@ import Foundation import UIKit +import SwiftUI import UserNotifications import MobileCoreServices import Intents @@ -43,7 +44,7 @@ final class AppManager static let shared = AppManager() private(set) var updatePatronsResult: Result? - + private let operationQueue = OperationQueue() private let serialOperationQueue = OperationQueue() @@ -1014,7 +1015,7 @@ private extension AppManager return group } - func removeAppExtensions(from application: ALTApplication, _ presentingViewController: UIViewController, completion: @escaping (Result) -> Void) + func removeAppExtensions(from application: ALTApplication, extensions: Set, _ presentingViewController: UIViewController, completion: @escaping (Result) -> Void) { guard !application.appExtensions.isEmpty else { return completion(.success(())) } @@ -1029,7 +1030,7 @@ private extension AppManager firstSentence = NSLocalizedString("Non-developer Apple IDs are limited to creating 10 App IDs per week.", comment: "") } - let message = firstSentence + " " + NSLocalizedString("Would you like to remove this app's extensions so they don't count towards your limit?", comment: "") + let message = firstSentence + " " + NSLocalizedString("Would you like to remove this app's extensions so they don't count towards your limit? There are \(extensions.count) Extensions", comment: "") let alertController = UIAlertController(title: NSLocalizedString("App Contains Extensions", comment: ""), message: message, preferredStyle: .alert) alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style, handler: { (action) in @@ -1054,6 +1055,43 @@ private extension AppManager } }) + alertController.addAction(UIAlertAction(title: NSLocalizedString("Choose App Extensions", comment: ""), style: .default) { (action) in + let popoverContentController = AppExtensionViewHostingController(extensions: extensions) { (selection) in + do + { + for appExtension in selection + { + print("Deleting extension \(appExtension.bundleIdentifier)") + + try FileManager.default.removeItem(at: appExtension.fileURL) + } + completion(.success(())) + } + catch + { + completion(.failure(error)) + } + return nil + } + + let suiview = popoverContentController.view! + suiview.translatesAutoresizingMaskIntoConstraints = false + + popoverContentController.modalPresentationStyle = .popover + + if let popoverPresentationController = popoverContentController.popoverPresentationController { + popoverPresentationController.sourceView = presentingViewController.view + popoverPresentationController.sourceRect = CGRect(x: 50, y: 50, width: 4, height: 4) + popoverPresentationController.delegate = popoverContentController + + DispatchQueue.main.async { + presentingViewController.present(popoverContentController, animated: true) + } + } + + + }) + DispatchQueue.main.async { presentingViewController.present(alertController, animated: true) } @@ -1151,7 +1189,7 @@ private extension AppManager guard let app = context.app, let presentingViewController = context.authenticatedContext.presentingViewController else { throw OperationError.invalidParameters } - self?.removeAppExtensions(from: app, presentingViewController) { result in + self?.removeAppExtensions(from: app, extensions: extensions, presentingViewController) { result in switch result { case .success(): break case .failure(let error): context.error = error diff --git a/AltStore/My Apps/MyAppsViewController.swift b/AltStore/My Apps/MyAppsViewController.swift index af268563..2c2efaa3 100644 --- a/AltStore/My Apps/MyAppsViewController.swift +++ b/AltStore/My Apps/MyAppsViewController.swift @@ -1084,7 +1084,7 @@ private extension MyAppsViewController message = NSLocalizedString("This will also erase all backup data for this app.", comment: "") } - let alertController = UIAlertController(title: title, message: message, preferredStyle: .actionSheet) + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) alertController.addAction(.cancel) alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove", comment: ""), style: .destructive, handler: { (action) in AppManager.shared.remove(installedApp) { (result) in