mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-17 18:53:40 +01:00
Hides source detail screens after adding/removing source
Fixes various issues due to saving/deleting source while viewing source details.
This commit is contained in:
@@ -399,6 +399,66 @@ extension AppManager
|
|||||||
|
|
||||||
NotificationCenter.default.post(name: AppManager.didRemoveSourceNotification, object: source)
|
NotificationCenter.default.post(name: AppManager.didRemoveSourceNotification, object: source)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func installAsync<T: AppProtocol>(@AsyncManaged _ app: T, presentingViewController: UIViewController?, context: AuthenticatedOperationContext = AuthenticatedOperationContext(),
|
||||||
|
completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) async -> RefreshGroup
|
||||||
|
{
|
||||||
|
@AsyncManaged var installingApp: AppProtocol = app
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
// Check if we need to add source first before installing app.
|
||||||
|
if let source = await $app.perform({ $0.storeApp?.source }), try await !source.isAdded
|
||||||
|
{
|
||||||
|
// This app's source is not yet added, so add it first.
|
||||||
|
guard let presentingViewController else { throw OperationError.sourceNotAdded(source) }
|
||||||
|
|
||||||
|
let (appName, appBundleID, sourceID) = await $app.perform { ($0.name, $0.bundleIdentifier, source.identifier) }
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
let message = String(format: NSLocalizedString("You must add this source before installing apps from it.\n\n“%@” will begin downloading once it has been added.", comment: ""), appName)
|
||||||
|
try await AppManager.shared.add(source, message: message, presentingViewController: presentingViewController)
|
||||||
|
}
|
||||||
|
catch let error as CancellationError
|
||||||
|
{
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// This should be an alert, so show directly rather than re-throwing error.
|
||||||
|
await presentingViewController.presentAlert(title: NSLocalizedString("Unable to Add Source", comment: ""), message: error.localizedDescription)
|
||||||
|
|
||||||
|
// Don't rethrow error
|
||||||
|
// throw error
|
||||||
|
|
||||||
|
throw CancellationError()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch persisted StoreApp to use for remainder of operation.
|
||||||
|
installingApp = try await DatabaseManager.shared.viewContext.performAsync {
|
||||||
|
let fetchRequest = StoreApp.fetchRequest()
|
||||||
|
fetchRequest.predicate = NSPredicate(format: "%K == %@ AND %K == %@",
|
||||||
|
#keyPath(StoreApp.bundleIdentifier), appBundleID,
|
||||||
|
#keyPath(StoreApp.sourceIdentifier), sourceID)
|
||||||
|
|
||||||
|
guard let storeApp = try DatabaseManager.shared.viewContext.fetch(fetchRequest).first else { throw OperationError.appNotFound(name: appName) }
|
||||||
|
return storeApp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
completionHandler(.failure(error))
|
||||||
|
|
||||||
|
let group = RefreshGroup(context: context)
|
||||||
|
return group
|
||||||
|
}
|
||||||
|
|
||||||
|
let group = await $installingApp.perform { self.install($0, presentingViewController: presentingViewController, context: context, completionHandler: completionHandler) }
|
||||||
|
return group
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AppManager
|
extension AppManager
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ extension OperationError
|
|||||||
|
|
||||||
case cacheClearError//(errors: [String])
|
case cacheClearError//(errors: [String])
|
||||||
case forbidden = 1013
|
case forbidden = 1013
|
||||||
|
case sourceNotAdded = 1014
|
||||||
|
|
||||||
/* Connection */
|
/* Connection */
|
||||||
case serverNotFound = 1200
|
case serverNotFound = 1200
|
||||||
@@ -130,6 +131,10 @@ extension OperationError
|
|||||||
OperationError(code: .forbidden, failureReason: failureReason, sourceFile: file, sourceLine: line)
|
OperationError(code: .forbidden, failureReason: failureReason, sourceFile: file, sourceLine: line)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func sourceNotAdded(@Managed _ source: Source, file: String = #fileID, line: UInt = #line) -> OperationError {
|
||||||
|
OperationError(code: .sourceNotAdded, sourceName: $source.name, sourceFile: file, sourceLine: line)
|
||||||
|
}
|
||||||
|
|
||||||
static func pledgeRequired(appName: String, file: String = #fileID, line: UInt = #line) -> OperationError {
|
static func pledgeRequired(appName: String, file: String = #fileID, line: UInt = #line) -> OperationError {
|
||||||
OperationError(code: .pledgeRequired, appName: appName, sourceFile: file, sourceLine: line)
|
OperationError(code: .pledgeRequired, appName: appName, sourceFile: file, sourceLine: line)
|
||||||
}
|
}
|
||||||
@@ -146,9 +151,13 @@ struct OperationError: ALTLocalizedError {
|
|||||||
|
|
||||||
var errorTitle: String?
|
var errorTitle: String?
|
||||||
var errorFailure: String?
|
var errorFailure: String?
|
||||||
|
|
||||||
|
@UserInfoValue
|
||||||
var appName: String?
|
var appName: String?
|
||||||
|
|
||||||
|
@UserInfoValue
|
||||||
|
var sourceName: String?
|
||||||
|
|
||||||
var requiredAppIDs: Int?
|
var requiredAppIDs: Int?
|
||||||
var availableAppIDs: Int?
|
var availableAppIDs: Int?
|
||||||
var expirationDate: Date?
|
var expirationDate: Date?
|
||||||
@@ -165,6 +174,7 @@ struct OperationError: ALTLocalizedError {
|
|||||||
self._failureReason = failureReason
|
self._failureReason = failureReason
|
||||||
|
|
||||||
self.appName = appName
|
self.appName = appName
|
||||||
|
self.sourceName = sourceName
|
||||||
self.requiredAppIDs = requiredAppIDs
|
self.requiredAppIDs = requiredAppIDs
|
||||||
self.availableAppIDs = availableAppIDs
|
self.availableAppIDs = availableAppIDs
|
||||||
self.expirationDate = expirationDate
|
self.expirationDate = expirationDate
|
||||||
@@ -191,6 +201,10 @@ struct OperationError: ALTLocalizedError {
|
|||||||
case .forbidden:
|
case .forbidden:
|
||||||
guard let failureReason = self._failureReason else { return NSLocalizedString("The operation is forbidden.", comment: "") }
|
guard let failureReason = self._failureReason else { return NSLocalizedString("The operation is forbidden.", comment: "") }
|
||||||
return failureReason
|
return failureReason
|
||||||
|
|
||||||
|
case .sourceNotAdded:
|
||||||
|
let sourceName = self.sourceName.map { String(format: NSLocalizedString("The source “%@”", comment: ""), $0) } ?? NSLocalizedString("The source", comment: "")
|
||||||
|
return String(format: NSLocalizedString("%@ is not added to AltStore.", comment: ""), sourceName)
|
||||||
|
|
||||||
case .appNotFound:
|
case .appNotFound:
|
||||||
let appName = self.appName ?? NSLocalizedString("The app", comment: "")
|
let appName = self.appName ?? NSLocalizedString("The app", comment: "")
|
||||||
|
|||||||
@@ -391,61 +391,50 @@ private extension SourceDetailContentViewController
|
|||||||
sender.isIndicatingActivity = true
|
sender.isIndicatingActivity = true
|
||||||
|
|
||||||
Task<Void, Never> {
|
Task<Void, Never> {
|
||||||
await self.addSourceThenDownloadApp(storeApp)
|
await self.downloadApp(storeApp)
|
||||||
sender.isIndicatingActivity = false
|
sender.isIndicatingActivity = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func addSourceThenDownloadApp(_ storeApp: StoreApp) async
|
@MainActor
|
||||||
|
func downloadApp(_ storeApp: StoreApp) async
|
||||||
{
|
{
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
let isAdded = try await self.source.isAdded
|
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
||||||
if !isAdded
|
if let installedApp = storeApp.installedApp, installedApp.isUpdateAvailable
|
||||||
{
|
{
|
||||||
let message = String(format: NSLocalizedString("You must add this source before you can install apps from it.\n\n“%@” will begin downloading once it has been added.", comment: ""), storeApp.name)
|
AppManager.shared.update(installedApp, presentingViewController: self) { result in
|
||||||
try await AppManager.shared.add(self.source, message: message, presentingViewController: self)
|
continuation.resume(with: result.map { _ in () })
|
||||||
}
|
}
|
||||||
|
|
||||||
do
|
reload()
|
||||||
{
|
}
|
||||||
try await self.downloadApp(storeApp)
|
else
|
||||||
}
|
{
|
||||||
catch is CancellationError {}
|
Task<Void, Never> { @MainActor in
|
||||||
catch
|
await AppManager.shared.installAsync(storeApp, presentingViewController: self) { result in
|
||||||
{
|
continuation.resume(with: result.map { _ in () })
|
||||||
let toastView = ToastView(error: error)
|
}
|
||||||
toastView.opensErrorLog = true
|
|
||||||
toastView.show(in: self)
|
reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch is CancellationError {}
|
catch is CancellationError {}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
await self.presentAlert(title: NSLocalizedString("Unable to Add Source", comment: ""), message: error.localizedDescription)
|
let toastView = ToastView(error: error)
|
||||||
|
toastView.opensErrorLog = true
|
||||||
|
toastView.show(in: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.collectionView.reloadSections([Section.featuredApps.rawValue])
|
self.collectionView.reloadSections([Section.featuredApps.rawValue])
|
||||||
}
|
|
||||||
|
func reload()
|
||||||
@MainActor
|
{
|
||||||
func downloadApp(_ storeApp: StoreApp) async throws
|
|
||||||
{
|
|
||||||
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
|
||||||
if let installedApp = storeApp.installedApp, installedApp.isUpdateAvailable
|
|
||||||
{
|
|
||||||
AppManager.shared.update(installedApp, presentingViewController: self) { result in
|
|
||||||
continuation.resume(with: result.map { _ in () })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
AppManager.shared.install(storeApp, presentingViewController: self) { result in
|
|
||||||
continuation.resume(with: result.map { _ in () })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UIView.performWithoutAnimation {
|
UIView.performWithoutAnimation {
|
||||||
guard let index = self.appsDataSource.items.firstIndex(of: storeApp) else {
|
guard let index = self.appsDataSource.items.firstIndex(of: storeApp) else {
|
||||||
self.collectionView.reloadSections([Section.featuredApps.rawValue])
|
self.collectionView.reloadSections([Section.featuredApps.rawValue])
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ class SourceDetailViewController: HeaderContentViewController<SourceHeaderView,
|
|||||||
private var addButton: VibrantButton!
|
private var addButton: VibrantButton!
|
||||||
|
|
||||||
private var previousBounds: CGRect?
|
private var previousBounds: CGRect?
|
||||||
private var cancellable: AnyCancellable?
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
init?(source: Source, coder: NSCoder)
|
init?(source: Source, coder: NSCoder)
|
||||||
{
|
{
|
||||||
@@ -307,11 +307,41 @@ private extension SourceDetailViewController
|
|||||||
{
|
{
|
||||||
func preparePipeline()
|
func preparePipeline()
|
||||||
{
|
{
|
||||||
self.cancellable = Publishers
|
Publishers.CombineLatest(self.viewModel.$isSourceAdded, self.viewModel.$isAddingSource)
|
||||||
.CombineLatest(self.viewModel.$isSourceAdded, self.viewModel.$isAddingSource)
|
|
||||||
.receive(on: RunLoop.main)
|
.receive(on: RunLoop.main)
|
||||||
.sink { [weak self] _ in
|
.sink { [weak self] _ in
|
||||||
self?.update()
|
self?.update()
|
||||||
}
|
}
|
||||||
|
.store(in: &self.cancellables)
|
||||||
|
|
||||||
|
// Adding or removing a source while viewing source details is currently broken,
|
||||||
|
// so for now we just dismiss the view whenever the source is added or removed.
|
||||||
|
self.viewModel.$isSourceAdded
|
||||||
|
.compactMap { $0 }
|
||||||
|
.dropFirst() // Ignore first non-nil value.
|
||||||
|
.removeDuplicates()
|
||||||
|
.receive(on: RunLoop.main)
|
||||||
|
.sink { [weak self] isAdded in
|
||||||
|
if isAdded
|
||||||
|
{
|
||||||
|
self?.didAddSource()
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self?.didRemoveSource()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &self.cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
func didAddSource()
|
||||||
|
{
|
||||||
|
guard let presentingViewController = self.navigationController?.presentingViewController else { return }
|
||||||
|
presentingViewController.dismiss(animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func didRemoveSource()
|
||||||
|
{
|
||||||
|
self.navigationController?.popToRootViewController(animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user