First implementation of avatars on timeline
This commit is contained in:
parent
c3f4ecdd3f
commit
776236b2f4
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -3,6 +3,6 @@
|
|||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>_XCCurrentVersionName</key>
|
||||
<string>Vernissage-007.xcdatamodel</string>
|
||||
<string>Vernissage-008.xcdatamodel</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21754" systemVersion="22E261" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="AccountData" representedClassName="AccountData" syncable="YES">
|
||||
<attribute name="accessToken" optional="YES" attributeType="String"/>
|
||||
<attribute name="acct" attributeType="String"/>
|
||||
<attribute name="avatar" optional="YES" attributeType="URI"/>
|
||||
<attribute name="avatarData" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="clientId" attributeType="String"/>
|
||||
<attribute name="clientSecret" attributeType="String"/>
|
||||
<attribute name="clientVapidKey" attributeType="String"/>
|
||||
<attribute name="createdAt" attributeType="String"/>
|
||||
<attribute name="displayName" optional="YES" attributeType="String"/>
|
||||
<attribute name="followersCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="followingCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="header" optional="YES" attributeType="URI"/>
|
||||
<attribute name="id" attributeType="String"/>
|
||||
<attribute name="lastSeenStatusId" optional="YES" attributeType="String"/>
|
||||
<attribute name="locked" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="note" optional="YES" attributeType="String"/>
|
||||
<attribute name="refreshToken" optional="YES" attributeType="String"/>
|
||||
<attribute name="serverUrl" attributeType="URI"/>
|
||||
<attribute name="statusesCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="url" optional="YES" attributeType="URI"/>
|
||||
<attribute name="username" attributeType="String"/>
|
||||
<relationship name="statuses" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="StatusData" inverseName="pixelfedAccount" inverseEntity="StatusData"/>
|
||||
</entity>
|
||||
<entity name="ApplicationSettings" representedClassName="ApplicationSettings" syncable="YES">
|
||||
<attribute name="activeIcon" attributeType="String" defaultValueString="Default"/>
|
||||
<attribute name="avatarShape" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="YES"/>
|
||||
<attribute name="currentAccount" optional="YES" attributeType="String"/>
|
||||
<attribute name="hapticAnimationEnabled" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="hapticButtonPressEnabled" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="hapticNotificationEnabled" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="hapticRefreshEnabled" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="hapticTabSelectionEnabled" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="lastRefreshTokens" attributeType="Date" defaultDateTimeInterval="694256400" usesScalarValueType="NO"/>
|
||||
<attribute name="menuPosition" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="YES"/>
|
||||
<attribute name="showAvatarsOnTimeline" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="showPhotoDescription" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="showSensitive" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="theme" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="tintColor" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="YES"/>
|
||||
</entity>
|
||||
<entity name="AttachmentData" representedClassName="AttachmentData" syncable="YES">
|
||||
<attribute name="blurhash" optional="YES" attributeType="String"/>
|
||||
<attribute name="data" optional="YES" attributeType="Binary" allowsExternalBinaryDataStorage="YES"/>
|
||||
<attribute name="exifCamera" optional="YES" attributeType="String"/>
|
||||
<attribute name="exifCreatedDate" optional="YES" attributeType="String"/>
|
||||
<attribute name="exifExposure" optional="YES" attributeType="String"/>
|
||||
<attribute name="exifLens" optional="YES" attributeType="String"/>
|
||||
<attribute name="id" attributeType="String"/>
|
||||
<attribute name="metaImageHeight" optional="YES" attributeType="Integer 32" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="metaImageWidth" optional="YES" attributeType="Integer 32" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="previewUrl" optional="YES" attributeType="URI"/>
|
||||
<attribute name="remoteUrl" optional="YES" attributeType="URI"/>
|
||||
<attribute name="statusId" attributeType="String"/>
|
||||
<attribute name="text" optional="YES" attributeType="String"/>
|
||||
<attribute name="type" attributeType="String"/>
|
||||
<attribute name="url" attributeType="URI"/>
|
||||
<relationship name="statusRelation" maxCount="1" deletionRule="Nullify" destinationEntity="StatusData" inverseName="attachmentsRelation" inverseEntity="StatusData"/>
|
||||
</entity>
|
||||
<entity name="StatusData" representedClassName="StatusData" syncable="YES">
|
||||
<attribute name="accountAvatar" optional="YES" attributeType="URI"/>
|
||||
<attribute name="accountDisplayName" optional="YES" attributeType="String"/>
|
||||
<attribute name="accountId" attributeType="String"/>
|
||||
<attribute name="accountUsername" optional="YES" attributeType="String"/>
|
||||
<attribute name="applicationName" optional="YES" attributeType="String"/>
|
||||
<attribute name="applicationWebsite" optional="YES" attributeType="URI"/>
|
||||
<attribute name="bookmarked" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="content" attributeType="String"/>
|
||||
<attribute name="createdAt" attributeType="String"/>
|
||||
<attribute name="favourited" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="favouritesCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="id" attributeType="String"/>
|
||||
<attribute name="inReplyToAccount" optional="YES" attributeType="String"/>
|
||||
<attribute name="inReplyToId" optional="YES" attributeType="String"/>
|
||||
<attribute name="muted" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="pinned" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="reblogged" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="rebloggedAccountAvatar" optional="YES" attributeType="URI"/>
|
||||
<attribute name="rebloggedAccountDisplayName" optional="YES" attributeType="String"/>
|
||||
<attribute name="rebloggedAccountId" optional="YES" attributeType="String"/>
|
||||
<attribute name="rebloggedAccountUsername" optional="YES" attributeType="String"/>
|
||||
<attribute name="rebloggedStatusId" optional="YES" attributeType="String"/>
|
||||
<attribute name="reblogsCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="repliesCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sensitive" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="spoilerText" optional="YES" attributeType="String"/>
|
||||
<attribute name="uri" attributeType="String"/>
|
||||
<attribute name="url" optional="YES" attributeType="URI"/>
|
||||
<attribute name="visibility" attributeType="String"/>
|
||||
<relationship name="attachmentsRelation" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="AttachmentData" inverseName="statusRelation" inverseEntity="AttachmentData"/>
|
||||
<relationship name="pixelfedAccount" maxCount="1" deletionRule="Nullify" destinationEntity="AccountData" inverseName="statuses" inverseEntity="AccountData"/>
|
||||
</entity>
|
||||
</model>
|
|
@ -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
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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 = "<group>"; };
|
||||
F89F0605299139F6003DC875 /* Vernissage-002.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-002.xcdatamodel"; sourceTree = "<group>"; };
|
||||
F89F57AF29D1C11200001EE3 /* RelationshipModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelationshipModel.swift; sourceTree = "<group>"; };
|
||||
F8A4A88229E3FD1C00267E36 /* ImageAvatar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageAvatar.swift; sourceTree = "<group>"; };
|
||||
F8A4A88429E4099900267E36 /* Vernissage-008.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-008.xcdatamodel"; sourceTree = "<group>"; };
|
||||
F8A93D7D2965FD89001D8331 /* UserProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileView.swift; sourceTree = "<group>"; };
|
||||
F8AFF7C029B259150087D083 /* HashtagsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagsView.swift; sourceTree = "<group>"; };
|
||||
F8AFF7C329B25EF40087D083 /* ImagesGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagesGrid.swift; sourceTree = "<group>"; };
|
||||
|
@ -630,6 +633,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
F88BC53A29E06A5100CE6141 /* ImageContextMenu.swift */,
|
||||
F8A4A88229E3FD1C00267E36 /* ImageAvatar.swift */,
|
||||
);
|
||||
path = ViewModifiers;
|
||||
sourceTree = "<group>";
|
||||
|
@ -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 = "<group>";
|
||||
versionGroupType = wrapper.xcdatamodel;
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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`<Content: View>(_ conditional: Bool, content: (Self) -> Content) -> some View {
|
||||
if conditional {
|
||||
content(self)
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue