mirror of
https://github.com/yattee/yattee.git
synced 2024-12-22 21:43:41 +00:00
94577332a1
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
328 lines
12 KiB
Swift
328 lines
12 KiB
Swift
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
|
|
|
|
@Default(.showCacheStatus) private var showCacheStatus
|
|
|
|
#if os(tvOS)
|
|
@Default(.subscriptionsListingStyle) private var subscriptionsListingStyle
|
|
@StateObject private var accountsModel = AccountsViewModel()
|
|
#endif
|
|
|
|
var videos: [ContentItem] {
|
|
guard let selectedChannel else {
|
|
return ContentItem.array(of: feed.videos)
|
|
}
|
|
return ContentItem.array(of: feed.videos.filter {
|
|
$0.channel.id == selectedChannel.id
|
|
})
|
|
}
|
|
|
|
var channels: [Channel] {
|
|
feed.videos.map(\.channel).unique()
|
|
}
|
|
|
|
@State private var selectedChannel: Channel?
|
|
#if os(tvOS)
|
|
@FocusState private var focusedChannel: String?
|
|
#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)
|
|
GeometryReader { geometry in
|
|
ZStack {
|
|
// selected channel feed view
|
|
HStack(spacing: 0) {
|
|
// sidebar - show channels
|
|
if feedChannelsViewVisible {
|
|
Spacer()
|
|
.frame(width: geometry.size.width * 0.3)
|
|
}
|
|
selectedFeedView
|
|
}
|
|
.disabled(feedChannelsViewVisible)
|
|
.frame(width: geometry.size.width, height: geometry.size.height)
|
|
|
|
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
|
|
}
|
|
|
|
#if os(tvOS)
|
|
|
|
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")
|
|
}
|
|
}
|
|
}
|
|
.buttonStyle(PlainButtonStyle())
|
|
}
|
|
}
|
|
|
|
var feedChannelsView: some View {
|
|
ScrollViewReader { proxy in
|
|
VStack {
|
|
Text("Channels")
|
|
.font(.subheadline)
|
|
if #available(tvOS 17.0, *) {
|
|
List(selection: $selectedChannel) {
|
|
Button(action: {
|
|
self.selectedChannel = nil
|
|
self.feedChannelsViewVisible = false
|
|
}) {
|
|
HStack(spacing: 16) {
|
|
Image(systemName: RecentsModel.symbolSystemImage("A"))
|
|
.imageScale(.large)
|
|
.foregroundColor(.accentColor)
|
|
.frame(width: 35, height: 35)
|
|
Text("All")
|
|
Spacer()
|
|
feedCount.unwatchedText
|
|
}
|
|
}
|
|
.padding(.all)
|
|
.background(RoundedRectangle(cornerRadius: 8.0)
|
|
.fill(self.selectedChannel == nil ? Color.secondary : Color.clear))
|
|
.font(.caption)
|
|
.buttonStyle(PlainButtonStyle())
|
|
.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)
|
|
}
|
|
}
|
|
.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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.onAppear {
|
|
guard let selectedChannel = self.selectedChannel else {
|
|
return
|
|
}
|
|
proxy.scrollTo(selectedChannel, anchor: .top)
|
|
}
|
|
.onExitCommand {
|
|
withAnimation {
|
|
self.feedChannelsViewVisible = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
var selectedFeedView: some View {
|
|
VerticalCells(items: videos) { if shouldDisplayHeader { header } }
|
|
.environment(\.loadMoreContentHandler) { feed.loadNextPage() }
|
|
.onAppear {
|
|
feed.loadResources()
|
|
}
|
|
#if os(iOS)
|
|
.refreshControl { refreshControl in
|
|
feed.loadResources(force: true) {
|
|
refreshControl.endRefreshing()
|
|
}
|
|
}
|
|
.backport
|
|
.refreshable {
|
|
await feed.loadResources(force: true)
|
|
}
|
|
#endif
|
|
#if !os(tvOS)
|
|
.background(
|
|
Button("Refresh") {
|
|
feed.loadResources(force: true)
|
|
}
|
|
.keyboardShortcut("r")
|
|
.opacity(0)
|
|
)
|
|
#endif
|
|
#if !os(macOS)
|
|
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
|
|
feed.loadResources()
|
|
}
|
|
#endif
|
|
}
|
|
|
|
var header: some View {
|
|
HStack(spacing: 16) {
|
|
#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)
|
|
}
|
|
channelHeaderView
|
|
if selectedChannel == nil {
|
|
Spacer()
|
|
}
|
|
if feedChannelsViewVisible == false {
|
|
ListingStyleButtons(listingStyle: $subscriptionsListingStyle)
|
|
HideWatchedButtons()
|
|
HideShortsButtons()
|
|
}
|
|
#endif
|
|
|
|
if feedChannelsViewVisible == false {
|
|
if showCacheStatus {
|
|
CacheStatusHeader(
|
|
refreshTime: feed.formattedFeedTime,
|
|
isLoading: feed.isLoading
|
|
)
|
|
}
|
|
|
|
#if os(tvOS)
|
|
Button {
|
|
feed.loadResources(force: true)
|
|
} label: {
|
|
Label("Refresh", systemImage: "arrow.clockwise")
|
|
.labelStyle(.iconOnly)
|
|
.imageScale(.small)
|
|
.font(.caption)
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
.padding(.leading, 30)
|
|
#if os(tvOS)
|
|
.padding(.bottom, 15)
|
|
.padding(.trailing, 30)
|
|
#endif
|
|
}
|
|
|
|
var channelHeaderView: some View {
|
|
guard let selectedChannel else {
|
|
return AnyView(
|
|
Text("All Channels")
|
|
.font(.caption)
|
|
.frame(alignment: .leading)
|
|
.lineLimit(1)
|
|
.padding(0)
|
|
.padding(.leading, 16)
|
|
)
|
|
}
|
|
|
|
return AnyView(
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
.padding(0)
|
|
.padding(.leading, 16)
|
|
)
|
|
}
|
|
|
|
var shouldDisplayHeader: Bool {
|
|
#if os(tvOS)
|
|
true
|
|
#else
|
|
showCacheStatus
|
|
#endif
|
|
}
|
|
}
|
|
|
|
struct FeedView_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
NavigationView {
|
|
FeedView()
|
|
}
|
|
}
|
|
}
|