spm: complex refactor, document of package

This commit is contained in:
Joe Mattiello
2023-03-10 19:23:32 -05:00
parent 1f2693bea6
commit 128b180c1f
40 changed files with 905 additions and 745 deletions

View File

@@ -0,0 +1,126 @@
//
// ComplicationView.swift
// AltStore
//
// Created by Riley Testut on 7/7/22.
// Copyright © 2022 Riley Testut. All rights reserved.
//
import SwiftUI
import WidgetKit
@available(iOS 16, *)
extension ComplicationView {
enum Style {
case text
case icon
}
}
@available(iOS 16, *)
struct ComplicationView: View {
let entry: AppEntry
let style: Style
var body: some View {
let refreshedDate = self.entry.app?.refreshedDate ?? .now
let expirationDate = self.entry.app?.expirationDate ?? .now
let totalDays = expirationDate.numberOfCalendarDays(since: refreshedDate)
let daysRemaining = expirationDate.numberOfCalendarDays(since: self.entry.date)
let progress = Double(daysRemaining) / Double(totalDays)
Gauge(value: progress) {
if daysRemaining < 0 {
Text("Expired")
.font(.system(size: 10, weight: .bold))
} else {
switch self.style {
case .text:
VStack(spacing: -1) {
let fontSize = daysRemaining > 99 ? 18.0 : 20.0
Text("\(daysRemaining)")
.font(.system(size: fontSize, weight: .bold, design: .rounded))
Text(daysRemaining == 1 ? "DAY" : "DAYS")
.font(.caption)
}
.fixedSize()
.offset(y: -1)
case .icon:
ZStack {
// Destination
Image("SmallIcon")
.resizable()
.aspectRatio(1.0, contentMode: .fill)
.scaleEffect(x: 0.8, y: 0.8)
// Source
(
daysRemaining > 7 ?
Text("7+")
.font(.system(size: 18, weight: .bold, design: .rounded))
.kerning(-2) :
Text("\(daysRemaining)")
.font(.system(size: 20, weight: .bold, design: .rounded))
)
.foregroundColor(Color.black)
.blendMode(.destinationOut) // Clip text out of image.
}
}
}
}
.gaugeStyle(.accessoryCircularCapacity)
.unredacted()
}
}
@available(iOS 16, *)
struct ComplicationView_Previews: PreviewProvider {
static var previews: some View {
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 expiredDate = shortExpirationDate.addingTimeInterval(1 * 60 * 60 * 24)
let weekAltstore = AppSnapshot(name: "AltStore",
bundleIdentifier: Bundle.Info.appbundleIdentifier,
expirationDate: shortExpirationDate,
refreshedDate: shortRefreshedDate,
tintColor: .altPrimary,
icon: UIImage(named: "AltStore"))
let yearAltstore = AppSnapshot(name: "AltStore",
bundleIdentifier: Bundle.Info.appbundleIdentifier,
expirationDate: longExpirationDate,
refreshedDate: longRefreshedDate,
tintColor: .altPrimary,
icon: UIImage(named: "AltStore"))
return Group {
ComplicationView(entry: AppEntry(date: Date(), app: weekAltstore), style: .icon)
.previewContext(WidgetPreviewContext(family: .accessoryCircular))
ComplicationView(entry: AppEntry(date: expiredDate, app: weekAltstore), style: .icon)
.previewContext(WidgetPreviewContext(family: .accessoryCircular))
ComplicationView(entry: AppEntry(date: longRefreshedDate, app: yearAltstore), style: .icon)
.previewContext(WidgetPreviewContext(family: .accessoryCircular))
ComplicationView(entry: AppEntry(date: Date(), app: weekAltstore), style: .text)
.previewContext(WidgetPreviewContext(family: .accessoryCircular))
ComplicationView(entry: AppEntry(date: expiredDate, app: weekAltstore), style: .text)
.previewContext(WidgetPreviewContext(family: .accessoryCircular))
ComplicationView(entry: AppEntry(date: longRefreshedDate, app: yearAltstore), style: .text)
.previewContext(WidgetPreviewContext(family: .accessoryCircular))
}
}
}

View File

