-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
43 changed files
with
2,030 additions
and
151 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
# This workflow will build a Swift project | ||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift | ||
|
||
name: "SteelyardApp CI" | ||
|
||
on: | ||
push: | ||
branches: | ||
- main | ||
pull_request: | ||
branches: | ||
- '*' | ||
workflow_dispatch: | ||
|
||
concurrency: | ||
group: ${{ github.ref_name }} | ||
cancel-in-progress: true | ||
|
||
jobs: | ||
macOS: | ||
name: ${{ matrix.name }} | ||
runs-on: ${{ matrix.runsOn }} | ||
timeout-minutes: 10 | ||
strategy: | ||
fail-fast: false | ||
matrix: | ||
include: | ||
- xcode: latest | ||
runsOn: macos-13 | ||
name: "macOS 13, Xcode latest" | ||
- xcode: latest-stable | ||
runsOn: macos-13 | ||
name: "macOS 13, Xcode latest-stable" | ||
- xcode: "15.0" | ||
runsOn: macos-13 | ||
name: "macOS 13, Xcode 15.0, Swift 5.9.0" | ||
steps: | ||
- uses: actions/checkout@v4 | ||
- name: Setup Xcode version | ||
uses: maxim-lobanov/setup-xcode@v1 | ||
with: | ||
xcode-version: ${{ matrix.xcode }} | ||
- name: ${{ matrix.name }} | ||
run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -scheme "Steelyard" clean build 2>&1 | xcpretty |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
// | ||
// Copyright © Marc Rollin. | ||
// | ||
|
||
import ApplicationArchive | ||
import Dependencies | ||
import DesignSystem | ||
import SwiftUI | ||
|
||
// MARK: - Explorer | ||
|
||
@Observable | ||
final class Explorer { | ||
|
||
// MARK: Lifecycle | ||
|
||
init(archive: Archive) { | ||
self.archive = archive | ||
currentNode = archive.apps.first?.node ?? archive.root | ||
duplicates = archive.duplicateIDs | ||
} | ||
|
||
// MARK: Public | ||
|
||
public func path(from node: ArchiveNode) -> [ArchiveNode] { | ||
var path = [node] | ||
var node = node | ||
while let parent = archive.parents[node.id] { | ||
path.insert(parent, at: 0) | ||
node = parent | ||
} | ||
return path | ||
} | ||
|
||
// MARK: Internal | ||
|
||
let archive: Archive | ||
private(set) var currentNode: ArchiveNode | ||
private(set) var hoveringNode: ArchiveNode? | ||
private let duplicates: Set<ArchiveNode.ID> | ||
|
||
// @ObservationIgnored @AppStorage("show_duplicates") private var __isShowingDuplicates = true | ||
// var isShowingDuplicates: Bool { | ||
// get { | ||
// access(keyPath: \.isShowingDuplicates) | ||
// return __isShowingDuplicates | ||
// } set { | ||
// withMutation(keyPath: \.isShowingDuplicates) { | ||
// __isShowingDuplicates = newValue | ||
// } | ||
// } | ||
// } | ||
|
||
func select(node: ArchiveNode, animated: Bool = true) { | ||
withAnimation(animated ? designSystem.animation(.select) : .none) { | ||
currentNode = node | ||
} | ||
} | ||
|
||
func hover(node: ArchiveNode, hovering: Bool, animated: Bool = true) { | ||
withAnimation(animated ? designSystem.animation(.highlight) : .none) { | ||
hoveringStack.removeAll { $0 == node } | ||
|
||
if hovering { | ||
hoveringStack.append(node) | ||
} | ||
|
||
hoveringNode = hoveringStack.last | ||
} | ||
} | ||
|
||
func parent(for node: ArchiveNode) -> ArchiveNode? { | ||
archive.parents[node.id] | ||
} | ||
|
||
func hasDuplicates(node: ArchiveNode) -> Bool { | ||
duplicates.contains(node.id) | ||
} | ||
|
||
// MARK: Private | ||
|
||
private var hoveringStack = [ArchiveNode]() | ||
@ObservationIgnored @Dependency(\.designSystem) private var designSystem | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
// | ||
// Copyright © Marc Rollin. | ||
// | ||
|
||
import ApplicationArchive | ||
import Dependencies | ||
import DesignSystem | ||
import SwiftUI | ||
|
||
// MARK: - Finder | ||
|
||
@Observable | ||
final class Finder { | ||
|
||
// MARK: Internal | ||
|
||
func isDisclosed(_ node: ArchiveNode) -> Binding<Bool> { | ||
Binding( | ||
get: { | ||
self.expandedNodes.contains(node.id) | ||
}, | ||
set: { newValue in | ||
if newValue { | ||
self.expandedNodes.insert(node.id) | ||
} else { | ||
self.expandedNodes.remove(node.id) | ||
} | ||
} | ||
) | ||
} | ||
|
||
@discardableResult | ||
func disclose(node: ArchiveNode, animated: Bool = true) -> Bool { | ||
withAnimation(animated ? designSystem.animation(.disclose) : .none) { | ||
expandedNodes.insert(node.id).inserted | ||
} | ||
} | ||
|
||
@discardableResult | ||
func conceal(node: ArchiveNode, animated: Bool = true) -> Bool { | ||
withAnimation(animated ? designSystem.animation(.disclose) : .none) { | ||
expandedNodes.remove(node.id) != nil | ||
} | ||
} | ||
|
||
func toggle(node: ArchiveNode, animated: Bool = true) { | ||
if expandedNodes.contains(node.id) { | ||
conceal(node: node, animated: animated) | ||
} else { | ||
disclose(node: node, animated: animated) | ||
} | ||
} | ||
|
||
// MARK: Private | ||
|
||
private var expandedNodes: Set<ArchiveNode.ID> = [] | ||
@ObservationIgnored @Dependency(\.designSystem) private var designSystem | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
// | ||
// Copyright © Marc Rollin. | ||
// | ||
|
||
import ApplicationArchive | ||
import Dependencies | ||
import DesignComponents | ||
import Foundation | ||
import UniformTypeIdentifiers | ||
|
||
// MARK: - Unarchiver | ||
|
||
@MainActor | ||
@Observable | ||
public final class Unarchiver { | ||
|
||
// MARK: Lifecycle | ||
|
||
init() { } | ||
|
||
// MARK: Internal | ||
|
||
private(set) var archive: Archive? | ||
|
||
var isProcessing: Bool { | ||
processingTask != nil | ||
|| archive != nil | ||
|| processingTask?.isCancelled == true | ||
} | ||
|
||
func open(at url: URL) async { | ||
close() | ||
let processingTask = Task<Archive, Error> { | ||
try await Self.processArchive(at: url) | ||
} | ||
self.processingTask = processingTask | ||
|
||
do { | ||
archive = try await processingTask.value | ||
} catch { | ||
toaster.show(message: error.localizedDescription, level: .error) | ||
} | ||
} | ||
|
||
func close() { | ||
processingTask?.cancel() | ||
processingTask = nil | ||
archive = nil | ||
} | ||
|
||
// MARK: Private | ||
|
||
private enum UnarchivingError: LocalizedError { | ||
case invalidFileType(String) | ||
case unidentifiedFileType(URL) | ||
case cancelled | ||
|
||
var errorDescription: String? { | ||
switch self { | ||
case .invalidFileType(let type): | ||
"Cannot handle \(type) files.\nPlease ensure the file is either a .ipa or a .app format and try again." | ||
case .unidentifiedFileType(let url): | ||
"The file type for \(url.relativePath) could not be determined.\nPlease verify the file is either a .ipa or a .app format and try again." | ||
case .cancelled: | ||
"Cancelled" | ||
} | ||
} | ||
} | ||
|
||
@ObservationIgnored @Dependency(\.toaster) private var toaster | ||
@ObservationIgnored private var processingTask: Task<Archive, Error>? | ||
|
||
private static func processArchive(at url: URL) async throws -> Archive { | ||
guard let identifier = try? url.resourceValues(forKeys: [.typeIdentifierKey]).typeIdentifier else { | ||
throw UnarchivingError.unidentifiedFileType(url) | ||
} | ||
|
||
let isCompressed: Bool = if UTType(identifier)?.conforms(to: .bundle) == true { | ||
false | ||
} else if UTType(identifier)?.conforms(to: .ipa) == true { | ||
true | ||
} else { | ||
throw UnarchivingError.invalidFileType(identifier) | ||
} | ||
|
||
let archive = try await Archive(from: url, isCompressed: isCompressed) | ||
|
||
guard !Task.isCancelled else { | ||
throw UnarchivingError.cancelled | ||
} | ||
|
||
return archive | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
// | ||
// Copyright © Marc Rollin. | ||
// | ||
|
||
import ApplicationArchive | ||
import DesignComponents | ||
import DesignSystem | ||
import SwiftUI | ||
|
||
// MARK: - ApplicationDisplayable | ||
|
||
public protocol ApplicationDisplayable { | ||
var name: String { get } | ||
var icon: Data? { get } | ||
var version: String { get } | ||
var platforms: [ArchiveApp.Platform] { get } | ||
} | ||
|
||
// MARK: - ApplicationView | ||
|
||
public struct ApplicationView: View { | ||
|
||
// MARK: Lifecycle | ||
|
||
public init(application: ApplicationDisplayable, didTap: (() -> Void)? = nil) { | ||
self.application = application | ||
self.didTap = didTap | ||
} | ||
|
||
// MARK: Public | ||
|
||
public var body: some View { | ||
Button { | ||
didTap?() | ||
} label: { | ||
HStack { | ||
icon(application.icon) | ||
.background(.secondary.opacity(designSystem.opacity(.faint))) | ||
.clipShape(RoundedRectangle(cornerRadius: 8)) | ||
.overlay(RoundedRectangle(cornerRadius: 8).stroke(.secondary, lineWidth: 1)) | ||
|
||
VStack(alignment: .leading) { | ||
Text(application.name) | ||
Text(application.version) | ||
.font(.subheadline) | ||
.foregroundStyle(.secondary) | ||
} | ||
Spacer() | ||
ForEach(application.platforms, id: \.self) { platform in | ||
platform.icon | ||
.frame(width: 16, height: 16) | ||
.foregroundStyle(.secondary) | ||
} | ||
} | ||
} | ||
.buttonStyle(.plain) | ||
} | ||
|
||
// MARK: Internal | ||
|
||
let application: ApplicationDisplayable | ||
let didTap: (() -> Void)? | ||
|
||
// MARK: Private | ||
|
||
@Environment(DesignSystem.self) private var designSystem | ||
|
||
@ViewBuilder | ||
private func icon(_ data: Data?) -> some View { | ||
if let data, | ||
let nsImage = NSImage(data: data) { | ||
Image(nsImage: nsImage) | ||
.resizable() | ||
.aspectRatio(contentMode: .fit) | ||
.frame(maxWidth: 32, maxHeight: 32) | ||
} else { | ||
Image(systemName: "apple.logo") | ||
.resizable() | ||
.aspectRatio(contentMode: .fit) | ||
.frame(maxWidth: 16, maxHeight: 16) | ||
.padding(8) | ||
} | ||
} | ||
} |
Oops, something went wrong.