diff --git a/docs/AGENTS.md b/docs/AGENTS.md index 13770b8..b8ce38f 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -224,6 +224,27 @@ Minor helper changes that do not alter behavior may proceed, but must still be r - Keep one clear orchestrator per workflow and move helper policies/utilities/IO/details to focused companion files. - Treat structural refactors as behavior-preserving by default (no runtime/UI changes unless explicitly requested). +## File naming rules + +- File names must use letters only (`A-Z`, `a-z`) by default. +- Use `PascalCase` and compose names from: + - feature/domain prefix, + - concrete responsibility, + - role suffix. +- Match existing repository patterns, for example: + - `AnalysisLogicMacOSCompatibility.swift` + - `MacOSDiscoveryCatalogParser.swift` + - `MacOSDownloadOrchestrator.swift` + - `MacOSDownloaderListView.swift` +- The underscore (`_`) is allowed only when an external or required structure mandates it (for example GitHub workflow conventions). +- Do not use spaces, hyphens, digits, or other special characters in file names unless required by external format constraints. + +## Large file split rule + +- If a file becomes materially larger after a change (many additional lines), the agent must explicitly evaluate whether splitting it into smaller focused files makes sense. +- When a split is reasonable, the agent should propose the split before implementation or include the split in the same change when safe. +- Splits should preserve behavior and separate concerns (orchestrator vs helpers/policies/parsers/ui parts). + ## Branch naming convention When branch creation is requested: diff --git a/docs/reference/core/FILE_STRUCTURE.md b/docs/reference/core/FILE_STRUCTURE.md index 1173409..4e2456e 100644 --- a/docs/reference/core/FILE_STRUCTURE.md +++ b/docs/reference/core/FILE_STRUCTURE.md @@ -13,6 +13,22 @@ - `macUSB/Features/Finish/*` — result and cleanup UX. - `macUSB/Features/Downloader/*` — downloader coordinator + UI + logic split. +### Analysis layout + +- `macUSB/Features/Analysis/SystemAnalysisView.swift` — analysis UI screen. +- `macUSB/Features/Analysis/AnalysisLogic.swift` — analysis state + facade API for UI bindings. +- `macUSB/Features/Analysis/AnalysisSelectionHandoff.swift` — handoff bridge for pending installer URL from downloader flow. +- `macUSB/Features/Analysis/AnalysisNotifications.swift` — shared `Notification.Name` constants used by analysis/flow wiring. +- `macUSB/Features/Analysis/Logic/AnalysisLogicFileSelection.swift` — file selection/drop/open-panel logic. +- `macUSB/Features/Analysis/Logic/AnalysisLogicAnalysisFlow.swift` — orchestration of analysis execution for `.app` and image sources. +- `macUSB/Features/Analysis/Logic/macOS/AnalysisLogicMacOSCompatibility.swift` — macOS-only compatibility/version-family detection rules and flag mapping. +- `macUSB/Features/Analysis/Logic/macOS/AnalysisLogicMacOSImageMounting.swift` — image mounting + mounted-source guard + legacy image read logic. +- `macUSB/Features/Analysis/Logic/macOS/AnalysisLogicMacOSInstallerMetadata.swift` — installer metadata/version parsing and USB capacity mapping helpers. +- `macUSB/Features/Analysis/Logic/macOS/AnalysisLogicMacOSInstallerIcon.swift` — installer icon discovery. +- `macUSB/Features/Analysis/Logic/AnalysisLogicUsbDrives.swift` — USB drive enumeration/refresh/capacity checks. +- `macUSB/Features/Analysis/Logic/macOS/AnalysisLogicMacOSLifecycle.swift` — reset/cleanup/manual Tiger flow helpers. +- Reserved naming for future non-macOS support: `AnalysisLogicLinuxCompatibility.swift` (not implemented yet). + ### Downloader layout - `macUSB/Features/Downloader/MacOSDownloaderCoordinator.swift` diff --git a/docs/reference/features/analysis/ANALYSIS_COMPATIBILITY.md b/docs/reference/features/analysis/ANALYSIS_COMPATIBILITY.md index e590377..3bd2be0 100644 --- a/docs/reference/features/analysis/ANALYSIS_COMPATIBILITY.md +++ b/docs/reference/features/analysis/ANALYSIS_COMPATIBILITY.md @@ -1,5 +1,7 @@ # Analysis and Compatibility Contract +Current implementation scope of this contract is the macOS analysis path (see `AnalysisLogicMacOSCompatibility.swift`). + ## Detection Source of Truth Analysis flags are the source of truth for workflow branch selection. diff --git a/macUSB.xcodeproj/project.pbxproj b/macUSB.xcodeproj/project.pbxproj index 7cc7ff6..782cbaa 100644 --- a/macUSB.xcodeproj/project.pbxproj +++ b/macUSB.xcodeproj/project.pbxproj @@ -107,6 +107,16 @@ App/ContentView.swift, App/macUSBApp.swift, Features/Analysis/AnalysisLogic.swift, + Features/Analysis/AnalysisNotifications.swift, + Features/Analysis/AnalysisSelectionHandoff.swift, + Features/Analysis/Logic/AnalysisLogicAnalysisFlow.swift, + Features/Analysis/Logic/AnalysisLogicFileSelection.swift, + Features/Analysis/Logic/AnalysisLogicUsbDrives.swift, + Features/Analysis/Logic/macOS/AnalysisLogicMacOSCompatibility.swift, + Features/Analysis/Logic/macOS/AnalysisLogicMacOSImageMounting.swift, + Features/Analysis/Logic/macOS/AnalysisLogicMacOSInstallerIcon.swift, + Features/Analysis/Logic/macOS/AnalysisLogicMacOSInstallerMetadata.swift, + Features/Analysis/Logic/macOS/AnalysisLogicMacOSLifecycle.swift, Features/Analysis/SystemAnalysisView.swift, Features/Downloader/Logic/Assembly/MacOSAssemblyFileUtils.swift, Features/Downloader/Logic/Assembly/MacOSAssemblyLegacyFlow.swift, diff --git a/macUSB/Features/Analysis/AnalysisLogic.swift b/macUSB/Features/Analysis/AnalysisLogic.swift index 58ed63e..fd567bc 100644 --- a/macUSB/Features/Analysis/AnalysisLogic.swift +++ b/macUSB/Features/Analysis/AnalysisLogic.swift @@ -1,16 +1,9 @@ import SwiftUI import AppKit -import UniformTypeIdentifiers -import Combine import Foundation +import Combine final class AnalysisLogic: ObservableObject { - private enum ImageReadResult { - case success(name: String, rawVersion: String, appURL: URL, mountPath: String) - case sourceAlreadyMounted(mountPath: String) - case failure - } - // MARK: - Published State (moved from SystemAnalysisView) @Published var selectedFilePath: String = "" @Published var selectedFileUrl: URL? @@ -90,18 +83,6 @@ final class AnalysisLogic: ObservableObject { requiredUSBCapacityGB.map(String.init) ?? "--" } - private var requiredUSBCapacityBytes: Int? { - guard let requiredGB = requiredUSBCapacityGB else { return nil } - switch requiredGB { - case 16: - return 15_000_000_000 - case 32: - return 28_000_000_000 - default: - return requiredGB * 1_000_000_000 - } - } - // Computed: true only when app has recognized a supported system and can proceed normally var isRecognizedAndSupported: Bool { // Recognized and supported when analysis finished, a valid source exists or PPC flow is selected, @@ -113,1116 +94,16 @@ final class AnalysisLogic: ObservableObject { return recognized && hasValidSourceOrPPC && detected && !unsupported } - private func updateRequiredUSBCapacity(rawVersion: String, name: String) { - guard let majorVersion = marketingMajorVersion(raw: rawVersion, name: name) else { - requiredUSBCapacityGB = nil - return - } - requiredUSBCapacityGB = (majorVersion >= 15) ? 32 : 16 - } - - private func marketingMajorVersion(raw: String, name: String) -> Int? { - let marketingVersion = formatMarketingVersion(raw: raw, name: name) - guard let majorToken = marketingVersion.split(separator: ".").first else { return nil } - return Int(majorToken) - } - - // MARK: - Helper to enumerate external hard drives (non-removable) - private func enumerateExternalUSBHardDrives() -> [USBDrive] { - let keys: [URLResourceKey] = [ - .volumeNameKey, - .volumeIsRemovableKey, - .volumeIsInternalKey, - .volumeTotalCapacityKey - ] - guard let urls = FileManager.default.mountedVolumeURLs(includingResourceValuesForKeys: keys, options: .skipHiddenVolumes) else { return [] } - - let candidates: [USBDrive] = urls.compactMap { url -> USBDrive? in - guard let v = try? url.resourceValues(forKeys: Set(keys)) else { return nil } - // Only external (non-internal), non-network, non-removable volumes (HDD/SSD) - if (v.volumeIsInternal ?? true) { return nil } - // Filter out obvious network-mounted volumes by scheme (e.g., afp, smb, nfs) - let scheme = url.scheme?.lowercased() - if let scheme = scheme, ["afp", "smb", "nfs", "ftp", "webdav"].contains(scheme) { return nil } - if (v.volumeIsRemovable ?? false) { return nil } - guard let name = v.volumeName else { return nil } - let bsd = USBDriveLogic.getBSDName(from: url) - guard !bsd.isEmpty && bsd != "unknown" else { return nil } - let totalCapacity = Int64(v.volumeTotalCapacity ?? 0) - let size = ByteCountFormatter.string(fromByteCount: totalCapacity, countStyle: .file) - let whole = USBDriveLogic.wholeDiskName(from: bsd) - let speed = USBDriveLogic.detectUSBSpeed(forBSDName: whole) - let partitionScheme = USBDriveLogic.detectPartitionScheme(forBSDName: whole) - let fileSystemFormat = USBDriveLogic.detectFileSystemFormat(forVolumeURL: url) - return USBDrive( - name: name, - device: bsd, - size: size, - url: url, - usbSpeed: speed, - partitionScheme: partitionScheme, - fileSystemFormat: fileSystemFormat - ) - } - return candidates - } - // MARK: - Logging - private func log(_ message: String, category: String = "FileAnalysis") { + func log(_ message: String, category: String = "FileAnalysis") { AppLogging.info(message, category: category) } - private func logError(_ message: String, category: String = "FileAnalysis") { - AppLogging.error(message, category: category) - } - private func stage(_ title: String) { - AppLogging.stage(title) - } - - // MARK: - Logic (moved from SystemAnalysisView) - func handleDrop(providers: [NSItemProvider]) -> Bool { - self.log("Odebrano przeciągnięcie pliku (providers=\(providers.count)). Szukam URL...") - if let provider = providers.first(where: { $0.hasItemConformingToTypeIdentifier(UTType.fileURL.identifier) }) { - provider.loadItem(forTypeIdentifier: UTType.fileURL.identifier, options: nil) { (item, error) in - if let data = item as? Data, let url = URL(dataRepresentation: data, relativeTo: nil) { - self.log("Przeciągnięto plik w formacie .\(url.pathExtension.lowercased())") - let ext = url.pathExtension.lowercased() - if ext == "dmg" || ext == "app" || ext == "iso" || ext == "cdr" { - self.processDroppedURL(url) - } - } - else if let url = item as? URL { - self.log("Przeciągnięto plik w formacie .\(url.pathExtension.lowercased())") - let ext = url.pathExtension.lowercased() - if ext == "dmg" || ext == "app" || ext == "iso" || ext == "cdr" { - self.processDroppedURL(url) - } - } - } - return true - } - return false - } - - func processDroppedURL(_ url: URL) { - DispatchQueue.main.async { - let ext = url.pathExtension.lowercased() - self.log("Wybrano plik w formacie .\(ext). Resetuję stan i przygotowuję analizę.") - if ext == "dmg" || ext == "app" || ext == "iso" || ext == "cdr" { - withAnimation { - self.selectedFilePath = url.path - self.selectedFileUrl = url - self.recognizedVersion = "" - self.isSystemDetected = false - self.sourceAppURL = nil - self.detectedSystemIcon = nil - self.selectedDrive = nil - self.capacityCheckFinished = false - self.showUSBSection = false - self.showUnsupportedMessage = false - self.isSierra = false - self.isMavericks = false - self.isUnsupportedSierra = false - self.isPPC = false - self.legacyArchInfo = nil - self.userSkippedAnalysis = false - self.shouldShowMavericksDialog = false - self.requiredUSBCapacityGB = nil - } - self.log("Lokalizacja wybranego pliku: \(url.path)") - self.log("Źródło do rozpoznania wersji: \(url.path)") - } - } - } - - func applySelectedURLAndStartAnalysis(_ url: URL) { - let ext = url.pathExtension.lowercased() - guard ext == "dmg" || ext == "app" || ext == "iso" || ext == "cdr" else { - logError("Pominięto automatyczne podstawienie ścieżki. Nieobsługiwany format: .\(ext)") - return - } - - processDroppedURL(url) - DispatchQueue.main.async { [weak self] in - self?.startAnalysis() - } - } - - func selectDMGFile() { - self.log("Otwieram panel wyboru pliku…") - let p = NSOpenPanel() - p.allowedContentTypes = [.diskImage, .applicationBundle] - // Dodajemy obsługę .iso i .cdr, które nie mają jeszcze UTType w UniformTypeIdentifiers, więc rozszerzamy allowedFileTypes - p.allowedFileTypes = ["dmg", "iso", "cdr", "app"] - p.allowsMultipleSelection = false - p.begin { if $0 == .OK, let url = p.url { - let ext = url.pathExtension.lowercased() - guard ext == "dmg" || ext == "iso" || ext == "cdr" || ext == "app" else { return } - withAnimation { - self.selectedFilePath = url.path - self.selectedFileUrl = url - self.recognizedVersion = "" - self.isSystemDetected = false - self.sourceAppURL = nil - self.detectedSystemIcon = nil - self.selectedDrive = nil - self.capacityCheckFinished = false - self.showUSBSection = false - self.showUnsupportedMessage = false - self.isSierra = false - self.isMavericks = false - self.isUnsupportedSierra = false - self.isPPC = false - self.legacyArchInfo = nil - self.userSkippedAnalysis = false - self.shouldShowMavericksDialog = false - self.requiredUSBCapacityGB = nil - } - self.log("Wybrano plik w formacie .\(ext)") - self.log("Lokalizacja wybranego pliku: \(url.path)") - self.log("Źródło do rozpoznania wersji: \(url.path)") - } else { - self.log("Anulowano wybór pliku") - } } - } - - func startAnalysis() { - guard let url = selectedFileUrl else { return } - self.stage("Analiza pliku — start") - self.log("Rozpoczynam analizę pliku") - self.log("Źródło pliku do odczytu wersji: \(url.path)") - withAnimation { isAnalyzing = true } - detectedSystemIcon = nil - selectedDrive = nil; capacityCheckFinished = false - showUSBSection = false; showUnsupportedMessage = false - isUnsupportedSierra = false - isPPC = false - isMavericks = false - shouldShowAlreadyMountedSourceAlert = false - requiredUSBCapacityGB = nil - - let ext = url.pathExtension.lowercased() - self.log("Wykryto rozszerzenie: \(ext)") - if ext == "dmg" || ext == "iso" || ext == "cdr" { - self.stage("Analiza obrazu (DMG/ISO/CDR) — start") - self.log("Analiza obrazu (DMG/ISO/CDR): montowanie obrazu przez hdiutil (attach -plist -nobrowse -readonly), odczyt Info.plist z aplikacji oraz wykrywanie wersji i trybu instalacji.") - let oldMountPath = self.mountedDMGPath - DispatchQueue.global(qos: .userInitiated).async { - if let path = oldMountPath { - let task = Process(); task.executableURL = URL(fileURLWithPath: "/usr/bin/hdiutil"); task.arguments = ["detach", path, "-force"]; try? task.run(); task.waitUntilExit() - } - let shouldDetectAlreadyMountedSource = (ext == "cdr" || ext == "iso") - let result = self.mountAndReadInfo(dmgUrl: url, detectPreMountedSource: shouldDetectAlreadyMountedSource) - DispatchQueue.main.async { - withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) { - self.isAnalyzing = false - let mountedReadInfo: (String, String, URL, String)? - let sourceAlreadyMounted: Bool - switch result { - case .success(let name, let rawVersion, let appURL, let mountPath): - mountedReadInfo = (name, rawVersion, appURL, mountPath) - sourceAlreadyMounted = false - case .sourceAlreadyMounted(let mountPath): - mountedReadInfo = nil - sourceAlreadyMounted = true - self.log("Wykryto, że wybrany obraz źródłowy jest już zamontowany: \(mountPath)") - case .failure: - mountedReadInfo = nil - sourceAlreadyMounted = false - } - if let (_, _, _, mp) = mountedReadInfo { self.mountedDMGPath = mp } else { self.mountedDMGPath = nil } - if let (name, rawVer, appURL, _) = mountedReadInfo { - let friendlyVer = self.formatMarketingVersion(raw: rawVer, name: name) - var cleanName = name - cleanName = cleanName.replacingOccurrences(of: "Install ", with: "") - cleanName = cleanName.replacingOccurrences(of: "macOS ", with: "") - cleanName = cleanName.replacingOccurrences(of: "Mac OS X ", with: "") - cleanName = cleanName.replacingOccurrences(of: "OS X ", with: "") - let prefix = name.contains("macOS") ? "macOS" : (name.contains("OS X") ? "OS X" : "macOS") - - self.recognizedVersion = "\(prefix) \(cleanName) \(friendlyVer)" - self.updateRequiredUSBCapacity(rawVersion: rawVer, name: name) - self.sourceAppURL = appURL - self.updateDetectedSystemIcon(from: appURL) - - // Try to read ProductUserVisibleVersion from mounted image (Tiger/Leopard) - var userVisibleVersionFromMounted: String? = nil - if let mountPath = self.mountedDMGPath { - let sysVerPlist = URL(fileURLWithPath: mountPath).appendingPathComponent("System/Library/CoreServices/SystemVersion.plist") - if let data = try? Data(contentsOf: sysVerPlist), - let dict = try? PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any], - let userVisible = dict["ProductUserVisibleVersion"] as? String { - userVisibleVersionFromMounted = userVisible - } - } - - // Use lowercase name for detection - let nameLower = name.lowercased() - - // Leopard/Tiger detection (PowerPC) using name, raw version, or mounted SystemVersion.plist - let isLeopard = nameLower.contains("leopard") || rawVer.starts(with: "10.5") || (userVisibleVersionFromMounted?.hasPrefix("10.5") ?? false) - let isTiger = nameLower.contains("tiger") || rawVer.starts(with: "10.4") || (userVisibleVersionFromMounted?.hasPrefix("10.4") ?? false) - let isPanther = nameLower.contains("panther") || rawVer.starts(with: "10.3") || (userVisibleVersionFromMounted?.hasPrefix("10.3") ?? false) - let isSnowLeopard = nameLower.contains("snow leopard") || rawVer.starts(with: "10.6") || (userVisibleVersionFromMounted?.hasPrefix("10.6") ?? false) - - // Disable kernel arch detection for Panther (10.3), Tiger (10.4), Leopard (10.5) and Snow Leopard (10.6). Always mark PPC flow for legacy, but do not set legacyArchInfo. - if (isLeopard || isTiger || isPanther || isSnowLeopard) { - self.isPPC = true // niezależnie od architektury, proces USB taki sam - self.legacyArchInfo = nil - } - - // Legacy versions exact recognition for mounted userVisibleVersion or fallback for legacy systems - if isLeopard { - if let userVisible = userVisibleVersionFromMounted { - self.recognizedVersion = "Mac OS X Leopard \(userVisible)" - } else if rawVer.starts(with: "10.5") { - self.recognizedVersion = "Mac OS X Leopard \(rawVer)" - } else { - self.recognizedVersion = "Mac OS X Leopard" - } - } - if isTiger { - if let userVisible = userVisibleVersionFromMounted { - self.recognizedVersion = "Mac OS X Tiger \(userVisible)" - } else if rawVer.starts(with: "10.4") { - self.recognizedVersion = "Mac OS X Tiger \(rawVer)" - } else { - self.recognizedVersion = "Mac OS X Tiger" - } - } - if isPanther { - self.logError("Wykryto niewspierany system: Mac OS X Panther (10.3). Przerywam analizę.") - if let userVisible = userVisibleVersionFromMounted { - self.recognizedVersion = "Mac OS X Panther \(userVisible)" - } else if rawVer.starts(with: "10.3") { - self.recognizedVersion = "Mac OS X Panther \(rawVer)" - } else { - self.recognizedVersion = "Mac OS X Panther" - } - } - if isSnowLeopard { - if let userVisible = userVisibleVersionFromMounted { - self.recognizedVersion = "Mac OS X Snow Leopard \(userVisible)" - } else if rawVer.starts(with: "10.6") { - self.recognizedVersion = "Mac OS X Snow Leopard \(rawVer)" - } else { - self.recognizedVersion = "Mac OS X Snow Leopard" - } - } - // If Panther is detected, mark as unsupported and block further processing - if isPanther { - self.isSystemDetected = false - self.showUSBSection = false - self.isPPC = false - self.legacyArchInfo = nil - // Show unsupported message immediately - withAnimation(.spring(response: 0.7, dampingFraction: 0.8)) { - self.showUnsupportedMessage = true - } - self.needsCodesign = false - self.isLegacyDetected = false - self.isRestoreLegacy = false - self.isCatalina = false - self.isSierra = false - self.isMavericks = false - self.isUnsupportedSierra = false - return - } - - // Systemy niewspierane (Explicit) - USUNIĘTO CATALINĘ - let isExplicitlyUnsupported = nameLower.contains("sierra") && !nameLower.contains("high") - - // Catalina detection - let isCatalina = nameLower.contains("catalina") || rawVer.starts(with: "10.15") - - // Sierra detection (supported only for installer version 12.6.06) - let isSierra = (rawVer == "12.6.06") - let isSierraName = nameLower.contains("sierra") && !nameLower.contains("high") - let isUnsupportedSierraVersion = isSierraName && !isSierra - self.isUnsupportedSierra = isUnsupportedSierraVersion - if isUnsupportedSierraVersion { self.logError("Ta wersja systemu macOS Sierra nie jest wspierana (wymagana 12.6.06).") } - - let isMavericks = nameLower.contains("mavericks") || rawVer.starts(with: "10.9") - - // Modern (Big Sur+) - let isModern = - nameLower.contains("tahoe") || // Dodano Tahoe - nameLower.contains("sur") || - nameLower.contains("monterey") || - nameLower.contains("ventura") || - nameLower.contains("sonoma") || - nameLower.contains("sequoia") || - rawVer.starts(with: "21.") || // Dodano Tahoe (v26/21.x) - rawVer.starts(with: "11.") || - (rawVer.starts(with: "12.") && !isExplicitlyUnsupported) || - (rawVer.starts(with: "13.") && !nameLower.contains("high")) || - (rawVer.starts(with: "14.") && !nameLower.contains("mojave")) || - (rawVer.starts(with: "15.") && !isExplicitlyUnsupported) - - // Old Supported (Mojave + High Sierra) - let isOldSupported = - nameLower.contains("mojave") || - nameLower.contains("high sierra") || - rawVer.starts(with: "10.14") || - rawVer.starts(with: "10.13") || - (rawVer.starts(with: "14.") && nameLower.contains("mojave")) || - (rawVer.starts(with: "13.") && nameLower.contains("high")) - - // Legacy No Codesign (Yosemite + El Capitan) - let isLegacyDetected = - nameLower.contains("yosemite") || - nameLower.contains("el capitan") || - rawVer.starts(with: "10.10") || - rawVer.starts(with: "10.11") - - // Legacy Restore (Lion + Mountain Lion) - let isRestoreLegacy = - nameLower.contains("mountain lion") || - nameLower.contains("lion") || - rawVer.starts(with: "10.8") || - rawVer.starts(with: "10.7") - - // ZMIANA: Dodanie isCatalina do isSystemDetected - self.isSystemDetected = isModern || isOldSupported || isLegacyDetected || isRestoreLegacy || isCatalina || isSierra || isMavericks - - // Catalina ma swój własny codesign, więc tu wyłączamy standardowy 'needsCodesign' - self.needsCodesign = isOldSupported && !isModern && !isLegacyDetected - self.isLegacyDetected = isLegacyDetected - self.isRestoreLegacy = isRestoreLegacy - self.isCatalina = isCatalina - self.isSierra = isSierra - self.isMavericks = isMavericks - if isMavericks { - self.shouldShowMavericksDialog = true - } - if isSierra { - self.recognizedVersion = "macOS Sierra 10.12" - self.needsCodesign = false - } - // Dla Leoparda/Tigera już ustawione na true powyżej, pozostaw - self.isPPC = self.isPPC || false - - let trueFlags = [ - self.isCatalina ? "isCatalina" : nil, - self.isSierra ? "isSierra" : nil, - self.isLegacyDetected ? "isLegacyDetected" : nil, - self.isRestoreLegacy ? "isRestoreLegacy" : nil, - self.isPPC ? "isPPC" : nil, - self.isUnsupportedSierra ? "isUnsupportedSierra" : nil, - self.isMavericks ? "isMavericks" : nil - ].compactMap { $0 }.joined(separator: ", ") - self.log("Analiza zakończona. Rozpoznano: \(self.recognizedVersion)") - self.log("Przypisane flagi: \(trueFlags.isEmpty ? "brak" : trueFlags)") - AppLogging.separator() - - if self.isSystemDetected { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { withAnimation(.spring(response: 0.7, dampingFraction: 0.8)) { self.showUSBSection = true } } - } else { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { withAnimation(.spring(response: 0.7, dampingFraction: 0.8)) { self.showUnsupportedMessage = true } } - } - } else if sourceAlreadyMounted { - self.recognizedVersion = "" - self.sourceAppURL = nil - self.detectedSystemIcon = nil - self.isSystemDetected = false - self.showUSBSection = false - self.showUnsupportedMessage = false - self.needsCodesign = true - self.isLegacyDetected = false - self.isRestoreLegacy = false - self.isCatalina = false - self.isSierra = false - self.isMavericks = false - self.isUnsupportedSierra = false - self.isPPC = false - self.legacyArchInfo = nil - self.userSkippedAnalysis = false - self.requiredUSBCapacityGB = nil - self.shouldShowAlreadyMountedSourceAlert = true - AppLogging.separator() - } else { - // Użyto String(localized:) aby ten ciąg został wykryty, mimo że jest przypisywany do zmiennej - self.recognizedVersion = String(localized: "Nie rozpoznano instalatora") - self.requiredUSBCapacityGB = nil - self.log("Analiza zakończona: nie rozpoznano instalatora.") - AppLogging.separator() - } - } - } - } - } - else if ext == "app" { - self.stage("Analiza aplikacji (.app) — start") - self.log("Analiza aplikacji (.app): odczyt Info.plist (CFBundleDisplayName, CFBundleShortVersionString) oraz wykrywanie wersji i trybu instalacji.") - self.log("Źródło pliku do odczytu wersji: \(url.path)") - DispatchQueue.global(qos: .userInitiated).async { - let result = self.readAppInfo(appUrl: url) - DispatchQueue.main.async { - withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) { - self.isAnalyzing = false - self.mountedDMGPath = nil - if let (name, rawVer, appURL) = result { - let friendlyVer = self.formatMarketingVersion(raw: rawVer, name: name) - var cleanName = name - cleanName = cleanName.replacingOccurrences(of: "Install ", with: "") - cleanName = cleanName.replacingOccurrences(of: "macOS ", with: "") - cleanName = cleanName.replacingOccurrences(of: "Mac OS X ", with: "") - cleanName = cleanName.replacingOccurrences(of: "OS X ", with: "") - let prefix = name.contains("macOS") ? "macOS" : (name.contains("OS X") ? "OS X" : "macOS") - - self.recognizedVersion = "\(prefix) \(cleanName) \(friendlyVer)" - self.updateRequiredUSBCapacity(rawVersion: rawVer, name: name) - self.sourceAppURL = appURL - self.updateDetectedSystemIcon(from: appURL) - - let nameLower = name.lowercased() - - // Leopard detection (PowerPC) - let isLeopard = nameLower.contains("leopard") || rawVer.starts(with: "10.5") - let isTiger = nameLower.contains("tiger") || rawVer.starts(with: "10.4") - let isPanther = nameLower.contains("panther") || rawVer.starts(with: "10.3") - let isSnowLeopard = nameLower.contains("snow leopard") || rawVer.starts(with: "10.6") - - self.legacyArchInfo = nil - - // Dla Snow Leoparda/Leoparda/Tigera zawsze traktujemy jako PPC flow - if isLeopard || isTiger || isSnowLeopard { - self.isPPC = true - } - - // Ustal dokładną wersję dla Panther/Tiger/Leopard/Snow Leopard (dla .app) - if isPanther || isTiger || isLeopard || isSnowLeopard { - let isExact = rawVer.starts(with: "10.3") || rawVer.starts(with: "10.4") || rawVer.starts(with: "10.5") || rawVer.starts(with: "10.6") - let exactSuffix = isExact ? " \(rawVer)" : "" - if isPanther { - self.logError("Wykryto niewspierany system: Mac OS X Panther (10.3). Przerywam analizę.") - self.recognizedVersion = "Mac OS X Panther\(exactSuffix)" - } else if isTiger { - self.recognizedVersion = "Mac OS X Tiger\(exactSuffix)" - } else if isLeopard { - self.recognizedVersion = "Mac OS X Leopard\(exactSuffix)" - } else if isSnowLeopard { - self.recognizedVersion = "Mac OS X Snow Leopard\(exactSuffix)" - } - } - // If Panther is detected, mark as unsupported and block further processing - if isPanther { - self.isSystemDetected = false - self.showUSBSection = false - self.isPPC = false - self.legacyArchInfo = nil - // Show unsupported message immediately - withAnimation(.spring(response: 0.7, dampingFraction: 0.8)) { - self.showUnsupportedMessage = true - } - self.needsCodesign = false - self.isLegacyDetected = false - self.isRestoreLegacy = false - self.isCatalina = false - self.isSierra = false - self.isMavericks = false - self.isUnsupportedSierra = false - return - } - - // Systemy niewspierane (Explicit) - USUNIĘTO CATALINĘ - let isExplicitlyUnsupported = nameLower.contains("sierra") && !nameLower.contains("high") - - // Catalina detection - let isCatalina = nameLower.contains("catalina") || rawVer.starts(with: "10.15") - - // Sierra detection (supported only for installer version 12.6.06) - let isSierra = (rawVer == "12.6.06") - let isSierraName = nameLower.contains("sierra") && !nameLower.contains("high") - let isUnsupportedSierraVersion = isSierraName && !isSierra - self.isUnsupportedSierra = isUnsupportedSierraVersion - if isUnsupportedSierraVersion { self.logError("Ta wersja systemu macOS Sierra nie jest wspierana (wymagana 12.6.06).") } - - let isMavericks = nameLower.contains("mavericks") || rawVer.starts(with: "10.9") - - // Modern (Big Sur+) - let isModern = - nameLower.contains("tahoe") || // Dodano Tahoe - nameLower.contains("sur") || - nameLower.contains("monterey") || - nameLower.contains("ventura") || - nameLower.contains("sonoma") || - nameLower.contains("sequoia") || - rawVer.starts(with: "21.") || // Dodano Tahoe (v26/21.x) - rawVer.starts(with: "11.") || - (rawVer.starts(with: "12.") && !isExplicitlyUnsupported) || - (rawVer.starts(with: "13.") && !nameLower.contains("high")) || - (rawVer.starts(with: "14.") && !nameLower.contains("mojave")) || - (rawVer.starts(with: "15.") && !isExplicitlyUnsupported) - - // Old Supported (Mojave + High Sierra) - let isOldSupported = - nameLower.contains("mojave") || - nameLower.contains("high sierra") || - rawVer.starts(with: "10.14") || - rawVer.starts(with: "10.13") || - (rawVer.starts(with: "14.") && nameLower.contains("mojave")) || - (rawVer.starts(with: "13.") && nameLower.contains("high")) - - // Legacy No Codesign (Yosemite + El Capitan) - let isLegacyDetected = - nameLower.contains("yosemite") || - nameLower.contains("el capitan") || - rawVer.starts(with: "10.10") || - rawVer.starts(with: "10.11") - - // Legacy Restore (Lion + Mountain Lion) - let isRestoreLegacy = - nameLower.contains("mountain lion") || - nameLower.contains("lion") || - rawVer.starts(with: "10.8") || - rawVer.starts(with: "10.7") - - self.isSystemDetected = isModern || isOldSupported || isLegacyDetected || isRestoreLegacy || isCatalina || isSierra - - self.needsCodesign = isOldSupported && !isModern && !isLegacyDetected - self.isLegacyDetected = isLegacyDetected - self.isRestoreLegacy = isRestoreLegacy - self.isCatalina = isCatalina - self.isSierra = isSierra - self.isMavericks = isMavericks - if isMavericks { - self.shouldShowMavericksDialog = true - } - if isSierra { - self.recognizedVersion = "macOS Sierra 10.12" - self.needsCodesign = false - } - // isPPC zostało ustawione wcześniej dla Snow Leoparda/Leoparda/Tigera; dla pozostałych pozostaje false - self.isPPC = self.isPPC || false - - let trueFlags = [ - self.isCatalina ? "isCatalina" : nil, - self.isSierra ? "isSierra" : nil, - self.isLegacyDetected ? "isLegacyDetected" : nil, - self.isRestoreLegacy ? "isRestoreLegacy" : nil, - self.isPPC ? "isPPC" : nil, - self.isUnsupportedSierra ? "isUnsupportedSierra" : nil, - self.isMavericks ? "isMavericks" : nil - ].compactMap { $0 }.joined(separator: ", ") - self.log("Analiza zakończona. Rozpoznano: \(self.recognizedVersion)") - self.log("Przypisane flagi: \(trueFlags.isEmpty ? "brak" : trueFlags)") - AppLogging.separator() - - if self.isSystemDetected { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { withAnimation(.spring(response: 0.7, dampingFraction: 0.8)) { self.showUSBSection = true } } - } else { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { withAnimation(.spring(response: 0.7, dampingFraction: 0.8)) { self.showUnsupportedMessage = true } } - } - } else { - self.recognizedVersion = String(localized: "Nie rozpoznano instalatora") - self.requiredUSBCapacityGB = nil - self.log("Analiza zakończona: nie rozpoznano instalatora.") - AppLogging.separator() - } - } - } - } - } - } - - func forceTigerMultiDVDSelection() { - self.log("Ręcznie wybrano tryb Tiger Multi DVD") - let fileURL = self.selectedFileUrl - DispatchQueue.global(qos: .userInitiated).async { - var mountPoint: String? = self.mountedDMGPath - var effectiveSourceAppURL: URL? = nil - if let url = fileURL { - let ext = url.pathExtension.lowercased() - if ext == "dmg" || ext == "iso" || ext == "cdr" { - if mountPoint == nil { - mountPoint = self.mountImageForPPC(dmgUrl: url) - } - if let mp = mountPoint { - effectiveSourceAppURL = URL(fileURLWithPath: mp).appendingPathComponent("Install") - } - } else if ext == "app" { - effectiveSourceAppURL = url - } - } - DispatchQueue.main.async { - withAnimation { - self.isAnalyzing = false - self.userSkippedAnalysis = true - self.recognizedVersion = "Mac OS X Tiger 10.4" - self.sourceAppURL = effectiveSourceAppURL - self.updateDetectedSystemIcon(from: effectiveSourceAppURL) - self.mountedDMGPath = mountPoint - self.isSystemDetected = true - self.showUnsupportedMessage = false - self.showUSBSection = true - self.needsCodesign = false - self.isLegacyDetected = false - self.isRestoreLegacy = false - self.isCatalina = false - self.isSierra = false - self.isMavericks = false - self.isUnsupportedSierra = false - self.isPPC = true - self.legacyArchInfo = nil - self.selectedDrive = nil - self.capacityCheckFinished = false - self.requiredUSBCapacityGB = 16 - } - let flags = [self.isPPC ? "isPPC" : nil].compactMap { $0 }.joined(separator: ", ") - self.log("Ustawiono Tiger Multi DVD: recognizedVersion=\(self.recognizedVersion). Flagi: \(flags.isEmpty ? "brak" : flags)") - } - } - } - - func formatMarketingVersion(raw: String, name: String) -> String { - let n = name.lowercased() - if n.contains("tahoe") { return "26" } // Dodano Tahoe - if n.contains("sequoia") { return "15" } - if n.contains("sonoma") { return "14" } - if n.contains("ventura") { return "13" } - if n.contains("monterey") { return "12" } - if n.contains("big sur") { return "11" } - if n.contains("catalina") { return "10.15" } - if n.contains("mojave") { return "10.14" } - if n.contains("high sierra") { return "10.13" } - if n.contains("sierra") && !n.contains("high") { return "10.12" } - if n.contains("el capitan") { return "10.11" } - if n.contains("yosemite") { return "10.10" } - if n.contains("mavericks") { return "10.9" } - if n.contains("mountain lion") { return "10.8" } - if n.contains("lion") { return "10.7" } - if n.contains("snow leopard") { return "10.6" } - if n.contains("panther") { return "10.3" } - return raw - } - - func readAppInfo(appUrl: URL) -> (String, String, URL)? { - let plistUrl = appUrl.appendingPathComponent("Contents/Info.plist") - self.log("Odczyt Info.plist: \(plistUrl.path)") - if let d = try? Data(contentsOf: plistUrl), - let dict = try? PropertyListSerialization.propertyList(from: d, format: nil) as? [String: Any] { - let name = (dict["CFBundleDisplayName"] as? String) ?? appUrl.lastPathComponent - let ver = (dict["CFBundleShortVersionString"] as? String) ?? "?" - self.log("Odczytano Info.plist: name=\(name), version=\(ver)") - return (name, ver, appUrl) - } - self.logError("Nie udało się odczytać Info.plist") - return nil - } - - private func updateDetectedSystemIcon(from appURL: URL?) { - guard let appURL = appURL else { - self.detectedSystemIcon = nil - return - } - - let iconFileCandidates = [ - "ProductPageIcon.icns", - "InstallAssistant.icns", - "Install Mac OS X.icns" - ] - - for installerURL in self.candidateInstallerLocations(from: appURL) { - let resourcesURL = installerURL.appendingPathComponent("Contents/Resources", isDirectory: true) - self.log("Próba odczytu ikony systemu z katalogu: \(resourcesURL.path)") - guard let iconURL = self.findIconURL(in: resourcesURL, preferredFileNames: iconFileCandidates), - let icon = NSImage(contentsOf: iconURL) else { - continue - } - - self.detectedSystemIcon = icon - self.log("Odczytano ikonę systemu z pliku: \(iconURL.path)") - return - } - - self.detectedSystemIcon = nil - self.log("Nie znaleziono ikony instalatora (\(iconFileCandidates.joined(separator: ", "))) dla: \(appURL.path)") - } - private func candidateInstallerLocations(from url: URL) -> [URL] { - var result: [URL] = [] - let fm = FileManager.default - - func appendUnique(_ candidate: URL) { - let normalized = candidate.standardizedFileURL - guard !result.contains(where: { $0.standardizedFileURL == normalized }) else { return } - result.append(candidate) - } - - appendUnique(url) - - if url.pathExtension.lowercased() != "app" { - // Część starych obrazów ma instalator pod klasyczną nazwą "Install Mac OS X". - appendUnique(url.appendingPathComponent("Install Mac OS X.app", isDirectory: true)) - appendUnique(url.appendingPathComponent("Install OS X.app", isDirectory: true)) - appendUnique(url.appendingPathComponent("Install macOS.app", isDirectory: true)) - - if let children = try? fm.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]) { - for child in children where child.pathExtension.lowercased() == "app" { - appendUnique(child) - } - } - } - - return result.filter { fm.fileExists(atPath: $0.path) } - } - - private func findIconURL(in resourcesURL: URL, preferredFileNames: [String]) -> URL? { - let fm = FileManager.default - guard fm.fileExists(atPath: resourcesURL.path), - let files = try? fm.contentsOfDirectory(at: resourcesURL, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]) else { - return nil - } - - var byLowercasedName: [String: URL] = [:] - for file in files { - byLowercasedName[file.lastPathComponent.lowercased()] = file - } - - for fileName in preferredFileNames { - if let match = byLowercasedName[fileName.lowercased()] { - return match - } - } - - return nil - } - - private func readLegacyInstallMacOSXInfo(from mountURL: URL) -> (String, String, URL)? { - let legacyInstallers = [ - "Install Mac OS X", - "Install Mac OS X.app" - ] - - var foundLegacyPath = false - for installerName in legacyInstallers { - let installerURL = mountURL.appendingPathComponent(installerName, isDirectory: true) - let plistURL = installerURL.appendingPathComponent("Contents/Info.plist") - guard FileManager.default.fileExists(atPath: plistURL.path) else { - continue - } - - foundLegacyPath = true - self.log("Znaleziono legacy installer path: \(installerURL.path)") - self.log("Odczyt Info.plist (legacy): \(plistURL.path)") - - guard let data = try? Data(contentsOf: plistURL), - let dict = try? PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any] else { - self.logError("Nie udało się odczytać Info.plist (legacy): \(plistURL.path)") - continue - } - - let name = (dict["CFBundleDisplayName"] as? String) ?? installerURL.lastPathComponent - let version = (dict["CFBundleShortVersionString"] as? String) ?? "?" - self.log("Odczytano Info.plist (legacy): name=\(name), version=\(version)") - return (name, version, installerURL) - } - - if !foundLegacyPath { - self.log("Nie znaleziono legacy path instalatora 'Install Mac OS X' w: \(mountURL.path)") - } - - return nil - } - - private func normalizedImagePath(_ path: String) -> String { - URL(fileURLWithPath: path).resolvingSymlinksInPath().standardizedFileURL.path - } - - private func mountedPathForAlreadyAttachedImage(sourceURL: URL) -> String? { - let task = Process() - task.executableURL = URL(fileURLWithPath: "/usr/bin/hdiutil") - task.arguments = ["info", "-plist"] - let outputPipe = Pipe() - let errorPipe = Pipe() - task.standardOutput = outputPipe - task.standardError = errorPipe - - do { - try task.run() - } catch { - self.logError("Nie udało się uruchomić hdiutil info: \(error.localizedDescription)") - return nil - } - task.waitUntilExit() - - let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile() - let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile() - if task.terminationStatus != 0 { - let stderrText = String(data: errorData, encoding: .utf8)? - .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - if stderrText.isEmpty { - self.logError("hdiutil info zakończył się błędem (kod \(task.terminationStatus)).") - } else { - self.logError("hdiutil info zakończył się błędem: \(stderrText)") - } - return nil - } - - guard let plist = try? PropertyListSerialization.propertyList(from: outputData, options: [], format: nil) as? [String: Any], - let images = plist["images"] as? [[String: Any]] else { - return nil - } - - let sourcePath = normalizedImagePath(sourceURL.path) - for image in images { - guard let imagePath = image["image-path"] as? String else { continue } - guard normalizedImagePath(imagePath) == sourcePath else { continue } - guard let entities = image["system-entities"] as? [[String: Any]], - let mountPoint = entities.compactMap({ $0["mount-point"] as? String }).first else { - continue - } - return mountPoint - } - - return nil - } - - private func mountAndReadInfo(dmgUrl: URL, detectPreMountedSource: Bool = false) -> ImageReadResult { - self.log("Montowanie obrazu (DMG/ISO/CDR)") - if detectPreMountedSource, - let mountPoint = mountedPathForAlreadyAttachedImage(sourceURL: dmgUrl) { - self.log("Wybrany obraz .\(dmgUrl.pathExtension.lowercased()) jest już zamontowany w systemie: \(mountPoint)") - return .sourceAlreadyMounted(mountPath: mountPoint) - } - - let task = Process(); task.executableURL = URL(fileURLWithPath: "/usr/bin/hdiutil") - task.arguments = ["attach", dmgUrl.path, "-plist", "-nobrowse", "-readonly"] - let pipe = Pipe() - let errorPipe = Pipe() - task.standardOutput = pipe - task.standardError = errorPipe - - do { - try task.run() - } catch { - self.logError("Nie udało się uruchomić hdiutil attach: \(error.localizedDescription)") - if detectPreMountedSource, - let mountPoint = mountedPathForAlreadyAttachedImage(sourceURL: dmgUrl) { - self.log("Po błędzie uruchomienia attach wykryto już zamontowany obraz źródłowy: \(mountPoint)") - return .sourceAlreadyMounted(mountPath: mountPoint) - } - return .failure - } - task.waitUntilExit() - - let data = pipe.fileHandleForReading.readDataToEndOfFile() - let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile() - let stderrText = String(data: errorData, encoding: .utf8)? - .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - if task.terminationStatus != 0 { - if stderrText.isEmpty { - self.logError("hdiutil attach zakończył się błędem (kod \(task.terminationStatus)).") - } else { - self.logError("hdiutil attach zakończył się błędem: \(stderrText)") - } - if detectPreMountedSource, - let mountPoint = mountedPathForAlreadyAttachedImage(sourceURL: dmgUrl) { - self.log("Po błędzie attach wykryto już zamontowany obraz źródłowy: \(mountPoint)") - return .sourceAlreadyMounted(mountPath: mountPoint) - } - return .failure - } - - guard let plist = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any], let entities = plist["system-entities"] as? [[String: Any]] else { - self.logError("Nie udało się odczytać informacji z obrazu") - if detectPreMountedSource, - let mountPoint = mountedPathForAlreadyAttachedImage(sourceURL: dmgUrl) { - self.log("Po nieudanym odczycie plist wykryto już zamontowany obraz źródłowy: \(mountPoint)") - return .sourceAlreadyMounted(mountPath: mountPoint) - } - return .failure - } - self.log("Przetwarzanie wyników hdiutil attach (\(entities.count) encji)") - for e in entities { - if let mp = e["mount-point"] as? String { - let devEntry = (e["dev-entry"] as? String) ?? (e["devname"] as? String) - var mountId = "unknown" - if let dev = devEntry { - let bsd = URL(fileURLWithPath: dev).lastPathComponent // e.g. disk9s1 - if let range = bsd.range(of: #"s\d+$"#, options: .regularExpression) { - mountId = String(bsd[.. String? { - self.log("Montowanie obrazu (PPC)") - let task = Process(); task.executableURL = URL(fileURLWithPath: "/usr/bin/hdiutil") - task.arguments = ["attach", dmgUrl.path, "-plist", "-nobrowse", "-readonly"] - let pipe = Pipe(); task.standardOutput = pipe; try? task.run(); task.waitUntilExit() - let data = pipe.fileHandleForReading.readDataToEndOfFile() - guard let plist = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any], let entities = plist["system-entities"] as? [[String: Any]] else { - self.logError("Nie udało się zamontować obrazu (PPC)") - return nil - } - for e in entities { - if let mp = e["mount-point"] as? String { - let devEntry = (e["dev-entry"] as? String) ?? (e["devname"] as? String) - var mountId = "unknown" - if let dev = devEntry { - let bsd = URL(fileURLWithPath: dev).lastPathComponent - if let range = bsd.range(of: #"s\d+$"#, options: .regularExpression) { - mountId = String(bsd[..= minCapacity; capacityCheckFinished = true } - } else { isCapacitySufficient = false; capacityCheckFinished = true } - } - - func resetAll() { - let oldMount = self.mountedDMGPath - if let path = oldMount { - let task = Process() - task.launchPath = "/usr/bin/hdiutil" - task.arguments = ["detach", path, "-force"] - try? task.run() - task.waitUntilExit() - } - DispatchQueue.main.async { - withAnimation { - self.selectedFilePath = "" - self.selectedFileUrl = nil - self.recognizedVersion = "" - self.sourceAppURL = nil - self.detectedSystemIcon = nil - self.mountedDMGPath = nil - - self.isAnalyzing = false - self.isSystemDetected = false - self.showUSBSection = false - self.showUnsupportedMessage = false - - self.needsCodesign = true - self.isLegacyDetected = false - self.isRestoreLegacy = false - self.isCatalina = false - self.isSierra = false - self.isMavericks = false - self.isUnsupportedSierra = false - self.isPPC = false - self.legacyArchInfo = nil - self.shouldShowAlreadyMountedSourceAlert = false - self.userSkippedAnalysis = false - self.shouldShowMavericksDialog = false - self.requiredUSBCapacityGB = nil - - self.availableDrives = [] - self.selectedDrive = nil - - self.isCapacitySufficient = false - self.capacityCheckFinished = false - } - } - } - - // Call this from the UI when the user presses the "Przejdź dalej" button - func recordProceedPressed() { - self.log("Użytkownik nacisnął przycisk 'Przejdź dalej'. Wybrany nośnik: \(self.selectedDrive?.url.path ?? "brak"), źródło: \(self.sourceAppURL?.path ?? "brak"), rozpoznano: \(self.recognizedVersion)") - } -} - -@MainActor -final class AnalysisSelectionHandoff { - static let shared = AnalysisSelectionHandoff() - - private var pendingInstallerURL: URL? - - private init() {} - - func setPendingInstallerURL(_ url: URL) { - pendingInstallerURL = url.standardizedFileURL + func logError(_ message: String, category: String = "FileAnalysis") { + AppLogging.error(message, category: category) } - func consumePendingInstallerURL() -> URL? { - defer { pendingInstallerURL = nil } - return pendingInstallerURL + func stage(_ title: String) { + AppLogging.stage(title) } } - -extension Notification.Name { - static let macUSBResetToStart = Notification.Name("macUSB.resetToStart") - static let macUSBStartTigerMultiDVD = Notification.Name("macUSB.startTigerMultiDVD") - static let macUSBDebugGoToBigSurSummary = Notification.Name("macUSB.debugGoToBigSurSummary") - static let macUSBDebugGoToTigerSummary = Notification.Name("macUSB.debugGoToTigerSummary") - static let macUSBNavigateToAnalysis = Notification.Name("macUSB.navigateToAnalysis") - static let macUSBApplyPendingDownloaderInstaller = Notification.Name("macUSB.applyPendingDownloaderInstaller") -} diff --git a/macUSB/Features/Analysis/AnalysisNotifications.swift b/macUSB/Features/Analysis/AnalysisNotifications.swift new file mode 100644 index 0000000..18bffc7 --- /dev/null +++ b/macUSB/Features/Analysis/AnalysisNotifications.swift @@ -0,0 +1,10 @@ +import Foundation + +extension Notification.Name { + static let macUSBResetToStart = Notification.Name("macUSB.resetToStart") + static let macUSBStartTigerMultiDVD = Notification.Name("macUSB.startTigerMultiDVD") + static let macUSBDebugGoToBigSurSummary = Notification.Name("macUSB.debugGoToBigSurSummary") + static let macUSBDebugGoToTigerSummary = Notification.Name("macUSB.debugGoToTigerSummary") + static let macUSBNavigateToAnalysis = Notification.Name("macUSB.navigateToAnalysis") + static let macUSBApplyPendingDownloaderInstaller = Notification.Name("macUSB.applyPendingDownloaderInstaller") +} diff --git a/macUSB/Features/Analysis/AnalysisSelectionHandoff.swift b/macUSB/Features/Analysis/AnalysisSelectionHandoff.swift new file mode 100644 index 0000000..423914d --- /dev/null +++ b/macUSB/Features/Analysis/AnalysisSelectionHandoff.swift @@ -0,0 +1,19 @@ +import Foundation + +@MainActor +final class AnalysisSelectionHandoff { + static let shared = AnalysisSelectionHandoff() + + private var pendingInstallerURL: URL? + + private init() {} + + func setPendingInstallerURL(_ url: URL) { + pendingInstallerURL = url.standardizedFileURL + } + + func consumePendingInstallerURL() -> URL? { + defer { pendingInstallerURL = nil } + return pendingInstallerURL + } +} diff --git a/macUSB/Features/Analysis/Logic/AnalysisLogicAnalysisFlow.swift b/macUSB/Features/Analysis/Logic/AnalysisLogicAnalysisFlow.swift new file mode 100644 index 0000000..fb0c550 --- /dev/null +++ b/macUSB/Features/Analysis/Logic/AnalysisLogicAnalysisFlow.swift @@ -0,0 +1,139 @@ +import SwiftUI +import Foundation + +extension AnalysisLogic { + func startAnalysis() { + guard let url = selectedFileUrl else { return } + self.stage("Analiza pliku — start") + self.log("Rozpoczynam analizę pliku") + self.log("Źródło pliku do odczytu wersji: \(url.path)") + withAnimation { isAnalyzing = true } + detectedSystemIcon = nil + selectedDrive = nil; capacityCheckFinished = false + showUSBSection = false; showUnsupportedMessage = false + isUnsupportedSierra = false + isPPC = false + isMavericks = false + shouldShowAlreadyMountedSourceAlert = false + requiredUSBCapacityGB = nil + + let ext = url.pathExtension.lowercased() + self.log("Wykryto rozszerzenie: \(ext)") + if ext == "dmg" || ext == "iso" || ext == "cdr" { + self.stage("Analiza obrazu (DMG/ISO/CDR) — start") + self.log("Analiza obrazu (DMG/ISO/CDR): montowanie obrazu przez hdiutil (attach -plist -nobrowse -readonly), odczyt Info.plist z aplikacji oraz wykrywanie wersji i trybu instalacji.") + let oldMountPath = self.mountedDMGPath + DispatchQueue.global(qos: .userInitiated).async { + if let path = oldMountPath { + let task = Process(); task.executableURL = URL(fileURLWithPath: "/usr/bin/hdiutil"); task.arguments = ["detach", path, "-force"]; try? task.run(); task.waitUntilExit() + } + let shouldDetectAlreadyMountedSource = (ext == "cdr" || ext == "iso") + let result = self.mountAndReadInfo(dmgUrl: url, detectPreMountedSource: shouldDetectAlreadyMountedSource) + DispatchQueue.main.async { + withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) { + self.isAnalyzing = false + let mountedReadInfo = result?.mountedReadInfo + let sourceAlreadyMountedPath = result?.sourceAlreadyMountedPath + let sourceAlreadyMounted = sourceAlreadyMountedPath != nil + if let mountPath = sourceAlreadyMountedPath { + self.log("Wykryto, że wybrany obraz źródłowy jest już zamontowany: \(mountPath)") + } + if let (_, _, _, mp) = mountedReadInfo { self.mountedDMGPath = mp } else { self.mountedDMGPath = nil } + if let (name, rawVer, appURL, _) = mountedReadInfo { + let friendlyVer = self.formatMarketingVersion(raw: rawVer, name: name) + var cleanName = name + cleanName = cleanName.replacingOccurrences(of: "Install ", with: "") + cleanName = cleanName.replacingOccurrences(of: "macOS ", with: "") + cleanName = cleanName.replacingOccurrences(of: "Mac OS X ", with: "") + cleanName = cleanName.replacingOccurrences(of: "OS X ", with: "") + let prefix = name.contains("macOS") ? "macOS" : (name.contains("OS X") ? "OS X" : "macOS") + + self.recognizedVersion = "\(prefix) \(cleanName) \(friendlyVer)" + self.updateRequiredUSBCapacity(rawVersion: rawVer, name: name) + self.sourceAppURL = appURL + self.updateDetectedSystemIcon(from: appURL) + + // Try to read ProductUserVisibleVersion from mounted image (Tiger/Leopard) + var userVisibleVersionFromMounted: String? = nil + if let mountPath = self.mountedDMGPath { + let sysVerPlist = URL(fileURLWithPath: mountPath).appendingPathComponent("System/Library/CoreServices/SystemVersion.plist") + if let data = try? Data(contentsOf: sysVerPlist), + let dict = try? PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any], + let userVisible = dict["ProductUserVisibleVersion"] as? String { + userVisibleVersionFromMounted = userVisible + } + } + + if !self.applyMacosCompatibilityForMountedInstaller(name: name, rawVer: rawVer, userVisibleVersionFromMounted: userVisibleVersionFromMounted) { + return + } + } else if sourceAlreadyMounted { + self.recognizedVersion = "" + self.sourceAppURL = nil + self.detectedSystemIcon = nil + self.isSystemDetected = false + self.showUSBSection = false + self.showUnsupportedMessage = false + self.needsCodesign = true + self.isLegacyDetected = false + self.isRestoreLegacy = false + self.isCatalina = false + self.isSierra = false + self.isMavericks = false + self.isUnsupportedSierra = false + self.isPPC = false + self.legacyArchInfo = nil + self.userSkippedAnalysis = false + self.requiredUSBCapacityGB = nil + self.shouldShowAlreadyMountedSourceAlert = true + AppLogging.separator() + } else { + // Użyto String(localized:) aby ten ciąg został wykryty, mimo że jest przypisywany do zmiennej + self.recognizedVersion = String(localized: "Nie rozpoznano instalatora") + self.requiredUSBCapacityGB = nil + self.log("Analiza zakończona: nie rozpoznano instalatora.") + AppLogging.separator() + } + } + } + } + } + else if ext == "app" { + self.stage("Analiza aplikacji (.app) — start") + self.log("Analiza aplikacji (.app): odczyt Info.plist (CFBundleDisplayName, CFBundleShortVersionString) oraz wykrywanie wersji i trybu instalacji.") + self.log("Źródło pliku do odczytu wersji: \(url.path)") + DispatchQueue.global(qos: .userInitiated).async { + let result = self.readAppInfo(appUrl: url) + DispatchQueue.main.async { + withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) { + self.isAnalyzing = false + self.mountedDMGPath = nil + if let (name, rawVer, appURL) = result { + let friendlyVer = self.formatMarketingVersion(raw: rawVer, name: name) + var cleanName = name + cleanName = cleanName.replacingOccurrences(of: "Install ", with: "") + cleanName = cleanName.replacingOccurrences(of: "macOS ", with: "") + cleanName = cleanName.replacingOccurrences(of: "Mac OS X ", with: "") + cleanName = cleanName.replacingOccurrences(of: "OS X ", with: "") + let prefix = name.contains("macOS") ? "macOS" : (name.contains("OS X") ? "OS X" : "macOS") + + self.recognizedVersion = "\(prefix) \(cleanName) \(friendlyVer)" + self.updateRequiredUSBCapacity(rawVersion: rawVer, name: name) + self.sourceAppURL = appURL + self.updateDetectedSystemIcon(from: appURL) + + if !self.applyMacosCompatibilityForAppInstaller(name: name, rawVer: rawVer) { + return + } + } else { + self.recognizedVersion = String(localized: "Nie rozpoznano instalatora") + self.requiredUSBCapacityGB = nil + self.log("Analiza zakończona: nie rozpoznano instalatora.") + AppLogging.separator() + } + } + } + } + } + } +} diff --git a/macUSB/Features/Analysis/Logic/AnalysisLogicFileSelection.swift b/macUSB/Features/Analysis/Logic/AnalysisLogicFileSelection.swift new file mode 100644 index 0000000..48a44fc --- /dev/null +++ b/macUSB/Features/Analysis/Logic/AnalysisLogicFileSelection.swift @@ -0,0 +1,113 @@ +import SwiftUI +import AppKit +import UniformTypeIdentifiers + +extension AnalysisLogic { + func handleDrop(providers: [NSItemProvider]) -> Bool { + self.log("Odebrano przeciągnięcie pliku (providers=\(providers.count)). Szukam URL...") + if let provider = providers.first(where: { $0.hasItemConformingToTypeIdentifier(UTType.fileURL.identifier) }) { + provider.loadItem(forTypeIdentifier: UTType.fileURL.identifier, options: nil) { (item, _) in + if let data = item as? Data, let url = URL(dataRepresentation: data, relativeTo: nil) { + self.log("Przeciągnięto plik w formacie .\(url.pathExtension.lowercased())") + let ext = url.pathExtension.lowercased() + if ext == "dmg" || ext == "app" || ext == "iso" || ext == "cdr" { + self.processDroppedURL(url) + } + } + else if let url = item as? URL { + self.log("Przeciągnięto plik w formacie .\(url.pathExtension.lowercased())") + let ext = url.pathExtension.lowercased() + if ext == "dmg" || ext == "app" || ext == "iso" || ext == "cdr" { + self.processDroppedURL(url) + } + } + } + return true + } + return false + } + + func processDroppedURL(_ url: URL) { + DispatchQueue.main.async { + let ext = url.pathExtension.lowercased() + self.log("Wybrano plik w formacie .\(ext). Resetuję stan i przygotowuję analizę.") + if ext == "dmg" || ext == "app" || ext == "iso" || ext == "cdr" { + withAnimation { + self.selectedFilePath = url.path + self.selectedFileUrl = url + self.recognizedVersion = "" + self.isSystemDetected = false + self.sourceAppURL = nil + self.detectedSystemIcon = nil + self.selectedDrive = nil + self.capacityCheckFinished = false + self.showUSBSection = false + self.showUnsupportedMessage = false + self.isSierra = false + self.isMavericks = false + self.isUnsupportedSierra = false + self.isPPC = false + self.legacyArchInfo = nil + self.userSkippedAnalysis = false + self.shouldShowMavericksDialog = false + self.requiredUSBCapacityGB = nil + } + self.log("Lokalizacja wybranego pliku: \(url.path)") + self.log("Źródło do rozpoznania wersji: \(url.path)") + } + } + } + + func applySelectedURLAndStartAnalysis(_ url: URL) { + let ext = url.pathExtension.lowercased() + guard ext == "dmg" || ext == "app" || ext == "iso" || ext == "cdr" else { + logError("Pominięto automatyczne podstawienie ścieżki. Nieobsługiwany format: .\(ext)") + return + } + + processDroppedURL(url) + DispatchQueue.main.async { [weak self] in + self?.startAnalysis() + } + } + + func selectDMGFile() { + self.log("Otwieram panel wyboru pliku…") + let p = NSOpenPanel() + p.allowedContentTypes = [.diskImage, .applicationBundle] + // Dodajemy obsługę .iso i .cdr, które nie mają jeszcze UTType w UniformTypeIdentifiers, więc rozszerzamy allowedFileTypes + p.allowedFileTypes = ["dmg", "iso", "cdr", "app"] + p.allowsMultipleSelection = false + p.begin { + if $0 == .OK, let url = p.url { + let ext = url.pathExtension.lowercased() + guard ext == "dmg" || ext == "iso" || ext == "cdr" || ext == "app" else { return } + withAnimation { + self.selectedFilePath = url.path + self.selectedFileUrl = url + self.recognizedVersion = "" + self.isSystemDetected = false + self.sourceAppURL = nil + self.detectedSystemIcon = nil + self.selectedDrive = nil + self.capacityCheckFinished = false + self.showUSBSection = false + self.showUnsupportedMessage = false + self.isSierra = false + self.isMavericks = false + self.isUnsupportedSierra = false + self.isPPC = false + self.legacyArchInfo = nil + self.userSkippedAnalysis = false + self.shouldShowMavericksDialog = false + self.requiredUSBCapacityGB = nil + } + self.log("Wybrano plik w formacie .\(ext)") + self.log("Lokalizacja wybranego pliku: \(url.path)") + self.log("Źródło do rozpoznania wersji: \(url.path)") + } else { + self.log("Anulowano wybór pliku") + } + } + } +} diff --git a/macUSB/Features/Analysis/Logic/AnalysisLogicUsbDrives.swift b/macUSB/Features/Analysis/Logic/AnalysisLogicUsbDrives.swift new file mode 100644 index 0000000..fd739cc --- /dev/null +++ b/macUSB/Features/Analysis/Logic/AnalysisLogicUsbDrives.swift @@ -0,0 +1,99 @@ +import SwiftUI +import Foundation + +extension AnalysisLogic { + private var requiredUSBCapacityBytes: Int? { + guard let requiredGB = requiredUSBCapacityGB else { return nil } + switch requiredGB { + case 16: + return 15_000_000_000 + case 32: + return 28_000_000_000 + default: + return requiredGB * 1_000_000_000 + } + } + + // MARK: - Helper to enumerate external hard drives (non-removable) + private func enumerateExternalUSBHardDrives() -> [USBDrive] { + let keys: [URLResourceKey] = [ + .volumeNameKey, + .volumeIsRemovableKey, + .volumeIsInternalKey, + .volumeTotalCapacityKey + ] + guard let urls = FileManager.default.mountedVolumeURLs(includingResourceValuesForKeys: keys, options: .skipHiddenVolumes) else { return [] } + + let candidates: [USBDrive] = urls.compactMap { url -> USBDrive? in + guard let v = try? url.resourceValues(forKeys: Set(keys)) else { return nil } + // Only external (non-internal), non-network, non-removable volumes (HDD/SSD) + if (v.volumeIsInternal ?? true) { return nil } + // Filter out obvious network-mounted volumes by scheme (e.g., afp, smb, nfs) + let scheme = url.scheme?.lowercased() + if let scheme = scheme, ["afp", "smb", "nfs", "ftp", "webdav"].contains(scheme) { return nil } + if (v.volumeIsRemovable ?? false) { return nil } + guard let name = v.volumeName else { return nil } + let bsd = USBDriveLogic.getBSDName(from: url) + guard !bsd.isEmpty && bsd != "unknown" else { return nil } + let totalCapacity = Int64(v.volumeTotalCapacity ?? 0) + let size = ByteCountFormatter.string(fromByteCount: totalCapacity, countStyle: .file) + let whole = USBDriveLogic.wholeDiskName(from: bsd) + let speed = USBDriveLogic.detectUSBSpeed(forBSDName: whole) + let partitionScheme = USBDriveLogic.detectPartitionScheme(forBSDName: whole) + let fileSystemFormat = USBDriveLogic.detectFileSystemFormat(forVolumeURL: url) + return USBDrive( + name: name, + device: bsd, + size: size, + url: url, + usbSpeed: speed, + partitionScheme: partitionScheme, + fileSystemFormat: fileSystemFormat + ) + } + return candidates + } + + func refreshDrives() { + let currentSelectedURL = selectedDrive?.url + var foundDrives = USBDriveLogic.enumerateAvailableDrives() + let allowExternal = UserDefaults.standard.bool(forKey: "AllowExternalDrives") + if allowExternal { + let extra = enumerateExternalUSBHardDrives() + // Merge unique by URL + for d in extra { + if !foundDrives.contains(where: { $0.url == d.url }) { + foundDrives.append(d) + } + } + } + self.availableDrives = foundDrives + if let currentURL = currentSelectedURL { + if let stillConnectedDrive = foundDrives.first(where: { $0.url == currentURL }) { + self.selectedDrive = stillConnectedDrive + } else { + self.selectedDrive = nil + self.capacityCheckFinished = false + } + } else { + if self.selectedDrive != nil { + self.selectedDrive = nil + self.capacityCheckFinished = false + } + } + } + + func checkCapacity() { + guard let drive = selectedDrive, let minCapacity = requiredUSBCapacityBytes else { + isCapacitySufficient = false + capacityCheckFinished = false + return + } + if let values = try? drive.url.resourceValues(forKeys: [.volumeTotalCapacityKey]), let capacity = values.volumeTotalCapacity { + withAnimation { isCapacitySufficient = capacity >= minCapacity; capacityCheckFinished = true } + } else { + isCapacitySufficient = false + capacityCheckFinished = true + } + } +} diff --git a/macUSB/Features/Analysis/Logic/macOS/AnalysisLogicMacOSCompatibility.swift b/macUSB/Features/Analysis/Logic/macOS/AnalysisLogicMacOSCompatibility.swift new file mode 100644 index 0000000..cc0c8af --- /dev/null +++ b/macUSB/Features/Analysis/Logic/macOS/AnalysisLogicMacOSCompatibility.swift @@ -0,0 +1,319 @@ +import SwiftUI +import Foundation + +extension AnalysisLogic { + func applyMacosCompatibilityForMountedInstaller(name: String, rawVer: String, userVisibleVersionFromMounted: String?) -> Bool { + // Use lowercase name for detection + let nameLower = name.lowercased() + + // Leopard/Tiger detection (PowerPC) using name, raw version, or mounted SystemVersion.plist + let isLeopard = nameLower.contains("leopard") || rawVer.starts(with: "10.5") || (userVisibleVersionFromMounted?.hasPrefix("10.5") ?? false) + let isTiger = nameLower.contains("tiger") || rawVer.starts(with: "10.4") || (userVisibleVersionFromMounted?.hasPrefix("10.4") ?? false) + let isPanther = nameLower.contains("panther") || rawVer.starts(with: "10.3") || (userVisibleVersionFromMounted?.hasPrefix("10.3") ?? false) + let isSnowLeopard = nameLower.contains("snow leopard") || rawVer.starts(with: "10.6") || (userVisibleVersionFromMounted?.hasPrefix("10.6") ?? false) + + // Disable kernel arch detection for Panther (10.3), Tiger (10.4), Leopard (10.5) and Snow Leopard (10.6). Always mark PPC flow for legacy, but do not set legacyArchInfo. + if (isLeopard || isTiger || isPanther || isSnowLeopard) { + self.isPPC = true // niezależnie od architektury, proces USB taki sam + self.legacyArchInfo = nil + } + + // Legacy versions exact recognition for mounted userVisibleVersion or fallback for legacy systems + if isLeopard { + if let userVisible = userVisibleVersionFromMounted { + self.recognizedVersion = "Mac OS X Leopard \(userVisible)" + } else if rawVer.starts(with: "10.5") { + self.recognizedVersion = "Mac OS X Leopard \(rawVer)" + } else { + self.recognizedVersion = "Mac OS X Leopard" + } + } + if isTiger { + if let userVisible = userVisibleVersionFromMounted { + self.recognizedVersion = "Mac OS X Tiger \(userVisible)" + } else if rawVer.starts(with: "10.4") { + self.recognizedVersion = "Mac OS X Tiger \(rawVer)" + } else { + self.recognizedVersion = "Mac OS X Tiger" + } + } + if isPanther { + self.logError("Wykryto niewspierany system: Mac OS X Panther (10.3). Przerywam analizę.") + if let userVisible = userVisibleVersionFromMounted { + self.recognizedVersion = "Mac OS X Panther \(userVisible)" + } else if rawVer.starts(with: "10.3") { + self.recognizedVersion = "Mac OS X Panther \(rawVer)" + } else { + self.recognizedVersion = "Mac OS X Panther" + } + } + if isSnowLeopard { + if let userVisible = userVisibleVersionFromMounted { + self.recognizedVersion = "Mac OS X Snow Leopard \(userVisible)" + } else if rawVer.starts(with: "10.6") { + self.recognizedVersion = "Mac OS X Snow Leopard \(rawVer)" + } else { + self.recognizedVersion = "Mac OS X Snow Leopard" + } + } + + // If Panther is detected, mark as unsupported and block further processing + if isPanther { + self.isSystemDetected = false + self.showUSBSection = false + self.isPPC = false + self.legacyArchInfo = nil + // Show unsupported message immediately + withAnimation(.spring(response: 0.7, dampingFraction: 0.8)) { + self.showUnsupportedMessage = true + } + self.needsCodesign = false + self.isLegacyDetected = false + self.isRestoreLegacy = false + self.isCatalina = false + self.isSierra = false + self.isMavericks = false + self.isUnsupportedSierra = false + return false + } + + // Systemy niewspierane (Explicit) - USUNIĘTO CATALINĘ + let isExplicitlyUnsupported = nameLower.contains("sierra") && !nameLower.contains("high") + + // Catalina detection + let isCatalina = nameLower.contains("catalina") || rawVer.starts(with: "10.15") + + // Sierra detection (supported only for installer version 12.6.06) + let isSierra = (rawVer == "12.6.06") + let isSierraName = nameLower.contains("sierra") && !nameLower.contains("high") + let isUnsupportedSierraVersion = isSierraName && !isSierra + self.isUnsupportedSierra = isUnsupportedSierraVersion + if isUnsupportedSierraVersion { self.logError("Ta wersja systemu macOS Sierra nie jest wspierana (wymagana 12.6.06).") } + + let isMavericks = nameLower.contains("mavericks") || rawVer.starts(with: "10.9") + + // Modern (Big Sur+) + let isModern = + nameLower.contains("tahoe") || // Dodano Tahoe + nameLower.contains("sur") || + nameLower.contains("monterey") || + nameLower.contains("ventura") || + nameLower.contains("sonoma") || + nameLower.contains("sequoia") || + rawVer.starts(with: "21.") || // Dodano Tahoe (v26/21.x) + rawVer.starts(with: "11.") || + (rawVer.starts(with: "12.") && !isExplicitlyUnsupported) || + (rawVer.starts(with: "13.") && !nameLower.contains("high")) || + (rawVer.starts(with: "14.") && !nameLower.contains("mojave")) || + (rawVer.starts(with: "15.") && !isExplicitlyUnsupported) + + // Old Supported (Mojave + High Sierra) + let isOldSupported = + nameLower.contains("mojave") || + nameLower.contains("high sierra") || + rawVer.starts(with: "10.14") || + rawVer.starts(with: "10.13") || + (rawVer.starts(with: "14.") && nameLower.contains("mojave")) || + (rawVer.starts(with: "13.") && nameLower.contains("high")) + + // Legacy No Codesign (Yosemite + El Capitan) + let isLegacyDetected = + nameLower.contains("yosemite") || + nameLower.contains("el capitan") || + rawVer.starts(with: "10.10") || + rawVer.starts(with: "10.11") + + // Legacy Restore (Lion + Mountain Lion) + let isRestoreLegacy = + nameLower.contains("mountain lion") || + nameLower.contains("lion") || + rawVer.starts(with: "10.8") || + rawVer.starts(with: "10.7") + + // ZMIANA: Dodanie isCatalina do isSystemDetected + self.isSystemDetected = isModern || isOldSupported || isLegacyDetected || isRestoreLegacy || isCatalina || isSierra || isMavericks + + // Catalina ma swój własny codesign, więc tu wyłączamy standardowy 'needsCodesign' + self.needsCodesign = isOldSupported && !isModern && !isLegacyDetected + self.isLegacyDetected = isLegacyDetected + self.isRestoreLegacy = isRestoreLegacy + self.isCatalina = isCatalina + self.isSierra = isSierra + self.isMavericks = isMavericks + if isMavericks { + self.shouldShowMavericksDialog = true + } + if isSierra { + self.recognizedVersion = "macOS Sierra 10.12" + self.needsCodesign = false + } + // Dla Leoparda/Tigera już ustawione na true powyżej, pozostaw + self.isPPC = self.isPPC || false + + let trueFlags = [ + self.isCatalina ? "isCatalina" : nil, + self.isSierra ? "isSierra" : nil, + self.isLegacyDetected ? "isLegacyDetected" : nil, + self.isRestoreLegacy ? "isRestoreLegacy" : nil, + self.isPPC ? "isPPC" : nil, + self.isUnsupportedSierra ? "isUnsupportedSierra" : nil, + self.isMavericks ? "isMavericks" : nil + ].compactMap { $0 }.joined(separator: ", ") + self.log("Analiza zakończona. Rozpoznano: \(self.recognizedVersion)") + self.log("Przypisane flagi: \(trueFlags.isEmpty ? "brak" : trueFlags)") + AppLogging.separator() + + if self.isSystemDetected { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { withAnimation(.spring(response: 0.7, dampingFraction: 0.8)) { self.showUSBSection = true } } + } else { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { withAnimation(.spring(response: 0.7, dampingFraction: 0.8)) { self.showUnsupportedMessage = true } } + } + + return true + } + + func applyMacosCompatibilityForAppInstaller(name: String, rawVer: String) -> Bool { + let nameLower = name.lowercased() + + // Leopard detection (PowerPC) + let isLeopard = nameLower.contains("leopard") || rawVer.starts(with: "10.5") + let isTiger = nameLower.contains("tiger") || rawVer.starts(with: "10.4") + let isPanther = nameLower.contains("panther") || rawVer.starts(with: "10.3") + let isSnowLeopard = nameLower.contains("snow leopard") || rawVer.starts(with: "10.6") + + self.legacyArchInfo = nil + + // Dla Snow Leoparda/Leoparda/Tigera zawsze traktujemy jako PPC flow + if isLeopard || isTiger || isSnowLeopard { + self.isPPC = true + } + + // Ustal dokładną wersję dla Panther/Tiger/Leopard/Snow Leopard (dla .app) + if isPanther || isTiger || isLeopard || isSnowLeopard { + let isExact = rawVer.starts(with: "10.3") || rawVer.starts(with: "10.4") || rawVer.starts(with: "10.5") || rawVer.starts(with: "10.6") + let exactSuffix = isExact ? " \(rawVer)" : "" + if isPanther { + self.logError("Wykryto niewspierany system: Mac OS X Panther (10.3). Przerywam analizę.") + self.recognizedVersion = "Mac OS X Panther\(exactSuffix)" + } else if isTiger { + self.recognizedVersion = "Mac OS X Tiger\(exactSuffix)" + } else if isLeopard { + self.recognizedVersion = "Mac OS X Leopard\(exactSuffix)" + } else if isSnowLeopard { + self.recognizedVersion = "Mac OS X Snow Leopard\(exactSuffix)" + } + } + + // If Panther is detected, mark as unsupported and block further processing + if isPanther { + self.isSystemDetected = false + self.showUSBSection = false + self.isPPC = false + self.legacyArchInfo = nil + // Show unsupported message immediately + withAnimation(.spring(response: 0.7, dampingFraction: 0.8)) { + self.showUnsupportedMessage = true + } + self.needsCodesign = false + self.isLegacyDetected = false + self.isRestoreLegacy = false + self.isCatalina = false + self.isSierra = false + self.isMavericks = false + self.isUnsupportedSierra = false + return false + } + + // Systemy niewspierane (Explicit) - USUNIĘTO CATALINĘ + let isExplicitlyUnsupported = nameLower.contains("sierra") && !nameLower.contains("high") + + // Catalina detection + let isCatalina = nameLower.contains("catalina") || rawVer.starts(with: "10.15") + + // Sierra detection (supported only for installer version 12.6.06) + let isSierra = (rawVer == "12.6.06") + let isSierraName = nameLower.contains("sierra") && !nameLower.contains("high") + let isUnsupportedSierraVersion = isSierraName && !isSierra + self.isUnsupportedSierra = isUnsupportedSierraVersion + if isUnsupportedSierraVersion { self.logError("Ta wersja systemu macOS Sierra nie jest wspierana (wymagana 12.6.06).") } + + let isMavericks = nameLower.contains("mavericks") || rawVer.starts(with: "10.9") + + // Modern (Big Sur+) + let isModern = + nameLower.contains("tahoe") || // Dodano Tahoe + nameLower.contains("sur") || + nameLower.contains("monterey") || + nameLower.contains("ventura") || + nameLower.contains("sonoma") || + nameLower.contains("sequoia") || + rawVer.starts(with: "21.") || // Dodano Tahoe (v26/21.x) + rawVer.starts(with: "11.") || + (rawVer.starts(with: "12.") && !isExplicitlyUnsupported) || + (rawVer.starts(with: "13.") && !nameLower.contains("high")) || + (rawVer.starts(with: "14.") && !nameLower.contains("mojave")) || + (rawVer.starts(with: "15.") && !isExplicitlyUnsupported) + + // Old Supported (Mojave + High Sierra) + let isOldSupported = + nameLower.contains("mojave") || + nameLower.contains("high sierra") || + rawVer.starts(with: "10.14") || + rawVer.starts(with: "10.13") || + (rawVer.starts(with: "14.") && nameLower.contains("mojave")) || + (rawVer.starts(with: "13.") && nameLower.contains("high")) + + // Legacy No Codesign (Yosemite + El Capitan) + let isLegacyDetected = + nameLower.contains("yosemite") || + nameLower.contains("el capitan") || + rawVer.starts(with: "10.10") || + rawVer.starts(with: "10.11") + + // Legacy Restore (Lion + Mountain Lion) + let isRestoreLegacy = + nameLower.contains("mountain lion") || + nameLower.contains("lion") || + rawVer.starts(with: "10.8") || + rawVer.starts(with: "10.7") + + self.isSystemDetected = isModern || isOldSupported || isLegacyDetected || isRestoreLegacy || isCatalina || isSierra + + self.needsCodesign = isOldSupported && !isModern && !isLegacyDetected + self.isLegacyDetected = isLegacyDetected + self.isRestoreLegacy = isRestoreLegacy + self.isCatalina = isCatalina + self.isSierra = isSierra + self.isMavericks = isMavericks + if isMavericks { + self.shouldShowMavericksDialog = true + } + if isSierra { + self.recognizedVersion = "macOS Sierra 10.12" + self.needsCodesign = false + } + // isPPC zostało ustawione wcześniej dla Snow Leoparda/Leoparda/Tigera; dla pozostałych pozostaje false + self.isPPC = self.isPPC || false + + let trueFlags = [ + self.isCatalina ? "isCatalina" : nil, + self.isSierra ? "isSierra" : nil, + self.isLegacyDetected ? "isLegacyDetected" : nil, + self.isRestoreLegacy ? "isRestoreLegacy" : nil, + self.isPPC ? "isPPC" : nil, + self.isUnsupportedSierra ? "isUnsupportedSierra" : nil, + self.isMavericks ? "isMavericks" : nil + ].compactMap { $0 }.joined(separator: ", ") + self.log("Analiza zakończona. Rozpoznano: \(self.recognizedVersion)") + self.log("Przypisane flagi: \(trueFlags.isEmpty ? "brak" : trueFlags)") + AppLogging.separator() + + if self.isSystemDetected { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { withAnimation(.spring(response: 0.7, dampingFraction: 0.8)) { self.showUSBSection = true } } + } else { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { withAnimation(.spring(response: 0.7, dampingFraction: 0.8)) { self.showUnsupportedMessage = true } } + } + + return true + } +} diff --git a/macUSB/Features/Analysis/Logic/macOS/AnalysisLogicMacOSImageMounting.swift b/macUSB/Features/Analysis/Logic/macOS/AnalysisLogicMacOSImageMounting.swift new file mode 100644 index 0000000..2f9583f --- /dev/null +++ b/macUSB/Features/Analysis/Logic/macOS/AnalysisLogicMacOSImageMounting.swift @@ -0,0 +1,223 @@ +import Foundation + +extension AnalysisLogic { + private func readLegacyInstallMacOSXInfo(from mountURL: URL) -> (String, String, URL)? { + let legacyInstallers = [ + "Install Mac OS X", + "Install Mac OS X.app" + ] + + var foundLegacyPath = false + for installerName in legacyInstallers { + let installerURL = mountURL.appendingPathComponent(installerName, isDirectory: true) + let plistURL = installerURL.appendingPathComponent("Contents/Info.plist") + guard FileManager.default.fileExists(atPath: plistURL.path) else { + continue + } + + foundLegacyPath = true + self.log("Znaleziono legacy installer path: \(installerURL.path)") + self.log("Odczyt Info.plist (legacy): \(plistURL.path)") + + guard let data = try? Data(contentsOf: plistURL), + let dict = try? PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any] else { + self.logError("Nie udało się odczytać Info.plist (legacy): \(plistURL.path)") + continue + } + + let name = (dict["CFBundleDisplayName"] as? String) ?? installerURL.lastPathComponent + let version = (dict["CFBundleShortVersionString"] as? String) ?? "?" + self.log("Odczytano Info.plist (legacy): name=\(name), version=\(version)") + return (name, version, installerURL) + } + + if !foundLegacyPath { + self.log("Nie znaleziono legacy path instalatora 'Install Mac OS X' w: \(mountURL.path)") + } + + return nil + } + + private func normalizedImagePath(_ path: String) -> String { + URL(fileURLWithPath: path).resolvingSymlinksInPath().standardizedFileURL.path + } + + private func mountedPathForAlreadyAttachedImage(sourceURL: URL) -> String? { + let task = Process() + task.executableURL = URL(fileURLWithPath: "/usr/bin/hdiutil") + task.arguments = ["info", "-plist"] + let outputPipe = Pipe() + let errorPipe = Pipe() + task.standardOutput = outputPipe + task.standardError = errorPipe + + do { + try task.run() + } catch { + self.logError("Nie udało się uruchomić hdiutil info: \(error.localizedDescription)") + return nil + } + task.waitUntilExit() + + let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile() + let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile() + if task.terminationStatus != 0 { + let stderrText = String(data: errorData, encoding: .utf8)? + .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + if stderrText.isEmpty { + self.logError("hdiutil info zakończył się błędem (kod \(task.terminationStatus)).") + } else { + self.logError("hdiutil info zakończył się błędem: \(stderrText)") + } + return nil + } + + guard let plist = try? PropertyListSerialization.propertyList(from: outputData, options: [], format: nil) as? [String: Any], + let images = plist["images"] as? [[String: Any]] else { + return nil + } + + let sourcePath = normalizedImagePath(sourceURL.path) + for image in images { + guard let imagePath = image["image-path"] as? String else { continue } + guard normalizedImagePath(imagePath) == sourcePath else { continue } + guard let entities = image["system-entities"] as? [[String: Any]], + let mountPoint = entities.compactMap({ $0["mount-point"] as? String }).first else { + continue + } + return mountPoint + } + + return nil + } + + func mountAndReadInfo(dmgUrl: URL, detectPreMountedSource: Bool = false) -> (mountedReadInfo: (String, String, URL, String)?, sourceAlreadyMountedPath: String?)? { + self.log("Montowanie obrazu (DMG/ISO/CDR)") + if detectPreMountedSource, + let mountPoint = mountedPathForAlreadyAttachedImage(sourceURL: dmgUrl) { + self.log("Wybrany obraz .\(dmgUrl.pathExtension.lowercased()) jest już zamontowany w systemie: \(mountPoint)") + return (mountedReadInfo: nil, sourceAlreadyMountedPath: mountPoint) + } + + let task = Process(); task.executableURL = URL(fileURLWithPath: "/usr/bin/hdiutil") + task.arguments = ["attach", dmgUrl.path, "-plist", "-nobrowse", "-readonly"] + let pipe = Pipe() + let errorPipe = Pipe() + task.standardOutput = pipe + task.standardError = errorPipe + + do { + try task.run() + } catch { + self.logError("Nie udało się uruchomić hdiutil attach: \(error.localizedDescription)") + if detectPreMountedSource, + let mountPoint = mountedPathForAlreadyAttachedImage(sourceURL: dmgUrl) { + self.log("Po błędzie uruchomienia attach wykryto już zamontowany obraz źródłowy: \(mountPoint)") + return (mountedReadInfo: nil, sourceAlreadyMountedPath: mountPoint) + } + return nil + } + task.waitUntilExit() + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile() + let stderrText = String(data: errorData, encoding: .utf8)? + .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + if task.terminationStatus != 0 { + if stderrText.isEmpty { + self.logError("hdiutil attach zakończył się błędem (kod \(task.terminationStatus)).") + } else { + self.logError("hdiutil attach zakończył się błędem: \(stderrText)") + } + if detectPreMountedSource, + let mountPoint = mountedPathForAlreadyAttachedImage(sourceURL: dmgUrl) { + self.log("Po błędzie attach wykryto już zamontowany obraz źródłowy: \(mountPoint)") + return (mountedReadInfo: nil, sourceAlreadyMountedPath: mountPoint) + } + return nil + } + + guard let plist = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any], let entities = plist["system-entities"] as? [[String: Any]] else { + self.logError("Nie udało się odczytać informacji z obrazu") + if detectPreMountedSource, + let mountPoint = mountedPathForAlreadyAttachedImage(sourceURL: dmgUrl) { + self.log("Po nieudanym odczycie plist wykryto już zamontowany obraz źródłowy: \(mountPoint)") + return (mountedReadInfo: nil, sourceAlreadyMountedPath: mountPoint) + } + return nil + } + self.log("Przetwarzanie wyników hdiutil attach (\(entities.count) encji)") + for e in entities { + if let mp = e["mount-point"] as? String { + let devEntry = (e["dev-entry"] as? String) ?? (e["devname"] as? String) + var mountId = "unknown" + if let dev = devEntry { + let bsd = URL(fileURLWithPath: dev).lastPathComponent // e.g. disk9s1 + if let range = bsd.range(of: #"s\d+$"#, options: .regularExpression) { + mountId = String(bsd[.. String? { + self.log("Montowanie obrazu (PPC)") + let task = Process(); task.executableURL = URL(fileURLWithPath: "/usr/bin/hdiutil") + task.arguments = ["attach", dmgUrl.path, "-plist", "-nobrowse", "-readonly"] + let pipe = Pipe(); task.standardOutput = pipe; try? task.run(); task.waitUntilExit() + let data = pipe.fileHandleForReading.readDataToEndOfFile() + guard let plist = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any], let entities = plist["system-entities"] as? [[String: Any]] else { + self.logError("Nie udało się zamontować obrazu (PPC)") + return nil + } + for e in entities { + if let mp = e["mount-point"] as? String { + let devEntry = (e["dev-entry"] as? String) ?? (e["devname"] as? String) + var mountId = "unknown" + if let dev = devEntry { + let bsd = URL(fileURLWithPath: dev).lastPathComponent + if let range = bsd.range(of: #"s\d+$"#, options: .regularExpression) { + mountId = String(bsd[.. [URL] { + var result: [URL] = [] + let fm = FileManager.default + + func appendUnique(_ candidate: URL) { + let normalized = candidate.standardizedFileURL + guard !result.contains(where: { $0.standardizedFileURL == normalized }) else { return } + result.append(candidate) + } + + appendUnique(url) + + if url.pathExtension.lowercased() != "app" { + // Część starych obrazów ma instalator pod klasyczną nazwą "Install Mac OS X". + appendUnique(url.appendingPathComponent("Install Mac OS X.app", isDirectory: true)) + appendUnique(url.appendingPathComponent("Install OS X.app", isDirectory: true)) + appendUnique(url.appendingPathComponent("Install macOS.app", isDirectory: true)) + + if let children = try? fm.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]) { + for child in children where child.pathExtension.lowercased() == "app" { + appendUnique(child) + } + } + } + + return result.filter { fm.fileExists(atPath: $0.path) } + } + + private func findIconURL(in resourcesURL: URL, preferredFileNames: [String]) -> URL? { + let fm = FileManager.default + guard fm.fileExists(atPath: resourcesURL.path), + let files = try? fm.contentsOfDirectory(at: resourcesURL, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]) else { + return nil + } + + var byLowercasedName: [String: URL] = [:] + for file in files { + byLowercasedName[file.lastPathComponent.lowercased()] = file + } + + for fileName in preferredFileNames { + if let match = byLowercasedName[fileName.lowercased()] { + return match + } + } + + return nil + } +} diff --git a/macUSB/Features/Analysis/Logic/macOS/AnalysisLogicMacOSInstallerMetadata.swift b/macUSB/Features/Analysis/Logic/macOS/AnalysisLogicMacOSInstallerMetadata.swift new file mode 100644 index 0000000..8fde3f6 --- /dev/null +++ b/macUSB/Features/Analysis/Logic/macOS/AnalysisLogicMacOSInstallerMetadata.swift @@ -0,0 +1,53 @@ +import Foundation + +extension AnalysisLogic { + func updateRequiredUSBCapacity(rawVersion: String, name: String) { + guard let majorVersion = marketingMajorVersion(raw: rawVersion, name: name) else { + requiredUSBCapacityGB = nil + return + } + requiredUSBCapacityGB = (majorVersion >= 15) ? 32 : 16 + } + + func marketingMajorVersion(raw: String, name: String) -> Int? { + let marketingVersion = formatMarketingVersion(raw: raw, name: name) + guard let majorToken = marketingVersion.split(separator: ".").first else { return nil } + return Int(majorToken) + } + + func formatMarketingVersion(raw: String, name: String) -> String { + let n = name.lowercased() + if n.contains("tahoe") { return "26" } // Dodano Tahoe + if n.contains("sequoia") { return "15" } + if n.contains("sonoma") { return "14" } + if n.contains("ventura") { return "13" } + if n.contains("monterey") { return "12" } + if n.contains("big sur") { return "11" } + if n.contains("catalina") { return "10.15" } + if n.contains("mojave") { return "10.14" } + if n.contains("high sierra") { return "10.13" } + if n.contains("sierra") && !n.contains("high") { return "10.12" } + if n.contains("el capitan") { return "10.11" } + if n.contains("yosemite") { return "10.10" } + if n.contains("mavericks") { return "10.9" } + if n.contains("mountain lion") { return "10.8" } + if n.contains("lion") { return "10.7" } + if n.contains("snow leopard") { return "10.6" } + if n.contains("panther") { return "10.3" } + return raw + } + + func readAppInfo(appUrl: URL) -> (String, String, URL)? { + let plistUrl = appUrl.appendingPathComponent("Contents/Info.plist") + self.log("Odczyt Info.plist: \(plistUrl.path)") + if let d = try? Data(contentsOf: plistUrl), + let dict = try? PropertyListSerialization.propertyList(from: d, format: nil) as? [String: Any] { + let name = (dict["CFBundleDisplayName"] as? String) ?? appUrl.lastPathComponent + let ver = (dict["CFBundleShortVersionString"] as? String) ?? "?" + self.log("Odczytano Info.plist: name=\(name), version=\(ver)") + return (name, ver, appUrl) + } + self.logError("Nie udało się odczytać Info.plist") + return nil + } +} diff --git a/macUSB/Features/Analysis/Logic/macOS/AnalysisLogicMacOSLifecycle.swift b/macUSB/Features/Analysis/Logic/macOS/AnalysisLogicMacOSLifecycle.swift new file mode 100644 index 0000000..f375ff4 --- /dev/null +++ b/macUSB/Features/Analysis/Logic/macOS/AnalysisLogicMacOSLifecycle.swift @@ -0,0 +1,104 @@ +import SwiftUI +import Foundation + +extension AnalysisLogic { + func forceTigerMultiDVDSelection() { + self.log("Ręcznie wybrano tryb Tiger Multi DVD") + let fileURL = self.selectedFileUrl + DispatchQueue.global(qos: .userInitiated).async { + var mountPoint: String? = self.mountedDMGPath + var effectiveSourceAppURL: URL? = nil + if let url = fileURL { + let ext = url.pathExtension.lowercased() + if ext == "dmg" || ext == "iso" || ext == "cdr" { + if mountPoint == nil { + mountPoint = self.mountImageForPPC(dmgUrl: url) + } + if let mp = mountPoint { + effectiveSourceAppURL = URL(fileURLWithPath: mp).appendingPathComponent("Install") + } + } else if ext == "app" { + effectiveSourceAppURL = url + } + } + DispatchQueue.main.async { + withAnimation { + self.isAnalyzing = false + self.userSkippedAnalysis = true + self.recognizedVersion = "Mac OS X Tiger 10.4" + self.sourceAppURL = effectiveSourceAppURL + self.updateDetectedSystemIcon(from: effectiveSourceAppURL) + self.mountedDMGPath = mountPoint + self.isSystemDetected = true + self.showUnsupportedMessage = false + self.showUSBSection = true + self.needsCodesign = false + self.isLegacyDetected = false + self.isRestoreLegacy = false + self.isCatalina = false + self.isSierra = false + self.isMavericks = false + self.isUnsupportedSierra = false + self.isPPC = true + self.legacyArchInfo = nil + self.selectedDrive = nil + self.capacityCheckFinished = false + self.requiredUSBCapacityGB = 16 + } + let flags = [self.isPPC ? "isPPC" : nil].compactMap { $0 }.joined(separator: ", ") + self.log("Ustawiono Tiger Multi DVD: recognizedVersion=\(self.recognizedVersion). Flagi: \(flags.isEmpty ? "brak" : flags)") + } + } + } + + func resetAll() { + let oldMount = self.mountedDMGPath + if let path = oldMount { + let task = Process() + task.launchPath = "/usr/bin/hdiutil" + task.arguments = ["detach", path, "-force"] + try? task.run() + task.waitUntilExit() + } + DispatchQueue.main.async { + withAnimation { + self.selectedFilePath = "" + self.selectedFileUrl = nil + self.recognizedVersion = "" + self.sourceAppURL = nil + self.detectedSystemIcon = nil + self.mountedDMGPath = nil + + self.isAnalyzing = false + self.isSystemDetected = false + self.showUSBSection = false + self.showUnsupportedMessage = false + + self.needsCodesign = true + self.isLegacyDetected = false + self.isRestoreLegacy = false + self.isCatalina = false + self.isSierra = false + self.isMavericks = false + self.isUnsupportedSierra = false + self.isPPC = false + self.legacyArchInfo = nil + self.shouldShowAlreadyMountedSourceAlert = false + self.userSkippedAnalysis = false + self.shouldShowMavericksDialog = false + self.requiredUSBCapacityGB = nil + + self.availableDrives = [] + self.selectedDrive = nil + + self.isCapacitySufficient = false + self.capacityCheckFinished = false + } + } + } + + // Call this from the UI when the user presses the "Przejdź dalej" button + func recordProceedPressed() { + self.log("Użytkownik nacisnął przycisk 'Przejdź dalej'. Wybrany nośnik: \(self.selectedDrive?.url.path ?? "brak"), źródło: \(self.sourceAppURL?.path ?? "brak"), rozpoznano: \(self.recognizedVersion)") + } +} diff --git a/macUSB/Resources/Localizable.xcstrings b/macUSB/Resources/Localizable.xcstrings index 133cc49..c212cb0 100644 --- a/macUSB/Resources/Localizable.xcstrings +++ b/macUSB/Resources/Localizable.xcstrings @@ -10868,6 +10868,7 @@ } }, "Lokalizacja aplikacji: poza /Applications" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { diff --git a/macUSB/Shared/UI/LiquidGlassCompatibility.swift b/macUSB/Shared/UI/LiquidGlassCompatibility.swift index 64c2a51..e218560 100644 --- a/macUSB/Shared/UI/LiquidGlassCompatibility.swift +++ b/macUSB/Shared/UI/LiquidGlassCompatibility.swift @@ -78,23 +78,6 @@ private extension MacUSBSurfaceTone { } } - @available(macOS 26.0, *) - var glass: Glass { - switch self { - case .neutral, .subtle: - return .regular.interactive(false) - case .info: - return .regular.tint(.blue.opacity(0.14)).interactive(false) - case .success: - return .regular.tint(.green.opacity(0.14)).interactive(false) - case .warning: - return .regular.tint(.orange.opacity(0.16)).interactive(false) - case .error: - return .regular.tint(.red.opacity(0.16)).interactive(false) - case .active: - return .regular.tint(.accentColor.opacity(0.18)).interactive(false) - } - } } private struct MacUSBTopRoundedRectangle: Shape { @@ -131,8 +114,26 @@ private struct MacUSBPanelSurfaceModifier: ViewModifier { let shape = RoundedRectangle(cornerRadius: radius, style: .continuous) if #available(macOS 26.0, *), !tone.usesStableSurfaceOnLiquidGlass { - content - .glassEffect(tone.glass, in: shape) + switch tone { + case .neutral, .subtle: + content + .glassEffect(.regular.interactive(false), in: shape) + case .info: + content + .glassEffect(.regular.tint(.blue.opacity(0.14)).interactive(false), in: shape) + case .success: + content + .glassEffect(.regular.tint(.green.opacity(0.14)).interactive(false), in: shape) + case .warning: + content + .glassEffect(.regular.tint(.orange.opacity(0.16)).interactive(false), in: shape) + case .error: + content + .glassEffect(.regular.tint(.red.opacity(0.16)).interactive(false), in: shape) + case .active: + content + .glassEffect(.regular.tint(.accentColor.opacity(0.18)).interactive(false), in: shape) + } } else { content .background(