mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-12-23 15:40:37 +00:00
Move AccountPopoverView
This commit is contained in:
parent
06a8ca67c3
commit
d2f7ab1464
2 changed files with 192 additions and 187 deletions
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue