diff --git a/Checks/app-startup.swift b/Checks/app-startup.swift index b29075a..11a6198 100644 --- a/Checks/app-startup.swift +++ b/Checks/app-startup.swift @@ -37,6 +37,8 @@ private enum WindowDragCheck { static var implementations: [(Method, IMP)] = [] static var target: NSWindow? static var dragDepth = 0 + static var urlWindowFrame = NSRect.zero + static let urlDraft = "https://www.youtube.com/watch?v=aqz-KE-bpKQ" static let names = ["video", "title", "playback padding", "play button", "volume slider", "start whitespace", "start button", "start artwork", "URL text field", "queue whitespace", "queue footer", "queue row", "queue button", @@ -76,6 +78,17 @@ private enum WindowDragCheck { return nil } + static func clickStartURL(in window: NSWindow) { + for type in [NSEvent.EventType.leftMouseDown, .leftMouseUp] { + let event = NSEvent.mouseEvent(with: type, location: NSPoint(x: 400, y: 272), + modifierFlags: [], timestamp: ProcessInfo.processInfo.systemUptime, + windowNumber: window.windowNumber, context: nil, + eventNumber: probe * 100 + phase * 2 + (type == .leftMouseDown ? 0 : 1), + clickCount: 1, pressure: type == .leftMouseDown ? 1 : 0)! + NSApp.postEvent(NSEvent(cgEvent: event.cgEvent!)!, atStart: false) + } + } + static func point(in window: NSWindow) -> NSPoint? { let content = window.contentView! let w = content.bounds.width @@ -150,11 +163,13 @@ private enum WindowDragCheck { model.setVolume(0) model.setPlayback(playing: false) window.makeKeyAndOrderFront(nil) + window.makeMain() NSApp.activate(ignoringOtherApps: true) } if phase == 0 { - guard NSApp.isActive, window.isKeyWindow else { + guard NSApp.isActive, window.isKeyWindow, window.isMainWindow else { window.makeKeyAndOrderFront(nil) + window.makeMain() NSApp.activate(ignoringOtherApps: true) return } @@ -162,13 +177,8 @@ private enum WindowDragCheck { switch probe { case 5: model.closeFile() case 8: - for type in [NSEvent.EventType.leftMouseDown, .leftMouseUp] { - let event = NSEvent.mouseEvent(with: type, location: NSPoint(x: 400, y: 272), - modifierFlags: [], timestamp: ProcessInfo.processInfo.systemUptime, - windowNumber: window.windowNumber, context: nil, eventNumber: type == .leftMouseDown ? 1 : 2, - clickCount: 1, pressure: type == .leftMouseDown ? 1 : 0)! - NSApp.postEvent(NSEvent(cgEvent: event.cgEvent!)!, atStart: false) - } + urlWindowFrame = window.frame + clickStartURL(in: window) case 9: model.open(fixtures.appendingPathComponent("cache.mp4")) model.showsQueue = true @@ -205,10 +215,37 @@ private enum WindowDragCheck { guard drags - previousDrags == expected else { finish("\(names[probe]): expected \(expected) native drag requests, got \(drags - previousDrags)"); return } + if probe == 8 { + guard let editor = window.firstResponder as? NSTextView, editor.isFieldEditor else { + finish("Revealing the URL field did not focus its editor"); return + } + editor.insertText(urlDraft, replacementRange: NSRange(location: NSNotFound, length: 0)) + clickStartURL(in: window) + phase = 7 + return + } probe += 1 if probe == names.count { finish(); return } phase = 0 return + } else if probe == 8 && phase == 7 { + guard (window.firstResponder as? NSTextView)?.isFieldEditor != true else { + finish("Collapsing the URL field retained keyboard focus"); return + } + clickStartURL(in: window) // Reverse the collapse before its animation completes. + } else if probe == 8 && phase == 8 { + guard let editor = window.firstResponder as? NSTextView, editor.isFieldEditor else { return } + guard editor.string == urlDraft else { finish("Reopening the URL field lost its draft"); return } + clickStartURL(in: window) + } else if probe == 8 && phase == 9 { + guard nativeView(in: window.contentView!, matching: { ($0 as? NSTextField)?.isEditable == true }) == nil else { return } + guard (window.firstResponder as? NSTextView)?.isFieldEditor != true, + window.frame == urlWindowFrame, drags == previousDrags else { + finish("URL-field transitions retained focus, resized or dragged the start window"); return + } + probe += 1 + phase = 0 + return } phase += 1 } diff --git a/README.md b/README.md index b34af76..bbaa9e8 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,8 @@ The start screen uses a fixed 640 × 400-point two-pane window: Terebi's identit Start-screen actions use 14-point text and begin 64 points below the top edge. Branding uses a 16-point app name and 12-point version; the drop hint uses 13-point text. Open actions have no idle fill; their background appears on hover or press. +URL-field expansion and collapse use a 250 ms ease-in/out fade with a subtle top-anchored scale; recent rows move with the transition without resizing the window. Reduce Motion makes the change immediate. Hiding or rapidly toggling the field preserves its draft, and hiding releases keyboard focus. + | Action | Default shortcut | | --- | --- | | Play / pause | Space or K | diff --git a/Sources/App/TerebiApp.swift b/Sources/App/TerebiApp.swift index fb9fcdd..acf6036 100644 --- a/Sources/App/TerebiApp.swift +++ b/Sources/App/TerebiApp.swift @@ -2189,7 +2189,10 @@ private struct PlayerContent: View { } .buttonStyle(StartScreenButtonStyle()) .accessibilityLabel("Open YouTube URL…") - if showsYouTubeInput { startURLInput.padding(.horizontal, 14) } + if showsYouTubeInput { + startURLInput.padding(.horizontal, 14) + .transition(.opacity.combined(with: .scale(scale: 0.96, anchor: .top))) + } if let latest, let resumePosition { Button { model.open(latest.url) } label: { HStack(spacing: 8) { @@ -2224,6 +2227,7 @@ private struct PlayerContent: View { .padding(.top, 20) } } + .animation(reduceMotion ? nil : .easeInOut(duration: 0.25), value: showsYouTubeInput) .font(.system(size: 14, weight: .medium)) .disabled(!model.canOpen) .padding(.horizontal, size.width < 600 ? 14 : 30) diff --git a/TODO.md b/TODO.md index feedfb1..8a908db 100644 --- a/TODO.md +++ b/TODO.md @@ -13,6 +13,7 @@ - [x] make the window draggable (in all cases/view states) so long as a button/clickable element is not hovered. - scoped native background dragging across start/playback/queue/error/Cast/compact views, with passive loading/audio overlays passing input through; 20 native input-routing probes passed, including controls and row-reorder exclusions; physical pointer dragging and Cast-screen interaction still need manual verification - [ ] on the start screen: - - [ ] make the youtube url input field that appears when clicking the youtube button appear with a nice transition + - [x] make the youtube url input field that appears when clicking the youtube button appear with a nice transition + - 250 ms fade/scale with animated row reflow; native focus, draft-preservation and rapid-reversal checks passed, including an isolated Reduce Motion branch smoke check; animation feel still needs manual verification - [ ] change the youtube button to general streaming - we can figure out the stream and how to handle it from the url (i.e. if youtube url, handle as youtube stream; if miruro url, handle accordingly) \ No newline at end of file