From 1576d9d6b7850afa6c0e32e3009210b99c5b9d74 Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Wed, 13 Oct 2021 15:58:45 -0600 Subject: [PATCH] Create new SessionManager and begin new connect flow --- .../Components/PublicUserButton.swift | 2 +- JellyfinPlayer tvOS/ConnectToServerView.swift | 2 +- JellyfinPlayer tvOS/LatestMediaView.swift | 2 +- .../VideoPlayerViewController.swift | 6 +- JellyfinPlayer.xcodeproj/project.pbxproj | 128 +++--- .../xcshareddata/swiftpm/Package.resolved | 8 +- JellyfinPlayer/App/AppDelegate.swift | 8 + JellyfinPlayer/App/JellyfinPlayerApp.swift | 2 - .../Coordinators/MainCoordinator.swift | 12 +- .../Coordinators/MainTabCoordinator.swift | 1 - .../Coordinators/ServerListCoordinator.swift | 30 ++ JellyfinPlayer/VideoPlayer.swift | 32 +- .../Views/ConnectToServerView.swift | 375 +++++++++------- JellyfinPlayer/Views/ServerDetailView.swift | 8 +- JellyfinPlayer/Views/ServerListView.swift | 34 ++ JellyfinPlayer/Views/SettingsView.swift | 7 +- .../BaseItemDtoExtensions.swift | 8 +- .../BaseItemPersonExtensions.swift | 2 +- .../ServerDiscovery.swift | 17 +- .../UDPBroadCastConnection.swift | 0 Shared/Singleton/ServerEnvironment.swift | 67 --- Shared/Singleton/SessionManager.swift | 423 ++++++++++-------- Shared/SwiftfinStore/SwiftfinStore.swift | 20 +- .../SwiftfinStore/SwiftfinStoreDefaults.swift | 29 ++ .../SwiftfinStore/SwiftfinStoreKeychain.swift | 33 ++ .../ViewModels/ConnectToServerViewModel.swift | 89 +--- Shared/ViewModels/EpisodeItemViewModel.swift | 4 +- Shared/ViewModels/HomeViewModel.swift | 6 +- Shared/ViewModels/ItemViewModel.swift | 10 +- Shared/ViewModels/LatestMediaViewModel.swift | 4 +- .../ViewModels/LibraryFilterViewModel.swift | 2 +- Shared/ViewModels/LibraryListViewModel.swift | 2 +- .../ViewModels/LibrarySearchViewModel.swift | 8 +- Shared/ViewModels/LibraryViewModel.swift | 4 +- Shared/ViewModels/SeasonItemViewModel.swift | 4 +- Shared/ViewModels/SeriesItemViewModel.swift | 4 +- Shared/ViewModels/ServerListViewModel.swift | 20 + Shared/ViewModels/SplashViewModel.swift | 3 +- Shared/ViewModels/UserLoginViewModel.swift | 47 ++ WidgetExtension/NextUpWidget.swift | 145 +++--- 40 files changed, 915 insertions(+), 693 deletions(-) create mode 100644 JellyfinPlayer/Coordinators/ServerListCoordinator.swift create mode 100644 JellyfinPlayer/Views/ServerListView.swift rename Shared/{ServerLocator => ServerDiscovery}/ServerDiscovery.swift (82%) rename Shared/{ServerLocator => ServerDiscovery}/UDPBroadCastConnection.swift (100%) delete mode 100644 Shared/Singleton/ServerEnvironment.swift create mode 100644 Shared/SwiftfinStore/SwiftfinStoreDefaults.swift create mode 100644 Shared/SwiftfinStore/SwiftfinStoreKeychain.swift create mode 100644 Shared/ViewModels/ServerListViewModel.swift create mode 100644 Shared/ViewModels/UserLoginViewModel.swift diff --git a/JellyfinPlayer tvOS/Components/PublicUserButton.swift b/JellyfinPlayer tvOS/Components/PublicUserButton.swift index ac49c59a..36943861 100644 --- a/JellyfinPlayer tvOS/Components/PublicUserButton.swift +++ b/JellyfinPlayer tvOS/Components/PublicUserButton.swift @@ -19,7 +19,7 @@ struct PublicUserButton: View { 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!)")!) + ImageView(src: URL(string: "\(SessionManager.main.currentLogin.server.uri)/Users/\(publicUser.id ?? "")/Images/Primary?width=500&quality=80&tag=\(publicUser.primaryImageTag!)")!) .frame(width: 250, height: 250) .cornerRadius(125.0) } else { diff --git a/JellyfinPlayer tvOS/ConnectToServerView.swift b/JellyfinPlayer tvOS/ConnectToServerView.swift index f42803ba..5044f6bc 100644 --- a/JellyfinPlayer tvOS/ConnectToServerView.swift +++ b/JellyfinPlayer tvOS/ConnectToServerView.swift @@ -26,7 +26,7 @@ struct ConnectToServerView: View { } 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 ?? "")")!) + ImageView(src: URL(string: "\(SessionManager.main.currentLogin.server.uri)/Users/\(viewModel.selectedPublicUser.id ?? "")/Images/Primary?width=500&quality=80&tag=\(viewModel.selectedPublicUser.primaryImageTag ?? "")")!) .frame(width: 250, height: 250) .cornerRadius(125.0) Spacer() diff --git a/JellyfinPlayer tvOS/LatestMediaView.swift b/JellyfinPlayer tvOS/LatestMediaView.swift index 582dd035..4034892a 100644 --- a/JellyfinPlayer tvOS/LatestMediaView.swift +++ b/JellyfinPlayer tvOS/LatestMediaView.swift @@ -27,7 +27,7 @@ struct LatestMediaView: View { 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) + UserLibraryAPI.getLatestMedia(userId: SessionManager.main.currentLogin.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 diff --git a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift index 875fcd06..dc996638 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift @@ -166,12 +166,12 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, // Item is being transcoded by request of server if let transcodiungUrl = mediaSource.transcodingUrl { item.videoType = .transcode - streamURL = URL(string: "\(ServerEnvironment.current.server.baseURI!)\(transcodiungUrl)")! + streamURL = URL(string: "\(SessionManager.main.currentLogin.server.uri)\(transcodiungUrl)")! } // Item will be directly played by the client else { item.videoType = .directPlay - streamURL = URL(string: "\(ServerEnvironment.current.server.baseURI!)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&deviceId=\(SessionManager.current.deviceID)&api_key=\(SessionManager.current.accessToken)&Tag=\(mediaSource.eTag!)")! + streamURL = URL(string: "\(SessionManager.main.currentLogin.server.uri)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&deviceId=\(SessionManager.current.deviceID)&api_key=\(SessionManager.current.accessToken)&Tag=\(mediaSource.eTag!)")! } item.videoUrl = streamURL @@ -186,7 +186,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, var deliveryUrl: URL? if stream.deliveryMethod == .external { - deliveryUrl = URL(string: "\(ServerEnvironment.current.server.baseURI!)\(stream.deliveryUrl!)")! + deliveryUrl = URL(string: "\(SessionManager.main.currentLogin.server.uri)\(stream.deliveryUrl!)")! } let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec ?? "webvtt", languageCode: stream.language ?? "") diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 840fe36a..e4b17dd3 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -71,7 +71,6 @@ 535870AD2669D8DD00D05A09 /* Typings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535870AC2669D8DD00D05A09 /* Typings.swift */; }; 535BAE9F2649E569005FA86D /* ItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535BAE9E2649E569005FA86D /* ItemView.swift */; }; 535BAEA5264A151C005FA86D /* VideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535BAEA4264A151C005FA86D /* VideoPlayer.swift */; }; - 53628C6D26B5AA0D008A64A0 /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = 53628C6C26B5AA0D008A64A0 /* Defaults */; }; 53649AAD269CFAEA00A2D8B7 /* Puppy in Frameworks */ = {isa = PBXBuildFile; productRef = 53649AAC269CFAEA00A2D8B7 /* Puppy */; }; 53649AAF269CFAF600A2D8B7 /* Puppy in Frameworks */ = {isa = PBXBuildFile; productRef = 53649AAE269CFAF600A2D8B7 /* Puppy */; }; 53649AB1269CFB1900A2D8B7 /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53649AB0269CFB1900A2D8B7 /* LogManager.swift */; }; @@ -177,8 +176,6 @@ 6220D0B426D5ED8000B8E046 /* LibraryCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0B326D5ED8000B8E046 /* LibraryCoordinator.swift */; }; 6220D0B726D5EE1100B8E046 /* SearchCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */; }; 6220D0BA26D6092100B8E046 /* FilterCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0B926D6092100B8E046 /* FilterCoordinator.swift */; }; - 6220D0BD26D60D6600B8E046 /* ItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0BC26D60D6600B8E046 /* ItemViewModel.swift */; }; - 6220D0BE26D60D6600B8E046 /* ItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0BC26D60D6600B8E046 /* ItemViewModel.swift */; }; 6220D0C026D61C5000B8E046 /* ItemCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */; }; 6220D0C626D62D8700B8E046 /* VideoPlayerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0C526D62D8700B8E046 /* VideoPlayerCoordinator.swift */; }; 6220D0C726D62D8700B8E046 /* VideoPlayerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0C526D62D8700B8E046 /* VideoPlayerCoordinator.swift */; }; @@ -217,8 +214,6 @@ 62C29EA326D1030F00C1D2E7 /* ConnectToServerCoodinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29EA226D1030F00C1D2E7 /* ConnectToServerCoodinator.swift */; }; 62C29EA626D1036A00C1D2E7 /* HomeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29EA526D1036A00C1D2E7 /* HomeCoordinator.swift */; }; 62C29EA826D103D500C1D2E7 /* LibraryListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29EA726D103D500C1D2E7 /* LibraryListCoordinator.swift */; }; - 62CB3F462685BAF7003D0A6F /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = 62CB3F452685BAF7003D0A6F /* Defaults */; }; - 62CB3F482685BB3B003D0A6F /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = 62CB3F472685BB3B003D0A6F /* Defaults */; }; 62CB3F4B2685BB77003D0A6F /* DefaultsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62CB3F4A2685BB77003D0A6F /* DefaultsExtension.swift */; }; 62CB3F4C2685BB77003D0A6F /* DefaultsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62CB3F4A2685BB77003D0A6F /* DefaultsExtension.swift */; }; 62D8535B26FC631300FDFC59 /* MainCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29E9E26D1016600C1D2E7 /* MainCoordinator.swift */; }; @@ -240,11 +235,8 @@ 62E632F0267D43320063E547 /* LibraryFilterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632EE267D43320063E547 /* LibraryFilterViewModel.swift */; }; 62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632F2267D54030063E547 /* ItemViewModel.swift */; }; 62E632F4267D54030063E547 /* ItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632F2267D54030063E547 /* ItemViewModel.swift */; }; - 62EC352C26766675000E9F2D /* ServerEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352B26766675000E9F2D /* ServerEnvironment.swift */; }; - 62EC352D26766675000E9F2D /* ServerEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352B26766675000E9F2D /* ServerEnvironment.swift */; }; 62EC352F267666A5000E9F2D /* SessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352E267666A5000E9F2D /* SessionManager.swift */; }; 62EC3530267666A5000E9F2D /* SessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352E267666A5000E9F2D /* SessionManager.swift */; }; - 62EC353126766848000E9F2D /* ServerEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352B26766675000E9F2D /* ServerEnvironment.swift */; }; 62EC353226766849000E9F2D /* SessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352E267666A5000E9F2D /* SessionManager.swift */; }; 62EC353426766B03000E9F2D /* DeviceRotationViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */; }; 62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62ECA01726FA685A00E8EBB7 /* DeepLink.swift */; }; @@ -267,6 +259,23 @@ E13DD3CB27164BA8009D4DAF /* UIDeviceExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3C727164B1E009D4DAF /* UIDeviceExtensions.swift */; }; E13DD3CD27164CA7009D4DAF /* CoreStore in Frameworks */ = {isa = PBXBuildFile; productRef = E13DD3CC27164CA7009D4DAF /* CoreStore */; }; E13DD3CF27164E1F009D4DAF /* CoreStore in Frameworks */ = {isa = PBXBuildFile; productRef = E13DD3CE27164E1F009D4DAF /* CoreStore */; }; + E13DD3D327168E65009D4DAF /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = E13DD3D227168E65009D4DAF /* Defaults */; }; + E13DD3D5271693CD009D4DAF /* SwiftfinStoreDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3D4271693CD009D4DAF /* SwiftfinStoreDefaults.swift */; }; + E13DD3D6271693CD009D4DAF /* SwiftfinStoreDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3D4271693CD009D4DAF /* SwiftfinStoreDefaults.swift */; }; + E13DD3D7271693CD009D4DAF /* SwiftfinStoreDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3D4271693CD009D4DAF /* SwiftfinStoreDefaults.swift */; }; + E13DD3D927169406009D4DAF /* SwiftfinStoreKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3D827169406009D4DAF /* SwiftfinStoreKeychain.swift */; }; + E13DD3DA27169406009D4DAF /* SwiftfinStoreKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3D827169406009D4DAF /* SwiftfinStoreKeychain.swift */; }; + E13DD3DB27169406009D4DAF /* SwiftfinStoreKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3D827169406009D4DAF /* SwiftfinStoreKeychain.swift */; }; + E13DD3DD27175CE3009D4DAF /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = E13DD3DC27175CE3009D4DAF /* Defaults */; }; + E13DD3DF27175CEA009D4DAF /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = E13DD3DE27175CEA009D4DAF /* Defaults */; }; + E13DD3E127176BD3009D4DAF /* ServerListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3E027176BD3009D4DAF /* ServerListViewModel.swift */; }; + E13DD3E227176BD3009D4DAF /* ServerListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3E027176BD3009D4DAF /* ServerListViewModel.swift */; }; + E13DD3E527177D15009D4DAF /* ServerListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3E427177D15009D4DAF /* ServerListView.swift */; }; + E13DD3E627177D15009D4DAF /* ServerListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3E427177D15009D4DAF /* ServerListView.swift */; }; + E13DD3E927177ED6009D4DAF /* ServerListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3E827177ED6009D4DAF /* ServerListCoordinator.swift */; }; + E13DD3EA27177ED6009D4DAF /* ServerListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3E827177ED6009D4DAF /* ServerListCoordinator.swift */; }; + E13DD3EC27178A54009D4DAF /* UserLoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3EB27178A54009D4DAF /* UserLoginViewModel.swift */; }; + E13DD3ED27178A54009D4DAF /* UserLoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3EB27178A54009D4DAF /* UserLoginViewModel.swift */; }; E14F7D0726DB36EF007C3AE6 /* ItemPortraitMainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14F7D0626DB36EF007C3AE6 /* ItemPortraitMainView.swift */; }; E14F7D0926DB36F7007C3AE6 /* ItemLandscapeMainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14F7D0826DB36F7007C3AE6 /* ItemLandscapeMainView.swift */; }; E173DA5026D048D600CC4EB7 /* ServerDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */; }; @@ -449,7 +458,6 @@ 6220D0B326D5ED8000B8E046 /* LibraryCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryCoordinator.swift; sourceTree = ""; }; 6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchCoordinator.swift; sourceTree = ""; }; 6220D0B926D6092100B8E046 /* FilterCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterCoordinator.swift; sourceTree = ""; }; - 6220D0BC26D60D6600B8E046 /* ItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemViewModel.swift; sourceTree = ""; }; 6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemCoordinator.swift; sourceTree = ""; }; 6220D0C526D62D8700B8E046 /* VideoPlayerCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerCoordinator.swift; sourceTree = ""; }; 6220D0CB26D640C400B8E046 /* AppURLHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppURLHandler.swift; sourceTree = ""; }; @@ -488,7 +496,6 @@ 62E632EB267D410B0063E547 /* SeriesItemViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeriesItemViewModel.swift; sourceTree = ""; }; 62E632EE267D43320063E547 /* LibraryFilterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryFilterViewModel.swift; sourceTree = ""; }; 62E632F2267D54030063E547 /* ItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemViewModel.swift; sourceTree = ""; }; - 62EC352B26766675000E9F2D /* ServerEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerEnvironment.swift; sourceTree = ""; }; 62EC352E267666A5000E9F2D /* SessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionManager.swift; sourceTree = ""; }; 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRotationViewModifier.swift; sourceTree = ""; }; 62ECA01726FA685A00E8EBB7 /* DeepLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLink.swift; sourceTree = ""; }; @@ -504,6 +511,12 @@ E13DD3BE27163DD7009D4DAF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; E13DD3C127164941009D4DAF /* SwiftfinStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftfinStore.swift; sourceTree = ""; }; E13DD3C727164B1E009D4DAF /* UIDeviceExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIDeviceExtensions.swift; sourceTree = ""; }; + E13DD3D4271693CD009D4DAF /* SwiftfinStoreDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftfinStoreDefaults.swift; sourceTree = ""; }; + E13DD3D827169406009D4DAF /* SwiftfinStoreKeychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftfinStoreKeychain.swift; sourceTree = ""; }; + E13DD3E027176BD3009D4DAF /* ServerListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerListViewModel.swift; sourceTree = ""; }; + E13DD3E427177D15009D4DAF /* ServerListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerListView.swift; sourceTree = ""; }; + E13DD3E827177ED6009D4DAF /* ServerListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerListCoordinator.swift; sourceTree = ""; }; + E13DD3EB27178A54009D4DAF /* UserLoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLoginViewModel.swift; sourceTree = ""; }; E14F7D0626DB36EF007C3AE6 /* ItemPortraitMainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemPortraitMainView.swift; sourceTree = ""; }; E14F7D0826DB36F7007C3AE6 /* ItemLandscapeMainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemLandscapeMainView.swift; sourceTree = ""; }; E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailView.swift; sourceTree = ""; }; @@ -536,7 +549,7 @@ 53A431BF266B0FFE0016769F /* JellyfinAPI in Frameworks */, 535870912669D7A800D05A09 /* Introspect in Frameworks */, 6261A0E026A0AB710072EF1C /* CombineExt in Frameworks */, - 62CB3F482685BB3B003D0A6F /* Defaults in Frameworks */, + E13DD3DF27175CEA009D4DAF /* Defaults in Frameworks */, 53272535268BF9710035FBF1 /* SwiftUIFocusGuide in Frameworks */, 5358708D2669D7A800D05A09 /* KeychainSwift in Frameworks */, 536D3D84267BEA550004248C /* ParallaxView in Frameworks */, @@ -551,9 +564,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + E13DD3D327168E65009D4DAF /* Defaults in Frameworks */, 53649AAD269CFAEA00A2D8B7 /* Puppy in Frameworks */, 62C29E9C26D0FE4200C1D2E7 /* Stinsen in Frameworks */, - 62CB3F462685BAF7003D0A6F /* Defaults in Frameworks */, 5338F757263B7E2E0014BF09 /* KeychainSwift in Frameworks */, 53EC6E25267EB10F006DD26A /* SwiftyJSON in Frameworks */, 53EC6E21267E80B1006DD26A /* Pods_JellyfinPlayer_iOS.framework in Frameworks */, @@ -570,10 +583,10 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 53628C6D26B5AA0D008A64A0 /* Defaults in Frameworks */, 628B95332670CAEA0091AF3B /* NukeUI in Frameworks */, 628B95242670CABD0091AF3B /* SwiftUI.framework in Frameworks */, 531ABF6C2671F5CC00C0FE20 /* WidgetKit.framework in Frameworks */, + E13DD3DD27175CE3009D4DAF /* Defaults in Frameworks */, 53649AB5269D423A00A2D8B7 /* Puppy in Frameworks */, 536D3D7D267BD5F90004248C /* ActivityIndicator in Frameworks */, 628B953A2670CE250091AF3B /* KeychainSwift in Frameworks */, @@ -585,13 +598,13 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 091B5A852683142E00D78B61 /* ServerLocator */ = { + 091B5A852683142E00D78B61 /* ServerDiscovery */ = { isa = PBXGroup; children = ( 091B5A872683142E00D78B61 /* ServerDiscovery.swift */, 091B5A882683142E00D78B61 /* UDPBroadCastConnection.swift */, ); - path = ServerLocator; + path = ServerDiscovery; sourceTree = ""; }; 5310694F2684E7EE00CFFDBA /* VideoPlayer */ = { @@ -612,9 +625,9 @@ isa = PBXGroup; children = ( 625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */, - 62E632F2267D54030063E547 /* ItemViewModel.swift */, 62E632E5267D3F5B0063E547 /* EpisodeItemViewModel.swift */, 625CB5722678C32A00530A6E /* HomeViewModel.swift */, + 62E632F2267D54030063E547 /* ItemViewModel.swift */, 62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */, 62E632EE267D43320063E547 /* LibraryFilterViewModel.swift */, 625CB5742678C33500530A6E /* LibraryListViewModel.swift */, @@ -625,11 +638,12 @@ 62E632E8267D3FF50063E547 /* SeasonItemViewModel.swift */, 62E632EB267D410B0063E547 /* SeriesItemViewModel.swift */, E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */, + E13DD3E027176BD3009D4DAF /* ServerListViewModel.swift */, 5321753A2671BCFC005491E6 /* SettingsViewModel.swift */, 625CB5692678B71200530A6E /* SplashViewModel.swift */, + E13DD3EB27178A54009D4DAF /* UserLoginViewModel.swift */, 09389CC626819B4500AE350E /* VideoPlayerModel.swift */, 625CB57B2678CE1000530A6E /* ViewModel.swift */, - 6220D0BC26D60D6600B8E046 /* ItemViewModel.swift */, ); path = ViewModels; sourceTree = ""; @@ -730,7 +744,7 @@ 621338912660106C00A81A2A /* Extensions */, 535870AB2669D8D300D05A09 /* Objects */, AE8C3157265D6F5E008AA076 /* Resources */, - 091B5A852683142E00D78B61 /* ServerLocator */, + 091B5A852683142E00D78B61 /* ServerDiscovery */, 62EC352A26766657000E9F2D /* Singleton */, 532175392671BCED005491E6 /* ViewModels */, E1AD105326D96F5A003E4A08 /* Views */, @@ -1014,6 +1028,7 @@ 62C29E9E26D1016600C1D2E7 /* MainCoordinator.swift */, 62C29EA026D102A500C1D2E7 /* MainTabCoordinator.swift */, 6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */, + E13DD3E827177ED6009D4DAF /* ServerListCoordinator.swift */, 6220D0B026D5EC9900B8E046 /* SettingsCoordinator.swift */, 6220D0C526D62D8700B8E046 /* VideoPlayerCoordinator.swift */, ); @@ -1025,7 +1040,6 @@ children = ( 536D3D73267BA8170004248C /* BackgroundManager.swift */, 53649AB0269CFB1900A2D8B7 /* LogManager.swift */, - 62EC352B26766675000E9F2D /* ServerEnvironment.swift */, 62EC352E267666A5000E9F2D /* SessionManager.swift */, ); path = Singleton; @@ -1074,6 +1088,8 @@ isa = PBXGroup; children = ( E13DD3C127164941009D4DAF /* SwiftfinStore.swift */, + E13DD3D4271693CD009D4DAF /* SwiftfinStoreDefaults.swift */, + E13DD3D827169406009D4DAF /* SwiftfinStoreKeychain.swift */, ); path = SwiftfinStore; sourceTree = ""; @@ -1093,6 +1109,7 @@ 53892771263C8C6F0035E14B /* LoadingView.swift */, 5389276F263C25230035E14B /* NextUpView.swift */, E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */, + E13DD3E427177D15009D4DAF /* ServerListView.swift */, 539B2DA4263BA5B8007FF1A4 /* SettingsView.swift */, 625CB5672678B6FB00530A6E /* SplashView.swift */, 53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */, @@ -1188,12 +1205,12 @@ 53A431BE266B0FFE0016769F /* JellyfinAPI */, 53ABFDEC26799D7700886593 /* ActivityIndicator */, 536D3D83267BEA550004248C /* ParallaxView */, - 62CB3F472685BB3B003D0A6F /* Defaults */, 53272534268BF9710035FBF1 /* SwiftUIFocusGuide */, 53649AAE269CFAF600A2D8B7 /* Puppy */, 6261A0DF26A0AB710072EF1C /* CombineExt */, 6220D0C826D63F3700B8E046 /* Stinsen */, E13DD3CC27164CA7009D4DAF /* CoreStore */, + E13DD3DE27175CEA009D4DAF /* Defaults */, ); productName = "JellyfinPlayer tvOS"; productReference = 535870602669D21600D05A09 /* JellyfinPlayer tvOS.app */; @@ -1225,11 +1242,11 @@ 53A431BC266B0FF20016769F /* JellyfinAPI */, 625CB5792678C4A400530A6E /* ActivityIndicator */, 53EC6E24267EB10F006DD26A /* SwiftyJSON */, - 62CB3F452685BAF7003D0A6F /* Defaults */, 53649AAC269CFAEA00A2D8B7 /* Puppy */, 6260FFF826A09754003FA968 /* CombineExt */, 62C29E9B26D0FE4200C1D2E7 /* Stinsen */, E13DD3C52716499E009D4DAF /* CoreStore */, + E13DD3D227168E65009D4DAF /* Defaults */, ); productName = JellyfinPlayer; productReference = 5377CBF1263B596A003A4E83 /* JellyfinPlayer iOS.app */; @@ -1254,8 +1271,8 @@ 628B95392670CE250091AF3B /* KeychainSwift */, 536D3D7C267BD5F90004248C /* ActivityIndicator */, 53649AB4269D423A00A2D8B7 /* Puppy */, - 53628C6C26B5AA0D008A64A0 /* Defaults */, E13DD3CE27164E1F009D4DAF /* CoreStore */, + E13DD3DC27175CE3009D4DAF /* Defaults */, ); productName = WidgetExtensionExtension; productReference = 628B95202670CABD0091AF3B /* WidgetExtension.appex */; @@ -1315,12 +1332,12 @@ 625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */, 536D3D82267BEA550004248C /* XCRemoteSwiftPackageReference "ParallaxView" */, 53EC6E23267EB10F006DD26A /* XCRemoteSwiftPackageReference "SwiftyJSON" */, - 62CB3F442685BAF7003D0A6F /* XCRemoteSwiftPackageReference "Defaults" */, 53272533268BF9710035FBF1 /* XCRemoteSwiftPackageReference "SwiftUIFocusGuide" */, 53649AAB269CFAEA00A2D8B7 /* XCRemoteSwiftPackageReference "Puppy" */, 6260FFF726A09754003FA968 /* XCRemoteSwiftPackageReference "CombineExt" */, 62C29E9A26D0FE4100C1D2E7 /* XCRemoteSwiftPackageReference "stinsen" */, E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore" */, + E13DD3D127168E65009D4DAF /* XCRemoteSwiftPackageReference "Defaults" */, ); productRefGroup = 5377CBF2263B596A003A4E83 /* Products */; projectDirPath = ""; @@ -1524,12 +1541,13 @@ C4E5081D2703F8370045C9AB /* LibrarySearchView.swift in Sources */, 53ABFDE9267974EF00886593 /* HomeViewModel.swift in Sources */, 53116A17268B919A003024C9 /* SeriesItemView.swift in Sources */, - 62EC352D26766675000E9F2D /* ServerEnvironment.swift in Sources */, + E13DD3DA27169406009D4DAF /* SwiftfinStoreKeychain.swift in Sources */, 531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */, 53ABFDDE267974E300886593 /* SplashView.swift in Sources */, 53ABFDE8267974EF00886593 /* SplashViewModel.swift in Sources */, 62E632DE267D2E170063E547 /* LatestMediaViewModel.swift in Sources */, E1FCD09726C47118007C8DCF /* ErrorMessage.swift in Sources */, + E13DD3EA27177ED6009D4DAF /* ServerListCoordinator.swift in Sources */, 53116A19268B947A003024C9 /* PlainLinkButton.swift in Sources */, 536D3D88267C17350004248C /* PublicUserButton.swift in Sources */, 62E632EA267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */, @@ -1539,8 +1557,10 @@ 091B5A8E268315D400D78B61 /* UDPBroadCastConnection.swift in Sources */, E1FCD08926C35A0D007C8DCF /* NetworkError.swift in Sources */, 531690ED267ABF46005D8AB9 /* ContinueWatchingView.swift in Sources */, + E13DD3ED27178A54009D4DAF /* UserLoginViewModel.swift in Sources */, 62EC3530267666A5000E9F2D /* SessionManager.swift in Sources */, E1AD104B26D94822003E4A08 /* DetailItem.swift in Sources */, + E13DD3E227176BD3009D4DAF /* ServerListViewModel.swift in Sources */, 53272539268C20100035FBF1 /* EpisodeItemView.swift in Sources */, 531690F7267ACC00005D8AB9 /* LandscapeItemElement.swift in Sources */, 62E632E1267D30CA0063E547 /* LibraryViewModel.swift in Sources */, @@ -1574,6 +1594,7 @@ 62E632E4267D3BA60063E547 /* MovieItemViewModel.swift in Sources */, 5358706C2669D21700D05A09 /* PersistenceController.swift in Sources */, 53649AB2269D019100A2D8B7 /* LogManager.swift in Sources */, + E13DD3D6271693CD009D4DAF /* SwiftfinStoreDefaults.swift in Sources */, 535870AA2669D8AE00D05A09 /* BlurHashDecode.swift in Sources */, 53ABFDE5267974EF00886593 /* ViewModel.swift in Sources */, C45B29BB26FAC5B600CEF5E0 /* ColorExtension.swift in Sources */, @@ -1592,10 +1613,10 @@ C4E5081B2703F82A0045C9AB /* LibraryListView.swift in Sources */, 536D3D74267BA8170004248C /* BackgroundManager.swift in Sources */, 535870632669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift in Sources */, + E13DD3E627177D15009D4DAF /* ServerListView.swift in Sources */, 53ABFDE4267974EF00886593 /* LibraryListViewModel.swift in Sources */, 5364F456266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */, 5364F456266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */, - 6220D0BE26D60D6600B8E046 /* ItemViewModel.swift in Sources */, 531690FA267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift in Sources */, E131691826C583BC0074BFEE /* LogConstructor.swift in Sources */, E1AD105726D981CE003E4A08 /* PortraitHStackView.swift in Sources */, @@ -1620,7 +1641,6 @@ 625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */, 62C29EA826D103D500C1D2E7 /* LibraryListCoordinator.swift in Sources */, 62E632DC267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */, - 6220D0BD26D60D6600B8E046 /* ItemViewModel.swift in Sources */, 62C29E9F26D1016600C1D2E7 /* MainCoordinator.swift in Sources */, 5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */, 53F866442687A45F00DCD1D7 /* PortraitItemView.swift in Sources */, @@ -1629,10 +1649,12 @@ 535BAE9F2649E569005FA86D /* ItemView.swift in Sources */, 6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */, 6220D0AD26D5EABB00B8E046 /* ViewExtensions.swift in Sources */, + E13DD3EC27178A54009D4DAF /* UserLoginViewModel.swift in Sources */, 625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */, 536D3D78267BD5C30004248C /* ViewModel.swift in Sources */, 62CB3F4B2685BB77003D0A6F /* DefaultsExtension.swift in Sources */, E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */, + E13DD3E527177D15009D4DAF /* ServerListView.swift in Sources */, E18845F826DEA9C900B0C5B7 /* ItemViewBody.swift in Sources */, E173DA5426D050F500CC4EB7 /* ServerDetailViewModel.swift in Sources */, E188460426DEF04800B0C5B7 /* EpisodeCardVStackView.swift in Sources */, @@ -1662,6 +1684,7 @@ 6267B3D626710B8900A7371D /* CollectionExtensions.swift in Sources */, E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */, 53649AB1269CFB1900A2D8B7 /* LogManager.swift in Sources */, + E13DD3E127176BD3009D4DAF /* ServerListViewModel.swift in Sources */, 62E632E9267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */, 625CB56A2678B71200530A6E /* SplashViewModel.swift in Sources */, 62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */, @@ -1676,6 +1699,7 @@ 6220D0B126D5EC9900B8E046 /* SettingsCoordinator.swift in Sources */, 62C29EA626D1036A00C1D2E7 /* HomeCoordinator.swift in Sources */, 531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */, + E13DD3E927177ED6009D4DAF /* ServerListCoordinator.swift in Sources */, E13DD3BD27163C63009D4DAF /* EmailHelper.swift in Sources */, E13DD3C227164941009D4DAF /* SwiftfinStore.swift in Sources */, E1AD104A26D94822003E4A08 /* DetailItem.swift in Sources */, @@ -1686,13 +1710,14 @@ 62E632E3267D3BA60063E547 /* MovieItemViewModel.swift in Sources */, 091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */, 62E632EF267D43320063E547 /* LibraryFilterViewModel.swift in Sources */, + E13DD3D927169406009D4DAF /* SwiftfinStoreKeychain.swift in Sources */, E13DD3C827164B1E009D4DAF /* UIDeviceExtensions.swift in Sources */, E1AD104D26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */, E13DD3BF27163DD7009D4DAF /* AppDelegate.swift in Sources */, 535870AD2669D8DD00D05A09 /* Typings.swift in Sources */, E1AD105F26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift in Sources */, + E13DD3D5271693CD009D4DAF /* SwiftfinStoreDefaults.swift in Sources */, 6220D0BA26D6092100B8E046 /* FilterCoordinator.swift in Sources */, - 62EC352C26766675000E9F2D /* ServerEnvironment.swift in Sources */, 6267B3DA2671138200A7371D /* ImageExtensions.swift in Sources */, 62EC353426766B03000E9F2D /* DeviceRotationViewModifier.swift in Sources */, 5389277C263CC3DB0035E14B /* BlurHashDecode.swift in Sources */, @@ -1714,7 +1739,6 @@ files = ( 53649AB3269D3F5B00A2D8B7 /* LogManager.swift in Sources */, E13DD3CB27164BA8009D4DAF /* UIDeviceExtensions.swift in Sources */, - 62EC353126766848000E9F2D /* ServerEnvironment.swift in Sources */, 6267B3D726710B9700A7371D /* CollectionExtensions.swift in Sources */, 628B953C2670D2430091AF3B /* StringExtensions.swift in Sources */, 6267B3DB2671139400A7371D /* ImageExtensions.swift in Sources */, @@ -1725,9 +1749,11 @@ 628B95272670CABD0091AF3B /* NextUpWidget.swift in Sources */, 6220D0AF26D5EABE00B8E046 /* ViewExtensions.swift in Sources */, 628B95382670CDAB0091AF3B /* Model.xcdatamodeld in Sources */, + E13DD3D7271693CD009D4DAF /* SwiftfinStoreDefaults.swift in Sources */, E1FCD09926C4F358007C8DCF /* NetworkError.swift in Sources */, E131691926C583BC0074BFEE /* LogConstructor.swift in Sources */, E13DD3CA27164B80009D4DAF /* SwiftfinStore.swift in Sources */, + E13DD3DB27169406009D4DAF /* SwiftfinStoreKeychain.swift in Sources */, 62EC353226766849000E9F2D /* SessionManager.swift in Sources */, 536D3D79267BD5D00004248C /* ViewModel.swift in Sources */, ); @@ -2312,14 +2338,6 @@ minimumVersion = 2.0.2; }; }; - 62CB3F442685BAF7003D0A6F /* XCRemoteSwiftPackageReference "Defaults" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/acvigue/Defaults"; - requirement = { - branch = main; - kind = branch; - }; - }; E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/JohnEstropia/CoreStore.git"; @@ -2328,6 +2346,14 @@ version = 8.1.0; }; }; + E13DD3D127168E65009D4DAF /* XCRemoteSwiftPackageReference "Defaults" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/sindresorhus/Defaults"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 6.0.0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -2361,11 +2387,6 @@ package = 621C637E26672A30004216EA /* XCRemoteSwiftPackageReference "NukeUI" */; productName = NukeUI; }; - 53628C6C26B5AA0D008A64A0 /* Defaults */ = { - isa = XCSwiftPackageProductDependency; - package = 62CB3F442685BAF7003D0A6F /* XCRemoteSwiftPackageReference "Defaults" */; - productName = Defaults; - }; 53649AAC269CFAEA00A2D8B7 /* Puppy */ = { isa = XCSwiftPackageProductDependency; package = 53649AAB269CFAEA00A2D8B7 /* XCRemoteSwiftPackageReference "Puppy" */; @@ -2456,16 +2477,6 @@ package = 62C29E9A26D0FE4100C1D2E7 /* XCRemoteSwiftPackageReference "stinsen" */; productName = Stinsen; }; - 62CB3F452685BAF7003D0A6F /* Defaults */ = { - isa = XCSwiftPackageProductDependency; - package = 62CB3F442685BAF7003D0A6F /* XCRemoteSwiftPackageReference "Defaults" */; - productName = Defaults; - }; - 62CB3F472685BB3B003D0A6F /* Defaults */ = { - isa = XCSwiftPackageProductDependency; - package = 62CB3F442685BAF7003D0A6F /* XCRemoteSwiftPackageReference "Defaults" */; - productName = Defaults; - }; E13DD3C52716499E009D4DAF /* CoreStore */ = { isa = XCSwiftPackageProductDependency; package = E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore" */; @@ -2481,6 +2492,21 @@ package = E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore" */; productName = CoreStore; }; + E13DD3D227168E65009D4DAF /* Defaults */ = { + isa = XCSwiftPackageProductDependency; + package = E13DD3D127168E65009D4DAF /* XCRemoteSwiftPackageReference "Defaults" */; + productName = Defaults; + }; + E13DD3DC27175CE3009D4DAF /* Defaults */ = { + isa = XCSwiftPackageProductDependency; + package = E13DD3D127168E65009D4DAF /* XCRemoteSwiftPackageReference "Defaults" */; + productName = Defaults; + }; + E13DD3DE27175CEA009D4DAF /* Defaults */ = { + isa = XCSwiftPackageProductDependency; + package = E13DD3D127168E65009D4DAF /* XCRemoteSwiftPackageReference "Defaults" */; + productName = Defaults; + }; /* End XCSwiftPackageProductDependency section */ /* Begin XCVersionGroup section */ diff --git a/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved b/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7c5ca76a..d524c9b5 100644 --- a/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -48,11 +48,11 @@ }, { "package": "Defaults", - "repositoryURL": "https://github.com/acvigue/Defaults", + "repositoryURL": "https://github.com/sindresorhus/Defaults", "state": { - "branch": "main", - "revision": "a4153b523ab3df9f5e3f70e9cfe9c54bed98c7e3", - "version": null + "branch": null, + "revision": "8a6e4a96fd38504a05903d136c85634b65fd7c4d", + "version": "6.0.0" } }, { diff --git a/JellyfinPlayer/App/AppDelegate.swift b/JellyfinPlayer/App/AppDelegate.swift index e887b225..c696bf56 100644 --- a/JellyfinPlayer/App/AppDelegate.swift +++ b/JellyfinPlayer/App/AppDelegate.swift @@ -11,6 +11,14 @@ import UIKit class AppDelegate: NSObject, UIApplicationDelegate { static var orientationLock = UIInterfaceOrientationMask.all + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { + + // Lazily initialize datastack + let _ = SwiftfinStore.dataStack + + return true + } func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { AppDelegate.orientationLock diff --git a/JellyfinPlayer/App/JellyfinPlayerApp.swift b/JellyfinPlayer/App/JellyfinPlayerApp.swift index 624dee3e..2e05a2b3 100644 --- a/JellyfinPlayer/App/JellyfinPlayerApp.swift +++ b/JellyfinPlayer/App/JellyfinPlayerApp.swift @@ -146,8 +146,6 @@ struct JellyfinPlayerApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate @Default(.appAppearance) var appAppearance - let persistenceController = PersistenceController.shared - var body: some Scene { WindowGroup { MainCoordinator().view() diff --git a/JellyfinPlayer/Coordinators/MainCoordinator.swift b/JellyfinPlayer/Coordinators/MainCoordinator.swift index f6381804..7b723617 100644 --- a/JellyfinPlayer/Coordinators/MainCoordinator.swift +++ b/JellyfinPlayer/Coordinators/MainCoordinator.swift @@ -20,13 +20,13 @@ import SwiftUI var stack: NavigationStack @Root var mainTab = makeMainTab - @Root var connectToServer = makeConnectToServer + @Root var serverList = makeServerList init() { - if ServerEnvironment.current.server != nil, SessionManager.current.user != nil { + if SessionManager.main.currentLogin != nil { self.stack = NavigationStack(initial: \MainCoordinator.mainTab) } else { - self.stack = NavigationStack(initial: \MainCoordinator.connectToServer) + self.stack = NavigationStack(initial: \MainCoordinator.serverList) } ImageCache.shared.costLimit = 125 * 1024 * 1024 // 125MB memory @@ -50,7 +50,7 @@ import SwiftUI @objc func didLogOut() { LogManager.shared.log.info("Received `didSignOut` from NSNotificationCenter.") - root(\.connectToServer) + root(\.serverList) } @objc func processDeepLink(_ notification: Notification) { @@ -70,8 +70,8 @@ import SwiftUI MainTabCoordinator() } - func makeConnectToServer() -> NavigationViewCoordinator { - NavigationViewCoordinator(ConnectToServerCoodinator()) + func makeServerList() -> NavigationViewCoordinator { + NavigationViewCoordinator(ServerListCoordinator()) } } diff --git a/JellyfinPlayer/Coordinators/MainTabCoordinator.swift b/JellyfinPlayer/Coordinators/MainTabCoordinator.swift index d5f430ab..52bcbca7 100644 --- a/JellyfinPlayer/Coordinators/MainTabCoordinator.swift +++ b/JellyfinPlayer/Coordinators/MainTabCoordinator.swift @@ -9,7 +9,6 @@ import Foundation import SwiftUI - import Stinsen final class MainTabCoordinator: TabCoordinatable { diff --git a/JellyfinPlayer/Coordinators/ServerListCoordinator.swift b/JellyfinPlayer/Coordinators/ServerListCoordinator.swift new file mode 100644 index 00000000..0dbdb704 --- /dev/null +++ b/JellyfinPlayer/Coordinators/ServerListCoordinator.swift @@ -0,0 +1,30 @@ +// + /* + * 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 Stinsen +import SwiftUI + +final class ServerListCoordinator: NavigationCoordinatable { + let stack = NavigationStack(initial: \ServerListCoordinator.start) + + @Root var start = makeStart + @Route(.push) var connectToServer = makeConnectToServer +// @Route(.push) var loginUser = makeLoginuser + + func makeConnectToServer() -> ConnectToServerCoodinator { + ConnectToServerCoodinator() + } + +// func makeLoginUser -> + + @ViewBuilder func makeStart() -> some View { + ServerListView(viewModel: ServerListViewModel()) + } +} diff --git a/JellyfinPlayer/VideoPlayer.swift b/JellyfinPlayer/VideoPlayer.swift index cf29da12..35c5f0c8 100644 --- a/JellyfinPlayer/VideoPlayer.swift +++ b/JellyfinPlayer/VideoPlayer.swift @@ -518,13 +518,13 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe let builder = DeviceProfileBuilder() builder.setMaxBitrate(bitrate: maxBitrate) let profile = builder.buildProfile() - let playbackInfo = PlaybackInfoDto(userId: SessionManager.current.user.user_id!, maxStreamingBitrate: Int(maxBitrate), + let playbackInfo = PlaybackInfoDto(userId: SessionManager.main.currentLogin.user.id, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, deviceProfile: profile, autoOpenLiveStream: true) DispatchQueue.global(qos: .userInitiated).async { [self] in delegate?.showLoadingView(self) - MediaInfoAPI.getPostedPlaybackInfo(itemId: manifest.id!, userId: SessionManager.current.user.user_id!, + MediaInfoAPI.getPostedPlaybackInfo(itemId: manifest.id!, userId: SessionManager.main.currentLogin.user.id, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, autoOpenLiveStream: true, playbackInfoDto: playbackInfo) @@ -537,7 +537,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe switch err { case .error(401, _, _, _): self.delegate?.exitPlayer(self) - SessionManager.current.logout() + // TODO: todo +// SessionManager.current.logout() main?.root(\.connectToServer) case .error: self.delegate?.exitPlayer(self) @@ -550,7 +551,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe let mediaSource = response.mediaSources!.first.self! if mediaSource.transcodingUrl != nil { // Item is being transcoded by request of server - let streamURL = URL(string: "\(ServerEnvironment.current.server.baseURI!)\(mediaSource.transcodingUrl!)") + let streamURL = URL(string: "\(SessionManager.main.currentLogin.server.uri)\(mediaSource.transcodingUrl!)") let item = PlaybackItem() item.videoType = .transcode item.videoUrl = streamURL! @@ -564,7 +565,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe if stream.type == .subtitle { var deliveryUrl: URL? if stream.deliveryMethod == .external { - deliveryUrl = URL(string: "\(ServerEnvironment.current.server.baseURI!)\(stream.deliveryUrl ?? "")")! + deliveryUrl = URL(string: "\(SessionManager.main.currentLogin.server.uri)\(stream.deliveryUrl ?? "")")! } else { deliveryUrl = nil } @@ -596,9 +597,10 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe self.sendPlayReport() playbackItem = item } else { + // TODO: todo // Item will be directly played by the client. - let streamURL = - URL(string: "\(ServerEnvironment.current.server.baseURI!)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&deviceId=\(SessionManager.current.deviceID)&api_key=\(SessionManager.current.accessToken)&Tag=\(mediaSource.eTag ?? "")")! + let streamURL = URL(string: "\(SessionManager.main.currentLogin.server.uri)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&Tag=\(mediaSource.eTag ?? "")")! +// URL(string: "\(SessionManager.main.currentLogin.server.uri)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&deviceId=\(SessionManager.current.deviceID)&api_key=\(SessionManager.current.accessToken)&Tag=\(mediaSource.eTag ?? "")")! let item = PlaybackItem() item.videoUrl = streamURL @@ -613,7 +615,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe if stream.type == .subtitle { var deliveryUrl: URL? if stream.deliveryMethod == .external { - deliveryUrl = URL(string: "\(ServerEnvironment.current.server.baseURI!)\(stream.deliveryUrl!)")! + deliveryUrl = URL(string: "\(SessionManager.main.currentLogin.server.uri)\(stream.deliveryUrl!)")! } else { deliveryUrl = nil } @@ -771,7 +773,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } func getNextEpisode() { - TvShowsAPI.getEpisodes(seriesId: manifest.seriesId!, userId: SessionManager.current.user.user_id!, startItemId: manifest.id, + TvShowsAPI.getEpisodes(seriesId: manifest.seriesId!, userId: SessionManager.main.currentLogin.user.id, startItemId: manifest.id, limit: 2) .sink(receiveCompletion: { completion in print(completion) @@ -873,11 +875,11 @@ extension PlayerViewController: GCKGenericChannelDelegate { let payload: [String: Any] = [ "options": options, "command": command, - "userId": SessionManager.current.user.user_id!, - "deviceId": SessionManager.current.deviceID, - "accessToken": SessionManager.current.accessToken, - "serverAddress": ServerEnvironment.current.server.baseURI!, - "serverId": ServerEnvironment.current.server.server_id!, + "userId": SessionManager.main.currentLogin.user.id, +// "deviceId": SessionManager.main.currentLogin.de.deviceID, + "accessToken": SessionManager.main.currentLogin.user.accessToken?.value ?? "", + "serverAddress": SessionManager.main.currentLogin.server.uri, + "serverId": SessionManager.main.currentLogin.server.id, "serverVersion": "10.8.0", "receiverName": castSessionManager.currentCastSession!.device.friendlyName!, "subtitleBurnIn": false, @@ -931,7 +933,7 @@ extension PlayerViewController: GCKSessionManagerListener { let playNowOptions: [String: Any] = [ "items": [[ "Id": manifest.id!, - "ServerId": ServerEnvironment.current.server.server_id!, + "ServerId": SessionManager.main.currentLogin.server.id, "Name": manifest.name!, "Type": manifest.type!, "MediaType": manifest.mediaType!, diff --git a/JellyfinPlayer/Views/ConnectToServerView.swift b/JellyfinPlayer/Views/ConnectToServerView.swift index 15f6fe04..fc923d2c 100644 --- a/JellyfinPlayer/Views/ConnectToServerView.swift +++ b/JellyfinPlayer/Views/ConnectToServerView.swift @@ -9,176 +9,243 @@ import SwiftUI import Stinsen struct ConnectToServerView: View { + @EnvironmentObject var mainRouter: MainCoordinator.Router @StateObject var viewModel = ConnectToServerViewModel() - @State var username = "" - @State var password = "" @State var uri = "" - + var body: some View { - ZStack { - Form { - if viewModel.isConnectedServer { - if viewModel.publicUsers.isEmpty { - Section(header: Text("Login to \(ServerEnvironment.current.server.name ?? "")")) { - TextField(NSLocalizedString("Username", comment: ""), text: $username) - .disableAutocorrection(true) - .autocapitalization(.none) - SecureField(NSLocalizedString("Password", comment: ""), text: $password) - .disableAutocorrection(true) - .autocapitalization(.none) - Button { - viewModel.login() - } label: { - HStack { - Text("Login") - Spacer() - if viewModel.isLoading { - ProgressView() - } - } - }.disabled(viewModel.isLoading || username.isEmpty) - } - - Section { - Button { - viewModel.isConnectedServer = false - } label: { - HStack { - HStack { - Image(systemName: "chevron.left") - Text("Change Server") - } - Spacer() - } - } - } - } else { - Section(header: Text("Login to \(ServerEnvironment.current.server.name ?? "")")) { - ForEach(viewModel.publicUsers, id: \.id) { publicUser in - HStack { - Button(action: { - if SessionManager.current.doesUserHaveSavedSession(userID: publicUser.id!) { - let user = SessionManager.current.getSavedSession(userID: publicUser.id!) - SessionManager.current.loginWithSavedSession(user: user) - mainRouter.root(\.mainTab) - } else { - username = publicUser.name ?? "" - viewModel.selectedPublicUser = publicUser - viewModel.hidePublicUsers() - if !(publicUser.hasPassword ?? true) { - password = "" - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { - viewModel.login() - } - } - } - }) { - HStack { - Text(publicUser.name ?? "").font(.subheadline).fontWeight(.semibold) - Spacer() - if publicUser.primaryImageTag != nil { - ImageView(src: URL(string: "\(ServerEnvironment.current.server.baseURI ?? "")/Users/\(publicUser.id ?? "")/Images/Primary?width=60&quality=80&tag=\(publicUser.primaryImageTag!)")!) - .frame(width: 60, height: 60) - .cornerRadius(30.0) - } else { - Image(systemName: "person.fill") - .foregroundColor(Color(red: 1, green: 1, blue: 1).opacity(0.8)) - .font(.system(size: 35)) - .frame(width: 60, height: 60) - .background(Color(red: 98 / 255, green: 121 / 255, blue: 205 / 255)) - .cornerRadius(30.0) - .shadow(radius: 6) - } - } - } - } - } - } - - Section { - Button { - viewModel.publicUsers.removeAll() - username = "" - } label: { - HStack { - Text("Other User").font(.subheadline).fontWeight(.semibold) - Spacer() - Image(systemName: "person.fill.questionmark") - .foregroundColor(Color(red: 1, green: 1, blue: 1).opacity(0.8)) - .font(.system(size: 35)) - .frame(width: 60, height: 60) - .background(Color(red: 98 / 255, green: 121 / 255, blue: 205 / 255)) - .cornerRadius(30.0) - .shadow(radius: 6) - } - } - } - } - } else { - Section(header: Text("Connect Manually")) { - TextField(NSLocalizedString("Server URL", comment: ""), text: $uri) - .disableAutocorrection(true) - .autocapitalization(.none) - .keyboardType(.URL) - Button { - viewModel.connectToServer() - } label: { - HStack { - Text("Connect") - Spacer() - if viewModel.isLoading { - ProgressView() - } - } - } - .disabled(viewModel.isLoading || uri.isEmpty) - } - - Section(header: Text("Discovered Servers")) { - if self.viewModel.searching { + List { + Section { + TextField(NSLocalizedString("Server URL", comment: ""), text: $uri) + .disableAutocorrection(true) + .autocapitalization(.none) + .keyboardType(.URL) + Button { + viewModel.connectToServer(uri: uri) + } label: { + HStack { + Text("Connect") + Spacer() + if viewModel.isLoading { ProgressView() } - ForEach(self.viewModel.servers, id: \.id) { server in - Button(action: { - viewModel.connectToServer(at: server.url) - }, label: { - HStack { - Text(server.name) - .font(.headline) - Text("• \(server.host)") - .font(.subheadline) - .foregroundColor(.secondary) - Spacer() - if viewModel.isLoading { - ProgressView() - } - } - - }) - } } - .onAppear(perform: self.viewModel.discoverServers) + } + .disabled(viewModel.isLoading || uri.isEmpty) + } + + Section(header: Text("Discovered Servers")) { + if viewModel.searching { + ProgressView() + } + ForEach(viewModel.discoveredServers.sorted(by: { $0.name < $1.name }), id: \.id) { discoveredServer in + Button(action: { + viewModel.connectToServer(uri: discoveredServer.url.absoluteString) + }, label: { + HStack { + Text(discoveredServer.name) + .font(.headline) + Text("• \(discoveredServer.host)") + .font(.subheadline) + .foregroundColor(.secondary) + Spacer() + if viewModel.isLoading { + ProgressView() + } + } + + }) } } - } - .onChange(of: uri) { uri in - viewModel.uriSubject.send(uri) - } - .onChange(of: username) { username in - viewModel.usernameSubject.send(username) - } - .onChange(of: password) { password in - viewModel.passwordSubject.send(password) + .onAppear(perform: self.viewModel.discoverServers) + .headerProminence(.increased) } .alert(item: $viewModel.errorMessage) { _ in Alert(title: Text("\(viewModel.errorMessage?.code ?? -1)\n\(viewModel.errorMessage?.title ?? "Error")"), message: Text(viewModel.errorMessage?.displayMessage ?? "Error"), dismissButton: .cancel()) } - .navigationTitle(NSLocalizedString("Connect to Server", comment: "")) - .onAppear { + .navigationTitle("Connect") + .onAppear { AppURLHandler.shared.appURLState = .allowedInLogin } } } + +//struct ConnectToServerView: View { +// @EnvironmentObject var mainRouter: MainCoordinator.Router +// @StateObject var viewModel = ConnectToServerViewModel() +// @State var username = "" +// @State var password = "" +// @State var uri = "" +// +// var body: some View { +// ZStack { +// Form { +// if viewModel.isConnectedServer { +// if viewModel.publicUsers.isEmpty { +// Section(header: Text("Login to \(SessionManager.main.currentLogin.server.name)")) { +// TextField(NSLocalizedString("Username", comment: ""), text: $username) +// .disableAutocorrection(true) +// .autocapitalization(.none) +// SecureField(NSLocalizedString("Password", comment: ""), text: $password) +// .disableAutocorrection(true) +// .autocapitalization(.none) +// Button { +// viewModel.login() +// } label: { +// HStack { +// Text("Login") +// Spacer() +// if viewModel.isLoading { +// ProgressView() +// } +// } +// }.disabled(viewModel.isLoading || username.isEmpty) +// } +// +// Section { +// Button { +// viewModel.isConnectedServer = false +// } label: { +// HStack { +// HStack { +// Image(systemName: "chevron.left") +// Text("Change Server") +// } +// Spacer() +// } +// } +// } +// } else { +// Section(header: Text("Login to \(SessionManager.main.currentLogin.server.name)")) { +// ForEach(viewModel.publicUsers, id: \.id) { publicUser in +// HStack { +// Button(action: { +// // TODO: todo +// print("TODO") +//// if SessionManager.current.doesUserHaveSavedSession(userID: publicUser.id!) { +//// let user = SessionManager.current.getSavedSession(userID: publicUser.id!) +//// SessionManager.current.loginWithSavedSession(user: user) +//// mainRouter.root(\.mainTab) +//// } else { +//// username = publicUser.name ?? "" +//// viewModel.selectedPublicUser = publicUser +//// viewModel.hidePublicUsers() +//// if !(publicUser.hasPassword ?? true) { +//// password = "" +//// DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { +//// viewModel.login() +//// } +//// } +//// } +// }) { +// HStack { +// Text(publicUser.name ?? "").font(.subheadline).fontWeight(.semibold) +// Spacer() +// if publicUser.primaryImageTag != nil { +// ImageView(src: URL(string: "\(SessionManager.main.currentLogin.server.uri)/Users/\(publicUser.id ?? "")/Images/Primary?width=60&quality=80&tag=\(publicUser.primaryImageTag!)")!) +// .frame(width: 60, height: 60) +// .cornerRadius(30.0) +// } else { +// Image(systemName: "person.fill") +// .foregroundColor(Color(red: 1, green: 1, blue: 1).opacity(0.8)) +// .font(.system(size: 35)) +// .frame(width: 60, height: 60) +// .background(Color(red: 98 / 255, green: 121 / 255, blue: 205 / 255)) +// .cornerRadius(30.0) +// .shadow(radius: 6) +// } +// } +// } +// } +// } +// } +// +// Section { +// Button { +// viewModel.publicUsers.removeAll() +// username = "" +// } label: { +// HStack { +// Text("Other User").font(.subheadline).fontWeight(.semibold) +// Spacer() +// Image(systemName: "person.fill.questionmark") +// .foregroundColor(Color(red: 1, green: 1, blue: 1).opacity(0.8)) +// .font(.system(size: 35)) +// .frame(width: 60, height: 60) +// .background(Color(red: 98 / 255, green: 121 / 255, blue: 205 / 255)) +// .cornerRadius(30.0) +// .shadow(radius: 6) +// } +// } +// } +// } +// } else { +// Section(header: Text("Connect Manually")) { +// TextField(NSLocalizedString("Server URL", comment: ""), text: $uri) +// .disableAutocorrection(true) +// .autocapitalization(.none) +// .keyboardType(.URL) +// Button { +// viewModel.connectToServer() +// } label: { +// HStack { +// Text("Connect") +// Spacer() +// if viewModel.isLoading { +// ProgressView() +// } +// } +// } +// .disabled(viewModel.isLoading || uri.isEmpty) +// } +// +// Section(header: Text("Discovered Servers")) { +// if self.viewModel.searching { +// ProgressView() +// } +// ForEach(self.viewModel.servers, id: \.id) { server in +// Button(action: { +// viewModel.connectToServer(at: server.url) +// }, label: { +// HStack { +// Text(server.name) +// .font(.headline) +// Text("• \(server.host)") +// .font(.subheadline) +// .foregroundColor(.secondary) +// Spacer() +// if viewModel.isLoading { +// ProgressView() +// } +// } +// +// }) +// } +// } +// .onAppear(perform: self.viewModel.discoverServers) +// } +// } +// } +// .onChange(of: uri) { uri in +// viewModel.uriSubject.send(uri) +// } +// .onChange(of: username) { username in +// viewModel.usernameSubject.send(username) +// } +// .onChange(of: password) { password in +// viewModel.passwordSubject.send(password) +// } +// .alert(item: $viewModel.errorMessage) { _ in +// Alert(title: Text("\(viewModel.errorMessage?.code ?? -1)\n\(viewModel.errorMessage?.title ?? "Error")"), +// message: Text(viewModel.errorMessage?.displayMessage ?? "Error"), +// dismissButton: .cancel()) +// } +// .navigationTitle(NSLocalizedString("Connect to Server", comment: "")) +// .onAppear { +// AppURLHandler.shared.appURLState = .allowedInLogin +// } +// } +//} diff --git a/JellyfinPlayer/Views/ServerDetailView.swift b/JellyfinPlayer/Views/ServerDetailView.swift index 8c4b9b56..98901a05 100644 --- a/JellyfinPlayer/Views/ServerDetailView.swift +++ b/JellyfinPlayer/Views/ServerDetailView.swift @@ -19,28 +19,28 @@ struct ServerDetailView: View { HStack { Text("Name") Spacer() - Text(ServerEnvironment.current.server.name ?? "") + Text(SessionManager.main.currentLogin.server.name) .foregroundColor(.secondary) } HStack { Text("URI") Spacer() - Text(ServerEnvironment.current.server.baseURI ?? "") + Text(SessionManager.main.currentLogin.server.uri) .foregroundColor(.secondary) } HStack { Text("Version") Spacer() - Text(ServerEnvironment.current.server.version ?? "") + Text(SessionManager.main.currentLogin.server.version) .foregroundColor(.secondary) } HStack { Text("Operating System") Spacer() - Text(ServerEnvironment.current.server.os ?? "") + Text(SessionManager.main.currentLogin.server.os) .foregroundColor(.secondary) } } diff --git a/JellyfinPlayer/Views/ServerListView.swift b/JellyfinPlayer/Views/ServerListView.swift new file mode 100644 index 00000000..9c208724 --- /dev/null +++ b/JellyfinPlayer/Views/ServerListView.swift @@ -0,0 +1,34 @@ +// + /* + * 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 + +struct ServerListView: View { + + @EnvironmentObject var serverListRouter: ServerListCoordinator.Router + @ObservedObject var viewModel: ServerListViewModel + + var body: some View { + List { + ForEach(viewModel.servers, id: \.id) { server in + Text(server.name) + } + } + .navigationTitle("Servers") + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button { + serverListRouter.route(to: \.connectToServer) + } label: { + Text("Connect") + } + } + } + } +} diff --git a/JellyfinPlayer/Views/SettingsView.swift b/JellyfinPlayer/Views/SettingsView.swift index c309b8c2..707b89e2 100644 --- a/JellyfinPlayer/Views/SettingsView.swift +++ b/JellyfinPlayer/Views/SettingsView.swift @@ -31,7 +31,7 @@ struct SettingsView: View { HStack { Text("User") Spacer() - Text(SessionManager.current.user?.username ?? "") + Text(SessionManager.main.currentLogin.user.username) .foregroundColor(.jellyfinPurple) } @@ -41,7 +41,7 @@ struct SettingsView: View { HStack { Text("Server") Spacer() - Text(ServerEnvironment.current.server?.name ?? "") + Text(SessionManager.main.currentLogin.server.name) .foregroundColor(.jellyfinPurple) Image(systemName: "chevron.right") @@ -51,7 +51,8 @@ struct SettingsView: View { Button { settingsRouter.dismissCoordinator() DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - SessionManager.current.logout() + // TODO: todo +// SessionManager.current.logout() let nc = NotificationCenter.default nc.post(name: Notification.Name("didSignOut"), object: nil) } diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift index 83ea48eb..78ad8991 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift @@ -74,7 +74,7 @@ public extension BaseItemDto { let x = UIScreen.main.nativeScale * CGFloat(maxWidth) let urlString = - "\(ServerEnvironment.current.server.baseURI!)/Items/\(imageItemId)/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)" + "\(SessionManager.main.currentLogin.server.uri)/Items/\(imageItemId)/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)" return URL(string: urlString)! } @@ -91,7 +91,7 @@ public extension BaseItemDto { let x = UIScreen.main.nativeScale * CGFloat(maxWidth) let urlString = - "\(ServerEnvironment.current.server.baseURI!)/Items/\(parentBackdropItemId ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)" + "\(SessionManager.main.currentLogin.server.uri)/Items/\(parentBackdropItemId ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)" return URL(string: urlString)! } @@ -100,7 +100,7 @@ public extension BaseItemDto { let imageTag = seriesPrimaryImageTag ?? "" let x = UIScreen.main.nativeScale * CGFloat(maxWidth) let urlString = - "\(ServerEnvironment.current.server.baseURI!)/Items/\(seriesId ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)" + "\(SessionManager.main.currentLogin.server.uri)/Items/\(seriesId ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)" return URL(string: urlString)! } @@ -117,7 +117,7 @@ public extension BaseItemDto { let x = UIScreen.main.nativeScale * CGFloat(maxWidth) let urlString = - "\(ServerEnvironment.current.server.baseURI!)/Items/\(imageItemId)/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)" + "\(SessionManager.main.currentLogin.server.uri)/Items/\(imageItemId)/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)" // print(urlString) return URL(string: urlString)! } diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemPersonExtensions.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemPersonExtensions.swift index a3a9c042..63b2d239 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/BaseItemPersonExtensions.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemPersonExtensions.swift @@ -57,7 +57,7 @@ extension BaseItemPerson { // MARK: PortraitImageStackable extension BaseItemPerson: PortraitImageStackable { public func imageURLContsructor(maxWidth: Int) -> URL { - return self.getImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: maxWidth) + return self.getImage(baseURL: SessionManager.main.currentLogin.server.uri, maxWidth: maxWidth) } public var title: String { diff --git a/Shared/ServerLocator/ServerDiscovery.swift b/Shared/ServerDiscovery/ServerDiscovery.swift similarity index 82% rename from Shared/ServerLocator/ServerDiscovery.swift rename to Shared/ServerDiscovery/ServerDiscovery.swift index 911239cd..7d718078 100644 --- a/Shared/ServerLocator/ServerDiscovery.swift +++ b/Shared/ServerDiscovery/ServerDiscovery.swift @@ -11,22 +11,6 @@ import Foundation public class ServerDiscovery { - public struct ServerCredential: Codable { - public let host: String - public let port: Int - public let username: String - public let password: String - public let deviceId: String - - public init(_ host: String, _ port: Int, _ username: String, _ password: String, _ deviceId: String = UUID().uuidString) { - self.host = host - self.port = port - self.username = username - self.password = password - self.deviceId = deviceId - } - } - public struct ServerLookupResponse: Codable, Hashable, Identifiable { public func hash(into hasher: inout Hasher) { @@ -62,6 +46,7 @@ public class ServerDiscovery { case name = "Name" } } + private let broadcastConn: UDPBroadcastConnection public init() { diff --git a/Shared/ServerLocator/UDPBroadCastConnection.swift b/Shared/ServerDiscovery/UDPBroadCastConnection.swift similarity index 100% rename from Shared/ServerLocator/UDPBroadCastConnection.swift rename to Shared/ServerDiscovery/UDPBroadCastConnection.swift diff --git a/Shared/Singleton/ServerEnvironment.swift b/Shared/Singleton/ServerEnvironment.swift deleted file mode 100644 index 326ede82..00000000 --- a/Shared/Singleton/ServerEnvironment.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -/* - * 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 Combine -import CoreData -import Foundation -import JellyfinAPI - -final class ServerEnvironment { - static let current = ServerEnvironment() - fileprivate(set) var server: Server! - - init() { - let serverRequest: NSFetchRequest = Server.fetchRequest() - let servers = try? PersistenceController.shared.container.viewContext.fetch(serverRequest) - - if servers?.count != 0 { - server = servers?.first - JellyfinAPI.basePath = server.baseURI! - } - } - - func create(with uri: String) -> AnyPublisher { - LogManager.shared.log.debug("Initializing new Server object with raw URI: \"\(uri)\"") - var uri = uri - if !uri.contains("http") { - uri = "https://" + uri - } - if uri.last == "/" { - uri = String(uri.dropLast()) - } - LogManager.shared.log.debug("Normalized URI: \"\(uri)\", attempting to getPublicSystemInfo()") - - JellyfinAPI.basePath = uri - return SystemAPI.getPublicSystemInfo() - .map { response in - let server = Server(context: PersistenceController.shared.container.viewContext) - server.baseURI = uri - server.name = response.serverName - server.server_id = response.id - server.version = response.version - server.os = response.operatingSystem - return server - } - .handleEvents(receiveOutput: { [unowned self] response in - server = response - _ = try? PersistenceController.shared.container.viewContext.save() - }).eraseToAnyPublisher() - } - - func reset() { - JellyfinAPI.basePath = "" - server = nil - - let serverRequest: NSFetchRequest = Server.fetchRequest() - let deleteRequest = NSBatchDeleteRequest(fetchRequest: serverRequest) - - // coredata will theoretically never throw - _ = try? PersistenceController.shared.container.viewContext.execute(deleteRequest) - } -} diff --git a/Shared/Singleton/SessionManager.swift b/Shared/Singleton/SessionManager.swift index 7cdb3b7c..13096968 100644 --- a/Shared/Singleton/SessionManager.swift +++ b/Shared/Singleton/SessionManager.swift @@ -21,8 +21,8 @@ import TVServices typealias CurrentLogin = (server: SwiftfinStore.Models.Server, user: SwiftfinStore.Models.User) -// MARK: New SessionManager -final class NewSessionManager { +// MARK: NewSessionManager +final class SessionManager { // MARK: currentLogin private(set) var currentLogin: CurrentLogin! @@ -32,13 +32,30 @@ final class NewSessionManager { private let JellyfinDefaults = UserDefaults(suiteName: "jellyfin-defaults")! - private init() { } + private init() { + if let lastServerUserID = SwiftfinStore.Defaults.suite[.lastServerUserID], + let userID = lastServerUserID.split(separator: "-")[safe: 1], + let user = try? SwiftfinStore.dataStack.fetchOne(From(), + [Where("id == %@", userID)]) { + // Strongly assuming that we didn't delete the server associate with the user + guard let server = user.server, let accessToken = user.accessToken else { return } + + setAuthHeader(with: accessToken.value) + currentLogin = (server: server, user: user) + } + } - func generateServerUserID(server: SwiftfinStore.Models.Server, user: SwiftfinStore.Models.User) -> String { + private func generateServerUserID(server: SwiftfinStore.Models.Server, user: SwiftfinStore.Models.User) -> String { return "\(server.id)-\(user.id)" } - func connectToServer(with uri: String) -> AnyPublisher { + func fetchServers() -> [SwiftfinStore.Models.Server] { + let servers = try! SwiftfinStore.dataStack.fetchAll(From()) + return servers + } + + // Connects to a server at the given uri, storing if successful + func connectToServer(with uri: String) -> AnyPublisher { var uri = uri if !uri.contains("http") { uri = "https://" + uri @@ -50,23 +67,58 @@ final class NewSessionManager { JellyfinAPI.basePath = uri return SystemAPI.getPublicSystemInfo() - .handleEvents(receiveOutput: { response in - print(response) - }).eraseToAnyPublisher() + .map({ response -> (SwiftfinStore.Models.Server, UnsafeDataTransaction) in + let transaction = SwiftfinStore.dataStack.beginUnsafe() + let newServer = transaction.create(Into()) + newServer.uri = response.localAddress ?? "SfUri" + newServer.name = response.serverName ?? "SfServerName" + newServer.id = response.id ?? "" + newServer.os = response.operatingSystem ?? "SfOS" + newServer.version = response.version ?? "SfVersion" + newServer.users = [] + + return (newServer, transaction) + }) + .handleEvents(receiveOutput: { (_, transaction) in + try? transaction.commitAndWait() + }) + .map({ (server, _) in + return server + }) + .eraseToAnyPublisher() } - func fetchServers() -> [SwiftfinStore.Models.Server] { - let servers = try! SwiftfinStore.dataStack.fetchAll(From()) - return servers - } - - func loginUser(server: SwiftfinStore.Models.Server, username: String, password: String) -> AnyPublisher { + // Logs in a user with an associated server, storing if successful + func loginUser(server: SwiftfinStore.Models.Server, username: String, password: String) -> AnyPublisher { setAuthHeader(with: "") return UserAPI.authenticateUserByName(authenticateUserByName: AuthenticateUserByName(username: username, pw: password)) - .handleEvents(receiveOutput: { [unowned self] response in - guard let accessToken = response.accessToken else { fatalError() } - setAuthHeader(with: accessToken) + .map({ response -> (SwiftfinStore.Models.User, UnsafeDataTransaction) in + + guard let accessToken = response.accessToken else { fatalError("Received successful user with no access token") } + + let transaction = SwiftfinStore.dataStack.beginUnsafe() + let newUser = transaction.create(Into()) + newUser.username = response.user?.name ?? "SfUsername" + newUser.id = response.user?.id ?? "SfID" + newUser.appleTVID = "" + + let newAccessToken = transaction.create(Into()) + newAccessToken.value = accessToken + newUser.accessToken = newAccessToken + + let userServer = transaction.edit(server) + userServer?.users.insert(newUser) + + return (newUser, transaction) + }) + .handleEvents(receiveOutput: { [unowned self] (user, transaction) in + setAuthHeader(with: user.accessToken?.value ?? "") + try? transaction.commitAndWait() + currentLogin = (server: server, user: user) + }) + .map({ (user, _) in + return user }) .eraseToAnyPublisher() } @@ -76,8 +128,6 @@ final class NewSessionManager { var deviceName = UIDevice.current.name deviceName = deviceName.folding(options: .diacriticInsensitive, locale: .current) deviceName = String(deviceName.unicodeScalars.filter {CharacterSet.urlQueryAllowed.contains($0) }) - - var header = "MediaBrowser " let platform: String #if os(tvOS) @@ -86,6 +136,7 @@ final class NewSessionManager { platform = "iOS" #endif + var header = "MediaBrowser " header.append("Client=\"Jellyfin \(platform)\", ") header.append("Device=\"\(deviceName)\", ") header.append("DeviceId=\"\(platform)_\(UIDevice.vendorUUIDString)_\(String(Date().timeIntervalSince1970))\", ") @@ -96,170 +147,170 @@ final class NewSessionManager { } } -final class SessionManager { - static let current = SessionManager() - fileprivate(set) var user: SignedInUser! - fileprivate(set) var deviceID: String = "" - fileprivate(set) var accessToken: String = "" - - #if os(tvOS) - let tvUserManager = TVUserManager() - #endif - let userDefaults = UserDefaults() - - init() { - let savedUserRequest: NSFetchRequest = SignedInUser.fetchRequest() - let lastUsedUserID = userDefaults.string(forKey: "lastUsedUserID") - let savedUsers = try? PersistenceController.shared.container.viewContext.fetch(savedUserRequest) - - #if os(tvOS) - savedUsers?.forEach { savedUser in - if savedUser.appletv_id == tvUserManager.currentUserIdentifier ?? "" { - self.user = savedUser - } - } - #else - if lastUsedUserID != nil { - savedUsers?.forEach { savedUser in - if savedUser.user_id ?? "" == lastUsedUserID! { - user = savedUser - } - } - } else { - user = savedUsers?.first - } - #endif - - if user != nil { - let authToken = getAuthToken(userID: user.user_id!) - generateAuthHeader(with: authToken, deviceID: user.device_uuid) - } - } - - fileprivate func generateAuthHeader(with authToken: String?, deviceID devID: String?) { - let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String - var deviceName = UIDevice.current.name - deviceName = deviceName.folding(options: .diacriticInsensitive, locale: .current) - deviceName = String(deviceName.unicodeScalars.filter {CharacterSet.urlQueryAllowed.contains($0) }) - - var header = "MediaBrowser " - #if os(tvOS) - header.append("Client=\"Jellyfin tvOS\", ") - #else - header.append("Client=\"SwiftFin iOS\", ") - #endif - - header.append("Device=\"\(deviceName)\", ") - - if devID == nil { - LogManager.shared.log.info("Generating device ID...") - #if os(tvOS) - header.append("DeviceId=\"tvOS_\(UIDevice.current.identifierForVendor!.uuidString)_\(String(Date().timeIntervalSince1970))\", ") - deviceID = "tvOS_\(UIDevice.current.identifierForVendor!.uuidString)_\(String(Date().timeIntervalSince1970))" - #else - header.append("DeviceId=\"iOS_\(UIDevice.current.identifierForVendor!.uuidString)_\(String(Date().timeIntervalSince1970))\", ") - deviceID = "iOS_\(UIDevice.current.identifierForVendor!.uuidString)_\(String(Date().timeIntervalSince1970))" - #endif - } else { - LogManager.shared.log.info("Using stored device ID...") - header.append("DeviceId=\"\(devID!)\", ") - deviceID = devID! - } - - header.append("Version=\"\(appVersion ?? "0.0.1")\", ") - - if authToken != nil { - header.append("Token=\"\(authToken!)\"") - accessToken = authToken! - } - - JellyfinAPI.customHeaders["X-Emby-Authorization"] = header - } - - fileprivate func getAuthToken(userID: String) -> String? { - let keychain = KeychainSwift() - keychain.accessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain" - return keychain.get("AccessToken_\(userID)") - } - - func doesUserHaveSavedSession(userID: String) -> Bool { - let savedUserRequest: NSFetchRequest = SignedInUser.fetchRequest() - savedUserRequest.predicate = NSPredicate(format: "user_id == %@", userID) - let savedUsers = try? PersistenceController.shared.container.viewContext.fetch(savedUserRequest) - - if savedUsers!.isEmpty { - return false - } - - return true - } - - func getSavedSession(userID: String) -> SignedInUser { - let savedUserRequest: NSFetchRequest = SignedInUser.fetchRequest() - savedUserRequest.predicate = NSPredicate(format: "user_id == %@", userID) - let savedUsers = try? PersistenceController.shared.container.viewContext.fetch(savedUserRequest) - return savedUsers!.first! - } - - func loginWithSavedSession(user: SignedInUser) { - let accessToken = getAuthToken(userID: user.user_id!) - userDefaults.set(user.user_id!, forKey: "lastUsedUserID") - self.user = user - generateAuthHeader(with: accessToken, deviceID: user.device_uuid) - 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, deviceID: nil) - - return UserAPI.authenticateUserByName(authenticateUserByName: AuthenticateUserByName(username: username, pw: password)) - .map { response -> (SignedInUser, String?) in - let user = SignedInUser(context: PersistenceController.shared.container.viewContext) - user.username = response.user?.name - user.user_id = response.user?.id - user.device_uuid = self.deviceID - - #if os(tvOS) - let descriptor: TVAppProfileDescriptor = TVAppProfileDescriptor(name: user.username!) - self.tvUserManager.shouldStorePreferenceForCurrentUser(to: descriptor) { should in - if should { - user.appletv_id = self.tvUserManager.currentUserIdentifier ?? "" - } - } - #endif - - return (user, response.accessToken) - } - .handleEvents(receiveOutput: { [unowned self] response, accessToken in - user = response - _ = try? PersistenceController.shared.container.viewContext.save() - - let keychain = KeychainSwift() - keychain.accessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain" - keychain.set(accessToken!, forKey: "AccessToken_\(user.user_id!)") - - generateAuthHeader(with: accessToken, deviceID: user.device_uuid) - - let nc = NotificationCenter.default - nc.post(name: Notification.Name("didSignIn"), object: nil) - }) - .map(\.0) - .eraseToAnyPublisher() - } - - func logout() { - let nc = NotificationCenter.default - nc.post(name: Notification.Name("didSignOut"), object: nil) - let keychain = KeychainSwift() - keychain.accessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain" - keychain.delete("AccessToken_\(user?.user_id ?? "")") - generateAuthHeader(with: nil, deviceID: nil) - if user != nil { - let deleteRequest = NSBatchDeleteRequest(objectIDs: [user.objectID]) - user = nil - _ = try? PersistenceController.shared.container.viewContext.execute(deleteRequest) - } - } -} +//final class SessionManager { +// static let current = SessionManager() +// fileprivate(set) var user: SignedInUser! +// fileprivate(set) var deviceID: String = "" +// fileprivate(set) var accessToken: String = "" +// +// #if os(tvOS) +// let tvUserManager = TVUserManager() +// #endif +// let userDefaults = UserDefaults() +// +// init() { +// let savedUserRequest: NSFetchRequest = SignedInUser.fetchRequest() +// let lastUsedUserID = userDefaults.string(forKey: "lastUsedUserID") +// let savedUsers = try? PersistenceController.shared.container.viewContext.fetch(savedUserRequest) +// +// #if os(tvOS) +// savedUsers?.forEach { savedUser in +// if savedUser.appletv_id == tvUserManager.currentUserIdentifier ?? "" { +// self.user = savedUser +// } +// } +// #else +// if lastUsedUserID != nil { +// savedUsers?.forEach { savedUser in +// if savedUser.user_id ?? "" == lastUsedUserID! { +// user = savedUser +// } +// } +// } else { +// user = savedUsers?.first +// } +// #endif +// +// if user != nil { +// let authToken = getAuthToken(userID: user.user_id!) +// generateAuthHeader(with: authToken, deviceID: user.device_uuid) +// } +// } +// +// fileprivate func generateAuthHeader(with authToken: String?, deviceID devID: String?) { +// let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String +// var deviceName = UIDevice.current.name +// deviceName = deviceName.folding(options: .diacriticInsensitive, locale: .current) +// deviceName = String(deviceName.unicodeScalars.filter {CharacterSet.urlQueryAllowed.contains($0) }) +// +// var header = "MediaBrowser " +// #if os(tvOS) +// header.append("Client=\"Jellyfin tvOS\", ") +// #else +// header.append("Client=\"SwiftFin iOS\", ") +// #endif +// +// header.append("Device=\"\(deviceName)\", ") +// +// if devID == nil { +// LogManager.shared.log.info("Generating device ID...") +// #if os(tvOS) +// header.append("DeviceId=\"tvOS_\(UIDevice.current.identifierForVendor!.uuidString)_\(String(Date().timeIntervalSince1970))\", ") +// deviceID = "tvOS_\(UIDevice.current.identifierForVendor!.uuidString)_\(String(Date().timeIntervalSince1970))" +// #else +// header.append("DeviceId=\"iOS_\(UIDevice.current.identifierForVendor!.uuidString)_\(String(Date().timeIntervalSince1970))\", ") +// deviceID = "iOS_\(UIDevice.current.identifierForVendor!.uuidString)_\(String(Date().timeIntervalSince1970))" +// #endif +// } else { +// LogManager.shared.log.info("Using stored device ID...") +// header.append("DeviceId=\"\(devID!)\", ") +// deviceID = devID! +// } +// +// header.append("Version=\"\(appVersion ?? "0.0.1")\", ") +// +// if authToken != nil { +// header.append("Token=\"\(authToken!)\"") +// accessToken = authToken! +// } +// +// JellyfinAPI.customHeaders["X-Emby-Authorization"] = header +// } +// +// fileprivate func getAuthToken(userID: String) -> String? { +// let keychain = KeychainSwift() +// keychain.accessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain" +// return keychain.get("AccessToken_\(userID)") +// } +// +// func doesUserHaveSavedSession(userID: String) -> Bool { +// let savedUserRequest: NSFetchRequest = SignedInUser.fetchRequest() +// savedUserRequest.predicate = NSPredicate(format: "user_id == %@", userID) +// let savedUsers = try? PersistenceController.shared.container.viewContext.fetch(savedUserRequest) +// +// if savedUsers!.isEmpty { +// return false +// } +// +// return true +// } +// +// func getSavedSession(userID: String) -> SignedInUser { +// let savedUserRequest: NSFetchRequest = SignedInUser.fetchRequest() +// savedUserRequest.predicate = NSPredicate(format: "user_id == %@", userID) +// let savedUsers = try? PersistenceController.shared.container.viewContext.fetch(savedUserRequest) +// return savedUsers!.first! +// } +// +// func loginWithSavedSession(user: SignedInUser) { +// let accessToken = getAuthToken(userID: user.user_id!) +// userDefaults.set(user.user_id!, forKey: "lastUsedUserID") +// self.user = user +// generateAuthHeader(with: accessToken, deviceID: user.device_uuid) +// 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, deviceID: nil) +// +// return UserAPI.authenticateUserByName(authenticateUserByName: AuthenticateUserByName(username: username, pw: password)) +// .map { response -> (SignedInUser, String?) in +// let user = SignedInUser(context: PersistenceController.shared.container.viewContext) +// user.username = response.user?.name +// user.user_id = response.user?.id +// user.device_uuid = self.deviceID +// +// #if os(tvOS) +// let descriptor: TVAppProfileDescriptor = TVAppProfileDescriptor(name: user.username!) +// self.tvUserManager.shouldStorePreferenceForCurrentUser(to: descriptor) { should in +// if should { +// user.appletv_id = self.tvUserManager.currentUserIdentifier ?? "" +// } +// } +// #endif +// +// return (user, response.accessToken) +// } +// .handleEvents(receiveOutput: { [unowned self] response, accessToken in +// user = response +// _ = try? PersistenceController.shared.container.viewContext.save() +// +// let keychain = KeychainSwift() +// keychain.accessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain" +// keychain.set(accessToken!, forKey: "AccessToken_\(user.user_id!)") +// +// generateAuthHeader(with: accessToken, deviceID: user.device_uuid) +// +// let nc = NotificationCenter.default +// nc.post(name: Notification.Name("didSignIn"), object: nil) +// }) +// .map(\.0) +// .eraseToAnyPublisher() +// } +// +// func logout() { +// let nc = NotificationCenter.default +// nc.post(name: Notification.Name("didSignOut"), object: nil) +// let keychain = KeychainSwift() +// keychain.accessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain" +// keychain.delete("AccessToken_\(user?.user_id ?? "")") +// generateAuthHeader(with: nil, deviceID: nil) +// if user != nil { +// let deleteRequest = NSBatchDeleteRequest(objectIDs: [user.objectID]) +// user = nil +// _ = try? PersistenceController.shared.container.viewContext.execute(deleteRequest) +// } +// } +//} diff --git a/Shared/SwiftfinStore/SwiftfinStore.swift b/Shared/SwiftfinStore/SwiftfinStore.swift index 85f285ac..c30ca41b 100644 --- a/Shared/SwiftfinStore/SwiftfinStore.swift +++ b/Shared/SwiftfinStore/SwiftfinStore.swift @@ -9,6 +9,7 @@ import Foundation import CoreStore +import Defaults enum SwiftfinStore { @@ -17,7 +18,7 @@ enum SwiftfinStore { final class Server: CoreStoreObject { @Field.Stored("uri") - var url: String = "" + var uri: String = "" @Field.Stored("name") var name: String = "" @@ -38,7 +39,7 @@ enum SwiftfinStore { final class User: CoreStoreObject { @Field.Stored("username") - var name: String = "" + var username: String = "" @Field.Stored("id") var id: String = "" @@ -48,6 +49,18 @@ enum SwiftfinStore { @Field.Relationship("server") var server: Server? + + @Field.Relationship("accessToken", inverse: \AccessToken.$user) + var accessToken: AccessToken? + } + + final class AccessToken: CoreStoreObject { + + @Field.Stored("value") + var value: String = "" + + @Field.Relationship("user") + var user: User? } } @@ -56,8 +69,9 @@ enum SwiftfinStore { entities: [ Entity("Server"), Entity("User"), + Entity("AccessToken") ], - versionLock: nil) + versionLock: nil) // TODO: todo let _dataStack = DataStack(schema) try! _dataStack.addStorageAndWait( diff --git a/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift b/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift new file mode 100644 index 00000000..7d6d7d30 --- /dev/null +++ b/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift @@ -0,0 +1,29 @@ +// + /* + * 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 Defaults +import Foundation + +extension SwiftfinStore { + + enum Defaults { + + static let suite: UserDefaults = { + return UserDefaults(suiteName: "swiftfinstore-defaults")! + }() + +// enum Keys { +// static let lastUserID = Defaults.Key("lastUserID", suite: SwiftfinStore.Defaults.suite) +// } + } +} + +extension Defaults.Keys { + static let lastServerUserID = Defaults.Key("lastServerUserID", suite: SwiftfinStore.Defaults.suite) +} diff --git a/Shared/SwiftfinStore/SwiftfinStoreKeychain.swift b/Shared/SwiftfinStore/SwiftfinStoreKeychain.swift new file mode 100644 index 00000000..31368ccf --- /dev/null +++ b/Shared/SwiftfinStore/SwiftfinStoreKeychain.swift @@ -0,0 +1,33 @@ +// + /* + * 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 KeychainSwift + +extension SwiftfinStore { + + enum Keychain { + + private static let keychainAccessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain" + + private static let keychain: KeychainSwift = { + let keychain = KeychainSwift() + keychain.accessGroup = keychainAccessGroup + return keychain + }() + + static func getAuthToken(serverUserID: String) -> String? { + return keychain.get("AccessToken_\(serverUserID)") + } + + static func delete(serverUserID: String) { + // TODO: todo + } + } +} diff --git a/Shared/ViewModels/ConnectToServerViewModel.swift b/Shared/ViewModels/ConnectToServerViewModel.swift index 04929d81..d0479cbe 100644 --- a/Shared/ViewModels/ConnectToServerViewModel.swift +++ b/Shared/ViewModels/ConnectToServerViewModel.swift @@ -13,77 +13,32 @@ import JellyfinAPI import Stinsen final class ConnectToServerViewModel: ViewModel { - @RouterObject - var main: MainCoordinator.Router? - - @Published var isConnectedServer = false - - var uriSubject = CurrentValueSubject("") - var usernameSubject = CurrentValueSubject("") - var passwordSubject = CurrentValueSubject("") - - @Published var lastPublicUsers = [UserDto]() - @Published var publicUsers = [UserDto]() - @Published var selectedPublicUser = UserDto() - - private let discovery = ServerDiscovery() - @Published var servers: [ServerDiscovery.ServerLookupResponse] = [] + + @RouterObject var main: MainCoordinator.Router? + @Published var discoveredServers: Set = [] @Published var searching = false + private let discovery = ServerDiscovery() - func getPublicUsers() { - if ServerEnvironment.current.server != nil { - LogManager.shared.log.debug("Attempting to read public users from \(ServerEnvironment.current.server.baseURI!)", - tag: "getPublicUsers") - UserAPI.getPublicUsers() - .trackActivity(loading) - .sink(receiveCompletion: { completion in - self.handleAPIRequestError(completion: completion) - }, receiveValue: { response in - self.publicUsers = response - LogManager.shared.log.debug("Received \(String(response.count)) public users.", tag: "getPublicUsers") - self.isConnectedServer = true - }) - .store(in: &cancellables) - } else { - LogManager.shared.log.debug("Not getting users - server is nil", tag: "getPublicUsers") - } - } - - func hidePublicUsers() { - lastPublicUsers = publicUsers - publicUsers = [] - } - - func showPublicUsers() { - publicUsers = lastPublicUsers - lastPublicUsers = [] - } - - func connectToServer() { + func connectToServer(uri: String) { #if targetEnvironment(simulator) - if uriSubject.value == "localhost" { - uriSubject.value = "http://localhost:8096" - } + var uri = uri + if uri == "localhost" { + uri = "http://localhost:8096" + } #endif - LogManager.shared.log.debug("Attempting to connect to server at \"\(uriSubject.value)\"", tag: "connectToServer") - ServerEnvironment.current.create(with: uriSubject.value) + LogManager.shared.log.debug("Attempting to connect to server at \"\(uri)\"", tag: "connectToServer") + SessionManager.main.connectToServer(with: uri) .trackActivity(loading) .sink(receiveCompletion: { completion in self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "connectToServer", completion: completion) }, receiveValue: { _ in - LogManager.shared.log.debug("Connected to server at \"\(self.uriSubject.value)\"", tag: "connectToServer") - self.getPublicUsers() + LogManager.shared.log.debug("Connected to server at \"\(uri)\"", tag: "connectToServer") }) .store(in: &cancellables) } - func connectToServer(at url: URL) { - uriSubject.send(url.absoluteString) - connectToServer() - } - func discoverServers() { searching = true @@ -93,26 +48,10 @@ final class ConnectToServerViewModel: ViewModel { } discovery.locateServer { [self] server in - if let server = server, !servers.contains(server) { - servers.append(server) + if let server = server { + discoveredServers.insert(server) } searching = false } } - - func login() { - LogManager.shared.log.debug("Attempting to login to server at \"\(uriSubject.value)\"", tag: "login") - LogManager.shared.log - .debug("username == \"\": \(usernameSubject.value.isEmpty), password == \"\": \(passwordSubject.value.isEmpty)", - tag: "login") - SessionManager.current.login(username: usernameSubject.value, password: passwordSubject.value) - .trackActivity(loading) - .sink(receiveCompletion: { completion in - self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "login", - completion: completion) - }, receiveValue: { [weak self] _ in - self?.main?.root(\.mainTab) - }) - .store(in: &cancellables) - } } diff --git a/Shared/ViewModels/EpisodeItemViewModel.swift b/Shared/ViewModels/EpisodeItemViewModel.swift index eaed1330..89a88ca2 100644 --- a/Shared/ViewModels/EpisodeItemViewModel.swift +++ b/Shared/ViewModels/EpisodeItemViewModel.swift @@ -26,7 +26,7 @@ final class EpisodeItemViewModel: ItemViewModel { func routeToSeasonItem() { guard let id = item.seasonId else { return } - UserLibraryAPI.getItem(userId: SessionManager.current.user.user_id!, itemId: id) + UserLibraryAPI.getItem(userId: SessionManager.main.currentLogin.user.id, itemId: id) .trackActivity(loading) .sink(receiveCompletion: { [weak self] completion in self?.handleAPIRequestError(completion: completion) @@ -38,7 +38,7 @@ final class EpisodeItemViewModel: ItemViewModel { func routeToSeriesItem() { guard let id = item.seriesId else { return } - UserLibraryAPI.getItem(userId: SessionManager.current.user.user_id!, itemId: id) + UserLibraryAPI.getItem(userId: SessionManager.main.currentLogin.user.id, itemId: id) .trackActivity(loading) .sink(receiveCompletion: { [weak self] completion in self?.handleAPIRequestError(completion: completion) diff --git a/Shared/ViewModels/HomeViewModel.swift b/Shared/ViewModels/HomeViewModel.swift index 0f98e7a4..88eb4188 100644 --- a/Shared/ViewModels/HomeViewModel.swift +++ b/Shared/ViewModels/HomeViewModel.swift @@ -29,7 +29,7 @@ final class HomeViewModel: ViewModel { func refresh() { LogManager.shared.log.debug("Refresh called.") - UserViewsAPI.getUserViews(userId: SessionManager.current.user.user_id!) + UserViewsAPI.getUserViews(userId: SessionManager.main.currentLogin.user.id) .trackActivity(loading) .sink(receiveCompletion: { completion in self.handleAPIRequestError(completion: completion) @@ -57,7 +57,7 @@ final class HomeViewModel: ViewModel { }) .store(in: &cancellables) - ItemsAPI.getResumeItems(userId: SessionManager.current.user.user_id!, limit: 12, + ItemsAPI.getResumeItems(userId: SessionManager.main.currentLogin.user.id, limit: 12, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], mediaTypes: ["Video"], imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb]) .trackActivity(loading) @@ -69,7 +69,7 @@ final class HomeViewModel: ViewModel { }) .store(in: &cancellables) - TvShowsAPI.getNextUp(userId: SessionManager.current.user.user_id!, limit: 12, + TvShowsAPI.getNextUp(userId: SessionManager.main.currentLogin.user.id, limit: 12, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people]) .trackActivity(loading) .sink(receiveCompletion: { completion in diff --git a/Shared/ViewModels/ItemViewModel.swift b/Shared/ViewModels/ItemViewModel.swift index f55b3171..7b2dc5c3 100644 --- a/Shared/ViewModels/ItemViewModel.swift +++ b/Shared/ViewModels/ItemViewModel.swift @@ -47,7 +47,7 @@ class ItemViewModel: ViewModel { } func getSimilarItems() { - LibraryAPI.getSimilarItems(itemId: item.id!, userId: SessionManager.current.user.user_id!, limit: 20, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people]) + LibraryAPI.getSimilarItems(itemId: item.id!, userId: SessionManager.main.currentLogin.user.id, limit: 20, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people]) .trackActivity(loading) .sink(receiveCompletion: { [weak self] completion in self?.handleAPIRequestError(completion: completion) @@ -59,7 +59,7 @@ class ItemViewModel: ViewModel { func updateWatchState() { if isWatched { - PlaystateAPI.markUnplayedItem(userId: SessionManager.current.user.user_id!, itemId: item.id!) + PlaystateAPI.markUnplayedItem(userId: SessionManager.main.currentLogin.user.id, itemId: item.id!) .trackActivity(loading) .sink(receiveCompletion: { [weak self] completion in self?.handleAPIRequestError(completion: completion) @@ -68,7 +68,7 @@ class ItemViewModel: ViewModel { }) .store(in: &cancellables) } else { - PlaystateAPI.markPlayedItem(userId: SessionManager.current.user.user_id!, itemId: item.id!) + PlaystateAPI.markPlayedItem(userId: SessionManager.main.currentLogin.user.id, itemId: item.id!) .trackActivity(loading) .sink(receiveCompletion: { [weak self] completion in self?.handleAPIRequestError(completion: completion) @@ -81,7 +81,7 @@ class ItemViewModel: ViewModel { func updateFavoriteState() { if isFavorited { - UserLibraryAPI.unmarkFavoriteItem(userId: SessionManager.current.user.user_id!, itemId: item.id!) + UserLibraryAPI.unmarkFavoriteItem(userId: SessionManager.main.currentLogin.user.id, itemId: item.id!) .trackActivity(loading) .sink(receiveCompletion: { [weak self] completion in self?.handleAPIRequestError(completion: completion) @@ -90,7 +90,7 @@ class ItemViewModel: ViewModel { }) .store(in: &cancellables) } else { - UserLibraryAPI.markFavoriteItem(userId: SessionManager.current.user.user_id!, itemId: item.id!) + UserLibraryAPI.markFavoriteItem(userId: SessionManager.main.currentLogin.user.id, itemId: item.id!) .trackActivity(loading) .sink(receiveCompletion: { [weak self] completion in self?.handleAPIRequestError(completion: completion) diff --git a/Shared/ViewModels/LatestMediaViewModel.swift b/Shared/ViewModels/LatestMediaViewModel.swift index d0ebbba8..c70f156e 100644 --- a/Shared/ViewModels/LatestMediaViewModel.swift +++ b/Shared/ViewModels/LatestMediaViewModel.swift @@ -25,8 +25,8 @@ final class LatestMediaViewModel: ViewModel { } func requestLatestMedia() { - LogManager.shared.log.debug("Requesting latest media for user id \(SessionManager.current.user.user_id ?? "NIL")") - UserLibraryAPI.getLatestMedia(userId: SessionManager.current.user.user_id!, + LogManager.shared.log.debug("Requesting latest media for user id \(SessionManager.main.currentLogin.user.id ?? "NIL")") + UserLibraryAPI.getLatestMedia(userId: SessionManager.main.currentLogin.user.id, parentId: libraryID, fields: [ .primaryImageAspectRatio, diff --git a/Shared/ViewModels/LibraryFilterViewModel.swift b/Shared/ViewModels/LibraryFilterViewModel.swift index 30cc442e..127e4d99 100644 --- a/Shared/ViewModels/LibraryFilterViewModel.swift +++ b/Shared/ViewModels/LibraryFilterViewModel.swift @@ -58,7 +58,7 @@ final class LibraryFilterViewModel: ViewModel { } func requestQueryFilters() { - FilterAPI.getQueryFilters(userId: SessionManager.current.user.user_id!, parentId: self.parentId) + FilterAPI.getQueryFilters(userId: SessionManager.main.currentLogin.user.id, parentId: self.parentId) .trackActivity(loading) .sink(receiveCompletion: { [weak self] completion in self?.handleAPIRequestError(completion: completion) diff --git a/Shared/ViewModels/LibraryListViewModel.swift b/Shared/ViewModels/LibraryListViewModel.swift index 1c85db9e..f352f686 100644 --- a/Shared/ViewModels/LibraryListViewModel.swift +++ b/Shared/ViewModels/LibraryListViewModel.swift @@ -24,7 +24,7 @@ final class LibraryListViewModel: ViewModel { } func requestLibraries() { - UserViewsAPI.getUserViews(userId: SessionManager.current.user.user_id ?? "val was nil") + UserViewsAPI.getUserViews(userId: SessionManager.main.currentLogin.user.id ?? "val was nil") .trackActivity(loading) .sink(receiveCompletion: { completion in self.handleAPIRequestError(completion: completion) diff --git a/Shared/ViewModels/LibrarySearchViewModel.swift b/Shared/ViewModels/LibrarySearchViewModel.swift index 9d0e035b..0aeb6098 100644 --- a/Shared/ViewModels/LibrarySearchViewModel.swift +++ b/Shared/ViewModels/LibrarySearchViewModel.swift @@ -77,7 +77,7 @@ final class LibrarySearchViewModel: ViewModel { } func requestSuggestions() { - ItemsAPI.getItemsByUserId(userId: SessionManager.current.user.user_id!, + ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, limit: 20, recursive: true, parentId: parentID, @@ -96,7 +96,7 @@ final class LibrarySearchViewModel: ViewModel { } func search(with query: String) { - ItemsAPI.getItemsByUserId(userId: SessionManager.current.user.user_id!, limit: 50, recursive: true, searchTerm: query, + ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, limit: 50, recursive: true, searchTerm: query, sortOrder: [.ascending], parentId: parentID, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], includeItemTypes: [ItemType.movie.rawValue], sortBy: ["SortName"], enableUserData: true, enableImages: true) @@ -107,7 +107,7 @@ final class LibrarySearchViewModel: ViewModel { self?.movieItems = response.items ?? [] }) .store(in: &cancellables) - ItemsAPI.getItemsByUserId(userId: SessionManager.current.user.user_id!, limit: 50, recursive: true, searchTerm: query, + ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, limit: 50, recursive: true, searchTerm: query, sortOrder: [.ascending], parentId: parentID, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], includeItemTypes: [ItemType.series.rawValue], sortBy: ["SortName"], enableUserData: true, enableImages: true) @@ -118,7 +118,7 @@ final class LibrarySearchViewModel: ViewModel { self?.showItems = response.items ?? [] }) .store(in: &cancellables) - ItemsAPI.getItemsByUserId(userId: SessionManager.current.user.user_id!, limit: 50, recursive: true, searchTerm: query, + ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, limit: 50, recursive: true, searchTerm: query, sortOrder: [.ascending], parentId: parentID, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], includeItemTypes: [ItemType.episode.rawValue], sortBy: ["SortName"], enableUserData: true, enableImages: true) diff --git a/Shared/ViewModels/LibraryViewModel.swift b/Shared/ViewModels/LibraryViewModel.swift index f73d2803..4999d044 100644 --- a/Shared/ViewModels/LibraryViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel.swift @@ -63,7 +63,7 @@ final class LibraryViewModel: ViewModel { } let sortBy = filters.sortBy.map(\.rawValue) let shouldBeRecursive: Bool = filters.filters.contains(.isFavorite) || personIDs != [] || studioIDs != [] || genreIDs != [] - ItemsAPI.getItemsByUserId(userId: SessionManager.current.user.user_id!, startIndex: currentPage * 100, limit: 100, recursive: shouldBeRecursive, + ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, startIndex: currentPage * 100, limit: 100, recursive: shouldBeRecursive, searchTerm: nil, sortOrder: filters.sortOrder, parentId: parentID, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], includeItemTypes: filters.filters.contains(.isFavorite) ? ["Movie", "Series", "Season", "Episode"] : ["Movie", "Series"], filters: filters.filters, sortBy: sortBy, tags: filters.tags, @@ -94,7 +94,7 @@ final class LibraryViewModel: ViewModel { } let sortBy = filters.sortBy.map(\.rawValue) let shouldBeRecursive: Bool = filters.filters.contains(.isFavorite) || personIDs != [] || studioIDs != [] || genreIDs != [] - ItemsAPI.getItemsByUserId(userId: SessionManager.current.user.user_id!, startIndex: currentPage * 100, limit: 100, recursive: shouldBeRecursive, + ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, startIndex: currentPage * 100, limit: 100, recursive: shouldBeRecursive, searchTerm: nil, sortOrder: filters.sortOrder, parentId: parentID, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], includeItemTypes: filters.filters.contains(.isFavorite) ? ["Movie", "Series", "Season", "Episode"] : ["Movie", "Series"], filters: filters.filters, sortBy: sortBy, tags: filters.tags, diff --git a/Shared/ViewModels/SeasonItemViewModel.swift b/Shared/ViewModels/SeasonItemViewModel.swift index e2919306..c3d1a739 100644 --- a/Shared/ViewModels/SeasonItemViewModel.swift +++ b/Shared/ViewModels/SeasonItemViewModel.swift @@ -32,7 +32,7 @@ final class SeasonItemViewModel: ItemViewModel { private func requestEpisodes() { LogManager.shared.log .debug("Getting episodes in season \(item.id!) (\(item.name!)) of show \(item.seriesId!) (\(item.seriesName!))") - TvShowsAPI.getEpisodes(seriesId: item.seriesId ?? "", userId: SessionManager.current.user.user_id!, + TvShowsAPI.getEpisodes(seriesId: item.seriesId ?? "", userId: SessionManager.main.currentLogin.user.id, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], seasonId: item.id ?? "") .trackActivity(loading) @@ -73,7 +73,7 @@ final class SeasonItemViewModel: ItemViewModel { func routeToSeriesItem() { guard let id = item.seriesId else { return } - UserLibraryAPI.getItem(userId: SessionManager.current.user.user_id!, itemId: id) + UserLibraryAPI.getItem(userId: SessionManager.main.currentLogin.user.id, itemId: id) .trackActivity(loading) .sink(receiveCompletion: { [weak self] completion in self?.handleAPIRequestError(completion: completion) diff --git a/Shared/ViewModels/SeriesItemViewModel.swift b/Shared/ViewModels/SeriesItemViewModel.swift index 81a9e8e1..b204c87b 100644 --- a/Shared/ViewModels/SeriesItemViewModel.swift +++ b/Shared/ViewModels/SeriesItemViewModel.swift @@ -35,7 +35,7 @@ final class SeriesItemViewModel: ItemViewModel { private func getNextUp() { LogManager.shared.log.debug("Getting next up for show \(self.item.id!) (\(self.item.name!))") - TvShowsAPI.getNextUp(userId: SessionManager.current.user.user_id!, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], seriesId: self.item.id!, enableUserData: true) + TvShowsAPI.getNextUp(userId: SessionManager.main.currentLogin.user.id, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], seriesId: self.item.id!, enableUserData: true) .trackActivity(loading) .sink(receiveCompletion: { [weak self] completion in self?.handleAPIRequestError(completion: completion) @@ -67,7 +67,7 @@ final class SeriesItemViewModel: ItemViewModel { private func requestSeasons() { LogManager.shared.log.debug("Getting seasons of show \(self.item.id!) (\(self.item.name!))") - TvShowsAPI.getSeasons(seriesId: item.id ?? "", userId: SessionManager.current.user.user_id!, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], enableUserData: true) + TvShowsAPI.getSeasons(seriesId: item.id ?? "", userId: SessionManager.main.currentLogin.user.id, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], enableUserData: true) .trackActivity(loading) .sink(receiveCompletion: { [weak self] completion in self?.handleAPIRequestError(completion: completion) diff --git a/Shared/ViewModels/ServerListViewModel.swift b/Shared/ViewModels/ServerListViewModel.swift new file mode 100644 index 00000000..d77c7225 --- /dev/null +++ b/Shared/ViewModels/ServerListViewModel.swift @@ -0,0 +1,20 @@ +// + /* + * 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 + +class ServerListViewModel: ObservableObject { + + @Published var servers: [SwiftfinStore.Models.Server] = [] + + init() { + self.servers = SessionManager.main.fetchServers() + } +} diff --git a/Shared/ViewModels/SplashViewModel.swift b/Shared/ViewModels/SplashViewModel.swift index df524bb1..dbbe9a56 100644 --- a/Shared/ViewModels/SplashViewModel.swift +++ b/Shared/ViewModels/SplashViewModel.swift @@ -21,7 +21,8 @@ final class SplashViewModel: ViewModel { @Published var isLoggedIn: Bool = false override init() { - isLoggedIn = ServerEnvironment.current.server != nil && SessionManager.current.user != nil + // TODO: Remove SplashViewModel + isLoggedIn = SessionManager.main.currentLogin != nil super.init() ImageCache.shared.costLimit = 125 * 1024 * 1024 // 125MB memory diff --git a/Shared/ViewModels/UserLoginViewModel.swift b/Shared/ViewModels/UserLoginViewModel.swift new file mode 100644 index 00000000..103d884c --- /dev/null +++ b/Shared/ViewModels/UserLoginViewModel.swift @@ -0,0 +1,47 @@ +// + /* + * 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 JellyfinAPI +import Stinsen + +final class UserLoginViewModel: ViewModel { + + let server: SwiftfinStore.Models.Server + + init(server: SwiftfinStore.Models.Server) { + self.server = server + } + + func login(username: String, password: String) { + LogManager.shared.log.debug("Attempting to login to server at \"\(server.uri)\"", tag: "login") + LogManager.shared.log.debug("username == \"\": \(username), password == \"\": \(password)", tag: "login") + + SessionManager.main.loginUser(server: server, username: username, password: password) + .trackActivity(loading) + .sink { completion in + self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "login", + completion: completion) + } receiveValue: { user in + print(user) + } + .store(in: &cancellables) +// +// +// SessionManager.current.login(username: username, password: password) +// .trackActivity(loading) +// .sink(receiveCompletion: { completion in +// self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "login", +// completion: completion) +// }, receiveValue: { [weak self] _ in +// self?.main?.root(\.mainTab) +// }) +// .store(in: &cancellables) + } +} diff --git a/WidgetExtension/NextUpWidget.swift b/WidgetExtension/NextUpWidget.swift index fbe1930a..eec8417d 100644 --- a/WidgetExtension/NextUpWidget.swift +++ b/WidgetExtension/NextUpWidget.swift @@ -24,92 +24,94 @@ struct NextUpWidgetProvider: TimelineProvider { } func getSnapshot(in context: Context, completion: @escaping (NextUpEntry) -> Void) { + + guard let currentLogin = SessionManager.main.currentLogin else { return } + let currentDate = Date() - let server = ServerEnvironment.current.server - let savedUser = SessionManager.current.user + let server = currentLogin.server + let savedUser = currentLogin.user var tempCancellables = Set() - if server != nil && savedUser != nil { - JellyfinAPI.basePath = server!.baseURI ?? "" - TvShowsAPI.getNextUp(userId: savedUser!.user_id, limit: 3, - fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], - imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb]) - .subscribe(on: DispatchQueue.global(qos: .background)) - .sink(receiveCompletion: { result in - switch result { - case .finished: - break - case let .failure(error): - completion(NextUpEntry(date: currentDate, items: [], error: error)) - } - }, receiveValue: { response in - let dispatchGroup = DispatchGroup() - let items = response.items ?? [] - var downloadedItems = [(BaseItemDto, UIImage?)]() - items.enumerated().forEach { _, item in - dispatchGroup.enter() - ImagePipeline.shared.loadImage(with: item.getBackdropImage(maxWidth: 320)) { result in - guard case let .success(image) = result else { - dispatchGroup.leave() - return - } - downloadedItems.append((item, image.image)) + JellyfinAPI.basePath = server.uri + TvShowsAPI.getNextUp(userId: savedUser.id, limit: 3, + fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], + imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb]) + .subscribe(on: DispatchQueue.global(qos: .background)) + .sink(receiveCompletion: { result in + switch result { + case .finished: + break + case let .failure(error): + completion(NextUpEntry(date: currentDate, items: [], error: error)) + } + }, receiveValue: { response in + let dispatchGroup = DispatchGroup() + let items = response.items ?? [] + var downloadedItems = [(BaseItemDto, UIImage?)]() + items.enumerated().forEach { _, item in + dispatchGroup.enter() + ImagePipeline.shared.loadImage(with: item.getBackdropImage(maxWidth: 320)) { result in + guard case let .success(image) = result else { dispatchGroup.leave() + return } + downloadedItems.append((item, image.image)) + dispatchGroup.leave() } + } - dispatchGroup.notify(queue: .main) { - completion(NextUpEntry(date: currentDate, items: downloadedItems, error: nil)) - } - }) - .store(in: &tempCancellables) - } + dispatchGroup.notify(queue: .main) { + completion(NextUpEntry(date: currentDate, items: downloadedItems, error: nil)) + } + }) + .store(in: &tempCancellables) } func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + + guard let currentLogin = SessionManager.main.currentLogin else { return } + let currentDate = Date() let entryDate = Calendar.current.date(byAdding: .hour, value: 1, to: currentDate)! - let server = ServerEnvironment.current.server - let savedUser = SessionManager.current.user + let server = currentLogin.server + let savedUser = currentLogin.user var tempCancellables = Set() - if server != nil && savedUser != nil { - JellyfinAPI.basePath = server!.baseURI ?? "" - TvShowsAPI.getNextUp(userId: savedUser!.user_id, limit: 3, - fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], - imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb]) - .subscribe(on: DispatchQueue.global(qos: .background)) - .sink(receiveCompletion: { result in - switch result { - case .finished: - break - case let .failure(error): - completion(Timeline(entries: [NextUpEntry(date: currentDate, items: [], error: error)], policy: .after(entryDate))) - } - }, receiveValue: { response in - let dispatchGroup = DispatchGroup() - let items = response.items ?? [] - var downloadedItems = [(BaseItemDto, UIImage?)]() - items.enumerated().forEach { _, item in - dispatchGroup.enter() - ImagePipeline.shared.loadImage(with: item.getBackdropImage(maxWidth: 320)) { result in - guard case let .success(image) = result else { - dispatchGroup.leave() - return - } - downloadedItems.append((item, image.image)) + JellyfinAPI.basePath = server.uri + TvShowsAPI.getNextUp(userId: savedUser.id, limit: 3, + fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], + imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb]) + .subscribe(on: DispatchQueue.global(qos: .background)) + .sink(receiveCompletion: { result in + switch result { + case .finished: + break + case let .failure(error): + completion(Timeline(entries: [NextUpEntry(date: currentDate, items: [], error: error)], policy: .after(entryDate))) + } + }, receiveValue: { response in + let dispatchGroup = DispatchGroup() + let items = response.items ?? [] + var downloadedItems = [(BaseItemDto, UIImage?)]() + items.enumerated().forEach { _, item in + dispatchGroup.enter() + ImagePipeline.shared.loadImage(with: item.getBackdropImage(maxWidth: 320)) { result in + guard case let .success(image) = result else { dispatchGroup.leave() + return } + downloadedItems.append((item, image.image)) + dispatchGroup.leave() } + } - dispatchGroup.notify(queue: .main) { - completion(Timeline(entries: [NextUpEntry(date: currentDate, items: downloadedItems, error: nil)], - policy: .after(entryDate))) - } - }) - .store(in: &tempCancellables) - } + dispatchGroup.notify(queue: .main) { + completion(Timeline(entries: [NextUpEntry(date: currentDate, items: downloadedItems, error: nil)], + policy: .after(entryDate))) + } + }) + .store(in: &tempCancellables) } } @@ -198,7 +200,8 @@ extension NextUpEntryView { } func smallVideoView(item: (BaseItemDto, UIImage?)) -> some View { - Link(destination: URL(string: "widget-extension://Users/\(SessionManager.current.user.user_id!)/Items/\(item.0.id!)")!, label: { + let url = URL(string: "widget-extension://Users/\(SessionManager.main.currentLogin.user.id)/Items/\(item.0.id!)")! + return Link(destination: url, label: { VStack(alignment: .leading) { if let image = item.1 { Image(uiImage: image) @@ -223,7 +226,8 @@ extension NextUpEntryView { } func largeVideoView(item: (BaseItemDto, UIImage?)) -> some View { - Link(destination: URL(string: "widget-extension://Users/\(SessionManager.current.user.user_id!)/Items/\(item.0.id!)")!, label: { + let url = URL(string: "widget-extension://Users/\(SessionManager.main.currentLogin.user.id)/Items/\(item.0.id!)")! + return Link(destination: url, label: { HStack(spacing: 20) { if let image = item.1 { Image(uiImage: image) @@ -285,7 +289,8 @@ extension NextUpEntryView { func large(items: [(BaseItemDto, UIImage?)]) -> some View { VStack(spacing: 0) { if let firstItem = items[safe: 0] { - Link(destination: URL(string: "widget-extension://Users/\(SessionManager.current.user.user_id!)/Items/\(firstItem.0.id!)")!, + let url = URL(string: "widget-extension://Users/\(SessionManager.main.currentLogin.user.id)/Items/\(firstItem.0.id!)")! + Link(destination: url, label: { ZStack(alignment: .topTrailing) { ZStack(alignment: .bottomLeading) {