some fixes for TabView getting cut off when scrolling out of viewport.

This commit is contained in:
Aiden Vigue 2021-06-17 22:33:41 -04:00
parent b3b49bf81b
commit ac26ac8077
No known key found for this signature in database
GPG Key ID: B9A09843AB079D5B
30 changed files with 338 additions and 177 deletions

View File

@ -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)!))
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())
} else {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
if(BackgroundManager.current.backgroundURL == backgroundURL) {
BackgroundManager.current.clearBackground()
}
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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()
}

View File

@ -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())
}
}
}.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
//LatestMediaView(usingParentID: libraryID)
}.padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0))
}
}
Spacer().frame(height: 16)
*/
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: 135, bottom: 0, trailing: 0))
LatestMediaView(usingParentID: libraryID)
}
}
}
}
}
}

View File

@ -6,6 +6,7 @@
*/
import SwiftUI
import UIKit
@main
struct JellyfinPlayer_tvOSApp: App {
@ -15,8 +16,7 @@ struct JellyfinPlayer_tvOSApp: App {
WindowGroup {
SplashView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
.padding(EdgeInsets(top: 0, leading: -90, bottom: 0, trailing: -90))
.ignoresSafeArea(.all)
.ignoresSafeArea(.all, edges: .all)
}
}
}

View File

@ -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)
}
}

View File

@ -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.
if(viewModel.backgroundURL != nil) {
//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(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(lastBackdropAnim ? 0.4 : 0)
.onChange(of: viewModel.backgroundURL) { _ in
withAnimation(.linear(duration: 0.15)) {
lastBackdropAnim = false
}
}
}
if(viewModel.backgroundURL != nil) {
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")

View File

@ -17,9 +17,7 @@ struct SplashView: View {
if viewModel.isLoggedIn {
NavigationView() {
MainTabView()
}
.padding(.leading, -60)
.padding(.trailing, -60)
}.padding(.all, -1)
} else {
NavigationView {
ConnectToServerView()

View File

@ -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 = "<group>"; };
531690F8267AD135005D8AB9 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlainNavigationLinkButton.swift; sourceTree = "<group>"; };
531690FC267AEDC5005D8AB9 /* HandleAPIRequestCompletion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandleAPIRequestCompletion.swift; sourceTree = "<group>"; };
53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceProfileBuilder.swift; sourceTree = "<group>"; };
531AC8BE26750DE20091C7EB /* ImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageView.swift; sourceTree = "<group>"; };
5321753A2671BCFC005491E6 /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = "<group>"; };
@ -209,6 +209,9 @@
5364F454266CA0DC0026ECBA /* APIExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIExtensions.swift; sourceTree = "<group>"; };
536D3D73267BA8170004248C /* BackgroundManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundManager.swift; sourceTree = "<group>"; };
536D3D75267BA9BB0004248C /* MainTabViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabViewModel.swift; sourceTree = "<group>"; };
536D3D7E267BDF100004248C /* LatestMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestMediaView.swift; sourceTree = "<group>"; };
536D3D80267BDFC60004248C /* PortraitItemElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitItemElement.swift; sourceTree = "<group>"; };
536D3D87267C17350004248C /* PublicUserButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicUserButton.swift; sourceTree = "<group>"; };
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 = "<group>"; };
5377CBF8263B596B003A4E83 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@ -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 = "<group>";
@ -380,6 +385,8 @@
isa = PBXGroup;
children = (
531690F6267ACC00005D8AB9 /* LandscapeItemElement.swift */,
536D3D80267BDFC60004248C /* PortraitItemElement.swift */,
536D3D87267C17350004248C /* PublicUserButton.swift */,
);
path = Components;
sourceTree = "<group>";
@ -476,7 +483,6 @@
6267B3D526710B8900A7371D /* CollectionExtensions.swift */,
6267B3D92671138200A7371D /* ImageExtensions.swift */,
62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */,
531690FC267AEDC5005D8AB9 /* HandleAPIRequestCompletion.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -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" */;

View File

@ -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",

View File

@ -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(

View File

@ -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)

View File

@ -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)

View File

@ -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 ?? "")

View File

@ -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 ?? "")

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -10,8 +10,7 @@
import SwiftUI
struct SplashView: View {
@StateObject
var viewModel = SplashViewModel()
@StateObject var viewModel = SplashViewModel()
var body: some View {
if viewModel.isLoggedIn {

View File

@ -15,28 +15,28 @@ 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") {
@ -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)!
}

View File

@ -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<Error>, 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
}
}

View File

@ -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<SignedInUser, Error> {
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])

View File

@ -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
})

View File

@ -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 ?? []
})

View File

@ -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 ?? [])
})

View File

@ -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<Error>) {
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
}
}
}

View File

@ -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