diff --git a/MiniSim/Extensions/UserDefaults+Configuration.swift b/MiniSim/Extensions/UserDefaults+Configuration.swift index f95cb4e..28cf311 100644 --- a/MiniSim/Extensions/UserDefaults+Configuration.swift +++ b/MiniSim/Extensions/UserDefaults+Configuration.swift @@ -15,6 +15,8 @@ extension UserDefaults { static let isOnboardingFinished = "isOnboardingFinished" static let enableiOSSimulators = "enableiOSSimulators" static let enableAndroidEmulators = "enableAndroidEmulators" + static let pinnediOSSimulators = "pinnediOSSimulators" + static let pinnedAndroidEmulators = "pinnedAndroidEmulators" } @objc public dynamic var androidHome: String? { @@ -46,4 +48,15 @@ extension UserDefaults { get { bool(forKey: Keys.enableAndroidEmulators) } set { set(newValue, forKey: Keys.enableAndroidEmulators) } } + + public var pinnediOSSimulators: [String]? { + get { array(forKey: Keys.pinnediOSSimulators) as? [String] } + set { set(newValue, forKey: Keys.pinnediOSSimulators) } + } + + public var pinnedAndroidEmulators: [String]? { + get { array(forKey: Keys.pinnedAndroidEmulators) as? [String] } + set { set(newValue, forKey: Keys.pinnedAndroidEmulators) } + } + } diff --git a/MiniSim/MenuItems/SubMenuItem.swift b/MiniSim/MenuItems/SubMenuItem.swift index 415846a..630903e 100644 --- a/MiniSim/MenuItems/SubMenuItem.swift +++ b/MiniSim/MenuItems/SubMenuItem.swift @@ -27,6 +27,7 @@ enum SubMenuItems { case toggleA11y case paste case delete + case togglePinned case customCommand = 200 } @@ -108,7 +109,18 @@ enum SubMenuItems { accessibilityDescription: "Keyboard" ) } - + + struct TogglePinToTop: SubMenuActionItem { + let title = NSLocalizedString("Pin/unpin to top", comment: "") + let tag = Tags.togglePinned.rawValue + let bootsDevice = false + let needBootedDevice = false + let image = NSImage( + systemSymbolName: "pin", + accessibilityDescription: "Pin or unpin to Top" + ) + } + struct Delete: SubMenuActionItem { let title = NSLocalizedString("Delete simulator", comment: "") let tag = Tags.delete.rawValue @@ -138,7 +150,8 @@ extension SubMenuItems { CopyID(), Separator(), - + + TogglePinToTop(), ColdBoot(), NoAudio(), ToggleA11y(), @@ -151,7 +164,8 @@ extension SubMenuItems { CopyUDID(), Separator(), - + + TogglePinToTop(), Delete() ] } diff --git a/MiniSim/Model/Device.swift b/MiniSim/Model/Device.swift index 78275bc..06a6120 100644 --- a/MiniSim/Model/Device.swift +++ b/MiniSim/Model/Device.swift @@ -11,22 +11,24 @@ struct Device: Hashable, Codable { var identifier: String? var booted: Bool var platform: Platform - + var pinned: Bool + var displayName: String { + let pinIcon = pinned ? " 📌" : "" switch platform { case .ios: if let version { - return "\(name) - (\(version))" + return "\(name) - (\(version))" + pinIcon } - return name - + return name + pinIcon + case .android: - return name + return name + pinIcon } } enum CodingKeys: String, CodingKey { - case name, version, identifier, booted, platform, displayName + case name, version, identifier, booted, platform, displayName, pinned } init(name: String, version: String? = nil, identifier: String?, booted: Bool = false, platform: Platform) { @@ -35,6 +37,7 @@ struct Device: Hashable, Codable { self.identifier = identifier self.booted = booted self.platform = platform + self.pinned = pinned } init(from decoder: Decoder) throws { @@ -44,6 +47,7 @@ struct Device: Hashable, Codable { identifier = try values.decode(String.self, forKey: .identifier) booted = try values.decode(Bool.self, forKey: .booted) platform = try values.decode(Platform.self, forKey: .platform) + pinned = try values.decode(Bool.self, forKey: .pinned) } func encode(to encoder: Encoder) throws { diff --git a/MiniSim/Service/DeviceService.swift b/MiniSim/Service/DeviceService.swift index ab0424b..030c353 100644 --- a/MiniSim/Service/DeviceService.swift +++ b/MiniSim/Service/DeviceService.swift @@ -208,6 +208,30 @@ class DeviceService: DeviceServiceProtocol { } } } + + static func togglePinned(device: Device) { + var pinnedDevices: [String]? + switch device.platform { + case .android: + pinnedDevices = UserDefaults.standard.pinnedAndroidEmulators + var dedupedPinnedDevices = Set(pinnedDevices ?? []) + if dedupedPinnedDevices.contains(device.name) { + dedupedPinnedDevices.remove(device.name) + } else { + dedupedPinnedDevices.insert(device.name) + } + UserDefaults.standard.pinnedAndroidEmulators = Array(dedupedPinnedDevices) + case .ios: + pinnedDevices = UserDefaults.standard.pinnediOSSimulators + var dedupedPinnedDevices = Set(pinnedDevices ?? []) + if dedupedPinnedDevices.contains(device.name) { + dedupedPinnedDevices.remove(device.name) + } else { + dedupedPinnedDevices.insert(device.name) + } + UserDefaults.standard.pinnediOSSimulators = Array(dedupedPinnedDevices) + } + } } // MARK: iOS Methods @@ -219,18 +243,21 @@ extension DeviceService { let identifierIdx = 4 let deviceStateIdx = 5 var osVersion = "" + let pinnediOSSimulators: [String] = UserDefaults.standard.pinnediOSSimulators ?? [] result.forEach { line in if let currentOs = line.match("-- (.*?) --").first, !currentOs.isEmpty { osVersion = currentOs[currentOSIdx] } if let device = line.match("(.*?) (\\(([0-9.]+)\\) )?\\(([0-9A-F-]+)\\) (\\(.*?)\\)").first { + let deviceName = device[1].trimmingCharacters(in: .whitespacesAndNewlines) devices.append( Device( name: device[deviceNameIdx].trimmingCharacters(in: .whitespacesAndNewlines), version: osVersion, identifier: device[identifierIdx], booted: device[deviceStateIdx].contains("Booted"), - platform: .ios + platform: .ios, + pinned: pinnediOSSimulators.contains(deviceName) ) ) } @@ -312,6 +339,9 @@ extension DeviceService { NSPasteboard.general.copyToPasteboard(text: deviceID) DeviceService.showSuccessMessage(title: "Device ID copied to clipboard!", message: deviceID) } + case .togglePinned: + DeviceService.togglePinned(device: device) + case .delete: guard let deviceID = device.identifier else { return } let result = !NSAlert.showQuestionDialog( @@ -381,12 +411,13 @@ extension DeviceService { let adbPath = try ADB.getAdbPath() let output = try shellOut(to: emulatorPath, arguments: ["-list-avds"]) let splitted = output.components(separatedBy: "\n") + let pinnedAndroidEmulators: [String] = UserDefaults.standard.pinnedAndroidEmulators ?? [] return splitted .filter { !$0.isEmpty } .map { deviceName in let adbId = try? ADB.getAdbId(for: deviceName, adbPath: adbPath) - return Device(name: deviceName, identifier: adbId, booted: adbId != nil, platform: .android) + return Device(name: deviceName, identifier: adbId, booted: adbId != nil, platform: .android, pinned: pinnedAndroidEmulators.contains(deviceName)) } } @@ -446,7 +477,10 @@ extension DeviceService { case .copyName: NSPasteboard.general.copyToPasteboard(text: device.name) DeviceService.showSuccessMessage(title: "Device name copied to clipboard!", message: device.name) - + + case .togglePinned: + DeviceService.togglePinned(device: device) + case .paste: guard let clipboard = NSPasteboard.general.pasteboardItems?.first, let text = clipboard.string(forType: .string) else {