IceCubesApp/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowCardView.swift

283 lines
8.6 KiB
Swift
Raw Normal View History

2023-01-17 10:36:01 +00:00
import DesignSystem
2022-12-19 16:18:16 +00:00
import Models
2023-02-19 14:29:07 +00:00
import Nuke
2022-12-25 06:43:02 +00:00
import NukeUI
2023-01-17 10:36:01 +00:00
import SwiftUI
2024-02-10 10:26:22 +00:00
import Env
2022-12-19 16:18:16 +00:00
2023-12-18 07:22:59 +00:00
@MainActor
public struct StatusRowCardView: View {
2022-12-19 16:18:16 +00:00
@Environment(\.openURL) private var openURL
2024-02-10 10:26:22 +00:00
@Environment(\.openWindow) private var openWindow
2023-02-19 14:29:07 +00:00
@Environment(\.isInCaptureMode) private var isInCaptureMode: Bool
2024-02-10 10:26:22 +00:00
@Environment(\.isCompact) private var isCompact: Bool
2023-02-21 06:23:42 +00:00
2023-09-18 19:03:52 +00:00
@Environment(Theme.self) private var theme
2024-02-10 10:26:22 +00:00
@Environment(RouterPath.self) private var routerPath
2023-02-22 18:09:39 +00:00
let card: Card
2023-01-17 10:36:01 +00:00
public init(card: Card) {
self.card = card
}
2023-02-22 18:09:39 +00:00
private var maxWidth: CGFloat? {
if theme.statusDisplayStyle == .medium {
return 300
}
return nil
}
2023-02-22 18:09:39 +00:00
private func imageWidthFor(proxy: GeometryProxy) -> CGFloat {
if theme.statusDisplayStyle == .medium, let maxWidth {
return maxWidth
}
return proxy.frame(in: .local).width
}
2023-02-22 18:09:39 +00:00
private var imageHeight: CGFloat {
2024-02-10 10:26:22 +00:00
if theme.statusDisplayStyle == .medium || isCompact {
return 100
}
return 200
}
2023-01-17 10:36:01 +00:00
public var body: some View {
2023-07-17 17:13:36 +00:00
Button {
if let url = URL(string: card.url) {
openURL(url)
}
} label: {
if let title = card.title, let url = URL(string: card.url) {
VStack(alignment: .leading, spacing: 0) {
let sitesWithIcons = ["apps.apple.com", "music.apple.com", "podcasts.apple.com", "open.spotify.com"]
2024-02-10 10:26:22 +00:00
if isCompact {
compactLinkPreview(title, url)
} else if (UIDevice.current.userInterfaceIdiom == .pad ||
2024-01-15 20:15:40 +00:00
UIDevice.current.userInterfaceIdiom == .mac ||
UIDevice.current.userInterfaceIdiom == .vision),
let host = url.host(), sitesWithIcons.contains(host) {
iconLinkPreview(title, url)
} else {
defaultLinkPreview(title, url)
2022-12-25 06:43:02 +00:00
}
2023-01-19 06:45:37 +00:00
}
2023-07-17 17:13:36 +00:00
.frame(maxWidth: maxWidth)
.fixedSize(horizontal: false, vertical: true)
2024-01-15 20:15:40 +00:00
#if os(visionOS)
2024-02-10 10:26:22 +00:00
.if(!isCompact, transform: { view in
view.background(.background)
})
2024-01-19 07:51:29 +00:00
.hoverEffect()
2024-01-15 20:15:40 +00:00
#else
2024-02-10 10:26:22 +00:00
.background(isCompact ? .clear : theme.secondaryBackgroundColor)
2024-01-15 20:15:40 +00:00
#endif
2024-02-10 10:26:22 +00:00
.cornerRadius(isCompact ? 0 : 10)
.overlay {
if !isCompact {
RoundedRectangle(cornerRadius: 10)
.stroke(.gray.opacity(0.35), lineWidth: 1)
}
}
2023-07-17 17:13:36 +00:00
.contextMenu {
ShareLink(item: url) {
Label("status.card.share", systemImage: "square.and.arrow.up")
}
Button { openURL(url) } label: {
Label("status.action.view-in-browser", systemImage: "safari")
}
Divider()
Button {
UIPasteboard.general.url = url
} label: {
Label("status.card.copy", systemImage: "doc.on.doc")
}
2023-01-19 06:45:37 +00:00
}
2023-07-17 17:13:36 +00:00
.accessibilityElement(children: .combine)
.accessibilityAddTraits(.isLink)
.accessibilityRemoveTraits(.isStaticText)
2023-01-19 06:45:37 +00:00
}
2022-12-19 16:18:16 +00:00
}
2023-07-17 17:13:36 +00:00
.buttonStyle(.plain)
2022-12-19 16:18:16 +00:00
}
2023-12-18 07:22:59 +00:00
@ViewBuilder
private func defaultLinkPreview(_ title: String, _ url: URL) -> some View {
2023-12-18 07:22:59 +00:00
if let imageURL = card.image, !isInCaptureMode {
DefaultPreviewImage(url: imageURL, originalWidth: card.width, originalHeight: card.height)
2023-12-18 07:22:59 +00:00
}
2024-01-19 08:01:40 +00:00
VStack(alignment: .leading, spacing: 4) {
2024-01-11 18:53:21 +00:00
Text(title)
.font(.scaledHeadline)
.lineLimit(2)
2024-01-11 18:53:21 +00:00
if let description = card.description, !description.isEmpty {
Text(description)
2024-01-19 08:01:40 +00:00
.font(.scaledFootnote)
2024-01-11 18:53:21 +00:00
.foregroundStyle(.secondary)
2023-12-18 07:22:59 +00:00
.lineLimit(3)
}
2024-01-11 18:53:21 +00:00
Text(url.host() ?? url.absoluteString)
.font(.scaledFootnote)
.foregroundColor(theme.tintColor)
.lineLimit(1)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(10)
}
2024-02-10 10:26:22 +00:00
private func compactLinkPreview(_ title: String, _ url: URL) -> some View {
HStack(alignment: .top) {
if let imageURL = card.image, !isInCaptureMode {
LazyResizableImage(url: imageURL) { state, _ in
if let image = state.image {
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: imageHeight, height: imageHeight)
.clipShape(RoundedRectangle(cornerRadius: 8))
.clipped()
} else if state.isLoading {
Rectangle()
.fill(Color.gray)
.frame(width: imageHeight, height: imageHeight)
}
}
// This image is decorative
.accessibilityHidden(true)
.frame(width: imageHeight, height: imageHeight)
}
VStack(alignment: .leading, spacing: 6) {
Text(title)
.font(.scaledHeadline)
.lineLimit(3)
Text(url.host() ?? url.absoluteString)
.font(.scaledFootnote)
.foregroundColor(theme.tintColor)
.lineLimit(1)
if let history = card.history {
let uses = history.compactMap{ Int($0.accounts )}.reduce(0, +)
HStack(spacing: 4) {
Image(systemName: "bubble.left.and.text.bubble.right")
Text("trending-tag-people-talking \(uses)")
Spacer()
Button {
#if targetEnvironment(macCatalyst)
openWindow(value: WindowDestinationEditor.quoteLinkStatusEditor(link: url))
#else
routerPath.presentedSheet = .quoteLinkStatusEditor(link: url)
#endif
} label: {
Image(systemName: "quote.opening")
}
.buttonStyle(.plain)
}
.font(.scaledCaption)
.foregroundStyle(.secondary)
.lineLimit(1)
.padding(.top, 12)
}
}
.padding(.horizontal, 8)
}
}
private func iconLinkPreview(_ title: String, _ url: URL) -> some View {
// ..where the image is known to be a square icon
HStack {
if let imageURL = card.image, !isInCaptureMode {
2023-12-18 07:22:59 +00:00
LazyResizableImage(url: imageURL) { state, _ in
if let image = state.image {
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: imageHeight, height: imageHeight)
.clipped()
} else if state.isLoading {
Rectangle()
.fill(Color.gray)
.frame(width: imageHeight, height: imageHeight)
}
}
// This image is decorative
.accessibilityHidden(true)
.frame(width: imageHeight, height: imageHeight)
}
2023-12-18 07:22:59 +00:00
VStack(alignment: .leading, spacing: 6) {
Text(title)
.font(.scaledHeadline)
.lineLimit(3)
if let description = card.description, !description.isEmpty {
Text(description)
.font(.scaledBody)
.foregroundStyle(.secondary)
.lineLimit(3)
}
Text(url.host() ?? url.absoluteString)
.font(.scaledFootnote)
.foregroundColor(theme.tintColor)
.lineLimit(1)
}.padding(16)
}
}
2022-12-19 16:18:16 +00:00
}
struct DefaultPreviewImage: View {
@Environment(Theme.self) private var theme
let url: URL
let originalWidth: CGFloat
let originalHeight: CGFloat
var body: some View {
_Layout(originalWidth: originalWidth, originalHeight: originalHeight) {
LazyResizableImage(url: url) { state, _ in
Rectangle()
.fill(theme.secondaryBackgroundColor)
.overlay {
if let image = state.image {
image.resizable().scaledToFill()
}
}
}
.accessibilityHidden(true) // This image is decorative
.clipped()
}
}
private struct _Layout: Layout {
let originalWidth: CGFloat
let originalHeight: CGFloat
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
guard !subviews.isEmpty else { return CGSize.zero }
return calculateSize(proposal)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
guard let view = subviews.first else { return }
let size = calculateSize(proposal)
view.place(at: bounds.origin, proposal: ProposedViewSize(size))
}
private func calculateSize(_ proposal: ProposedViewSize) -> CGSize {
switch (proposal.width, proposal.height) {
case (nil, nil):
CGSize(width: originalWidth, height: originalWidth)
case let (nil, .some(height)):
CGSize(width: originalWidth, height: min(height, originalWidth))
case (0, _):
CGSize.zero
case let (.some(width), _):
if originalWidth == 0 {
CGSize(width: width, height: width / 2)
} else {
CGSize(width: width, height: width / originalWidth * originalHeight)
}
}
}
}
}