diff --git a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowMediaPreviewView.swift b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowMediaPreviewView.swift index edeb0b67..88539649 100644 --- a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowMediaPreviewView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowMediaPreviewView.swift @@ -9,11 +9,7 @@ import SwiftUI @MainActor public struct StatusRowMediaPreviewView: View { @Environment(\.openWindow) private var openWindow - @Environment(\.extraLeadingInset) private var extraLeadingInset: CGFloat @Environment(\.isMediaCompact) private var isCompact: Bool - @Environment(\.horizontalSizeClass) private var horizontalSizeClass - @Environment(SceneDelegate.self) private var sceneDelegate - @Environment(UserPreferences.self) private var preferences @Environment(QuickLook.self) private var quickLook @Environment(Theme.self) private var theme @@ -35,33 +31,6 @@ public struct StatusRowMediaPreviewView: View { private var scrollBottomPadding: CGFloat? = 0 #endif - var availableWidth: CGFloat { - #if os(visionOS) - return sceneDelegate.windowWidth * 0.96 - #else - if UIDevice.current.userInterfaceIdiom == .phone && - (UIDevice.current.orientation == .landscapeLeft || UIDevice.current.orientation == .landscapeRight) || theme.statusDisplayStyle == .medium - { - return sceneDelegate.windowWidth * 0.80 - } - return sceneDelegate.windowWidth - #endif - } - - var appLayoutWidth: CGFloat { - let avatarColumnWidth = theme.avatarPosition == .leading ? AvatarView.FrameConfig.status.width + .statusColumnsSpacing : 0 - var sidebarWidth: CGFloat = 0 - var secondaryColumnWidth: CGFloat = 0 - let layoutPading: CGFloat = .layoutPadding * 2 - if UIDevice.current.userInterfaceIdiom == .pad && horizontalSizeClass != .compact { - sidebarWidth = .sidebarWidth - if preferences.showiPadSecondaryColumn { - secondaryColumnWidth = .secondaryColumnWidth - } - } - return layoutPading + avatarColumnWidth + sidebarWidth + extraLeadingInset + secondaryColumnWidth - } - private var imageMaxHeight: CGFloat { if isCompact { return 50 @@ -80,11 +49,10 @@ public struct StatusRowMediaPreviewView: View { if attachments.count == 1 { FeaturedImagePreView( attachment: attachments[0], - imageMaxHeight: imageMaxHeight, - sensitive: sensitive, - appLayoutWidth: appLayoutWidth, - availableWidth: availableWidth, - availableHeight: sceneDelegate.windowHeight + maxSize: imageMaxHeight == 300 + ? nil + : CGSize(width: imageMaxHeight, height: imageMaxHeight), + sensitive: sensitive ) .accessibilityElement(children: .ignore) .accessibilityLabel(Self.accessibilityLabel(for: attachments[0])) @@ -152,11 +120,8 @@ private struct MediaPreview: View { let imageMaxHeight: CGFloat let displayData: DisplayData - @Environment(UserPreferences.self) private var preferences - @Environment(\.isCompact) private var isCompact: Bool - var body: some View { - GeometryReader { _ in + Group { switch displayData.type { case .image: LazyResizableImage(url: displayData.previewUrl) { state, _ in @@ -167,11 +132,11 @@ private struct MediaPreview: View { .frame(width: displayData.isLandscape ? imageMaxHeight * 1.2 : imageMaxHeight / 1.5, height: imageMaxHeight) .overlay( - RoundedRectangle(cornerRadius: 4) + RoundedRectangle(cornerRadius: 10) .stroke(.gray.opacity(0.35), lineWidth: 1) ) } else if state.isLoading { - RoundedRectangle(cornerRadius: 4) + RoundedRectangle(cornerRadius: 10) .fill(Color.gray) } } @@ -189,7 +154,7 @@ private struct MediaPreview: View { .frame(width: displayData.isLandscape ? imageMaxHeight * 1.2 : imageMaxHeight / 1.5, height: imageMaxHeight) .clipped() - .cornerRadius(4) + .cornerRadius(10) // #965: do not create overlapping tappable areas, when multiple images are shown .contentShape(Rectangle()) .accessibilityElement(children: .ignore) @@ -198,102 +163,6 @@ private struct MediaPreview: View { } } -@MainActor -private struct FeaturedImagePreView: View { - let attachment: MediaAttachment - let imageMaxHeight: CGFloat - let sensitive: Bool - let appLayoutWidth: CGFloat - let availableWidth: CGFloat - let availableHeight: CGFloat - - @Environment(\.isSecondaryColumn) private var isSecondaryColumn: Bool - @Environment(Theme.self) private var theme - @Environment(\.isCompact) private var isCompact: Bool - @Environment(\.isModal) private var isModal: Bool - - var body: some View { - let size: CGSize = size(for: attachment) ?? .init(width: imageMaxHeight, height: imageMaxHeight) - let newSize = imageSize(from: size) - Group { - switch attachment.supportedType { - case .image: - LazyImage(url: attachment.url) { state in - if let image = state.image { - image - .resizable() - .aspectRatio(contentMode: .fill) - .overlay( - RoundedRectangle(cornerRadius: 4) - .stroke(.gray.opacity(0.35), lineWidth: 1) - ) - } else { - RoundedRectangle(cornerRadius: 4).fill(Color.gray) - } - } - .processors([.resize(size: newSize)]) - case .gifv, .video, .audio: - if let url = attachment.url { - MediaUIAttachmentVideoView(viewModel: .init(url: url)) - } - case .none: - EmptyView() - } - } - .frame(width: newSize.width, height: newSize.height) - .overlay { - BlurOverLay(sensitive: sensitive, font: .scaledFootnote) - } - .overlay { - AltTextButton( - text: attachment.description, - font: theme.statusDisplayStyle == .compact ? .footnote : .body - ) - } - .clipped() - .cornerRadius(4) - } - - private func size(for media: MediaAttachment) -> CGSize? { - guard let width = media.meta?.original?.width, - let height = media.meta?.original?.height - else { return nil } - - guard width != 1 && height != 1 else { - return .init(width: 800, height: 600) - } - - return .init(width: CGFloat(width), height: CGFloat(height)) - } - - private func imageSize(from: CGSize) -> CGSize { - if isCompact || theme.statusDisplayStyle == .compact || isSecondaryColumn { - return .init(width: imageMaxHeight, height: imageMaxHeight) - } - - var boxWidth = availableWidth - appLayoutWidth - if isModal && - (UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac) { - boxWidth = availableWidth * 0.50 - } - let boxHeight = availableHeight * 0.8 // use only 80% of window height to leave room for text - - if from.width <= boxWidth, from.height <= boxHeight { - // intrinsic size of image fits just fine - return from - } - - // shrink image proportionally to fit inside the box - let xRatio = boxWidth / from.width - let yRatio = boxHeight / from.height - if xRatio < yRatio { - return .init(width: boxWidth, height: from.height * xRatio) - } else { - return .init(width: from.width * yRatio, height: boxHeight) - } - } -} - @MainActor struct BlurOverLay: View { let sensitive: Bool @@ -517,3 +386,137 @@ struct WrapperForPreview: View { private static let attachment = MediaAttachment.imageWith(url: url) private static let local = Locale(identifier: "en") } + +@MainActor +private struct FeaturedImagePreView: View { + let attachment: MediaAttachment + let maxSize: CGSize? + let sensitive: Bool + + @Environment(\.isSecondaryColumn) private var isSecondaryColumn: Bool + @Environment(Theme.self) private var theme + @Environment(\.isCompact) private var isCompact: Bool + @Environment(\.isModal) private var isModal: Bool + + private var originalWidth: CGFloat { + CGFloat(attachment.meta?.original?.width ?? 300) + } + + private var originalHeight: CGFloat { + CGFloat(attachment.meta?.original?.height ?? 300) + } + + var body: some View { + if let url = attachment.url { + _Layout(originalWidth: originalWidth, originalHeight: originalHeight, maxSize: maxSize) { + Group { + RoundedRectangle(cornerRadius: 10).fill(Color.gray) + .overlay { + switch attachment.supportedType { + case .image: + LazyResizableImage(url: attachment.url) { state, _ in + if let image = state.image { + image + .resizable() + .scaledToFill() + } else { + RoundedRectangle(cornerRadius: 10).fill(Color.gray) + } + } + case .gifv, .video, .audio: + MediaUIAttachmentVideoView(viewModel: .init(url: url)) + default: + EmptyView() + } + } + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(.gray.opacity(0.35), lineWidth: 1) + ) + } + } + .overlay { + BlurOverLay(sensitive: sensitive, font: .scaledFootnote) + } + .overlay { + AltTextButton( + text: attachment.description, + font: theme.statusDisplayStyle == .compact ? .footnote : .body + ) + } + .clipped() + .cornerRadius(10) + } + } + + private struct _Layout: Layout { + let originalWidth: CGFloat + let originalHeight: CGFloat + let maxSize: CGSize? + + init(originalWidth: CGFloat?, originalHeight: CGFloat?, maxSize: CGSize?) { + self.originalWidth = originalWidth ?? 200 + self.originalHeight = originalHeight ?? 200 + self.maxSize = maxSize + } + + func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { + guard !subviews.isEmpty else { return CGSize.zero } + + if let maxSize { return maxSize } + + return calculateSize(proposal) + } + + func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { + guard let view = subviews.first else { return } + + let size = if let maxSize { maxSize } else { calculateSize(proposal) } + view.place(at: bounds.origin, proposal: ProposedViewSize(size)) + } + + private func calculateSize(_ proposal: ProposedViewSize) -> CGSize { + var size: CGSize + switch (proposal.width, proposal.height) { + case (0, _), (_, 0): + size = CGSize.zero + + case (nil, nil), (nil, .some(.infinity)), (.some(.infinity), .some(.infinity)), (.some(.infinity), nil): + size = CGSize(width: originalWidth, height: originalWidth) + + case let (nil, .some(height)), let (.some(.infinity), .some(height)): + let minHeight = min(height, originalWidth) + if originalHeight == 0 { + size = CGSize.zero + } else { + size = CGSize(width: originalWidth * minHeight / originalHeight, height: minHeight) + } + + case let (.some(width), .some(.infinity)), let (.some(width), nil): + if originalWidth == 0 { + size = CGSize(width: width, height: width) + } else { + size = CGSize(width: width, height: width / originalWidth * originalHeight) + } + + case let (.some(width), .some(height)): + // intrinsic size of image fits just fine + if originalWidth <= width, originalHeight <= height { + size = CGSize(width: originalWidth, height: originalHeight) + } + + // shrink image proportionally to fit inside the box + let xRatio = width / originalWidth + let yRatio = height / originalHeight + // use small ratio to fit the image in + if xRatio < yRatio { + size = CGSize(width: width, height: originalHeight * xRatio) + } else { + size = CGSize(width: originalWidth * yRatio, height: height) + } + } + + return CGSize(width: max(size.width, 200), height: min(size.height, 450)) + } + } +}