ContentView.swift 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. import SwiftUI
  2. struct ContentView: View {
  3. @StateObject private var networkService = NetworkService()
  4. @State private var targetAngle: Double = 30
  5. @State private var showingConnectionSettings = false
  6. @State private var customHost = ""
  7. @State private var customPort = ""
  8. @State private var streamImage: UIImage? = nil
  9. @State private var isStreaming = false
  10. @State private var isVideoStreaming = false
  11. @State private var isAutoMode = true
  12. var body: some View {
  13. NavigationView {
  14. VStack(spacing: 20) {
  15. // Connection Status
  16. HStack {
  17. Circle()
  18. .fill(networkService.isConnected ? Color.green : Color.red)
  19. .frame(width: 12, height: 12)
  20. Text(networkService.connectionStatus)
  21. .font(.headline)
  22. Spacer()
  23. Button(action: {
  24. showingConnectionSettings = true
  25. }) {
  26. Image(systemName: "gearshape.fill")
  27. .font(.title2)
  28. .foregroundColor(.blue)
  29. }
  30. .frame(width: 44, height: 44)
  31. }
  32. .padding()
  33. // Video Stream Area
  34. VStack {
  35. HStack {
  36. Text("Camera Feed")
  37. .font(.headline)
  38. Spacer()
  39. Button(isVideoStreaming ? "Stop Stream" : "Start Stream") {
  40. toggleVideoStream()
  41. }
  42. .font(.caption)
  43. .padding(.horizontal, 12)
  44. .padding(.vertical, 6)
  45. .background(isVideoStreaming ? Color.red : Color.green)
  46. .foregroundColor(.white)
  47. .cornerRadius(15)
  48. }
  49. ZStack {
  50. Rectangle()
  51. .fill(Color.black.opacity(0.1))
  52. .frame(height: 200)
  53. .cornerRadius(10)
  54. if let image = streamImage {
  55. Image(uiImage: image)
  56. .resizable()
  57. .aspectRatio(contentMode: .fit)
  58. .frame(height: 200)
  59. .cornerRadius(10)
  60. } else {
  61. VStack {
  62. Image(systemName: isVideoStreaming ? "video.fill" : "camera.fill")
  63. .font(.largeTitle)
  64. .foregroundColor(.gray)
  65. Text(isVideoStreaming ? "Live feed - Tap to capture photo" : (isStreaming ? "Capturing photo..." : "Tap to capture photo"))
  66. .font(.caption)
  67. .foregroundColor(.gray)
  68. }
  69. }
  70. }
  71. .onTapGesture {
  72. capturePhoto()
  73. }
  74. }
  75. .padding()
  76. // Control Panel
  77. VStack(spacing: 25) {
  78. // Mode Toggle
  79. HStack {
  80. Text("Mode:")
  81. .font(.headline)
  82. Spacer()
  83. Toggle(isOn: $isAutoMode) {
  84. Text(isAutoMode ? "Auto" : "Manual")
  85. .font(.headline)
  86. .foregroundColor(isAutoMode ? .green : .orange)
  87. }
  88. .onChange(of: isAutoMode) { _, newValue in
  89. let mode = newValue ? "auto" : "manual"
  90. let command = LauncherCommand.setMode(mode)
  91. networkService.sendCommand(command)
  92. }
  93. }
  94. .padding(.horizontal)
  95. // Home Button
  96. Button(action: homeDevice) {
  97. HStack {
  98. Image(systemName: "house.fill")
  99. Text("Home Device")
  100. }
  101. .font(.headline)
  102. .foregroundColor(.white)
  103. .frame(maxWidth: .infinity)
  104. .padding()
  105. .background(Color.purple)
  106. .cornerRadius(10)
  107. }
  108. .disabled(!networkService.isConnected)
  109. // Angle Control
  110. VStack {
  111. Text("Target Angle: \(Int(targetAngle))°")
  112. .font(.title2)
  113. Slider(value: $targetAngle, in: 0...60, step: 1)
  114. .accentColor(.blue)
  115. }
  116. // Aim Controls
  117. HStack(spacing: 20) {
  118. Button(action: aimLeft) {
  119. HStack {
  120. Image(systemName: "arrow.left")
  121. Text("Aim Left")
  122. }
  123. .font(.headline)
  124. .foregroundColor(.white)
  125. .frame(maxWidth: .infinity)
  126. .padding()
  127. .background(Color.blue)
  128. .cornerRadius(10)
  129. }
  130. .disabled(!networkService.isConnected)
  131. Button(action: aimRight) {
  132. HStack {
  133. Image(systemName: "arrow.right")
  134. Text("Aim Right")
  135. }
  136. .font(.headline)
  137. .foregroundColor(.white)
  138. .frame(maxWidth: .infinity)
  139. .padding()
  140. .background(Color.blue)
  141. .cornerRadius(10)
  142. }
  143. .disabled(!networkService.isConnected)
  144. }
  145. // Fire Button
  146. Button(action: fireOreo) {
  147. HStack {
  148. Image(systemName: "paperplane.fill")
  149. Text("FIRE OREO!")
  150. }
  151. .font(.title)
  152. .fontWeight(.bold)
  153. .foregroundColor(.white)
  154. .frame(maxWidth: .infinity)
  155. .padding(.vertical, 15)
  156. .background(Color.red)
  157. .cornerRadius(10)
  158. }
  159. .disabled(!networkService.isConnected)
  160. }
  161. .padding()
  162. Spacer()
  163. // Connection Button
  164. Button(action: toggleConnection) {
  165. Text(networkService.isConnected ? "Disconnect" : "Connect to Launcher")
  166. .font(.headline)
  167. .foregroundColor(.white)
  168. .frame(maxWidth: .infinity)
  169. .padding()
  170. .background(networkService.isConnected ? Color.gray : Color.green)
  171. .cornerRadius(10)
  172. }
  173. .padding()
  174. }
  175. .navigationTitle("🍪 OCTv2")
  176. .navigationBarTitleDisplayMode(.inline)
  177. }
  178. .sheet(isPresented: $showingConnectionSettings) {
  179. ConnectionSettingsView(
  180. networkService: networkService,
  181. customHost: $customHost,
  182. customPort: $customPort
  183. )
  184. }
  185. }
  186. // MARK: - Actions
  187. private func toggleConnection() {
  188. if networkService.isConnected {
  189. networkService.disconnect()
  190. } else {
  191. let host = customHost.isEmpty ? nil : customHost
  192. let port = UInt16(customPort) ?? 0
  193. networkService.connect(host: host ?? "", port: port)
  194. }
  195. }
  196. private func aimLeft() {
  197. let command = LauncherCommand.aimLeft()
  198. networkService.sendCommand(command)
  199. print("Aiming left")
  200. }
  201. private func aimRight() {
  202. let command = LauncherCommand.aimRight()
  203. networkService.sendCommand(command)
  204. print("Aiming right")
  205. }
  206. private func fireOreo() {
  207. let command = LauncherCommand.fire(angle: targetAngle)
  208. networkService.sendCommand(command)
  209. print("Firing Oreo at angle: \(targetAngle)°")
  210. }
  211. private func capturePhoto() {
  212. guard networkService.isConnected else {
  213. print("Cannot capture photo: Not connected to Pi")
  214. return
  215. }
  216. isStreaming = true
  217. let command = LauncherCommand.capturePhoto()
  218. networkService.sendCommand(command)
  219. networkService.requestPhoto { image in
  220. DispatchQueue.main.async {
  221. if image == nil {
  222. print("Failed to capture photo")
  223. }
  224. // If we're not video streaming, show the captured photo
  225. // If we are streaming, the photo was saved on Pi but video continues
  226. if !self.isVideoStreaming {
  227. self.streamImage = image
  228. }
  229. self.isStreaming = false
  230. }
  231. }
  232. print(isVideoStreaming ? "Capturing high-res photo while streaming" : "Capturing photo")
  233. }
  234. private func homeDevice() {
  235. let command = LauncherCommand.home()
  236. networkService.sendCommand(command)
  237. print("Homing device")
  238. }
  239. private func toggleVideoStream() {
  240. if isVideoStreaming {
  241. networkService.stopVideoStream()
  242. isVideoStreaming = false
  243. streamImage = nil
  244. } else {
  245. isVideoStreaming = true
  246. networkService.startVideoStream { image in
  247. self.streamImage = image
  248. }
  249. }
  250. }
  251. }
  252. struct ContentView_Previews: PreviewProvider {
  253. static var previews: some View {
  254. ContentView()
  255. }
  256. }