2026-04-12 00:46:30 -07:00
|
|
|
import UIKit
|
|
|
|
|
import SwiftUI
|
|
|
|
|
import Foundation
|
|
|
|
|
|
|
|
|
|
enum WorkingCopyGuard {
|
|
|
|
|
enum GuardError: LocalizedError {
|
|
|
|
|
case notInstalled
|
|
|
|
|
case apiKeyMissing
|
|
|
|
|
case repoNameMissing(String)
|
|
|
|
|
|
|
|
|
|
var errorDescription: String? {
|
|
|
|
|
switch self {
|
|
|
|
|
case .notInstalled: return "Working Copy is not installed."
|
|
|
|
|
case .apiKeyMissing: return "Working Copy API key not set. Go to Settings → Working Copy."
|
|
|
|
|
case .repoNameMissing(let p): return "No Working Copy repo name set for '\(p)'."
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
var actionLabel: String {
|
|
|
|
|
switch self {
|
|
|
|
|
case .notInstalled: return "App Store"
|
|
|
|
|
default: return "Open Settings"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static func validate(projectName: String, requiresApiKey: Bool = true) throws {
|
|
|
|
|
guard UIApplication.shared.canOpenURL(URL(string: "working-copy://")!) else {
|
|
|
|
|
throw GuardError.notInstalled
|
|
|
|
|
}
|
|
|
|
|
if requiresApiKey {
|
|
|
|
|
let key = UserDefaults.standard.string(forKey: "workingCopyAPIKey") ?? ""
|
|
|
|
|
guard !key.trimmingCharacters(in: .whitespaces).isEmpty else {
|
|
|
|
|
throw GuardError.apiKeyMissing
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let repoName = UserDefaults.standard.string(forKey: "wc_repo_for_\(projectName)") ?? ""
|
|
|
|
|
guard !repoName.trimmingCharacters(in: .whitespaces).isEmpty else {
|
|
|
|
|
throw GuardError.repoNameMissing(projectName)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct WorkingCopyErrorBanner: ViewModifier {
|
|
|
|
|
@Binding var error: WorkingCopyGuard.GuardError?
|
|
|
|
|
func body(content: Content) -> some View {
|
|
|
|
|
content.overlay(alignment: .top) {
|
|
|
|
|
if let err = error {
|
|
|
|
|
HStack(spacing: 10) {
|
|
|
|
|
Image(systemName: "exclamationmark.triangle.fill").foregroundStyle(.orange)
|
2026-04-12 22:07:35 -07:00
|
|
|
Text(err.localizedDescription).font(.subheadline).lineLimit(2)
|
2026-04-12 00:46:30 -07:00
|
|
|
Spacer()
|
|
|
|
|
Button(err.actionLabel) { handleAction(for: err) }.buttonStyle(.bordered).controlSize(.small)
|
|
|
|
|
Button { withAnimation { self.error = nil } }
|
|
|
|
|
label: { Image(systemName: "xmark").font(.caption.bold()) }
|
|
|
|
|
.buttonStyle(.plain).foregroundStyle(.secondary)
|
|
|
|
|
}
|
|
|
|
|
.padding(.horizontal, 14).padding(.vertical, 10)
|
|
|
|
|
.background(.regularMaterial)
|
|
|
|
|
.transition(.move(edge: .top).combined(with: .opacity))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.animation(.spring(response: 0.3), value: error != nil)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func handleAction(for err: WorkingCopyGuard.GuardError) {
|
|
|
|
|
switch err {
|
|
|
|
|
case .notInstalled:
|
|
|
|
|
UIApplication.shared.open(URL(string: "https://apps.apple.com/app/working-copy-git-client/id896694807")!)
|
|
|
|
|
default:
|
|
|
|
|
NotificationCenter.default.post(name: .openSettings, object: nil)
|
|
|
|
|
}
|
|
|
|
|
self.error = nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
extension View {
|
|
|
|
|
func workingCopyErrorBanner(_ error: Binding<WorkingCopyGuard.GuardError?>) -> some View {
|
|
|
|
|
modifier(WorkingCopyErrorBanner(error: error))
|
|
|
|
|
}
|
|
|
|
|
}
|