mirror of
https://github.com/yattee/yattee.git
synced 2026-10-23 05:02:53 +00:00
Videos carry isShort from Yattee Server and Piped, and Invidious feed entries are taken as Shorts when they arrive with no length and aren't live or upcoming: the feed comes from RSS, which lists Shorts, while lengths come from the Videos tab, which doesn't. Subscriptions, search and instance browse hide videos known to be Shorts when the toggle is on; videos whose source can't tell are always shown.
192 lines
7.0 KiB
Swift
192 lines
7.0 KiB
Swift
//
|
|
// ViewOptionsSheet.swift
|
|
// Yattee
|
|
//
|
|
// Reusable sheet for customizing video list display options.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
/// A sheet for customizing video list display options.
|
|
///
|
|
/// Shows layout picker (list/grid), size options, and filter toggles.
|
|
/// Reusable across different views with their own storage bindings.
|
|
struct ViewOptionsSheet: View {
|
|
@Binding var layout: VideoListLayout
|
|
@Binding var rowStyle: VideoRowStyle
|
|
@Binding var gridColumns: Int
|
|
|
|
/// Optional binding for hide watched toggle (only shown if provided).
|
|
var hideWatched: Binding<Bool>?
|
|
|
|
/// Optional binding for hide shorts toggle (only shown if provided).
|
|
var hideShorts: Binding<Bool>?
|
|
|
|
/// Optional binding for channel strip size (only shown if provided).
|
|
var channelStripSize: Binding<ChannelStripSize>?
|
|
|
|
/// Maximum columns allowed based on current view width.
|
|
var maxGridColumns: Int
|
|
|
|
/// Effective columns clamped to valid range.
|
|
private var effectiveColumns: Int {
|
|
min(max(GridConstants.minAllowedColumns, gridColumns), max(GridConstants.minAllowedColumns, maxGridColumns))
|
|
}
|
|
|
|
var body: some View {
|
|
#if os(tvOS)
|
|
tvOSContent
|
|
#else
|
|
formContent
|
|
#endif
|
|
}
|
|
|
|
#if os(tvOS)
|
|
private var tvOSContent: some View {
|
|
NavigationStack {
|
|
List {
|
|
Section {
|
|
Picker(selection: $layout) {
|
|
ForEach(VideoListLayout.allCases, id: \.self) { option in
|
|
Label(option.displayName, systemImage: option.systemImage)
|
|
.tag(option)
|
|
}
|
|
} label: {
|
|
Text("viewOptions.layout")
|
|
}
|
|
.pickerStyle(.segmented)
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
if layout == .grid {
|
|
Picker("viewOptions.columns.header", selection: $gridColumns) {
|
|
ForEach(GridConstants.minAllowedColumns...max(GridConstants.minAllowedColumns, maxGridColumns), id: \.self) { count in
|
|
Text("\(count)").tag(count)
|
|
}
|
|
}
|
|
.pickerStyle(.segmented)
|
|
}
|
|
|
|
if let hideWatched = hideWatched {
|
|
Toggle("viewOptions.hideWatched", isOn: hideWatched)
|
|
}
|
|
|
|
if let hideShorts = hideShorts {
|
|
Toggle("viewOptions.hideShorts", isOn: hideShorts)
|
|
}
|
|
|
|
if let channelStripSize = channelStripSize {
|
|
PlatformMenuPicker(String(localized: "viewOptions.channelStrip"), selection: channelStripSize) {
|
|
ForEach(ChannelStripSize.allCases, id: \.self) { size in
|
|
Text(size.displayName).tag(size)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.scrollClipDisabled()
|
|
.padding(.horizontal, 40)
|
|
.padding(.vertical, 24)
|
|
}
|
|
.presentationDetents([.height(360), .large])
|
|
.presentationDragIndicator(.visible)
|
|
}
|
|
#endif
|
|
|
|
private var formContent: some View {
|
|
Form {
|
|
// Single section with all options
|
|
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, maxGridColumns), id: \.self) { count in
|
|
Text("\(count)").tag(count)
|
|
}
|
|
}
|
|
#else
|
|
Stepper(
|
|
"viewOptions.columns \(effectiveColumns)",
|
|
value: $gridColumns,
|
|
in: GridConstants.minAllowedColumns...max(GridConstants.minAllowedColumns, maxGridColumns)
|
|
)
|
|
#endif
|
|
}
|
|
|
|
// Filters (optional)
|
|
if let hideWatched = hideWatched {
|
|
Toggle("viewOptions.hideWatched", isOn: hideWatched)
|
|
}
|
|
|
|
if let hideShorts = hideShorts {
|
|
Toggle("viewOptions.hideShorts", isOn: hideShorts)
|
|
}
|
|
|
|
// Channel Strip (subscriptions only)
|
|
if let channelStripSize = channelStripSize {
|
|
PlatformMenuPicker(String(localized: "viewOptions.channelStrip"), selection: channelStripSize) {
|
|
ForEach(ChannelStripSize.allCases, id: \.self) { size in
|
|
Text(size.displayName).tag(size)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#if os(iOS)
|
|
.presentationDetents([.height(260)])
|
|
.presentationDragIndicator(.visible)
|
|
#endif
|
|
#if os(macOS)
|
|
.formStyle(.grouped)
|
|
.frame(width: 300)
|
|
#endif
|
|
}
|
|
}
|
|
|
|
// MARK: - Preview
|
|
|
|
#Preview {
|
|
@Previewable @State var layout: VideoListLayout = .grid
|
|
@Previewable @State var rowStyle: VideoRowStyle = .regular
|
|
@Previewable @State var gridColumns = 2
|
|
@Previewable @State var hideWatched = false
|
|
|
|
ViewOptionsSheet(
|
|
layout: $layout,
|
|
rowStyle: $rowStyle,
|
|
gridColumns: $gridColumns,
|
|
hideWatched: $hideWatched,
|
|
channelStripSize: nil,
|
|
maxGridColumns: 4
|
|
)
|
|
}
|