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? {
|
||||
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]()
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user