yattee/Shared/Player/Video Details/VideoDescription.swift

229 lines
7.1 KiB
Swift
Raw Normal View History

#if os(iOS)
import ActiveLabel
#endif
import Defaults
import Foundation
import SwiftUI
struct VideoDescription: View {
2023-04-22 17:22:13 +00:00
static let collapsedLines = 5
private var search: SearchModel { .shared }
@Default(.showKeywords) private var showKeywords
2023-04-22 17:22:13 +00:00
@Default(.expandVideoDescription) private var expandVideoDescription
var video: Video
var detailsSize: CGSize?
2023-04-22 17:22:13 +00:00
@Binding var expand: Bool
var description: String {
video.description ?? ""
}
var body: some View {
2023-04-22 17:22:13 +00:00
Group {
if !expandVideoDescription && !expand {
Button {
expand = true
} label: {
descriptionView
}
.buttonStyle(.plain)
} else {
descriptionView
}
}
.id(video.videoID)
}
var descriptionView: some View {
VStack {
#if os(iOS)
2023-04-22 17:22:13 +00:00
ActiveLabelDescriptionRepresentable(
description: description,
detailsSize: detailsSize,
expand: shouldExpand
)
#else
textDescription
#endif
keywords
}
2023-04-22 18:06:30 +00:00
.contentShape(Rectangle())
2023-04-22 17:22:13 +00:00
}
var shouldExpand: Bool {
expandVideoDescription || expand
}
@ViewBuilder var textDescription: some View {
#if !os(iOS)
Group {
if #available(macOS 12, *) {
DescriptionWithLinks(description: description, detailsSize: detailsSize)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(shouldExpand ? 500 : Self.collapsedLines)
#if !os(tvOS)
.textSelection(.enabled)
#endif
} else {
Text(description)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(shouldExpand ? 500 : Self.collapsedLines)
}
}
.multilineTextAlignment(.leading)
.font(.system(size: 14))
.lineSpacing(3)
#endif
}
// If possibe convert URLs to clickable links
#if os(macOS)
@available(macOS 12, *)
struct DescriptionWithLinks: View {
let description: String
let detailsSize: CGSize?
let separators = CharacterSet(charactersIn: " \n")
var formattedString: AttributedString {
var attrString = AttributedString(description)
let words = description.unicodeScalars.split(whereSeparator: separators.contains).map(String.init)
words.forEach { word in
2023-11-21 00:13:01 +00:00
if word.hasPrefix("https://") || word.hasPrefix("http://"), let url = URL(string: String(word)) {
if let range = attrString.range(of: word) {
attrString[range].link = url
}
}
}
return attrString
}
var body: some View {
Text(formattedString)
}
}
#endif
@ViewBuilder var keywords: some View {
if showKeywords {
ScrollView(.horizontal, showsIndicators: showScrollIndicators) {
HStack {
ForEach(video.keywords, id: \.self) { keyword in
Button {
NavigationModel.shared.openSearchQuery(keyword)
} label: {
2022-12-18 12:39:39 +00:00
HStack(spacing: 0) {
Text("#")
.font(.system(size: 14).bold())
Text(keyword)
.frame(maxWidth: 500)
}
.font(.caption)
.foregroundColor(.white)
.padding(.vertical, 4)
.padding(.horizontal, 8)
.background(Color("KeywordBackgroundColor"))
.mask(RoundedRectangle(cornerRadius: 3))
}
.buttonStyle(.plain)
}
}
}
}
}
var showScrollIndicators: Bool {
#if os(macOS)
false
#else
true
#endif
}
}
#if os(iOS)
struct ActiveLabelDescriptionRepresentable: UIViewRepresentable {
var description: String
var detailsSize: CGSize?
2023-04-22 17:22:13 +00:00
var expand: Bool
@State private var label = ActiveLabel()
@Environment(\.openURL) private var openURL
var player = PlayerModel.shared
func makeUIView(context _: Context) -> some UIView {
customizeLabel()
return label
}
func updateUIView(_: UIViewType, context _: Context) {
2022-08-23 21:14:13 +00:00
updatePreferredMaxLayoutWidth()
2023-04-22 17:22:13 +00:00
updateNumberOfLines()
}
func customizeLabel() {
label.customize { label in
label.enabledTypes = [.url, .timestamp]
label.text = description
label.contentMode = .scaleAspectFill
label.font = .systemFont(ofSize: 14)
label.lineSpacing = 3
label.preferredMaxLayoutWidth = (detailsSize?.width ?? 330) - 30
label.URLColor = UIColor(Color.accentColor)
label.timestampColor = UIColor(Color.accentColor)
label.handleURLTap(urlTapHandler(_:))
label.handleTimestampTap(timestampTapHandler(_:))
}
2023-04-22 17:22:13 +00:00
updateNumberOfLines()
}
2022-08-23 21:14:13 +00:00
func updatePreferredMaxLayoutWidth() {
label.preferredMaxLayoutWidth = (detailsSize?.width ?? 330) - 30
}
2023-04-22 17:22:13 +00:00
func updateNumberOfLines() {
label.numberOfLines = expand ? 0 : VideoDescription.collapsedLines
}
func urlTapHandler(_ url: URL) {
var urlToOpen = url
if var components = URLComponents(url: url, resolvingAgainstBaseURL: false) {
components.scheme = "yattee"
if let yatteeURL = components.url {
2022-11-18 21:27:21 +00:00
let parser = URLParser(url: urlToOpen, allowFileURLs: false)
let destination = parser.destination
if destination == .video,
parser.videoID == player.currentVideo?.videoID,
let time = parser.time
{
player.backend.seek(to: Double(time), seekType: .userInteracted)
return
2023-06-17 12:09:51 +00:00
}
if destination != nil {
urlToOpen = yatteeURL
}
}
}
openURL(urlToOpen)
}
func timestampTapHandler(_ timestamp: Timestamp) {
player.backend.seek(to: timestamp.timeInterval, seekType: .userInteracted)
}
}
#endif
struct VideoDescription_Previews: PreviewProvider {
static var previews: some View {
2023-04-22 17:22:13 +00:00
VideoDescription(video: .fixture, expand: .constant(false))
.injectFixtureEnvironmentObjects()
}
}