From ea2600aba91a385a11a8e88bd1a93ba508241150 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Fri, 1 Sep 2023 13:27:28 -0500 Subject: [PATCH] [AltWidget] Adds interactive Active Apps widget to view + refresh all active apps (iOS 17+) --- AltStore.xcodeproj/project.pbxproj | 20 ++ .../Extensions/UIColor+AltStore.swift | 3 +- .../Model/DatabaseManager+Async.swift | 28 +++ .../Contents.json | 6 +- AltWidget/AltWidgetBundle.swift | 5 + AltWidget/AppsTimelineProvider.swift | 202 ++++++++++++++++++ AltWidget/Components/Countdown.swift | 6 +- AltWidget/Model/AppSnapshot.swift | 48 +++++ .../Clip.imageset/ClipIcon@2x.png | Bin 0 -> 7204 bytes .../Clip.imageset/ClipIcon@3x.png | Bin 0 -> 16896 bytes .../Clip.imageset/Contents.json | 22 ++ AltWidget/Previews.xcassets/Contents.json | 6 + .../Delta.imageset/Contents.json | 0 .../Delta.imageset/icon-120.png | Bin .../Delta.imageset/icon-180.png | Bin AltWidget/Widgets/ActiveAppsWidget.swift | 158 ++++++++++++++ 16 files changed, 497 insertions(+), 7 deletions(-) create mode 100644 AltStoreCore/Model/DatabaseManager+Async.swift rename AltStoreCore/Resources/Colors.xcassets/{Pink.colorset => ClipPrimary.colorset}/Contents.json (74%) create mode 100644 AltWidget/AppsTimelineProvider.swift create mode 100644 AltWidget/Previews.xcassets/Clip.imageset/ClipIcon@2x.png create mode 100644 AltWidget/Previews.xcassets/Clip.imageset/ClipIcon@3x.png create mode 100644 AltWidget/Previews.xcassets/Clip.imageset/Contents.json create mode 100644 AltWidget/Previews.xcassets/Contents.json rename AltWidget/{Assets.xcassets => Previews.xcassets}/Delta.imageset/Contents.json (100%) rename AltWidget/{Assets.xcassets => Previews.xcassets}/Delta.imageset/icon-120.png (100%) rename AltWidget/{Assets.xcassets => Previews.xcassets}/Delta.imageset/icon-180.png (100%) create mode 100644 AltWidget/Widgets/ActiveAppsWidget.swift diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 26957f4c..153b27f4 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -365,12 +365,14 @@ D54DED1428CBC44B008B27A0 /* ErrorLogTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54DED1328CBC44B008B27A0 /* ErrorLogTableViewCell.swift */; }; D552B1D82A042A740066216F /* AppPermissionsCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = D552B1D72A042A740066216F /* AppPermissionsCard.swift */; }; D55467B82A8D5E2600F4CE90 /* AppShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55467B12A8D5E2600F4CE90 /* AppShortcuts.swift */; }; + D55467C52A8D72C300F4CE90 /* ActiveAppsWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55467C42A8D72C300F4CE90 /* ActiveAppsWidget.swift */; }; D561B2EB28EF5A4F006752E4 /* AltSign-Dynamic in Frameworks */ = {isa = PBXBuildFile; productRef = D561B2EA28EF5A4F006752E4 /* AltSign-Dynamic */; }; D561B2ED28EF5A4F006752E4 /* AltSign-Dynamic in Embed Frameworks */ = {isa = PBXBuildFile; productRef = D561B2EA28EF5A4F006752E4 /* AltSign-Dynamic */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; D5708417292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift */; }; D570841A2924680D00D42D34 /* OperatingSystemVersion+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift */; }; D571ADD02A02FC7200B24B63 /* ALTAppPermission.swift in Sources */ = {isa = PBXBuildFile; fileRef = D571ADCF2A02FC7200B24B63 /* ALTAppPermission.swift */; }; D5728CA72A0D79D30014E73C /* OptionalProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5728CA62A0D79D30014E73C /* OptionalProtocol.swift */; }; + D577AB7B2A967DF5007FE952 /* AppsTimelineProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D577AB7A2A967DF5007FE952 /* AppsTimelineProvider.swift */; }; D577AB7F2A96878A007FE952 /* AppDetailWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = D577AB7E2A96878A007FE952 /* AppDetailWidget.swift */; }; D57968CB29CB99EF00539069 /* VibrantButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57968CA29CB99EF00539069 /* VibrantButton.swift */; }; D57DF638271E32F000677701 /* PatchApp.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D57DF637271E32F000677701 /* PatchApp.storyboard */; }; @@ -412,8 +414,10 @@ D5F5AF7D28ECEA990067C736 /* ErrorDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F5AF7C28ECEA990067C736 /* ErrorDetailsViewController.swift */; }; D5F99A1828D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = D5F99A1728D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel */; }; D5F99A1A28D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F99A1928D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift */; }; + D5FB7A0E2AA25A4E00EF863D /* Previews.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D5FB7A0D2AA25A4E00EF863D /* Previews.xcassets */; }; D5FD4EC52A952EAD0097BEE8 /* AltWidgetBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FD4EC42A952EAD0097BEE8 /* AltWidgetBundle.swift */; }; D5FD4EC92A9530C00097BEE8 /* AppSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FD4EC82A9530C00097BEE8 /* AppSnapshot.swift */; }; + D5FD4ECB2A9532960097BEE8 /* DatabaseManager+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FD4ECA2A9532960097BEE8 /* DatabaseManager+Async.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -939,10 +943,12 @@ D54DED1328CBC44B008B27A0 /* ErrorLogTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorLogTableViewCell.swift; sourceTree = ""; }; D552B1D72A042A740066216F /* AppPermissionsCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPermissionsCard.swift; sourceTree = ""; }; D55467B12A8D5E2600F4CE90 /* AppShortcuts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppShortcuts.swift; sourceTree = ""; }; + D55467C42A8D72C300F4CE90 /* ActiveAppsWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveAppsWidget.swift; sourceTree = ""; }; D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OperatingSystemVersion+Comparable.swift"; sourceTree = ""; }; D571ADCD2A02FA7400B24B63 /* SourceError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceError.swift; sourceTree = ""; }; D571ADCF2A02FC7200B24B63 /* ALTAppPermission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ALTAppPermission.swift; sourceTree = ""; }; D5728CA62A0D79D30014E73C /* OptionalProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalProtocol.swift; sourceTree = ""; }; + D577AB7A2A967DF5007FE952 /* AppsTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppsTimelineProvider.swift; sourceTree = ""; }; D577AB7E2A96878A007FE952 /* AppDetailWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDetailWidget.swift; sourceTree = ""; }; D57968CA29CB99EF00539069 /* VibrantButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VibrantButton.swift; sourceTree = ""; }; D57DF637271E32F000677701 /* PatchApp.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = PatchApp.storyboard; sourceTree = ""; }; @@ -986,8 +992,10 @@ D5F5AF7C28ECEA990067C736 /* ErrorDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorDetailsViewController.swift; sourceTree = ""; }; D5F99A1728D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore10ToAltStore11.xcmappingmodel; sourceTree = ""; }; D5F99A1928D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreApp10ToStoreApp11Policy.swift; sourceTree = ""; }; + D5FB7A0D2AA25A4E00EF863D /* Previews.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Previews.xcassets; sourceTree = ""; }; D5FD4EC42A952EAD0097BEE8 /* AltWidgetBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AltWidgetBundle.swift; sourceTree = ""; }; D5FD4EC82A9530C00097BEE8 /* AppSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSnapshot.swift; sourceTree = ""; }; + D5FD4ECA2A9532960097BEE8 /* DatabaseManager+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DatabaseManager+Async.swift"; sourceTree = ""; }; EA79A60285C6AF5848AA16E9 /* Pods-AltStore.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AltStore.debug.xcconfig"; path = "Target Support Files/Pods-AltStore/Pods-AltStore.debug.xcconfig"; sourceTree = ""; }; FC3822AB1C4CF1D4CDF7445D /* Pods_AltServer.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AltServer.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -1507,6 +1515,7 @@ BF66EEC62501AECA007EE018 /* AppPermission.swift */, D52C08ED28AEC37A006C4AE5 /* AppVersion.swift */, BF66EECA2501AECA007EE018 /* DatabaseManager.swift */, + D5FD4ECA2A9532960097BEE8 /* DatabaseManager+Async.swift */, BF66EEC02501AECA007EE018 /* InstalledApp.swift */, BF66EECB2501AECA007EE018 /* InstalledExtension.swift */, D58916FD28C7C55C00E39C8B /* LoggedError.swift */, @@ -1618,11 +1627,13 @@ BF8B17F0250AC62400F8157F /* AltWidgetExtension.entitlements */, D5FD4EC42A952EAD0097BEE8 /* AltWidgetBundle.swift */, D586222D2AA115D000A493E1 /* Provider.swift */, + D577AB7A2A967DF5007FE952 /* AppsTimelineProvider.swift */, D50C29F22A8ECD71009AB488 /* Widgets */, D51AF9752A97D29100471312 /* Model */, D577AB802A968B7E007FE952 /* Components */, D5151BE42A9038FA00C96F28 /* Extensions */, BF989170250AABF4002ACF50 /* Assets.xcassets */, + D5FB7A0D2AA25A4E00EF863D /* Previews.xcassets */, BF989172250AABF4002ACF50 /* Info.plist */, ); path = AltWidget; @@ -1971,6 +1982,7 @@ isa = PBXGroup; children = ( D577AB7E2A96878A007FE952 /* AppDetailWidget.swift */, + D55467C42A8D72C300F4CE90 /* ActiveAppsWidget.swift */, BF98917D250AAC4F002ACF50 /* LockScreenWidget.swift */, ); path = Widgets; @@ -2487,6 +2499,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + D5FB7A0E2AA25A4E00EF863D /* Previews.xcassets in Resources */, BF989171250AABF4002ACF50 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2748,6 +2761,7 @@ D58916FE28C7C55C00E39C8B /* LoggedError.swift in Sources */, BFBF331B2526762200B7B8C9 /* AltStore8ToAltStore9.xcmappingmodel in Sources */, 0EE7FDC72BE8CF4100D1E390 /* ALTWrappedError.m in Sources */, + D5FD4ECB2A9532960097BEE8 /* DatabaseManager+Async.swift in Sources */, D5893F802A1419E800E767CD /* NSManagedObjectContext+Conveniences.swift in Sources */, D5CA0C4E280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel in Sources */, D51AD27F29356B7B00967AAA /* ALTWrappedError.m in Sources */, @@ -2800,6 +2814,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D55467C52A8D72C300F4CE90 /* ActiveAppsWidget.swift in Sources */, + D577AB7B2A967DF5007FE952 /* AppsTimelineProvider.swift in Sources */, D577AB7F2A96878A007FE952 /* AppDetailWidget.swift in Sources */, BF98917E250AAC4F002ACF50 /* Countdown.swift in Sources */, D5151BE22A90363300C96F28 /* RefreshAllAppsWidgetIntent.swift in Sources */, @@ -3408,6 +3424,8 @@ DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_DEBUG_DYLIB = NO; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; + CURRENT_PROJECT_VERSION = 6; + DEVELOPMENT_ASSET_PATHS = AltWidget/Previews.xcassets; INFOPLIST_FILE = AltWidget/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -3439,6 +3457,8 @@ DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_DEBUG_DYLIB = NO; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; + CURRENT_PROJECT_VERSION = 6; + DEVELOPMENT_ASSET_PATHS = AltWidget/Previews.xcassets; INFOPLIST_FILE = AltWidget/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", diff --git a/AltStoreCore/Extensions/UIColor+AltStore.swift b/AltStoreCore/Extensions/UIColor+AltStore.swift index 74af4c5d..0d82756c 100644 --- a/AltStoreCore/Extensions/UIColor+AltStore.swift +++ b/AltStoreCore/Extensions/UIColor+AltStore.swift @@ -14,8 +14,7 @@ public extension UIColor static let altPrimary = UIColor(named: "Primary", in: colorBundle, compatibleWith: nil)! static let deltaPrimary = UIColor(named: "DeltaPrimary", in: colorBundle, compatibleWith: nil) - - static let altPink = UIColor(named: "Pink", in: colorBundle, compatibleWith: nil)! + static let clipPrimary = UIColor(named: "ClipPrimary", in: colorBundle, compatibleWith: nil) static let refreshRed = UIColor(named: "RefreshRed", in: colorBundle, compatibleWith: nil)! static let refreshOrange = UIColor(named: "RefreshOrange", in: colorBundle, compatibleWith: nil)! diff --git a/AltStoreCore/Model/DatabaseManager+Async.swift b/AltStoreCore/Model/DatabaseManager+Async.swift new file mode 100644 index 00000000..f7cedd4e --- /dev/null +++ b/AltStoreCore/Model/DatabaseManager+Async.swift @@ -0,0 +1,28 @@ +// +// DatabaseManager+Async.swift +// AltStoreCore +// +// Created by Riley Testut on 8/22/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// + +import Foundation + +public extension DatabaseManager +{ + func start() async throws + { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + self.start { error in + if let error + { + continuation.resume(throwing: error) + } + else + { + continuation.resume() + } + } + } + } +} diff --git a/AltStoreCore/Resources/Colors.xcassets/Pink.colorset/Contents.json b/AltStoreCore/Resources/Colors.xcassets/ClipPrimary.colorset/Contents.json similarity index 74% rename from AltStoreCore/Resources/Colors.xcassets/Pink.colorset/Contents.json rename to AltStoreCore/Resources/Colors.xcassets/ClipPrimary.colorset/Contents.json index 4d3bc04a..e350f753 100644 --- a/AltStoreCore/Resources/Colors.xcassets/Pink.colorset/Contents.json +++ b/AltStoreCore/Resources/Colors.xcassets/ClipPrimary.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.698", - "green" : "0.255", - "red" : "0.925" + "blue" : "140", + "green" : "0", + "red" : "236" } }, "idiom" : "universal" diff --git a/AltWidget/AltWidgetBundle.swift b/AltWidget/AltWidgetBundle.swift index a3c36462..b8446a86 100644 --- a/AltWidget/AltWidgetBundle.swift +++ b/AltWidget/AltWidgetBundle.swift @@ -17,5 +17,10 @@ struct AltWidgetBundle: WidgetBundle IconLockScreenWidget() TextLockScreenWidget() + + if #available(iOS 17, *) + { + ActiveAppsWidget() + } } } diff --git a/AltWidget/AppsTimelineProvider.swift b/AltWidget/AppsTimelineProvider.swift new file mode 100644 index 00000000..398182b7 --- /dev/null +++ b/AltWidget/AppsTimelineProvider.swift @@ -0,0 +1,202 @@ +// +// AppsTimelineProvider.swift +// AltWidgetExtension +// +// Created by Riley Testut on 8/23/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// + +import WidgetKit +import CoreData + +import AltStoreCore + +struct AppsEntry: TimelineEntry +{ + var date: Date + var relevance: TimelineEntryRelevance? + + var apps: [AppSnapshot] + var isPlaceholder: Bool = false +} + +struct AppsTimelineProvider +{ + typealias Entry = AppsEntry + + func placeholder(in context: TimelineProviderContext) -> AppsEntry + { + return AppsEntry(date: Date(), apps: [], isPlaceholder: true) + } + + func snapshot(for appBundleIDs: [String]) async -> AppsEntry + { + do + { + try await self.prepare() + + let apps = try await self.fetchApps(withBundleIDs: appBundleIDs) + + let entry = AppsEntry(date: Date(), apps: apps) + return entry + } + catch + { + print("Failed to prepare widget snapshot:", error) + + let entry = AppsEntry(date: Date(), apps: []) + return entry + } + } + + func timeline(for appBundleIDs: [String]) async -> Timeline + { + do + { + try await self.prepare() + + let apps = try await self.fetchApps(withBundleIDs: appBundleIDs) + + let entries = self.makeEntries(for: apps) + let timeline = Timeline(entries: entries, policy: .atEnd) + return timeline + } + catch + { + print("Failed to prepare widget timeline:", error) + + let entry = AppsEntry(date: Date(), apps: []) + let timeline = Timeline(entries: [entry], policy: .atEnd) + return timeline + } + } +} + +private extension AppsTimelineProvider +{ + func prepare() async throws + { + try await DatabaseManager.shared.start() + } + + func fetchApps(withBundleIDs bundleIDs: [String]) async throws -> [AppSnapshot] + { + let context = DatabaseManager.shared.persistentContainer.newBackgroundContext() + let apps = try await context.performAsync { + let fetchRequest = InstalledApp.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "%K IN %@", #keyPath(InstalledApp.bundleIdentifier), bundleIDs) + fetchRequest.returnsObjectsAsFaults = false + + let installedApps = try context.fetch(fetchRequest) + + let apps = installedApps.map { AppSnapshot(installedApp: $0) } + + // Always list apps in alphabetical order. + let sortedApps = apps.sorted { $0.name < $1.name } + return sortedApps + } + + return apps + } + + func makeEntries(for snapshots: [AppSnapshot]) -> [AppsEntry] + { + let sortedAppsByExpirationDate = snapshots.sorted { $0.expirationDate < $1.expirationDate } + guard let firstExpiringApp = sortedAppsByExpirationDate.first, let lastExpiringApp = sortedAppsByExpirationDate.last else { return [] } + + let currentDate = Calendar.current.startOfDay(for: Date()) + let numberOfDays = lastExpiringApp.expirationDate.numberOfCalendarDays(since: currentDate) + + // Generate a timeline consisting of one entry per day. + var entries: [AppsEntry] = [] + + switch numberOfDays + { + case ..<0: + let entry = AppsEntry(date: currentDate, relevance: TimelineEntryRelevance(score: 0.0), apps: snapshots) + entries.append(entry) + + case 0: + let entry = AppsEntry(date: currentDate, relevance: TimelineEntryRelevance(score: 1.0), apps: snapshots) + entries.append(entry) + + default: + // To reduce memory consumption, we only generate entries for the next week. This includes: + // * 1 for each day the "least expired" app is valid (up to 7) + // * 1 "0 days remaining" + // * 1 "Expired" + + let numberOfEntries = min(numberOfDays, 7) + 2 + + let appEntries = (0 ..< numberOfEntries).map { (dayOffset) -> AppsEntry in + let entryDate = Calendar.current.date(byAdding: .day, value: dayOffset, to: currentDate) ?? currentDate.addingTimeInterval(Double(dayOffset) * 60 * 60 * 24) + + let daysSinceRefresh = entryDate.numberOfCalendarDays(since: firstExpiringApp.refreshedDate) + let totalNumberOfDays = firstExpiringApp.expirationDate.numberOfCalendarDays(since: firstExpiringApp.refreshedDate) + + var score = (entryDate <= firstExpiringApp.expirationDate) ? Float(daysSinceRefresh + 1) / Float(totalNumberOfDays + 1) : 1 // Expired apps have a score of 1. + if snapshots.allSatisfy({ $0.expirationDate > currentDate }) + { + // Unless ALL apps are expired, in which case relevance is 0. + score = 0 + } + + let entry = AppsEntry(date: entryDate, relevance: TimelineEntryRelevance(score: score), apps: snapshots) + return entry + } + + entries.append(contentsOf: appEntries) + } + + 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] + { + do + { + try await self.prepare() + + let context = DatabaseManager.shared.persistentContainer.newBackgroundContext() + let bundleIDs = try await context.performAsync { + let fetchRequest = InstalledApp.activeAppsFetchRequest() as! NSFetchRequest + fetchRequest.resultType = .dictionaryResultType + fetchRequest.propertiesToFetch = [#keyPath(InstalledApp.bundleIdentifier)] + + let bundleIDs = try context.fetch(fetchRequest).compactMap { $0[#keyPath(InstalledApp.bundleIdentifier)] as? String } + return bundleIDs + } + + return bundleIDs + } + catch + { + print("Failed to fetch active bundle IDs, falling back to AltStore bundle ID.", error) + + return [StoreApp.altstoreAppID] + } + } +} diff --git a/AltWidget/Components/Countdown.swift b/AltWidget/Components/Countdown.swift index 0bdf4106..65ca61bb 100644 --- a/AltWidget/Components/Countdown.swift +++ b/AltWidget/Components/Countdown.swift @@ -15,13 +15,15 @@ struct Countdown: View var endDate: Date? var currentDate: Date = Date() + var strokeWidth: Double = 4.0 + @Environment(\.font) private var font private var numberOfDays: Int { guard let date = self.endDate else { return 0 } let numberOfDays = date.numberOfCalendarDays(since: self.currentDate) - return numberOfDays + return max(numberOfDays, 0) // Never show negative values. } private var fractionComplete: CGFloat { @@ -35,7 +37,7 @@ struct Countdown: View @ViewBuilder private func overlay(progress: CGFloat) -> some View { - let strokeStyle = StrokeStyle(lineWidth: 4.0, lineCap: .round, lineJoin: .round) + let strokeStyle = StrokeStyle(lineWidth: self.strokeWidth, lineCap: .round, lineJoin: .round) if self.numberOfDays > 9 || self.numberOfDays < 0 { Capsule(style: .continuous) diff --git a/AltWidget/Model/AppSnapshot.swift b/AltWidget/Model/AppSnapshot.swift index 24842cda..7cd6a8ae 100644 --- a/AltWidget/Model/AppSnapshot.swift +++ b/AltWidget/Model/AppSnapshot.swift @@ -39,3 +39,51 @@ extension AppSnapshot self.icon = application?.icon?.resizing(toFill: CGSize(width: 180, height: 180)) } } + +extension AppSnapshot +{ + static func makePreviewSnapshots() -> (altstore: AppSnapshot, delta: AppSnapshot, clip: AppSnapshot, longAltStore: AppSnapshot, longDelta: AppSnapshot, longClip: AppSnapshot) + { + let shortRefreshedDate = Calendar.current.date(byAdding: .day, value: -2, to: Date()) ?? Date() + let shortExpirationDate = Calendar.current.date(byAdding: .day, value: 7, to: shortRefreshedDate) ?? Date() + + let longRefreshedDate = Calendar.current.date(byAdding: .day, value: -100, to: Date()) ?? Date() + let longExpirationDate = Calendar.current.date(byAdding: .day, value: 365, to: longRefreshedDate) ?? Date() + + let altstore = AppSnapshot(name: "AltStore", + bundleIdentifier: "com.rileytestut.AltStore", + expirationDate: shortExpirationDate, + refreshedDate: shortRefreshedDate, + tintColor: .altPrimary, + icon: UIImage(named: "AltStore")) + + let delta = AppSnapshot(name: "Delta", + bundleIdentifier: "com.rileytestut.Delta", + expirationDate: shortExpirationDate, + refreshedDate: shortRefreshedDate, + tintColor: .deltaPrimary, + icon: UIImage(named: "Delta")) + + let clip = AppSnapshot(name: "Clip", + bundleIdentifier: "com.rileytestut.Clip", + expirationDate: shortExpirationDate, + refreshedDate: shortRefreshedDate, + tintColor: .clipPrimary, + icon: UIImage(named: "Clip")) + + let longAltStore = altstore.with(refreshedDate: longRefreshedDate, expirationDate: longExpirationDate) + let longDelta = delta.with(refreshedDate: longRefreshedDate, expirationDate: longExpirationDate) + let longClip = clip.with(refreshedDate: longRefreshedDate, expirationDate: longExpirationDate) + + return (altstore, delta, clip, longAltStore, longDelta, longClip) + } + + private func with(refreshedDate: Date, expirationDate: Date) -> AppSnapshot + { + var app = self + app.refreshedDate = refreshedDate + app.expirationDate = expirationDate + + return app + } +} diff --git a/AltWidget/Previews.xcassets/Clip.imageset/ClipIcon@2x.png b/AltWidget/Previews.xcassets/Clip.imageset/ClipIcon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..cd7060c36580c4bcfb758273ada5d4241d9005b0 GIT binary patch literal 7204 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 O(^S`1D|uiM^8WxKV*XzM literal 0 HcmV?d00001 diff --git a/AltWidget/Previews.xcassets/Clip.imageset/ClipIcon@3x.png b/AltWidget/Previews.xcassets/Clip.imageset/ClipIcon@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..eac14e24e3a8d0383439a998e034a418358c6ce4 GIT binary patch literal 16896 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 literal 0 HcmV?d00001 diff --git a/AltWidget/Previews.xcassets/Clip.imageset/Contents.json b/AltWidget/Previews.xcassets/Clip.imageset/Contents.json new file mode 100644 index 00000000..8067994c --- /dev/null +++ b/AltWidget/Previews.xcassets/Clip.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ClipIcon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ClipIcon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AltWidget/Previews.xcassets/Contents.json b/AltWidget/Previews.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/AltWidget/Previews.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AltWidget/Assets.xcassets/Delta.imageset/Contents.json b/AltWidget/Previews.xcassets/Delta.imageset/Contents.json similarity index 100% rename from AltWidget/Assets.xcassets/Delta.imageset/Contents.json rename to AltWidget/Previews.xcassets/Delta.imageset/Contents.json diff --git a/AltWidget/Assets.xcassets/Delta.imageset/icon-120.png b/AltWidget/Previews.xcassets/Delta.imageset/icon-120.png similarity index 100% rename from AltWidget/Assets.xcassets/Delta.imageset/icon-120.png rename to AltWidget/Previews.xcassets/Delta.imageset/icon-120.png diff --git a/AltWidget/Assets.xcassets/Delta.imageset/icon-180.png b/AltWidget/Previews.xcassets/Delta.imageset/icon-180.png similarity index 100% rename from AltWidget/Assets.xcassets/Delta.imageset/icon-180.png rename to AltWidget/Previews.xcassets/Delta.imageset/icon-180.png diff --git a/AltWidget/Widgets/ActiveAppsWidget.swift b/AltWidget/Widgets/ActiveAppsWidget.swift new file mode 100644 index 00000000..468f9522 --- /dev/null +++ b/AltWidget/Widgets/ActiveAppsWidget.swift @@ -0,0 +1,158 @@ +// +// HomeScreenWidget.swift +// AltWidgetExtension +// +// Created by Riley Testut on 8/16/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// + +import SwiftUI +import WidgetKit +import CoreData + +import AltStoreCore +import AltSign + +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 altGradientDark = Color.init(.displayP3, red: 0.0/255.0, green: 128.0/255.0, blue: 132.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, *) +struct ActiveAppsWidget: Widget +{ + private let kind: String = "ActiveApps" + + public var body: some WidgetConfiguration { + StaticConfiguration(kind: kind, provider: AppsTimelineProvider()) { entry in + ActiveAppsWidgetView(entry: entry) + } + .supportedFamilies([.systemMedium]) + .configurationDisplayName("AltWidget") + .description("View remaining days until your active apps expire.") + } +} + +@available(iOS 17, *) +private struct ActiveAppsWidgetView: View +{ + var entry: AppsEntry + + @Environment(\.colorScheme) + private var colorScheme + + var body: some View { + Group { + if entry.apps.isEmpty + { + placeholder + } + else + { + content + } + } + .foregroundStyle(.white) + .containerBackground(for: .widget) { + if colorScheme == .dark + { + LinearGradient(colors: [.altGradientDark, .altGradientExtraDark], startPoint: .top, endPoint: .bottom) + } + else + { + LinearGradient(colors: [.altGradientLight, .altGradientDark], startPoint: .top, endPoint: .bottom) + } + } + } + + private var content: some View { + GeometryReader { (geometry) in + + let numberOfApps = max(entry.apps.count, 1) // Ensure we don't divide by 0 + let preferredRowHeight = (geometry.size.height / Double(numberOfApps)) - 8 + let rowHeight = min(preferredRowHeight, geometry.size.height / 2) + + ZStack(alignment: .center) { + VStack(spacing: 12) { + ForEach(entry.apps, id: \.bundleIdentifier) { app in + + let daysRemaining = app.expirationDate.numberOfCalendarDays(since: entry.date) + let cornerRadius = rowHeight / 5.0 + + HStack(spacing: 10) { + Image(uiImage: app.icon ?? UIImage(named: "AltStore")!) + .resizable() + .aspectRatio(contentMode: .fit) + .cornerRadius(cornerRadius) + + VStack(alignment: .leading, spacing: 1) { + Text(app.name) + .font(.system(size: 15, weight: .semibold, design: .rounded)) + + let text = if entry.date > app.expirationDate + { + Text("Expired") + } + else + { + Text("Expires in \(daysRemaining) ") + (daysRemaining == 1 ? Text("day") : Text("days")) + } + + text + .font(.system(size: 13, weight: .semibold, design: .rounded)) + .foregroundStyle(.secondary) + } + + HStack { + Spacer() + + Countdown(startDate: app.refreshedDate, + endDate: app.expirationDate, + currentDate: entry.date, + strokeWidth: 3.0) // Slightly thinner circle stroke width + .background { + Color.black.opacity(0.1) + .mask(Capsule()) + .padding(.all, -5) + } + .font(.system(size: 16, weight: .semibold, design: .rounded)) + .invalidatableContent() + } + .activatesRefreshAllAppsIntent() + } + .frame(height: rowHeight) + } + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } + + private var placeholder: some View { + Text("App Not Found") + .font(.system(.body, design: .rounded)) + .fontWeight(.semibold) + .foregroundColor(Color.white.opacity(0.4)) + } +} + +#Preview(as: .systemMedium) { + ActiveAppsWidget() +} timeline: { + let expiredDate = Date().addingTimeInterval(1 * 60 * 60 * 24 * 7) + let (altstore, delta, clip, longAltStore, longDelta, longClip) = AppSnapshot.makePreviewSnapshots() + + AppsEntry(date: Date(), apps: [altstore, delta, clip]) + AppsEntry(date: Date(), apps: [longAltStore, longDelta, longClip]) + + AppsEntry(date: expiredDate, apps: [altstore, delta, clip]) + + AppsEntry(date: Date(), apps: [altstore, delta]) + AppsEntry(date: Date(), apps: [altstore]) + + AppsEntry(date: Date(), apps: []) + AppsEntry(date: Date(), apps: [], isPlaceholder: true) +}