diff --git a/Fixtures/Comment+Fixtures.swift b/Fixtures/Comment+Fixtures.swift new file mode 100644 index 00000000..6b539d6c --- /dev/null +++ b/Fixtures/Comment+Fixtures.swift @@ -0,0 +1,18 @@ +import Foundation + +extension Comment { + static var fixture: Comment { + Comment( + id: UUID().uuidString, + author: "The Author", + authorAvatarURL: "https://pipedproxy-ams-2.kavin.rocks/Si7ZhtmpX84wj6MoJYLs8kwALw2Hm53wzbrPamoU-z3qvCKs2X3zPNYKMSJEvPDLUHzbvTfLcg=s176-c-k-c0x00ffffff-no-rw?host=yt3.ggpht.com", + time: "2 months ago", + pinned: true, + hearted: true, + likeCount: 30032, + text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut luctus feugiat mi, suscipit pharetra lectus dapibus vel. Vivamus orci erat, sagittis sit amet dui vel, feugiat cursus ante. Pellentesque eget orci tortor. Suspendisse pulvinar orci tortor, eu scelerisque neque consequat nec. Aliquam sit amet turpis et nunc placerat finibus eget sit amet justo. Nullam tincidunt ornare neque. Donec ornare, arcu at elementum pulvinar, urna elit pharetra diam, vel ultrices lacus diam at lorem. Sed vel maximus dolor. Morbi massa est, interdum quis justo sit amet, dapibus bibendum tellus. Integer at purus nec neque tincidunt convallis sit amet eu odio. Duis et ante vitae sem tincidunt facilisis sit amet ac mauris. Quisque varius non nisi vel placerat. Nulla orci metus, imperdiet ac accumsan sed, pellentesque eget nisl. Praesent a suscipit lacus, ut finibus orci. Nulla ut eros commodo, fermentum purus at, porta leo. In finibus luctus nulla, eget posuere eros mollis vel. ", + repliesPage: "some url", + channel: .init(id: "", name: "") + ) + } +} diff --git a/Fixtures/View+Fixtures.swift b/Fixtures/View+Fixtures.swift index 38fa11d9..80183d3d 100644 --- a/Fixtures/View+Fixtures.swift +++ b/Fixtures/View+Fixtures.swift @@ -5,6 +5,7 @@ struct FixtureEnvironmentObjectsModifier: ViewModifier { func body(content: Content) -> some View { content .environmentObject(AccountsModel()) + .environmentObject(CommentsModel()) .environmentObject(InstancesModel()) .environmentObject(invidious) .environmentObject(NavigationModel()) diff --git a/Model/Applications/InvidiousAPI.swift b/Model/Applications/InvidiousAPI.swift index 805c57ac..8dd15292 100644 --- a/Model/Applications/InvidiousAPI.swift +++ b/Model/Applications/InvidiousAPI.swift @@ -30,7 +30,6 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI { signedIn = false configure() - validate() } func validate() { @@ -257,6 +256,8 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI { .withParam("q", query.lowercased()) } + func comments(_: Video.ID, page _: String?) -> Resource? { nil } + private func searchQuery(_ query: String) -> String { var searchQuery = query diff --git a/Model/Applications/PipedAPI.swift b/Model/Applications/PipedAPI.swift index 73d7db36..99eced7a 100644 --- a/Model/Applications/PipedAPI.swift +++ b/Model/Applications/PipedAPI.swift @@ -71,6 +71,13 @@ final class PipedAPI: Service, ObservableObject, VideosAPI { content.json.arrayValue.map { PipedAPI.extractVideo(from: $0)! } } + configureTransformer(pathPattern("comments/*")) { (content: Entity) -> CommentsPage in + let comments = content.json.dictionaryValue["comments"]?.arrayValue.map { PipedAPI.extractComment(from: $0)! } ?? [] + let nextPage = content.json.dictionaryValue["nextpage"]?.stringValue + + return CommentsPage(comments: comments, nextPage: nextPage) + } + if account.token.isNil { updateToken() } @@ -80,9 +87,14 @@ final class PipedAPI: Service, ObservableObject, VideosAPI { PipedAPI.authorizedEndpoints.contains { url.absoluteString.contains($0) } } - @discardableResult func updateToken() -> Request { + func updateToken() { + guard !account.anonymous else { + return + } + account.token = nil - return login.request( + + login.request( .post, json: ["username": account.username, "password": account.password] ) @@ -161,6 +173,17 @@ final class PipedAPI: Service, ObservableObject, VideosAPI { func playlistVideo(_: String, _: String) -> Resource? { nil } func playlistVideos(_: String) -> Resource? { nil } + func comments(_ id: Video.ID, page: String?) -> Resource? { + let path = page.isNil ? "comments/\(id)" : "nextpage/comments/\(id)" + let resource = resource(baseURL: account.url, path: path) + + if page.isNil { + return resource + } + + return resource.withParam("nextpage", page) + } + private func pathPattern(_ path: String) -> String { "**\(path)" } @@ -395,4 +418,23 @@ final class PipedAPI: Service, ObservableObject, VideosAPI { .arrayValue .filter { $0.dictionaryValue["format"] == "MPEG_4" } ?? [] } + + private static func extractComment(from content: JSON) -> Comment? { + let details = content.dictionaryValue + let author = details["author"]?.stringValue ?? "" + let commentorUrl = details["commentorUrl"]?.stringValue + let channelId = commentorUrl?.components(separatedBy: "/")[2] ?? "" + return Comment( + id: details["commentId"]?.stringValue ?? UUID().uuidString, + author: author, + authorAvatarURL: details["thumbnail"]?.stringValue ?? "", + time: details["commentedTime"]?.stringValue ?? "", + pinned: details["pinned"]?.boolValue ?? false, + hearted: details["hearted"]?.boolValue ?? false, + likeCount: details["likeCount"]?.intValue ?? 0, + text: details["commentText"]?.stringValue ?? "", + repliesPage: details["repliesPage"]?.stringValue, + channel: Channel(id: channelId, name: author) + ) + } } diff --git a/Model/Applications/VideosAPI.swift b/Model/Applications/VideosAPI.swift index c103aec4..ade65e22 100644 --- a/Model/Applications/VideosAPI.swift +++ b/Model/Applications/VideosAPI.swift @@ -31,6 +31,8 @@ protocol VideosAPI { func loadDetails(_ item: PlayerQueueItem, completionHandler: @escaping (PlayerQueueItem) -> Void) func shareURL(_ item: ContentItem, frontendHost: String?, time: CMTime?) -> URL? + + func comments(_ id: Video.ID, page: String?) -> Resource? } extension VideosAPI { diff --git a/Model/Applications/VideosApp.swift b/Model/Applications/VideosApp.swift index 43135642..f4c58d4a 100644 --- a/Model/Applications/VideosApp.swift +++ b/Model/Applications/VideosApp.swift @@ -38,4 +38,8 @@ enum VideosApp: String, CaseIterable { var hasFrontendURL: Bool { self == .piped } + + var supportsComments: Bool { + self == .piped + } } diff --git a/Model/Comment.swift b/Model/Comment.swift new file mode 100644 index 00000000..faeec337 --- /dev/null +++ b/Model/Comment.swift @@ -0,0 +1,16 @@ +struct Comment: Identifiable, Equatable { + let id: String + let author: String + let authorAvatarURL: String + let time: String + let pinned: Bool + let hearted: Bool + var likeCount: Int + let text: String + let repliesPage: String? + let channel: Channel + + var hasReplies: Bool { + !(repliesPage?.isEmpty ?? true) + } +} diff --git a/Model/CommentsModel.swift b/Model/CommentsModel.swift new file mode 100644 index 00000000..7c553072 --- /dev/null +++ b/Model/CommentsModel.swift @@ -0,0 +1,79 @@ +import Defaults +import Foundation +import SwiftyJSON + +final class CommentsModel: ObservableObject { + @Published var all = [Comment]() + @Published var replies = [Comment]() + + @Published var nextPage: String? + @Published var firstPage = true + + @Published var loaded = false + + var accounts: AccountsModel! + var player: PlayerModel! + + static var enabled: Bool { + !Defaults[.commentsInstanceID].isNil + } + + var nextPageAvailable: Bool { + !(nextPage?.isEmpty ?? true) + } + + func load(page: String? = nil) { + guard Self.enabled else { + return + } + + loaded = false + clear() + + guard let instance = InstancesModel.find(Defaults[.commentsInstanceID]), + !player.currentVideo.isNil + else { + return + } + + firstPage = page.isNil || page!.isEmpty + + PipedAPI(account: instance.anonymousAccount).comments(player.currentVideo!.videoID, page: page)? + .load() + .onSuccess { [weak self] response in + if let page: CommentsPage = response.typedContent() { + self?.all = page.comments + self?.nextPage = page.nextPage + } + } + .onCompletion { [weak self] _ in + self?.loaded = true + } + } + + func loadNextPage() { + load(page: nextPage) + } + + func loadReplies(page: String) { + guard !player.currentVideo.isNil else { + return + } + + replies = [] + + accounts.api.comments(player.currentVideo!.videoID, page: page)?.load().onSuccess { response in + if let page: CommentsPage = response.typedContent() { + self.replies = page.comments + } + } + } + + func clear() { + all = [] + replies = [] + firstPage = true + nextPage = nil + loaded = false + } +} diff --git a/Model/CommentsPage.swift b/Model/CommentsPage.swift new file mode 100644 index 00000000..78128378 --- /dev/null +++ b/Model/CommentsPage.swift @@ -0,0 +1,6 @@ +import Foundation + +struct CommentsPage { + var comments = [Comment]() + var nextPage: String? +} diff --git a/Model/Player/PlayerModel.swift b/Model/Player/PlayerModel.swift index 28b7309c..0d730ea3 100644 --- a/Model/Player/PlayerModel.swift +++ b/Model/Player/PlayerModel.swift @@ -43,6 +43,7 @@ final class PlayerModel: ObservableObject { @Published var restoredSegments = [Segment]() var accounts: AccountsModel + var comments: CommentsModel var composition = AVMutableComposition() var loadedCompositionAssets = [AVMediaType]() @@ -67,8 +68,9 @@ final class PlayerModel: ObservableObject { #endif }} - init(accounts: AccountsModel? = nil, instances _: InstancesModel? = nil) { + init(accounts: AccountsModel? = nil, comments: CommentsModel? = nil) { self.accounts = accounts ?? AccountsModel() + self.comments = comments ?? CommentsModel() addItemDidPlayToEndTimeObserver() addFrequentTimeObserver() @@ -138,6 +140,7 @@ final class PlayerModel: ObservableObject { playerError = nil resetSegments() sponsorBlock.loadSegments(videoID: video.videoID, categories: Defaults[.sponsorBlockCategories]) + comments.load() if let url = stream.singleAssetURL { logger.info("playing stream with one asset\(stream.kind == .hls ? " (HLS)" : ""): \(url)") diff --git a/Model/Player/PlayerQueue.swift b/Model/Player/PlayerQueue.swift index 70545351..271a2805 100644 --- a/Model/Player/PlayerQueue.swift +++ b/Model/Player/PlayerQueue.swift @@ -37,6 +37,7 @@ extension PlayerModel { } func playItem(_ item: PlayerQueueItem, video: Video? = nil, at time: TimeInterval? = nil) { + comments.clear() currentItem = item if !time.isNil { diff --git a/README.md b/README.md index d04b3a7a..cc344cdf 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ Video player for [Invidious](https://github.com/iv-org/invidious) and [Piped](ht | Search Suggestions | ✅ | ✅ | | Search Filters | ✅ | 🔴 | | Subtitles | 🔴 | ✅ | +| Comments | 🔴 | ✅ | ## Installation ### Requirements diff --git a/Shared/Defaults.swift b/Shared/Defaults.swift index cc337c77..edf0a567 100644 --- a/Shared/Defaults.swift +++ b/Shared/Defaults.swift @@ -2,10 +2,11 @@ import Defaults import Foundation extension Defaults.Keys { + static let kavinPipedInstanceID = "kavin-piped" static let instances = Key<[Instance]>("instances", default: [ .init( app: .piped, - id: "default-piped-instance", + id: kavinPipedInstanceID, name: "Kavin", apiURL: "https://pipedapi.kavin.rocks", frontendURL: "https://piped.kavin.rocks" @@ -32,6 +33,7 @@ extension Defaults.Keys { static let playerSidebar = Key("playerSidebar", default: PlayerSidebarSetting.defaultValue) static let playerInstanceID = Key("playerInstance") static let showKeywords = Key("showKeywords", default: false) + static let commentsInstanceID = Key("commentsInstance", default: kavinPipedInstanceID) static let recentlyOpened = Key<[RecentItem]>("recentlyOpened", default: []) diff --git a/Shared/Navigation/AppSidebarNavigation.swift b/Shared/Navigation/AppSidebarNavigation.swift index 8b7478bf..dbb477f9 100644 --- a/Shared/Navigation/AppSidebarNavigation.swift +++ b/Shared/Navigation/AppSidebarNavigation.swift @@ -6,11 +6,19 @@ import SwiftUI struct AppSidebarNavigation: View { @EnvironmentObject private var accounts + @EnvironmentObject private var comments + @EnvironmentObject private var instances + @EnvironmentObject private var navigation + @EnvironmentObject private var player + @EnvironmentObject private var playlists + @EnvironmentObject private var recents + @EnvironmentObject private var search + @EnvironmentObject private var subscriptions + @EnvironmentObject private var thumbnailsModel @Default(.visibleSections) private var visibleSections #if os(iOS) - @EnvironmentObject private var navigation @State private var didApplyPrimaryViewWorkAround = false #endif @@ -42,15 +50,50 @@ struct AppSidebarNavigation: View { .frame(minWidth: sidebarMinWidth) VStack { - Image(systemName: "play.tv") - .renderingMode(.original) - .font(.system(size: 60)) - .foregroundColor(.accentColor) + PlayerControlsView { + HStack(alignment: .center) { + Spacer() + Image(systemName: "play.tv") + .renderingMode(.original) + .font(.system(size: 60)) + .foregroundColor(.accentColor) + Spacer() + } + } } } + #if os(iOS) + .background( + EmptyView().fullScreenCover(isPresented: $player.presentingPlayer) { + videoPlayer + .environment(\.navigationStyle, .sidebar) + } + ) + #elseif os(macOS) + .background( + EmptyView().sheet(isPresented: $player.presentingPlayer) { + videoPlayer + .frame(minWidth: 1000, minHeight: 750) + .environment(\.navigationStyle, .sidebar) + } + ) + #endif .environment(\.navigationStyle, .sidebar) } + private var videoPlayer: some View { + VideoPlayerView() + .environmentObject(accounts) + .environmentObject(comments) + .environmentObject(instances) + .environmentObject(navigation) + .environmentObject(player) + .environmentObject(playlists) + .environmentObject(recents) + .environmentObject(subscriptions) + .environmentObject(thumbnailsModel) + } + var toolbarContent: some ToolbarContent { Group { #if os(iOS) diff --git a/Shared/Navigation/AppTabNavigation.swift b/Shared/Navigation/AppTabNavigation.swift index 4aaae676..99b64d75 100644 --- a/Shared/Navigation/AppTabNavigation.swift +++ b/Shared/Navigation/AppTabNavigation.swift @@ -3,10 +3,15 @@ import SwiftUI struct AppTabNavigation: View { @EnvironmentObject private var accounts + @EnvironmentObject private var comments + @EnvironmentObject private var instances @EnvironmentObject private var navigation @EnvironmentObject private var player + @EnvironmentObject private var playlists @EnvironmentObject private var recents @EnvironmentObject private var search + @EnvironmentObject private var subscriptions + @EnvironmentObject private var thumbnailsModel @Default(.visibleSections) private var visibleSections @@ -67,6 +72,12 @@ struct AppTabNavigation: View { } } ) + .background( + EmptyView().fullScreenCover(isPresented: $player.presentingPlayer) { + videoPlayer + .environment(\.navigationStyle, .sidebar) + } + ) } private var favoritesNavigationView: some View { @@ -155,6 +166,19 @@ struct AppTabNavigation: View { } } + private var videoPlayer: some View { + VideoPlayerView() + .environmentObject(accounts) + .environmentObject(comments) + .environmentObject(instances) + .environmentObject(navigation) + .environmentObject(player) + .environmentObject(playlists) + .environmentObject(recents) + .environmentObject(subscriptions) + .environmentObject(thumbnailsModel) + } + var toolbarContent: some ToolbarContent { #if os(iOS) Group { diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift index ba61f0ac..45cc4aa7 100644 --- a/Shared/Navigation/ContentView.swift +++ b/Shared/Navigation/ContentView.swift @@ -8,6 +8,7 @@ import SwiftUI struct ContentView: View { @StateObject private var accounts = AccountsModel() + @StateObject private var comments = CommentsModel() @StateObject private var instances = InstancesModel() @StateObject private var navigation = NavigationModel() @StateObject private var player = PlayerModel() @@ -40,6 +41,7 @@ struct ContentView: View { .onAppear(perform: configure) .environmentObject(accounts) + .environmentObject(comments) .environmentObject(instances) .environmentObject(navigation) .environmentObject(player) @@ -58,20 +60,6 @@ struct ContentView: View { .environmentObject(navigation) } ) - #if os(iOS) - .background( - EmptyView().fullScreenCover(isPresented: $player.presentingPlayer) { - videoPlayer - } - ) - #elseif os(macOS) - .background( - EmptyView().sheet(isPresented: $player.presentingPlayer) { - videoPlayer - .frame(minWidth: 1000, minHeight: 750) - } - ) - #endif #if !os(tvOS) .handlesExternalEvents(preferring: Set(["*"]), allowing: Set(["*"])) .onOpenURL(perform: handleOpenedURL) @@ -98,17 +86,6 @@ struct ContentView: View { #endif } - private var videoPlayer: some View { - VideoPlayerView() - .environmentObject(accounts) - .environmentObject(instances) - .environmentObject(navigation) - .environmentObject(player) - .environmentObject(playlists) - .environmentObject(subscriptions) - .environmentObject(thumbnailsModel) - } - func configure() { SiestaLog.Category.enabled = .common SDImageCodersManager.shared.addCoder(SDImageWebPCoder.shared) @@ -128,15 +105,20 @@ struct ContentView: View { navigation.presentingWelcomeScreen = true } - player.accounts = accounts playlists.accounts = accounts search.accounts = accounts subscriptions.accounts = accounts + comments.accounts = accounts + comments.player = player + menu.accounts = accounts menu.navigation = navigation menu.player = player + player.accounts = accounts + player.comments = comments + if !accounts.current.isNil { player.loadHistoryDetails() } diff --git a/Shared/Player/CommentView.swift b/Shared/Player/CommentView.swift new file mode 100644 index 00000000..6dc16d95 --- /dev/null +++ b/Shared/Player/CommentView.swift @@ -0,0 +1,221 @@ +import SDWebImageSwiftUI + +import SwiftUI + +struct CommentView: View { + let comment: Comment + @Binding var repliesID: Comment.ID? + + #if os(iOS) + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + #endif + @Environment(\.navigationStyle) private var navigationStyle + + @EnvironmentObject private var comments + @EnvironmentObject private var navigation + @EnvironmentObject private var player + @EnvironmentObject private var recents + + var body: some View { + VStack(alignment: .leading) { + HStack(alignment: .center, spacing: 10) { + authorAvatar + + #if os(iOS) + Group { + if horizontalSizeClass == .regular { + HStack(spacing: 20) { + authorAndTime + + Spacer() + + Group { + statusIcons + likes + } + } + } else { + HStack(alignment: .center, spacing: 20) { + authorAndTime + + Spacer() + + VStack(spacing: 5) { + likes + statusIcons + } + } + } + } + .font(.system(size: 15)) + + #else + HStack(spacing: 20) { + authorAndTime + + Spacer() + + statusIcons + likes + } + #endif + } + + Group { + commentText + + if comment.hasReplies { + repliesButton + + if comment.id == repliesID { + repliesList + } + } + } + } + #if os(tvOS) + .padding(.horizontal, 20) + #endif + } + + private var authorAvatar: some View { + WebImage(url: URL(string: comment.authorAvatarURL)!) + .resizable() + .placeholder { + Rectangle().fill(Color("PlaceholderColor")) + } + .retryOnAppear(false) + .indicator(.activity) + .mask(RoundedRectangle(cornerRadius: 60)) + .frame(width: 45, height: 45, alignment: .leading) + .contextMenu { + Button(action: openChannelAction) { + Label("\(comment.channel.name) Channel", systemImage: "rectangle.stack.fill.badge.person.crop") + } + } + #if os(tvOS) + .focusable() + #endif + } + + private var authorAndTime: some View { + VStack(alignment: .leading) { + Text(comment.author) + .fontWeight(.bold) + + Text(comment.time) + .foregroundColor(.secondary) + } + .lineLimit(1) + } + + private var statusIcons: some View { + HStack(spacing: 15) { + if comment.pinned { + Image(systemName: "pin.fill") + } + if comment.hearted { + Image(systemName: "heart.fill") + } + } + .foregroundColor(.secondary) + } + + private var likes: some View { + Group { + if comment.likeCount > 0 { + HStack(spacing: 5) { + Image(systemName: "hand.thumbsup") + Text("\(comment.likeCount.formattedAsAbbreviation())") + } + } + } + .foregroundColor(.secondary) + } + + private var repliesButton: some View { + Button { + repliesID = repliesID == comment.id ? nil : comment.id + + if repliesID.isNil { + comments.replies = [] + } + + guard !repliesID.isNil, !comment.repliesPage.isNil else { + return + } + + comments.loadReplies(page: comment.repliesPage!) + } label: { + HStack(spacing: 5) { + Image(systemName: self.repliesID == comment.id ? "arrow.turn.left.up" : "arrow.turn.right.down") + Text("Replies") + } + #if os(tvOS) + .padding(10) + #endif + } + + .buttonStyle(.plain) + .padding(.top, 2) + #if os(tvOS) + .padding(.leading, 5) + #else + .foregroundColor(.secondary) + #endif + } + + private var repliesList: some View { + Group { + let last = comments.replies.last + ForEach(comments.replies) { comment in + CommentView(comment: comment, repliesID: $repliesID) + #if os(tvOS) + .focusable() + #endif + + if comment != last { + Divider() + .padding(.vertical, 5) + } + } + .padding(.leading, 22) + } + } + + private var commentText: some View { + Group { + let text = Text(comment.text) + #if os(macOS) + .font(.system(size: 14)) + #elseif os(iOS) + .font(.system(size: 15)) + #endif + .lineSpacing(3) + .fixedSize(horizontal: false, vertical: true) + + if #available(iOS 15.0, macOS 12.0, *) { + text + #if !os(tvOS) + .textSelection(.enabled) + #endif + } else { + text + } + } + } + + private func openChannelAction() { + player.presentingPlayer = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + let recent = RecentItem(from: comment.channel) + recents.add(recent) + navigation.presentingChannel = true + + if navigationStyle == .sidebar { + navigation.sidebarSectionChanged.toggle() + navigation.tabSelection = .recentlyOpened(recent.tag) + } + } + } +} diff --git a/Shared/Player/CommentsView.swift b/Shared/Player/CommentsView.swift new file mode 100644 index 00000000..9e63e2d6 --- /dev/null +++ b/Shared/Player/CommentsView.swift @@ -0,0 +1,59 @@ +import SwiftUI + +struct CommentsView: View { + @State private var repliesID: Comment.ID? + + @EnvironmentObject private var comments + @EnvironmentObject private var player + + var body: some View { + ScrollView(.vertical, showsIndicators: false) { + VStack(alignment: .leading) { + let last = comments.all.last + ForEach(comments.all) { comment in + CommentView(comment: comment, repliesID: $repliesID) + + if comment != last { + Divider() + .padding(.vertical, 5) + } + } + + HStack { + if comments.nextPageAvailable { + Button { + comments.loadNextPage() + } label: { + Label("Show more", systemImage: "arrow.turn.down.right") + } + } + + if !comments.firstPage { + Button { + comments.load(page: nil) + } label: { + Label("Show first", systemImage: "arrow.turn.down.left") + } + } + } + .buttonStyle(.plain) + .padding(.vertical, 5) + .foregroundColor(.secondary) + } + } + .padding(.horizontal) + } +} + +struct CommentsView_Previews: PreviewProvider { + static var previews: some View { + if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) { + CommentsView() + .previewInterfaceOrientation(.landscapeRight) + .injectFixtureEnvironmentObjects() + } + + CommentsView() + .injectFixtureEnvironmentObjects() + } +} diff --git a/Shared/Player/Player.swift b/Shared/Player/Player.swift index 4c39209b..c0b204b5 100644 --- a/Shared/Player/Player.swift +++ b/Shared/Player/Player.swift @@ -2,6 +2,7 @@ import Defaults import SwiftUI struct Player: UIViewControllerRepresentable { + @EnvironmentObject private var comments @EnvironmentObject private var navigation @EnvironmentObject private var player @@ -18,6 +19,7 @@ struct Player: UIViewControllerRepresentable { let controller = PlayerViewController() + controller.commentsModel = comments controller.navigationModel = navigation controller.playerModel = player player.controller = controller diff --git a/Shared/Player/PlayerViewController.swift b/Shared/Player/PlayerViewController.swift index 255ff7a1..27b1a9cd 100644 --- a/Shared/Player/PlayerViewController.swift +++ b/Shared/Player/PlayerViewController.swift @@ -4,6 +4,7 @@ import SwiftUI final class PlayerViewController: UIViewController { var playerLoaded = false + var commentsModel: CommentsModel! var navigationModel: NavigationModel! var playerModel: PlayerModel! var playerViewController = AVPlayerViewController() @@ -45,6 +46,7 @@ final class PlayerViewController: UIViewController { #if os(tvOS) playerModel.avPlayerViewController = playerViewController playerViewController.customInfoViewControllers = [ + infoViewController([.comments], title: "Comments"), infoViewController([.related], title: "Related"), infoViewController([.playingNext, .playedPreviously], title: "Playing Next") ] @@ -62,6 +64,7 @@ final class PlayerViewController: UIViewController { AnyView( NowPlayingView(sections: sections, inInfoViewController: true) .frame(maxHeight: 600) + .environmentObject(commentsModel) .environmentObject(playerModel) ) ) diff --git a/Shared/Player/VideoDetails.swift b/Shared/Player/VideoDetails.swift index 1d206d7e..793afb10 100644 --- a/Shared/Player/VideoDetails.swift +++ b/Shared/Player/VideoDetails.swift @@ -4,7 +4,7 @@ import SwiftUI struct VideoDetails: View { enum Page { - case details, queue, related + case info, queue, related, comments } @Binding var sidebarQueue: Bool @@ -16,7 +16,7 @@ struct VideoDetails: View { @State private var presentingShareSheet = false @State private var shareURL: URL? - @State private var currentPage = Page.details + @State private var currentPage = Page.info @Environment(\.presentationMode) private var presentationMode @Environment(\.inNavigationView) private var inNavigationView @@ -65,7 +65,7 @@ struct VideoDetails: View { } .padding(.horizontal) - if !sidebarQueue { + if CommentsModel.enabled { pagePicker .padding(.horizontal) } @@ -89,7 +89,7 @@ struct VideoDetails: View { ) switch currentPage { - case .details: + case .info: ScrollView(.vertical) { detailsPage } @@ -100,6 +100,9 @@ struct VideoDetails: View { case .related: RelatedView() .edgesIgnoringSafeArea(.horizontal) + case .comments: + CommentsView() + .edgesIgnoringSafeArea(.horizontal) } } .padding(.top, inNavigationView && fullScreen ? 10 : 0) @@ -116,7 +119,7 @@ struct VideoDetails: View { .onChange(of: sidebarQueue) { queue in if queue { if currentPage == .queue { - currentPage = .details + currentPage = .info } } else if video.isNil { currentPage = .queue @@ -131,7 +134,7 @@ struct VideoDetails: View { if video != nil { Text(video!.title) .onAppear { - currentPage = .details + currentPage = .info } .contextMenu { Button { @@ -239,15 +242,23 @@ struct VideoDetails: View { var pagePicker: some View { Picker("Page", selection: $currentPage) { if !video.isNil { - Text("Details").tag(Page.details) - Text("Related").tag(Page.related) + Text("Info").tag(Page.info) + if !sidebarQueue { + Text("Related").tag(Page.related) + } + if CommentsModel.enabled { + Text("Comments") + .tag(Page.comments) + } + } + if !sidebarQueue { + Text("Queue").tag(Page.queue) } - Text("Queue").tag(Page.queue) } .labelsHidden() .pickerStyle(.segmented) .onDisappear { - currentPage = .details + currentPage = .info } } @@ -297,19 +308,19 @@ struct VideoDetails: View { Spacer() if let views = video.viewsCount { - videoDetail(label: "Views", value: views, symbol: "eye.fill") + videoDetail(label: "Views", value: views, symbol: "eye") } if let likes = video.likesCount { Divider() - videoDetail(label: "Likes", value: likes, symbol: "hand.thumbsup.circle.fill") + videoDetail(label: "Likes", value: likes, symbol: "hand.thumbsup") } if let dislikes = video.dislikesCount { Divider() - videoDetail(label: "Dislikes", value: dislikes, symbol: "hand.thumbsdown.circle.fill") + videoDetail(label: "Dislikes", value: dislikes, symbol: "hand.thumbsdown") } Spacer() @@ -378,7 +389,8 @@ struct VideoDetails: View { } } .frame(maxWidth: .infinity, alignment: .leading) - .font(.caption) + .font(.system(size: 14)) + .lineSpacing(3) .padding(.bottom, 4) } else { Text("No description") diff --git a/Shared/Search/SearchField.swift b/Shared/Search/SearchField.swift index 50e39b55..ecaec21b 100644 --- a/Shared/Search/SearchField.swift +++ b/Shared/Search/SearchField.swift @@ -6,6 +6,12 @@ struct SearchTextField: View { @EnvironmentObject private var recents @EnvironmentObject private var state + @Binding var favoriteItem: FavoriteItem? + + init(favoriteItem: Binding? = nil) { + _favoriteItem = favoriteItem ?? .constant(nil) + } + var body: some View { ZStack { #if os(macOS) @@ -37,7 +43,14 @@ struct SearchTextField: View { .padding(.leading) .padding(.trailing, 15) #endif + if !self.state.queryText.isEmpty { + #if os(iOS) + FavoriteButton(item: favoriteItem) + .id(favoriteItem?.id) + .labelStyle(.iconOnly) + .padding(.trailing) + #endif clearButton } } diff --git a/Shared/Search/SearchView.swift b/Shared/Search/SearchView.swift index 4312a5c5..b38e148a 100644 --- a/Shared/Search/SearchView.swift +++ b/Shared/Search/SearchView.swift @@ -42,7 +42,7 @@ struct SearchView: View { PlayerControlsView { #if os(iOS) VStack { - SearchTextField() + SearchTextField(favoriteItem: $favoriteItem) if state.query.query != state.queryText, !state.queryText.isEmpty, !state.querySuggestions.collection.isEmpty { SearchSuggestions() @@ -93,15 +93,6 @@ struct SearchView: View { .transaction { t in t.animation = .none } } - #if os(iOS) - Spacer() - - FavoriteButton(item: favoriteItem) - .id(favoriteItem?.id) - - Spacer() - #endif - if accounts.app.supportsSearchFilters { filtersMenu } diff --git a/Shared/Settings/PlaybackSettings.swift b/Shared/Settings/PlaybackSettings.swift index 85488743..04d7aff7 100644 --- a/Shared/Settings/PlaybackSettings.swift +++ b/Shared/Settings/PlaybackSettings.swift @@ -57,7 +57,7 @@ struct PlaybackSettings: View { Text("Best available stream").tag(String?.none) ForEach(instances) { instance in - Text(instance.longDescription).tag(Optional(instance.id)) + Text(instance.description).tag(Optional(instance.id)) } } .labelsHidden() diff --git a/Shared/Settings/ServicesSettings.swift b/Shared/Settings/ServicesSettings.swift index 52069682..02125638 100644 --- a/Shared/Settings/ServicesSettings.swift +++ b/Shared/Settings/ServicesSettings.swift @@ -4,8 +4,13 @@ import SwiftUI struct ServicesSettings: View { @Default(.sponsorBlockInstance) private var sponsorBlockInstance @Default(.sponsorBlockCategories) private var sponsorBlockCategories + @Default(.commentsInstanceID) private var commentsInstanceID var body: some View { + Section(header: SettingsHeader(text: "Comments")) { + commentsInstancePicker + } + Section(header: SettingsHeader(text: "SponsorBlock API")) { TextField( "SponsorBlock API Instance", @@ -52,6 +57,22 @@ struct ServicesSettings: View { } } + private var commentsInstancePicker: some View { + Picker("Comments", selection: $commentsInstanceID) { + Text("Disabled").tag(String?.none) + + ForEach(InstancesModel.all.filter { $0.app.supportsComments }) { instance in + Text(instance.description).tag(Optional(instance.id)) + } + } + .labelsHidden() + #if os(iOS) + .pickerStyle(.automatic) + #elseif os(tvOS) + .pickerStyle(.inline) + #endif + } + func toggleCategory(_ category: String, value: Bool) { if let index = sponsorBlockCategories.firstIndex(where: { $0 == category }), !value { sponsorBlockCategories.remove(at: index) diff --git a/Shared/Views/ChannelVideosView.swift b/Shared/Views/ChannelVideosView.swift index c6ae41ca..efaa09e9 100644 --- a/Shared/Views/ChannelVideosView.swift +++ b/Shared/Views/ChannelVideosView.swift @@ -90,9 +90,14 @@ struct ChannelVideosView: View { ToolbarItem { HStack { - Text("**\(store.item?.subscriptionsString ?? "loading")** subscribers") - .foregroundColor(.secondary) - .opacity(store.item?.subscriptionsString != nil ? 1 : 0) + HStack(spacing: 3) { + Text("\(store.item?.subscriptionsString ?? "loading")") + .fontWeight(.bold) + Text(" subscribers") + } + .allowsTightening(true) + .foregroundColor(.secondary) + .opacity(store.item?.subscriptionsString != nil ? 1 : 0) subscriptionToggleButton diff --git a/Shared/Views/SignInRequiredView.swift b/Shared/Views/SignInRequiredView.swift index d8585ebc..6fe4422c 100644 --- a/Shared/Views/SignInRequiredView.swift +++ b/Shared/Views/SignInRequiredView.swift @@ -34,9 +34,9 @@ struct SignInRequiredView: View { Group { if instances.isEmpty { - Text("You need to create an instance and accounts\nto access **\(title)** section") + Text("You need to create an instance and accounts\nto access \(title) section") } else { - Text("You need to select an account\nto access **\(title)** section") + Text("You need to select an account\nto access \(title) section") } } .multilineTextAlignment(.center) diff --git a/Yattee.xcodeproj/project.pbxproj b/Yattee.xcodeproj/project.pbxproj index 9f362862..eb343925 100644 --- a/Yattee.xcodeproj/project.pbxproj +++ b/Yattee.xcodeproj/project.pbxproj @@ -74,6 +74,20 @@ 37169AA62729E2CC0011DE61 /* AccountsBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37169AA52729E2CC0011DE61 /* AccountsBridge.swift */; }; 37169AA72729E2CC0011DE61 /* AccountsBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37169AA52729E2CC0011DE61 /* AccountsBridge.swift */; }; 37169AA82729E2CC0011DE61 /* AccountsBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37169AA52729E2CC0011DE61 /* AccountsBridge.swift */; }; + 371B7E5C27596B8400D21217 /* Comment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371B7E5B27596B8400D21217 /* Comment.swift */; }; + 371B7E5D27596B8400D21217 /* Comment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371B7E5B27596B8400D21217 /* Comment.swift */; }; + 371B7E5E27596B8400D21217 /* Comment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371B7E5B27596B8400D21217 /* Comment.swift */; }; + 371B7E5F27596B8400D21217 /* Comment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371B7E5B27596B8400D21217 /* Comment.swift */; }; + 371B7E612759706A00D21217 /* CommentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371B7E602759706A00D21217 /* CommentsView.swift */; }; + 371B7E622759706A00D21217 /* CommentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371B7E602759706A00D21217 /* CommentsView.swift */; }; + 371B7E632759706A00D21217 /* CommentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371B7E602759706A00D21217 /* CommentsView.swift */; }; + 371B7E642759706A00D21217 /* CommentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371B7E602759706A00D21217 /* CommentsView.swift */; }; + 371B7E662759786B00D21217 /* Comment+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371B7E652759786B00D21217 /* Comment+Fixtures.swift */; }; + 371B7E672759786B00D21217 /* Comment+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371B7E652759786B00D21217 /* Comment+Fixtures.swift */; }; + 371B7E682759786B00D21217 /* Comment+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371B7E652759786B00D21217 /* Comment+Fixtures.swift */; }; + 371B7E6A2759791900D21217 /* CommentsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371B7E692759791900D21217 /* CommentsModel.swift */; }; + 371B7E6B2759791900D21217 /* CommentsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371B7E692759791900D21217 /* CommentsModel.swift */; }; + 371B7E6C2759791900D21217 /* CommentsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371B7E692759791900D21217 /* CommentsModel.swift */; }; 371F2F1A269B43D300E4A7AB /* NavigationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371F2F19269B43D300E4A7AB /* NavigationModel.swift */; }; 371F2F1B269B43D300E4A7AB /* NavigationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371F2F19269B43D300E4A7AB /* NavigationModel.swift */; }; 371F2F1C269B43D300E4A7AB /* NavigationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371F2F19269B43D300E4A7AB /* NavigationModel.swift */; }; @@ -92,6 +106,10 @@ 37319F0527103F94004ECCD0 /* PlayerQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37319F0427103F94004ECCD0 /* PlayerQueue.swift */; }; 37319F0627103F94004ECCD0 /* PlayerQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37319F0427103F94004ECCD0 /* PlayerQueue.swift */; }; 37319F0727103F94004ECCD0 /* PlayerQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37319F0427103F94004ECCD0 /* PlayerQueue.swift */; }; + 373C8FE4275B955100CB5936 /* CommentsPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373C8FE3275B955100CB5936 /* CommentsPage.swift */; }; + 373C8FE5275B955100CB5936 /* CommentsPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373C8FE3275B955100CB5936 /* CommentsPage.swift */; }; + 373C8FE6275B955100CB5936 /* CommentsPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373C8FE3275B955100CB5936 /* CommentsPage.swift */; }; + 373C8FE7275B955100CB5936 /* CommentsPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373C8FE3275B955100CB5936 /* CommentsPage.swift */; }; 373CFACB26966264003CB2C6 /* SearchQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFACA26966264003CB2C6 /* SearchQuery.swift */; }; 373CFACC26966264003CB2C6 /* SearchQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFACA26966264003CB2C6 /* SearchQuery.swift */; }; 373CFACD26966264003CB2C6 /* SearchQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFACA26966264003CB2C6 /* SearchQuery.swift */; }; @@ -489,6 +507,10 @@ 37EF5C222739D37B00B03725 /* MenuModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF5C212739D37B00B03725 /* MenuModel.swift */; }; 37EF5C232739D37B00B03725 /* MenuModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF5C212739D37B00B03725 /* MenuModel.swift */; }; 37EF5C242739D37B00B03725 /* MenuModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF5C212739D37B00B03725 /* MenuModel.swift */; }; + 37EF9A76275BEB8E0043B585 /* CommentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF9A75275BEB8E0043B585 /* CommentView.swift */; }; + 37EF9A77275BEB8E0043B585 /* CommentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF9A75275BEB8E0043B585 /* CommentView.swift */; }; + 37EF9A78275BEB8E0043B585 /* CommentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF9A75275BEB8E0043B585 /* CommentView.swift */; }; + 37EF9A79275BEB8E0043B585 /* CommentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF9A75275BEB8E0043B585 /* CommentView.swift */; }; 37F49BA326CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */; }; 37F49BA426CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */; }; 37F49BA526CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */; }; @@ -561,6 +583,10 @@ 37152EE926EFEB95004FB96D /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = ""; }; 37169AA12729D98A0011DE61 /* InstancesBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesBridge.swift; sourceTree = ""; }; 37169AA52729E2CC0011DE61 /* AccountsBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsBridge.swift; sourceTree = ""; }; + 371B7E5B27596B8400D21217 /* Comment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Comment.swift; sourceTree = ""; }; + 371B7E602759706A00D21217 /* CommentsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentsView.swift; sourceTree = ""; }; + 371B7E652759786B00D21217 /* Comment+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Comment+Fixtures.swift"; sourceTree = ""; }; + 371B7E692759791900D21217 /* CommentsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentsModel.swift; sourceTree = ""; }; 371F2F19269B43D300E4A7AB /* NavigationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModel.swift; sourceTree = ""; }; 3722AEBB274DA396005EA4D6 /* Badge+Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Badge+Backport.swift"; sourceTree = ""; }; 3722AEBD274DA401005EA4D6 /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = ""; }; @@ -570,6 +596,7 @@ 3730D89F2712E2B70020ED53 /* NowPlayingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingView.swift; sourceTree = ""; }; 373197D82732015300EF734F /* RelatedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelatedView.swift; sourceTree = ""; }; 37319F0427103F94004ECCD0 /* PlayerQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerQueue.swift; sourceTree = ""; }; + 373C8FE3275B955100CB5936 /* CommentsPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentsPage.swift; sourceTree = ""; }; 373CFACA26966264003CB2C6 /* SearchQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchQuery.swift; sourceTree = ""; }; 373CFADA269663F1003CB2C6 /* Thumbnail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thumbnail.swift; sourceTree = ""; }; 373CFAEA26975CBF003CB2C6 /* PlaylistFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistFormView.swift; sourceTree = ""; }; @@ -713,6 +740,7 @@ 37EAD86A267B9C5600D9E01B /* SponsorBlockAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockAPI.swift; sourceTree = ""; }; 37EAD86E267B9ED100D9E01B /* Segment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Segment.swift; sourceTree = ""; }; 37EF5C212739D37B00B03725 /* MenuModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuModel.swift; sourceTree = ""; }; + 37EF9A75275BEB8E0043B585 /* CommentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentView.swift; sourceTree = ""; }; 37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Playlist+Fixtures.swift"; sourceTree = ""; }; 37F4AE7126828F0900BD60EA /* VerticalCells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalCells.swift; sourceTree = ""; }; 37F64FE326FE70A60081B69E /* RedrawOnModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedrawOnModifier.swift; sourceTree = ""; }; @@ -835,6 +863,8 @@ 371AAE2426CEBA4100901972 /* Player */ = { isa = PBXGroup; children = ( + 371B7E602759706A00D21217 /* CommentsView.swift */, + 37EF9A75275BEB8E0043B585 /* CommentView.swift */, 37B81B0126D2CAE700675966 /* PlaybackBar.swift */, 37BE0BD226A1D4780092E2DB /* Player.swift */, 3743CA4D270EFE3400E4D32B /* PlayerQueueRow.swift */, @@ -957,6 +987,7 @@ isa = PBXGroup; children = ( 37C3A24C272360470087A57A /* ChannelPlaylist+Fixtures.swift */, + 371B7E652759786B00D21217 /* Comment+Fixtures.swift */, 376CD21526FBE18D001E1AC1 /* Instance+Fixtures.swift */, 37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */, 3748186926A764FB0084E870 /* Thumbnail+Fixtures.swift */, @@ -1212,6 +1243,9 @@ 374C0539272436DA009BDDBE /* SponsorBlock */, 37AAF28F26740715007FC770 /* Channel.swift */, 37C3A24427235DA70087A57A /* ChannelPlaylist.swift */, + 371B7E5B27596B8400D21217 /* Comment.swift */, + 371B7E692759791900D21217 /* CommentsModel.swift */, + 373C8FE3275B955100CB5936 /* CommentsPage.swift */, 37FB28402721B22200A57617 /* ContentItem.swift */, 37141672267A8E10006CA35D /* Country.swift */, 37599F2F272B42810087F250 /* FavoriteItem.swift */, @@ -1722,6 +1756,8 @@ files = ( 374710052755291C00CE0F87 /* SearchField.swift in Sources */, 37CEE4BD2677B670005A1EFE /* SingleAssetStream.swift in Sources */, + 371B7E612759706A00D21217 /* CommentsView.swift in Sources */, + 371B7E6A2759791900D21217 /* CommentsModel.swift in Sources */, 37CC3F45270CE30600608308 /* PlayerQueueItem.swift in Sources */, 37BD07C82698B71C003EBB87 /* AppTabNavigation.swift in Sources */, 37599F38272B4D740087F250 /* FavoriteButton.swift in Sources */, @@ -1752,6 +1788,7 @@ 37B81AF926D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */, 37C0698227260B2100F7F6CB /* ThumbnailsModel.swift in Sources */, 37DD87C7271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */, + 371B7E662759786B00D21217 /* Comment+Fixtures.swift in Sources */, 37BE0BD326A1D4780092E2DB /* Player.swift in Sources */, 37A9965E26D6F9B9006E3224 /* FavoritesView.swift in Sources */, 37CEE4C12677B697005A1EFE /* Stream.swift in Sources */, @@ -1779,6 +1816,8 @@ 3782B9522755667600990149 /* String+Format.swift in Sources */, 373CFAEF2697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */, 37BF661F27308884008CCFB0 /* DropFavoriteOutside.swift in Sources */, + 37EF9A76275BEB8E0043B585 /* CommentView.swift in Sources */, + 373C8FE4275B955100CB5936 /* CommentsPage.swift in Sources */, 3700155B271B0D4D0049C794 /* PipedAPI.swift in Sources */, 37B044B726F7AB9000E1419D /* SettingsView.swift in Sources */, 377FC7E3267A084A00A6BBAF /* VideoCell.swift in Sources */, @@ -1805,6 +1844,7 @@ 376B2E0726F920D600B1D64D /* SignInRequiredView.swift in Sources */, 37C0697E2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */, 3743CA4E270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */, + 371B7E5C27596B8400D21217 /* Comment.swift in Sources */, 37BD672426F13D65004BE0C1 /* AppSidebarPlaylists.swift in Sources */, 37B17DA2268A1F8A006AEE9B /* VideoContextMenuView.swift in Sources */, 3784B23B272894DA00B09468 /* ShareSheet.swift in Sources */, @@ -1876,6 +1916,7 @@ 37C3A24E272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */, 37CEE4C22677B697005A1EFE /* Stream.swift in Sources */, 3782B95027553A6700990149 /* SearchSuggestions.swift in Sources */, + 371B7E6B2759791900D21217 /* CommentsModel.swift in Sources */, 371F2F1B269B43D300E4A7AB /* NavigationModel.swift in Sources */, 37001564271B1F250049C794 /* AccountsModel.swift in Sources */, 3761ABFE26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */, @@ -1925,6 +1966,7 @@ 37AAF29126740715007FC770 /* Channel.swift in Sources */, 376A33E12720CAD6000C1D6B /* VideosApp.swift in Sources */, 37BD07BC2698AB60003EBB87 /* AppSidebarNavigation.swift in Sources */, + 37EF9A77275BEB8E0043B585 /* CommentView.swift in Sources */, 37BF661D27308859008CCFB0 /* DropFavorite.swift in Sources */, 3748186F26A769D60084E870 /* DetailBadge.swift in Sources */, 372915E72687E3B900F5A35B /* Defaults.swift in Sources */, @@ -1956,6 +1998,7 @@ 37319F0627103F94004ECCD0 /* PlayerQueue.swift in Sources */, 37B767DC2677C3CA0098BAA8 /* PlayerModel.swift in Sources */, 3797758C2689345500DD52A8 /* Store.swift in Sources */, + 371B7E622759706A00D21217 /* CommentsView.swift in Sources */, 37141674267A8E10006CA35D /* Country.swift in Sources */, 37FD43E42704847C0073EE42 /* View+Fixtures.swift in Sources */, 37C069782725962F00F7F6CB /* ScreenSaverManager.swift in Sources */, @@ -1967,12 +2010,14 @@ 3700155C271B0D4D0049C794 /* PipedAPI.swift in Sources */, 376BE50C27349108009AD608 /* BrowsingSettings.swift in Sources */, 37D4B19826717E1500C925CA /* Video.swift in Sources */, + 371B7E5D27596B8400D21217 /* Comment.swift in Sources */, 37EF5C232739D37B00B03725 /* MenuModel.swift in Sources */, 37599F31272B42810087F250 /* FavoriteItem.swift in Sources */, 3730F75A2733481E00F385FC /* RelatedView.swift in Sources */, 37E04C0F275940FB00172673 /* VerticalScrollingFix.swift in Sources */, 375168D72700FDB8008F96A6 /* Debounce.swift in Sources */, 37D526DF2720AC4400ED2F5E /* VideosAPI.swift in Sources */, + 373C8FE5275B955100CB5936 /* CommentsPage.swift in Sources */, 37D4B0E52671614900C925CA /* YatteeApp.swift in Sources */, 37BD07C12698AD3B003EBB87 /* TrendingCountry.swift in Sources */, 37BA794026DB8F97002A0235 /* ChannelVideosView.swift in Sources */, @@ -1993,6 +2038,7 @@ 37C3A24A27235FAA0087A57A /* ChannelPlaylistCell.swift in Sources */, 373CFAF02697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */, 3763495226DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */, + 371B7E672759786B00D21217 /* Comment+Fixtures.swift in Sources */, 37CB127A2724C76D00213B45 /* VideoURLParser.swift in Sources */, 37BA794426DBA973002A0235 /* PlaylistsModel.swift in Sources */, ); @@ -2012,6 +2058,7 @@ files = ( 3774124C27387D2300423605 /* RecentsModel.swift in Sources */, 3774122A27387B6C00423605 /* InstancesModelTests.swift in Sources */, + 371B7E642759706A00D21217 /* CommentsView.swift in Sources */, 3774124927387D2300423605 /* Channel.swift in Sources */, 3774125727387D2300423605 /* FavoriteItem.swift in Sources */, 3774126B27387D6D00423605 /* CMTime+DefaultTimescale.swift in Sources */, @@ -2021,6 +2068,7 @@ 3774126827387D6D00423605 /* Double+Format.swift in Sources */, 3774126E27387D8800423605 /* PlayerQueueItem.swift in Sources */, 3774125627387D2300423605 /* Segment.swift in Sources */, + 373C8FE7275B955100CB5936 /* CommentsPage.swift in Sources */, 3774126427387D4A00423605 /* VideosAPI.swift in Sources */, 3774124D27387D2300423605 /* PlaylistsModel.swift in Sources */, 3774123427387CC100423605 /* InvidiousAPI.swift in Sources */, @@ -2028,6 +2076,7 @@ 37CB128B2724CC1F00213B45 /* VideoURLParserTests.swift in Sources */, 3774125427387D2300423605 /* Store.swift in Sources */, 3774125027387D2300423605 /* Video.swift in Sources */, + 37EF9A79275BEB8E0043B585 /* CommentView.swift in Sources */, 3774125327387D2300423605 /* Country.swift in Sources */, 3774125E27387D2D00423605 /* InstancesModel.swift in Sources */, 37CB128C2724CC8400213B45 /* VideoURLParser.swift in Sources */, @@ -2045,6 +2094,7 @@ 3774125D27387D2D00423605 /* Instance.swift in Sources */, 3774125927387D2300423605 /* ChannelPlaylist.swift in Sources */, 3774125527387D2300423605 /* Stream.swift in Sources */, + 371B7E5F27596B8400D21217 /* Comment.swift in Sources */, 3774126F27387D8D00423605 /* SearchQuery.swift in Sources */, 3774127127387D9E00423605 /* PlayerQueueItemBridge.swift in Sources */, 3774125227387D2300423605 /* Thumbnail.swift in Sources */, @@ -2067,6 +2117,7 @@ buildActionMask = 2147483647; files = ( 37EAD871267B9ED100D9E01B /* Segment.swift in Sources */, + 373C8FE6275B955100CB5936 /* CommentsPage.swift in Sources */, 37CC3F52270D010D00608308 /* VideoBanner.swift in Sources */, 37F49BA526CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */, 376CD21826FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */, @@ -2080,6 +2131,7 @@ 376578872685429C00D4EA09 /* CaseIterable+Next.swift in Sources */, 37D4B1802671650A00C925CA /* YatteeApp.swift in Sources */, 3748187026A769D60084E870 /* DetailBadge.swift in Sources */, + 371B7E632759706A00D21217 /* CommentsView.swift in Sources */, 37A9965C26D6F8CA006E3224 /* HorizontalCells.swift in Sources */, 3782B95727557E6E00990149 /* SearchSuggestions.swift in Sources */, 37BD07C92698FBDB003EBB87 /* ContentView.swift in Sources */, @@ -2100,6 +2152,7 @@ 37C3A243272359900087A57A /* Double+Format.swift in Sources */, 37AAF29226740715007FC770 /* Channel.swift in Sources */, 37EAD86D267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */, + 371B7E5E27596B8400D21217 /* Comment.swift in Sources */, 37732FF22703A26300F04329 /* AccountValidationStatus.swift in Sources */, 37C0698427260B2100F7F6CB /* ThumbnailsModel.swift in Sources */, 37666BAA27023AF000F869E5 /* AccountSelectionView.swift in Sources */, @@ -2114,6 +2167,7 @@ 3730D8A02712E2B70020ED53 /* NowPlayingView.swift in Sources */, 37169AA42729D98A0011DE61 /* InstancesBridge.swift in Sources */, 37D4B18E26717B3800C925CA /* VideoCell.swift in Sources */, + 371B7E682759786B00D21217 /* Comment+Fixtures.swift in Sources */, 37BE0BD126A0E2D50092E2DB /* VideoPlayerView.swift in Sources */, 37AAF27E26737323007FC770 /* PopularView.swift in Sources */, 3743CA50270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */, @@ -2162,6 +2216,7 @@ 37141675267A8E10006CA35D /* Country.swift in Sources */, 3782B9542755667600990149 /* String+Format.swift in Sources */, 37152EEC26EFEB95004FB96D /* LazyView.swift in Sources */, + 37EF9A78275BEB8E0043B585 /* CommentView.swift in Sources */, 37484C2726FC83E000287258 /* InstanceForm.swift in Sources */, 37F49BA826CB0FCE00304AC0 /* PlaylistFormView.swift in Sources */, 373197DA2732060100EF734F /* RelatedView.swift in Sources */, @@ -2172,6 +2227,7 @@ 37599F36272B44000087F250 /* FavoritesModel.swift in Sources */, 3705B184267B4E4900704544 /* TrendingCategory.swift in Sources */, 37E084AD2753D95F00039B7D /* AccountsNavigationLink.swift in Sources */, + 371B7E6C2759791900D21217 /* CommentsModel.swift in Sources */, 3782B95627557E4E00990149 /* SearchView.swift in Sources */, 3761ABFF26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */, 37C3A24F272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */, diff --git a/tvOS/NowPlayingView.swift b/tvOS/NowPlayingView.swift index 17d17ecb..8c8e8673 100644 --- a/tvOS/NowPlayingView.swift +++ b/tvOS/NowPlayingView.swift @@ -3,13 +3,17 @@ import SwiftUI struct NowPlayingView: View { enum ViewSection: CaseIterable { - case nowPlaying, playingNext, playedPreviously, related + case nowPlaying, playingNext, playedPreviously, related, comments } - var sections = ViewSection.allCases + var sections = [ViewSection.nowPlaying, .playingNext, .playedPreviously, .related] var inInfoViewController = false + @State private var repliesID: Comment.ID? + + @EnvironmentObject private var comments @EnvironmentObject private var player + @EnvironmentObject private var recents @Default(.saveHistory) private var saveHistory @@ -111,6 +115,14 @@ struct NowPlayingView: View { } } } + + if sections.contains(.comments) { + Section { + ForEach(comments.all) { comment in + CommentView(comment: comment, repliesID: $repliesID) + } + } + } } .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 20)) .padding(.vertical, 20)