diff --git a/JellyfinPlayer tvOS/ConnectToServerView.swift b/JellyfinPlayer tvOS/ConnectToServerView.swift index de8b8dc5..b5115683 100644 --- a/JellyfinPlayer tvOS/ConnectToServerView.swift +++ b/JellyfinPlayer tvOS/ConnectToServerView.swift @@ -14,97 +14,96 @@ struct ConnectToServerView: View { @Binding var isLoggedIn: Bool var body: some View { - ZStack { - Form { - if viewModel.isConnectedServer { - if viewModel.publicUsers.isEmpty { - Section(header: Text("Login to \(ServerEnvironment.current.server.name ?? "")")) { + VStack(alignment: .leading) { + if viewModel.isConnectedServer { + if viewModel.publicUsers.isEmpty { + Section(header: Text(viewModel.lastPublicUsers.isEmpty || viewModel.username == "" ? "Login to \(ServerEnvironment.current.server.name ?? "")": "Password for \(viewModel.username)")) { + if(viewModel.lastPublicUsers.isEmpty || viewModel.username == "") { TextField("Username", text: $viewModel.username) .disableAutocorrection(true) .autocapitalization(.none) - SecureField("Password", text: $viewModel.password) - .disableAutocorrection(true) - .autocapitalization(.none) + } + SecureField("Password (optional)", text: $viewModel.password) + .disableAutocorrection(true) + .autocapitalization(.none) + } + + Section { + HStack() { + Button { + if(!viewModel.lastPublicUsers.isEmpty) { + viewModel.username = "" + viewModel.showPublicUsers() + } else { + viewModel.isConnectedServer = false + } + } label: { + Spacer() + HStack { + Text("Back") + } + Spacer() + } + Button { viewModel.login() } label: { - HStack { + Spacer() + if viewModel.isLoading { + ProgressView() + } else { Text("Login") - Spacer() - if viewModel.isLoading { - ProgressView() - } } + Spacer() }.disabled(viewModel.isLoading || viewModel.username.isEmpty) } - - Section { - Button { - viewModel.isConnectedServer = false - } label: { - HStack { - HStack { - Image(systemName: "chevron.left") - Text("Change Server") - } - Spacer() - } - } - } - } else { - Section(header: Text("Login to \(ServerEnvironment.current.server.name ?? "")")) { - ForEach(viewModel.publicUsers, id: \.id) { publicUser in - HStack { - Button(action: { - 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 { - 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) - } - } - } + Text("Logging in will link this user to your Apple TV profile").font(.caption).foregroundColor(.secondary) } } else { + VStack() { + HStack() { + ForEach(viewModel.publicUsers, id: \.id) { publicUser in + Button(action: { + viewModel.username = publicUser.name ?? "" + viewModel.hidePublicUsers() + if !(publicUser.hasPassword ?? true) { + viewModel.password = "" + viewModel.login() + } + }) { + VStack { + if publicUser.primaryImageTag != nil { + ImageView(src: URL(string: "\(ServerEnvironment.current.server.baseURI ?? "")/Users/\(publicUser.id ?? "")/Images/Primary?width=500&quality=80&tag=\(publicUser.primaryImageTag!)")!) + .frame(width: 250, height: 250) + .cornerRadius(125.0) + } else { + Image(systemName: "person.fill") + .foregroundColor(Color(red: 1, green: 1, blue: 1).opacity(0.8)) + .font(.system(size: 35)) + .frame(width: 250, height: 250) + .background(Color(red: 98 / 255, green: 121 / 255, blue: 205 / 255)) + .cornerRadius(125.0) + .shadow(radius: 6) + } + Text(publicUser.name ?? "").font(.headline).fontWeight(.semibold) + } + } + } + } + HStack() { + Spacer() + Button { + viewModel.hidePublicUsers() + viewModel.username = "" + } label: { + Text("Other User").font(.headline).fontWeight(.semibold) + } + Spacer() + }.padding(.top, 12) + } + } + } else { + Form { Section(header: Text("Server Information")) { TextField("Jellyfin Server URL", text: $viewModel.uri) .disableAutocorrection(true) @@ -126,11 +125,11 @@ struct ConnectToServerView: View { } } .alert(item: $viewModel.errorMessage) { _ in - Alert(title: Text("Error"), message: Text("message"), dismissButton: .default(Text("Try again"))) + Alert(title: Text("Error"), message: Text(viewModel.errorMessage ?? ""), dismissButton: .default(Text("Ok"))) } .onReceive(viewModel.$isLoggedIn, perform: { flag in isLoggedIn = flag }) - .navigationTitle("Connect to Server") + .navigationTitle(viewModel.isConnectedServer ? "Who's watching?" : "Connect to Jellyfin") } } diff --git a/JellyfinPlayer tvOS/ContentView.swift b/JellyfinPlayer tvOS/ContentView.swift deleted file mode 100644 index eb320d11..00000000 --- a/JellyfinPlayer tvOS/ContentView.swift +++ /dev/null @@ -1,58 +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 CoreData - -struct ContentView: View { - @Environment(\.managedObjectContext) private var viewContext - - var body: some View { - NavigationView { - List { - ForEach(items) { item in - Text("Item at \(item.timestamp!, formatter: itemFormatter)") - } - } - .toolbar { - Button(action: addItem) { - Label("Add Item", systemImage: "plus") - } - } - } - } - - private func addItem() { - withAnimation { - let newItem = Item(context: viewContext) - newItem.timestamp = Date() - - do { - try viewContext.save() - } catch { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - let nsError = error as NSError - fatalError("Unresolved error \(nsError), \(nsError.userInfo)") - } - } - } - -} - -private let itemFormatter: DateFormatter = { - let formatter = DateFormatter() - formatter.dateStyle = .short - formatter.timeStyle = .medium - return formatter -}() - -struct ContentView_Previews: PreviewProvider { - static var previews: some View { - ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) - } -} diff --git a/JellyfinPlayer tvOS/JellyfinPlayer_tvOSApp.swift b/JellyfinPlayer tvOS/JellyfinPlayer_tvOSApp.swift index a8973d18..38520299 100644 --- a/JellyfinPlayer tvOS/JellyfinPlayer_tvOSApp.swift +++ b/JellyfinPlayer tvOS/JellyfinPlayer_tvOSApp.swift @@ -13,7 +13,7 @@ struct JellyfinPlayer_tvOSApp: App { var body: some Scene { WindowGroup { - ContentView() + SplashView() .environment(\.managedObjectContext, persistenceController.container.viewContext) } } diff --git a/JellyfinPlayer tvOS/PersistenceController.swift b/JellyfinPlayer tvOS/PersistenceController.swift index 5d7dfb93..056d1330 100644 --- a/JellyfinPlayer tvOS/PersistenceController.swift +++ b/JellyfinPlayer tvOS/PersistenceController.swift @@ -10,28 +10,10 @@ import CoreData struct PersistenceController { static let shared = PersistenceController() - static var preview: PersistenceController = { - let result = PersistenceController(inMemory: true) - let viewContext = result.container.viewContext - for _ in 0..<10 { - let newItem = Item(context: viewContext) - newItem.timestamp = Date() - } - do { - try viewContext.save() - } catch { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - let nsError = error as NSError - fatalError("Unresolved error \(nsError), \(nsError.userInfo)") - } - return result - }() - let container: NSPersistentContainer init(inMemory: Bool = false) { - container = NSPersistentContainer(name: "JellyfinPlayer") + container = NSPersistentContainer(name: "Model") if inMemory { container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") } diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 164e8125..73478ebb 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -18,7 +18,6 @@ 5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5338F74D263B61370014BF09 /* ConnectToServerView.swift */; }; 5338F757263B7E2E0014BF09 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 5338F756263B7E2E0014BF09 /* KeychainSwift */; }; 535870632669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535870622669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift */; }; - 535870652669D21600D05A09 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535870642669D21600D05A09 /* ContentView.swift */; }; 535870672669D21700D05A09 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 535870662669D21700D05A09 /* Assets.xcassets */; }; 5358706A2669D21700D05A09 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 535870692669D21700D05A09 /* Preview Assets.xcassets */; }; 5358706C2669D21700D05A09 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5358706B2669D21700D05A09 /* PersistenceController.swift */; }; @@ -59,6 +58,8 @@ 53ABFDE8267974EF00886593 /* SplashViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5692678B71200530A6E /* SplashViewModel.swift */; }; 53ABFDE9267974EF00886593 /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5722678C32A00530A6E /* HomeViewModel.swift */; }; 53ABFDEB2679753200886593 /* ConnectToServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53ABFDEA2679753200886593 /* ConnectToServerView.swift */; }; + 53ABFDED26799D7700886593 /* ActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 53ABFDEC26799D7700886593 /* ActivityIndicator */; }; + 53ABFDEE26799DCD00886593 /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531AC8BE26750DE20091C7EB /* ImageView.swift */; }; 53AD124D267029D60094A276 /* SeriesItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53987CA526572F0700E7EA70 /* SeriesItemView.swift */; }; 53AD124E26702B8A0094A276 /* SeasonItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53987CA326572C1300E7EA70 /* SeasonItemView.swift */; }; 53DE4BD02670961400739748 /* EpisodeItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53987CA72657424A00E7EA70 /* EpisodeItemView.swift */; }; @@ -177,7 +178,6 @@ 5338F74D263B61370014BF09 /* ConnectToServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectToServerView.swift; sourceTree = ""; }; 535870602669D21600D05A09 /* JellyfinPlayer tvOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "JellyfinPlayer tvOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 535870622669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinPlayer_tvOSApp.swift; sourceTree = ""; }; - 535870642669D21600D05A09 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 535870662669D21700D05A09 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 535870692669D21700D05A09 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 5358706B2669D21700D05A09 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = ""; }; @@ -256,6 +256,7 @@ 5358708D2669D7A800D05A09 /* KeychainSwift in Frameworks */, 53ABFDDC267972BF00886593 /* TVServices.framework in Frameworks */, 5358709B2669D7A800D05A09 /* NukeUI in Frameworks */, + 53ABFDED26799D7700886593 /* ActivityIndicator in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -305,7 +306,6 @@ children = ( 53ABFDDA267972BF00886593 /* JellyfinPlayer tvOS.entitlements */, 535870622669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift */, - 535870642669D21600D05A09 /* ContentView.swift */, 535870662669D21700D05A09 /* Assets.xcassets */, 5358706B2669D21700D05A09 /* PersistenceController.swift */, 535870702669D21700D05A09 /* Info.plist */, @@ -396,7 +396,6 @@ 53313B8F265EEA6D00947AA3 /* VideoPlayer.storyboard */, 53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */, 53DE4BD1267098F300739748 /* SearchBarView.swift */, - 531AC8BE26750DE20091C7EB /* ImageView.swift */, 625CB5672678B6FB00530A6E /* SplashView.swift */, 625CB56B2678C0FD00530A6E /* MainTabView.swift */, 625CB56E2678C23300530A6E /* HomeView.swift */, @@ -427,6 +426,7 @@ 621338912660106C00A81A2A /* Extensions */ = { isa = PBXGroup; children = ( + 531AC8BE26750DE20091C7EB /* ImageView.swift */, 5364F454266CA0DC0026ECBA /* APIExtensions.swift */, 5389277B263CC3DB0035E14B /* BlurHashDecode.swift */, 621338B22660A07800A81A2A /* LazyView.swift */, @@ -492,6 +492,7 @@ 535870902669D7A800D05A09 /* Introspect */, 5358709A2669D7A800D05A09 /* NukeUI */, 53A431BE266B0FFE0016769F /* JellyfinAPI */, + 53ABFDEC26799D7700886593 /* ActivityIndicator */, ); productName = "JellyfinPlayer tvOS"; productReference = 535870602669D21600D05A09 /* JellyfinPlayer tvOS.app */; @@ -646,10 +647,10 @@ 535870A52669D8AE00D05A09 /* ParallaxHeader.swift in Sources */, 535870A72669D8AE00D05A09 /* MultiSelectorView.swift in Sources */, 53ABFDE7267974EF00886593 /* ConnectToServerViewModel.swift in Sources */, + 53ABFDEE26799DCD00886593 /* ImageView.swift in Sources */, 5358706C2669D21700D05A09 /* PersistenceController.swift in Sources */, 535870AA2669D8AE00D05A09 /* BlurHashDecode.swift in Sources */, 53ABFDE5267974EF00886593 /* ViewModel.swift in Sources */, - 535870652669D21600D05A09 /* ContentView.swift in Sources */, 535870A62669D8AE00D05A09 /* LazyView.swift in Sources */, 5321753E2671DE9C005491E6 /* Typings.swift in Sources */, 53ABFDEB2679753200886593 /* ConnectToServerView.swift in Sources */, @@ -1161,6 +1162,11 @@ package = 53A431BB266B0FF20016769F /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */; productName = JellyfinAPI; }; + 53ABFDEC26799D7700886593 /* ActivityIndicator */ = { + isa = XCSwiftPackageProductDependency; + package = 625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */; + productName = ActivityIndicator; + }; 621C637F26672A30004216EA /* NukeUI */ = { isa = XCSwiftPackageProductDependency; package = 621C637E26672A30004216EA /* XCRemoteSwiftPackageReference "NukeUI" */; diff --git a/JellyfinPlayer/ImageView.swift b/Shared/Extensions/ImageView.swift similarity index 100% rename from JellyfinPlayer/ImageView.swift rename to Shared/Extensions/ImageView.swift diff --git a/Shared/ViewModels/ConnectToServerViewModel.swift b/Shared/ViewModels/ConnectToServerViewModel.swift index 64315bf4..2933994a 100644 --- a/Shared/ViewModels/ConnectToServerViewModel.swift +++ b/Shared/ViewModels/ConnectToServerViewModel.swift @@ -24,6 +24,8 @@ final class ConnectToServerViewModel: ViewModel { var username = "" @Published var password = "" + @Published + var lastPublicUsers = [UserDto]() override init() { @@ -50,6 +52,16 @@ final class ConnectToServerViewModel: ViewModel { } } + func hidePublicUsers() { + self.lastPublicUsers = publicUsers; + publicUsers = []; + } + + func showPublicUsers() { + self.publicUsers = lastPublicUsers; + lastPublicUsers = []; + } + func connectToServer() { ServerEnvironment.current.setUp(with: uri) .sink(receiveCompletion: { result in @@ -63,7 +75,7 @@ final class ConnectToServerViewModel: ViewModel { guard response.server_id != nil else { return } - self.isConnectedServer = true + self.refresh() }) .store(in: &cancellables) } diff --git a/Shared/ViewModels/SplashViewModel.swift b/Shared/ViewModels/SplashViewModel.swift index 34d80867..7b9d76ce 100644 --- a/Shared/ViewModels/SplashViewModel.swift +++ b/Shared/ViewModels/SplashViewModel.swift @@ -10,7 +10,10 @@ import Foundation import Combine import Nuke + +#if !os(tvOS) import WidgetKit +#endif final class SplashViewModel: ViewModel { @@ -24,7 +27,9 @@ final class SplashViewModel: ViewModel { ImageCache.shared.costLimit = 125 * 1024 * 1024 // 125MB memory DataLoader.sharedUrlCache.diskCapacity = 1000 * 1024 * 1024 // 1000MB disk + #if !os(tvOS) WidgetCenter.shared.reloadAllTimelines() + #endif let defaults = UserDefaults.standard if defaults.integer(forKey: "InNetworkBandwidth") == 0 {