Merge branch 'main' into serverDiscovery

This commit is contained in:
Stephen Byatt 2021-06-24 17:03:29 +10:00 committed by GitHub
commit 2dd60b6ce5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 253 additions and 210 deletions

View File

@ -1022,7 +1022,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "JellyfinPlayer tvOS/JellyfinPlayer tvOS.entitlements"; CODE_SIGN_ENTITLEMENTS = "JellyfinPlayer tvOS/JellyfinPlayer tvOS.entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 49; CURRENT_PROJECT_VERSION = 53;
DEVELOPMENT_ASSET_PATHS = "\"JellyfinPlayer tvOS/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"JellyfinPlayer tvOS/Preview Content\"";
DEVELOPMENT_TEAM = 9R8RREG67J; DEVELOPMENT_TEAM = 9R8RREG67J;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@ -1050,7 +1050,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "JellyfinPlayer tvOS/JellyfinPlayer tvOS.entitlements"; CODE_SIGN_ENTITLEMENTS = "JellyfinPlayer tvOS/JellyfinPlayer tvOS.entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 49; CURRENT_PROJECT_VERSION = 53;
DEVELOPMENT_ASSET_PATHS = "\"JellyfinPlayer tvOS/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"JellyfinPlayer tvOS/Preview Content\"";
DEVELOPMENT_TEAM = 9R8RREG67J; DEVELOPMENT_TEAM = 9R8RREG67J;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@ -1199,7 +1199,7 @@
CODE_SIGN_ENTITLEMENTS = JellyfinPlayer/JellyfinPlayer.entitlements; CODE_SIGN_ENTITLEMENTS = JellyfinPlayer/JellyfinPlayer.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 49; CURRENT_PROJECT_VERSION = 53;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = 9R8RREG67J; DEVELOPMENT_TEAM = 9R8RREG67J;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
@ -1233,7 +1233,7 @@
CODE_SIGN_ENTITLEMENTS = JellyfinPlayer/JellyfinPlayer.entitlements; CODE_SIGN_ENTITLEMENTS = JellyfinPlayer/JellyfinPlayer.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 49; CURRENT_PROJECT_VERSION = 53;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = 9R8RREG67J; DEVELOPMENT_TEAM = 9R8RREG67J;
@ -1265,7 +1265,7 @@
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements; CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 49; CURRENT_PROJECT_VERSION = 53;
DEVELOPMENT_TEAM = 9R8RREG67J; DEVELOPMENT_TEAM = 9R8RREG67J;
INFOPLIST_FILE = WidgetExtension/Info.plist; INFOPLIST_FILE = WidgetExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1; IPHONEOS_DEPLOYMENT_TARGET = 14.1;
@ -1290,7 +1290,7 @@
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements; CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 49; CURRENT_PROJECT_VERSION = 53;
DEVELOPMENT_TEAM = 9R8RREG67J; DEVELOPMENT_TEAM = 9R8RREG67J;
INFOPLIST_FILE = WidgetExtension/Info.plist; INFOPLIST_FILE = WidgetExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1; IPHONEOS_DEPLOYMENT_TARGET = 14.1;

View File

