Tusker/Tusker/Screens/Preferences/Appearance/MockStatusView.swift

271 lines
9.7 KiB
Swift

//
// 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)
}