diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 8f10ab6e..e34b32f8 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -42,7 +42,6 @@ 5364F455266CA0DC0026ECBA /* APIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* APIExtensions.swift */; }; 5364F456266CA0DC0026ECBA /* APIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* APIExtensions.swift */; }; 5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBF4263B596A003A4E83 /* JellyfinPlayerApp.swift */; }; - 5377CBF7263B596A003A4E83 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBF6263B596A003A4E83 /* ContentView.swift */; }; 5377CBF9263B596B003A4E83 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5377CBF8263B596B003A4E83 /* Assets.xcassets */; }; 5377CBFC263B596B003A4E83 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5377CBFB263B596B003A4E83 /* Preview Assets.xcassets */; }; 5377CBFE263B596B003A4E83 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFD263B596B003A4E83 /* PersistenceController.swift */; }; @@ -70,6 +69,15 @@ 621C638026672A30004216EA /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 621C637F26672A30004216EA /* NukeUI */; }; 6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */; }; 6228B1C22670EB010067FD35 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFD263B596B003A4E83 /* PersistenceController.swift */; }; + 625CB5682678B6FB00530A6E /* SplashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5672678B6FB00530A6E /* SplashView.swift */; }; + 625CB56A2678B71200530A6E /* SplashViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5692678B71200530A6E /* SplashViewModel.swift */; }; + 625CB56C2678C0FD00530A6E /* MainTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB56B2678C0FD00530A6E /* MainTabView.swift */; }; + 625CB56F2678C23300530A6E /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB56E2678C23300530A6E /* HomeView.swift */; }; + 625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5722678C32A00530A6E /* HomeViewModel.swift */; }; + 625CB5752678C33500530A6E /* LibraryListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5742678C33500530A6E /* LibraryListViewModel.swift */; }; + 625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */; }; + 625CB57A2678C4A400530A6E /* ActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 625CB5792678C4A400530A6E /* ActivityIndicator */; }; + 625CB57C2678CE1000530A6E /* ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB57B2678CE1000530A6E /* ViewModel.swift */; }; 6267B3D42671024A00A7371D /* APIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* APIExtensions.swift */; }; 6267B3D626710B8900A7371D /* CollectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6267B3D526710B8900A7371D /* CollectionExtensions.swift */; }; 6267B3D726710B9700A7371D /* CollectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6267B3D526710B8900A7371D /* CollectionExtensions.swift */; }; @@ -177,7 +185,6 @@ 5364F454266CA0DC0026ECBA /* APIExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIExtensions.swift; sourceTree = ""; }; 5377CBF1263B596A003A4E83 /* JellyfinPlayer iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "JellyfinPlayer iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 5377CBF4263B596A003A4E83 /* JellyfinPlayerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinPlayerApp.swift; sourceTree = ""; }; - 5377CBF6263B596A003A4E83 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 5377CBF8263B596B003A4E83 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 5377CBFB263B596B003A4E83 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 5377CBFD263B596B003A4E83 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = ""; }; @@ -206,6 +213,14 @@ 621338922660107500A81A2A /* StringExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtensions.swift; sourceTree = ""; }; 621338B22660A07800A81A2A /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = ""; }; 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParallaxHeader.swift; sourceTree = ""; }; + 625CB5672678B6FB00530A6E /* SplashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashView.swift; sourceTree = ""; }; + 625CB5692678B71200530A6E /* SplashViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashViewModel.swift; sourceTree = ""; }; + 625CB56B2678C0FD00530A6E /* MainTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabView.swift; sourceTree = ""; }; + 625CB56E2678C23300530A6E /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; + 625CB5722678C32A00530A6E /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = ""; }; + 625CB5742678C33500530A6E /* LibraryListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListViewModel.swift; sourceTree = ""; }; + 625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectToServerViewModel.swift; sourceTree = ""; }; + 625CB57B2678CE1000530A6E /* ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModel.swift; sourceTree = ""; }; 6267B3D526710B8900A7371D /* CollectionExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionExtensions.swift; sourceTree = ""; }; 6267B3D92671138200A7371D /* ImageExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageExtensions.swift; sourceTree = ""; }; 628B95202670CABD0091AF3B /* WidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -242,6 +257,7 @@ 5338F757263B7E2E0014BF09 /* KeychainSwift in Frameworks */, 53352571265EA0A0006CCA86 /* Introspect in Frameworks */, 621C638026672A30004216EA /* NukeUI in Frameworks */, + 625CB57A2678C4A400530A6E /* ActivityIndicator in Frameworks */, 53A431BD266B0FF20016769F /* JellyfinAPI in Frameworks */, 62EC3527267665D8000E9F2D /* MobileVLCKit.xcframework in Frameworks */, ); @@ -266,6 +282,11 @@ isa = PBXGroup; children = ( 5321753A2671BCFC005491E6 /* SettingsViewModel.swift */, + 625CB5692678B71200530A6E /* SplashViewModel.swift */, + 625CB5722678C32A00530A6E /* HomeViewModel.swift */, + 625CB5742678C33500530A6E /* LibraryListViewModel.swift */, + 625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */, + 625CB57B2678CE1000530A6E /* ViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -338,10 +359,10 @@ 5377CBF3263B596A003A4E83 /* JellyfinPlayer */ = { isa = PBXGroup; children = ( + 625CB56D2678C1C400530A6E /* ViewModels */, 53AD124C2670278D0094A276 /* JellyfinPlayer.entitlements */, 5377CBF8263B596B003A4E83 /* Assets.xcassets */, 5338F74D263B61370014BF09 /* ConnectToServerView.swift */, - 5377CBF6263B596A003A4E83 /* ContentView.swift */, 5389276D263C25100035E14B /* ContinueWatchingView.swift */, 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */, 53987CA72657424A00E7EA70 /* EpisodeItemView.swift */, @@ -366,6 +387,9 @@ 53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */, 53DE4BD1267098F300739748 /* SearchBarView.swift */, 531AC8BE26750DE20091C7EB /* ImageView.swift */, + 625CB5672678B6FB00530A6E /* SplashView.swift */, + 625CB56B2678C0FD00530A6E /* MainTabView.swift */, + 625CB56E2678C23300530A6E /* HomeView.swift */, ); path = JellyfinPlayer; sourceTree = ""; @@ -406,6 +430,13 @@ path = Extensions; sourceTree = ""; }; + 625CB56D2678C1C400530A6E /* ViewModels */ = { + isa = PBXGroup; + children = ( + ); + path = ViewModels; + sourceTree = ""; + }; 628B95252670CABD0091AF3B /* WidgetExtension */ = { isa = PBXGroup; children = ( @@ -485,6 +516,7 @@ 53352570265EA0A0006CCA86 /* Introspect */, 621C637F26672A30004216EA /* NukeUI */, 53A431BC266B0FF20016769F /* JellyfinAPI */, + 625CB5792678C4A400530A6E /* ActivityIndicator */, ); productName = JellyfinPlayer; productReference = 5377CBF1263B596A003A4E83 /* JellyfinPlayer iOS.app */; @@ -549,6 +581,7 @@ 5335256F265EA0A0006CCA86 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */, 621C637E26672A30004216EA /* XCRemoteSwiftPackageReference "NukeUI" */, 53A431BB266B0FF20016769F /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */, + 625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */, ); productRefGroup = 5377CBF2263B596A003A4E83 /* Products */; projectDirPath = ""; @@ -626,16 +659,20 @@ 5364F455266CA0DC0026ECBA /* APIExtensions.swift in Sources */, 621338932660107500A81A2A /* StringExtensions.swift in Sources */, 53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */, + 625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */, 5377CBFE263B596B003A4E83 /* PersistenceController.swift in Sources */, 5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */, 53AD124E26702B8A0094A276 /* SeasonItemView.swift in Sources */, 535BAE9F2649E569005FA86D /* ItemView.swift in Sources */, 6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */, + 625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */, 53DE4BD02670961400739748 /* EpisodeItemView.swift in Sources */, 53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */, 53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */, 62133890265F83A900A81A2A /* LibraryListView.swift in Sources */, + 625CB56F2678C23300530A6E /* HomeView.swift in Sources */, 53892770263C25230035E14B /* NextUpView.swift in Sources */, + 625CB5682678B6FB00530A6E /* SplashView.swift in Sources */, 535BAEA5264A151C005FA86D /* VideoPlayer.swift in Sources */, 5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */, 532175402671EE4F005491E6 /* LibraryFilterView.swift in Sources */, @@ -643,23 +680,26 @@ 53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */, 6267B3D626710B8900A7371D /* CollectionExtensions.swift in Sources */, 53A089D0264DA9DA00D57806 /* MovieItemView.swift in Sources */, + 625CB56A2678B71200530A6E /* SplashViewModel.swift in Sources */, 53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */, 53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */, + 625CB57C2678CE1000530A6E /* ViewModel.swift in Sources */, 621338B32660A07800A81A2A /* LazyView.swift in Sources */, 531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */, 62EC352F267666A5000E9F2D /* SessionManager.swift in Sources */, 535870AD2669D8DD00D05A09 /* Typings.swift in Sources */, 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 */, + 625CB56C2678C0FD00530A6E /* MainTabView.swift in Sources */, 539B2DA5263BA5B8007FF1A4 /* SettingsView.swift in Sources */, 5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */, 53AD124D267029D60094A276 /* SeriesItemView.swift in Sources */, 5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */, 53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */, 53892772263C8C6F0035E14B /* LoadingView.swift in Sources */, + 625CB5752678C33500530A6E /* LibraryListViewModel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1065,6 +1105,14 @@ version = 0.3.0; }; }; + 625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/duyquang91/ActivityIndicator"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.1.0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -1108,6 +1156,11 @@ package = 621C637E26672A30004216EA /* XCRemoteSwiftPackageReference "NukeUI" */; productName = NukeUI; }; + 625CB5792678C4A400530A6E /* ActivityIndicator */ = { + isa = XCSwiftPackageProductDependency; + package = 625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */; + productName = ActivityIndicator; + }; 628B95322670CAEA0091AF3B /* NukeUI */ = { isa = XCSwiftPackageProductDependency; package = 621C637E26672A30004216EA /* XCRemoteSwiftPackageReference "NukeUI" */; diff --git a/JellyfinPlayer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/JellyfinPlayer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9dacef05..cb410d09 100644 --- a/JellyfinPlayer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/JellyfinPlayer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,6 +1,15 @@ { "object": { "pins": [ + { + "package": "ActivityIndicator", + "repositoryURL": "https://github.com/duyquang91/ActivityIndicator", + "state": { + "branch": null, + "revision": "0101a02196f6a67cf26f6434b007d3db6bd07fee", + "version": "1.1.0" + } + }, { "package": "AnyCodable", "repositoryURL": "https://github.com/Flight-School/AnyCodable", diff --git a/JellyfinPlayer/ConnectToServerView.swift b/JellyfinPlayer/ConnectToServerView.swift index 5e3250f3..a6e826c5 100644 --- a/JellyfinPlayer/ConnectToServerView.swift +++ b/JellyfinPlayer/ConnectToServerView.swift @@ -5,259 +5,46 @@ * Copyright 2021 Aiden Vigue & Jellyfin Contributors */ -import SwiftUI import CoreData -import KeychainSwift import JellyfinAPI +import KeychainSwift +import SwiftUI struct ConnectToServerView: View { - @Environment(\.managedObjectContext) private var viewContext - @EnvironmentObject var jsi: justSignedIn + @StateObject + var viewModel = ConnectToServerViewModel() - @State private var uri = "" - @State private var isWorking = false - @State private var isErrored = false - @State private var isDone = false - @State private var isSignInErrored = false - @State private var isConnected = false - @State private var serverName = "" - @State private var usernameDisabled: Bool = false - @State private var publicUsers: [UserDto] = [] - @State private var lastPublicUsers: [UserDto] = [] - @State private var username = "" - @State private var password = "" - @State private var server_id = "" - @State private var serverSkipped: Bool = false - @State private var serverSkippedAlert: Bool = false - @State private var skip_server_bool: Bool = false - @State private var skip_server_obj: Server! - - @Binding var rootIsActive: Bool - - private var reauthDeviceID: String = "" - private let userUUID = UUID() - - init(skip_server: Bool, skip_server_prefill: Server, reauth_deviceId: String, isActive: Binding) { - _rootIsActive = isActive - skip_server_bool = skip_server - skip_server_obj = skip_server_prefill - reauthDeviceID = reauth_deviceId - } - - init(isActive: Binding) { - _rootIsActive = isActive - } - - func start() { - if skip_server_bool { - uri = skip_server_obj.baseURI! - - UserAPI.getPublicUsers() - .sink(receiveCompletion: { completion in - switch completion { - case .finished: - break - case .failure: - skip_server_bool = false - skip_server_obj = Server() - break - } - }, receiveValue: { response in - publicUsers = response - - serverSkipped = true - serverSkippedAlert = true - server_id = skip_server_obj.server_id! - serverName = skip_server_obj.name! - isConnected = true - }) - .store(in: &globalData.pendingAPIRequests) - } - } - - func doLogin() { - isWorking = true - - let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String - var deviceName = UIDevice.current.name - deviceName = deviceName.folding(options: .diacriticInsensitive, locale: .current) - deviceName = deviceName.removeRegexMatches(pattern: "[^\\w\\s]") - - let authHeader = "MediaBrowser Client=\"SwiftFin\", Device=\"\(deviceName)\", DeviceId=\"\(serverSkipped ? reauthDeviceID : userUUID.uuidString)\", Version=\"\(appVersion ?? "0.0.1")\"" - print(authHeader) - - JellyfinAPI.customHeaders["X-Emby-Authorization"] = authHeader - - let x: AuthenticateUserByName = AuthenticateUserByName(username: username, pw: password, password: nil) - - UserAPI.authenticateUserByName(authenticateUserByName: x) - .sink(receiveCompletion: { completion in - switch completion { - case .finished: - break - case .failure(let error): - isWorking = false - if let err = error as? ErrorResponse { - switch err { - case .error(401, _, _, _): - isSignInErrored = true - case .error: - globalData.networkError = true - } - } - break - } - }, receiveValue: { response in - let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: "Server") - let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) - - do { - try viewContext.execute(deleteRequest) - } catch _ as NSError { - - } - - let fetchRequest2: NSFetchRequest = NSFetchRequest(entityName: "SignedInUser") - let deleteRequest2 = NSBatchDeleteRequest(fetchRequest: fetchRequest2) - - do { - try viewContext.execute(deleteRequest2) - } catch _ as NSError { - - } - - let newServer = Server(context: viewContext) - newServer.baseURI = uri - newServer.name = serverName - newServer.server_id = server_id - - let newUser = SignedInUser(context: viewContext) - newUser.device_uuid = userUUID.uuidString - newUser.username = username - newUser.user_id = response.user!.id! - - let keychain = KeychainSwift() - keychain.set(response.accessToken!, forKey: "AccessToken_\(newUser.user_id!)") - - do { - try viewContext.save() - DispatchQueue.main.async { [self] in - globalData.authHeader = authHeader - _rootIsActive.wrappedValue = false - - globalData.expiredCredentials = false - globalData.networkError = false - globalData.user = newUser - globalData.server = newServer - - jsi.did = true - print("logged in") - } - } catch { - print("Couldn't store objects to CoreData") - } - }) - .store(in: &globalData.pendingAPIRequests) - } + @Binding + var isLoggedIn: Bool var body: some View { - Form { - if !isConnected { - Section(header: Text("Server Information")) { - TextField("Jellyfin Server URL", text: $uri) - .disableAutocorrection(true) - .autocapitalization(.none) - Button { - isWorking = true - if !uri.contains("http") { - uri = "https://" + uri - } - if uri.last == "/" { - uri = String(uri.dropLast()) + ZStack { + Form { + if viewModel.isConnectedServer { + if viewModel.publicUsers.isEmpty { + Section(header: Text("Login to \(ServerEnvironment.current.server.name ?? "")")) { + TextField("Username", text: $viewModel.username) + .disableAutocorrection(true) + .autocapitalization(.none) + SecureField("Password", text: $viewModel.password) + .disableAutocorrection(true) + .autocapitalization(.none) + Button { + viewModel.login() + } label: { + HStack { + Text("Login") + Spacer() + if viewModel.isLoading { + ProgressView() + } + } + }.disabled(viewModel.isLoading || viewModel.username.isEmpty) } - JellyfinAPI.basePath = uri - SystemAPI.getPublicSystemInfo() - .sink(receiveCompletion: { completion in - switch completion { - case .finished: - break - case .failure: - isErrored = true - isWorking = false - break - } - }, receiveValue: { response in - let server = response - serverName = server.serverName! - server_id = server.id! - if server.startupWizardCompleted ?? true { - isConnected = true - - UserAPI.getPublicUsers() - .sink(receiveCompletion: { completion in - switch completion { - case .finished: - break - case .failure: - isErrored = true - isWorking = false - break - } - }, receiveValue: { response in - publicUsers = response - isWorking = false - }) - .store(in: &globalData.pendingAPIRequests) - } - }) - .store(in: &globalData.pendingAPIRequests) - } label: { - HStack { - Text("Connect") - Spacer() - if isWorking == true { - ProgressView() - } - } - }.disabled(isWorking || uri.isEmpty) - }.alert(isPresented: $isErrored) { - Alert(title: Text("Error"), message: Text("Couldn't connect to server"), dismissButton: .default(Text("Try again"))) - } - } else { - if publicUsers.count == 0 { - Section(header: Text("\(serverSkipped ? "Reauthenticate" : "Login") to \(serverName)")) { - TextField("Username", text: $username) - .disableAutocorrection(true) - .autocapitalization(.none) - .disabled(usernameDisabled) - SecureField("Password", text: $password) - .disableAutocorrection(true) - .autocapitalization(.none) - Button { - doLogin() - } label: { - HStack { - Text("Login") - Spacer() - if isWorking { - ProgressView() - } - } - }.disabled(isWorking || username.isEmpty) - .alert(isPresented: $isSignInErrored) { - Alert(title: Text("Error"), message: Text("Invalid credentials"), dismissButton: .default(Text("Back"))) - } - } - - if serverSkipped { Section { Button { - serverSkippedAlert = false - server_id = "" - serverName = "" - isConnected = false - serverSkipped = false + viewModel.isConnectedServer = false } label: { HStack { HStack { @@ -269,85 +56,85 @@ struct ConnectToServerView: View { } } } else { - Section { - Button { - publicUsers = lastPublicUsers - usernameDisabled = false - } label: { + Section(header: Text("Login to \(ServerEnvironment.current.server.name ?? "")")) { + ForEach(viewModel.publicUsers, id: \.id) { publicUser in HStack { - HStack { - Image(systemName: "chevron.left") - Text("Back") - } - Spacer() - } - } - } - } - } else { - Section(header: Text("\(serverSkipped ? "Reauthenticate" : "Login") to \(serverName)")) { - ForEach(publicUsers, id: \.id) { publicUser in - HStack { - Button() { - if publicUser.hasPassword ?? true { - lastPublicUsers = publicUsers - username = publicUser.name ?? "" - usernameDisabled = true - publicUsers = [] - } else { - publicUsers = [] - password = "" - username = publicUser.name ?? "" - doLogin() - } - } label: { - HStack { - Text(publicUser.name ?? "").font(.subheadline).fontWeight(.semibold) - Spacer() - if publicUser.primaryImageTag != nil { - ImageView(src: URL(string: "\(uri)/Users/\(publicUser.id ?? "")/Images/Primary?width=200&quality=80&tag=\(publicUser.primaryImageTag!)")!) - .frame(width: 60, height: 60) - .cornerRadius(30.0) - } else { - Image(systemName: "person.fill") - .foregroundColor(Color(red: 1, green: 1, blue: 1).opacity(0.8)) - .font(.system(size: 35)) - .frame(width: 60, height: 60) - .background(Color(red: 98/255, green: 121/255, blue: 205/255)) - .cornerRadius(30.0) - .shadow(radius: 6) + Button(action: { + viewModel.username = publicUser.name ?? "" + viewModel.publicUsers.removeAll() + if !(publicUser.hasPassword ?? true) { + viewModel.password = "" + viewModel.login() + } + }) { + HStack { + Text(publicUser.name ?? "").font(.subheadline).fontWeight(.semibold) + Spacer() + if publicUser.primaryImageTag != nil { + ImageView(src: URL(string: "\(ServerEnvironment.current.server.baseURI ?? "")/Users/\(publicUser.id ?? "")/Images/Primary?width=200&quality=80&tag=\(publicUser.primaryImageTag!)")!) + .frame(width: 60, height: 60) + .cornerRadius(30.0) + } else { + Image(systemName: "person.fill") + .foregroundColor(Color(red: 1, green: 1, blue: 1).opacity(0.8)) + .font(.system(size: 35)) + .frame(width: 60, height: 60) + .background(Color(red: 98 / 255, green: 121 / 255, blue: 205 / 255)) + .cornerRadius(30.0) + .shadow(radius: 6) + } } } } } } - } - Section { - Button() { - lastPublicUsers = publicUsers - publicUsers = [] - username = "" - } label: { - HStack { - Text("Other User").font(.subheadline).fontWeight(.semibold) - Spacer() - Image(systemName: "person.fill.questionmark") - .foregroundColor(Color(red: 1, green: 1, blue: 1).opacity(0.8)) - .font(.system(size: 35)) - .frame(width: 60, height: 60) - .background(Color(red: 98/255, green: 121/255, blue: 205/255)) - .cornerRadius(30.0) - .shadow(radius: 6) + Section { + Button { + viewModel.publicUsers.removeAll() + viewModel.username = "" + } label: { + HStack { + Text("Other User").font(.subheadline).fontWeight(.semibold) + Spacer() + Image(systemName: "person.fill.questionmark") + .foregroundColor(Color(red: 1, green: 1, blue: 1).opacity(0.8)) + .font(.system(size: 35)) + .frame(width: 60, height: 60) + .background(Color(red: 98 / 255, green: 121 / 255, blue: 205 / 255)) + .cornerRadius(30.0) + .shadow(radius: 6) + } } } } + } else { + Section(header: Text("Server Information")) { + TextField("Jellyfin Server URL", text: $viewModel.uri) + .disableAutocorrection(true) + .autocapitalization(.none) + Button { + viewModel.connectToServer() + } label: { + HStack { + Text("Connect") + Spacer() + } + if viewModel.isLoading { + ProgressView() + } + } + .disabled(viewModel.isLoading || viewModel.uri.isEmpty) + } } } - }.navigationTitle("Connect to Server") - .alert(isPresented: $serverSkippedAlert) { - Alert(title: Text("Error"), message: Text("Credentials have expired"), dismissButton: .default(Text("Sign in again"))) } - .onAppear(perform: start) + .alert(item: $viewModel.errorMessage) { _ in + Alert(title: Text("Error"), message: Text("message"), dismissButton: .default(Text("Try again"))) + } + .onReceive(viewModel.$isLoggedIn, perform: { flag in + isLoggedIn = flag + }) + .navigationTitle("Connect to Server") } } diff --git a/JellyfinPlayer/ContentView.swift b/JellyfinPlayer/ContentView.swift deleted file mode 100644 index 6439b302..00000000 --- a/JellyfinPlayer/ContentView.swift +++ /dev/null @@ -1,231 +0,0 @@ -/* JellyfinPlayer/Swiftfin is subject to the terms of the Mozilla Public - * License, v2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright 2021 Aiden Vigue & Jellyfin Contributors - */ - -import SwiftUI - -import KeychainSwift -import Nuke -import JellyfinAPI -import WidgetKit - -struct ContentView: View { - @Environment(\.managedObjectContext) private var viewContext - @EnvironmentObject var jsi: justSignedIn - - @State private var orientation = UIDeviceOrientation.unknown - - @State private var needsToSelectServer = false - @State private var isLoading = false - @State private var tabSelection: String = "Home" - @State private var libraries: [String] = [] - @State private var library_names: [String: String] = [:] - @State private var librariesShowRecentlyAdded: [String] = [] - @State private var libraryPrefillID: String = "" - @State private var showSettingsPopover: Bool = false - @State private var viewDidLoad: Bool = false - @State private var loadState: Int = 2 - - @FetchRequest(entity: Server.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Server.name, ascending: true)]) - var servers: FetchedResults - - @FetchRequest(entity: SignedInUser.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \SignedInUser.username, ascending: true)]) - var savedUsers: FetchedResults - - private var recentFilterSet: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.descending], sortBy: ["DateCreated"]) - - func startup() { - if viewDidLoad == true { - return - } - - ImageCache.shared.costLimit = 125 * 1024 * 1024 // 125MB memory - DataLoader.sharedUrlCache.diskCapacity = 1000 * 1024 * 1024 // 1000MB disk - - if servers.isEmpty { - isLoading = false - needsToSelectServer = true - } else { - isLoading = true - let savedUser = savedUsers[0] - - let keychain = KeychainSwift() - keychain.accessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain" - if keychain.get("AccessToken_\(savedUser.user_id ?? "")") != nil { - globalData.authToken = keychain.get("AccessToken_\(savedUser.user_id ?? "")") ?? "" - globalData.server = servers[0] - globalData.user = savedUser - } - - let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String - var deviceName = UIDevice.current.name - deviceName = deviceName.folding(options: .diacriticInsensitive, locale: .current) - deviceName = deviceName.removeRegexMatches(pattern: "[^\\w\\s]") - - var header = "MediaBrowser " - header.append("Client=\"SwiftFin\", ") - header.append("Device=\"\(deviceName)\", ") - header.append("DeviceId=\"\(globalData.user.device_uuid ?? "")\", ") - header.append("Version=\"\(appVersion ?? "0.0.1")\", ") - header.append("Token=\"\(globalData.authToken)\"") - - globalData.authHeader = header - JellyfinAPI.basePath = ServerEnvironment.current.server.baseURI ?? "" - JellyfinAPI.customHeaders = ["X-Emby-Authorization": globalData.authHeader] - - DispatchQueue.global(qos: .userInitiated).async { - UserAPI.getCurrentUser() - .sink(receiveCompletion: { completion in - print(completion) - loadState = loadState - 1 - }, receiveValue: { response in - libraries = response.configuration?.orderedViews ?? [] - librariesShowRecentlyAdded = libraries.filter { element in - return !(response.configuration?.latestItemsExcludes?.contains(element))! - } - - if loadState == 1 { - isLoading = false - viewDidLoad = true - } - }) - .store(in: &globalData.pendingAPIRequests) - - UserViewsAPI.getUserViews(userId: SessionManager.current.userID ?? "") - .sink(receiveCompletion: { completion in - print(completion) - loadState = loadState - 1 - }, receiveValue: { response in - response.items?.forEach({ item in - library_names[item.id ?? ""] = item.name - }) - - if loadState == 1 { - isLoading = false - viewDidLoad = true - } - }) - .store(in: &globalData.pendingAPIRequests) - } - - let defaults = UserDefaults.standard - if defaults.integer(forKey: "InNetworkBandwidth") == 0 { - defaults.setValue(40_000_000, forKey: "InNetworkBandwidth") - } - if defaults.integer(forKey: "OutOfNetworkBandwidth") == 0 { - defaults.setValue(40_000_000, forKey: "OutOfNetworkBandwidth") - } - } - WidgetCenter.shared.reloadAllTimelines() - } - - var body: some View { - if needsToSelectServer == true || globalData.user == nil || globalData.server == nil { - NavigationView { - ConnectToServerView(isActive: $needsToSelectServer) - } - .navigationViewStyle(StackNavigationViewStyle()) - .environmentObject(globalData) - .onAppear(perform: startup) - } else if globalData.expiredCredentials == true { - NavigationView { - ConnectToServerView(skip_server: true, skip_server_prefill: globalData.server, - reauth_deviceId: globalData.user.device_uuid!, isActive: $globalData.expiredCredentials) - } - .navigationViewStyle(StackNavigationViewStyle()) - .environmentObject(globalData) - .onAppear(perform: startup) - } else { - if !jsi.did { - if isLoading || globalData.user == nil || SessionManager.current.userID == nil { - ProgressView() - .onAppear(perform: startup) - } else { - VStack { - TabView(selection: $tabSelection) { - NavigationView { - VStack(alignment: .leading) { - ScrollView { - Spacer().frame(height: orientation == .portrait ? 0 : 16) - ContinueWatchingView() - NextUpView() - - ForEach(librariesShowRecentlyAdded, id: \.self) { library_id in - VStack(alignment: .leading) { - HStack { - Text("Latest \(library_names[library_id] ?? "")").font(.title2).fontWeight(.bold) - .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 16)) - Spacer() - NavigationLink(destination: LazyView { - LibraryView(usingParentID: library_id, - title: library_names[library_id] ?? "", usingFilters: recentFilterSet) - }) { - HStack { - Text("See All").font(.subheadline).fontWeight(.bold) - Image(systemName: "chevron.right").font(Font.subheadline.bold()) - } - } - }.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)) - LatestMediaView(usingParentID: library_id) - }.padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0)) - } - - Spacer().frame(height: UIDevice.current.userInterfaceIdiom == .phone ? 20 : 30) - } - .navigationTitle("Home") - .toolbar { - ToolbarItemGroup(placement: .navigationBarTrailing) { - Button { - showSettingsPopover = true - } label: { - Image(systemName: "gear") - } - } - } - .fullScreenCover(isPresented: $showSettingsPopover) { - SettingsView(viewModel: SettingsViewModel(), close: $showSettingsPopover) - } - } - } - .navigationViewStyle(StackNavigationViewStyle()) - .tabItem { - Text("Home") - Image(systemName: "house") - } - .tag("Home") - NavigationView { - LibraryListView(libraries: library_names) - } - .navigationViewStyle(StackNavigationViewStyle()) - .tabItem { - Text("All Media") - Image(systemName: "folder") - } - .tag("All Media") - } - } - .environmentObject(globalData) - .onAppear(perform: startup) - .alert(isPresented: $globalData.networkError) { - Alert(title: Text("Network Error"), message: Text("An error occured while performing a network request"), dismissButton: .default(Text("Ok"))) - } - .onRotate { orientation in - self.orientation = orientation - } - } - } else { - Text("Please wait...") - .onAppear(perform: { - DispatchQueue.main.async { [self] in - _viewDidLoad.wrappedValue = false - sleep(1) - self.jsi.did = false - } - }) - } - } - } -} diff --git a/JellyfinPlayer/ContinueWatchingView.swift b/JellyfinPlayer/ContinueWatchingView.swift index 5dedff11..2882781e 100644 --- a/JellyfinPlayer/ContinueWatchingView.swift +++ b/JellyfinPlayer/ContinueWatchingView.swift @@ -32,21 +32,7 @@ struct ProgressBar: Shape { } struct ContinueWatchingView: View { - @State private var items: [BaseItemDto] = [] - - func onAppear() { - var tempCancellables = Set() - - DispatchQueue.global(qos: .userInitiated).async { - ItemsAPI.getResumeItems(userId: SessionManager.current.userID!, limit: 12, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], mediaTypes: ["Video"], imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb]) - .sink(receiveCompletion: { completion in - print(completion) - }, receiveValue: { response in - items = response.items ?? [] - }) - .store(in: &tempCancellables) - } - } + var items: [BaseItemDto] var body: some View { ScrollView(.horizontal, showsIndicators: false) { @@ -97,6 +83,6 @@ struct ContinueWatchingView: View { } else { EmptyView() } - }.onAppear(perform: onAppear) + } } } diff --git a/JellyfinPlayer/EpisodeItemView.swift b/JellyfinPlayer/EpisodeItemView.swift index b372f9bb..cf9e4a36 100644 --- a/JellyfinPlayer/EpisodeItemView.swift +++ b/JellyfinPlayer/EpisodeItemView.swift @@ -10,7 +10,11 @@ import JellyfinAPI import Combine struct EpisodeItemView: View { + @StateObject + var tempViewModel = ViewModel() @State private var orientation = UIDeviceOrientation.unknown + @Environment(\.horizontalSizeClass) var hSizeClass + @Environment(\.verticalSizeClass) var vSizeClass @EnvironmentObject private var playbackInfo: VideoPlayerItem var item: BaseItemDto @@ -18,7 +22,6 @@ struct EpisodeItemView: View { @State private var settingState: Bool = true @State private var watched: Bool = false { didSet { - var tempCancellables = Set() if !settingState { if watched == true { PlaystateAPI.markPlayedItem(userId: SessionManager.current.userID!, itemId: item.id!) @@ -26,14 +29,14 @@ struct EpisodeItemView: View { print(completion) }, receiveValue: { _ in }) - .store(in: &tempCancellables) + .store(in: &tempViewModel.cancellables) } else { PlaystateAPI.markUnplayedItem(userId: SessionManager.current.userID!, itemId: item.id!) .sink(receiveCompletion: { completion in print(completion) }, receiveValue: { _ in }) - .store(in: &tempCancellables) + .store(in: &tempViewModel.cancellables) } } } @@ -42,7 +45,6 @@ struct EpisodeItemView: View { @State private var favorite: Bool = false { didSet { - var tempCancellables = Set() if !settingState { if favorite == true { UserLibraryAPI.markFavoriteItem(userId: SessionManager.current.userID!, itemId: item.id!) @@ -50,14 +52,14 @@ struct EpisodeItemView: View { print(completion) }, receiveValue: { _ in }) - .store(in: &tempCancellables) + .store(in: &tempViewModel.cancellables) } else { UserLibraryAPI.unmarkFavoriteItem(userId: SessionManager.current.userID!, itemId: item.id!) .sink(receiveCompletion: { completion in print(completion) }, receiveValue: { _ in }) - .store(in: &tempCancellables) + .store(in: &tempViewModel.cancellables) } } } @@ -152,7 +154,7 @@ struct EpisodeItemView: View { var body: some View { VStack(alignment: .leading) { - if orientation == .portrait { + if hSizeClass == .compact && vSizeClass == .regular { ParallaxHeaderScrollView(header: portraitHeaderView, staticOverlayView: portraitHeaderOverlayView, overlayAlignment: .bottomLeading, headerHeight: UIDevice.current.userInterfaceIdiom == .pad ? 350 : UIScreen.main.bounds.width * 0.5625) { VStack(alignment: .leading) { Spacer() diff --git a/JellyfinPlayer/HomeView.swift b/JellyfinPlayer/HomeView.swift new file mode 100644 index 00000000..b01803cb --- /dev/null +++ b/JellyfinPlayer/HomeView.swift @@ -0,0 +1,85 @@ +// +/* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import Foundation +import SwiftUI + +struct HomeView: View { + @StateObject + var viewModel = HomeViewModel() + @State + private var orientation = UIDevice.current.orientation + @Environment(\.horizontalSizeClass) + var hSizeClass + @Environment(\.verticalSizeClass) + var vSizeClass + @State + var showingSettings = false + + var body: some View { + ZStack { + ScrollView { + LazyVStack(alignment: .leading) { + Spacer().frame(height: hSizeClass == .compact && vSizeClass == .regular ? 0 : 16) + if !viewModel.resumeItems.isEmpty { + ContinueWatchingView(items: viewModel.resumeItems) + } + if !viewModel.nextUpItems.isEmpty { + NextUpView(items: viewModel.nextUpItems) + } + if !viewModel.librariesShowRecentlyAddedIDs.isEmpty { + ForEach(viewModel.librariesShowRecentlyAddedIDs, id: \.self) { libraryID in + VStack(alignment: .leading) { + let library = viewModel.libraries.first(where: { $0.id == libraryID }) + HStack { + Text("Latest \(library?.name ?? "")") + .font(.title2) + .fontWeight(.bold) + .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 16)) + Spacer() + NavigationLink(destination: LazyView { + LibraryView(usingParentID: libraryID, + title: library?.name ?? "", usingFilters: viewModel.recentFilterSet) + }) { + HStack { + Text("See All").font(.subheadline).fontWeight(.bold) + Image(systemName: "chevron.right").font(Font.subheadline.bold()) + } + } + }.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)) + LatestMediaView(usingParentID: libraryID) + }.padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0)) + } + } + + Spacer().frame(height: UIDevice.current.userInterfaceIdiom == .phone ? 20 : 30) + } + } + if viewModel.isLoading { + ProgressView() + } + } + .onRotate { + orientation = $0 + } + .navigationTitle(MainTabView.Tab.home.localized) + .toolbar { + ToolbarItemGroup(placement: .navigationBarTrailing) { + Button { + showingSettings = true + } label: { + Image(systemName: "gear") + } + } + } + .fullScreenCover(isPresented: $showingSettings) { + SettingsView(viewModel: SettingsViewModel(), close: $showingSettings) + } + } +} diff --git a/JellyfinPlayer/JellyfinPlayerApp.swift b/JellyfinPlayer/JellyfinPlayerApp.swift index aef8b3b6..1afbf8a2 100644 --- a/JellyfinPlayer/JellyfinPlayerApp.swift +++ b/JellyfinPlayer/JellyfinPlayerApp.swift @@ -141,15 +141,13 @@ extension View { @main struct JellyfinPlayerApp: App { let persistenceController = PersistenceController.shared - @StateObject private var jsi = justSignedIn() var body: some Scene { WindowGroup { - ContentView() + SplashView() .environment(\.managedObjectContext, persistenceController.container.viewContext) - .environmentObject(jsi) .withHostingWindow { window in - window?.rootViewController = PreferenceUIHostingController(wrappedView: ContentView().environment(\.managedObjectContext, persistenceController.container.viewContext).environmentObject(jsi)) + window?.rootViewController = PreferenceUIHostingController(wrappedView: SplashView().environment(\.managedObjectContext, persistenceController.container.viewContext)) } } } diff --git a/JellyfinPlayer/LatestMediaView.swift b/JellyfinPlayer/LatestMediaView.swift index 0bc47e5c..dd75a03f 100644 --- a/JellyfinPlayer/LatestMediaView.swift +++ b/JellyfinPlayer/LatestMediaView.swift @@ -11,6 +11,8 @@ import Combine struct LatestMediaView: View { + @StateObject + var tempViewModel = ViewModel() @State var items: [BaseItemDto] = [] private var library_id: String = "" @State private var viewDidLoad: Bool = false @@ -24,8 +26,6 @@ struct LatestMediaView: View { return } viewDidLoad = true - - var tempCancellables = Set() DispatchQueue.global(qos: .userInitiated).async { UserLibraryAPI.getLatestMedia(userId: SessionManager.current.userID!, parentId: library_id, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], enableUserData: true, limit: 12) @@ -34,7 +34,7 @@ struct LatestMediaView: View { }, receiveValue: { response in items = response }) - .store(in: &tempCancellables) + .store(in: &tempViewModel.cancellables) } } diff --git a/JellyfinPlayer/LibraryListView.swift b/JellyfinPlayer/LibraryListView.swift index 223d1d01..f916894a 100644 --- a/JellyfinPlayer/LibraryListView.swift +++ b/JellyfinPlayer/LibraryListView.swift @@ -9,51 +9,33 @@ import Foundation import SwiftUI struct LibraryListView: View { - - @State var library_ids: [String] = ["favorites", "genres"] - @State var library_names: [String: String] = ["favorites": "Favorites", "genres": "Genres"] - var libraries: [String: String] = [:] // input libraries - var withFavorites: LibraryFilters = LibraryFilters(filters: [.isFavorite], sortOrder: [], withGenres: [], sortBy: []) - - init(libraries: [String: String]) { - self.libraries = libraries - } - - func onAppear() { - if library_ids.count == 2 { - libraries.forEach { k, v in - print("\(k): \(v)") - _library_ids.wrappedValue.append(k) - _library_names.wrappedValue[k] = v - } - } - } + @StateObject + var viewModel = LibraryListViewModel() var body: some View { - List(library_ids, id: \.self) { key in - switch key { - case "favorites": - NavigationLink(destination: LazyView { - LibraryView(usingParentID: "", title: library_names[key] ?? "", usingFilters: withFavorites) - }) { - Text(library_names[key] ?? "") - } - case "genres": - NavigationLink(destination: LazyView { - EmptyView() - }) { - Text(library_names[key] ?? "") - } - default: - NavigationLink(destination: LazyView { - LibraryView(usingParentID: key, title: library_names[key] ?? "") - }) { - Text(library_names[key] ?? "") - } + List(viewModel.libraries, id: \.self) { library in + switch library.id { + case "favorites": + NavigationLink(destination: LazyView { + LibraryView(usingParentID: "", title: library.name ?? "", usingFilters: viewModel.withFavorites) + }) { + Text(library.name ?? "") + } + case "genres": + NavigationLink(destination: LazyView { + EmptyView() + }) { + Text(library.name ?? "") + } + default: + NavigationLink(destination: LazyView { + LibraryView(usingParentID: library.id ?? "", title: library.name ?? "") + }) { + Text(library.name ?? "") + } } } .navigationTitle("All Media") - .onAppear(perform: onAppear) .toolbar { ToolbarItemGroup(placement: .navigationBarTrailing) { NavigationLink(destination: LazyView { diff --git a/JellyfinPlayer/LibrarySearchView.swift b/JellyfinPlayer/LibrarySearchView.swift index d6ae534f..353211b7 100644 --- a/JellyfinPlayer/LibrarySearchView.swift +++ b/JellyfinPlayer/LibrarySearchView.swift @@ -11,6 +11,8 @@ import Combine struct LibrarySearchView: View { + @StateObject + var tempViewModel = ViewModel() @State private var items: [BaseItemDto] = [] @State private var searchQuery: String = "" @State private var isLoading: Bool = false @@ -28,7 +30,6 @@ struct LibrarySearchView: View { func requestSearch(query: String) { isLoading = true - var tempCancellables = Set() DispatchQueue.global(qos: .userInitiated).async { ItemsAPI.getItemsByUserId(userId: SessionManager.current.userID!, limit: 60, recursive: true, searchTerm: query, sortOrder: [.ascending], parentId: (usingParentID != "" ? usingParentID : nil), fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], includeItemTypes: ["Movie", "Series"], sortBy: ["SortName"], enableUserData: true, enableImages: true) .sink(receiveCompletion: { completion in @@ -37,7 +38,7 @@ struct LibrarySearchView: View { items = response.items ?? [] isLoading = false }) - .store(in: &tempCancellables) + .store(in: &tempViewModel.cancellables) } } diff --git a/JellyfinPlayer/LibraryView.swift b/JellyfinPlayer/LibraryView.swift index 2549d4a9..a6709f9a 100644 --- a/JellyfinPlayer/LibraryView.swift +++ b/JellyfinPlayer/LibraryView.swift @@ -13,6 +13,8 @@ import Combine struct LibraryView: View { + @StateObject + var tempViewModel = ViewModel() @State private var items: [BaseItemDto] = [] @State private var isLoading: Bool = false @@ -66,8 +68,6 @@ struct LibraryView: View { isLoading = true items = [] - - var tempCancellables = Set() DispatchQueue.global(qos: .userInitiated).async { ItemsAPI.getItemsByUserId(userId: SessionManager.current.userID!, startIndex: currentPage * 100, limit: 100, recursive: true, searchTerm: nil, sortOrder: filters.sortOrder, parentId: (usingParentID != "" ? usingParentID : nil), fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], includeItemTypes: ["Movie", "Series"], filters: filters.filters, sortBy: filters.sortBy, enableUserData: true, personIds: (personId == "" ? nil : [personId]), studioIds: (studio == "" ? nil : [studio]), genreIds: (genre == "" ? nil : [genre]), enableImages: true) @@ -81,7 +81,7 @@ struct LibraryView: View { isLoading = false viewDidLoad = true }) - .store(in: &tempCancellables) + .store(in: &tempViewModel.cancellables) } } diff --git a/JellyfinPlayer/MainTabView.swift b/JellyfinPlayer/MainTabView.swift new file mode 100644 index 00000000..c100d5a0 --- /dev/null +++ b/JellyfinPlayer/MainTabView.swift @@ -0,0 +1,55 @@ +// + /* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import Foundation +import SwiftUI + +struct MainTabView: View { + @State private var tabSelection: Tab = .home + + var body: some View { + TabView(selection: $tabSelection) { + NavigationView { + HomeView() + } + .navigationViewStyle(StackNavigationViewStyle()) + .tabItem { + Text(Tab.home.localized) + Image(systemName: "house") + } + .tag(Tab.home) + NavigationView { + LibraryListView() + } + .navigationViewStyle(StackNavigationViewStyle()) + .tabItem { + Text(Tab.allMedia.localized) + Image(systemName: "folder") + } + .tag(Tab.allMedia) + } + } +} + +extension MainTabView { + + enum Tab: String { + case home + case allMedia + + var localized: String { + switch self { + case .home: + return "Home" + case .allMedia: + return "All Media" + } + } + } +} diff --git a/JellyfinPlayer/MovieItemView.swift b/JellyfinPlayer/MovieItemView.swift index 8934ed04..441065ac 100644 --- a/JellyfinPlayer/MovieItemView.swift +++ b/JellyfinPlayer/MovieItemView.swift @@ -5,21 +5,29 @@ * Copyright 2021 Aiden Vigue & Jellyfin Contributors */ -import SwiftUI -import JellyfinAPI import Combine +import JellyfinAPI +import SwiftUI struct MovieItemView: View { - @State private var orientation = UIDeviceOrientation.unknown - @EnvironmentObject private var playbackInfo: VideoPlayerItem + @StateObject + var tempViewModel = ViewModel() + @State + private var orientation = UIDeviceOrientation.unknown + @Environment(\.horizontalSizeClass) + var hSizeClass + @Environment(\.verticalSizeClass) + var vSizeClass + @EnvironmentObject + private var playbackInfo: VideoPlayerItem var item: BaseItemDto - @State private var settingState: Bool = true - @State private var watched: Bool = false { + @State + private var settingState: Bool = true + @State + private var watched: Bool = false { didSet { - var tempCancellables = Set() - if !settingState { if watched == true { PlaystateAPI.markPlayedItem(userId: SessionManager.current.userID!, itemId: item.id!) @@ -27,14 +35,14 @@ struct MovieItemView: View { print(completion) }, receiveValue: { _ in }) - .store(in: &tempCancellables) + .store(in: &tempViewModel.cancellables) } else { PlaystateAPI.markUnplayedItem(userId: SessionManager.current.userID!, itemId: item.id!) .sink(receiveCompletion: { completion in print(completion) }, receiveValue: { _ in }) - .store(in: &tempCancellables) + .store(in: &tempViewModel.cancellables) } } } @@ -43,8 +51,6 @@ struct MovieItemView: View { @State private var favorite: Bool = false { didSet { - var tempCancellables = Set() - if !settingState { if favorite == true { UserLibraryAPI.markFavoriteItem(userId: SessionManager.current.userID!, itemId: item.id!) @@ -52,21 +58,24 @@ struct MovieItemView: View { print(completion) }, receiveValue: { _ in }) - .store(in: &tempCancellables) + .store(in: &tempViewModel.cancellables) } else { UserLibraryAPI.unmarkFavoriteItem(userId: SessionManager.current.userID!, itemId: item.id!) .sink(receiveCompletion: { completion in print(completion) }, receiveValue: { _ in }) - .store(in: &tempCancellables) + .store(in: &tempViewModel.cancellables) } } } } var portraitHeaderView: some View { - ImageView(src: item.getBackdropImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)), bh: item.getBackdropImageBlurHash()) + ImageView(src: item + .getBackdropImage(baseURL: ServerEnvironment.current.server.baseURI!, + maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)), + bh: item.getBackdropImageBlurHash()) .opacity(0.4) .blur(radius: 2.0) } @@ -156,8 +165,11 @@ struct MovieItemView: View { var body: some View { VStack(alignment: .leading) { - if orientation == .portrait { - ParallaxHeaderScrollView(header: portraitHeaderView, staticOverlayView: portraitHeaderOverlayView, overlayAlignment: .bottomLeading, headerHeight: UIDevice.current.userInterfaceIdiom == .pad ? 350 : UIScreen.main.bounds.width * 0.5625) { + if hSizeClass == .compact && vSizeClass == .regular { + ParallaxHeaderScrollView(header: portraitHeaderView, staticOverlayView: portraitHeaderOverlayView, + overlayAlignment: .bottomLeading, + headerHeight: UIDevice.current.userInterfaceIdiom == .pad ? 350 : UIScreen.main.bounds + .width * 0.5625) { VStack(alignment: .leading) { Spacer() .frame(height: UIDevice.current.userInterfaceIdiom == .pad ? 135 : 40) @@ -196,7 +208,9 @@ struct MovieItemView: View { LibraryView(withPerson: person) }) { VStack { - ImageView(src: person.getImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100), bh: person.getBlurHash()) + ImageView(src: person + .getImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100), + bh: person.getBlurHash()) .frame(width: 100, height: 100) .cornerRadius(10) Text(person.name ?? "").font(.footnote).fontWeight(.regular).lineLimit(1) @@ -235,7 +249,8 @@ struct MovieItemView: View { } else { GeometryReader { geometry in ZStack { - ImageView(src: item.getBackdropImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 200), bh: item.getBackdropImageBlurHash()) + ImageView(src: item.getBackdropImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 200), + bh: item.getBackdropImageBlurHash()) .opacity(0.3) .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom) @@ -243,7 +258,8 @@ struct MovieItemView: View { .blur(radius: 4) HStack { VStack { - ImageView(src: item.getPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 120), bh: item.getPrimaryImageBlurHash()) + ImageView(src: item.getPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 120), + bh: item.getPrimaryImageBlurHash()) .frame(width: 120, height: 180) .cornerRadius(10) Spacer().frame(height: 15) @@ -369,10 +385,14 @@ struct MovieItemView: View { LibraryView(withPerson: person) }) { VStack { - ImageView(src: person.getImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100), bh: person.getBlurHash()) + ImageView(src: person + .getImage(baseURL: ServerEnvironment.current.server.baseURI!, + maxWidth: 100), + bh: person.getBlurHash()) .frame(width: 100, height: 100) .cornerRadius(10) - Text(person.name ?? "").font(.footnote).fontWeight(.regular).lineLimit(1) + Text(person.name ?? "").font(.footnote).fontWeight(.regular) + .lineLimit(1) .frame(width: 100).foregroundColor(Color.primary) if person.role != "" { Text(person.role!).font(.caption).fontWeight(.medium).lineLimit(1) diff --git a/JellyfinPlayer/NextUpView.swift b/JellyfinPlayer/NextUpView.swift index f9a15df6..d77e0038 100644 --- a/JellyfinPlayer/NextUpView.swift +++ b/JellyfinPlayer/NextUpView.swift @@ -11,27 +11,7 @@ import JellyfinAPI struct NextUpView: View { - @State private var items: [BaseItemDto] = [] - @State private var viewDidLoad: Bool = false - - func onAppear() { - if viewDidLoad == true { - return - } - viewDidLoad = true - - var tempCancellables = Set() - - DispatchQueue.global(qos: .userInitiated).async { - TvShowsAPI.getNextUp(userId: SessionManager.current.userID!, limit: 12, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people]) - .sink(receiveCompletion: { result in - print(result) - }, receiveValue: { response in - items = response.items ?? [] - }) - .store(in: &tempCancellables) - } - } + var items: [BaseItemDto] var body: some View { VStack(alignment: .leading) { @@ -69,7 +49,6 @@ struct NextUpView: View { .frame(height: 200) } } - .onAppear(perform: onAppear) .padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0)) } } diff --git a/JellyfinPlayer/SeasonItemView.swift b/JellyfinPlayer/SeasonItemView.swift index c4faa0fe..988dbb40 100644 --- a/JellyfinPlayer/SeasonItemView.swift +++ b/JellyfinPlayer/SeasonItemView.swift @@ -10,10 +10,15 @@ import Combine import JellyfinAPI struct SeasonItemView: View { + @StateObject + var tempViewModel = ViewModel() @State private var orientation = UIDeviceOrientation.unknown + @Environment(\.horizontalSizeClass) var hSizeClass + @Environment(\.verticalSizeClass) var vSizeClass var item: BaseItemDto = BaseItemDto() @State private var episodes: [BaseItemDto] = [] + @State private var isLoading: Bool = true @State private var viewDidLoad: Bool = false @@ -26,7 +31,6 @@ struct SeasonItemView: View { if viewDidLoad { return } - var tempCancellables = Set() DispatchQueue.global(qos: .userInitiated).async { TvShowsAPI.getEpisodes(seriesId: item.seriesId ?? "", userId: SessionManager.current.userID!, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], seasonId: item.id ?? "") @@ -37,7 +41,7 @@ struct SeasonItemView: View { viewDidLoad = true episodes = response.items ?? [] }) - .store(in: &tempCancellables) + .store(in: &tempViewModel.cancellables) } } @@ -76,7 +80,7 @@ struct SeasonItemView: View { @ViewBuilder var innerBody: some View { - if orientation == .portrait { + if hSizeClass == .compact && vSizeClass == .regular { ParallaxHeaderScrollView(header: portraitHeaderView, staticOverlayView: portraitHeaderOverlayView, overlayAlignment: .bottomLeading, diff --git a/JellyfinPlayer/SeriesItemView.swift b/JellyfinPlayer/SeriesItemView.swift index 4ce099ca..63892b94 100644 --- a/JellyfinPlayer/SeriesItemView.swift +++ b/JellyfinPlayer/SeriesItemView.swift @@ -10,6 +10,8 @@ import JellyfinAPI import Combine struct SeriesItemView: View { + @StateObject + var tempViewModel = ViewModel() var item: BaseItemDto @@ -25,7 +27,6 @@ struct SeriesItemView: View { isLoading = true - var tempCancellables = Set() DispatchQueue.global(qos: .userInitiated).async { TvShowsAPI.getSeasons(seriesId: item.id ?? "", fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people]) @@ -36,7 +37,7 @@ struct SeriesItemView: View { viewDidLoad = true seasons = response.items ?? [] }) - .store(in: &tempCancellables) + .store(in: &tempViewModel.cancellables) } } diff --git a/JellyfinPlayer/SettingsView.swift b/JellyfinPlayer/SettingsView.swift index ed94f0eb..b42c0fd4 100644 --- a/JellyfinPlayer/SettingsView.swift +++ b/JellyfinPlayer/SettingsView.swift @@ -10,18 +10,16 @@ import SwiftUI struct SettingsView: View { @Environment(\.managedObjectContext) private var viewContext - - @EnvironmentObject var jsi: justSignedIn - + @ObservedObject var viewModel: SettingsViewModel - + @Binding var close: Bool @State private var inNetworkStreamBitrate: Int = 40_000_000 @State private var outOfNetworkStreamBitrate: Int = 40_000_000 @State private var autoSelectSubtitles: Bool = false @State private var autoSelectSubtitlesLangcode: String = "none" @State private var username: String = "" - + func onAppear() { let defaults = UserDefaults.standard username = SessionManager.current.user.username! @@ -30,7 +28,7 @@ struct SettingsView: View { autoSelectSubtitles = defaults.bool(forKey: "AutoSelectSubtitles") autoSelectSubtitlesLangcode = defaults.string(forKey: "AutoSelectSubtitlesLangcode") ?? "" } - + var body: some View { NavigationView { Form { @@ -43,7 +41,7 @@ struct SettingsView: View { let defaults = UserDefaults.standard defaults.setValue(_inNetworkStreamBitrate.wrappedValue, forKey: "InNetworkBandwidth") } - + Picker("Default remote quality", selection: $outOfNetworkStreamBitrate) { ForEach(self.viewModel.bitrates, id: \.self) { bitrate in Text(bitrate.name).tag(bitrate.value) @@ -53,7 +51,7 @@ struct SettingsView: View { defaults.setValue(_outOfNetworkStreamBitrate.wrappedValue, forKey: "OutOfNetworkBandwidth") } } - + Section(header: Text("Accessibility")) { Toggle("Automatically show subtitles", isOn: $autoSelectSubtitles).onChange(of: autoSelectSubtitles, perform: { _ in let defaults = UserDefaults.standard @@ -61,7 +59,7 @@ struct SettingsView: View { }) Picker("Language preferences", selection: $autoSelectSubtitlesLangcode) {} } - + Section { HStack { Text("Signed in as \(username)").foregroundColor(.primary) @@ -69,27 +67,28 @@ struct SettingsView: View { Button { let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: "Server") let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) - + do { try viewContext.execute(deleteRequest) } catch _ as NSError { // TODO: handle the error } - + let fetchRequest2: NSFetchRequest = NSFetchRequest(entityName: "SignedInUser") let deleteRequest2 = NSBatchDeleteRequest(fetchRequest: fetchRequest2) - + do { try viewContext.execute(deleteRequest2) } catch _ as NSError { // TODO: handle the error } - - globalData.server = Server() - globalData.user = SignedInUser() - globalData.authToken = "" - globalData.authHeader = "" - jsi.did = true + + do { + try SessionManager.current.logout() + try ServerEnvironment.current.reset() + } catch { + print(error) + } // TODO: This should redirect to the server selection screen exit(-1) } label: { diff --git a/JellyfinPlayer/SplashView.swift b/JellyfinPlayer/SplashView.swift new file mode 100644 index 00000000..099a3a27 --- /dev/null +++ b/JellyfinPlayer/SplashView.swift @@ -0,0 +1,26 @@ +// +/* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import SwiftUI + +struct SplashView: View { + @StateObject + var viewModel = SplashViewModel() + + var body: some View { + if viewModel.isLoggedIn { + MainTabView() + } else { + NavigationView { + ConnectToServerView(isLoggedIn: $viewModel.isLoggedIn) + } + .navigationViewStyle(StackNavigationViewStyle()) + } + } +} diff --git a/Shared/Shared/SessionManager.swift b/Shared/Shared/SessionManager.swift index 47f3ad2e..d18ff620 100644 --- a/Shared/Shared/SessionManager.swift +++ b/Shared/Shared/SessionManager.swift @@ -21,7 +21,7 @@ final class SessionManager { fileprivate(set) var authToken: String! fileprivate(set) var deviceID: String var userID: String? { - user.user_id + user?.user_id } init() { diff --git a/Shared/Typings/Typings.swift b/Shared/Typings/Typings.swift index a3270db8..87d62223 100644 --- a/Shared/Typings/Typings.swift +++ b/Shared/Typings/Typings.swift @@ -22,7 +22,3 @@ public enum SortBy: String, Codable, CaseIterable { case name = "SortName" case dateAdded = "DateCreated" } - -class justSignedIn: ObservableObject { - @Published var did: Bool = false -} diff --git a/Shared/ViewModel/ConnectToServerViewModel.swift b/Shared/ViewModel/ConnectToServerViewModel.swift new file mode 100644 index 00000000..64315bf4 --- /dev/null +++ b/Shared/ViewModel/ConnectToServerViewModel.swift @@ -0,0 +1,88 @@ +// +/* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import Combine +import Foundation +import JellyfinAPI + +final class ConnectToServerViewModel: ViewModel { + @Published + var publicUsers = [UserDto]() + @Published + var isConnectedServer = false + @Published + var isLoggedIn = false + @Published + var uri = "" + @Published + var username = "" + @Published + var password = "" + + + override init() { + super.init() + + refresh() + } + + func refresh() { + if ServerEnvironment.current.server != nil { + UserAPI.getPublicUsers() + .sink(receiveCompletion: { completion in + switch completion { + case .finished: + break + case .failure: + self.isConnectedServer = false + } + }, receiveValue: { response in + self.publicUsers = response + self.isConnectedServer = true + }) + .store(in: &cancellables) + } + } + + func connectToServer() { + ServerEnvironment.current.setUp(with: uri) + .sink(receiveCompletion: { result in + switch result { + case let .failure(error): + self.errorMessage = error.localizedDescription + default: + break + } + }, receiveValue: { response in + guard response.server_id != nil else { + return + } + self.isConnectedServer = true + }) + .store(in: &cancellables) + } + + func login() { + SessionManager.current.login(username: username, password: password) + .sink(receiveCompletion: { result in + switch result { + case let .failure(error): + self.errorMessage = error.localizedDescription + default: + break + } + }, receiveValue: { response in + guard response.user_id != nil else { + return + } + self.isLoggedIn = true + }) + .store(in: &cancellables) + } +} diff --git a/Shared/ViewModel/HomeViewModel.swift b/Shared/ViewModel/HomeViewModel.swift new file mode 100644 index 00000000..c93bf8a6 --- /dev/null +++ b/Shared/ViewModel/HomeViewModel.swift @@ -0,0 +1,78 @@ +// +/* + * 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 ActivityIndicator +import Combine +import Foundation +import JellyfinAPI + +final class HomeViewModel: ViewModel { + + @Published + var librariesShowRecentlyAddedIDs = [String]() + @Published + var libraries = [BaseItemDto]() + @Published + var resumeItems = [BaseItemDto]() + @Published + var nextUpItems = [BaseItemDto]() + + // temp + var recentFilterSet: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.descending], sortBy: ["DateCreated"]) + + override init() { + super.init() + + refresh() + } + + func refresh() { + UserAPI.getCurrentUser() + .trackActivity(loading) + .sink(receiveCompletion: { completion in + print(completion) + }, receiveValue: { response in + let libraries = response.configuration?.orderedViews ?? [] + self.librariesShowRecentlyAddedIDs = libraries.filter { element in + !(response.configuration?.latestItemsExcludes?.contains(element))! + } + }) + .store(in: &cancellables) + + UserViewsAPI.getUserViews(userId: SessionManager.current.userID ?? "") + .trackActivity(loading) + .sink(receiveCompletion: { completion in + print(completion) + }, receiveValue: { response in + self.libraries = response.items ?? [] + }) + .store(in: &cancellables) + + ItemsAPI.getResumeItems(userId: SessionManager.current.userID!, limit: 12, + fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], + mediaTypes: ["Video"], imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb]) + .trackActivity(loading) + .sink(receiveCompletion: { completion in + print(completion) + }, receiveValue: { response in + self.resumeItems = response.items ?? [] + }) + .store(in: &cancellables) + + TvShowsAPI.getNextUp(userId: SessionManager.current.userID!, limit: 12, + fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people]) + .trackActivity(loading) + .sink(receiveCompletion: { result in + print(result) + }, receiveValue: { response in + self.nextUpItems = response.items ?? [] + }) + .store(in: &cancellables) + } +} diff --git a/Shared/ViewModel/LibraryListViewModel.swift b/Shared/ViewModel/LibraryListViewModel.swift new file mode 100644 index 00000000..0c6aa0aa --- /dev/null +++ b/Shared/ViewModel/LibraryListViewModel.swift @@ -0,0 +1,38 @@ +// +/* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import Foundation +import JellyfinAPI + +final class LibraryListViewModel: ViewModel { + @Published + var libraries = [BaseItemDto]() + + // temp + var withFavorites = LibraryFilters(filters: [.isFavorite], sortOrder: [], withGenres: [], sortBy: []) + + override init() { + super.init() + + libraries.append(.init(name: "Favorites", id: "favorites")) + libraries.append(.init(name: "Genres", id: "genres")) + refresh() + } + + func refresh() { + UserViewsAPI.getUserViews(userId: SessionManager.current.userID ?? "") + .trackActivity(loading) + .sink(receiveCompletion: { completion in + print(completion) + }, receiveValue: { response in + self.libraries.append(contentsOf: response.items ?? []) + }) + .store(in: &cancellables) + } +} diff --git a/Shared/ViewModel/SplashViewModel.swift b/Shared/ViewModel/SplashViewModel.swift new file mode 100644 index 00000000..34d80867 --- /dev/null +++ b/Shared/ViewModel/SplashViewModel.swift @@ -0,0 +1,37 @@ +// + /* + * 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 Nuke +import WidgetKit + +final class SplashViewModel: ViewModel { + + @Published + var isLoggedIn: Bool + + override init() { + isLoggedIn = ServerEnvironment.current.server != nil && SessionManager.current.user != nil + super.init() + + ImageCache.shared.costLimit = 125 * 1024 * 1024 // 125MB memory + DataLoader.sharedUrlCache.diskCapacity = 1000 * 1024 * 1024 // 1000MB disk + + WidgetCenter.shared.reloadAllTimelines() + + let defaults = UserDefaults.standard + if defaults.integer(forKey: "InNetworkBandwidth") == 0 { + defaults.setValue(40_000_000, forKey: "InNetworkBandwidth") + } + if defaults.integer(forKey: "OutOfNetworkBandwidth") == 0 { + defaults.setValue(40_000_000, forKey: "OutOfNetworkBandwidth") + } + } +} diff --git a/Shared/ViewModel/ViewModel.swift b/Shared/ViewModel/ViewModel.swift new file mode 100644 index 00000000..c13eee7f --- /dev/null +++ b/Shared/ViewModel/ViewModel.swift @@ -0,0 +1,33 @@ +// +/* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import Combine +import Foundation +import ActivityIndicator + +typealias ErrorMessage = String + +extension ErrorMessage: Identifiable { + public var id: String { + self + } +} + +class ViewModel: ObservableObject { + var cancellables = Set() + @Published + var isLoading = true + let loading = ActivityIndicator() + @Published + var errorMessage: ErrorMessage? + + init() { + loading.loading.assign(to: \.isLoading, on: self).store(in: &cancellables) + } +}