mirror of
https://github.com/yattee/yattee.git
synced 2026-07-19 22:02:10 +00:00
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:
@@ -144,6 +144,16 @@ struct ChannelView: View {
|
||||
showInsetBackground ? ListBackgroundStyle.grouped.color : ListBackgroundStyle.plain.color
|
||||
}
|
||||
|
||||
private var viewOptionsSheetContent: some View {
|
||||
ViewOptionsSheet(
|
||||
layout: $layout,
|
||||
rowStyle: $rowStyle,
|
||||
gridColumns: $gridColumns,
|
||||
hideWatched: $hideWatched,
|
||||
maxGridColumns: gridConfig.maxColumns
|
||||
)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if let channel {
|
||||
@@ -328,6 +338,11 @@ struct ChannelView: View {
|
||||
Label(String(localized: "viewOptions.title"), systemImage: "slider.horizontal.3")
|
||||
}
|
||||
.liquidGlassTransitionSource(id: "channelViewOptions", in: sheetTransition)
|
||||
#if os(macOS)
|
||||
.popover(isPresented: $showViewOptions, arrowEdge: .bottom) {
|
||||
viewOptionsSheetContent
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !os(tvOS)
|
||||
@@ -340,16 +355,12 @@ struct ChannelView: View {
|
||||
channelMenu
|
||||
}
|
||||
}
|
||||
#if !os(macOS)
|
||||
.sheet(isPresented: $showViewOptions) {
|
||||
ViewOptionsSheet(
|
||||
layout: $layout,
|
||||
rowStyle: $rowStyle,
|
||||
gridColumns: $gridColumns,
|
||||
hideWatched: $hideWatched,
|
||||
maxGridColumns: gridConfig.maxColumns
|
||||
)
|
||||
.liquidGlassSheetContent(sourceID: "channelViewOptions", in: sheetTransition)
|
||||
viewOptionsSheetContent
|
||||
.liquidGlassSheetContent(sourceID: "channelViewOptions", in: sheetTransition)
|
||||
}
|
||||
#endif
|
||||
#if os(iOS)
|
||||
.toolbarBackground(collapseProgress > 0.8 ? .visible : .hidden, for: .navigationBar)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
@@ -487,6 +498,11 @@ struct ChannelView: View {
|
||||
Label(String(localized: "viewOptions.title"), systemImage: "slider.horizontal.3")
|
||||
}
|
||||
.liquidGlassTransitionSource(id: "channelViewOptions", in: sheetTransition)
|
||||
#if os(macOS)
|
||||
.popover(isPresented: $showViewOptions, arrowEdge: .bottom) {
|
||||
viewOptionsSheetContent
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !os(tvOS)
|
||||
@@ -499,16 +515,12 @@ struct ChannelView: View {
|
||||
channelMenu
|
||||
}
|
||||
}
|
||||
#if !os(macOS)
|
||||
.sheet(isPresented: $showViewOptions) {
|
||||
ViewOptionsSheet(
|
||||
layout: $layout,
|
||||
rowStyle: $rowStyle,
|
||||
gridColumns: $gridColumns,
|
||||
hideWatched: $hideWatched,
|
||||
maxGridColumns: gridConfig.maxColumns
|
||||
)
|
||||
.liquidGlassSheetContent(sourceID: "channelViewOptions", in: sheetTransition)
|
||||
viewOptionsSheetContent
|
||||
.liquidGlassSheetContent(sourceID: "channelViewOptions", in: sheetTransition)
|
||||
}
|
||||
#endif
|
||||
#if os(iOS)
|
||||
.toolbarBackground(collapseProgress > 0.8 ? .visible : .hidden, for: .navigationBar)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
|
||||
@@ -156,7 +156,8 @@ struct ViewOptionsSheet: View {
|
||||
.presentationDragIndicator(.visible)
|
||||
#endif
|
||||
#if os(macOS)
|
||||
.frame(minWidth: 420, minHeight: 320)
|
||||
.padding()
|
||||
.frame(width: 300)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,16 @@ struct BookmarksListView: View {
|
||||
return ([], nil)
|
||||
}
|
||||
|
||||
private var viewOptionsSheetContent: some View {
|
||||
ViewOptionsSheet(
|
||||
layout: $layout,
|
||||
rowStyle: $rowStyle,
|
||||
gridColumns: $gridColumns,
|
||||
hideWatched: $hideWatched,
|
||||
maxGridColumns: gridConfig.maxColumns
|
||||
)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
#if os(tvOS)
|
||||
@@ -150,19 +160,20 @@ struct BookmarksListView: View {
|
||||
Label(String(localized: "viewOptions.title"), systemImage: "slider.horizontal.3")
|
||||
}
|
||||
.liquidGlassTransitionSource(id: "bookmarksViewOptions", in: sheetTransition)
|
||||
#if os(macOS)
|
||||
.popover(isPresented: $showViewOptions, arrowEdge: .bottom) {
|
||||
viewOptionsSheetContent
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#if !os(macOS)
|
||||
.sheet(isPresented: $showViewOptions) {
|
||||
ViewOptionsSheet(
|
||||
layout: $layout,
|
||||
rowStyle: $rowStyle,
|
||||
gridColumns: $gridColumns,
|
||||
hideWatched: $hideWatched,
|
||||
maxGridColumns: gridConfig.maxColumns
|
||||
)
|
||||
.liquidGlassSheetContent(sourceID: "bookmarksViewOptions", in: sheetTransition)
|
||||
viewOptionsSheetContent
|
||||
.liquidGlassSheetContent(sourceID: "bookmarksViewOptions", in: sheetTransition)
|
||||
}
|
||||
#endif
|
||||
.onAppear {
|
||||
loadBookmarks()
|
||||
loadWatchEntries()
|
||||
|
||||
@@ -41,6 +41,15 @@ struct ContinueWatchingView: View {
|
||||
GridLayoutConfiguration(viewWidth: viewWidth, gridColumns: gridColumns)
|
||||
}
|
||||
|
||||
private var viewOptionsSheetContent: some View {
|
||||
ViewOptionsSheet(
|
||||
layout: $layout,
|
||||
rowStyle: $rowStyle,
|
||||
gridColumns: $gridColumns,
|
||||
maxGridColumns: gridConfig.maxColumns
|
||||
)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
#if os(tvOS)
|
||||
@@ -122,6 +131,11 @@ struct ContinueWatchingView: View {
|
||||
Label(String(localized: "viewOptions.title"), systemImage: "slider.horizontal.3")
|
||||
}
|
||||
.liquidGlassTransitionSource(id: "continueWatchingViewOptions", in: sheetTransition)
|
||||
#if os(macOS)
|
||||
.popover(isPresented: $showViewOptions, arrowEdge: .bottom) {
|
||||
viewOptionsSheetContent
|
||||
}
|
||||
#endif
|
||||
}
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Menu {
|
||||
@@ -137,15 +151,12 @@ struct ContinueWatchingView: View {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#if !os(macOS)
|
||||
.sheet(isPresented: $showViewOptions) {
|
||||
ViewOptionsSheet(
|
||||
layout: $layout,
|
||||
rowStyle: $rowStyle,
|
||||
gridColumns: $gridColumns,
|
||||
maxGridColumns: gridConfig.maxColumns
|
||||
)
|
||||
.liquidGlassSheetContent(sourceID: "continueWatchingViewOptions", in: sheetTransition)
|
||||
viewOptionsSheetContent
|
||||
.liquidGlassSheetContent(sourceID: "continueWatchingViewOptions", in: sheetTransition)
|
||||
}
|
||||
#endif
|
||||
.onAppear {
|
||||
loadHistory()
|
||||
}
|
||||
|
||||
@@ -79,6 +79,16 @@ struct HistoryListView: View {
|
||||
return ([], nil)
|
||||
}
|
||||
|
||||
private var viewOptionsSheetContent: some View {
|
||||
ViewOptionsSheet(
|
||||
layout: $layout,
|
||||
rowStyle: $rowStyle,
|
||||
gridColumns: $gridColumns,
|
||||
hideWatched: nil, // No hide watched for history view
|
||||
maxGridColumns: gridConfig.maxColumns
|
||||
)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
#if os(tvOS)
|
||||
@@ -165,6 +175,11 @@ struct HistoryListView: View {
|
||||
Label(String(localized: "viewOptions.title"), systemImage: "slider.horizontal.3")
|
||||
}
|
||||
.liquidGlassTransitionSource(id: "historyViewOptions", in: sheetTransition)
|
||||
#if os(macOS)
|
||||
.popover(isPresented: $showViewOptions, arrowEdge: .bottom) {
|
||||
viewOptionsSheetContent
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Clear history menu (only when not empty)
|
||||
@@ -186,16 +201,12 @@ struct HistoryListView: View {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#if !os(macOS)
|
||||
.sheet(isPresented: $showViewOptions) {
|
||||
ViewOptionsSheet(
|
||||
layout: $layout,
|
||||
rowStyle: $rowStyle,
|
||||
gridColumns: $gridColumns,
|
||||
hideWatched: nil, // No hide watched for history view
|
||||
maxGridColumns: gridConfig.maxColumns
|
||||
)
|
||||
.liquidGlassSheetContent(sourceID: "historyViewOptions", in: sheetTransition)
|
||||
viewOptionsSheetContent
|
||||
.liquidGlassSheetContent(sourceID: "historyViewOptions", in: sheetTransition)
|
||||
}
|
||||
#endif
|
||||
.confirmationDialog(
|
||||
confirmationTitle,
|
||||
isPresented: $showingClearConfirmation,
|
||||
|
||||
@@ -112,6 +112,16 @@ struct InstanceBrowseView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var viewOptionsSheetContent: some View {
|
||||
ViewOptionsSheet(
|
||||
layout: $layout,
|
||||
rowStyle: $rowStyle,
|
||||
gridColumns: $gridColumnCount,
|
||||
hideWatched: $hideWatched,
|
||||
maxGridColumns: gridConfig.maxColumns
|
||||
)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let backgroundStyle: ListBackgroundStyle = listStyle == .inset ? .grouped : .plain
|
||||
GeometryReader { geometry in
|
||||
@@ -337,19 +347,20 @@ struct InstanceBrowseView: View {
|
||||
Label(String(localized: "viewOptions.title"), systemImage: "slider.horizontal.3")
|
||||
}
|
||||
.liquidGlassTransitionSource(id: "instanceBrowseViewOptions", in: sheetTransition)
|
||||
#if os(macOS)
|
||||
.popover(isPresented: $showViewOptions, arrowEdge: .bottom) {
|
||||
viewOptionsSheetContent
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#if !os(macOS)
|
||||
.sheet(isPresented: $showViewOptions) {
|
||||
ViewOptionsSheet(
|
||||
layout: $layout,
|
||||
rowStyle: $rowStyle,
|
||||
gridColumns: $gridColumnCount,
|
||||
hideWatched: $hideWatched,
|
||||
maxGridColumns: gridConfig.maxColumns
|
||||
)
|
||||
.liquidGlassSheetContent(sourceID: "instanceBrowseViewOptions", in: sheetTransition)
|
||||
viewOptionsSheetContent
|
||||
.liquidGlassSheetContent(sourceID: "instanceBrowseViewOptions", in: sheetTransition)
|
||||
}
|
||||
#endif
|
||||
.task {
|
||||
// Initialize search view model
|
||||
if let appEnvironment {
|
||||
|
||||
@@ -59,6 +59,15 @@ struct MediaBrowserView: View {
|
||||
return sortedFiles(result)
|
||||
}
|
||||
|
||||
private var viewOptionsSheetContent: some View {
|
||||
MediaBrowserViewOptionsSheet(
|
||||
sourceType: source.type,
|
||||
sortOrder: $sortOrder,
|
||||
sortAscending: $sortAscending,
|
||||
showOnlyPlayable: $showOnlyPlayable
|
||||
)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
content
|
||||
#if !os(tvOS)
|
||||
@@ -91,18 +100,20 @@ struct MediaBrowserView: View {
|
||||
)
|
||||
}
|
||||
.liquidGlassTransitionSource(id: "mediaBrowserViewOptions", in: sheetTransition)
|
||||
#if os(macOS)
|
||||
.popover(isPresented: $showViewOptions, arrowEdge: .bottom) {
|
||||
viewOptionsSheetContent
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#if !os(macOS)
|
||||
.sheet(isPresented: $showViewOptions) {
|
||||
MediaBrowserViewOptionsSheet(
|
||||
sourceType: source.type,
|
||||
sortOrder: $sortOrder,
|
||||
sortAscending: $sortAscending,
|
||||
showOnlyPlayable: $showOnlyPlayable
|
||||
)
|
||||
.liquidGlassSheetContent(sourceID: "mediaBrowserViewOptions", in: sheetTransition)
|
||||
viewOptionsSheetContent
|
||||
.liquidGlassSheetContent(sourceID: "mediaBrowserViewOptions", in: sheetTransition)
|
||||
}
|
||||
#endif
|
||||
.task {
|
||||
await loadFiles()
|
||||
}
|
||||
|
||||
@@ -21,26 +21,43 @@ struct MediaBrowserViewOptionsSheet: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
#if os(macOS)
|
||||
// Popover content: no navigation chrome, click-outside dismisses.
|
||||
formContent
|
||||
.padding()
|
||||
.frame(width: 300)
|
||||
.onAppear {
|
||||
// Reset sort order if current selection is not available for this source type
|
||||
if !availableSortOptions.contains(sortOrder) {
|
||||
sortOrder = .name
|
||||
}
|
||||
}
|
||||
#else
|
||||
NavigationStack {
|
||||
#if os(tvOS)
|
||||
listContent
|
||||
#else
|
||||
formContent
|
||||
.navigationTitle("mediaBrowser.viewOptions.title")
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
.toolbar {
|
||||
sheetCloseToolbarItem { dismiss() }
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#if os(iOS)
|
||||
.presentationDetents([.height(280)])
|
||||
.presentationDragIndicator(.visible)
|
||||
#endif
|
||||
#if os(macOS)
|
||||
.frame(minWidth: 420, minHeight: 320)
|
||||
#endif
|
||||
.onAppear {
|
||||
// Reset sort order if current selection is not available for this source type
|
||||
if !availableSortOptions.contains(sortOrder) {
|
||||
sortOrder = .name
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
@@ -83,13 +100,6 @@ struct MediaBrowserViewOptionsSheet: View {
|
||||
sharedOptions
|
||||
}
|
||||
}
|
||||
.navigationTitle("mediaBrowser.viewOptions.title")
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
.toolbar {
|
||||
sheetCloseToolbarItem { dismiss() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -96,34 +96,20 @@ struct SearchView: View {
|
||||
|
||||
var body: some View {
|
||||
tvOSOrDefaultContent
|
||||
#if !os(macOS)
|
||||
.sheet(isPresented: $showFilterSheet) {
|
||||
SearchFiltersSheet(onApply: {
|
||||
if hasResults {
|
||||
Task { await searchViewModel?.search(query: searchTextBinding.wrappedValue) }
|
||||
}
|
||||
}, filters: Binding(
|
||||
get: { searchViewModel?.filters ?? .defaults },
|
||||
set: { newFilters in
|
||||
searchViewModel?.filters = newFilters
|
||||
saveFilters(newFilters)
|
||||
}
|
||||
))
|
||||
searchFiltersSheetContent
|
||||
#if !os(tvOS)
|
||||
.presentationDetents([.medium, .large])
|
||||
#endif
|
||||
}
|
||||
.sheet(isPresented: $showViewOptions) {
|
||||
ViewOptionsSheet(
|
||||
layout: $layout,
|
||||
rowStyle: $rowStyle,
|
||||
gridColumns: $gridColumns,
|
||||
hideWatched: $hideWatched,
|
||||
maxGridColumns: gridConfig.maxColumns
|
||||
)
|
||||
viewOptionsSheetContent
|
||||
#if !os(tvOS)
|
||||
.liquidGlassSheetContent(sourceID: "searchViewOptions", in: sheetTransition)
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
.onChange(of: searchTextBinding.wrappedValue) { _, newValue in
|
||||
if newValue.isEmpty {
|
||||
searchViewModel?.clearResults() // Clear everything when empty
|
||||
@@ -182,6 +168,43 @@ struct SearchView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var searchFiltersSheetContent: some View {
|
||||
SearchFiltersSheet(onApply: {
|
||||
if hasResults {
|
||||
Task { await searchViewModel?.search(query: searchTextBinding.wrappedValue) }
|
||||
}
|
||||
}, filters: Binding(
|
||||
get: { searchViewModel?.filters ?? .defaults },
|
||||
set: { newFilters in
|
||||
searchViewModel?.filters = newFilters
|
||||
saveFilters(newFilters)
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
private var viewOptionsSheetContent: some View {
|
||||
ViewOptionsSheet(
|
||||
layout: $layout,
|
||||
rowStyle: $rowStyle,
|
||||
gridColumns: $gridColumns,
|
||||
hideWatched: $hideWatched,
|
||||
maxGridColumns: gridConfig.maxColumns
|
||||
)
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
private var searchFiltersToolbarButton: some View {
|
||||
Button {
|
||||
showFilterSheet = true
|
||||
} label: {
|
||||
Label(String(localized: "search.filters"), systemImage: filtersIconName)
|
||||
}
|
||||
.popover(isPresented: $showFilterSheet, arrowEdge: .bottom) {
|
||||
searchFiltersSheetContent
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@ViewBuilder
|
||||
private var tvOSOrDefaultContent: some View {
|
||||
#if os(tvOS)
|
||||
@@ -239,14 +262,10 @@ struct SearchView: View {
|
||||
// Items stay in the toolbar even before a search is submitted
|
||||
// (invisible and inert) so the search field keeps a stable size.
|
||||
ToolbarItem(placement: .navigation) {
|
||||
Button {
|
||||
showFilterSheet = true
|
||||
} label: {
|
||||
Label(String(localized: "search.filters"), systemImage: filtersIconName)
|
||||
}
|
||||
.opacity(hasSearched ? 1 : 0)
|
||||
.disabled(!hasSearched)
|
||||
.accessibilityHidden(!hasSearched)
|
||||
searchFiltersToolbarButton
|
||||
.opacity(hasSearched ? 1 : 0)
|
||||
.disabled(!hasSearched)
|
||||
.accessibilityHidden(!hasSearched)
|
||||
}
|
||||
.sharedBackgroundVisibility(hasSearched ? .automatic : .hidden)
|
||||
ToolbarItem(placement: .principal) {
|
||||
@@ -259,11 +278,7 @@ struct SearchView: View {
|
||||
.sharedBackgroundVisibility(hasSearched ? .automatic : .hidden)
|
||||
} else if hasSearched {
|
||||
ToolbarItem(placement: .navigation) {
|
||||
Button {
|
||||
showFilterSheet = true
|
||||
} label: {
|
||||
Label(String(localized: "search.filters"), systemImage: filtersIconName)
|
||||
}
|
||||
searchFiltersToolbarButton
|
||||
}
|
||||
ToolbarItem(placement: .principal) {
|
||||
contentTypePicker
|
||||
@@ -286,6 +301,11 @@ struct SearchView: View {
|
||||
Label(String(localized: "viewOptions.title"), systemImage: "slider.horizontal.3")
|
||||
}
|
||||
.liquidGlassTransitionSource(id: "searchViewOptions", in: sheetTransition)
|
||||
#if os(macOS)
|
||||
.popover(isPresented: $showViewOptions, arrowEdge: .bottom) {
|
||||
viewOptionsSheetContent
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
.searchable(text: searchTextBinding, prompt: Text(String(localized: "search.placeholder")))
|
||||
@@ -1493,45 +1513,15 @@ struct SearchFiltersSheet: View {
|
||||
@Binding var filters: SearchFilters
|
||||
|
||||
var body: some View {
|
||||
#if os(macOS)
|
||||
// Popover content: filters apply live, click-outside dismisses.
|
||||
filtersForm
|
||||
.onChange(of: filters) { _, _ in onApply() }
|
||||
.padding()
|
||||
.frame(width: 300)
|
||||
#else
|
||||
NavigationStack {
|
||||
Form {
|
||||
// Sort, Upload Date, Duration in one section
|
||||
Section {
|
||||
Picker(String(localized: "search.sort"), selection: $filters.sort) {
|
||||
ForEach(SearchSortOption.allCases) { option in
|
||||
Text(option.title).tag(option)
|
||||
}
|
||||
}
|
||||
|
||||
Picker(String(localized: "search.uploadDate"), selection: $filters.date) {
|
||||
ForEach(SearchDateFilter.allCases) { option in
|
||||
Text(option.title).tag(option)
|
||||
}
|
||||
}
|
||||
|
||||
Picker(String(localized: "search.duration"), selection: $filters.duration) {
|
||||
ForEach(SearchDurationFilter.allCases) { option in
|
||||
Text(option.title).tag(option)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset Button
|
||||
Section {
|
||||
Button(role: .destructive) {
|
||||
let currentType = filters.type
|
||||
filters = .defaults
|
||||
filters.type = currentType
|
||||
} label: {
|
||||
HStack {
|
||||
Spacer()
|
||||
Text(String(localized: "search.filters.reset"))
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.disabled(filters.isDefault)
|
||||
}
|
||||
}
|
||||
filtersForm
|
||||
.navigationTitle(String(localized: "search.filters"))
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
@@ -1550,10 +1540,49 @@ struct SearchFiltersSheet: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
#if os(macOS)
|
||||
.frame(minWidth: 500, minHeight: 400)
|
||||
#endif
|
||||
}
|
||||
|
||||
private var filtersForm: some View {
|
||||
Form {
|
||||
// Sort, Upload Date, Duration in one section
|
||||
Section {
|
||||
Picker(String(localized: "search.sort"), selection: $filters.sort) {
|
||||
ForEach(SearchSortOption.allCases) { option in
|
||||
Text(option.title).tag(option)
|
||||
}
|
||||
}
|
||||
|
||||
Picker(String(localized: "search.uploadDate"), selection: $filters.date) {
|
||||
ForEach(SearchDateFilter.allCases) { option in
|
||||
Text(option.title).tag(option)
|
||||
}
|
||||
}
|
||||
|
||||
Picker(String(localized: "search.duration"), selection: $filters.duration) {
|
||||
ForEach(SearchDurationFilter.allCases) { option in
|
||||
Text(option.title).tag(option)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset Button
|
||||
Section {
|
||||
Button(role: .destructive) {
|
||||
let currentType = filters.type
|
||||
filters = .defaults
|
||||
filters.type = currentType
|
||||
} label: {
|
||||
HStack {
|
||||
Spacer()
|
||||
Text(String(localized: "search.filters.reset"))
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.disabled(filters.isDefault)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -15,46 +15,15 @@ struct PeerTubeFiltersSheet: View {
|
||||
let onApply: () -> Void
|
||||
|
||||
var body: some View {
|
||||
#if os(macOS)
|
||||
// Popover content: filters apply live, click-outside dismisses.
|
||||
filtersForm
|
||||
.onChange(of: filters) { _, _ in onApply() }
|
||||
.padding()
|
||||
.frame(width: 300)
|
||||
#else
|
||||
NavigationStack {
|
||||
Form {
|
||||
// Language filter
|
||||
if !languages.isEmpty {
|
||||
Section {
|
||||
Picker(String(localized: "peertube.filter.language"), selection: $filters.language) {
|
||||
Text(String(localized: "common.any")).tag(nil as String?)
|
||||
ForEach(languages, id: \.self) { lang in
|
||||
Text(languageDisplayName(lang)).tag(lang as String?)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Country filter
|
||||
if !countries.isEmpty {
|
||||
Section {
|
||||
Picker(String(localized: "peertube.filter.country"), selection: $filters.country) {
|
||||
Text(String(localized: "common.any")).tag(nil as String?)
|
||||
ForEach(countries, id: \.self) { country in
|
||||
Text(countryDisplayName(country)).tag(country as String?)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset button
|
||||
Section {
|
||||
Button(role: .destructive) {
|
||||
filters = PeerTubeDirectoryFilters()
|
||||
} label: {
|
||||
HStack {
|
||||
Spacer()
|
||||
Text(String(localized: "common.reset"))
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.disabled(filters.isDefault)
|
||||
}
|
||||
}
|
||||
filtersForm
|
||||
#if os(tvOS)
|
||||
.scrollClipDisabled()
|
||||
.padding(.horizontal, 40)
|
||||
@@ -63,9 +32,7 @@ struct PeerTubeFiltersSheet: View {
|
||||
.onChange(of: filters.country) { _, _ in onApply() }
|
||||
#else
|
||||
.navigationTitle(String(localized: "peertube.explore.filters"))
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button(String(localized: "common.cancel")) {
|
||||
@@ -83,9 +50,50 @@ struct PeerTubeFiltersSheet: View {
|
||||
}
|
||||
#if os(iOS)
|
||||
.presentationDetents([.medium])
|
||||
#elseif os(macOS)
|
||||
.frame(minWidth: 350, minHeight: 250)
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
private var filtersForm: some View {
|
||||
Form {
|
||||
// Language filter
|
||||
if !languages.isEmpty {
|
||||
Section {
|
||||
Picker(String(localized: "peertube.filter.language"), selection: $filters.language) {
|
||||
Text(String(localized: "common.any")).tag(nil as String?)
|
||||
ForEach(languages, id: \.self) { lang in
|
||||
Text(languageDisplayName(lang)).tag(lang as String?)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Country filter
|
||||
if !countries.isEmpty {
|
||||
Section {
|
||||
Picker(String(localized: "peertube.filter.country"), selection: $filters.country) {
|
||||
Text(String(localized: "common.any")).tag(nil as String?)
|
||||
ForEach(countries, id: \.self) { country in
|
||||
Text(countryDisplayName(country)).tag(country as String?)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset button
|
||||
Section {
|
||||
Button(role: .destructive) {
|
||||
filters = PeerTubeDirectoryFilters()
|
||||
} label: {
|
||||
HStack {
|
||||
Spacer()
|
||||
Text(String(localized: "common.reset"))
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.disabled(filters.isDefault)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func languageDisplayName(_ code: String) -> String {
|
||||
|
||||
@@ -93,17 +93,23 @@ struct PeerTubeInstancesExploreView: View {
|
||||
.task {
|
||||
await loadAllInstances()
|
||||
}
|
||||
#if !os(macOS)
|
||||
.sheet(isPresented: $showFiltersSheet) {
|
||||
PeerTubeFiltersSheet(
|
||||
filters: $filters,
|
||||
languages: availableLanguages,
|
||||
countries: availableCountries,
|
||||
onApply: {
|
||||
// Reset display limit when filters change
|
||||
displayLimit = pageSize
|
||||
}
|
||||
)
|
||||
filtersSheetContent
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private var filtersSheetContent: some View {
|
||||
PeerTubeFiltersSheet(
|
||||
filters: $filters,
|
||||
languages: availableLanguages,
|
||||
countries: availableCountries,
|
||||
onApply: {
|
||||
// Reset display limit when filters change
|
||||
displayLimit = pageSize
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Content
|
||||
@@ -227,6 +233,11 @@ struct PeerTubeInstancesExploreView: View {
|
||||
systemImage: filters.isDefault ? "line.3.horizontal.decrease.circle" : "line.3.horizontal.decrease.circle.fill"
|
||||
)
|
||||
}
|
||||
#if os(macOS)
|
||||
.popover(isPresented: $showFiltersSheet, arrowEdge: .bottom) {
|
||||
filtersSheetContent
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,10 @@ struct ManageChannelsView: View {
|
||||
@Namespace private var sheetTransition
|
||||
@State private var channels: [Channel] = []
|
||||
@State private var showViewOptions = false
|
||||
#if os(macOS)
|
||||
@State private var showSubscriptionsData = false
|
||||
@State private var pendingSubscriptionsData = false
|
||||
#endif
|
||||
@State private var searchText = ""
|
||||
@State private var isLoading = false
|
||||
@State private var notificationStates: [String: Bool] = [:]
|
||||
@@ -29,6 +33,92 @@ struct ManageChannelsView: View {
|
||||
appEnvironment?.settingsManager.listStyle ?? .inset
|
||||
}
|
||||
|
||||
private var viewOptionsForm: some View {
|
||||
Form {
|
||||
// View options section
|
||||
Section {
|
||||
// Layout picker (segmented)
|
||||
Picker(selection: $layout) {
|
||||
ForEach(VideoListLayout.allCases, id: \.self) { option in
|
||||
Label(option.displayName, systemImage: option.systemImage)
|
||||
.tag(option)
|
||||
}
|
||||
} label: {
|
||||
Text("viewOptions.layout")
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.listRowBackground(Color.clear)
|
||||
.listRowInsets(EdgeInsets(top: 8, leading: 0, bottom: 8, trailing: 0))
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
PlatformMenuPicker(String(localized: "manageChannels.sortBy"), selection: $sortOrder) {
|
||||
Text("manageChannels.sortBy.name").tag(SidebarChannelSort.alphabetical)
|
||||
Text("manageChannels.sortBy.recentlySubscribed").tag(SidebarChannelSort.recentlySubscribed)
|
||||
Text("manageChannels.sortBy.lastUploaded").tag(SidebarChannelSort.lastUploaded)
|
||||
}
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
// Subscriptions Data (opens as a sheet; popovers can't push)
|
||||
Section {
|
||||
Button {
|
||||
pendingSubscriptionsData = true
|
||||
showViewOptions = false
|
||||
} label: {
|
||||
Label(String(localized: "manageChannels.subscriptionsData"), systemImage: "person.2.badge.gearshape")
|
||||
}
|
||||
}
|
||||
#elseif os(iOS)
|
||||
// Subscriptions Data navigation link
|
||||
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
|
||||
|
||||
// Grid layout configuration
|
||||
@State private var viewWidth: CGFloat = 0
|
||||
private var gridConfig: GridLayoutConfiguration {
|
||||
@@ -108,72 +198,32 @@ struct ManageChannelsView: View {
|
||||
Label(String(localized: "viewOptions.title"), systemImage: "slider.horizontal.3")
|
||||
}
|
||||
.liquidGlassTransitionSource(id: "manageChannelsViewOptions", 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 {
|
||||
// View options section
|
||||
Section {
|
||||
// Layout picker (segmented)
|
||||
Picker(selection: $layout) {
|
||||
ForEach(VideoListLayout.allCases, id: \.self) { option in
|
||||
Label(option.displayName, systemImage: option.systemImage)
|
||||
.tag(option)
|
||||
}
|
||||
} label: {
|
||||
Text("viewOptions.layout")
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.listRowBackground(Color.clear)
|
||||
.listRowInsets(EdgeInsets(top: 8, leading: 0, bottom: 8, trailing: 0))
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
PlatformMenuPicker(String(localized: "manageChannels.sortBy"), selection: $sortOrder) {
|
||||
Text("manageChannels.sortBy.name").tag(SidebarChannelSort.alphabetical)
|
||||
Text("manageChannels.sortBy.recentlySubscribed").tag(SidebarChannelSort.recentlySubscribed)
|
||||
Text("manageChannels.sortBy.lastUploaded").tag(SidebarChannelSort.lastUploaded)
|
||||
}
|
||||
}
|
||||
|
||||
#if !os(tvOS)
|
||||
// Subscriptions Data navigation link
|
||||
Section {
|
||||
NavigationLink {
|
||||
SubscriptionsSettingsView()
|
||||
} label: {
|
||||
Label(String(localized: "manageChannels.subscriptionsData"), systemImage: "person.2.badge.gearshape")
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
viewOptionsForm
|
||||
#if os(tvOS)
|
||||
.scrollClipDisabled()
|
||||
.padding(.horizontal, 40)
|
||||
@@ -185,13 +235,11 @@ struct ManageChannelsView: View {
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
#if os(macOS)
|
||||
.frame(minWidth: 500, minHeight: 400)
|
||||
#endif
|
||||
.presentationDetents([.height(360), .large])
|
||||
.presentationDragIndicator(.visible)
|
||||
.liquidGlassSheetContent(sourceID: "manageChannelsViewOptions", in: sheetTransition)
|
||||
}
|
||||
#endif
|
||||
.onAppear {
|
||||
if let syncChannels = subscriptionService?.fetchSubscriptionsSync() {
|
||||
channels = syncChannels
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user