diff --git a/CoreData/ApplicationSettings+CoreDataProperties.swift b/CoreData/ApplicationSettings+CoreDataProperties.swift index 4263b4a..8cb4d3e 100644 --- a/CoreData/ApplicationSettings+CoreDataProperties.swift +++ b/CoreData/ApplicationSettings+CoreDataProperties.swift @@ -29,6 +29,7 @@ extension ApplicationSettings { @NSManaged public var showSensitive: Bool @NSManaged public var showPhotoDescription: Bool @NSManaged public var menuPosition: Int32 + @NSManaged public var showAvatarsOnTimeline: Bool } extension ApplicationSettings: Identifiable { diff --git a/CoreData/ApplicationSettingsHandler.swift b/CoreData/ApplicationSettingsHandler.swift index 8ebe66e..0143946 100644 --- a/CoreData/ApplicationSettingsHandler.swift +++ b/CoreData/ApplicationSettingsHandler.swift @@ -144,6 +144,12 @@ class ApplicationSettingsHandler { CoreDataHandler.shared.save() } + func set(showAvatarsOnTimeline: Bool) { + let defaultSettings = self.get() + defaultSettings.showAvatarsOnTimeline = showAvatarsOnTimeline + CoreDataHandler.shared.save() + } + private func createApplicationSettingsEntity(viewContext: NSManagedObjectContext? = nil) -> ApplicationSettings { let context = viewContext ?? CoreDataHandler.shared.container.viewContext return ApplicationSettings(context: context) diff --git a/CoreData/Vernissage.xcdatamodeld/.xccurrentversion b/CoreData/Vernissage.xcdatamodeld/.xccurrentversion index caec0e4..7d4618c 100644 --- a/CoreData/Vernissage.xcdatamodeld/.xccurrentversion +++ b/CoreData/Vernissage.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - Vernissage-007.xcdatamodel + Vernissage-008.xcdatamodel diff --git a/CoreData/Vernissage.xcdatamodeld/Vernissage-008.xcdatamodel/contents b/CoreData/Vernissage.xcdatamodeld/Vernissage-008.xcdatamodel/contents new file mode 100644 index 0000000..c0023cb --- /dev/null +++ b/CoreData/Vernissage.xcdatamodeld/Vernissage-008.xcdatamodel/contents @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/EnvironmentKit/Sources/EnvironmentKit/ApplicationState.swift b/EnvironmentKit/Sources/EnvironmentKit/ApplicationState.swift index bc7ade4..e81fd10 100644 --- a/EnvironmentKit/Sources/EnvironmentKit/ApplicationState.swift +++ b/EnvironmentKit/Sources/EnvironmentKit/ApplicationState.swift @@ -89,6 +89,9 @@ public class ApplicationState: ObservableObject { /// Information which menu should be shown (top or bottom). @Published public var menuPosition = MenuPosition.top + /// Should avatars be visible on timelines. + @Published public var showAvatarsOnTimeline = false + public func changeApplicationState(accountModel: AccountModel, instance: Instance?, lastSeenStatusId: String?) { self.account = accountModel self.lastSeenStatusId = lastSeenStatusId diff --git a/Localization/en.lproj/Localizable.strings b/Localization/en.lproj/Localizable.strings index 9181c01..a32c92b 100644 --- a/Localization/en.lproj/Localizable.strings +++ b/Localization/en.lproj/Localizable.strings @@ -211,6 +211,7 @@ "settings.title.topMenu" = "Navigation bar"; "settings.title.bottomRightMenu" = "Bottom right"; "settings.title.bottomLeftMenu" = "Bottom left"; +"settings.title.showAvatarsOnTimeline" = "Show avatars on timelines"; // Mark: Signin view. "signin.navigationBar.title" = "Sign in to Pixelfed"; diff --git a/Localization/eu.lproj/Localizable.strings b/Localization/eu.lproj/Localizable.strings index ea9eb8c..e64bd42 100644 --- a/Localization/eu.lproj/Localizable.strings +++ b/Localization/eu.lproj/Localizable.strings @@ -211,6 +211,7 @@ "settings.title.topMenu" = "Nabigazio barra"; "settings.title.bottomRightMenu" = "Behe eskumaldean"; "settings.title.bottomLeftMenu" = "Behe ezkerraldean"; +"settings.title.showAvatarsOnTimeline" = "Show avatars on timelines"; // Mark: Signin view. "signin.navigationBar.title" = "Hasi saioa Pixelfed-en"; diff --git a/Localization/pl.lproj/Localizable.strings b/Localization/pl.lproj/Localizable.strings index 5814d1a..112865e 100644 --- a/Localization/pl.lproj/Localizable.strings +++ b/Localization/pl.lproj/Localizable.strings @@ -211,6 +211,7 @@ "settings.title.topMenu" = "Panel tytułowy"; "settings.title.bottomRightMenu" = "Dolny prawy"; "settings.title.bottomLeftMenu" = "Dolny lewy"; +"settings.title.showAvatarsOnTimeline" = "Wyświetlaj awatary na osiach zdjęć"; // Mark: Signin view. "signin.navigationBar.title" = "Zaloguj się do Pixelfed"; diff --git a/Vernissage.xcodeproj/project.pbxproj b/Vernissage.xcodeproj/project.pbxproj index fb7e15a..5c0a875 100644 --- a/Vernissage.xcodeproj/project.pbxproj +++ b/Vernissage.xcodeproj/project.pbxproj @@ -157,6 +157,7 @@ F89D6C4629718193001DA3D4 /* GeneralSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89D6C4529718193001DA3D4 /* GeneralSectionView.swift */; }; F89D6C4A297196FF001DA3D4 /* ImageViewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89D6C49297196FF001DA3D4 /* ImageViewer.swift */; }; F89F57B029D1C11200001EE3 /* RelationshipModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89F57AF29D1C11200001EE3 /* RelationshipModel.swift */; }; + F8A4A88329E3FD1C00267E36 /* ImageAvatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A4A88229E3FD1C00267E36 /* ImageAvatar.swift */; }; F8A93D7E2965FD89001D8331 /* UserProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A93D7D2965FD89001D8331 /* UserProfileView.swift */; }; F8AFF7C129B259150087D083 /* HashtagsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8AFF7C029B259150087D083 /* HashtagsView.swift */; }; F8AFF7C429B25EF40087D083 /* ImagesGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8AFF7C329B25EF40087D083 /* ImagesGrid.swift */; }; @@ -326,6 +327,8 @@ F89D6C49297196FF001DA3D4 /* ImageViewer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewer.swift; sourceTree = ""; }; F89F0605299139F6003DC875 /* Vernissage-002.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-002.xcdatamodel"; sourceTree = ""; }; F89F57AF29D1C11200001EE3 /* RelationshipModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelationshipModel.swift; sourceTree = ""; }; + F8A4A88229E3FD1C00267E36 /* ImageAvatar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageAvatar.swift; sourceTree = ""; }; + F8A4A88429E4099900267E36 /* Vernissage-008.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-008.xcdatamodel"; sourceTree = ""; }; F8A93D7D2965FD89001D8331 /* UserProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileView.swift; sourceTree = ""; }; F8AFF7C029B259150087D083 /* HashtagsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagsView.swift; sourceTree = ""; }; F8AFF7C329B25EF40087D083 /* ImagesGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagesGrid.swift; sourceTree = ""; }; @@ -630,6 +633,7 @@ isa = PBXGroup; children = ( F88BC53A29E06A5100CE6141 /* ImageContextMenu.swift */, + F8A4A88229E3FD1C00267E36 /* ImageAvatar.swift */, ); path = ViewModifiers; sourceTree = ""; @@ -1069,6 +1073,7 @@ F80048042961850500E6868A /* AttachmentData+CoreDataProperties.swift in Sources */, F88E4D4A297EA0490057491A /* RouterPath.swift in Sources */, F88E4D48297E90CD0057491A /* TrendStatusesView.swift in Sources */, + F8A4A88329E3FD1C00267E36 /* ImageAvatar.swift in Sources */, F800480A2961EA1900E6868A /* AttachmentDataHandler.swift in Sources */, F80048032961850500E6868A /* AttachmentData+CoreDataClass.swift in Sources */, F891E7D029C368750022C449 /* ImageRowItemAsync.swift in Sources */, @@ -1603,6 +1608,7 @@ F88C2476295C37BB0006098B /* Vernissage.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + F8A4A88429E4099900267E36 /* Vernissage-008.xcdatamodel */, F8911A1829DE9E5500770F44 /* Vernissage-007.xcdatamodel */, F8EF371429C624DA00669F45 /* Vernissage-006.xcdatamodel */, F8CAE64129B8F1AF001E0372 /* Vernissage-005.xcdatamodel */, @@ -1612,7 +1618,7 @@ F8C937A929882CA90004D782 /* Vernissage-001.xcdatamodel */, F88C2477295C37BB0006098B /* Vernissage.xcdatamodel */, ); - currentVersion = F8911A1829DE9E5500770F44 /* Vernissage-007.xcdatamodel */; + currentVersion = F8A4A88429E4099900267E36 /* Vernissage-008.xcdatamodel */; path = Vernissage.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; diff --git a/Vernissage/ViewModifiers/ImageAvatar.swift b/Vernissage/ViewModifiers/ImageAvatar.swift new file mode 100644 index 0000000..564c3ab --- /dev/null +++ b/Vernissage/ViewModifiers/ImageAvatar.swift @@ -0,0 +1,82 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the Apache License 2.0. +// + +import Foundation +import SwiftUI +import NukeUI +import ClientKit +import ServicesKit +import EnvironmentKit + +public extension View { + func imageAvatar(applicationState: ApplicationState, displayName: String?, avatarUrl: URL?) -> some View { + modifier(ImageAvatar(applicationState: applicationState, displayName: displayName, avatarUrl: avatarUrl)) + } +} + +private struct ImageAvatar: ViewModifier { + private let applicationState: ApplicationState + private let displayName: String? + private let avatarUrl: URL? + + init(applicationState: ApplicationState, displayName: String?, avatarUrl: URL?) { + self.applicationState = applicationState + self.displayName = displayName + self.avatarUrl = avatarUrl + } + + func body(content: Content) -> some View { + if self.applicationState.showAvatarsOnTimeline { + ZStack { + // Image. + content + + // Avatar. + VStack(alignment: .leading) { + + HStack(alignment: .center) { + LazyImage(url: avatarUrl) { state in + if let image = state.image { + self.buildAvatar(image: image) + } else if state.isLoading { + self.buildAvatar() + } else { + self.buildAvatar() + } + } + + Text(displayName ?? "") + .foregroundColor(.white) + .fontWeight(.semibold) + .shadow(color: .black, radius: 2) + Spacer() + } + + Spacer() + } + .padding(.leading, 6) + .padding(.top, 6) + } + } else { + content + } + } + + @ViewBuilder + private func buildAvatar(image: Image? = nil) -> some View { + (image ?? Image("Avatar")) + .resizable() + .clipShape(applicationState.avatarShape.shape()) + .aspectRatio(contentMode: .fit) + .frame(width: 24, height: 24) + .overlay( + applicationState.avatarShape.shape() + .stroke(Color.white.opacity(0.6), lineWidth: 1) + .frame(width: 24, height: 24) + ) + .shadow(color: .black, radius: 2) + } +} diff --git a/Vernissage/Views/SettingsView/Subviews/GeneralSectionView.swift b/Vernissage/Views/SettingsView/Subviews/GeneralSectionView.swift index 1b0d61c..af8d09b 100644 --- a/Vernissage/Views/SettingsView/Subviews/GeneralSectionView.swift +++ b/Vernissage/Views/SettingsView/Subviews/GeneralSectionView.swift @@ -68,7 +68,6 @@ struct GeneralSectionView: View { Text("settings.title.theme", comment: "Theme") } .onChange(of: self.applicationState.theme) { theme in - self.applicationState.theme = theme ApplicationSettingsHandler.shared.set(theme: theme) } @@ -82,9 +81,13 @@ struct GeneralSectionView: View { Text("settings.title.menuPosition", comment: "Menu position") } .onChange(of: self.applicationState.menuPosition) { menuPosition in - self.applicationState.menuPosition = menuPosition ApplicationSettingsHandler.shared.set(menuPosition: menuPosition) } + + Toggle("settings.title.showAvatarsOnTimeline", isOn: $applicationState.showAvatarsOnTimeline) + .onChange(of: self.applicationState.showAvatarsOnTimeline) { newValue in + ApplicationSettingsHandler.shared.set(showAvatarsOnTimeline: newValue) + } } } } diff --git a/Vernissage/Views/UserProfileView/Subviews/UserProfileStatusesView.swift b/Vernissage/Views/UserProfileView/Subviews/UserProfileStatusesView.swift index 0f64749..ad5f293 100644 --- a/Vernissage/Views/UserProfileView/Subviews/UserProfileStatusesView.swift +++ b/Vernissage/Views/UserProfileView/Subviews/UserProfileStatusesView.swift @@ -26,7 +26,7 @@ struct UserProfileStatusesView: View { LazyVStack(alignment: .center) { if firstLoadFinished == true { ForEach(self.statusViewModels, id: \.id) { item in - ImageRowAsync(statusViewModel: item) + ImageRowAsync(statusViewModel: item, withAvatar: false) } if allItemsLoaded == false && firstLoadFinished == true { diff --git a/Vernissage/Widgets/ImageRowAsync.swift b/Vernissage/Widgets/ImageRowAsync.swift index c4f7f5d..9563f08 100644 --- a/Vernissage/Widgets/ImageRowAsync.swift +++ b/Vernissage/Widgets/ImageRowAsync.swift @@ -12,12 +12,14 @@ import ServicesKit struct ImageRowAsync: View { private let statusViewModel: StatusModel private let firstAttachment: AttachmentModel? + private let showAvatar: Bool @State private var selected: String @State private var imageHeight: Double @State private var imageWidth: Double - init(statusViewModel: StatusModel) { + init(statusViewModel: StatusModel, withAvatar showAvatar: Bool = true) { + self.showAvatar = showAvatar self.statusViewModel = statusViewModel self.firstAttachment = statusViewModel.mediaAttachments.first self.selected = String.empty() @@ -41,7 +43,7 @@ struct ImageRowAsync: View { var body: some View { if statusViewModel.mediaAttachments.count == 1, let firstAttachment = self.firstAttachment { - ImageRowItemAsync(statusViewModel: self.statusViewModel, attachment: firstAttachment) { (imageWidth, imageHeight) in + ImageRowItemAsync(statusViewModel: self.statusViewModel, attachment: firstAttachment, withAvatar: self.showAvatar) { (imageWidth, imageHeight) in // When we download image and calculate real size we have to change view size. if imageWidth != self.imageWidth || imageHeight != self.imageHeight { withAnimation(.linear(duration: 0.4)) { @@ -54,7 +56,7 @@ struct ImageRowAsync: View { } else { TabView(selection: $selected) { ForEach(statusViewModel.mediaAttachments, id: \.id) { attachment in - ImageRowItemAsync(statusViewModel: self.statusViewModel, attachment: attachment) { (imageWidth, imageHeight) in + ImageRowItemAsync(statusViewModel: self.statusViewModel, attachment: attachment, withAvatar: self.showAvatar) { (imageWidth, imageHeight) in // When we download image and calculate real size we have to change view size (only when image is now visible). if attachment.id == self.selected { if imageWidth != self.imageWidth || imageHeight != self.imageHeight { diff --git a/Vernissage/Widgets/ImageRowItem.swift b/Vernissage/Widgets/ImageRowItem.swift index 803ea20..c742a03 100644 --- a/Vernissage/Widgets/ImageRowItem.swift +++ b/Vernissage/Widgets/ImageRowItem.swift @@ -38,32 +38,9 @@ struct ImageRowItem: View { var body: some View { if let uiImage { - ZStack { - if self.status.sensitive && !self.applicationState.showSensitive { - ZStack { - ContentWarning(spoilerText: self.status.spoilerText) { - self.imageView(uiImage: uiImage) - - if showThumbImage { - FavouriteTouch { - self.showThumbImage = false - } - } - } blurred: { - BlurredImage(blurhash: attachmentData.blurhash) - .onTapGesture { - self.navigateToStatus() - } - } - } - .opacity(self.opacity) - .onAppear { - withAnimation { - self.opacity = 1.0 - } - } - } else { - ZStack { + if self.status.sensitive && !self.applicationState.showSensitive { + ZStack { + ContentWarning(spoilerText: self.status.spoilerText) { self.imageView(uiImage: uiImage) if showThumbImage { @@ -71,14 +48,35 @@ struct ImageRowItem: View { self.showThumbImage = false } } + } blurred: { + BlurredImage(blurhash: attachmentData.blurhash) + .onTapGesture { + self.navigateToStatus() + } } - .opacity(self.opacity) - .onAppear { - withAnimation { - self.opacity = 1.0 + } + .opacity(self.opacity) + .onAppear { + withAnimation { + self.opacity = 1.0 + } + } + } else { + ZStack { + self.imageView(uiImage: uiImage) + + if showThumbImage { + FavouriteTouch { + self.showThumbImage = false } } } + .opacity(self.opacity) + .onAppear { + withAnimation { + self.opacity = 1.0 + } + } } } else { if cancelled { @@ -123,6 +121,9 @@ struct ImageRowItem: View { .onTapGesture { self.navigateToStatus() } + .imageAvatar(applicationState: self.applicationState, + displayName: self.status.accountDisplayName, + avatarUrl: self.status.accountAvatar) .imageContextMenu(client: self.client, statusData: self.status) } diff --git a/Vernissage/Widgets/ImageRowItemAsync.swift b/Vernissage/Widgets/ImageRowItemAsync.swift index 6eeee11..49a157e 100644 --- a/Vernissage/Widgets/ImageRowItemAsync.swift +++ b/Vernissage/Widgets/ImageRowItemAsync.swift @@ -19,13 +19,17 @@ struct ImageRowItemAsync: View { private var statusViewModel: StatusModel private var attachment: AttachmentModel + private let showAvatar: Bool @State private var showThumbImage = false @State private var opacity = 0.0 private let onImageDownloaded: (Double, Double) -> Void - init(statusViewModel: StatusModel, attachment: AttachmentModel, onImageDownloaded: @escaping (_: Double, _: Double) -> Void) { + init(statusViewModel: StatusModel, + attachment: AttachmentModel, + withAvatar showAvatar: Bool = true, onImageDownloaded: @escaping (_: Double, _: Double) -> Void) { + self.showAvatar = showAvatar self.statusViewModel = statusViewModel self.attachment = attachment self.onImageDownloaded = onImageDownloaded @@ -123,6 +127,11 @@ struct ImageRowItemAsync: View { .onTapGesture { self.navigateToStatus() } + .if(self.showAvatar) { + $0.imageAvatar(applicationState: self.applicationState, + displayName: self.statusViewModel.account.displayNameWithoutEmojis, + avatarUrl: self.statusViewModel.account.avatar) + } .imageContextMenu(client: self.client, statusModel: self.statusViewModel) } diff --git a/WidgetsKit/Sources/WidgetsKit/Extensions/View+If.swift b/WidgetsKit/Sources/WidgetsKit/Extensions/View+If.swift new file mode 100644 index 0000000..5929eb5 --- /dev/null +++ b/WidgetsKit/Sources/WidgetsKit/Extensions/View+If.swift @@ -0,0 +1,19 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +import Foundation +import SwiftUI + +public extension View { + @ViewBuilder + func `if`(_ conditional: Bool, content: (Self) -> Content) -> some View { + if conditional { + content(self) + } else { + self + } + } +}