Add macOS window UI (ContentView) alongside menu bar
WindowGroup with resizable window (560×520 default): connection toolbar, feed list with source icons + text selection + copy button, composer with Cmd+Return shortcut. Both window and menu bar share the same store. Removed LSUIElement — app now shows in Dock. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@
|
|||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
5AE0D6380E8D23C337DD0EB0 /* TetherKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7A65056F38679ECD9498103E /* TetherKit */; };
|
5AE0D6380E8D23C337DD0EB0 /* TetherKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7A65056F38679ECD9498103E /* TetherKit */; };
|
||||||
9386BB2E2BBBCF245A7E605D /* MacTetherStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 010C4E8B3DC255487AE12A75 /* MacTetherStore.swift */; };
|
9386BB2E2BBBCF245A7E605D /* MacTetherStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 010C4E8B3DC255487AE12A75 /* MacTetherStore.swift */; };
|
||||||
|
9617ED2468FED9FC38A37FD8 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 826E98E1B2A0C7A9AC4ED879 /* ContentView.swift */; };
|
||||||
CB143A31027F3CDB3CF5E894 /* MenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDF5D8809D1EC415AB35059B /* MenuView.swift */; };
|
CB143A31027F3CDB3CF5E894 /* MenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDF5D8809D1EC415AB35059B /* MenuView.swift */; };
|
||||||
E1F3C377CB6A58F791A83EAA /* MacTetherApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EA7FA20BA797A0117F705E7 /* MacTetherApp.swift */; };
|
E1F3C377CB6A58F791A83EAA /* MacTetherApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EA7FA20BA797A0117F705E7 /* MacTetherApp.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
@@ -19,6 +20,7 @@
|
|||||||
6C53AB32EB02089B09AD31D6 /* MacTetherApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MacTetherApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
6C53AB32EB02089B09AD31D6 /* MacTetherApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MacTetherApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
6F7F88910D568FDE283249B4 /* TetherKit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = TetherKit; path = ../TetherKit; sourceTree = SOURCE_ROOT; };
|
6F7F88910D568FDE283249B4 /* TetherKit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = TetherKit; path = ../TetherKit; sourceTree = SOURCE_ROOT; };
|
||||||
7D193BC76AB5D363A4CB1F6C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
7D193BC76AB5D363A4CB1F6C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
826E98E1B2A0C7A9AC4ED879 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||||
BDF5D8809D1EC415AB35059B /* MenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuView.swift; sourceTree = "<group>"; };
|
BDF5D8809D1EC415AB35059B /* MenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuView.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
@@ -45,6 +47,7 @@
|
|||||||
D08DCB1AE8C0173418D178ED /* Sources */ = {
|
D08DCB1AE8C0173418D178ED /* Sources */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
826E98E1B2A0C7A9AC4ED879 /* ContentView.swift */,
|
||||||
7D193BC76AB5D363A4CB1F6C /* Info.plist */,
|
7D193BC76AB5D363A4CB1F6C /* Info.plist */,
|
||||||
2EA7FA20BA797A0117F705E7 /* MacTetherApp.swift */,
|
2EA7FA20BA797A0117F705E7 /* MacTetherApp.swift */,
|
||||||
010C4E8B3DC255487AE12A75 /* MacTetherStore.swift */,
|
010C4E8B3DC255487AE12A75 /* MacTetherStore.swift */,
|
||||||
@@ -134,6 +137,7 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
9617ED2468FED9FC38A37FD8 /* ContentView.swift in Sources */,
|
||||||
E1F3C377CB6A58F791A83EAA /* MacTetherApp.swift in Sources */,
|
E1F3C377CB6A58F791A83EAA /* MacTetherApp.swift in Sources */,
|
||||||
9386BB2E2BBBCF245A7E605D /* MacTetherStore.swift in Sources */,
|
9386BB2E2BBBCF245A7E605D /* MacTetherStore.swift in Sources */,
|
||||||
CB143A31027F3CDB3CF5E894 /* MenuView.swift in Sources */,
|
CB143A31027F3CDB3CF5E894 /* MenuView.swift in Sources */,
|
||||||
@@ -277,7 +281,6 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
LSUIElement = YES;
|
|
||||||
MARKETING_VERSION = 0.1.0;
|
MARKETING_VERSION = 0.1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = io.pecord.tether.mac;
|
PRODUCT_BUNDLE_IDENTIFIER = io.pecord.tether.mac;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
@@ -298,7 +301,6 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
LSUIElement = YES;
|
|
||||||
MARKETING_VERSION = 0.1.0;
|
MARKETING_VERSION = 0.1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = io.pecord.tether.mac;
|
PRODUCT_BUNDLE_IDENTIFIER = io.pecord.tether.mac;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import TetherKit
|
||||||
|
|
||||||
|
struct ContentView: View {
|
||||||
|
@Bindable var store: MacTetherStore
|
||||||
|
@State private var draft = ""
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
toolbar
|
||||||
|
Divider()
|
||||||
|
feedArea
|
||||||
|
Divider()
|
||||||
|
composer
|
||||||
|
}
|
||||||
|
.frame(minWidth: 480, minHeight: 400)
|
||||||
|
.task { store.connectIfSaved() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Toolbar
|
||||||
|
|
||||||
|
private var toolbar: some View {
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
Circle()
|
||||||
|
.fill(store.connected ? Color.green : Color.secondary.opacity(0.5))
|
||||||
|
.frame(width: 10, height: 10)
|
||||||
|
.animation(.easeInOut, value: store.connected)
|
||||||
|
|
||||||
|
if store.connected {
|
||||||
|
Text("Room \(store.room)").font(.callout).foregroundStyle(.secondary)
|
||||||
|
} else {
|
||||||
|
TextField("Server URL", text: $store.serverURL)
|
||||||
|
.textFieldStyle(.roundedBorder)
|
||||||
|
.frame(maxWidth: 240)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if let err = store.lastError, store.hasAttemptedConnect {
|
||||||
|
Text(err).font(.caption).foregroundStyle(.red)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(store.connected ? "Disconnect" : "Connect") {
|
||||||
|
store.connected ? store.disconnect() : store.connect()
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
.tint(store.connected ? .secondary : .accentColor)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
.padding(.vertical, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Feed
|
||||||
|
|
||||||
|
private var feedArea: some View {
|
||||||
|
List(store.feed) { msg in
|
||||||
|
feedRow(msg)
|
||||||
|
.listRowSeparator(.hidden)
|
||||||
|
.listRowInsets(EdgeInsets(top: 4, leading: 12, bottom: 4, trailing: 12))
|
||||||
|
}
|
||||||
|
.listStyle(.plain)
|
||||||
|
.overlay {
|
||||||
|
if store.feed.isEmpty {
|
||||||
|
ContentUnavailableView(
|
||||||
|
"No messages yet",
|
||||||
|
systemImage: "link.circle",
|
||||||
|
description: Text(store.connected
|
||||||
|
? "Copy anything on a connected device — it'll appear here."
|
||||||
|
: "Connect to your tether server to start syncing.")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func feedRow(_ msg: Message) -> some View {
|
||||||
|
HStack(alignment: .top, spacing: 10) {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text(msg.text ?? "")
|
||||||
|
.font(.body)
|
||||||
|
.lineLimit(6)
|
||||||
|
.textSelection(.enabled)
|
||||||
|
HStack {
|
||||||
|
Label(msg.source ?? "unknown", systemImage: sourceIcon(msg.source))
|
||||||
|
.font(.caption).foregroundStyle(.secondary)
|
||||||
|
Spacer()
|
||||||
|
Text(msg.date, style: .time)
|
||||||
|
.font(.caption).foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
store.copyToPasteboard(msg)
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "doc.on.doc")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.help("Copy to clipboard")
|
||||||
|
}
|
||||||
|
.padding(10)
|
||||||
|
.background(Color.primary.opacity(0.04), in: RoundedRectangle(cornerRadius: 8))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func sourceIcon(_ source: String?) -> String {
|
||||||
|
let s = source ?? ""
|
||||||
|
if s.contains("iphone") || s.contains("ios") { return "iphone" }
|
||||||
|
if s.contains("ipad") { return "ipad" }
|
||||||
|
if s.contains("macos") || s.contains("mac") { return "laptopcomputer" }
|
||||||
|
if s.contains("windows") { return "pc" }
|
||||||
|
return "desktopcomputer"
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Composer
|
||||||
|
|
||||||
|
private var composer: some View {
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
TextField("Type a message…", text: $draft, axis: .vertical)
|
||||||
|
.textFieldStyle(.roundedBorder)
|
||||||
|
.lineLimit(1...4)
|
||||||
|
.onSubmit {
|
||||||
|
guard !draft.isEmpty else { return }
|
||||||
|
store.send(draft); draft = ""
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
store.send(draft); draft = ""
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "paperplane.fill")
|
||||||
|
}
|
||||||
|
.keyboardShortcut(.return, modifiers: .command)
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
.disabled(!store.connected || draft.isEmpty)
|
||||||
|
}
|
||||||
|
.padding(12)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,8 +18,6 @@
|
|||||||
<string>1.0</string>
|
<string>1.0</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1</string>
|
<string>1</string>
|
||||||
<key>LSUIElement</key>
|
|
||||||
<true/>
|
|
||||||
<key>NSAppTransportSecurity</key>
|
<key>NSAppTransportSecurity</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSAllowsLocalNetworking</key>
|
<key>NSAllowsLocalNetworking</key>
|
||||||
|
|||||||
@@ -3,7 +3,14 @@ import SwiftUI
|
|||||||
@main
|
@main
|
||||||
struct MacTetherApp: App {
|
struct MacTetherApp: App {
|
||||||
@State private var store = MacTetherStore()
|
@State private var store = MacTetherStore()
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
|
WindowGroup {
|
||||||
|
ContentView(store: store)
|
||||||
|
}
|
||||||
|
.windowResizability(.contentMinSize)
|
||||||
|
.defaultSize(width: 560, height: 520)
|
||||||
|
|
||||||
MenuBarExtra("tether", systemImage: store.connected ? "link.circle.fill" : "link.circle") {
|
MenuBarExtra("tether", systemImage: store.connected ? "link.circle.fill" : "link.circle") {
|
||||||
MenuView(store: store)
|
MenuView(store: store)
|
||||||
.frame(width: 320)
|
.frame(width: 320)
|
||||||
|
|||||||
@@ -25,15 +25,12 @@ targets:
|
|||||||
GENERATE_INFOPLIST_FILE: true
|
GENERATE_INFOPLIST_FILE: true
|
||||||
DEVELOPMENT_TEAM: ""
|
DEVELOPMENT_TEAM: ""
|
||||||
CODE_SIGN_STYLE: Automatic
|
CODE_SIGN_STYLE: Automatic
|
||||||
# Hide from Dock — menu bar only
|
|
||||||
LSUIElement: true
|
|
||||||
configs:
|
configs:
|
||||||
Debug:
|
Debug:
|
||||||
INFOPLIST_KEY_NSAppTransportSecurity: ""
|
INFOPLIST_KEY_NSAppTransportSecurity: ""
|
||||||
info:
|
info:
|
||||||
path: Sources/Info.plist
|
path: Sources/Info.plist
|
||||||
properties:
|
properties:
|
||||||
LSUIElement: true
|
|
||||||
NSAppTransportSecurity:
|
NSAppTransportSecurity:
|
||||||
NSAllowsLocalNetworking: true
|
NSAllowsLocalNetworking: true
|
||||||
NSLocalNetworkUsageDescription: "tether connects to your clipboard relay on the local network."
|
NSLocalNetworkUsageDescription: "tether connects to your clipboard relay on the local network."
|
||||||
|
|||||||
Reference in New Issue
Block a user