mirror of
https://github.com/yattee/yattee.git
synced 2024-12-22 21:43:41 +00:00
Show channel thumbnail in player
This commit is contained in:
parent
02e66e4520
commit
c4ca5eb4c7
@ -55,11 +55,12 @@ extension VideosAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func shareURL(_ item: ContentItem, frontendHost: String? = nil, time: CMTime? = nil) -> URL? {
|
func shareURL(_ item: ContentItem, frontendHost: String? = nil, time: CMTime? = nil) -> URL? {
|
||||||
guard let frontendHost = frontendHost ?? account.instance.frontendHost else {
|
guard let frontendHost = frontendHost ?? account?.instance?.frontendHost,
|
||||||
|
var urlComponents = account?.instance?.urlComponents
|
||||||
|
else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var urlComponents = account.instance.urlComponents
|
|
||||||
urlComponents.host = frontendHost
|
urlComponents.host = frontendHost
|
||||||
|
|
||||||
var queryItems = [URLQueryItem]()
|
var queryItems = [URLQueryItem]()
|
||||||
|
@ -28,6 +28,10 @@ struct Channel: Identifiable, Hashable {
|
|||||||
self.videos = videos
|
self.videos = videos
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var detailsLoaded: Bool {
|
||||||
|
!subscriptionsString.isNil
|
||||||
|
}
|
||||||
|
|
||||||
var subscriptionsString: String? {
|
var subscriptionsString: String? {
|
||||||
if subscriptionsCount != nil, subscriptionsCount! > 0 {
|
if subscriptionsCount != nil, subscriptionsCount! > 0 {
|
||||||
return subscriptionsCount!.formattedAsAbbreviation()
|
return subscriptionsCount!.formattedAsAbbreviation()
|
||||||
|
@ -33,15 +33,17 @@ final class PlayerModel: ObservableObject {
|
|||||||
@Published var currentItem: PlayerQueueItem! { didSet { Defaults[.lastPlayed] = currentItem } }
|
@Published var currentItem: PlayerQueueItem! { didSet { Defaults[.lastPlayed] = currentItem } }
|
||||||
@Published var history = [PlayerQueueItem]() { didSet { Defaults[.history] = history } }
|
@Published var history = [PlayerQueueItem]() { didSet { Defaults[.history] = history } }
|
||||||
|
|
||||||
@Published var savedTime: CMTime?
|
@Published var preservedTime: CMTime?
|
||||||
|
|
||||||
@Published var playerNavigationLinkActive = false
|
@Published var playerNavigationLinkActive = false { didSet { pauseOnChannelPlayerDismiss() } }
|
||||||
|
|
||||||
@Published var sponsorBlock = SponsorBlockAPI()
|
@Published var sponsorBlock = SponsorBlockAPI()
|
||||||
@Published var segmentRestorationTime: CMTime?
|
@Published var segmentRestorationTime: CMTime?
|
||||||
@Published var lastSkipped: Segment? { didSet { rebuildTVMenu() } }
|
@Published var lastSkipped: Segment? { didSet { rebuildTVMenu() } }
|
||||||
@Published var restoredSegments = [Segment]()
|
@Published var restoredSegments = [Segment]()
|
||||||
|
|
||||||
|
@Published var channelWithDetails: Channel?
|
||||||
|
|
||||||
var accounts: AccountsModel
|
var accounts: AccountsModel
|
||||||
var comments: CommentsModel
|
var comments: CommentsModel
|
||||||
|
|
||||||
@ -177,6 +179,14 @@ final class PlayerModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func pauseOnChannelPlayerDismiss() {
|
||||||
|
if !playingInPictureInPicture, !playerNavigationLinkActive {
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||||
|
self.pause()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func insertPlayerItem(
|
private func insertPlayerItem(
|
||||||
_ stream: Stream,
|
_ stream: Stream,
|
||||||
for video: Video,
|
for video: Video,
|
||||||
@ -212,18 +222,18 @@ final class PlayerModel: ObservableObject {
|
|||||||
|
|
||||||
let replaceItemAndSeek = {
|
let replaceItemAndSeek = {
|
||||||
self.player.replaceCurrentItem(with: playerItem)
|
self.player.replaceCurrentItem(with: playerItem)
|
||||||
self.seekToSavedTime { finished in
|
self.seekToPreservedTime { finished in
|
||||||
guard finished else {
|
guard finished else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.savedTime = nil
|
self.preservedTime = nil
|
||||||
|
|
||||||
startPlaying()
|
startPlaying()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if preservingTime {
|
if preservingTime {
|
||||||
if savedTime.isNil {
|
if preservedTime.isNil {
|
||||||
saveTime {
|
saveTime {
|
||||||
replaceItemAndSeek()
|
replaceItemAndSeek()
|
||||||
startPlaying()
|
startPlaying()
|
||||||
@ -394,13 +404,13 @@ final class PlayerModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
self?.savedTime = currentTime
|
self?.preservedTime = currentTime
|
||||||
completionHandler()
|
completionHandler()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func seekToSavedTime(completionHandler: @escaping (Bool) -> Void = { _ in }) {
|
private func seekToPreservedTime(completionHandler: @escaping (Bool) -> Void = { _ in }) {
|
||||||
guard let time = savedTime else {
|
guard let time = preservedTime else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -528,6 +538,36 @@ final class PlayerModel: ObservableObject {
|
|||||||
currentArtwork = MPMediaItemArtwork(boundsSize: image!.size) { _ in image! }
|
currentArtwork = MPMediaItemArtwork(boundsSize: image!.size) { _ in image! }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadCurrentItemChannelDetails() {
|
||||||
|
guard let video = currentVideo,
|
||||||
|
!video.channel.detailsLoaded
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if restoreLoadedChannel() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
accounts.api.channel(video.channel.id).load().onSuccess { [weak self] response in
|
||||||
|
if let channel: Channel = response.typedContent() {
|
||||||
|
self?.channelWithDetails = channel
|
||||||
|
withAnimation {
|
||||||
|
self?.currentItem.video.channel = channel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult func restoreLoadedChannel() -> Bool {
|
||||||
|
if !currentVideo.isNil, channelWithDetails?.id == currentVideo!.channel.id {
|
||||||
|
currentItem.video.channel = channelWithDetails!
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func rateLabel(_ rate: Float) -> String {
|
func rateLabel(_ rate: Float) -> String {
|
||||||
let formatter = NumberFormatter()
|
let formatter = NumberFormatter()
|
||||||
formatter.minimumFractionDigits = 0
|
formatter.minimumFractionDigits = 0
|
||||||
|
@ -51,7 +51,8 @@ extension PlayerModel {
|
|||||||
currentItem.video = video!
|
currentItem.video = video!
|
||||||
}
|
}
|
||||||
|
|
||||||
savedTime = currentItem.playbackTime
|
preservedTime = currentItem.playbackTime
|
||||||
|
restoreLoadedChannel()
|
||||||
|
|
||||||
loadAvailableStreams(currentVideo!) { streams in
|
loadAvailableStreams(currentVideo!) { streams in
|
||||||
guard let stream = self.preferredStream(streams) else {
|
guard let stream = self.preferredStream(streams) else {
|
||||||
@ -126,7 +127,7 @@ extension PlayerModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func isAutoplaying(_ item: AVPlayerItem) -> Bool {
|
func isAutoplaying(_ item: AVPlayerItem) -> Bool {
|
||||||
player.currentItem == item && presentingPlayer
|
player.currentItem == item && (presentingPlayer || playerNavigationLinkActive || playingInPictureInPicture)
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult func enqueueVideo(
|
@discardableResult func enqueueVideo(
|
||||||
|
@ -33,6 +33,7 @@ extension Defaults.Keys {
|
|||||||
static let playerSidebar = Key<PlayerSidebarSetting>("playerSidebar", default: PlayerSidebarSetting.defaultValue)
|
static let playerSidebar = Key<PlayerSidebarSetting>("playerSidebar", default: PlayerSidebarSetting.defaultValue)
|
||||||
static let playerInstanceID = Key<Instance.ID?>("playerInstance")
|
static let playerInstanceID = Key<Instance.ID?>("playerInstance")
|
||||||
static let showKeywords = Key<Bool>("showKeywords", default: false)
|
static let showKeywords = Key<Bool>("showKeywords", default: false)
|
||||||
|
static let showChannelSubscribers = Key<Bool>("showChannelSubscribers", default: true)
|
||||||
static let commentsInstanceID = Key<Instance.ID?>("commentsInstance", default: kavinPipedInstanceID)
|
static let commentsInstanceID = Key<Instance.ID?>("commentsInstance", default: kavinPipedInstanceID)
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
static let commentsPlacement = Key<CommentsPlacement>("commentsPlacement", default: .separate)
|
static let commentsPlacement = Key<CommentsPlacement>("commentsPlacement", default: .separate)
|
||||||
|
@ -5,6 +5,7 @@ struct Player: UIViewControllerRepresentable {
|
|||||||
@EnvironmentObject<CommentsModel> private var comments
|
@EnvironmentObject<CommentsModel> private var comments
|
||||||
@EnvironmentObject<NavigationModel> private var navigation
|
@EnvironmentObject<NavigationModel> private var navigation
|
||||||
@EnvironmentObject<PlayerModel> private var player
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
|
@EnvironmentObject<SubscriptionsModel> private var subscriptions
|
||||||
|
|
||||||
var controller: PlayerViewController?
|
var controller: PlayerViewController?
|
||||||
|
|
||||||
@ -22,6 +23,7 @@ struct Player: UIViewControllerRepresentable {
|
|||||||
controller.commentsModel = comments
|
controller.commentsModel = comments
|
||||||
controller.navigationModel = navigation
|
controller.navigationModel = navigation
|
||||||
controller.playerModel = player
|
controller.playerModel = player
|
||||||
|
controller.subscriptionsModel = subscriptions
|
||||||
player.controller = controller
|
player.controller = controller
|
||||||
|
|
||||||
return controller
|
return controller
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import AVKit
|
import AVKit
|
||||||
import Logging
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
final class PlayerViewController: UIViewController {
|
final class PlayerViewController: UIViewController {
|
||||||
@ -7,6 +6,7 @@ final class PlayerViewController: UIViewController {
|
|||||||
var commentsModel: CommentsModel!
|
var commentsModel: CommentsModel!
|
||||||
var navigationModel: NavigationModel!
|
var navigationModel: NavigationModel!
|
||||||
var playerModel: PlayerModel!
|
var playerModel: PlayerModel!
|
||||||
|
var subscriptionsModel: SubscriptionsModel!
|
||||||
var playerViewController = AVPlayerViewController()
|
var playerViewController = AVPlayerViewController()
|
||||||
|
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
@ -71,6 +71,7 @@ final class PlayerViewController: UIViewController {
|
|||||||
.frame(maxHeight: 600)
|
.frame(maxHeight: 600)
|
||||||
.environmentObject(commentsModel)
|
.environmentObject(commentsModel)
|
||||||
.environmentObject(playerModel)
|
.environmentObject(playerModel)
|
||||||
|
.environmentObject(subscriptionsModel)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import Defaults
|
import Defaults
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import SDWebImageSwiftUI
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct VideoDetails: View {
|
struct VideoDetails: View {
|
||||||
@ -20,11 +21,15 @@ struct VideoDetails: View {
|
|||||||
|
|
||||||
@Environment(\.presentationMode) private var presentationMode
|
@Environment(\.presentationMode) private var presentationMode
|
||||||
@Environment(\.inNavigationView) private var inNavigationView
|
@Environment(\.inNavigationView) private var inNavigationView
|
||||||
|
@Environment(\.navigationStyle) private var navigationStyle
|
||||||
|
|
||||||
@EnvironmentObject<AccountsModel> private var accounts
|
@EnvironmentObject<AccountsModel> private var accounts
|
||||||
|
@EnvironmentObject<NavigationModel> private var navigation
|
||||||
@EnvironmentObject<PlayerModel> private var player
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
|
@EnvironmentObject<RecentsModel> private var recents
|
||||||
@EnvironmentObject<SubscriptionsModel> private var subscriptions
|
@EnvironmentObject<SubscriptionsModel> private var subscriptions
|
||||||
|
|
||||||
|
@Default(.showChannelSubscribers) private var showChannelSubscribers
|
||||||
@Default(.showKeywords) private var showKeywords
|
@Default(.showKeywords) private var showKeywords
|
||||||
|
|
||||||
init(
|
init(
|
||||||
@ -65,7 +70,9 @@ struct VideoDetails: View {
|
|||||||
}
|
}
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
|
|
||||||
if CommentsModel.enabled, CommentsModel.placement == .separate {
|
if !sidebarQueue ||
|
||||||
|
(CommentsModel.enabled && CommentsModel.placement == .separate)
|
||||||
|
{
|
||||||
pagePicker
|
pagePicker
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
}
|
}
|
||||||
@ -178,21 +185,52 @@ struct VideoDetails: View {
|
|||||||
Group {
|
Group {
|
||||||
if video != nil {
|
if video != nil {
|
||||||
HStack(alignment: .center) {
|
HStack(alignment: .center) {
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 10) {
|
||||||
|
Group {
|
||||||
|
ZStack(alignment: .bottomTrailing) {
|
||||||
|
authorAvatar
|
||||||
|
|
||||||
if subscribed {
|
if subscribed {
|
||||||
Image(systemName: "star.circle.fill")
|
Image(systemName: "star.circle.fill")
|
||||||
|
.background(Color.background)
|
||||||
|
.clipShape(Circle())
|
||||||
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Text(video!.channel.name)
|
Text(video!.channel.name)
|
||||||
.font(.system(size: 13))
|
.font(.system(size: 14))
|
||||||
.bold()
|
.bold()
|
||||||
|
|
||||||
|
if showChannelSubscribers {
|
||||||
|
Group {
|
||||||
if let subscribers = video!.channel.subscriptionsString {
|
if let subscribers = video!.channel.subscriptionsString {
|
||||||
Text("\(subscribers) subscribers")
|
Text("\(subscribers) subscribers")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.foregroundColor(.secondary)
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.foregroundColor(.secondary)
|
}
|
||||||
|
.contentShape(RoundedRectangle(cornerRadius: 12))
|
||||||
|
.contextMenu {
|
||||||
|
if let video = video {
|
||||||
|
Button(action: {
|
||||||
|
NavigationModel.openChannel(
|
||||||
|
video.channel,
|
||||||
|
player: player,
|
||||||
|
recents: recents,
|
||||||
|
navigation: navigation,
|
||||||
|
navigationStyle: navigationStyle
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
Label("\(video.channel.name) Channel", systemImage: "rectangle.stack.fill.badge.person.crop")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if accounts.app.supportsSubscriptions {
|
if accounts.app.supportsSubscriptions {
|
||||||
Spacer()
|
Spacer()
|
||||||
@ -209,7 +247,7 @@ struct VideoDetails: View {
|
|||||||
.alert(isPresented: $presentingUnsubscribeAlert) {
|
.alert(isPresented: $presentingUnsubscribeAlert) {
|
||||||
Alert(
|
Alert(
|
||||||
title: Text(
|
title: Text(
|
||||||
"Are you you want to unsubscribe from \(video!.channel.name)?"
|
"Are you sure you want to unsubscribe from \(video!.channel.name)?"
|
||||||
),
|
),
|
||||||
primaryButton: .destructive(Text("Unsubscribe")) {
|
primaryButton: .destructive(Text("Unsubscribe")) {
|
||||||
subscriptions.unsubscribe(video!.channel.id)
|
subscriptions.unsubscribe(video!.channel.id)
|
||||||
@ -364,6 +402,22 @@ struct VideoDetails: View {
|
|||||||
ContentItem(video: player.currentVideo!)
|
ContentItem(video: player.currentVideo!)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var authorAvatar: some View {
|
||||||
|
Group {
|
||||||
|
if let video = video, let url = video.channel.thumbnailURL {
|
||||||
|
WebImage(url: url)
|
||||||
|
.resizable()
|
||||||
|
.placeholder {
|
||||||
|
Rectangle().fill(Color("PlaceholderColor"))
|
||||||
|
}
|
||||||
|
.retryOnAppear(false)
|
||||||
|
.indicator(.activity)
|
||||||
|
.clipShape(Circle())
|
||||||
|
.frame(width: 45, height: 45, alignment: .leading)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var detailsPage: some View {
|
var detailsPage: some View {
|
||||||
Group {
|
Group {
|
||||||
Group {
|
Group {
|
||||||
|
@ -7,6 +7,7 @@ struct PlaybackSettings: View {
|
|||||||
@Default(.quality) private var quality
|
@Default(.quality) private var quality
|
||||||
@Default(.playerSidebar) private var playerSidebar
|
@Default(.playerSidebar) private var playerSidebar
|
||||||
@Default(.showKeywords) private var showKeywords
|
@Default(.showKeywords) private var showKeywords
|
||||||
|
@Default(.showChannelSubscribers) private var channelSubscribers
|
||||||
@Default(.saveHistory) private var saveHistory
|
@Default(.saveHistory) private var saveHistory
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
@ -27,6 +28,7 @@ struct PlaybackSettings: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
keywordsToggle
|
keywordsToggle
|
||||||
|
channelSubscribersToggle
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
Section(header: SettingsHeader(text: "Source")) {
|
Section(header: SettingsHeader(text: "Source")) {
|
||||||
@ -44,6 +46,7 @@ struct PlaybackSettings: View {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
keywordsToggle
|
keywordsToggle
|
||||||
|
channelSubscribersToggle
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,6 +110,10 @@ struct PlaybackSettings: View {
|
|||||||
private var keywordsToggle: some View {
|
private var keywordsToggle: some View {
|
||||||
Toggle("Show video keywords", isOn: $showKeywords)
|
Toggle("Show video keywords", isOn: $showKeywords)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var channelSubscribersToggle: some View {
|
||||||
|
Toggle("Show channel subscribers count", isOn: $channelSubscribers)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PlaybackSettings_Previews: PreviewProvider {
|
struct PlaybackSettings_Previews: PreviewProvider {
|
||||||
|
Loading…
Reference in New Issue
Block a user