Show ALT image on timelines

This commit is contained in:
Marcin Czachursk 2023-04-16 17:44:35 +02:00
parent 4262dc82db
commit 6111f0d615
15 changed files with 163 additions and 68 deletions

View File

@ -259,6 +259,7 @@
"status.title.showMediaDescription" = "Show media description"; "status.title.showMediaDescription" = "Show media description";
"status.title.mediaDescription" = "Media description"; "status.title.mediaDescription" = "Media description";
"status.title.shareImage" = "Share image"; "status.title.shareImage" = "Share image";
"status.title.altText" = "ALT";
"status.error.loadingStatusFailed" = "Loading status failed."; "status.error.loadingStatusFailed" = "Loading status failed.";
"status.error.notFound" = "Status not existing anymore."; "status.error.notFound" = "Status not existing anymore.";
"status.error.loadingCommentsFailed" = "Comments cannot be downloaded."; "status.error.loadingCommentsFailed" = "Comments cannot be downloaded.";

View File

@ -259,6 +259,7 @@
"status.title.showMediaDescription" = "Show media description"; "status.title.showMediaDescription" = "Show media description";
"status.title.mediaDescription" = "Media description"; "status.title.mediaDescription" = "Media description";
"status.title.shareImage" = "Share image"; "status.title.shareImage" = "Share image";
"status.title.altText" = "ALT";
"status.error.loadingStatusFailed" = "Egoera kargatzeak huts egin du."; "status.error.loadingStatusFailed" = "Egoera kargatzeak huts egin du.";
"status.error.notFound" = "Egoera ez da dagoeneko existitzen."; "status.error.notFound" = "Egoera ez da dagoeneko existitzen.";
"status.error.loadingCommentsFailed" = "Ezin dira iruzkinak eskuratu."; "status.error.loadingCommentsFailed" = "Ezin dira iruzkinak eskuratu.";

View File

@ -259,6 +259,7 @@
"status.title.showMediaDescription" = "Pokaż opis zdjęcia"; "status.title.showMediaDescription" = "Pokaż opis zdjęcia";
"status.title.mediaDescription" = "Opis zdjęcia"; "status.title.mediaDescription" = "Opis zdjęcia";
"status.title.shareImage" = "Udostępnij zdjęcie"; "status.title.shareImage" = "Udostępnij zdjęcie";
"status.title.altText" = "ALT";
"status.error.loadingStatusFailed" = "Błąd podczas wczytywanie statusu."; "status.error.loadingStatusFailed" = "Błąd podczas wczytywanie statusu.";
"status.error.notFound" = "Status już nie istnieje."; "status.error.notFound" = "Status już nie istnieje.";
"status.error.loadingCommentsFailed" =" Błąd podczas wczytywanie komentarzy."; "status.error.loadingCommentsFailed" =" Błąd podczas wczytywanie komentarzy.";

View File

@ -82,4 +82,19 @@ extension View {
} }
} }
} }
func withAlertDestinations(alertDestinations: Binding<AlertDestinations?>) -> some View {
self.alert(item: alertDestinations) { destination in
switch destination {
case .alternativeText(let text):
return Alert(title: Text("status.title.mediaDescription", comment: "Media description"),
message: Text(text),
dismissButton: .default(Text("global.title.ok", comment: "OK")))
case .savePhotoSuccess:
return Alert(title: Text("global.title.success", comment: "Success"),
message: Text("global.title.photoSaved", comment: "Photo has been saved"),
dismissButton: .default(Text("global.title.ok", comment: "OK")))
}
}
}
} }

View File

