diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 9988cbc9..3e275f8f 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -342,6 +342,8 @@ BFF7C9342578492100E55F36 /* ALTAnisetteData.m in Sources */ = {isa = PBXBuildFile; fileRef = BFB49AA823834CF900D542D9 /* ALTAnisetteData.m */; }; D513F6162A12CE4E0061EAA1 /* SourceError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D571ADCD2A02FA7400B24B63 /* SourceError.swift */; }; D5151BD92A8FF64300C96F28 /* RefreshAllAppsIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5151BD82A8FF64300C96F28 /* RefreshAllAppsIntent.swift */; }; + D5151BE12A90344300C96F28 /* RefreshAllAppsWidgetIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5151BE02A90344300C96F28 /* RefreshAllAppsWidgetIntent.swift */; }; + D5151BE22A90363300C96F28 /* RefreshAllAppsWidgetIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5151BE02A90344300C96F28 /* RefreshAllAppsWidgetIntent.swift */; }; D5151BE72A90395400C96F28 /* View+AltWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5151BE52A90391900C96F28 /* View+AltWidget.swift */; }; D5177B0D2A26944600270065 /* AltStore12ToAltStore13.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = D5177B0C2A26944600270065 /* AltStore12ToAltStore13.xcmappingmodel */; }; D5189C012A01BC6800F44625 /* UserInfoValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5189C002A01BC6800F44625 /* UserInfoValue.swift */; }; @@ -913,6 +915,7 @@ D52E988928D002D30032BE6B /* AltStore 11.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 11.xcdatamodel"; sourceTree = ""; }; C9EEAA842DA87A88A870053B /* Pods_AltStore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AltStore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D5151BD82A8FF64300C96F28 /* RefreshAllAppsIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshAllAppsIntent.swift; sourceTree = ""; }; + D5151BE02A90344300C96F28 /* RefreshAllAppsWidgetIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshAllAppsWidgetIntent.swift; sourceTree = ""; }; D5151BE52A90391900C96F28 /* View+AltWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+AltWidget.swift"; sourceTree = ""; }; D5177B0C2A26944600270065 /* AltStore12ToAltStore13.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore12ToAltStore13.xcmappingmodel; sourceTree = ""; }; D5189C002A01BC6800F44625 /* UserInfoValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserInfoValue.swift; sourceTree = ""; }; @@ -1987,6 +1990,7 @@ children = ( D55467B12A8D5E2600F4CE90 /* AppShortcuts.swift */, D5151BD82A8FF64300C96F28 /* RefreshAllAppsIntent.swift */, + D5151BE02A90344300C96F28 /* RefreshAllAppsWidgetIntent.swift */, ); path = "App Intents"; sourceTree = ""; @@ -2764,6 +2768,7 @@ buildActionMask = 2147483647; files = ( BF98917E250AAC4F002ACF50 /* Countdown.swift in Sources */, + D5151BE22A90363300C96F28 /* RefreshAllAppsWidgetIntent.swift in Sources */, BF42345A25101C35006D1EB2 /* WidgetView.swift in Sources */, D55E163728776CB700A627A1 /* ComplicationView.swift in Sources */, D5151BE72A90395400C96F28 /* View+AltWidget.swift in Sources */, @@ -2781,6 +2786,7 @@ BF74989B23621C0700CED65F /* ForwardingNavigationController.swift in Sources */, D57F2C9426E01BC700B9FA39 /* UIDevice+Vibration.swift in Sources */, BFD2478F2284C8F900981D42 /* Button.swift in Sources */, + D5151BE12A90344300C96F28 /* RefreshAllAppsWidgetIntent.swift in Sources */, D540E93828EE1BDE000F1B0F /* ErrorDetailsViewController.swift in Sources */, D513F6162A12CE4E0061EAA1 /* SourceError.swift in Sources */, BF56D2AC23DF8E170006506D /* FetchAppIDsOperation.swift in Sources */, @@ -3375,6 +3381,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$inherited WIDGET_EXTENSION"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -3405,6 +3412,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$inherited WIDGET_EXTENSION"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; diff --git a/AltStore/Intents/App Intents/RefreshAllAppsIntent.swift b/AltStore/Intents/App Intents/RefreshAllAppsIntent.swift index b7de3539..eb7d19fd 100644 --- a/AltStore/Intents/App Intents/RefreshAllAppsIntent.swift +++ b/AltStore/Intents/App Intents/RefreshAllAppsIntent.swift @@ -69,12 +69,21 @@ struct RefreshAllAppsIntent: AppIntent, CustomIntentMigratedAppIntent, Predictab } } + let presentsNotifications: Bool + private let operationActor = OperationActor() + init(presentsNotifications: Bool) + { + self.presentsNotifications = presentsNotifications + + self.progress.completedUnitCount = 0 + self.progress.totalUnitCount = 1 + } + init() { - self.progress.completedUnitCount = 0 - self.progress.totalUnitCount = 1 + self.init(presentsNotifications: false) } func perform() async throws -> some IntentResult & ProvidesDialog @@ -99,6 +108,7 @@ struct RefreshAllAppsIntent: AppIntent, CustomIntentMigratedAppIntent, Predictab for try await _ in taskGroup.prefix(1) { // We only care about the first child task to complete. + taskGroup.cancelAll() break } } @@ -148,7 +158,7 @@ private extension RefreshAllAppsIntent let installedApps = await context.perform { InstalledApp.fetchAppsForRefreshingAll(in: context) } try await withCheckedThrowingContinuation { continuation in - let operation = AppManager.shared.backgroundRefresh(installedApps, presentsNotifications: false) { (result) in + let operation = AppManager.shared.backgroundRefresh(installedApps, presentsNotifications: self.presentsNotifications) { (result) in do { let results = try result.get() diff --git a/AltStore/Intents/App Intents/RefreshAllAppsWidgetIntent.swift b/AltStore/Intents/App Intents/RefreshAllAppsWidgetIntent.swift new file mode 100644 index 00000000..22ebe116 --- /dev/null +++ b/AltStore/Intents/App Intents/RefreshAllAppsWidgetIntent.swift @@ -0,0 +1,38 @@ +// +// RefreshAllAppsWidgetIntent.swift +// AltStore +// +// Created by Riley Testut on 8/18/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// + +import AppIntents + +@available(iOS 17, *) +struct RefreshAllAppsWidgetIntent: AppIntent, ProgressReportingIntent +{ + static var title: LocalizedStringResource { "Refresh Apps via Widget" } + static var isDiscoverable: Bool { false } // Don't show in Shortcuts or Spotlight. + + #if !WIDGET_EXTENSION + private let intent = RefreshAllAppsIntent(presentsNotifications: true) + #endif + + func perform() async throws -> some IntentResult & ProvidesDialog + { + #if WIDGET_EXTENSION + return .result(dialog: "") + #else + return try await self.intent.perform() + #endif + } +} + +// To ensure this intent is handled by the app itself (and not widget extension) +// we need to conform to either `ForegroundContinuableIntent` or `AudioPlaybackIntent`. +// https://mastodon.social/@mgorbach/110812347476671807 +// +// Unfortunately `ForegroundContinuableIntent` is marked as unavailable in app extensions, +// so we conform to AudioPlaybackIntent instead despite not playing audio ¯\_(ツ)_/¯ +@available(iOS 17, *) +extension RefreshAllAppsWidgetIntent: AudioPlaybackIntent {} diff --git a/AltWidget/Extensions/View+AltWidget.swift b/AltWidget/Extensions/View+AltWidget.swift index ed212ef0..74dc4231 100644 --- a/AltWidget/Extensions/View+AltWidget.swift +++ b/AltWidget/Extensions/View+AltWidget.swift @@ -24,4 +24,33 @@ extension View background(backgroundView) } } + + @ViewBuilder + func invalidatableContentIfAvailable() -> some View + { + if #available(iOSApplicationExtension 17, *) + { + self.invalidatableContent() + } + else + { + self + } + } + + @ViewBuilder + func activatesRefreshAllAppsIntent() -> some View + { + if #available(iOSApplicationExtension 17, *) + { + Button(intent: RefreshAllAppsWidgetIntent()) { + self + } + .buttonStyle(.plain) + } + else + { + self + } + } } diff --git a/AltWidget/WidgetView.swift b/AltWidget/WidgetView.swift index 4234521f..a14da63e 100644 --- a/AltWidget/WidgetView.swift +++ b/AltWidget/WidgetView.swift @@ -80,9 +80,11 @@ struct WidgetView : View .opacity(0.8) .fixedSize(horizontal: true, vertical: false) .offset(x: 5) + .invalidatableContentIfAvailable() } } .fixedSize(horizontal: false, vertical: true) + .activatesRefreshAllAppsIntent() } .padding() }