diff --git a/JellyfinPlayer tvOS/ConnectToServerView.swift b/JellyfinPlayer tvOS/ConnectToServerView.swift index b5115683..7d788136 100644 --- a/JellyfinPlayer tvOS/ConnectToServerView.swift +++ b/JellyfinPlayer tvOS/ConnectToServerView.swift @@ -17,11 +17,19 @@ struct ConnectToServerView: View { VStack(alignment: .leading) { if viewModel.isConnectedServer { if viewModel.publicUsers.isEmpty { - Section(header: Text(viewModel.lastPublicUsers.isEmpty || viewModel.username == "" ? "Login to \(ServerEnvironment.current.server.name ?? "")": "Password for \(viewModel.username)")) { + Section(header: Text(viewModel.lastPublicUsers.isEmpty || viewModel.username == "" ? "Login to \(ServerEnvironment.current.server.name ?? "")": "")) { if(viewModel.lastPublicUsers.isEmpty || viewModel.username == "") { TextField("Username", text: $viewModel.username) .disableAutocorrection(true) .autocapitalization(.none) + } else { + HStack() { + Spacer() + ImageView(src: URL(string: "\(ServerEnvironment.current.server.baseURI ?? "")/Users/\(viewModel.selectedPublicUser.id ?? "")/Images/Primary?width=500&quality=80&tag=\(viewModel.selectedPublicUser.primaryImageTag ?? "")")!) + .frame(width: 250, height: 250) + .cornerRadius(125.0) + Spacer() + } } SecureField("Password (optional)", text: $viewModel.password) .disableAutocorrection(true) @@ -57,18 +65,22 @@ struct ConnectToServerView: View { Spacer() }.disabled(viewModel.isLoading || viewModel.username.isEmpty) } - Text("Logging in will link this user to your Apple TV profile").font(.caption).foregroundColor(.secondary) } } else { VStack() { HStack() { ForEach(viewModel.publicUsers, id: \.id) { publicUser in Button(action: { - viewModel.username = publicUser.name ?? "" - viewModel.hidePublicUsers() - if !(publicUser.hasPassword ?? true) { - viewModel.password = "" - viewModel.login() + if(!viewModel.userHasSavedCredentials(userID: publicUser.id!)) { + viewModel.username = publicUser.name ?? "" + viewModel.selectedPublicUser = publicUser + viewModel.hidePublicUsers() + if !(publicUser.hasPassword ?? true) { + viewModel.password = "" + viewModel.login() + } + } else { + viewModel.loginWithSavedCredentials(user: publicUser) } }) { VStack { @@ -124,6 +136,8 @@ struct ConnectToServerView: View { } } } + .padding(.leading, 90) + .padding(.trailing, 90) .alert(item: $viewModel.errorMessage) { _ in Alert(title: Text("Error"), message: Text(viewModel.errorMessage ?? ""), dismissButton: .default(Text("Ok"))) } diff --git a/JellyfinPlayer tvOS/ContinueWatching/ContinueWatchingItem.swift b/JellyfinPlayer tvOS/ContinueWatching/ContinueWatchingItem.swift new file mode 100644 index 00000000..74974eca --- /dev/null +++ b/JellyfinPlayer tvOS/ContinueWatching/ContinueWatchingItem.swift @@ -0,0 +1,69 @@ +// + /* + * 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 ProgressBar: Shape { + 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 br = CGPoint(x: rect.maxX, y: rect.maxY) + let bls = CGPoint(x: rect.minX + 10, y: rect.maxY) + let blc = CGPoint(x: rect.minX + 10, y: rect.maxY - 10) + + path.move(to: tl) + path.addLine(to: tr) + path.addLine(to: br) + path.addLine(to: bls) + path.addRelativeArc(center: blc, radius: 10, + startAngle: Angle.degrees(90), delta: Angle.degrees(90)) + + return path + } +} + +struct ContinueWatchingItem: View { + @Environment(\.isFocused) var envFocused: Bool + @State var focused: Bool = false; + + var item: BaseItemDto; + + var body: some View { + VStack() { + ImageView(src: item.getBackdropImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 375), bh: item.getBackdropImageBlurHash()) + .frame(width: 375, height: 250) + .cornerRadius(10) + .overlay( + Rectangle() + .fill(Color(red: 172/255, green: 92/255, blue: 195/255)) + .mask(ProgressBar()) + .frame(width: CGFloat(item.userData?.playedPercentage ?? 0 * 3.75), height: 12) + .padding(6), alignment: .bottomLeading + ) + if(focused) { + Text(item.seriesName ?? item.name ?? "") + .font(.callout) + .fontWeight(.semibold) + .lineLimit(1) + .frame(width: 375) + } else { + Spacer().frame(height: 25) + } + } + .onChange(of: envFocused) { envFocus in + withAnimation(.linear(duration: 0.15)) { + self.focused = envFocus + } + } + .scaleEffect(focused ? 1.1 : 1) + } +} diff --git a/JellyfinPlayer tvOS/ContinueWatching/ContinueWatchingView.swift b/JellyfinPlayer tvOS/ContinueWatching/ContinueWatchingView.swift new file mode 100644 index 00000000..10f17b3c --- /dev/null +++ b/JellyfinPlayer tvOS/ContinueWatching/ContinueWatchingView.swift @@ -0,0 +1,40 @@ +/* + * 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 ContinueWatchingView: View { + var items: [BaseItemDto] + + var body: some View { + VStack(alignment: .leading) { + if items.count > 0 { + Text("Continue Watching") + .font(.headline) + .fontWeight(.semibold) + .padding(.leading, 135) + ScrollView(.horizontal, showsIndicators: false) { + LazyHStack { + Spacer().frame(width: 90) + ForEach(items, id: \.id) { item in + NavigationLink(destination: Text("itemv")) { + ContinueWatchingItem(item: item) + }.buttonStyle(PlainNavigationLinkButtonStyle()) + } + Spacer().frame(width: 90) + } + }.frame(height: 330) + .offset(y: -10) + } else { + EmptyView() + } + } + } +} diff --git a/JellyfinPlayer tvOS/HomeView.swift b/JellyfinPlayer tvOS/HomeView.swift new file mode 100644 index 00000000..f99e92ec --- /dev/null +++ b/JellyfinPlayer tvOS/HomeView.swift @@ -0,0 +1,62 @@ +// +/* + * 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 SwiftUI + +struct HomeView: View { + @StateObject var viewModel = HomeViewModel() + + @State var showingSettings = false + + var body: some View { + ScrollView { + if viewModel.isLoading { + ProgressView() + } else { + LazyVStack(alignment: .leading) { + if !viewModel.resumeItems.isEmpty { + ContinueWatchingView(items: viewModel.resumeItems) + } + /* + 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) + */ + } + } + } + } +} diff --git a/JellyfinPlayer tvOS/ImageButtonStyle.swift b/JellyfinPlayer tvOS/ImageButtonStyle.swift new file mode 100644 index 00000000..a73874ed --- /dev/null +++ b/JellyfinPlayer tvOS/ImageButtonStyle.swift @@ -0,0 +1,14 @@ +struct ImageButtonStyle: ButtonStyle { + + let focused: Bool + func makeBody(configuration: Configuration) -> some View { + configuration + .label + .padding(6) + .foregroundColor(Color.white) + .background(Color.blue) + .cornerRadius(100) + .shadow(color: .black, radius: self.focused ? 20 : 0, x: 0, y: 0) // 0 + + } +} diff --git a/JellyfinPlayer tvOS/JellyfinPlayer tvOS.entitlements b/JellyfinPlayer tvOS/JellyfinPlayer tvOS.entitlements index 8d925a13..0273a6a6 100644 --- a/JellyfinPlayer tvOS/JellyfinPlayer tvOS.entitlements +++ b/JellyfinPlayer tvOS/JellyfinPlayer tvOS.entitlements @@ -7,5 +7,9 @@ get-current-user runs-as-current-user + keychain-access-groups + + $(AppIdentifierPrefix)me.vigue.jellyfin.sharedKeychain + diff --git a/JellyfinPlayer tvOS/JellyfinPlayer_tvOSApp.swift b/JellyfinPlayer tvOS/JellyfinPlayer_tvOSApp.swift index 38520299..add6ef7a 100644 --- a/JellyfinPlayer tvOS/JellyfinPlayer_tvOSApp.swift +++ b/JellyfinPlayer tvOS/JellyfinPlayer_tvOSApp.swift @@ -15,6 +15,8 @@ struct JellyfinPlayer_tvOSApp: App { WindowGroup { SplashView() .environment(\.managedObjectContext, persistenceController.container.viewContext) + .padding(EdgeInsets(top: 0, leading: -90, bottom: 0, trailing: -90)) + .ignoresSafeArea(.all) } } } diff --git a/JellyfinPlayer tvOS/MainTabView.swift b/JellyfinPlayer tvOS/MainTabView.swift new file mode 100644 index 00000000..adb83b3d --- /dev/null +++ b/JellyfinPlayer tvOS/MainTabView.swift @@ -0,0 +1,51 @@ +// + /* + * 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 SwiftUI + +struct MainTabView: View { + @State private var tabSelection: Tab = .home + + var body: some View { + TabView(selection: $tabSelection) { + HomeView() + .navigationViewStyle(StackNavigationViewStyle()) + .tabItem { + Text(Tab.home.localized) + Image(systemName: "house") + } + .tag(Tab.home) + + Text("Library") + .navigationViewStyle(StackNavigationViewStyle()) + .tabItem { + Text(Tab.allMedia.localized) + Image(systemName: "folder") + } + .tag(Tab.allMedia) + } + } +} + +extension MainTabView { + enum Tab: String { + case home + case allMedia + + var localized: String { + switch self { + case .home: + return "Home" + case .allMedia: + return "All Media" + } + } + } +} diff --git a/JellyfinPlayer tvOS/NextUp/NextUpView.swift b/JellyfinPlayer tvOS/NextUp/NextUpView.swift new file mode 100644 index 00000000..4daaa8cd --- /dev/null +++ b/JellyfinPlayer tvOS/NextUp/NextUpView.swift @@ -0,0 +1,54 @@ +/* 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 Combine +import JellyfinAPI + +struct NextUpView: View { + + var items: [BaseItemDto] + + var body: some View { + VStack(alignment: .leading) { + if items.count != 0 { + Text("Next Up") + .font(.headline) + .fontWeight(.semibold) + .padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)) + ScrollView(.horizontal, showsIndicators: false) { + LazyHStack { + Spacer().frame(width: 16) + ForEach(items, id: \.id) { item in + NavigationLink(destination: EmptyView()) { + VStack(alignment: .leading) { + ImageView(src: item.getSeriesPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100), bh: item.getSeriesPrimaryImageBlurHash()) + .frame(width: 100, height: 150) + .cornerRadius(10) + Spacer().frame(height: 5) + Text(item.seriesName!) + .font(.caption) + .fontWeight(.semibold) + .foregroundColor(.primary) + .lineLimit(1) + Text("S\(item.parentIndexNumber ?? 0):E\(item.indexNumber ?? 0)") + .font(.caption) + .fontWeight(.semibold) + .foregroundColor(.secondary) + .lineLimit(1) + }.frame(width: 100) + Spacer().frame(width: 16) + } + } + } + } + .frame(height: 200) + } + } + .padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0)) + } +} diff --git a/JellyfinPlayer tvOS/PlainNavigationLinkButton.swift b/JellyfinPlayer tvOS/PlainNavigationLinkButton.swift new file mode 100644 index 00000000..2a458505 --- /dev/null +++ b/JellyfinPlayer tvOS/PlainNavigationLinkButton.swift @@ -0,0 +1,15 @@ +import SwiftUI + +struct PlainNavigationLinkButtonStyle: ButtonStyle { + func makeBody(configuration: Self.Configuration) -> some View { + PlainNavigationLinkButton(configuration: configuration) + } +} + +struct PlainNavigationLinkButton: View { + let configuration: ButtonStyle.Configuration + + var body: some View { + configuration.label + } +} diff --git a/JellyfinPlayer tvOS/README.md b/JellyfinPlayer tvOS/README.md new file mode 100644 index 00000000..9bf4fecd --- /dev/null +++ b/JellyfinPlayer tvOS/README.md @@ -0,0 +1,4 @@ +# Design Notes + +tvos is dumb and how I got around the ScrollViews clipping requires ALL interface elements to have a leading and trailing padding of 135 pt to align with the original "safe area bounds" + diff --git a/JellyfinPlayer tvOS/SplashView.swift b/JellyfinPlayer tvOS/SplashView.swift index 59217c70..9d43b740 100644 --- a/JellyfinPlayer tvOS/SplashView.swift +++ b/JellyfinPlayer tvOS/SplashView.swift @@ -11,15 +11,28 @@ import SwiftUI struct SplashView: View { @StateObject var viewModel = SplashViewModel() - + @State var showingAlert: Bool = false + var body: some View { - if viewModel.isLoggedIn { - Text("home") - } else { - NavigationView { - ConnectToServerView(isLoggedIn: $viewModel.isLoggedIn) + Group { + if viewModel.isLoggedIn { + NavigationView() { + MainTabView() + } + .padding(.leading, -60) + .padding(.trailing, -60) + } else { + NavigationView { + ConnectToServerView(isLoggedIn: $viewModel.isLoggedIn) + } + .navigationViewStyle(StackNavigationViewStyle()) } - .navigationViewStyle(StackNavigationViewStyle()) + } + .alert(isPresented: $showingAlert) { + Alert(title: Text("Important message"), message: Text("\(ServerEnvironment.current.errorMessage)"), dismissButton: .default(Text("Got it!"))) + } + .onChange(of: ServerEnvironment.current.hasErrorMessage) { hEM in + self.showingAlert = hEM } } } diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 73478ebb..dddf2636 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -7,6 +7,17 @@ objects = { /* Begin PBXBuildFile section */ + 531690E5267ABD5C005D8AB9 /* MainTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690E4267ABD5C005D8AB9 /* MainTabView.swift */; }; + 531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690E6267ABD79005D8AB9 /* HomeView.swift */; }; + 531690EC267ABF46005D8AB9 /* ContinueWatchingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690EB267ABF46005D8AB9 /* ContinueWatchingView.swift */; }; + 531690ED267ABF46005D8AB9 /* ContinueWatchingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690EB267ABF46005D8AB9 /* ContinueWatchingView.swift */; }; + 531690EF267ABF72005D8AB9 /* NextUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690EE267ABF72005D8AB9 /* NextUpView.swift */; }; + 531690F0267ABF72005D8AB9 /* NextUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690EE267ABF72005D8AB9 /* NextUpView.swift */; }; + 531690F7267ACC00005D8AB9 /* ContinueWatchingItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690F6267ACC00005D8AB9 /* ContinueWatchingItem.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 */; }; @@ -171,6 +182,14 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 531690E4267ABD5C005D8AB9 /* MainTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabView.swift; sourceTree = ""; }; + 531690E6267ABD79005D8AB9 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; + 531690EB267ABF46005D8AB9 /* ContinueWatchingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueWatchingView.swift; sourceTree = ""; }; + 531690EE267ABF72005D8AB9 /* NextUpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextUpView.swift; sourceTree = ""; }; + 531690F6267ACC00005D8AB9 /* ContinueWatchingItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueWatchingItem.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 = ""; }; @@ -288,6 +307,23 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 531690F5267ACBF2005D8AB9 /* ContinueWatching */ = { + isa = PBXGroup; + children = ( + 531690EB267ABF46005D8AB9 /* ContinueWatchingView.swift */, + 531690F6267ACC00005D8AB9 /* ContinueWatchingItem.swift */, + ); + path = ContinueWatching; + sourceTree = ""; + }; + 531690FB267AD7FA005D8AB9 /* NextUp */ = { + isa = PBXGroup; + children = ( + 531690EE267ABF72005D8AB9 /* NextUpView.swift */, + ); + path = NextUp; + sourceTree = ""; + }; 532175392671BCED005491E6 /* ViewModels */ = { isa = PBXGroup; children = ( @@ -304,7 +340,10 @@ 535870612669D21600D05A09 /* JellyfinPlayer tvOS */ = { isa = PBXGroup; children = ( + 531690FB267AD7FA005D8AB9 /* NextUp */, + 531690F5267ACBF2005D8AB9 /* ContinueWatching */, 53ABFDDA267972BF00886593 /* JellyfinPlayer tvOS.entitlements */, + 531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */, 535870622669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift */, 535870662669D21700D05A09 /* Assets.xcassets */, 5358706B2669D21700D05A09 /* PersistenceController.swift */, @@ -312,6 +351,9 @@ 535870682669D21700D05A09 /* Preview Content */, 53ABFDDD267974E300886593 /* SplashView.swift */, 53ABFDEA2679753200886593 /* ConnectToServerView.swift */, + 531690E4267ABD5C005D8AB9 /* MainTabView.swift */, + 531690E6267ABD79005D8AB9 /* HomeView.swift */, + 531690F8267AD135005D8AB9 /* README.md */, ); path = "JellyfinPlayer tvOS"; sourceTree = ""; @@ -348,12 +390,12 @@ isa = PBXGroup; children = ( 53D5E3DA264B460200BADDC8 /* Cartfile */, - 628B95252670CABD0091AF3B /* WidgetExtension */, 53D5E3DB264B47EE00BADDC8 /* Frameworks */, 5377CBF3263B596A003A4E83 /* JellyfinPlayer */, 535870612669D21600D05A09 /* JellyfinPlayer tvOS */, 5377CBF2263B596A003A4E83 /* Products */, 535870752669D60C00D05A09 /* Shared */, + 628B95252670CABD0091AF3B /* WidgetExtension */, ); sourceTree = ""; }; @@ -436,6 +478,7 @@ 6267B3D526710B8900A7371D /* CollectionExtensions.swift */, 6267B3D92671138200A7371D /* ImageExtensions.swift */, 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */, + 531690FC267AEDC5005D8AB9 /* HandleAPIRequestCompletion.swift */, ); path = Extensions; sourceTree = ""; @@ -638,18 +681,24 @@ 6267B3DC2671139500A7371D /* ImageExtensions.swift in Sources */, 53ABFDE9267974EF00886593 /* HomeViewModel.swift in Sources */, 62EC352D26766675000E9F2D /* ServerEnvironment.swift in Sources */, + 531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */, 53ABFDDE267974E300886593 /* SplashView.swift in Sources */, 53ABFDE8267974EF00886593 /* SplashViewModel.swift in Sources */, + 531690ED267ABF46005D8AB9 /* ContinueWatchingView.swift in Sources */, 62EC3530267666A5000E9F2D /* SessionManager.swift in Sources */, + 531690F7267ACC00005D8AB9 /* ContinueWatchingItem.swift in Sources */, 535870A82669D8AE00D05A09 /* StringExtensions.swift in Sources */, 53ABFDE6267974EF00886593 /* SettingsViewModel.swift in Sources */, 6267B3D826710B9800A7371D /* CollectionExtensions.swift in Sources */, 535870A52669D8AE00D05A09 /* ParallaxHeader.swift in Sources */, + 531690F0267ABF72005D8AB9 /* NextUpView.swift in Sources */, 535870A72669D8AE00D05A09 /* MultiSelectorView.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 */, @@ -657,6 +706,7 @@ 535870632669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift in Sources */, 53ABFDE4267974EF00886593 /* LibraryListViewModel.swift in Sources */, 5364F456266CA0DC0026ECBA /* APIExtensions.swift in Sources */, + 531690FA267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift in Sources */, 535870A32669D89F00D05A09 /* Model.xcdatamodeld in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -688,6 +738,7 @@ 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 */, @@ -697,12 +748,14 @@ 531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */, 62EC352F267666A5000E9F2D /* SessionManager.swift in Sources */, 535870AD2669D8DD00D05A09 /* Typings.swift in Sources */, + 531690EF267ABF72005D8AB9 /* NextUpView.swift in Sources */, 62EC352C26766675000E9F2D /* ServerEnvironment.swift in Sources */, 6267B3DA2671138200A7371D /* ImageExtensions.swift in Sources */, 62EC353426766B03000E9F2D /* DeviceRotationViewModifier.swift in Sources */, 5389277C263CC3DB0035E14B /* BlurHashDecode.swift in Sources */, 625CB56C2678C0FD00530A6E /* MainTabView.swift in Sources */, 539B2DA5263BA5B8007FF1A4 /* SettingsView.swift in Sources */, + 531690EC267ABF46005D8AB9 /* ContinueWatchingView.swift in Sources */, 5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */, 53AD124D267029D60094A276 /* SeriesItemView.swift in Sources */, 5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */, @@ -726,6 +779,7 @@ 628B95272670CABD0091AF3B /* NextUpWidget.swift in Sources */, 628B95382670CDAB0091AF3B /* Model.xcdatamodeld in Sources */, 62EC353226766849000E9F2D /* SessionManager.swift in Sources */, + 531690FF267AEDC5005D8AB9 /* HandleAPIRequestCompletion.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Shared/Extensions/HandleAPIRequestCompletion.swift b/Shared/Extensions/HandleAPIRequestCompletion.swift new file mode 100644 index 00000000..f9db9805 --- /dev/null +++ b/Shared/Extensions/HandleAPIRequestCompletion.swift @@ -0,0 +1,30 @@ +/* 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) { + switch completion { + case .finished: + break + case .failure(let error): + if let err = error as? ErrorResponse { + switch err { + case .error(401, _, _, _): + ServerEnvironment.current.errorMessage = "User unauthorized." + ServerEnvironment.current.hasErrorMessage = true + SessionManager.current.logout() + case .error: + ServerEnvironment.current.errorMessage = err.localizedDescription + ServerEnvironment.current.hasErrorMessage = true + } + } + break + } +} diff --git a/Shared/Resources/Model.xcdatamodeld/JellyfinPlayer.xcdatamodel/contents b/Shared/Resources/Model.xcdatamodeld/JellyfinPlayer.xcdatamodel/contents index 6b3aa76a..e6d94afb 100644 --- a/Shared/Resources/Model.xcdatamodeld/JellyfinPlayer.xcdatamodel/contents +++ b/Shared/Resources/Model.xcdatamodeld/JellyfinPlayer.xcdatamodel/contents @@ -1,17 +1,18 @@ - + + - + \ No newline at end of file