mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-11-26 10:11:00 +00:00
All new accounts selector
This commit is contained in:
parent
5c69aa64bc
commit
15b704c97a
5 changed files with 98 additions and 120 deletions
|
@ -328,6 +328,12 @@ public struct AccountDetailView: View {
|
||||||
} label: {
|
} label: {
|
||||||
Label("settings.push.navigation-title", systemImage: "bell")
|
Label("settings.push.navigation-title", systemImage: "bell")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
routerPath.presentedSheet = .settings
|
||||||
|
} label: {
|
||||||
|
Label("settings.title", systemImage: "gear")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "ellipsis.circle")
|
Image(systemName: "ellipsis.circle")
|
||||||
|
|
|
@ -5,7 +5,9 @@ import SwiftUI
|
||||||
|
|
||||||
public struct AppAccountView: View {
|
public struct AppAccountView: View {
|
||||||
@EnvironmentObject private var routerPath: RouterPath
|
@EnvironmentObject private var routerPath: RouterPath
|
||||||
@EnvironmentObject var appAccounts: AppAccountsManager
|
@EnvironmentObject private var appAccounts: AppAccountsManager
|
||||||
|
@EnvironmentObject private var preferences: UserPreferences
|
||||||
|
|
||||||
@StateObject var viewModel: AppAccountViewModel
|
@StateObject var viewModel: AppAccountViewModel
|
||||||
|
|
||||||
public init(viewModel: AppAccountViewModel) {
|
public init(viewModel: AppAccountViewModel) {
|
||||||
|
@ -47,6 +49,19 @@ public struct AppAccountView: View {
|
||||||
Image(systemName: "checkmark.circle.fill")
|
Image(systemName: "checkmark.circle.fill")
|
||||||
.foregroundStyle(.white, .green)
|
.foregroundStyle(.white, .green)
|
||||||
.offset(x: 5, y: -5)
|
.offset(x: 5, y: -5)
|
||||||
|
} else if viewModel.showBadge,
|
||||||
|
let token = viewModel.appAccount.oauthToken,
|
||||||
|
let notificationsCount = preferences.getNotificationsCount(for: token),
|
||||||
|
notificationsCount > 0{
|
||||||
|
ZStack {
|
||||||
|
Circle()
|
||||||
|
.fill(.red)
|
||||||
|
Text(notificationsCount > 99 ? "99+" : String(notificationsCount))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.font(.system(size: 9))
|
||||||
|
}
|
||||||
|
.frame(width: 20, height: 20)
|
||||||
|
.offset(x: 5, y: -5)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -66,9 +81,11 @@ public struct AppAccountView: View {
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer()
|
if viewModel.isInNavigation {
|
||||||
Image(systemName: "chevron.right")
|
Spacer()
|
||||||
.foregroundColor(.gray)
|
Image(systemName: "chevron.right")
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
|
|
|
@ -12,6 +12,8 @@ public class AppAccountViewModel: ObservableObject {
|
||||||
var appAccount: AppAccount
|
var appAccount: AppAccount
|
||||||
let client: Client
|
let client: Client
|
||||||
let isCompact: Bool
|
let isCompact: Bool
|
||||||
|
let isInNavigation: Bool
|
||||||
|
let showBadge: Bool
|
||||||
|
|
||||||
@Published var account: Account? {
|
@Published var account: Account? {
|
||||||
didSet {
|
didSet {
|
||||||
|
@ -21,8 +23,6 @@ public class AppAccountViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Published var roundedAvatar: UIImage?
|
|
||||||
|
|
||||||
var acct: String {
|
var acct: String {
|
||||||
if let acct = appAccount.accountName {
|
if let acct = appAccount.accountName {
|
||||||
return acct
|
return acct
|
||||||
|
@ -31,24 +31,20 @@ public class AppAccountViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(appAccount: AppAccount, isCompact: Bool = false) {
|
public init(appAccount: AppAccount, isCompact: Bool = false, isInNavigation: Bool = true, showBadge: Bool = false) {
|
||||||
self.appAccount = appAccount
|
self.appAccount = appAccount
|
||||||
self.isCompact = isCompact
|
self.isCompact = isCompact
|
||||||
|
self.isInNavigation = isInNavigation
|
||||||
|
self.showBadge = showBadge
|
||||||
client = .init(server: appAccount.server, oauthToken: appAccount.oauthToken)
|
client = .init(server: appAccount.server, oauthToken: appAccount.oauthToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchAccount() async {
|
func fetchAccount() async {
|
||||||
do {
|
do {
|
||||||
account = Self.accountsCache[appAccount.id]
|
account = Self.accountsCache[appAccount.id]
|
||||||
roundedAvatar = Self.avatarsCache[appAccount.id]
|
|
||||||
|
|
||||||
account = try await client.get(endpoint: Accounts.verifyCredentials)
|
account = try await client.get(endpoint: Accounts.verifyCredentials)
|
||||||
Self.accountsCache[appAccount.id] = account
|
Self.accountsCache[appAccount.id] = account
|
||||||
|
|
||||||
if let account {
|
|
||||||
await refreshAvatar(account: account)
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,18 +56,4 @@ public class AppAccountViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func refreshAvatar(account: Account) async {
|
|
||||||
// Warning: Non-sendable type '(any URLSessionTaskDelegate)?' exiting main actor-isolated
|
|
||||||
// context in call to non-isolated instance method 'data(for:delegate:)' cannot cross actor
|
|
||||||
// boundary.
|
|
||||||
// This is on the defaulted-to-nil second parameter of `.data(from:delegate:)`.
|
|
||||||
// There is a Radar tracking this & others like it.
|
|
||||||
if let (data, _) = try? await URLSession.shared.data(from: account.avatar),
|
|
||||||
let image = UIImage(data: data)?.roundedImage
|
|
||||||
{
|
|
||||||
roundedAvatar = image
|
|
||||||
Self.avatarsCache[account.id] = image
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,10 +6,12 @@ public struct AppAccountsSelectorView: View {
|
||||||
@EnvironmentObject private var preferences: UserPreferences
|
@EnvironmentObject private var preferences: UserPreferences
|
||||||
@EnvironmentObject private var currentAccount: CurrentAccount
|
@EnvironmentObject private var currentAccount: CurrentAccount
|
||||||
@EnvironmentObject private var appAccounts: AppAccountsManager
|
@EnvironmentObject private var appAccounts: AppAccountsManager
|
||||||
|
@EnvironmentObject private var theme: Theme
|
||||||
|
|
||||||
@ObservedObject var routerPath: RouterPath
|
@ObservedObject var routerPath: RouterPath
|
||||||
|
|
||||||
@State private var accountsViewModel: [AppAccountViewModel] = []
|
@State private var accountsViewModel: [AppAccountViewModel] = []
|
||||||
|
@State private var isPresented: Bool = false
|
||||||
|
|
||||||
private let accountCreationEnabled: Bool
|
private let accountCreationEnabled: Bool
|
||||||
private let avatarSize: AvatarView.Size
|
private let avatarSize: AvatarView.Size
|
||||||
|
@ -32,26 +34,18 @@ public struct AppAccountsSelectorView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
Group {
|
Button {
|
||||||
if UIDevice.current.userInterfaceIdiom == .pad {
|
isPresented.toggle()
|
||||||
labelView
|
|
||||||
.contextMenu {
|
|
||||||
menuView
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Menu {
|
|
||||||
menuView
|
|
||||||
} label: {
|
|
||||||
labelView
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onTapGesture {
|
|
||||||
HapticManager.shared.fireHaptic(of: .buttonPress)
|
HapticManager.shared.fireHaptic(of: .buttonPress)
|
||||||
|
} label: {
|
||||||
|
labelView
|
||||||
}
|
}
|
||||||
.onAppear {
|
.sheet(isPresented: $isPresented, content: {
|
||||||
refreshAccounts()
|
accountsView.presentationDetents([.medium])
|
||||||
}
|
.onAppear {
|
||||||
|
refreshAccounts()
|
||||||
|
}
|
||||||
|
})
|
||||||
.onChange(of: currentAccount.account?.id) { _ in
|
.onChange(of: currentAccount.account?.id) { _ in
|
||||||
refreshAccounts()
|
refreshAccounts()
|
||||||
}
|
}
|
||||||
|
@ -67,7 +61,7 @@ public struct AppAccountsSelectorView: View {
|
||||||
.redacted(reason: .placeholder)
|
.redacted(reason: .placeholder)
|
||||||
}
|
}
|
||||||
}.overlay(alignment: .topTrailing) {
|
}.overlay(alignment: .topTrailing) {
|
||||||
if !currentAccount.followRequests.isEmpty || showNotificationBadge {
|
if (!currentAccount.followRequests.isEmpty || showNotificationBadge) && accountCreationEnabled {
|
||||||
Circle()
|
Circle()
|
||||||
.fill(Color.red)
|
.fill(Color.red)
|
||||||
.frame(width: 9, height: 9)
|
.frame(width: 9, height: 9)
|
||||||
|
@ -76,77 +70,71 @@ public struct AppAccountsSelectorView: View {
|
||||||
.accessibilityLabel("accessibility.app-account.selector.accounts")
|
.accessibilityLabel("accessibility.app-account.selector.accounts")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
private var accountsView: some View {
|
||||||
private var menuView: some View {
|
NavigationStack {
|
||||||
ForEach(accountsViewModel.sorted { $0.acct < $1.acct }, id: \.appAccount.id) { viewModel in
|
List {
|
||||||
Section(viewModel.acct) {
|
Section {
|
||||||
Button {
|
ForEach(accountsViewModel.sorted { $0.acct < $1.acct }, id: \.appAccount.id) { viewModel in
|
||||||
if let account = currentAccount.account,
|
AppAccountView(viewModel: viewModel)
|
||||||
viewModel.account?.id == account.id
|
|
||||||
{
|
|
||||||
routerPath.navigate(to: .accountDetailWithAccount(account: account))
|
|
||||||
} else {
|
|
||||||
var transation = Transaction()
|
|
||||||
transation.disablesAnimations = true
|
|
||||||
withTransaction(transation) {
|
|
||||||
appAccounts.currentAccount = viewModel.appAccount
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
HapticManager.shared.fireHaptic(of: .buttonPress)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
} label: {
|
|
||||||
HStack {
|
if accountCreationEnabled {
|
||||||
if let image = viewModel.roundedAvatar {
|
Section {
|
||||||
Image(uiImage: image)
|
Button {
|
||||||
}
|
isPresented = false
|
||||||
|
HapticManager.shared.fireHaptic(of: .buttonPress)
|
||||||
let name = viewModel.account.flatMap { account in
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
||||||
account.displayName?.isEmpty != false ? "@\(account.acct)" : account.displayName
|
routerPath.presentedSheet = .addAccount
|
||||||
} ?? ""
|
}
|
||||||
if let token = viewModel.appAccount.oauthToken,
|
} label: {
|
||||||
preferences.getNotificationsCount(for: token) > 0
|
Label("app-account.button.add", systemImage: "person.badge.plus")
|
||||||
{
|
|
||||||
Text("\(name) (\(preferences.getNotificationsCount(for: token)))")
|
|
||||||
} else {
|
|
||||||
Text("\(name)")
|
|
||||||
}
|
}
|
||||||
|
settingsButton
|
||||||
|
}
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.listStyle(.insetGrouped)
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
.background(theme.secondaryBackgroundColor)
|
||||||
|
.navigationTitle("settings.section.accounts")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
|
Button {
|
||||||
|
isPresented.toggle()
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "xmark.circle")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if accountCreationEnabled {
|
||||||
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
|
settingsButton
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if accountCreationEnabled {
|
}
|
||||||
Divider()
|
|
||||||
Button {
|
private var settingsButton: some View {
|
||||||
HapticManager.shared.fireHaptic(of: .buttonPress)
|
Button {
|
||||||
routerPath.presentedSheet = .addAccount
|
isPresented = false
|
||||||
} label: {
|
HapticManager.shared.fireHaptic(of: .buttonPress)
|
||||||
Label("app-account.button.add", systemImage: "person.badge.plus")
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if UIDevice.current.userInterfaceIdiom == .phone && accountCreationEnabled {
|
|
||||||
Divider()
|
|
||||||
Button {
|
|
||||||
HapticManager.shared.fireHaptic(of: .buttonPress)
|
|
||||||
routerPath.presentedSheet = .settings
|
routerPath.presentedSheet = .settings
|
||||||
} label: {
|
|
||||||
Label("tab.settings", systemImage: "gear")
|
|
||||||
}
|
}
|
||||||
|
} label: {
|
||||||
|
Label("tab.settings", systemImage: "gear")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func refreshAccounts() {
|
private func refreshAccounts() {
|
||||||
if accountsViewModel.isEmpty || appAccounts.availableAccounts.count != accountsViewModel.count {
|
accountsViewModel = []
|
||||||
accountsViewModel = []
|
for account in appAccounts.availableAccounts {
|
||||||
for account in appAccounts.availableAccounts {
|
let viewModel: AppAccountViewModel = .init(appAccount: account, isInNavigation: false, showBadge: true)
|
||||||
let viewModel: AppAccountViewModel = .init(appAccount: account)
|
accountsViewModel.append(viewModel)
|
||||||
Task {
|
|
||||||
await viewModel.fetchAccount()
|
|
||||||
if !accountsViewModel.contains(where: { $0.acct == viewModel.acct }) {
|
|
||||||
accountsViewModel.append(viewModel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
import UIKit
|
|
||||||
|
|
||||||
public extension UIImage {
|
|
||||||
var roundedImage: UIImage? {
|
|
||||||
let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: size)
|
|
||||||
UIGraphicsBeginImageContextWithOptions(size, false, 1)
|
|
||||||
defer { UIGraphicsEndImageContext() }
|
|
||||||
UIBezierPath(
|
|
||||||
roundedRect: rect,
|
|
||||||
cornerRadius: size.height
|
|
||||||
).addClip()
|
|
||||||
draw(in: rect)
|
|
||||||
return UIGraphicsGetImageFromCurrentImageContext()
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue