diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index e8a752b5..d1e42dce 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -51,6 +51,8 @@ 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 */; }; 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 */; }; 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 */; }; @@ -63,6 +65,7 @@ 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 */; }; 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 */; }; A8B516E62D2668170047047C /* DateTimeUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8B516E52D2668020047047C /* DateTimeUtil.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 = ""; }; 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 = ""; }; + A8096D172D30AD4F000C39C6 /* WidgetUpdateIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetUpdateIntent.swift; sourceTree = ""; }; + A8096D1B2D30ADA9000C39C6 /* ActiveAppsTimelineProvider+Simulator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ActiveAppsTimelineProvider+Simulator.swift"; sourceTree = ""; }; A809F6A22D04DA1900F0F0F3 /* minimuxer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = minimuxer.h; sourceTree = ""; }; A809F6A32D04DA1900F0F0F3 /* minimuxer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = minimuxer.swift; sourceTree = ""; }; A809F6A42D04DA1900F0F0F3 /* minimuxer-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "minimuxer-Bridging-Header.h"; sourceTree = ""; }; @@ -638,6 +643,7 @@ A86202322D1F35640091187B /* AltStore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltStore.xcconfig; sourceTree = ""; }; A86202332D1F35640091187B /* AltStoreCore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltStoreCore.xcconfig; sourceTree = ""; }; 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 = ""; }; A8B516E22D2666CA0047047C /* CoreDataHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataHelper.swift; sourceTree = ""; }; A8B516E52D2668020047047C /* DateTimeUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimeUtil.swift; sourceTree = ""; }; A8C38C1D2D206A3A00E83DBD /* ConsoleLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLogger.swift; sourceTree = ""; }; @@ -1148,6 +1154,16 @@ path = importexport; sourceTree = ""; }; + A8096D1D2D30ADD5000C39C6 /* Providers */ = { + isa = PBXGroup; + children = ( + D577AB7A2A967DF5007FE952 /* AppsTimelineProvider.swift */, + A8A853AE2D3065A300995795 /* ActiveAppsTimelineProvider.swift */, + A8096D1B2D30ADA9000C39C6 /* ActiveAppsTimelineProvider+Simulator.swift */, + ); + path = Providers; + sourceTree = ""; + }; A809F68A2D04D71200F0F0F3 /* Products */ = { isa = PBXGroup; children = ( @@ -1168,6 +1184,7 @@ A80D790B2D2F209700A40F40 /* Intents */ = { isa = PBXGroup; children = ( + A8096D172D30AD4F000C39C6 /* WidgetUpdateIntent.swift */, A80D790C2D2F20AF00A40F40 /* PaginationIntent.swift */, ); path = Intents; @@ -1199,6 +1216,14 @@ name = Products; sourceTree = ""; }; + A8A853AD2D3050CC00995795 /* pagination */ = { + isa = PBXGroup; + children = ( + A80D790E2D2F217000A40F40 /* PaginationDataHolder.swift */, + ); + path = pagination; + sourceTree = ""; + }; A8B516DE2D2666900047047C /* dignostics */ = { isa = PBXGroup; children = ( @@ -1218,6 +1243,7 @@ A8C38C1C2D2068D100E83DBD /* Utils */ = { isa = PBXGroup; children = ( + A8A853AD2D3050CC00995795 /* pagination */, A8087E712D2D291B002DB21B /* importexport */, A8B516DE2D2666900047047C /* dignostics */, A8C38C272D206AA500E83DBD /* common */, @@ -1706,10 +1732,10 @@ isa = PBXGroup; children = ( A80D790B2D2F209700A40F40 /* Intents */, + A8096D1D2D30ADD5000C39C6 /* Providers */, A800F6FE2CE28DE300208744 /* Extensions */, BF8B17F0250AC62400F8157F /* AltWidgetExtension.entitlements */, D5FD4EC42A952EAD0097BEE8 /* AltWidgetBundle.swift */, - D577AB7A2A967DF5007FE952 /* AppsTimelineProvider.swift */, D50C29F22A8ECD71009AB488 /* Widgets */, D51AF9752A97D29100471312 /* Model */, D577AB802A968B7E007FE952 /* Components */, @@ -2080,7 +2106,6 @@ D50C29F22A8ECD71009AB488 /* Widgets */ = { isa = PBXGroup; children = ( - A80D790E2D2F217000A40F40 /* PaginationDataHolder.swift */, D577AB7E2A96878A007FE952 /* AppDetailWidget.swift */, D55467C42A8D72C300F4CE90 /* ActiveAppsWidget.swift */, BF98917D250AAC4F002ACF50 /* LockScreenWidget.swift */, @@ -2912,13 +2937,16 @@ D577AB7F2A96878A007FE952 /* AppDetailWidget.swift in Sources */, BF98917E250AAC4F002ACF50 /* Countdown.swift in Sources */, D5151BE22A90363300C96F28 /* RefreshAllAppsWidgetIntent.swift in Sources */, + A8096D1C2D30ADA9000C39C6 /* ActiveAppsTimelineProvider+Simulator.swift in Sources */, A80D790D2D2F20AF00A40F40 /* PaginationIntent.swift in Sources */, D5FD4EC92A9530C00097BEE8 /* AppSnapshot.swift in Sources */, D5151BE72A90395400C96F28 /* View+AltWidget.swift in Sources */, + A8A853AF2D3065A300995795 /* ActiveAppsTimelineProvider.swift in Sources */, BF98917F250AAC4F002ACF50 /* LockScreenWidget.swift in Sources */, A800F7042CE28E3800208744 /* View+AltWidget.swift in Sources */, BF98917F250AAC4F002ACF50 /* LockScreenWidget.swift in Sources */, D5FD4EC52A952EAD0097BEE8 /* AltWidgetBundle.swift in Sources */, + A8096D182D30AD4F000C39C6 /* WidgetUpdateIntent.swift in Sources */, A80D790F2D2F217000A40F40 /* PaginationDataHolder.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/AltWidget/Intents/PaginationIntent.swift b/AltWidget/Intents/PaginationIntent.swift index bde9b99f..48041467 100644 --- a/AltWidget/Intents/PaginationIntent.swift +++ b/AltWidget/Intents/PaginationIntent.swift @@ -7,7 +7,6 @@ // import AppIntents -import WidgetKit public enum Direction: String, Sendable{ case up = "up" @@ -15,43 +14,42 @@ public enum Direction: String, Sendable{ } @available(iOS 17, *) -class PaginationIntent: AppIntent, @unchecked Sendable { - static var title: LocalizedStringResource { "Scroll up or down in Active Apps Widget" } +final class PaginationIntent: AppIntent, @unchecked Sendable { + + static var title: LocalizedStringResource { "Page Navigation Intent" } static var isDiscoverable: Bool { false } @Parameter(title: "Direction") var direction: String - @Parameter(title: "Widget Identifier") + @Parameter(title: "WidgetID") var widgetID: String - - private lazy var widgetHolderQ = { - DispatchQueue(label: widgetID) - }() - required init(){} + var uuid: String = UUID().uuidString + + required init(){ + print() + } init(_ direction: Direction, _ widgetID: String){ self.direction = direction.rawValue self.widgetID = widgetID } - + func perform() async throws -> some IntentResult { - guard let direction = Direction(rawValue: self.direction) else { - return .result() - } - - widgetHolderQ.sync { - // update direction for this widgetID - let dataholder = PaginationDataHolder.holder(for: self.widgetID) - dataholder?.updateDirection(direction) +// if let widgetID = self.widgetID +// { +// WidgetCenter.shared.reloadTimelines(ofKind: widgetID) +// } +// return .result() - // ask widget views to be re-drawn by triggering timeline update - // for the widget uniquely identified by the 'kind: widgetID' - WidgetCenter.shared.reloadTimelines(ofKind: self.widgetID) - } + let result = try await WidgetUpdateIntent( + Direction(rawValue: self.direction), + self.widgetID + ).perform() - return .result() + return result } } + diff --git a/AltWidget/Intents/WidgetUpdateIntent.swift b/AltWidget/Intents/WidgetUpdateIntent.swift new file mode 100644 index 00000000..8781d6b2 --- /dev/null +++ b/AltWidget/Intents/WidgetUpdateIntent.swift @@ -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() + } +} diff --git a/AltWidget/Providers/ActiveAppsTimelineProvider+Simulator.swift b/AltWidget/Providers/ActiveAppsTimelineProvider+Simulator.swift new file mode 100644 index 00000000..f074e7a1 --- /dev/null +++ b/AltWidget/Providers/ActiveAppsTimelineProvider+Simulator.swift @@ -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 + diff --git a/AltWidget/Providers/ActiveAppsTimelineProvider.swift b/AltWidget/Providers/ActiveAppsTimelineProvider.swift new file mode 100644 index 00000000..ce02bd09 --- /dev/null +++ b/AltWidget/Providers/ActiveAppsTimelineProvider.swift @@ -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 { + + 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 { + let data = IntentData(direction: intent.getDirection(widgetID)) + + let bundleIDs = await self.fetchActiveAppBundleIDs() + + let timeline = await self.timeline(for: bundleIDs, in: data) + + return timeline + } +} diff --git a/AltWidget/AppsTimelineProvider.swift b/AltWidget/Providers/AppsTimelineProvider.swift similarity index 77% rename from AltWidget/AppsTimelineProvider.swift rename to AltWidget/Providers/AppsTimelineProvider.swift index e44ac268..eee98ef4 100644 --- a/AltWidget/AppsTimelineProvider.swift +++ b/AltWidget/Providers/AppsTimelineProvider.swift @@ -20,22 +20,16 @@ struct AppsEntry: TimelineEntry var isPlaceholder: Bool = false } -struct AppsTimelineProvider +class AppsTimelineProviderBase { typealias Entry = AppsEntry - private var dataHolder: PaginationDataHolder? - - init(_ dataHolder: PaginationDataHolder? = nil){ - self.dataHolder = dataHolder - } - func placeholder(in context: TimelineProviderContext) -> AppsEntry { 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 { @@ -43,7 +37,7 @@ struct AppsTimelineProvider var apps = try await self.fetchApps(withBundleIDs: appBundleIDs) - apps = getUpdatedData(apps) + apps = getUpdatedData(apps, context) let entry = AppsEntry(date: Date(), apps: apps) return entry @@ -57,7 +51,7 @@ struct AppsTimelineProvider } } - func timeline(for appBundleIDs: [String]) async -> Timeline + func timeline(for appBundleIDs: [String], in context: T? = nil) async -> Timeline { do { @@ -65,7 +59,7 @@ struct AppsTimelineProvider var apps = try await self.fetchApps(withBundleIDs: appBundleIDs) - apps = getUpdatedData(apps) + apps = getUpdatedData(apps, context) let entries = self.makeEntries(for: apps) let timeline = Timeline(entries: entries, policy: .atEnd) @@ -80,32 +74,22 @@ struct AppsTimelineProvider return timeline } } -} - -private extension AppsTimelineProvider -{ - 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) - } + func getUpdatedData(_ apps: [AppSnapshot], _ context: T?) -> [AppSnapshot]{ + // override in subclasses as required return apps } +} + +extension AppsTimelineProviderBase +{ - func prepare() async throws + private func prepare() async throws { 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 apps = try await context.performAsync { @@ -176,31 +160,8 @@ private extension AppsTimelineProvider return entries } -} - -extension AppsTimelineProvider: TimelineProvider -{ - func getSnapshot(in context: Context, completion: @escaping (AppsEntry) -> Void) - { - Task { - let bundleIDs = await self.fetchActiveAppBundleIDs() - - let snapshot = await self.snapshot(for: bundleIDs) - completion(snapshot) - } - } - func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) - { - Task { - let bundleIDs = await self.fetchActiveAppBundleIDs() - - let timeline = await self.timeline(for: bundleIDs) - completion(timeline) - } - } - - private func fetchActiveAppBundleIDs() async -> [String] + func fetchActiveAppBundleIDs() async -> [String] { do { @@ -227,9 +188,8 @@ extension AppsTimelineProvider: TimelineProvider } } -extension AppsTimelineProvider: IntentTimelineProvider +class AppsTimelineProvider: AppsTimelineProviderBase, IntentTimelineProvider { - typealias Intent = ViewAppIntent func getSnapshot(for intent: Intent, in context: Context, completion: @escaping (AppsEntry) -> Void) @@ -237,7 +197,7 @@ extension AppsTimelineProvider: IntentTimelineProvider Task { 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) } } @@ -247,7 +207,7 @@ extension AppsTimelineProvider: IntentTimelineProvider Task { 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) } } diff --git a/AltWidget/Widgets/ActiveAppsWidget.swift b/AltWidget/Widgets/ActiveAppsWidget.swift index 6ccb9c19..7d937624 100644 --- a/AltWidget/Widgets/ActiveAppsWidget.swift +++ b/AltWidget/Widgets/ActiveAppsWidget.swift @@ -9,6 +9,8 @@ import SwiftUI import WidgetKit +import AltStoreCore + private extension Color { 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) } + //@available(iOS 17, *) struct ActiveAppsWidget: Widget { - // only constants/singleton what needs to be for the life of all widgets of ActiveAppsWidget type - // should be declared as instance or class fields for ActiveAppWidgets + struct Constants{ + static let MAX_ROWS_PER_PAGE: UInt = 3 + } - // 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) + let ID = UUID().uuidString + public var body: some WidgetConfiguration { + print("Executing ActiveAppsWidget.body for instance \(ID)") + if #available(iOS 17, *) { - let kind = "ActiveApps" - let widgetID = kind + "-" + UUID().uuidString + let widgetID = "ActiveApps - \(UUID().uuidString)" - let holder = PaginationDataHolder.instance(widgetID) - 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( + let widgetConfig = AppIntentConfiguration( kind: widgetID, - provider: timelineProvider +// intent: PaginationIntent.self, // Use the defined AppIntent + intent: WidgetUpdateIntent.self, // Use the defined AppIntent + provider: ActiveAppsTimelineProvider(kind: widgetID) ) { 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) } .supportedFamilies([.systemMedium]) .configurationDisplayName("Active Apps") .description("View remaining days until your active apps expire. Tap the countdown timers to refresh them in the background.") - return staticConfig + return widgetConfig } else { @@ -71,6 +65,15 @@ private struct ActiveAppsWidgetView: View var entry: AppsEntry 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) private var colorScheme @@ -101,9 +104,9 @@ private struct ActiveAppsWidgetView: View private var content: some View { 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) HStack(alignment: .center) { diff --git a/AltWidget/Widgets/PaginationDataHolder.swift b/AltWidget/Widgets/PaginationDataHolder.swift deleted file mode 100644 index d934fdbd..00000000 --- a/AltWidget/Widgets/PaginationDataHolder.swift +++ /dev/null @@ -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) - } -} diff --git a/SideStore/Utils/pagination/PaginationDataHolder.swift b/SideStore/Utils/pagination/PaginationDataHolder.swift new file mode 100644 index 00000000..97231e4b --- /dev/null +++ b/SideStore/Utils/pagination/PaginationDataHolder.swift @@ -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(inItems: [T], whenUnavailable: PageLimitResult = .current) -> [T]? { + return targetPage(for: .down, inItems: inItems, whenUnavailable: whenUnavailable) + } + + public func prevPage(inItems: [T], whenUnavailable: PageLimitResult = .current) -> [T]? { + return targetPage(for: .up, inItems: inItems, whenUnavailable: whenUnavailable) + } + + public func targetPage(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(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) + } +}