From 5b451ceaaa710ce12edd5c75bee9947ac5e9262e Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Sun, 2 Jan 2022 21:20:20 -0700 Subject: [PATCH] begin final work --- .../Views/ItemView/EpisodeItemView.swift | 1 + .../Views/ItemView/SeasonItemView.swift | 11 +- .../Views/ItemView/SeriesItemView.swift | 12 +- .../NativePlayerViewController.swift | 12 +- .../VideoPlayer/PlayerOverlayDelegate.swift | 12 +- .../VideoPlayer/VLCPlayerViewController.swift | 38 ++- .../Views/VideoPlayer/VideoPlayerView.swift | 12 +- .../tvOSOverlay/SmallMenuOverlay.swift | 62 +++++ JellyfinPlayer.xcodeproj/project.pbxproj | 70 ++--- .../Components/PrimaryButtonView.swift | 41 +++ JellyfinPlayer/Views/HomeView.swift | 27 +- JellyfinPlayer/Views/ServerListView.swift | 16 +- JellyfinPlayer/Views/SettingsView.swift | 54 ++-- .../NativePlayerViewController.swift | 112 -------- .../Overlays/VLCPlayerOverlayView.swift | 251 ----------------- .../Overlays/VideoPlayerOverlay.swift | 26 -- .../Views/VideoPlayer/PlaybackSpeed.swift | 12 +- .../VideoPlayer/PlayerOverlayDelegate.swift | 19 +- ...yView.swift => VLCPlayerOverlayView.swift} | 118 ++++---- .../Views/VideoPlayer/VLCPlayerView.swift | 27 ++ .../VideoPlayer/VLCPlayerViewController.swift | 253 ++++++++++-------- .../Views/VideoPlayer/VideoPlayerView.swift | 41 --- .../iOSVideoPlayerCoordinator.swift | 28 +- .../BaseItemDto+VideoPlayerViewModel.swift | 30 +-- Shared/Objects/OverlaySliderColor.swift | 25 ++ Shared/Objects/OverlayType.swift | 17 ++ Shared/Singleton/SessionManager.swift | 14 +- .../SwiftfinStore/SwiftfinStoreDefaults.swift | 51 ++-- Shared/ViewModels/HomeViewModel.swift | 3 +- Shared/ViewModels/SettingsViewModel.swift | 11 +- Shared/ViewModels/VideoPlayerViewModel.swift | 123 ++++----- Shared/ViewModels/ViewModel.swift | 2 +- 32 files changed, 675 insertions(+), 856 deletions(-) create mode 100644 JellyfinPlayer tvOS/Views/VideoPlayer/tvOSOverlay/SmallMenuOverlay.swift create mode 100644 JellyfinPlayer/Components/PrimaryButtonView.swift delete mode 100644 JellyfinPlayer/Views/VideoPlayer/NativePlayerViewController.swift delete mode 100644 JellyfinPlayer/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift delete mode 100644 JellyfinPlayer/Views/VideoPlayer/Overlays/VideoPlayerOverlay.swift rename JellyfinPlayer/Views/VideoPlayer/{Overlays/VLCPlayerCompactOverlayView.swift => VLCPlayerOverlayView.swift} (78%) create mode 100644 JellyfinPlayer/Views/VideoPlayer/VLCPlayerView.swift delete mode 100644 JellyfinPlayer/Views/VideoPlayer/VideoPlayerView.swift create mode 100644 Shared/Objects/OverlaySliderColor.swift create mode 100644 Shared/Objects/OverlayType.swift diff --git a/JellyfinPlayer tvOS/Views/ItemView/EpisodeItemView.swift b/JellyfinPlayer tvOS/Views/ItemView/EpisodeItemView.swift index 7ba8750e..b1caaee7 100644 --- a/JellyfinPlayer tvOS/Views/ItemView/EpisodeItemView.swift +++ b/JellyfinPlayer tvOS/Views/ItemView/EpisodeItemView.swift @@ -122,6 +122,7 @@ struct EpisodeItemView: View { .foregroundColor(.primary) MediaPlayButtonRowView(viewModel: viewModel) + .environmentObject(itemRouter) } }.padding(.top, 50) diff --git a/JellyfinPlayer tvOS/Views/ItemView/SeasonItemView.swift b/JellyfinPlayer tvOS/Views/ItemView/SeasonItemView.swift index 475102fd..85631bd7 100644 --- a/JellyfinPlayer tvOS/Views/ItemView/SeasonItemView.swift +++ b/JellyfinPlayer tvOS/Views/ItemView/SeasonItemView.swift @@ -11,6 +11,8 @@ import SwiftUI import JellyfinAPI struct SeasonItemView: View { + + @EnvironmentObject var itemRouter: ItemCoordinator.Router @ObservedObject var viewModel: SeasonItemViewModel @State var wrappedScrollView: UIScrollView? @@ -101,10 +103,15 @@ struct SeasonItemView: View { ScrollView(.horizontal) { LazyHStack { Spacer().frame(width: 45) + ForEach(viewModel.episodes, id: \.id) { episode in - NavigationLink(destination: ItemView(item: episode)) { + + Button { + itemRouter.route(to: \.item, episode) + } label: { LandscapeItemElement(item: episode, inSeasonView: true) - }.buttonStyle(PlainNavigationLinkButtonStyle()) + } + .buttonStyle(PlainNavigationLinkButtonStyle()) } Spacer().frame(width: 45) } diff --git a/JellyfinPlayer tvOS/Views/ItemView/SeriesItemView.swift b/JellyfinPlayer tvOS/Views/ItemView/SeriesItemView.swift index 6a3a2ee3..21cb1d1b 100644 --- a/JellyfinPlayer tvOS/Views/ItemView/SeriesItemView.swift +++ b/JellyfinPlayer tvOS/Views/ItemView/SeriesItemView.swift @@ -11,6 +11,8 @@ import SwiftUI import JellyfinAPI struct SeriesItemView: View { + + @EnvironmentObject var itemRouter: ItemCoordinator.Router @ObservedObject var viewModel: SeriesItemViewModel @State var actors: [BaseItemPerson] = [] @@ -141,10 +143,16 @@ struct SeriesItemView: View { ScrollView(.horizontal) { LazyHStack { Spacer().frame(width: 45) + + + ForEach(viewModel.seasons, id: \.id) { season in - NavigationLink(destination: ItemView(item: season)) { + Button { + itemRouter.route(to: \.item, season) + } label: { PortraitItemElement(item: season) - }.buttonStyle(PlainNavigationLinkButtonStyle()) + } + .buttonStyle(PlainNavigationLinkButtonStyle()) } Spacer().frame(width: 45) } diff --git a/JellyfinPlayer tvOS/Views/VideoPlayer/NativePlayerViewController.swift b/JellyfinPlayer tvOS/Views/VideoPlayer/NativePlayerViewController.swift index 695f548f..8c28964c 100644 --- a/JellyfinPlayer tvOS/Views/VideoPlayer/NativePlayerViewController.swift +++ b/JellyfinPlayer tvOS/Views/VideoPlayer/NativePlayerViewController.swift @@ -1,9 +1,11 @@ // -// NativePlayerViewController.swift -// JellyfinVideoPlayerDev -// -// Created by Ethan Pippin on 11/20/21. -// + /* + * 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 AVKit import Combine diff --git a/JellyfinPlayer tvOS/Views/VideoPlayer/PlayerOverlayDelegate.swift b/JellyfinPlayer tvOS/Views/VideoPlayer/PlayerOverlayDelegate.swift index 456b8823..0a0f8cae 100644 --- a/JellyfinPlayer tvOS/Views/VideoPlayer/PlayerOverlayDelegate.swift +++ b/JellyfinPlayer tvOS/Views/VideoPlayer/PlayerOverlayDelegate.swift @@ -1,9 +1,11 @@ // -// PlayerOverlayDelegate.swift -// JellyfinVideoPlayerDev -// -// Created by Ethan Pippin on 12/27/21. -// + /* + * 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 Foundation diff --git a/JellyfinPlayer tvOS/Views/VideoPlayer/VLCPlayerViewController.swift b/JellyfinPlayer tvOS/Views/VideoPlayer/VLCPlayerViewController.swift index ce6e2a29..fc46bdd0 100644 --- a/JellyfinPlayer tvOS/Views/VideoPlayer/VLCPlayerViewController.swift +++ b/JellyfinPlayer tvOS/Views/VideoPlayer/VLCPlayerViewController.swift @@ -1,9 +1,11 @@ // -// PlayerViewController.swift -// JellyfinVideoPlayerDev -// -// Created by Ethan Pippin on 11/12/21. -// + /* + * 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 AVKit import AVFoundation @@ -203,7 +205,10 @@ class VLCPlayerViewController: UIViewController { case .upArrow: print("Up arrow") case .downArrow: - print("Down arrow") + stopOverlayDismissTimer() + + hideOverlay() + showOverlayContent() case .leftArrow: didSelectBackward() print("Left arrow") @@ -227,6 +232,8 @@ class VLCPlayerViewController: UIViewController { @objc private func didPressMenu() { if displayingOverlay { hideOverlay() + } else if displayingContentOverlay { + hideOverlayContent() } else { vlcMediaPlayer.pause() @@ -294,6 +301,11 @@ class VLCPlayerViewController: UIViewController { currentOverlayContentHostingController.removeFromParent() } +// let newSmallMenuOverlayView = SmallMediaStreamSelectionView(items: viewModel.subtitleStreams, +// selectedItem: viewModel.subtitleStreams.first(where: { $0.index == viewModel.selectedSubtitleStreamIndex })) { selectedMediaStream in +// self.didSelectSubtitleStream(index: selectedMediaStream.index ?? -1) +// } +// let newOverlayContentHostingController = UIHostingController(rootView: newSmallMenuOverlayView) let newOverlayContentView = tvOSOverlayContentView(viewModel: viewModel) let newOverlayContentHostingController = UIHostingController(rootView: newOverlayContentView) @@ -452,6 +464,10 @@ extension VLCPlayerViewController { guard currentOverlayContentHostingController.view.alpha != 1 else { return } + currentOverlayContentHostingController.view.setNeedsFocusUpdate() + currentOverlayContentHostingController.setNeedsFocusUpdate() + setNeedsFocusUpdate() + UIView.animate(withDuration: 0.2) { currentOverlayContentHostingController.view.alpha = 1 } @@ -462,6 +478,8 @@ extension VLCPlayerViewController { guard currentOverlayContentHostingController.view.alpha != 0 else { return } + setNeedsFocusUpdate() + UIView.animate(withDuration: 0.2) { currentOverlayContentHostingController.view.alpha = 0 } @@ -629,10 +647,10 @@ extension VLCPlayerViewController: PlayerOverlayDelegate { // TODO: Implement properly in overlays func didSelectMenu() { -// stopOverlayDismissTimer() -// -// hideOverlay() -// showOverlayContent() + stopOverlayDismissTimer() + + hideOverlay() + showOverlayContent() } // TODO: Implement properly in overlays diff --git a/JellyfinPlayer tvOS/Views/VideoPlayer/VideoPlayerView.swift b/JellyfinPlayer tvOS/Views/VideoPlayer/VideoPlayerView.swift index 8f9bf3e9..c1eb827e 100644 --- a/JellyfinPlayer tvOS/Views/VideoPlayer/VideoPlayerView.swift +++ b/JellyfinPlayer tvOS/Views/VideoPlayer/VideoPlayerView.swift @@ -1,9 +1,11 @@ // -// VideoPlayerView.swift -// JellyfinVideoPlayerDev -// -// Created by Ethan Pippin on 11/12/21. -// + /* + * 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 UIKit import SwiftUI diff --git a/JellyfinPlayer tvOS/Views/VideoPlayer/tvOSOverlay/SmallMenuOverlay.swift b/JellyfinPlayer tvOS/Views/VideoPlayer/tvOSOverlay/SmallMenuOverlay.swift new file mode 100644 index 00000000..c7f6d8e8 --- /dev/null +++ b/JellyfinPlayer tvOS/Views/VideoPlayer/tvOSOverlay/SmallMenuOverlay.swift @@ -0,0 +1,62 @@ +// + /* + * 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 JellyfinAPI +import SwiftUI + +struct SmallMediaStreamSelectionView: View { + + @State var selectedItem: MediaStream? + private var items: [MediaStream] + private var selectedAction: (MediaStream) -> Void + + init(items: [MediaStream], selectedItem: MediaStream? = nil, selectedAction: @escaping (MediaStream) -> Void) { + self.items = items + self.selectedItem = selectedItem + self.selectedAction = selectedAction + } + + var body: some View { + ZStack(alignment: .bottom) { + LinearGradient(gradient: Gradient(colors: [.clear, .black.opacity(0.7)]), + startPoint: .top, + endPoint: .bottom) + .ignoresSafeArea() + .frame(height: 150) + + VStack { + + Spacer() + + HStack { + Text("Subtitles") + .font(.title3) + Spacer() + } + + ScrollView(.horizontal) { + HStack { + ForEach(items, id: \.self) { item in + Button { +// self.selectedItem = item + } label: { + if item == selectedItem { + Label(item.displayTitle ?? "No Title", systemImage: "checkmark") + } else { + Text(item.displayTitle ?? "No Title") + } + } + } + } + } + .frame(maxHeight: 100) + } + } + } +} diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 008c5a65..522bee82 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -232,7 +232,6 @@ E100720726BDABC100CE3E31 /* MediaPlayButtonRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */; }; E10EAA45277BB646000269ED /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = E10EAA44277BB646000269ED /* JellyfinAPI */; }; E10EAA47277BB670000269ED /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = E10EAA46277BB670000269ED /* JellyfinAPI */; }; - E10EAA4A277BB6F5000269ED /* VideoPlayerOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10EAA49277BB6F5000269ED /* VideoPlayerOverlay.swift */; }; E10EAA4D277BB716000269ED /* Sliders in Frameworks */ = {isa = PBXBuildFile; productRef = E10EAA4C277BB716000269ED /* Sliders */; }; E10EAA4F277BBCC4000269ED /* CGSizeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10EAA4E277BBCC4000269ED /* CGSizeExtensions.swift */; }; E10EAA50277BBCC4000269ED /* CGSizeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10EAA4E277BBCC4000269ED /* CGSizeExtensions.swift */; }; @@ -329,6 +328,12 @@ E193D553271943D500900D82 /* tvOSMainTabCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D552271943D500900D82 /* tvOSMainTabCoordinator.swift */; }; E1A99999271A3429008E78C0 /* SwiftUICollection in Frameworks */ = {isa = PBXBuildFile; productRef = E1A99998271A3429008E78C0 /* SwiftUICollection */; }; E1A9999B271A343C008E78C0 /* SwiftUICollection in Frameworks */ = {isa = PBXBuildFile; productRef = E1A9999A271A343C008E78C0 /* SwiftUICollection */; }; + E1AA331D2782541500F6439C /* PrimaryButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AA331C2782541500F6439C /* PrimaryButtonView.swift */; }; + E1AA331F2782639D00F6439C /* OverlayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AA331E2782639D00F6439C /* OverlayType.swift */; }; + E1AA33202782639D00F6439C /* OverlayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AA331E2782639D00F6439C /* OverlayType.swift */; }; + E1AA33222782648000F6439C /* OverlaySliderColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AA33212782648000F6439C /* OverlaySliderColor.swift */; }; + E1AA33232782648000F6439C /* OverlaySliderColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AA33212782648000F6439C /* OverlaySliderColor.swift */; }; + E1AA332427829B5200F6439C /* OverlayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AA331E2782639D00F6439C /* OverlayType.swift */; }; E1AD104A26D94822003E4A08 /* DetailItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104926D94822003E4A08 /* DetailItem.swift */; }; E1AD104B26D94822003E4A08 /* DetailItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104926D94822003E4A08 /* DetailItem.swift */; }; E1AD104D26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */; }; @@ -344,10 +349,8 @@ E1C812BC277A8E5D00918266 /* PlaybackSpeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */; }; E1C812BD277A8E5D00918266 /* PlayerOverlayDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B5277A8E5D00918266 /* PlayerOverlayDelegate.swift */; }; E1C812BE277A8E5D00918266 /* VLCPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B6277A8E5D00918266 /* VLCPlayerViewController.swift */; }; - E1C812BF277A8E5D00918266 /* VLCPlayerOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B7277A8E5D00918266 /* VLCPlayerOverlayView.swift */; }; - E1C812C0277A8E5D00918266 /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B8277A8E5D00918266 /* VideoPlayerView.swift */; }; - E1C812C1277A8E5D00918266 /* NativePlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B9277A8E5D00918266 /* NativePlayerViewController.swift */; }; - E1C812C3277A8E5D00918266 /* VLCPlayerCompactOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812BB277A8E5D00918266 /* VLCPlayerCompactOverlayView.swift */; }; + E1C812C0277A8E5D00918266 /* VLCPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B8277A8E5D00918266 /* VLCPlayerView.swift */; }; + E1C812C3277A8E5D00918266 /* VLCPlayerOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812BB277A8E5D00918266 /* VLCPlayerOverlayView.swift */; }; E1C812C5277A90B200918266 /* URLComponentsExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812C4277A90B200918266 /* URLComponentsExtensions.swift */; }; E1C812CA277AE40900918266 /* PlayerOverlayDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812C6277AE40900918266 /* PlayerOverlayDelegate.swift */; }; E1C812CB277AE40900918266 /* NativePlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812C7277AE40900918266 /* NativePlayerViewController.swift */; }; @@ -373,6 +376,7 @@ E1E48CC9271E6D410021A2F9 /* RefreshHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E48CC8271E6D410021A2F9 /* RefreshHelper.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 */; }; E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD08726C35A0D007C8DCF /* NetworkError.swift */; }; E1FCD08926C35A0D007C8DCF /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD08726C35A0D007C8DCF /* NetworkError.swift */; }; E1FCD09626C47118007C8DCF /* ErrorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD09526C47118007C8DCF /* ErrorMessage.swift */; }; @@ -571,7 +575,6 @@ C4E508172703E8190045C9AB /* LibraryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListView.swift; sourceTree = ""; }; C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchView.swift; sourceTree = ""; }; E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayButtonRowView.swift; sourceTree = ""; }; - E10EAA49277BB6F5000269ED /* VideoPlayerOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerOverlay.swift; sourceTree = ""; }; E10EAA4E277BBCC4000269ED /* CGSizeExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGSizeExtensions.swift; sourceTree = ""; }; E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseItemDto+VideoPlayerViewModel.swift"; sourceTree = ""; }; E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinAPIError.swift; sourceTree = ""; }; @@ -617,6 +620,9 @@ E193D54C2719426600900D82 /* LibraryFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryFilterView.swift; sourceTree = ""; }; E193D54F2719430400900D82 /* ServerDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailView.swift; sourceTree = ""; }; E193D552271943D500900D82 /* tvOSMainTabCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = tvOSMainTabCoordinator.swift; sourceTree = ""; }; + E1AA331C2782541500F6439C /* PrimaryButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryButtonView.swift; sourceTree = ""; }; + E1AA331E2782639D00F6439C /* OverlayType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayType.swift; sourceTree = ""; }; + E1AA33212782648000F6439C /* OverlaySliderColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlaySliderColor.swift; sourceTree = ""; }; E1AD104926D94822003E4A08 /* DetailItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailItem.swift; sourceTree = ""; }; E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseItemDtoExtensions.swift; sourceTree = ""; }; E1AD105526D981CE003E4A08 /* PortraitHStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitHStackView.swift; sourceTree = ""; }; @@ -626,10 +632,8 @@ E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaybackSpeed.swift; sourceTree = ""; }; E1C812B5277A8E5D00918266 /* PlayerOverlayDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerOverlayDelegate.swift; sourceTree = ""; }; E1C812B6277A8E5D00918266 /* VLCPlayerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VLCPlayerViewController.swift; sourceTree = ""; }; - E1C812B7277A8E5D00918266 /* VLCPlayerOverlayView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VLCPlayerOverlayView.swift; sourceTree = ""; }; - E1C812B8277A8E5D00918266 /* VideoPlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerView.swift; sourceTree = ""; }; - E1C812B9277A8E5D00918266 /* NativePlayerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NativePlayerViewController.swift; sourceTree = ""; }; - E1C812BB277A8E5D00918266 /* VLCPlayerCompactOverlayView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VLCPlayerCompactOverlayView.swift; sourceTree = ""; }; + E1C812B8277A8E5D00918266 /* VLCPlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VLCPlayerView.swift; sourceTree = ""; }; + E1C812BB277A8E5D00918266 /* VLCPlayerOverlayView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VLCPlayerOverlayView.swift; sourceTree = ""; }; E1C812C4277A90B200918266 /* URLComponentsExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLComponentsExtensions.swift; sourceTree = ""; }; E1C812C6277AE40900918266 /* PlayerOverlayDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerOverlayDelegate.swift; sourceTree = ""; }; E1C812C7277AE40900918266 /* NativePlayerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NativePlayerViewController.swift; sourceTree = ""; }; @@ -645,6 +649,7 @@ E1D4BF8E271A079A00A11E64 /* BasicAppSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicAppSettingsView.swift; sourceTree = ""; }; E1E48CC8271E6D410021A2F9 /* RefreshHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshHelper.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 = ""; }; E1FCD09526C47118007C8DCF /* ErrorMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMessage.swift; sourceTree = ""; }; FDEDADB92FA8523BC8432E45 /* Pods-WidgetExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetExtension.release.xcconfig"; path = "Target Support Files/Pods-WidgetExtension/Pods-WidgetExtension.release.xcconfig"; sourceTree = ""; }; @@ -858,6 +863,8 @@ 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */, 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */, E19169CD272514760085832A /* HTTPScheme.swift */, + E1AA33212782648000F6439C /* OverlaySliderColor.swift */, + E1AA331E2782639D00F6439C /* OverlayType.swift */, E193D4DA27193CCA00900D82 /* PillStackable.swift */, E193D4D727193CAC00900D82 /* PortraitImageStackable.swift */, E1D4BF832719D25A00A11E64 /* TrackLanguage.swift */, @@ -1081,11 +1088,12 @@ 53F866422687A45400DCD1D7 /* Components */ = { isa = PBXGroup; children = ( + E188460326DEF04800B0C5B7 /* EpisodeCardVStackView.swift */, E1AD105B26D9ABDD003E4A08 /* PillHStackView.swift */, E1AD105526D981CE003E4A08 /* PortraitHStackView.swift */, - 53F866432687A45F00DCD1D7 /* PortraitItemView.swift */, - E188460326DEF04800B0C5B7 /* EpisodeCardVStackView.swift */, C4BE076D2720FEA8003F4AD1 /* PortraitItemElement.swift */, + 53F866432687A45F00DCD1D7 /* PortraitItemView.swift */, + E1AA331C2782541500F6439C /* PrimaryButtonView.swift */, ); path = Components; sourceTree = ""; @@ -1201,16 +1209,6 @@ path = Pods; sourceTree = ""; }; - E10EAA48277BB6D7000269ED /* Overlays */ = { - isa = PBXGroup; - children = ( - E10EAA49277BB6F5000269ED /* VideoPlayerOverlay.swift */, - E1C812BB277A8E5D00918266 /* VLCPlayerCompactOverlayView.swift */, - E1C812B7277A8E5D00918266 /* VLCPlayerOverlayView.swift */, - ); - path = Overlays; - sourceTree = ""; - }; E12186DF2718F2030010884C /* App */ = { isa = PBXGroup; children = ( @@ -1312,6 +1310,7 @@ E17885A7278130690094FBCF /* tvOSOverlay */ = { isa = PBXGroup; children = ( + E1FA2F7327818A8800B4C270 /* SmallMenuOverlay.swift */, E17885A5278130610094FBCF /* tvOSOverlayContent.swift */, E178859F2780F55C0094FBCF /* tvOSVLCOverlay.swift */, ); @@ -1350,11 +1349,10 @@ E193D5452719418B00900D82 /* VideoPlayer */ = { isa = PBXGroup; children = ( - E1C812B9277A8E5D00918266 /* NativePlayerViewController.swift */, - E10EAA48277BB6D7000269ED /* Overlays */, E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */, E1C812B5277A8E5D00918266 /* PlayerOverlayDelegate.swift */, - E1C812B8277A8E5D00918266 /* VideoPlayerView.swift */, + E1C812BB277A8E5D00918266 /* VLCPlayerOverlayView.swift */, + E1C812B8277A8E5D00918266 /* VLCPlayerView.swift */, E1C812B6277A8E5D00918266 /* VLCPlayerViewController.swift */, ); path = VideoPlayer; @@ -1911,6 +1909,7 @@ E1AD104E26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */, 62E632DD267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */, 536D3D81267BDFC60004248C /* PortraitItemElement.swift in Sources */, + E1AA33232782648000F6439C /* OverlaySliderColor.swift in Sources */, 62E1DCC4273CE19800C9AE76 /* URLExtensions.swift in Sources */, 5398514726B64E4100101B49 /* SearchBarView.swift in Sources */, E10EAA54277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift in Sources */, @@ -1935,6 +1934,7 @@ 53272537268C1DBB0035FBF1 /* SeasonItemView.swift in Sources */, 09389CC526814E4500AE350E /* DeviceProfileBuilder.swift in Sources */, E1C812D2277AE50A00918266 /* URLComponentsExtensions.swift in Sources */, + E1FA2F7427818A8800B4C270 /* SmallMenuOverlay.swift in Sources */, E193D53C27193F9500900D82 /* UserListCoordinator.swift in Sources */, E13DD3C927164B1E009D4DAF /* UIDeviceExtensions.swift in Sources */, 535870A62669D8AE00D05A09 /* LazyView.swift in Sources */, @@ -1942,6 +1942,7 @@ 6220D0AE26D5EABB00B8E046 /* ViewExtensions.swift in Sources */, 5321753E2671DE9C005491E6 /* Typings.swift in Sources */, E1C812CA277AE40900918266 /* PlayerOverlayDelegate.swift in Sources */, + E1AA33202782639D00F6439C /* OverlayType.swift in Sources */, E1F0204F26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */, 53ABFDEB2679753200886593 /* ConnectToServerView.swift in Sources */, 6264E88D273850380081A12A /* Strings.swift in Sources */, @@ -1992,7 +1993,7 @@ E1AD105626D981CE003E4A08 /* PortraitHStackView.swift in Sources */, 62C29EA126D102A500C1D2E7 /* iOSMainTabCoordinator.swift in Sources */, C4BE076E2720FEA8003F4AD1 /* PortraitItemElement.swift in Sources */, - E1C812C0277A8E5D00918266 /* VideoPlayerView.swift in Sources */, + E1C812C0277A8E5D00918266 /* VLCPlayerView.swift in Sources */, 535BAE9F2649E569005FA86D /* ItemView.swift in Sources */, E10EAA4F277BBCC4000269ED /* CGSizeExtensions.swift in Sources */, C40CD925271F8D1E000FB198 /* MovieLibrariesViewModel.swift in Sources */, @@ -2027,16 +2028,14 @@ 532175402671EE4F005491E6 /* LibraryFilterView.swift in Sources */, C4BE0763271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift in Sources */, 53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */, - E1C812BF277A8E5D00918266 /* VLCPlayerOverlayView.swift in Sources */, E1C812CE277AE43100918266 /* VideoPlayerViewModel.swift in Sources */, - E1C812C3277A8E5D00918266 /* VLCPlayerCompactOverlayView.swift in Sources */, + E1C812C3277A8E5D00918266 /* VLCPlayerOverlayView.swift in Sources */, E188460026DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift in Sources */, 091B5A8B2683142E00D78B61 /* UDPBroadCastConnection.swift in Sources */, 6267B3D626710B8900A7371D /* CollectionExtensions.swift in Sources */, E13DD3F5271793BB009D4DAF /* UserSignInView.swift in Sources */, E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */, 53649AB1269CFB1900A2D8B7 /* LogManager.swift in Sources */, - E1C812C1277A8E5D00918266 /* NativePlayerViewController.swift in Sources */, E13DD3E127176BD3009D4DAF /* ServerListViewModel.swift in Sources */, 62E632E9267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */, 62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */, @@ -2045,6 +2044,7 @@ 62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */, 53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */, E1E48CC9271E6D410021A2F9 /* RefreshHelper.swift in Sources */, + E1AA33222782648000F6439C /* OverlaySliderColor.swift in Sources */, E1D4BF842719D25A00A11E64 /* TrackLanguage.swift in Sources */, E14F7D0726DB36EF007C3AE6 /* ItemPortraitMainView.swift in Sources */, E1AD106226D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift in Sources */, @@ -2055,6 +2055,7 @@ 621338B32660A07800A81A2A /* LazyView.swift in Sources */, 6220D0B126D5EC9900B8E046 /* SettingsCoordinator.swift in Sources */, E1C812BE277A8E5D00918266 /* VLCPlayerViewController.swift in Sources */, + E1AA331D2782541500F6439C /* PrimaryButtonView.swift in Sources */, 62C29EA626D1036A00C1D2E7 /* HomeCoordinator.swift in Sources */, 531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */, E13DD3E927177ED6009D4DAF /* ServerListCoordinator.swift in Sources */, @@ -2063,6 +2064,7 @@ E1AD104A26D94822003E4A08 /* DetailItem.swift in Sources */, 62E632E0267D30CA0063E547 /* LibraryViewModel.swift in Sources */, E193D4DB27193CCA00900D82 /* PillStackable.swift in Sources */, + E1AA331F2782639D00F6439C /* OverlayType.swift in Sources */, E193D4D827193CAC00900D82 /* PortraitImageStackable.swift in Sources */, 624C21752685CF60007F1390 /* SearchablePickerView.swift in Sources */, E1D4BF7C2719D05000A11E64 /* BasicAppSettingsView.swift in Sources */, @@ -2101,7 +2103,6 @@ E13DD4022717EE79009D4DAF /* UserListCoordinator.swift in Sources */, E1FCD09626C47118007C8DCF /* ErrorMessage.swift in Sources */, 53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */, - E10EAA4A277BB6F5000269ED /* VideoPlayerOverlay.swift in Sources */, 53892772263C8C6F0035E14B /* LoadingView.swift in Sources */, 625CB5752678C33500530A6E /* LibraryListViewModel.swift in Sources */, ); @@ -2135,6 +2136,7 @@ 62E1DCC5273CE19800C9AE76 /* URLExtensions.swift in Sources */, 62EC353226766849000E9F2D /* SessionManager.swift in Sources */, 536D3D79267BD5D00004248C /* ViewModel.swift in Sources */, + E1AA332427829B5200F6439C /* OverlayType.swift in Sources */, E1D4BF8C2719F39F00A11E64 /* AppAppearance.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2291,7 +2293,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_ASSET_PATHS = "\"JellyfinPlayer tvOS/Preview Content\""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = TY84JMYEFE; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = "JellyfinPlayer tvOS/Info.plist"; @@ -2300,7 +2302,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = com.swiftfin; + PRODUCT_BUNDLE_IDENTIFIER = pips.swiftfin; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; @@ -2321,7 +2323,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_ASSET_PATHS = "\"JellyfinPlayer tvOS/Preview Content\""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = TY84JMYEFE; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = "JellyfinPlayer tvOS/Info.plist"; @@ -2330,7 +2332,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = com.swiftfin; + PRODUCT_BUNDLE_IDENTIFIER = pips.swiftfin; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/JellyfinPlayer/Components/PrimaryButtonView.swift b/JellyfinPlayer/Components/PrimaryButtonView.swift new file mode 100644 index 00000000..7f33ff9a --- /dev/null +++ b/JellyfinPlayer/Components/PrimaryButtonView.swift @@ -0,0 +1,41 @@ +// + /* + * 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 PrimaryButtonView: View { + + private let title: String + private let action: () -> Void + + init(title: String, _ action: @escaping () -> Void) { + self.title = title + self.action = action + } + + var body: some View { + Button { + action() + } label: { + ZStack { + Rectangle() + .foregroundColor(Color(UIColor.systemPurple)) + .frame(maxWidth: 400, maxHeight: 50) + .frame(height: 50) + .cornerRadius(10) + .padding(.horizontal, 30) + .padding([.top, .bottom], 20) + + Text(title) + .foregroundColor(Color.white) + .bold() + } + } + } +} diff --git a/JellyfinPlayer/Views/HomeView.swift b/JellyfinPlayer/Views/HomeView.swift index 26132fb9..892d0ab7 100644 --- a/JellyfinPlayer/Views/HomeView.swift +++ b/JellyfinPlayer/Views/HomeView.swift @@ -20,8 +20,33 @@ struct HomeView: View { @ViewBuilder var innerBody: some View { - if viewModel.isLoading { + if let errorMessage = viewModel.errorMessage { + VStack(spacing: 5) { + if viewModel.isLoading { + ProgressView() + .frame(width: 100, height: 100) + .scaleEffect(2) + } else { + Image(systemName: "xmark.circle.fill") + .font(.system(size: 72)) + .foregroundColor(Color.red) + .frame(width: 100, height: 100) + } + + Text("\(errorMessage.code)") + Text(errorMessage.displayMessage) + .frame(minWidth: 50, maxWidth: 240) + .multilineTextAlignment(.center) + + PrimaryButtonView(title: "Retry") { + viewModel.refresh() + } + } + .offset(y: -50) + } else if viewModel.isLoading { ProgressView() + .frame(width: 100, height: 100) + .scaleEffect(2) } else { ScrollView { VStack(alignment: .leading) { diff --git a/JellyfinPlayer/Views/ServerListView.swift b/JellyfinPlayer/Views/ServerListView.swift index a09f36ff..85833f3b 100644 --- a/JellyfinPlayer/Views/ServerListView.swift +++ b/JellyfinPlayer/Views/ServerListView.swift @@ -69,22 +69,8 @@ struct ServerListView: View { .frame(minWidth: 50, maxWidth: 240) .multilineTextAlignment(.center) - Button { + PrimaryButtonView(title: L10n.connect.stringValue) { serverListRouter.route(to: \.connectToServer) - } label: { - ZStack { - Rectangle() - .foregroundColor(Color.jellyfinPurple) - .frame(maxWidth: 400, maxHeight: 50) - .frame(height: 50) - .cornerRadius(10) - .padding(.horizontal, 30) - .padding([.top, .bottom], 20) - - L10n.connect.text - .foregroundColor(Color.white) - .bold() - } } } } diff --git a/JellyfinPlayer/Views/SettingsView.swift b/JellyfinPlayer/Views/SettingsView.swift index 63819e44..6f4ec697 100644 --- a/JellyfinPlayer/Views/SettingsView.swift +++ b/JellyfinPlayer/Views/SettingsView.swift @@ -23,7 +23,6 @@ struct SettingsView: View { @Default(.appAppearance) var appAppearance @Default(.videoPlayerJumpForward) var jumpForwardLength @Default(.videoPlayerJumpBackward) var jumpBackwardLength - @Default(.nativeVideoPlayer) var nativeVideoPlayer var body: some View { Form { @@ -83,8 +82,7 @@ struct SettingsView: View { } } - Section(header: Text("Playback")) { - Toggle("Native Player", isOn: $nativeVideoPlayer) + Section(header: Text("Networking")) { Picker("Default local quality", selection: $inNetworkStreamBitrate) { ForEach(self.viewModel.bitrates, id: \.self) { bitrate in Text(bitrate.name).tag(bitrate.value) @@ -96,43 +94,45 @@ struct SettingsView: View { Text(bitrate.name).tag(bitrate.value) } } - + } + + Section(header: Text("Video Player")) { Picker("Jump Forward Length", selection: $jumpForwardLength) { - ForEach(self.viewModel.videoPlayerJumpLengths, id: \.self) { length in + ForEach(VideoPlayerJumpLength.allCases, id: \.self) { length in Text(length.label).tag(length.rawValue) } } Picker("Jump Backward Length", selection: $jumpBackwardLength) { - ForEach(self.viewModel.videoPlayerJumpLengths, id: \.self) { length in + ForEach(VideoPlayerJumpLength.allCases, id: \.self) { length in Text(length.label).tag(length.rawValue) } } } Section(header: L10n.accessibility.text) { - Toggle("Automatically show subtitles", isOn: $isAutoSelectSubtitles) - SearchablePicker(label: "Preferred subtitle language", - options: viewModel.langs, - optionToString: { $0.name }, - selected: Binding(get: { - viewModel.langs - .first(where: { $0.isoCode == autoSelectSubtitlesLangcode - }) ?? - .auto - }, - set: { autoSelectSubtitlesLangcode = $0.isoCode })) - SearchablePicker(label: "Preferred audio language", - options: viewModel.langs, - optionToString: { $0.name }, - selected: Binding(get: { - viewModel.langs - .first(where: { $0.isoCode == autoSelectAudioLangcode }) ?? - .auto - }, - set: { autoSelectAudioLangcode = $0.isoCode })) +// Toggle("Automatically show subtitles", isOn: $isAutoSelectSubtitles) +// SearchablePicker(label: "Preferred subtitle language", +// options: viewModel.langs, +// optionToString: { $0.name }, +// selected: Binding(get: { +// viewModel.langs +// .first(where: { $0.isoCode == autoSelectSubtitlesLangcode +// }) ?? +// .auto +// }, +// set: { autoSelectSubtitlesLangcode = $0.isoCode })) +// SearchablePicker(label: "Preferred audio language", +// options: viewModel.langs, +// optionToString: { $0.name }, +// selected: Binding(get: { +// viewModel.langs +// .first(where: { $0.isoCode == autoSelectAudioLangcode }) ?? +// .auto +// }, +// set: { autoSelectAudioLangcode = $0.isoCode })) Picker(L10n.appearance, selection: $appAppearance) { - ForEach(self.viewModel.appearances, id: \.self) { appearance in + ForEach(AppAppearance.allCases, id: \.self) { appearance in Text(appearance.localizedName).tag(appearance.rawValue) } } diff --git a/JellyfinPlayer/Views/VideoPlayer/NativePlayerViewController.swift b/JellyfinPlayer/Views/VideoPlayer/NativePlayerViewController.swift deleted file mode 100644 index e6cb3f16..00000000 --- a/JellyfinPlayer/Views/VideoPlayer/NativePlayerViewController.swift +++ /dev/null @@ -1,112 +0,0 @@ -// -// NativePlayerViewController.swift -// JellyfinVideoPlayerDev -// -// Created by Ethan Pippin on 11/20/21. -// - -import AVKit -import Combine -import JellyfinAPI -import UIKit - -class NativePlayerViewController: AVPlayerViewController { - - let viewModel: VideoPlayerViewModel - - private var timeObserverToken: Any? - - private var lastProgressTicks: Int64 = 0 - - init(viewModel: VideoPlayerViewModel) { - - self.viewModel = viewModel - - super.init(nibName: nil, bundle: nil) - - let player = AVPlayer(url: viewModel.hlsURL) - - player.appliesMediaSelectionCriteriaAutomatically = false - player.currentItem?.externalMetadata = createMetadata() - - let timeScale = CMTimeScale(NSEC_PER_SEC) - let time = CMTime(seconds: 5, preferredTimescale: timeScale) - - timeObserverToken = player.addPeriodicTimeObserver(forInterval: time, queue: .main) { [weak self] time in -// print("Timer timed: \(time)") - - if time.seconds != 0 { - self?.sendProgressReport(seconds: time.seconds) - } - } - - self.player = player - - self.allowsPictureInPicturePlayback = true - self.player?.allowsExternalPlayback = true - } - - private func createMetadata() -> [AVMetadataItem] { - let allMetadata: [AVMetadataIdentifier: Any] = [ - .commonIdentifierTitle: viewModel.title, - .iTunesMetadataTrackSubTitle: viewModel.subtitle ?? "", - .commonIdentifierArtwork: UIImage(data: try! Data(contentsOf: viewModel.item.getBackdropImage(maxWidth: 200)))?.pngData() as Any, - .commonIdentifierDescription: viewModel.item.overview ?? "" - ] - - return allMetadata.compactMap { createMetadataItem(for:$0, value:$1) } - } - - private func createMetadataItem(for identifier: AVMetadataIdentifier, - value: Any) -> AVMetadataItem { - let item = AVMutableMetadataItem() - item.identifier = identifier - item.value = value as? NSCopying & NSObjectProtocol - // Specify "und" to indicate an undefined language. - item.extendedLanguageTag = "und" - return item.copy() as! AVMetadataItem - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - - stop() - removePeriodicTimeObserver() - } - - func removePeriodicTimeObserver() { - if let timeObserverToken = timeObserverToken { - player?.removeTimeObserver(timeObserverToken) - self.timeObserverToken = nil - } - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - player?.seek(to: CMTimeMake(value: viewModel.item.userData?.playbackPositionTicks ?? 0, timescale: 10_000_000), toleranceBefore: CMTimeMake(value: 5, timescale: 1), toleranceAfter: CMTimeMake(value: 5, timescale: 1), completionHandler: { _ in - self.play() - }) - } - - private func play() { - player?.play() - viewModel.sendPlayReport() - } - - private func sendProgressReport(seconds: Double) { - viewModel.sendProgressReport() - } - - private func stop() { - viewModel.sendStopReport() - } -} diff --git a/JellyfinPlayer/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift b/JellyfinPlayer/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift deleted file mode 100644 index a57f3bf6..00000000 --- a/JellyfinPlayer/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift +++ /dev/null @@ -1,251 +0,0 @@ -// -// VLCPlayerOverlayView.swift -// JellyfinVideoPlayerDev -// -// Created by Ethan Pippin on 11/24/21. -// - -import Combine -import MobileVLCKit -import SwiftUI -import JellyfinAPI - - - -struct VLCPlayerOverlayView: View { - - @ObservedObject var viewModel: VideoPlayerViewModel - - @ViewBuilder - private var mainButtonView: some View { - switch viewModel.playerState { - case .stopped, .paused: - Image(systemName: "play") - .font(.system(size: 56)) - case .playing: - Image(systemName: "pause") - .font(.system(size: 56)) - default: - ProgressView() - } - } - - @ViewBuilder - private var mainBody: some View { - VStack { - - VStack(alignment: .EpisodeSeriesAlignmentGuide) { - - // MARK: Top Bar - HStack(alignment: .top) { - - VStack(alignment: .leading) { - HStack { - Button { - viewModel.playerOverlayDelegate?.didSelectClose() - } label: { - Image(systemName: "chevron.backward") - } - - Text(viewModel.title) - .font(.system(size: 28, weight: .regular, design: .default)) - .alignmentGuide(.EpisodeSeriesAlignmentGuide) { context in - context[.leading] - } - } - } - - Spacer() - - HStack(spacing: 20) { - - if viewModel.shouldShowGoogleCast { - Button { - viewModel.playerOverlayDelegate?.didSelectGoogleCast() - } label: { - Image(systemName: "rectangle.badge.plus") - } - } - - if viewModel.shouldShowAirplay { - Button { - viewModel.playerOverlayDelegate?.didSelectAirplay() - } label: { - Image(systemName: "airplayvideo") - } - } - - Button { - viewModel.playerOverlayDelegate?.didSelectSubtitles() - } label: { - if viewModel.subtitlesEnabled { - Image(systemName: "captions.bubble.fill") - } else { - Image(systemName: "captions.bubble") - } - } - - // MARK: Settings Menu - Menu { - - Menu { - ForEach(viewModel.audioStreams, id: \.self) { audioStream in - Button { - viewModel.selectedAudioStreamIndex = audioStream.index ?? -1 - } label: { - if audioStream.index == viewModel.selectedAudioStreamIndex { - Label.init(audioStream.displayTitle ?? "No Title", systemImage: "checkmark") - } else { - Text(audioStream.displayTitle ?? "No Title") - } - } - } - } label: { - HStack { - Image(systemName: "speaker.wave.3") - Text("Audio") - } - } - - Menu { - ForEach(viewModel.subtitleStreams, id: \.self) { subtitleStream in - Button { - viewModel.selectedSubtitleStreamIndex = subtitleStream.index ?? -1 - } label: { - if subtitleStream.index == viewModel.selectedSubtitleStreamIndex { - Label.init(subtitleStream.displayTitle ?? "No Title", systemImage: "checkmark") - } else { - Text(subtitleStream.displayTitle ?? "No Title") - } - } - } - } label: { - HStack { - Image(systemName: "captions.bubble") - Text("Subtitles") - } - } - - Menu { - Button { - print("third pressed") - } label: { - Text("TODO") - } - } label: { - HStack { - Image(systemName: "speedometer") - Text("Playback Speed") - } - } - - } label: { - Image(systemName: "ellipsis.circle") - } - } - } - .font(.system(size: 24)) - - if let seriesTitle = viewModel.subtitle { - Text(seriesTitle) - .font(.subheadline) - .foregroundColor(Color.gray) - .alignmentGuide(.EpisodeSeriesAlignmentGuide) { context in - context[.leading] - } - .offset(y: -10) - } - } - - Spacer() - - // MARK: Center Buttons - HStack(spacing: 80) { - Button { - viewModel.playerOverlayDelegate?.didSelectBackward() - } label: { - Image(systemName: "gobackward.10") - } - - Button { - viewModel.playerOverlayDelegate?.didSelectMain() - } label: { - mainButtonView - } - - Button { - viewModel.playerOverlayDelegate?.didSelectForward() - } label: { - Image(systemName: "goforward.10") - } - } - .font(.system(size: 48)) - - Spacer() - - // MARK: Bottom Bar - HStack { - Text(viewModel.leftLabelText) - .font(.system(size: 18, weight: .semibold, design: .default)) - - Slider(value: $viewModel.sliderPercentage) { editing in - viewModel.sliderIsScrubbing = editing - } - .foregroundColor(.purple) - .tint(.purple) - - Text(viewModel.rightLabelText) - .font(.system(size: 18, weight: .semibold, design: .default)) - } - .frame(height: 50) - } - .padding(.top) - .ignoresSafeArea(edges: .vertical) - .tint(Color.white) - .foregroundColor(Color.white) - } - - var body: some View { - mainBody - .background { - Color(uiColor: .black.withAlphaComponent(0.2)) - .ignoresSafeArea() - .onTapGesture { - viewModel.playerOverlayDelegate?.didGenerallyTap() - } - } - } -} - -struct VLCPlayerOverlayView_Previews: PreviewProvider { - static var previews: some View { - ZStack { - Color.gray - .ignoresSafeArea() - - VLCPlayerOverlayView(viewModel: VideoPlayerViewModel(item: BaseItemDto(), - title: "Glorious Purpose", - subtitle: "Loki - S1E1", - streamURL: URL(string: "www.apple.com")!, - hlsURL: URL(string: "www.apple.com")!, - response: PlaybackInfoResponse(), - audioStreams: [MediaStream(displayTitle: "English", index: -1)], - subtitleStreams: [MediaStream(displayTitle: "None", index: -1)], - defaultAudioStreamIndex: -1, - defaultSubtitleStreamIndex: -1, - playerState: .playing, - shouldShowGoogleCast: false, - shouldShowAirplay: false, - subtitlesEnabled: true, - sliderPercentage: 0.0, - selectedAudioStreamIndex: -1, - selectedSubtitleStreamIndex: -1, - showAdjacentItems: true, - shouldShowAutoPlayNextItem: true, - autoPlayNextItem: true)) - } - .previewInterfaceOrientation(.landscapeLeft) - } -} - - diff --git a/JellyfinPlayer/Views/VideoPlayer/Overlays/VideoPlayerOverlay.swift b/JellyfinPlayer/Views/VideoPlayer/Overlays/VideoPlayerOverlay.swift deleted file mode 100644 index 720266c7..00000000 --- a/JellyfinPlayer/Views/VideoPlayer/Overlays/VideoPlayerOverlay.swift +++ /dev/null @@ -1,26 +0,0 @@ -// - /* - * 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 - -protocol VideoPlayerOverlay: View { - var viewModel: VideoPlayerViewModel { get set } -} - -extension HorizontalAlignment { - - private struct EpisodeSeriesTitleAlignment: AlignmentID { - static func defaultValue(in context: ViewDimensions) -> CGFloat { - context[HorizontalAlignment.leading] - } - } - - static let EpisodeSeriesAlignmentGuide = HorizontalAlignment(EpisodeSeriesTitleAlignment.self) - -} diff --git a/JellyfinPlayer/Views/VideoPlayer/PlaybackSpeed.swift b/JellyfinPlayer/Views/VideoPlayer/PlaybackSpeed.swift index 78b410f1..90806983 100644 --- a/JellyfinPlayer/Views/VideoPlayer/PlaybackSpeed.swift +++ b/JellyfinPlayer/Views/VideoPlayer/PlaybackSpeed.swift @@ -1,9 +1,11 @@ // -// PlaybackSpeed.swift -// JellyfinVideoPlayerDev -// -// Created by Ethan Pippin on 12/27/21. -// + /* + * 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 Foundation diff --git a/JellyfinPlayer/Views/VideoPlayer/PlayerOverlayDelegate.swift b/JellyfinPlayer/Views/VideoPlayer/PlayerOverlayDelegate.swift index f5affe9d..f80db501 100644 --- a/JellyfinPlayer/Views/VideoPlayer/PlayerOverlayDelegate.swift +++ b/JellyfinPlayer/Views/VideoPlayer/PlayerOverlayDelegate.swift @@ -1,18 +1,17 @@ // -// PlayerOverlayDelegate.swift -// JellyfinVideoPlayerDev -// -// Created by Ethan Pippin on 12/27/21. -// + /* + * 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 Foundation protocol PlayerOverlayDelegate { func didSelectClose() - func didSelectGoogleCast() - func didSelectAirplay() - func didSelectSubtitles() func didSelectMenu() func didDeselectMenu() @@ -28,6 +27,6 @@ protocol PlayerOverlayDelegate { func didSelectAudioStream(index: Int) func didSelectSubtitleStream(index: Int) - func didSelectPreviousItem() - func didSelectNextItem() + func didSelectPlayPreviousItem() + func didSelectPlayNextItem() } diff --git a/JellyfinPlayer/Views/VideoPlayer/Overlays/VLCPlayerCompactOverlayView.swift b/JellyfinPlayer/Views/VideoPlayer/VLCPlayerOverlayView.swift similarity index 78% rename from JellyfinPlayer/Views/VideoPlayer/Overlays/VLCPlayerCompactOverlayView.swift rename to JellyfinPlayer/Views/VideoPlayer/VLCPlayerOverlayView.swift index 9a9ceda0..b578ecf1 100644 --- a/JellyfinPlayer/Views/VideoPlayer/Overlays/VLCPlayerCompactOverlayView.swift +++ b/JellyfinPlayer/Views/VideoPlayer/VLCPlayerOverlayView.swift @@ -1,9 +1,10 @@ -// -// VLCPlayerCompactOverlayView.swift -// JellyfinVideoPlayerDev -// -// Created by Ethan Pippin on 12/26/21. -// +/* + * 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 Combine import Defaults @@ -12,11 +13,9 @@ import MobileVLCKit import Sliders import SwiftUI -struct VLCPlayerCompactOverlayView: View, VideoPlayerOverlay { +struct VLCPlayerOverlayView: View { @ObservedObject var viewModel: VideoPlayerViewModel - @Default(.videoPlayerJumpForward) var jumpForwardLength - @Default(.videoPlayerJumpBackward) var jumpBackwardLength @ViewBuilder private var mainButtonView: some View { @@ -69,33 +68,19 @@ struct VLCPlayerCompactOverlayView: View, VideoPlayerOverlay { HStack(spacing: 20) { - if viewModel.shouldShowGoogleCast { + if viewModel.shouldShowPlayPreviousItem { Button { - viewModel.playerOverlayDelegate?.didSelectGoogleCast() - } label: { - Image(systemName: "rectangle.badge.plus") - } - } - - if viewModel.shouldShowAirplay { - Button { - viewModel.playerOverlayDelegate?.didSelectAirplay() - } label: { - Image(systemName: "airplayvideo") - } - } - - if viewModel.showAdjacentItems { - Button { - viewModel.playerOverlayDelegate?.didSelectPreviousItem() + viewModel.playerOverlayDelegate?.didSelectPlayPreviousItem() } label: { Image(systemName: "chevron.left.circle") } .disabled(viewModel.previousItemVideoPlayerViewModel == nil) .foregroundColor(viewModel.nextItemVideoPlayerViewModel == nil ? .gray : .white) - + } + + if viewModel.shouldShowPlayNextItem { Button { - viewModel.playerOverlayDelegate?.didSelectNextItem() + viewModel.playerOverlayDelegate?.didSelectPlayNextItem() } label: { Image(systemName: "chevron.right.circle") } @@ -105,9 +90,9 @@ struct VLCPlayerCompactOverlayView: View, VideoPlayerOverlay { if viewModel.shouldShowAutoPlayNextItem { Button { - viewModel.autoPlayNextItem.toggle() + viewModel.autoplayEnabled.toggle() } label: { - if viewModel.autoPlayNextItem { + if viewModel.autoplayEnabled { Image(systemName: "play.circle.fill") } else { Image(systemName: "play.circle") @@ -117,7 +102,7 @@ struct VLCPlayerCompactOverlayView: View, VideoPlayerOverlay { if !viewModel.subtitleStreams.isEmpty { Button { - viewModel.playerOverlayDelegate?.didSelectSubtitles() + viewModel.subtitlesEnabled.toggle() } label: { if viewModel.subtitlesEnabled { Image(systemName: "captions.bubble.fill") @@ -192,9 +177,9 @@ struct VLCPlayerCompactOverlayView: View, VideoPlayerOverlay { Menu { ForEach(VideoPlayerJumpLength.allCases, id: \.self) { forwardLength in Button { - jumpForwardLength = forwardLength + viewModel.jumpForwardLength = forwardLength } label: { - if forwardLength == jumpForwardLength { + if forwardLength == viewModel.jumpForwardLength { Label(forwardLength.shortLabel, systemImage: "checkmark") } else { Text(forwardLength.shortLabel) @@ -211,9 +196,9 @@ struct VLCPlayerCompactOverlayView: View, VideoPlayerOverlay { Menu { ForEach(VideoPlayerJumpLength.allCases, id: \.self) { backwardLength in Button { - jumpBackwardLength = backwardLength + viewModel.jumpBackwardLength = backwardLength } label: { - if backwardLength == jumpBackwardLength { + if backwardLength == viewModel.jumpBackwardLength { Label(backwardLength.shortLabel, systemImage: "checkmark") } else { Text(backwardLength.shortLabel) @@ -247,6 +232,11 @@ struct VLCPlayerCompactOverlayView: View, VideoPlayerOverlay { } } + // MARK: Center + + Spacer() + + Spacer() // MARK: Bottom Bar @@ -264,7 +254,7 @@ struct VLCPlayerCompactOverlayView: View, VideoPlayerOverlay { Button { viewModel.playerOverlayDelegate?.didSelectBackward() } label: { - Image(systemName: jumpBackwardLength.backwardImageLabel) + Image(systemName: viewModel.jumpBackwardLength.backwardImageLabel) .padding(.horizontal, 5) } @@ -279,12 +269,11 @@ struct VLCPlayerCompactOverlayView: View, VideoPlayerOverlay { Button { viewModel.playerOverlayDelegate?.didSelectForward() } label: { - Image(systemName: jumpForwardLength.forwardImageLabel) + Image(systemName: viewModel.jumpForwardLength.forwardImageLabel) .padding(.horizontal, 5) } } .font(.system(size: 24, weight: .semibold, design: .default)) -// .padding(.trailing, 10) Text(viewModel.leftLabelText) .font(.system(size: 18, weight: .semibold, design: .default)) @@ -332,32 +321,43 @@ struct VLCPlayerCompactOverlayView: View, VideoPlayerOverlay { } struct VLCPlayerCompactOverlayView_Previews: PreviewProvider { + + static let videoPlayerViewModel = VideoPlayerViewModel(item: BaseItemDto(), + title: "Glorious Purpose", + subtitle: "Loki - S1E1", + streamURL: URL(string: "www.apple.com")!, + hlsURL: URL(string: "www.apple.com")!, + response: PlaybackInfoResponse(), + audioStreams: [MediaStream(displayTitle: "English", index: -1)], + subtitleStreams: [MediaStream(displayTitle: "None", index: -1)], + selectedAudioStreamIndex: -1, + selectedSubtitleStreamIndex: -1, + subtitlesEnabled: true, + autoplayEnabled: false, + overlayType: .compact, + shouldShowPlayPreviousItem: true, + shouldShowPlayNextItem: true, + shouldShowAutoPlayNextItem: true) + static var previews: some View { ZStack { Color.red .ignoresSafeArea() - VLCPlayerCompactOverlayView(viewModel: VideoPlayerViewModel(item: BaseItemDto(runTimeTicks: 720 * 10_000_000), - title: "Glorious Purpose", - subtitle: "Loki - S1E1", - streamURL: URL(string: "www.apple.com")!, - hlsURL: URL(string: "www.apple.com")!, - response: PlaybackInfoResponse(), - audioStreams: [MediaStream(displayTitle: "English", index: -1)], - subtitleStreams: [MediaStream(displayTitle: "None", index: -1)], - defaultAudioStreamIndex: -1, - defaultSubtitleStreamIndex: -1, - playerState: .playing, - shouldShowGoogleCast: false, - shouldShowAirplay: false, - subtitlesEnabled: true, - sliderPercentage: 0.432, - selectedAudioStreamIndex: -1, - selectedSubtitleStreamIndex: -1, - showAdjacentItems: true, - shouldShowAutoPlayNextItem: true, - autoPlayNextItem: true)) + VLCPlayerOverlayView(viewModel: videoPlayerViewModel) } .previewInterfaceOrientation(.landscapeLeft) } } + +// MARK: TitleSubtitleAlignment +extension HorizontalAlignment { + + private struct TitleSubtitleAlignment: AlignmentID { + static func defaultValue(in context: ViewDimensions) -> CGFloat { + context[HorizontalAlignment.leading] + } + } + + static let EpisodeSeriesAlignmentGuide = HorizontalAlignment(TitleSubtitleAlignment.self) +} diff --git a/JellyfinPlayer/Views/VideoPlayer/VLCPlayerView.swift b/JellyfinPlayer/Views/VideoPlayer/VLCPlayerView.swift new file mode 100644 index 00000000..099d0838 --- /dev/null +++ b/JellyfinPlayer/Views/VideoPlayer/VLCPlayerView.swift @@ -0,0 +1,27 @@ +// + /* + * 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 UIKit +import SwiftUI + +struct VLCPlayerView: UIViewControllerRepresentable { + + let viewModel: VideoPlayerViewModel + + typealias UIViewControllerType = VLCPlayerViewController + + func makeUIViewController(context: Context) -> VLCPlayerViewController { + + return VLCPlayerViewController(viewModel: viewModel) + } + + func updateUIViewController(_ uiViewController: VLCPlayerViewController, context: Context) { + + } +} diff --git a/JellyfinPlayer/Views/VideoPlayer/VLCPlayerViewController.swift b/JellyfinPlayer/Views/VideoPlayer/VLCPlayerViewController.swift index 9b38fa2f..490e242e 100644 --- a/JellyfinPlayer/Views/VideoPlayer/VLCPlayerViewController.swift +++ b/JellyfinPlayer/Views/VideoPlayer/VLCPlayerViewController.swift @@ -1,9 +1,11 @@ // -// PlayerViewController.swift -// JellyfinVideoPlayerDev -// -// Created by Ethan Pippin on 11/12/21. -// + /* + * 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 AVKit import AVFoundation @@ -25,7 +27,7 @@ class VLCPlayerViewController: UIViewController { private var vlcMediaPlayer = VLCMediaPlayer() private var lastPlayerTicks: Int64 = 0 private var lastProgressReportTicks: Int64 = 0 - private var viewModelReactCancellables = Set() + private var viewModelListeners = Set() private var overlayDismissTimer: Timer? private var currentPlayerTicks: Int64 { @@ -36,19 +38,11 @@ class VLCPlayerViewController: UIViewController { return currentOverlayHostingController?.view.alpha ?? 0 > 0 } - private var jumpForwardLength: VideoPlayerJumpLength { - return Defaults[.videoPlayerJumpForward] - } - - private var jumpBackwardLength: VideoPlayerJumpLength { - return Defaults[.videoPlayerJumpBackward] - } - private lazy var videoContentView = makeVideoContentView() - private lazy var jumpBackwardOverlayView = makeJumpBackwardOverlayView() - private lazy var jumpForwardOverlayView = makeJumpForwardOverlayView() - private lazy var tapGestureView = makeTapGestureView() - private var currentOverlayHostingController: UIHostingController? + private lazy var mainGestureView = makeTapGestureView() + private var currentOverlayHostingController: UIHostingController? + private var currentJumpBackwardOverlayView: UIImageView? + private var currentJumpForwardOverlayView: UIImageView? // MARK: init @@ -67,12 +61,7 @@ class VLCPlayerViewController: UIViewController { private func setupSubviews() { view.addSubview(videoContentView) - view.addSubview(jumpForwardOverlayView) - view.addSubview(jumpBackwardOverlayView) - view.addSubview(tapGestureView) - - jumpBackwardOverlayView.alpha = 0 - jumpForwardOverlayView.alpha = 0 + view.addSubview(mainGestureView) } private func setupConstraints() { @@ -83,18 +72,10 @@ class VLCPlayerViewController: UIViewController { videoContentView.rightAnchor.constraint(equalTo: view.rightAnchor) ]) NSLayoutConstraint.activate([ - jumpBackwardOverlayView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 150), - jumpBackwardOverlayView.centerYAnchor.constraint(equalTo: view.centerYAnchor) - ]) - NSLayoutConstraint.activate([ - jumpForwardOverlayView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -150), - jumpForwardOverlayView.centerYAnchor.constraint(equalTo: view.centerYAnchor) - ]) - NSLayoutConstraint.activate([ - tapGestureView.topAnchor.constraint(equalTo: videoContentView.topAnchor), - tapGestureView.bottomAnchor.constraint(equalTo: videoContentView.bottomAnchor), - tapGestureView.leftAnchor.constraint(equalTo: videoContentView.leftAnchor), - tapGestureView.rightAnchor.constraint(equalTo: videoContentView.rightAnchor) + mainGestureView.topAnchor.constraint(equalTo: videoContentView.topAnchor), + mainGestureView.bottomAnchor.constraint(equalTo: videoContentView.bottomAnchor), + mainGestureView.leftAnchor.constraint(equalTo: videoContentView.leftAnchor), + mainGestureView.rightAnchor.constraint(equalTo: videoContentView.rightAnchor) ]) } @@ -127,6 +108,9 @@ class VLCPlayerViewController: UIViewController { setupMediaPlayer(newViewModel: viewModel) + refreshJumpBackwardOverlayView(with: viewModel.jumpBackwardLength) + refreshJumpForwardOverlayView(with: viewModel.jumpForwardLength) + let defaultNotificationCenter = NotificationCenter.default defaultNotificationCenter.addObserver(self, selector: #selector(appWillTerminate), name: UIApplication.willTerminateNotification, object: nil) defaultNotificationCenter.addObserver(self, selector: #selector(appWillResignActive), name: UIApplication.willResignActiveNotification, object: nil) @@ -192,26 +176,6 @@ class VLCPlayerViewController: UIViewController { self.didSelectBackward() } - private func makeJumpBackwardOverlayView() -> UIImageView { - let symbolConfig = UIImage.SymbolConfiguration(pointSize: 48) - let forwardSymbolImage = UIImage(systemName: jumpBackwardLength.backwardImageLabel, withConfiguration: symbolConfig) - let imageView = UIImageView(image: forwardSymbolImage) - imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.tintColor = .white - - return imageView - } - - private func makeJumpForwardOverlayView() -> UIImageView { - let symbolConfig = UIImage.SymbolConfiguration(pointSize: 48) - let forwardSymbolImage = UIImage(systemName: jumpForwardLength.forwardImageLabel, withConfiguration: symbolConfig) - let imageView = UIImageView(image: forwardSymbolImage) - imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.tintColor = .white - - return imageView - } - // MARK: setupOverlayHostingController private func setupOverlayHostingController(viewModel: VideoPlayerViewModel) { @@ -225,11 +189,10 @@ class VLCPlayerViewController: UIViewController { currentOverlayHostingController.view.removeFromSuperview() currentOverlayHostingController.removeFromParent() -// self.currentOverlayHostingController = nil } } - let newOverlayView = VLCPlayerCompactOverlayView(viewModel: viewModel) + let newOverlayView = VLCPlayerOverlayView(viewModel: viewModel) let newOverlayHostingController = UIHostingController(rootView: newOverlayView) newOverlayHostingController.view.translatesAutoresizingMaskIntoConstraints = false @@ -256,10 +219,59 @@ class VLCPlayerViewController: UIViewController { self.currentOverlayHostingController = newOverlayHostingController - // There is a behavior when setting this that the navigation bar - // on the current navigation controller pops up, re-hide it + // There is a weird behavior when after setting the new overlays that the navigation bar pops up, re-hide it self.navigationController?.isNavigationBarHidden = true } + + private func refreshJumpBackwardOverlayView(with jumpBackwardLength: VideoPlayerJumpLength) { + + if let currentJumpBackwardOverlayView = currentJumpBackwardOverlayView { + currentJumpBackwardOverlayView.removeFromSuperview() + } + + let symbolConfig = UIImage.SymbolConfiguration(pointSize: 48) + let backwardSymbolImage = UIImage(systemName: jumpBackwardLength.backwardImageLabel, withConfiguration: symbolConfig) + let newJumpBackwardImageView = UIImageView(image: backwardSymbolImage) + + newJumpBackwardImageView.translatesAutoresizingMaskIntoConstraints = false + newJumpBackwardImageView.tintColor = .white + + newJumpBackwardImageView.alpha = 0 + + view.addSubview(newJumpBackwardImageView) + + NSLayoutConstraint.activate([ + newJumpBackwardImageView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 150), + newJumpBackwardImageView.centerYAnchor.constraint(equalTo: view.centerYAnchor) + ]) + + currentJumpBackwardOverlayView = newJumpBackwardImageView + } + + private func refreshJumpForwardOverlayView(with jumpForwardLength: VideoPlayerJumpLength) { + + if let currentJumpForwardOverlayView = currentJumpForwardOverlayView { + currentJumpForwardOverlayView.removeFromSuperview() + } + + let symbolConfig = UIImage.SymbolConfiguration(pointSize: 48) + let forwardSymbolImage = UIImage(systemName: jumpForwardLength.forwardImageLabel, withConfiguration: symbolConfig) + let newJumpForwardImageView = UIImageView(image: forwardSymbolImage) + + newJumpForwardImageView.translatesAutoresizingMaskIntoConstraints = false + newJumpForwardImageView.tintColor = .white + + newJumpForwardImageView.alpha = 0 + + view.addSubview(newJumpForwardImageView) + + NSLayoutConstraint.activate([ + newJumpForwardImageView.leftAnchor.constraint(equalTo: view.rightAnchor, constant: -150), + newJumpForwardImageView.centerYAnchor.constraint(equalTo: view.centerYAnchor) + ]) + + currentJumpForwardOverlayView = newJumpForwardImageView + } } // MARK: setupMediaPlayer @@ -275,7 +287,7 @@ extension VLCPlayerViewController { // Stop current media if there is one if vlcMediaPlayer.media != nil { - viewModelReactCancellables.forEach({ $0.cancel() }) + viewModelListeners.forEach({ $0.cancel() }) vlcMediaPlayer.stop() viewModel.sendStopReport() @@ -297,7 +309,7 @@ extension VLCPlayerViewController { newViewModel.getAdjacentEpisodes() newViewModel.playerOverlayDelegate = self - let startPercentage = viewModel.item.userData?.playedPercentage ?? 0 + let startPercentage = newViewModel.item.userData?.playedPercentage ?? 0 if startPercentage > 0 { newViewModel.sliderPercentage = startPercentage / 100 @@ -320,9 +332,10 @@ extension VLCPlayerViewController { // MARK: setupViewModelListeners private func setupViewModelListeners(viewModel: VideoPlayerViewModel) { + viewModel.$playbackSpeed.sink { newSpeed in self.vlcMediaPlayer.rate = Float(newSpeed.rawValue) - }.store(in: &viewModelReactCancellables) + }.store(in: &viewModelListeners) viewModel.$sliderIsScrubbing.sink { sliderIsScrubbing in if sliderIsScrubbing { @@ -330,15 +343,27 @@ extension VLCPlayerViewController { } else { self.didEndScrubbing() } - }.store(in: &viewModelReactCancellables) + }.store(in: &viewModelListeners) viewModel.$selectedAudioStreamIndex.sink { newAudioStreamIndex in self.didSelectAudioStream(index: newAudioStreamIndex) - }.store(in: &viewModelReactCancellables) + }.store(in: &viewModelListeners) viewModel.$selectedSubtitleStreamIndex.sink { newSubtitleStreamIndex in self.didSelectSubtitleStream(index: newSubtitleStreamIndex) - }.store(in: &viewModelReactCancellables) + }.store(in: &viewModelListeners) + + viewModel.$subtitlesEnabled.sink { newSubtitlesEnabled in + self.didToggleSubtitles(newValue: newSubtitlesEnabled) + }.store(in: &viewModelListeners) + + viewModel.$jumpBackwardLength.sink { newJumpBackwardLength in + self.refreshJumpBackwardOverlayView(with: newJumpBackwardLength) + }.store(in: &viewModelListeners) + + viewModel.$jumpForwardLength.sink { newJumpForwardLength in + self.refreshJumpForwardOverlayView(with: newJumpForwardLength) + }.store(in: &viewModelListeners) } func setMediaPlayerTimeAtCurrentSlider() { @@ -395,34 +420,42 @@ extension VLCPlayerViewController { extension VLCPlayerViewController { private func flashJumpBackwardOverlay() { - jumpBackwardOverlayView.layer.removeAllAnimations() + guard let currentJumpBackwardOverlayView = currentJumpBackwardOverlayView else { return } + + currentJumpBackwardOverlayView.layer.removeAllAnimations() UIView.animate(withDuration: 0.1) { - self.jumpBackwardOverlayView.alpha = 1 + currentJumpBackwardOverlayView.alpha = 1 } completion: { _ in self.hideJumpBackwardOverlay() } } private func hideJumpBackwardOverlay() { + guard let currentJumpBackwardOverlayView = currentJumpBackwardOverlayView else { return } + UIView.animate(withDuration: 0.3) { - self.jumpBackwardOverlayView.alpha = 0 + currentJumpBackwardOverlayView.alpha = 0 } } private func flashJumpFowardOverlay() { - jumpForwardOverlayView.layer.removeAllAnimations() + guard let currentJumpForwardOverlayView = currentJumpForwardOverlayView else { return } + + currentJumpForwardOverlayView.layer.removeAllAnimations() UIView.animate(withDuration: 0.1) { - self.jumpForwardOverlayView.alpha = 1 + currentJumpForwardOverlayView.alpha = 1 } completion: { _ in self.hideJumpForwardOverlay() } } private func hideJumpForwardOverlay() { + guard let currentJumpForwardOverlayView = currentJumpForwardOverlayView else { return } + UIView.animate(withDuration: 0.3) { - self.jumpForwardOverlayView.alpha = 0 + currentJumpForwardOverlayView.alpha = 0 } } } @@ -459,8 +492,8 @@ extension VLCPlayerViewController: VLCMediaPlayerDelegate { viewModel.playerState = vlcMediaPlayer.state if vlcMediaPlayer.state == VLCMediaPlayerState.ended { - if viewModel.autoPlayNextItem && viewModel.shouldShowAutoPlayNextItem && viewModel.nextItemVideoPlayerViewModel != nil { - didSelectNextItem() + if viewModel.autoplayEnabled && viewModel.nextItemVideoPlayerViewModel != nil { + didSelectPlayNextItem() } else { didSelectClose() } @@ -470,13 +503,10 @@ extension VLCPlayerViewController: VLCMediaPlayerDelegate { // MARK: mediaPlayerTimeChanged func mediaPlayerTimeChanged(_ aNotification: Notification!) { - guard !viewModel.sliderIsScrubbing else { - lastPlayerTicks = currentPlayerTicks - return + if !viewModel.sliderIsScrubbing { + viewModel.sliderPercentage = Double(vlcMediaPlayer.position) } - viewModel.sliderPercentage = Double(vlcMediaPlayer.position) - // Have to manually set playing because VLCMediaPlayer doesn't // properly set it itself if abs(currentPlayerTicks - lastPlayerTicks) >= 10_000 { @@ -486,6 +516,9 @@ extension VLCPlayerViewController: VLCMediaPlayerDelegate { // If needing to fix subtitle streams during playback if vlcMediaPlayer.currentVideoSubTitleIndex != viewModel.selectedSubtitleStreamIndex && viewModel.subtitlesEnabled { didSelectSubtitleStream(index: viewModel.selectedSubtitleStreamIndex) + } + + if vlcMediaPlayer.currentAudioTrackIndex != viewModel.selectedAudioStreamIndex { didSelectAudioStream(index: viewModel.selectedAudioStreamIndex) } @@ -500,7 +533,7 @@ extension VLCPlayerViewController: VLCMediaPlayerDelegate { } } -// MARK: PlayerOverlayDelegate +// MARK: PlayerOverlayDelegate and more extension VLCPlayerViewController: PlayerOverlayDelegate { func didSelectAudioStream(index: Int) { @@ -511,12 +544,11 @@ extension VLCPlayerViewController: PlayerOverlayDelegate { lastProgressReportTicks = currentPlayerTicks } + /// Do not call when setting to index -1 func didSelectSubtitleStream(index: Int) { - if viewModel.subtitlesEnabled { - vlcMediaPlayer.currentVideoSubTitleIndex = Int32(index) - } else { - vlcMediaPlayer.currentVideoSubTitleIndex = -1 - } + + viewModel.subtitlesEnabled = true + vlcMediaPlayer.currentVideoSubTitleIndex = Int32(index) viewModel.sendProgressReport() @@ -531,19 +563,8 @@ extension VLCPlayerViewController: PlayerOverlayDelegate { dismiss(animated: true, completion: nil) } - func didSelectGoogleCast() { - print("didSelectCast") - } - - func didSelectAirplay() { - print("didSelectAirplay") - } - - func didSelectSubtitles() { - - viewModel.subtitlesEnabled = !viewModel.subtitlesEnabled - - if viewModel.subtitlesEnabled { + func didToggleSubtitles(newValue: Bool) { + if newValue { vlcMediaPlayer.currentVideoSubTitleIndex = Int32(viewModel.selectedSubtitleStreamIndex) } else { vlcMediaPlayer.currentVideoSubTitleIndex = -1 @@ -561,27 +582,33 @@ extension VLCPlayerViewController: PlayerOverlayDelegate { } func didSelectBackward() { + flashJumpBackwardOverlay() - vlcMediaPlayer.jumpBackward(jumpBackwardLength.rawValue) + vlcMediaPlayer.jumpBackward(viewModel.jumpBackwardLength.rawValue) - restartOverlayDismissTimer() + if displayingOverlay { + restartOverlayDismissTimer() + } viewModel.sendProgressReport() - self.lastProgressReportTicks = currentPlayerTicks + lastProgressReportTicks = currentPlayerTicks } func didSelectForward() { + flashJumpFowardOverlay() - vlcMediaPlayer.jumpForward(jumpForwardLength.rawValue) + vlcMediaPlayer.jumpForward(viewModel.jumpForwardLength.rawValue) - restartOverlayDismissTimer() + if displayingOverlay { + restartOverlayDismissTimer() + } viewModel.sendProgressReport() - self.lastProgressReportTicks = currentPlayerTicks + lastProgressReportTicks = currentPlayerTicks } func didSelectMain() { @@ -619,16 +646,20 @@ extension VLCPlayerViewController: PlayerOverlayDelegate { viewModel.sendProgressReport() - self.lastProgressReportTicks = currentPlayerTicks + lastProgressReportTicks = currentPlayerTicks } - func didSelectPreviousItem() { - setupMediaPlayer(newViewModel: viewModel.previousItemVideoPlayerViewModel!) - startPlayback() + func didSelectPlayPreviousItem() { + if let previousItemVideoPlayerViewModel = viewModel.previousItemVideoPlayerViewModel { + setupMediaPlayer(newViewModel: previousItemVideoPlayerViewModel) + startPlayback() + } } - func didSelectNextItem() { - setupMediaPlayer(newViewModel: viewModel.nextItemVideoPlayerViewModel!) - startPlayback() + func didSelectPlayNextItem() { + if let nextItemVideoPlayerViewModel = viewModel.nextItemVideoPlayerViewModel { + setupMediaPlayer(newViewModel: nextItemVideoPlayerViewModel) + startPlayback() + } } } diff --git a/JellyfinPlayer/Views/VideoPlayer/VideoPlayerView.swift b/JellyfinPlayer/Views/VideoPlayer/VideoPlayerView.swift deleted file mode 100644 index 8f9bf3e9..00000000 --- a/JellyfinPlayer/Views/VideoPlayer/VideoPlayerView.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// VideoPlayerView.swift -// JellyfinVideoPlayerDev -// -// Created by Ethan Pippin on 11/12/21. -// - -import UIKit -import SwiftUI - -struct NativePlayerView: UIViewControllerRepresentable { - - let viewModel: VideoPlayerViewModel - - typealias UIViewControllerType = NativePlayerViewController - - func makeUIViewController(context: Context) -> NativePlayerViewController { - - return NativePlayerViewController(viewModel: viewModel) - } - - func updateUIViewController(_ uiViewController: NativePlayerViewController, context: Context) { - - } -} - -struct VLCPlayerView: UIViewControllerRepresentable { - - let viewModel: VideoPlayerViewModel - - typealias UIViewControllerType = VLCPlayerViewController - - func makeUIViewController(context: Context) -> VLCPlayerViewController { - - return VLCPlayerViewController(viewModel: viewModel) - } - - func updateUIViewController(_ uiViewController: VLCPlayerViewController, context: Context) { - - } -} diff --git a/Shared/Coordinators/VideoPlayerCoordinator/iOSVideoPlayerCoordinator.swift b/Shared/Coordinators/VideoPlayerCoordinator/iOSVideoPlayerCoordinator.swift index f15a8b63..ea74d736 100644 --- a/Shared/Coordinators/VideoPlayerCoordinator/iOSVideoPlayerCoordinator.swift +++ b/Shared/Coordinators/VideoPlayerCoordinator/iOSVideoPlayerCoordinator.swift @@ -19,7 +19,6 @@ final class VideoPlayerCoordinator: NavigationCoordinatable { @Root var start = makeStart - @Default(.nativeVideoPlayer) var nativeVideoPlayer let viewModel: VideoPlayerViewModel init(viewModel: VideoPlayerViewModel) { @@ -27,24 +26,13 @@ final class VideoPlayerCoordinator: NavigationCoordinatable { } @ViewBuilder func makeStart() -> some View { - if nativeVideoPlayer { - PreferenceUIHostingControllerView { - NativePlayerView(viewModel: self.viewModel) - .navigationBarHidden(true) - .statusBar(hidden: true) - .ignoresSafeArea() - .prefersHomeIndicatorAutoHidden(true) - .supportedOrientations(.landscape) - }.ignoresSafeArea() - } else { - PreferenceUIHostingControllerView { - VLCPlayerView(viewModel: self.viewModel) - .navigationBarHidden(true) - .statusBar(hidden: true) - .ignoresSafeArea() - .prefersHomeIndicatorAutoHidden(true) - .supportedOrientations(.landscape) - }.ignoresSafeArea() - } + PreferenceUIHostingControllerView { + VLCPlayerView(viewModel: self.viewModel) + .navigationBarHidden(true) + .statusBar(hidden: true) + .ignoresSafeArea() + .prefersHomeIndicatorAutoHidden(true) + .supportedOrientations(.landscape) + }.ignoresSafeArea() } } diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift index 959a91bf..bbed415e 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift @@ -80,6 +80,8 @@ extension BaseItemDto { hlsURL.addQueryItem(name: "SubtitleStreamIndex", value: "\(defaultSubtitleStream!.index!)") } + // MARK: VidoPlayerViewModel Creation + var subtitle: String? = nil // TODO: other forms of media subtitle @@ -89,34 +91,32 @@ extension BaseItemDto { } } + let subtitlesEnabled = Defaults[.subtitlesEnabledIfDefault] && defaultSubtitleStream != nil - // MARK: VidoPlayerViewModel Creation + let shouldShowAutoPlay = Defaults[.shouldShowAutoPlay] && itemType == .episode + let autoplayEnabled = Defaults[.autoplayEnabled] && shouldShowAutoPlay - // TODO: show adjacent items + let overlayType = Defaults[.overlayType] - let shouldShowAutoPlayNextItem = Defaults[.shouldShowAutoPlayNextItem] && itemType == .episode - let autoPlayNextItem = Defaults[.autoPlayNextItem] + let shouldShowPlayPreviousItem = Defaults[.shouldShowPlayPreviousItem] && itemType == .episode + let shouldShowPlayNextItem = Defaults[.shouldShowPlayNextItem] && itemType == .episode let videoPlayerViewModel = VideoPlayerViewModel(item: self, - title: self.name!, + title: self.name ?? "", subtitle: subtitle, streamURL: streamURL.url!, hlsURL: hlsURL.url!, response: response, audioStreams: audioStreams, subtitleStreams: subtitleStreams, - defaultAudioStreamIndex: defaultAudioStream?.index ?? -1, - defaultSubtitleStreamIndex: defaultSubtitleStream?.index ?? -1, - playerState: .playing, - shouldShowGoogleCast: false, - shouldShowAirplay: false, - subtitlesEnabled: defaultSubtitleStream?.index != nil, - sliderPercentage: (self.userData?.playedPercentage ?? 0) / 100, selectedAudioStreamIndex: defaultAudioStream?.index ?? -1, selectedSubtitleStreamIndex: defaultSubtitleStream?.index ?? -1, - showAdjacentItems: true, - shouldShowAutoPlayNextItem: shouldShowAutoPlayNextItem, - autoPlayNextItem: autoPlayNextItem) + subtitlesEnabled: subtitlesEnabled, + autoplayEnabled: autoplayEnabled, + overlayType: overlayType, + shouldShowPlayPreviousItem: shouldShowPlayPreviousItem, + shouldShowPlayNextItem: shouldShowPlayNextItem, + shouldShowAutoPlayNextItem: shouldShowAutoPlay) return videoPlayerViewModel }) diff --git a/Shared/Objects/OverlaySliderColor.swift b/Shared/Objects/OverlaySliderColor.swift new file mode 100644 index 00000000..4844d851 --- /dev/null +++ b/Shared/Objects/OverlaySliderColor.swift @@ -0,0 +1,25 @@ +// + /* + * 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 Defaults +import UIKit + +enum OverlaySliderColor: String, CaseIterable, DefaultsSerializable { + case white + case jellyfinPurple + + var displayLabel: String { + switch self { + case .white: + return "White" + case .jellyfinPurple: + return "Jellyfin Purple" + } + } +} diff --git a/Shared/Objects/OverlayType.swift b/Shared/Objects/OverlayType.swift new file mode 100644 index 00000000..4e2cfe8f --- /dev/null +++ b/Shared/Objects/OverlayType.swift @@ -0,0 +1,17 @@ +// + /* + * 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 Defaults +import Foundation + +enum OverlayType: String, CaseIterable, Defaults.Serializable { + case normal + case compact + case bottom +} diff --git a/Shared/Singleton/SessionManager.swift b/Shared/Singleton/SessionManager.swift index d15ad30f..59c7cae2 100644 --- a/Shared/Singleton/SessionManager.swift +++ b/Shared/Singleton/SessionManager.swift @@ -31,7 +31,7 @@ final class SessionManager { // MARK: init private init() { - if let lastUserID = SwiftfinStore.Defaults.suite[.lastServerUserID], + if let lastUserID = Defaults[.lastServerUserID], let user = try? SwiftfinStore.dataStack.fetchOne(From(), [Where("id == %@", lastUserID)]) { @@ -64,7 +64,7 @@ final class SessionManager { var uriComponents = URLComponents(string: uri) ?? URLComponents() if uriComponents.scheme == nil { - uriComponents.scheme = SwiftfinStore.Defaults.suite[.defaultHTTPScheme].rawValue + uriComponents.scheme = Defaults[.defaultHTTPScheme].rawValue } var uri = uriComponents.string ?? "" @@ -216,7 +216,7 @@ final class SessionManager { let currentServer = SwiftfinStore.dataStack.fetchExisting(server)! let currentUser = SwiftfinStore.dataStack.fetchExisting(user)! - SwiftfinStore.Defaults.suite[.lastServerUserID] = user.id + Defaults[.lastServerUserID] = user.id currentLogin = (server: currentServer.state, user: currentUser.state) SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil) @@ -230,7 +230,7 @@ final class SessionManager { // MARK: loginUser func loginUser(server: SwiftfinStore.State.Server, user: SwiftfinStore.State.User) { JellyfinAPI.basePath = server.currentURI - SwiftfinStore.Defaults.suite[.lastServerUserID] = user.id + Defaults[.lastServerUserID] = user.id setAuthHeader(with: user.accessToken) currentLogin = (server: server, user: user) SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil) @@ -241,7 +241,7 @@ final class SessionManager { currentLogin = nil JellyfinAPI.basePath = "" setAuthHeader(with: "") - SwiftfinStore.Defaults.suite[.lastServerUserID] = nil + Defaults[.lastServerUserID] = nil SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil) } @@ -254,8 +254,8 @@ final class SessionManager { delete(server: server) } - // Delete UserDefaults - SwiftfinStore.Defaults.suite.removeAll() + // Delete general UserDefaults + SwiftfinStore.Defaults.generalSuite.removeAll() SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didPurge, object: nil) } diff --git a/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift b/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift index 118fa85f..25aefd4f 100644 --- a/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift +++ b/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift @@ -14,25 +14,44 @@ extension SwiftfinStore { enum Defaults { - static let suite: UserDefaults = { - return UserDefaults(suiteName: "swiftfinstore-defaults")! + static let generalSuite: UserDefaults = { + return UserDefaults(suiteName: "swiftfinstore-general-defaults")! + }() + + static let universalSuite: UserDefaults = { + return UserDefaults(suiteName: "swiftfinstore-universal-defaults")! }() } } extension Defaults.Keys { - static let lastServerUserID = Defaults.Key("lastServerUserID", suite: SwiftfinStore.Defaults.suite) - - static let defaultHTTPScheme = Key("defaultHTTPScheme", default: .http, suite: SwiftfinStore.Defaults.suite) - static let inNetworkBandwidth = Key("InNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.suite) - static let outOfNetworkBandwidth = Key("OutOfNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.suite) - static let isAutoSelectSubtitles = Key("isAutoSelectSubtitles", default: false, suite: SwiftfinStore.Defaults.suite) - static let autoSelectSubtitlesLangCode = Key("AutoSelectSubtitlesLangCode", default: "Auto", suite: SwiftfinStore.Defaults.suite) - static let autoSelectAudioLangCode = Key("AutoSelectAudioLangCode", default: "Auto", suite: SwiftfinStore.Defaults.suite) - static let appAppearance = Key("appAppearance", default: .system, suite: SwiftfinStore.Defaults.suite) - static let videoPlayerJumpForward = Key("videoPlayerJumpForward", default: .fifteen, suite: SwiftfinStore.Defaults.suite) - static let videoPlayerJumpBackward = Key("videoPlayerJumpBackward", default: .fifteen, suite: SwiftfinStore.Defaults.suite) - static let nativeVideoPlayer = Key("nativeVideoPlayer", default: false, suite: SwiftfinStore.Defaults.suite) - static let shouldShowAutoPlayNextItem = Key("shouldShowAutoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.suite) - static let autoPlayNextItem = Key("autoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.suite) + + // Universal settings + static let defaultHTTPScheme = Key("defaultHTTPScheme", default: .http, suite: SwiftfinStore.Defaults.universalSuite) + static let appAppearance = Key("appAppearance", default: .system, suite: SwiftfinStore.Defaults.universalSuite) + + // General settings + static let lastServerUserID = Defaults.Key("lastServerUserID", suite: SwiftfinStore.Defaults.generalSuite) + static let inNetworkBandwidth = Key("InNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.generalSuite) + static let outOfNetworkBandwidth = Key("OutOfNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.generalSuite) + static let isAutoSelectSubtitles = Key("isAutoSelectSubtitles", default: false, suite: SwiftfinStore.Defaults.generalSuite) + static let autoSelectSubtitlesLangCode = Key("AutoSelectSubtitlesLangCode", default: "Auto", suite: SwiftfinStore.Defaults.generalSuite) + static let autoSelectAudioLangCode = Key("AutoSelectAudioLangCode", default: "Auto", suite: SwiftfinStore.Defaults.generalSuite) + + static let overlayType = Key("overlayType", default: .normal, suite: SwiftfinStore.Defaults.generalSuite) + static let gesturesEnabled = Key("gesturesEnabled", default: true, suite: SwiftfinStore.Defaults.generalSuite) + static let videoPlayerJumpForward = Key("videoPlayerJumpForward", default: .fifteen, suite: SwiftfinStore.Defaults.generalSuite) + static let videoPlayerJumpBackward = Key("videoPlayerJumpBackward", default: .fifteen, suite: SwiftfinStore.Defaults.generalSuite) + static let subtitlesEnabledIfDefault = Key("subtitlesEnabledIfDefault", default: false, suite: SwiftfinStore.Defaults.generalSuite) + static let autoplayEnabled = Key("autoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite) + + // Should show video player items + static let shouldShowPlayPreviousItem = Key("shouldShowPreviousItem", default: true, suite: SwiftfinStore.Defaults.generalSuite) + static let shouldShowPlayNextItem = Key("shouldShowNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite) + static let shouldShowAutoPlay = Key("shouldShowAutoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite) + + // Experimental settings + struct Experimental { + static let syncSubtitleStateWithAdjacent = Key("experimental.syncSubtitleState", default: false, suite: SwiftfinStore.Defaults.generalSuite) + } } diff --git a/Shared/ViewModels/HomeViewModel.swift b/Shared/ViewModels/HomeViewModel.swift index 716dafea..6143c459 100644 --- a/Shared/ViewModels/HomeViewModel.swift +++ b/Shared/ViewModels/HomeViewModel.swift @@ -64,8 +64,9 @@ final class HomeViewModel: ViewModel { case .finished: () case .failure: self.libraries = [] - self.handleAPIRequestError(completion: completion) } + + self.handleAPIRequestError(completion: completion) }, receiveValue: { response in var newLibraries: [BaseItemDto] = [] diff --git a/Shared/ViewModels/SettingsViewModel.swift b/Shared/ViewModels/SettingsViewModel.swift index 5cedb82f..eef24a8d 100644 --- a/Shared/ViewModels/SettingsViewModel.swift +++ b/Shared/ViewModels/SettingsViewModel.swift @@ -12,13 +12,13 @@ import SwiftUI import Defaults final class SettingsViewModel: ObservableObject { - let currentLocale = Locale.current + var bitrates: [Bitrates] = [] - var langs = [TrackLanguage]() - let appearances = AppAppearance.allCases - let videoPlayerJumpLengths = VideoPlayerJumpLength.allCases + var langs: [TrackLanguage] = [] init() { + + // Bitrates let url = Bundle.main.url(forResource: "bitrates", withExtension: "json")! do { @@ -32,8 +32,9 @@ final class SettingsViewModel: ObservableObject { LogManager.shared.log.error("Error processing JSON file `bitrates.json`") } + // Track languages self.langs = Locale.isoLanguageCodes.compactMap { - guard let name = currentLocale.localizedString(forLanguageCode: $0) else { return nil } + guard let name = Locale.current.localizedString(forLanguageCode: $0) else { return nil } return TrackLanguage(name: name, isoCode: $0) }.sorted(by: { $0.name < $1.name }) self.langs.insert(.auto, at: 0) diff --git a/Shared/ViewModels/VideoPlayerViewModel.swift b/Shared/ViewModels/VideoPlayerViewModel.swift index a1eeab45..79ec3195 100644 --- a/Shared/ViewModels/VideoPlayerViewModel.swift +++ b/Shared/ViewModels/VideoPlayerViewModel.swift @@ -1,9 +1,11 @@ // -// VideoPlayerViewModel.swift -// JellyfinVideoPlayerDev -// -// Created by Ethan Pippin on 11/12/21. -// + /* + * 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 Combine import Defaults @@ -19,71 +21,59 @@ import MobileVLCKit final class VideoPlayerViewModel: ViewModel { + // MARK: Published + // Manually kept state because VLCKit doesn't properly set "played" // on the VLCMediaPlayer object - @Published var playerState: VLCMediaPlayerState - @Published var shouldShowGoogleCast: Bool - @Published var shouldShowAirplay: Bool - @Published var subtitlesEnabled: Bool { - didSet { - if subtitlesEnabled != oldValue { - previousItemVideoPlayerViewModel?.matchSubtitlesEnabled(with: self) - nextItemVideoPlayerViewModel?.matchSubtitlesEnabled(with: self) - } - } - } + @Published var playerState: VLCMediaPlayerState = .buffering @Published var leftLabelText: String = "--:--" @Published var rightLabelText: String = "--:--" @Published var playbackSpeed: PlaybackSpeed = .one - @Published var sliderPercentage: Double { + @Published var subtitlesEnabled: Bool + @Published var selectedAudioStreamIndex: Int + @Published var selectedSubtitleStreamIndex: Int + @Published var previousItemVideoPlayerViewModel: VideoPlayerViewModel? + @Published var nextItemVideoPlayerViewModel: VideoPlayerViewModel? + @Published var jumpBackwardLength: VideoPlayerJumpLength + @Published var jumpForwardLength: VideoPlayerJumpLength + @Published var sliderIsScrubbing: Bool = false + @Published var sliderPercentage: Double = 0 { willSet { sliderScrubbingSubject.send(self) sliderPercentageChanged(newValue: newValue) } } - @Published var sliderIsScrubbing: Bool = false - @Published var selectedAudioStreamIndex: Int { - didSet { - previousItemVideoPlayerViewModel?.matchAudioStream(with: self) - nextItemVideoPlayerViewModel?.matchAudioStream(with: self) - } - } - @Published var selectedSubtitleStreamIndex: Int { - didSet { - previousItemVideoPlayerViewModel?.matchSubtitleStream(with: self) - nextItemVideoPlayerViewModel?.matchSubtitleStream(with: self) - } - } - @Published var showAdjacentItems: Bool - @Published var shouldShowAutoPlayNextItem: Bool { + @Published var autoplayEnabled: Bool { willSet { - Defaults[.shouldShowAutoPlayNextItem] = newValue + Defaults[.autoplayEnabled] = newValue } } - @Published var autoPlayNextItem: Bool { - willSet { - Defaults[.autoPlayNextItem] = newValue - } - } - @Published var previousItemVideoPlayerViewModel: VideoPlayerViewModel? - @Published var nextItemVideoPlayerViewModel: VideoPlayerViewModel? + // MARK: ShouldShowItems + + let shouldShowPlayPreviousItem: Bool + let shouldShowPlayNextItem: Bool + let shouldShowAutoPlayNextItem: Bool + + // MARK: General let item: BaseItemDto let title: String let subtitle: String? let streamURL: URL let hlsURL: URL - // Full response kept for convenience - let response: PlaybackInfoResponse let audioStreams: [MediaStream] let subtitleStreams: [MediaStream] - let defaultAudioStreamIndex: Int - let defaultSubtitleStreamIndex: Int + let overlayType: OverlayType + + // Full response kept for convenience + let response: PlaybackInfoResponse var playerOverlayDelegate: PlayerOverlayDelegate? - // Ticks of the time the media has begun - var startTimeTicks: Int64? + // Ticks of the time the media began playing + private var startTimeTicks: Int64 = 0 + + // MARK: Current Time var currentSeconds: Double { let videoDuration = Double(item.runTimeTicks! / 10_000_000) @@ -107,18 +97,16 @@ final class VideoPlayerViewModel: ViewModel { response: PlaybackInfoResponse, audioStreams: [MediaStream], subtitleStreams: [MediaStream], - defaultAudioStreamIndex: Int, - defaultSubtitleStreamIndex: Int, - playerState: VLCMediaPlayerState, - shouldShowGoogleCast: Bool, - shouldShowAirplay: Bool, - subtitlesEnabled: Bool, - sliderPercentage: Double, selectedAudioStreamIndex: Int, selectedSubtitleStreamIndex: Int, - showAdjacentItems: Bool, - shouldShowAutoPlayNextItem: Bool, - autoPlayNextItem: Bool) { + subtitlesEnabled: Bool, + autoplayEnabled: Bool, + overlayType: OverlayType, + shouldShowPlayPreviousItem: Bool, + shouldShowPlayNextItem: Bool, + shouldShowAutoPlayNextItem: Bool + + ) { self.item = item self.title = title self.subtitle = subtitle @@ -127,26 +115,21 @@ final class VideoPlayerViewModel: ViewModel { self.response = response self.audioStreams = audioStreams self.subtitleStreams = subtitleStreams - self.defaultAudioStreamIndex = defaultAudioStreamIndex - self.defaultSubtitleStreamIndex = defaultSubtitleStreamIndex - self.playerState = playerState - self.shouldShowGoogleCast = shouldShowGoogleCast - self.shouldShowAirplay = shouldShowAirplay - self.subtitlesEnabled = subtitlesEnabled - self.sliderPercentage = sliderPercentage self.selectedAudioStreamIndex = selectedAudioStreamIndex self.selectedSubtitleStreamIndex = selectedSubtitleStreamIndex - self.showAdjacentItems = showAdjacentItems + self.subtitlesEnabled = subtitlesEnabled + self.autoplayEnabled = autoplayEnabled + self.overlayType = overlayType + self.shouldShowPlayPreviousItem = shouldShowPlayPreviousItem + self.shouldShowPlayNextItem = shouldShowPlayNextItem self.shouldShowAutoPlayNextItem = shouldShowAutoPlayNextItem - self.autoPlayNextItem = autoPlayNextItem + + self.jumpBackwardLength = Defaults[.videoPlayerJumpBackward] + self.jumpForwardLength = Defaults[.videoPlayerJumpForward] super.init() - self.sliderPercentageChanged(newValue: (item.userData?.playedPercentage ?? 0) / 100) - - if item.itemType != .episode { - self.showAdjacentItems = false - } + self.sliderPercentage = (item.userData?.playedPercentage ?? 0) / 100 } private func sliderPercentageChanged(newValue: Double) { diff --git a/Shared/ViewModels/ViewModel.swift b/Shared/ViewModels/ViewModel.swift index 18f023cc..1795c8b6 100644 --- a/Shared/ViewModels/ViewModel.swift +++ b/Shared/ViewModels/ViewModel.swift @@ -27,7 +27,7 @@ class ViewModel: ObservableObject { func handleAPIRequestError(displayMessage: String? = nil, logLevel: LogLevel = .error, tag: String = "", function: String = #function, file: String = #file, line: UInt = #line, completion: Subscribers.Completion) { switch completion { case .finished: - break + self.errorMessage = nil case .failure(let error): let logConstructor = LogConstructor(message: "__NOTHING__", tag: tag, level: logLevel, function: function, file: file, line: line)