@ -36,14 +36,13 @@ struct ContinueWatchingView: View {
var body: some View { var body: some View {
ScrollView(.horizontal, showsIndicators: false) { ScrollView(.horizontal, showsIndicators: false) {
LazyHStack { LazyHStack {
Spacer().frame(width: 14)
ForEach(items, id: \.id) { item in ForEach(items, id: \.id) { item in
NavigationLink(destination: ItemView(item: item)) { NavigationLink(destination: LazyView { ItemView(item: item) }) {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Spacer().frame(height: 10)
ImageView(src: item.getBackdropImage(maxWidth: 320), bh: item.getBackdropImageBlurHash()) ImageView(src: item.getBackdropImage(maxWidth: 320), bh: item.getBackdropImageBlurHash())
.frame(width: 320, height: 180) .frame(width: 320, height: 180)
.cornerRadius(10) .cornerRadius(10)
.shadow(radius: 4)
.overlay( .overlay(
Group { Group {
if item.type == "Episode" { if item.type == "Episode" {
@ -70,14 +69,12 @@ struct ContinueWatchingView: View {
.foregroundColor(.primary) .foregroundColor(.primary)
.lineLimit(1) .lineLimit(1)
.frame(width: 320, alignment: .leading) .frame(width: 320, alignment: .leading)
Spacer().frame(height: 5) }.padding(.top, 10)
} .padding(.bottom, 5)
} }
Spacer().frame(width: 16) }.padding(.trailing, 16)
}
Spacer().frame(width: 2)
}.frame(height: 215) }.frame(height: 215)
.padding(.bottom, 10) .padding(EdgeInsets(top: 8, leading: 20, bottom: 10, trailing: 2))
} }
} }
} }

View File

@ -203,7 +203,7 @@ struct EpisodeItemView: View {
self.playbackInfo.shouldShowPlayer = true self.playbackInfo.shouldShowPlayer = true
} label: { } label: {
HStack { HStack {
Text(viewModel.item.getItemProgressString() == "" ? "Play" : "\(viewModel.item.getItemProgressString()) left") Text(viewModel.item.getItemProgressString() == "" ? "Play" : viewModel.item.getItemProgressString())
.foregroundColor(Color.white).font(.callout).fontWeight(.semibold) .foregroundColor(Color.white).font(.callout).fontWeight(.semibold)
Image(systemName: "play.fill").foregroundColor(Color.white).font(.system(size: 20)) Image(systemName: "play.fill").foregroundColor(Color.white).font(.system(size: 20))
} }

View File

@ -12,8 +12,6 @@ import SwiftUI
struct HomeView: View { struct HomeView: View {
@StateObject var viewModel = HomeViewModel() @StateObject var viewModel = HomeViewModel()
@Environment(\.horizontalSizeClass) var hSizeClass
@Environment(\.verticalSizeClass) var vSizeClass
@State var showingSettings = false @State var showingSettings = false
@ViewBuilder @ViewBuilder
@ -25,36 +23,32 @@ struct HomeView: View {
LazyVStack(alignment: .leading) { LazyVStack(alignment: .leading) {
if !viewModel.resumeItems.isEmpty { if !viewModel.resumeItems.isEmpty {
ContinueWatchingView(items: viewModel.resumeItems) ContinueWatchingView(items: viewModel.resumeItems)
.padding(.top, hSizeClass == .compact && vSizeClass == .regular ? 0 : 16)
} }
if !viewModel.nextUpItems.isEmpty { if !viewModel.nextUpItems.isEmpty {
NextUpView(items: viewModel.nextUpItems) NextUpView(items: viewModel.nextUpItems)
} }
if !viewModel.librariesShowRecentlyAddedIDs.isEmpty { if !viewModel.librariesShowRecentlyAddedIDs.isEmpty {
ForEach(viewModel.librariesShowRecentlyAddedIDs, id: \.self) { libraryID in ForEach(viewModel.librariesShowRecentlyAddedIDs, id: \.self) { libraryID in
VStack(alignment: .leading) { let library = viewModel.libraries.first(where: { $0.id == libraryID })
let library = viewModel.libraries.first(where: { $0.id == libraryID }) HStack {
HStack { Text("Latest \(library?.name ?? "")")
Text("Latest \(library?.name ?? "")") .font(.title2)
.font(.title2) .fontWeight(.bold)
.fontWeight(.bold) Spacer()
.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 16)) NavigationLink(destination: LazyView {
Spacer() LibraryView(viewModel: .init(parentID: libraryID, filters: viewModel.recentFilterSet), title: library?.name ?? "")
NavigationLink(destination: LazyView { }) {
LibraryView(viewModel: .init(parentID: libraryID, filters: viewModel.recentFilterSet), title: library?.name ?? "") HStack {
}) { Text("See All").font(.subheadline).fontWeight(.bold)
HStack { Image(systemName: "chevron.right").font(Font.subheadline.bold())
Text("See All").font(.subheadline).fontWeight(.bold)
Image(systemName: "chevron.right").font(Font.subheadline.bold())
}
} }
}.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)) }
LatestMediaView(viewModel: .init(libraryID: libraryID)) }.padding(.leading, 16)
}.padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0)) .padding(.trailing, 16)
LatestMediaView(viewModel: .init(libraryID: libraryID))
} }
} }
} }
.padding(.top, hSizeClass == .compact && vSizeClass == .regular ? 0 : 16)
.padding(.bottom, UIDevice.current.userInterfaceIdiom == .phone ? 20 : 30) .padding(.bottom, UIDevice.current.userInterfaceIdiom == .phone ? 20 : 30)
} }
} }
@ -63,6 +57,7 @@ struct HomeView: View {
var body: some View { var body: some View {
innerBody innerBody
.navigationTitle(MainTabView.Tab.home.localized) .navigationTitle(MainTabView.Tab.home.localized)
/*
.toolbar { .toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) { ToolbarItemGroup(placement: .navigationBarTrailing) {
Button { Button {
@ -75,5 +70,6 @@ struct HomeView: View {
.fullScreenCover(isPresented: $showingSettings) { .fullScreenCover(isPresented: $showingSettings) {
SettingsView(viewModel: SettingsViewModel(), close: $showingSettings) SettingsView(viewModel: SettingsViewModel(), close: $showingSettings)
} }
*/
} }
} }

View File

