mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
Various iOS improvements, remove halucinated bearer, make project sync based
This commit is contained in:
parent
e5a7d22bf4
commit
2a63599ce0
8 changed files with 263 additions and 134 deletions
|
|
@ -18,7 +18,7 @@
|
||||||
78868B612DFF808300B22C15 /* Exceptions for "VibeTunnel" folder in "VibeTunnel" target */ = {
|
78868B612DFF808300B22C15 /* Exceptions for "VibeTunnel" folder in "VibeTunnel" target */ = {
|
||||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||||
membershipExceptions = (
|
membershipExceptions = (
|
||||||
"Resources/Info.plist",
|
Resources/Info.plist,
|
||||||
);
|
);
|
||||||
target = 788687F02DFF4FCB00B22C15 /* VibeTunnel */;
|
target = 788687F02DFF4FCB00B22C15 /* VibeTunnel */;
|
||||||
};
|
};
|
||||||
|
|
@ -463,4 +463,4 @@
|
||||||
/* End XCSwiftPackageProductDependency section */
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = 4C82CEFDA8E4EFE0B37538D7 /* Project object */;
|
rootObject = 4C82CEFDA8E4EFE0B37538D7 /* Project object */;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
25
ios/VibeTunnel/Models/FileInfo.swift
Normal file
25
ios/VibeTunnel/Models/FileInfo.swift
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct FileInfo: Codable {
|
||||||
|
let name: String
|
||||||
|
let path: String
|
||||||
|
let isDir: Bool
|
||||||
|
let size: Int64
|
||||||
|
let mode: String
|
||||||
|
let modTime: Date
|
||||||
|
let mimeType: String
|
||||||
|
let readable: Bool
|
||||||
|
let executable: Bool
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case name
|
||||||
|
case path
|
||||||
|
case isDir = "is_dir"
|
||||||
|
case size
|
||||||
|
case mode
|
||||||
|
case modTime = "mod_time"
|
||||||
|
case mimeType = "mime_type"
|
||||||
|
case readable
|
||||||
|
case executable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,20 +1,5 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// Authentication type for server connections
|
|
||||||
enum AuthType: String, Codable, CaseIterable {
|
|
||||||
case none = "none"
|
|
||||||
case basic = "basic"
|
|
||||||
case bearer = "bearer"
|
|
||||||
|
|
||||||
var displayName: String {
|
|
||||||
switch self {
|
|
||||||
case .none: return "No Authentication"
|
|
||||||
case .basic: return "Basic Auth (Username/Password)"
|
|
||||||
case .bearer: return "Bearer Token"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Configuration for connecting to a VibeTunnel server.
|
/// Configuration for connecting to a VibeTunnel server.
|
||||||
///
|
///
|
||||||
/// ServerConfig stores all necessary information to establish
|
/// ServerConfig stores all necessary information to establish
|
||||||
|
|
@ -25,23 +10,17 @@ struct ServerConfig: Codable, Equatable {
|
||||||
let port: Int
|
let port: Int
|
||||||
let name: String?
|
let name: String?
|
||||||
let password: String?
|
let password: String?
|
||||||
let authType: AuthType
|
|
||||||
let bearerToken: String?
|
|
||||||
|
|
||||||
init(
|
init(
|
||||||
host: String,
|
host: String,
|
||||||
port: Int,
|
port: Int,
|
||||||
name: String? = nil,
|
name: String? = nil,
|
||||||
password: String? = nil,
|
password: String? = nil
|
||||||
authType: AuthType = .none,
|
|
||||||
bearerToken: String? = nil
|
|
||||||
) {
|
) {
|
||||||
self.host = host
|
self.host = host
|
||||||
self.port = port
|
self.port = port
|
||||||
self.name = name
|
self.name = name
|
||||||
self.password = password
|
self.password = password
|
||||||
self.authType = authType
|
|
||||||
self.bearerToken = bearerToken
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Constructs the base URL for API requests.
|
/// Constructs the base URL for API requests.
|
||||||
|
|
@ -67,46 +46,23 @@ struct ServerConfig: Codable, Equatable {
|
||||||
|
|
||||||
/// Indicates whether the server requires authentication.
|
/// Indicates whether the server requires authentication.
|
||||||
///
|
///
|
||||||
/// - Returns: true if authentication is configured, false otherwise.
|
/// - Returns: true if a password is configured, false otherwise.
|
||||||
var requiresAuthentication: Bool {
|
var requiresAuthentication: Bool {
|
||||||
switch authType {
|
if let password {
|
||||||
case .none:
|
return !password.isEmpty
|
||||||
return false
|
|
||||||
case .basic:
|
|
||||||
if let password {
|
|
||||||
return !password.isEmpty
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
case .bearer:
|
|
||||||
if let bearerToken {
|
|
||||||
return !bearerToken.isEmpty
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generates the Authorization header value based on auth type.
|
/// Generates the Authorization header value if a password is configured.
|
||||||
///
|
///
|
||||||
/// - Returns: A properly formatted auth header string,
|
/// - Returns: A Basic auth header string using "admin" as username,
|
||||||
/// or nil if no authentication is configured.
|
/// or nil if no password is configured.
|
||||||
///
|
|
||||||
/// For Basic auth: uses "admin" as the username with the configured password.
|
|
||||||
/// For Bearer auth: uses the configured bearer token.
|
|
||||||
var authorizationHeader: String? {
|
var authorizationHeader: String? {
|
||||||
switch authType {
|
guard let password, !password.isEmpty else { return nil }
|
||||||
case .none:
|
let credentials = "admin:\(password)"
|
||||||
return nil
|
guard let data = credentials.data(using: .utf8) else { return nil }
|
||||||
|
let base64 = data.base64EncodedString()
|
||||||
case .basic:
|
return "Basic \(base64)"
|
||||||
guard let password, !password.isEmpty else { return nil }
|
|
||||||
let credentials = "admin:\(password)"
|
|
||||||
guard let data = credentials.data(using: .utf8) else { return nil }
|
|
||||||
let base64 = data.base64EncodedString()
|
|
||||||
return "Basic \(base64)"
|
|
||||||
|
|
||||||
case .bearer:
|
|
||||||
guard let bearerToken, !bearerToken.isEmpty else { return nil }
|
|
||||||
return "Bearer \(bearerToken)"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,13 +21,13 @@ struct Session: Codable, Identifiable, Equatable {
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case id
|
case id
|
||||||
case command = "cmdline"
|
case command
|
||||||
case workingDir = "cwd"
|
case workingDir
|
||||||
case name
|
case name
|
||||||
case status
|
case status
|
||||||
case exitCode = "exit_code"
|
case exitCode
|
||||||
case startedAt = "started_at"
|
case startedAt
|
||||||
case lastModified = "last_modified"
|
case lastModified
|
||||||
case pid
|
case pid
|
||||||
case waiting
|
case waiting
|
||||||
case width
|
case width
|
||||||
|
|
|
||||||
|
|
@ -456,4 +456,63 @@ class APIClient: APIClientProtocol {
|
||||||
try validateResponse(response)
|
try validateResponse(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func downloadFile(path: String, progressHandler: ((Double) -> Void)? = nil) async throws -> Data {
|
||||||
|
guard let baseURL else {
|
||||||
|
throw APIError.noServerConfigured
|
||||||
|
}
|
||||||
|
|
||||||
|
guard var components = URLComponents(
|
||||||
|
url: baseURL.appendingPathComponent("api/fs/read"),
|
||||||
|
resolvingAgainstBaseURL: false
|
||||||
|
) else {
|
||||||
|
throw APIError.invalidURL
|
||||||
|
}
|
||||||
|
components.queryItems = [URLQueryItem(name: "path", value: path)]
|
||||||
|
|
||||||
|
guard let url = components.url else {
|
||||||
|
throw APIError.invalidURL
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "GET"
|
||||||
|
|
||||||
|
// Add authentication header if needed
|
||||||
|
addAuthenticationIfNeeded(&request)
|
||||||
|
|
||||||
|
// For progress tracking, we'll use URLSession delegate
|
||||||
|
// For now, just download the whole file
|
||||||
|
let (data, response) = try await session.data(for: request)
|
||||||
|
try validateResponse(response)
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFileInfo(path: String) async throws -> FileInfo {
|
||||||
|
guard let baseURL else {
|
||||||
|
throw APIError.noServerConfigured
|
||||||
|
}
|
||||||
|
|
||||||
|
guard var components = URLComponents(
|
||||||
|
url: baseURL.appendingPathComponent("api/fs/info"),
|
||||||
|
resolvingAgainstBaseURL: false
|
||||||
|
) else {
|
||||||
|
throw APIError.invalidURL
|
||||||
|
}
|
||||||
|
components.queryItems = [URLQueryItem(name: "path", value: path)]
|
||||||
|
|
||||||
|
guard let url = components.url else {
|
||||||
|
throw APIError.invalidURL
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "GET"
|
||||||
|
|
||||||
|
// Add authentication header if needed
|
||||||
|
addAuthenticationIfNeeded(&request)
|
||||||
|
|
||||||
|
let (data, response) = try await session.data(for: request)
|
||||||
|
try validateResponse(response)
|
||||||
|
|
||||||
|
return try decoder.decode(FileInfo.self, from: data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
147
ios/VibeTunnel/Services/QuickLookManager.swift
Normal file
147
ios/VibeTunnel/Services/QuickLookManager.swift
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
import Foundation
|
||||||
|
import QuickLook
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
class QuickLookManager: NSObject, ObservableObject {
|
||||||
|
static let shared = QuickLookManager()
|
||||||
|
|
||||||
|
@Published var isPresenting = false
|
||||||
|
@Published var downloadProgress: Double = 0
|
||||||
|
@Published var isDownloading = false
|
||||||
|
|
||||||
|
private var previewItems: [QLPreviewItem] = []
|
||||||
|
private var currentFile: FileEntry?
|
||||||
|
private let temporaryDirectory: URL
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
// Create a temporary directory for downloaded files
|
||||||
|
let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent("QuickLookCache", isDirectory: true)
|
||||||
|
try? FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
|
||||||
|
self.temporaryDirectory = tempDir
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
// Clean up old files on init
|
||||||
|
cleanupTemporaryFiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
func previewFile(_ file: FileEntry, apiClient: APIClient) async throws {
|
||||||
|
guard !file.isDir else {
|
||||||
|
throw QuickLookError.isDirectory
|
||||||
|
}
|
||||||
|
|
||||||
|
currentFile = file
|
||||||
|
isDownloading = true
|
||||||
|
downloadProgress = 0
|
||||||
|
|
||||||
|
do {
|
||||||
|
let localURL = try await downloadFileForPreview(file: file, apiClient: apiClient)
|
||||||
|
|
||||||
|
// Create preview item
|
||||||
|
let previewItem = PreviewItem(url: localURL, title: file.name)
|
||||||
|
previewItems = [previewItem]
|
||||||
|
|
||||||
|
isDownloading = false
|
||||||
|
isPresenting = true
|
||||||
|
} catch {
|
||||||
|
isDownloading = false
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func downloadFileForPreview(file: FileEntry, apiClient: APIClient) async throws -> URL {
|
||||||
|
// Check if file is already cached
|
||||||
|
let cachedURL = temporaryDirectory.appendingPathComponent(file.name)
|
||||||
|
|
||||||
|
// For now, always download fresh (could implement proper caching later)
|
||||||
|
if FileManager.default.fileExists(atPath: cachedURL.path) {
|
||||||
|
try FileManager.default.removeItem(at: cachedURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download the file
|
||||||
|
let data = try await apiClient.downloadFile(path: file.path) { progress in
|
||||||
|
Task { @MainActor in
|
||||||
|
self.downloadProgress = progress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save to temporary location
|
||||||
|
try data.write(to: cachedURL)
|
||||||
|
|
||||||
|
return cachedURL
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanupTemporaryFiles() {
|
||||||
|
// Remove files older than 1 hour
|
||||||
|
let oneHourAgo = Date().addingTimeInterval(-3600)
|
||||||
|
|
||||||
|
guard let files = try? FileManager.default.contentsOfDirectory(at: temporaryDirectory, includingPropertiesForKeys: [.creationDateKey]) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for file in files {
|
||||||
|
if let creationDate = try? file.resourceValues(forKeys: [.creationDateKey]).creationDate,
|
||||||
|
creationDate < oneHourAgo {
|
||||||
|
try? FileManager.default.removeItem(at: file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makePreviewController() -> QLPreviewController {
|
||||||
|
let controller = QLPreviewController()
|
||||||
|
controller.dataSource = self
|
||||||
|
controller.delegate = self
|
||||||
|
return controller
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - QLPreviewControllerDataSource
|
||||||
|
extension QuickLookManager: QLPreviewControllerDataSource {
|
||||||
|
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
|
||||||
|
previewItems.count
|
||||||
|
}
|
||||||
|
|
||||||
|
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
|
||||||
|
previewItems[index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - QLPreviewControllerDelegate
|
||||||
|
extension QuickLookManager: QLPreviewControllerDelegate {
|
||||||
|
nonisolated func previewControllerDidDismiss(_ controller: QLPreviewController) {
|
||||||
|
Task { @MainActor in
|
||||||
|
isPresenting = false
|
||||||
|
previewItems = []
|
||||||
|
currentFile = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Preview Item
|
||||||
|
private class PreviewItem: NSObject, QLPreviewItem {
|
||||||
|
let previewItemURL: URL?
|
||||||
|
let previewItemTitle: String?
|
||||||
|
|
||||||
|
init(url: URL, title: String) {
|
||||||
|
self.previewItemURL = url
|
||||||
|
self.previewItemTitle = title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Errors
|
||||||
|
enum QuickLookError: LocalizedError {
|
||||||
|
case isDirectory
|
||||||
|
case downloadFailed
|
||||||
|
case unsupportedFileType
|
||||||
|
|
||||||
|
var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case .isDirectory:
|
||||||
|
return "Cannot preview directories"
|
||||||
|
case .downloadFailed:
|
||||||
|
return "Failed to download file"
|
||||||
|
case .unsupportedFileType:
|
||||||
|
return "This file type cannot be previewed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,7 +6,8 @@ import SwiftUI
|
||||||
/// Displays the app branding and provides interface for entering
|
/// Displays the app branding and provides interface for entering
|
||||||
/// server connection details with saved server management.
|
/// server connection details with saved server management.
|
||||||
struct ConnectionView: View {
|
struct ConnectionView: View {
|
||||||
@Environment(ConnectionManager.self) var connectionManager
|
@Environment(ConnectionManager.self)
|
||||||
|
var connectionManager
|
||||||
@ObservedObject private var networkMonitor = NetworkMonitor.shared
|
@ObservedObject private var networkMonitor = NetworkMonitor.shared
|
||||||
@State private var viewModel = ConnectionViewModel()
|
@State private var viewModel = ConnectionViewModel()
|
||||||
@State private var logoScale: CGFloat = 0.8
|
@State private var logoScale: CGFloat = 0.8
|
||||||
|
|
@ -67,8 +68,6 @@ struct ConnectionView: View {
|
||||||
port: $viewModel.port,
|
port: $viewModel.port,
|
||||||
name: $viewModel.name,
|
name: $viewModel.name,
|
||||||
password: $viewModel.password,
|
password: $viewModel.password,
|
||||||
authType: $viewModel.authType,
|
|
||||||
bearerToken: $viewModel.bearerToken,
|
|
||||||
isConnecting: viewModel.isConnecting,
|
isConnecting: viewModel.isConnecting,
|
||||||
errorMessage: viewModel.errorMessage,
|
errorMessage: viewModel.errorMessage,
|
||||||
onConnect: connectToServer
|
onConnect: connectToServer
|
||||||
|
|
@ -115,8 +114,6 @@ class ConnectionViewModel {
|
||||||
var port: String = "4020"
|
var port: String = "4020"
|
||||||
var name: String = ""
|
var name: String = ""
|
||||||
var password: String = ""
|
var password: String = ""
|
||||||
var authType: AuthType = .none
|
|
||||||
var bearerToken: String = ""
|
|
||||||
var isConnecting: Bool = false
|
var isConnecting: Bool = false
|
||||||
var errorMessage: String?
|
var errorMessage: String?
|
||||||
|
|
||||||
|
|
@ -128,8 +125,6 @@ class ConnectionViewModel {
|
||||||
self.port = String(serverConfig.port)
|
self.port = String(serverConfig.port)
|
||||||
self.name = serverConfig.name ?? ""
|
self.name = serverConfig.name ?? ""
|
||||||
self.password = serverConfig.password ?? ""
|
self.password = serverConfig.password ?? ""
|
||||||
self.authType = serverConfig.authType
|
|
||||||
self.bearerToken = serverConfig.bearerToken ?? ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -153,9 +148,7 @@ class ConnectionViewModel {
|
||||||
host: host,
|
host: host,
|
||||||
port: portNumber,
|
port: portNumber,
|
||||||
name: name.isEmpty ? nil : name,
|
name: name.isEmpty ? nil : name,
|
||||||
password: password.isEmpty ? nil : password,
|
password: password.isEmpty ? nil : password
|
||||||
authType: authType,
|
|
||||||
bearerToken: bearerToken.isEmpty ? nil : bearerToken
|
|
||||||
)
|
)
|
||||||
|
|
||||||
do {
|
do {
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,6 @@ struct ServerConfigForm: View {
|
||||||
@Binding var port: String
|
@Binding var port: String
|
||||||
@Binding var name: String
|
@Binding var name: String
|
||||||
@Binding var password: String
|
@Binding var password: String
|
||||||
@Binding var authType: AuthType
|
|
||||||
@Binding var bearerToken: String
|
|
||||||
let isConnecting: Bool
|
let isConnecting: Bool
|
||||||
let errorMessage: String?
|
let errorMessage: String?
|
||||||
let onConnect: () -> Void
|
let onConnect: () -> Void
|
||||||
|
|
@ -24,7 +22,6 @@ struct ServerConfigForm: View {
|
||||||
case port
|
case port
|
||||||
case name
|
case name
|
||||||
case password
|
case password
|
||||||
case bearerToken
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|
@ -79,66 +76,20 @@ struct ServerConfigForm: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authentication Type
|
// Password Field (optional)
|
||||||
VStack(alignment: .leading, spacing: Theme.Spacing.small) {
|
VStack(alignment: .leading, spacing: Theme.Spacing.small) {
|
||||||
Label("Authentication", systemImage: "lock.shield")
|
Label("Password (optional)", systemImage: "lock")
|
||||||
.font(Theme.Typography.terminalSystem(size: 12))
|
.font(Theme.Typography.terminalSystem(size: 12))
|
||||||
.foregroundColor(Theme.Colors.primaryAccent)
|
.foregroundColor(Theme.Colors.primaryAccent)
|
||||||
|
|
||||||
Picker("Auth Type", selection: $authType) {
|
SecureField("Enter password", text: $password)
|
||||||
ForEach(AuthType.allCases, id: \.self) { type in
|
.textFieldStyle(TerminalTextFieldStyle())
|
||||||
Text(type.displayName).tag(type)
|
.focused($focusedField, equals: .password)
|
||||||
|
.submitLabel(.done)
|
||||||
|
.onSubmit {
|
||||||
|
focusedField = nil
|
||||||
|
onConnect()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.pickerStyle(.segmented)
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: Theme.CornerRadius.small)
|
|
||||||
.stroke(Theme.Colors.cardBorder, lineWidth: 1)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Password Field (for Basic Auth)
|
|
||||||
if authType == .basic {
|
|
||||||
VStack(alignment: .leading, spacing: Theme.Spacing.small) {
|
|
||||||
Label("Password", systemImage: "lock")
|
|
||||||
.font(Theme.Typography.terminalSystem(size: 12))
|
|
||||||
.foregroundColor(Theme.Colors.primaryAccent)
|
|
||||||
|
|
||||||
SecureField("Enter password", text: $password)
|
|
||||||
.textFieldStyle(TerminalTextFieldStyle())
|
|
||||||
.focused($focusedField, equals: .password)
|
|
||||||
.submitLabel(.done)
|
|
||||||
.onSubmit {
|
|
||||||
focusedField = nil
|
|
||||||
onConnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.transition(.asymmetric(
|
|
||||||
insertion: .scale.combined(with: .opacity),
|
|
||||||
removal: .scale.combined(with: .opacity)
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bearer Token Field
|
|
||||||
if authType == .bearer {
|
|
||||||
VStack(alignment: .leading, spacing: Theme.Spacing.small) {
|
|
||||||
Label("Bearer Token", systemImage: "key")
|
|
||||||
.font(Theme.Typography.terminalSystem(size: 12))
|
|
||||||
.foregroundColor(Theme.Colors.primaryAccent)
|
|
||||||
|
|
||||||
SecureField("Enter bearer token", text: $bearerToken)
|
|
||||||
.textFieldStyle(TerminalTextFieldStyle())
|
|
||||||
.focused($focusedField, equals: .bearerToken)
|
|
||||||
.submitLabel(.done)
|
|
||||||
.onSubmit {
|
|
||||||
focusedField = nil
|
|
||||||
onConnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.transition(.asymmetric(
|
|
||||||
insertion: .scale.combined(with: .opacity),
|
|
||||||
removal: .scale.combined(with: .opacity)
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
|
|
@ -224,8 +175,6 @@ struct ServerConfigForm: View {
|
||||||
port = String(server.port)
|
port = String(server.port)
|
||||||
name = server.name ?? ""
|
name = server.name ?? ""
|
||||||
password = server.password ?? ""
|
password = server.password ?? ""
|
||||||
authType = server.authType
|
|
||||||
bearerToken = server.bearerToken ?? ""
|
|
||||||
HapticFeedback.selection()
|
HapticFeedback.selection()
|
||||||
}, label: {
|
}, label: {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue