Files
clawdbot/apps/macos/Sources/Clawdbot/ChannelsSettings+View.swift
2026-01-17 00:43:05 +00:00

168 lines
5.9 KiB
Swift

import SwiftUI
extension ChannelsSettings {
var body: some View {
HStack(spacing: 0) {
self.sidebar
self.detail
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.onAppear {
self.store.start()
self.ensureSelection()
}
.onChange(of: self.orderedChannels) { _, _ in
self.ensureSelection()
}
.onDisappear { self.store.stop() }
}
private var sidebar: some View {
ScrollView {
LazyVStack(alignment: .leading, spacing: 8) {
if !self.enabledChannels.isEmpty {
self.sidebarSectionHeader("Configured")
ForEach(self.enabledChannels) { channel in
self.sidebarRow(channel)
}
}
if !self.availableChannels.isEmpty {
self.sidebarSectionHeader("Available")
ForEach(self.availableChannels) { channel in
self.sidebarRow(channel)
}
}
}
.padding(.vertical, 10)
.padding(.horizontal, 10)
}
.frame(minWidth: 220, idealWidth: 240, maxWidth: 280, maxHeight: .infinity, alignment: .topLeading)
.background(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(Color(nsColor: .windowBackgroundColor)))
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
}
private var detail: some View {
Group {
if let channel = self.selectedChannel {
self.channelDetail(channel)
} else {
self.emptyDetail
}
}
.frame(minWidth: 460, maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
private var emptyDetail: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Channels")
.font(.title3.weight(.semibold))
Text("Select a channel to view status and settings.")
.font(.callout)
.foregroundStyle(.secondary)
}
.padding(.horizontal, 24)
.padding(.vertical, 18)
}
private func channelDetail(_ channel: ChannelItem) -> some View {
ScrollView(.vertical) {
VStack(alignment: .leading, spacing: 16) {
self.detailHeader(for: channel)
Divider()
self.channelSection(channel)
Spacer(minLength: 0)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 24)
.padding(.vertical, 18)
}
}
private func sidebarRow(_ channel: ChannelItem) -> some View {
let isSelected = self.selectedChannel == channel
return Button {
self.selectedChannel = channel
} label: {
HStack(spacing: 8) {
Circle()
.fill(self.channelTint(channel))
.frame(width: 8, height: 8)
VStack(alignment: .leading, spacing: 2) {
Text(channel.title)
Text(self.channelSummary(channel))
.font(.caption)
.foregroundStyle(.secondary)
}
}
.padding(.vertical, 4)
.padding(.horizontal, 6)
.frame(maxWidth: .infinity, alignment: .leading)
.background(isSelected ? Color.accentColor.opacity(0.18) : Color.clear)
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
.background(Color.clear) // ensure full-width hit test area
.contentShape(Rectangle())
}
.frame(maxWidth: .infinity, alignment: .leading)
.buttonStyle(.plain)
.contentShape(Rectangle())
}
private func sidebarSectionHeader(_ title: String) -> some View {
Text(title)
.font(.caption.weight(.semibold))
.foregroundStyle(.secondary)
.textCase(.uppercase)
.padding(.horizontal, 4)
.padding(.top, 2)
}
private func detailHeader(for channel: ChannelItem) -> some View {
VStack(alignment: .leading, spacing: 8) {
HStack(alignment: .firstTextBaseline, spacing: 10) {
Label(channel.detailTitle, systemImage: channel.systemImage)
.font(.title3.weight(.semibold))
self.statusBadge(
self.channelSummary(channel),
color: self.channelTint(channel))
Spacer()
self.channelHeaderActions(channel)
}
HStack(spacing: 10) {
Text("Last check \(self.channelLastCheckText(channel))")
.font(.caption)
.foregroundStyle(.secondary)
if self.channelHasError(channel) {
Text("Error")
.font(.caption2.weight(.semibold))
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.red.opacity(0.15))
.foregroundStyle(.red)
.clipShape(Capsule())
}
}
if let details = self.channelDetails(channel) {
Text(details)
.font(.caption)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
}
}
private func statusBadge(_ text: String, color: Color) -> some View {
Text(text)
.font(.caption2.weight(.semibold))
.padding(.horizontal, 8)
.padding(.vertical, 3)
.background(color.opacity(0.16))
.foregroundStyle(color)
.clipShape(Capsule())
}
}