From 498a282396c9036660ac8441e089f60f7528eb6e Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Mon, 5 Oct 2026 23:37:14 +0200 Subject: [PATCH] Open Settings from yattee://settings SettingsView owns its navigation stack, so pushing it onto a section's stack rendered nothing. Select the Settings section where the layout has one, otherwise present Home's settings sheet; open the Settings window on macOS. --- Yattee/ContentView.swift | 6 +++++ .../Navigation/NavigationCoordinator.swift | 23 +++++++++++++++++++ .../Navigation/NavigationRouter.swift | 9 ++++++++ Yattee/Views/Home/HomeView.swift | 18 +++++++++++++++ spec/ui/smoke/url_schemes_spec.rb | 14 +++++++---- 5 files changed, 66 insertions(+), 4 deletions(-) diff --git a/Yattee/ContentView.swift b/Yattee/ContentView.swift index 7cb43e9b..bc195419 100644 --- a/Yattee/ContentView.swift +++ b/Yattee/ContentView.swift @@ -9,6 +9,9 @@ import SwiftUI struct ContentView: View { @Environment(\.appEnvironment) private var appEnvironment + #if os(macOS) + @Environment(\.openWindow) private var openWindow + #endif var body: some View { Group { @@ -79,6 +82,9 @@ struct ContentView: View { ExpandedPlayerWindowManager.shared.endInlineOverlay() } } + .onChange(of: appEnvironment.navigationCoordinator.openSettingsWindowRequest) { _, _ in + openWindow(id: "settings") + } .onChange(of: appEnvironment.navigationCoordinator.playerExpandTrigger) { _, _ in if appEnvironment.settingsManager.macPlayerSeparateWindow { presentExpandedPlayerWindow(appEnvironment: appEnvironment) diff --git a/Yattee/Services/Navigation/NavigationCoordinator.swift b/Yattee/Services/Navigation/NavigationCoordinator.swift index 8b08f009..c36b9ce8 100644 --- a/Yattee/Services/Navigation/NavigationCoordinator.swift +++ b/Yattee/Services/Navigation/NavigationCoordinator.swift @@ -37,6 +37,12 @@ final class NavigationCoordinator { /// Sheets observe it via `.dismissesOnExternalNavigation(_:)`. var dismissPresentationsGeneration = 0 + #if os(macOS) + /// Bumped to ask the main window to open the Settings window, which only a + /// view can do (`openWindow`). + var openSettingsWindowRequest = 0 + #endif + /// Whether the mini player is expanded to full screen. var isPlayerExpanded = false { didSet { @@ -231,10 +237,27 @@ final class NavigationCoordinator { /// Navigate to a destination by pushing it onto the selected section's stack. func navigate(to destination: NavigationDestination) { + // Settings has its own navigation stack, so it can't be pushed onto a + // section's stack: it is a section, a sheet, or a window instead. + if case .settings = destination { + openSettings() + return + } router.push(destination) handoffManager?.updateActivity(for: destination) } + /// Shows Settings the way the current layout presents it: the Settings + /// section where the layout has one, otherwise the Home screen's sheet + /// (`NavigationRouter.ensureSelectionIsAvailable`), or on macOS its window. + func openSettings() { + #if os(macOS) + openSettingsWindowRequest += 1 + #else + router.select(.settings) + #endif + } + // MARK: - Sheet Presentation /// Dismiss the main window's sheets so external navigation lands on a diff --git a/Yattee/Services/Navigation/NavigationRouter.swift b/Yattee/Services/Navigation/NavigationRouter.swift index 299b0bb1..6570f55e 100644 --- a/Yattee/Services/Navigation/NavigationRouter.swift +++ b/Yattee/Services/Navigation/NavigationRouter.swift @@ -32,6 +32,11 @@ final class NavigationRouter { /// sidebar layout so it is created and loaded once. let sidebarManager = SidebarManager() + /// Set when Settings was requested in a layout without a Settings section + /// (the iPhone tab bar, unless the user added the tab). Home presents its + /// Settings sheet and clears it. + var pendingSettingsSheet = false + @ObservationIgnored private var hasAppliedStartupTab = false @ObservationIgnored private var isSidebarManagerConfigured = false @@ -139,6 +144,10 @@ final class NavigationRouter { case .sources: host = .home root = .mediaSources + case .settings: + host = .home + root = nil + pendingSettingsSheet = true default: host = .home root = current.navigationDestination() diff --git a/Yattee/Views/Home/HomeView.swift b/Yattee/Views/Home/HomeView.swift index f97ff1a4..cf089b33 100644 --- a/Yattee/Views/Home/HomeView.swift +++ b/Yattee/Views/Home/HomeView.swift @@ -198,6 +198,10 @@ struct HomeView: View { .liquidGlassSheetContent(sourceID: "homeSettings", in: sheetTransition) } .dismissesOnExternalNavigation($showingSettings) + .onAppear { presentPendingSettingsSheet() } + .onChange(of: appEnvironment?.navigationCoordinator.router.pendingSettingsSheet) { _, _ in + presentPendingSettingsSheet() + } #endif .sheet(isPresented: $showingRemoteControl) { RemoteDevicesSheet() @@ -306,6 +310,20 @@ struct HomeView: View { } } + #if !os(tvOS) && !os(macOS) + /// Presents Settings for a request the layout had no Settings section for. + /// Deferred a turn: the same external navigation also dismisses sheets, + /// and that reset must not land after this one. + private func presentPendingSettingsSheet() { + guard let router = appEnvironment?.navigationCoordinator.router, + router.pendingSettingsSheet else { return } + router.pendingSettingsSheet = false + Task { @MainActor in + showingSettings = true + } + } + #endif + // MARK: - Main Content #if os(tvOS) diff --git a/spec/ui/smoke/url_schemes_spec.rb b/spec/ui/smoke/url_schemes_spec.rb index 6c84fbef..3441c5db 100644 --- a/spec/ui/smoke/url_schemes_spec.rb +++ b/spec/ui/smoke/url_schemes_spec.rb @@ -170,10 +170,6 @@ RSpec.describe 'URL Schemes', :smoke do @axe.screenshot('url-scheme-continue-watching') end - # NOTE: yattee://settings deep link is broken in the app - - # handlePendingNavigation pushes .settings onto homePath but it doesn't render. - # Skipping until the app's deep link handler is fixed to present Settings properly. - it 'opens Search via yattee://search?q=test' do open_url('yattee://search?q=test') sleep 1 @@ -184,6 +180,16 @@ RSpec.describe 'URL Schemes', :smoke do expect(@axe).to have_element('Search') @axe.screenshot('url-scheme-search') end + + # Last in the group: on iPhone Settings opens as a sheet over Home, and the + # next deep link dismisses it. + it 'opens Settings via yattee://settings' do + open_url('yattee://settings') + sleep 1 + wait_for_text('Appearance') + expect(@axe).to have_text('Appearance') + @axe.screenshot('url-scheme-settings') + end end end