mirror of
https://github.com/yattee/yattee.git
synced 2024-12-22 13:33:42 +00:00
Add related videos
This commit is contained in:
parent
f49453e871
commit
f8e6560698
@ -314,8 +314,8 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
likes: json["likeCount"].int,
|
||||
dislikes: json["dislikeCount"].int,
|
||||
keywords: json["keywords"].arrayValue.map { $0.stringValue },
|
||||
streams: extractFormatStreams(from: json["formatStreams"].arrayValue) +
|
||||
extractAdaptiveFormats(from: json["adaptiveFormats"].arrayValue)
|
||||
streams: extractStreams(from: json),
|
||||
related: extractRelated(from: json)
|
||||
)
|
||||
}
|
||||
|
||||
@ -349,6 +349,11 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
}
|
||||
}
|
||||
|
||||
private static func extractStreams(from json: JSON) -> [Stream] {
|
||||
extractFormatStreams(from: json["formatStreams"].arrayValue) +
|
||||
extractAdaptiveFormats(from: json["adaptiveFormats"].arrayValue)
|
||||
}
|
||||
|
||||
private static func extractFormatStreams(from streams: [JSON]) -> [Stream] {
|
||||
streams.map {
|
||||
SingleAssetStream(
|
||||
@ -378,4 +383,11 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private static func extractRelated(from content: JSON) -> [Video] {
|
||||
content
|
||||
.dictionaryValue["recommendedVideos"]?
|
||||
.arrayValue
|
||||
.compactMap(extractVideo(from:)) ?? []
|
||||
}
|
||||
}
|
||||
|
@ -224,7 +224,8 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
||||
thumbnails: thumbnails,
|
||||
likes: details["likes"]?.int,
|
||||
dislikes: details["dislikes"]?.int,
|
||||
streams: extractStreams(from: content)
|
||||
streams: extractStreams(from: content),
|
||||
related: extractRelated(from: content)
|
||||
)
|
||||
}
|
||||
|
||||
@ -310,6 +311,13 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
||||
return streams
|
||||
}
|
||||
|
||||
private static func extractRelated(from content: JSON) -> [Video] {
|
||||
content
|
||||
.dictionaryValue["relatedStreams"]?
|
||||
.arrayValue
|
||||
.compactMap(extractVideo(from:)) ?? []
|
||||
}
|
||||
|
||||
private static func compatibleAudioStreams(from content: JSON) -> [JSON] {
|
||||
content
|
||||
.dictionaryValue["audioStreams"]?
|
||||
|
@ -30,6 +30,8 @@ struct Video: Identifiable, Equatable, Hashable {
|
||||
|
||||
var channel: Channel
|
||||
|
||||
var related = [Video]()
|
||||
|
||||
init(
|
||||
id: String? = nil,
|
||||
videoID: String,
|
||||
@ -49,7 +51,8 @@ struct Video: Identifiable, Equatable, Hashable {
|
||||
likes: Int? = nil,
|
||||
dislikes: Int? = nil,
|
||||
keywords: [String] = [],
|
||||
streams: [Stream] = []
|
||||
streams: [Stream] = [],
|
||||
related: [Video] = []
|
||||
) {
|
||||
self.id = id ?? UUID().uuidString
|
||||
self.videoID = videoID
|
||||
@ -70,6 +73,7 @@ struct Video: Identifiable, Equatable, Hashable {
|
||||
self.dislikes = dislikes
|
||||
self.keywords = keywords
|
||||
self.streams = streams
|
||||
self.related = related
|
||||
}
|
||||
|
||||
var publishedDate: String? {
|
||||
|
@ -82,6 +82,8 @@
|
||||
372915E72687E3B900F5A35B /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372915E52687E3B900F5A35B /* Defaults.swift */; };
|
||||
372915E82687E3B900F5A35B /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372915E52687E3B900F5A35B /* Defaults.swift */; };
|
||||
3730D8A02712E2B70020ED53 /* NowPlayingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3730D89F2712E2B70020ED53 /* NowPlayingView.swift */; };
|
||||
373197D92732015300EF734F /* RelatedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373197D82732015300EF734F /* RelatedView.swift */; };
|
||||
373197DA2732060100EF734F /* RelatedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373197D82732015300EF734F /* RelatedView.swift */; };
|
||||
37319F0527103F94004ECCD0 /* PlayerQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37319F0427103F94004ECCD0 /* PlayerQueue.swift */; };
|
||||
37319F0627103F94004ECCD0 /* PlayerQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37319F0427103F94004ECCD0 /* PlayerQueue.swift */; };
|
||||
37319F0727103F94004ECCD0 /* PlayerQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37319F0427103F94004ECCD0 /* PlayerQueue.swift */; };
|
||||
@ -525,6 +527,7 @@
|
||||
371F2F19269B43D300E4A7AB /* NavigationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModel.swift; sourceTree = "<group>"; };
|
||||
372915E52687E3B900F5A35B /* Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Defaults.swift; sourceTree = "<group>"; };
|
||||
3730D89F2712E2B70020ED53 /* NowPlayingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingView.swift; sourceTree = "<group>"; };
|
||||
373197D82732015300EF734F /* RelatedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelatedView.swift; sourceTree = "<group>"; };
|
||||
37319F0427103F94004ECCD0 /* PlayerQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerQueue.swift; sourceTree = "<group>"; };
|
||||
373CFACA26966264003CB2C6 /* SearchQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchQuery.swift; sourceTree = "<group>"; };
|
||||
373CFADA269663F1003CB2C6 /* Thumbnail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thumbnail.swift; sourceTree = "<group>"; };
|
||||
@ -783,6 +786,7 @@
|
||||
3743CA4D270EFE3400E4D32B /* PlayerQueueRow.swift */,
|
||||
37CC3F4B270CFE1700608308 /* PlayerQueueView.swift */,
|
||||
37BE0BD526A1D4A90092E2DB /* PlayerViewController.swift */,
|
||||
373197D82732015300EF734F /* RelatedView.swift */,
|
||||
37B81AFE26D2CA3700675966 /* VideoDetails.swift */,
|
||||
37B81AFB26D2C9C900675966 /* VideoDetailsPaddingModifier.swift */,
|
||||
37B81AF826D2C9A700675966 /* VideoPlayerSizeModifier.swift */,
|
||||
@ -1743,6 +1747,7 @@
|
||||
3748186E26A769D60084E870 /* DetailBadge.swift in Sources */,
|
||||
37AAF2A026741C97007FC770 /* SubscriptionsView.swift in Sources */,
|
||||
37599F30272B42810087F250 /* FavoriteItem.swift in Sources */,
|
||||
373197D92732015300EF734F /* RelatedView.swift in Sources */,
|
||||
373CFAEB26975CBF003CB2C6 /* PlaylistFormView.swift in Sources */,
|
||||
37B81B0226D2CAE700675966 /* PlaybackBar.swift in Sources */,
|
||||
372915E62687E3B900F5A35B /* Defaults.swift in Sources */,
|
||||
@ -2011,6 +2016,7 @@
|
||||
37152EEC26EFEB95004FB96D /* LazyView.swift in Sources */,
|
||||
37484C2726FC83E000287258 /* InstanceForm.swift in Sources */,
|
||||
37F49BA826CB0FCE00304AC0 /* PlaylistFormView.swift in Sources */,
|
||||
373197DA2732060100EF734F /* RelatedView.swift in Sources */,
|
||||
37D4B19926717E1500C925CA /* Video.swift in Sources */,
|
||||
378E50FD26FE8B9F00F49626 /* Instance.swift in Sources */,
|
||||
37169AA82729E2CC0011DE61 /* AccountsBridge.swift in Sources */,
|
||||
|
@ -10,12 +10,12 @@ struct PlayerQueueView: View {
|
||||
List {
|
||||
Group {
|
||||
playingNext
|
||||
related
|
||||
playedPreviously
|
||||
}
|
||||
.padding(.vertical, 5)
|
||||
.listRowInsets(EdgeInsets())
|
||||
#if os(iOS)
|
||||
.padding(.horizontal, 10)
|
||||
#if !os(iOS)
|
||||
.padding(.vertical, 5)
|
||||
.listRowInsets(EdgeInsets())
|
||||
#endif
|
||||
}
|
||||
|
||||
@ -71,7 +71,27 @@ struct PlayerQueueView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func removeButton(_ item: PlayerQueueItem, history: Bool) -> some View {
|
||||
private var related: some View {
|
||||
Group {
|
||||
if !player.currentVideo.isNil, !player.currentVideo!.related.isEmpty {
|
||||
Section(header: Text("Related")) {
|
||||
ForEach(player.currentVideo!.related) { video in
|
||||
PlayerQueueRow(item: PlayerQueueItem(video), fullScreen: $fullScreen)
|
||||
.contextMenu {
|
||||
Button("Play Next") {
|
||||
player.playNext(video)
|
||||
}
|
||||
Button("Play Last") {
|
||||
player.enqueueVideo(video)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func removeButton(_ item: PlayerQueueItem, history: Bool) -> some View {
|
||||
Button(role: .destructive) {
|
||||
if history {
|
||||
player.removeHistory(item)
|
||||
@ -83,7 +103,7 @@ struct PlayerQueueView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func removeAllButton(history: Bool) -> some View {
|
||||
private func removeAllButton(history: Bool) -> some View {
|
||||
Button(role: .destructive) {
|
||||
if history {
|
||||
player.removeHistoryItems()
|
||||
|
@ -32,23 +32,29 @@ final class PlayerViewController: UIViewController {
|
||||
|
||||
#if os(tvOS)
|
||||
playerModel.avPlayerViewController = playerViewController
|
||||
playerViewController.customInfoViewControllers = [playerQueueInfoViewController]
|
||||
playerViewController.customInfoViewControllers = [
|
||||
infoViewController([.related], title: "Related"),
|
||||
infoViewController([.playingNext, .playedPreviously], title: "Playing Next")
|
||||
]
|
||||
#else
|
||||
embedViewController()
|
||||
#endif
|
||||
}
|
||||
|
||||
#if os(tvOS)
|
||||
var playerQueueInfoViewController: UIHostingController<AnyView> {
|
||||
func infoViewController(
|
||||
_ sections: [NowPlayingView.ViewSection],
|
||||
title: String
|
||||
) -> UIHostingController<AnyView> {
|
||||
let controller = UIHostingController(rootView:
|
||||
AnyView(
|
||||
NowPlayingView(inInfoViewController: true)
|
||||
NowPlayingView(sections: sections, inInfoViewController: true)
|
||||
.frame(maxHeight: 600)
|
||||
.environmentObject(playerModel)
|
||||
)
|
||||
)
|
||||
|
||||
controller.title = "Playing Next"
|
||||
controller.title = title
|
||||
|
||||
return controller
|
||||
}
|
||||
|
30
Shared/Player/RelatedView.swift
Normal file
30
Shared/Player/RelatedView.swift
Normal file
@ -0,0 +1,30 @@
|
||||
import SwiftUI
|
||||
|
||||
struct RelatedView: View {
|
||||
@EnvironmentObject<PlayerModel> private var player
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
if !player.currentVideo.isNil, !player.currentVideo!.related.isEmpty {
|
||||
Section(header: Text("Related")) {
|
||||
ForEach(player.currentVideo!.related) { video in
|
||||
PlayerQueueRow(item: PlayerQueueItem(video), fullScreen: .constant(false))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#if os(macOS)
|
||||
.listStyle(.inset)
|
||||
#elseif os(iOS)
|
||||
.listStyle(.grouped)
|
||||
#else
|
||||
.listStyle(.plain)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
struct RelatedView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
RelatedView()
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@ import SwiftUI
|
||||
|
||||
struct VideoDetails: View {
|
||||
enum Page {
|
||||
case details, queue
|
||||
case details, queue, related
|
||||
}
|
||||
|
||||
@Binding var sidebarQueue: Bool
|
||||
@ -89,6 +89,14 @@ struct VideoDetails: View {
|
||||
case .queue:
|
||||
PlayerQueueView(fullScreen: $fullScreen)
|
||||
.edgesIgnoringSafeArea(.horizontal)
|
||||
|
||||
case .related:
|
||||
#if os(macOS)
|
||||
EmptyView()
|
||||
#else
|
||||
RelatedView()
|
||||
.edgesIgnoringSafeArea(.horizontal)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
.padding(.top, inNavigationView && fullScreen ? 10 : 0)
|
||||
@ -109,7 +117,9 @@ struct VideoDetails: View {
|
||||
.onChange(of: sidebarQueue) { queue in
|
||||
#if !os(macOS)
|
||||
if queue {
|
||||
currentPage = .details
|
||||
if currentPage == .queue {
|
||||
currentPage = .details
|
||||
}
|
||||
} else {
|
||||
currentPage = .queue
|
||||
}
|
||||
@ -216,6 +226,7 @@ struct VideoDetails: View {
|
||||
var pagePicker: some View {
|
||||
Picker("Page", selection: $currentPage) {
|
||||
Text("Details").tag(Page.details)
|
||||
Text("Related").tag(Page.related)
|
||||
Text("Queue").tag(Page.queue)
|
||||
}
|
||||
|
||||
|
@ -62,8 +62,14 @@ struct VideoPlayerView: View {
|
||||
if player.currentItem.isNil {
|
||||
playerPlaceholder(geometry: geometry)
|
||||
} else {
|
||||
player.playerView
|
||||
.modifier(VideoPlayerSizeModifier(geometry: geometry))
|
||||
#if os(macOS)
|
||||
Player()
|
||||
.modifier(VideoPlayerSizeModifier(geometry: geometry))
|
||||
|
||||
#else
|
||||
player.playerView
|
||||
.modifier(VideoPlayerSizeModifier(geometry: geometry))
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#if os(iOS)
|
||||
@ -107,6 +113,11 @@ struct VideoPlayerView: View {
|
||||
.frame(minWidth: 250)
|
||||
#endif
|
||||
}
|
||||
.onDisappear {
|
||||
if !player.playingInPictureInPicture {
|
||||
player.pause()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func playerPlaceholder(geometry: GeometryProxy) -> some View {
|
||||
|
@ -21,6 +21,7 @@ struct ShareButton: View {
|
||||
var body: some View {
|
||||
Menu {
|
||||
instanceActions
|
||||
Divider()
|
||||
youtubeActions
|
||||
} label: {
|
||||
Label("Share", systemImage: "square.and.arrow.up")
|
||||
|
@ -9,20 +9,26 @@ final class PictureInPictureDelegate: NSObject, AVPlayerViewPictureInPictureDele
|
||||
}
|
||||
|
||||
func playerViewWillStartPicture(inPicture _: AVPlayerView) {
|
||||
playerModel.playingInPictureInPicture = true
|
||||
playerModel.presentingPlayer = false
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
|
||||
self?.playerModel.playingInPictureInPicture = true
|
||||
self?.playerModel.presentingPlayer = false
|
||||
}
|
||||
}
|
||||
|
||||
func playerViewWillStopPicture(inPicture _: AVPlayerView) {
|
||||
playerModel.playingInPictureInPicture = false
|
||||
playerModel.presentPlayer()
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
|
||||
self?.playerModel.playingInPictureInPicture = false
|
||||
self?.playerModel.presentPlayer()
|
||||
}
|
||||
}
|
||||
|
||||
func playerView(
|
||||
_: AVPlayerView,
|
||||
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: (Bool) -> Void
|
||||
) {
|
||||
playerModel.presentingPlayer = true
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
|
||||
self?.playerModel.presentingPlayer = true
|
||||
}
|
||||
completionHandler(true)
|
||||
}
|
||||
}
|
||||
|
@ -7,9 +7,6 @@ final class PlayerViewController: NSViewController {
|
||||
var pictureInPictureDelegate = PictureInPictureDelegate()
|
||||
|
||||
override func viewDidDisappear() {
|
||||
if !playerModel.playingInPictureInPicture {
|
||||
playerModel.pause()
|
||||
}
|
||||
super.viewDidDisappear()
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,11 @@
|
||||
import SwiftUI
|
||||
|
||||
struct NowPlayingView: View {
|
||||
enum ViewSection: CaseIterable {
|
||||
case nowPlaying, playingNext, playedPreviously, related
|
||||
}
|
||||
|
||||
var sections = ViewSection.allCases
|
||||
var inInfoViewController = false
|
||||
|
||||
@EnvironmentObject<PlayerModel> private var player
|
||||
@ -18,7 +23,7 @@ struct NowPlayingView: View {
|
||||
var content: some View {
|
||||
List {
|
||||
Group {
|
||||
if !inInfoViewController, let item = player.currentItem {
|
||||
if sections.contains(.nowPlaying), let item = player.currentItem {
|
||||
Section(header: Text("Now Playing")) {
|
||||
Button {
|
||||
player.presentPlayer()
|
||||
@ -29,29 +34,53 @@ struct NowPlayingView: View {
|
||||
.onPlayPauseCommand(perform: player.togglePlay)
|
||||
}
|
||||
|
||||
Section(header: Text("Playing Next")) {
|
||||
if player.queue.isEmpty {
|
||||
Text("Playback queue is empty")
|
||||
.padding([.vertical, .leading], 40)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
ForEach(player.queue) { item in
|
||||
Button {
|
||||
player.advanceToItem(item)
|
||||
player.presentPlayer()
|
||||
} label: {
|
||||
VideoBanner(video: item.video)
|
||||
if sections.contains(.playingNext) {
|
||||
Section(header: Text("Playing Next")) {
|
||||
if player.queue.isEmpty {
|
||||
Text("Playback queue is empty")
|
||||
.padding([.vertical, .leading], 40)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.contextMenu {
|
||||
Button("Delete", role: .destructive) {
|
||||
player.remove(item)
|
||||
|
||||
ForEach(player.queue) { item in
|
||||
Button {
|
||||
player.advanceToItem(item)
|
||||
player.presentPlayer()
|
||||
} label: {
|
||||
VideoBanner(video: item.video)
|
||||
}
|
||||
.contextMenu {
|
||||
Button("Delete", role: .destructive) {
|
||||
player.remove(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !player.history.isEmpty {
|
||||
if sections.contains(.related), !player.currentVideo.isNil, !player.currentVideo!.related.isEmpty {
|
||||
Section(header: inInfoViewController ? AnyView(EmptyView()) : AnyView(Text("Related"))) {
|
||||
ForEach(player.currentVideo!.related) { video in
|
||||
Button {
|
||||
player.playNow(video)
|
||||
player.presentPlayer()
|
||||
} label: {
|
||||
VideoBanner(video: video)
|
||||
}
|
||||
.contextMenu {
|
||||
Button("Play Next") {
|
||||
player.playNext(video)
|
||||
}
|
||||
Button("Play Last") {
|
||||
player.enqueueVideo(video)
|
||||
}
|
||||
Button("Cancel", role: .cancel) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if sections.contains(.playedPreviously), !player.history.isEmpty {
|
||||
Section(header: Text("Played Previously")) {
|
||||
ForEach(player.history) { item in
|
||||
Button {
|
||||
@ -75,7 +104,6 @@ struct NowPlayingView: View {
|
||||
}
|
||||
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 20))
|
||||
.padding(.vertical, 20)
|
||||
// .padding(.horizontal, 40)
|
||||
}
|
||||
.padding(.horizontal, inInfoViewController ? 40 : 0)
|
||||
.listStyle(.grouped)
|
||||
@ -86,7 +114,6 @@ struct NowPlayingView: View {
|
||||
Text(text)
|
||||
.font((inInfoViewController ? Font.system(size: 40) : .title3).bold())
|
||||
.foregroundColor(.secondary)
|
||||
// .padding(.leading, 40)
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user