From 4dae6bc00e7b4d65e3cf1c7a2b6c91e5fd779953 Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Fri, 28 May 2021 12:20:49 +0900 Subject: [PATCH] add LibraryListViewModel --- JellyfinPlayer.xcodeproj/project.pbxproj | 4 + JellyfinPlayer/ContentView.swift | 233 ++++++++++-------- .../ViewModels/LibraryListViewModel.swift | 38 +++ JellyfinPlayer/LibraryListView.swift | 40 +-- 4 files changed, 178 insertions(+), 137 deletions(-) create mode 100644 JellyfinPlayer/Domains/Library/ViewModels/LibraryListViewModel.swift diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index fa3fc3cb..7441143f 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -45,6 +45,7 @@ 6213388E265F777C00A81A2A /* LibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6213388D265F777C00A81A2A /* LibraryViewModel.swift */; }; 62133890265F83A900A81A2A /* LibraryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6213388F265F83A900A81A2A /* LibraryListView.swift */; }; 621338932660107500A81A2A /* String++.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* String++.swift */; }; + 62133895266096EF00A81A2A /* LibraryListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62133894266096EF00A81A2A /* LibraryListViewModel.swift */; }; 6273DD43265F4195009C1D0B /* Moya in Frameworks */ = {isa = PBXBuildFile; productRef = 6273DD42265F4195009C1D0B /* Moya */; }; 6273DD45265F4195009C1D0B /* CombineMoya in Frameworks */ = {isa = PBXBuildFile; productRef = 6273DD44265F4195009C1D0B /* CombineMoya */; }; 6273DD48265F41B3009C1D0B /* JellyfinAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6273DD47265F41B3009C1D0B /* JellyfinAPI.swift */; }; @@ -112,6 +113,7 @@ 6213388D265F777C00A81A2A /* LibraryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryViewModel.swift; sourceTree = ""; }; 6213388F265F83A900A81A2A /* LibraryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListView.swift; sourceTree = ""; }; 621338922660107500A81A2A /* String++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String++.swift"; sourceTree = ""; }; + 62133894266096EF00A81A2A /* LibraryListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListViewModel.swift; sourceTree = ""; }; 6273DD47265F41B3009C1D0B /* JellyfinAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinAPI.swift; sourceTree = ""; }; 6273DD4D265F47B2009C1D0B /* LibrarySearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchViewModel.swift; sourceTree = ""; }; AE8C3153265D60BF008AA076 /* SettingsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsModel.swift; sourceTree = ""; }; @@ -224,6 +226,7 @@ isa = PBXGroup; children = ( 6213388D265F777C00A81A2A /* LibraryViewModel.swift */, + 62133894266096EF00A81A2A /* LibraryListViewModel.swift */, ); path = ViewModels; sourceTree = ""; @@ -417,6 +420,7 @@ 53E4E649263F725B00F67C6B /* MultiSelector.swift in Sources */, 53E4E647263F6CF100F67C6B /* LibraryFilterView.swift in Sources */, 6213388E265F777C00A81A2A /* LibraryViewModel.swift in Sources */, + 62133895266096EF00A81A2A /* LibraryListViewModel.swift in Sources */, 6273DD48265F41B3009C1D0B /* JellyfinAPI.swift in Sources */, 53892777263CBB000035E14B /* JellyApiTypings.swift in Sources */, 5377CBF7263B596A003A4E83 /* ContentView.swift in Sources */, diff --git a/JellyfinPlayer/ContentView.swift b/JellyfinPlayer/ContentView.swift index 25a8b59b..dd05a9c2 100644 --- a/JellyfinPlayer/ContentView.swift +++ b/JellyfinPlayer/ContentView.swift @@ -8,78 +8,98 @@ import SwiftUI import KeychainSwift -import SwiftyRequest -import SwiftyJSON -import Sentry import SDWebImageSwiftUI +import Sentry +import SwiftyJSON +import SwiftyRequest struct ContentView: View { - @Environment(\.managedObjectContext) private var viewContext - @EnvironmentObject var orientationInfo: OrientationInfo - @StateObject private var globalData = GlobalData() - @EnvironmentObject var jsi: justSignedIn + @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; + @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; + orientationInfo.orientation = .portrait } else { - orientationInfo.orientation = .landscape; + orientationInfo.orientation = .landscape } - - if(_viewDidLoad.wrappedValue) { + + if _viewDidLoad.wrappedValue { return } - - _viewDidLoad.wrappedValue = true; + + _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.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; + if servers.isEmpty { + _isLoading.wrappedValue = false + _needsToSelectServer.wrappedValue = true } else { - _isLoading.wrappedValue = true; - let savedUser = savedUsers[0]; + _isLoading.wrappedValue = true + let savedUser = savedUsers[0] - let keychain = KeychainSwift(); - if(keychain.get("AccessToken_\(savedUser.user_id ?? "")") != nil) { + 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; + + let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String var header = "MediaBrowser " header.append("Client=\"SwiftFin\",") header.append("Device=\"\(UIDevice.current.name.removeRegexMatches(pattern: "[^\\w\\s]"))\",") @@ -87,48 +107,50 @@ struct ContentView: View { header.append("Version=\"\(appVersion ?? "0.0.1")\",") header.append("Token=\"\(globalData.authToken)\"") globalData.authHeader = header - + 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 + + request.responseData { (result: Result, RestError>) in switch result { - case .success( let resp): + case let .success(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") + + 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 + + request2.responseData { (result2: Result, RestError>) in switch result2 { - case .success( let resp): + case let .success(resp): do { let json2 = try JSON(data: resp.body) - for (_,item2):(String, JSON) in json2["Items"] { + 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") { + + 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) + !array2.contains(element) } - + _libraries.wrappedValue.forEach { library in - if(_library_names.wrappedValue[library] == nil) { + if _library_names.wrappedValue[library] == nil { _libraries.wrappedValue.removeAll { ele in - if(library == ele) { + if library == ele { return true } else { return false @@ -136,39 +158,32 @@ struct ContentView: View { } } } - + dump(_libraries.wrappedValue) dump(_librariesShowRecentlyAdded.wrappedValue) dump(_library_names.wrappedValue) - } catch { - - } - break - case .failure(let error): + } catch {} + case let .failure(error): SentrySDK.capture(error: error) - break } - let defaults = UserDefaults.standard; - if(defaults.integer(forKey: "InNetworkBandwidth") == 0) { - defaults.setValue(40000000, forKey: "InNetworkBandwidth") + 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(40000000, forKey: "OutOfNetworkBandwidth") + if defaults.integer(forKey: "OutOfNetworkBandwidth") == 0 { + defaults.setValue(40_000_000, forKey: "OutOfNetworkBandwidth") } - _isLoading.wrappedValue = false; + _isLoading.wrappedValue = false } - } catch { - - } - break - case .failure( let error): - if(error.response?.status.code == 401) { - _isLoading.wrappedValue = false; - _isSignInErrored.wrappedValue = true; + } catch {} + case let .failure(error): + if error.response?.status.code == 401 { + _isLoading.wrappedValue = false + _isSignInErrored.wrappedValue = true } else { SentrySDK.capture(error: error) - _isLoading.wrappedValue = false; - _isNetworkErrored.wrappedValue = true; + _isLoading.wrappedValue = false + _isNetworkErrored.wrappedValue = true } } } @@ -176,34 +191,37 @@ struct ContentView: View { } var body: some View { - if(needsToSelectServer) { - NavigationView() { + if needsToSelectServer { + NavigationView { ConnectToServerView(isActive: $needsToSelectServer) } .navigationViewStyle(StackNavigationViewStyle()) .environmentObject(globalData) - } else if(isSignInErrored) { - NavigationView() { - ConnectToServerView(skip_server: true, skip_server_prefill: globalData.server, reauth_deviceId: globalData.user?.device_uuid ?? "", isActive: $isSignInErrored) + } else if isSignInErrored { + NavigationView { + ConnectToServerView(skip_server: true, skip_server_prefill: globalData.server, + reauth_deviceId: globalData.user?.device_uuid ?? "", isActive: $isSignInErrored) } .navigationViewStyle(StackNavigationViewStyle()) .environmentObject(globalData) } else { - if(!jsi.did) { + if !jsi.did { LoadingView(isShowing: $isLoading) { TabView(selection: $tabSelection) { - NavigationView() { + NavigationView { VStack(alignment: .leading) { - ScrollView() { + 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)) + 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(viewModel: .init(filter: Filter(parentID: library_id)), title: library_names[library_id] ?? "")) { + NavigationLink(destination: LibraryView(viewModel: .init(filter: Filter(parentID: library_id)), + title: library_names[library_id] ?? "")) { Text("See All").font(.subheadline).fontWeight(.bold) } }.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)) @@ -216,28 +234,31 @@ struct ContentView: View { .toolbar { ToolbarItemGroup(placement: .navigationBarTrailing) { Button { - showSettingsPopover = true; + showSettingsPopover = true } label: { Image(systemName: "gear") } } - }.fullScreenCover( isPresented: $showSettingsPopover) { SettingsView(viewModel: SettingsViewModel(), close: $showSettingsPopover) } + } + .fullScreenCover(isPresented: $showSettingsPopover) { + SettingsView(viewModel: SettingsViewModel(), close: $showSettingsPopover) + } } } .navigationViewStyle(StackNavigationViewStyle()) - .tabItem({ + .tabItem { Text("Home") Image(systemName: "house") - }) + } .tag("Home") NavigationView { - LibraryListView(libraryNames: library_names, libraryIDs: libraries) + LibraryListView(viewModel: .init(libraryNames: library_names, libraryIDs: libraries)) } .navigationViewStyle(StackNavigationViewStyle()) - .tabItem({ + .tabItem { Text("All Media") Image(systemName: "folder") - }) + } .tag("All Media") } } @@ -248,13 +269,13 @@ struct ContentView: View { } } else { Text("Signing in...") - .onAppear(perform: { - DispatchQueue.main.async { [self] in - _viewDidLoad.wrappedValue = false - usleep(500000); - self.jsi.did = false; - } - }) + .onAppear(perform: { + DispatchQueue.main.async { [self] in + _viewDidLoad.wrappedValue = false + usleep(500_000) + self.jsi.did = false + } + }) } } } diff --git a/JellyfinPlayer/Domains/Library/ViewModels/LibraryListViewModel.swift b/JellyfinPlayer/Domains/Library/ViewModels/LibraryListViewModel.swift new file mode 100644 index 00000000..2103abe9 --- /dev/null +++ b/JellyfinPlayer/Domains/Library/ViewModels/LibraryListViewModel.swift @@ -0,0 +1,38 @@ +// +// LibraryListViewModel.swift +// JellyfinPlayer +// +// Created by PangMo5 on 2021/05/28. +// + +import Combine +import CombineMoya +import Foundation +import Moya +import SwiftyJSON + +final class LibraryListViewModel: ObservableObject { + fileprivate var provider = + MoyaProvider(plugins: [NetworkLoggerPlugin()]) + + @Published + var libraryIDs = [String]() + @Published + var libraryNames = [String: String]() + + fileprivate var cancellables = Set() + + init(libraryNames: [String: String], libraryIDs: [String]) { + self.libraryIDs = libraryIDs + self.libraryNames = libraryNames + refresh() + } + + func refresh() { + libraryIDs.append("favorites") + libraryNames["favorites"] = "Favorites" + + libraryIDs.append("genres") + libraryNames["genres"] = "Genres - WIP" + } +} diff --git a/JellyfinPlayer/LibraryListView.swift b/JellyfinPlayer/LibraryListView.swift index 1b2ba3c8..8f2073e4 100644 --- a/JellyfinPlayer/LibraryListView.swift +++ b/JellyfinPlayer/LibraryListView.swift @@ -13,48 +13,26 @@ struct LibraryListView: View { private var viewContext @EnvironmentObject var globalData: GlobalData - @State - private var libraryIDs: [String] = [] - @State - private var libraryNames: [String: String] = [:] - @State - private var viewDidLoad: Bool = false - @State - private var closeSearch: Bool = false - - init(libraryNames: [String: String], libraryIDs: [String]) { - self._libraryNames = State(initialValue: libraryNames) - self._libraryIDs = State(initialValue: libraryIDs) - } - - func listOnAppear() { - if viewDidLoad == false { - viewDidLoad = true - libraryIDs.append("favorites") - libraryNames["favorites"] = "Favorites" - - libraryIDs.append("genres") - libraryNames["genres"] = "Genres - WIP" - } - } + @ObservedObject + var viewModel: LibraryListViewModel var body: some View { - List(libraryIDs, id: \.self) { id in + List(viewModel.libraryIDs, id: \.self) { id in switch id { case "favorites": NavigationLink(destination: LibraryView(viewModel: .init(filter: Filter(filterTypes: [.isFavorite])), - title: libraryNames[id] ?? "")) { - Text(libraryNames[id] ?? "").foregroundColor(Color.primary) + title: viewModel.libraryNames[id] ?? "")) { + Text(viewModel.libraryNames[id] ?? "").foregroundColor(Color.primary) } case "genres": - Text(libraryNames[id] ?? "").foregroundColor(Color.primary) + Text(viewModel.libraryNames[id] ?? "").foregroundColor(Color.primary) default: - NavigationLink(destination: LibraryView(viewModel: .init(filter: Filter(parentID: id)), title: libraryNames[id] ?? "")) { - Text(libraryNames[id] ?? "").foregroundColor(Color.primary) + NavigationLink(destination: LibraryView(viewModel: .init(filter: Filter(parentID: id)), + title: viewModel.libraryNames[id] ?? "")) { + Text(viewModel.libraryNames[id] ?? "").foregroundColor(Color.primary) } } } - .onAppear(perform: listOnAppear) .navigationTitle("All Media") .navigationBarItems(trailing: NavigationLink(destination: LibrarySearchView(viewModel: .init(filter: .init()))) {