Add tappable description links and timestamps in iOS

This commit is contained in:
Arkadiusz Fal
2022-08-19 23:55:02 +02:00
parent eeda7a5c6e
commit 97fc8fa4b7
14 changed files with 308 additions and 70 deletions

View File

@@ -102,8 +102,6 @@ struct PlayerControls: View {
if model.presentingDetailsOverlay {
VideoDetailsOverlay()
.frame(maxWidth: detailsWidth, maxHeight: detailsHeight)
.modifier(ControlBackgroundModifier())
.clipShape(RoundedRectangle(cornerRadius: 4))
.transition(.opacity)
}

View File

@@ -6,6 +6,8 @@ struct VideoDetailsOverlay: View {
var body: some View {
VideoDetails(sidebarQueue: false, fullScreen: fullScreenBinding)
.modifier(ControlBackgroundModifier())
.clipShape(RoundedRectangle(cornerRadius: 4))
}
var fullScreenBinding: Binding<Bool> {

View File

@@ -0,0 +1,147 @@
#if os(iOS)
import ActiveLabel
#endif
import Defaults
import Foundation
import SwiftUI
struct VideoDescription: View {
@EnvironmentObject<NavigationModel> private var navigation
@EnvironmentObject<PlayerModel> private var player
@EnvironmentObject<RecentsModel> private var recents
@EnvironmentObject<SearchModel> private var search
@Default(.showKeywords) private var showKeywords
var video: Video
var detailsSize: CGSize?
var description: String {
video.description ?? ""
}
var body: some View {
VStack {
#if os(iOS)
ActiveLabelDescriptionRepresentable(description: description, detailsSize: detailsSize)
#else
textDescription
#endif
keywords
}
}
@ViewBuilder var textDescription: some View {
#if !os(iOS)
Group {
if #available(macOS 12, *) {
Text(description)
#if !os(tvOS)
.textSelection(.enabled)
#endif
} else {
Text(description)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.font(.system(size: 14))
.lineSpacing(3)
#endif
}
@ViewBuilder var keywords: some View {
if showKeywords {
ScrollView(.horizontal, showsIndicators: showScrollIndicators) {
HStack {
ForEach(video.keywords, id: \.self) { keyword in
Button {
NavigationModel.openSearchQuery(keyword, player: player, recents: recents, navigation: navigation, search: search)
} label: {
HStack(alignment: .center, 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?
@State private var label = ActiveLabel()
@Environment(\.openURL) private var openURL
@EnvironmentObject<PlayerModel> private var player
func makeUIView(context _: Context) -> some UIView {
customizeLabel()
return label
}
func updateUIView(_: UIViewType, context _: Context) {
customizeLabel()
}
func customizeLabel() {
label.customize { label in
label.enabledTypes = [.url, .timestamp]
label.numberOfLines = 0
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 { url in
var urlToOpen = url
if var components = URLComponents(url: url, resolvingAgainstBaseURL: false) {
components.scheme = "yattee"
if let yatteeURL = components.url,
URLParser(url: urlToOpen).destination != nil
{
urlToOpen = yatteeURL
}
}
openURL(urlToOpen)
}
label.handleTimestampTap { timestamp in
player.backend.seek(to: timestamp.timeInterval)
}
}
}
}
#endif
struct VideoDescription_Previews: PreviewProvider {
static var previews: some View {
VideoDescription(video: .fixture)
.injectFixtureEnvironmentObjects()
}
}

View File

@@ -33,6 +33,7 @@ struct VideoDetails: View {
@StateObject private var page: Page = .first()
@Environment(\.navigationStyle) private var navigationStyle
@Environment(\.verticalSizeClass) private var verticalSizeClass
@EnvironmentObject<AccountsModel> private var accounts
@EnvironmentObject<CommentsModel> private var comments
@@ -41,8 +42,6 @@ struct VideoDetails: View {
@EnvironmentObject<RecentsModel> private var recents
@EnvironmentObject<SubscriptionsModel> private var subscriptions
@Default(.videoDetailsPage) private var videoDetailsPage
@Default(.showKeywords) private var showKeywords
@Default(.playerDetailsPageButtonLabelStyle) private var playerDetailsPageButtonLabelStyle
var currentPage: DetailsPage {
@@ -92,12 +91,10 @@ struct VideoDetails: View {
if pageIndex == DetailsPage.comments.index {
comments.load()
}
videoDetailsPage = DetailsPage.allCases.first { $0.index == pageIndex } ?? .info
}
}
.onAppear {
page.update(.new(index: videoDetailsPage.index))
page.update(.moveToFirst)
guard video != nil, accounts.app.supportsSubscriptions else {
subscribed = false
@@ -114,11 +111,20 @@ struct VideoDetails: View {
}
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading)
.overlay(GeometryReader { proxy in
Color.clear
.onAppear {
detailsSize = proxy.size
}
.onChange(of: proxy.size) { newSize in
detailsSize = newSize
}
})
}
var publishedDateSection: some View {
Group {
if let video = player.currentVideo {
if let video = video {
HStack(spacing: 4) {
if let published = video.publishedDate {
Text(published)
@@ -144,7 +150,6 @@ struct VideoDetails: View {
Button(action: {
page.update(.new(index: destination.index))
pageChangeAction?()
videoDetailsPage = destination
}) {
HStack {
Spacer()
@@ -199,10 +204,12 @@ struct VideoDetails: View {
.contentShape(Rectangle())
}
@State private var detailsSize = CGSize.zero
var detailsPage: some View {
Group {
VStack(alignment: .leading, spacing: 0) {
if let video = player.currentVideo {
if let video = video {
VStack(spacing: 6) {
videoProperties
@@ -218,47 +225,13 @@ struct VideoDetails: View {
}
}
.redacted(reason: .placeholder)
} else if let description = video.description {
Group {
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) {
Text(description)
#if !os(tvOS)
.textSelection(.enabled)
#endif
} else {
Text(description)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.font(.system(size: 14))
.lineSpacing(3)
} else if video.description != nil, !video.description!.isEmpty {
VideoDescription(video: video, detailsSize: detailsSize)
.padding(.bottom, fullScreenLayout ? 10 : SafeArea.insets.bottom)
} else {
Text("No description")
.foregroundColor(.secondary)
}
if showKeywords {
ScrollView(.horizontal, showsIndicators: showScrollIndicators) {
HStack {
ForEach(video.keywords, id: \.self) { keyword in
HStack(alignment: .center, spacing: 0) {
Text("#")
.font(.system(size: 11).bold())
Text(keyword)
.frame(maxWidth: 500)
}
.font(.caption)
.foregroundColor(.white)
.padding(.vertical, 4)
.padding(.horizontal, 8)
.background(Color("KeywordBackgroundColor"))
.mask(RoundedRectangle(cornerRadius: 3))
}
}
.padding(.bottom, 10)
}
}
}
}
}
@@ -266,6 +239,14 @@ struct VideoDetails: View {
}
}
var fullScreenLayout: Bool {
#if os(iOS)
return player.playingFullScreen || verticalSizeClass == .compact
#else
return player.playingFullScreen
#endif
}
@ViewBuilder var videoProperties: some View {
HStack(spacing: 2) {
publishedDateSection
@@ -319,14 +300,6 @@ struct VideoDetails: View {
.frame(maxWidth: 100)
}
var showScrollIndicators: Bool {
#if os(macOS)
false
#else
true
#endif
}
}
struct VideoDetails_Previews: PreviewProvider {

View File

@@ -303,6 +303,11 @@ struct VideoPlayerView: View {
playerSize: player.playerSize,
fullScreen: fullScreenDetails
))
.onDisappear {
if player.presentingPlayer {
player.setNeedsDrawing(true)
}
}
}
#endif
}