diff --git a/Localization/en.lproj/Localizable.strings b/Localization/en.lproj/Localizable.strings index bc52e01..98981fe 100644 --- a/Localization/en.lproj/Localizable.strings +++ b/Localization/en.lproj/Localizable.strings @@ -259,6 +259,7 @@ "status.title.showMediaDescription" = "Show media description"; "status.title.mediaDescription" = "Media description"; "status.title.shareImage" = "Share image"; +"status.title.altText" = "ALT"; "status.error.loadingStatusFailed" = "Loading status failed."; "status.error.notFound" = "Status not existing anymore."; "status.error.loadingCommentsFailed" = "Comments cannot be downloaded."; diff --git a/Localization/eu.lproj/Localizable.strings b/Localization/eu.lproj/Localizable.strings index 1c3dc15..5c79bce 100644 --- a/Localization/eu.lproj/Localizable.strings +++ b/Localization/eu.lproj/Localizable.strings @@ -259,6 +259,7 @@ "status.title.showMediaDescription" = "Show media description"; "status.title.mediaDescription" = "Media description"; "status.title.shareImage" = "Share image"; +"status.title.altText" = "ALT"; "status.error.loadingStatusFailed" = "Egoera kargatzeak huts egin du."; "status.error.notFound" = "Egoera ez da dagoeneko existitzen."; "status.error.loadingCommentsFailed" = "Ezin dira iruzkinak eskuratu."; diff --git a/Localization/pl.lproj/Localizable.strings b/Localization/pl.lproj/Localizable.strings index 148c625..005980b 100644 --- a/Localization/pl.lproj/Localizable.strings +++ b/Localization/pl.lproj/Localizable.strings @@ -259,6 +259,7 @@ "status.title.showMediaDescription" = "Pokaż opis zdjęcia"; "status.title.mediaDescription" = "Opis zdjęcia"; "status.title.shareImage" = "Udostępnij zdjęcie"; +"status.title.altText" = "ALT"; "status.error.loadingStatusFailed" = "Błąd podczas wczytywanie statusu."; "status.error.notFound" = "Status już nie istnieje."; "status.error.loadingCommentsFailed" =" Błąd podczas wczytywanie komentarzy."; diff --git a/Vernissage/AppRouteur.swift b/Vernissage/AppRouteur.swift index 54b432b..b4b4c7a 100644 --- a/Vernissage/AppRouteur.swift +++ b/Vernissage/AppRouteur.swift @@ -82,4 +82,19 @@ extension View { } } } + + func withAlertDestinations(alertDestinations: Binding) -> 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"))) + } + } + } } diff --git a/Vernissage/Services/RouterPath.swift b/Vernissage/Services/RouterPath.swift index 67fc22e..0437410 100644 --- a/Vernissage/Services/RouterPath.swift +++ b/Vernissage/Services/RouterPath.swift @@ -53,6 +53,20 @@ enum OverlayDestinations { 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 class RouterPath: ObservableObject { public var urlHandler: ((URL) -> OpenURLAction.Result)? @@ -60,6 +74,7 @@ class RouterPath: ObservableObject { @Published public var path: [RouteurDestinations] = [] @Published public var presentedSheet: SheetDestinations? @Published public var presentedOverlay: OverlayDestinations? + @Published public var presentedAlert: AlertDestinations? public init() {} diff --git a/Vernissage/VernissageApp.swift b/Vernissage/VernissageApp.swift index 3507201..eee05a4 100644 --- a/Vernissage/VernissageApp.swift +++ b/Vernissage/VernissageApp.swift @@ -47,6 +47,7 @@ struct VernissageApp: App { .withAppRouteur() .withSheetDestinations(sheetDestinations: $routerPath.presentedSheet) .withOverlayDestinations(overlayDestinations: $routerPath.presentedOverlay) + .withAlertDestinations(alertDestinations: $routerPath.presentedAlert) } } .environment(\.managedObjectContext, coreDataHandler.container.viewContext) diff --git a/Vernissage/ViewModifiers/ImageContextMenu.swift b/Vernissage/ViewModifiers/ImageContextMenu.swift index 06eb4a3..f0af4fa 100644 --- a/Vernissage/ViewModifiers/ImageContextMenu.swift +++ b/Vernissage/ViewModifiers/ImageContextMenu.swift @@ -20,22 +20,9 @@ public extension View { } 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 routerPath: RouterPath - @State private var alertInfo: AlertInfo? - private let id: String private let url: URL? private let altText: String? @@ -92,11 +79,7 @@ private struct ImageContextMenu: ViewModifier { if let altText, altText.count > 0 { Button { - self.alertInfo = AlertInfo( - id: .showAlternativeText, - title: Text("status.title.mediaDescription", comment: "Media description"), - message: Text(altText) - ) + self.routerPath.presentedAlert = .alternativeText(text: altText) } label: { Label("status.title.showMediaDescription", systemImage: "eye.trianglebadge.exclamationmark") } @@ -113,11 +96,7 @@ private struct ImageContextMenu: ViewModifier { Button { let imageSaver = ImageSaver { - self.alertInfo = AlertInfo( - id: .photoHasBeenSaved, - title: Text("global.title.success", comment: "Success"), - message: Text("global.title.photoSaved", comment: "Photo has been saved") - ) + self.routerPath.presentedAlert = .savePhotoSuccess } 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 { diff --git a/Vernissage/Views/SettingsView/Subviews/GeneralSectionView.swift b/Vernissage/Views/SettingsView/Subviews/GeneralSectionView.swift index 01ab3b5..9f8112c 100644 --- a/Vernissage/Views/SettingsView/Subviews/GeneralSectionView.swift +++ b/Vernissage/Views/SettingsView/Subviews/GeneralSectionView.swift @@ -83,30 +83,6 @@ struct GeneralSectionView: View { .onChange(of: self.applicationState.menuPosition) { menuPosition in 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) - } } } } diff --git a/Vernissage/Views/SettingsView/Subviews/MediaSettingsView.swift b/Vernissage/Views/SettingsView/Subviews/MediaSettingsView.swift index d45d894..247c775 100644 --- a/Vernissage/Views/SettingsView/Subviews/MediaSettingsView.swift +++ b/Vernissage/Views/SettingsView/Subviews/MediaSettingsView.swift @@ -11,13 +11,10 @@ struct MediaSettingsView: View { @EnvironmentObject var applicationState: ApplicationState @Environment(\.colorScheme) var colorScheme - @State var showSensitive = true - @State var showPhotoDescription = true - var body: some View { Section("settings.title.mediaSettings") { - Toggle(isOn: $showSensitive) { + Toggle(isOn: $applicationState.showSensitive) { VStack(alignment: .leading) { Text("settings.title.alwaysShowSensitiveTitle", comment: "Always show NSFW") Text("settings.title.alwaysShowSensitiveDescription", comment: "Force show all NFSW (sensitive) media without warnings") @@ -25,12 +22,11 @@ struct MediaSettingsView: View { .foregroundColor(.lightGrayColor) } } - .onChange(of: showSensitive) { newValue in - self.applicationState.showSensitive = newValue + .onChange(of: self.applicationState.showSensitive) { newValue in ApplicationSettingsHandler.shared.set(showSensitive: newValue) } - Toggle(isOn: $showPhotoDescription) { + Toggle(isOn: $applicationState.showPhotoDescription) { VStack(alignment: .leading) { Text("settings.title.alwaysShowAltTitle", comment: "Show alternative text") Text("settings.title.alwaysShowAltDescription", comment: "Show alternative text if present on status details screen") @@ -38,15 +34,33 @@ struct MediaSettingsView: View { .foregroundColor(.lightGrayColor) } } - .onChange(of: showPhotoDescription) { newValue in - self.applicationState.showPhotoDescription = newValue + .onChange(of: self.applicationState.showPhotoDescription) { newValue in ApplicationSettingsHandler.shared.set(showPhotoDescription: newValue) } - } - .onAppear { - let defaultSettings = ApplicationSettingsHandler.shared.get() - self.showSensitive = defaultSettings.showSensitive - self.showPhotoDescription = defaultSettings.showPhotoDescription + + 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) + } } } } diff --git a/Vernissage/Widgets/ImageRow.swift b/Vernissage/Widgets/ImageRow.swift index 09e832e..e422fd1 100644 --- a/Vernissage/Widgets/ImageRow.swift +++ b/Vernissage/Widgets/ImageRow.swift @@ -6,6 +6,7 @@ import SwiftUI import ServicesKit +import WidgetsKit struct ImageRow: View { private let status: StatusData @@ -84,7 +85,8 @@ struct ImageRow: View { } }) .frame(width: self.imageWidth, height: self.imageHeight) - .tabViewStyle(PageTabViewStyle()) + .tabViewStyle(.page(indexDisplayMode: .never)) + .overlay(CustomPageTabViewStyleView(pages: self.attachmentsData, currentId: $selected)) } } } diff --git a/Vernissage/Widgets/ImageRowAsync.swift b/Vernissage/Widgets/ImageRowAsync.swift index 9563f08..9e5cf52 100644 --- a/Vernissage/Widgets/ImageRowAsync.swift +++ b/Vernissage/Widgets/ImageRowAsync.swift @@ -8,6 +8,7 @@ import SwiftUI import PixelfedKit import ClientKit import ServicesKit +import WidgetsKit struct ImageRowAsync: View { private let statusViewModel: StatusModel @@ -86,7 +87,8 @@ struct ImageRowAsync: View { } }) .frame(width: self.imageWidth, height: self.imageHeight) - .tabViewStyle(PageTabViewStyle()) + .tabViewStyle(.page(indexDisplayMode: .never)) + .overlay(CustomPageTabViewStyleView(pages: self.statusViewModel.mediaAttachments, currentId: $selected)) } } } diff --git a/Vernissage/Widgets/ImageRowItem.swift b/Vernissage/Widgets/ImageRowItem.swift index dcaa669..21da885 100644 --- a/Vernissage/Widgets/ImageRowItem.swift +++ b/Vernissage/Widgets/ImageRowItem.swift @@ -104,6 +104,10 @@ struct ImageRowItem: View { ImageAvatar(displayName: self.status.accountDisplayName, avatarUrl: self.status.accountAvatar) ImageFavourite(isFavourited: $isFavourited) + ImageAlternativeText(text: self.attachmentData.text) { text in + self.routerPath.presentedAlert = .alternativeText(text: text) + } + FavouriteTouch(showFavouriteAnimation: $showThumbImage) } } diff --git a/Vernissage/Widgets/ImageRowItemAsync.swift b/Vernissage/Widgets/ImageRowItemAsync.swift index e63ac7e..69e7792 100644 --- a/Vernissage/Widgets/ImageRowItemAsync.swift +++ b/Vernissage/Widgets/ImageRowItemAsync.swift @@ -118,6 +118,10 @@ struct ImageRowItemAsync: View { avatarUrl: self.statusViewModel.account.avatar) } + ImageAlternativeText(text: self.attachment.description) { text in + self.routerPath.presentedAlert = .alternativeText(text: text) + } + ImageFavourite(isFavourited: $isFavourited) FavouriteTouch(showFavouriteAnimation: $showThumbImage) } diff --git a/WidgetsKit/Sources/WidgetsKit/Views/CustomPageTabViewStyleView.swift b/WidgetsKit/Sources/WidgetsKit/Views/CustomPageTabViewStyleView.swift new file mode 100644 index 0000000..ab1d264 --- /dev/null +++ b/WidgetsKit/Sources/WidgetsKit/Views/CustomPageTabViewStyleView.swift @@ -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: View where T: Identifiable { + @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) { + 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() + } +} diff --git a/WidgetsKit/Sources/WidgetsKit/Widgets/ImageAlternativeText.swift b/WidgetsKit/Sources/WidgetsKit/Widgets/ImageAlternativeText.swift new file mode 100644 index 0000000..736d2ca --- /dev/null +++ b/WidgetsKit/Sources/WidgetsKit/Widgets/ImageAlternativeText.swift @@ -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) + } + } +}