From cee2793399673153d6a79ba5a604140682b1ec35 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Sun, 19 Apr 2026 14:01:06 +0200 Subject: [PATCH] Use NavigationSplitView on macOS with persistent sidebar Settings button Replaces the macOS TabView(.sidebarAdaptable) root with NavigationSplitView so the Settings gear can live in the sidebar column's toolbar (next to the sidebar toggle) instead of only appearing in HomeView's detail toolbar. --- Yattee/Views/Home/HomeView.swift | 8 +- Yattee/Views/Navigation/UnifiedTabView.swift | 267 ++++++++----------- Yattee/YatteeApp.swift | 1 + 3 files changed, 125 insertions(+), 151 deletions(-) diff --git a/Yattee/Views/Home/HomeView.swift b/Yattee/Views/Home/HomeView.swift index 45522f56..55dfc3a2 100644 --- a/Yattee/Views/Home/HomeView.swift +++ b/Yattee/Views/Home/HomeView.swift @@ -10,7 +10,9 @@ import SwiftUI struct HomeView: View { @Environment(\.appEnvironment) private var appEnvironment @Environment(\.scenePhase) private var scenePhase + #if !os(tvOS) && !os(macOS) @Namespace private var sheetTransition + #endif @State private var playlists: [LocalPlaylist] = [] @State private var bookmarksCount: Int = 0 @State private var recentBookmarks: [Bookmark] = [] @@ -18,7 +20,9 @@ struct HomeView: View { @State private var recentContinueWatching: [WatchEntry] = [] @State private var historyCount: Int = 0 @State private var recentHistory: [WatchEntry] = [] + #if !os(tvOS) && !os(macOS) @State private var showingSettings = false + #endif @State private var showingOpenLink = false @State private var showingRemoteControl = false @State private var showingCustomizeHome = false @@ -78,7 +82,7 @@ struct HomeView: View { .toolbarTitleDisplayMode(.inlineLarge) #endif .toolbar { - #if !os(tvOS) + #if !os(tvOS) && !os(macOS) ToolbarItem(placement: .primaryAction) { Button { showingSettings = true @@ -91,7 +95,7 @@ struct HomeView: View { } #endif } - #if !os(tvOS) + #if !os(tvOS) && !os(macOS) .sheet(isPresented: $showingSettings) { SettingsView() .liquidGlassSheetContent(sourceID: "homeSettings", in: sheetTransition) diff --git a/Yattee/Views/Navigation/UnifiedTabView.swift b/Yattee/Views/Navigation/UnifiedTabView.swift index d1fe0ed8..e02d5a33 100644 --- a/Yattee/Views/Navigation/UnifiedTabView.swift +++ b/Yattee/Views/Navigation/UnifiedTabView.swift @@ -376,17 +376,41 @@ struct UnifiedTabView: View { // Zoom transition namespace (local to this tab view) @Namespace private var zoomTransition + // Settings sheet (rendered from the NavigationSplitView toolbar so the gear sits + // immediately after the system-vended sidebar-toggle in the sidebar column). + @State private var showingSettings = false + @Namespace private var sheetTransition + private var yatteeServerAuthHeader: String? { guard let server = appEnvironment?.instancesManager.enabledYatteeServerInstances.first else { return nil } return appEnvironment?.basicAuthCredentialsManager.basicAuthHeader(for: server) } var body: some View { - TabView(selection: $selection) { - mainTabs - sidebarSections + NavigationSplitView { + sidebar + .toolbar { + ToolbarItem(placement: .primaryAction) { + Button { + showingSettings = true + } label: { + Image(systemName: "gear") + } + .accessibilityIdentifier("app.settingsButton") + .accessibilityLabel(String(localized: "settings.title")) + .liquidGlassTransitionSource(id: "appSettings", in: sheetTransition) + } + } + } detail: { + detailForSelection + } + .sheet(isPresented: $showingSettings) { + SettingsView() + .liquidGlassSheetContent(sourceID: "appSettings", in: sheetTransition) + } + .onChange(of: appEnvironment?.navigationCoordinator.dismissSettingsTrigger) { + showingSettings = false } - .tabViewStyle(.sidebarAdaptable) .zoomTransitionNamespace(zoomTransition) .onAppear { configureSidebarManager() @@ -420,177 +444,122 @@ struct UnifiedTabView: View { settingsManager?.visibleSidebarMainItems() ?? SidebarMainItem.defaultOrder } - // MARK: - Tab Builders + // MARK: - Sidebar - @TabContentBuilder - private var mainTabs: some TabContent { - ForEach(visibleMainItems) { item in - mainTab(for: item) + private var sidebar: some View { + List(selection: $selection) { + ForEach(visibleMainItems) { item in + sidebarRow(for: item) + } + + if !sidebarManager.hasNoSources && (settingsManager?.sidebarSourcesEnabled ?? true) { + Section(String(localized: "sidebar.section.sources")) { + ForEach(sidebarManager.sortedSourceItems) { item in + NavigationLink(value: item) { + Label(item.title, systemImage: item.systemImage) + } + } + } + } + + if !sidebarManager.channelItems.isEmpty && (settingsManager?.sidebarChannelsEnabled ?? true) { + Section(String(localized: "sidebar.section.channels")) { + ForEach(sidebarManager.channelItems) { item in + NavigationLink(value: item) { + channelLabel(for: item) + } + } + } + } + + if !sidebarManager.playlistItems.isEmpty && (settingsManager?.sidebarPlaylistsEnabled ?? true) { + Section(String(localized: "sidebar.section.playlists")) { + ForEach(sidebarManager.playlistItems) { item in + NavigationLink(value: item) { + playlistLabel(for: item) + } + } + } + } } } - @TabContentBuilder - private func mainTab(for item: SidebarMainItem) -> some TabContent { - switch item { - case .search: - Tab(value: SidebarItem.search, role: .search) { - NavigationStack(path: $searchPath) { - SearchView().withNavigationDestinations() - } - } label: { - Label(SidebarItem.search.title, systemImage: SidebarItem.search.systemImage) - } + @ViewBuilder + private func sidebarRow(for item: SidebarMainItem) -> some View { + let sidebarItem = item.sidebarItem + NavigationLink(value: sidebarItem) { + Label(sidebarItem.title, systemImage: sidebarItem.systemImage) + } + } + // MARK: - Detail + + @ViewBuilder + private var detailForSelection: some View { + switch selection { case .home: - Tab(value: SidebarItem.home) { - NavigationStack(path: $homePath) { - HomeView().withNavigationDestinations() - } - } label: { - Label(SidebarItem.home.title, systemImage: SidebarItem.home.systemImage) + NavigationStack(path: $homePath) { + HomeView().withNavigationDestinations() } - - case .subscriptions: - Tab(value: SidebarItem.subscriptionsFeed) { - NavigationStack(path: $subscriptionsFeedPath) { - SubscriptionsView().withNavigationDestinations() - } - } label: { - Label(SidebarItem.subscriptionsFeed.title, systemImage: SidebarItem.subscriptionsFeed.systemImage) + case .search: + NavigationStack(path: $searchPath) { + SearchView().withNavigationDestinations() + } + case .subscriptionsFeed: + NavigationStack(path: $subscriptionsFeedPath) { + SubscriptionsView().withNavigationDestinations() } - case .bookmarks: - Tab(value: SidebarItem.bookmarks) { - NavigationStack(path: $bookmarksPath) { - BookmarksListView().withNavigationDestinations() - } - } label: { - Label(SidebarItem.bookmarks.title, systemImage: SidebarItem.bookmarks.systemImage) + NavigationStack(path: $bookmarksPath) { + BookmarksListView().withNavigationDestinations() } - case .history: - Tab(value: SidebarItem.history) { - NavigationStack(path: $historyPath) { - HistoryListView().withNavigationDestinations() - } - } label: { - Label(SidebarItem.history.title, systemImage: SidebarItem.history.systemImage) + NavigationStack(path: $historyPath) { + HistoryListView().withNavigationDestinations() } - case .downloads: - Tab(value: SidebarItem.downloads) { - NavigationStack(path: $downloadsPath) { - DownloadsView().withNavigationDestinations() - } - } label: { - Label(SidebarItem.downloads.title, systemImage: SidebarItem.downloads.systemImage) + NavigationStack(path: $downloadsPath) { + DownloadsView().withNavigationDestinations() } - - case .channels: - Tab(value: SidebarItem.manageChannels) { - NavigationStack(path: $manageChannelsPath) { - ManageChannelsView().withNavigationDestinations() - } - } label: { - Label(SidebarItem.manageChannels.title, systemImage: SidebarItem.manageChannels.systemImage) + case .manageChannels: + NavigationStack(path: $manageChannelsPath) { + ManageChannelsView().withNavigationDestinations() } - - case .playlists: - Tab(value: SidebarItem.playlistsList) { - NavigationStack(path: $playlistsListPath) { - PlaylistsListView().withNavigationDestinations() - } - } label: { - Label(SidebarItem.playlistsList.title, systemImage: SidebarItem.playlistsList.systemImage) + case .playlistsList: + NavigationStack(path: $playlistsListPath) { + PlaylistsListView().withNavigationDestinations() } - case .sources: - Tab(value: SidebarItem.sources) { - NavigationStack(path: $sourcesPath) { - MediaSourcesView().withNavigationDestinations() - } - } label: { - Label(SidebarItem.sources.title, systemImage: SidebarItem.sources.systemImage) + NavigationStack(path: $sourcesPath) { + MediaSourcesView().withNavigationDestinations() } - case .settings: - Tab(value: SidebarItem.settings) { - NavigationStack(path: $settingsPath) { - SettingsView().withNavigationDestinations() - } - } label: { - Label(SidebarItem.settings.title, systemImage: SidebarItem.settings.systemImage) + NavigationStack(path: $settingsPath) { + SettingsView().withNavigationDestinations() } - case .openURL: - Tab(value: SidebarItem.openURL) { - NavigationStack(path: $openURLPath) { - OpenLinkView().withNavigationDestinations() - } - } label: { - Label(SidebarItem.openURL.title, systemImage: SidebarItem.openURL.systemImage) + NavigationStack(path: $openURLPath) { + OpenLinkView().withNavigationDestinations() } - case .remoteControl: - Tab(value: SidebarItem.remoteControl) { - NavigationStack(path: $remoteControlPath) { - RemoteControlContentView(navigationStyle: .link) - .navigationTitle(String(localized: "remoteControl.title")) - .withNavigationDestinations() - } - } label: { - Label(SidebarItem.remoteControl.title, systemImage: SidebarItem.remoteControl.systemImage) + NavigationStack(path: $remoteControlPath) { + RemoteControlContentView(navigationStyle: .link) + .navigationTitle(String(localized: "remoteControl.title")) + .withNavigationDestinations() } - case .continueWatching: - Tab(value: SidebarItem.continueWatching) { - NavigationStack(path: $continueWatchingPath) { - ContinueWatchingView() - .withNavigationDestinations() - } - } label: { - Label(SidebarItem.continueWatching.title, systemImage: SidebarItem.continueWatching.systemImage) - } - } - } - - @TabContentBuilder - private var sidebarSections: some TabContent { - // Unified Sources Section (instances + media sources) - if !sidebarManager.hasNoSources && (settingsManager?.sidebarSourcesEnabled ?? true) { - TabSection(String(localized: "sidebar.section.sources")) { - ForEach(sidebarManager.sortedSourceItems) { item in - Tab(value: item) { - sourceContent(for: item) - } label: { - Label(item.title, systemImage: item.systemImage) - } - } - } - } - - if !sidebarManager.channelItems.isEmpty && (settingsManager?.sidebarChannelsEnabled ?? true) { - TabSection(String(localized: "sidebar.section.channels")) { - ForEach(sidebarManager.channelItems) { item in - Tab(value: item) { - channelContent(for: item) - } label: { - channelLabel(for: item) - } - } - } - } - - if !sidebarManager.playlistItems.isEmpty && (settingsManager?.sidebarPlaylistsEnabled ?? true) { - TabSection(String(localized: "sidebar.section.playlists")) { - ForEach(sidebarManager.playlistItems) { item in - Tab(value: item) { - playlistContent(for: item) - } label: { - playlistLabel(for: item) - } - } + NavigationStack(path: $continueWatchingPath) { + ContinueWatchingView() + .withNavigationDestinations() } + case .channel: + channelContent(for: selection) + case .playlist: + playlistContent(for: selection) + case .mediaSource, .instance: + sourceContent(for: selection) + case .nowPlaying: + EmptyView() } } diff --git a/Yattee/YatteeApp.swift b/Yattee/YatteeApp.swift index 73f73324..17bc3d70 100644 --- a/Yattee/YatteeApp.swift +++ b/Yattee/YatteeApp.swift @@ -167,6 +167,7 @@ struct YatteeApp: App { } #if os(macOS) .windowStyle(.hiddenTitleBar) + .windowToolbarStyle(.unified) .defaultSize(width: 1200, height: 800) // Handle URLs in the existing window instead of opening a new one .handlesExternalEvents(matching: Set(["*"]))