1
0
mirror of https://github.com/yattee/yattee.git synced 2024-12-14 06:10:32 +05:30
yattee/Shared/Channels/ChannelVideosView.swift

478 lines
16 KiB
Swift
Raw Normal View History

2022-12-12 05:48:29 +05:30
import Defaults
2022-11-27 16:12:16 +05:30
import SDWebImageSwiftUI
2021-08-30 03:06:18 +05:30
import Siesta
import SwiftUI
struct ChannelVideosView: View {
2023-05-29 20:01:01 +05:30
var channel: Channel
var showCloseButton = false
var inNavigationView = true
2021-08-30 03:06:18 +05:30
2021-10-27 04:29:59 +05:30
@State private var presentingShareSheet = false
2021-11-13 21:15:47 +05:30
@State private var shareURL: URL?
2022-03-26 19:07:55 +05:30
@State private var subscriptionToggleButtonDisabled = false
2021-10-27 04:29:59 +05:30
@State private var page: ChannelPage?
2022-11-27 16:12:16 +05:30
@State private var contentType = Channel.ContentType.videos
@StateObject private var contentTypeItems = Store<[ContentItem]>()
2022-12-17 18:54:09 +05:30
@State private var descriptionExpanded = false
@StateObject private var store = Store<ChannelPage>()
2021-09-25 13:48:22 +05:30
@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 20:45:42 +05:30
@ObservedObject private var subscriptions = SubscribedChannelsModel.shared
@Namespace private var focusNamespace
2021-08-30 03:06:18 +05:30
2022-12-12 05:48:29 +05:30
@Default(.channelPlaylistListingStyle) private var channelPlaylistListingStyle
2022-12-19 06:07:09 +05:30
@Default(.expandChannelDescription) private var expandChannelDescription
2022-12-12 05:48:29 +05:30
2022-05-29 23:56:56 +05:30
var presentedChannel: Channel? {
2023-05-29 20:01:01 +05:30
store.item?.channel ?? channel
2022-05-29 23:56:56 +05:30
}
2022-11-27 16:12:16 +05:30
var contentItems: [ContentItem] {
2023-04-22 14:26:18 +05:30
contentTypeItems.collection
}
2021-08-30 03:06:18 +05:30
var body: some View {
2021-11-28 20:07:55 +05:30
let content = VStack {
#if os(tvOS)
2022-11-27 16:12:16 +05:30
VStack {
HStack(spacing: 24) {
thumbnail
2022-11-27 16:12:16 +05:30
Text(navigationTitle)
.font(.headline)
2022-11-27 16:12:16 +05:30
.frame(alignment: .leading)
2022-11-27 16:12:16 +05:30
Spacer()
2021-11-02 03:26:18 +05:30
2022-11-27 16:12:16 +05:30
subscriptionsLabel
viewsLabel
2022-11-27 16:12:16 +05:30
subscriptionToggleButton
2023-02-26 23:44:06 +05:30
favoriteButton
.labelStyle(.iconOnly)
2022-11-27 16:12:16 +05:30
}
contentTypePicker
.pickerStyle(.automatic)
}
.frame(maxWidth: .infinity)
#endif
2023-05-27 04:44:48 +05:30
VerticalCells(items: contentItems, isLoading: resource?.isLoading ?? false, edgesIgnoringSafeArea: verticalCellsEdgesIgnoringSafeArea) {
2022-12-17 18:54:09 +05:30
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-19 00:13:16 +05:30
.padding(.bottom, 10)
2022-12-17 18:54:09 +05:30
}
.buttonStyle(.plain)
} else {
banner
}
2022-11-27 16:12:16 +05:30
}
.environment(\.loadMoreContentHandler) { loadNextPage() }
2022-11-27 16:12:16 +05:30
.environment(\.inChannelView, true)
2022-12-12 05:48:29 +05:30
.environment(\.listingStyle, channelPlaylistListingStyle)
#if os(tvOS)
.prefersDefaultFocus(in: focusNamespace)
#endif
}
2021-11-28 20:07:55 +05:30
2021-08-30 03:06:18 +05:30
#if !os(tvOS)
.toolbar {
2022-11-27 16:12:16 +05:30
#if os(iOS)
ToolbarItem(placement: .principal) {
channelMenu
}
#endif
2022-08-26 13:28:08 +05:30
ToolbarItem(placement: .cancellationAction) {
if showCloseButton {
2022-08-26 13:28:08 +05:30
Button {
2022-08-25 22:39:55 +05:30
withAnimation(Constants.overlayAnimation) {
2022-07-09 05:51:04 +05:30
navigation.presentingChannel = false
navigation.presentingChannelSheet = false
2022-07-09 05:51:04 +05:30
}
2022-08-26 13:28:08 +05:30
} label: {
Label("Close", systemImage: "xmark")
2022-05-29 23:56:56 +05:30
}
#if !os(macOS)
2022-11-27 16:12:16 +05:30
.buttonStyle(.plain)
#endif
2022-05-29 23:56:56 +05:30
}
}
#if os(macOS)
2022-11-27 16:12:16 +05:30
ToolbarItem(placement: .navigation) {
thumbnail
}
ToolbarItemGroup {
if !inNavigationView {
Text(navigationTitle)
.fontWeight(.bold)
}
2022-12-12 05:48:29 +05:30
ListingStyleButtons(listingStyle: $channelPlaylistListingStyle)
HideWatchedButtons()
2023-05-23 22:24:53 +05:30
HideShortsButtons()
2022-11-27 16:12:16 +05:30
contentTypePicker
}
2021-10-27 04:29:59 +05:30
2023-04-23 02:14:59 +05:30
ToolbarItemGroup {
HStack(spacing: 3) {
2022-11-27 16:12:16 +05:30
subscriptionsLabel
viewsLabel
}
2022-05-29 23:56:56 +05:30
2022-11-27 16:12:16 +05:30
if let contentItem = presentedChannel?.contentItem {
ShareButton(contentItem: contentItem)
}
subscriptionToggleButton
2022-11-27 16:12:16 +05:30
.layoutPriority(2)
2023-02-26 23:44:06 +05:30
favoriteButton
.labelStyle(.iconOnly)
toggleWatchedButton
.labelStyle(.iconOnly)
}
2022-11-27 16:12:16 +05:30
#endif
}
#endif
2021-11-08 21:59:35 +05:30
.onAppear {
2022-12-19 06:07:09 +05:30
descriptionExpanded = expandChannelDescription
2023-05-29 20:01:01 +05:30
if let cache = ChannelsCacheModel.shared.retrieve(channel.cacheKey), store.item.isNil {
2022-12-14 04:37:32 +05:30
store.replace(cache)
}
load()
2021-11-08 21:59:35 +05:30
}
2022-11-27 16:12:16 +05:30
.onChange(of: contentType) { _ in
load()
2022-11-27 16:12:16 +05:30
}
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
2022-05-29 23:56:56 +05:30
#if !os(tvOS)
2021-11-08 21:59:35 +05:30
.navigationTitle(navigationTitle)
2022-05-29 23:56:56 +05:30
#endif
2021-11-28 20:07:55 +05:30
return Group {
if #available(macOS 12.0, *) {
content
#if os(tvOS)
.background(Color.background(scheme: colorScheme))
#endif
2021-11-28 20:07:55 +05:30
#if !os(iOS)
.focusScope(focusNamespace)
#endif
} else {
content
}
}
}
2023-05-23 02:22:36 +05:30
var verticalCellsEdgesIgnoringSafeArea: Edge.Set {
#if os(tvOS)
return .horizontal
#else
return .init()
#endif
}
2023-02-26 23:44:06 +05:30
@ViewBuilder var favoriteButton: some View {
if let presentedChannel {
FavoriteButton(item: FavoriteItem(section: .channel(accounts.app.appType.rawValue, presentedChannel.id, presentedChannel.name)))
}
}
2022-11-27 16:12:16 +05:30
var thumbnail: some View {
ChannelAvatarView(channel: store.item?.channel)
2022-11-27 16:12:16 +05:30
#if os(tvOS)
.frame(width: 80, height: 80, alignment: .trailing)
2022-11-27 16:12:16 +05:30
#else
.frame(width: 30, height: 30, alignment: .trailing)
2022-11-27 16:12:16 +05:30
#endif
}
2022-05-29 23:56:56 +05:30
2022-11-27 16:12:16 +05:30
@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 16:12:16 +05:30
}
}
.imageScale(.small)
2022-11-27 16:12:16 +05:30
.foregroundColor(.secondary)
}
var viewsLabel: some View {
HStack(spacing: 0) {
if let views = store.item?.channel?.totalViewsString {
2022-11-27 16:12:16 +05:30
Image(systemName: "eye.fill")
.imageScale(.small)
Text(views)
2022-11-27 16:12:16 +05:30
}
}
.foregroundColor(.secondary)
}
#if !os(tvOS)
var channelMenu: some View {
Menu {
if let channel = presentedChannel {
contentTypePicker
Section {
subscriptionToggleButton
2022-12-11 20:30:20 +05:30
FavoriteButton(item: FavoriteItem(section: .channel(accounts.app.appType.rawValue, channel.id, channel.name)))
2022-11-27 16:12:16 +05:30
}
2022-12-12 05:48:29 +05:30
if subscriptions.isSubscribing(channel.id) {
toggleWatchedButton
}
2022-12-12 05:48:29 +05:30
ListingStyleButtons(listingStyle: $channelPlaylistListingStyle)
2023-02-25 21:12:18 +05:30
Section {
HideWatchedButtons()
2023-05-23 22:24:53 +05:30
HideShortsButtons()
2023-02-25 21:12:18 +05:30
}
2022-11-27 16:12:16 +05:30
}
} 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 16:12:16 +05:30
Group {
HStack(spacing: 12) {
subscriptionsLabel
if presentedChannel?.verified ?? false {
Image(systemName: "checkmark.seal.fill")
.imageScale(.small)
2022-11-27 16:12:16 +05:30
}
viewsLabel
}
.frame(minWidth: 160, alignment: .leading)
2022-11-27 16:12:16 +05:30
}
.font(.caption2.bold())
.foregroundColor(.secondary)
}
Image(systemName: "chevron.down.circle.fill")
.foregroundColor(.accentColor)
.imageScale(.small)
}
.frame(maxWidth: 320)
2022-11-27 16:12:16 +05:30
}
}
#endif
private var contentTypePicker: some View {
Picker("Content type", selection: $contentType) {
2023-04-22 18:38:33 +05:30
if presentedChannel != nil {
2022-12-04 17:31:05 +05:30
ForEach(Channel.ContentType.allCases, id: \.self) { type in
if typeAvailable(type) {
2022-12-04 17:31:05 +05:30
Label(type.description, systemImage: type.systemImage).tag(type)
}
2022-11-27 16:12:16 +05:30
}
}
}
.labelsHidden()
2022-11-27 16:12:16 +05:30
}
private func typeAvailable(_ type: Channel.ContentType) -> Bool {
type.alwaysAvailable || (presentedChannel?.hasData(for: type) ?? false)
}
2022-11-27 16:12:16 +05:30
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 16:12:16 +05:30
if contentType == .videos {
resource.addObserver(store)
}
resource.addObserver(contentTypeItems)
2021-09-25 13:48:22 +05:30
return resource
}
2022-05-29 23:56:56 +05:30
@ViewBuilder private var subscriptionToggleButton: some View {
if let channel = presentedChannel {
Group {
if accounts.app.supportsSubscriptions && accounts.signedIn {
if subscriptions.isSubscribing(channel.id) {
2022-08-22 04:07:52 +05:30
Button {
2022-05-29 23:56:56 +05:30
subscriptionToggleButtonDisabled = true
subscriptions.unsubscribe(channel.id) {
subscriptionToggleButtonDisabled = false
}
2022-08-22 04:07:52 +05:30
} label: {
Label("Unsubscribe", systemImage: "xmark.circle")
2022-08-22 04:07:52 +05:30
#if os(iOS)
.labelStyle(.automatic)
#else
.labelStyle(.titleOnly)
#endif
2022-03-26 19:07:55 +05:30
}
2022-05-29 23:56:56 +05:30
} else {
2022-08-22 04:07:52 +05:30
Button {
2022-05-29 23:56:56 +05:30
subscriptionToggleButtonDisabled = true
subscriptions.subscribe(channel.id) {
subscriptionToggleButtonDisabled = false
navigation.sidebarSectionChanged.toggle()
}
2022-08-22 04:07:52 +05:30
} label: {
2022-08-23 20:40:14 +05:30
Label("Subscribe", systemImage: "circle")
2022-08-22 04:07:52 +05:30
#if os(iOS)
.labelStyle(.automatic)
#else
.labelStyle(.titleOnly)
#endif
2021-10-21 03:51:50 +05:30
}
}
}
}
2022-05-29 23:56:56 +05:30
.disabled(subscriptionToggleButtonDisabled)
2021-08-30 03:06:18 +05:30
}
}
2021-10-27 04:29:59 +05:30
private var navigationTitle: String {
2022-11-27 16:12:16 +05:30
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 18:38:33 +05:30
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 18:38:33 +05:30
.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-30 03:06:18 +05:30
}
2022-08-23 20:40:14 +05:30
struct ChannelVideosView_Previews: PreviewProvider {
static var previews: some View {
2022-12-17 18:54:09 +05:30
#if os(macOS)
ChannelVideosView(channel: Video.fixture.channel, showCloseButton: true, inNavigationView: false)
2022-11-27 16:12:16 +05:30
.environment(\.navigationStyle, .sidebar)
2022-12-17 18:54:09 +05:30
#else
NavigationView {
ChannelVideosView(channel: Video.fixture.channel)
}
#endif
2022-08-23 20:40:14 +05:30
}
}