diff --git a/SimDirs.xcodeproj/project.pbxproj b/SimDirs.xcodeproj/project.pbxproj index 1a05d63..02693b6 100644 --- a/SimDirs.xcodeproj/project.pbxproj +++ b/SimDirs.xcodeproj/project.pbxproj @@ -19,6 +19,8 @@ C90DCC162896B0370072E403 /* AppearancePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90DCC152896B0370072E403 /* AppearancePicker.swift */; }; C927A0D92846414900533D66 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = C927A0D82846414900533D66 /* Helpers.swift */; }; C927A0DB2846502300533D66 /* PathActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C927A0DA2846502300533D66 /* PathActions.swift */; }; + C95CC0F828B2411700928FAE /* AppearanceButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C95CC0F728B2411700928FAE /* AppearanceButtonStyle.swift */; }; + C95CC0FA28B2414900928FAE /* SystemIconButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C95CC0F928B2414900928FAE /* SystemIconButtonStyle.swift */; }; C9779742284F6DE000706DFB /* ToolbarMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9779741284F6DE000706DFB /* ToolbarMenu.swift */; }; C982F859283B9F9000D491F4 /* SimDirsApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C982F858283B9F9000D491F4 /* SimDirsApp.swift */; }; C982F85B283B9F9000D491F4 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C982F85A283B9F9000D491F4 /* ContentView.swift */; }; @@ -60,6 +62,8 @@ C90DCC152896B0370072E403 /* AppearancePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePicker.swift; sourceTree = ""; }; C927A0D82846414900533D66 /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; }; C927A0DA2846502300533D66 /* PathActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathActions.swift; sourceTree = ""; }; + C95CC0F728B2411700928FAE /* AppearanceButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceButtonStyle.swift; sourceTree = ""; }; + C95CC0F928B2414900928FAE /* SystemIconButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemIconButtonStyle.swift; sourceTree = ""; }; C9779741284F6DE000706DFB /* ToolbarMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarMenu.swift; sourceTree = ""; }; C982F855283B9F9000D491F4 /* SimDirs.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SimDirs.app; sourceTree = BUILT_PRODUCTS_DIR; }; C982F858283B9F9000D491F4 /* SimDirsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimDirsApp.swift; sourceTree = ""; }; @@ -101,6 +105,16 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + C95CC0F628B240F500928FAE /* Styles */ = { + isa = PBXGroup; + children = ( + C95CC0F728B2411700928FAE /* AppearanceButtonStyle.swift */, + C9BF5233289FE99600BDDC91 /* DescriptiveToggleStyle.swift */, + C95CC0F928B2414900928FAE /* SystemIconButtonStyle.swift */, + ); + path = Styles; + sourceTree = ""; + }; C982F84C283B9F9000D491F4 = { isa = PBXGroup; children = ( @@ -159,12 +173,12 @@ C982F881283E7F0400D491F4 /* Views */ = { isa = PBXGroup; children = ( + C95CC0F628B240F500928FAE /* Styles */, C9DD54C12860935300D46AB3 /* SourceItem Views */, C9DD54CC2860992200D46AB3 /* Model Views */, C90DCC152896B0370072E403 /* AppearancePicker.swift */, C90DCC132896AAAA0072E403 /* ContentHeader.swift */, C9BF5231289FE95D00BDDC91 /* DescriptiveToggle.swift */, - C9BF5233289FE99600BDDC91 /* DescriptiveToggleStyle.swift */, C90BCE472861D70500C2EF35 /* ErrorView.swift */, C927A0DA2846502300533D66 /* PathActions.swift */, C9EE0CD128478FDB00E9B97A /* PathRow.swift */, @@ -309,6 +323,7 @@ C982F877283D020C00D491F4 /* SimProductFamily.swift in Sources */, C982F859283B9F9000D491F4 /* SimDirsApp.swift in Sources */, C982F871283CE7B800D491F4 /* SimRuntime.swift in Sources */, + C95CC0FA28B2414900928FAE /* SystemIconButtonStyle.swift in Sources */, C9EE0CD228478FDB00E9B97A /* PathRow.swift in Sources */, C982F86B283BA22100D491F4 /* SimPlatform.swift in Sources */, C927A0D92846414900533D66 /* Helpers.swift in Sources */, @@ -317,6 +332,7 @@ C9D73C29285C8C4B0044A279 /* SourceItem.swift in Sources */, C982F879283D042E00D491F4 /* SimModel.swift in Sources */, C982F87B283E40C800D491F4 /* SimCtl.swift in Sources */, + C95CC0F828B2411700928FAE /* AppearanceButtonStyle.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/SimDirs/Model/SimCtl.swift b/SimDirs/Model/SimCtl.swift index 683bdbb..d2b4d6f 100644 --- a/SimDirs/Model/SimCtl.swift +++ b/SimDirs/Model/SimCtl.swift @@ -115,4 +115,8 @@ struct SimCtl { func setDeviceIncreaseContrast(_ device: SimDevice, increaseContrast: SimDevice.IncreaseContrast) throws { try runAsync(args: ["ui", device.udid, "increase_contrast", increaseContrast.rawValue]) } + + func saveScreen(_ device: SimDevice, url: URL) throws { + try runAsync(args: ["io", device.udid, "screenshot", url.path]) + } } diff --git a/SimDirs/Model/SimDevice.swift b/SimDirs/Model/SimDevice.swift index 5bb2e28..b0ae697 100644 --- a/SimDirs/Model/SimDevice.swift +++ b/SimDirs/Model/SimDevice.swift @@ -182,6 +182,14 @@ class SimDevice: ObservableObject, Decodable { print("Failed to set device increase contrast: \(error)") } } + + func saveScreen(_ url: URL) { + do { + try SimCtl().saveScreen(self, url: url) + } catch { + print("Failed to save screen: \(error)") + } + } } extension SimDevice { diff --git a/SimDirs/Views/AppearancePicker.swift b/SimDirs/Views/AppearancePicker.swift index 66700a1..40f00a5 100644 --- a/SimDirs/Views/AppearancePicker.swift +++ b/SimDirs/Views/AppearancePicker.swift @@ -7,42 +7,6 @@ import SwiftUI -extension ButtonStyle where Self == AppearanceButtonStyle { - static func appearance(selected: Bool, scheme: ColorScheme) -> AppearanceButtonStyle { - AppearanceButtonStyle(selected: selected, scheme: scheme) - } -} - -struct AppearanceButtonStyle: ButtonStyle { - let selected : Bool - let scheme : ColorScheme - - func makeBody(configuration: Configuration) -> some View { - VStack { - let bordered = selected != configuration.isPressed - let color = scheme == .light ? Color.white : Color.black - let content = color - .frame(width: 48, height: 32) - .cornerRadius(5.0) - - if bordered { - content - .overlay( - RoundedRectangle(cornerRadius: 6.0) - .stroke(Color.accentColor, lineWidth: 2.0) - ) - } - else { - content - .shadow(color: .black.opacity(0.5), radius: 1.0, x: 0, y: 1.0) - } - - configuration.label - } - .padding(1.0) - } -} - struct AppearancePicker: View { @Environment(\.isEnabled) var isEnabled @Binding var scheme : ColorScheme? diff --git a/SimDirs/Views/Model Views/DeviceContent.swift b/SimDirs/Views/Model Views/DeviceContent.swift index b4c66ba..35c780e 100644 --- a/SimDirs/Views/Model Views/DeviceContent.swift +++ b/SimDirs/Views/Model Views/DeviceContent.swift @@ -48,6 +48,14 @@ struct DeviceContent: View { @ObservedObject var device : SimDevice @State var isBooted : Bool + var fileDateFormatter : DateFormatter { + let formatter = DateFormatter() + + formatter.dateFormat = "yyyy.MM.dd'_'HH.mm.ss" + + return formatter + } + init(_ device: SimDevice) { self.device = device self.isBooted = device.isBooted @@ -91,6 +99,14 @@ struct DeviceContent: View { .opacity(isBooted ? 1.0 : 0.5) } } + + ContentHeader("Actions") + Button(action: saveScreen) { + Text("Save Screen") + .fontWeight(.semibold) + .font(.system(size: 11)) + } + .buttonStyle(.systemIcon("camera.on.rectangle")) } .environment(\.isEnabled, isBooted) .onAppear { @@ -108,6 +124,24 @@ struct DeviceContent: View { } } } + + func saveScreen() { + let savePanel = NSSavePanel() + + savePanel.allowedContentTypes = [.png] + savePanel.canCreateDirectories = true + savePanel.isExtensionHidden = false + savePanel.title = "Save Screen" + savePanel.message = "Select destination" + savePanel.nameFieldLabel = "Filename:" + savePanel.nameFieldStringValue = "\(device.name) - \(fileDateFormatter.string(from: Date()))" + + if savePanel.runModal() == .OK { + if let url = savePanel.url { + device.saveScreen(url) + } + } + } } struct DeviceContent_Previews: PreviewProvider { diff --git a/SimDirs/Views/Styles/AppearanceButtonStyle.swift b/SimDirs/Views/Styles/AppearanceButtonStyle.swift new file mode 100644 index 0000000..9d57f52 --- /dev/null +++ b/SimDirs/Views/Styles/AppearanceButtonStyle.swift @@ -0,0 +1,44 @@ +// +// AppearanceButtonStyle.swift +// SimDirs +// +// Created by Casey Fleser on 8/21/22. +// + +import SwiftUI + +extension ButtonStyle where Self == AppearanceButtonStyle { + static func appearance(selected: Bool, scheme: ColorScheme) -> AppearanceButtonStyle { + AppearanceButtonStyle(selected: selected, scheme: scheme) + } +} + +struct AppearanceButtonStyle: ButtonStyle { + let selected : Bool + let scheme : ColorScheme + + func makeBody(configuration: Configuration) -> some View { + VStack { + let bordered = selected != configuration.isPressed + let color = scheme == .light ? Color.white : Color.black + let content = color + .frame(width: 48, height: 32) + .cornerRadius(5.0) + + if bordered { + content + .overlay( + RoundedRectangle(cornerRadius: 6.0) + .stroke(Color.accentColor, lineWidth: 2.0) + ) + } + else { + content + .shadow(color: .black.opacity(0.5), radius: 1.0, x: 0, y: 1.0) + } + + configuration.label + } + .padding(1.0) + } +} diff --git a/SimDirs/Views/DescriptiveToggleStyle.swift b/SimDirs/Views/Styles/DescriptiveToggleStyle.swift similarity index 100% rename from SimDirs/Views/DescriptiveToggleStyle.swift rename to SimDirs/Views/Styles/DescriptiveToggleStyle.swift diff --git a/SimDirs/Views/Styles/SystemIconButtonStyle.swift b/SimDirs/Views/Styles/SystemIconButtonStyle.swift new file mode 100644 index 0000000..b1a0123 --- /dev/null +++ b/SimDirs/Views/Styles/SystemIconButtonStyle.swift @@ -0,0 +1,64 @@ +// +// SystemIconButtonStyle.swift +// SimDirs +// +// Created by Casey Fleser on 8/21/22. +// + +import SwiftUI + +extension ButtonStyle where Self == SystemIconButtonStyle { + static func systemIcon(_ imageName: String) -> SystemIconButtonStyle { + SystemIconButtonStyle(imageName) + } +} + +struct SystemIconButtonStyle: ButtonStyle { + @State var isFocused = false + let imageName : String + + init(_ imageName: String) { + self.imageName = imageName + } + + func makeBody(configuration: Configuration) -> some View { + VStack { + ZStack { + backgroundColor(pressed: configuration.isPressed) + .frame(width: 36, height: 36) + .cornerRadius(5.0) + + Image(systemName: imageName) + .resizable() + .aspectRatio(contentMode: ContentMode.fit) + .frame(width: 24, height: 24) + } + + configuration.label + } + .foregroundColor(foregroundColor(pressed: configuration.isPressed)) + .padding(1.0) + .onHover { isFocused = $0 } + } + + func foregroundColor(pressed: Bool) -> Color { + return .primary.opacity(pressed ? 1.0 : 0.6) + } + + func backgroundColor(pressed: Bool) -> Color { + return .primary.opacity(0.05 + (isFocused ? 0.1 : 0.0) + (pressed ? 0.1 : 0.0)) + } +} + +struct SystemIconButtonStyle_Previews: PreviewProvider { + static var previews: some View { + Button("Button") { print("do stuff") } + .preferredColorScheme(.light) + .buttonStyle(.systemIcon("camera.on.rectangle")) + .padding(20) + Button("Button") { print("do stuff") } + .preferredColorScheme(.dark) + .buttonStyle(.systemIcon("camera.on.rectangle")) + .padding(20) + } +}