From 4bea0ddf434948d230a058b0354f1961dd95d9a6 Mon Sep 17 00:00:00 2001 From: jhays Date: Sat, 26 Mar 2022 00:22:26 -0500 Subject: [PATCH] tv settings, channel item improvements --- .../tvOSLiveTVVideoPlayerCoordinator.swift | 18 +-- .../BaseItemDto+VideoPlayerViewModel.swift | 4 +- .../SwiftfinStore/SwiftfinStoreDefaults.swift | 4 +- .../VideoPlayerViewModel.swift | 2 +- Shared/Views/LiveTVChannelItemElement.swift | 148 ++++++++++++------ Swiftfin tvOS/Views/LibraryListView.swift | 50 +++--- Swiftfin tvOS/Views/LiveTVChannelsView.swift | 33 ++-- .../ExperimentalSettingsView.swift | 21 ++- .../LiveTVNativeVideoPlayerView.swift | 20 +-- 9 files changed, 185 insertions(+), 115 deletions(-) diff --git a/Shared/Coordinators/VideoPlayerCoordinator/tvOSLiveTVVideoPlayerCoordinator.swift b/Shared/Coordinators/VideoPlayerCoordinator/tvOSLiveTVVideoPlayerCoordinator.swift index 8adc56fd..0e1247ec 100644 --- a/Shared/Coordinators/VideoPlayerCoordinator/tvOSLiveTVVideoPlayerCoordinator.swift +++ b/Shared/Coordinators/VideoPlayerCoordinator/tvOSLiveTVVideoPlayerCoordinator.swift @@ -27,14 +27,14 @@ final class LiveTVVideoPlayerCoordinator: NavigationCoordinatable { @ViewBuilder func makeStart() -> some View { - if Defaults[.Experimental.nativePlayer] { - LiveTVNativeVideoPlayerView(viewModel: viewModel) - .navigationBarHidden(true) - .ignoresSafeArea() - } else { - LiveTVVideoPlayerView(viewModel: viewModel) - .navigationBarHidden(true) - .ignoresSafeArea() - } + if Defaults[.Experimental.liveTVNativePlayer] { + LiveTVNativeVideoPlayerView(viewModel: viewModel) + .navigationBarHidden(true) + .ignoresSafeArea() + } else { + LiveTVVideoPlayerView(viewModel: viewModel) + .navigationBarHidden(true) + .ignoresSafeArea() + } } } diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift index 5069dddc..6df7c30c 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift @@ -226,14 +226,14 @@ extension BaseItemDto { mediaSourceId: mediaSourceID) directStreamURL = URL(string: directStreamBuilder.URLString)! - if let transcodeURL = currentMediaSource.transcodingUrl, !Defaults[.Experimental.forceDirectPlay] { + if let transcodeURL = currentMediaSource.transcodingUrl, !Defaults[.Experimental.liveTVForceDirectPlay] { streamType = .transcode transcodedStreamURL = URLComponents(string: SessionManager.main.currentLogin.server.currentURI .appending(transcodeURL))! } else { streamType = .direct transcodedStreamURL = nil - } + } let hlsStreamBuilder = DynamicHlsAPI.getMasterHlsVideoPlaylistWithRequestBuilder(itemId: id ?? "", mediaSourceId: id ?? "", diff --git a/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift b/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift index 864e0033..d2300763 100644 --- a/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift +++ b/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift @@ -74,8 +74,10 @@ extension Defaults.Keys { static let syncSubtitleStateWithAdjacent = Key("experimental.syncSubtitleState", default: false, suite: SwiftfinStore.Defaults.generalSuite) static let forceDirectPlay = Key("forceDirectPlay", default: false, suite: SwiftfinStore.Defaults.generalSuite) - static let liveTVAlphaEnabled = Key("liveTVAlphaEnabled", default: false, suite: SwiftfinStore.Defaults.generalSuite) static let nativePlayer = Key("nativePlayer", default: false, suite: SwiftfinStore.Defaults.generalSuite) + static let liveTVAlphaEnabled = Key("liveTVAlphaEnabled", default: false, suite: SwiftfinStore.Defaults.generalSuite) + static let liveTVForceDirectPlay = Key("liveTVForceDirectPlay", default: false, suite: SwiftfinStore.Defaults.generalSuite) + static let liveTVNativePlayer = Key("liveTVNativePlayer", default: false, suite: SwiftfinStore.Defaults.generalSuite) } // tvos specific diff --git a/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift b/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift index 715292c5..10dc2140 100644 --- a/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift +++ b/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift @@ -151,7 +151,7 @@ final class VideoPlayerViewModel: ViewModel { } func setSeconds(_ seconds: Int64) { - guard let runTimeTicks = item.runTimeTicks else { return } + guard let runTimeTicks = item.runTimeTicks else { return } let videoDuration = runTimeTicks let percentage = Double(seconds * 10_000_000) / Double(videoDuration) diff --git a/Shared/Views/LiveTVChannelItemElement.swift b/Shared/Views/LiveTVChannelItemElement.swift index f8671a25..f36aa1bf 100644 --- a/Shared/Views/LiveTVChannelItemElement.swift +++ b/Shared/Views/LiveTVChannelItemElement.swift @@ -10,71 +10,127 @@ import JellyfinAPI import SwiftUI struct LiveTVChannelItemElement: View { - @Environment(\.isFocused) - var envFocused: Bool + @FocusState + private var focused: Bool @State - var focused: Bool = false + private var loading: Bool = false + @State + private var isFocused: Bool = false var channel: BaseItemDto var program: BaseItemDto? var startString = " " var endString = " " var progressPercent = Double(0) + var onSelect: (@escaping (Bool) -> Void) -> Void + + private var detailText: String { + guard let program = program else { + return "" + } + var text = "" + if let season = program.parentIndexNumber, + let episode = program.indexNumber + { + text.append("\(season)x\(episode) ") + } else if let episode = program.indexNumber { + text.append("\(episode) ") + } + if let title = program.episodeTitle { + text.append("\(title) ") + } + if let year = program.productionYear { + text.append("\(year) ") + } + if let rating = program.officialRating { + text.append("\(rating)") + } + return text + } var body: some View { - VStack { - HStack { - Spacer() - Text(channel.number ?? "") - .font(.footnote) - .frame(alignment: .trailing) - }.frame(alignment: .top) - ImageView(channel.getPrimaryImage(maxWidth: 125)) - .frame(width: 125, alignment: .center) - .offset(x: 0, y: -32) - Text(channel.name ?? "?") - .font(.footnote) - .lineLimit(1) - .frame(alignment: .center) - Text(program?.name ?? L10n.notAvailableSlash) - .font(.body) - .lineLimit(1) - .foregroundColor(.green) + ZStack { VStack { HStack { - Text(startString) + Text(channel.number ?? "") .font(.footnote) - .lineLimit(1) .frame(alignment: .leading) - + .padding() Spacer() + }.frame(alignment: .top) + Spacer() + } + VStack { + ImageView(channel.getPrimaryImage(maxWidth: 128)) + .aspectRatio(contentMode: .fit) + .frame(width: 128, alignment: .center) + .padding(.init(top: 8, leading: 0, bottom: 0, trailing: 0)) + Text(channel.name ?? "?") + .font(.footnote) + .lineLimit(1) + .frame(alignment: .center) + Text(program?.name ?? L10n.notAvailableSlash) + .font(.body) + .lineLimit(1) + .foregroundColor(.green) + Text(detailText) + .font(.body) + .lineLimit(1) + .foregroundColor(.green) + Spacer() + HStack(alignment: .bottom) { + VStack { + Spacer() + HStack { + Text(startString) + .font(.footnote) + .lineLimit(1) + .frame(alignment: .leading) - Text(endString) - .font(.footnote) - .lineLimit(1) - .frame(alignment: .trailing) - } - GeometryReader { gp in - ZStack(alignment: .leading) { - RoundedRectangle(cornerRadius: 6) - .fill(Color.gray) - .opacity(0.4) - .frame(minWidth: 100, maxWidth: .infinity, minHeight: 12, maxHeight: 12) - RoundedRectangle(cornerRadius: 6) - .fill(Color.jellyfinPurple) - .frame(width: CGFloat(progressPercent * gp.size.width), height: 12) + Spacer() + + Text(endString) + .font(.footnote) + .lineLimit(1) + .frame(alignment: .trailing) + } + GeometryReader { gp in + ZStack(alignment: .leading) { + RoundedRectangle(cornerRadius: 6) + .fill(Color.gray) + .opacity(0.4) + .frame(minWidth: 100, maxWidth: .infinity, minHeight: 12, maxHeight: 12) + RoundedRectangle(cornerRadius: 6) + .fill(Color.jellyfinPurple) + .frame(width: CGFloat(progressPercent * gp.size.width), height: 12) + } + .frame(alignment: .bottom) + } } } } - } - .padding() - .background(Color.clear) - .border(focused ? Color.blue : Color.clear, width: 4) - .onChange(of: envFocused) { envFocus in - withAnimation(.linear(duration: 0.15)) { - self.focused = envFocus + .padding() + .opacity(loading ? 0.5 : 1.0) + + if loading { + ProgressView() + } + } + .overlay(RoundedRectangle(cornerRadius: 20) + .stroke(isFocused ? Color.blue : Color.clear, lineWidth: 4)) + .cornerRadius(20) + .scaleEffect(isFocused ? 1.1 : 1) + .focusable(true) + .focused($focused) + .onChange(of: focused) { foc in + withAnimation(.linear(duration: 0.15)) { + self.isFocused = foc + } + } + .onLongPressGesture(minimumDuration: 0.01, pressing: { _ in }) { + onSelect { loadingState in + loading = loadingState } } - .scaleEffect(focused ? 1.1 : 1) } } diff --git a/Swiftfin tvOS/Views/LibraryListView.swift b/Swiftfin tvOS/Views/LibraryListView.swift index 4760e74e..9001544d 100644 --- a/Swiftfin tvOS/Views/LibraryListView.swift +++ b/Swiftfin tvOS/Views/LibraryListView.swift @@ -38,6 +38,31 @@ struct LibraryListView: View { self.mainCoordinator.root(\.liveTV) } label: { + ZStack { + HStack { + Spacer() + VStack { + Text(library.name ?? "") + .foregroundColor(.white) + .font(.title2) + .fontWeight(.semibold) + } + Spacer() + }.padding(32) + } + .frame(minWidth: 100, maxWidth: .infinity) + .frame(height: 100) + } + .cornerRadius(10) + .shadow(radius: 5) + .padding(.bottom, 5) + } + } else { + Button { + self.libraryListRouter.route(to: \.library, + (viewModel: LibraryViewModel(parentID: library.id), title: library.name ?? "")) + } + label: { ZStack { HStack { Spacer() @@ -56,31 +81,6 @@ struct LibraryListView: View { .cornerRadius(10) .shadow(radius: 5) .padding(.bottom, 5) - } - } else { - Button { - self.libraryListRouter.route(to: \.library, - (viewModel: LibraryViewModel(parentID: library.id), title: library.name ?? "")) - } - label: { - ZStack { - HStack { - Spacer() - VStack { - Text(library.name ?? "") - .foregroundColor(.white) - .font(.title2) - .fontWeight(.semibold) - } - Spacer() - }.padding(32) - } - .frame(minWidth: 100, maxWidth: .infinity) - .frame(height: 100) - } - .cornerRadius(10) - .shadow(radius: 5) - .padding(.bottom, 5) } } } else { diff --git a/Swiftfin tvOS/Views/LiveTVChannelsView.swift b/Swiftfin tvOS/Views/LiveTVChannelsView.swift index d34fb881..19d66649 100644 --- a/Swiftfin tvOS/Views/LiveTVChannelsView.swift +++ b/Swiftfin tvOS/Views/LiveTVChannelsView.swift @@ -54,24 +54,21 @@ struct LiveTVChannelsView: View { let item = cell.item let channel = item.channel if channel.type != "Folder" { - Button { - self.viewModel.isLoading = true - self.viewModel.fetchVideoPlayerViewModel(item: channel) { playerViewModel in - self.router.route(to: \.videoPlayer, playerViewModel) - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - self.viewModel.isLoading = false - } - } - } label: { - let progressPercent = item.program?.getLiveProgressPercentage() ?? 0 - LiveTVChannelItemElement(channel: channel, - program: item.program, - startString: item.program?.getLiveStartTimeString(formatter: viewModel.timeFormatter) ?? " ", - endString: item.program?.getLiveEndTimeString(formatter: viewModel.timeFormatter) ?? " ", - progressPercent: progressPercent > 1.0 ? 1.0 : progressPercent - ) - } - .buttonStyle(PlainNavigationLinkButtonStyle()) + let progressPercent = item.program?.getLiveProgressPercentage() ?? 0 + LiveTVChannelItemElement(channel: channel, + program: item.program, + startString: item.program?.getLiveStartTimeString(formatter: viewModel.timeFormatter) ?? " ", + endString: item.program?.getLiveEndTimeString(formatter: viewModel.timeFormatter) ?? " ", + progressPercent: progressPercent > 1.0 ? 1.0 : progressPercent, + onSelect: { loadingAction in + loadingAction(true) + self.viewModel.fetchVideoPlayerViewModel(item: channel) { playerViewModel in + self.router.route(to: \.videoPlayer, playerViewModel) + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + loadingAction(false) + } + } + }) } } diff --git a/Swiftfin tvOS/Views/SettingsView/ExperimentalSettingsView.swift b/Swiftfin tvOS/Views/SettingsView/ExperimentalSettingsView.swift index ab5c1b39..15be056c 100644 --- a/Swiftfin tvOS/Views/SettingsView/ExperimentalSettingsView.swift +++ b/Swiftfin tvOS/Views/SettingsView/ExperimentalSettingsView.swift @@ -15,11 +15,16 @@ struct ExperimentalSettingsView: View { var forceDirectPlay @Default(.Experimental.syncSubtitleStateWithAdjacent) var syncSubtitleStateWithAdjacent - @Default(.Experimental.liveTVAlphaEnabled) - var liveTVAlphaEnabled @Default(.Experimental.nativePlayer) var nativePlayer + @Default(.Experimental.liveTVAlphaEnabled) + var liveTVAlphaEnabled + @Default(.Experimental.liveTVForceDirectPlay) + var liveTVForceDirectPlay + @Default(.Experimental.liveTVNativePlayer) + var liveTVNativePlayer + var body: some View { Form { Section { @@ -28,9 +33,19 @@ struct ExperimentalSettingsView: View { Toggle("Sync Subtitles with Adjacent Episodes", isOn: $syncSubtitleStateWithAdjacent) + Toggle("Native Player", isOn: $nativePlayer) + + } header: { + L10n.experimental.text + } + + Section { + Toggle("Live TV (Alpha)", isOn: $liveTVAlphaEnabled) - Toggle("Native Player", isOn: $nativePlayer) + Toggle("Live TV Force Direct Play", isOn: $liveTVForceDirectPlay) + + Toggle("Live TV Native Player", isOn: $liveTVNativePlayer) } header: { L10n.experimental.text diff --git a/Swiftfin tvOS/Views/VideoPlayer/LiveTVNativeVideoPlayerView.swift b/Swiftfin tvOS/Views/VideoPlayer/LiveTVNativeVideoPlayerView.swift index 57fade2e..0c3fb97e 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/LiveTVNativeVideoPlayerView.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/LiveTVNativeVideoPlayerView.swift @@ -10,14 +10,14 @@ import SwiftUI import UIKit struct LiveTVNativeVideoPlayerView: UIViewControllerRepresentable { - - let viewModel: VideoPlayerViewModel - - typealias UIViewControllerType = NativePlayerViewController - - func makeUIViewController(context: Context) -> NativePlayerViewController { - NativePlayerViewController(viewModel: viewModel) - } - - func updateUIViewController(_ uiViewController: NativePlayerViewController, context: Context) {} + + let viewModel: VideoPlayerViewModel + + typealias UIViewControllerType = NativePlayerViewController + + func makeUIViewController(context: Context) -> NativePlayerViewController { + NativePlayerViewController(viewModel: viewModel) + } + + func updateUIViewController(_ uiViewController: NativePlayerViewController, context: Context) {} }