diff --git a/Package.swift b/Package.swift
index c1e1dda..9ba543a 100644
--- a/Package.swift
+++ b/Package.swift
@@ -14,15 +14,11 @@ let package = Package(
targets: ["ClickIt"]
)
],
- dependencies: [
- .package(url: "https://github.com/sparkle-project/Sparkle", from: "2.5.2")
- ],
+ dependencies: [],
targets: [
.executableTarget(
name: "ClickIt",
- dependencies: [
- .product(name: "Sparkle", package: "Sparkle")
- ],
+ dependencies: [],
resources: [.process("Resources")]
),
.testTarget(
diff --git a/Sources/ClickIt/Core/Update/AppcastGenerator.swift b/Sources/ClickIt/Core/Update/AppcastGenerator.swift
deleted file mode 100644
index 36f67b1..0000000
--- a/Sources/ClickIt/Core/Update/AppcastGenerator.swift
+++ /dev/null
@@ -1,302 +0,0 @@
-import Foundation
-
-/// Generates Sparkle-compatible appcast XML from GitHub Releases API data
-struct AppcastGenerator {
-
- // MARK: - Data Models
-
- struct GitHubRelease: Codable {
- let id: Int
- let tagName: String
- let name: String?
- let body: String?
- let prerelease: Bool
- let draft: Bool
- let publishedAt: String?
- let assets: [GitHubAsset]
- let htmlUrl: String
-
- private enum CodingKeys: String, CodingKey {
- case id, name, body, prerelease, draft, assets
- case tagName = "tag_name"
- case publishedAt = "published_at"
- case htmlUrl = "html_url"
- }
- }
-
- struct GitHubAsset: Codable {
- let id: Int
- let name: String
- let size: Int
- let downloadCount: Int
- let browserDownloadUrl: String
-
- private enum CodingKeys: String, CodingKey {
- case id, name, size
- case downloadCount = "download_count"
- case browserDownloadUrl = "browser_download_url"
- }
- }
-
- // MARK: - Configuration
-
- struct AppcastConfig {
- let appName: String
- let bundleId: String
- let repository: String
- let minimumSystemVersion: String
- let includeBetaReleases: Bool
-
- static let `default` = AppcastConfig(
- appName: "ClickIt",
- bundleId: AppConstants.appcastURL.contains("clickit") ? "com.jsonify.clickit" : "com.example.clickit",
- repository: AppConstants.githubRepository,
- minimumSystemVersion: AppConstants.minimumOSVersion,
- includeBetaReleases: false
- )
- }
-
- // MARK: - Public Methods
-
- /// Fetches GitHub releases and generates appcast XML
- static func generateAppcast(config: AppcastConfig = .default) async throws -> String {
- let releases = try await fetchGitHubReleases(repository: config.repository)
- let filteredReleases = filterReleases(releases, includeBeta: config.includeBetaReleases)
- return generateAppcastXML(releases: filteredReleases, config: config)
- }
-
- /// Generates appcast XML from provided releases data
- static func generateAppcastXML(releases: [GitHubRelease], config: AppcastConfig) -> String {
- let items = releases.compactMap { release -> String? in
- generateAppcastItem(release: release, config: config)
- }
-
- return generateFullAppcast(items: items, config: config)
- }
-
- // MARK: - Private Methods
-
- /// Fetches releases from GitHub API
- private static func fetchGitHubReleases(repository: String) async throws -> [GitHubRelease] {
- guard let url = URL(string: "https://api.github.com/repos/\(repository)/releases") else {
- throw AppcastError.invalidURL
- }
-
- let (data, response) = try await URLSession.shared.data(from: url)
-
- guard let httpResponse = response as? HTTPURLResponse,
- 200...299 ~= httpResponse.statusCode else {
- throw AppcastError.networkError
- }
-
- do {
- return try JSONDecoder().decode([GitHubRelease].self, from: data)
- } catch {
- throw AppcastError.decodingError(error)
- }
- }
-
- /// Filters releases based on configuration
- private static func filterReleases(_ releases: [GitHubRelease], includeBeta: Bool) -> [GitHubRelease] {
- return releases.filter { release in
- // Skip drafts
- guard !release.draft else { return false }
-
- // Include/exclude prerelease based on configuration
- if release.prerelease && !includeBeta {
- return false
- }
-
- // Must have at least one asset
- return !release.assets.isEmpty
- }
- .sorted { lhs, rhs in
- // Sort by published date, newest first
- guard let lhsDate = parseDate(lhs.publishedAt),
- let rhsDate = parseDate(rhs.publishedAt) else {
- return false
- }
- return lhsDate > rhsDate
- }
- }
-
- /// Generates individual appcast item XML
- private static func generateAppcastItem(release: GitHubRelease, config: AppcastConfig) -> String? {
- // Find the main app asset (typically .zip or .dmg)
- guard let mainAsset = findMainAsset(in: release.assets) else {
- return nil
- }
-
- let version = extractVersion(from: release.tagName)
- let title = release.name ?? "\(config.appName) \(version)"
- let description = formatReleaseNotes(release.body)
- let pubDate = formatPubDate(release.publishedAt)
-
- return """
- -
-
-
- \(release.htmlUrl)
- \(version)
- \(version)
- \(config.minimumSystemVersion)
- \(pubDate)
-
-
- """
- }
-
- /// Finds the main downloadable asset (ZIP or DMG)
- private static func findMainAsset(in assets: [GitHubAsset]) -> GitHubAsset? {
- // Prefer ZIP files for auto-updates, then DMG
- return assets.first { asset in
- asset.name.lowercased().hasSuffix(".zip")
- } ?? assets.first { asset in
- asset.name.lowercased().hasSuffix(".dmg")
- }
- }
-
- /// Extracts version number from Git tag
- private static func extractVersion(from tagName: String) -> String {
- // Remove common prefixes like "v", "beta-v", etc.
- let cleanTag = tagName.replacingOccurrences(of: "^(beta-)?v?", with: "", options: .regularExpression)
- return cleanTag.isEmpty ? tagName : cleanTag
- }
-
- /// Formats release notes for XML
- private static func formatReleaseNotes(_ body: String?) -> String {
- guard let body = body, !body.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
- return "No release notes available."
- }
-
- // Basic HTML conversion for better display
- let formatted = body
- .replacingOccurrences(of: "### ", with: "
")
- .replacingOccurrences(of: "## ", with: "")
- .replacingOccurrences(of: "# ", with: "")
- .replacingOccurrences(of: "\n", with: "
")
-
- return formatted
- }
-
- /// Formats publication date for RSS
- private static func formatPubDate(_ publishedAt: String?) -> String {
- guard let publishedAt = publishedAt,
- let date = parseDate(publishedAt) else {
- return RFC822DateFormatter.string(from: Date())
- }
-
- return RFC822DateFormatter.string(from: date)
- }
-
- /// Parses ISO 8601 date string
- private static func parseDate(_ dateString: String?) -> Date? {
- guard let dateString = dateString else { return nil }
-
- let formatter = ISO8601DateFormatter()
- formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
-
- return formatter.date(from: dateString) ?? ISO8601DateFormatter().date(from: dateString)
- }
-
- /// Generates complete appcast XML structure
- private static func generateFullAppcast(items: [String], config: AppcastConfig) -> String {
- let itemsXML = items.joined(separator: "\n ")
- let lastBuildDate = RFC822DateFormatter.string(from: Date())
-
- return """
-
-
-
- \(config.appName) Updates
- https://github.com/\(config.repository)
- Software updates for \(config.appName)
- en
- \(lastBuildDate)
-
- \(itemsXML)
-
-
-
- """
- }
-
- // MARK: - Date Formatter
-
- private static let RFC822DateFormatter: DateFormatter = {
- let formatter = DateFormatter()
- formatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss Z"
- formatter.locale = Locale(identifier: "en_US_POSIX")
- formatter.timeZone = TimeZone(abbreviation: "GMT")
- return formatter
- }()
-}
-
-// MARK: - Error Types
-
-enum AppcastError: Error, LocalizedError {
- case invalidURL
- case networkError
- case decodingError(Error)
- case noValidReleases
-
- var errorDescription: String? {
- switch self {
- case .invalidURL:
- return "Invalid GitHub API URL"
- case .networkError:
- return "Network request failed"
- case .decodingError(let error):
- return "Failed to decode GitHub API response: \(error.localizedDescription)"
- case .noValidReleases:
- return "No valid releases found"
- }
- }
-}
-
-// MARK: - Convenience Extensions
-
-extension AppcastGenerator {
-
- /// Generates appcast for beta releases
- static func generateBetaAppcast() async throws -> String {
- var config = AppcastConfig.default
- config = AppcastConfig(
- appName: config.appName,
- bundleId: config.bundleId,
- repository: config.repository,
- minimumSystemVersion: config.minimumSystemVersion,
- includeBetaReleases: true
- )
- return try await generateAppcast(config: config)
- }
-
- /// Validates that the generated XML is well-formed
- static func validateAppcastXML(_ xml: String) -> Bool {
- guard let data = xml.data(using: .utf8) else { return false }
-
- let parser = XMLParser(data: data)
- let delegate = XMLValidationDelegate()
- parser.delegate = delegate
-
- return parser.parse() && !delegate.hasError
- }
-}
-
-// MARK: - XML Validation
-
-private class XMLValidationDelegate: NSObject, XMLParserDelegate {
- var hasError = false
-
- func parser(_ parser: XMLParser, parseErrorOccurred parseError: Error) {
- hasError = true
- }
-
- func parser(_ parser: XMLParser, validationErrorOccurred validationError: Error) {
- hasError = true
- }
-}
\ No newline at end of file
diff --git a/Sources/ClickIt/Core/Update/UpdaterManager.swift b/Sources/ClickIt/Core/Update/UpdaterManager.swift
deleted file mode 100644
index f04a5de..0000000
--- a/Sources/ClickIt/Core/Update/UpdaterManager.swift
+++ /dev/null
@@ -1,284 +0,0 @@
-import Foundation
-import Sparkle
-
-/// Central manager for handling app updates using Sparkle framework
-@MainActor
-class UpdaterManager: NSObject, ObservableObject {
-
- // MARK: - Published Properties
- @Published var isUpdateAvailable: Bool = false
- @Published var updateVersion: String?
- @Published var updateBuildNumber: String?
- @Published var updateReleaseNotes: String?
- @Published var isCheckingForUpdates: Bool = false
- @Published var lastUpdateCheck: Date?
- @Published var updateError: String?
- @Published var lastCheckResult: String?
-
- // Note: Direct SUAppcastItem storage omitted due to sendability constraints
-
- // MARK: - Private Properties
- private var updaterController: SPUStandardUpdaterController
- private let userDefaults = UserDefaults.standard
-
- // MARK: - Settings
- var autoUpdateEnabled: Bool {
- get { userDefaults.bool(forKey: AppConstants.autoUpdateEnabledKey) }
- set {
- userDefaults.set(newValue, forKey: AppConstants.autoUpdateEnabledKey)
- configureAutomaticChecks()
- }
- }
-
- var checkForBetaUpdates: Bool {
- get { userDefaults.bool(forKey: AppConstants.checkForBetaUpdatesKey) }
- set {
- userDefaults.set(newValue, forKey: AppConstants.checkForBetaUpdatesKey)
- // Note: Appcast URL will be configured via Info.plist or separately
- }
- }
-
- // MARK: - Initialization
- override init() {
- // Initialize without starting
- self.updaterController = SPUStandardUpdaterController(
- startingUpdater: false,
- updaterDelegate: nil,
- userDriverDelegate: nil
- )
-
- super.init()
-
- // Only start updater if not in manual-only mode
- let shouldStartUpdater: Bool
- #if DEBUG
- shouldStartUpdater = !AppConstants.DeveloperUpdateConfig.manualCheckOnly
- #else
- shouldStartUpdater = true
- #endif
-
- // Recreate with self as delegate after super.init()
- self.updaterController = SPUStandardUpdaterController(
- startingUpdater: shouldStartUpdater,
- updaterDelegate: self,
- userDriverDelegate: nil
- )
-
- setupUpdater()
- configureAutomaticChecks()
- }
-
- // MARK: - Public Methods
-
- /// Manually check for updates
- func checkForUpdates() {
- guard !isCheckingForUpdates else { return }
-
- print("🔄 Starting manual update check...")
- isCheckingForUpdates = true
- updateError = nil
- lastCheckResult = nil
-
- updaterController.updater.checkForUpdates()
- userDefaults.set(Date(), forKey: AppConstants.lastUpdateCheckKey)
- lastUpdateCheck = Date()
-
- // Reset checking state after a timeout to handle cases where delegate isn't called
- // This handles scenarios like empty appcast feeds
- DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
- if self.isCheckingForUpdates {
- self.isCheckingForUpdates = false
- self.lastCheckResult = "Current version \(self.currentVersionDetailed) is up to date"
- print("⏰ Update check timed out - assuming up to date")
- }
- }
- }
-
- /// Trigger the update installation process
- func installUpdate() {
- updaterController.updater.checkForUpdates()
- }
-
- /// Check if an update should be skipped
- func shouldSkipVersion(_ version: String) -> Bool {
- let skippedVersion = userDefaults.string(forKey: AppConstants.skipVersionKey)
- return skippedVersion == version
- }
-
- /// Mark a version to be skipped
- func skipVersion(_ version: String) {
- userDefaults.set(version, forKey: AppConstants.skipVersionKey)
- }
-
- /// Clear skipped version
- func clearSkippedVersion() {
- userDefaults.removeObject(forKey: AppConstants.skipVersionKey)
- }
-
- /// Get the current app version
- var currentVersion: String {
- return AppConstants.appVersion
- }
-
- /// Get the current app version with build number
- var currentVersionDetailed: String {
- let version = AppConstants.appVersion
- let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "1"
- return version != buildNumber ? "\(version) (\(buildNumber))" : version
- }
-
- /// Get time since last update check
- var timeSinceLastCheck: TimeInterval? {
- guard let lastCheck = userDefaults.object(forKey: AppConstants.lastUpdateCheckKey) as? Date else {
- return nil
- }
- return Date().timeIntervalSince(lastCheck)
- }
-
- // MARK: - Private Methods
-
- private func setupUpdater() {
- // Configure updater settings (will be set by configureAutomaticChecks)
- updaterController.updater.updateCheckInterval = AppConstants.updateCheckInterval
-
- // Log feed URL configuration (will be provided by delegate)
- print("✅ Sparkle configured with delegate feed URL: \(AppConstants.appcastURL)")
-
- // Set initial defaults if not set
- if !userDefaults.bool(forKey: "hasSetDefaultUpdateSettings") {
- // In manual-only mode, disable auto-updates by default
- #if DEBUG
- let defaultAutoUpdate = !AppConstants.DeveloperUpdateConfig.manualCheckOnly
- #else
- let defaultAutoUpdate = true
- #endif
-
- userDefaults.set(defaultAutoUpdate, forKey: AppConstants.autoUpdateEnabledKey)
- userDefaults.set(false, forKey: AppConstants.checkForBetaUpdatesKey)
- userDefaults.set(true, forKey: "hasSetDefaultUpdateSettings")
- }
-
- // Load last update check date
- if let lastCheck = userDefaults.object(forKey: AppConstants.lastUpdateCheckKey) as? Date {
- lastUpdateCheck = lastCheck
- }
- }
-
- private func configureAutomaticChecks() {
- // Disable automatic checks in manual-only mode (debug builds)
- let enableAutomaticChecks: Bool
- #if DEBUG
- enableAutomaticChecks = !AppConstants.DeveloperUpdateConfig.manualCheckOnly && autoUpdateEnabled
- #else
- enableAutomaticChecks = autoUpdateEnabled
- #endif
-
- updaterController.updater.automaticallyChecksForUpdates = enableAutomaticChecks
- updaterController.updater.updateCheckInterval = AppConstants.updateCheckInterval
- }
-
- // Note: Appcast URL configuration will be handled via Info.plist
- // or through a different Sparkle configuration method
-}
-
-// MARK: - Sparkle Delegate Extensions
-
-extension UpdaterManager: SPUUpdaterDelegate {
-
- /// Provide the feed URL if not configured in Info.plist
- nonisolated func feedURLString(for updater: SPUUpdater) -> String? {
- print("🔍 Sparkle requesting feed URL: \(AppConstants.appcastURL)")
- return AppConstants.appcastURL
- }
-
- /// Called when update check begins
- nonisolated func updater(_ updater: SPUUpdater, willInstallUpdate item: SUAppcastItem) {
- print("📦 Sparkle will install update: \(item.displayVersionString)")
- }
-
- /// Called when update check starts
- nonisolated func updater(_ updater: SPUUpdater, userDidSkipThisVersion item: SUAppcastItem) {
- print("⏭️ User skipped version: \(item.displayVersionString)")
- }
-
- /// Called when appcast download finishes
- nonisolated func updater(_ updater: SPUUpdater, didFinishLoading appcast: SUAppcast) {
- print("📥 Appcast loaded with \(appcast.items.count) items")
- if appcast.items.isEmpty {
- print("⚠️ Empty appcast - no releases available")
- }
- }
-
- nonisolated func updaterDidNotFindUpdate(_ updater: SPUUpdater) {
- print("✅ Sparkle: No updates found")
- DispatchQueue.main.async {
- self.isCheckingForUpdates = false
- self.isUpdateAvailable = false
- self.updateVersion = nil
- self.updateBuildNumber = nil
- self.updateReleaseNotes = nil
- self.lastCheckResult = "Current version \(self.currentVersionDetailed) is up to date"
- }
- }
-
- nonisolated func updater(_ updater: SPUUpdater, didFindValidUpdate item: SUAppcastItem) {
- // Extract string data to avoid sendability issues
- let version = item.displayVersionString
- let buildNumber = item.versionString
- let releaseNotesURL = item.releaseNotesURL?.absoluteString
- let currentVersion = AppConstants.appVersion
-
- print("🆕 Sparkle: Found update \(version)")
- DispatchQueue.main.async {
- self.isCheckingForUpdates = false
- self.isUpdateAvailable = true
- self.updateVersion = version
- self.updateBuildNumber = buildNumber
- self.updateReleaseNotes = releaseNotesURL
- self.lastCheckResult = "Update available: \(self.currentVersionDetailed) → \(version)"
- // Note: currentUpdateItem is omitted due to sendability constraints
- }
- }
-
- nonisolated func updater(_ updater: SPUUpdater, didAbortWithError error: Error) {
- print("❌ Sparkle error: \(error.localizedDescription)")
- DispatchQueue.main.async {
- self.isCheckingForUpdates = false
- self.updateError = error.localizedDescription
- self.lastCheckResult = "Failed to check for updates: \(error.localizedDescription)"
- }
- }
-}
-
-// MARK: - Update Information Helper
-
-extension UpdaterManager {
-
- /// Format the version information for display
- func formatVersionInfo() -> String {
- guard let version = updateVersion else { return "No update information available" }
-
- if let buildNumber = updateBuildNumber, version != buildNumber {
- return "\(version) (\(buildNumber))"
- } else {
- return version
- }
- }
-
- /// Get release notes as attributed string if available
- func getReleaseNotes() -> AttributedString? {
- guard let releaseNotesURL = updateReleaseNotes,
- let releaseNotesData = releaseNotesURL.data(using: .utf8) else {
- return nil
- }
-
- // Simple plain text conversion - could be enhanced to support HTML/Markdown
- let plainText = String(data: releaseNotesData, encoding: .utf8) ?? "Release notes unavailable"
- return AttributedString(plainText)
- }
-
- /// Check if current version is a beta version
- var isCurrentVersionBeta: Bool {
- return currentVersion.contains("beta") || currentVersion.contains("rc")
- }
-}
diff --git a/Sources/ClickIt/UI/Components/DeveloperUpdateButton.swift b/Sources/ClickIt/UI/Components/DeveloperUpdateButton.swift
deleted file mode 100644
index 625786c..0000000
--- a/Sources/ClickIt/UI/Components/DeveloperUpdateButton.swift
+++ /dev/null
@@ -1,61 +0,0 @@
-import SwiftUI
-
-/// Simple update button component for development builds
-/// Part of Phase 1 MVP implementation for manual update checking
-struct DeveloperUpdateButton: View {
- @ObservedObject var updaterManager: UpdaterManager
-
- var body: some View {
- VStack(spacing: 8) {
- Button("Check for Updates") {
- updaterManager.checkForUpdates()
- }
- .disabled(updaterManager.isCheckingForUpdates)
- .buttonStyle(.bordered)
- .controlSize(.regular)
-
- if updaterManager.isUpdateAvailable {
- Button("Install Update") {
- updaterManager.installUpdate()
- }
- .buttonStyle(.borderedProminent)
- .controlSize(.regular)
- }
-
- // Show status information
- if updaterManager.isCheckingForUpdates {
- HStack(spacing: 4) {
- ProgressView()
- .scaleEffect(0.6)
- Text("Checking...")
- .font(.caption)
- .foregroundColor(.secondary)
- }
- } else if let result = updaterManager.lastCheckResult {
- Text(result)
- .font(.caption)
- .foregroundColor(updaterManager.isUpdateAvailable ? .green :
- updaterManager.updateError != nil ? .red : .secondary)
- .multilineTextAlignment(.center)
- } else if let error = updaterManager.updateError {
- Text("Error: \(error)")
- .font(.caption)
- .foregroundColor(.red)
- .multilineTextAlignment(.center)
- }
- }
- .padding(12)
- .background(Color(.controlBackgroundColor))
- .cornerRadius(8)
- .overlay(
- RoundedRectangle(cornerRadius: 8)
- .stroke(Color.secondary.opacity(0.3), lineWidth: 1)
- )
- }
-}
-
-struct DeveloperUpdateButton_Previews: PreviewProvider {
- static var previews: some View {
- DeveloperUpdateButton(updaterManager: UpdaterManager())
- }
-}
\ No newline at end of file
diff --git a/Sources/ClickIt/UI/Components/UpdateNotificationCard.swift b/Sources/ClickIt/UI/Components/UpdateNotificationCard.swift
deleted file mode 100644
index b20d4f2..0000000
--- a/Sources/ClickIt/UI/Components/UpdateNotificationCard.swift
+++ /dev/null
@@ -1,211 +0,0 @@
-import SwiftUI
-
-/// Card component that displays update notifications and controls
-struct UpdateNotificationCard: View {
- @ObservedObject var updaterManager: UpdaterManager
- @State private var showingUpdateDetails = false
-
- var body: some View {
- VStack(spacing: 12) {
- if updaterManager.isCheckingForUpdates {
- checkingForUpdatesView
- } else if updaterManager.isUpdateAvailable {
- updateAvailableView
- }
- }
- .padding(16)
- .background(backgroundGradient)
- .cornerRadius(12)
- .shadow(color: .black.opacity(0.1), radius: 4, x: 0, y: 2)
- .sheet(isPresented: $showingUpdateDetails) {
- UpdateDetailsView(updaterManager: updaterManager)
- }
- }
-
- @ViewBuilder
- private var checkingForUpdatesView: some View {
- HStack(spacing: 12) {
- ProgressView()
- .scaleEffect(0.8)
- .progressViewStyle(CircularProgressViewStyle(tint: .blue))
-
- VStack(alignment: .leading, spacing: 4) {
- Text("Checking for Updates")
- .font(.headline)
- .foregroundColor(.primary)
-
- Text("Looking for the latest version...")
- .font(.caption)
- .foregroundColor(.secondary)
- }
-
- Spacer()
- }
- }
-
- @ViewBuilder
- private var updateAvailableView: some View {
- VStack(spacing: 12) {
- // Update Header
- HStack(spacing: 12) {
- Image(systemName: "arrow.down.circle.fill")
- .font(.title2)
- .foregroundColor(.green)
-
- VStack(alignment: .leading, spacing: 4) {
- Text("Update Available")
- .font(.headline)
- .foregroundColor(.primary)
-
- if updaterManager.updateVersion != nil {
- Text("Version \(updaterManager.formatVersionInfo())")
- .font(.caption)
- .foregroundColor(.secondary)
- }
- }
-
- Spacer()
-
- // Update Actions
- HStack(spacing: 8) {
- Button("Details") {
- showingUpdateDetails = true
- }
- .buttonStyle(.bordered)
- .controlSize(.small)
-
- Button("Update Now") {
- updaterManager.installUpdate()
- }
- .buttonStyle(.borderedProminent)
- .controlSize(.small)
- }
- }
- }
- }
-
- private var backgroundGradient: some View {
- LinearGradient(
- gradient: Gradient(colors: [
- Color.blue.opacity(0.05),
- Color.green.opacity(0.05)
- ]),
- startPoint: .topLeading,
- endPoint: .bottomTrailing
- )
- }
-}
-
-/// Detailed view for update information
-struct UpdateDetailsView: View {
- @ObservedObject var updaterManager: UpdaterManager
- @Environment(\.dismiss) private var dismiss
-
- var body: some View {
- NavigationView {
- ScrollView {
- VStack(alignment: .leading, spacing: 20) {
- if updaterManager.updateVersion != nil {
- // Version Information
- VStack(alignment: .leading, spacing: 8) {
- Text("Update Information")
- .font(.headline)
-
- InfoRow(label: "Current Version", value: updaterManager.currentVersion)
- InfoRow(label: "New Version", value: updaterManager.formatVersionInfo())
-
- if let releaseNotesURL = updaterManager.updateReleaseNotes {
- InfoRow(label: "Release Notes URL", value: releaseNotesURL)
- }
- }
- .padding()
- .background(Color(.controlBackgroundColor))
- .cornerRadius(8)
-
- // Release Notes
- VStack(alignment: .leading, spacing: 8) {
- Text("Release Notes")
- .font(.headline)
-
- if let releaseNotes = updaterManager.getReleaseNotes() {
- Text(releaseNotes)
- .font(.body)
- .padding()
- .background(Color(.textBackgroundColor))
- .cornerRadius(8)
- } else {
- Text("No release notes available")
- .font(.body)
- .foregroundColor(.secondary)
- .italic()
- }
- }
-
- // Update Actions
- VStack(spacing: 12) {
- Button("Install Update") {
- updaterManager.installUpdate()
- dismiss()
- }
- .buttonStyle(.borderedProminent)
- .controlSize(.large)
- .frame(maxWidth: .infinity)
-
- HStack(spacing: 12) {
- Button("Skip This Version") {
- if let version = updaterManager.updateVersion {
- updaterManager.skipVersion(version)
- }
- dismiss()
- }
- .buttonStyle(.bordered)
-
- Button("Remind Me Later") {
- dismiss()
- }
- .buttonStyle(.bordered)
- }
- }
- .padding(.top)
- }
- }
- .padding()
- }
- .navigationTitle("Update Available")
- .toolbar {
- ToolbarItem(placement: .primaryAction) {
- Button("Close") {
- dismiss()
- }
- }
- }
- }
- .frame(width: 500, height: 600)
- }
-}
-
-/// Helper view for displaying information rows
-struct InfoRow: View {
- let label: String
- let value: String
-
- var body: some View {
- HStack {
- Text(label)
- .fontWeight(.medium)
- .foregroundColor(.secondary)
-
- Spacer()
-
- Text(value)
- .foregroundColor(.primary)
- .multilineTextAlignment(.trailing)
- }
- }
-}
-
-struct UpdateNotificationCard_Previews: PreviewProvider {
- static var previews: some View {
- UpdateNotificationCard(updaterManager: UpdaterManager())
- }
-}
\ No newline at end of file
diff --git a/Sources/ClickIt/UI/Views/ContentView.swift b/Sources/ClickIt/UI/Views/ContentView.swift
index b1100af..2d473d0 100644
--- a/Sources/ClickIt/UI/Views/ContentView.swift
+++ b/Sources/ClickIt/UI/Views/ContentView.swift
@@ -12,9 +12,7 @@ struct ContentView: View {
@EnvironmentObject private var permissionManager: PermissionManager
@EnvironmentObject private var hotkeyManager: HotkeyManager
@EnvironmentObject private var viewModel: ClickItViewModel
- @StateObject private var updaterManager = UpdaterManager()
@State private var showingPermissionSetup = false
- @State private var showingUpdateSettings = false
var body: some View {
if permissionManager.allPermissionsGranted {
@@ -33,17 +31,6 @@ struct ContentView: View {
// Status Header Card
StatusHeaderCard(viewModel: viewModel)
- // Development Update Button (Phase 1 MVP)
- #if DEBUG
- if AppConstants.DeveloperUpdateConfig.enabled {
- DeveloperUpdateButton(updaterManager: updaterManager)
- }
- #endif
-
- // Update Notification (if available)
- if updaterManager.isUpdateAvailable || updaterManager.isCheckingForUpdates {
- UpdateNotificationCard(updaterManager: updaterManager)
- }
// Target Point Selection Card
TargetPointSelectionCard(viewModel: viewModel)
@@ -61,24 +48,6 @@ struct ContentView: View {
.background(Color(NSColor.controlBackgroundColor))
.onAppear {
permissionManager.updatePermissionStatus()
-
- // Check for updates on app launch (respecting development configuration)
- #if DEBUG
- // In development builds, only manual checking is enabled
- if !AppConstants.DeveloperUpdateConfig.manualCheckOnly,
- updaterManager.autoUpdateEnabled,
- let timeSinceLastCheck = updaterManager.timeSinceLastCheck,
- timeSinceLastCheck > AppConstants.updateCheckInterval {
- updaterManager.checkForUpdates()
- }
- #else
- // In production builds, use normal automatic checking
- if updaterManager.autoUpdateEnabled,
- let timeSinceLastCheck = updaterManager.timeSinceLastCheck,
- timeSinceLastCheck > AppConstants.updateCheckInterval {
- updaterManager.checkForUpdates()
- }
- #endif
}
}
diff --git a/Sources/ClickIt/Utils/Constants/AppConstants.swift b/Sources/ClickIt/Utils/Constants/AppConstants.swift
index 1a4e479..1f3b547 100644
--- a/Sources/ClickIt/Utils/Constants/AppConstants.swift
+++ b/Sources/ClickIt/Utils/Constants/AppConstants.swift
@@ -53,46 +53,6 @@ struct AppConstants {
// System Requirements
static let minimumMemoryRequirementGB: Double = 4.0
- // Auto-Update Configuration
- static let appcastURL = "https://jsonify.github.io/clickit/appcast.xml"
- static let updateCheckInterval: TimeInterval = 24 * 60 * 60 // 24 hours
- static let betaAppcastURL = "https://jsonify.github.io/clickit/appcast-beta.xml"
- static let githubReleasesAPI = "https://api.github.com/repos/jsonify/clickit/releases"
- static let githubRepository = "jsonify/clickit"
-
- // Update Settings Keys (UserDefaults)
- static let autoUpdateEnabledKey = "autoUpdateEnabled"
- static let checkForBetaUpdatesKey = "checkForBetaUpdates"
- static let lastUpdateCheckKey = "lastUpdateCheck"
- static let skipVersionKey = "skipVersion"
-
- // Development Update Configuration (Phase 1 MVP)
- struct DeveloperUpdateConfig {
- static let enabled = true
- static let manualCheckOnly = true
- static let skipBetaChannel = true
- static let skipSkipVersion = true
-
- // Build-specific configuration
- #if DEBUG
- static let updateConfigMode = "development"
- static let enableAutomaticChecking = false
- static let showAdvancedOptions = false
- #elseif BETA
- static let updateConfigMode = "beta"
- static let enableAutomaticChecking = true
- static let showAdvancedOptions = true
- static let enableBetaChannel = true
- #else
- static let updateConfigMode = "production"
- static let enableAutomaticChecking = true
- static let showAdvancedOptions = true
- static let enableBetaChannel = false
- #endif
-
- // Private initializer to prevent instantiation
- private init() {}
- }
// Private initializer to prevent instantiation
private init() {}
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index f1fbe8d..7a08f43 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -18,6 +18,20 @@ platform :mac do
lane :build_debug do
Dir.chdir("..") do
sh("./build_app_unified.sh debug spm")
+
+ # Apply adhoc signature if no certificate was found
+ app_path = "dist/ClickIt.app"
+ if File.exist?(app_path)
+ begin
+ # Check if app is already properly signed
+ sh("codesign -v '#{app_path}' 2>/dev/null")
+ UI.message("✅ App already properly signed")
+ rescue
+ UI.message("🔐 Applying adhoc signature for local development...")
+ sh("codesign --force --deep --sign - --timestamp=none --options=runtime '#{app_path}'")
+ UI.success("✅ Adhoc signature applied successfully")
+ end
+ end
end
UI.success("Debug build completed! 🎉")
UI.message("App location: dist/ClickIt.app")
@@ -27,6 +41,20 @@ platform :mac do
lane :build_release do
Dir.chdir("..") do
sh("./build_app_unified.sh release spm")
+
+ # Apply adhoc signature if no certificate was found
+ app_path = "dist/ClickIt.app"
+ if File.exist?(app_path)
+ begin
+ # Check if app is already properly signed
+ sh("codesign -v '#{app_path}' 2>/dev/null")
+ UI.message("✅ App already properly signed")
+ rescue
+ UI.message("🔐 Applying adhoc signature for local development...")
+ sh("codesign --force --deep --sign - --timestamp=none --options=runtime '#{app_path}'")
+ UI.success("✅ Adhoc signature applied successfully")
+ end
+ end
end
UI.success("Release build completed! 🚀")
UI.message("App location: dist/ClickIt.app")
diff --git a/run_clickit_unified.sh b/run_clickit_unified.sh
index 62a98e6..c4c7f98 100755
--- a/run_clickit_unified.sh
+++ b/run_clickit_unified.sh
@@ -41,10 +41,22 @@ case "$BUILD_SYSTEM" in
fi
echo "🚀 Launching ClickIt.app..."
- open "dist/ClickIt.app"
-
- echo "✅ ClickIt launched successfully!"
- echo "🔧 The app should appear in your Dock and System Settings > Accessibility"
+ if open "dist/ClickIt.app" 2>/dev/null; then
+ echo "✅ ClickIt launched successfully!"
+ echo "🔧 The app should appear in your Dock and System Settings > Accessibility"
+ else
+ echo "⚠️ App bundle launch failed - trying direct executable..."
+ if [ -f "dist/ClickIt.app/Contents/MacOS/ClickIt" ]; then
+ echo "🚀 Launching executable directly..."
+ "./dist/ClickIt.app/Contents/MacOS/ClickIt" &
+ echo "✅ ClickIt launched via direct executable!"
+ echo "🔧 The app should appear in your Dock and System Settings > Accessibility"
+ else
+ echo "❌ Could not launch ClickIt"
+ echo "💡 Try: codesign --force --deep --sign - dist/ClickIt.app"
+ exit 1
+ fi
+ fi
;;
"xcode")