mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
Add window size animation for Settings and build script for tty-fwd
- Implement smooth window size animations when switching Settings tabs - Add Xcode build phase to automatically build tty-fwd universal binary - Configure build script with proper input/output dependencies for efficiency - Fix Settings window animation using NSViewAnimation API - Update project configuration for optimized builds
This commit is contained in:
parent
2324be0706
commit
066d2882f9
13 changed files with 170 additions and 111 deletions
|
|
@ -126,6 +126,7 @@
|
||||||
788687ED2DFF4FCB00B22C15 /* Sources */,
|
788687ED2DFF4FCB00B22C15 /* Sources */,
|
||||||
788687EE2DFF4FCB00B22C15 /* Frameworks */,
|
788687EE2DFF4FCB00B22C15 /* Frameworks */,
|
||||||
788687EF2DFF4FCB00B22C15 /* Resources */,
|
788687EF2DFF4FCB00B22C15 /* Resources */,
|
||||||
|
A189466CB0AD49BEBE16B954 /* Build tty-fwd Universal Binary */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
|
|
@ -261,6 +262,34 @@
|
||||||
};
|
};
|
||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
|
A189466CB0AD49BEBE16B954 /* Build tty-fwd Universal Binary */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"$(SRCROOT)/tty-fwd/src/main.rs",
|
||||||
|
"$(SRCROOT)/tty-fwd/src/protocol.rs",
|
||||||
|
"$(SRCROOT)/tty-fwd/src/tty_spawn.rs",
|
||||||
|
"$(SRCROOT)/tty-fwd/src/utils.rs",
|
||||||
|
"$(SRCROOT)/tty-fwd/Cargo.toml",
|
||||||
|
"$(SRCROOT)/tty-fwd/build-universal.sh",
|
||||||
|
);
|
||||||
|
name = "Build tty-fwd Universal Binary";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
"$(BUILT_PRODUCTS_DIR)/$(CONTENTS_FOLDER_PATH)/Resources/tty-fwd",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "# Build tty-fwd universal binary\necho \"Building tty-fwd universal binary...\"\n\n# Get the project directory\nPROJECT_DIR=\"${SRCROOT}\"\nTTY_FWD_DIR=\"${PROJECT_DIR}/tty-fwd\"\nBUILD_SCRIPT=\"${TTY_FWD_DIR}/build-universal.sh\"\nSOURCE_BINARY=\"${TTY_FWD_DIR}/target/release/tty-fwd-universal\"\nDEST_BINARY=\"${BUILT_PRODUCTS_DIR}/${CONTENTS_FOLDER_PATH}/Resources/tty-fwd\"\n\n# Check if build script exists\nif [ ! -f \"${BUILD_SCRIPT}\" ]; then\n echo \"error: Build script not found at ${BUILD_SCRIPT}\"\n exit 1\nfi\n\n# Make build script executable\nchmod +x \"${BUILD_SCRIPT}\"\n\n# Change to tty-fwd directory and run build\ncd \"${TTY_FWD_DIR}\"\n./build-universal.sh\n\n# Check if build succeeded\nif [ ! -f \"${SOURCE_BINARY}\" ]; then\n echo \"error: Universal binary not found at ${SOURCE_BINARY}\"\n exit 1\nfi\n\n# Create Resources directory if it doesn't exist\nmkdir -p \"${BUILT_PRODUCTS_DIR}/${CONTENTS_FOLDER_PATH}/Resources\"\n\n# Copy the binary\ncp \"${SOURCE_BINARY}\" \"${DEST_BINARY}\"\nchmod +x \"${DEST_BINARY}\"\n\necho \"tty-fwd universal binary copied to ${DEST_BINARY}\"\n";
|
||||||
|
};
|
||||||
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
788687ED2DFF4FCB00B22C15 /* Sources */ = {
|
788687ED2DFF4FCB00B22C15 /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
|
|
@ -361,6 +390,8 @@
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
SWIFT_STRICT_CONCURRENCY = complete;
|
||||||
|
SWIFT_VERSION = 6.0;
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
|
|
@ -418,6 +449,8 @@
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
SWIFT_STRICT_CONCURRENCY = complete;
|
||||||
|
SWIFT_VERSION = 6.0;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
|
@ -450,7 +483,6 @@
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = sh.vibetunnel.vibetunnel;
|
PRODUCT_BUNDLE_IDENTIFIER = sh.vibetunnel.vibetunnel;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
|
|
@ -483,7 +515,6 @@
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = sh.vibetunnel.vibetunnel;
|
PRODUCT_BUNDLE_IDENTIFIER = sh.vibetunnel.vibetunnel;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
|
@ -501,7 +532,6 @@
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = sh.vibetunnel.vibetunnelTests;
|
PRODUCT_BUNDLE_IDENTIFIER = sh.vibetunnel.vibetunnelTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/VibeTunnel.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/VibeTunnel";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/VibeTunnel.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/VibeTunnel";
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
|
|
@ -520,7 +550,6 @@
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = sh.vibetunnel.vibetunnelTests;
|
PRODUCT_BUNDLE_IDENTIFIER = sh.vibetunnel.vibetunnelTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/VibeTunnel.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/VibeTunnel";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/VibeTunnel.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/VibeTunnel";
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
|
|
@ -537,7 +566,6 @@
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.steipete.VibeTunnelUITests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.steipete.VibeTunnelUITests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
TEST_TARGET_NAME = VibeTunnel;
|
TEST_TARGET_NAME = VibeTunnel;
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -22,7 +22,7 @@ public struct TunnelSession: Identifiable, Codable, Sendable {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Request to create a new terminal session
|
/// Request to create a new terminal session
|
||||||
public struct CreateSessionRequest: Codable {
|
public struct CreateSessionRequest: Codable, Sendable {
|
||||||
public let workingDirectory: String?
|
public let workingDirectory: String?
|
||||||
public let environment: [String: String]?
|
public let environment: [String: String]?
|
||||||
public let shell: String?
|
public let shell: String?
|
||||||
|
|
@ -35,7 +35,7 @@ public struct CreateSessionRequest: Codable {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Response after creating a session
|
/// Response after creating a session
|
||||||
public struct CreateSessionResponse: Codable {
|
public struct CreateSessionResponse: Codable, Sendable {
|
||||||
public let sessionId: String
|
public let sessionId: String
|
||||||
public let createdAt: Date
|
public let createdAt: Date
|
||||||
|
|
||||||
|
|
@ -46,7 +46,7 @@ public struct CreateSessionResponse: Codable {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Command execution request
|
/// Command execution request
|
||||||
public struct CommandRequest: Codable {
|
public struct CommandRequest: Codable, Sendable {
|
||||||
public let sessionId: String
|
public let sessionId: String
|
||||||
public let command: String
|
public let command: String
|
||||||
public let args: [String]?
|
public let args: [String]?
|
||||||
|
|
@ -61,7 +61,7 @@ public struct CommandRequest: Codable {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Command execution response
|
/// Command execution response
|
||||||
public struct CommandResponse: Codable {
|
public struct CommandResponse: Codable, Sendable {
|
||||||
public let sessionId: String
|
public let sessionId: String
|
||||||
public let output: String?
|
public let output: String?
|
||||||
public let error: String?
|
public let error: String?
|
||||||
|
|
@ -84,7 +84,7 @@ public struct CommandResponse: Codable {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Session information
|
/// Session information
|
||||||
public struct SessionInfo: Codable {
|
public struct SessionInfo: Codable, Sendable {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let createdAt: Date
|
public let createdAt: Date
|
||||||
public let lastActivity: Date
|
public let lastActivity: Date
|
||||||
|
|
@ -99,7 +99,7 @@ public struct SessionInfo: Codable {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// List sessions response
|
/// List sessions response
|
||||||
public struct ListSessionsResponse: Codable {
|
public struct ListSessionsResponse: Codable, Sendable {
|
||||||
public let sessions: [SessionInfo]
|
public let sessions: [SessionInfo]
|
||||||
|
|
||||||
public init(sessions: [SessionInfo]) {
|
public init(sessions: [SessionInfo]) {
|
||||||
|
|
@ -134,7 +134,7 @@ extension TunnelSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Request to create a new session
|
/// Request to create a new session
|
||||||
public struct CreateRequest: Codable {
|
public struct CreateRequest: Codable, Sendable {
|
||||||
public let clientInfo: ClientInfo?
|
public let clientInfo: ClientInfo?
|
||||||
|
|
||||||
public init(clientInfo: ClientInfo? = nil) {
|
public init(clientInfo: ClientInfo? = nil) {
|
||||||
|
|
@ -143,7 +143,7 @@ extension TunnelSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Response after creating a session
|
/// Response after creating a session
|
||||||
public struct CreateResponse: Codable {
|
public struct CreateResponse: Codable, Sendable {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let session: TunnelSession
|
public let session: TunnelSession
|
||||||
|
|
||||||
|
|
@ -154,7 +154,7 @@ extension TunnelSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Request to execute a command
|
/// Request to execute a command
|
||||||
public struct ExecuteCommandRequest: Codable {
|
public struct ExecuteCommandRequest: Codable, Sendable {
|
||||||
public let sessionId: String
|
public let sessionId: String
|
||||||
public let command: String
|
public let command: String
|
||||||
public let environment: [String: String]?
|
public let environment: [String: String]?
|
||||||
|
|
@ -174,7 +174,7 @@ extension TunnelSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Response from command execution
|
/// Response from command execution
|
||||||
public struct ExecuteCommandResponse: Codable {
|
public struct ExecuteCommandResponse: Codable, Sendable {
|
||||||
public let exitCode: Int32
|
public let exitCode: Int32
|
||||||
public let stdout: String
|
public let stdout: String
|
||||||
public let stderr: String
|
public let stderr: String
|
||||||
|
|
@ -187,7 +187,7 @@ extension TunnelSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Health check response
|
/// Health check response
|
||||||
public struct HealthResponse: Codable {
|
public struct HealthResponse: Codable, Sendable {
|
||||||
public let status: String
|
public let status: String
|
||||||
public let timestamp: Date
|
public let timestamp: Date
|
||||||
public let sessions: Int
|
public let sessions: Int
|
||||||
|
|
@ -202,7 +202,7 @@ extension TunnelSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// List sessions response
|
/// List sessions response
|
||||||
public struct ListResponse: Codable {
|
public struct ListResponse: Codable, Sendable {
|
||||||
public let sessions: [TunnelSession]
|
public let sessions: [TunnelSession]
|
||||||
|
|
||||||
public init(sessions: [TunnelSession]) {
|
public init(sessions: [TunnelSession]) {
|
||||||
|
|
@ -211,7 +211,7 @@ extension TunnelSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Error response from server
|
/// Error response from server
|
||||||
public struct ErrorResponse: Codable {
|
public struct ErrorResponse: Codable, Sendable {
|
||||||
public let error: String
|
public let error: String
|
||||||
public let code: String?
|
public let code: String?
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import UserNotifications
|
||||||
/// Stub implementation of SparkleUpdaterManager
|
/// Stub implementation of SparkleUpdaterManager
|
||||||
/// TODO: Add Sparkle dependency through Xcode Package Manager and restore full implementation
|
/// TODO: Add Sparkle dependency through Xcode Package Manager and restore full implementation
|
||||||
@available(macOS 10.15, *)
|
@available(macOS 10.15, *)
|
||||||
|
@MainActor
|
||||||
public final class SparkleUpdaterManager: NSObject {
|
public final class SparkleUpdaterManager: NSObject {
|
||||||
|
|
||||||
public static let shared = SparkleUpdaterManager()
|
public static let shared = SparkleUpdaterManager()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import os.log
|
import os.log
|
||||||
|
|
||||||
|
@MainActor
|
||||||
final class TTYForwardManager {
|
final class TTYForwardManager {
|
||||||
static let shared = TTYForwardManager()
|
static let shared = TTYForwardManager()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -109,9 +109,12 @@ public final class TunnelServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sessions endpoint - calls tty-fwd --list-sessions
|
// Sessions endpoint - calls tty-fwd --list-sessions
|
||||||
router.get("/sessions") { _, _ -> Response in
|
router.get("/sessions") { _, _ async -> Response in
|
||||||
let ttyManager = TTYForwardManager.shared
|
let process = await MainActor.run {
|
||||||
guard let process = ttyManager.createTTYForwardProcess(with: ["--list-sessions"]) else {
|
TTYForwardManager.shared.createTTYForwardProcess(with: ["--list-sessions"])
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let process = process else {
|
||||||
self.logger.error("Failed to create tty-fwd process")
|
self.logger.error("Failed to create tty-fwd process")
|
||||||
let errorJson = "{\"error\": \"tty-fwd binary not found\"}"
|
let errorJson = "{\"error\": \"tty-fwd binary not found\"}"
|
||||||
var buffer = ByteBuffer()
|
var buffer = ByteBuffer()
|
||||||
|
|
@ -259,4 +262,4 @@ public final class TunnelServer {
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ struct SettingsView: View {
|
||||||
.tag(SettingsTab.about)
|
.tag(SettingsTab.about)
|
||||||
}
|
}
|
||||||
.frame(width: contentSize.width, height: contentSize.height)
|
.frame(width: contentSize.width, height: contentSize.height)
|
||||||
.animatedWindowContainer(size: contentSize)
|
.animatedWindowSizing(size: contentSize)
|
||||||
.onReceive(NotificationCenter.default.publisher(for: .openSettingsTab)) { notification in
|
.onReceive(NotificationCenter.default.publisher(for: .openSettingsTab)) { notification in
|
||||||
if let tab = notification.object as? SettingsTab {
|
if let tab = notification.object as? SettingsTab {
|
||||||
selectedTab = tab
|
selectedTab = tab
|
||||||
|
|
|
||||||
|
|
@ -11,4 +11,8 @@
|
||||||
// Default values (can be overridden in Local.xcconfig)
|
// Default values (can be overridden in Local.xcconfig)
|
||||||
// These will be used if Local.xcconfig doesn't exist or doesn't define them
|
// These will be used if Local.xcconfig doesn't exist or doesn't define them
|
||||||
DEVELOPMENT_TEAM = $(inherited)
|
DEVELOPMENT_TEAM = $(inherited)
|
||||||
CODE_SIGN_STYLE = $(inherited)
|
CODE_SIGN_STYLE = $(inherited)
|
||||||
|
|
||||||
|
// Swift version and concurrency settings
|
||||||
|
SWIFT_VERSION = 6.0
|
||||||
|
SWIFT_STRICT_CONCURRENCY = complete
|
||||||
|
|
@ -2,6 +2,7 @@ import AppKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
/// Window controller for the About window
|
/// Window controller for the About window
|
||||||
|
@MainActor
|
||||||
final class AboutWindowController {
|
final class AboutWindowController {
|
||||||
static let shared = AboutWindowController()
|
static let shared = AboutWindowController()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,86 +0,0 @@
|
||||||
import SwiftUI
|
|
||||||
import AppKit
|
|
||||||
|
|
||||||
/// A view that enables animated window resizing for Settings windows
|
|
||||||
struct AnimatedWindowContainer<Content: View>: NSViewRepresentable {
|
|
||||||
let content: Content
|
|
||||||
let targetSize: CGSize
|
|
||||||
|
|
||||||
class Coordinator: NSObject {
|
|
||||||
var lastSize: CGSize = .zero
|
|
||||||
weak var window: NSWindow?
|
|
||||||
var isAnimating = false
|
|
||||||
|
|
||||||
func animateWindowResize(to newSize: CGSize) {
|
|
||||||
guard let window = window,
|
|
||||||
newSize != lastSize,
|
|
||||||
!isAnimating else { return }
|
|
||||||
|
|
||||||
lastSize = newSize
|
|
||||||
isAnimating = true
|
|
||||||
|
|
||||||
// Calculate the new frame maintaining the window's top-left position
|
|
||||||
var newFrame = window.frame
|
|
||||||
let heightDifference = newSize.height - newFrame.height
|
|
||||||
newFrame.size = newSize
|
|
||||||
newFrame.origin.y -= heightDifference // Keep top edge in place
|
|
||||||
|
|
||||||
// Animate the window frame change
|
|
||||||
NSAnimationContext.runAnimationGroup({ context in
|
|
||||||
context.duration = 0.25
|
|
||||||
context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
|
||||||
window.animator().setFrame(newFrame, display: true)
|
|
||||||
}, completionHandler: {
|
|
||||||
self.isAnimating = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeCoordinator() -> Coordinator {
|
|
||||||
Coordinator()
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeNSView(context: Context) -> NSView {
|
|
||||||
let view = NSView()
|
|
||||||
view.wantsLayer = true
|
|
||||||
view.layer?.backgroundColor = NSColor.clear.cgColor
|
|
||||||
|
|
||||||
// Use async to ensure window is available
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
if let window = view.window {
|
|
||||||
context.coordinator.window = window
|
|
||||||
// Disable automatic window resizing
|
|
||||||
window.styleMask.remove(.resizable)
|
|
||||||
// Set initial size
|
|
||||||
context.coordinator.lastSize = window.frame.size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateNSView(_ nsView: NSView, context: Context) {
|
|
||||||
// Ensure we have a window
|
|
||||||
if context.coordinator.window == nil, let window = nsView.window {
|
|
||||||
context.coordinator.window = window
|
|
||||||
window.styleMask.remove(.resizable)
|
|
||||||
context.coordinator.lastSize = window.frame.size
|
|
||||||
}
|
|
||||||
|
|
||||||
// Animate to the new size
|
|
||||||
context.coordinator.animateWindowResize(to: targetSize)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extension to make it easy to use with any SwiftUI view
|
|
||||||
extension View {
|
|
||||||
func animatedWindowContainer(size: CGSize) -> some View {
|
|
||||||
background(
|
|
||||||
AnimatedWindowContainer(
|
|
||||||
content: Color.clear,
|
|
||||||
targetSize: size
|
|
||||||
)
|
|
||||||
.allowsHitTesting(false)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
105
VibeTunnel/Utilities/WindowSizeAnimator.swift
Normal file
105
VibeTunnel/Utilities/WindowSizeAnimator.swift
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
import SwiftUI
|
||||||
|
import AppKit
|
||||||
|
|
||||||
|
/// A custom window size animator that works with SwiftUI Settings windows
|
||||||
|
@MainActor
|
||||||
|
final class WindowSizeAnimator: ObservableObject {
|
||||||
|
static let shared = WindowSizeAnimator()
|
||||||
|
|
||||||
|
private weak var window: NSWindow?
|
||||||
|
private var animator: NSViewAnimation?
|
||||||
|
|
||||||
|
private init() {}
|
||||||
|
|
||||||
|
/// Find and store reference to the settings window
|
||||||
|
func captureSettingsWindow() {
|
||||||
|
// Try multiple strategies to find the window
|
||||||
|
if let window = NSApp.windows.first(where: { window in
|
||||||
|
// Check if it's a settings-like window
|
||||||
|
window.isVisible &&
|
||||||
|
window.level == .normal &&
|
||||||
|
!window.isKind(of: NSPanel.self) &&
|
||||||
|
window.canBecomeKey &&
|
||||||
|
(window.title.isEmpty || window.title.contains("VibeTunnel") ||
|
||||||
|
window.title.lowercased().contains("settings") ||
|
||||||
|
window.title.lowercased().contains("preferences"))
|
||||||
|
}) {
|
||||||
|
self.window = window
|
||||||
|
// Disable user resizing
|
||||||
|
window.styleMask.remove(.resizable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Animate window to new size using NSViewAnimation
|
||||||
|
func animateWindowSize(to newSize: CGSize, duration: TimeInterval = 0.25) {
|
||||||
|
guard let window = window else {
|
||||||
|
// Try to capture window if we haven't yet
|
||||||
|
captureSettingsWindow()
|
||||||
|
guard self.window != nil else { return }
|
||||||
|
animateWindowSize(to: newSize, duration: duration)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel any existing animation
|
||||||
|
animator?.stop()
|
||||||
|
|
||||||
|
// Calculate new frame keeping top-left corner fixed
|
||||||
|
var newFrame = window.frame
|
||||||
|
let heightDifference = newSize.height - newFrame.height
|
||||||
|
newFrame.size = newSize
|
||||||
|
newFrame.origin.y -= heightDifference
|
||||||
|
|
||||||
|
// Create animation dictionary
|
||||||
|
let windowDict: [NSViewAnimation.Key: Any] = [
|
||||||
|
.target: window,
|
||||||
|
.startFrame: window.frame,
|
||||||
|
.endFrame: newFrame
|
||||||
|
]
|
||||||
|
|
||||||
|
// Create and configure animation
|
||||||
|
let animation = NSViewAnimation(viewAnimations: [windowDict])
|
||||||
|
animation.animationBlockingMode = .nonblocking
|
||||||
|
animation.animationCurve = .easeInOut
|
||||||
|
animation.duration = duration
|
||||||
|
|
||||||
|
// Store animator reference
|
||||||
|
self.animator = animation
|
||||||
|
|
||||||
|
// Start animation
|
||||||
|
animation.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A view modifier that captures the window and enables animated resizing
|
||||||
|
struct AnimatedWindowSizing: ViewModifier {
|
||||||
|
let size: CGSize
|
||||||
|
@StateObject private var animator = WindowSizeAnimator.shared
|
||||||
|
|
||||||
|
func body(content: Content) -> some View {
|
||||||
|
content
|
||||||
|
.onAppear {
|
||||||
|
// Capture window after a delay to ensure it's created
|
||||||
|
Task {
|
||||||
|
try? await Task.sleep(for: .milliseconds(100))
|
||||||
|
await MainActor.run {
|
||||||
|
animator.captureSettingsWindow()
|
||||||
|
// Set initial size without animation
|
||||||
|
if let window = NSApp.keyWindow {
|
||||||
|
var frame = window.frame
|
||||||
|
frame.size = size
|
||||||
|
window.setFrame(frame, display: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: size) { _, newSize in
|
||||||
|
animator.animateWindowSize(to: newSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
func animatedWindowSizing(size: CGSize) -> some View {
|
||||||
|
modifier(AnimatedWindowSizing(size: size))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -130,7 +130,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
DistributedNotificationCenter.default().post(name: Self.showSettingsNotification, object: nil)
|
DistributedNotificationCenter.default().post(name: Self.showSettingsNotification, object: nil)
|
||||||
|
|
||||||
// Show alert that another instance is running
|
// Show alert that another instance is running
|
||||||
DispatchQueue.main.async {
|
Task { @MainActor in
|
||||||
let alert = NSAlert()
|
let alert = NSAlert()
|
||||||
alert.messageText = "VibeTunnel is already running"
|
alert.messageText = "VibeTunnel is already running"
|
||||||
alert
|
alert
|
||||||
|
|
@ -202,6 +202,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shows the About section in the Settings window
|
/// Shows the About section in the Settings window
|
||||||
|
@MainActor
|
||||||
private func showAboutInSettings() {
|
private func showAboutInSettings() {
|
||||||
NSApp.openSettings()
|
NSApp.openSettings()
|
||||||
Task {
|
Task {
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,7 @@ struct MenuButtonStyle: ButtonStyle {
|
||||||
// MARK: - Helper Functions
|
// MARK: - Helper Functions
|
||||||
|
|
||||||
/// Shows the About section in the Settings window
|
/// Shows the About section in the Settings window
|
||||||
|
@MainActor
|
||||||
private func showAboutInSettings() {
|
private func showAboutInSettings() {
|
||||||
NSApp.openSettings()
|
NSApp.openSettings()
|
||||||
Task {
|
Task {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue