import SwiftUI import AppKit struct ContentView: View { @EnvironmentObject var store: ProfileStore @StateObject private var terminals = TerminalManager() @State private var selection: ConnectionProfile.ID? @State private var openSessions: [ConnectionProfile] = [] @State private var activeSessionID: ConnectionProfile.ID? @State private var showingEditor = false @State private var editingProfile: ConnectionProfile? @State private var showingKeys = false @State private var statusMessage = "Bereit." var body: some View { NavigationSplitView { // Sidebar wie Remmina: Gruppen + Profile VStack(spacing: 0) { Picker("Gruppe", selection: $store.selectedGroup) { ForEach(store.groups, id: \.self) { g in Text(g).tag(g) } } .pickerStyle(.menu) .padding(8) SearchField(text: $store.searchText) .padding(.horizontal, 8) List(store.filteredProfiles, selection: $selection) { p in HStack { Image(systemName: p.proto.systemImage) .foregroundStyle(color(for: p.colorTag)) .frame(width: 22) VStack(alignment: .leading) { Text(p.name).font(.headline) Text("\(p.username)@\(p.host):\(p.port) · \(p.proto.rawValue)") .font(.caption) .foregroundStyle(.secondary) } Spacer() } .tag(p.id) .contextMenu { Button("Verbinden") { connect(p) } Button("Duplizieren") { store.duplicate(p) } Button("Bearbeiten") { edit(p) } Button("Löschen", role: .destructive) { store.delete(p) } } } .onDoubleClick { if let p = selectedProfile { connect(p) } } } .frame(minWidth: 280) .toolbar { ToolbarItemGroup(placement: .primaryAction) { Button(action: addProfile) { Image(systemName: "plus") } .help("Neues Profil") Button(action: { if let p = selectedProfile { edit(p) } }) { Image(systemName: "pencil") } .help("Bearbeiten").disabled(selection == nil) Button(action: { if let p = selectedProfile { store.duplicate(p) } }) { Image(systemName: "plus.square.on.square") } .help("Duplizieren").disabled(selection == nil) Button(action: { showingKeys = true }) { Image(systemName: "key") } .help("SSH-Keys verwalten") } } } detail: { VStack(spacing: 0) { // Verbindungs-Tabs if openSessions.isEmpty { emptyState } else { TabView(selection: $activeSessionID) { ForEach(openSessions) { session in SessionView( profile: session, statusMessage: $statusMessage, term: terminals.session(for: session, key: store.key(for: session)), onClose: { closeSession(session) } ) .tag(Optional(session.id)) .tabItem { Text(session.name) } } } } Divider() // Statusbar wie Remmina HStack { Image(systemName: "circle.fill").foregroundStyle(.green).font(.caption2) Text(statusMessage).font(.caption).foregroundStyle(.secondary) Spacer() Text("\(store.profiles.count) Profile · \(openSessions.count) Sitzungen") .font(.caption).foregroundStyle(.secondary) } .padding(6) .background(.bar) } .toolbar { ToolbarItem(placement: .navigation) { Button(action: { if let p = selectedProfile { connect(p) } }) { Label("Verbinden", systemImage: "play.fill") }.disabled(selection == nil) } ToolbarItem { Button(action: quickConnect) { Label("Quick-Connect", systemImage: "bolt") } } } } .sheet(item: $editingProfile) { profile in ProfileEditorView(profile: profile) { saved in if store.profiles.contains(where: { $0.id == saved.id }) { store.update(saved) } else { store.add(saved) } statusMessage = "Profil '\(saved.name)' gespeichert." } } .sheet(isPresented: $showingKeys) { SSHKeysView().environmentObject(store) } } var selectedProfile: ConnectionProfile? { store.profiles.first { $0.id == selection } ?? store.filteredProfiles.first { $0.id == selection } } var emptyState: some View { VStack(spacing: 12) { Image(systemName: "rectangle.connected.to.line.below").font(.system(size: 48)).foregroundStyle(.secondary) Text("Kein Profil geöffnet").font(.title2) Text("Wähle links ein Profil und klicke Verbinden (Doppelklick geht auch).").foregroundStyle(.secondary) Button("Neues Profil anlegen", action: addProfile) } .frame(maxWidth: .infinity, maxHeight: .infinity) } func addProfile() { editingProfile = ConnectionProfile() } func edit(_ p: ConnectionProfile) { editingProfile = p } func connect(_ p: ConnectionProfile) { var updated = p updated.lastConnected = Date() store.update(updated) if !openSessions.contains(where: { $0.id == p.id }) { openSessions.append(updated) } activeSessionID = p.id switch p.proto { case .ssh, .sftp: // In-App-Terminal verbindet automatisch (onAppear) — kein externes Fenster nötig statusMessage = "SSH-Terminal zu \(p.username)@\(p.host) …" case .rdp: RDPService.connect(profile: p) statusMessage = "RDP zu \(p.host):\(p.port) gestartet." case .vnc: RDPService.connectVNC(profile: p) statusMessage = "VNC zu \(p.host):\(p.port) geöffnet." } } func closeSession(_ p: ConnectionProfile) { terminals.close(p) openSessions.removeAll { $0.id == p.id } if activeSessionID == p.id { activeSessionID = openSessions.last?.id } statusMessage = "Sitzung '\(p.name)' geschlossen." } func quickConnect() { // Simple Quick-Connect: nimmt Auswahl oder leeres SSH-Profil if let p = selectedProfile { connect(p) } else { editingProfile = ConnectionProfile(name: "Quick-Connect") } } func color(for tag: String) -> Color { switch tag { case "red": return .red case "green": return .green case "orange": return .orange case "purple": return .purple default: return .blue } } } struct SearchField: View { @Binding var text: String var body: some View { HStack { Image(systemName: "magnifyingglass").foregroundStyle(.secondary) TextField("Suchen …", text: $text) .textFieldStyle(.plain) if !text.isEmpty { Button(action: { text = "" }) { Image(systemName: "xmark.circle.fill") } .buttonStyle(.plain).foregroundStyle(.secondary) } } .padding(6) .background(.quaternary.opacity(0.5), in: RoundedRectangle(cornerRadius: 8)) } } extension View { func onDoubleClick(perform action: @escaping () -> Void) -> some View { background(DoubleClickHandler(action: action)) } } struct DoubleClickHandler: NSViewRepresentable { var action: () -> Void func makeNSView(context: Context) -> NSClickView { let v = NSClickView() v.onDoubleClick = action return v } func updateNSView(_ nsView: NSClickView, context: Context) { nsView.onDoubleClick = action } } final class NSClickView: NSView { var onDoubleClick: (() -> Void)? override func mouseDown(with event: NSEvent) { if event.clickCount == 2 { onDoubleClick?() } super.mouseDown(with: event) } }