@ -53,6 +53,20 @@ enum OverlayDestinations {
case successPayment case successPayment
} }
enum AlertDestinations: Identifiable {
case alternativeText(text: String)
case savePhotoSuccess
public var id: String {
switch self {
case .alternativeText:
return "alternativeText"
case .savePhotoSuccess:
return "savePhotoSuccess"
}
}
}
@MainActor @MainActor
class RouterPath: ObservableObject { class RouterPath: ObservableObject {
public var urlHandler: ((URL) -> OpenURLAction.Result)? public var urlHandler: ((URL) -> OpenURLAction.Result)?
@ -60,6 +74,7 @@ class RouterPath: ObservableObject {
@Published public var path: [RouteurDestinations] = [] @Published public var path: [RouteurDestinations] = []
@Published public var presentedSheet: SheetDestinations? @Published public var presentedSheet: SheetDestinations?
@Published public var presentedOverlay: OverlayDestinations? @Published public var presentedOverlay: OverlayDestinations?
@Published public var presentedAlert: AlertDestinations?
public init() {} public init() {}

View File

@ -47,6 +47,7 @@ struct VernissageApp: App {
.withAppRouteur() .withAppRouteur()
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet) .withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
.withOverlayDestinations(overlayDestinations: $routerPath.presentedOverlay) .withOverlayDestinations(overlayDestinations: $routerPath.presentedOverlay)
.withAlertDestinations(alertDestinations: $routerPath.presentedAlert)
} }
} }
.environment(\.managedObjectContext, coreDataHandler.container.viewContext) .environment(\.managedObjectContext, coreDataHandler.container.viewContext)

View File

@ -20,22 +20,9 @@ public extension View {
} }
private struct ImageContextMenu: ViewModifier { private struct ImageContextMenu: ViewModifier {
private struct AlertInfo: Identifiable {
enum AlertType {
case showAlternativeText
case photoHasBeenSaved
}
let id: AlertType
let title: Text
let message: Text
}
@EnvironmentObject var client: Client @EnvironmentObject var client: Client
@EnvironmentObject var routerPath: RouterPath @EnvironmentObject var routerPath: RouterPath
@State private var alertInfo: AlertInfo?
private let id: String private let id: String
private let url: URL? private let url: URL?
private let altText: String? private let altText: String?
@ -92,11 +79,7 @@ private struct ImageContextMenu: ViewModifier {
if let altText, altText.count > 0 { if let altText, altText.count > 0 {
Button { Button {
self.alertInfo = AlertInfo( self.routerPath.presentedAlert = .alternativeText(text: altText)
id: .showAlternativeText,
title: Text("status.title.mediaDescription", comment: "Media description"),
message: Text(altText)
)
} label: { } label: {
Label("status.title.showMediaDescription", systemImage: "eye.trianglebadge.exclamationmark") Label("status.title.showMediaDescription", systemImage: "eye.trianglebadge.exclamationmark")
} }
@ -113,11 +96,7 @@ private struct ImageContextMenu: ViewModifier {
Button { Button {
let imageSaver = ImageSaver { let imageSaver = ImageSaver {
self.alertInfo = AlertInfo( self.routerPath.presentedAlert = .savePhotoSuccess
id: .photoHasBeenSaved,
title: Text("global.title.success", comment: "Success"),
message: Text("global.title.photoSaved", comment: "Photo has been saved")
)
} }
imageSaver.writeToPhotoAlbum(image: uiImage) imageSaver.writeToPhotoAlbum(image: uiImage)
@ -127,11 +106,6 @@ private struct ImageContextMenu: ViewModifier {
} }
} }
} }
.alert(item: $alertInfo, content: { info in
Alert(title: info.title,
message: info.message,
dismissButton: .default(Text("global.title.ok", comment: "OK")))
})
} }
private func reboost() async { private func reboost() async {

View File

@ -83,30 +83,6 @@ struct GeneralSectionView: View {
.onChange(of: self.applicationState.menuPosition) { menuPosition in .onChange(of: self.applicationState.menuPosition) { menuPosition in
ApplicationSettingsHandler.shared.set(menuPosition: menuPosition) ApplicationSettingsHandler.shared.set(menuPosition: menuPosition)
} }
Toggle(isOn: $applicationState.showAvatarsOnTimeline) {
VStack(alignment: .leading) {
Text("settings.title.showAvatars", comment: "Show avatars")
Text("settings.title.showAvatarsOnTimeline", comment: "Show avatars on timeline")
.font(.footnote)
.foregroundColor(.lightGrayColor)
}
}
.onChange(of: self.applicationState.showAvatarsOnTimeline) { newValue in
ApplicationSettingsHandler.shared.set(showAvatarsOnTimeline: newValue)
}
Toggle(isOn: $applicationState.showFavouritesOnTimeline) {
VStack(alignment: .leading) {
Text("settings.title.showFavourite", comment: "Show favourites")
Text("settings.title.showFavouriteOnTimeline", comment: "Show favourites on timeline")
.font(.footnote)
.foregroundColor(.lightGrayColor)
}
}
.onChange(of: self.applicationState.showFavouritesOnTimeline) { newValue in
ApplicationSettingsHandler.shared.set(showFavouritesOnTimeline: newValue)
}
} }
} }
} }

