diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index a4d9c384..f335a46e 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -97,6 +97,8 @@ 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 */; }; + 62EC353526766B03000E9F2D /* DeviceRotationViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */; }; AE8C3159265D6F90008AA076 /* bitrates.json in Resources */ = {isa = PBXBuildFile; fileRef = AE8C3158265D6F90008AA076 /* bitrates.json */; }; /* End PBXBuildFile section */ @@ -219,6 +221,7 @@ 628B953B2670D1FC0091AF3B /* WidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetExtension.entitlements; 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 = ""; }; AE8C3158265D6F90008AA076 /* bitrates.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = bitrates.json; sourceTree = ""; }; /* End PBXFileReference section */ @@ -402,6 +405,7 @@ 621338922660107500A81A2A /* StringExtensions.swift */, 6267B3D526710B8900A7371D /* CollectionExtensions.swift */, 6267B3D92671138200A7371D /* ImageExtensions.swift */, + 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */, ); path = Extensions; sourceTree = ""; @@ -609,6 +613,7 @@ 5321753F2671DEA6005491E6 /* SettingsViewModel.swift in Sources */, 535870AA2669D8AE00D05A09 /* BlurHashDecode.swift in Sources */, 535870652669D21600D05A09 /* ContentView.swift in Sources */, + 62EC353526766B03000E9F2D /* DeviceRotationViewModifier.swift in Sources */, 535870A62669D8AE00D05A09 /* LazyView.swift in Sources */, 53C4404F266C75C70049424C /* HandleAPIRequestCompletion.swift in Sources */, 5358706F2669D21700D05A09 /* JellyfinPlayer_tvOS.xcdatamodeld in Sources */, @@ -653,6 +658,7 @@ 62EC352C26766675000E9F2D /* ServerEnvironment.swift in Sources */, 6267B3DA2671138200A7371D /* ImageExtensions.swift in Sources */, 5377CBF7263B596A003A4E83 /* ContentView.swift in Sources */, + 62EC353426766B03000E9F2D /* DeviceRotationViewModifier.swift in Sources */, 5389277C263CC3DB0035E14B /* BlurHashDecode.swift in Sources */, 539B2DA5263BA5B8007FF1A4 /* SettingsView.swift in Sources */, 5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */, diff --git a/JellyfinPlayer/ConnectToServerView.swift b/JellyfinPlayer/ConnectToServerView.swift index 5bfd9064..5e3250f3 100644 --- a/JellyfinPlayer/ConnectToServerView.swift +++ b/JellyfinPlayer/ConnectToServerView.swift @@ -12,7 +12,6 @@ import JellyfinAPI struct ConnectToServerView: View { @Environment(\.managedObjectContext) private var viewContext - @EnvironmentObject var globalData: GlobalData @EnvironmentObject var jsi: justSignedIn @State private var uri = "" diff --git a/JellyfinPlayer/ContentView.swift b/JellyfinPlayer/ContentView.swift index 2d73f1f5..a3dee434 100644 --- a/JellyfinPlayer/ContentView.swift +++ b/JellyfinPlayer/ContentView.swift @@ -17,8 +17,6 @@ struct ContentView: View { @EnvironmentObject var orientationInfo: OrientationInfo @EnvironmentObject var jsi: justSignedIn - @StateObject private var globalData = GlobalData() - @State private var needsToSelectServer = false @State private var isLoading = false @State private var tabSelection: String = "Home" diff --git a/JellyfinPlayer/LibraryListView.swift b/JellyfinPlayer/LibraryListView.swift index 70574c4e..223d1d01 100644 --- a/JellyfinPlayer/LibraryListView.swift +++ b/JellyfinPlayer/LibraryListView.swift @@ -9,7 +9,6 @@ import Foundation import SwiftUI struct LibraryListView: View { - @EnvironmentObject var globalData: GlobalData @State var library_ids: [String] = ["favorites", "genres"] @State var library_names: [String: String] = ["favorites": "Favorites", "genres": "Genres"] diff --git a/JellyfinPlayer/NextUpView.swift b/JellyfinPlayer/NextUpView.swift index f0fd9400..1a9b0804 100644 --- a/JellyfinPlayer/NextUpView.swift +++ b/JellyfinPlayer/NextUpView.swift @@ -9,7 +9,6 @@ import SwiftUI import JellyfinAPI struct NextUpView: View { - @EnvironmentObject var globalData: GlobalData @State private var items: [BaseItemDto] = [] @State private var viewDidLoad: Bool = false diff --git a/JellyfinPlayer/SeriesItemView.swift b/JellyfinPlayer/SeriesItemView.swift index f84d97b0..b6895105 100644 --- a/JellyfinPlayer/SeriesItemView.swift +++ b/JellyfinPlayer/SeriesItemView.swift @@ -9,7 +9,6 @@ import SwiftUI import JellyfinAPI struct SeriesItemView: View { - @EnvironmentObject private var globalData: GlobalData @EnvironmentObject private var orientationInfo: OrientationInfo var item: BaseItemDto diff --git a/JellyfinPlayer/SettingsView.swift b/JellyfinPlayer/SettingsView.swift index c38d2c49..1d65a8a8 100644 --- a/JellyfinPlayer/SettingsView.swift +++ b/JellyfinPlayer/SettingsView.swift @@ -11,7 +11,6 @@ import SwiftUI struct SettingsView: View { @Environment(\.managedObjectContext) private var viewContext - @EnvironmentObject var globalData: GlobalData @EnvironmentObject var jsi: justSignedIn @ObservedObject var viewModel: SettingsViewModel diff --git a/JellyfinPlayer/VideoPlayer.swift b/JellyfinPlayer/VideoPlayer.swift index 979f7aec..0fc42d12 100644 --- a/JellyfinPlayer/VideoPlayer.swift +++ b/JellyfinPlayer/VideoPlayer.swift @@ -39,7 +39,6 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe weak var delegate: PlayerViewControllerDelegate? var mediaPlayer = VLCMediaPlayer() - var globalData = GlobalData() @IBOutlet weak var timeText: UILabel! @IBOutlet weak var videoContentView: UIView! diff --git a/Shared/Extensions/DeviceRotationViewModifier.swift b/Shared/Extensions/DeviceRotationViewModifier.swift new file mode 100644 index 00000000..9543501c --- /dev/null +++ b/Shared/Extensions/DeviceRotationViewModifier.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 + */ + +// https://www.hackingwithswift.com/quick-start/swiftui/how-to-detect-device-rotation +import Foundation +import SwiftUI + +// Our custom view modifier to track rotation and +// call our action +struct DeviceRotationViewModifier: ViewModifier { + let action: (UIDeviceOrientation) -> Void + + func body(content: Content) -> some View { + content + .onAppear() + .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in + action(UIDevice.current.orientation) + } + } +} + +// A View wrapper to make the modifier easier to use +extension View { + func onRotate(perform action: @escaping (UIDeviceOrientation) -> Void) -> some View { + self.modifier(DeviceRotationViewModifier(action: action)) + } +} diff --git a/Shared/Shared/ServerEnvironment.swift b/Shared/Shared/ServerEnvironment.swift index 71e6a9f8..d6f0943a 100644 --- a/Shared/Shared/ServerEnvironment.swift +++ b/Shared/Shared/ServerEnvironment.swift @@ -1,23 +1,59 @@ // - /* - * 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 - */ +/* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ -import Foundation +import Combine import CoreData +import Foundation +import JellyfinAPI final class ServerEnvironment { - static let shared = ServerEnvironment() - var server: Server? - + fileprivate(set) var server: Server! + init() { let serverRequest = NSFetchRequest(entityName: "Server") let servers = try? PersistenceController.shared.container.viewContext.fetch(serverRequest) as? [Server] server = servers?.first + guard let baseURI = server?.baseURI else { return } + JellyfinAPI.basePath = baseURI + } + + func setUp(with uri: String) -> AnyPublisher { + var uri = uri + if !uri.contains("http") { + uri = "https://" + uri + } + if uri.last == "/" { + uri = String(uri.dropLast()) + } + 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 + return server + } + .handleEvents(receiveOutput: { [unowned self] response in + server = response + _ = try? PersistenceController.shared.container.viewContext.save() + }).eraseToAnyPublisher() + } + + func reset() throws { + JellyfinAPI.basePath = "" + server = nil + + let serverRequest: NSFetchRequest = NSFetchRequest(entityName: "Server") + let deleteRequest = NSBatchDeleteRequest(fetchRequest: serverRequest) + + try PersistenceController.shared.container.viewContext.execute(deleteRequest) } } diff --git a/Shared/Shared/SessionManager.swift b/Shared/Shared/SessionManager.swift index 3f4d2677..e259ab12 100644 --- a/Shared/Shared/SessionManager.swift +++ b/Shared/Shared/SessionManager.swift @@ -1,35 +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 - */ +/* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ -import Foundation +import Combine import CoreData +import Foundation +import JellyfinAPI import KeychainSwift import UIKit final class SessionManager { - static let shared = SessionManager() - var user: SignedInUser? - var authHeader: String? - + fileprivate(set) var user: SignedInUser! + fileprivate(set) var authHeader: String! + fileprivate(set) var deviceIDString: String + init() { let savedUserRequest = NSFetchRequest(entityName: "SignedInUser") let savedUsers = try? PersistenceController.shared.container.viewContext.fetch(savedUserRequest) as? [SignedInUser] user = savedUsers?.first - + let keychain = KeychainSwift() - // need prefix keychain.accessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain" + if let deviceID = keychain.get("DeviceIDString") { + self.deviceIDString = deviceID + } else { + self.deviceIDString = UUID().uuidString + keychain.set(deviceIDString, forKey: "DeviceIDString") + } + guard let authToken = keychain.get("AccessToken_\(user?.user_id ?? "")") else { return } + updateHeader(with: authToken) + } + + func updateHeader(with authToken: String?) { let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String var deviceName = UIDevice.current.name deviceName = deviceName.folding(options: .diacriticInsensitive, locale: .current) @@ -38,10 +50,54 @@ final class SessionManager { var header = "MediaBrowser " header.append("Client=\"SwiftFin\", ") header.append("Device=\"\(deviceName)\", ") - header.append("DeviceId=\"\(user?.device_uuid ?? "")\", ") + header.append("DeviceId=\"\(deviceIDString)\", ") header.append("Version=\"\(appVersion ?? "0.0.1")\", ") - header.append("Token=\"\(authToken)\"") + if let token = authToken { + header.append("Token=\"\(token)\"") + } - self.authHeader = header + authHeader = header + JellyfinAPI.customHeaders["X-Emby-Authorization"] = authHeader + } + + func login(username: String, password: String) -> AnyPublisher { + updateHeader(with: nil) + + return UserAPI.authenticateUserByName(authenticateUserByName: AuthenticateUserByName(username: username, pw: password)) + .map { [unowned self] response -> (SignedInUser, String?) in + let user = SignedInUser(context: PersistenceController.shared.container.viewContext) + user.device_uuid = deviceIDString + user.username = response.user?.name + user.user_id = response.user?.id + return (user, response.accessToken) + } + .handleEvents(receiveOutput: { [unowned self] response, accessToken in + user = response + _ = try? PersistenceController.shared.container.viewContext.save() + if let userID = user.user_id, + let token = accessToken + { + let keychain = KeychainSwift() + keychain.accessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain" + keychain.set(token, forKey: "AccessToken_\(userID)") + } + updateHeader(with: accessToken) + }) + .map(\.0) + .eraseToAnyPublisher() + } + + func logout() throws { + let keychain = KeychainSwift() + keychain.accessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain" + keychain.delete("AccessToken_\(user.user_id ?? "")") + JellyfinAPI.customHeaders["X-Emby-Authorization"] = nil + user = nil + authHeader = nil + + let userRequest: NSFetchRequest = NSFetchRequest(entityName: "SignedInUser") + let deleteRequest = NSBatchDeleteRequest(fetchRequest: userRequest) + + try PersistenceController.shared.container.viewContext.execute(deleteRequest) } } diff --git a/Shared/Typings/Typings.swift b/Shared/Typings/Typings.swift index 915415e8..a3270db8 100644 --- a/Shared/Typings/Typings.swift +++ b/Shared/Typings/Typings.swift @@ -26,24 +26,3 @@ public enum SortBy: String, Codable, CaseIterable { class justSignedIn: ObservableObject { @Published var did: Bool = false } - -class GlobalData: ObservableObject { - @Published var user: SignedInUser! - @Published var authToken: String = "" - @Published var server: Server! - @Published var authHeader: String = "" - @Published var isInNetwork: Bool = true - @Published var networkError: Bool = false - @Published var expiredCredentials: Bool = false - var pendingAPIRequests = Set() -} - -extension GlobalData: Equatable { - - static func == (lhs: GlobalData, rhs: GlobalData) -> Bool { - lhs.user == rhs.user - && lhs.authToken == rhs.authToken - && lhs.server == rhs.server - && lhs.authHeader == rhs.authHeader - } -}