Peter Steinbergerfeat: add provider search4c9e6a85 months agoHistory
271 lines · 9.9 KB · Swift
RawDownload
import CodexBarCoreimport SwiftUIimport UniformTypeIdentifiers @MainActorstruct ProviderSidebarListView: View {    let providers: [UsageProvider]    let orderedProviders: [UsageProvider]    @Bindable var store: UsageStore    let isEnabled: (UsageProvider) -> Binding<Bool>    let subtitle: (UsageProvider) -> String    @Binding var searchText: String    @Binding var selection: UsageProvider?    let moveProviders: (IndexSet, Int) -> Void    @State private var draggingProvider: UsageProvider?     var body: some View {        VStack(spacing: 8) {            ProviderSidebarSearchField(searchText: self.$searchText)                .padding(.horizontal, 8)                .padding(.top, 8)             ScrollView {                VStack(spacing: 0) {                    if self.providers.isEmpty {                        Text(L("No matching providers"))                            .font(.caption)                            .foregroundStyle(.secondary)                            .frame(maxWidth: .infinity, minHeight: 80)                    }                     ForEach(self.providers, id: \.self) { provider in                        ProviderSidebarRowView(                            provider: provider,                            store: self.store,                            isEnabled: self.isEnabled(provider),                            subtitle: self.subtitle(provider),                            draggingProvider: self.$draggingProvider)                            .padding(.horizontal, 8)                            .background(                                RoundedRectangle(cornerRadius: 6, style: .continuous)                                    .fill(                                        self.selection == provider                                            ? Color(nsColor: .selectedContentBackgroundColor)                                            : Color.clear)                                    .padding(.horizontal, 4))                            .contentShape(Rectangle())                            .onTapGesture { self.selection = provider }                            .onDrop(                                of: [UTType.plainText],                                delegate: ProviderSidebarDropDelegate(                                    item: provider,                                    providers: self.orderedProviders,                                    dragging: self.$draggingProvider,                                    moveProviders: self.moveProviders))                    }                }                .padding(.vertical, 4)            }        }        .background(            RoundedRectangle(cornerRadius: ProviderSettingsMetrics.sidebarCornerRadius, style: .continuous)                .fill(Color(nsColor: .controlBackgroundColor).opacity(0.8)))        .overlay(            RoundedRectangle(cornerRadius: ProviderSettingsMetrics.sidebarCornerRadius, style: .continuous)                .stroke(Color(nsColor: .separatorColor).opacity(0.7), lineWidth: 1))        .clipShape(RoundedRectangle(cornerRadius: ProviderSettingsMetrics.sidebarCornerRadius, style: .continuous))        .frame(minWidth: ProviderSettingsMetrics.sidebarWidth, maxWidth: ProviderSettingsMetrics.sidebarWidth)    }} private struct ProviderSidebarSearchField: View {    @Binding var searchText: String     var body: some View {        HStack(spacing: 6) {            Image(systemName: "magnifyingglass")                .foregroundStyle(.secondary)                .accessibilityHidden(true)             TextField(L("Search providers"), text: self.$searchText)                .textFieldStyle(.plain)             if !self.searchText.isEmpty {                Button {                    self.searchText = ""                } label: {                    Image(systemName: "xmark.circle.fill")                        .foregroundStyle(.secondary)                        .accessibilityLabel(L("Clear"))                }                .buttonStyle(.plain)            }        }        .font(.callout)        .padding(.horizontal, 8)        .padding(.vertical, 6)        .background(            RoundedRectangle(cornerRadius: 6, style: .continuous)                .fill(Color(nsColor: .textBackgroundColor)))        .overlay(            RoundedRectangle(cornerRadius: 6, style: .continuous)                .stroke(Color(nsColor: .separatorColor).opacity(0.7), lineWidth: 1))    }} @MainActorprivate struct ProviderSidebarRowView: View {    let provider: UsageProvider    @Bindable var store: UsageStore    @Binding var isEnabled: Bool    let subtitle: String    @Binding var draggingProvider: UsageProvider?     var body: some View {        let isRefreshing = self.store.refreshingProviders.contains(self.provider)        let showStatus = self.store.statusChecksEnabled        let statusText = self.statusText         HStack(alignment: .center, spacing: 10) {            ProviderSidebarReorderHandle()                .contentShape(Rectangle())                .padding(.vertical, 4)                .padding(.horizontal, 2)                .help(L("Drag to reorder"))                .onDrag {                    self.draggingProvider = self.provider                    return NSItemProvider(object: self.provider.rawValue as NSString)                }             ProviderSidebarBrandIcon(provider: self.provider)             VStack(alignment: .leading, spacing: 2) {                HStack(spacing: 6) {                    Text(self.store.metadata(for: self.provider).displayName)                        .font(.subheadline.weight(.semibold))                        .foregroundStyle(.primary)                     if showStatus {                        ProviderStatusDot(indicator: self.store.statusIndicator(for: self.provider))                    }                     if isRefreshing {                        ProgressView()                            .controlSize(.mini)                    }                }                Text(statusText)                    .font(.caption)                    .foregroundStyle(.secondary)                    .lineLimit(2)                    .frame(height: ProviderSettingsMetrics.sidebarSubtitleHeight, alignment: .topLeading)            }             Spacer(minLength: 8)             Toggle("", isOn: self.$isEnabled)                .labelsHidden()                .toggleStyle(.checkbox)                .controlSize(.small)        }        .padding(.trailing, 6)        .contentShape(Rectangle())        .padding(.vertical, 2)    }     private var statusText: String {        guard !self.isEnabled else { return self.subtitle }        let lines = self.subtitle.split(separator: "\n", omittingEmptySubsequences: false)        if lines.count >= 2 {            let first = lines[0]            let rest = lines.dropFirst().joined(separator: "\n")            return "\(L("Disabled")) — \(first)\n\(rest)"        }        return "\(L("Disabled")) — \(self.subtitle)"    }} private struct ProviderSidebarReorderHandle: View {    var body: some View {        VStack(spacing: ProviderSettingsMetrics.reorderDotSpacing) {            ForEach(0..<3, id: \.self) { _ in                HStack(spacing: ProviderSettingsMetrics.reorderDotSpacing) {                    Circle()                        .frame(                            width: ProviderSettingsMetrics.reorderDotSize,                            height: ProviderSettingsMetrics.reorderDotSize)                    Circle()                        .frame(                            width: ProviderSettingsMetrics.reorderDotSize,                            height: ProviderSettingsMetrics.reorderDotSize)                }            }        }        .frame(            width: ProviderSettingsMetrics.reorderHandleSize,            height: ProviderSettingsMetrics.reorderHandleSize)        .foregroundStyle(.tertiary)        .accessibilityLabel(L("Reorder"))    }} @MainActorprivate struct ProviderSidebarBrandIcon: View {    let provider: UsageProvider     var body: some View {        if let brand = ProviderBrandIcon.image(for: self.provider) {            Image(nsImage: brand)                .resizable()                .scaledToFit()                .frame(width: ProviderSettingsMetrics.iconSize, height: ProviderSettingsMetrics.iconSize)                .foregroundStyle(.secondary)                .accessibilityHidden(true)        } else {            Image(systemName: "circle.dotted")                .font(.system(size: ProviderSettingsMetrics.iconSize, weight: .regular))                .foregroundStyle(.secondary)                .accessibilityHidden(true)        }    }} private struct ProviderSidebarDropDelegate: DropDelegate {    let item: UsageProvider    let providers: [UsageProvider]    @Binding var dragging: UsageProvider?    let moveProviders: (IndexSet, Int) -> Void     func dropEntered(info _: DropInfo) {        guard let dragging, dragging != self.item else { return }        guard let fromIndex = self.providers.firstIndex(of: dragging),              let toIndex = self.providers.firstIndex(of: self.item)        else { return }         if fromIndex == toIndex { return }        let adjustedIndex = toIndex > fromIndex ? toIndex + 1 : toIndex        self.moveProviders(IndexSet(integer: fromIndex), adjustedIndex)    }     func dropUpdated(info _: DropInfo) -> DropProposal? {        DropProposal(operation: .move)    }     func performDrop(info _: DropInfo) -> Bool {        self.dragging = nil        return true    }} private struct ProviderStatusDot: View {    let indicator: ProviderStatusIndicator     var body: some View {        Circle()            .fill(self.statusColor)            .frame(width: 6, height: 6)            .accessibilityHidden(true)    }     private var statusColor: Color {        switch self.indicator {        case .none: .green        case .minor: .yellow        case .major: .orange        case .critical: .red        case .maintenance: .gray        case .unknown: .gray        }    }}