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 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 titleView: TitleView
var items: ItemView var items: ItemView
@@ -22,40 +11,20 @@ public struct MultilineInfoView<TitleView: View, ItemView: View>: View {
self.items = items() self.items = items()
} }
// public init(title: LocalizedStringResource, subtitle: LocalizedStringResource, image: Image, items: [String]) where ItemView == Text { public init(title: LocalizedStringResource, subtitle: LocalizedStringResource? = nil, image: Image, items: [String]) where TitleView == FixedTitleView, ItemView == FixedItemsView {
// self.init { self.init {
// HStack { FixedTitleView(title: title, subtitle: subtitle, image: image)
// image } items: {
// .renderingMode(.template) FixedItemsView(items: items)
//// .imageScale(.large) }
// .foregroundColor(primaryTextColor) }
// Text(title) public init(title: LocalizedStringResource, subtitle: LocalizedStringResource? = nil, image: Image, @ContentBuilder items: () -> ItemView) where TitleView == FixedTitleView {
// .font(.headline) self.init {
// .foregroundColor(primaryTextColor) FixedTitleView(title: title, subtitle: subtitle, image: image)
// Spacer() } items: {
// } items()
// } 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 var body: some View { public var body: some View {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
@@ -67,72 +36,95 @@ public struct MultilineInfoView<TitleView: View, ItemView: View>: View {
Divider() Divider()
.ignoresSafeArea() .ignoresSafeArea()
.opacity(subview.id == subviews.first?.id ? 1 : 0.5) .opacity(subview.id == subviews.first?.id ? 1 : 0.5)
HoveringSubview {
subview 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
} }
} }
)
}
}
// 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) ._background(interactionState: .normal)
.frame(minWidth: 150, maxWidth: .infinity) .frame(minWidth: 150, maxWidth: .infinity)
} }
var primaryTextColor: Color { }
switch interactionState {
case .normal, .hovering: struct HoveringSubview<Content: View>: View {
return Color(.textColor)
@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 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
} }
} }
var secondaryTextColor: Color { }
switch interactionState {
case .normal, .hovering:
return Color(.secondaryLabelColor) 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 { fileprivate enum InteractionState {
@@ -197,23 +189,68 @@ fileprivate struct BackgroundViewModifier: ViewModifier {
} }
//#Preview { public struct MultilineItemAction: ViewModifier {
// MultilineInfoView(title: "Multiple", image: Image(systemName: "figure.wave"), items: [
// MultilineInfoView.Item(text: "hello", action: (Image(systemName: "chevron.forward"), {})), let image: Image?
// MultilineInfoView.Item(text: "World", action: (Image(systemName: "chevron.forward"), {})), let action: () -> Void
// ])
// .padding() 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 { #Preview {
MultilineInfoView { MultilineInfoView {
Text("Hello") Text("Hello")
} items: { } items: {
Text("World") Text("World")
.multilineItemAction(image: Image(systemName: "person.wave")) {
print("Hello")
}
Text("World") Text("World")
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() .padding()
} }
@@ -46,42 +46,42 @@ struct CertificateDetailView: View {
text: URL.certificatePath(for: certificate.id, in: URL.certificatesDirectory), text: URL.certificatePath(for: certificate.id, in: URL.certificatesDirectory),
showRevealInFinder: true showRevealInFinder: true
) )
// if let validityRange = certificate.validityRange { if let validityRange = certificate.validityRange {
// let epoch = Date(timeIntervalSince1970: 0) let epoch = Date(timeIntervalSince1970: 0)
// let end = Date(timeIntervalSince1970: TimeInterval(UInt64.max)) let end = Date(timeIntervalSince1970: TimeInterval(UInt64.max))
// switch (validityRange.lowerBound, validityRange.upperBound) { switch (validityRange.lowerBound, validityRange.upperBound) {
// case (epoch, end): case (epoch, end):
// EmptyView() EmptyView()
// case (epoch, let otherEnd): case (epoch, let otherEnd):
// Spacer() Spacer()
// .frame(height: 20) .frame(height: 20)
// MultilineInfoView(title: .certificateDetailValidUntilLabel, image: Image(systemName: "calendar.badge.clock"), items: [otherEnd.formatted()]) MultilineInfoView(title: .certificateDetailValidUntilLabel, image: Image(systemName: "calendar.badge.clock"), items: [otherEnd.formatted()])
// case (let otherStart, end): case (let otherStart, end):
// Spacer() Spacer()
// .frame(height: 20) .frame(height: 20)
// MultilineInfoView(title: .certificateDetailValidAfterLabel, image: Image(systemName: "calendar.badge.clock"), items: [otherStart.formatted()]) MultilineInfoView(title: .certificateDetailValidAfterLabel, image: Image(systemName: "calendar.badge.clock"), items: [otherStart.formatted()])
// default: default:
// Spacer() Spacer()
// .frame(height: 20) .frame(height: 20)
// MultilineInfoView(title: .certificateDetailValidityRangeLabel, image: Image(systemName: "calendar.badge.clock"), items: [validityRange.formatted()]) MultilineInfoView(title: .certificateDetailValidityRangeLabel, image: Image(systemName: "calendar.badge.clock"), items: [validityRange.formatted()])
// } }
// } }
// if !certificate.principals.isEmpty { if !certificate.principals.isEmpty {
// Spacer() Spacer()
// .frame(height: 20) .frame(height: 20)
// MultilineInfoView(title: .certificateDetailPrincipalsLabel, image: Image(systemName: "person.2"), items: certificate.principals) MultilineInfoView(title: .certificateDetailPrincipalsLabel, image: Image(systemName: "person.2"), items: certificate.principals)
// } }
// if !certificate.criticalOptions.isEmpty { if !certificate.criticalOptions.isEmpty {
// Spacer() Spacer()
// .frame(height: 20) .frame(height: 20)
// MultilineInfoView(title: .certificateDetailCriticalOptionsLabel, image: Image(systemName: "person.2"), items: certificate.criticalOptions) MultilineInfoView(title: .certificateDetailCriticalOptionsLabel, image: Image(systemName: "person.2"), items: certificate.criticalOptions)
// } }
// if !certificate.extensions.isEmpty { if !certificate.extensions.isEmpty {
// Spacer() Spacer()
// .frame(height: 20) .frame(height: 20)
// MultilineInfoView(title: .certificateDetailExtensionsLabel, image: Image(systemName: "person.2"), items: certificate.extensions) MultilineInfoView(title: .certificateDetailExtensionsLabel, image: Image(systemName: "person.2"), items: certificate.extensions)
// } }
// Spacer() Spacer()
} }
} }
.padding() .padding()
@@ -43,22 +43,18 @@ struct SecretDetailView<SecretType: Secret>: View {
text: URL.publicKeyPath(for: secret, in: URL.publicKeyDirectory), text: URL.publicKeyPath(for: secret, in: URL.publicKeyDirectory),
showRevealInFinder: true showRevealInFinder: true
) )
// if !certificates.isEmpty { if !certificates.isEmpty {
// Spacer() Spacer()
// .frame(height: 20) .frame(height: 20)
// MultilineInfoView( MultilineInfoView(title: .secretDetailCertificatePathLabel, image: Image(systemName: "checkmark.seal.text.page")) {
// title: .secretDetailCertificatePathLabel, ForEach(certificates) { certificate in
// image: Image( Text(certificate.name)
// systemName: "checkmark.seal.text.page" .multilineItemAction(image: Image(systemName: "chevron.forward")) {
// ), navigateToCertificate?(certificate)
// items: certificates.map({ certificate in }
// MultilineInfoView.Item( }
// text: certificate.name, }
// action: (Image(systemName: "chevron.forward"), { navigateToCertificate?(certificate) }) }
// )
// })
// )
// }
Spacer() Spacer()
} }
} }