Add in capturing on startup
This commit is contained in:
parent
8c27f34ee1
commit
de60cfb2a1
3 changed files with 211 additions and 43 deletions
|
|
@ -1,28 +1,107 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import AppKit
|
||||||
|
|
||||||
|
class CustomNSView: NSView {
|
||||||
|
private static let invisibleCursor: NSCursor = {
|
||||||
|
let image = NSImage(size: NSSize(width: 1, height: 1))
|
||||||
|
image.lockFocus()
|
||||||
|
NSColor.clear.set()
|
||||||
|
NSRect(x: 0, y: 0, width: 1, height: 1).fill()
|
||||||
|
image.unlockFocus()
|
||||||
|
return NSCursor(image: image, hotSpot: .zero)
|
||||||
|
}()
|
||||||
|
|
||||||
|
override init(frame frameRect: NSRect) {
|
||||||
|
super.init(frame: frameRect)
|
||||||
|
setupView()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
setupView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupView() {
|
||||||
|
wantsLayer = true
|
||||||
|
layer?.backgroundColor = NSColor.clear.cgColor
|
||||||
|
}
|
||||||
|
|
||||||
|
override func resetCursorRects() {
|
||||||
|
super.resetCursorRects()
|
||||||
|
addCursorRect(bounds, cursor: Self.invisibleCursor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HiddenCursorView: NSViewRepresentable {
|
||||||
|
func makeNSView(context: Context) -> CustomNSView {
|
||||||
|
let view = CustomNSView()
|
||||||
|
view.wantsLayer = true
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateNSView(_ nsView: CustomNSView, context: Context) {
|
||||||
|
nsView.setFrameSize(nsView.frame.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CaptureStatusButton: View {
|
||||||
|
let isCapturing: Bool
|
||||||
|
let isBoardDetected: Bool
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack {
|
||||||
|
Circle()
|
||||||
|
.fill(statusColor)
|
||||||
|
.frame(width: 12, height: 12)
|
||||||
|
Text(statusText)
|
||||||
|
.foregroundColor(statusColor)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 8)
|
||||||
|
.stroke(statusColor, lineWidth: 2)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var statusColor: Color {
|
||||||
|
if isCapturing && isBoardDetected {
|
||||||
|
return .green
|
||||||
|
} else if isCapturing {
|
||||||
|
return .yellow
|
||||||
|
} else {
|
||||||
|
return .gray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var statusText: String {
|
||||||
|
if isCapturing && isBoardDetected {
|
||||||
|
return "Capturing"
|
||||||
|
} else if isCapturing {
|
||||||
|
return "Waiting for board"
|
||||||
|
} else {
|
||||||
|
return "Not capturing"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
@StateObject private var viewModel = ScreenCaptureViewModel()
|
@StateObject private var viewModel = ScreenCaptureViewModel()
|
||||||
@State private var showDebugOverlay = false
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
// Capture controls
|
// Capture status indicator
|
||||||
HStack {
|
CaptureStatusButton(
|
||||||
Button(action: {
|
isCapturing: viewModel.isCapturing,
|
||||||
viewModel.startCapture()
|
isBoardDetected: viewModel.isBoardDetected
|
||||||
}) {
|
)
|
||||||
Text("Start Capture")
|
.onAppear {
|
||||||
}
|
// Start monitoring for chess boards when view appears
|
||||||
.disabled(viewModel.isCapturing)
|
viewModel.startMonitoring()
|
||||||
|
}
|
||||||
Button(action: {
|
.onDisappear {
|
||||||
viewModel.stopCapture()
|
// Stop monitoring when view disappears
|
||||||
}) {
|
viewModel.stopMonitoring()
|
||||||
Text("Stop Capture")
|
|
||||||
}
|
|
||||||
.disabled(!viewModel.isCapturing)
|
|
||||||
}
|
}
|
||||||
.padding()
|
|
||||||
|
|
||||||
// Error display
|
// Error display
|
||||||
if let error = viewModel.captureError {
|
if let error = viewModel.captureError {
|
||||||
|
|
@ -55,7 +134,10 @@ struct ContentView: View {
|
||||||
.resizable()
|
.resizable()
|
||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(maxWidth: 400)
|
.frame(maxWidth: 400)
|
||||||
.aspectRatio(contentMode: .fit)
|
.overlay(
|
||||||
|
HiddenCursorView()
|
||||||
|
.allowsHitTesting(true)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
Text("No board detected")
|
Text("No board detected")
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,13 @@ class ScreenCaptureViewModel: ObservableObject {
|
||||||
@Published var croppedBoardImage: NSImage?
|
@Published var croppedBoardImage: NSImage?
|
||||||
@Published var captureError: CaptureError?
|
@Published var captureError: CaptureError?
|
||||||
@Published var isCapturing = false
|
@Published var isCapturing = false
|
||||||
|
@Published var isBoardDetected = false
|
||||||
|
@Published var isAutoCapturing = true // Default to auto-capture mode
|
||||||
|
|
||||||
private let screenCapture = ScreenCapture()
|
private let screenCapture = ScreenCapture()
|
||||||
private let boardDetector = BoardDetector()
|
private let boardDetector = BoardDetector()
|
||||||
private var captureTask: Task<Void, Never>?
|
private var captureTask: Task<Void, Never>?
|
||||||
|
private var monitorTask: Task<Void, Never>?
|
||||||
// Share CIContext to avoid creating too many Metal command queues
|
// Share CIContext to avoid creating too many Metal command queues
|
||||||
private static let shared = CIContext()
|
private static let shared = CIContext()
|
||||||
private var context: CIContext { ScreenCaptureViewModel.shared }
|
private var context: CIContext { ScreenCaptureViewModel.shared }
|
||||||
|
|
@ -22,6 +25,7 @@ class ScreenCaptureViewModel: ObservableObject {
|
||||||
enum CaptureError: LocalizedError {
|
enum CaptureError: LocalizedError {
|
||||||
case chessWindowNotFound
|
case chessWindowNotFound
|
||||||
case boardDetectionFailed
|
case boardDetectionFailed
|
||||||
|
case noBoardDetected
|
||||||
|
|
||||||
var errorDescription: String? {
|
var errorDescription: String? {
|
||||||
switch self {
|
switch self {
|
||||||
|
|
@ -29,11 +33,58 @@ class ScreenCaptureViewModel: ObservableObject {
|
||||||
return "Chess window not found"
|
return "Chess window not found"
|
||||||
case .boardDetectionFailed:
|
case .boardDetectionFailed:
|
||||||
return "Failed to detect chess board"
|
return "Failed to detect chess board"
|
||||||
|
case .noBoardDetected:
|
||||||
|
return "No chess board detected"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func startMonitoring() {
|
||||||
|
guard monitorTask == nil else { return }
|
||||||
|
|
||||||
|
monitorTask = Task {
|
||||||
|
do {
|
||||||
|
// Start continuous monitoring
|
||||||
|
try await screenCapture.startCapture()
|
||||||
|
|
||||||
|
monitorLoop: while !Task.isCancelled {
|
||||||
|
do {
|
||||||
|
if let image = screenCapture.getCurrentImage(),
|
||||||
|
let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) {
|
||||||
|
// Check for board
|
||||||
|
let ciImage = CIImage(cgImage: cgImage)
|
||||||
|
if boardDetector.detectBoard(in: ciImage) != nil {
|
||||||
|
// Board detected, start capture if not already capturing
|
||||||
|
if !isCapturing {
|
||||||
|
startCapture()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try await Task.sleep(nanoseconds: 500_000_000) // 0.5 seconds
|
||||||
|
} catch is CancellationError {
|
||||||
|
break monitorLoop
|
||||||
|
} catch {
|
||||||
|
// Just log the error and continue monitoring
|
||||||
|
print("Monitor error: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try? await screenCapture.stopCapture()
|
||||||
|
} catch {
|
||||||
|
print("Monitor setup error: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopMonitoring() {
|
||||||
|
monitorTask?.cancel()
|
||||||
|
monitorTask = nil
|
||||||
|
stopCapture()
|
||||||
|
}
|
||||||
|
|
||||||
func startCapture() {
|
func startCapture() {
|
||||||
|
guard !isCapturing else { return }
|
||||||
|
|
||||||
isCapturing = true
|
isCapturing = true
|
||||||
captureError = nil
|
captureError = nil
|
||||||
|
|
||||||
|
|
@ -54,6 +105,11 @@ class ScreenCaptureViewModel: ObservableObject {
|
||||||
// Just update the error state but continue capturing
|
// Just update the error state but continue capturing
|
||||||
if !Task.isCancelled {
|
if !Task.isCancelled {
|
||||||
handleCaptureError(error)
|
handleCaptureError(error)
|
||||||
|
if isAutoCapturing && error as? CaptureError == .noBoardDetected {
|
||||||
|
// In auto-capture mode, stop capture but keep monitoring
|
||||||
|
stopCapture()
|
||||||
|
break captureLoop
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -75,6 +131,7 @@ class ScreenCaptureViewModel: ObservableObject {
|
||||||
captureTask = nil
|
captureTask = nil
|
||||||
isCapturing = false
|
isCapturing = false
|
||||||
captureError = nil
|
captureError = nil
|
||||||
|
isBoardDetected = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private func processImage(_ image: NSImage) async throws {
|
private func processImage(_ image: NSImage) async throws {
|
||||||
|
|
@ -86,16 +143,29 @@ class ScreenCaptureViewModel: ObservableObject {
|
||||||
let ciImage = CIImage(cgImage: cgImage)
|
let ciImage = CIImage(cgImage: cgImage)
|
||||||
|
|
||||||
// Detect board
|
// Detect board
|
||||||
guard let boardRect = boardDetector.detectBoard(in: ciImage) else {
|
if let boardRect = boardDetector.detectBoard(in: ciImage) {
|
||||||
throw CaptureError.boardDetectionFailed
|
// Board detected
|
||||||
|
isBoardDetected = true
|
||||||
|
self.detectedBoardRect = boardRect
|
||||||
|
|
||||||
|
// Crop board image
|
||||||
|
let croppedImage = ciImage.cropped(to: boardRect)
|
||||||
|
updateImages(ciImage: ciImage, croppedImage: croppedImage)
|
||||||
|
} else {
|
||||||
|
// No board detected
|
||||||
|
isBoardDetected = false
|
||||||
|
self.detectedBoardRect = nil
|
||||||
|
|
||||||
|
// Only update the full capture image
|
||||||
|
if let cgImage = context.createCGImage(ciImage, from: ciImage.extent) {
|
||||||
|
self.capturedImage = NSImage(cgImage: cgImage, size: .zero)
|
||||||
|
}
|
||||||
|
self.croppedBoardImage = nil
|
||||||
|
|
||||||
|
if isAutoCapturing {
|
||||||
|
throw CaptureError.noBoardDetected
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update detected rectangle
|
|
||||||
self.detectedBoardRect = boardRect
|
|
||||||
|
|
||||||
// Crop board image
|
|
||||||
let croppedImage = ciImage.cropped(to: boardRect)
|
|
||||||
updateImages(ciImage: ciImage, croppedImage: croppedImage)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateImages(ciImage: CIImage, croppedImage: CIImage) {
|
private func updateImages(ciImage: CIImage, croppedImage: CIImage) {
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,34 @@
|
||||||
- Error handling system properly managing states
|
- Error handling system properly managing states
|
||||||
- Resource cleanup implemented
|
- Resource cleanup implemented
|
||||||
- Metal resource management optimized
|
- Metal resource management optimized
|
||||||
|
- Automatic board detection and capture implemented
|
||||||
|
- Visual capture status indicator added
|
||||||
|
- Continuous board monitoring system implemented
|
||||||
|
|
||||||
## Recent Changes
|
## Recent Changes
|
||||||
1. Resource Management Optimization:
|
1. Implemented Continuous Board Monitoring:
|
||||||
|
- Added separate monitoring and capture tasks:
|
||||||
|
* Monitor constantly checks for chess boards (0.5s interval)
|
||||||
|
* Capture processes frames when active (0.1s interval)
|
||||||
|
- Auto-capture behavior:
|
||||||
|
* Starts monitoring when app launches
|
||||||
|
* Automatically starts capture when board appears
|
||||||
|
* Stops capture (but keeps monitoring) when board disappears
|
||||||
|
* Resumes capture when new board is detected
|
||||||
|
- Fixed image conversion pipeline:
|
||||||
|
* Proper NSImage → CGImage → CIImage conversion
|
||||||
|
* Efficient resource management
|
||||||
|
* Clean error handling
|
||||||
|
|
||||||
|
2. Enhanced Status Indication:
|
||||||
|
- Visual status indicator shows capture state:
|
||||||
|
* Green: Actively capturing board
|
||||||
|
* Yellow: Waiting for board
|
||||||
|
* Gray: Not capturing
|
||||||
|
- Clear error messages for different states
|
||||||
|
- Automatic status updates based on board detection
|
||||||
|
|
||||||
|
3. Resource Management Optimization:
|
||||||
- Implemented shared CIContext pattern:
|
- Implemented shared CIContext pattern:
|
||||||
* Prevents command queue exhaustion
|
* Prevents command queue exhaustion
|
||||||
* Reduces Metal resource usage
|
* Reduces Metal resource usage
|
||||||
|
|
@ -17,14 +42,14 @@
|
||||||
- Proper cleanup on task completion
|
- Proper cleanup on task completion
|
||||||
- Efficient resource utilization
|
- Efficient resource utilization
|
||||||
|
|
||||||
2. Improved Window Detection:
|
4. Improved Window Detection:
|
||||||
- Using SCShareableContent for window access
|
- Using SCShareableContent for window access
|
||||||
- Precise window identification:
|
- Precise window identification:
|
||||||
* Exact bundle ID matching (com.chess.iphone)
|
* Exact bundle ID matching (com.chess.iphone)
|
||||||
* Window visibility verification (isOnScreen)
|
* Window visibility verification (isOnScreen)
|
||||||
* Size validation (width > 100 && height > 100)
|
* Size validation (width > 100 && height > 100)
|
||||||
|
|
||||||
3. Enhanced Capture System:
|
5. Enhanced Capture System:
|
||||||
- Continuous capture implementation:
|
- Continuous capture implementation:
|
||||||
* Single persistent capture stream
|
* Single persistent capture stream
|
||||||
* Smooth frame processing (no flickering)
|
* Smooth frame processing (no flickering)
|
||||||
|
|
@ -33,19 +58,11 @@
|
||||||
- Clean task cancellation handling
|
- Clean task cancellation handling
|
||||||
- Main thread safety for UI updates
|
- Main thread safety for UI updates
|
||||||
|
|
||||||
4. UI Simplification:
|
6. Error Handling:
|
||||||
- Removed debug overlay functionality
|
|
||||||
- Cleaner, focused interface
|
|
||||||
- Essential controls only:
|
|
||||||
* Start/Stop capture
|
|
||||||
* Full capture view
|
|
||||||
* Board preview
|
|
||||||
|
|
||||||
5. Error Handling:
|
|
||||||
- Improved error resilience:
|
- Improved error resilience:
|
||||||
* Continues capturing even if board detection fails
|
* Continues monitoring even if capture stops
|
||||||
* Only stops on critical errors (e.g., window not found)
|
* Only stops on critical errors
|
||||||
* Shows error state without interrupting capture
|
* Shows error state without interrupting monitoring
|
||||||
- Clear error states
|
- Clear error states
|
||||||
- Proper async/await usage
|
- Proper async/await usage
|
||||||
- Task cancellation management
|
- Task cancellation management
|
||||||
|
|
@ -68,7 +85,6 @@
|
||||||
5. Integrate Stockfish engine
|
5. Integrate Stockfish engine
|
||||||
|
|
||||||
## Known Issues
|
## Known Issues
|
||||||
- Board detection not yet implemented
|
|
||||||
- Need to handle different chess.com themes
|
- Need to handle different chess.com themes
|
||||||
- Need to implement piece recognition
|
- Need to implement piece recognition
|
||||||
- Position analysis pending implementation
|
- Position analysis pending implementation
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue