Skip to content

Commit 406998d

Browse files
authored
Added [Local Files] submenu section with [Upload...] submenu to upload file/folder to simulator/emulator/device (#146)
* Add [Upload to Downloads...] action for Android emulators with folder/file selection and upload via adb (+use deprecated MediaScanner broadcast to force reindex - see https://stackoverflow.com/questions/66929450/images-not-shown-in-photos-using-adb-push-pictures-to-android-11-emulator and https://stackoverflow.com/questions/64552886/adb-push-files-are-not-showing-on-android-11-emulator for reference). * Added [Upload to Downloads...] action to connected Android device (uploads to phone internal storage /Downloads folder) actions * Move Delete emulator to end of Android menu * Add iOS simulator local files actions * Tweak Android upload menu and tooltips * Moved upload-related functionality in separate file. Added fallback/checking to /sdcard/Downloads if /sdcard/Download does not exist. Fixed issues uploading files/folders containing spaces in name. Added tests. Added logger for debug purposes and warnings/errors.
1 parent c0532a6 commit 406998d

16 files changed

Lines changed: 1030 additions & 13 deletions

MiniSim.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@
112112
76FCABAB29B390D5003BBF9A /* Collection+get.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76FCABAA29B390D5003BBF9A /* Collection+get.swift */; };
113113
7D4142B4F3AF1D150D9222BD /* DeviceFamily.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F78DD26087D5448AAB2CC3B /* DeviceFamily.swift */; };
114114
7D850183B2E53A3E4F17994A /* StringMatchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A9C6AA804E851167EA01B17 /* StringMatchTests.swift */; };
115+
7A9C0F1B2D3E4A5B6C7D8EA1 /* UploadHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9C0F1B2D3E4A5B6C7D8E9F /* UploadHelpers.swift */; };
116+
7A9C0F1B2D3E4A5B6C7D8EA3 /* UploadHelpersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9C0F1B2D3E4A5B6C7D8EA2 /* UploadHelpersTests.swift */; };
117+
7B1E9F672D3F4B3C9A0D1B22 /* ShellEscapingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1E9F662D3F4B3C9A0D1B21 /* ShellEscapingTests.swift */; };
115118
9B225A9C2C7E360D002620BA /* DeviceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B225A9B2C7E360D002620BA /* DeviceType.swift */; };
116119
/* End PBXBuildFile section */
117120

