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
|
2022-12-25 06:43:02 +00:00
|
|
|
import NukeUI
|
2023-01-17 10:36:01 +00:00
|
|
|
import Shimmer
|
|
|
|
import SwiftUI
|
2023-01-19 06:56:24 +00:00
|
|
|
import Nuke
|
2022-12-17 12:37:46 +00:00
|
|
|
|
|
|
|
public struct StatusMediaPreviewView: View {
|
2023-01-19 06:56:24 +00:00
|
|
|
@Environment(\.openURL) private var openURL
|
|
|
|
|
2023-01-09 19:39:42 +00:00
|
|
|
@EnvironmentObject private var preferences: UserPreferences
|
2022-12-22 09:53:36 +00:00
|
|
|
@EnvironmentObject private var quickLook: QuickLook
|
2022-12-31 11:29:19 +00:00
|
|
|
@EnvironmentObject private var theme: Theme
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2023-01-17 14:14:50 +00:00
|
|
|
public let attachments: [MediaAttachment]
|
2023-01-09 19:39:42 +00:00
|
|
|
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
|
2022-12-27 05:44:31 +00:00
|
|
|
@State private var width: CGFloat = 0
|
2023-01-03 07:45:27 +00:00
|
|
|
@State private var altTextDisplayed: String?
|
|
|
|
@State private var isAltAlertDisplayed: Bool = false
|
2023-01-09 19:39:42 +00:00
|
|
|
@State private var isHidingMedia: Bool = false
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2022-12-24 07:01:35 +00:00
|
|
|
private var imageMaxHeight: CGFloat {
|
2023-01-07 16:44:25 +00:00
|
|
|
if isNotifications {
|
2023-01-17 06:54:59 +00:00
|
|
|
if UIDevice.current.userInterfaceIdiom == .pad {
|
|
|
|
return 150
|
|
|
|
}
|
2022-12-29 16:22:07 +00:00
|
|
|
return 50
|
|
|
|
}
|
2023-01-07 16:44:25 +00:00
|
|
|
if theme.statusDisplayStyle == .compact {
|
|
|
|
return 100
|
|
|
|
}
|
2023-01-17 14:14:50 +00:00
|
|
|
if attachments.count == 1 {
|
2022-12-24 07:01:35 +00:00
|
|
|
return 300
|
|
|
|
}
|
2023-01-19 06:45:42 +00:00
|
|
|
return attachments.count > 2 ? 150 : 200
|
2022-12-24 07:01:35 +00:00
|
|
|
}
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2023-01-17 14:14:50 +00:00
|
|
|
private func size(for media: MediaAttachment) -> CGSize? {
|
2023-01-07 16:44:25 +00:00
|
|
|
if isNotifications {
|
2022-12-29 16:22:07 +00:00
|
|
|
return .init(width: 50, height: 50)
|
|
|
|
}
|
2023-01-07 16:44:25 +00:00
|
|
|
if theme.statusDisplayStyle == .compact {
|
|
|
|
return .init(width: 100, height: 100)
|
|
|
|
}
|
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
|
|
|
|
{
|
2022-12-27 05:44:31 +00:00
|
|
|
return .init(width: CGFloat(width), height: CGFloat(height))
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2022-12-27 05:44:31 +00:00
|
|
|
private func imageSize(from: CGSize, newWidth: CGFloat) -> CGSize {
|
2023-01-07 16:44:25 +00:00
|
|
|
if isNotifications {
|
2022-12-29 16:22:07 +00:00
|
|
|
return .init(width: 50, height: 50)
|
|
|
|
}
|
2022-12-27 05:44:31 +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 {
|
2022-12-24 07:01:35 +00:00
|
|
|
Group {
|
2023-01-17 14:14:50 +00:00
|
|
|
if attachments.count == 1, let attachment = attachments.first {
|
|
|
|
makeFeaturedImagePreview(attachment: attachment)
|
2022-12-24 07:01:35 +00:00
|
|
|
.onTapGesture {
|
|
|
|
Task {
|
2023-01-17 14:14:50 +00:00
|
|
|
await quickLook.prepareFor(urls: attachments.compactMap { $0.url }, selectedURL: attachment.url!)
|
2022-12-24 07:01:35 +00:00
|
|
|
}
|
|
|
|
}
|
2023-01-19 06:56:24 +00:00
|
|
|
.contextMenu {
|
|
|
|
contextMenuForMedia(mediaAttachement: attachment)
|
|
|
|
}
|
2022-12-24 07:01:35 +00:00
|
|
|
} else {
|
2023-01-07 16:44:25 +00:00
|
|
|
if isNotifications || theme.statusDisplayStyle == .compact {
|
2022-12-24 07:01:35 +00:00
|
|
|
HStack {
|
2023-01-17 14:14:50 +00:00
|
|
|
makeAttachmentView(for: 0)
|
|
|
|
makeAttachmentView(for: 1)
|
|
|
|
makeAttachmentView(for: 2)
|
|
|
|
makeAttachmentView(for: 3)
|
2022-12-24 07:01:35 +00:00
|
|
|
}
|
2022-12-29 16:22:07 +00:00
|
|
|
} else {
|
|
|
|
VStack {
|
|
|
|
HStack {
|
2023-01-17 14:14:50 +00:00
|
|
|
makeAttachmentView(for: 0)
|
|
|
|
makeAttachmentView(for: 1)
|
2022-12-24 07:01:35 +00:00
|
|
|
}
|
2022-12-29 16:22:07 +00:00
|
|
|
HStack {
|
2023-01-17 14:14:50 +00:00
|
|
|
makeAttachmentView(for: 2)
|
|
|
|
makeAttachmentView(for: 3)
|
2022-12-24 07:01:35 +00:00
|
|
|
}
|
|
|
|
}
|
2022-12-17 12:37:46 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-12-22 09:53:36 +00:00
|
|
|
.overlay {
|
|
|
|
if quickLook.isPreparing {
|
2022-12-24 07:01:35 +00:00
|
|
|
quickLookLoadingView
|
2022-12-22 09:53:36 +00:00
|
|
|
.transition(.opacity)
|
|
|
|
}
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2023-01-09 19:39:42 +00:00
|
|
|
if isHidingMedia {
|
|
|
|
sensitiveMediaOverlay
|
|
|
|
.transition(.opacity)
|
|
|
|
}
|
2022-12-19 16:18:16 +00:00
|
|
|
}
|
2023-01-19 17:14:08 +00:00
|
|
|
.alert("status.editor.media.image-description",
|
2023-01-03 07:45:27 +00:00
|
|
|
isPresented: $isAltAlertDisplayed) {
|
2023-01-19 17:14:08 +00:00
|
|
|
Button("alert.button.ok", action: {})
|
2023-01-03 07:45:27 +00:00
|
|
|
} message: {
|
|
|
|
Text(altTextDisplayed ?? "")
|
|
|
|
}
|
2023-01-09 19:39:42 +00:00
|
|
|
.onAppear {
|
2023-01-17 14:14:50 +00:00
|
|
|
if sensitive && preferences.serverPreferences?.autoExpandMedia == .hideSensitive {
|
2023-01-09 19:39:42 +00:00
|
|
|
isHidingMedia = true
|
2023-01-17 14:14:50 +00:00
|
|
|
} else if preferences.serverPreferences?.autoExpandMedia == .hideAll {
|
2023-01-09 19:39:42 +00:00
|
|
|
isHidingMedia = true
|
|
|
|
} else {
|
|
|
|
isHidingMedia = false
|
|
|
|
}
|
|
|
|
}
|
2022-12-24 07:01:35 +00:00
|
|
|
}
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2022-12-29 16:22:07 +00:00
|
|
|
@ViewBuilder
|
2023-01-17 14:14:50 +00:00
|
|
|
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
|
|
|
|
2022-12-24 07:01:35 +00:00
|
|
|
@ViewBuilder
|
2023-01-17 14:14:50 +00:00
|
|
|
private func makeFeaturedImagePreview(attachment: MediaAttachment) -> some View {
|
|
|
|
switch attachment.supportedType {
|
2022-12-24 07:01:35 +00:00
|
|
|
case .image:
|
2023-01-07 16:44:25 +00:00
|
|
|
if theme.statusDisplayStyle == .large,
|
2023-01-17 14:14:50 +00:00
|
|
|
let size = size(for: attachment),
|
2023-01-04 11:50:57 +00:00
|
|
|
UIDevice.current.userInterfaceIdiom != .pad,
|
2023-01-17 10:36:01 +00:00
|
|
|
UIDevice.current.userInterfaceIdiom != .mac
|
|
|
|
{
|
2023-01-03 06:41:29 +00:00
|
|
|
let avatarColumnWidth = theme.avatarPosition == .leading ? AvatarView.Size.status.size.width + .statusColumnsSpacing : 0
|
|
|
|
let availableWidth = UIScreen.main.bounds.width - (.layoutPadding * 2) - avatarColumnWidth
|
2022-12-27 05:44:31 +00:00
|
|
|
let newSize = imageSize(from: size,
|
2022-12-31 11:29:19 +00:00
|
|
|
newWidth: availableWidth)
|
2023-01-03 07:45:27 +00:00
|
|
|
ZStack(alignment: .bottomTrailing) {
|
2023-01-17 14:14:50 +00:00
|
|
|
LazyImage(url: attachment.url) { state in
|
2023-01-03 07:45:27 +00:00
|
|
|
if let image = state.image {
|
|
|
|
image
|
|
|
|
.resizingMode(.aspectFill)
|
|
|
|
.cornerRadius(4)
|
|
|
|
.frame(width: newSize.width, height: newSize.height)
|
|
|
|
} else {
|
|
|
|
RoundedRectangle(cornerRadius: 4)
|
|
|
|
.fill(Color.gray)
|
|
|
|
.frame(width: newSize.width, height: newSize.height)
|
|
|
|
.shimmering()
|
|
|
|
}
|
|
|
|
}
|
2023-01-11 17:35:06 +00:00
|
|
|
if sensitive {
|
|
|
|
cornerSensitiveButton
|
|
|
|
}
|
2023-01-17 14:14:50 +00:00
|
|
|
if let alt = attachment.description, !alt.isEmpty, !isNotifications {
|
2023-01-03 07:45:27 +00:00
|
|
|
Button {
|
|
|
|
altTextDisplayed = alt
|
|
|
|
isAltAlertDisplayed = true
|
|
|
|
} label: {
|
2023-01-19 17:14:08 +00:00
|
|
|
Text("status.image.alt-text.abbreviation")
|
2023-01-03 07:45:27 +00:00
|
|
|
}
|
|
|
|
.padding(8)
|
|
|
|
.background(.thinMaterial)
|
|
|
|
.cornerRadius(4)
|
2022-12-27 05:44:31 +00:00
|
|
|
}
|
2022-12-24 07:01:35 +00:00
|
|
|
}
|
2022-12-27 05:44:31 +00:00
|
|
|
} else {
|
|
|
|
AsyncImage(
|
2023-01-17 14:14:50 +00:00
|
|
|
url: attachment.url,
|
2023-01-17 10:36:01 +00:00
|
|
|
content: { image in
|
|
|
|
image
|
|
|
|
.resizable()
|
2023-01-17 18:41:46 +00:00
|
|
|
.aspectRatio(contentMode: .fit)
|
|
|
|
.frame(maxHeight: isNotifications ? imageMaxHeight : nil)
|
2023-01-17 10:36:01 +00:00
|
|
|
.cornerRadius(4)
|
|
|
|
},
|
|
|
|
placeholder: {
|
|
|
|
RoundedRectangle(cornerRadius: 4)
|
|
|
|
.fill(Color.gray)
|
2023-01-19 06:45:42 +00:00
|
|
|
.frame(height: imageMaxHeight)
|
2023-01-17 10:36:01 +00:00
|
|
|
.shimmering()
|
|
|
|
}
|
|
|
|
)
|
2022-12-27 05:44:31 +00:00
|
|
|
}
|
2023-01-08 15:18:38 +00:00
|
|
|
case .gifv, .video, .audio:
|
2023-01-17 14:14:50 +00:00
|
|
|
if let url = attachment.url {
|
2022-12-27 15:16:25 +00:00
|
|
|
VideoPlayerView(viewModel: .init(url: url))
|
|
|
|
.frame(height: imageMaxHeight)
|
|
|
|
}
|
2022-12-24 07:01:35 +00:00
|
|
|
case .none:
|
|
|
|
EmptyView()
|
|
|
|
}
|
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
|
2023-01-17 14:14:50 +00:00
|
|
|
private func makePreview(attachment: MediaAttachment) -> some View {
|
|
|
|
if let type = attachment.supportedType {
|
2022-12-20 07:14:57 +00:00
|
|
|
Group {
|
|
|
|
GeometryReader { proxy in
|
|
|
|
switch type {
|
|
|
|
case .image:
|
2023-01-03 07:45:27 +00:00
|
|
|
ZStack(alignment: .bottomTrailing) {
|
2023-01-17 14:14:50 +00:00
|
|
|
LazyImage(url: attachment.url) { state in
|
2023-01-03 07:45:27 +00:00
|
|
|
if let image = state.image {
|
|
|
|
image
|
|
|
|
.resizingMode(.aspectFill)
|
|
|
|
.cornerRadius(4)
|
|
|
|
} else if state.isLoading {
|
|
|
|
RoundedRectangle(cornerRadius: 4)
|
|
|
|
.fill(Color.gray)
|
|
|
|
.frame(maxHeight: imageMaxHeight)
|
2023-01-17 18:41:46 +00:00
|
|
|
.frame(maxWidth: isNotifications ? imageMaxHeight : proxy.frame(in: .local).width)
|
2023-01-03 07:45:27 +00:00
|
|
|
.shimmering()
|
|
|
|
}
|
|
|
|
}
|
2023-01-17 18:41:46 +00:00
|
|
|
.frame(maxWidth: isNotifications ? imageMaxHeight : proxy.frame(in: .local).width)
|
2023-01-03 07:45:27 +00:00
|
|
|
.frame(height: imageMaxHeight)
|
2023-01-11 17:35:06 +00:00
|
|
|
if sensitive {
|
|
|
|
cornerSensitiveButton
|
|
|
|
}
|
2023-01-17 14:14:50 +00:00
|
|
|
if let alt = attachment.description, !alt.isEmpty, !isNotifications {
|
2023-01-03 07:45:27 +00:00
|
|
|
Button {
|
|
|
|
altTextDisplayed = alt
|
|
|
|
isAltAlertDisplayed = true
|
|
|
|
} label: {
|
2023-01-19 17:14:08 +00:00
|
|
|
Text("status.image.alt-text.abbreviation")
|
2023-01-17 18:41:46 +00:00
|
|
|
.font(.scaledFootnote)
|
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
|
|
|
}
|
2023-01-08 15:18:38 +00:00
|
|
|
case .gifv, .video, .audio:
|
2023-01-17 14:14:50 +00:00
|
|
|
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
|
|
|
}
|
|
|
|
}
|
2023-01-17 18:41:46 +00:00
|
|
|
.frame(maxWidth: isNotifications ? imageMaxHeight : nil)
|
2022-12-24 07:01:35 +00:00
|
|
|
.frame(height: imageMaxHeight)
|
2022-12-20 07:14:57 +00:00
|
|
|
}
|
|
|
|
.onTapGesture {
|
2022-12-22 09:53:36 +00:00
|
|
|
Task {
|
2023-01-17 14:14:50 +00:00
|
|
|
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
|
|
|
}
|
2023-01-19 06:56:24 +00:00
|
|
|
.contextMenu {
|
|
|
|
contextMenuForMedia(mediaAttachement: attachment)
|
|
|
|
}
|
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
|
|
|
|
2023-01-09 19:39:42 +00:00
|
|
|
private var sensitiveMediaOverlay: some View {
|
|
|
|
Rectangle()
|
|
|
|
.background(.ultraThinMaterial)
|
|
|
|
.overlay {
|
|
|
|
if !isNotifications {
|
|
|
|
Button {
|
|
|
|
withAnimation {
|
|
|
|
isHidingMedia = false
|
|
|
|
}
|
|
|
|
} label: {
|
|
|
|
if sensitive {
|
2023-01-19 17:14:08 +00:00
|
|
|
Label("status.media.sensitive.show", systemImage: "eye")
|
2023-01-09 19:39:42 +00:00
|
|
|
} else {
|
2023-01-19 17:14:08 +00:00
|
|
|
Label("status.media.content.show", systemImage: "eye")
|
2023-01-09 19:39:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
.buttonStyle(.borderedProminent)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2023-01-11 17:35:06 +00:00
|
|
|
private var cornerSensitiveButton: some View {
|
|
|
|
Button {
|
|
|
|
withAnimation {
|
|
|
|
isHidingMedia = true
|
|
|
|
}
|
|
|
|
} label: {
|
2023-01-17 10:36:01 +00:00
|
|
|
Image(systemName: "eye.slash")
|
2023-01-11 17:35:06 +00:00
|
|
|
}
|
|
|
|
.position(x: 30, y: 30)
|
|
|
|
.buttonStyle(.borderedProminent)
|
|
|
|
}
|
2023-01-19 06:56:24 +00:00
|
|
|
|
|
|
|
@ViewBuilder
|
|
|
|
private func contextMenuForMedia(mediaAttachement: MediaAttachment) -> some View {
|
|
|
|
if let url = mediaAttachement.url {
|
|
|
|
ShareLink(item: url) {
|
|
|
|
Label("Share this image", systemImage: "square.and.arrow.up")
|
|
|
|
}
|
|
|
|
Button { openURL(url) } label: {
|
|
|
|
Label("View in Browser", systemImage: "safari")
|
|
|
|
}
|
|
|
|
Divider()
|
|
|
|
Button {
|
|
|
|
Task {
|
|
|
|
do {
|
|
|
|
let image = try await ImagePipeline.shared.image(for: url).image
|
|
|
|
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
|
|
|
|
} catch { }
|
|
|
|
}
|
|
|
|
} label: {
|
|
|
|
Label("Save image", systemImage: "square.and.arrow.down")
|
|
|
|
}
|
|
|
|
Button {
|
|
|
|
Task {
|
|
|
|
do {
|
|
|
|
let image = try await ImagePipeline.shared.image(for: url).image
|
|
|
|
UIPasteboard.general.image = image
|
|
|
|
} catch { }
|
|
|
|
}
|
|
|
|
} label: {
|
|
|
|
Label("Copy image", systemImage: "doc.on.doc")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-12-17 12:37:46 +00:00
|
|
|
}
|