diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..56c08a3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -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 \ No newline at end of file diff --git a/Sources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 96% rename from Sources/Assets.xcassets/AppIcon.appiconset/Contents.json rename to Assets.xcassets/AppIcon.appiconset/Contents.json index 3f00db4..5becc02 100644 --- a/Sources/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -46,6 +46,7 @@ "size" : "512x512" }, { + "filename" : "logo.png", "idiom" : "mac", "scale" : "2x", "size" : "512x512" diff --git a/Assets.xcassets/AppIcon.appiconset/logo.png b/Assets.xcassets/AppIcon.appiconset/logo.png new file mode 100644 index 0000000..102504a Binary files /dev/null and b/Assets.xcassets/AppIcon.appiconset/logo.png differ diff --git a/Sources/Assets.xcassets/Contents.json b/Assets.xcassets/Contents.json similarity index 100% rename from Sources/Assets.xcassets/Contents.json rename to Assets.xcassets/Contents.json diff --git a/Sources/Preview Content/Preview Assets.xcassets/Contents.json b/Preview Content/Preview Assets.xcassets/Contents.json similarity index 100% rename from Sources/Preview Content/Preview Assets.xcassets/Contents.json rename to Preview Content/Preview Assets.xcassets/Contents.json diff --git a/Sources/Archive/State/Explorer.swift b/Sources/Archive/State/Explorer.swift new file mode 100644 index 0000000..1c11ef0 --- /dev/null +++ b/Sources/Archive/State/Explorer.swift @@ -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 + +// @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 +} diff --git a/Sources/Archive/State/Finder.swift b/Sources/Archive/State/Finder.swift new file mode 100644 index 0000000..c25ca38 --- /dev/null +++ b/Sources/Archive/State/Finder.swift @@ -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 { + 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 = [] + @ObservationIgnored @Dependency(\.designSystem) private var designSystem +} diff --git a/Sources/Archive/State/Unarchiver.swift b/Sources/Archive/State/Unarchiver.swift new file mode 100644 index 0000000..cf0b041 --- /dev/null +++ b/Sources/Archive/State/Unarchiver.swift @@ -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 { + 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? + + 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 + } +} diff --git a/Sources/Archive/Views/ApplicationView.swift b/Sources/Archive/Views/ApplicationView.swift new file mode 100644 index 0000000..558c94c --- /dev/null +++ b/Sources/Archive/Views/ApplicationView.swift @@ -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) + } + } +} diff --git a/Sources/Archive/Views/ArchiveView.swift b/Sources/Archive/Views/ArchiveView.swift new file mode 100644 index 0000000..7a0333f --- /dev/null +++ b/Sources/Archive/Views/ArchiveView.swift @@ -0,0 +1,37 @@ +// +// Copyright © Marc Rollin. +// + +import ApplicationArchive +import DesignComponents +import DesignSystem +import SwiftUI + +// MARK: - ArchiveView + +@MainActor +struct ArchiveView: View { + + // MARK: Internal + + var body: some View { + content + .background(designSystem.color(.background)) + } + + @ViewBuilder + var content: some View { + if let archive = unarchiver.archive { + ExplorerNavigation(archive: archive) + } else if unarchiver.isProcessing { + LoadingView() + } else { + LauncherView() + } + } + + // MARK: Private + + @Environment(DesignSystem.self) private var designSystem + @Environment(Unarchiver.self) private var unarchiver +} diff --git a/Sources/Archive/Views/Explorer/Content/ExplorerContent.swift b/Sources/Archive/Views/Explorer/Content/ExplorerContent.swift new file mode 100644 index 0000000..7da1d88 --- /dev/null +++ b/Sources/Archive/Views/Explorer/Content/ExplorerContent.swift @@ -0,0 +1,32 @@ +// +// Copyright © Marc Rollin. +// + +import DesignComponents +import DesignSystem +import SwiftUI + +struct ExplorerContent: View { + + // MARK: Internal + + var body: some View { + VStack(spacing: .zero) { + TreeMap( + node: explorer.currentNode, + hovering: explorer.hoveringNode, + duplicates: explorer.archive.duplicateIDs + ) { node in + explorer.select(node: node) + } onHover: { node, hovering in + explorer.hover(node: node, hovering: hovering) + }.layoutPriority(1) + PathView() + } + } + + // MARK: Private + + @Environment(Explorer.self) private var explorer + @Environment(DesignSystem.self) private var designSystem +} diff --git a/Sources/Archive/Views/Explorer/Content/PathItemView.swift b/Sources/Archive/Views/Explorer/Content/PathItemView.swift new file mode 100644 index 0000000..c3890d8 --- /dev/null +++ b/Sources/Archive/Views/Explorer/Content/PathItemView.swift @@ -0,0 +1,73 @@ +// +// Copyright © Marc Rollin. +// + +import ApplicationArchive +import DesignSystem +import SwiftUI + +// MARK: - PathItemView + +struct PathItemView: View { + + // MARK: Internal + + let node: ArchiveNode + let path: [ArchiveNode] + + var body: some View { + HStack { + disclosure + Text(node.name) + .fontWeight(.semibold) + .foregroundStyle(foregroundStyle) + .frame(minWidth: 20) + } + .layoutPriority(layoutPriority) + .onHover { hovering in + explorer.hover(node: node, hovering: hovering) + } + .onTapGesture { + explorer.select(node: node) + } + } + + // MARK: Private + + @Environment(Explorer.self) private var explorer + @Environment(DesignSystem.self) private var designSystem + + @ViewBuilder + private var disclosure: some View { + if node != path.first { + Image(systemName: "chevron.right") + .resizable() + .scaledToFit() + .frame(width: 8, height: 8, alignment: .center) + .foregroundColor(.secondary) + .layoutPriority(0.75) + } else { + EmptyView() + } + } + + private var foregroundStyle: Color { + if node == explorer.hoveringNode { + designSystem.color(.highlight) + } else { + .primary + } + } + + private var layoutPriority: Double { + if node == explorer.hoveringNode { + 1.0 + } else if node == path.last { + 0.5 + } else if node == path.first { + 0.25 + } else { + 0.1 + } + } +} diff --git a/Sources/Archive/Views/Explorer/Content/PathView.swift b/Sources/Archive/Views/Explorer/Content/PathView.swift new file mode 100644 index 0000000..7996d27 --- /dev/null +++ b/Sources/Archive/Views/Explorer/Content/PathView.swift @@ -0,0 +1,27 @@ +// +// Copyright © Marc Rollin. +// + +import DesignSystem +import SwiftUI + +// MARK: - PathView + +struct PathView: View { + + var body: some View { + let path = explorer.path(from: explorer.currentNode) + HStack { + ForEach(path) { node in + PathItemView(node: node, path: path) + } + Spacer() + } + .padding(.horizontal, designSystem.spacing) + .padding(.vertical, designSystem.spacing(.semiSmall)) + .background(designSystem.color(.backgroundSubdued)) + } + + @Environment(Explorer.self) private var explorer + @Environment(DesignSystem.self) private var designSystem +} diff --git a/Sources/Archive/Views/Explorer/ExplorerInspector.swift b/Sources/Archive/Views/Explorer/ExplorerInspector.swift new file mode 100644 index 0000000..6dad838 --- /dev/null +++ b/Sources/Archive/Views/Explorer/ExplorerInspector.swift @@ -0,0 +1,87 @@ +// +// Copyright © Marc Rollin. +// + +import ApplicationArchive +import DesignSystem +import SwiftUI + +// MARK: - Inspector + +struct ExplorerInspector: View { + let node: ArchiveNode + + var body: some View { + List { + if let sections = node.metadata?.inspectorSections() { + ForEach(sections) { section in + sectionGrid(section) + } + } + } + .listStyle(.sidebar) + } + + private func sectionGrid(_ section: InspectorSection) -> some View { + Section(section.title) { + Grid( + alignment: .topLeading, + horizontalSpacing: designSystem.spacing(.small), + verticalSpacing: designSystem.spacing(.small) + ) { + ForEach(section.items) { item in +// if section.items.first?.id != item.id { +// Divider().gridCellUnsizedAxes(.horizontal) +// } + GridRow(alignment: .firstTextBaseline) { + switch item { + case let .text(key, value): + Text(key) + .font(.body) + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity, alignment: .trailing) + Text(value.description) + .font(.body) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + } + } + } + } + + @Environment(DesignSystem.self) private var designSystem +} + +struct InspectorSection: Identifiable { + enum Item: Identifiable { + case text(key: String, value: any CustomStringConvertible) + + var id: String { + switch self { + case let .text(key, _): + key + } + } + } + + let id = UUID() + let title: String + let items: [Item] +} + +extension ArchiveNodeMetadata { + + fileprivate func inspectorSections() -> [InspectorSection] { + switch self { + case let .app(info), + let .appex(info): + [ + .init(title: "Info", items: [ + .text(key: "marketing version", value: info.shortVersion), + .text(key: "build number", value: info.version), + ]) + ] + } + } +} diff --git a/Sources/Archive/Views/Explorer/ExplorerNavigation.swift b/Sources/Archive/Views/Explorer/ExplorerNavigation.swift new file mode 100644 index 0000000..e761847 --- /dev/null +++ b/Sources/Archive/Views/Explorer/ExplorerNavigation.swift @@ -0,0 +1,84 @@ +// +// Copyright © Marc Rollin. +// + +import ApplicationArchive +import DesignSystem +import SwiftUI + +// MARK: - ArchiveExplorerView + +@MainActor +struct ExplorerNavigation: View { + + // MARK: Lifecycle + + init(archive: Archive) { + _explorer = .init(initialValue: Explorer(archive: archive)) + } + + // MARK: Internal + + var body: some View { + NavigationSplitView { + ExplorerSidebar() + .navigationSplitViewColumnWidth(min: 320, ideal: 400, max: 500) + } detail: { + ExplorerContent() + .background(designSystem.color(.background)) + .navigationSplitViewColumnWidth(min: 400, ideal: 800) + .inspector(isPresented: $showInspector) { + ExplorerInspector(node: explorer.currentNode) + .inspectorColumnWidth(min: 300, ideal: 360, max: 400) + } + } + .navigationTitle(explorer.currentNode.name) + .navigationSubtitle(explorer.currentNode.sizeInBytes.formattedBytes()) + .toolbar { + toolbarContent + } + .environment(explorer) + } + + // MARK: Private + + @Environment(Unarchiver.self) private var unarchiver + @Environment(DesignSystem.self) private var designSystem + @State private var explorer: Explorer + @State private var showInspector = true + + @ToolbarContentBuilder + private var toolbarContent: some ToolbarContent { + if let parent = explorer.parent(for: explorer.currentNode) { + ToolbarItem(placement: .navigation) { + Button { + explorer.select(node: parent) + } label: { + Label("Navigate to parent item", systemImage: "arrow.backward") + } + } + } + + ToolbarItem(placement: .navigation) { + explorer.currentNode.icon + .foregroundStyle(explorer.currentNode.color ?? .secondary) + .frame(width: 20, height: 20, alignment: .center) + } + + ToolbarItem(placement: .cancellationAction) { + Button { + unarchiver.close() + } label: { + Label("Close archive", systemImage: "xmark.circle") + } + } + + ToolbarItem(placement: .confirmationAction) { + Button { + showInspector.toggle() + } label: { + Label("Toggle Inspector", systemImage: "sidebar.trailing") + } + } + } +} diff --git a/Sources/Archive/Views/Explorer/Sidebar/ExpandableGroup.swift b/Sources/Archive/Views/Explorer/Sidebar/ExpandableGroup.swift new file mode 100644 index 0000000..e380144 --- /dev/null +++ b/Sources/Archive/Views/Explorer/Sidebar/ExpandableGroup.swift @@ -0,0 +1,41 @@ +// +// Copyright © Marc Rollin. +// + +import SwiftUI + +// MARK: - BranchDisclosureGroup + +public struct ExpandableGroup: View { + + // MARK: Lifecycle + + public init( + isExpanded: Binding, + content: @escaping () -> Content, + label: @escaping () -> Label + ) { + _isExpanded = isExpanded + self.content = content + self.label = label + } + + // MARK: Public + + public var body: some View { + VStack(spacing: .zero) { + label() + LazyVStack(spacing: .zero) { + if isExpanded { + content() + } + } + } + } + + // MARK: Internal + + @Binding var isExpanded: Bool + let content: () -> Content + let label: () -> Label +} diff --git a/Sources/Archive/Views/Explorer/Sidebar/ExplorerSidebar.swift b/Sources/Archive/Views/Explorer/Sidebar/ExplorerSidebar.swift new file mode 100644 index 0000000..2fb0388 --- /dev/null +++ b/Sources/Archive/Views/Explorer/Sidebar/ExplorerSidebar.swift @@ -0,0 +1,21 @@ +// +// Copyright © Marc Rollin. +// + +import SwiftUI + +struct ExplorerSidebar: View { + + // MARK: Internal + + var body: some View { + VStack(spacing: .zero) { + ExplorerSidebarTreeView(node: explorer.archive.root) + ExplorerSidebarApplicationsView() + } + } + + // MARK: Private + + @Environment(Explorer.self) private var explorer +} diff --git a/Sources/Archive/Views/Explorer/Sidebar/ExplorerSidebarApplicationsView.swift b/Sources/Archive/Views/Explorer/Sidebar/ExplorerSidebarApplicationsView.swift new file mode 100644 index 0000000..77bf47b --- /dev/null +++ b/Sources/Archive/Views/Explorer/Sidebar/ExplorerSidebarApplicationsView.swift @@ -0,0 +1,61 @@ +// +// Copyright © Marc Rollin. +// + +import ApplicationArchive +import DesignComponents +import DesignSystem +import SwiftUI + +// MARK: - ExplorerSidebarApplicationsView + +struct ExplorerSidebarApplicationsView: View { + + // MARK: Internal + + var body: some View { + ExpandableGroup(isExpanded: $isExpanded) { + VStack(spacing: designSystem.spacing) { + ForEach(explorer.archive.apps) { app in + ApplicationView(application: app) { + explorer.select(node: app.node) + } + } + } + .padding(.top, designSystem.spacing) + .padding(.bottom, designSystem.spacing(.small)) + } label: { + HStack { + Text("Applications") + .font(.headline) + Spacer() + if isHovering { + DiscloseButton( + isExpanded: $isExpanded, + size: 10, + padding: .extraExtraSmall + ) + } + } + .foregroundColor(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + .onHover { hovering in + isHovering = hovering + } + } + .padding(.horizontal, designSystem.spacing) + .padding(.vertical, designSystem.spacing(.semiSmall)) + .background(designSystem.color(.backgroundSubdued)) + } + + // MARK: Private + + @State private var isHovering = false + @State private var isExpanded = true + @Environment(Explorer.self) private var explorer + @Environment(DesignSystem.self) private var designSystem +} + +// MARK: - ArchiveApp + ApplicationDisplayable + +extension ArchiveApp: ApplicationDisplayable { } diff --git a/Sources/Archive/Views/Explorer/Sidebar/Tree/ExplorerSidebarTreeBranchView.swift b/Sources/Archive/Views/Explorer/Sidebar/Tree/ExplorerSidebarTreeBranchView.swift new file mode 100644 index 0000000..ae7ffd9 --- /dev/null +++ b/Sources/Archive/Views/Explorer/Sidebar/Tree/ExplorerSidebarTreeBranchView.swift @@ -0,0 +1,35 @@ +// +// Copyright © Marc Rollin. +// + +import ApplicationArchive +import DesignComponents +import SwiftUI + +// MARK: - ExplorerSidebarTreeBranchView + +struct ExplorerSidebarTreeBranchView: View { + + // MARK: Internal + + let node: ArchiveNode + let level: Int + + var body: some View { + if node.children.isEmpty { + ExplorerSidebarTreeLeafView(node: node, level: level) + } else { + ExpandableGroup(isExpanded: finder.isDisclosed(node)) { + ForEach(node.childrenByCategory) { child in + ExplorerSidebarTreeBranchView(node: child, level: level + 1) + } + } label: { + ExplorerSidebarTreeLeafView(node: node, level: level) + } + } + } + + // MARK: Private + + @Environment(Finder.self) private var finder +} diff --git a/Sources/Archive/Views/Explorer/Sidebar/Tree/ExplorerSidebarTreeLeafView.swift b/Sources/Archive/Views/Explorer/Sidebar/Tree/ExplorerSidebarTreeLeafView.swift new file mode 100644 index 0000000..3f78c7b --- /dev/null +++ b/Sources/Archive/Views/Explorer/Sidebar/Tree/ExplorerSidebarTreeLeafView.swift @@ -0,0 +1,107 @@ +// +// Copyright © Marc Rollin. +// + +import ApplicationArchive +import DesignComponents +import DesignSystem +import SwiftUI + +// MARK: - ExplorerSidebarTreeItemView + +struct ExplorerSidebarTreeLeafView: View { + + // MARK: Internal + + let node: ArchiveNode + let level: Int + + var body: some View { + let foreground = foregroundColor(node: node) + let background = backgroundColor(node: node) + + return HStack(spacing: 0) { + DiscloseButton(isExpanded: finder.isDisclosed(node)) + .foregroundColor(foreground ?? .secondary) + .opacity(designSystem.opacity(node.children.isEmpty ? .clear : .opaque)) + + node.icon + .foregroundColor(foreground ?? node.color ?? .secondary) + .frame(width: 14, height: 14, alignment: .center) + .padding(.trailing, designSystem.spacing(.extraSmall)) + + Text(node.name) + .font(.system(.body)) + .foregroundColor(foreground ?? .primary) + .lineLimit(1) + .padding(.trailing, designSystem.spacing(.extraSmall)) + + Spacer() + + Text(node.sizeInBytes.formattedBytes()) + .font(.system(.caption).weight(.bold).monospacedDigit()) + .foregroundStyle(.secondary) + .lineLimit(1) + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background( + Capsule() + .fill(designSystem.color(.background).opacity(designSystem.opacity(.medium))) + .stroke(.secondary.opacity(designSystem.opacity(.faint)), lineWidth: 1) + ) + .layoutPriority(1) + } + .padding(.vertical, designSystem.spacing(.extraExtraSmall) + 1) + .padding(.leading, designSystem.spacing(.extraExtraSmall) + (designSystem.spacing * CGFloat(level))) + .padding(.trailing, designSystem.spacing(.small)) + .background { + RoundedRectangle(cornerRadius: 6, style: .circular) + .fill(background) + } + .id(node.id) + .onHover { hovering in + explorer.hover(node: node, hovering: hovering) + } + .onTapGesture { + select(node: node) + } + } + + // MARK: Private + + @Environment(Finder.self) private var finder + @Environment(Explorer.self) private var explorer + @Environment(DesignSystem.self) private var designSystem + + private func select(node: ArchiveNode, animated: Bool = true) { + if explorer.currentNode == node { + finder.toggle(node: node, animated: animated) + } else { + explorer.select(node: node, animated: animated) + } + } + + private func foregroundColor(node: ArchiveNode) -> Color? { + if explorer.hasDuplicates(node: node) { + designSystem.color(.negative) + } else if explorer.currentNode == node { + designSystem.color(.highlight) + } else { + nil + } + } + + private func backgroundColor(node: ArchiveNode) -> Color { + if explorer.currentNode == node { + if explorer.hasDuplicates(node: node) { + designSystem.color(.negative).opacity(designSystem.opacity(.faint)) + } else { + designSystem.color(.highlight).opacity(designSystem.opacity(.faint)) + } + } else if explorer.hoveringNode == node { + designSystem.color(.highlight).opacity(designSystem.opacity(.hint)) + } else { + .black.opacity(designSystem.opacity(.veil)) + } + } +} diff --git a/Sources/Archive/Views/Explorer/Sidebar/Tree/ExplorerSidebarTreeView.swift b/Sources/Archive/Views/Explorer/Sidebar/Tree/ExplorerSidebarTreeView.swift new file mode 100644 index 0000000..2abbd3a --- /dev/null +++ b/Sources/Archive/Views/Explorer/Sidebar/Tree/ExplorerSidebarTreeView.swift @@ -0,0 +1,102 @@ +// +// Copyright © Marc Rollin. +// + +import ApplicationArchive +import DesignComponents +import DesignSystem +import SwiftUI + +struct ExplorerSidebarTreeView: View { + + // MARK: Internal + + let node: ArchiveNode + + var body: some View { + ZStack { + ScrollView { + ExplorerSidebarTreeBranchView(node: node, level: 0) + .padding(designSystem.spacing) + } + .frame(minHeight: 20, maxHeight: .infinity) + shortcuts + } + .onAppear { + disclose(from: explorer.currentNode) + } + .onChange(of: explorer.currentNode) { _, _ in + disclose(from: explorer.parent(for: explorer.currentNode)) + } + .environment(finder) + } + + // MARK: Private + + @Environment(DesignSystem.self) private var designSystem + @Environment(Explorer.self) private var explorer + + @State private var finder = Finder() + + private var shortcuts: some View { + ZStack { + Button(String()) { + let node = explorer.currentNode + + if finder.isDisclosed(node).wrappedValue, + let firstChild = node.childrenByCategory.first { + explorer.select(node: firstChild) + } else { + var node = node + while let parent = explorer.parent(for: node) { + if let next = parent.childrenByCategory.element(after: node) { + explorer.select(node: next) + break + } + node = parent + } + } + }.keyboardShortcut(.downArrow, modifiers: []) + + Button(String()) { + let node = explorer.currentNode + + guard let parent = explorer.parent(for: node) else { return } + if parent.childrenByCategory.first == node { + explorer.select(node: parent) + } else if var previous = parent.childrenByCategory.element(before: node) { + while finder.isDisclosed(previous).wrappedValue, + let last = previous.childrenByCategory.last { + previous = last + } + explorer.select(node: previous) + } + }.keyboardShortcut(.upArrow, modifiers: []) + + Button(String()) { + if !finder.conceal(node: explorer.currentNode) + || !explorer.currentNode.shouldShowDetails, + let parent = explorer.parent(for: explorer.currentNode) { + explorer.select(node: parent) + } + }.keyboardShortcut(.leftArrow, modifiers: []) + + Button(String()) { + if !finder.disclose(node: explorer.currentNode), + let firstChild = explorer.currentNode.childrenByCategory.first { + explorer.select(node: firstChild) + } + }.keyboardShortcut(.rightArrow, modifiers: []) + } + .opacity(designSystem.opacity(.clear)) + } + + private func disclose(from node: ArchiveNode?) { + var node: ArchiveNode? = node + while let next = node { + finder.disclose(node: next) + node = explorer.parent(for: next) + } + } + +} diff --git a/Sources/Archive/Views/Launcher/LauncherView.swift b/Sources/Archive/Views/Launcher/LauncherView.swift new file mode 100644 index 0000000..8fe9e99 --- /dev/null +++ b/Sources/Archive/Views/Launcher/LauncherView.swift @@ -0,0 +1,167 @@ +// +// Copyright © Marc Rollin. +// + +import DesignComponents +import DesignSystem +import Platform +import SwiftUI +import UniformTypeIdentifiers + +// MARK: - LauncherView + +struct LauncherView: View { + + // MARK: Internal + + var body: some View { + content + .padding(.vertical, designSystem.spacing(.large)) + .padding(.horizontal, designSystem.spacing(.extraExtraLarge)) + .inspector(isPresented: $showInspector) { + RecentlyOpenedView() + .inspectorColumnWidth( + min: 300, ideal: 360, max: 400 + ) + } +// .blur(radius: isDragging ? 10 : 0) + .onDrop(of: [.fileURL], isTargeted: $isDragging) { providers, _ in + handleDrop(providers: providers) + return true + } + .overlay { + if isDragging { + Image(systemName: "arrow.down.circle.dotted") + .symbolEffect(.bounce.up.byLayer, options: .repeating.speed(0.8), value: dropAnimationRunning) + .font(.system(size: 50)) + .onAppear { + dropAnimationRunning = true + } + .onDisappear { + dropAnimationRunning = false + } + } + } + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button { + showInspector.toggle() + } label: { + Label("Toggle Inspector", systemImage: "sidebar.trailing") + } + } + } + } + + @ViewBuilder + var content: some View { + VStack(spacing: designSystem.spacing) { + VStack(spacing: designSystem.spacing(.small)) { + Image(nsImage: NSImage(resource: .init(name: "AppIcon", bundle: .main))) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 120, height: 120) + .border(.primary) + Text("Steelyard") + .font(.system(size: 40, weight: .black)) + + if let marketingVersion = Bundle.marketingVersion, + let buildNumber = Bundle.buildNumber { + Text("Version \(marketingVersion) (\(buildNumber))") + .font(.body) + .foregroundStyle(.secondary) + } + } + .padding(.bottom, designSystem.spacing) + + Button { + openFilePicker() + } label: { + HStack { + Image(systemName: "folder") + .foregroundColor(.secondary) + Text("Open File...") + } + } + .buttonStyle(.secondary) + + Text("Drag an IPA file here or click the button to select one") + } + } + + // MARK: Private + + @Environment(Unarchiver.self) private var unarchiver + + @State private var dropAnimationRunning = false + @State private var isDragging = false + @State private var showInspector = true + + @Environment(DesignSystem.self) private var designSystem + + private func handleDrop(providers: [NSItemProvider]) { + guard let itemProvider = providers.first(where: { $0.canLoadObject(ofClass: URL.self) }) else { return } + + _ = itemProvider.loadDataRepresentation(for: UTType.fileURL) { [unarchiver] data, _ in + Task { + guard let data, let url = URL(dataRepresentation: data, relativeTo: nil) else { + return + } + + await unarchiver.open(at: url) + } + } + } + + private func openFilePicker() { + Task { @MainActor [unarchiver] in + let panel = NSOpenPanel()..{ + $0.allowedContentTypes = [.ipa, .bundle] + } + + guard panel.runModal() == .OK, let url = panel.url else { return } + + await unarchiver.open(at: url) + } + } +} + + +// MARK: - SecondaryButtonStyle + +struct SecondaryButtonStyle: ButtonStyle { + + func makeBody(configuration: Configuration) -> some View { + configuration.label + .padding(16) + .foregroundColor(.primary) + .font(.headline) + .frame(maxWidth: .infinity) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(Color.secondary.opacity(0.2)) + ) + .animation(.easeInOut, value: configuration.isPressed) + .onHover { hover in + withAnimation { + if hover { + // Your hover state changes + } else { + // Revert to default state + } + } + } + } +} + +extension ButtonStyle where Self == SecondaryButtonStyle { + static var secondary: SecondaryButtonStyle { + SecondaryButtonStyle() + } +} + +extension View { + func secondaryButtonStyle() -> some View { + buttonStyle(SecondaryButtonStyle()) + } +} diff --git a/Sources/Archive/Views/Launcher/RecentArchive.swift b/Sources/Archive/Views/Launcher/RecentArchive.swift new file mode 100644 index 0000000..06c2fa4 --- /dev/null +++ b/Sources/Archive/Views/Launcher/RecentArchive.swift @@ -0,0 +1,38 @@ +// +// Copyright © Marc Rollin. +// + +import ApplicationArchive +import Dependencies +import Foundation +import SwiftData + +@Model +final class RecentArchive { + + // MARK: Lifecycle + + init(url: URL, name: String, version: String, platforms: [ArchiveApp.Platform], icon: Data?, openedDate: Date = now) { + self.url = url + self.name = name + self.version = version + rawPlatforms = platforms.map(\.rawValue) + self.icon = icon + self.openedDate = openedDate + } + + // MARK: Internal + + @Dependency(\.date.now) static var now + + @Attribute(.unique) let url: URL + let name: String + let version: String + let rawPlatforms: [String] + let icon: Data? + let openedDate: Date + + var platforms: [ArchiveApp.Platform] { + rawPlatforms.compactMap(ArchiveApp.Platform.init) + } +} diff --git a/Sources/Archive/Views/Launcher/RecentlyOpenedView.swift b/Sources/Archive/Views/Launcher/RecentlyOpenedView.swift new file mode 100644 index 0000000..5a32fcc --- /dev/null +++ b/Sources/Archive/Views/Launcher/RecentlyOpenedView.swift @@ -0,0 +1,48 @@ +// +// Copyright © Marc Rollin. +// + +import ApplicationArchive +import DesignComponents +import DesignSystem +import SwiftData +import SwiftUI + +// MARK: - RecentlyOpenedView + +struct RecentlyOpenedView: View { + + // MARK: Internal + + var body: some View { + if recentArchives.isEmpty { + Text("No Recent Archives") + .font(.body) + .foregroundStyle(.secondary) + } else { + ZStack { + List(recentArchives) { item in + ApplicationView(application: item) { + Task { + await unarchiver.open(at: item.url) + } + } + .background( + RoundedRectangle(cornerRadius: 8) + .fill(designSystem.color(.accent)) + ) + }.listStyle(.sidebar) + } + } + } + + // MARK: Private + + @Query(sort: \RecentArchive.openedDate, order: .reverse) private var recentArchives: [RecentArchive] + @Environment(Unarchiver.self) private var unarchiver + @Environment(DesignSystem.self) private var designSystem +} + +// MARK: - RecentArchive + ApplicationDisplayable + +extension RecentArchive: ApplicationDisplayable { } diff --git a/Sources/Archive/Views/LoadingView.swift b/Sources/Archive/Views/LoadingView.swift new file mode 100644 index 0000000..15c59d3 --- /dev/null +++ b/Sources/Archive/Views/LoadingView.swift @@ -0,0 +1,24 @@ +// +// Copyright © Marc Rollin. +// + +import SwiftUI + +struct LoadingView: View { + + var body: some View { + ProgressView() + .progressViewStyle(.circular) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button { + unarchiver.close() + } label: { + Label("Close archive", systemImage: "xmark.circle") + } + } + } + } + + @Environment(Unarchiver.self) private var unarchiver +} diff --git a/Sources/Assets.xcassets/AccentColor.colorset/Contents.json b/Sources/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index eb87897..0000000 --- a/Sources/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift deleted file mode 100644 index 55a0711..0000000 --- a/Sources/ContentView.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// Copyright © Marc Rollin. -// - -import SwiftData -import SwiftUI - -struct ContentView: View { - - // MARK: Internal - - var body: some View { - NavigationSplitView { - List { - ForEach(items) { item in - NavigationLink { - Text("Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))") - } label: { - Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard)) - } - } - .onDelete(perform: deleteItems) - } - .navigationSplitViewColumnWidth(min: 180, ideal: 200) - .toolbar { - ToolbarItem { - Button(action: addItem) { - Label("Add Item", systemImage: "plus") - } - } - } - } detail: { - Text("Select an item") - } - } - - // MARK: Private - - @Environment(\.modelContext) private var modelContext - @Query private var items: [Item] - - private func addItem() { - withAnimation { - let newItem = Item(timestamp: Date()) - modelContext.insert(newItem) - } - } - - private func deleteItems(offsets: IndexSet) { - withAnimation { - for index in offsets { - modelContext.delete(items[index]) - } - } - } -} - -#Preview { - ContentView() - .modelContainer(for: Item.self, inMemory: true) -} diff --git a/Sources/Environment/DesignSystemEnvironment.swift b/Sources/Environment/DesignSystemEnvironment.swift new file mode 100644 index 0000000..bee8d84 --- /dev/null +++ b/Sources/Environment/DesignSystemEnvironment.swift @@ -0,0 +1,33 @@ +// +// Copyright © Marc Rollin. +// + +import DesignSystem +import SwiftUI + +// MARK: - DesignSystemContainer + +private struct DesignSystemContainer: View { + let content: Content + + var body: some View { + content + .environment(designSystem) + .onAppear { + designSystem.dynamicTypeSize = dynamicTypeSize + } + .onChange(of: dynamicTypeSize) { _, newValue in + designSystem.dynamicTypeSize = newValue + } + } + + private let designSystem = DesignSystem() + @Environment(\.dynamicTypeSize) private var dynamicTypeSize +} + +extension View { + + public func withDesignSystem() -> some View { + DesignSystemContainer(content: self) + } +} diff --git a/Sources/Environment/ToasterEnvironment.swift b/Sources/Environment/ToasterEnvironment.swift new file mode 100644 index 0000000..2bbf95e --- /dev/null +++ b/Sources/Environment/ToasterEnvironment.swift @@ -0,0 +1,26 @@ +// +// Copyright © Marc Rollin. +// + +import SwiftUI + +// MARK: - ToastContainer + +private struct ToastContainer: View { + let content: Content + + var body: some View { + content + .overlay(ToastOverlay()) + .environment(toaster) + } + + private let toaster = Toaster() +} + +extension View { + + func withToaster() -> some View { + ToastContainer(content: self) + } +} diff --git a/Sources/Environment/UnarchiverEnvironment.swift b/Sources/Environment/UnarchiverEnvironment.swift new file mode 100644 index 0000000..83b93ee --- /dev/null +++ b/Sources/Environment/UnarchiverEnvironment.swift @@ -0,0 +1,55 @@ +// +// Copyright © Marc Rollin. +// + +import ApplicationArchive +import DesignComponents +import SwiftData +import SwiftUI + +// MARK: - UnarchiverContainer + +@MainActor +private struct UnarchiverContainer: View { + + // MARK: Lifecycle + + init(content: Content) { + self.content = content + } + + // MARK: Internal + + let content: Content + + var body: some View { + content + .environment(Unarchiver()) + .modelContainer(modelContainer) + .onChange(of: unarchiver.archive) { _, archive in + guard let archive, let app = archive.apps.first else { return } + modelContainer.mainContext.insert( + RecentArchive( + url: archive.url, + name: app.name, + version: app.version, + platforms: archive.apps.flatMap(\.platforms), + icon: app.icon + ) + ) + } + } + + // MARK: Private + + private let modelContainer = try! ModelContainer(for: RecentArchive.self) + private let unarchiver = Unarchiver() +} + +@MainActor +extension View { + + func withUnarchiver() -> some View { + UnarchiverContainer(content: self) + } +} diff --git a/Sources/Item.swift b/Sources/Item.swift deleted file mode 100644 index a302cdb..0000000 --- a/Sources/Item.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// Copyright © Marc Rollin. -// - -import Foundation -import SwiftData - -@Model -final class Item { - - // MARK: Lifecycle - - - init(timestamp: Date) { - self.timestamp = timestamp - } - - // MARK: Internal - - var timestamp: Date -} diff --git a/Sources/SteelyardApp.swift b/Sources/SteelyardApp.swift index 031b70b..001d280 100644 --- a/Sources/SteelyardApp.swift +++ b/Sources/SteelyardApp.swift @@ -2,28 +2,50 @@ // Copyright © Marc Rollin. // -import SwiftData +import DesignComponents import SwiftUI +// MARK: - SteelyardApp + @main struct SteelyardApp: App { - var sharedModelContainer: ModelContainer = { - let schema = Schema([ - Item.self, - ]) - let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false) - do { - return try ModelContainer(for: schema, configurations: [modelConfiguration]) - } catch { - fatalError("Could not create ModelContainer: \(error)") - } - }() + // MARK: Internal var body: some Scene { WindowGroup { - ContentView() + ArchiveView() + .withUnarchiver() + .withToaster() + .withDesignSystem() + } + .commands { + appMenu + } + } + + // MARK: Private + + private var appMenu: some Commands { + CommandGroup(replacing: .newItem) { + Button("menu.new-window") { +// openWindow(id: "main") + } + .keyboardShortcut("n", modifiers: .shift) } - .modelContainer(sharedModelContainer) +// CommandGroup(replacing: .textFormatting) { +// Menu("menu.font") { +// Button("menu.font.bigger") { +// if theme.fontSizeScale < 1.5 { +// theme.fontSizeScale += 0.1 +// } +// } +// Button("menu.font.smaller") { +// if theme.fontSizeScale > 0.5 { +// theme.fontSizeScale -= 0.1 +// } +// } +// } +// } } } diff --git a/Sources/Toast/Toast.swift b/Sources/Toast/Toast.swift new file mode 100644 index 0000000..3554a00 --- /dev/null +++ b/Sources/Toast/Toast.swift @@ -0,0 +1,13 @@ +// +// Copyright © Marc Rollin. +// + +import Foundation + +// MARK: - Toast + +struct Toast: Identifiable { + let id = UUID() + let message: String + let duration: Double +} diff --git a/Sources/Toast/ToastOverlay.swift b/Sources/Toast/ToastOverlay.swift new file mode 100644 index 0000000..cddc41a --- /dev/null +++ b/Sources/Toast/ToastOverlay.swift @@ -0,0 +1,21 @@ +// +// Copyright © Marc Rollin. +// + +import SwiftUI + +// MARK: - ToastOverlayView + +struct ToastOverlay: View { + @State private var toasts: [Toast] = [] + + var body: some View { + VStack(alignment: .center, spacing: 5) { + ForEach(toaster.toasts) { toast in + ToastView(toast: toast) + } + } + } + + @Environment(Toaster.self) private var toaster +} diff --git a/Sources/Toast/ToastView.swift b/Sources/Toast/ToastView.swift new file mode 100644 index 0000000..12e25e9 --- /dev/null +++ b/Sources/Toast/ToastView.swift @@ -0,0 +1,28 @@ +// +// Copyright © Marc Rollin. +// + +import DesignSystem +import SwiftUI + +// MARK: - ToastView + +struct ToastView: View { + + let toast: Toast + + var body: some View { + Text(toast.message) + .padding() + .background(designSystem.color(.background).opacity(designSystem.opacity(.medium))) + .foregroundColor(.white) + .cornerRadius(8) + .onTapGesture { + toaster.remove(toast: toast) + } + } + + @Environment(Toaster.self) private var toaster + @Environment(DesignSystem.self) private var designSystem +} + diff --git a/Sources/Toast/Toaster.swift b/Sources/Toast/Toaster.swift new file mode 100644 index 0000000..8ee35ff --- /dev/null +++ b/Sources/Toast/Toaster.swift @@ -0,0 +1,29 @@ +// +// Copyright © Marc Rollin. +// + +import SwiftUI + +// MARK: - Toaster + +@Observable +final class Toaster { + private(set) var toasts = [Toast]() + + func show(message: String, duration: Double) { + let toast = Toast(message: message, duration: duration) + withAnimation { + toasts.append(toast) + } + Task { + try await Task.sleep(for: .seconds(toast.duration)) + remove(toast: toast) + } + } + + func remove(toast: Toast) { + withAnimation { + toasts.removeAll { $0.id == toast.id } + } + } +} diff --git a/Sources/Tree/ApplicationArchiveNode+TreeMapDisplayable.swift b/Sources/Tree/ApplicationArchiveNode+TreeMapDisplayable.swift new file mode 100644 index 0000000..8e76b50 --- /dev/null +++ b/Sources/Tree/ApplicationArchiveNode+TreeMapDisplayable.swift @@ -0,0 +1,92 @@ +// +// Copyright © Marc Rollin. +// + +import ApplicationArchive +import DesignComponents +import Foundation +import SwiftUI + +extension ApplicationArchive.App.Platform { + + var icon: some View { + let systemName = switch self { + case .iphone: "iphone" + case .mac: "macbook" + case .ipad: "ipad" + case .tv: "appletv" + case .watch: "applewatch" + } + + return Image(systemName: systemName) + .resizable() + .aspectRatio(contentMode: .fit) + } +} + +// MARK: - ApplicationArchive.Node + TreeMapDisplayable + +extension ApplicationArchive.Node: TreeMapDisplayable { + + // MARK: Public + + public var description: String { + name + } + + public var color: Color? { + if isDuplicate { + .red + } else { + switch category { + case .app: .teal + case .appExtension: .brown + case .assetCatalog: .green + case .binary: .blue + case .bundle: .indigo + case .content: .mint + case .data: .gray + case .font: .cyan + case .framework: .brown + case .localization: .orange + case .model: .purple + case .folder: nil + } + } + } + + public var shouldShowDetails: Bool { + !childrenBySize.isEmpty + } + + public var size: CGFloat { + CGFloat(sizeInBytes) + } + + public var segments: [ApplicationArchive.Node] { + childrenBySize + } + + // MARK: Internal + + var icon: some View { + let systemName = switch category { + case .app: "folder.fill.badge.gearshape" + case .appExtension: "puzzlepiece.extension.fill" + case .assetCatalog: "photo.on.rectangle.fill" + case .binary: "apple.terminal.fill" + case .bundle: "shippingbox.fill" + case .content: "doc.text.fill" + case .data: "doc.badge.gearshape.fill" + case .font: "textformat" + case .framework: "shippingbox.fill" + case .localization: "character.bubble.fill" + case .model: "doc.badge.gearshape.fill" + case .folder: "folder.fill" + } + + return Image(systemName: systemName) + .resizable() + .aspectRatio(contentMode: .fit) + } +} diff --git a/Steelyard-Info.plist b/Steelyard-Info.plist new file mode 100644 index 0000000..5dd2de6 --- /dev/null +++ b/Steelyard-Info.plist @@ -0,0 +1,26 @@ + + + + + UTExportedTypeDeclarations + + + UTTypeConformsTo + + public.data + + UTTypeIcons + + UTTypeIdentifier + com.apple.itunes.ipa + UTTypeTagSpecification + + public.filename-extension + + ipa + + + + + + diff --git a/Steelyard.entitlements b/Steelyard.entitlements index 18aff0c..0c67376 100644 --- a/Steelyard.entitlements +++ b/Steelyard.entitlements @@ -1,10 +1,5 @@ - - com.apple.security.app-sandbox - - com.apple.security.files.user-selected.read-only - - + diff --git a/Steelyard.xcodeproj/project.pbxproj b/Steelyard.xcodeproj/project.pbxproj index eb67655..b272f3a 100644 --- a/Steelyard.xcodeproj/project.pbxproj +++ b/Steelyard.xcodeproj/project.pbxproj @@ -7,24 +7,60 @@ objects = { /* Begin PBXBuildFile section */ - 496C1A0C2AED594B00D98229 /* ApplicationArchive in Frameworks */ = {isa = PBXBuildFile; productRef = 496C1A0B2AED594B00D98229 /* ApplicationArchive */; }; - 496C1A0E2AED594B00D98229 /* Platform in Frameworks */ = {isa = PBXBuildFile; productRef = 496C1A0D2AED594B00D98229 /* Platform */; }; - 496C1A102AED594B00D98229 /* TreeMap in Frameworks */ = {isa = PBXBuildFile; productRef = 496C1A0F2AED594B00D98229 /* TreeMap */; }; + 4916AD642AEEBF8700E90247 /* RecentArchive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4916AD632AEEBF8700E90247 /* RecentArchive.swift */; }; + 4916AD662AEEEFF800E90247 /* Unarchiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4916AD652AEEEFF800E90247 /* Unarchiver.swift */; }; + 4916AD682AEEFC6500E90247 /* RecentlyOpenedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4916AD672AEEFC6500E90247 /* RecentlyOpenedView.swift */; }; + 4917FFA72AFF6B8800ED45F6 /* DesignComponents in Frameworks */ = {isa = PBXBuildFile; productRef = 4917FFA62AFF6B8800ED45F6 /* DesignComponents */; }; + 491CC66E2AED78A500F8F422 /* Explorer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491CC66C2AED78A500F8F422 /* Explorer.swift */; }; + 492C6D7C2AF9925500B9BB82 /* ExplorerSidebarTreeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492C6D7B2AF9925500B9BB82 /* ExplorerSidebarTreeView.swift */; }; + 494188C52AFA401F00FF8D6D /* ExplorerSidebarTreeBranchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 494188C42AFA401F00FF8D6D /* ExplorerSidebarTreeBranchView.swift */; }; + 494188C72AFA40B700FF8D6D /* ExplorerSidebarApplicationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 494188C62AFA40B700FF8D6D /* ExplorerSidebarApplicationsView.swift */; }; + 498749F22AF0F956000705DA /* ExplorerContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 498749F12AF0F956000705DA /* ExplorerContent.swift */; }; + 498749F42AF0F98D000705DA /* ExplorerSidebar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 498749F32AF0F98D000705DA /* ExplorerSidebar.swift */; }; 49B582392AED57190082B4C2 /* SteelyardApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49B582382AED57190082B4C2 /* SteelyardApp.swift */; }; - 49B5823B2AED57190082B4C2 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49B5823A2AED57190082B4C2 /* ContentView.swift */; }; - 49B5823D2AED57190082B4C2 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49B5823C2AED57190082B4C2 /* Item.swift */; }; 49B5823F2AED571A0082B4C2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 49B5823E2AED571A0082B4C2 /* Assets.xcassets */; }; 49B582422AED571A0082B4C2 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 49B582412AED571A0082B4C2 /* Preview Assets.xcassets */; }; + 49B7BD972AED97320069DBCD /* LauncherView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49B7BD962AED97320069DBCD /* LauncherView.swift */; }; + 49B834672AF3820900B31D61 /* ApplicationArchive in Frameworks */ = {isa = PBXBuildFile; productRef = 49B834662AF3820900B31D61 /* ApplicationArchive */; }; + 49B834692AF3820900B31D61 /* DesignSystem in Frameworks */ = {isa = PBXBuildFile; productRef = 49B834682AF3820900B31D61 /* DesignSystem */; }; + 49B8346B2AF3820900B31D61 /* Platform in Frameworks */ = {isa = PBXBuildFile; productRef = 49B8346A2AF3820900B31D61 /* Platform */; }; + 49B834742AF388D900B31D61 /* UnarchiverEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49B834732AF388D900B31D61 /* UnarchiverEnvironment.swift */; }; + 49B8349C2AF51E3500B31D61 /* Finder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49B8349B2AF51E3500B31D61 /* Finder.swift */; }; + 49B835012AF7EF4700B31D61 /* ExplorerSidebarTreeLeafView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49B835002AF7EF4700B31D61 /* ExplorerSidebarTreeLeafView.swift */; }; + 49B8350B2AF8307100B31D61 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49B8350A2AF8307100B31D61 /* LoadingView.swift */; }; + 49F71BA22AEE3CBD0080157F /* ArchiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49F71BA12AEE3CBD0080157F /* ArchiveView.swift */; }; + 49F71BA42AEE3D1E0080157F /* ExplorerNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49F71BA32AEE3D1E0080157F /* ExplorerNavigation.swift */; }; + 49F71BA62AEE3D4A0080157F /* PathView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49F71BA52AEE3D4A0080157F /* PathView.swift */; }; + 49F71BA82AEE3DAE0080157F /* PathItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49F71BA72AEE3DAE0080157F /* PathItemView.swift */; }; + 49F71BAA2AEE3DE00080157F /* ExplorerInspector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49F71BA92AEE3DE00080157F /* ExplorerInspector.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 4916AD632AEEBF8700E90247 /* RecentArchive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentArchive.swift; sourceTree = ""; }; + 4916AD652AEEEFF800E90247 /* Unarchiver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Unarchiver.swift; sourceTree = ""; }; + 4916AD672AEEFC6500E90247 /* RecentlyOpenedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentlyOpenedView.swift; sourceTree = ""; }; + 491CC66C2AED78A500F8F422 /* Explorer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Explorer.swift; sourceTree = ""; }; + 492C6D7B2AF9925500B9BB82 /* ExplorerSidebarTreeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExplorerSidebarTreeView.swift; sourceTree = ""; }; + 494188C42AFA401F00FF8D6D /* ExplorerSidebarTreeBranchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExplorerSidebarTreeBranchView.swift; sourceTree = ""; }; + 494188C62AFA40B700FF8D6D /* ExplorerSidebarApplicationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExplorerSidebarApplicationsView.swift; sourceTree = ""; }; + 498749F12AF0F956000705DA /* ExplorerContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExplorerContent.swift; sourceTree = ""; }; + 498749F32AF0F98D000705DA /* ExplorerSidebar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExplorerSidebar.swift; sourceTree = ""; }; 49B582352AED57190082B4C2 /* Steelyard.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Steelyard.app; sourceTree = BUILT_PRODUCTS_DIR; }; 49B582382AED57190082B4C2 /* SteelyardApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SteelyardApp.swift; sourceTree = ""; }; - 49B5823A2AED57190082B4C2 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; - 49B5823C2AED57190082B4C2 /* Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = ""; }; 49B5823E2AED571A0082B4C2 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 49B582412AED571A0082B4C2 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 49B582432AED571A0082B4C2 /* Steelyard.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Steelyard.entitlements; sourceTree = ""; }; + 49B7BD962AED97320069DBCD /* LauncherView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LauncherView.swift; sourceTree = ""; }; + 49B834732AF388D900B31D61 /* UnarchiverEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnarchiverEnvironment.swift; sourceTree = ""; }; + 49B8349B2AF51E3500B31D61 /* Finder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Finder.swift; sourceTree = ""; }; + 49B835002AF7EF4700B31D61 /* ExplorerSidebarTreeLeafView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExplorerSidebarTreeLeafView.swift; sourceTree = ""; }; + 49B8350A2AF8307100B31D61 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = ""; }; + 49F71B7C2AEDA6740080157F /* Steelyard-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "Steelyard-Info.plist"; sourceTree = ""; }; + 49F71BA12AEE3CBD0080157F /* ArchiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArchiveView.swift; sourceTree = ""; }; + 49F71BA32AEE3D1E0080157F /* ExplorerNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExplorerNavigation.swift; sourceTree = ""; }; + 49F71BA52AEE3D4A0080157F /* PathView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathView.swift; sourceTree = ""; }; + 49F71BA72AEE3DAE0080157F /* PathItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathItemView.swift; sourceTree = ""; }; + 49F71BA92AEE3DE00080157F /* ExplorerInspector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExplorerInspector.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -32,21 +68,57 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 496C1A102AED594B00D98229 /* TreeMap in Frameworks */, - 496C1A0C2AED594B00D98229 /* ApplicationArchive in Frameworks */, - 496C1A0E2AED594B00D98229 /* Platform in Frameworks */, + 4917FFA72AFF6B8800ED45F6 /* DesignComponents in Frameworks */, + 49B834672AF3820900B31D61 /* ApplicationArchive in Frameworks */, + 49B8346B2AF3820900B31D61 /* Platform in Frameworks */, + 49B834692AF3820900B31D61 /* DesignSystem in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 4916AD692AEEFCD800E90247 /* State */ = { + isa = PBXGroup; + children = ( + 491CC66C2AED78A500F8F422 /* Explorer.swift */, + 4916AD652AEEEFF800E90247 /* Unarchiver.swift */, + 49B8349B2AF51E3500B31D61 /* Finder.swift */, + ); + path = State; + sourceTree = ""; + }; + 4916AD6A2AEEFCE000E90247 /* Views */ = { + isa = PBXGroup; + children = ( + 49F71BA12AEE3CBD0080157F /* ArchiveView.swift */, + 49B8350A2AF8307100B31D61 /* LoadingView.swift */, + 49B835082AF8302400B31D61 /* Explorer */, + 49B835092AF8305700B31D61 /* Launcher */, + ); + path = Views; + sourceTree = ""; + }; + 494188C32AFA3F7400FF8D6D /* Tree */ = { + isa = PBXGroup; + children = ( + 494188C42AFA401F00FF8D6D /* ExplorerSidebarTreeBranchView.swift */, + 49B835002AF7EF4700B31D61 /* ExplorerSidebarTreeLeafView.swift */, + 492C6D7B2AF9925500B9BB82 /* ExplorerSidebarTreeView.swift */, + ); + path = Tree; + sourceTree = ""; + }; 49B5822C2AED57190082B4C2 = { isa = PBXGroup; children = ( + 49F71B7C2AEDA6740080157F /* Steelyard-Info.plist */, 49B582432AED571A0082B4C2 /* Steelyard.entitlements */, + 49B5823E2AED571A0082B4C2 /* Assets.xcassets */, + 49B582402AED571A0082B4C2 /* Preview Content */, 49B582372AED57190082B4C2 /* Sources */, 49B582362AED57190082B4C2 /* Products */, + 49B8350C2AF8F60600B31D61 /* Frameworks */, ); sourceTree = ""; }; @@ -62,10 +134,8 @@ isa = PBXGroup; children = ( 49B582382AED57190082B4C2 /* SteelyardApp.swift */, - 49B5823A2AED57190082B4C2 /* ContentView.swift */, - 49B5823C2AED57190082B4C2 /* Item.swift */, - 49B5823E2AED571A0082B4C2 /* Assets.xcassets */, - 49B582402AED571A0082B4C2 /* Preview Content */, + 49F71B9F2AEE3C420080157F /* Archive */, + 49B8346E2AF3884600B31D61 /* Environment */, ); path = Sources; sourceTree = ""; @@ -78,6 +148,71 @@ path = "Preview Content"; sourceTree = ""; }; + 49B8346E2AF3884600B31D61 /* Environment */ = { + isa = PBXGroup; + children = ( + 49B834732AF388D900B31D61 /* UnarchiverEnvironment.swift */, + ); + path = Environment; + sourceTree = ""; + }; + 49B835062AF82FDF00B31D61 /* Sidebar */ = { + isa = PBXGroup; + children = ( + 494188C62AFA40B700FF8D6D /* ExplorerSidebarApplicationsView.swift */, + 498749F32AF0F98D000705DA /* ExplorerSidebar.swift */, + 494188C32AFA3F7400FF8D6D /* Tree */, + ); + path = Sidebar; + sourceTree = ""; + }; + 49B835072AF8300700B31D61 /* Content */ = { + isa = PBXGroup; + children = ( + 498749F12AF0F956000705DA /* ExplorerContent.swift */, + 49F71BA72AEE3DAE0080157F /* PathItemView.swift */, + 49F71BA52AEE3D4A0080157F /* PathView.swift */, + ); + path = Content; + sourceTree = ""; + }; + 49B835082AF8302400B31D61 /* Explorer */ = { + isa = PBXGroup; + children = ( + 49F71BA92AEE3DE00080157F /* ExplorerInspector.swift */, + 49F71BA32AEE3D1E0080157F /* ExplorerNavigation.swift */, + 49B835072AF8300700B31D61 /* Content */, + 49B835062AF82FDF00B31D61 /* Sidebar */, + ); + path = Explorer; + sourceTree = ""; + }; + 49B835092AF8305700B31D61 /* Launcher */ = { + isa = PBXGroup; + children = ( + 4916AD632AEEBF8700E90247 /* RecentArchive.swift */, + 4916AD672AEEFC6500E90247 /* RecentlyOpenedView.swift */, + 49B7BD962AED97320069DBCD /* LauncherView.swift */, + ); + path = Launcher; + sourceTree = ""; + }; + 49B8350C2AF8F60600B31D61 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; + 49F71B9F2AEE3C420080157F /* Archive */ = { + isa = PBXGroup; + children = ( + 4916AD6A2AEEFCE000E90247 /* Views */, + 4916AD692AEEFCD800E90247 /* State */, + ); + path = Archive; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -95,9 +230,10 @@ ); name = Steelyard; packageProductDependencies = ( - 496C1A0B2AED594B00D98229 /* ApplicationArchive */, - 496C1A0D2AED594B00D98229 /* Platform */, - 496C1A0F2AED594B00D98229 /* TreeMap */, + 49B834662AF3820900B31D61 /* ApplicationArchive */, + 49B834682AF3820900B31D61 /* DesignSystem */, + 49B8346A2AF3820900B31D61 /* Platform */, + 4917FFA62AFF6B8800ED45F6 /* DesignComponents */, ); productName = SteelyardApp; productReference = 49B582352AED57190082B4C2 /* Steelyard.app */; @@ -128,7 +264,7 @@ ); mainGroup = 49B5822C2AED57190082B4C2; packageReferences = ( - 496C1A0A2AED594B00D98229 /* XCRemoteSwiftPackageReference "SteelyardCore" */, + 49B834652AF3820900B31D61 /* XCRemoteSwiftPackageReference "SteelyardCore" */, ); productRefGroup = 49B582362AED57190082B4C2 /* Products */; projectDirPath = ""; @@ -156,8 +292,25 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 49B5823B2AED57190082B4C2 /* ContentView.swift in Sources */, - 49B5823D2AED57190082B4C2 /* Item.swift in Sources */, + 49B835012AF7EF4700B31D61 /* ExplorerSidebarTreeLeafView.swift in Sources */, + 4916AD682AEEFC6500E90247 /* RecentlyOpenedView.swift in Sources */, + 492C6D7C2AF9925500B9BB82 /* ExplorerSidebarTreeView.swift in Sources */, + 49F71BA62AEE3D4A0080157F /* PathView.swift in Sources */, + 49B8349C2AF51E3500B31D61 /* Finder.swift in Sources */, + 494188C52AFA401F00FF8D6D /* ExplorerSidebarTreeBranchView.swift in Sources */, + 49B834742AF388D900B31D61 /* UnarchiverEnvironment.swift in Sources */, + 498749F42AF0F98D000705DA /* ExplorerSidebar.swift in Sources */, + 49F71BA22AEE3CBD0080157F /* ArchiveView.swift in Sources */, + 491CC66E2AED78A500F8F422 /* Explorer.swift in Sources */, + 49B8350B2AF8307100B31D61 /* LoadingView.swift in Sources */, + 49B7BD972AED97320069DBCD /* LauncherView.swift in Sources */, + 494188C72AFA40B700FF8D6D /* ExplorerSidebarApplicationsView.swift in Sources */, + 4916AD642AEEBF8700E90247 /* RecentArchive.swift in Sources */, + 49F71BAA2AEE3DE00080157F /* ExplorerInspector.swift in Sources */, + 498749F22AF0F956000705DA /* ExplorerContent.swift in Sources */, + 4916AD662AEEEFF800E90247 /* Unarchiver.swift in Sources */, + 49F71BA82AEE3DAE0080157F /* PathItemView.swift in Sources */, + 49F71BA42AEE3D1E0080157F /* ExplorerNavigation.swift in Sources */, 49B582392AED57190082B4C2 /* SteelyardApp.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -292,11 +445,12 @@ CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"Sources/Preview Content\""; - DEVELOPMENT_TEAM = 5ZANXJRW9Q; + DEVELOPMENT_ASSET_PATHS = "\"Preview Content\""; + DEVELOPMENT_TEAM = FKKWUHNWNH; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "Steelyard-Info.plist"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -307,6 +461,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.mcrollin.SteelyardApp; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; }; name = Debug; @@ -320,11 +475,12 @@ CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"Sources/Preview Content\""; - DEVELOPMENT_TEAM = 5ZANXJRW9Q; + DEVELOPMENT_ASSET_PATHS = "\"Preview Content\""; + DEVELOPMENT_TEAM = FKKWUHNWNH; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "Steelyard-Info.plist"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -335,6 +491,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.mcrollin.SteelyardApp; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; }; name = Release; @@ -363,31 +520,36 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 496C1A0A2AED594B00D98229 /* XCRemoteSwiftPackageReference "SteelyardCore" */ = { + 49B834652AF3820900B31D61 /* XCRemoteSwiftPackageReference "SteelyardCore" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "git@github.com:mcrollin/SteelyardCore.git"; requirement = { - branch = main; - kind = branch; + kind = revision; + revision = c9c53b47d776479e04680912f06822d018cd201c; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 496C1A0B2AED594B00D98229 /* ApplicationArchive */ = { + 4917FFA62AFF6B8800ED45F6 /* DesignComponents */ = { isa = XCSwiftPackageProductDependency; - package = 496C1A0A2AED594B00D98229 /* XCRemoteSwiftPackageReference "SteelyardCore" */; + package = 49B834652AF3820900B31D61 /* XCRemoteSwiftPackageReference "SteelyardCore" */; + productName = DesignComponents; + }; + 49B834662AF3820900B31D61 /* ApplicationArchive */ = { + isa = XCSwiftPackageProductDependency; + package = 49B834652AF3820900B31D61 /* XCRemoteSwiftPackageReference "SteelyardCore" */; productName = ApplicationArchive; }; - 496C1A0D2AED594B00D98229 /* Platform */ = { + 49B834682AF3820900B31D61 /* DesignSystem */ = { isa = XCSwiftPackageProductDependency; - package = 496C1A0A2AED594B00D98229 /* XCRemoteSwiftPackageReference "SteelyardCore" */; - productName = Platform; + package = 49B834652AF3820900B31D61 /* XCRemoteSwiftPackageReference "SteelyardCore" */; + productName = DesignSystem; }; - 496C1A0F2AED594B00D98229 /* TreeMap */ = { + 49B8346A2AF3820900B31D61 /* Platform */ = { isa = XCSwiftPackageProductDependency; - package = 496C1A0A2AED594B00D98229 /* XCRemoteSwiftPackageReference "SteelyardCore" */; - productName = TreeMap; + package = 49B834652AF3820900B31D61 /* XCRemoteSwiftPackageReference "SteelyardCore" */; + productName = Platform; }; /* End XCSwiftPackageProductDependency section */ }; diff --git a/Steelyard.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Steelyard.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 1d1783d..919434a 100644 --- a/Steelyard.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/Steelyard.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> diff --git a/Steelyard.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Steelyard.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 36a4f97..cc94824 100644 --- a/Steelyard.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Steelyard.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -27,6 +27,15 @@ "version" : "1.0.201" } }, + { + "identity" : "combine-schedulers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/combine-schedulers", + "state" : { + "revision" : "9dc9cbe4bc45c65164fa653a563d8d8db61b09bb", + "version" : "1.0.0" + } + }, { "identity" : "kituracontracts", "kind" : "remoteSourceControl", @@ -50,8 +59,34 @@ "kind" : "remoteSourceControl", "location" : "git@github.com:mcrollin/SteelyardCore.git", "state" : { - "branch" : "main", - "revision" : "80a3f72169dd2a963813c4b2a4e02547afbf9bce" + "revision" : "c9c53b47d776479e04680912f06822d018cd201c" + } + }, + { + "identity" : "swift-clocks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-clocks", + "state" : { + "revision" : "d1fd837326aa719bee979bdde1f53cd5797443eb", + "version" : "1.0.0" + } + }, + { + "identity" : "swift-concurrency-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-concurrency-extras", + "state" : { + "revision" : "bb5059bde9022d69ac516803f4f227d8ac967f71", + "version" : "1.1.0" + } + }, + { + "identity" : "swift-dependencies", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-dependencies.git", + "state" : { + "revision" : "4e1eb6e28afe723286d8cc60611237ffbddba7c5", + "version" : "1.0.0" } }, { @@ -81,6 +116,15 @@ "version" : "1.5.3" } }, + { + "identity" : "xctest-dynamic-overlay", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state" : { + "revision" : "23cbf2294e350076ea4dbd7d5d047c1e76b03631", + "version" : "1.0.2" + } + }, { "identity" : "zip", "kind" : "remoteSourceControl", diff --git a/Steelyard.xcodeproj/xcshareddata/xcschemes/Steelyard.xcscheme b/Steelyard.xcodeproj/xcshareddata/xcschemes/Steelyard.xcscheme new file mode 100644 index 0000000..9f94939 --- /dev/null +++ b/Steelyard.xcodeproj/xcshareddata/xcschemes/Steelyard.xcscheme @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +