Files

155 lines
5.0 KiB
Swift

import SwiftUI
struct PluginManagementRowView: View {
let entry: PluginManagementRow
@ObservedObject var catalog: PluginCatalog
let beginManagedSettings: (String) -> Void
let beginOverriding: (String) -> Void
let beginEditing: (UUID) -> Void
let remove: () -> Void
var body: some View {
VStack(alignment: .leading, spacing: 6) {
VStack(alignment: .leading, spacing: 3) {
HStack(alignment: .top, spacing: 16) {
pluginIdentity
.frame(maxWidth: .infinity, alignment: .leading)
actionButtons
}
if let path = entry.path {
Text(path).font(.caption).foregroundStyle(.secondary)
.lineLimit(1).truncationMode(.middle).help(path).textSelection(.enabled)
}
}
if !entry.overriddenNames.isEmpty {
Text("已被用户覆盖:" + entry.overriddenNames.joined(separator: ", "))
.font(.caption).foregroundStyle(.secondary)
}
availabilityNotice
if let program = entry.securityProgram, entry.availability == .available {
PluginSecurityDetails(facts: catalog.securityFacts[program])
}
}
.padding(.vertical, 4)
}
@ViewBuilder
private var pluginIdentity: some View {
VStack(alignment: .leading, spacing: 3) {
Text(entry.names.joined(separator: ", ")).font(.body)
if let program = entry.managedProgram,
let info = ManagedPluginCatalog.plugins.first(where: { $0.program == program })
{
managedIdentity(info)
} else {
Text(identityKind)
.font(.caption).foregroundStyle(.secondary)
}
}
}
private func managedIdentity(_ info: ManagedPluginInfo) -> some View {
HStack(alignment: .firstTextBaseline, spacing: 8) {
Text("内置").foregroundStyle(.secondary)
if let dependency = info.dependency {
Text(verbatim: dependency.version).foregroundStyle(.secondary)
if let projectURL = URL(string: dependency.projectURL) {
Link(destination: projectURL) {
projectLink(dependency.projectURL)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
} else {
Text("版本信息不可用").foregroundStyle(.secondary)
}
}
.font(.caption)
}
private func projectLink(_ url: String) -> some View {
Text(verbatim: url)
.textSelection(.enabled)
.fixedSize(horizontal: false, vertical: true)
}
private var identityKind: String {
if entry.managedProgram != nil { return String(localized: "内置") }
return entry.overridesManaged ? String(localized: "用户覆盖") : String(localized: "用户添加")
}
@ViewBuilder
private var actionButtons: some View {
if let program = entry.managedProgram {
Button {
beginManagedSettings(program)
} label: {
HStack(spacing: 8) {
Text("设置")
Image(systemName: "chevron.right").font(.caption.weight(.semibold))
}
}
.buttonStyle(.bordered)
Menu(String(localized: "覆盖名称")) {
ForEach(entry.names, id: \.self) { name in
Button(name) { beginOverriding(name) }
.disabled(entry.overriddenNames.contains(name))
}
}
} else if let id = entry.recordID {
Button {
beginEditing(id)
} label: {
HStack(spacing: 8) {
Text("编辑")
Image(systemName: "chevron.right").font(.caption.weight(.semibold))
}
}
.buttonStyle(.bordered)
Button("删除", action: remove)
}
}
@ViewBuilder
private var availabilityNotice: some View {
if let availability = entry.availability,
let issue = PluginManagementCopy.availability(availability)
{
Label(issue, systemImage: "exclamationmark.triangle.fill")
.font(.caption).foregroundStyle(.red)
}
}
}
private struct PluginSecurityDetails: View {
let facts: PluginSecurityFacts?
@State private var isExpanded = false
var body: some View {
if let facts {
if let warning = PluginManagementCopy.securitySummary(facts) {
Label(warning, systemImage: "exclamationmark.triangle")
.font(.caption).foregroundStyle(.orange)
}
DisclosureGroup(isExpanded: $isExpanded) {
VStack(alignment: .leading, spacing: 3) {
Text(PluginManagementCopy.quarantine(facts.quarantine))
Text(PluginManagementCopy.signature(facts.signature))
Text(PluginManagementCopy.policy(facts.policy))
Text("检查结果仅供参考,不能保证 macOS 允许或禁止运行。")
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
} label: {
// macOS 上 DisclosureGroup 默认仅三角图标可点,整行铺开让文字也响应点击。
Text("安全检查详情")
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
.onTapGesture { withAnimation { isExpanded.toggle() } }
}
.font(.caption)
} else {
Text("正在检查安全信息…").font(.caption).foregroundStyle(.secondary)
}
}
}