IceCubesApp/Packages/Status/Sources/Status/Row/Subviews/StatusRowMediaPreviewView.swift

330 lines
10 KiB
Swift
Raw Normal View History

2023-01-17 10:36:01 +00:00
import DesignSystem
2022-12-22 09:53:36 +00:00
import Env
2023-01-17 10:36:01 +00:00
import Models
2023-01-22 05:38:30 +00:00
import Nuke
2022-12-25 06:43:02 +00:00
import NukeUI
2023-01-17 10:36:01 +00:00
import SwiftUI
2022-12-17 12:37:46 +00:00
public struct StatusRowMediaPreviewView: View {
2023-02-19 14:29:07 +00:00
@Environment(\.isSecondaryColumn) private var isSecondaryColumn: Bool
@Environment(\.extraLeadingInset) private var extraLeadingInset: CGFloat
2023-02-19 14:29:07 +00:00
@Environment(\.isInCaptureMode) private var isInCaptureMode: Bool
2023-01-22 05:38:30 +00:00
@EnvironmentObject var sceneDelegate: SceneDelegate
@EnvironmentObject private var preferences: UserPreferences
2022-12-22 09:53:36 +00:00
@EnvironmentObject private var quickLook: QuickLook
@EnvironmentObject private var theme: Theme
2023-01-17 10:36:01 +00:00
public let attachments: [MediaAttachment]
public let sensitive: Bool
2023-01-07 16:44:25 +00:00
public let isNotifications: Bool
2022-12-24 07:29:45 +00:00
2022-12-22 09:53:36 +00:00
@State private var isQuickLookLoading: Bool = false
2023-01-03 07:45:27 +00:00
@State private var altTextDisplayed: String?
@State private var isAltAlertDisplayed: Bool = false
@State private var isHidingMedia: Bool = false
2023-01-17 10:36:01 +00:00
var availableWidth: CGFloat {
if UIDevice.current.userInterfaceIdiom == .phone &&
2023-02-22 18:09:39 +00:00
(UIDevice.current.orientation == .landscapeLeft || UIDevice.current.orientation == .landscapeRight) || theme.statusDisplayStyle == .medium
{
return sceneDelegate.windowWidth * 0.80
}
return sceneDelegate.windowWidth
}
var appLayoutWidth: CGFloat {
let avatarColumnWidth = theme.avatarPosition == .leading ? AvatarView.Size.status.size.width + .statusColumnsSpacing : 0
var sidebarWidth: CGFloat = 0
var secondaryColumnWidth: CGFloat = 0
2023-02-21 17:46:28 +00:00
let layoutPading: CGFloat = .layoutPadding * 2
2023-02-22 18:09:39 +00:00
if UIDevice.current.userInterfaceIdiom == .pad {
sidebarWidth = .sidebarWidth
if preferences.showiPadSecondaryColumn {
secondaryColumnWidth = .secondaryColumnWidth
}
}
return layoutPading + avatarColumnWidth + sidebarWidth + extraLeadingInset + secondaryColumnWidth
}
private var imageMaxHeight: CGFloat {
2023-01-07 16:44:25 +00:00
if isNotifications {
2022-12-29 16:22:07 +00:00
return 50
}
2023-01-07 16:44:25 +00:00
if theme.statusDisplayStyle == .compact {
2023-02-22 06:26:32 +00:00
if attachments.count == 1 {
return 200
}
2023-01-07 16:44:25 +00:00
return 100
}
if attachments.count == 1 {
return 300
}
2023-01-19 06:45:42 +00:00
return attachments.count > 2 ? 150 : 200
}
2023-01-17 10:36:01 +00:00
private func size(for media: MediaAttachment) -> CGSize? {
2023-01-05 12:27:04 +00:00
if let width = media.meta?.original?.width,
2023-01-17 10:36:01 +00:00
let height = media.meta?.original?.height
{
return .init(width: CGFloat(width), height: CGFloat(height))
}
return nil
}
2023-01-17 10:36:01 +00:00
private func imageSize(from: CGSize, newWidth: CGFloat) -> CGSize {
2023-01-29 16:37:15 +00:00
if isNotifications || theme.statusDisplayStyle == .compact || isSecondaryColumn {
return .init(width: imageMaxHeight, height: imageMaxHeight)
2022-12-29 16:22:07 +00:00
}
let ratio = newWidth / from.width
let newHeight = from.height * ratio
return .init(width: newWidth, height: newHeight)
}
2023-01-17 10:36:01 +00:00
2022-12-17 12:37:46 +00:00
public var body: some View {
Group {
if attachments.count == 1, let attachment = attachments.first {
makeFeaturedImagePreview(attachment: attachment)
.onTapGesture {
Task {
await quickLook.prepareFor(urls: attachments.compactMap { $0.url }, selectedURL: attachment.url!)
}
}
} else {
2023-01-07 16:44:25 +00:00
if isNotifications || theme.statusDisplayStyle == .compact {
HStack {
makeAttachmentView(for: 0)
makeAttachmentView(for: 1)
makeAttachmentView(for: 2)
makeAttachmentView(for: 3)
}
2022-12-29 16:22:07 +00:00
} else {
VStack {
HStack {
makeAttachmentView(for: 0)
makeAttachmentView(for: 1)
}
2022-12-29 16:22:07 +00:00
HStack {
makeAttachmentView(for: 2)
makeAttachmentView(for: 3)
}
}
2022-12-17 12:37:46 +00:00
}
}
}
2022-12-22 09:53:36 +00:00
.overlay {
if quickLook.isPreparing {
quickLookLoadingView
2022-12-22 09:53:36 +00:00
.transition(.opacity)
}
2023-01-17 10:36:01 +00:00
if isHidingMedia {
sensitiveMediaOverlay
.transition(.opacity)
}
2022-12-19 16:18:16 +00:00
}
.alert("status.editor.media.image-description",
2023-01-03 07:45:27 +00:00
isPresented: $isAltAlertDisplayed) {
Button("alert.button.ok", action: {})
2023-01-03 07:45:27 +00:00
} message: {
Text(altTextDisplayed ?? "")
}
.onAppear {
if sensitive && preferences.autoExpandMedia == .hideSensitive {
isHidingMedia = true
} else if preferences.autoExpandMedia == .hideAll {
isHidingMedia = true
} else {
isHidingMedia = false
}
}
}
2023-01-17 10:36:01 +00:00
2022-12-29 16:22:07 +00:00
@ViewBuilder
private func makeAttachmentView(for index: Int) -> some View {
if attachments.count > index {
makePreview(attachment: attachments[index])
2022-12-29 16:22:07 +00:00
}
}
2023-01-17 10:36:01 +00:00
@ViewBuilder
private func makeFeaturedImagePreview(attachment: MediaAttachment) -> some View {
ZStack(alignment: .bottomTrailing) {
2023-02-12 09:53:59 +00:00
let size: CGSize = size(for: attachment) ?? .init(width: imageMaxHeight, height: imageMaxHeight)
let newSize = imageSize(from: size, newWidth: availableWidth - appLayoutWidth)
switch attachment.supportedType {
case .image:
LazyImage(url: attachment.url) { state in
if let image = state.image {
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: newSize.width, height: newSize.height)
.clipped()
.cornerRadius(4)
} else {
RoundedRectangle(cornerRadius: 4)
.fill(Color.gray)
.frame(width: newSize.width, height: newSize.height)
}
}
.processors([.resize(size: newSize)])
.frame(width: newSize.width, height: newSize.height)
2023-02-12 15:29:41 +00:00
case .gifv, .video, .audio:
if let url = attachment.url {
VideoPlayerView(viewModel: .init(url: url))
2023-02-12 09:53:59 +00:00
.frame(width: newSize.width, height: newSize.height)
}
case .none:
EmptyView()
}
2023-02-19 14:29:07 +00:00
if !isInCaptureMode, sensitive {
cornerSensitiveButton
}
2023-02-19 14:29:07 +00:00
if !isInCaptureMode, let alt = attachment.description, !alt.isEmpty, !isNotifications, preferences.showAltTextForMedia {
Group {
Button {
altTextDisplayed = alt
isAltAlertDisplayed = true
} label: {
Text("status.image.alt-text.abbreviation")
.font(theme.statusDisplayStyle == .compact ? .footnote : .body)
}
.buttonStyle(.borderless)
.padding(4)
.background(.thinMaterial)
.cornerRadius(4)
}
.padding(theme.statusDisplayStyle == .compact ? 0 : 10)
2022-12-27 15:16:25 +00:00
}
}
2022-12-19 16:18:16 +00:00
}
2023-01-17 10:36:01 +00:00
2022-12-19 16:18:16 +00:00
@ViewBuilder
private func makePreview(attachment: MediaAttachment) -> some View {
2023-02-19 15:03:27 +00:00
if let type = attachment.supportedType, !isInCaptureMode {
2022-12-20 07:14:57 +00:00
Group {
GeometryReader { proxy in
switch type {
case .image:
let width = isNotifications ? imageMaxHeight : proxy.frame(in: .local).width
2023-02-19 14:29:07 +00:00
let processors: [ImageProcessing] = [.resize(size: .init(width: width, height: imageMaxHeight))]
2023-01-03 07:45:27 +00:00
ZStack(alignment: .bottomTrailing) {
2023-02-19 15:03:27 +00:00
LazyImage(url: attachment.previewUrl ?? attachment.url) { state in
if let image = state.image {
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(maxWidth: width)
.frame(maxHeight: imageMaxHeight)
.clipped()
.cornerRadius(4)
} else if state.isLoading {
RoundedRectangle(cornerRadius: 4)
.fill(Color.gray)
.frame(maxHeight: imageMaxHeight)
.frame(maxWidth: width)
2023-01-03 07:45:27 +00:00
}
}
2023-02-19 15:03:27 +00:00
.processors(processors)
2023-02-19 14:29:07 +00:00
if sensitive, !isInCaptureMode {
cornerSensitiveButton
}
2023-02-19 14:29:07 +00:00
if !isInCaptureMode,
2023-02-21 06:23:42 +00:00
let alt = attachment.description,
2023-02-19 14:29:07 +00:00
!alt.isEmpty,
!isNotifications,
2023-02-21 06:23:42 +00:00
preferences.showAltTextForMedia
{
2023-01-03 07:45:27 +00:00
Button {
altTextDisplayed = alt
isAltAlertDisplayed = true
} label: {
Text("status.image.alt-text.abbreviation")
.font(.scaledFootnote)
2023-01-03 07:45:27 +00:00
}
.buttonStyle(.borderless)
2023-01-03 07:45:27 +00:00
.padding(4)
.background(.thinMaterial)
.cornerRadius(4)
2022-12-19 16:18:16 +00:00
}
2022-12-25 06:43:02 +00:00
}
case .gifv, .video, .audio:
if let url = attachment.url {
2022-12-27 15:16:25 +00:00
VideoPlayerView(viewModel: .init(url: url))
2023-01-17 10:36:01 +00:00
.frame(width: isNotifications ? imageMaxHeight : proxy.frame(in: .local).width)
2022-12-27 15:16:25 +00:00
.frame(height: imageMaxHeight)
}
2022-12-19 16:18:16 +00:00
}
}
.frame(maxWidth: isNotifications ? imageMaxHeight : nil)
.frame(height: imageMaxHeight)
2022-12-20 07:14:57 +00:00
}
2023-02-22 06:04:02 +00:00
// #965: do not create overlapping tappable areas, when multiple images are shown
.contentShape(Rectangle())
2022-12-20 07:14:57 +00:00
.onTapGesture {
2022-12-22 09:53:36 +00:00
Task {
await quickLook.prepareFor(urls: attachments.compactMap { $0.url }, selectedURL: attachment.url!)
2022-12-22 09:53:36 +00:00
}
2022-12-17 12:37:46 +00:00
}
2022-12-19 15:01:23 +00:00
}
2022-12-17 12:37:46 +00:00
}
2023-01-17 10:36:01 +00:00
2022-12-22 09:53:36 +00:00
private var quickLookLoadingView: some View {
ZStack(alignment: .center) {
VStack {
Spacer()
HStack {
Spacer()
ProgressView()
Spacer()
2022-12-19 18:04:07 +00:00
}
2022-12-22 09:53:36 +00:00
Spacer()
2022-12-19 18:04:07 +00:00
}
}
2022-12-22 09:53:36 +00:00
.background(.ultraThinMaterial)
2022-12-19 18:04:07 +00:00
}
2023-01-17 10:36:01 +00:00
private var sensitiveMediaOverlay: some View {
ZStack {
Rectangle()
.foregroundColor(.clear)
.background(.ultraThinMaterial)
if !isNotifications {
Button {
withAnimation {
isHidingMedia = false
}
} label: {
Group {
if sensitive {
Label("status.media.sensitive.show", systemImage: "eye")
} else {
Label("status.media.content.show", systemImage: "eye")
}
}
.foregroundColor(theme.labelColor)
}
.buttonStyle(.borderedProminent)
}
}
}
2023-01-17 10:36:01 +00:00
private var cornerSensitiveButton: some View {
2023-01-22 05:38:30 +00:00
HStack {
Button {
withAnimation {
isHidingMedia = true
}
} label: {
Image(systemName: "eye.slash")
2023-01-22 05:38:30 +00:00
.frame(minHeight: 21) // Match the alt button in case it is also present
}
.padding(10)
.buttonStyle(.borderedProminent)
Spacer()
}
}
2022-12-17 12:37:46 +00:00
}