import SwiftUI import MacToolsPluginKit struct LaunchControlManagerView: View { @ObservedObject var controller: LaunchControlController private let localization: PluginLocalization @State private var scopeFilter: LaunchControlFilter = .user @State private var originFilter: LaunchControlOriginFilter = .all @State private var stateFilter: LaunchControlStateFilter = .all @State private var searchText = "" @State private var pendingAction: LaunchControlPendingAction? init( controller: LaunchControlController, localization: PluginLocalization = PluginLocalization(bundle: .main) ) { self.controller = controller self.localization = localization } var body: some View { VStack(alignment: .leading, spacing: PluginSettingsTheme.Spacing.section) { summaryHeader toolbar if let message = controller.snapshot.errorMessage { statusBanner(message: message, systemImage: "exclamationmark.triangle.fill", color: .orange) } else if let message = controller.snapshot.operationMessage { statusBanner(message: message, systemImage: "checkmark.circle.fill", color: .green) } HStack(alignment: .top, spacing: 0) { itemList .frame(minWidth: 280, idealWidth: 330, maxWidth: 360) Divider() detailPane .frame(minWidth: 0, maxWidth: .infinity, alignment: .topLeading) } .frame(minHeight: 500) .pluginSettingsCardBackground(.plugin) scanActivity } .onAppear { if controller.snapshot.items.isEmpty { controller.refresh() } } .confirmationDialog( pendingAction?.confirmationTitle(localization: localization) ?? localization.string("manager.confirm.defaultTitle", defaultValue: "确认操作"), isPresented: Binding( get: { pendingAction != nil }, set: { if !$0 { pendingAction = nil } } ), titleVisibility: .visible ) { if let pendingAction { Button(pendingAction.buttonTitle(localization: localization), role: pendingAction.role) { perform(pendingAction) self.pendingAction = nil } } Button(localization.string("manager.confirm.cancel", defaultValue: "取消"), role: .cancel) { pendingAction = nil } } message: { if let pendingAction { Text(pendingAction.message(localization: localization)) } } } private var summaryHeader: some View { HStack(alignment: .center, spacing: 12) { metric(localization.string("manager.metric.total", defaultValue: "总数"), value: controller.snapshot.items.count, color: .primary) metric(localization.string("manager.metric.favorite", defaultValue: "关注"), value: controller.snapshot.items.filter(\.isFavorite).count, color: .yellow) metric(localization.string("manager.metric.running", defaultValue: "运行中"), value: controller.snapshot.items.filter { $0.state == .running }.count, color: .green) metric(localization.string("manager.metric.userCreated", defaultValue: "用户创建"), value: controller.snapshot.items.filter { $0.origin == .userCreated }.count, color: .orange) metric(localization.string("manager.metric.appCreated", defaultValue: "应用创建"), value: controller.snapshot.items.filter { $0.origin == .thirdParty }.count, color: .blue) metric(localization.string("manager.metric.failed", defaultValue: "异常"), value: controller.snapshot.items.filter { $0.state == .failed }.count, color: .red) Spacer() if let target = controller.snapshot.currentScanTarget { HStack(spacing: 6) { ProgressView() .controlSize(.small) .scaleEffect(0.72) Text(URL(fileURLWithPath: target).lastPathComponent.isEmpty ? target : URL(fileURLWithPath: target).lastPathComponent) .font(PluginSettingsTheme.Typography.secondaryLabel) .foregroundStyle(.secondary) .lineLimit(1) } .frame(maxWidth: 260, alignment: .trailing) } } } private var toolbar: some View { HStack(spacing: 10) { Picker(localization.string("manager.filter.scope", defaultValue: "范围"), selection: $scopeFilter) { ForEach(LaunchControlFilter.allCases) { filter in Text(filter.title(localization: localization)).tag(filter) } } .labelsHidden() .frame(width: 112) Picker(localization.string("manager.filter.origin", defaultValue: "来源"), selection: $originFilter) { ForEach(LaunchControlOriginFilter.allCases) { filter in Text(filter.title(localization: localization)).tag(filter) } } .labelsHidden() .frame(width: 112) Picker(localization.string("manager.filter.state", defaultValue: "状态"), selection: $stateFilter) { ForEach(LaunchControlStateFilter.allCases) { filter in Text(filter.title(localization: localization)).tag(filter) } } .labelsHidden() .frame(width: 112) TextField(localization.string("manager.search.placeholder", defaultValue: "搜索 label、命令或路径"), text: $searchText) .textFieldStyle(.roundedBorder) .frame(minWidth: 180, idealWidth: 260, maxWidth: 320) Spacer() Button { originFilter = originFilter == .favorite ? .all : .favorite } label: { Label(localization.string("manager.action.favoriteFilter", defaultValue: "关注"), systemImage: originFilter == .favorite ? "star.fill" : "star") } .buttonStyle(.bordered) Button { controller.refresh() } label: { Label( controller.snapshot.isRefreshing ? localization.string("manager.action.refreshing", defaultValue: "刷新中") : localization.string("manager.action.refresh", defaultValue: "刷新"), systemImage: "arrow.clockwise" ) } .disabled(controller.snapshot.isRefreshing) } } private var itemList: some View { VStack(alignment: .leading, spacing: 10) { HStack { Text(localization.string("manager.list.title", defaultValue: "启动项")) .font(PluginSettingsTheme.Typography.emphasizedRowTitle) Spacer() Text("\(filteredItems.count) / \(controller.snapshot.items.count)") .font(PluginSettingsTheme.Typography.secondaryLabel) .foregroundStyle(.secondary) } .padding(.horizontal, 12) .padding(.top, 12) List(selection: selectedItemBinding) { ForEach(filteredItems) { item in LaunchControlItemRow( item: item, localization: localization, onFavoriteToggle: { controller.setFavorite(!item.isFavorite, for: item) } ) .tag(item.id) } } .listStyle(.inset) .overlay { if controller.snapshot.isRefreshing && controller.snapshot.items.isEmpty { ProgressView(localization.string("manager.list.scanning", defaultValue: "正在扫描")) .controlSize(.small) } else if filteredItems.isEmpty { ContentUnavailableView( localization.string("manager.list.empty", defaultValue: "没有匹配项目"), systemImage: "magnifyingglass" ) } } } .background(PluginSettingsTheme.Palette.nativeFieldBackground) } private var detailPane: some View { Group { if let item = selectedItem { ScrollView { VStack(alignment: .leading, spacing: 18) { detailHeader(item) actionBar(item) LaunchControlNoteEditor(item: item, controller: controller, localization: localization) .id(item.id) keyFields(item) rawPlist(item) } .frame(maxWidth: .infinity, alignment: .topLeading) .padding(16) } } else { placeholder } } .background(Color(nsColor: .windowBackgroundColor)) } private var placeholder: some View { VStack(spacing: 10) { Image(systemName: "list.bullet.rectangle") .font(.system(size: PluginSettingsTheme.Size.emptyStateIcon, weight: .regular)) .foregroundStyle(.secondary) Text(controller.snapshot.isRefreshing ? localization.string("manager.placeholder.loadingTitle", defaultValue: "正在读取启动项") : localization.string("manager.placeholder.title", defaultValue: "选择一个启动项") ) .font(PluginSettingsTheme.Typography.pageDescription.weight(.semibold)) Text(controller.snapshot.isRefreshing ? localization.string("manager.placeholder.loadingDescription", defaultValue: "左侧列表会随扫描进度逐步更新") : localization.string("manager.placeholder.description", defaultValue: "查看 plist 字段、运行状态和可用操作") ) .font(PluginSettingsTheme.Typography.secondaryLabel) .foregroundStyle(.secondary) .multilineTextAlignment(.center) } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding() } @ViewBuilder private var scanActivity: some View { if controller.snapshot.isRefreshing || !controller.snapshot.scanLogEntries.isEmpty { VStack(alignment: .leading, spacing: 8) { HStack { Text(localization.string("manager.scanActivity.title", defaultValue: "扫描进度")) .font(PluginSettingsTheme.Typography.sectionTitle) Spacer() if controller.snapshot.isRefreshing { ProgressView() .controlSize(.small) .scaleEffect(0.72) } } ScrollViewReader { proxy in ScrollView { LazyVStack(alignment: .leading, spacing: 4) { ForEach(Array(controller.snapshot.scanLogEntries.enumerated()), id: \.offset) { index, entry in Text(entry) .font(.system(size: 11, design: .monospaced)) .foregroundStyle(.secondary) .lineLimit(1) .frame(maxWidth: .infinity, alignment: .leading) .id(index) } } } .frame(height: 88) .onChange(of: controller.snapshot.scanLogEntries.count) { if let lastIndex = controller.snapshot.scanLogEntries.indices.last { proxy.scrollTo(lastIndex, anchor: .bottom) } } } } .padding(10) .pluginSettingsCardBackground(.plugin) } } private var filteredItems: [LaunchControlItem] { controller.snapshot.items.filter { item in let scopeMatches = scopeFilter.scope.map { $0 == item.scope } ?? true let originMatches = originFilter.matches(item) let stateMatches = stateFilter.matches(item.state) let query = searchText.trimmingCharacters(in: .whitespacesAndNewlines) let searchMatches: Bool if query.isEmpty { searchMatches = true } else { let haystack = [ item.label, item.commandText(localization: localization), item.plistURL.path, item.origin.title(localization: localization), item.triggerSummary(localization: localization), item.note ].joined(separator: "\n") searchMatches = haystack.localizedCaseInsensitiveContains(query) } return scopeMatches && originMatches && stateMatches && searchMatches } } private var selectedItem: LaunchControlItem? { if let selectedID = controller.snapshot.selectedItemID, let visibleItem = filteredItems.first(where: { $0.id == selectedID }) { return visibleItem } return filteredItems.first ?? controller.snapshot.selectedItem } private var selectedItemBinding: Binding { Binding( get: { selectedItem?.id }, set: { controller.selectItem(id: $0) } ) } private func detailHeader(_ item: LaunchControlItem) -> some View { VStack(alignment: .leading, spacing: 8) { HStack(alignment: .top, spacing: 12) { Image(systemName: item.origin == .system ? "lock.shield" : "powerplug") .font(PluginSettingsTheme.Typography.pageTitle.weight(.semibold)) .foregroundStyle(item.origin == .system ? Color.secondary : Color.orange) .frame(width: 28) VStack(alignment: .leading, spacing: 5) { Text(item.label) .font(PluginSettingsTheme.Typography.pageTitle) .lineLimit(3) .fixedSize(horizontal: false, vertical: true) .textSelection(.enabled) Text(item.plistURL.path) .font(PluginSettingsTheme.Typography.controlLabel) .foregroundStyle(.secondary) .lineLimit(3) .fixedSize(horizontal: false, vertical: true) .textSelection(.enabled) } .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) Button { controller.setFavorite(!item.isFavorite, for: item) } label: { Image(systemName: item.isFavorite ? "star.fill" : "star") .font(PluginSettingsTheme.Typography.pageDescription.weight(.semibold)) .foregroundStyle(item.isFavorite ? Color.yellow : Color.secondary) } .buttonStyle(.plain) .help(item.isFavorite ? localization.string("manager.favorite.removeHelp", defaultValue: "取消关注") : localization.string("manager.favorite.addHelp", defaultValue: "关注启动项") ) } FlowLayout(spacing: 8, rowSpacing: 6) { badge(item.scope.title(localization: localization), color: .blue) badge(item.origin.title(localization: localization), color: item.origin == .system ? .gray : .orange) badge(item.statusText(localization: localization), color: item.state == .failed ? .red : .green) if !item.canManage { badge(localization.string("manager.badge.readOnly", defaultValue: "只读"), color: .gray) } } } } private func actionBar(_ item: LaunchControlItem) -> some View { FlowLayout(spacing: 8, rowSpacing: 8) { Button { controller.setFavorite(!item.isFavorite, for: item) } label: { Label( item.isFavorite ? localization.string("manager.action.unfavorite", defaultValue: "取消关注") : localization.string("manager.action.favorite", defaultValue: "关注"), systemImage: item.isFavorite ? "star.slash" : "star" ) } Button { controller.openInFinder(item) } label: { Label(localization.string("manager.action.revealInFinder", defaultValue: "在 Finder 中显示"), systemImage: "finder") } if item.canManage { Button { pendingAction = .bootstrap(item) } label: { Label(LaunchControlManagedAction.bootstrap.title(localization: localization), systemImage: "tray.and.arrow.down") } .disabled(item.isLoaded) Button { pendingAction = .bootout(item) } label: { Label(LaunchControlManagedAction.bootout.title(localization: localization), systemImage: "tray.and.arrow.up") } .disabled(!item.isLoaded) Button { pendingAction = item.isDisabled ? .enable(item) : .disable(item) } label: { Label( item.isDisabled ? LaunchControlManagedAction.enable.title(localization: localization) : LaunchControlManagedAction.disable.title(localization: localization), systemImage: item.isDisabled ? "checkmark.circle" : "nosign" ) } Button { pendingAction = item.state == .running ? .stop(item) : .start(item) } label: { Label( item.state == .running ? LaunchControlManagedAction.stop.title(localization: localization) : LaunchControlManagedAction.start.title(localization: localization), systemImage: item.state == .running ? "stop.circle" : "play.circle" ) } .disabled(item.isDisabled) Button { pendingAction = .restart(item) } label: { Label(LaunchControlManagedAction.restart.title(localization: localization), systemImage: "arrow.clockwise.circle") } .disabled(item.isDisabled) } } .buttonStyle(.bordered) } private func keyFields(_ item: LaunchControlItem) -> some View { VStack(alignment: .leading, spacing: 10) { Text(localization.string("manager.keyFields.title", defaultValue: "关键字段")) .font(PluginSettingsTheme.Typography.emphasizedRowTitle) fieldRow( "ProgramArguments", value: item.commandText(localization: localization), help: localization.string("manager.field.programArguments.help", defaultValue: "启动时执行的命令与参数。") ) fieldRow( "RunAtLoad", value: item.runAtLoad ? "true" : "false", help: localization.string("manager.field.runAtLoad.help", defaultValue: "加载 LaunchAgent 后是否立即运行一次。") ) fieldRow( "KeepAlive", value: item.keepAliveDescription ?? notSetText, help: localization.string("manager.field.keepAlive.help", defaultValue: "进程退出后是否按条件自动拉起。") ) fieldRow( "StartInterval", value: item.startInterval.map { secondsText($0) } ?? notSetText, help: localization.string("manager.field.startInterval.help", defaultValue: "按固定秒数间隔触发。") ) fieldRow( "StartCalendarInterval", value: item.startCalendarDescription ?? notSetText, help: localization.string("manager.field.startCalendar.help", defaultValue: "按日历时间触发。") ) fieldRow( localization.string("manager.field.triggerSummary.title", defaultValue: "触发摘要"), value: item.triggerSummary(localization: localization), help: localization.string("manager.field.triggerSummary.help", defaultValue: "根据常见字段生成的可读说明。") ) } } private func rawPlist(_ item: LaunchControlItem) -> some View { VStack(alignment: .leading, spacing: 8) { Text(localization.string("manager.rawPlist.title", defaultValue: "原始 plist")) .font(PluginSettingsTheme.Typography.emphasizedRowTitle) ScrollView(.horizontal) { Text(item.rawPlist.isEmpty ? localization.string("manager.rawPlist.unreadable", defaultValue: "无法以 UTF-8 显示原始内容") : item.rawPlist ) .font(.system(.caption, design: .monospaced)) .textSelection(.enabled) .frame(maxWidth: .infinity, alignment: .leading) .padding(10) } .frame(minHeight: 180, maxHeight: 260) .pluginSettingsCardBackground(.recessed) } } private func fieldRow(_ title: String, value: String, help: String) -> some View { Grid(alignment: .leadingFirstTextBaseline, horizontalSpacing: 12, verticalSpacing: 4) { GridRow { Text(title) .font(.system(size: 12, weight: .semibold, design: .monospaced)) .foregroundStyle(.secondary) .frame(width: 138, alignment: .leading) VStack(alignment: .leading, spacing: 2) { Text(value) .font(PluginSettingsTheme.Typography.rowTitle) .fixedSize(horizontal: false, vertical: true) .textSelection(.enabled) Text(help) .font(PluginSettingsTheme.Typography.rowDescription) .foregroundStyle(.secondary) } } } .padding(10) .pluginSettingsCardBackground(.plugin) } private func statusBanner(message: String, systemImage: String, color: Color) -> some View { HStack(spacing: 8) { Image(systemName: systemImage) .foregroundStyle(color) Text(message) .font(PluginSettingsTheme.Typography.secondaryLabel) .lineLimit(3) .fixedSize(horizontal: false, vertical: true) Spacer() } .padding(10) .background(color.opacity(0.12)) .clipShape(RoundedRectangle(cornerRadius: PluginSettingsTheme.Radius.control, style: .continuous)) } private func metric(_ title: String, value: Int, color: Color) -> some View { HStack(spacing: 6) { Text("\(value)") .font(PluginSettingsTheme.Typography.pageTitle) .foregroundStyle(color) .monospacedDigit() Text(title) .font(PluginSettingsTheme.Typography.secondaryLabel) .foregroundStyle(.secondary) } .padding(.horizontal, 10) .padding(.vertical, 7) .pluginSettingsCardBackground(.plugin) } private func badge(_ title: String, color: Color) -> some View { Text(title) .font(PluginSettingsTheme.Typography.statusBadge) .foregroundStyle(color) .padding(.horizontal, 8) .padding(.vertical, 4) .background(color.opacity(0.12)) .clipShape(Capsule()) } private func perform(_ action: LaunchControlPendingAction) { switch action { case let .bootstrap(item): controller.bootstrap(item) case let .bootout(item): controller.bootout(item) case let .enable(item): controller.enable(item) case let .disable(item): controller.disable(item) case let .start(item): controller.start(item) case let .stop(item): controller.stop(item) case let .restart(item): controller.restart(item) } } private var notSetText: String { localization.string("manager.field.notSet", defaultValue: "未设置") } private func secondsText(_ seconds: Int) -> String { localization.format("manager.seconds", defaultValue: "%d 秒", seconds) } } private struct FlowLayout: Layout { var spacing: CGFloat var rowSpacing: CGFloat func sizeThatFits( proposal: ProposedViewSize, subviews: Subviews, cache: inout () ) -> CGSize { let rows = rows(proposal: proposal, subviews: subviews) return CGSize( width: proposal.width ?? rows.map(\.width).max() ?? 0, height: rows.last.map { $0.y + $0.height } ?? 0 ) } func placeSubviews( in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout () ) { for row in rows(proposal: ProposedViewSize(width: bounds.width, height: proposal.height), subviews: subviews) { for item in row.items { subviews[item.index].place( at: CGPoint(x: bounds.minX + item.x, y: bounds.minY + row.y), proposal: ProposedViewSize(item.size) ) } } } private func rows(proposal: ProposedViewSize, subviews: Subviews) -> [FlowRow] { let maxWidth = proposal.width ?? .greatestFiniteMagnitude var rows: [FlowRow] = [] var currentItems: [FlowItem] = [] var currentWidth: CGFloat = 0 var currentHeight: CGFloat = 0 var y: CGFloat = 0 for index in subviews.indices { let size = subviews[index].sizeThatFits(.unspecified) let nextWidth = currentItems.isEmpty ? size.width : currentWidth + spacing + size.width if nextWidth > maxWidth, !currentItems.isEmpty { rows.append(FlowRow(items: currentItems, y: y, width: currentWidth, height: currentHeight)) y += currentHeight + rowSpacing currentItems = [] currentWidth = 0 currentHeight = 0 } let x = currentItems.isEmpty ? 0 : currentWidth + spacing currentItems.append(FlowItem(index: index, x: x, size: size)) currentWidth = currentItems.isEmpty ? size.width : x + size.width currentHeight = max(currentHeight, size.height) } if !currentItems.isEmpty { rows.append(FlowRow(items: currentItems, y: y, width: currentWidth, height: currentHeight)) } return rows } } private struct FlowRow { let items: [FlowItem] let y: CGFloat let width: CGFloat let height: CGFloat } private struct FlowItem { let index: Int let x: CGFloat let size: CGSize } private struct LaunchControlItemRow: View { let item: LaunchControlItem let localization: PluginLocalization let onFavoriteToggle: () -> Void var body: some View { HStack(alignment: .top, spacing: 10) { Circle() .fill(statusColor) .frame(width: 8, height: 8) .padding(.top, 6) VStack(alignment: .leading, spacing: 4) { HStack(spacing: 6) { Text(item.label) .font(PluginSettingsTheme.Typography.emphasizedRowTitle) .lineLimit(1) if item.origin == .userCreated { Text(localization.string("manager.itemRow.userBadge", defaultValue: "用户")) .font(PluginSettingsTheme.Typography.statusBadge) .foregroundStyle(.orange) } } Text(item.commandText(localization: localization)) .font(PluginSettingsTheme.Typography.rowDescription) .foregroundStyle(.secondary) .lineLimit(1) Text(localization.format( "manager.itemRow.scopeStatus", defaultValue: "%@ · %@", item.scope.title(localization: localization), item.statusText(localization: localization) )) .font(PluginSettingsTheme.Typography.secondaryLabel) .foregroundStyle(.secondary) .lineLimit(1) } Spacer(minLength: 4) Button(action: onFavoriteToggle) { Image(systemName: item.isFavorite ? "star.fill" : "star") .font(PluginSettingsTheme.Typography.secondaryLabel.weight(.semibold)) .foregroundStyle(item.isFavorite ? Color.yellow : Color.secondary) .frame(width: 18, height: 18) } .buttonStyle(.plain) .help(item.isFavorite ? localization.string("manager.favorite.removeHelp", defaultValue: "取消关注") : localization.string("manager.favorite.addHelp", defaultValue: "关注启动项") ) } .padding(.vertical, 4) } private var statusColor: Color { switch item.state { case .running: return .green case .failed: return .red case .disabled: return .gray case .loaded: return .blue case .unloaded, .unknown: return .secondary } } } private enum LaunchControlPendingAction: Identifiable { case bootstrap(LaunchControlItem) case bootout(LaunchControlItem) case enable(LaunchControlItem) case disable(LaunchControlItem) case start(LaunchControlItem) case stop(LaunchControlItem) case restart(LaunchControlItem) var id: String { "\(actionName)-\(item.id)" } var item: LaunchControlItem { switch self { case let .bootstrap(item), let .bootout(item), let .enable(item), let .disable(item), let .start(item), let .stop(item), let .restart(item): return item } } var actionName: String { action.title() } func actionName(localization: PluginLocalization) -> String { action.title(localization: localization) } var action: LaunchControlManagedAction { switch self { case .bootstrap: return .bootstrap case .bootout: return .bootout case .enable: return .enable case .disable: return .disable case .start: return .start case .stop: return .stop case .restart: return .restart } } var buttonTitle: String { "\(actionName) \(item.label)" } func buttonTitle(localization: PluginLocalization) -> String { localization.format( "manager.confirm.buttonTitle", defaultValue: "%@ %@", actionName(localization: localization), item.label ) } var confirmationTitle: String { confirmationTitle(localization: PluginLocalization(bundle: .main)) } func confirmationTitle(localization: PluginLocalization) -> String { localization.format( "manager.confirm.title", defaultValue: "确认%@启动项?", actionName(localization: localization) ) } var message: String { message(localization: PluginLocalization(bundle: .main)) } func message(localization: PluginLocalization) -> String { localization.format( "manager.confirm.message", defaultValue: "%@\n%@\n\n此操作会调用 launchctl %@。用户级 LaunchAgent 通常可以恢复,但禁用、卸载或停止可能影响后台任务。", item.label, item.plistURL.path, actionName(localization: localization) ) } var role: ButtonRole? { switch self { case .bootout, .disable, .stop: return .destructive case .bootstrap, .enable, .start, .restart: return nil } } } private struct LaunchControlNoteEditor: View { let item: LaunchControlItem let controller: LaunchControlController let localization: PluginLocalization @State private var draft: String init( item: LaunchControlItem, controller: LaunchControlController, localization: PluginLocalization = PluginLocalization(bundle: .main) ) { self.item = item self.controller = controller self.localization = localization _draft = State(initialValue: item.note) } private var isDirty: Bool { draft.trimmingCharacters(in: .whitespacesAndNewlines) != item.note.trimmingCharacters(in: .whitespacesAndNewlines) } var body: some View { VStack(alignment: .leading, spacing: 10) { HStack { Text(localization.string("manager.note.title", defaultValue: "备注")) .font(PluginSettingsTheme.Typography.emphasizedRowTitle) Spacer() if isDirty { Button(localization.string("manager.note.save", defaultValue: "保存")) { controller.setNote(draft, for: item) } .controlSize(.small) .buttonStyle(.borderedProminent) } } TextEditor(text: $draft) .font(.system(size: 12)) .scrollContentBackground(.hidden) .frame(minHeight: 60, maxHeight: 120) .padding(6) .pluginSettingsCardBackground(.recessed) .overlay(alignment: .topLeading) { if draft.isEmpty { Text(localization.string( "manager.note.placeholder", defaultValue: "为这个启动项添加本地备注(仅保存在本机,不写入 plist)。" )) .font(.system(size: 12)) .foregroundStyle(.tertiary) .padding(.horizontal, 11) .padding(.top, 13) .allowsHitTesting(false) } } } } }