mirror of
https://github.com/yattee/yattee.git
synced 2026-07-20 14:22:02 +00:00
On tvOS a Form row is one focusable unit, so the legacy account/source cards - header, credential fields, and both buttons stacked in a single row - focused as a whole and every click fired the first button (Remove), making credentials and Import unreachable. - Split the tvOS rows into individually focusable rows (info header, fields, Import, Remove), keeping the card layout on iOS/macOS - Wrap the tvOS entry points in TVSidebarDetailContainer and drop the tvOS navigationTitle that rendered as a ghost behind the form - Present the review screen from the first-launch alert as a fullScreenCover on tvOS: sheets render as a small transparent card that cannot fit the sidebar layout (Menu button dismisses the cover) - Dim disabled TVSettingsButtonStyle buttons - Widen the row icon frame for tvOS glyph sizes and skip the caption when it would repeat the host shown as the title
180 lines
5.5 KiB
Swift
180 lines
5.5 KiB
Swift
//
|
|
// TVSettingsButtonStyles.swift
|
|
// Yattee
|
|
//
|
|
// Button styles for tvOS settings forms - removes native glow effect.
|
|
//
|
|
|
|
#if os(tvOS)
|
|
import SwiftUI
|
|
|
|
// MARK: - Custom Picker for tvOS
|
|
|
|
/// Custom picker for tvOS that avoids the native focus glow effect
|
|
struct TVSettingsPicker<SelectionValue: Hashable, Content: View>: View {
|
|
let title: String
|
|
@Binding var selection: SelectionValue
|
|
let content: () -> Content
|
|
|
|
@State private var isExpanded = false
|
|
|
|
var body: some View {
|
|
Button {
|
|
isExpanded.toggle()
|
|
} label: {
|
|
HStack {
|
|
Text(title)
|
|
Spacer()
|
|
Image(systemName: isExpanded ? "chevron.up" : "chevron.down")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
.buttonStyle(TVFormRowButtonStyle())
|
|
|
|
if isExpanded {
|
|
content()
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Custom TextField for tvOS
|
|
|
|
/// Custom text field for tvOS that avoids the native focus glow effect
|
|
/// Uses a Button that shows an alert for text input
|
|
struct TVSettingsTextField: View {
|
|
let title: String
|
|
@Binding var text: String
|
|
var isSecure: Bool = false
|
|
|
|
@State private var isEditing = false
|
|
@State private var editText = ""
|
|
|
|
var body: some View {
|
|
Button {
|
|
editText = text
|
|
isEditing = true
|
|
} label: {
|
|
HStack {
|
|
if text.isEmpty {
|
|
Text(title)
|
|
.foregroundStyle(.secondary)
|
|
} else if isSecure {
|
|
Text(String(repeating: "•", count: min(text.count, 12)))
|
|
.foregroundStyle(.primary)
|
|
} else {
|
|
Text(text)
|
|
.foregroundStyle(.primary)
|
|
}
|
|
Spacer()
|
|
}
|
|
}
|
|
.buttonStyle(TVFormRowButtonStyle())
|
|
.alert(title, isPresented: $isEditing) {
|
|
if isSecure {
|
|
SecureField(title, text: $editText)
|
|
} else {
|
|
TextField(title, text: $editText)
|
|
}
|
|
Button("common.ok") {
|
|
text = editText
|
|
}
|
|
Button("common.cancel", role: .cancel) { }
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Button Styles
|
|
|
|
/// Button style for settings forms - subtle focus effect without glow
|
|
struct TVSettingsButtonStyle: ButtonStyle {
|
|
@Environment(\.isFocused) private var isFocused
|
|
@Environment(\.isEnabled) private var isEnabled
|
|
|
|
func makeBody(configuration: Configuration) -> some View {
|
|
configuration.label
|
|
.opacity(isEnabled ? 1 : 0.4)
|
|
.padding(.horizontal, 20)
|
|
.padding(.vertical, 12)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 10)
|
|
.fill(isFocused ? .white.opacity(0.2) : .clear)
|
|
)
|
|
.scaleEffect(configuration.isPressed ? 0.97 : (isFocused ? 1.02 : 1.0))
|
|
.animation(.easeInOut(duration: 0.15), value: isFocused)
|
|
.animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
|
|
}
|
|
}
|
|
|
|
/// Button style for toolbar Done/Cancel buttons - pill shaped like tvOS standard
|
|
struct TVToolbarButtonStyle: ButtonStyle {
|
|
@Environment(\.isFocused) private var isFocused
|
|
|
|
func makeBody(configuration: Configuration) -> some View {
|
|
configuration.label
|
|
.font(.callout)
|
|
.fontWeight(.medium)
|
|
.foregroundStyle(isFocused ? .white : .secondary)
|
|
.padding(.horizontal, 24)
|
|
.padding(.vertical, 12)
|
|
.background(
|
|
Capsule()
|
|
.fill(isFocused ? Color.white.opacity(0.3) : Color.white.opacity(0.1))
|
|
)
|
|
.scaleEffect(configuration.isPressed ? 0.95 : (isFocused ? 1.05 : 1.0))
|
|
.animation(.easeInOut(duration: 0.15), value: isFocused)
|
|
.animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
|
|
}
|
|
}
|
|
|
|
/// Custom toggle view for tvOS that replaces native Toggle to avoid glow effect
|
|
struct TVSettingsToggle: View {
|
|
let title: String
|
|
@Binding var isOn: Bool
|
|
|
|
var body: some View {
|
|
Button {
|
|
isOn.toggle()
|
|
} label: {
|
|
HStack {
|
|
Text(title)
|
|
Spacer()
|
|
Text(isOn ? "common.on" : "common.off")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
.buttonStyle(TVFormRowButtonStyle())
|
|
}
|
|
}
|
|
|
|
/// Label style for source rows - explicit icon/text spacing
|
|
struct TVSourceRowLabelStyle: LabelStyle {
|
|
func makeBody(configuration: Configuration) -> some View {
|
|
HStack(spacing: 20) {
|
|
configuration.icon
|
|
.frame(width: 36, height: 36)
|
|
configuration.title
|
|
Spacer(minLength: 0)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Button style for form rows (toggles, pickers) - matches form appearance
|
|
struct TVFormRowButtonStyle: ButtonStyle {
|
|
@Environment(\.isFocused) private var isFocused
|
|
|
|
func makeBody(configuration: Configuration) -> some View {
|
|
configuration.label
|
|
.padding(.horizontal, 20)
|
|
.padding(.vertical, 16)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 12)
|
|
.fill(isFocused ? .white.opacity(0.15) : .clear)
|
|
)
|
|
.scaleEffect(configuration.isPressed ? 0.98 : (isFocused ? 1.01 : 1.0))
|
|
.animation(.easeInOut(duration: 0.15), value: isFocused)
|
|
.animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
|
|
}
|
|
}
|
|
#endif
|