Move AccountPopoverView

This commit is contained in:
Thomas Ricouard 2023-11-27 09:19:43 +01:00
parent 06a8ca67c3
commit d2f7ab1464
2 changed files with 192 additions and 187 deletions

View file

@ -0,0 +1,192 @@
import Nuke
import NukeUI
import Shimmer
import SwiftUI
import Models
struct AccountPopoverView: View {
let account: Account
let theme: Theme // using `@Environment(Theme.self) will crash the SwiftUI preview
private let config: AvatarView.FrameConfig = .account
@Binding var showPopup: Bool
@Binding var autoDismiss: Bool
@Binding var toggleTask: Task<Void, Never>
var body: some View {
VStack(alignment: .leading) {
LazyImage(request: ImageRequest(url: account.header)
) { state in
if let image = state.image {
image.resizable().scaledToFill()
}
}
.frame(width: 500, height: 150)
.clipped()
.background(theme.secondaryBackgroundColor)
VStack(alignment: .leading) {
HStack(alignment: .bottomAvatar) {
AvatarImage(account.avatar, config: adaptiveConfig)
Spacer()
makeCustomInfoLabel(title: "account.following", count: account.followingCount ?? 0)
makeCustomInfoLabel(title: "account.posts", count: account.statusesCount ?? 0)
makeCustomInfoLabel(title: "account.followers", count: account.followersCount ?? 0)
}
.frame(height: adaptiveConfig.height / 2, alignment: .bottom)
EmojiTextApp(.init(stringValue: account.safeDisplayName ), emojis: account.emojis)
.font(.headline)
.foregroundColor(theme.labelColor)
.emojiSize(Font.scaledHeadlineFont.emojiSize)
.emojiBaselineOffset(Font.scaledHeadlineFont.emojiBaselineOffset)
.accessibilityAddTraits(.isHeader)
.help(account.safeDisplayName)
Text("@\(account.acct)")
.font(.callout)
.foregroundColor(.gray)
.textSelection(.enabled)
.accessibilityRespondsToUserInteraction(false)
.help("@\(account.acct)")
HStack(spacing: 4) {
Image(systemName: "calendar")
.accessibilityHidden(true)
Text("account.joined")
Text(account.createdAt.asDate, style: .date)
}
.foregroundColor(.gray)
.font(.footnote)
.accessibilityElement(children: .combine)
EmojiTextApp(account.note, emojis: account.emojis, lineLimit: 5)
.font(.body)
.emojiSize(Font.scaledFootnoteFont.emojiSize)
.emojiBaselineOffset(Font.scaledFootnoteFont.emojiBaselineOffset)
.padding(.top, 3)
}
.padding([.leading, .trailing, .bottom])
}
.frame(width: 500)
.onAppear {
toggleTask.cancel()
toggleTask = Task {
try? await Task.sleep(nanoseconds: NSEC_PER_SEC * 2)
guard !Task.isCancelled else { return }
if autoDismiss {
showPopup = false
}
}
}
.onHover { hovering in
toggleTask.cancel()
toggleTask = Task {
try? await Task.sleep(nanoseconds: NSEC_PER_SEC / 2)
guard !Task.isCancelled else { return }
if hovering {
autoDismiss = false
} else {
showPopup = false
autoDismiss = true
}
}
}
}
@MainActor
private func makeCustomInfoLabel(title: LocalizedStringKey, count: Int, needsBadge: Bool = false) -> some View {
VStack {
Text(count, format: .number.notation(.compactName))
.font(.scaledHeadline)
.foregroundColor(theme.tintColor)
.overlay(alignment: .trailing) {
if needsBadge {
Circle()
.fill(Color.red)
.frame(width: 9, height: 9)
.offset(x: 12)
}
}
Text(title)
.font(.scaledFootnote)
.foregroundColor(.gray)
.alignmentGuide(.bottomAvatar, computeValue: { dimension in
dimension[.firstTextBaseline]
})
}
.accessibilityElement(children: .ignore)
.accessibilityLabel(title)
.accessibilityValue("\(count)")
}
private var adaptiveConfig: AvatarView.FrameConfig {
var cornerRadius: CGFloat
if config == .badge || theme.avatarShape == .circle {
cornerRadius = config.width / 2
} else {
cornerRadius = config.cornerRadius
}
return AvatarView.FrameConfig(width: config.width, height: config.height, cornerRadius: cornerRadius)
}
}
private enum BottomAvatarAlignment: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
context.height
}
}
extension VerticalAlignment {
static let bottomAvatar = VerticalAlignment(BottomAvatarAlignment.self)
}
public struct AccountPopoverModifier : ViewModifier {
@Environment(Theme.self) private var theme
@State private var showPopup = false
@State private var autoDismiss = true
@State private var toggleTask: Task<Void, Never> = Task {}
let account: Account
public func body(content: Content) -> some View {
content
.onHover { hovering in
if hovering {
toggleTask.cancel()
toggleTask = Task {
try? await Task.sleep(nanoseconds: NSEC_PER_SEC / 2)
guard !Task.isCancelled else { return }
if !showPopup {
showPopup = true
}
}
} else {
if !showPopup {
toggleTask.cancel()
}
}
}
.hoverEffect(.lift)
.popover(isPresented: $showPopup) {
AccountPopoverView(
account: account,
theme: theme,
showPopup: $showPopup,
autoDismiss: $autoDismiss,
toggleTask: $toggleTask
)
}
}
init(_ account: Account) {
self.account = account
}
}
extension View {
public func accountPopover(_ account: Account) -> some View {
modifier(AccountPopoverModifier(account))
}
}

View file

@ -148,190 +148,3 @@ struct AvatarPlaceHolder: View {
.frame(width: config.width, height: config.height)
}
}
struct AccountPopoverView: View {
let account: Account
let theme: Theme // using `@Environment(Theme.self) will crash the SwiftUI preview
private let config: AvatarView.FrameConfig = .account
@Binding var showPopup: Bool
@Binding var autoDismiss: Bool
@Binding var toggleTask: Task<Void, Never>
var body: some View {
VStack(alignment: .leading) {
LazyImage(request: ImageRequest(url: account.header)
) { state in
if let image = state.image {
image.resizable().scaledToFill()
}
}
.frame(width: 500, height: 150)
.clipped()
.background(theme.secondaryBackgroundColor)
VStack(alignment: .leading) {
HStack(alignment: .bottomAvatar) {
AvatarImage(account.avatar, config: adaptiveConfig)
Spacer()
makeCustomInfoLabel(title: "account.following", count: account.followingCount ?? 0)
makeCustomInfoLabel(title: "account.posts", count: account.statusesCount ?? 0)
makeCustomInfoLabel(title: "account.followers", count: account.followersCount ?? 0)
}
.frame(height: adaptiveConfig.height / 2, alignment: .bottom)
EmojiTextApp(.init(stringValue: account.safeDisplayName ), emojis: account.emojis)
.font(.headline)
.foregroundColor(theme.labelColor)
.emojiSize(Font.scaledHeadlineFont.emojiSize)
.emojiBaselineOffset(Font.scaledHeadlineFont.emojiBaselineOffset)
.accessibilityAddTraits(.isHeader)
.help(account.safeDisplayName)
Text("@\(account.acct)")
.font(.callout)
.foregroundColor(.gray)
.textSelection(.enabled)
.accessibilityRespondsToUserInteraction(false)
.help("@\(account.acct)")
HStack(spacing: 4) {
Image(systemName: "calendar")
.accessibilityHidden(true)
Text("account.joined")
Text(account.createdAt.asDate, style: .date)
}
.foregroundColor(.gray)
.font(.footnote)
.accessibilityElement(children: .combine)
EmojiTextApp(account.note, emojis: account.emojis, lineLimit: 5)
.font(.body)
.emojiSize(Font.scaledFootnoteFont.emojiSize)
.emojiBaselineOffset(Font.scaledFootnoteFont.emojiBaselineOffset)
.padding(.top, 3)
}
.padding([.leading, .trailing, .bottom])
}
.frame(width: 500)
.onAppear {
toggleTask.cancel()
toggleTask = Task {
try? await Task.sleep(nanoseconds: NSEC_PER_SEC * 2)
guard !Task.isCancelled else { return }
if autoDismiss {
showPopup = false
}
}
}
.onHover { hovering in
toggleTask.cancel()
toggleTask = Task {
try? await Task.sleep(nanoseconds: NSEC_PER_SEC / 2)
guard !Task.isCancelled else { return }
if hovering {
autoDismiss = false
} else {
showPopup = false
autoDismiss = true
}
}
}
}
@MainActor
private func makeCustomInfoLabel(title: LocalizedStringKey, count: Int, needsBadge: Bool = false) -> some View {
VStack {
Text(count, format: .number.notation(.compactName))
.font(.scaledHeadline)
.foregroundColor(theme.tintColor)
.overlay(alignment: .trailing) {
if needsBadge {
Circle()
.fill(Color.red)
.frame(width: 9, height: 9)
.offset(x: 12)
}
}
Text(title)
.font(.scaledFootnote)
.foregroundColor(.gray)
.alignmentGuide(.bottomAvatar, computeValue: { dimension in
dimension[.firstTextBaseline]
})
}
.accessibilityElement(children: .ignore)
.accessibilityLabel(title)
.accessibilityValue("\(count)")
}
private var adaptiveConfig: AvatarView.FrameConfig {
var cornerRadius: CGFloat
if config == .badge || theme.avatarShape == .circle {
cornerRadius = config.width / 2
} else {
cornerRadius = config.cornerRadius
}
return AvatarView.FrameConfig(width: config.width, height: config.height, cornerRadius: cornerRadius)
}
}
private enum BottomAvatarAlignment: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
context.height
}
}
extension VerticalAlignment {
static let bottomAvatar = VerticalAlignment(BottomAvatarAlignment.self)
}
public struct AccountPopoverModifier : ViewModifier {
@Environment(Theme.self) private var theme
@State private var showPopup = false
@State private var autoDismiss = true
@State private var toggleTask: Task<Void, Never> = Task {}
let account: Account
public func body(content: Content) -> some View {
content
.onHover { hovering in
if hovering {
toggleTask.cancel()
toggleTask = Task {
try? await Task.sleep(nanoseconds: NSEC_PER_SEC / 2)
guard !Task.isCancelled else { return }
if !showPopup {
showPopup = true
}
}
} else {
if !showPopup {
toggleTask.cancel()
}
}
}
.hoverEffect(.lift)
.popover(isPresented: $showPopup) {
AccountPopoverView(
account: account,
theme: theme,
showPopup: $showPopup,
autoDismiss: $autoDismiss,
toggleTask: $toggleTask
)
}
}
init(_ account: Account) {
self.account = account
}
}
extension View {
public func accountPopover(_ account: Account) -> some View {
modifier(AccountPopoverModifier(account))
}
}