qstatus/qStatus/Views/InputPanelView.swift
Paweł Orzech c27437b33c
Initial implementation of qStatus macOS menubar app
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.
2026-02-27 23:40:51 +01:00

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)
}
}