diff --git a/JellyfinPlayer tvOS/Components/LandscapeItemElement.swift b/JellyfinPlayer tvOS/Components/LandscapeItemElement.swift index 3b27e7ca..39b5e35a 100644 --- a/JellyfinPlayer tvOS/Components/LandscapeItemElement.swift +++ b/JellyfinPlayer tvOS/Components/LandscapeItemElement.swift @@ -1,5 +1,5 @@ // - /* + /* * 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/. @@ -45,8 +45,8 @@ struct LandscapeItemElement: View { var body: some View { VStack() { - ImageView(src: item.getBackdropImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 375), bh: item.getBackdropImageBlurHash()) - .frame(width: 375, height: 250) + ImageView(src: (item.type == "Episode" ? item.getSeriesBackdropImage(maxWidth: 445) : item.getBackdropImage(maxWidth: 445)), bh: item.type == "Episode" ? item.getSeriesBackdropImageBlurHash() : item.getBackdropImageBlurHash()) + .frame(width: 445, height: 250) .cornerRadius(10) .overlay( Group { @@ -54,7 +54,7 @@ struct LandscapeItemElement: View { ZStack(alignment: .leading) { Rectangle() .fill(LinearGradient(colors: [.black,.clear], startPoint: .bottom, endPoint: .top)) - .frame(width: 375, height: 90) + .frame(width: 445, height: 90) .mask(CutOffShadow()) VStack(alignment: .leading) { Text("CONTINUE • \(item.getItemProgressString())") @@ -68,9 +68,9 @@ struct LandscapeItemElement: View { .frame(minWidth: 100, maxWidth: .infinity, minHeight: 12, maxHeight: 12) RoundedRectangle(cornerRadius: 6) .fill(Color(red: 172/255, green: 92/255, blue: 195/255)) - .frame(width: CGFloat(item.userData?.playedPercentage ?? 0 * 3.59), height: 12) + .frame(width: CGFloat(item.userData?.playedPercentage ?? 0 * 4.45 - 0.16), height: 12) } - }.padding(8) + }.padding(12) } } else { EmptyView() @@ -80,11 +80,11 @@ struct LandscapeItemElement: View { .shadow(radius: focused ? 10.0 : 0, y: focused ? 10.0 : 0) .shadow(radius: focused ? 10.0 : 0, y: focused ? 10.0 : 0) if(focused) { - Text(item.seriesName ?? item.name ?? "") + Text(item.type == "Episode" ? "\(item.seriesName ?? "") • S\(String(item.parentIndexNumber ?? 0)):E\(String(item.indexNumber ?? 0))" : item.name ?? "") .font(.callout) .fontWeight(.semibold) .lineLimit(1) - .frame(width: 375) + .frame(width: 445) } else { Spacer().frame(height: 25) } @@ -95,12 +95,11 @@ struct LandscapeItemElement: View { } if(envFocus == true) { - backgroundURL = item.getBackdropImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: Int((UIScreen.main.currentMode?.size.width)!)) - BackgroundManager.current.setBackground(to: backgroundURL!, hash: item.getBackdropImageBlurHash()) - } else { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { - if(BackgroundManager.current.backgroundURL == backgroundURL) { - BackgroundManager.current.clearBackground() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + // your code here + if(self.focused == true) { + backgroundURL = item.getBackdropImage(maxWidth: 1080) + BackgroundManager.current.setBackground(to: backgroundURL!, hash: item.getBackdropImageBlurHash()) } } } diff --git a/JellyfinPlayer tvOS/Components/PortraitItemElement.swift b/JellyfinPlayer tvOS/Components/PortraitItemElement.swift new file mode 100644 index 00000000..51197b6c --- /dev/null +++ b/JellyfinPlayer tvOS/Components/PortraitItemElement.swift @@ -0,0 +1,71 @@ +// + /* + * 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 + +fileprivate struct CutOffShadow: Shape { + let radius = 6.0; + + func path(in rect: CGRect) -> Path { + var path = Path() + + let tl = CGPoint(x: rect.minX, y: rect.minY) + let tr = CGPoint(x: rect.maxX, y: rect.minY) + let brs = CGPoint(x: rect.maxX, y: rect.maxY - radius) + let brc = CGPoint(x: rect.maxX - radius, y: rect.maxY - radius) + let bls = CGPoint(x: rect.minX + radius, y: rect.maxY) + let blc = CGPoint(x: rect.minX + radius, y: rect.maxY - radius) + + path.move(to: tl) + path.addLine(to: tr) + path.addLine(to: brs) + path.addRelativeArc(center: brc, radius: radius, + startAngle: Angle.degrees(0), delta: Angle.degrees(90)) + path.addLine(to: bls) + path.addRelativeArc(center: blc, radius: radius, + startAngle: Angle.degrees(90), delta: Angle.degrees(90)) + + return path + } +} + +struct PortraitItemElement: View { + @Environment(\.isFocused) var envFocused: Bool + @State var focused: Bool = false; + @State var backgroundURL: URL?; + + var item: BaseItemDto; + + var body: some View { + VStack() { + ImageView(src: item.type == "Episode" ? item.getSeriesPrimaryImage(maxWidth: 200) : item.getPrimaryImage(maxWidth: 200), bh: item.type == "Episode" ? item.getSeriesPrimaryImageBlurHash() : item.getPrimaryImageBlurHash()) + .frame(width: 200, height: 300) + .cornerRadius(10) + .shadow(radius: focused ? 10.0 : 0) + .shadow(radius: focused ? 10.0 : 0) + } + .onChange(of: envFocused) { envFocus in + withAnimation(.linear(duration: 0.15)) { + self.focused = envFocus + } + + if(envFocus == true) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + // your code here + if(self.focused == true) { + backgroundURL = item.getBackdropImage(maxWidth: 1080) + BackgroundManager.current.setBackground(to: backgroundURL!, hash: item.getBackdropImageBlurHash()) + } + } + } + } + .scaleEffect(focused ? 1.1 : 1) + } +} diff --git a/JellyfinPlayer tvOS/Components/PublicUserButton.swift b/JellyfinPlayer tvOS/Components/PublicUserButton.swift new file mode 100644 index 00000000..88512cd4 --- /dev/null +++ b/JellyfinPlayer tvOS/Components/PublicUserButton.swift @@ -0,0 +1,45 @@ +// + /* + * 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 +import CoreMedia + +struct PublicUserButton: View { + @Environment(\.isFocused) var envFocused: Bool + @State var focused: Bool = false; + var publicUser: UserDto + + var body: some View { + VStack { + if publicUser.primaryImageTag != nil { + ImageView(src: URL(string: "\(ServerEnvironment.current.server.baseURI ?? "")/Users/\(publicUser.id ?? "")/Images/Primary?width=500&quality=80&tag=\(publicUser.primaryImageTag!)")!) + .frame(width: 250, height: 250) + .cornerRadius(125.0) + } else { + Image(systemName: "person.fill") + .foregroundColor(Color(red: 1, green: 1, blue: 1).opacity(0.8)) + .font(.system(size: 35)) + .frame(width: 250, height: 250) + .background(Color(red: 98 / 255, green: 121 / 255, blue: 205 / 255)) + .cornerRadius(125.0) + .shadow(radius: 6) + } + if(focused) { + Text(publicUser.name ?? "").font(.headline).fontWeight(.semibold) + } else { + Spacer().frame(height: 60) + } + }.onChange(of: envFocused) { envFocus in + withAnimation(.linear(duration: 0.15)) { + self.focused = envFocus + } + }.scaleEffect(focused ? 1.1 : 1) + } +} diff --git a/JellyfinPlayer tvOS/ConnectToServerView.swift b/JellyfinPlayer tvOS/ConnectToServerView.swift index de4d7a88..1e32019c 100644 --- a/JellyfinPlayer tvOS/ConnectToServerView.swift +++ b/JellyfinPlayer tvOS/ConnectToServerView.swift @@ -83,25 +83,10 @@ struct ConnectToServerView: View { } } }) { - VStack { - if publicUser.primaryImageTag != nil { - ImageView(src: URL(string: "\(ServerEnvironment.current.server.baseURI ?? "")/Users/\(publicUser.id ?? "")/Images/Primary?width=500&quality=80&tag=\(publicUser.primaryImageTag!)")!) - .frame(width: 250, height: 250) - .cornerRadius(125.0) - } else { - Image(systemName: "person.fill") - .foregroundColor(Color(red: 1, green: 1, blue: 1).opacity(0.8)) - .font(.system(size: 35)) - .frame(width: 250, height: 250) - .background(Color(red: 98 / 255, green: 121 / 255, blue: 205 / 255)) - .cornerRadius(125.0) - .shadow(radius: 6) - } - Text(publicUser.name ?? "").font(.headline).fontWeight(.semibold) - } - } + PublicUserButton(publicUser: publicUser) + }.buttonStyle(PlainNavigationLinkButtonStyle()) } - } + }.padding(.bottom, 20) HStack() { Spacer() Button { diff --git a/JellyfinPlayer tvOS/ContinueWatchingView.swift b/JellyfinPlayer tvOS/ContinueWatchingView.swift index ed5229dd..c0493f20 100644 --- a/JellyfinPlayer tvOS/ContinueWatchingView.swift +++ b/JellyfinPlayer tvOS/ContinueWatchingView.swift @@ -12,6 +12,7 @@ import Combine struct ContinueWatchingView: View { var items: [BaseItemDto] + @Namespace private var namespace var body: some View { VStack(alignment: .leading) { @@ -27,11 +28,11 @@ struct ContinueWatchingView: View { NavigationLink(destination: Text("itemv")) { LandscapeItemElement(item: item) }.buttonStyle(PlainNavigationLinkButtonStyle()) + .prefersDefaultFocus(item == items.first, in: namespace) } Spacer().frame(width: 90) } }.frame(height: 330) - .offset(y: -10) } else { EmptyView() } diff --git a/JellyfinPlayer tvOS/HomeView.swift b/JellyfinPlayer tvOS/HomeView.swift index 0dd3a24f..231a3b23 100644 --- a/JellyfinPlayer tvOS/HomeView.swift +++ b/JellyfinPlayer tvOS/HomeView.swift @@ -27,34 +27,24 @@ struct HomeView: View { if !viewModel.nextUpItems.isEmpty { NextUpView(items: viewModel.nextUpItems) } - /* + if !viewModel.librariesShowRecentlyAddedIDs.isEmpty { ForEach(viewModel.librariesShowRecentlyAddedIDs, id: \.self) { libraryID in VStack(alignment: .leading) { let library = viewModel.libraries.first(where: { $0.id == libraryID }) - HStack { - Text("Latest \(library?.name ?? "")") - .font(.title2) - .fontWeight(.bold) - .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 16)) - Spacer() - NavigationLink(destination: LazyView { - //LibraryView(usingParentID: libraryID, title: library?.name ?? "", usingFilters: viewModel.recentFilterSet) - Text("library here") - }) { - HStack { - Text("See All").font(.subheadline).fontWeight(.bold) - Image(systemName: "chevron.right").font(Font.subheadline.bold()) - } + + NavigationLink(destination: Text("library_latest")) { + HStack() { + Text("Latest \(library?.name ?? "")") + .font(.headline) + .fontWeight(.semibold) + Image(systemName: "chevron.forward.circle.fill") } - }.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)) - //LatestMediaView(usingParentID: libraryID) - }.padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0)) + }.padding(EdgeInsets(top: 0, leading: 135, bottom: 0, trailing: 0)) + LatestMediaView(usingParentID: libraryID) + } } } - - Spacer().frame(height: 16) - */ } } } diff --git a/JellyfinPlayer tvOS/JellyfinPlayer_tvOSApp.swift b/JellyfinPlayer tvOS/JellyfinPlayer_tvOSApp.swift index add6ef7a..eb7a4fac 100644 --- a/JellyfinPlayer tvOS/JellyfinPlayer_tvOSApp.swift +++ b/JellyfinPlayer tvOS/JellyfinPlayer_tvOSApp.swift @@ -6,17 +6,17 @@ */ import SwiftUI +import UIKit @main struct JellyfinPlayer_tvOSApp: App { let persistenceController = PersistenceController.shared - + var body: some Scene { WindowGroup { SplashView() .environment(\.managedObjectContext, persistenceController.container.viewContext) - .padding(EdgeInsets(top: 0, leading: -90, bottom: 0, trailing: -90)) - .ignoresSafeArea(.all) + .ignoresSafeArea(.all, edges: .all) } } } diff --git a/JellyfinPlayer tvOS/LatestMediaView.swift b/JellyfinPlayer tvOS/LatestMediaView.swift new file mode 100644 index 00000000..d9b82526 --- /dev/null +++ b/JellyfinPlayer tvOS/LatestMediaView.swift @@ -0,0 +1,55 @@ +/* JellyfinPlayer/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 +import Combine + +struct LatestMediaView: View { + + @StateObject + var tempViewModel = ViewModel() + @State var items: [BaseItemDto] = [] + private var library_id: String = "" + @State private var viewDidLoad: Bool = false + + init(usingParentID: String) { + library_id = usingParentID + } + + func onAppear() { + if viewDidLoad == true { + return + } + viewDidLoad = true + + DispatchQueue.global(qos: .userInitiated).async { + UserLibraryAPI.getLatestMedia(userId: SessionManager.current.user.user_id!, parentId: library_id, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], enableUserData: true, limit: 12) + .sink(receiveCompletion: { completion in + print(completion) + }, receiveValue: { response in + items = response + }) + .store(in: &tempViewModel.cancellables) + } + } + + var body: some View { + ScrollView(.horizontal, showsIndicators: false) { + LazyHStack { + Spacer().frame(width: 90) + ForEach(items, id: \.id) { item in + NavigationLink(destination: Text("itemv")) { + PortraitItemElement(item: item) + }.buttonStyle(PlainNavigationLinkButtonStyle()) + } + Spacer().frame(width: 90) + } + }.frame(height: 350) + .onAppear(perform: onAppear) + } +} diff --git a/JellyfinPlayer tvOS/MainTabView.swift b/JellyfinPlayer tvOS/MainTabView.swift index a218fba0..a24750e1 100644 --- a/JellyfinPlayer tvOS/MainTabView.swift +++ b/JellyfinPlayer tvOS/MainTabView.swift @@ -13,39 +13,34 @@ import SwiftUI struct MainTabView: View { @State private var tabSelection: Tab = .home @StateObject private var viewModel = MainTabViewModel() - @State private var backdropAnim: Bool = false + @State private var backdropAnim: Bool = true @State private var lastBackdropAnim: Bool = false var body: some View { ZStack() { - //please do not touch my magical crossfading. + //please do not touch my magical crossfading. i will wave my magical github wand and cry + if(viewModel.lastBackgroundURL != nil) { + ImageView(src: viewModel.lastBackgroundURL!, bh: viewModel.backgroundBlurHash) + .frame(minWidth: 100, maxWidth: .infinity, minHeight: 100, maxHeight: .infinity) + .opacity(lastBackdropAnim ? 0.4 : 0) + } if(viewModel.backgroundURL != nil) { - if(viewModel.lastBackgroundURL != nil) { - ImageView(src: viewModel.lastBackgroundURL!, bh: viewModel.backgroundBlurHash) - .frame(width: UIScreen.main.currentMode?.size.width, height: UIScreen.main.currentMode?.size.height) - .blur(radius: 2) - .opacity(lastBackdropAnim ? 0.4 : 0) - .onChange(of: viewModel.backgroundURL) { _ in - withAnimation(.linear(duration: 0.15)) { - lastBackdropAnim = false - } - } - } ImageView(src: viewModel.backgroundURL!, bh: viewModel.backgroundBlurHash) - .frame(width: UIScreen.main.currentMode?.size.width, height: UIScreen.main.currentMode?.size.height) - .blur(radius: 2) + .frame(minWidth: 100, maxWidth: .infinity, minHeight: 100, maxHeight: .infinity) .opacity(backdropAnim ? 0.4 : 0) .onChange(of: viewModel.backgroundURL) { _ in lastBackdropAnim = true backdropAnim = false - withAnimation(.linear(duration: 0.15)) { + withAnimation(.linear(duration: 0.33)) { + lastBackdropAnim = false backdropAnim = true } } } + TabView(selection: $tabSelection) { HomeView() - .navigationViewStyle(StackNavigationViewStyle()) + .offset(y: -20) .tabItem { Text(Tab.home.localized) Image(systemName: "house") @@ -53,7 +48,6 @@ struct MainTabView: View { .tag(Tab.home) Text("Library") - .navigationViewStyle(StackNavigationViewStyle()) .tabItem { Text(Tab.allMedia.localized) Image(systemName: "folder") diff --git a/JellyfinPlayer tvOS/SplashView.swift b/JellyfinPlayer tvOS/SplashView.swift index d0c5fb18..0a4025d8 100644 --- a/JellyfinPlayer tvOS/SplashView.swift +++ b/JellyfinPlayer tvOS/SplashView.swift @@ -17,9 +17,7 @@ struct SplashView: View { if viewModel.isLoggedIn { NavigationView() { MainTabView() - } - .padding(.leading, -60) - .padding(.trailing, -60) + }.padding(.all, -1) } else { NavigationView { ConnectToServerView() diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 8503c02c..cf06d89c 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -13,9 +13,6 @@ 531690F0267ABF72005D8AB9 /* NextUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690EE267ABF72005D8AB9 /* NextUpView.swift */; }; 531690F7267ACC00005D8AB9 /* LandscapeItemElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690F6267ACC00005D8AB9 /* LandscapeItemElement.swift */; }; 531690FA267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */; }; - 531690FD267AEDC5005D8AB9 /* HandleAPIRequestCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690FC267AEDC5005D8AB9 /* HandleAPIRequestCompletion.swift */; }; - 531690FE267AEDC5005D8AB9 /* HandleAPIRequestCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690FC267AEDC5005D8AB9 /* HandleAPIRequestCompletion.swift */; }; - 531690FF267AEDC5005D8AB9 /* HandleAPIRequestCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690FC267AEDC5005D8AB9 /* HandleAPIRequestCompletion.swift */; }; 53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */; }; 531ABF6C2671F5CC00C0FE20 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 628B95212670CABD0091AF3B /* WidgetKit.framework */; }; 531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531AC8BE26750DE20091C7EB /* ImageView.swift */; }; @@ -50,6 +47,10 @@ 536D3D78267BD5C30004248C /* ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB57B2678CE1000530A6E /* ViewModel.swift */; }; 536D3D79267BD5D00004248C /* ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB57B2678CE1000530A6E /* ViewModel.swift */; }; 536D3D7D267BD5F90004248C /* ActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 536D3D7C267BD5F90004248C /* ActivityIndicator */; }; + 536D3D7F267BDF100004248C /* LatestMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536D3D7E267BDF100004248C /* LatestMediaView.swift */; }; + 536D3D81267BDFC60004248C /* PortraitItemElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536D3D80267BDFC60004248C /* PortraitItemElement.swift */; }; + 536D3D84267BEA550004248C /* ParallaxView in Frameworks */ = {isa = PBXBuildFile; productRef = 536D3D83267BEA550004248C /* ParallaxView */; }; + 536D3D88267C17350004248C /* PublicUserButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536D3D87267C17350004248C /* PublicUserButton.swift */; }; 5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBF4263B596A003A4E83 /* JellyfinPlayerApp.swift */; }; 5377CBF9263B596B003A4E83 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5377CBF8263B596B003A4E83 /* Assets.xcassets */; }; 5377CBFC263B596B003A4E83 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5377CBFB263B596B003A4E83 /* Preview Assets.xcassets */; }; @@ -191,7 +192,6 @@ 531690F6267ACC00005D8AB9 /* LandscapeItemElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandscapeItemElement.swift; sourceTree = ""; }; 531690F8267AD135005D8AB9 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlainNavigationLinkButton.swift; sourceTree = ""; }; - 531690FC267AEDC5005D8AB9 /* HandleAPIRequestCompletion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandleAPIRequestCompletion.swift; sourceTree = ""; }; 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceProfileBuilder.swift; sourceTree = ""; }; 531AC8BE26750DE20091C7EB /* ImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageView.swift; sourceTree = ""; }; 5321753A2671BCFC005491E6 /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; @@ -209,6 +209,9 @@ 5364F454266CA0DC0026ECBA /* APIExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIExtensions.swift; sourceTree = ""; }; 536D3D73267BA8170004248C /* BackgroundManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundManager.swift; sourceTree = ""; }; 536D3D75267BA9BB0004248C /* MainTabViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabViewModel.swift; sourceTree = ""; }; + 536D3D7E267BDF100004248C /* LatestMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestMediaView.swift; sourceTree = ""; }; + 536D3D80267BDFC60004248C /* PortraitItemElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitItemElement.swift; sourceTree = ""; }; + 536D3D87267C17350004248C /* PublicUserButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicUserButton.swift; sourceTree = ""; }; 5377CBF1263B596A003A4E83 /* JellyfinPlayer iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "JellyfinPlayer iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 5377CBF4263B596A003A4E83 /* JellyfinPlayerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinPlayerApp.swift; sourceTree = ""; }; 5377CBF8263B596B003A4E83 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -277,6 +280,7 @@ 535870912669D7A800D05A09 /* Introspect in Frameworks */, 625CB57E2678E81E00530A6E /* TVVLCKit.xcframework in Frameworks */, 5358708D2669D7A800D05A09 /* KeychainSwift in Frameworks */, + 536D3D84267BEA550004248C /* ParallaxView in Frameworks */, 53ABFDDC267972BF00886593 /* TVServices.framework in Frameworks */, 5358709B2669D7A800D05A09 /* NukeUI in Frameworks */, 53ABFDED26799D7700886593 /* ActivityIndicator in Frameworks */, @@ -344,6 +348,7 @@ 531690E4267ABD5C005D8AB9 /* MainTabView.swift */, 531690E6267ABD79005D8AB9 /* HomeView.swift */, 531690F8267AD135005D8AB9 /* README.md */, + 536D3D7E267BDF100004248C /* LatestMediaView.swift */, ); path = "JellyfinPlayer tvOS"; sourceTree = ""; @@ -380,6 +385,8 @@ isa = PBXGroup; children = ( 531690F6267ACC00005D8AB9 /* LandscapeItemElement.swift */, + 536D3D80267BDFC60004248C /* PortraitItemElement.swift */, + 536D3D87267C17350004248C /* PublicUserButton.swift */, ); path = Components; sourceTree = ""; @@ -476,7 +483,6 @@ 6267B3D526710B8900A7371D /* CollectionExtensions.swift */, 6267B3D92671138200A7371D /* ImageExtensions.swift */, 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */, - 531690FC267AEDC5005D8AB9 /* HandleAPIRequestCompletion.swift */, ); path = Extensions; sourceTree = ""; @@ -535,6 +541,7 @@ 5358709A2669D7A800D05A09 /* NukeUI */, 53A431BE266B0FFE0016769F /* JellyfinAPI */, 53ABFDEC26799D7700886593 /* ActivityIndicator */, + 536D3D83267BEA550004248C /* ParallaxView */, ); productName = "JellyfinPlayer tvOS"; productReference = 535870602669D21600D05A09 /* JellyfinPlayer tvOS.app */; @@ -629,6 +636,7 @@ 621C637E26672A30004216EA /* XCRemoteSwiftPackageReference "NukeUI" */, 53A431BB266B0FF20016769F /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */, 625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */, + 536D3D82267BEA550004248C /* XCRemoteSwiftPackageReference "ParallaxView" */, ); productRefGroup = 5377CBF2263B596A003A4E83 /* Products */; projectDirPath = ""; @@ -684,6 +692,8 @@ 531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */, 53ABFDDE267974E300886593 /* SplashView.swift in Sources */, 53ABFDE8267974EF00886593 /* SplashViewModel.swift in Sources */, + 536D3D88267C17350004248C /* PublicUserButton.swift in Sources */, + 536D3D7F267BDF100004248C /* LatestMediaView.swift in Sources */, 531690ED267ABF46005D8AB9 /* ContinueWatchingView.swift in Sources */, 62EC3530267666A5000E9F2D /* SessionManager.swift in Sources */, 531690F7267ACC00005D8AB9 /* LandscapeItemElement.swift in Sources */, @@ -693,12 +703,12 @@ 535870A52669D8AE00D05A09 /* ParallaxHeader.swift in Sources */, 531690F0267ABF72005D8AB9 /* NextUpView.swift in Sources */, 535870A72669D8AE00D05A09 /* MultiSelectorView.swift in Sources */, + 536D3D81267BDFC60004248C /* PortraitItemElement.swift in Sources */, 531690E5267ABD5C005D8AB9 /* MainTabView.swift in Sources */, 53ABFDE7267974EF00886593 /* ConnectToServerViewModel.swift in Sources */, 53ABFDEE26799DCD00886593 /* ImageView.swift in Sources */, 5358706C2669D21700D05A09 /* PersistenceController.swift in Sources */, 535870AA2669D8AE00D05A09 /* BlurHashDecode.swift in Sources */, - 531690FE267AEDC5005D8AB9 /* HandleAPIRequestCompletion.swift in Sources */, 53ABFDE5267974EF00886593 /* ViewModel.swift in Sources */, 535870A62669D8AE00D05A09 /* LazyView.swift in Sources */, 5321753E2671DE9C005491E6 /* Typings.swift in Sources */, @@ -741,7 +751,6 @@ 5377CC01263B596B003A4E83 /* Model.xcdatamodeld in Sources */, 53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */, 6267B3D626710B8900A7371D /* CollectionExtensions.swift in Sources */, - 531690FD267AEDC5005D8AB9 /* HandleAPIRequestCompletion.swift in Sources */, 53A089D0264DA9DA00D57806 /* MovieItemView.swift in Sources */, 625CB56A2678B71200530A6E /* SplashViewModel.swift in Sources */, 53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */, @@ -780,7 +789,6 @@ 628B95382670CDAB0091AF3B /* Model.xcdatamodeld in Sources */, 62EC353226766849000E9F2D /* SessionManager.swift in Sources */, 536D3D79267BD5D00004248C /* ViewModel.swift in Sources */, - 531690FF267AEDC5005D8AB9 /* HandleAPIRequestCompletion.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1155,6 +1163,14 @@ minimumVersion = 19.0.0; }; }; + 536D3D82267BEA550004248C /* XCRemoteSwiftPackageReference "ParallaxView" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/PGSSoft/ParallaxView"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 3.0.0; + }; + }; 53A431BB266B0FF20016769F /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/jellyfin/jellyfin-sdk-swift"; @@ -1212,6 +1228,11 @@ package = 625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */; productName = ActivityIndicator; }; + 536D3D83267BEA550004248C /* ParallaxView */ = { + isa = XCSwiftPackageProductDependency; + package = 536D3D82267BEA550004248C /* XCRemoteSwiftPackageReference "ParallaxView" */; + productName = ParallaxView; + }; 53A431BC266B0FF20016769F /* JellyfinAPI */ = { isa = XCSwiftPackageProductDependency; package = 53A431BB266B0FF20016769F /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */; diff --git a/JellyfinPlayer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/JellyfinPlayer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0dae625c..80b243dc 100644 --- a/JellyfinPlayer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/JellyfinPlayer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -64,6 +64,15 @@ "version": "0.3.0" } }, + { + "package": "ParallaxView", + "repositoryURL": "https://github.com/PGSSoft/ParallaxView", + "state": { + "branch": null, + "revision": "a4165b0edd9c9c923a1d6e3e4c9a807302a1a475", + "version": "3.1.2" + } + }, { "package": "SwiftUI-Introspect", "repositoryURL": "https://github.com/siteline/SwiftUI-Introspect", diff --git a/JellyfinPlayer/ContinueWatchingView.swift b/JellyfinPlayer/ContinueWatchingView.swift index 2882781e..0f4c6c1e 100644 --- a/JellyfinPlayer/ContinueWatchingView.swift +++ b/JellyfinPlayer/ContinueWatchingView.swift @@ -43,7 +43,7 @@ struct ContinueWatchingView: View { NavigationLink(destination: ItemView(item: item)) { VStack(alignment: .leading) { Spacer().frame(height: 10) - ImageView(src: item.getBackdropImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 320), bh: item.getBackdropImageBlurHash()) + ImageView(src: item.getBackdropImage(maxWidth: 320), bh: item.getBackdropImageBlurHash()) .frame(width: 320, height: 180) .cornerRadius(10) .overlay( diff --git a/JellyfinPlayer/EpisodeItemView.swift b/JellyfinPlayer/EpisodeItemView.swift index 593b8fd2..2ff21f82 100644 --- a/JellyfinPlayer/EpisodeItemView.swift +++ b/JellyfinPlayer/EpisodeItemView.swift @@ -66,7 +66,7 @@ struct EpisodeItemView: View { } var portraitHeaderView: some View { - ImageView(src: item.getBackdropImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)), bh: item.getBackdropImageBlurHash()) + ImageView(src: item.getBackdropImage(maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)), bh: item.getBackdropImageBlurHash()) .opacity(0.4) .blur(radius: 2.0) } @@ -74,7 +74,7 @@ struct EpisodeItemView: View { var portraitHeaderOverlayView: some View { VStack(alignment: .leading) { HStack(alignment: .bottom, spacing: 12) { - ImageView(src: item.getSeriesPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 120), bh: item.getSeriesPrimaryImageBlurHash()) + ImageView(src: item.getSeriesPrimaryImage(maxWidth: 120), bh: item.getSeriesPrimaryImageBlurHash()) .frame(width: 120, height: 180) .cornerRadius(10) VStack(alignment: .leading) { @@ -233,7 +233,7 @@ struct EpisodeItemView: View { } else { GeometryReader { geometry in ZStack { - ImageView(src: item.getBackdropImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 200), bh: item.getBackdropImageBlurHash()) + ImageView(src: item.getBackdropImage(maxWidth: 200), bh: item.getBackdropImageBlurHash()) .opacity(0.3) .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom) @@ -241,7 +241,7 @@ struct EpisodeItemView: View { .blur(radius: 4) HStack { VStack { - ImageView(src: item.getSeriesPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 120), bh: item.getSeriesPrimaryImageBlurHash()) + ImageView(src: item.getSeriesPrimaryImage(maxWidth: 120), bh: item.getSeriesPrimaryImageBlurHash()) .frame(width: 120, height: 180) .cornerRadius(10) Spacer().frame(height: 15) diff --git a/JellyfinPlayer/LatestMediaView.swift b/JellyfinPlayer/LatestMediaView.swift index 540971f0..c1acee9f 100644 --- a/JellyfinPlayer/LatestMediaView.swift +++ b/JellyfinPlayer/LatestMediaView.swift @@ -47,7 +47,7 @@ struct LatestMediaView: View { NavigationLink(destination: ItemView(item: item)) { VStack(alignment: .leading) { Spacer().frame(height: 10) - ImageView(src: item.getPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100), bh: item.getPrimaryImageBlurHash()) + ImageView(src: item.getPrimaryImage(maxWidth: 100), bh: item.getPrimaryImageBlurHash()) .frame(width: 100, height: 150) .cornerRadius(10) Spacer().frame(height: 5) diff --git a/JellyfinPlayer/LibrarySearchView.swift b/JellyfinPlayer/LibrarySearchView.swift index a3f4b8fa..5c28d8f7 100644 --- a/JellyfinPlayer/LibrarySearchView.swift +++ b/JellyfinPlayer/LibrarySearchView.swift @@ -68,7 +68,7 @@ struct LibrarySearchView: View { ForEach(items, id: \.id) { item in NavigationLink(destination: ItemView(item: item)) { VStack(alignment: .leading) { - ImageView(src: item.getPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100), bh: item.getPrimaryImageBlurHash()) + ImageView(src: item.getPrimaryImage(maxWidth: 100), bh: item.getPrimaryImageBlurHash()) .frame(width: 100, height: 150) .cornerRadius(10) Text(item.name ?? "") diff --git a/JellyfinPlayer/LibraryView.swift b/JellyfinPlayer/LibraryView.swift index 52e8a2b6..de9b71a0 100644 --- a/JellyfinPlayer/LibraryView.swift +++ b/JellyfinPlayer/LibraryView.swift @@ -108,7 +108,7 @@ struct LibraryView: View { ForEach(items, id: \.id) { item in NavigationLink(destination: ItemView(item: item)) { VStack(alignment: .leading) { - ImageView(src: item.getPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100), bh: item.getPrimaryImageBlurHash()) + ImageView(src: item.getPrimaryImage(maxWidth: 100), bh: item.getPrimaryImageBlurHash()) .frame(width: 100, height: 150) .cornerRadius(10) Text(item.name ?? "") diff --git a/JellyfinPlayer/MovieItemView.swift b/JellyfinPlayer/MovieItemView.swift index 2f75ad6d..a65fcf20 100644 --- a/JellyfinPlayer/MovieItemView.swift +++ b/JellyfinPlayer/MovieItemView.swift @@ -73,8 +73,7 @@ struct MovieItemView: View { var portraitHeaderView: some View { ImageView(src: item - .getBackdropImage(baseURL: ServerEnvironment.current.server.baseURI!, - maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)), + .getBackdropImage(maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)), bh: item.getBackdropImageBlurHash()) .opacity(0.4) .blur(radius: 2.0) @@ -83,7 +82,7 @@ struct MovieItemView: View { var portraitHeaderOverlayView: some View { VStack(alignment: .leading) { HStack(alignment: .bottom, spacing: 12) { - ImageView(src: item.getPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 120)) + ImageView(src: item.getPrimaryImage(maxWidth: 120)) .frame(width: 120, height: 180) .cornerRadius(10) VStack(alignment: .leading) { @@ -249,7 +248,7 @@ struct MovieItemView: View { } else { GeometryReader { geometry in ZStack { - ImageView(src: item.getBackdropImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 200), + ImageView(src: item.getBackdropImage(maxWidth: 200), bh: item.getBackdropImageBlurHash()) .opacity(0.3) .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, @@ -258,7 +257,7 @@ struct MovieItemView: View { .blur(radius: 4) HStack { VStack { - ImageView(src: item.getPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 120), + ImageView(src: item.getPrimaryImage(maxWidth: 120), bh: item.getPrimaryImageBlurHash()) .frame(width: 120, height: 180) .cornerRadius(10) diff --git a/JellyfinPlayer/NextUpView.swift b/JellyfinPlayer/NextUpView.swift index d77e0038..8c7bedc5 100644 --- a/JellyfinPlayer/NextUpView.swift +++ b/JellyfinPlayer/NextUpView.swift @@ -26,7 +26,7 @@ struct NextUpView: View { ForEach(items, id: \.id) { item in NavigationLink(destination: ItemView(item: item)) { VStack(alignment: .leading) { - ImageView(src: item.getSeriesPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100), bh: item.getSeriesPrimaryImageBlurHash()) + ImageView(src: item.getSeriesPrimaryImage(maxWidth: 100), bh: item.getSeriesPrimaryImageBlurHash()) .frame(width: 100, height: 150) .cornerRadius(10) Spacer().frame(height: 5) diff --git a/JellyfinPlayer/SeasonItemView.swift b/JellyfinPlayer/SeasonItemView.swift index 7cbde0a5..8509397d 100644 --- a/JellyfinPlayer/SeasonItemView.swift +++ b/JellyfinPlayer/SeasonItemView.swift @@ -50,7 +50,7 @@ struct SeasonItemView: View { if isLoading { EmptyView() } else { - ImageView(src: item.getSeriesBackdropImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)), bh: item.getSeriesBackdropImageBlurHash()) + ImageView(src: item.getSeriesBackdropImage(maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)), bh: item.getSeriesBackdropImageBlurHash()) .opacity(0.4) .blur(radius: 2.0) } @@ -58,7 +58,7 @@ struct SeasonItemView: View { var portraitHeaderOverlayView: some View { HStack(alignment: .bottom, spacing: 12) { - ImageView(src: item.getPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 120), bh: item.getPrimaryImageBlurHash()) + ImageView(src: item.getPrimaryImage(maxWidth: 120), bh: item.getPrimaryImageBlurHash()) .frame(width: 120, height: 180) .cornerRadius(10) VStack(alignment: .leading) { @@ -97,7 +97,7 @@ struct SeasonItemView: View { ForEach(episodes, id: \.id) { episode in NavigationLink(destination: ItemView(item: episode)) { HStack { - ImageView(src: episode.getPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 150), bh: episode.getPrimaryImageBlurHash()) + ImageView(src: episode.getPrimaryImage(maxWidth: 150), bh: episode.getPrimaryImageBlurHash()) .shadow(radius: 5) .frame(width: 150, height: 90) .cornerRadius(10) @@ -156,7 +156,7 @@ struct SeasonItemView: View { } else { GeometryReader { geometry in ZStack { - ImageView(src: item.getSeriesBackdropImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 200), bh: item.getSeriesBackdropImageBlurHash()) + ImageView(src: item.getSeriesBackdropImage(maxWidth: 200), bh: item.getSeriesBackdropImageBlurHash()) .opacity(0.4) .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom) @@ -165,7 +165,7 @@ struct SeasonItemView: View { HStack { VStack(alignment: .leading) { Spacer().frame(height: 16) - ImageView(src: item.getPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 120), bh: item.getPrimaryImageBlurHash()) + ImageView(src: item.getPrimaryImage(maxWidth: 120), bh: item.getPrimaryImageBlurHash()) .frame(width: 120, height: 180) .cornerRadius(10) Spacer().frame(height: 4) @@ -190,7 +190,7 @@ struct SeasonItemView: View { ForEach(episodes, id: \.id) { episode in NavigationLink(destination: ItemView(item: episode)) { HStack { - ImageView(src: episode.getPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 150), bh: episode.getPrimaryImageBlurHash()) + ImageView(src: episode.getPrimaryImage(maxWidth: 150), bh: episode.getPrimaryImageBlurHash()) .shadow(radius: 5) .frame(width: 150, height: 90) .cornerRadius(10) diff --git a/JellyfinPlayer/SeriesItemView.swift b/JellyfinPlayer/SeriesItemView.swift index 63892b94..3c4feddb 100644 --- a/JellyfinPlayer/SeriesItemView.swift +++ b/JellyfinPlayer/SeriesItemView.swift @@ -62,7 +62,7 @@ struct SeriesItemView: View { ForEach(seasons, id: \.id) { season in NavigationLink(destination: ItemView(item: season)) { VStack(alignment: .leading) { - ImageView(src: season.getPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100), bh: season.getPrimaryImageBlurHash()) + ImageView(src: season.getPrimaryImage(maxWidth: 100), bh: season.getPrimaryImageBlurHash()) .frame(width: 100, height: 150) .cornerRadius(10) .shadow(radius: 5) diff --git a/JellyfinPlayer/SplashView.swift b/JellyfinPlayer/SplashView.swift index 7c8256a6..1b264644 100644 --- a/JellyfinPlayer/SplashView.swift +++ b/JellyfinPlayer/SplashView.swift @@ -10,8 +10,7 @@ import SwiftUI struct SplashView: View { - @StateObject - var viewModel = SplashViewModel() + @StateObject var viewModel = SplashViewModel() var body: some View { if viewModel.isLoggedIn { diff --git a/Shared/Extensions/APIExtensions.swift b/Shared/Extensions/APIExtensions.swift index 533df91a..39ac4d98 100644 --- a/Shared/Extensions/APIExtensions.swift +++ b/Shared/Extensions/APIExtensions.swift @@ -15,30 +15,30 @@ extension BaseItemDto { // MARK: Images func getSeriesBackdropImageBlurHash() -> String { - let rawImgURL = self.getSeriesBackdropImage(baseURL: "", maxWidth: 1).absoluteString + let rawImgURL = self.getSeriesBackdropImage(maxWidth: 1).absoluteString let imgTag = rawImgURL.components(separatedBy: "&tag=")[1] return self.imageBlurHashes?.backdrop?[imgTag] ?? "001fC^" } func getSeriesPrimaryImageBlurHash() -> String { - let rawImgURL = self.getSeriesPrimaryImage(baseURL: "", maxWidth: 1).absoluteString + let rawImgURL = self.getSeriesPrimaryImage(maxWidth: 1).absoluteString let imgTag = rawImgURL.components(separatedBy: "&tag=")[1] return self.imageBlurHashes?.primary?[imgTag] ?? "001fC^" } func getPrimaryImageBlurHash() -> String { - let rawImgURL = self.getPrimaryImage(baseURL: "", maxWidth: 1).absoluteString + let rawImgURL = self.getPrimaryImage(maxWidth: 1).absoluteString let imgTag = rawImgURL.components(separatedBy: "&tag=")[1] return self.imageBlurHashes?.primary?[imgTag] ?? "001fC^" } func getBackdropImageBlurHash() -> String { - let rawImgURL = self.getBackdropImage(baseURL: "", maxWidth: 1).absoluteString + let rawImgURL = self.getBackdropImage(maxWidth: 1).absoluteString let imgTag = rawImgURL.components(separatedBy: "&tag=")[1] - + if rawImgURL.contains("Backdrop") { return self.imageBlurHashes?.backdrop?[imgTag] ?? "001fC^" } else { @@ -46,7 +46,7 @@ extension BaseItemDto { } } - func getBackdropImage(baseURL: String, maxWidth: Int) -> URL { + func getBackdropImage(maxWidth: Int) -> URL { var imageType = "" var imageTag = "" @@ -68,31 +68,28 @@ extension BaseItemDto { } let x = UIScreen.main.nativeScale * CGFloat(maxWidth) - let urlString = "\(baseURL)/Items/\(self.id ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=60&tag=\(imageTag)" + let urlString = "\(ServerEnvironment.current.server.baseURI!)/Items/\(self.id ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=60&tag=\(imageTag)" return URL(string: urlString)! } - func getSeriesBackdropImage(baseURL: String, maxWidth: Int) -> URL { + func getSeriesBackdropImage(maxWidth: Int) -> URL { let imageType = "Backdrop" let imageTag = (self.parentBackdropImageTags ?? [""])[0] - print(imageType) - print(imageTag) - let x = UIScreen.main.nativeScale * CGFloat(maxWidth) - let urlString = "\(baseURL)/Items/\(self.parentBackdropItemId ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=60&tag=\(imageTag)" + let urlString = "\(ServerEnvironment.current.server.baseURI!)/Items/\(self.parentBackdropItemId ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=60&tag=\(imageTag)" return URL(string: urlString)! } - func getSeriesPrimaryImage(baseURL: String, maxWidth: Int) -> URL { + func getSeriesPrimaryImage(maxWidth: Int) -> URL { let imageType = "Primary" let imageTag = self.seriesPrimaryImageTag ?? "" let x = UIScreen.main.nativeScale * CGFloat(maxWidth) - let urlString = "\(baseURL)/Items/\(self.seriesId ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=60&tag=\(imageTag)" + let urlString = "\(ServerEnvironment.current.server.baseURI!)/Items/\(self.seriesId ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=60&tag=\(imageTag)" return URL(string: urlString)! } - func getPrimaryImage(baseURL: String, maxWidth: Int) -> URL { + func getPrimaryImage(maxWidth: Int) -> URL { let imageType = "Primary" var imageTag = self.imageTags?["Primary"] ?? "" @@ -101,7 +98,7 @@ extension BaseItemDto { } let x = UIScreen.main.nativeScale * CGFloat(maxWidth) - let urlString = "\(baseURL)/Items/\(self.id ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=60&tag=\(imageTag)" + let urlString = "\(ServerEnvironment.current.server.baseURI!)/Items/\(self.id ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=60&tag=\(imageTag)" return URL(string: urlString)! } diff --git a/Shared/Extensions/HandleAPIRequestCompletion.swift b/Shared/Extensions/HandleAPIRequestCompletion.swift deleted file mode 100644 index 0f7dd1a9..00000000 --- a/Shared/Extensions/HandleAPIRequestCompletion.swift +++ /dev/null @@ -1,28 +0,0 @@ -/* JellyfinPlayer/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 Foundation -import Combine -import JellyfinAPI - -func HandleAPIRequestCompletion(completion: Subscribers.Completion, vm: ViewModel) { - switch completion { - case .finished: - break - case .failure(let error): - if let err = error as? ErrorResponse { - switch err { - case .error(401, _, _, _): - vm.errorMessage = err.localizedDescription - SessionManager.current.logout() - case .error: - vm.errorMessage = err.localizedDescription - } - } - break - } -} diff --git a/Shared/Singleton/SessionManager.swift b/Shared/Singleton/SessionManager.swift index 4c6db7e7..5046da2e 100644 --- a/Shared/Singleton/SessionManager.swift +++ b/Shared/Singleton/SessionManager.swift @@ -109,12 +109,14 @@ final class SessionManager { self.user = user generateAuthHeader(with: accessToken) - + print(JellyfinAPI.customHeaders) let nc = NotificationCenter.default nc.post(name: Notification.Name("didSignIn"), object: nil) } func login(username: String, password: String) -> AnyPublisher { + generateAuthHeader(with: nil) + return UserAPI.authenticateUserByName(authenticateUserByName: AuthenticateUserByName(username: username, pw: password)) .map { response -> (SignedInUser, String?) in let user = SignedInUser(context: PersistenceController.shared.container.viewContext) @@ -147,7 +149,7 @@ final class SessionManager { func logout() { let keychain = KeychainSwift() keychain.accessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain" - keychain.delete("AccessToken_\(user.user_id ?? "")") + keychain.delete("AccessToken_\(user?.user_id ?? "")") generateAuthHeader(with: nil) let deleteRequest = NSBatchDeleteRequest(objectIDs: [user.objectID]) diff --git a/Shared/ViewModels/ConnectToServerViewModel.swift b/Shared/ViewModels/ConnectToServerViewModel.swift index 14c5f346..e61c250a 100644 --- a/Shared/ViewModels/ConnectToServerViewModel.swift +++ b/Shared/ViewModels/ConnectToServerViewModel.swift @@ -37,7 +37,7 @@ final class ConnectToServerViewModel: ViewModel { if ServerEnvironment.current.server != nil { UserAPI.getPublicUsers() .sink(receiveCompletion: { completion in - HandleAPIRequestCompletion(completion: completion, vm: self) + self.HandleAPIRequestCompletion(completion: completion) }, receiveValue: { response in self.publicUsers = response self.isConnectedServer = true @@ -74,7 +74,7 @@ final class ConnectToServerViewModel: ViewModel { func login() { SessionManager.current.login(username: username, password: password) .sink(receiveCompletion: { completion in - HandleAPIRequestCompletion(completion: completion, vm: self) + self.HandleAPIRequestCompletion(completion: completion) }, receiveValue: { _ in }) diff --git a/Shared/ViewModels/HomeViewModel.swift b/Shared/ViewModels/HomeViewModel.swift index 0170a81f..f7e39099 100644 --- a/Shared/ViewModels/HomeViewModel.swift +++ b/Shared/ViewModels/HomeViewModel.swift @@ -33,24 +33,29 @@ final class HomeViewModel: ViewModel { } func refresh() { - UserAPI.getCurrentUser() - .trackActivity(loading) - .sink(receiveCompletion: { completion in - print(completion) - }, receiveValue: { response in - let libraries = response.configuration?.orderedViews ?? [] - self.librariesShowRecentlyAddedIDs = libraries.filter { element in - !(response.configuration?.latestItemsExcludes?.contains(element))! - } - }) - .store(in: &cancellables) - UserViewsAPI.getUserViews(userId: SessionManager.current.user.user_id!) .trackActivity(loading) .sink(receiveCompletion: { completion in - print(completion) + self.HandleAPIRequestCompletion(completion: completion) }, receiveValue: { response in - self.libraries = response.items ?? [] + response.items!.forEach { item in + if(item.collectionType == "movies" || item.collectionType == "tvshows") { + self.libraries.append(item) + } + } + + UserAPI.getCurrentUser() + .trackActivity(self.loading) + .sink(receiveCompletion: { completion in + self.HandleAPIRequestCompletion(completion: completion) + }, receiveValue: { response in + self.libraries.forEach { library in + if(!(response.configuration?.latestItemsExcludes?.contains(library.id!))!) { + self.librariesShowRecentlyAddedIDs.append(library.id!) + } + } + }) + .store(in: &self.cancellables) }) .store(in: &cancellables) @@ -59,7 +64,7 @@ final class HomeViewModel: ViewModel { mediaTypes: ["Video"], imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb]) .trackActivity(loading) .sink(receiveCompletion: { completion in - print(completion) + self.HandleAPIRequestCompletion(completion: completion) }, receiveValue: { response in self.resumeItems = response.items ?? [] }) @@ -68,8 +73,8 @@ final class HomeViewModel: ViewModel { TvShowsAPI.getNextUp(userId: SessionManager.current.user.user_id!, limit: 12, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people]) .trackActivity(loading) - .sink(receiveCompletion: { result in - print(result) + .sink(receiveCompletion: { completion in + self.HandleAPIRequestCompletion(completion: completion) }, receiveValue: { response in self.nextUpItems = response.items ?? [] }) diff --git a/Shared/ViewModels/LibraryListViewModel.swift b/Shared/ViewModels/LibraryListViewModel.swift index ac1ffd68..7dba1888 100644 --- a/Shared/ViewModels/LibraryListViewModel.swift +++ b/Shared/ViewModels/LibraryListViewModel.swift @@ -29,7 +29,7 @@ final class LibraryListViewModel: ViewModel { UserViewsAPI.getUserViews(userId: SessionManager.current.user.user_id!) .trackActivity(loading) .sink(receiveCompletion: { completion in - print(completion) + self.HandleAPIRequestCompletion(completion: completion) }, receiveValue: { response in self.libraries.append(contentsOf: response.items ?? []) }) diff --git a/Shared/ViewModels/ViewModel.swift b/Shared/ViewModels/ViewModel.swift index c13eee7f..4fe78a46 100644 --- a/Shared/ViewModels/ViewModel.swift +++ b/Shared/ViewModels/ViewModel.swift @@ -10,6 +10,7 @@ import Combine import Foundation import ActivityIndicator +import JellyfinAPI typealias ErrorMessage = String @@ -30,4 +31,22 @@ class ViewModel: ObservableObject { init() { loading.loading.assign(to: \.isLoading, on: self).store(in: &cancellables) } + + func HandleAPIRequestCompletion(completion: Subscribers.Completion) { + switch completion { + case .finished: + break + case .failure(let error): + if let err = error as? ErrorResponse { + switch err { + case .error(401, _, _, _): + self.errorMessage = err.localizedDescription + SessionManager.current.logout() + case .error: + self.errorMessage = err.localizedDescription + } + } + break + } + } } diff --git a/WidgetExtension/NextUpWidget.swift b/WidgetExtension/NextUpWidget.swift index 2d35128a..5e28d5f8 100644 --- a/WidgetExtension/NextUpWidget.swift +++ b/WidgetExtension/NextUpWidget.swift @@ -47,7 +47,7 @@ struct NextUpWidgetProvider: TimelineProvider { var downloadedItems = [(BaseItemDto, UIImage?)]() items.enumerated().forEach { _, item in dispatchGroup.enter() - ImagePipeline.shared.loadImage(with: item.getBackdropImage(baseURL: server.baseURI ?? "", maxWidth: 320)) { result in + ImagePipeline.shared.loadImage(with: item.getBackdropImage(maxWidth: 320)) { result in guard case let .success(image) = result else { dispatchGroup.leave() return @@ -89,7 +89,7 @@ struct NextUpWidgetProvider: TimelineProvider { var downloadedItems = [(BaseItemDto, UIImage?)]() items.enumerated().forEach { _, item in dispatchGroup.enter() - ImagePipeline.shared.loadImage(with: item.getBackdropImage(baseURL: server.baseURI ?? "", maxWidth: 320)) { result in + ImagePipeline.shared.loadImage(with: item.getBackdropImage(maxWidth: 320)) { result in guard case let .success(image) = result else { dispatchGroup.leave() return