@ -34,7 +34,7 @@ struct ItemView: View {
.statusBar(hidden: true) .statusBar(hidden: true)
.edgesIgnoringSafeArea(.all) .edgesIgnoringSafeArea(.all)
.prefersHomeIndicatorAutoHidden(true) .prefersHomeIndicatorAutoHidden(true)
}, isActive: $videoPlayerItem.shouldShowPlayer) { }.supportedOrientations(.landscape), isActive: $videoPlayerItem.shouldShowPlayer) {
EmptyView() EmptyView()
} }
VStack { VStack {

View File

@ -103,10 +103,10 @@ class PreferenceUIHostingController: UIHostingController<AnyView> {
public var _orientations: UIInterfaceOrientationMask = .allButUpsideDown { public var _orientations: UIInterfaceOrientationMask = .allButUpsideDown {
didSet { didSet {
UIViewController.attemptRotationToDeviceOrientation()
if _orientations == .landscape { if _orientations == .landscape {
let value = UIInterfaceOrientation.landscapeRight.rawValue let value = UIInterfaceOrientation.landscapeRight.rawValue
UIDevice.current.setValue(value, forKey: "orientation") UIDevice.current.setValue(value, forKey: "orientation")
UIViewController.attemptRotationToDeviceOrientation()
} }
} }
} }

View File

@ -8,21 +8,19 @@
import SwiftUI import SwiftUI
struct LatestMediaView: View { struct LatestMediaView: View {
@ObservedObject var viewModel: LatestMediaViewModel @StateObject var viewModel: LatestMediaViewModel
var body: some View { var body: some View {
ScrollView(.horizontal, showsIndicators: false) { ScrollView(.horizontal, showsIndicators: false) {
LazyHStack { LazyHStack {
Spacer().frame(width: 16)
ForEach(viewModel.items, id: \.id) { item in ForEach(viewModel.items, id: \.id) { item in
if item.type == "Series" || item.type == "Movie" { if item.type == "Series" || item.type == "Movie" {
NavigationLink(destination: ItemView(item: item)) { NavigationLink(destination: LazyView { ItemView(item: item) }) {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Spacer().frame(height: 10)
ImageView(src: item.getPrimaryImage(maxWidth: 100), bh: item.getPrimaryImageBlurHash()) ImageView(src: item.getPrimaryImage(maxWidth: 100), bh: item.getPrimaryImageBlurHash())
.frame(width: 100, height: 150) .frame(width: 100, height: 150)
.cornerRadius(10) .cornerRadius(10)
Spacer().frame(height: 5) .shadow(radius: 4)
Text(item.seriesName ?? item.name ?? "") Text(item.seriesName ?? item.name ?? "")
.font(.caption) .font(.caption)
.fontWeight(.semibold) .fontWeight(.semibold)
@ -35,15 +33,15 @@ struct LatestMediaView: View {
.fontWeight(.medium) .fontWeight(.medium)
} else { } else {
Text(item.type!) Text(item.type!)
.foregroundColor(.secondary)
.font(.caption)
.fontWeight(.medium)
} }
}.frame(width: 100) }.frame(width: 100)
Spacer().frame(width: 15)
} }
} }
} }.padding(.trailing, 16)
} }.padding(.leading, 20)
.frame(height: 190) }.frame(height: 195)
}
.padding(EdgeInsets(top: -2, leading: 0, bottom: 0, trailing: 0)).frame(height: 190)
} }
} }

View File

