mirror of
https://github.com/somegeekintn/SimDirs.git
synced 2026-04-27 14:57:40 +00:00
Add ability to record screen although not 100% satisfied with it.
This commit is contained in:
parent
7b02bd3da2
commit
4d67317ebb
4 changed files with 93 additions and 20 deletions
|
|
@ -8,14 +8,24 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct SimCtl {
|
struct SimCtl {
|
||||||
func run(args: [String]) throws -> Data {
|
func run(args: [String], run: Bool = true) throws -> Process {
|
||||||
let process = Process()
|
let process = Process()
|
||||||
let pipe = Pipe()
|
|
||||||
|
|
||||||
process.executableURL = URL(fileURLWithPath: "/usr/bin/xcrun")
|
process.executableURL = URL(fileURLWithPath: "/usr/bin/xcrun")
|
||||||
process.arguments = ["simctl"] + args
|
process.arguments = ["simctl"] + args
|
||||||
process.standardOutput = pipe
|
|
||||||
process.standardError = nil
|
process.standardError = nil
|
||||||
|
if run {
|
||||||
|
try process.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
return process
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(args: [String]) throws -> Data {
|
||||||
|
let process : Process = try run(args: args, run: false)
|
||||||
|
let pipe = Pipe()
|
||||||
|
|
||||||
|
process.standardOutput = pipe
|
||||||
try process.run()
|
try process.run()
|
||||||
|
|
||||||
return pipe.fileHandleForReading.readDataToEndOfFile()
|
return pipe.fileHandleForReading.readDataToEndOfFile()
|
||||||
|
|
@ -119,4 +129,8 @@ struct SimCtl {
|
||||||
func saveScreen(_ device: SimDevice, url: URL) throws {
|
func saveScreen(_ device: SimDevice, url: URL) throws {
|
||||||
try runAsync(args: ["io", device.udid, "screenshot", url.path])
|
try runAsync(args: ["io", device.udid, "screenshot", url.path])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func saveVideo(_ device: SimDevice, url: URL) throws -> Process {
|
||||||
|
return try run(args: ["io", device.udid, "recordVideo", "--force", url.path])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,8 @@ class SimDevice: ObservableObject, Decodable {
|
||||||
@Published var appearance = Appearance.unknown
|
@Published var appearance = Appearance.unknown
|
||||||
@Published var contentSize = ContentSize.unknown
|
@Published var contentSize = ContentSize.unknown
|
||||||
@Published var increaseContrast = IncreaseContrast.unknown
|
@Published var increaseContrast = IncreaseContrast.unknown
|
||||||
|
@Published var isRecording = false
|
||||||
|
var recordingProcess : Process?
|
||||||
var isTransitioning : Bool { state == .booting || state == .shuttingDown }
|
var isTransitioning : Bool { state == .booting || state == .shuttingDown }
|
||||||
var isBooted : Bool {
|
var isBooted : Bool {
|
||||||
get { state.showBooted == true }
|
get { state.showBooted == true }
|
||||||
|
|
@ -190,6 +192,28 @@ class SimDevice: ObservableObject, Decodable {
|
||||||
print("Failed to save screen: \(error)")
|
print("Failed to save screen: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func saveVideo(_ url: URL) {
|
||||||
|
do {
|
||||||
|
recordingProcess = try SimCtl().saveVideo(self, url: url)
|
||||||
|
if recordingProcess != nil {
|
||||||
|
isRecording = true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("Failed to save video: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func endRecording() {
|
||||||
|
if let process = recordingProcess {
|
||||||
|
process.interrupt()
|
||||||
|
recordingProcess = nil
|
||||||
|
isRecording = false
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
isRecording = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SimDevice {
|
extension SimDevice {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
extension SimDevice {
|
extension SimDevice {
|
||||||
public var content : some View { DeviceContent(self) }
|
public var content : some View { DeviceContent(self) }
|
||||||
|
|
@ -45,6 +46,25 @@ extension SimDevice.IncreaseContrast: ToggleDescriptor {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DeviceContent: View {
|
struct DeviceContent: View {
|
||||||
|
enum SaveType {
|
||||||
|
case image
|
||||||
|
case video
|
||||||
|
|
||||||
|
var allowedContentTypes : [UTType] {
|
||||||
|
switch self {
|
||||||
|
case .image: return [.png]
|
||||||
|
case .video: return [.mpeg4Movie]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var title : String {
|
||||||
|
switch self {
|
||||||
|
case .image: return "Save Screen"
|
||||||
|
case .video: return "Save Recording"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ObservedObject var device : SimDevice
|
@ObservedObject var device : SimDevice
|
||||||
@State var isBooted : Bool
|
@State var isBooted : Bool
|
||||||
|
|
||||||
|
|
@ -77,6 +97,23 @@ struct DeviceContent: View {
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.textSelection(.enabled)
|
.textSelection(.enabled)
|
||||||
|
|
||||||
|
ContentHeader("Actions")
|
||||||
|
HStack(spacing: 16) {
|
||||||
|
Button(action: { saveScreen(.image) }) {
|
||||||
|
Text("Save Screen")
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.font(.system(size: 11))
|
||||||
|
}
|
||||||
|
.buttonStyle(.systemIcon("camera.on.rectangle"))
|
||||||
|
|
||||||
|
Button(action: { device.isRecording ? device.endRecording() : saveScreen(.video) }) {
|
||||||
|
Text(device.isRecording ? "End Recording" : "Record Screen")
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.font(.system(size: 11))
|
||||||
|
}
|
||||||
|
.buttonStyle(.systemIcon("record.circle", active: device.isRecording))
|
||||||
|
}
|
||||||
|
|
||||||
ContentHeader("UI")
|
ContentHeader("UI")
|
||||||
HStack(spacing: 16) {
|
HStack(spacing: 16) {
|
||||||
if device.appearance != .unsupported {
|
if device.appearance != .unsupported {
|
||||||
|
|
@ -99,14 +136,6 @@ struct DeviceContent: View {
|
||||||
.opacity(isBooted ? 1.0 : 0.5)
|
.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)
|
.environment(\.isEnabled, isBooted)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
|
|
@ -124,21 +153,24 @@ struct DeviceContent: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveScreen() {
|
func saveScreen(_ type: SaveType = .image) {
|
||||||
let savePanel = NSSavePanel()
|
let savePanel = NSSavePanel()
|
||||||
|
|
||||||
savePanel.allowedContentTypes = [.png]
|
savePanel.allowedContentTypes = type.allowedContentTypes
|
||||||
savePanel.canCreateDirectories = true
|
savePanel.canCreateDirectories = true
|
||||||
savePanel.isExtensionHidden = false
|
savePanel.isExtensionHidden = false
|
||||||
savePanel.title = "Save Screen"
|
savePanel.title = type.title
|
||||||
savePanel.message = "Select destination"
|
savePanel.message = "Select destination"
|
||||||
savePanel.nameFieldLabel = "Filename:"
|
savePanel.nameFieldLabel = "Filename:"
|
||||||
savePanel.nameFieldStringValue = "\(device.name) - \(fileDateFormatter.string(from: Date()))"
|
savePanel.nameFieldStringValue = "\(device.name) - \(fileDateFormatter.string(from: Date()))"
|
||||||
|
|
||||||
if savePanel.runModal() == .OK {
|
if savePanel.runModal() == .OK {
|
||||||
if let url = savePanel.url {
|
if let url = savePanel.url {
|
||||||
device.saveScreen(url)
|
switch type {
|
||||||
|
case .image: device.saveScreen(url)
|
||||||
|
case .video: device.saveVideo(url)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,17 +8,19 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
extension ButtonStyle where Self == SystemIconButtonStyle {
|
extension ButtonStyle where Self == SystemIconButtonStyle {
|
||||||
static func systemIcon(_ imageName: String) -> SystemIconButtonStyle {
|
static func systemIcon(_ imageName: String, active: Bool = false) -> SystemIconButtonStyle {
|
||||||
SystemIconButtonStyle(imageName)
|
SystemIconButtonStyle(imageName, active: active)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SystemIconButtonStyle: ButtonStyle {
|
struct SystemIconButtonStyle: ButtonStyle {
|
||||||
@State var isFocused = false
|
@State var isFocused = false
|
||||||
|
let isActive : Bool
|
||||||
let imageName : String
|
let imageName : String
|
||||||
|
|
||||||
init(_ imageName: String) {
|
init(_ imageName: String, active: Bool = false) {
|
||||||
self.imageName = imageName
|
self.imageName = imageName
|
||||||
|
self.isActive = active
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeBody(configuration: Configuration) -> some View {
|
func makeBody(configuration: Configuration) -> some View {
|
||||||
|
|
@ -32,11 +34,12 @@ struct SystemIconButtonStyle: ButtonStyle {
|
||||||
.resizable()
|
.resizable()
|
||||||
.aspectRatio(contentMode: ContentMode.fit)
|
.aspectRatio(contentMode: ContentMode.fit)
|
||||||
.frame(width: 24, height: 24)
|
.frame(width: 24, height: 24)
|
||||||
|
.foregroundColor(isActive ? .accentColor : foregroundColor(pressed: configuration.isPressed))
|
||||||
}
|
}
|
||||||
|
|
||||||
configuration.label
|
configuration.label
|
||||||
|
.foregroundColor(foregroundColor(pressed: configuration.isPressed))
|
||||||
}
|
}
|
||||||
.foregroundColor(foregroundColor(pressed: configuration.isPressed))
|
|
||||||
.padding(1.0)
|
.padding(1.0)
|
||||||
.onHover { isFocused = $0 }
|
.onHover { isFocused = $0 }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue