mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2025-02-04 00:17:30 +01:00
Feature: popover the account overview when hovering on the avatar (#1682)
* fix avatar scale * refactor avatar config data * add `AvatarView_Previews` * refactor shape and placeholder of avatar * refactor `AvatarView` and add `AvatarPopup` * add `hoverEffect` for iPad * fix auto-dismiss bug * fix `showPopup` bug * disable inappropriate avatar popups
This commit is contained in:
parent
534b098ca6
commit
94172cef27
@ -105,7 +105,7 @@ struct AccountSettingsView: View {
|
|||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .principal) {
|
ToolbarItem(placement: .principal) {
|
||||||
HStack {
|
HStack {
|
||||||
AvatarView(url: account.avatar, size: .embed)
|
AvatarView(account: account, config: .embed)
|
||||||
Text(account.safeDisplayName)
|
Text(account.safeDisplayName)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
}
|
}
|
||||||
|
@ -99,7 +99,7 @@ struct AccountDetailHeaderView: View {
|
|||||||
private var accountAvatarView: some View {
|
private var accountAvatarView: some View {
|
||||||
HStack {
|
HStack {
|
||||||
ZStack(alignment: .topTrailing) {
|
ZStack(alignment: .topTrailing) {
|
||||||
AvatarView(url: account.avatar, size: .account)
|
AvatarView(account: account, config: .account)
|
||||||
.accessibilityLabel("accessibility.tabs.profile.user-avatar.label")
|
.accessibilityLabel("accessibility.tabs.profile.user-avatar.label")
|
||||||
if viewModel.isCurrentUser, isSupporter {
|
if viewModel.isCurrentUser, isSupporter {
|
||||||
Image(systemName: "checkmark.seal.fill")
|
Image(systemName: "checkmark.seal.fill")
|
||||||
|
@ -214,7 +214,7 @@ public struct AccountDetailView: View {
|
|||||||
Button {
|
Button {
|
||||||
routerPath.navigate(to: .accountDetailWithAccount(account: account))
|
routerPath.navigate(to: .accountDetailWithAccount(account: account))
|
||||||
} label: {
|
} label: {
|
||||||
AvatarView(url: account.avatar, size: .badge)
|
AvatarView(account: account, config: .badge)
|
||||||
.padding(.leading, -4)
|
.padding(.leading, -4)
|
||||||
.accessibilityLabel(account.safeDisplayName)
|
.accessibilityLabel(account.safeDisplayName)
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ public struct AccountsListRow: View {
|
|||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
HStack(alignment: .top) {
|
HStack(alignment: .top) {
|
||||||
AvatarView(url: viewModel.account.avatar, size: .status)
|
AvatarView(account: viewModel.account, config: .status)
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
EmojiTextApp(.init(stringValue: viewModel.account.safeDisplayName), emojis: viewModel.account.emojis)
|
EmojiTextApp(.init(stringValue: viewModel.account.safeDisplayName), emojis: viewModel.account.emojis)
|
||||||
.font(.scaledSubheadline)
|
.font(.scaledSubheadline)
|
||||||
|
@ -35,7 +35,7 @@ public struct AppAccountView: View {
|
|||||||
private var compactView: some View {
|
private var compactView: some View {
|
||||||
HStack {
|
HStack {
|
||||||
if let account = viewModel.account {
|
if let account = viewModel.account {
|
||||||
AvatarView(url: account.avatar)
|
AvatarView(account: account)
|
||||||
} else {
|
} else {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
}
|
}
|
||||||
@ -61,7 +61,7 @@ public struct AppAccountView: View {
|
|||||||
HStack {
|
HStack {
|
||||||
if let account = viewModel.account {
|
if let account = viewModel.account {
|
||||||
ZStack(alignment: .topTrailing) {
|
ZStack(alignment: .topTrailing) {
|
||||||
AvatarView(url: account.avatar)
|
AvatarView(account: account)
|
||||||
if viewModel.appAccount.id == appAccounts.currentAccount.id {
|
if viewModel.appAccount.id == appAccounts.currentAccount.id {
|
||||||
Image(systemName: "checkmark.circle.fill")
|
Image(systemName: "checkmark.circle.fill")
|
||||||
.foregroundStyle(.white, .green)
|
.foregroundStyle(.white, .green)
|
||||||
|
@ -15,7 +15,7 @@ public struct AppAccountsSelectorView: View {
|
|||||||
@State private var isPresented: Bool = false
|
@State private var isPresented: Bool = false
|
||||||
|
|
||||||
private let accountCreationEnabled: Bool
|
private let accountCreationEnabled: Bool
|
||||||
private let avatarSize: AvatarView.Size
|
private let avatarConfig: AvatarView.FrameConfig
|
||||||
|
|
||||||
private var showNotificationBadge: Bool {
|
private var showNotificationBadge: Bool {
|
||||||
accountsViewModel
|
accountsViewModel
|
||||||
@ -33,11 +33,11 @@ public struct AppAccountsSelectorView: View {
|
|||||||
|
|
||||||
public init(routerPath: RouterPath,
|
public init(routerPath: RouterPath,
|
||||||
accountCreationEnabled: Bool = true,
|
accountCreationEnabled: Bool = true,
|
||||||
avatarSize: AvatarView.Size = .badge)
|
avatarConfig: AvatarView.FrameConfig = .badge)
|
||||||
{
|
{
|
||||||
self.routerPath = routerPath
|
self.routerPath = routerPath
|
||||||
self.accountCreationEnabled = accountCreationEnabled
|
self.accountCreationEnabled = accountCreationEnabled
|
||||||
self.avatarSize = avatarSize
|
self.avatarConfig = avatarConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
@ -71,10 +71,10 @@ public struct AppAccountsSelectorView: View {
|
|||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var labelView: some View {
|
private var labelView: some View {
|
||||||
Group {
|
Group {
|
||||||
if let avatar = currentAccount.account?.avatar, !currentAccount.isLoadingAccount {
|
if let account = currentAccount.account, !currentAccount.isLoadingAccount {
|
||||||
AvatarView(url: avatar, size: avatarSize)
|
AvatarView(account: account, config: avatarConfig)
|
||||||
} else {
|
} else {
|
||||||
AvatarView(url: nil, size: avatarSize)
|
AvatarView(account: nil, config: avatarConfig)
|
||||||
.redacted(reason: .placeholder)
|
.redacted(reason: .placeholder)
|
||||||
.allowsHitTesting(false)
|
.allowsHitTesting(false)
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ struct ConversationMessageView: View {
|
|||||||
if isOwnMessage {
|
if isOwnMessage {
|
||||||
Spacer()
|
Spacer()
|
||||||
} else {
|
} else {
|
||||||
AvatarView(url: message.account.avatar, size: .status)
|
AvatarView(account: message.account, config: .status)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
routerPath.navigate(to: .accountDetailWithAccount(account: message.account))
|
routerPath.navigate(to: .accountDetailWithAccount(account: message.account))
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ struct ConversationsListRow: View {
|
|||||||
} label: {
|
} label: {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
HStack(alignment: .top, spacing: 8) {
|
HStack(alignment: .top, spacing: 8) {
|
||||||
AvatarView(url: conversation.accounts.first!.avatar)
|
AvatarView(account: conversation.accounts.first!)
|
||||||
.accessibilityHidden(true)
|
.accessibilityHidden(true)
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
HStack {
|
HStack {
|
||||||
|
@ -2,104 +2,315 @@ import Nuke
|
|||||||
import NukeUI
|
import NukeUI
|
||||||
import Shimmer
|
import Shimmer
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import Models
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public struct AvatarView: View {
|
public struct AvatarView: View {
|
||||||
@Environment(\.redactionReasons) private var reasons
|
|
||||||
@Environment(Theme.self) private var theme
|
@Environment(Theme.self) private var theme
|
||||||
|
|
||||||
public enum Size {
|
@State private var showPopup = false
|
||||||
case account, status, embed, badge, list, boost
|
@State private var autoDismiss = true
|
||||||
|
@State private var toggleTask: Task<Void, Never> = Task {}
|
||||||
|
|
||||||
public var size: CGSize {
|
public let account: Account?
|
||||||
switch self {
|
public let config: FrameConfig
|
||||||
case .account:
|
public let hasPopup: Bool
|
||||||
return .init(width: 80, height: 80)
|
|
||||||
case .status:
|
|
||||||
if ProcessInfo.processInfo.isMacCatalystApp {
|
|
||||||
return .init(width: 48, height: 48)
|
|
||||||
}
|
|
||||||
return .init(width: 40, height: 40)
|
|
||||||
case .embed:
|
|
||||||
return .init(width: 34, height: 34)
|
|
||||||
case .badge:
|
|
||||||
return .init(width: 28, height: 28)
|
|
||||||
case .list:
|
|
||||||
return .init(width: 20, height: 20)
|
|
||||||
case .boost:
|
|
||||||
return .init(width: 12, height: 12)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var cornerRadius: CGFloat {
|
|
||||||
switch self {
|
|
||||||
case .badge, .boost, .list:
|
|
||||||
size.width / 2
|
|
||||||
default:
|
|
||||||
4
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public let url: URL?
|
|
||||||
public let size: Size
|
|
||||||
|
|
||||||
public init(url: URL?, size: Size = .status) {
|
|
||||||
self.url = url
|
|
||||||
self.size = size
|
|
||||||
}
|
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
Group {
|
if let account = account {
|
||||||
if reasons == .placeholder {
|
if hasPopup {
|
||||||
RoundedRectangle(cornerRadius: size.cornerRadius)
|
AvatarImage(account: account, config: adaptiveConfig)
|
||||||
.fill(.gray)
|
.onHover { hovering in
|
||||||
.frame(width: size.size.width, height: size.size.height)
|
if hovering {
|
||||||
} else {
|
toggleTask.cancel()
|
||||||
LazyImage(request: url.map { makeImageRequest(for: $0) }) { state in
|
toggleTask = Task {
|
||||||
if let image = state.image {
|
try? await Task.sleep(nanoseconds: NSEC_PER_SEC / 2)
|
||||||
image
|
guard !Task.isCancelled else { return }
|
||||||
.resizable()
|
if !showPopup {
|
||||||
.aspectRatio(contentMode: .fit)
|
showPopup = true
|
||||||
} else {
|
}
|
||||||
AvatarPlaceholderView(size: size)
|
}
|
||||||
|
} else {
|
||||||
|
if !showPopup {
|
||||||
|
toggleTask.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
.hoverEffect(.lift)
|
||||||
.frame(width: size.size.width, height: size.size.height)
|
.popover(isPresented: $showPopup) {
|
||||||
|
AccountPopupView(
|
||||||
|
account: account,
|
||||||
|
theme: theme,
|
||||||
|
showPopup: $showPopup,
|
||||||
|
autoDismiss: $autoDismiss,
|
||||||
|
toggleTask: $toggleTask
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
AvatarImage(account: account, config: adaptiveConfig)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
AvatarPlaceHolder(config: adaptiveConfig)
|
||||||
}
|
}
|
||||||
.clipShape(clipShape)
|
|
||||||
.overlay(
|
|
||||||
clipShape.stroke(Color.primary.opacity(0.25), lineWidth: 1)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makeImageRequest(for url: URL) -> ImageRequest {
|
private var adaptiveConfig: FrameConfig {
|
||||||
ImageRequest(url: url, processors: [.resize(size: size.size)])
|
var cornerRadius: CGFloat
|
||||||
|
if config == .badge || theme.avatarShape == .circle {
|
||||||
|
cornerRadius = config.width / 2
|
||||||
|
} else {
|
||||||
|
cornerRadius = config.cornerRadius
|
||||||
|
}
|
||||||
|
return FrameConfig(width: config.width, height: config.height, cornerRadius: cornerRadius)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var clipShape: some Shape {
|
public init(account: Account?, config: FrameConfig = FrameConfig.status, hasPopup: Bool = false) {
|
||||||
switch theme.avatarShape {
|
self.account = account
|
||||||
case .circle:
|
self.config = config
|
||||||
AnyShape(Circle())
|
self.hasPopup = hasPopup
|
||||||
case .rounded:
|
}
|
||||||
AnyShape(RoundedRectangle(cornerRadius: size.cornerRadius))
|
|
||||||
|
public struct FrameConfig: Equatable {
|
||||||
|
public let size: CGSize
|
||||||
|
public var width: CGFloat { size.width }
|
||||||
|
public var height: CGFloat { size.height }
|
||||||
|
let cornerRadius: CGFloat
|
||||||
|
|
||||||
|
init(width: CGFloat, height: CGFloat, cornerRadius: CGFloat = 4) {
|
||||||
|
self.size = CGSize(width: width, height: height)
|
||||||
|
self.cornerRadius = cornerRadius
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static let account = FrameConfig(width: 80, height: 80)
|
||||||
|
public static let status = {
|
||||||
|
if ProcessInfo.processInfo.isMacCatalystApp {
|
||||||
|
return FrameConfig(width: 48, height: 48)
|
||||||
|
}
|
||||||
|
return FrameConfig(width: 40, height: 40)
|
||||||
|
}()
|
||||||
|
public static let embed = FrameConfig(width: 34, height: 34)
|
||||||
|
public static let badge = FrameConfig(width: 28, height: 28, cornerRadius: 14)
|
||||||
|
public static let list = FrameConfig(width: 20, height: 20, cornerRadius: 10)
|
||||||
|
public static let boost = FrameConfig(width: 12, height: 12, cornerRadius: 6)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct AvatarPlaceholderView: View {
|
struct AvatarView_Previews: PreviewProvider {
|
||||||
let size: AvatarView.Size
|
static var previews: some View {
|
||||||
|
PreviewWrapper()
|
||||||
|
.padding()
|
||||||
|
.previewLayout(.sizeThatFits)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PreviewWrapper: View {
|
||||||
|
@State private var isCircleAvatar = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if size == .badge {
|
VStack(alignment: .leading) {
|
||||||
Circle()
|
AvatarView(account: Self.account, config: .status)
|
||||||
.fill(.gray)
|
.environment(Theme.shared)
|
||||||
.frame(width: size.size.width, height: size.size.height)
|
Toggle("Avatar Shape", isOn: $isCircleAvatar)
|
||||||
|
}
|
||||||
|
.onChange(of: isCircleAvatar) {
|
||||||
|
Theme.shared.avatarShape = self.isCircleAvatar ? .circle : .rounded
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
Theme.shared.avatarShape = self.isCircleAvatar ? .circle : .rounded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static let account = Account(
|
||||||
|
id: UUID().uuidString,
|
||||||
|
username: "@clattner_llvm",
|
||||||
|
displayName: "Chris Lattner",
|
||||||
|
avatar: URL(string: "https://pbs.twimg.com/profile_images/1484209565788897285/1n6Viahb_400x400.jpg")!,
|
||||||
|
header: URL(string: "https://pbs.twimg.com/profile_banners/2543588034/1656822255/1500x500")!,
|
||||||
|
acct: "clattner_llvm@example.com",
|
||||||
|
note: .init(stringValue: "Building beautiful things @Modular_AI 🔥, lifting the world of production AI/ML software into a new phase of innovation. We’re hiring! 🚀🧠"),
|
||||||
|
createdAt: ServerDate(),
|
||||||
|
followersCount: 77100,
|
||||||
|
followingCount: 167,
|
||||||
|
statusesCount: 123,
|
||||||
|
lastStatusAt: nil,
|
||||||
|
fields: [],
|
||||||
|
locked: false,
|
||||||
|
emojis: [],
|
||||||
|
url: URL(string: "https://nondot.org/sabre/")!,
|
||||||
|
source: nil,
|
||||||
|
bot: false,
|
||||||
|
discoverable: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AvatarImage: View {
|
||||||
|
@Environment(\.redactionReasons) private var reasons
|
||||||
|
|
||||||
|
public let account: Account
|
||||||
|
public let config: AvatarView.FrameConfig
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if reasons == .placeholder {
|
||||||
|
AvatarPlaceHolder(config: config)
|
||||||
} else {
|
} else {
|
||||||
RoundedRectangle(cornerRadius: size.cornerRadius)
|
LazyImage(request: ImageRequest(url: account.avatar, processors: [.resize(size: config.size)])
|
||||||
.fill(.gray)
|
) { state in
|
||||||
.frame(width: size.size.width, height: size.size.height)
|
if let image = state.image {
|
||||||
|
image.resizable().scaledToFill()
|
||||||
|
.frame(width: config.width, height: config.height)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: config.cornerRadius))
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: config.cornerRadius)
|
||||||
|
.stroke(.primary.opacity(0.25), lineWidth: 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct AvatarPlaceHolder: View {
|
||||||
|
let config: AvatarView.FrameConfig
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
RoundedRectangle(cornerRadius: config.cornerRadius)
|
||||||
|
.fill(.gray)
|
||||||
|
.frame(width: config.width, height: config.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AccountPopupView: 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: account, 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)
|
||||||
|
}
|
||||||
|
@ -30,7 +30,7 @@ public struct ListEditView: View {
|
|||||||
} else {
|
} else {
|
||||||
ForEach(viewModel.accounts) { account in
|
ForEach(viewModel.accounts) { account in
|
||||||
HStack {
|
HStack {
|
||||||
AvatarView(url: account.avatar, size: .status)
|
AvatarView(account: account, config: .status)
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
EmojiTextApp(.init(stringValue: account.safeDisplayName),
|
EmojiTextApp(.init(stringValue: account.safeDisplayName),
|
||||||
emojis: account.emojis)
|
emojis: account.emojis)
|
||||||
|
@ -23,8 +23,8 @@ struct NotificationRowView: View {
|
|||||||
.accessibilityHidden(true)
|
.accessibilityHidden(true)
|
||||||
} else {
|
} else {
|
||||||
makeNotificationIconView(type: notification.type)
|
makeNotificationIconView(type: notification.type)
|
||||||
.frame(width: AvatarView.Size.status.size.width,
|
.frame(width: AvatarView.FrameConfig.status.width,
|
||||||
height: AvatarView.Size.status.size.height)
|
height: AvatarView.FrameConfig.status.height)
|
||||||
.accessibilityHidden(true)
|
.accessibilityHidden(true)
|
||||||
}
|
}
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
@ -52,7 +52,7 @@ struct NotificationRowView: View {
|
|||||||
|
|
||||||
private func makeAvatarView(type: Models.Notification.NotificationType) -> some View {
|
private func makeAvatarView(type: Models.Notification.NotificationType) -> some View {
|
||||||
ZStack(alignment: .topLeading) {
|
ZStack(alignment: .topLeading) {
|
||||||
AvatarView(url: notification.accounts[0].avatar)
|
AvatarView(account: notification.accounts[0], hasPopup: true)
|
||||||
makeNotificationIconView(type: type)
|
makeNotificationIconView(type: type)
|
||||||
.offset(x: -8, y: -8)
|
.offset(x: -8, y: -8)
|
||||||
}
|
}
|
||||||
@ -83,7 +83,7 @@ struct NotificationRowView: View {
|
|||||||
ScrollView(.horizontal, showsIndicators: false) {
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
LazyHStack(spacing: 8) {
|
LazyHStack(spacing: 8) {
|
||||||
ForEach(notification.accounts) { account in
|
ForEach(notification.accounts) { account in
|
||||||
AvatarView(url: account.avatar)
|
AvatarView(account: account, hasPopup: true)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
routerPath.navigate(to: .accountDetailWithAccount(account: account))
|
routerPath.navigate(to: .accountDetailWithAccount(account: account))
|
||||||
@ -91,7 +91,7 @@ struct NotificationRowView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.leading, 1)
|
.padding(.leading, 1)
|
||||||
.frame(height: AvatarView.Size.status.size.height + 2)
|
.frame(height: AvatarView.FrameConfig.status.size.height + 2)
|
||||||
}.offset(y: -1)
|
}.offset(y: -1)
|
||||||
}
|
}
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
|
@ -31,7 +31,7 @@ struct StatusEditorAutoCompleteView: View {
|
|||||||
viewModel.selectMentionSuggestion(account: account)
|
viewModel.selectMentionSuggestion(account: account)
|
||||||
} label: {
|
} label: {
|
||||||
HStack {
|
HStack {
|
||||||
AvatarView(url: account.avatar, size: .badge)
|
AvatarView(account: account, config: AvatarView.FrameConfig.badge)
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
EmojiTextApp(.init(stringValue: account.safeDisplayName),
|
EmojiTextApp(.init(stringValue: account.safeDisplayName),
|
||||||
emojis: account.emojis)
|
emojis: account.emojis)
|
||||||
|
@ -246,9 +246,9 @@ public struct StatusEditorView: View {
|
|||||||
if viewModel.mode.isInShareExtension {
|
if viewModel.mode.isInShareExtension {
|
||||||
AppAccountsSelectorView(routerPath: RouterPath(),
|
AppAccountsSelectorView(routerPath: RouterPath(),
|
||||||
accountCreationEnabled: false,
|
accountCreationEnabled: false,
|
||||||
avatarSize: .status)
|
avatarConfig: .status)
|
||||||
} else {
|
} else {
|
||||||
AvatarView(url: account.avatar, size: .status)
|
AvatarView(account: account, config: AvatarView.FrameConfig.status)
|
||||||
.environment(theme)
|
.environment(theme)
|
||||||
.accessibilityHidden(true)
|
.accessibilityHidden(true)
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@ public struct StatusEmbeddedView: View {
|
|||||||
|
|
||||||
private func makeAccountView(account: Account) -> some View {
|
private func makeAccountView(account: Account) -> some View {
|
||||||
HStack(alignment: .center) {
|
HStack(alignment: .center) {
|
||||||
AvatarView(url: account.avatar, size: .embed)
|
AvatarView(account: account, config: .embed, hasPopup: true)
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
EmojiTextApp(.init(stringValue: account.safeDisplayName), emojis: account.emojis)
|
EmojiTextApp(.init(stringValue: account.safeDisplayName), emojis: account.emojis)
|
||||||
.font(.scaledFootnote)
|
.font(.scaledFootnote)
|
||||||
|
@ -57,7 +57,7 @@ public struct StatusRowView: View {
|
|||||||
StatusRowReblogView(viewModel: viewModel)
|
StatusRowReblogView(viewModel: viewModel)
|
||||||
StatusRowReplyView(viewModel: viewModel)
|
StatusRowReplyView(viewModel: viewModel)
|
||||||
}
|
}
|
||||||
.padding(.leading, AvatarView.Size.status.size.width + .statusColumnsSpacing)
|
.padding(.leading, AvatarView.FrameConfig.status.width + .statusColumnsSpacing)
|
||||||
}
|
}
|
||||||
HStack(alignment: .top, spacing: .statusColumnsSpacing) {
|
HStack(alignment: .top, spacing: .statusColumnsSpacing) {
|
||||||
if !isCompact,
|
if !isCompact,
|
||||||
@ -66,7 +66,7 @@ public struct StatusRowView: View {
|
|||||||
Button {
|
Button {
|
||||||
viewModel.navigateToAccountDetail(account: viewModel.finalStatus.account)
|
viewModel.navigateToAccountDetail(account: viewModel.finalStatus.account)
|
||||||
} label: {
|
} label: {
|
||||||
AvatarView(url: viewModel.finalStatus.account.avatar, size: .status)
|
AvatarView(account: viewModel.finalStatus.account, config: .status, hasPopup: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
|
@ -103,7 +103,7 @@ struct StatusRowDetailView: View {
|
|||||||
ScrollView(.horizontal, showsIndicators: false) {
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
LazyHStack(spacing: 0) {
|
LazyHStack(spacing: 0) {
|
||||||
ForEach(accounts) { account in
|
ForEach(accounts) { account in
|
||||||
AvatarView(url: account.avatar, size: .list)
|
AvatarView(account: account, config: .list, hasPopup: true)
|
||||||
.padding(.leading, -4)
|
.padding(.leading, -4)
|
||||||
}
|
}
|
||||||
.transition(.opacity)
|
.transition(.opacity)
|
||||||
|
@ -42,7 +42,7 @@ struct StatusRowHeaderView: View {
|
|||||||
private var accountView: some View {
|
private var accountView: some View {
|
||||||
HStack(alignment: .center) {
|
HStack(alignment: .center) {
|
||||||
if theme.avatarPosition == .top {
|
if theme.avatarPosition == .top {
|
||||||
AvatarView(url: viewModel.finalStatus.account.avatar, size: .status)
|
AvatarView(account: viewModel.finalStatus.account, config: .status, hasPopup: true)
|
||||||
}
|
}
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
HStack(alignment: .firstTextBaseline, spacing: 2) {
|
HStack(alignment: .firstTextBaseline, spacing: 2) {
|
||||||
|
@ -31,7 +31,7 @@ public struct StatusRowMediaPreviewView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var appLayoutWidth: CGFloat {
|
var appLayoutWidth: CGFloat {
|
||||||
let avatarColumnWidth = theme.avatarPosition == .leading ? AvatarView.Size.status.size.width + .statusColumnsSpacing : 0
|
let avatarColumnWidth = theme.avatarPosition == .leading ? AvatarView.FrameConfig.status.width + .statusColumnsSpacing : 0
|
||||||
var sidebarWidth: CGFloat = 0
|
var sidebarWidth: CGFloat = 0
|
||||||
var secondaryColumnWidth: CGFloat = 0
|
var secondaryColumnWidth: CGFloat = 0
|
||||||
let layoutPading: CGFloat = .layoutPadding * 2
|
let layoutPading: CGFloat = .layoutPadding * 2
|
||||||
|
@ -8,7 +8,7 @@ struct StatusRowReblogView: View {
|
|||||||
if viewModel.status.reblog != nil {
|
if viewModel.status.reblog != nil {
|
||||||
HStack(spacing: 2) {
|
HStack(spacing: 2) {
|
||||||
Image("Rocket.Fill")
|
Image("Rocket.Fill")
|
||||||
AvatarView(url: viewModel.status.account.avatar, size: .boost)
|
AvatarView(account: viewModel.status.account, config: .boost, hasPopup: true)
|
||||||
EmojiTextApp(.init(stringValue: viewModel.status.account.safeDisplayName), emojis: viewModel.status.account.emojis)
|
EmojiTextApp(.init(stringValue: viewModel.status.account.safeDisplayName), emojis: viewModel.status.account.emojis)
|
||||||
Text("status.row.was-boosted")
|
Text("status.row.was-boosted")
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user