Merge branch 'sources_tab'

# Conflicts:
#	AltStore.xcodeproj/project.pbxproj
This commit is contained in:
Riley Testut
2023-10-19 14:18:43 -05:00
39 changed files with 1978 additions and 795 deletions

View File

@@ -10,6 +10,13 @@ import Foundation
public extension Date
{
private static let mediumDateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium
dateFormatter.timeStyle = .none
return dateFormatter
}()
func numberOfCalendarDays(since date: Date) -> Int
{
let today = Calendar.current.startOfDay(for: self)
@@ -19,15 +26,15 @@ public extension Date
return components.day!
}
func relativeDateString(since date: Date, dateFormatter: DateFormatter) -> String
func relativeDateString(since date: Date, dateFormatter: DateFormatter? = nil) -> String
{
let dateFormatter = dateFormatter ?? Date.mediumDateFormatter
let numberOfDays = self.numberOfCalendarDays(since: date)
switch numberOfDays
{
case 0: return NSLocalizedString("Today", comment: "")
case 1: return NSLocalizedString("Yesterday", comment: "")
case 2...7: return String(format: NSLocalizedString("%@ days ago", comment: ""), NSNumber(value: numberOfDays))
default: return dateFormatter.string(from: date)
}
}

View File

@@ -0,0 +1,16 @@
//
// ProcessInfo+Previews.swift
// AltStoreCore
//
// Created by Riley Testut on 10/11/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import Foundation
public extension ProcessInfo
{
var isPreview: Bool {
ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
}
}

View File

@@ -87,6 +87,22 @@ public extension DatabaseManager
guard !self.isStarted else { return finish(nil) }
#if DEBUG
// Wrap in #if DEBUG to *ensure* we never accidentally delete production databases.
if ProcessInfo.processInfo.isPreview
{
do
{
print("!!! Purging database for preview...")
try FileManager.default.removeItem(at: PersistentContainer.defaultDirectoryURL())
}
catch
{
print("Failed to remove database directory for preview.", error)
}
}
#endif
if self.persistentContainer.isMigrationRequired
{
// Quit any other running AltStore processes to prevent concurrent database access during and after migration.
@@ -166,6 +182,22 @@ public extension DatabaseManager
}
}
public extension DatabaseManager
{
func startForPreview()
{
let semaphore = DispatchSemaphore(value: 0)
self.dispatchQueue.async {
self.startCompletionHandlers.append { error in
semaphore.signal()
}
}
_ = semaphore.wait(timeout: .now() + 2.0)
}
}
public extension DatabaseManager
{
var viewContext: NSManagedObjectContext {

View File

@@ -236,6 +236,23 @@ public extension Source
return isAdded
}
}
var isRecommended: Bool {
guard let recommendedSources = UserDefaults.shared.recommendedSources else { return false }
// TODO: Support alternate URLs
let isRecommended = recommendedSources.contains { source in
return source.identifier == self.identifier || source.sourceURL?.absoluteString.lowercased() == self.sourceURL.absoluteString
}
return isRecommended
}
var lastUpdatedDate: Date? {
let allDates = self.apps.compactMap { $0.latestAvailableVersion?.date } + self.newsItems.map { $0.date }
let lastUpdatedDate = allDates.sorted().last
return lastUpdatedDate
}
}
internal extension Source
@@ -283,4 +300,14 @@ public extension Source
let source = Source.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Source.identifier), Source.altStoreIdentifier), in: context)
return source
}
class func make(name: String, identifier: String, sourceURL: URL, context: NSManagedObjectContext) -> Source
{
let source = Source(context: context)
source.name = name
source.identifier = identifier
source.sourceURL = sourceURL
return source
}
}

View File

@@ -0,0 +1,69 @@
//
// KnownSource.swift
// AltStore
//
// Created by Riley Testut on 5/16/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import Foundation
public struct KnownSource: Decodable
{
public var identifier: String
public var sourceURL: URL?
public var bundleIDs: [String]?
}
private extension KnownSource
{
var dictionaryRepresentation: [String: Any] {
let dictionary: [String: Any?] = [
KnownSource.CodingKeys.identifier.stringValue: identifier,
KnownSource.CodingKeys.sourceURL.stringValue: self.sourceURL?.absoluteString,
KnownSource.CodingKeys.bundleIDs.stringValue: self.bundleIDs
]
return dictionary.compactMapValues { $0 }
}
init?(dictionary: [String: Any])
{
guard let identifier = dictionary[CodingKeys.identifier.stringValue] as? String else { return nil }
self.identifier = identifier
if let sourceURLString = dictionary[CodingKeys.sourceURL.stringValue] as? String
{
self.sourceURL = URL(string: sourceURLString)
}
let bundleIDs = dictionary[CodingKeys.bundleIDs.stringValue] as? [String]
self.bundleIDs = bundleIDs
}
}
public extension UserDefaults
{
// Cache recommended sources just in case we need to check whether source is recommended or not.
@nonobjc var recommendedSources: [KnownSource]? {
get {
guard let sources = _recommendedSources?.compactMap({ KnownSource(dictionary: $0) }) else { return nil }
return sources
}
set {
_recommendedSources = newValue?.map { $0.dictionaryRepresentation }
}
}
@NSManaged @objc(recommendedSources) private var _recommendedSources: [[String: Any]]?
@nonobjc var blockedSources: [KnownSource]? {
get {
guard let sources = _blockedSources?.compactMap({ KnownSource(dictionary: $0) }) else { return nil }
return sources
}
set {
_blockedSources = newValue?.map { $0.dictionaryRepresentation }
}
}
@NSManaged @objc(blockedSources) private var _blockedSources: [[String: Any]]?
}