@@ -0,0 +1,77 @@
//
// Countdown.swift
// SideWidgetExtension
//
// Created by Riley Testut on 7/6/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import SwiftUI
import WidgetKit
struct Countdown: View {
var startDate: Date?
var endDate: Date?
var currentDate: Date = .init()
@Environment(\.font) private var font
private var numberOfDays: Int {
guard let date = endDate else { return 0 }
let numberOfDays = date.numberOfCalendarDays(since: currentDate)
return numberOfDays
}
private var fractionComplete: CGFloat {
guard let startDate = startDate, let endDate = endDate else { return 1.0 }
let totalNumberOfDays = endDate.numberOfCalendarDays(since: startDate)
let fractionComplete = CGFloat(numberOfDays) / CGFloat(totalNumberOfDays)
return fractionComplete
}
@ViewBuilder
private func overlay(progress: CGFloat) -> some View {
let strokeStyle = StrokeStyle(lineWidth: 4.0, lineCap: .round, lineJoin: .round)
if numberOfDays > 9 || numberOfDays < 0 {
Capsule(style: .continuous)
.trim(from: 0.0, to: progress)
.stroke(style: strokeStyle)
} else {
Circle()
.trim(from: 0.0, to: progress)
.rotation(Angle(degrees: -90), anchor: .center)
.stroke(style: strokeStyle)
}
}
var body: some View {
Text("\(numberOfDays)")
.font((font ?? .title).monospacedDigit())
.bold()
.opacity(endDate != nil ? 1 : 0)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.overlay(
ZStack {
overlay(progress: 1.0)
.opacity(0.3)
overlay(progress: fractionComplete)
}
)
}
}
struct Countdown_Previews: PreviewProvider {
static var previews: some View {
let startDate = Calendar.current.date(byAdding: .day, value: -2, to: Date()) ?? Date()
Group {
Countdown(startDate: startDate, endDate: Calendar.current.date(byAdding: .day, value: 7, to: startDate))
Countdown(startDate: startDate, endDate: Calendar.current.date(byAdding: .day, value: 365, to: startDate))
}
.previewContext(WidgetPreviewContext(family: .systemSmall))
}
}

View File

