157 lines
4.9 KiB
Swift
157 lines
4.9 KiB
Swift
import SwiftUI
|
|
import MacToolsPluginKit
|
|
|
|
struct PluginFilterBarView: View {
|
|
@Binding var searchText: String
|
|
@Binding var selectedFilter: PluginCategoryFilter
|
|
let countsByFilter: [PluginCategoryFilter: Int]
|
|
var searchPrompt: String = AppL10n.plugins("plugin.filter.searchPrompt", defaultValue: "搜索插件名称或简介")
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 10) {
|
|
searchField
|
|
|
|
if visibleFilters.count > 1 {
|
|
chipsRow
|
|
}
|
|
}
|
|
}
|
|
|
|
private var searchField: some View {
|
|
HStack(spacing: 8) {
|
|
Image(systemName: "magnifyingglass")
|
|
.font(PluginSettingsTheme.Typography.rowDescription)
|
|
.foregroundStyle(.secondary)
|
|
|
|
TextField(searchPrompt, text: $searchText)
|
|
.textFieldStyle(.plain)
|
|
.font(PluginSettingsTheme.Typography.rowTitle)
|
|
|
|
if !searchText.isEmpty {
|
|
Button {
|
|
searchText = ""
|
|
} label: {
|
|
Image(systemName: "xmark.circle.fill")
|
|
.font(PluginSettingsTheme.Typography.rowDescription)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
.buttonStyle(.plain)
|
|
.help(AppL10n.plugins("plugin.filter.clearSearch", defaultValue: "清除搜索"))
|
|
}
|
|
}
|
|
.padding(.horizontal, 10)
|
|
.padding(.vertical, 7)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 8, style: .continuous)
|
|
.fill(Color(nsColor: .controlBackgroundColor))
|
|
)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 8, style: .continuous)
|
|
.strokeBorder(Color(nsColor: .separatorColor).opacity(0.4), lineWidth: 1)
|
|
)
|
|
}
|
|
|
|
private var chipsRow: some View {
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
HStack(spacing: 6) {
|
|
ForEach(visibleFilters) { filter in
|
|
PluginFilterChip(
|
|
filter: filter,
|
|
count: countsByFilter[filter] ?? 0,
|
|
isSelected: selectedFilter == filter,
|
|
action: { selectedFilter = filter }
|
|
)
|
|
}
|
|
}
|
|
.padding(.vertical, 1)
|
|
}
|
|
}
|
|
|
|
private var visibleFilters: [PluginCategoryFilter] {
|
|
var filters: [PluginCategoryFilter] = [.all]
|
|
|
|
for category in PluginCategory.orderedCases {
|
|
let filter = PluginCategoryFilter.category(category)
|
|
if (countsByFilter[filter] ?? 0) > 0 || filter == selectedFilter {
|
|
filters.append(filter)
|
|
}
|
|
}
|
|
|
|
return filters
|
|
}
|
|
}
|
|
|
|
private struct PluginFilterChip: View {
|
|
let filter: PluginCategoryFilter
|
|
let count: Int
|
|
let isSelected: Bool
|
|
let action: () -> Void
|
|
@State private var isHovered = false
|
|
|
|
var body: some View {
|
|
Button(action: action) {
|
|
HStack(spacing: 6) {
|
|
Image(systemName: filter.iconName)
|
|
.font(PluginSettingsTheme.Typography.secondaryLabel.weight(.semibold))
|
|
.foregroundStyle(iconColor)
|
|
|
|
Text(filter.displayName)
|
|
.font(PluginSettingsTheme.Typography.secondaryLabel.weight(.medium))
|
|
.foregroundStyle(textColor)
|
|
|
|
Text("\(count)")
|
|
.font(PluginSettingsTheme.Typography.statusBadge.weight(.semibold))
|
|
.foregroundStyle(countTextColor)
|
|
.padding(.horizontal, 6)
|
|
.padding(.vertical, 1)
|
|
.background(
|
|
Capsule(style: .continuous)
|
|
.fill(countBackground)
|
|
)
|
|
}
|
|
.padding(.horizontal, 10)
|
|
.padding(.vertical, 5)
|
|
.background(
|
|
Capsule(style: .continuous)
|
|
.fill(chipBackground)
|
|
)
|
|
.contentShape(Capsule(style: .continuous))
|
|
}
|
|
.buttonStyle(.plain)
|
|
.onHover { isHovered = $0 }
|
|
.help(filter.displayName)
|
|
}
|
|
|
|
private var iconColor: Color {
|
|
isSelected ? .white : filter.iconTint
|
|
}
|
|
|
|
private var textColor: Color {
|
|
isSelected ? .white : .primary
|
|
}
|
|
|
|
private var countTextColor: Color {
|
|
isSelected ? .white : .secondary
|
|
}
|
|
|
|
private var countBackground: Color {
|
|
if isSelected {
|
|
return Color.white.opacity(0.22)
|
|
}
|
|
|
|
return Color(nsColor: .quaternaryLabelColor).opacity(0.7)
|
|
}
|
|
|
|
private var chipBackground: Color {
|
|
if isSelected {
|
|
return Color.accentColor
|
|
}
|
|
|
|
if isHovered {
|
|
return Color(nsColor: .controlBackgroundColor)
|
|
}
|
|
|
|
return Color(nsColor: .controlBackgroundColor).opacity(0.5)
|
|
}
|
|
}
|