View File

@ -11,13 +11,10 @@ struct MediaSettingsView: View {
@EnvironmentObject var applicationState: ApplicationState @EnvironmentObject var applicationState: ApplicationState
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@State var showSensitive = true
@State var showPhotoDescription = true
var body: some View { var body: some View {
Section("settings.title.mediaSettings") { Section("settings.title.mediaSettings") {
Toggle(isOn: $showSensitive) { Toggle(isOn: $applicationState.showSensitive) {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("settings.title.alwaysShowSensitiveTitle", comment: "Always show NSFW") Text("settings.title.alwaysShowSensitiveTitle", comment: "Always show NSFW")
Text("settings.title.alwaysShowSensitiveDescription", comment: "Force show all NFSW (sensitive) media without warnings") Text("settings.title.alwaysShowSensitiveDescription", comment: "Force show all NFSW (sensitive) media without warnings")
@ -25,12 +22,11 @@ struct MediaSettingsView: View {
.foregroundColor(.lightGrayColor) .foregroundColor(.lightGrayColor)
} }
} }
.onChange(of: showSensitive) { newValue in .onChange(of: self.applicationState.showSensitive) { newValue in
self.applicationState.showSensitive = newValue
ApplicationSettingsHandler.shared.set(showSensitive: newValue) ApplicationSettingsHandler.shared.set(showSensitive: newValue)
} }
Toggle(isOn: $showPhotoDescription) { Toggle(isOn: $applicationState.showPhotoDescription) {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("settings.title.alwaysShowAltTitle", comment: "Show alternative text") Text("settings.title.alwaysShowAltTitle", comment: "Show alternative text")
Text("settings.title.alwaysShowAltDescription", comment: "Show alternative text if present on status details screen") Text("settings.title.alwaysShowAltDescription", comment: "Show alternative text if present on status details screen")
@ -38,15 +34,33 @@ struct MediaSettingsView: View {
.foregroundColor(.lightGrayColor) .foregroundColor(.lightGrayColor)
} }
} }
.onChange(of: showPhotoDescription) { newValue in .onChange(of: self.applicationState.showPhotoDescription) { newValue in
self.applicationState.showPhotoDescription = newValue
ApplicationSettingsHandler.shared.set(showPhotoDescription: newValue) ApplicationSettingsHandler.shared.set(showPhotoDescription: newValue)
} }
}
.onAppear { Toggle(isOn: $applicationState.showAvatarsOnTimeline) {
let defaultSettings = ApplicationSettingsHandler.shared.get() VStack(alignment: .leading) {
self.showSensitive = defaultSettings.showSensitive Text("settings.title.showAvatars", comment: "Show avatars")
self.showPhotoDescription = defaultSettings.showPhotoDescription Text("settings.title.showAvatarsOnTimeline", comment: "Show avatars on timeline")
.font(.footnote)
.foregroundColor(.lightGrayColor)
}
}
.onChange(of: self.applicationState.showAvatarsOnTimeline) { newValue in
ApplicationSettingsHandler.shared.set(showAvatarsOnTimeline: newValue)
}
Toggle(isOn: $applicationState.showFavouritesOnTimeline) {
VStack(alignment: .leading) {
Text("settings.title.showFavourite", comment: "Show favourites")
Text("settings.title.showFavouriteOnTimeline", comment: "Show favourites on timeline")
.font(.footnote)
.foregroundColor(.lightGrayColor)
}
}
.onChange(of: self.applicationState.showFavouritesOnTimeline) { newValue in
ApplicationSettingsHandler.shared.set(showFavouritesOnTimeline: newValue)
}
} }
} }
} }

View File

@ -6,6 +6,7 @@
import SwiftUI import SwiftUI
import ServicesKit import ServicesKit
import WidgetsKit
struct ImageRow: View { struct ImageRow: View {
private let status: StatusData private let status: StatusData
@ -84,7 +85,8 @@ struct ImageRow: View {
} }
}) })
.frame(width: self.imageWidth, height: self.imageHeight) .frame(width: self.imageWidth, height: self.imageHeight)
.tabViewStyle(PageTabViewStyle()) .tabViewStyle(.page(indexDisplayMode: .never))
.overlay(CustomPageTabViewStyleView(pages: self.attachmentsData, currentId: $selected))
} }
} }
} }

