[Widgets]: Use AppIntentConfiguration instead of StaticConfiguration and cleanup

This commit is contained in:
Magesh K
2025-01-10 08:11:35 +05:30
parent 4e10527f03
commit f69b293004
9 changed files with 340 additions and 178 deletions

View File

@@ -51,6 +51,8 @@
A800F7042CE28E3800208744 /* View+AltWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = A800F7032CE28E2F00208744 /* View+AltWidget.swift */; }; A800F7042CE28E3800208744 /* View+AltWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = A800F7032CE28E2F00208744 /* View+AltWidget.swift */; };
A805C3CD2D0C316A00E76BDD /* Pods_SideStore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A805C3CC2D0C316A00E76BDD /* Pods_SideStore.framework */; }; A805C3CD2D0C316A00E76BDD /* Pods_SideStore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A805C3CC2D0C316A00E76BDD /* Pods_SideStore.framework */; };
A8087E752D2D2958002DB21B /* ImportExport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8087E742D2D2958002DB21B /* ImportExport.swift */; }; A8087E752D2D2958002DB21B /* ImportExport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8087E742D2D2958002DB21B /* ImportExport.swift */; };
A8096D182D30AD4F000C39C6 /* WidgetUpdateIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8096D172D30AD4F000C39C6 /* WidgetUpdateIntent.swift */; };
A8096D1C2D30ADA9000C39C6 /* ActiveAppsTimelineProvider+Simulator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8096D1B2D30ADA9000C39C6 /* ActiveAppsTimelineProvider+Simulator.swift */; };
A809F69E2D04D7AC00F0F0F3 /* libminimuxer_static.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A809F68E2D04D71200F0F0F3 /* libminimuxer_static.a */; }; A809F69E2D04D7AC00F0F0F3 /* libminimuxer_static.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A809F68E2D04D71200F0F0F3 /* libminimuxer_static.a */; };
A809F69F2D04D7B300F0F0F3 /* libem_proxy_static.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A809F6942D04D71200F0F0F3 /* libem_proxy_static.a */; }; A809F69F2D04D7B300F0F0F3 /* libem_proxy_static.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A809F6942D04D71200F0F0F3 /* libem_proxy_static.a */; };
A809F6A82D04DA1900F0F0F3 /* minimuxer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A809F6A32D04DA1900F0F0F3 /* minimuxer.swift */; }; A809F6A82D04DA1900F0F0F3 /* minimuxer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A809F6A32D04DA1900F0F0F3 /* minimuxer.swift */; };
@@ -63,6 +65,7 @@
A859ED5D2D1EE827003DCC58 /* OpenSSL.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; A859ED5D2D1EE827003DCC58 /* OpenSSL.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
A8945AA62D059B6100D86CBE /* Roxas.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A8945AA52D059B6100D86CBE /* Roxas.framework */; }; A8945AA62D059B6100D86CBE /* Roxas.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A8945AA52D059B6100D86CBE /* Roxas.framework */; };
A8A543302D04F14400D72399 /* libfragmentzip.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A8A5432F2D04F0C100D72399 /* libfragmentzip.a */; }; A8A543302D04F14400D72399 /* libfragmentzip.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A8A5432F2D04F0C100D72399 /* libfragmentzip.a */; };
A8A853AF2D3065A300995795 /* ActiveAppsTimelineProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8A853AE2D3065A300995795 /* ActiveAppsTimelineProvider.swift */; };
A8B516E32D2666CA0047047C /* CoreDataHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8B516E22D2666CA0047047C /* CoreDataHelper.swift */; }; A8B516E32D2666CA0047047C /* CoreDataHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8B516E22D2666CA0047047C /* CoreDataHelper.swift */; };
A8B516E62D2668170047047C /* DateTimeUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8B516E52D2668020047047C /* DateTimeUtil.swift */; }; A8B516E62D2668170047047C /* DateTimeUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8B516E52D2668020047047C /* DateTimeUtil.swift */; };
A8BB34E52D04EC8E000A8B4D /* minimuxer-helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = A809F6A52D04DA1900F0F0F3 /* minimuxer-helpers.swift */; }; A8BB34E52D04EC8E000A8B4D /* minimuxer-helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = A809F6A52D04DA1900F0F0F3 /* minimuxer-helpers.swift */; };
@@ -620,6 +623,8 @@
A800F7032CE28E2F00208744 /* View+AltWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+AltWidget.swift"; sourceTree = "<group>"; }; A800F7032CE28E2F00208744 /* View+AltWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+AltWidget.swift"; sourceTree = "<group>"; };
A805C3CC2D0C316A00E76BDD /* Pods_SideStore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Pods_SideStore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; A805C3CC2D0C316A00E76BDD /* Pods_SideStore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Pods_SideStore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
A8087E742D2D2958002DB21B /* ImportExport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportExport.swift; sourceTree = "<group>"; }; A8087E742D2D2958002DB21B /* ImportExport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportExport.swift; sourceTree = "<group>"; };
A8096D172D30AD4F000C39C6 /* WidgetUpdateIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetUpdateIntent.swift; sourceTree = "<group>"; };
A8096D1B2D30ADA9000C39C6 /* ActiveAppsTimelineProvider+Simulator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ActiveAppsTimelineProvider+Simulator.swift"; sourceTree = "<group>"; };
A809F6A22D04DA1900F0F0F3 /* minimuxer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = minimuxer.h; sourceTree = "<group>"; }; A809F6A22D04DA1900F0F0F3 /* minimuxer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = minimuxer.h; sourceTree = "<group>"; };
A809F6A32D04DA1900F0F0F3 /* minimuxer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = minimuxer.swift; sourceTree = "<group>"; }; A809F6A32D04DA1900F0F0F3 /* minimuxer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = minimuxer.swift; sourceTree = "<group>"; };
A809F6A42D04DA1900F0F0F3 /* minimuxer-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "minimuxer-Bridging-Header.h"; sourceTree = "<group>"; }; A809F6A42D04DA1900F0F0F3 /* minimuxer-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "minimuxer-Bridging-Header.h"; sourceTree = "<group>"; };
@@ -638,6 +643,7 @@
A86202322D1F35640091187B /* AltStore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltStore.xcconfig; sourceTree = "<group>"; }; A86202322D1F35640091187B /* AltStore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltStore.xcconfig; sourceTree = "<group>"; };
A86202332D1F35640091187B /* AltStoreCore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltStoreCore.xcconfig; sourceTree = "<group>"; }; A86202332D1F35640091187B /* AltStoreCore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltStoreCore.xcconfig; sourceTree = "<group>"; };
A8945AA52D059B6100D86CBE /* Roxas.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Roxas.framework; sourceTree = BUILT_PRODUCTS_DIR; }; A8945AA52D059B6100D86CBE /* Roxas.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Roxas.framework; sourceTree = BUILT_PRODUCTS_DIR; };
A8A853AE2D3065A300995795 /* ActiveAppsTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveAppsTimelineProvider.swift; sourceTree = "<group>"; };
A8B516E22D2666CA0047047C /* CoreDataHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataHelper.swift; sourceTree = "<group>"; }; A8B516E22D2666CA0047047C /* CoreDataHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataHelper.swift; sourceTree = "<group>"; };
A8B516E52D2668020047047C /* DateTimeUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimeUtil.swift; sourceTree = "<group>"; }; A8B516E52D2668020047047C /* DateTimeUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimeUtil.swift; sourceTree = "<group>"; };
A8C38C1D2D206A3A00E83DBD /* ConsoleLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLogger.swift; sourceTree = "<group>"; }; A8C38C1D2D206A3A00E83DBD /* ConsoleLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLogger.swift; sourceTree = "<group>"; };
@@ -1148,6 +1154,16 @@
path = importexport; path = importexport;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
A8096D1D2D30ADD5000C39C6 /* Providers */ = {
isa = PBXGroup;
children = (
D577AB7A2A967DF5007FE952 /* AppsTimelineProvider.swift */,
A8A853AE2D3065A300995795 /* ActiveAppsTimelineProvider.swift */,
A8096D1B2D30ADA9000C39C6 /* ActiveAppsTimelineProvider+Simulator.swift */,
);
path = Providers;
sourceTree = "<group>";
};
A809F68A2D04D71200F0F0F3 /* Products */ = { A809F68A2D04D71200F0F0F3 /* Products */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -1168,6 +1184,7 @@
A80D790B2D2F209700A40F40 /* Intents */ = { A80D790B2D2F209700A40F40 /* Intents */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
A8096D172D30AD4F000C39C6 /* WidgetUpdateIntent.swift */,
A80D790C2D2F20AF00A40F40 /* PaginationIntent.swift */, A80D790C2D2F20AF00A40F40 /* PaginationIntent.swift */,
); );
path = Intents; path = Intents;
@@ -1199,6 +1216,14 @@
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
A8A853AD2D3050CC00995795 /* pagination */ = {
isa = PBXGroup;
children = (
A80D790E2D2F217000A40F40 /* PaginationDataHolder.swift */,
);
path = pagination;
sourceTree = "<group>";
};
A8B516DE2D2666900047047C /* dignostics */ = { A8B516DE2D2666900047047C /* dignostics */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -1218,6 +1243,7 @@
A8C38C1C2D2068D100E83DBD /* Utils */ = { A8C38C1C2D2068D100E83DBD /* Utils */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
A8A853AD2D3050CC00995795 /* pagination */,
A8087E712D2D291B002DB21B /* importexport */, A8087E712D2D291B002DB21B /* importexport */,
A8B516DE2D2666900047047C /* dignostics */, A8B516DE2D2666900047047C /* dignostics */,
A8C38C272D206AA500E83DBD /* common */, A8C38C272D206AA500E83DBD /* common */,
@@ -1706,10 +1732,10 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
A80D790B2D2F209700A40F40 /* Intents */, A80D790B2D2F209700A40F40 /* Intents */,
A8096D1D2D30ADD5000C39C6 /* Providers */,
A800F6FE2CE28DE300208744 /* Extensions */, A800F6FE2CE28DE300208744 /* Extensions */,
BF8B17F0250AC62400F8157F /* AltWidgetExtension.entitlements */, BF8B17F0250AC62400F8157F /* AltWidgetExtension.entitlements */,
D5FD4EC42A952EAD0097BEE8 /* AltWidgetBundle.swift */, D5FD4EC42A952EAD0097BEE8 /* AltWidgetBundle.swift */,
D577AB7A2A967DF5007FE952 /* AppsTimelineProvider.swift */,
D50C29F22A8ECD71009AB488 /* Widgets */, D50C29F22A8ECD71009AB488 /* Widgets */,
D51AF9752A97D29100471312 /* Model */, D51AF9752A97D29100471312 /* Model */,
D577AB802A968B7E007FE952 /* Components */, D577AB802A968B7E007FE952 /* Components */,
@@ -2080,7 +2106,6 @@
D50C29F22A8ECD71009AB488 /* Widgets */ = { D50C29F22A8ECD71009AB488 /* Widgets */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
A80D790E2D2F217000A40F40 /* PaginationDataHolder.swift */,
D577AB7E2A96878A007FE952 /* AppDetailWidget.swift */, D577AB7E2A96878A007FE952 /* AppDetailWidget.swift */,
D55467C42A8D72C300F4CE90 /* ActiveAppsWidget.swift */, D55467C42A8D72C300F4CE90 /* ActiveAppsWidget.swift */,
BF98917D250AAC4F002ACF50 /* LockScreenWidget.swift */, BF98917D250AAC4F002ACF50 /* LockScreenWidget.swift */,
@@ -2912,13 +2937,16 @@
D577AB7F2A96878A007FE952 /* AppDetailWidget.swift in Sources */, D577AB7F2A96878A007FE952 /* AppDetailWidget.swift in Sources */,
BF98917E250AAC4F002ACF50 /* Countdown.swift in Sources */, BF98917E250AAC4F002ACF50 /* Countdown.swift in Sources */,
D5151BE22A90363300C96F28 /* RefreshAllAppsWidgetIntent.swift in Sources */, D5151BE22A90363300C96F28 /* RefreshAllAppsWidgetIntent.swift in Sources */,
A8096D1C2D30ADA9000C39C6 /* ActiveAppsTimelineProvider+Simulator.swift in Sources */,
A80D790D2D2F20AF00A40F40 /* PaginationIntent.swift in Sources */, A80D790D2D2F20AF00A40F40 /* PaginationIntent.swift in Sources */,
D5FD4EC92A9530C00097BEE8 /* AppSnapshot.swift in Sources */, D5FD4EC92A9530C00097BEE8 /* AppSnapshot.swift in Sources */,
D5151BE72A90395400C96F28 /* View+AltWidget.swift in Sources */, D5151BE72A90395400C96F28 /* View+AltWidget.swift in Sources */,
A8A853AF2D3065A300995795 /* ActiveAppsTimelineProvider.swift in Sources */,
BF98917F250AAC4F002ACF50 /* LockScreenWidget.swift in Sources */, BF98917F250AAC4F002ACF50 /* LockScreenWidget.swift in Sources */,
A800F7042CE28E3800208744 /* View+AltWidget.swift in Sources */, A800F7042CE28E3800208744 /* View+AltWidget.swift in Sources */,
BF98917F250AAC4F002ACF50 /* LockScreenWidget.swift in Sources */, BF98917F250AAC4F002ACF50 /* LockScreenWidget.swift in Sources */,
D5FD4EC52A952EAD0097BEE8 /* AltWidgetBundle.swift in Sources */, D5FD4EC52A952EAD0097BEE8 /* AltWidgetBundle.swift in Sources */,
A8096D182D30AD4F000C39C6 /* WidgetUpdateIntent.swift in Sources */,
A80D790F2D2F217000A40F40 /* PaginationDataHolder.swift in Sources */, A80D790F2D2F217000A40F40 /* PaginationDataHolder.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;

View File

@@ -7,7 +7,6 @@
// //
import AppIntents import AppIntents
import WidgetKit
public enum Direction: String, Sendable{ public enum Direction: String, Sendable{
case up = "up" case up = "up"
@@ -15,21 +14,22 @@ public enum Direction: String, Sendable{
} }
@available(iOS 17, *) @available(iOS 17, *)
class PaginationIntent: AppIntent, @unchecked Sendable { final class PaginationIntent: AppIntent, @unchecked Sendable {
static var title: LocalizedStringResource { "Scroll up or down in Active Apps Widget" }
static var title: LocalizedStringResource { "Page Navigation Intent" }
static var isDiscoverable: Bool { false } static var isDiscoverable: Bool { false }
@Parameter(title: "Direction") @Parameter(title: "Direction")
var direction: String var direction: String
@Parameter(title: "Widget Identifier") @Parameter(title: "WidgetID")
var widgetID: String var widgetID: String
private lazy var widgetHolderQ = { var uuid: String = UUID().uuidString
DispatchQueue(label: widgetID)
}()
required init(){} required init(){
print()
}
init(_ direction: Direction, _ widgetID: String){ init(_ direction: Direction, _ widgetID: String){
self.direction = direction.rawValue self.direction = direction.rawValue
@@ -37,21 +37,19 @@ class PaginationIntent: AppIntent, @unchecked Sendable {
} }
func perform() async throws -> some IntentResult { func perform() async throws -> some IntentResult {
guard let direction = Direction(rawValue: self.direction) else { // if let widgetID = self.widgetID
return .result() // {
} // WidgetCenter.shared.reloadTimelines(ofKind: widgetID)
// }
// return .result()
widgetHolderQ.sync { let result = try await WidgetUpdateIntent(
// update direction for this widgetID Direction(rawValue: self.direction),
let dataholder = PaginationDataHolder.holder(for: self.widgetID) self.widgetID
dataholder?.updateDirection(direction) ).perform()
// ask widget views to be re-drawn by triggering timeline update return result
// for the widget uniquely identified by the 'kind: widgetID'
WidgetCenter.shared.reloadTimelines(ofKind: self.widgetID)
}
return .result()
} }
} }

View File

@@ -0,0 +1,42 @@
//
// WidgetUpdateIntent.swift
// AltStore
//
// Created by Magesh K on 10/01/25.
// Copyright © 2025 SideStore. All rights reserved.
//
import AppIntents
@available(iOS 17, *)
final class WidgetUpdateIntent: WidgetConfigurationIntent, @unchecked Sendable {
static var title: LocalizedStringResource { "Intent for WidgetAppIntentConfiguration receiver type" }
static var isDiscoverable: Bool { false }
var uuid: String = UUID().uuidString
private var widgetID: String?
@Parameter(title: "ID", description: "Change this to unique ID to keep changes isolated from other widgets", default: "1")
var ID: String?
// this static hack is required, coz we are making these intents stateful
private static var directionMap: [String: Direction] = [:]
init(){
print()
}
func getDirection( _ widgetID: String) -> Direction? {
// remove it, since the event is processed. if needed it will be added again
return Self.directionMap.removeValue(forKey: widgetID)
}
init(_ direction: Direction?, _ widgetID: String){
Self.directionMap[widgetID] = direction
}
func perform() async throws -> some IntentResult {
return .result()
}
}

View File

@@ -0,0 +1,36 @@
//
// ActiveAppsTimelineProvider+Simulator.swift
// AltStore
//
// Created by Magesh K on 10/01/25.
// Copyright © 2025 SideStore. All rights reserved.
//
/// Simulator data generator
#if DEBUG
@available(iOS 17, *)
extension ActiveAppsTimelineProvider {
func getSimulatedData(apps: [AppSnapshot]) -> [AppSnapshot]{
var apps = apps
var newSets: [AppSnapshot] = []
// this dummy data is for simulator (uncomment when testing ActiveAppsWidget pagination)
if (apps.count > 0){
let app = apps[0]
for i in 0..<10 {
let name = "\(app.name) - \(i)"
let x = AppSnapshot(name: name,
bundleIdentifier: app.bundleIdentifier,
expirationDate: app.expirationDate,
refreshedDate: app.refreshedDate
)
newSets.append(x)
}
apps = newSets
}
return apps
}
}
#endif

View File

@@ -0,0 +1,85 @@
//
// ActiveAppsTimelineProvider.swift
// AltStore
//
// Created by Magesh K on 10/01/25.
// Copyright © 2025 SideStore. All rights reserved.
//
import WidgetKit
protocol Navigation{
var direction: Direction? { get }
}
@available(iOS 17, *)
class ActiveAppsTimelineProvider: AppsTimelineProviderBase<Navigation> {
let uuid = UUID().uuidString
private let dataHolder: PaginationDataHolder
private let widgetID: String
init(kind: String){
print("Executing ActiveAppsTimelineProvider.init() for instance \(uuid)")
let itemsPerPage = ActiveAppsWidget.Constants.MAX_ROWS_PER_PAGE
self.dataHolder = PaginationDataHolder(itemsPerPage: itemsPerPage)
self.widgetID = kind
}
override func getUpdatedData(_ apps: [AppSnapshot], _ context: Navigation?) -> [AppSnapshot] {
guard let context = context else { return apps }
var apps = apps
// #if DEBUG
// apps = getSimulatedData(apps: apps)
// #endif
if let direction = context.direction{
// get paged data if available
switch (direction){
case Direction.up:
apps = dataHolder.prevPage(inItems: apps, whenUnavailable: .current)!
case Direction.down:
apps = dataHolder.nextPage(inItems: apps, whenUnavailable: .current)!
}
}else{
// retain what ever page we were on as-is
apps = dataHolder.currentPage(inItems: apps)
}
return apps
}
}
@available(iOS 17, *)
extension ActiveAppsTimelineProvider: AppIntentTimelineProvider {
struct IntentData: Navigation{
let direction: Direction?
}
typealias Intent = WidgetUpdateIntent
func snapshot(for intent: Intent, in context: Context) async -> AppsEntry {
let data = IntentData(direction: intent.getDirection(widgetID))
let bundleIDs = await super.fetchActiveAppBundleIDs()
let snapshot = await self.snapshot(for: bundleIDs, in: data)
return snapshot
}
func timeline(for intent: Intent, in context: Context) async -> Timeline<AppsEntry> {
let data = IntentData(direction: intent.getDirection(widgetID))
let bundleIDs = await self.fetchActiveAppBundleIDs()
let timeline = await self.timeline(for: bundleIDs, in: data)
return timeline
}
}

View File

@@ -20,22 +20,16 @@ struct AppsEntry: TimelineEntry
var isPlaceholder: Bool = false var isPlaceholder: Bool = false
} }
struct AppsTimelineProvider class AppsTimelineProviderBase<T>
{ {
typealias Entry = AppsEntry typealias Entry = AppsEntry
private var dataHolder: PaginationDataHolder?
init(_ dataHolder: PaginationDataHolder? = nil){
self.dataHolder = dataHolder
}
func placeholder(in context: TimelineProviderContext) -> AppsEntry func placeholder(in context: TimelineProviderContext) -> AppsEntry
{ {
return AppsEntry(date: Date(), apps: [], isPlaceholder: true) return AppsEntry(date: Date(), apps: [], isPlaceholder: true)
} }
func snapshot(for appBundleIDs: [String]) async -> AppsEntry func snapshot(for appBundleIDs: [String], in context: T? = nil) async -> AppsEntry
{ {
do do
{ {
@@ -43,7 +37,7 @@ struct AppsTimelineProvider
var apps = try await self.fetchApps(withBundleIDs: appBundleIDs) var apps = try await self.fetchApps(withBundleIDs: appBundleIDs)
apps = getUpdatedData(apps) apps = getUpdatedData(apps, context)
let entry = AppsEntry(date: Date(), apps: apps) let entry = AppsEntry(date: Date(), apps: apps)
return entry return entry
@@ -57,7 +51,7 @@ struct AppsTimelineProvider
} }
} }
func timeline(for appBundleIDs: [String]) async -> Timeline<AppsEntry> func timeline(for appBundleIDs: [String], in context: T? = nil) async -> Timeline<AppsEntry>
{ {
do do
{ {
@@ -65,7 +59,7 @@ struct AppsTimelineProvider
var apps = try await self.fetchApps(withBundleIDs: appBundleIDs) var apps = try await self.fetchApps(withBundleIDs: appBundleIDs)
apps = getUpdatedData(apps) apps = getUpdatedData(apps, context)
let entries = self.makeEntries(for: apps) let entries = self.makeEntries(for: apps)
let timeline = Timeline(entries: entries, policy: .atEnd) let timeline = Timeline(entries: entries, policy: .atEnd)
@@ -80,32 +74,22 @@ struct AppsTimelineProvider
return timeline return timeline
} }
} }
}
private extension AppsTimelineProvider func getUpdatedData(_ apps: [AppSnapshot], _ context: T?) -> [AppSnapshot]{
{ // override in subclasses as required
private func getUpdatedData(_ apps: [AppSnapshot]) -> [AppSnapshot]{
var apps = apps
// #if DEBUG
// // this dummy data is for simulator (uncomment when testing ActiveAppsWidget pagination)
// apps = apps + apps + apps + apps + apps + apps + apps + apps
// #endif
if let dataHolder{
// get paged data if present if available
apps = dataHolder.getUpdatedData(entries: apps)
}
return apps return apps
} }
}
func prepare() async throws extension AppsTimelineProviderBase
{
private func prepare() async throws
{ {
try await DatabaseManager.shared.start() try await DatabaseManager.shared.start()
} }
func fetchApps(withBundleIDs bundleIDs: [String]) async throws -> [AppSnapshot] private func fetchApps(withBundleIDs bundleIDs: [String]) async throws -> [AppSnapshot]
{ {
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext() let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
let apps = try await context.performAsync { let apps = try await context.performAsync {
@@ -176,31 +160,8 @@ private extension AppsTimelineProvider
return entries return entries
} }
}
extension AppsTimelineProvider: TimelineProvider func fetchActiveAppBundleIDs() async -> [String]
{
func getSnapshot(in context: Context, completion: @escaping (AppsEntry) -> Void)
{
Task<Void, Never> {
let bundleIDs = await self.fetchActiveAppBundleIDs()
let snapshot = await self.snapshot(for: bundleIDs)
completion(snapshot)
}
}
func getTimeline(in context: Context, completion: @escaping (Timeline<AppsEntry>) -> Void)
{
Task<Void, Never> {
let bundleIDs = await self.fetchActiveAppBundleIDs()
let timeline = await self.timeline(for: bundleIDs)
completion(timeline)
}
}
private func fetchActiveAppBundleIDs() async -> [String]
{ {
do do
{ {
@@ -227,9 +188,8 @@ extension AppsTimelineProvider: TimelineProvider
} }
} }
extension AppsTimelineProvider: IntentTimelineProvider class AppsTimelineProvider: AppsTimelineProviderBase<ViewAppIntent>, IntentTimelineProvider
{ {
typealias Intent = ViewAppIntent typealias Intent = ViewAppIntent
func getSnapshot(for intent: Intent, in context: Context, completion: @escaping (AppsEntry) -> Void) func getSnapshot(for intent: Intent, in context: Context, completion: @escaping (AppsEntry) -> Void)
@@ -237,7 +197,7 @@ extension AppsTimelineProvider: IntentTimelineProvider
Task<Void, Never> { Task<Void, Never> {
let bundleIDs = [intent.app?.identifier ?? StoreApp.altstoreAppID] let bundleIDs = [intent.app?.identifier ?? StoreApp.altstoreAppID]
let snapshot = await self.snapshot(for: bundleIDs) let snapshot = await self.snapshot(for: bundleIDs, in: intent)
completion(snapshot) completion(snapshot)
} }
} }
@@ -247,7 +207,7 @@ extension AppsTimelineProvider: IntentTimelineProvider
Task<Void, Never> { Task<Void, Never> {
let bundleIDs = [intent.app?.identifier ?? StoreApp.altstoreAppID] let bundleIDs = [intent.app?.identifier ?? StoreApp.altstoreAppID]
let timeline = await self.timeline(for: bundleIDs) let timeline = await self.timeline(for: bundleIDs, in: intent)
completion(timeline) completion(timeline)
} }
} }

View File

@@ -9,6 +9,8 @@
import SwiftUI import SwiftUI
import WidgetKit import WidgetKit
import AltStoreCore
private extension Color private extension Color
{ {
static let altGradientLight = Color.init(.displayP3, red: 123.0/255.0, green: 200.0/255.0, blue: 176.0/255.0) static let altGradientLight = Color.init(.displayP3, red: 123.0/255.0, green: 200.0/255.0, blue: 176.0/255.0)
@@ -17,44 +19,36 @@ private extension Color
static let altGradientExtraDark = Color.init(.displayP3, red: 2.0/255.0, green: 82.0/255.0, blue: 103.0/255.0) static let altGradientExtraDark = Color.init(.displayP3, red: 2.0/255.0, green: 82.0/255.0, blue: 103.0/255.0)
} }
//@available(iOS 17, *) //@available(iOS 17, *)
struct ActiveAppsWidget: Widget struct ActiveAppsWidget: Widget
{ {
// only constants/singleton what needs to be for the life of all widgets of ActiveAppsWidget type struct Constants{
// should be declared as instance or class fields for ActiveAppWidgets static let MAX_ROWS_PER_PAGE: UInt = 3
}
let ID = UUID().uuidString
// NOTE: The computed property (widget)body is recomputed/re-run for every
// 'type' refers to struct/class types and 'kind' refers to the tag which the widget is marked with
// multiple instances of same type or multiple types can be tagged with same 'kind'
// or each instance of same kind too, can be tagged as different(unique) 'kind'
// 1. widget-resizing(of same type)
// 2. new widget addition(of same type)
public var body: some WidgetConfiguration { public var body: some WidgetConfiguration {
print("Executing ActiveAppsWidget.body for instance \(ID)")
if #available(iOS 17, *) if #available(iOS 17, *)
{ {
let kind = "ActiveApps" let widgetID = "ActiveApps - \(UUID().uuidString)"
let widgetID = kind + "-" + UUID().uuidString
let holder = PaginationDataHolder.instance(widgetID) let widgetConfig = AppIntentConfiguration(
let timelineProvider = AppsTimelineProvider(holder) // pass the holder
// each instance of this widget type is identified by unique 'kind' tag
// so that a reloadTimelineFor(kind:) will trigger reload only for that instance
let staticConfig = StaticConfiguration(
kind: widgetID, kind: widgetID,
provider: timelineProvider // intent: PaginationIntent.self, // Use the defined AppIntent
intent: WidgetUpdateIntent.self, // Use the defined AppIntent
provider: ActiveAppsTimelineProvider(kind: widgetID)
) { entry in ) { entry in
// actual view of the widget
// this gets recreated for each trigger from the scheduled timeline entries provided by the timeline provider
// NOTE: widget views do not adhere to statefulness
// so, Combine constructs such as @State, @StateObject, @ObservedObject etc are simply ignored
ActiveAppsWidgetView(entry: entry, widgetID: widgetID) ActiveAppsWidgetView(entry: entry, widgetID: widgetID)
} }
.supportedFamilies([.systemMedium]) .supportedFamilies([.systemMedium])
.configurationDisplayName("Active Apps") .configurationDisplayName("Active Apps")
.description("View remaining days until your active apps expire. Tap the countdown timers to refresh them in the background.") .description("View remaining days until your active apps expire. Tap the countdown timers to refresh them in the background.")
return staticConfig return widgetConfig
} }
else else
{ {
@@ -71,6 +65,15 @@ private struct ActiveAppsWidgetView: View
var entry: AppsEntry var entry: AppsEntry
var widgetID: String var widgetID: String
let ID = UUID().uuidString
init(entry: AppsEntry, widgetID: String) {
print("Executing ActiveAppsWidgetView.init() for instance \(ID)")
self.entry = entry
self.widgetID = widgetID
}
@Environment(\.colorScheme) @Environment(\.colorScheme)
private var colorScheme private var colorScheme
@@ -101,9 +104,9 @@ private struct ActiveAppsWidgetView: View
private var content: some View { private var content: some View {
GeometryReader { (geometry) in GeometryReader { (geometry) in
let MAX_ROWS_PER_PAGE = PaginationDataHolder.MAX_ROWS_PER_PAGE let itemsPerPage = ActiveAppsWidget.Constants.MAX_ROWS_PER_PAGE
let preferredRowHeight = (geometry.size.height / Double(MAX_ROWS_PER_PAGE)) - 8 let preferredRowHeight = (geometry.size.height / Double(itemsPerPage)) - 8
let rowHeight = min(preferredRowHeight, geometry.size.height / 2) let rowHeight = min(preferredRowHeight, geometry.size.height / 2)
HStack(alignment: .center) { HStack(alignment: .center) {

View File

@@ -1,72 +0,0 @@
//
// PaginationViewModel.swift
// AltStore
//
// Created by Magesh K on 09/01/25.
// Copyright © 2025 SideStore. All rights reserved.
//
import Foundation
class PaginationDataHolder: ObservableObject {
public static let MAX_ROWS_PER_PAGE: UInt = 3
private static var instances: [String:PaginationDataHolder] = [:]
public static func instance(_ widgetID: String) -> PaginationDataHolder {
let instance = PaginationDataHolder(widgetID)
Self.instances[widgetID] = instance
return instance
}
public static func holder(for widgetID: String) -> PaginationDataHolder? {
return Self.instances[widgetID]
}
private lazy var serializationQ = {
DispatchQueue(label: widgetID)
}()
public let widgetID: String
private var currentPageindex: UInt = 0
private init(_ widgetID: String){
self.widgetID = widgetID
}
deinit {
Self.instances.removeValue(forKey: widgetID)
}
public func updateDirection(_ direction: Direction) {
switch(direction){
case .up:
let pageIndex = Int(currentPageindex)
currentPageindex = UInt(max(0, pageIndex-1))
case .down:
// upper-bounds is checked when computing targetPageIndex in getUpdatedData
currentPageindex+=1
}
}
public func getUpdatedData(entries: [AppSnapshot]) -> [AppSnapshot] {
let count = UInt(entries.count)
if(count == 0) { return entries }
let availablePages = UInt(ceil(Double(entries.count) / Double(Self.MAX_ROWS_PER_PAGE)))
let targetPageIndex: UInt = currentPageindex < availablePages ? currentPageindex : availablePages-1
// do blocking update
serializationQ.sync {
self.currentPageindex = targetPageIndex // preserve it
}
let startIndex = targetPageIndex * Self.MAX_ROWS_PER_PAGE
let estimatedEndIndex = startIndex + (Self.MAX_ROWS_PER_PAGE-1)
let endIndex: UInt = min(count-1, estimatedEndIndex)
let currentPageEntries = entries[Int(startIndex) ... Int(endIndex)]
return Array(currentPageEntries)
}
}

View File

@@ -0,0 +1,82 @@
//
// PaginationDataHolder.swift
// AltStore
//
// Created by Magesh K on 09/01/25.
// Copyright © 2025 SideStore. All rights reserved.
//
import Foundation
public class PaginationDataHolder {
public let itemsPerPage: UInt
public private(set) var currentPageindex: UInt
init(itemsPerPage: UInt, startPageIndex: UInt = 0) {
self.itemsPerPage = itemsPerPage
self.currentPageindex = startPageIndex
}
public enum PageLimitResult{
case null
case empty
case current
}
private func updatePageIndexForDirection(_ direction: Direction, itemsCount: Int) -> Bool {
var targetPageIndex = Int(currentPageindex)
let availablePages = UInt(ceil(Double(itemsCount) / Double(itemsPerPage)))
switch(direction){
case .up:
targetPageIndex -= 1
case .down:
targetPageIndex += 1
}
let isUpdateValid = (targetPageIndex >= 0 && targetPageIndex < availablePages)
if isUpdateValid{
self.currentPageindex = UInt(targetPageIndex)
}
return isUpdateValid
}
public func nextPage<T>(inItems: [T], whenUnavailable: PageLimitResult = .current) -> [T]? {
return targetPage(for: .down, inItems: inItems, whenUnavailable: whenUnavailable)
}
public func prevPage<T>(inItems: [T], whenUnavailable: PageLimitResult = .current) -> [T]? {
return targetPage(for: .up, inItems: inItems, whenUnavailable: whenUnavailable)
}
public func targetPage<T>(for direction: Direction, inItems: [T], whenUnavailable: PageLimitResult = .current) -> [T]? {
if updatePageIndexForDirection(direction, itemsCount: inItems.count){
return currentPage(inItems: inItems)
}
switch whenUnavailable {
case .null:
return nil // null was requested
case .empty:
return [] // empty list was requested
case .current:
return currentPage(inItems: inItems) // Stay on the current page and return the same items
}
}
public func currentPage<T>(inItems items: [T]) -> [T] {
let count = UInt(items.count)
if(count == 0) { return items }
let startIndex = currentPageindex * itemsPerPage
let estimatedEndIndex = startIndex + (itemsPerPage-1)
let endIndex: UInt = min(count-1, estimatedEndIndex)
let currentPageEntries = items[Int(startIndex) ... Int(endIndex)]
return Array(currentPageEntries)
}
}