import SDWebImageSwiftUI import Siesta import SwiftUI struct ChannelVideosView: View { var channel: Channel? @State private var presentingShareSheet = false @State private var shareURL: URL? @State private var subscriptionToggleButtonDisabled = false @State private var contentType = Channel.ContentType.videos @StateObject private var contentTypeItems = Store<[ContentItem]>() @StateObject private var store = Store() @Environment(\.colorScheme) private var colorScheme @Environment(\.navigationStyle) private var navigationStyle #if os(iOS) @Environment(\.horizontalSizeClass) private var horizontalSizeClass #endif @ObservedObject private var accounts = AccountsModel.shared @ObservedObject private var navigation = NavigationModel.shared @ObservedObject private var recents = RecentsModel.shared @ObservedObject private var subscriptions = SubscriptionsModel.shared @Namespace private var focusNamespace var presentedChannel: Channel? { store.item ?? channel ?? recents.presentedChannel } var contentItems: [ContentItem] { guard contentType != .videos else { return ContentItem.array(of: presentedChannel?.videos ?? []) } return contentTypeItems.collection } var body: some View { if navigationStyle == .tab { NavigationView { BrowserPlayerControls { content } } } else { BrowserPlayerControls { content } } } var content: some View { let content = VStack { #if os(tvOS) VStack { HStack(spacing: 24) { thumbnail Text(navigationTitle) .font(.title2) .frame(alignment: .leading) Spacer() subscriptionsLabel viewsLabel subscriptionToggleButton if let channel = presentedChannel { FavoriteButton(item: FavoriteItem(section: .channel(channel.id, channel.name))) .labelStyle(.iconOnly) } } contentTypePicker .pickerStyle(.automatic) } .frame(maxWidth: .infinity) #endif VerticalCells(items: contentItems) { banner } .environment(\.inChannelView, true) #if os(tvOS) .prefersDefaultFocus(in: focusNamespace) #endif } #if !os(tvOS) .toolbar { #if os(iOS) ToolbarItem(placement: .principal) { channelMenu } #endif ToolbarItem(placement: .cancellationAction) { if navigationStyle == .tab { Button { withAnimation(Constants.overlayAnimation) { navigation.presentingChannel = false } } label: { Label("Close", systemImage: "xmark") } .buttonStyle(.plain) } } #if !os(iOS) ToolbarItem(placement: .navigation) { thumbnail } ToolbarItem { contentTypePicker } ToolbarItem { HStack(spacing: 3) { subscriptionsLabel viewsLabel } } ToolbarItem { if let contentItem = presentedChannel?.contentItem { ShareButton(contentItem: contentItem) } } ToolbarItem { subscriptionToggleButton .layoutPriority(2) } ToolbarItem { if let presentedChannel { FavoriteButton(item: FavoriteItem(section: .channel(presentedChannel.id, presentedChannel.name))) } } #endif } #endif .onAppear { if navigationStyle == .tab { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { resource?.loadIfNeeded() } } else { resource?.loadIfNeeded() } } .onChange(of: contentType) { _ in resource?.load() } #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif #if !os(tvOS) .navigationTitle(navigationTitle) #endif return Group { if #available(macOS 12.0, *) { content #if os(tvOS) .background(Color.background(scheme: colorScheme)) #endif #if !os(iOS) .focusScope(focusNamespace) #endif } else { content } } } var thumbnail: some View { Group { if let thumbnail = store.item?.thumbnailURL { WebImage(url: thumbnail) .resizable() } else { ZStack { Color(white: 0.6) .opacity(0.5) Image(systemName: "play.rectangle") .foregroundColor(.accentColor) .imageScale(.small) .contentShape(Rectangle()) } } } #if os(tvOS) .frame(width: 80, height: 80, alignment: .trailing) #else .frame(width: 30, height: 30, alignment: .trailing) #endif .clipShape(Circle()) } @ViewBuilder var banner: some View { if let banner = presentedChannel?.bannerURL { WebImage(url: banner) .resizable() .placeholder { Color.clear.frame(height: 0) } .scaledToFit() .clipShape(RoundedRectangle(cornerRadius: 3)) } } var subscriptionsLabel: some View { HStack(spacing: 0) { if let subscribers = presentedChannel?.subscriptionsString { Text(subscribers) } else { Text("1234") .redacted(reason: .placeholder) } Image(systemName: "person.2.fill") .imageScale(.small) } .foregroundColor(.secondary) } var viewsLabel: some View { HStack(spacing: 0) { if let views = presentedChannel?.totalViewsString { Text(views) Image(systemName: "eye.fill") .imageScale(.small) } } .foregroundColor(.secondary) } #if !os(tvOS) var channelMenu: some View { Menu { if let channel = presentedChannel { contentTypePicker Section { subscriptionToggleButton FavoriteButton(item: FavoriteItem(section: .channel(channel.id, channel.name))) } } } label: { HStack(spacing: 12) { thumbnail VStack(alignment: .leading) { Text(presentedChannel?.name ?? "Channel") .font(.headline) .foregroundColor(.primary) .layoutPriority(1) .frame(minWidth: 120, alignment: .leading) Group { HStack(spacing: 12) { subscriptionsLabel if presentedChannel?.verified ?? false { Text("Verified") } viewsLabel } .frame(minWidth: 120, alignment: .leading) } .font(.caption2.bold()) .foregroundColor(.secondary) } Image(systemName: "chevron.down.circle.fill") .foregroundColor(.accentColor) .imageScale(.small) } .frame(maxWidth: 300) } } #endif private var contentTypePicker: some View { Picker("Content type", selection: $contentType) { if let channel = presentedChannel { Text("Videos").tag(Channel.ContentType.videos) Text("Playlists").tag(Channel.ContentType.playlists) if channel.tabs.contains(where: { $0.contentType == .livestreams }) { Text("Live streams").tag(Channel.ContentType.livestreams) } if channel.tabs.contains(where: { $0.contentType == .shorts }) { Text("Shorts").tag(Channel.ContentType.shorts) } if channel.tabs.contains(where: { $0.contentType == .channels }) { Text("Channels").tag(Channel.ContentType.channels) } } } } private var resource: Resource? { guard let channel = presentedChannel else { return nil } let data = contentType != .videos ? channel.tabs.first(where: { $0.contentType == contentType })?.data : nil let resource = accounts.api.channel(channel.id, contentType: contentType, data: data) if contentType == .videos { resource.addObserver(store) } else { resource.addObserver(contentTypeItems) } return resource } @ViewBuilder private var subscriptionToggleButton: some View { if let channel = presentedChannel { Group { if accounts.app.supportsSubscriptions && accounts.signedIn { if subscriptions.isSubscribing(channel.id) { Button { subscriptionToggleButtonDisabled = true subscriptions.unsubscribe(channel.id) { subscriptionToggleButtonDisabled = false } } label: { Label("Unsubscribe", systemImage: "star.circle") #if os(iOS) .labelStyle(.automatic) #else .labelStyle(.titleOnly) #endif } } else { Button { subscriptionToggleButtonDisabled = true subscriptions.subscribe(channel.id) { subscriptionToggleButtonDisabled = false navigation.sidebarSectionChanged.toggle() } } label: { Label("Subscribe", systemImage: "circle") #if os(iOS) .labelStyle(.automatic) #else .labelStyle(.titleOnly) #endif } } } } .disabled(subscriptionToggleButtonDisabled) } } private var navigationTitle: String { presentedChannel?.name ?? "No channel" } } struct ChannelVideosView_Previews: PreviewProvider { static var previews: some View { ChannelVideosView(channel: Video.fixture.channel) .environment(\.navigationStyle, .tab) .injectFixtureEnvironmentObjects() NavigationView { Spacer() ChannelVideosView(channel: Video.fixture.channel) .environment(\.navigationStyle, .sidebar) } } }