@@ -0,0 +1,242 @@
//
// SideWidget.swift
// SideWidget
//
// Created by Riley Testut on 6/26/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import CoreData
import SwiftUI
import UIKit
import WidgetKit
import AltSign
import SideStoreCore
import Roxas
import RoxasUIKit
import os.log
struct AppEntry: TimelineEntry {
var date: Date
var relevance: TimelineEntryRelevance?
var app: AppSnapshot?
var isPlaceholder: Bool = false
}
struct AppSnapshot {
var name: String
var bundleIdentifier: String
var expirationDate: Date
var refreshedDate: Date
var tintColor: UIColor?
var icon: UIImage?
}
extension AppSnapshot {
// Declared in extension so we retain synthesized initializer.
init(installedApp: InstalledApp) {
name = installedApp.name
bundleIdentifier = installedApp.bundleIdentifier
expirationDate = installedApp.expirationDate
refreshedDate = installedApp.refreshedDate
tintColor = installedApp.storeApp?.tintColor
let application = ALTApplication(fileURL: installedApp.fileURL)
icon = application?.icon?.resizing(toFill: CGSize(width: 180, height: 180))
}
}
struct Provider: IntentTimelineProvider {
typealias Intent = ViewAppIntent
typealias Entry = AppEntry
func placeholder(in _: Context) -> AppEntry {
AppEntry(date: Date(), app: nil, isPlaceholder: true)
}
func getSnapshot(for _: ViewAppIntent, in _: Context, completion: @escaping (AppEntry) -> Void) {
prepare { result in
do {
let context = try result.get()
let snapshot = InstalledApp.fetchAltStore(in: context).map(AppSnapshot.init)
let entry = AppEntry(date: Date(), app: snapshot)
completion(entry)
} catch {
os_log(" %@", type: .error , error.localizedDescription)
let entry = AppEntry(date: Date(), app: nil)
completion(entry)
}
}
}
func getTimeline(for configuration: ViewAppIntent, in _: Context, completion: @escaping (Timeline<AppEntry>) -> Void) {
prepare { result in
autoreleasepool {
do {
let context = try result.get()
let installedApp: InstalledApp?
if let identifier = configuration.app?.identifier {
let app = InstalledApp.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), identifier),
in: context)
installedApp = app
} else {
installedApp = InstalledApp.fetchAltStore(in: context)
}
guard let snapshot = installedApp.map(AppSnapshot.init) else { throw ALTError(.invalidApp) }
let currentDate = Calendar.current.startOfDay(for: Date())
let numberOfDays = snapshot.expirationDate.numberOfCalendarDays(since: currentDate)
// Generate a timeline consisting of one entry per day.
var entries: [AppEntry] = []
switch numberOfDays {
case ..<0:
let entry = AppEntry(date: currentDate, relevance: TimelineEntryRelevance(score: 0.0), app: snapshot)
entries.append(entry)
case 0:
let entry = AppEntry(date: currentDate, relevance: TimelineEntryRelevance(score: 1.0), app: snapshot)
entries.append(entry)
default:
// To reduce memory consumption, we only generate entries for the next week. This includes:
// * 1 for each day the app is valid (up to 7)
// * 1 "0 days remaining"
// * 1 "Expired"
let numberOfEntries = min(numberOfDays, 7) + 2
let appEntries = (0 ..< numberOfEntries).map { dayOffset -> AppEntry in
let entryDate = Calendar.current.date(byAdding: .day, value: dayOffset, to: currentDate) ?? currentDate.addingTimeInterval(Double(dayOffset) * 60 * 60 * 24)
let daysSinceRefresh = entryDate.numberOfCalendarDays(since: snapshot.refreshedDate)
let totalNumberOfDays = snapshot.expirationDate.numberOfCalendarDays(since: snapshot.refreshedDate)
let score = (entryDate <= snapshot.expirationDate) ? Float(daysSinceRefresh + 1) / Float(totalNumberOfDays + 1) : 0 // Expired apps have a score of 0.
let entry = AppEntry(date: entryDate, relevance: TimelineEntryRelevance(score: score), app: snapshot)
return entry
}
entries.append(contentsOf: appEntries)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
} catch {
os_log(" %@", type: .error , error.localizedDescription)
let entry = AppEntry(date: Date(), app: nil)
let timeline = Timeline(entries: [entry], policy: .atEnd)
completion(timeline)
}
}
}
}
private func prepare(completion: @escaping (Result<NSManagedObjectContext, Error>) -> Void) {
DatabaseManager.shared.start { error in
if let error = error {
completion(.failure(error))
} else {
DatabaseManager.shared.viewContext.perform {
completion(.success(DatabaseManager.shared.viewContext))
}
}
}
}
}
struct HomeScreenWidget: Widget {
private let kind: String = "AppDetail"
public var body: some WidgetConfiguration {
IntentConfiguration(kind: kind,
intent: ViewAppIntent.self,
provider: Provider()) { entry in
WidgetView(entry: entry)
}
.supportedFamilies([.systemSmall])
.configurationDisplayName("SideWidget")
.description("View remaining days until your sideloaded apps expire.")
}
}
struct TextLockScreenWidget: Widget {
private let kind: String = "TextLockAppDetail"
public var body: some WidgetConfiguration {
if #available(iOSApplicationExtension 16, *) {
return IntentConfiguration(kind: kind,
intent: ViewAppIntent.self,
provider: Provider()) { entry in
ComplicationView(entry: entry, style: .text)
}
.supportedFamilies([.accessoryCircular])
.configurationDisplayName("SideWidget (Text)")
.description("View remaining days until SideStore expires.")
} else {
return EmptyWidgetConfiguration()
}
}
}
struct IconLockScreenWidget: Widget {
private let kind: String = "IconLockAppDetail"
public var body: some WidgetConfiguration {
if #available(iOSApplicationExtension 16, *) {
return IntentConfiguration(kind: kind,
intent: ViewAppIntent.self,
provider: Provider()) { entry in
ComplicationView(entry: entry, style: .icon)
}
.supportedFamilies([.accessoryCircular])
.configurationDisplayName("SideWidget (Icon)")
.description("View remaining days until SideStore expires.")
} else {
return EmptyWidgetConfiguration()
}
}
}
//
// struct LockScreenWidget: Widget
// {
// private let kind: String = "LockAppDetail"
//
// public var body: some WidgetConfiguration {
// if #available(iOSApplicationExtension 16, *)
// {
// return IntentConfiguration(kind: kind,
// intent: ViewAppIntent.self,
// provider: Provider()) { (entry) in
// ComplicationView(entry: entry, style: .icon)
// }
// .supportedFamilies([.accessoryCircular])
// .configurationDisplayName("SideWidget")
// .description("View remaining days until SideStore expires.")
// }
// else
// {
// return EmptyWidgetConfiguration()
// }
// }
// }
@main
struct SideWidgets: WidgetBundle {
var body: some Widget {
HomeScreenWidget()
IconLockScreenWidget()
TextLockScreenWidget()
}
}

View File

