Comments (fixes #4)

This commit is contained in:
Arkadiusz Fal 2021-12-04 20:35:41 +01:00
parent eb537676e6
commit 19a3f08336
29 changed files with 688 additions and 68 deletions

View File

@ -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: "")
)
}
}

View File

@ -5,6 +5,7 @@ struct FixtureEnvironmentObjectsModifier: ViewModifier {
func body(content: Content) -> some View { func body(content: Content) -> some View {
content content
.environmentObject(AccountsModel()) .environmentObject(AccountsModel())
.environmentObject(CommentsModel())
.environmentObject(InstancesModel()) .environmentObject(InstancesModel())
.environmentObject(invidious) .environmentObject(invidious)
.environmentObject(NavigationModel()) .environmentObject(NavigationModel())

View File

@ -30,7 +30,6 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
signedIn = false signedIn = false
configure() configure()
validate()
} }
func validate() { func validate() {
@ -257,6 +256,8 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
.withParam("q", query.lowercased()) .withParam("q", query.lowercased())
} }
func comments(_: Video.ID, page _: String?) -> Resource? { nil }
private func searchQuery(_ query: String) -> String { private func searchQuery(_ query: String) -> String {
var searchQuery = query var searchQuery = query

View File

@ -71,6 +71,13 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
content.json.arrayValue.map { PipedAPI.extractVideo(from: $0)! } content.json.arrayValue.map { PipedAPI.extractVideo(from: $0)! }
} }
configureTransformer(pathPattern("comments/*")) { (content: Entity<JSON>) -> 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 { if account.token.isNil {
updateToken() updateToken()
} }
@ -80,9 +87,14 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
PipedAPI.authorizedEndpoints.contains { url.absoluteString.contains($0) } PipedAPI.authorizedEndpoints.contains { url.absoluteString.contains($0) }
} }
@discardableResult func updateToken() -> Request { func updateToken() {
guard !account.anonymous else {
return
}
account.token = nil account.token = nil
return login.request(
login.request(
.post, .post,
json: ["username": account.username, "password": account.password] json: ["username": account.username, "password": account.password]
) )
@ -161,6 +173,17 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
func playlistVideo(_: String, _: String) -> Resource? { nil } func playlistVideo(_: String, _: String) -> Resource? { nil }
func playlistVideos(_: 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 { private func pathPattern(_ path: String) -> String {
"**\(path)" "**\(path)"
} }
@ -395,4 +418,23 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
.arrayValue .arrayValue
.filter { $0.dictionaryValue["format"] == "MPEG_4" } ?? [] .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)
)
}
} }

View File

@ -31,6 +31,8 @@ protocol VideosAPI {
func loadDetails(_ item: PlayerQueueItem, completionHandler: @escaping (PlayerQueueItem) -> Void) func loadDetails(_ item: PlayerQueueItem, completionHandler: @escaping (PlayerQueueItem) -> Void)
func shareURL(_ item: ContentItem, frontendHost: String?, time: CMTime?) -> URL? func shareURL(_ item: ContentItem, frontendHost: String?, time: CMTime?) -> URL?
func comments(_ id: Video.ID, page: String?) -> Resource?
} }
extension VideosAPI { extension VideosAPI {

View File

@ -38,4 +38,8 @@ enum VideosApp: String, CaseIterable {
var hasFrontendURL: Bool { var hasFrontendURL: Bool {
self == .piped self == .piped
} }
var supportsComments: Bool {
self == .piped
}
} }

16
Model/Comment.swift Normal file
View File

@ -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)
}
}

79
Model/CommentsModel.swift Normal file
View File

@ -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
}
}

6
Model/CommentsPage.swift Normal file
View File

@ -0,0 +1,6 @@
import Foundation
struct CommentsPage {
var comments = [Comment]()
var nextPage: String?
}

View File

@ -43,6 +43,7 @@ final class PlayerModel: ObservableObject {
@Published var restoredSegments = [Segment]() @Published var restoredSegments = [Segment]()
var accounts: AccountsModel var accounts: AccountsModel
var comments: CommentsModel
var composition = AVMutableComposition() var composition = AVMutableComposition()
var loadedCompositionAssets = [AVMediaType]() var loadedCompositionAssets = [AVMediaType]()
@ -67,8 +68,9 @@ final class PlayerModel: ObservableObject {
#endif #endif
}} }}
init(accounts: AccountsModel? = nil, instances _: InstancesModel? = nil) { init(accounts: AccountsModel? = nil, comments: CommentsModel? = nil) {
self.accounts = accounts ?? AccountsModel() self.accounts = accounts ?? AccountsModel()
self.comments = comments ?? CommentsModel()
addItemDidPlayToEndTimeObserver() addItemDidPlayToEndTimeObserver()
addFrequentTimeObserver() addFrequentTimeObserver()
@ -138,6 +140,7 @@ final class PlayerModel: ObservableObject {
playerError = nil playerError = nil
resetSegments() resetSegments()
sponsorBlock.loadSegments(videoID: video.videoID, categories: Defaults[.sponsorBlockCategories]) sponsorBlock.loadSegments(videoID: video.videoID, categories: Defaults[.sponsorBlockCategories])
comments.load()
if let url = stream.singleAssetURL { if let url = stream.singleAssetURL {
logger.info("playing stream with one asset\(stream.kind == .hls ? " (HLS)" : ""): \(url)") logger.info("playing stream with one asset\(stream.kind == .hls ? " (HLS)" : ""): \(url)")

View File

@ -37,6 +37,7 @@ extension PlayerModel {
} }
func playItem(_ item: PlayerQueueItem, video: Video? = nil, at time: TimeInterval? = nil) { func playItem(_ item: PlayerQueueItem, video: Video? = nil, at time: TimeInterval? = nil) {
comments.clear()
currentItem = item currentItem = item
if !time.isNil { if !time.isNil {

View File

@ -35,6 +35,7 @@ Video player for [Invidious](https://github.com/iv-org/invidious) and [Piped](ht
| Search Suggestions | ✅ | ✅ | | Search Suggestions | ✅ | ✅ |
| Search Filters | ✅ | 🔴 | | Search Filters | ✅ | 🔴 |
| Subtitles | 🔴 | ✅ | | Subtitles | 🔴 | ✅ |
| Comments | 🔴 | ✅ |
## Installation ## Installation
### Requirements ### Requirements

View File

@ -2,10 +2,11 @@ import Defaults
import Foundation import Foundation
extension Defaults.Keys { extension Defaults.Keys {
static let kavinPipedInstanceID = "kavin-piped"
static let instances = Key<[Instance]>("instances", default: [ static let instances = Key<[Instance]>("instances", default: [
.init( .init(
app: .piped, app: .piped,
id: "default-piped-instance", id: kavinPipedInstanceID,
name: "Kavin", name: "Kavin",
apiURL: "https://pipedapi.kavin.rocks", apiURL: "https://pipedapi.kavin.rocks",
frontendURL: "https://piped.kavin.rocks" frontendURL: "https://piped.kavin.rocks"
@ -32,6 +33,7 @@ extension Defaults.Keys {
static let playerSidebar = Key<PlayerSidebarSetting>("playerSidebar", default: PlayerSidebarSetting.defaultValue) static let playerSidebar = Key<PlayerSidebarSetting>("playerSidebar", default: PlayerSidebarSetting.defaultValue)
static let playerInstanceID = Key<Instance.ID?>("playerInstance") static let playerInstanceID = Key<Instance.ID?>("playerInstance")
static let showKeywords = Key<Bool>("showKeywords", default: false) static let showKeywords = Key<Bool>("showKeywords", default: false)
static let commentsInstanceID = Key<Instance.ID?>("commentsInstance", default: kavinPipedInstanceID)
static let recentlyOpened = Key<[RecentItem]>("recentlyOpened", default: []) static let recentlyOpened = Key<[RecentItem]>("recentlyOpened", default: [])

View File

@ -6,11 +6,19 @@ import SwiftUI
struct AppSidebarNavigation: View { struct AppSidebarNavigation: View {
@EnvironmentObject<AccountsModel> private var accounts @EnvironmentObject<AccountsModel> private var accounts
@EnvironmentObject<CommentsModel> private var comments
@EnvironmentObject<InstancesModel> private var instances
@EnvironmentObject<NavigationModel> private var navigation
@EnvironmentObject<PlayerModel> private var player
@EnvironmentObject<PlaylistsModel> private var playlists
@EnvironmentObject<RecentsModel> private var recents
@EnvironmentObject<SearchModel> private var search
@EnvironmentObject<SubscriptionsModel> private var subscriptions
@EnvironmentObject<ThumbnailsModel> private var thumbnailsModel
@Default(.visibleSections) private var visibleSections @Default(.visibleSections) private var visibleSections
#if os(iOS) #if os(iOS)
@EnvironmentObject<NavigationModel> private var navigation
@State private var didApplyPrimaryViewWorkAround = false @State private var didApplyPrimaryViewWorkAround = false
#endif #endif
@ -42,14 +50,49 @@ struct AppSidebarNavigation: View {
.frame(minWidth: sidebarMinWidth) .frame(minWidth: sidebarMinWidth)
VStack { VStack {
PlayerControlsView {
HStack(alignment: .center) {
Spacer()
Image(systemName: "play.tv") Image(systemName: "play.tv")
.renderingMode(.original) .renderingMode(.original)
.font(.system(size: 60)) .font(.system(size: 60))
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
Spacer()
} }
} }
}
}
#if os(iOS)
.background(
EmptyView().fullScreenCover(isPresented: $player.presentingPlayer) {
videoPlayer
.environment(\.navigationStyle, .sidebar) .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 { var toolbarContent: some ToolbarContent {
Group { Group {

View File

@ -3,10 +3,15 @@ import SwiftUI
struct AppTabNavigation: View { struct AppTabNavigation: View {
@EnvironmentObject<AccountsModel> private var accounts @EnvironmentObject<AccountsModel> private var accounts
@EnvironmentObject<CommentsModel> private var comments
@EnvironmentObject<InstancesModel> private var instances
@EnvironmentObject<NavigationModel> private var navigation @EnvironmentObject<NavigationModel> private var navigation
@EnvironmentObject<PlayerModel> private var player @EnvironmentObject<PlayerModel> private var player
@EnvironmentObject<PlaylistsModel> private var playlists
@EnvironmentObject<RecentsModel> private var recents @EnvironmentObject<RecentsModel> private var recents
@EnvironmentObject<SearchModel> private var search @EnvironmentObject<SearchModel> private var search
@EnvironmentObject<SubscriptionsModel> private var subscriptions
@EnvironmentObject<ThumbnailsModel> private var thumbnailsModel
@Default(.visibleSections) private var visibleSections @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 { 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 { var toolbarContent: some ToolbarContent {
#if os(iOS) #if os(iOS)
Group { Group {

View File

@ -8,6 +8,7 @@ import SwiftUI
struct ContentView: View { struct ContentView: View {
@StateObject private var accounts = AccountsModel() @StateObject private var accounts = AccountsModel()
@StateObject private var comments = CommentsModel()
@StateObject private var instances = InstancesModel() @StateObject private var instances = InstancesModel()
@StateObject private var navigation = NavigationModel() @StateObject private var navigation = NavigationModel()
@StateObject private var player = PlayerModel() @StateObject private var player = PlayerModel()
@ -40,6 +41,7 @@ struct ContentView: View {
.onAppear(perform: configure) .onAppear(perform: configure)
.environmentObject(accounts) .environmentObject(accounts)
.environmentObject(comments)
.environmentObject(instances) .environmentObject(instances)
.environmentObject(navigation) .environmentObject(navigation)
.environmentObject(player) .environmentObject(player)
@ -58,20 +60,6 @@ struct ContentView: View {
.environmentObject(navigation) .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) #if !os(tvOS)
.handlesExternalEvents(preferring: Set(["*"]), allowing: Set(["*"])) .handlesExternalEvents(preferring: Set(["*"]), allowing: Set(["*"]))
.onOpenURL(perform: handleOpenedURL) .onOpenURL(perform: handleOpenedURL)
@ -98,17 +86,6 @@ struct ContentView: View {
#endif #endif
} }
private var videoPlayer: some View {
VideoPlayerView()
.environmentObject(accounts)
.environmentObject(instances)
.environmentObject(navigation)
.environmentObject(player)
.environmentObject(playlists)
.environmentObject(subscriptions)
.environmentObject(thumbnailsModel)
}
func configure() { func configure() {
SiestaLog.Category.enabled = .common SiestaLog.Category.enabled = .common
SDImageCodersManager.shared.addCoder(SDImageWebPCoder.shared) SDImageCodersManager.shared.addCoder(SDImageWebPCoder.shared)
@ -128,15 +105,20 @@ struct ContentView: View {
navigation.presentingWelcomeScreen = true navigation.presentingWelcomeScreen = true
} }
player.accounts = accounts
playlists.accounts = accounts playlists.accounts = accounts
search.accounts = accounts search.accounts = accounts
subscriptions.accounts = accounts subscriptions.accounts = accounts
comments.accounts = accounts
comments.player = player
menu.accounts = accounts menu.accounts = accounts
menu.navigation = navigation menu.navigation = navigation
menu.player = player menu.player = player
player.accounts = accounts
player.comments = comments
if !accounts.current.isNil { if !accounts.current.isNil {
player.loadHistoryDetails() player.loadHistoryDetails()
} }

View File

@ -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<CommentsModel> private var comments
@EnvironmentObject<NavigationModel> private var navigation
@EnvironmentObject<PlayerModel> private var player
@EnvironmentObject<RecentsModel> 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)
}
}
}
}

View File

@ -0,0 +1,59 @@
import SwiftUI
struct CommentsView: View {
@State private var repliesID: Comment.ID?
@EnvironmentObject<CommentsModel> private var comments
@EnvironmentObject<PlayerModel> 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()
}
}

View File

@ -2,6 +2,7 @@ import Defaults
import SwiftUI import SwiftUI
struct Player: UIViewControllerRepresentable { struct Player: UIViewControllerRepresentable {
@EnvironmentObject<CommentsModel> private var comments
@EnvironmentObject<NavigationModel> private var navigation @EnvironmentObject<NavigationModel> private var navigation
@EnvironmentObject<PlayerModel> private var player @EnvironmentObject<PlayerModel> private var player
@ -18,6 +19,7 @@ struct Player: UIViewControllerRepresentable {
let controller = PlayerViewController() let controller = PlayerViewController()
controller.commentsModel = comments
controller.navigationModel = navigation controller.navigationModel = navigation
controller.playerModel = player controller.playerModel = player
player.controller = controller player.controller = controller

View File

@ -4,6 +4,7 @@ import SwiftUI
final class PlayerViewController: UIViewController { final class PlayerViewController: UIViewController {
var playerLoaded = false var playerLoaded = false
var commentsModel: CommentsModel!
var navigationModel: NavigationModel! var navigationModel: NavigationModel!
var playerModel: PlayerModel! var playerModel: PlayerModel!
var playerViewController = AVPlayerViewController() var playerViewController = AVPlayerViewController()
@ -45,6 +46,7 @@ final class PlayerViewController: UIViewController {
#if os(tvOS) #if os(tvOS)
playerModel.avPlayerViewController = playerViewController playerModel.avPlayerViewController = playerViewController
playerViewController.customInfoViewControllers = [ playerViewController.customInfoViewControllers = [
infoViewController([.comments], title: "Comments"),
infoViewController([.related], title: "Related"), infoViewController([.related], title: "Related"),
infoViewController([.playingNext, .playedPreviously], title: "Playing Next") infoViewController([.playingNext, .playedPreviously], title: "Playing Next")
] ]
@ -62,6 +64,7 @@ final class PlayerViewController: UIViewController {
AnyView( AnyView(
NowPlayingView(sections: sections, inInfoViewController: true) NowPlayingView(sections: sections, inInfoViewController: true)
.frame(maxHeight: 600) .frame(maxHeight: 600)
.environmentObject(commentsModel)
.environmentObject(playerModel) .environmentObject(playerModel)
) )
) )

View File

@ -4,7 +4,7 @@ import SwiftUI
struct VideoDetails: View { struct VideoDetails: View {
enum Page { enum Page {
case details, queue, related case info, queue, related, comments
} }
@Binding var sidebarQueue: Bool @Binding var sidebarQueue: Bool
@ -16,7 +16,7 @@ struct VideoDetails: View {
@State private var presentingShareSheet = false @State private var presentingShareSheet = false
@State private var shareURL: URL? @State private var shareURL: URL?
@State private var currentPage = Page.details @State private var currentPage = Page.info
@Environment(\.presentationMode) private var presentationMode @Environment(\.presentationMode) private var presentationMode
@Environment(\.inNavigationView) private var inNavigationView @Environment(\.inNavigationView) private var inNavigationView
@ -65,7 +65,7 @@ struct VideoDetails: View {
} }
.padding(.horizontal) .padding(.horizontal)
if !sidebarQueue { if CommentsModel.enabled {
pagePicker pagePicker
.padding(.horizontal) .padding(.horizontal)
} }
@ -89,7 +89,7 @@ struct VideoDetails: View {
) )
switch currentPage { switch currentPage {
case .details: case .info:
ScrollView(.vertical) { ScrollView(.vertical) {
detailsPage detailsPage
} }
@ -100,6 +100,9 @@ struct VideoDetails: View {
case .related: case .related:
RelatedView() RelatedView()
.edgesIgnoringSafeArea(.horizontal) .edgesIgnoringSafeArea(.horizontal)
case .comments:
CommentsView()
.edgesIgnoringSafeArea(.horizontal)
} }
} }
.padding(.top, inNavigationView && fullScreen ? 10 : 0) .padding(.top, inNavigationView && fullScreen ? 10 : 0)
@ -116,7 +119,7 @@ struct VideoDetails: View {
.onChange(of: sidebarQueue) { queue in .onChange(of: sidebarQueue) { queue in
if queue { if queue {
if currentPage == .queue { if currentPage == .queue {
currentPage = .details currentPage = .info
} }
} else if video.isNil { } else if video.isNil {
currentPage = .queue currentPage = .queue
@ -131,7 +134,7 @@ struct VideoDetails: View {
if video != nil { if video != nil {
Text(video!.title) Text(video!.title)
.onAppear { .onAppear {
currentPage = .details currentPage = .info
} }
.contextMenu { .contextMenu {
Button { Button {
@ -239,15 +242,23 @@ struct VideoDetails: View {
var pagePicker: some View { var pagePicker: some View {
Picker("Page", selection: $currentPage) { Picker("Page", selection: $currentPage) {
if !video.isNil { if !video.isNil {
Text("Details").tag(Page.details) Text("Info").tag(Page.info)
if !sidebarQueue {
Text("Related").tag(Page.related) 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() .labelsHidden()
.pickerStyle(.segmented) .pickerStyle(.segmented)
.onDisappear { .onDisappear {
currentPage = .details currentPage = .info
} }
} }
@ -297,19 +308,19 @@ struct VideoDetails: View {
Spacer() Spacer()
if let views = video.viewsCount { 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 { if let likes = video.likesCount {
Divider() Divider()
videoDetail(label: "Likes", value: likes, symbol: "hand.thumbsup.circle.fill") videoDetail(label: "Likes", value: likes, symbol: "hand.thumbsup")
} }
if let dislikes = video.dislikesCount { if let dislikes = video.dislikesCount {
Divider() Divider()
videoDetail(label: "Dislikes", value: dislikes, symbol: "hand.thumbsdown.circle.fill") videoDetail(label: "Dislikes", value: dislikes, symbol: "hand.thumbsdown")
} }
Spacer() Spacer()
@ -378,7 +389,8 @@ struct VideoDetails: View {
} }
} }
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.font(.caption) .font(.system(size: 14))
.lineSpacing(3)
.padding(.bottom, 4) .padding(.bottom, 4)
} else { } else {
Text("No description") Text("No description")

View File

@ -6,6 +6,12 @@ struct SearchTextField: View {
@EnvironmentObject<RecentsModel> private var recents @EnvironmentObject<RecentsModel> private var recents
@EnvironmentObject<SearchModel> private var state @EnvironmentObject<SearchModel> private var state
@Binding var favoriteItem: FavoriteItem?
init(favoriteItem: Binding<FavoriteItem?>? = nil) {
_favoriteItem = favoriteItem ?? .constant(nil)
}
var body: some View { var body: some View {
ZStack { ZStack {
#if os(macOS) #if os(macOS)
@ -37,7 +43,14 @@ struct SearchTextField: View {
.padding(.leading) .padding(.leading)
.padding(.trailing, 15) .padding(.trailing, 15)
#endif #endif
if !self.state.queryText.isEmpty { if !self.state.queryText.isEmpty {
#if os(iOS)
FavoriteButton(item: favoriteItem)
.id(favoriteItem?.id)
.labelStyle(.iconOnly)
.padding(.trailing)
#endif
clearButton clearButton
} }
} }

View File

@ -42,7 +42,7 @@ struct SearchView: View {
PlayerControlsView { PlayerControlsView {
#if os(iOS) #if os(iOS)
VStack { VStack {
SearchTextField() SearchTextField(favoriteItem: $favoriteItem)
if state.query.query != state.queryText, !state.queryText.isEmpty, !state.querySuggestions.collection.isEmpty { if state.query.query != state.queryText, !state.queryText.isEmpty, !state.querySuggestions.collection.isEmpty {
SearchSuggestions() SearchSuggestions()
@ -93,15 +93,6 @@ struct SearchView: View {
.transaction { t in t.animation = .none } .transaction { t in t.animation = .none }
} }
#if os(iOS)
Spacer()
FavoriteButton(item: favoriteItem)
.id(favoriteItem?.id)
Spacer()
#endif
if accounts.app.supportsSearchFilters { if accounts.app.supportsSearchFilters {
filtersMenu filtersMenu
} }

View File

@ -57,7 +57,7 @@ struct PlaybackSettings: View {
Text("Best available stream").tag(String?.none) Text("Best available stream").tag(String?.none)
ForEach(instances) { instance in ForEach(instances) { instance in
Text(instance.longDescription).tag(Optional(instance.id)) Text(instance.description).tag(Optional(instance.id))
} }
} }
.labelsHidden() .labelsHidden()

View File

@ -4,8 +4,13 @@ import SwiftUI
struct ServicesSettings: View { struct ServicesSettings: View {
@Default(.sponsorBlockInstance) private var sponsorBlockInstance @Default(.sponsorBlockInstance) private var sponsorBlockInstance
@Default(.sponsorBlockCategories) private var sponsorBlockCategories @Default(.sponsorBlockCategories) private var sponsorBlockCategories
@Default(.commentsInstanceID) private var commentsInstanceID
var body: some View { var body: some View {
Section(header: SettingsHeader(text: "Comments")) {
commentsInstancePicker
}
Section(header: SettingsHeader(text: "SponsorBlock API")) { Section(header: SettingsHeader(text: "SponsorBlock API")) {
TextField( TextField(
"SponsorBlock API Instance", "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) { func toggleCategory(_ category: String, value: Bool) {
if let index = sponsorBlockCategories.firstIndex(where: { $0 == category }), !value { if let index = sponsorBlockCategories.firstIndex(where: { $0 == category }), !value {
sponsorBlockCategories.remove(at: index) sponsorBlockCategories.remove(at: index)

View File

@ -90,7 +90,12 @@ struct ChannelVideosView: View {
ToolbarItem { ToolbarItem {
HStack { HStack {
Text("**\(store.item?.subscriptionsString ?? "loading")** subscribers") HStack(spacing: 3) {
Text("\(store.item?.subscriptionsString ?? "loading")")
.fontWeight(.bold)
Text(" subscribers")
}
.allowsTightening(true)
.foregroundColor(.secondary) .foregroundColor(.secondary)
.opacity(store.item?.subscriptionsString != nil ? 1 : 0) .opacity(store.item?.subscriptionsString != nil ? 1 : 0)

View File

@ -34,9 +34,9 @@ struct SignInRequiredView<Content: View>: View {
Group { Group {
if instances.isEmpty { 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 { } 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) .multilineTextAlignment(.center)

View File

@ -74,6 +74,20 @@
37169AA62729E2CC0011DE61 /* AccountsBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37169AA52729E2CC0011DE61 /* AccountsBridge.swift */; }; 37169AA62729E2CC0011DE61 /* AccountsBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37169AA52729E2CC0011DE61 /* AccountsBridge.swift */; };
37169AA72729E2CC0011DE61 /* 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 */; }; 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 */; }; 371F2F1A269B43D300E4A7AB /* NavigationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371F2F19269B43D300E4A7AB /* NavigationModel.swift */; };
371F2F1B269B43D300E4A7AB /* 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 */; }; 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 */; }; 37319F0527103F94004ECCD0 /* PlayerQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37319F0427103F94004ECCD0 /* PlayerQueue.swift */; };
37319F0627103F94004ECCD0 /* 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 */; }; 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 */; }; 373CFACB26966264003CB2C6 /* SearchQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFACA26966264003CB2C6 /* SearchQuery.swift */; };
373CFACC26966264003CB2C6 /* 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 */; }; 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 */; }; 37EF5C222739D37B00B03725 /* MenuModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF5C212739D37B00B03725 /* MenuModel.swift */; };
37EF5C232739D37B00B03725 /* 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 */; }; 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 */; }; 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 */; }; 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 */; }; 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 = "<group>"; }; 37152EE926EFEB95004FB96D /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = "<group>"; };
37169AA12729D98A0011DE61 /* InstancesBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesBridge.swift; sourceTree = "<group>"; }; 37169AA12729D98A0011DE61 /* InstancesBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesBridge.swift; sourceTree = "<group>"; };
37169AA52729E2CC0011DE61 /* AccountsBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsBridge.swift; sourceTree = "<group>"; }; 37169AA52729E2CC0011DE61 /* AccountsBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsBridge.swift; sourceTree = "<group>"; };
371B7E5B27596B8400D21217 /* Comment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Comment.swift; sourceTree = "<group>"; };
371B7E602759706A00D21217 /* CommentsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentsView.swift; sourceTree = "<group>"; };
371B7E652759786B00D21217 /* Comment+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Comment+Fixtures.swift"; sourceTree = "<group>"; };
371B7E692759791900D21217 /* CommentsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentsModel.swift; sourceTree = "<group>"; };
371F2F19269B43D300E4A7AB /* NavigationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModel.swift; sourceTree = "<group>"; }; 371F2F19269B43D300E4A7AB /* NavigationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModel.swift; sourceTree = "<group>"; };
3722AEBB274DA396005EA4D6 /* Badge+Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Badge+Backport.swift"; sourceTree = "<group>"; }; 3722AEBB274DA396005EA4D6 /* Badge+Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Badge+Backport.swift"; sourceTree = "<group>"; };
3722AEBD274DA401005EA4D6 /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = "<group>"; }; 3722AEBD274DA401005EA4D6 /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = "<group>"; };
@ -570,6 +596,7 @@
3730D89F2712E2B70020ED53 /* NowPlayingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingView.swift; sourceTree = "<group>"; }; 3730D89F2712E2B70020ED53 /* NowPlayingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingView.swift; sourceTree = "<group>"; };
373197D82732015300EF734F /* RelatedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelatedView.swift; sourceTree = "<group>"; }; 373197D82732015300EF734F /* RelatedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelatedView.swift; sourceTree = "<group>"; };
37319F0427103F94004ECCD0 /* PlayerQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerQueue.swift; sourceTree = "<group>"; }; 37319F0427103F94004ECCD0 /* PlayerQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerQueue.swift; sourceTree = "<group>"; };
373C8FE3275B955100CB5936 /* CommentsPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentsPage.swift; sourceTree = "<group>"; };
373CFACA26966264003CB2C6 /* SearchQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchQuery.swift; sourceTree = "<group>"; }; 373CFACA26966264003CB2C6 /* SearchQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchQuery.swift; sourceTree = "<group>"; };
373CFADA269663F1003CB2C6 /* Thumbnail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thumbnail.swift; sourceTree = "<group>"; }; 373CFADA269663F1003CB2C6 /* Thumbnail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thumbnail.swift; sourceTree = "<group>"; };
373CFAEA26975CBF003CB2C6 /* PlaylistFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistFormView.swift; sourceTree = "<group>"; }; 373CFAEA26975CBF003CB2C6 /* PlaylistFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistFormView.swift; sourceTree = "<group>"; };
@ -713,6 +740,7 @@
37EAD86A267B9C5600D9E01B /* SponsorBlockAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockAPI.swift; sourceTree = "<group>"; }; 37EAD86A267B9C5600D9E01B /* SponsorBlockAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockAPI.swift; sourceTree = "<group>"; };
37EAD86E267B9ED100D9E01B /* Segment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Segment.swift; sourceTree = "<group>"; }; 37EAD86E267B9ED100D9E01B /* Segment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Segment.swift; sourceTree = "<group>"; };
37EF5C212739D37B00B03725 /* MenuModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuModel.swift; sourceTree = "<group>"; }; 37EF5C212739D37B00B03725 /* MenuModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuModel.swift; sourceTree = "<group>"; };
37EF9A75275BEB8E0043B585 /* CommentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentView.swift; sourceTree = "<group>"; };
37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Playlist+Fixtures.swift"; sourceTree = "<group>"; }; 37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Playlist+Fixtures.swift"; sourceTree = "<group>"; };
37F4AE7126828F0900BD60EA /* VerticalCells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalCells.swift; sourceTree = "<group>"; }; 37F4AE7126828F0900BD60EA /* VerticalCells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalCells.swift; sourceTree = "<group>"; };
37F64FE326FE70A60081B69E /* RedrawOnModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedrawOnModifier.swift; sourceTree = "<group>"; }; 37F64FE326FE70A60081B69E /* RedrawOnModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedrawOnModifier.swift; sourceTree = "<group>"; };
@ -835,6 +863,8 @@
371AAE2426CEBA4100901972 /* Player */ = { 371AAE2426CEBA4100901972 /* Player */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
371B7E602759706A00D21217 /* CommentsView.swift */,
37EF9A75275BEB8E0043B585 /* CommentView.swift */,
37B81B0126D2CAE700675966 /* PlaybackBar.swift */, 37B81B0126D2CAE700675966 /* PlaybackBar.swift */,
37BE0BD226A1D4780092E2DB /* Player.swift */, 37BE0BD226A1D4780092E2DB /* Player.swift */,
3743CA4D270EFE3400E4D32B /* PlayerQueueRow.swift */, 3743CA4D270EFE3400E4D32B /* PlayerQueueRow.swift */,
@ -957,6 +987,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
37C3A24C272360470087A57A /* ChannelPlaylist+Fixtures.swift */, 37C3A24C272360470087A57A /* ChannelPlaylist+Fixtures.swift */,
371B7E652759786B00D21217 /* Comment+Fixtures.swift */,
376CD21526FBE18D001E1AC1 /* Instance+Fixtures.swift */, 376CD21526FBE18D001E1AC1 /* Instance+Fixtures.swift */,
37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */, 37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */,
3748186926A764FB0084E870 /* Thumbnail+Fixtures.swift */, 3748186926A764FB0084E870 /* Thumbnail+Fixtures.swift */,
@ -1212,6 +1243,9 @@
374C0539272436DA009BDDBE /* SponsorBlock */, 374C0539272436DA009BDDBE /* SponsorBlock */,
37AAF28F26740715007FC770 /* Channel.swift */, 37AAF28F26740715007FC770 /* Channel.swift */,
37C3A24427235DA70087A57A /* ChannelPlaylist.swift */, 37C3A24427235DA70087A57A /* ChannelPlaylist.swift */,
371B7E5B27596B8400D21217 /* Comment.swift */,
371B7E692759791900D21217 /* CommentsModel.swift */,
373C8FE3275B955100CB5936 /* CommentsPage.swift */,
37FB28402721B22200A57617 /* ContentItem.swift */, 37FB28402721B22200A57617 /* ContentItem.swift */,
37141672267A8E10006CA35D /* Country.swift */, 37141672267A8E10006CA35D /* Country.swift */,
37599F2F272B42810087F250 /* FavoriteItem.swift */, 37599F2F272B42810087F250 /* FavoriteItem.swift */,
@ -1722,6 +1756,8 @@
files = ( files = (
374710052755291C00CE0F87 /* SearchField.swift in Sources */, 374710052755291C00CE0F87 /* SearchField.swift in Sources */,
37CEE4BD2677B670005A1EFE /* SingleAssetStream.swift in Sources */, 37CEE4BD2677B670005A1EFE /* SingleAssetStream.swift in Sources */,
371B7E612759706A00D21217 /* CommentsView.swift in Sources */,
371B7E6A2759791900D21217 /* CommentsModel.swift in Sources */,
37CC3F45270CE30600608308 /* PlayerQueueItem.swift in Sources */, 37CC3F45270CE30600608308 /* PlayerQueueItem.swift in Sources */,
37BD07C82698B71C003EBB87 /* AppTabNavigation.swift in Sources */, 37BD07C82698B71C003EBB87 /* AppTabNavigation.swift in Sources */,
37599F38272B4D740087F250 /* FavoriteButton.swift in Sources */, 37599F38272B4D740087F250 /* FavoriteButton.swift in Sources */,
@ -1752,6 +1788,7 @@
37B81AF926D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */, 37B81AF926D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */,
37C0698227260B2100F7F6CB /* ThumbnailsModel.swift in Sources */, 37C0698227260B2100F7F6CB /* ThumbnailsModel.swift in Sources */,
37DD87C7271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */, 37DD87C7271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */,
371B7E662759786B00D21217 /* Comment+Fixtures.swift in Sources */,
37BE0BD326A1D4780092E2DB /* Player.swift in Sources */, 37BE0BD326A1D4780092E2DB /* Player.swift in Sources */,
37A9965E26D6F9B9006E3224 /* FavoritesView.swift in Sources */, 37A9965E26D6F9B9006E3224 /* FavoritesView.swift in Sources */,
37CEE4C12677B697005A1EFE /* Stream.swift in Sources */, 37CEE4C12677B697005A1EFE /* Stream.swift in Sources */,
@ -1779,6 +1816,8 @@
3782B9522755667600990149 /* String+Format.swift in Sources */, 3782B9522755667600990149 /* String+Format.swift in Sources */,
373CFAEF2697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */, 373CFAEF2697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
37BF661F27308884008CCFB0 /* DropFavoriteOutside.swift in Sources */, 37BF661F27308884008CCFB0 /* DropFavoriteOutside.swift in Sources */,
37EF9A76275BEB8E0043B585 /* CommentView.swift in Sources */,
373C8FE4275B955100CB5936 /* CommentsPage.swift in Sources */,
3700155B271B0D4D0049C794 /* PipedAPI.swift in Sources */, 3700155B271B0D4D0049C794 /* PipedAPI.swift in Sources */,
37B044B726F7AB9000E1419D /* SettingsView.swift in Sources */, 37B044B726F7AB9000E1419D /* SettingsView.swift in Sources */,
377FC7E3267A084A00A6BBAF /* VideoCell.swift in Sources */, 377FC7E3267A084A00A6BBAF /* VideoCell.swift in Sources */,
@ -1805,6 +1844,7 @@
376B2E0726F920D600B1D64D /* SignInRequiredView.swift in Sources */, 376B2E0726F920D600B1D64D /* SignInRequiredView.swift in Sources */,
37C0697E2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */, 37C0697E2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */,
3743CA4E270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */, 3743CA4E270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */,
371B7E5C27596B8400D21217 /* Comment.swift in Sources */,
37BD672426F13D65004BE0C1 /* AppSidebarPlaylists.swift in Sources */, 37BD672426F13D65004BE0C1 /* AppSidebarPlaylists.swift in Sources */,
37B17DA2268A1F8A006AEE9B /* VideoContextMenuView.swift in Sources */, 37B17DA2268A1F8A006AEE9B /* VideoContextMenuView.swift in Sources */,
3784B23B272894DA00B09468 /* ShareSheet.swift in Sources */, 3784B23B272894DA00B09468 /* ShareSheet.swift in Sources */,
@ -1876,6 +1916,7 @@
37C3A24E272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */, 37C3A24E272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */,
37CEE4C22677B697005A1EFE /* Stream.swift in Sources */, 37CEE4C22677B697005A1EFE /* Stream.swift in Sources */,
3782B95027553A6700990149 /* SearchSuggestions.swift in Sources */, 3782B95027553A6700990149 /* SearchSuggestions.swift in Sources */,
371B7E6B2759791900D21217 /* CommentsModel.swift in Sources */,
371F2F1B269B43D300E4A7AB /* NavigationModel.swift in Sources */, 371F2F1B269B43D300E4A7AB /* NavigationModel.swift in Sources */,
37001564271B1F250049C794 /* AccountsModel.swift in Sources */, 37001564271B1F250049C794 /* AccountsModel.swift in Sources */,
3761ABFE26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */, 3761ABFE26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */,
@ -1925,6 +1966,7 @@
37AAF29126740715007FC770 /* Channel.swift in Sources */, 37AAF29126740715007FC770 /* Channel.swift in Sources */,
376A33E12720CAD6000C1D6B /* VideosApp.swift in Sources */, 376A33E12720CAD6000C1D6B /* VideosApp.swift in Sources */,
37BD07BC2698AB60003EBB87 /* AppSidebarNavigation.swift in Sources */, 37BD07BC2698AB60003EBB87 /* AppSidebarNavigation.swift in Sources */,
37EF9A77275BEB8E0043B585 /* CommentView.swift in Sources */,
37BF661D27308859008CCFB0 /* DropFavorite.swift in Sources */, 37BF661D27308859008CCFB0 /* DropFavorite.swift in Sources */,
3748186F26A769D60084E870 /* DetailBadge.swift in Sources */, 3748186F26A769D60084E870 /* DetailBadge.swift in Sources */,
372915E72687E3B900F5A35B /* Defaults.swift in Sources */, 372915E72687E3B900F5A35B /* Defaults.swift in Sources */,
@ -1956,6 +1998,7 @@
37319F0627103F94004ECCD0 /* PlayerQueue.swift in Sources */, 37319F0627103F94004ECCD0 /* PlayerQueue.swift in Sources */,
37B767DC2677C3CA0098BAA8 /* PlayerModel.swift in Sources */, 37B767DC2677C3CA0098BAA8 /* PlayerModel.swift in Sources */,
3797758C2689345500DD52A8 /* Store.swift in Sources */, 3797758C2689345500DD52A8 /* Store.swift in Sources */,
371B7E622759706A00D21217 /* CommentsView.swift in Sources */,
37141674267A8E10006CA35D /* Country.swift in Sources */, 37141674267A8E10006CA35D /* Country.swift in Sources */,
37FD43E42704847C0073EE42 /* View+Fixtures.swift in Sources */, 37FD43E42704847C0073EE42 /* View+Fixtures.swift in Sources */,
37C069782725962F00F7F6CB /* ScreenSaverManager.swift in Sources */, 37C069782725962F00F7F6CB /* ScreenSaverManager.swift in Sources */,
@ -1967,12 +2010,14 @@
3700155C271B0D4D0049C794 /* PipedAPI.swift in Sources */, 3700155C271B0D4D0049C794 /* PipedAPI.swift in Sources */,
376BE50C27349108009AD608 /* BrowsingSettings.swift in Sources */, 376BE50C27349108009AD608 /* BrowsingSettings.swift in Sources */,
37D4B19826717E1500C925CA /* Video.swift in Sources */, 37D4B19826717E1500C925CA /* Video.swift in Sources */,
371B7E5D27596B8400D21217 /* Comment.swift in Sources */,
37EF5C232739D37B00B03725 /* MenuModel.swift in Sources */, 37EF5C232739D37B00B03725 /* MenuModel.swift in Sources */,
37599F31272B42810087F250 /* FavoriteItem.swift in Sources */, 37599F31272B42810087F250 /* FavoriteItem.swift in Sources */,
3730F75A2733481E00F385FC /* RelatedView.swift in Sources */, 3730F75A2733481E00F385FC /* RelatedView.swift in Sources */,
37E04C0F275940FB00172673 /* VerticalScrollingFix.swift in Sources */, 37E04C0F275940FB00172673 /* VerticalScrollingFix.swift in Sources */,
375168D72700FDB8008F96A6 /* Debounce.swift in Sources */, 375168D72700FDB8008F96A6 /* Debounce.swift in Sources */,
37D526DF2720AC4400ED2F5E /* VideosAPI.swift in Sources */, 37D526DF2720AC4400ED2F5E /* VideosAPI.swift in Sources */,
373C8FE5275B955100CB5936 /* CommentsPage.swift in Sources */,
37D4B0E52671614900C925CA /* YatteeApp.swift in Sources */, 37D4B0E52671614900C925CA /* YatteeApp.swift in Sources */,
37BD07C12698AD3B003EBB87 /* TrendingCountry.swift in Sources */, 37BD07C12698AD3B003EBB87 /* TrendingCountry.swift in Sources */,
37BA794026DB8F97002A0235 /* ChannelVideosView.swift in Sources */, 37BA794026DB8F97002A0235 /* ChannelVideosView.swift in Sources */,
@ -1993,6 +2038,7 @@
37C3A24A27235FAA0087A57A /* ChannelPlaylistCell.swift in Sources */, 37C3A24A27235FAA0087A57A /* ChannelPlaylistCell.swift in Sources */,
373CFAF02697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */, 373CFAF02697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
3763495226DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */, 3763495226DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */,
371B7E672759786B00D21217 /* Comment+Fixtures.swift in Sources */,
37CB127A2724C76D00213B45 /* VideoURLParser.swift in Sources */, 37CB127A2724C76D00213B45 /* VideoURLParser.swift in Sources */,
37BA794426DBA973002A0235 /* PlaylistsModel.swift in Sources */, 37BA794426DBA973002A0235 /* PlaylistsModel.swift in Sources */,
); );
@ -2012,6 +2058,7 @@
files = ( files = (
3774124C27387D2300423605 /* RecentsModel.swift in Sources */, 3774124C27387D2300423605 /* RecentsModel.swift in Sources */,
3774122A27387B6C00423605 /* InstancesModelTests.swift in Sources */, 3774122A27387B6C00423605 /* InstancesModelTests.swift in Sources */,
371B7E642759706A00D21217 /* CommentsView.swift in Sources */,
3774124927387D2300423605 /* Channel.swift in Sources */, 3774124927387D2300423605 /* Channel.swift in Sources */,
3774125727387D2300423605 /* FavoriteItem.swift in Sources */, 3774125727387D2300423605 /* FavoriteItem.swift in Sources */,
3774126B27387D6D00423605 /* CMTime+DefaultTimescale.swift in Sources */, 3774126B27387D6D00423605 /* CMTime+DefaultTimescale.swift in Sources */,
@ -2021,6 +2068,7 @@
3774126827387D6D00423605 /* Double+Format.swift in Sources */, 3774126827387D6D00423605 /* Double+Format.swift in Sources */,
3774126E27387D8800423605 /* PlayerQueueItem.swift in Sources */, 3774126E27387D8800423605 /* PlayerQueueItem.swift in Sources */,
3774125627387D2300423605 /* Segment.swift in Sources */, 3774125627387D2300423605 /* Segment.swift in Sources */,
373C8FE7275B955100CB5936 /* CommentsPage.swift in Sources */,
3774126427387D4A00423605 /* VideosAPI.swift in Sources */, 3774126427387D4A00423605 /* VideosAPI.swift in Sources */,
3774124D27387D2300423605 /* PlaylistsModel.swift in Sources */, 3774124D27387D2300423605 /* PlaylistsModel.swift in Sources */,
3774123427387CC100423605 /* InvidiousAPI.swift in Sources */, 3774123427387CC100423605 /* InvidiousAPI.swift in Sources */,
@ -2028,6 +2076,7 @@
37CB128B2724CC1F00213B45 /* VideoURLParserTests.swift in Sources */, 37CB128B2724CC1F00213B45 /* VideoURLParserTests.swift in Sources */,
3774125427387D2300423605 /* Store.swift in Sources */, 3774125427387D2300423605 /* Store.swift in Sources */,
3774125027387D2300423605 /* Video.swift in Sources */, 3774125027387D2300423605 /* Video.swift in Sources */,
37EF9A79275BEB8E0043B585 /* CommentView.swift in Sources */,
3774125327387D2300423605 /* Country.swift in Sources */, 3774125327387D2300423605 /* Country.swift in Sources */,
3774125E27387D2D00423605 /* InstancesModel.swift in Sources */, 3774125E27387D2D00423605 /* InstancesModel.swift in Sources */,
37CB128C2724CC8400213B45 /* VideoURLParser.swift in Sources */, 37CB128C2724CC8400213B45 /* VideoURLParser.swift in Sources */,
@ -2045,6 +2094,7 @@
3774125D27387D2D00423605 /* Instance.swift in Sources */, 3774125D27387D2D00423605 /* Instance.swift in Sources */,
3774125927387D2300423605 /* ChannelPlaylist.swift in Sources */, 3774125927387D2300423605 /* ChannelPlaylist.swift in Sources */,
3774125527387D2300423605 /* Stream.swift in Sources */, 3774125527387D2300423605 /* Stream.swift in Sources */,
371B7E5F27596B8400D21217 /* Comment.swift in Sources */,
3774126F27387D8D00423605 /* SearchQuery.swift in Sources */, 3774126F27387D8D00423605 /* SearchQuery.swift in Sources */,
3774127127387D9E00423605 /* PlayerQueueItemBridge.swift in Sources */, 3774127127387D9E00423605 /* PlayerQueueItemBridge.swift in Sources */,
3774125227387D2300423605 /* Thumbnail.swift in Sources */, 3774125227387D2300423605 /* Thumbnail.swift in Sources */,
@ -2067,6 +2117,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
37EAD871267B9ED100D9E01B /* Segment.swift in Sources */, 37EAD871267B9ED100D9E01B /* Segment.swift in Sources */,
373C8FE6275B955100CB5936 /* CommentsPage.swift in Sources */,
37CC3F52270D010D00608308 /* VideoBanner.swift in Sources */, 37CC3F52270D010D00608308 /* VideoBanner.swift in Sources */,
37F49BA526CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */, 37F49BA526CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */,
376CD21826FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */, 376CD21826FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */,
@ -2080,6 +2131,7 @@
376578872685429C00D4EA09 /* CaseIterable+Next.swift in Sources */, 376578872685429C00D4EA09 /* CaseIterable+Next.swift in Sources */,
37D4B1802671650A00C925CA /* YatteeApp.swift in Sources */, 37D4B1802671650A00C925CA /* YatteeApp.swift in Sources */,
3748187026A769D60084E870 /* DetailBadge.swift in Sources */, 3748187026A769D60084E870 /* DetailBadge.swift in Sources */,
371B7E632759706A00D21217 /* CommentsView.swift in Sources */,
37A9965C26D6F8CA006E3224 /* HorizontalCells.swift in Sources */, 37A9965C26D6F8CA006E3224 /* HorizontalCells.swift in Sources */,
3782B95727557E6E00990149 /* SearchSuggestions.swift in Sources */, 3782B95727557E6E00990149 /* SearchSuggestions.swift in Sources */,
37BD07C92698FBDB003EBB87 /* ContentView.swift in Sources */, 37BD07C92698FBDB003EBB87 /* ContentView.swift in Sources */,
@ -2100,6 +2152,7 @@
37C3A243272359900087A57A /* Double+Format.swift in Sources */, 37C3A243272359900087A57A /* Double+Format.swift in Sources */,
37AAF29226740715007FC770 /* Channel.swift in Sources */, 37AAF29226740715007FC770 /* Channel.swift in Sources */,
37EAD86D267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */, 37EAD86D267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */,
371B7E5E27596B8400D21217 /* Comment.swift in Sources */,
37732FF22703A26300F04329 /* AccountValidationStatus.swift in Sources */, 37732FF22703A26300F04329 /* AccountValidationStatus.swift in Sources */,
37C0698427260B2100F7F6CB /* ThumbnailsModel.swift in Sources */, 37C0698427260B2100F7F6CB /* ThumbnailsModel.swift in Sources */,
37666BAA27023AF000F869E5 /* AccountSelectionView.swift in Sources */, 37666BAA27023AF000F869E5 /* AccountSelectionView.swift in Sources */,
@ -2114,6 +2167,7 @@
3730D8A02712E2B70020ED53 /* NowPlayingView.swift in Sources */, 3730D8A02712E2B70020ED53 /* NowPlayingView.swift in Sources */,
37169AA42729D98A0011DE61 /* InstancesBridge.swift in Sources */, 37169AA42729D98A0011DE61 /* InstancesBridge.swift in Sources */,
37D4B18E26717B3800C925CA /* VideoCell.swift in Sources */, 37D4B18E26717B3800C925CA /* VideoCell.swift in Sources */,
371B7E682759786B00D21217 /* Comment+Fixtures.swift in Sources */,
37BE0BD126A0E2D50092E2DB /* VideoPlayerView.swift in Sources */, 37BE0BD126A0E2D50092E2DB /* VideoPlayerView.swift in Sources */,
37AAF27E26737323007FC770 /* PopularView.swift in Sources */, 37AAF27E26737323007FC770 /* PopularView.swift in Sources */,
3743CA50270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */, 3743CA50270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */,
@ -2162,6 +2216,7 @@
37141675267A8E10006CA35D /* Country.swift in Sources */, 37141675267A8E10006CA35D /* Country.swift in Sources */,
3782B9542755667600990149 /* String+Format.swift in Sources */, 3782B9542755667600990149 /* String+Format.swift in Sources */,
37152EEC26EFEB95004FB96D /* LazyView.swift in Sources */, 37152EEC26EFEB95004FB96D /* LazyView.swift in Sources */,
37EF9A78275BEB8E0043B585 /* CommentView.swift in Sources */,
37484C2726FC83E000287258 /* InstanceForm.swift in Sources */, 37484C2726FC83E000287258 /* InstanceForm.swift in Sources */,
37F49BA826CB0FCE00304AC0 /* PlaylistFormView.swift in Sources */, 37F49BA826CB0FCE00304AC0 /* PlaylistFormView.swift in Sources */,
373197DA2732060100EF734F /* RelatedView.swift in Sources */, 373197DA2732060100EF734F /* RelatedView.swift in Sources */,
@ -2172,6 +2227,7 @@
37599F36272B44000087F250 /* FavoritesModel.swift in Sources */, 37599F36272B44000087F250 /* FavoritesModel.swift in Sources */,
3705B184267B4E4900704544 /* TrendingCategory.swift in Sources */, 3705B184267B4E4900704544 /* TrendingCategory.swift in Sources */,
37E084AD2753D95F00039B7D /* AccountsNavigationLink.swift in Sources */, 37E084AD2753D95F00039B7D /* AccountsNavigationLink.swift in Sources */,
371B7E6C2759791900D21217 /* CommentsModel.swift in Sources */,
3782B95627557E4E00990149 /* SearchView.swift in Sources */, 3782B95627557E4E00990149 /* SearchView.swift in Sources */,
3761ABFF26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */, 3761ABFF26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */,
37C3A24F272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */, 37C3A24F272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */,

View File

@ -3,13 +3,17 @@ import SwiftUI
struct NowPlayingView: View { struct NowPlayingView: View {
enum ViewSection: CaseIterable { 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 var inInfoViewController = false
@State private var repliesID: Comment.ID?
@EnvironmentObject<CommentsModel> private var comments
@EnvironmentObject<PlayerModel> private var player @EnvironmentObject<PlayerModel> private var player
@EnvironmentObject<RecentsModel> private var recents
@Default(.saveHistory) private var saveHistory @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)) .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 20))
.padding(.vertical, 20) .padding(.vertical, 20)