@ -12,27 +12,78 @@ struct LibraryListView: View {
@StateObject var viewModel = LibraryListViewModel() @StateObject var viewModel = LibraryListViewModel()
var body: some View { var body: some View {
List(viewModel.libraries, id: \.self) { library in ScrollView {
switch library.id { LazyVStack() {
case "favorites":
NavigationLink(destination: LazyView { NavigationLink(destination: LazyView {
LibraryView(viewModel: .init(filters: viewModel.withFavorites), title: library.name ?? "") LibraryView(viewModel: .init(filters: viewModel.withFavorites), title: "Favorites")
}) { }) {
Text(library.name ?? "") ZStack() {
HStack() {
Spacer()
Text("Your Favorites")
.foregroundColor(.black)
.font(.subheadline)
.fontWeight(.semibold)
Spacer()
}
}
.padding(16)
.background(Color.white)
.frame(minWidth: 100, maxWidth: .infinity)
} }
case "genres": .cornerRadius(10)
.shadow(radius: 5)
.padding(.bottom, 5)
NavigationLink(destination: LazyView { NavigationLink(destination: LazyView {
EmptyView() Text("WIP")
}) { }) {
Text(library.name ?? "") ZStack() {
HStack() {
Spacer()
Text("All Genres")
.foregroundColor(.black)
.font(.subheadline)
.fontWeight(.semibold)
Spacer()
}
}
.padding(16)
.background(Color.white)
.frame(minWidth: 100, maxWidth: .infinity)
} }
default: .cornerRadius(10)
NavigationLink(destination: LazyView { .shadow(radius: 5)
LibraryView(viewModel: .init(parentID: library.id), title: library.name ?? "") .padding(.bottom, 15)
}) {
Text(library.name ?? "") ForEach(viewModel.libraries, id: \.id) { library in
if(library.collectionType ?? "" == "movies" || library.collectionType ?? "" == "tvshows") {
NavigationLink(destination: LazyView {
LibraryView(viewModel: .init(parentID: library.id), title: library.name ?? "")
}) {
ZStack() {
ImageView(src: library.getPrimaryImage(maxWidth: 500), bh: library.getPrimaryImageBlurHash())
.opacity(0.4)
HStack() {
Spacer()
Text(library.name ?? "")
.foregroundColor(.white)
.font(.title2)
.fontWeight(.semibold)
Spacer()
}.padding(32)
}.background(Color.black)
.frame(minWidth: 100, maxWidth: .infinity)
}
.cornerRadius(10)
.shadow(radius: 5)
.padding(.bottom, 5)
} else {
EmptyView()
}
} }
} }.padding(.leading, 16)
.padding(.trailing, 16)
} }
.navigationTitle("All Media") .navigationTitle("All Media")
.toolbar { .toolbar {

View File

@ -18,17 +18,16 @@ struct NextUpView: View {
Text("Next Up") Text("Next Up")
.font(.title2) .font(.title2)
.fontWeight(.bold) .fontWeight(.bold)
.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)) .padding(.leading, 16)
ScrollView(.horizontal, showsIndicators: false) { ScrollView(.horizontal, showsIndicators: false) {
LazyHStack { LazyHStack {
Spacer().frame(width: 16)
ForEach(items, id: \.id) { item in ForEach(items, id: \.id) { item in
NavigationLink(destination: ItemView(item: item)) { NavigationLink(destination: LazyView { ItemView(item: item) }) {
VStack(alignment: .leading) { VStack(alignment: .leading) {
ImageView(src: item.getSeriesPrimaryImage(maxWidth: 100), bh: item.getSeriesPrimaryImageBlurHash()) ImageView(src: item.getSeriesPrimaryImage(maxWidth: 100), bh: item.getSeriesPrimaryImageBlurHash())
.frame(width: 100, height: 150) .frame(width: 100, height: 150)
.cornerRadius(10) .cornerRadius(10)
Spacer().frame(height: 5) .shadow(radius: 4)
Text(item.seriesName!) Text(item.seriesName!)
.font(.caption) .font(.caption)
.fontWeight(.semibold) .fontWeight(.semibold)
@ -40,13 +39,12 @@ struct NextUpView: View {
.foregroundColor(.secondary) .foregroundColor(.secondary)
.lineLimit(1) .lineLimit(1)
}.frame(width: 100) }.frame(width: 100)
Spacer().frame(width: 16)
} }
} }.padding(.trailing, 16)
} }
.padding(.leading, 20)
} }
.frame(height: 200) .frame(height: 200)
} }
.padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0))
} }
} }

View File

