Reenable multilineinfoview

This commit is contained in:
Max Goedjen
2026-09-08 21:52:02 -07:00
parent 525ca1ce3e
commit 11b6adb648
3 changed files with 194 additions and 161 deletions
@@ -3,17 +3,6 @@ import UniformTypeIdentifiers
public struct MultilineInfoView<TitleView: View, ItemView: View>: View {
// public struct Item {
// public let text: String
// public let action: (Image, () -> Void)?
//
// public init(text: String, action: (Image, () -> Void)?) {
// self.text = text
// self.action = action
// }
//
// }
var titleView: TitleView
var items: ItemView
@@ -22,40 +11,20 @@ public struct MultilineInfoView<TitleView: View, ItemView: View>: View {
self.items = items()
}
// public init(title: LocalizedStringResource, subtitle: LocalizedStringResource, image: Image, items: [String]) where ItemView == Text {
// self.init {
// HStack {
// image
// .renderingMode(.template)
//// .imageScale(.large)
// .foregroundColor(primaryTextColor)
// Text(title)
// .font(.headline)
// .foregroundColor(primaryTextColor)
// Spacer()
// }
// } items: {
// [Text("Hello")]
// }
//
//// self.init {
//// } items: {
//// ForEach(items) { item in
//// return HStack {
//// Text(item)
//// Spacer()
//// // if let (image, _) = $0.1 {
//// // image
//// // .foregroundStyle(.secondary)
//// // }
//// }
//// }
//// }
//
// }
@State private var interactionState: InteractionState = .normal
@State private var interactionStateIndex: Int?
public init(title: LocalizedStringResource, subtitle: LocalizedStringResource? = nil, image: Image, items: [String]) where TitleView == FixedTitleView, ItemView == FixedItemsView {
self.init {
FixedTitleView(title: title, subtitle: subtitle, image: image)
} items: {
FixedItemsView(items: items)
}
}
public init(title: LocalizedStringResource, subtitle: LocalizedStringResource? = nil, image: Image, @ContentBuilder items: () -> ItemView) where TitleView == FixedTitleView {
self.init {
FixedTitleView(title: title, subtitle: subtitle, image: image)
} items: {
items()
}
}
public var body: some View {
VStack(alignment: .leading, spacing: 0) {
@@ -67,74 +36,97 @@ public struct MultilineInfoView<TitleView: View, ItemView: View>: View {
Divider()
.ignoresSafeArea()
.opacity(subview.id == subviews.first?.id ? 1 : 0.5)
subview
.safeAreaPadding(20)
.onHover { hovering in
withAnimation {
// guard item.element.action != nil else { return }
interactionState = hovering ? .hovering : .normal
// interactionStateIndex = item.offset
}
}
.gesture(
TapGesture()
.onEnded {
// item.element.action?.1()
withAnimation {
interactionState = .normal
interactionStateIndex = nil
}
}
)
HoveringSubview {
subview
}
}
}
// ForEach(Array(items.enumerated()), id: \.offset) { item in
// Divider()
// .ignoresSafeArea()
// .opacity(item.offset == 0 ? 1 : 0.75)
// items.element
// .safeAreaPadding(20)
// .onHover { hovering in
// withAnimation {
// guard item.element.action != nil else { return }
// interactionState = hovering ? .hovering : .normal
// interactionStateIndex = item.offset
// }
// }
// .gesture(
// TapGesture()
// .onEnded {
// item.element.action?.1()
// withAnimation {
// interactionState = .normal
// interactionStateIndex = nil
// }
// }
// )
//
// }
}
._background(interactionState: .normal)
.frame(minWidth: 150, maxWidth: .infinity)
}
var primaryTextColor: Color {
switch interactionState {
case .normal, .hovering:
return Color(.textColor)
}
}
struct HoveringSubview<Content: View>: View {
@State private var multilineAction: MultilineItemActionKey.Box?
private let content: Content
@State private var interactionState: InteractionState = .normal
init(@ViewBuilder _ content: () -> Content) {
self.multilineAction = nil
self.content = content()
}
var secondaryTextColor: Color {
switch interactionState {
case .normal, .hovering:
return Color(.secondaryLabelColor)
var body: some View {
HStack {
content
Spacer()
if let image = multilineAction?.image {
image
}
}
.safeAreaPadding(20)
.onHover { hovering in
withAnimation {
guard multilineAction != nil else { return }
interactionState = hovering ? .hovering : .normal
}
}
.gesture(
TapGesture()
.onEnded {
multilineAction?.closure()
withAnimation {
interactionState = .normal
}
}
)
.backgroundStyle(.primary.opacity(interactionState == .hovering ? 0.3 : 0))
.onPreferenceChange(MultilineItemActionKey.self) {
multilineAction = $0
}
}
}
public struct FixedTitleView: View {
let title: LocalizedStringResource
let subtitle: LocalizedStringResource?
let image: Image
public var body: some View {
HStack {
image
.renderingMode(.template)
.imageScale(.large)
VStack(alignment: .leading) {
Text(title)
.font(.headline)
if let subtitle {
Text(subtitle)
.font(.subheadline)
}
}
Spacer()
}
}
}
public struct FixedItemsView: View {
let items: [String]
public var body: some View {
ForEach(Array(items.enumerated()), id: \.offset) {
Text($0.element)
}
}
}
fileprivate enum InteractionState {
case normal, hovering
}
@@ -197,23 +189,68 @@ fileprivate struct BackgroundViewModifier: ViewModifier {
}
//#Preview {
// MultilineInfoView(title: "Multiple", image: Image(systemName: "figure.wave"), items: [
// MultilineInfoView.Item(text: "hello", action: (Image(systemName: "chevron.forward"), {})),
// MultilineInfoView.Item(text: "World", action: (Image(systemName: "chevron.forward"), {})),
// ])
// .padding()
//}
//
//
public struct MultilineItemAction: ViewModifier {
let image: Image?
let action: () -> Void
public init(image: Image?, action: @escaping () -> Void) {
self.image = image
self.action = action
}
public func body(content: Content) -> some View {
content
.preference(key: MultilineItemActionKey.self, value: MultilineItemActionKey.Box(image: image, closure: action))
}
}
extension View {
public func multilineItemAction(image: Image? = nil, action: @escaping () -> Void) -> some View {
modifier(MultilineItemAction(image: image, action: action))
}
}
public struct MultilineItemActionKey : @MainActor PreferenceKey, ~Sendable {
public struct Box: Equatable {
let id: UUID = UUID()
let image: Image?
let closure: () -> Void
public static func == (lhs: borrowing MultilineItemActionKey.Box, rhs: borrowing MultilineItemActionKey.Box) -> Bool {
lhs.id == rhs.id
}
}
public typealias Value = Box?
@MainActor public static let defaultValue: Box? = nil
public static func reduce(value: inout Box?, nextValue: () -> Box?) {
value = nextValue()
}
}
#Preview {
MultilineInfoView {
Text("Hello")
} items: {
Text("World")
.multilineItemAction(image: Image(systemName: "person.wave")) {
print("Hello")
}
Text("World")
Text("World")
}
// MultilineInfoView(title: "One", image: Image(systemName: "figure.wave"), items: ["Hello world."])
.padding()
}
#Preview {
MultilineInfoView(title: "One", image: Image(systemName: "figure.wave"), items: ["Hello world.", "Hello world."])
.padding()
}
@@ -46,42 +46,42 @@ struct CertificateDetailView: View {
text: URL.certificatePath(for: certificate.id, in: URL.certificatesDirectory),
showRevealInFinder: true
)
// if let validityRange = certificate.validityRange {
// let epoch = Date(timeIntervalSince1970: 0)
// let end = Date(timeIntervalSince1970: TimeInterval(UInt64.max))
// switch (validityRange.lowerBound, validityRange.upperBound) {
// case (epoch, end):
// EmptyView()
// case (epoch, let otherEnd):
// Spacer()
// .frame(height: 20)
// MultilineInfoView(title: .certificateDetailValidUntilLabel, image: Image(systemName: "calendar.badge.clock"), items: [otherEnd.formatted()])
// case (let otherStart, end):
// Spacer()
// .frame(height: 20)
// MultilineInfoView(title: .certificateDetailValidAfterLabel, image: Image(systemName: "calendar.badge.clock"), items: [otherStart.formatted()])
// default:
// Spacer()
// .frame(height: 20)
// MultilineInfoView(title: .certificateDetailValidityRangeLabel, image: Image(systemName: "calendar.badge.clock"), items: [validityRange.formatted()])
// }
// }
// if !certificate.principals.isEmpty {
// Spacer()
// .frame(height: 20)
// MultilineInfoView(title: .certificateDetailPrincipalsLabel, image: Image(systemName: "person.2"), items: certificate.principals)
// }
// if !certificate.criticalOptions.isEmpty {
// Spacer()
// .frame(height: 20)
// MultilineInfoView(title: .certificateDetailCriticalOptionsLabel, image: Image(systemName: "person.2"), items: certificate.criticalOptions)
// }
// if !certificate.extensions.isEmpty {
// Spacer()
// .frame(height: 20)
// MultilineInfoView(title: .certificateDetailExtensionsLabel, image: Image(systemName: "person.2"), items: certificate.extensions)
// }
// Spacer()
if let validityRange = certificate.validityRange {
let epoch = Date(timeIntervalSince1970: 0)
let end = Date(timeIntervalSince1970: TimeInterval(UInt64.max))
switch (validityRange.lowerBound, validityRange.upperBound) {
case (epoch, end):
EmptyView()
case (epoch, let otherEnd):
Spacer()
.frame(height: 20)
MultilineInfoView(title: .certificateDetailValidUntilLabel, image: Image(systemName: "calendar.badge.clock"), items: [otherEnd.formatted()])
case (let otherStart, end):
Spacer()
.frame(height: 20)
MultilineInfoView(title: .certificateDetailValidAfterLabel, image: Image(systemName: "calendar.badge.clock"), items: [otherStart.formatted()])
default:
Spacer()
.frame(height: 20)
MultilineInfoView(title: .certificateDetailValidityRangeLabel, image: Image(systemName: "calendar.badge.clock"), items: [validityRange.formatted()])
}
}
if !certificate.principals.isEmpty {
Spacer()
.frame(height: 20)
MultilineInfoView(title: .certificateDetailPrincipalsLabel, image: Image(systemName: "person.2"), items: certificate.principals)
}
if !certificate.criticalOptions.isEmpty {
Spacer()
.frame(height: 20)
MultilineInfoView(title: .certificateDetailCriticalOptionsLabel, image: Image(systemName: "person.2"), items: certificate.criticalOptions)
}
if !certificate.extensions.isEmpty {
Spacer()
.frame(height: 20)
MultilineInfoView(title: .certificateDetailExtensionsLabel, image: Image(systemName: "person.2"), items: certificate.extensions)
}
Spacer()
}
}
.padding()
@@ -43,22 +43,18 @@ struct SecretDetailView<SecretType: Secret>: View {
text: URL.publicKeyPath(for: secret, in: URL.publicKeyDirectory),
showRevealInFinder: true
)
// if !certificates.isEmpty {
// Spacer()
// .frame(height: 20)
// MultilineInfoView(
// title: .secretDetailCertificatePathLabel,
// image: Image(
// systemName: "checkmark.seal.text.page"
// ),
// items: certificates.map({ certificate in
// MultilineInfoView.Item(
// text: certificate.name,
// action: (Image(systemName: "chevron.forward"), { navigateToCertificate?(certificate) })
// )
// })
// )
// }
if !certificates.isEmpty {
Spacer()
.frame(height: 20)
MultilineInfoView(title: .secretDetailCertificatePathLabel, image: Image(systemName: "checkmark.seal.text.page")) {
ForEach(certificates) { certificate in
Text(certificate.name)
.multilineItemAction(image: Image(systemName: "chevron.forward")) {
navigateToCertificate?(certificate)
}
}
}
}
Spacer()
}
}