// // ContentView.swift // JellyfinPlayer // // Created by Aiden Vigue on 4/29/21. // import SwiftUI import KeychainSwift import SwiftyRequest import SwiftyJSON import Introspect import Sentry import SDWebImageSwiftUI class GlobalData: ObservableObject { @Published var user: SignedInUser? @Published var authToken: String = "" @Published var server: Server? @Published var authHeader: String = "" @Published var isInNetwork: Bool = true; } extension View { func withHostingWindow(_ callback: @escaping (UIWindow?) -> Void) -> some View { self.background(HostingWindowFinder(callback: callback)) } } struct HostingWindowFinder: UIViewRepresentable { var callback: (UIWindow?) -> () func makeUIView(context: Context) -> UIView { let view = UIView() DispatchQueue.main.async { [weak view] in callback(view?.window) } return view } func updateUIView(_ uiView: UIView, context: Context) { } } struct PrefersHomeIndicatorAutoHiddenPreferenceKey: PreferenceKey { typealias Value = Bool static var defaultValue: Value = false static func reduce(value: inout Value, nextValue: () -> Value) { value = nextValue() || value } } struct ViewPreferenceKey: PreferenceKey { typealias Value = UIUserInterfaceStyle static var defaultValue: UIUserInterfaceStyle = .unspecified static func reduce(value: inout UIUserInterfaceStyle, nextValue: () -> UIUserInterfaceStyle) { value = nextValue() } } struct SupportedOrientationsPreferenceKey: PreferenceKey { typealias Value = UIInterfaceOrientationMask static var defaultValue: UIInterfaceOrientationMask = .allButUpsideDown static func reduce(value: inout UIInterfaceOrientationMask, nextValue: () -> UIInterfaceOrientationMask) { // use the most restrictive set from the stack value.formIntersection(nextValue()) } } extension View { /// Navigate to a new view. /// - Parameters: /// - view: View to navigate to. /// - binding: Only navigates when this condition is `true`. func navigate(to view: NewView, when binding: Binding) -> some View { NavigationView { ZStack { self .navigationBarTitle("") .navigationBarHidden(true) NavigationLink( destination: view .navigationBarTitle("") .navigationBarHidden(true), isActive: binding ) { EmptyView() } } } } } class PreferenceUIHostingController: UIHostingController { init(wrappedView: V) { let box = Box() super.init(rootView: AnyView(wrappedView .onPreferenceChange(PrefersHomeIndicatorAutoHiddenPreferenceKey.self) { box.value?._prefersHomeIndicatorAutoHidden = $0 }.onPreferenceChange(SupportedOrientationsPreferenceKey.self) { box.value?._orientations = $0 }.onPreferenceChange(ViewPreferenceKey.self) { box.value?._viewPreference = $0 } )) box.value = self } @objc required dynamic init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) super.modalPresentationStyle = .fullScreen } private class Box { weak var value: PreferenceUIHostingController? init() {} } // MARK: Prefers Home Indicator Auto Hidden public var _prefersHomeIndicatorAutoHidden = false { didSet { setNeedsUpdateOfHomeIndicatorAutoHidden() } } override var prefersHomeIndicatorAutoHidden: Bool { _prefersHomeIndicatorAutoHidden } // MARK: Lock orientation public var _orientations: UIInterfaceOrientationMask = .allButUpsideDown { didSet { UIViewController.attemptRotationToDeviceOrientation(); if(_orientations == .landscape) { let value = UIInterfaceOrientation.landscapeRight.rawValue; UIDevice.current.setValue(value, forKey: "orientation") } } }; override var supportedInterfaceOrientations: UIInterfaceOrientationMask { _orientations } public var _viewPreference: UIUserInterfaceStyle = .unspecified { didSet { overrideUserInterfaceStyle = _viewPreference } }; } extension View { // Controls the application's preferred home indicator auto-hiding when this view is shown. func prefersHomeIndicatorAutoHidden(_ value: Bool) -> some View { preference(key: PrefersHomeIndicatorAutoHiddenPreferenceKey.self, value: value) } func supportedOrientations(_ supportedOrientations: UIInterfaceOrientationMask) -> some View { // When rendered, export the requested orientations upward to Root preference(key: SupportedOrientationsPreferenceKey.self, value: supportedOrientations) } func overrideViewPreference(_ viewPreference: UIUserInterfaceStyle) -> some View { // When rendered, export the requested orientations upward to Root preference(key: ViewPreferenceKey.self, value: viewPreference) } } struct ContentView: View { @Environment(\.managedObjectContext) private var viewContext @EnvironmentObject var orientationInfo: OrientationInfo @StateObject private var globalData = GlobalData() @EnvironmentObject var jsi: justSignedIn @FetchRequest(entity: Server.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Server.name, ascending: true)]) private var servers: FetchedResults @FetchRequest(entity: SignedInUser.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \SignedInUser.username, ascending: true)]) private var savedUsers: FetchedResults @State private var needsToSelectServer = false; @State private var isSignInErrored = false; @State private var isNetworkErrored = 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; func startup() { let size = UIScreen.main.bounds.size if size.width < size.height { orientationInfo.orientation = .portrait; } else { orientationInfo.orientation = .landscape; } if(_viewDidLoad.wrappedValue) { return } _viewDidLoad.wrappedValue = true; SentrySDK.start { options in options.dsn = "https://75ac77d6af4d406eb989f3d8ef0f119f@o513670.ingest.sentry.io/5778242" options.debug = false // Enabled debug when first installing is always helpful options.tracesSampleRate = 1.0 options.releaseName = "ios-" + (Bundle.main.infoDictionary?["CFBundleVersion"] as! String); options.enableOutOfMemoryTracking = true } let cache = SDImageCache(namespace: "tiny") cache.config.maxMemoryCost = 125 * 1024 * 1024 // 125MB memory cache.config.maxDiskSize = 1000 * 1024 * 1024 // 1000MB disk SDImageCachesManager.shared.addCache(cache) SDWebImageManager.defaultImageCache = SDImageCachesManager.shared _libraries.wrappedValue = [] _library_names.wrappedValue = [:] _librariesShowRecentlyAdded.wrappedValue = [] if(servers.isEmpty) { _isLoading.wrappedValue = false; _needsToSelectServer.wrappedValue = true; } else { _isLoading.wrappedValue = true; let savedUser = savedUsers[0]; let keychain = KeychainSwift(); if(keychain.get("AccessToken_\(savedUser.user_id ?? "")") != nil) { _globalData.wrappedValue.authToken = keychain.get("AccessToken_\(savedUser.user_id ?? "")") ?? "" _globalData.wrappedValue.server = servers[0] _globalData.wrappedValue.user = savedUser } let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String; globalData.authHeader = "MediaBrowser Client=\"SwiftFin\", Device=\"\(UIDevice.current.name)\", DeviceId=\"\(globalData.user?.device_uuid ?? "")\", Version=\"\(appVersion ?? "0.0.1")\", Token=\"\(globalData.authToken)\""; let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + "/Users/Me") request.headerParameters["X-Emby-Authorization"] = globalData.authHeader request.contentType = "application/json" request.acceptType = "application/json" request.responseData() { (result: Result, RestError>) in switch result { case .success( let resp): do { let json = try JSON(data: resp.body) let array2 = json["Configuration"]["LatestItemsExcludes"].arrayObject as? [String] ?? [] let request2 = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + "/Users/\(globalData.user?.user_id ?? "")/Views") request2.headerParameters["X-Emby-Authorization"] = globalData.authHeader request2.contentType = "application/json" request2.acceptType = "application/json" request2.responseData() { (result2: Result, RestError>) in switch result2 { case .success( let resp): do { let json2 = try JSON(data: resp.body) for (_,item2):(String, JSON) in json2["Items"] { _library_names.wrappedValue[item2["Id"].string ?? ""] = item2["Name"].string ?? "" } for (_,item2):(String, JSON) in json2["Items"] { if(item2["CollectionType"].string == "tvshows" || item2["CollectionType"].string == "movies") { _libraries.wrappedValue.append(item2["Id"].string ?? "") _librariesShowRecentlyAdded.wrappedValue.append(item2["Id"].string ?? "") } } _librariesShowRecentlyAdded.wrappedValue = _libraries.wrappedValue.filter { element in return !array2.contains(element) } _libraries.wrappedValue.forEach { library in if(_library_names.wrappedValue[library] == nil) { _libraries.wrappedValue.removeAll { ele in if(library == ele) { return true } else { return false } } } } } catch { } break case .failure(let error): SentrySDK.capture(error: error) break } let defaults = UserDefaults.standard; if(defaults.integer(forKey: "InNetworkBandwidth") == 0) { defaults.setValue(40000000, forKey: "InNetworkBandwidth") } if(defaults.integer(forKey: "OutOfNetworkBandwidth") == 0) { defaults.setValue(40000000, forKey: "OutOfNetworkBandwidth") } _isLoading.wrappedValue = false; } } catch { } break case .failure( let error): if(error.response?.status.code == 401) { _isLoading.wrappedValue = false; _isSignInErrored.wrappedValue = true; } else { SentrySDK.capture(error: error) _isLoading.wrappedValue = false; _isNetworkErrored.wrappedValue = true; } } } } } var body: some View { if(needsToSelectServer) { NavigationView() { ConnectToServerView(isActive: $needsToSelectServer) }.environmentObject(globalData).navigationViewStyle(StackNavigationViewStyle()) } else if(isSignInErrored) { NavigationView() { ConnectToServerView(skip_server: true, skip_server_prefill: globalData.server, reauth_deviceId: globalData.user?.device_uuid ?? "", isActive: $isSignInErrored) }.environmentObject(globalData).navigationViewStyle(StackNavigationViewStyle()) } else { if(!jsi.did) { LoadingView(isShowing: $isLoading) { TabView(selection: $tabSelection) { NavigationView() { VStack { if(!needsToSelectServer && !isSignInErrored) { VStack(alignment: .leading) { ScrollView() { Spacer().frame(height: orientationInfo.orientation == .portrait ? 0 : 15) ContinueWatchingView() NextUpView().padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0)) 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: LibraryView(prefill: library_id, names: [library_id: library_names[library_id] ?? ""], libraries: [library_id], filter: "&SortBy=DateCreated&SortOrder=Descending")) { Text("See All").font(.subheadline).fontWeight(.bold) } }.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)) LatestMediaView(library: library_id) }.padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0)) } Spacer().frame(height: 7) } } } } .navigationTitle("Home") .toolbar { ToolbarItemGroup(placement: .navigationBarTrailing) { Button { showSettingsPopover = true; } label: { Image(systemName: "gear") } } }.fullScreenCover( isPresented: $showSettingsPopover) { SettingsView(viewModel: SettingsViewModel(), close: $showSettingsPopover).environmentObject(globalData) } } .navigationViewStyle(StackNavigationViewStyle()) .tabItem({ Text("Home") Image(systemName: "house") }) .tag("Home") NavigationView() { LibraryView(prefill: "", names: library_names, libraries: libraries) .navigationTitle("Library") } .tabItem({ Text("All Media") Image(systemName: "folder") }) .tag("All Media") } }.environmentObject(globalData) .onAppear(perform: startup) .navigationViewStyle(StackNavigationViewStyle()) .alert(isPresented: $isNetworkErrored) { Alert(title: Text("Network Error"), message: Text("Couldn't connect to Jellyfin"), dismissButton: .default(Text("Ok"))) }.introspectTabBarController { (UITabBarController) in UITabBarController.tabBar.isHidden = false } } else { Text("Signing in...") .onAppear(perform: { DispatchQueue.main.async { [self] in _viewDidLoad.wrappedValue = false usleep(500000); self.jsi.did = false; } }) } } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) } }