Replace server tabs with one window and a server picker
This commit is contained in:
@@ -24,7 +24,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
if flag {
|
||||
return true
|
||||
}
|
||||
Servers.shared.openAll()
|
||||
Servers.shared.openWindow?()
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -64,8 +64,8 @@ struct ByreApp: App {
|
||||
@NSApplicationDelegateAdaptor private var delegate: AppDelegate
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup(id: "main", for: String.self) { $origin in
|
||||
ServerWindow(origin: $origin)
|
||||
Window("byre", id: "main") {
|
||||
ServerWindow()
|
||||
}
|
||||
.defaultSize(width: 1280, height: 820)
|
||||
.restorationBehavior(.disabled)
|
||||
@@ -86,23 +86,16 @@ struct ByreCommands: Commands {
|
||||
Updater.shared.check()
|
||||
}
|
||||
Button("Forget Server") {
|
||||
focus?.forget?()
|
||||
focus?.forget()
|
||||
}
|
||||
.disabled(focus?.forget == nil)
|
||||
.disabled(focus == nil)
|
||||
}
|
||||
CommandGroup(replacing: .newItem) {
|
||||
Button("New Server Window") {
|
||||
Button("Add Server…") {
|
||||
Servers.shared.current = nil
|
||||
openWindow(id: "main")
|
||||
}
|
||||
.keyboardShortcut("n")
|
||||
Menu("Open Server") {
|
||||
ForEach(Servers.shared.origins, id: \.self) { origin in
|
||||
Button(URL(string: origin)?.host() ?? origin) {
|
||||
openWindow(id: "main", value: origin)
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled(Servers.shared.origins.isEmpty)
|
||||
}
|
||||
PasteboardCommands()
|
||||
CommandMenu("Go") {
|
||||
@@ -119,9 +112,7 @@ struct ByreCommands: Commands {
|
||||
|
||||
private func serverButton(_ label: String, _ key: Character, _ step: Int) -> some View {
|
||||
Button(label) {
|
||||
if let origin = Servers.shared.step(from: focus?.origin, by: step) {
|
||||
openWindow(id: "main", value: origin)
|
||||
}
|
||||
Servers.shared.current = Servers.shared.step(from: Servers.shared.current, by: step)
|
||||
}
|
||||
.keyboardShortcut(KeyEquivalent(key))
|
||||
.disabled(Servers.shared.origins.isEmpty)
|
||||
|
||||
@@ -12,7 +12,7 @@ final class Notifier: NSObject, UNUserNotificationCenterDelegate {
|
||||
|
||||
private func open(_ origin: String, _ taskId: String) {
|
||||
NSApp.activate()
|
||||
Servers.shared.openWindow?(origin)
|
||||
Servers.shared.show(origin)
|
||||
pending = (origin, taskId)
|
||||
deliver()
|
||||
}
|
||||
|
||||
+62
-108
@@ -11,13 +11,12 @@ final class Servers {
|
||||
|
||||
private(set) var entries: [(origin: String, record: ServerRecord)] = []
|
||||
private(set) var connections: [String: Connection] = [:]
|
||||
@ObservationIgnored var shown: Set<String> = []
|
||||
@ObservationIgnored var windows: [String: WindowSlot] = [:]
|
||||
@ObservationIgnored var launched = false
|
||||
@ObservationIgnored var openWindow: ((String?) -> Void)?
|
||||
var current: String?
|
||||
@ObservationIgnored var openWindow: (() -> Void)?
|
||||
|
||||
private init() {
|
||||
load()
|
||||
current = origins.first
|
||||
Task {
|
||||
for await note in NotificationCenter.default.notifications(named: NSPersistentCloudKitContainer.eventChangedNotification) {
|
||||
if let event = note.userInfo?[NSPersistentCloudKitContainer.eventNotificationUserInfoKey] as? NSPersistentCloudKitContainer.Event, event.type == .import, event.endDate != nil {
|
||||
@@ -50,6 +49,9 @@ final class Servers {
|
||||
}
|
||||
}
|
||||
entries = loaded
|
||||
if let current, record(current) == nil {
|
||||
self.current = origins.first
|
||||
}
|
||||
for (origin, _) in loaded where connections[origin] == nil {
|
||||
connections[origin] = Connection(origin: parseOrigin(origin)!)
|
||||
}
|
||||
@@ -93,31 +95,17 @@ final class Servers {
|
||||
return origins[(i + step + origins.count) % origins.count]
|
||||
}
|
||||
|
||||
func arrange(_ group: NSWindowTabGroup?) {
|
||||
guard let group else {
|
||||
return
|
||||
}
|
||||
let selected = group.selectedWindow
|
||||
let ordered = origins.compactMap { windows[$0]?.window }.filter { group.windows.contains($0) }
|
||||
for (i, window) in ordered.enumerated() where group.windows[i] !== window {
|
||||
group.insertWindow(window, at: i)
|
||||
}
|
||||
group.selectedWindow = selected
|
||||
}
|
||||
|
||||
func openAll() {
|
||||
if origins.isEmpty {
|
||||
openWindow?(nil)
|
||||
}
|
||||
for origin in origins {
|
||||
openWindow?(origin)
|
||||
func show(_ origin: String) {
|
||||
if record(origin) != nil {
|
||||
current = origin
|
||||
}
|
||||
openWindow?()
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class WindowSlot {
|
||||
weak var window: NSWindow?
|
||||
func serverName(_ origin: String) -> String {
|
||||
let host = URL(string: origin)?.host() ?? origin
|
||||
return host.split(separator: ".").first.map(String.init) ?? host
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@@ -130,6 +118,7 @@ final class Connection {
|
||||
}
|
||||
|
||||
let origin: URL
|
||||
let overlays = Overlays()
|
||||
var phase: Phase = .starting
|
||||
@ObservationIgnored private var stopped = false
|
||||
|
||||
@@ -368,10 +357,9 @@ struct FatalView: View {
|
||||
}
|
||||
|
||||
struct ServerFocus {
|
||||
let origin: String?
|
||||
let store: Store?
|
||||
let overlays: Overlays
|
||||
let forget: (() -> Void)?
|
||||
let forget: () -> Void
|
||||
}
|
||||
|
||||
extension FocusedValues {
|
||||
@@ -379,23 +367,15 @@ extension FocusedValues {
|
||||
}
|
||||
|
||||
struct ServerWindow: View {
|
||||
@Binding var origin: String?
|
||||
@ViewState private var overlays = Overlays()
|
||||
@ViewState private var form = ConnectForm()
|
||||
@ViewState private var slot = WindowSlot()
|
||||
@Environment(\.openWindow) private var openWindow
|
||||
@Environment(\.dismiss) private var dismissWindow
|
||||
|
||||
private var connection: Connection? {
|
||||
origin.flatMap { Servers.shared.connections[$0] }
|
||||
Servers.shared.current.flatMap { Servers.shared.connections[$0] }
|
||||
}
|
||||
|
||||
private var focus: ServerFocus {
|
||||
var forgetting: (() -> Void)?
|
||||
if connection != nil {
|
||||
forgetting = forget
|
||||
}
|
||||
return ServerFocus(origin: connection?.origin.absoluteString, store: connection?.store, overlays: overlays, forget: forgetting)
|
||||
private var focus: ServerFocus? {
|
||||
connection.map { ServerFocus(store: $0.store, overlays: $0.overlays, forget: forget) }
|
||||
}
|
||||
|
||||
private var frameColor: Color {
|
||||
@@ -413,16 +393,13 @@ struct ServerWindow: View {
|
||||
case .running(let store):
|
||||
MainView()
|
||||
.environment(store)
|
||||
.environment(connection.overlays)
|
||||
.id(connection.origin)
|
||||
}
|
||||
} else {
|
||||
ConnectView(form: form) { connected in
|
||||
let key = connected.absoluteString
|
||||
if Servers.shared.shown.contains(key) {
|
||||
openWindow(id: "main", value: key)
|
||||
dismissWindow()
|
||||
return
|
||||
}
|
||||
origin = key
|
||||
form.serverText = ""
|
||||
Servers.shared.current = connected.absoluteString
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -434,98 +411,75 @@ struct ServerWindow: View {
|
||||
.background(Palette.bg)
|
||||
.padding(connection == nil ? 0 : 3)
|
||||
.background(frameColor)
|
||||
.environment(overlays)
|
||||
.font(Fonts.font(13))
|
||||
.frame(minWidth: 640, minHeight: 400)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
framed
|
||||
.navigationTitle(connection?.origin.host() ?? "byre")
|
||||
.background {
|
||||
WindowReader(found: joinTabs)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .principal) {
|
||||
ServerPicker()
|
||||
}
|
||||
}
|
||||
.toolbar(removing: .title)
|
||||
.focusedSceneValue(\.serverWindow, focus)
|
||||
.onChange(of: origin, initial: true) { old, new in
|
||||
moved(old, new)
|
||||
}
|
||||
.onChange(of: Servers.shared.origins) { old, new in
|
||||
adopt(new.filter { !old.contains($0) })
|
||||
}
|
||||
.onAppear(perform: appeared)
|
||||
.onDisappear {
|
||||
if let origin {
|
||||
Servers.shared.shown.remove(origin)
|
||||
release(origin)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func moved(_ old: String?, _ new: String?) {
|
||||
let servers = Servers.shared
|
||||
if let old {
|
||||
servers.shown.remove(old)
|
||||
release(old)
|
||||
}
|
||||
if let new {
|
||||
servers.shown.insert(new)
|
||||
servers.windows[new] = slot
|
||||
}
|
||||
servers.arrange(slot.window?.tabGroup)
|
||||
}
|
||||
|
||||
private func release(_ origin: String) {
|
||||
if Servers.shared.windows[origin] === slot {
|
||||
Servers.shared.windows[origin] = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func appeared() {
|
||||
let servers = Servers.shared
|
||||
let openWindow = openWindow
|
||||
servers.openWindow = { origin in
|
||||
if let origin {
|
||||
openWindow(id: "main", value: origin)
|
||||
} else {
|
||||
openWindow(id: "main")
|
||||
}
|
||||
}
|
||||
if !servers.launched {
|
||||
servers.launched = true
|
||||
adopt(servers.origins)
|
||||
for o in servers.origins where !servers.shown.contains(o) {
|
||||
openWindow(id: "main", value: o)
|
||||
}
|
||||
Servers.shared.openWindow = {
|
||||
openWindow(id: "main")
|
||||
}
|
||||
Notifier.shared.deliver()
|
||||
}
|
||||
|
||||
private func joinTabs(_ window: NSWindow) {
|
||||
window.tabbingMode = .preferred
|
||||
slot.window = window
|
||||
DispatchQueue.main.async {
|
||||
let host = NSApp.windows.first { $0 !== window && $0.isVisible && $0.tabbingIdentifier == window.tabbingIdentifier }
|
||||
if let host, !(host.tabbedWindows ?? []).contains(window) {
|
||||
host.addTabbedWindow(window, ordered: .above)
|
||||
}
|
||||
Servers.shared.arrange(window.tabGroup)
|
||||
}
|
||||
}
|
||||
|
||||
private func adopt(_ candidates: [String]) {
|
||||
guard origin == nil, form.serverText.isEmpty, !form.connecting, let next = candidates.first(where: { !Servers.shared.shown.contains($0) }) else {
|
||||
guard Servers.shared.current == nil, form.serverText.isEmpty, !form.connecting, let next = candidates.first else {
|
||||
return
|
||||
}
|
||||
origin = next
|
||||
Servers.shared.shown.insert(next)
|
||||
Servers.shared.current = next
|
||||
}
|
||||
|
||||
private func forget() {
|
||||
guard let current = origin else {
|
||||
guard let current = Servers.shared.current else {
|
||||
return
|
||||
}
|
||||
form.serverText = current
|
||||
origin = nil
|
||||
Servers.shared.current = nil
|
||||
Servers.shared.remove(current)
|
||||
}
|
||||
}
|
||||
|
||||
struct ServerPicker: View {
|
||||
var body: some View {
|
||||
let servers = Servers.shared
|
||||
Menu {
|
||||
ForEach(servers.origins, id: \.self) { origin in
|
||||
Button(serverName(origin)) {
|
||||
servers.current = origin
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
Button("Add Server…") {
|
||||
servers.current = nil
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: 6) {
|
||||
if let current = servers.current {
|
||||
Circle()
|
||||
.fill(servers.color(current))
|
||||
.frame(width: 8, height: 8)
|
||||
Text(serverName(current))
|
||||
} else {
|
||||
Text("Add Server")
|
||||
}
|
||||
}
|
||||
.font(Fonts.font(13))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user