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 */
|
||||
5AE0D6380E8D23C337DD0EB0 /* TetherKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7A65056F38679ECD9498103E /* TetherKit */; };
|
||||
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 */; };
|
||||
E1F3C377CB6A58F791A83EAA /* MacTetherApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EA7FA20BA797A0117F705E7 /* MacTetherApp.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
@@ -19,6 +20,7 @@
|
||||
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; };
|
||||
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>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
@@ -45,6 +47,7 @@
|
||||
D08DCB1AE8C0173418D178ED /* Sources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
826E98E1B2A0C7A9AC4ED879 /* ContentView.swift */,
|
||||
7D193BC76AB5D363A4CB1F6C /* Info.plist */,
|
||||
2EA7FA20BA797A0117F705E7 /* MacTetherApp.swift */,
|
||||
010C4E8B3DC255487AE12A75 /* MacTetherStore.swift */,
|
||||
@@ -134,6 +137,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9617ED2468FED9FC38A37FD8 /* ContentView.swift in Sources */,
|
||||
E1F3C377CB6A58F791A83EAA /* MacTetherApp.swift in Sources */,
|
||||
9386BB2E2BBBCF245A7E605D /* MacTetherStore.swift in Sources */,
|
||||
CB143A31027F3CDB3CF5E894 /* MenuView.swift in Sources */,
|
||||
@@ -277,7 +281,6 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
LSUIElement = YES;
|
||||
MARKETING_VERSION = 0.1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.pecord.tether.mac;
|
||||
SDKROOT = macosx;
|
||||
@@ -298,7 +301,6 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
LSUIElement = YES;
|
||||
MARKETING_VERSION = 0.1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.pecord.tether.mac;
|
||||
SDKROOT = macosx;
|
||||
|
||||
134
ios/MacTetherApp/Sources/ContentView.swift
Normal file
134
ios/MacTetherApp/Sources/ContentView.swift
Normal file
@@ -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>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSUIElement</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
|
||||
@@ -3,7 +3,14 @@ import SwiftUI
|
||||
@main
|
||||
struct MacTetherApp: App {
|
||||
@State private var store = MacTetherStore()
|
||||
|
||||
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") {
|
||||
MenuView(store: store)
|
||||
.frame(width: 320)
|
||||
|
||||
@@ -25,15 +25,12 @@ targets:
|
||||
GENERATE_INFOPLIST_FILE: true
|
||||
DEVELOPMENT_TEAM: ""
|
||||
CODE_SIGN_STYLE: Automatic
|
||||
# Hide from Dock — menu bar only
|
||||
LSUIElement: true
|
||||
configs:
|
||||
Debug:
|
||||
INFOPLIST_KEY_NSAppTransportSecurity: ""
|
||||
info:
|
||||
path: Sources/Info.plist
|
||||
properties:
|
||||
LSUIElement: true
|
||||
NSAppTransportSecurity:
|
||||
NSAllowsLocalNetworking: true
|
||||
NSLocalNetworkUsageDescription: "tether connects to your clipboard relay on the local network."
|
||||
|
||||
Reference in New Issue
Block a user