tv settings, channel item improvements

This commit is contained in:
jhays 2022-03-26 00:22:26 -05:00
parent c2ad99ba83
commit 4bea0ddf43
9 changed files with 185 additions and 115 deletions

View File

@ -27,14 +27,14 @@ final class LiveTVVideoPlayerCoordinator: NavigationCoordinatable {
@ViewBuilder @ViewBuilder
func makeStart() -> some View { func makeStart() -> some View {
if Defaults[.Experimental.nativePlayer] { if Defaults[.Experimental.liveTVNativePlayer] {
LiveTVNativeVideoPlayerView(viewModel: viewModel) LiveTVNativeVideoPlayerView(viewModel: viewModel)
.navigationBarHidden(true) .navigationBarHidden(true)
.ignoresSafeArea() .ignoresSafeArea()
} else { } else {
LiveTVVideoPlayerView(viewModel: viewModel) LiveTVVideoPlayerView(viewModel: viewModel)
.navigationBarHidden(true) .navigationBarHidden(true)
.ignoresSafeArea() .ignoresSafeArea()
} }
} }
} }

View File

@ -226,14 +226,14 @@ extension BaseItemDto {
mediaSourceId: mediaSourceID) mediaSourceId: mediaSourceID)
directStreamURL = URL(string: directStreamBuilder.URLString)! directStreamURL = URL(string: directStreamBuilder.URLString)!
if let transcodeURL = currentMediaSource.transcodingUrl, !Defaults[.Experimental.forceDirectPlay] { if let transcodeURL = currentMediaSource.transcodingUrl, !Defaults[.Experimental.liveTVForceDirectPlay] {
streamType = .transcode streamType = .transcode
transcodedStreamURL = URLComponents(string: SessionManager.main.currentLogin.server.currentURI transcodedStreamURL = URLComponents(string: SessionManager.main.currentLogin.server.currentURI
.appending(transcodeURL))! .appending(transcodeURL))!
} else { } else {
streamType = .direct streamType = .direct
transcodedStreamURL = nil transcodedStreamURL = nil
} }
let hlsStreamBuilder = DynamicHlsAPI.getMasterHlsVideoPlaylistWithRequestBuilder(itemId: id ?? "", let hlsStreamBuilder = DynamicHlsAPI.getMasterHlsVideoPlaylistWithRequestBuilder(itemId: id ?? "",
mediaSourceId: id ?? "", mediaSourceId: id ?? "",

View File

@ -74,8 +74,10 @@ extension Defaults.Keys {
static let syncSubtitleStateWithAdjacent = Key<Bool>("experimental.syncSubtitleState", default: false, static let syncSubtitleStateWithAdjacent = Key<Bool>("experimental.syncSubtitleState", default: false,
suite: SwiftfinStore.Defaults.generalSuite) suite: SwiftfinStore.Defaults.generalSuite)
static let forceDirectPlay = Key<Bool>("forceDirectPlay", default: false, suite: SwiftfinStore.Defaults.generalSuite) static let forceDirectPlay = Key<Bool>("forceDirectPlay", default: false, suite: SwiftfinStore.Defaults.generalSuite)
static let liveTVAlphaEnabled = Key<Bool>("liveTVAlphaEnabled", default: false, suite: SwiftfinStore.Defaults.generalSuite)
static let nativePlayer = Key<Bool>("nativePlayer", default: false, suite: SwiftfinStore.Defaults.generalSuite) static let nativePlayer = Key<Bool>("nativePlayer", default: false, suite: SwiftfinStore.Defaults.generalSuite)
static let liveTVAlphaEnabled = Key<Bool>("liveTVAlphaEnabled", default: false, suite: SwiftfinStore.Defaults.generalSuite)
static let liveTVForceDirectPlay = Key<Bool>("liveTVForceDirectPlay", default: false, suite: SwiftfinStore.Defaults.generalSuite)
static let liveTVNativePlayer = Key<Bool>("liveTVNativePlayer", default: false, suite: SwiftfinStore.Defaults.generalSuite)
} }
// tvos specific // tvos specific

View File

@ -151,7 +151,7 @@ final class VideoPlayerViewModel: ViewModel {
} }
func setSeconds(_ seconds: Int64) { func setSeconds(_ seconds: Int64) {
guard let runTimeTicks = item.runTimeTicks else { return } guard let runTimeTicks = item.runTimeTicks else { return }
let videoDuration = runTimeTicks let videoDuration = runTimeTicks
let percentage = Double(seconds * 10_000_000) / Double(videoDuration) let percentage = Double(seconds * 10_000_000) / Double(videoDuration)

View File

@ -10,71 +10,127 @@ import JellyfinAPI
import SwiftUI import SwiftUI
struct LiveTVChannelItemElement: View { struct LiveTVChannelItemElement: View {
@Environment(\.isFocused) @FocusState
var envFocused: Bool private var focused: Bool
@State @State
var focused: Bool = false private var loading: Bool = false
@State
private var isFocused: Bool = false
var channel: BaseItemDto var channel: BaseItemDto
var program: BaseItemDto? var program: BaseItemDto?
var startString = " " var startString = " "
var endString = " " var endString = " "
var progressPercent = Double(0) 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 { var body: some View {
VStack { ZStack {
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)
VStack { VStack {
HStack { HStack {
Text(startString) Text(channel.number ?? "")
.font(.footnote) .font(.footnote)
.lineLimit(1)
.frame(alignment: .leading) .frame(alignment: .leading)
.padding()
Spacer() 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) Spacer()
.font(.footnote)
.lineLimit(1) Text(endString)
.frame(alignment: .trailing) .font(.footnote)
} .lineLimit(1)
GeometryReader { gp in .frame(alignment: .trailing)
ZStack(alignment: .leading) { }
RoundedRectangle(cornerRadius: 6) GeometryReader { gp in
.fill(Color.gray) ZStack(alignment: .leading) {
.opacity(0.4) RoundedRectangle(cornerRadius: 6)
.frame(minWidth: 100, maxWidth: .infinity, minHeight: 12, maxHeight: 12) .fill(Color.gray)
RoundedRectangle(cornerRadius: 6) .opacity(0.4)
.fill(Color.jellyfinPurple) .frame(minWidth: 100, maxWidth: .infinity, minHeight: 12, maxHeight: 12)
.frame(width: CGFloat(progressPercent * gp.size.width), height: 12) RoundedRectangle(cornerRadius: 6)
.fill(Color.jellyfinPurple)
.frame(width: CGFloat(progressPercent * gp.size.width), height: 12)
}
.frame(alignment: .bottom)
}
} }
} }
} }
} .padding()
.padding() .opacity(loading ? 0.5 : 1.0)
.background(Color.clear)
.border(focused ? Color.blue : Color.clear, width: 4) if loading {
.onChange(of: envFocused) { envFocus in ProgressView()
withAnimation(.linear(duration: 0.15)) { }
self.focused = envFocus }
.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)
} }
} }

View File

@ -38,6 +38,31 @@ struct LibraryListView: View {
self.mainCoordinator.root(\.liveTV) self.mainCoordinator.root(\.liveTV)
} }
label: { 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 { ZStack {
HStack { HStack {
Spacer() Spacer()
@ -56,31 +81,6 @@ struct LibraryListView: View {
.cornerRadius(10) .cornerRadius(10)
.shadow(radius: 5) .shadow(radius: 5)
.padding(.bottom, 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 { } else {

View File

@ -54,24 +54,21 @@ struct LiveTVChannelsView: View {
let item = cell.item let item = cell.item
let channel = item.channel let channel = item.channel
if channel.type != "Folder" { if channel.type != "Folder" {
Button { let progressPercent = item.program?.getLiveProgressPercentage() ?? 0
self.viewModel.isLoading = true LiveTVChannelItemElement(channel: channel,
self.viewModel.fetchVideoPlayerViewModel(item: channel) { playerViewModel in program: item.program,
self.router.route(to: \.videoPlayer, playerViewModel) startString: item.program?.getLiveStartTimeString(formatter: viewModel.timeFormatter) ?? " ",
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { endString: item.program?.getLiveEndTimeString(formatter: viewModel.timeFormatter) ?? " ",
self.viewModel.isLoading = false progressPercent: progressPercent > 1.0 ? 1.0 : progressPercent,
} onSelect: { loadingAction in
} loadingAction(true)
} label: { self.viewModel.fetchVideoPlayerViewModel(item: channel) { playerViewModel in
let progressPercent = item.program?.getLiveProgressPercentage() ?? 0 self.router.route(to: \.videoPlayer, playerViewModel)
LiveTVChannelItemElement(channel: channel, DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
program: item.program, loadingAction(false)
startString: item.program?.getLiveStartTimeString(formatter: viewModel.timeFormatter) ?? " ", }
endString: item.program?.getLiveEndTimeString(formatter: viewModel.timeFormatter) ?? " ", }
progressPercent: progressPercent > 1.0 ? 1.0 : progressPercent })
)
}
.buttonStyle(PlainNavigationLinkButtonStyle())
} }
} }

View File

@ -15,11 +15,16 @@ struct ExperimentalSettingsView: View {
var forceDirectPlay var forceDirectPlay
@Default(.Experimental.syncSubtitleStateWithAdjacent) @Default(.Experimental.syncSubtitleStateWithAdjacent)
var syncSubtitleStateWithAdjacent var syncSubtitleStateWithAdjacent
@Default(.Experimental.liveTVAlphaEnabled)
var liveTVAlphaEnabled
@Default(.Experimental.nativePlayer) @Default(.Experimental.nativePlayer)
var nativePlayer var nativePlayer
@Default(.Experimental.liveTVAlphaEnabled)
var liveTVAlphaEnabled
@Default(.Experimental.liveTVForceDirectPlay)
var liveTVForceDirectPlay
@Default(.Experimental.liveTVNativePlayer)
var liveTVNativePlayer
var body: some View { var body: some View {
Form { Form {
Section { Section {
@ -28,9 +33,19 @@ struct ExperimentalSettingsView: View {
Toggle("Sync Subtitles with Adjacent Episodes", isOn: $syncSubtitleStateWithAdjacent) 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("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: { } header: {
L10n.experimental.text L10n.experimental.text

View File

@ -11,13 +11,13 @@ import UIKit
struct LiveTVNativeVideoPlayerView: UIViewControllerRepresentable { struct LiveTVNativeVideoPlayerView: UIViewControllerRepresentable {
let viewModel: VideoPlayerViewModel let viewModel: VideoPlayerViewModel
typealias UIViewControllerType = NativePlayerViewController typealias UIViewControllerType = NativePlayerViewController
func makeUIViewController(context: Context) -> NativePlayerViewController { func makeUIViewController(context: Context) -> NativePlayerViewController {
NativePlayerViewController(viewModel: viewModel) NativePlayerViewController(viewModel: viewModel)
} }
func updateUIViewController(_ uiViewController: NativePlayerViewController, context: Context) {} func updateUIViewController(_ uiViewController: NativePlayerViewController, context: Context) {}
} }