Add series item view
This commit is contained in:
parent
897d158707
commit
2b6f0c5ea1
|
@ -43,7 +43,7 @@ struct LandscapeItemElement: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
ImageView(src: (item.type == "Episode" ? item.getSeriesBackdropImage(maxWidth: 445) : item.getBackdropImage(maxWidth: 445)), bh: item.type == "Episode" ? item.getSeriesBackdropImageBlurHash() : item.getBackdropImageBlurHash())
|
ImageView(src: (item.type == "Episode" ? item.getSeriesBackdropImage(maxWidth: 800) : item.getBackdropImage(maxWidth: 800)), bh: item.type == "Episode" ? item.getSeriesBackdropImageBlurHash() : item.getBackdropImageBlurHash())
|
||||||
.frame(width: 445, height: 250)
|
.frame(width: 445, height: 250)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
.overlay(
|
.overlay(
|
||||||
|
@ -97,7 +97,7 @@ struct LandscapeItemElement: View {
|
||||||
if envFocus == true {
|
if envFocus == true {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||||
// your code here
|
// your code here
|
||||||
if self.focused == true {
|
if focused == true {
|
||||||
backgroundURL = item.getBackdropImage(maxWidth: 1080)
|
backgroundURL = item.getBackdropImage(maxWidth: 1080)
|
||||||
BackgroundManager.current.setBackground(to: backgroundURL!, hash: item.getBackdropImageBlurHash())
|
BackgroundManager.current.setBackground(to: backgroundURL!, hash: item.getBackdropImageBlurHash())
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
//
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
import JellyfinAPI
|
||||||
|
|
||||||
|
struct PlainLinkButton: View {
|
||||||
|
@Environment(\.isFocused) var envFocused: Bool
|
||||||
|
@State var focused: Bool = false
|
||||||
|
@State var label: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Text(label)
|
||||||
|
.fontWeight(focused ? .bold : .regular)
|
||||||
|
.foregroundColor(.blue)
|
||||||
|
.onChange(of: envFocused) { envFocus in
|
||||||
|
withAnimation(.linear(duration: 0.15)) {
|
||||||
|
self.focused = envFocus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.scaleEffect(focused ? 1.1 : 1)
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,7 +19,7 @@ struct PortraitItemElement: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
ImageView(src: item.type == "Episode" ? item.getSeriesPrimaryImage(maxWidth: 200) : item.getPrimaryImage(maxWidth: 200), bh: item.type == "Episode" ? item.getSeriesPrimaryImageBlurHash() : item.getPrimaryImageBlurHash())
|
ImageView(src: item.type == "Episode" ? item.getSeriesPrimaryImage(maxWidth: 400) : item.getPrimaryImage(maxWidth: 400), bh: item.type == "Episode" ? item.getSeriesPrimaryImageBlurHash() : item.getPrimaryImageBlurHash())
|
||||||
.frame(width: 200, height: 300)
|
.frame(width: 200, height: 300)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
.shadow(radius: focused ? 10.0 : 0)
|
.shadow(radius: focused ? 10.0 : 0)
|
||||||
|
@ -65,7 +65,7 @@ struct PortraitItemElement: View {
|
||||||
if envFocus == true {
|
if envFocus == true {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||||
// your code here
|
// your code here
|
||||||
if self.focused == true {
|
if focused == true {
|
||||||
backgroundURL = item.getBackdropImage(maxWidth: 1080)
|
backgroundURL = item.getBackdropImage(maxWidth: 1080)
|
||||||
BackgroundManager.current.setBackground(to: backgroundURL!, hash: item.getBackdropImageBlurHash())
|
BackgroundManager.current.setBackground(to: backgroundURL!, hash: item.getBackdropImageBlurHash())
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,21 +27,14 @@ struct ItemView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
Group {
|
||||||
NavigationLink(destination: VideoPlayerView(item: videoPlayerItem.itemToPlay), isActive: $videoPlayerItem.shouldShowPlayer) {
|
if item.type == "Movie" {
|
||||||
EmptyView()
|
MovieItemView(viewModel: .init(item: item))
|
||||||
|
} else if item.type == "Series" {
|
||||||
|
SeriesItemView(viewModel: .init(item: item))
|
||||||
|
} else {
|
||||||
|
Text("Type: \(item.type ?? "") not implemented yet :(")
|
||||||
}
|
}
|
||||||
.buttonStyle(PlainNavigationLinkButtonStyle())
|
|
||||||
.focusable(false)
|
|
||||||
|
|
||||||
Group {
|
|
||||||
if item.type == "Movie" {
|
|
||||||
MovieItemView(item: item)
|
|
||||||
} else {
|
|
||||||
Text("Type: \(item.type ?? "") not implemented yet :(")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.environmentObject(videoPlayerItem)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,9 +11,8 @@ import SwiftUI
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
|
|
||||||
struct MovieItemView: View {
|
struct MovieItemView: View {
|
||||||
let item: BaseItemDto
|
@ObservedObject var viewModel: MovieItemViewModel
|
||||||
@EnvironmentObject private var playbackInfo: VideoPlayerItem
|
|
||||||
|
|
||||||
@State var actors: [BaseItemPerson] = [];
|
@State var actors: [BaseItemPerson] = [];
|
||||||
@State var studio: String? = nil;
|
@State var studio: String? = nil;
|
||||||
@State var director: String? = nil;
|
@State var director: String? = nil;
|
||||||
|
@ -25,9 +24,9 @@ struct MovieItemView: View {
|
||||||
director = nil
|
director = nil
|
||||||
studio = nil
|
studio = nil
|
||||||
var actor_index = 0;
|
var actor_index = 0;
|
||||||
item.people?.forEach { person in
|
viewModel.item.people?.forEach { person in
|
||||||
if(person.type == "Actor") {
|
if(person.type == "Actor") {
|
||||||
if(actor_index < 8) {
|
if(actor_index < 4) {
|
||||||
actors.append(person)
|
actors.append(person)
|
||||||
}
|
}
|
||||||
actor_index = actor_index + 1;
|
actor_index = actor_index + 1;
|
||||||
|
@ -36,141 +35,153 @@ struct MovieItemView: View {
|
||||||
director = person.name ?? ""
|
director = person.name ?? ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
studio = viewModel.item.studios?.first?.name ?? nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
ImageView(src: item.getBackdropImage(maxWidth: 1920), bh: item.getBackdropImageBlurHash())
|
ImageView(src: viewModel.item.getBackdropImage(maxWidth: 1920), bh: viewModel.item.getBackdropImageBlurHash())
|
||||||
.opacity(0.4)
|
.opacity(0.4)
|
||||||
ScrollView {
|
ScrollView {
|
||||||
LazyVStack {
|
LazyVStack(alignment: .leading) {
|
||||||
|
Spacer() //i hate ficus engine
|
||||||
|
.frame(width: 1920, height: 2)
|
||||||
|
.focusable()
|
||||||
|
Text(viewModel.item.name ?? "")
|
||||||
|
.font(.title)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
.foregroundColor(.primary)
|
||||||
HStack {
|
HStack {
|
||||||
VStack(alignment: .leading) {
|
if viewModel.item.productionYear != nil {
|
||||||
Text(item.name ?? "")
|
Text(String(viewModel.item.productionYear!)).font(.subheadline)
|
||||||
.font(.title)
|
.fontWeight(.medium)
|
||||||
.fontWeight(.bold)
|
.foregroundColor(.secondary)
|
||||||
.foregroundColor(.primary)
|
.lineLimit(1)
|
||||||
HStack {
|
}
|
||||||
if item.productionYear != nil {
|
Text(viewModel.item.getItemRuntime()).font(.subheadline)
|
||||||
Text(String(item.productionYear!)).font(.subheadline)
|
.fontWeight(.medium)
|
||||||
.fontWeight(.medium)
|
.foregroundColor(.secondary)
|
||||||
.foregroundColor(.secondary)
|
.lineLimit(1)
|
||||||
.lineLimit(1)
|
if viewModel.item.officialRating != nil {
|
||||||
}
|
Text(viewModel.item.officialRating!).font(.subheadline)
|
||||||
Text(item.getItemRuntime()).font(.subheadline)
|
.fontWeight(.semibold)
|
||||||
.fontWeight(.medium)
|
.foregroundColor(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
.padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4))
|
||||||
|
.overlay(RoundedRectangle(cornerRadius: 2)
|
||||||
|
.stroke(Color.secondary, lineWidth: 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
VStack(alignment: .trailing) {
|
||||||
|
if(studio != nil) {
|
||||||
|
Text("STUDIO")
|
||||||
|
.font(.body)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
Text(studio!)
|
||||||
|
.font(.body)
|
||||||
|
.fontWeight(.semibold)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.lineLimit(1)
|
.padding(.bottom, 40)
|
||||||
if item.officialRating != nil {
|
|
||||||
Text(item.officialRating!).font(.subheadline)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.lineLimit(1)
|
|
||||||
.padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4))
|
|
||||||
.overlay(RoundedRectangle(cornerRadius: 2)
|
|
||||||
.stroke(Color.secondary, lineWidth: 1))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
HStack {
|
if(director != nil) {
|
||||||
VStack(alignment: .trailing) {
|
Text("DIRECTOR")
|
||||||
if(studio != nil) {
|
.font(.body)
|
||||||
Text("STUDIO")
|
.fontWeight(.semibold)
|
||||||
.font(.body)
|
.foregroundColor(.primary)
|
||||||
.fontWeight(.semibold)
|
Text(director!)
|
||||||
.foregroundColor(.primary)
|
.font(.body)
|
||||||
Text(studio!)
|
.fontWeight(.semibold)
|
||||||
.font(.body)
|
.foregroundColor(.secondary)
|
||||||
.fontWeight(.semibold)
|
.padding(.bottom, 40)
|
||||||
.foregroundColor(.secondary)
|
}
|
||||||
.padding(.bottom, 40)
|
|
||||||
}
|
if(!actors.isEmpty) {
|
||||||
|
Text("CAST")
|
||||||
if(director != nil) {
|
.font(.body)
|
||||||
Text("DIRECTOR")
|
.fontWeight(.semibold)
|
||||||
.font(.body)
|
.foregroundColor(.primary)
|
||||||
.fontWeight(.semibold)
|
ForEach(actors, id: \.id) { person in
|
||||||
.foregroundColor(.primary)
|
Text(person.name!)
|
||||||
Text(director!)
|
|
||||||
.font(.body)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.padding(.bottom, 40)
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!actors.isEmpty) {
|
|
||||||
Text("CAST")
|
|
||||||
.font(.body)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
ForEach(actors, id: \.id) { person in
|
|
||||||
Text(person.name!)
|
|
||||||
.font(.body)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
Text(item.taglines?.first ?? "")
|
|
||||||
.font(.body)
|
.font(.body)
|
||||||
.italic()
|
.fontWeight(.semibold)
|
||||||
.fontWeight(.medium)
|
.foregroundColor(.secondary)
|
||||||
.foregroundColor(.primary)
|
|
||||||
|
|
||||||
Text(item.overview ?? "")
|
|
||||||
.font(.body)
|
|
||||||
.fontWeight(.medium)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
VStack {
|
|
||||||
Button {
|
|
||||||
playbackInfo.shouldShowPlayer = true
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "heart.fill")
|
|
||||||
.font(.system(size: 40))
|
|
||||||
.padding(.vertical, 12).padding(.horizontal, 20)
|
|
||||||
}
|
|
||||||
Text("Favorite")
|
|
||||||
.font(.caption)
|
|
||||||
}
|
|
||||||
VStack {
|
|
||||||
Button {
|
|
||||||
playbackInfo.itemToPlay = item
|
|
||||||
playbackInfo.shouldShowPlayer = true
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "play.fill")
|
|
||||||
.font(.system(size: 40))
|
|
||||||
.padding(.vertical, 12).padding(.horizontal, 20)
|
|
||||||
}.prefersDefaultFocus(in: namespace)
|
|
||||||
Text("Play")
|
|
||||||
.font(.caption)
|
|
||||||
}
|
|
||||||
VStack {
|
|
||||||
Button {
|
|
||||||
playbackInfo.shouldShowPlayer = true
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "eye.fill")
|
|
||||||
.font(.system(size: 40))
|
|
||||||
.padding(.vertical, 12).padding(.horizontal, 20)
|
|
||||||
}
|
|
||||||
Text("Mark Watched")
|
|
||||||
.font(.caption)
|
|
||||||
}
|
|
||||||
}.padding(.top, 15)
|
|
||||||
Spacer()
|
|
||||||
}
|
}
|
||||||
}.padding(.top, 50)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
VStack {
|
|
||||||
ImageView(src: item.getPrimaryImage(maxWidth: 450), bh: item.getPrimaryImageBlurHash())
|
|
||||||
.frame(width: 450, height: 675)
|
|
||||||
.cornerRadius(10)
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
if(!(viewModel.item.taglines ?? []).isEmpty) {
|
||||||
|
Text(viewModel.item.taglines?.first ?? "")
|
||||||
|
.font(.body)
|
||||||
|
.italic()
|
||||||
|
.fontWeight(.medium)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
}
|
||||||
|
Text(viewModel.item.overview ?? "")
|
||||||
|
.font(.body)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
VStack {
|
||||||
|
Button {
|
||||||
|
viewModel.updateFavoriteState()
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "heart.fill")
|
||||||
|
.foregroundColor(viewModel.isFavorited ? .red : .primary)
|
||||||
|
.font(.system(size: 40))
|
||||||
|
.padding(.vertical, 12).padding(.horizontal, 20)
|
||||||
|
}
|
||||||
|
Text(viewModel.isFavorited ? "Unfavorite" : "Favorite")
|
||||||
|
.font(.caption)
|
||||||
|
}
|
||||||
|
VStack {
|
||||||
|
NavigationLink(destination: VideoPlayerView(item: viewModel.item)) {
|
||||||
|
Image(systemName: "play.fill")
|
||||||
|
.font(.system(size: 40))
|
||||||
|
.padding(.vertical, 12).padding(.horizontal, 20)
|
||||||
|
}
|
||||||
|
Text(viewModel.item.getItemProgressString() != "" ? "\(viewModel.item.getItemProgressString()) left" : "Play")
|
||||||
|
.font(.caption)
|
||||||
|
}
|
||||||
|
VStack {
|
||||||
|
Button {
|
||||||
|
viewModel.updateWatchState()
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "eye.fill")
|
||||||
|
.foregroundColor(viewModel.isWatched ? .red : .primary)
|
||||||
|
.font(.system(size: 40))
|
||||||
|
.padding(.vertical, 12).padding(.horizontal, 20)
|
||||||
|
}
|
||||||
|
Text(viewModel.isWatched ? "Unwatch" : "Mark Watched")
|
||||||
|
.font(.caption)
|
||||||
|
}
|
||||||
|
}.padding(.top, 15)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}.padding(.top, 50)
|
||||||
|
|
||||||
|
if(!viewModel.similarItems.isEmpty) {
|
||||||
|
Text("More Like This")
|
||||||
|
.font(.headline)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
ScrollView(.horizontal) {
|
||||||
|
LazyHStack {
|
||||||
|
Spacer().frame(width: 45)
|
||||||
|
ForEach(viewModel.similarItems, id: \.id) { similarItems in
|
||||||
|
NavigationLink(destination: ItemView(item: similarItems)) {
|
||||||
|
PortraitItemElement(item: similarItems)
|
||||||
|
}.buttonStyle(PlainNavigationLinkButtonStyle())
|
||||||
|
}
|
||||||
|
Spacer().frame(width: 45)
|
||||||
|
}
|
||||||
|
}.padding(EdgeInsets(top: -30, leading: -90, bottom: 0, trailing: -90))
|
||||||
|
.frame(height: 360)
|
||||||
}
|
}
|
||||||
}.padding(EdgeInsets(top: 90, leading: 90, bottom: 0, trailing: 90))
|
}.padding(EdgeInsets(top: 90, leading: 90, bottom: 0, trailing: 90))
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,227 @@
|
||||||
|
//
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
import JellyfinAPI
|
||||||
|
|
||||||
|
struct SeriesItemView: View {
|
||||||
|
@ObservedObject var viewModel: SeriesItemViewModel
|
||||||
|
|
||||||
|
@State var actors: [BaseItemPerson] = [];
|
||||||
|
@State var studio: String? = nil;
|
||||||
|
@State var director: String? = nil;
|
||||||
|
|
||||||
|
@Environment(\.resetFocus) var resetFocus
|
||||||
|
@Namespace private var namespace
|
||||||
|
|
||||||
|
func onAppear() {
|
||||||
|
actors = []
|
||||||
|
director = nil
|
||||||
|
studio = nil
|
||||||
|
var actor_index = 0;
|
||||||
|
viewModel.item.people?.forEach { person in
|
||||||
|
if(person.type == "Actor") {
|
||||||
|
if(actor_index < 4) {
|
||||||
|
actors.append(person)
|
||||||
|
}
|
||||||
|
actor_index = actor_index + 1;
|
||||||
|
}
|
||||||
|
if(person.type == "Director") {
|
||||||
|
director = person.name ?? ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
studio = viewModel.item.studios?.first?.name ?? nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
ImageView(src: viewModel.item.getBackdropImage(maxWidth: 1920), bh: viewModel.item.getBackdropImageBlurHash())
|
||||||
|
.opacity(0.4)
|
||||||
|
ScrollView {
|
||||||
|
ScrollViewReader { reader in
|
||||||
|
LazyVStack(alignment: .leading) {
|
||||||
|
Spacer() //i hate ficus engine
|
||||||
|
.frame(width: 1920, height: 2)
|
||||||
|
.focusable()
|
||||||
|
Text(viewModel.item.name ?? "")
|
||||||
|
.font(.title)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
HStack {
|
||||||
|
Text(viewModel.getRunYears()).font(.subheadline)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
if viewModel.item.officialRating != nil {
|
||||||
|
Text(viewModel.item.officialRating!).font(.subheadline)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
.padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4))
|
||||||
|
.overlay(RoundedRectangle(cornerRadius: 2)
|
||||||
|
.stroke(Color.secondary, lineWidth: 1))
|
||||||
|
}
|
||||||
|
if viewModel.item.communityRating != nil {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "star.fill")
|
||||||
|
.foregroundColor(.yellow)
|
||||||
|
.font(.subheadline)
|
||||||
|
Text(String(viewModel.item.communityRating!)).font(.subheadline)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
VStack(alignment: .trailing) {
|
||||||
|
if(studio != nil) {
|
||||||
|
Text("STUDIO")
|
||||||
|
.font(.body)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
Text(studio!)
|
||||||
|
.font(.body)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.padding(.bottom, 40)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(director != nil) {
|
||||||
|
Text("DIRECTOR")
|
||||||
|
.font(.body)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
Text(director!)
|
||||||
|
.font(.body)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.padding(.bottom, 40)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!actors.isEmpty) {
|
||||||
|
Text("CAST")
|
||||||
|
.font(.body)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
ForEach(actors, id: \.id) { person in
|
||||||
|
Text(person.name!)
|
||||||
|
.font(.body)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
if(!(viewModel.item.taglines ?? []).isEmpty) {
|
||||||
|
Text(viewModel.item.taglines?.first ?? "")
|
||||||
|
.font(.body)
|
||||||
|
.italic()
|
||||||
|
.fontWeight(.medium)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
}
|
||||||
|
Text(viewModel.item.overview ?? "")
|
||||||
|
.font(.body)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
VStack {
|
||||||
|
Button {
|
||||||
|
viewModel.updateFavoriteState()
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "heart.fill")
|
||||||
|
.foregroundColor(viewModel.isFavorited ? .red : .primary)
|
||||||
|
.font(.system(size: 40))
|
||||||
|
.padding(.vertical, 12).padding(.horizontal, 20)
|
||||||
|
}.prefersDefaultFocus(in: namespace)
|
||||||
|
Text(viewModel.isFavorited ? "Unfavorite" : "Favorite")
|
||||||
|
.font(.caption)
|
||||||
|
}
|
||||||
|
if(viewModel.nextUpItem != nil) {
|
||||||
|
VStack {
|
||||||
|
NavigationLink(destination: VideoPlayerView(item: viewModel.nextUpItem!)) {
|
||||||
|
Image(systemName: "play.fill")
|
||||||
|
.font(.system(size: 40))
|
||||||
|
.padding(.vertical, 12).padding(.horizontal, 20)
|
||||||
|
}
|
||||||
|
Text("Play • \(viewModel.nextUpItem!.getEpisodeLocator())")
|
||||||
|
.font(.caption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VStack {
|
||||||
|
Button {
|
||||||
|
viewModel.updateWatchState()
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "eye.fill")
|
||||||
|
.foregroundColor(viewModel.isWatched ? .red : .primary)
|
||||||
|
.font(.system(size: 40))
|
||||||
|
.padding(.vertical, 12).padding(.horizontal, 20)
|
||||||
|
}
|
||||||
|
Text(viewModel.isWatched ? "Unwatch" : "Mark Watched")
|
||||||
|
.font(.caption)
|
||||||
|
}
|
||||||
|
}.padding(.top, 15)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}.padding(.top, 50)
|
||||||
|
|
||||||
|
if(viewModel.nextUpItem != nil) {
|
||||||
|
Text("Next Up")
|
||||||
|
.font(.headline)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
NavigationLink(destination: ItemView(item: viewModel.nextUpItem!)) {
|
||||||
|
LandscapeItemElement(item: viewModel.nextUpItem!)
|
||||||
|
}.buttonStyle(PlainNavigationLinkButtonStyle()).padding(.bottom, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!viewModel.seasons.isEmpty) {
|
||||||
|
Text("Seasons")
|
||||||
|
.font(.headline)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
ScrollView(.horizontal) {
|
||||||
|
LazyHStack {
|
||||||
|
Spacer().frame(width: 45)
|
||||||
|
ForEach(viewModel.seasons, id: \.id) { season in
|
||||||
|
NavigationLink(destination: ItemView(item: season)) {
|
||||||
|
PortraitItemElement(item: season)
|
||||||
|
}.buttonStyle(PlainNavigationLinkButtonStyle())
|
||||||
|
}
|
||||||
|
Spacer().frame(width: 45)
|
||||||
|
}
|
||||||
|
}.padding(EdgeInsets(top: -30, leading: -90, bottom: 0, trailing: -90))
|
||||||
|
.frame(height: 360)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!viewModel.similarItems.isEmpty) {
|
||||||
|
Text("More Like This")
|
||||||
|
.font(.headline)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
ScrollView(.horizontal) {
|
||||||
|
LazyHStack {
|
||||||
|
Spacer().frame(width: 45)
|
||||||
|
ForEach(viewModel.similarItems, id: \.id) { similarItems in
|
||||||
|
NavigationLink(destination: ItemView(item: similarItems)) {
|
||||||
|
PortraitItemElement(item: similarItems)
|
||||||
|
}.buttonStyle(PlainNavigationLinkButtonStyle())
|
||||||
|
}
|
||||||
|
Spacer().frame(width: 45)
|
||||||
|
}
|
||||||
|
}.padding(EdgeInsets(top: -30, leading: -90, bottom: 0, trailing: -90))
|
||||||
|
.frame(height: 360)
|
||||||
|
}
|
||||||
|
}.padding(EdgeInsets(top: 90, leading: 90, bottom: 45, trailing: 90))
|
||||||
|
}
|
||||||
|
}.focusScope(namespace)
|
||||||
|
}.onAppear(perform: onAppear)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,9 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder.AppleTV.Storyboard" version="3.0" toolsVersion="18122" targetRuntime="AppleTV" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
<document type="com.apple.InterfaceBuilder.AppleTV.Storyboard" version="3.0" toolsVersion="19115.3" targetRuntime="AppleTV" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
<device id="appleTV" appearance="light"/>
|
<device id="appleTV" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
|
<deployment identifier="tvOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19107.5"/>
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
@ -38,6 +39,9 @@
|
||||||
<rect key="frame" x="0.0" y="0.0" width="1920" height="1080"/>
|
<rect key="frame" x="0.0" y="0.0" width="1920" height="1080"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<viewLayoutGuide key="safeArea" id="jNU-Xf-Kyx"/>
|
<viewLayoutGuide key="safeArea" id="jNU-Xf-Kyx"/>
|
||||||
|
<accessibility key="accessibilityConfiguration">
|
||||||
|
<accessibilityTraits key="traits" notEnabled="YES"/>
|
||||||
|
</accessibility>
|
||||||
</view>
|
</view>
|
||||||
<view hidden="YES" contentMode="scaleToFill" id="OG6-kk-N7Z" userLabel="Controls">
|
<view hidden="YES" contentMode="scaleToFill" id="OG6-kk-N7Z" userLabel="Controls">
|
||||||
<rect key="frame" x="-1" y="0.0" width="1920" height="1080"/>
|
<rect key="frame" x="-1" y="0.0" width="1920" height="1080"/>
|
||||||
|
@ -82,6 +86,9 @@
|
||||||
</view>
|
</view>
|
||||||
</subviews>
|
</subviews>
|
||||||
<viewLayoutGuide key="safeArea" id="IS7-IU-teh"/>
|
<viewLayoutGuide key="safeArea" id="IS7-IU-teh"/>
|
||||||
|
<accessibility key="accessibilityConfiguration">
|
||||||
|
<accessibilityTraits key="traits" allowsDirectInteraction="YES"/>
|
||||||
|
</accessibility>
|
||||||
</view>
|
</view>
|
||||||
<containerView opaque="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="lie-K8-LNT">
|
<containerView opaque="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="lie-K8-LNT">
|
||||||
<rect key="frame" x="88" y="87" width="1744" height="635"/>
|
<rect key="frame" x="88" y="87" width="1744" height="635"/>
|
||||||
|
|
|
@ -49,16 +49,8 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
|
||||||
var lastTime: Float = 0.0
|
var lastTime: Float = 0.0
|
||||||
var startTime: Int = 0
|
var startTime: Int = 0
|
||||||
|
|
||||||
var selectedAudioTrack: Int32 = -1 {
|
var selectedAudioTrack: Int32 = -1
|
||||||
didSet {
|
var selectedCaptionTrack: Int32 = -1
|
||||||
print(selectedAudioTrack)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var selectedCaptionTrack: Int32 = -1 {
|
|
||||||
didSet {
|
|
||||||
print(selectedCaptionTrack)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var subtitleTrackArray: [Subtitle] = []
|
var subtitleTrackArray: [Subtitle] = []
|
||||||
var audioTrackArray: [AudioTrack] = []
|
var audioTrackArray: [AudioTrack] = []
|
||||||
|
@ -86,7 +78,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
|
||||||
// Check if focused on the tab bar, allows for swipe up to dismiss the info panel
|
// Check if focused on the tab bar, allows for swipe up to dismiss the info panel
|
||||||
if context.nextFocusedView!.description.contains("UITabBarButton") {
|
if context.nextFocusedView!.description.contains("UITabBarButton") {
|
||||||
// Set value after half a second so info panel is not dismissed instantly when swiping up from content
|
// Set value after half a second so info panel is not dismissed instantly when swiping up from content
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
|
||||||
self.focusedOnTabBar = true
|
self.focusedOnTabBar = true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -103,6 +95,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
|
||||||
|
|
||||||
mediaPlayer.delegate = self
|
mediaPlayer.delegate = self
|
||||||
mediaPlayer.drawable = videoContentView
|
mediaPlayer.drawable = videoContentView
|
||||||
|
mediaPlayer.libraryInstance.debugLogging = true;
|
||||||
|
|
||||||
if let runTimeTicks = manifest.runTimeTicks {
|
if let runTimeTicks = manifest.runTimeTicks {
|
||||||
videoDuration = Double(runTimeTicks / 10_000_000)
|
videoDuration = Double(runTimeTicks / 10_000_000)
|
||||||
|
@ -132,9 +125,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
|
||||||
transportBarView.layer.cornerRadius = CGFloat(5)
|
transportBarView.layer.cornerRadius = CGFloat(5)
|
||||||
|
|
||||||
setupGestures()
|
setupGestures()
|
||||||
|
|
||||||
fetchVideo()
|
fetchVideo()
|
||||||
|
|
||||||
setupNowPlayingCC()
|
setupNowPlayingCC()
|
||||||
|
|
||||||
// Adjust subtitle size
|
// Adjust subtitle size
|
||||||
|
@ -321,13 +312,6 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// commandCenter.enableLanguageOptionCommand.addTarget { [weak self](remoteEvent) in
|
|
||||||
// guard let self = self else {return .commandFailed}
|
|
||||||
//
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
|
|
||||||
var runTicks = 0
|
var runTicks = 0
|
||||||
var playbackTicks = 0
|
var playbackTicks = 0
|
||||||
|
|
||||||
|
@ -376,12 +360,11 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grabs a refference to the info panel view controller
|
// Grabs a reference to the info panel view controller
|
||||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||||||
if segue.identifier == "infoView" {
|
if segue.identifier == "infoView" {
|
||||||
containerViewController = segue.destination as? InfoTabBarViewController
|
containerViewController = segue.destination as? InfoTabBarViewController
|
||||||
containerViewController?.videoPlayer = self
|
containerViewController?.videoPlayer = self
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -403,7 +386,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
|
||||||
self.sendProgressReport(eventName: "pause")
|
self.sendProgressReport(eventName: "pause")
|
||||||
|
|
||||||
self.updateNowPlayingCenter(time: nil, playing: false)
|
self.updateNowPlayingCenter(time: nil, playing: false)
|
||||||
|
self.toggleInfoContainer()
|
||||||
animateScrubber()
|
animateScrubber()
|
||||||
|
|
||||||
self.scrubLabel.frame = CGRect(x: self.scrubberView.frame.minX - self.scrubLabel.frame.width/2, y: self.scrubLabel.frame.minY, width: self.scrubLabel.frame.width, height: self.scrubLabel.frame.height)
|
self.scrubLabel.frame = CGRect(x: self.scrubberView.frame.minX - self.scrubLabel.frame.width/2, y: self.scrubLabel.frame.minY, width: self.scrubLabel.frame.width, height: self.scrubLabel.frame.height)
|
||||||
|
@ -414,9 +397,8 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
|
||||||
mediaPlayer.play()
|
mediaPlayer.play()
|
||||||
|
|
||||||
self.updateNowPlayingCenter(time: nil, playing: true)
|
self.updateNowPlayingCenter(time: nil, playing: true)
|
||||||
|
self.toggleInfoContainer()
|
||||||
self.sendProgressReport(eventName: "unpause")
|
self.sendProgressReport(eventName: "unpause")
|
||||||
|
|
||||||
animateScrubber()
|
animateScrubber()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -463,15 +445,6 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
|
||||||
|
|
||||||
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.userPanned(panGestureRecognizer:)))
|
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.userPanned(panGestureRecognizer:)))
|
||||||
view.addGestureRecognizer(panGestureRecognizer)
|
view.addGestureRecognizer(panGestureRecognizer)
|
||||||
|
|
||||||
let swipeRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(self.swipe(swipe:)))
|
|
||||||
swipeRecognizer.direction = .right
|
|
||||||
view.addGestureRecognizer(swipeRecognizer)
|
|
||||||
|
|
||||||
let swipeRecognizerl = UISwipeGestureRecognizer(target: self, action: #selector(self.swipe(swipe:)))
|
|
||||||
swipeRecognizerl.direction = .left
|
|
||||||
view.addGestureRecognizer(swipeRecognizerl)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func backButtonPressed(tap: UITapGestureRecognizer) {
|
@objc func backButtonPressed(tap: UITapGestureRecognizer) {
|
||||||
|
@ -509,7 +482,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
|
||||||
let velocity = panGestureRecognizer.velocity(in: view)
|
let velocity = panGestureRecognizer.velocity(in: view)
|
||||||
|
|
||||||
// Swiped up - Handle dismissing info panel
|
// Swiped up - Handle dismissing info panel
|
||||||
if translation.y < -700 && (focusedOnTabBar && showingInfoPanel) {
|
if translation.y < -400 && (focusedOnTabBar && showingInfoPanel) {
|
||||||
toggleInfoContainer()
|
toggleInfoContainer()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -519,7 +492,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Swiped down - Show the info panel
|
// Swiped down - Show the info panel
|
||||||
if translation.y > 700 {
|
if translation.y > 400 {
|
||||||
toggleInfoContainer()
|
toggleInfoContainer()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -549,40 +522,13 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not currently used
|
|
||||||
@objc func swipe(swipe: UISwipeGestureRecognizer!) {
|
|
||||||
print("swiped")
|
|
||||||
switch swipe.direction {
|
|
||||||
case .left:
|
|
||||||
print("swiped left")
|
|
||||||
// mediaPlayer.pause()
|
|
||||||
// player.seek(to: CMTime(value: Int64(self.currentSeconds) + 10, timescale: 1))
|
|
||||||
// mediaPlayer.play()
|
|
||||||
case .right:
|
|
||||||
print("swiped right")
|
|
||||||
// mediaPlayer.pause()
|
|
||||||
// player.seek(to: CMTime(value: Int64(self.currentSeconds) + 10, timescale: 1))
|
|
||||||
// mediaPlayer.play()
|
|
||||||
case .up:
|
|
||||||
break
|
|
||||||
case .down:
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Play/Pause or Select is pressed on the AppleTV remote
|
/// Play/Pause or Select is pressed on the AppleTV remote
|
||||||
@objc func selectButtonTapped() {
|
@objc func selectButtonTapped() {
|
||||||
|
print("select")
|
||||||
if loading {
|
if loading {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
showingControls = true
|
|
||||||
controlsView.isHidden = false
|
|
||||||
controlsAppearTime = CACurrentMediaTime()
|
|
||||||
|
|
||||||
// Move to seeked position
|
// Move to seeked position
|
||||||
if seeking {
|
if seeking {
|
||||||
scrubLabel.isHidden = true
|
scrubLabel.isHidden = true
|
||||||
|
@ -787,12 +733,6 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
|
||||||
return text.hasPrefix("0") && text.count > 4 ?
|
return text.hasPrefix("0") && text.count > 4 ?
|
||||||
.init(text.dropFirst()) : text
|
.init(text.dropFirst()) : text
|
||||||
}
|
}
|
||||||
|
|
||||||
// When VLC video starts playing a real device can no longer receive gesture recognisers, adding this in hopes to fix the issue but no luck
|
|
||||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
||||||
print("recognisesimultaneousvideoplayer")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Comparable {
|
extension Comparable {
|
||||||
|
|
|
@ -22,6 +22,8 @@
|
||||||
5310695B2684E7EE00CFFDBA /* AudioView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531069542684E7EE00CFFDBA /* AudioView.swift */; };
|
5310695B2684E7EE00CFFDBA /* AudioView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531069542684E7EE00CFFDBA /* AudioView.swift */; };
|
||||||
5310695C2684E7EE00CFFDBA /* VideoPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531069552684E7EE00CFFDBA /* VideoPlayerViewController.swift */; };
|
5310695C2684E7EE00CFFDBA /* VideoPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531069552684E7EE00CFFDBA /* VideoPlayerViewController.swift */; };
|
||||||
5310695D2684E7EE00CFFDBA /* VideoPlayer.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 531069562684E7EE00CFFDBA /* VideoPlayer.storyboard */; };
|
5310695D2684E7EE00CFFDBA /* VideoPlayer.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 531069562684E7EE00CFFDBA /* VideoPlayer.storyboard */; };
|
||||||
|
53116A17268B919A003024C9 /* SeriesItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53116A16268B919A003024C9 /* SeriesItemView.swift */; };
|
||||||
|
53116A19268B947A003024C9 /* PlainLinkButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53116A18268B947A003024C9 /* PlainLinkButton.swift */; };
|
||||||
531690E5267ABD5C005D8AB9 /* MainTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690E4267ABD5C005D8AB9 /* MainTabView.swift */; };
|
531690E5267ABD5C005D8AB9 /* MainTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690E4267ABD5C005D8AB9 /* MainTabView.swift */; };
|
||||||
531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690E6267ABD79005D8AB9 /* HomeView.swift */; };
|
531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690E6267ABD79005D8AB9 /* HomeView.swift */; };
|
||||||
531690ED267ABF46005D8AB9 /* ContinueWatchingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690EB267ABF46005D8AB9 /* ContinueWatchingView.swift */; };
|
531690ED267ABF46005D8AB9 /* ContinueWatchingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690EB267ABF46005D8AB9 /* ContinueWatchingView.swift */; };
|
||||||
|
@ -218,6 +220,8 @@
|
||||||
531069542684E7EE00CFFDBA /* AudioView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioView.swift; sourceTree = "<group>"; };
|
531069542684E7EE00CFFDBA /* AudioView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioView.swift; sourceTree = "<group>"; };
|
||||||
531069552684E7EE00CFFDBA /* VideoPlayerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewController.swift; sourceTree = "<group>"; };
|
531069552684E7EE00CFFDBA /* VideoPlayerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewController.swift; sourceTree = "<group>"; };
|
||||||
531069562684E7EE00CFFDBA /* VideoPlayer.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = VideoPlayer.storyboard; sourceTree = "<group>"; };
|
531069562684E7EE00CFFDBA /* VideoPlayer.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = VideoPlayer.storyboard; sourceTree = "<group>"; };
|
||||||
|
53116A16268B919A003024C9 /* SeriesItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeriesItemView.swift; sourceTree = "<group>"; };
|
||||||
|
53116A18268B947A003024C9 /* PlainLinkButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlainLinkButton.swift; sourceTree = "<group>"; };
|
||||||
531690E4267ABD5C005D8AB9 /* MainTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabView.swift; sourceTree = "<group>"; };
|
531690E4267ABD5C005D8AB9 /* MainTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabView.swift; sourceTree = "<group>"; };
|
||||||
531690E6267ABD79005D8AB9 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
|
531690E6267ABD79005D8AB9 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
|
||||||
531690EB267ABF46005D8AB9 /* ContinueWatchingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueWatchingView.swift; sourceTree = "<group>"; };
|
531690EB267ABF46005D8AB9 /* ContinueWatchingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueWatchingView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -458,6 +462,7 @@
|
||||||
53A83C32268A309300DF3D92 /* LibraryView.swift */,
|
53A83C32268A309300DF3D92 /* LibraryView.swift */,
|
||||||
53CD2A3F268A49C2002ABD4E /* ItemView.swift */,
|
53CD2A3F268A49C2002ABD4E /* ItemView.swift */,
|
||||||
53CD2A41268A4B38002ABD4E /* MovieItemView.swift */,
|
53CD2A41268A4B38002ABD4E /* MovieItemView.swift */,
|
||||||
|
53116A16268B919A003024C9 /* SeriesItemView.swift */,
|
||||||
);
|
);
|
||||||
path = "JellyfinPlayer tvOS";
|
path = "JellyfinPlayer tvOS";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -497,6 +502,7 @@
|
||||||
531690F6267ACC00005D8AB9 /* LandscapeItemElement.swift */,
|
531690F6267ACC00005D8AB9 /* LandscapeItemElement.swift */,
|
||||||
536D3D80267BDFC60004248C /* PortraitItemElement.swift */,
|
536D3D80267BDFC60004248C /* PortraitItemElement.swift */,
|
||||||
536D3D87267C17350004248C /* PublicUserButton.swift */,
|
536D3D87267C17350004248C /* PublicUserButton.swift */,
|
||||||
|
53116A18268B947A003024C9 /* PlainLinkButton.swift */,
|
||||||
);
|
);
|
||||||
path = Components;
|
path = Components;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -950,11 +956,13 @@
|
||||||
6267B3DC2671139500A7371D /* ImageExtensions.swift in Sources */,
|
6267B3DC2671139500A7371D /* ImageExtensions.swift in Sources */,
|
||||||
531069592684E7EE00CFFDBA /* SubtitlesView.swift in Sources */,
|
531069592684E7EE00CFFDBA /* SubtitlesView.swift in Sources */,
|
||||||
53ABFDE9267974EF00886593 /* HomeViewModel.swift in Sources */,
|
53ABFDE9267974EF00886593 /* HomeViewModel.swift in Sources */,
|
||||||
|
53116A17268B919A003024C9 /* SeriesItemView.swift in Sources */,
|
||||||
62EC352D26766675000E9F2D /* ServerEnvironment.swift in Sources */,
|
62EC352D26766675000E9F2D /* ServerEnvironment.swift in Sources */,
|
||||||
531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */,
|
531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */,
|
||||||
53ABFDDE267974E300886593 /* SplashView.swift in Sources */,
|
53ABFDDE267974E300886593 /* SplashView.swift in Sources */,
|
||||||
53ABFDE8267974EF00886593 /* SplashViewModel.swift in Sources */,
|
53ABFDE8267974EF00886593 /* SplashViewModel.swift in Sources */,
|
||||||
62E632DE267D2E170063E547 /* LatestMediaViewModel.swift in Sources */,
|
62E632DE267D2E170063E547 /* LatestMediaViewModel.swift in Sources */,
|
||||||
|
53116A19268B947A003024C9 /* PlainLinkButton.swift in Sources */,
|
||||||
536D3D88267C17350004248C /* PublicUserButton.swift in Sources */,
|
536D3D88267C17350004248C /* PublicUserButton.swift in Sources */,
|
||||||
62E632EA267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */,
|
62E632EA267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */,
|
||||||
53CD2A40268A49C2002ABD4E /* ItemView.swift in Sources */,
|
53CD2A40268A49C2002ABD4E /* ItemView.swift in Sources */,
|
||||||
|
|
|
@ -57,7 +57,7 @@ struct ContinueWatchingView: View {
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
if item.type == "Episode" {
|
if item.type == "Episode" {
|
||||||
Text("• S\(String(item.parentIndexNumber ?? 0)):E\(String(item.indexNumber ?? 0)) - \(item.name ?? "")")
|
Text("• \(item.getEpisodeLocator()) - \(item.name ?? "")")
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
|
|
@ -103,7 +103,7 @@ struct SeasonItemView: View {
|
||||||
.opacity(1), alignment: .topTrailing).opacity(1)
|
.opacity(1), alignment: .topTrailing).opacity(1)
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
HStack {
|
HStack {
|
||||||
Text("S\(String(episode.parentIndexNumber ?? 0)):E\(String(episode.indexNumber ?? 0))").font(.subheadline)
|
Text(episode.getEpisodeLocator()).font(.subheadline)
|
||||||
.fontWeight(.medium)
|
.fontWeight(.medium)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
|
|
|
@ -15,13 +15,6 @@ class UpNextViewModel: ObservableObject {
|
||||||
@Published var item: BaseItemDto? = nil
|
@Published var item: BaseItemDto? = nil
|
||||||
var delegate: PlayerViewController?
|
var delegate: PlayerViewController?
|
||||||
|
|
||||||
func getEpisodeLocator() -> String {
|
|
||||||
if let seasonNo = item?.parentIndexNumber, let episodeNo = item?.indexNumber {
|
|
||||||
return "S\(seasonNo):E\(episodeNo)"
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func nextUp() {
|
func nextUp() {
|
||||||
if delegate != nil {
|
if delegate != nil {
|
||||||
delegate?.setPlayerToNextUp()
|
delegate?.setPlayerToNextUp()
|
||||||
|
@ -43,7 +36,7 @@ struct VideoUpNextView: View {
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
Text(viewModel.getEpisodeLocator())
|
Text(viewModel.item.getEpisodeLocator())
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,6 +73,13 @@ extension BaseItemDto {
|
||||||
let urlString = "\(ServerEnvironment.current.server.baseURI!)/Items/\(imageItemId)/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)"
|
let urlString = "\(ServerEnvironment.current.server.baseURI!)/Items/\(imageItemId)/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)"
|
||||||
return URL(string: urlString)!
|
return URL(string: urlString)!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getEpisodeLocator() -> String {
|
||||||
|
if let seasonNo = self.parentIndexNumber, let episodeNo = self.indexNumber {
|
||||||
|
return "S\(seasonNo):E\(episodeNo)"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func getSeriesBackdropImage(maxWidth: Int) -> URL {
|
func getSeriesBackdropImage(maxWidth: Int) -> URL {
|
||||||
let imageType = "Backdrop"
|
let imageType = "Backdrop"
|
||||||
|
@ -104,6 +111,7 @@ extension BaseItemDto {
|
||||||
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
|
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
|
||||||
|
|
||||||
let urlString = "\(ServerEnvironment.current.server.baseURI!)/Items/\(imageItemId)/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)"
|
let urlString = "\(ServerEnvironment.current.server.baseURI!)/Items/\(imageItemId)/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)"
|
||||||
|
//print(urlString)
|
||||||
return URL(string: urlString)!
|
return URL(string: urlString)!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -61,7 +61,9 @@ final class SessionManager {
|
||||||
#else
|
#else
|
||||||
header.append("Client=\"SwiftFin iOS\", ")
|
header.append("Client=\"SwiftFin iOS\", ")
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
header.append("Device=\"\(deviceName)\", ")
|
header.append("Device=\"\(deviceName)\", ")
|
||||||
|
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
header.append("DeviceId=\"tvOS_\(UIDevice.current.identifierForVendor!.uuidString)_\(user?.user_id ?? "")\", ")
|
header.append("DeviceId=\"tvOS_\(UIDevice.current.identifierForVendor!.uuidString)_\(user?.user_id ?? "")\", ")
|
||||||
deviceID = "tvOS_\(UIDevice.current.identifierForVendor!.uuidString)_\(user?.user_id ?? "")"
|
deviceID = "tvOS_\(UIDevice.current.identifierForVendor!.uuidString)_\(user?.user_id ?? "")"
|
||||||
|
|
|
@ -12,19 +12,30 @@ import Foundation
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
|
|
||||||
class DetailItemViewModel: ViewModel {
|
class DetailItemViewModel: ViewModel {
|
||||||
@Published
|
@Published var item: BaseItemDto
|
||||||
var item: BaseItemDto
|
@Published var similarItems: [BaseItemDto] = []
|
||||||
|
|
||||||
@Published
|
@Published var isWatched = false
|
||||||
var isWatched = false
|
@Published var isFavorited = false
|
||||||
@Published
|
|
||||||
var isFavorited = false
|
|
||||||
|
|
||||||
init(item: BaseItemDto) {
|
init(item: BaseItemDto) {
|
||||||
self.item = item
|
self.item = item
|
||||||
isFavorited = item.userData?.isFavorite ?? false
|
isFavorited = item.userData?.isFavorite ?? false
|
||||||
isWatched = item.userData?.played ?? false
|
isWatched = item.userData?.played ?? false
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
|
getRelatedItems()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRelatedItems() {
|
||||||
|
LibraryAPI.getSimilarItems(itemId: item.id!, userId: SessionManager.current.user.user_id!, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people])
|
||||||
|
.trackActivity(loading)
|
||||||
|
.sink(receiveCompletion: { [weak self] completion in
|
||||||
|
self?.handleAPIRequestCompletion(completion: completion)
|
||||||
|
}, receiveValue: { [weak self] response in
|
||||||
|
self?.similarItems = response.items ?? []
|
||||||
|
})
|
||||||
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateWatchState() {
|
func updateWatchState() {
|
||||||
|
|
|
@ -11,17 +11,13 @@ import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
|
|
||||||
final class SeasonItemViewModel: ViewModel {
|
final class SeasonItemViewModel: DetailItemViewModel {
|
||||||
@Published
|
@Published var episodes = [BaseItemDto]()
|
||||||
var item: BaseItemDto
|
|
||||||
|
|
||||||
@Published
|
override init(item: BaseItemDto) {
|
||||||
var episodes = [BaseItemDto]()
|
super.init(item: item)
|
||||||
|
|
||||||
init(item: BaseItemDto) {
|
|
||||||
self.item = item
|
self.item = item
|
||||||
super.init()
|
|
||||||
|
|
||||||
requestEpisodes()
|
requestEpisodes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,18 +11,45 @@ import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
|
|
||||||
final class SeriesItemViewModel: ViewModel {
|
final class SeriesItemViewModel: DetailItemViewModel {
|
||||||
@Published
|
@Published var seasons = [BaseItemDto]()
|
||||||
var item: BaseItemDto
|
@Published var nextUpItem: BaseItemDto?
|
||||||
|
|
||||||
@Published
|
override init(item: BaseItemDto) {
|
||||||
var seasons = [BaseItemDto]()
|
super.init(item: item)
|
||||||
|
|
||||||
init(item: BaseItemDto) {
|
|
||||||
self.item = item
|
self.item = item
|
||||||
super.init()
|
|
||||||
|
|
||||||
requestSeasons()
|
requestSeasons()
|
||||||
|
getNextUp()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getNextUp() {
|
||||||
|
TvShowsAPI.getNextUp(userId: SessionManager.current.user.user_id!, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], seriesId: self.item.id!, enableUserData: true)
|
||||||
|
.trackActivity(loading)
|
||||||
|
.sink(receiveCompletion: { [weak self] completion in
|
||||||
|
self?.handleAPIRequestCompletion(completion: completion)
|
||||||
|
}, receiveValue: { [weak self] response in
|
||||||
|
self?.nextUpItem = response.items?.first ?? nil
|
||||||
|
})
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRunYears() -> String {
|
||||||
|
let dateFormatter = DateFormatter()
|
||||||
|
dateFormatter.dateFormat = "yyyy"
|
||||||
|
|
||||||
|
var startYear: String? = nil
|
||||||
|
var endYear: String? = nil
|
||||||
|
|
||||||
|
if(item.premiereDate != nil) {
|
||||||
|
startYear = dateFormatter.string(from: item.premiereDate!)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(item.endDate != nil) {
|
||||||
|
endYear = dateFormatter.string(from: item.endDate!)
|
||||||
|
}
|
||||||
|
|
||||||
|
return "\(startYear ?? "Unknown") - \(endYear ?? "Present")"
|
||||||
}
|
}
|
||||||
|
|
||||||
func requestSeasons() {
|
func requestSeasons() {
|
||||||
|
|
Loading…
Reference in New Issue