mirror of
https://github.com/jellyfin/Swiftfin.git
synced 2024-11-23 14:10:01 +00:00
tv settings, channel item improvements
This commit is contained in:
parent
c2ad99ba83
commit
4bea0ddf43
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 ?? "",
|
||||
|
@ -74,8 +74,10 @@ extension Defaults.Keys {
|
||||
static let syncSubtitleStateWithAdjacent = Key<Bool>("experimental.syncSubtitleState", 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 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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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) {}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user