[AltWidget] Adds interactive Active Apps widget to view + refresh all active apps (iOS 17+)

This commit is contained in:
Riley Testut
2023-09-01 13:27:28 -05:00
committed by Magesh K
parent 3f6688523a
commit ea2600aba9
16 changed files with 497 additions and 7 deletions

View File

@@ -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 = "<group>"; };
D552B1D72A042A740066216F /* AppPermissionsCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPermissionsCard.swift; sourceTree = "<group>"; };
D55467B12A8D5E2600F4CE90 /* AppShortcuts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppShortcuts.swift; sourceTree = "<group>"; };
D55467C42A8D72C300F4CE90 /* ActiveAppsWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveAppsWidget.swift; sourceTree = "<group>"; };
D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OperatingSystemVersion+Comparable.swift"; sourceTree = "<group>"; };
D571ADCD2A02FA7400B24B63 /* SourceError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceError.swift; sourceTree = "<group>"; };
D571ADCF2A02FC7200B24B63 /* ALTAppPermission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ALTAppPermission.swift; sourceTree = "<group>"; };
D5728CA62A0D79D30014E73C /* OptionalProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalProtocol.swift; sourceTree = "<group>"; };
D577AB7A2A967DF5007FE952 /* AppsTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppsTimelineProvider.swift; sourceTree = "<group>"; };
D577AB7E2A96878A007FE952 /* AppDetailWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDetailWidget.swift; sourceTree = "<group>"; };
D57968CA29CB99EF00539069 /* VibrantButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VibrantButton.swift; sourceTree = "<group>"; };
D57DF637271E32F000677701 /* PatchApp.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = PatchApp.storyboard; sourceTree = "<group>"; };
@@ -986,8 +992,10 @@
D5F5AF7C28ECEA990067C736 /* ErrorDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorDetailsViewController.swift; sourceTree = "<group>"; };
D5F99A1728D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore10ToAltStore11.xcmappingmodel; sourceTree = "<group>"; };
D5F99A1928D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreApp10ToStoreApp11Policy.swift; sourceTree = "<group>"; };
D5FB7A0D2AA25A4E00EF863D /* Previews.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Previews.xcassets; sourceTree = "<group>"; };
D5FD4EC42A952EAD0097BEE8 /* AltWidgetBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AltWidgetBundle.swift; sourceTree = "<group>"; };
D5FD4EC82A9530C00097BEE8 /* AppSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSnapshot.swift; sourceTree = "<group>"; };
D5FD4ECA2A9532960097BEE8 /* DatabaseManager+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DatabaseManager+Async.swift"; sourceTree = "<group>"; };
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 = "<group>"; };
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)",

View File

@@ -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)!

View File

@@ -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<Void, Error>) in
self.start { error in
if let error
{
continuation.resume(throwing: error)
}
else
{
continuation.resume()
}
}
}
}
}

View File

@@ -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"

View File

@@ -17,5 +17,10 @@ struct AltWidgetBundle: WidgetBundle
IconLockScreenWidget()
TextLockScreenWidget()
if #available(iOS 17, *)
{
ActiveAppsWidget()
}
}
}

View File

@@ -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<AppsEntry>
{
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<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
{
try await self.prepare()
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
let bundleIDs = try await context.performAsync {
let fetchRequest = InstalledApp.activeAppsFetchRequest() as! NSFetchRequest<NSDictionary>
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]
}
}
}

View File

@@ -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)

View File

@@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -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
}
}

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

@@ -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)
}