Show channel thumbnail in player

This commit is contained in:
Arkadiusz Fal 2021-12-17 21:01:05 +01:00
parent 02e66e4520
commit c4ca5eb4c7
9 changed files with 137 additions and 26 deletions

View File

@ -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]()

View File

@ -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()

View File

@ -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

View File

@ -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(

View File

@ -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)

View File

@ -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

View File

@ -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)
) )
) )

View File

@ -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 {

View File

@ -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 {