Present view options and search filters as popovers on macOS

Plain-Form sheets rendered as columns-style content floating inside
oversized fixed frames on macOS. Anchor these panels to their toolbar
buttons as popovers instead, sized to content; iOS/tvOS keep sheets.

- Search and PeerTube explore filters apply live (Cancel never
  reverted anything anyway) and dismiss on outside click
- Subscriptions/Manage Channels "Subscriptions Data" opens as a sheet
  from the popover's onDisappear via a pending flag, since presenting
  while the popover dismisses gets swallowed
- Media browser view options drop navigation chrome on macOS
This commit is contained in:
Arkadiusz Fal
2026-06-06 22:59:36 +02:00
parent dbe494f18a
commit 271aff86b5
13 changed files with 554 additions and 333 deletions

View File

@@ -26,6 +26,10 @@ struct SubscriptionsView: View {
@State private var errorMessage: String?
@State private var watchEntriesMap: [String: WatchEntry] = [:]
@State private var showViewOptions = false
#if os(macOS)
@State private var showSubscriptionsData = false
@State private var pendingSubscriptionsData = false
#endif
// View options (persisted)
@AppStorage("subscriptionsLayout") private var layout: VideoListLayout = .list
@@ -53,6 +57,108 @@ struct SubscriptionsView: View {
GridLayoutConfiguration(viewWidth: viewWidth, gridColumns: gridColumns)
}
private var viewOptionsForm: some View {
Form {
Section {
#if os(macOS)
Toggle("viewOptions.showSidebar", isOn: $showSidebar)
#elseif os(iOS)
if isIPadRegular {
Toggle("viewOptions.showSidebar", isOn: $showSidebar)
}
#elseif os(tvOS)
PlatformMenuPicker(String(localized: "viewOptions.showSidebar"), selection: $showSidebar) {
Text("common.on").tag(true)
Text("common.off").tag(false)
}
#endif
// Layout picker (inline menu)
PlatformMenuPicker(String(localized: "viewOptions.layout"), selection: $layout) {
ForEach(VideoListLayout.allCases, id: \.self) { option in
Text(option.displayName).tag(option)
}
}
// List-specific options
if layout == .list {
PlatformMenuPicker(String(localized: "viewOptions.rowSize"), selection: $rowStyle) {
Text("viewOptions.rowSize.compact").tag(VideoRowStyle.compact)
Text("viewOptions.rowSize.regular").tag(VideoRowStyle.regular)
Text("viewOptions.rowSize.large").tag(VideoRowStyle.large)
}
}
// Grid-specific options
if layout == .grid {
#if os(tvOS)
Picker("viewOptions.columns.header", selection: $gridColumns) {
ForEach(GridConstants.minAllowedColumns...max(GridConstants.minAllowedColumns, gridConfig.maxColumns), id: \.self) { count in
Text("\(count)").tag(count)
}
}
.pickerStyle(.segmented)
#else
Stepper(
"viewOptions.columns \(min(max(GridConstants.minAllowedColumns, gridColumns), gridConfig.maxColumns))",
value: $gridColumns,
in: GridConstants.minAllowedColumns...max(GridConstants.minAllowedColumns, gridConfig.maxColumns)
)
#endif
}
#if os(tvOS)
PlatformMenuPicker(String(localized: "viewOptions.hideWatched"), selection: $hideWatched) {
Text("common.on").tag(true)
Text("common.off").tag(false)
}
#else
Toggle("viewOptions.hideWatched", isOn: $hideWatched)
#endif
#if os(iOS)
Picker("viewOptions.channelStrip", selection: $channelStripSize) {
ForEach(ChannelStripSize.allCases, id: \.self) { size in
Text(size.displayName).tag(size)
}
}
.disabled(isIPadRegular && showSidebar)
#endif
}
#if os(macOS)
Section {
Button {
pendingSubscriptionsData = true
showViewOptions = false
} label: {
Label(String(localized: "manageChannels.subscriptionsData"), systemImage: "person.2.badge.gearshape")
}
}
#elseif os(iOS)
Section {
NavigationLink {
SubscriptionsSettingsView()
} label: {
Label(String(localized: "manageChannels.subscriptionsData"), systemImage: "person.2.badge.gearshape")
}
}
#endif
}
}
#if os(macOS)
private var subscriptionsDataSheet: some View {
NavigationStack {
SubscriptionsSettingsView()
.toolbar {
sheetCloseToolbarItem { showSubscriptionsData = false }
}
}
.frame(minWidth: 500, minHeight: 450)
}
#endif
private var isShowingFullScreenError: Bool {
if case .error = feedCache.feedLoadState, feedCache.videos.isEmpty {
return true
@@ -319,89 +425,32 @@ struct SubscriptionsView: View {
Label(String(localized: "viewOptions.title"), systemImage: "slider.horizontal.3")
}
.liquidGlassTransitionSource(id: "subscriptionsViewOptions", in: sheetTransition)
#if os(macOS)
.popover(isPresented: $showViewOptions, arrowEdge: .bottom) {
viewOptionsForm
.padding()
.frame(width: 300)
.onDisappear {
// Present the sheet only after the popover is gone,
// otherwise the presentation is swallowed.
if pendingSubscriptionsData {
pendingSubscriptionsData = false
showSubscriptionsData = true
}
}
}
#endif
}
}
#endif
#if os(macOS)
.sheet(isPresented: $showSubscriptionsData) {
subscriptionsDataSheet
}
#else
.sheet(isPresented: $showViewOptions) {
NavigationStack {
Form {
Section {
#if os(macOS)
Toggle("viewOptions.showSidebar", isOn: $showSidebar)
#elseif os(iOS)
if isIPadRegular {
Toggle("viewOptions.showSidebar", isOn: $showSidebar)
}
#elseif os(tvOS)
PlatformMenuPicker(String(localized: "viewOptions.showSidebar"), selection: $showSidebar) {
Text("common.on").tag(true)
Text("common.off").tag(false)
}
#endif
// Layout picker (inline menu)
PlatformMenuPicker(String(localized: "viewOptions.layout"), selection: $layout) {
ForEach(VideoListLayout.allCases, id: \.self) { option in
Text(option.displayName).tag(option)
}
}
// List-specific options
if layout == .list {
PlatformMenuPicker(String(localized: "viewOptions.rowSize"), selection: $rowStyle) {
Text("viewOptions.rowSize.compact").tag(VideoRowStyle.compact)
Text("viewOptions.rowSize.regular").tag(VideoRowStyle.regular)
Text("viewOptions.rowSize.large").tag(VideoRowStyle.large)
}
}
// Grid-specific options
if layout == .grid {
#if os(tvOS)
Picker("viewOptions.columns.header", selection: $gridColumns) {
ForEach(GridConstants.minAllowedColumns...max(GridConstants.minAllowedColumns, gridConfig.maxColumns), id: \.self) { count in
Text("\(count)").tag(count)
}
}
.pickerStyle(.segmented)
#else
Stepper(
"viewOptions.columns \(min(max(GridConstants.minAllowedColumns, gridColumns), gridConfig.maxColumns))",
value: $gridColumns,
in: GridConstants.minAllowedColumns...max(GridConstants.minAllowedColumns, gridConfig.maxColumns)
)
#endif
}
#if os(tvOS)
PlatformMenuPicker(String(localized: "viewOptions.hideWatched"), selection: $hideWatched) {
Text("common.on").tag(true)
Text("common.off").tag(false)
}
#else
Toggle("viewOptions.hideWatched", isOn: $hideWatched)
#endif
#if os(iOS)
Picker("viewOptions.channelStrip", selection: $channelStripSize) {
ForEach(ChannelStripSize.allCases, id: \.self) { size in
Text(size.displayName).tag(size)
}
}
.disabled(isIPadRegular && showSidebar)
#endif
}
#if !os(tvOS)
Section {
NavigationLink {
SubscriptionsSettingsView()
} label: {
Label(String(localized: "manageChannels.subscriptionsData"), systemImage: "person.2.badge.gearshape")
}
}
#endif
}
viewOptionsForm
#if os(tvOS)
.scrollClipDisabled()
.padding(.horizontal, 40)
@@ -413,13 +462,11 @@ struct SubscriptionsView: View {
#endif
#endif
}
#if os(macOS)
.frame(minWidth: 500, minHeight: 450)
#endif
.presentationDetents([.height(520), .large])
.presentationDragIndicator(.visible)
.liquidGlassSheetContent(sourceID: "subscriptionsViewOptions", in: sheetTransition)
}
#endif
.task {
await loadSubscriptionsAsync()
loadWatchEntries()