From 25b4e382f2fbb67ec6c8d8b0af2bfe5229c1e047 Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Mon, 3 Jan 2022 23:37:48 -0700 Subject: [PATCH] confirm close tfor tvos --- .../Views/SettingsView/SettingsView.swift | 3 + .../VideoPlayer/VLCPlayerViewController.swift | 116 ++++++++++++++---- .../tvOSOverlay/ConfirmCloseOverlay.swift | 40 ++++++ JellyfinPlayer.xcodeproj/project.pbxproj | 4 + .../SwiftfinStore/SwiftfinStoreDefaults.swift | 1 + Shared/ViewModels/VideoPlayerViewModel.swift | 15 ++- 6 files changed, 154 insertions(+), 25 deletions(-) create mode 100644 JellyfinPlayer tvOS/Views/VideoPlayer/tvOSOverlay/ConfirmCloseOverlay.swift diff --git a/JellyfinPlayer tvOS/Views/SettingsView/SettingsView.swift b/JellyfinPlayer tvOS/Views/SettingsView/SettingsView.swift index 2ce3ce98..a211cd19 100644 --- a/JellyfinPlayer tvOS/Views/SettingsView/SettingsView.swift +++ b/JellyfinPlayer tvOS/Views/SettingsView/SettingsView.swift @@ -19,6 +19,7 @@ struct SettingsView: View { @Default(.videoPlayerJumpForward) var jumpForwardLength @Default(.videoPlayerJumpBackward) var jumpBackwardLength @Default(.downActionShowsMenu) var downActionShowsMenu + @Default(.confirmClose) var confirmClose var body: some View { GeometryReader { reader in @@ -76,6 +77,8 @@ struct SettingsView: View { Toggle("Press Down for Menu", isOn: $downActionShowsMenu) + Toggle("Confirm Close", isOn: $confirmClose) + Button { settingsRouter.route(to: \.overlaySettings) } label: { diff --git a/JellyfinPlayer tvOS/Views/VideoPlayer/VLCPlayerViewController.swift b/JellyfinPlayer tvOS/Views/VideoPlayer/VLCPlayerViewController.swift index 08218ee3..451b664e 100644 --- a/JellyfinPlayer tvOS/Views/VideoPlayer/VLCPlayerViewController.swift +++ b/JellyfinPlayer tvOS/Views/VideoPlayer/VLCPlayerViewController.swift @@ -29,6 +29,7 @@ class VLCPlayerViewController: UIViewController { private var lastProgressReportTicks: Int64 = 0 private var viewModelListeners = Set() private var overlayDismissTimer: Timer? + private var confirmCloseOverlayDismissTimer: Timer? private var currentPlayerTicks: Int64 { return Int64(vlcMediaPlayer.time.intValue) * 100_000 @@ -42,11 +43,16 @@ class VLCPlayerViewController: UIViewController { return currentOverlayContentHostingController?.view.alpha ?? 0 > 0 } + private var displayingConfirmClose: Bool { + return currentConfirmCloseHostingController?.view.alpha ?? 0 > 0 + } + private lazy var videoContentView = makeVideoContentView() private lazy var jumpBackwardOverlayView = makeJumpBackwardOverlayView() private lazy var jumpForwardOverlayView = makeJumpForwardOverlayView() private var currentOverlayHostingController: UIHostingController? private var currentOverlayContentHostingController: UIHostingController? + private var currentConfirmCloseHostingController: UIHostingController? // MARK: init @@ -192,10 +198,16 @@ class VLCPlayerViewController: UIViewController { switch(buttonPress) { case .menu: () // Captured by other gesture case .playPause: + hideConfirmCloseOverlay() + didSelectMain() case .select: + hideConfirmCloseOverlay() + didGenerallyTap() case .upArrow: + hideConfirmCloseOverlay() + if displayingContentOverlay { hideOverlayContent() @@ -203,16 +215,22 @@ class VLCPlayerViewController: UIViewController { restartOverlayDismissTimer() } case .downArrow: + hideConfirmCloseOverlay() + if Defaults[.downActionShowsMenu] { if !displayingContentOverlay { didSelectMenu() } } case .leftArrow: + hideConfirmCloseOverlay() + if !displayingContentOverlay { didSelectBackward() } case .rightArrow: + hideConfirmCloseOverlay() + if !displayingContentOverlay { didSelectForward() } @@ -229,6 +247,7 @@ class VLCPlayerViewController: UIViewController { view.addGestureRecognizer(pressRecognizer) } + // MARK: didPressMenu @objc private func didPressMenu() { if displayingOverlay { hideOverlay() @@ -237,6 +256,11 @@ class VLCPlayerViewController: UIViewController { showOverlay() restartOverlayDismissTimer() + } else if viewModel.confirmClose && !displayingConfirmClose { + + showConfirmCloseOverlay() + restartConfirmCloseDismissTimer() + } else { vlcMediaPlayer.pause() @@ -255,7 +279,7 @@ class VLCPlayerViewController: UIViewController { // TODO: Look at injecting viewModel into the environment so it updates the current overlay - // Overlay + // Main overlay if let currentOverlayHostingController = currentOverlayHostingController { // UX fade-out UIView.animate(withDuration: 0.5) { @@ -295,7 +319,7 @@ class VLCPlayerViewController: UIViewController { self.currentOverlayHostingController = newOverlayHostingController - // OverlayContent + // Media Stream selection if let currentOverlayContentHostingController = currentOverlayContentHostingController { currentOverlayContentHostingController.view.isHidden = true @@ -303,26 +327,9 @@ class VLCPlayerViewController: UIViewController { currentOverlayContentHostingController.removeFromParent() } -// let newSmallMenuOverlayView = SmallMediaStreamSelectionView(viewModel: viewModel, -// title: "Subtitles", -// items: viewModel.subtitleStreams) { selectedMediaStream in -// self.didSelectSubtitleStream(index: selectedMediaStream.index ?? -1) -// } -// let newSmallMenuOverlayView = SmallMediaStreamSelectionView(viewModel: viewModel, -// items: viewModel.subtitleStreams, -// selectedIndex: viewModel.selectedSubtitleStreamIndex, -// title: "Subtitles") { selectedMediaStream in -// DispatchQueue.main.async { -// self.viewModel.selectedSubtitleStreamIndex = selectedMediaStream.index ?? -1 -// self.didSelectSubtitleStream(index: selectedMediaStream.index ?? -1) -// } -// } - let newSmallMenuOverlayView = SmallMediaStreamSelectionView(viewModel: viewModel) let newOverlayContentHostingController = UIHostingController(rootView: newSmallMenuOverlayView) -// let newOverlayContentView = tvOSOverlayContentView(viewModel: viewModel) -// let newOverlayContentHostingController = UIHostingController(rootView: newOverlayContentView) newOverlayContentHostingController.view.translatesAutoresizingMaskIntoConstraints = false newOverlayContentHostingController.view.backgroundColor = UIColor.clear @@ -341,6 +348,36 @@ class VLCPlayerViewController: UIViewController { ]) self.currentOverlayContentHostingController = newOverlayContentHostingController + + // Confirm close + if let currentConfirmCloseHostingController = currentConfirmCloseHostingController { + currentConfirmCloseHostingController.view.isHidden = true + + currentConfirmCloseHostingController.view.removeFromSuperview() + currentConfirmCloseHostingController.removeFromParent() + } + + let newConfirmCloseOverlay = ConfirmCloseOverlay() + + let newConfirmCloseHostingController = UIHostingController(rootView: newConfirmCloseOverlay) + + newConfirmCloseHostingController.view.translatesAutoresizingMaskIntoConstraints = false + newConfirmCloseHostingController.view.backgroundColor = UIColor.clear + + newConfirmCloseHostingController.view.alpha = 0 + + addChild(newConfirmCloseHostingController) + view.addSubview(newConfirmCloseHostingController.view) + newConfirmCloseHostingController.didMove(toParent: self) + + NSLayoutConstraint.activate([ + newConfirmCloseHostingController.view.topAnchor.constraint(equalTo: videoContentView.topAnchor), + newConfirmCloseHostingController.view.bottomAnchor.constraint(equalTo: videoContentView.bottomAnchor), + newConfirmCloseHostingController.view.leftAnchor.constraint(equalTo: videoContentView.leftAnchor), + newConfirmCloseHostingController.view.rightAnchor.constraint(equalTo: videoContentView.rightAnchor) + ]) + + self.currentConfirmCloseHostingController = newConfirmCloseHostingController // There is a behavior when setting this that the navigation bar // on the current navigation controller pops up, re-hide it @@ -543,6 +580,26 @@ extension VLCPlayerViewController { } } +// MARK: Show/Hide Confirm close +extension VLCPlayerViewController { + + private func showConfirmCloseOverlay() { + guard let currentConfirmCloseHostingController = currentConfirmCloseHostingController else { return } + + UIView.animate(withDuration: 0.2) { + currentConfirmCloseHostingController.view.alpha = 1 + } + } + + private func hideConfirmCloseOverlay() { + guard let currentConfirmCloseHostingController = currentConfirmCloseHostingController else { return } + + UIView.animate(withDuration: 0.5) { + currentConfirmCloseHostingController.view.alpha = 0 + } + } +} + // MARK: OverlayTimer extension VLCPlayerViewController { @@ -552,11 +609,28 @@ extension VLCPlayerViewController { } @objc private func dismissTimerFired() { - self.hideOverlay() + hideOverlay() } private func stopOverlayDismissTimer() { - self.overlayDismissTimer?.invalidate() + overlayDismissTimer?.invalidate() + } +} + +// MARK: Confirm Close Overlay Timer +extension VLCPlayerViewController { + + private func restartConfirmCloseDismissTimer() { + self.confirmCloseOverlayDismissTimer?.invalidate() + self.confirmCloseOverlayDismissTimer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(confirmCloseTimerFired), userInfo: nil, repeats: false) + } + + @objc private func confirmCloseTimerFired() { + hideConfirmCloseOverlay() + } + + private func stopConfirmCloseDismissTimer() { + confirmCloseOverlayDismissTimer?.invalidate() } } diff --git a/JellyfinPlayer tvOS/Views/VideoPlayer/tvOSOverlay/ConfirmCloseOverlay.swift b/JellyfinPlayer tvOS/Views/VideoPlayer/tvOSOverlay/ConfirmCloseOverlay.swift new file mode 100644 index 00000000..b9e2b493 --- /dev/null +++ b/JellyfinPlayer tvOS/Views/VideoPlayer/tvOSOverlay/ConfirmCloseOverlay.swift @@ -0,0 +1,40 @@ +// + /* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import SwiftUI + +struct ConfirmCloseOverlay: View { + var body: some View { + VStack { + HStack { + Image(systemName: "chevron.left.circle.fill") + .font(.system(size: 96)) + .padding(3) + .background(Color.black.opacity(0.4).mask(Circle())) + + Spacer() + } + .padding() + + Spacer() + } + .padding() + } +} + +struct ConfirmCloseOverlay_Previews: PreviewProvider { + static var previews: some View { + ZStack { + Color.red.ignoresSafeArea() + + ConfirmCloseOverlay() + .ignoresSafeArea() + } + } +} diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 51fa5158..f1d84a70 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -385,6 +385,7 @@ E1E5D54C2783E27200692DFE /* ExperimentalSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D54B2783E27200692DFE /* ExperimentalSettingsView.swift */; }; E1E5D54F2783E67100692DFE /* OverlaySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D54E2783E67100692DFE /* OverlaySettingsView.swift */; }; E1E5D5512783E67700692DFE /* ExperimentalSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D5502783E67700692DFE /* ExperimentalSettingsView.swift */; }; + E1E5D553278419D900692DFE /* ConfirmCloseOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D552278419D900692DFE /* ConfirmCloseOverlay.swift */; }; E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; }; E1F0204F26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; }; E1FA2F7427818A8800B4C270 /* SmallMenuOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FA2F7327818A8800B4C270 /* SmallMenuOverlay.swift */; }; @@ -670,6 +671,7 @@ E1E5D54B2783E27200692DFE /* ExperimentalSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperimentalSettingsView.swift; sourceTree = ""; }; E1E5D54E2783E67100692DFE /* OverlaySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlaySettingsView.swift; sourceTree = ""; }; E1E5D5502783E67700692DFE /* ExperimentalSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperimentalSettingsView.swift; sourceTree = ""; }; + E1E5D552278419D900692DFE /* ConfirmCloseOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmCloseOverlay.swift; sourceTree = ""; }; E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerJumpLength.swift; sourceTree = ""; }; E1FA2F7327818A8800B4C270 /* SmallMenuOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmallMenuOverlay.swift; sourceTree = ""; }; E1FCD08726C35A0D007C8DCF /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = ""; }; @@ -1332,6 +1334,7 @@ E17885A7278130690094FBCF /* tvOSOverlay */ = { isa = PBXGroup; children = ( + E1E5D552278419D900692DFE /* ConfirmCloseOverlay.swift */, E1FA2F7327818A8800B4C270 /* SmallMenuOverlay.swift */, E178859F2780F55C0094FBCF /* tvOSVLCOverlay.swift */, ); @@ -1983,6 +1986,7 @@ 53ABFDEE26799DCD00886593 /* ImageView.swift in Sources */, 62E632E4267D3BA60063E547 /* MovieItemViewModel.swift in Sources */, 53649AB2269D019100A2D8B7 /* LogManager.swift in Sources */, + E1E5D553278419D900692DFE /* ConfirmCloseOverlay.swift in Sources */, E13DD3D6271693CD009D4DAF /* SwiftfinStoreDefaults.swift in Sources */, E193D549271941CC00900D82 /* UserSignInView.swift in Sources */, 535870AA2669D8AE00D05A09 /* BlurHashDecode.swift in Sources */, diff --git a/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift b/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift index 8f999033..79393781 100644 --- a/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift +++ b/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift @@ -59,4 +59,5 @@ extension Defaults.Keys { // tvos specific static let downActionShowsMenu = Key("downActionShowsMenu", default: true, suite: SwiftfinStore.Defaults.generalSuite) + static let confirmClose = Key("confirmClose", default: false, suite: SwiftfinStore.Defaults.generalSuite) } diff --git a/Shared/ViewModels/VideoPlayerViewModel.swift b/Shared/ViewModels/VideoPlayerViewModel.swift index 274db3d7..cba905f3 100644 --- a/Shared/ViewModels/VideoPlayerViewModel.swift +++ b/Shared/ViewModels/VideoPlayerViewModel.swift @@ -92,6 +92,9 @@ final class VideoPlayerViewModel: ViewModel { // MARK: Experimental let syncSubtitleStateWithAdjacent: Bool + // MARK: tvOS + let confirmClose: Bool + // Full response kept for convenience let response: PlaybackInfoResponse @@ -161,6 +164,8 @@ final class VideoPlayerViewModel: ViewModel { self.syncSubtitleStateWithAdjacent = Defaults[.Experimental.syncSubtitleStateWithAdjacent] + self.confirmClose = Defaults[.confirmClose] + super.init() self.sliderPercentage = (item.userData?.playedPercentage ?? 0) / 100 @@ -355,7 +360,7 @@ extension VideoPlayerViewModel { .sink { completion in self.handleAPIRequestError(completion: completion) } receiveValue: { _ in - print("Playback start report sent!") + LogManager.shared.log.debug("Start report sent for item: \(self.item.id ?? "No ID")") } .store(in: &cancellables) } @@ -391,7 +396,7 @@ extension VideoPlayerViewModel { .sink { completion in self.handleAPIRequestError(completion: completion) } receiveValue: { _ in - print("Pause report sent!") + LogManager.shared.log.debug("Pause report sent for item: \(self.item.id ?? "No ID")") } .store(in: &cancellables) } @@ -434,9 +439,11 @@ extension VideoPlayerViewModel { .sink { completion in self.handleAPIRequestError(completion: completion) } receiveValue: { _ in - print("Playback progress sent!") + LogManager.shared.log.debug("Playback progress sent for item: \(self.item.id ?? "No ID")") } .store(in: &cancellables) + + self.lastProgressReport = nil } // MARK: sendStopReport @@ -458,7 +465,7 @@ extension VideoPlayerViewModel { .sink { completion in self.handleAPIRequestError(completion: completion) } receiveValue: { _ in - print("Playback stop report sent!") + LogManager.shared.log.debug("Stop report sent for item: \(self.item.id ?? "No ID")") } .store(in: &cancellables) }