mirror of
https://github.com/yattee/yattee.git
synced 2025-08-05 02:04:07 +00:00
Add tappable description links and timestamps in iOS
This commit is contained in:
@@ -102,8 +102,6 @@ struct PlayerControls: View {
|
||||
if model.presentingDetailsOverlay {
|
||||
VideoDetailsOverlay()
|
||||
.frame(maxWidth: detailsWidth, maxHeight: detailsHeight)
|
||||
.modifier(ControlBackgroundModifier())
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||
.transition(.opacity)
|
||||
}
|
||||
|
||||
|
@@ -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> {
|
||||
|
147
Shared/Player/VideoDescription.swift
Normal file
147
Shared/Player/VideoDescription.swift
Normal 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()
|
||||
}
|
||||
}
|
@@ -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 {
|
||||
|
@@ -303,6 +303,11 @@ struct VideoPlayerView: View {
|
||||
playerSize: player.playerSize,
|
||||
fullScreen: fullScreenDetails
|
||||
))
|
||||
.onDisappear {
|
||||
if player.presentingPlayer {
|
||||
player.setNeedsDrawing(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
Reference in New Issue
Block a user