@@ -226,6 +229,9 @@
226229
76F2A913299050F9002D4EF6 /* UserDefaults+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+Configuration.swift"; sourceTree = "<group>"; };
227230
76F2A9162991B7B6002D4EF6 /* ViewModifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModifiers.swift; sourceTree = "<group>"; };
228231
76FCABAA29B390D5003BBF9A /* Collection+get.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+get.swift"; sourceTree = "<group>"; };
232+
7A9C0F1B2D3E4A5B6C7D8E9F /* UploadHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadHelpers.swift; sourceTree = "<group>"; };
233+
7A9C0F1B2D3E4A5B6C7D8EA2 /* UploadHelpersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadHelpersTests.swift; sourceTree = "<group>"; };
234+
7B1E9F662D3F4B3C9A0D1B21 /* ShellEscapingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellEscapingTests.swift; sourceTree = "<group>"; };
229235
912D7A2027F3DDA86839E7AE /* CollectionGetTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CollectionGetTests.swift; sourceTree = "<group>"; };
230236
9B225A9B2C7E360D002620BA /* DeviceType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceType.swift; sourceTree = "<group>"; };
231237
/* End PBXFileReference section */
@@ -359,6 +365,7 @@
359365
76BF0ADE2C8E01B3003BE568 /* CustomCommandService.swift */,
360366
76BF0AE02C8E027D003BE568 /* DeviceConstants.swift */,
361367
76BF0AE52C8E0F83003BE568 /* Actions.swift */,
368+
7A9C0F1B2D3E4A5B6C7D8E9F /* UploadHelpers.swift */,
362369
76BF0AE72C8E1077003BE568 /* ActionFactory.swift */,
363370
76BF0AEF2C9061E8003BE568 /* DeviceDiscoveryService.swift */,
364371
76BF0AF12C907032003BE568 /* DeviceServiceFactory.swift */,
@@ -471,6 +478,8 @@
471478
76BF0AF32C90A74E003BE568 /* AppleUtilsTests.swift */,
472479
76BF0AF52C90AB03003BE568 /* DeviceDiscoveryTests.swift */,
473480
76BF0AF72C90ACF2003BE568 /* ActionFactoryTests.swift */,
481+
7A9C0F1B2D3E4A5B6C7D8EA2 /* UploadHelpersTests.swift */,
482+
7B1E9F662D3F4B3C9A0D1B21 /* ShellEscapingTests.swift */,
474483
470EFF3B34FB31AC5A22B024 /* DeviceTests.swift */,
475484
6A9C6AA804E851167EA01B17 /* StringMatchTests.swift */,
476485
912D7A2027F3DDA86839E7AE /* CollectionGetTests.swift */,
@@ -694,6 +703,7 @@
694703
76BF0AF22C907033003BE568 /* DeviceServiceFactory.swift in Sources */,
695704
764BA3EB2A5AD43F003A78AF /* LaunchDeviceCommand.swift in Sources */,
696705
76BF0AE62C8E0F83003BE568 /* Actions.swift in Sources */,
706+
7A9C0F1B2D3E4A5B6C7D8EA1 /* UploadHelpers.swift in Sources */,
697707
7630B2732986C68000D8B57D /* PaneIdentifier.swift in Sources */,
698708
7630B2662985C44A00D8B57D /* Preferences.swift in Sources */,
699709
7625140B2992B46D0060A225 /* Pasteboard+utils.swift in Sources */,
@@ -750,6 +760,8 @@
750760
76BF0AE32C8E041C003BE568 /* CustomCommandServiceTests.swift in Sources */,
751761
76BF0AF42C90A74E003BE568 /* AppleUtilsTests.swift in Sources */,
752762
76BF0AF82C90ACF2003BE568 /* ActionFactoryTests.swift in Sources */,
763+
7A9C0F1B2D3E4A5B6C7D8EA3 /* UploadHelpersTests.swift in Sources */,
764+
7B1E9F672D3F4B3C9A0D1B22 /* ShellEscapingTests.swift in Sources */,
753765
76B70F7E2B0D361A009D87A4 /* UserDefaultsTests.swift in Sources */,
754766
760DEACE2B0DFB6600253576 /* ShellStub.swift in Sources */,
755767
76B70F822B0D50FE009D87A4 /* ADBTests.swift in Sources */,

MiniSim/Extensions/NSAlert+showError.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,31 @@ extension NSAlert {
3333
alert.addButton(withTitle: "Cancel")
3434
return alert.runModal() == .alertFirstButtonReturn
3535
}
36+
37+
static func showWarningDialog(
38+
title: String,
39+
message: String,
40+
primaryButton: String,
41+
secondaryButton: String?
42+
) -> NSApplication.ModalResponse {
43+
let showAlert: () -> NSApplication.ModalResponse = {
44+
let alert = self.init()
45+
alert.messageText = title
46+
alert.informativeText = message
47+
alert.alertStyle = .warning
48+
alert.addButton(withTitle: primaryButton)
49+
if let secondaryButton {
50+
alert.addButton(withTitle: secondaryButton)
51+
}
52+
return alert.runModal()
53+
}
54+
55+
if Thread.isMainThread {
56+
return showAlert()
57+
}
58+
59+
return DispatchQueue.main.sync {
60+
showAlert()
61+
}
62+
}
3663
}

