mirror of
https://github.com/yattee/yattee.git
synced 2026-10-23 05:02:53 +00:00
Let the login sheet respect its edges and the account footer tell the truth
MAC2/MAC15: the macOS login form is grouped like its Edit Source parent, so labels sit inside the sheet and the add-flow step stops clipping; footers wrap instead of truncating. MAC12: the account section footer consults account statuses before claiming the user is logged in.
This commit is contained in:
@@ -77,7 +77,7 @@ struct InstanceAccountsSection: View {
|
||||
} header: {
|
||||
Text(String(localized: "login.header.account"))
|
||||
} footer: {
|
||||
Text(String(localized: accounts.isEmpty ? "login.footer.loginBenefits" : "login.footer.loggedIn"))
|
||||
Text(footerText)
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .accountCredentialDidChange)) { _ in
|
||||
credentialRevision &+= 1
|
||||
@@ -216,6 +216,45 @@ struct InstanceAccountsSection: View {
|
||||
|
||||
// MARK: - Presentation
|
||||
|
||||
/// What the section says under the rows.
|
||||
///
|
||||
/// Read from the statuses, not from the count: this is the screen the user
|
||||
/// comes to in order to fix a login the server turned down, and a footer
|
||||
/// that only counts accounts tells them, right there, that they are logged
|
||||
/// in. The worst state on the list is the one that gets named — a rejected
|
||||
/// credential is something to act on, and it outranks a server that
|
||||
/// happens to be down.
|
||||
private var footerText: String {
|
||||
guard !accounts.isEmpty else {
|
||||
return String(localized: "login.footer.loginBenefits")
|
||||
}
|
||||
|
||||
// `checking` counts as healthy: it is the absence of an answer, and a
|
||||
// footer that reacted to it would blink a warning at every launch.
|
||||
let unhealthy = accounts.map { status(for: $0) }
|
||||
.filter { $0 != .signedIn && $0 != .checking }
|
||||
|
||||
guard !unhealthy.isEmpty else {
|
||||
return String(localized: "login.footer.loggedIn")
|
||||
}
|
||||
|
||||
if unhealthy.contains(.credentialRejected) {
|
||||
return String(localized: "login.footer.credentialRejected")
|
||||
}
|
||||
|
||||
if unhealthy.contains(.unreachable) {
|
||||
return String(localized: "login.footer.unreachable")
|
||||
}
|
||||
|
||||
if unhealthy.contains(.credentialMissing) {
|
||||
return String(localized: "login.footer.credentialMissing")
|
||||
}
|
||||
|
||||
// A state named after this was written. Saying only that something is
|
||||
// wrong is worth little, but it is true, and the rows above say what.
|
||||
return String(localized: "login.footer.accountUnusable")
|
||||
}
|
||||
|
||||
private func displayName(for account: Account) -> String {
|
||||
appEnvironment?.dataContextValidator.displayName(for: account) ?? instance.displayName
|
||||
}
|
||||
|
||||
@@ -73,7 +73,12 @@ struct InstanceLoginView: View {
|
||||
.accessibilityIdentifier("instance.login.view")
|
||||
}
|
||||
#if os(macOS)
|
||||
.frame(minWidth: 450, minHeight: 350)
|
||||
// Sized to what the form actually holds: two fields and a button leave
|
||||
// most of a taller sheet empty, and the optional step carries two more
|
||||
// rows than the plain one. The width is what the labels have to fit
|
||||
// in, which matters most where this is pushed on top of the add flow's
|
||||
// own sheet.
|
||||
.frame(minWidth: 480, minHeight: isOptionalStep ? 400 : 300)
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
@@ -84,6 +89,19 @@ struct InstanceLoginView: View {
|
||||
#if os(tvOS)
|
||||
TVSettingsTextField(title: usernameFieldLabel, text: $username)
|
||||
TVSettingsTextField(title: String(localized: "login.password"), text: $password, isSecure: true)
|
||||
#elseif os(macOS)
|
||||
LabeledContent(usernameFieldLabel) {
|
||||
TextField("", text: $username)
|
||||
.textContentType(.username)
|
||||
.autocorrectionDisabled()
|
||||
.accessibilityIdentifier("instance.login.usernameField")
|
||||
}
|
||||
|
||||
LabeledContent(String(localized: "login.password")) {
|
||||
SecureField("", text: $password)
|
||||
.textContentType(.password)
|
||||
.accessibilityIdentifier("instance.login.passwordField")
|
||||
}
|
||||
#else
|
||||
TextField(usernameFieldLabel, text: $username)
|
||||
.textContentType(.username)
|
||||
@@ -100,7 +118,7 @@ struct InstanceLoginView: View {
|
||||
} header: {
|
||||
Text(String(localized: "login.header.credentials"))
|
||||
} footer: {
|
||||
Text(footerText)
|
||||
wrappingFooter(footerText)
|
||||
}
|
||||
|
||||
if let error = errorMessage {
|
||||
@@ -144,13 +162,17 @@ struct InstanceLoginView: View {
|
||||
.buttonStyle(TVSettingsButtonStyle())
|
||||
#endif
|
||||
} footer: {
|
||||
Text(String(localized: "login.footer.optionalStep"))
|
||||
wrappingFooter(String(localized: "login.footer.optionalStep"))
|
||||
}
|
||||
}
|
||||
}
|
||||
#if os(iOS)
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
#endif
|
||||
#if os(macOS)
|
||||
.formStyle(.grouped)
|
||||
.opaqueSettingsFormBackground()
|
||||
#endif
|
||||
.onAppear {
|
||||
// Only ever fills an untouched field: the sheet's body runs again
|
||||
// for every keystroke, and a prefill that reasserted itself would
|
||||
@@ -163,6 +185,17 @@ struct InstanceLoginView: View {
|
||||
|
||||
// MARK: - Computed Properties
|
||||
|
||||
/// A section footer that takes as many lines as it needs.
|
||||
///
|
||||
/// The sentences here explain what the form is for and that skipping it is
|
||||
/// safe, and the sheet is narrow — narrower still when it is pushed on top
|
||||
/// of the add flow's sheet. Left to lay itself out, macOS keeps the footer
|
||||
/// on one line and cuts the end of it off.
|
||||
private func wrappingFooter(_ text: String) -> some View {
|
||||
Text(text)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
/// What the first field is called on this backend.
|
||||
///
|
||||
/// Named per backend rather than genericised: every backend that has
|
||||
|
||||
Reference in New Issue
Block a user