Add two new menu items: save photo and show alt text

This commit is contained in:
Marcin Czachursk 2023-04-16 09:05:41 +02:00
parent 392948d84c
commit 8c2f8f830a
9 changed files with 123 additions and 21 deletions

View File

@ -3,6 +3,9 @@
"global.title.seePost" = "See post";
"global.title.refresh" = "Refresh";
"global.title.momentsAgo" = "moments ago";
"global.title.success" = "Success";
"global.title.photoSaved" = "Photo has been saved.";
"global.title.ok" = "OK";
// MARK: Global errors.
"global.error.unexpected" = "Unexpected error.";
@ -252,6 +255,9 @@
"status.title.unbookmark" = "Unbookmark";
"status.title.comment" = "Comment";
"status.title.report" = "Report";
"status.title.saveImage" = "Save image";
"status.title.showMediaDescription" = "Show media description";
"status.title.mediaDescription" = "Media description";
"status.error.loadingStatusFailed" = "Loading status failed.";
"status.error.notFound" = "Status not existing anymore.";
"status.error.loadingCommentsFailed" = "Comments cannot be downloaded.";

View File

@ -3,6 +3,9 @@
"global.title.seePost" = "Ikusi bidalketa";
"global.title.refresh" = "Freskatu";
"global.title.momentsAgo" = "oraintxe bertan";
"global.title.success" = "Success";
"global.title.photoSaved" = "Photo has been saved.";
"global.title.ok" = "OK";
// MARK: Global errors.
"global.error.unexpected" = "Espero ez zen errorea.";
@ -252,6 +255,9 @@
"status.title.unbookmark" = "Kendu laster-marka";
"status.title.comment" = "Egin iruzkina";
"status.title.report" = "Salatu";
"status.title.saveImage" = "Save image";
"status.title.showMediaDescription" = "Show media description";
"status.title.mediaDescription" = "Media description";
"status.error.loadingStatusFailed" = "Egoera kargatzeak huts egin du.";
"status.error.notFound" = "Egoera ez da dagoeneko existitzen.";
"status.error.loadingCommentsFailed" = "Ezin dira iruzkinak eskuratu.";

View File

@ -3,6 +3,9 @@
"global.title.seePost" = "Pokaż zdjęcie";
"global.title.refresh" = "Odśwież";
"global.title.momentsAgo" = "chwilę temu";
"global.title.success" = "Sukces";
"global.title.photoSaved" = "Zdjęcie zostało zapisane.";
"global.title.ok" = "OK";
// MARK: Global errors.
"global.error.unexpected" = "Wystąpił nieoczekiwany błąd.";
@ -252,6 +255,9 @@
"status.title.unbookmark" = "Usuń z zakładek";
"status.title.comment" = "Skomentuj";
"status.title.report" = "Zgłoś";
"status.title.saveImage" = "Zapisz zdjęcie";
"status.title.showMediaDescription" = "Pokaż opis zdjęcia";
"status.title.mediaDescription" = "Opis zdjęcia";
"status.error.loadingStatusFailed" = "Błąd podczas wczytywanie statusu.";
"status.error.notFound" = "Status już nie istnieje.";
"status.error.loadingCommentsFailed" =" Błąd podczas wczytywanie komentarzy.";

View File

@ -82,6 +82,7 @@
F86B7214296BFDCE00EE59EC /* UserProfileHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86B7213296BFDCE00EE59EC /* UserProfileHeaderView.swift */; };
F86B7216296BFFDA00EE59EC /* UserProfileStatusesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86B7215296BFFDA00EE59EC /* UserProfileStatusesView.swift */; };
F86B7221296C49A300EE59EC /* EmptyButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86B7220296C49A300EE59EC /* EmptyButtonStyle.swift */; };
F86BC9E929EBBB67009415EC /* ImageSaver.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86BC9E829EBBB66009415EC /* ImageSaver.swift */; };
F8742FC429990AFB00E9642B /* ClientError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8742FC329990AFB00E9642B /* ClientError.swift */; };
F8764187298ABB520057D362 /* ViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8764186298ABB520057D362 /* ViewState.swift */; };
F876418D298AE5020057D362 /* PaginableStatusesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F876418C298AE5020057D362 /* PaginableStatusesView.swift */; };
@ -272,6 +273,7 @@
F86B7213296BFDCE00EE59EC /* UserProfileHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileHeaderView.swift; sourceTree = "<group>"; };
F86B7215296BFFDA00EE59EC /* UserProfileStatusesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileStatusesView.swift; sourceTree = "<group>"; };
F86B7220296C49A300EE59EC /* EmptyButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyButtonStyle.swift; sourceTree = "<group>"; };
F86BC9E829EBBB66009415EC /* ImageSaver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageSaver.swift; sourceTree = "<group>"; };
F8742FC329990AFB00E9642B /* ClientError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientError.swift; sourceTree = "<group>"; };
F8764186298ABB520057D362 /* ViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewState.swift; sourceTree = "<group>"; };
F876418C298AE5020057D362 /* PaginableStatusesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginableStatusesView.swift; sourceTree = "<group>"; };
@ -718,6 +720,7 @@
F85D4974296407F100751DF7 /* HomeTimelineService.swift */,
F88E4D49297EA0490057491A /* RouterPath.swift */,
F878842129A4A4E3003CFAD2 /* AppMetadataService.swift */,
F86BC9E829EBBB66009415EC /* ImageSaver.swift */,
);
path = Services;
sourceTree = "<group>";
@ -1117,6 +1120,7 @@
F89F57B029D1C11200001EE3 /* RelationshipModel.swift in Sources */,
F88AB05829B36B8200345EDE /* AccountsPhotoView.swift in Sources */,
F85D4971296402DC00751DF7 /* AuthorizationService.swift in Sources */,
F86BC9E929EBBB67009415EC /* ImageSaver.swift in Sources */,
F88AB05329B3613900345EDE /* PhotoUrl.swift in Sources */,
F88E4D56297EAD6E0057491A /* AppRouteur.swift in Sources */,
F88FAD27295F400E009B20C9 /* NotificationsView.swift in Sources */,
@ -1412,6 +1416,7 @@
INFOPLIST_FILE = Vernissage/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Vernissage;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.photography";
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Saving photos from Pixelfed";
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
@ -1452,6 +1457,7 @@
INFOPLIST_FILE = Vernissage/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Vernissage;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.photography";
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Saving photos from Pixelfed";
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;

View File

