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