MiniSim/Extensions/NSMenuItem+ConvenienceInit.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ extension NSMenuItem {
2424
self.tag = menuItem.tag
2525
self.image = menuItem.image
2626
self.title = menuItem.title
27-
self.toolTip = menuItem.title
27+
self.toolTip = menuItem.toolTip ?? menuItem.title
2828
self.target = target
2929
self.action = action
3030
}

MiniSim/Menu.swift

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -265,25 +265,63 @@ class Menu: NSMenu {
265265
isDeviceBooted: Bool,
266266
callback: Selector
267267
) -> [NSMenuItem] {
268-
subMenuItems.compactMap { item in
268+
var menuItems: [NSMenuItem] = []
269+
270+
subMenuItems.forEach { item in
269271
if item is SubMenuItems.Separator {
270-
return NSMenuItem.separator()
272+
menuItems.append(NSMenuItem.separator())
273+
return
274+
}
275+
276+
if let item = item as? SubMenuSectionItem {
277+
if item.needBootedDevice && !isDeviceBooted {
278+
return
279+
}
280+
let menuItem = NSMenuItem(title: item.title, action: nil, keyEquivalent: "")
281+
menuItem.isEnabled = false
282+
menuItems.append(menuItem)
283+
return
271284
}
272285

273286
if let item = item as? SubMenuActionItem {
274287
if item.needBootedDevice && !isDeviceBooted {
275-
return nil
288+
return
276289
}
277290

278291
if item.bootsDevice && isDeviceBooted {
279-
return nil
292+
return
280293
}
281294

282-
return NSMenuItem(menuItem: item, target: self, action: callback)
295+
menuItems.append(NSMenuItem(menuItem: item, target: self, action: callback))
296+
return
283297
}
298+
}
284299

285-
return nil
300+
return normalizeSeparators(menuItems)
301+
}
302+
303+
private func normalizeSeparators(_ items: [NSMenuItem]) -> [NSMenuItem] {
304+
var normalized: [NSMenuItem] = []
305+
var previousWasSeparator = true
306+
307+
for item in items {
308+
if item.isSeparatorItem {
309+
if previousWasSeparator {
310+
continue
311+
}
312+
normalized.append(item)
313+
previousWasSeparator = true
314+
} else {
315+
normalized.append(item)
316+
previousWasSeparator = false
317+
}
318+
}
319+
320+
while normalized.last?.isSeparatorItem == true {
321+
normalized.removeLast()
286322
}
323+
324+
return normalized
287325
}
288326

289327
func createCustomCommandsMenu(for platform: Platform, isDeviceBooted: Bool, callback: Selector) -> [NSMenuItem] {

MiniSim/MenuItems/SubMenuItem.swift

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,18 @@ protocol SubMenuActionItem: SubMenuItem {
1616
var needBootedDevice: Bool { get }
1717
var bootsDevice: Bool { get }
1818
var image: NSImage? { get }
19+
var toolTip: String? { get }
20+
}
21+
22+
extension SubMenuActionItem {
23+
var toolTip: String? {
24+
nil
25+
}
26+
}
27+
28+
protocol SubMenuSectionItem: SubMenuItem {
29+
var title: String { get }
30+
var needBootedDevice: Bool { get }
1931
}
2032

2133
enum SubMenuItems {
@@ -28,11 +40,18 @@ enum SubMenuItems {
2840
case paste
2941
case delete
3042
case logcat
43+
case upload
44+
case localFiles
3145
case customCommand = 200
3246
}
3347

3448
struct Separator: SubMenuItem { }
3549

50+
struct SectionTitle: SubMenuSectionItem {
51+
let title: String
52+
let needBootedDevice: Bool
53+
}
54+
3655
struct CopyName: SubMenuActionItem {
3756
let title = NSLocalizedString("Copy name", comment: "")
3857
let tag = Tags.copyName.rawValue
@@ -110,6 +129,43 @@ enum SubMenuItems {
110129
)
111130
}
112131

132+
struct Upload: SubMenuActionItem {
133+
let title = NSLocalizedString("Upload...", comment: "")
134+
let tag = Tags.upload.rawValue
135+
let bootsDevice = false
136+
let needBootedDevice = true
137+
let image = NSImage(
138+
systemSymbolName: "tray.and.arrow.up",
139+
accessibilityDescription: "Upload"
140+
)
141+
let toolTip: String? = NSLocalizedString(
142+
"Upload file/folder to internal storage Downloads folder",
143+
comment: ""
144+
)
145+
}
146+
147+
struct UploadToFiles: SubMenuActionItem {
148+
let title = NSLocalizedString("Upload...", comment: "")
149+
let tag = Tags.upload.rawValue
150+
let bootsDevice = false
151+
let needBootedDevice = true
152+
let image = NSImage(
153+
systemSymbolName: "tray.and.arrow.up",
154+
accessibilityDescription: "Upload"
155+
)
156+
}
157+
158+
struct LocalFiles: SubMenuActionItem {
159+
let title = NSLocalizedString("Open in Finder", comment: "")
160+
let tag = Tags.localFiles.rawValue
161+
let bootsDevice = false
162+
let needBootedDevice = true
163+
let image = NSImage(
164+
systemSymbolName: "folder",
165+
accessibilityDescription: "Local Files"
166+
)
167+
}
168+
113169
struct Delete: SubMenuActionItem {
114170
let title = NSLocalizedString("Delete simulator", comment: "")
115171
let tag = Tags.delete.rawValue
@@ -159,6 +215,15 @@ extension SubMenuItems {
159215

160216
Separator(),
161217

218+
SectionTitle(
219+
title: NSLocalizedString("Local Files", comment: ""),
220+
needBootedDevice: true
221+
),
222+
UploadToFiles(),
223+
LocalFiles(),
224+
225+
Separator(),
226+
162227
Delete()
163228
]
164229
case (.android, .physical):
@@ -170,7 +235,15 @@ extension SubMenuItems {
170235

171236
ToggleA11y(),
172237
Paste(),
173-
LaunchLogCat()
238+
LaunchLogCat(),
239+
240+
Separator(),
241+
242+
SectionTitle(
243+
title: NSLocalizedString("Local Files", comment: ""),
244+
needBootedDevice: true
245+
),
246+
Upload()
174247
]
175248

176249
case (.android, .virtual):
@@ -184,8 +257,19 @@ extension SubMenuItems {
184257
NoAudio(),
185258
ToggleA11y(),
186259
Paste(),
187-
DeleteEmulator(),
188-
LaunchLogCat()
260+
LaunchLogCat(),
261+
262+
Separator(),
263+
264+
SectionTitle(
265+
title: NSLocalizedString("Local Files", comment: ""),
266+
needBootedDevice: true
267+
),
268+
Upload(),
269+
270+
Separator(),
271+
272+
DeleteEmulator()
189273
]
190274
}
191275
}

MiniSim/Service/ActionFactory.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ class AndroidActionFactory: ActionFactory {
2020
return ToggleA11yCommand(device: device)
2121
case .paste:
2222
return PasteClipboardAction(device: device)
23+
case .upload:
24+
return UploadToDownloadsAction(device: device)
25+
case .localFiles:
26+
return UnsupportedAction(message: "Local Files is only available for iOS simulators.")
2327
case .delete:
2428
return DeleteAction(device: device, skipConfirmation: skipConfirmation)
2529
case .customCommand:
@@ -37,6 +41,10 @@ class IOSActionFactory: ActionFactory {
3741
return CopyNameAction(device: device)
3842
case .copyID:
3943
return CopyIDAction(device: device)
44+
case .upload:
45+
return UploadToSimulatorFilesAction(device: device)
46+
case .localFiles:
47+
return OpenSimulatorFilesAction(device: device)
4048
case .customCommand:
4149
return CustomCommandAction(device: device, itemName: itemName)
4250
case .coldBoot:

0 commit comments

Comments
 (0)