yattee/Shared/Channels/ChannelVideosView.swift

473 lines
16 KiB
Swift
Raw Normal View History

2022-12-12 00:18:29 +00:00
import Defaults
2022-11-27 10:42:16 +00:00
import SDWebImageSwiftUI
2021-08-29 21:36:18 +00:00
import Siesta
import SwiftUI
struct ChannelVideosView: View {
2022-05-29 18:26:56 +00:00
var channel: Channel?
var showCloseButton = false
var inNavigationView = true
2021-08-29 21:36:18 +00:00
2021-10-26 22:59:59 +00:00
@State private var presentingShareSheet = false
2021-11-13 15:45:47 +00:00
@State private var shareURL: URL?
2022-03-26 13:37:55 +00:00
@State private var subscriptionToggleButtonDisabled = false
2021-10-26 22:59:59 +00:00
@State private var page: ChannelPage?
2022-11-27 10:42:16 +00:00
@State private var contentType = Channel.ContentType.videos
@StateObject private var contentTypeItems = Store<[ContentItem]>()
2022-12-17 13:24:09 +00:00
@State private var descriptionExpanded = false
@StateObject private var store = Store<ChannelPage>()
2021-09-25 08:18:22 +00:00
@Environment(\.colorScheme) private var colorScheme
@ObservedObject private var accounts = AccountsModel.shared
@ObservedObject private var feed = FeedModel.shared
@ObservedObject private var navigation = NavigationModel.shared
@ObservedObject private var recents = RecentsModel.shared
2022-12-11 15:15:42 +00:00
@ObservedObject private var subscriptions = SubscribedChannelsModel.shared
@Namespace private var focusNamespace
2021-08-29 21:36:18 +00:00
2022-12-12 00:18:29 +00:00
@Default(.channelPlaylistListingStyle) private var channelPlaylistListingStyle
2022-12-19 00:37:09 +00:00
@Default(.expandChannelDescription) private var expandChannelDescription
2023-02-25 15:42:18 +00:00
@Default(.hideShorts) private var hideShorts
2022-12-12 00:18:29 +00:00
2022-05-29 18:26:56 +00:00
var presentedChannel: Channel? {
store.item?.channel ?? channel ?? recents.presentedChannel
2022-05-29 18:26:56 +00:00
}
2022-11-27 10:42:16 +00:00
var contentItems: [ContentItem] {
2023-04-22 08:56:18 +00:00
contentTypeItems.collection
}
2021-08-29 21:36:18 +00:00
var body: some View {
2021-11-28 14:37:55 +00:00
let content = VStack {
#if os(tvOS)
2022-11-27 10:42:16 +00:00
VStack {
HStack(spacing: 24) {
thumbnail
2022-11-27 10:42:16 +00:00
Text(navigationTitle)
.font(.headline)
2022-11-27 10:42:16 +00:00
.frame(alignment: .leading)
2022-11-27 10:42:16 +00:00
Spacer()
2021-11-01 21:56:18 +00:00
2022-11-27 10:42:16 +00:00
subscriptionsLabel
viewsLabel
2022-11-27 10:42:16 +00:00
subscriptionToggleButton
2023-02-26 18:14:06 +00:00
favoriteButton
.labelStyle(.iconOnly)
2022-11-27 10:42:16 +00:00
}
contentTypePicker
.pickerStyle(.automatic)
}
.frame(maxWidth: .infinity)
#endif
2023-05-21 15:17:56 +00:00
VerticalCells(items: contentItems, edgesIgnoringSafeArea: .init()) {
2022-12-17 13:24:09 +00:00
if let description = presentedChannel?.description, !description.isEmpty {
Button {
withAnimation(.spring()) {
descriptionExpanded.toggle()
}
} label: {
VStack(alignment: .leading) {
banner
ZStack(alignment: .topTrailing) {
Text(description)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(descriptionExpanded ? 50 : 1)
.multilineTextAlignment(.leading)
#if os(tvOS)
.foregroundColor(.primary)
#else
.foregroundColor(.secondary)
#endif
}
}
2022-12-18 18:43:16 +00:00
.padding(.bottom, 10)
2022-12-17 13:24:09 +00:00
}
.buttonStyle(.plain)
} else {
banner
}
2022-11-27 10:42:16 +00:00
}
.environment(\.loadMoreContentHandler) { loadNextPage() }
2022-11-27 10:42:16 +00:00
.environment(\.inChannelView, true)
2022-12-12 00:18:29 +00:00
.environment(\.listingStyle, channelPlaylistListingStyle)
2023-02-25 15:42:18 +00:00
.environment(\.hideShorts, hideShorts)
#if os(tvOS)
.prefersDefaultFocus(in: focusNamespace)
#endif
}
2021-11-28 14:37:55 +00:00
2021-08-29 21:36:18 +00:00
#if !os(tvOS)
.toolbar {
2022-11-27 10:42:16 +00:00
#if os(iOS)
ToolbarItem(placement: .principal) {
channelMenu
}
#endif
2022-08-26 07:58:08 +00:00
ToolbarItem(placement: .cancellationAction) {
if showCloseButton {
2022-08-26 07:58:08 +00:00
Button {
2022-08-25 17:09:55 +00:00
withAnimation(Constants.overlayAnimation) {
2022-07-09 00:21:04 +00:00
navigation.presentingChannel = false
navigation.presentingChannelSheet = false
2022-07-09 00:21:04 +00:00
}
2022-08-26 07:58:08 +00:00
} label: {
Label("Close", systemImage: "xmark")
2022-05-29 18:26:56 +00:00
}
#if !os(macOS)
2022-11-27 10:42:16 +00:00
.buttonStyle(.plain)
#endif
2022-05-29 18:26:56 +00:00
}
}
#if os(macOS)
2022-11-27 10:42:16 +00:00
ToolbarItem(placement: .navigation) {
thumbnail
}
ToolbarItemGroup {
if !inNavigationView {
Text(navigationTitle)
.fontWeight(.bold)
}
2022-12-12 00:18:29 +00:00
ListingStyleButtons(listingStyle: $channelPlaylistListingStyle)
2023-02-25 15:42:18 +00:00
HideShortsButtons(hide: $hideShorts)
2022-11-27 10:42:16 +00:00
contentTypePicker
}
2021-10-26 22:59:59 +00:00
2023-04-22 20:44:59 +00:00
ToolbarItemGroup {
HStack(spacing: 3) {
2022-11-27 10:42:16 +00:00
subscriptionsLabel
viewsLabel
}
2022-05-29 18:26:56 +00:00
2022-11-27 10:42:16 +00:00
if let contentItem = presentedChannel?.contentItem {
ShareButton(contentItem: contentItem)
}
subscriptionToggleButton
2022-11-27 10:42:16 +00:00
.layoutPriority(2)
2023-02-26 18:14:06 +00:00
favoriteButton
.labelStyle(.iconOnly)
toggleWatchedButton
.labelStyle(.iconOnly)
}
2022-11-27 10:42:16 +00:00
#endif
}
#endif
2021-11-08 16:29:35 +00:00
.onAppear {
2022-12-19 00:37:09 +00:00
descriptionExpanded = expandChannelDescription
2022-12-13 23:07:32 +00:00
if let channel,
let cache = ChannelsCacheModel.shared.retrieve(channel.cacheKey),
store.item.isNil
{
store.replace(cache)
}
load()
2021-11-08 16:29:35 +00:00
}
2022-11-27 10:42:16 +00:00
.onChange(of: contentType) { _ in
load()
2022-11-27 10:42:16 +00:00
}
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
2022-05-29 18:26:56 +00:00
#if !os(tvOS)
2021-11-08 16:29:35 +00:00
.navigationTitle(navigationTitle)
2022-05-29 18:26:56 +00:00
#endif
2021-11-28 14:37:55 +00:00
return Group {
if #available(macOS 12.0, *) {
content
#if os(tvOS)
.background(Color.background(scheme: colorScheme))
#endif
2021-11-28 14:37:55 +00:00
#if !os(iOS)
.focusScope(focusNamespace)
#endif
} else {
content
}
}
}
2023-02-26 18:14:06 +00:00
@ViewBuilder var favoriteButton: some View {
if let presentedChannel {
FavoriteButton(item: FavoriteItem(section: .channel(accounts.app.appType.rawValue, presentedChannel.id, presentedChannel.name)))
}
}
2022-11-27 10:42:16 +00:00
var thumbnail: some View {
ChannelAvatarView(channel: store.item?.channel)
2022-11-27 10:42:16 +00:00
#if os(tvOS)
.frame(width: 80, height: 80, alignment: .trailing)
2022-11-27 10:42:16 +00:00
#else
.frame(width: 30, height: 30, alignment: .trailing)
2022-11-27 10:42:16 +00:00
#endif
}
2022-05-29 18:26:56 +00:00
2022-11-27 10:42:16 +00:00
@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 {
Group {
if let subscribers = store.item?.channel?.subscriptionsString {
HStack(spacing: 0) {
Image(systemName: "person.2.fill")
Text(subscribers)
}
} else if store.item.isNil {
HStack(spacing: 0) {
Image(systemName: "person.2.fill")
Text("1234")
.redacted(reason: .placeholder)
}
2022-11-27 10:42:16 +00:00
}
}
.imageScale(.small)
2022-11-27 10:42:16 +00:00
.foregroundColor(.secondary)
}
var viewsLabel: some View {
HStack(spacing: 0) {
if let views = store.item?.channel?.totalViewsString {
2022-11-27 10:42:16 +00:00
Image(systemName: "eye.fill")
.imageScale(.small)
Text(views)
2022-11-27 10:42:16 +00:00
}
}
.foregroundColor(.secondary)
}
#if !os(tvOS)
var channelMenu: some View {
Menu {
if let channel = presentedChannel {
contentTypePicker
Section {
subscriptionToggleButton
2022-12-11 15:00:20 +00:00
FavoriteButton(item: FavoriteItem(section: .channel(accounts.app.appType.rawValue, channel.id, channel.name)))
2022-11-27 10:42:16 +00:00
}
2022-12-12 00:18:29 +00:00
if subscriptions.isSubscribing(channel.id) {
toggleWatchedButton
}
2022-12-12 00:18:29 +00:00
ListingStyleButtons(listingStyle: $channelPlaylistListingStyle)
2023-02-25 15:42:18 +00:00
Section {
HideShortsButtons(hide: $hideShorts)
}
2022-11-27 10:42:16 +00:00
}
} label: {
HStack(spacing: 12) {
thumbnail
VStack(alignment: .leading) {
Text(presentedChannel?.name ?? "Channel")
.font(.headline)
.foregroundColor(.primary)
.layoutPriority(1)
.frame(minWidth: 160, alignment: .leading)
2022-11-27 10:42:16 +00:00
Group {
HStack(spacing: 12) {
subscriptionsLabel
if presentedChannel?.verified ?? false {
Image(systemName: "checkmark.seal.fill")
.imageScale(.small)
2022-11-27 10:42:16 +00:00
}
viewsLabel
}
.frame(minWidth: 160, alignment: .leading)
2022-11-27 10:42:16 +00:00
}
.font(.caption2.bold())
.foregroundColor(.secondary)
}
Image(systemName: "chevron.down.circle.fill")
.foregroundColor(.accentColor)
.imageScale(.small)
}
.frame(maxWidth: 320)
2022-11-27 10:42:16 +00:00
}
}
#endif
private var contentTypePicker: some View {
Picker("Content type", selection: $contentType) {
2023-04-22 13:08:33 +00:00
if presentedChannel != nil {
2022-12-04 12:01:05 +00:00
ForEach(Channel.ContentType.allCases, id: \.self) { type in
if typeAvailable(type) {
2022-12-04 12:01:05 +00:00
Label(type.description, systemImage: type.systemImage).tag(type)
}
2022-11-27 10:42:16 +00:00
}
}
}
.labelsHidden()
2022-11-27 10:42:16 +00:00
}
private func typeAvailable(_ type: Channel.ContentType) -> Bool {
type.alwaysAvailable || (presentedChannel?.hasData(for: type) ?? false)
}
2022-11-27 10:42:16 +00:00
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)
2022-11-27 10:42:16 +00:00
if contentType == .videos {
resource.addObserver(store)
}
resource.addObserver(contentTypeItems)
2021-09-25 08:18:22 +00:00
return resource
}
2022-05-29 18:26:56 +00:00
@ViewBuilder private var subscriptionToggleButton: some View {
if let channel = presentedChannel {
Group {
if accounts.app.supportsSubscriptions && accounts.signedIn {
if subscriptions.isSubscribing(channel.id) {
2022-08-21 22:37:52 +00:00
Button {
2022-05-29 18:26:56 +00:00
subscriptionToggleButtonDisabled = true
subscriptions.unsubscribe(channel.id) {
subscriptionToggleButtonDisabled = false
}
2022-08-21 22:37:52 +00:00
} label: {
Label("Unsubscribe", systemImage: "xmark.circle")
2022-08-21 22:37:52 +00:00
#if os(iOS)
.labelStyle(.automatic)
#else
.labelStyle(.titleOnly)
#endif
2022-03-26 13:37:55 +00:00
}
2022-05-29 18:26:56 +00:00
} else {
2022-08-21 22:37:52 +00:00
Button {
2022-05-29 18:26:56 +00:00
subscriptionToggleButtonDisabled = true
subscriptions.subscribe(channel.id) {
subscriptionToggleButtonDisabled = false
navigation.sidebarSectionChanged.toggle()
}
2022-08-21 22:37:52 +00:00
} label: {
2022-08-23 15:10:14 +00:00
Label("Subscribe", systemImage: "circle")
2022-08-21 22:37:52 +00:00
#if os(iOS)
.labelStyle(.automatic)
#else
.labelStyle(.titleOnly)
#endif
2021-10-20 22:21:50 +00:00
}
}
}
}
2022-05-29 18:26:56 +00:00
.disabled(subscriptionToggleButtonDisabled)
2021-08-29 21:36:18 +00:00
}
}
2021-10-26 22:59:59 +00:00
private var navigationTitle: String {
2022-11-27 10:42:16 +00:00
presentedChannel?.name ?? "No channel"
}
@ViewBuilder var toggleWatchedButton: some View {
if let channel = presentedChannel {
if feed.canMarkChannelAsWatched(channel.id) {
markChannelAsWatchedButton
} else {
markChannelAsUnwatchedButton
}
}
}
var markChannelAsWatchedButton: some View {
Button {
guard let channel = presentedChannel else { return }
feed.markChannelAsWatched(channel.id)
} label: {
Label("Mark channel feed as watched", systemImage: "checkmark.circle.fill")
}
.disabled(!feed.canMarkAllFeedAsWatched)
}
var markChannelAsUnwatchedButton: some View {
Button {
guard let channel = presentedChannel else { return }
feed.markChannelAsUnwatched(channel.id)
} label: {
Label("Mark channel feed as unwatched", systemImage: "checkmark.circle")
}
}
func load() {
2023-04-22 13:08:33 +00:00
resource?
.load()
.onSuccess { response in
if let page: ChannelPage = response.typedContent() {
if let channel = page.channel {
ChannelsCacheModel.shared.store(channel)
}
self.page = page
self.contentTypeItems.replace(page.results)
}
}
2023-04-22 13:08:33 +00:00
.onFailure { error in
navigation.presentAlert(title: "Could not load channel data", message: error.userMessage)
}
}
func loadNextPage() {
guard let channel = presentedChannel, let pageToLoad = page, !pageToLoad.last else {
return
}
var next = pageToLoad.nextPage
if contentType == .videos, !pageToLoad.last {
next = next ?? ""
}
let data = contentType != .videos ? channel.tabs.first(where: { $0.contentType == contentType })?.data : nil
accounts.api.channel(channel.id, contentType: contentType, data: data, page: next).load().onSuccess { response in
if let page: ChannelPage = response.typedContent() {
self.page = page
let keys = self.contentTypeItems.collection.map(\.cacheKey)
let items = self.contentTypeItems.collection + page.results.filter { !keys.contains($0.cacheKey) }
self.contentTypeItems.replace(items)
}
}
}
2021-08-29 21:36:18 +00:00
}
2022-08-23 15:10:14 +00:00
struct ChannelVideosView_Previews: PreviewProvider {
static var previews: some View {
2022-12-17 13:24:09 +00:00
#if os(macOS)
ChannelVideosView(channel: Video.fixture.channel, showCloseButton: true, inNavigationView: false)
2022-11-27 10:42:16 +00:00
.environment(\.navigationStyle, .sidebar)
2022-12-17 13:24:09 +00:00
#else
NavigationView {
ChannelVideosView(channel: Video.fixture.channel)
}
#endif
2022-08-23 15:10:14 +00:00
}
}