From 93ef16a46d3202999363514527a2461a739f6863 Mon Sep 17 00:00:00 2001 From: Aiden Vigue Date: Tue, 8 Jun 2021 17:04:33 -0700 Subject: [PATCH] more to mvc & gen. client --- JellyfinPlayer.xcodeproj/project.pbxproj | 26 ++- .../xcschemes/JellyfinPlayer.xcscheme | 12 +- JellyfinPlayer/ContentView.swift | 4 +- JellyfinPlayer/ItemView.swift | 41 ++-- JellyfinPlayer/JellyfinPlayer.entitlements | 2 + JellyfinPlayer/LatestMediaView.swift | 19 +- JellyfinPlayer/LibraryListView.swift | 3 +- JellyfinPlayer/LibraryView.swift | 49 +++-- JellyfinPlayer/SeasonItemView.swift | 206 +++--------------- JellyfinPlayer/SeriesItemView.swift | 124 +++-------- Shared/Typings/Typings.swift | 14 ++ 11 files changed, 174 insertions(+), 326 deletions(-) diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index feff0197..d4cc7145 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -50,6 +50,8 @@ 53A089D0264DA9DA00D57806 /* MovieItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53A089CF264DA9DA00D57806 /* MovieItemView.swift */; }; 53A431BD266B0FF20016769F /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = 53A431BC266B0FF20016769F /* JellyfinAPI */; }; 53A431BF266B0FFE0016769F /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = 53A431BE266B0FFE0016769F /* JellyfinAPI */; }; + 53AD124D267029D60094A276 /* SeriesItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53987CA526572F0700E7EA70 /* SeriesItemView.swift */; }; + 53AD124E26702B8A0094A276 /* SeasonItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53987CA326572C1300E7EA70 /* SeasonItemView.swift */; }; 53C4404E266C75C70049424C /* HandleAPIRequestCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53C4404D266C75C70049424C /* HandleAPIRequestCompletion.swift */; }; 53C4404F266C75C70049424C /* HandleAPIRequestCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53C4404D266C75C70049424C /* HandleAPIRequestCompletion.swift */; }; 53D5E3DD264B47EE00BADDC8 /* MobileVLCKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 53D5E3DC264B47EE00BADDC8 /* MobileVLCKit.xcframework */; }; @@ -119,7 +121,7 @@ 535BAE9E2649E569005FA86D /* ItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemView.swift; sourceTree = ""; }; 535BAEA4264A151C005FA86D /* VideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayer.swift; sourceTree = ""; }; 5364F454266CA0DC0026ECBA /* APIExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIExtensions.swift; sourceTree = ""; }; - 5377CBF1263B596A003A4E83 /* JellyfinPlayer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = JellyfinPlayer.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 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 = ""; }; @@ -136,6 +138,7 @@ 53987CA72657424A00E7EA70 /* EpisodeItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeItemView.swift; sourceTree = ""; }; 539B2DA4263BA5B8007FF1A4 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 53A089CF264DA9DA00D57806 /* MovieItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieItemView.swift; sourceTree = ""; }; + 53AD124C2670278D0094A276 /* JellyfinPlayer.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = JellyfinPlayer.entitlements; sourceTree = ""; }; 53C4404D266C75C70049424C /* HandleAPIRequestCompletion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandleAPIRequestCompletion.swift; sourceTree = ""; }; 53D5E3DA264B460200BADDC8 /* Cartfile */ = {isa = PBXFileReference; lastKnownFileType = text; path = Cartfile; sourceTree = ""; }; 53D5E3DC264B47EE00BADDC8 /* MobileVLCKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = MobileVLCKit.xcframework; path = Carthage/Build/MobileVLCKit.xcframework; sourceTree = ""; }; @@ -236,7 +239,7 @@ 5377CBF2263B596A003A4E83 /* Products */ = { isa = PBXGroup; children = ( - 5377CBF1263B596A003A4E83 /* JellyfinPlayer.app */, + 5377CBF1263B596A003A4E83 /* JellyfinPlayer iOS.app */, 535870602669D21600D05A09 /* JellyfinPlayer tvOS.app */, ); name = Products; @@ -245,6 +248,7 @@ 5377CBF3263B596A003A4E83 /* JellyfinPlayer */ = { isa = PBXGroup; children = ( + 53AD124C2670278D0094A276 /* JellyfinPlayer.entitlements */, 5377CBF8263B596B003A4E83 /* Assets.xcassets */, 5338F74D263B61370014BF09 /* ConnectToServerView.swift */, 5377CBF6263B596A003A4E83 /* ContentView.swift */, @@ -342,9 +346,9 @@ productReference = 535870602669D21600D05A09 /* JellyfinPlayer tvOS.app */; productType = "com.apple.product-type.application"; }; - 5377CBF0263B596A003A4E83 /* JellyfinPlayer */ = { + 5377CBF0263B596A003A4E83 /* JellyfinPlayer iOS */ = { isa = PBXNativeTarget; - buildConfigurationList = 5377CC1B263B596B003A4E83 /* Build configuration list for PBXNativeTarget "JellyfinPlayer" */; + buildConfigurationList = 5377CC1B263B596B003A4E83 /* Build configuration list for PBXNativeTarget "JellyfinPlayer iOS" */; buildPhases = ( 5377CBED263B596A003A4E83 /* Sources */, 5377CBEE263B596A003A4E83 /* Frameworks */, @@ -356,7 +360,7 @@ ); dependencies = ( ); - name = JellyfinPlayer; + name = "JellyfinPlayer iOS"; packageProductDependencies = ( 5338F756263B7E2E0014BF09 /* KeychainSwift */, 53352570265EA0A0006CCA86 /* Introspect */, @@ -364,7 +368,7 @@ 53A431BC266B0FF20016769F /* JellyfinAPI */, ); productName = JellyfinPlayer; - productReference = 5377CBF1263B596A003A4E83 /* JellyfinPlayer.app */; + productReference = 5377CBF1263B596A003A4E83 /* JellyfinPlayer iOS.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -406,7 +410,7 @@ projectDirPath = ""; projectRoot = ""; targets = ( - 5377CBF0263B596A003A4E83 /* JellyfinPlayer */, + 5377CBF0263B596A003A4E83 /* JellyfinPlayer iOS */, 5358705F2669D21600D05A09 /* JellyfinPlayer tvOS */, ); }; @@ -467,6 +471,7 @@ 53FF7F2A263CF3F500585C35 /* LatestMediaView.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 */, 53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */, @@ -485,6 +490,7 @@ 5389277C263CC3DB0035E14B /* BlurHashDecode.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 */, @@ -513,7 +519,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = me.vigue.swiftfin.tv; + PRODUCT_BUNDLE_IDENTIFIER = me.vigue.jellyfin; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; SWIFT_VERSION = 5.0; @@ -541,7 +547,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = me.vigue.swiftfin.tv; + PRODUCT_BUNDLE_IDENTIFIER = me.vigue.jellyfin; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; SWIFT_VERSION = 5.0; @@ -749,7 +755,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 5377CC1B263B596B003A4E83 /* Build configuration list for PBXNativeTarget "JellyfinPlayer" */ = { + 5377CC1B263B596B003A4E83 /* Build configuration list for PBXNativeTarget "JellyfinPlayer iOS" */ = { isa = XCConfigurationList; buildConfigurations = ( 5377CC1C263B596B003A4E83 /* Debug */, diff --git a/JellyfinPlayer.xcodeproj/xcshareddata/xcschemes/JellyfinPlayer.xcscheme b/JellyfinPlayer.xcodeproj/xcshareddata/xcschemes/JellyfinPlayer.xcscheme index 2cceb211..496cbad2 100644 --- a/JellyfinPlayer.xcodeproj/xcshareddata/xcschemes/JellyfinPlayer.xcscheme +++ b/JellyfinPlayer.xcodeproj/xcshareddata/xcschemes/JellyfinPlayer.xcscheme @@ -15,8 +15,8 @@ @@ -45,8 +45,8 @@ @@ -62,8 +62,8 @@ diff --git a/JellyfinPlayer/ContentView.swift b/JellyfinPlayer/ContentView.swift index 1d622232..f852dacd 100644 --- a/JellyfinPlayer/ContentView.swift +++ b/JellyfinPlayer/ContentView.swift @@ -35,6 +35,8 @@ struct ContentView: View { @State private var showSettingsPopover: Bool = false @State private var viewDidLoad: Bool = false @State private var loadState: Int = 2 + + private var recentFilterSet: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.descending], sortBy: ["DateCreated"]) func startup() { if(viewDidLoad == true) { @@ -158,7 +160,7 @@ struct ContentView: View { Spacer() NavigationLink(destination: LazyView { LibraryView(usingParentID: library_id, - title: library_names[library_id] ?? "") + title: library_names[library_id] ?? "", usingFilters: recentFilterSet) }) { Text("See All").font(.subheadline).fontWeight(.bold) } diff --git a/JellyfinPlayer/ItemView.swift b/JellyfinPlayer/ItemView.swift index 81dd16d4..612e33b0 100644 --- a/JellyfinPlayer/ItemView.swift +++ b/JellyfinPlayer/ItemView.swift @@ -25,21 +25,31 @@ struct ItemView: View { @StateObject private var videoPlayerItem: VideoPlayerItem = VideoPlayerItem() @State private var videoIsLoading: Bool = false; //This variable is only changed by the underlying VLC view. @State private var isLoading: Bool = false; + @State private var viewDidLoad: Bool = false; init(item: BaseItemDto) { self.item = item } func onAppear() { - isLoading = true; - UserLibraryAPI.getItem(userId: globalData.user.user_id!, itemId: item.id!) - .sink(receiveCompletion: { completion in - HandleAPIRequestCompletion(globalData: globalData, completion: completion) - }, receiveValue: { response in - isLoading = false - fullItem = response - }) - .store(in: &globalData.pendingAPIRequests) + if(viewDidLoad) { + return + } + + if(item.type == "Movie" || item.type == "Episode") { + isLoading = true; + UserLibraryAPI.getItem(userId: globalData.user.user_id!, itemId: item.id!) + .sink(receiveCompletion: { completion in + HandleAPIRequestCompletion(globalData: globalData, completion: completion) + }, receiveValue: { response in + isLoading = false + viewDidLoad = true + fullItem = response + }) + .store(in: &globalData.pendingAPIRequests) + } else { + viewDidLoad = true + } } var body: some View { @@ -60,15 +70,14 @@ struct ItemView: View { ProgressView() } else { VStack { - if(fullItem.type == "Movie") { + if(item.type == "Movie") { MovieItemView(item: fullItem) - } else if(fullItem.type == "Season") { + } else if(item.type == "Season") { EmptyView() - //SeasonItemView(item: fullItem) - } else if(fullItem.type == "Series") { - EmptyView() - //SeriesItemView(item: fullItem) - } else if(fullItem.type == "Episode") { + SeasonItemView(item: item) + } else if(item.type == "Series") { + SeriesItemView(item: item) + } else if(item.type == "Episode") { EmptyView() //EpisodeItemView(item: fullItem) } else { diff --git a/JellyfinPlayer/JellyfinPlayer.entitlements b/JellyfinPlayer/JellyfinPlayer.entitlements index ee95ab7e..6d616546 100644 --- a/JellyfinPlayer/JellyfinPlayer.entitlements +++ b/JellyfinPlayer/JellyfinPlayer.entitlements @@ -6,5 +6,7 @@ com.apple.security.network.client + com.apple.developer.coremedia.hls.low-latency + diff --git a/JellyfinPlayer/LatestMediaView.swift b/JellyfinPlayer/LatestMediaView.swift index 2ac3c387..24416b7b 100644 --- a/JellyfinPlayer/LatestMediaView.swift +++ b/JellyfinPlayer/LatestMediaView.swift @@ -40,11 +40,11 @@ struct LatestMediaView: View { LazyHStack() { Spacer().frame(width:16) ForEach(items, id: \.id) { item in - if(item.type == "Series" || item.type == "Movie") { + if(item.type == "Series" || item.type == "Movie" || item.type == "Episode") { NavigationLink(destination: ItemView(item: item)) { VStack(alignment: .leading) { Spacer().frame(height:10) - LazyImage(source: item.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 100)) + LazyImage(source: (item.type != "Episode" ? item.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 100) : item.getSeriesPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 100))) .placeholderAndFailure { Image(uiImage: UIImage(blurHash: item.getPrimaryImageBlurHash(), size: CGSize(width: 16, height: 20))!) .resizable() @@ -54,11 +54,24 @@ struct LatestMediaView: View { .frame(width: 100, height: 150) .cornerRadius(10) Spacer().frame(height:5) - Text(item.seasonName ?? item.name ?? "") + Text(item.seriesName ?? item.name ?? "") .font(.caption) .fontWeight(.semibold) .foregroundColor(.primary) .lineLimit(1) + if(item.type == "Episode") { + Text("S\(String(item.parentIndexNumber ?? 0)):E\(String(item.indexNumber ?? 0))") + .font(.caption) + .fontWeight(.semibold) + .foregroundColor(.secondary) + .lineLimit(1) + } else { + Text(String(item.productionYear ?? 0)) + .font(.caption) + .fontWeight(.semibold) + .foregroundColor(.secondary) + .lineLimit(1) + } }.frame(width: 100) Spacer().frame(width: 15) } diff --git a/JellyfinPlayer/LibraryListView.swift b/JellyfinPlayer/LibraryListView.swift index a5b14623..532185c0 100644 --- a/JellyfinPlayer/LibraryListView.swift +++ b/JellyfinPlayer/LibraryListView.swift @@ -14,6 +14,7 @@ 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: [.descending], sortBy: ["SortName"]) init(libraries: [String: String]) { self.libraries = libraries @@ -34,7 +35,7 @@ struct LibraryListView: View { switch key { case "favorites": NavigationLink(destination: LazyView { - LibraryView(usingParentID: "", title: library_names[key] ?? "", filters: [.isFavorite]) + LibraryView(usingParentID: "", title: library_names[key] ?? "", usingFilters: withFavorites) }) { Text(library_names[key] ?? "") } diff --git a/JellyfinPlayer/LibraryView.swift b/JellyfinPlayer/LibraryView.swift index 4e7b0c66..682528bf 100644 --- a/JellyfinPlayer/LibraryView.swift +++ b/JellyfinPlayer/LibraryView.swift @@ -18,7 +18,7 @@ struct LibraryView: View { var usingParentID: String = "" var title: String = "" - var filters: [ItemFilter] = [] + var filters: LibraryFilters = LibraryFilters() var personId: String = "" var genre: String = "" var studio: String = "" @@ -31,10 +31,10 @@ struct LibraryView: View { self.title = title } - init(usingParentID: String, title: String, filters: [ItemFilter]) { + init(usingParentID: String, title: String, usingFilters: LibraryFilters) { self.usingParentID = usingParentID self.title = title - self.filters = filters + self.filters = usingFilters } init(withPerson: BaseItemPerson) { @@ -60,7 +60,7 @@ struct LibraryView: View { isLoading = true items = [] - ItemsAPI.getItemsByUserId(userId: globalData.user.user_id!, startIndex: currentPage * 100, limit: 100, recursive: true, searchTerm: nil, sortOrder: [.ascending], parentId: (usingParentID != "" ? usingParentID : nil), fields: [.parentId,.primaryImageAspectRatio,.basicSyncInfo], includeItemTypes: ["Movie","Series"], filters: filters, enableUserData: true, personIds: (personId == "" ? nil : [personId]), studioIds: (studio == "" ? nil : [studio]), genreIds: (genre == "" ? nil : [genre]), enableImages: true) + ItemsAPI.getItemsByUserId(userId: globalData.user.user_id!, startIndex: currentPage * 100, limit: 100, recursive: true, searchTerm: nil, sortOrder: filters.sortOrder, parentId: (usingParentID != "" ? usingParentID : nil), fields: [.parentId,.primaryImageAspectRatio,.basicSyncInfo], 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) .sink(receiveCompletion: { completion in HandleAPIRequestCompletion(globalData: globalData, completion: completion) isLoading = false @@ -121,29 +121,36 @@ struct LibraryView: View { }.onChange(of: orientationInfo.orientation) { _ in recalcTracks() } - HStack() { - Spacer() + if(totalPages > 1) { HStack() { - Button { - currentPage = currentPage - 1 - onAppear() - } label: { - Image(systemName: "chevron.left") - }.disabled(currentPage == 0) - Text("\(String(currentPage+1)) of \(String(totalPages))") - Button { - currentPage = currentPage + 1 - onAppear() - } label: { - Image(systemName: "chevron.right") - }.disabled(currentPage > totalPages - 1) + Spacer() + HStack() { + Button { + currentPage = currentPage - 1 + onAppear() + } label: { + Image(systemName: "chevron.left") + .font(.system(size: 25)) + }.disabled(currentPage == 0) + Text("Page \(String(currentPage+1)) of \(String(totalPages))") + .font(.headline) + .fontWeight(.semibold) + Button { + currentPage = currentPage + 1 + onAppear() + } label: { + Image(systemName: "chevron.right") + .font(.system(size: 25)) + }.disabled(currentPage > totalPages - 1) + } + Spacer() } - Spacer() } + Spacer().frame(height: 16) } } } else { - Text("No results found :(") + Text("No results.") } } } diff --git a/JellyfinPlayer/SeasonItemView.swift b/JellyfinPlayer/SeasonItemView.swift index 796a7c18..01be3be3 100644 --- a/JellyfinPlayer/SeasonItemView.swift +++ b/JellyfinPlayer/SeasonItemView.swift @@ -12,168 +12,27 @@ import JellyfinAPI struct SeasonItemView: View { @EnvironmentObject var globalData: GlobalData @EnvironmentObject var orientationInfo: OrientationInfo - @State private var isLoading: Bool = true - var item: ResumeItem + var item: BaseItemDto @State private var episodes: [BaseItemDto] = [] - - init(item: ResumeItem) { - self.item = item - } - + @State private var isLoading: Bool = true + @State private var viewDidLoad: Bool = false + func onAppear() { - let url = "/Users/\(globalData.user.user_id ?? "")/Items/\(item.Id)" - - let request = RestRequest(method: .get, url: (globalData.server.baseURI ?? "") + url) - request.headerParameters["X-Emby-Authorization"] = globalData.authHeader - request.contentType = "application/json" - request.acceptType = "application/json" - - request.responseData { (result: Result, RestError>) in - switch result { - case let .success(response): - let body = response.body - do { - let json = try JSON(data: body) - let responseItem = DetailItem() - responseItem.ProductionYear = json["ProductionYear"].int ?? 0 - responseItem.Poster = json["ImageTags"]["Primary"].string ?? "" - responseItem.PosterBlurHash = json["ImageBlurHashes"]["Primary"][responseItem.Poster].string ?? "" - responseItem.Backdrop = json["BackdropImageTags"][0].string ?? "" - responseItem.BackdropBlurHash = json["ImageBlurHashes"]["Backdrop"][responseItem.Backdrop].string ?? "" - responseItem.Name = json["Name"].string ?? "" - responseItem.Type = json["Type"].string ?? "" - responseItem.IndexNumber = json["IndexNumber"].int ?? nil - responseItem.SeriesId = json["ParentId"].string ?? nil - responseItem.Id = item.Id - responseItem.Overview = json["Overview"].string ?? "" - responseItem.Tagline = json["Taglines"][0].string ?? "" - responseItem.SeriesName = json["SeriesName"].string ?? nil - responseItem.ParentId = json["ParentId"].string ?? "" - // People - responseItem.Directors = [] - responseItem.Studios = [] - responseItem.Writers = [] - responseItem.Cast = [] - responseItem.Genres = [] - - for (_, person): (String, JSON) in json["People"] { - if person["Type"].stringValue == "Director" { - responseItem.Directors.append(person["Name"].string ?? "") - } else if person["Type"].stringValue == "Writer" { - responseItem.Writers.append(person["Name"].string ?? "") - } else if person["Type"].stringValue == "Actor" { - let cast = CastMember() - cast.Name = person["Name"].string ?? "" - cast.Id = person["Id"].string ?? "" - let imageTag = person["PrimaryImageTag"].string ?? "" - cast.ImageBlurHash = person["ImageBlurHashes"]["Primary"][imageTag].string ?? "" - cast.Role = person["Role"].string ?? "" - cast - .Image = - URL(string: "\(globalData.server.baseURI ?? "")/Items/\(cast.Id)/Images/Primary?maxWidth=2000&quality=90&tag=\(imageTag)")! - responseItem.Cast.append(cast) - } - } - - _fullItem.wrappedValue = responseItem - - let url2 = - "/Shows/\(fullItem.SeriesId ?? "")/Episodes?SeasonId=\(item.Id)&UserId=\(globalData.user.user_id ?? "")&Fields=ItemCounts%2CPrimaryImageAspectRatio%2CBasicSyncInfo%2CCanDelete%2CMediaSourceCount%2COverview" - let request2 = RestRequest(method: .get, url: (globalData.server.baseURI ?? "") + url2) - request2.headerParameters["X-Emby-Authorization"] = globalData.authHeader - request2.contentType = "application/json" - request2.acceptType = "application/json" - - request2.responseData { (result: Result, RestError>) in - switch result { - case let .success(response): - let body = response.body - do { - let jsonroot = try JSON(data: body) - for (_, json): (String, JSON) in jsonroot["Items"] { - let episode = DetailItem() - episode.ProductionYear = json["ProductionYear"].int ?? 0 - episode.Poster = json["ImageTags"]["Primary"].string ?? "" - episode.PosterBlurHash = json["ImageBlurHashes"]["Primary"][fullItem.Poster].string ?? "" - episode.Backdrop = json["BackdropImageTags"][0].string ?? "" - episode.BackdropBlurHash = json["ImageBlurHashes"]["Backdrop"][fullItem.Backdrop].string ?? "" - episode.Name = json["Name"].string ?? "" - episode.Type = "Episode" - episode.IndexNumber = json["IndexNumber"].int ?? nil - episode.Id = json["Id"].string ?? "" - episode.ParentIndexNumber = json["ParentIndexNumber"].int ?? nil - episode.SeasonId = json["SeasonId"].string ?? nil - episode.SeriesId = json["SeriesId"].string ?? nil - episode.Overview = json["Overview"].string ?? "" - episode.SeriesName = json["SeriesName"].string ?? nil - episode.Progress = Double(json["UserData"]["PlaybackPositionTicks"].int ?? 0) - episode.OfficialRating = json["OfficialRating"].string ?? "PG-13" - episode.Watched = json["UserData"]["Played"].bool ?? false - episode.ParentId = episode.SeasonId ?? "" - episode.CommunityRating = String(json["CommunityRating"].float ?? 0.0) - - var rI = ResumeItem() - rI.Name = episode.Name - rI.Id = episode.Id - rI.IndexNumber = episode.IndexNumber - rI.ParentIndexNumber = episode.ParentIndexNumber - rI.Image = episode.Poster - rI.ImageType = "Primary" - rI.BlurHash = episode.PosterBlurHash - rI.Type = "Episode" - rI.SeasonId = episode.SeasonId - rI.SeriesId = episode.SeriesId - rI.SeriesName = episode.SeriesName - rI.ProductionYear = episode.ProductionYear - episode.ResumeItem = rI - - let seconds: Int = ((json["RunTimeTicks"].int ?? 0) / 10_000_000) - episode.RuntimeTicks = json["RunTimeTicks"].int ?? 0 - let hours = (seconds / 3600) - let minutes = ((seconds - (hours * 3600)) / 60) - if hours != 0 { - episode.Runtime = "\(hours):\(String(minutes).leftPad(toWidth: 2, withString: "0"))" - } else { - episode.Runtime = "\(String(minutes).leftPad(toWidth: 2, withString: "0"))m" - } - - if episode.Progress != 0 { - let remainingSecs = (Double(json["RunTimeTicks"].int ?? 0) - episode.Progress) / 10_000_000 - let proghours = Int(remainingSecs / 3600) - let progminutes = Int((Int(remainingSecs) - (proghours * 3600)) / 60) - if proghours != 0 { - episode.ProgressStr = "\(proghours):\(String(progminutes).leftPad(toWidth: 2, withString: "0"))" - } else { - episode.ProgressStr = "\(String(progminutes).leftPad(toWidth: 2, withString: "0"))m" - } - } - - _episodes.wrappedValue.append(episode) - } - _isLoading.wrappedValue = false - _hasAppearedOnce.wrappedValue = true - } catch {} - case let .failure(error): - debugPrint(error) - } - } - } catch {} - case let .failure(error): - debugPrint(error) - } + if(viewDidLoad) { + return } - } - - @Environment(\.verticalSizeClass) - var verticalSizeClass: UserInterfaceSizeClass? - @Environment(\.horizontalSizeClass) - var horizontalSizeClass: UserInterfaceSizeClass? - - var isPortrait: Bool { - let result = verticalSizeClass == .regular && horizontalSizeClass == .compact - return result + + TvShowsAPI.getEpisodes(seriesId: item.seriesId!, fields: [.primaryImageAspectRatio], seasonId: item.id!) + .sink(receiveCompletion: { completion in + HandleAPIRequestCompletion(globalData: globalData, completion: completion) + isLoading = false + }, receiveValue: { response in + viewDidLoad = true + episodes = response.items ?? [] + }) + .store(in: &globalData.pendingAPIRequests) } @ViewBuilder @@ -181,11 +40,9 @@ struct SeasonItemView: View { if isLoading { EmptyView() } else { - LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Backdrop?maxWidth=550&quality=90&tag=\(item.SeasonImage ?? "")")) + LazyImage(source: item.getBackdropImage(baseURL: globalData.server.baseURI!, maxWidth: 1500)) .placeholderAndFailure { - Image(uiImage: UIImage(blurHash: item - .SeasonImageBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item - .SeasonImageBlurHash ?? "", + Image(uiImage: UIImage(blurHash: item.getBackdropImageBlurHash(), size: CGSize(width: 32, height: 32))!) .resizable() } @@ -197,11 +54,9 @@ struct SeasonItemView: View { var portraitHeaderOverlayView: some View { HStack(alignment: .bottom, spacing: 12) { - LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")) + LazyImage(source: item.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 120)) .placeholderAndFailure { - Image(uiImage: UIImage(blurHash: fullItem - .PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem - .PosterBlurHash, + Image(uiImage: UIImage(blurHash: item.getPrimaryImageBlurHash(), size: CGSize(width: 32, height: 32))!) .resizable() .frame(width: 120, height: 180) @@ -211,13 +66,13 @@ struct SeasonItemView: View { .frame(width: 120, height: 180) .cornerRadius(10) VStack(alignment: .leading) { - Text(fullItem.Name).font(.headline) + Text(item.name ?? "").font(.headline) .fontWeight(.semibold) .foregroundColor(.primary) .fixedSize(horizontal: false, vertical: true) .offset(y: -4) - if fullItem.ProductionYear != 0 { - Text(String(fullItem.ProductionYear)).font(.subheadline) + if item.productionYear != 0 { + Text(String(item.productionYear!)).font(.subheadline) .fontWeight(.medium) .foregroundColor(.secondary) .lineLimit(1) @@ -235,23 +90,20 @@ struct SeasonItemView: View { overlayAlignment: .bottomLeading, headerHeight: UIScreen.main.bounds.width * 0.5625) { LazyVStack(alignment: .leading) { - if fullItem.Tagline != "" { - Text(fullItem.Tagline).font(.body).italic().padding(.top, 7) + if !(item.taglines ?? []).isEmpty { + Text(item.taglines!.first!).font(.body).italic().padding(.top, 7) .fixedSize(horizontal: false, vertical: true).padding(.leading, 16) .padding(.trailing, 16) } - Text(fullItem.Overview).font(.footnote).padding(.top, 3) + Text(item.overview ?? "").font(.footnote).padding(.top, 3) .fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16) .padding(.trailing, 16) ForEach(episodes, id: \.Id) { episode in - NavigationLink(destination: ItemView(item: episode.ResumeItem ?? ResumeItem())) { + NavigationLink(destination: ItemView(item: episode)) { HStack { - LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(episode.Id)/Images/Primary?maxWidth=300&quality=90&tag=\(episode.Poster)")) + LazyImage(source: episode.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 150)) .placeholderAndFailure { - Image(uiImage: UIImage(blurHash: episode - .PosterBlurHash == "" ? - "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem - .PosterBlurHash, + Image(uiImage: UIImage(blurHash: episode.getPrimaryImageBlurHash())) size: CGSize(width: 32, height: 32))!) .resizable() .frame(width: 150, height: 90) diff --git a/JellyfinPlayer/SeriesItemView.swift b/JellyfinPlayer/SeriesItemView.swift index 6bce33be..3cc1e90e 100644 --- a/JellyfinPlayer/SeriesItemView.swift +++ b/JellyfinPlayer/SeriesItemView.swift @@ -6,87 +6,46 @@ */ import SwiftUI -import SwiftyRequest -import SwiftyJSON import NukeUI +import JellyfinAPI struct SeriesItemView: View { - @EnvironmentObject var globalData: GlobalData + @EnvironmentObject private var globalData: GlobalData + @EnvironmentObject private var orientationInfo: OrientationInfo + + var item: BaseItemDto; + + @State private var seasons: [BaseItemDto] = []; @State private var isLoading: Bool = true; - var item: ResumeItem; - @State private var items: [ResumeItem] = []; - @State private var hasAppearedOnce: Bool = false; + @State private var viewDidLoad: Bool = false; + func onAppear() { recalcTracks() - if(hasAppearedOnce) { + if(viewDidLoad) { return; } - _isLoading.wrappedValue = true; - let url = "/Shows/\(item.Id )/Seasons?userId=\(globalData.user.user_id ?? "")&Fields=ItemCount" + isLoading = true - let request = RestRequest(method: .get, url: (globalData.server.baseURI ?? "") + url) - 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 response): - let body = response.body - do { - let json = try JSON(data: body) - for (_,item):(String, JSON) in json["Items"] { - // Do something you want - var itemObj = ResumeItem() - itemObj.Type = "Season" - itemObj.Id = item["Id"].string ?? "" - itemObj.ProductionYear = item["ProductionYear"].int ?? 0 - itemObj.ItemBadge = item["UserData"]["UnplayedItemCount"].int ?? 0 - itemObj.Image = item["ImageTags"]["Primary"].string ?? "" - - if(itemObj.Image == "") { - itemObj.Image = item["ParentBackdropImageTags"][0].string ?? "" - } - - itemObj.ImageType = "Primary" - itemObj.SeasonImage = item["ParentBackdropImageTags"][0].string ?? "" - itemObj.SeasonImageType = "Backdrop" - itemObj.SeasonImageBlurHash = item["ImageBlurHashes"]["Backdrop"][itemObj.SeasonImage ?? ""].string ?? "" - itemObj.BlurHash = item["ImageBlurHashes"]["Primary"][itemObj.Image].string ?? "" - itemObj.SeriesName = item["SeriesName"].string ?? "" - itemObj.Name = item["Name"].string ?? "" - _items.wrappedValue.append(itemObj) - } - } catch { - - } - break - case .failure(let error): - debugPrint(error) - break - } - _isLoading.wrappedValue = false; - _hasAppearedOnce.wrappedValue = true; - } + TvShowsAPI.getSeasons(seriesId: item.id ?? "") + .sink(receiveCompletion: { completion in + HandleAPIRequestCompletion(globalData: globalData, completion: completion) + }, receiveValue: { response in + isLoading = false + viewDidLoad = true + seasons = response.items ?? [] + }) + .store(in: &globalData.pendingAPIRequests) } - - @Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass? - @Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass? - var isPortrait: Bool { - let result = verticalSizeClass == .regular && horizontalSizeClass == .compact - return result - } - + //MARK: Grid tracks func recalcTracks() { let trkCnt: Int = Int(floor(UIScreen.main.bounds.size.width / 125)); - _tracks.wrappedValue = [] + tracks = [] for _ in (0..