mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-19 11:43:24 +01:00
Limits installed Patreon apps that no longer have active pledge
Patreon apps with inactive pledges still support these actions: * Backed up * Deactivated * Export backup
This commit is contained in:
@@ -117,6 +117,9 @@ class MyAppsViewController: UICollectionViewController, PeekPopPreviewing
|
|||||||
{
|
{
|
||||||
super.viewIsAppearing(animated)
|
super.viewIsAppearing(animated)
|
||||||
|
|
||||||
|
// Ensure the button for each app reflects correct Patreon status.
|
||||||
|
self.collectionView.reloadData()
|
||||||
|
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
self.fetchAppIDs()
|
self.fetchAppIDs()
|
||||||
@@ -367,9 +370,22 @@ private extension MyAppsViewController
|
|||||||
|
|
||||||
cell.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Refresh %@", comment: ""), installedApp.name)
|
cell.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Refresh %@", comment: ""), installedApp.name)
|
||||||
|
|
||||||
formatter.includesTimeRemainingPhrase = true
|
// formatter.includesTimeRemainingPhrase = true
|
||||||
|
|
||||||
cell.bannerView.accessibilityLabel? += ". " + (formatter.string(from: currentDate, to: installedApp.expirationDate) ?? NSLocalizedString("Unknown", comment: "")) + " "
|
// cell.bannerView.accessibilityLabel? += ". " + (formatter.string(from: currentDate, to: installedApp.expirationDate) ?? NSLocalizedString("Unknown", comment: "")) + " "
|
||||||
|
|
||||||
|
if let storeApp = installedApp.storeApp, storeApp.isPledgeRequired, !storeApp.isPledged
|
||||||
|
{
|
||||||
|
cell.bannerView.button.isEnabled = false
|
||||||
|
cell.bannerView.button.alpha = 0.5
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
cell.bannerView.button.isEnabled = true
|
||||||
|
cell.bannerView.button.alpha = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
cell.bannerView.accessibilityLabel? += ". " + String(format: NSLocalizedString("Expires in %@", comment: ""), numberOfDaysText)
|
||||||
|
|
||||||
// Make sure refresh button is correct size.
|
// Make sure refresh button is correct size.
|
||||||
cell.layoutIfNeeded()
|
cell.layoutIfNeeded()
|
||||||
@@ -450,6 +466,17 @@ private extension MyAppsViewController
|
|||||||
cell.bannerView.button.addTarget(self, action: #selector(MyAppsViewController.activateApp(_:)), for: .primaryActionTriggered)
|
cell.bannerView.button.addTarget(self, action: #selector(MyAppsViewController.activateApp(_:)), for: .primaryActionTriggered)
|
||||||
cell.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Activate %@", comment: ""), installedApp.name)
|
cell.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Activate %@", comment: ""), installedApp.name)
|
||||||
|
|
||||||
|
if let storeApp = installedApp.storeApp, storeApp.isPledgeRequired, !storeApp.isPledged
|
||||||
|
{
|
||||||
|
cell.bannerView.button.isEnabled = false
|
||||||
|
cell.bannerView.button.alpha = 0.5
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
cell.bannerView.button.isEnabled = true
|
||||||
|
cell.bannerView.button.alpha = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
// Make sure refresh button is correct size.
|
// Make sure refresh button is correct size.
|
||||||
cell.layoutIfNeeded()
|
cell.layoutIfNeeded()
|
||||||
|
|
||||||
@@ -1689,7 +1716,7 @@ extension MyAppsViewController
|
|||||||
|
|
||||||
extension MyAppsViewController
|
extension MyAppsViewController
|
||||||
{
|
{
|
||||||
private func actions(for installedApp: InstalledApp) -> [UIMenuElement]
|
private func contextMenu(for installedApp: InstalledApp) -> UIMenu
|
||||||
{
|
{
|
||||||
var actions = [UIMenuElement]()
|
var actions = [UIMenuElement]()
|
||||||
|
|
||||||
@@ -1747,103 +1774,128 @@ extension MyAppsViewController
|
|||||||
|
|
||||||
let changeIconMenu = UIMenu(title: NSLocalizedString("Change Icon", comment: ""), image: UIImage(systemName: "photo"), children: changeIconActions)
|
let changeIconMenu = UIMenu(title: NSLocalizedString("Change Icon", comment: ""), image: UIImage(systemName: "photo"), children: changeIconActions)
|
||||||
|
|
||||||
guard installedApp.bundleIdentifier != StoreApp.altstoreAppID else {
|
if installedApp.bundleIdentifier == StoreApp.altstoreAppID
|
||||||
#if BETA
|
|
||||||
return [refreshAction, changeIconMenu]
|
|
||||||
#else
|
|
||||||
return [refreshAction]
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
if installedApp.isActive
|
|
||||||
{
|
{
|
||||||
actions.append(openMenu)
|
#if BETA
|
||||||
actions.append(refreshAction)
|
actions = [refreshAction, changeIconMenu]
|
||||||
|
#else
|
||||||
|
actions = [refreshAction]
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
actions.append(activateAction)
|
if installedApp.isActive
|
||||||
}
|
{
|
||||||
|
actions.append(openMenu)
|
||||||
if installedApp.isActive
|
actions.append(refreshAction)
|
||||||
{
|
}
|
||||||
actions.append(jitAction)
|
else
|
||||||
}
|
{
|
||||||
|
actions.append(activateAction)
|
||||||
#if BETA
|
|
||||||
actions.append(changeIconMenu)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if installedApp.isActive
|
|
||||||
{
|
|
||||||
actions.append(backupAction)
|
|
||||||
}
|
|
||||||
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(backupAction)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let backupDirectoryURL = FileManager.default.backupDirectoryURL(for: installedApp)
|
|
||||||
{
|
|
||||||
var backupExists = false
|
|
||||||
var outError: NSError? = nil
|
|
||||||
|
|
||||||
self.coordinator.coordinate(readingItemAt: backupDirectoryURL, options: [.withoutChanges], error: &outError) { (backupDirectoryURL) in
|
|
||||||
#if DEBUG
|
|
||||||
backupExists = true
|
|
||||||
#else
|
|
||||||
backupExists = FileManager.default.fileExists(atPath: backupDirectoryURL.path)
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if backupExists
|
if installedApp.isActive
|
||||||
{
|
{
|
||||||
actions.append(exportBackupAction)
|
actions.append(jitAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if BETA
|
||||||
|
actions.append(changeIconMenu)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if installedApp.isActive
|
||||||
|
{
|
||||||
|
actions.append(backupAction)
|
||||||
|
}
|
||||||
|
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(backupAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let backupDirectoryURL = FileManager.default.backupDirectoryURL(for: installedApp)
|
||||||
|
{
|
||||||
|
var backupExists = false
|
||||||
|
var outError: NSError? = nil
|
||||||
|
|
||||||
if installedApp.isActive
|
self.coordinator.coordinate(readingItemAt: backupDirectoryURL, options: [.withoutChanges], error: &outError) { (backupDirectoryURL) in
|
||||||
|
#if DEBUG
|
||||||
|
backupExists = true
|
||||||
|
#else
|
||||||
|
backupExists = FileManager.default.fileExists(atPath: backupDirectoryURL.path)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
if backupExists
|
||||||
{
|
{
|
||||||
actions.append(restoreBackupAction)
|
actions.append(exportBackupAction)
|
||||||
|
|
||||||
|
if installedApp.isActive
|
||||||
|
{
|
||||||
|
actions.append(restoreBackupAction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if let error = outError
|
||||||
|
{
|
||||||
|
print("Unable to check if backup exists:", error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if let error = outError
|
|
||||||
|
if installedApp.isActive
|
||||||
{
|
{
|
||||||
print("Unable to check if backup exists:", error)
|
actions.append(deactivateAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
|
||||||
|
if installedApp.bundleIdentifier != StoreApp.altstoreAppID
|
||||||
|
{
|
||||||
|
actions.append(removeAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
if (UserDefaults.standard.legacySideloadedApps ?? []).contains(installedApp.bundleIdentifier)
|
||||||
|
{
|
||||||
|
// Legacy sideloaded app, so can't detect if it's deleted.
|
||||||
|
actions.append(removeAction)
|
||||||
|
}
|
||||||
|
else if !UserDefaults.standard.isLegacyDeactivationSupported && !installedApp.isActive
|
||||||
|
{
|
||||||
|
// Inactive apps are actually deleted, so we need another way
|
||||||
|
// for user to remove them from AltStore.
|
||||||
|
actions.append(removeAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
if installedApp.isActive
|
var title: String?
|
||||||
|
|
||||||
|
if let storeApp = installedApp.storeApp, storeApp.isPledgeRequired, !storeApp.isPledged
|
||||||
{
|
{
|
||||||
actions.append(deactivateAction)
|
let error = OperationError.pledgeInactive(appName: installedApp.name)
|
||||||
|
title = error.localizedDescription
|
||||||
|
|
||||||
|
// Limit options for apps requiring pledges that we are no longer pledged to.
|
||||||
|
actions = actions.filter {
|
||||||
|
$0 == openMenu ||
|
||||||
|
$0 == deactivateAction ||
|
||||||
|
$0 == removeAction ||
|
||||||
|
$0 == backupAction ||
|
||||||
|
$0 == exportBackupAction ||
|
||||||
|
($0 == refreshAction && storeApp.bundleIdentifier == StoreApp.altstoreAppID) // Always show refresh option for AltStore so the menu will be shown.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable refresh action for AltStore.
|
||||||
|
refreshAction.attributes = .disabled
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
let menu = UIMenu(title: title ?? "", children: actions)
|
||||||
|
return menu
|
||||||
if installedApp.bundleIdentifier != StoreApp.altstoreAppID
|
|
||||||
{
|
|
||||||
actions.append(removeAction)
|
|
||||||
}
|
|
||||||
|
|
||||||
#else
|
|
||||||
|
|
||||||
if (UserDefaults.standard.legacySideloadedApps ?? []).contains(installedApp.bundleIdentifier)
|
|
||||||
{
|
|
||||||
// Legacy sideloaded app, so can't detect if it's deleted.
|
|
||||||
actions.append(removeAction)
|
|
||||||
}
|
|
||||||
else if !UserDefaults.standard.isLegacyDeactivationSupported && !installedApp.isActive
|
|
||||||
{
|
|
||||||
// Inactive apps are actually deleted, so we need another way
|
|
||||||
// for user to remove them from AltStore.
|
|
||||||
actions.append(removeAction)
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
return actions
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration?
|
override func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration?
|
||||||
@@ -1856,9 +1908,7 @@ extension MyAppsViewController
|
|||||||
let installedApp = self.dataSource.item(at: indexPath)
|
let installedApp = self.dataSource.item(at: indexPath)
|
||||||
|
|
||||||
return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: nil) { (suggestedActions) -> UIMenu? in
|
return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: nil) { (suggestedActions) -> UIMenu? in
|
||||||
let actions = self.actions(for: installedApp)
|
let menu = self.contextMenu(for: installedApp)
|
||||||
|
|
||||||
let menu = UIMenu(title: "", children: actions)
|
|
||||||
return menu
|
return menu
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -252,18 +252,12 @@ public extension InstalledApp
|
|||||||
|
|
||||||
class func fetchAppsForRefreshingAll(in context: NSManagedObjectContext) -> [InstalledApp]
|
class func fetchAppsForRefreshingAll(in context: NSManagedObjectContext) -> [InstalledApp]
|
||||||
{
|
{
|
||||||
let predicate = NSPredicate(format: "%K == YES AND %K != %@", #keyPath(InstalledApp.isActive), #keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
|
let predicate = NSPredicate(format: "(%K == YES AND %K != %@) AND (%K == nil OR %K == NO OR %K == YES)",
|
||||||
print("Fetch Apps for Refreshing All 'AltStore' predicate: \(String(describing: predicate))")
|
#keyPath(InstalledApp.isActive),
|
||||||
|
#keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID,
|
||||||
// if let patreonAccount = DatabaseManager.shared.patreonAccount(in: context), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated
|
#keyPath(InstalledApp.storeApp),
|
||||||
// {
|
#keyPath(InstalledApp.storeApp.isPledgeRequired),
|
||||||
// // No additional predicate
|
#keyPath(InstalledApp.storeApp.isPledged))
|
||||||
// }
|
|
||||||
// else
|
|
||||||
// {
|
|
||||||
// predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate,
|
|
||||||
// NSPredicate(format: "%K == nil OR %K == NO", #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.storeApp.isBeta))])
|
|
||||||
// }
|
|
||||||
|
|
||||||
var installedApps = InstalledApp.all(satisfying: predicate,
|
var installedApps = InstalledApp.all(satisfying: predicate,
|
||||||
sortedBy: [NSSortDescriptor(keyPath: \InstalledApp.expirationDate, ascending: true)],
|
sortedBy: [NSSortDescriptor(keyPath: \InstalledApp.expirationDate, ascending: true)],
|
||||||
@@ -272,7 +266,17 @@ public extension InstalledApp
|
|||||||
if let altStoreApp = InstalledApp.fetchAltStore(in: context)
|
if let altStoreApp = InstalledApp.fetchAltStore(in: context)
|
||||||
{
|
{
|
||||||
// Refresh AltStore last since it causes app to quit.
|
// Refresh AltStore last since it causes app to quit.
|
||||||
installedApps.append(altStoreApp)
|
|
||||||
|
if let storeApp = altStoreApp.storeApp, !storeApp.isPledgeRequired || storeApp.isPledged
|
||||||
|
{
|
||||||
|
// Only add AltStore if it's the public version OR if it's the beta and we're pledged to it.
|
||||||
|
installedApps.append(altStoreApp)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// No associated storeApp, so add it just to be safe.
|
||||||
|
installedApps.append(altStoreApp)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return installedApps
|
return installedApps
|
||||||
@@ -283,21 +287,14 @@ public extension InstalledApp
|
|||||||
// Date 6 hours before now.
|
// Date 6 hours before now.
|
||||||
let date = Date().addingTimeInterval(-1 * 6 * 60 * 60)
|
let date = Date().addingTimeInterval(-1 * 6 * 60 * 60)
|
||||||
|
|
||||||
let predicate = NSPredicate(format: "(%K == YES) AND (%K < %@) AND (%K != %@)",
|
let predicate = NSPredicate(format: "(%K == YES) AND (%K < %@) AND (%K != %@) AND (%K == nil OR %K == NO OR %K == YES)",
|
||||||
#keyPath(InstalledApp.isActive),
|
#keyPath(InstalledApp.isActive),
|
||||||
#keyPath(InstalledApp.refreshedDate), date as NSDate,
|
#keyPath(InstalledApp.refreshedDate), date as NSDate,
|
||||||
#keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
|
#keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID,
|
||||||
print("Active Apps For Background Refresh 'AltStore' predicate: \(String(describing: predicate))")
|
#keyPath(InstalledApp.storeApp),
|
||||||
|
#keyPath(InstalledApp.storeApp.isPledgeRequired),
|
||||||
// if let patreonAccount = DatabaseManager.shared.patreonAccount(in: context), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated
|
#keyPath(InstalledApp.storeApp.isPledged)
|
||||||
// {
|
)
|
||||||
// // No additional predicate
|
|
||||||
// }
|
|
||||||
// else
|
|
||||||
// {
|
|
||||||
// predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate,
|
|
||||||
// NSPredicate(format: "%K == nil OR %K == NO", #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.storeApp.isBeta))])
|
|
||||||
// }
|
|
||||||
|
|
||||||
var installedApps = InstalledApp.all(satisfying: predicate,
|
var installedApps = InstalledApp.all(satisfying: predicate,
|
||||||
sortedBy: [NSSortDescriptor(keyPath: \InstalledApp.expirationDate, ascending: true)],
|
sortedBy: [NSSortDescriptor(keyPath: \InstalledApp.expirationDate, ascending: true)],
|
||||||
@@ -305,8 +302,16 @@ public extension InstalledApp
|
|||||||
|
|
||||||
if let altStoreApp = InstalledApp.fetchAltStore(in: context), altStoreApp.refreshedDate < date
|
if let altStoreApp = InstalledApp.fetchAltStore(in: context), altStoreApp.refreshedDate < date
|
||||||
{
|
{
|
||||||
// Refresh AltStore last since it may cause app to quit.
|
if let storeApp = altStoreApp.storeApp, !storeApp.isPledgeRequired || storeApp.isPledged
|
||||||
installedApps.append(altStoreApp)
|
{
|
||||||
|
// Only add AltStore if it's the public version OR if it's the beta and we're pledged to it.
|
||||||
|
installedApps.append(altStoreApp)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// No associated storeApp, so add it just to be safe.
|
||||||
|
installedApps.append(altStoreApp)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return installedApps
|
return installedApps
|
||||||
|
|||||||
Reference in New Issue
Block a user