yattee/Shared/Subscriptions/FeedView.swift

330 lines
12 KiB
Swift
Raw Normal View History

import Defaults
import Siesta
import SwiftUI
struct FeedView: View {
@ObservedObject private var feed = FeedModel.shared
@ObservedObject private var subscriptions = SubscribedChannelsModel.shared
@ObservedObject private var feedCount = UnwatchedFeedCountModel.shared
2022-12-13 11:09:20 +00:00
@Default(.showCacheStatus) private var showCacheStatus
2022-12-12 00:18:29 +00:00
#if os(tvOS)
@Default(.subscriptionsListingStyle) private var subscriptionsListingStyle
@StateObject private var accountsModel = AccountsViewModel()
2022-12-12 00:18:29 +00:00
#endif
var videos: [ContentItem] {
guard let selectedChannel = selectedChannel else {
return ContentItem.array(of: feed.videos)
}
return ContentItem.array(of: feed.videos.filter {
$0.channel.id == selectedChannel.id
})
}
2024-06-13 17:05:09 +00:00
var channels: [Channel] {
feed.videos.map {
$0.channel
}.unique()
}
2024-06-13 17:05:09 +00:00
@State private var selectedChannel: Channel?
2024-06-13 17:11:15 +00:00
#if os(tvOS)
2024-07-06 09:48:49 +00:00
@FocusState private var focusedChannel: String?
2024-06-13 17:11:15 +00:00
#endif
@State private var feedChannelsViewVisible = false
private var navigation = NavigationModel.shared
private let dismiss_channel_list_id = "dismiss_channel_list_id"
var body: some View {
#if os(tvOS)
2024-06-13 17:05:09 +00:00
GeometryReader { geometry in
ZStack {
// selected channel feed view
HStack(spacing: 0) {
// sidebar - show channels
2024-06-13 17:05:09 +00:00
if feedChannelsViewVisible {
Spacer()
.frame(width: geometry.size.width * 0.3)
}
selectedFeedView
}
2024-06-13 17:05:09 +00:00
.disabled(feedChannelsViewVisible)
.frame(width: geometry.size.width, height: geometry.size.height)
2024-06-13 17:05:09 +00:00
if feedChannelsViewVisible {
HStack(spacing: 0) {
// sidebar - show channels
feedChannelsView
.padding(.all)
.frame(width: geometry.size.width * 0.3)
.background()
.clipShape(RoundedRectangle(cornerRadius: 16))
.contentShape(RoundedRectangle(cornerRadius: 16))
Rectangle()
.fill(.clear)
.id(dismiss_channel_list_id)
.focusable()
.focused(self.$focusedChannel, equals: dismiss_channel_list_id)
}
.frame(width: geometry.size.width, height: geometry.size.height)
.clipped()
}
}
}
#else
selectedFeedView
#endif
}
2024-07-06 09:48:49 +00:00
2024-06-13 17:11:15 +00:00
#if os(tvOS)
2024-07-06 09:48:49 +00:00
var accountsPicker: some View {
ForEach(accountsModel.sortedAccounts.filter { $0.anonymous == false }) { account in
Button(action: {
AccountsModel.shared.setCurrent(account)
}) {
HStack {
Text("\(account.description) (\(account.instance.app.rawValue))")
if account == accountsModel.currentAccount {
Image(systemName: "checkmark")
}
}
}
2024-07-06 09:48:49 +00:00
.buttonStyle(PlainButtonStyle())
}
}
2024-06-13 17:05:09 +00:00
2024-07-06 09:48:49 +00:00
var feedChannelsView: some View {
ScrollViewReader { proxy in
VStack {
Text("Channels")
.font(.subheadline)
if #available(tvOS 17.0, *) {
List(selection: $selectedChannel) {
Button(action: {
2024-07-06 09:48:49 +00:00
self.selectedChannel = nil
self.feedChannelsViewVisible = false
}) {
HStack(spacing: 16) {
2024-07-06 09:48:49 +00:00
Image(systemName: RecentsModel.symbolSystemImage("A"))
.imageScale(.large)
.foregroundColor(.accentColor)
.frame(width: 35, height: 35)
Text("All")
Spacer()
2024-07-06 09:48:49 +00:00
feedCount.unwatchedText
}
}
.padding(.all)
.background(RoundedRectangle(cornerRadius: 8.0)
2024-07-06 09:48:49 +00:00
.fill(self.selectedChannel == nil ? Color.secondary : Color.clear))
.font(.caption)
.buttonStyle(PlainButtonStyle())
2024-07-06 09:48:49 +00:00
.focused(self.$focusedChannel, equals: "all")
ForEach(channels, id: \.self) { channel in
Button(action: {
self.selectedChannel = channel
self.feedChannelsViewVisible = false
}) {
HStack(spacing: 16) {
ChannelAvatarView(channel: channel, subscribedBadge: false)
.frame(width: 50, height: 50)
Text(channel.name)
.lineLimit(1)
Spacer()
if let unwatchedCount = feedCount.unwatchedByChannelText(channel) {
unwatchedCount
}
}
}
.padding(.all)
.background(RoundedRectangle(cornerRadius: 8.0)
.fill(self.selectedChannel == channel ? Color.secondary : Color.clear))
.font(.caption)
.buttonStyle(PlainButtonStyle())
.focused(self.$focusedChannel, equals: channel.id)
}
2024-07-06 09:48:49 +00:00
}
.onChange(of: self.focusedChannel) {
if self.focusedChannel == "all" {
withAnimation {
self.selectedChannel = nil
}
} else if self.focusedChannel == dismiss_channel_list_id {
self.feedChannelsViewVisible = false
} else {
withAnimation {
self.selectedChannel = channels.first {
$0.id == self.focusedChannel
}
}
}
}
2024-07-06 09:48:49 +00:00
.onAppear {
guard let selectedChannel = self.selectedChannel else {
return
}
proxy.scrollTo(selectedChannel, anchor: .top)
}
2024-07-06 09:48:49 +00:00
.onExitCommand {
withAnimation {
self.feedChannelsViewVisible = false
}
}
}
}
}
}
2024-06-13 17:11:15 +00:00
#endif
2024-06-13 17:05:09 +00:00
var selectedFeedView: some View {
VerticalCells(items: videos) { if shouldDisplayHeader { header } }
2022-12-13 19:15:00 +00:00
.environment(\.loadMoreContentHandler) { feed.loadNextPage() }
.onAppear {
feed.loadResources()
}
#if os(iOS)
.refreshControl { refreshControl in
feed.loadResources(force: true) {
refreshControl.endRefreshing()
}
}
.backport
2022-12-13 19:15:00 +00:00
.refreshable {
2023-10-15 11:46:30 +00:00
await feed.loadResources(force: true)
}
#endif
#if !os(tvOS)
2023-06-17 12:09:51 +00:00
.background(
Button("Refresh") {
feed.loadResources(force: true)
}
.keyboardShortcut("r")
.opacity(0)
)
#endif
#if !os(macOS)
2023-06-17 12:09:51 +00:00
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
feed.loadResources()
}
#endif
}
2022-12-13 19:15:00 +00:00
var header: some View {
HStack(spacing: 16) {
2022-12-13 19:15:00 +00:00
#if os(tvOS)
if #available(tvOS 17.0, *) {
Menu {
accountsPicker
} label: {
Label("Channels", systemImage: "filemenu.and.selection")
.labelStyle(.iconOnly)
.imageScale(.small)
.font(.caption)
} primaryAction: {
withAnimation {
self.feedChannelsViewVisible = true
self.focusedChannel = selectedChannel?.id ?? "all"
}
}
.opacity(feedChannelsViewVisible ? 0 : 1)
.frame(minWidth: feedChannelsViewVisible ? 0 : nil, maxWidth: feedChannelsViewVisible ? 0 : nil)
}
2024-06-13 17:05:09 +00:00
channelHeaderView
if selectedChannel == nil {
Spacer()
}
if feedChannelsViewVisible == false {
ListingStyleButtons(listingStyle: $subscriptionsListingStyle)
HideWatchedButtons()
HideShortsButtons()
}
#endif
2022-12-13 19:15:00 +00:00
if feedChannelsViewVisible == false {
if showCacheStatus {
CacheStatusHeader(
refreshTime: feed.formattedFeedTime,
isLoading: feed.isLoading
)
2022-12-13 19:15:00 +00:00
}
2024-06-13 17:05:09 +00:00
#if os(tvOS)
Button {
feed.loadResources(force: true)
} label: {
Label("Refresh", systemImage: "arrow.clockwise")
.labelStyle(.iconOnly)
.imageScale(.small)
.font(.caption)
}
#endif
}
2022-12-13 19:15:00 +00:00
}
.padding(.leading, 30)
#if os(tvOS)
.padding(.bottom, 15)
2023-02-25 15:42:18 +00:00
.padding(.trailing, 30)
2022-12-13 19:15:00 +00:00
#endif
}
2024-06-13 17:05:09 +00:00
var channelHeaderView: some View {
2024-06-13 17:05:09 +00:00
guard let selectedChannel = selectedChannel else {
return AnyView(
Text("All Channels")
2024-06-13 17:05:09 +00:00
.font(.caption)
.frame(alignment: .leading)
.lineLimit(1)
.padding(0)
.padding(.leading, 16)
)
}
2024-06-13 17:05:09 +00:00
return AnyView(
2024-06-13 17:05:09 +00:00
HStack(spacing: 16) {
ChannelAvatarView(channel: selectedChannel, subscribedBadge: false)
.id("channel-avatar-\(selectedChannel.id)")
.frame(width: 80, height: 80)
Text("\(selectedChannel.name)")
.font(.caption)
.frame(alignment: .leading)
.lineLimit(1)
.fixedSize(horizontal: true, vertical: false)
Spacer()
if feedChannelsViewVisible == false {
Button(action: {
navigation.openChannel(selectedChannel, navigationStyle: .tab)
}) {
Text("Visit Channel")
.font(.caption)
.frame(alignment: .leading)
.lineLimit(1)
.fixedSize(horizontal: true, vertical: false)
}
}
2024-06-13 17:05:09 +00:00
}
.padding(0)
.padding(.leading, 16)
)
}
2022-12-13 19:15:00 +00:00
var shouldDisplayHeader: Bool {
#if os(tvOS)
true
#else
showCacheStatus
#endif
}
}
2023-02-25 15:42:18 +00:00
struct FeedView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
FeedView()
}
}
}