Merge branch 'main' into serverDiscovery
This commit is contained in:
commit
2dd60b6ce5
|
@ -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;
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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)!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ struct ImageView: View {
|
||||||
}
|
}
|
||||||
.failure {
|
.failure {
|
||||||
Rectangle()
|
Rectangle()
|
||||||
.background(Color.gray)
|
.fill(Color.gray)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue