From 43090831cb8c8e48ff85d2db27a11b53bbe126af Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Sun, 24 Oct 2021 21:33:07 -0600 Subject: [PATCH 1/8] Add multi url support to stored server --- .../Components/PublicUserButton.swift | 2 +- .../Views/ServerDetailView.swift | 2 +- .../VideoPlayerViewController.swift | 8 +-- .../Views/ConnectToServerView.swift | 10 ++- JellyfinPlayer/Views/ServerDetailView.swift | 2 +- JellyfinPlayer/Views/ServerListView.swift | 2 +- .../Views/VideoPlayer/VideoPlayer.swift | 12 ++-- .../BaseItemDtoExtensions.swift | 8 +-- .../BaseItemPersonExtensions.swift | 2 +- Shared/Singleton/SessionManager.swift | 67 ++++++++++++++++--- Shared/SwiftfinStore/SwiftfinStore.swift | 44 ++++++++---- .../ViewModels/ConnectToServerViewModel.swift | 44 +++++++++++- Shared/ViewModels/UserSignInViewModel.swift | 2 +- WidgetExtension/NextUpWidget.swift | 4 +- 14 files changed, 162 insertions(+), 47 deletions(-) diff --git a/JellyfinPlayer tvOS/Components/PublicUserButton.swift b/JellyfinPlayer tvOS/Components/PublicUserButton.swift index 36943861..b7da051e 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: "\(SessionManager.main.currentLogin.server.uri)/Users/\(publicUser.id ?? "")/Images/Primary?width=500&quality=80&tag=\(publicUser.primaryImageTag!)")!) + ImageView(src: URL(string: "\(SessionManager.main.currentLogin.server.currentURI)/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/Views/ServerDetailView.swift b/JellyfinPlayer tvOS/Views/ServerDetailView.swift index 74c88ce6..e876b003 100644 --- a/JellyfinPlayer tvOS/Views/ServerDetailView.swift +++ b/JellyfinPlayer tvOS/Views/ServerDetailView.swift @@ -26,7 +26,7 @@ struct ServerDetailView: View { HStack { Text("URI") Spacer() - Text(SessionManager.main.currentLogin.server.uri) + Text(SessionManager.main.currentLogin.server.currentURI) .foregroundColor(.secondary) } diff --git a/JellyfinPlayer tvOS/Views/VideoPlayer/VideoPlayerViewController.swift b/JellyfinPlayer tvOS/Views/VideoPlayer/VideoPlayerViewController.swift index 0c4c5bae..d99c47e9 100644 --- a/JellyfinPlayer tvOS/Views/VideoPlayer/VideoPlayerViewController.swift +++ b/JellyfinPlayer tvOS/Views/VideoPlayer/VideoPlayerViewController.swift @@ -164,13 +164,13 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, // Item is being transcoded by request of server if let transcodiungUrl = mediaSource.transcodingUrl { item.videoType = .transcode - streamURL = URL(string: "\(SessionManager.main.currentLogin.server.uri)\(transcodiungUrl)")! + streamURL = URL(string: "\(SessionManager.main.currentLogin.server.currentURI)\(transcodiungUrl)")! } // Item will be directly played by the client else { item.videoType = .directPlay -// 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!)")! - streamURL = URL(string: "\(SessionManager.main.currentLogin.server.uri)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&Tag=\(mediaSource.eTag ?? "")")! +// streamURL = URL(string: "\(SessionManager.main.currentLogin.server.currentURI)/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.currentURI)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&Tag=\(mediaSource.eTag ?? "")")! } item.videoUrl = streamURL @@ -185,7 +185,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, var deliveryUrl: URL? if stream.deliveryMethod == .external { - deliveryUrl = URL(string: "\(SessionManager.main.currentLogin.server.uri)\(stream.deliveryUrl!)")! + deliveryUrl = URL(string: "\(SessionManager.main.currentLogin.server.currentURI)\(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/Views/ConnectToServerView.swift b/JellyfinPlayer/Views/ConnectToServerView.swift index eb546920..25189eaa 100644 --- a/JellyfinPlayer/Views/ConnectToServerView.swift +++ b/JellyfinPlayer/Views/ConnectToServerView.swift @@ -11,7 +11,7 @@ import Stinsen struct ConnectToServerView: View { - @StateObject var viewModel: ConnectToServerViewModel + @ObservedObject var viewModel: ConnectToServerViewModel @State var uri = "" var body: some View { @@ -98,6 +98,14 @@ struct ConnectToServerView: View { message: Text(viewModel.errorMessage?.displayMessage ?? "Unknown Error"), dismissButton: .cancel()) } + .alert(item: $viewModel.addServerURIPayload) { _ in + Alert(title: Text("Existing Server"), + message: Text("Server \(viewModel.addServerURIPayload?.server.name ?? "") already exists. Add new URI?"), + primaryButton: .default(Text("Existing Server"), action: { + viewModel.addURIToServer(addServerURIPayload: viewModel.addServerURIPayload!) + }), + secondaryButton: .cancel()) + } .navigationTitle("Connect") .onAppear { viewModel.discoverServers() diff --git a/JellyfinPlayer/Views/ServerDetailView.swift b/JellyfinPlayer/Views/ServerDetailView.swift index 89fb08ab..cd07026b 100644 --- a/JellyfinPlayer/Views/ServerDetailView.swift +++ b/JellyfinPlayer/Views/ServerDetailView.swift @@ -26,7 +26,7 @@ struct ServerDetailView: View { HStack { Text("URI") Spacer() - Text(SessionManager.main.currentLogin.server.uri) + Text(SessionManager.main.currentLogin.server.currentURI) .foregroundColor(.secondary) } diff --git a/JellyfinPlayer/Views/ServerListView.swift b/JellyfinPlayer/Views/ServerListView.swift index bd0ea63e..3d454bff 100644 --- a/JellyfinPlayer/Views/ServerListView.swift +++ b/JellyfinPlayer/Views/ServerListView.swift @@ -38,7 +38,7 @@ struct ServerListView: View { .font(.title2) .foregroundColor(.primary) - Text(server.uri) + Text(server.currentURI) .font(.footnote) .disabled(true) .foregroundColor(.secondary) diff --git a/JellyfinPlayer/Views/VideoPlayer/VideoPlayer.swift b/JellyfinPlayer/Views/VideoPlayer/VideoPlayer.swift index e0ddcb71..bfe64e16 100644 --- a/JellyfinPlayer/Views/VideoPlayer/VideoPlayer.swift +++ b/JellyfinPlayer/Views/VideoPlayer/VideoPlayer.swift @@ -549,7 +549,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: "\(SessionManager.main.currentLogin.server.uri)\(mediaSource.transcodingUrl!)") + let streamURL = URL(string: "\(SessionManager.main.currentLogin.server.currentURI)\(mediaSource.transcodingUrl!)") let item = PlaybackItem() item.videoType = .transcode item.videoUrl = streamURL! @@ -563,7 +563,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe if stream.type == .subtitle { var deliveryUrl: URL? if stream.deliveryMethod == .external { - deliveryUrl = URL(string: "\(SessionManager.main.currentLogin.server.uri)\(stream.deliveryUrl ?? "")")! + deliveryUrl = URL(string: "\(SessionManager.main.currentLogin.server.currentURI)\(stream.deliveryUrl ?? "")")! } else { deliveryUrl = nil } @@ -597,8 +597,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } else { // TODO: todo // Item will be directly played by the client. - 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 streamURL = URL(string: "\(SessionManager.main.currentLogin.server.currentURI)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&Tag=\(mediaSource.eTag ?? "")")! +// URL(string: "\(SessionManager.main.currentLogin.server.currentURI)/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 +613,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe if stream.type == .subtitle { var deliveryUrl: URL? if stream.deliveryMethod == .external { - deliveryUrl = URL(string: "\(SessionManager.main.currentLogin.server.uri)\(stream.deliveryUrl!)")! + deliveryUrl = URL(string: "\(SessionManager.main.currentLogin.server.currentURI)\(stream.deliveryUrl!)")! } else { deliveryUrl = nil } @@ -876,7 +876,7 @@ extension PlayerViewController: GCKGenericChannelDelegate { "userId": SessionManager.main.currentLogin.user.id, // "deviceId": SessionManager.main.currentLogin.de.deviceID, "accessToken": SessionManager.main.currentLogin.user.accessToken, - "serverAddress": SessionManager.main.currentLogin.server.uri, + "serverAddress": SessionManager.main.currentLogin.server.currentURI, "serverId": SessionManager.main.currentLogin.server.id, "serverVersion": "10.8.0", "receiverName": castSessionManager.currentCastSession!.device.friendlyName!, diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift index 78ad8991..48979800 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 = - "\(SessionManager.main.currentLogin.server.uri)/Items/\(imageItemId)/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)" + "\(SessionManager.main.currentLogin.server.currentURI)/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 = - "\(SessionManager.main.currentLogin.server.uri)/Items/\(parentBackdropItemId ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)" + "\(SessionManager.main.currentLogin.server.currentURI)/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 = - "\(SessionManager.main.currentLogin.server.uri)/Items/\(seriesId ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)" + "\(SessionManager.main.currentLogin.server.currentURI)/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 = - "\(SessionManager.main.currentLogin.server.uri)/Items/\(imageItemId)/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)" + "\(SessionManager.main.currentLogin.server.currentURI)/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 63b2d239..6d9a06a3 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: SessionManager.main.currentLogin.server.uri, maxWidth: maxWidth) + return self.getImage(baseURL: SessionManager.main.currentLogin.server.currentURI, maxWidth: maxWidth) } public var title: String { diff --git a/Shared/Singleton/SessionManager.swift b/Shared/Singleton/SessionManager.swift index d803e8ac..dbf3db37 100644 --- a/Shared/Singleton/SessionManager.swift +++ b/Shared/Singleton/SessionManager.swift @@ -34,16 +34,12 @@ final class SessionManager { guard let server = user.server, let accessToken = user.accessToken else { fatalError("No associated server or access token for last user?") } guard let existingServer = SwiftfinStore.dataStack.fetchExisting(server) else { return } - JellyfinAPI.basePath = server.uri + JellyfinAPI.basePath = server.currentURI setAuthHeader(with: accessToken.value) currentLogin = (server: existingServer.state, user: user.state) } } - private func generateServerUserID(server: SwiftfinStore.Models.StoredServer, user: SwiftfinStore.Models.StoredUser) -> String { - return "\(server.id)-\(user.id)" - } - func fetchServers() -> [SwiftfinStore.State.Server] { let servers = try! SwiftfinStore.dataStack.fetchAll(From()) return servers.map({ $0.state }) @@ -79,7 +75,8 @@ final class SessionManager { let os = response.operatingSystem, let version = response.version else { throw JellyfinAPIError("Missing server data from network call") } - newServer.uri = uri + newServer.uris = [uri] + newServer.currentURI = uri newServer.name = name newServer.id = id newServer.os = os @@ -103,11 +100,65 @@ final class SessionManager { .eraseToAnyPublisher() } + func addURIToServer(server: SwiftfinStore.State.Server, uri: String) -> AnyPublisher { + return Just(server) + .tryMap { server -> (SwiftfinStore.Models.StoredServer, UnsafeDataTransaction) in + + let transaction = SwiftfinStore.dataStack.beginUnsafe() + + guard let existingServer = try? SwiftfinStore.dataStack.fetchOne(From(), + [Where("id == %@", server.id)]) else { + fatalError("No stored server associated with given state server?") + } + + guard let editServer = transaction.edit(existingServer) else { fatalError("Can't get proxy for existing object?") } + editServer.uris.insert(uri) + + return (editServer, transaction) + } + .handleEvents(receiveOutput: { (server, transaction) in + try? transaction.commitAndWait() + }) + .map({ (server, _) in + return server.state + }) + .eraseToAnyPublisher() + } + + func setServerCurrentURI(server: SwiftfinStore.State.Server, uri: String) -> AnyPublisher { + return Just(server) + .tryMap { server -> (SwiftfinStore.Models.StoredServer, UnsafeDataTransaction) in + + let transaction = SwiftfinStore.dataStack.beginUnsafe() + + guard let existingServer = try? SwiftfinStore.dataStack.fetchOne(From(), + [Where("id == %@", server.id)]) else { + fatalError("No stored server associated with given state server?") + } + + if !existingServer.uris.contains(uri) { + fatalError("Attempting to set current uri while server doesn't contain it?") + } + + guard let editServer = transaction.edit(existingServer) else { fatalError("Can't get proxy for existing object?") } + editServer.currentURI = uri + + return (editServer, transaction) + } + .handleEvents(receiveOutput: { (server, transaction) in + try? transaction.commitAndWait() + }) + .map({ (server, _) in + return server.state + }) + .eraseToAnyPublisher() + } + // Logs in a user with an associated server, storing if successful func loginUser(server: SwiftfinStore.State.Server, username: String, password: String) -> AnyPublisher { setAuthHeader(with: "") - JellyfinAPI.basePath = server.uri + JellyfinAPI.basePath = server.currentURI return UserAPI.authenticateUserByName(authenticateUserByName: AuthenticateUserByName(username: username, pw: password)) .tryMap({ response -> (SwiftfinStore.Models.StoredServer, SwiftfinStore.Models.StoredUser, UnsafeDataTransaction) in @@ -163,7 +214,7 @@ final class SessionManager { } func loginUser(server: SwiftfinStore.State.Server, user: SwiftfinStore.State.User) { - JellyfinAPI.basePath = server.uri + JellyfinAPI.basePath = server.currentURI SwiftfinStore.Defaults.suite[.lastServerUserID] = user.id setAuthHeader(with: user.accessToken) currentLogin = (server: server, user: user) diff --git a/Shared/SwiftfinStore/SwiftfinStore.swift b/Shared/SwiftfinStore/SwiftfinStore.swift index 1711e9cb..ff8aa8e7 100644 --- a/Shared/SwiftfinStore/SwiftfinStore.swift +++ b/Shared/SwiftfinStore/SwiftfinStore.swift @@ -14,20 +14,22 @@ import Defaults enum SwiftfinStore { // MARK: State - // Safe, copyable representations of their underlying CoreStoredObject's + // Safe, copyable representations of their underlying CoreStoredObject // Relationships are represented by the related object's IDs or value enum State { struct Server { - let uri: String + let uris: Set + let currentURI: String let name: String let id: String let os: String let version: String let userIDs: [String] - fileprivate init(uri: String, name: String, id: String, os: String, version: String, usersIDs: [String]) { - self.uri = uri + fileprivate init(uris: Set, currentURI: String, name: String, id: String, os: String, version: String, usersIDs: [String]) { + self.uris = uris + self.currentURI = currentURI self.name = name self.id = id self.os = os @@ -36,7 +38,13 @@ enum SwiftfinStore { } static var sample: Server { - return Server(uri: "https://www.notaurl.com", name: "Johnny's Tree", id: "123abc", os: "macOS", version: "1.1.1", usersIDs: ["1", "2"]) + return Server(uris: ["https://www.notaurl.com", "http://www.maybeaurl.org"], + currentURI: "https://www.notaurl.com", + name: "Johnny's Tree", + id: "123abc", + os: "macOS", + version: "1.1.1", + usersIDs: ["1", "2"]) } } @@ -54,7 +62,10 @@ enum SwiftfinStore { } static var sample: User { - return User(username: "JohnnyAppleseed", id: "123abc", serverID: "123abc", accessToken: "open-sesame") + return User(username: "JohnnyAppleseed", + id: "123abc", + serverID: "123abc", + accessToken: "open-sesame") } } } @@ -64,8 +75,11 @@ enum SwiftfinStore { final class StoredServer: CoreStoreObject { - @Field.Stored("uri") - var uri: String = "" + @Field.Coded("uris", coder: FieldCoders.Json.self) + var uris: Set = [] + + @Field.Stored("currentURI") + var currentURI: String = "" @Field.Stored("name") var name: String = "" @@ -83,7 +97,8 @@ enum SwiftfinStore { var users: Set var state: State.Server { - return State.Server(uri: uri, + return State.Server(uris: uris, + currentURI: currentURI, name: name, id: id, os: os, @@ -143,11 +158,12 @@ enum SwiftfinStore { Entity("User"), Entity("AccessToken") ], - versionLock: [ - "AccessToken": [0xa8c475e874494bb1, 0x79486e93449f0b3d, 0xa7dc4a0003541edb, 0x94183fae7580ef72], - "Server": [0x39c64a826739077e, 0xa7ac63744fd7df32, 0xef3c9d4fe638fbfb, 0xdabd796256df14db], - "User": [0x845de08a74bc53ed, 0xe95a406a29f3a5d0, 0x9eda732821a15ea9, 0xb5afa531e41ce8a] - ]) + versionLock: nil) +// versionLock: [ +// "AccessToken": [0xa8c475e874494bb1, 0x79486e93449f0b3d, 0xa7dc4a0003541edb, 0x94183fae7580ef72], +// "Server": [0x39c64a826739077e, 0xa7ac63744fd7df32, 0xef3c9d4fe638fbfb, 0xdabd796256df14db], +// "User": [0x845de08a74bc53ed, 0xe95a406a29f3a5d0, 0x9eda732821a15ea9, 0xb5afa531e41ce8a] +// ]) let _dataStack = DataStack(schema) try! _dataStack.addStorageAndWait( diff --git a/Shared/ViewModels/ConnectToServerViewModel.swift b/Shared/ViewModels/ConnectToServerViewModel.swift index 2f4b1460..ad9ee7e2 100644 --- a/Shared/ViewModels/ConnectToServerViewModel.swift +++ b/Shared/ViewModels/ConnectToServerViewModel.swift @@ -12,11 +12,23 @@ import Foundation import JellyfinAPI import Stinsen +struct AddServerURIPayload: Identifiable { + + let server: SwiftfinStore.State.Server + let uri: String + + var id: String { + return server.id.appending(uri) + } +} + final class ConnectToServerViewModel: ViewModel { @RouterObject var router: ConnectToServerCoodinator.Router? @Published var discoveredServers: Set = [] @Published var searching = false + @Published var addServerURIPayload: AddServerURIPayload? + private let discovery = ServerDiscovery() var alertTitle: String { @@ -40,8 +52,25 @@ final class ConnectToServerViewModel: ViewModel { 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) + // This is disgusting. ViewModel Error handling overall needs to be refactored + switch completion { + case .finished: () + case .failure(let error): + switch error { + case is SwiftfinStore.Errors: + let swiftfinError = error as! SwiftfinStore.Errors + switch swiftfinError { + case .existingServer(let server): + self.addServerURIPayload = AddServerURIPayload(server: server, uri: uri) + default: + self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "connectToServer", + completion: completion) + } + default: + self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "connectToServer", + completion: completion) + } + } }, receiveValue: { server in LogManager.shared.log.debug("Connected to server at \"\(uri)\"", tag: "connectToServer") self.router?.route(to: \.userSignIn, server) @@ -65,6 +94,17 @@ final class ConnectToServerViewModel: ViewModel { } } + func addURIToServer(addServerURIPayload: AddServerURIPayload) { + SessionManager.main.addURIToServer(server: addServerURIPayload.server, uri: addServerURIPayload.uri) + .sink { completion in + self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "connectToServer", + completion: completion) + } receiveValue: { _ in + print("Here") + } + .store(in: &cancellables) + } + func cancelConnection() { for cancellable in cancellables { cancellable.cancel() diff --git a/Shared/ViewModels/UserSignInViewModel.swift b/Shared/ViewModels/UserSignInViewModel.swift index 14d2b115..e58e122d 100644 --- a/Shared/ViewModels/UserSignInViewModel.swift +++ b/Shared/ViewModels/UserSignInViewModel.swift @@ -30,7 +30,7 @@ final class UserSignInViewModel: ViewModel { } func login(username: String, password: String) { - LogManager.shared.log.debug("Attempting to login to server at \"\(server.uri)\"", tag: "login") + LogManager.shared.log.debug("Attempting to login to server at \"\(server.currentURI)\"", tag: "login") LogManager.shared.log.debug("username: \(username), password: \(password)", tag: "login") SessionManager.main.loginUser(server: server, username: username, password: password) diff --git a/WidgetExtension/NextUpWidget.swift b/WidgetExtension/NextUpWidget.swift index eec8417d..7fe9c78c 100644 --- a/WidgetExtension/NextUpWidget.swift +++ b/WidgetExtension/NextUpWidget.swift @@ -32,7 +32,7 @@ struct NextUpWidgetProvider: TimelineProvider { let savedUser = currentLogin.user var tempCancellables = Set() - JellyfinAPI.basePath = server.uri + JellyfinAPI.basePath = server.currentURI TvShowsAPI.getNextUp(userId: savedUser.id, limit: 3, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb]) @@ -78,7 +78,7 @@ struct NextUpWidgetProvider: TimelineProvider { var tempCancellables = Set() - JellyfinAPI.basePath = server.uri + JellyfinAPI.basePath = server.currentURI TvShowsAPI.getNextUp(userId: savedUser.id, limit: 3, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb]) From efb751f00fed13bb7a77b74c9b19c19ac9a86b2c Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Mon, 25 Oct 2021 12:00:30 -0600 Subject: [PATCH 2/8] Add setting current URI after new connection --- JellyfinPlayer/Views/ConnectToServerView.swift | 6 +++--- Shared/ViewModels/ConnectToServerViewModel.swift | 13 +++++++++++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/JellyfinPlayer/Views/ConnectToServerView.swift b/JellyfinPlayer/Views/ConnectToServerView.swift index 25189eaa..5f65ab3a 100644 --- a/JellyfinPlayer/Views/ConnectToServerView.swift +++ b/JellyfinPlayer/Views/ConnectToServerView.swift @@ -100,9 +100,9 @@ struct ConnectToServerView: View { } .alert(item: $viewModel.addServerURIPayload) { _ in Alert(title: Text("Existing Server"), - message: Text("Server \(viewModel.addServerURIPayload?.server.name ?? "") already exists. Add new URI?"), - primaryButton: .default(Text("Existing Server"), action: { - viewModel.addURIToServer(addServerURIPayload: viewModel.addServerURIPayload!) + message: Text("Server \(viewModel.addServerURIPayload?.server.name ?? "") already exists. Add new URL?"), + primaryButton: .default(Text("Add URL"), action: { + viewModel.addURIToServer(addServerURIPayload: viewModel.backAddServerURIPayload!) }), secondaryButton: .cancel()) } diff --git a/Shared/ViewModels/ConnectToServerViewModel.swift b/Shared/ViewModels/ConnectToServerViewModel.swift index ad9ee7e2..fca80514 100644 --- a/Shared/ViewModels/ConnectToServerViewModel.swift +++ b/Shared/ViewModels/ConnectToServerViewModel.swift @@ -28,6 +28,7 @@ final class ConnectToServerViewModel: ViewModel { @Published var discoveredServers: Set = [] @Published var searching = false @Published var addServerURIPayload: AddServerURIPayload? + var backAddServerURIPayload: AddServerURIPayload? private let discovery = ServerDiscovery() @@ -62,6 +63,7 @@ final class ConnectToServerViewModel: ViewModel { switch swiftfinError { case .existingServer(let server): self.addServerURIPayload = AddServerURIPayload(server: server, uri: uri) + self.backAddServerURIPayload = AddServerURIPayload(server: server, uri: uri) default: self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "connectToServer", completion: completion) @@ -99,8 +101,15 @@ final class ConnectToServerViewModel: ViewModel { .sink { completion in self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "connectToServer", completion: completion) - } receiveValue: { _ in - print("Here") + } receiveValue: { server in + SessionManager.main.setServerCurrentURI(server: server, uri: addServerURIPayload.uri) + .sink { completion in + self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "connectToServer", + completion: completion) + } receiveValue: { server in + self.router?.dismissCoordinator() + } + .store(in: &self.cancellables) } .store(in: &cancellables) } From 55dc1ec7e434545c0286f9a03cb2348820d3e671 Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Sat, 6 Nov 2021 22:21:53 -0600 Subject: [PATCH 3/8] Allow switching URIs and see details from UserListView --- JellyfinPlayer.xcodeproj/project.pbxproj | 12 ++++-- JellyfinPlayer/Info.plist | 8 ++-- JellyfinPlayer/Views/ServerDetailView.swift | 39 ++++++++----------- JellyfinPlayer/Views/UserListView.swift | 12 ++++-- .../MainCoordinator/iOSMainCoordinator.swift | 10 +++++ .../ServerDetailCoordinator.swift | 29 ++++++++++++++ Shared/Coordinators/SettingsCoordinator.swift | 3 +- Shared/Coordinators/UserListCoordinator.swift | 5 +++ .../SwiftfinNotificationCenter.swift | 1 + Shared/ViewModels/HomeViewModel.swift | 30 +++++++++++++- Shared/ViewModels/ServerDetailViewModel.swift | 26 ++++++++----- Shared/ViewModels/UserListViewModel.swift | 13 ++++++- 12 files changed, 143 insertions(+), 45 deletions(-) create mode 100644 Shared/Coordinators/ServerDetailCoordinator.swift diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index a759e910..d06a9ed3 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -240,6 +240,8 @@ E11B1B6C2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */; }; E11B1B6D2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */; }; E11B1B6E2718CDBA006DA3E8 /* JellyfinAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */; }; + E11D224227378428003F9CB3 /* ServerDetailCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11D224127378428003F9CB3 /* ServerDetailCoordinator.swift */; }; + E11D224327378428003F9CB3 /* ServerDetailCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11D224127378428003F9CB3 /* ServerDetailCoordinator.swift */; }; E12186DE2718F1C50010884C /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = E12186DD2718F1C50010884C /* Defaults */; }; E1218C9A271A26BA00EA0737 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = E1218C99271A26BA00EA0737 /* Nuke */; }; E1218C9C271A26C400EA0737 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = E1218C9B271A26C400EA0737 /* Nuke */; }; @@ -553,6 +555,7 @@ DE5004F745B19E28744A7DE7 /* Pods-JellyfinPlayer tvOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JellyfinPlayer tvOS.debug.xcconfig"; path = "Target Support Files/Pods-JellyfinPlayer tvOS/Pods-JellyfinPlayer tvOS.debug.xcconfig"; sourceTree = ""; }; E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayButtonRowView.swift; sourceTree = ""; }; E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinAPIError.swift; sourceTree = ""; }; + E11D224127378428003F9CB3 /* ServerDetailCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailCoordinator.swift; sourceTree = ""; }; E1267D3D271A1F46003C492E /* PreferenceUIHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferenceUIHostingController.swift; sourceTree = ""; }; E131691626C583BC0074BFEE /* LogConstructor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogConstructor.swift; sourceTree = ""; }; E13DD3BC27163C63009D4DAF /* EmailHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailHelper.swift; sourceTree = ""; }; @@ -1073,19 +1076,20 @@ 62C29E9D26D0FE5900C1D2E7 /* Coordinators */ = { isa = PBXGroup; children = ( - 62C29EA226D1030F00C1D2E7 /* ConnectToServerCoodinator.swift */, E1D4BF892719D3D000A11E64 /* BasicAppSettingsCoordinator.swift */, + 62C29EA226D1030F00C1D2E7 /* ConnectToServerCoodinator.swift */, 6220D0B926D6092100B8E046 /* FilterCoordinator.swift */, 62C29EA526D1036A00C1D2E7 /* HomeCoordinator.swift */, - E193D5412719404B00900D82 /* MainCoordinator */, 6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */, 6220D0B326D5ED8000B8E046 /* LibraryCoordinator.swift */, 62C29EA726D103D500C1D2E7 /* LibraryListCoordinator.swift */, + E193D5412719404B00900D82 /* MainCoordinator */, C40CD921271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift */, - C4BE0762271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift */, 6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */, + E11D224127378428003F9CB3 /* ServerDetailCoordinator.swift */, E13DD3E827177ED6009D4DAF /* ServerListCoordinator.swift */, 6220D0B026D5EC9900B8E046 /* SettingsCoordinator.swift */, + C4BE0762271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift */, E13DD4012717EE79009D4DAF /* UserListCoordinator.swift */, E13DD3F127179378009D4DAF /* UserSignInCoordinator.swift */, 6220D0C526D62D8700B8E046 /* VideoPlayerCoordinator.swift */, @@ -1683,6 +1687,7 @@ 53116A17268B919A003024C9 /* SeriesItemView.swift in Sources */, E13DD3F027178F87009D4DAF /* SwiftfinNotificationCenter.swift in Sources */, 531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */, + E11D224327378428003F9CB3 /* ServerDetailCoordinator.swift in Sources */, E1D4BF8B2719D3D000A11E64 /* BasicAppSettingsCoordinator.swift in Sources */, E13DD3FA2717E961009D4DAF /* UserListViewModel.swift in Sources */, C40CD926271F8D1E000FB198 /* MovieLibrariesViewModel.swift in Sources */, @@ -1835,6 +1840,7 @@ 625CB56F2678C23300530A6E /* HomeView.swift in Sources */, E173DA5226D04AAF00CC4EB7 /* ColorExtension.swift in Sources */, 53892770263C25230035E14B /* NextUpView.swift in Sources */, + E11D224227378428003F9CB3 /* ServerDetailCoordinator.swift in Sources */, C4BE0766271FC109003F4AD1 /* TVLibrariesViewModel.swift in Sources */, 62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */, 535BAEA5264A151C005FA86D /* VideoPlayer.swift in Sources */, diff --git a/JellyfinPlayer/Info.plist b/JellyfinPlayer/Info.plist index 8ad4f31b..0cf5920e 100644 --- a/JellyfinPlayer/Info.plist +++ b/JellyfinPlayer/Info.plist @@ -63,12 +63,12 @@ network. UILaunchScreen - UIImageRespectsSafeAreaInsets - - UIImageName - swiftfin-logo UIColorName LaunchScreenBackground + UIImageName + swiftfin-logo + UIImageRespectsSafeAreaInsets + UIRequiredDeviceCapabilities diff --git a/JellyfinPlayer/Views/ServerDetailView.swift b/JellyfinPlayer/Views/ServerDetailView.swift index cd07026b..0bf68786 100644 --- a/JellyfinPlayer/Views/ServerDetailView.swift +++ b/JellyfinPlayer/Views/ServerDetailView.swift @@ -11,7 +11,13 @@ import SwiftUI struct ServerDetailView: View { - @ObservedObject var viewModel = ServerDetailViewModel() + @ObservedObject var viewModel: ServerDetailViewModel + @State var currentServerURI: String + + init(viewModel: ServerDetailViewModel) { + self.viewModel = viewModel + self.currentServerURI = viewModel.server.currentURI + } var body: some View { Form { @@ -19,44 +25,33 @@ struct ServerDetailView: View { HStack { Text("Name") Spacer() - Text(SessionManager.main.currentLogin.server.name) + Text(viewModel.server.name) .foregroundColor(.secondary) } - HStack { - Text("URI") - Spacer() - Text(SessionManager.main.currentLogin.server.currentURI) - .foregroundColor(.secondary) + Picker("URI", selection: $currentServerURI) { + ForEach(viewModel.server.uris.sorted(), id: \.self) { uri in + Text(uri).tag(uri) + .foregroundColor(.secondary) + }.onChange(of: currentServerURI) { newValue in + viewModel.setServerCurrentURI(uri: newValue) + } } HStack { Text("Version") Spacer() - Text(SessionManager.main.currentLogin.server.version) + Text(viewModel.server.version) .foregroundColor(.secondary) } HStack { Text("Operating System") Spacer() - Text(SessionManager.main.currentLogin.server.os) + Text(viewModel.server.os) .foregroundColor(.secondary) } } - - Button(action: { - viewModel.refreshServerLibrary() - }, label: { - HStack { - Text("Refresh Library") - .font(.callout) - Spacer() - if viewModel.isLoading { - ProgressView() - } - } - }).disabled(viewModel.isLoading) } } } diff --git a/JellyfinPlayer/Views/UserListView.swift b/JellyfinPlayer/Views/UserListView.swift index cd2f6411..868026ed 100644 --- a/JellyfinPlayer/Views/UserListView.swift +++ b/JellyfinPlayer/Views/UserListView.swift @@ -90,10 +90,14 @@ struct UserListView: View { @ViewBuilder private var toolbarContent: some View { - if viewModel.users.isEmpty { - EmptyView() - } else { - HStack { + HStack { + Button { + userListRouter.route(to: \.serverDetail, viewModel.server) + } label: { + Image(systemName: "info.circle.fill") + } + + if !viewModel.users.isEmpty { Button { userListRouter.route(to: \.userSignIn, viewModel.server) } label: { diff --git a/Shared/Coordinators/MainCoordinator/iOSMainCoordinator.swift b/Shared/Coordinators/MainCoordinator/iOSMainCoordinator.swift index 9c82fd44..7d72d2c9 100644 --- a/Shared/Coordinators/MainCoordinator/iOSMainCoordinator.swift +++ b/Shared/Coordinators/MainCoordinator/iOSMainCoordinator.swift @@ -44,6 +44,7 @@ final class MainCoordinator: NavigationCoordinatable { nc.addObserver(self, selector: #selector(didLogIn), name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil) nc.addObserver(self, selector: #selector(didLogOut), name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil) nc.addObserver(self, selector: #selector(processDeepLink), name: SwiftfinNotificationCenter.Keys.processDeepLink, object: nil) + nc.addObserver(self, selector: #selector(didChangeServerCurrentURI), name: SwiftfinNotificationCenter.Keys.didChangeServerCurrentURI, object: nil) } @objc func didLogIn() { @@ -68,6 +69,15 @@ final class MainCoordinator: NavigationCoordinatable { } } } + + @objc func didChangeServerCurrentURI(_ notification: Notification) { + guard let newCurrentServerState = notification.object as? SwiftfinStore.State.Server else { fatalError("Need to have new current login state server") } + guard SessionManager.main.currentLogin != nil else { return } + if newCurrentServerState.id == SessionManager.main.currentLogin.server.id { + SessionManager.main.loginUser(server: newCurrentServerState, user: SessionManager.main.currentLogin.user) + } + } + func makeMainTab() -> MainTabCoordinator { MainTabCoordinator() } diff --git a/Shared/Coordinators/ServerDetailCoordinator.swift b/Shared/Coordinators/ServerDetailCoordinator.swift new file mode 100644 index 00000000..ec8ba9b5 --- /dev/null +++ b/Shared/Coordinators/ServerDetailCoordinator.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 Foundation +import Stinsen +import SwiftUI + +final class ServerDetailCoordinator: NavigationCoordinatable { + + let stack = NavigationStack(initial: \ServerDetailCoordinator.start) + + @Root var start = makeStart + + let viewModel: ServerDetailViewModel + + init(viewModel: ServerDetailViewModel) { + self.viewModel = viewModel + } + + @ViewBuilder func makeStart() -> some View { + ServerDetailView(viewModel: viewModel) + } +} diff --git a/Shared/Coordinators/SettingsCoordinator.swift b/Shared/Coordinators/SettingsCoordinator.swift index 0b8f8a23..f24bab01 100644 --- a/Shared/Coordinators/SettingsCoordinator.swift +++ b/Shared/Coordinators/SettingsCoordinator.swift @@ -19,7 +19,8 @@ final class SettingsCoordinator: NavigationCoordinatable { @Route(.push) var serverDetail = makeServerDetail @ViewBuilder func makeServerDetail() -> some View { - ServerDetailView() + let viewModel = ServerDetailViewModel(server: SessionManager.main.currentLogin.server) + ServerDetailView(viewModel: viewModel) } @ViewBuilder func makeStart() -> some View { diff --git a/Shared/Coordinators/UserListCoordinator.swift b/Shared/Coordinators/UserListCoordinator.swift index ff728bb6..da2e67bd 100644 --- a/Shared/Coordinators/UserListCoordinator.swift +++ b/Shared/Coordinators/UserListCoordinator.swift @@ -17,6 +17,7 @@ final class UserListCoordinator: NavigationCoordinatable { @Root var start = makeStart @Route(.push) var userSignIn = makeUserSignIn + @Route(.push) var serverDetail = makeServerDetail let viewModel: UserListViewModel @@ -28,6 +29,10 @@ final class UserListCoordinator: NavigationCoordinatable { return UserSignInCoordinator(viewModel: .init(server: server)) } + func makeServerDetail(server: SwiftfinStore.State.Server) -> ServerDetailCoordinator { + return ServerDetailCoordinator(viewModel: .init(server: server)) + } + @ViewBuilder func makeStart() -> some View { UserListView(viewModel: viewModel) } diff --git a/Shared/Singleton/SwiftfinNotificationCenter.swift b/Shared/Singleton/SwiftfinNotificationCenter.swift index c20973a2..2bd2d680 100644 --- a/Shared/Singleton/SwiftfinNotificationCenter.swift +++ b/Shared/Singleton/SwiftfinNotificationCenter.swift @@ -20,5 +20,6 @@ enum SwiftfinNotificationCenter { static let didSignOut = Notification.Name("didSignOut") static let processDeepLink = Notification.Name("processDeepLink") static let didPurge = Notification.Name("didPurge") + static let didChangeServerCurrentURI = Notification.Name("didChangeCurrentLoginURI") } } diff --git a/Shared/ViewModels/HomeViewModel.swift b/Shared/ViewModels/HomeViewModel.swift index bd2c1e24..5b76dbb9 100644 --- a/Shared/ViewModels/HomeViewModel.swift +++ b/Shared/ViewModels/HomeViewModel.swift @@ -25,9 +25,37 @@ final class HomeViewModel: ViewModel { override init() { super.init() refresh() + + // Nov. 6, 2021 + // This is a workaround since Stinsen doesn't have the ability to rebuild a root at the time of writing. + // See ServerDetailViewModel.swift for feature request issue + let nc = SwiftfinNotificationCenter.main + nc.addObserver(self, selector: #selector(didSignIn), name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil) + nc.addObserver(self, selector: #selector(didSignOut), name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil) + } + + @objc func didSignIn() { + for cancellable in cancellables { + cancellable.cancel() + } + + librariesShowRecentlyAddedIDs = [] + libraries = [] + resumeItems = [] + nextUpItems = [] + + refresh() + } + + @objc func didSignOut() { + for cancellable in cancellables { + cancellable.cancel() + } + + cancellables.removeAll() } - func refresh() { + func refresh() { LogManager.shared.log.debug("Refresh called.") UserViewsAPI.getUserViews(userId: SessionManager.main.currentLogin.user.id) .trackActivity(loading) diff --git a/Shared/ViewModels/ServerDetailViewModel.swift b/Shared/ViewModels/ServerDetailViewModel.swift index cddc5cf6..5c70880d 100644 --- a/Shared/ViewModels/ServerDetailViewModel.swift +++ b/Shared/ViewModels/ServerDetailViewModel.swift @@ -11,15 +11,23 @@ import Foundation import JellyfinAPI class ServerDetailViewModel: ViewModel { - - func refreshServerLibrary() { - LibraryAPI.refreshLibrary() - .trackActivity(loading) - .sink(receiveCompletion: { completion in - self.handleAPIRequestError(completion: completion) - }, receiveValue: { - LogManager.shared.log.debug("Refreshed server library successfully") - }) + + @Published var server: SwiftfinStore.State.Server + + init(server: SwiftfinStore.State.Server) { + self.server = server + } + + func setServerCurrentURI(uri: String) { + SessionManager.main.setServerCurrentURI(server: server, uri: uri) + .sink { c in + print(c) + } receiveValue: { newServerState in + self.server = newServerState + + let nc = SwiftfinNotificationCenter.main + nc.post(name: SwiftfinNotificationCenter.Keys.didChangeServerCurrentURI, object: newServerState) + } .store(in: &cancellables) } } diff --git a/Shared/ViewModels/UserListViewModel.swift b/Shared/ViewModels/UserListViewModel.swift index 630c64a5..4d3433e8 100644 --- a/Shared/ViewModels/UserListViewModel.swift +++ b/Shared/ViewModels/UserListViewModel.swift @@ -14,10 +14,20 @@ class UserListViewModel: ViewModel { @Published var users: [SwiftfinStore.State.User] = [] - let server: SwiftfinStore.State.Server + var server: SwiftfinStore.State.Server init(server: SwiftfinStore.State.Server) { self.server = server + + super.init() + + let nc = SwiftfinNotificationCenter.main + nc.addObserver(self, selector: #selector(didChangeCurrentLoginURI), name: SwiftfinNotificationCenter.Keys.didChangeServerCurrentURI, object: nil) + } + + @objc func didChangeCurrentLoginURI(_ notification: Notification) { + guard let newServerState = notification.object as? SwiftfinStore.State.Server else { fatalError("Need to have new state server") } + self.server = newServerState } func fetchUsers() { @@ -33,4 +43,5 @@ class UserListViewModel: ViewModel { SessionManager.main.delete(user: user) fetchUsers() } + } From 039e469ebb0d12be7cebb784bb770ee0074c04d2 Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Sat, 6 Nov 2021 22:27:37 -0600 Subject: [PATCH 4/8] Lock datastack versions --- Shared/SwiftfinStore/SwiftfinStore.swift | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Shared/SwiftfinStore/SwiftfinStore.swift b/Shared/SwiftfinStore/SwiftfinStore.swift index ff8aa8e7..4dd50ef8 100644 --- a/Shared/SwiftfinStore/SwiftfinStore.swift +++ b/Shared/SwiftfinStore/SwiftfinStore.swift @@ -158,12 +158,11 @@ enum SwiftfinStore { Entity("User"), Entity("AccessToken") ], - versionLock: nil) -// versionLock: [ -// "AccessToken": [0xa8c475e874494bb1, 0x79486e93449f0b3d, 0xa7dc4a0003541edb, 0x94183fae7580ef72], -// "Server": [0x39c64a826739077e, 0xa7ac63744fd7df32, 0xef3c9d4fe638fbfb, 0xdabd796256df14db], -// "User": [0x845de08a74bc53ed, 0xe95a406a29f3a5d0, 0x9eda732821a15ea9, 0xb5afa531e41ce8a] -// ]) + versionLock: [ + "AccessToken": [0xa8c475e874494bb1, 0x79486e93449f0b3d, 0xa7dc4a0003541edb, 0x94183fae7580ef72], + "Server": [0x936b46acd8e8f0e3, 0x59890d4d9f3f885f, 0x819cf7a4abf98b22, 0xe16125c5af885a06], + "User": [0x845de08a74bc53ed, 0xe95a406a29f3a5d0, 0x9eda732821a15ea9, 0xb5afa531e41ce8a] + ]) let _dataStack = DataStack(schema) try! _dataStack.addStorageAndWait( From 86cd963d4b2b47d0c716decda8c7a860af45c3b7 Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Mon, 8 Nov 2021 11:47:45 -0700 Subject: [PATCH 5/8] Fix tvOS build --- JellyfinPlayer tvOS/Views/ServerDetailView.swift | 15 +-------------- JellyfinPlayer tvOS/Views/ServerListView.swift | 2 +- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/JellyfinPlayer tvOS/Views/ServerDetailView.swift b/JellyfinPlayer tvOS/Views/ServerDetailView.swift index e876b003..d15219b1 100644 --- a/JellyfinPlayer tvOS/Views/ServerDetailView.swift +++ b/JellyfinPlayer tvOS/Views/ServerDetailView.swift @@ -11,7 +11,7 @@ import SwiftUI struct ServerDetailView: View { - @ObservedObject var viewModel = ServerDetailViewModel() + @ObservedObject var viewModel: ServerDetailViewModel var body: some View { Form { @@ -44,19 +44,6 @@ struct ServerDetailView: View { .foregroundColor(.secondary) } } - - Button(action: { - viewModel.refreshServerLibrary() - }, label: { - HStack { - Text("Refresh Library") - .font(.callout) - Spacer() - if viewModel.isLoading { - ProgressView() - } - } - }).disabled(viewModel.isLoading) } } } diff --git a/JellyfinPlayer tvOS/Views/ServerListView.swift b/JellyfinPlayer tvOS/Views/ServerListView.swift index 5ca66b3f..e89388e8 100644 --- a/JellyfinPlayer tvOS/Views/ServerListView.swift +++ b/JellyfinPlayer tvOS/Views/ServerListView.swift @@ -33,7 +33,7 @@ struct ServerListView: View { .font(.title2) .foregroundColor(.primary) - Text(server.uri) + Text(server.currentURI) .font(.footnote) .disabled(true) .foregroundColor(.secondary) From 43f03c1c9c50c04218b86bcbd7c306789ce040be Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Wed, 10 Nov 2021 23:03:58 -0700 Subject: [PATCH 6/8] cleanup --- JellyfinPlayer tvOS/Views/SettingsView.swift | 2 +- JellyfinPlayer/Views/ServerDetailView.swift | 2 +- JellyfinPlayer/Views/UserListView.swift | 2 +- .../MainCoordinator/iOSMainCoordinator.swift | 4 ++-- .../ServerDetailCoordinator.swift | 10 ++++----- Shared/Singleton/SessionManager.swift | 4 ++-- Shared/SwiftfinStore/SwiftfinStore.swift | 4 ++-- .../ViewModels/ConnectToServerViewModel.swift | 2 +- Shared/ViewModels/HomeViewModel.swift | 22 +++++++++---------- Shared/ViewModels/ServerDetailViewModel.swift | 8 +++---- 10 files changed, 30 insertions(+), 30 deletions(-) diff --git a/JellyfinPlayer tvOS/Views/SettingsView.swift b/JellyfinPlayer tvOS/Views/SettingsView.swift index 21c27444..dcc93767 100644 --- a/JellyfinPlayer tvOS/Views/SettingsView.swift +++ b/JellyfinPlayer tvOS/Views/SettingsView.swift @@ -11,7 +11,7 @@ import Defaults import JellyfinAPI struct SettingsView: View { - + @ObservedObject var viewModel: SettingsViewModel @Default(.inNetworkBandwidth) var inNetworkStreamBitrate diff --git a/JellyfinPlayer/Views/ServerDetailView.swift b/JellyfinPlayer/Views/ServerDetailView.swift index 0bf68786..ac13d54b 100644 --- a/JellyfinPlayer/Views/ServerDetailView.swift +++ b/JellyfinPlayer/Views/ServerDetailView.swift @@ -13,7 +13,7 @@ struct ServerDetailView: View { @ObservedObject var viewModel: ServerDetailViewModel @State var currentServerURI: String - + init(viewModel: ServerDetailViewModel) { self.viewModel = viewModel self.currentServerURI = viewModel.server.currentURI diff --git a/JellyfinPlayer/Views/UserListView.swift b/JellyfinPlayer/Views/UserListView.swift index f3122276..cf32deb9 100644 --- a/JellyfinPlayer/Views/UserListView.swift +++ b/JellyfinPlayer/Views/UserListView.swift @@ -96,7 +96,7 @@ struct UserListView: View { } label: { Image(systemName: "info.circle.fill") } - + if !viewModel.users.isEmpty { Button { userListRouter.route(to: \.userSignIn, viewModel.server) diff --git a/Shared/Coordinators/MainCoordinator/iOSMainCoordinator.swift b/Shared/Coordinators/MainCoordinator/iOSMainCoordinator.swift index 3172a5b0..067d16f9 100644 --- a/Shared/Coordinators/MainCoordinator/iOSMainCoordinator.swift +++ b/Shared/Coordinators/MainCoordinator/iOSMainCoordinator.swift @@ -69,7 +69,7 @@ final class MainCoordinator: NavigationCoordinatable { } } } - + @objc func didChangeServerCurrentURI(_ notification: Notification) { guard let newCurrentServerState = notification.object as? SwiftfinStore.State.Server else { fatalError("Need to have new current login state server") } guard SessionManager.main.currentLogin != nil else { return } @@ -77,7 +77,7 @@ final class MainCoordinator: NavigationCoordinatable { SessionManager.main.loginUser(server: newCurrentServerState, user: SessionManager.main.currentLogin.user) } } - + func makeMainTab() -> MainTabCoordinator { MainTabCoordinator() } diff --git a/Shared/Coordinators/ServerDetailCoordinator.swift b/Shared/Coordinators/ServerDetailCoordinator.swift index ec8ba9b5..8b50efe1 100644 --- a/Shared/Coordinators/ServerDetailCoordinator.swift +++ b/Shared/Coordinators/ServerDetailCoordinator.swift @@ -12,17 +12,17 @@ import Stinsen import SwiftUI final class ServerDetailCoordinator: NavigationCoordinatable { - + let stack = NavigationStack(initial: \ServerDetailCoordinator.start) - + @Root var start = makeStart - + let viewModel: ServerDetailViewModel - + init(viewModel: ServerDetailViewModel) { self.viewModel = viewModel } - + @ViewBuilder func makeStart() -> some View { ServerDetailView(viewModel: viewModel) } diff --git a/Shared/Singleton/SessionManager.swift b/Shared/Singleton/SessionManager.swift index 8666860b..cff2222b 100644 --- a/Shared/Singleton/SessionManager.swift +++ b/Shared/Singleton/SessionManager.swift @@ -120,7 +120,7 @@ final class SessionManager { return (editServer, transaction) } - .handleEvents(receiveOutput: { (server, transaction) in + .handleEvents(receiveOutput: { (_, transaction) in try? transaction.commitAndWait() }) .map({ (server, _) in @@ -149,7 +149,7 @@ final class SessionManager { return (editServer, transaction) } - .handleEvents(receiveOutput: { (server, transaction) in + .handleEvents(receiveOutput: { (_, transaction) in try? transaction.commitAndWait() }) .map({ (server, _) in diff --git a/Shared/SwiftfinStore/SwiftfinStore.swift b/Shared/SwiftfinStore/SwiftfinStore.swift index db7fb87c..14411370 100644 --- a/Shared/SwiftfinStore/SwiftfinStore.swift +++ b/Shared/SwiftfinStore/SwiftfinStore.swift @@ -180,9 +180,9 @@ extension SwiftfinStore.Errors: LocalizedError { var title: String { switch self { - case .existingServer(_): + case .existingServer: return "Existing Server" - case .existingUser(_): + case .existingUser: return "Existing User" } } diff --git a/Shared/ViewModels/ConnectToServerViewModel.swift b/Shared/ViewModels/ConnectToServerViewModel.swift index dcfa2c6f..b28581a8 100644 --- a/Shared/ViewModels/ConnectToServerViewModel.swift +++ b/Shared/ViewModels/ConnectToServerViewModel.swift @@ -106,7 +106,7 @@ final class ConnectToServerViewModel: ViewModel { .sink { completion in self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "connectToServer", completion: completion) - } receiveValue: { server in + } receiveValue: { _ in self.router?.dismissCoordinator() } .store(in: &self.cancellables) diff --git a/Shared/ViewModels/HomeViewModel.swift b/Shared/ViewModels/HomeViewModel.swift index 1101ba8f..df73d4b6 100644 --- a/Shared/ViewModels/HomeViewModel.swift +++ b/Shared/ViewModels/HomeViewModel.swift @@ -25,7 +25,7 @@ final class HomeViewModel: ViewModel { override init() { super.init() refresh() - + // Nov. 6, 2021 // This is a workaround since Stinsen doesn't have the ability to rebuild a root at the time of writing. // See ServerDetailViewModel.swift for feature request issue @@ -33,36 +33,36 @@ final class HomeViewModel: ViewModel { nc.addObserver(self, selector: #selector(didSignIn), name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil) nc.addObserver(self, selector: #selector(didSignOut), name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil) } - + @objc func didSignIn() { for cancellable in cancellables { cancellable.cancel() } - + librariesShowRecentlyAddedIDs = [] libraries = [] resumeItems = [] nextUpItems = [] - + refresh() } - + @objc func didSignOut() { for cancellable in cancellables { cancellable.cancel() } - + cancellables.removeAll() } - func refresh() { + func refresh() { LogManager.shared.log.debug("Refresh called.") UserViewsAPI.getUserViews(userId: SessionManager.main.currentLogin.user.id) .trackActivity(loading) .sink(receiveCompletion: { completion in switch completion { case .finished: () - case .failure(_): + case .failure: self.libraries = [] self.handleAPIRequestError(completion: completion) } @@ -82,7 +82,7 @@ final class HomeViewModel: ViewModel { .sink(receiveCompletion: { completion in switch completion { case .finished: () - case .failure(_): + case .failure: self.libraries = [] self.handleAPIRequestError(completion: completion) } @@ -110,7 +110,7 @@ final class HomeViewModel: ViewModel { .sink(receiveCompletion: { completion in switch completion { case .finished: () - case .failure(_): + case .failure: self.resumeItems = [] self.handleAPIRequestError(completion: completion) } @@ -127,7 +127,7 @@ final class HomeViewModel: ViewModel { .sink(receiveCompletion: { completion in switch completion { case .finished: () - case .failure(_): + case .failure: self.nextUpItems = [] self.handleAPIRequestError(completion: completion) } diff --git a/Shared/ViewModels/ServerDetailViewModel.swift b/Shared/ViewModels/ServerDetailViewModel.swift index 5c70880d..fd71a6ee 100644 --- a/Shared/ViewModels/ServerDetailViewModel.swift +++ b/Shared/ViewModels/ServerDetailViewModel.swift @@ -11,20 +11,20 @@ import Foundation import JellyfinAPI class ServerDetailViewModel: ViewModel { - + @Published var server: SwiftfinStore.State.Server - + init(server: SwiftfinStore.State.Server) { self.server = server } - + func setServerCurrentURI(uri: String) { SessionManager.main.setServerCurrentURI(server: server, uri: uri) .sink { c in print(c) } receiveValue: { newServerState in self.server = newServerState - + let nc = SwiftfinNotificationCenter.main nc.post(name: SwiftfinNotificationCenter.Keys.didChangeServerCurrentURI, object: newServerState) } From fca4b551184234021f3daacf381ab0dcb4851542 Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Mon, 29 Nov 2021 15:27:29 -0700 Subject: [PATCH 7/8] Fix image query from messed up merge --- .../BaseItemDtoExtensions.swift | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift index c8c7fe86..2f27801d 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift @@ -84,8 +84,12 @@ public extension BaseItemDto { } let x = UIScreen.main.nativeScale * CGFloat(maxWidth) - let urlString = - "\(SessionManager.main.currentLogin.server.currentURI)/Items/\(imageItemId)/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)" + + let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: imageItemId, + imageType: imageType, + maxWidth: Int(x), + quality: 96, + tag: imageTag).URLString return URL(string: urlString)! } @@ -98,15 +102,21 @@ public extension BaseItemDto { func getSeriesBackdropImage(maxWidth: Int) -> URL { let x = UIScreen.main.nativeScale * CGFloat(maxWidth) - let urlString = - "\(SessionManager.main.currentLogin.server.currentURI)/Items/\(parentBackdropItemId ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)" + let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: parentBackdropItemId ?? "", + imageType: .backdrop, + maxWidth: Int(x), + quality: 96, + tag: parentBackdropImageTags?.first).URLString return URL(string: urlString)! } func getSeriesPrimaryImage(maxWidth: Int) -> URL { let x = UIScreen.main.nativeScale * CGFloat(maxWidth) - let urlString = - "\(SessionManager.main.currentLogin.server.currentURI)/Items/\(seriesId ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)" + let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: seriesId ?? "", + imageType: .primary, + maxWidth: Int(x), + quality: 96, + tag: seriesPrimaryImageTag).URLString return URL(string: urlString)! } @@ -122,8 +132,11 @@ public extension BaseItemDto { let x = UIScreen.main.nativeScale * CGFloat(maxWidth) - let urlString = - "\(SessionManager.main.currentLogin.server.currentURI)/Items/\(imageItemId)/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)" + let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: imageItemId, + imageType: imageType, + maxWidth: Int(x), + quality: 96, + tag: imageTag).URLString return URL(string: urlString)! } From 9d2ae12b1699b435b82533e5dd590ef1b938ad0e Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Mon, 29 Nov 2021 15:34:16 -0700 Subject: [PATCH 8/8] Localize new text --- .../Views/ConnectToServerView.swift | 6 +++--- Shared/Generated/Strings.swift | 12 +++++++++++- Translations/en.lproj/Localizable.strings | Bin 4374 -> 4714 bytes 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/JellyfinPlayer/Views/ConnectToServerView.swift b/JellyfinPlayer/Views/ConnectToServerView.swift index 3bb35d9c..044755e8 100644 --- a/JellyfinPlayer/Views/ConnectToServerView.swift +++ b/JellyfinPlayer/Views/ConnectToServerView.swift @@ -107,9 +107,9 @@ struct ConnectToServerView: View { dismissButton: .cancel()) } .alert(item: $viewModel.addServerURIPayload) { _ in - Alert(title: Text("Existing Server"), - message: Text("Server \(viewModel.addServerURIPayload?.server.name ?? "") already exists. Add new URL?"), - primaryButton: .default(Text("Add URL"), action: { + Alert(title: L10n.existingServer.text, + message: L10n.serverAlreadyExistsPrompt(viewModel.addServerURIPayload?.server.name ?? "").text, + primaryButton: .default(L10n.addURL.text, action: { viewModel.addURIToServer(addServerURIPayload: viewModel.backAddServerURIPayload!) }), secondaryButton: .cancel()) diff --git a/Shared/Generated/Strings.swift b/Shared/Generated/Strings.swift index 792f5bd1..6e6ea374 100644 --- a/Shared/Generated/Strings.swift +++ b/Shared/Generated/Strings.swift @@ -12,6 +12,8 @@ import Foundation internal enum L10n { /// Accessibility internal static let accessibility = L10n.tr("Localizable", "accessibility") + /// Add URL + internal static let addURL = L10n.tr("Localizable", "addURL") /// All Genres internal static let allGenres = L10n.tr("Localizable", "allGenres") /// All Media @@ -56,6 +58,8 @@ internal enum L10n { internal static let episodes = L10n.tr("Localizable", "episodes") /// Error internal static let error = L10n.tr("Localizable", "error") + /// Existing Server + internal static let existingServer = L10n.tr("Localizable", "existingServer") /// Filter Results internal static let filterResults = L10n.tr("Localizable", "filterResults") /// Filters @@ -114,7 +118,7 @@ internal enum L10n { internal static let playNext = L10n.tr("Localizable", "playNext") /// Reset internal static let reset = L10n.tr("Localizable", "reset") - /// Search... + /// Search… internal static let search = L10n.tr("Localizable", "search") /// S%1$@:E%2$@ internal static func seasonAndEpisode(_ p1: Any, _ p2: Any) -> String { @@ -126,6 +130,10 @@ internal enum L10n { internal static let seeAll = L10n.tr("Localizable", "seeAll") /// Select Cast Destination internal static let selectCastDestination = L10n.tr("Localizable", "selectCastDestination") + /// Server %s already exists. Add new URL? + internal static func serverAlreadyExistsPrompt(_ p1: UnsafePointer) -> String { + return L10n.tr("Localizable", "serverAlreadyExistsPrompt", p1) + } /// Server Information internal static let serverInformation = L10n.tr("Localizable", "serverInformation") /// Server URL @@ -150,6 +158,8 @@ internal enum L10n { internal static let tags = L10n.tr("Localizable", "tags") /// Try again internal static let tryAgain = L10n.tr("Localizable", "tryAgain") + /// Unknown Error + internal static let unknownError = L10n.tr("Localizable", "unknownError") /// Username internal static let username = L10n.tr("Localizable", "username") /// Who's watching? diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index c7cfec2aafa85c549bf5abf02df8d20b2bcb351b..a22e6cfc0557333da07933dee274ee9d36e13f32 100644 GIT binary patch delta 316 zcmY+9Jqp4=5QQJu#mXYq%b=~jh)9Zb7XFX}2pdGyD7$L3@(=;f;gxJXg)fOqvMe*R z?7VN@>`T6bcfU6dAV_h?4i*>Y3|p-EVsNXnKw-)d#J;LncEV>#U&-tlC*;gANyEEg z$B~JrP@ZOjfJf-wct$voPqBf>WR2m+n}