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 """ - - <![CDATA[\(title)]]> - - \(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")