@ -17,7 +17,7 @@ struct SearchBar: View {
var body: some View { var body: some View {
HStack { HStack {
TextField("Search ...", text: $text) TextField("Search...", text: $text)
.padding(7) .padding(7)
.padding(.horizontal, 16) .padding(.horizontal, 16)
.background(Color(.systemGray6)) .background(Color(.systemGray6))

View File

@ -65,26 +65,7 @@ struct SettingsView: View {
Text("Signed in as \(username)").foregroundColor(.primary) Text("Signed in as \(username)").foregroundColor(.primary)
Spacer() Spacer()
Button { Button {
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Server")
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
do {
try viewContext.execute(deleteRequest)
} catch _ as NSError {
// TODO: handle the error
}
let fetchRequest2: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "SignedInUser")
let deleteRequest2 = NSBatchDeleteRequest(fetchRequest: fetchRequest2)
do {
try viewContext.execute(deleteRequest2)
} catch _ as NSError {
// TODO: handle the error
}
SessionManager.current.logout() SessionManager.current.logout()
ServerEnvironment.current.reset()
} label: { } label: {
Text("Log out").font(.callout) Text("Log out").font(.callout)
} }

View File

@ -129,11 +129,6 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
let secondsScrubbedTo = round(Double(seekSlider.value) * videoDuration) let secondsScrubbedTo = round(Double(seekSlider.value) * videoDuration)
let offset = secondsScrubbedTo - videoPosition let offset = secondsScrubbedTo - videoPosition
print(videoPosition)
print(videoDuration)
print(secondsScrubbedTo)
print(offset)
if(playerDestination == .local) { if(playerDestination == .local) {
if offset > 0 { if offset > 0 {
mediaPlayer.jumpForward(Int32(offset)) mediaPlayer.jumpForward(Int32(offset))
@ -178,7 +173,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
if(playerDestination == .local) { if(playerDestination == .local) {
mediaPlayer.jumpBackward(15) mediaPlayer.jumpBackward(15)
} else { } else {
self.sendJellyfinCommand(command: "Seek", options: ["position": (remotePositionTicks/10_000_000)-15])
} }
} }
} }
@ -188,6 +183,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
if(playerDestination == .local) { if(playerDestination == .local) {
mediaPlayer.jumpForward(30) mediaPlayer.jumpForward(30)
} else { } else {
self.sendJellyfinCommand(command: "Seek", options: ["position": (remotePositionTicks/10_000_000)+30])
} }
} }
} }
@ -281,14 +277,6 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
} }
} }
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .landscape
}
override var shouldAutorotate: Bool {
return true
}
func setupNowPlayingCC() { func setupNowPlayingCC() {
let commandCenter = MPRemoteCommandCenter.shared() let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.playCommand.isEnabled = true commandCenter.playCommand.isEnabled = true
@ -327,7 +315,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
self.mediaPlayer.jumpForward(30) self.mediaPlayer.jumpForward(30)
self.sendProgressReport(eventName: "timeupdate") self.sendProgressReport(eventName: "timeupdate")
} else { } else {
self.sendJellyfinCommand(command: "Seek", options: ["position": (self.remotePositionTicks/10_000_000)+30])
} }
return .success return .success
} }
@ -337,6 +325,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
if(self.playerDestination == .local) { if(self.playerDestination == .local) {
self.mediaPlayer.jumpBackward(15) self.mediaPlayer.jumpBackward(15)
self.sendProgressReport(eventName: "timeupdate") self.sendProgressReport(eventName: "timeupdate")
} else {
self.sendJellyfinCommand(command: "Seek", options: ["position": (self.remotePositionTicks/10_000_000)-15])
} }
return .success return .success
} }
@ -376,10 +366,6 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
UIApplication.shared.beginReceivingRemoteControlEvents() UIApplication.shared.beginReceivingRemoteControlEvents()
} }
override func remoteControlReceived(with event: UIEvent?) {
dump(event)
}
// MARK: viewDidLoad // MARK: viewDidLoad
override func viewDidLoad() { override func viewDidLoad() {
if manifest.type == "Movie" { if manifest.type == "Movie" {
@ -387,12 +373,13 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
} else { } else {
titleLabel.text = "S\(String(manifest.parentIndexNumber ?? 0)):E\(String(manifest.indexNumber ?? 0))\(manifest.name ?? "")" titleLabel.text = "S\(String(manifest.parentIndexNumber ?? 0)):E\(String(manifest.indexNumber ?? 0))\(manifest.name ?? "")"
} }
super.viewDidLoad() if(!UIDevice.current.orientation.isLandscape || UIDevice.current.orientation.isFlat) {
if !UIDevice.current.orientation.isLandscape { let value = UIInterfaceOrientation.landscapeRight.rawValue
let value = UIInterfaceOrientation.landscapeLeft.rawValue
UIDevice.current.setValue(value, forKey: "orientation") UIDevice.current.setValue(value, forKey: "orientation")
UIViewController.attemptRotationToDeviceOrientation()
} }
super.viewDidLoad()
} }
func mediaHasStartedPlaying() { func mediaHasStartedPlaying() {
@ -426,12 +413,19 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
} }
} }
override func viewWillDisappear(_ animated: Bool) {
self.tabBarController?.tabBar.isHidden = false
self.navigationController?.isNavigationBarHidden = false
overrideUserInterfaceStyle = .unspecified
super.viewWillDisappear(animated)
}
//MARK: viewDidAppear //MARK: viewDidAppear
override func viewDidAppear(_ animated: Bool) { override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated) super.viewDidAppear(animated)
overrideUserInterfaceStyle = .dark overrideUserInterfaceStyle = .dark
self.tabBarController?.tabBar.isHidden = true self.tabBarController?.tabBar.isHidden = true
// View has loaded. self.navigationController?.isNavigationBarHidden = true
mediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 14) mediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 14)
// mediaPlayer.wrappedValue.perform(Selector(("setTextRendererFont:")), with: "Copperplate") // mediaPlayer.wrappedValue.perform(Selector(("setTextRendererFont:")), with: "Copperplate")
@ -453,11 +447,23 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
DispatchQueue.global(qos: .userInitiated).async { [self] in DispatchQueue.global(qos: .userInitiated).async { [self] in
delegate?.showLoadingView(self) delegate?.showLoadingView(self)
MediaInfoAPI.getPostedPlaybackInfo(itemId: manifest.id!, userId: SessionManager.current.user.user_id!, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, autoOpenLiveStream: true, playbackInfoDto: playbackInfo) MediaInfoAPI.getPostedPlaybackInfo(itemId: manifest.id!, userId: SessionManager.current.user.user_id!, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, autoOpenLiveStream: true, playbackInfoDto: playbackInfo)
.sink(receiveCompletion: { result in .sink(receiveCompletion: { completion in
print(result) switch completion {
case .finished:
break
case .failure(let error):
if let err = error as? ErrorResponse {
switch err {
case .error(401, _, _, _):
self.delegate?.exitPlayer(self)
SessionManager.current.logout()
case .error:
self.delegate?.exitPlayer(self)
}
}
break
}
}, receiveValue: { [self] response in }, receiveValue: { [self] response in
videoContentView.setNeedsLayout()
videoContentView.setNeedsDisplay()
playSessionId = response.playSessionId ?? "" playSessionId = response.playSessionId ?? ""
let mediaSource = response.mediaSources!.first.self! let mediaSource = response.mediaSources!.first.self!
if mediaSource.transcodingUrl != nil { if mediaSource.transcodingUrl != nil {
@ -471,11 +477,11 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
subtitleTrackArray.append(disableSubtitleTrack) subtitleTrackArray.append(disableSubtitleTrack)
// Loop through media streams and add to array // Loop through media streams and add to array
for stream in mediaSource.mediaStreams! { for stream in mediaSource.mediaStreams ?? [] {
if stream.type == .subtitle { if stream.type == .subtitle {
var deliveryUrl: URL? var deliveryUrl: URL?
if stream.deliveryMethod == .external { if stream.deliveryMethod == .external {
deliveryUrl = URL(string: "\(ServerEnvironment.current.server.baseURI!)\(stream.deliveryUrl!)")! deliveryUrl = URL(string: "\(ServerEnvironment.current.server.baseURI!)\(stream.deliveryUrl ?? "")")!
} else { } else {
deliveryUrl = nil deliveryUrl = nil
} }
@ -505,7 +511,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
playbackItem = item playbackItem = item
} else { } else {
// Item will be directly played by the client. // Item will be directly played by the client.
let streamURL: URL = URL(string: "\(ServerEnvironment.current.server.baseURI!)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&deviceId=\(SessionManager.current.deviceID)&api_key=\(SessionManager.current.accessToken)&Tag=\(mediaSource.eTag!)")! let streamURL: URL = URL(string: "\(ServerEnvironment.current.server.baseURI!)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&deviceId=\(SessionManager.current.deviceID)&api_key=\(SessionManager.current.accessToken)&Tag=\(mediaSource.eTag ?? "")")!
let item = PlaybackItem() let item = PlaybackItem()
item.videoUrl = streamURL item.videoUrl = streamURL
@ -515,7 +521,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
subtitleTrackArray.append(disableSubtitleTrack) subtitleTrackArray.append(disableSubtitleTrack)
// Loop through media streams and add to array // Loop through media streams and add to array
for stream in mediaSource.mediaStreams! { for stream in mediaSource.mediaStreams ?? [] {
if stream.type == .subtitle { if stream.type == .subtitle {
var deliveryUrl: URL? var deliveryUrl: URL?
if stream.deliveryMethod == .external { if stream.deliveryMethod == .external {
@ -548,7 +554,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
self.sendPlayReport() self.sendPlayReport()
playbackItem = item playbackItem = item
} }
dump(playbackItem)
startLocalPlaybackEngine(true) startLocalPlaybackEngine(true)
}) })
.store(in: &cancellables) .store(in: &cancellables)
@ -597,6 +603,13 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
self.mediaHasStartedPlaying() self.mediaHasStartedPlaying()
delegate?.hideLoadingView(self) delegate?.hideLoadingView(self)
videoContentView.setNeedsLayout()
videoContentView.setNeedsDisplay()
self.view.setNeedsLayout()
self.view.setNeedsDisplay()
self.videoControlsView.setNeedsLayout()
self.videoControlsView.setNeedsDisplay()
mediaPlayer.pause() mediaPlayer.pause()
mediaPlayer.play() mediaPlayer.play()
@ -793,11 +806,6 @@ extension PlayerViewController: VLCMediaPlayerDelegate {
case .buffering : case .buffering :
print("Video is buffering)") print("Video is buffering)")
delegate?.showLoadingView(self) delegate?.showLoadingView(self)
mediaPlayer.pause()
usleep(10000)
mediaPlayer.play()
delegate?.hideLoadingView(self)
paused = false
case .error : case .error :
print("Video has error)") print("Video has error)")
sendStopReport() sendStopReport()
@ -925,7 +933,7 @@ extension PlayerViewController {
} }
func sendPlayReport() { func sendPlayReport() {
startTime = Int(Date().timeIntervalSince1970) * 10000000 startTime = Int(Date().timeIntervalSince1970) * 10_000_000
print("sending play report!") print("sending play report!")
@ -940,3 +948,9 @@ extension PlayerViewController {
.store(in: &cancellables) .store(in: &cancellables)
} }
} }
extension UINavigationController {
open override var childForHomeIndicatorAutoHidden: UIViewController? {
return nil
}
}

View File

@ -49,6 +49,7 @@ extension BaseItemDto {
func getBackdropImage(maxWidth: Int) -> URL { func getBackdropImage(maxWidth: Int) -> URL {
var imageType = "" var imageType = ""
var imageTag = "" var imageTag = ""
var imageItemId = self.id ?? ""
if self.primaryImageAspectRatio ?? 0.0 < 1.0 { if self.primaryImageAspectRatio ?? 0.0 < 1.0 {
imageType = "Backdrop" imageType = "Backdrop"
@ -60,15 +61,16 @@ extension BaseItemDto {
imageTag = self.imageTags?["Primary"] ?? "" imageTag = self.imageTags?["Primary"] ?? ""
} }
if imageTag == "" { if imageTag == "" || imageItemId == "" {
imageType = "Backdrop" imageType = "Backdrop"
if !(self.parentBackdropImageTags?.isEmpty ?? true) { if !(self.parentBackdropImageTags?.isEmpty ?? true) {
imageTag = (self.parentBackdropImageTags ?? [""])[0] imageTag = (self.parentBackdropImageTags ?? [""])[0]
imageItemId = self.parentBackdropItemId ?? ""
} }
} }
let x = UIScreen.main.nativeScale * CGFloat(maxWidth) let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
let urlString = "\(ServerEnvironment.current.server.baseURI!)/Items/\(self.id ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=60&tag=\(imageTag)" let urlString = "\(ServerEnvironment.current.server.baseURI!)/Items/\(imageItemId)/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=60&tag=\(imageTag)"
return URL(string: urlString)! return URL(string: urlString)!
} }
@ -92,13 +94,16 @@ extension BaseItemDto {
func getPrimaryImage(maxWidth: Int) -> URL { func getPrimaryImage(maxWidth: Int) -> URL {
let imageType = "Primary" let imageType = "Primary"
var imageTag = self.imageTags?["Primary"] ?? "" var imageTag = self.imageTags?["Primary"] ?? ""
var imageItemId = self.id ?? ""
if imageTag == "" {
if imageTag == "" || imageItemId == "" {
imageTag = self.seriesPrimaryImageTag ?? "" imageTag = self.seriesPrimaryImageTag ?? ""
imageItemId = self.seriesId ?? ""
} }
let x = UIScreen.main.nativeScale * CGFloat(maxWidth) let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
let urlString = "\(ServerEnvironment.current.server.baseURI!)/Items/\(self.id ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=60&tag=\(imageTag)" let urlString = "\(ServerEnvironment.current.server.baseURI!)/Items/\(imageItemId)/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=60&tag=\(imageTag)"
return URL(string: urlString)! return URL(string: urlString)!
} }

View File

@ -31,7 +31,7 @@ struct ImageView: View {
} }
.failure { .failure {
Rectangle() Rectangle()
.background(Color.gray) .fill(Color.gray)
} }
} }
} }

View File

@ -147,6 +147,9 @@ final class SessionManager {
} }
func logout() { func logout() {
let nc = NotificationCenter.default
nc.post(name: Notification.Name("didSignOut"), object: nil)
let keychain = KeychainSwift() let keychain = KeychainSwift()
keychain.accessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain" keychain.accessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain"
keychain.delete("AccessToken_\(user?.user_id ?? "")") keychain.delete("AccessToken_\(user?.user_id ?? "")")
@ -155,8 +158,5 @@ final class SessionManager {
let deleteRequest = NSBatchDeleteRequest(objectIDs: [user.objectID]) let deleteRequest = NSBatchDeleteRequest(objectIDs: [user.objectID])
user = nil user = nil
_ = try? PersistenceController.shared.container.viewContext.execute(deleteRequest) _ = try? PersistenceController.shared.container.viewContext.execute(deleteRequest)
let nc = NotificationCenter.default
nc.post(name: Notification.Name("didSignOut"), object: nil)
} }
} }

View File

@ -20,8 +20,6 @@ final class LibraryListViewModel: ViewModel {
override init() { override init() {
super.init() super.init()
libraries.append(.init(name: "Favorites", id: "favorites"))
libraries.append(.init(name: "Genres", id: "genres"))
requestLibraries() requestLibraries()
} }

View File

@ -25,86 +25,91 @@ struct NextUpWidgetProvider: TimelineProvider {
func getSnapshot(in context: Context, completion: @escaping (NextUpEntry) -> Void) { func getSnapshot(in context: Context, completion: @escaping (NextUpEntry) -> Void) {
let currentDate = Date() let currentDate = Date()
let server = ServerEnvironment.current.server! let server = ServerEnvironment.current.server
let savedUser = SessionManager.current.user! let savedUser = SessionManager.current.user
var tempCancellables = Set<AnyCancellable>() var tempCancellables = Set<AnyCancellable>()
JellyfinAPI.basePath = server.baseURI ?? "" if(server != nil && savedUser != nil) {
TvShowsAPI.getNextUp(userId: savedUser.user_id, limit: 3, JellyfinAPI.basePath = server!.baseURI ?? ""
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], TvShowsAPI.getNextUp(userId: savedUser!.user_id, limit: 3,
imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb]) fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
.subscribe(on: DispatchQueue.global(qos: .background)) imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb])
.sink(receiveCompletion: { result in .subscribe(on: DispatchQueue.global(qos: .background))
switch result { .sink(receiveCompletion: { result in
case .finished: switch result {
break case .finished:
case let .failure(error): break
completion(NextUpEntry(date: currentDate, items: [], error: error)) case let .failure(error):
} completion(NextUpEntry(date: currentDate, items: [], error: error))
}, receiveValue: { response in }
let dispatchGroup = DispatchGroup() }, receiveValue: { response in
let items = response.items ?? [] let dispatchGroup = DispatchGroup()
var downloadedItems = [(BaseItemDto, UIImage?)]() let items = response.items ?? []
items.enumerated().forEach { _, item in var downloadedItems = [(BaseItemDto, UIImage?)]()
dispatchGroup.enter() items.enumerated().forEach { _, item in
ImagePipeline.shared.loadImage(with: item.getBackdropImage(maxWidth: 320)) { result in dispatchGroup.enter()
guard case let .success(image) = result else { ImagePipeline.shared.loadImage(with: item.getBackdropImage(maxWidth: 320)) { result in
dispatchGroup.leave() guard case let .success(image) = result else {
return dispatchGroup.leave()
} return
downloadedItems.append((item, image.image)) }
dispatchGroup.leave() downloadedItems.append((item, image.image))
dispatchGroup.leave()
}
} }
}
dispatchGroup.notify(queue: .main) { dispatchGroup.notify(queue: .main) {
completion(NextUpEntry(date: currentDate, items: downloadedItems, error: nil)) completion(NextUpEntry(date: currentDate, items: downloadedItems, error: nil))
} }
}) })
.store(in: &tempCancellables) .store(in: &tempCancellables)
}
} }
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> Void) { func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> Void) {
let currentDate = Date() let currentDate = Date()
let entryDate = Calendar.current.date(byAdding: .hour, value: 1, to: currentDate)! let entryDate = Calendar.current.date(byAdding: .hour, value: 1, to: currentDate)!
let server = ServerEnvironment.current.server! let server = ServerEnvironment.current.server
let savedUser = SessionManager.current.user! let savedUser = SessionManager.current.user
var tempCancellables = Set<AnyCancellable>() var tempCancellables = Set<AnyCancellable>()
JellyfinAPI.basePath = server.baseURI ?? ""
TvShowsAPI.getNextUp(userId: savedUser.user_id, limit: 3, if(server != nil && savedUser != nil) {
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], JellyfinAPI.basePath = server!.baseURI ?? ""
imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb]) TvShowsAPI.getNextUp(userId: savedUser!.user_id, limit: 3,
.subscribe(on: DispatchQueue.global(qos: .background)) fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
.sink(receiveCompletion: { result in imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb])
switch result { .subscribe(on: DispatchQueue.global(qos: .background))
case .finished: .sink(receiveCompletion: { result in
break switch result {
case let .failure(error): case .finished:
completion(Timeline(entries: [NextUpEntry(date: currentDate, items: [], error: error)], policy: .after(entryDate))) break
} case let .failure(error):
}, receiveValue: { response in completion(Timeline(entries: [NextUpEntry(date: currentDate, items: [], error: error)], policy: .after(entryDate)))
let dispatchGroup = DispatchGroup() }
let items = response.items ?? [] }, receiveValue: { response in
var downloadedItems = [(BaseItemDto, UIImage?)]() let dispatchGroup = DispatchGroup()
items.enumerated().forEach { _, item in let items = response.items ?? []
dispatchGroup.enter() var downloadedItems = [(BaseItemDto, UIImage?)]()
ImagePipeline.shared.loadImage(with: item.getBackdropImage(maxWidth: 320)) { result in items.enumerated().forEach { _, item in
guard case let .success(image) = result else { dispatchGroup.enter()
dispatchGroup.leave() ImagePipeline.shared.loadImage(with: item.getBackdropImage(maxWidth: 320)) { result in
return guard case let .success(image) = result else {
} dispatchGroup.leave()
downloadedItems.append((item, image.image)) return
dispatchGroup.leave() }
downloadedItems.append((item, image.image))
dispatchGroup.leave()
}
} }
}
dispatchGroup.notify(queue: .main) { dispatchGroup.notify(queue: .main) {
completion(Timeline(entries: [NextUpEntry(date: currentDate, items: downloadedItems, error: nil)], completion(Timeline(entries: [NextUpEntry(date: currentDate, items: downloadedItems, error: nil)],
policy: .after(entryDate))) policy: .after(entryDate)))
} }
}) })
.store(in: &tempCancellables) .store(in: &tempCancellables)
}
} }
} }