Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
mcrollin committed Nov 12, 2023
1 parent d4ca8b5 commit c85d7f5
Show file tree
Hide file tree
Showing 43 changed files with 2,030 additions and 151 deletions.
44 changes: 44 additions & 0 deletions .github/workflows/ci.yml
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
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"size" : "512x512"
},
{
"filename" : "logo.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
Expand Down
Binary file added Assets.xcassets/AppIcon.appiconset/logo.png
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.
84 changes: 84 additions & 0 deletions Sources/Archive/State/Explorer.swift
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
}
58 changes: 58 additions & 0 deletions Sources/Archive/State/Finder.swift
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
}
94 changes: 94 additions & 0 deletions Sources/Archive/State/Unarchiver.swift
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
}
}
84 changes: 84 additions & 0 deletions Sources/Archive/Views/ApplicationView.swift
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)
}
}
}
Loading

0 comments on commit c85d7f5

Please sign in to comment.