Native macOS app for posting to Mastodon, WordPress (self-hosted), and Micro.blog. Features: global hotkey (Ctrl+Option+Cmd+T), multiple accounts with selection, image attachments (up to 4, drag-and-drop), floating panel UI, Keychain storage.
127 lines
4 KiB
Swift
127 lines
4 KiB
Swift
import SwiftUI
|
|
|
|
struct InputPanelView: View {
|
|
@Bindable var appState: AppState
|
|
let accountStore: AccountStore
|
|
let onPost: () -> Void
|
|
let onDismiss: () -> Void
|
|
|
|
@FocusState private var isTextFieldFocused: Bool
|
|
|
|
var body: some View {
|
|
VStack(spacing: 12) {
|
|
// Account selector
|
|
AccountSelectorView(
|
|
accounts: accountStore.accounts,
|
|
selectedIDs: $appState.selectedAccountIDs
|
|
)
|
|
|
|
// Text input
|
|
TextEditor(text: $appState.inputText)
|
|
.font(.system(size: 14))
|
|
.frame(minHeight: 80, maxHeight: 160)
|
|
.focused($isTextFieldFocused)
|
|
.scrollContentBackground(.hidden)
|
|
.padding(8)
|
|
.background(Color(nsColor: .controlBackgroundColor))
|
|
.clipShape(RoundedRectangle(cornerRadius: 8))
|
|
|
|
// Image attachments
|
|
ImageAttachmentArea(attachedImages: Bindable(appState).attachedImages)
|
|
|
|
// Status message
|
|
if let status = appState.statusMessage {
|
|
Text(status.text)
|
|
.font(.caption)
|
|
.foregroundStyle(status.isError ? .red : .green)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
|
|
// Bottom bar
|
|
HStack {
|
|
Text("\(appState.inputText.count)")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
.monospacedDigit()
|
|
|
|
Spacer()
|
|
|
|
if appState.isSubmitting {
|
|
ProgressView()
|
|
.controlSize(.small)
|
|
}
|
|
|
|
Button("Post") {
|
|
onPost()
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
.disabled(!appState.canPost)
|
|
.keyboardShortcut(.return, modifiers: .command)
|
|
}
|
|
}
|
|
.padding()
|
|
.frame(width: 420)
|
|
.onAppear {
|
|
isTextFieldFocused = true
|
|
}
|
|
}
|
|
}
|
|
|
|
struct AccountSelectorView: View {
|
|
let accounts: [Account]
|
|
@Binding var selectedIDs: Set<UUID>
|
|
|
|
var body: some View {
|
|
if accounts.isEmpty {
|
|
Text("No accounts configured. Open Settings to add one.")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
.frame(maxWidth: .infinity, alignment: .center)
|
|
.padding(.vertical, 4)
|
|
} else {
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
HStack(spacing: 6) {
|
|
ForEach(accounts) { account in
|
|
AccountChip(
|
|
account: account,
|
|
isSelected: selectedIDs.contains(account.id)
|
|
) {
|
|
if selectedIDs.contains(account.id) {
|
|
selectedIDs.remove(account.id)
|
|
} else {
|
|
selectedIDs.insert(account.id)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct AccountChip: View {
|
|
let account: Account
|
|
let isSelected: Bool
|
|
let action: () -> Void
|
|
|
|
var body: some View {
|
|
Button(action: action) {
|
|
HStack(spacing: 4) {
|
|
Image(systemName: account.serviceType.iconName)
|
|
.font(.caption2)
|
|
Text(account.displayName)
|
|
.font(.caption)
|
|
}
|
|
.padding(.horizontal, 8)
|
|
.padding(.vertical, 4)
|
|
.background(isSelected ? Color.accentColor.opacity(0.2) : Color.secondary.opacity(0.1))
|
|
.foregroundStyle(isSelected ? Color.accentColor : .secondary)
|
|
.clipShape(Capsule())
|
|
.overlay(
|
|
Capsule()
|
|
.strokeBorder(isSelected ? Color.accentColor : Color.clear, lineWidth: 1)
|
|
)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|