View File

@ -8,6 +8,7 @@ import SwiftUI
import PixelfedKit import PixelfedKit
import ClientKit import ClientKit
import ServicesKit import ServicesKit
import WidgetsKit
struct ImageRowAsync: View { struct ImageRowAsync: View {
private let statusViewModel: StatusModel private let statusViewModel: StatusModel
@ -86,7 +87,8 @@ struct ImageRowAsync: View {
} }
}) })
.frame(width: self.imageWidth, height: self.imageHeight) .frame(width: self.imageWidth, height: self.imageHeight)
.tabViewStyle(PageTabViewStyle()) .tabViewStyle(.page(indexDisplayMode: .never))
.overlay(CustomPageTabViewStyleView(pages: self.statusViewModel.mediaAttachments, currentId: $selected))
} }
} }
} }

View File

@ -104,6 +104,10 @@ struct ImageRowItem: View {
ImageAvatar(displayName: self.status.accountDisplayName, avatarUrl: self.status.accountAvatar) ImageAvatar(displayName: self.status.accountDisplayName, avatarUrl: self.status.accountAvatar)
ImageFavourite(isFavourited: $isFavourited) ImageFavourite(isFavourited: $isFavourited)
ImageAlternativeText(text: self.attachmentData.text) { text in
self.routerPath.presentedAlert = .alternativeText(text: text)
}
FavouriteTouch(showFavouriteAnimation: $showThumbImage) FavouriteTouch(showFavouriteAnimation: $showThumbImage)
} }
} }

View File

@ -118,6 +118,10 @@ struct ImageRowItemAsync: View {
avatarUrl: self.statusViewModel.account.avatar) avatarUrl: self.statusViewModel.account.avatar)
} }
ImageAlternativeText(text: self.attachment.description) { text in
self.routerPath.presentedAlert = .alternativeText(text: text)
}
ImageFavourite(isFavourited: $isFavourited) ImageFavourite(isFavourited: $isFavourited)
FavouriteTouch(showFavouriteAnimation: $showThumbImage) FavouriteTouch(showFavouriteAnimation: $showThumbImage)
} }

View File

@ -0,0 +1,39 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import SwiftUI
public struct CustomPageTabViewStyleView<T>: View where T: Identifiable<String> {
@Binding var currentId: String
private let pages: [T]
private let circleSize: CGFloat = 8
private let circleSpacing: CGFloat = 9
private let primaryColor = Color.white.opacity(0.7)
private let secondaryColor = Color.white.opacity(0.4)
public init(pages: [T], currentId: Binding<String>) {
self.pages = pages
self._currentId = currentId
}
public var body: some View {
VStack {
Spacer()
HStack(spacing: circleSpacing) {
ForEach(self.pages, id: \.id) { page in
Circle()
.fill(currentId == page.id ? primaryColor : secondaryColor)
.frame(width: circleSize, height: circleSize)
.id(page.id)
}
}
}
.padding()
}
}

View File

@ -0,0 +1,46 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import SwiftUI
import EnvironmentKit
public struct ImageAlternativeText: View {
@EnvironmentObject var applicationState: ApplicationState
private let text: String?
private let open: (String) -> Void
public init(text: String?, open: @escaping (String) -> Void) {
self.text = text
self.open = open
}
public var body: some View {
if let text = self.text, text.count > 0 && self.applicationState.showPhotoDescription {
VStack(alignment: .leading) {
Spacer()
HStack(alignment: .center) {
Spacer()
Button {
self.open(text)
} label: {
Text("status.title.altText", comment: "ALT")
.font(.system(size: 12))
.shadow(color: .black, radius: 4)
.foregroundColor(.white.opacity(0.8))
.padding(.vertical, 4)
.padding(.horizontal, 8)
.background(RoundedRectangle(cornerRadius: 8).foregroundColor(.black.opacity(0.8)))
}
}
}
.padding(.trailing, 12)
.padding(.bottom, 12)
}
}
}