mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2025-02-18 02:35:15 +00:00
Refactor StatusRowMediaPreviewView
(#1654)
* improve the sensitive content overlay animation and refactor subviews * fix alt text button and refactor views * refactor `StatusRowMediaPreviewView.onTapGesture` * simplify `MediaPreview` and `FeaturedImagePreView` * make alt text button adaptable
This commit is contained in:
parent
9e4b333981
commit
6e1e83cace
2 changed files with 385 additions and 237 deletions
|
@ -50,7 +50,7 @@ public struct MediaAttachment: Codable, Identifiable, Hashable, Equatable {
|
||||||
type: "image",
|
type: "image",
|
||||||
url: url,
|
url: url,
|
||||||
previewUrl: url,
|
previewUrl: url,
|
||||||
description: nil,
|
description: "demo alt text here",
|
||||||
meta: nil)
|
meta: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,11 +9,8 @@ import SwiftUI
|
||||||
@MainActor
|
@MainActor
|
||||||
public struct StatusRowMediaPreviewView: View {
|
public struct StatusRowMediaPreviewView: View {
|
||||||
@Environment(\.openWindow) private var openWindow
|
@Environment(\.openWindow) private var openWindow
|
||||||
@Environment(\.isSecondaryColumn) private var isSecondaryColumn: Bool
|
|
||||||
@Environment(\.extraLeadingInset) private var extraLeadingInset: CGFloat
|
@Environment(\.extraLeadingInset) private var extraLeadingInset: CGFloat
|
||||||
@Environment(\.isInCaptureMode) private var isInCaptureMode: Bool
|
|
||||||
@Environment(\.isCompact) private var isCompact: Bool
|
@Environment(\.isCompact) private var isCompact: Bool
|
||||||
|
|
||||||
@Environment(SceneDelegate.self) private var sceneDelegate
|
@Environment(SceneDelegate.self) private var sceneDelegate
|
||||||
@Environment(UserPreferences.self) private var preferences
|
@Environment(UserPreferences.self) private var preferences
|
||||||
@Environment(QuickLook.self) private var quickLook
|
@Environment(QuickLook.self) private var quickLook
|
||||||
|
@ -23,9 +20,6 @@ public struct StatusRowMediaPreviewView: View {
|
||||||
public let sensitive: Bool
|
public let sensitive: Bool
|
||||||
|
|
||||||
@State private var isQuickLookLoading: Bool = false
|
@State private var isQuickLookLoading: Bool = false
|
||||||
@State private var altTextDisplayed: String?
|
|
||||||
@State private var isAltAlertDisplayed: Bool = false
|
|
||||||
@State private var isHidingMedia: Bool = false
|
|
||||||
|
|
||||||
var availableWidth: CGFloat {
|
var availableWidth: CGFloat {
|
||||||
if UIDevice.current.userInterfaceIdiom == .phone &&
|
if UIDevice.current.userInterfaceIdiom == .phone &&
|
||||||
|
@ -66,39 +60,20 @@ public struct StatusRowMediaPreviewView: View {
|
||||||
return attachments.count > 2 ? 150 : 200
|
return attachments.count > 2 ? 150 : 200
|
||||||
}
|
}
|
||||||
|
|
||||||
private func size(for media: MediaAttachment) -> CGSize? {
|
|
||||||
if let width = media.meta?.original?.width,
|
|
||||||
let height = media.meta?.original?.height
|
|
||||||
{
|
|
||||||
return .init(width: CGFloat(width), height: CGFloat(height))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
private func imageSize(from: CGSize, newWidth: CGFloat) -> CGSize {
|
|
||||||
if isCompact || theme.statusDisplayStyle == .compact || isSecondaryColumn {
|
|
||||||
return .init(width: imageMaxHeight, height: imageMaxHeight)
|
|
||||||
}
|
|
||||||
let ratio = newWidth / from.width
|
|
||||||
let newHeight = from.height * ratio
|
|
||||||
return .init(width: newWidth, height: newHeight)
|
|
||||||
}
|
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
Group {
|
Group {
|
||||||
if attachments.count == 1, let attachment = attachments.first {
|
if attachments.count == 1 {
|
||||||
makeFeaturedImagePreview(attachment: attachment)
|
FeaturedImagePreView(
|
||||||
.onTapGesture {
|
attachment: attachments[0],
|
||||||
if ProcessInfo.processInfo.isMacCatalystApp {
|
imageMaxHeight: imageMaxHeight,
|
||||||
openWindow(value: WindowDestination.mediaViewer(attachments: attachments,
|
sensitive: sensitive,
|
||||||
selectedAttachment: attachment))
|
appLayoutWidth: appLayoutWidth,
|
||||||
} else {
|
availableWidth: availableWidth
|
||||||
quickLook.prepareFor(selectedMediaAttachment: attachment, mediaAttachments: attachments)
|
)
|
||||||
}
|
.accessibilityElement(children: .ignore)
|
||||||
}
|
.accessibilityLabel(Self.accessibilityLabel(for: attachments[0]))
|
||||||
.accessibilityElement(children: .ignore)
|
.accessibilityAddTraits([.isButton, .isImage])
|
||||||
.accessibilityLabel(Self.accessibilityLabel(for: attachment))
|
.onTapGesture { tabAction(for: 0) }
|
||||||
.accessibilityAddTraits([.isButton, .isImage])
|
|
||||||
} else {
|
} else {
|
||||||
if isCompact || theme.statusDisplayStyle == .compact {
|
if isCompact || theme.statusDisplayStyle == .compact {
|
||||||
HStack {
|
HStack {
|
||||||
|
@ -121,212 +96,36 @@ public struct StatusRowMediaPreviewView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.overlay {
|
|
||||||
if isHidingMedia {
|
|
||||||
sensitiveMediaOverlay
|
|
||||||
.transition(.opacity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.alert("status.editor.media.image-description",
|
|
||||||
isPresented: $isAltAlertDisplayed)
|
|
||||||
{
|
|
||||||
Button("alert.button.ok", action: {})
|
|
||||||
} message: {
|
|
||||||
Text(altTextDisplayed ?? "")
|
|
||||||
}
|
|
||||||
.onAppear {
|
|
||||||
if sensitive, preferences.autoExpandMedia == .hideSensitive {
|
|
||||||
isHidingMedia = true
|
|
||||||
} else if preferences.autoExpandMedia == .hideAll {
|
|
||||||
isHidingMedia = true
|
|
||||||
} else {
|
|
||||||
isHidingMedia = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func makeAttachmentView(for index: Int) -> some View {
|
private func makeAttachmentView(for index: Int) -> some View {
|
||||||
if attachments.count > index {
|
if
|
||||||
makePreview(attachment: attachments[index])
|
attachments.count > index,
|
||||||
|
let data = DisplayData(from: attachments[index])
|
||||||
|
{
|
||||||
|
MediaPreview(
|
||||||
|
sensitive: sensitive,
|
||||||
|
imageMaxHeight: imageMaxHeight,
|
||||||
|
displayData: data
|
||||||
|
)
|
||||||
|
.onTapGesture { tabAction(for: index) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
private func tabAction(for index: Int) {
|
||||||
private func makeFeaturedImagePreview(attachment: MediaAttachment) -> some View {
|
if ProcessInfo.processInfo.isMacCatalystApp {
|
||||||
ZStack(alignment: .bottomLeading) {
|
openWindow(
|
||||||
let size: CGSize = size(for: attachment) ?? .init(width: imageMaxHeight, height: imageMaxHeight)
|
value: WindowDestination.mediaViewer(
|
||||||
let newSize = imageSize(from: size, newWidth: availableWidth - appLayoutWidth)
|
attachments: attachments,
|
||||||
switch attachment.supportedType {
|
selectedAttachment: attachments[index]
|
||||||
case .image:
|
)
|
||||||
LazyImage(url: attachment.url) { state in
|
)
|
||||||
if let image = state.image {
|
} else {
|
||||||
image
|
quickLook.prepareFor(
|
||||||
.resizable()
|
selectedMediaAttachment: attachments[index],
|
||||||
.aspectRatio(contentMode: .fill)
|
mediaAttachments: attachments
|
||||||
.frame(width: newSize.width, height: newSize.height)
|
)
|
||||||
.clipped()
|
|
||||||
.cornerRadius(4)
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: 4)
|
|
||||||
.stroke(.gray.opacity(0.35), lineWidth: 1)
|
|
||||||
)
|
|
||||||
} 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)
|
|
||||||
|
|
||||||
case .gifv, .video, .audio:
|
|
||||||
if let url = attachment.url {
|
|
||||||
MediaUIAttachmentVideoView(viewModel: .init(url: url))
|
|
||||||
.frame(width: newSize.width, height: newSize.height)
|
|
||||||
}
|
|
||||||
case .none:
|
|
||||||
EmptyView()
|
|
||||||
}
|
|
||||||
if !isInCaptureMode, sensitive {
|
|
||||||
cornerSensitiveButton
|
|
||||||
}
|
|
||||||
if !isInCaptureMode, let alt = attachment.description, !alt.isEmpty, !isCompact, 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private func makePreview(attachment: MediaAttachment) -> some View {
|
|
||||||
if let type = attachment.supportedType, !isInCaptureMode {
|
|
||||||
Group {
|
|
||||||
GeometryReader { proxy in
|
|
||||||
switch type {
|
|
||||||
case .image:
|
|
||||||
ZStack(alignment: .bottomTrailing) {
|
|
||||||
LazyResizableImage(url: attachment.previewUrl ?? attachment.url) { state, proxy in
|
|
||||||
let width = isCompact ? imageMaxHeight : proxy.frame(in: .local).width
|
|
||||||
if let image = state.image {
|
|
||||||
image
|
|
||||||
.resizable()
|
|
||||||
.aspectRatio(contentMode: .fill)
|
|
||||||
.frame(maxWidth: width)
|
|
||||||
.frame(maxHeight: imageMaxHeight)
|
|
||||||
.clipped()
|
|
||||||
.cornerRadius(4)
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: 4)
|
|
||||||
.stroke(.gray.opacity(0.35), lineWidth: 1)
|
|
||||||
)
|
|
||||||
} else if state.isLoading {
|
|
||||||
RoundedRectangle(cornerRadius: 4)
|
|
||||||
.fill(Color.gray)
|
|
||||||
.frame(maxHeight: imageMaxHeight)
|
|
||||||
.frame(maxWidth: width)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if sensitive, !isInCaptureMode {
|
|
||||||
cornerSensitiveButton
|
|
||||||
}
|
|
||||||
if !isInCaptureMode,
|
|
||||||
let alt = attachment.description,
|
|
||||||
!alt.isEmpty,
|
|
||||||
!isCompact,
|
|
||||||
preferences.showAltTextForMedia
|
|
||||||
{
|
|
||||||
Button {
|
|
||||||
altTextDisplayed = alt
|
|
||||||
isAltAlertDisplayed = true
|
|
||||||
} label: {
|
|
||||||
Text("status.image.alt-text.abbreviation")
|
|
||||||
.font(.scaledFootnote)
|
|
||||||
}
|
|
||||||
.buttonStyle(.borderless)
|
|
||||||
.padding(4)
|
|
||||||
.background(.thinMaterial)
|
|
||||||
.cornerRadius(4)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case .gifv, .video, .audio:
|
|
||||||
if let url = attachment.url {
|
|
||||||
MediaUIAttachmentVideoView(viewModel: .init(url: url))
|
|
||||||
.frame(width: isCompact ? imageMaxHeight : proxy.frame(in: .local).width)
|
|
||||||
.frame(height: imageMaxHeight)
|
|
||||||
.accessibilityAddTraits(.startsMediaSession)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(maxWidth: isCompact ? imageMaxHeight : nil)
|
|
||||||
.frame(height: imageMaxHeight)
|
|
||||||
}
|
|
||||||
// #965: do not create overlapping tappable areas, when multiple images are shown
|
|
||||||
.contentShape(Rectangle())
|
|
||||||
.onTapGesture {
|
|
||||||
if ProcessInfo.processInfo.isMacCatalystApp {
|
|
||||||
openWindow(value: WindowDestination.mediaViewer(attachments: attachments,
|
|
||||||
selectedAttachment: attachment))
|
|
||||||
} else {
|
|
||||||
quickLook.prepareFor(selectedMediaAttachment: attachment, mediaAttachments: attachments)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.accessibilityElement(children: .ignore)
|
|
||||||
.accessibilityLabel(Self.accessibilityLabel(for: attachment))
|
|
||||||
.accessibilityAddTraits(attachment.supportedType == .image ? [.isImage, .isButton] : .isButton)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var sensitiveMediaOverlay: some View {
|
|
||||||
ZStack {
|
|
||||||
Rectangle()
|
|
||||||
.foregroundColor(.clear)
|
|
||||||
.background(.ultraThinMaterial)
|
|
||||||
if !isCompact {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var cornerSensitiveButton: some View {
|
|
||||||
HStack {
|
|
||||||
Button {
|
|
||||||
withAnimation {
|
|
||||||
isHidingMedia = true
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "eye.slash")
|
|
||||||
.frame(minHeight: 21) // Match the alt button in case it is also present
|
|
||||||
}
|
|
||||||
.padding(10)
|
|
||||||
.buttonStyle(.borderedProminent)
|
|
||||||
Spacer()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -340,3 +139,352 @@ public struct StatusRowMediaPreviewView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct MediaPreview: View {
|
||||||
|
let sensitive: Bool
|
||||||
|
let imageMaxHeight: CGFloat
|
||||||
|
let displayData: DisplayData
|
||||||
|
|
||||||
|
@Environment(UserPreferences.self) private var preferences
|
||||||
|
@Environment(\.isCompact) private var isCompact: Bool
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
GeometryReader { proxy in
|
||||||
|
switch displayData.type {
|
||||||
|
case .image:
|
||||||
|
LazyResizableImage(url: displayData.previewUrl) { state, proxy in
|
||||||
|
let width = isCompact ? imageMaxHeight : proxy.frame(in: .local).width
|
||||||
|
if let image = state.image {
|
||||||
|
image
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fill)
|
||||||
|
.frame(maxWidth: width, maxHeight: imageMaxHeight)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 4)
|
||||||
|
.stroke(.gray.opacity(0.35), lineWidth: 1)
|
||||||
|
)
|
||||||
|
} else if state.isLoading {
|
||||||
|
RoundedRectangle(cornerRadius: 4)
|
||||||
|
.fill(Color.gray)
|
||||||
|
.frame(maxWidth: width, maxHeight: imageMaxHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.overlay {
|
||||||
|
BlurOverLay(sensitive: sensitive, font: .scaledFootnote)
|
||||||
|
}
|
||||||
|
.overlay {
|
||||||
|
AltTextButton(text: displayData.description, font: .scaledFootnote)
|
||||||
|
}
|
||||||
|
case .av:
|
||||||
|
MediaUIAttachmentVideoView(viewModel: .init(url: displayData.url))
|
||||||
|
.frame(width: isCompact ? imageMaxHeight : proxy.frame(in: .local).width)
|
||||||
|
.frame(height: imageMaxHeight)
|
||||||
|
.accessibilityAddTraits(.startsMediaSession)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: isCompact ? imageMaxHeight : nil)
|
||||||
|
.frame(height: imageMaxHeight)
|
||||||
|
.clipped()
|
||||||
|
.cornerRadius(4)
|
||||||
|
// #965: do not create overlapping tappable areas, when multiple images are shown
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.accessibilityElement(children: .ignore)
|
||||||
|
.accessibilityLabel(Text(displayData.accessibilityText))
|
||||||
|
.accessibilityAddTraits(displayData.type == .image ? [.isImage, .isButton] : .isButton)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct FeaturedImagePreView: View {
|
||||||
|
let attachment: MediaAttachment
|
||||||
|
let imageMaxHeight: CGFloat
|
||||||
|
let sensitive: Bool
|
||||||
|
let appLayoutWidth: CGFloat
|
||||||
|
let availableWidth: CGFloat
|
||||||
|
|
||||||
|
@Environment(\.isSecondaryColumn) private var isSecondaryColumn: Bool
|
||||||
|
@Environment(Theme.self) private var theme
|
||||||
|
@Environment(\.isCompact) private var isCompact: Bool
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
let size: CGSize = size(for: attachment) ?? .init(width: imageMaxHeight, height: imageMaxHeight)
|
||||||
|
let newSize = imageSize(from: size, newWidth: availableWidth - appLayoutWidth)
|
||||||
|
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 }
|
||||||
|
|
||||||
|
return .init(width: CGFloat(width), height: CGFloat(height))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func imageSize(from: CGSize, newWidth: CGFloat) -> CGSize {
|
||||||
|
if isCompact || theme.statusDisplayStyle == .compact || isSecondaryColumn {
|
||||||
|
return .init(width: imageMaxHeight, height: imageMaxHeight)
|
||||||
|
}
|
||||||
|
let ratio = newWidth / from.width
|
||||||
|
let newHeight = from.height * ratio
|
||||||
|
return .init(width: newWidth, height: newHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
struct BlurOverLay: View {
|
||||||
|
let sensitive: Bool
|
||||||
|
let font: Font?
|
||||||
|
|
||||||
|
@State private var isFrameExpanded = true
|
||||||
|
@State private var isTextExpanded = true
|
||||||
|
|
||||||
|
@Environment(Theme.self) private var theme
|
||||||
|
@Environment(\.isInCaptureMode) private var isInCaptureMode: Bool
|
||||||
|
@Environment(UserPreferences.self) private var preferences
|
||||||
|
@Environment(\.isCompact) private var isCompact: Bool
|
||||||
|
|
||||||
|
|
||||||
|
@Namespace var buttonSpace
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if hasOverlay {
|
||||||
|
ZStack {
|
||||||
|
Rectangle()
|
||||||
|
.foregroundColor(.clear)
|
||||||
|
.background(.ultraThinMaterial)
|
||||||
|
.frame(
|
||||||
|
width: isFrameExpanded ? nil : 0,
|
||||||
|
height: isFrameExpanded ? nil : 0)
|
||||||
|
if !isCompact {
|
||||||
|
Button {
|
||||||
|
withAnimation(.spring(duration: 0.2)) {
|
||||||
|
isTextExpanded.toggle()
|
||||||
|
} completion: {
|
||||||
|
withAnimation(.spring(duration: 0.3)) {
|
||||||
|
isFrameExpanded.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
if isTextExpanded {
|
||||||
|
ViewThatFits(in: .horizontal) {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "eye")
|
||||||
|
Text(sensitive ? "status.media.sensitive.show" : "status.media.content.show" )
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "eye")
|
||||||
|
Text("Show")
|
||||||
|
}
|
||||||
|
Image(systemName: "eye")
|
||||||
|
}
|
||||||
|
.lineLimit(1)
|
||||||
|
.foregroundColor(theme.labelColor)
|
||||||
|
.matchedGeometryEffect(id: "text", in: buttonSpace)
|
||||||
|
} else {
|
||||||
|
Image(systemName: "eye.slash")
|
||||||
|
.matchedGeometryEffect(id: "text", in: buttonSpace)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.foregroundColor(theme.labelColor)
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
.padding(theme.statusDisplayStyle == .compact ? 0 : 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.font(font)
|
||||||
|
.frame(
|
||||||
|
maxWidth: .infinity,
|
||||||
|
maxHeight: .infinity,
|
||||||
|
alignment: isFrameExpanded ? .center : .bottomLeading
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var hasOverlay: Bool {
|
||||||
|
switch (sensitive, preferences.autoExpandMedia) {
|
||||||
|
case (_, .hideAll), (true, .hideSensitive):
|
||||||
|
switch isInCaptureMode {
|
||||||
|
case true: false
|
||||||
|
case false: true
|
||||||
|
}
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AltTextButton: View {
|
||||||
|
let text: String?
|
||||||
|
let font: Font?
|
||||||
|
|
||||||
|
@Environment(\.isInCaptureMode) private var isInCaptureMode: Bool
|
||||||
|
@Environment(\.isCompact) private var isCompact: Bool
|
||||||
|
@Environment(UserPreferences.self) private var preferences
|
||||||
|
@Environment(\.locale) private var locale
|
||||||
|
@Environment(Theme.self) private var theme
|
||||||
|
|
||||||
|
@State private var isDisplayingAlert = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if !isInCaptureMode,
|
||||||
|
let text = text,
|
||||||
|
!text.isEmpty,
|
||||||
|
!isCompact,
|
||||||
|
preferences.showAltTextForMedia
|
||||||
|
{
|
||||||
|
Button {
|
||||||
|
isDisplayingAlert = true
|
||||||
|
} label: {
|
||||||
|
ZStack {
|
||||||
|
// use to sync button with show/hide content button
|
||||||
|
Image(systemName: "eye.slash").opacity(0)
|
||||||
|
Text("status.image.alt-text.abbreviation")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderless)
|
||||||
|
.padding(EdgeInsets(top: 5, leading: 7, bottom: 5, trailing: 7))
|
||||||
|
.background(.thinMaterial)
|
||||||
|
.cornerRadius(4)
|
||||||
|
.padding(theme.statusDisplayStyle == .compact ? 0 : 10)
|
||||||
|
.alert(
|
||||||
|
"status.editor.media.image-description",
|
||||||
|
isPresented: $isDisplayingAlert
|
||||||
|
) {
|
||||||
|
Button("alert.button.ok", action: {})
|
||||||
|
} message: {
|
||||||
|
Text(text)
|
||||||
|
}
|
||||||
|
.frame(
|
||||||
|
maxWidth: .infinity,
|
||||||
|
maxHeight: .infinity,
|
||||||
|
alignment: .bottomTrailing
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct DisplayData: Identifiable, Hashable {
|
||||||
|
let id: String
|
||||||
|
let url: URL
|
||||||
|
let previewUrl: URL?
|
||||||
|
let description: String?
|
||||||
|
let type: DisplayType
|
||||||
|
let accessibilityText: String
|
||||||
|
|
||||||
|
init?(from attachment: MediaAttachment) {
|
||||||
|
guard let url = attachment.url else { return nil }
|
||||||
|
guard let type = attachment.supportedType else { return nil }
|
||||||
|
|
||||||
|
id = attachment.id
|
||||||
|
self.url = url
|
||||||
|
self.previewUrl = attachment.previewUrl ?? attachment.url
|
||||||
|
description = attachment.description
|
||||||
|
self.type = DisplayType(from: type)
|
||||||
|
accessibilityText = Self.getAccessibilityString(from: attachment)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func getAccessibilityString(from attachment: MediaAttachment) -> String {
|
||||||
|
if let altText = attachment.description {
|
||||||
|
"accessibility.image.alt-text-\(altText)"
|
||||||
|
} else if let typeDescription = attachment.localizedTypeDescription {
|
||||||
|
typeDescription
|
||||||
|
} else {
|
||||||
|
"accessibility.tabs.profile.picker.media"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum DisplayType {
|
||||||
|
case image
|
||||||
|
case av
|
||||||
|
|
||||||
|
init(from attachmentType: MediaAttachment.SupportedType) {
|
||||||
|
switch attachmentType {
|
||||||
|
case .image:
|
||||||
|
self = .image
|
||||||
|
case .video, .gifv, .audio:
|
||||||
|
self = .av
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct StatusRowMediaPreviewView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
WrapperForPreview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WrapperForPreview: View {
|
||||||
|
@State private var isCompact = false
|
||||||
|
@State private var isInCaptureMode = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
ScrollView {
|
||||||
|
VStack {
|
||||||
|
ForEach(1..<5) { number in
|
||||||
|
VStack {
|
||||||
|
Text("Preview for \(number) item(s)")
|
||||||
|
StatusRowMediaPreviewView(
|
||||||
|
attachments: Array(repeating: Self.attachment, count: number),
|
||||||
|
sensitive: true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.border(.red)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.environment(SceneDelegate())
|
||||||
|
.environment(UserPreferences.shared)
|
||||||
|
.environment(QuickLook.shared)
|
||||||
|
.environment(Theme.shared)
|
||||||
|
.environment(\.isCompact, isCompact)
|
||||||
|
.environment(\.isInCaptureMode, isInCaptureMode)
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
Toggle("Compact Mode", isOn: $isCompact.animation())
|
||||||
|
Toggle("Capture Mode", isOn: $isInCaptureMode)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
|
||||||
|
static private let url = URL(string: "https://www.upwork.com/catalog-images/c5dffd9b5094556adb26e0a193a1c494")!
|
||||||
|
static private let attachment = MediaAttachment.imageWith(url: url)
|
||||||
|
static private let local = Locale(identifier: "en")
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue