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,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
struct Player: UIViewControllerRepresentable {
@EnvironmentObject<CommentsModel> private var comments
@EnvironmentObject<NavigationModel> private var navigation
@EnvironmentObject<PlayerModel> 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

View File

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

View File

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