2022-08-20 03:25:02 +05:30
|
|
|
#if os(iOS)
|
|
|
|
import ActiveLabel
|
|
|
|
#endif
|
|
|
|
import Defaults
|
|
|
|
import Foundation
|
|
|
|
import SwiftUI
|
|
|
|
|
|
|
|
struct VideoDescription: View {
|
2022-11-25 02:06:05 +05:30
|
|
|
private var search: SearchModel { .shared }
|
2022-08-20 03:25:02 +05:30
|
|
|
@Default(.showKeywords) private var showKeywords
|
2023-04-22 22:52:13 +05:30
|
|
|
@Default(.expandVideoDescription) private var expandVideoDescription
|
2023-11-23 20:11:33 +05:30
|
|
|
@Default(.collapsedLinesDescription) private var collapsedLinesDescription
|
2022-08-20 03:25:02 +05:30
|
|
|
|
|
|
|
var video: Video
|
|
|
|
var detailsSize: CGSize?
|
2023-04-22 22:52:13 +05:30
|
|
|
@Binding var expand: Bool
|
2022-08-20 03:25:02 +05:30
|
|
|
|
|
|
|
var description: String {
|
|
|
|
video.description ?? ""
|
|
|
|
}
|
|
|
|
|
|
|
|
var body: some View {
|
2023-11-21 02:21:28 +05:30
|
|
|
descriptionView.id(video.videoID)
|
2023-04-22 22:52:13 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
var descriptionView: some View {
|
2022-08-20 03:25:02 +05:30
|
|
|
VStack {
|
|
|
|
#if os(iOS)
|
2023-04-22 22:52:13 +05:30
|
|
|
ActiveLabelDescriptionRepresentable(
|
|
|
|
description: description,
|
|
|
|
detailsSize: detailsSize,
|
|
|
|
expand: shouldExpand
|
|
|
|
)
|
2022-08-20 03:25:02 +05:30
|
|
|
#else
|
|
|
|
textDescription
|
|
|
|
#endif
|
|
|
|
|
|
|
|
keywords
|
|
|
|
}
|
2023-04-22 23:36:30 +05:30
|
|
|
.contentShape(Rectangle())
|
2023-04-22 22:52:13 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
var shouldExpand: Bool {
|
2023-11-21 02:21:28 +05:30
|
|
|
expand
|
2022-08-20 03:25:02 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
@ViewBuilder var textDescription: some View {
|
|
|
|
#if !os(iOS)
|
2023-10-15 17:05:23 +05:30
|
|
|
Group {
|
|
|
|
if #available(macOS 12, *) {
|
2023-11-21 05:28:40 +05:30
|
|
|
DescriptionWithLinks(description: description, detailsSize: detailsSize)
|
2023-10-15 17:05:23 +05:30
|
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
2023-11-23 20:11:33 +05:30
|
|
|
.lineLimit(shouldExpand ? 500 : collapsedLinesDescription)
|
2023-10-15 17:05:23 +05:30
|
|
|
#if !os(tvOS)
|
|
|
|
.textSelection(.enabled)
|
|
|
|
#endif
|
|
|
|
} else {
|
|
|
|
Text(description)
|
|
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
2023-11-23 20:11:33 +05:30
|
|
|
.lineLimit(shouldExpand ? 500 : collapsedLinesDescription)
|
2023-10-15 17:05:23 +05:30
|
|
|
}
|
|
|
|
}
|
|
|
|
.multilineTextAlignment(.leading)
|
|
|
|
.font(.system(size: 14))
|
|
|
|
.lineSpacing(3)
|
2022-08-20 03:25:02 +05:30
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
2023-11-21 05:28:40 +05:30
|
|
|
// If possibe convert URLs to clickable links
|
|
|
|
#if os(macOS)
|
2023-11-22 14:51:21 +05:30
|
|
|
@available(macOS 12, *)
|
|
|
|
struct DescriptionWithLinks: View {
|
|
|
|
let description: String
|
|
|
|
let detailsSize: CGSize?
|
|
|
|
let separators = CharacterSet(charactersIn: " \n")
|
2023-11-21 05:28:40 +05:30
|
|
|
|
2023-11-22 14:51:21 +05:30
|
|
|
var formattedString: AttributedString {
|
|
|
|
var attrString = AttributedString(description)
|
|
|
|
let words = description.unicodeScalars.split(whereSeparator: separators.contains).map(String.init)
|
|
|
|
words.forEach { word in
|
|
|
|
if word.hasPrefix("https://") || word.hasPrefix("http://"), let url = URL(string: String(word)) {
|
|
|
|
if let range = attrString.range(of: word) {
|
|
|
|
attrString[range].link = url
|
|
|
|
}
|
2023-11-21 05:28:40 +05:30
|
|
|
}
|
|
|
|
}
|
2023-11-22 14:51:21 +05:30
|
|
|
return attrString
|
2023-11-21 05:28:40 +05:30
|
|
|
}
|
|
|
|
|
2023-11-22 14:51:21 +05:30
|
|
|
var body: some View {
|
2023-11-21 05:28:40 +05:30
|
|
|
Text(formattedString)
|
2023-11-22 14:51:21 +05:30
|
|
|
}
|
2023-11-21 05:28:40 +05:30
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
2022-08-20 03:25:02 +05:30
|
|
|
@ViewBuilder var keywords: some View {
|
|
|
|
if showKeywords {
|
|
|
|
ScrollView(.horizontal, showsIndicators: showScrollIndicators) {
|
|
|
|
HStack {
|
|
|
|
ForEach(video.keywords, id: \.self) { keyword in
|
|
|
|
Button {
|
2022-11-25 02:06:05 +05:30
|
|
|
NavigationModel.shared.openSearchQuery(keyword)
|
2022-08-20 03:25:02 +05:30
|
|
|
} label: {
|
2022-12-18 18:09:39 +05:30
|
|
|
HStack(spacing: 0) {
|
2022-08-20 03:25:02 +05:30
|
|
|
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 22:52:13 +05:30
|
|
|
var expand: Bool
|
2022-08-20 03:25:02 +05:30
|
|
|
|
|
|
|
@State private var label = ActiveLabel()
|
|
|
|
|
|
|
|
@Environment(\.openURL) private var openURL
|
2022-11-25 02:06:05 +05:30
|
|
|
|
2023-11-23 20:11:33 +05:30
|
|
|
@Default(.collapsedLinesDescription) private var collapsedLinesDescription
|
|
|
|
|
2022-11-25 02:06:05 +05:30
|
|
|
var player = PlayerModel.shared
|
2022-08-20 03:25:02 +05:30
|
|
|
|
|
|
|
func makeUIView(context _: Context) -> some UIView {
|
|
|
|
customizeLabel()
|
|
|
|
return label
|
|
|
|
}
|
|
|
|
|
|
|
|
func updateUIView(_: UIViewType, context _: Context) {
|
2022-08-24 02:44:13 +05:30
|
|
|
updatePreferredMaxLayoutWidth()
|
2023-04-22 22:52:13 +05:30
|
|
|
updateNumberOfLines()
|
2022-08-20 03:25:02 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
2022-08-28 22:48:49 +05:30
|
|
|
label.handleURLTap(urlTapHandler(_:))
|
|
|
|
label.handleTimestampTap(timestampTapHandler(_:))
|
2022-08-20 03:25:02 +05:30
|
|
|
}
|
2023-04-22 22:52:13 +05:30
|
|
|
updateNumberOfLines()
|
2022-08-20 03:25:02 +05:30
|
|
|
}
|
2022-08-24 02:44:13 +05:30
|
|
|
|
|
|
|
func updatePreferredMaxLayoutWidth() {
|
|
|
|
label.preferredMaxLayoutWidth = (detailsSize?.width ?? 330) - 30
|
|
|
|
}
|
2022-08-28 22:48:49 +05:30
|
|
|
|
2023-04-22 22:52:13 +05:30
|
|
|
func updateNumberOfLines() {
|
2023-11-23 20:11:33 +05:30
|
|
|
label.numberOfLines = expand ? 0 : collapsedLinesDescription
|
2023-04-22 22:52:13 +05:30
|
|
|
}
|
|
|
|
|
2022-08-28 22:48:49 +05:30
|
|
|
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-19 02:57:21 +05:30
|
|
|
let parser = URLParser(url: urlToOpen, allowFileURLs: false)
|
2022-08-28 22:48:49 +05:30
|
|
|
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 17:39:51 +05:30
|
|
|
}
|
|
|
|
if destination != nil {
|
2022-08-28 22:48:49 +05:30
|
|
|
urlToOpen = yatteeURL
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
openURL(urlToOpen)
|
|
|
|
}
|
|
|
|
|
|
|
|
func timestampTapHandler(_ timestamp: Timestamp) {
|
|
|
|
player.backend.seek(to: timestamp.timeInterval, seekType: .userInteracted)
|
|
|
|
}
|
2022-08-20 03:25:02 +05:30
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
|
|
struct VideoDescription_Previews: PreviewProvider {
|
|
|
|
static var previews: some View {
|
2023-04-22 22:52:13 +05:30
|
|
|
VideoDescription(video: .fixture, expand: .constant(false))
|
2022-08-20 03:25:02 +05:30
|
|
|
.injectFixtureEnvironmentObjects()
|
|
|
|
}
|
|
|
|
}
|