@ -0,0 +1,24 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import UIKit
class ImageSaver: NSObject {
private let completed: () -> Void
init(completed: @escaping () -> Void) {
self.completed = completed
}
func writeToPhotoAlbum(image: UIImage) {
UIImageWriteToSavedPhotosAlbum(image, self, #selector(saveCompleted), nil)
}
@objc func saveCompleted(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) {
self.completed()
}
}

View File

@ -10,24 +10,41 @@ import ClientKit
import ServicesKit
public extension View {
func imageContextMenu(statusModel: StatusModel) -> some View {
modifier(ImageContextMenu(id: statusModel.id, url: statusModel.url))
func imageContextMenu(statusModel: StatusModel, attachmentModel: AttachmentModel, uiImage: UIImage?) -> some View {
modifier(ImageContextMenu(id: statusModel.id, url: statusModel.url, altText: attachmentModel.description, uiImage: uiImage))
}
func imageContextMenu(statusData: StatusData) -> some View {
modifier(ImageContextMenu(id: statusData.id, url: statusData.url))
func imageContextMenu(statusData: StatusData, attachmentData: AttachmentData, uiImage: UIImage?) -> some View {
modifier(ImageContextMenu(id: statusData.id, url: statusData.url, altText: attachmentData.text, uiImage: uiImage))
}
}
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
@State private var alertInfo: AlertInfo?
private let id: String
private let url: URL?
private let altText: String?
private let uiImage: UIImage?
init(id: String, url: URL?) {
init(id: String, url: URL?, altText: String?, uiImage: UIImage?) {
self.id = id
self.url = url
self.altText = altText
self.uiImage = uiImage
}
func body(content: Content) -> some View {
@ -69,8 +86,43 @@ private struct ImageContextMenu: ViewModifier {
Label("status.title.shareStatus", systemImage: "square.and.arrow.up")
}
}
Divider()
if let altText, altText.count > 0 {
Button {
self.alertInfo = AlertInfo(
id: .showAlternativeText,
title: Text("status.title.mediaDescription", comment: "Media description"),
message: Text(altText)
)
} label: {
Label("status.title.showMediaDescription", systemImage: "eye.trianglebadge.exclamationmark")
}
}
if let uiImage {
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")
)
}
imageSaver.writeToPhotoAlbum(image: uiImage)
} label: {
Label("status.title.saveImage", systemImage: "square.and.arrow.down")
}
}
}
}
.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 {

View File

@ -73,7 +73,7 @@ private struct NavigationMenu<MenuItems>: ViewModifier where MenuItems: View {
}
.padding(.horizontal, 8)
.background(.ultraThinMaterial)
.clipShape(RoundedRectangle(cornerRadius: 22))
.clipShape(Capsule())
}
@ViewBuilder
@ -81,12 +81,11 @@ private struct NavigationMenu<MenuItems>: ViewModifier where MenuItems: View {
Menu {
self.menuItems()
} label: {
Image(systemName: "line.3.horizontal")
.resizable()
.foregroundColor(.mainTextColor.opacity(0.8))
.shadow(radius: 5)
.padding(12)
.frame(width: 44, height: 44)
Image(systemName: "ellipsis")
.font(.system(size: 26))
.foregroundColor(.mainTextColor.opacity(0.75))
.padding(.vertical, 10)
.padding(.horizontal, 8)
}
}
@ -96,11 +95,10 @@ private struct NavigationMenu<MenuItems>: ViewModifier where MenuItems: View {
self.routerPath.presentedSheet = .newStatusEditor
} label: {
Image(systemName: "plus")
.resizable()
.foregroundColor(.mainTextColor.opacity(0.8))
.shadow(radius: 5)
.padding(12)
.frame(width: 44, height: 44)
.font(.system(size: 26))
.foregroundColor(.mainTextColor.opacity(0.75))
.padding(.vertical, 10)
.padding(.horizontal, 8)
}
}
}

View File

@ -43,7 +43,7 @@ struct ImageRowItem: View {
ZStack {
ContentWarning(spoilerText: self.status.spoilerText) {
self.imageContainerView(uiImage: uiImage)
.imageContextMenu(statusData: self.status)
.imageContextMenu(statusData: self.status, attachmentData: self.attachmentData, uiImage: uiImage)
} blurred: {
ZStack {
BlurredImage(blurhash: attachmentData.blurhash)
@ -62,7 +62,7 @@ struct ImageRowItem: View {
}
} else {
self.imageContainerView(uiImage: uiImage)
.imageContextMenu(statusData: self.status)
.imageContextMenu(statusData: self.status, attachmentData: self.attachmentData, uiImage: uiImage)
.opacity(self.opacity)
.onAppear {
withAnimation {

View File

@ -43,7 +43,9 @@ struct ImageRowItemAsync: View {
ZStack {
ContentWarning(spoilerText: self.statusViewModel.spoilerText) {
self.imageContainerView(image: image)
.imageContextMenu(statusModel: self.statusViewModel)
.imageContextMenu(statusModel: self.statusViewModel,
attachmentModel: self.attachment,
uiImage: state.imageResponse?.image)
} blurred: {
ZStack {
BlurredImage(blurhash: attachment.blurhash)
@ -67,7 +69,9 @@ struct ImageRowItemAsync: View {
}
} else {
self.imageContainerView(image: image)
.imageContextMenu(statusModel: self.statusViewModel)
.imageContextMenu(statusModel: self.statusViewModel,
attachmentModel: self.attachment,
uiImage: state.imageResponse?.image)
.opacity(self.opacity)
.onAppear {
if let uiImage = state.imageResponse?.image {