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? {
guard let frontendHost = frontendHost ?? account.instance.frontendHost else {
guard let frontendHost = frontendHost ?? account?.instance?.frontendHost,
var urlComponents = account?.instance?.urlComponents
else {
return nil
}
var urlComponents = account.instance.urlComponents
urlComponents.host = frontendHost
var queryItems = [URLQueryItem]()

View File

@ -28,6 +28,10 @@ struct Channel: Identifiable, Hashable {
self.videos = videos
}
var detailsLoaded: Bool {
!subscriptionsString.isNil
}
var subscriptionsString: String? {
if subscriptionsCount != nil, subscriptionsCount! > 0 {
return subscriptionsCount!.formattedAsAbbreviation()

View File

@ -33,15 +33,17 @@ final class PlayerModel: ObservableObject {
@Published var currentItem: PlayerQueueItem! { didSet { Defaults[.lastPlayed] = currentItem } }
@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 segmentRestorationTime: CMTime?
@Published var lastSkipped: Segment? { didSet { rebuildTVMenu() } }
@Published var restoredSegments = [Segment]()
@Published var channelWithDetails: Channel?
var accounts: AccountsModel
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(
_ stream: Stream,
for video: Video,
@ -212,18 +222,18 @@ final class PlayerModel: ObservableObject {
let replaceItemAndSeek = {
self.player.replaceCurrentItem(with: playerItem)
self.seekToSavedTime { finished in
self.seekToPreservedTime { finished in
guard finished else {
return
}
self.savedTime = nil
self.preservedTime = nil
startPlaying()
}
}
if preservingTime {
if savedTime.isNil {
if preservedTime.isNil {
saveTime {
replaceItemAndSeek()
startPlaying()
@ -394,13 +404,13 @@ final class PlayerModel: ObservableObject {
}
DispatchQueue.main.async { [weak self] in
self?.savedTime = currentTime
self?.preservedTime = currentTime
completionHandler()
}
}
private func seekToSavedTime(completionHandler: @escaping (Bool) -> Void = { _ in }) {
guard let time = savedTime else {
private func seekToPreservedTime(completionHandler: @escaping (Bool) -> Void = { _ in }) {
guard let time = preservedTime else {
return
}
@ -528,6 +538,36 @@ final class PlayerModel: ObservableObject {
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 {
let formatter = NumberFormatter()
formatter.minimumFractionDigits = 0

View File

@ -51,7 +51,8 @@ extension PlayerModel {
currentItem.video = video!
}
savedTime = currentItem.playbackTime
preservedTime = currentItem.playbackTime
restoreLoadedChannel()
loadAvailableStreams(currentVideo!) { streams in
guard let stream = self.preferredStream(streams) else {
@ -126,7 +127,7 @@ extension PlayerModel {
}
func isAutoplaying(_ item: AVPlayerItem) -> Bool {
player.currentItem == item && presentingPlayer
player.currentItem == item && (presentingPlayer || playerNavigationLinkActive || playingInPictureInPicture)
}
@discardableResult func enqueueVideo(

View File

@ -33,6 +33,7 @@ extension Defaults.Keys {
static let playerSidebar = Key<PlayerSidebarSetting>("playerSidebar", default: PlayerSidebarSetting.defaultValue)
static let playerInstanceID = Key<Instance.ID?>("playerInstance")
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)
#if !os(tvOS)
static let commentsPlacement = Key<CommentsPlacement>("commentsPlacement", default: .separate)

View File

@ -5,6 +5,7 @@ struct Player: UIViewControllerRepresentable {
@EnvironmentObject<CommentsModel> private var comments
@EnvironmentObject<NavigationModel> private var navigation
@EnvironmentObject<PlayerModel> private var player
@EnvironmentObject<SubscriptionsModel> private var subscriptions
var controller: PlayerViewController?
@ -22,6 +23,7 @@ struct Player: UIViewControllerRepresentable {
controller.commentsModel = comments
controller.navigationModel = navigation
controller.playerModel = player
controller.subscriptionsModel = subscriptions
player.controller = controller
return controller

View File

@ -1,5 +1,4 @@
import AVKit
import Logging
import SwiftUI
final class PlayerViewController: UIViewController {
@ -7,6 +6,7 @@ final class PlayerViewController: UIViewController {
var commentsModel: CommentsModel!
var navigationModel: NavigationModel!
var playerModel: PlayerModel!
var subscriptionsModel: SubscriptionsModel!
var playerViewController = AVPlayerViewController()
#if !os(tvOS)
@ -71,6 +71,7 @@ final class PlayerViewController: UIViewController {
.frame(maxHeight: 600)
.environmentObject(commentsModel)
.environmentObject(playerModel)
.environmentObject(subscriptionsModel)
)
)

View File

@ -1,5 +1,6 @@
import Defaults
import Foundation
import SDWebImageSwiftUI
import SwiftUI
struct VideoDetails: View {
@ -20,11 +21,15 @@ struct VideoDetails: View {
@Environment(\.presentationMode) private var presentationMode
@Environment(\.inNavigationView) private var inNavigationView
@Environment(\.navigationStyle) private var navigationStyle
@EnvironmentObject<AccountsModel> private var accounts
@EnvironmentObject<NavigationModel> private var navigation
@EnvironmentObject<PlayerModel> private var player
@EnvironmentObject<RecentsModel> private var recents
@EnvironmentObject<SubscriptionsModel> private var subscriptions
@Default(.showChannelSubscribers) private var showChannelSubscribers
@Default(.showKeywords) private var showKeywords
init(
@ -65,7 +70,9 @@ struct VideoDetails: View {
}
.padding(.horizontal)
if CommentsModel.enabled, CommentsModel.placement == .separate {
if !sidebarQueue ||
(CommentsModel.enabled && CommentsModel.placement == .separate)
{
pagePicker
.padding(.horizontal)
}
@ -178,21 +185,52 @@ struct VideoDetails: View {
Group {
if video != nil {
HStack(alignment: .center) {
HStack(spacing: 4) {
if subscribed {
Image(systemName: "star.circle.fill")
}
VStack(alignment: .leading) {
Text(video!.channel.name)
.font(.system(size: 13))
.bold()
if let subscribers = video!.channel.subscriptionsString {
Text("\(subscribers) subscribers")
HStack(spacing: 10) {
Group {
ZStack(alignment: .bottomTrailing) {
authorAvatar
if subscribed {
Image(systemName: "star.circle.fill")
.background(Color.background)
.clipShape(Circle())
.foregroundColor(.secondary)
}
}
VStack(alignment: .leading) {
Text(video!.channel.name)
.font(.system(size: 14))
.bold()
if showChannelSubscribers {
Group {
if let subscribers = video!.channel.subscriptionsString {
Text("\(subscribers) subscribers")
}
}
.foregroundColor(.secondary)
.font(.caption2)
}
}
}
}
.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")
}
}
}
.foregroundColor(.secondary)
if accounts.app.supportsSubscriptions {
Spacer()
@ -209,7 +247,7 @@ struct VideoDetails: View {
.alert(isPresented: $presentingUnsubscribeAlert) {
Alert(
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")) {
subscriptions.unsubscribe(video!.channel.id)
@ -364,6 +402,22 @@ struct VideoDetails: View {
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 {
Group {
Group {

View File

@ -7,6 +7,7 @@ struct PlaybackSettings: View {
@Default(.quality) private var quality
@Default(.playerSidebar) private var playerSidebar
@Default(.showKeywords) private var showKeywords
@Default(.showChannelSubscribers) private var channelSubscribers
@Default(.saveHistory) private var saveHistory
#if os(iOS)
@ -27,6 +28,7 @@ struct PlaybackSettings: View {
}
keywordsToggle
channelSubscribersToggle
}
#else
Section(header: SettingsHeader(text: "Source")) {
@ -44,6 +46,7 @@ struct PlaybackSettings: View {
#endif
keywordsToggle
channelSubscribersToggle
#endif
}
@ -107,6 +110,10 @@ struct PlaybackSettings: View {
private var keywordsToggle: some View {
Toggle("Show video keywords", isOn: $showKeywords)
}
private var channelSubscribersToggle: some View {
Toggle("Show channel subscribers count", isOn: $channelSubscribers)
}
}
struct PlaybackSettings_Previews: PreviewProvider {