mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-11-26 10:11:00 +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)
|
.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