diff --git a/Yattee/Views/Settings/AboutView.swift b/Yattee/Views/Settings/AboutView.swift index e7a9affe..d829d230 100644 --- a/Yattee/Views/Settings/AboutView.swift +++ b/Yattee/Views/Settings/AboutView.swift @@ -51,7 +51,9 @@ struct AboutView: View { versionInfoSection mpvInfoSection } + #if !os(tvOS) .navigationTitle(String(localized: "settings.about.title")) + #endif #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif diff --git a/Yattee/Views/Settings/AddSourceView.swift b/Yattee/Views/Settings/AddSourceView.swift index 30dc97ba..e6315ed1 100644 --- a/Yattee/Views/Settings/AddSourceView.swift +++ b/Yattee/Views/Settings/AddSourceView.swift @@ -25,43 +25,28 @@ struct AddSourceView: View { @State private var discoveredAllowInvalidCerts = false var body: some View { - NavigationStack { - #if os(tvOS) - VStack(spacing: 0) { - HStack { - Button(String(localized: "common.cancel")) { - dismiss() - } - .buttonStyle(TVToolbarButtonStyle()) - Spacer() - Text(String(localized: "sources.newSource")) - .font(.title2) - .fontWeight(.semibold) - Spacer() - Text(String(localized: "common.cancel")) - .opacity(0) - } - .padding(.horizontal, 48) - .padding(.vertical, 24) - - listContent - } + #if os(tvOS) + listContent .navigationDestination(isPresented: $navigateToWebDAV) { AddWebDAVView( prefillURL: discoveredWebDAVURL, prefillName: discoveredName, - prefillAllowInvalidCertificates: discoveredAllowInvalidCerts, - dismissSheet: dismiss + prefillAllowInvalidCertificates: discoveredAllowInvalidCerts ) } .navigationDestination(isPresented: $navigateToSMB) { AddSMBView( prefillServer: discoveredSMBServer, - prefillName: discoveredName, - dismissSheet: dismiss + prefillName: discoveredName ) } - #else + .sheet(isPresented: $showingNetworkDiscovery) { + NetworkShareDiscoverySheet(filterType: selectedShareType) { share in + handleSelectedShare(share) + } + } + #else + NavigationStack { listContent .navigationTitle(String(localized: "sources.newSource")) #if os(iOS) @@ -92,13 +77,13 @@ struct AddSourceView: View { dismissSheet: dismiss ) } - #endif } .sheet(isPresented: $showingNetworkDiscovery) { NetworkShareDiscoverySheet(filterType: selectedShareType) { share in handleSelectedShare(share) } } + #endif } private var listContent: some View { @@ -113,19 +98,31 @@ struct AddSourceView: View { #endif NavigationLink { + #if os(tvOS) + AddWebDAVView() + #else AddWebDAVView(dismissSheet: dismiss) + #endif } label: { Label(String(localized: "sources.addWebDAV"), systemImage: "externaldrive.connected.to.line.below") } NavigationLink { + #if os(tvOS) + AddSMBView() + #else AddSMBView(dismissSheet: dismiss) + #endif } label: { Label(String(localized: "sources.addSMB"), systemImage: "server.rack") } NavigationLink { + #if os(tvOS) + AddRemoteServerView() + #else AddRemoteServerView(dismissSheet: dismiss) + #endif } label: { Label(String(localized: "sources.addRemoteServer"), systemImage: "globe") } diff --git a/Yattee/Views/Settings/AdvancedSettingsView.swift b/Yattee/Views/Settings/AdvancedSettingsView.swift index 34c3ac16..78fb9b62 100644 --- a/Yattee/Views/Settings/AdvancedSettingsView.swift +++ b/Yattee/Views/Settings/AdvancedSettingsView.swift @@ -35,7 +35,9 @@ struct AdvancedSettingsView: View { #endif developerSection } + #if !os(tvOS) .navigationTitle(String(localized: "settings.advanced.title")) + #endif #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif diff --git a/Yattee/Views/Settings/AppearanceSettingsView.swift b/Yattee/Views/Settings/AppearanceSettingsView.swift index a63c5c75..728afc03 100644 --- a/Yattee/Views/Settings/AppearanceSettingsView.swift +++ b/Yattee/Views/Settings/AppearanceSettingsView.swift @@ -35,7 +35,9 @@ struct AppearanceSettingsView: View { ThumbnailSection(settings: settings) } } + #if !os(tvOS) .navigationTitle(String(localized: "settings.appearance.title")) + #endif #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif diff --git a/Yattee/Views/Settings/EditSourceView.swift b/Yattee/Views/Settings/EditSourceView.swift index 56b45ad2..11218a17 100644 --- a/Yattee/Views/Settings/EditSourceView.swift +++ b/Yattee/Views/Settings/EditSourceView.swift @@ -77,31 +77,11 @@ private struct EditRemoteServerContent: View { } var body: some View { + #if os(tvOS) + formContent + .accessibilityIdentifier("editSource.view") + #else NavigationStack { - #if os(tvOS) - VStack(spacing: 0) { - HStack { - Button(String(localized: "common.cancel")) { - dismiss() - } - .buttonStyle(TVToolbarButtonStyle()) - Spacer() - Text(String(localized: "sources.editSource")) - .font(.title2) - .fontWeight(.semibold) - Spacer() - Button(String(localized: "common.save")) { - saveChanges() - } - .buttonStyle(TVToolbarButtonStyle()) - } - .padding(.horizontal, 48) - .padding(.vertical, 24) - - formContent - .accessibilityIdentifier("editSource.view") - } - #else formContent .navigationTitle(String(localized: "sources.editSource")) #if os(iOS) @@ -120,8 +100,8 @@ private struct EditRemoteServerContent: View { } } .accessibilityIdentifier("editSource.view") - #endif } + #endif } private var formContent: some View { @@ -313,6 +293,17 @@ private struct EditRemoteServerContent: View { testResultSection(result) } + #if os(tvOS) + Section { + Button { + saveChanges() + } label: { + Label(String(localized: "common.save"), systemImage: "checkmark.circle") + } + .buttonStyle(TVSettingsButtonStyle()) + } + #endif + Section { Button(role: .destructive) { showingDeleteConfirmation = true @@ -529,35 +520,14 @@ private struct EditFileSourceContent: View { } var body: some View { - NavigationStack { - #if os(tvOS) - VStack(spacing: 0) { - HStack { - Button(String(localized: "common.cancel")) { - dismiss() - } - .buttonStyle(TVToolbarButtonStyle()) - Spacer() - Text(String(localized: "sources.editSource")) - .font(.title2) - .fontWeight(.semibold) - Spacer() - Button(String(localized: "common.save")) { - saveChanges() - } - .disabled(name.isEmpty) - .buttonStyle(TVToolbarButtonStyle()) - } - .padding(.horizontal, 48) - .padding(.vertical, 24) - - formContent - } + #if os(tvOS) + formContent .onAppear { hasExistingPassword = appEnvironment?.mediaSourcesManager.password(for: source) != nil smbProtocolVersion = source.smbProtocolVersion ?? .auto } - #else + #else + NavigationStack { formContent .navigationTitle(String(localized: "sources.editSource")) #if os(iOS) @@ -581,8 +551,8 @@ private struct EditFileSourceContent: View { hasExistingPassword = appEnvironment?.mediaSourcesManager.password(for: source) != nil smbProtocolVersion = source.smbProtocolVersion ?? .auto } - #endif } + #endif } private var formContent: some View { @@ -718,6 +688,18 @@ private struct EditFileSourceContent: View { } } + #if os(tvOS) + Section { + Button { + saveChanges() + } label: { + Label(String(localized: "common.save"), systemImage: "checkmark.circle") + } + .disabled(name.isEmpty) + .buttonStyle(TVSettingsButtonStyle()) + } + #endif + Section { Button(role: .destructive) { showingDeleteConfirmation = true diff --git a/Yattee/Views/Settings/LayoutNavigationSettingsView.swift b/Yattee/Views/Settings/LayoutNavigationSettingsView.swift index e80a8a5b..728f06ad 100644 --- a/Yattee/Views/Settings/LayoutNavigationSettingsView.swift +++ b/Yattee/Views/Settings/LayoutNavigationSettingsView.swift @@ -30,7 +30,9 @@ struct LayoutNavigationSettingsView: View { HandoffSection(settings: settings) } } + #if !os(tvOS) .navigationTitle(String(localized: "settings.layoutNavigation.title")) + #endif #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif diff --git a/Yattee/Views/Settings/PlaybackSettingsView.swift b/Yattee/Views/Settings/PlaybackSettingsView.swift index 55f55b9b..26ba4877 100644 --- a/Yattee/Views/Settings/PlaybackSettingsView.swift +++ b/Yattee/Views/Settings/PlaybackSettingsView.swift @@ -26,7 +26,9 @@ struct PlaybackSettingsView: View { #endif } } + #if !os(tvOS) .navigationTitle(String(localized: "settings.playback.title")) + #endif #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif diff --git a/Yattee/Views/Settings/PrivacySettingsView.swift b/Yattee/Views/Settings/PrivacySettingsView.swift index 23a1c333..55cbc6f2 100644 --- a/Yattee/Views/Settings/PrivacySettingsView.swift +++ b/Yattee/Views/Settings/PrivacySettingsView.swift @@ -19,7 +19,9 @@ struct PrivacySettingsView: View { historySection searchSection } + #if !os(tvOS) .navigationTitle(String(localized: "settings.privacy.title")) + #endif #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif diff --git a/Yattee/Views/Settings/SettingsView.swift b/Yattee/Views/Settings/SettingsView.swift index 483176b6..e70c7458 100644 --- a/Yattee/Views/Settings/SettingsView.swift +++ b/Yattee/Views/Settings/SettingsView.swift @@ -21,6 +21,8 @@ struct SettingsView: View { #if os(macOS) macOSSettings .frame(minWidth: 600, minHeight: 400) + #elseif os(tvOS) + tvOSSettings #else iOSSettings #endif @@ -83,9 +85,107 @@ struct SettingsView: View { } #endif - // MARK: - iOS/tvOS Settings + // MARK: - tvOS Settings - #if os(iOS) || os(tvOS) + #if os(tvOS) + private var tvOSSettings: some View { + NavigationStack { + List { + if let appEnvironment { + NavigationLink { + TVSettingsContainer(systemImage: "server.rack", title: String(localized: "sources.title")) { SourcesListView() } + } label: { + HStack { + Label(String(localized: "sources.title"), systemImage: "server.rack") + Spacer() + if appEnvironment.mediaSourcesManager.hasSourcesNeedingPassword { + Image(systemName: "exclamationmark.circle.fill") + .foregroundStyle(.orange) + } + } + } + .accessibilityIdentifier("settings.row.sources") + + NavigationLink { + TVSettingsContainer(systemImage: "icloud", title: String(localized: "settings.icloud.title")) { iCloudSettingsView() } + } label: { + HStack { + Label(String(localized: "settings.icloud.title"), systemImage: "icloud") + #if DEBUG + Spacer() + Text(String(localized: "settings.icloud.dev.badge")) + .font(.caption2.bold()) + .foregroundStyle(.white) + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background(.orange, in: Capsule()) + #endif + } + } + + NavigationLink { TVSettingsContainer(systemImage: "paintbrush", title: String(localized: "settings.appearance.sectionTitle")) { AppearanceSettingsView() } } label: { + Label(String(localized: "settings.appearance.sectionTitle"), systemImage: "paintbrush") + } + + NavigationLink { TVSettingsContainer(systemImage: "hand.tap", title: String(localized: "settings.layoutNavigation.title")) { LayoutNavigationSettingsView() } } label: { + Label(String(localized: "settings.layoutNavigation.title"), systemImage: "hand.tap") + } + + NavigationLink { TVSettingsContainer(systemImage: "play.circle", title: String(localized: "settings.playback.sectionTitle")) { PlaybackSettingsView() } } label: { + Label(String(localized: "settings.playback.sectionTitle"), systemImage: "play.circle") + } + + NavigationLink { TVSettingsContainer(systemImage: "hand.raised", title: String(localized: "settings.privacy.title")) { PrivacySettingsView() } } label: { + Label(String(localized: "settings.privacy.title"), systemImage: "hand.raised") + } + + NavigationLink { TVSettingsContainer(systemImage: "gearshape.2", title: String(localized: "settings.advanced.title")) { AdvancedSettingsView() } } label: { + Label(String(localized: "settings.advanced.title"), systemImage: "gearshape.2") + } + + if appEnvironment.instancesManager.enabledInstances.contains(where: \.isYouTubeInstance) { + NavigationLink { TVSettingsContainer(systemImage: "play.rectangle", title: String(localized: "settings.youtubeEnhancements.title")) { YouTubeEnhancementsSettingsView() } } label: { + Label(String(localized: "settings.youtubeEnhancements.title"), systemImage: "play.rectangle") + } + } + + NavigationLink { TVSettingsContainer(systemImage: "info.circle", title: String(localized: "settings.about.title")) { AboutView() } } label: { + Label(String(localized: "settings.about.title"), systemImage: "info.circle") + } + } + } + .listStyle(.grouped) + .safeAreaInset(edge: .leading) { + VStack(spacing: 20) { + Spacer() + + Image("AppIconPreview") + .resizable() + .scaledToFit() + .frame(width: 200, height: 200) + .clipShape(RoundedRectangle(cornerRadius: 46)) + + Text(verbatim: "Yattee") + .font(.title2) + .fontWeight(.semibold) + + Text("\(appVersion) (\(buildNumber))") + .font(.callout) + .foregroundStyle(.secondary) + + Spacer() + } + .frame(width: 400) + .allowsHitTesting(false) + } + .accessibilityIdentifier("settings.view") + } + } + #endif + + // MARK: - iOS Settings + + #if os(iOS) private var iOSSettings: some View { NavigationStack { List { @@ -206,12 +306,8 @@ struct SettingsView: View { } } } - #if !os(tvOS) .navigationTitle(String(localized: "settings.title")) - #endif - #if os(iOS) .navigationBarTitleDisplayMode(.inline) - #endif .toolbar { if showCloseButton { ToolbarItem(placement: .confirmationAction) { @@ -288,6 +384,50 @@ enum SettingsSection: String, CaseIterable, Identifiable { } } +// MARK: - tvOS Settings Container + +#if os(tvOS) +struct TVSettingsContainer: View { + let content: Content + var systemImage: String? + var title: String? + + init(systemImage: String? = nil, title: String? = nil, @ViewBuilder content: () -> Content) { + self.content = content() + self.systemImage = systemImage + self.title = title + } + + var body: some View { + content + .safeAreaInset(edge: .leading) { + if let systemImage { + VStack(spacing: 16) { + Spacer() + Image(systemName: systemImage) + .font(.system(size: 80)) + .foregroundStyle(.secondary) + if let title { + Text(title) + .font(.title3) + .fontWeight(.semibold) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + } + Spacer() + } + .frame(width: 400) + .allowsHitTesting(false) + } else { + Spacer() + .frame(width: 400) + .allowsHitTesting(false) + } + } + } +} +#endif + #Preview { SettingsView() .appEnvironment(.preview) diff --git a/Yattee/Views/Settings/SourcesListView.swift b/Yattee/Views/Settings/SourcesListView.swift index addc5d97..097d0938 100644 --- a/Yattee/Views/Settings/SourcesListView.swift +++ b/Yattee/Views/Settings/SourcesListView.swift @@ -42,7 +42,9 @@ struct SourcesListView: View { sourcesList } } + #if !os(tvOS) .navigationTitle(String(localized: "sources.title")) + #endif #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif @@ -56,12 +58,24 @@ struct SourcesListView: View { .accessibilityIdentifier("sources.addButton") } } + #if os(tvOS) + .navigationDestination(isPresented: $showingAddSheet) { + TVSettingsContainer(systemImage: "plus.circle", title: String(localized: "sources.newSource")) { AddSourceView() } + } + #else .sheet(isPresented: $showingAddSheet) { AddSourceView() } + #endif + #if os(tvOS) + .navigationDestination(item: $sourceToEdit) { source in + TVSettingsContainer(systemImage: "pencil.circle", title: String(localized: "sources.editSource")) { EditSourceView(source: source) } + } + #else .sheet(item: $sourceToEdit) { source in EditSourceView(source: source) } + #endif .confirmationDialog( deleteConfirmationMessage, isPresented: $showingDeleteConfirmation, @@ -98,6 +112,37 @@ struct SourcesListView: View { // MARK: - Sources List private var sourcesList: some View { + #if os(tvOS) + List { + if let manager = instancesManager, !manager.instances.isEmpty { + Section(String(localized: "sources.section.remoteServers")) { + let instances = manager.instances.sorted { $0.dateAdded < $1.dateAdded } + ForEach(instances) { instance in + Button { + sourceToEdit = .remoteServer(instance) + } label: { + instanceRow(instance) + } + } + } + } + + let allFileSources = allMediaSources + if !allFileSources.isEmpty { + Section(String(localized: "sources.section.fileSources")) { + ForEach(allFileSources) { source in + let needsPassword = mediaSourcesManager?.needsPassword(for: source) ?? false + Button { + sourceToEdit = .fileSource(source) + } label: { + mediaSourceRow(source, needsPassword: needsPassword) + } + } + } + } + } + .listStyle(.grouped) + #else (listStyle == .inset ? ListBackgroundStyle.grouped.color : ListBackgroundStyle.plain.color) .ignoresSafeArea() .overlay( @@ -108,6 +153,7 @@ struct SourcesListView: View { } } ) + #endif } // MARK: - Section Header @@ -131,9 +177,13 @@ struct SourcesListView: View { LazyVStack(spacing: 0) { content() } + #if os(tvOS) + .padding(.horizontal, 16) + #else .background(ListBackgroundStyle.card.color) .clipShape(RoundedRectangle(cornerRadius: 10)) .padding(.horizontal, 16) + #endif .padding(.bottom, 16) } else { LazyVStack(spacing: 0) { diff --git a/Yattee/Views/Settings/YouTubeEnhancementsSettingsView.swift b/Yattee/Views/Settings/YouTubeEnhancementsSettingsView.swift index 232dfbc6..eaee14c8 100644 --- a/Yattee/Views/Settings/YouTubeEnhancementsSettingsView.swift +++ b/Yattee/Views/Settings/YouTubeEnhancementsSettingsView.swift @@ -18,7 +18,9 @@ struct YouTubeEnhancementsSettingsView: View { DeArrowSection(settings: settings) } } + #if !os(tvOS) .navigationTitle(String(localized: "settings.youtubeEnhancements.title")) + #endif #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif diff --git a/Yattee/Views/Settings/iCloudSettingsView.swift b/Yattee/Views/Settings/iCloudSettingsView.swift index e842a8da..980f37bb 100644 --- a/Yattee/Views/Settings/iCloudSettingsView.swift +++ b/Yattee/Views/Settings/iCloudSettingsView.swift @@ -82,7 +82,9 @@ struct iCloudSettingsView: View { syncStatusSection } } + #if !os(tvOS) .navigationTitle(String(localized: "settings.icloud.title")) + #endif #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif