// // MockStatusView.swift // Tusker // // Created by Shadowfacts on 4/13/24. // Copyright © 2024 Shadowfacts. All rights reserved. // import SwiftUI import Pachyderm import WebURL struct MockStatusView: View { @ObservedObject private var preferences = Preferences.shared @ScaledMetric(relativeTo: .body) private var attachmentsLabelHeight = 17 var body: some View { HStack(alignment: .top, spacing: 8) { VStack(spacing: 4) { Image("AboutIcon") .resizable() .clipShape(RoundedRectangle(cornerRadius: preferences.avatarStyle.cornerRadiusFraction * 50)) .frame(width: 50, height: 50) MockMetaIndicatorsView() Spacer() } VStack(alignment: .leading, spacing: 4) { HStack(spacing: 4) { MockDisplayNameLabel() Text(verbatim: "@tusker@example.com") .foregroundStyle(.secondary) .font(.body.weight(.light)) .lineLimit(1) .truncationMode(.tail) .layoutPriority(-100) Spacer() Text("1h") .foregroundStyle(.secondary) .font(.body.weight(.light)) } MockStatusContentView() if preferences.showLinkPreviews { MockStatusCardView() .frame(height: StatusContentContainer.cardViewHeight) } MockAttachmentsContainerView() .aspectRatio(preferences.showAttachmentsInTimeline ? 16/9 : nil, contentMode: .fill) .frame(height: preferences.showAttachmentsInTimeline ? nil : attachmentsLabelHeight) .padding(.bottom, preferences.showAttachmentsInTimeline && preferences.hideActionsInTimeline ? 8 : 0) if !preferences.hideActionsInTimeline { MockStatusActionButtons() } } .layoutPriority(100) } } } private struct MockMetaIndicatorsView: UIViewRepresentable { @ObservedObject private var preferences = Preferences.shared func makeUIView(context: Context) -> StatusMetaIndicatorsView { let view = StatusMetaIndicatorsView() view.primaryAxis = .vertical view.secondaryAxisAlignment = .trailing return view } func updateUIView(_ uiView: StatusMetaIndicatorsView, context: Context) { var indicators: StatusMetaIndicatorsView.Indicator = [] if preferences.showIsStatusReplyIcon { indicators.insert(.reply) } if preferences.alwaysShowStatusVisibilityIcon { indicators.insert(.visibility) } uiView.setIndicators(indicators, visibility: .public) } } private struct MockDisplayNameLabel: View { @ObservedObject private var preferences = Preferences.shared @ScaledMetric(relativeTo: .body) private var emojiSize = 17 @State var textWithImage = Text("Tusker") var body: some View { displayName .font(.body.weight(.semibold)) // don't let the height change depending on whether emojis are present or not .frame(height: emojiSize) .task(id: emojiSize) { let size = CGSize(width: emojiSize, height: emojiSize) let renderer = UIGraphicsImageRenderer(size: size) let image = renderer.image { ctx in let bounds = CGRect(origin: .zero, size: size) UIBezierPath(roundedRect: bounds, cornerRadius: 2).addClip() UIImage(named: "AboutIcon")!.draw(in: bounds) } textWithImage = Text("Tusker \(Image(uiImage: image))") } } private var displayName: Text { if preferences.hideCustomEmojiInUsernames { Text("Tusker") } else { textWithImage } } } private struct MockStatusContentView: View { @ObservedObject private var preferences = Preferences.shared var body: some View { Text("This is an example post so you can check out how things look.\n\nThanks for using \(link)!") .lineLimit(nil) } private var link: Text { Text("Tusker") .foregroundColor(.accentColor) .underline(preferences.underlineTextLinks) } } private struct MockStatusCardView: UIViewRepresentable { func makeUIView(context: Context) -> StatusCardView { let view = StatusCardView() view.isUserInteractionEnabled = false let card = Card( url: WebURL("https://vaccor.space/tusker")!, title: "Tusker", description: "Tusker is an iOS app for Mastodon", image: WebURL("https://vaccor.space/tusker/img/icon.png")!, kind: .link ) view.updateUI(card: card, sensitive: false) return view } func updateUIView(_ uiView: StatusCardView, context: Context) { } } private actor MockAttachmentsGenerator { static let shared = MockAttachmentsGenerator() private var attachmentURLs: [URL]? func getAttachmentURLs(displayScale: CGFloat) -> [URL] { if let attachmentURLs, attachmentURLs.allSatisfy({ FileManager.default.fileExists(atPath: $0.path) }) { return attachmentURLs } let size = CGSize(width: 100, height: 100) let bounds = CGRect(origin: .zero, size: size) let format = UIGraphicsImageRendererFormat() format.scale = displayScale let renderer = UIGraphicsImageRenderer(size: size, format: format) let firstImage = renderer.image { ctx in UIColor(red: 0x56 / 255, green: 0x03 / 255, blue: 0xad / 255, alpha: 1).setFill() ctx.fill(bounds) ctx.cgContext.concatenate(CGAffineTransform(1, 0, -0.5, 1, 0, 0)) for minX in stride(from: 0, through: 100, by: 30) { UIColor(red: 0x83 / 255, green: 0x67 / 255, blue: 0xc7 / 255, alpha: 1).setFill() ctx.fill(CGRect(x: minX + 20, y: 0, width: 15, height: 100)) } } let secondImage = renderer.image { ctx in UIColor(red: 0x00 / 255, green: 0x43 / 255, blue: 0x85 / 255, alpha: 1).setFill() ctx.fill(bounds) UIColor(red: 0x05 / 255, green: 0xb2 / 255, blue: 0xdc / 255, alpha: 1).setFill() for y in 0..<2 { for x in 0..<4 { let rect = CGRect(x: x * 45 - 5, y: y * 50 + 15, width: 20, height: 20) ctx.cgContext.fillEllipse(in: rect) } } UIColor(red: 0x08 / 255, green: 0x7c / 255, blue: 0xa7 / 255, alpha: 1).setFill() for y in 0..<3 { for x in 0..<2 { let rect = CGRect(x: CGFloat(x) * 45 + 22.5, y: CGFloat(y) * 50 - 5, width: 10, height: 10) ctx.cgContext.fillEllipse(in: rect) } } } let tempDirectory = FileManager.default.temporaryDirectory let firstURL = tempDirectory.appendingPathComponent("\(UUID().description)", conformingTo: .png) let secondURL = tempDirectory.appendingPathComponent("\(UUID().description)", conformingTo: .png) do { try firstImage.pngData()!.write(to: firstURL) try secondImage.pngData()!.write(to: secondURL) attachmentURLs = [firstURL, secondURL] return [firstURL, secondURL] } catch { return [] } } } private struct MockAttachmentsContainerView: View { @State private var attachments: [Attachment] = [] @Environment(\.displayScale) private var displayScale var body: some View { MockAttachmentsContainerRepresentable(attachments: attachments) .task { let attachmentURLs = await MockAttachmentsGenerator.shared.getAttachmentURLs(displayScale: displayScale) self.attachments = [ .init(id: "1", kind: .image, url: attachmentURLs[0], description: "test"), .init(id: "2", kind: .image, url: attachmentURLs[1], description: nil), ] } } } private struct MockAttachmentsContainerRepresentable: UIViewRepresentable { let attachments: [Attachment] @ObservedObject private var preferences = Preferences.shared func makeUIView(context: Context) -> AttachmentsContainerView { let view = AttachmentsContainerView() view.isUserInteractionEnabled = false return view } func updateUIView(_ uiView: AttachmentsContainerView, context: Context) { uiView.updateUI(attachments: attachments, labelOnly: !preferences.showAttachmentsInTimeline) uiView.contentHidden = preferences.attachmentBlurMode == .always for attachmentView in uiView.attachmentViews.allObjects { attachmentView.updateBadges() } } } private struct MockStatusActionButtons: View { var body: some View { HStack(spacing: 0) { Image(systemName: "arrowshape.turn.up.left.fill") .foregroundStyle(.tint) Spacer() Image(systemName: "star.fill") .foregroundStyle(.tint) Spacer() Image(systemName: "repeat") .foregroundStyle(.yellow) Spacer() Image(systemName: "ellipsis") .foregroundStyle(.tint) Spacer() } } } #Preview { MockStatusView() .frame(height: 300) }