mirror of
https://github.com/yattee/yattee.git
synced 2026-07-21 14:52:03 +00:00
Fix tvOS legacy import rows acting as a single Remove button
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
This commit is contained in:
@@ -412,7 +412,12 @@ struct AdvancedSettingsView: View {
|
|||||||
if appEnvironment?.legacyMigrationService.hasLegacyDataToImport() == true {
|
if appEnvironment?.legacyMigrationService.hasLegacyDataToImport() == true {
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
LegacyDataImportView()
|
TVSidebarDetailContainer(
|
||||||
|
systemImage: "person.badge.key",
|
||||||
|
title: String(localized: "migration.accounts.title")
|
||||||
|
) {
|
||||||
|
LegacyDataImportView()
|
||||||
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Label(String(localized: "migration.accounts.title"), systemImage: "person.badge.key")
|
Label(String(localized: "migration.accounts.title"), systemImage: "person.badge.key")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,11 @@ struct LegacyAccountsImportView: View {
|
|||||||
contentList
|
contentList
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#if !os(tvOS)
|
||||||
|
// On tvOS the title comes from the TVSidebarDetailContainer sidebar;
|
||||||
|
// a navigationTitle would render as a giant ghost behind the form.
|
||||||
.navigationTitle(String(localized: "migration.accounts.title"))
|
.navigationTitle(String(localized: "migration.accounts.title"))
|
||||||
|
#endif
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
#endif
|
#endif
|
||||||
@@ -255,31 +259,70 @@ private struct LegacyAccountImportRow: View {
|
|||||||
!state.username.isEmpty && !state.password.isEmpty && !state.isImporting
|
!state.username.isEmpty && !state.password.isEmpty && !state.isImporting
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
private var infoHeader: some View {
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
HStack(alignment: .top, spacing: 12) {
|
||||||
HStack(alignment: .top, spacing: 12) {
|
Image(systemName: legacyInstanceIcon(for: item.instanceType))
|
||||||
Image(systemName: legacyInstanceIcon(for: item.instanceType))
|
.font(.title2)
|
||||||
.font(.title2)
|
.foregroundStyle(.secondary)
|
||||||
|
.frame(width: legacyRowIconWidth)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 3) {
|
||||||
|
Text(item.displayName)
|
||||||
|
.font(.headline)
|
||||||
|
|
||||||
|
Text(item.instanceDisplayName)
|
||||||
|
.font(.subheadline)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.frame(width: 28)
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 3) {
|
Text(item.url.host ?? item.url.absoluteString)
|
||||||
Text(item.displayName)
|
.font(.caption)
|
||||||
.font(.headline)
|
.foregroundStyle(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
Text(item.instanceDisplayName)
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
|
|
||||||
Text(item.url.host ?? item.url.absoluteString)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
.lineLimit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
#if os(tvOS)
|
||||||
|
// On tvOS a Form row is a single focusable unit, so packing the whole card
|
||||||
|
// into one row makes the first button (Remove) swallow every click.
|
||||||
|
// Emit each interactive element as its own row instead.
|
||||||
|
infoHeader
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
|
||||||
|
credentialsFields
|
||||||
|
|
||||||
|
if let errorMessage = state.errorMessage {
|
||||||
|
Label(errorMessage, systemImage: "exclamationmark.triangle")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.red)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(action: onImport) {
|
||||||
|
if state.isImporting {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
ProgressView()
|
||||||
|
Text(String(localized: "migration.importing"))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text(String(localized: "migration.import"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(TVSettingsButtonStyle())
|
||||||
|
.disabled(!canImport)
|
||||||
|
|
||||||
|
Button(role: .destructive, action: onRemove) {
|
||||||
|
Text(String(localized: "common.remove"))
|
||||||
|
.foregroundStyle(.red)
|
||||||
|
}
|
||||||
|
.buttonStyle(TVSettingsButtonStyle())
|
||||||
|
.disabled(state.isImporting)
|
||||||
|
#else
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
infoHeader
|
||||||
|
|
||||||
credentialsFields
|
credentialsFields
|
||||||
|
|
||||||
if let errorMessage = state.errorMessage {
|
if let errorMessage = state.errorMessage {
|
||||||
@@ -314,6 +357,7 @@ private struct LegacyAccountImportRow: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
@@ -351,27 +395,50 @@ private struct LegacyInstanceImportRow: View {
|
|||||||
let onImport: () -> Void
|
let onImport: () -> Void
|
||||||
let onRemove: () -> Void
|
let onRemove: () -> Void
|
||||||
|
|
||||||
var body: some View {
|
private var infoHeader: some View {
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
HStack(alignment: .top, spacing: 12) {
|
||||||
HStack(alignment: .top, spacing: 12) {
|
Image(systemName: legacyInstanceIcon(for: item.instanceType))
|
||||||
Image(systemName: legacyInstanceIcon(for: item.instanceType))
|
.font(.title2)
|
||||||
.font(.title2)
|
.foregroundStyle(.secondary)
|
||||||
.foregroundStyle(.secondary)
|
.frame(width: legacyRowIconWidth)
|
||||||
.frame(width: 28)
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 3) {
|
VStack(alignment: .leading, spacing: 3) {
|
||||||
Text(item.instanceDisplayName)
|
Text(item.instanceDisplayName)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
|
|
||||||
Text(item.url.host ?? item.url.absoluteString)
|
// instanceDisplayName falls back to the host for unnamed
|
||||||
|
// instances; skip the caption instead of repeating it.
|
||||||
|
if let host = item.url.host, host != item.instanceDisplayName {
|
||||||
|
Text(host)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
#if os(tvOS)
|
||||||
|
infoHeader
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
|
||||||
|
Button(action: onImport) {
|
||||||
|
Text(String(localized: "migration.import"))
|
||||||
|
}
|
||||||
|
.buttonStyle(TVSettingsButtonStyle())
|
||||||
|
|
||||||
|
Button(role: .destructive, action: onRemove) {
|
||||||
|
Text(String(localized: "common.remove"))
|
||||||
|
.foregroundStyle(.red)
|
||||||
|
}
|
||||||
|
.buttonStyle(TVSettingsButtonStyle())
|
||||||
|
#else
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
infoHeader
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
Button(role: .destructive, action: onRemove) {
|
Button(role: .destructive, action: onRemove) {
|
||||||
Text(String(localized: "common.remove"))
|
Text(String(localized: "common.remove"))
|
||||||
@@ -388,9 +455,19 @@ private struct LegacyInstanceImportRow: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// tvOS renders .title2 glyphs far larger than the 28pt frame used on other platforms.
|
||||||
|
private var legacyRowIconWidth: CGFloat {
|
||||||
|
#if os(tvOS)
|
||||||
|
return 64
|
||||||
|
#else
|
||||||
|
return 28
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
private func legacyInstanceIcon(for type: InstanceType) -> String {
|
private func legacyInstanceIcon(for type: InstanceType) -> String {
|
||||||
switch type {
|
switch type {
|
||||||
case .invidious:
|
case .invidious:
|
||||||
|
|||||||
@@ -88,9 +88,11 @@ struct TVSettingsTextField: View {
|
|||||||
/// Button style for settings forms - subtle focus effect without glow
|
/// Button style for settings forms - subtle focus effect without glow
|
||||||
struct TVSettingsButtonStyle: ButtonStyle {
|
struct TVSettingsButtonStyle: ButtonStyle {
|
||||||
@Environment(\.isFocused) private var isFocused
|
@Environment(\.isFocused) private var isFocused
|
||||||
|
@Environment(\.isEnabled) private var isEnabled
|
||||||
|
|
||||||
func makeBody(configuration: Configuration) -> some View {
|
func makeBody(configuration: Configuration) -> some View {
|
||||||
configuration.label
|
configuration.label
|
||||||
|
.opacity(isEnabled ? 1 : 0.4)
|
||||||
.padding(.horizontal, 20)
|
.padding(.horizontal, 20)
|
||||||
.padding(.vertical, 12)
|
.padding(.vertical, 12)
|
||||||
.background(
|
.background(
|
||||||
|
|||||||
@@ -184,6 +184,22 @@ struct YatteeApp: App {
|
|||||||
.appEnvironment(appEnvironment)
|
.appEnvironment(appEnvironment)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
#if os(tvOS)
|
||||||
|
// tvOS sheets render as a small centered card that cannot fit
|
||||||
|
// the sidebar-detail layout — use a full screen cover instead.
|
||||||
|
.fullScreenCover(isPresented: $showingLegacyAccountsImport) {
|
||||||
|
NavigationStack {
|
||||||
|
TVSidebarDetailContainer(
|
||||||
|
systemImage: "person.badge.key",
|
||||||
|
title: String(localized: "migration.accounts.title")
|
||||||
|
) {
|
||||||
|
LegacyAccountsImportView()
|
||||||
|
}
|
||||||
|
.appEnvironment(appEnvironment)
|
||||||
|
}
|
||||||
|
.background(Color.black.ignoresSafeArea())
|
||||||
|
}
|
||||||
|
#else
|
||||||
.sheet(isPresented: $showingLegacyAccountsImport) {
|
.sheet(isPresented: $showingLegacyAccountsImport) {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
LegacyAccountsImportView()
|
LegacyAccountsImportView()
|
||||||
@@ -193,6 +209,7 @@ struct YatteeApp: App {
|
|||||||
.frame(minWidth: 560, minHeight: 560)
|
.frame(minWidth: 560, minHeight: 560)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
.sheet(isPresented: $showingSettings) {
|
.sheet(isPresented: $showingSettings) {
|
||||||
SettingsView()
|
SettingsView()
|
||||||
|
|||||||
Reference in New Issue
Block a user