mirror of
https://github.com/XcodesOrg/XcodesApp.git
synced 2026-03-25 08:55:46 +00:00
Merge XcodeList into AppState
This commit is contained in:
parent
f06d72f3eb
commit
33b5f96ed2
3 changed files with 95 additions and 92 deletions
|
|
@ -37,7 +37,7 @@
|
|||
CABFA9C52592EEEA00380FEE /* FileManager+.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABFA9B82592EEEA00380FEE /* FileManager+.swift */; };
|
||||
CABFA9C72592EEEA00380FEE /* Entry+.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABFA9B22592EEEA00380FEE /* Entry+.swift */; };
|
||||
CABFA9C92592EEEA00380FEE /* URLRequest+Apple.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABFA9AB2592EEE900380FEE /* URLRequest+Apple.swift */; };
|
||||
CABFA9CA2592EEEA00380FEE /* XcodeList.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABFA9A72592EEE900380FEE /* XcodeList.swift */; };
|
||||
CABFA9CA2592EEEA00380FEE /* AppState+Update.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABFA9A72592EEE900380FEE /* AppState+Update.swift */; };
|
||||
CABFA9CC2592EEEA00380FEE /* Path+.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABFA9AE2592EEE900380FEE /* Path+.swift */; };
|
||||
CABFA9CD2592EEEA00380FEE /* Foundation.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABFA9AC2592EEE900380FEE /* Foundation.swift */; };
|
||||
CABFA9CE2592EEEA00380FEE /* Version+Xcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABFA9A62592EEE900380FEE /* Version+Xcode.swift */; };
|
||||
|
|
@ -99,7 +99,7 @@
|
|||
CABFA9A12592EAFB00380FEE /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
|
||||
CABFA9A32592ED5700380FEE /* Apple.paw */ = {isa = PBXFileReference; lastKnownFileType = file; name = Apple.paw; path = ../xcodes/Apple.paw; sourceTree = "<group>"; };
|
||||
CABFA9A62592EEE900380FEE /* Version+Xcode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Version+Xcode.swift"; sourceTree = "<group>"; };
|
||||
CABFA9A72592EEE900380FEE /* XcodeList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeList.swift; sourceTree = "<group>"; };
|
||||
CABFA9A72592EEE900380FEE /* AppState+Update.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppState+Update.swift"; sourceTree = "<group>"; };
|
||||
CABFA9A82592EEE900380FEE /* Version+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Version+.swift"; sourceTree = "<group>"; };
|
||||
CABFA9A92592EEE900380FEE /* Environment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Environment.swift; sourceTree = "<group>"; };
|
||||
CABFA9AB2592EEE900380FEE /* URLRequest+Apple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLRequest+Apple.swift"; sourceTree = "<group>"; };
|
||||
|
|
@ -209,6 +209,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
CA378F982466567600A58CE0 /* AppState.swift */,
|
||||
CABFA9A72592EEE900380FEE /* AppState+Update.swift */,
|
||||
CA9FF88025955C7000E47BAF /* AvailableXcode.swift */,
|
||||
CABFAA2B2592FBFC00380FEE /* Configure.swift */,
|
||||
CABFAA482593162500380FEE /* Bundle+InfoPlistValues.swift */,
|
||||
|
|
@ -228,7 +229,6 @@
|
|||
CABFA9A82592EEE900380FEE /* Version+.swift */,
|
||||
CA9FF876259528CC00E47BAF /* Version+XcodeReleases.swift */,
|
||||
CABFA9A62592EEE900380FEE /* Version+Xcode.swift */,
|
||||
CABFA9A72592EEE900380FEE /* XcodeList.swift */,
|
||||
);
|
||||
path = Backend;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -461,7 +461,7 @@
|
|||
CA735109257BF96D00EA9CF8 /* AttributedText.swift in Sources */,
|
||||
CABFAA492593162500380FEE /* Bundle+InfoPlistValues.swift in Sources */,
|
||||
CA9FF8662595130600E47BAF /* View+IsHidden.swift in Sources */,
|
||||
CABFA9CA2592EEEA00380FEE /* XcodeList.swift in Sources */,
|
||||
CABFA9CA2592EEEA00380FEE /* AppState+Update.swift in Sources */,
|
||||
CA44901F2463AD34003D8213 /* Tag.swift in Sources */,
|
||||
CABFA9BF2592EEEA00380FEE /* URLSession+Promise.swift in Sources */,
|
||||
CABFA9BB2592EEEA00380FEE /* DateFormatter+.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -5,15 +5,47 @@ import Version
|
|||
import SwiftSoup
|
||||
import struct XCModel.Xcode
|
||||
|
||||
/// Provides lists of available Xcodes
|
||||
public final class XcodeList {
|
||||
public init() {
|
||||
try? loadCachedAvailableXcodes()
|
||||
extension AppState {
|
||||
private var dataSource: DataSource {
|
||||
Current.defaults.string(forKey: "dataSource").flatMap(DataSource.init(rawValue:)) ?? .default
|
||||
}
|
||||
|
||||
func updateIfNeeded() {
|
||||
guard
|
||||
let lastUpdated = Current.defaults.date(forKey: "lastUpdated"),
|
||||
// This is bad date math but for this use case it doesn't need to be exact
|
||||
lastUpdated < Current.date().addingTimeInterval(-60 * 60 * 24)
|
||||
else { return }
|
||||
update() as Void
|
||||
}
|
||||
|
||||
public private(set) var availableXcodes: [Xcode] = []
|
||||
func update() {
|
||||
guard !isUpdating else { return }
|
||||
updatePublisher = update()
|
||||
.sink(
|
||||
receiveCompletion: { [unowned self] completion in
|
||||
switch completion {
|
||||
case let .failure(error):
|
||||
self.error = AlertContent(title: "Update Error", message: error.legibleLocalizedDescription)
|
||||
case .finished:
|
||||
Current.defaults.setDate(Current.date(), forKey: "lastUpdated")
|
||||
}
|
||||
|
||||
public func update(dataSource: DataSource) -> AnyPublisher<[Xcode], Error> {
|
||||
self.updatePublisher = nil
|
||||
},
|
||||
receiveValue: { _ in }
|
||||
)
|
||||
}
|
||||
|
||||
private func update() -> AnyPublisher<[AvailableXcode], Error> {
|
||||
signInIfNeeded()
|
||||
.flatMap { [unowned self] in
|
||||
self.updateAvailableXcodes(from: self.dataSource)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
private func updateAvailableXcodes(from dataSource: DataSource) -> AnyPublisher<[AvailableXcode], Error> {
|
||||
switch dataSource {
|
||||
case .apple:
|
||||
return releasedXcodes().combineLatest(prereleaseXcodes())
|
||||
|
|
@ -26,16 +58,21 @@ public final class XcodeList {
|
|||
let xcodes = releasedXcodes.filter { releasedXcode in
|
||||
prereleaseXcodes.contains { $0.version.isEqualWithoutBuildMetadataIdentifiers(to: releasedXcode.version) } == false
|
||||
} + prereleaseXcodes
|
||||
self.availableXcodes = xcodes
|
||||
try? self.cacheAvailableXcodes(xcodes)
|
||||
return xcodes
|
||||
}
|
||||
.handleEvents(
|
||||
receiveOutput: { xcodes in
|
||||
self.availableXcodes = xcodes
|
||||
try? self.cacheAvailableXcodes(xcodes)
|
||||
}
|
||||
)
|
||||
.eraseToAnyPublisher()
|
||||
case .xcodeReleases:
|
||||
return xcodeReleases()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.handleEvents(
|
||||
receiveOutput: { xcodes in
|
||||
self.availableXcodes = xcodes
|
||||
try? self.cacheAvailableXcodes(xcodes)
|
||||
}
|
||||
)
|
||||
|
|
@ -44,14 +81,16 @@ public final class XcodeList {
|
|||
}
|
||||
}
|
||||
|
||||
extension XcodeList {
|
||||
private func loadCachedAvailableXcodes() throws {
|
||||
extension AppState {
|
||||
// MARK: - Available Xcode Cache
|
||||
|
||||
func loadCachedAvailableXcodes() throws {
|
||||
guard let data = Current.files.contents(atPath: Path.cacheFile.string) else { return }
|
||||
let xcodes = try JSONDecoder().decode([Xcode].self, from: data)
|
||||
let xcodes = try JSONDecoder().decode([AvailableXcode].self, from: data)
|
||||
self.availableXcodes = xcodes
|
||||
}
|
||||
|
||||
private func cacheAvailableXcodes(_ xcodes: [Xcode]) throws {
|
||||
func cacheAvailableXcodes(_ xcodes: [AvailableXcode]) throws {
|
||||
let data = try JSONEncoder().encode(xcodes)
|
||||
try FileManager.default.createDirectory(at: Path.cacheFile.url.deletingLastPathComponent(),
|
||||
withIntermediateDirectories: true)
|
||||
|
|
@ -59,20 +98,20 @@ extension XcodeList {
|
|||
}
|
||||
}
|
||||
|
||||
extension XcodeList {
|
||||
// MARK: Apple
|
||||
extension AppState {
|
||||
// MARK: - Apple
|
||||
|
||||
private func releasedXcodes() -> AnyPublisher<[Xcode], Error> {
|
||||
private func releasedXcodes() -> AnyPublisher<[AvailableXcode], Error> {
|
||||
Current.network.dataTask(with: URLRequest.downloads)
|
||||
.map(\.data)
|
||||
.decode(type: Downloads.self, decoder: configure(JSONDecoder()) {
|
||||
$0.dateDecodingStrategy = .formatted(.downloadsDateModified)
|
||||
})
|
||||
.map { downloads -> [Xcode] in
|
||||
.map { downloads -> [AvailableXcode] in
|
||||
let xcodes = downloads
|
||||
.downloads
|
||||
.filter { $0.name.range(of: "^Xcode [0-9]", options: .regularExpression) != nil }
|
||||
.compactMap { download -> Xcode? in
|
||||
.compactMap { download -> AvailableXcode? in
|
||||
let urlPrefix = URL(string: "https://download.developer.apple.com/")!
|
||||
guard
|
||||
let xcodeFile = download.files.first(where: { $0.remotePath.hasSuffix("dmg") || $0.remotePath.hasSuffix("xip") }),
|
||||
|
|
@ -80,22 +119,22 @@ extension XcodeList {
|
|||
else { return nil }
|
||||
|
||||
let url = urlPrefix.appendingPathComponent(xcodeFile.remotePath)
|
||||
return Xcode(version: version, url: url, filename: String(xcodeFile.remotePath.suffix(fromLast: "/")), releaseDate: download.dateModified)
|
||||
return AvailableXcode(version: version, url: url, filename: String(xcodeFile.remotePath.suffix(fromLast: "/")), releaseDate: download.dateModified)
|
||||
}
|
||||
return xcodes
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
private func prereleaseXcodes() -> AnyPublisher<[Xcode], Error> {
|
||||
private func prereleaseXcodes() -> AnyPublisher<[AvailableXcode], Error> {
|
||||
Current.network.dataTask(with: URLRequest.download)
|
||||
.tryMap { (data, _) -> [Xcode] in
|
||||
.tryMap { (data, _) -> [AvailableXcode] in
|
||||
try self.parsePrereleaseXcodes(from: data)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
private func parsePrereleaseXcodes(from data: Data) throws -> [Xcode] {
|
||||
private func parsePrereleaseXcodes(from data: Data) throws -> [AvailableXcode] {
|
||||
let body = String(data: data, encoding: .utf8)!
|
||||
let document = try SwiftSoup.parse(body)
|
||||
|
||||
|
|
@ -110,19 +149,19 @@ extension XcodeList {
|
|||
|
||||
let filename = String(path.suffix(fromLast: "/"))
|
||||
|
||||
return [Xcode(version: version, url: url, filename: filename, releaseDate: DateFormatter.downloadsReleaseDate.date(from: releaseDateString))]
|
||||
return [AvailableXcode(version: version, url: url, filename: filename, releaseDate: DateFormatter.downloadsReleaseDate.date(from: releaseDateString))]
|
||||
}
|
||||
}
|
||||
|
||||
extension XcodeList {
|
||||
// MARK: XcodeReleases
|
||||
extension AppState {
|
||||
// MARK: - XcodeReleases
|
||||
|
||||
private func xcodeReleases() -> AnyPublisher<[Xcode], Error> {
|
||||
private func xcodeReleases() -> AnyPublisher<[AvailableXcode], Error> {
|
||||
Current.network.dataTask(with: URLRequest(url: URL(string: "https://xcodereleases.com/data.json")!))
|
||||
.map(\.data)
|
||||
.decode(type: [XCModel.Xcode].self, decoder: JSONDecoder())
|
||||
.map { xcReleasesXcodes in
|
||||
let xcodes = xcReleasesXcodes.compactMap { xcReleasesXcode -> Xcode? in
|
||||
let xcodes = xcReleasesXcodes.compactMap { xcReleasesXcode -> AvailableXcode? in
|
||||
guard
|
||||
let downloadURL = xcReleasesXcode.links?.download?.url,
|
||||
let version = Version(xcReleasesXcode: xcReleasesXcode)
|
||||
|
|
@ -134,7 +173,7 @@ extension XcodeList {
|
|||
day: xcReleasesXcode.date.day
|
||||
))
|
||||
|
||||
return Xcode(
|
||||
return AvailableXcode(
|
||||
version: version,
|
||||
url: downloadURL,
|
||||
filename: String(downloadURL.path.suffix(fromLast: "/")),
|
||||
|
|
@ -7,12 +7,16 @@ import KeychainAccess
|
|||
import SwiftUI
|
||||
|
||||
class AppState: ObservableObject {
|
||||
private let list = XcodeList()
|
||||
private let client = AppleAPI.Client()
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
@Published var authenticationState: AuthenticationState = .unauthenticated
|
||||
@Published var allVersions: [XcodeVersion] = []
|
||||
@Published var availableXcodes: [AvailableXcode] = [] {
|
||||
willSet {
|
||||
updateAllVersions(newValue)
|
||||
}
|
||||
}
|
||||
var allVersions: [XcodeVersion] = []
|
||||
@Published var updatePublisher: AnyCancellable?
|
||||
var isUpdating: Bool { updatePublisher != nil }
|
||||
@Published var error: AlertContent?
|
||||
|
|
@ -21,8 +25,8 @@ class AppState: ObservableObject {
|
|||
@Published var isProcessingAuthRequest = false
|
||||
@Published var secondFactorData: SecondFactorData?
|
||||
|
||||
private var dataSource: DataSource {
|
||||
Current.defaults.string(forKey: "dataSource").flatMap(DataSource.init(rawValue:)) ?? .default
|
||||
init() {
|
||||
try? loadCachedAvailableXcodes()
|
||||
}
|
||||
|
||||
// MARK: - Authentication
|
||||
|
|
@ -165,53 +169,29 @@ class AppState: ObservableObject {
|
|||
authenticationState = .unauthenticated
|
||||
}
|
||||
|
||||
// MARK: - Load Xcode Versions
|
||||
// MARK: -
|
||||
|
||||
func update() {
|
||||
guard !isUpdating else { return }
|
||||
updatePublisher = update()
|
||||
.sink(
|
||||
receiveCompletion: { [unowned self] _ in
|
||||
Current.defaults.setDate(Current.date(), forKey: "lastUpdated")
|
||||
self.updatePublisher = nil
|
||||
},
|
||||
receiveValue: { _ in }
|
||||
)
|
||||
func install(id: String) {
|
||||
// TODO:
|
||||
}
|
||||
|
||||
func updateIfNeeded() {
|
||||
guard
|
||||
let lastUpdated = Current.defaults.date(forKey: "lastUpdated"),
|
||||
// This is bad date math but for this use case it doesn't need to be exact
|
||||
lastUpdated < Current.date().addingTimeInterval(-60 * 60 * 24)
|
||||
else { return }
|
||||
update() as Void
|
||||
func uninstall(id: String) {
|
||||
// TODO:
|
||||
}
|
||||
|
||||
private func update() -> AnyPublisher<[Xcode], Never> {
|
||||
signInIfNeeded()
|
||||
.flatMap { [unowned self] in
|
||||
self.list.update(dataSource: self.dataSource)
|
||||
}
|
||||
.handleEvents(
|
||||
receiveCompletion: { completion in
|
||||
if case let .failure(error) = completion {
|
||||
self.error = AlertContent(title: "Update Error", message: error.legibleLocalizedDescription)
|
||||
}
|
||||
}
|
||||
)
|
||||
.catch { _ in
|
||||
Just(self.list.availableXcodes)
|
||||
}
|
||||
.handleEvents(
|
||||
receiveOutput: { [unowned self] xcodes in
|
||||
self.updateAllVersions(xcodes)
|
||||
}
|
||||
)
|
||||
.eraseToAnyPublisher()
|
||||
func reveal(id: String) {
|
||||
// TODO: show error if not
|
||||
guard let installedXcode = Current.files.installedXcodes(Path.root/"Applications").first(where: { $0.version.xcodeDescription == id }) else { return }
|
||||
NSWorkspace.shared.activateFileViewerSelecting([installedXcode.path.url])
|
||||
}
|
||||
|
||||
func select(id: String) {
|
||||
// TODO:
|
||||
}
|
||||
|
||||
private func updateAllVersions(_ xcodes: [Xcode]) {
|
||||
// MARK: - Private
|
||||
|
||||
private func updateAllVersions(_ xcodes: [AvailableXcode]) {
|
||||
let installedXcodes = Current.files.installedXcodes(Path.root/"Applications")
|
||||
var allXcodeVersions = xcodes.map { $0.version }
|
||||
for installedXcode in installedXcodes {
|
||||
|
|
@ -243,26 +223,10 @@ class AppState: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
func install(id: String) {
|
||||
// TODO:
|
||||
}
|
||||
|
||||
func uninstall(id: String) {
|
||||
// TODO:
|
||||
}
|
||||
|
||||
func reveal(id: String) {
|
||||
// TODO: show error if not
|
||||
guard let installedXcode = Current.files.installedXcodes(Path.root/"Applications").first(where: { $0.version.xcodeDescription == id }) else { return }
|
||||
NSWorkspace.shared.activateFileViewerSelecting([installedXcode.path.url])
|
||||
}
|
||||
|
||||
func select(id: String) {
|
||||
// TODO:
|
||||
}
|
||||
|
||||
// MARK: - Nested Types
|
||||
|
||||
/// A merging of AvailableXcode and InstalledXcode prepared for display
|
||||
struct XcodeVersion: Identifiable {
|
||||
let title: String
|
||||
let installState: InstallState
|
||||
|
|
|
|||
Loading…
Reference in a new issue