@@ -0,0 +1,186 @@
//
// WidgetView.swift
// AltStore
//
// Created by Riley Testut on 9/14/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import SwiftUI
import WidgetKit
import AltSign
import SideStoreCore
import CoreData
struct WidgetView: View {
var entry: AppEntry
var body: some View {
Group {
if let app = self.entry.app {
let daysRemaining = app.expirationDate.numberOfCalendarDays(since: self.entry.date)
GeometryReader { geometry in
Group {
VStack(alignment: .leading) {
VStack(alignment: .leading, spacing: 5) {
let imageHeight = geometry.size.height * 0.4
Image(uiImage: app.icon ?? UIImage())
.resizable()
.aspectRatio(CGSize(width: 1, height: 1), contentMode: .fit)
.frame(height: imageHeight)
.mask(RoundedRectangle(cornerRadius: imageHeight / 5.0, style: .continuous))
Text(app.name.uppercased())
.font(.system(size: 12, weight: .semibold, design: .rounded))
.foregroundColor(.white)
.lineLimit(1)
.minimumScaleFactor(0.5)
}
.fixedSize(horizontal: false, vertical: true)
Spacer(minLength: 0)
HStack(alignment: .center) {
let expirationText: Text = {
switch daysRemaining {
case ..<0: return Text("Expired")
case 1: return Text("1 day")
default: return Text("\(daysRemaining) days")
}
}()
(
Text("Expires in\n")
.font(.system(size: 13, weight: .semibold, design: .rounded))
.foregroundColor(Color.white.opacity(0.45)) +
expirationText
.font(.system(size: 15, weight: .semibold, design: .rounded))
.foregroundColor(.white)
)
.lineLimit(2)
.lineSpacing(1.0)
.minimumScaleFactor(0.5)
Spacer()
if daysRemaining >= 0 {
Countdown(startDate: app.refreshedDate,
endDate: app.expirationDate,
currentDate: self.entry.date)
.font(.system(size: 20, weight: .semibold, design: .rounded))
.foregroundColor(Color.white)
.opacity(0.8)
.fixedSize(horizontal: true, vertical: false)
.offset(x: 5)
}
}
.fixedSize(horizontal: false, vertical: true)
}
.padding()
}
}
} else {
VStack {
// Put conditional inside VStack, or else an empty view will be returned
// if isPlaceholder == false, which messes up layout.
if !entry.isPlaceholder {
Text("App Not Found")
.font(.system(.body, design: .rounded))
.fontWeight(.semibold)
.foregroundColor(Color.white.opacity(0.4))
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
.background(backgroundView(icon: entry.app?.icon, tintColor: entry.app?.tintColor))
}
}
private extension WidgetView {
func backgroundView(icon: UIImage? = nil, tintColor: UIColor? = nil) -> some View {
let icon = icon ?? UIImage(named: "AltStore")!
let tintColor = tintColor ?? .gray
let imageHeight = 60 as CGFloat
let saturation = 1.8
let blurRadius = 5 as CGFloat
let tintOpacity = 0.45
return ZStack(alignment: .topTrailing) {
// Blurred Image
GeometryReader { geometry in
ZStack {
Image(uiImage: icon)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: imageHeight, height: imageHeight, alignment: .center)
.saturation(saturation)
.blur(radius: blurRadius, opaque: true)
.scaleEffect(geometry.size.width / imageHeight, anchor: .center)
Color(tintColor)
.opacity(tintOpacity)
}
}
Image("Badge")
.resizable()
.frame(width: 26, height: 26)
.padding()
}
}
}
struct WidgetView_Previews: PreviewProvider {
static var previews: some View {
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 expiredExpirationDate = Calendar.current.date(byAdding: .day, value: -155, to: Date()) ?? 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: Bundle.Info.appbundleIdentifier,
expirationDate: shortExpirationDate,
refreshedDate: shortRefreshedDate,
tintColor: .altPrimary,
icon: UIImage(named: "AltStore"))
let delta = AppSnapshot(name: "Delta",
bundleIdentifier: "com.rileytestut.Delta",
expirationDate: longExpirationDate,
refreshedDate: longRefreshedDate,
tintColor: .deltaPrimary,
icon: UIImage(named: "Delta"))
let expiredDelta = AppSnapshot(name: "Delta",
bundleIdentifier: "com.rileytestut.Delta",
expirationDate: expiredExpirationDate,
refreshedDate: shortRefreshedDate,
tintColor: .deltaPrimary,
icon: UIImage(named: "Delta"))
return Group {
WidgetView(entry: AppEntry(date: Date(), app: altstore))
.previewContext(WidgetPreviewContext(family: .systemSmall))
WidgetView(entry: AppEntry(date: Date(), app: delta))
.previewContext(WidgetPreviewContext(family: .systemSmall))
WidgetView(entry: AppEntry(date: Date(), app: expiredDelta))
.previewContext(WidgetPreviewContext(family: .systemSmall))
WidgetView(entry: AppEntry(date: Date(), app: nil))
.previewContext(WidgetPreviewContext(family: .systemSmall))
WidgetView(entry: AppEntry(date: Date(), app: nil, isPlaceholder: true))
.previewContext(WidgetPreviewContext(family: .systemSmall))
}
}
}