mirror of
https://github.com/yattee/yattee.git
synced 2026-07-20 14:22:02 +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
|
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 {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
if let channel {
|
if let channel {
|
||||||
@@ -328,6 +338,11 @@ struct ChannelView: View {
|
|||||||
Label(String(localized: "viewOptions.title"), systemImage: "slider.horizontal.3")
|
Label(String(localized: "viewOptions.title"), systemImage: "slider.horizontal.3")
|
||||||
}
|
}
|
||||||
.liquidGlassTransitionSource(id: "channelViewOptions", in: sheetTransition)
|
.liquidGlassTransitionSource(id: "channelViewOptions", in: sheetTransition)
|
||||||
|
#if os(macOS)
|
||||||
|
.popover(isPresented: $showViewOptions, arrowEdge: .bottom) {
|
||||||
|
viewOptionsSheetContent
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
@@ -340,16 +355,12 @@ struct ChannelView: View {
|
|||||||
channelMenu
|
channelMenu
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#if !os(macOS)
|
||||||
.sheet(isPresented: $showViewOptions) {
|
.sheet(isPresented: $showViewOptions) {
|
||||||
ViewOptionsSheet(
|
viewOptionsSheetContent
|
||||||
layout: $layout,
|
.liquidGlassSheetContent(sourceID: "channelViewOptions", in: sheetTransition)
|
||||||
rowStyle: $rowStyle,
|
|
||||||
gridColumns: $gridColumns,
|
|
||||||
hideWatched: $hideWatched,
|
|
||||||
maxGridColumns: gridConfig.maxColumns
|
|
||||||
)
|
|
||||||
.liquidGlassSheetContent(sourceID: "channelViewOptions", in: sheetTransition)
|
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
.toolbarBackground(collapseProgress > 0.8 ? .visible : .hidden, for: .navigationBar)
|
.toolbarBackground(collapseProgress > 0.8 ? .visible : .hidden, for: .navigationBar)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
@@ -487,6 +498,11 @@ struct ChannelView: View {
|
|||||||
Label(String(localized: "viewOptions.title"), systemImage: "slider.horizontal.3")
|
Label(String(localized: "viewOptions.title"), systemImage: "slider.horizontal.3")
|
||||||
}
|
}
|
||||||
.liquidGlassTransitionSource(id: "channelViewOptions", in: sheetTransition)
|
.liquidGlassTransitionSource(id: "channelViewOptions", in: sheetTransition)
|
||||||
|
#if os(macOS)
|
||||||
|
.popover(isPresented: $showViewOptions, arrowEdge: .bottom) {
|
||||||
|
viewOptionsSheetContent
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
@@ -499,16 +515,12 @@ struct ChannelView: View {
|
|||||||
channelMenu
|
channelMenu
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#if !os(macOS)
|
||||||
.sheet(isPresented: $showViewOptions) {
|
.sheet(isPresented: $showViewOptions) {
|
||||||
ViewOptionsSheet(
|
viewOptionsSheetContent
|
||||||
layout: $layout,
|
.liquidGlassSheetContent(sourceID: "channelViewOptions", in: sheetTransition)
|
||||||
rowStyle: $rowStyle,
|
|
||||||
gridColumns: $gridColumns,
|
|
||||||
hideWatched: $hideWatched,
|
|
||||||
maxGridColumns: gridConfig.maxColumns
|
|
||||||
)
|
|
||||||
.liquidGlassSheetContent(sourceID: "channelViewOptions", in: sheetTransition)
|
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
.toolbarBackground(collapseProgress > 0.8 ? .visible : .hidden, for: .navigationBar)
|
.toolbarBackground(collapseProgress > 0.8 ? .visible : .hidden, for: .navigationBar)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
|||||||
@@ -156,7 +156,8 @@ struct ViewOptionsSheet: View {
|
|||||||
.presentationDragIndicator(.visible)
|
.presentationDragIndicator(.visible)
|
||||||
#endif
|
#endif
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
.frame(minWidth: 420, minHeight: 320)
|
.padding()
|
||||||
|
.frame(width: 300)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,16 @@ struct BookmarksListView: View {
|
|||||||
return ([], nil)
|
return ([], nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var viewOptionsSheetContent: some View {
|
||||||
|
ViewOptionsSheet(
|
||||||
|
layout: $layout,
|
||||||
|
rowStyle: $rowStyle,
|
||||||
|
gridColumns: $gridColumns,
|
||||||
|
hideWatched: $hideWatched,
|
||||||
|
maxGridColumns: gridConfig.maxColumns
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
GeometryReader { geometry in
|
GeometryReader { geometry in
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
@@ -150,19 +160,20 @@ struct BookmarksListView: View {
|
|||||||
Label(String(localized: "viewOptions.title"), systemImage: "slider.horizontal.3")
|
Label(String(localized: "viewOptions.title"), systemImage: "slider.horizontal.3")
|
||||||
}
|
}
|
||||||
.liquidGlassTransitionSource(id: "bookmarksViewOptions", in: sheetTransition)
|
.liquidGlassTransitionSource(id: "bookmarksViewOptions", in: sheetTransition)
|
||||||
|
#if os(macOS)
|
||||||
|
.popover(isPresented: $showViewOptions, arrowEdge: .bottom) {
|
||||||
|
viewOptionsSheetContent
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
#if !os(macOS)
|
||||||
.sheet(isPresented: $showViewOptions) {
|
.sheet(isPresented: $showViewOptions) {
|
||||||
ViewOptionsSheet(
|
viewOptionsSheetContent
|
||||||
layout: $layout,
|
.liquidGlassSheetContent(sourceID: "bookmarksViewOptions", in: sheetTransition)
|
||||||
rowStyle: $rowStyle,
|
|
||||||
gridColumns: $gridColumns,
|
|
||||||
hideWatched: $hideWatched,
|
|
||||||
maxGridColumns: gridConfig.maxColumns
|
|
||||||
)
|
|
||||||
.liquidGlassSheetContent(sourceID: "bookmarksViewOptions", in: sheetTransition)
|
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
.onAppear {
|
.onAppear {
|
||||||
loadBookmarks()
|
loadBookmarks()
|
||||||
loadWatchEntries()
|
loadWatchEntries()
|
||||||
|
|||||||
@@ -41,6 +41,15 @@ struct ContinueWatchingView: View {
|
|||||||
GridLayoutConfiguration(viewWidth: viewWidth, gridColumns: gridColumns)
|
GridLayoutConfiguration(viewWidth: viewWidth, gridColumns: gridColumns)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var viewOptionsSheetContent: some View {
|
||||||
|
ViewOptionsSheet(
|
||||||
|
layout: $layout,
|
||||||
|
rowStyle: $rowStyle,
|
||||||
|
gridColumns: $gridColumns,
|
||||||
|
maxGridColumns: gridConfig.maxColumns
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
GeometryReader { geometry in
|
GeometryReader { geometry in
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
@@ -122,6 +131,11 @@ struct ContinueWatchingView: View {
|
|||||||
Label(String(localized: "viewOptions.title"), systemImage: "slider.horizontal.3")
|
Label(String(localized: "viewOptions.title"), systemImage: "slider.horizontal.3")
|
||||||
}
|
}
|
||||||
.liquidGlassTransitionSource(id: "continueWatchingViewOptions", in: sheetTransition)
|
.liquidGlassTransitionSource(id: "continueWatchingViewOptions", in: sheetTransition)
|
||||||
|
#if os(macOS)
|
||||||
|
.popover(isPresented: $showViewOptions, arrowEdge: .bottom) {
|
||||||
|
viewOptionsSheetContent
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
ToolbarItem(placement: .primaryAction) {
|
ToolbarItem(placement: .primaryAction) {
|
||||||
Menu {
|
Menu {
|
||||||
@@ -137,15 +151,12 @@ struct ContinueWatchingView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
#if !os(macOS)
|
||||||
.sheet(isPresented: $showViewOptions) {
|
.sheet(isPresented: $showViewOptions) {
|
||||||
ViewOptionsSheet(
|
viewOptionsSheetContent
|
||||||
layout: $layout,
|
.liquidGlassSheetContent(sourceID: "continueWatchingViewOptions", in: sheetTransition)
|
||||||
rowStyle: $rowStyle,
|
|
||||||
gridColumns: $gridColumns,
|
|
||||||
maxGridColumns: gridConfig.maxColumns
|
|
||||||
)
|
|
||||||
.liquidGlassSheetContent(sourceID: "continueWatchingViewOptions", in: sheetTransition)
|
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
.onAppear {
|
.onAppear {
|
||||||
loadHistory()
|
loadHistory()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,16 @@ struct HistoryListView: View {
|
|||||||
return ([], nil)
|
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 {
|
var body: some View {
|
||||||
GeometryReader { geometry in
|
GeometryReader { geometry in
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
@@ -165,6 +175,11 @@ struct HistoryListView: View {
|
|||||||
Label(String(localized: "viewOptions.title"), systemImage: "slider.horizontal.3")
|
Label(String(localized: "viewOptions.title"), systemImage: "slider.horizontal.3")
|
||||||
}
|
}
|
||||||
.liquidGlassTransitionSource(id: "historyViewOptions", in: sheetTransition)
|
.liquidGlassTransitionSource(id: "historyViewOptions", in: sheetTransition)
|
||||||
|
#if os(macOS)
|
||||||
|
.popover(isPresented: $showViewOptions, arrowEdge: .bottom) {
|
||||||
|
viewOptionsSheetContent
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear history menu (only when not empty)
|
// Clear history menu (only when not empty)
|
||||||
@@ -186,16 +201,12 @@ struct HistoryListView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
#if !os(macOS)
|
||||||
.sheet(isPresented: $showViewOptions) {
|
.sheet(isPresented: $showViewOptions) {
|
||||||
ViewOptionsSheet(
|
viewOptionsSheetContent
|
||||||
layout: $layout,
|
.liquidGlassSheetContent(sourceID: "historyViewOptions", in: sheetTransition)
|
||||||
rowStyle: $rowStyle,
|
|
||||||
gridColumns: $gridColumns,
|
|
||||||
hideWatched: nil, // No hide watched for history view
|
|
||||||
maxGridColumns: gridConfig.maxColumns
|
|
||||||
)
|
|
||||||
.liquidGlassSheetContent(sourceID: "historyViewOptions", in: sheetTransition)
|
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
.confirmationDialog(
|
.confirmationDialog(
|
||||||
confirmationTitle,
|
confirmationTitle,
|
||||||
isPresented: $showingClearConfirmation,
|
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 {
|
var body: some View {
|
||||||
let backgroundStyle: ListBackgroundStyle = listStyle == .inset ? .grouped : .plain
|
let backgroundStyle: ListBackgroundStyle = listStyle == .inset ? .grouped : .plain
|
||||||
GeometryReader { geometry in
|
GeometryReader { geometry in
|
||||||
@@ -337,19 +347,20 @@ struct InstanceBrowseView: View {
|
|||||||
Label(String(localized: "viewOptions.title"), systemImage: "slider.horizontal.3")
|
Label(String(localized: "viewOptions.title"), systemImage: "slider.horizontal.3")
|
||||||
}
|
}
|
||||||
.liquidGlassTransitionSource(id: "instanceBrowseViewOptions", in: sheetTransition)
|
.liquidGlassTransitionSource(id: "instanceBrowseViewOptions", in: sheetTransition)
|
||||||
|
#if os(macOS)
|
||||||
|
.popover(isPresented: $showViewOptions, arrowEdge: .bottom) {
|
||||||
|
viewOptionsSheetContent
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
#if !os(macOS)
|
||||||
.sheet(isPresented: $showViewOptions) {
|
.sheet(isPresented: $showViewOptions) {
|
||||||
ViewOptionsSheet(
|
viewOptionsSheetContent
|
||||||
layout: $layout,
|
.liquidGlassSheetContent(sourceID: "instanceBrowseViewOptions", in: sheetTransition)
|
||||||
rowStyle: $rowStyle,
|
|
||||||
gridColumns: $gridColumnCount,
|
|
||||||
hideWatched: $hideWatched,
|
|
||||||
maxGridColumns: gridConfig.maxColumns
|
|
||||||
)
|
|
||||||
.liquidGlassSheetContent(sourceID: "instanceBrowseViewOptions", in: sheetTransition)
|
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
.task {
|
.task {
|
||||||
// Initialize search view model
|
// Initialize search view model
|
||||||
if let appEnvironment {
|
if let appEnvironment {
|
||||||
|
|||||||
@@ -59,6 +59,15 @@ struct MediaBrowserView: View {
|
|||||||
return sortedFiles(result)
|
return sortedFiles(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var viewOptionsSheetContent: some View {
|
||||||
|
MediaBrowserViewOptionsSheet(
|
||||||
|
sourceType: source.type,
|
||||||
|
sortOrder: $sortOrder,
|
||||||
|
sortAscending: $sortAscending,
|
||||||
|
showOnlyPlayable: $showOnlyPlayable
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
content
|
content
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
@@ -91,18 +100,20 @@ struct MediaBrowserView: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
.liquidGlassTransitionSource(id: "mediaBrowserViewOptions", in: sheetTransition)
|
.liquidGlassTransitionSource(id: "mediaBrowserViewOptions", in: sheetTransition)
|
||||||
|
#if os(macOS)
|
||||||
|
.popover(isPresented: $showViewOptions, arrowEdge: .bottom) {
|
||||||
|
viewOptionsSheetContent
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
#if !os(macOS)
|
||||||
.sheet(isPresented: $showViewOptions) {
|
.sheet(isPresented: $showViewOptions) {
|
||||||
MediaBrowserViewOptionsSheet(
|
viewOptionsSheetContent
|
||||||
sourceType: source.type,
|
.liquidGlassSheetContent(sourceID: "mediaBrowserViewOptions", in: sheetTransition)
|
||||||
sortOrder: $sortOrder,
|
|
||||||
sortAscending: $sortAscending,
|
|
||||||
showOnlyPlayable: $showOnlyPlayable
|
|
||||||
)
|
|
||||||
.liquidGlassSheetContent(sourceID: "mediaBrowserViewOptions", in: sheetTransition)
|
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
.task {
|
.task {
|
||||||
await loadFiles()
|
await loadFiles()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,26 +21,43 @@ struct MediaBrowserViewOptionsSheet: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some 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 {
|
NavigationStack {
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
listContent
|
listContent
|
||||||
#else
|
#else
|
||||||
formContent
|
formContent
|
||||||
|
.navigationTitle("mediaBrowser.viewOptions.title")
|
||||||
|
#if os(iOS)
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
#endif
|
||||||
|
.toolbar {
|
||||||
|
sheetCloseToolbarItem { dismiss() }
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
.presentationDetents([.height(280)])
|
.presentationDetents([.height(280)])
|
||||||
.presentationDragIndicator(.visible)
|
.presentationDragIndicator(.visible)
|
||||||
#endif
|
#endif
|
||||||
#if os(macOS)
|
|
||||||
.frame(minWidth: 420, minHeight: 320)
|
|
||||||
#endif
|
|
||||||
.onAppear {
|
.onAppear {
|
||||||
// Reset sort order if current selection is not available for this source type
|
// Reset sort order if current selection is not available for this source type
|
||||||
if !availableSortOptions.contains(sortOrder) {
|
if !availableSortOptions.contains(sortOrder) {
|
||||||
sortOrder = .name
|
sortOrder = .name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
@@ -83,13 +100,6 @@ struct MediaBrowserViewOptionsSheet: View {
|
|||||||
sharedOptions
|
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 {
|
var body: some View {
|
||||||
tvOSOrDefaultContent
|
tvOSOrDefaultContent
|
||||||
|
#if !os(macOS)
|
||||||
.sheet(isPresented: $showFilterSheet) {
|
.sheet(isPresented: $showFilterSheet) {
|
||||||
SearchFiltersSheet(onApply: {
|
searchFiltersSheetContent
|
||||||
if hasResults {
|
|
||||||
Task { await searchViewModel?.search(query: searchTextBinding.wrappedValue) }
|
|
||||||
}
|
|
||||||
}, filters: Binding(
|
|
||||||
get: { searchViewModel?.filters ?? .defaults },
|
|
||||||
set: { newFilters in
|
|
||||||
searchViewModel?.filters = newFilters
|
|
||||||
saveFilters(newFilters)
|
|
||||||
}
|
|
||||||
))
|
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
.presentationDetents([.medium, .large])
|
.presentationDetents([.medium, .large])
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showViewOptions) {
|
.sheet(isPresented: $showViewOptions) {
|
||||||
ViewOptionsSheet(
|
viewOptionsSheetContent
|
||||||
layout: $layout,
|
|
||||||
rowStyle: $rowStyle,
|
|
||||||
gridColumns: $gridColumns,
|
|
||||||
hideWatched: $hideWatched,
|
|
||||||
maxGridColumns: gridConfig.maxColumns
|
|
||||||
)
|
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
.liquidGlassSheetContent(sourceID: "searchViewOptions", in: sheetTransition)
|
.liquidGlassSheetContent(sourceID: "searchViewOptions", in: sheetTransition)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
.onChange(of: searchTextBinding.wrappedValue) { _, newValue in
|
.onChange(of: searchTextBinding.wrappedValue) { _, newValue in
|
||||||
if newValue.isEmpty {
|
if newValue.isEmpty {
|
||||||
searchViewModel?.clearResults() // Clear everything when empty
|
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
|
@ViewBuilder
|
||||||
private var tvOSOrDefaultContent: some View {
|
private var tvOSOrDefaultContent: some View {
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
@@ -239,14 +262,10 @@ struct SearchView: View {
|
|||||||
// Items stay in the toolbar even before a search is submitted
|
// Items stay in the toolbar even before a search is submitted
|
||||||
// (invisible and inert) so the search field keeps a stable size.
|
// (invisible and inert) so the search field keeps a stable size.
|
||||||
ToolbarItem(placement: .navigation) {
|
ToolbarItem(placement: .navigation) {
|
||||||
Button {
|
searchFiltersToolbarButton
|
||||||
showFilterSheet = true
|
.opacity(hasSearched ? 1 : 0)
|
||||||
} label: {
|
.disabled(!hasSearched)
|
||||||
Label(String(localized: "search.filters"), systemImage: filtersIconName)
|
.accessibilityHidden(!hasSearched)
|
||||||
}
|
|
||||||
.opacity(hasSearched ? 1 : 0)
|
|
||||||
.disabled(!hasSearched)
|
|
||||||
.accessibilityHidden(!hasSearched)
|
|
||||||
}
|
}
|
||||||
.sharedBackgroundVisibility(hasSearched ? .automatic : .hidden)
|
.sharedBackgroundVisibility(hasSearched ? .automatic : .hidden)
|
||||||
ToolbarItem(placement: .principal) {
|
ToolbarItem(placement: .principal) {
|
||||||
@@ -259,11 +278,7 @@ struct SearchView: View {
|
|||||||
.sharedBackgroundVisibility(hasSearched ? .automatic : .hidden)
|
.sharedBackgroundVisibility(hasSearched ? .automatic : .hidden)
|
||||||
} else if hasSearched {
|
} else if hasSearched {
|
||||||
ToolbarItem(placement: .navigation) {
|
ToolbarItem(placement: .navigation) {
|
||||||
Button {
|
searchFiltersToolbarButton
|
||||||
showFilterSheet = true
|
|
||||||
} label: {
|
|
||||||
Label(String(localized: "search.filters"), systemImage: filtersIconName)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ToolbarItem(placement: .principal) {
|
ToolbarItem(placement: .principal) {
|
||||||
contentTypePicker
|
contentTypePicker
|
||||||
@@ -286,6 +301,11 @@ struct SearchView: View {
|
|||||||
Label(String(localized: "viewOptions.title"), systemImage: "slider.horizontal.3")
|
Label(String(localized: "viewOptions.title"), systemImage: "slider.horizontal.3")
|
||||||
}
|
}
|
||||||
.liquidGlassTransitionSource(id: "searchViewOptions", in: sheetTransition)
|
.liquidGlassTransitionSource(id: "searchViewOptions", in: sheetTransition)
|
||||||
|
#if os(macOS)
|
||||||
|
.popover(isPresented: $showViewOptions, arrowEdge: .bottom) {
|
||||||
|
viewOptionsSheetContent
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.searchable(text: searchTextBinding, prompt: Text(String(localized: "search.placeholder")))
|
.searchable(text: searchTextBinding, prompt: Text(String(localized: "search.placeholder")))
|
||||||
@@ -1493,45 +1513,15 @@ struct SearchFiltersSheet: View {
|
|||||||
@Binding var filters: SearchFilters
|
@Binding var filters: SearchFilters
|
||||||
|
|
||||||
var body: some View {
|
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 {
|
NavigationStack {
|
||||||
Form {
|
filtersForm
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.navigationTitle(String(localized: "search.filters"))
|
.navigationTitle(String(localized: "search.filters"))
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
@@ -1550,10 +1540,49 @@ struct SearchFiltersSheet: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#if os(macOS)
|
|
||||||
.frame(minWidth: 500, minHeight: 400)
|
|
||||||
#endif
|
#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
|
let onApply: () -> Void
|
||||||
|
|
||||||
var body: some View {
|
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 {
|
NavigationStack {
|
||||||
Form {
|
filtersForm
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
.scrollClipDisabled()
|
.scrollClipDisabled()
|
||||||
.padding(.horizontal, 40)
|
.padding(.horizontal, 40)
|
||||||
@@ -63,9 +32,7 @@ struct PeerTubeFiltersSheet: View {
|
|||||||
.onChange(of: filters.country) { _, _ in onApply() }
|
.onChange(of: filters.country) { _, _ in onApply() }
|
||||||
#else
|
#else
|
||||||
.navigationTitle(String(localized: "peertube.explore.filters"))
|
.navigationTitle(String(localized: "peertube.explore.filters"))
|
||||||
#if os(iOS)
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
#endif
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .cancellationAction) {
|
ToolbarItem(placement: .cancellationAction) {
|
||||||
Button(String(localized: "common.cancel")) {
|
Button(String(localized: "common.cancel")) {
|
||||||
@@ -83,9 +50,50 @@ struct PeerTubeFiltersSheet: View {
|
|||||||
}
|
}
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
.presentationDetents([.medium])
|
.presentationDetents([.medium])
|
||||||
#elseif os(macOS)
|
|
||||||
.frame(minWidth: 350, minHeight: 250)
|
|
||||||
#endif
|
#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 {
|
private func languageDisplayName(_ code: String) -> String {
|
||||||
|
|||||||
@@ -93,17 +93,23 @@ struct PeerTubeInstancesExploreView: View {
|
|||||||
.task {
|
.task {
|
||||||
await loadAllInstances()
|
await loadAllInstances()
|
||||||
}
|
}
|
||||||
|
#if !os(macOS)
|
||||||
.sheet(isPresented: $showFiltersSheet) {
|
.sheet(isPresented: $showFiltersSheet) {
|
||||||
PeerTubeFiltersSheet(
|
filtersSheetContent
|
||||||
filters: $filters,
|
|
||||||
languages: availableLanguages,
|
|
||||||
countries: availableCountries,
|
|
||||||
onApply: {
|
|
||||||
// Reset display limit when filters change
|
|
||||||
displayLimit = pageSize
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private var filtersSheetContent: some View {
|
||||||
|
PeerTubeFiltersSheet(
|
||||||
|
filters: $filters,
|
||||||
|
languages: availableLanguages,
|
||||||
|
countries: availableCountries,
|
||||||
|
onApply: {
|
||||||
|
// Reset display limit when filters change
|
||||||
|
displayLimit = pageSize
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Content
|
// MARK: - Content
|
||||||
@@ -227,6 +233,11 @@ struct PeerTubeInstancesExploreView: View {
|
|||||||
systemImage: filters.isDefault ? "line.3.horizontal.decrease.circle" : "line.3.horizontal.decrease.circle.fill"
|
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
|
@Namespace private var sheetTransition
|
||||||
@State private var channels: [Channel] = []
|
@State private var channels: [Channel] = []
|
||||||
@State private var showViewOptions = false
|
@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 searchText = ""
|
||||||
@State private var isLoading = false
|
@State private var isLoading = false
|
||||||
@State private var notificationStates: [String: Bool] = [:]
|
@State private var notificationStates: [String: Bool] = [:]
|
||||||
@@ -29,6 +33,92 @@ struct ManageChannelsView: View {
|
|||||||
appEnvironment?.settingsManager.listStyle ?? .inset
|
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
|
// Grid layout configuration
|
||||||
@State private var viewWidth: CGFloat = 0
|
@State private var viewWidth: CGFloat = 0
|
||||||
private var gridConfig: GridLayoutConfiguration {
|
private var gridConfig: GridLayoutConfiguration {
|
||||||
@@ -108,72 +198,32 @@ struct ManageChannelsView: View {
|
|||||||
Label(String(localized: "viewOptions.title"), systemImage: "slider.horizontal.3")
|
Label(String(localized: "viewOptions.title"), systemImage: "slider.horizontal.3")
|
||||||
}
|
}
|
||||||
.liquidGlassTransitionSource(id: "manageChannelsViewOptions", in: sheetTransition)
|
.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
|
#endif
|
||||||
|
#if os(macOS)
|
||||||
|
.sheet(isPresented: $showSubscriptionsData) {
|
||||||
|
subscriptionsDataSheet
|
||||||
|
}
|
||||||
|
#else
|
||||||
.sheet(isPresented: $showViewOptions) {
|
.sheet(isPresented: $showViewOptions) {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
Form {
|
viewOptionsForm
|
||||||
// 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
|
|
||||||
}
|
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
.scrollClipDisabled()
|
.scrollClipDisabled()
|
||||||
.padding(.horizontal, 40)
|
.padding(.horizontal, 40)
|
||||||
@@ -185,13 +235,11 @@ struct ManageChannelsView: View {
|
|||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
#if os(macOS)
|
|
||||||
.frame(minWidth: 500, minHeight: 400)
|
|
||||||
#endif
|
|
||||||
.presentationDetents([.height(360), .large])
|
.presentationDetents([.height(360), .large])
|
||||||
.presentationDragIndicator(.visible)
|
.presentationDragIndicator(.visible)
|
||||||
.liquidGlassSheetContent(sourceID: "manageChannelsViewOptions", in: sheetTransition)
|
.liquidGlassSheetContent(sourceID: "manageChannelsViewOptions", in: sheetTransition)
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
.onAppear {
|
.onAppear {
|
||||||
if let syncChannels = subscriptionService?.fetchSubscriptionsSync() {
|
if let syncChannels = subscriptionService?.fetchSubscriptionsSync() {
|
||||||
channels = syncChannels
|
channels = syncChannels
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ struct SubscriptionsView: View {
|
|||||||
@State private var errorMessage: String?
|
@State private var errorMessage: String?
|
||||||
@State private var watchEntriesMap: [String: WatchEntry] = [:]
|
@State private var watchEntriesMap: [String: WatchEntry] = [:]
|
||||||
@State private var showViewOptions = false
|
@State private var showViewOptions = false
|
||||||
|
#if os(macOS)
|
||||||
|
@State private var showSubscriptionsData = false
|
||||||
|
@State private var pendingSubscriptionsData = false
|
||||||
|
#endif
|
||||||
|
|
||||||
// View options (persisted)
|
// View options (persisted)
|
||||||
@AppStorage("subscriptionsLayout") private var layout: VideoListLayout = .list
|
@AppStorage("subscriptionsLayout") private var layout: VideoListLayout = .list
|
||||||
@@ -53,6 +57,108 @@ struct SubscriptionsView: View {
|
|||||||
GridLayoutConfiguration(viewWidth: viewWidth, gridColumns: gridColumns)
|
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 {
|
private var isShowingFullScreenError: Bool {
|
||||||
if case .error = feedCache.feedLoadState, feedCache.videos.isEmpty {
|
if case .error = feedCache.feedLoadState, feedCache.videos.isEmpty {
|
||||||
return true
|
return true
|
||||||
@@ -319,89 +425,32 @@ struct SubscriptionsView: View {
|
|||||||
Label(String(localized: "viewOptions.title"), systemImage: "slider.horizontal.3")
|
Label(String(localized: "viewOptions.title"), systemImage: "slider.horizontal.3")
|
||||||
}
|
}
|
||||||
.liquidGlassTransitionSource(id: "subscriptionsViewOptions", in: sheetTransition)
|
.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
|
#endif
|
||||||
|
#if os(macOS)
|
||||||
|
.sheet(isPresented: $showSubscriptionsData) {
|
||||||
|
subscriptionsDataSheet
|
||||||
|
}
|
||||||
|
#else
|
||||||
.sheet(isPresented: $showViewOptions) {
|
.sheet(isPresented: $showViewOptions) {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
Form {
|
viewOptionsForm
|
||||||
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
|
|
||||||
}
|
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
.scrollClipDisabled()
|
.scrollClipDisabled()
|
||||||
.padding(.horizontal, 40)
|
.padding(.horizontal, 40)
|
||||||
@@ -413,13 +462,11 @@ struct SubscriptionsView: View {
|
|||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
#if os(macOS)
|
|
||||||
.frame(minWidth: 500, minHeight: 450)
|
|
||||||
#endif
|
|
||||||
.presentationDetents([.height(520), .large])
|
.presentationDetents([.height(520), .large])
|
||||||
.presentationDragIndicator(.visible)
|
.presentationDragIndicator(.visible)
|
||||||
.liquidGlassSheetContent(sourceID: "subscriptionsViewOptions", in: sheetTransition)
|
.liquidGlassSheetContent(sourceID: "subscriptionsViewOptions", in: sheetTransition)
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
.task {
|
.task {
|
||||||
await loadSubscriptionsAsync()
|
await loadSubscriptionsAsync()
|
||||||
loadWatchEntries()
|
loadWatchEntries()
|
||||||
|
|||||||
Reference in New Issue
Block a user