diff --git a/Model/OpenVideosModel.swift b/Model/OpenVideosModel.swift index fb4f8de4..b546b07d 100644 --- a/Model/OpenVideosModel.swift +++ b/Model/OpenVideosModel.swift @@ -107,7 +107,7 @@ struct OpenVideosModel { prepending: playbackMode == .playNow || playbackMode == .playNext ) - WatchNextViewModel.shared.presentingOutro = false + WatchNextViewModel.shared.hide() if playbackMode == .playNow || playbackMode == .shuffleAll { #if os(iOS) diff --git a/Model/Player/Backends/AVPlayerBackend.swift b/Model/Player/Backends/AVPlayerBackend.swift index bde69f1e..38c236b7 100644 --- a/Model/Player/Backends/AVPlayerBackend.swift +++ b/Model/Player/Backends/AVPlayerBackend.swift @@ -531,10 +531,6 @@ final class AVPlayerBackend: PlayerBackend { } @objc func itemDidPlayToEndTime() { - if Defaults[.closeLastItemOnPlaybackEnd] { - model.prepareCurrentItemForHistory(finished: true) - } - eofPlaybackModeAction() } diff --git a/Model/Player/Backends/PlayerBackend.swift b/Model/Player/Backends/PlayerBackend.swift index 64c40449..2063ee88 100644 --- a/Model/Player/Backends/PlayerBackend.swift +++ b/Model/Player/Backends/PlayerBackend.swift @@ -94,37 +94,59 @@ extension PlayerBackend { } func eofPlaybackModeAction() { - let timer = Delay.by(5) { + let loopAction = { + model.backend.seek(to: .zero, seekType: .loopRestart) { _ in + self.model.play() + } + } + + guard model.playbackMode != .loopOne else { + loopAction() + return + } + + let action = { switch model.playbackMode { case .queue, .shuffle: - if Defaults[.closeLastItemOnPlaybackEnd] { - model.prepareCurrentItemForHistory(finished: true) - } + model.prepareCurrentItemForHistory(finished: true) if model.queue.isEmpty { - if Defaults[.closeLastItemOnPlaybackEnd] { - #if os(tvOS) - if model.activeBackend == .appleAVPlayer { - model.avPlayerBackend.controller?.dismiss(animated: false) - } - #endif - model.resetQueue() - model.hide() - } + #if os(tvOS) + if model.activeBackend == .appleAVPlayer { + model.avPlayerBackend.controller?.dismiss(animated: false) + } + #endif + model.resetQueue() + model.hide() } else { model.advanceToNextItem() } case .loopOne: - model.backend.seek(to: .zero, seekType: .loopRestart) { _ in - self.model.play() - } + loopAction() case .related: guard let item = model.autoplayItem else { return } model.resetAutoplay() model.advanceToItem(item) } } - WatchNextViewModel.shared.prepareForNextItem(model.currentItem, timer: timer) + let actionAndHideWatchNext: (Bool) -> Void = { delay in + WatchNextViewModel.shared.hide() + if delay { + Delay.by(0.3) { + action() + } + } else { + action() + } + } + if Defaults[.openWatchNextOnFinishedWatching], model.presentingPlayer { + let timer = Delay.by(TimeInterval(Defaults[.openWatchNextOnFinishedWatchingDelay]) ?? 5.0) { + actionAndHideWatchNext(true) + } + WatchNextViewModel.shared.finishedWatching(model.currentItem, timer: timer) + } else { + actionAndHideWatchNext(false) + } } func updateControls(completionHandler: (() -> Void)? = nil) { diff --git a/Model/Player/PlayerModel.swift b/Model/Player/PlayerModel.swift index 60376156..b80001d7 100644 --- a/Model/Player/PlayerModel.swift +++ b/Model/Player/PlayerModel.swift @@ -80,6 +80,9 @@ final class PlayerModel: ObservableObject { @Published var playerSize: CGSize = .zero { didSet { #if !os(tvOS) + #if os(macOS) + guard videoForDisplay != nil else { return } + #endif backend.setSize(playerSize.width, playerSize.height) #endif }} @@ -162,7 +165,6 @@ final class PlayerModel: ObservableObject { #if !os(macOS) @Default(.closePiPAndOpenPlayerOnEnteringForeground) var closePiPAndOpenPlayerOnEnteringForeground - @Default(.closePlayerOnItemClose) private var closePlayerOnItemClose #endif private var currentArtwork: MPMediaItemArtwork? @@ -324,7 +326,7 @@ final class PlayerModel: ObservableObject { pause() videoBeingOpened = video - WatchNextViewModel.shared.presentingOutro = false + WatchNextViewModel.shared.hide() var changeBackendHandler: (() -> Void)? diff --git a/Model/Player/PlayerQueue.swift b/Model/Player/PlayerQueue.swift index aa2e2b88..39d08950 100644 --- a/Model/Player/PlayerQueue.swift +++ b/Model/Player/PlayerQueue.swift @@ -14,7 +14,7 @@ extension PlayerModel { } func play(_ videos: [Video], shuffling: Bool = false) { - WatchNextViewModel.shared.presentingOutro = false + WatchNextViewModel.shared.hide() playbackMode = shuffling ? .shuffle : .queue videos.forEach { enqueueVideo($0, loadDetails: false) } @@ -55,7 +55,7 @@ extension PlayerModel { comments.reset() stream = nil - WatchNextViewModel.shared.close() + WatchNextViewModel.shared.hide() withAnimation { aspectRatio = VideoPlayerView.defaultAspectRatio @@ -175,7 +175,7 @@ extension PlayerModel { remove(newItem) - WatchNextViewModel.shared.close() + WatchNextViewModel.shared.hide() currentItem = newItem currentItem.playbackTime = time @@ -221,7 +221,7 @@ extension PlayerModel { if play { withAnimation { aspectRatio = VideoPlayerView.defaultAspectRatio - WatchNextViewModel.shared.close() + WatchNextViewModel.shared.hide() currentItem = item } videoBeingOpened = video diff --git a/Model/Player/PlayerQueueItem.swift b/Model/Player/PlayerQueueItem.swift index 28720489..ad98ab58 100644 --- a/Model/Player/PlayerQueueItem.swift +++ b/Model/Player/PlayerQueueItem.swift @@ -25,7 +25,7 @@ struct PlayerQueueItem: Hashable, Identifiable, Defaults.Serializable { } init( - _ video: Video? = nil, + _ video: Video? = .fixture, videoID: Video.ID? = nil, app: VideosApp? = nil, instanceURL: URL? = nil, diff --git a/Model/UnwatchedFeedCountModel.swift b/Model/UnwatchedFeedCountModel.swift index f87e8be8..d97b9cac 100644 --- a/Model/UnwatchedFeedCountModel.swift +++ b/Model/UnwatchedFeedCountModel.swift @@ -9,10 +9,12 @@ final class UnwatchedFeedCountModel: ObservableObject { private var accounts = AccountsModel.shared + // swiftlint:disable empty_count var unwatchedText: Text? { if let account = accounts.current, !account.anonymous, - let count = unwatched[account] + let count = unwatched[account], + count > 0 { return Text(String(count)) } @@ -23,7 +25,8 @@ final class UnwatchedFeedCountModel: ObservableObject { func unwatchedByChannelText(_ channel: Channel) -> Text? { if let account = accounts.current, !account.anonymous, - let count = unwatchedByChannel[account]?[channel.id] + let count = unwatchedByChannel[account]?[channel.id], + count > 0 { return Text(String(count)) } diff --git a/Model/Video.swift b/Model/Video.swift index 9596982e..0b14bfc9 100644 --- a/Model/Video.swift +++ b/Model/Video.swift @@ -327,6 +327,10 @@ struct Video: Identifiable, Equatable, Hashable { return path.contains(".") ? path.components(separatedBy: ".").last?.uppercased() : nil } + var isShareable: Bool { + !isLocal || localStreamIsRemoteURL + } + private var localStreamURLComponents: URLComponents? { guard let localStream else { return nil } return URLComponents(url: localStream.localURL, resolvingAgainstBaseURL: false) diff --git a/Model/WatchNextViewModel.swift b/Model/WatchNextViewModel.swift index f7e1441d..92e7c613 100644 --- a/Model/WatchNextViewModel.swift +++ b/Model/WatchNextViewModel.swift @@ -1,47 +1,192 @@ +import Combine +import Defaults import Foundation import SwiftUI final class WatchNextViewModel: ObservableObject { + enum Page: String, CaseIterable { + case queue + case related + case history + + var title: String { + rawValue.capitalized.localized() + } + + var systemImageName: String { + switch self { + case .queue: + return "list.and.film" + case .related: + return "rectangle.stack.fill" + case .history: + return "clock" + } + } + } + + enum PresentationReason { + case userInteracted + case finishedWatching + case closed + } + static let animation = Animation.easeIn(duration: 0.25) static let shared = WatchNextViewModel() @Published var item: PlayerQueueItem? - @Published var presentingOutro = true - @Published var isAutoplaying = true - var timer: Timer? + @Published private(set) var isPresenting = true + @Published var reason: PresentationReason? + @Published var page = Page.queue - func prepareForEmptyPlayerPlaceholder(_ item: PlayerQueueItem? = nil) { - self.item = item + @Published var countdown = 0.0 + var countdownTimer: Timer? + + private var player = PlayerModel.shared + + var autoplayTimer: Timer? + + var isAutoplaying: Bool { + reason == .finishedWatching } - func prepareForNextItem(_ item: PlayerQueueItem? = nil, timer: Timer? = nil) { - self.item = item - self.timer?.invalidate() - self.timer = timer - isAutoplaying = true - withAnimation(Self.animation) { - presentingOutro = true + var isHideable: Bool { + reason == .userInteracted + } + + var isRestartable: Bool { + player.currentItem != nil && reason != .userInteracted + } + + var canAutoplay: Bool { + switch player.playbackMode { + case .shuffle: + return !player.queue.isEmpty + default: + return nextFromTheQueue != nil } } - func cancelAutoplay() { - timer?.invalidate() - isAutoplaying = false + func userInteractedOpen(_ item: PlayerQueueItem?) { + self.item = item + open(reason: .userInteracted) } - func open() { + func finishedWatching(_ item: PlayerQueueItem?, timer: Timer? = nil) { + if canAutoplay { + countdown = TimeInterval(Defaults[.openWatchNextOnFinishedWatchingDelay]) ?? 5.0 + resetCountdownTimer() + autoplayTimer?.invalidate() + autoplayTimer = timer + } else { + timer?.invalidate() + } + self.item = item + open(reason: .finishedWatching) + } + + func resetCountdownTimer() { + countdownTimer?.invalidate() + countdownTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in + guard self.countdown > 0 else { + timer.invalidate() + return + } + self.countdown = max(0, self.countdown - 1) + } + } + + func closed(_ item: PlayerQueueItem) { + self.item = item + open(reason: .closed) + } + + func keepFromAutoplaying() { + userInteractedOpen(item) + cancelAutoplay() + } + + func cancelAutoplay() { + autoplayTimer?.invalidate() + countdownTimer?.invalidate() + } + + func restart() { + cancelAutoplay() + + guard player.currentItem != nil else { return } + + if reason == .closed { + hide() + return + } + + player.backend.seek(to: .zero, seekType: .loopRestart) { _ in + self.hide() + self.player.play() + } + } + + private func open(reason: PresentationReason) { + self.reason = reason + page = Page.allCases.first { isAvailable($0) } ?? .history + + guard !isPresenting else { return } withAnimation(Self.animation) { - presentingOutro = true + isPresenting = true } } func close() { + let close = { + self.player.closeCurrentItem() + self.player.hide() + Delay.by(0.5) { + self.isPresenting = false + } + } + if reason == .closed { + close() + return + } + if canAutoplay { + cancelAutoplay() + hide() + } else { + close() + } + } + + func hide() { + guard isPresenting else { return } withAnimation(Self.animation) { - presentingOutro = false + isPresenting = false } } func resetItem() { item = nil } + + func isAvailable(_ page: Page) -> Bool { + switch page { + case .queue: + return !player.queue.isEmpty + case .related: + guard let video = item?.video else { return false } + return !video.related.isEmpty + case .history: + return true + } + } + + var nextFromTheQueue: PlayerQueueItem? { + if player.playbackMode == .related { + return player.autoplayItem + } else if player.playbackMode == .queue { + return player.queue.first + } + + return nil + } } diff --git a/Shared/Constants.swift b/Shared/Constants.swift index 62c8b3ef..a85aee9f 100644 --- a/Shared/Constants.swift +++ b/Shared/Constants.swift @@ -43,4 +43,12 @@ struct Constants { true #endif } + + static var nextSystemImage: String { + if #available(iOS 16, macOS 13, tvOS 16, *) { + return "film.stack" + } else { + return "list.and.film" + } + } } diff --git a/Shared/Defaults.swift b/Shared/Defaults.swift index 01defd22..560f13d7 100644 --- a/Shared/Defaults.swift +++ b/Shared/Defaults.swift @@ -107,8 +107,6 @@ extension Defaults.Keys { static let chargingNonCellularProfile = Key("chargingNonCellularProfile", default: chargingNonCellularProfileDefault) static let forceAVPlayerForLiveStreams = Key("forceAVPlayerForLiveStreams", default: true) static let playerSidebar = Key("playerSidebar", default: .defaultValue) - static let showInspector = Key("showInspector", default: .onlyLocal) - static let detailsToolbarPosition = Key("detailsToolbarPosition", default: .center) static let playerInstanceID = Key("playerInstance") #if os(iOS) @@ -142,20 +140,6 @@ extension Defaults.Keys { #if !os(macOS) static let pauseOnEnteringBackground = Key("pauseOnEnteringBackground", default: true) #endif - #if os(tvOS) - static let closeLastItemOnPlaybackEndDefault = true - #else - static let closeLastItemOnPlaybackEndDefault = false - #endif - static let closeLastItemOnPlaybackEnd = Key("closeLastItemOnPlaybackEnd", default: closeLastItemOnPlaybackEndDefault) - - #if os(tvOS) - static let closePlayerOnItemCloseDefault = true - #else - static let closePlayerOnItemCloseDefault = false - #endif - static let closePlayerOnItemClose = Key("closePlayerOnItemClose", default: closePlayerOnItemCloseDefault) - static let closePiPOnNavigation = Key("closePiPOnNavigation", default: false) static let closePiPOnOpeningPlayer = Key("closePiPOnOpeningPlayer", default: false) #if !os(macOS) @@ -198,7 +182,6 @@ extension Defaults.Keys { static let playerDetailsPageButtonLabelStyleDefault = UIDevice.current.userInterfaceIdiom == .phone ? ButtonLabelStyle.iconOnly : .iconAndText #endif static let playerActionsButtonLabelStyle = Key("playerActionsButtonLabelStyle", default: .iconAndText) - static let playerDetailsPageButtonLabelStyle = Key("playerDetailsPageButtonLabelStyle", default: playerDetailsPageButtonLabelStyleDefault) static let systemControlsCommands = Key("systemControlsCommands", default: .restartAndAdvanceToNext) static let mpvCacheSecs = Key("mpvCacheSecs", default: "120") @@ -216,6 +199,10 @@ extension Defaults.Keys { static let playlistListingStyle = Key("playlistListingStyle", default: .cells) static let channelPlaylistListingStyle = Key("channelPlaylistListingStyle", default: .cells) static let searchListingStyle = Key("searchListingStyle", default: .cells) + + static let openWatchNextOnFinishedWatching = Key("openWatchNextOnFinishedWatching", default: true) + static let openWatchNextOnClose = Key("openWatchNextOnClose", default: false) + static let openWatchNextOnFinishedWatchingDelay = Key("openWatchNextOnFinishedWatchingDelay", default: "5") } enum ResolutionSetting: String, CaseIterable, Defaults.Serializable { diff --git a/Shared/Navigation/AppSidebarSubscriptions.swift b/Shared/Navigation/AppSidebarSubscriptions.swift index 48d3c245..bc5a8262 100644 --- a/Shared/Navigation/AppSidebarSubscriptions.swift +++ b/Shared/Navigation/AppSidebarSubscriptions.swift @@ -15,17 +15,18 @@ struct AppSidebarSubscriptions: View { LazyView(ChannelVideosView(channel: channel)) } label: { HStack { - if channel.thumbnailURL != nil { + if channel.thumbnailURLOrCached != nil { ChannelAvatarView(channel: channel, subscribedBadge: false) .frame(width: Constants.sidebarChannelThumbnailSize, height: Constants.sidebarChannelThumbnailSize) - Text(channel.name) } else { Label(channel.name, systemImage: RecentsModel.symbolSystemImage(channel.name)) } - feedCount.unwatchedByChannelText(channel) + Spacer() } + .backport + .badge(feedCount.unwatchedByChannelText(channel)) } .contextMenu { if subscriptions.isSubscribing(channel.id) { diff --git a/Shared/Player/Controls/PlayerControls.swift b/Shared/Player/Controls/PlayerControls.swift index 065bdfac..56a4d33a 100644 --- a/Shared/Player/Controls/PlayerControls.swift +++ b/Shared/Player/Controls/PlayerControls.swift @@ -26,12 +26,9 @@ struct PlayerControls: View { @FocusState private var focusedField: Field? #endif - #if !os(macOS) - @Default(.closePlayerOnItemClose) private var closePlayerOnItemClose - #endif - @Default(.playerControlsLayout) private var regularPlayerControlsLayout @Default(.fullScreenPlayerControlsLayout) private var fullScreenPlayerControlsLayout + @Default(.openWatchNextOnClose) private var openWatchNextOnClose private let controlsOverlayModel = ControlOverlaysModel.shared @@ -164,6 +161,7 @@ struct PlayerControls: View { #if os(tvOS) .onChange(of: model.presentingControls) { newValue in if newValue { focusedField = .play } + else { focusedField = nil } } .onChange(of: focusedField) { _ in model.resetTimer() } #else @@ -222,6 +220,8 @@ struct PlayerControls: View { .frame(maxWidth: .infinity, maxHeight: .infinity) .transition(.opacity) .animation(.default) + } else if player.videoForDisplay == nil { + Color.black } } @@ -320,10 +320,12 @@ struct PlayerControls: View { private var closeVideoButton: some View { button("Close", systemImage: "xmark") { -// TODO: Setting -// WatchNextViewModel.shared.prepareForEmptyPlayerPlaceholder(player.currentItem) -// WatchNextViewModel.shared.open() - player.closeCurrentItem() + if openWatchNextOnClose { + player.pause() + WatchNextViewModel.shared.closed(player.currentItem) + } else { + player.closeCurrentItem() + } } #if os(tvOS) .focused($focusedField, equals: .close) diff --git a/Shared/Player/Controls/VideoDetailsOverlay.swift b/Shared/Player/Controls/VideoDetailsOverlay.swift index 33e12abf..972869bb 100644 --- a/Shared/Player/Controls/VideoDetailsOverlay.swift +++ b/Shared/Player/Controls/VideoDetailsOverlay.swift @@ -5,7 +5,7 @@ struct VideoDetailsOverlay: View { @ObservedObject private var controls = PlayerControlsModel.shared var body: some View { - VideoDetails(video: PlayerModel.shared.currentVideo, fullScreen: fullScreenBinding) + VideoDetails(video: controls.player.videoForDisplay, fullScreen: fullScreenBinding) .clipShape(RoundedRectangle(cornerRadius: 4)) } diff --git a/Shared/Player/Video Details/VideoActions.swift b/Shared/Player/Video Details/VideoActions.swift index 45681147..3a7cb912 100644 --- a/Shared/Player/Video Details/VideoActions.swift +++ b/Shared/Player/Video Details/VideoActions.swift @@ -9,13 +9,14 @@ struct VideoActions: View { var video: Video? + @Default(.openWatchNextOnClose) private var openWatchNextOnClose @Default(.playerActionsButtonLabelStyle) private var playerActionsButtonLabelStyle var body: some View { - HStack { + HStack(spacing: 6) { if let video { #if !os(tvOS) - if !video.isLocal || video.localStreamIsRemoteURL { + if video.isShareable { ShareButton(contentItem: .init(video: video)) { actionButton("Share", systemImage: "square.and.arrow.up") } @@ -50,21 +51,29 @@ struct VideoActions: View { Spacer() } } + } else { + Spacer() } + actionButton("Next", systemImage: Constants.nextSystemImage) { + WatchNextViewModel.shared.userInteractedOpen(player.currentItem) + } + + Spacer() actionButton("Hide", systemImage: "chevron.down") { player.hide(animate: true) } Spacer() + actionButton("Close", systemImage: "xmark") { -// TODO: setting -// player.pause() -// WatchNextViewModel.shared.prepareForEmptyPlayerPlaceholder(player.currentItem) -// WatchNextViewModel.shared.open() - player.closeCurrentItem() + if openWatchNextOnClose { + player.pause() + WatchNextViewModel.shared.closed(player.currentItem) + } else { + player.closeCurrentItem() + } } - .disabled(player.currentItem == nil) } .padding(.horizontal) .multilineTextAlignment(.center) diff --git a/Shared/Player/Video Details/VideoDetails.swift b/Shared/Player/Video Details/VideoDetails.swift index cb49f8c1..21233140 100644 --- a/Shared/Player/Video Details/VideoDetails.swift +++ b/Shared/Player/Video Details/VideoDetails.swift @@ -27,10 +27,9 @@ struct VideoDetails: View { @ObservedObject private var accounts = AccountsModel.shared let comments = CommentsModel.shared - var player = PlayerModel.shared + @ObservedObject private var player = PlayerModel.shared @Default(.enableReturnYouTubeDislike) private var enableReturnYouTubeDislike - @Default(.detailsToolbarPosition) private var detailsToolbarPosition @Default(.playerSidebar) private var playerSidebar var body: some View { @@ -46,7 +45,7 @@ struct VideoDetails: View { ) .animation(nil, value: player.currentItem) - VideoActions(video: video) + VideoActions(video: player.videoForDisplay) .animation(nil, value: player.currentItem) detailsPage diff --git a/Shared/Player/Video Details/VideoDetailsTool.swift b/Shared/Player/Video Details/VideoDetailsTool.swift deleted file mode 100644 index 0d8dd2c4..00000000 --- a/Shared/Player/Video Details/VideoDetailsTool.swift +++ /dev/null @@ -1,46 +0,0 @@ -import Defaults -import Foundation - -struct VideoDetailsTool: Identifiable { - static let all = [ - Self(icon: "info.circle", name: "Info", page: .info), - Self(icon: "wand.and.stars", name: "Inspector", page: .inspector), - Self(icon: "bookmark", name: "Chapters", page: .chapters), - Self(icon: "text.bubble", name: "Comments", page: .comments), - Self(icon: "rectangle.stack.fill", name: "Related", page: .related), - Self(icon: "list.number", name: "Queue", page: .queue) - ] - - static func find(for page: VideoDetails.DetailsPage) -> Self? { - all.first { $0.page == page } - } - - var id: String { - page.rawValue - } - - var icon: String - var name: String - var toolPostion: CGRect = .zero - var page = VideoDetails.DetailsPage.info - - func isAvailable(for video: Video?, sidebarQueue: Bool) -> Bool { - guard !YatteeApp.isForPreviews else { - return true - } - switch page { - case .info: - return true - case .inspector: - return video == nil || Defaults[.showInspector] == .always || video!.isLocal - case .chapters: - return video != nil && !video!.chapters.isEmpty - case .comments: - return video != nil && !video!.isLocal - case .related: - return !sidebarQueue && video != nil && !video!.isLocal - case .queue: - return !sidebarQueue - } - } -} diff --git a/Shared/Player/Video Details/VideoDetailsToolbar.swift b/Shared/Player/Video Details/VideoDetailsToolbar.swift deleted file mode 100644 index 152942e2..00000000 --- a/Shared/Player/Video Details/VideoDetailsToolbar.swift +++ /dev/null @@ -1,154 +0,0 @@ -import Defaults -import SwiftUI - -struct VideoDetailsToolbar: View { - static let lowOpacity = 0.5 - var video: Video? - @Binding var page: VideoDetails.DetailsPage - var sidebarQueue: Bool - - @State private var tools = VideoDetailsTool.all - - @State private var activeTool: VideoDetailsTool? - @State private var startedToolPosition: CGRect = .zero - @State private var opacity = 1.0 - - @ObservedObject private var player = PlayerModel.shared - @Default(.playerDetailsPageButtonLabelStyle) private var playerDetailsPageButtonLabelStyle - - var body: some View { - VStack { - HStack(spacing: 12) { - ForEach($tools) { $tool in - if $tool.wrappedValue.isAvailable(for: video, sidebarQueue: sidebarQueue) { - ToolView(tool: $tool) - .padding(.vertical, 10) - } - } - } - .id(video?.id) - .onAppear { - activeTool = .find(for: page) - } - .onChange(of: page) { newValue in - activeTool = tools.first { $0.id == newValue.rawValue } - } - .coordinateSpace(name: "toolbarArea") - #if !os(tvOS) - .gesture( - DragGesture(minimumDistance: 0) - .onChanged { value in - withAnimation(.linear(duration: 0.2)) { - opacity = 1 - } - - guard let firstTool = tools.first else { return } - if startedToolPosition == .zero { - startedToolPosition = firstTool.toolPostion - } - let location = CGPoint(x: value.location.x, y: value.location.y) - - if let index = tools.firstIndex(where: { $0.toolPostion.contains(location) }), - activeTool?.id != tools[index].id, - tools[index].isAvailable(for: video, sidebarQueue: sidebarQueue) - { - withAnimation(.interpolatingSpring(stiffness: 230, damping: 22)) { - activeTool = tools[index] - } - withAnimation(.linear(duration: 0.25)) { - page = activeTool?.page ?? .info - } - } - } - .onEnded { _ in - withAnimation(.interactiveSpring(response: 0.5, dampingFraction: 1, blendDuration: 1)) { - startedToolPosition = .zero - } - Delay.by(2) { - lowerOpacity() - } - } - ) - #endif - } - #if !os(tvOS) - .onHover { hovering in - hovering ? resetOpacity(0.2) : lowerOpacity(0.2) - } - #endif - .onAppear { - Delay.by(2) { lowerOpacity() } - } - .opacity(opacity) - .background( - Rectangle() - .contentShape(Rectangle()) - .foregroundColor(.clear) - ) - } - - func lowerOpacity(_ duration: Double = 1.0) { - withAnimation(.linear(duration: duration)) { - opacity = Self.lowOpacity - } - } - - func resetOpacity(_ duration: Double = 1.0) { - withAnimation(.linear(duration: duration)) { - opacity = 1 - } - } - - @ViewBuilder func ToolView(tool: Binding) -> some View { - HStack(spacing: 0) { - Image(systemName: tool.wrappedValue.icon) - .font(.title2) - .foregroundColor(.white) - .frame(width: 30, height: 30) - .layoutPriority(1) - - if activeToolID == tool.wrappedValue.id, - playerDetailsPageButtonLabelStyle.text, - player.playerSize.width > 450 - { - Text(tool.wrappedValue.name.localized()) - .font(.system(size: 14).bold()) - .padding(.trailing, 4) - .foregroundColor(.white) - .allowsTightening(true) - .lineLimit(1) - } - } - .padding(.horizontal, 10) - .padding(.vertical, 6) - .background( - RoundedRectangle(cornerRadius: 10, style: .continuous) - .fill(activeToolID == tool.wrappedValue.id ? Color.accentColor : Color.secondary) - ) - .background( - GeometryReader { proxy in - let frame = proxy.frame(in: .named("toolbarArea")) - Color.clear - .preference(key: RectKey.self, value: frame) - .onPreferenceChange(RectKey.self) { rect in - tool.wrappedValue.toolPostion = rect - } - } - ) - } - - var visibleToolsCount: Int { - tools.filter { $0.isAvailable(for: video, sidebarQueue: sidebarQueue) }.count - } - - var activeToolID: VideoDetailsTool.ID { - activeTool?.id ?? "info" - } -} - -struct VideoDetailsToolbar_Previews: PreviewProvider { - static var previews: some View { - VideoDetailsToolbar(page: .constant(.queue), sidebarQueue: false) - .injectFixtureEnvironmentObjects() - } -} diff --git a/Shared/Player/VideoPlayerView.swift b/Shared/Player/VideoPlayerView.swift index 5492aa5d..f9506ad2 100644 --- a/Shared/Player/VideoPlayerView.swift +++ b/Shared/Player/VideoPlayerView.swift @@ -126,6 +126,11 @@ struct VideoPlayerView: View { } } .onAppear { + #if os(macOS) + if player.videoForDisplay.isNil { + player.hide() + } + #endif viewDragOffset = 0 Delay.by(0.2) { @@ -178,6 +183,9 @@ struct VideoPlayerView: View { .backport .persistentSystemOverlays(!fullScreenPlayer) #endif + #if os(macOS) + .frame(minWidth: 1000, minHeight: 700) + #endif } func updateSidebarQueue() { diff --git a/Shared/Player/WatchNextView.swift b/Shared/Player/WatchNextView.swift index f7a4a8bb..594ccfc2 100644 --- a/Shared/Player/WatchNextView.swift +++ b/Shared/Player/WatchNextView.swift @@ -14,15 +14,36 @@ struct WatchNextView: View { #if os(iOS) NavigationView { watchNext + .toolbar { + ToolbarItem(placement: .principal) { + watchNextMenu + } + } } #else VStack { HStack { - closeButton + hideCloseButton + .labelStyle(.iconOnly) + .frame(maxWidth: .infinity, alignment: .leading) + Spacer() - reopenButton + + watchNextMenu + .frame(maxWidth: .infinity) + + Spacer() + + HStack { + if model.isRestartable { + reopenButton + } + } + .frame(maxWidth: .infinity, alignment: .trailing) } + #if os(macOS) .padding() + #endif watchNext } #endif @@ -32,143 +53,215 @@ struct WatchNextView: View { #else .background(Color.background) #endif - .opacity(model.presentingOutro ? 1 : 0) + .opacity(model.isPresenting ? 1 : 0) } var watchNext: some View { ScrollView { VStack(alignment: .leading) { if model.isAutoplaying, - let item = nextFromTheQueue + let item = model.nextFromTheQueue { HStack { - Text("Playing Next in 5...") - .font(.headline) + Text("Playing Next in \(Int(model.countdown.rounded()))...") + .font(.headline.monospacedDigit()) Spacer() Button { - model.cancelAutoplay() + model.keepFromAutoplaying() } label: { - Label("Cancel", systemImage: "xmark") + Label("Cancel", systemImage: "pause.fill") + #if os(iOS) + .imageScale(.large) + .padding([.vertical, .leading]) + .font(.headline.bold()) + #endif } } + #if os(tvOS) + .padding(.top, 10) + #endif PlayerQueueRow(item: item) - .padding(.bottom, 10) } + moreVideos + .padding(.top, 15) } .padding(.horizontal) } #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif - .navigationTitle("Watch Next") #if !os(macOS) - .toolbar { - ToolbarItem(placement: .cancellationAction) { - closeButton - } - - ToolbarItem(placement: .primaryAction) { - reopenButton - } + .navigationTitle(model.page.title) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + hideCloseButton } + + ToolbarItem(placement: .primaryAction) { + reopenButton + } + } #endif } + var watchNextMenu: some View { + #if os(tvOS) + Button { + model.page = model.page.next() + } label: { + menuLabel + } + #elseif os(macOS) + pagePicker + .modifier(SettingsPickerModifier()) + #if os(macOS) + .frame(maxWidth: 150) + #endif + #else + Menu { + pagePicker + } label: { + HStack(spacing: 12) { + menuLabel + .foregroundColor(.primary) + + Image(systemName: "chevron.down.circle.fill") + .foregroundColor(.accentColor) + .imageScale(.small) + } + .transaction { t in t.animation = nil } + } + + #endif + } + + var menuLabel: some View { + HStack { + Image(systemName: model.page.systemImageName) + .imageScale(.small) + Text(model.page.title) + .font(.headline) + } + } + + var pagePicker: some View { + Picker("Page", selection: $model.page) { + ForEach(WatchNextViewModel.Page.allCases, id: \.rawValue) { page in + Label(page.title, systemImage: page.systemImageName) + .tag(page) + } + } + } + + @ViewBuilder var hideCloseButton: some View { + if model.isHideable { + hideButton + } else { + closeButton + } + } + + var hideButton: some View { + Button { + model.hide() + } label: { + Label("Hide", systemImage: "chevron.down") + } + } + var closeButton: some View { Button { - player.closeCurrentItem() - player.hide() - Delay.by(0.8) { - model.presentingOutro = false - } + model.close() } label: { Label("Close", systemImage: "xmark") } } @ViewBuilder var reopenButton: some View { - if player.currentItem != nil, model.item != nil { + if model.isRestartable { Button { - model.close() + model.restart() } label: { - Label("Back to last video", systemImage: "arrow.counterclockwise") + Label(model.reason == .userInteracted ? "Back" : "Reopen", systemImage: "arrow.counterclockwise") } } } @ViewBuilder var moreVideos: some View { VStack(spacing: 12) { - let queueForMoreVideos = player.queue.isEmpty ? [] : player.queue.suffix(from: model.isAutoplaying ? 1 : 0) - if !queueForMoreVideos.isEmpty { - VStack(spacing: 12) { - Text("Next in Queue") - .frame(maxWidth: .infinity, alignment: .leading) - .font(.headline) - + switch model.page { + case .queue: + let queueForMoreVideos = player.queue.isEmpty ? [] : player.queue.suffix(from: model.isAutoplaying ? 1 : 0) + if !queueForMoreVideos.isEmpty { ForEach(queueForMoreVideos) { item in - ContentItemView(item: .init(video: item.video)) - .environment(\.listingStyle, .list) + PlayerQueueRow(item: item) + .contextMenu { + removeButton(item) + removeAllButton() + + if let video = item.video { + VideoContextMenuView(video: video) + } + } + #if os(tvOS) + .padding(.horizontal, 30) + #endif + + #if !os(tvOS) + Divider() + #endif } + } else if player.playbackMode != .related && player.playbackMode != .loopOne { + Label( + model.isAutoplaying ? "Nothing more in the queue" : "Queue is empty", + systemImage: WatchNextViewModel.Page.queue.systemImageName + ) + .foregroundColor(.secondary) } - } - - if let item = model.item { - VStack(spacing: 12) { - Text("Related videos") - .frame(maxWidth: .infinity, alignment: .leading) - .font(.headline) - + case .related: + if let item = model.item { ForEach(item.video.related) { video in ContentItemView(item: .init(video: video)) .environment(\.listingStyle, .list) } - .padding(.bottom, 4) + } else { + Label("Nothing was played", + systemImage: WatchNextViewModel.Page.related.systemImageName) + .foregroundColor(.secondary) } - } - - if saveHistory { - VStack(spacing: 12) { - Text("History") - .frame(maxWidth: .infinity, alignment: .leading) - .font(.headline) - - HStack { - Text("Playing Next in 5...") - .font(.headline) - Spacer() - - Button { - model.cancelAutoplay() - } label: { - Label("Cancel", systemImage: "pause.fill") - } - } - + case .history: + if saveHistory { HistoryView(limit: 15) } } } } - var nextFromTheQueue: PlayerQueueItem? { - if player.playbackMode == .related { - return player.autoplayItem - } else if player.playbackMode == .queue { - return player.queue.first + private func removeButton(_ item: PlayerQueueItem) -> some View { + Button { + player.remove(item) + } label: { + Label("Remove from the queue", systemImage: "trash") } + } - return nil + private func removeAllButton() -> some View { + Button { + player.removeQueueItems() + } label: { + Label("Clear the queue", systemImage: "trash.fill") + } } } -struct OutroView_Previews: PreviewProvider { +struct WatchNextView_Previews: PreviewProvider { static var previews: some View { WatchNextView() .onAppear { - WatchNextViewModel.shared.prepareForNextItem(.init(.fixture)) + WatchNextViewModel.shared.finishedWatching(.init(.fixture)) } } } diff --git a/Shared/Settings/PlayerSettings.swift b/Shared/Settings/PlayerSettings.swift index b2347130..8145478e 100644 --- a/Shared/Settings/PlayerSettings.swift +++ b/Shared/Settings/PlayerSettings.swift @@ -6,10 +6,6 @@ struct PlayerSettings: View { @Default(.playerInstanceID) private var playerInstanceID @Default(.playerSidebar) private var playerSidebar - @Default(.playerActionsButtonLabelStyle) private var playerActionsButtonLabelStyle - @Default(.playerDetailsPageButtonLabelStyle) private var playerDetailsPageButtonLabelStyle - @Default(.detailsToolbarPosition) private var detailsToolbarPosition - @Default(.showInspector) private var showInspector @Default(.playerControlsLayout) private var playerControlsLayout @Default(.fullScreenPlayerControlsLayout) private var fullScreenPlayerControlsLayout @Default(.horizontalPlayerGestureEnabled) private var horizontalPlayerGestureEnabled @@ -17,7 +13,6 @@ struct PlayerSettings: View { @Default(.seekGestureSensitivity) private var seekGestureSensitivity @Default(.showKeywords) private var showKeywords @Default(.pauseOnHidingPlayer) private var pauseOnHidingPlayer - @Default(.closeLastItemOnPlaybackEnd) private var closeLastItemOnPlaybackEnd #if os(iOS) @Default(.honorSystemOrientationLock) private var honorSystemOrientationLock @Default(.enterFullscreenInLandscape) private var enterFullscreenInLandscape @@ -27,7 +22,6 @@ struct PlayerSettings: View { @Default(.closePiPOnOpeningPlayer) private var closePiPOnOpeningPlayer @Default(.closePlayerOnOpeningPiP) private var closePlayerOnOpeningPiP #if !os(macOS) - @Default(.closePlayerOnItemClose) private var closePlayerOnItemClose @Default(.pauseOnEnteringBackground) private var pauseOnEnteringBackground @Default(.closePiPAndOpenPlayerOnEnteringForeground) private var closePiPAndOpenPlayerOnEnteringForeground #endif @@ -35,6 +29,10 @@ struct PlayerSettings: View { @Default(.enableReturnYouTubeDislike) private var enableReturnYouTubeDislike @Default(.systemControlsCommands) private var systemControlsCommands + @Default(.openWatchNextOnClose) private var openWatchNextOnClose + @Default(.openWatchNextOnFinishedWatching) private var openWatchNextOnFinishedWatching + @Default(.openWatchNextOnFinishedWatchingDelay) private var openWatchNextOnFinishedWatchingDelay + @ObservedObject private var accounts = AccountsModel.shared private var player = PlayerModel.shared @@ -73,12 +71,16 @@ struct PlayerSettings: View { pauseOnHidingPlayerToggle #if !os(macOS) pauseOnEnteringBackgroundToogle - closePlayerOnItemCloseToggle #endif - closeLastItemOnPlaybackEndToggle systemControlsCommandsPicker } + Section(header: SettingsHeader(text: "Watch Next")) { + openWatchNextOnFinishedWatchingToggle + openWatchNextOnFinishedWatchingDelayTextField + openWatchNextOnCloseToggle + } + #if !os(tvOS) Section(header: SettingsHeader(text: "Controls".localized()), footer: controlsLayoutFooter) { horizontalPlayerGestureEnabledToggle @@ -122,21 +124,6 @@ struct PlayerSettings: View { } #endif - #if !os(tvOS) - Section(header: SettingsHeader(text: "Video Details").padding(.bottom, videoDetailsHeaderPadding)) { - SettingsHeader(text: "Actions buttons".localized(), secondary: true) - playerActionsButtonLabelStylePicker - SettingsHeader(text: "Pages buttons".localized(), secondary: true) - detailsButtonLabelStylePicker - - SettingsHeader(text: "Show Inspector".localized(), secondary: true) - showInspectorPicker - - SettingsHeader(text: "Pages toolbar position".localized(), secondary: true) - detailsToolbarPositionPicker - } - #endif - #if os(iOS) Section(header: SettingsHeader(text: "Orientation".localized())) { if idiom == .pad { @@ -196,6 +183,33 @@ struct PlayerSettings: View { .modifier(SettingsPickerModifier()) } + private var openWatchNextOnCloseToggle: some View { + Toggle("Open after manual close of video", isOn: $openWatchNextOnClose) + } + + private var openWatchNextOnFinishedWatchingToggle: some View { + Toggle("Open after watching video", isOn: $openWatchNextOnFinishedWatching) + } + + private var openWatchNextOnFinishedWatchingDelayTextField: some View { + HStack { + Text("Autoplay delay") + .frame(minWidth: 140, alignment: .leading) + #if !os(iOS) + Spacer() + #endif + TextField("Delay", text: $openWatchNextOnFinishedWatchingDelay) + #if !os(iOS) + .frame(maxWidth: 100, alignment: .trailing) + #endif + .labelsHidden() + #if !os(macOS) + .keyboardType(.numberPad) + #endif + } + .multilineTextAlignment(.trailing) + } + private var sidebarPicker: some View { Picker("Sidebar", selection: $playerSidebar) { #if os(macOS) @@ -211,39 +225,6 @@ struct PlayerSettings: View { .modifier(SettingsPickerModifier()) } - private var playerActionsButtonLabelStylePicker: some View { - Picker("Video actions buttons", selection: $playerActionsButtonLabelStyle) { - Text("Show only icons").tag(ButtonLabelStyle.iconOnly) - Text("Show icons and text when space permits").tag(ButtonLabelStyle.iconAndText) - } - .modifier(SettingsPickerModifier()) - } - - private var detailsButtonLabelStylePicker: some View { - Picker("Pages buttons", selection: $playerDetailsPageButtonLabelStyle) { - Text("Show only icons").tag(ButtonLabelStyle.iconOnly) - Text("Show icons and text when space permits").tag(ButtonLabelStyle.iconAndText) - } - .modifier(SettingsPickerModifier()) - } - - private var showInspectorPicker: some View { - Picker("Inspector visibility", selection: $showInspector) { - Text("Always").tag(ShowInspectorSetting.always) - Text("Only for local files and URLs").tag(ShowInspectorSetting.onlyLocal) - } - .modifier(SettingsPickerModifier()) - } - - private var detailsToolbarPositionPicker: some View { - Picker("Pages toolbar position", selection: $detailsToolbarPosition) { - ForEach(DetailsToolbarPositionSetting.allCases, id: \.self) { setting in - Text(setting.rawValue.capitalized.localized()).tag(setting) - } - } - .modifier(SettingsPickerModifier()) - } - private var horizontalPlayerGestureEnabledToggle: some View { Toggle("Seek with horizontal swipe on video", isOn: $horizontalPlayerGestureEnabled) } @@ -310,16 +291,8 @@ struct PlayerSettings: View { private var pauseOnEnteringBackgroundToogle: some View { Toggle("Pause when entering background", isOn: $pauseOnEnteringBackground) } - - private var closePlayerOnItemCloseToggle: some View { - Toggle("Close player when closing video", isOn: $closePlayerOnItemClose) - } #endif - private var closeLastItemOnPlaybackEndToggle: some View { - Toggle("Close video after playing last in the queue", isOn: $closeLastItemOnPlaybackEnd) - } - #if os(iOS) private var honorSystemOrientationLockToggle: some View { Toggle("Honor orientation lock", isOn: $honorSystemOrientationLock) diff --git a/Shared/Settings/SettingsView.swift b/Shared/Settings/SettingsView.swift index 3c95552e..f17c3d07 100644 --- a/Shared/Settings/SettingsView.swift +++ b/Shared/Settings/SettingsView.swift @@ -38,7 +38,7 @@ struct SettingsView: View { .tabItem { Label("Browsing", systemImage: "list.and.film") } - .tag(Optional(Tabs.browsing)) + .tag(Tabs.browsing) Form { PlayerSettings() @@ -46,7 +46,7 @@ struct SettingsView: View { .tabItem { Label("Player", systemImage: "play.rectangle") } - .tag(Optional(Tabs.player)) + .tag(Tabs.player) Form { QualitySettings() @@ -54,7 +54,7 @@ struct SettingsView: View { .tabItem { Label("Quality", systemImage: "4k.tv") } - .tag(Optional(Tabs.quality)) + .tag(Tabs.quality) Form { HistorySettings() @@ -62,7 +62,7 @@ struct SettingsView: View { .tabItem { Label("History", systemImage: "clock.arrow.circlepath") } - .tag(Optional(Tabs.history)) + .tag(Tabs.history) if !accounts.isEmpty { Form { @@ -71,7 +71,7 @@ struct SettingsView: View { .tabItem { Label("SponsorBlock", systemImage: "dollarsign.circle") } - .tag(Optional(Tabs.sponsorBlock)) + .tag(Tabs.sponsorBlock) } Form { LocationsSettings() @@ -79,7 +79,7 @@ struct SettingsView: View { .tabItem { Label("Locations", systemImage: "globe") } - .tag(Optional(Tabs.locations)) + .tag(Tabs.locations) Group { AdvancedSettings() @@ -87,7 +87,7 @@ struct SettingsView: View { .tabItem { Label("Advanced", systemImage: "wrench.and.screwdriver") } - .tag(Optional(Tabs.advanced)) + .tag(Tabs.advanced) Form { Help() @@ -95,7 +95,7 @@ struct SettingsView: View { .tabItem { Label("Help", systemImage: "questionmark.circle") } - .tag(Optional(Tabs.help)) + .tag(Tabs.help) } .padding(20) .frame(width: 600, height: windowHeight) @@ -225,9 +225,9 @@ struct SettingsView: View { private var windowHeight: Double { switch selection { case .browsing: - return 680 + return 700 case .player: - return 900 + return 730 case .quality: return 420 case .history: diff --git a/Yattee.xcodeproj/project.pbxproj b/Yattee.xcodeproj/project.pbxproj index 11c65f7f..1588b5c0 100644 --- a/Yattee.xcodeproj/project.pbxproj +++ b/Yattee.xcodeproj/project.pbxproj @@ -177,8 +177,6 @@ 3718B9A02921A9620003DB2E /* VideoDetailsOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E80F3B287B107F00561799 /* VideoDetailsOverlay.swift */; }; 3718B9A12921A9640003DB2E /* VideoDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81AFE26D2CA3700675966 /* VideoDetails.swift */; }; 3718B9A22921A9670003DB2E /* VideoActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374924EF29216C630017D862 /* VideoActions.swift */; }; - 3718B9A32921A96A0003DB2E /* VideoDetailsToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374924DF292126A00017D862 /* VideoDetailsToolbar.swift */; }; - 3718B9A42921A96C0003DB2E /* VideoDetailsTool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374924E92921666E0017D862 /* VideoDetailsTool.swift */; }; 3718B9A52921A97F0003DB2E /* InspectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374924E2292141320017D862 /* InspectorView.swift */; }; 3718B9A62921A9BE0003DB2E /* PreferenceKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374924EC2921669B0017D862 /* PreferenceKeys.swift */; }; 37192D5528B0D5D60012EEDD /* PlayerLayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373031F22838388A000CFD59 /* PlayerLayerView.swift */; }; @@ -354,14 +352,10 @@ 374924DA2921050B0017D862 /* LocationsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374924D92921050B0017D862 /* LocationsSettings.swift */; }; 374924DB2921050B0017D862 /* LocationsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374924D92921050B0017D862 /* LocationsSettings.swift */; }; 374924DC2921050B0017D862 /* LocationsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374924D92921050B0017D862 /* LocationsSettings.swift */; }; - 374924E0292126A00017D862 /* VideoDetailsToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374924DF292126A00017D862 /* VideoDetailsToolbar.swift */; }; - 374924E1292126A00017D862 /* VideoDetailsToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374924DF292126A00017D862 /* VideoDetailsToolbar.swift */; }; 374924E3292141320017D862 /* InspectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374924E2292141320017D862 /* InspectorView.swift */; }; 374924E4292141320017D862 /* InspectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374924E2292141320017D862 /* InspectorView.swift */; }; 374924E729215FB60017D862 /* TapRecognizerViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374924E629215FB60017D862 /* TapRecognizerViewModifier.swift */; }; 374924E829215FB60017D862 /* TapRecognizerViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374924E629215FB60017D862 /* TapRecognizerViewModifier.swift */; }; - 374924EA2921666E0017D862 /* VideoDetailsTool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374924E92921666E0017D862 /* VideoDetailsTool.swift */; }; - 374924EB2921666E0017D862 /* VideoDetailsTool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374924E92921666E0017D862 /* VideoDetailsTool.swift */; }; 374924ED2921669B0017D862 /* PreferenceKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374924EC2921669B0017D862 /* PreferenceKeys.swift */; }; 374924EE2921669B0017D862 /* PreferenceKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374924EC2921669B0017D862 /* PreferenceKeys.swift */; }; 374924F029216C630017D862 /* VideoActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374924EF29216C630017D862 /* VideoActions.swift */; }; @@ -1235,10 +1229,8 @@ 37484C3026FCB8F900287258 /* AccountValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountValidator.swift; sourceTree = ""; }; 374924D92921050B0017D862 /* LocationsSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationsSettings.swift; sourceTree = ""; }; 374924DE29211F5F0017D862 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/Localizable.strings; sourceTree = ""; }; - 374924DF292126A00017D862 /* VideoDetailsToolbar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoDetailsToolbar.swift; sourceTree = ""; }; 374924E2292141320017D862 /* InspectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorView.swift; sourceTree = ""; }; 374924E629215FB60017D862 /* TapRecognizerViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TapRecognizerViewModifier.swift; sourceTree = ""; }; - 374924E92921666E0017D862 /* VideoDetailsTool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDetailsTool.swift; sourceTree = ""; }; 374924EC2921669B0017D862 /* PreferenceKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferenceKeys.swift; sourceTree = ""; }; 374924EF29216C630017D862 /* VideoActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoActions.swift; sourceTree = ""; }; 37494EA429200B14000DF176 /* DocumentsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentsView.swift; sourceTree = ""; }; @@ -2010,8 +2002,6 @@ 37CFB48428AFE2510070024C /* VideoDescription.swift */, 37B81AFE26D2CA3700675966 /* VideoDetails.swift */, 37B81AFB26D2C9C900675966 /* VideoDetailsPaddingModifier.swift */, - 374924DF292126A00017D862 /* VideoDetailsToolbar.swift */, - 374924E92921666E0017D862 /* VideoDetailsTool.swift */, ); path = "Video Details"; sourceTree = ""; @@ -3043,7 +3033,6 @@ 374DE88028BB896C0062BBF2 /* PlayerDragGesture.swift in Sources */, 37ECED56289FE166002BC2C9 /* SafeArea.swift in Sources */, 37CEE4BD2677B670005A1EFE /* SingleAssetStream.swift in Sources */, - 374924E0292126A00017D862 /* VideoDetailsToolbar.swift in Sources */, 37C2211D27ADA33300305B41 /* MPVViewController.swift in Sources */, 371B7E612759706A00D21217 /* CommentsView.swift in Sources */, 379DC3D128BA4EB400B09677 /* Seek.swift in Sources */, @@ -3196,7 +3185,6 @@ 374C053F272472C0009BDDBE /* PlayerSponsorBlock.swift in Sources */, 375F7410289DC35A00747050 /* PlayerBackendView.swift in Sources */, 37FB28412721B22200A57617 /* ContentItem.swift in Sources */, - 374924EA2921666E0017D862 /* VideoDetailsTool.swift in Sources */, 379F141F289ECE7F00DE48B5 /* QualitySettings.swift in Sources */, 378E9C4029455A5800B2D696 /* ChannelsView.swift in Sources */, 37C3A24D272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */, @@ -3428,7 +3416,6 @@ 37169AA32729D98A0011DE61 /* InstancesBridge.swift in Sources */, 37B044B826F7AB9000E1419D /* SettingsView.swift in Sources */, 377692572946476F0055EC18 /* ChannelPlaylistsCacheModel.swift in Sources */, - 374924E1292126A00017D862 /* VideoDetailsToolbar.swift in Sources */, 37F5E8BB291BEF69006C15F5 /* BaseCacheModel.swift in Sources */, 3765788A2685471400D4EA09 /* Playlist.swift in Sources */, 37030FFC27B0398000ECDDAA /* MPVClient.swift in Sources */, @@ -3447,7 +3434,6 @@ 37BD07BC2698AB60003EBB87 /* AppSidebarNavigation.swift in Sources */, 37579D5E27864F5F00FD0B98 /* Help.swift in Sources */, 37EF9A77275BEB8E0043B585 /* CommentView.swift in Sources */, - 374924EB2921666E0017D862 /* VideoDetailsTool.swift in Sources */, 3748186F26A769D60084E870 /* DetailBadge.swift in Sources */, 3744A96128B99ADD005DE0A7 /* PlayerControlsLayout.swift in Sources */, 372915E72687E3B900F5A35B /* Defaults.swift in Sources */, @@ -3652,7 +3638,6 @@ 379DC3D328BA4EB400B09677 /* Seek.swift in Sources */, 375DFB5A26F9DA010013F468 /* InstancesModel.swift in Sources */, 3769C0302779F18600DDB3EA /* PlaceholderProgressView.swift in Sources */, - 3718B9A32921A96A0003DB2E /* VideoDetailsToolbar.swift in Sources */, 37F4AE7426828F0900BD60EA /* VerticalCells.swift in Sources */, 376578872685429C00D4EA09 /* CaseIterable+Next.swift in Sources */, 37BDFF1D29487C5A000C6404 /* ChannelListItem.swift in Sources */, @@ -3693,7 +3678,6 @@ 37F4AD2828613B81004D0F66 /* Color+Debug.swift in Sources */, 37E8B0F227B326F30024006F /* Comparable+Clamped.swift in Sources */, 375168D82700FDB9008F96A6 /* Debounce.swift in Sources */, - 3718B9A42921A96C0003DB2E /* VideoDetailsTool.swift in Sources */, 37BA794126DB8F97002A0235 /* ChannelVideosView.swift in Sources */, 371CC77229468BDC00979C1A /* SettingsButtons.swift in Sources */, 37C0697C2725C09E00F7F6CB /* PlayerQueueItemBridge.swift in Sources */,