mirror of
https://github.com/yattee/yattee.git
synced 2026-10-23 21:22:53 +00:00
Fold the tvOS account actions into a press-raised dialog
Each account was four focusable rows tall on tvOS — Use this account, Sign in again, Rename and Sign out all spelled out as siblings, a lot of clicking past on a remote. The row itself is now the one focusable unit: pressing it raises the same actions as a confirmation dialog, mirroring the context menu the other platforms already attach.
This commit is contained in:
@@ -38,6 +38,12 @@ struct AccountsListView: View {
|
||||
@State private var pendingRemoval: PendingAccountRemoval?
|
||||
@State private var renameRequest: AccountRenameRequest?
|
||||
|
||||
#if os(tvOS)
|
||||
/// The account whose actions dialog is up. tvOS only: everywhere else the
|
||||
/// row grows a context menu instead of a press target.
|
||||
@State private var actionsAccountID: UUID?
|
||||
#endif
|
||||
|
||||
/// Bumped whenever a credential is replaced, purely to re-read the login
|
||||
/// identifier the rows display.
|
||||
///
|
||||
@@ -255,6 +261,38 @@ struct AccountsListView: View {
|
||||
|
||||
@ViewBuilder
|
||||
private func accountRow(_ account: Account, instance: Instance?) -> some View {
|
||||
#if os(tvOS)
|
||||
// One focusable row per account: pressing it raises the actions as a
|
||||
// dialog. Spelling them out as sibling rows made every account four
|
||||
// rows tall, which is a lot of clicking past on a remote.
|
||||
Button {
|
||||
actionsAccountID = account.id
|
||||
} label: {
|
||||
rowContent(account, instance: instance)
|
||||
}
|
||||
.buttonStyle(TVSettingsButtonStyle())
|
||||
.confirmationDialog(
|
||||
title(for: account, instance: instance),
|
||||
isPresented: Binding(
|
||||
// By ID: a label edit arriving from another device while the
|
||||
// dialog is up would otherwise close it.
|
||||
get: { actionsAccountID == account.id },
|
||||
set: { if !$0 { actionsAccountID = nil } }
|
||||
),
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
rowActions(account, instance: instance)
|
||||
Button(String(localized: "common.cancel"), role: .cancel) {}
|
||||
}
|
||||
#else
|
||||
rowContent(account, instance: instance)
|
||||
.contextMenu {
|
||||
rowActions(account, instance: instance)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private func rowContent(_ account: Account, instance: Instance?) -> some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack(spacing: 6) {
|
||||
// The same checkmark the switcher marks the selected context
|
||||
@@ -289,17 +327,6 @@ struct AccountsListView: View {
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
#if !os(tvOS)
|
||||
.contextMenu {
|
||||
rowActions(account, instance: instance)
|
||||
}
|
||||
#endif
|
||||
|
||||
#if os(tvOS)
|
||||
// A context menu is not reachable on tvOS, and a Form row is a single
|
||||
// focusable unit — so the actions are rows of their own.
|
||||
rowActions(account, instance: instance)
|
||||
#endif
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
@@ -311,9 +338,6 @@ struct AccountsListView: View {
|
||||
} label: {
|
||||
Label(String(localized: "accounts.useThisAccount"), systemImage: "checkmark.circle")
|
||||
}
|
||||
#if os(tvOS)
|
||||
.buttonStyle(TVSettingsButtonStyle())
|
||||
#endif
|
||||
}
|
||||
|
||||
Button {
|
||||
@@ -325,9 +349,6 @@ struct AccountsListView: View {
|
||||
} label: {
|
||||
Label(String(localized: "login.signInAgain"), systemImage: "person.badge.key")
|
||||
}
|
||||
#if os(tvOS)
|
||||
.buttonStyle(TVSettingsButtonStyle())
|
||||
#endif
|
||||
}
|
||||
|
||||
// Offered on orphans too: with no source to fall back to, a label of
|
||||
@@ -337,9 +358,6 @@ struct AccountsListView: View {
|
||||
} label: {
|
||||
Label(String(localized: "accounts.rename"), systemImage: "pencil")
|
||||
}
|
||||
#if os(tvOS)
|
||||
.buttonStyle(TVSettingsButtonStyle())
|
||||
#endif
|
||||
|
||||
Button(role: .destructive) {
|
||||
pendingRemoval = PendingAccountRemoval(
|
||||
@@ -353,9 +371,6 @@ struct AccountsListView: View {
|
||||
systemImage: instance == nil ? "trash" : "rectangle.portrait.and.arrow.forward"
|
||||
)
|
||||
}
|
||||
#if os(tvOS)
|
||||
.buttonStyle(TVSettingsButtonStyle())
|
||||
#endif
|
||||
}
|
||||
|
||||
private func loginSheet(_ login: AccountsListLogin) -> some View {
|
||||
|
||||
@@ -49,6 +49,12 @@ struct InstanceAccountsSection: View {
|
||||
/// the section rather than the row, and matched back to a row by ID.
|
||||
@State private var renameRequest: AccountRenameRequest?
|
||||
|
||||
#if os(tvOS)
|
||||
/// The account whose actions dialog is up. tvOS only: everywhere else the
|
||||
/// row grows a context menu instead of a press target.
|
||||
@State private var actionsAccountID: UUID?
|
||||
#endif
|
||||
|
||||
private var accounts: [Account] {
|
||||
appEnvironment?.accountsManager.accounts(forInstance: instance.id) ?? []
|
||||
}
|
||||
@@ -88,6 +94,76 @@ struct InstanceAccountsSection: View {
|
||||
|
||||
@ViewBuilder
|
||||
private func accountRow(_ account: Account) -> some View {
|
||||
rowBase(account)
|
||||
// Attached to the row that raised it rather than to the section: a
|
||||
// presentation modifier on a `Section` inside a `Form` has no view of
|
||||
// its own to present from.
|
||||
.confirmationDialog(
|
||||
String(localized: "accounts.remove.confirm.title \(displayName(for: account))"),
|
||||
isPresented: Binding(
|
||||
// By ID: a label edit arriving from another device while the
|
||||
// dialog is up would otherwise close it.
|
||||
get: { pendingSignOut?.id == account.id },
|
||||
set: { if !$0 { pendingSignOut = nil } }
|
||||
),
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
Button(String(localized: "common.remove"), role: .destructive) {
|
||||
confirmSignOut()
|
||||
}
|
||||
Button(String(localized: "common.cancel"), role: .cancel) {
|
||||
pendingSignOut = nil
|
||||
}
|
||||
} message: {
|
||||
Text(String(localized: "accounts.remove.confirm.message"))
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.presentationCompactAdaptation(.sheet)
|
||||
#endif
|
||||
// Attached to the row for the same reason the dialog above it is, and
|
||||
// narrowed to the row that raised it so only one alert is ever live.
|
||||
.accountRenameAlert(
|
||||
Binding(
|
||||
get: { renameRequest?.id == account.id ? renameRequest : nil },
|
||||
set: { if $0 == nil { renameRequest = nil } }
|
||||
)
|
||||
) { accountID, name in
|
||||
appEnvironment?.accountsManager.updateDisplayName(name, for: accountID)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func rowBase(_ account: Account) -> some View {
|
||||
#if os(tvOS)
|
||||
// One focusable row per account: pressing it raises the actions as a
|
||||
// dialog. Spelling them out as sibling rows made every account four
|
||||
// rows tall, which is a lot of clicking past on a remote.
|
||||
Button {
|
||||
actionsAccountID = account.id
|
||||
} label: {
|
||||
rowContent(account)
|
||||
}
|
||||
.buttonStyle(TVSettingsButtonStyle())
|
||||
.confirmationDialog(
|
||||
displayName(for: account),
|
||||
isPresented: Binding(
|
||||
get: { actionsAccountID == account.id },
|
||||
set: { if !$0 { actionsAccountID = nil } }
|
||||
),
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
rowActions(account)
|
||||
Button(String(localized: "common.cancel"), role: .cancel) {}
|
||||
}
|
||||
#else
|
||||
rowContent(account)
|
||||
.contextMenu {
|
||||
rowActions(account)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private func rowContent(_ account: Account) -> some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack(spacing: 6) {
|
||||
// The same checkmark the switcher marks the selected context
|
||||
@@ -107,54 +183,6 @@ struct InstanceAccountsSection: View {
|
||||
AccountStatusBadge(status: status(for: account))
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
#if !os(tvOS)
|
||||
.contextMenu {
|
||||
rowActions(account)
|
||||
}
|
||||
#endif
|
||||
// Attached to the row that raised it rather than to the section: a
|
||||
// presentation modifier on a `Section` inside a `Form` has no view of
|
||||
// its own to present from.
|
||||
.confirmationDialog(
|
||||
String(localized: "accounts.remove.confirm.title \(displayName(for: account))"),
|
||||
isPresented: Binding(
|
||||
// By ID: a label edit arriving from another device while the
|
||||
// dialog is up would otherwise close it.
|
||||
get: { pendingSignOut?.id == account.id },
|
||||
set: { if !$0 { pendingSignOut = nil } }
|
||||
),
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
Button(String(localized: "common.remove"), role: .destructive) {
|
||||
confirmSignOut()
|
||||
}
|
||||
Button(String(localized: "common.cancel"), role: .cancel) {
|
||||
pendingSignOut = nil
|
||||
}
|
||||
} message: {
|
||||
Text(String(localized: "accounts.remove.confirm.message"))
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.presentationCompactAdaptation(.sheet)
|
||||
#endif
|
||||
// Attached to the row for the same reason the dialog above it is, and
|
||||
// narrowed to the row that raised it so only one alert is ever live.
|
||||
.accountRenameAlert(
|
||||
Binding(
|
||||
get: { renameRequest?.id == account.id ? renameRequest : nil },
|
||||
set: { if $0 == nil { renameRequest = nil } }
|
||||
)
|
||||
) { accountID, name in
|
||||
appEnvironment?.accountsManager.updateDisplayName(name, for: accountID)
|
||||
}
|
||||
|
||||
#if os(tvOS)
|
||||
// A context menu is not reachable on tvOS, and a Form row is a single
|
||||
// focusable unit — so the actions are rows of their own. Without them
|
||||
// the whole account is unreachable here: directional focus walks
|
||||
// straight past a row that is only text.
|
||||
rowActions(account)
|
||||
#endif
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
@@ -167,9 +195,6 @@ struct InstanceAccountsSection: View {
|
||||
} label: {
|
||||
Label(String(localized: "accounts.useThisAccount"), systemImage: "checkmark.circle")
|
||||
}
|
||||
#if os(tvOS)
|
||||
.buttonStyle(TVSettingsButtonStyle())
|
||||
#endif
|
||||
}
|
||||
|
||||
// tvOS never syncs app Keychain items, so an account that arrived from
|
||||
@@ -191,27 +216,18 @@ struct InstanceAccountsSection: View {
|
||||
Label(String(localized: "login.signInAgain"), systemImage: "person.badge.key")
|
||||
#endif
|
||||
}
|
||||
#if os(tvOS)
|
||||
.buttonStyle(TVSettingsButtonStyle())
|
||||
#endif
|
||||
|
||||
Button {
|
||||
renameRequest = makeRenameRequest(for: account)
|
||||
} label: {
|
||||
Label(String(localized: "accounts.rename"), systemImage: "pencil")
|
||||
}
|
||||
#if os(tvOS)
|
||||
.buttonStyle(TVSettingsButtonStyle())
|
||||
#endif
|
||||
|
||||
Button(role: .destructive) {
|
||||
pendingSignOut = account
|
||||
} label: {
|
||||
Label(String(localized: "login.signOutAndRemove"), systemImage: "rectangle.portrait.and.arrow.forward")
|
||||
}
|
||||
#if os(tvOS)
|
||||
.buttonStyle(TVSettingsButtonStyle())
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: - Presentation
|
||||
|
||||
Reference in New Issue
Block a user